Skip to main content

oximedia_plugin/
static_plugin.rs

1//! Static plugin implementation.
2//!
3//! Provides [`StaticPlugin`], a convenience type for creating plugins
4//! without shared library loading. This is useful for:
5//!
6//! - Testing and development
7//! - Embedding plugins directly in the application binary
8//! - Creating adapter plugins for existing codec implementations
9//!
10//! Also provides the `declare_plugin!` macro for creating shared
11//! library entry points.
12
13use crate::traits::{CodecPlugin, CodecPluginInfo, PluginCapability};
14use oximedia_codec::{CodecError, CodecResult, EncoderConfig, VideoDecoder, VideoEncoder};
15
16/// A static plugin that wraps decoder/encoder factory functions.
17///
18/// Use the builder pattern to construct a plugin with custom factories:
19///
20/// ```rust
21/// use oximedia_plugin::{StaticPlugin, CodecPluginInfo, PluginCapability, PLUGIN_API_VERSION};
22/// use std::collections::HashMap;
23///
24/// let info = CodecPluginInfo {
25///     name: "my-plugin".to_string(),
26///     version: "1.0.0".to_string(),
27///     author: "Me".to_string(),
28///     description: "My custom plugin".to_string(),
29///     api_version: PLUGIN_API_VERSION,
30///     license: "MIT".to_string(),
31///     patent_encumbered: false,
32/// };
33///
34/// let plugin = StaticPlugin::new(info)
35///     .add_capability(PluginCapability {
36///         codec_name: "custom-codec".to_string(),
37///         can_decode: true,
38///         can_encode: false,
39///         pixel_formats: vec!["yuv420p".to_string()],
40///         properties: HashMap::new(),
41///     });
42/// ```
43pub 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    /// Create a new static plugin with the given metadata.
54    ///
55    /// The plugin starts with no capabilities and no factories.
56    /// Use [`add_capability`](Self::add_capability),
57    /// [`with_decoder`](Self::with_decoder), and
58    /// [`with_encoder`](Self::with_encoder) to configure it.
59    #[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    /// Register a decoder factory function.
70    ///
71    /// The factory receives the codec name and should return a new
72    /// decoder instance or an error if the codec is not supported.
73    #[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    /// Register an encoder factory function.
83    ///
84    /// The factory receives the codec name and encoder configuration,
85    /// and should return a new encoder instance or an error.
86    #[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    /// Add a codec capability to this plugin.
96    #[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 for defining a plugin entry point in a shared library.
136///
137/// This macro generates the two required `extern "C"` functions
138/// that the host uses to load the plugin:
139///
140/// - `oximedia_plugin_api_version() -> u32` - returns the API version
141/// - `oximedia_plugin_create() -> *mut dyn CodecPlugin` - creates the plugin
142///
143/// # Usage
144///
145/// In your plugin crate's `lib.rs`:
146///
147/// ```rust,ignore
148/// use oximedia_plugin::{CodecPlugin, CodecPluginInfo};
149///
150/// struct MyPlugin;
151///
152/// impl CodecPlugin for MyPlugin {
153///     // ... implement trait methods
154/// }
155///
156/// fn create_my_plugin() -> MyPlugin {
157///     MyPlugin
158/// }
159///
160/// oximedia_plugin::declare_plugin!(MyPlugin, create_my_plugin);
161/// ```
162///
163/// # Safety
164///
165/// The generated functions use `unsafe extern "C"` ABI. The create
166/// function allocates the plugin on the heap and returns a raw pointer.
167/// The host is responsible for taking ownership (via `Arc::from_raw`).
168#[macro_export]
169macro_rules! declare_plugin {
170    ($plugin_type:ty, $create_fn:ident) => {
171        /// Return the plugin API version for compatibility checking.
172        ///
173        /// # Safety
174        ///
175        /// This function is called by the host through FFI.
176        #[no_mangle]
177        pub unsafe extern "C" fn oximedia_plugin_api_version() -> u32 {
178            $crate::PLUGIN_API_VERSION
179        }
180
181        /// Create a new plugin instance.
182        ///
183        /// # Safety
184        ///
185        /// This function is called by the host through FFI.
186        /// The returned pointer must be passed to `Arc::from_raw` by the caller.
187        #[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                // Return an error to test the factory is called correctly
284                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        // supports_codec
345        assert!(plugin.supports_codec("codec-a"));
346        assert!(plugin.supports_codec("codec-b"));
347        assert!(!plugin.supports_codec("codec-c"));
348
349        // can_decode
350        assert!(plugin.can_decode("codec-a"));
351        assert!(plugin.can_decode("codec-b"));
352        assert!(!plugin.can_decode("codec-c"));
353
354        // can_encode
355        assert!(plugin.can_encode("codec-a"));
356        assert!(!plugin.can_encode("codec-b"));
357        assert!(!plugin.can_encode("codec-c"));
358    }
359}