mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
496 lines
18 KiB
Rust
496 lines
18 KiB
Rust
//! StdioStream — PES frames via stdin/stdout with FMKV metadata header.
|
|
//!
|
|
//! The FMKV header carries stream metadata (PIDs, codecs, languages, codec_privates)
|
|
//! so the receiving end can set up muxing without scanning the content.
|
|
|
|
use super::meta;
|
|
use crate::disc::DiscTitle;
|
|
use std::io::{self, Read, Write};
|
|
|
|
/// Stdio stream — reads PES from stdin, writes PES to stdout.
|
|
/// FMKV metadata header is written/read automatically.
|
|
pub struct StdioStream {
|
|
disc_title: DiscTitle,
|
|
// Boxed so tests can drive the header logic without real stdin/stdout.
|
|
reader: Option<Box<dyn Read + Send + Sync>>,
|
|
writer: Option<io::BufWriter<Box<dyn Write + Send + Sync>>>,
|
|
header_written: bool,
|
|
header_read: bool,
|
|
/// True once an FMKV header was actually parsed on the read side
|
|
/// (set only inside the `Some(meta)` arm). Distinct from
|
|
/// `header_read`, which is true after the first read attempt even
|
|
/// when no header was present — `headers_ready()` must gate on the
|
|
/// metadata actually being available, not merely on having looked.
|
|
meta_parsed: bool,
|
|
/// Write side: per-track decoder timing, sent in the header. Read side: the
|
|
/// header's timing.
|
|
timings: Vec<crate::pes::TrackTiming>,
|
|
/// Frames carry the DiscardPadding extension (FMKV header v2).
|
|
padded: bool,
|
|
}
|
|
|
|
impl StdioStream {
|
|
/// Create a stdio stream for reading (stdin).
|
|
pub fn input() -> Self {
|
|
Self::input_staged(false)
|
|
}
|
|
|
|
// `input` whose decryption stage passes ciphertext when `raw`.
|
|
pub(crate) fn input_staged(raw: bool) -> Self {
|
|
Self::from_reader(Box::new(crate::sector::stage::Stage::lazy(
|
|
io::stdin(),
|
|
raw,
|
|
)))
|
|
}
|
|
|
|
fn from_reader(reader: Box<dyn Read + Send + Sync>) -> Self {
|
|
Self {
|
|
disc_title: DiscTitle::empty(),
|
|
reader: Some(reader),
|
|
writer: None,
|
|
header_written: false,
|
|
header_read: false,
|
|
meta_parsed: false,
|
|
timings: Vec::new(),
|
|
padded: false,
|
|
}
|
|
}
|
|
|
|
/// Create a stdio stream for writing (stdout).
|
|
pub fn output(title: &DiscTitle) -> Self {
|
|
Self::from_writer(title, Box::new(io::stdout()))
|
|
}
|
|
|
|
fn from_writer(title: &DiscTitle, writer: Box<dyn Write + Send + Sync>) -> Self {
|
|
Self {
|
|
disc_title: title.clone(),
|
|
reader: None,
|
|
writer: Some(io::BufWriter::new(writer)),
|
|
header_written: false,
|
|
header_read: false,
|
|
meta_parsed: false,
|
|
timings: Vec::new(),
|
|
padded: false,
|
|
}
|
|
}
|
|
|
|
// Write the FMKV header to stdout exactly once, even for zero-frame
|
|
// output, keeping the wire protocol symmetric with read_header().
|
|
fn ensure_header_written(&mut self) -> io::Result<()> {
|
|
if let Some(w) = &mut self.writer
|
|
&& !self.header_written
|
|
{
|
|
let m = meta::M2tsMeta::from_title(&self.disc_title).with_timings(&self.timings);
|
|
meta::write_header(w, &m)?;
|
|
self.padded = m.frame_padding;
|
|
self.header_written = true;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Read the FMKV metadata header from stdin on first read.
|
|
fn ensure_header_read(&mut self) -> io::Result<()> {
|
|
if self.header_read {
|
|
return Ok(());
|
|
}
|
|
if let Some(ref mut r) = self.reader {
|
|
// stdin cannot rewind, so any header failure after bytes were consumed is an error
|
|
// (never a misaligned frame read); only a clean zero-byte EOF is headerless.
|
|
let mut counted = Counted { inner: r, n: 0 };
|
|
let header = meta::read_header(&mut counted)?;
|
|
if header.is_none() && counted.n > 0 {
|
|
return Err(crate::error::Error::NoMetadata.into());
|
|
}
|
|
if let Some(m) = header {
|
|
self.disc_title = m.to_title();
|
|
self.timings = (0..self.disc_title.streams.len())
|
|
.map(|i| m.timing(i))
|
|
.collect();
|
|
self.padded = m.frame_padding;
|
|
self.meta_parsed = true;
|
|
}
|
|
}
|
|
// Set only on success: a failed header leaves the next read() failing, not parsing
|
|
// mid-header bytes as frames.
|
|
self.header_read = true;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// Counts the bytes a header probe consumed from a non-rewindable reader.
|
|
struct Counted<'a, R: ?Sized> {
|
|
inner: &'a mut R,
|
|
n: u64,
|
|
}
|
|
|
|
impl<R: Read + ?Sized> Read for Counted<'_, R> {
|
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
let got = self.inner.read(buf)?;
|
|
self.n += got as u64;
|
|
Ok(got)
|
|
}
|
|
}
|
|
|
|
impl StdioStream {
|
|
/// The title being read or written (both directions open this type).
|
|
pub fn info(&self) -> &crate::disc::DiscTitle {
|
|
&self.disc_title
|
|
}
|
|
|
|
// Read the FMKV header now, so `info()` is the stream's title before the first frame
|
|
// (a stream selection is resolved against it at open).
|
|
pub(crate) fn prime(&mut self) -> io::Result<()> {
|
|
self.ensure_header_read()
|
|
}
|
|
}
|
|
|
|
impl crate::pes::PesSource for StdioStream {
|
|
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
|
|
self.ensure_header_read()?;
|
|
match &mut self.reader {
|
|
Some(r) => crate::pes::PesFrame::deserialize_ext(r, self.padded),
|
|
None => Err(crate::error::Error::StreamWriteOnly.into()),
|
|
}
|
|
}
|
|
|
|
fn info(&self) -> &DiscTitle {
|
|
&self.disc_title
|
|
}
|
|
|
|
// Read side: the header's timing, available once the first read parsed it.
|
|
fn track_timing(&self, track: usize) -> crate::pes::TrackTiming {
|
|
self.timings.get(track).copied().unwrap_or_default()
|
|
}
|
|
|
|
fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
|
|
// Single source of truth: the title's own codec_privates. (The
|
|
// previous `stored_codec_privates` field was a redundant clone
|
|
// of exactly this, populated from the same header.)
|
|
self.disc_title
|
|
.codec_privates
|
|
.get(track)
|
|
.and_then(|c| c.clone())
|
|
}
|
|
|
|
fn headers_ready(&self) -> bool {
|
|
// Write side: title supplied up front, always ready. Read side: ready only once an
|
|
// FMKV header was parsed — gating on `header_read` alone would claim readiness for a
|
|
// headerless stream (codec_private() all None), starving the MKV writer of init data.
|
|
self.writer.is_some() || self.meta_parsed
|
|
}
|
|
}
|
|
|
|
impl crate::pes::PesSink for StdioStream {
|
|
fn write(&mut self, frame: &crate::pes::PesFrame) -> io::Result<()> {
|
|
if self.writer.is_none() {
|
|
return Err(crate::error::Error::StreamReadOnly.into());
|
|
}
|
|
self.ensure_header_written()?;
|
|
match &mut self.writer {
|
|
Some(w) => frame.serialize_ext(w, self.padded),
|
|
None => Err(crate::error::Error::StreamReadOnly.into()),
|
|
}
|
|
}
|
|
|
|
fn finish(&mut self) -> io::Result<()> {
|
|
// Emit the header even when write() was never called, so a zero-frame
|
|
// title still produces the FMKV magic + metadata header on stdout
|
|
// (symmetric with the read side's read_header()).
|
|
self.ensure_header_written()?;
|
|
if let Some(w) = &mut self.writer {
|
|
w.flush()?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn info(&self) -> &DiscTitle {
|
|
&self.disc_title
|
|
}
|
|
|
|
fn set_track_timing(
|
|
&mut self,
|
|
track: usize,
|
|
timing: crate::pes::TrackTiming,
|
|
) -> io::Result<()> {
|
|
if self.writer.is_none() {
|
|
return Err(crate::error::Error::StreamReadOnly.into());
|
|
}
|
|
if self.header_written {
|
|
return Err(crate::error::Error::StreamHeaderWritten.into());
|
|
}
|
|
let tracks = self.disc_title.streams.len();
|
|
super::network::set_timing(&mut self.timings, track, timing, tracks)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::pes::{PesSink as _, PesSource as _};
|
|
|
|
// The pub type keeps the auto traits it had when it held Stdin/Stdout.
|
|
#[test]
|
|
fn stdio_stream_is_send_and_sync() {
|
|
fn check<T: Send + Sync>() {}
|
|
check::<StdioStream>();
|
|
}
|
|
|
|
fn title_with_codec_privates() -> DiscTitle {
|
|
use crate::disc::{Codec, Stream, VideoStream};
|
|
let mut t = DiscTitle::empty();
|
|
t.playlist = "StdioTitle".into();
|
|
t.streams.push(Stream::Video(VideoStream {
|
|
pid: 0x1011,
|
|
codec: Codec::Hevc,
|
|
resolution: crate::disc::Resolution::R2160p,
|
|
frame_rate: crate::disc::FrameRate::F23_976,
|
|
hdr: crate::disc::HdrFormat::Hdr10,
|
|
color_space: crate::disc::ColorSpace::Bt2020,
|
|
display_aspect: None,
|
|
secondary: false,
|
|
label: String::new(),
|
|
measured_cicp: None,
|
|
}));
|
|
// Index 0 = the video stream's codec init data.
|
|
t.codec_privates = vec![Some(vec![0xDE, 0xAD, 0xBE, 0xEF])];
|
|
t
|
|
}
|
|
|
|
// write() on an input stream must error StreamReadOnly without touching
|
|
// stdin/stdout (writer.is_none() guard short-circuits header logic),
|
|
// not silently discard frames.
|
|
#[test]
|
|
fn write_on_input_stream_is_read_only_error() {
|
|
let mut s = StdioStream::input();
|
|
let frame = crate::pes::PesFrame {
|
|
discard_padding_ns: 0,
|
|
coding: None,
|
|
source: None,
|
|
track: 0,
|
|
pts: 0,
|
|
keyframe: true,
|
|
data: vec![1, 2, 3],
|
|
duration_ns: None,
|
|
};
|
|
let err = s.write(&frame).expect_err("write on input must error");
|
|
// E_STREAM_READ_ONLY (9000) maps to Unsupported.
|
|
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
|
|
}
|
|
|
|
// read() on an output stream must error StreamWriteOnly;
|
|
// ensure_header_read is a no-op when reader is None, so this
|
|
// never blocks on real stdin.
|
|
#[test]
|
|
fn read_on_output_stream_is_write_only_error() {
|
|
let mut s = StdioStream::output(&DiscTitle::empty());
|
|
let err = s.read().expect_err("read on output must error");
|
|
// E_STREAM_WRITE_ONLY (9001) maps to Unsupported.
|
|
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
|
|
}
|
|
|
|
// The write side has the title up front, so headers_ready() must be
|
|
// true immediately — the MKV writer starts the container header
|
|
// without waiting for a (nonexistent) read-side header parse.
|
|
#[test]
|
|
fn output_headers_ready_immediately() {
|
|
let s = StdioStream::output(&DiscTitle::empty());
|
|
assert!(s.headers_ready(), "write side is always header-ready");
|
|
}
|
|
|
|
// A fresh input side has not parsed any header yet, so headers_ready()
|
|
// must be false; claiming readiness early would starve the MKV
|
|
// writer of codec init data.
|
|
#[test]
|
|
fn input_not_header_ready_before_any_read() {
|
|
let s = StdioStream::input();
|
|
assert!(
|
|
!s.headers_ready(),
|
|
"read side not ready until header parsed"
|
|
);
|
|
}
|
|
|
|
/// codec_private(track) on the write side returns the title's own
|
|
/// codec_private for that track (single source of truth = the title).
|
|
#[test]
|
|
fn output_codec_private_comes_from_title() {
|
|
let s = StdioStream::output(&title_with_codec_privates());
|
|
assert_eq!(
|
|
s.codec_private(0).as_deref(),
|
|
Some(&[0xDE, 0xAD, 0xBE, 0xEF][..]),
|
|
"track 0 codec_private must mirror title.codec_privates[0]"
|
|
);
|
|
// Out-of-range track → None (no panic, no wrong-track data).
|
|
assert_eq!(s.codec_private(99), None);
|
|
}
|
|
|
|
/// A fresh input stream defaults to an empty title until a header is
|
|
/// parsed — info() must not invent stream metadata.
|
|
#[test]
|
|
fn input_default_title_is_empty() {
|
|
let s = StdioStream::input();
|
|
assert!(s.info().streams.is_empty());
|
|
assert_eq!(s.codec_private(0), None);
|
|
}
|
|
|
|
fn frame(data: &[u8]) -> crate::pes::PesFrame {
|
|
crate::pes::PesFrame {
|
|
discard_padding_ns: 0,
|
|
coding: None,
|
|
source: None,
|
|
track: 0,
|
|
pts: 7,
|
|
keyframe: true,
|
|
data: data.to_vec(),
|
|
duration_ns: None,
|
|
}
|
|
}
|
|
|
|
fn reader(bytes: Vec<u8>) -> StdioStream {
|
|
StdioStream::from_reader(Box::new(io::Cursor::new(bytes)))
|
|
}
|
|
|
|
fn code(e: &io::Error) -> Option<u16> {
|
|
crate::error::error_code(e)
|
|
}
|
|
|
|
#[derive(Clone, Default)]
|
|
struct Shared(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
|
|
impl Write for Shared {
|
|
fn write(&mut self, b: &[u8]) -> io::Result<usize> {
|
|
self.0.lock().unwrap().extend_from_slice(b);
|
|
Ok(b.len())
|
|
}
|
|
fn flush(&mut self) -> io::Result<()> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// Write side -> read side over the FMKV wire: header parsed on the first read (title,
|
|
// codec_private, timing), headers_ready only after it, timing frozen once written.
|
|
#[test]
|
|
fn header_round_trips_and_gates_readiness() {
|
|
let out = Shared::default();
|
|
let mut w = StdioStream::from_writer(&title_with_codec_privates(), Box::new(out.clone()));
|
|
let timing = crate::pes::TrackTiming {
|
|
codec_delay_ns: 5,
|
|
seek_preroll_ns: 9,
|
|
};
|
|
w.set_track_timing(0, timing).unwrap();
|
|
w.write(&frame(&[1, 2, 3])).unwrap();
|
|
let e = w.set_track_timing(0, timing).unwrap_err();
|
|
assert_eq!(code(&e), Some(crate::error::E_STREAM_HEADER_WRITTEN));
|
|
w.finish().unwrap();
|
|
|
|
let mut r = reader(out.0.lock().unwrap().clone());
|
|
assert!(!r.headers_ready());
|
|
let f = r.read().unwrap().unwrap();
|
|
assert_eq!((f.pts, f.data), (7, vec![1, 2, 3]));
|
|
assert!(r.headers_ready());
|
|
assert_eq!(r.info().playlist, "StdioTitle");
|
|
assert_eq!(r.codec_private(0), Some(vec![0xDE, 0xAD, 0xBE, 0xEF]));
|
|
assert_eq!(r.track_timing(0), timing);
|
|
assert!(r.read().unwrap().is_none());
|
|
}
|
|
|
|
// A zero-frame title still puts the header on the wire: the reader sees the title.
|
|
#[test]
|
|
fn finish_without_frames_still_writes_the_header() {
|
|
let out = Shared::default();
|
|
let mut w = StdioStream::from_writer(&title_with_codec_privates(), Box::new(out.clone()));
|
|
w.finish().unwrap();
|
|
let mut r = reader(out.0.lock().unwrap().clone());
|
|
r.prime().unwrap();
|
|
assert_eq!(r.info().playlist, "StdioTitle");
|
|
assert!(r.read().unwrap().is_none());
|
|
}
|
|
|
|
// Frame framing follows the header: with no timing set (v1) frames carry no padding field
|
|
// and read back whole; with a timing set (v2) the DiscardPadding survives.
|
|
#[test]
|
|
fn frame_padding_follows_the_header_version() {
|
|
let round_trip = |timed: bool, padding: i64| {
|
|
let out = Shared::default();
|
|
let mut w =
|
|
StdioStream::from_writer(&title_with_codec_privates(), Box::new(out.clone()));
|
|
if timed {
|
|
let timing = crate::pes::TrackTiming {
|
|
codec_delay_ns: 5,
|
|
seek_preroll_ns: 9,
|
|
};
|
|
w.set_track_timing(0, timing).unwrap();
|
|
}
|
|
let mut f = frame(&[1, 2, 3]);
|
|
f.discard_padding_ns = padding;
|
|
w.write(&f).unwrap();
|
|
w.write(&frame(&[4, 5])).unwrap();
|
|
w.finish().unwrap();
|
|
let mut r = reader(out.0.lock().unwrap().clone());
|
|
let a = r.read().unwrap().unwrap();
|
|
let b = r.read().unwrap().unwrap();
|
|
assert!(r.read().unwrap().is_none());
|
|
(a.discard_padding_ns, a.data, b.data)
|
|
};
|
|
assert_eq!(round_trip(false, 0), (0, vec![1, 2, 3], vec![4, 5]));
|
|
assert_eq!(
|
|
round_trip(true, -2_500_000),
|
|
(-2_500_000, vec![1, 2, 3], vec![4, 5])
|
|
);
|
|
}
|
|
|
|
// prime() reads the header up front: info() is the stream's title before any frame,
|
|
// and the first read still returns the first frame.
|
|
#[test]
|
|
fn prime_reads_the_header_before_the_first_frame() {
|
|
let out = Shared::default();
|
|
let mut w = StdioStream::from_writer(&title_with_codec_privates(), Box::new(out.clone()));
|
|
w.write(&frame(&[1, 2, 3])).unwrap();
|
|
w.finish().unwrap();
|
|
|
|
let mut r = reader(out.0.lock().unwrap().clone());
|
|
assert_eq!(r.info().streams.len(), 0);
|
|
r.prime().unwrap();
|
|
assert_eq!(r.info().playlist, "StdioTitle");
|
|
assert_eq!(
|
|
r.info().streams.len(),
|
|
title_with_codec_privates().streams.len()
|
|
);
|
|
let f = r.read().unwrap().unwrap();
|
|
assert_eq!(f.data, vec![1, 2, 3]);
|
|
}
|
|
|
|
// A zero-byte stdin is a clean headerless end, and never header-ready.
|
|
#[test]
|
|
fn empty_input_is_clean_eof_not_ready() {
|
|
let mut r = reader(Vec::new());
|
|
assert!(r.read().unwrap().is_none());
|
|
assert!(!r.headers_ready(), "no header was parsed");
|
|
}
|
|
|
|
// Bytes that are not an FMKV header were consumed from a non-rewindable stream, so the
|
|
// frames after them cannot be aligned: NoMetadata, not a misread frame.
|
|
#[test]
|
|
fn non_fmkv_input_is_no_metadata() {
|
|
for lead in [&[0x00u8][..], b"FMKX\0\x01\0\0"] {
|
|
let mut bytes = lead.to_vec();
|
|
frame(&[4, 5]).serialize(&mut bytes).unwrap();
|
|
let e = reader(bytes).read().unwrap_err();
|
|
assert_eq!(code(&e), Some(crate::error::E_NO_METADATA), "{lead:?}");
|
|
}
|
|
}
|
|
|
|
// A header that failed mid-way is not skipped on a retry: the next read must not parse
|
|
// the rest of the header region as frames.
|
|
#[test]
|
|
fn failed_header_is_not_retried_as_frames() {
|
|
let mut bytes = b"FMKV\0\x01\0\0".to_vec();
|
|
bytes.extend_from_slice(&u32::MAX.to_be_bytes()); // over the JSON size cap
|
|
frame(&[4, 5]).serialize(&mut bytes).unwrap();
|
|
let mut r = reader(bytes);
|
|
assert_eq!(
|
|
code(&r.read().unwrap_err()),
|
|
Some(crate::error::E_NO_METADATA)
|
|
);
|
|
assert!(r.read().is_err(), "retry must not yield a frame");
|
|
}
|
|
}
|