Fix M2TS/MKV roundtrip: codec_private in FMKV header, Annex B conversion

Bug 1: M2TS roundtrip dropped frames — TsMuxer converts length-prefixed
NALs to Annex B, prepends VPS/SPS/PPS from HEVCDecoderConfigurationRecord.

Bug 2: MKV remux lost codec_private — MkvStream.codec_private() now returns
data from EBML header.

FMKV header carries codec_private (base64) per video stream for lossless
M2TS roundtrip.
This commit is contained in:
MattJackson
2026-04-15 16:09:34 +00:00
parent a2922b991f
commit 55d1aff2b3
6 changed files with 176 additions and 10 deletions
+34
View File
@@ -46,6 +46,9 @@ pub enum MetaStream {
label: String,
#[serde(default)]
secondary: bool,
/// Base64-encoded codec initialization data (HEVCDecoderConfigurationRecord, etc.)
#[serde(default, skip_serializing_if = "Option::is_none")]
codec_private: Option<String>,
},
#[serde(rename = "audio")]
Audio {
@@ -74,6 +77,20 @@ pub enum MetaStream {
}
impl M2tsMeta {
/// Build metadata from a disc Title with optional codec_private per stream.
pub fn from_title_with_privates(title: &DiscTitle, codec_privates: &[Option<Vec<u8>>]) -> Self {
let mut meta = Self::from_title(title);
for (i, s) in meta.streams.iter_mut().enumerate() {
if let MetaStream::Video { codec_private, .. } = s {
if let Some(Some(cp)) = codec_privates.get(i) {
use base64::Engine;
*codec_private = Some(base64::engine::general_purpose::STANDARD.encode(cp));
}
}
}
meta
}
/// Build metadata from a disc Title.
pub fn from_title(title: &DiscTitle) -> Self {
let streams = title
@@ -88,6 +105,7 @@ impl M2tsMeta {
hdr: v.hdr.id().into(),
label: v.label.clone(),
secondary: v.secondary,
codec_private: None,
},
Stream::Audio(a) => MetaStream::Audio {
pid: a.pid,
@@ -129,6 +147,7 @@ impl M2tsMeta {
hdr,
label,
secondary,
codec_private: _,
} => Stream::Video(VideoStream {
pid: *pid,
codec: codec.parse().unwrap(),
@@ -183,6 +202,21 @@ impl M2tsMeta {
content_format: crate::disc::ContentFormat::BdTs,
}
}
/// Extract codec_private data per stream (from FMKV header).
/// Returns a Vec matching stream order — None for streams without codec_private.
pub fn codec_privates(&self) -> Vec<Option<Vec<u8>>> {
self.streams.iter().map(|s| {
if let MetaStream::Video { codec_private: Some(ref b64), .. } = s {
{
use base64::Engine;
base64::engine::general_purpose::STANDARD.decode(b64).ok()
}
} else {
None
}
}).collect()
}
}
/// Write the metadata header to a writer. Padded to 192-byte boundary.