styx_codec/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{
4    collections::VecDeque,
5    sync::{
6        Arc, Mutex, RwLock,
7        atomic::{AtomicU64, Ordering},
8    },
9    time::{Duration, Instant},
10};
11
12use styx_core::prelude::*;
13/// Encoders/decoders share the same entry-point; the kind distinguishes behavior.
14///
15/// # Example
16/// ```rust
17/// use styx_codec::CodecKind;
18///
19/// let kind = CodecKind::Decoder;
20/// assert_eq!(kind, CodecKind::Decoder);
21/// ```
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
25pub enum CodecKind {
26    /// Encodes raw frames into compressed payloads.
27    Encoder,
28    /// Decodes compressed payloads into raw frames.
29    Decoder,
30}
31
32/// Descriptor for a codec implementation.
33///
34/// # Example
35/// ```rust
36/// use styx_codec::{CodecDescriptor, CodecKind};
37/// use styx_core::prelude::FourCc;
38///
39/// let desc = CodecDescriptor {
40///     kind: CodecKind::Decoder,
41///     input: FourCc::new(*b"MJPG"),
42///     output: FourCc::new(*b"RG24"),
43///     name: "mjpeg",
44///     impl_name: "jpeg-decoder",
45/// };
46/// assert_eq!(desc.name, "mjpeg");
47/// ```
48#[derive(Debug, Clone)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize))]
50#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
51pub struct CodecDescriptor {
52    /// Encoder or decoder.
53    pub kind: CodecKind,
54    /// Expected input FourCc.
55    pub input: FourCc,
56    /// Output FourCc produced.
57    pub output: FourCc,
58    /// Algorithm family (e.g. "mjpeg", "h264").
59    pub name: &'static str,
60    /// Implementation/backend identifier (e.g. "jpeg-decoder", "vaapi", "ffmpeg").
61    pub impl_name: &'static str,
62}
63
64/// Unified codec trait for zero-copy processing.
65///
66/// # Example
67/// ```rust,ignore
68/// use styx_codec::{Codec, CodecDescriptor, CodecError, CodecKind};
69/// use styx_core::prelude::{FourCc, FrameLease};
70///
71/// struct Passthrough {
72///     desc: CodecDescriptor,
73/// }
74///
75/// impl Codec for Passthrough {
76///     fn descriptor(&self) -> &CodecDescriptor { &self.desc }
77///     fn process(&self, input: FrameLease) -> Result<FrameLease, CodecError> {
78///         Ok(input)
79///     }
80/// }
81/// ```
82pub trait Codec: Send + Sync + 'static {
83    /// Describes what this codec expects and produces.
84    fn descriptor(&self) -> &CodecDescriptor;
85
86    /// Process a frame and return the transformed frame.
87    ///
88    /// Implementations should preserve plane references when possible to avoid copies.
89    fn process(&self, input: FrameLease) -> Result<FrameLease, CodecError>;
90}
91
92/// Errors emitted by codecs.
93///
94/// # Example
95/// ```rust
96/// use styx_codec::CodecError;
97/// use styx_core::prelude::FourCc;
98///
99/// let err = CodecError::FormatMismatch {
100///     expected: FourCc::new(*b"MJPG"),
101///     actual: FourCc::new(*b"RG24"),
102/// };
103/// assert!(matches!(err, CodecError::FormatMismatch { .. }));
104/// ```
105#[derive(Debug, thiserror::Error)]
106pub enum CodecError {
107    /// Input did not match the expected FourCc.
108    #[error("format mismatch: expected {expected}, got {actual}")]
109    FormatMismatch {
110        /// Expected input FourCc.
111        expected: FourCc,
112        /// Actual FourCc encountered.
113        actual: FourCc,
114    },
115    /// Codec-specific failure detail.
116    #[error("codec error: {0}")]
117    Codec(String),
118    /// The codec accepted input but did not (yet) produce output (e.g. encoder pipeline delay).
119    #[error("codec backpressure")]
120    Backpressure,
121}
122
123/// Errors surfaced by the registry.
124///
125/// # Example
126/// ```rust
127/// use styx_codec::RegistryError;
128/// use styx_core::prelude::FourCc;
129///
130/// let err = RegistryError::NotFound(FourCc::new(*b"MJPG"));
131/// assert!(matches!(err, RegistryError::NotFound(_)));
132/// ```
133#[derive(Debug, thiserror::Error)]
134pub enum RegistryError {
135    /// No codec registered for the requested FourCc.
136    #[error("codec not registered for {0}")]
137    NotFound(FourCc),
138    /// Codec failed while processing.
139    #[error(transparent)]
140    Codec(#[from] CodecError),
141}
142
143/// Basic stats for codec processing.
144///
145/// # Example
146/// ```rust
147/// use styx_codec::CodecStats;
148///
149/// let stats = CodecStats::default();
150/// stats.inc_processed();
151/// assert_eq!(stats.processed(), 1);
152/// ```
153#[derive(Debug, Clone, Default)]
154pub struct CodecStats {
155    processed: Arc<AtomicU64>,
156    errors: Arc<AtomicU64>,
157    backpressure: Arc<AtomicU64>,
158    last_nanos: Arc<AtomicU64>,
159    window: Arc<Mutex<WindowState>>,
160}
161
162#[derive(Debug, Clone)]
163struct WindowState {
164    samples: VecDeque<(Instant, u64)>,
165    max: usize,
166}
167
168impl Default for WindowState {
169    fn default() -> Self {
170        Self {
171            samples: VecDeque::new(),
172            max: DEFAULT_WINDOW,
173        }
174    }
175}
176
177const DEFAULT_WINDOW: usize = 120;
178
179impl CodecStats {
180    /// Increment processed count.
181    pub fn inc_processed(&self) {
182        self.processed.fetch_add(1, Ordering::Relaxed);
183    }
184
185    /// Increment error count.
186    pub fn inc_errors(&self) {
187        self.errors.fetch_add(1, Ordering::Relaxed);
188    }
189
190    /// Increment backpressure count.
191    pub fn inc_backpressure(&self) {
192        self.backpressure.fetch_add(1, Ordering::Relaxed);
193    }
194
195    /// Snapshot of processed frames.
196    pub fn processed(&self) -> u64 {
197        self.processed.load(Ordering::Relaxed)
198    }
199
200    /// Snapshot of errors.
201    pub fn errors(&self) -> u64 {
202        self.errors.load(Ordering::Relaxed)
203    }
204
205    /// Snapshot of backpressure events.
206    pub fn backpressure(&self) -> u64 {
207        self.backpressure.load(Ordering::Relaxed)
208    }
209
210    /// Samples within the current window.
211    pub fn samples(&self) -> u64 {
212        self.window
213            .lock()
214            .map(|w| w.samples.len() as u64)
215            .unwrap_or(0)
216    }
217
218    /// Record a successful processing duration in nanoseconds.
219    pub fn record_duration(&self, dur: Duration) {
220        let nanos = dur.as_nanos().min(u64::MAX as u128) as u64;
221        self.last_nanos.store(nanos, Ordering::Relaxed);
222        if let Ok(mut win) = self.window.lock() {
223            if win.max == 0 {
224                win.max = DEFAULT_WINDOW;
225            }
226            win.samples.push_back((Instant::now(), nanos));
227            while win.samples.len() > win.max {
228                win.samples.pop_front();
229            }
230        }
231    }
232
233    /// Configure the rolling window size (minimum 1).
234    pub fn set_window_size(&self, window: usize) {
235        let window = window.max(1);
236        if let Ok(mut win) = self.window.lock() {
237            win.max = window;
238            while win.samples.len() > win.max {
239                win.samples.pop_front();
240            }
241        }
242    }
243
244    /// Average processing time in milliseconds, if any samples were recorded.
245    pub fn avg_millis(&self) -> Option<f64> {
246        self.window.lock().ok().and_then(|w| {
247            let count = w.samples.len();
248            if count == 0 {
249                return None;
250            }
251            let total: u128 = w.samples.iter().map(|(_, n)| *n as u128).sum();
252            Some(total as f64 / 1_000_000.0 / count as f64)
253        })
254    }
255
256    /// Last processing duration in milliseconds, if any samples were recorded.
257    pub fn last_millis(&self) -> Option<f64> {
258        let last = self.last_nanos.load(Ordering::Relaxed);
259        if last == 0 {
260            None
261        } else {
262            Some(last as f64 / 1_000_000.0)
263        }
264    }
265
266    /// Approximate frames per second based on processed frames since first sample.
267    pub fn fps(&self) -> Option<f64> {
268        self.window.lock().ok().and_then(|w| {
269            if w.samples.len() < 2 {
270                return None;
271            }
272            let first = w.samples.front()?.0;
273            let last = w.samples.back()?.0;
274            let span = last.saturating_duration_since(first).as_secs_f64();
275            if span > 0.0 {
276                Some(w.samples.len() as f64 / span)
277            } else {
278                None
279            }
280        })
281    }
282}
283
284struct RegistryInner {
285    codecs: std::collections::HashMap<FourCc, Vec<Arc<dyn Codec>>>,
286    preferences: std::collections::HashMap<FourCc, Preference>,
287    impl_priority: std::collections::HashMap<(FourCc, String), i32>,
288    default_prefer_hardware: bool,
289    policies: std::collections::HashMap<FourCc, CodecPolicy>,
290}
291
292impl RegistryInner {
293    fn new() -> Self {
294        Self {
295            codecs: std::collections::HashMap::new(),
296            preferences: std::collections::HashMap::new(),
297            impl_priority: std::collections::HashMap::new(),
298            default_prefer_hardware: true,
299            policies: std::collections::HashMap::new(),
300        }
301    }
302}
303
304fn sort_backends_for(
305    priorities: &std::collections::HashMap<(FourCc, String), i32>,
306    default_prefer_hardware: bool,
307    fourcc: FourCc,
308    list: &mut Vec<Arc<dyn Codec>>,
309) {
310    list.sort_by_key(|c| {
311        let impl_name = c.descriptor().impl_name.to_ascii_lowercase();
312        let prio = priorities
313            .get(&(fourcc, impl_name.clone()))
314            .copied()
315            .unwrap_or(i32::MAX);
316        let hw_bias = if default_prefer_hardware && is_hardware_impl(&impl_name) {
317            0
318        } else {
319            1
320        };
321        (prio, hw_bias, impl_name)
322    });
323}
324
325fn is_hardware_impl(name: &str) -> bool {
326    let n = name.to_ascii_lowercase();
327    [
328        "vaapi",
329        "nvenc",
330        "nvdec",
331        "cuvid",
332        "qsv",
333        "v4l2",
334        "videotoolbox",
335        "v4l2m2m",
336    ]
337    .iter()
338    .any(|tok| n.contains(tok))
339}
340
341/// Thread-safe handle for codec registration/lookups.
342///
343/// # Example
344/// ```rust,ignore
345/// use std::sync::Arc;
346/// use styx_codec::{Codec, CodecDescriptor, CodecError, CodecKind, CodecRegistry};
347/// use styx_core::prelude::{FourCc, FrameLease};
348///
349/// struct Passthrough {
350///     desc: CodecDescriptor,
351/// }
352///
353/// impl Codec for Passthrough {
354///     fn descriptor(&self) -> &CodecDescriptor { &self.desc }
355///     fn process(&self, input: FrameLease) -> Result<FrameLease, CodecError> { Ok(input) }
356/// }
357///
358/// let registry = CodecRegistry::new();
359/// registry.register(
360///     FourCc::new(*b"RG24"),
361///     Arc::new(Passthrough {
362///         desc: CodecDescriptor {
363///             kind: CodecKind::Decoder,
364///             input: FourCc::new(*b"RG24"),
365///             output: FourCc::new(*b"RG24"),
366///             name: "passthrough",
367///             impl_name: "passthrough",
368///         },
369///     }),
370/// );
371/// let handle = registry.handle();
372/// let _ = handle.lookup(FourCc::new(*b"RG24"))?;
373/// # Ok::<(), styx_codec::RegistryError>(())
374/// ```
375#[derive(Clone)]
376pub struct CodecRegistryHandle {
377    inner: Arc<RwLock<RegistryInner>>,
378    stats: CodecStats,
379}
380
381impl CodecRegistryHandle {
382    /// Lookup a codec by FourCc.
383    pub fn lookup(&self, fourcc: FourCc) -> Result<Arc<dyn Codec>, RegistryError> {
384        let guard = self.inner.read().unwrap();
385        guard
386            .codecs
387            .get(&fourcc)
388            .and_then(|v| v.first().cloned())
389            .ok_or(RegistryError::NotFound(fourcc))
390    }
391
392    /// Lookup a codec by FourCc and implementation name.
393    pub fn lookup_named(
394        &self,
395        fourcc: FourCc,
396        impl_name: &str,
397    ) -> Result<Arc<dyn Codec>, RegistryError> {
398        let guard = self.inner.read().unwrap();
399        guard
400            .codecs
401            .get(&fourcc)
402            .and_then(|v| {
403                v.iter()
404                    .find(|c| c.descriptor().impl_name.eq_ignore_ascii_case(impl_name))
405                    .cloned()
406            })
407            .ok_or(RegistryError::NotFound(fourcc))
408    }
409
410    /// Lookup a codec by FourCc, kind, and implementation name.
411    pub fn lookup_named_kind(
412        &self,
413        fourcc: FourCc,
414        kind: CodecKind,
415        impl_name: &str,
416    ) -> Result<Arc<dyn Codec>, RegistryError> {
417        let guard = self.inner.read().unwrap();
418        guard
419            .codecs
420            .get(&fourcc)
421            .and_then(|v| {
422                v.iter()
423                    .find(|c| {
424                        c.descriptor().kind == kind
425                            && c.descriptor().impl_name.eq_ignore_ascii_case(impl_name)
426                    })
427                    .cloned()
428            })
429            .ok_or(RegistryError::NotFound(fourcc))
430    }
431
432    /// Lookup a codec by FourCc honoring an ordered list of preferred impl names and hardware preference.
433    pub fn lookup_preferred(
434        &self,
435        fourcc: FourCc,
436        preferred_impls: &[&str],
437        prefer_hardware: bool,
438    ) -> Result<Arc<dyn Codec>, RegistryError> {
439        let guard = self.inner.read().unwrap();
440        let list = guard
441            .codecs
442            .get(&fourcc)
443            .ok_or(RegistryError::NotFound(fourcc))?;
444
445        if !preferred_impls.is_empty() {
446            for pref in preferred_impls {
447                if let Some(c) = list
448                    .iter()
449                    .find(|c| c.descriptor().impl_name.eq_ignore_ascii_case(pref))
450                {
451                    return Ok(c.clone());
452                }
453            }
454        }
455
456        if prefer_hardware
457            && let Some(c) = list
458                .iter()
459                .find(|c| is_hardware_impl(c.descriptor().impl_name))
460        {
461            return Ok(c.clone());
462        }
463
464        list.first().cloned().ok_or(RegistryError::NotFound(fourcc))
465    }
466
467    /// Find a codec by implementation name and kind across all FourCc entries.
468    pub fn lookup_by_impl(
469        &self,
470        kind: CodecKind,
471        impl_name: &str,
472    ) -> Result<(FourCc, Arc<dyn Codec>), RegistryError> {
473        let guard = self.inner.read().unwrap();
474        for (fcc, list) in guard.codecs.iter() {
475            if let Some(c) = list.iter().find(|c| {
476                c.descriptor().kind == kind
477                    && c.descriptor().impl_name.eq_ignore_ascii_case(impl_name)
478            }) {
479                return Ok((*fcc, c.clone()));
480            }
481        }
482        Err(RegistryError::NotFound(FourCc::new(*b"    ")))
483    }
484
485    /// Process a frame with the registered codec.
486    pub fn process(&self, fourcc: FourCc, frame: FrameLease) -> Result<FrameLease, RegistryError> {
487        let start = Instant::now();
488        let codec = self.lookup(fourcc)?;
489        self.run_codec(start, codec, frame)
490    }
491
492    /// Process with a specific implementation name.
493    pub fn process_named(
494        &self,
495        fourcc: FourCc,
496        impl_name: &str,
497        frame: FrameLease,
498    ) -> Result<FrameLease, RegistryError> {
499        let start = Instant::now();
500        let codec = self.lookup_named(fourcc, impl_name)?;
501        self.run_codec(start, codec, frame)
502    }
503
504    /// Process with an ordered implementation preference and optional hardware bias.
505    pub fn process_preferred(
506        &self,
507        fourcc: FourCc,
508        preferred_impls: &[&str],
509        prefer_hardware: bool,
510        frame: FrameLease,
511    ) -> Result<FrameLease, RegistryError> {
512        let start = Instant::now();
513        let codec = self.lookup_preferred(fourcc, preferred_impls, prefer_hardware)?;
514        self.run_codec(start, codec, frame)
515    }
516
517    /// Configure preferences for a FourCc (impl order + hardware bias).
518    pub fn set_preference(&self, fourcc: FourCc, preference: Preference) {
519        let mut guard = self.inner.write().unwrap();
520        guard.preferences.insert(fourcc, preference);
521    }
522
523    /// Disable a codec implementation by impl_name for the given FourCc.
524    pub fn disable_impl(&self, fourcc: FourCc, impl_name: &str) {
525        let mut guard = self.inner.write().unwrap();
526        if let Some(list) = guard.codecs.get_mut(&fourcc) {
527            list.retain(|c| !c.descriptor().impl_name.eq_ignore_ascii_case(impl_name));
528        }
529    }
530
531    /// Enable only the listed impl_names for a FourCc (removes others).
532    pub fn enable_only(&self, fourcc: FourCc, impl_names: &[&str]) {
533        let mut guard = self.inner.write().unwrap();
534        let priorities = guard.impl_priority.clone();
535        let prefer_hw = guard.default_prefer_hardware;
536        if let Some(list) = guard.codecs.get_mut(&fourcc) {
537            let names: Vec<String> = impl_names.iter().map(|s| s.to_ascii_lowercase()).collect();
538            list.retain(|c| {
539                names
540                    .iter()
541                    .any(|n| c.descriptor().impl_name.eq_ignore_ascii_case(n))
542            });
543            sort_backends_for(&priorities, prefer_hw, fourcc, list);
544        }
545    }
546
547    /// Dynamically register a new codec impl at runtime.
548    pub fn register_dynamic(&self, fourcc: FourCc, codec: Arc<dyn Codec>) {
549        let mut guard = self.inner.write().unwrap();
550        let priorities = guard.impl_priority.clone();
551        let prefer_hw = guard.default_prefer_hardware;
552        let list = guard.codecs.entry(fourcc).or_default();
553        list.push(codec);
554        sort_backends_for(&priorities, prefer_hw, fourcc, list);
555    }
556
557    /// Assign an explicit priority for an impl name (lower wins).
558    pub fn set_impl_priority(&self, fourcc: FourCc, impl_name: &str, priority: i32) {
559        let mut guard = self.inner.write().unwrap();
560        guard
561            .impl_priority
562            .insert((fourcc, impl_name.to_ascii_lowercase()), priority);
563        let priorities = guard.impl_priority.clone();
564        let prefer_hw = guard.default_prefer_hardware;
565        if let Some(list) = guard.codecs.get_mut(&fourcc) {
566            sort_backends_for(&priorities, prefer_hw, fourcc, list);
567        }
568    }
569
570    /// Toggle the default bias toward hardware implementations.
571    pub fn set_default_hardware_bias(&self, prefer: bool) {
572        let mut guard = self.inner.write().unwrap();
573        guard.default_prefer_hardware = prefer;
574    }
575
576    /// Install a full policy for a FourCc.
577    pub fn set_policy(&self, policy: CodecPolicy) {
578        let mut guard = self.inner.write().unwrap();
579        guard.default_prefer_hardware = policy.prefer_hardware;
580        guard.impl_priority.extend(
581            policy
582                .priorities
583                .clone()
584                .into_iter()
585                .map(|(k, v)| ((policy.fourcc, k), v)),
586        );
587        if !policy.ordered_impls.is_empty() {
588            guard.preferences.insert(
589                policy.fourcc,
590                Preference {
591                    impls: policy.ordered_impls.clone(),
592                    prefer_hardware: policy.prefer_hardware,
593                },
594            );
595        }
596        guard.policies.insert(policy.fourcc, policy);
597    }
598
599    /// Lookup honoring stored preferences when present.
600    pub fn lookup_auto(&self, fourcc: FourCc) -> Result<Arc<dyn Codec>, RegistryError> {
601        let guard = self.inner.read().unwrap();
602        let list = guard
603            .codecs
604            .get(&fourcc)
605            .ok_or(RegistryError::NotFound(fourcc))?;
606
607        let policy = guard.policies.get(&fourcc);
608        let prefer_hw = policy
609            .map(|p| p.prefer_hardware)
610            .unwrap_or(guard.default_prefer_hardware);
611
612        // Apply explicit preference first.
613        if let Some(pref) = guard.preferences.get(&fourcc) {
614            if !pref.impls.is_empty() {
615                for name in &pref.impls {
616                    if let Some(c) = list
617                        .iter()
618                        .find(|c| c.descriptor().impl_name.eq_ignore_ascii_case(name))
619                    {
620                        return Ok(c.clone());
621                    }
622                }
623            }
624            if pref.prefer_hardware
625                && let Some(c) = list
626                    .iter()
627                    .find(|c| is_hardware_impl(c.descriptor().impl_name))
628            {
629                return Ok(c.clone());
630            }
631        }
632
633        // Fall back to priority + default hardware bias.
634        let impl_prio = &guard.impl_priority;
635        let best = list
636            .iter()
637            .min_by_key(|c| {
638                let name = c.descriptor().impl_name.to_ascii_lowercase();
639                let prio = impl_prio
640                    .get(&(fourcc, name.clone()))
641                    .copied()
642                    .unwrap_or(i32::MAX);
643                let hw_bias = if prefer_hw && is_hardware_impl(&name) {
644                    0
645                } else {
646                    1
647                };
648                (prio, hw_bias, name)
649            })
650            .cloned();
651
652        best.ok_or(RegistryError::NotFound(fourcc))
653    }
654
655    /// Lookup honoring stored preferences when present, constrained to a codec kind.
656    pub fn lookup_auto_kind(
657        &self,
658        fourcc: FourCc,
659        kind: CodecKind,
660    ) -> Result<Arc<dyn Codec>, RegistryError> {
661        let guard = self.inner.read().unwrap();
662        let list_all = guard
663            .codecs
664            .get(&fourcc)
665            .ok_or(RegistryError::NotFound(fourcc))?;
666        let list: Vec<&Arc<dyn Codec>> = list_all
667            .iter()
668            .filter(|c| c.descriptor().kind == kind)
669            .collect();
670        if list.is_empty() {
671            return Err(RegistryError::NotFound(fourcc));
672        }
673
674        let policy = guard.policies.get(&fourcc);
675        let prefer_hw = policy
676            .map(|p| p.prefer_hardware)
677            .unwrap_or(guard.default_prefer_hardware);
678
679        if let Some(pref) = guard.preferences.get(&fourcc) {
680            if !pref.impls.is_empty() {
681                for name in &pref.impls {
682                    if let Some(c) = list
683                        .iter()
684                        .find(|c| c.descriptor().impl_name.eq_ignore_ascii_case(name))
685                    {
686                        return Ok((*c).clone());
687                    }
688                }
689            }
690            if pref.prefer_hardware
691                && let Some(c) = list
692                    .iter()
693                    .find(|c| is_hardware_impl(c.descriptor().impl_name))
694            {
695                return Ok((*c).clone());
696            }
697        }
698
699        let impl_prio = &guard.impl_priority;
700        let best = list
701            .iter()
702            .min_by_key(|c| {
703                let name = c.descriptor().impl_name.to_ascii_lowercase();
704                let prio = impl_prio
705                    .get(&(fourcc, name.clone()))
706                    .copied()
707                    .unwrap_or(i32::MAX);
708                let hw_bias = if prefer_hw && is_hardware_impl(&name) {
709                    0
710                } else {
711                    1
712                };
713                (prio, hw_bias, name)
714            })
715            .cloned();
716
717        best.cloned().ok_or(RegistryError::NotFound(fourcc))
718    }
719
720    /// Lookup honoring stored preferences, constrained to a codec kind and algorithm family name.
721    pub fn lookup_auto_kind_by_name(
722        &self,
723        fourcc: FourCc,
724        kind: CodecKind,
725        codec_name: &str,
726    ) -> Result<Arc<dyn Codec>, RegistryError> {
727        let guard = self.inner.read().unwrap();
728        let list_all = guard
729            .codecs
730            .get(&fourcc)
731            .ok_or(RegistryError::NotFound(fourcc))?;
732        let list: Vec<&Arc<dyn Codec>> = list_all
733            .iter()
734            .filter(|c| c.descriptor().kind == kind && c.descriptor().name.eq_ignore_ascii_case(codec_name))
735            .collect();
736        if list.is_empty() {
737            return Err(RegistryError::NotFound(fourcc));
738        }
739
740        let policy = guard.policies.get(&fourcc);
741        let prefer_hw = policy
742            .map(|p| p.prefer_hardware)
743            .unwrap_or(guard.default_prefer_hardware);
744
745        if let Some(pref) = guard.preferences.get(&fourcc) {
746            if !pref.impls.is_empty() {
747                for name in &pref.impls {
748                    if let Some(c) = list
749                        .iter()
750                        .find(|c| c.descriptor().impl_name.eq_ignore_ascii_case(name))
751                    {
752                        return Ok((*c).clone());
753                    }
754                }
755            }
756            if pref.prefer_hardware
757                && let Some(c) = list
758                    .iter()
759                    .find(|c| is_hardware_impl(c.descriptor().impl_name))
760            {
761                return Ok((*c).clone());
762            }
763        }
764
765        let impl_prio = &guard.impl_priority;
766        let best = list
767            .iter()
768            .min_by_key(|c| {
769                let name = c.descriptor().impl_name.to_ascii_lowercase();
770                let prio = impl_prio
771                    .get(&(fourcc, name.clone()))
772                    .copied()
773                    .unwrap_or(i32::MAX);
774                let hw_bias = if prefer_hw && is_hardware_impl(&name) {
775                    0
776                } else {
777                    1
778                };
779                (prio, hw_bias, name)
780            })
781            .cloned();
782
783        best.cloned().ok_or(RegistryError::NotFound(fourcc))
784    }
785
786    /// Process honoring stored preferences when present.
787    pub fn process_auto(
788        &self,
789        fourcc: FourCc,
790        frame: FrameLease,
791    ) -> Result<FrameLease, RegistryError> {
792        let start = Instant::now();
793        let codec = self.lookup_auto(fourcc)?;
794        self.run_codec(start, codec, frame)
795    }
796
797    /// Process honoring stored preferences when present, constrained to a codec kind.
798    pub fn process_auto_kind(
799        &self,
800        fourcc: FourCc,
801        kind: CodecKind,
802        frame: FrameLease,
803    ) -> Result<FrameLease, RegistryError> {
804        let start = Instant::now();
805        let codec = self.lookup_auto_kind(fourcc, kind)?;
806        self.run_codec(start, codec, frame)
807    }
808
809    /// Process honoring stored preferences, constrained to a codec kind and algorithm family name.
810    pub fn process_auto_kind_by_name(
811        &self,
812        fourcc: FourCc,
813        kind: CodecKind,
814        codec_name: &str,
815        frame: FrameLease,
816    ) -> Result<FrameLease, RegistryError> {
817        let start = Instant::now();
818        let codec = self.lookup_auto_kind_by_name(fourcc, kind, codec_name)?;
819        self.run_codec(start, codec, frame)
820    }
821
822    /// Stats snapshot.
823    pub fn stats(&self) -> CodecStats {
824        self.stats.clone()
825    }
826
827    /// Snapshot of all registered codecs grouped by FourCc (descriptor-only).
828    pub fn list_registered(&self) -> Vec<(FourCc, Vec<CodecDescriptor>)> {
829        let guard = self.inner.read().unwrap();
830        guard
831            .codecs
832            .iter()
833            .map(|(fourcc, list)| {
834                let descs = list.iter().map(|c| c.descriptor().clone()).collect();
835                (*fourcc, descs)
836            })
837            .collect()
838    }
839
840    /// List registered codecs filtered by kind.
841    pub fn list_registered_by_kind(&self, kind: CodecKind) -> Vec<(FourCc, Vec<CodecDescriptor>)> {
842        self.list_registered()
843            .into_iter()
844            .filter_map(|(fcc, descs)| {
845                let filtered: Vec<_> = descs.into_iter().filter(|d| d.kind == kind).collect();
846                if filtered.is_empty() {
847                    None
848                } else {
849                    Some((fcc, filtered))
850                }
851            })
852            .collect()
853    }
854}
855
856impl CodecRegistryHandle {
857    fn run_codec(
858        &self,
859        start: Instant,
860        codec: Arc<dyn Codec>,
861        frame: FrameLease,
862    ) -> Result<FrameLease, RegistryError> {
863        let expected = codec.descriptor().input;
864        let actual = frame.meta().format.code;
865
866        let frame = if actual != expected {
867            if let Some(converter) = self.lookup_converter(actual, expected) {
868                match converter.process(frame) {
869                    Ok(converted) => converted,
870                    Err(err) => {
871                        self.stats.inc_errors();
872                        return Err(RegistryError::Codec(err));
873                    }
874                }
875            } else {
876                frame
877            }
878        } else {
879            frame
880        };
881
882        match codec.process(frame) {
883            Ok(out) => {
884                self.stats.inc_processed();
885                self.stats.record_duration(start.elapsed());
886                Ok(out)
887            }
888            Err(err) => {
889                if matches!(err, CodecError::Backpressure) {
890                    self.stats.inc_backpressure();
891                } else {
892                    self.stats.inc_errors();
893                }
894                Err(RegistryError::Codec(err))
895            }
896        }
897    }
898
899    fn lookup_converter(&self, actual: FourCc, expected: FourCc) -> Option<Arc<dyn Codec>> {
900        let guard = self.inner.read().unwrap();
901        let list = guard.codecs.get(&actual)?;
902        list.iter()
903            .find(|c| c.descriptor().output == expected)
904            .cloned()
905    }
906}
907
908/// Registry used to install codecs.
909///
910/// # Example
911/// ```rust,ignore
912/// use styx_codec::CodecRegistry;
913///
914/// let registry = CodecRegistry::new();
915/// let handle = registry.handle();
916/// let _ = handle.list_registered();
917/// ```
918pub struct CodecRegistry {
919    handle: CodecRegistryHandle,
920}
921
922const DEFAULT_CODEC_MAX_WIDTH: u32 = 1920;
923const DEFAULT_CODEC_MAX_HEIGHT: u32 = 1080;
924
925impl Default for CodecRegistry {
926    fn default() -> Self {
927        Self::new()
928    }
929}
930
931impl CodecRegistry {
932    /// Create an empty registry.
933    pub fn new() -> Self {
934        let inner = RegistryInner::new();
935        let handle = CodecRegistryHandle {
936            inner: Arc::new(RwLock::new(inner)),
937            stats: CodecStats::default(),
938        };
939        Self { handle }
940    }
941
942    /// Obtain a clonable handle.
943    pub fn handle(&self) -> CodecRegistryHandle {
944        self.handle.clone()
945    }
946
947    /// Register a codec implementation.
948    pub fn register(&self, fourcc: FourCc, codec: Arc<dyn Codec>) {
949        let mut guard = self.handle.inner.write().unwrap();
950        let priorities = guard.impl_priority.clone();
951        let prefer_hw = guard.default_prefer_hardware;
952        let list = guard.codecs.entry(fourcc).or_default();
953        list.push(codec);
954        sort_backends_for(&priorities, prefer_hw, fourcc, list);
955    }
956
957    /// Create a registry pre-populated with codecs enabled for the current build.
958    pub fn with_enabled_codecs() -> Result<Self, CodecError> {
959        Self::with_enabled_codecs_for_max(DEFAULT_CODEC_MAX_WIDTH, DEFAULT_CODEC_MAX_HEIGHT)
960    }
961
962    /// Create a registry and register built-ins using a suggested max frame size (for buffer pools).
963    pub fn with_enabled_codecs_for_max(
964        max_width: u32,
965        max_height: u32,
966    ) -> Result<Self, CodecError> {
967        let registry = Self::new();
968        registry.register_enabled_codecs(max_width, max_height)?;
969        Ok(registry)
970    }
971
972    /// Register codecs that are available under the current feature set using default pool sizing.
973    pub fn register_enabled_codecs_default(&self) -> Result<(), CodecError> {
974        self.register_enabled_codecs(DEFAULT_CODEC_MAX_WIDTH, DEFAULT_CODEC_MAX_HEIGHT)
975    }
976
977    /// Register codecs that are available under the current feature set.
978    pub fn register_enabled_codecs(
979        &self,
980        max_width: u32,
981        max_height: u32,
982    ) -> Result<(), CodecError> {
983        let max_width = max_width.max(1);
984        let max_height = max_height.max(1);
985
986        // Core decoders.
987        self.register(
988            FourCc::new(*b"MJPG"),
989            Arc::new(mjpeg::MjpegDecoder::new(FourCc::new(*b"RG24"))),
990        );
991        // Some V4L2 stacks report JPEG-coded streams as `JPEG` instead of `MJPG`.
992        self.register(
993            FourCc::new(*b"JPEG"),
994            Arc::new(mjpeg::MjpegDecoder::new_for_input(
995                FourCc::new(*b"JPEG"),
996                FourCc::new(*b"RG24"),
997            )),
998        );
999        self.register(
1000            FourCc::new(*b"BGR3"),
1001            Arc::new(decoder::raw::BgrToRgbDecoder::new(max_width, max_height)),
1002        );
1003        self.register(
1004            FourCc::new(*b"BGRA"),
1005            Arc::new(decoder::raw::BgraToRgbDecoder::new(max_width, max_height)),
1006        );
1007        self.register(
1008            FourCc::new(*b"RGBA"),
1009            Arc::new(decoder::raw::RgbaToRgbDecoder::new(max_width, max_height)),
1010        );
1011        self.register(
1012            FourCc::new(*b"YUYV"),
1013            Arc::new(decoder::raw::YuyvToRgbDecoder::new(max_width, max_height)),
1014        );
1015        self.register(
1016            FourCc::new(*b"YUYV"),
1017            Arc::new(decoder::raw::YuyvToLumaDecoder::new(max_width, max_height)),
1018        );
1019        self.register(
1020            FourCc::new(*b"NV12"),
1021            Arc::new(decoder::raw::Nv12ToRgbDecoder::new(max_width, max_height)),
1022        );
1023        self.register(
1024            FourCc::new(*b"I420"),
1025            Arc::new(decoder::raw::I420ToRgbDecoder::new(max_width, max_height)),
1026        );
1027        self.register(
1028            FourCc::new(*b"YU12"),
1029            Arc::new(decoder::raw::Yuv420pToRgbDecoder::new(
1030                FourCc::new(*b"YU12"),
1031                "yu12-cpu",
1032                true,
1033                max_width,
1034                max_height,
1035            )),
1036        );
1037        self.register(
1038            FourCc::new(*b"YV12"),
1039            Arc::new(decoder::raw::Yuv420pToRgbDecoder::new(
1040                FourCc::new(*b"YV12"),
1041                "yv12-cpu",
1042                false,
1043                max_width,
1044                max_height,
1045            )),
1046        );
1047        self.register(
1048            FourCc::new(*b"R8  "),
1049            Arc::new(decoder::raw::Mono8ToRgbDecoder::new(max_width, max_height)),
1050        );
1051        self.register(
1052            FourCc::new(*b"R16 "),
1053            Arc::new(decoder::raw::Mono16ToRgbDecoder::new(max_width, max_height)),
1054        );
1055
1056        // Additional YUV/NV decoders.
1057        self.register(
1058            FourCc::new(*b"NV21"),
1059            Arc::new(decoder::raw::NvToRgbDecoder::new(
1060                FourCc::new(*b"NV21"),
1061                "nv21-cpu",
1062                2,
1063                2,
1064                false,
1065                max_width,
1066                max_height,
1067            )),
1068        );
1069        self.register(
1070            FourCc::new(*b"NV16"),
1071            Arc::new(decoder::raw::NvToRgbDecoder::new(
1072                FourCc::new(*b"NV16"),
1073                "nv16-cpu",
1074                2,
1075                1,
1076                true,
1077                max_width,
1078                max_height,
1079            )),
1080        );
1081        self.register(
1082            FourCc::new(*b"NV61"),
1083            Arc::new(decoder::raw::NvToRgbDecoder::new(
1084                FourCc::new(*b"NV61"),
1085                "nv61-cpu",
1086                2,
1087                1,
1088                false,
1089                max_width,
1090                max_height,
1091            )),
1092        );
1093        self.register(
1094            FourCc::new(*b"NV24"),
1095            Arc::new(decoder::raw::NvToRgbDecoder::new(
1096                FourCc::new(*b"NV24"),
1097                "nv24-cpu",
1098                1,
1099                1,
1100                true,
1101                max_width,
1102                max_height,
1103            )),
1104        );
1105        self.register(
1106            FourCc::new(*b"NV42"),
1107            Arc::new(decoder::raw::NvToRgbDecoder::new(
1108                FourCc::new(*b"NV42"),
1109                "nv42-cpu",
1110                1,
1111                1,
1112                false,
1113                max_width,
1114                max_height,
1115            )),
1116        );
1117
1118        // Common RGB passthroughs (already in target ordering).
1119        for code in [*b"RG24", *b"RGB3", *b"RGB6"] {
1120            self.register(
1121                FourCc::new(code),
1122                Arc::new(decoder::raw::PassthroughDecoder::new(FourCc::new(code))),
1123            );
1124        }
1125
1126        // Bayer demosaic.
1127        let bayer_codes = [
1128            *b"BA81", *b"BA10", *b"BA12", *b"BA14", *b"BG10", *b"BG12", *b"BG14", *b"BG16",
1129            *b"GB10", *b"GB12", *b"GB14", *b"GB16", *b"RG10", *b"RG12", *b"RG14", *b"RG16",
1130            *b"GR10", *b"GR12", *b"GR14", *b"GR16", *b"BYR2", *b"RGGB", *b"GRBG", *b"GBRG",
1131            *b"BGGR", // MIPI packed RAW10/RAW12 (V4L2_PIX_FMT_S*10P / S*12P).
1132            *b"pBAA", *b"pGAA", *b"pgAA", *b"pRAA", *b"pBCC", *b"pGCC", *b"pgCC", *b"pRCC",
1133        ];
1134        for code in bayer_codes {
1135            let fcc = FourCc::new(code);
1136            if let Some(info) = decoder::raw::bayer_info(fcc) {
1137                self.register(
1138                    fcc,
1139                    decoder::raw::bayer_decoder_for(fcc, info, max_width, max_height),
1140                );
1141            }
1142        }
1143
1144        self.register(
1145            FourCc::new(*b"YV12"),
1146            Arc::new(decoder::raw::PlanarYuvToRgbDecoder::new(
1147                FourCc::new(*b"YV12"),
1148                "yv12-cpu",
1149                2,
1150                2,
1151                false,
1152                max_width,
1153                max_height,
1154            )),
1155        );
1156        self.register(
1157            FourCc::new(*b"YU16"),
1158            Arc::new(decoder::raw::PlanarYuvToRgbDecoder::new(
1159                FourCc::new(*b"YU16"),
1160                "yu16-cpu",
1161                2,
1162                1,
1163                true,
1164                max_width,
1165                max_height,
1166            )),
1167        );
1168        self.register(
1169            FourCc::new(*b"YV16"),
1170            Arc::new(decoder::raw::PlanarYuvToRgbDecoder::new(
1171                FourCc::new(*b"YV16"),
1172                "yv16-cpu",
1173                2,
1174                1,
1175                false,
1176                max_width,
1177                max_height,
1178            )),
1179        );
1180        self.register(
1181            FourCc::new(*b"YU24"),
1182            Arc::new(decoder::raw::PlanarYuvToRgbDecoder::new(
1183                FourCc::new(*b"YU24"),
1184                "yu24-cpu",
1185                1,
1186                1,
1187                true,
1188                max_width,
1189                max_height,
1190            )),
1191        );
1192        self.register(
1193            FourCc::new(*b"YV24"),
1194            Arc::new(decoder::raw::PlanarYuvToRgbDecoder::new(
1195                FourCc::new(*b"YV24"),
1196                "yv24-cpu",
1197                1,
1198                1,
1199                false,
1200                max_width,
1201                max_height,
1202            )),
1203        );
1204
1205        self.register(
1206            FourCc::new(*b"YVYU"),
1207            Arc::new(decoder::raw::Packed422ToRgbDecoder::new(
1208                FourCc::new(*b"YVYU"),
1209                "yvyu-cpu",
1210                [0, 3, 2, 1],
1211                max_width,
1212                max_height,
1213            )),
1214        );
1215        self.register(
1216            FourCc::new(*b"UYVY"),
1217            Arc::new(decoder::raw::Packed422ToRgbDecoder::new(
1218                FourCc::new(*b"UYVY"),
1219                "uyvy-cpu",
1220                [1, 0, 3, 2],
1221                max_width,
1222                max_height,
1223            )),
1224        );
1225        self.register(
1226            FourCc::new(*b"VYUY"),
1227            Arc::new(decoder::raw::Packed422ToRgbDecoder::new(
1228                FourCc::new(*b"VYUY"),
1229                "vyuy-cpu",
1230                [1, 2, 3, 0],
1231                max_width,
1232                max_height,
1233            )),
1234        );
1235
1236        // Channel swap/strip variants.
1237        self.register(
1238            FourCc::new(*b"BG24"),
1239            Arc::new(decoder::raw::BgrToRgbDecoder::with_input_for_max(
1240                FourCc::new(*b"BG24"),
1241                "bg24-swap",
1242                max_width,
1243                max_height,
1244            )),
1245        );
1246        self.register(
1247            FourCc::new(*b"XB24"),
1248            Arc::new(decoder::raw::BgraToRgbDecoder::with_input_for_max(
1249                FourCc::new(*b"XB24"),
1250                "xb24-strip",
1251                max_width,
1252                max_height,
1253            )),
1254        );
1255        self.register(
1256            FourCc::new(*b"XR24"),
1257            Arc::new(decoder::raw::RgbaToRgbDecoder::with_input_for_max(
1258                FourCc::new(*b"XR24"),
1259                "xr24-strip",
1260                max_width,
1261                max_height,
1262            )),
1263        );
1264
1265        // 16-bit RGB/BGR converters.
1266        self.register(
1267            FourCc::new(*b"BG48"),
1268            Arc::new(decoder::raw::Rgb48ToRgbDecoder::new(
1269                FourCc::new(*b"BG48"),
1270                "bg48-strip",
1271                true,
1272                max_width,
1273                max_height,
1274            )),
1275        );
1276        self.register(
1277            FourCc::new(*b"RG48"),
1278            Arc::new(decoder::raw::Rgb48ToRgbDecoder::new(
1279                FourCc::new(*b"RG48"),
1280                "rg48-strip",
1281                false,
1282                max_width,
1283                max_height,
1284            )),
1285        );
1286
1287        // Optional image crate decoder.
1288        #[cfg(feature = "image")]
1289        self.register(
1290            FourCc::new(*b"ANY "),
1291            Arc::new(image_any::ImageAnyDecoder::new(FourCc::new(*b"RGBA"))),
1292        );
1293
1294        // Optional FFmpeg codecs.
1295        #[cfg(feature = "codec-ffmpeg")]
1296        {
1297            use crate::ffmpeg::{
1298                FfmpegEncoderOptions, FfmpegH264Decoder, FfmpegH264Encoder, FfmpegH265Decoder,
1299                FfmpegH265Encoder, FfmpegMjpegDecoder, FfmpegMjpegEncoder,
1300            };
1301            let default_decoder_threads = std::env::var("STYX_FFMPEG_DECODER_THREADS")
1302                .ok()
1303                .and_then(|v| v.parse::<usize>().ok())
1304                .filter(|v| *v > 0);
1305            let mjpeg_mjpg = Arc::new(FfmpegMjpegDecoder::with_options_for_input(
1306                FourCc::new(*b"MJPG"),
1307                false,
1308                default_decoder_threads,
1309                None,
1310            )?);
1311            self.register(FourCc::new(*b"MJPG"), mjpeg_mjpg);
1312            // Some V4L2 stacks report JPEG-coded streams as `JPEG` instead of `MJPG`.
1313            let mjpeg_jpeg = Arc::new(FfmpegMjpegDecoder::with_options_for_input(
1314                FourCc::new(*b"JPEG"),
1315                false,
1316                default_decoder_threads,
1317                None,
1318            )?);
1319            self.register(FourCc::new(*b"JPEG"), mjpeg_jpeg);
1320            self.register(
1321                FourCc::new(*b"H264"),
1322                Arc::new(FfmpegH264Decoder::with_options(
1323                    false,
1324                    default_decoder_threads,
1325                    None,
1326                )?),
1327            );
1328            if let Ok(dec) = FfmpegH264Decoder::new_v4l2request_nv12_zero_copy() {
1329                self.register(FourCc::new(*b"H264"), Arc::new(dec));
1330            }
1331            let h265 = Arc::new(FfmpegH265Decoder::with_options_for_input(
1332                FourCc::new(*b"H265"),
1333                false,
1334                default_decoder_threads,
1335                None,
1336            )?);
1337            self.register(FourCc::new(*b"H265"), h265);
1338            // V4L2 uses `HEVC` FourCC for H.265.
1339            let hevc = Arc::new(FfmpegH265Decoder::with_options_for_input(
1340                FourCc::new(*b"HEVC"),
1341                false,
1342                default_decoder_threads,
1343                None,
1344            )?);
1345            self.register(FourCc::new(*b"HEVC"), hevc);
1346            if let Ok(dec) = FfmpegH265Decoder::new_v4l2request_nv12_zero_copy() {
1347                let dec = Arc::new(dec);
1348                self.register(FourCc::new(*b"H265"), dec.clone());
1349                // Note: the v4l2request decoder advertises `H265` input. Registering it under `HEVC`
1350                // would cause a format mismatch if upstream frames are labeled as `HEVC`.
1351            }
1352            self.register(
1353                FourCc::new(*b"RG24"),
1354                Arc::new(FfmpegMjpegEncoder::new_rgb24()?),
1355            );
1356            self.register(
1357                FourCc::new(*b"NV12"),
1358                Arc::new(FfmpegMjpegEncoder::new_nv12()?),
1359            );
1360            self.register(
1361                FourCc::new(*b"YUYV"),
1362                Arc::new(FfmpegMjpegEncoder::with_options_for_input(
1363                    FourCc::new(*b"YUYV"),
1364                    FfmpegEncoderOptions::default(),
1365                )?),
1366            );
1367            self.register(
1368                FourCc::new(*b"RG24"),
1369                Arc::new(FfmpegH264Encoder::new_rgb24()?),
1370            );
1371            self.register(
1372                FourCc::new(*b"NV12"),
1373                Arc::new(FfmpegH264Encoder::new_nv12()?),
1374            );
1375            if let Ok(enc) = FfmpegH264Encoder::new_v4l2m2m_rgb24() {
1376                self.register(FourCc::new(*b"RG24"), Arc::new(enc));
1377            }
1378            if let Ok(enc) = FfmpegH264Encoder::new_v4l2m2m_nv12() {
1379                self.register(FourCc::new(*b"NV12"), Arc::new(enc));
1380            }
1381            self.register(
1382                FourCc::new(*b"YUYV"),
1383                Arc::new(FfmpegH264Encoder::with_options_for_input(
1384                    FourCc::new(*b"YUYV"),
1385                    FfmpegEncoderOptions::default(),
1386                )?),
1387            );
1388            self.register(
1389                FourCc::new(*b"RG24"),
1390                Arc::new(FfmpegH265Encoder::new_rgb24()?),
1391            );
1392            self.register(
1393                FourCc::new(*b"NV12"),
1394                Arc::new(FfmpegH265Encoder::new_nv12()?),
1395            );
1396            if let Ok(enc) = FfmpegH265Encoder::new_v4l2m2m_rgb24() {
1397                self.register(FourCc::new(*b"RG24"), Arc::new(enc));
1398            }
1399            if let Ok(enc) = FfmpegH265Encoder::new_v4l2m2m_nv12() {
1400                self.register(FourCc::new(*b"NV12"), Arc::new(enc));
1401            }
1402            self.register(
1403                FourCc::new(*b"YUYV"),
1404                Arc::new(FfmpegH265Encoder::with_options_for_input(
1405                    FourCc::new(*b"YUYV"),
1406                    FfmpegEncoderOptions::default(),
1407                )?),
1408            );
1409        }
1410
1411        // Optional mozjpeg encoder.
1412        #[cfg(feature = "codec-mozjpeg")]
1413        self.register(
1414            FourCc::new(*b"RG24"),
1415            Arc::new(jpeg_encoder::MozjpegEncoder::new(FourCc::new(*b"RG24"), 85)),
1416        );
1417
1418        // Optional turbojpeg decoder.
1419        #[cfg(feature = "codec-turbojpeg")]
1420        self.register(
1421            FourCc::new(*b"MJPG"),
1422            Arc::new(mjpeg_turbojpeg::TurbojpegDecoder::new(FourCc::new(
1423                *b"RG24",
1424            ))),
1425        );
1426
1427        // Optional zune-jpeg decoder.
1428        #[cfg(feature = "codec-zune")]
1429        self.register(
1430            FourCc::new(*b"MJPG"),
1431            Arc::new(mjpeg_zune::ZuneMjpegDecoder::new(FourCc::new(*b"RG24"))),
1432        );
1433
1434        Ok(())
1435    }
1436
1437    /// List codec descriptors for the currently enabled set without requiring callers to register manually.
1438    pub fn list_enabled_codecs() -> Result<Vec<(FourCc, Vec<CodecDescriptor>)>, CodecError> {
1439        let registry = CodecRegistry::with_enabled_codecs()?;
1440        Ok(registry.handle.list_registered())
1441    }
1442
1443    /// List enabled decoders for the current build (descriptors only).
1444    pub fn list_enabled_decoders() -> Result<Vec<(FourCc, Vec<CodecDescriptor>)>, CodecError> {
1445        let registry = CodecRegistry::with_enabled_codecs()?;
1446        Ok(registry.handle.list_registered_by_kind(CodecKind::Decoder))
1447    }
1448
1449    /// List enabled encoders for the current build (descriptors only).
1450    pub fn list_enabled_encoders() -> Result<Vec<(FourCc, Vec<CodecDescriptor>)>, CodecError> {
1451        let registry = CodecRegistry::with_enabled_codecs()?;
1452        Ok(registry.handle.list_registered_by_kind(CodecKind::Encoder))
1453    }
1454}
1455
1456/// Preference for selecting codecs by FourCc.
1457///
1458/// # Example
1459/// ```rust
1460/// use styx_codec::Preference;
1461///
1462/// let pref = Preference::hardware_biased(vec!["ffmpeg".into()]);
1463/// assert!(pref.prefer_hardware);
1464/// ```
1465#[derive(Clone, Debug, Default)]
1466#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1467#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
1468pub struct Preference {
1469    /// Ordered list of impl names to prefer.
1470    pub impls: Vec<String>,
1471    /// Whether to favor hardware-accelerated impls.
1472    pub prefer_hardware: bool,
1473}
1474
1475impl Preference {
1476    pub fn hardware_biased(impls: Vec<String>) -> Self {
1477        Self {
1478            impls,
1479            prefer_hardware: true,
1480        }
1481    }
1482}
1483
1484/// Typed policy for choosing codecs (impl priorities + hardware bias).
1485///
1486/// # Example
1487/// ```rust
1488/// use styx_codec::CodecPolicy;
1489/// use styx_core::prelude::FourCc;
1490///
1491/// let policy = CodecPolicy::builder(FourCc::new(*b"MJPG"))
1492///     .prefer_hardware(false)
1493///     .build();
1494/// ```
1495#[derive(Clone, Debug)]
1496#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1497#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
1498pub struct CodecPolicy {
1499    fourcc: FourCc,
1500    prefer_hardware: bool,
1501    ordered_impls: Vec<String>,
1502    priorities: std::collections::HashMap<String, i32>,
1503}
1504
1505impl CodecPolicy {
1506    /// Builder entry-point.
1507    pub fn builder(fourcc: FourCc) -> CodecPolicyBuilder {
1508        CodecPolicyBuilder {
1509            fourcc,
1510            prefer_hardware: true,
1511            ordered_impls: Vec::new(),
1512            priorities: std::collections::HashMap::new(),
1513        }
1514    }
1515}
1516
1517/// Builder for codec selection policy.
1518///
1519/// # Example
1520/// ```rust
1521/// use styx_codec::CodecPolicy;
1522/// use styx_core::prelude::FourCc;
1523///
1524/// let policy = CodecPolicy::builder(FourCc::new(*b"MJPG"))
1525///     .ordered_impls(["ffmpeg", "jpeg-decoder"])
1526///     .priority("ffmpeg", 0)
1527///     .build();
1528/// ```
1529pub struct CodecPolicyBuilder {
1530    fourcc: FourCc,
1531    prefer_hardware: bool,
1532    ordered_impls: Vec<String>,
1533    priorities: std::collections::HashMap<String, i32>,
1534}
1535
1536impl CodecPolicyBuilder {
1537    /// Disable or enable hardware bias.
1538    pub fn prefer_hardware(mut self, prefer: bool) -> Self {
1539        self.prefer_hardware = prefer;
1540        self
1541    }
1542
1543    /// Explicit ordered implementations to try first.
1544    pub fn ordered_impls<I, S>(mut self, impls: I) -> Self
1545    where
1546        I: IntoIterator<Item = S>,
1547        S: Into<String>,
1548    {
1549        self.ordered_impls = impls.into_iter().map(|s| s.into()).collect();
1550        self
1551    }
1552
1553    /// Set an integer priority for an impl name (lower wins).
1554    pub fn priority<S: Into<String>>(mut self, impl_name: S, priority: i32) -> Self {
1555        self.priorities
1556            .insert(impl_name.into().to_ascii_lowercase(), priority);
1557        self
1558    }
1559
1560    pub fn build(self) -> CodecPolicy {
1561        CodecPolicy {
1562            fourcc: self.fourcc,
1563            prefer_hardware: self.prefer_hardware,
1564            ordered_impls: self.ordered_impls,
1565            priorities: self.priorities,
1566        }
1567    }
1568}
1569
1570pub mod decoder;
1571pub mod encoder;
1572#[cfg(feature = "codec-ffmpeg")]
1573pub mod ffmpeg;
1574#[cfg(feature = "image")]
1575pub mod image_any;
1576#[cfg(feature = "image")]
1577pub mod image_utils;
1578#[cfg(feature = "codec-mozjpeg")]
1579pub mod jpeg_encoder;
1580pub mod mjpeg;
1581#[cfg(feature = "codec-turbojpeg")]
1582pub mod mjpeg_turbojpeg;
1583#[cfg(feature = "codec-zune")]
1584pub mod mjpeg_zune;
1585
1586pub mod prelude {
1587    pub use crate::decoder::raw::{
1588        BgrToRgbDecoder, BgraToRgbDecoder, I420ToRgbDecoder, Nv12ToBgrDecoder, Nv12ToRgbDecoder,
1589        PassthroughDecoder, RgbaToRgbDecoder, YuyvToRgbDecoder,
1590    };
1591    #[cfg(feature = "codec-ffmpeg")]
1592    pub use crate::ffmpeg::{
1593        FfmpegH264Decoder, FfmpegH264Encoder, FfmpegH265Decoder, FfmpegH265Encoder,
1594        FfmpegMjpegDecoder, FfmpegMjpegEncoder,
1595    };
1596    #[cfg(feature = "image")]
1597    pub use crate::image_any::ImageAnyDecoder;
1598    #[cfg(feature = "image")]
1599    pub use crate::image_utils::{CodecImageExt, dynamic_image_to_frame};
1600    #[cfg(feature = "codec-mozjpeg")]
1601    pub use crate::jpeg_encoder::MozjpegEncoder;
1602    #[cfg(feature = "codec-turbojpeg")]
1603    pub use crate::mjpeg_turbojpeg::TurbojpegDecoder;
1604    #[cfg(feature = "codec-zune")]
1605    pub use crate::mjpeg_zune::ZuneMjpegDecoder;
1606    pub use crate::{
1607        Codec, CodecDescriptor, CodecError, CodecKind, CodecPolicy, CodecPolicyBuilder,
1608        CodecRegistry, CodecRegistryHandle, CodecStats, RegistryError, mjpeg::MjpegDecoder,
1609    };
1610    pub use styx_capture::prelude::*;
1611    #[allow(unused_imports)]
1612    pub use styx_core::prelude::*;
1613}
1614
1615#[cfg(test)]
1616mod tests {
1617    use super::*;
1618    use std::sync::Arc;
1619
1620    struct Rg24Passthrough {
1621        descriptor: CodecDescriptor,
1622    }
1623
1624    impl Default for Rg24Passthrough {
1625        fn default() -> Self {
1626            Self {
1627                descriptor: CodecDescriptor {
1628                    kind: CodecKind::Encoder,
1629                    input: FourCc::new(*b"RG24"),
1630                    output: FourCc::new(*b"RG24"),
1631                    name: "passthrough",
1632                    impl_name: "test",
1633                },
1634            }
1635        }
1636    }
1637
1638    impl Codec for Rg24Passthrough {
1639        fn descriptor(&self) -> &CodecDescriptor {
1640            &self.descriptor
1641        }
1642
1643        fn process(&self, input: FrameLease) -> Result<FrameLease, CodecError> {
1644            if input.meta().format.code != self.descriptor.input {
1645                return Err(CodecError::FormatMismatch {
1646                    expected: self.descriptor.input,
1647                    actual: input.meta().format.code,
1648                });
1649            }
1650            Ok(input)
1651        }
1652    }
1653
1654    #[test]
1655    fn auto_converts_rgba_to_rg24_for_rg24_codecs() {
1656        let registry = CodecRegistry::with_enabled_codecs_for_max(8, 8).expect("registry");
1657        registry.register(FourCc::new(*b"RG24"), Arc::new(Rg24Passthrough::default()));
1658        let handle = registry.handle();
1659
1660        let res = Resolution::new(2, 2).unwrap();
1661        let layout = plane_layout_from_dims(res.width, res.height, 4);
1662        let pool = BufferPool::with_limits(1, layout.len, 4);
1663        let mut buf = pool.lease();
1664        buf.resize(layout.len);
1665        for (i, b) in buf.as_mut_slice().iter_mut().enumerate() {
1666            *b = i as u8;
1667        }
1668        let format = MediaFormat::new(FourCc::new(*b"RGBA"), res, ColorSpace::Srgb);
1669        let frame =
1670            FrameLease::single_plane(FrameMeta::new(format, 0), buf, layout.len, layout.stride);
1671
1672        let out = handle
1673            .process_named(FourCc::new(*b"RG24"), "test", frame)
1674            .expect("process");
1675        assert_eq!(out.meta().format.code, FourCc::new(*b"RG24"));
1676        assert_eq!(out.meta().format.resolution.width.get(), 2);
1677        assert_eq!(out.meta().format.resolution.height.get(), 2);
1678
1679        let data = out.planes().first().unwrap().data();
1680        assert_eq!(data.len(), 2 * 2 * 3);
1681
1682        let expected: Vec<u8> = (0u8..16)
1683            .collect::<Vec<_>>()
1684            .chunks_exact(4)
1685            .flat_map(|px| px[..3].iter().copied())
1686            .collect();
1687        assert_eq!(data, expected.as_slice());
1688    }
1689}