Audit pass against the CLAUDE.md "no English text in library code" rule.
Found 9 call sites that violated the contract by stuffing English into
io::Error::new(kind, "…") or by abusing Error::DeviceNotFound { path }
as a free-form description field. Each is now a typed Error variant.
New variants and codes: ScsiInterfaceUnavailable (E1004), DeviceLocked
(E1005), IoKitPluginFailed (E1006), UnsupportedPlatform (E2003),
PlatformNotImplemented (E2004), MapfileInvalid (E6011), DiscUrlNotDirect
(E9009).
labels::apply() previously pushed Commentary/Descriptive/Score/IME and
" (Secondary)" English literals into AudioStream.label, leaking into
MKV titles + autorip UI. AudioStream now exposes structured `purpose:
LabelPurpose`, SubtitleStream `qualifier: LabelQualifier`. Callers
translate to localized text. label keeps codec-formatting only.
API hygiene: 11 mux/* modules dropped from `pub` to `pub(crate)` —
their *types* are still re-exported from lib.rs, but the modules were
leaking low-level EBML/TS/network primitives. Stream trait gets a real
rustdoc explaining read-vs-write split. lib.rs grouped re-exports into
documented sections. ScanOptions::with_keydb() removed (one-method-per-
action rule); use struct literal.
Dead-code sweep: removed lookahead.rs (orphan, never declared as mod),
tsreader.rs (TsDemuxReader unused), ebml::{write_int,read_vint,SEEK_*},
ts::{scan_first/last_pts,scan_duration,SCAN_HEAD/TAIL_SIZE,take/set_
remainder}, MkvMuxer codec_private_slots/filled fields and
fill_codec_private method (deferred-codecPrivate path never used since
the v0.10 PES rewrite). cargo clippy --all-targets -D warnings clean.
Tests: new error::tests for variant codes + Display "no English" guard +
io::ErrorKind mapping. 233 lib tests, all green (was 230).
Breaking: ScanOptions::with_keydb removed; mux/* modules pub(crate);
AudioStream and SubtitleStream gained required fields; UnsupportedDrive
{ product_revision: "Renesas not yet implemented" } no longer produced
(use PlatformNotImplemented).
197 lines
7.1 KiB
Rust
197 lines
7.1 KiB
Rust
//! Stream — read PES frames in, write PES frames out.
|
|
//!
|
|
//! A stream is a stream. You read() from it or write() to it.
|
|
//! The stream handles its own format internally.
|
|
//!
|
|
//! disc.read() → PES frame (sectors → decrypt → demux internally)
|
|
//! mkv.write(frame) → MKV file (mux internally)
|
|
|
|
/// One frame of elementary stream data.
|
|
#[derive(Debug, Clone)]
|
|
pub struct PesFrame {
|
|
/// Track index (0-based, matches stream info track order).
|
|
pub track: usize,
|
|
/// Presentation timestamp in nanoseconds.
|
|
pub pts: i64,
|
|
/// True if this is a keyframe (IDR for video).
|
|
pub keyframe: bool,
|
|
/// Raw elementary stream data (NAL units, audio samples, etc).
|
|
pub data: Vec<u8>,
|
|
}
|
|
|
|
impl PesFrame {
|
|
/// Serialize to bytes: track(1) | pts(8) | keyframe(1) | len(4) | data
|
|
pub fn serialize(&self, w: &mut dyn std::io::Write) -> std::io::Result<()> {
|
|
if self.track > 255 {
|
|
return Err(crate::error::Error::PesInvalidMagic.into());
|
|
}
|
|
if self.data.len() > u32::MAX as usize {
|
|
return Err(crate::error::Error::PesFrameTooLarge {
|
|
size: self.data.len(),
|
|
}
|
|
.into());
|
|
}
|
|
w.write_all(&[self.track as u8])?;
|
|
w.write_all(&self.pts.to_le_bytes())?;
|
|
w.write_all(&[if self.keyframe { 1 } else { 0 }])?;
|
|
w.write_all(&(self.data.len() as u32).to_le_bytes())?;
|
|
w.write_all(&self.data)
|
|
}
|
|
|
|
/// Deserialize from bytes. Returns None at EOF.
|
|
pub fn deserialize(r: &mut dyn std::io::Read) -> std::io::Result<Option<Self>> {
|
|
const MAX_FRAME_SIZE: usize = 256 * 1024 * 1024; // 256 MB
|
|
|
|
let mut header = [0u8; 14]; // 1 + 8 + 1 + 4
|
|
match r.read_exact(&mut header) {
|
|
Ok(_) => {}
|
|
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
|
|
Err(e) => return Err(e),
|
|
}
|
|
let track = header[0] as usize;
|
|
let pts = i64::from_le_bytes([
|
|
header[1], header[2], header[3], header[4], header[5], header[6], header[7], header[8],
|
|
]);
|
|
let keyframe = header[9] != 0;
|
|
let len = u32::from_le_bytes([header[10], header[11], header[12], header[13]]) as usize;
|
|
if len > MAX_FRAME_SIZE {
|
|
return Err(crate::error::Error::PesFrameTooLarge { size: len }.into());
|
|
}
|
|
let mut data = vec![0u8; len];
|
|
r.read_exact(&mut data)?;
|
|
Ok(Some(Self {
|
|
track,
|
|
pts,
|
|
keyframe,
|
|
data,
|
|
}))
|
|
}
|
|
|
|
/// Create from a codec::Frame with a track index.
|
|
pub fn from_codec_frame(track: usize, frame: crate::mux::codec::Frame) -> Self {
|
|
Self {
|
|
track,
|
|
pts: frame.pts_ns,
|
|
keyframe: frame.keyframe,
|
|
data: frame.data,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A PES frame source or sink. Each implementor is **either** read-only or
|
|
/// write-only — never both.
|
|
///
|
|
/// Implementors fall into two camps:
|
|
///
|
|
/// - **Read sources**: `DiscStream` (drive or ISO), `M2tsStream` (when
|
|
/// constructed from an existing file), `MkvStream` (demux), `NetworkStream`
|
|
/// (TCP listener), `StdioStream::input()`. These return frames from
|
|
/// `read()` and surface `StreamWriteOnly` (E9001) from `write()`.
|
|
/// - **Write sinks**: `MkvStream::create`, `M2tsStream::create`,
|
|
/// `NetworkStream::connect`, `StdioStream::output()`, `NullStream`.
|
|
/// These accept frames in `write()` and surface `StreamReadOnly` (E9000)
|
|
/// from `read()`. Always call `finish()` when done — that's where MKV
|
|
/// writes its `Cues` index and `M2tsStream` flushes the TS muxer.
|
|
///
|
|
/// Direction is established at construction; mixing produces an error code,
|
|
/// not a panic. Most consumers don't construct streams directly — call
|
|
/// `mux::input(url, opts)` / `mux::output(url, title)` and let URL parsing
|
|
/// pick the right type.
|
|
///
|
|
/// `info()` returns the stream's `DiscTitle` metadata (track list, codec
|
|
/// info, duration). For sources it's parsed from the input; for sinks it's
|
|
/// the metadata supplied at creation. Stable across all reads.
|
|
///
|
|
/// `codec_private(track)` exposes per-track initialization data
|
|
/// (H.264 SPS/PPS, HEVC VPS/SPS/PPS, AC-3 fscod, etc.) that some output
|
|
/// formats need before any frame can be 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 Stream {
|
|
/// Read the next frame, or `Ok(None)` at end of stream. Returns
|
|
/// `StreamWriteOnly` (E9001) on a write-only sink.
|
|
fn read(&mut self) -> std::io::Result<Option<PesFrame>>;
|
|
|
|
/// Write a frame to the sink. Returns `StreamReadOnly` (E9000) on a
|
|
/// read-only source.
|
|
fn write(&mut self, frame: &PesFrame) -> std::io::Result<()>;
|
|
|
|
/// Finalize the stream: flush buffered frames, write any container
|
|
/// index (MKV `Cues`), close the underlying file/socket. Idempotent
|
|
/// for read-only streams (no-op).
|
|
fn finish(&mut self) -> std::io::Result<()>;
|
|
|
|
/// Stream metadata. Stable across reads — implementors must return a
|
|
/// consistent reference for the lifetime of the stream.
|
|
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
|
|
}
|
|
}
|
|
|
|
/// Wraps any output stream and counts bytes written.
|
|
///
|
|
/// Progress tracking is a CLI concern — streams don't know their size.
|
|
/// Wrap the output with CountingStream, then query bytes_written().
|
|
///
|
|
/// ```text
|
|
/// let mut output = CountingStream::new(libfreemkv::output(dest, &title)?);
|
|
/// while let Ok(Some(frame)) = input.read() {
|
|
/// output.write(&frame)?;
|
|
/// let pct = output.bytes_written() as f64 / total as f64;
|
|
/// }
|
|
/// ```
|
|
pub struct CountingStream {
|
|
inner: Box<dyn Stream>,
|
|
written: u64,
|
|
}
|
|
|
|
impl CountingStream {
|
|
pub fn new(inner: Box<dyn Stream>) -> Self {
|
|
Self { inner, written: 0 }
|
|
}
|
|
|
|
/// Total bytes of PES frame data written through this stream.
|
|
pub fn bytes_written(&self) -> u64 {
|
|
self.written
|
|
}
|
|
}
|
|
|
|
impl Stream for CountingStream {
|
|
fn read(&mut self) -> std::io::Result<Option<PesFrame>> {
|
|
self.inner.read()
|
|
}
|
|
|
|
fn write(&mut self, frame: &PesFrame) -> std::io::Result<()> {
|
|
self.written += frame.data.len() as u64;
|
|
self.inner.write(frame)
|
|
}
|
|
|
|
fn finish(&mut self) -> std::io::Result<()> {
|
|
self.inner.finish()
|
|
}
|
|
|
|
fn info(&self) -> &crate::disc::DiscTitle {
|
|
self.inner.info()
|
|
}
|
|
|
|
fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
|
|
self.inner.codec_private(track)
|
|
}
|
|
|
|
fn headers_ready(&self) -> bool {
|
|
self.inner.headers_ready()
|
|
}
|
|
}
|