Skip to main content

oximedia_codec/
frame_types.rs

1//! Frame type management for video encoding.
2//!
3//! Covers I/P/B frame type decisions, GOP (Group Of Pictures) structure,
4//! reference frame lists, and frame ordering utilities.
5
6#![allow(dead_code)]
7
8/// The coding type of a video frame.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum CodingFrameType {
11    /// Intra-coded frame – no reference to other frames; also called a keyframe.
12    I,
13    /// Predictively coded frame – references one past frame.
14    P,
15    /// Bi-directionally coded frame – references past and future frames.
16    B,
17    /// An IDR (Instantaneous Decoder Refresh) I-frame that clears the DPB.
18    Idr,
19}
20
21impl CodingFrameType {
22    /// Returns `true` if this frame can be used as a reference by subsequent frames.
23    #[must_use]
24    pub fn is_reference(self) -> bool {
25        !matches!(self, Self::B)
26    }
27
28    /// Returns `true` if this frame is intra-coded (no inter dependencies).
29    #[must_use]
30    pub fn is_intra(self) -> bool {
31        matches!(self, Self::I | Self::Idr)
32    }
33
34    /// Returns a short ASCII label for this frame type.
35    #[must_use]
36    pub fn label(self) -> &'static str {
37        match self {
38            Self::I => "I",
39            Self::P => "P",
40            Self::B => "B",
41            Self::Idr => "IDR",
42        }
43    }
44}
45
46/// A display-order frame descriptor with its assigned coding type.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct FrameDescriptor {
49    /// Zero-based display-order index.
50    pub display_index: u64,
51    /// Assigned coding type for this frame.
52    pub frame_type: CodingFrameType,
53    /// Decode-order index (may differ from `display_index` for B-frames).
54    pub decode_index: u64,
55    /// Quantizer parameter override, if any (0 = use default).
56    pub qp_override: Option<u8>,
57}
58
59impl FrameDescriptor {
60    /// Creates a new frame descriptor with matching display and decode indices.
61    #[must_use]
62    pub fn new(index: u64, frame_type: CodingFrameType) -> Self {
63        Self {
64            display_index: index,
65            frame_type,
66            decode_index: index,
67            qp_override: None,
68        }
69    }
70
71    /// Sets an explicit decode-order index.
72    #[must_use]
73    pub fn with_decode_index(mut self, decode_index: u64) -> Self {
74        self.decode_index = decode_index;
75        self
76    }
77
78    /// Attaches a QP override.
79    #[must_use]
80    pub fn with_qp(mut self, qp: u8) -> Self {
81        self.qp_override = Some(qp);
82        self
83    }
84}
85
86/// Configuration for a Group Of Pictures (GOP).
87#[derive(Debug, Clone)]
88pub struct GopConfig {
89    /// Maximum GOP length (number of frames between I-frames).
90    pub max_gop_size: u32,
91    /// Number of consecutive B-frames between each pair of reference frames.
92    pub b_frames: u32,
93    /// Whether closed GOPs are used (each GOP is independently decodable).
94    pub closed_gop: bool,
95    /// Whether adaptive scene-change detection may insert extra I-frames.
96    pub adaptive_keyframes: bool,
97}
98
99impl GopConfig {
100    /// Creates a config for a simple all-I-frame stream.
101    #[must_use]
102    pub fn intra_only() -> Self {
103        Self {
104            max_gop_size: 1,
105            b_frames: 0,
106            closed_gop: true,
107            adaptive_keyframes: false,
108        }
109    }
110
111    /// Creates a standard IP-only GOP (no B-frames).
112    #[must_use]
113    pub fn ip_only(gop_size: u32) -> Self {
114        Self {
115            max_gop_size: gop_size,
116            b_frames: 0,
117            closed_gop: true,
118            adaptive_keyframes: true,
119        }
120    }
121
122    /// Creates a standard IBP GOP with the given B-frame count.
123    #[must_use]
124    pub fn with_b_frames(gop_size: u32, b_frames: u32) -> Self {
125        Self {
126            max_gop_size: gop_size,
127            b_frames,
128            closed_gop: false,
129            adaptive_keyframes: true,
130        }
131    }
132}
133
134/// Generates a sequence of [`FrameDescriptor`] entries for `num_frames` frames
135/// given a [`GopConfig`].
136///
137/// This produces a simplified IBBBP…P pattern: one IDR at position 0, then
138/// I-frames at every `max_gop_size` boundary, B-frames filling the gaps, and
139/// P-frames at sub-GOP boundaries.
140#[must_use]
141pub fn generate_gop_sequence(config: &GopConfig, num_frames: u64) -> Vec<FrameDescriptor> {
142    let mut descriptors = Vec::with_capacity(num_frames as usize);
143    let gop = config.max_gop_size as u64;
144    let b = config.b_frames as u64;
145
146    for i in 0..num_frames {
147        let frame_type = if i == 0 {
148            CodingFrameType::Idr
149        } else if gop <= 1 {
150            // Intra-only: every frame after the first IDR is I
151            CodingFrameType::I
152        } else if i % gop == 0 {
153            CodingFrameType::I
154        } else if b > 0 {
155            // Frames just before a P-frame anchor.
156            let pos_in_gop = i % gop;
157            let sub_period = b + 1;
158            if pos_in_gop % sub_period == 0 {
159                CodingFrameType::P
160            } else {
161                CodingFrameType::B
162            }
163        } else {
164            CodingFrameType::P
165        };
166        descriptors.push(FrameDescriptor::new(i, frame_type));
167    }
168    descriptors
169}
170
171/// A pool of decoded reference frames available for inter-prediction.
172#[derive(Debug, Default)]
173pub struct ReferenceFramePool {
174    frames: Vec<u64>, // display indices of currently held reference frames
175    /// Maximum number of references to retain simultaneously.
176    pub capacity: usize,
177}
178
179impl ReferenceFramePool {
180    /// Creates a new pool with the given capacity.
181    #[must_use]
182    pub fn new(capacity: usize) -> Self {
183        Self {
184            frames: Vec::with_capacity(capacity),
185            capacity,
186        }
187    }
188
189    /// Inserts a reference frame by its display index.
190    /// If the pool is at capacity, the oldest entry is evicted (FIFO).
191    pub fn insert(&mut self, display_index: u64) {
192        if self.frames.len() == self.capacity {
193            self.frames.remove(0);
194        }
195        self.frames.push(display_index);
196    }
197
198    /// Returns `true` if `display_index` is currently in the pool.
199    #[must_use]
200    pub fn contains(&self, display_index: u64) -> bool {
201        self.frames.contains(&display_index)
202    }
203
204    /// Returns the number of frames currently in the pool.
205    #[must_use]
206    pub fn len(&self) -> usize {
207        self.frames.len()
208    }
209
210    /// Returns `true` if the pool is empty.
211    #[must_use]
212    pub fn is_empty(&self) -> bool {
213        self.frames.is_empty()
214    }
215
216    /// Clears all reference frames (used on IDR boundaries).
217    pub fn clear(&mut self) {
218        self.frames.clear();
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn test_i_frame_is_intra() {
228        assert!(CodingFrameType::I.is_intra());
229        assert!(CodingFrameType::Idr.is_intra());
230    }
231
232    #[test]
233    fn test_p_frame_not_intra() {
234        assert!(!CodingFrameType::P.is_intra());
235    }
236
237    #[test]
238    fn test_b_frame_not_reference() {
239        assert!(!CodingFrameType::B.is_reference());
240    }
241
242    #[test]
243    fn test_i_and_p_are_references() {
244        assert!(CodingFrameType::I.is_reference());
245        assert!(CodingFrameType::P.is_reference());
246        assert!(CodingFrameType::Idr.is_reference());
247    }
248
249    #[test]
250    fn test_frame_type_labels() {
251        assert_eq!(CodingFrameType::I.label(), "I");
252        assert_eq!(CodingFrameType::P.label(), "P");
253        assert_eq!(CodingFrameType::B.label(), "B");
254        assert_eq!(CodingFrameType::Idr.label(), "IDR");
255    }
256
257    #[test]
258    fn test_frame_descriptor_defaults() {
259        let fd = FrameDescriptor::new(5, CodingFrameType::P);
260        assert_eq!(fd.display_index, 5);
261        assert_eq!(fd.decode_index, 5);
262        assert_eq!(fd.qp_override, None);
263    }
264
265    #[test]
266    fn test_frame_descriptor_with_qp() {
267        let fd = FrameDescriptor::new(0, CodingFrameType::I).with_qp(22);
268        assert_eq!(fd.qp_override, Some(22));
269    }
270
271    #[test]
272    fn test_gop_sequence_starts_with_idr() {
273        let cfg = GopConfig::ip_only(30);
274        let seq = generate_gop_sequence(&cfg, 10);
275        assert_eq!(seq[0].frame_type, CodingFrameType::Idr);
276    }
277
278    #[test]
279    fn test_intra_only_all_idr_or_i() {
280        let cfg = GopConfig::intra_only();
281        let seq = generate_gop_sequence(&cfg, 5);
282        for (i, fd) in seq.iter().enumerate() {
283            if i == 0 {
284                assert_eq!(fd.frame_type, CodingFrameType::Idr);
285            } else {
286                assert!(fd.frame_type.is_intra());
287            }
288        }
289    }
290
291    #[test]
292    fn test_ip_sequence_no_b_frames() {
293        let cfg = GopConfig::ip_only(8);
294        let seq = generate_gop_sequence(&cfg, 16);
295        for fd in &seq {
296            assert!(!matches!(fd.frame_type, CodingFrameType::B));
297        }
298    }
299
300    #[test]
301    fn test_reference_pool_capacity_eviction() {
302        let mut pool = ReferenceFramePool::new(3);
303        pool.insert(0);
304        pool.insert(1);
305        pool.insert(2);
306        pool.insert(3); // should evict 0
307        assert!(!pool.contains(0));
308        assert!(pool.contains(3));
309    }
310
311    #[test]
312    fn test_reference_pool_len() {
313        let mut pool = ReferenceFramePool::new(4);
314        assert!(pool.is_empty());
315        pool.insert(10);
316        pool.insert(11);
317        assert_eq!(pool.len(), 2);
318    }
319
320    #[test]
321    fn test_reference_pool_clear_on_idr() {
322        let mut pool = ReferenceFramePool::new(4);
323        pool.insert(0);
324        pool.insert(1);
325        pool.clear();
326        assert!(pool.is_empty());
327    }
328}