Skip to main content

pixelflux/webcam/
decode.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7//! Decoders for the client camera uplink, and for the checks that read a session's own output
8//! back.
9//!
10//! Clients send whatever their browser can produce: H.264 (WebCodecs or the WebRTC media track),
11//! VP8/VP9/AV1/HEVC (same sources), or MJPEG from the canvas fallback. Every codec lands in the
12//! same I420 view so the rest of the pipeline is codec-agnostic. H.264 decodes through Cisco
13//! OpenH264, VP8 and VP9 through libvpx, AV1 through dav1d, HEVC through libde265, and MJPEG
14//! through TurboJPEG, which pixelflux already uses for its own JPEG stripes. Decoding is
15//! software here; a hardware decoder only changes which `Decoder` is constructed.
16
17use std::ffi::{CStr, c_int, c_void};
18use std::ptr;
19
20use turbojpeg::{Decompressor, Image, PixelFormat, Subsamp, YuvImage};
21
22use super::convert::{I420Buffer, I420View};
23
24/// Input codecs, by wire id. The ids are part of the Selkies WebSocket framing and are exported to
25/// Python as `VirtualCamera.CODEC_*`.
26pub use crate::encoders::codec::Codec;
27use crate::encoders::codec::{
28    FRAME_KEY, annexb_nals, av1_is_key, h264_frame_type, h265_frame_type, vp8_is_key, vp9_is_key,
29};
30use crate::encoders::sps;
31
32/// Whether an encoded frame is a key frame, read from the bitstream itself: every codec here
33/// declares it in its first bytes or NAL types.
34pub fn sniff_keyframe(codec: Codec, data: &[u8]) -> Option<bool> {
35    Some(match codec {
36        Codec::Jpeg => true,
37        Codec::H264 => h264_frame_type(data) == FRAME_KEY,
38        Codec::H265 => h265_frame_type(data) == FRAME_KEY,
39        Codec::Vp8 => vp8_is_key(data),
40        Codec::Vp9 => vp9_is_key(data),
41        Codec::Av1 => av1_is_key(data),
42    })
43}
44
45#[derive(Debug)]
46pub enum DecodeError {
47    /// The packet could not be decoded; the stream needs a keyframe to resynchronize.
48    Corrupt(String),
49    /// The decoder itself failed and must be recreated.
50    Fatal(String),
51}
52
53pub trait Decoder {
54    fn codec(&self) -> Codec;
55    /// Decode one encoded frame. `Ok(true)` when `frame()` now holds a new picture.
56    fn decode(&mut self, data: &[u8]) -> Result<bool, DecodeError>;
57    fn frame(&self) -> Option<I420View<'_>>;
58}
59
60pub fn new_decoder(codec: Codec) -> Result<Box<dyn Decoder>, String> {
61    match codec {
62        Codec::Jpeg => Ok(Box::new(JpegDecoder::new()?)),
63        _ => Ok(Box::new(VideoDecoder::new(codec)?)),
64    }
65}
66
67/// The color a decoded stream declares: the matrix as ISO/IEC 23091-2 codes it (1 BT.709,
68/// 5 BT.470BG, 6 SMPTE 170M, 2 unspecified) and whether the range is full.
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub struct ColorTags {
71    pub matrix: u8,
72    pub full_range: bool,
73}
74
75impl ColorTags {
76    pub const BT709_LIMITED: Self = Self {
77        matrix: 1,
78        full_range: false,
79    };
80    pub const BT709_FULL: Self = Self {
81        matrix: 1,
82        full_range: true,
83    };
84    pub const BT470BG_LIMITED: Self = Self {
85        matrix: 5,
86        full_range: false,
87    };
88    pub const BT470BG_FULL: Self = Self {
89        matrix: 5,
90        full_range: true,
91    };
92}
93
94/// The chroma sampling of a decoded picture.
95#[derive(Clone, Copy, PartialEq, Eq)]
96enum Chroma {
97    C420,
98    C422,
99    C444,
100}
101
102/// One decoded picture as a backend hands it back: borrowed planes, valid until the backend
103/// decodes again.
104struct Picture {
105    width: usize,
106    height: usize,
107    chroma: Chroma,
108    planes: [*const u8; 3],
109    strides: [usize; 3],
110    tags: Option<ColorTags>,
111}
112
113impl Picture {
114    unsafe fn plane(&self, i: usize, rows: usize, cols: usize) -> &[u8] {
115        unsafe { std::slice::from_raw_parts(self.planes[i], self.strides[i] * (rows - 1) + cols) }
116    }
117}
118
119/// A software decoder for one of the inter-coded codecs.
120pub struct VideoDecoder {
121    codec: Codec,
122    backend: Backend,
123    picture: Option<Picture>,
124    /// Planar copy of pictures that are not 4:2:0, or that a backend does not keep for us.
125    converted: I420Buffer,
126    from_converted: bool,
127}
128
129unsafe impl Send for VideoDecoder {}
130
131enum Backend {
132    H264(H264Backend),
133    Vpx(VpxBackend),
134    Av1(Box<Dav1dBackend>),
135    Hevc(De265Backend),
136}
137
138impl VideoDecoder {
139    pub fn new(codec: Codec) -> Result<Self, String> {
140        let backend = match codec {
141            Codec::H264 => Backend::H264(H264Backend::new()?),
142            Codec::Vp8 | Codec::Vp9 => Backend::Vpx(VpxBackend::new(codec)?),
143            Codec::Av1 => Backend::Av1(Box::new(Dav1dBackend::new()?)),
144            Codec::H265 => Backend::Hevc(De265Backend::new()?),
145            Codec::Jpeg => return Err("MJPEG decodes through the JPEG decoder".into()),
146        };
147        Ok(Self {
148            codec,
149            backend,
150            picture: None,
151            converted: I420Buffer::new(2, 2),
152            from_converted: false,
153        })
154    }
155
156    /// The color matrix and range the last decoded frame carries, as the bitstream declared
157    /// them; unspecified where the codec cannot declare a matrix.
158    pub fn color_tags(&self) -> Option<ColorTags> {
159        self.picture.as_ref()?.tags
160    }
161
162    /// Average a 4:2:2 or 4:4:4 picture's chroma into the I420 copy.
163    fn convert(&mut self) {
164        let Some(p) = &self.picture else { return };
165        let (w, h) = (p.width, p.height);
166        let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
167        self.converted.resize(w, h);
168        let y_len = self.converted.y_len();
169        let uv_len = self.converted.uv_len();
170        let (yp, rest) = self.converted.data.split_at_mut(y_len);
171        let (up, vp) = rest.split_at_mut(uv_len);
172        let y = unsafe { p.plane(0, h, w) };
173        for row in 0..h {
174            yp[row * w..row * w + w]
175                .copy_from_slice(&y[row * p.strides[0]..row * p.strides[0] + w]);
176        }
177        let (src_cw, src_ch) = match p.chroma {
178            Chroma::C420 => (cw, ch),
179            Chroma::C422 => (cw, h),
180            Chroma::C444 => (w, h),
181        };
182        for (i, dst) in [(1usize, &mut *up), (2usize, &mut *vp)] {
183            let src = unsafe { p.plane(i, src_ch, src_cw) };
184            let s = p.strides[i];
185            for row in 0..ch {
186                for x in 0..cw {
187                    let sum: u32 = match p.chroma {
188                        Chroma::C420 => u32::from(src[row * s + x]) * 4,
189                        Chroma::C422 => {
190                            let (r0, r1) =
191                                ((2 * row).min(src_ch - 1), (2 * row + 1).min(src_ch - 1));
192                            (u32::from(src[r0 * s + x]) + u32::from(src[r1 * s + x])) * 2
193                        }
194                        Chroma::C444 => {
195                            let (r0, r1) =
196                                ((2 * row).min(src_ch - 1), (2 * row + 1).min(src_ch - 1));
197                            let (x0, x1) = (2 * x, (2 * x + 1).min(src_cw - 1));
198                            u32::from(src[r0 * s + x0])
199                                + u32::from(src[r0 * s + x1])
200                                + u32::from(src[r1 * s + x0])
201                                + u32::from(src[r1 * s + x1])
202                        }
203                    };
204                    dst[row * cw + x] = ((sum + 2) / 4) as u8;
205                }
206            }
207        }
208    }
209}
210
211impl Decoder for VideoDecoder {
212    fn codec(&self) -> Codec {
213        self.codec
214    }
215
216    fn decode(&mut self, data: &[u8]) -> Result<bool, DecodeError> {
217        self.picture = None;
218        let picture = match &mut self.backend {
219            Backend::H264(b) => b.decode(data)?,
220            Backend::Vpx(b) => b.decode(data)?,
221            Backend::Av1(b) => b.decode(data)?,
222            Backend::Hevc(b) => b.decode(data)?,
223        };
224        let Some(picture) = picture else {
225            return Ok(false);
226        };
227        self.from_converted = picture.chroma != Chroma::C420;
228        self.picture = Some(picture);
229        if self.from_converted {
230            self.convert();
231        }
232        Ok(true)
233    }
234
235    fn frame(&self) -> Option<I420View<'_>> {
236        let p = self.picture.as_ref()?;
237        let full_range = p.tags.is_some_and(|t| t.full_range);
238        if self.from_converted {
239            return Some(self.converted.view(full_range));
240        }
241        let (w, h) = (p.width, p.height);
242        let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
243        Some(I420View {
244            width: w,
245            height: h,
246            y: unsafe { p.plane(0, h, w) },
247            u: unsafe { p.plane(1, ch, cw) },
248            v: unsafe { p.plane(2, ch, cw) },
249            y_stride: p.strides[0],
250            uv_stride: p.strides[1].min(p.strides[2]),
251            full_range,
252        })
253    }
254}
255
256/// Cisco OpenH264, driven through its decoder interface: it conceals the blocks a stream
257/// lost and hands back every picture it decodes, one behind a gap included, and the picture
258/// stays in its own buffer until the next call.
259struct H264Backend {
260    decoder: *mut openh264_sys2::ISVCDecoder,
261    tags: Option<ColorTags>,
262}
263
264impl Drop for H264Backend {
265    fn drop(&mut self) {
266        unsafe {
267            if let Some(uninit) = self.api().and_then(|a| a.Uninitialize) {
268                uninit(self.decoder);
269            }
270            openh264_sys2::source::APILoader::WelsDestroyDecoder(self.decoder);
271        }
272    }
273}
274
275impl H264Backend {
276    /// The decoder's method table, behind the two pointers OpenH264 hands out.
277    fn api(&self) -> Option<&openh264_sys2::ISVCDecoderVtbl> {
278        unsafe { self.decoder.as_ref().and_then(|d| d.as_ref()) }
279    }
280
281    fn new() -> Result<Self, String> {
282        use openh264_sys2::*;
283        let mut decoder: *mut ISVCDecoder = ptr::null_mut();
284        if unsafe { source::APILoader::WelsCreateDecoder(&mut decoder) } != 0 {
285            return Err("OpenH264 created no decoder".into());
286        }
287        let me = Self {
288            decoder,
289            tags: None,
290        };
291        let initialize = me
292            .api()
293            .and_then(|a| a.Initialize)
294            .ok_or("OpenH264 exposes no Initialize")?;
295        let mut param: SDecodingParam = unsafe { std::mem::zeroed() };
296        param.eEcActiveIdc = ERROR_CON_SLICE_MV_COPY_CROSS_IDR;
297        param.sVideoProperty.size = std::mem::size_of::<SVideoProperty>() as u32;
298        param.sVideoProperty.eVideoBsType = VIDEO_BITSTREAM_AVC;
299        if unsafe { initialize(decoder, &param) } != 0 {
300            return Err("OpenH264 refused to initialize".into());
301        }
302        Ok(me)
303    }
304
305    fn decode(&mut self, data: &[u8]) -> Result<Option<Picture>, DecodeError> {
306        use openh264_sys2::*;
307        let mut sps = None;
308        let mut scalable = false;
309        for nal in annexb_nals(data) {
310            match nal[0] & 0x1f {
311                7 if sps.is_none() => sps = Some(nal),
312                14 | 15 | 20 => scalable = true,
313                _ => {}
314            }
315        }
316        if let Some(sps) = sps {
317            self.tags = Some(match sps::read_color(sps) {
318                Some(signal) => ColorTags {
319                    matrix: signal.matrix,
320                    full_range: signal.full_range,
321                },
322                None => ColorTags {
323                    matrix: 2,
324                    full_range: false,
325                },
326            });
327        }
328        // OpenH264 also decodes the scalable extension, and a prefix or extension NAL unit naming
329        // a layer above the base turns it to that layer for good, so it hands over no picture
330        // again, key frames included. An AVC decoder ignores the extension's units (prefix,
331        // subset SPS, extension slice), and so does this one.
332        let mut avc = Vec::new();
333        let data = if scalable {
334            for nal in annexb_nals(data).filter(|n| !matches!(n[0] & 0x1f, 14 | 15 | 20)) {
335                avc.extend_from_slice(&[0, 0, 0, 1]);
336                avc.extend_from_slice(nal);
337            }
338            &avc[..]
339        } else {
340            data
341        };
342        let mut dst: [*mut u8; 3] = [ptr::null_mut(); 3];
343        let mut info: SBufferInfo = unsafe { std::mem::zeroed() };
344        let decode = self
345            .api()
346            .and_then(|a| a.DecodeFrameNoDelay)
347            .ok_or_else(|| DecodeError::Fatal("OpenH264 exposes no DecodeFrameNoDelay".into()))?;
348        let mut state = unsafe {
349            decode(
350                self.decoder,
351                data.as_ptr(),
352                data.len() as c_int,
353                dst.as_mut_ptr(),
354                &mut info,
355            )
356        };
357        // A profile above baseline has the decoder hold each picture back until the next
358        // one arrives, against reordering the stream never does: the held picture is this
359        // access unit's, and flushing hands it over now. OpenH264 2.6 keeps a flushed
360        // picture's buffer when decoding single-threaded, so such a stream stalls once the
361        // pool runs dry; a Baseline stream is never held.
362        if info.iBufferStatus != 1
363            && let Some(flush) = self.api().and_then(|a| a.FlushFrame)
364        {
365            state |= unsafe { flush(self.decoder, dst.as_mut_ptr(), &mut info) };
366        }
367        if info.iBufferStatus != 1 || dst.iter().any(|p| p.is_null()) {
368            if state != dsErrorFree {
369                return Err(DecodeError::Corrupt(format!(
370                    "OpenH264 decoding state {state:#x}"
371                )));
372            }
373            return Ok(None);
374        }
375        let system = unsafe { info.UsrData.sSystemBuffer };
376        Ok(Some(Picture {
377            width: system.iWidth as usize,
378            height: system.iHeight as usize,
379            chroma: Chroma::C420,
380            planes: [dst[0], dst[1], dst[2]],
381            strides: [
382                system.iStride[0] as usize,
383                system.iStride[1] as usize,
384                system.iStride[1] as usize,
385            ],
386            tags: self.tags,
387        }))
388    }
389}
390
391/// libvpx's VP8 and VP9 decoders. VP9 declares its matrix and range in every frame; VP8's
392/// keyframe header carries one color-space bit, BT.601 or reserved, and a clamping bit.
393struct VpxBackend {
394    codec: Codec,
395    ctx: codec_sys::vpx::vpx_codec_ctx_t,
396    tags: Option<ColorTags>,
397}
398
399impl Drop for VpxBackend {
400    fn drop(&mut self) {
401        unsafe { codec_sys::vpx::vpx_codec_destroy(&mut self.ctx) };
402    }
403}
404
405/// VP8's `color_space` and `clamping_type` bits, the first two bool-coded fields of a key
406/// frame's first partition.
407fn vp8_color_bits(frame: &[u8]) -> Option<(bool, bool)> {
408    if !vp8_is_key(frame) || frame.len() < 12 {
409        return None;
410    }
411    let data = &frame[10..];
412    let mut value = (u32::from(data[0]) << 8) | u32::from(data[1]);
413    let mut range = 255u32;
414    let mut bit_count = 0;
415    let mut at = 2;
416    let mut read = || {
417        let split = 1 + (((range - 1) * 128) >> 8);
418        let big = split << 8;
419        let bit = value >= big;
420        if bit {
421            range -= split;
422            value -= big;
423        } else {
424            range = split;
425        }
426        while range < 128 {
427            value <<= 1;
428            range <<= 1;
429            bit_count += 1;
430            if bit_count == 8 {
431                bit_count = 0;
432                value |= u32::from(data.get(at).copied().unwrap_or(0));
433                at += 1;
434            }
435        }
436        bit
437    };
438    let color_space = read();
439    let clamping = read();
440    Some((color_space, clamping))
441}
442
443impl VpxBackend {
444    fn new(codec: Codec) -> Result<Self, String> {
445        use codec_sys::vpx::*;
446        let iface = unsafe {
447            if codec == Codec::Vp8 {
448                vpx_codec_vp8_dx()
449            } else {
450                vpx_codec_vp9_dx()
451            }
452        };
453        let mut ctx: vpx_codec_ctx_t = unsafe { std::mem::zeroed() };
454        if unsafe {
455            vpx_codec_dec_init_ver(
456                &mut ctx,
457                iface,
458                ptr::null(),
459                0,
460                VPX_DECODER_ABI_VERSION as c_int,
461            )
462        } != VPX_CODEC_OK
463        {
464            return Err(format!("libvpx opened no {} decoder", codec.name()));
465        }
466        Ok(Self {
467            codec,
468            ctx,
469            tags: None,
470        })
471    }
472
473    fn decode(&mut self, data: &[u8]) -> Result<Option<Picture>, DecodeError> {
474        use codec_sys::vpx::*;
475        if self.codec == Codec::Vp8
476            && let Some((reserved, clamping)) = vp8_color_bits(data)
477        {
478            self.tags = Some(ColorTags {
479                matrix: if reserved { 2 } else { 5 },
480                full_range: clamping,
481            });
482        }
483        let res = unsafe {
484            vpx_codec_decode(
485                &mut self.ctx,
486                data.as_ptr(),
487                data.len() as u32,
488                ptr::null_mut(),
489                0,
490            )
491        };
492        if res != VPX_CODEC_OK {
493            let detail = unsafe { CStr::from_ptr(vpx_codec_err_to_string(res)) }
494                .to_string_lossy()
495                .into_owned();
496            return Err(DecodeError::Corrupt(detail));
497        }
498        let mut iter: vpx_codec_iter_t = ptr::null_mut();
499        let mut image = None;
500        while let Some(next) =
501            ptr::NonNull::new(unsafe { vpx_codec_get_frame(&mut self.ctx, &mut iter) })
502        {
503            image = Some(next);
504        }
505        let Some(image) = image else { return Ok(None) };
506        let img = unsafe { image.as_ref() };
507        if img.bit_depth != 8 {
508            return Err(DecodeError::Fatal(format!(
509                "unsupported libvpx bit depth {}",
510                img.bit_depth
511            )));
512        }
513        let chroma = match (img.x_chroma_shift, img.y_chroma_shift) {
514            (1, 1) => Chroma::C420,
515            (1, 0) => Chroma::C422,
516            (0, 0) => Chroma::C444,
517            _ => {
518                return Err(DecodeError::Fatal(format!(
519                    "unsupported libvpx image format {}",
520                    img.fmt
521                )));
522            }
523        };
524        if self.codec == Codec::Vp9 {
525            let matrix = match img.cs {
526                VPX_CS_BT_601 => 5,
527                VPX_CS_BT_709 => 1,
528                VPX_CS_SMPTE_170 => 6,
529                VPX_CS_SMPTE_240 => 7,
530                VPX_CS_BT_2020 => 9,
531                VPX_CS_SRGB => 0,
532                _ => 2,
533            };
534            self.tags = Some(ColorTags {
535                matrix,
536                full_range: img.range == VPX_CR_FULL_RANGE,
537            });
538        }
539        Ok(Some(Picture {
540            width: img.d_w as usize,
541            height: img.d_h as usize,
542            chroma,
543            planes: [img.planes[0], img.planes[1], img.planes[2]],
544            strides: [
545                img.stride[0] as usize,
546                img.stride[1] as usize,
547                img.stride[2] as usize,
548            ],
549            tags: self.tags,
550        }))
551    }
552}
553
554/// dav1d, at the frame delay of one a live uplink wants.
555struct Dav1dBackend {
556    ctx: *mut codec_sys::dav1d::Dav1dContext,
557    picture: codec_sys::dav1d::Dav1dPicture,
558    held: bool,
559}
560
561impl Drop for Dav1dBackend {
562    fn drop(&mut self) {
563        unsafe {
564            if self.held {
565                codec_sys::dav1d::dav1d_picture_unref(&mut self.picture);
566            }
567            codec_sys::dav1d::dav1d_close(&mut self.ctx);
568        }
569    }
570}
571
572impl Dav1dBackend {
573    fn new() -> Result<Self, String> {
574        use codec_sys::dav1d::*;
575        let mut settings: Dav1dSettings = unsafe { std::mem::zeroed() };
576        unsafe { dav1d_default_settings(&mut settings) };
577        settings.n_threads = 2;
578        settings.max_frame_delay = 1;
579        settings.logger.callback = None;
580        let mut ctx: *mut Dav1dContext = ptr::null_mut();
581        if unsafe { dav1d_open(&mut ctx, &settings) } != 0 || ctx.is_null() {
582            return Err("dav1d opened no decoder".into());
583        }
584        Ok(Self {
585            ctx,
586            picture: unsafe { std::mem::zeroed() },
587            held: false,
588        })
589    }
590
591    fn decode(&mut self, data: &[u8]) -> Result<Option<Picture>, DecodeError> {
592        use codec_sys::dav1d::*;
593        const AGAIN: c_int = -(libc::EAGAIN);
594        let mut input: Dav1dData = unsafe { std::mem::zeroed() };
595        let buffer = unsafe { dav1d_data_create(&mut input, data.len()) };
596        if buffer.is_null() {
597            return Err(DecodeError::Fatal("dav1d allocated no input buffer".into()));
598        }
599        unsafe { ptr::copy_nonoverlapping(data.as_ptr(), buffer, data.len()) };
600        let mut got = false;
601        loop {
602            let sent = unsafe { dav1d_send_data(self.ctx, &mut input) };
603            if sent < 0 && sent != AGAIN {
604                unsafe { dav1d_data_unref(&mut input) };
605                return Err(DecodeError::Corrupt(format!(
606                    "dav1d refused the data ({sent})"
607                )));
608            }
609            loop {
610                let mut next: Dav1dPicture = unsafe { std::mem::zeroed() };
611                let res = unsafe { dav1d_get_picture(self.ctx, &mut next) };
612                if res == AGAIN {
613                    break;
614                }
615                if res < 0 {
616                    unsafe { dav1d_data_unref(&mut input) };
617                    return Err(DecodeError::Corrupt(format!(
618                        "dav1d decoded no picture ({res})"
619                    )));
620                }
621                if self.held {
622                    unsafe { dav1d_picture_unref(&mut self.picture) };
623                }
624                self.picture = next;
625                self.held = true;
626                got = true;
627            }
628            if sent == 0 {
629                break;
630            }
631        }
632        unsafe { dav1d_data_unref(&mut input) };
633        if !got {
634            return Ok(None);
635        }
636        let p = &self.picture;
637        let chroma = match p.p.layout {
638            DAV1D_PIXEL_LAYOUT_I420 => Chroma::C420,
639            DAV1D_PIXEL_LAYOUT_I422 => Chroma::C422,
640            DAV1D_PIXEL_LAYOUT_I444 => Chroma::C444,
641            other => {
642                return Err(DecodeError::Fatal(format!(
643                    "unsupported dav1d layout {other}"
644                )));
645            }
646        };
647        if p.p.bpc != 8 {
648            return Err(DecodeError::Fatal(format!(
649                "unsupported dav1d bit depth {}",
650                p.p.bpc
651            )));
652        }
653        let tags = unsafe { p.seq_hdr.as_ref() }.map(|s| ColorTags {
654            matrix: s.mtrx as u8,
655            full_range: s.color_range != 0,
656        });
657        Ok(Some(Picture {
658            width: p.p.w as usize,
659            height: p.p.h as usize,
660            chroma,
661            planes: [
662                p.data[0] as *const u8,
663                p.data[1] as *const u8,
664                p.data[2] as *const u8,
665            ],
666            strides: [
667                p.stride[0] as usize,
668                p.stride[1] as usize,
669                p.stride[1] as usize,
670            ],
671            tags,
672        }))
673    }
674}
675
676/// libde265. Its picture is valid only until the next call into the library, so the planes are
677/// copied out as they come.
678struct De265Backend {
679    ctx: *mut codec_sys::de265::de265_decoder_context,
680    planes: Vec<u8>,
681}
682
683impl Drop for De265Backend {
684    fn drop(&mut self) {
685        unsafe { codec_sys::de265::de265_free_decoder(self.ctx) };
686    }
687}
688
689impl De265Backend {
690    fn new() -> Result<Self, String> {
691        use codec_sys::de265::*;
692        let ctx = unsafe { de265_new_decoder() };
693        if ctx.is_null() {
694            return Err("libde265 opened no decoder".into());
695        }
696        unsafe {
697            de265_set_parameter_bool(ctx, DE265_DECODER_PARAM_BOOL_SEI_CHECK_HASH, 0);
698            de265_start_worker_threads(ctx, 2);
699        }
700        Ok(Self {
701            ctx,
702            planes: Vec::new(),
703        })
704    }
705
706    fn decode(&mut self, data: &[u8]) -> Result<Option<Picture>, DecodeError> {
707        use codec_sys::de265::*;
708        let text = |err: de265_error| {
709            unsafe { CStr::from_ptr(de265_get_error_text(err)) }
710                .to_string_lossy()
711                .into_owned()
712        };
713        unsafe {
714            let err = de265_push_data(
715                self.ctx,
716                data.as_ptr() as *const c_void,
717                data.len() as c_int,
718                0,
719                ptr::null_mut(),
720            );
721            if err != DE265_OK {
722                return Err(DecodeError::Corrupt(text(err)));
723            }
724            de265_push_end_of_frame(self.ctx);
725            // A decoded picture sits in the output queue until it is taken, and the decoder
726            // reports more to do for as long as one does, so each round takes what is there.
727            let mut image: *const de265_image = ptr::null();
728            let mut more: c_int = 1;
729            while more != 0 && image.is_null() {
730                let err = de265_decode(self.ctx, &mut more);
731                if err != DE265_OK && err != DE265_ERROR_WAITING_FOR_INPUT_DATA {
732                    return Err(DecodeError::Corrupt(text(err)));
733                }
734                loop {
735                    let next = de265_get_next_picture(self.ctx);
736                    if next.is_null() {
737                        break;
738                    }
739                    image = next;
740                }
741                if err == DE265_ERROR_WAITING_FOR_INPUT_DATA {
742                    break;
743                }
744            }
745            if image.is_null() {
746                return Ok(None);
747            }
748            let (w, h) = (
749                de265_get_image_width(image, 0) as usize,
750                de265_get_image_height(image, 0) as usize,
751            );
752            let format = de265_get_chroma_format(image);
753            let chroma = if format == de265_chroma_420 {
754                Chroma::C420
755            } else if format == de265_chroma_422 {
756                Chroma::C422
757            } else if format == de265_chroma_444 {
758                Chroma::C444
759            } else {
760                return Err(DecodeError::Fatal(format!(
761                    "unsupported libde265 chroma format {format}"
762                )));
763            };
764            if (0..3).any(|c| de265_get_bits_per_pixel(image, c) != 8) {
765                return Err(DecodeError::Fatal("unsupported libde265 bit depth".into()));
766            }
767            let tags = Some(ColorTags {
768                matrix: de265_get_image_matrix_coefficients(image) as u8,
769                full_range: de265_get_image_full_range_flag(image) != 0,
770            });
771            // The image lives only until the next libde265 call, so its planes are copied
772            // into this backend's own buffer, laid out as the picture was.
773            let (cw, ch) = match chroma {
774                Chroma::C420 => (w.div_ceil(2), h.div_ceil(2)),
775                Chroma::C422 => (w.div_ceil(2), h),
776                Chroma::C444 => (w, h),
777            };
778            self.planes.resize(w * h + 2 * cw * ch, 0);
779            let (yd, rest) = self.planes.split_at_mut(w * h);
780            let (ud, vd) = rest.split_at_mut(cw * ch);
781            for (c, dst, cols, rows) in [(0, yd, w, h), (1, ud, cw, ch), (2, vd, cw, ch)] {
782                let mut stride: c_int = 0;
783                let src = de265_get_image_plane(image, c, &mut stride);
784                for row in 0..rows {
785                    ptr::copy_nonoverlapping(
786                        src.add(row * stride as usize),
787                        dst.as_mut_ptr().add(row * cols),
788                        cols,
789                    );
790                }
791            }
792            Ok(Some(Picture {
793                width: w,
794                height: h,
795                chroma,
796                planes: [
797                    self.planes.as_ptr(),
798                    self.planes[w * h..].as_ptr(),
799                    self.planes[w * h + cw * ch..].as_ptr(),
800                ],
801                strides: [w, cw, cw],
802                tags,
803            }))
804        }
805    }
806}
807
808/// TurboJPEG decoder: 4:2:0 JPEGs decode straight into I420 planes; other subsamplings take the
809/// RGB route and are converted.
810pub struct JpegDecoder {
811    dec: Decompressor,
812    out: I420Buffer,
813    rgb: Vec<u8>,
814    have_frame: bool,
815}
816
817impl JpegDecoder {
818    pub fn new() -> Result<Self, String> {
819        Ok(JpegDecoder {
820            dec: Decompressor::new().map_err(|e| format!("turbojpeg: {}", e))?,
821            out: I420Buffer::new(2, 2),
822            rgb: Vec::new(),
823            have_frame: false,
824        })
825    }
826}
827
828/// The widest or tallest picture a camera's JPEG may claim, far past any webcam.
829const MAX_JPEG_SIDE: usize = 8192;
830
831impl Decoder for JpegDecoder {
832    fn codec(&self) -> Codec {
833        Codec::Jpeg
834    }
835
836    fn decode(&mut self, data: &[u8]) -> Result<bool, DecodeError> {
837        self.have_frame = false;
838        let hdr = self
839            .dec
840            .read_header(data)
841            .map_err(|e| DecodeError::Corrupt(format!("jpeg header: {}", e)))?;
842        if hdr.width == 0 || hdr.height == 0 {
843            return Err(DecodeError::Corrupt("empty jpeg".into()));
844        }
845        // The header is the peer's word, and the buffers are sized from it before any
846        // pixel decodes.
847        if hdr.width > MAX_JPEG_SIDE || hdr.height > MAX_JPEG_SIDE {
848            return Err(DecodeError::Corrupt(format!(
849                "jpeg of {}x{}",
850                hdr.width, hdr.height
851            )));
852        }
853        // A camera streams baseline JPEG, and so does every engine's canvas encoder the page
854        // falls back to. Only a progressive JPEG makes libjpeg-turbo hold the whole picture's
855        // coefficients while it decodes (the memory of CVE-2019-13960), so one is refused
856        // before decoding.
857        if hdr.is_progressive {
858            return Err(DecodeError::Corrupt("progressive jpeg".into()));
859        }
860        self.out.resize(hdr.width, hdr.height);
861        if hdr.subsamp == Subsamp::Sub2x2 {
862            let img = YuvImage {
863                pixels: &mut self.out.data[..],
864                width: hdr.width,
865                align: 1,
866                height: hdr.height,
867                subsamp: Subsamp::Sub2x2,
868            };
869            self.dec
870                .decompress_to_yuv(data, img)
871                .map_err(|e| DecodeError::Corrupt(format!("jpeg: {}", e)))?;
872        } else {
873            self.rgb.resize(hdr.width * hdr.height * 4, 0);
874            let img = Image {
875                pixels: &mut self.rgb[..],
876                width: hdr.width,
877                pitch: hdr.width * 4,
878                height: hdr.height,
879                format: PixelFormat::RGBA,
880            };
881            self.dec
882                .decompress(data, img)
883                .map_err(|e| DecodeError::Corrupt(format!("jpeg: {}", e)))?;
884            let y_len = self.out.y_len();
885            let uv_len = self.out.uv_len();
886            let cw = hdr.width.div_ceil(2);
887            let (yp, rest) = self.out.data.split_at_mut(y_len);
888            let (up, vp) = rest.split_at_mut(uv_len);
889            let mut planar = yuv::YuvPlanarImageMut {
890                y_plane: yuv::BufferStoreMut::Borrowed(yp),
891                y_stride: hdr.width as u32,
892                u_plane: yuv::BufferStoreMut::Borrowed(up),
893                u_stride: cw as u32,
894                v_plane: yuv::BufferStoreMut::Borrowed(vp),
895                v_stride: cw as u32,
896                width: hdr.width as u32,
897                height: hdr.height as u32,
898            };
899            yuv::rgba_to_yuv420(
900                &mut planar,
901                &self.rgb,
902                (hdr.width * 4) as u32,
903                yuv::YuvRange::Full,
904                yuv::YuvStandardMatrix::Bt601,
905                yuv::YuvConversionMode::Fast,
906            )
907            .map_err(|e| DecodeError::Corrupt(format!("rgb->i420: {:?}", e)))?;
908        }
909        self.have_frame = true;
910        Ok(true)
911    }
912
913    fn frame(&self) -> Option<I420View<'_>> {
914        if self.have_frame {
915            Some(self.out.view(true))
916        } else {
917            None
918        }
919    }
920}
921
922#[cfg(test)]
923mod tests {
924    use super::*;
925
926    #[test]
927    fn codec_ids_round_trip() {
928        for id in 0..6 {
929            let c = Codec::from_id(id).unwrap();
930            assert_eq!(c as u32, id);
931        }
932        assert!(Codec::from_id(6).is_none());
933    }
934
935    #[test]
936    fn h264_keyframe_sniff() {
937        let idr = [
938            0, 0, 0, 1, 0x67, 0x42, 0, 0, 0, 1, 0x68, 0xCE, 0, 0, 1, 0x65, 0x88,
939        ];
940        assert_eq!(sniff_keyframe(Codec::H264, &idr), Some(true));
941        let p = [0, 0, 0, 1, 0x41, 0x9A];
942        assert_eq!(sniff_keyframe(Codec::H264, &p), Some(false));
943        assert_eq!(sniff_keyframe(Codec::Vp8, &[0x10, 0, 0]), Some(true));
944        assert_eq!(sniff_keyframe(Codec::Vp8, &[0x11, 0, 0]), Some(false));
945        assert_eq!(sniff_keyframe(Codec::Vp9, &[0x82, 0x49, 0x83]), Some(true));
946        assert_eq!(sniff_keyframe(Codec::Vp9, &[0x86, 0]), Some(false));
947        assert_eq!(
948            sniff_keyframe(Codec::H265, &[0, 0, 1, 0x26, 0x01]),
949            Some(true)
950        );
951        assert_eq!(
952            sniff_keyframe(Codec::H265, &[0, 0, 1, 0x02, 0x01]),
953            Some(false)
954        );
955        assert_eq!(
956            sniff_keyframe(Codec::Av1, &[0x12, 0, 0x32, 0x01, 0x10]),
957            Some(true)
958        );
959        assert_eq!(sniff_keyframe(Codec::Av1, &[0x32, 0x01, 0x30]), Some(false));
960    }
961
962    #[test]
963    fn jpeg_round_trip() {
964        let w = 64;
965        let h = 48;
966        let mut img = I420Buffer::new(w, h);
967        let yl = img.y_len();
968        let ul = img.uv_len();
969        img.data[..yl].fill(200);
970        img.data[yl..yl + ul].fill(100);
971        img.data[yl + ul..].fill(150);
972        let src = YuvImage {
973            pixels: &img.data[..],
974            width: w,
975            align: 1,
976            height: h,
977            subsamp: Subsamp::Sub2x2,
978        };
979        let jpeg = turbojpeg::compress_yuv(src, 90).unwrap();
980        let mut dec = JpegDecoder::new().unwrap();
981        assert!(dec.decode(&jpeg).unwrap());
982        let v = dec.frame().unwrap();
983        assert_eq!((v.width, v.height), (w, h));
984        assert!(v.full_range);
985        let mid = v.y[(h / 2) * v.y_stride + w / 2];
986        assert!((i32::from(mid) - 200).abs() <= 3, "luma {}", mid);
987        let cu = v.u[(h / 4) * v.uv_stride + w / 4];
988        assert!((i32::from(cu) - 100).abs() <= 3, "cb {}", cu);
989    }
990
991    /// A JPEG whose header claims a picture past `MAX_JPEG_SIDE` is refused before its
992    /// buffers are sized from the claim (65535x65535 would ask 6 GiB of the session host).
993    #[test]
994    fn a_jpeg_claiming_a_huge_picture_is_refused_before_allocating() {
995        let (w, h) = (16usize, 16usize);
996        let img = I420Buffer::new(w, h);
997        let src = YuvImage {
998            pixels: &img.data[..],
999            width: w,
1000            align: 1,
1001            height: h,
1002            subsamp: Subsamp::Sub2x2,
1003        };
1004        let mut jpeg = turbojpeg::compress_yuv(src, 90).unwrap().to_vec();
1005        // SOF0: FF C0, length, precision, then height and width.
1006        let sof = jpeg.windows(2).position(|m| m == [0xFF, 0xC0]).unwrap();
1007        jpeg[sof + 5..sof + 9].copy_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
1008        let mut dec = JpegDecoder::new().unwrap();
1009        let err = dec.decode(&jpeg).unwrap_err();
1010        assert!(matches!(err, DecodeError::Corrupt(_)), "{err:?}");
1011        assert!(dec.out.data.len() < 1 << 20, "{} bytes", dec.out.data.len());
1012        assert!(dec.rgb.len() < 1 << 20, "{} bytes", dec.rgb.len());
1013    }
1014
1015    /// A progressive JPEG is refused before its picture is sized or decoded, and the decoder
1016    /// still decodes the baseline JPEG of the same picture after it.
1017    #[test]
1018    fn a_progressive_jpeg_is_refused_before_decoding() {
1019        let (w, h) = (64usize, 48usize);
1020        let img = I420Buffer::new(w, h);
1021        let src = || YuvImage {
1022            pixels: &img.data[..],
1023            width: w,
1024            align: 1,
1025            height: h,
1026            subsamp: Subsamp::Sub2x2,
1027        };
1028        let mut comp = turbojpeg::Compressor::new().unwrap();
1029        comp.set_quality(90).unwrap();
1030        comp.set_progressive(true).unwrap();
1031        let progressive = comp.compress_yuv_to_vec(src()).unwrap();
1032        assert!(turbojpeg::read_header(&progressive).unwrap().is_progressive);
1033        let mut dec = JpegDecoder::new().unwrap();
1034        let err = dec.decode(&progressive).unwrap_err();
1035        assert!(matches!(err, DecodeError::Corrupt(_)), "{err:?}");
1036        assert!(dec.frame().is_none(), "a picture after a refused JPEG");
1037        assert!(dec.out.data.len() < w * h, "{} bytes", dec.out.data.len());
1038        comp.set_progressive(false).unwrap();
1039        let baseline = comp.compress_yuv_to_vec(src()).unwrap();
1040        assert!(!turbojpeg::read_header(&baseline).unwrap().is_progressive);
1041        assert!(
1042            dec.decode(&baseline).unwrap(),
1043            "the baseline JPEG did not decode"
1044        );
1045        let v = dec.frame().unwrap();
1046        assert_eq!((v.width, v.height), (w, h));
1047    }
1048
1049    /// Every inter-coded codec has a decoder, and none holds a picture before decoding one.
1050    #[test]
1051    fn a_decoder_constructs_for_every_video_codec() {
1052        for c in Codec::VIDEO {
1053            let d = VideoDecoder::new(c).unwrap();
1054            assert_eq!(d.codec(), c);
1055            assert!(d.frame().is_none());
1056            assert_eq!(d.color_tags(), None);
1057        }
1058        assert!(VideoDecoder::new(Codec::Jpeg).is_err());
1059    }
1060
1061    #[test]
1062    fn h264_round_trip_through_openh264() {
1063        use openh264::encoder::Encoder;
1064        use openh264::formats::YUVBuffer;
1065        let (w, h) = (64usize, 48usize);
1066        let mut yuv = vec![0u8; w * h * 3 / 2];
1067        yuv[..w * h].fill(145);
1068        yuv[w * h..w * h + w * h / 4].fill(54);
1069        yuv[w * h + w * h / 4..].fill(34);
1070        let source = YUVBuffer::from_vec(yuv, w, h);
1071        let mut enc = Encoder::new().unwrap();
1072        let mut dec = VideoDecoder::new(Codec::H264).unwrap();
1073        let mut decoded = 0;
1074        for _ in 0..6 {
1075            let bitstream = enc.encode(&source).unwrap().to_vec();
1076            assert!(!bitstream.is_empty());
1077            if dec.decode(&bitstream).unwrap() {
1078                decoded += 1;
1079                let v = dec.frame().unwrap();
1080                assert_eq!((v.width, v.height), (w, h));
1081                assert!(!v.full_range);
1082                let y = v.y[(h / 2) * v.y_stride + w / 2];
1083                let u = v.u[(h / 4) * v.uv_stride + w / 4];
1084                let vv = v.v[(h / 4) * v.uv_stride + w / 4];
1085                assert!(
1086                    (i32::from(y) - 145).abs() <= 6
1087                        && (i32::from(u) - 54).abs() <= 6
1088                        && (i32::from(vv) - 34).abs() <= 6,
1089                    "yuv {} {} {}",
1090                    y,
1091                    u,
1092                    vv
1093                );
1094            }
1095        }
1096        assert!(decoded >= 5, "decoded {} of 6 frames", decoded);
1097    }
1098
1099    /// A Constrained Baseline H.264 stream of `frames` access units at `mbs` macroblocks square,
1100    /// its fields in the order of the specification's syntax tables: an SPS with picture order
1101    /// count type 0 and two reference frames, a CAVLC PPS, an IDR of I_PCM macroblocks holding
1102    /// `sample`, then P pictures of skipped macroblocks.
1103    fn h264_skipping_stream(mbs: u32, frames: u32, sample: u8) -> Vec<Vec<u8>> {
1104        struct Bits(Vec<u8>, usize);
1105        impl Bits {
1106            fn u(&mut self, value: u32, count: usize) {
1107                for shift in (0..count).rev() {
1108                    if self.1.is_multiple_of(8) {
1109                        self.0.push(0);
1110                    }
1111                    if (value >> shift) & 1 != 0 {
1112                        *self.0.last_mut().unwrap() |= 0x80 >> (self.1 % 8);
1113                    }
1114                    self.1 += 1;
1115                }
1116            }
1117            fn ue(&mut self, value: u32) {
1118                let coded = value + 1;
1119                let length = 32 - coded.leading_zeros() as usize;
1120                self.u(0, length - 1);
1121                self.u(coded, length);
1122            }
1123            fn align(&mut self) {
1124                while !self.1.is_multiple_of(8) {
1125                    self.u(0, 1);
1126                }
1127            }
1128            fn nal(mut self, header: u8) -> Vec<u8> {
1129                self.u(1, 1);
1130                self.align();
1131                let mut out = vec![0, 0, 0, 1, header];
1132                let mut zeros = 0;
1133                for byte in self.0 {
1134                    if zeros >= 2 && byte <= 3 {
1135                        out.push(3);
1136                        zeros = 0;
1137                    }
1138                    zeros = if byte == 0 { zeros + 1 } else { 0 };
1139                    out.push(byte);
1140                }
1141                out
1142            }
1143        }
1144        let mut sps = Bits(Vec::new(), 0);
1145        sps.u(66, 8);
1146        sps.u(0xc0, 8);
1147        sps.u(40, 8);
1148        for value in [0, 0, 0, 0, 2] {
1149            sps.ue(value);
1150        }
1151        sps.u(0, 1);
1152        sps.ue(mbs - 1);
1153        sps.ue(mbs - 1);
1154        sps.u(0b1100, 4);
1155        let mut pps = Bits(Vec::new(), 0);
1156        pps.ue(0);
1157        pps.ue(0);
1158        pps.u(0, 2);
1159        for value in [0, 0, 0] {
1160            pps.ue(value);
1161        }
1162        pps.u(0, 3);
1163        for value in [0, 0, 0] {
1164            pps.ue(value);
1165        }
1166        pps.u(0, 3);
1167        let mut idr = Bits(Vec::new(), 0);
1168        for value in [0, 7, 0] {
1169            idr.ue(value);
1170        }
1171        idr.u(0, 4);
1172        idr.ue(0);
1173        idr.u(0, 4);
1174        idr.u(0, 2);
1175        idr.ue(0);
1176        for _ in 0..mbs * mbs {
1177            idr.ue(25);
1178            idr.align();
1179            for _ in 0..384 {
1180                idr.u(u32::from(sample), 8);
1181            }
1182        }
1183        let mut units = vec![[sps.nal(0x67), pps.nal(0x68), idr.nal(0x65)].concat()];
1184        for n in 1..frames {
1185            let mut p = Bits(Vec::new(), 0);
1186            for value in [0, 5, 0] {
1187                p.ue(value);
1188            }
1189            p.u(n % 16, 4);
1190            p.u(2 * n % 16, 4);
1191            p.u(0, 3);
1192            p.ue(0);
1193            p.ue(mbs * mbs);
1194            units.push(p.nal(0x41));
1195        }
1196        units
1197    }
1198
1199    /// A prefix NAL unit naming dependency layer 1, which only a scalable stream carries, leaves
1200    /// every picture after it decoding, a key frame's included.
1201    #[test]
1202    fn h264_scalable_extension_units_never_retarget_the_decoder() {
1203        let mut units = h264_skipping_stream(2, 12, 0x60);
1204        units[4].splice(0..0, [0, 0, 0, 1, 0x4e, 0x80, 0x90, 0x07, 0x20]);
1205        let mut dec = VideoDecoder::new(Codec::H264).unwrap();
1206        for (n, unit) in units.iter().chain(&units).enumerate() {
1207            match dec.decode(unit) {
1208                Ok(true) => {}
1209                other => panic!("access unit {n}: {other:?}"),
1210            }
1211        }
1212    }
1213
1214    /// Garbage is a corrupt packet, never a decoder to throw away.
1215    #[test]
1216    fn a_decoder_rejects_garbage_without_fatal() {
1217        for c in Codec::VIDEO {
1218            let mut d = VideoDecoder::new(c).unwrap();
1219            match d.decode(&[1, 2, 3, 4, 5, 6, 7, 8]) {
1220                Ok(false) | Err(DecodeError::Corrupt(_)) => {}
1221                Ok(true) => panic!("{c:?}: garbage decoded to a frame"),
1222                Err(DecodeError::Fatal(e)) => panic!("{c:?}: fatal: {}", e),
1223            }
1224        }
1225    }
1226
1227    /// The VP8 color bits are the first two bool-coded fields of a key frame: a header whose
1228    /// first partition begins with two zero bits reads as BT.601 at limited range.
1229    /// A VP9 profile 2 key frame, 10-bit 4:2:0 at 320x240, is refused rather than read as 8-bit:
1230    /// a libvpx built with high bit depth hands it back in 16-bit samples, and one built without
1231    /// cannot decode it.
1232    #[test]
1233    fn a_10_bit_vp9_frame_never_decodes_as_8_bit() {
1234        const KEY: [u8; 77] = [
1235            0x92, 0x49, 0x83, 0x42, 0x00, 0x09, 0xf8, 0x07, 0x7b, 0x03, 0x1c, 0x12, 0x0e, 0x0c,
1236            0x2c, 0x00, 0x00, 0x48, 0x47, 0xa8, 0x3d, 0x8f, 0xc8, 0xc0, 0x0c, 0x30, 0x00, 0x6b,
1237            0x41, 0x13, 0x20, 0x95, 0xe2, 0x6a, 0x41, 0xdc, 0xb8, 0x00, 0x11, 0x50, 0x1b, 0x55,
1238            0x82, 0x76, 0x20, 0x4f, 0xd6, 0xa9, 0x82, 0x10, 0xc5, 0xe9, 0x8f, 0xe0, 0xa3, 0xf7,
1239            0xcd, 0x66, 0x00, 0xbc, 0xd3, 0xeb, 0x11, 0x3a, 0xb5, 0xed, 0x25, 0x7b, 0x25, 0x6d,
1240            0xa9, 0x8e, 0xf7, 0xf7, 0xfa, 0x2b, 0x10,
1241        ];
1242        let mut d = VideoDecoder::new(Codec::Vp9).unwrap();
1243        assert!(
1244            !matches!(d.decode(&KEY), Ok(true)),
1245            "a 10-bit picture was handed on as an 8-bit one"
1246        );
1247    }
1248
1249    #[test]
1250    fn vp8_color_bits_read_from_the_first_partition() {
1251        let mut key = vec![0x10, 0, 0, 0x9d, 0x01, 0x2a, 64, 0, 48, 0];
1252        key.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
1253        assert_eq!(vp8_color_bits(&key), Some((false, false)));
1254        key[10] = 0xff;
1255        key[11] = 0xff;
1256        assert_eq!(vp8_color_bits(&key), Some((true, true)));
1257        assert_eq!(
1258            vp8_color_bits(&[0x11, 0, 0]),
1259            None,
1260            "an inter frame carries no header"
1261        );
1262    }
1263}