Skip to main content

oximedia_graph/filters/video/
fps.rs

1//! Frame rate adjustment filter.
2//!
3//! This filter adjusts the frame rate of video streams by dropping, duplicating,
4//! or blending frames as needed.
5
6#![forbid(unsafe_code)]
7#![allow(clippy::cast_lossless)]
8#![allow(clippy::cast_precision_loss)]
9#![allow(clippy::cast_possible_truncation)]
10#![allow(clippy::cast_sign_loss)]
11#![allow(clippy::cast_possible_wrap)]
12#![allow(clippy::similar_names)]
13#![allow(clippy::many_single_char_names)]
14#![allow(clippy::missing_errors_doc)]
15#![allow(clippy::match_same_arms)]
16#![allow(clippy::doc_markdown)]
17#![allow(clippy::unused_self)]
18#![allow(clippy::unnecessary_cast)]
19#![allow(clippy::bool_to_int_with_if)]
20#![allow(clippy::needless_range_loop)]
21#![allow(clippy::too_many_lines)]
22#![allow(clippy::unnecessary_wraps)]
23#![allow(clippy::map_unwrap_or)]
24#![allow(clippy::no_effect_underscore_binding)]
25#![allow(clippy::unreadable_literal)]
26#![allow(dead_code)]
27
28use std::collections::VecDeque;
29
30use crate::error::{GraphError, GraphResult};
31use crate::frame::FilterFrame;
32use crate::node::{Node, NodeId, NodeState, NodeType};
33use crate::port::{InputPort, OutputPort, PortFormat, PortId, PortType, VideoPortFormat};
34use oximedia_codec::{Plane, VideoFrame};
35use oximedia_core::{Rational, Timestamp};
36
37/// Frame rate adjustment mode.
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
39pub enum FpsMode {
40    /// Drop frames when downsampling, duplicate when upsampling.
41    #[default]
42    DropDuplicate,
43    /// Drop frames only (never duplicate).
44    Drop,
45    /// Duplicate frames only (never drop).
46    Duplicate,
47    /// Blend adjacent frames for smooth interpolation.
48    Blend,
49    /// Variable frame rate passthrough (adjust timestamps only).
50    Vfr,
51}
52
53impl FpsMode {
54    /// Check if this mode allows frame dropping.
55    #[must_use]
56    pub fn allows_drop(&self) -> bool {
57        matches!(self, Self::DropDuplicate | Self::Drop | Self::Blend)
58    }
59
60    /// Check if this mode allows frame duplication.
61    #[must_use]
62    pub fn allows_duplicate(&self) -> bool {
63        matches!(self, Self::DropDuplicate | Self::Duplicate | Self::Blend)
64    }
65}
66
67/// Configuration for the FPS filter.
68#[derive(Clone, Debug)]
69pub struct FpsConfig {
70    /// Target frame rate numerator.
71    pub fps_num: u32,
72    /// Target frame rate denominator.
73    pub fps_den: u32,
74    /// Frame rate adjustment mode.
75    pub mode: FpsMode,
76    /// Round timestamps to nearest frame (vs floor).
77    pub round: bool,
78    /// Start time offset in timebase units.
79    pub start_time: i64,
80    /// End-of-stream handling: output remaining buffered frames.
81    pub eof_action: EofAction,
82}
83
84impl FpsConfig {
85    /// Create a new FPS configuration with the given target frame rate.
86    #[must_use]
87    pub fn new(fps_num: u32, fps_den: u32) -> Self {
88        Self {
89            fps_num,
90            fps_den,
91            mode: FpsMode::default(),
92            round: true,
93            start_time: 0,
94            eof_action: EofAction::Pass,
95        }
96    }
97
98    /// Create a configuration for common frame rates.
99    #[must_use]
100    pub fn from_rate(fps: f64) -> Self {
101        let (num, den) = rational_from_float(fps);
102        Self::new(num, den)
103    }
104
105    /// Create a 24 fps configuration.
106    #[must_use]
107    pub fn fps_24() -> Self {
108        Self::new(24, 1)
109    }
110
111    /// Create a 25 fps configuration (PAL).
112    #[must_use]
113    pub fn fps_25() -> Self {
114        Self::new(25, 1)
115    }
116
117    /// Create a 30 fps configuration.
118    #[must_use]
119    pub fn fps_30() -> Self {
120        Self::new(30, 1)
121    }
122
123    /// Create a 29.97 fps configuration (NTSC).
124    #[must_use]
125    pub fn fps_29_97() -> Self {
126        Self::new(30000, 1001)
127    }
128
129    /// Create a 60 fps configuration.
130    #[must_use]
131    pub fn fps_60() -> Self {
132        Self::new(60, 1)
133    }
134
135    /// Create a 59.94 fps configuration (NTSC).
136    #[must_use]
137    pub fn fps_59_94() -> Self {
138        Self::new(60000, 1001)
139    }
140
141    /// Set the adjustment mode.
142    #[must_use]
143    pub fn with_mode(mut self, mode: FpsMode) -> Self {
144        self.mode = mode;
145        self
146    }
147
148    /// Enable or disable rounding.
149    #[must_use]
150    pub fn with_round(mut self, round: bool) -> Self {
151        self.round = round;
152        self
153    }
154
155    /// Set the start time offset.
156    #[must_use]
157    pub fn with_start_time(mut self, start_time: i64) -> Self {
158        self.start_time = start_time;
159        self
160    }
161
162    /// Set the EOF action.
163    #[must_use]
164    pub fn with_eof_action(mut self, action: EofAction) -> Self {
165        self.eof_action = action;
166        self
167    }
168
169    /// Get the target frame rate as a float.
170    #[must_use]
171    pub fn fps(&self) -> f64 {
172        self.fps_num as f64 / self.fps_den as f64
173    }
174
175    /// Get the frame duration in the given timebase.
176    #[must_use]
177    pub fn frame_duration(&self, timebase: Rational) -> i64 {
178        let duration_sec = self.fps_den as f64 / self.fps_num as f64;
179        let tb_rate = timebase.den as f64 / timebase.num as f64;
180        (duration_sec * tb_rate).round() as i64
181    }
182}
183
184/// End-of-stream action.
185#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
186pub enum EofAction {
187    /// Pass through remaining frames.
188    #[default]
189    Pass,
190    /// Repeat last frame until expected end.
191    Repeat,
192    /// Discard remaining frames.
193    Discard,
194}
195
196/// Convert a floating point frame rate to a rational approximation.
197fn rational_from_float(fps: f64) -> (u32, u32) {
198    const COMMON_RATES: [(f64, u32, u32); 10] = [
199        (23.976, 24000, 1001),
200        (24.0, 24, 1),
201        (25.0, 25, 1),
202        (29.97, 30000, 1001),
203        (30.0, 30, 1),
204        (50.0, 50, 1),
205        (59.94, 60000, 1001),
206        (60.0, 60, 1),
207        (120.0, 120, 1),
208        (144.0, 144, 1),
209    ];
210
211    // Check for common rates first
212    for (rate, num, den) in COMMON_RATES {
213        if (fps - rate).abs() < 0.01 {
214            return (num, den);
215        }
216    }
217
218    // Fall back to simple integer if close enough
219    let int_fps = fps.round() as u32;
220    if (fps - int_fps as f64).abs() < 0.01 {
221        return (int_fps, 1);
222    }
223
224    // Use continued fraction approximation
225    let (num, den) = continued_fraction(fps, 1000000);
226    (num as u32, den as u32)
227}
228
229/// Continued fraction approximation of a float.
230fn continued_fraction(value: f64, max_den: i64) -> (i64, i64) {
231    let mut n0 = 0i64;
232    let mut d0 = 1i64;
233    let mut n1 = 1i64;
234    let mut d1 = 0i64;
235
236    let mut x = value;
237    loop {
238        let a = x.floor() as i64;
239        let n2 = a * n1 + n0;
240        let d2 = a * d1 + d0;
241
242        if d2 > max_den {
243            break;
244        }
245
246        n0 = n1;
247        d0 = d1;
248        n1 = n2;
249        d1 = d2;
250
251        let rem = x - a as f64;
252        if rem.abs() < 1e-10 {
253            break;
254        }
255        x = 1.0 / rem;
256    }
257
258    (n1, d1)
259}
260
261/// Frame rate adjustment filter.
262///
263/// Adjusts video frame rate by dropping, duplicating, or blending frames.
264///
265/// # Example
266///
267/// ```ignore
268/// use oximedia_graph::filters::video::{FpsFilter, FpsConfig, FpsMode};
269/// use oximedia_graph::node::NodeId;
270///
271/// // Convert to 30 fps with blending
272/// let config = FpsConfig::fps_30()
273///     .with_mode(FpsMode::Blend);
274///
275/// let filter = FpsFilter::new(NodeId(0), "fps", config);
276/// ```
277pub struct FpsFilter {
278    id: NodeId,
279    name: String,
280    state: NodeState,
281    inputs: Vec<InputPort>,
282    outputs: Vec<OutputPort>,
283    config: FpsConfig,
284    /// Buffer of recent frames for interpolation.
285    frame_buffer: VecDeque<VideoFrame>,
286    /// Current output frame index.
287    output_frame_idx: u64,
288    /// Last input frame PTS.
289    last_input_pts: Option<i64>,
290    /// Input timebase.
291    input_timebase: Rational,
292    /// Frames dropped count.
293    frames_dropped: u64,
294    /// Frames duplicated count.
295    frames_duplicated: u64,
296}
297
298impl FpsFilter {
299    /// Create a new FPS filter.
300    #[must_use]
301    pub fn new(id: NodeId, name: impl Into<String>, config: FpsConfig) -> Self {
302        Self {
303            id,
304            name: name.into(),
305            state: NodeState::Idle,
306            inputs: vec![InputPort::new(PortId(0), "input", PortType::Video)
307                .with_format(PortFormat::Video(VideoPortFormat::any()))],
308            outputs: vec![OutputPort::new(PortId(0), "output", PortType::Video)
309                .with_format(PortFormat::Video(VideoPortFormat::any()))],
310            config,
311            frame_buffer: VecDeque::with_capacity(3),
312            output_frame_idx: 0,
313            last_input_pts: None,
314            input_timebase: Rational::new(1, 1000),
315            frames_dropped: 0,
316            frames_duplicated: 0,
317        }
318    }
319
320    /// Get the current configuration.
321    #[must_use]
322    pub fn config(&self) -> &FpsConfig {
323        &self.config
324    }
325
326    /// Get the number of frames dropped.
327    #[must_use]
328    pub fn frames_dropped(&self) -> u64 {
329        self.frames_dropped
330    }
331
332    /// Get the number of frames duplicated.
333    #[must_use]
334    pub fn frames_duplicated(&self) -> u64 {
335        self.frames_duplicated
336    }
337
338    /// Calculate the expected PTS for an output frame index.
339    fn expected_pts(&self, frame_idx: u64) -> i64 {
340        let frame_duration = self.config.frame_duration(self.input_timebase);
341        self.config.start_time + (frame_idx as i64 * frame_duration)
342    }
343
344    /// Find the nearest input frame to the target PTS.
345    fn find_nearest_frame(&self, target_pts: i64) -> Option<&VideoFrame> {
346        self.frame_buffer.iter().min_by_key(|f| {
347            let pts = f.timestamp.pts;
348            (pts - target_pts).abs()
349        })
350    }
351
352    /// Blend two frames together.
353    fn blend_frames(
354        &self,
355        frame1: &VideoFrame,
356        frame2: &VideoFrame,
357        blend_factor: f64,
358    ) -> VideoFrame {
359        let mut output = frame1.clone();
360
361        for (i, (p1, p2)) in frame1.planes.iter().zip(frame2.planes.iter()).enumerate() {
362            let (w, h) = frame1.plane_dimensions(i);
363            let size = (w * h) as usize;
364            let mut blended_data = vec![0u8; size];
365
366            for j in 0..size {
367                let v1 = p1.data.get(j).copied().unwrap_or(0) as f64;
368                let v2 = p2.data.get(j).copied().unwrap_or(0) as f64;
369                let blended = v1 * (1.0 - blend_factor) + v2 * blend_factor;
370                blended_data[j] = blended.round().clamp(0.0, 255.0) as u8;
371            }
372
373            output.planes[i] = Plane::new(blended_data, p1.stride);
374        }
375
376        output
377    }
378
379    /// Process a frame and potentially produce output.
380    fn process_frame(&mut self, input: VideoFrame) -> GraphResult<Vec<VideoFrame>> {
381        // Update input timebase
382        self.input_timebase = input.timestamp.timebase;
383
384        let input_pts = input.timestamp.pts;
385        self.frame_buffer.push_back(input);
386
387        // Keep only the last few frames for interpolation
388        while self.frame_buffer.len() > 3 {
389            self.frame_buffer.pop_front();
390        }
391
392        let mut output_frames = Vec::new();
393
394        // Generate output frames
395        loop {
396            let target_pts = self.expected_pts(self.output_frame_idx);
397
398            // Check if we have enough frames
399            if let Some(latest) = self.frame_buffer.back() {
400                if latest.timestamp.pts < target_pts && self.last_input_pts.is_none() {
401                    // Need more input frames
402                    break;
403                }
404            } else {
405                break;
406            }
407
408            match self.config.mode {
409                FpsMode::DropDuplicate | FpsMode::Drop | FpsMode::Duplicate => {
410                    if let Some(nearest) = self.find_nearest_frame(target_pts) {
411                        let nearest_pts = nearest.timestamp.pts;
412                        let frame_duration = self.config.frame_duration(self.input_timebase);
413
414                        // Check if we should output this frame
415                        let should_output = if self.config.round {
416                            (nearest_pts - target_pts).abs() <= frame_duration / 2
417                        } else {
418                            nearest_pts <= target_pts + frame_duration
419                        };
420
421                        if should_output {
422                            let mut output = nearest.clone();
423                            output.timestamp = Timestamp::new(target_pts, self.input_timebase);
424                            output_frames.push(output);
425
426                            // Track duplicates
427                            if let Some(last_pts) = self.last_input_pts {
428                                if nearest_pts == last_pts {
429                                    self.frames_duplicated += 1;
430                                }
431                            }
432                        } else if self.config.mode.allows_duplicate() {
433                            // Duplicate last frame
434                            if let Some(last) = self.frame_buffer.back() {
435                                let mut output = last.clone();
436                                output.timestamp = Timestamp::new(target_pts, self.input_timebase);
437                                output_frames.push(output);
438                                self.frames_duplicated += 1;
439                            }
440                        } else {
441                            self.frames_dropped += 1;
442                        }
443                    }
444                }
445                FpsMode::Blend => {
446                    // Find two frames to blend
447                    let mut prev_frame: Option<&VideoFrame> = None;
448                    let mut next_frame: Option<&VideoFrame> = None;
449
450                    for frame in &self.frame_buffer {
451                        if frame.timestamp.pts <= target_pts {
452                            prev_frame = Some(frame);
453                        }
454                        if frame.timestamp.pts >= target_pts && next_frame.is_none() {
455                            next_frame = Some(frame);
456                        }
457                    }
458
459                    match (prev_frame, next_frame) {
460                        (Some(prev), Some(next)) => {
461                            let prev_pts = prev.timestamp.pts;
462                            let next_pts = next.timestamp.pts;
463
464                            if prev_pts == next_pts {
465                                // Same frame, no blending needed
466                                let mut output = prev.clone();
467                                output.timestamp = Timestamp::new(target_pts, self.input_timebase);
468                                output_frames.push(output);
469                            } else {
470                                // Blend between frames
471                                let blend_factor =
472                                    (target_pts - prev_pts) as f64 / (next_pts - prev_pts) as f64;
473                                let blend_factor = blend_factor.clamp(0.0, 1.0);
474
475                                let mut blended = self.blend_frames(prev, next, blend_factor);
476                                blended.timestamp = Timestamp::new(target_pts, self.input_timebase);
477                                output_frames.push(blended);
478                            }
479                        }
480                        (Some(prev), None) => {
481                            // Only have previous frame, duplicate
482                            let mut output = prev.clone();
483                            output.timestamp = Timestamp::new(target_pts, self.input_timebase);
484                            output_frames.push(output);
485                            self.frames_duplicated += 1;
486                        }
487                        _ => {
488                            // Need more input
489                            break;
490                        }
491                    }
492                }
493                FpsMode::Vfr => {
494                    // Pass through with adjusted timestamp
495                    if let Some(frame) = self.frame_buffer.back() {
496                        let mut output = frame.clone();
497                        output.timestamp = Timestamp::new(target_pts, self.input_timebase);
498                        output_frames.push(output);
499                    }
500                }
501            }
502
503            self.output_frame_idx += 1;
504
505            // Limit output frames per call
506            if output_frames.len() >= 10 {
507                break;
508            }
509        }
510
511        self.last_input_pts = Some(input_pts);
512        Ok(output_frames)
513    }
514}
515
516impl Node for FpsFilter {
517    fn id(&self) -> NodeId {
518        self.id
519    }
520
521    fn name(&self) -> &str {
522        &self.name
523    }
524
525    fn node_type(&self) -> NodeType {
526        NodeType::Filter
527    }
528
529    fn state(&self) -> NodeState {
530        self.state
531    }
532
533    fn set_state(&mut self, state: NodeState) -> GraphResult<()> {
534        if !self.state.can_transition_to(state) {
535            return Err(GraphError::InvalidStateTransition {
536                node: self.id,
537                from: self.state.to_string(),
538                to: state.to_string(),
539            });
540        }
541        self.state = state;
542        Ok(())
543    }
544
545    fn inputs(&self) -> &[InputPort] {
546        &self.inputs
547    }
548
549    fn outputs(&self) -> &[OutputPort] {
550        &self.outputs
551    }
552
553    fn process(&mut self, input: Option<FilterFrame>) -> GraphResult<Option<FilterFrame>> {
554        match input {
555            Some(FilterFrame::Video(frame)) => {
556                let output_frames = self.process_frame(frame)?;
557                // Return the first output frame; additional frames would need buffering
558                Ok(output_frames.into_iter().next().map(FilterFrame::Video))
559            }
560            Some(_) => Err(GraphError::PortTypeMismatch {
561                expected: "Video".to_string(),
562                actual: "Audio".to_string(),
563            }),
564            None => Ok(None),
565        }
566    }
567
568    fn flush(&mut self) -> GraphResult<Vec<FilterFrame>> {
569        let mut output = Vec::new();
570
571        match self.config.eof_action {
572            EofAction::Pass => {
573                // Output remaining buffered frames
574                for frame in self.frame_buffer.drain(..) {
575                    output.push(FilterFrame::Video(frame));
576                }
577            }
578            EofAction::Repeat => {
579                // Repeat last frame until expected end
580                if let Some(last) = self.frame_buffer.back().cloned() {
581                    let target_pts = self.expected_pts(self.output_frame_idx);
582                    let mut frame = last;
583                    frame.timestamp = Timestamp::new(target_pts, self.input_timebase);
584                    output.push(FilterFrame::Video(frame));
585                }
586            }
587            EofAction::Discard => {
588                // Discard remaining frames
589                self.frame_buffer.clear();
590            }
591        }
592
593        Ok(output)
594    }
595
596    fn reset(&mut self) -> GraphResult<()> {
597        self.frame_buffer.clear();
598        self.output_frame_idx = 0;
599        self.last_input_pts = None;
600        self.frames_dropped = 0;
601        self.frames_duplicated = 0;
602        self.set_state(NodeState::Idle)
603    }
604}
605
606/// Calculate the frame rate from a video stream's timestamps.
607#[allow(dead_code)]
608pub struct FrameRateDetector {
609    /// Collected frame timestamps.
610    timestamps: Vec<i64>,
611    /// Detected frame rate (num, den).
612    detected_rate: Option<(u32, u32)>,
613    /// Minimum frames needed for detection.
614    min_frames: usize,
615}
616
617impl Default for FrameRateDetector {
618    fn default() -> Self {
619        Self {
620            timestamps: Vec::new(),
621            detected_rate: None,
622            min_frames: 10,
623        }
624    }
625}
626
627impl FrameRateDetector {
628    /// Create a new frame rate detector.
629    #[must_use]
630    pub fn new(min_frames: usize) -> Self {
631        Self {
632            timestamps: Vec::new(),
633            detected_rate: None,
634            min_frames,
635        }
636    }
637
638    /// Add a frame timestamp.
639    pub fn add_timestamp(&mut self, pts: i64) {
640        self.timestamps.push(pts);
641
642        if self.timestamps.len() >= self.min_frames && self.detected_rate.is_none() {
643            self.detect();
644        }
645    }
646
647    /// Detect the frame rate from collected timestamps.
648    fn detect(&mut self) {
649        if self.timestamps.len() < 2 {
650            return;
651        }
652
653        // Calculate average frame duration
654        let mut total_duration = 0i64;
655        for i in 1..self.timestamps.len() {
656            total_duration += self.timestamps[i] - self.timestamps[i - 1];
657        }
658
659        let avg_duration = total_duration as f64 / (self.timestamps.len() - 1) as f64;
660
661        // Assuming 1000 timebase (ms), convert to fps
662        let fps = 1000.0 / avg_duration;
663        let (num, den) = rational_from_float(fps);
664        self.detected_rate = Some((num, den));
665    }
666
667    /// Get the detected frame rate.
668    #[must_use]
669    pub fn frame_rate(&self) -> Option<(u32, u32)> {
670        self.detected_rate
671    }
672
673    /// Get the detected frame rate as a float.
674    #[must_use]
675    pub fn fps(&self) -> Option<f64> {
676        self.detected_rate.map(|(num, den)| num as f64 / den as f64)
677    }
678}
679
680#[cfg(test)]
681mod tests {
682    use super::*;
683
684    fn create_test_frame(pts: i64) -> VideoFrame {
685        use oximedia_core::PixelFormat;
686
687        let mut frame = VideoFrame::new(PixelFormat::Yuv420p, 64, 48);
688        frame.timestamp = Timestamp::new(pts, Rational::new(1, 1000));
689        frame.allocate();
690        frame
691    }
692
693    #[test]
694    fn test_fps_mode_properties() {
695        assert!(FpsMode::DropDuplicate.allows_drop());
696        assert!(FpsMode::DropDuplicate.allows_duplicate());
697        assert!(FpsMode::Drop.allows_drop());
698        assert!(!FpsMode::Drop.allows_duplicate());
699        assert!(!FpsMode::Duplicate.allows_drop());
700        assert!(FpsMode::Duplicate.allows_duplicate());
701    }
702
703    #[test]
704    fn test_fps_config_creation() {
705        let config = FpsConfig::new(30, 1);
706        assert_eq!(config.fps_num, 30);
707        assert_eq!(config.fps_den, 1);
708        assert!((config.fps() - 30.0).abs() < 0.001);
709    }
710
711    #[test]
712    fn test_fps_config_presets() {
713        assert!((FpsConfig::fps_24().fps() - 24.0).abs() < 0.001);
714        assert!((FpsConfig::fps_25().fps() - 25.0).abs() < 0.001);
715        assert!((FpsConfig::fps_30().fps() - 30.0).abs() < 0.001);
716        assert!((FpsConfig::fps_29_97().fps() - 29.97).abs() < 0.01);
717        assert!((FpsConfig::fps_60().fps() - 60.0).abs() < 0.001);
718        assert!((FpsConfig::fps_59_94().fps() - 59.94).abs() < 0.01);
719    }
720
721    #[test]
722    fn test_fps_config_from_rate() {
723        let config = FpsConfig::from_rate(23.976);
724        assert_eq!(config.fps_num, 24000);
725        assert_eq!(config.fps_den, 1001);
726
727        let config = FpsConfig::from_rate(30.0);
728        assert_eq!(config.fps_num, 30);
729        assert_eq!(config.fps_den, 1);
730    }
731
732    #[test]
733    fn test_fps_config_frame_duration() {
734        let config = FpsConfig::fps_30();
735        let duration = config.frame_duration(Rational::new(1, 1000));
736        // 30 fps = ~33.33ms per frame
737        assert!((duration - 33).abs() <= 1);
738    }
739
740    #[test]
741    fn test_rational_from_float() {
742        assert_eq!(rational_from_float(24.0), (24, 1));
743        assert_eq!(rational_from_float(29.97), (30000, 1001));
744        assert_eq!(rational_from_float(59.94), (60000, 1001));
745    }
746
747    #[test]
748    fn test_fps_filter_creation() {
749        let config = FpsConfig::fps_30();
750        let filter = FpsFilter::new(NodeId(0), "fps", config);
751
752        assert_eq!(filter.id(), NodeId(0));
753        assert_eq!(filter.name(), "fps");
754        assert_eq!(filter.node_type(), NodeType::Filter);
755    }
756
757    #[test]
758    fn test_fps_filter_process() {
759        let config = FpsConfig::fps_30().with_mode(FpsMode::DropDuplicate);
760        let mut filter = FpsFilter::new(NodeId(0), "fps", config);
761
762        // Process a few frames
763        for i in 0..5 {
764            let frame = create_test_frame(i * 40); // ~25 fps input
765            let _ = filter.process(Some(FilterFrame::Video(frame)));
766        }
767
768        // Check that we've processed frames
769        assert!(filter.output_frame_idx > 0);
770    }
771
772    #[test]
773    fn test_fps_filter_statistics() {
774        let config = FpsConfig::fps_30();
775        let filter = FpsFilter::new(NodeId(0), "fps", config);
776
777        assert_eq!(filter.frames_dropped(), 0);
778        assert_eq!(filter.frames_duplicated(), 0);
779    }
780
781    #[test]
782    fn test_fps_filter_reset() {
783        let config = FpsConfig::fps_30();
784        let mut filter = FpsFilter::new(NodeId(0), "fps", config);
785
786        // Process some frames
787        for i in 0..3 {
788            let frame = create_test_frame(i * 33);
789            let _ = filter.process(Some(FilterFrame::Video(frame)));
790        }
791
792        // Reset
793        filter.reset().expect("reset should succeed");
794
795        assert_eq!(filter.output_frame_idx, 0);
796        assert!(filter.frame_buffer.is_empty());
797    }
798
799    #[test]
800    fn test_fps_filter_flush() {
801        let config = FpsConfig::fps_30().with_eof_action(EofAction::Pass);
802        let mut filter = FpsFilter::new(NodeId(0), "fps", config);
803
804        // Add a frame to the buffer
805        let frame = create_test_frame(0);
806        let _ = filter.process(Some(FilterFrame::Video(frame)));
807
808        // Flush
809        let flushed = filter.flush().expect("flush should succeed");
810        assert!(!flushed.is_empty());
811    }
812
813    #[test]
814    fn test_frame_rate_detector() {
815        let mut detector = FrameRateDetector::new(5);
816
817        // Add timestamps at 30 fps (33.33ms interval)
818        for i in 0..10 {
819            detector.add_timestamp(i * 33);
820        }
821
822        let fps = detector.fps().expect("fps should succeed");
823        assert!((fps - 30.0).abs() < 1.0);
824    }
825
826    #[test]
827    fn test_node_state_transitions() {
828        let config = FpsConfig::fps_30();
829        let mut filter = FpsFilter::new(NodeId(0), "fps", config);
830
831        assert_eq!(filter.state(), NodeState::Idle);
832        filter
833            .set_state(NodeState::Processing)
834            .expect("set_state should succeed");
835        assert_eq!(filter.state(), NodeState::Processing);
836    }
837
838    #[test]
839    fn test_process_none_input() {
840        let config = FpsConfig::fps_30();
841        let mut filter = FpsFilter::new(NodeId(0), "fps", config);
842
843        let result = filter.process(None).expect("process should succeed");
844        assert!(result.is_none());
845    }
846
847    #[test]
848    fn test_continued_fraction() {
849        let (num, den) = continued_fraction(29.97, 10000);
850        let result = num as f64 / den as f64;
851        assert!((result - 29.97).abs() < 0.01);
852    }
853
854    #[test]
855    fn test_eof_actions() {
856        // Test Pass action
857        let config = FpsConfig::fps_30().with_eof_action(EofAction::Pass);
858        let mut filter = FpsFilter::new(NodeId(0), "fps", config);
859        let _ = filter.process(Some(FilterFrame::Video(create_test_frame(0))));
860        let flushed = filter.flush().expect("flush should succeed");
861        assert!(!flushed.is_empty());
862
863        // Test Discard action
864        let config = FpsConfig::fps_30().with_eof_action(EofAction::Discard);
865        let mut filter = FpsFilter::new(NodeId(0), "fps", config);
866        let _ = filter.process(Some(FilterFrame::Video(create_test_frame(0))));
867        let flushed = filter.flush().expect("flush should succeed");
868        assert!(flushed.is_empty());
869    }
870}