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),
+ }
+ }
+ }
+}