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:
parent
ea4be9f9ad
commit
0f57b69ead
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
playbackStateStream,
|
_fullPlaybackStateSubject.addStream(Rx.combineLatest3<AudioPlaybackState,
|
||||||
bufferingStream,
|
bool, IcyMetadata, FullAudioPlaybackState>(
|
||||||
(state, buffering) => FullAudioPlaybackState(state, buffering)));
|
playbackStateStream,
|
||||||
|
bufferingStream,
|
||||||
|
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});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue