#![forbid(unsafe_code)]
#![allow(clippy::cast_lossless)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_possible_wrap)]
#![allow(clippy::similar_names)]
#![allow(clippy::many_single_char_names)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::match_same_arms)]
#![allow(clippy::doc_markdown)]
#![allow(clippy::unused_self)]
#![allow(clippy::unnecessary_cast)]
#![allow(clippy::bool_to_int_with_if)]
#![allow(clippy::needless_range_loop)]
#![allow(clippy::too_many_lines)]
#![allow(clippy::unnecessary_wraps)]
#![allow(clippy::map_unwrap_or)]
#![allow(clippy::no_effect_underscore_binding)]
#![allow(clippy::unreadable_literal)]
#![allow(dead_code)]
use std::collections::VecDeque;
use crate::error::{GraphError, GraphResult};
use crate::frame::FilterFrame;
use crate::node::{Node, NodeId, NodeState, NodeType};
use crate::port::{InputPort, OutputPort, PortFormat, PortId, PortType, VideoPortFormat};
use oximedia_codec::{Plane, VideoFrame};
use oximedia_core::{Rational, Timestamp};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum FpsMode {
#[default]
DropDuplicate,
Drop,
Duplicate,
Blend,
Vfr,
}
impl FpsMode {
#[must_use]
pub fn allows_drop(&self) -> bool {
matches!(self, Self::DropDuplicate | Self::Drop | Self::Blend)
}
#[must_use]
pub fn allows_duplicate(&self) -> bool {
matches!(self, Self::DropDuplicate | Self::Duplicate | Self::Blend)
}
}
#[derive(Clone, Debug)]
pub struct FpsConfig {
pub fps_num: u32,
pub fps_den: u32,
pub mode: FpsMode,
pub round: bool,
pub start_time: i64,
pub eof_action: EofAction,
}
impl FpsConfig {
#[must_use]
pub fn new(fps_num: u32, fps_den: u32) -> Self {
Self {
fps_num,
fps_den,
mode: FpsMode::default(),
round: true,
start_time: 0,
eof_action: EofAction::Pass,
}
}
#[must_use]
pub fn from_rate(fps: f64) -> Self {
let (num, den) = rational_from_float(fps);
Self::new(num, den)
}
#[must_use]
pub fn fps_24() -> Self {
Self::new(24, 1)
}
#[must_use]
pub fn fps_25() -> Self {
Self::new(25, 1)
}
#[must_use]
pub fn fps_30() -> Self {
Self::new(30, 1)
}
#[must_use]
pub fn fps_29_97() -> Self {
Self::new(30000, 1001)
}
#[must_use]
pub fn fps_60() -> Self {
Self::new(60, 1)
}
#[must_use]
pub fn fps_59_94() -> Self {
Self::new(60000, 1001)
}
#[must_use]
pub fn with_mode(mut self, mode: FpsMode) -> Self {
self.mode = mode;
self
}
#[must_use]
pub fn with_round(mut self, round: bool) -> Self {
self.round = round;
self
}
#[must_use]
pub fn with_start_time(mut self, start_time: i64) -> Self {
self.start_time = start_time;
self
}
#[must_use]
pub fn with_eof_action(mut self, action: EofAction) -> Self {
self.eof_action = action;
self
}
#[must_use]
pub fn fps(&self) -> f64 {
self.fps_num as f64 / self.fps_den as f64
}
#[must_use]
pub fn frame_duration(&self, timebase: Rational) -> i64 {
let duration_sec = self.fps_den as f64 / self.fps_num as f64;
let tb_rate = timebase.den as f64 / timebase.num as f64;
(duration_sec * tb_rate).round() as i64
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum EofAction {
#[default]
Pass,
Repeat,
Discard,
}
fn rational_from_float(fps: f64) -> (u32, u32) {
const COMMON_RATES: [(f64, u32, u32); 10] = [
(23.976, 24000, 1001),
(24.0, 24, 1),
(25.0, 25, 1),
(29.97, 30000, 1001),
(30.0, 30, 1),
(50.0, 50, 1),
(59.94, 60000, 1001),
(60.0, 60, 1),
(120.0, 120, 1),
(144.0, 144, 1),
];
for (rate, num, den) in COMMON_RATES {
if (fps - rate).abs() < 0.01 {
return (num, den);
}
}
let int_fps = fps.round() as u32;
if (fps - int_fps as f64).abs() < 0.01 {
return (int_fps, 1);
}
let (num, den) = continued_fraction(fps, 1000000);
(num as u32, den as u32)
}
fn continued_fraction(value: f64, max_den: i64) -> (i64, i64) {
let mut n0 = 0i64;
let mut d0 = 1i64;
let mut n1 = 1i64;
let mut d1 = 0i64;
let mut x = value;
loop {
let a = x.floor() as i64;
let n2 = a * n1 + n0;
let d2 = a * d1 + d0;
if d2 > max_den {
break;
}
n0 = n1;
d0 = d1;
n1 = n2;
d1 = d2;
let rem = x - a as f64;
if rem.abs() < 1e-10 {
break;
}
x = 1.0 / rem;
}
(n1, d1)
}
pub struct FpsFilter {
id: NodeId,
name: String,
state: NodeState,
inputs: Vec<InputPort>,
outputs: Vec<OutputPort>,
config: FpsConfig,
frame_buffer: VecDeque<VideoFrame>,
output_frame_idx: u64,
last_input_pts: Option<i64>,
input_timebase: Rational,
frames_dropped: u64,
frames_duplicated: u64,
}
impl FpsFilter {
#[must_use]
pub fn new(id: NodeId, name: impl Into<String>, config: FpsConfig) -> Self {
Self {
id,
name: name.into(),
state: NodeState::Idle,
inputs: vec![InputPort::new(PortId(0), "input", PortType::Video)
.with_format(PortFormat::Video(VideoPortFormat::any()))],
outputs: vec![OutputPort::new(PortId(0), "output", PortType::Video)
.with_format(PortFormat::Video(VideoPortFormat::any()))],
config,
frame_buffer: VecDeque::with_capacity(3),
output_frame_idx: 0,
last_input_pts: None,
input_timebase: Rational::new(1, 1000),
frames_dropped: 0,
frames_duplicated: 0,
}
}
#[must_use]
pub fn config(&self) -> &FpsConfig {
&self.config
}
#[must_use]
pub fn frames_dropped(&self) -> u64 {
self.frames_dropped
}
#[must_use]
pub fn frames_duplicated(&self) -> u64 {
self.frames_duplicated
}
fn expected_pts(&self, frame_idx: u64) -> i64 {
let frame_duration = self.config.frame_duration(self.input_timebase);
self.config.start_time + (frame_idx as i64 * frame_duration)
}
fn find_nearest_frame(&self, target_pts: i64) -> Option<&VideoFrame> {
self.frame_buffer.iter().min_by_key(|f| {
let pts = f.timestamp.pts;
(pts - target_pts).abs()
})
}
fn blend_frames(
&self,
frame1: &VideoFrame,
frame2: &VideoFrame,
blend_factor: f64,
) -> VideoFrame {
let mut output = frame1.clone();
for (i, (p1, p2)) in frame1.planes.iter().zip(frame2.planes.iter()).enumerate() {
let (w, h) = frame1.plane_dimensions(i);
let size = (w * h) as usize;
let mut blended_data = vec![0u8; size];
for j in 0..size {
let v1 = p1.data.get(j).copied().unwrap_or(0) as f64;
let v2 = p2.data.get(j).copied().unwrap_or(0) as f64;
let blended = v1 * (1.0 - blend_factor) + v2 * blend_factor;
blended_data[j] = blended.round().clamp(0.0, 255.0) as u8;
}
output.planes[i] = Plane::new(blended_data, p1.stride);
}
output
}
fn process_frame(&mut self, input: VideoFrame) -> GraphResult<Vec<VideoFrame>> {
self.input_timebase = input.timestamp.timebase;
let input_pts = input.timestamp.pts;
self.frame_buffer.push_back(input);
while self.frame_buffer.len() > 3 {
self.frame_buffer.pop_front();
}
let mut output_frames = Vec::new();
loop {
let target_pts = self.expected_pts(self.output_frame_idx);
if let Some(latest) = self.frame_buffer.back() {
if latest.timestamp.pts < target_pts && self.last_input_pts.is_none() {
break;
}
} else {
break;
}
match self.config.mode {
FpsMode::DropDuplicate | FpsMode::Drop | FpsMode::Duplicate => {
if let Some(nearest) = self.find_nearest_frame(target_pts) {
let nearest_pts = nearest.timestamp.pts;
let frame_duration = self.config.frame_duration(self.input_timebase);
let should_output = if self.config.round {
(nearest_pts - target_pts).abs() <= frame_duration / 2
} else {
nearest_pts <= target_pts + frame_duration
};
if should_output {
let mut output = nearest.clone();
output.timestamp = Timestamp::new(target_pts, self.input_timebase);
output_frames.push(output);
if let Some(last_pts) = self.last_input_pts {
if nearest_pts == last_pts {
self.frames_duplicated += 1;
}
}
} else if self.config.mode.allows_duplicate() {
if let Some(last) = self.frame_buffer.back() {
let mut output = last.clone();
output.timestamp = Timestamp::new(target_pts, self.input_timebase);
output_frames.push(output);
self.frames_duplicated += 1;
}
} else {
self.frames_dropped += 1;
}
}
}
FpsMode::Blend => {
let mut prev_frame: Option<&VideoFrame> = None;
let mut next_frame: Option<&VideoFrame> = None;
for frame in &self.frame_buffer {
if frame.timestamp.pts <= target_pts {
prev_frame = Some(frame);
}
if frame.timestamp.pts >= target_pts && next_frame.is_none() {
next_frame = Some(frame);
}
}
match (prev_frame, next_frame) {
(Some(prev), Some(next)) => {
let prev_pts = prev.timestamp.pts;
let next_pts = next.timestamp.pts;
if prev_pts == next_pts {
let mut output = prev.clone();
output.timestamp = Timestamp::new(target_pts, self.input_timebase);
output_frames.push(output);
} else {
let blend_factor =
(target_pts - prev_pts) as f64 / (next_pts - prev_pts) as f64;
let blend_factor = blend_factor.clamp(0.0, 1.0);
let mut blended = self.blend_frames(prev, next, blend_factor);
blended.timestamp = Timestamp::new(target_pts, self.input_timebase);
output_frames.push(blended);
}
}
(Some(prev), None) => {
let mut output = prev.clone();
output.timestamp = Timestamp::new(target_pts, self.input_timebase);
output_frames.push(output);
self.frames_duplicated += 1;
}
_ => {
break;
}
}
}
FpsMode::Vfr => {
if let Some(frame) = self.frame_buffer.back() {
let mut output = frame.clone();
output.timestamp = Timestamp::new(target_pts, self.input_timebase);
output_frames.push(output);
}
}
}
self.output_frame_idx += 1;
if output_frames.len() >= 10 {
break;
}
}
self.last_input_pts = Some(input_pts);
Ok(output_frames)
}
}
impl Node for FpsFilter {
fn id(&self) -> NodeId {
self.id
}
fn name(&self) -> &str {
&self.name
}
fn node_type(&self) -> NodeType {
NodeType::Filter
}
fn state(&self) -> NodeState {
self.state
}
fn set_state(&mut self, state: NodeState) -> GraphResult<()> {
if !self.state.can_transition_to(state) {
return Err(GraphError::InvalidStateTransition {
node: self.id,
from: self.state.to_string(),
to: state.to_string(),
});
}
self.state = state;
Ok(())
}
fn inputs(&self) -> &[InputPort] {
&self.inputs
}
fn outputs(&self) -> &[OutputPort] {
&self.outputs
}
fn process(&mut self, input: Option<FilterFrame>) -> GraphResult<Option<FilterFrame>> {
match input {
Some(FilterFrame::Video(frame)) => {
let output_frames = self.process_frame(frame)?;
Ok(output_frames.into_iter().next().map(FilterFrame::Video))
}
Some(_) => Err(GraphError::PortTypeMismatch {
expected: "Video".to_string(),
actual: "Audio".to_string(),
}),
None => Ok(None),
}
}
fn flush(&mut self) -> GraphResult<Vec<FilterFrame>> {
let mut output = Vec::new();
match self.config.eof_action {
EofAction::Pass => {
for frame in self.frame_buffer.drain(..) {
output.push(FilterFrame::Video(frame));
}
}
EofAction::Repeat => {
if let Some(last) = self.frame_buffer.back().cloned() {
let target_pts = self.expected_pts(self.output_frame_idx);
let mut frame = last;
frame.timestamp = Timestamp::new(target_pts, self.input_timebase);
output.push(FilterFrame::Video(frame));
}
}
EofAction::Discard => {
self.frame_buffer.clear();
}
}
Ok(output)
}
fn reset(&mut self) -> GraphResult<()> {
self.frame_buffer.clear();
self.output_frame_idx = 0;
self.last_input_pts = None;
self.frames_dropped = 0;
self.frames_duplicated = 0;
self.set_state(NodeState::Idle)
}
}
#[allow(dead_code)]
pub struct FrameRateDetector {
timestamps: Vec<i64>,
detected_rate: Option<(u32, u32)>,
min_frames: usize,
}
impl Default for FrameRateDetector {
fn default() -> Self {
Self {
timestamps: Vec::new(),
detected_rate: None,
min_frames: 10,
}
}
}
impl FrameRateDetector {
#[must_use]
pub fn new(min_frames: usize) -> Self {
Self {
timestamps: Vec::new(),
detected_rate: None,
min_frames,
}
}
pub fn add_timestamp(&mut self, pts: i64) {
self.timestamps.push(pts);
if self.timestamps.len() >= self.min_frames && self.detected_rate.is_none() {
self.detect();
}
}
fn detect(&mut self) {
if self.timestamps.len() < 2 {
return;
}
let mut total_duration = 0i64;
for i in 1..self.timestamps.len() {
total_duration += self.timestamps[i] - self.timestamps[i - 1];
}
let avg_duration = total_duration as f64 / (self.timestamps.len() - 1) as f64;
let fps = 1000.0 / avg_duration;
let (num, den) = rational_from_float(fps);
self.detected_rate = Some((num, den));
}
#[must_use]
pub fn frame_rate(&self) -> Option<(u32, u32)> {
self.detected_rate
}
#[must_use]
pub fn fps(&self) -> Option<f64> {
self.detected_rate.map(|(num, den)| num as f64 / den as f64)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_frame(pts: i64) -> VideoFrame {
use oximedia_core::PixelFormat;
let mut frame = VideoFrame::new(PixelFormat::Yuv420p, 64, 48);
frame.timestamp = Timestamp::new(pts, Rational::new(1, 1000));
frame.allocate();
frame
}
#[test]
fn test_fps_mode_properties() {
assert!(FpsMode::DropDuplicate.allows_drop());
assert!(FpsMode::DropDuplicate.allows_duplicate());
assert!(FpsMode::Drop.allows_drop());
assert!(!FpsMode::Drop.allows_duplicate());
assert!(!FpsMode::Duplicate.allows_drop());
assert!(FpsMode::Duplicate.allows_duplicate());
}
#[test]
fn test_fps_config_creation() {
let config = FpsConfig::new(30, 1);
assert_eq!(config.fps_num, 30);
assert_eq!(config.fps_den, 1);
assert!((config.fps() - 30.0).abs() < 0.001);
}
#[test]
fn test_fps_config_presets() {
assert!((FpsConfig::fps_24().fps() - 24.0).abs() < 0.001);
assert!((FpsConfig::fps_25().fps() - 25.0).abs() < 0.001);
assert!((FpsConfig::fps_30().fps() - 30.0).abs() < 0.001);
assert!((FpsConfig::fps_29_97().fps() - 29.97).abs() < 0.01);
assert!((FpsConfig::fps_60().fps() - 60.0).abs() < 0.001);
assert!((FpsConfig::fps_59_94().fps() - 59.94).abs() < 0.01);
}
#[test]
fn test_fps_config_from_rate() {
let config = FpsConfig::from_rate(23.976);
assert_eq!(config.fps_num, 24000);
assert_eq!(config.fps_den, 1001);
let config = FpsConfig::from_rate(30.0);
assert_eq!(config.fps_num, 30);
assert_eq!(config.fps_den, 1);
}
#[test]
fn test_fps_config_frame_duration() {
let config = FpsConfig::fps_30();
let duration = config.frame_duration(Rational::new(1, 1000));
assert!((duration - 33).abs() <= 1);
}
#[test]
fn test_rational_from_float() {
assert_eq!(rational_from_float(24.0), (24, 1));
assert_eq!(rational_from_float(29.97), (30000, 1001));
assert_eq!(rational_from_float(59.94), (60000, 1001));
}
#[test]
fn test_fps_filter_creation() {
let config = FpsConfig::fps_30();
let filter = FpsFilter::new(NodeId(0), "fps", config);
assert_eq!(filter.id(), NodeId(0));
assert_eq!(filter.name(), "fps");
assert_eq!(filter.node_type(), NodeType::Filter);
}
#[test]
fn test_fps_filter_process() {
let config = FpsConfig::fps_30().with_mode(FpsMode::DropDuplicate);
let mut filter = FpsFilter::new(NodeId(0), "fps", config);
for i in 0..5 {
let frame = create_test_frame(i * 40); let _ = filter.process(Some(FilterFrame::Video(frame)));
}
assert!(filter.output_frame_idx > 0);
}
#[test]
fn test_fps_filter_statistics() {
let config = FpsConfig::fps_30();
let filter = FpsFilter::new(NodeId(0), "fps", config);
assert_eq!(filter.frames_dropped(), 0);
assert_eq!(filter.frames_duplicated(), 0);
}
#[test]
fn test_fps_filter_reset() {
let config = FpsConfig::fps_30();
let mut filter = FpsFilter::new(NodeId(0), "fps", config);
for i in 0..3 {
let frame = create_test_frame(i * 33);
let _ = filter.process(Some(FilterFrame::Video(frame)));
}
filter.reset().expect("reset should succeed");
assert_eq!(filter.output_frame_idx, 0);
assert!(filter.frame_buffer.is_empty());
}
#[test]
fn test_fps_filter_flush() {
let config = FpsConfig::fps_30().with_eof_action(EofAction::Pass);
let mut filter = FpsFilter::new(NodeId(0), "fps", config);
let frame = create_test_frame(0);
let _ = filter.process(Some(FilterFrame::Video(frame)));
let flushed = filter.flush().expect("flush should succeed");
assert!(!flushed.is_empty());
}
#[test]
fn test_frame_rate_detector() {
let mut detector = FrameRateDetector::new(5);
for i in 0..10 {
detector.add_timestamp(i * 33);
}
let fps = detector.fps().expect("fps should succeed");
assert!((fps - 30.0).abs() < 1.0);
}
#[test]
fn test_node_state_transitions() {
let config = FpsConfig::fps_30();
let mut filter = FpsFilter::new(NodeId(0), "fps", config);
assert_eq!(filter.state(), NodeState::Idle);
filter
.set_state(NodeState::Processing)
.expect("set_state should succeed");
assert_eq!(filter.state(), NodeState::Processing);
}
#[test]
fn test_process_none_input() {
let config = FpsConfig::fps_30();
let mut filter = FpsFilter::new(NodeId(0), "fps", config);
let result = filter.process(None).expect("process should succeed");
assert!(result.is_none());
}
#[test]
fn test_continued_fraction() {
let (num, den) = continued_fraction(29.97, 10000);
let result = num as f64 / den as f64;
assert!((result - 29.97).abs() < 0.01);
}
#[test]
fn test_eof_actions() {
let config = FpsConfig::fps_30().with_eof_action(EofAction::Pass);
let mut filter = FpsFilter::new(NodeId(0), "fps", config);
let _ = filter.process(Some(FilterFrame::Video(create_test_frame(0))));
let flushed = filter.flush().expect("flush should succeed");
assert!(!flushed.is_empty());
let config = FpsConfig::fps_30().with_eof_action(EofAction::Discard);
let mut filter = FpsFilter::new(NodeId(0), "fps", config);
let _ = filter.process(Some(FilterFrame::Video(create_test_frame(0))));
let flushed = filter.flush().expect("flush should succeed");
assert!(flushed.is_empty());
}
}