use std::fmt;
use oximedia_core::{PixelFormat, SampleFormat};
use crate::node::NodeId;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub struct PortId(pub u32);
impl fmt::Display for PortId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Port({})", self.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PortType {
Video,
Audio,
Data,
}
impl fmt::Display for PortType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Video => write!(f, "Video"),
Self::Audio => write!(f, "Audio"),
Self::Data => write!(f, "Data"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum PortFormat {
Video(VideoPortFormat),
Audio(AudioPortFormat),
Data(DataPortFormat),
Any,
}
impl PortFormat {
#[must_use]
pub fn is_compatible(&self, other: &Self) -> bool {
match (self, other) {
(Self::Any, _) | (_, Self::Any) => true,
(Self::Video(a), Self::Video(b)) => a.is_compatible(b),
(Self::Audio(a), Self::Audio(b)) => a.is_compatible(b),
(Self::Data(a), Self::Data(b)) => a.is_compatible(b),
_ => false,
}
}
#[must_use]
pub fn port_type(&self) -> Option<PortType> {
match self {
Self::Video(_) => Some(PortType::Video),
Self::Audio(_) => Some(PortType::Audio),
Self::Data(_) => Some(PortType::Data),
Self::Any => None,
}
}
}
#[allow(clippy::derivable_impls)]
impl Default for PortFormat {
fn default() -> Self {
Self::Any
}
}
impl fmt::Display for PortFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Video(v) => write!(f, "Video({v})"),
Self::Audio(a) => write!(f, "Audio({a})"),
Self::Data(d) => write!(f, "Data({d})"),
Self::Any => write!(f, "Any"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct VideoPortFormat {
pub pixel_format: Option<PixelFormat>,
pub width: Option<u32>,
pub height: Option<u32>,
}
impl VideoPortFormat {
#[must_use]
pub fn new(pixel_format: PixelFormat) -> Self {
Self {
pixel_format: Some(pixel_format),
width: None,
height: None,
}
}
#[must_use]
pub fn any() -> Self {
Self {
pixel_format: None,
width: None,
height: None,
}
}
#[must_use]
pub fn with_dimensions(mut self, width: u32, height: u32) -> Self {
self.width = Some(width);
self.height = Some(height);
self
}
#[must_use]
pub fn is_compatible(&self, other: &Self) -> bool {
let pixel_ok = self.pixel_format.is_none()
|| other.pixel_format.is_none()
|| self.pixel_format == other.pixel_format;
let width_ok = self.width.is_none() || other.width.is_none() || self.width == other.width;
let height_ok =
self.height.is_none() || other.height.is_none() || self.height == other.height;
pixel_ok && width_ok && height_ok
}
}
impl Default for VideoPortFormat {
fn default() -> Self {
Self::any()
}
}
impl fmt::Display for VideoPortFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let pixel = self
.pixel_format
.map_or("any".to_string(), |p| format!("{p:?}"));
let dims = match (self.width, self.height) {
(Some(w), Some(h)) => format!("{w}x{h}"),
_ => "any".to_string(),
};
write!(f, "{pixel}@{dims}")
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AudioPortFormat {
pub sample_format: Option<SampleFormat>,
pub sample_rate: Option<u32>,
pub channels: Option<u32>,
}
impl AudioPortFormat {
#[must_use]
pub fn new(sample_format: SampleFormat) -> Self {
Self {
sample_format: Some(sample_format),
sample_rate: None,
channels: None,
}
}
#[must_use]
pub fn any() -> Self {
Self {
sample_format: None,
sample_rate: None,
channels: None,
}
}
#[must_use]
pub fn with_sample_rate(mut self, rate: u32) -> Self {
self.sample_rate = Some(rate);
self
}
#[must_use]
pub fn with_channels(mut self, channels: u32) -> Self {
self.channels = Some(channels);
self
}
#[must_use]
pub fn is_compatible(&self, other: &Self) -> bool {
let format_ok = self.sample_format.is_none()
|| other.sample_format.is_none()
|| self.sample_format == other.sample_format;
let rate_ok = self.sample_rate.is_none()
|| other.sample_rate.is_none()
|| self.sample_rate == other.sample_rate;
let channels_ok =
self.channels.is_none() || other.channels.is_none() || self.channels == other.channels;
format_ok && rate_ok && channels_ok
}
}
impl Default for AudioPortFormat {
fn default() -> Self {
Self::any()
}
}
impl fmt::Display for AudioPortFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let format = self
.sample_format
.map_or("any".to_string(), |s| format!("{s:?}"));
let rate = self
.sample_rate
.map_or("any".to_string(), |r| format!("{r}Hz"));
let channels = self
.channels
.map_or("any".to_string(), |c| format!("{c}ch"));
write!(f, "{format}@{rate}/{channels}")
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct DataPortFormat {
pub mime_type: Option<String>,
}
impl DataPortFormat {
#[must_use]
pub fn new(mime_type: impl Into<String>) -> Self {
Self {
mime_type: Some(mime_type.into()),
}
}
#[must_use]
pub fn any() -> Self {
Self { mime_type: None }
}
#[must_use]
pub fn is_compatible(&self, other: &Self) -> bool {
self.mime_type.is_none() || other.mime_type.is_none() || self.mime_type == other.mime_type
}
}
impl fmt::Display for DataPortFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.mime_type {
Some(t) => write!(f, "{t}"),
None => write!(f, "any"),
}
}
}
#[derive(Clone, Debug)]
pub struct InputPort {
pub id: PortId,
pub name: String,
pub port_type: PortType,
pub format: PortFormat,
pub required: bool,
}
impl InputPort {
#[must_use]
pub fn new(id: PortId, name: impl Into<String>, port_type: PortType) -> Self {
let format = match port_type {
PortType::Video => PortFormat::Video(VideoPortFormat::any()),
PortType::Audio => PortFormat::Audio(AudioPortFormat::any()),
PortType::Data => PortFormat::Data(DataPortFormat::any()),
};
Self {
id,
name: name.into(),
port_type,
format,
required: true,
}
}
#[must_use]
pub fn with_format(mut self, format: PortFormat) -> Self {
self.format = format;
self
}
#[must_use]
pub fn optional(mut self) -> Self {
self.required = false;
self
}
}
#[derive(Clone, Debug)]
pub struct OutputPort {
pub id: PortId,
pub name: String,
pub port_type: PortType,
pub format: PortFormat,
}
impl OutputPort {
#[must_use]
pub fn new(id: PortId, name: impl Into<String>, port_type: PortType) -> Self {
let format = match port_type {
PortType::Video => PortFormat::Video(VideoPortFormat::any()),
PortType::Audio => PortFormat::Audio(AudioPortFormat::any()),
PortType::Data => PortFormat::Data(DataPortFormat::any()),
};
Self {
id,
name: name.into(),
port_type,
format,
}
}
#[must_use]
pub fn with_format(mut self, format: PortFormat) -> Self {
self.format = format;
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Connection {
pub from_node: NodeId,
pub from_port: PortId,
pub to_node: NodeId,
pub to_port: PortId,
}
impl Connection {
#[must_use]
pub fn new(from_node: NodeId, from_port: PortId, to_node: NodeId, to_port: PortId) -> Self {
Self {
from_node,
from_port,
to_node,
to_port,
}
}
}
impl fmt::Display for Connection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:?}:{:?} -> {:?}:{:?}",
self.from_node, self.from_port, self.to_node, self.to_port
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_port_id_display() {
let id = PortId(0);
assert_eq!(format!("{id}"), "Port(0)");
}
#[test]
fn test_port_type_display() {
assert_eq!(format!("{}", PortType::Video), "Video");
assert_eq!(format!("{}", PortType::Audio), "Audio");
assert_eq!(format!("{}", PortType::Data), "Data");
}
#[test]
fn test_video_format_compatibility() {
let any = VideoPortFormat::any();
let yuv420 = VideoPortFormat::new(PixelFormat::Yuv420p);
let yuv444 = VideoPortFormat::new(PixelFormat::Yuv444p);
assert!(any.is_compatible(&yuv420));
assert!(yuv420.is_compatible(&any));
assert!(yuv420.is_compatible(&yuv420));
assert!(!yuv420.is_compatible(&yuv444));
}
#[test]
fn test_audio_format_compatibility() {
let any = AudioPortFormat::any();
let f32_48k = AudioPortFormat::new(SampleFormat::F32).with_sample_rate(48000);
let f32_44k = AudioPortFormat::new(SampleFormat::F32).with_sample_rate(44100);
assert!(any.is_compatible(&f32_48k));
assert!(f32_48k.is_compatible(&any));
assert!(!f32_48k.is_compatible(&f32_44k));
}
#[test]
fn test_port_format_compatibility() {
let video = PortFormat::Video(VideoPortFormat::any());
let audio = PortFormat::Audio(AudioPortFormat::any());
let any = PortFormat::Any;
assert!(any.is_compatible(&video));
assert!(any.is_compatible(&audio));
assert!(!video.is_compatible(&audio));
}
#[test]
fn test_input_port() {
let port = InputPort::new(PortId(0), "input", PortType::Video).optional();
assert_eq!(port.id, PortId(0));
assert_eq!(port.name, "input");
assert_eq!(port.port_type, PortType::Video);
assert!(!port.required);
}
#[test]
fn test_output_port() {
let port = OutputPort::new(PortId(0), "output", PortType::Audio);
assert_eq!(port.id, PortId(0));
assert_eq!(port.name, "output");
assert_eq!(port.port_type, PortType::Audio);
}
#[test]
fn test_connection() {
let conn = Connection::new(NodeId(0), PortId(0), NodeId(1), PortId(0));
assert_eq!(conn.from_node, NodeId(0));
assert_eq!(conn.to_node, NodeId(1));
}
}