From 27431fc55421bfb5f2bd7733b7309becccef33aa Mon Sep 17 00:00:00 2001 From: Styly Date: Tue, 13 Oct 2026 21:44:33 +0100 Subject: [PATCH 1/2] Keep videos open at their end and survive failed seeks mpv unloaded a file when it finished, so seeking or replaying afterwards failed. fxcode stored that failure as a fatal player error, which ended the whole render loop. Keep the file open at its end without pausing, and treat a failed seek as a no-op. Co-Authored-By: Claude Opus 5.5 --- src/mpv.rs | 74 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 19 deletions(-) diff --git a/src/mpv.rs b/src/mpv.rs index d80fc9f..77dd954 100644 --- a/src/mpv.rs +++ b/src/mpv.rs @@ -281,17 +281,19 @@ impl Player { let encoded_capacity = encoded_capacity(frame)?; let handle = NonNull::new(unsafe { mpv_create() }) .ok_or_else(|| io::Error::other("mpv_create failed"))?; - if let Err(error) = mpv_result(unsafe { - mpv_set_option_string(handle.as_ptr(), c"vo".as_ptr(), c"libmpv".as_ptr()) - }) { - unsafe { mpv_terminate_destroy(handle.as_ptr()) }; - return Err(error); - } - if let Err(error) = mpv_result(unsafe { - mpv_set_option_string(handle.as_ptr(), c"profile".as_ptr(), c"sw-fast".as_ptr()) - }) { - unsafe { mpv_terminate_destroy(handle.as_ptr()) }; - return Err(error); + // Keep the file open at its end, so seeking back and replaying still work. + for (name, value) in [ + (c"vo", c"libmpv"), + (c"profile", c"sw-fast"), + (c"keep-open", c"yes"), + (c"keep-open-pause", c"no"), + ] { + if let Err(error) = mpv_result(unsafe { + mpv_set_option_string(handle.as_ptr(), name.as_ptr(), value.as_ptr()) + }) { + unsafe { mpv_terminate_destroy(handle.as_ptr()) }; + return Err(error); + } } if let Err(error) = mpv_result(unsafe { mpv_initialize(handle.as_ptr()) }) { unsafe { mpv_terminate_destroy(handle.as_ptr()) }; @@ -377,21 +379,18 @@ impl Player { Err(error) => thread_error.store(error, Ordering::Release), } } + // A seek can fail, such as before the file loads; playback goes on. let seek_steps = thread_control.seek_steps.swap(0, Ordering::AcqRel); - if seek_steps != 0 - && let Err(error) = thread_handle.seek(seek_steps) - { - thread_error.store(error, Ordering::Release); + if seek_steps != 0 { + let _ = thread_handle.seek(seek_steps); } let seek_target = f64::from_bits( thread_control .seek_target .swap((-1.0_f64).to_bits(), Ordering::AcqRel), ); - if seek_target >= 0.0 - && let Err(error) = thread_handle.set_position(seek_target) - { - thread_error.store(error, Ordering::Release); + if seek_target >= 0.0 { + let _ = thread_handle.set_position(seek_target); } if last_status.elapsed() >= Duration::from_millis(100) { if let Ok(position) = thread_handle.get_double(c"time-pos") { @@ -942,4 +941,41 @@ mod tests { } panic!("libmpv did not report the bad media"); } + #[test] + #[ignore = "requires FXCODE_TEST_VIDEO"] + fn seeking_past_the_end_keeps_the_player_usable() { + let path = std::env::var_os("FXCODE_TEST_VIDEO").expect("set FXCODE_TEST_VIDEO"); + let video = Video::from_path(path).unwrap(); + let frame = Frame::new( + Rect { + x: 0, + y: 0, + w: 8, + h: 4, + }, + TerminalSize::default(), + 0, + ) + .unwrap(); + let mut player = Player::new(&video, frame, 1).unwrap(); + let mut wait = |done: &dyn Fn() -> bool| { + for _ in 0..400 { + player.render().unwrap(); + if done() { + return; + } + std::thread::sleep(std::time::Duration::from_millis(5)); + } + panic!("libmpv did not get there"); + }; + let controls = video.controls(); + wait(&|| controls.duration() > 0.0); + for _ in 0..40 { + video.seek_forward(); + } + wait(&|| controls.position() >= controls.duration() - 0.5); + video.seek_forward(); + controls.seek_to(0.0); + wait(&|| controls.position() < 1.0); + } } From 086ebb0acf47557bd1b15a8baafffe2a058b1f33 Mon Sep 17 00:00:00 2001 From: Styly Date: Wed, 14 Oct 2026 12:30:40 +0100 Subject: [PATCH 2/2] Check the return seek against a position libmpv reports seek_to stores the target as the position right away, so the regression test passed before the control thread applied the seek, and even when set_position was never called. Replace that optimistic value with a sentinel, so only a time-pos refresh from libmpv can satisfy the check. Co-Authored-By: Claude Opus 5.5 --- src/mpv.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/mpv.rs b/src/mpv.rs index 77dd954..29b5964 100644 --- a/src/mpv.rs +++ b/src/mpv.rs @@ -976,6 +976,11 @@ mod tests { wait(&|| controls.position() >= controls.duration() - 0.5); video.seek_forward(); controls.seek_to(0.0); + // Replace the optimistic position, so only a libmpv report can pass. + controls + .inner + .position + .store(f64::INFINITY.to_bits(), Ordering::Release); wait(&|| controls.position() < 1.0); } }