diff --git a/crates/reco-gui/src/main.rs b/crates/reco-gui/src/main.rs index 090b6fc8..9288503f 100644 --- a/crates/reco-gui/src/main.rs +++ b/crates/reco-gui/src/main.rs @@ -2801,7 +2801,9 @@ fn main() -> anyhow::Result<()> { let img = s.render_current(); if let (Some(app), Some(img)) = (app_weak.upgrade(), img) { app.set_preview_frame(img); - app.set_current_frame(s.playback.frame_index() as i32); + let fps = s.playback.fps(); + let total = s.playback.total_frames().unwrap_or(0); + sync_frame_display(&app, s.playback.frame_index(), total, fps); } } Ok(false) => {} @@ -2831,7 +2833,9 @@ fn main() -> anyhow::Result<()> { let img = s.render_current(); if let (Some(app), Some(img)) = (app_weak.upgrade(), img) { app.set_preview_frame(img); - app.set_current_frame(s.playback.frame_index() as i32); + let fps = s.playback.fps(); + let total = s.playback.total_frames().unwrap_or(0); + sync_frame_display(&app, s.playback.frame_index(), total, fps); } } Err(e) => log::error!("Step backward error: {e}"), @@ -2948,7 +2952,9 @@ fn main() -> anyhow::Result<()> { let img = s.render_current(); if let (Some(app), Some(img)) = (app_weak.upgrade(), img) { app.set_preview_frame(img); - app.set_current_frame(s.playback.frame_index() as i32); + let fps = s.playback.fps(); + let total = s.playback.total_frames().unwrap_or(0); + sync_frame_display(&app, s.playback.frame_index(), total, fps); } }); @@ -4179,7 +4185,9 @@ fn main() -> anyhow::Result<()> { let img = s.render_current(); if let (Some(app), Some(img)) = (app_weak.upgrade(), img) { app.set_preview_frame(img); - app.set_current_frame(s.playback.frame_index() as i32); + let fps = s.playback.fps(); + let total = s.playback.total_frames().unwrap_or(0); + sync_frame_display(&app, s.playback.frame_index(), total, fps); s.last_render_at = Some(Instant::now()); } } diff --git a/crates/reco-io/src/ffmpeg/decoder.rs b/crates/reco-io/src/ffmpeg/decoder.rs index 2f04f4ad..e8716306 100644 --- a/crates/reco-io/src/ffmpeg/decoder.rs +++ b/crates/reco-io/src/ffmpeg/decoder.rs @@ -241,6 +241,40 @@ impl Drop for VideoDecoder { } } +fn container_duration_secs(path: &Path) -> Result, DecodeError> { + let ictx = input(path)?; + let duration = ictx.duration(); + if duration > 0 { + Ok(Some(duration as f64 / f64::from(ffmpeg::ffi::AV_TIME_BASE))) + } else { + Ok(None) + } +} + +fn concat_seek_durations(paths: &[std::path::PathBuf]) -> Option> { + let mut durations = Vec::with_capacity(paths.len()); + for path in paths { + match container_duration_secs(path) { + Ok(Some(duration)) => durations.push(duration), + Ok(None) => { + log::warn!( + "Concat demuxer: duration unknown for {}; cross-segment seek may be unavailable", + path.display() + ); + return None; + } + Err(e) => { + log::warn!( + "Concat demuxer: failed to probe duration for {} ({e}); cross-segment seek may be unavailable", + path.display() + ); + return None; + } + } + } + Some(durations) +} + impl VideoDecoder { /// Open a video file for decoding. /// @@ -509,12 +543,17 @@ impl VideoDecoder { .suffix(".txt") .tempfile() .map_err(|e| DecodeError::Ffmpeg(format!("concat manifest: {e}")))?; + let durations = concat_seek_durations(paths); writeln!(manifest, "ffconcat version 1.0") .map_err(|e| DecodeError::Ffmpeg(format!("write manifest: {e}")))?; - for p in paths { + for (idx, p) in paths.iter().enumerate() { writeln!(manifest, "file '{}'", p.display()) .map_err(|e| DecodeError::Ffmpeg(format!("write manifest: {e}")))?; + if let Some(durations) = durations.as_ref() { + writeln!(manifest, "duration {:.6}", durations[idx]) + .map_err(|e| DecodeError::Ffmpeg(format!("write manifest: {e}")))?; + } } manifest .flush()