From 0f57b69eadc4f7acace80b3f93bac5658f693827 Mon Sep 17 00:00:00 2001 From: Andrea Jonus Date: Tue, 21 Apr 2020 22:57:32 +1000 Subject: [PATCH] Add Icy metadata support on Android (#78) * Add Icy "info" metadata to Android playback events * Add IcyMetadata stream to the Flutter module * Update ExoPlayer to 2.11.4 * Use Player.EventListener.onTracksChanged to retrieve IcyHeaders * Iterate over each track of trackGroups to look for IcyHeaders --- android/build.gradle | 8 +- .../com/ryanheise/just_audio/AudioPlayer.java | 75 +++++++++++++++++-- lib/just_audio.dart | 75 +++++++++++++++++-- 3 files changed, 143 insertions(+), 15 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index dedf85b..2d0e0fe 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -40,8 +40,8 @@ android { } dependencies { - implementation 'com.google.android.exoplayer:exoplayer-core:2.11.1' - implementation 'com.google.android.exoplayer:exoplayer-dash:2.11.1' - implementation 'com.google.android.exoplayer:exoplayer-hls:2.11.1' - implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.11.1' + implementation 'com.google.android.exoplayer:exoplayer-core:2.11.4' + implementation 'com.google.android.exoplayer:exoplayer-dash:2.11.4' + implementation 'com.google.android.exoplayer:exoplayer-hls:2.11.4' + implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.11.4' } diff --git a/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java b/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java index 084f6b8..fa689a4 100644 --- a/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java +++ b/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java @@ -7,11 +7,18 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataOutput; +import com.google.android.exoplayer2.metadata.icy.IcyHeaders; +import com.google.android.exoplayer2.metadata.icy.IcyInfo; import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; @@ -28,17 +35,14 @@ import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.PluginRegistry.Registrar; import java.io.IOException; -import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; - +import java.util.Collections; import android.content.Context; import android.net.Uri; import java.util.List; -public class AudioPlayer implements MethodCallHandler, Player.EventListener { +public class AudioPlayer implements MethodCallHandler, Player.EventListener, MetadataOutput { static final String TAG = "AudioPlayer"; private final Registrar registrar; @@ -64,6 +68,8 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener { private boolean buffering; private boolean justConnected; private MediaSource mediaSource; + private IcyInfo icyInfo; + private IcyHeaders icyHeaders; private final SimpleExoPlayer player; private final Handler handler = new Handler(); @@ -109,6 +115,7 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener { state = PlaybackState.none; player = new SimpleExoPlayer.Builder(context).build(); + player.addMetadataOutput(this); player.addListener(this); } @@ -117,6 +124,38 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener { handler.post(bufferWatcher); } + @Override + public void onMetadata(Metadata metadata) { + for (int i = 0; i < metadata.length(); i++) { + final Metadata.Entry entry = metadata.get(i); + if (entry instanceof IcyInfo) { + icyInfo = (IcyInfo) entry; + broadcastPlaybackEvent(); + } + } + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + for (int i = 0; i < trackGroups.length; i++) { + TrackGroup trackGroup = trackGroups.get(i); + + for (int j = 0; j < trackGroup.length; j++) { + Metadata metadata = trackGroup.getFormat(j).metadata; + + if (metadata != null) { + for (int k = 0; k < metadata.length(); k++) { + final Metadata.Entry entry = metadata.get(k); + if (entry instanceof IcyHeaders) { + icyHeaders = (IcyHeaders) entry; + broadcastPlaybackEvent(); + } + } + } + } + } + } + @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { switch (playbackState) { @@ -261,6 +300,32 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener { event.add(updatePosition = getCurrentPosition()); event.add(updateTime = System.currentTimeMillis()); event.add(Math.max(updatePosition, bufferedPosition)); + + final ArrayList icyData = new ArrayList<>(); + final ArrayList info; + final ArrayList headers; + if (icyInfo != null) { + info = new ArrayList<>(); + info.add(icyInfo.title); + info.add(icyInfo.url); + } else { + info = new ArrayList<>(Collections.nCopies(2, null)); + } + if (icyHeaders != null) { + headers = new ArrayList<>(); + headers.add(icyHeaders.bitrate); + headers.add(icyHeaders.genre); + headers.add(icyHeaders.name); + headers.add(icyHeaders.metadataInterval); + headers.add(icyHeaders.url); + headers.add(icyHeaders.isPublic); + } else { + headers = new ArrayList<>(Collections.nCopies(6, null)); + } + icyData.add(info); + icyData.add(headers); + event.add(icyData); + if (eventSink != null) { eventSink.success(event); } diff --git a/lib/just_audio.dart b/lib/just_audio.dart index 0bf701d..cce161b 100644 --- a/lib/just_audio.dart +++ b/lib/just_audio.dart @@ -69,6 +69,15 @@ class AudioPlayer { bufferedPosition: Duration.zero, speed: 1.0, duration: Duration.zero, + icyMetadata: IcyMetadata( + info: IcyInfo(title: null, url: null), + headers: IcyHeaders( + bitrate: null, + genre: null, + name: null, + metadataInterval: null, + url: null, + isPublic: null)), ); Stream _eventChannelStream; @@ -83,6 +92,8 @@ class AudioPlayer { final _bufferedPositionSubject = BehaviorSubject(); + final _icyMetadataSubject = BehaviorSubject(); + final _fullPlaybackStateSubject = BehaviorSubject(); double _volume = 1.0; @@ -108,6 +119,15 @@ class AudioPlayer { bufferedPosition: Duration(milliseconds: data[4]), speed: _speed, duration: _duration, + icyMetadata: IcyMetadata( + info: IcyInfo(title: data[5][0][0], url: data[5][0][1]), + headers: IcyHeaders( + bitrate: data[5][1][0], + genre: data[5][1][1], + name: data[5][1][2], + metadataInterval: data[5][1][3], + url: data[5][1][4], + isPublic: data[5][1][5])), )); _eventChannelStreamSubscription = _eventChannelStream.listen(_playbackEventSubject.add); @@ -117,11 +137,15 @@ class AudioPlayer { playbackEventStream.map((state) => state.buffering).distinct()); _bufferedPositionSubject.addStream( playbackEventStream.map((state) => state.bufferedPosition).distinct()); - _fullPlaybackStateSubject.addStream( - Rx.combineLatest2( - playbackStateStream, - bufferingStream, - (state, buffering) => FullAudioPlaybackState(state, buffering))); + _icyMetadataSubject.addStream( + playbackEventStream.map((state) => state.icyMetadata).distinct()); + _fullPlaybackStateSubject.addStream(Rx.combineLatest3( + playbackStateStream, + bufferingStream, + icyMetadataStream, + (state, buffering, icyMetadata) => + FullAudioPlaybackState(state, buffering, icyMetadata))); } /// The duration of any media set via [setUrl], [setFilePath] or [setAsset], @@ -148,9 +172,13 @@ class AudioPlayer { /// Whether the player is buffering. bool get buffering => _audioPlaybackEvent.buffering; + IcyMetadata get icyMetadata => _audioPlaybackEvent.icyMetadata; + /// A stream of buffering state changes. Stream get bufferingStream => _bufferingSubject.stream; + Stream get icyMetadataStream => _icyMetadataSubject.stream; + /// A stream of buffered positions. Stream get bufferedPositionStream => _bufferedPositionSubject.stream; @@ -394,6 +422,8 @@ class AudioPlaybackEvent { /// The media duration. final Duration duration; + final IcyMetadata icyMetadata; + AudioPlaybackEvent({ @required this.state, @required this.buffering, @@ -402,6 +432,7 @@ class AudioPlaybackEvent { @required this.bufferedPosition, @required this.speed, @required this.duration, + @required this.icyMetadata, }); AudioPlaybackEvent copyWith({ @@ -462,6 +493,38 @@ enum AudioPlaybackState { class FullAudioPlaybackState { final AudioPlaybackState state; final bool buffering; + final IcyMetadata icyMetadata; - FullAudioPlaybackState(this.state, this.buffering); + FullAudioPlaybackState(this.state, this.buffering, this.icyMetadata); +} + +class IcyInfo { + final String title; + final String url; + + IcyInfo({@required this.title, @required this.url}); +} + +class IcyHeaders { + final int bitrate; + final String genre; + final String name; + final int metadataInterval; + final String url; + final bool isPublic; + + IcyHeaders( + {@required this.bitrate, + @required this.genre, + @required this.name, + @required this.metadataInterval, + @required this.url, + @required this.isPublic}); +} + +class IcyMetadata { + final IcyInfo info; + final IcyHeaders headers; + + IcyMetadata({@required this.info, @required this.headers}); }