Skip to main content

oximedia_graph/
port.rs

1//! Port types for connecting nodes in the filter graph.
2//!
3//! Ports define the connection points between nodes. Each node has input ports
4//! (for receiving data) and output ports (for sending data).
5
6use std::fmt;
7
8use oximedia_core::{PixelFormat, SampleFormat};
9
10use crate::node::NodeId;
11
12/// Unique identifier for a port within a node.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
14pub struct PortId(pub u32);
15
16impl fmt::Display for PortId {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        write!(f, "Port({})", self.0)
19    }
20}
21
22/// Type of data flowing through a port.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum PortType {
25    /// Video frames.
26    Video,
27    /// Audio samples.
28    Audio,
29    /// Generic data (subtitles, metadata, etc.).
30    Data,
31}
32
33impl fmt::Display for PortType {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Self::Video => write!(f, "Video"),
37            Self::Audio => write!(f, "Audio"),
38            Self::Data => write!(f, "Data"),
39        }
40    }
41}
42
43/// Format specification for ports.
44///
45/// Used during format negotiation to ensure compatible connections.
46#[derive(Clone, Debug, PartialEq)]
47pub enum PortFormat {
48    /// Video format specification.
49    Video(VideoPortFormat),
50    /// Audio format specification.
51    Audio(AudioPortFormat),
52    /// Generic data format.
53    Data(DataPortFormat),
54    /// Any format (accepts/produces any compatible format).
55    Any,
56}
57
58impl PortFormat {
59    /// Check if this format is compatible with another.
60    #[must_use]
61    pub fn is_compatible(&self, other: &Self) -> bool {
62        match (self, other) {
63            (Self::Any, _) | (_, Self::Any) => true,
64            (Self::Video(a), Self::Video(b)) => a.is_compatible(b),
65            (Self::Audio(a), Self::Audio(b)) => a.is_compatible(b),
66            (Self::Data(a), Self::Data(b)) => a.is_compatible(b),
67            _ => false,
68        }
69    }
70
71    /// Get the port type for this format.
72    #[must_use]
73    pub fn port_type(&self) -> Option<PortType> {
74        match self {
75            Self::Video(_) => Some(PortType::Video),
76            Self::Audio(_) => Some(PortType::Audio),
77            Self::Data(_) => Some(PortType::Data),
78            Self::Any => None,
79        }
80    }
81}
82
83#[allow(clippy::derivable_impls)]
84impl Default for PortFormat {
85    fn default() -> Self {
86        Self::Any
87    }
88}
89
90impl fmt::Display for PortFormat {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        match self {
93            Self::Video(v) => write!(f, "Video({v})"),
94            Self::Audio(a) => write!(f, "Audio({a})"),
95            Self::Data(d) => write!(f, "Data({d})"),
96            Self::Any => write!(f, "Any"),
97        }
98    }
99}
100
101/// Video port format specification.
102#[derive(Clone, Debug, PartialEq)]
103pub struct VideoPortFormat {
104    /// Pixel format (or None for any).
105    pub pixel_format: Option<PixelFormat>,
106    /// Frame width (or None for any).
107    pub width: Option<u32>,
108    /// Frame height (or None for any).
109    pub height: Option<u32>,
110}
111
112impl VideoPortFormat {
113    /// Create a new video format with specific pixel format.
114    #[must_use]
115    pub fn new(pixel_format: PixelFormat) -> Self {
116        Self {
117            pixel_format: Some(pixel_format),
118            width: None,
119            height: None,
120        }
121    }
122
123    /// Create a format that accepts any video.
124    #[must_use]
125    pub fn any() -> Self {
126        Self {
127            pixel_format: None,
128            width: None,
129            height: None,
130        }
131    }
132
133    /// Set the dimensions.
134    #[must_use]
135    pub fn with_dimensions(mut self, width: u32, height: u32) -> Self {
136        self.width = Some(width);
137        self.height = Some(height);
138        self
139    }
140
141    /// Check if compatible with another video format.
142    #[must_use]
143    pub fn is_compatible(&self, other: &Self) -> bool {
144        let pixel_ok = self.pixel_format.is_none()
145            || other.pixel_format.is_none()
146            || self.pixel_format == other.pixel_format;
147
148        let width_ok = self.width.is_none() || other.width.is_none() || self.width == other.width;
149
150        let height_ok =
151            self.height.is_none() || other.height.is_none() || self.height == other.height;
152
153        pixel_ok && width_ok && height_ok
154    }
155}
156
157impl Default for VideoPortFormat {
158    fn default() -> Self {
159        Self::any()
160    }
161}
162
163impl fmt::Display for VideoPortFormat {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        let pixel = self
166            .pixel_format
167            .map_or("any".to_string(), |p| format!("{p:?}"));
168        let dims = match (self.width, self.height) {
169            (Some(w), Some(h)) => format!("{w}x{h}"),
170            _ => "any".to_string(),
171        };
172        write!(f, "{pixel}@{dims}")
173    }
174}
175
176/// Audio port format specification.
177#[derive(Clone, Debug, PartialEq)]
178pub struct AudioPortFormat {
179    /// Sample format (or None for any).
180    pub sample_format: Option<SampleFormat>,
181    /// Sample rate in Hz (or None for any).
182    pub sample_rate: Option<u32>,
183    /// Number of channels (or None for any).
184    pub channels: Option<u32>,
185}
186
187impl AudioPortFormat {
188    /// Create a new audio format with specific sample format.
189    #[must_use]
190    pub fn new(sample_format: SampleFormat) -> Self {
191        Self {
192            sample_format: Some(sample_format),
193            sample_rate: None,
194            channels: None,
195        }
196    }
197
198    /// Create a format that accepts any audio.
199    #[must_use]
200    pub fn any() -> Self {
201        Self {
202            sample_format: None,
203            sample_rate: None,
204            channels: None,
205        }
206    }
207
208    /// Set the sample rate.
209    #[must_use]
210    pub fn with_sample_rate(mut self, rate: u32) -> Self {
211        self.sample_rate = Some(rate);
212        self
213    }
214
215    /// Set the number of channels.
216    #[must_use]
217    pub fn with_channels(mut self, channels: u32) -> Self {
218        self.channels = Some(channels);
219        self
220    }
221
222    /// Check if compatible with another audio format.
223    #[must_use]
224    pub fn is_compatible(&self, other: &Self) -> bool {
225        let format_ok = self.sample_format.is_none()
226            || other.sample_format.is_none()
227            || self.sample_format == other.sample_format;
228
229        let rate_ok = self.sample_rate.is_none()
230            || other.sample_rate.is_none()
231            || self.sample_rate == other.sample_rate;
232
233        let channels_ok =
234            self.channels.is_none() || other.channels.is_none() || self.channels == other.channels;
235
236        format_ok && rate_ok && channels_ok
237    }
238}
239
240impl Default for AudioPortFormat {
241    fn default() -> Self {
242        Self::any()
243    }
244}
245
246impl fmt::Display for AudioPortFormat {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        let format = self
249            .sample_format
250            .map_or("any".to_string(), |s| format!("{s:?}"));
251        let rate = self
252            .sample_rate
253            .map_or("any".to_string(), |r| format!("{r}Hz"));
254        let channels = self
255            .channels
256            .map_or("any".to_string(), |c| format!("{c}ch"));
257        write!(f, "{format}@{rate}/{channels}")
258    }
259}
260
261/// Data port format specification.
262#[derive(Clone, Debug, PartialEq, Default)]
263pub struct DataPortFormat {
264    /// MIME type or format identifier.
265    pub mime_type: Option<String>,
266}
267
268impl DataPortFormat {
269    /// Create a new data format with MIME type.
270    #[must_use]
271    pub fn new(mime_type: impl Into<String>) -> Self {
272        Self {
273            mime_type: Some(mime_type.into()),
274        }
275    }
276
277    /// Create a format that accepts any data.
278    #[must_use]
279    pub fn any() -> Self {
280        Self { mime_type: None }
281    }
282
283    /// Check if compatible with another data format.
284    #[must_use]
285    pub fn is_compatible(&self, other: &Self) -> bool {
286        self.mime_type.is_none() || other.mime_type.is_none() || self.mime_type == other.mime_type
287    }
288}
289
290impl fmt::Display for DataPortFormat {
291    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292        match &self.mime_type {
293            Some(t) => write!(f, "{t}"),
294            None => write!(f, "any"),
295        }
296    }
297}
298
299/// Input port on a node.
300#[derive(Clone, Debug)]
301pub struct InputPort {
302    /// Port identifier.
303    pub id: PortId,
304    /// Port name.
305    pub name: String,
306    /// Type of data accepted.
307    pub port_type: PortType,
308    /// Format specification.
309    pub format: PortFormat,
310    /// Whether this port is required (must be connected).
311    pub required: bool,
312}
313
314impl InputPort {
315    /// Create a new input port.
316    #[must_use]
317    pub fn new(id: PortId, name: impl Into<String>, port_type: PortType) -> Self {
318        let format = match port_type {
319            PortType::Video => PortFormat::Video(VideoPortFormat::any()),
320            PortType::Audio => PortFormat::Audio(AudioPortFormat::any()),
321            PortType::Data => PortFormat::Data(DataPortFormat::any()),
322        };
323
324        Self {
325            id,
326            name: name.into(),
327            port_type,
328            format,
329            required: true,
330        }
331    }
332
333    /// Set the format specification.
334    #[must_use]
335    pub fn with_format(mut self, format: PortFormat) -> Self {
336        self.format = format;
337        self
338    }
339
340    /// Set whether this port is required.
341    #[must_use]
342    pub fn optional(mut self) -> Self {
343        self.required = false;
344        self
345    }
346}
347
348/// Output port on a node.
349#[derive(Clone, Debug)]
350pub struct OutputPort {
351    /// Port identifier.
352    pub id: PortId,
353    /// Port name.
354    pub name: String,
355    /// Type of data produced.
356    pub port_type: PortType,
357    /// Format specification.
358    pub format: PortFormat,
359}
360
361impl OutputPort {
362    /// Create a new output port.
363    #[must_use]
364    pub fn new(id: PortId, name: impl Into<String>, port_type: PortType) -> Self {
365        let format = match port_type {
366            PortType::Video => PortFormat::Video(VideoPortFormat::any()),
367            PortType::Audio => PortFormat::Audio(AudioPortFormat::any()),
368            PortType::Data => PortFormat::Data(DataPortFormat::any()),
369        };
370
371        Self {
372            id,
373            name: name.into(),
374            port_type,
375            format,
376        }
377    }
378
379    /// Set the format specification.
380    #[must_use]
381    pub fn with_format(mut self, format: PortFormat) -> Self {
382        self.format = format;
383        self
384    }
385}
386
387/// Connection between two nodes.
388#[derive(Clone, Debug, PartialEq, Eq, Hash)]
389pub struct Connection {
390    /// Source node ID.
391    pub from_node: NodeId,
392    /// Source port ID.
393    pub from_port: PortId,
394    /// Destination node ID.
395    pub to_node: NodeId,
396    /// Destination port ID.
397    pub to_port: PortId,
398}
399
400impl Connection {
401    /// Create a new connection.
402    #[must_use]
403    pub fn new(from_node: NodeId, from_port: PortId, to_node: NodeId, to_port: PortId) -> Self {
404        Self {
405            from_node,
406            from_port,
407            to_node,
408            to_port,
409        }
410    }
411}
412
413impl fmt::Display for Connection {
414    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
415        write!(
416            f,
417            "{:?}:{:?} -> {:?}:{:?}",
418            self.from_node, self.from_port, self.to_node, self.to_port
419        )
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426
427    #[test]
428    fn test_port_id_display() {
429        let id = PortId(0);
430        assert_eq!(format!("{id}"), "Port(0)");
431    }
432
433    #[test]
434    fn test_port_type_display() {
435        assert_eq!(format!("{}", PortType::Video), "Video");
436        assert_eq!(format!("{}", PortType::Audio), "Audio");
437        assert_eq!(format!("{}", PortType::Data), "Data");
438    }
439
440    #[test]
441    fn test_video_format_compatibility() {
442        let any = VideoPortFormat::any();
443        let yuv420 = VideoPortFormat::new(PixelFormat::Yuv420p);
444        let yuv444 = VideoPortFormat::new(PixelFormat::Yuv444p);
445
446        assert!(any.is_compatible(&yuv420));
447        assert!(yuv420.is_compatible(&any));
448        assert!(yuv420.is_compatible(&yuv420));
449        assert!(!yuv420.is_compatible(&yuv444));
450    }
451
452    #[test]
453    fn test_audio_format_compatibility() {
454        let any = AudioPortFormat::any();
455        let f32_48k = AudioPortFormat::new(SampleFormat::F32).with_sample_rate(48000);
456        let f32_44k = AudioPortFormat::new(SampleFormat::F32).with_sample_rate(44100);
457
458        assert!(any.is_compatible(&f32_48k));
459        assert!(f32_48k.is_compatible(&any));
460        assert!(!f32_48k.is_compatible(&f32_44k));
461    }
462
463    #[test]
464    fn test_port_format_compatibility() {
465        let video = PortFormat::Video(VideoPortFormat::any());
466        let audio = PortFormat::Audio(AudioPortFormat::any());
467        let any = PortFormat::Any;
468
469        assert!(any.is_compatible(&video));
470        assert!(any.is_compatible(&audio));
471        assert!(!video.is_compatible(&audio));
472    }
473
474    #[test]
475    fn test_input_port() {
476        let port = InputPort::new(PortId(0), "input", PortType::Video).optional();
477        assert_eq!(port.id, PortId(0));
478        assert_eq!(port.name, "input");
479        assert_eq!(port.port_type, PortType::Video);
480        assert!(!port.required);
481    }
482
483    #[test]
484    fn test_output_port() {
485        let port = OutputPort::new(PortId(0), "output", PortType::Audio);
486        assert_eq!(port.id, PortId(0));
487        assert_eq!(port.name, "output");
488        assert_eq!(port.port_type, PortType::Audio);
489    }
490
491    #[test]
492    fn test_connection() {
493        let conn = Connection::new(NodeId(0), PortId(0), NodeId(1), PortId(0));
494        assert_eq!(conn.from_node, NodeId(0));
495        assert_eq!(conn.to_node, NodeId(1));
496    }
497}