diff --git a/lib/screens/video_player/media_controls_screen_controller.dart b/lib/screens/video_player/media_controls_screen_controller.dart new file mode 100644 index 000000000..336c9bc1c --- /dev/null +++ b/lib/screens/video_player/media_controls_screen_controller.dart @@ -0,0 +1,186 @@ +import 'dart:async'; +import 'dart:io'; + +import '../../media/media_item.dart'; +import '../../media/media_item_types.dart'; +import '../../media/media_server_client.dart'; +import '../../mpv/mpv.dart'; +import '../../services/media_controls_manager.dart'; +import '../../services/settings_service.dart'; +import '../../utils/app_logger.dart'; +import '../../utils/platform_detector.dart'; +import '../../utils/player_utils.dart'; +import 'wakelock_controller.dart'; + +/// Screen-side adapter over [MediaControlsManager]: owns the Android TV +/// background suspension latch and the availability/restore-on-resume +/// policies for the OS media session. +/// +/// Plain State-owned object following the established player helper pattern +/// (see [WakelockController]). Every dependency that can change across a +/// playback attempt — the manager, the player, the current item — is injected +/// as a late-bound getter because both are re-created on in-place reloads and +/// nulled during teardown; caching them here would touch freed objects. +class MediaControlsScreenController { + MediaControlsScreenController({ + required this._manager, + required this._player, + required this._isMounted, + required this.isLive, + required this._shouldSkipForPip, + required this._isPlayerInitialized, + required this._metadata, + required this._client, + required this._isPlaylistActive, + required this._canControlPlayback, + required this._canNavigateMediaItems, + required this._rewindOnResumeSeconds, + required this._seek, + required this._play, + required this._wasPlayingBeforeInactive, + required this._clearWasPlayingBeforeInactive, + required this._wakelock, + required this._recordLifecycle, + }); + + final MediaControlsManager? Function() _manager; + final Player? Function() _player; + final bool Function() _isMounted; + final bool isLive; + final bool Function() _shouldSkipForPip; + final bool Function() _isPlayerInitialized; + final MediaItem Function() _metadata; + final MediaServerClient? Function() _client; + final bool Function() _isPlaylistActive; + final bool Function() _canControlPlayback; + final bool Function() _canNavigateMediaItems; + final int Function() _rewindOnResumeSeconds; + final Future Function(Duration position) _seek; + final Future Function(Player player) _play; + final bool Function() _wasPlayingBeforeInactive; + final void Function() _clearWasPlayingBeforeInactive; + final WakelockController _wakelock; + final void Function(String state, {String? action}) _recordLifecycle; + + bool _suspendedForTvBackground = false; + + /// Whether OS media-control events are currently ignored because the app is + /// backgrounded on Android TV. + bool get suspendedForTvBackground => _suspendedForTvBackground; + + bool get shouldSuspendForTvBackground => Platform.isAndroid && PlatformDetector.isTV() && !_shouldSkipForPip(); + + Future suspendForTvBackground(String reason) async { + if (!shouldSuspendForTvBackground) return; + + _manager()?.suspendUpdates(); + if (!_suspendedForTvBackground) { + _suspendedForTvBackground = true; + _recordLifecycle('media_controls', action: 'suspended:$reason'); + } + + await _manager()?.clear(); + } + + void resumeAfterTvBackground(String reason) { + if (!_suspendedForTvBackground) return; + + _suspendedForTvBackground = false; + _manager()?.resumeUpdates(); + _recordLifecycle('media_controls', action: 'resumed:$reason'); + } + + /// Clears the suspension latch without touching the manager — used when the + /// manager itself is being torn down with the playback attempt. + void resetSuspension() { + _suspendedForTvBackground = false; + } + + Future syncAvailability() async { + if (_suspendedForTvBackground) return; + + final manager = _manager(); + final currentPlayer = _player(); + if (!_isMounted() || manager == null || currentPlayer == null) return; + + final hasNavigableItems = _metadata().isEpisode || _isPlaylistActive(); + final contentCanSeek = !isLive && currentPlayer.state.seekable; + final canControlPlayback = _canControlPlayback(); + final canNavigateMediaItems = _canNavigateMediaItems(); + + await manager.setControlsEnabled( + canPlayPause: canControlPlayback, + canGoNext: hasNavigableItems && canNavigateMediaItems, + canGoPrevious: hasNavigableItems && canNavigateMediaItems, + canSeek: contentCanSeek && canControlPlayback, + canStop: true, + // In-track skips work on live TV too through the capture buffer. + canSkip: canControlPlayback, + // Video claims the lock-screen / remote-card side slots for ±skip + // (#1994); the step mirrors the in-player small skip. A mid-playback + // seekTimeSmall change applies on the next availability sync. + preferSkipOverTrackButtons: true, + skipInterval: Duration(seconds: SettingsService.instance.read(SettingsService.seekTimeSmall)), + // Rate changes don't apply to a live stream. + canSetSpeed: !isLive && canControlPlayback, + ); + } + + Future seekBackForRewind(Player p) async { + final rewindOnResume = _rewindOnResumeSeconds(); + if (rewindOnResume <= 0) return; + final target = p.state.position - Duration(seconds: rewindOnResume); + await _seek(clampSeekPosition(p, target)); + } + + Future restoreAfterResume() async { + if (!_isPlayerInitialized() || !_isMounted()) return; + + unawaited(_wakelock.setEnabled(_player()?.state.isActive ?? false)); + + final manager = _manager(); + final currentPlayer = _player(); + if (manager != null && currentPlayer != null) { + final metadata = _metadata(); + final durationMs = metadata.durationMs; + await manager.updateMetadata( + metadata: metadata, + client: _client(), + duration: durationMs != null ? Duration(milliseconds: durationMs) : null, + ); + await syncAvailability(); + } + + if (!_isMounted() || currentPlayer != _player() || currentPlayer == null) return; + + final wasPlayingBeforeInactive = _wasPlayingBeforeInactive(); + if (wasPlayingBeforeInactive) { + try { + await seekBackForRewind(currentPlayer); + await _play(currentPlayer); + appLogger.d('Video resumed after returning from inactive state'); + } catch (e) { + appLogger.w('Failed to resume playback after returning from inactive state', error: e); + } finally { + _clearWasPlayingBeforeInactive(); + } + } + + pushPlaybackState(); + appLogger.d('Media controls restored on app resume'); + } + + /// Pushes the current playback state to the OS media session. + void pushPlaybackState() { + if (_suspendedForTvBackground) return; + final currentPlayer = _player(); + if (currentPlayer == null) return; + + _manager()?.updatePlaybackState( + isPlaying: currentPlayer.state.isActive, + position: currentPlayer.state.position, + speed: currentPlayer.state.rate, + force: true, // Force update since this is an explicit state change + ); + } +} diff --git a/lib/screens/video_player/parts/build.dart b/lib/screens/video_player/parts/build.dart index 01f403f95..58b221370 100644 --- a/lib/screens/video_player/parts/build.dart +++ b/lib/screens/video_player/parts/build.dart @@ -246,7 +246,7 @@ extension _VideoPlayerBuildMethods on VideoPlayerScreenState { if (_lastMediaControlAuthority != authority) { _lastMediaControlAuthority = authority; WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) unawaited(_syncMediaControlsAvailability()); + if (mounted) unawaited(_mediaControls.syncAvailability()); }); } diff --git a/lib/screens/video_player/parts/episode_navigation.dart b/lib/screens/video_player/parts/episode_navigation.dart index ba15e7061..e3f05c96d 100644 --- a/lib/screens/video_player/parts/episode_navigation.dart +++ b/lib/screens/video_player/parts/episode_navigation.dart @@ -87,7 +87,7 @@ extension _VideoPlayerEpisodeNavigationMethods on VideoPlayerScreenState { if (!mounted || currentPlayer != player) return; _notifyWatchTogetherSeek(target); - _updateMediaControlsPlaybackState(); + _mediaControls.pushPlaybackState(); } /// Replace this screen with a fresh player route — the fallback for flows diff --git a/lib/screens/video_player/parts/lifecycle.dart b/lib/screens/video_player/parts/lifecycle.dart index 50f5852db..789aaa10a 100644 --- a/lib/screens/video_player/parts/lifecycle.dart +++ b/lib/screens/video_player/parts/lifecycle.dart @@ -30,7 +30,7 @@ extension _VideoPlayerLifecycleMethods on VideoPlayerScreenState { 'pipTransitionInFlight': _androidAutoPipTransitionInFlight, 'hiddenForBackground': _hiddenForBackground, 'playerSuspendedForTvBackground': _playerSuspendedForTvBackground, - 'mediaControlsSuspendedForTvBackground': _mediaControlsSuspendedForTvBackground, + 'mediaControlsSuspendedForTvBackground': _mediaControls.suspendedForTvBackground, 'backend': _playerBackendLabel, }; if (action != null) { @@ -50,7 +50,7 @@ extension _VideoPlayerLifecycleMethods on VideoPlayerScreenState { ' pipTransitionInFlight=$_androidAutoPipTransitionInFlight' ' hiddenForBackground=$_hiddenForBackground' ' playerSuspendedForTvBackground=$_playerSuspendedForTvBackground' - ' mediaControlsSuspendedForTvBackground=$_mediaControlsSuspendedForTvBackground' + ' mediaControlsSuspendedForTvBackground=$_mediaControls.suspendedForTvBackground' ' backend=$_playerBackendLabel', ); } @@ -106,7 +106,7 @@ extension _VideoPlayerLifecycleMethods on VideoPlayerScreenState { } await stoppedReport; if (!mounted || currentPlayer != player) return; - await _suspendMediaControlsForTvBackground('hidden_live_stopped'); + await _mediaControls.suspendForTvBackground('hidden_live_stopped'); _recordLifecycleState('hidden', action: 'live_stopped_exit_on_resume'); return; } @@ -159,7 +159,7 @@ extension _VideoPlayerLifecycleMethods on VideoPlayerScreenState { _suspendLiveTimelineForBackground(); if (isTv) { - await _suspendMediaControlsForTvBackground('hidden'); + await _mediaControls.suspendForTvBackground('hidden'); if (_armTvBackgroundPlayerSuspendTimer()) { _recordLifecycleState('hidden', action: 'tv_background_pause_suspend_armed'); } else { @@ -228,8 +228,8 @@ extension _VideoPlayerLifecycleMethods on VideoPlayerScreenState { // Restore media controls and wakelock when app is resumed. if (_isPlayerInitialized && mounted) { - _resumeMediaControlsAfterTvBackground('app_resumed'); - await _restoreMediaControlsAfterResume(); + _mediaControls.resumeAfterTvBackground('app_resumed'); + await _mediaControls.restoreAfterResume(); } _resumeLiveTimelineAfterBackgroundIfNeeded(); @@ -350,7 +350,7 @@ extension _VideoPlayerLifecycleMethods on VideoPlayerScreenState { /// place through the regular reload flow — a fresh playback decision, since /// the old session is closed and its stream URL may have expired /// server-side — and comes back paused; the caller's - /// [_restoreMediaControlsAfterResume] then resumes it (with + /// [MediaControlsScreenController.restoreAfterResume] then resumes it (with /// rewind-on-resume) exactly like a plain background pause. Live sessions /// never enter this flow because their tuned session and capture-buffer /// position must remain intact across backgrounding. diff --git a/lib/screens/video_player/parts/media_controls.dart b/lib/screens/video_player/parts/media_controls.dart deleted file mode 100644 index 5b8427075..000000000 --- a/lib/screens/video_player/parts/media_controls.dart +++ /dev/null @@ -1,112 +0,0 @@ -part of '../../video_player_screen.dart'; - -extension _VideoPlayerMediaControlsMethods on VideoPlayerScreenState { - bool get _shouldSuspendMediaControlsForTvBackground => - Platform.isAndroid && PlatformDetector.isTV() && !_shouldSkipForPip; - - Future _suspendMediaControlsForTvBackground(String reason) async { - if (!_shouldSuspendMediaControlsForTvBackground) return; - - _mediaControlsManager?.suspendUpdates(); - if (!_mediaControlsSuspendedForTvBackground) { - _mediaControlsSuspendedForTvBackground = true; - _recordLifecycleState('media_controls', action: 'suspended:$reason'); - } - - await _mediaControlsManager?.clear(); - } - - void _resumeMediaControlsAfterTvBackground(String reason) { - if (!_mediaControlsSuspendedForTvBackground) return; - - _mediaControlsSuspendedForTvBackground = false; - _mediaControlsManager?.resumeUpdates(); - _recordLifecycleState('media_controls', action: 'resumed:$reason'); - } - - Future _syncMediaControlsAvailability() async { - if (_mediaControlsSuspendedForTvBackground) return; - - final manager = _mediaControlsManager; - final currentPlayer = player; - if (!mounted || manager == null || currentPlayer == null) return; - - final playbackState = context.read(); - final hasNavigableItems = _currentMetadata.isEpisode || playbackState.isPlaylistActive; - final contentCanSeek = !widget.isLive && currentPlayer.state.seekable; - final canControlPlayback = _canControlPlayback(); - final canNavigateMediaItems = _canNavigateMediaItems(); - - await manager.setControlsEnabled( - canPlayPause: canControlPlayback, - canGoNext: hasNavigableItems && canNavigateMediaItems, - canGoPrevious: hasNavigableItems && canNavigateMediaItems, - canSeek: contentCanSeek && canControlPlayback, - canStop: true, - // In-track skips work on live TV too through the capture buffer. - canSkip: canControlPlayback, - // Video claims the lock-screen / remote-card side slots for ±skip - // (#1994); the step mirrors the in-player small skip. A mid-playback - // seekTimeSmall change applies on the next availability sync. - preferSkipOverTrackButtons: true, - skipInterval: Duration(seconds: SettingsService.instance.read(SettingsService.seekTimeSmall)), - // Rate changes don't apply to a live stream. - canSetSpeed: !widget.isLive && canControlPlayback, - ); - } - - Future _seekBackForRewind(Player p) async { - if (_rewindOnResume <= 0) return; - final target = p.state.position - Duration(seconds: _rewindOnResume); - await _seekPlayback(clampSeekPosition(p, target)); - } - - Future _restoreMediaControlsAfterResume() async { - if (!_isPlayerInitialized || !mounted) return; - - unawaited(_wakelockController.setEnabled(player?.state.isActive ?? false)); - - final manager = _mediaControlsManager; - final currentPlayer = player; - if (manager != null && currentPlayer != null) { - final client = _isOfflinePlayback ? null : _getMediaServerClient(context); - await manager.updateMetadata( - metadata: _currentMetadata, - client: client, - duration: _currentMetadata.durationMs != null ? Duration(milliseconds: _currentMetadata.durationMs!) : null, - ); - await _syncMediaControlsAvailability(); - } - - if (!mounted || currentPlayer != player || currentPlayer == null) return; - - final wasPlayingBeforeInactive = _wasPlayingBeforeInactive; - if (wasPlayingBeforeInactive) { - try { - await _seekBackForRewind(currentPlayer); - await _playWithPlaybackIntent(currentPlayer); - appLogger.d('Video resumed after returning from inactive state'); - } catch (e) { - appLogger.w('Failed to resume playback after returning from inactive state', error: e); - } finally { - _wasPlayingBeforeInactive = false; - } - } - - _updateMediaControlsPlaybackState(); - appLogger.d('Media controls restored on app resume'); - } - - /// Wrapper method to update media controls playback state - void _updateMediaControlsPlaybackState() { - if (_mediaControlsSuspendedForTvBackground) return; - if (player == null) return; - - _mediaControlsManager?.updatePlaybackState( - isPlaying: player!.state.isActive, - position: player!.state.position, - speed: player!.state.rate, - force: true, // Force update since this is an explicit state change - ); - } -} diff --git a/lib/screens/video_player/parts/playback_prompts.dart b/lib/screens/video_player/parts/playback_prompts.dart index e7cdaf163..bdff24385 100644 --- a/lib/screens/video_player/parts/playback_prompts.dart +++ b/lib/screens/video_player/parts/playback_prompts.dart @@ -20,7 +20,7 @@ extension _VideoPlayerPlaybackPromptMethods on VideoPlayerScreenState { ? _sendStoppedProgressOnce(positionOverride: duration) : _sendStoppedProgressOnce(), ); - _updateMediaControlsPlaybackState(); + _mediaControls.pushPlaybackState(); unawaited(DiscordRPCService.instance.pausePlayback()); // The item finished, so real-time trackers get a terminal report now rather // than whenever the screen happens to tear down: a completion prompt or diff --git a/lib/screens/video_player/parts/playback_services.dart b/lib/screens/video_player/parts/playback_services.dart index 87c845f3f..fd7ff0ebf 100644 --- a/lib/screens/video_player/parts/playback_services.dart +++ b/lib/screens/video_player/parts/playback_services.dart @@ -264,7 +264,7 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState { _audioFocusFuture = null; _playbackDataFuture = null; _playbackSession = null; - _mediaControlsSuspendedForTvBackground = false; + _mediaControls.resetSuspension(); if (progressTracker != null) { try { @@ -439,11 +439,11 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState { onPlay: () { final currentPlayer = player; if (currentPlayer == null) return; - unawaited(_seekBackForRewind(currentPlayer)); + unawaited(_mediaControls.seekBackForRewind(currentPlayer)); unawaited(_playWithPlaybackIntent(currentPlayer)); _wasPlayingBeforeInactive = false; _announceTransportCommand(willPlay: true); - _updateMediaControlsPlaybackState(); + _mediaControls.pushPlaybackState(); }, onPause: () { final currentPlayer = player; @@ -454,7 +454,7 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState { } unawaited(_pauseWithPlaybackIntent(currentPlayer)); _announceTransportCommand(willPlay: false); - _updateMediaControlsPlaybackState(); + _mediaControls.pushPlaybackState(); }, onTogglePlayPause: () { final currentPlayer = player; @@ -463,12 +463,12 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState { unawaited(_pauseWithPlaybackIntent(currentPlayer)); _announceTransportCommand(willPlay: false); } else { - unawaited(_seekBackForRewind(currentPlayer)); + unawaited(_mediaControls.seekBackForRewind(currentPlayer)); unawaited(_playWithPlaybackIntent(currentPlayer)); _wasPlayingBeforeInactive = false; _announceTransportCommand(willPlay: true); } - _updateMediaControlsPlaybackState(); + _mediaControls.pushPlaybackState(); }, onSeek: (position) { final currentPlayer = player; @@ -491,7 +491,7 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState { // Set up media control event handling _mediaControlSubscription = mediaControlsManager.controlEvents.listen((event) { - if (_mediaControlsSuspendedForTvBackground) { + if (_mediaControls.suspendedForTvBackground) { appLogger.d('Media control: ${event.runtimeType} ignored while Android TV background-suspended'); return; } @@ -523,12 +523,12 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState { if (!mounted) return; - await _syncMediaControlsAvailability(); + await _mediaControls.syncAvailability(); if (!mounted || player != currentPlayer || _mediaControlsManager != mediaControlsManager) return; // Listen to playing state and update media controls _mediaControlsPlayingSubscription = currentPlayer.streams.playing.listen((isPlaying) { - _updateMediaControlsPlaybackState(); + _mediaControls.pushPlaybackState(); }); // Listen to position updates for media controls and Discord @@ -552,7 +552,7 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState { }); _mediaControlsSeekableSubscription = currentPlayer.streams.seekable.listen((_) { - unawaited(_syncMediaControlsAvailability()); + unawaited(_mediaControls.syncAvailability()); }); } @@ -579,7 +579,7 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState { return; } - if (isPlaying && _mediaControlsSuspendedForTvBackground) { + if (isPlaying && _mediaControls.suspendedForTvBackground) { appLogger.w('Playback started while Android TV background media controls are suspended; pausing'); Sentry.addBreadcrumb( Breadcrumb(message: 'Blocked TV background playback start', category: 'player.media_controls'), @@ -605,7 +605,7 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState { _progressTracker?.sendProgress(isPlaying ? 'playing' : 'paused'); // Update OS media controls playback state - _updateMediaControlsPlaybackState(); + _mediaControls.pushPlaybackState(); // Update Discord Rich Presence + real-time trackers if (isPlaying) { @@ -664,7 +664,7 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState { } catch (e) { appLogger.w('Failed to pause after Apple audio session $reason', error: e); } finally { - _updateMediaControlsPlaybackState(); + _mediaControls.pushPlaybackState(); } } @@ -692,7 +692,7 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState { } catch (e) { appLogger.w('Failed to resume after Apple audio session $reason', error: e); } finally { - _updateMediaControlsPlaybackState(); + _mediaControls.pushPlaybackState(); } } diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 3d5680226..949074d33 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -89,6 +89,7 @@ import '../utils/video_player_navigation.dart'; import '../utils/android_exit_diagnostics.dart'; import 'video_player/completion_latch.dart'; import 'video_player/frame_rate_matcher.dart'; +import 'video_player/media_controls_screen_controller.dart'; import 'video_player/live_stream_retry.dart'; import 'video_player/live_timeline_report.dart'; import 'video_player/wakelock_controller.dart'; @@ -118,7 +119,6 @@ part 'video_player/parts/episode_queue.dart'; part 'video_player/parts/errors.dart'; part 'video_player/parts/lifecycle.dart'; part 'video_player/parts/live_tv.dart'; -part 'video_player/parts/media_controls.dart'; part 'video_player/parts/pip.dart'; part 'video_player/parts/shader.dart'; part 'video_player/parts/playback_open.dart'; @@ -691,7 +691,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin // App lifecycle state tracking bool _wasPlayingBeforeInactive = false; bool _hiddenForBackground = false; - bool _mediaControlsSuspendedForTvBackground = false; bool _resumeAfterAppleAudioSessionPause = false; DateTime? _lastPlaybackPauseAt; bool _autoPipEnabled = false; @@ -736,6 +735,26 @@ class VideoPlayerScreenState extends State with WidgetsBindin (Platform.isAndroid && _androidAutoPipTransitionInFlight); MediaControlsManager? _mediaControlsManager; + late final MediaControlsScreenController _mediaControls = MediaControlsScreenController( + manager: () => _mediaControlsManager, + player: () => player, + isMounted: () => mounted, + isLive: widget.isLive, + shouldSkipForPip: () => _shouldSkipForPip, + isPlayerInitialized: () => _isPlayerInitialized, + metadata: () => _currentMetadata, + client: () => _isOfflinePlayback ? null : _getMediaServerClient(context), + isPlaylistActive: () => context.read().isPlaylistActive, + canControlPlayback: () => _canControlPlayback(), + canNavigateMediaItems: () => _canNavigateMediaItems(), + rewindOnResumeSeconds: () => _rewindOnResume, + seek: (position) => _seekPlayback(position), + play: _playWithPlaybackIntent, + wasPlayingBeforeInactive: () => _wasPlayingBeforeInactive, + clearWasPlayingBeforeInactive: () => _wasPlayingBeforeInactive = false, + wakelock: _wakelockController, + recordLifecycle: (state, {action}) => _recordLifecycleState(state, action: action), + ); ({bool canControlPlayback, bool canNavigateMediaItems})? _lastMediaControlAuthority; PlaybackProgressTracker? _progressTracker; VideoFilterManager? _videoFilterManager; @@ -1194,8 +1213,8 @@ class VideoPlayerScreenState extends State with WidgetsBindin break; } // We don't support background playback - if (_shouldSuspendMediaControlsForTvBackground) { - unawaited(_suspendMediaControlsForTvBackground('paused')); + if (_mediaControls.shouldSuspendForTvBackground) { + unawaited(_mediaControls.suspendForTvBackground('paused')); } else { unawaited(_mediaControlsManager?.clear()); } @@ -2199,7 +2218,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin try { if (resumes) { - await _seekBackForRewind(currentPlayer); + await _mediaControls.seekBackForRewind(currentPlayer); if (!mounted || player != currentPlayer) return; } await switch (command) {