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
This commit is contained in:
Andrea Jonus 2020-04-21 22:57:32 +10:00 committed by GitHub
parent ea4be9f9ad
commit 0f57b69ead
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 143 additions and 15 deletions

View File

@ -40,8 +40,8 @@ android {
} }
dependencies { dependencies {
implementation 'com.google.android.exoplayer:exoplayer-core:2.11.1' implementation 'com.google.android.exoplayer:exoplayer-core:2.11.4'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.11.1' implementation 'com.google.android.exoplayer:exoplayer-dash:2.11.4'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.11.1' implementation 'com.google.android.exoplayer:exoplayer-hls:2.11.4'
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.11.1' implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.11.4'
} }

View File

@ -7,11 +7,18 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.SimpleExoPlayer; 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.ClippingMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource; 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.dash.DashMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource; 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.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; 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 io.flutter.plugin.common.PluginRegistry.Registrar;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Collections;
import java.util.LinkedList;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import java.util.List; import java.util.List;
public class AudioPlayer implements MethodCallHandler, Player.EventListener { public class AudioPlayer implements MethodCallHandler, Player.EventListener, MetadataOutput {
static final String TAG = "AudioPlayer"; static final String TAG = "AudioPlayer";
private final Registrar registrar; private final Registrar registrar;
@ -64,6 +68,8 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener {
private boolean buffering; private boolean buffering;
private boolean justConnected; private boolean justConnected;
private MediaSource mediaSource; private MediaSource mediaSource;
private IcyInfo icyInfo;
private IcyHeaders icyHeaders;
private final SimpleExoPlayer player; private final SimpleExoPlayer player;
private final Handler handler = new Handler(); private final Handler handler = new Handler();
@ -109,6 +115,7 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener {
state = PlaybackState.none; state = PlaybackState.none;
player = new SimpleExoPlayer.Builder(context).build(); player = new SimpleExoPlayer.Builder(context).build();
player.addMetadataOutput(this);
player.addListener(this); player.addListener(this);
} }
@ -117,6 +124,38 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener {
handler.post(bufferWatcher); 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 @Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
switch (playbackState) { switch (playbackState) {
@ -261,6 +300,32 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener {
event.add(updatePosition = getCurrentPosition()); event.add(updatePosition = getCurrentPosition());
event.add(updateTime = System.currentTimeMillis()); event.add(updateTime = System.currentTimeMillis());
event.add(Math.max(updatePosition, bufferedPosition)); event.add(Math.max(updatePosition, bufferedPosition));
final ArrayList<Object> icyData = new ArrayList<>();
final ArrayList<String> info;
final ArrayList<Object> 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) { if (eventSink != null) {
eventSink.success(event); eventSink.success(event);
} }

View File

@ -69,6 +69,15 @@ class AudioPlayer {
bufferedPosition: Duration.zero, bufferedPosition: Duration.zero,
speed: 1.0, speed: 1.0,
duration: Duration.zero, 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<AudioPlaybackEvent> _eventChannelStream; Stream<AudioPlaybackEvent> _eventChannelStream;
@ -83,6 +92,8 @@ class AudioPlayer {
final _bufferedPositionSubject = BehaviorSubject<Duration>(); final _bufferedPositionSubject = BehaviorSubject<Duration>();
final _icyMetadataSubject = BehaviorSubject<IcyMetadata>();
final _fullPlaybackStateSubject = BehaviorSubject<FullAudioPlaybackState>(); final _fullPlaybackStateSubject = BehaviorSubject<FullAudioPlaybackState>();
double _volume = 1.0; double _volume = 1.0;
@ -108,6 +119,15 @@ class AudioPlayer {
bufferedPosition: Duration(milliseconds: data[4]), bufferedPosition: Duration(milliseconds: data[4]),
speed: _speed, speed: _speed,
duration: _duration, 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 = _eventChannelStreamSubscription =
_eventChannelStream.listen(_playbackEventSubject.add); _eventChannelStream.listen(_playbackEventSubject.add);
@ -117,11 +137,15 @@ class AudioPlayer {
playbackEventStream.map((state) => state.buffering).distinct()); playbackEventStream.map((state) => state.buffering).distinct());
_bufferedPositionSubject.addStream( _bufferedPositionSubject.addStream(
playbackEventStream.map((state) => state.bufferedPosition).distinct()); playbackEventStream.map((state) => state.bufferedPosition).distinct());
_fullPlaybackStateSubject.addStream( _icyMetadataSubject.addStream(
Rx.combineLatest2<AudioPlaybackState, bool, FullAudioPlaybackState>( playbackEventStream.map((state) => state.icyMetadata).distinct());
_fullPlaybackStateSubject.addStream(Rx.combineLatest3<AudioPlaybackState,
bool, IcyMetadata, FullAudioPlaybackState>(
playbackStateStream, playbackStateStream,
bufferingStream, bufferingStream,
(state, buffering) => FullAudioPlaybackState(state, buffering))); icyMetadataStream,
(state, buffering, icyMetadata) =>
FullAudioPlaybackState(state, buffering, icyMetadata)));
} }
/// The duration of any media set via [setUrl], [setFilePath] or [setAsset], /// The duration of any media set via [setUrl], [setFilePath] or [setAsset],
@ -148,9 +172,13 @@ class AudioPlayer {
/// Whether the player is buffering. /// Whether the player is buffering.
bool get buffering => _audioPlaybackEvent.buffering; bool get buffering => _audioPlaybackEvent.buffering;
IcyMetadata get icyMetadata => _audioPlaybackEvent.icyMetadata;
/// A stream of buffering state changes. /// A stream of buffering state changes.
Stream<bool> get bufferingStream => _bufferingSubject.stream; Stream<bool> get bufferingStream => _bufferingSubject.stream;
Stream<IcyMetadata> get icyMetadataStream => _icyMetadataSubject.stream;
/// A stream of buffered positions. /// A stream of buffered positions.
Stream<Duration> get bufferedPositionStream => Stream<Duration> get bufferedPositionStream =>
_bufferedPositionSubject.stream; _bufferedPositionSubject.stream;
@ -394,6 +422,8 @@ class AudioPlaybackEvent {
/// The media duration. /// The media duration.
final Duration duration; final Duration duration;
final IcyMetadata icyMetadata;
AudioPlaybackEvent({ AudioPlaybackEvent({
@required this.state, @required this.state,
@required this.buffering, @required this.buffering,
@ -402,6 +432,7 @@ class AudioPlaybackEvent {
@required this.bufferedPosition, @required this.bufferedPosition,
@required this.speed, @required this.speed,
@required this.duration, @required this.duration,
@required this.icyMetadata,
}); });
AudioPlaybackEvent copyWith({ AudioPlaybackEvent copyWith({
@ -462,6 +493,38 @@ enum AudioPlaybackState {
class FullAudioPlaybackState { class FullAudioPlaybackState {
final AudioPlaybackState state; final AudioPlaybackState state;
final bool buffering; 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});
} }