1#![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#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
39pub enum FpsMode {
40 #[default]
42 DropDuplicate,
43 Drop,
45 Duplicate,
47 Blend,
49 Vfr,
51}
52
53impl FpsMode {
54 #[must_use]
56 pub fn allows_drop(&self) -> bool {
57 matches!(self, Self::DropDuplicate | Self::Drop | Self::Blend)
58 }
59
60 #[must_use]
62 pub fn allows_duplicate(&self) -> bool {
63 matches!(self, Self::DropDuplicate | Self::Duplicate | Self::Blend)
64 }
65}
66
67#[derive(Clone, Debug)]
69pub struct FpsConfig {
70 pub fps_num: u32,
72 pub fps_den: u32,
74 pub mode: FpsMode,
76 pub round: bool,
78 pub start_time: i64,
80 pub eof_action: EofAction,
82}
83
84impl FpsConfig {
85 #[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 #[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 #[must_use]
107 pub fn fps_24() -> Self {
108 Self::new(24, 1)
109 }
110
111 #[must_use]
113 pub fn fps_25() -> Self {
114 Self::new(25, 1)
115 }
116
117 #[must_use]
119 pub fn fps_30() -> Self {
120 Self::new(30, 1)
121 }
122
123 #[must_use]
125 pub fn fps_29_97() -> Self {
126 Self::new(30000, 1001)
127 }
128
129 #[must_use]
131 pub fn fps_60() -> Self {
132 Self::new(60, 1)
133 }
134
135 #[must_use]
137 pub fn fps_59_94() -> Self {
138 Self::new(60000, 1001)
139 }
140
141 #[must_use]
143 pub fn with_mode(mut self, mode: FpsMode) -> Self {
144 self.mode = mode;
145 self
146 }
147
148 #[must_use]
150 pub fn with_round(mut self, round: bool) -> Self {
151 self.round = round;
152 self
153 }
154
155 #[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 #[must_use]
164 pub fn with_eof_action(mut self, action: EofAction) -> Self {
165 self.eof_action = action;
166 self
167 }
168
169 #[must_use]
171 pub fn fps(&self) -> f64 {
172 self.fps_num as f64 / self.fps_den as f64
173 }
174
175 #[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#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
186pub enum EofAction {
187 #[default]
189 Pass,
190 Repeat,
192 Discard,
194}
195
196fn 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 for (rate, num, den) in COMMON_RATES {
213 if (fps - rate).abs() < 0.01 {
214 return (num, den);
215 }
216 }
217
218 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 let (num, den) = continued_fraction(fps, 1000000);
226 (num as u32, den as u32)
227}
228
229fn 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
261pub struct FpsFilter {
278 id: NodeId,
279 name: String,
280 state: NodeState,
281 inputs: Vec<InputPort>,
282 outputs: Vec<OutputPort>,
283 config: FpsConfig,
284 frame_buffer: VecDeque<VideoFrame>,
286 output_frame_idx: u64,
288 last_input_pts: Option<i64>,
290 input_timebase: Rational,
292 frames_dropped: u64,
294 frames_duplicated: u64,
296}
297
298impl FpsFilter {
299 #[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 #[must_use]
322 pub fn config(&self) -> &FpsConfig {
323 &self.config
324 }
325
326 #[must_use]
328 pub fn frames_dropped(&self) -> u64 {
329 self.frames_dropped
330 }
331
332 #[must_use]
334 pub fn frames_duplicated(&self) -> u64 {
335 self.frames_duplicated
336 }
337
338 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 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 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 fn process_frame(&mut self, input: VideoFrame) -> GraphResult<Vec<VideoFrame>> {
381 self.input_timebase = input.timestamp.timebase;
383
384 let input_pts = input.timestamp.pts;
385 self.frame_buffer.push_back(input);
386
387 while self.frame_buffer.len() > 3 {
389 self.frame_buffer.pop_front();
390 }
391
392 let mut output_frames = Vec::new();
393
394 loop {
396 let target_pts = self.expected_pts(self.output_frame_idx);
397
398 if let Some(latest) = self.frame_buffer.back() {
400 if latest.timestamp.pts < target_pts && self.last_input_pts.is_none() {
401 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 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 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 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 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 let mut output = prev.clone();
467 output.timestamp = Timestamp::new(target_pts, self.input_timebase);
468 output_frames.push(output);
469 } else {
470 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 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 break;
490 }
491 }
492 }
493 FpsMode::Vfr => {
494 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 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 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 for frame in self.frame_buffer.drain(..) {
575 output.push(FilterFrame::Video(frame));
576 }
577 }
578 EofAction::Repeat => {
579 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 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#[allow(dead_code)]
608pub struct FrameRateDetector {
609 timestamps: Vec<i64>,
611 detected_rate: Option<(u32, u32)>,
613 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 #[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 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 fn detect(&mut self) {
649 if self.timestamps.len() < 2 {
650 return;
651 }
652
653 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 let fps = 1000.0 / avg_duration;
663 let (num, den) = rational_from_float(fps);
664 self.detected_rate = Some((num, den));
665 }
666
667 #[must_use]
669 pub fn frame_rate(&self) -> Option<(u32, u32)> {
670 self.detected_rate
671 }
672
673 #[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 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 for i in 0..5 {
764 let frame = create_test_frame(i * 40); let _ = filter.process(Some(FilterFrame::Video(frame)));
766 }
767
768 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 for i in 0..3 {
788 let frame = create_test_frame(i * 33);
789 let _ = filter.process(Some(FilterFrame::Video(frame)));
790 }
791
792 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 let frame = create_test_frame(0);
806 let _ = filter.process(Some(FilterFrame::Video(frame)));
807
808 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 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 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 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}