Fix ISO → MKV producing empty files (TsDemuxer remainder lost)

When the MKV muxer transitions from Scanning to Streaming phase,
it creates a fresh TsDemuxer. The old demuxer's remainder bytes
(partial 192-byte BD-TS packets) were lost, causing the new demuxer
to lose sync. All subsequent feed() calls found 0 packets.

Fix: transfer remainder via take_remainder/set_remainder so the
new demuxer maintains packet alignment.
This commit is contained in:
MattJackson
2026-04-15 02:17:34 +00:00
parent f400e4fc72
commit 610f1b62b6
2 changed files with 167 additions and 51 deletions
+131 -27
View File
@@ -38,6 +38,11 @@ struct ReadState {
pos: usize, pos: usize,
len: usize, len: usize,
cluster_ts_ms: i64, 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 { enum Mode {
@@ -125,7 +130,7 @@ impl MkvStream {
pub fn open(mut reader: impl Read + Seek + 'static) -> io::Result<Self> { pub fn open(mut reader: impl Read + Seek + 'static) -> io::Result<Self> {
let file_size = reader.seek(SeekFrom::End(0))?; let file_size = reader.seek(SeekFrom::End(0))?;
reader.seek(SeekFrom::Start(0))?; reader.seek(SeekFrom::Start(0))?;
let disc_title = parse_mkv_header(&mut reader)?; let (disc_title, codec_privates) = parse_mkv_header(&mut reader)?;
Ok(Self { Ok(Self {
disc_title, disc_title,
mode: Mode::Read(ReadState { mode: Mode::Read(ReadState {
@@ -134,6 +139,8 @@ impl MkvStream {
pos: 0, pos: 0,
len: 0, len: 0,
cluster_ts_ms: 0, cluster_ts_ms: 0,
codec_privates,
initialized_tracks: Vec::new(),
}), }),
max_buffer: 0, max_buffer: 0,
finished: false, finished: false,
@@ -280,9 +287,22 @@ impl Read for MkvStream {
let rel_ts = i16::from_be_bytes([block[vl], block[vl + 1]]); let rel_ts = i16::from_be_bytes([block[vl], block[vl + 1]]);
let frame = &block[vl + 3..]; let frame = &block[vl + 3..];
let pts_ms = rs.cluster_ts_ms + rel_ts as i64; let pts_ms = rs.cluster_ts_ms + rel_ts as i64;
let tnum = track as u16;
rs.buf.clear(); rs.buf.clear();
frame_to_ts(&mut rs.buf, track as u16, pts_ms, frame);
// 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.pos = 0;
rs.len = rs.buf.len(); rs.len = rs.buf.len();
@@ -351,7 +371,11 @@ fn begin_streaming(ws: &mut WriteState, dt: &DiscTitle) -> io::Result<()> {
} }
} }
} }
// 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 = TsDemuxer::new(&pids);
ws.demuxer.set_remainder(remainder);
Ok(()) Ok(())
} }
@@ -377,11 +401,15 @@ fn write_pes(
// ── MKV header parsing (read side) ──────────────────────────── // ── MKV header parsing (read side) ────────────────────────────
fn parse_mkv_header(r: &mut (impl Read + Seek)) -> io::Result<DiscTitle> { /// Returns (DiscTitle, codec_privates: Vec<(track_number, codec_private_bytes)>)
fn parse_mkv_header(
r: &mut (impl Read + Seek),
) -> io::Result<(DiscTitle, Vec<(u16, Vec<u8>)>)> {
let mut title = String::new(); let mut title = String::new();
let mut duration_ms = 0.0f64; let mut duration_ms = 0.0f64;
let mut ts_scale: u64 = 1_000_000; let mut ts_scale: u64 = 1_000_000;
let mut streams: Vec<crate::disc::Stream> = Vec::new(); let mut streams: Vec<crate::disc::Stream> = Vec::new();
let mut codec_privates: Vec<(u16, Vec<u8>)> = Vec::new();
let (id, size, _) = ebml::read_element_header(r)?; let (id, size, _) = ebml::read_element_header(r)?;
if id != ebml::EBML { if id != ebml::EBML {
@@ -432,9 +460,13 @@ fn parse_mkv_header(r: &mut (impl Read + Seek)) -> io::Result<DiscTitle> {
while r.stream_position()? < end { while r.stream_position()? < end {
let (cid, cs, _) = ebml::read_element_header(r)?; let (cid, cs, _) = ebml::read_element_header(r)?;
if cid == ebml::TRACK_ENTRY { if cid == ebml::TRACK_ENTRY {
if let Some(s) = parse_track(r, cs)? { let (stream, tnum, cp) = parse_track(r, cs)?;
if let Some(s) = stream {
streams.push(s); streams.push(s);
} }
if let Some(cp) = cp {
codec_privates.push((tnum, cp));
}
} else { } else {
r.seek(SeekFrom::Current(cs as i64))?; r.seek(SeekFrom::Current(cs as i64))?;
} }
@@ -449,19 +481,25 @@ fn parse_mkv_header(r: &mut (impl Read + Seek)) -> io::Result<DiscTitle> {
} }
} }
Ok(DiscTitle { let disc_title = DiscTitle {
playlist: title, playlist: title,
duration_secs: duration_ms * (ts_scale as f64) / 1_000_000_000.0, duration_secs: duration_ms * (ts_scale as f64) / 1_000_000_000.0,
streams, streams,
..DiscTitle::empty() ..DiscTitle::empty()
}) };
Ok((disc_title, codec_privates))
} }
fn parse_track(r: &mut (impl Read + Seek), size: u64) -> io::Result<Option<crate::disc::Stream>> { /// Returns (stream, track_number, codec_private_bytes)
fn parse_track(
r: &mut (impl Read + Seek),
size: u64,
) -> io::Result<(Option<crate::disc::Stream>, u16, Option<Vec<u8>>)> {
let end = r.stream_position()? + size; let end = r.stream_position()? + size;
let (mut ttype, mut tnum) = (0u64, 0u16); let (mut ttype, mut tnum) = (0u64, 0u16);
let (mut codec_id, mut lang, mut name) = (String::new(), String::from("und"), String::new()); let (mut codec_id, mut lang, mut name) = (String::new(), String::from("und"), String::new());
let (mut ph, mut sr, mut ch, mut forced) = (0u32, 0.0f64, 0u8, false); let (mut ph, mut sr, mut ch, mut forced) = (0u32, 0.0f64, 0u8, false);
let mut codec_priv: Option<Vec<u8>> = None;
while r.stream_position()? < end { while r.stream_position()? < end {
let (cid, cs, _) = ebml::read_element_header(r)?; let (cid, cs, _) = ebml::read_element_header(r)?;
@@ -469,6 +507,7 @@ fn parse_track(r: &mut (impl Read + Seek), size: u64) -> io::Result<Option<crate
ebml::TRACK_NUMBER => tnum = ebml::read_uint_val(r, cs as usize)? as u16, ebml::TRACK_NUMBER => tnum = ebml::read_uint_val(r, cs as usize)? as u16,
ebml::TRACK_TYPE => ttype = ebml::read_uint_val(r, cs as usize)?, ebml::TRACK_TYPE => ttype = ebml::read_uint_val(r, cs as usize)?,
ebml::CODEC_ID => codec_id = ebml::read_string_val(r, cs as usize)?, ebml::CODEC_ID => codec_id = ebml::read_string_val(r, cs as usize)?,
ebml::CODEC_PRIVATE => codec_priv = Some(ebml::read_binary_val(r, cs as usize)?),
ebml::LANGUAGE => lang = ebml::read_string_val(r, cs as usize)?, ebml::LANGUAGE => lang = ebml::read_string_val(r, cs as usize)?,
ebml::TRACK_NAME => name = ebml::read_string_val(r, cs as usize)?, ebml::TRACK_NAME => name = ebml::read_string_val(r, cs as usize)?,
ebml::FLAG_FORCED => forced = ebml::read_uint_val(r, cs as usize)? != 0, ebml::FLAG_FORCED => forced = ebml::read_uint_val(r, cs as usize)? != 0,
@@ -516,30 +555,33 @@ fn parse_track(r: &mut (impl Read + Seek), size: u64) -> io::Result<Option<crate
"S_VOBSUB" => Codec::DvdSub, "S_VOBSUB" => Codec::DvdSub,
_ => Codec::Unknown(0), _ => Codec::Unknown(0),
}; };
let res = format!("{ph}p"); let res = Resolution::from_height(ph);
let chs: String = match ch { let chs = AudioChannels::from_count(ch);
8 => "7.1", let srs = if sr >= 96000.0 {
6 => "5.1", SampleRate::S96
2 => "stereo", } else {
1 => "mono", SampleRate::S48
_ => "5.1", };
}
.into();
let srs: String = if sr >= 96000.0 { "96kHz" } else { "48kHz" }.into();
Ok(match ttype { // Map MKV track numbers to BD-TS PIDs (same mapping as frame_to_ts)
1 => Some(crate::disc::Stream::Video(VideoStream { let ts_pid = if tnum == 1 { 0x1011 } else { 0x1100 + (tnum - 2) };
pid: tnum,
let stream = match ttype {
1 => {
let is_secondary = name.contains("Dolby Vision EL") || name.contains("DV EL");
Some(crate::disc::Stream::Video(VideoStream {
pid: ts_pid,
codec, codec,
resolution: res, resolution: res,
frame_rate: String::new(), frame_rate: FrameRate::Unknown,
hdr: HdrFormat::Sdr, hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709, color_space: ColorSpace::Bt709,
secondary: false, secondary: is_secondary,
label: name, label: name,
})), }))
}
2 => Some(crate::disc::Stream::Audio(AudioStream { 2 => Some(crate::disc::Stream::Audio(AudioStream {
pid: tnum, pid: ts_pid,
codec, codec,
channels: chs, channels: chs,
language: lang, language: lang,
@@ -548,14 +590,15 @@ fn parse_track(r: &mut (impl Read + Seek), size: u64) -> io::Result<Option<crate
label: name, label: name,
})), })),
17 => Some(crate::disc::Stream::Subtitle(SubtitleStream { 17 => Some(crate::disc::Stream::Subtitle(SubtitleStream {
pid: tnum, pid: ts_pid,
codec, codec,
language: lang, language: lang,
forced, forced,
codec_data: None, codec_data: None,
})), })),
_ => None, _ => None,
}) };
Ok((stream, tnum, codec_priv))
} }
// ── BD-TS frame wrapping (read side) ────────────────────────── // ── BD-TS frame wrapping (read side) ──────────────────────────
@@ -573,20 +616,81 @@ fn block_vint(d: &[u8]) -> (u64, usize) {
(0, 1) (0, 1)
} }
/// 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]) { fn frame_to_ts(out: &mut Vec<u8>, track: u16, pts_ms: i64, data: &[u8]) {
let pid = if track == 1 { let pid = if track == 1 {
0x1011 0x1011
} else { } else {
0x1100 + (track - 2) 0x1100 + (track - 2)
}; };
let stream_id: u8 = if track == 1 { 0xE0 } else { 0xBD }; 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 pts = encode_pts(pts_ms * 90);
let hdr = [0x00, 0x00, 0x01, stream_id, 0x00, 0x00, 0x80, 0x80, 0x05]; let hdr = [0x00, 0x00, 0x01, stream_id, 0x00, 0x00, 0x80, 0x80, 0x05];
let mut pes = Vec::with_capacity(hdr.len() + pts.len() + data.len()); let mut pes = Vec::with_capacity(hdr.len() + pts.len() + data.len());
pes.extend_from_slice(&hdr); pes.extend_from_slice(&hdr);
pes.extend_from_slice(&pts); 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); pes.extend_from_slice(data);
}
let mut off = 0; let mut off = 0;
let mut pusi = true; let mut pusi = true;
+30 -18
View File
@@ -104,6 +104,18 @@ pub struct TsDemuxer {
} }
impl TsDemuxer { impl TsDemuxer {
/// Take the remainder bytes (leftover from last feed() that didn't
/// align to a 192-byte packet boundary).
pub fn take_remainder(&mut self) -> Vec<u8> {
std::mem::take(&mut self.remainder)
}
/// Set the remainder bytes. Used to transfer alignment state from
/// one demuxer to another without losing sync.
pub fn set_remainder(&mut self, data: Vec<u8>) {
self.remainder = data;
}
/// Create a new demuxer tracking the given PIDs. /// Create a new demuxer tracking the given PIDs.
pub fn new(pids: &[u16]) -> Self { pub fn new(pids: &[u16]) -> Self {
let mut pid_index = [-1i16; 8192]; let mut pid_index = [-1i16; 8192];
@@ -363,8 +375,8 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
0x1B => Some(Stream::Video(VideoStream { 0x1B => Some(Stream::Video(VideoStream {
pid: es_pid, pid: es_pid,
codec: Codec::H264, codec: Codec::H264,
resolution: "1080p".into(), resolution: Resolution::R1080p,
frame_rate: String::new(), frame_rate: FrameRate::Unknown,
hdr: HdrFormat::Sdr, hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709, color_space: ColorSpace::Bt709,
secondary: false, secondary: false,
@@ -373,8 +385,8 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
0x24 => Some(Stream::Video(VideoStream { 0x24 => Some(Stream::Video(VideoStream {
pid: es_pid, pid: es_pid,
codec: Codec::Hevc, codec: Codec::Hevc,
resolution: "2160p".into(), resolution: Resolution::R2160p,
frame_rate: String::new(), frame_rate: FrameRate::Unknown,
hdr: HdrFormat::Sdr, hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709, color_space: ColorSpace::Bt709,
secondary: false, secondary: false,
@@ -383,8 +395,8 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
0xEA => Some(Stream::Video(VideoStream { 0xEA => Some(Stream::Video(VideoStream {
pid: es_pid, pid: es_pid,
codec: Codec::Vc1, codec: Codec::Vc1,
resolution: "1080p".into(), resolution: Resolution::R1080p,
frame_rate: String::new(), frame_rate: FrameRate::Unknown,
hdr: HdrFormat::Sdr, hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709, color_space: ColorSpace::Bt709,
secondary: false, secondary: false,
@@ -393,8 +405,8 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
0x02 => Some(Stream::Video(VideoStream { 0x02 => Some(Stream::Video(VideoStream {
pid: es_pid, pid: es_pid,
codec: Codec::Mpeg2, codec: Codec::Mpeg2,
resolution: "1080i".into(), resolution: Resolution::R1080i,
frame_rate: String::new(), frame_rate: FrameRate::Unknown,
hdr: HdrFormat::Sdr, hdr: HdrFormat::Sdr,
color_space: ColorSpace::Bt709, color_space: ColorSpace::Bt709,
secondary: false, secondary: false,
@@ -403,45 +415,45 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
0x81 => Some(Stream::Audio(AudioStream { 0x81 => Some(Stream::Audio(AudioStream {
pid: es_pid, pid: es_pid,
codec: Codec::Ac3, codec: Codec::Ac3,
channels: "5.1".into(), channels: AudioChannels::Surround51,
language: "und".into(), language: "und".into(),
sample_rate: "48kHz".into(), sample_rate: SampleRate::S48,
secondary: false, secondary: false,
label: String::new(), label: String::new(),
})), })),
0x83 => Some(Stream::Audio(AudioStream { 0x83 => Some(Stream::Audio(AudioStream {
pid: es_pid, pid: es_pid,
codec: Codec::TrueHd, codec: Codec::TrueHd,
channels: "5.1".into(), channels: AudioChannels::Surround51,
language: "und".into(), language: "und".into(),
sample_rate: "48kHz".into(), sample_rate: SampleRate::S48,
secondary: false, secondary: false,
label: String::new(), label: String::new(),
})), })),
0x84 | 0xA1 => Some(Stream::Audio(AudioStream { 0x84 | 0xA1 => Some(Stream::Audio(AudioStream {
pid: es_pid, pid: es_pid,
codec: Codec::Ac3Plus, codec: Codec::Ac3Plus,
channels: "5.1".into(), channels: AudioChannels::Surround51,
language: "und".into(), language: "und".into(),
sample_rate: "48kHz".into(), sample_rate: SampleRate::S48,
secondary: false, secondary: false,
label: String::new(), label: String::new(),
})), })),
0x85 | 0x86 => Some(Stream::Audio(AudioStream { 0x85 | 0x86 => Some(Stream::Audio(AudioStream {
pid: es_pid, pid: es_pid,
codec: Codec::DtsHdMa, codec: Codec::DtsHdMa,
channels: "5.1".into(), channels: AudioChannels::Surround51,
language: "und".into(), language: "und".into(),
sample_rate: "48kHz".into(), sample_rate: SampleRate::S48,
secondary: false, secondary: false,
label: String::new(), label: String::new(),
})), })),
0x82 => Some(Stream::Audio(AudioStream { 0x82 => Some(Stream::Audio(AudioStream {
pid: es_pid, pid: es_pid,
codec: Codec::Dts, codec: Codec::Dts,
channels: "5.1".into(), channels: AudioChannels::Surround51,
language: "und".into(), language: "und".into(),
sample_rate: "48kHz".into(), sample_rate: SampleRate::S48,
secondary: false, secondary: false,
label: String::new(), label: String::new(),
})), })),