v0.10.1: Streams are PES, Disc::copy() for sector dumps, zero English
Architecture: - One stream per format, bidirectional PES (read/write on same type) - IsoStream merged into DiscStream (one type, any SectorReader) - Disc::copy() for disc→ISO raw sector dump - IOStream trait deleted, all byte-level Read/Write removed - ContentReader/OpenDisc/open_title/open_input/open_output deleted - CountingStream wrapper for progress tracking Error codes: - All io::Error English strings replaced with Error enum variants - From<Error> for io::Error conversion - Unused variants removed, new stream/mux variants added Deleted: mkvout.rs, pesout.rs, isowriter.rs, mkv-muxer-plan.md Updated: all docs, README stream table, CHANGELOG 238 tests, 0 clippy warnings.
This commit is contained in:
+56
-496
@@ -1,14 +1,10 @@
|
||||
//! MkvStream — Matroska container stream.
|
||||
//!
|
||||
//! Write: BD-TS bytes in → demux → codec parse → MKV container out.
|
||||
//! Read: MKV container in → extract frames → wrap as BD-TS → bytes out.
|
||||
//! Read: MKV container → demux EBML → PES frames out.
|
||||
//! Write: PES frames in → MKV mux → Matroska container.
|
||||
|
||||
use super::codec::{self, CodecParser};
|
||||
use super::lookahead::{LookaheadBuffer, LookaheadState, DEFAULT_LOOKAHEAD_SIZE};
|
||||
use super::mkv::{MkvMuxer, MkvTrack};
|
||||
use super::ts::TsDemuxer;
|
||||
use super::{ebml, IOStream, WriteSeek};
|
||||
use std::io::Seek;
|
||||
use super::{ebml, WriteSeek};
|
||||
|
||||
type MkvHeaderResult = io::Result<(crate::disc::DiscTitle, Vec<(u16, Vec<u8>)>)>;
|
||||
|
||||
@@ -19,44 +15,19 @@ fn skip_bytes(r: &mut impl Read, n: u64) -> io::Result<()> {
|
||||
}
|
||||
|
||||
use crate::disc::*;
|
||||
use std::io::{self, Read, Write};
|
||||
|
||||
/// Lookahead buffer for codec header detection (5 MB default).
|
||||
const DEFAULT_MAX_BUFFER: usize = DEFAULT_LOOKAHEAD_SIZE;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum WritePhase {
|
||||
Scanning,
|
||||
Streaming,
|
||||
}
|
||||
|
||||
struct WriteState {
|
||||
demuxer: TsDemuxer,
|
||||
muxer: Option<MkvMuxer<Box<dyn WriteSeek>>>,
|
||||
writer: Option<Box<dyn WriteSeek>>,
|
||||
parsers: Vec<(u16, Box<dyn CodecParser>)>,
|
||||
pid_to_track: Vec<(u16, usize)>,
|
||||
tracks: Vec<MkvTrack>,
|
||||
lookahead: LookaheadBuffer,
|
||||
phase: WritePhase,
|
||||
video_pending: usize,
|
||||
}
|
||||
use std::io::{self, Read};
|
||||
|
||||
struct ReadState {
|
||||
reader: Box<dyn Read>,
|
||||
buf: Vec<u8>,
|
||||
pos: usize,
|
||||
len: usize,
|
||||
cluster_ts_ms: i64,
|
||||
/// Codec private data per track (track_number, hvcC/avcC bytes).
|
||||
/// Emitted as Annex B NALs before first frame of each video track.
|
||||
codec_privates: Vec<(u16, Vec<u8>)>,
|
||||
/// Tracks that have already had their codec_private emitted.
|
||||
initialized_tracks: Vec<u16>,
|
||||
}
|
||||
|
||||
enum Mode {
|
||||
Write(Box<WriteState>),
|
||||
Write {
|
||||
muxer: Option<MkvMuxer<Box<dyn WriteSeek>>>,
|
||||
},
|
||||
Read(ReadState),
|
||||
}
|
||||
|
||||
@@ -64,95 +35,52 @@ enum Mode {
|
||||
pub struct MkvStream {
|
||||
disc_title: DiscTitle,
|
||||
mode: Mode,
|
||||
max_buffer: usize,
|
||||
finished: bool,
|
||||
/// File size in bytes, set for read mode.
|
||||
file_size: Option<u64>,
|
||||
}
|
||||
|
||||
impl MkvStream {
|
||||
/// Create for writing. BD-TS bytes written to this stream produce MKV output.
|
||||
pub fn new(writer: impl Write + Seek + 'static) -> Self {
|
||||
Self {
|
||||
disc_title: DiscTitle::empty(),
|
||||
mode: Mode::Write(Box::new(WriteState {
|
||||
demuxer: TsDemuxer::new(&[]),
|
||||
muxer: None,
|
||||
writer: Some(Box::new(writer)),
|
||||
parsers: Vec::new(),
|
||||
pid_to_track: Vec::new(),
|
||||
tracks: Vec::new(),
|
||||
lookahead: LookaheadBuffer::new(DEFAULT_MAX_BUFFER),
|
||||
phase: WritePhase::Scanning,
|
||||
video_pending: 0,
|
||||
})),
|
||||
max_buffer: DEFAULT_MAX_BUFFER,
|
||||
finished: false,
|
||||
file_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set stream metadata. Returns self for chaining.
|
||||
pub fn meta(mut self, dt: &DiscTitle) -> Self {
|
||||
if let Mode::Write(ref mut ws) = self.mode {
|
||||
let mut pids = Vec::new();
|
||||
for s in &dt.streams {
|
||||
let (pid, track, parser) = match s {
|
||||
crate::disc::Stream::Video(v) => {
|
||||
// Only count primary video as pending — secondary streams
|
||||
// (Dolby Vision EL, PiP) may never produce codec headers
|
||||
if !v.secondary {
|
||||
ws.video_pending += 1;
|
||||
}
|
||||
(v.pid, MkvTrack::video(v), codec::parser_for_codec(v.codec))
|
||||
}
|
||||
crate::disc::Stream::Audio(a) => {
|
||||
(a.pid, MkvTrack::audio(a), codec::parser_for_codec(a.codec))
|
||||
}
|
||||
crate::disc::Stream::Subtitle(s) => (
|
||||
s.pid,
|
||||
MkvTrack::subtitle(s),
|
||||
codec::parser_for_codec_with_data(s.codec, s.codec_data.clone()),
|
||||
),
|
||||
};
|
||||
let idx = ws.tracks.len();
|
||||
pids.push(pid);
|
||||
ws.pid_to_track.push((pid, idx));
|
||||
ws.parsers.push((pid, parser));
|
||||
ws.tracks.push(track);
|
||||
/// Create for writing PES frames → MKV container.
|
||||
/// Codec privates come from title.codec_privates (populated by input stream).
|
||||
pub fn create(
|
||||
writer: Box<dyn WriteSeek>,
|
||||
title: &DiscTitle,
|
||||
) -> io::Result<Self> {
|
||||
let mut tracks = Vec::new();
|
||||
for (idx, s) in title.streams.iter().enumerate() {
|
||||
let mut track = match s {
|
||||
crate::disc::Stream::Video(v) => MkvTrack::video(v),
|
||||
crate::disc::Stream::Audio(a) => MkvTrack::audio(a),
|
||||
crate::disc::Stream::Subtitle(s) => MkvTrack::subtitle(s),
|
||||
};
|
||||
if let Some(cp) = title.codec_privates.get(idx).and_then(|c| c.as_ref()) {
|
||||
track.codec_private = Some(cp.clone());
|
||||
}
|
||||
ws.demuxer = TsDemuxer::new(&pids);
|
||||
tracks.push(track);
|
||||
}
|
||||
self.disc_title = dt.clone();
|
||||
self
|
||||
|
||||
let muxer = MkvMuxer::new_with_chapters(
|
||||
writer,
|
||||
&tracks,
|
||||
Some(&title.playlist),
|
||||
title.duration_secs,
|
||||
&title.chapters,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
disc_title: title.clone(),
|
||||
mode: Mode::Write { muxer: Some(muxer) },
|
||||
})
|
||||
}
|
||||
|
||||
/// Set lookahead buffer size. Returns self.
|
||||
pub fn max_buffer(mut self, size: usize) -> Self {
|
||||
self.max_buffer = size;
|
||||
if let Mode::Write(ref mut ws) = self.mode {
|
||||
ws.lookahead = LookaheadBuffer::new(size);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Open an MKV file for reading.
|
||||
/// Open an MKV file for reading → PES frames.
|
||||
pub fn open(mut reader: impl Read + 'static) -> io::Result<Self> {
|
||||
let (disc_title, codec_privates) = parse_mkv_header(&mut reader)?;
|
||||
Ok(Self {
|
||||
disc_title,
|
||||
mode: Mode::Read(ReadState {
|
||||
reader: Box::new(reader),
|
||||
buf: Vec::new(),
|
||||
pos: 0,
|
||||
len: 0,
|
||||
cluster_ts_ms: 0,
|
||||
codec_privates,
|
||||
initialized_tracks: Vec::new(),
|
||||
}),
|
||||
max_buffer: 0,
|
||||
finished: false,
|
||||
file_size: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -161,7 +89,7 @@ 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")),
|
||||
Mode::Write { .. } => return Err(crate::error::Error::StreamWriteOnly.into()),
|
||||
};
|
||||
|
||||
loop {
|
||||
@@ -209,11 +137,22 @@ impl crate::pes::Stream for MkvStream {
|
||||
}
|
||||
}
|
||||
|
||||
fn write(&mut self, _frame: &crate::pes::PesFrame) -> io::Result<()> {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported, "use MkvOutputStream for writing"))
|
||||
fn write(&mut self, frame: &crate::pes::PesFrame) -> io::Result<()> {
|
||||
match &mut self.mode {
|
||||
Mode::Write { muxer: Some(ref mut m) } => m.write_frame(frame.track, frame.pts, frame.keyframe, &frame.data),
|
||||
Mode::Write { muxer: None } => Ok(()),
|
||||
Mode::Read(_) => Err(crate::error::Error::StreamReadOnly.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&mut self) -> io::Result<()> { Ok(()) }
|
||||
fn finish(&mut self) -> io::Result<()> {
|
||||
if let Mode::Write { ref mut muxer } = self.mode {
|
||||
if let Some(m) = muxer.take() {
|
||||
m.finish()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn info(&self) -> &crate::disc::DiscTitle { &self.disc_title }
|
||||
|
||||
@@ -234,256 +173,6 @@ impl crate::pes::Stream for MkvStream {
|
||||
}
|
||||
}
|
||||
|
||||
impl IOStream for MkvStream {
|
||||
fn info(&self) -> &DiscTitle {
|
||||
&self.disc_title
|
||||
}
|
||||
|
||||
fn finish(&mut self) -> io::Result<()> {
|
||||
if self.finished {
|
||||
return Ok(());
|
||||
}
|
||||
self.finished = true;
|
||||
if let Mode::Write(ref mut ws) = self.mode {
|
||||
// Flush remaining PES packets
|
||||
if let Some(ref mut muxer) = ws.muxer {
|
||||
for pes in &ws.demuxer.flush() {
|
||||
write_pes(&ws.pid_to_track, &mut ws.parsers, muxer, pes)?;
|
||||
}
|
||||
}
|
||||
// Write cues and finalize
|
||||
if let Some(muxer) = ws.muxer.take() {
|
||||
muxer.finish()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn total_bytes(&self) -> Option<u64> {
|
||||
self.file_size
|
||||
}
|
||||
}
|
||||
|
||||
// ── Write ──────────────────────────────────────────────────────
|
||||
|
||||
impl Write for MkvStream {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let dt = &self.disc_title;
|
||||
let ws = match self.mode {
|
||||
Mode::Write(ref mut ws) => ws,
|
||||
Mode::Read(_) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"stream opened for reading",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
match ws.phase {
|
||||
WritePhase::Scanning => {
|
||||
// Feed demuxer for codec detection
|
||||
let packets = ws.demuxer.feed(buf);
|
||||
for pes in &packets {
|
||||
if let Some((_, p)) = ws.parsers.iter_mut().find(|(pid, _)| *pid == pes.pid) {
|
||||
let _ = p.parse(pes);
|
||||
}
|
||||
}
|
||||
|
||||
let state = ws.lookahead.push(buf);
|
||||
|
||||
// Check if all video codec headers found
|
||||
if check_codec_private(ws) {
|
||||
ws.lookahead.mark_ready();
|
||||
begin_streaming(ws, dt)?;
|
||||
return Ok(buf.len());
|
||||
}
|
||||
|
||||
match state {
|
||||
LookaheadState::Collecting | LookaheadState::Ready => Ok(buf.len()),
|
||||
LookaheadState::Overflow => Err(io::Error::new(
|
||||
io::ErrorKind::OutOfMemory,
|
||||
"no codec headers found within lookahead buffer",
|
||||
)),
|
||||
}
|
||||
}
|
||||
WritePhase::Streaming => {
|
||||
let packets = ws.demuxer.feed(buf);
|
||||
if let Some(ref mut muxer) = ws.muxer {
|
||||
for pes in &packets {
|
||||
write_pes(&ws.pid_to_track, &mut ws.parsers, muxer, pes)?;
|
||||
}
|
||||
}
|
||||
Ok(buf.len())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ── Read ───────────────────────────────────────────────────────
|
||||
|
||||
impl Read for MkvStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let rs = match self.mode {
|
||||
Mode::Read(ref mut rs) => rs,
|
||||
Mode::Write(_) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"stream opened for writing",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Drain internal buffer first
|
||||
if rs.pos < rs.len {
|
||||
let n = (rs.len - rs.pos).min(buf.len());
|
||||
buf[..n].copy_from_slice(&rs.buf[rs.pos..rs.pos + n]);
|
||||
rs.pos += n;
|
||||
return Ok(n);
|
||||
}
|
||||
|
||||
// Read next element from MKV
|
||||
loop {
|
||||
let (id, size, _) = match ebml::read_element_header(&mut rs.reader) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return Ok(0),
|
||||
};
|
||||
|
||||
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 frame = &block[vl + 3..];
|
||||
let pts_ms = rs.cluster_ts_ms + rel_ts as i64;
|
||||
let tnum = track as u16;
|
||||
|
||||
rs.buf.clear();
|
||||
|
||||
// First frame of a track: emit codec_private as Annex B NALs
|
||||
if !rs.initialized_tracks.contains(&tnum) {
|
||||
rs.initialized_tracks.push(tnum);
|
||||
if let Some((_, cp)) = rs.codec_privates.iter().find(|(t, _)| *t == tnum) {
|
||||
let annex_b = hvcc_to_annex_b(cp);
|
||||
if !annex_b.is_empty() {
|
||||
frame_to_ts(&mut rs.buf, tnum, pts_ms, &annex_b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frame_to_ts(&mut rs.buf, tnum, pts_ms, frame);
|
||||
rs.pos = 0;
|
||||
rs.len = rs.buf.len();
|
||||
|
||||
if rs.len > 0 {
|
||||
let n = rs.len.min(buf.len());
|
||||
buf[..n].copy_from_slice(&rs.buf[..n]);
|
||||
rs.pos = n;
|
||||
return Ok(n);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if size != u64::MAX && size > 0 {
|
||||
skip_bytes(&mut rs.reader, size)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Write internals ────────────────────────────────────────────
|
||||
|
||||
fn check_codec_private(ws: &mut WriteState) -> bool {
|
||||
if ws.video_pending == 0 {
|
||||
return true;
|
||||
}
|
||||
for (pid, parser) in &ws.parsers {
|
||||
if let Some(cp) = parser.codec_private() {
|
||||
if let Some((_, idx)) = ws.pid_to_track.iter().find(|(p, _)| p == pid) {
|
||||
if ws.tracks[*idx].codec_private.is_none() {
|
||||
ws.tracks[*idx].codec_private = Some(cp);
|
||||
ws.video_pending -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ws.video_pending == 0
|
||||
}
|
||||
|
||||
fn begin_streaming(ws: &mut WriteState, dt: &DiscTitle) -> io::Result<()> {
|
||||
let writer = ws
|
||||
.writer
|
||||
.take()
|
||||
.ok_or_else(|| io::Error::other("writer already consumed"))?;
|
||||
|
||||
ws.muxer = Some(MkvMuxer::new_with_chapters(
|
||||
writer,
|
||||
&ws.tracks,
|
||||
Some(&dt.playlist),
|
||||
dt.duration_secs,
|
||||
&dt.chapters,
|
||||
)?);
|
||||
ws.phase = WritePhase::Streaming;
|
||||
|
||||
// Re-parse buffered data through a fresh demuxer, then reset the main
|
||||
// demuxer so stale PES assembler state from scanning doesn't cause
|
||||
// duplicate or incomplete packets during streaming.
|
||||
let pids: Vec<u16> = ws.pid_to_track.iter().map(|(pid, _)| *pid).collect();
|
||||
let buffered = ws.lookahead.drain();
|
||||
if !buffered.is_empty() {
|
||||
let mut temp = TsDemuxer::new(&pids);
|
||||
let packets = temp.feed(&buffered);
|
||||
if let Some(ref mut muxer) = ws.muxer {
|
||||
for pes in &packets {
|
||||
write_pes(&ws.pid_to_track, &mut ws.parsers, muxer, pes)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Transfer remainder bytes from old demuxer to new one.
|
||||
// Preserves 192-byte packet alignment across the reset.
|
||||
let remainder = ws.demuxer.take_remainder();
|
||||
ws.demuxer = TsDemuxer::new(&pids);
|
||||
ws.demuxer.set_remainder(remainder);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_pes(
|
||||
pid_to_track: &[(u16, usize)],
|
||||
parsers: &mut [(u16, Box<dyn CodecParser>)],
|
||||
muxer: &mut MkvMuxer<Box<dyn WriteSeek>>,
|
||||
pes: &super::ts::PesPacket,
|
||||
) -> io::Result<()> {
|
||||
let idx = match pid_to_track.iter().find(|(pid, _)| *pid == pes.pid) {
|
||||
Some((_, idx)) => *idx,
|
||||
None => return Ok(()),
|
||||
};
|
||||
let parser = match parsers.iter_mut().find(|(pid, _)| *pid == pes.pid) {
|
||||
Some((_, p)) => p,
|
||||
None => return Ok(()),
|
||||
};
|
||||
for frame in parser.parse(pes) {
|
||||
muxer.write_frame(idx, frame.pts_ns, frame.keyframe, &frame.data)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── MKV header parsing (read side) ────────────────────────────
|
||||
|
||||
/// Returns (DiscTitle, codec_privates: Vec<(track_number, codec_private_bytes)>)
|
||||
@@ -498,19 +187,16 @@ fn parse_mkv_header(
|
||||
|
||||
let (id, size, _) = ebml::read_element_header(r)?;
|
||||
if id != ebml::EBML {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "not EBML"));
|
||||
return Err(crate::error::Error::MkvInvalid.into());
|
||||
}
|
||||
if size > i64::MAX as u64 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"EBML header too large",
|
||||
));
|
||||
return Err(crate::error::Error::MkvInvalid.into());
|
||||
}
|
||||
skip_bytes(r, size)?;
|
||||
|
||||
let (id, _, _) = ebml::read_element_header(r)?;
|
||||
if id != ebml::SEGMENT {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "no Segment"));
|
||||
return Err(crate::error::Error::MkvInvalid.into());
|
||||
}
|
||||
|
||||
let (mut got_info, mut got_tracks) = (false, false);
|
||||
@@ -647,7 +333,7 @@ fn parse_track(
|
||||
SampleRate::S48
|
||||
};
|
||||
|
||||
// Map MKV track numbers to BD-TS PIDs (same mapping as frame_to_ts)
|
||||
// Map MKV track numbers to BD-TS PIDs
|
||||
let ts_pid = if tnum == 1 { 0x1011 } else { 0x1100 + (tnum - 2) };
|
||||
|
||||
let stream = match ttype {
|
||||
@@ -685,8 +371,6 @@ fn parse_track(
|
||||
Ok((stream, tnum, codec_priv))
|
||||
}
|
||||
|
||||
// ── BD-TS frame wrapping (read side) ──────────────────────────
|
||||
|
||||
fn block_vint(d: &[u8]) -> (u64, usize) {
|
||||
if d.is_empty() {
|
||||
return (0, 0);
|
||||
@@ -705,127 +389,3 @@ fn block_vint(d: &[u8]) -> (u64, usize) {
|
||||
}
|
||||
(0, 1) // Unsupported 5+ byte VINT — treat as track 0
|
||||
}
|
||||
|
||||
/// Convert HEVCDecoderConfigurationRecord (hvcC) to Annex B NAL units.
|
||||
/// Extracts VPS, SPS, PPS arrays and prefixes each with 0x00000001.
|
||||
fn hvcc_to_annex_b(hvcc: &[u8]) -> Vec<u8> {
|
||||
// hvcC format (ISO 14496-15):
|
||||
// byte 0: configurationVersion (1)
|
||||
// bytes 1-21: profile/level info
|
||||
// byte 22: numOfArrays
|
||||
// For each array:
|
||||
// byte 0: array_completeness(1) + reserved(1) + NAL_unit_type(6)
|
||||
// bytes 1-2: numNalus (big-endian u16)
|
||||
// For each NAL:
|
||||
// bytes 0-1: nalUnitLength (big-endian u16)
|
||||
// bytes 2..: NAL data
|
||||
if hvcc.len() < 23 {
|
||||
return Vec::new();
|
||||
}
|
||||
let num_arrays = hvcc[22] as usize;
|
||||
let mut pos = 23;
|
||||
let mut out = Vec::new();
|
||||
|
||||
for _ in 0..num_arrays {
|
||||
if pos + 3 > hvcc.len() {
|
||||
break;
|
||||
}
|
||||
pos += 1; // skip array_completeness + NAL type byte
|
||||
let num_nalus = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]) as usize;
|
||||
pos += 2;
|
||||
for _ in 0..num_nalus {
|
||||
if pos + 2 > hvcc.len() {
|
||||
break;
|
||||
}
|
||||
let nal_len = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]) as usize;
|
||||
pos += 2;
|
||||
if pos + nal_len > hvcc.len() {
|
||||
break;
|
||||
}
|
||||
out.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
|
||||
out.extend_from_slice(&hvcc[pos..pos + nal_len]);
|
||||
pos += nal_len;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn frame_to_ts(out: &mut Vec<u8>, track: u16, pts_ms: i64, data: &[u8]) {
|
||||
let pid = if track == 1 {
|
||||
0x1011
|
||||
} else {
|
||||
0x1100 + (track - 2)
|
||||
};
|
||||
let is_video = track <= 1 || pid == 0x1011;
|
||||
let stream_id: u8 = if is_video { 0xE0 } else { 0xBD };
|
||||
let pts = encode_pts(pts_ms * 90);
|
||||
let hdr = [0x00, 0x00, 0x01, stream_id, 0x00, 0x00, 0x80, 0x80, 0x05];
|
||||
|
||||
let mut pes = Vec::with_capacity(hdr.len() + pts.len() + data.len());
|
||||
pes.extend_from_slice(&hdr);
|
||||
pes.extend_from_slice(&pts);
|
||||
|
||||
// Video: convert MKV length-prefixed NALs to Annex B start codes
|
||||
if is_video && data.len() > 4 {
|
||||
let mut pos = 0;
|
||||
while pos + 4 <= data.len() {
|
||||
let nal_len = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
|
||||
pos += 4;
|
||||
if nal_len == 0 || pos + nal_len > data.len() {
|
||||
break;
|
||||
}
|
||||
pes.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
|
||||
pes.extend_from_slice(&data[pos..pos + nal_len]);
|
||||
pos += nal_len;
|
||||
}
|
||||
} else {
|
||||
pes.extend_from_slice(data);
|
||||
}
|
||||
|
||||
let mut off = 0;
|
||||
let mut pusi = true;
|
||||
while off < pes.len() {
|
||||
let mut pkt = [0u8; 192];
|
||||
pkt[4] = 0x47;
|
||||
pkt[5] = (pid >> 8) as u8 & 0x1F;
|
||||
if pusi {
|
||||
pkt[5] |= 0x40;
|
||||
pusi = false;
|
||||
}
|
||||
pkt[6] = pid as u8;
|
||||
|
||||
let space = 184;
|
||||
let rem = pes.len() - off;
|
||||
let n = rem.min(space);
|
||||
|
||||
if n < space {
|
||||
let pad = space - n;
|
||||
pkt[7] = 0x30; // AF + payload
|
||||
pkt[8] = pad as u8;
|
||||
if pad > 1 {
|
||||
pkt[9] = 0x00;
|
||||
}
|
||||
for byte in pkt.iter_mut().take((8 + pad).min(192)).skip(10) {
|
||||
*byte = 0xFF;
|
||||
}
|
||||
pkt[8 + pad..8 + pad + n].copy_from_slice(&pes[off..off + n]);
|
||||
} else {
|
||||
pkt[7] = 0x10; // payload only
|
||||
pkt[8..8 + n].copy_from_slice(&pes[off..off + n]);
|
||||
}
|
||||
|
||||
out.extend_from_slice(&pkt);
|
||||
off += n;
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_pts(pts: i64) -> [u8; 5] {
|
||||
let p = pts as u64;
|
||||
[
|
||||
0x21 | ((p >> 29) & 0x0E) as u8,
|
||||
((p >> 22) & 0xFF) as u8,
|
||||
0x01 | ((p >> 14) & 0xFE) as u8,
|
||||
((p >> 7) & 0xFF) as u8,
|
||||
0x01 | ((p << 1) & 0xFE) as u8,
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user