oximedia_plugin/
static_plugin.rs1use crate::traits::{CodecPlugin, CodecPluginInfo, PluginCapability};
14use oximedia_codec::{CodecError, CodecResult, EncoderConfig, VideoDecoder, VideoEncoder};
15
16pub struct StaticPlugin {
44 info: CodecPluginInfo,
45 capabilities: Vec<PluginCapability>,
46 decoder_factory: Option<Box<dyn Fn(&str) -> CodecResult<Box<dyn VideoDecoder>> + Send + Sync>>,
47 encoder_factory: Option<
48 Box<dyn Fn(&str, EncoderConfig) -> CodecResult<Box<dyn VideoEncoder>> + Send + Sync>,
49 >,
50}
51
52impl StaticPlugin {
53 #[must_use]
60 pub fn new(info: CodecPluginInfo) -> Self {
61 Self {
62 info,
63 capabilities: Vec::new(),
64 decoder_factory: None,
65 encoder_factory: None,
66 }
67 }
68
69 #[must_use]
74 pub fn with_decoder<F>(mut self, factory: F) -> Self
75 where
76 F: Fn(&str) -> CodecResult<Box<dyn VideoDecoder>> + Send + Sync + 'static,
77 {
78 self.decoder_factory = Some(Box::new(factory));
79 self
80 }
81
82 #[must_use]
87 pub fn with_encoder<F>(mut self, factory: F) -> Self
88 where
89 F: Fn(&str, EncoderConfig) -> CodecResult<Box<dyn VideoEncoder>> + Send + Sync + 'static,
90 {
91 self.encoder_factory = Some(Box::new(factory));
92 self
93 }
94
95 #[must_use]
97 pub fn add_capability(mut self, cap: PluginCapability) -> Self {
98 self.capabilities.push(cap);
99 self
100 }
101}
102
103impl CodecPlugin for StaticPlugin {
104 fn info(&self) -> CodecPluginInfo {
105 self.info.clone()
106 }
107
108 fn capabilities(&self) -> Vec<PluginCapability> {
109 self.capabilities.clone()
110 }
111
112 fn create_decoder(&self, codec_name: &str) -> CodecResult<Box<dyn VideoDecoder>> {
113 match &self.decoder_factory {
114 Some(factory) => factory(codec_name),
115 None => Err(CodecError::UnsupportedFeature(format!(
116 "No decoder factory registered for '{codec_name}'"
117 ))),
118 }
119 }
120
121 fn create_encoder(
122 &self,
123 codec_name: &str,
124 config: EncoderConfig,
125 ) -> CodecResult<Box<dyn VideoEncoder>> {
126 match &self.encoder_factory {
127 Some(factory) => factory(codec_name, config),
128 None => Err(CodecError::UnsupportedFeature(format!(
129 "No encoder factory registered for '{codec_name}'"
130 ))),
131 }
132 }
133}
134
135#[macro_export]
169macro_rules! declare_plugin {
170 ($plugin_type:ty, $create_fn:ident) => {
171 #[no_mangle]
177 pub unsafe extern "C" fn oximedia_plugin_api_version() -> u32 {
178 $crate::PLUGIN_API_VERSION
179 }
180
181 #[no_mangle]
188 pub unsafe extern "C" fn oximedia_plugin_create() -> *mut dyn $crate::CodecPlugin {
189 let plugin = $create_fn();
190 let boxed: Box<dyn $crate::CodecPlugin> = Box::new(plugin);
191 Box::into_raw(boxed)
192 }
193 };
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use crate::traits::PLUGIN_API_VERSION;
200 use std::collections::HashMap;
201
202 fn make_test_info(name: &str) -> CodecPluginInfo {
203 CodecPluginInfo {
204 name: name.to_string(),
205 version: "1.0.0".to_string(),
206 author: "Test".to_string(),
207 description: "Test plugin".to_string(),
208 api_version: PLUGIN_API_VERSION,
209 license: "MIT".to_string(),
210 patent_encumbered: false,
211 }
212 }
213
214 #[test]
215 fn test_static_plugin_info() {
216 let plugin = StaticPlugin::new(make_test_info("my-plugin"));
217 let info = plugin.info();
218 assert_eq!(info.name, "my-plugin");
219 assert_eq!(info.version, "1.0.0");
220 assert_eq!(info.api_version, PLUGIN_API_VERSION);
221 }
222
223 #[test]
224 fn test_static_plugin_no_capabilities() {
225 let plugin = StaticPlugin::new(make_test_info("empty"));
226 assert!(plugin.capabilities().is_empty());
227 assert!(!plugin.supports_codec("h264"));
228 }
229
230 #[test]
231 fn test_static_plugin_add_capability() {
232 let plugin = StaticPlugin::new(make_test_info("cap-test"))
233 .add_capability(PluginCapability {
234 codec_name: "h264".to_string(),
235 can_decode: true,
236 can_encode: false,
237 pixel_formats: vec!["yuv420p".to_string()],
238 properties: HashMap::new(),
239 })
240 .add_capability(PluginCapability {
241 codec_name: "h265".to_string(),
242 can_decode: true,
243 can_encode: true,
244 pixel_formats: vec!["yuv420p".to_string(), "nv12".to_string()],
245 properties: HashMap::new(),
246 });
247
248 assert_eq!(plugin.capabilities().len(), 2);
249 assert!(plugin.supports_codec("h264"));
250 assert!(plugin.supports_codec("h265"));
251 assert!(plugin.can_decode("h264"));
252 assert!(!plugin.can_encode("h264"));
253 assert!(plugin.can_decode("h265"));
254 assert!(plugin.can_encode("h265"));
255 }
256
257 #[test]
258 fn test_static_plugin_no_decoder_factory() {
259 let plugin = StaticPlugin::new(make_test_info("no-factory"));
260 let result = plugin.create_decoder("h264");
261 assert!(result.is_err());
262 }
263
264 #[test]
265 fn test_static_plugin_no_encoder_factory() {
266 let plugin = StaticPlugin::new(make_test_info("no-factory"));
267 let config = EncoderConfig::default();
268 let result = plugin.create_encoder("h264", config);
269 assert!(result.is_err());
270 }
271
272 #[test]
273 fn test_static_plugin_with_decoder_factory() {
274 let plugin = StaticPlugin::new(make_test_info("factory-test"))
275 .add_capability(PluginCapability {
276 codec_name: "test".to_string(),
277 can_decode: true,
278 can_encode: false,
279 pixel_formats: vec![],
280 properties: HashMap::new(),
281 })
282 .with_decoder(|codec_name| {
283 Err(CodecError::UnsupportedFeature(format!(
285 "Mock decoder for '{codec_name}' - factory was called"
286 )))
287 });
288
289 let result = plugin.create_decoder("test");
290 match result {
291 Err(e) => {
292 let err_msg = e.to_string();
293 assert!(err_msg.contains("Mock decoder for 'test'"));
294 assert!(err_msg.contains("factory was called"));
295 }
296 Ok(_) => panic!("Expected error from mock decoder factory"),
297 }
298 }
299
300 #[test]
301 fn test_static_plugin_with_encoder_factory() {
302 let plugin = StaticPlugin::new(make_test_info("enc-factory"))
303 .add_capability(PluginCapability {
304 codec_name: "test".to_string(),
305 can_decode: false,
306 can_encode: true,
307 pixel_formats: vec![],
308 properties: HashMap::new(),
309 })
310 .with_encoder(|codec_name, _config| {
311 Err(CodecError::UnsupportedFeature(format!(
312 "Mock encoder for '{codec_name}'"
313 )))
314 });
315
316 let config = EncoderConfig::default();
317 let result = plugin.create_encoder("test", config);
318 match result {
319 Err(e) => {
320 assert!(e.to_string().contains("Mock encoder"));
321 }
322 Ok(_) => panic!("Expected error from mock encoder factory"),
323 }
324 }
325
326 #[test]
327 fn test_codec_plugin_trait_default_methods() {
328 let plugin = StaticPlugin::new(make_test_info("defaults"))
329 .add_capability(PluginCapability {
330 codec_name: "codec-a".to_string(),
331 can_decode: true,
332 can_encode: true,
333 pixel_formats: vec![],
334 properties: HashMap::new(),
335 })
336 .add_capability(PluginCapability {
337 codec_name: "codec-b".to_string(),
338 can_decode: true,
339 can_encode: false,
340 pixel_formats: vec![],
341 properties: HashMap::new(),
342 });
343
344 assert!(plugin.supports_codec("codec-a"));
346 assert!(plugin.supports_codec("codec-b"));
347 assert!(!plugin.supports_codec("codec-c"));
348
349 assert!(plugin.can_decode("codec-a"));
351 assert!(plugin.can_decode("codec-b"));
352 assert!(!plugin.can_decode("codec-c"));
353
354 assert!(plugin.can_encode("codec-a"));
356 assert!(!plugin.can_encode("codec-b"));
357 assert!(!plugin.can_encode("codec-c"));
358 }
359}