Skip to main content

rskit_media/encoding/
registry.rs

1//! Data-driven codec & format registry.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use rskit_errors::{AppError, AppResult, ErrorCode};
7
8use crate::{
9    codec::{Codec, CodecKind},
10    executor::MediaExecutor,
11    format::Format,
12    probe::MediaProbe,
13    types::MediaType,
14};
15
16use super::defaults;
17
18/// Factory function used to build a configured media executor.
19pub type ExecutorFactory = Arc<dyn Fn() -> AppResult<Arc<dyn MediaExecutor>> + Send + Sync>;
20
21/// Factory function used to build a configured media probe.
22pub type ProbeFactory = Arc<dyn Fn() -> AppResult<Arc<dyn MediaProbe>> + Send + Sync>;
23
24/// Metadata about a codec.
25#[derive(Debug, Clone)]
26pub struct CodecInfo {
27    /// Codec identifier.
28    pub id: Codec,
29    /// What domain this codec belongs to.
30    pub kind: CodecKind,
31    /// Human-readable name.
32    pub display_name: String,
33    /// FFmpeg encoder name (e.g., "libx264").
34    pub ffmpeg_encoder: Option<String>,
35    /// FFmpeg decoder name.
36    pub ffmpeg_decoder: Option<String>,
37    /// Compatible container formats.
38    pub compatible_formats: Vec<Format>,
39}
40
41/// Metadata about a container format.
42#[derive(Debug, Clone)]
43pub struct FormatInfo {
44    /// Format identifier.
45    pub id: Format,
46    /// Default file extension (e.g., "mp4").
47    pub extension: String,
48    /// MIME type (e.g., "video/mp4").
49    pub mime_type: String,
50    /// Whether this format is a multi-track container.
51    pub is_container: bool,
52    /// What media types this format can hold.
53    pub supported_media_types: Vec<MediaType>,
54    /// Default video codec for this format.
55    pub default_video_codec: Option<Codec>,
56    /// Default audio codec for this format.
57    pub default_audio_codec: Option<Codec>,
58}
59
60/// Central knowledge base for codec/format information and compatibility.
61#[derive(Clone)]
62pub struct Registry {
63    codecs: HashMap<Codec, CodecInfo>,
64    formats: HashMap<Format, FormatInfo>,
65    executor_factories: HashMap<String, ExecutorFactory>,
66    probe_factories: HashMap<String, ProbeFactory>,
67}
68
69impl Default for Registry {
70    fn default() -> Self {
71        let mut reg = Self {
72            codecs: HashMap::new(),
73            formats: HashMap::new(),
74            executor_factories: HashMap::new(),
75            probe_factories: HashMap::new(),
76        };
77        defaults::load_defaults(&mut reg);
78        reg
79    }
80}
81
82impl Registry {
83    /// Register a custom codec.
84    pub fn register_codec(&mut self, info: CodecInfo) {
85        self.codecs.insert(info.id.clone(), info);
86    }
87
88    /// Register a custom format.
89    pub fn register_format(&mut self, info: FormatInfo) {
90        self.formats.insert(info.id.clone(), info);
91    }
92
93    /// Register a configured media executor factory.
94    pub fn register_executor(
95        &mut self,
96        name: impl Into<String>,
97        factory: ExecutorFactory,
98    ) -> AppResult<()> {
99        let name = Self::normalize_backend_name(name)?;
100        if self.executor_factories.contains_key(&name) {
101            return Err(AppError::new(
102                ErrorCode::AlreadyExists,
103                format!("media executor '{name}' is already registered"),
104            ));
105        }
106        self.executor_factories.insert(name, factory);
107        Ok(())
108    }
109
110    /// Register a configured media probe factory.
111    pub fn register_probe(
112        &mut self,
113        name: impl Into<String>,
114        factory: ProbeFactory,
115    ) -> AppResult<()> {
116        let name = Self::normalize_backend_name(name)?;
117        if self.probe_factories.contains_key(&name) {
118            return Err(AppError::new(
119                ErrorCode::AlreadyExists,
120                format!("media probe '{name}' is already registered"),
121            ));
122        }
123        self.probe_factories.insert(name, factory);
124        Ok(())
125    }
126
127    /// Build the configured executor for `name`.
128    pub fn executor(&self, name: &str) -> AppResult<Arc<dyn MediaExecutor>> {
129        let name = Self::normalize_backend_name(name)?;
130        self.executor_factories.get(&name).ok_or_else(|| {
131            AppError::new(
132                ErrorCode::NotFound,
133                format!("media executor '{name}' is not registered"),
134            )
135        })?()
136    }
137
138    /// Build the configured probe for `name`.
139    pub fn probe(&self, name: &str) -> AppResult<Arc<dyn MediaProbe>> {
140        let name = Self::normalize_backend_name(name)?;
141        self.probe_factories.get(&name).ok_or_else(|| {
142            AppError::new(
143                ErrorCode::NotFound,
144                format!("media probe '{name}' is not registered"),
145            )
146        })?()
147    }
148
149    /// Check if a codec is compatible with a format.
150    pub fn is_compatible(&self, codec: &Codec, format: &Format) -> bool {
151        self.codecs
152            .get(codec)
153            .is_some_and(|info| info.compatible_formats.contains(format))
154    }
155
156    fn normalize_backend_name(name: impl Into<String>) -> AppResult<String> {
157        let name = name.into().trim().to_owned();
158        if name.is_empty() {
159            return Err(AppError::new(
160                ErrorCode::InvalidInput,
161                "media backend name is required",
162            ));
163        }
164        Ok(name)
165    }
166
167    /// Get the default codec pair (video, audio) for a format.
168    pub fn default_codecs(&self, format: &Format) -> Option<(Codec, Codec)> {
169        let info = self.formats.get(format)?;
170        let video = info.default_video_codec.clone()?;
171        let audio = info.default_audio_codec.clone()?;
172        Some((video, audio))
173    }
174
175    /// Look up codec metadata.
176    pub fn codec_info(&self, codec: &Codec) -> Option<&CodecInfo> {
177        self.codecs.get(codec)
178    }
179
180    /// Look up format metadata.
181    pub fn format_info(&self, format: &Format) -> Option<&FormatInfo> {
182        self.formats.get(format)
183    }
184
185    /// Detect format from a file extension.
186    pub fn format_from_extension(&self, ext: &str) -> Option<&FormatInfo> {
187        let ext_lower = ext.to_lowercase();
188        self.formats.values().find(|f| f.extension == ext_lower)
189    }
190
191    /// Detect format from a MIME type.
192    pub fn format_from_mime(&self, mime: &str) -> Option<&FormatInfo> {
193        self.formats.values().find(|f| f.mime_type == mime)
194    }
195
196    /// List all registered codecs of a given kind.
197    pub fn codecs_by_kind(&self, kind: CodecKind) -> Vec<&CodecInfo> {
198        self.codecs.values().filter(|c| c.kind == kind).collect()
199    }
200
201    /// List all formats compatible with a given codec.
202    pub fn formats_for_codec(&self, codec: &Codec) -> Vec<&FormatInfo> {
203        match self.codecs.get(codec) {
204            Some(info) => info
205                .compatible_formats
206                .iter()
207                .filter_map(|f| self.formats.get(f))
208                .collect(),
209            None => Vec::new(),
210        }
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use rskit_errors::ErrorCode;
217
218    use super::Registry;
219
220    #[test]
221    fn executor_rejects_empty_backend_name() {
222        let Err(err) = Registry::default().executor(" \t ") else {
223            panic!("empty executor name should be rejected");
224        };
225        assert_eq!(err.code(), ErrorCode::InvalidInput);
226    }
227
228    #[test]
229    fn probe_rejects_empty_backend_name() {
230        let Err(err) = Registry::default().probe(" \t ") else {
231            panic!("empty probe name should be rejected");
232        };
233        assert_eq!(err.code(), ErrorCode::InvalidInput);
234    }
235}