@@ -238,6 +238,9 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
|
|||||||
setOption("opengl-es", "yes")
|
setOption("opengl-es", "yes")
|
||||||
setOption("vd-lavc-film-grain", "cpu")
|
setOption("vd-lavc-film-grain", "cpu")
|
||||||
setOption("ao", "audiotrack,opensles")
|
setOption("ao", "audiotrack,opensles")
|
||||||
|
// Pause on the last frame at EOF instead of unloading the file, so a
|
||||||
|
// seek after the video ends still works (matches Linux/Windows).
|
||||||
|
setOption("keep-open", "yes")
|
||||||
if (displayFpsOverride != null) {
|
if (displayFpsOverride != null) {
|
||||||
setOption("display-fps-override", displayFpsOverride)
|
setOption("display-fps-override", displayFpsOverride)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,12 +99,25 @@ extension _VideoPlayerPlaybackPromptMethods on VideoPlayerScreenState {
|
|||||||
_autoPlayTimer?.cancel();
|
_autoPlayTimer?.cancel();
|
||||||
_unfocusPlayNextPrompt();
|
_unfocusPlayNextPrompt();
|
||||||
_progressTracker?.resumeAfterStoppedReport();
|
_progressTracker?.resumeAfterStoppedReport();
|
||||||
_completionTriggered = false; // Reset so it can trigger again if user seeks near end
|
// Keep _completionTriggered set: playback is still parked inside the
|
||||||
|
// end-of-video window, so clearing it here would let the position listener
|
||||||
|
// re-fire this prompt on the next tick. It is re-armed once playback seeks
|
||||||
|
// back clear of the end region (see the position listener) or new media loads.
|
||||||
_setPlayerState(() {
|
_setPlayerState(() {
|
||||||
_showPlayNextDialog = false;
|
_showPlayNextDialog = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Re-arm the end-of-video latch so Play Next can fire again — but only when no
|
||||||
|
/// prompt is visible and no auto-play countdown is running, so we never clobber
|
||||||
|
/// an active dialog. Callers decide *when* it is safe to re-arm (media reloaded,
|
||||||
|
/// or playback moved back out of the end region).
|
||||||
|
void _rearmCompletionLatch() {
|
||||||
|
if (_completionTriggered && !_showPlayNextDialog && _autoPlayTimer?.isActive != true) {
|
||||||
|
_completionTriggered = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _showStillWatchingDialog() {
|
void _showStillWatchingDialog() {
|
||||||
// Don't show if auto-play dialog is already visible
|
// Don't show if auto-play dialog is already visible
|
||||||
if (_showPlayNextDialog) return;
|
if (_showPlayNextDialog) return;
|
||||||
|
|||||||
@@ -323,9 +323,7 @@ extension _VideoPlayerPlaybackServiceMethods on VideoPlayerScreenState {
|
|||||||
appLogger.i('Network restored while buffering, forcing stream reconnect at ${pos.inSeconds}s');
|
appLogger.i('Network restored while buffering, forcing stream reconnect at ${pos.inSeconds}s');
|
||||||
// Clear any stale completion latch caused by a spurious EOF during the drop,
|
// Clear any stale completion latch caused by a spurious EOF during the drop,
|
||||||
// so the real end-of-file can trigger Play Next after we recover.
|
// so the real end-of-file can trigger Play Next after we recover.
|
||||||
if (_completionTriggered && !_showPlayNextDialog && _autoPlayTimer?.isActive != true) {
|
_rearmCompletionLatch();
|
||||||
_completionTriggered = false;
|
|
||||||
}
|
|
||||||
unawaited(_seekPlayback(pos));
|
unawaited(_seekPlayback(pos));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -352,6 +352,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
|||||||
int _autoPlayCountdown = 5;
|
int _autoPlayCountdown = 5;
|
||||||
bool _completionTriggered = false;
|
bool _completionTriggered = false;
|
||||||
|
|
||||||
|
// End-of-video Play Next thresholds. Fire the prompt within _kPlayNextTriggerMs
|
||||||
|
// of the end; re-arm (allow it to fire again) only once playback is more than
|
||||||
|
// _kPlayNextRearmMs from the end. The gap is hysteresis so a position parked at
|
||||||
|
// the boundary can't oscillate between firing and re-arming.
|
||||||
|
static const int _kPlayNextTriggerMs = 1000;
|
||||||
|
static const int _kPlayNextRearmMs = 2000;
|
||||||
|
|
||||||
late final FocusNode _playNextCancelFocusNode;
|
late final FocusNode _playNextCancelFocusNode;
|
||||||
late final FocusNode _playNextConfirmFocusNode;
|
late final FocusNode _playNextConfirmFocusNode;
|
||||||
|
|
||||||
@@ -881,13 +888,18 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
|||||||
|
|
||||||
_playingSubscription = currentPlayer.streams.playing.listen(_onPlayingStateChanged);
|
_playingSubscription = currentPlayer.streams.playing.listen(_onPlayingStateChanged);
|
||||||
|
|
||||||
// Listen to completion. When mpv emits completed=false (file-loaded after a
|
|
||||||
// reconnect-seek or fresh open), clear a stale _completionTriggered so the
|
|
||||||
// real end-of-file can still show Play Next. Guarded against clobbering an
|
|
||||||
// active dialog or running auto-play countdown.
|
|
||||||
_completedSubscription = currentPlayer.streams.completed.listen((done) {
|
_completedSubscription = currentPlayer.streams.completed.listen((done) {
|
||||||
if (!done && _completionTriggered && !_showPlayNextDialog && _autoPlayTimer?.isActive != true) {
|
// completed=false means a file (re)loaded after a reconnect-seek or fresh
|
||||||
_completionTriggered = false;
|
// open — re-arm the end-of-video latch so the real EOF can still show Play
|
||||||
|
// Next. But only when playback is clear of the end region: a stray
|
||||||
|
// completed=false while parked at EOF must NOT re-arm, or the position
|
||||||
|
// listener would immediately re-fire the Play Next prompt.
|
||||||
|
if (!done) {
|
||||||
|
final durMs = currentPlayer.state.duration.inMilliseconds;
|
||||||
|
final posMs = currentPlayer.state.position.inMilliseconds;
|
||||||
|
if (durMs <= 0 || posMs < durMs - _kPlayNextRearmMs) {
|
||||||
|
_rearmCompletionLatch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_onVideoCompleted(done);
|
_onVideoCompleted(done);
|
||||||
});
|
});
|
||||||
@@ -973,11 +985,16 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
|
|||||||
}
|
}
|
||||||
|
|
||||||
final duration = activePlayer.state.duration;
|
final duration = activePlayer.state.duration;
|
||||||
if (duration.inMilliseconds > 0 &&
|
if (duration.inMilliseconds > 0) {
|
||||||
position.inMilliseconds >= duration.inMilliseconds - 1000 &&
|
if (position.inMilliseconds >= duration.inMilliseconds - _kPlayNextTriggerMs &&
|
||||||
!_showPlayNextDialog &&
|
!_showPlayNextDialog &&
|
||||||
!_completionTriggered) {
|
!_completionTriggered) {
|
||||||
_onVideoCompleted(true);
|
_onVideoCompleted(true);
|
||||||
|
} else if (position.inMilliseconds < duration.inMilliseconds - _kPlayNextRearmMs) {
|
||||||
|
// Seeked back out of the end region after dismissing Play Next — re-arm
|
||||||
|
// so the prompt can fire again if the user returns to the end.
|
||||||
|
_rearmCompletionLatch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -708,6 +708,9 @@ class MpvPlayerCoreBase: NSObject {
|
|||||||
checkError(mpv_set_option_string(mpv, "hwdec-codecs", "all"))
|
checkError(mpv_set_option_string(mpv, "hwdec-codecs", "all"))
|
||||||
checkError(mpv_set_option_string(mpv, "hwdec-software-fallback", "yes"))
|
checkError(mpv_set_option_string(mpv, "hwdec-software-fallback", "yes"))
|
||||||
checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "auto"))
|
checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "auto"))
|
||||||
|
// Pause on the last frame at EOF instead of unloading the file, so seeking
|
||||||
|
// back after the video ends still works (matches Linux/Windows).
|
||||||
|
checkError(mpv_set_option_string(mpv, "keep-open", "yes"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
|||||||
Reference in New Issue
Block a user