wgui: <Video> support [Squash]

commit da9ad32fe4
Author: Aleksander <aleksander@oo8.dev>
Date:   Sat Jun 6 14:44:15 2026 +0200

    wgui: video: variable speed control

commit 683b1115ba
Author: Aleksander <aleksander@oo8.dev>
Date:   Fri Jun 5 18:44:45 2026 +0200

    wgui: video: framerate control, `looping` parameter in <Video>

commit ddf99d0b43
Author: Aleksander <aleksander@oo8.dev>
Date:   Fri Jun 5 12:19:44 2026 +0200

    do not break toml 1.0 syntax compatibility

commit 69803ec6cd
Author: Aleksander <aleksander@oo8.dev>
Date:   Fri Jun 5 12:08:25 2026 +0200

    wgui: dav1d video playback works

commit 5c929c4fee
Author: Aleksander <aleksander@oo8.dev>
Date:   Thu Jun 4 23:09:30 2026 +0200

    wgui: use dav1d (wip)

commit 3c2ac4b5f1
Author: Aleksander <aleksander@oo8.dev>
Date:   Thu Jun 4 21:34:44 2026 +0200

    wgui: <Video> tag (wip), Duck IVF parser (video container), add `video` feature
This commit is contained in:
Aleksander 2026-06-06 16:17:40 +02:00
parent 00d4e35dd8
commit eac1f08e5c
16 changed files with 785 additions and 16 deletions

11
Cargo.lock generated
View File

@ -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",

View File

@ -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",
] }

View File

@ -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 {}

View File

@ -0,0 +1,25 @@
<layout>
<theme>
<var key="width" value="426.666" />
<var key="height" value="133.333" />
</theme>
<elements>
<rectangle position="absolute" width="10000" height="10000" color="#112233"/>
<div flex_direction="row">
<div margin="16" gap="8" flex_direction="column" overflow_y="scroll" >
<label text="640x200, looping"/>
<Video src_builtin="video.ivf" width="~width" height="~height" looping="1"/>
<label text="640x200, non-looping "/>
<Video src_builtin="video.ivf" width="~width" height="~height" looping="0"/>
</div>
<div margin="16" gap="8" flex_direction="column" overflow_y="scroll" >
<label text="640x200, looping, speed 2x"/>
<Video src_builtin="video.ivf" width="~width" height="~height" looping="1" speed="2"/>
<label text="640x200, looping, speed 0.5x"/>
<Video src_builtin="video.ivf" width="~width" height="~height" looping="1" speed="0.5"/>
<label text="640x200, looping, speed 4x"/>
<Video src_builtin="video.ivf" width="~width" height="~height" looping="1" speed="4"/>
</div>
</div>
</elements>
</layout>

BIN
uidev/assets/video.ivf Normal file

Binary file not shown.

View File

@ -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"]

View File

@ -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

View File

@ -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"]

View File

@ -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<dyn Fn(&mut CallbackDataCommon, &mut CallbackData)>;
@ -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;
}
}
}

View File

@ -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,
}

View File

@ -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<AssetPath<'a>>,
pub looping: bool,
pub speed: f32,
}
struct PlayingSource {
demuxer: IvfReader,
decoder: video_dec::Av1Decoder,
cur_frame: u32,
}
struct State {
source: Option<PlayingSource>,
self_ref: Weak<ComponentVideo>,
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<Data>,
state: Rc<RefCell<State>>,
}
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::<WidgetImage>().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<bool> {
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<ComponentVideo>)> {
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))
}

View File

@ -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;

View File

@ -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<WidgetID> {
let mut src: Option<AssetPath> = 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)
}

View File

@ -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)?);
}

9
wgui/src/time.rs Normal file
View File

@ -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
}

339
wgui/src/video_dec.rs Normal file
View File

@ -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<u8>, // 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<u8>) -> anyhow::Result<IvfReader> {
// 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<IvfReadFrameResult<'_>> {
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<u8>,
}
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::<u8>::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<Self> {
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<bool> {
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<ReadFrameIterResult> {
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<ReadFrameResult> {
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),
}
}
}
}