feat(player): handle stop/skip/speed media-session commands
The platforms advertise stop, skip forward/backward, and playback-rate commands by default, but both the music and video handlers silently dropped them (Android Auto/Bluetooth stop and FF/rewind did nothing; iOS/macOS showed a dead rate control). setControlsEnabled now manages those controls: music handles Stop and in-track skips and stops advertising a speed control; video handles Stop (exit, matching the companion remote), skips via a shared relative-seek helper, and rate changes through player.setRate. Skip commands stay off on iOS/macOS where they would displace the next/previous lock-screen buttons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
73be8ab1c8
commit
28bc4a5df8
@@ -451,8 +451,19 @@ class FakeMediaControlsManager extends MediaControlsManager {
|
||||
bool force = false,
|
||||
}) async {}
|
||||
|
||||
final List<({bool canGoNext, bool canStop, bool canSkip, bool canSetSpeed})> controlSyncs = [];
|
||||
|
||||
@override
|
||||
Future<void> setControlsEnabled({bool canGoNext = false, bool canGoPrevious = false, bool canSeek = false}) async {}
|
||||
Future<void> setControlsEnabled({
|
||||
bool canGoNext = false,
|
||||
bool canGoPrevious = false,
|
||||
bool canSeek = false,
|
||||
bool canStop = false,
|
||||
bool canSkip = false,
|
||||
bool canSetSpeed = false,
|
||||
}) async {
|
||||
controlSyncs.add((canGoNext: canGoNext, canStop: canStop, canSkip: canSkip, canSetSpeed: canSetSpeed));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clear() async {
|
||||
@@ -739,6 +750,46 @@ void main() {
|
||||
expect(h.player.playCalls, 1);
|
||||
});
|
||||
|
||||
test('OS stop command stops the session', () async {
|
||||
await h.playTracks([t1, t2]);
|
||||
final player = h.player;
|
||||
|
||||
h.controls.eventsCtrl.add(const StopEvent());
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(h.service.status, MusicPlaybackStatus.idle);
|
||||
expect(h.service.currentTrack, isNull);
|
||||
expect(player.disposed, isTrue);
|
||||
});
|
||||
|
||||
test('OS skip commands seek within the track, clamped to its bounds', () async {
|
||||
await h.playTracks([t1, t2]);
|
||||
h.player.setPosition(const Duration(seconds: 30));
|
||||
|
||||
h.controls.eventsCtrl.add(const SkipForwardEvent(Duration(seconds: 15)));
|
||||
await pumpEventQueue();
|
||||
expect(h.player.seeks, [const Duration(seconds: 45)]);
|
||||
|
||||
h.controls.eventsCtrl.add(const SkipBackwardEvent(null)); // default interval
|
||||
await pumpEventQueue();
|
||||
expect(h.player.seeks.last, const Duration(seconds: 30));
|
||||
|
||||
h.player.setPosition(const Duration(seconds: 5));
|
||||
h.controls.eventsCtrl.add(const SkipBackwardEvent(Duration(seconds: 15)));
|
||||
await pumpEventQueue();
|
||||
expect(h.player.seeks.last, Duration.zero);
|
||||
});
|
||||
|
||||
test('music advertises stop and skip but never a speed control', () async {
|
||||
await h.playTracks([t1, t2]);
|
||||
|
||||
expect(h.controls.controlSyncs, isNotEmpty);
|
||||
final last = h.controls.controlSyncs.last;
|
||||
expect(last.canStop, isTrue);
|
||||
expect(last.canSkip, isTrue);
|
||||
expect(last.canSetSpeed, isFalse);
|
||||
});
|
||||
|
||||
test('interruption without shouldResume stays paused', () async {
|
||||
await h.playTracks([t1]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user