use alloc::vec::Vec;
use azul_core::callbacks::{Update, VirtualViewCallbackInfo, VirtualViewReturn};
use azul_core::dom::{ComponentEventFilter, DatasetMergeCallbackType, Dom, EventFilter, OptionDom};
use azul_core::geom::LogicalPosition;
use azul_core::refany::{OptionRefAny, RefAny};
use azul_core::resources::{ImageRef, RawImage, RawImageData, RawImageFormat};
use azul_core::task::{ThreadId, ThreadReceiver, ThreadSendMsg};
use azul_core::video::{VideoConfig, VideoFrame};
use super::capture_common::{
invoke_on_frame, OnVideoFrame, OnVideoFrameCallback, OptionOnVideoFrame,
};
use crate::callbacks::{Callback, CallbackInfo, CallbackType};
use crate::thread::{
Thread, ThreadCallback, ThreadReceiveMsg, ThreadSender, ThreadWriteBackMsg, WriteBackCallback,
};
const DEFAULT_W: u32 = 1280;
const DEFAULT_H: u32 = 720;
#[derive(Debug)]
pub struct VideoWidgetState {
pub config: VideoConfig,
pub started: bool,
pub gl_texture_id: Option<u32>,
pub on_frame: OptionOnVideoFrame,
pub frames: OptionRefAny,
pub decode_callback: Option<ThreadCallback>,
pub current_frame: Option<ImageRef>,
pub thread_id: Option<ThreadId>,
pub seek_sender: Option<std::sync::mpsc::Sender<ThreadSendMsg>>,
}
#[repr(C)]
#[derive(Debug)]
pub struct VideoWidget {
pub config: VideoConfig,
pub on_frame: OptionOnVideoFrame,
pub frames: OptionRefAny,
}
impl VideoWidget {
#[must_use] pub const fn create(config: VideoConfig) -> Self {
Self {
config,
on_frame: OptionOnVideoFrame::None,
frames: OptionRefAny::None,
}
}
pub fn set_on_frame<C: Into<OnVideoFrameCallback>>(&mut self, data: RefAny, on_frame: C) {
self.on_frame = Some(OnVideoFrame {
refany: data,
callback: on_frame.into(),
})
.into();
}
#[must_use]
pub fn with_on_frame<C: Into<OnVideoFrameCallback>>(
mut self,
data: RefAny,
on_frame: C,
) -> Self {
self.set_on_frame(data, on_frame);
self
}
#[must_use] pub fn with_frames(mut self, frames: RefAny) -> Self {
self.frames = Some(frames).into();
self
}
fn build_dom(self, decode_cb: Option<ThreadCallback>) -> Dom {
let state = VideoWidgetState {
config: self.config,
started: false,
gl_texture_id: None,
on_frame: self.on_frame,
frames: self.frames,
decode_callback: decode_cb,
current_frame: None,
thread_id: None,
seek_sender: None,
};
let dataset = RefAny::new(state);
let vv_data = dataset.clone();
Dom::create_div()
.with_dataset(OptionRefAny::Some(dataset.clone()))
.with_merge_callback(azul_core::dom::DatasetMergeCallback::from_ptr(merge_video_state))
.with_callback(
EventFilter::Component(ComponentEventFilter::AfterMount),
dataset.clone(),
Callback::from_ptr(video_on_after_mount),
)
.with_callback(
EventFilter::Component(ComponentEventFilter::NodeResized),
dataset,
Callback::from_ptr(video_on_resize),
)
.with_child(
Dom::create_virtual_view(
vv_data,
azul_core::callbacks::VirtualViewCallback::create(video_widget_render),
)
.with_css("width: 100%; height: 100%; overflow: hidden;"),
)
}
#[must_use] pub fn dom(self) -> Dom {
self.build_dom(None)
}
#[must_use] pub fn dom_with_decoder(self, cb: ThreadCallback) -> Dom {
self.build_dom(Some(cb))
}
}
extern "C" fn video_widget_render(
mut data: RefAny,
info: VirtualViewCallbackInfo,
) -> VirtualViewReturn {
let bounds = info.get_bounds().get_logical_size();
if std::env::var("AZ_VIDEO_FRAMELOG").is_ok() {
eprintln!("[vrender] bounds {}x{}", bounds.width, bounds.height);
}
let dom = if !bounds.width.is_finite()
|| !bounds.height.is_finite()
|| bounds.width <= 0.0
|| bounds.height <= 0.0
{
OptionDom::None
} else {
data.downcast_ref::<VideoWidgetState>().map_or(OptionDom::None, |s| {
s.current_frame.as_ref().map_or(OptionDom::None, |img| {
OptionDom::Some(
Dom::create_image(img.clone()).with_css("width: 100%; height: 100%;"),
)
})
})
};
VirtualViewReturn {
dom,
scroll_size: bounds,
scroll_offset: LogicalPosition::zero(),
virtual_scroll_size: bounds,
virtual_scroll_offset: LogicalPosition::zero(),
}
}
extern "C" fn video_on_after_mount(mut data: RefAny, mut info: CallbackInfo) -> Update {
let (decode_cb, config, frames) = {
let Some(mut s) = data.downcast_mut::<VideoWidgetState>() else {
return Update::DoNothing;
};
if s.started {
return Update::DoNothing;
}
s.started = true;
let frames = match &s.frames {
OptionRefAny::Some(f) => Some(f.clone()),
OptionRefAny::None => None,
};
(s.decode_callback.clone(), s.config.clone(), frames)
};
if let Some(cb) = decode_cb {
let init = RefAny::new(config);
let tid = ThreadId::unique();
let thread = Thread::create(init, data.clone(), cb);
let seek_sender = thread.clone_sender();
info.add_thread(tid, thread);
if let Some(mut s) = data.downcast_mut::<VideoWidgetState>() {
s.thread_id = Some(tid);
s.seek_sender = seek_sender;
}
} else if let Some(frames) = frames {
info.add_thread(
ThreadId::unique(),
Thread::create(frames, data.clone(), ThreadCallback::new(video_replay_worker)),
);
} else {
info.add_thread(
ThreadId::unique(),
Thread::create(
RefAny::new(()),
data.clone(),
ThreadCallback::new(video_test_worker),
),
);
}
Update::DoNothing
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] extern "C" fn video_on_resize(mut data: RefAny, mut info: CallbackInfo) -> Update {
let tid = match data.downcast_ref::<VideoWidgetState>() {
Some(s) => s.thread_id,
None => return Update::DoNothing,
};
let Some(tid) = tid else {
return Update::DoNothing;
};
let node = info.get_hit_node();
let Some(size) = info.get_node_size(node) else {
return Update::DoNothing;
};
let target = (size.width.max(1.0) as u32, size.height.max(1.0) as u32);
if let Some(thread) = info.get_thread(&tid) {
let _ = thread.send_message(ThreadSendMsg::Custom(RefAny::new(target)));
}
Update::DoNothing
}
#[allow(clippy::cast_possible_truncation)] extern "C" fn video_test_worker(_init: RefAny, mut sender: ThreadSender, _recv: ThreadReceiver) {
const BARS: [[u8; 3]; 7] = [
[235, 235, 235],
[235, 235, 16],
[16, 235, 235],
[16, 235, 16],
[235, 16, 235],
[235, 16, 16],
[16, 16, 235],
];
let (w, h) = (DEFAULT_W as usize, DEFAULT_H as usize);
let mut tick: u32 = 0;
loop {
let shift = (tick as usize / 4) % 7;
let mut bytes = Vec::with_capacity(w * h * 4);
for _y in 0..h {
for x in 0..w {
let c = BARS[((x * 7 / w) + shift) % 7];
bytes.extend_from_slice(&[c[0], c[1], c[2], 255]);
}
}
let frame = VideoFrame {
width: w as u32,
height: h as u32,
bytes: bytes.into(),
};
let sent = sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
WriteBackCallback::new(video_writeback),
RefAny::new(frame),
)));
if !sent {
break;
}
std::thread::sleep(std::time::Duration::from_millis(33));
tick = tick.wrapping_add(2);
}
}
extern "C" fn video_replay_worker(mut init: RefAny, mut sender: ThreadSender, _recv: ThreadReceiver) {
let frames: Vec<VideoFrame> = match init.downcast_ref::<Vec<VideoFrame>>() {
Some(f) => f.clone(),
None => return,
};
if frames.is_empty() {
return;
}
let mut idx: usize = 0;
loop {
let frame = frames[idx % frames.len()].clone();
let sent = sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
WriteBackCallback::new(video_writeback),
RefAny::new(frame),
)));
if !sent {
break;
}
std::thread::sleep(std::time::Duration::from_millis(33));
idx = idx.wrapping_add(1);
}
}
#[must_use] pub extern "C" fn video_writeback(
mut writeback_data: RefAny,
mut frame_data: RefAny,
mut info: CallbackInfo,
) -> Update {
let hook = writeback_data.downcast_ref::<VideoWidgetState>().map_or_else(|| OptionOnVideoFrame::None, |s| s.on_frame.clone());
let mut user_update = Update::DoNothing;
match frame_data.downcast_ref::<VideoFrame>() {
Some(frame) => {
if let Some(img) = ImageRef::new_rawimage(RawImage {
pixels: RawImageData::U8(frame.bytes.clone()),
width: frame.width as usize,
height: frame.height as usize,
premultiplied_alpha: false,
data_format: RawImageFormat::RGBA8,
tag: b"azul-video-frame".to_vec().into(),
}) {
if let Some(mut s) = writeback_data.downcast_mut::<VideoWidgetState>() {
s.current_frame = Some(img);
}
}
user_update = invoke_on_frame(&hook, &mut info, &frame);
}
None => return Update::DoNothing,
}
info.trigger_all_virtual_view_rerender();
user_update
}
#[allow(clippy::float_cmp)] extern "C" fn merge_video_state(mut new_data: RefAny, mut old_data: RefAny) -> RefAny {
{
let new_guard = new_data.downcast_mut::<VideoWidgetState>();
let old_guard = old_data.downcast_ref::<VideoWidgetState>();
if let (Some(mut new_g), Some(old_g)) = (new_guard, old_guard) {
new_g.started = old_g.started;
new_g.gl_texture_id = old_g.gl_texture_id;
new_g.frames = old_g.frames.clone();
new_g.decode_callback.clone_from(&old_g.decode_callback);
new_g.current_frame.clone_from(&old_g.current_frame);
new_g.thread_id = old_g.thread_id;
new_g.seek_sender.clone_from(&old_g.seek_sender);
if old_g.config.timestamp != new_g.config.timestamp {
if let Some(snd) = new_g.seek_sender.as_ref() {
drop(snd.send(ThreadSendMsg::Custom(RefAny::new(new_g.config.timestamp))));
}
}
if old_g.config.source != new_g.config.source {
if let Some(snd) = new_g.seek_sender.as_ref() {
drop(snd.send(ThreadSendMsg::Custom(RefAny::new(new_g.config.source.clone()))));
}
}
}
}
new_data
}