fix(player): switch refresh rate before ExoPlayer open

This commit is contained in:
edde746
2026-05-31 00:28:54 +02:00
parent 0bb085c70c
commit 63ab80fa82
@@ -187,9 +187,9 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
}
_playbackContext = playbackContext;
// Primary refresh-rate path: when metadata provides FPS, Android MPV can
// switch before `loadfile`; ExoPlayer and MPV fallback cases still open
// paused and switch before visible playback starts.
// Primary refresh-rate path: when metadata provides FPS, Android players
// can switch before creating decoders. MPV still needs a startup refresh
// when MediaCodec has already produced its first paused frame.
final settingsService = await SettingsService.getInstance();
if (!_isCurrentPlaybackGeneration(playbackGeneration, currentPlayer)) return;
final displayCriteria = result.mediaInfo?.displayCriteria;
@@ -201,12 +201,29 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
preKnownFps > 0;
final isExoPlayer = currentPlayer is PlayerAndroid;
final isAndroidMpv = Platform.isAndroid && !isExoPlayer;
final needsExoPlayerFrameRateStartup = willAutoSwitch && isExoPlayer && result.videoUrl != null;
final needsAndroidMpvFrameRateStartup = willAutoSwitch && isAndroidMpv && result.videoUrl != null;
var didPreLoadFrameRateSwitch = false;
var needsPostOpenFrameRateSwitch = willAutoSwitch && !needsAndroidMpvFrameRateStartup;
var didPreOpenExoFrameRateSwitch = false;
var preOpenExoFrameRateHandled = false;
var needsPostOpenFrameRateSwitch =
willAutoSwitch && !needsAndroidMpvFrameRateStartup && !needsExoPlayerFrameRateStartup;
var needsAndroidMpvStartupRefresh = false;
final hasExternalSubs = result.externalSubtitles.isNotEmpty;
Future<bool>? androidMpvStartupReady;
var audioFocusReady = false;
Future<void> ensureAudioFocus() async {
if (audioFocusReady) return;
final focusFuture = _audioFocusFuture;
if (focusFuture != null) {
await focusFuture;
_audioFocusFuture = null;
} else {
await currentPlayer.requestAudioFocus();
}
audioFocusReady = true;
}
// MPV on Android can decode and present its first paused frame before a
// post-open display switch settles. Switch first when metadata already
@@ -245,6 +262,41 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
}
}
// ExoPlayer prepares AudioTrack during open() even when opened paused.
// On Shield/AVR chains, switching HDMI refresh rate after that can break
// direct passthrough, so switch before ExoPlayer creates renderers.
if (needsExoPlayerFrameRateStartup) {
final delaySec = settingsService.read(SettingsService.displaySwitchDelay);
final durationMs = _currentMetadata.durationMs ?? currentPlayer.state.duration.inMilliseconds;
_suppressMediaPauseDuringFrameRateSwitch = true;
Future.delayed(Duration(seconds: 2 + delaySec + 1), () {
_suppressMediaPauseDuringFrameRateSwitch = false;
});
try {
await ensureAudioFocus();
if (!mounted || player != currentPlayer) return;
appLogger.d(
'Frame rate matching: pre-open ExoPlayer switch to ${preKnownFps}fps '
'(duration: ${durationMs}ms, delay=${delaySec}s)',
);
didPreOpenExoFrameRateSwitch = await currentPlayer.setVideoFrameRate(
preKnownFps,
durationMs,
extraDelayMs: delaySec * 1000,
);
if (!mounted || player != currentPlayer) return;
preOpenExoFrameRateHandled = true;
appLogger.d(
'Frame rate matching: pre-open ExoPlayer switch complete '
'(switched=$didPreOpenExoFrameRateSwitch, delay=${delaySec}s)',
);
} catch (e) {
appLogger.w('Failed to apply pre-open ExoPlayer frame rate matching', error: e);
needsPostOpenFrameRateSwitch = true;
preOpenExoFrameRateHandled = false;
}
}
final shouldHoldPlaybackStart = needsPostOpenFrameRateSwitch || needsAndroidMpvStartupRefresh;
Duration? resumePosition;
@@ -254,7 +306,7 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
_hasFirstFrame.value = false;
_frameRateRetries = 0;
_frameRateMatchingApplied = false;
if (didPreLoadFrameRateSwitch || needsAndroidMpvFrameRateStartup) {
if (didPreLoadFrameRateSwitch || needsAndroidMpvFrameRateStartup || preOpenExoFrameRateHandled) {
_frameRateMatchingApplied = true;
}
@@ -262,12 +314,7 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
// This causes other media apps (Spotify, podcasts, etc.) to pause.
// Fired in parallel with MPV setup in `_initializePlayer`; we await
// the in-flight future here (usually already resolved).
if (_audioFocusFuture != null) {
await _audioFocusFuture;
_audioFocusFuture = null;
} else {
await currentPlayer.requestAudioFocus();
}
await ensureAudioFocus();
if (!_isCurrentPlaybackGeneration(playbackGeneration, currentPlayer)) return;
// Pass resume position if available.