Skip to main content

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}