/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2026 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
use async_trait::async_trait;
use parking_lot::Mutex as SyncMutex;
use rand::{rngs::OsRng, Rng};
use std::sync::Arc;
use tracing::instrument;
use crate::{
gfx::{
anim::Frame, gfxtag, DrawCall, DrawInstruction, DrawMesh, EpochCache, GraphicPipeline,
ManagedSeqAnimPtr, ManagedTexturePtr, Rectangle, RenderApi, Renderer,
},
mesh::{MeshBuilder, MeshInfo, COLOR_WHITE},
prop::{BatchGuardPtr, PropertyAtomicGuard, PropertyRect, PropertyStr, PropertyUint32, Role},
scene::{Pimpl, SceneNodeWeak},
ExecutorPtr,
};
use super::{DrawUpdate, OnModify, RedrawTrigger, UIObject};
mod decode;
#[allow(dead_code)]
mod ivf;
use decode::spawn_decoder_thread;
pub type VideoPtr = Arc;
#[derive(Clone)]
pub struct YuvTextures {
y: ManagedTexturePtr,
u: ManagedTexturePtr,
v: ManagedTexturePtr,
}
#[derive(Clone)]
pub struct Av1VideoData {
textures: Vec>,
anim: ManagedSeqAnimPtr,
textures_pub: async_broadcast::Sender<(usize, YuvTextures)>,
textures_sub: async_broadcast::Receiver<(usize, YuvTextures)>,
}
impl Av1VideoData {
fn new(len: usize, renderer: &Renderer) -> Self {
let (textures_pub, textures_sub) = async_broadcast::broadcast(len);
let anim = renderer.new_anim(len, false, gfxtag!("video"));
Self { textures: vec![None; len], anim, textures_pub, textures_sub }
}
}
pub struct Video {
node: SceneNodeWeak,
renderer: Renderer,
redraw: RedrawTrigger,
tasks: SyncMutex>>,
load_tasks: SyncMutex>>,
ex: ExecutorPtr,
dc_key: u64,
vid_data: Arc>>,
_load_handle: SyncMutex>>,
_decoder_handle: SyncMutex >>,
rect: PropertyRect,
uv: PropertyRect,
z_index: PropertyUint32,
priority: PropertyUint32,
path: PropertyStr,
/// Cached draw instructions. Empty means stale. Entries from a dead
/// UI epoch are evicted automatically.
draw_cache: EpochCache>,
parent_rect: SyncMutex>,
}
impl Video {
pub async fn new(
node: SceneNodeWeak,
renderer: Renderer,
redraw: RedrawTrigger,
ex: ExecutorPtr,
) -> Pimpl {
let node_ref = &node.upgrade().unwrap();
let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
let uv = PropertyRect::wrap(node_ref, Role::Internal, "uv").unwrap();
let z_index = PropertyUint32::wrap(node_ref, Role::Internal, "z_index", 0).unwrap();
let priority = PropertyUint32::wrap(node_ref, Role::Internal, "priority", 0).unwrap();
let path = PropertyStr::wrap(node_ref, Role::Internal, "path", 0).unwrap();
let draw_cache = EpochCache::new(&renderer);
let self_ = Arc::new(Self {
node,
renderer,
redraw,
tasks: SyncMutex::new(vec![]),
load_tasks: SyncMutex::new(vec![]),
ex,
dc_key: OsRng.gen(),
vid_data: Arc::new(SyncMutex::new(None)),
_load_handle: SyncMutex::new(None),
_decoder_handle: SyncMutex::new(None),
rect,
uv,
z_index,
priority,
path,
draw_cache,
parent_rect: SyncMutex::new(None),
});
Pimpl::Video(self_)
}
async fn reload(self_: Arc, _batch: BatchGuardPtr) {
self_.load_video();
self_.draw_cache.clear();
self_.redraw.trigger();
}
fn load_video(&self) {
let path = self.path.get();
// Decoder thread:
// loads path, decodes AV1 -> RGB, creates textures directly
let decoder_handle =
spawn_decoder_thread(path, self.vid_data.clone(), self.renderer.clone());
*self._decoder_handle.lock() = Some(decoder_handle);
}
fn regen_mesh(&self) -> MeshInfo {
let rect = self.rect.get();
let uv = self.uv.get();
let mesh_rect = Rectangle::from([0., 0., rect.w, rect.h]);
let mut mesh = MeshBuilder::new(gfxtag!("img"));
mesh.draw_box(&mesh_rect, COLOR_WHITE, &uv);
mesh.alloc(&self.renderer)
}
/// Wire decoder output into the renderer anim and build the video
/// instructions. Called only when the draw cache is stale or the
/// rect changed; the anim then advances renderer-side.
fn make_instrs(&self, rect: &Rectangle) -> Option> {
let mesh = self.regen_mesh();
let (vid_data, tsubs) = {
let vid_data_lock = self.vid_data.lock();
let Some(vid_data) = vid_data_lock.as_ref() else {
// Video not loaded yet, skip draw
return None;
};
let tsubs = vec![vid_data.textures_sub.clone(); vid_data.textures.len()];
// Clone the data before the lock is released
(vid_data.clone(), tsubs)
};
assert_eq!(vid_data.textures.len(), tsubs.len());
let mut load_tasks = self.load_tasks.lock();
load_tasks.clear();
let mut loaded_n_frames = 0;
let total_frames = vid_data.textures.len();
for (tex_idx, (mut tex, mut tsub)) in
vid_data.textures.into_iter().zip(tsubs.into_iter()).enumerate()
{
let vertex_buffer = mesh.vertex_buffer.clone();
let index_buffer = mesh.index_buffer.clone();
let Some(tex) = tex.take() else {
let anim = vid_data.anim.clone();
let task = self.ex.spawn(async move {
while let Ok((frame_idx, tex)) = tsub.recv().await {
if frame_idx != tex_idx {
continue
}
let mesh = DrawMesh {
vertex_buffer,
index_buffer,
textures: Some(vec![tex.y, tex.u, tex.v]),
num_elements: mesh.num_elements,
};
let dc = DrawCall {
instrs: vec![DrawInstruction::Draw(mesh)],
dcs: vec![],
z_index: 0,
debug_str: "video",
};
anim.update(frame_idx, Frame::new(40, dc));
break
}
});
load_tasks.push(task);
continue
};
let mesh = DrawMesh {
vertex_buffer,
index_buffer,
textures: Some(vec![tex.y, tex.u, tex.v]),
num_elements: mesh.num_elements,
};
let dc = DrawCall {
instrs: vec![DrawInstruction::Draw(mesh)],
dcs: vec![],
z_index: 0,
debug_str: "video",
};
vid_data.anim.update(tex_idx, Frame::new(40, dc));
loaded_n_frames += 1;
}
debug!(target: "ui::video", "Loaded {loaded_n_frames} / {total_frames} frames");
Some(vec![
DrawInstruction::SetPipeline(GraphicPipeline::YUV),
DrawInstruction::Move(rect.pos()),
DrawInstruction::Animation(vid_data.anim.clone()),
])
}
}
#[async_trait]
impl UIObject for Video {
fn priority(&self) -> u32 {
self.priority.get()
}
fn init(&self) {
// Drop textures from a dead UI epoch (if any) before reloading
*self.vid_data.lock() = None;
self.load_video();
}
async fn start(self: Arc, ex: ExecutorPtr) {
let me = Arc::downgrade(&self);
let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
on_modify.when_change_external(self.rect.prop(), |self_, _| async move {
self_.draw_cache.clear();
self_.redraw.trigger();
});
on_modify.when_change_external(self.uv.prop(), |self_, _| async move {
self_.draw_cache.clear();
self_.redraw.trigger();
});
on_modify.when_change_external(self.z_index.prop(), |self_, _| async move {
self_.draw_cache.clear();
self_.redraw.trigger();
});
on_modify.when_change(self.path.prop(), Self::reload);
*self.tasks.lock() = on_modify.tasks;
}
fn stop(&self) {
self.tasks.lock().clear();
*self.parent_rect.lock() = None;
*self.vid_data.lock() = None;
self.draw_cache.clear();
// Threads terminate naturally when channels close
}
#[instrument(target = "ui::video")]
async fn draw(
&self,
parent_rect: Rectangle,
atom: &mut PropertyAtomicGuard,
) -> Option {
*self.parent_rect.lock() = Some(parent_rect);
// Rect property is its own memo: compare before/after eval.
let prev_rect = self.rect.get();
self.rect.eval(atom, &parent_rect).ok()?;
let rect = self.rect.get();
let rect_changed = rect != prev_rect;
self.uv.eval(atom, &rect).ok()?;
// Compute under the cache lock so a concurrent invalidation lands
// before or after, never between. A video that has not loaded
// yet stays uncached so the next pass retries.
if rect_changed {
self.draw_cache.clear();
}
let instrs = match self.draw_cache.get() {
Some(instrs) => instrs,
None => {
let Some(instrs) = self.make_instrs(&rect) else { return None };
self.draw_cache.set(instrs.clone());
instrs
}
};
Some(DrawUpdate {
key: self.dc_key,
draw_calls: vec![(
self.dc_key,
DrawCall::new(instrs, vec![], self.z_index.get(), "vid"),
)],
})
}
}
impl Drop for Video {
fn drop(&mut self) {
self.renderer.replace_draw_calls(vec![(self.dc_key, Default::default())]);
}
}
impl std::fmt::Debug for Video {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self.node.upgrade().unwrap())
}
}