fix(player): resume mpv startup frame after subtitles

This commit is contained in:
edde746
2026-06-15 12:06:04 +02:00
parent 5fc599439e
commit 8bf5dbb379
3 changed files with 31 additions and 8 deletions
@@ -439,14 +439,20 @@ extension _VideoPlayerEpisodeNavigationMethods on VideoPlayerScreenState {
_trackManager = trackManager;
trackManager.cacheExternalSubtitles(result.externalSubtitles);
final resumeForStartupFrame =
frameRatePlan.needsStartupRefresh && externalSubtitlePlan.requiresPostOpenAdd && !wtOwnsStart;
await _applyTracksAfterOpen(
trackManager: trackManager,
externalSubtitlePlan: externalSubtitlePlan,
// Same guard as the start path: don't resume a player a newer flow
// owns, and let a pending startup gate (or Watch Together's group
// start) own the resume instead.
// start) own the resume instead. Android mpv external-subtitle opens
// resume once here so the startup refresh gate can observe a frame.
shouldResumeAfterSubtitleLoad: () =>
!frameRatePlan.holdPlaybackStart && !wtOwnsStart && mounted && player == currentPlayer,
(!frameRatePlan.holdPlaybackStart || resumeForStartupFrame) &&
!wtOwnsStart &&
mounted &&
player == currentPlayer,
applySelectionWhenResumeSkipped: wtOwnsStart && !frameRatePlan.holdPlaybackStart,
);
if (!isCurrentReload()) return true;
@@ -461,6 +467,7 @@ extension _VideoPlayerEpisodeNavigationMethods on VideoPlayerScreenState {
reason: reason,
wtOwnsStart: wtOwnsStart,
),
playbackResumedForStartupFrame: resumeForStartupFrame,
);
if (!isCurrentReload()) return true;
@@ -215,7 +215,17 @@ extension _VideoPlayerOpenMethods on VideoPlayerScreenState {
required SettingsService settingsService,
required _FrameRateStartupPlan plan,
required Future<void> Function(String reason) resumeAfterStartupGate,
bool playbackResumedForStartupFrame = false,
}) async {
Future<void> resumeAfterRefresh(String reason) async {
if (playbackResumedForStartupFrame) {
appLogger.d('Frame rate matching: continuing already-resumed playback after $reason');
await _playWithPlaybackIntent(currentPlayer);
} else {
await resumeAfterStartupGate(reason);
}
}
// Fallback refresh-rate path. The player was opened paused;
// setVideoFrameRate awaits the real display-change event (+ settle +
// user delay) before returning, then we start playback.
@@ -241,7 +251,7 @@ extension _VideoPlayerOpenMethods on VideoPlayerScreenState {
// Always resume — either the switch completed and we want to play,
// or no switch was needed and we need to start playback now that the
// preparation gate has been cleared.
await resumeAfterStartupGate('post-open frame rate switch');
await resumeAfterRefresh('post-open frame rate switch');
unawaited(
Sentry.addBreadcrumb(
@@ -256,10 +266,10 @@ extension _VideoPlayerOpenMethods on VideoPlayerScreenState {
if (startupReady) {
await Future<void>.delayed(const Duration(milliseconds: 100));
await _refreshAndroidMpvDecoderAfterFrameRateSwitch(reason: 'pre-load frame rate startup');
await resumeAfterStartupGate('startup decoder refresh');
await resumeAfterRefresh('startup decoder refresh');
} else {
appLogger.w('Frame rate matching: skipping Android MPV decoder refresh because startup frame timed out');
await resumeAfterStartupGate('startup frame timeout');
await resumeAfterRefresh('startup frame timeout');
}
}
@@ -320,14 +320,19 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
// Store external subtitles for re-use after backend fallback
_trackManager!.cacheExternalSubtitles(result.externalSubtitles);
final resumeForStartupFrame =
frameRatePlan.needsStartupRefresh && externalSubtitlePlan.requiresPostOpenAdd && !wtOwnsStart;
await _applyTracksAfterOpen(
trackManager: _trackManager!,
externalSubtitlePlan: externalSubtitlePlan,
// When a startup gate below owns the resume, skip this one to
// avoid a double-play. Watch Together stays paused for the group
// start, so selection is armed through the resume-skipped branch.
// avoid a double-play. Android mpv external-subtitle opens are the
// exception: after sub-add we must resume once so mpv can produce the
// startup frame that the decoder-refresh gate is waiting for.
// Watch Together stays paused for the group start, so selection is
// armed through the resume-skipped branch.
shouldResumeAfterSubtitleLoad: () =>
!shouldHoldPlaybackStart && !wtOwnsStart && mounted && player == currentPlayer,
(!shouldHoldPlaybackStart || resumeForStartupFrame) && !wtOwnsStart && mounted && player == currentPlayer,
applySelectionWhenResumeSkipped: wtOwnsStart && !shouldHoldPlaybackStart,
);
@@ -342,6 +347,7 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
wtOwnsStart: wtOwnsStart,
wtStartupHold: wtStartupHold,
),
playbackResumedForStartupFrame: resumeForStartupFrame,
);
// Backstop: if the gate never ran its resume path (unmounted race),
// don't leave Watch Together readiness held forever.