Skip to main content

oximedia_graph/filters/video/
passthrough.rs

1//! Passthrough video filter.
2//!
3//! This filter simply passes video frames through unchanged. It's useful for
4//! testing and as a template for more complex filters.
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 video filter that passes frames through unchanged.
12///
13/// This filter is useful for:
14/// - Testing graph connectivity
15/// - Serving as a template for custom filters
16/// - Acting as a source node when configured as such
17pub struct PassthroughFilter {
18    id: NodeId,
19    name: String,
20    state: NodeState,
21    node_type: NodeType,
22    inputs: Vec<InputPort>,
23    outputs: Vec<OutputPort>,
24}
25
26impl PassthroughFilter {
27    /// Create a new passthrough filter.
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            node_type: NodeType::Filter,
37            inputs: vec![InputPort::new(PortId(0), "input", PortType::Video)
38                .with_format(video_format.clone())],
39            outputs: vec![
40                OutputPort::new(PortId(0), "output", PortType::Video).with_format(video_format)
41            ],
42        }
43    }
44
45    /// Create a passthrough filter configured as a source node.
46    ///
47    /// Source nodes have no required inputs and are entry points to the graph.
48    #[must_use]
49    pub fn new_source(id: NodeId, name: impl Into<String>) -> Self {
50        let video_format = PortFormat::Video(VideoPortFormat::any());
51
52        Self {
53            id,
54            name: name.into(),
55            state: NodeState::Idle,
56            node_type: NodeType::Source,
57            inputs: vec![InputPort::new(PortId(0), "input", PortType::Video)
58                .with_format(video_format.clone())
59                .optional()],
60            outputs: vec![
61                OutputPort::new(PortId(0), "output", PortType::Video).with_format(video_format)
62            ],
63        }
64    }
65}
66
67impl Node for PassthroughFilter {
68    fn id(&self) -> NodeId {
69        self.id
70    }
71
72    fn name(&self) -> &str {
73        &self.name
74    }
75
76    fn node_type(&self) -> NodeType {
77        self.node_type
78    }
79
80    fn state(&self) -> NodeState {
81        self.state
82    }
83
84    fn set_state(&mut self, state: NodeState) -> GraphResult<()> {
85        if !self.state.can_transition_to(state) {
86            return Err(GraphError::InvalidStateTransition {
87                node: self.id,
88                from: self.state.to_string(),
89                to: state.to_string(),
90            });
91        }
92        self.state = state;
93        Ok(())
94    }
95
96    fn inputs(&self) -> &[InputPort] {
97        &self.inputs
98    }
99
100    fn outputs(&self) -> &[OutputPort] {
101        &self.outputs
102    }
103
104    fn process(&mut self, input: Option<FilterFrame>) -> GraphResult<Option<FilterFrame>> {
105        // Passthrough: simply return the input
106        match input {
107            Some(frame) if frame.is_video() => Ok(Some(frame)),
108            Some(_) => Err(GraphError::PortTypeMismatch {
109                expected: "Video".to_string(),
110                actual: "Audio".to_string(),
111            }),
112            None => Ok(None),
113        }
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use oximedia_codec::VideoFrame;
121    use oximedia_core::PixelFormat;
122
123    #[test]
124    fn test_passthrough_creation() {
125        let filter = PassthroughFilter::new(NodeId(42), "test_filter");
126
127        assert_eq!(filter.id(), NodeId(42));
128        assert_eq!(filter.name(), "test_filter");
129        assert_eq!(filter.node_type(), NodeType::Filter);
130        assert_eq!(filter.state(), NodeState::Idle);
131    }
132
133    #[test]
134    fn test_passthrough_source() {
135        let filter = PassthroughFilter::new_source(NodeId(0), "source");
136
137        assert_eq!(filter.node_type(), NodeType::Source);
138        assert!(!filter.inputs()[0].required);
139    }
140
141    #[test]
142    fn test_passthrough_ports() {
143        let filter = PassthroughFilter::new(NodeId(0), "test");
144
145        assert_eq!(filter.inputs().len(), 1);
146        assert_eq!(filter.outputs().len(), 1);
147
148        let input = &filter.inputs()[0];
149        assert_eq!(input.port_type, PortType::Video);
150        assert!(input.required);
151
152        let output = &filter.outputs()[0];
153        assert_eq!(output.port_type, PortType::Video);
154    }
155
156    #[test]
157    fn test_passthrough_process() {
158        let mut filter = PassthroughFilter::new(NodeId(0), "test");
159
160        // Create a test frame
161        let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
162        let frame = FilterFrame::Video(video);
163
164        // Process should pass through
165        let result = filter.process(Some(frame)).expect("process should succeed");
166        assert!(result.is_some());
167        assert!(result.expect("value should be valid").is_video());
168    }
169
170    #[test]
171    fn test_passthrough_no_input() {
172        let mut filter = PassthroughFilter::new(NodeId(0), "test");
173
174        // No input should produce no output
175        let result = filter.process(None).expect("process should succeed");
176        assert!(result.is_none());
177    }
178
179    #[test]
180    fn test_state_transitions() {
181        let mut filter = PassthroughFilter::new(NodeId(0), "test");
182
183        // Valid transitions
184        assert!(filter.set_state(NodeState::Processing).is_ok());
185        assert_eq!(filter.state(), NodeState::Processing);
186
187        assert!(filter.set_state(NodeState::Idle).is_ok());
188        assert_eq!(filter.state(), NodeState::Idle);
189
190        assert!(filter.set_state(NodeState::Done).is_ok());
191        assert_eq!(filter.state(), NodeState::Done);
192
193        // Invalid transition from Done to Processing
194        assert!(filter.set_state(NodeState::Processing).is_err());
195    }
196}