diff --git a/src/lib.rs b/src/lib.rs
index e474444..a243836 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -169,6 +169,11 @@ pub use disc::{
// Most consumers use the URL resolvers (`input()` / `output()`) which pick
// 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).
+// 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::M2tsStream;
pub use mux::MkvStream;
diff --git a/src/mux/disc.rs b/src/mux/disc.rs
index 04e61c3..95b9c16 100644
--- a/src/mux/disc.rs
+++ b/src/mux/disc.rs
@@ -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 {
fn read(&mut self) -> io::Result> {
if let Some(frame) = self.pending_frames.pop_front() {
diff --git a/src/mux/m2ts.rs b/src/mux/m2ts.rs
index 1cf48bd..513a762 100644
--- a/src/mux/m2ts.rs
+++ b/src/mux/m2ts.rs
@@ -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 {
fn read(&mut self) -> io::Result > {
if let Some(frame) = self.pending_frames.pop_front() {
diff --git a/src/mux/mkvstream.rs b/src/mux/mkvstream.rs
index 6133607..3ccb027 100644
--- a/src/mux/mkvstream.rs
+++ b/src/mux/mkvstream.rs
@@ -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 {
fn read(&mut self) -> io::Result > {
let rs = match self.mode {
diff --git a/src/mux/network.rs b/src/mux/network.rs
index c69e019..265dad9 100644
--- a/src/mux/network.rs
+++ b/src/mux/network.rs
@@ -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 {
fn read(&mut self) -> io::Result > {
match &mut self.mode {
@@ -157,6 +158,7 @@ mod tests {
#[test]
#[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() {
use crate::pes;
diff --git a/src/mux/null.rs b/src/mux/null.rs
index c961e38..b547bc9 100644
--- a/src/mux/null.rs
+++ b/src/mux/null.rs
@@ -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 {
fn read(&mut self) -> io::Result > {
Ok(None)
diff --git a/src/mux/resolve.rs b/src/mux/resolve.rs
index 7bf921a..4effbb4 100644
--- a/src/mux/resolve.rs
+++ b/src/mux/resolve.rs
@@ -167,6 +167,7 @@ pub struct InputOptions {
}
/// 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> {
let parsed = parse_url(url);
match parsed {
@@ -234,6 +235,7 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result io::Result> {
self.ensure_header_read()?;
diff --git a/src/pes.rs b/src/pes.rs
index 65a5754..97ef31d 100644
--- a/src/pes.rs
+++ b/src/pes.rs
@@ -108,6 +108,10 @@ impl PesFrame {
/// 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.
+#[deprecated(
+ since = "0.18.0-dev",
+ note = "use FrameSource (read-only) or FrameSink (write-only) instead"
+)]
pub trait Stream {
/// Read the next frame, or `Ok(None)` at end of stream. Returns
/// `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 >;
+
+ /// 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> {
+ 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` (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) -> 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`; concrete types will be migrated in a follow-up commit.
+#[allow(deprecated)]
+impl FrameSource for T {
+ fn read(&mut self) -> std::io::Result> {
+ ::read(self)
+ }
+
+ fn info(&self) -> &crate::disc::DiscTitle {
+ ::info(self)
+ }
+
+ fn codec_private(&self, track: usize) -> Option> {
+ ::codec_private(self, track)
+ }
+
+ fn headers_ready(&self) -> bool {
+ ::headers_ready(self)
+ }
+}
+
/// Wraps any output stream and counts bytes written.
///
/// 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;
/// }
/// ```
+#[allow(deprecated)]
pub struct CountingStream {
inner: Box,
written: u64,
}
+#[allow(deprecated)]
impl CountingStream {
pub fn new(inner: Box) -> Self {
Self { inner, written: 0 }
@@ -168,6 +264,7 @@ impl CountingStream {
}
}
+#[allow(deprecated)]
impl Stream for CountingStream {
fn read(&mut self) -> std::io::Result> {
self.inner.read()
@@ -194,3 +291,178 @@ impl Stream for CountingStream {
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,
+ title: DiscTitle,
+ }
+
+ impl MockSource {
+ fn new(frames: Vec) -> Self {
+ Self {
+ frames: frames.into_iter(),
+ title: DiscTitle::empty(),
+ }
+ }
+ }
+
+ impl FrameSource for MockSource {
+ fn read(&mut self) -> std::io::Result> {
+ Ok(self.frames.next())
+ }
+
+ fn info(&self) -> &DiscTitle {
+ &self.title
+ }
+ }
+
+ /// Direct `FrameSink` impl (not via the deprecated `Stream` blanket).
+ struct MockSink {
+ collected: Vec,
+ 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) -> 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` call.
+ struct CollectingSink {
+ collected: Vec,
+ 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) -> 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` 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 = 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());
+ }
+}
diff --git a/tests/integration_progress_and_halt.rs b/tests/integration_progress_and_halt.rs
index 4e4cd8c..75c9624 100644
--- a/tests/integration_progress_and_halt.rs
+++ b/tests/integration_progress_and_halt.rs
@@ -1,6 +1,11 @@
//! Integration tests for progress reporting, halt behavior, drop safety,
//! 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::error::Result;
use libfreemkv::pes::Stream as PesStream;
diff --git a/tests/streams.rs b/tests/streams.rs
index 8f339e1..fa0a01e 100644
--- a/tests/streams.rs
+++ b/tests/streams.rs
@@ -1,5 +1,10 @@
//! 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::pes::Stream as PesStream;
use libfreemkv::*;