From: Kostya Shishkov Date: Fri, 30 Oct 2026 19:55:25 +0000 (+0100) Subject: hwdec_vaapi: add (simplified) VP9 decoder X-Git-Url: https://git.nihav.org/?a=commitdiff_plain;h=067c6546e437f5fe2b5abcc7efa1823992b7e194;p=nihav-player.git hwdec_vaapi: add (simplified) VP9 decoder --- diff --git a/hwdec-vaapi/src/lib.rs b/hwdec-vaapi/src/lib.rs index ff99300..aaeeb40 100644 --- a/hwdec-vaapi/src/lib.rs +++ b/hwdec-vaapi/src/lib.rs @@ -435,3 +435,6 @@ fn fill_frame(ifmt: VAImageFormat, pic: &Picture, frm: &mut NABuffe #[allow(dead_code)] mod h264; pub use h264::new_h264_hwdec; + +mod vp9; +pub use vp9::new_vp9_hwdec; diff --git a/hwdec-vaapi/src/vp9.rs b/hwdec-vaapi/src/vp9.rs new file mode 100644 index 0000000..506ec3c --- /dev/null +++ b/hwdec-vaapi/src/vp9.rs @@ -0,0 +1,652 @@ +use std::collections::VecDeque; +use std::rc::Rc; + +use nihav_core::codecs::*; +use nihav_core::io::byteio::*; +use nihav_core::io::bitreader::*; + +use libva::*; + +use crate::{HWDecoder, HWDecoderCore, HWWrapper, fill_frame, profile_name}; + +const NUM_REFS: usize = 8; +const MAX_SB_FRAMES: usize = 8; + +trait ReadBitsSgn { + fn read_sgn(&mut self, n: u8) -> BitReaderResult; +} + +impl<'a> ReadBitsSgn for BitReader<'a> { + fn read_sgn(&mut self, n: u8) -> BitReaderResult { + let val = self.read(n)? as i32; + if self.read_bool()? { + Ok(-val) + } else { + Ok(val) + } + } +} + +const VP9_PROFILES: &[i32; 4] = &[ + VAProfile::VAProfileVP9Profile0, VAProfile::VAProfileVP9Profile1, + VAProfile::VAProfileVP9Profile2, VAProfile::VAProfileVP9Profile3 +]; + +enum QFrame { + Frame(bool, bool, Picture), + ShowAgain(u32), +} + +#[allow(dead_code)] +struct VaapiInternals { + display: Rc, + context: Rc, + surfaces: Vec, + ifmt: VAImageFormat, + queue: VecDeque<(u64, QFrame)>, + ref_pics: Vec>, +} + +#[derive(Default)] +struct FrameState { + width: u16, + height: u16, + profile: u8, + depth: u8, + cspace: u8, + crange: bool, + profile_supp: [bool; 4], + sub_x: u8, + sub_y: u8, + seg_tree_probs: [u8; 7], + seg_pred_probs: [u8; 3], + refresh_frame_flags: u8, +} + +fn read_prob(br: &mut BitReader) -> DecoderResult { + if br.read_bool()? { + Ok(br.read(8)? as u8) + } else { + Ok(255) + } +} + +impl FrameState { + fn read_color_config(&mut self, br: &mut BitReader) -> DecoderResult<()> { + if self.profile >= 2 { + self.depth = if br.read_bool()? { 12 } else { 10 }; + } else { + self.depth = 8; + } + self.cspace = br.read(3)? as u8; + if self.cspace != 7 { + self.crange = br.read_bool()?; + if self.profile == 1 || self.profile == 3 { + self.sub_x = br.read(1)? as u8; + self.sub_y = br.read(1)? as u8; + let reserved_zero = br.read_bool()?; + validate!(!reserved_zero); + } else { + self.sub_x = 1; + self.sub_y = 1; + } + } else { + self.crange = true; + if self.profile == 1 || self.profile == 3 { + self.sub_x = 0; + self.sub_y = 0; + let reserved_zero = br.read_bool()?; + validate!(!reserved_zero); + } + } + Ok(()) + } + fn read_frame_size(&mut self, br: &mut BitReader) -> DecoderResult<()> { + let width1 = br.read(16)? as u16; + let height1 = br.read(16)? as u16; + validate!(width1 < 16384 && height1 < 16384); + self.width = width1 + 1; + self.height = height1 + 1; + Ok(()) + } + fn read_render_size(&mut self, br: &mut BitReader) -> DecoderResult<()> { + if br.read_bool()? { + let _render_width1 = br.read(16)? as u16; + let _render_height1 = br.read(16)? as u16; + } + Ok(()) + } + fn do_frame(&mut self, vactx: &mut VaapiInternals, src: &[u8], timestamp: u64, ref_frames: &mut [u32; NUM_REFS]) -> DecoderResult<()> { + let mut br = BitReader::new(src, BitReaderMode::BE); + let frame_marker = br.read(2)?; + if frame_marker != 2 { + return Err(DecoderError::InvalidData); + } + let profile = br.read(2)? as usize; + if !self.profile_supp[profile] { + println!("Unsupported {}", profile_name(VP9_PROFILES[profile])); + return Err(DecoderError::NotImplemented); + } + if profile == 3 { + let reserved_zero = br.read_bool()?; + if reserved_zero { + return Err(DecoderError::InvalidData); + } + } + if br.read_bool()? { + let idx = br.read(3)? as usize; + if ref_frames[idx] == VA_INVALID_ID { + return Err(DecoderError::MissingReference); + } + vactx.queue.push_back((timestamp, QFrame::ShowAgain(ref_frames[idx]))); + return Ok(()); + } + let frame_type = br.read(1)?; + let show_frame = br.read_bool()?; + let error_resilient_mode = br.read_bool()?; + + let intra_only; + let mut reset_frame_context = 0; + let mut allow_high_precision_mv = false; + let mut interp_type = 0; + let mut ref_frame_idx = [0; 3]; + let mut ref_frame_sign_bias = [0; 3]; + if frame_type == 0 { + let sync_code = br.read(24)?; + if sync_code != 0x498342 { + return Err(DecoderError::InvalidData); + } + self.read_color_config(&mut br)?; + self.read_frame_size(&mut br)?; + self.read_render_size(&mut br)?; + intra_only = true; + self.refresh_frame_flags = 0xFF; + } else { + if self.width == 0 { + return Err(DecoderError::MissingReference)?; + } + intra_only = !show_frame && br.read_bool()?; + if !error_resilient_mode { + reset_frame_context = br.read(2)?; + } + if intra_only { + let sync_code = br.read(24)?; + if sync_code != 0x498342 { + return Err(DecoderError::InvalidData); + } + if profile > 0 { + self.read_color_config(&mut br)?; + } else { + self.cspace = 1; + self.sub_x = 1; + self.sub_y = 1; + self.depth = 8; + } + self.refresh_frame_flags = br.read(8)? as u8; + self.read_frame_size(&mut br)?; + self.read_render_size(&mut br)?; + } else { + self.refresh_frame_flags = br.read(8)? as u8; + for (idx, sign_bias) in ref_frame_idx.iter_mut().zip(ref_frame_sign_bias.iter_mut()) { + *idx = br.read(3)?; + *sign_bias = br.read(1)?; + } + let mut found = false; + for _i in 0..3 { + if br.read_bool()? { + found = true; + break; + } + } + if !found { + self.read_frame_size(&mut br)?; + } + self.read_render_size(&mut br)?; + + allow_high_precision_mv = br.read_bool()?; + if br.read_bool()? { + interp_type = 4; + } else { + const INTERP_TYPES: [u8; 4] = [1, 0, 2, 3]; + let idx = br.read(2)? as usize; + interp_type = INTERP_TYPES[idx]; + } + } + } + + let refresh_frame_context; + let frame_par_dec_mode; + if !error_resilient_mode { + refresh_frame_context = br.read_bool()?; + frame_par_dec_mode = br.read_bool()?; + } else { + refresh_frame_context = false; + frame_par_dec_mode = true; + } + let frame_context_idx = br.read(2)?; + + let loop_filter_level = br.read(6)? as i8; + let loop_filter_sharpness = br.read(3)? as u8; + let loop_filter_delta = br.read_bool()?; + let mut update_ref_delta = [false; 4]; + let mut lf_ref_deltas = [0i8; 4]; + let mut upd_mode_delta = [false; 2]; + let mut lf_mode_deltas = [0i8; 2]; + if loop_filter_delta { + let loop_filter_delta_update = br.read_bool()?; + if loop_filter_delta_update { + for (upd_ref, lf_ref) in update_ref_delta.iter_mut().zip(lf_ref_deltas.iter_mut()) { + *upd_ref = br.read_bool()?; + if *upd_ref { + *lf_ref = br.read_sgn(6)? as i8; + } + } + for (upd_ref, lf_ref) in upd_mode_delta.iter_mut().zip(lf_mode_deltas.iter_mut()) { + *upd_ref = br.read_bool()?; + if *upd_ref { + *lf_ref = br.read_sgn(6)? as i8; + } + } + } + } + + let base_q_idx = br.read(8)? as i32; + let dq_y_dc = if br.read_bool()? { br.read_sgn(4)? } else { 0 }; + let dq_uv_dc = if br.read_bool()? { br.read_sgn(4)? } else { 0 }; + let dq_uv_ac = if br.read_bool()? { br.read_sgn(4)? } else { 0 }; + let lossless = base_q_idx == 0 && dq_y_dc == 0 && dq_uv_dc == 0 && dq_uv_ac == 0; + + let q_y_dc = (base_q_idx + dq_y_dc).clamp(0, 255) as i16; + let q_y_ac = base_q_idx as i16; + let q_uv_dc = (base_q_idx + dq_uv_dc).clamp(0, 255) as i16; + let q_uv_ac = (base_q_idx + dq_uv_ac).clamp(0, 255) as i16; + let seg_enabled = br.read_bool()?; + let mut seg_update_map = false; + let mut seg_temp_update = false; + if seg_enabled { + seg_update_map = br.read_bool()?; + if seg_update_map { + for prob in self.seg_tree_probs.iter_mut() { + *prob = read_prob(&mut br)?; + } + seg_temp_update = br.read_bool()?; + if seg_temp_update { + for prob in self.seg_pred_probs.iter_mut() { + *prob = read_prob(&mut br)?; + } + } else { + for prob in self.seg_pred_probs.iter_mut() { + *prob = 255; + } + } + } + let seg_update_data = br.read_bool()?; + if seg_update_data { +unimplemented!(); + } + } + + let sb64_cols = (((self.width + 7) >> 3) + 7) >> 3; + let mut min_log2_tile_cols = 0; + while (64 << min_log2_tile_cols) < sb64_cols { + min_log2_tile_cols += 1; + } + let mut max_log2_tile_cols = 1; + while (sb64_cols >> max_log2_tile_cols) >= 4 { + max_log2_tile_cols += 1; + } + max_log2_tile_cols -= 1; + let mut tile_cols_log2 = min_log2_tile_cols; + while (tile_cols_log2 < max_log2_tile_cols) && br.read_bool()? { + tile_cols_log2 += 1; + } + let mut tile_rows_log2 = br.read(1)? as u8; + if tile_rows_log2 == 1 && br.read_bool()? { + tile_rows_log2 += 1; + } + + let header_size = br.read(16)? as u16; + let bytes_read = (br.tell() + 7) / 8; + + if vactx.surfaces.is_empty() { +panic!("ran out of free surfaces"); +// return Err(DecoderError::AllocError); + } + let surface = vactx.surfaces.pop().unwrap(); + //let surface_id = surface.id(); + let mut pic = Picture::new(timestamp, vactx.context.clone(), surface); + + let pic_fields = VP9PicFields::new(self.sub_x.into(), self.sub_y.into(), frame_type, show_frame as u32, + error_resilient_mode as u32, intra_only as u32, allow_high_precision_mv as u32, + interp_type.into(), frame_par_dec_mode as u32, + reset_frame_context, refresh_frame_context as u32, frame_context_idx, + seg_enabled as u32, seg_temp_update as u32, seg_update_map as u32, + // last, golden, altref + ref_frame_idx[0], ref_frame_sign_bias[0], + ref_frame_idx[1], ref_frame_sign_bias[1], + ref_frame_idx[2], ref_frame_sign_bias[2], + lossless as u32); + let pic_param = PictureParameterBufferVP9::new(self.width, self.height, *ref_frames, &pic_fields, + loop_filter_level as u8, loop_filter_sharpness, tile_rows_log2, tile_cols_log2, + bytes_read as u8, header_size, self.seg_tree_probs, + self.seg_pred_probs, profile as u8, self.depth); + let pic_param = BufferType::PictureParameter(PictureParameter::VP9(pic_param)); + let buf = vactx.context.create_buffer(pic_param).map_err(|_| DecoderError::Bug)?; + pic.add_buffer(buf); + + let lf_scale = if loop_filter_level >= 32 { 2 } else { 1 }; + let mut filter_level = [[0; 2]; 4]; + filter_level[0] = [loop_filter_level as u8; 2]; + for (i, (fl, &delta)) in filter_level.iter_mut().zip(lf_ref_deltas.iter()).enumerate() { + for (val, &mode) in fl.iter_mut().zip(lf_mode_deltas.iter()) { + *val = (loop_filter_level + (delta + if i > 0 { mode } else { 0 }) * lf_scale).clamp(0, 63) as u8; + } + } + let seg_flags = VP9SegmentFlags::new(0, 0, 0); // TODO support segmented stuff? + let mut seg_params = std::array::from_fn(|_i| SegmentParameterVP9::new(&seg_flags, [[0; 2]; 4], 0, 0, 0, 0)); + seg_params[0] = SegmentParameterVP9::new(&seg_flags, filter_level, q_y_ac, q_y_dc, q_uv_ac, q_uv_dc); + let slc_param = SliceParameterBufferVP9::new(src.len() as u32, 0, VASliceDataFlag::All as u32, seg_params); + let slc_param = BufferType::SliceParameter(SliceParameter::VP9(slc_param)); + let buf = vactx.context.create_buffer(slc_param).map_err(|_| DecoderError::Bug)?; + pic.add_buffer(buf); + + let pic_data = BufferType::SliceData(src.to_vec()); + let buf = vactx.context.create_buffer(pic_data).map_err(|_| DecoderError::Bug)?; + pic.add_buffer(buf); + + let cur_id = pic.surface().id(); + for (i, rf) in ref_frames.iter_mut().enumerate() { + if (self.refresh_frame_flags & (1 << i)) != 0 { + *rf = cur_id; + } + } + + let bpic = pic.begin().expect("begin"); + let rpic = bpic.render().expect("render"); + let epic = rpic.end().expect("end"); + + vactx.queue.push_back((timestamp, QFrame::Frame(frame_type == 0, show_frame, epic))); + + Ok(()) + } +} + +pub struct VaapiVP9Decoder { + info: NACodecInfoRef, + vaapi: Option, + needs_derive: bool, + tb_num: u32, + tb_den: u32, + out_frm: NABufferType, + fstate: FrameState, + ref_ids: [u32; NUM_REFS], +} + +impl Default for VaapiVP9Decoder { + fn default() -> Self { + Self { + info: NACodecInfoRef::default(), + vaapi: None, + needs_derive: false, + tb_num: 0, + tb_den: 0, + out_frm: NABufferType::None, + fstate: FrameState::default(), + ref_ids: [VA_INVALID_ID; NUM_REFS], + } + } +} + +impl VaapiVP9Decoder { + pub fn new() -> Self { Self::default() } +} + +impl HWDecoderCore for VaapiVP9Decoder { + fn init(&mut self, info: NACodecInfoRef) -> DecoderResult<()> { + if let NACodecTypeInfo::Video(vinfo) = info.get_properties() { + let width = (vinfo.get_width() + 15) & !15; + let height = (vinfo.get_height() + 15) & !15; + + let display = Display::open_silently().ok_or_else(||{ println!(" cannot open display"); DecoderError::InvalidData})?; + + let num_surfaces = NUM_REFS + MAX_SB_FRAMES; + + for (supp, &va_profile) in self.fstate.profile_supp.iter_mut().zip(VP9_PROFILES.iter()) { + if let Ok(profiles) = display.query_config_profiles() { + *supp = profiles.contains(&va_profile); + if let Ok(points) = display.query_config_entrypoints(va_profile) { + if !points.contains(&VAEntrypoint::VAEntrypointVLD) { + *supp = false; + } + } + } else { + return Err(DecoderError::Bug); + } + } + + let needs_derive= if let Ok(vendor) = display.query_vendor_string() { + vendor.contains("Kaby Lake") + } else { false }; + + let config = display.create_config(vec![ + VAConfigAttrib { type_: VAConfigAttribType::VAConfigAttribRTFormat, value: RTFormat::YUV420.into() }, + ], VP9_PROFILES[0], VAEntrypoint::VAEntrypointVLD).map_err(|_| { +println!("config creation failed!"); + DecoderError::Bug + })?; + let surfaces = display.create_surfaces(RTFormat::YUV420, None, width as u32, height as u32, Some(UsageHint::Decoder.into()), num_surfaces as u32).map_err(|_| DecoderError::AllocError)?; + let context = display.create_context(&config, width as i32, height as i32, Some(&surfaces), true).map_err(|_| DecoderError::Bug)?; + + let image_formats = display.query_image_formats().map_err(|_| DecoderError::Bug)?; + validate!(!image_formats.is_empty()); + let mut ifmt = image_formats[0]; + for fmt in image_formats.iter() { + if fmt.bits_per_pixel == 12 { + ifmt = *fmt; + break; + } + } + + let ref_pics = Vec::with_capacity(NUM_REFS); + let queue = VecDeque::with_capacity(MAX_SB_FRAMES); + self.vaapi = Some(VaapiInternals { display, context, surfaces, ifmt, ref_pics, queue }); + self.needs_derive = needs_derive; + + let vinfo = NAVideoInfo::new(vinfo.get_width(), vinfo.get_height(), false, YUV420_FORMAT); + self.info = NACodecInfo::new_ref(info.get_name(), NACodecTypeInfo::Video(vinfo), info.get_extradata()).into_ref(); + self.out_frm = alloc_video_buffer(vinfo, 4)?; + + Ok(()) + } else { + Err(DecoderError::InvalidData) + } + } + fn decode(&mut self, pkt: &NAPacket) -> DecoderResult<()> { + let src = pkt.get_buffer(); + validate!(!src.is_empty()); + let vactx = if let Some(ref mut ctx) = self.vaapi { ctx } else { return Err(DecoderError::Bug) }; + + let timestamp = pkt.get_pts().unwrap_or(0); + self.tb_num = pkt.ts.tb_num; + self.tb_den = pkt.ts.tb_den; + + let mut frame_sizes = [0; 8]; + let last_byte = src[src.len() - 1]; + if (last_byte & 0xE0) == 0xC0 { + let width = usize::from((last_byte >> 3) & 3) + 1; + let nframes = usize::from(last_byte & 7) + 1; + let sfidx_size = nframes * width + 2; + validate!(src.len() >= nframes + sfidx_size); + let mut rest_size = src.len() - sfidx_size; + let idx_pos = src.len() - sfidx_size + 1; + for (i, fsize) in frame_sizes.iter_mut().enumerate().take(nframes) { + let size_src = &src[idx_pos + width * i..]; + *fsize = match width { + 1 => usize::from(size_src[0]), + 2 => usize::from(read_u16le(size_src).unwrap_or_default()), + 3 => read_u24le(size_src).unwrap_or_default() as usize, + _ => read_u32le(size_src).unwrap_or_default() as usize, + }; + validate!(*fsize > 0 && *fsize <= rest_size); + rest_size -= *fsize; + } + } else { + frame_sizes[0] = src.len(); + } + + let mut offset = 0; + for &fsize in frame_sizes.iter() { + if fsize == 0 { break; } + self.fstate.do_frame(vactx, &src[offset..][..fsize], timestamp, &mut self.ref_ids)?; + offset += fsize; + } + + Ok(()) + } + fn get_frame(&mut self) -> Option { + if let Some(ref mut vactx) = self.vaapi { + while let Some((ts, entry)) = vactx.queue.pop_front() { + match entry { + QFrame::Frame(intra, show_frame, epic) => { + if let Ok(pic) = epic.sync() { + if show_frame { + let _ = fill_frame(vactx.ifmt, &pic, &mut self.out_frm, self.needs_derive); + } + let id = pic.surface().id(); + if self.ref_ids.contains(&id) { + vactx.ref_pics.push(pic); + } else if let Ok(surf) = pic.take_surface() { + vactx.surfaces.push(surf); + } else { + panic!("can't take surface"); + } + let mut idx = 0; + while idx < vactx.ref_pics.len() { + if !self.ref_ids.contains(&vactx.ref_pics[idx].surface().id()) { + let pic = vactx.ref_pics.remove(idx); + if let Ok(surf) = pic.take_surface() { + vactx.surfaces.push(surf); + } else { + panic!("can't take surface"); + } + } else { + idx += 1; + } + } + if !show_frame { + continue; + } + let ts = NATimeInfo::new(Some(ts), None, None, self.tb_num, self.tb_den); + return Some(NAFrame::new(ts, if intra { FrameType::I } else { FrameType::P }, intra, self.info.clone(), self.out_frm.clone()).into_ref()); + } else { + panic!("sync failed"); + } + }, + QFrame::ShowAgain(id) => { + if let Some(pic) = vactx.ref_pics.iter().find(|p| p.surface().id() == id) { + let _ = fill_frame(vactx.ifmt, pic, &mut self.out_frm, self.needs_derive); + let ts = NATimeInfo::new(Some(ts), None, None, self.tb_num, self.tb_den); + return Some(NAFrame::new(ts, FrameType::P, false, self.info.clone(), self.out_frm.clone()).into_ref()); + } + }, + } + } + } + None + } + fn get_last_frames(&mut self) -> Option { + self.get_frame() + } + fn flush(&mut self) { + if let Some(ref mut vactx) = self.vaapi { + for pic in vactx.ref_pics.drain(..) { + let surface = pic.take_surface().expect("take surface"); + vactx.surfaces.push(surface); + } + for el in self.ref_ids.iter_mut() { + *el = VA_INVALID_ID; + } + } + } +} + +impl NAOptionHandler for VaapiVP9Decoder { + fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] } + fn set_options(&mut self, _options: &[NAOption]) {} + fn query_option_value(&self, _name: &str) -> Option { None } +} + +fn new_vp9_vadec() -> Box { + Box::new(VaapiVP9Decoder::new()) +} + +pub fn new_vp9_hwdec() -> Box { + Box::new(HWWrapper::new(new_vp9_vadec)) +} + +#[cfg(test)] +mod test { + use nihav_core::codecs::*; + use nihav_core::io::byteio::*; + use nihav_core::demuxers::{RegisteredDemuxers, create_demuxer}; + use nihav_commonfmt::generic_register_all_demuxers; + use super::new_vp9_vadec; + use std::io::prelude::*; + + fn decode_vp9(name: &str, dname: &str, dmx_reg: &RegisteredDemuxers, opfx: &str, limit: Option) -> DecoderResult<()> { + let dmx_f = dmx_reg.find_demuxer(dname).expect("demuxer exists"); + let file = std::fs::File::open(name).expect("file exists"); + let mut br = FileReader::new_read(file); + let mut dmx = create_demuxer(dmx_f, &mut br).expect("create demuxer"); + + let mut vstream_id = 0; + let mut dec = new_vp9_vadec(); + for stream in dmx.get_streams() { + if stream.get_media_type() == StreamType::Video { + dec.init(stream.get_info()).expect("inited"); + vstream_id = stream.get_id(); + break; + } + } + + let mut frameno = 0; + while let Ok(pkt) = dmx.get_frame() { + if pkt.get_stream().get_id() != vstream_id { + continue; + } + dec.decode(&pkt).expect("decoded"); + let frm = dec.get_last_frames().expect("get frame"); + let timestamp = frm.get_pts().unwrap_or(0); + + let pic = frm.get_buffer().get_vbuf().expect("got picture"); + + let nname = format!("assets/test_out/{}{:06}_{}.pgm", opfx, timestamp, frameno); + frameno += 1; + let mut file = std::fs::File::create(&nname).expect("create file"); + let (w, h) = pic.get_dimensions(0); + file.write_all(format!("P5\n{} {}\n255\n", w, h * 3 / 2).as_bytes()).expect("header written"); + let data = pic.get_data(); + for yline in data.chunks(pic.get_stride(0)).take(h) { + file.write_all(&yline[..w]).expect("Y line written"); + } + for (uline, vline) in data[pic.get_offset(1)..].chunks(pic.get_stride(1)) + .zip(data[pic.get_offset(2)..].chunks(pic.get_stride(2))).take(h / 2) { + file.write_all(&uline[..w / 2]).expect("U line written"); + file.write_all(&vline[..w / 2]).expect("V line written"); + } + if Some(frameno) == limit { break; } + } + Ok(()) + } + + + // samples if not specified are from VP9 test vectors + + #[test] + fn test_vp9() { + let mut dmx_reg = RegisteredDemuxers::new(); + generic_register_all_demuxers(&mut dmx_reg); + + decode_vp9("assets/Duck/vp90-2-00-quantizer-00.webm", "matroska", &dmx_reg, "hw", Some(32)).unwrap(); + } +}