Mux Blu-ray 3D (MVC) as a single MVC video track
Fold the MVC dependent (right-eye) view into the base H.264 track as a per-frame BlockAdditional under an mvcC BlockAdditionMapping, so a 3D title produces one MVC video track instead of two independent H.264 tracks. - h264: MVC-passthrough parser mode keeps the dependent view's subset SPS/PPS in-band, so each emitted frame is a self-contained dependent access unit for a BlockAdditional - resolve: route the dependent stream through the passthrough parser - mkvstream: detect the dependent view, pair it to the base frame by PTS (bounded FIFO), attach it as a BlockAdditional (BlockAddID=2), and skip building its own track; build the mvcC MVCDecoderConfigurationRecord from the captured subset SPS/PPS and set it on the base track at activation - mkv: emit the mvcC BlockAdditionMapping and BlockGroup/BlockAdditions, with a ReferenceBlock on non-keyframe base frames - ebml: add BlockAdditions/BlockMore/BlockAdditional/BlockAddID/ BlockAddIDValue/ReferenceBlock elements and a signed-int writer Verified against a Blu-ray 3D ISO: ffprobe shows a single MVC track, the mvcC mapping is present, ~144k BlockAdditionals carry the dependent view (8.7 GB), and the base view decodes cleanly with no regression. MVCDecoderConfigurationRecord follows ISO/IEC 14496-15 7.6.2; StereoMode is intentionally omitted (no enum value describes MVC-in-BlockAdditional; the mvcC mapping is the primary 3D signal per RFC 9559).
This commit is contained in:
+1
-1
@@ -277,7 +277,7 @@ impl Disc {
|
|||||||
streams.push(Stream::Video(VideoStream {
|
streams.push(Stream::Video(VideoStream {
|
||||||
pid: dep_pid,
|
pid: dep_pid,
|
||||||
secondary: true,
|
secondary: true,
|
||||||
label: "MVC dependent view (3D right eye)".to_string(),
|
label: crate::disc::MVC_DEPENDENT_LABEL.to_string(),
|
||||||
..base
|
..base
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,6 +216,22 @@ pub struct VideoStream {
|
|||||||
pub measured_cicp: Option<MeasuredCicp>,
|
pub measured_cicp: Option<MeasuredCicp>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
/// Measured CICP colour signalling read directly from a video elementary stream
|
||||||
/// (ITU-T H.273). Preferred over the coarse [`ColorSpace`] enum when present.
|
/// (ITU-T H.273). Preferred over the coarse [`ColorSpace`] enum when present.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|||||||
+26
-3
@@ -56,6 +56,13 @@ pub struct H264Parser {
|
|||||||
/// (HD-DVD EVO) path where the source stamps a PTS once per GOP. `None` on
|
/// (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.
|
/// the BD/UHD transport path, which carries a per-frame PTS.
|
||||||
reorder: Option<super::reorder::SparsePtsReorder>,
|
reorder: Option<super::reorder::SparsePtsReorder>,
|
||||||
|
/// 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 {
|
impl Default for H264Parser {
|
||||||
@@ -73,6 +80,7 @@ impl H264Parser {
|
|||||||
cur_sps: None,
|
cur_sps: None,
|
||||||
cur_pps: None,
|
cur_pps: None,
|
||||||
reorder: None,
|
reorder: None,
|
||||||
|
mvc_passthrough: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +93,15 @@ impl H264Parser {
|
|||||||
self
|
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
|
/// Route a finished frame through the PTS reorderer when enabled, else emit
|
||||||
/// it directly (unchanged transport-stream behaviour).
|
/// it directly (unchanged transport-stream behaviour).
|
||||||
fn finish(&mut self, explicit: Option<i64>, frame: Frame) -> Vec<Frame> {
|
fn finish(&mut self, explicit: Option<i64>, frame: Frame) -> Vec<Frame> {
|
||||||
@@ -192,17 +209,23 @@ impl CodecParser for H264Parser {
|
|||||||
// frame in the mux hot path (mirrors the HEVC parser).
|
// frame in the mux hot path (mirrors the HEVC parser).
|
||||||
let mut frame_data = Vec::with_capacity(pes.data.len() + 64);
|
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) {
|
for nal in NalIterator::new(&pes.data) {
|
||||||
let nal_type = nal[0] & 0x1F;
|
let nal_type = nal[0] & 0x1F;
|
||||||
|
|
||||||
match nal_type {
|
match nal_type {
|
||||||
// Param sets: seed avcC, strip if unchanged vs the active set,
|
// Param sets: seed avcC, strip if unchanged vs the active set,
|
||||||
// emit in-band on any change (incl. reverting to the avcC copy).
|
// 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 |=
|
emitted_sps |=
|
||||||
handle_param_set(&mut self.sps, &mut self.cur_sps, nal, &mut frame_data)
|
handle_param_set(&mut self.sps, &mut self.cur_sps, nal, &mut frame_data)
|
||||||
}
|
}
|
||||||
NAL_PPS => {
|
NAL_PPS if !mvc => {
|
||||||
emitted_pps |=
|
emitted_pps |=
|
||||||
handle_param_set(&mut self.pps, &mut self.cur_pps, nal, &mut frame_data)
|
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
|
// 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
|
// 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.
|
// can't revert it. Skipped per-type only when this AU already carried it.
|
||||||
if keyframe {
|
if keyframe && !mvc {
|
||||||
let mut prefix = Vec::new();
|
let mut prefix = Vec::new();
|
||||||
reassert_active(&mut prefix, &self.cur_sps, emitted_sps);
|
reassert_active(&mut prefix, &self.cur_sps, emitted_sps);
|
||||||
reassert_active(&mut prefix, &self.cur_pps, emitted_pps);
|
reassert_active(&mut prefix, &self.cur_pps, emitted_pps);
|
||||||
|
|||||||
@@ -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<dyn CodecParser> {
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -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).
|
/// Write a complete EBML float element (8-byte double).
|
||||||
pub fn write_float(w: &mut impl Write, id: u32, val: f64) -> io::Result<()> {
|
pub fn write_float(w: &mut impl Write, id: u32, val: f64) -> io::Result<()> {
|
||||||
write_id(w, id)?;
|
write_id(w, id)?;
|
||||||
@@ -481,6 +506,25 @@ pub const LUMINANCE_MIN: u32 = 0x55DA;
|
|||||||
pub const BLOCK_ADDITION_MAPPING: u32 = 0x41E4;
|
pub const BLOCK_ADDITION_MAPPING: u32 = 0x41E4;
|
||||||
pub const BLOCK_ADD_ID_TYPE: u32 = 0x41E7;
|
pub const BLOCK_ADD_ID_TYPE: u32 = 0x41E7;
|
||||||
pub const BLOCK_ADD_ID_EXTRA_DATA: u32 = 0x41ED;
|
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
|
// Audio
|
||||||
pub const AUDIO: u32 = 0xE1;
|
pub const AUDIO: u32 = 0xE1;
|
||||||
|
|||||||
+290
-8
@@ -68,6 +68,59 @@ const COLOUR_RANGE_LIMITED: u8 = 1;
|
|||||||
/// Vision configuration record (RFC 9559 + Dolby Vision-in-Matroska spec).
|
/// Vision configuration record (RFC 9559 + Dolby Vision-in-Matroska spec).
|
||||||
const BLOCK_ADD_ID_TYPE_DVCC: u64 = 0x6476_6343;
|
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<Vec<u8>> {
|
||||||
|
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,
|
/// Resolve a video stream's CICP colour code points — `(matrix, transfer,
|
||||||
/// primaries, range)`, ITU-T H.273 — using a single precedence so EVERY sink
|
/// 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
|
/// (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 +
|
/// stream is parsed. When `Some`, the serializer emits MasteringMetadata +
|
||||||
/// MaxCLL/MaxFALL inside Colour; when `None` they are omitted entirely.
|
/// MaxCLL/MaxFALL inside Colour; when `None` they are omitted entirely.
|
||||||
pub hdr10: Option<crate::mux::codec::Hdr10Metadata>,
|
pub hdr10: Option<crate::mux::codec::Hdr10Metadata>,
|
||||||
|
/// 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<u8>, Vec<u8>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a DOVIDecoderConfigurationRecord (dvcC) — 24 bytes — for the Matroska
|
/// 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
|
// coded picture's PictureInfo before the header is written (the same
|
||||||
// deferred path FieldOrder uses). `None` here → omitted unless seen.
|
// deferred path FieldOrder uses). `None` here → omitted unless seen.
|
||||||
hdr10: None,
|
hdr10: None,
|
||||||
|
mvc_params: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,6 +510,7 @@ impl MkvTrack {
|
|||||||
bit_depth: 0,
|
bit_depth: 0,
|
||||||
dv_config: None,
|
dv_config: None,
|
||||||
hdr10: None,
|
hdr10: None,
|
||||||
|
mvc_params: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,6 +548,7 @@ impl MkvTrack {
|
|||||||
bit_depth: 0,
|
bit_depth: 0,
|
||||||
dv_config: None,
|
dv_config: None,
|
||||||
hdr10: None,
|
hdr10: None,
|
||||||
|
mvc_params: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -565,6 +629,11 @@ pub struct MkvMuxer<W: Write + Seek> {
|
|||||||
/// Highest block timestamp (TimestampScale ticks) written across all tracks —
|
/// Highest block timestamp (TimestampScale ticks) written across all tracks —
|
||||||
/// the muxed runtime, used to back-patch the DURATION placeholder.
|
/// the muxed runtime, used to back-patch the DURATION placeholder.
|
||||||
max_block_ticks: i64,
|
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<i64>,
|
||||||
/// Per-AC-3-audio-track channel-correction state. The DVD IFO audio nibble
|
/// 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
|
/// 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
|
/// corrected from the AC-3 bitstream `acmod` of the first frame on the
|
||||||
@@ -923,6 +992,33 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
|||||||
ebml::end_master(&mut writer, vid_pos)?;
|
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
|
// Dolby Vision signaling — BlockAdditionMapping is a child of the
|
||||||
// TrackEntry (sibling of Video). Carries the dvcC so players /
|
// TrackEntry (sibling of Video). Carries the dvcC so players /
|
||||||
// mediainfo recognise the track as Dolby Vision.
|
// mediainfo recognise the track as Dolby Vision.
|
||||||
@@ -1027,6 +1123,7 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
|||||||
duration_secs,
|
duration_secs,
|
||||||
duration_patch_pos,
|
duration_patch_pos,
|
||||||
max_block_ticks: 0,
|
max_block_ticks: 0,
|
||||||
|
last_video_keyframe_ticks: None,
|
||||||
ac3_channel_fixups,
|
ac3_channel_fixups,
|
||||||
opening_capture: None,
|
opening_capture: None,
|
||||||
})
|
})
|
||||||
@@ -1065,6 +1162,43 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
|||||||
keyframe: bool,
|
keyframe: bool,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
duration_ns: Option<u64>,
|
duration_ns: Option<u64>,
|
||||||
|
) -> 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<u64>,
|
||||||
|
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<u64>,
|
||||||
|
block_additional: Option<&[u8]>,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
// --log-level 3: capture the first ~100 coded frames per track to the
|
// --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
|
// side file BEFORE any timeline mangling, with the codec parser's own
|
||||||
@@ -1226,15 +1360,45 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
|||||||
self.max_block_ticks = self.max_block_ticks.max(block_end_ticks);
|
self.max_block_ticks = self.max_block_ticks.max(block_end_ticks);
|
||||||
|
|
||||||
let relative_ts = (pts_ticks - self.cluster_ts_ticks) as i16;
|
let relative_ts = (pts_ticks - self.cluster_ts_ticks) as i16;
|
||||||
match duration_ns {
|
let duration_ticks =
|
||||||
Some(dur_ns) => {
|
duration_ns.map(|dur_ns| (dur_ns as i64 / TIMESTAMP_SCALE_NS).max(1) as u64);
|
||||||
// BlockDuration is in TimestampScale ticks, floored at 1.
|
match block_additional {
|
||||||
let duration_ticks = (dur_ns as i64 / TIMESTAMP_SCALE_NS).max(1) as u64;
|
// MVC: base view Block + dependent-view BlockAdditional, always a
|
||||||
self.write_block_group(track_idx + 1, relative_ts, keyframe, data, duration_ticks)?;
|
// BlockGroup. Non-keyframe base frames get a ReferenceBlock to the
|
||||||
}
|
// last keyframe (a keyframe carries none), so a player never treats a
|
||||||
None => {
|
// P/B frame as a seek point.
|
||||||
self.write_simple_block(track_idx + 1, relative_ts, keyframe, data)?;
|
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;
|
self.frame_count += 1;
|
||||||
|
|
||||||
@@ -1519,6 +1683,52 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
|||||||
ebml::end_master(&mut self.writer, bg_pos)?;
|
ebml::end_master(&mut self.writer, bg_pos)?;
|
||||||
Ok(())
|
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<i64>,
|
||||||
|
duration_ticks: Option<u64>,
|
||||||
|
) -> 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,
|
bit_depth: 0,
|
||||||
dv_config: None,
|
dv_config: None,
|
||||||
hdr10: None,
|
hdr10: None,
|
||||||
|
mvc_params: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1731,6 +1942,7 @@ mod tests {
|
|||||||
bit_depth: 0,
|
bit_depth: 0,
|
||||||
dv_config: None,
|
dv_config: None,
|
||||||
hdr10: None,
|
hdr10: None,
|
||||||
|
mvc_params: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4234,6 +4446,76 @@ mod tests {
|
|||||||
assert!(find_id(&data, ebml::BLOCK_ADDITION_MAPPING).is_none());
|
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<u8> = [
|
||||||
|
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) -------------
|
// ---- CodecPrivate emission (avcC / hvcC / VC-1 / MPEG-2) -------------
|
||||||
//
|
//
|
||||||
// `MkvTrack::video` always builds with `codec_private: None`; the PES mux
|
// `MkvTrack::video` always builds with `codec_private: None`; the PES mux
|
||||||
|
|||||||
+430
-52
@@ -120,14 +120,196 @@ struct PendingMux {
|
|||||||
video_track: Option<usize>,
|
video_track: Option<usize>,
|
||||||
/// `--log-level 3` opening-capture side-file path (if any).
|
/// `--log-level 3` opening-capture side-file path (if any).
|
||||||
opening_capture_path: Option<std::path::PathBuf>,
|
opening_capture_path: Option<std::path::PathBuf>,
|
||||||
/// Frames received before activation, replayed in order once built.
|
/// Frames received before activation, replayed in order once built. Each
|
||||||
buffered: Vec<crate::pes::PesFrame>,
|
/// 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<Vec<u8>>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Matroska container stream.
|
/// Matroska container stream.
|
||||||
pub struct MkvStream {
|
pub struct MkvStream {
|
||||||
disc_title: DiscTitle,
|
disc_title: DiscTitle,
|
||||||
mode: Mode,
|
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<MvcMerge>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<Option<usize>>,
|
||||||
|
/// Base frames (decode order) awaiting their dependent or a window flush.
|
||||||
|
pending_base: std::collections::VecDeque<PendingBase>,
|
||||||
|
/// Dependent AU data keyed by PTS, waiting for the matching base.
|
||||||
|
dep_by_pts: std::collections::HashMap<i64, Vec<u8>>,
|
||||||
|
/// `(subset_sps, pps)` from the first dependent AU — builds the `mvcC`
|
||||||
|
/// MVCDecoderConfigurationRecord for the base track's BlockAdditionMapping.
|
||||||
|
captured_params: Option<(Vec<u8>, Vec<u8>)>,
|
||||||
|
/// 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<Vec<u8>>)> {
|
||||||
|
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<Vec<u8>>)>) {
|
||||||
|
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<Vec<u8>>)> {
|
||||||
|
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<Box<dyn WriteSeek + Send>>,
|
||||||
|
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<u8>, Vec<u8>)> {
|
||||||
|
let mut subset_sps: Option<Vec<u8>> = None;
|
||||||
|
let mut pps: Option<Vec<u8>> = 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 {
|
impl MkvStream {
|
||||||
@@ -146,10 +328,30 @@ impl MkvStream {
|
|||||||
title: &DiscTitle,
|
title: &DiscTitle,
|
||||||
output_path: Option<&std::path::Path>,
|
output_path: Option<&std::path::Path>,
|
||||||
) -> io::Result<Self> {
|
) -> io::Result<Self> {
|
||||||
|
// 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 tracks = Vec::new();
|
||||||
let mut has_default_video = false;
|
let mut has_default_video = false;
|
||||||
let mut has_default_audio = 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<Option<usize>> = Vec::with_capacity(title.streams.len());
|
||||||
for (idx, s) in title.streams.iter().enumerate() {
|
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 {
|
let mut track = match s {
|
||||||
crate::disc::Stream::Video(v) => MkvTrack::video(v),
|
crate::disc::Stream::Video(v) => MkvTrack::video(v),
|
||||||
crate::disc::Stream::Audio(a) => MkvTrack::audio(a),
|
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()) {
|
if let Some(cp) = title.codec_privates.get(idx).and_then(|c| c.as_ref()) {
|
||||||
track.codec_private = Some(cp.clone());
|
track.codec_private = Some(cp.clone());
|
||||||
}
|
}
|
||||||
|
stream_to_track.push(Some(tracks.len()));
|
||||||
tracks.push(track);
|
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
|
// Defer muxer construction (and the TrackEntry dump) until the first
|
||||||
// coded picture arrives, so the primary video track's FieldOrder is set
|
// coded picture arrives, so the primary video track's FieldOrder is set
|
||||||
// from the parser's MEASURED value before the header is written — never
|
// from the parser's MEASURED value before the header is written — never
|
||||||
@@ -177,6 +400,7 @@ impl MkvStream {
|
|||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
disc_title: title.clone(),
|
disc_title: title.clone(),
|
||||||
|
mvc,
|
||||||
mode: Mode::Write(WriteMode::Pending(Box::new(PendingMux {
|
mode: Mode::Write(WriteMode::Pending(Box::new(PendingMux {
|
||||||
writer,
|
writer,
|
||||||
tracks,
|
tracks,
|
||||||
@@ -209,6 +433,24 @@ impl MkvStream {
|
|||||||
if let Some(vt) = pending.video_track {
|
if let Some(vt) = pending.video_track {
|
||||||
apply_coding_to_track(&mut pending.tracks[vt], coding, video_picture_seen);
|
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).
|
// --log-level 3: dump the FINAL TrackEntry metadata (field order set).
|
||||||
for (i, track) in pending.tracks.iter().enumerate() {
|
for (i, track) in pending.tracks.iter().enumerate() {
|
||||||
crate::diag::dump_mkv_track((i + 1) as u64, track);
|
crate::diag::dump_mkv_track((i + 1) as u64, track);
|
||||||
@@ -223,18 +465,72 @@ impl MkvStream {
|
|||||||
if let Some(path) = &pending.opening_capture_path {
|
if let Some(path) = &pending.opening_capture_path {
|
||||||
muxer.set_opening_capture(crate::diag::OpeningCapture::new(path, pending.tracks.len()));
|
muxer.set_opening_capture(crate::diag::OpeningCapture::new(path, pending.tracks.len()));
|
||||||
}
|
}
|
||||||
for f in pending.buffered.drain(..) {
|
for (f, additional) in pending.buffered.drain(..) {
|
||||||
muxer.write_frame(f.track, f.pts, f.keyframe, &f.data, f.duration_ns)?;
|
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)));
|
self.mode = Mode::Write(WriteMode::Active(Box::new(muxer)));
|
||||||
Ok(())
|
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.
|
/// Open an MKV file for reading → PES frames.
|
||||||
pub fn open(mut reader: impl Read + Send + 'static) -> io::Result<Self> {
|
pub fn open(mut reader: impl Read + Send + 'static) -> io::Result<Self> {
|
||||||
let (disc_title, codec_privates, ts_scale_ns) = parse_mkv_header(&mut reader)?;
|
let (disc_title, codec_privates, ts_scale_ns) = parse_mkv_header(&mut reader)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
disc_title,
|
disc_title,
|
||||||
|
mvc: None,
|
||||||
mode: Mode::Read(ReadState {
|
mode: Mode::Read(ReadState {
|
||||||
reader: Box::new(reader),
|
reader: Box::new(reader),
|
||||||
cluster_ts_ticks: 0,
|
cluster_ts_ticks: 0,
|
||||||
@@ -425,59 +721,40 @@ impl crate::pes::Stream for MkvStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn write(&mut self, frame: &crate::pes::PesFrame) -> io::Result<()> {
|
fn write(&mut self, frame: &crate::pes::PesFrame) -> io::Result<()> {
|
||||||
// Fast paths.
|
if matches!(self.mode, Mode::Read(_)) {
|
||||||
match &mut self.mode {
|
return Err(crate::error::Error::StreamReadOnly.into());
|
||||||
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(_)) => {}
|
|
||||||
}
|
}
|
||||||
// Pending: the first video frame (or the safety cap) triggers muxer
|
// Non-3D fast path: emit the frame directly, no clone, no buffering.
|
||||||
// construction; that frame's coding sets the field order. Other frames
|
if self.mvc.is_none() {
|
||||||
// buffer until then.
|
return self.emit(frame, None);
|
||||||
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(())
|
|
||||||
}
|
}
|
||||||
|
// 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<()> {
|
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
|
// A title that produced no frames (or only buffered ones) is still
|
||||||
// finalized into a valid MKV: activate now with no measured coding.
|
// finalized into a valid MKV: activate now with no measured coding.
|
||||||
if matches!(self.mode, Mode::Write(WriteMode::Pending(_))) {
|
if matches!(self.mode, Mode::Write(WriteMode::Pending(_))) {
|
||||||
@@ -916,6 +1193,107 @@ mod tests {
|
|||||||
use crate::pes::Stream as _;
|
use crate::pes::Stream as _;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
/// Length-prefix (4-byte big-endian) each NAL, as the H.264 parser emits.
|
||||||
|
fn lp(nals: &[&[u8]]) -> Vec<u8> {
|
||||||
|
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<u8>) -> 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]
|
#[test]
|
||||||
fn apply_coding_to_track_sets_measured_field_order_never_guesses() {
|
fn apply_coding_to_track_sets_measured_field_order_never_guesses() {
|
||||||
use crate::disc::{Codec, ColorSpace, FrameRate, HdrFormat, Resolution, VideoStream};
|
use crate::disc::{Codec, ColorSpace, FrameRate, HdrFormat, Resolution, VideoStream};
|
||||||
|
|||||||
+11
-1
@@ -575,7 +575,17 @@ fn build_demux_state(title: &DiscTitle, format: ContentFormat) -> DemuxState {
|
|||||||
pids.push(pid);
|
pids.push(pid);
|
||||||
pid_to_track.push((pid, idx));
|
pid_to_track.push((pid, idx));
|
||||||
let is_dvd_ps = matches!(format, ContentFormat::MpegPs);
|
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 {
|
let (ts, ps) = match format {
|
||||||
ContentFormat::MpegPs => (None, Some(super::ps::PsDemuxer::new())),
|
ContentFormat::MpegPs => (None, Some(super::ps::PsDemuxer::new())),
|
||||||
|
|||||||
Reference in New Issue
Block a user