Skip to main content

oximedia_codec/multipass/
lookahead.rs

1//! Look-ahead buffer and frame analysis.
2//!
3//! The look-ahead system analyzes future frames to make better encoding
4//! decisions for the current frame, including scene change detection and
5//! adaptive quantization.
6
7#![forbid(unsafe_code)]
8#![allow(clippy::cast_precision_loss)]
9#![allow(clippy::cast_possible_truncation)]
10#![allow(clippy::cast_sign_loss)]
11#![allow(clippy::cast_lossless)]
12#![allow(clippy::too_many_arguments)]
13
14use crate::frame::{FrameType, VideoFrame};
15use crate::multipass::complexity::{ComplexityAnalyzer, FrameComplexity};
16use std::collections::VecDeque;
17
18/// Configuration for the look-ahead system.
19#[derive(Clone, Debug)]
20pub struct LookaheadConfig {
21    /// Number of frames to look ahead (10-250).
22    pub window_size: usize,
23    /// Minimum keyframe interval.
24    pub min_keyint: u32,
25    /// Maximum keyframe interval.
26    pub max_keyint: u32,
27    /// Scene change threshold (0.0-1.0).
28    pub scene_change_threshold: f64,
29    /// Enable adaptive quantization.
30    pub enable_aq: bool,
31}
32
33impl Default for LookaheadConfig {
34    fn default() -> Self {
35        Self {
36            window_size: 40,
37            min_keyint: 10,
38            max_keyint: 250,
39            scene_change_threshold: 0.4,
40            enable_aq: true,
41        }
42    }
43}
44
45impl LookaheadConfig {
46    /// Create a new lookahead configuration.
47    #[must_use]
48    pub fn new(window_size: usize) -> Self {
49        Self {
50            window_size: window_size.clamp(10, 250),
51            ..Default::default()
52        }
53    }
54
55    /// Set keyframe interval range.
56    #[must_use]
57    pub fn with_keyint_range(mut self, min: u32, max: u32) -> Self {
58        self.min_keyint = min;
59        self.max_keyint = max;
60        self
61    }
62
63    /// Set scene change threshold.
64    #[must_use]
65    pub fn with_scene_threshold(mut self, threshold: f64) -> Self {
66        self.scene_change_threshold = threshold.clamp(0.0, 1.0);
67        self
68    }
69}
70
71/// A frame stored in the lookahead buffer.
72#[derive(Clone, Debug)]
73pub struct LookaheadFrame {
74    /// The actual video frame.
75    pub frame: VideoFrame,
76    /// Frame complexity analysis.
77    pub complexity: FrameComplexity,
78    /// Assigned frame type (may be updated during lookahead).
79    pub assigned_type: FrameType,
80    /// QP offset from base (for adaptive quantization).
81    pub qp_offset: i32,
82}
83
84impl LookaheadFrame {
85    /// Create a new lookahead frame.
86    #[must_use]
87    pub fn new(frame: VideoFrame, complexity: FrameComplexity) -> Self {
88        Self {
89            assigned_type: frame.frame_type,
90            frame,
91            complexity,
92            qp_offset: 0,
93        }
94    }
95}
96
97/// Look-ahead buffer for analyzing future frames.
98pub struct LookaheadBuffer {
99    config: LookaheadConfig,
100    buffer: VecDeque<LookaheadFrame>,
101    complexity_analyzer: ComplexityAnalyzer,
102    frames_since_keyframe: u32,
103    total_frames_analyzed: u64,
104}
105
106impl LookaheadBuffer {
107    /// Create a new lookahead buffer.
108    #[must_use]
109    pub fn new(config: LookaheadConfig, width: u32, height: u32) -> Self {
110        let mut analyzer = ComplexityAnalyzer::new(width, height);
111        analyzer.set_scene_change_threshold(config.scene_change_threshold);
112
113        let window_size = config.window_size;
114
115        Self {
116            config,
117            buffer: VecDeque::with_capacity(window_size),
118            complexity_analyzer: analyzer,
119            frames_since_keyframe: 0,
120            total_frames_analyzed: 0,
121        }
122    }
123
124    /// Add a frame to the lookahead buffer.
125    pub fn add_frame(&mut self, frame: VideoFrame) {
126        let complexity = self
127            .complexity_analyzer
128            .analyze(&frame, self.total_frames_analyzed);
129        let mut lookahead_frame = LookaheadFrame::new(frame, complexity);
130
131        // Detect scene changes and force keyframes
132        if lookahead_frame.complexity.is_scene_change
133            && self.frames_since_keyframe >= self.config.min_keyint
134        {
135            lookahead_frame.assigned_type = FrameType::Key;
136            self.frames_since_keyframe = 0;
137        } else if self.frames_since_keyframe >= self.config.max_keyint {
138            // Force keyframe at max interval
139            lookahead_frame.assigned_type = FrameType::Key;
140            self.frames_since_keyframe = 0;
141        } else {
142            lookahead_frame.assigned_type = FrameType::Inter;
143            self.frames_since_keyframe += 1;
144        }
145
146        // Calculate adaptive QP offset
147        if self.config.enable_aq {
148            lookahead_frame.qp_offset = self.calculate_aq_offset(&lookahead_frame);
149        }
150
151        self.buffer.push_back(lookahead_frame);
152        self.total_frames_analyzed += 1;
153
154        // Limit buffer size
155        while self.buffer.len() > self.config.window_size {
156            self.buffer.pop_front();
157        }
158    }
159
160    /// Get the next frame to encode (removes from buffer).
161    pub fn get_next_frame(&mut self) -> Option<LookaheadFrame> {
162        self.buffer.pop_front()
163    }
164
165    /// Peek at the next frame without removing it.
166    #[must_use]
167    pub fn peek_next(&self) -> Option<&LookaheadFrame> {
168        self.buffer.front()
169    }
170
171    /// Get number of frames in buffer.
172    #[must_use]
173    pub fn buffer_size(&self) -> usize {
174        self.buffer.len()
175    }
176
177    /// Check if buffer is full.
178    #[must_use]
179    pub fn is_full(&self) -> bool {
180        self.buffer.len() >= self.config.window_size
181    }
182
183    /// Check if buffer is empty.
184    #[must_use]
185    pub fn is_empty(&self) -> bool {
186        self.buffer.is_empty()
187    }
188
189    /// Analyze future frames and get encoding recommendations.
190    #[must_use]
191    pub fn analyze_window(&self) -> LookaheadAnalysis {
192        if self.buffer.is_empty() {
193            return LookaheadAnalysis::default();
194        }
195
196        let mut analysis = LookaheadAnalysis::default();
197        analysis.total_frames = self.buffer.len();
198
199        // Analyze complexity distribution
200        let complexities: Vec<f64> = self
201            .buffer
202            .iter()
203            .map(|f| f.complexity.combined_complexity)
204            .collect();
205
206        analysis.avg_complexity = complexities.iter().sum::<f64>() / complexities.len() as f64;
207
208        analysis.min_complexity = complexities
209            .iter()
210            .copied()
211            .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
212            .unwrap_or(0.0);
213
214        analysis.max_complexity = complexities
215            .iter()
216            .copied()
217            .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
218            .unwrap_or(0.0);
219
220        // Count scene changes
221        analysis.scene_changes = self
222            .buffer
223            .iter()
224            .filter(|f| f.complexity.is_scene_change)
225            .count();
226
227        // Count keyframes
228        analysis.keyframes = self
229            .buffer
230            .iter()
231            .filter(|f| f.assigned_type == FrameType::Key)
232            .count();
233
234        // Find next scene change distance
235        analysis.next_scene_change = self
236            .buffer
237            .iter()
238            .position(|f| f.complexity.is_scene_change)
239            .map(|pos| pos as u32);
240
241        // Calculate complexity variance
242        let variance: f64 = complexities
243            .iter()
244            .map(|c| (c - analysis.avg_complexity).powi(2))
245            .sum::<f64>()
246            / complexities.len() as f64;
247        analysis.complexity_variance = variance;
248
249        analysis
250    }
251
252    /// Calculate adaptive QP offset based on frame complexity.
253    fn calculate_aq_offset(&self, frame: &LookaheadFrame) -> i32 {
254        // Calculate relative complexity compared to recent history
255        let relative_complexity = if self.buffer.is_empty() {
256            1.0
257        } else {
258            let avg_complexity: f64 = self
259                .buffer
260                .iter()
261                .map(|f| f.complexity.combined_complexity)
262                .sum::<f64>()
263                / self.buffer.len() as f64;
264
265            if avg_complexity > 0.01 {
266                frame.complexity.combined_complexity / avg_complexity
267            } else {
268                1.0
269            }
270        };
271
272        // Convert to QP offset (-10 to +10)
273        // Lower complexity -> higher QP (worse quality, fewer bits)
274        // Higher complexity -> lower QP (better quality, more bits)
275        let offset = (10.0 * (1.0 - relative_complexity)).clamp(-10.0, 10.0);
276        offset as i32
277    }
278
279    /// Get the current configuration.
280    #[must_use]
281    pub fn config(&self) -> &LookaheadConfig {
282        &self.config
283    }
284
285    /// Reset the lookahead buffer.
286    pub fn reset(&mut self) {
287        self.buffer.clear();
288        self.complexity_analyzer.reset();
289        self.frames_since_keyframe = 0;
290        self.total_frames_analyzed = 0;
291    }
292
293    /// Flush remaining frames from the buffer.
294    pub fn flush(&mut self) -> Vec<LookaheadFrame> {
295        self.buffer.drain(..).collect()
296    }
297}
298
299/// Analysis results from the lookahead window.
300#[derive(Clone, Debug, Default)]
301pub struct LookaheadAnalysis {
302    /// Total frames in lookahead window.
303    pub total_frames: usize,
304    /// Average complexity across window.
305    pub avg_complexity: f64,
306    /// Minimum complexity in window.
307    pub min_complexity: f64,
308    /// Maximum complexity in window.
309    pub max_complexity: f64,
310    /// Variance in complexity.
311    pub complexity_variance: f64,
312    /// Number of scene changes detected.
313    pub scene_changes: usize,
314    /// Number of keyframes in window.
315    pub keyframes: usize,
316    /// Distance to next scene change (if any).
317    pub next_scene_change: Option<u32>,
318}
319
320impl LookaheadAnalysis {
321    /// Check if complexity is stable (low variance).
322    #[must_use]
323    pub fn is_stable(&self) -> bool {
324        self.complexity_variance < 0.1
325    }
326
327    /// Get complexity range.
328    #[must_use]
329    pub fn complexity_range(&self) -> f64 {
330        self.max_complexity - self.min_complexity
331    }
332}
333
334/// Scene change detector for video frames.
335pub struct SceneChangeDetector {
336    threshold: f64,
337    width: u32,
338    height: u32,
339    prev_frame: Option<Vec<u8>>,
340}
341
342impl SceneChangeDetector {
343    /// Create a new scene change detector.
344    #[must_use]
345    pub fn new(width: u32, height: u32, threshold: f64) -> Self {
346        Self {
347            threshold: threshold.clamp(0.0, 1.0),
348            width,
349            height,
350            prev_frame: None,
351        }
352    }
353
354    /// Detect if the current frame is a scene change.
355    #[must_use]
356    pub fn detect(&mut self, frame: &VideoFrame) -> bool {
357        if let Some(luma_plane) = frame.planes.first() {
358            let luma_data = luma_plane.data.as_ref();
359
360            if let Some(prev) = &self.prev_frame {
361                let sad = self.compute_sad(luma_data, prev);
362                let pixels = (self.width as u64) * (self.height as u64);
363                let avg_sad = if pixels > 0 {
364                    sad as f64 / pixels as f64
365                } else {
366                    0.0
367                };
368
369                let normalized_sad = (avg_sad / 50.0).min(1.0);
370                let is_scene_change = normalized_sad > self.threshold;
371
372                self.prev_frame = Some(luma_data.to_vec());
373                is_scene_change
374            } else {
375                self.prev_frame = Some(luma_data.to_vec());
376                true // First frame is always a scene change
377            }
378        } else {
379            false
380        }
381    }
382
383    /// Compute Sum of Absolute Differences.
384    fn compute_sad(&self, current: &[u8], previous: &[u8]) -> u64 {
385        let mut sad = 0u64;
386        let len = current.len().min(previous.len());
387
388        for i in 0..len {
389            let diff = (current[i] as i32 - previous[i] as i32).abs();
390            sad += diff as u64;
391        }
392
393        sad
394    }
395
396    /// Reset detector state.
397    pub fn reset(&mut self) {
398        self.prev_frame = None;
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405    use crate::frame::Plane;
406    use oximedia_core::{PixelFormat, Rational, Timestamp};
407
408    fn create_test_frame(width: u32, height: u32, value: u8) -> VideoFrame {
409        let mut frame = VideoFrame::new(PixelFormat::Yuv420p, width, height);
410        let size = (width * height) as usize;
411        let data = vec![value; size];
412        frame.planes.push(Plane::new(data, width as usize));
413        frame.timestamp = Timestamp::new(0, Rational::new(1, 30));
414        frame
415    }
416
417    #[test]
418    fn test_lookahead_config_new() {
419        let config = LookaheadConfig::new(50);
420        assert_eq!(config.window_size, 50);
421        assert_eq!(config.max_keyint, 250);
422    }
423
424    #[test]
425    fn test_lookahead_config_clamp() {
426        let config = LookaheadConfig::new(5); // Below minimum
427        assert_eq!(config.window_size, 10); // Clamped to min
428
429        let config2 = LookaheadConfig::new(300); // Above maximum
430        assert_eq!(config2.window_size, 250); // Clamped to max
431    }
432
433    #[test]
434    fn test_lookahead_buffer_new() {
435        let config = LookaheadConfig::new(40);
436        let buffer = LookaheadBuffer::new(config, 1920, 1080);
437        assert_eq!(buffer.buffer_size(), 0);
438        assert!(buffer.is_empty());
439    }
440
441    #[test]
442    fn test_lookahead_add_frame() {
443        let config = LookaheadConfig::new(40);
444        let mut buffer = LookaheadBuffer::new(config, 320, 240);
445
446        let frame = create_test_frame(320, 240, 128);
447        buffer.add_frame(frame);
448
449        assert_eq!(buffer.buffer_size(), 1);
450        assert!(!buffer.is_empty());
451    }
452
453    #[test]
454    fn test_lookahead_get_next_frame() {
455        let config = LookaheadConfig::new(40);
456        let mut buffer = LookaheadBuffer::new(config, 320, 240);
457
458        let frame = create_test_frame(320, 240, 128);
459        buffer.add_frame(frame);
460
461        let next = buffer.get_next_frame();
462        assert!(next.is_some());
463        assert!(buffer.is_empty());
464    }
465
466    #[test]
467    fn test_lookahead_buffer_full() {
468        let config = LookaheadConfig::new(10);
469        let mut buffer = LookaheadBuffer::new(config, 320, 240);
470
471        for i in 0..15 {
472            let frame = create_test_frame(320, 240, i as u8);
473            buffer.add_frame(frame);
474        }
475
476        // Buffer should be limited to window size
477        assert_eq!(buffer.buffer_size(), 10);
478        assert!(buffer.is_full());
479    }
480
481    #[test]
482    fn test_lookahead_analyze_window() {
483        let config = LookaheadConfig::new(40);
484        let mut buffer = LookaheadBuffer::new(config, 320, 240);
485
486        for i in 0..10 {
487            let frame = create_test_frame(320, 240, i as u8);
488            buffer.add_frame(frame);
489        }
490
491        let analysis = buffer.analyze_window();
492        assert_eq!(analysis.total_frames, 10);
493        assert!(analysis.avg_complexity >= 0.0);
494    }
495
496    #[test]
497    fn test_scene_change_detector() {
498        let mut detector = SceneChangeDetector::new(320, 240, 0.4);
499
500        let frame1 = create_test_frame(320, 240, 0);
501        assert!(detector.detect(&frame1)); // First frame
502
503        let frame2 = create_test_frame(320, 240, 255);
504        assert!(detector.detect(&frame2)); // Big change
505
506        let frame3 = create_test_frame(320, 240, 250);
507        assert!(!detector.detect(&frame3)); // Small change
508    }
509}