Skip to main content

oxideav_core/
capabilities.rs

1//! Codec capability description.
2//!
3//! Each codec implementation registered with the codec registry attaches one
4//! of these structs to declare what it can do, what its constraints are, and
5//! how the registry should rank it against alternative implementations of
6//! the same codec id.
7//!
8//! The flag layout is a 6-column capability string (one letter per
9//! capability, `.` when absent):
10//!
11//! ```text
12//!  D..... = Decoding supported
13//!  .E.... = Encoding supported
14//!  ..V... = Video codec       ..A... = Audio       ..S... = Subtitle
15//!  ..D... = Data              ..T... = Attachment
16//!  ...I.. = Intra-frame-only codec
17//!  ....L. = Lossy compression
18//!  .....S = Lossless compression
19//! ```
20
21use std::fmt;
22
23use crate::blob::BlobKind;
24use crate::format::{MediaType, PixelFormat, SampleFormat};
25
26/// Default priority for software implementations. Lower numbers are preferred
27/// at resolution time, so register hardware impls with a smaller value (e.g.
28/// `10`) and software fallbacks with the default `100`.
29///
30/// The same value and the same "lower is preferred" convention serve as
31/// the default *resolution* priority for every registry tie-break
32/// (container probes, codec tag / payload-magic claims) — see the
33/// crate-level "Resolution order" section.
34pub const DEFAULT_PRIORITY: i32 = 100;
35
36/// What an implementation can do plus how it ranks vs alternatives.
37///
38/// `#[non_exhaustive]`: build it with [`audio`](Self::audio),
39/// [`video`](Self::video) or [`subtitle`](Self::subtitle) and the
40/// `with_*` builders, then read the public fields. Fields are added
41/// over time (0.1.38 added the accepted sample rates / formats) and a
42/// struct literal in a downstream crate would stop compiling each time.
43#[derive(Clone, Debug)]
44#[non_exhaustive]
45pub struct CodecCapabilities {
46    /// Decoding supported by this implementation.
47    pub decode: bool,
48    /// Encoding supported by this implementation.
49    pub encode: bool,
50    /// Media type this implementation handles (audio, video, ...).
51    pub media_type: MediaType,
52    /// Every coded unit is independently decodable (no inter-frame
53    /// prediction).
54    pub intra_only: bool,
55    /// Supports lossy compression.
56    pub lossy: bool,
57    /// Supports lossless compression. `lossy` and `lossless` may both
58    /// be set for codecs that offer both modes.
59    pub lossless: bool,
60    /// Hardware-accelerated implementation (VAAPI/NVENC/QSV/VideoToolbox/...).
61    pub hardware_accelerated: bool,
62    /// Short identifier for this implementation, e.g. "flac_sw", "h264_qsv".
63    pub implementation: String,
64    /// Restrictions — `None` means "no constraint".
65    pub max_width: Option<u32>,
66    /// Maximum supported frame height in pixels; `None` = unconstrained.
67    pub max_height: Option<u32>,
68    /// Maximum supported bit rate in bits per second; `None` =
69    /// unconstrained.
70    pub max_bitrate: Option<u64>,
71    /// Maximum supported audio sample rate in Hz; `None` = unconstrained.
72    pub max_sample_rate: Option<u32>,
73    /// Maximum supported audio channel count; `None` = unconstrained.
74    pub max_channels: Option<u16>,
75    /// Lower numbers are preferred. HW impls should be ~10, SW impls ~100.
76    pub priority: i32,
77    /// Pixel formats this implementation accepts (video only). An empty
78    /// `Vec` means "any format" — resolution won't filter on it. When
79    /// populated, the registry can skip impls whose accepted set does not
80    /// include the format requested by the caller.
81    pub accepted_pixel_formats: Vec<PixelFormat>,
82    /// Audio sample rates (Hz) this implementation accepts as encoder
83    /// input (audio only). An empty `Vec` means "any rate". When
84    /// populated, a pipeline feeding the encoder is expected to resample
85    /// a stream whose rate is not listed to one of these rates (the
86    /// listed order is not a preference — consumers pick the closest
87    /// rate that does not discard bandwidth).
88    pub accepted_sample_rates: Vec<u32>,
89    /// Audio sample formats this implementation accepts as encoder input
90    /// (audio only). An empty `Vec` means "any format". When populated,
91    /// the first entry is the preferred conversion target for a stream
92    /// whose format is not listed.
93    pub accepted_sample_formats: Vec<SampleFormat>,
94    /// Metadata blob kinds this implementation's **encoder** embeds in
95    /// its output: the [`BlobKind`]s it reads from the input stream's
96    /// [`CodecParameters::blobs`](crate::CodecParameters::blobs) and
97    /// from frame records and writes into the coded output (an ICC
98    /// profile into the codestream's colour box, an Exif block into
99    /// its application segment, a format's own custom kind into its
100    /// native chunk). Empty — the default — means "nothing declared":
101    /// a caller cannot tell whether a blob it hands over survives, and
102    /// a gateway reports that as pass-through. When populated it is a
103    /// promise in both directions: every listed kind is embedded when
104    /// present on the input, every unlisted kind is dropped. Lets a
105    /// caller answer "will the ICC profile survive this save?" before
106    /// encoding a pixel. Attached with
107    /// [`with_embedded_blob_kind`](Self::with_embedded_blob_kind) /
108    /// [`with_embedded_blob_kinds`](Self::with_embedded_blob_kinds),
109    /// queried with [`embeds_blob`](Self::embeds_blob). Decoder-only
110    /// registrations leave it empty.
111    pub embedded_blob_kinds: Vec<BlobKind>,
112}
113
114impl CodecCapabilities {
115    /// Construct a software audio decoder/encoder capability set with sensible
116    /// defaults — adjust fields after creation.
117    pub fn audio(implementation: impl Into<String>) -> Self {
118        Self {
119            decode: false,
120            encode: false,
121            media_type: MediaType::Audio,
122            intra_only: true, // audio packets are independently decodable in most codecs
123            lossy: false,
124            lossless: false,
125            hardware_accelerated: false,
126            implementation: implementation.into(),
127            max_width: None,
128            max_height: None,
129            max_bitrate: None,
130            max_sample_rate: None,
131            max_channels: None,
132            priority: DEFAULT_PRIORITY,
133            accepted_pixel_formats: Vec::new(),
134            accepted_sample_rates: Vec::new(),
135            accepted_sample_formats: Vec::new(),
136            embedded_blob_kinds: Vec::new(),
137        }
138    }
139
140    /// Construct a software video decoder/encoder capability set with
141    /// sensible defaults — adjust fields after creation.
142    pub fn video(implementation: impl Into<String>) -> Self {
143        Self {
144            decode: false,
145            encode: false,
146            media_type: MediaType::Video,
147            intra_only: false,
148            lossy: false,
149            lossless: false,
150            hardware_accelerated: false,
151            implementation: implementation.into(),
152            max_width: None,
153            max_height: None,
154            max_bitrate: None,
155            max_sample_rate: None,
156            max_channels: None,
157            priority: DEFAULT_PRIORITY,
158            accepted_pixel_formats: Vec::new(),
159            accepted_sample_rates: Vec::new(),
160            accepted_sample_formats: Vec::new(),
161            embedded_blob_kinds: Vec::new(),
162        }
163    }
164
165    /// Construct a software subtitle decoder/encoder capability set with
166    /// sensible defaults (intra-only, lossless) — adjust with the
167    /// `with_*` builders.
168    pub fn subtitle(implementation: impl Into<String>) -> Self {
169        let mut caps = Self::audio(implementation);
170        caps.media_type = MediaType::Subtitle;
171        caps.intra_only = true;
172        caps.lossless = true;
173        caps
174    }
175
176    /// Override the media type (for capability sets built from the
177    /// closest constructor, e.g. data or attachment streams).
178    pub fn with_media_type(mut self, media_type: MediaType) -> Self {
179        self.media_type = media_type;
180        self
181    }
182
183    /// 6-character capability flag string (see the module docs for the
184    /// column layout). Useful for `oxideav list`-style
185    /// output.
186    pub fn flag_string(&self) -> String {
187        let mut s = String::with_capacity(6);
188        s.push(if self.decode { 'D' } else { '.' });
189        s.push(if self.encode { 'E' } else { '.' });
190        s.push(match self.media_type {
191            MediaType::Video => 'V',
192            MediaType::Audio => 'A',
193            MediaType::Subtitle => 'S',
194            MediaType::Data => 'D',
195            MediaType::Unknown => '.',
196        });
197        s.push(if self.intra_only { 'I' } else { '.' });
198        s.push(if self.lossy { 'L' } else { '.' });
199        s.push(if self.lossless { 'S' } else { '.' });
200        s
201    }
202
203    // Builder-style helpers so registrations stay compact.
204
205    /// Mark this implementation as supporting decode.
206    pub fn with_decode(mut self) -> Self {
207        self.decode = true;
208        self
209    }
210    /// Mark this implementation as supporting encode.
211    pub fn with_encode(mut self) -> Self {
212        self.encode = true;
213        self
214    }
215    /// Set the intra-frame-only flag.
216    pub fn with_intra_only(mut self, v: bool) -> Self {
217        self.intra_only = v;
218        self
219    }
220    /// Set the lossy-compression flag.
221    pub fn with_lossy(mut self, v: bool) -> Self {
222        self.lossy = v;
223        self
224    }
225    /// Set the lossless-compression flag.
226    pub fn with_lossless(mut self, v: bool) -> Self {
227        self.lossless = v;
228        self
229    }
230    /// Set the hardware-accelerated flag.
231    pub fn with_hardware(mut self, v: bool) -> Self {
232        self.hardware_accelerated = v;
233        self
234    }
235    /// Set the registry ranking priority (lower is preferred; HW ~10,
236    /// SW ~100).
237    pub fn with_priority(mut self, p: i32) -> Self {
238        self.priority = p;
239        self
240    }
241    /// Constrain the maximum frame size to `w` × `h` pixels.
242    pub fn with_max_size(mut self, w: u32, h: u32) -> Self {
243        self.max_width = Some(w);
244        self.max_height = Some(h);
245        self
246    }
247    /// Constrain the maximum bit rate (bits per second).
248    pub fn with_max_bitrate(mut self, br: u64) -> Self {
249        self.max_bitrate = Some(br);
250        self
251    }
252    /// Constrain the maximum audio sample rate (Hz).
253    pub fn with_max_sample_rate(mut self, sr: u32) -> Self {
254        self.max_sample_rate = Some(sr);
255        self
256    }
257    /// Constrain the maximum audio channel count.
258    pub fn with_max_channels(mut self, ch: u16) -> Self {
259        self.max_channels = Some(ch);
260        self
261    }
262
263    /// Add one accepted pixel format. Appends — call multiple times to
264    /// list several.
265    pub fn with_pixel_format(mut self, fmt: PixelFormat) -> Self {
266        self.accepted_pixel_formats.push(fmt);
267        self
268    }
269
270    /// Replace the accepted pixel-format set wholesale.
271    pub fn with_pixel_formats(mut self, fmts: Vec<PixelFormat>) -> Self {
272        self.accepted_pixel_formats = fmts;
273        self
274    }
275
276    /// Replace the accepted encoder-input sample-rate set wholesale
277    /// (see [`Self::accepted_sample_rates`]).
278    pub fn with_sample_rates(mut self, rates: Vec<u32>) -> Self {
279        self.accepted_sample_rates = rates;
280        self
281    }
282
283    /// Replace the accepted encoder-input sample-format set wholesale
284    /// (see [`Self::accepted_sample_formats`]; the first entry is the
285    /// preferred conversion target).
286    pub fn with_sample_formats(mut self, fmts: Vec<SampleFormat>) -> Self {
287        self.accepted_sample_formats = fmts;
288        self
289    }
290
291    /// Add one metadata blob kind the encoder embeds (see
292    /// [`Self::embedded_blob_kinds`]). Appends — call several times to
293    /// list several; a kind already listed is not added twice.
294    pub fn with_embedded_blob_kind(mut self, kind: impl Into<BlobKind>) -> Self {
295        let kind = kind.into();
296        if !self.embedded_blob_kinds.contains(&kind) {
297            self.embedded_blob_kinds.push(kind);
298        }
299        self
300    }
301
302    /// Replace the embedded blob-kind set wholesale (see
303    /// [`Self::embedded_blob_kinds`]). Duplicates are removed, first
304    /// occurrence kept.
305    pub fn with_embedded_blob_kinds(mut self, kinds: Vec<BlobKind>) -> Self {
306        self.embedded_blob_kinds.clear();
307        for kind in kinds {
308            if !self.embedded_blob_kinds.contains(&kind) {
309                self.embedded_blob_kinds.push(kind);
310            }
311        }
312        self
313    }
314
315    /// `true` when the encoder declares that it embeds blobs of `kind`
316    /// (see [`Self::embedded_blob_kinds`]). `false` both for an
317    /// unlisted kind and for an encoder that declares nothing — use
318    /// [`declares_embedded_blobs`](Self::declares_embedded_blobs) to
319    /// tell the two apart.
320    pub fn embeds_blob(&self, kind: &BlobKind) -> bool {
321        self.embedded_blob_kinds.contains(kind)
322    }
323
324    /// `true` when the encoder declares its embedded blob kinds at
325    /// all, i.e. [`Self::embedded_blob_kinds`] is non-empty; `false`
326    /// means every answer from [`embeds_blob`](Self::embeds_blob) is
327    /// "unknown", not "no".
328    pub fn declares_embedded_blobs(&self) -> bool {
329        !self.embedded_blob_kinds.is_empty()
330    }
331
332    /// Pick the encoder-input sample rate for a stream at `rate` Hz:
333    /// `rate` itself when it is accepted (or every rate is), otherwise
334    /// the smallest accepted rate above it (no bandwidth is lost), and
335    /// failing that the largest accepted rate.
336    pub fn pick_sample_rate(&self, rate: u32) -> u32 {
337        if self.accepted_sample_rates.is_empty() || self.accepted_sample_rates.contains(&rate) {
338            return rate;
339        }
340        let above = self
341            .accepted_sample_rates
342            .iter()
343            .copied()
344            .filter(|&r| r > rate)
345            .min();
346        above.unwrap_or_else(|| {
347            self.accepted_sample_rates
348                .iter()
349                .copied()
350                .max()
351                .unwrap_or(rate)
352        })
353    }
354}
355
356impl fmt::Display for CodecCapabilities {
357    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358        write!(f, "{} {}", self.flag_string(), self.implementation)
359    }
360}
361
362impl CodecCapabilities {
363    /// Whether this implementation's max-* restrictions are compatible
364    /// with the requested codec parameters. `for_encode` is reserved
365    /// for restrictions that apply asymmetrically. Used by the
366    /// registry's `make_decoder` / `make_encoder` walker and by
367    /// out-of-tree selection layers (e.g. `oxideav-pipeline`'s
368    /// `CodecPreferences` filter).
369    pub fn fits_params(&self, p: &crate::CodecParameters, for_encode: bool) -> bool {
370        let _ = for_encode;
371        if let (Some(max), Some(w)) = (self.max_width, p.width) {
372            if w > max {
373                return false;
374            }
375        }
376        if let (Some(max), Some(h)) = (self.max_height, p.height) {
377            if h > max {
378                return false;
379            }
380        }
381        if let (Some(max), Some(br)) = (self.max_bitrate, p.bit_rate) {
382            if br > max {
383                return false;
384            }
385        }
386        if let (Some(max), Some(sr)) = (self.max_sample_rate, p.sample_rate) {
387            if sr > max {
388                return false;
389            }
390        }
391        if let (Some(max), Some(ch)) = (self.max_channels, p.channels) {
392            if ch > max {
393                return false;
394            }
395        }
396        true
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403
404    #[test]
405    fn pick_sample_rate_prefers_exact_then_next_above_then_max() {
406        let any = CodecCapabilities::audio("any");
407        assert_eq!(any.pick_sample_rate(44_100), 44_100);
408        let opus = CodecCapabilities::audio("opus")
409            .with_sample_rates(vec![8_000, 12_000, 16_000, 24_000, 48_000]);
410        assert_eq!(opus.pick_sample_rate(16_000), 16_000);
411        assert_eq!(opus.pick_sample_rate(44_100), 48_000);
412        assert_eq!(opus.pick_sample_rate(11_025), 12_000);
413        assert_eq!(opus.pick_sample_rate(96_000), 48_000);
414    }
415
416    #[test]
417    fn embedded_blob_kinds_default_empty_and_builders_dedupe() {
418        for caps in [
419            CodecCapabilities::audio("a"),
420            CodecCapabilities::video("v"),
421            CodecCapabilities::subtitle("s"),
422        ] {
423            assert!(caps.embedded_blob_kinds.is_empty(), "{caps}");
424            assert!(!caps.declares_embedded_blobs(), "{caps}");
425            assert!(!caps.embeds_blob(&BlobKind::ICC), "{caps}");
426        }
427        let c = CodecCapabilities::video("png")
428            .with_encode()
429            .with_embedded_blob_kind(BlobKind::ICC)
430            .with_embedded_blob_kind(BlobKind::EXIF)
431            .with_embedded_blob_kind("png-text")
432            .with_embedded_blob_kind(BlobKind::ICC);
433        assert_eq!(
434            c.embedded_blob_kinds,
435            vec![BlobKind::ICC, BlobKind::EXIF, BlobKind::custom("png-text")]
436        );
437        assert!(c.declares_embedded_blobs());
438        assert!(c.embeds_blob(&BlobKind::ICC));
439        assert!(c.embeds_blob(&BlobKind::custom("png-text")));
440        assert!(!c.embeds_blob(&BlobKind::XMP));
441        // Wholesale replacement dedupes and keeps first occurrences.
442        let c = c.with_embedded_blob_kinds(vec![BlobKind::XMP, BlobKind::IPTC, BlobKind::XMP]);
443        assert_eq!(c.embedded_blob_kinds, vec![BlobKind::XMP, BlobKind::IPTC]);
444        assert!(!c.embeds_blob(&BlobKind::ICC));
445        // Replacing with nothing returns to "undeclared".
446        let c = c.with_embedded_blob_kinds(Vec::new());
447        assert!(!c.declares_embedded_blobs());
448        // The declaration is orthogonal to every other field and to
449        // the flag string.
450        assert_eq!(c.flag_string(), ".EV...");
451    }
452
453    #[test]
454    fn sample_format_builder_replaces_set() {
455        let c = CodecCapabilities::audio("x")
456            .with_sample_formats(vec![SampleFormat::S16, SampleFormat::F32]);
457        assert_eq!(
458            c.accepted_sample_formats,
459            vec![SampleFormat::S16, SampleFormat::F32]
460        );
461    }
462}