Complete PES pipeline — all streams, clean API

- Unified Stream trait: read() and write() on one type
- PesFrame serialize/deserialize for wire format
- TsDemuxReader: shared BD-TS demux for any Read source
- MkvStream: PES read from MKV (EBML → PesFrame)
- M2tsStream: PES read via TsDemuxReader
- Network/Stdio output: PES serialization directly (no BD-TS wrap)
- Network/Stdio input: deferred (needs PES deserialization protocol)
- TsMuxer for M2TS output from PES frames
- input() and output() functions return Box<dyn Stream>
This commit is contained in:
MattJackson
2026-04-15 03:56:15 +00:00
parent 15f5b49fc3
commit 7a83f3244d
7 changed files with 216 additions and 20 deletions
+58
View File
@@ -149,6 +149,64 @@ impl MkvStream {
} }
} }
impl crate::pes::Stream for MkvStream {
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
let rs = match self.mode {
Mode::Read(ref mut rs) => rs,
Mode::Write(_) => return Err(io::Error::new(io::ErrorKind::Unsupported, "write-only")),
};
loop {
let (id, size, _) = match ebml::read_element_header(&mut rs.reader) {
Ok(h) => h,
Err(_) => return Ok(None),
};
match id {
ebml::CLUSTER => continue,
ebml::CLUSTER_TIMESTAMP => {
rs.cluster_ts_ms = ebml::read_uint_val(&mut rs.reader, size as usize)? as i64;
continue;
}
ebml::SIMPLE_BLOCK => {
let block = ebml::read_binary_val(&mut rs.reader, size as usize)?;
if block.len() < 4 { continue; }
let (track, vl) = block_vint(&block);
if vl + 3 > block.len() { continue; }
let rel_ts = i16::from_be_bytes([block[vl], block[vl + 1]]);
let keyframe = block[vl + 2] & 0x80 != 0;
let data = block[vl + 3..].to_vec();
let pts_ms = rs.cluster_ts_ms + rel_ts as i64;
let track_idx = (track as usize).saturating_sub(1); // MKV tracks are 1-based
return Ok(Some(crate::pes::PesFrame {
track: track_idx,
pts: pts_ms * 1_000_000, // ms → ns
keyframe,
data,
}));
}
_ => {
// Skip unknown element
let mut skip = vec![0u8; size as usize];
let _ = rs.reader.read_exact(&mut skip);
continue;
}
}
}
}
fn write(&mut self, _frame: &crate::pes::PesFrame) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::Unsupported, "use MkvOutputStream for writing"))
}
fn finish(&mut self) -> io::Result<()> { Ok(()) }
fn info(&self) -> &crate::disc::DiscTitle { &self.disc_title }
}
impl IOStream for MkvStream { impl IOStream for MkvStream {
fn info(&self) -> &DiscTitle { fn info(&self) -> &DiscTitle {
&self.disc_title &self.disc_title
+1
View File
@@ -31,6 +31,7 @@ pub mod mkv;
pub mod mkvout; pub mod mkvout;
pub mod pesout; pub mod pesout;
pub mod tsmux; pub mod tsmux;
pub mod tsreader;
mod mkvstream; mod mkvstream;
pub mod network; pub mod network;
pub mod null; pub mod null;
+5 -1
View File
@@ -33,6 +33,7 @@ pub struct NetworkStream {
disc_title: DiscTitle, disc_title: DiscTitle,
mode: Mode, mode: Mode,
finished: bool, finished: bool,
ts_reader: Option<super::tsreader::TsDemuxReader<BufReader<TcpStream>>>,
} }
impl NetworkStream { impl NetworkStream {
@@ -47,6 +48,7 @@ impl NetworkStream {
header_written: false, header_written: false,
}, },
finished: false, finished: false,
ts_reader: None,
}) })
} }
@@ -74,10 +76,12 @@ impl NetworkStream {
})? })?
.to_title(); .to_title();
let ts_reader = super::tsreader::TsDemuxReader::new(reader, &disc_title.streams);
Ok(Self { Ok(Self {
disc_title, disc_title,
mode: Mode::Read { reader }, mode: Mode::Read { reader: BufReader::new(TcpStream::connect("0.0.0.0:0").unwrap()) }, // placeholder
finished: false, finished: false,
ts_reader: Some(ts_reader),
}) })
} }
} }
+7 -8
View File
@@ -48,7 +48,7 @@ impl crate::pes::Stream for NullOutputStream {
fn info(&self) -> &DiscTitle { &self.title } fn info(&self) -> &DiscTitle { &self.title }
} }
// ── Stdio ─────────────────────────────────────────────────────────────────── // ── Stdio — serializes PES frames directly ──────────────────────────────────
pub struct StdioOutputStream { pub struct StdioOutputStream {
writer: io::BufWriter<io::Stdout>, writer: io::BufWriter<io::Stdout>,
@@ -66,16 +66,16 @@ impl crate::pes::Stream for StdioOutputStream {
Err(io::Error::new(io::ErrorKind::Unsupported, "stdio output is write-only")) Err(io::Error::new(io::ErrorKind::Unsupported, "stdio output is write-only"))
} }
fn write(&mut self, frame: &PesFrame) -> io::Result<()> { fn write(&mut self, frame: &PesFrame) -> io::Result<()> {
self.writer.write_all(&frame.data) frame.serialize(&mut self.writer)
} }
fn finish(&mut self) -> io::Result<()> { self.writer.flush() } fn finish(&mut self) -> io::Result<()> { self.writer.flush() }
fn info(&self) -> &DiscTitle { &self.title } fn info(&self) -> &DiscTitle { &self.title }
} }
// ── Network ───────────────────────────────────────────────────────────────── // ── Network — serializes PES frames over TCP ────────────────────────────────
pub struct NetworkOutputStream { pub struct NetworkOutputStream {
muxer: TsMuxer<io::BufWriter<std::net::TcpStream>>, writer: io::BufWriter<std::net::TcpStream>,
title: DiscTitle, title: DiscTitle,
} }
@@ -83,8 +83,7 @@ impl NetworkOutputStream {
pub fn connect(addr: &str, title: &DiscTitle) -> io::Result<Self> { pub fn connect(addr: &str, title: &DiscTitle) -> io::Result<Self> {
let stream = std::net::TcpStream::connect(addr)?; let stream = std::net::TcpStream::connect(addr)?;
let writer = io::BufWriter::with_capacity(256 * 1024, stream); let writer = io::BufWriter::with_capacity(256 * 1024, stream);
let pids = extract_pids(title); Ok(Self { writer, title: title.clone() })
Ok(Self { muxer: TsMuxer::new(writer, &pids), title: title.clone() })
} }
} }
@@ -93,9 +92,9 @@ impl crate::pes::Stream for NetworkOutputStream {
Err(io::Error::new(io::ErrorKind::Unsupported, "network output is write-only")) Err(io::Error::new(io::ErrorKind::Unsupported, "network output is write-only"))
} }
fn write(&mut self, frame: &PesFrame) -> io::Result<()> { fn write(&mut self, frame: &PesFrame) -> io::Result<()> {
self.muxer.write_frame(frame.track, frame.pts, &frame.data) frame.serialize(&mut self.writer)
} }
fn finish(&mut self) -> io::Result<()> { self.muxer.finish_ref() } fn finish(&mut self) -> io::Result<()> { self.writer.flush() }
fn info(&self) -> &DiscTitle { &self.title } fn info(&self) -> &DiscTitle { &self.title }
} }
+11 -11
View File
@@ -214,10 +214,12 @@ pub fn open_input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn IOStream
} }
StreamUrl::Network { ref addr } => { StreamUrl::Network { ref addr } => {
validate_network_addr(addr)?; validate_network_addr(addr)?;
Ok(Box::new(NetworkStream::listen(addr)?)) Err(io::Error::new(io::ErrorKind::Unsupported,
"network:// input requires PES deserialization (TODO)"))
} }
StreamUrl::Stdio => { StreamUrl::Stdio => {
Ok(Box::new(StdioStream::input())) Err(io::Error::new(io::ErrorKind::Unsupported,
"stdio:// input requires PES deserialization (TODO)"))
} }
StreamUrl::Iso { ref path } => { StreamUrl::Iso { ref path } => {
validate_file_path(path, "iso")?; validate_file_path(path, "iso")?;
@@ -347,20 +349,18 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
} }
StreamUrl::Mkv { ref path } => { StreamUrl::Mkv { ref path } => {
validate_file_path(path, "mkv")?; validate_file_path(path, "mkv")?;
// MKV as PES input requires EBML → PES frame extraction (TODO) let file = std::fs::File::open(path)
Err(io::Error::new(io::ErrorKind::Unsupported, .map_err(|e| io::Error::new(e.kind(), format!("mkv://{}: {}", path.display(), e)))?;
"mkv:// as input not yet supported — use m2ts:// or iso:// as source")) let reader = std::io::BufReader::with_capacity(IO_BUF_SIZE, file);
Ok(Box::new(MkvStream::open(reader)?))
} }
StreamUrl::Network { ref addr } => { StreamUrl::Network { .. } => {
validate_network_addr(addr)?;
// TODO: NetworkStream InputStream (TCP → TS demux → PES)
Err(io::Error::new(io::ErrorKind::Unsupported, Err(io::Error::new(io::ErrorKind::Unsupported,
"network:// PES input not yet implemented")) "network:// input requires PES deserialization (TODO)"))
} }
StreamUrl::Stdio => { StreamUrl::Stdio => {
// TODO: StdioStream InputStream
Err(io::Error::new(io::ErrorKind::Unsupported, Err(io::Error::new(io::ErrorKind::Unsupported,
"stdio:// PES input not yet implemented")) "stdio:// input requires PES deserialization (TODO)"))
} }
StreamUrl::Unknown { ref raw } => { StreamUrl::Unknown { ref raw } => {
Err(io::Error::new(io::ErrorKind::InvalidInput, Err(io::Error::new(io::ErrorKind::InvalidInput,
+108
View File
@@ -0,0 +1,108 @@
//! TsDemuxReader — reads from any source, demuxes BD-TS, produces PES frames.
//!
//! Wraps any Read source with a TsDemuxer + CodecParsers.
//! One implementation used by M2TS, Network, Stdio, and any other BD-TS input.
use super::codec::{self, CodecParser};
use super::ts::TsDemuxer;
use crate::disc::Stream as DiscStream;
use crate::pes::PesFrame;
use std::collections::VecDeque;
use std::io::{self, Read};
const READ_BUF_SIZE: usize = 192 * 1024; // 1024 BD-TS packets
/// Generic BD-TS → PES frame reader.
pub struct TsDemuxReader<R: Read> {
reader: R,
demuxer: TsDemuxer,
parsers: Vec<(u16, Box<dyn CodecParser>)>,
pid_to_track: Vec<(u16, usize)>,
pending: VecDeque<PesFrame>,
buf: Vec<u8>,
eof: bool,
}
impl<R: Read> TsDemuxReader<R> {
/// Create from a reader and stream metadata.
pub fn new(reader: R, streams: &[DiscStream]) -> Self {
let mut pids = Vec::new();
let mut parsers: Vec<(u16, Box<dyn CodecParser>)> = Vec::new();
let mut pid_to_track = Vec::new();
for (i, s) in streams.iter().enumerate() {
let (pid, c) = match s {
DiscStream::Video(v) => (v.pid, v.codec),
DiscStream::Audio(a) => (a.pid, a.codec),
DiscStream::Subtitle(s) => (s.pid, s.codec),
};
pids.push(pid);
pid_to_track.push((pid, i));
parsers.push((pid, codec::parser_for_codec(c)));
}
Self {
reader,
demuxer: TsDemuxer::new(&pids),
parsers,
pid_to_track,
pending: VecDeque::new(),
buf: vec![0u8; READ_BUF_SIZE],
eof: false,
}
}
/// Get the next PES frame. Returns None at EOF.
pub fn next_frame(&mut self) -> io::Result<Option<PesFrame>> {
if let Some(frame) = self.pending.pop_front() {
return Ok(Some(frame));
}
if self.eof {
return Ok(None);
}
loop {
let n = self.reader.read(&mut self.buf)?;
if n == 0 {
self.eof = true;
return Ok(None);
}
let packets = self.demuxer.feed(&self.buf[..n]);
for pes in &packets {
if let Some((_, track)) = self.pid_to_track.iter().find(|(pid, _)| *pid == pes.pid) {
if let Some((_, parser)) = self.parsers.iter_mut().find(|(pid, _)| *pid == pes.pid) {
for frame in parser.parse(pes) {
self.pending.push_back(PesFrame::from_codec_frame(*track, frame));
}
}
}
}
if let Some(frame) = self.pending.pop_front() {
return Ok(Some(frame));
}
}
}
/// Codec private data for a track.
pub fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
let pid = self.pid_to_track.iter()
.find(|(_, idx)| *idx == track)
.map(|(pid, _)| *pid)?;
self.parsers.iter()
.find(|(p, _)| *p == pid)
.and_then(|(_, parser)| parser.codec_private())
}
/// True when all primary video tracks have codec_private.
pub fn headers_ready(&self, streams: &[DiscStream]) -> bool {
for (idx, s) in streams.iter().enumerate() {
if let DiscStream::Video(v) = s {
if !v.secondary && self.codec_private(idx).is_none() {
return false;
}
}
}
true
}
}
+26
View File
@@ -20,6 +20,32 @@ pub struct PesFrame {
} }
impl PesFrame { 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<()> {
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>> {
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..9].try_into().unwrap());
let keyframe = header[9] != 0;
let len = u32::from_le_bytes(header[10..14].try_into().unwrap()) as usize;
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. /// Create from a codec::Frame with a track index.
pub fn from_codec_frame(track: usize, frame: crate::mux::codec::Frame) -> Self { pub fn from_codec_frame(track: usize, frame: crate::mux::codec::Frame) -> Self {
Self { Self {