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:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user