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:
edde746
2026-07-06 15:28:24 +02:00
co-authored by Claude Fable 5
parent 73be8ab1c8
commit 28bc4a5df8
7 changed files with 167 additions and 25 deletions
@@ -1,5 +1,9 @@
part of '../../video_player_screen.dart';
/// Fallback for OS skip commands that arrive without an interval (the
/// platforms normally send one — Android hardcodes 15s).
const _defaultMediaControlSkip = Duration(seconds: 15);
extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState {
void _queueScrubPreviewLoad({
required MediaItem metadata,
@@ -331,6 +335,21 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState {
} else if (event is PreviousTrackEvent) {
appLogger.d('Media control: Previous track event received');
unawaited(_restartOrPlayPrevious());
} else if (event is StopEvent) {
// Same semantics as the companion remote's stop: exit the player.
appLogger.d('Media control: Stop event received');
unawaited(_handleBackButton());
} else if (event is SkipForwardEvent) {
appLogger.d('Media control: Skip forward event received (${event.interval})');
unawaited(_seekRelative(event.interval ?? _defaultMediaControlSkip));
} else if (event is SkipBackwardEvent) {
appLogger.d('Media control: Skip backward event received (${event.interval})');
unawaited(_seekRelative(-(event.interval ?? _defaultMediaControlSkip)));
} else if (event is SetSpeedEvent) {
// UI, Discord, and the media-session state all follow reactively
// via streams.rate — same unguarded path as keyboard shortcuts.
appLogger.d('Media control: Set speed event received (${event.speed}x)');
unawaited(activePlayer!.setRate(event.speed));
}
});