mirror of https://github.com/wayvr-org/wayvr.git
wgui: video: framerate control, `looping` parameter in <Video>
This commit is contained in:
parent
ddf99d0b43
commit
683b1115ba
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue