From 2b4875d38900ee2402f722eab31f339c4411cefe Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:07:14 +0200 Subject: [PATCH] fix(player): keep hidden and cycled subtitles off in the next episode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Episode navigation carries the subtitle choice this screen has committed, so a way of turning subtitles off that the screen never sees is undone by the next episode. ExoPlayer has no renderer-level visibility switch, so the player's hide toggle is emulated by deselecting the track. That emulation lasted until the next selection: the automatic pass after an episode change put subtitles straight back on screen while the toggle still read "hidden", and un-hiding then restored a track id belonging to the episode that had already ended. Hiding is now sticky across media opens the way mpv's global sub-visibility is, selections made while hidden become what un-hiding restores, and the toggle no longer refuses to restore because the hidden track reads as Off. Cycling subtitles over the native track list — downloads, and items whose server exposes no subtitle rows — went straight to the track manager, which owns the player selection and the server write-back but not the committed choice. The screen records the cycled track now. --- lib/mpv/player/platform/player_android.dart | 51 ++++-- .../video_player/parts/companion_remote.dart | 17 +- lib/services/track_manager.dart | 9 +- .../video_controls/parts/track_controls.dart | 16 +- ...ayer_android_subtitle_visibility_test.dart | 146 ++++++++++++++++++ test/services/track_manager_test.dart | 27 +++- 6 files changed, 245 insertions(+), 21 deletions(-) create mode 100644 test/mpv/player_android_subtitle_visibility_test.dart diff --git a/lib/mpv/player/platform/player_android.dart b/lib/mpv/player/platform/player_android.dart index 25a76cfb..2ee9f8e5 100644 --- a/lib/mpv/player/platform/player_android.dart +++ b/lib/mpv/player/platform/player_android.dart @@ -28,6 +28,13 @@ class PlayerAndroid extends PlayerBase { bool get usingMpvFallback => _usingMpvFallback; + /// Subtitles are hidden through the player's visibility toggle. Sticky + /// across media opens, like mpv's global `sub-visibility`, so an episode + /// change cannot put them back on screen. + bool _subtitlesHidden = false; + + /// Track that un-hiding restores: whatever was selected when subtitles were + /// hidden, then whatever was selected for the current media while hidden. String? _hiddenSubtitleTrackId; @override @@ -248,14 +255,36 @@ class PlayerAndroid extends PlayerBase { await invoke('selectAudioTrack', {'trackId': track.id}); } + /// ExoPlayer has no renderer-level subtitle visibility switch, so hiding is + /// implemented as deselection (see [setProperty]'s `sub-visibility` case). + /// A selection arriving while subtitles are hidden — the automatic pass + /// after an episode change, or a manual pick — becomes what un-hiding + /// restores instead of putting subtitles back on screen, matching how mpv's + /// global `sub-visibility` keeps hiding across files (#1779). @override Future selectSubtitleTrack(SubtitleTrack track) async { + if (_subtitlesHidden) { + _hiddenSubtitleTrackId = track.id == SubtitleTrack.off.id ? null : track.id; + return _selectSubtitleTrackNatively(SubtitleTrack.off); + } + return _selectSubtitleTrackNatively(track); + } + + Future _selectSubtitleTrackNatively(SubtitleTrack track) async { await invoke('selectSubtitleTrack', {'trackId': track.id}); } + /// A sidecar flagged default must not draw itself onto a hidden renderer + /// either; the selection pass that follows the add records it the same way + /// [selectSubtitleTrack] does. @override Future addSubtitleTrack({required String uri, String? title, String? language, bool select = false}) async { - await invoke('addSubtitleTrack', {'uri': uri, 'title': title, 'language': language, 'select': select}); + await invoke('addSubtitleTrack', { + 'uri': uri, + 'title': title, + 'language': language, + 'select': select && !_subtitlesHidden, + }); } @override @@ -307,19 +336,21 @@ class PlayerAndroid extends PlayerBase { break; case 'sub-visibility': if (value == 'no') { + if (_subtitlesHidden) break; + _subtitlesHidden = true; final current = state.track.subtitle; - if (current != null && current.id != 'no') { - _hiddenSubtitleTrackId = current.id; - await selectSubtitleTrack(SubtitleTrack.off); + _hiddenSubtitleTrackId = current != null && current.id != SubtitleTrack.off.id ? current.id : null; + if (_hiddenSubtitleTrackId != null) { + await _selectSubtitleTrackNatively(SubtitleTrack.off); } } else { + if (!_subtitlesHidden) break; + _subtitlesHidden = false; final storedId = _hiddenSubtitleTrackId; - if (storedId != null) { - _hiddenSubtitleTrackId = null; - final track = state.tracks.subtitle.firstWhereOrNull((t) => t.id == storedId); - if (track != null) { - await selectSubtitleTrack(track); - } + _hiddenSubtitleTrackId = null; + final track = storedId == null ? null : state.tracks.subtitle.firstWhereOrNull((t) => t.id == storedId); + if (track != null) { + await _selectSubtitleTrackNatively(track); } } break; diff --git a/lib/screens/video_player/parts/companion_remote.dart b/lib/screens/video_player/parts/companion_remote.dart index 9a974585..b5b91426 100644 --- a/lib/screens/video_player/parts/companion_remote.dart +++ b/lib/screens/video_player/parts/companion_remote.dart @@ -109,7 +109,20 @@ extension _VideoPlayerCompanionRemoteMethods on VideoPlayerScreenState { if (!_subtitleCycleDrainActive) unawaited(_drainSubtitleCycles()); return; } - _trackManager?.cycleSubtitleTrack(); + _cycleSubtitleTrackNatively(); + } + + /// Cycle through the native track list, for playback with no source + /// catalog to advance through (downloads, and items whose server exposes no + /// subtitle rows). + /// + /// The manager owns the selection and the server write-back; the committed + /// choice is this screen's, and the episode carry-over reads it, so a cycle + /// that lands on Off has to be recorded here or the next episode inherits + /// the choice this one started with. + void _cycleSubtitleTrackNatively() { + final cycled = _trackManager?.cycleSubtitleTrack(); + if (cycled != null) _rememberNativeSubtitleSelection(cycled); } Future _drainSubtitleCycles() async { @@ -127,7 +140,7 @@ extension _VideoPlayerCompanionRemoteMethods on VideoPlayerScreenState { if (_isOfflinePlayback || sourceTracks.isEmpty) { _pendingSubtitleCycleCount -= advances; for (var i = 0; i < advances; i++) { - _trackManager?.cycleSubtitleTrack(); + _cycleSubtitleTrackNatively(); } continue; } diff --git a/lib/services/track_manager.dart b/lib/services/track_manager.dart index 706f3995..dfdf8ba2 100644 --- a/lib/services/track_manager.dart +++ b/lib/services/track_manager.dart @@ -385,10 +385,12 @@ class TrackManager { // ── Track cycling (remote/keyboard shortcuts) ────────────────────── - /// Cycle to the next subtitle track and save the preference. - void cycleSubtitleTrack() { + /// Cycle to the next subtitle track, save the preference, and return the + /// track now playing so the caller can record it as the committed choice. + /// Returns null when there was nothing to cycle. + SubtitleTrack? cycleSubtitleTrack() { final tracks = player.state.tracks.subtitle.where((t) => t.id != 'auto').toList(); - if (tracks.isEmpty) return; + if (tracks.isEmpty) return null; final current = player.state.track.subtitle; final currentIndex = tracks.indexWhere((t) => t.id == current?.id); @@ -403,6 +405,7 @@ class TrackManager { : 'Subtitles: ${TrackLabelBuilder.subtitleLabel(title: next.title, language: next.language, codec: next.codec, forced: next.isForced, index: nextIndex).joined}'; showMessage?.call(label, duration: const Duration(seconds: 1)); } + return next; } /// Cycle to the next audio track and save the preference. diff --git a/lib/widgets/video_controls/parts/track_controls.dart b/lib/widgets/video_controls/parts/track_controls.dart index 43d4a919..5db6353e 100644 --- a/lib/widgets/video_controls/parts/track_controls.dart +++ b/lib/widgets/video_controls/parts/track_controls.dart @@ -4,11 +4,19 @@ final Expando> _subtitleVisibilityWrites = Expando