diff --git a/Cargo.lock b/Cargo.lock index c7bc2cb3..d3200744 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1469,6 +1469,15 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" +[[package]] +name = "dav1d-sys" +version = "0.8.3" +source = "git+https://github.com/rust-av/dav1d-rs.git?rev=03477a36c3de4f2aacbab81a8951150ffdb9c4c3#03477a36c3de4f2aacbab81a8951150ffdb9c4c3" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "dbus" version = "0.9.11" @@ -6640,7 +6649,9 @@ name = "wgui" version = "0.1.0" dependencies = [ "anyhow", + "bytes", "cosmic-text", + "dav1d-sys", "etagere", "flate2", "glam", diff --git a/Cargo.toml b/Cargo.toml index 1550b9d5..a7a39e5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ [workspace.dependencies] anyhow = "1.0.100" +bytes = { version = "1.11.1" } clap = { version = "4.5.53", features = ["derive"] } glam = { version = "0.30.9", features = ["mint", "serde"] } idmap = "0.2.2" @@ -26,7 +27,7 @@ serde_json = "1.0.145" slotmap = "1.1.1" smol = "2.0.2" strum = { version = "0.27.2", features = ["derive"] } -uuid = { version = "1.19.0", features = ["fast-rng", "v4", "serde"] } +uuid = { version = "1.19.0", features = ["fast-rng", "serde", "v4"] } vulkano = { version = "0.35.2", default-features = false, features = [ "macros", ] } diff --git a/dash-frontend/src/tab/settings/tab_features.rs b/dash-frontend/src/tab/settings/tab_features.rs index a69113ba..96d40fc3 100644 --- a/dash-frontend/src/tab/settings/tab_features.rs +++ b/dash-frontend/src/tab/settings/tab_features.rs @@ -1,6 +1,6 @@ use crate::tab::settings::{ SettingType, SettingsMountParams, SettingsTab, - macros::{options_category, options_checkbox, options_range_f32, options_slider_f32}, + macros::{options_category, options_checkbox, options_range_f32}, }; pub struct State {} diff --git a/uidev/assets/gui/video.xml b/uidev/assets/gui/video.xml new file mode 100644 index 00000000..0f8cf5e9 --- /dev/null +++ b/uidev/assets/gui/video.xml @@ -0,0 +1,25 @@ + + + + + + + +
+
+
+
+
+
+
+
diff --git a/uidev/assets/video.ivf b/uidev/assets/video.ivf new file mode 100644 index 00000000..5f430163 Binary files /dev/null and b/uidev/assets/video.ivf differ diff --git a/wayvr-ipc/Cargo.toml b/wayvr-ipc/Cargo.toml index bcdead92..bc3fee03 100644 --- a/wayvr-ipc/Cargo.toml +++ b/wayvr-ipc/Cargo.toml @@ -8,18 +8,18 @@ authors = ["galister", "oo8dev"] repository = "https://github.com/wlx-team/wayvr" [dependencies] -bytes = "1.11.1" -smallvec = "1.13.2" -serde.workspace = true anyhow = "1.0.93" +bytes.workspace = true log = "0.4.22" +serde.workspace = true +smallvec = "1.13.2" # client-only deps interprocess = { version = "2.2.2", features = ["tokio"], optional = true } +serde_json.workspace = true tokio = { version = "1.43.1", features = ["macros"], optional = true } tokio-util = { version = "0.7.13", optional = true } -serde_json.workspace = true [features] default = ["client"] -client = ["dep:tokio", "dep:tokio-util", "dep:interprocess"] +client = ["dep:interprocess", "dep:tokio", "dep:tokio-util"] diff --git a/wayvr/Cargo.toml b/wayvr/Cargo.toml index aedac498..57431c68 100644 --- a/wayvr/Cargo.toml +++ b/wayvr/Cargo.toml @@ -42,8 +42,8 @@ vulkano.workspace = true vulkano-shaders.workspace = true xdg.workspace = true -ash = "^0.38.0" # must match vulkano -bytes = { version = "1.11.1" } +ash = "^0.38.0" # must match vulkano +bytes = { workspace = true } chrono = { version = "0.4.42", features = ["unstable-locales"] } chrono-tz = "0.10.4" config = "0.15.19" @@ -89,7 +89,7 @@ winit = { version = "0.30.12", optional = true } xcb = { version = "1.6.0", features = [ "as-raw-xcb-connection", ], optional = true } -xkbcommon = { version = "0.8.0" } # 0.9.0 breaks keymap import on some distros +xkbcommon = { version = "0.8.0" } # 0.9.0 breaks keymap import on some distros [build-dependencies] regex.workspace = true diff --git a/wgui/Cargo.toml b/wgui/Cargo.toml index 3b6ab1f8..c90a8783 100644 --- a/wgui/Cargo.toml +++ b/wgui/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/wlx-team/wayvr" anyhow.workspace = true cosmic-text = "0.15.0" etagere = "0.2.15" +flate2 = "1.1.5" glam.workspace = true image = { version = "0.25.9", default-features = false, features = [ "gif", @@ -26,6 +27,7 @@ parking_lot = "0.12.5" regex.workspace = true resvg = { version = "0.45.1", default-features = false } roxmltree = "0.21.1" +rust-embed.workspace = true rustc-hash = "2.1.1" serde_json.workspace = true slotmap.workspace = true @@ -33,5 +35,11 @@ smallvec = "1.15.1" taffy = "0.9.2" vulkano.workspace = true vulkano-shaders.workspace = true -rust-embed.workspace = true -flate2 = "1.1.5" + +# `video` wgui feature +bytes = { workspace = true, optional = true } +dav1d-sys = { git = "https://github.com/rust-av/dav1d-rs.git", rev = "03477a36c3de4f2aacbab81a8951150ffdb9c4c3", optional = true } + +[features] +default = ["video"] +video = ["dep:bytes", "dep:dav1d-sys"] diff --git a/wgui/src/animation.rs b/wgui/src/animation.rs index 2269e716..ff8c6a0d 100644 --- a/wgui/src/animation.rs +++ b/wgui/src/animation.rs @@ -49,6 +49,7 @@ pub struct CallbackData<'a> { pub widget_id: WidgetID, pub widget_boundary: Boundary, pub pos: f32, // 0.0 (start of animation) - 1.0 (end of animation) + pub stop_me: &'a mut bool, } pub type AnimationCallback = Box; @@ -93,13 +94,16 @@ impl Animation { } } - fn call(&self, state: &LayoutState, alterables: &mut EventAlterables, pos: f32) { + /// @returns false if it wants to be stopped + #[must_use] + fn call(&self, state: &LayoutState, alterables: &mut EventAlterables, pos: f32) -> bool { let Some(widget) = state.widgets.get(self.target_widget).cloned() else { - return; // failed + return false; // failed }; let mut widget_state = widget.state(); let (data, obj) = widget_state.get_data_obj_mut(); + let mut stop_me = false; let data = &mut CallbackData { widget_id: self.target_widget, @@ -107,11 +111,14 @@ impl Animation { obj, data, pos, + stop_me: &mut stop_me, }; let common = &mut CallbackDataCommon { state, alterables }; (self.callback)(common, data); + + !stop_me } } @@ -135,7 +142,7 @@ impl Animations { anim.pos = pos; if anim.last_tick { - anim.call(state, alterables, 1.0); + let _ = anim.call(state, alterables, 1.0); alterables.needs_redraw = true; } else { anim.ticks_remaining -= 1; @@ -148,7 +155,9 @@ impl Animations { pub fn process(&mut self, state: &LayoutState, alterables: &mut EventAlterables, alpha: f32) { for anim in &mut self.running_animations { let pos = anim.pos_prev.lerp(anim.pos, alpha); - anim.call(state, alterables, pos); + if !anim.call(state, alterables, pos) { + anim.ticks_remaining = 0; + } } } diff --git a/wgui/src/components/mod.rs b/wgui/src/components/mod.rs index d91e7d71..bc00f366 100644 --- a/wgui/src/components/mod.rs +++ b/wgui/src/components/mod.rs @@ -18,6 +18,9 @@ pub mod slider; pub mod tabs; pub mod tooltip; +#[cfg(feature = "video")] +pub mod video; + pub struct RefreshData<'a> { pub layout: &'a mut Layout, } diff --git a/wgui/src/components/video.rs b/wgui/src/components/video.rs new file mode 100644 index 00000000..9d709114 --- /dev/null +++ b/wgui/src/components/video.rs @@ -0,0 +1,285 @@ +use image::ImageBuffer; +use taffy::prelude::percent; + +use crate::{ + animation::Animation, + assets::AssetPath, + components::{Component, ComponentBase, ComponentTrait, RefreshData}, + drawing::Color, + event::EventAlterables, + globals::WguiGlobals, + layout::{Layout, WidgetID, WidgetPair}, + renderer_vk::text::custom_glyph::{CustomGlyphContent, CustomGlyphData}, + time::get_millis, + video_dec::{self, Av1Decoder, IvfReader}, + widget::{ + ConstructEssentials, + image::WidgetImage, + rectangle::{WidgetRectangle, WidgetRectangleParams}, + }, +}; +use std::{ + cell::RefCell, + rc::{Rc, Weak}, + sync::Arc, +}; + +#[derive(Default)] +pub struct Params<'a> { + pub style: taffy::Style, + pub src: Option>, + pub looping: bool, + pub speed: f32, +} + +struct PlayingSource { + demuxer: IvfReader, + decoder: video_dec::Av1Decoder, + cur_frame: u32, +} + +struct State { + source: Option, + self_ref: Weak, + playing: bool, + play_requested: bool, +} + +struct Data { + #[allow(dead_code)] + id_container: WidgetID, + id_image: WidgetID, + + looping: bool, + speed: f32, +} + +#[allow(dead_code)] +pub struct ComponentVideo { + base: ComponentBase, + data: Rc, + state: Rc>, +} + +impl ComponentTrait for ComponentVideo { + fn base(&self) -> &ComponentBase { + &self.base + } + + fn base_mut(&mut self) -> &mut ComponentBase { + &mut self.base + } + + fn refresh(&self, data: &mut RefreshData) { + let mut state = self.state.borrow_mut(); + + if state.play_requested { + state.play_requested = false; + if let Err(e) = self.play(&mut state, data.layout) { + log::error!("play failed: {e:?}"); + } + } + } +} + +const PLAYBACK_ANIMATION_ID: u32 = 1000; + +impl ComponentVideo { + fn play(&self, state: &mut State, layout: &mut Layout) -> anyhow::Result<()> { + let Some(source) = &mut state.source else { + // no source available, do nothing + return Ok(()); + }; + + state.playing = false; + source.decoder = Av1Decoder::new()?; + source.demuxer.rewind(); + source.cur_frame = 0; + + if let Some(component) = state.self_ref.upgrade() { + layout.defer_component_refresh(Component(component)); + } + + layout + .animations + .stop_by_widget(self.data.id_image, Some(PLAYBACK_ANIMATION_ID)); + + state.playing = true; + + let framerate = source.demuxer.framerate * self.data.speed; + let looping = self.data.looping; + let id_image = self.data.id_image; + let start_time = get_millis(); + // log::info!("num frames: {}", source.demuxer.num_frames); + + layout.animations.add(Animation::new_ex( + self.data.id_image, + PLAYBACK_ANIMATION_ID, + u32::MAX, // infinity + crate::animation::AnimationEasing::Linear, + Box::new({ + let state_ref = self.state.clone(); + + move |common, data| { + let mut state = state_ref.borrow_mut(); + if !state.playing { + *data.stop_me = true; + return; + } + + loop { + let Some(source) = &mut state.source else { + return; + }; + + let cur_time = get_millis() - start_time; + let target_frame = ((cur_time as f32) / 1000.0 * framerate) as u32; + + let cur_frame = source.cur_frame; + if cur_frame >= target_frame { + break; + } + + let image = data.obj.cast_mut::().unwrap(); + + match ComponentVideo::read_next_frame(image, &mut state, common.alterables) { + Ok(data_available) => { + if !data_available { + if looping { + state.play_requested = true; + common.alterables.refresh_component_once(&state.self_ref); + common.mark_widget_dirty(id_image); + } else { + state.playing = false; + } + break; + } + } + Err(e) => { + state.playing = false; + log::error!("read_next_frame failed: {e:?}"); + } + } + } + } + }), + )); + Ok(()) + } + + fn read_next_frame( + image: &mut WidgetImage, + state: &mut State, + alterables: &mut EventAlterables, + ) -> anyhow::Result { + let Some(source) = &mut state.source else { + return Ok(false); + }; + + let rgbx_frame = match source.decoder.read_frame(&mut source.demuxer)? { + video_dec::ReadFrameResult::Ok(rgbx_frame) => rgbx_frame, + video_dec::ReadFrameResult::EndOfFile => { + // log::info!("got EOF"); + return Ok(false); + } + }; + + let Some(buffer) = ImageBuffer::from_raw(rgbx_frame.width.into(), rgbx_frame.height.into(), rgbx_frame.data) else { + anyhow::bail!("ImageBuffer failed"); + }; + + let glyph_content = CustomGlyphContent::Image(buffer); + + image.set_content( + alterables, + Some(CustomGlyphData { + // force-update image content (FIXME: stream image data directly instead) + id: source.cur_frame as usize, + content: Arc::new(glyph_content), + }), + ); + + source.cur_frame += 1; + + Ok(true) + } +} + +impl State { + fn set_source(&mut self, globals: &WguiGlobals, src: AssetPath) -> anyhow::Result<()> { + let video_data = globals.get_asset(src)?; + let demuxer = IvfReader::new(video_data)?; + let decoder = Av1Decoder::new()?; + + self.source = Some(PlayingSource { + demuxer, + decoder, + cur_frame: 0, + }); + + Ok(()) + } +} + +pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc)> { + let style = params.style; + + let (root, _) = ess.layout.add_child( + ess.parent, + WidgetRectangle::create(WidgetRectangleParams { + color: Color::new(0.1, 0.1, 0.1, 1.0), + ..Default::default() + }), + style, + )?; + + let (image, _) = ess.layout.add_child( + root.id, + WidgetImage::create(Default::default()), + taffy::Style { + size: taffy::Size { + width: percent(1.0), + height: percent(1.0), + }, + ..Default::default() + }, + )?; + + let id_container = root.id; + let data = Rc::new(Data { + id_container, + id_image: image.id, + looping: params.looping, + speed: params.speed, + }); + + let state = Rc::new(RefCell::new(State { + source: None, + self_ref: Default::default(), + playing: false, + play_requested: false, + })); + + let base = ComponentBase { + id: root.id, + lhandles: Default::default(), + }; + + let video = Rc::new(ComponentVideo { base, data, state }); + + // configure state and set video source + { + let mut state = video.state.borrow_mut(); + state.self_ref = Rc::downgrade(&video); + if let Some(src) = params.src + && let Err(e) = state.set_source(&ess.layout.state.globals, src) + { + log::error!("set_source failed: {e:?}"); + } + + state.play_requested = true; + } + + ess.layout.defer_component_refresh(Component(video.clone())); + Ok((root, video)) +} diff --git a/wgui/src/lib.rs b/wgui/src/lib.rs index d913f0bf..7be1645e 100644 --- a/wgui/src/lib.rs +++ b/wgui/src/lib.rs @@ -41,9 +41,13 @@ pub mod sound; pub mod stack; pub mod task; pub mod theme; +pub mod time; pub mod widget; pub mod windowing; +#[cfg(feature = "video")] +pub mod video_dec; + // re-exported libs pub use cosmic_text; pub use taffy; diff --git a/wgui/src/parser/component_video.rs b/wgui/src/parser/component_video.rs new file mode 100644 index 00000000..90233002 --- /dev/null +++ b/wgui/src/parser/component_video.rs @@ -0,0 +1,63 @@ +use crate::{ + assets::AssetPath, + components::{self, Component}, + layout::WidgetID, + parser::{ + AttribPair, ParserContext, ParserFile, get_asset_path_from_kv, parse_children, parse_f32, parse_i32, + process_component, style::parse_style, + }, +}; + +pub fn parse_component_video<'a>( + file: &'a ParserFile, + ctx: &mut ParserContext, + node: roxmltree::Node<'a, 'a>, + parent_id: WidgetID, + attribs: &[AttribPair], + tag_name: &str, +) -> anyhow::Result { + let mut src: Option = None; + let mut looping: bool = false; + let mut speed: f32 = 1.0; + + let style = parse_style(ctx, attribs, tag_name); + + for pair in attribs { + let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref()); + match key { + "speed" => { + speed = parse_f32(value).unwrap_or(speed); + } + "looping" => { + if let Some(v) = parse_i32(value) + && v != 0 + { + looping = true; + } + } + "src" | "src_ext" | "src_builtin" | "src_internal" => { + let asset_path = get_asset_path_from_kv("", key, value); + + if !value.is_empty() { + src = Some(asset_path); + } + } + _ => {} + } + } + + let (widget, video) = components::video::construct( + &mut ctx.get_construct_essentials(parent_id), + components::video::Params { + style, + src, + looping, + speed, + }, + )?; + + process_component(ctx, Component(video), widget.id, attribs); + parse_children(file, ctx, node, widget.id)?; + + Ok(widget.id) +} diff --git a/wgui/src/parser/mod.rs b/wgui/src/parser/mod.rs index a9510ae0..f17fc256 100644 --- a/wgui/src/parser/mod.rs +++ b/wgui/src/parser/mod.rs @@ -6,6 +6,10 @@ mod component_editbox; mod component_radio_group; mod component_slider; mod component_tabs; + +#[cfg(feature = "video")] +mod component_video; + mod helpers; mod style; mod widget_div; @@ -1084,6 +1088,14 @@ fn parse_child<'a>( file, ctx, child_node, parent_id, &attribs, tag_name, )?); } + #[cfg(feature = "video")] + "Video" => { + use crate::parser::component_video::parse_component_video; + + new_widget_id = Some(parse_component_video( + file, ctx, child_node, parent_id, &attribs, tag_name, + )?); + } "Slider" => { new_widget_id = Some(parse_component_slider(ctx, parent_id, &attribs, tag_name)?); } diff --git a/wgui/src/time.rs b/wgui/src/time.rs new file mode 100644 index 00000000..f62d2c5d --- /dev/null +++ b/wgui/src/time.rs @@ -0,0 +1,9 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +// Returns milliseconds since unix epoch +pub fn get_millis() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u64 +} diff --git a/wgui/src/video_dec.rs b/wgui/src/video_dec.rs new file mode 100644 index 00000000..380f67fa --- /dev/null +++ b/wgui/src/video_dec.rs @@ -0,0 +1,339 @@ +use std::ptr::null_mut; + +use bytes::{Buf, Bytes}; +use dav1d_sys::{ + DAV1D_ERR_AGAIN, Dav1dContext, Dav1dData, Dav1dPicture, Dav1dSettings, dav1d_close, dav1d_default_settings, + dav1d_get_picture, dav1d_open, dav1d_picture_unref, dav1d_send_data, +}; + +pub struct IvfReader { + file_data: Vec, // Used by `reader`, do not remove! + reader: Bytes, + header_size: usize, + pub cur_frame: u32, + pub num_frames: u32, + pub framerate: f32, + pub width: u16, + pub height: u16, +} + +// Duck IVF video container. Documentation: +// https://wiki.multimedia.cx/index.php/Duck_IVF +// Yes, it's just as simple as that. +// +// Header: +// bytes 0-3 signature: 'DKIF' +// bytes 4-5 version (should be 0) +// bytes 6-7 length of header in bytes +// bytes 8-11 codec FourCC (e.g., 'VP80') +// bytes 12-13 width in pixels +// bytes 14-15 height in pixels +// bytes 16-19 time base denominator +// bytes 20-23 time base numerator +// bytes 24-27 number of `Frame`s in file +// bytes 28-31 unused +// +// Frame: +// bytes 0-3 size of frame in bytes (not including the 12-byte header) +// bytes 4-11 64-bit presentation timestamp +// bytes 12.. frame data + +const IVF_MAGIC: [u8; 4] = [0x44, 0x4B, 0x49, 0x46]; + +pub enum IvfReadFrameResult<'a> { + Ok((&'a [u8], u64 /* pts */)), + EndOfFile, +} + +impl IvfReader { + pub fn new(file_data: Vec) -> anyhow::Result { + // safety: both reader and file_data are located in the same struct + // file_data won't move at all. + let mut reader = Bytes::from_static(unsafe { std::mem::transmute::<&[u8], &'static [u8]>(&file_data) }); + + // read ivf magic + let mut header_magic: [u8; 4] = [0; 4]; + reader.try_copy_to_slice(&mut header_magic)?; + + if header_magic != IVF_MAGIC { + anyhow::bail!("invalid magic"); + } + + let header_version = reader.try_get_u16_le()?; + + if header_version != 0 { + anyhow::bail!("unsupported version"); // there's no other version than 0 + } + + let header_len = reader.try_get_u16_le()?; + if header_len != 32 { + anyhow::bail!("header length mismatching"); + } + + let mut header_fourcc: [u8; 4] = [0; 4]; + reader.try_copy_to_slice(&mut header_fourcc)?; + + let header_width = reader.try_get_u16_le()?; + let header_height = reader.try_get_u16_le()?; + let header_timebase_den = reader.try_get_u32_le()?; + let header_timebase_num = reader.try_get_u32_le()?; + let header_num_frames = reader.try_get_u32_le()?; + + let framerate = header_timebase_den as f32 / header_timebase_num as f32; + + let mut padding: [u8; 4] = [0; 4]; + reader.try_copy_to_slice(&mut padding)?; + + log::debug!("IvfReader: width {header_width}, height {header_height}, framerate {framerate}"); + + let header_size = file_data.len() - reader.remaining(); + + Ok(IvfReader { + header_size, + file_data, + reader, + cur_frame: 0, + width: header_width, + height: header_height, + framerate, + num_frames: header_num_frames, + }) + } + + pub fn rewind(&mut self) { + self.reader = Bytes::from_static(unsafe { std::mem::transmute::<&[u8], &'static [u8]>(&self.file_data) }); + // do not read header again + self.reader.advance(self.header_size); + } + + // Read demuxed video packet to a chunk + pub fn read_frame(&mut self) -> anyhow::Result> { + if self.reader.remaining() == 0 { + return Ok(IvfReadFrameResult::EndOfFile); + } + + let frame_size = self.reader.try_get_u32_le()? as usize; + let frame_pts = self.reader.try_get_u64_le()?; + + if frame_size > 8 * 1024 * 1024 { + // something went really wrong + anyhow::bail!("Invalid frame size {frame_size}"); + } + + // SAFETY: reader.chunk() slice lifetime is the same as Self, no risk here. + let chunk_a /* 'a */ = unsafe /* it's safe */ { + let Some(chunk) = self.reader.chunk().get(0..(frame_size)) else { + anyhow::bail!("chunk read error"); + }; + std::mem::transmute::<&[u8], &'static [u8]>(chunk) + }; + + self.reader.advance(frame_size); + + self.cur_frame += 1; + + Ok(IvfReadFrameResult::Ok((chunk_a, frame_pts))) + } +} + +// RGB with 255 A +pub struct RgbxFrame { + pub width: u16, + pub height: u16, + pub data: Vec, +} + +fn yuv_to_rgb(y: u8, u: u8, v: u8) -> (u8, u8, u8) { + let y_i = i32::from(y); + let u_i = i32::from(u) - 128; + let v_i = i32::from(v) - 128; + let c = (1192 * (y_i - 16)).max(0); + let out_r = (c + 1634 * v_i) >> 10; + let out_g = (c - 401 * u_i - 834 * v_i) >> 10; + let out_b = (c + 2066 * u_i) >> 10; + ( + out_r.clamp(0, 255) as u8, + out_g.clamp(0, 255) as u8, + out_b.clamp(0, 255) as u8, + ) +} + +impl RgbxFrame { + // Simple, temporary, best-effort yuv420->rgb converter. Not performant at all, but at least it works. + // Temporary till we get native YUV support in shaders (as 3 vulkan textures). + // SAFETY: this function expects YUV420 data only. + #[allow(clippy::uninit_vec)] + fn from_yuv( + width: u16, + height: u16, + data_y: *const u8, + data_u: *const u8, + data_v: *const u8, + stride_luma: usize, + stride_chroma: usize, + ) -> RgbxFrame { + unsafe { + let channels = 4; // rgbx + + let rgbx_size = width as usize * height as usize * channels; + let mut rgbx = Vec::::with_capacity(rgbx_size); + rgbx.set_len(rgbx_size); + + let ptr_rgbx = rgbx.as_mut_ptr(); + + for y in 0..height as usize { + for x in 0..width as usize { + let val_y = *data_y.add(y * stride_luma + x); + let val_u = *data_u.add((y / 2) * stride_chroma + (x / 2)); + let val_v = *data_v.add((y / 2) * stride_chroma + (x / 2)); + let (r, g, b) = yuv_to_rgb(val_y, val_u, val_v); + + let pos = y * (width as usize * channels) + x * channels; + *ptr_rgbx.add(pos) = r; + *ptr_rgbx.add(pos + 1) = g; + *ptr_rgbx.add(pos + 2) = b; + *ptr_rgbx.add(pos + 3) = 255; + } + } + + RgbxFrame { + width, + height, + data: rgbx, + } + } + } +} + +pub struct Av1Decoder { + ctx: *mut Dav1dContext, +} + +impl Drop for Av1Decoder { + fn drop(&mut self) { + unsafe { + dav1d_close(&raw mut self.ctx); + } + } +} + +enum GetPictureResult { + Ok, + Again, + Failed(i32), +} + +enum ReadFrameIterResult { + Ok(RgbxFrame), + Again, + EndOfFile, +} + +pub enum ReadFrameResult { + Ok(RgbxFrame), + EndOfFile, +} + +impl Av1Decoder { + pub fn new() -> anyhow::Result { + unsafe { + let mut set: Dav1dSettings = std::mem::zeroed(); + dav1d_default_settings(&raw mut set); + + let mut ctx = null_mut(); + let ret = dav1d_open(&raw mut ctx, &raw mut set); + if ret < 0 { + anyhow::bail!("dav1d_open failed: {ret}"); + } + Ok(Self { ctx }) + } + } + + fn get_picture(&mut self, picture: &mut Dav1dPicture) -> GetPictureResult { + let ret = unsafe { dav1d_get_picture(self.ctx, picture) }; + if ret == DAV1D_ERR_AGAIN { + return GetPictureResult::Again; + } + + if ret < 0 { + return GetPictureResult::Failed(ret); + } + + GetPictureResult::Ok + } + + fn send_data(&mut self, reader: &mut IvfReader) -> anyhow::Result { + unsafe { + let (packet, pts) = match reader.read_frame()? { + IvfReadFrameResult::Ok((packet, pts)) => (packet, pts), + IvfReadFrameResult::EndOfFile => return Ok(false), + }; + let mut data: Dav1dData = std::mem::zeroed(); + data.data = packet.as_ptr(); + data.sz = packet.len(); + data.m.timestamp = pts as i64; + data.m.size = packet.len(); + data.m.offset = -1; + data.m.duration = 0; + + let ret = dav1d_send_data(self.ctx, &raw mut data); + if ret < 0 { + anyhow::bail!("dav1d_send_data failed: {ret}"); + } + + Ok(true) + } + } + + fn read_frame_iter(&mut self, reader: &mut IvfReader) -> anyhow::Result { + unsafe { + let mut picture: Dav1dPicture = std::mem::zeroed(); + match self.get_picture(&mut picture) { + GetPictureResult::Ok => {} + GetPictureResult::Again => { + dav1d_picture_unref(&raw mut picture); + if !self.send_data(reader)? { + return Ok(ReadFrameIterResult::EndOfFile); + } + return Ok(ReadFrameIterResult::Again); + } + GetPictureResult::Failed(code) => { + dav1d_picture_unref(&raw mut picture); + anyhow::bail!("dav1d_get_picture failed: {code}"); + } + } + + let pic_y = picture.data[0]; // luma Y + let pic_u = picture.data[1]; // chroma U + let pic_v = picture.data[2]; // chroma V + let stride_luma = picture.stride[0]; + let stride_chroma = picture.stride[1]; + + let rgbx = RgbxFrame::from_yuv( + reader.width, + reader.height, + pic_y as *const u8, + pic_u as *const u8, + pic_v as *const u8, + stride_luma as usize, + stride_chroma as usize, + ); + + dav1d_picture_unref(&raw mut picture); + + Ok(ReadFrameIterResult::Ok(rgbx)) + } + } + + pub fn read_frame(&mut self, reader: &mut IvfReader) -> anyhow::Result { + loop { + match self.read_frame_iter(reader)? { + ReadFrameIterResult::Ok(rgbx_frame) => { + return Ok(ReadFrameResult::Ok(rgbx_frame)); + } + ReadFrameIterResult::Again => { /* loop again */ } + ReadFrameIterResult::EndOfFile => return Ok(ReadFrameResult::EndOfFile), + } + } + } +}