diff --git a/src/disc/bluray.rs b/src/disc/bluray.rs index 3aa9461..7a23704 100644 --- a/src/disc/bluray.rs +++ b/src/disc/bluray.rs @@ -277,7 +277,7 @@ impl Disc { streams.push(Stream::Video(VideoStream { pid: dep_pid, secondary: true, - label: "MVC dependent view (3D right eye)".to_string(), + label: crate::disc::MVC_DEPENDENT_LABEL.to_string(), ..base })); } diff --git a/src/disc/mod.rs b/src/disc/mod.rs index 8fa8c81..3ef3900 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -216,6 +216,22 @@ pub struct VideoStream { pub measured_cicp: Option, } +/// Label marking a video stream as the Blu-ray 3D **MVC dependent (right-eye) +/// view** — the paired substream of the AVC base view. Set by the BD scan +/// (`bluray.rs`) and recognised by the mux path (`resolve.rs` builds its parser +/// in param-set-preserving mode; `mkvstream.rs` folds it into the base track as +/// per-frame `BlockAdditional`). Single source of truth for the contract. +pub const MVC_DEPENDENT_LABEL: &str = "MVC dependent view (3D right eye)"; + +impl VideoStream { + /// Whether this video stream is the MVC dependent (right-eye) view — the + /// 3D substream that the muxer merges into the base track as a per-frame + /// `BlockAdditional` rather than emitting as an independent track. + pub fn is_mvc_dependent(&self) -> bool { + self.label == MVC_DEPENDENT_LABEL + } +} + /// Measured CICP colour signalling read directly from a video elementary stream /// (ITU-T H.273). Preferred over the coarse [`ColorSpace`] enum when present. #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/src/mux/codec/h264.rs b/src/mux/codec/h264.rs index f32bbea..1bfc6b2 100644 --- a/src/mux/codec/h264.rs +++ b/src/mux/codec/h264.rs @@ -56,6 +56,13 @@ pub struct H264Parser { /// (HD-DVD EVO) path where the source stamps a PTS once per GOP. `None` on /// the BD/UHD transport path, which carries a per-frame PTS. reorder: Option, + /// MVC dependent-view (Blu-ray 3D right-eye) passthrough mode. When set, the + /// parser does NOT strip SPS/PPS (nor re-assert at keyframes): every NAL — + /// subset SPS (type 15), prefix (14), coded-slice-extension (20), PPS (8) — + /// is length-prefixed in-band, so each emitted frame is a self-contained + /// dependent access unit suitable for a Matroska `BlockAdditional`. The base + /// view's avcC/param-set stripping is unchanged (separate parser instance). + mvc_passthrough: bool, } impl Default for H264Parser { @@ -73,6 +80,7 @@ impl H264Parser { cur_sps: None, cur_pps: None, reorder: None, + mvc_passthrough: false, } } @@ -85,6 +93,15 @@ impl H264Parser { self } + /// Enable MVC dependent-view passthrough (see the `mvc_passthrough` field): + /// keep every parameter set in-band so each frame is a self-contained + /// dependent access unit for a Matroska `BlockAdditional`. Used only for the + /// Blu-ray 3D dependent (right-eye) stream. + pub(crate) fn with_mvc_passthrough(mut self, enabled: bool) -> Self { + self.mvc_passthrough = enabled; + self + } + /// Route a finished frame through the PTS reorderer when enabled, else emit /// it directly (unchanged transport-stream behaviour). fn finish(&mut self, explicit: Option, frame: Frame) -> Vec { @@ -192,17 +209,23 @@ impl CodecParser for H264Parser { // frame in the mux hot path (mirrors the HEVC parser). let mut frame_data = Vec::with_capacity(pes.data.len() + 64); + // MVC dependent-view passthrough: keep ALL param sets in-band (the frame + // is a self-contained BlockAdditional access unit), never strip/re-assert. + let mvc = self.mvc_passthrough; + for nal in NalIterator::new(&pes.data) { let nal_type = nal[0] & 0x1F; match nal_type { // Param sets: seed avcC, strip if unchanged vs the active set, // emit in-band on any change (incl. reverting to the avcC copy). - NAL_SPS => { + // In MVC passthrough these fall through to the default arm so the + // subset SPS / PPS stay in-band (self-contained dependent AU). + NAL_SPS if !mvc => { emitted_sps |= handle_param_set(&mut self.sps, &mut self.cur_sps, nal, &mut frame_data) } - NAL_PPS => { + NAL_PPS if !mvc => { emitted_pps |= handle_param_set(&mut self.pps, &mut self.cur_pps, nal, &mut frame_data) } @@ -250,7 +273,7 @@ impl CodecParser for H264Parser { // ahead of the slices (even when unchanged vs codecPrivate) so a decoder // that dropped the set at a reset recovers, and a stale avcC re-apply // can't revert it. Skipped per-type only when this AU already carried it. - if keyframe { + if keyframe && !mvc { let mut prefix = Vec::new(); reassert_active(&mut prefix, &self.cur_sps, emitted_sps); reassert_active(&mut prefix, &self.cur_pps, emitted_pps); diff --git a/src/mux/codec/mod.rs b/src/mux/codec/mod.rs index 3902015..3ab4aac 100644 --- a/src/mux/codec/mod.rs +++ b/src/mux/codec/mod.rs @@ -207,6 +207,22 @@ pub fn parser_for_codec( } } +/// Build the codec parser for a Blu-ray 3D **MVC dependent (right-eye)** video +/// stream. Same codec space as the base view (H.264), but in param-set +/// passthrough mode so each emitted frame is a self-contained dependent access +/// unit for a Matroska `BlockAdditional`. Non-H.264 (unexpected) falls back to +/// the ordinary parser. +pub fn parser_for_mvc_dependent(codec: Codec, is_dvd_ps: bool) -> Box { + match codec { + Codec::H264 => Box::new( + h264::H264Parser::new() + .with_ps_reorder(is_dvd_ps) + .with_mvc_passthrough(true), + ), + _ => parser_for_codec(codec, None, is_dvd_ps), + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/mux/ebml.rs b/src/mux/ebml.rs index 38a0454..f6de89b 100644 --- a/src/mux/ebml.rs +++ b/src/mux/ebml.rs @@ -94,6 +94,31 @@ pub fn write_uint(w: &mut impl Write, id: u32, val: u64) -> io::Result<()> { } } +/// Write a complete EBML signed-integer element (two's-complement, big-endian, +/// minimal width). Used for `ReferenceBlock` (0xFB), whose value is a signed +/// tick offset relative to the current block's timestamp. +pub fn write_int(w: &mut impl Write, id: u32, val: i64) -> io::Result<()> { + write_id(w, id)?; + // Minimal two's-complement width: shrink while the top byte is pure sign + // extension of the next byte's MSB. + let be = val.to_be_bytes(); + let mut start = 0usize; + while start < 7 { + let sign_ext = if be[start + 1] & 0x80 != 0 { + 0xFF + } else { + 0x00 + }; + if be[start] != sign_ext { + break; + } + start += 1; + } + let bytes = &be[start..]; + write_size(w, bytes.len() as u64)?; + w.write_all(bytes) +} + /// Write a complete EBML float element (8-byte double). pub fn write_float(w: &mut impl Write, id: u32, val: f64) -> io::Result<()> { write_id(w, id)?; @@ -481,6 +506,25 @@ pub const LUMINANCE_MIN: u32 = 0x55DA; pub const BLOCK_ADDITION_MAPPING: u32 = 0x41E4; pub const BLOCK_ADD_ID_TYPE: u32 = 0x41E7; pub const BLOCK_ADD_ID_EXTRA_DATA: u32 = 0x41ED; +/// BlockAddIDValue (RFC 9559) — the value a per-frame `BlockAddID` references +/// to select this BlockAdditionMapping. Values ≥ 2 (1 is the default plain +/// BlockAdditional). Used by the MVC (`mvcC`) mapping for Blu-ray 3D. +pub const BLOCK_ADD_ID_VALUE: u32 = 0x41F0; + +// Block additions carried inside a BlockGroup — per-frame side data. For +// Blu-ray 3D (MVC) the dependent (right-eye) view NAL units for an access unit +// ride here as a BlockAdditional under the track's `mvcC` mapping (RFC 9559 +// §5.1.4.1.4; Matroska Codec Specifications §4.1.5). +pub const BLOCK_ADDITIONS: u32 = 0x75A1; +pub const BLOCK_MORE: u32 = 0xA6; +pub const BLOCK_ADDITIONAL: u32 = 0xA5; +pub const BLOCK_ADD_ID: u32 = 0xEE; +/// ReferenceBlock (RFC 9559 element 0xFB, child of BlockGroup) — signed +/// timestamp (in TimestampScale ticks) of a block this one references, relative +/// to this block's own timestamp. Its PRESENCE marks the Block as non-keyframe +/// (a keyframe Block in a BlockGroup carries none). Written for non-keyframe +/// video frames that must live in a BlockGroup to carry an MVC BlockAdditional. +pub const REFERENCE_BLOCK: u32 = 0xFB; // Audio pub const AUDIO: u32 = 0xE1; diff --git a/src/mux/mkv.rs b/src/mux/mkv.rs index b1f4ca9..5dbbd4c 100644 --- a/src/mux/mkv.rs +++ b/src/mux/mkv.rs @@ -68,6 +68,59 @@ const COLOUR_RANGE_LIMITED: u8 = 1; /// Vision configuration record (RFC 9559 + Dolby Vision-in-Matroska spec). const BLOCK_ADD_ID_TYPE_DVCC: u64 = 0x6476_6343; +/// BlockAddIDType "mvcC" — the MVCDecoderConfigurationRecord fourcc, big-endian +/// ASCII 'm''v''c''C'. Matroska BlockAdditionMapping/BlockAddIDType for a +/// Blu-ray 3D MVC configuration (RFC 9559 + Matroska Codec Specifications +/// §4.1.5; equals ISO/IEC 14496-15 MVCConfigurationBox('mvcC')). The per-frame +/// dependent (right-eye) view rides as a BlockAdditional under this mapping. +const BLOCK_ADD_ID_TYPE_MVCC: u64 = 0x6D76_6343; + +/// BlockAddIDValue for the MVC mapping — the value each per-frame `BlockAddID` +/// references (RFC 9559 requires ≥ 2; 1 is the default plain BlockAdditional). +const BLOCK_ADD_ID_VALUE_MVC: u64 = 2; + +/// Build an `MVCDecoderConfigurationRecord` (ISO/IEC 14496-15:2013 §7.6.2) from +/// the dependent view's subset-SPS (NAL type 15) and PPS NAL units. This is the +/// `BlockAddIDExtraData` for the `mvcC` BlockAdditionMapping (and, in the ISO +/// container, the `MVCConfigurationBox('mvcC')` payload). +/// +/// Layout mirrors the AVCDecoderConfigurationRecord except byte[4] repurposes +/// the AVC record's `bit(6) reserved` as +/// `complete_representation(1) | explicit_au_track(1) | reserved '1111'(4)`. +/// `profile`/`compat`/`level` describe the WHOLE MVC stream and come from the +/// subset SPS (bytes 1..=3). `length_size_minus_one` MUST match the base avcC +/// (freemkv always emits 4-byte length prefixes → 3). SPSs precede subset SPSs +/// in the array; here the array carries the dependent view's parameter sets. +/// +/// Returns `None` if either param set is absent, too short, or exceeds the +/// 16-bit length field (a param set > 65535 bytes is non-conforming and would +/// mis-frame the record). +fn mvc_decoder_config_record(subset_sps: &[u8], pps: &[u8]) -> Option> { + if subset_sps.len() < 4 || subset_sps.len() > 0xFFFF || pps.is_empty() || pps.len() > 0xFFFF { + return None; + } + let mut record = vec![ + 1, // configurationVersion + subset_sps[1], // AVCProfileIndication (whole MVC stream, from subset SPS) + subset_sps[2], // profile_compatibility + subset_sps[3], // AVCLevelIndication + // complete_representation(1)=1 | explicit_au_track(1)=0 | + // reserved '1111'(4) | lengthSizeMinusOne(2)=3(11) → 1_0_1111_11 = 0xBF. + 0xBF, + // reserved '0'(1) | numOfSequenceParameterSets(7)=1 → 0_0000001 = 0x01. + // NB: distinct from the AVC record's byte[5] (reserved(3)|numSPS(5)). + 0x01, + (subset_sps.len() >> 8) as u8, + subset_sps.len() as u8, + ]; + record.extend_from_slice(subset_sps); + record.push(1); // numOfPictureParameterSets + record.push((pps.len() >> 8) as u8); + record.push(pps.len() as u8); + record.extend_from_slice(pps); + Some(record) +} + /// Resolve a video stream's CICP colour code points — `(matrix, transfer, /// primaries, range)`, ITU-T H.273 — using a single precedence so EVERY sink /// (the MKV muxer here AND the FVI sidecar in `videomap.rs`) agrees and can @@ -176,6 +229,14 @@ pub struct MkvTrack { /// stream is parsed. When `Some`, the serializer emits MasteringMetadata + /// MaxCLL/MaxFALL inside Colour; when `None` they are omitted entirely. pub hdr10: Option, + /// Blu-ray 3D (MVC): the dependent (right-eye) view's `(subset_sps, pps)` + /// NAL units, from which the serializer builds the `mvcC` + /// MVCDecoderConfigurationRecord (ISO/IEC 14496-15 §7.6.2) for the track's + /// BlockAdditionMapping, and emits `StereoMode`. `None` for non-3D tracks. + /// Set at muxer activation from the dependent stream's parameter sets — the + /// same deferred path `hdr10`/FieldOrder use — never at construction. When + /// `Some`, the per-frame dependent view rides as a `BlockAdditional`. + pub mvc_params: Option<(Vec, Vec)>, } /// Build a DOVIDecoderConfigurationRecord (dvcC) — 24 bytes — for the Matroska @@ -380,6 +441,7 @@ impl MkvTrack { // coded picture's PictureInfo before the header is written (the same // deferred path FieldOrder uses). `None` here → omitted unless seen. hdr10: None, + mvc_params: None, } } @@ -448,6 +510,7 @@ impl MkvTrack { bit_depth: 0, dv_config: None, hdr10: None, + mvc_params: None, } } @@ -485,6 +548,7 @@ impl MkvTrack { bit_depth: 0, dv_config: None, hdr10: None, + mvc_params: None, } } } @@ -565,6 +629,11 @@ pub struct MkvMuxer { /// Highest block timestamp (TimestampScale ticks) written across all tracks — /// the muxed runtime, used to back-patch the DURATION placeholder. max_block_ticks: i64, + /// Timestamp (TimestampScale ticks) of the last video keyframe written on the + /// primary video track. A non-keyframe MVC base frame lives in a BlockGroup + /// (to carry its dependent-view BlockAdditional) and needs a `ReferenceBlock` + /// so players don't mistake it for a keyframe; it references this keyframe. + last_video_keyframe_ticks: Option, /// Per-AC-3-audio-track channel-correction state. The DVD IFO audio nibble /// is unreliable, so the channel count written in the track header is /// corrected from the AC-3 bitstream `acmod` of the first frame on the @@ -923,6 +992,33 @@ impl MkvMuxer { ebml::end_master(&mut writer, vid_pos)?; } + // Blu-ray 3D (MVC) signaling — BlockAdditionMapping (sibling of + // Video) carries the mvcC MVCDecoderConfigurationRecord so players / + // mediainfo recognise the dependent (right-eye) view that rides as a + // per-frame BlockAdditional under this mapping (BlockAddIDValue = 2). + if let Some((subset_sps, pps)) = track.mvc_params.as_ref() { + if let Some(record) = mvc_decoder_config_record(subset_sps, pps) { + let map_pos = ebml::start_master(&mut writer, ebml::BLOCK_ADDITION_MAPPING)?; + ebml::write_uint( + &mut writer, + ebml::BLOCK_ADD_ID_VALUE, + BLOCK_ADD_ID_VALUE_MVC, + )?; + ebml::write_uint(&mut writer, ebml::BLOCK_ADD_ID_TYPE, BLOCK_ADD_ID_TYPE_MVCC)?; + ebml::write_binary(&mut writer, ebml::BLOCK_ADD_ID_EXTRA_DATA, &record)?; + ebml::end_master(&mut writer, map_pos)?; + } else { + tracing::warn!( + target: "mux", + "MVC track: could not build MVCDecoderConfigurationRecord from the \ + dependent view's parameter sets (subset_sps={} B, pps={} B); \ + emitting no mvcC mapping — the 3D pairing will not be signalled.", + subset_sps.len(), + pps.len(), + ); + } + } + // Dolby Vision signaling — BlockAdditionMapping is a child of the // TrackEntry (sibling of Video). Carries the dvcC so players / // mediainfo recognise the track as Dolby Vision. @@ -1027,6 +1123,7 @@ impl MkvMuxer { duration_secs, duration_patch_pos, max_block_ticks: 0, + last_video_keyframe_ticks: None, ac3_channel_fixups, opening_capture: None, }) @@ -1065,6 +1162,43 @@ impl MkvMuxer { keyframe: bool, data: &[u8], duration_ns: Option, + ) -> io::Result<()> { + self.write_frame_inner(track_idx, pts_ns, keyframe, data, duration_ns, None) + } + + /// As [`write_frame`](Self::write_frame), but attaches `block_additional` to + /// the frame as a Matroska `BlockAdditional` (BlockAddID=2). Used for Blu-ray + /// 3D (MVC): the base view is the Block, the dependent (right-eye) access + /// unit rides as the BlockAdditional under the track's `mvcC` mapping. A frame + /// carrying an additional is always written as a `BlockGroup` (never a + /// SimpleBlock), with a `ReferenceBlock` when it is not a keyframe. + pub fn write_frame_with_additional( + &mut self, + track_idx: usize, + pts_ns: i64, + keyframe: bool, + data: &[u8], + duration_ns: Option, + block_additional: Option<&[u8]>, + ) -> io::Result<()> { + self.write_frame_inner( + track_idx, + pts_ns, + keyframe, + data, + duration_ns, + block_additional, + ) + } + + fn write_frame_inner( + &mut self, + track_idx: usize, + pts_ns: i64, + keyframe: bool, + data: &[u8], + duration_ns: Option, + block_additional: Option<&[u8]>, ) -> io::Result<()> { // --log-level 3: capture the first ~100 coded frames per track to the // side file BEFORE any timeline mangling, with the codec parser's own @@ -1226,15 +1360,45 @@ impl MkvMuxer { self.max_block_ticks = self.max_block_ticks.max(block_end_ticks); let relative_ts = (pts_ticks - self.cluster_ts_ticks) as i16; - match duration_ns { - Some(dur_ns) => { - // BlockDuration is in TimestampScale ticks, floored at 1. - let duration_ticks = (dur_ns as i64 / TIMESTAMP_SCALE_NS).max(1) as u64; - self.write_block_group(track_idx + 1, relative_ts, keyframe, data, duration_ticks)?; - } - None => { - self.write_simple_block(track_idx + 1, relative_ts, keyframe, data)?; + let duration_ticks = + duration_ns.map(|dur_ns| (dur_ns as i64 / TIMESTAMP_SCALE_NS).max(1) as u64); + match block_additional { + // MVC: base view Block + dependent-view BlockAdditional, always a + // BlockGroup. Non-keyframe base frames get a ReferenceBlock to the + // last keyframe (a keyframe carries none), so a player never treats a + // P/B frame as a seek point. + Some(additional) => { + let reference = if keyframe { + None + } else { + // Offset (ticks) of the referenced keyframe relative to this + // block. `None` only if no keyframe preceded (unreachable for a + // real non-keyframe); then the ReferenceBlock is simply omitted. + self.last_video_keyframe_ticks.map(|kf| kf - pts_ticks) + }; + self.write_block_group_mvc( + track_idx + 1, + relative_ts, + data, + additional, + reference, + duration_ticks, + )?; } + None => match duration_ticks { + // BlockDuration present (PGS subtitles) → BlockGroup. + Some(dt) => { + self.write_block_group(track_idx + 1, relative_ts, keyframe, data, dt)?; + } + None => { + self.write_simple_block(track_idx + 1, relative_ts, keyframe, data)?; + } + }, + } + // Remember the last video keyframe's tick so a later non-keyframe MVC + // base frame can reference it (see block_additional path above). + if is_video && keyframe { + self.last_video_keyframe_ticks = Some(pts_ticks); } self.frame_count += 1; @@ -1519,6 +1683,52 @@ impl MkvMuxer { ebml::end_master(&mut self.writer, bg_pos)?; Ok(()) } + + /// Write a BlockGroup carrying the base view Block plus the MVC dependent + /// (right-eye) access unit as a `BlockAdditional` (BlockAddID=2), per the + /// track's `mvcC` BlockAdditionMapping. A non-keyframe frame gets a + /// `ReferenceBlock` (`reference` = referenced keyframe offset in ticks) so it + /// is not mistaken for a seek point; `BlockDuration` is written when known. + fn write_block_group_mvc( + &mut self, + track_num: usize, + relative_ts: i16, + data: &[u8], + additional: &[u8], + reference: Option, + duration_ticks: Option, + ) -> io::Result<()> { + let (tv, tv_len) = track_vint(track_num); + let track_vint = &tv[..tv_len]; + // The 0x80 Keyframe flag is SimpleBlock-only; inside a BlockGroup Block + // it is reserved and MUST be 0 — keyframe-ness is signalled by the + // presence/absence of ReferenceBlock. + let flags: u8 = 0x00; + let block_size = track_vint.len() + 2 + 1 + data.len(); + + let bg_pos = ebml::start_master(&mut self.writer, ebml::BLOCK_GROUP)?; + ebml::write_id(&mut self.writer, ebml::BLOCK)?; + ebml::write_size(&mut self.writer, block_size as u64)?; + self.writer.write_all(track_vint)?; + self.writer.write_all(&relative_ts.to_be_bytes())?; + self.writer.write_all(&[flags])?; + self.writer.write_all(data)?; + if let Some(dt) = duration_ticks { + ebml::write_uint(&mut self.writer, ebml::BLOCK_DURATION, dt)?; + } + if let Some(ref_off) = reference { + ebml::write_int(&mut self.writer, ebml::REFERENCE_BLOCK, ref_off)?; + } + // BlockAdditions → BlockMore { BlockAddID=2, BlockAdditional=dependent AU }. + let adds_pos = ebml::start_master(&mut self.writer, ebml::BLOCK_ADDITIONS)?; + let more_pos = ebml::start_master(&mut self.writer, ebml::BLOCK_MORE)?; + ebml::write_uint(&mut self.writer, ebml::BLOCK_ADD_ID, BLOCK_ADD_ID_VALUE_MVC)?; + ebml::write_binary(&mut self.writer, ebml::BLOCK_ADDITIONAL, additional)?; + ebml::end_master(&mut self.writer, more_pos)?; + ebml::end_master(&mut self.writer, adds_pos)?; + ebml::end_master(&mut self.writer, bg_pos)?; + Ok(()) + } } // ============================================================ @@ -1702,6 +1912,7 @@ mod tests { bit_depth: 0, dv_config: None, hdr10: None, + mvc_params: None, } } @@ -1731,6 +1942,7 @@ mod tests { bit_depth: 0, dv_config: None, hdr10: None, + mvc_params: None, } } @@ -4234,6 +4446,76 @@ mod tests { assert!(find_id(&data, ebml::BLOCK_ADDITION_MAPPING).is_none()); } + #[test] + fn mvc_decoder_config_record_matches_iso_14496_15_layout() { + // subset SPS (NAL type 15): [nal_hdr, profile, compat, level, ...]. + let subset_sps = vec![0x6F, 0x80, 0x00, 0x33, 0x11, 0x22]; + let pps = vec![0x68, 0xEE, 0x3C]; + let rec = mvc_decoder_config_record(&subset_sps, &pps).expect("record builds"); + + // ISO/IEC 14496-15:2013 §7.6.2 byte layout, verbatim: + let expected: Vec = [ + vec![ + 1, // configurationVersion + 0x80, // AVCProfileIndication = subset_sps[1] + 0x00, // profile_compatibility = subset_sps[2] + 0x33, // AVCLevelIndication = subset_sps[3] + 0xBF, // complete_rep(1) explicit_au(0) reserved'1111' lengthSizeMinusOne=3 + 0x01, // reserved'0'(1) numOfSequenceParameterSets(7)=1 + 0x00, 0x06, // sequenceParameterSetLength = 6 + ], + subset_sps.clone(), + vec![ + 1, // numOfPictureParameterSets + 0x00, 0x03, // pictureParameterSetLength = 3 + ], + pps.clone(), + ] + .concat(); + assert_eq!(rec, expected, "MVCDecoderConfigurationRecord byte layout"); + + // Guards: too-short SPS and empty PPS both refuse (no corrupt record). + assert!(mvc_decoder_config_record(&[0x6F, 0x80, 0x00], &pps).is_none()); + assert!(mvc_decoder_config_record(&subset_sps, &[]).is_none()); + } + + #[test] + fn mvc_track_emits_mvcc_block_addition_mapping() { + // A track with mvc_params must emit BlockAdditionMapping (0x41E4) with the + // mvcC BlockAddIDType (0x6D766343) and a BlockAddIDValue (0x41F0), so + // players / mediainfo recognise the Blu-ray 3D dependent view. + let mut v = make_video_track(); + v.mvc_params = Some(( + vec![0x6F, 0x80, 0x00, 0x33, 0x11, 0x22], + vec![0x68, 0xEE, 0x3C], + )); + let muxer = MkvMuxer::new(Cursor::new(Vec::new()), &[v], None, 0.0, &[]).unwrap(); + let data = muxer.writer.into_inner(); + assert!( + find_id(&data, ebml::BLOCK_ADDITION_MAPPING).is_some(), + "MVC track must emit BlockAdditionMapping" + ); + assert!( + find_id(&data, ebml::BLOCK_ADD_ID_VALUE).is_some(), + "MVC mapping must carry BlockAddIDValue" + ); + assert!( + data.windows(4).any(|w| w == [0x6D, 0x76, 0x63, 0x43]), + "mvcC fourcc must be present as the BlockAddIDType value" + ); + // Without mvc_params, no mapping. + let muxer = MkvMuxer::new( + Cursor::new(Vec::new()), + &[make_video_track()], + None, + 0.0, + &[], + ) + .unwrap(); + let data = muxer.writer.into_inner(); + assert!(find_id(&data, ebml::BLOCK_ADDITION_MAPPING).is_none()); + } + // ---- CodecPrivate emission (avcC / hvcC / VC-1 / MPEG-2) ------------- // // `MkvTrack::video` always builds with `codec_private: None`; the PES mux diff --git a/src/mux/mkvstream.rs b/src/mux/mkvstream.rs index 971e84c..955dae0 100644 --- a/src/mux/mkvstream.rs +++ b/src/mux/mkvstream.rs @@ -120,14 +120,196 @@ struct PendingMux { video_track: Option, /// `--log-level 3` opening-capture side-file path (if any). opening_capture_path: Option, - /// Frames received before activation, replayed in order once built. - buffered: Vec, + /// Frames received before activation, replayed in order once built. Each + /// carries an optional MVC dependent-view `BlockAdditional` (present only + /// for a 3D base-view frame that was already paired before activation). + buffered: Vec<(crate::pes::PesFrame, Option>)>, } /// Matroska container stream. pub struct MkvStream { disc_title: DiscTitle, mode: Mode, + /// Blu-ray 3D (MVC) merge state — present iff the title carries an MVC + /// dependent (right-eye) view. Folds the dependent stream's frames into the + /// base video track as per-frame `BlockAdditional`, paired by PTS, so the + /// output is a single MVC track instead of two independent H.264 tracks. + mvc: Option, +} + +/// Largest number of base frames held awaiting their PTS-matching dependent AU +/// before the oldest is flushed unpaired (a plain Block). The SSIF interleaves +/// base and dependent access units per unit, so a base's dependent normally +/// arrives within one or two frames; this window only bounds memory/latency for +/// a stream where the pairing drifts. +const MVC_PAIR_WINDOW: usize = 32; + +/// A base-view frame (track already remapped to the muxer's base track index) +/// awaiting — or already carrying — its dependent-view `BlockAdditional`. +struct PendingBase { + frame: crate::pes::PesFrame, + additional: Option>, +} + +/// State for folding the MVC dependent (right-eye) view into the base track. +struct MvcMerge { + /// `title.streams` index of the base (left-eye) video stream. + base_stream_idx: usize, + /// `title.streams` index of the dependent (right-eye) video stream. + dep_stream_idx: usize, + /// Muxer track index of the base view — where the dependent AU is attached + /// as a `BlockAdditional` and where `mvc_params` (the `mvcC` mapping) lives. + base_track_idx: usize, + /// `title.streams` index → muxer track index. The dependent maps to `None` + /// (it becomes a BlockAdditional, not a track); every other stream shifts + /// down by one if it followed the dependent in stream order. + stream_to_track: Vec>, + /// Base frames (decode order) awaiting their dependent or a window flush. + pending_base: std::collections::VecDeque, + /// Dependent AU data keyed by PTS, waiting for the matching base. + dep_by_pts: std::collections::HashMap>, + /// `(subset_sps, pps)` from the first dependent AU — builds the `mvcC` + /// MVCDecoderConfigurationRecord for the base track's BlockAdditionMapping. + captured_params: Option<(Vec, Vec)>, + /// Count of dependent AUs dropped with no matching base (diagnostic). + orphan_deps: u64, +} + +impl MvcMerge { + /// Ingest one incoming frame; returns `(frame, additional)` pairs ready to + /// hand to the muxer, in emit order. Base frames buffer briefly to pair with + /// their dependent by PTS; the dependent stream produces no frames of its own + /// (it becomes `BlockAdditional`); all other streams pass straight through + /// with their track index remapped. + fn ingest( + &mut self, + frame: &crate::pes::PesFrame, + ) -> Vec<(crate::pes::PesFrame, Option>)> { + let mut out = Vec::new(); + if frame.track == self.dep_stream_idx { + if self.captured_params.is_none() { + self.captured_params = extract_mvc_params(&frame.data); + } + // Attach to a waiting base of the same PTS, else stash by PTS. + if let Some(pb) = self + .pending_base + .iter_mut() + .find(|pb| pb.frame.pts == frame.pts && pb.additional.is_none()) + { + pb.additional = Some(frame.data.clone()); + } else { + self.dep_by_pts.insert(frame.pts, frame.data.clone()); + // Bound the orphan map: if dependents pile up unpaired (pairing + // badly drifted), drop the surplus rather than grow unbounded. + if self.dep_by_pts.len() > MVC_PAIR_WINDOW * 4 { + self.orphan_deps += self.dep_by_pts.len() as u64; + self.dep_by_pts.clear(); + } + } + } else if frame.track == self.base_stream_idx { + let additional = self.dep_by_pts.remove(&frame.pts); + let mut remapped = frame.clone(); + remapped.track = self.base_track_idx; + self.pending_base.push_back(PendingBase { + frame: remapped, + additional, + }); + } else { + // Audio / subtitle / other video: remap the track index and forward. + let mut remapped = frame.clone(); + if let Some(Some(t)) = self.stream_to_track.get(frame.track) { + remapped.track = *t; + out.push((remapped, None)); + } + } + self.drain_ready(&mut out); + out + } + + /// Emit base frames from the FIFO front once each has its dependent attached, + /// or flush the oldest unpaired base as a plain Block when the window is full. + fn drain_ready(&mut self, out: &mut Vec<(crate::pes::PesFrame, Option>)>) { + loop { + let front_ready = self + .pending_base + .front() + .map(|pb| pb.additional.is_some()) + .unwrap_or(false); + if front_ready || self.pending_base.len() > MVC_PAIR_WINDOW { + if let Some(pb) = self.pending_base.pop_front() { + out.push((pb.frame, pb.additional)); + continue; + } + } + break; + } + } + + /// Flush every remaining buffered base frame (unpaired → plain Block) at EOF. + fn flush(&mut self) -> Vec<(crate::pes::PesFrame, Option>)> { + let mut out = Vec::new(); + for pb in self.pending_base.drain(..) { + out.push((pb.frame, pb.additional)); + } + self.orphan_deps += self.dep_by_pts.len() as u64; + self.dep_by_pts.clear(); + out + } +} + +/// Hand a frame to the muxer: the plain path (`write_frame`) when there is no +/// MVC dependent view to attach, the BlockAdditional path otherwise. +fn emit_to_muxer( + m: &mut MkvMuxer>, + frame: &crate::pes::PesFrame, + additional: Option<&[u8]>, +) -> io::Result<()> { + match additional { + None => m.write_frame( + frame.track, + frame.pts, + frame.keyframe, + &frame.data, + frame.duration_ns, + ), + Some(_) => m.write_frame_with_additional( + frame.track, + frame.pts, + frame.keyframe, + &frame.data, + frame.duration_ns, + additional, + ), + } +} + +/// Scan a length-prefixed (4-byte big-endian) H.264 NAL stream for the first +/// subset SPS (NAL type 15) and first PPS (NAL type 8) — the two parameter sets +/// that populate the `mvcC` MVCDecoderConfigurationRecord. Returns +/// `Some((subset_sps, pps))` only when BOTH are found; `None` otherwise (the +/// serializer then emits no mvcC mapping and logs it). +fn extract_mvc_params(data: &[u8]) -> Option<(Vec, Vec)> { + let mut subset_sps: Option> = None; + let mut pps: Option> = None; + let mut i = 0usize; + while i + 4 <= data.len() { + let len = u32::from_be_bytes([data[i], data[i + 1], data[i + 2], data[i + 3]]) as usize; + i += 4; + if len == 0 || i + len > data.len() { + break; + } + let nal = &data[i..i + len]; + i += len; + match nal[0] & 0x1F { + 15 if subset_sps.is_none() => subset_sps = Some(nal.to_vec()), + 8 if pps.is_none() => pps = Some(nal.to_vec()), + _ => {} + } + if subset_sps.is_some() && pps.is_some() { + break; + } + } + Some((subset_sps?, pps?)) } impl MkvStream { @@ -146,10 +328,30 @@ impl MkvStream { title: &DiscTitle, output_path: Option<&std::path::Path>, ) -> io::Result { + // Blu-ray 3D (MVC): a dependent (right-eye) view stream is NOT emitted as + // its own track — it is folded into the base track as per-frame + // BlockAdditional. Detect it so we skip building a track for it and set up + // the merge. `base_stream_idx` is the first video stream. + let dep_stream_idx = title + .streams + .iter() + .position(|s| matches!(s, crate::disc::Stream::Video(v) if v.is_mvc_dependent())); + let base_stream_idx = title + .streams + .iter() + .position(|s| matches!(s, crate::disc::Stream::Video(_))); + let mut tracks = Vec::new(); let mut has_default_video = false; let mut has_default_audio = false; + // `title.streams` index → muxer track index (`None` = the dependent view, + // which has no track). Streams after the dependent shift down by one. + let mut stream_to_track: Vec> = Vec::with_capacity(title.streams.len()); for (idx, s) in title.streams.iter().enumerate() { + if Some(idx) == dep_stream_idx { + stream_to_track.push(None); + continue; + } let mut track = match s { crate::disc::Stream::Video(v) => MkvTrack::video(v), crate::disc::Stream::Audio(a) => MkvTrack::audio(a), @@ -166,9 +368,30 @@ impl MkvStream { if let Some(cp) = title.codec_privates.get(idx).and_then(|c| c.as_ref()) { track.codec_private = Some(cp.clone()); } + stream_to_track.push(Some(tracks.len())); tracks.push(track); } + // Assemble the MVC merge when a dependent view is present and paired with + // a base video track. + let mvc = match (dep_stream_idx, base_stream_idx) { + (Some(dep_stream_idx), Some(base_stream_idx)) => { + let base_track_idx = + stream_to_track[base_stream_idx].expect("base video stream always has a track"); + Some(MvcMerge { + base_stream_idx, + dep_stream_idx, + base_track_idx, + stream_to_track, + pending_base: std::collections::VecDeque::new(), + dep_by_pts: std::collections::HashMap::new(), + captured_params: None, + orphan_deps: 0, + }) + } + _ => None, + }; + // Defer muxer construction (and the TrackEntry dump) until the first // coded picture arrives, so the primary video track's FieldOrder is set // from the parser's MEASURED value before the header is written — never @@ -177,6 +400,7 @@ impl MkvStream { Ok(Self { disc_title: title.clone(), + mvc, mode: Mode::Write(WriteMode::Pending(Box::new(PendingMux { writer, tracks, @@ -209,6 +433,24 @@ impl MkvStream { if let Some(vt) = pending.video_track { apply_coding_to_track(&mut pending.tracks[vt], coding, video_picture_seen); } + // Blu-ray 3D: set the base video track's `mvc_params` from the dependent + // view's captured subset-SPS/PPS BEFORE the header is written, so the + // TrackEntry carries the `mvcC` BlockAdditionMapping. Captured from the + // first dependent AU (which arrives right after the first base AU in the + // SSIF), so it is available by the time the first base frame activates. + if let Some(mvc) = &self.mvc { + if let Some(params) = &mvc.captured_params { + if let Some(t) = pending.tracks.get_mut(mvc.base_track_idx) { + t.mvc_params = Some(params.clone()); + } + } else { + tracing::warn!( + target: "mux", + "MVC: no dependent-view subset-SPS/PPS captured before activation; \ + the base track will carry no mvcC mapping (3D not signalled)." + ); + } + } // --log-level 3: dump the FINAL TrackEntry metadata (field order set). for (i, track) in pending.tracks.iter().enumerate() { crate::diag::dump_mkv_track((i + 1) as u64, track); @@ -223,18 +465,72 @@ impl MkvStream { if let Some(path) = &pending.opening_capture_path { muxer.set_opening_capture(crate::diag::OpeningCapture::new(path, pending.tracks.len())); } - for f in pending.buffered.drain(..) { - muxer.write_frame(f.track, f.pts, f.keyframe, &f.data, f.duration_ns)?; + for (f, additional) in pending.buffered.drain(..) { + muxer.write_frame_with_additional( + f.track, + f.pts, + f.keyframe, + &f.data, + f.duration_ns, + additional.as_deref(), + )?; } self.mode = Mode::Write(WriteMode::Active(Box::new(muxer))); Ok(()) } + /// Emit one frame (track index already muxer-relative) with an optional MVC + /// dependent-view `BlockAdditional`, honouring the deferred-activation + /// machinery: the first video frame triggers muxer construction (its coding + /// sets FieldOrder); earlier frames buffer. `additional` is `None` for every + /// non-3D frame and for the 3D base frames that had no paired dependent. + fn emit(&mut self, frame: &crate::pes::PesFrame, additional: Option<&[u8]>) -> io::Result<()> { + match &mut self.mode { + Mode::Read(_) => return Err(crate::error::Error::StreamReadOnly.into()), + Mode::Write(WriteMode::Active(m)) => { + return emit_to_muxer(m, frame, additional); + } + Mode::Write(WriteMode::Building) => return Ok(()), + Mode::Write(WriteMode::Pending(_)) => {} + } + // Pending: the first video frame (or the safety cap) triggers muxer + // construction; that frame's coding sets the field order. Other frames + // buffer until then. + let (activate_now, use_coding) = match &self.mode { + Mode::Write(WriteMode::Pending(p)) => { + let is_video = match p.video_track { + Some(vt) => frame.track == vt, + // No video track: nothing to wait for — build on frame one. + None => true, + }; + (is_video || p.buffered.len() >= MAX_PENDING_FRAMES, is_video) + } + _ => unreachable!("guarded above"), + }; + if activate_now { + // Pass the trigger frame's coding only when it IS the video frame; a + // cap-triggered build never saw the video frame, so nothing measured + // is passed (apply_coding_to_track then logs + leaves UNDETERMINED). + self.activate(if use_coding { frame.coding } else { None }, use_coding)?; + if let Mode::Write(WriteMode::Active(m)) = &mut self.mode { + return emit_to_muxer(m, frame, additional); + } + Ok(()) + } else { + if let Mode::Write(WriteMode::Pending(p)) = &mut self.mode { + p.buffered + .push((frame.clone(), additional.map(|a| a.to_vec()))); + } + Ok(()) + } + } + /// Open an MKV file for reading → PES frames. pub fn open(mut reader: impl Read + Send + 'static) -> io::Result { let (disc_title, codec_privates, ts_scale_ns) = parse_mkv_header(&mut reader)?; Ok(Self { disc_title, + mvc: None, mode: Mode::Read(ReadState { reader: Box::new(reader), cluster_ts_ticks: 0, @@ -425,59 +721,40 @@ impl crate::pes::Stream for MkvStream { } fn write(&mut self, frame: &crate::pes::PesFrame) -> io::Result<()> { - // Fast paths. - match &mut self.mode { - Mode::Read(_) => return Err(crate::error::Error::StreamReadOnly.into()), - Mode::Write(WriteMode::Active(m)) => { - return m.write_frame( - frame.track, - frame.pts, - frame.keyframe, - &frame.data, - frame.duration_ns, - ); - } - Mode::Write(WriteMode::Building) => return Ok(()), - Mode::Write(WriteMode::Pending(_)) => {} + if matches!(self.mode, Mode::Read(_)) { + return Err(crate::error::Error::StreamReadOnly.into()); } - // Pending: the first video frame (or the safety cap) triggers muxer - // construction; that frame's coding sets the field order. Other frames - // buffer until then. - let (activate_now, use_coding) = match &self.mode { - Mode::Write(WriteMode::Pending(p)) => { - let is_video = match p.video_track { - Some(vt) => frame.track == vt, - // No video track: nothing to wait for — build on frame one. - None => true, - }; - (is_video || p.buffered.len() >= MAX_PENDING_FRAMES, is_video) - } - _ => unreachable!("guarded above"), - }; - if activate_now { - // Pass the trigger frame's coding only when it IS the video frame; a - // cap-triggered build never saw the video frame, so nothing measured - // is passed (apply_coding_to_track then logs + leaves UNDETERMINED). - self.activate(if use_coding { frame.coding } else { None }, use_coding)?; - if let Mode::Write(WriteMode::Active(m)) = &mut self.mode { - return m.write_frame( - frame.track, - frame.pts, - frame.keyframe, - &frame.data, - frame.duration_ns, - ); - } - Ok(()) - } else { - if let Mode::Write(WriteMode::Pending(p)) = &mut self.mode { - p.buffered.push(frame.clone()); - } - Ok(()) + // Non-3D fast path: emit the frame directly, no clone, no buffering. + if self.mvc.is_none() { + return self.emit(frame, None); } + // Blu-ray 3D: run the frame through the MVC merge, which remaps track + // indices, folds the dependent view into the base as BlockAdditional + // (paired by PTS), and yields 0+ frames ready to emit. `ingest` returns + // owned pairs so the `self.mvc` borrow is released before `emit`. + let emits = self.mvc.as_mut().unwrap().ingest(frame); + for (f, additional) in emits { + self.emit(&f, additional.as_deref())?; + } + Ok(()) } fn finish(&mut self) -> io::Result<()> { + // Blu-ray 3D: flush any base frames still awaiting a dependent (emitted + // unpaired as plain Blocks) before finalizing. + if let Some(mvc) = self.mvc.as_mut() { + let tail = mvc.flush(); + let orphans = mvc.orphan_deps; + if orphans > 0 { + tracing::debug!( + target: "mux", + "MVC: {orphans} dependent-view access units had no matching base frame (dropped)" + ); + } + for (f, additional) in tail { + self.emit(&f, additional.as_deref())?; + } + } // A title that produced no frames (or only buffered ones) is still // finalized into a valid MKV: activate now with no measured coding. if matches!(self.mode, Mode::Write(WriteMode::Pending(_))) { @@ -916,6 +1193,107 @@ mod tests { use crate::pes::Stream as _; use std::io::Cursor; + /// Length-prefix (4-byte big-endian) each NAL, as the H.264 parser emits. + fn lp(nals: &[&[u8]]) -> Vec { + let mut v = Vec::new(); + for n in nals { + v.extend_from_slice(&(n.len() as u32).to_be_bytes()); + v.extend_from_slice(n); + } + v + } + + fn mvc_frame(track: usize, pts: i64, keyframe: bool, data: Vec) -> crate::pes::PesFrame { + crate::pes::PesFrame { + track, + pts, + keyframe, + data, + duration_ns: None, + source: None, + coding: None, + } + } + + // A subset SPS (NAL type 15), a PPS (type 8), and a coded-slice-extension + // (type 20) — the shape of a dependent-view access unit. + const SUBSET_SPS: [u8; 5] = [0x6F, 0x80, 0x00, 0x33, 0xAA]; // 0x6F & 0x1F = 15 + const DEP_PPS: [u8; 3] = [0x68, 0xEE, 0x3C]; // 0x68 & 0x1F = 8 + const DEP_SLICE: [u8; 3] = [0x74, 0x11, 0x22]; // 0x74 & 0x1F = 20 + + #[test] + fn extract_mvc_params_finds_subset_sps_and_pps() { + let data = lp(&[&SUBSET_SPS, &DEP_PPS, &DEP_SLICE]); + let (s, p) = extract_mvc_params(&data).expect("both param sets present"); + assert_eq!(s, SUBSET_SPS, "subset SPS (NAL 15) captured verbatim"); + assert_eq!(p, DEP_PPS, "PPS (NAL 8) captured verbatim"); + // Missing PPS → None (the serializer then emits no mvcC mapping). + assert!(extract_mvc_params(&lp(&[&SUBSET_SPS, &DEP_SLICE])).is_none()); + // Missing subset SPS → None. + assert!(extract_mvc_params(&lp(&[&DEP_PPS, &DEP_SLICE])).is_none()); + } + + fn empty_merge() -> MvcMerge { + MvcMerge { + base_stream_idx: 0, + dep_stream_idx: 2, + base_track_idx: 0, + stream_to_track: vec![Some(0), Some(1), None], + pending_base: std::collections::VecDeque::new(), + dep_by_pts: std::collections::HashMap::new(), + captured_params: None, + orphan_deps: 0, + } + } + + #[test] + fn mvc_merge_pairs_base_and_dependent_by_pts() { + let mut m = empty_merge(); + let dep = lp(&[&SUBSET_SPS, &DEP_PPS, &DEP_SLICE]); + + // Base arrives first (SSIF order): buffered, nothing emitted yet. + let e = m.ingest(&mvc_frame(0, 100, true, lp(&[&[0x65, 1, 2]]))); + assert!(e.is_empty(), "base held until its dependent arrives"); + + // Dependent arrives → base is emitted, remapped to the base track, with + // the dependent AU as its BlockAdditional; params are captured. + let e = m.ingest(&mvc_frame(2, 100, false, dep.clone())); + assert_eq!(e.len(), 1, "the paired base frame is emitted"); + assert_eq!(e[0].0.track, 0, "remapped to the base muxer track"); + assert_eq!( + e[0].1.as_deref(), + Some(dep.as_slice()), + "dependent attached" + ); + assert!(m.captured_params.is_some(), "mvcC params captured"); + + // Audio passes straight through (remapped, no additional). + let e = m.ingest(&mvc_frame(1, 100, true, vec![0xAA])); + assert_eq!(e.len(), 1); + assert_eq!(e[0].0.track, 1); + assert!(e[0].1.is_none()); + + // Dependent-before-base (reordered) also pairs. + let dep2 = lp(&[&DEP_SLICE]); + assert!(m.ingest(&mvc_frame(2, 200, false, dep2.clone())).is_empty()); + let e = m.ingest(&mvc_frame(0, 200, false, lp(&[&[0x61, 3, 4]]))); + assert_eq!(e.len(), 1); + assert_eq!(e[0].1.as_deref(), Some(dep2.as_slice())); + } + + #[test] + fn mvc_merge_flushes_unpaired_base_at_eof() { + let mut m = empty_merge(); + // Base with no dependent ever → held, then flushed unpaired at EOF. + assert!( + m.ingest(&mvc_frame(0, 10, true, vec![0, 0, 0, 1])) + .is_empty() + ); + let tail = m.flush(); + assert_eq!(tail.len(), 1, "unpaired base still emitted"); + assert!(tail[0].1.is_none(), "no BlockAdditional when unpaired"); + } + #[test] fn apply_coding_to_track_sets_measured_field_order_never_guesses() { use crate::disc::{Codec, ColorSpace, FrameRate, HdrFormat, Resolution, VideoStream}; diff --git a/src/mux/resolve.rs b/src/mux/resolve.rs index 6f71751..050a874 100644 --- a/src/mux/resolve.rs +++ b/src/mux/resolve.rs @@ -575,7 +575,17 @@ fn build_demux_state(title: &DiscTitle, format: ContentFormat) -> DemuxState { pids.push(pid); pid_to_track.push((pid, idx)); let is_dvd_ps = matches!(format, ContentFormat::MpegPs); - parsers.push((pid, super::codec::parser_for_codec(codec, None, is_dvd_ps))); + // The Blu-ray 3D MVC dependent (right-eye) view uses a param-set- + // passthrough H.264 parser so each frame is a self-contained dependent + // access unit for a BlockAdditional; every other stream uses the + // ordinary parser for its codec. + let parser = match s { + crate::disc::Stream::Video(v) if v.is_mvc_dependent() => { + super::codec::parser_for_mvc_dependent(codec, is_dvd_ps) + } + _ => super::codec::parser_for_codec(codec, None, is_dvd_ps), + }; + parsers.push((pid, parser)); } let (ts, ps) = match format { ContentFormat::MpegPs => (None, Some(super::ps::PsDemuxer::new())),