Skip to main content

codec/
hevc_sei.rs

1//! HEVC SEI extractor for HDR static metadata.
2//!
3//! libde265 does not expose SEI messages through its public C API
4//! (the `sei_message` type lives in `libde265/libde265/sei.h` as
5//! internal C++; only the processing side that hashes pictures is
6//! visible), so we vendor a minimal pure-Rust NAL/SEI parser here
7//! that reads just the two payload types HDR10 pass-through needs:
8//!
9//!   * **Mastering display colour volume** — `payload_type=137`,
10//!     per HEVC spec D.2.28.
11//!   * **Content light level information** — `payload_type=144`,
12//!     per HEVC spec D.2.35.
13//!
14//! Inputs are raw Annex-B samples (start-code delimited NAL units)
15//! — the same bytes we push into libde265. Output merges into
16//! `ColorMetadata.mastering_display` / `content_light_level`. The
17//! parser does not touch the decode path at all; it just scans
18//! the bitstream once at demux / decoder-construction time and
19//! caches the two structs.
20//!
21//! Referenced normative text:
22//!
23//!   * ITU-T H.265 (2021) §7.3.5 "SEI payload syntax"
24//!   * ITU-T H.265 (2021) D.2.28 mastering_display_colour_volume()
25//!   * ITU-T H.265 (2021) D.2.35 content_light_level_info()
26//!   * ITU-T H.265 (2021) §7.3.2.4 "SEI RBSP syntax" — NAL units 39/40
27//!
28//! Anti-emulation: the SEI RBSP uses emulation-prevention byte stuffing
29//! (any `0x00 0x00 0x00`, `0x00 0x00 0x01`, `0x00 0x00 0x02`, or
30//! `0x00 0x00 0x03` in the decoded payload is written as the original
31//! first two bytes followed by a `0x03` in the coded bitstream). For
32//! the two payload types we parse, all fields are single bytes or
33//! 16/32-bit BE words whose payload lengths are fixed; we strip
34//! emulation-prevention bytes out on a per-NAL basis before parsing.
35
36use crate::frame::{ContentLightLevel, MasteringDisplay};
37
38#[derive(Debug, Clone, Copy, Default)]
39pub struct HevcHdrSei {
40    pub mastering_display: Option<MasteringDisplay>,
41    pub content_light_level: Option<ContentLightLevel>,
42}
43
44impl HevcHdrSei {
45    /// Fold `other` into `self`: later samples overwrite earlier ones
46    /// only when they populate a field the current state lacks OR when
47    /// the payload differs (most streams repeat the SEI on every
48    /// IRAP; folding keeps the newest).
49    pub fn merge(&mut self, other: HevcHdrSei) {
50        if other.mastering_display.is_some() {
51            self.mastering_display = other.mastering_display;
52        }
53        if other.content_light_level.is_some() {
54            self.content_light_level = other.content_light_level;
55        }
56    }
57
58    pub fn is_empty(&self) -> bool {
59        self.mastering_display.is_none() && self.content_light_level.is_none()
60    }
61}
62
63/// Scan an Annex-B byte buffer for HEVC SEI NAL units (nal_unit_type 39
64/// prefix, 40 suffix) and extract HDR static metadata payloads.
65/// Returns a potentially-empty `HevcHdrSei`; callers should fold it
66/// into `ColorMetadata` only when non-empty.
67pub fn parse_annexb(buf: &[u8]) -> HevcHdrSei {
68    let mut out = HevcHdrSei::default();
69    for nal in annexb_split(buf) {
70        if nal.is_empty() {
71            continue;
72        }
73        // HEVC NAL unit header (2 bytes): forbidden_zero_bit(1) |
74        // nal_unit_type(6) | nuh_layer_id(6) | nuh_temporal_id_plus1(3).
75        // We care about types 39 (PREFIX_SEI_NUT) and 40 (SUFFIX_SEI_NUT).
76        if nal.len() < 2 {
77            continue;
78        }
79        let nal_unit_type = (nal[0] >> 1) & 0x3F;
80        if nal_unit_type != 39 && nal_unit_type != 40 {
81            continue;
82        }
83        let rbsp = strip_emulation_prevention(&nal[2..]);
84        parse_sei_rbsp(&rbsp, &mut out);
85    }
86    out
87}
88
89/// Iterator: split an Annex-B byte buffer into NAL payloads (start
90/// codes and trailing zero-byte fillers removed). Start codes are
91/// `0x00 0x00 0x01` (3 bytes) or `0x00 0x00 0x00 0x01` (4 bytes).
92fn annexb_split(buf: &[u8]) -> Vec<&[u8]> {
93    let mut out = Vec::new();
94    let mut i = 0;
95    // Advance to first start code.
96    while i + 2 < buf.len() {
97        if buf[i] == 0 && buf[i + 1] == 0 && buf[i + 2] == 1 {
98            i += 3;
99            break;
100        }
101        if i + 3 < buf.len() && buf[i] == 0 && buf[i + 1] == 0 && buf[i + 2] == 0 && buf[i + 3] == 1
102        {
103            i += 4;
104            break;
105        }
106        i += 1;
107    }
108    let mut nal_start = i;
109    while i + 2 < buf.len() {
110        if buf[i] == 0 && buf[i + 1] == 0 && (buf[i + 2] == 1 || buf[i + 2] == 0) {
111            // Check for a true start code (may be 3 or 4 bytes).
112            let is_3byte = buf[i + 2] == 1;
113            let is_4byte = !is_3byte && i + 3 < buf.len() && buf[i + 3] == 1;
114            if is_3byte || is_4byte {
115                let mut end = i;
116                // Trim trailing zero fill before the start code.
117                while end > nal_start && buf[end - 1] == 0 {
118                    end -= 1;
119                }
120                if end > nal_start {
121                    out.push(&buf[nal_start..end]);
122                }
123                i += if is_3byte { 3 } else { 4 };
124                nal_start = i;
125                continue;
126            }
127        }
128        i += 1;
129    }
130    if nal_start < buf.len() {
131        let mut end = buf.len();
132        while end > nal_start && buf[end - 1] == 0 {
133            end -= 1;
134        }
135        if end > nal_start {
136            out.push(&buf[nal_start..end]);
137        }
138    }
139    out
140}
141
142/// Remove HEVC emulation-prevention bytes (a `0x03` inserted after any
143/// `0x00 0x00` pair whose next original byte was ≤ 0x03). Input is an
144/// EBSP slice; output is the underlying RBSP.
145fn strip_emulation_prevention(ebsp: &[u8]) -> Vec<u8> {
146    let mut rbsp = Vec::with_capacity(ebsp.len());
147    let mut i = 0;
148    while i < ebsp.len() {
149        if i + 2 < ebsp.len() && ebsp[i] == 0 && ebsp[i + 1] == 0 && ebsp[i + 2] == 0x03 {
150            rbsp.push(0);
151            rbsp.push(0);
152            i += 3;
153            continue;
154        }
155        rbsp.push(ebsp[i]);
156        i += 1;
157    }
158    rbsp
159}
160
161/// Parse one SEI RBSP: a concatenation of
162///   `(payload_type, payload_size, payload_bytes)`
163/// triples, ending with an `rbsp_trailing_bits()` byte. Each `_type` /
164/// `_size` is variable-length via 0xFF-run encoding:
165///   payload_type = sum of leading 0xFF bytes + final non-0xFF byte.
166fn parse_sei_rbsp(rbsp: &[u8], out: &mut HevcHdrSei) {
167    let mut cursor = 0;
168    while cursor < rbsp.len() {
169        // payload_type
170        let (payload_type, after_type) = match read_sei_ff_byte_sum(rbsp, cursor) {
171            Some(v) => v,
172            None => return,
173        };
174        cursor = after_type;
175        if cursor >= rbsp.len() {
176            return;
177        }
178        // payload_size
179        let (payload_size, after_size) = match read_sei_ff_byte_sum(rbsp, cursor) {
180            Some(v) => v,
181            None => return,
182        };
183        cursor = after_size;
184        if cursor + payload_size > rbsp.len() {
185            return;
186        }
187        let payload = &rbsp[cursor..cursor + payload_size];
188        cursor += payload_size;
189
190        match payload_type {
191            137 => {
192                if let Some(mdcv) = parse_mastering_display(payload) {
193                    out.mastering_display = Some(mdcv);
194                }
195            }
196            144 => {
197                if let Some(clli) = parse_content_light_level(payload) {
198                    out.content_light_level = Some(clli);
199                }
200            }
201            _ => { /* ignore other payload types */ }
202        }
203
204        // Check for rbsp_trailing_bits: a single `1` bit followed by
205        // zeros. Any remaining byte at `cursor` that's 0x80 or similar
206        // marks the end; SEI RBSPs rarely concatenate multiple messages
207        // without a trailing bit, but the spec allows it.
208        if cursor < rbsp.len() && rbsp[cursor] == 0x80 {
209            break;
210        }
211    }
212}
213
214/// Sum leading 0xFF bytes with the first non-0xFF byte, yielding the
215/// SEI payload_type or payload_size field. Returns `(value, next_idx)`.
216fn read_sei_ff_byte_sum(buf: &[u8], mut idx: usize) -> Option<(usize, usize)> {
217    let mut acc = 0usize;
218    while idx < buf.len() && buf[idx] == 0xFF {
219        acc += 0xFF;
220        idx += 1;
221    }
222    if idx >= buf.len() {
223        return None;
224    }
225    acc += buf[idx] as usize;
226    Some((acc, idx + 1))
227}
228
229/// Parse mastering_display_colour_volume() per HEVC D.2.28.
230///
231/// Payload (big-endian):
232///   u16 display_primaries_x[0], u16 display_primaries_y[0]   // G
233///   u16 display_primaries_x[1], u16 display_primaries_y[1]   // B
234///   u16 display_primaries_x[2], u16 display_primaries_y[2]   // R
235///   u16 white_point_x,          u16 white_point_y
236///   u32 max_display_mastering_luminance
237///   u32 min_display_mastering_luminance
238///
239/// = 24 bytes. Note the spec-defined wire order is GBR (not RGB); we
240/// remap to the struct's R/G/B field order.
241fn parse_mastering_display(p: &[u8]) -> Option<MasteringDisplay> {
242    if p.len() < 24 {
243        return None;
244    }
245    let u16be = |o: usize| u16::from_be_bytes([p[o], p[o + 1]]);
246    let u32be = |o: usize| u32::from_be_bytes([p[o], p[o + 1], p[o + 2], p[o + 3]]);
247    Some(MasteringDisplay {
248        // Wire order GBR → struct field RGB remap.
249        primaries_g_x: u16be(0),
250        primaries_g_y: u16be(2),
251        primaries_b_x: u16be(4),
252        primaries_b_y: u16be(6),
253        primaries_r_x: u16be(8),
254        primaries_r_y: u16be(10),
255        white_point_x: u16be(12),
256        white_point_y: u16be(14),
257        max_luminance: u32be(16),
258        min_luminance: u32be(20),
259    })
260}
261
262/// Parse content_light_level_info() per HEVC D.2.35.
263///
264/// Payload (big-endian):
265///   u16 max_content_light_level
266///   u16 max_pic_average_light_level
267///
268/// = 4 bytes.
269fn parse_content_light_level(p: &[u8]) -> Option<ContentLightLevel> {
270    if p.len() < 4 {
271        return None;
272    }
273    Some(ContentLightLevel {
274        max_cll: u16::from_be_bytes([p[0], p[1]]),
275        max_fall: u16::from_be_bytes([p[2], p[3]]),
276    })
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    fn emit_sei_payload(payload_type: u8, payload: &[u8]) -> Vec<u8> {
284        let mut out = Vec::new();
285        out.push(payload_type);
286        out.push(payload.len() as u8);
287        out.extend_from_slice(payload);
288        // rbsp_trailing_bits: single '1' bit then zero-fill to byte.
289        out.push(0x80);
290        out
291    }
292
293    fn wrap_as_prefix_sei_nal(rbsp: &[u8]) -> Vec<u8> {
294        // NAL header type=39 (PREFIX_SEI_NUT), layer_id=0, tid+1=1.
295        // byte[0] = (0<<7) | (39<<1) | ((0 >> 5) & 1)   = 0x4E
296        // byte[1] = ((0 & 0x1F) << 3) | 1               = 0x01
297        let mut v = Vec::with_capacity(2 + rbsp.len());
298        v.push(0x4E);
299        v.push(0x01);
300        v.extend_from_slice(rbsp);
301        v
302    }
303
304    fn mastering_display_sei_bytes() -> Vec<u8> {
305        // BT.2020 primaries (HDR10 canonical):
306        //   G = (0.265, 0.690) → (13250, 34500)
307        //   B = (0.150, 0.060) → ( 7500,  3000)
308        //   R = (0.680, 0.320) → (34000, 16000)
309        //   W = (0.3127, 0.3290) → (15635, 16450)
310        // max_luminance = 1000 cd/m² in 0.0001 units → 10_000_000
311        // min_luminance = 0.005 cd/m² in 0.0001 units →         50
312        let mut p = Vec::new();
313        p.extend_from_slice(&13250u16.to_be_bytes());
314        p.extend_from_slice(&34500u16.to_be_bytes());
315        p.extend_from_slice(&7500u16.to_be_bytes());
316        p.extend_from_slice(&3000u16.to_be_bytes());
317        p.extend_from_slice(&34000u16.to_be_bytes());
318        p.extend_from_slice(&16000u16.to_be_bytes());
319        p.extend_from_slice(&15635u16.to_be_bytes());
320        p.extend_from_slice(&16450u16.to_be_bytes());
321        p.extend_from_slice(&10_000_000u32.to_be_bytes());
322        p.extend_from_slice(&50u32.to_be_bytes());
323        assert_eq!(p.len(), 24);
324        p
325    }
326
327    fn content_light_level_sei_bytes() -> Vec<u8> {
328        // MaxCLL = 1000, MaxFALL = 400.
329        let mut p = Vec::new();
330        p.extend_from_slice(&1000u16.to_be_bytes());
331        p.extend_from_slice(&400u16.to_be_bytes());
332        p
333    }
334
335    fn build_annexb(nals: &[&[u8]]) -> Vec<u8> {
336        let mut out = Vec::new();
337        for nal in nals {
338            out.extend_from_slice(&[0, 0, 0, 1]);
339            out.extend_from_slice(nal);
340        }
341        out
342    }
343
344    #[test]
345    fn parses_mastering_display_sei_from_prefix_nal() {
346        let rbsp = emit_sei_payload(137, &mastering_display_sei_bytes());
347        let nal = wrap_as_prefix_sei_nal(&rbsp);
348        let stream = build_annexb(&[&nal]);
349        let sei = parse_annexb(&stream);
350        let md = sei.mastering_display.expect("mastering display populated");
351        assert_eq!(md.primaries_r_x, 34000);
352        assert_eq!(md.primaries_r_y, 16000);
353        assert_eq!(md.primaries_g_x, 13250);
354        assert_eq!(md.primaries_g_y, 34500);
355        assert_eq!(md.primaries_b_x, 7500);
356        assert_eq!(md.primaries_b_y, 3000);
357        assert_eq!(md.white_point_x, 15635);
358        assert_eq!(md.white_point_y, 16450);
359        assert_eq!(md.max_luminance, 10_000_000);
360        assert_eq!(md.min_luminance, 50);
361        assert!(sei.content_light_level.is_none());
362    }
363
364    #[test]
365    fn parses_content_light_level_sei_from_prefix_nal() {
366        let rbsp = emit_sei_payload(144, &content_light_level_sei_bytes());
367        let nal = wrap_as_prefix_sei_nal(&rbsp);
368        let stream = build_annexb(&[&nal]);
369        let sei = parse_annexb(&stream);
370        let cll = sei.content_light_level.expect("clli populated");
371        assert_eq!(cll.max_cll, 1000);
372        assert_eq!(cll.max_fall, 400);
373        assert!(sei.mastering_display.is_none());
374    }
375
376    #[test]
377    fn parses_both_sei_messages_in_same_nal() {
378        let mut rbsp = emit_sei_payload(137, &mastering_display_sei_bytes());
379        // Drop the earlier rbsp_trailing_bits — the second payload follows
380        // directly — then emit the second message with its own trailing.
381        rbsp.pop();
382        rbsp.extend(emit_sei_payload(144, &content_light_level_sei_bytes()));
383        let nal = wrap_as_prefix_sei_nal(&rbsp);
384        let stream = build_annexb(&[&nal]);
385        let sei = parse_annexb(&stream);
386        assert!(sei.mastering_display.is_some());
387        assert!(sei.content_light_level.is_some());
388    }
389
390    #[test]
391    fn handles_emulation_prevention_bytes() {
392        // Hand-craft a payload that contains an embedded 0x00 0x00 0x01
393        // sequence — inject a 0x03 emulation-prevention byte before the
394        // trailing 0x01 so the encoder output parses cleanly. The parser
395        // must strip the 0x03 before reading the u32.
396        //
397        // Place the sensitive sequence inside the CLLI payload:
398        //   MaxCLL = 0x0000 → 0x00 0x00
399        //   MaxFALL = 0x0001 → 0x00 0x01
400        // Full payload bytes: 0x00 0x00 0x00 0x01 → must be encoded as
401        // 0x00 0x00 0x03 0x00 0x01 in the EBSP.
402        let payload = vec![0x00, 0x00, 0x00, 0x01];
403        let mut rbsp_without_prevention = Vec::new();
404        rbsp_without_prevention.push(144); // payload_type
405        rbsp_without_prevention.push(payload.len() as u8); // payload_size
406        rbsp_without_prevention.extend_from_slice(&payload);
407        rbsp_without_prevention.push(0x80); // rbsp_trailing_bits
408
409        // Now produce the EBSP by inserting 0x03 after any two zero bytes
410        // whose next byte is ≤ 0x03.
411        let mut ebsp = Vec::new();
412        let mut zero_run = 0;
413        for &b in &rbsp_without_prevention {
414            if zero_run >= 2 && b <= 0x03 {
415                ebsp.push(0x03);
416                zero_run = 0;
417            }
418            ebsp.push(b);
419            if b == 0 {
420                zero_run += 1;
421            } else {
422                zero_run = 0;
423            }
424        }
425
426        // Wrap with NAL header and Annex-B start code.
427        let mut nal = vec![0x4E, 0x01];
428        nal.extend_from_slice(&ebsp);
429        let stream = build_annexb(&[&nal]);
430        let sei = parse_annexb(&stream);
431        let cll = sei.content_light_level.expect("clli after emulation strip");
432        assert_eq!(cll.max_cll, 0);
433        assert_eq!(cll.max_fall, 1);
434    }
435
436    #[test]
437    fn returns_empty_when_no_sei_nal_present() {
438        // A random VCL NAL (type 1, non-IDR slice). Parser must skip.
439        let mut nal = vec![0x02, 0x01]; // (1 << 1) = 0x02
440        nal.extend_from_slice(&[0xFF, 0xFF, 0xFF]);
441        let stream = build_annexb(&[&nal]);
442        let sei = parse_annexb(&stream);
443        assert!(sei.mastering_display.is_none());
444        assert!(sei.content_light_level.is_none());
445        assert!(sei.is_empty());
446    }
447
448    #[test]
449    fn handles_start_code_4byte_variant() {
450        let rbsp = emit_sei_payload(144, &content_light_level_sei_bytes());
451        let nal = wrap_as_prefix_sei_nal(&rbsp);
452        // 4-byte start code form: 0x00 0x00 0x00 0x01.
453        let mut stream = vec![0, 0, 0, 1];
454        stream.extend_from_slice(&nal);
455        let sei = parse_annexb(&stream);
456        assert!(sei.content_light_level.is_some());
457    }
458
459    #[test]
460    fn suffix_sei_nal_type_40_also_parsed() {
461        let rbsp = emit_sei_payload(144, &content_light_level_sei_bytes());
462        // NAL type 40 (SUFFIX_SEI_NUT):
463        //   byte[0] = (40 << 1) | 0 = 0x50
464        //   byte[1] = 0x01
465        let mut nal = vec![0x50, 0x01];
466        nal.extend_from_slice(&rbsp);
467        let stream = build_annexb(&[&nal]);
468        let sei = parse_annexb(&stream);
469        assert!(sei.content_light_level.is_some());
470    }
471
472    #[test]
473    fn ff_byte_sum_handles_large_payload_type() {
474        // payload_type = 255 + 7 = 262 (fictional type; parser must skip).
475        // Verify the 0xFF run-length decode advances the cursor correctly.
476        let mut rbsp = vec![0xFF, 7, /* size */ 0, /* trailing */ 0x80];
477        // Append a valid clli after.
478        rbsp.pop();
479        rbsp.extend(emit_sei_payload(144, &content_light_level_sei_bytes()));
480        let nal = wrap_as_prefix_sei_nal(&rbsp);
481        let stream = build_annexb(&[&nal]);
482        let sei = parse_annexb(&stream);
483        assert!(sei.content_light_level.is_some());
484    }
485}