Skip to main content

oximedia_codec/
sei_nal.rs

1//! SEI / NAL-unit helpers for VP8/AV1 metadata attachment.
2//!
3//! VP8 does not have a formal SEI syntax, but its partition 0 allows embedding
4//! application-level metadata as extension bytes.  AV1 has OBU metadata blocks
5//! that serve a similar purpose (OBU type 5 — METADATA_OBU).
6//!
7//! This module provides:
8//! - [`SeiPayloadType`] — typed catalogue of SEI payload kinds.
9//! - [`UserDataUnregistered`] — UUID-tagged opaque metadata (SEI type 5).
10//! - [`PictureTiming`] — clock tick / frame-rate SEI (type 1).
11//! - [`SeiMessage`] — a complete SEI message (type + payload).
12//! - [`SeiEncoder`] — serialises SEI messages into a byte buffer suitable for
13//!   embedding into a VP8 partition or AV1 METADATA_OBU payload.
14//! - [`SeiDecoder`] — parses that same byte buffer back into [`SeiMessage`]s.
15//! - [`Av1MetadataObu`] — wraps a serialised SEI payload into a minimal
16//!   AV1 METADATA OBU.
17//! - [`Vp8MetadataBlock`] — wraps serialised metadata into a VP8-style
18//!   user-data extension block.
19
20use crate::error::{CodecError, CodecResult};
21
22// ── Constants ─────────────────────────────────────────────────────────────────
23
24/// OBU type value for AV1 METADATA_OBU.
25pub const AV1_OBU_TYPE_METADATA: u8 = 5;
26
27/// Marker byte that introduces a VP8 user-data extension block.
28pub const VP8_USER_DATA_MARKER: u8 = 0xFE;
29
30/// Length of a UUID as defined by RFC 4122 (16 bytes).
31pub const UUID_LEN: usize = 16;
32
33// ── SeiPayloadType ────────────────────────────────────────────────────────────
34
35/// SEI payload type codes (ITU-T H.274 / ISO/IEC 23002-7 analogues for
36/// royalty-free codecs).
37///
38/// The numeric values are deliberately kept aligned with the H.264/H.265 SEI
39/// tables for tooling interoperability, but no H.264/H.265 patents apply here:
40/// these structures are used solely inside VP8 extension blocks and AV1
41/// METADATA_OBUs.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43#[repr(u8)]
44pub enum SeiPayloadType {
45    /// Buffering period (type 0).
46    BufferingPeriod = 0,
47    /// Picture timing (type 1).
48    PictureTiming = 1,
49    /// Pan-scan rectangle (type 2).
50    PanScanRect = 2,
51    /// User data registered by ITU-T Rec. T.35 (type 4).
52    UserDataRegistered = 4,
53    /// User data unregistered — UUID + opaque bytes (type 5).
54    UserDataUnregistered = 5,
55    /// Recovery point (type 6).
56    RecoveryPoint = 6,
57    /// Frame packing arrangement (type 45).
58    FramePacking = 45,
59    /// Display orientation (type 47).
60    DisplayOrientation = 47,
61    /// Unknown / custom.
62    Unknown = 255,
63}
64
65impl SeiPayloadType {
66    /// Parse from a raw byte value.  Unrecognised values map to [`Self::Unknown`].
67    pub fn from_byte(b: u8) -> Self {
68        match b {
69            0 => Self::BufferingPeriod,
70            1 => Self::PictureTiming,
71            2 => Self::PanScanRect,
72            4 => Self::UserDataRegistered,
73            5 => Self::UserDataUnregistered,
74            6 => Self::RecoveryPoint,
75            45 => Self::FramePacking,
76            47 => Self::DisplayOrientation,
77            _ => Self::Unknown,
78        }
79    }
80}
81
82// ── UserDataUnregistered ──────────────────────────────────────────────────────
83
84/// SEI type 5: user-data unregistered.
85///
86/// Carries a 16-byte UUID (RFC 4122) followed by arbitrary application data.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct UserDataUnregistered {
89    /// 128-bit UUID identifying the data format.
90    pub uuid: [u8; UUID_LEN],
91    /// Arbitrary application payload.
92    pub data: Vec<u8>,
93}
94
95impl UserDataUnregistered {
96    /// Create with an explicit UUID and data.
97    pub fn new(uuid: [u8; UUID_LEN], data: Vec<u8>) -> Self {
98        Self { uuid, data }
99    }
100
101    /// Create with a nil UUID (all zeros) — useful for tests.
102    pub fn with_nil_uuid(data: Vec<u8>) -> Self {
103        Self::new([0u8; UUID_LEN], data)
104    }
105
106    /// Serialise into raw bytes: 16-byte UUID followed by payload.
107    pub fn to_bytes(&self) -> Vec<u8> {
108        let mut out = Vec::with_capacity(UUID_LEN + self.data.len());
109        out.extend_from_slice(&self.uuid);
110        out.extend_from_slice(&self.data);
111        out
112    }
113
114    /// Parse from raw bytes.
115    pub fn from_bytes(raw: &[u8]) -> CodecResult<Self> {
116        if raw.len() < UUID_LEN {
117            return Err(CodecError::InvalidBitstream(format!(
118                "UserDataUnregistered: need {UUID_LEN} bytes for UUID, got {}",
119                raw.len()
120            )));
121        }
122        let mut uuid = [0u8; UUID_LEN];
123        uuid.copy_from_slice(&raw[..UUID_LEN]);
124        Ok(Self {
125            uuid,
126            data: raw[UUID_LEN..].to_vec(),
127        })
128    }
129}
130
131// ── PictureTiming ─────────────────────────────────────────────────────────────
132
133/// SEI type 1: picture timing.
134///
135/// Carries HRD clock tick information and picture-structure metadata.
136/// Fields correspond to the simplified timing syntax used in royalty-free
137/// codec streams (VP8 extension / AV1 metadata).
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct PictureTiming {
140    /// Clock timestamp flag — `true` if clock fields are valid.
141    pub clock_timestamp_flag: bool,
142    /// Clock units elapsed since the start of the sequence.
143    pub clock_timestamp: u64,
144    /// Presentation delay in ticks.
145    pub presentation_delay: u32,
146    /// Picture structure hint.
147    pub pic_struct: PicStructure,
148}
149
150/// Picture structure for timing SEI.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152#[repr(u8)]
153pub enum PicStructure {
154    /// Progressive frame.
155    Frame = 0,
156    /// Top field only.
157    TopField = 1,
158    /// Bottom field only.
159    BottomField = 2,
160    /// Top field then bottom field (interleaved).
161    TopBottomField = 3,
162    /// Bottom field then top field (interleaved).
163    BottomTopField = 4,
164}
165
166impl PicStructure {
167    fn from_byte(b: u8) -> Self {
168        match b {
169            1 => Self::TopField,
170            2 => Self::BottomField,
171            3 => Self::TopBottomField,
172            4 => Self::BottomTopField,
173            _ => Self::Frame,
174        }
175    }
176}
177
178impl PictureTiming {
179    /// Create a simple frame-level timing entry with a clock timestamp.
180    pub fn frame(clock_timestamp: u64, presentation_delay: u32) -> Self {
181        Self {
182            clock_timestamp_flag: true,
183            clock_timestamp,
184            presentation_delay,
185            pic_struct: PicStructure::Frame,
186        }
187    }
188
189    /// Serialise to bytes (16 bytes: flags 1 + clock 8 + delay 4 + pic_struct 1 + pad 2).
190    pub fn to_bytes(&self) -> Vec<u8> {
191        let mut out = Vec::with_capacity(16);
192        out.push(u8::from(self.clock_timestamp_flag));
193        out.extend_from_slice(&self.clock_timestamp.to_be_bytes());
194        out.extend_from_slice(&self.presentation_delay.to_be_bytes());
195        out.push(self.pic_struct as u8);
196        out.extend_from_slice(&[0u8; 2]); // reserved / padding
197        out
198    }
199
200    /// Parse from bytes.
201    pub fn from_bytes(raw: &[u8]) -> CodecResult<Self> {
202        if raw.len() < 14 {
203            return Err(CodecError::InvalidBitstream(format!(
204                "PictureTiming: need 14 bytes, got {}",
205                raw.len()
206            )));
207        }
208        let clock_timestamp_flag = raw[0] != 0;
209        let clock_timestamp =
210            u64::from_be_bytes(raw[1..9].try_into().map_err(|_| {
211                CodecError::InvalidBitstream("PictureTiming: bad clock bytes".into())
212            })?);
213        let presentation_delay =
214            u32::from_be_bytes(raw[9..13].try_into().map_err(|_| {
215                CodecError::InvalidBitstream("PictureTiming: bad delay bytes".into())
216            })?);
217        let pic_struct = PicStructure::from_byte(raw[13]);
218        Ok(Self {
219            clock_timestamp_flag,
220            clock_timestamp,
221            presentation_delay,
222            pic_struct,
223        })
224    }
225}
226
227// ── SeiMessage ────────────────────────────────────────────────────────────────
228
229/// A single SEI message: a payload type tag plus the encoded bytes.
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct SeiMessage {
232    /// Payload type discriminant.
233    pub payload_type: SeiPayloadType,
234    /// Raw serialised payload bytes.
235    pub payload: Vec<u8>,
236}
237
238impl SeiMessage {
239    /// Create a raw SEI message with the given type and payload.
240    pub fn new(payload_type: SeiPayloadType, payload: Vec<u8>) -> Self {
241        Self {
242            payload_type,
243            payload,
244        }
245    }
246
247    /// Build a `UserDataUnregistered` SEI message.
248    pub fn user_data_unregistered(udu: &UserDataUnregistered) -> Self {
249        Self::new(SeiPayloadType::UserDataUnregistered, udu.to_bytes())
250    }
251
252    /// Build a `PictureTiming` SEI message.
253    pub fn picture_timing(pt: &PictureTiming) -> Self {
254        Self::new(SeiPayloadType::PictureTiming, pt.to_bytes())
255    }
256
257    /// Parse the payload as [`UserDataUnregistered`].
258    pub fn as_user_data_unregistered(&self) -> CodecResult<UserDataUnregistered> {
259        if self.payload_type != SeiPayloadType::UserDataUnregistered {
260            return Err(CodecError::InvalidData(
261                "SEI: expected UserDataUnregistered payload type".into(),
262            ));
263        }
264        UserDataUnregistered::from_bytes(&self.payload)
265    }
266
267    /// Parse the payload as [`PictureTiming`].
268    pub fn as_picture_timing(&self) -> CodecResult<PictureTiming> {
269        if self.payload_type != SeiPayloadType::PictureTiming {
270            return Err(CodecError::InvalidData(
271                "SEI: expected PictureTiming payload type".into(),
272            ));
273        }
274        PictureTiming::from_bytes(&self.payload)
275    }
276}
277
278// ── SeiEncoder / SeiDecoder ───────────────────────────────────────────────────
279
280/// Wire format for a sequence of SEI messages.
281///
282/// Each message is framed as:
283/// ```text
284/// [type: u8][length: u32 big-endian][payload: <length> bytes]
285/// ```
286#[derive(Debug, Default)]
287pub struct SeiEncoder {
288    buf: Vec<u8>,
289}
290
291impl SeiEncoder {
292    /// Create a new encoder.
293    pub fn new() -> Self {
294        Self::default()
295    }
296
297    /// Append a single [`SeiMessage`].
298    pub fn write_message(&mut self, msg: &SeiMessage) {
299        self.buf.push(msg.payload_type as u8);
300        let len = msg.payload.len() as u32;
301        self.buf.extend_from_slice(&len.to_be_bytes());
302        self.buf.extend_from_slice(&msg.payload);
303    }
304
305    /// Append multiple messages.
306    pub fn write_messages(&mut self, msgs: &[SeiMessage]) {
307        for msg in msgs {
308            self.write_message(msg);
309        }
310    }
311
312    /// Consume the encoder and return the serialised bytes.
313    pub fn finish(self) -> Vec<u8> {
314        self.buf
315    }
316
317    /// Return the current byte length of the accumulated buffer.
318    pub fn len(&self) -> usize {
319        self.buf.len()
320    }
321
322    /// Returns `true` if nothing has been written yet.
323    pub fn is_empty(&self) -> bool {
324        self.buf.is_empty()
325    }
326}
327
328/// Parses a byte buffer produced by [`SeiEncoder`] into a list of [`SeiMessage`]s.
329#[derive(Debug)]
330pub struct SeiDecoder<'a> {
331    data: &'a [u8],
332    pos: usize,
333}
334
335impl<'a> SeiDecoder<'a> {
336    /// Create a decoder over `data`.
337    pub fn new(data: &'a [u8]) -> Self {
338        Self { data, pos: 0 }
339    }
340
341    /// Read the next message, or `None` if at end.
342    pub fn next_message(&mut self) -> CodecResult<Option<SeiMessage>> {
343        if self.pos >= self.data.len() {
344            return Ok(None);
345        }
346        // Read type byte.
347        let type_byte = self.data[self.pos];
348        self.pos += 1;
349
350        // Read 4-byte big-endian length.
351        if self.pos + 4 > self.data.len() {
352            return Err(CodecError::InvalidBitstream(
353                "SEI: truncated length field".into(),
354            ));
355        }
356        let length = u32::from_be_bytes(
357            self.data[self.pos..self.pos + 4]
358                .try_into()
359                .map_err(|_| CodecError::InvalidBitstream("SEI: bad length bytes".into()))?,
360        ) as usize;
361        self.pos += 4;
362
363        // Read payload.
364        if self.pos + length > self.data.len() {
365            return Err(CodecError::InvalidBitstream(format!(
366                "SEI: payload truncated (need {length}, have {})",
367                self.data.len() - self.pos
368            )));
369        }
370        let payload = self.data[self.pos..self.pos + length].to_vec();
371        self.pos += length;
372
373        Ok(Some(SeiMessage {
374            payload_type: SeiPayloadType::from_byte(type_byte),
375            payload,
376        }))
377    }
378
379    /// Collect all messages.
380    pub fn collect_all(&mut self) -> CodecResult<Vec<SeiMessage>> {
381        let mut result = Vec::new();
382        while let Some(msg) = self.next_message()? {
383            result.push(msg);
384        }
385        Ok(result)
386    }
387}
388
389// ── Av1MetadataObu ────────────────────────────────────────────────────────────
390
391/// A minimal AV1 METADATA_OBU wrapper.
392///
393/// AV1 metadata OBUs (type 5) carry a `metadata_type` varint followed by
394/// the payload.  This struct uses a simple fixed-length encoding for the
395/// metadata type (1 byte) to keep the implementation patent-free and simple.
396#[derive(Debug, Clone, PartialEq, Eq)]
397pub struct Av1MetadataObu {
398    /// AV1 metadata type.  Commonly 4 (ITUT T35) or 5 (custom).
399    pub metadata_type: u8,
400    /// OBU extension byte present flag.
401    pub extension_flag: bool,
402    /// Serialised SEI payload.
403    pub payload: Vec<u8>,
404}
405
406impl Av1MetadataObu {
407    /// Wrap a pre-serialised SEI payload into a METADATA_OBU.
408    pub fn new(metadata_type: u8, payload: Vec<u8>) -> Self {
409        Self {
410            metadata_type,
411            extension_flag: false,
412            payload,
413        }
414    }
415
416    /// Serialise to a byte slice ready for muxing.
417    ///
418    /// Wire format:
419    /// ```text
420    /// [obu_header: 1 byte][extension? 1 byte if extension_flag][metadata_type: 1 byte][payload...]
421    /// ```
422    pub fn to_bytes(&self) -> Vec<u8> {
423        // OBU header: type=5 (0b0101 << 3), extension_flag bit, has_size_field=0
424        let extension_bit = u8::from(self.extension_flag) << 2;
425        let obu_header = (AV1_OBU_TYPE_METADATA << 3) | extension_bit;
426
427        let mut out = Vec::with_capacity(2 + self.payload.len());
428        out.push(obu_header);
429        if self.extension_flag {
430            out.push(0x00); // placeholder extension byte
431        }
432        out.push(self.metadata_type);
433        out.extend_from_slice(&self.payload);
434        out
435    }
436
437    /// Parse an AV1 METADATA_OBU from raw bytes.
438    pub fn from_bytes(raw: &[u8]) -> CodecResult<Self> {
439        if raw.is_empty() {
440            return Err(CodecError::InvalidBitstream(
441                "Av1MetadataObu: empty input".into(),
442            ));
443        }
444        let obu_header = raw[0];
445        let obu_type = (obu_header >> 3) & 0x0F;
446        if obu_type != AV1_OBU_TYPE_METADATA {
447            return Err(CodecError::InvalidBitstream(format!(
448                "Av1MetadataObu: expected type {AV1_OBU_TYPE_METADATA}, got {obu_type}"
449            )));
450        }
451        let extension_flag = (obu_header >> 2) & 1 != 0;
452        let mut pos = 1usize;
453        if extension_flag {
454            pos += 1; // skip extension byte
455        }
456        if pos >= raw.len() {
457            return Err(CodecError::InvalidBitstream(
458                "Av1MetadataObu: missing metadata_type".into(),
459            ));
460        }
461        let metadata_type = raw[pos];
462        pos += 1;
463        let payload = raw[pos..].to_vec();
464        Ok(Self {
465            metadata_type,
466            extension_flag,
467            payload,
468        })
469    }
470}
471
472// ── Vp8MetadataBlock ─────────────────────────────────────────────────────────
473
474/// A VP8 user-data extension block.
475///
476/// VP8 does not define a formal SEI syntax, so this structure embeds metadata
477/// using an application-level convention: a marker byte (`0xFE`) followed by a
478/// 4-byte big-endian length, followed by the payload.  This block is placed
479/// after the last partition data and before the end of the data partition.
480///
481/// **Important**: this is not part of the VP8 bitstream specification.  It is
482/// OxiMedia's application-level convention for attaching metadata to VP8
483/// packets when the container does not provide a side-data channel.
484#[derive(Debug, Clone, PartialEq, Eq)]
485pub struct Vp8MetadataBlock {
486    /// Serialised SEI payload (output of [`SeiEncoder::finish`]).
487    pub payload: Vec<u8>,
488}
489
490impl Vp8MetadataBlock {
491    /// Create a VP8 metadata block from a pre-serialised SEI payload.
492    pub fn new(payload: Vec<u8>) -> Self {
493        Self { payload }
494    }
495
496    /// Serialise to bytes.
497    ///
498    /// Wire format: `[0xFE][length: u32 BE][payload...]`
499    pub fn to_bytes(&self) -> Vec<u8> {
500        let mut out = Vec::with_capacity(5 + self.payload.len());
501        out.push(VP8_USER_DATA_MARKER);
502        let len = self.payload.len() as u32;
503        out.extend_from_slice(&len.to_be_bytes());
504        out.extend_from_slice(&self.payload);
505        out
506    }
507
508    /// Parse a VP8 metadata block from raw bytes.
509    pub fn from_bytes(raw: &[u8]) -> CodecResult<Self> {
510        if raw.is_empty() || raw[0] != VP8_USER_DATA_MARKER {
511            return Err(CodecError::InvalidBitstream(
512                "Vp8MetadataBlock: missing marker byte".into(),
513            ));
514        }
515        if raw.len() < 5 {
516            return Err(CodecError::InvalidBitstream(
517                "Vp8MetadataBlock: truncated header".into(),
518            ));
519        }
520        let length = u32::from_be_bytes(
521            raw[1..5]
522                .try_into()
523                .map_err(|_| CodecError::InvalidBitstream("Vp8MetadataBlock: bad length".into()))?,
524        ) as usize;
525        if raw.len() < 5 + length {
526            return Err(CodecError::InvalidBitstream(format!(
527                "Vp8MetadataBlock: payload truncated (need {length}, have {})",
528                raw.len() - 5
529            )));
530        }
531        Ok(Self {
532            payload: raw[5..5 + length].to_vec(),
533        })
534    }
535}
536
537#[cfg(test)]
538mod tests {
539    use super::*;
540
541    #[test]
542    fn test_user_data_unregistered_roundtrip() {
543        let uuid = [0x01u8; UUID_LEN];
544        let data = b"hello sei world".to_vec();
545        let udu = UserDataUnregistered::new(uuid, data.clone());
546        let raw = udu.to_bytes();
547        let parsed = UserDataUnregistered::from_bytes(&raw).unwrap();
548        assert_eq!(parsed.uuid, uuid);
549        assert_eq!(parsed.data, data);
550    }
551
552    #[test]
553    fn test_user_data_unregistered_too_short() {
554        let result = UserDataUnregistered::from_bytes(&[0u8; 10]);
555        assert!(result.is_err());
556    }
557
558    #[test]
559    fn test_picture_timing_roundtrip() {
560        let pt = PictureTiming::frame(123_456_789, 3000);
561        let raw = pt.to_bytes();
562        let parsed = PictureTiming::from_bytes(&raw).unwrap();
563        assert_eq!(parsed.clock_timestamp, 123_456_789);
564        assert_eq!(parsed.presentation_delay, 3000);
565        assert_eq!(parsed.pic_struct, PicStructure::Frame);
566        assert!(parsed.clock_timestamp_flag);
567    }
568
569    #[test]
570    fn test_picture_timing_too_short() {
571        let result = PictureTiming::from_bytes(&[0u8; 5]);
572        assert!(result.is_err());
573    }
574
575    #[test]
576    fn test_sei_encoder_decoder_roundtrip() {
577        let udu = UserDataUnregistered::with_nil_uuid(b"test payload".to_vec());
578        let pt = PictureTiming::frame(999, 500);
579
580        let mut enc = SeiEncoder::new();
581        enc.write_message(&SeiMessage::user_data_unregistered(&udu));
582        enc.write_message(&SeiMessage::picture_timing(&pt));
583        let bytes = enc.finish();
584
585        let mut dec = SeiDecoder::new(&bytes);
586        let messages = dec.collect_all().unwrap();
587        assert_eq!(messages.len(), 2);
588
589        assert_eq!(
590            messages[0].payload_type,
591            SeiPayloadType::UserDataUnregistered
592        );
593        let recovered_udu = messages[0].as_user_data_unregistered().unwrap();
594        assert_eq!(recovered_udu.data, b"test payload");
595
596        assert_eq!(messages[1].payload_type, SeiPayloadType::PictureTiming);
597        let recovered_pt = messages[1].as_picture_timing().unwrap();
598        assert_eq!(recovered_pt.clock_timestamp, 999);
599    }
600
601    #[test]
602    fn test_sei_decoder_truncated_length() {
603        // Only type byte, no length field.
604        let bad = &[SeiPayloadType::PictureTiming as u8];
605        let mut dec = SeiDecoder::new(bad);
606        assert!(dec.next_message().is_err());
607    }
608
609    #[test]
610    fn test_sei_decoder_truncated_payload() {
611        let mut enc = SeiEncoder::new();
612        enc.write_message(&SeiMessage::new(
613            SeiPayloadType::UserDataUnregistered,
614            vec![0u8; 20],
615        ));
616        let mut bytes = enc.finish();
617        // Truncate the payload
618        bytes.truncate(bytes.len() - 5);
619        let mut dec = SeiDecoder::new(&bytes);
620        assert!(dec.next_message().is_err());
621    }
622
623    #[test]
624    fn test_av1_metadata_obu_roundtrip() {
625        let sei_payload = b"av1 sei data".to_vec();
626        let obu = Av1MetadataObu::new(5, sei_payload.clone());
627        let bytes = obu.to_bytes();
628        let parsed = Av1MetadataObu::from_bytes(&bytes).unwrap();
629        assert_eq!(parsed.metadata_type, 5);
630        assert_eq!(parsed.payload, sei_payload);
631        assert!(!parsed.extension_flag);
632    }
633
634    #[test]
635    fn test_av1_metadata_obu_wrong_type() {
636        // Build a header with OBU type = 1 (sequence header) instead of 5
637        let bad = &[0x08u8, 0x00, 0x00]; // type = 1 << 3 = 0x08
638        let result = Av1MetadataObu::from_bytes(bad);
639        assert!(result.is_err());
640    }
641
642    #[test]
643    fn test_vp8_metadata_block_roundtrip() {
644        let payload = b"vp8 metadata payload".to_vec();
645        let block = Vp8MetadataBlock::new(payload.clone());
646        let bytes = block.to_bytes();
647        assert_eq!(bytes[0], VP8_USER_DATA_MARKER);
648        let parsed = Vp8MetadataBlock::from_bytes(&bytes).unwrap();
649        assert_eq!(parsed.payload, payload);
650    }
651
652    #[test]
653    fn test_vp8_metadata_block_bad_marker() {
654        let result = Vp8MetadataBlock::from_bytes(&[0x00, 0x00, 0x00, 0x00, 0x00]);
655        assert!(result.is_err());
656    }
657
658    #[test]
659    fn test_sei_payload_type_roundtrip() {
660        for &(byte, expected) in &[
661            (0u8, SeiPayloadType::BufferingPeriod),
662            (1, SeiPayloadType::PictureTiming),
663            (5, SeiPayloadType::UserDataUnregistered),
664            (255, SeiPayloadType::Unknown),
665        ] {
666            assert_eq!(SeiPayloadType::from_byte(byte), expected);
667        }
668    }
669}