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:
Matthew Jackson
2026-06-25 21:59:43 -07:00
parent 539b170f7e
commit dc1d05985b
6 changed files with 823 additions and 6 deletions
+60
View File
@@ -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