re_video/decode/mod.rs
1//! Video frame decoding.
2//! =========================
3//!
4//! Whirlwind tour of how to interpret picture data (from a Video perspective)
5//! ---------------------------------------------------------------------------------
6//!
7//! Extracted from the [av1 codec wiki](https://web.archive.org/web/20260318141718/https://wiki.x266.mov/docs/colorimetry/intro) and other sources.
8//! Follows the trail of information we get from our AV1 decoder.
9//!
10//! ### How to get from YUV to RGB?
11//!
12//! Things to know about the incoming yuv data:
13//! * `picture.bit_depth()`
14//! * is either 8 or 16
15//! * that's how the decoder stores for us but the per component we have either 8 or 10 or 12 bits -> see `picture.bits_per_component()`
16//! * `picture.pixel_layout()`
17//! * `4:0:0` grayscale
18//! * `4:2:0` half horizontal and half vertical resolution for chroma
19//! * `4:2:2` half horizontal resolution for chroma
20//! * `4:4:4` full resolution for chroma
21//! * note that the AV1 decoder gives us always (!) planar data
22//! * `picture.color_range()`
23//! * yuv data range may be either `limited` or `full`
24//! * `full` is what you'd naively expect, just full use up the entire 8/10/12 bits!
25//! * `limited` means that only a certain range of values is valid
26//! * weirdly enough, DO NOT CLAMP! a lot of software may say it's limited but then use the so-called foot and head space anyways to go outside the regular colors
27//! * reportedly (read this on some forums ;-)) some players _do_ clamp, so let's not get too concerned about this
28//! * it's a remnant of the analog age, but it's still very common!
29//!
30//! ### Given a normalized YUV triplet, how do we get color?
31//!
32//! * `picture.matrix_coefficients()` (see <https://web.archive.org/web/20260318141742/https://wiki.x266.mov/docs/colorimetry/matrix>)
33//! * this tells us what to multiply the incoming YUV data with to get SOME RGB data
34//! * there's various standards of how to do this, but the most common is BT.709
35//! * here's a fun special one: `identity` means it's not actually YUV, but GBR!
36//! * `picture.primaries()`
37//! * now we have RGB but we kinda have no idea what that means!
38//! * the color primaries tell us which space we're in
39//! * …meaning that if the primaries are anything else we'd have to do some conversion BUT
40//! it also means that we have no chance of displaying the picture perfectly on a screen taking in sRGB (or any other not-matching color space)
41//! * [Wikipedia says](https://en.wikipedia.org/wiki/Rec._709#Relationship_to_sRGB) sRGB uses the same primaries as BT.709
42//! * but I also found other sources (e.g. [this forum post](https://forum.doom9.org/showthread.php?p=1640342#post1640342))
43//! clamining that they're just close enough to be considered the same for practical purposes
44//! * `picture.transfer_characteristics()`
45//! * until this point everything is "gamma compressed", or more accurately, went through Opto Electric Transfer Function (OETF)
46//! * i.e. measure of light in, electronic signal out
47//! * we have to keep in mind the EOTF that our screen at the other end will use which for today's renderpipeline is always sRGB
48//! (meaning it's a 2.2 gamma curve with a small linear part)
49//! * Similar to the primaries, BT.709 uses a _similar_ transfer function as sRGB, but not exactly the same
50//! <https://www.image-engineering.de/library/technotes/714-color-spaces-rec-709-vs-srgb>
51//! * There's reason to believe players just ignore this:
52//! * From a [VLC issue](https://code.videolan.org/videolan/vlc/-/issues/26999):
53//! > We do not support transfers or primaries anyway, so it does not matter
54//! > (we do support HDR transfer functions PQ and HLG, not SDR ones and we support BT.2020 primaries, but not SMPTE C (which is what BT.601 NTSC is))."
55//! * …I'm sure I found a report of other video players ignoring this and most of everything except `matrix_coefficients` but I can't find it anymore :(
56//!
57//! All of the above are completely optional for a video to specify and there's sometimes some interplay of relationships with those.
58//! (a standard would often specify several things at once, there's typical and less typical combinations)
59//! So naturally, people will use terms sloppily and interchangeably,
60//! If anything is lacking a video player has to make a guess.
61//! … and as discussed above, even it's there, often video players tend to ignore some settings!
62//!
63//! With all this out of the way…
64//!
65//! ### What's the state of us making use of all these things?
66//!
67//! * ❌ `picture.bit_depth()`
68//! * TODO(#7594): ignored, we just pretend everything is 8 bits
69//! * ✅ `picture.pixel_layout()`
70//! * ✅ `picture.color_range()`
71//! * 🟧 `picture.matrix_coefficients()`
72//! * we try to figure out whether to use `BT.709` or `BT.601` coefficients, using other characteristics for guessing if nothing else is available.
73//! * ❌ `picture.primaries()`
74//! * ❌ `picture.transfer_characteristics()`
75//!
76//! We'll very likely be good with this until either we get specific feature requests and/or we'll start
77//! supporting HDR content at which point more properties will be important!
78//!
79
80mod sync_decoder;
81
82#[cfg_attr(target_arch = "wasm32", path = "sync_decoder_wrapper_wasm.rs")]
83#[cfg_attr(not(target_arch = "wasm32"), path = "sync_decoder_wrapper_native.rs")]
84mod sync_decoder_wrapper;
85
86mod image_decoder;
87
88#[cfg(with_dav1d)]
89mod av1;
90
91#[cfg(with_ffmpeg)]
92mod ffmpeg_cli;
93
94#[cfg(with_ffmpeg)]
95pub use ffmpeg_cli::FFmpegCliDecoder;
96#[cfg(with_ffmpeg)]
97pub use ffmpeg_cli::{
98 Error as FFmpegError, FFmpegVersion, FFmpegVersionParseError, TranscodedMp4,
99 ffmpeg_download_url, transcode_mp4,
100};
101
102#[cfg(target_arch = "wasm32")]
103mod web_image_decoder;
104#[cfg(target_arch = "wasm32")]
105mod webcodecs;
106
107#[cfg(target_arch = "wasm32")]
108pub use webcodecs::WebVideoFrame;
109
110mod rvl_decoder;
111
112use crate::{
113 FrameNumber, SampleIndex, Time, VideoDataDescription, player::VideoPlaybackIssueSeverity,
114};
115
116#[derive(thiserror::Error, Debug, Clone, re_byte_size::SizeBytes)]
117pub enum DecodeError {
118 #[error("Waiting for encoding details")]
119 WaitingForCodecDetails,
120
121 #[error("Unsupported codec: {0}")]
122 UnsupportedCodec(#[size_bytes(ignore)] String),
123
124 #[cfg(with_dav1d)]
125 #[error("dav1d: {0}")]
126 Dav1d(
127 #[from]
128 #[size_bytes(ignore)]
129 dav1d::Error,
130 ),
131
132 #[error("To enabled native AV1 decoding, compile Rerun with the `nasm` feature enabled.")]
133 Dav1dWithoutNasm,
134
135 #[error(
136 "Rerun does not yet support native AV1 decoding on Linux ARM64. See https://github.com/rerun-io/rerun/issues/7755"
137 )]
138 NoDav1dOnLinuxArm64,
139
140 #[error("Image decode error: {0}")]
141 ImageDecoder(#[size_bytes(ignore)] String),
142
143 #[error(transparent)]
144 RvlDecoder(#[size_bytes(ignore)] re_rvl::RvlDecodeError),
145
146 #[cfg(target_arch = "wasm32")]
147 #[error(transparent)]
148 WebDecoder(
149 #[from]
150 #[size_bytes(ignore)]
151 webcodecs::WebError,
152 ),
153
154 #[cfg(with_ffmpeg)]
155 #[error(transparent)]
156 Ffmpeg(#[size_bytes(ignore)] std::sync::Arc<FFmpegError>),
157
158 #[error("Unsupported bits per component: {0}")]
159 BadBitsPerComponent(#[size_bytes(ignore)] usize),
160}
161
162impl DecodeError {
163 pub fn should_request_more_frames(&self) -> bool {
164 // Decoders often (not always!) recover from errors and will succeed eventually.
165 // Gotta keep trying!
166 match self {
167 // Unsupported codec / decoder not available:
168 Self::WaitingForCodecDetails
169 | Self::UnsupportedCodec(_)
170 | Self::Dav1dWithoutNasm
171 | Self::NoDav1dOnLinuxArm64
172 | Self::RvlDecoder(_) => false,
173
174 // Issue with AV1 decoding.
175 #[cfg(with_dav1d)]
176 Self::Dav1d(_) => true,
177
178 Self::ImageDecoder(_) => false,
179
180 // Issue with WebCodecs decoding.
181 #[cfg(target_arch = "wasm32")]
182 Self::WebDecoder(_) => true,
183
184 // Issue with FFmpeg decoding.
185 #[cfg(with_ffmpeg)]
186 Self::Ffmpeg(err) => err.should_request_more_frames(),
187
188 // Unsupported format.
189 Self::BadBitsPerComponent(_) => false,
190 }
191 }
192
193 pub fn severity(&self) -> VideoPlaybackIssueSeverity {
194 match self {
195 Self::WaitingForCodecDetails => VideoPlaybackIssueSeverity::Informational,
196 #[cfg(with_dav1d)]
197 Self::Dav1d(err) => match err {
198 dav1d::Error::Again => VideoPlaybackIssueSeverity::Loading,
199 _ => VideoPlaybackIssueSeverity::Error,
200 },
201 Self::ImageDecoder(_) => VideoPlaybackIssueSeverity::Error,
202 #[cfg(target_arch = "wasm32")]
203 Self::WebDecoder(err) => err.severity(),
204 #[cfg(with_ffmpeg)]
205 Self::Ffmpeg(_) => VideoPlaybackIssueSeverity::Error,
206
207 Self::UnsupportedCodec(_)
208 | Self::Dav1dWithoutNasm
209 | Self::NoDav1dOnLinuxArm64
210 | Self::BadBitsPerComponent(_)
211 | Self::RvlDecoder(_) => VideoPlaybackIssueSeverity::Error,
212 }
213 }
214}
215
216pub type Result<T = (), E = DecodeError> = std::result::Result<T, E>;
217
218pub type FrameResult = Result<Frame>;
219
220/// Interface for an asynchronous video decoder.
221///
222/// Output callback is passed in on creation of a concrete type.
223pub trait AsyncDecoder: Send + Sync {
224 /// Submits a chunk for decoding in the background.
225 ///
226 /// Chunks are expected to come in the order of their decoding timestamp.
227 fn submit_chunk(&mut self, chunk: Chunk) -> Result<()>;
228
229 /// Called after submitting the last chunk.
230 ///
231 /// Should flush all pending frames.
232 /// If you plan on sending more chunks after calling `end_of_video`,
233 /// you MUST call [`Self::reset`] FIRST.
234 ///
235 /// Implementation note:
236 /// As of writing there's two decoders that have requirements on what happens for new frames after `end_of_video`
237 /// * WebCodec: The next submitted chunk has to be a key frame.
238 /// * FFmpeg-executable: We've shut down stdin, thus we need to restart the process. Doing this without the full context of `reset` is not possible right now.
239 fn end_of_video(&mut self) -> Result<()> {
240 Ok(())
241 }
242
243 /// Resets the decoder.
244 ///
245 /// Expected to be called for backward seeking and major jumps forward in the video.
246 /// Newly created decoder can assume to get reset at least once before any chunks are submitted.
247 ///
248 /// This does not block, all chunks sent to `decode` before this point will be discarded.
249 /// Previously missing [`VideoDataDescription::encoding_details`] may be present now.
250 fn reset(&mut self, video_descr: &VideoDataDescription) -> Result<()>;
251
252 /// Minimum number of samples the decoder requests to stay head of the currently requested sample.
253 ///
254 /// I.e. if sample N is requested, then the encoder would like to see at least all the samples from
255 /// [start of N's GOP] until [N + `min_num_samples_to_enqueue_ahead`].
256 /// Codec specific constraints regarding what samples can be decoded (samples may depend on other samples in their GOP)
257 /// still apply independently of this.
258 ///
259 /// This can be used as a workaround for decoders that are known to need additional samples to produce outputs.
260 fn min_num_samples_to_enqueue_ahead(&self) -> usize {
261 0
262 }
263}
264
265/// Creates a new async decoder for the given `video` data.
266pub fn new_decoder(
267 debug_name: &str,
268 video: &crate::VideoDataDescription,
269 decode_settings: &DecodeSettings,
270 output_sender: crate::Sender<FrameResult>,
271) -> Result<Box<dyn AsyncDecoder>> {
272 #![allow(clippy::allow_attributes, unused_variables, clippy::needless_return)] // With some feature flags
273
274 re_tracing::profile_function!();
275
276 re_log::trace!(
277 "Looking for decoder for {}",
278 video.human_readable_codec_string()
279 );
280
281 cfg_select! {
282 target_arch = "wasm32" => {
283 match &video.codec {
284 crate::VideoCodec::ImageSequence(codec) => {
285 if codec.as_deref() == Some("application/rvl") {
286 Ok(Box::new(sync_decoder_wrapper::SyncDecoderWrapper::new(
287 "rvl decoder".to_owned(),
288 Box::new(rvl_decoder::RvlDecoder),
289 output_sender,
290 )))
291 } else if let Some(decoder) =
292 web_image_decoder::WebImageDecoder::try_new(video, output_sender.clone())
293 {
294 Ok(Box::new(decoder))
295 } else {
296 Err(DecodeError::WaitingForCodecDetails)
297 }
298 }
299 _ => Ok(Box::new(webcodecs::WebVideoDecoder::new(
300 video,
301 decode_settings.hw_acceleration,
302 output_sender,
303 )?)),
304 }
305 }
306 _ => {
307 match &video.codec {
308 #[cfg(feature = "av1")]
309 crate::VideoCodec::AV1 => {
310 #[cfg(linux_arm64)]
311 {
312 return Err(DecodeError::NoDav1dOnLinuxArm64);
313 }
314
315 #[cfg(with_dav1d)]
316 {
317 re_log::trace!("Decoding AV1…");
318 return Ok(Box::new(sync_decoder_wrapper::SyncDecoderWrapper::new(
319 debug_name.to_owned(),
320 Box::new(av1::SyncDav1dDecoder::new(debug_name.to_owned())?),
321 output_sender,
322 )));
323 }
324 }
325
326 #[cfg(with_ffmpeg)]
327 crate::VideoCodec::H264
328 | crate::VideoCodec::H265
329 | crate::VideoCodec::VP8
330 | crate::VideoCodec::VP9 => Ok(Box::new(FFmpegCliDecoder::new(
331 debug_name.to_owned(),
332 video.encoding_details.as_ref(),
333 output_sender,
334 decode_settings.ffmpeg_path.clone(),
335 &video.codec,
336 )?)),
337
338 crate::VideoCodec::ImageSequence(codec) => {
339 if codec.as_deref() == Some("application/rvl") {
340 Ok(Box::new(sync_decoder_wrapper::SyncDecoderWrapper::new(
341 "rvl decoder".to_owned(),
342 Box::new(rvl_decoder::RvlDecoder),
343 output_sender,
344 )))
345 } else if let Some(decoder) = image_decoder::SyncImageDecoder::try_new(video) {
346 Ok(Box::new(sync_decoder_wrapper::SyncDecoderWrapper::new(
347 format!("image decoder ({})", decoder.mime_type()),
348 Box::new(decoder),
349 output_sender,
350 )))
351 } else {
352 Err(DecodeError::WaitingForCodecDetails)
353 }
354 }
355
356 #[cfg(not(all(feature = "av1", with_ffmpeg)))]
357 _ => Err(DecodeError::UnsupportedCodec(
358 video.human_readable_codec_string(),
359 )),
360 }
361 }
362 }
363}
364
365/// One chunk of encoded video data, representing a single [`crate::SampleMetadata`].
366///
367/// For details on how to interpret the data, see [`crate::SampleMetadata`].
368///
369/// In MP4, one sample is one frame.
370#[derive(re_byte_size::SizeBytes)]
371pub struct Chunk {
372 /// The start of a new group of pictures?
373 ///
374 /// This probably means this is a _keyframe_, and that and entire frame
375 /// can be decoded from only this one sample (though I'm not 100% sure).
376 pub is_sync: bool,
377
378 pub data: Vec<u8>,
379
380 /// Which sample (frame) did this chunk come from?
381 ///
382 /// This is the order of which the samples appear in the container,
383 /// which is usually ordered by [`Self::decode_timestamp`].
384 pub sample_idx: SampleIndex,
385
386 /// Which frame does this chunk belong to?
387 ///
388 /// This is on the assumption that each sample produces a single frame,
389 /// which is true for MP4.
390 ///
391 /// This is the index of samples ordered by [`Self::presentation_timestamp`].
392 ///
393 /// Do *not* use this to index into the video data description!
394 /// Use [`Self::sample_idx`] instead.
395 pub frame_nr: FrameNumber,
396
397 /// Decode timestamp of this sample.
398 /// Chunks are expected to be submitted in the order of decode timestamp.
399 ///
400 /// `decode_timestamp <= presentation_timestamp`
401 pub decode_timestamp: Time,
402
403 /// Time at which this sample appears in the frame stream, in time units.
404 ///
405 /// The frame should be shown at this time.
406 /// Often synonymous with `composition_timestamp`.
407 ///
408 /// `decode_timestamp <= presentation_timestamp`
409 pub presentation_timestamp: Time,
410
411 /// Duration of the sample.
412 ///
413 /// Typically the time difference in presentation timestamp to the next sample.
414 /// May be unknown if this is the last sample in an ongoing video stream.
415 pub duration: Option<Time>,
416}
417
418/// CPU-side data for a decoded frame.
419#[derive(re_byte_size::SizeBytes)]
420pub struct DecodedFrameContent {
421 pub data: Vec<u8>,
422 pub width: u32,
423 pub height: u32,
424 #[size_bytes(ignore)]
425 pub format: PixelFormat,
426}
427
428impl DecodedFrameContent {
429 pub fn width(&self) -> u32 {
430 self.width
431 }
432
433 pub fn height(&self) -> u32 {
434 self.height
435 }
436}
437
438cfg_select! {
439 target_arch = "wasm32" => {
440 /// Data for a decoded frame on the web.
441 ///
442 /// Frames either come from the browser's `WebCodecs` API (color/luma video) or
443 /// from a CPU-side decoder (e.g. RVL depth). The two are kept in one type so
444 /// downstream code can treat them uniformly.
445 #[derive(re_byte_size::SizeBytes)]
446 pub enum FrameContent {
447 /// Browser-owned frame produced by WebCodecs/browser image decoding.
448 ///
449 /// Prefer that whenever possible.
450 WebVideoFrame(webcodecs::WebVideoFrame),
451
452 /// CPU-side decoded data, used when browser decoding would lose information,
453 /// for instance when decoding 16bit images (for which as of writing there's no way to get out the raw data)
454 Decoded(DecodedFrameContent),
455 }
456
457 impl FrameContent {
458 pub fn width(&self) -> u32 {
459 match self {
460 Self::WebVideoFrame(frame) => frame.display_width(),
461 Self::Decoded(frame) => frame.width(),
462 }
463 }
464
465 pub fn height(&self) -> u32 {
466 match self {
467 Self::WebVideoFrame(frame) => frame.display_height(),
468 Self::Decoded(frame) => frame.height(),
469 }
470 }
471 }
472 }
473 _ => {
474 /// Data for a decoded frame on native targets.
475 pub type FrameContent = DecodedFrameContent;
476 }
477}
478
479/// Meta information about a decoded video frame, as reported by the decoder.
480#[derive(Debug, Clone, re_byte_size::SizeBytes)]
481pub struct FrameInfo {
482 /// The start of a new group of pictures?
483 ///
484 /// This probably means this is a _keyframe_, and that and entire frame
485 /// can be decoded from only this one sample (though I'm not 100% sure).
486 ///
487 /// None = unknown.
488 pub is_sync: Option<bool>,
489
490 /// Which sample in the video is this from?
491 ///
492 /// We always assume one sample leads one frame
493 /// (but may provide arbitrary additional information which may be needed for other frames in the GOP).
494 ///
495 /// This is the order of which the samples appear in the container,
496 /// which is ordered by [`Self::latest_decode_timestamp`].
497 /// I.e. this is NOT ordered by [`Self::presentation_timestamp`].
498 ///
499 /// None = unknown.
500 pub sample_idx: Option<SampleIndex>,
501
502 /// Which frame is this?
503 ///
504 /// This is on the assumption that each sample produces a single frame,
505 /// which is true for MP4.
506 ///
507 /// This is the index of frames ordered by [`Self::presentation_timestamp`].
508 ///
509 /// None = unknown.
510 pub frame_nr: Option<FrameNumber>,
511
512 /// Time at which this frame appears in the frame stream, in time units.
513 ///
514 /// The frame should be shown at this time.
515 /// We expect this timestamp to be identical with a the presentation timestamp of the [`crate::Chunk`]
516 /// which is associated with this frame.
517 /// Often synonymous with `composition_timestamp`.
518 ///
519 /// `decode_timestamp <= presentation_timestamp`
520 pub presentation_timestamp: Time,
521
522 /// Duration of the frame.
523 ///
524 /// Typically the time difference in presentation timestamp to the next frame.
525 /// May be unknown if this is the last frame in an ongoing video stream.
526 pub duration: Option<Time>,
527
528 /// The decode timestamp of the last chunk that was needed to decode this frame.
529 ///
530 /// None = unknown.
531 pub latest_decode_timestamp: Option<Time>,
532}
533
534impl FrameInfo {
535 /// Presentation timestamp range in which this frame is valid.
536 ///
537 /// If there's no known duration, the range is open ended.
538 pub fn presentation_time_range(&self) -> std::ops::Range<Time> {
539 if let Some(duration) = self.duration {
540 self.presentation_timestamp..self.presentation_timestamp + duration
541 } else {
542 self.presentation_timestamp..Time::MAX
543 }
544 }
545}
546
547/// One decoded video frame.
548#[derive(re_byte_size::SizeBytes)]
549pub struct Frame {
550 pub content: FrameContent,
551 #[size_bytes(ignore)]
552 pub info: FrameInfo,
553}
554
555/// Pixel format/layout used by [`FrameContent::data`].
556#[derive(Debug, Clone)]
557pub enum PixelFormat {
558 L8,
559 L16,
560 R32Float,
561 Rgb8Unorm,
562 Rgba8Unorm,
563
564 Yuv {
565 layout: YuvPixelLayout,
566 range: YuvRange,
567 // TODO(andreas): Color primaries should also apply to RGB data,
568 // but for now we just always assume RGB to be BT.709 ~= sRGB.
569 coefficients: YuvMatrixCoefficients,
570 // Note that we don't handle chroma sample location at all so far.
571 },
572}
573
574impl PixelFormat {
575 pub fn bits_per_pixel(&self) -> u32 {
576 match self {
577 Self::L8 => 8,
578 Self::L16 => 16,
579 Self::Rgb8Unorm { .. } => 24,
580 Self::R32Float | Self::Rgba8Unorm { .. } => 32,
581 Self::Yuv { layout, .. } => match layout {
582 YuvPixelLayout::Y_U_V444 => 24,
583 YuvPixelLayout::Y_U_V422 => 16,
584 YuvPixelLayout::Y_U_V420 => 12,
585 YuvPixelLayout::Y400 => 8,
586 },
587 }
588 }
589}
590
591/// Pixel layout used by [`PixelFormat::Yuv`].
592///
593/// For details see `re_renderer`'s `YuvPixelLayout` type.
594#[expect(non_camel_case_types)]
595#[derive(Debug, Clone, Copy, PartialEq, Eq)]
596pub enum YuvPixelLayout {
597 Y_U_V444,
598 Y_U_V422,
599 Y_U_V420,
600 Y400,
601}
602
603/// Yuv value range used by [`PixelFormat::Yuv`].
604///
605/// For details see `re_renderer`'s `YuvRange` type.
606#[derive(Debug, Clone, Copy)]
607pub enum YuvRange {
608 Limited,
609 Full,
610}
611
612/// Yuv matrix coefficients used by [`PixelFormat::Yuv`].
613///
614/// For details see `re_renderer`'s `YuvMatrixCoefficients` type.
615#[derive(Debug, Clone, Copy)]
616pub enum YuvMatrixCoefficients {
617 /// Interpret YUV as GBR.
618 Identity,
619
620 Bt601,
621
622 Bt709,
623}
624
625/// How the video should be decoded.
626///
627/// Depending on the decoder backend, these settings are merely hints and may be ignored.
628/// However, they can be useful in some situations to work around issues.
629///
630/// On the web this directly corresponds to
631/// <https://www.w3.org/TR/webcodecs/#hardware-acceleration>
632#[derive(
633 Debug, Clone, Copy, PartialEq, Eq, Default, Hash, serde::Deserialize, serde::Serialize,
634)]
635pub enum DecodeHardwareAcceleration {
636 /// May use hardware acceleration if available and compatible with the codec.
637 #[default]
638 Auto,
639
640 /// Should use a software decoder even if hardware acceleration is available.
641 ///
642 /// If no software decoder is present, this may cause decoding to fail.
643 PreferSoftware,
644
645 /// Should use a hardware decoder.
646 ///
647 /// If no hardware decoder is present, this may cause decoding to fail.
648 PreferHardware,
649}
650
651/// Settings for video decoding.
652#[derive(Debug, Clone, PartialEq, Eq, Default, Hash, serde::Deserialize, serde::Serialize)]
653pub struct DecodeSettings {
654 /// How the video should be decoded.
655 pub hw_acceleration: DecodeHardwareAcceleration,
656
657 /// Custom path for the ffmpeg binary.
658 ///
659 /// If not provided, we use the path automatically determined by `ffmpeg_sidecar`.
660 #[cfg(not(target_arch = "wasm32"))]
661 pub ffmpeg_path: Option<std::path::PathBuf>,
662}
663
664impl std::fmt::Display for DecodeHardwareAcceleration {
665 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
666 match self {
667 Self::Auto => write!(f, "Auto"),
668 Self::PreferSoftware => write!(f, "Prefer software"),
669 Self::PreferHardware => write!(f, "Prefer hardware"),
670 }
671 }
672}
673
674impl std::str::FromStr for DecodeHardwareAcceleration {
675 type Err = ();
676
677 fn from_str(s: &str) -> Result<Self, Self::Err> {
678 match s.trim().to_lowercase().replace('-', "_").as_str() {
679 "auto" => Ok(Self::Auto),
680 "prefer_software" | "software" => Ok(Self::PreferSoftware),
681 "prefer_hardware" | "hardware" => Ok(Self::PreferHardware),
682 _ => Err(()),
683 }
684 }
685}