mux: add high-level mux_stream driver (layering step 4a)

Add the shared decrypt+mux driver both consumers hand-roll today, as a
library-only API. `mux_stream(input, dest_url, opts, halt, events)` runs
the construct -> headers gate -> open sink -> pump -> finish pipeline:

- MuxInput::{Session, Iso, Url} selects the source. The file/ISO path
  calls the untouched build_iso_pipeline highway (zero added copies —
  the driver reads frames exactly where consumers call stream.read());
  the live path builds a DiscStream; a URL source goes through input().
- chapters:// / json:// metadata sinks short-circuit BEFORE the header
  pump/gate. They write their whole file from the scanned title and need
  no codec headers; the CLI placed this after the gate, so a metadata
  export on a title whose video headers never resolved failed with
  MkvInvalid. Fixed by construction.
- The header gate refuses a stream with no resolved codec_private
  (MkvInvalid); the zero-output gate refuses an empty/undecryptable drain
  (NoStreams); a halt mid-pump yields completed=false, never a success.
- Frames are written through a WRITE_PIPELINE_DEPTH consumer pipeline so
  the latency-bound sink write overlaps the next read.

Add error::is_skippable_title_stub(&io::Error) so consumers can drop the
E7023/E6008 string-match, and DiscSession::take_reader for the live arm.

Driver body unit-tested in isolation via a synthetic Stream against
null:// / chapters:// / json:// sinks (short-circuit, header gate,
zero-output gate, halt, happy path); each gate mutation-verified.
Consumers are NOT migrated yet (steps 4b/4c).
This commit is contained in:
Matthew Jackson
2026-07-24 00:27:47 -07:00
parent f2b7cc9bdd
commit a711ee1d00
5 changed files with 735 additions and 0 deletions
+11
View File
@@ -321,6 +321,17 @@ impl DiscSession {
pub fn into_reader(self) -> Option<Box<dyn SectorSource>> {
self.reader
}
/// Take the staged sector source out of the session by mutable borrow,
/// leaving `None` behind. Used by [`crate::mux::mux_stream`]'s
/// [`MuxInput::Session`](crate::mux::MuxInput::Session) arm, which drives
/// the mux from `&mut DiscSession` and so cannot consume the whole session.
/// A second call (or a call before the reader is staged) returns `None`, and
/// the driver maps that to a clean error rather than a panic (see Q2 of the
/// boundary-audit contract).
pub fn take_reader(&mut self) -> Option<Box<dyn SectorSource>> {
self.reader.take()
}
}
/// Scan an ISO image's structure from a file path, returning the scanned