From d6e14ae9d6e9da4f7416ba6a7b48139ff77e9d1d Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Sat, 8 Aug 2020 01:11:15 +1000 Subject: [PATCH] Skip over error items in playlist on iOS. Fix positionStream. --- darwin/Classes/AudioPlayer.m | 41 +++++++- example/lib/main.dart | 178 +++++++++++++++++++---------------- lib/just_audio.dart | 14 ++- 3 files changed, 143 insertions(+), 90 deletions(-) diff --git a/darwin/Classes/AudioPlayer.m b/darwin/Classes/AudioPlayer.m index 655690d..ccbfdea 100644 --- a/darwin/Classes/AudioPlayer.m +++ b/darwin/Classes/AudioPlayer.m @@ -425,6 +425,7 @@ } - (void)enqueueFrom:(int)index { + int oldIndex = _index; _index = index; // Update the queue while keeping the currently playing item untouched. @@ -436,13 +437,23 @@ IndexedPlayerItem *oldItem = _player.currentItem; IndexedPlayerItem *existingItem = nil; NSArray *oldPlayerItems = [NSArray arrayWithArray:_player.items]; + // In the first pass, preserve the old and new items. for (int i = 0; i < oldPlayerItems.count; i++) { - if (oldPlayerItems[i] != _indexedAudioSources[_index].playerItem) { - [_player removeItem:oldPlayerItems[i]]; - } else { + if (oldPlayerItems[i] == _indexedAudioSources[_index].playerItem) { + // Preserve and tag new item if it is already in the queue. existingItem = oldPlayerItems[i]; + } else if (oldPlayerItems[i] == oldItem) { + // Temporarily preserve old item, just to avoid jumping to + // intermediate queue positions unnecessarily. We only want to jump + // once to _index. + } else { + [_player removeItem:oldPlayerItems[i]]; } } + // In the second pass, remove the old item (if different from new item). + if (_index != oldIndex) { + [_player removeItem:oldItem]; + } /* NSLog(@"inter order: _player.items.count: ", _player.items.count); */ /* [self dumpQueue]; */ @@ -601,6 +612,7 @@ } - (void)onComplete:(NSNotification *)notification { + NSLog(@"onComplete"); if (_loopMode == loopOne) { [self seek:kCMTimeZero index:@(_index) completionHandler:^(BOOL finished) { // XXX: Not necessary? @@ -771,11 +783,31 @@ } } } else if ([keyPath isEqualToString:@"currentItem"] && _player.currentItem) { + if (_player.currentItem.status == AVPlayerItemStatusFailed) { + if ([_orderInv[_index] intValue] + 1 < [_order count]) { + // account for automatic move to next item + _index = [_order[[_orderInv[_index] intValue] + 1] intValue]; + NSLog(@"advance to next on error: index = %d", _index); + [self broadcastPlaybackEvent]; + } else { + NSLog(@"error on last item"); + } + return; + } else { + int expectedIndex = [self indexForItem:_player.currentItem]; + if (_index != expectedIndex) { + // AVQueuePlayer will sometimes skip over error items without + // notifying this observer. + NSLog(@"Queue change detected. Adjusting index from %d -> %d", _index, expectedIndex); + _index = expectedIndex; + [self broadcastPlaybackEvent]; + } + } //NSLog(@"currentItem changed. _index=%d", _index); _bufferUnconfirmed = YES; // If we've skipped or transitioned to a new item and we're not // currently in the middle of a seek - if (_player.currentItem && CMTIME_IS_INVALID(_seekPos) && _player.currentItem.status == AVPlayerItemStatusReadyToPlay) { + if (CMTIME_IS_INVALID(_seekPos) && _player.currentItem.status == AVPlayerItemStatusReadyToPlay) { [self updatePosition]; IndexedAudioSource *source = ((IndexedPlayerItem *)_player.currentItem).audioSource; // We should already be at position zero but for @@ -827,6 +859,7 @@ } - (void)sendError:(FlutterError *)flutterError playerItem:(IndexedPlayerItem *)playerItem { + NSLog(@"sendError"); if (_loadResult && playerItem == _player.currentItem) { _loadResult(flutterError); _loadResult = nil; diff --git a/example/lib/main.dart b/example/lib/main.dart index dbfc2ff..2e59fbe 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -115,87 +115,7 @@ class _MyAppState extends State { }, ), ), - StreamBuilder( - stream: _player.playerStateStream, - builder: (context, snapshot) { - final playerState = snapshot.data; - final processingState = playerState?.processingState; - final playing = playerState?.playing; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon(Icons.volume_up), - onPressed: () { - _showSliderDialog( - context: context, - title: "Adjust volume", - divisions: 10, - min: 0.0, - max: 1.0, - stream: _player.volumeStream, - onChanged: _player.setVolume, - ); - }, - ), - IconButton( - icon: Icon(Icons.skip_previous), - onPressed: - _player.hasPrevious ? _player.seekToPrevious : null, - ), - if (processingState == ProcessingState.loading || - processingState == ProcessingState.buffering) - Container( - margin: EdgeInsets.all(8.0), - width: 64.0, - height: 64.0, - child: CircularProgressIndicator(), - ) - else if (playing != true) - IconButton( - icon: Icon(Icons.play_arrow), - iconSize: 64.0, - onPressed: _player.play, - ) - else if (processingState != ProcessingState.completed) - IconButton( - icon: Icon(Icons.pause), - iconSize: 64.0, - onPressed: _player.pause, - ) - else - IconButton( - icon: Icon(Icons.replay), - iconSize: 64.0, - onPressed: () => - _player.seek(Duration.zero, index: 0), - ), - IconButton( - icon: Icon(Icons.skip_next), - onPressed: _player.hasNext ? _player.seekToNext : null, - ), - IconButton( - icon: StreamBuilder( - stream: _player.speedStream, - builder: (context, snapshot) => Text( - "${snapshot.data?.toStringAsFixed(1)}x", - style: TextStyle(fontWeight: FontWeight.bold))), - onPressed: () { - _showSliderDialog( - context: context, - title: "Adjust speed", - divisions: 10, - min: 0.5, - max: 1.5, - stream: _player.speedStream, - onChanged: _player.setSpeed, - ); - }, - ), - ], - ); - }, - ), + ControlButtons(_player), StreamBuilder( stream: _player.durationStream, builder: (context, snapshot) { @@ -299,6 +219,102 @@ class _MyAppState extends State { } } +class ControlButtons extends StatelessWidget { + final AudioPlayer player; + + ControlButtons(this.player); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon(Icons.volume_up), + onPressed: () { + _showSliderDialog( + context: context, + title: "Adjust volume", + divisions: 10, + min: 0.0, + max: 1.0, + stream: player.volumeStream, + onChanged: player.setVolume, + ); + }, + ), + StreamBuilder( + stream: player.currentIndexStream, + builder: (context, snapshot) => IconButton( + icon: Icon(Icons.skip_previous), + onPressed: player.hasPrevious ? player.seekToPrevious : null, + ), + ), + StreamBuilder( + stream: player.playerStateStream, + builder: (context, snapshot) { + final playerState = snapshot.data; + final processingState = playerState?.processingState; + final playing = playerState?.playing; + if (processingState == ProcessingState.loading || + processingState == ProcessingState.buffering) { + return Container( + margin: EdgeInsets.all(8.0), + width: 64.0, + height: 64.0, + child: CircularProgressIndicator(), + ); + } else if (playing != true) { + return IconButton( + icon: Icon(Icons.play_arrow), + iconSize: 64.0, + onPressed: player.play, + ); + } else if (processingState != ProcessingState.completed) { + return IconButton( + icon: Icon(Icons.pause), + iconSize: 64.0, + onPressed: player.pause, + ); + } else { + return IconButton( + icon: Icon(Icons.replay), + iconSize: 64.0, + onPressed: () => player.seek(Duration.zero, index: 0), + ); + } + }, + ), + StreamBuilder( + stream: player.currentIndexStream, + builder: (context, snapshot) => IconButton( + icon: Icon(Icons.skip_next), + onPressed: player.hasNext ? player.seekToNext : null, + ), + ), + StreamBuilder( + stream: player.speedStream, + builder: (context, snapshot) => IconButton( + icon: Text("${snapshot.data?.toStringAsFixed(1)}x", + style: TextStyle(fontWeight: FontWeight.bold)), + onPressed: () { + _showSliderDialog( + context: context, + title: "Adjust speed", + divisions: 10, + min: 0.5, + max: 1.5, + stream: player.speedStream, + onChanged: player.setSpeed, + ); + }, + ), + ), + ], + ); + } +} + class SeekBar extends StatefulWidget { final Duration duration; final Duration position; diff --git a/lib/just_audio.dart b/lib/just_audio.dart index f6c058e..54ab25e 100644 --- a/lib/just_audio.dart +++ b/lib/just_audio.dart @@ -311,15 +311,18 @@ class AudioPlayer { StreamController controller = StreamController.broadcast(); Timer currentTimer; StreamSubscription durationSubscription; + StreamSubscription playbackEventSubscription; void yieldPosition(Timer timer) { if (controller.isClosed) { timer.cancel(); - durationSubscription.cancel(); + durationSubscription?.cancel(); + playbackEventSubscription?.cancel(); return; } if (_durationSubject.isClosed) { timer.cancel(); - durationSubscription.cancel(); + durationSubscription?.cancel(); + playbackEventSubscription?.cancel(); controller.close(); return; } @@ -331,9 +334,10 @@ class AudioPlayer { currentTimer.cancel(); currentTimer = Timer.periodic(step(), yieldPosition); }); - return Rx.combineLatest2( - playbackEventStream, controller.stream, (event, period) => position) - .distinct(); + playbackEventSubscription = playbackEventStream.listen((event) { + controller.add(position); + }); + return controller.stream.distinct(); } /// Convenience method to load audio from a URL with optional headers,