fix(android): stabilize mpv frame-rate startup

This commit is contained in:
edde746
2026-05-08 06:12:04 +02:00
parent 5fe5534bae
commit 65b42bca3f
2 changed files with 70 additions and 15 deletions
@@ -47,6 +47,9 @@ extension _VideoPlayerDisplayMatchingMethods on VideoPlayerScreenState {
}
final didSwitch = await player!.setVideoFrameRate(fps, durationMs, extraDelayMs: delaySec * 1000);
if (didSwitch) {
await _refreshAndroidMpvDecoderAfterFrameRateSwitch();
}
if (mounted && player != null) {
await player!.play();
@@ -66,6 +69,20 @@ extension _VideoPlayerDisplayMatchingMethods on VideoPlayerScreenState {
}
}
Future<void> _refreshAndroidMpvDecoderAfterFrameRateSwitch() async {
final p = player;
if (!mounted || !Platform.isAndroid || p == null || p is PlayerAndroid) return;
final positionMs = p.state.position.inMilliseconds;
final targetMs = positionMs <= 0 ? 1 : positionMs;
try {
await p.command(['seek', (targetMs / 1000.0).toStringAsFixed(3), 'absolute+exact']);
appLogger.d('Frame rate matching: refreshed Android MPV decoder at ${targetMs}ms');
} catch (e) {
appLogger.w('Failed to refresh Android MPV decoder after frame rate switch', error: e);
}
}
/// Clear frame rate matching and restore default display mode
Future<void> _clearFrameRateMatching() async {
if (player == null || !Platform.isAndroid) return;
@@ -167,9 +167,9 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
}
}
// Primary refresh-rate path: when Plex metadata provides an fps and the
// user has frame-rate matching on, open the player paused so the HDMI
// refresh-rate switch can complete before any frame renders.
// 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.
final settingsService = await SettingsService.getInstance();
final preKnownFps = result.mediaInfo?.frameRate;
final willAutoSwitch =
@@ -177,6 +177,40 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
settingsService.read(SettingsService.matchContentFrameRate) &&
preKnownFps != null &&
preKnownFps > 0;
final isExoPlayer = player is PlayerAndroid;
final isAndroidMpv = Platform.isAndroid && !isExoPlayer;
var didPreLoadFrameRateSwitch = false;
var shouldPauseForFrameRateSwitch = willAutoSwitch;
// MPV on Android can decode and present its first paused frame before a
// post-open display switch settles. Switch first when metadata already
// gives us the FPS so MediaCodec starts after the display mode change.
if (willAutoSwitch && isAndroidMpv && result.videoUrl != null) {
final delaySec = settingsService.read(SettingsService.displaySwitchDelay);
final durationMs = _currentMetadata.durationMs ?? player!.state.duration.inMilliseconds;
_suppressMediaPauseDuringFrameRateSwitch = true;
Future.delayed(Duration(seconds: 2 + delaySec + 1), () {
_suppressMediaPauseDuringFrameRateSwitch = false;
});
try {
appLogger.d('Frame rate matching: pre-load MPV switch to ${preKnownFps}fps');
didPreLoadFrameRateSwitch = await player!.setVideoFrameRate(
preKnownFps,
durationMs,
extraDelayMs: delaySec * 1000,
);
if (didPreLoadFrameRateSwitch) {
_frameRateMatchingApplied = true;
shouldPauseForFrameRateSwitch = false;
}
appLogger.d(
'Frame rate matching: pre-load MPV switch complete '
'(switched=$didPreLoadFrameRateSwitch, delay=${delaySec}s)',
);
} catch (e) {
appLogger.w('Failed to apply pre-load MPV frame rate matching', error: e);
}
}
// Open video through Player
if (result.videoUrl != null) {
@@ -184,6 +218,9 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
_hasFirstFrame.value = false;
_frameRateRetries = 0;
_frameRateMatchingApplied = false;
if (didPreLoadFrameRateSwitch) {
_frameRateMatchingApplied = true;
}
// Request audio focus before starting playback (Android)
// This causes other media apps (Spotify, podcasts, etc.) to pause.
@@ -224,14 +261,14 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
}
final hasExternalSubs = result.externalSubtitles.isNotEmpty;
final isExoPlayer = player is PlayerAndroid;
final shouldAutoPlay = !shouldPauseForFrameRateSwitch && (isExoPlayer || !hasExternalSubs);
// ExoPlayer: attach external subs at open time so it discovers
// them in a single prepare() — no media reload needed for selection.
// MPV (all platforms including Android): external subs added after open via sub-add.
await player!.open(
Media(result.videoUrl!, start: resumePosition, headers: streamHeaders),
play: !willAutoSwitch && (isExoPlayer || !hasExternalSubs),
play: shouldAutoPlay,
externalSubtitles: isExoPlayer && hasExternalSubs ? result.externalSubtitles : null,
);
@@ -350,9 +387,9 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
try {
await _trackManager!.addExternalSubtitles(result.externalSubtitles);
} finally {
// When willAutoSwitch the pre-playback refresh-rate block below
// owns the resume, so skip this one to avoid a double-play.
if (!willAutoSwitch) {
// When the post-open refresh-rate block below owns the resume,
// skip this one to avoid a double-play.
if (!shouldPauseForFrameRateSwitch) {
await _trackManager!.resumeAfterSubtitleLoad();
}
}
@@ -362,12 +399,10 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
_trackManager!.applyTrackSelectionWhenReady();
}
// Initiate the HDMI refresh-rate switch BEFORE any frame renders.
// The player was opened paused; setVideoFrameRate awaits the real
// display-change event (+ settle + user delay) before returning, and
// then we start playback — so the first frame the user sees is after
// the switch has settled.
if (willAutoSwitch && mounted && player != null) {
// 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.
if (shouldPauseForFrameRateSwitch && mounted && player != null) {
_frameRateMatchingApplied = true;
final delaySec = settingsService.read(SettingsService.displaySwitchDelay);
final durationMs = _currentMetadata.durationMs ?? player!.state.duration.inMilliseconds;
@@ -377,7 +412,10 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
});
bool didSwitch = false;
try {
didSwitch = await player!.setVideoFrameRate(preKnownFps, durationMs, extraDelayMs: delaySec * 1000);
didSwitch = await player!.setVideoFrameRate(preKnownFps!, durationMs, extraDelayMs: delaySec * 1000);
if (didSwitch) {
await _refreshAndroidMpvDecoderAfterFrameRateSwitch();
}
} catch (e) {
appLogger.w('Failed to apply pre-playback frame rate matching', error: e);
}