Skip to main content

oximedia_codec/
gop_structure.rs

1//! GOP (Group of Pictures) structure analysis and planning.
2//!
3//! Provides GOP boundary detection, hierarchical B-frame pyramid layouts,
4//! scene-change–based keyframe insertion, and GOP statistics.
5
6#![allow(dead_code)]
7
8/// The role a frame plays within the B-pyramid hierarchy.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub enum PyramidLevel {
11    /// Top-level anchor frame (P or I); level 0 has highest quality.
12    Anchor = 0,
13    /// Level-1 B-frame (references two anchors).
14    L1 = 1,
15    /// Level-2 B-frame (references two level-1 frames).
16    L2 = 2,
17    /// Level-3 B-frame (leaf; references two level-2 frames).
18    L3 = 3,
19}
20
21impl PyramidLevel {
22    /// Returns the suggested QP delta relative to the top-level anchor.
23    #[must_use]
24    pub fn qp_delta(self) -> i8 {
25        match self {
26            Self::Anchor => 0,
27            Self::L1 => 1,
28            Self::L2 => 2,
29            Self::L3 => 4,
30        }
31    }
32}
33
34/// Describes a single frame's position within a planned GOP.
35#[derive(Debug, Clone)]
36pub struct GopFrame {
37    /// Display-order position (0-based within the GOP).
38    pub position: u32,
39    /// Whether this frame is the GOP's opening I- or IDR-frame.
40    pub is_keyframe: bool,
41    /// Whether this frame is a B-frame.
42    pub is_b_frame: bool,
43    /// The B-pyramid level for this frame.
44    pub pyramid_level: PyramidLevel,
45}
46
47impl GopFrame {
48    /// Convenience constructor for a keyframe.
49    #[must_use]
50    pub fn keyframe(position: u32) -> Self {
51        Self {
52            position,
53            is_keyframe: true,
54            is_b_frame: false,
55            pyramid_level: PyramidLevel::Anchor,
56        }
57    }
58
59    /// Convenience constructor for a P-frame anchor.
60    #[must_use]
61    pub fn p_frame(position: u32) -> Self {
62        Self {
63            position,
64            is_keyframe: false,
65            is_b_frame: false,
66            pyramid_level: PyramidLevel::Anchor,
67        }
68    }
69
70    /// Convenience constructor for a B-frame at a given pyramid level.
71    #[must_use]
72    pub fn b_frame(position: u32, level: PyramidLevel) -> Self {
73        Self {
74            position,
75            is_keyframe: false,
76            is_b_frame: true,
77            pyramid_level: level,
78        }
79    }
80}
81
82/// Aggregate statistics about a planned or observed GOP.
83#[derive(Debug, Clone, Default)]
84pub struct GopStatistics {
85    /// Total number of frames in the GOP.
86    pub total_frames: u32,
87    /// Number of I-frames (including IDR).
88    pub i_frame_count: u32,
89    /// Number of P-frames.
90    pub p_frame_count: u32,
91    /// Number of B-frames.
92    pub b_frame_count: u32,
93    /// Average pyramid level across all frames.
94    pub avg_pyramid_level: f32,
95}
96
97impl GopStatistics {
98    /// Computes the B-frame ratio (0.0–1.0).
99    #[must_use]
100    pub fn b_ratio(&self) -> f32 {
101        if self.total_frames == 0 {
102            return 0.0;
103        }
104        self.b_frame_count as f32 / self.total_frames as f32
105    }
106}
107
108/// Plans a mini-GOP with a 2-level B-pyramid for `gop_size` frames.
109///
110/// Returns a `Vec<GopFrame>` in display order where frame 0 is always a
111/// keyframe and subsequent anchors appear at intervals of `anchor_interval`.
112#[must_use]
113pub fn plan_gop(gop_size: u32, anchor_interval: u32) -> Vec<GopFrame> {
114    let mut frames = Vec::with_capacity(gop_size as usize);
115    if gop_size == 0 {
116        return frames;
117    }
118    for pos in 0..gop_size {
119        let frame = if pos == 0 {
120            GopFrame::keyframe(pos)
121        } else if anchor_interval == 0 || pos % anchor_interval == 0 {
122            GopFrame::p_frame(pos)
123        } else {
124            let offset = pos % anchor_interval;
125            let half = anchor_interval / 2;
126            if offset == half {
127                GopFrame::b_frame(pos, PyramidLevel::L1)
128            } else if offset % 2 == 0 {
129                GopFrame::b_frame(pos, PyramidLevel::L2)
130            } else {
131                GopFrame::b_frame(pos, PyramidLevel::L3)
132            }
133        };
134        frames.push(frame);
135    }
136    frames
137}
138
139/// Computes aggregate statistics over a slice of `GopFrame` descriptors.
140#[must_use]
141pub fn compute_statistics(frames: &[GopFrame]) -> GopStatistics {
142    let total = frames.len() as u32;
143    let mut i_count = 0u32;
144    let mut p_count = 0u32;
145    let mut b_count = 0u32;
146    let mut level_sum = 0u32;
147
148    for f in frames {
149        if f.is_keyframe {
150            i_count += 1;
151        } else if f.is_b_frame {
152            b_count += 1;
153        } else {
154            p_count += 1;
155        }
156        level_sum += f.pyramid_level as u32;
157    }
158
159    let avg_level = if total > 0 {
160        level_sum as f32 / total as f32
161    } else {
162        0.0
163    };
164
165    GopStatistics {
166        total_frames: total,
167        i_frame_count: i_count,
168        p_frame_count: p_count,
169        b_frame_count: b_count,
170        avg_pyramid_level: avg_level,
171    }
172}
173
174/// Scene-change detector that decides whether a new keyframe should be
175/// inserted based on a simple sum-of-absolute-differences threshold.
176#[derive(Debug)]
177pub struct SceneChangeDetector {
178    /// SAD threshold above which a scene change is declared.
179    pub threshold: u64,
180    /// Minimum number of frames between forced keyframes.
181    pub min_keyframe_interval: u32,
182    frames_since_last_key: u32,
183}
184
185impl SceneChangeDetector {
186    /// Creates a new detector.
187    #[must_use]
188    pub fn new(threshold: u64, min_keyframe_interval: u32) -> Self {
189        Self {
190            threshold,
191            min_keyframe_interval,
192            frames_since_last_key: 0,
193        }
194    }
195
196    /// Updates the detector with the SAD value for the current frame.
197    /// Returns `true` if a scene change keyframe should be inserted.
198    pub fn update(&mut self, sad: u64) -> bool {
199        self.frames_since_last_key += 1;
200        let is_change =
201            self.frames_since_last_key >= self.min_keyframe_interval && sad >= self.threshold;
202        if is_change {
203            self.frames_since_last_key = 0;
204        }
205        is_change
206    }
207
208    /// Forces a keyframe reset (call on IDR insertion).
209    pub fn reset(&mut self) {
210        self.frames_since_last_key = 0;
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_pyramid_level_ordering() {
220        assert!(PyramidLevel::Anchor < PyramidLevel::L1);
221        assert!(PyramidLevel::L1 < PyramidLevel::L2);
222        assert!(PyramidLevel::L2 < PyramidLevel::L3);
223    }
224
225    #[test]
226    fn test_pyramid_qp_delta_increases() {
227        assert!(PyramidLevel::L3.qp_delta() > PyramidLevel::L2.qp_delta());
228        assert!(PyramidLevel::L2.qp_delta() > PyramidLevel::L1.qp_delta());
229        assert!(PyramidLevel::L1.qp_delta() > PyramidLevel::Anchor.qp_delta());
230    }
231
232    #[test]
233    fn test_gop_frame_keyframe() {
234        let f = GopFrame::keyframe(0);
235        assert!(f.is_keyframe);
236        assert!(!f.is_b_frame);
237    }
238
239    #[test]
240    fn test_gop_frame_b_frame() {
241        let f = GopFrame::b_frame(3, PyramidLevel::L2);
242        assert!(f.is_b_frame);
243        assert_eq!(f.pyramid_level, PyramidLevel::L2);
244    }
245
246    #[test]
247    fn test_plan_gop_first_frame_is_keyframe() {
248        let frames = plan_gop(16, 4);
249        assert!(frames[0].is_keyframe);
250    }
251
252    #[test]
253    fn test_plan_gop_length() {
254        let frames = plan_gop(30, 5);
255        assert_eq!(frames.len(), 30);
256    }
257
258    #[test]
259    fn test_plan_gop_zero_returns_empty() {
260        let frames = plan_gop(0, 4);
261        assert!(frames.is_empty());
262    }
263
264    #[test]
265    fn test_statistics_total_equals_i_plus_p_plus_b() {
266        let frames = plan_gop(16, 4);
267        let stats = compute_statistics(&frames);
268        assert_eq!(stats.total_frames, 16);
269        assert_eq!(
270            stats.i_frame_count + stats.p_frame_count + stats.b_frame_count,
271            stats.total_frames
272        );
273    }
274
275    #[test]
276    fn test_statistics_i_frame_count_at_least_one() {
277        let frames = plan_gop(10, 4);
278        let stats = compute_statistics(&frames);
279        assert!(stats.i_frame_count >= 1);
280    }
281
282    #[test]
283    fn test_b_ratio_range() {
284        let frames = plan_gop(20, 4);
285        let stats = compute_statistics(&frames);
286        assert!(stats.b_ratio() >= 0.0 && stats.b_ratio() <= 1.0);
287    }
288
289    #[test]
290    fn test_scene_change_not_triggered_below_interval() {
291        let mut det = SceneChangeDetector::new(1000, 5);
292        // Below min_keyframe_interval – should never trigger.
293        for _ in 0..4 {
294            assert!(!det.update(u64::MAX));
295        }
296    }
297
298    #[test]
299    fn test_scene_change_triggered_above_threshold() {
300        let mut det = SceneChangeDetector::new(500, 2);
301        det.update(0); // frame 1
302        let triggered = det.update(1000); // frame 2 – above threshold and at min interval
303        assert!(triggered);
304    }
305
306    #[test]
307    fn test_scene_change_reset() {
308        let mut det = SceneChangeDetector::new(100, 1);
309        det.update(200); // triggers keyframe, resets counter
310                         // After reset, next frame should NOT trigger (counter = 0, then 1 = interval).
311        let triggered = det.update(200);
312        assert!(triggered); // min_interval=1, so immediately eligible again
313        det.reset();
314        // Now forcibly reset – one more update needed.
315        assert!(det.update(200));
316    }
317
318    #[test]
319    fn test_statistics_empty_gop() {
320        let stats = compute_statistics(&[]);
321        assert_eq!(stats.total_frames, 0);
322        assert!((stats.b_ratio() - 0.0).abs() < 1e-6);
323    }
324}