Skip over error items in playlist on iOS. Fix positionStream.
This commit is contained in:
parent
f266ce0b7a
commit
d6e14ae9d6
|
@ -425,6 +425,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)enqueueFrom:(int)index {
|
- (void)enqueueFrom:(int)index {
|
||||||
|
int oldIndex = _index;
|
||||||
_index = index;
|
_index = index;
|
||||||
|
|
||||||
// Update the queue while keeping the currently playing item untouched.
|
// Update the queue while keeping the currently playing item untouched.
|
||||||
|
@ -436,13 +437,23 @@
|
||||||
IndexedPlayerItem *oldItem = _player.currentItem;
|
IndexedPlayerItem *oldItem = _player.currentItem;
|
||||||
IndexedPlayerItem *existingItem = nil;
|
IndexedPlayerItem *existingItem = nil;
|
||||||
NSArray *oldPlayerItems = [NSArray arrayWithArray:_player.items];
|
NSArray *oldPlayerItems = [NSArray arrayWithArray:_player.items];
|
||||||
|
// In the first pass, preserve the old and new items.
|
||||||
for (int i = 0; i < oldPlayerItems.count; i++) {
|
for (int i = 0; i < oldPlayerItems.count; i++) {
|
||||||
if (oldPlayerItems[i] != _indexedAudioSources[_index].playerItem) {
|
if (oldPlayerItems[i] == _indexedAudioSources[_index].playerItem) {
|
||||||
[_player removeItem:oldPlayerItems[i]];
|
// Preserve and tag new item if it is already in the queue.
|
||||||
} else {
|
|
||||||
existingItem = oldPlayerItems[i];
|
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); */
|
/* NSLog(@"inter order: _player.items.count: ", _player.items.count); */
|
||||||
/* [self dumpQueue]; */
|
/* [self dumpQueue]; */
|
||||||
|
@ -601,6 +612,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)onComplete:(NSNotification *)notification {
|
- (void)onComplete:(NSNotification *)notification {
|
||||||
|
NSLog(@"onComplete");
|
||||||
if (_loopMode == loopOne) {
|
if (_loopMode == loopOne) {
|
||||||
[self seek:kCMTimeZero index:@(_index) completionHandler:^(BOOL finished) {
|
[self seek:kCMTimeZero index:@(_index) completionHandler:^(BOOL finished) {
|
||||||
// XXX: Not necessary?
|
// XXX: Not necessary?
|
||||||
|
@ -771,11 +783,31 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if ([keyPath isEqualToString:@"currentItem"] && _player.currentItem) {
|
} 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);
|
//NSLog(@"currentItem changed. _index=%d", _index);
|
||||||
_bufferUnconfirmed = YES;
|
_bufferUnconfirmed = YES;
|
||||||
// If we've skipped or transitioned to a new item and we're not
|
// If we've skipped or transitioned to a new item and we're not
|
||||||
// currently in the middle of a seek
|
// 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];
|
[self updatePosition];
|
||||||
IndexedAudioSource *source = ((IndexedPlayerItem *)_player.currentItem).audioSource;
|
IndexedAudioSource *source = ((IndexedPlayerItem *)_player.currentItem).audioSource;
|
||||||
// We should already be at position zero but for
|
// We should already be at position zero but for
|
||||||
|
@ -827,6 +859,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)sendError:(FlutterError *)flutterError playerItem:(IndexedPlayerItem *)playerItem {
|
- (void)sendError:(FlutterError *)flutterError playerItem:(IndexedPlayerItem *)playerItem {
|
||||||
|
NSLog(@"sendError");
|
||||||
if (_loadResult && playerItem == _player.currentItem) {
|
if (_loadResult && playerItem == _player.currentItem) {
|
||||||
_loadResult(flutterError);
|
_loadResult(flutterError);
|
||||||
_loadResult = nil;
|
_loadResult = nil;
|
||||||
|
|
|
@ -115,87 +115,7 @@ class _MyAppState extends State<MyApp> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
StreamBuilder<PlayerState>(
|
ControlButtons(_player),
|
||||||
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<double>(
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
StreamBuilder<Duration>(
|
StreamBuilder<Duration>(
|
||||||
stream: _player.durationStream,
|
stream: _player.durationStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
|
@ -299,6 +219,102 @@ class _MyAppState extends State<MyApp> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<int>(
|
||||||
|
stream: player.currentIndexStream,
|
||||||
|
builder: (context, snapshot) => IconButton(
|
||||||
|
icon: Icon(Icons.skip_previous),
|
||||||
|
onPressed: player.hasPrevious ? player.seekToPrevious : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
StreamBuilder<PlayerState>(
|
||||||
|
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<double>(
|
||||||
|
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 {
|
class SeekBar extends StatefulWidget {
|
||||||
final Duration duration;
|
final Duration duration;
|
||||||
final Duration position;
|
final Duration position;
|
||||||
|
|
|
@ -311,15 +311,18 @@ class AudioPlayer {
|
||||||
StreamController<Duration> controller = StreamController.broadcast();
|
StreamController<Duration> controller = StreamController.broadcast();
|
||||||
Timer currentTimer;
|
Timer currentTimer;
|
||||||
StreamSubscription durationSubscription;
|
StreamSubscription durationSubscription;
|
||||||
|
StreamSubscription playbackEventSubscription;
|
||||||
void yieldPosition(Timer timer) {
|
void yieldPosition(Timer timer) {
|
||||||
if (controller.isClosed) {
|
if (controller.isClosed) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
durationSubscription.cancel();
|
durationSubscription?.cancel();
|
||||||
|
playbackEventSubscription?.cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_durationSubject.isClosed) {
|
if (_durationSubject.isClosed) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
durationSubscription.cancel();
|
durationSubscription?.cancel();
|
||||||
|
playbackEventSubscription?.cancel();
|
||||||
controller.close();
|
controller.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -331,9 +334,10 @@ class AudioPlayer {
|
||||||
currentTimer.cancel();
|
currentTimer.cancel();
|
||||||
currentTimer = Timer.periodic(step(), yieldPosition);
|
currentTimer = Timer.periodic(step(), yieldPosition);
|
||||||
});
|
});
|
||||||
return Rx.combineLatest2<void, void, Duration>(
|
playbackEventSubscription = playbackEventStream.listen((event) {
|
||||||
playbackEventStream, controller.stream, (event, period) => position)
|
controller.add(position);
|
||||||
.distinct();
|
});
|
||||||
|
return controller.stream.distinct();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience method to load audio from a URL with optional headers,
|
/// Convenience method to load audio from a URL with optional headers,
|
||||||
|
|
Loading…
Reference in New Issue