From 338294441a35e423f32e751af86cefa585356da8 Mon Sep 17 00:00:00 2001 From: Rufan Date: Sun, 12 Jul 2026 15:45:28 +0200 Subject: [PATCH] fix(reco-io): concat-demuxer seek fails on multi-segment sources Scrubbing/seeking to any timestamp (e.g. entering a frame number) in a chained multi-segment recording (DJI's auto-split 4GB files) failed with "FFmpeg: Invalid seek" for both cameras. Root cause confirmed directly against libavformat (not just inferred): without a `duration` hint per `file` entry, FFmpeg's concat demuxer can't build a seekable index across segments and reports the whole chain as non-seekable - `avformat_seek_file` returns AVERROR(ESPIPE) for *any* target, even one within the first segment. Single-file sources were unaffected, which is why this only showed up once a user picked multiple segments per side. Fix: probe each segment's duration (format-only open, no decoder/hw setup) when building the concat manifest and emit a `duration ` line next to each `file` entry. One-time cost at chain-open time, not per-seek or per-frame. Co-Authored-By: Claude Sonnet 5 --- crates/reco-io/src/ffmpeg/decoder.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/reco-io/src/ffmpeg/decoder.rs b/crates/reco-io/src/ffmpeg/decoder.rs index 1248f460..b154bcaa 100644 --- a/crates/reco-io/src/ffmpeg/decoder.rs +++ b/crates/reco-io/src/ffmpeg/decoder.rs @@ -487,6 +487,23 @@ impl VideoDecoder { for p in paths { writeln!(manifest, "file '{}'", p.display()) .map_err(|e| DecodeError::Ffmpeg(format!("write manifest: {e}")))?; + // Without a `duration` hint, FFmpeg's concat demuxer can't build + // a seekable index across segments and reports the whole chain + // as non-seekable (`avformat_seek_file` -> AVERROR(ESPIPE)) for + // *any* target, even one within the first segment - confirmed + // directly against libavformat, not just inferred. One extra + // cheap format-only probe per segment (no decoder/hw setup) is + // worth it for seeking to actually work. + match probe_duration_secs(p) { + Some(secs) => { + writeln!(manifest, "duration {secs}") + .map_err(|e| DecodeError::Ffmpeg(format!("write manifest: {e}")))?; + } + None => log::warn!( + "Concat demuxer: could not probe duration of {} - seeking across this chain may fail", + p.display() + ), + } } manifest .flush() @@ -1035,6 +1052,15 @@ fn is_hw_frame(frame: &VideoFrame) -> bool { unsafe { !(*frame.as_ptr()).hw_frames_ctx.is_null() } } +/// Probe a single segment's duration for the concat manifest's `duration` +/// hint (see `VideoDecoder::open_chained_impl`). Format-only open - no +/// decoder/hw device setup - since only the container-level duration is +/// needed. +fn probe_duration_secs(path: &Path) -> Option { + let dur = input(path).ok()?.duration(); + (dur > 0).then(|| dur as f64 / f64::from(ffi::AV_TIME_BASE)) +} + /// Pre-created hw device that can be shared across multiple decoders. /// /// D3D11VA textures are device-bound: `CopySubresourceRegion` requires source