mux: reconcile fvi:// video-index sink onto rc6
Port + adapt the freemkv native per-picture video index (FVI) from the
old feat/fvi-sink branch onto rc6's codec-agnostic PictureInfo model.
This is a surgical adaptation, not a merge.
Adaptations (fvi_sink.rs, videomap.rs, tests/fvi_pipeline.rs):
- Retarget from the removed crate::mux::codec::mpeg2::PictureInfo (raw
public fields) to rc6's authoritative crate::mux::codec::PictureInfo
in codec/coding.rs, via its accessors.
- type from coding_type() -> CodingType{I,P,B}; emitted for ANY frame
that carries coding (every video codec now fills it), with the
keyframe-flag I/P fallback only when coding is absent.
- Replace the mpeg2-only tff/rff/progressive members with codec-agnostic
members derived through the accessors: field_order (tff/bff/progressive)
and progressive, emitted ONLY when the codec measured the signal
(Option::Some) and omitted otherwise; plus nb_fields.
- Test fixtures rebuilt via PictureInfo::mpeg2(CodingType, Mpeg2Coding{..})
/ coding_type_only(..); added measured_cicp: None to VideoStream
literals for rc6's struct.
Honesty decision (key / random-access):
- The codec-agnostic PictureInfo carries NO GOP-closure (no closed_gop /
gop_start), so key is set from the frame's intra / decode-restart flag
(frame.keyframe == coding.keyframe() for video), NOT a fabricated
clean-RAP claim. The old gop member is honestly omitted. FVI_FORMAT.md
is updated to document this as a limitation: key is an intra picture /
parser-flagged decode-restart point; MPEG-2 open-GOP clean-RAP precision
(closed_gop) is not currently distinguished. §7.1 rewritten for the
new field_order/progressive/nb_fields members.
Wiring:
- mux/mod.rs: pub(crate) mod fvi_sink; pub(crate) mod videomap
(#[allow(dead_code)] on videomap — the VideoMap accumulator is staged
for side-channel reuse, sink builds records directly); pub use
fvi_sink::FviSink.
- mux/resolve.rs: add the fvi:// output scheme to StreamUrl, parse_url,
scheme(), path_str(), input() (write-only reject) and output()
(constructs FviSink), mirroring the mkv:///demux:// patterns.
Provenance fix surfaced by the end-to-end test:
- pipelined_stream::consume_ps was dropping the PS demuxer's byte-exact
source stamp (source: None) when rebuilding PesPacket, so PS/DVD-path
frames reached the mux/index with no provenance (FVI src null). Carry
ps.source through, matching the TS path; the real-pipeline fvi test now
sees the stamped src sectors.
Gate: cargo +1.86 fmt + clippy --lib -D warnings clean; cargo +1.86 test
--lib (2182 passed) and --test fvi_pipeline (2 passed); precommit.sh
libfreemkv green.
This commit is contained in:
+29
-1
@@ -12,6 +12,7 @@
|
||||
//! | network:// | Yes (listen) | Yes (connect) | host:port (required) |
|
||||
//! | stdio:// | Yes (stdin) | Yes (stdout) | empty |
|
||||
//! | null:// | -- | Yes | empty |
|
||||
//! | fvi:// | -- | Yes | file path (required) — per-picture video index |
|
||||
//!
|
||||
//! Bare paths without a scheme are rejected.
|
||||
//! For disc→ISO (raw sector copy), use `Disc::copy()` instead.
|
||||
@@ -60,6 +61,10 @@ pub enum StreamUrl {
|
||||
/// chapters + delay metadata). Like `dir://` it targets a directory; the
|
||||
/// CLI constructs the `DemuxSink` with full options before the mux loop.
|
||||
Demux { dir: PathBuf },
|
||||
/// freemkv native per-picture video index (`fvi://`). A write-only PES sink
|
||||
/// that emits one JSON-Lines record per coded picture of the title's primary
|
||||
/// video track to a `.fvi` file (normative spec `docs/FVI_FORMAT.md`).
|
||||
Fvi { path: PathBuf },
|
||||
/// Unrecognized URL.
|
||||
Unknown { raw: String },
|
||||
}
|
||||
@@ -77,6 +82,7 @@ impl StreamUrl {
|
||||
StreamUrl::Dir { .. } => "dir",
|
||||
StreamUrl::Null => "null",
|
||||
StreamUrl::Demux { .. } => "demux",
|
||||
StreamUrl::Fvi { .. } => "fvi",
|
||||
StreamUrl::Unknown { .. } => "unknown",
|
||||
}
|
||||
}
|
||||
@@ -90,7 +96,8 @@ impl StreamUrl {
|
||||
| StreamUrl::Mkv { path }
|
||||
| StreamUrl::Iso { path }
|
||||
| StreamUrl::Dir { path }
|
||||
| StreamUrl::Demux { dir: path } => path.to_str().unwrap_or(""),
|
||||
| StreamUrl::Demux { dir: path }
|
||||
| StreamUrl::Fvi { path } => path.to_str().unwrap_or(""),
|
||||
StreamUrl::Network { addr } => addr,
|
||||
StreamUrl::Stdio | StreamUrl::Null => "",
|
||||
StreamUrl::Unknown { raw } => raw,
|
||||
@@ -163,6 +170,11 @@ pub fn parse_url(url: &str) -> StreamUrl {
|
||||
dir: PathBuf::from(rest),
|
||||
};
|
||||
}
|
||||
if let Some(rest) = url.strip_prefix("fvi://") {
|
||||
return StreamUrl::Fvi {
|
||||
path: PathBuf::from(rest),
|
||||
};
|
||||
}
|
||||
StreamUrl::Unknown {
|
||||
raw: url.to_string(),
|
||||
}
|
||||
@@ -403,6 +415,8 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
|
||||
StreamUrl::Null => Err(crate::error::Error::StreamWriteOnly.into()),
|
||||
// `demux://` is an output-only sink (per-track ES files); never a source.
|
||||
StreamUrl::Demux { .. } => Err(crate::error::Error::StreamWriteOnly.into()),
|
||||
// `fvi://` is an output-only sink (per-picture video index); never a source.
|
||||
StreamUrl::Fvi { .. } => Err(crate::error::Error::StreamWriteOnly.into()),
|
||||
StreamUrl::Unknown { ref raw } => {
|
||||
Err(crate::error::Error::StreamUrlInvalid { url: raw.clone() }.into())
|
||||
}
|
||||
@@ -479,6 +493,20 @@ pub fn output(
|
||||
dir, title, &opts,
|
||||
)?))
|
||||
}
|
||||
// `fvi://` writes the per-picture video index (`docs/FVI_FORMAT.md`).
|
||||
// The bare `output()` arm records the resolver path as the provenance
|
||||
// `source.path` and defaults the title index to 0 (the resolver carries
|
||||
// no title-index context — the CLI follow-up passes the real medium /
|
||||
// title via `FviSink::create_with_source`).
|
||||
StreamUrl::Fvi { ref path } => {
|
||||
validate_file_path(path, "fvi")?;
|
||||
Ok(Box::new(super::fvi_sink::FviSink::create(
|
||||
path,
|
||||
title,
|
||||
path.to_string_lossy().into_owned(),
|
||||
0,
|
||||
)?))
|
||||
}
|
||||
StreamUrl::Unknown { ref raw } => {
|
||||
Err(crate::error::Error::StreamUrlInvalid { url: raw.clone() }.into())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user