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

This commit is contained in:
Aleksander 2026-06-05 18:44:45 +02:00
parent ddf99d0b43
commit 683b1115ba
6 changed files with 178 additions and 60 deletions

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

@ -2,11 +2,15 @@ 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,
@ -24,20 +28,28 @@ use std::{
pub struct Params<'a> {
pub style: taffy::Style,
pub src: Option<AssetPath<'a>>,
pub looping: bool,
}
struct PlayingSource {
demuxer: IvfReader,
decoder: video_dec::Av1Decoder,
cur_frame: u32,
}
struct State {
demuxer: IvfReader,
decoder: video_dec::Av1Decoder,
source: Option<PlayingSource>,
self_ref: Weak<ComponentVideo>,
glyph_id: usize,
playing: bool,
play_requested: bool,
}
struct Data {
#[allow(dead_code)]
id_container: WidgetID,
id_image: WidgetID,
looping: bool,
}
#[allow(dead_code)]
@ -59,37 +71,113 @@ impl ComponentTrait for ComponentVideo {
fn refresh(&self, data: &mut RefreshData) {
let mut state = self.state.borrow_mut();
match self.read_frame(&mut state, data.layout) {
Ok(playing) => {
if !playing {
if let Err(e) = state.play(data.layout) {
log::error!("{e}");
}
return;
}
if state.play_requested {
state.play_requested = false;
if let Err(e) = self.play(&mut state, data.layout) {
log::error!("play failed: {e:?}");
}
Err(e) => {
log::error!("read_frame failed: {e}");
return;
}
}
if let Some(component) = state.self_ref.upgrade() {
data.layout.defer_component_refresh(Component(component));
}
}
}
const PLAYBACK_ANIMATION_ID: u32 = 1000;
impl ComponentVideo {
fn read_frame(&self, state: &mut State, layout: &mut Layout) -> anyhow::Result<bool> {
let Some(mut image) = layout.state.widgets.get_as::<WidgetImage>(self.data.id_image) else {
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;
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 state.decoder.read_frame(&mut state.demuxer)? {
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");
// log::info!("got EOF");
return Ok(false);
}
};
@ -101,27 +189,32 @@ impl ComponentVideo {
let glyph_content = CustomGlyphContent::Image(buffer);
image.set_content(
&mut layout.alterables,
alterables,
Some(CustomGlyphData {
id: state.glyph_id,
// force-update image content (FIXME: stream image data directly instead)
id: source.cur_frame as usize,
content: Arc::new(glyph_content),
}),
);
// force-update image content (FIXME: stream image data directly instead)
state.glyph_id += 1;
source.cur_frame += 1;
Ok(true)
}
}
impl State {
fn play(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
self.decoder = Av1Decoder::new()?;
self.demuxer.rewind();
if let Some(component) = self.self_ref.upgrade() {
layout.defer_component_refresh(Component(component));
}
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(())
}
}
@ -129,15 +222,6 @@ impl State {
pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc<ComponentVideo>)> {
let style = params.style;
let Some(src) = params.src else {
anyhow::bail!("missing src");
};
let video_data = ess.layout.state.globals.get_asset(src)?;
let demuxer = IvfReader::new(video_data)?;
let decoder = Av1Decoder::new()?;
let (root, _) = ess.layout.add_child(
ess.parent,
WidgetRectangle::create(WidgetRectangleParams {
@ -163,13 +247,14 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
let data = Rc::new(Data {
id_container,
id_image: image.id,
looping: params.looping,
});
let state = Rc::new(RefCell::new(State {
demuxer,
decoder,
source: None,
self_ref: Default::default(),
glyph_id: 0,
playing: false,
play_requested: false,
}));
let base = ComponentBase {
@ -179,7 +264,18 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
let video = Rc::new(ComponentVideo { base, data, state });
video.state.borrow_mut().self_ref = Rc::downgrade(&video);
// 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,6 +41,7 @@ pub mod sound;
pub mod stack;
pub mod task;
pub mod theme;
pub mod time;
pub mod widget;
pub mod windowing;

View File

@ -3,9 +3,7 @@ use crate::{
components::{self, Component},
layout::WidgetID,
parser::{
AttribPair, ParserContext, ParserFile, get_asset_path_from_kv,
helpers::{TooltipAttribs, parse_attrib_tooltip},
parse_children, process_component,
AttribPair, ParserContext, ParserFile, get_asset_path_from_kv, parse_children, parse_i32, process_component,
style::parse_style,
},
};
@ -18,14 +16,21 @@ pub fn parse_component_video<'a>(
attribs: &[AttribPair],
tag_name: &str,
) -> anyhow::Result<WidgetID> {
let mut tooltip = TooltipAttribs::default();
let mut src: Option<AssetPath> = None;
let mut looping: bool = false;
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 {
"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);
@ -33,15 +38,13 @@ pub fn parse_component_video<'a>(
src = Some(asset_path);
}
}
_ => {
parse_attrib_tooltip(ctx, tag_name, pair, &mut tooltip);
}
_ => {}
}
}
let (widget, video) = components::video::construct(
&mut ctx.get_construct_essentials(parent_id),
components::video::Params { style, src },
components::video::Params { style, src, looping },
)?;
process_component(ctx, Component(video), widget.id, attribs);

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
}

View File

@ -84,7 +84,7 @@ impl IvfReader {
let mut padding: [u8; 4] = [0; 4];
reader.try_copy_to_slice(&mut padding)?;
log::info!("IvfReader: width {header_width}, height {header_height}, framerate {framerate}");
log::debug!("IvfReader: width {header_width}, height {header_height}, framerate {framerate}");
let header_size = file_data.len() - reader.remaining();