Separate buffering from playbackState. Improve Android buffering detection.
This commit is contained in:
parent
280f1a8208
commit
eee50d712e
|
@ -35,23 +35,9 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener {
|
|||
private final MethodChannel methodChannel;
|
||||
private final EventChannel eventChannel;
|
||||
private EventSink eventSink;
|
||||
private final Handler handler = new Handler();
|
||||
private final Runnable positionObserver = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (state != PlaybackState.playing && state != PlaybackState.buffering)
|
||||
return;
|
||||
|
||||
if (eventSink != null) {
|
||||
checkForDiscontinuity();
|
||||
}
|
||||
handler.postDelayed(this, 200);
|
||||
}
|
||||
};
|
||||
|
||||
private final String id;
|
||||
private volatile PlaybackState state;
|
||||
private PlaybackState stateBeforeSeek;
|
||||
private long updateTime;
|
||||
private long updatePosition;
|
||||
|
||||
|
@ -64,6 +50,7 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener {
|
|||
private Result prepareResult;
|
||||
private Result seekResult;
|
||||
private boolean seekProcessed;
|
||||
private boolean buffering;
|
||||
private MediaSource mediaSource;
|
||||
|
||||
private SimpleExoPlayer player;
|
||||
|
@ -106,15 +93,19 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener {
|
|||
completeSeek();
|
||||
}
|
||||
break;
|
||||
case Player.STATE_BUFFERING:
|
||||
// TODO: use this instead of checkForDiscontinuity.
|
||||
break;
|
||||
case Player.STATE_ENDED:
|
||||
if (state != PlaybackState.completed) {
|
||||
transition(PlaybackState.completed);
|
||||
}
|
||||
break;
|
||||
}
|
||||
final boolean buffering = playbackState == Player.STATE_BUFFERING;
|
||||
// don't notify buffering if (buffering && state == stopped)
|
||||
final boolean notifyBuffering = !buffering || state != PlaybackState.stopped;
|
||||
if (notifyBuffering && (buffering != this.buffering)) {
|
||||
this.buffering = buffering;
|
||||
broadcastPlaybackEvent();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -130,29 +121,10 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener {
|
|||
private void completeSeek() {
|
||||
seekProcessed = false;
|
||||
seekPos = null;
|
||||
transition(stateBeforeSeek);
|
||||
seekResult.success(null);
|
||||
stateBeforeSeek = null;
|
||||
seekResult = null;
|
||||
}
|
||||
|
||||
private void checkForDiscontinuity() {
|
||||
final long now = System.currentTimeMillis();
|
||||
final long position = getCurrentPosition();
|
||||
final long timeSinceLastUpdate = now - updateTime;
|
||||
final long expectedPosition = updatePosition + (long)(timeSinceLastUpdate * speed);
|
||||
final long drift = position - expectedPosition;
|
||||
// Update if we've drifted or just started observing
|
||||
if (updateTime == 0L) {
|
||||
broadcastPlaybackEvent();
|
||||
} else if (drift < -100) {
|
||||
System.out.println("time discontinuity detected: " + drift);
|
||||
transition(PlaybackState.buffering);
|
||||
} else if (state == PlaybackState.buffering) {
|
||||
transition(PlaybackState.playing);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(final MethodCall call, final Result result) {
|
||||
final List<?> args = (List<?>)call.arguments;
|
||||
|
@ -219,6 +191,7 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener {
|
|||
private void broadcastPlaybackEvent() {
|
||||
final ArrayList<Object> event = new ArrayList<Object>();
|
||||
event.add(state.ordinal());
|
||||
event.add(buffering);
|
||||
event.add(updatePosition = getCurrentPosition());
|
||||
event.add(updateTime = System.currentTimeMillis());
|
||||
eventSink.success(event);
|
||||
|
@ -237,9 +210,6 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener {
|
|||
private void transition(final PlaybackState newState) {
|
||||
final PlaybackState oldState = state;
|
||||
state = newState;
|
||||
if (oldState != PlaybackState.playing && newState == PlaybackState.playing) {
|
||||
startObservingPosition();
|
||||
}
|
||||
broadcastPlaybackEvent();
|
||||
}
|
||||
|
||||
|
@ -282,14 +252,9 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener {
|
|||
break;
|
||||
case stopped:
|
||||
case completed:
|
||||
case buffering:
|
||||
case paused:
|
||||
player.setPlayWhenReady(true);
|
||||
if (seekResult != null) {
|
||||
stateBeforeSeek = PlaybackState.playing;
|
||||
} else {
|
||||
transition(PlaybackState.playing);
|
||||
}
|
||||
player.setPlayWhenReady(true);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Cannot call play from connecting/none states (" + state + ")");
|
||||
|
@ -301,12 +266,8 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener {
|
|||
case paused:
|
||||
break;
|
||||
case playing:
|
||||
case buffering:
|
||||
player.setPlayWhenReady(false);
|
||||
transition(PlaybackState.paused);
|
||||
if (seekResult != null) {
|
||||
stateBeforeSeek = PlaybackState.paused;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Can call pause only from playing and buffering states (" + state + ")");
|
||||
|
@ -320,18 +281,17 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener {
|
|||
break;
|
||||
case connecting:
|
||||
abortExistingConnection();
|
||||
buffering = false;
|
||||
transition(PlaybackState.stopped);
|
||||
result.success(null);
|
||||
break;
|
||||
case buffering:
|
||||
abortSeek();
|
||||
// no break
|
||||
case completed:
|
||||
case playing:
|
||||
case paused:
|
||||
abortSeek();
|
||||
player.setPlayWhenReady(false);
|
||||
player.seekTo(0L);
|
||||
transition(PlaybackState.stopped);
|
||||
player.seekTo(0L);
|
||||
result.success(null);
|
||||
break;
|
||||
default:
|
||||
|
@ -358,11 +318,6 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener {
|
|||
seekPos = position;
|
||||
seekResult = result;
|
||||
seekProcessed = false;
|
||||
if (stateBeforeSeek == null) {
|
||||
stateBeforeSeek = state;
|
||||
}
|
||||
handler.removeCallbacks(positionObserver);
|
||||
transition(PlaybackState.buffering);
|
||||
player.seekTo(position);
|
||||
}
|
||||
|
||||
|
@ -376,7 +331,6 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener {
|
|||
seekResult.success(null);
|
||||
seekResult = null;
|
||||
seekPos = null;
|
||||
stateBeforeSeek = null;
|
||||
seekProcessed = false;
|
||||
}
|
||||
}
|
||||
|
@ -388,17 +342,11 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener {
|
|||
}
|
||||
}
|
||||
|
||||
private void startObservingPosition() {
|
||||
handler.removeCallbacks(positionObserver);
|
||||
handler.post(positionObserver);
|
||||
}
|
||||
|
||||
enum PlaybackState {
|
||||
none,
|
||||
stopped,
|
||||
paused,
|
||||
playing,
|
||||
buffering,
|
||||
connecting,
|
||||
completed
|
||||
}
|
||||
|
|
|
@ -43,27 +43,29 @@ class _MyAppState extends State<MyApp> {
|
|||
children: [
|
||||
Text("Science Friday"),
|
||||
Text("Science Friday and WNYC Studios"),
|
||||
StreamBuilder<AudioPlaybackState>(
|
||||
stream: _player.playbackStateStream,
|
||||
StreamBuilder<FullAudioPlaybackState>(
|
||||
stream: _player.fullPlaybackStateStream,
|
||||
builder: (context, snapshot) {
|
||||
final state = snapshot.data;
|
||||
final fullState = snapshot.data;
|
||||
final state = fullState?.state;
|
||||
final buffering = fullState?.buffering;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (state == AudioPlaybackState.playing)
|
||||
IconButton(
|
||||
icon: Icon(Icons.pause),
|
||||
iconSize: 64.0,
|
||||
onPressed: _player.pause,
|
||||
)
|
||||
else if (state == AudioPlaybackState.buffering ||
|
||||
state == AudioPlaybackState.connecting)
|
||||
if (state == AudioPlaybackState.connecting ||
|
||||
buffering == true)
|
||||
Container(
|
||||
margin: EdgeInsets.all(8.0),
|
||||
width: 64.0,
|
||||
height: 64.0,
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
else if (state == AudioPlaybackState.playing)
|
||||
IconButton(
|
||||
icon: Icon(Icons.pause),
|
||||
iconSize: 64.0,
|
||||
onPressed: _player.pause,
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
icon: Icon(Icons.play_arrow),
|
||||
|
|
|
@ -11,7 +11,6 @@ enum PlaybackState {
|
|||
stopped,
|
||||
paused,
|
||||
playing,
|
||||
buffering,
|
||||
connecting,
|
||||
completed
|
||||
};
|
||||
|
|
|
@ -10,11 +10,11 @@
|
|||
NSString* _playerId;
|
||||
AVPlayer* _player;
|
||||
enum PlaybackState _state;
|
||||
enum PlaybackState _stateBeforeSeek;
|
||||
long long _updateTime;
|
||||
int _updatePosition;
|
||||
int _seekPos;
|
||||
FlutterResult _connectionResult;
|
||||
BOOL _buffering;
|
||||
id _endObserver;
|
||||
id _timeObserver;
|
||||
}
|
||||
|
@ -32,9 +32,9 @@
|
|||
binaryMessenger:[registrar messenger]];
|
||||
[_eventChannel setStreamHandler:self];
|
||||
_state = none;
|
||||
_stateBeforeSeek = none;
|
||||
_player = nil;
|
||||
_seekPos = -1;
|
||||
_buffering = NO;
|
||||
_endObserver = 0;
|
||||
_timeObserver = 0;
|
||||
__weak __typeof__(self) weakSelf = self;
|
||||
|
@ -94,7 +94,7 @@
|
|||
|
||||
- (void)checkForDiscontinuity {
|
||||
if (!_eventSink) return;
|
||||
if (_state != playing && _state != buffering) return;
|
||||
if ((_state != playing) && !_buffering) return;
|
||||
long long now = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0);
|
||||
int position = [self getCurrentPosition];
|
||||
long long timeSinceLastUpdate = now - _updateTime;
|
||||
|
@ -105,9 +105,11 @@
|
|||
[self broadcastPlaybackEvent];
|
||||
} else if (drift < -100) {
|
||||
NSLog(@"time discontinuity detected: %lld", drift);
|
||||
[self setPlaybackState:buffering];
|
||||
} else if (_state == buffering) {
|
||||
[self setPlaybackState:playing];
|
||||
_buffering = YES;
|
||||
[self broadcastPlaybackEvent];
|
||||
} else if (_buffering) {
|
||||
_buffering = NO;
|
||||
[self broadcastPlaybackEvent];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,6 +120,8 @@
|
|||
_eventSink(@[
|
||||
// state
|
||||
@(_state),
|
||||
// buffering
|
||||
@(_buffering),
|
||||
// updatePosition
|
||||
@(_updatePosition),
|
||||
// updateTime
|
||||
|
@ -292,10 +296,10 @@
|
|||
}
|
||||
|
||||
- (void)seek:(int)position result:(FlutterResult)result {
|
||||
_stateBeforeSeek = _state;
|
||||
_seekPos = position;
|
||||
NSLog(@"seek. enter buffering");
|
||||
[self setPlaybackState:buffering];
|
||||
_buffering = YES;
|
||||
[self broadcastPlaybackEvent];
|
||||
[_player seekToTime:CMTimeMake(position, 1000)
|
||||
completionHandler:^(BOOL finished) {
|
||||
NSLog(@"seek completed");
|
||||
|
@ -305,8 +309,8 @@
|
|||
|
||||
- (void)onSeekCompletion:(FlutterResult)result {
|
||||
_seekPos = -1;
|
||||
[self setPlaybackState:_stateBeforeSeek];
|
||||
_stateBeforeSeek = none;
|
||||
_buffering = NO;
|
||||
[self broadcastPlaybackEvent];
|
||||
result(nil);
|
||||
}
|
||||
|
||||
|
|
|
@ -34,10 +34,7 @@ import 'package:rxdart/rxdart.dart';
|
|||
/// * [AudioPlaybackState.stopped]: eventually after [setUrl], [setFilePath],
|
||||
/// [setAsset] or [setClip] completes, and immediately after [stop].
|
||||
/// * [AudioPlaybackState.paused]: after [pause].
|
||||
/// * [AudioPlaybackState.playing]: after [play] and after sufficiently
|
||||
/// buffering during normal playback.
|
||||
/// * [AudioPlaybackState.buffering]: immediately after a seek request and
|
||||
/// during normal playback when the next buffer is not ready to be played.
|
||||
/// * [AudioPlaybackState.playing]: after [play].
|
||||
/// * [AudioPlaybackState.connecting]: immediately after [setUrl],
|
||||
/// [setFilePath] and [setAsset] while waiting for the media to load.
|
||||
/// * [AudioPlaybackState.completed]: immediately after playback reaches the
|
||||
|
@ -64,6 +61,7 @@ class AudioPlayer {
|
|||
// TODO: also broadcast this event on instantiation.
|
||||
AudioPlaybackEvent _audioPlaybackEvent = AudioPlaybackEvent(
|
||||
state: AudioPlaybackState.none,
|
||||
buffering: false,
|
||||
updatePosition: Duration.zero,
|
||||
updateTime: Duration.zero,
|
||||
speed: 1.0,
|
||||
|
@ -77,6 +75,10 @@ class AudioPlayer {
|
|||
|
||||
final _playbackStateSubject = BehaviorSubject<AudioPlaybackState>();
|
||||
|
||||
final _bufferingSubject = BehaviorSubject<bool>();
|
||||
|
||||
final _fullPlaybackStateSubject = BehaviorSubject<FullAudioPlaybackState>();
|
||||
|
||||
double _volume = 1.0;
|
||||
|
||||
double _speed = 1.0;
|
||||
|
@ -90,14 +92,22 @@ class AudioPlayer {
|
|||
.receiveBroadcastStream()
|
||||
.map((data) => _audioPlaybackEvent = AudioPlaybackEvent(
|
||||
state: AudioPlaybackState.values[data[0]],
|
||||
updatePosition: Duration(milliseconds: data[1]),
|
||||
updateTime: Duration(milliseconds: data[2]),
|
||||
buffering: data[1],
|
||||
updatePosition: Duration(milliseconds: data[2]),
|
||||
updateTime: Duration(milliseconds: data[3]),
|
||||
speed: _speed,
|
||||
));
|
||||
_eventChannelStreamSubscription =
|
||||
_eventChannelStream.listen(_playbackEventSubject.add);
|
||||
_playbackStateSubject
|
||||
.addStream(playbackEventStream.map((state) => state.state).distinct());
|
||||
_bufferingSubject.addStream(
|
||||
playbackEventStream.map((state) => state.buffering).distinct());
|
||||
_fullPlaybackStateSubject.addStream(
|
||||
Rx.combineLatest2<AudioPlaybackState, bool, FullAudioPlaybackState>(
|
||||
playbackStateStream,
|
||||
bufferingStream,
|
||||
(state, buffering) => FullAudioPlaybackState(state, buffering)));
|
||||
}
|
||||
|
||||
/// The duration of any media set via [setUrl], [setFilePath] or [setAsset],
|
||||
|
@ -121,6 +131,16 @@ class AudioPlayer {
|
|||
Stream<AudioPlaybackState> get playbackStateStream =>
|
||||
_playbackStateSubject.stream;
|
||||
|
||||
/// Whether the player is buffering.
|
||||
bool get buffering => _audioPlaybackEvent.buffering;
|
||||
|
||||
/// A stream of buffering state changes.
|
||||
Stream<bool> get bufferingStream => _bufferingSubject.stream;
|
||||
|
||||
/// A stream of [FullAudioPlaybackState]s.
|
||||
Stream<FullAudioPlaybackState> get fullPlaybackStateStream =>
|
||||
_fullPlaybackStateSubject.stream;
|
||||
|
||||
/// A stream periodically tracking the current position of this player.
|
||||
Stream<Duration> getPositionStream(
|
||||
[final Duration period = const Duration(milliseconds: 200)]) =>
|
||||
|
@ -209,10 +229,7 @@ class AudioPlayer {
|
|||
}
|
||||
|
||||
/// Pauses the currently playing media. It is legal to invoke this method
|
||||
/// only from the following states:
|
||||
///
|
||||
/// * [AudioPlaybackState.playing]
|
||||
/// * [AudioPlaybackState.buffering]
|
||||
/// only from the [AudioPlaybackState.playing] state.
|
||||
Future<void> pause() async {
|
||||
await _invokeMethod('pause');
|
||||
}
|
||||
|
@ -272,6 +289,9 @@ class AudioPlaybackEvent {
|
|||
/// The current playback state.
|
||||
final AudioPlaybackState state;
|
||||
|
||||
/// Whether the player is buffering.
|
||||
final bool buffering;
|
||||
|
||||
/// When the last time a position discontinuity happened, as measured in time
|
||||
/// since the epoch.
|
||||
final Duration updateTime;
|
||||
|
@ -284,6 +304,7 @@ class AudioPlaybackEvent {
|
|||
|
||||
AudioPlaybackEvent({
|
||||
@required this.state,
|
||||
@required this.buffering,
|
||||
@required this.updateTime,
|
||||
@required this.updatePosition,
|
||||
@required this.speed,
|
||||
|
@ -308,7 +329,13 @@ enum AudioPlaybackState {
|
|||
stopped,
|
||||
paused,
|
||||
playing,
|
||||
buffering,
|
||||
connecting,
|
||||
completed,
|
||||
}
|
||||
|
||||
class FullAudioPlaybackState {
|
||||
final AudioPlaybackState state;
|
||||
final bool buffering;
|
||||
|
||||
FullAudioPlaybackState(this.state, this.buffering);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue