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}