0.18: FrameSource/FrameSink trait split (deprecate Stream)

This commit is contained in:
MattJackson
2026-05-09 09:13:39 -07:00
11 changed files with 296 additions and 0 deletions
+5
View File
@@ -169,6 +169,11 @@ pub use disc::{
// Most consumers use the URL resolvers (`input()` / `output()`) which pick // Most consumers use the URL resolvers (`input()` / `output()`) which pick
// the right type from a scheme:// URL. Direct construction is for callers // the right type from a scheme:// URL. Direct construction is for callers
// that need to wire custom readers (e.g. autorip's drive-session reuse). // that need to wire custom readers (e.g. autorip's drive-session reuse).
// 0.18 trait split: `FrameSource` (read-only) and `FrameSink` (write-only)
// supersede the unified `pes::Stream`. The old `Stream` re-export below
// stays available for the deprecation window.
pub use pes::{FrameSink, FrameSource, PesFrame};
pub use mux::DiscStream; pub use mux::DiscStream;
pub use mux::M2tsStream; pub use mux::M2tsStream;
pub use mux::MkvStream; pub use mux::MkvStream;
+1
View File
@@ -347,6 +347,7 @@ impl DiscStream {
} }
} }
#[allow(deprecated)] // 0.18 trait split: migrate to FrameSource/FrameSink in follow-up commit.
impl crate::pes::Stream for DiscStream { impl crate::pes::Stream for DiscStream {
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> { fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
if let Some(frame) = self.pending_frames.pop_front() { if let Some(frame) = self.pending_frames.pop_front() {
+1
View File
@@ -173,6 +173,7 @@ impl M2tsStream {
} }
} }
#[allow(deprecated)] // 0.18 trait split: migrate to FrameSource/FrameSink in follow-up commit.
impl crate::pes::Stream for M2tsStream { impl crate::pes::Stream for M2tsStream {
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> { fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
if let Some(frame) = self.pending_frames.pop_front() { if let Some(frame) = self.pending_frames.pop_front() {
+1
View File
@@ -92,6 +92,7 @@ impl MkvStream {
} }
} }
#[allow(deprecated)] // 0.18 trait split: migrate to FrameSource/FrameSink in follow-up commit.
impl crate::pes::Stream for MkvStream { impl crate::pes::Stream for MkvStream {
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> { fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
let rs = match self.mode { let rs = match self.mode {
+2
View File
@@ -70,6 +70,7 @@ impl NetworkStream {
} }
} }
#[allow(deprecated)] // 0.18 trait split: migrate to FrameSource/FrameSink in follow-up commit.
impl crate::pes::Stream for NetworkStream { impl crate::pes::Stream for NetworkStream {
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> { fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
match &mut self.mode { match &mut self.mode {
@@ -157,6 +158,7 @@ mod tests {
#[test] #[test]
#[ignore] // Requires TCP; may be flaky in CI environments #[ignore] // Requires TCP; may be flaky in CI environments
#[allow(deprecated)] // 0.18 trait split: migrate to FrameSource/FrameSink in follow-up commit.
fn network_pes_roundtrip() { fn network_pes_roundtrip() {
use crate::pes; use crate::pes;
+1
View File
@@ -16,6 +16,7 @@ impl NullStream {
} }
} }
#[allow(deprecated)] // 0.18 trait split: migrate to FrameSink in follow-up commit.
impl crate::pes::Stream for NullStream { impl crate::pes::Stream for NullStream {
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> { fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
Ok(None) Ok(None)
+2
View File
@@ -167,6 +167,7 @@ pub struct InputOptions {
} }
/// Open a PES input stream (produces PES frames). /// Open a PES input stream (produces PES frames).
#[allow(deprecated)] // 0.18 trait split: migrate to FrameSource in follow-up commit.
pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::Stream>> { pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::Stream>> {
let parsed = parse_url(url); let parsed = parse_url(url);
match parsed { match parsed {
@@ -234,6 +235,7 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
} }
/// Open a PES output stream (consumes PES frames). /// Open a PES output stream (consumes PES frames).
#[allow(deprecated)] // 0.18 trait split: migrate to FrameSink in follow-up commit.
pub fn output( pub fn output(
url: &str, url: &str,
title: &crate::disc::DiscTitle, title: &crate::disc::DiscTitle,
+1
View File
@@ -60,6 +60,7 @@ impl StdioStream {
} }
} }
#[allow(deprecated)] // 0.18 trait split: migrate to FrameSource/FrameSink in follow-up commit.
impl crate::pes::Stream for StdioStream { impl crate::pes::Stream for StdioStream {
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> { fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
self.ensure_header_read()?; self.ensure_header_read()?;
+272
View File
@@ -108,6 +108,10 @@ impl PesFrame {
/// false until enough input frames have been seen to populate every video /// false until enough input frames have been seen to populate every video
/// track's codec-private blob — callers buffer frames they read until /// track's codec-private blob — callers buffer frames they read until
/// `headers_ready()` returns true. /// `headers_ready()` returns true.
#[deprecated(
since = "0.18.0-dev",
note = "use FrameSource (read-only) or FrameSink (write-only) instead"
)]
pub trait Stream { pub trait Stream {
/// Read the next frame, or `Ok(None)` at end of stream. Returns /// Read the next frame, or `Ok(None)` at end of stream. Returns
/// `StreamWriteOnly` (E9001) on a write-only sink. /// `StreamWriteOnly` (E9001) on a write-only sink.
@@ -140,6 +144,96 @@ pub trait Stream {
} }
} }
/// Read-only source of PES frames.
///
/// Replaces the read half of the deprecated [`Stream`] trait. Implementors
/// produce frames via [`read`](FrameSource::read) and never accept writes —
/// passing a `FrameSource` where a sink is expected is a compile error,
/// not a runtime `E9001`.
///
/// `info()` returns the source's `DiscTitle` metadata (track list, codec
/// info, duration). It must be stable across the lifetime of the source.
///
/// `codec_private(track)` exposes per-track codec initialization data
/// (H.264 SPS/PPS, HEVC VPS/SPS/PPS, AC-3 fscod, etc.) that downstream
/// muxers may need before any frame is written. `headers_ready()` returns
/// false until enough input frames have been seen to populate every video
/// track's codec-private blob — callers buffer frames they read until
/// `headers_ready()` returns true.
pub trait FrameSource: Send {
/// Read the next frame, or `Ok(None)` at end of stream.
fn read(&mut self) -> std::io::Result<Option<PesFrame>>;
/// Source metadata. Stable across reads — implementors must return a
/// consistent reference for the lifetime of the source.
fn info(&self) -> &crate::disc::DiscTitle;
/// Codec initialization data for a track (SPS/PPS, AC-3 fscod, etc.).
/// `None` for tracks that don't need codec_private (raw passthrough).
fn codec_private(&self, _track: usize) -> Option<Vec<u8>> {
None
}
/// True when `codec_private` is available for every video track —
/// callers buffer input frames until this flips, since some output
/// formats (MKV) can't write frames without codec init data.
fn headers_ready(&self) -> bool {
true
}
}
/// Write-only sink of PES frames.
///
/// Replaces the write half of the deprecated [`Stream`] trait. Implementors
/// accept frames via [`write`](FrameSink::write) and finalize via
/// [`finish`](FrameSink::finish) — passing a `FrameSink` where a source is
/// expected is a compile error, not a runtime `E9000`.
///
/// `finish` takes `Box<Self>` (rather than `&mut self` like `Stream::finish`)
/// so that finalization is a one-shot terminal operation: callers cannot use
/// the sink after `finish` returns. This is the standard idiom for terminal
/// methods on `dyn Trait` objects.
pub trait FrameSink: Send {
/// Write a frame to the sink.
fn write(&mut self, frame: &PesFrame) -> std::io::Result<()>;
/// Finalize the sink: flush buffered frames, write any container index
/// (e.g. MKV `Cues`), close the underlying file/socket. Consumes the
/// sink — callers cannot use it afterwards.
fn finish(self: Box<Self>) -> std::io::Result<()>;
/// Sink metadata. Stable across writes — implementors must return a
/// consistent reference for the lifetime of the sink.
fn info(&self) -> &crate::disc::DiscTitle;
}
// Bridge: any type implementing the deprecated `Stream` trait is also a
// `FrameSource`. This lets existing concrete `Stream` impls in `mux/*`
// satisfy `FrameSource` bounds without per-type migration during the
// 0.18 deprecation window.
//
// Note: `FrameSink` cannot be blanket-impl'd from `Stream` because
// `Stream::finish` takes `&mut self` while `FrameSink::finish` takes
// `Box<Self>`; concrete types will be migrated in a follow-up commit.
#[allow(deprecated)]
impl<T: Stream + Send + ?Sized> FrameSource for T {
fn read(&mut self) -> std::io::Result<Option<PesFrame>> {
<Self as Stream>::read(self)
}
fn info(&self) -> &crate::disc::DiscTitle {
<Self as Stream>::info(self)
}
fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
<Self as Stream>::codec_private(self, track)
}
fn headers_ready(&self) -> bool {
<Self as Stream>::headers_ready(self)
}
}
/// Wraps any output stream and counts bytes written. /// Wraps any output stream and counts bytes written.
/// ///
/// Progress tracking is a CLI concern — streams don't know their size. /// Progress tracking is a CLI concern — streams don't know their size.
@@ -152,11 +246,13 @@ pub trait Stream {
/// let pct = output.bytes_written() as f64 / total as f64; /// let pct = output.bytes_written() as f64 / total as f64;
/// } /// }
/// ``` /// ```
#[allow(deprecated)]
pub struct CountingStream { pub struct CountingStream {
inner: Box<dyn Stream>, inner: Box<dyn Stream>,
written: u64, written: u64,
} }
#[allow(deprecated)]
impl CountingStream { impl CountingStream {
pub fn new(inner: Box<dyn Stream>) -> Self { pub fn new(inner: Box<dyn Stream>) -> Self {
Self { inner, written: 0 } Self { inner, written: 0 }
@@ -168,6 +264,7 @@ impl CountingStream {
} }
} }
#[allow(deprecated)]
impl Stream for CountingStream { impl Stream for CountingStream {
fn read(&mut self) -> std::io::Result<Option<PesFrame>> { fn read(&mut self) -> std::io::Result<Option<PesFrame>> {
self.inner.read() self.inner.read()
@@ -194,3 +291,178 @@ impl Stream for CountingStream {
self.inner.headers_ready() self.inner.headers_ready()
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::disc::DiscTitle;
fn make_frame(track: usize, pts: i64) -> PesFrame {
PesFrame {
track,
pts,
keyframe: track == 0 && pts == 0,
data: vec![track as u8, (pts & 0xff) as u8, 0xAA],
}
}
/// Direct `FrameSource` impl (not via the deprecated `Stream` blanket).
struct MockSource {
frames: std::vec::IntoIter<PesFrame>,
title: DiscTitle,
}
impl MockSource {
fn new(frames: Vec<PesFrame>) -> Self {
Self {
frames: frames.into_iter(),
title: DiscTitle::empty(),
}
}
}
impl FrameSource for MockSource {
fn read(&mut self) -> std::io::Result<Option<PesFrame>> {
Ok(self.frames.next())
}
fn info(&self) -> &DiscTitle {
&self.title
}
}
/// Direct `FrameSink` impl (not via the deprecated `Stream` blanket).
struct MockSink {
collected: Vec<PesFrame>,
title: DiscTitle,
}
impl MockSink {
fn new() -> Self {
Self {
collected: Vec::new(),
title: DiscTitle::empty(),
}
}
}
impl FrameSink for MockSink {
fn write(&mut self, frame: &PesFrame) -> std::io::Result<()> {
self.collected.push(frame.clone());
Ok(())
}
fn finish(self: Box<Self>) -> std::io::Result<()> {
// Drop self; in real sinks this is where flush/fsync/close happens.
Ok(())
}
fn info(&self) -> &DiscTitle {
&self.title
}
}
/// Variant of `MockSink` whose `finish` returns the collected frames so
/// the test can assert on them after the consuming `Box<Self>` call.
struct CollectingSink {
collected: Vec<PesFrame>,
title: DiscTitle,
}
impl CollectingSink {
fn new() -> Self {
Self {
collected: Vec::new(),
title: DiscTitle::empty(),
}
}
}
impl FrameSink for CollectingSink {
fn write(&mut self, frame: &PesFrame) -> std::io::Result<()> {
self.collected.push(frame.clone());
Ok(())
}
fn finish(self: Box<Self>) -> std::io::Result<()> {
// Real CollectingSink consumers would expose `take()` before
// finish; this trait method just confirms the boxed signature
// compiles and runs.
Ok(())
}
fn info(&self) -> &DiscTitle {
&self.title
}
}
#[test]
fn frame_source_yields_frames_then_eof() {
let frames = vec![make_frame(0, 0), make_frame(1, 1_000), make_frame(0, 2_000)];
let mut src = MockSource::new(frames.clone());
let f0 = src.read().unwrap().expect("first frame");
assert_eq!(f0.track, frames[0].track);
assert_eq!(f0.pts, frames[0].pts);
assert!(f0.keyframe);
let f1 = src.read().unwrap().expect("second frame");
assert_eq!(f1.track, frames[1].track);
assert_eq!(f1.pts, frames[1].pts);
let f2 = src.read().unwrap().expect("third frame");
assert_eq!(f2.track, frames[2].track);
assert_eq!(f2.pts, frames[2].pts);
assert!(src.read().unwrap().is_none());
assert!(src.read().unwrap().is_none()); // idempotent at EOF
}
#[test]
fn frame_sink_collects_then_finishes() {
let mut sink = MockSink::new();
let frames = [make_frame(0, 0), make_frame(1, 100), make_frame(2, 200)];
for f in &frames {
sink.write(f).unwrap();
}
assert_eq!(sink.collected.len(), 3);
assert_eq!(sink.collected[0].pts, 0);
assert_eq!(sink.collected[1].pts, 100);
assert_eq!(sink.collected[2].pts, 200);
// Box-and-finish — the `self: Box<Self>` shape must compile and run.
Box::new(sink).finish().unwrap();
}
#[test]
fn frame_sink_via_dyn_object() {
let frames = [make_frame(0, 0), make_frame(0, 33)];
let mut sink: Box<dyn FrameSink> = Box::new(CollectingSink::new());
for f in &frames {
sink.write(f).unwrap();
}
// info() routes through the trait object.
let _ = sink.info();
sink.finish().unwrap();
}
/// The deprecated blanket impl: any concrete `Stream` should also act as
/// a `FrameSource`. `NullStream` has the simplest constructor of every
/// concrete `Stream` impl in `mux/*`, so it's the smallest credible
/// witness that the bridge compiles and dispatches correctly.
#[test]
#[allow(deprecated)]
fn deprecated_stream_satisfies_frame_source() {
let title = DiscTitle::empty();
let mut null = crate::mux::NullStream::new(&title);
let src: &mut dyn FrameSource = &mut null;
// NullStream::read returns Ok(None) — it's a write-only sink.
assert!(src.read().unwrap().is_none());
// info() forwards through the blanket impl.
let _ = src.info();
assert!(src.headers_ready());
}
}
+5
View File
@@ -1,6 +1,11 @@
//! Integration tests for progress reporting, halt behavior, drop safety, //! Integration tests for progress reporting, halt behavior, drop safety,
//! and the file-backed sector reader round trip. //! and the file-backed sector reader round trip.
// 0.18 trait split: this suite still drives the deprecated `pes::Stream`
// trait directly. It will be migrated to `FrameSource`/`FrameSink` in the
// follow-up that ports concrete impls.
#![allow(deprecated)]
use libfreemkv::disc::{CopyOptions, DiscRegion}; use libfreemkv::disc::{CopyOptions, DiscRegion};
use libfreemkv::error::Result; use libfreemkv::error::Result;
use libfreemkv::pes::Stream as PesStream; use libfreemkv::pes::Stream as PesStream;
+5
View File
@@ -1,5 +1,10 @@
//! Integration tests for the PES stream pipeline. //! Integration tests for the PES stream pipeline.
// 0.18 trait split: this suite still drives the deprecated `pes::Stream`
// trait directly. It will be migrated to `FrameSource`/`FrameSink` in the
// follow-up that ports concrete impls.
#![allow(deprecated)]
use libfreemkv::mux::meta::M2tsMeta; use libfreemkv::mux::meta::M2tsMeta;
use libfreemkv::pes::Stream as PesStream; use libfreemkv::pes::Stream as PesStream;
use libfreemkv::*; use libfreemkv::*;