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}