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/video.rs b/wgui/src/components/video.rs index c080b206..657355c7 100644 --- a/wgui/src/components/video.rs +++ b/wgui/src/components/video.rs @@ -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>, + pub looping: bool, +} + +struct PlayingSource { + demuxer: IvfReader, + decoder: video_dec::Av1Decoder, + cur_frame: u32, } struct State { - demuxer: IvfReader, - decoder: video_dec::Av1Decoder, + source: Option, self_ref: Weak, - 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 { - let Some(mut image) = layout.state.widgets.get_as::(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::().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 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)> { 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)) diff --git a/wgui/src/lib.rs b/wgui/src/lib.rs index 6711b1e5..7be1645e 100644 --- a/wgui/src/lib.rs +++ b/wgui/src/lib.rs @@ -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; diff --git a/wgui/src/parser/component_video.rs b/wgui/src/parser/component_video.rs index 3908aa37..3882cddc 100644 --- a/wgui/src/parser/component_video.rs +++ b/wgui/src/parser/component_video.rs @@ -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 { - let mut tooltip = TooltipAttribs::default(); let mut src: Option = 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); 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 index b12c241e..380f67fa 100644 --- a/wgui/src/video_dec.rs +++ b/wgui/src/video_dec.rs @@ -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();