fix(player): flush mpv buffers on frame-rate startup
This commit is contained in:
@@ -69,37 +69,31 @@ extension _VideoPlayerDisplayMatchingMethods on VideoPlayerScreenState {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshAndroidMpvDecoderAfterFrameRateSwitch({
|
||||
required String reason,
|
||||
Duration? fallbackPosition,
|
||||
}) async {
|
||||
Future<void> _refreshAndroidMpvDecoderAfterFrameRateSwitch({required String reason}) async {
|
||||
final p = player;
|
||||
if (!mounted || !Platform.isAndroid || p == null || p is PlayerAndroid) return;
|
||||
|
||||
final positionMs = p.state.position.inMilliseconds;
|
||||
final fallbackMs = fallbackPosition?.inMilliseconds ?? 0;
|
||||
final targetMs = positionMs > 0 ? positionMs : (fallbackMs > 0 ? fallbackMs : 1);
|
||||
// Subscribe before the seek so the broadcast event isn't dropped when
|
||||
// Subscribe before flushing so the broadcast event isn't dropped when
|
||||
// the restart fires synchronously fast.
|
||||
var timedOut = false;
|
||||
final restartFuture = p.streams.playbackRestart.first.timeout(
|
||||
const Duration(milliseconds: 2500),
|
||||
const Duration(seconds: 4),
|
||||
onTimeout: () {
|
||||
timedOut = true;
|
||||
},
|
||||
);
|
||||
final sw = Stopwatch()..start();
|
||||
try {
|
||||
appLogger.d('Frame rate matching: refreshing Android MPV decoder ($reason, target=${targetMs}ms)');
|
||||
await p.command(['seek', (targetMs / 1000.0).toStringAsFixed(3), 'absolute+exact']);
|
||||
appLogger.d('Frame rate matching: flushing Android MPV buffers ($reason, command=drop-buffers)');
|
||||
await p.command(['drop-buffers']);
|
||||
await restartFuture;
|
||||
appLogger.d(
|
||||
'Frame rate matching: refreshed Android MPV decoder '
|
||||
'($reason, target=${targetMs}ms, waited=${sw.elapsedMilliseconds}ms, '
|
||||
'Frame rate matching: flushed Android MPV buffers '
|
||||
'($reason, waited=${sw.elapsedMilliseconds}ms, '
|
||||
'gate=${timedOut ? 'timeout' : 'playback-restart'})',
|
||||
);
|
||||
} catch (e) {
|
||||
appLogger.w('Failed to refresh Android MPV decoder after frame rate switch ($reason)', error: e);
|
||||
appLogger.w('Failed to flush Android MPV buffers after frame rate switch ($reason)', error: e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -179,16 +179,17 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
|
||||
preKnownFps > 0;
|
||||
final isExoPlayer = player is PlayerAndroid;
|
||||
final isAndroidMpv = Platform.isAndroid && !isExoPlayer;
|
||||
final needsAndroidMpvFrameRateStartup = willAutoSwitch && isAndroidMpv && result.videoUrl != null;
|
||||
var didPreLoadFrameRateSwitch = false;
|
||||
var needsPostOpenFrameRateSwitch = willAutoSwitch;
|
||||
var needsAndroidMpvStartupRefresh = false;
|
||||
var needsPostOpenFrameRateSwitch = willAutoSwitch && !needsAndroidMpvFrameRateStartup;
|
||||
var needsAndroidMpvStartupRefresh = needsAndroidMpvFrameRateStartup;
|
||||
final hasExternalSubs = result.externalSubtitles.isNotEmpty;
|
||||
Future<void>? androidMpvStartupReady;
|
||||
Future<bool>? androidMpvStartupReady;
|
||||
|
||||
// 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) {
|
||||
if (needsAndroidMpvFrameRateStartup) {
|
||||
final delaySec = settingsService.read(SettingsService.displaySwitchDelay);
|
||||
final durationMs = _currentMetadata.durationMs ?? player!.state.duration.inMilliseconds;
|
||||
_suppressMediaPauseDuringFrameRateSwitch = true;
|
||||
@@ -207,8 +208,6 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
|
||||
);
|
||||
if (didPreLoadFrameRateSwitch) {
|
||||
_frameRateMatchingApplied = true;
|
||||
needsPostOpenFrameRateSwitch = false;
|
||||
needsAndroidMpvStartupRefresh = true;
|
||||
}
|
||||
appLogger.d(
|
||||
'Frame rate matching: pre-load MPV switch complete '
|
||||
@@ -217,6 +216,8 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
|
||||
);
|
||||
} catch (e) {
|
||||
appLogger.w('Failed to apply pre-load MPV frame rate matching', error: e);
|
||||
needsPostOpenFrameRateSwitch = true;
|
||||
needsAndroidMpvStartupRefresh = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +230,7 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
|
||||
_hasFirstFrame.value = false;
|
||||
_frameRateRetries = 0;
|
||||
_frameRateMatchingApplied = false;
|
||||
if (didPreLoadFrameRateSwitch) {
|
||||
if (didPreLoadFrameRateSwitch || needsAndroidMpvFrameRateStartup) {
|
||||
_frameRateMatchingApplied = true;
|
||||
}
|
||||
|
||||
@@ -272,13 +273,16 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
|
||||
|
||||
final shouldAutoPlay = !shouldHoldPlaybackStart && (isExoPlayer || !hasExternalSubs);
|
||||
if (needsAndroidMpvStartupRefresh) {
|
||||
appLogger.d('Frame rate matching: opening Android MPV paused for startup decoder refresh');
|
||||
androidMpvStartupReady = player!.streams.playbackRestart.first.timeout(
|
||||
const Duration(seconds: 8),
|
||||
onTimeout: () {
|
||||
appLogger.w('Timed out waiting for Android MPV startup frame before decoder refresh');
|
||||
},
|
||||
);
|
||||
appLogger.d('Frame rate matching: opening Android MPV paused for startup buffer flush');
|
||||
androidMpvStartupReady = player!.streams.playbackRestart.first
|
||||
.then((_) => true)
|
||||
.timeout(
|
||||
const Duration(seconds: 15),
|
||||
onTimeout: () {
|
||||
appLogger.w('Timed out waiting for Android MPV startup frame before buffer flush');
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ExoPlayer: attach external subs at open time so it discovers
|
||||
@@ -442,10 +446,7 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
|
||||
try {
|
||||
didSwitch = await player!.setVideoFrameRate(preKnownFps!, durationMs, extraDelayMs: delaySec * 1000);
|
||||
if (didSwitch) {
|
||||
await _refreshAndroidMpvDecoderAfterFrameRateSwitch(
|
||||
reason: 'post-open frame rate switch',
|
||||
fallbackPosition: resumePosition,
|
||||
);
|
||||
await _refreshAndroidMpvDecoderAfterFrameRateSwitch(reason: 'post-open frame rate switch');
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.w('Failed to apply pre-playback frame rate matching', error: e);
|
||||
@@ -465,20 +466,23 @@ extension _VideoPlayerPlaybackStartMethods on VideoPlayerScreenState {
|
||||
),
|
||||
);
|
||||
} else if (needsAndroidMpvStartupRefresh && mounted && player != null) {
|
||||
appLogger.d('Frame rate matching: waiting for Android MPV startup frame before decoder refresh');
|
||||
await androidMpvStartupReady;
|
||||
appLogger.d('Frame rate matching: waiting for Android MPV startup frame before buffer flush');
|
||||
final startupReady = androidMpvStartupReady == null ? false : await androidMpvStartupReady;
|
||||
if (mounted && player != null) {
|
||||
await _refreshAndroidMpvDecoderAfterFrameRateSwitch(
|
||||
reason: 'pre-load frame rate startup',
|
||||
fallbackPosition: resumePosition,
|
||||
);
|
||||
await resumeAfterStartupGate('startup decoder refresh');
|
||||
if (startupReady) {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
await _refreshAndroidMpvDecoderAfterFrameRateSwitch(reason: 'pre-load frame rate startup');
|
||||
await resumeAfterStartupGate('startup buffer flush');
|
||||
} else {
|
||||
appLogger.w('Frame rate matching: skipping Android MPV buffer flush because startup frame timed out');
|
||||
await resumeAfterStartupGate('startup frame timeout');
|
||||
}
|
||||
}
|
||||
|
||||
unawaited(
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message: 'Android MPV startup decoder refresh after pre-load frame-rate switch',
|
||||
message: 'Android MPV startup buffer flush after pre-load frame-rate switch',
|
||||
category: 'player',
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user