Skip to main content

oximedia_codec/av1/
sequence.rs

1//! AV1 Sequence Header parsing and SVC temporal scalability.
2//!
3//! The Sequence Header OBU contains codec-level configuration.
4//! This module also provides temporal scalability (SVC) layer support
5//! as defined in AV1 Annex A for scalable encoding.
6
7use crate::error::{CodecError, CodecResult};
8use oximedia_io::BitReader;
9
10/// AV1 Sequence Header OBU.
11#[derive(Clone, Debug)]
12#[allow(clippy::struct_excessive_bools)]
13pub struct SequenceHeader {
14    /// Profile (0=Main, 1=High, 2=Professional).
15    pub profile: u8,
16    /// Still picture mode.
17    pub still_picture: bool,
18    /// Reduced still picture header.
19    pub reduced_still_picture_header: bool,
20    /// Maximum frame width minus 1.
21    pub max_frame_width_minus_1: u32,
22    /// Maximum frame height minus 1.
23    pub max_frame_height_minus_1: u32,
24    /// Enable order hint.
25    pub enable_order_hint: bool,
26    /// Order hint bits minus 1.
27    pub order_hint_bits: u8,
28    /// Enable superres.
29    pub enable_superres: bool,
30    /// Enable CDEF.
31    pub enable_cdef: bool,
32    /// Enable restoration.
33    pub enable_restoration: bool,
34    /// Color configuration.
35    pub color_config: ColorConfig,
36    /// Film grain params present.
37    pub film_grain_params_present: bool,
38}
39
40impl SequenceHeader {
41    /// Get maximum frame width.
42    #[must_use]
43    pub const fn max_frame_width(&self) -> u32 {
44        self.max_frame_width_minus_1 + 1
45    }
46
47    /// Get maximum frame height.
48    #[must_use]
49    pub const fn max_frame_height(&self) -> u32 {
50        self.max_frame_height_minus_1 + 1
51    }
52
53    /// Parse sequence header from OBU payload.
54    ///
55    /// # Errors
56    ///
57    /// Returns error if the header is malformed.
58    #[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
59    pub fn parse(data: &[u8]) -> CodecResult<Self> {
60        let mut reader = BitReader::new(data);
61
62        let profile = reader.read_bits(3).map_err(CodecError::Core)? as u8;
63        if profile > 2 {
64            return Err(CodecError::InvalidBitstream(format!(
65                "Invalid profile: {profile}"
66            )));
67        }
68
69        let still_picture = reader.read_bit().map_err(CodecError::Core)? != 0;
70        let reduced_still_picture_header = reader.read_bit().map_err(CodecError::Core)? != 0;
71
72        if reduced_still_picture_header && !still_picture {
73            return Err(CodecError::InvalidBitstream(
74                "reduced_still_picture_header requires still_picture".to_string(),
75            ));
76        }
77
78        // Skip timing info and operating points for simplified parsing
79        if reduced_still_picture_header {
80            reader.read_bits(5).map_err(CodecError::Core)?; // seq_level_idx[0]
81        } else {
82            // timing_info_present_flag
83            let timing_info_present = reader.read_bit().map_err(CodecError::Core)? != 0;
84            if timing_info_present {
85                reader.skip_bits(64); // Simplified: skip timing info
86                let decoder_model_info_present = reader.read_bit().map_err(CodecError::Core)? != 0;
87                if decoder_model_info_present {
88                    reader.skip_bits(47); // Simplified: skip decoder model info
89                }
90            }
91            // initial_display_delay_present_flag
92            reader.read_bit().map_err(CodecError::Core)?;
93            // operating_points_cnt_minus_1
94            let op_count = reader.read_bits(5).map_err(CodecError::Core)? as usize + 1;
95            for _ in 0..op_count {
96                reader.skip_bits(12); // operating_point_idc
97                let level = reader.read_bits(5).map_err(CodecError::Core)? as u8;
98                if level > 7 {
99                    reader.skip_bits(1); // seq_tier
100                }
101            }
102        }
103
104        let frame_width_bits = reader.read_bits(4).map_err(CodecError::Core)? as u8 + 1;
105        let frame_height_bits = reader.read_bits(4).map_err(CodecError::Core)? as u8 + 1;
106        let max_frame_width_minus_1 = reader
107            .read_bits(frame_width_bits)
108            .map_err(CodecError::Core)? as u32;
109        let max_frame_height_minus_1 = reader
110            .read_bits(frame_height_bits)
111            .map_err(CodecError::Core)? as u32;
112
113        let enable_order_hint;
114        let order_hint_bits;
115        let enable_superres;
116        let enable_cdef;
117        let enable_restoration;
118
119        if reduced_still_picture_header {
120            enable_order_hint = false;
121            order_hint_bits = 0;
122            enable_superres = false;
123            enable_cdef = false;
124            enable_restoration = false;
125        } else {
126            // frame_id_numbers_present_flag
127            let frame_id_present = reader.read_bit().map_err(CodecError::Core)? != 0;
128            if frame_id_present {
129                reader.skip_bits(7); // delta_frame_id_length_minus_2 + additional_frame_id_length_minus_1
130            }
131
132            // Tool enables
133            reader.skip_bits(7); // Various tool flags
134
135            enable_order_hint = reader.read_bit().map_err(CodecError::Core)? != 0;
136
137            if enable_order_hint {
138                reader.skip_bits(2); // enable_jnt_comp + enable_ref_frame_mvs
139            }
140
141            // seq_choose_screen_content_tools
142            let seq_choose_screen_content_tools = reader.read_bit().map_err(CodecError::Core)? != 0;
143            if !seq_choose_screen_content_tools {
144                reader.skip_bits(1);
145            }
146
147            // seq_choose_integer_mv
148            let seq_choose_integer_mv = reader.read_bit().map_err(CodecError::Core)? != 0;
149            if !seq_choose_integer_mv {
150                reader.skip_bits(1);
151            }
152
153            order_hint_bits = if enable_order_hint {
154                reader.read_bits(3).map_err(CodecError::Core)? as u8 + 1
155            } else {
156                0
157            };
158
159            enable_superres = reader.read_bit().map_err(CodecError::Core)? != 0;
160            enable_cdef = reader.read_bit().map_err(CodecError::Core)? != 0;
161            enable_restoration = reader.read_bit().map_err(CodecError::Core)? != 0;
162        }
163
164        let color_config = ColorConfig::parse(&mut reader, profile)?;
165        let film_grain_params_present = reader.read_bit().map_err(CodecError::Core)? != 0;
166
167        Ok(Self {
168            profile,
169            still_picture,
170            reduced_still_picture_header,
171            max_frame_width_minus_1,
172            max_frame_height_minus_1,
173            enable_order_hint,
174            order_hint_bits,
175            enable_superres,
176            enable_cdef,
177            enable_restoration,
178            color_config,
179            film_grain_params_present,
180        })
181    }
182}
183
184/// Color configuration.
185#[derive(Clone, Debug, Default)]
186#[allow(clippy::struct_excessive_bools)]
187pub struct ColorConfig {
188    /// Bit depth (8, 10, or 12).
189    pub bit_depth: u8,
190    /// Monochrome mode.
191    pub mono_chrome: bool,
192    /// Number of planes.
193    pub num_planes: u8,
194    /// Color primaries.
195    pub color_primaries: u8,
196    /// Transfer characteristics.
197    pub transfer_characteristics: u8,
198    /// Matrix coefficients.
199    pub matrix_coefficients: u8,
200    /// Full color range.
201    pub color_range: bool,
202    /// Subsampling X.
203    pub subsampling_x: bool,
204    /// Subsampling Y.
205    pub subsampling_y: bool,
206    /// Separate UV delta Q.
207    pub separate_uv_delta_q: bool,
208}
209
210impl ColorConfig {
211    /// Check if this is 4:2:0 subsampling.
212    #[must_use]
213    pub const fn is_420(&self) -> bool {
214        self.subsampling_x && self.subsampling_y
215    }
216
217    /// Parse color config from bitstream.
218    #[allow(clippy::cast_possible_truncation)]
219    fn parse(reader: &mut BitReader<'_>, profile: u8) -> CodecResult<Self> {
220        let high_bitdepth = reader.read_bit().map_err(CodecError::Core)? != 0;
221
222        let twelve_bit = if profile == 2 && high_bitdepth {
223            reader.read_bit().map_err(CodecError::Core)? != 0
224        } else {
225            false
226        };
227
228        let bit_depth = if profile == 2 && twelve_bit {
229            12
230        } else if high_bitdepth {
231            10
232        } else {
233            8
234        };
235
236        let mono_chrome = if profile == 1 {
237            false
238        } else {
239            reader.read_bit().map_err(CodecError::Core)? != 0
240        };
241
242        let num_planes = if mono_chrome { 1 } else { 3 };
243
244        let color_description_present = reader.read_bit().map_err(CodecError::Core)? != 0;
245
246        let (color_primaries, transfer_characteristics, matrix_coefficients) =
247            if color_description_present {
248                let cp = reader.read_bits(8).map_err(CodecError::Core)? as u8;
249                let tc = reader.read_bits(8).map_err(CodecError::Core)? as u8;
250                let mc = reader.read_bits(8).map_err(CodecError::Core)? as u8;
251                (cp, tc, mc)
252            } else {
253                (2, 2, 2)
254            };
255
256        let color_range;
257        let subsampling_x;
258        let subsampling_y;
259
260        if mono_chrome {
261            color_range = reader.read_bit().map_err(CodecError::Core)? != 0;
262            subsampling_x = true;
263            subsampling_y = true;
264        } else if color_primaries == 1 && transfer_characteristics == 13 && matrix_coefficients == 0
265        {
266            color_range = true;
267            subsampling_x = false;
268            subsampling_y = false;
269        } else {
270            color_range = reader.read_bit().map_err(CodecError::Core)? != 0;
271
272            if profile == 0 {
273                subsampling_x = true;
274                subsampling_y = true;
275            } else if profile == 1 {
276                subsampling_x = false;
277                subsampling_y = false;
278            } else if bit_depth == 12 {
279                subsampling_x = reader.read_bit().map_err(CodecError::Core)? != 0;
280                subsampling_y = if subsampling_x {
281                    reader.read_bit().map_err(CodecError::Core)? != 0
282                } else {
283                    false
284                };
285            } else {
286                subsampling_x = true;
287                subsampling_y = false;
288            }
289
290            if subsampling_x && subsampling_y {
291                reader.skip_bits(2); // chroma_sample_position
292            }
293        }
294
295        let separate_uv_delta_q = if mono_chrome {
296            false
297        } else {
298            reader.read_bit().map_err(CodecError::Core)? != 0
299        };
300
301        Ok(Self {
302            bit_depth,
303            mono_chrome,
304            num_planes,
305            color_primaries,
306            transfer_characteristics,
307            matrix_coefficients,
308            color_range,
309            subsampling_x,
310            subsampling_y,
311            separate_uv_delta_q,
312        })
313    }
314}
315
316// =============================================================================
317// Temporal Scalability (SVC) Support
318// =============================================================================
319
320/// Maximum number of temporal layers in AV1 SVC.
321pub const MAX_TEMPORAL_LAYERS: usize = 8;
322
323/// Maximum number of spatial layers in AV1 SVC.
324pub const MAX_SPATIAL_LAYERS: usize = 4;
325
326/// Maximum total operating points (temporal x spatial).
327pub const MAX_OPERATING_POINTS: usize = MAX_TEMPORAL_LAYERS * MAX_SPATIAL_LAYERS;
328
329/// SVC (Scalable Video Coding) configuration for AV1.
330///
331/// AV1 supports temporal scalability through operating points defined
332/// in the sequence header. Each operating point specifies which temporal
333/// and spatial layers are included, enabling adaptive streaming where
334/// decoders can drop higher layers for lower latency or bandwidth.
335///
336/// # Temporal Layer Structure
337///
338/// ```text
339/// T0: I----P---------P---------P    (base layer, always decodable)
340/// T1:      |    P         P         (enhancement, depends on T0)
341/// T2:      |  P   P     P   P      (highest, depends on T0+T1)
342/// ```
343///
344/// # Example
345///
346/// ```ignore
347/// use oximedia_codec::av1::sequence::{SvcConfig, TemporalLayerConfig};
348///
349/// let mut svc = SvcConfig::new(3, 1); // 3 temporal, 1 spatial
350/// svc.set_temporal_layer(0, TemporalLayerConfig {
351///     layer_id: 0,
352///     framerate_fraction: 0.25,
353///     bitrate_fraction: 0.5,
354///     qp_offset: 0,
355///     reference_mode: SvcReferenceMode::KeyOnly,
356/// });
357/// ```
358#[derive(Clone, Debug)]
359pub struct SvcConfig {
360    /// Number of temporal layers (1-8).
361    pub num_temporal_layers: u8,
362    /// Number of spatial layers (1-4).
363    pub num_spatial_layers: u8,
364    /// Per-layer temporal configuration.
365    pub temporal_layers: Vec<TemporalLayerConfig>,
366    /// Per-layer spatial configuration.
367    pub spatial_layers: Vec<SpatialLayerConfig>,
368    /// Operating points derived from layer configuration.
369    pub operating_points: Vec<OperatingPoint>,
370    /// Enable inter-layer prediction.
371    pub inter_layer_prediction: bool,
372}
373
374impl SvcConfig {
375    /// Create a new SVC configuration.
376    ///
377    /// # Arguments
378    ///
379    /// * `temporal_layers` - Number of temporal layers (clamped to 1-8)
380    /// * `spatial_layers` - Number of spatial layers (clamped to 1-4)
381    #[must_use]
382    pub fn new(temporal_layers: u8, spatial_layers: u8) -> Self {
383        let num_t = temporal_layers.clamp(1, MAX_TEMPORAL_LAYERS as u8);
384        let num_s = spatial_layers.clamp(1, MAX_SPATIAL_LAYERS as u8);
385
386        let mut config = Self {
387            num_temporal_layers: num_t,
388            num_spatial_layers: num_s,
389            temporal_layers: Vec::with_capacity(num_t as usize),
390            spatial_layers: Vec::with_capacity(num_s as usize),
391            operating_points: Vec::new(),
392            inter_layer_prediction: true,
393        };
394
395        // Initialize default temporal layers with dyadic framerate distribution
396        for t in 0..num_t {
397            let fraction = 1.0 / (1 << (num_t - 1 - t)) as f32;
398            let bitrate_frac = Self::default_bitrate_fraction(t, num_t);
399            config.temporal_layers.push(TemporalLayerConfig {
400                layer_id: t,
401                framerate_fraction: fraction,
402                bitrate_fraction: bitrate_frac,
403                qp_offset: t as i8 * 2,
404                reference_mode: if t == 0 {
405                    SvcReferenceMode::KeyAndPrevious
406                } else {
407                    SvcReferenceMode::PreviousLayer
408                },
409            });
410        }
411
412        // Initialize default spatial layers (full resolution for single layer)
413        for s in 0..num_s {
414            let scale = 1.0 / (1 << (num_s - 1 - s)) as f32;
415            config.spatial_layers.push(SpatialLayerConfig {
416                layer_id: s,
417                width_scale: scale,
418                height_scale: scale,
419                bitrate_fraction: 1.0 / num_s as f32,
420            });
421        }
422
423        config.build_operating_points();
424        config
425    }
426
427    /// Compute default bitrate fraction for temporal layer using dyadic distribution.
428    fn default_bitrate_fraction(layer: u8, total: u8) -> f32 {
429        if total <= 1 {
430            return 1.0;
431        }
432        // Base layer gets largest share; each enhancement gets progressively less
433        // For 3 layers: T0=0.5, T1=0.3, T2=0.2
434        let weight = (1 << (total - 1 - layer)) as f32;
435        let total_weight: f32 = (0..total).map(|t| (1 << (total - 1 - t)) as f32).sum();
436        weight / total_weight
437    }
438
439    /// Set configuration for a specific temporal layer.
440    ///
441    /// # Arguments
442    ///
443    /// * `layer_id` - Temporal layer index (0-based)
444    /// * `config` - Layer configuration
445    pub fn set_temporal_layer(&mut self, layer_id: u8, config: TemporalLayerConfig) {
446        let idx = layer_id as usize;
447        if idx < self.temporal_layers.len() {
448            self.temporal_layers[idx] = config;
449            self.build_operating_points();
450        }
451    }
452
453    /// Set configuration for a specific spatial layer.
454    pub fn set_spatial_layer(&mut self, layer_id: u8, config: SpatialLayerConfig) {
455        let idx = layer_id as usize;
456        if idx < self.spatial_layers.len() {
457            self.spatial_layers[idx] = config;
458            self.build_operating_points();
459        }
460    }
461
462    /// Build operating points from layer configurations.
463    ///
464    /// Each operating point is identified by an `operating_point_idc` bitmask
465    /// where bits 0-7 indicate temporal layers and bits 8-11 indicate spatial layers.
466    fn build_operating_points(&mut self) {
467        self.operating_points.clear();
468
469        // Generate operating points for each combination of cumulative layers
470        for s in 0..self.num_spatial_layers {
471            for t in 0..self.num_temporal_layers {
472                let temporal_mask: u16 = (1 << (t + 1)) - 1; // Include layers 0..=t
473                let spatial_mask: u16 = ((1u16 << (s + 1)) - 1) << 8;
474                let idc = temporal_mask | spatial_mask;
475
476                let cumulative_bitrate: f32 = self
477                    .temporal_layers
478                    .iter()
479                    .take((t + 1) as usize)
480                    .map(|l| l.bitrate_fraction)
481                    .sum();
482
483                let framerate: f32 = self
484                    .temporal_layers
485                    .get(t as usize)
486                    .map_or(1.0, |l| l.framerate_fraction);
487
488                self.operating_points.push(OperatingPoint {
489                    idc,
490                    temporal_id: t,
491                    spatial_id: s,
492                    level: Self::estimate_level(t, s),
493                    tier: 0, // Main tier
494                    cumulative_bitrate_fraction: cumulative_bitrate,
495                    cumulative_framerate_fraction: framerate,
496                });
497            }
498        }
499    }
500
501    /// Estimate AV1 level for a given layer combination.
502    fn estimate_level(temporal_id: u8, spatial_id: u8) -> u8 {
503        // Simplified level estimation:
504        // Base layer starts at level 2.0 (idx=0), each enhancement bumps it
505        let base = 0u8; // Level 2.0
506        base.saturating_add(temporal_id)
507            .saturating_add(spatial_id * 2)
508    }
509
510    /// Get operating point for given temporal and spatial IDs.
511    #[must_use]
512    pub fn get_operating_point(&self, temporal_id: u8, spatial_id: u8) -> Option<&OperatingPoint> {
513        self.operating_points
514            .iter()
515            .find(|op| op.temporal_id == temporal_id && op.spatial_id == spatial_id)
516    }
517
518    /// Get total number of operating points.
519    #[must_use]
520    pub fn num_operating_points(&self) -> usize {
521        self.operating_points.len()
522    }
523
524    /// Determine which temporal layer a frame belongs to based on its index.
525    ///
526    /// Uses a dyadic temporal structure:
527    /// - Layer 0 (base): frames 0, 4, 8, ...  (for 3 layers)
528    /// - Layer 1:        frames 2, 6, 10, ...
529    /// - Layer 2:        frames 1, 3, 5, 7, ...
530    #[must_use]
531    pub fn frame_temporal_layer(&self, frame_index: u64) -> u8 {
532        if self.num_temporal_layers <= 1 {
533            return 0;
534        }
535
536        let n = self.num_temporal_layers;
537        let period = 1u64 << (n - 1);
538
539        if frame_index % period == 0 {
540            return 0; // Base layer
541        }
542
543        // Find highest power of 2 that divides frame_index
544        for layer in 1..n {
545            let step = period >> layer;
546            if step > 0 && frame_index % step == 0 {
547                return layer;
548            }
549        }
550
551        n - 1 // Highest layer
552    }
553
554    /// Get QP offset for a frame based on its temporal layer.
555    #[must_use]
556    pub fn frame_qp_offset(&self, frame_index: u64) -> i8 {
557        let layer = self.frame_temporal_layer(frame_index);
558        self.temporal_layers
559            .get(layer as usize)
560            .map_or(0, |l| l.qp_offset)
561    }
562
563    /// Check if a frame at given index can be dropped without affecting
564    /// lower temporal layers.
565    #[must_use]
566    pub fn is_droppable(&self, frame_index: u64) -> bool {
567        self.frame_temporal_layer(frame_index) > 0
568    }
569
570    /// Get the reference mode for a frame based on its temporal layer.
571    #[must_use]
572    pub fn frame_reference_mode(&self, frame_index: u64) -> SvcReferenceMode {
573        let layer = self.frame_temporal_layer(frame_index);
574        self.temporal_layers
575            .get(layer as usize)
576            .map_or(SvcReferenceMode::KeyAndPrevious, |l| l.reference_mode)
577    }
578
579    /// Validate the SVC configuration.
580    ///
581    /// # Errors
582    ///
583    /// Returns error if the configuration is inconsistent.
584    pub fn validate(&self) -> CodecResult<()> {
585        if self.num_temporal_layers == 0 || self.num_temporal_layers > MAX_TEMPORAL_LAYERS as u8 {
586            return Err(CodecError::InvalidParameter(format!(
587                "Invalid temporal layer count: {}",
588                self.num_temporal_layers
589            )));
590        }
591
592        if self.num_spatial_layers == 0 || self.num_spatial_layers > MAX_SPATIAL_LAYERS as u8 {
593            return Err(CodecError::InvalidParameter(format!(
594                "Invalid spatial layer count: {}",
595                self.num_spatial_layers
596            )));
597        }
598
599        // Verify bitrate fractions sum approximately to 1.0
600        let total_bitrate: f32 = self
601            .temporal_layers
602            .iter()
603            .map(|l| l.bitrate_fraction)
604            .sum();
605        if (total_bitrate - 1.0).abs() > 0.01 {
606            return Err(CodecError::InvalidParameter(format!(
607                "Temporal bitrate fractions sum to {total_bitrate}, expected ~1.0"
608            )));
609        }
610
611        // Verify framerate fractions are monotonically increasing
612        for i in 1..self.temporal_layers.len() {
613            if self.temporal_layers[i].framerate_fraction
614                < self.temporal_layers[i - 1].framerate_fraction
615            {
616                return Err(CodecError::InvalidParameter(
617                    "Temporal framerate fractions must be non-decreasing".to_string(),
618                ));
619            }
620        }
621
622        Ok(())
623    }
624}
625
626/// Configuration for a single temporal layer.
627#[derive(Clone, Debug)]
628pub struct TemporalLayerConfig {
629    /// Layer identifier (0 = base, higher = enhancement).
630    pub layer_id: u8,
631    /// Fraction of full framerate this layer represents (0.0-1.0).
632    pub framerate_fraction: f32,
633    /// Fraction of total bitrate allocated to this layer (0.0-1.0).
634    pub bitrate_fraction: f32,
635    /// QP offset relative to base layer (positive = lower quality).
636    pub qp_offset: i8,
637    /// Reference frame mode for this layer.
638    pub reference_mode: SvcReferenceMode,
639}
640
641/// Configuration for a single spatial layer.
642#[derive(Clone, Debug)]
643pub struct SpatialLayerConfig {
644    /// Layer identifier (0 = lowest resolution).
645    pub layer_id: u8,
646    /// Width scale relative to full resolution (0.0-1.0).
647    pub width_scale: f32,
648    /// Height scale relative to full resolution (0.0-1.0).
649    pub height_scale: f32,
650    /// Fraction of total bitrate allocated to this layer.
651    pub bitrate_fraction: f32,
652}
653
654/// Reference frame mode for SVC temporal layers.
655#[derive(Clone, Copy, Debug, PartialEq, Eq)]
656pub enum SvcReferenceMode {
657    /// Only reference key frames (most restrictive).
658    KeyOnly,
659    /// Reference key frames and previous same-layer frame.
660    KeyAndPrevious,
661    /// Reference the previous layer's frame.
662    PreviousLayer,
663    /// Reference any available frame (least restrictive).
664    Any,
665}
666
667/// An AV1 operating point defined by temporal+spatial layer combination.
668#[derive(Clone, Debug)]
669pub struct OperatingPoint {
670    /// Operating point IDC bitmask (bits 0-7: temporal, bits 8-11: spatial).
671    pub idc: u16,
672    /// Maximum temporal ID included.
673    pub temporal_id: u8,
674    /// Maximum spatial ID included.
675    pub spatial_id: u8,
676    /// AV1 level index for this operating point.
677    pub level: u8,
678    /// Tier (0 = Main, 1 = High).
679    pub tier: u8,
680    /// Cumulative bitrate fraction up to this temporal layer.
681    pub cumulative_bitrate_fraction: f32,
682    /// Cumulative framerate fraction at this temporal layer.
683    pub cumulative_framerate_fraction: f32,
684}
685
686impl OperatingPoint {
687    /// Get the operating point IDC for a given temporal and spatial layer set.
688    #[must_use]
689    pub fn compute_idc(max_temporal_id: u8, max_spatial_id: u8) -> u16 {
690        let temporal_mask: u16 = (1 << (max_temporal_id + 1)) - 1;
691        let spatial_mask: u16 = ((1u16 << (max_spatial_id + 1)) - 1) << 8;
692        temporal_mask | spatial_mask
693    }
694
695    /// Check if this operating point includes a given temporal layer.
696    #[must_use]
697    pub fn includes_temporal(&self, temporal_id: u8) -> bool {
698        (self.idc & (1 << temporal_id)) != 0
699    }
700
701    /// Check if this operating point includes a given spatial layer.
702    #[must_use]
703    pub fn includes_spatial(&self, spatial_id: u8) -> bool {
704        (self.idc & (1 << (spatial_id + 8))) != 0
705    }
706}
707
708#[cfg(test)]
709mod tests {
710    use super::*;
711
712    #[test]
713    fn test_sequence_header_dimensions() {
714        let header = SequenceHeader {
715            profile: 0,
716            still_picture: false,
717            reduced_still_picture_header: false,
718            max_frame_width_minus_1: 1919,
719            max_frame_height_minus_1: 1079,
720            enable_order_hint: false,
721            order_hint_bits: 0,
722            enable_superres: false,
723            enable_cdef: false,
724            enable_restoration: false,
725            color_config: ColorConfig::default(),
726            film_grain_params_present: false,
727        };
728
729        assert_eq!(header.max_frame_width(), 1920);
730        assert_eq!(header.max_frame_height(), 1080);
731    }
732
733    #[test]
734    fn test_color_config_subsampling() {
735        let config_420 = ColorConfig {
736            subsampling_x: true,
737            subsampling_y: true,
738            ..Default::default()
739        };
740        assert!(config_420.is_420());
741    }
742
743    // =====================================================================
744    // SVC Tests
745    // =====================================================================
746
747    #[test]
748    fn test_svc_config_creation() {
749        let svc = SvcConfig::new(3, 1);
750        assert_eq!(svc.num_temporal_layers, 3);
751        assert_eq!(svc.num_spatial_layers, 1);
752        assert_eq!(svc.temporal_layers.len(), 3);
753        assert_eq!(svc.spatial_layers.len(), 1);
754        assert!(svc.inter_layer_prediction);
755    }
756
757    #[test]
758    fn test_svc_config_clamping() {
759        let svc = SvcConfig::new(0, 10);
760        assert_eq!(svc.num_temporal_layers, 1);
761        assert_eq!(svc.num_spatial_layers, 4);
762    }
763
764    #[test]
765    fn test_svc_single_layer() {
766        let svc = SvcConfig::new(1, 1);
767        assert_eq!(svc.num_operating_points(), 1);
768        assert_eq!(svc.frame_temporal_layer(0), 0);
769        assert_eq!(svc.frame_temporal_layer(1), 0);
770        assert!(!svc.is_droppable(0));
771    }
772
773    #[test]
774    fn test_svc_two_temporal_layers() {
775        let svc = SvcConfig::new(2, 1);
776        assert_eq!(svc.num_temporal_layers, 2);
777
778        // Dyadic pattern: T0 at even frames, T1 at odd frames
779        assert_eq!(svc.frame_temporal_layer(0), 0);
780        assert_eq!(svc.frame_temporal_layer(1), 1);
781        assert_eq!(svc.frame_temporal_layer(2), 0);
782        assert_eq!(svc.frame_temporal_layer(3), 1);
783
784        assert!(!svc.is_droppable(0));
785        assert!(svc.is_droppable(1));
786        assert!(!svc.is_droppable(2));
787    }
788
789    #[test]
790    fn test_svc_three_temporal_layers() {
791        let svc = SvcConfig::new(3, 1);
792
793        // Dyadic pattern for 3 layers:
794        // T0: frames 0, 4, 8, ...
795        // T1: frames 2, 6, 10, ...
796        // T2: frames 1, 3, 5, 7, ...
797        assert_eq!(svc.frame_temporal_layer(0), 0);
798        assert_eq!(svc.frame_temporal_layer(1), 2);
799        assert_eq!(svc.frame_temporal_layer(2), 1);
800        assert_eq!(svc.frame_temporal_layer(3), 2);
801        assert_eq!(svc.frame_temporal_layer(4), 0);
802    }
803
804    #[test]
805    fn test_svc_operating_points() {
806        let svc = SvcConfig::new(3, 2);
807        // 3 temporal x 2 spatial = 6 operating points
808        assert_eq!(svc.num_operating_points(), 6);
809
810        // Base operating point (T0, S0)
811        let base_op = svc.get_operating_point(0, 0);
812        assert!(base_op.is_some());
813        let base = base_op.expect("base operating point exists");
814        assert_eq!(base.temporal_id, 0);
815        assert_eq!(base.spatial_id, 0);
816        assert!(base.includes_temporal(0));
817        assert!(!base.includes_temporal(1));
818    }
819
820    #[test]
821    fn test_svc_operating_point_idc() {
822        // T0+T1, S0 => temporal bits 0b11, spatial bits 0b01 << 8
823        let idc = OperatingPoint::compute_idc(1, 0);
824        assert_eq!(idc, 0x0103); // 0b0000_0001_0000_0011
825    }
826
827    #[test]
828    fn test_svc_qp_offset() {
829        let svc = SvcConfig::new(3, 1);
830
831        // Base layer has lowest QP offset (highest quality)
832        let qp0 = svc.frame_qp_offset(0); // T0
833        let qp1 = svc.frame_qp_offset(2); // T1
834        let qp2 = svc.frame_qp_offset(1); // T2
835
836        // Higher layers have higher QP offset (lower quality)
837        assert!(qp0 <= qp1);
838        assert!(qp1 <= qp2);
839    }
840
841    #[test]
842    fn test_svc_bitrate_fractions() {
843        let svc = SvcConfig::new(3, 1);
844
845        let total: f32 = svc.temporal_layers.iter().map(|l| l.bitrate_fraction).sum();
846        assert!((total - 1.0).abs() < 0.01);
847
848        // Base layer should get largest share
849        assert!(svc.temporal_layers[0].bitrate_fraction > svc.temporal_layers[1].bitrate_fraction);
850        assert!(svc.temporal_layers[1].bitrate_fraction > svc.temporal_layers[2].bitrate_fraction);
851    }
852
853    #[test]
854    fn test_svc_framerate_fractions() {
855        let svc = SvcConfig::new(3, 1);
856
857        // Framerate should be monotonically non-decreasing
858        for i in 1..svc.temporal_layers.len() {
859            assert!(
860                svc.temporal_layers[i].framerate_fraction
861                    >= svc.temporal_layers[i - 1].framerate_fraction
862            );
863        }
864    }
865
866    #[test]
867    fn test_svc_validation_valid() {
868        let svc = SvcConfig::new(3, 1);
869        assert!(svc.validate().is_ok());
870    }
871
872    #[test]
873    fn test_svc_validation_bad_bitrate() {
874        let mut svc = SvcConfig::new(2, 1);
875        svc.temporal_layers[0].bitrate_fraction = 0.1;
876        svc.temporal_layers[1].bitrate_fraction = 0.1;
877        // Sum = 0.2, far from 1.0
878        assert!(svc.validate().is_err());
879    }
880
881    #[test]
882    fn test_svc_set_temporal_layer() {
883        let mut svc = SvcConfig::new(3, 1);
884        svc.set_temporal_layer(
885            1,
886            TemporalLayerConfig {
887                layer_id: 1,
888                framerate_fraction: 0.5,
889                bitrate_fraction: 0.3,
890                qp_offset: 4,
891                reference_mode: SvcReferenceMode::Any,
892            },
893        );
894
895        assert_eq!(svc.temporal_layers[1].qp_offset, 4);
896        assert_eq!(svc.temporal_layers[1].reference_mode, SvcReferenceMode::Any);
897    }
898
899    #[test]
900    fn test_svc_spatial_layer_defaults() {
901        let svc = SvcConfig::new(2, 2);
902
903        assert_eq!(svc.spatial_layers.len(), 2);
904        // Lower spatial layer has smaller scale
905        assert!(svc.spatial_layers[0].width_scale < svc.spatial_layers[1].width_scale);
906    }
907
908    #[test]
909    fn test_svc_reference_mode() {
910        let svc = SvcConfig::new(3, 1);
911
912        let ref0 = svc.frame_reference_mode(0); // T0
913        let ref2 = svc.frame_reference_mode(1); // T2
914
915        // Base layer references key+previous; enhancement references previous layer
916        assert_eq!(ref0, SvcReferenceMode::KeyAndPrevious);
917        assert_eq!(ref2, SvcReferenceMode::PreviousLayer);
918    }
919
920    #[test]
921    fn test_operating_point_includes() {
922        let op = OperatingPoint {
923            idc: 0x0107, // T0+T1+T2, S0
924            temporal_id: 2,
925            spatial_id: 0,
926            level: 2,
927            tier: 0,
928            cumulative_bitrate_fraction: 1.0,
929            cumulative_framerate_fraction: 1.0,
930        };
931
932        assert!(op.includes_temporal(0));
933        assert!(op.includes_temporal(1));
934        assert!(op.includes_temporal(2));
935        assert!(!op.includes_temporal(3));
936        assert!(op.includes_spatial(0));
937        assert!(!op.includes_spatial(1));
938    }
939
940    #[test]
941    fn test_svc_droppable_frames() {
942        let svc = SvcConfig::new(3, 1);
943
944        // Collect droppable status for first 8 frames
945        let droppable: Vec<bool> = (0..8).map(|i| svc.is_droppable(i)).collect();
946
947        // Only base layer frames are not droppable
948        assert!(!droppable[0]); // T0
949        assert!(droppable[1]); // T2
950        assert!(droppable[2]); // T1
951        assert!(droppable[3]); // T2
952        assert!(!droppable[4]); // T0
953    }
954}