Skip to main content

oximedia_graph/filters/video/
null.rs

1//! Null sink for video frames.
2//!
3//! This sink discards all incoming frames. It's useful for benchmarking
4//! encoder/decoder performance without the overhead of writing to disk.
5
6use crate::error::{GraphError, GraphResult};
7use crate::frame::FilterFrame;
8use crate::node::{Node, NodeId, NodeState, NodeType};
9use crate::port::{InputPort, OutputPort, PortFormat, PortId, PortType, VideoPortFormat};
10
11/// A sink that discards all incoming video frames.
12///
13/// This is useful for:
14/// - Benchmarking without I/O overhead
15/// - Testing graph connectivity
16/// - Counting frames processed
17pub struct NullSink {
18    id: NodeId,
19    name: String,
20    state: NodeState,
21    inputs: Vec<InputPort>,
22    outputs: Vec<OutputPort>,
23    frames_received: u64,
24}
25
26impl NullSink {
27    /// Create a new null sink.
28    #[must_use]
29    pub fn new(id: NodeId, name: impl Into<String>) -> Self {
30        let video_format = PortFormat::Video(VideoPortFormat::any());
31
32        Self {
33            id,
34            name: name.into(),
35            state: NodeState::Idle,
36            inputs: vec![
37                InputPort::new(PortId(0), "input", PortType::Video).with_format(video_format)
38            ],
39            outputs: Vec::new(),
40            frames_received: 0,
41        }
42    }
43
44    /// Get the number of frames received.
45    #[must_use]
46    pub fn frames_received(&self) -> u64 {
47        self.frames_received
48    }
49
50    /// Reset the frame counter.
51    pub fn reset_counter(&mut self) {
52        self.frames_received = 0;
53    }
54}
55
56impl Node for NullSink {
57    fn id(&self) -> NodeId {
58        self.id
59    }
60
61    fn name(&self) -> &str {
62        &self.name
63    }
64
65    fn node_type(&self) -> NodeType {
66        NodeType::Sink
67    }
68
69    fn state(&self) -> NodeState {
70        self.state
71    }
72
73    fn set_state(&mut self, state: NodeState) -> GraphResult<()> {
74        if !self.state.can_transition_to(state) {
75            return Err(GraphError::InvalidStateTransition {
76                node: self.id,
77                from: self.state.to_string(),
78                to: state.to_string(),
79            });
80        }
81        self.state = state;
82        Ok(())
83    }
84
85    fn inputs(&self) -> &[InputPort] {
86        &self.inputs
87    }
88
89    fn outputs(&self) -> &[OutputPort] {
90        &self.outputs
91    }
92
93    fn process(&mut self, input: Option<FilterFrame>) -> GraphResult<Option<FilterFrame>> {
94        if let Some(frame) = input {
95            if frame.is_video() {
96                self.frames_received += 1;
97            }
98        }
99        // Sink produces no output
100        Ok(None)
101    }
102
103    fn reset(&mut self) -> GraphResult<()> {
104        self.frames_received = 0;
105        self.set_state(NodeState::Idle)
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use oximedia_codec::VideoFrame;
113    use oximedia_core::PixelFormat;
114
115    #[test]
116    fn test_null_sink_creation() {
117        let sink = NullSink::new(NodeId(0), "null");
118
119        assert_eq!(sink.id(), NodeId(0));
120        assert_eq!(sink.name(), "null");
121        assert_eq!(sink.node_type(), NodeType::Sink);
122        assert_eq!(sink.frames_received(), 0);
123    }
124
125    #[test]
126    fn test_null_sink_ports() {
127        let sink = NullSink::new(NodeId(0), "null");
128
129        // Should have one input, no outputs
130        assert_eq!(sink.inputs().len(), 1);
131        assert_eq!(sink.outputs().len(), 0);
132    }
133
134    #[test]
135    fn test_null_sink_process() {
136        let mut sink = NullSink::new(NodeId(0), "null");
137
138        // Process some frames
139        for _ in 0..5 {
140            let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
141            let frame = FilterFrame::Video(video);
142            let result = sink.process(Some(frame)).expect("process should succeed");
143            // Sink should produce no output
144            assert!(result.is_none());
145        }
146
147        assert_eq!(sink.frames_received(), 5);
148    }
149
150    #[test]
151    fn test_null_sink_reset() {
152        let mut sink = NullSink::new(NodeId(0), "null");
153
154        // Process a frame
155        let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
156        sink.process(Some(FilterFrame::Video(video)))
157            .expect("operation should succeed");
158        assert_eq!(sink.frames_received(), 1);
159
160        // Reset
161        sink.reset().expect("reset should succeed");
162        assert_eq!(sink.frames_received(), 0);
163        assert_eq!(sink.state(), NodeState::Idle);
164    }
165
166    #[test]
167    fn test_null_sink_no_input() {
168        let mut sink = NullSink::new(NodeId(0), "null");
169
170        let result = sink.process(None).expect("process should succeed");
171        assert!(result.is_none());
172        assert_eq!(sink.frames_received(), 0);
173    }
174}