feat(mux): emit HDR10 static metadata from HEVC SEI
Parse the two HDR10 HEVC SEI messages and emit the corresponding Matroska Colour metadata, only when actually present in the bitstream (SDR / no-SEI tracks omit it; nothing is fabricated). Parse (Rec. ITU-T H.265 Annex D): - Mastering Display Colour Volume SEI, payloadType 137 (D.2.28): display_primaries_x/y[3] (SEI order G,B,R), white_point_x/y (0.00002 units), max/min_display_mastering_luminance (0.0001 cd/m²). - Content Light Level Info SEI, payloadType 144 (D.2.35): MaxCLL / MaxFALL (cd/m² integers). HevcParser::scan_sei walks the sei_rbsp ff-extension payloadType/ payloadSize coding and de-emulates (00 00 03) before reading, reusing the existing strip_emulation_prevention helper. Both SEI are required before any metadata is surfaced; SEI NALs still pass through unchanged. Carry: the measured Hdr10Metadata rides PictureInfo (the same per-coded- picture seam FieldOrder uses), flowing through from_codec_frame onto PesFrame.coding to the deferred-muxer activate path, where apply_coding_to_track stamps it on the video track before the header is written. Set only when both SEI were seen. Emit (RFC 9559 / Matroska): new Colour children in ebml.rs (MasteringMetadata 0x55D0, Primary R/G/B + WhitePoint chromaticity 0x55D1..0x55D8, Luminance max/min 0x55D9/0x55DA, MaxCLL 0x55BC, MaxFALL 0x55BD). write_hdr10 converts chromaticity SEI int × 0.00002 → Matroska float, luminance SEI int × 0.0001 → cd/m² float; MaxCLL/MaxFALL are uints verbatim. SEI primary index 0/1/2 (G/B/R) mapped to the Matroska R/G/B element layout. Emitted only when hdr10 is present. Tests: SEI parse with exact raw values, requires-both-SEI, SDR omission, and emulation-prevention stripping (hevc.rs); muxer emit with exact unit scaling + SDR omission of MasteringMetadata/MaxCLL/MaxFALL (mkv.rs); apply_coding_to_track HDR10 plumbing (mkvstream.rs).
This commit is contained in:
@@ -246,6 +246,13 @@ impl MkvStream {
|
||||
/// picture, so it should never be missing): LOG it loudly so the source can be
|
||||
/// debugged, and leave UNDETERMINED — a muxer never fabricates a source fact.
|
||||
fn apply_coding_to_track(track: &mut MkvTrack, coding: Option<crate::mux::codec::PictureInfo>) {
|
||||
// HDR10 static metadata measured from the bitstream (HEVC SEI). Applied for
|
||||
// ANY track type that carries it (independent of interlace): the first coded
|
||||
// picture's PictureInfo holds it once both HDR10 SEI messages were seen.
|
||||
// `None` (SDR / no-SEI) leaves the track's `hdr10` untouched → omitted.
|
||||
if let Some(h) = coding.and_then(|c| c.hdr10()) {
|
||||
track.hdr10 = Some(h);
|
||||
}
|
||||
if !track.interlaced {
|
||||
return;
|
||||
}
|
||||
@@ -954,6 +961,59 @@ mod tests {
|
||||
assert_eq!(prog.field_order, ebml::FIELD_ORDER_UNDETERMINED);
|
||||
}
|
||||
|
||||
/// `apply_coding_to_track` routes MEASURED HDR10 static metadata from the
|
||||
/// first coded picture onto the track (independent of interlace), and leaves
|
||||
/// it `None` when the picture carried none — never fabricated.
|
||||
#[test]
|
||||
fn apply_coding_to_track_plumbs_measured_hdr10() {
|
||||
use crate::disc::{Codec, ColorSpace, FrameRate, HdrFormat, Resolution, VideoStream};
|
||||
use crate::mux::codec::Hdr10Metadata;
|
||||
use crate::mux::codec::coding::{CodingType, PictureInfo};
|
||||
|
||||
let make = || {
|
||||
MkvTrack::video(&VideoStream {
|
||||
pid: 0xE0,
|
||||
codec: Codec::Hevc,
|
||||
resolution: Resolution::R2160p, // progressive UHD
|
||||
frame_rate: FrameRate::F24,
|
||||
hdr: HdrFormat::Hdr10,
|
||||
color_space: ColorSpace::Bt2020,
|
||||
display_aspect: None,
|
||||
secondary: false,
|
||||
label: String::new(),
|
||||
measured_cicp: None,
|
||||
})
|
||||
};
|
||||
let h = Hdr10Metadata {
|
||||
display_primaries_x: [8500, 6550, 35400],
|
||||
display_primaries_y: [39850, 2300, 14600],
|
||||
white_point_x: 15635,
|
||||
white_point_y: 16450,
|
||||
max_display_mastering_luminance: 10_000_000,
|
||||
min_display_mastering_luminance: 1,
|
||||
max_content_light_level: 1000,
|
||||
max_pic_average_light_level: 400,
|
||||
};
|
||||
|
||||
// Picture carries HDR10 → plumbed onto the track.
|
||||
let mut t = make();
|
||||
assert!(t.hdr10.is_none(), "fresh track has no HDR10");
|
||||
let pic = PictureInfo::coding_type_only(CodingType::I).with_hdr10(Some(h));
|
||||
apply_coding_to_track(&mut t, Some(pic));
|
||||
assert_eq!(t.hdr10, Some(h), "measured HDR10 must reach the track");
|
||||
|
||||
// Picture without HDR10 → track stays None (never fabricated).
|
||||
let mut t = make();
|
||||
let pic = PictureInfo::coding_type_only(CodingType::I);
|
||||
apply_coding_to_track(&mut t, Some(pic));
|
||||
assert!(t.hdr10.is_none(), "no measured HDR10 → track stays None");
|
||||
|
||||
// No coding at all → None.
|
||||
let mut t = make();
|
||||
apply_coding_to_track(&mut t, None);
|
||||
assert!(t.hdr10.is_none());
|
||||
}
|
||||
|
||||
// `From<Error> for io::Error` encodes the numeric code into the
|
||||
// Display string as "E{code}: ...". Check the prefix.
|
||||
/// Extract the error from a `MkvStream::open` result without requiring
|
||||
|
||||
Reference in New Issue
Block a user