Skip to main content

qubit_mime/classifier/
media_stream_classifier_registry.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10//! Registry for pluggable media stream classifier providers.
11// qubit-style: allow coverage-cfg
12
13#[cfg(coverage)]
14use std::sync::PoisonError;
15use std::sync::{
16    Arc,
17    LazyLock,
18    RwLock,
19    RwLockReadGuard,
20    RwLockWriteGuard,
21};
22
23use qubit_spi::{
24    ProviderRegistry,
25    ProviderSelection,
26    ServiceProvider,
27};
28
29use crate::{
30    MediaStreamClassifier,
31    MimeConfig,
32    MimeError,
33    MimeResult,
34};
35
36use super::{
37    FfprobeCommandMediaStreamClassifierProvider,
38    MediaStreamClassifierProvider,
39    MediaStreamClassifierSpec,
40};
41
42/// Registry of media stream classifier providers.
43///
44/// Provider names and aliases are matched case-insensitively. Duplicate ids or
45/// aliases are rejected at registration time so a selector always resolves to
46/// at most one provider.
47///
48/// [`MediaStreamClassifierRegistry::create_default_box`] and
49/// [`MediaStreamClassifierRegistry::create_default_arc`] read
50/// [`MimeConfig::media_stream_classifier_default`] first. When the configured
51/// selector is empty or `auto`, the registry tries all available providers
52/// ordered by descending provider priority and then by provider id. Otherwise
53/// it creates the named provider.
54#[derive(Debug, Clone, Default)]
55pub struct MediaStreamClassifierRegistry {
56    /// Typed provider registry supplied by `qubit-spi`.
57    providers: ProviderRegistry<MediaStreamClassifierSpec>,
58}
59
60/// Process-wide default classifier registry.
61static DEFAULT_MEDIA_STREAM_CLASSIFIER_REGISTRY: LazyLock<RwLock<MediaStreamClassifierRegistry>> =
62    LazyLock::new(|| RwLock::new(MediaStreamClassifierRegistry::builtin()));
63
64/// Backend name used when reporting default registry lock failures.
65#[cfg(not(coverage))]
66const BACKEND: &str = "media-stream-classifier-registry";
67
68/// Error reason used when a default registry lock is poisoned.
69#[cfg(not(coverage))]
70const LOCK_ERR: &str = "lock poisoned";
71
72impl MediaStreamClassifierRegistry {
73    /// Creates an empty classifier registry.
74    ///
75    /// # Returns
76    /// Empty registry.
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// Creates a registry containing only built-in classifier providers.
82    ///
83    /// # Returns
84    /// Registry with the FFprobe provider.
85    pub fn builtin() -> Self {
86        let mut registry = Self::new();
87        registry
88            .register(FfprobeCommandMediaStreamClassifierProvider)
89            .expect("built-in FFprobe classifier provider should register");
90        registry
91    }
92
93    /// Gets a snapshot of the process-wide default classifier registry.
94    ///
95    /// The returned registry is cloned from the global default registry, so
96    /// callers can inspect or create classifiers without holding a global lock.
97    ///
98    /// # Returns
99    /// Snapshot of the default registry.
100    ///
101    /// # Errors
102    /// Returns [`MimeError::ClassifierBackend`] when the global registry lock
103    /// has been poisoned by another thread.
104    pub fn default_registry() -> MimeResult<Self> {
105        let registry = read_default_registry()?;
106        Ok(registry.clone())
107    }
108
109    /// Registers a provider in the process-wide default classifier registry.
110    ///
111    /// Successfully registered providers become visible to
112    /// [`MediaStreamClassifierRegistry::default_registry`] snapshots throughout
113    /// the current process.
114    ///
115    /// # Parameters
116    /// - `provider`: Provider to register globally.
117    ///
118    /// # Errors
119    /// Returns [`MimeError::DuplicateClassifierName`] when the provider id or
120    /// one of its aliases already exists in the default registry. Returns
121    /// [`MimeError::ClassifierBackend`] when the global registry lock has been
122    /// poisoned by another thread.
123    pub fn register_default<P>(provider: P) -> MimeResult<()>
124    where
125        P: MediaStreamClassifierProvider + 'static,
126    {
127        let mut registry = write_default_registry()?;
128        registry.register(provider)
129    }
130
131    /// Registers a provider.
132    ///
133    /// # Parameters
134    /// - `provider`: Provider to register.
135    ///
136    /// # Errors
137    /// Returns [`MimeError::DuplicateClassifierName`] when the provider id or
138    /// one of its aliases conflicts with an existing provider.
139    pub fn register<P>(&mut self, provider: P) -> MimeResult<()>
140    where
141        P: MediaStreamClassifierProvider + 'static,
142    {
143        self.providers
144            .register(provider)
145            .map_err(MimeError::classifier_registry_error)
146    }
147
148    /// Registers a shared provider.
149    ///
150    /// # Parameters
151    /// - `provider`: Shared provider to register.
152    ///
153    /// # Errors
154    /// Returns a [`MimeError`] when the provider descriptor is invalid or one
155    /// of its names conflicts with an existing provider.
156    pub fn register_shared<P>(&mut self, provider: Arc<P>) -> MimeResult<()>
157    where
158        P: MediaStreamClassifierProvider + 'static,
159    {
160        self.providers
161            .register_shared(provider)
162            .map_err(MimeError::classifier_registry_error)
163    }
164
165    /// Registers a shared provider.
166    ///
167    /// # Parameters
168    /// - `provider`: Shared provider to register.
169    ///
170    /// # Errors
171    /// Returns a [`MimeError`] when the provider descriptor is invalid or one
172    /// of its names conflicts with an existing provider.
173    pub fn register_arc<P>(&mut self, provider: Arc<P>) -> MimeResult<()>
174    where
175        P: MediaStreamClassifierProvider + 'static,
176    {
177        self.register_shared(provider)
178    }
179
180    /// Gets canonical provider names in registration order.
181    ///
182    /// # Returns
183    /// Provider ids.
184    pub fn provider_names(&self) -> Vec<&str> {
185        self.providers.provider_names()
186    }
187
188    /// Finds a provider by id or alias.
189    ///
190    /// # Parameters
191    /// - `name`: Provider id or alias. Matching is case-insensitive.
192    ///
193    /// # Returns
194    /// Matching provider, or `None`.
195    pub fn find_provider(
196        &self,
197        name: &str,
198    ) -> Option<&dyn ServiceProvider<MediaStreamClassifierSpec>> {
199        self.resolve_provider(name).ok()
200    }
201
202    /// Resolves a provider by id or alias.
203    ///
204    /// # Parameters
205    /// - `name`: Provider id or alias. Names are normalized before lookup.
206    ///
207    /// # Returns
208    /// Matching provider.
209    ///
210    /// # Errors
211    /// Returns [`MimeError::EmptyClassifierName`] or [`MimeError::InvalidClassifierName`]
212    /// when `name` is invalid, or [`MimeError::UnknownClassifier`] when no provider
213    /// matches.
214    pub fn resolve_provider(
215        &self,
216        name: &str,
217    ) -> MimeResult<&dyn ServiceProvider<MediaStreamClassifierSpec>> {
218        self.providers
219            .resolve_provider(name)
220            .map_err(MimeError::classifier_registry_error)
221    }
222
223    /// Creates a boxed classifier from a provider name.
224    ///
225    /// # Parameters
226    /// - `name`: Provider id or alias.
227    /// - `config`: MIME configuration passed to the provider.
228    ///
229    /// # Returns
230    /// Boxed media stream classifier trait object.
231    ///
232    /// # Errors
233    /// Returns [`MimeError::UnknownClassifier`] when no provider matches
234    /// `name`, [`MimeError::ClassifierUnavailable`] when the provider is
235    /// unavailable, or another [`MimeError`] when provider initialization fails.
236    pub fn create_box(
237        &self,
238        name: &str,
239        config: &MimeConfig,
240    ) -> MimeResult<Box<dyn MediaStreamClassifier>> {
241        self.providers
242            .create_box(name, config)
243            .map_err(MimeError::classifier_registry_error)
244    }
245
246    /// Creates a shared classifier from a provider name.
247    ///
248    /// # Parameters
249    /// - `name`: Provider id or alias.
250    /// - `config`: MIME configuration passed to the provider.
251    ///
252    /// # Returns
253    /// Shared media stream classifier trait object.
254    ///
255    /// # Errors
256    /// Returns [`MimeError::UnknownClassifier`] when no provider matches
257    /// `name`, [`MimeError::ClassifierUnavailable`] when the provider is
258    /// unavailable, or another [`MimeError`] when provider initialization fails.
259    pub fn create_arc(
260        &self,
261        name: &str,
262        config: &MimeConfig,
263    ) -> MimeResult<Arc<dyn MediaStreamClassifier>> {
264        self.providers
265            .create_arc(name, config)
266            .map_err(MimeError::classifier_registry_error)
267    }
268
269    /// Creates a boxed classifier from the configured default selector.
270    ///
271    /// # Parameters
272    /// - `config`: MIME configuration.
273    ///
274    /// # Returns
275    /// First boxed classifier that can be created.
276    ///
277    /// # Errors
278    /// Returns [`MimeError::NoAvailableClassifier`] when no configured provider
279    /// can be created.
280    pub fn create_default_box(
281        &self,
282        config: &MimeConfig,
283    ) -> MimeResult<Box<dyn MediaStreamClassifier>> {
284        let selection = provider_selection_from_config(config)?;
285        self.providers
286            .create_selected_box(&selection, config)
287            .map_err(MimeError::classifier_registry_error)
288    }
289
290    /// Creates a shared classifier from the configured default selector.
291    ///
292    /// # Parameters
293    /// - `config`: MIME configuration.
294    ///
295    /// # Returns
296    /// First shared classifier that can be created.
297    ///
298    /// # Errors
299    /// Returns [`MimeError::NoAvailableClassifier`] when no configured provider
300    /// can be created.
301    pub fn create_default_arc(
302        &self,
303        config: &MimeConfig,
304    ) -> MimeResult<Arc<dyn MediaStreamClassifier>> {
305        let selection = provider_selection_from_config(config)?;
306        self.providers
307            .create_selected_arc(&selection, config)
308            .map_err(MimeError::classifier_registry_error)
309    }
310}
311
312/// Builds the provider selection policy from MIME configuration.
313///
314/// # Parameters
315/// - `config`: MIME configuration.
316///
317/// # Returns
318/// Provider selection used by `qubit-spi`.
319///
320/// # Errors
321/// Returns [`MimeError`] when a configured provider name is invalid.
322fn provider_selection_from_config(config: &MimeConfig) -> MimeResult<ProviderSelection> {
323    let configured = config.media_stream_classifier_default().trim();
324    if configured.is_empty() || configured.eq_ignore_ascii_case("auto") {
325        return Ok(ProviderSelection::Auto);
326    }
327    ProviderSelection::named(configured).map_err(MimeError::classifier_registry_error)
328}
329
330/// Locks the default registry for reading.
331///
332/// # Returns
333/// Read guard for the default registry.
334///
335/// # Errors
336/// Returns [`MimeError::ClassifierBackend`] when the global registry lock has
337/// been poisoned by another thread.
338#[cfg(not(coverage))]
339fn read_default_registry() -> MimeResult<RwLockReadGuard<'static, MediaStreamClassifierRegistry>> {
340    match DEFAULT_MEDIA_STREAM_CLASSIFIER_REGISTRY.read() {
341        Ok(registry) => Ok(registry),
342        Err(_) => Err(MimeError::ClassifierBackend {
343            backend: BACKEND.into(),
344            reason: LOCK_ERR.into(),
345        }),
346    }
347}
348
349/// Locks the default registry for reading during coverage runs.
350///
351/// Poisoning cannot be triggered reliably through public behavior, so coverage
352/// runs recover the guard and keep the public API path covered.
353///
354/// # Returns
355/// Read guard for the default registry.
356#[cfg(coverage)]
357fn read_default_registry() -> MimeResult<RwLockReadGuard<'static, MediaStreamClassifierRegistry>> {
358    Ok(DEFAULT_MEDIA_STREAM_CLASSIFIER_REGISTRY
359        .read()
360        .unwrap_or_else(PoisonError::into_inner))
361}
362
363/// Locks the default registry for writing.
364///
365/// # Returns
366/// Write guard for the default registry.
367///
368/// # Errors
369/// Returns [`MimeError::ClassifierBackend`] when the global registry lock has
370/// been poisoned by another thread.
371#[cfg(not(coverage))]
372fn write_default_registry() -> MimeResult<RwLockWriteGuard<'static, MediaStreamClassifierRegistry>>
373{
374    match DEFAULT_MEDIA_STREAM_CLASSIFIER_REGISTRY.write() {
375        Ok(registry) => Ok(registry),
376        Err(_) => Err(MimeError::ClassifierBackend {
377            backend: BACKEND.into(),
378            reason: LOCK_ERR.into(),
379        }),
380    }
381}
382
383/// Locks the default registry for writing during coverage runs.
384///
385/// Poisoning cannot be triggered reliably through public behavior, so coverage
386/// runs recover the guard and keep the public API path covered.
387///
388/// # Returns
389/// Write guard for the default registry.
390#[cfg(coverage)]
391fn write_default_registry() -> MimeResult<RwLockWriteGuard<'static, MediaStreamClassifierRegistry>>
392{
393    Ok(DEFAULT_MEDIA_STREAM_CLASSIFIER_REGISTRY
394        .write()
395        .unwrap_or_else(PoisonError::into_inner))
396}