mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-03 08:50:47 -06:00
181 lines
6.2 KiB
Rust
181 lines
6.2 KiB
Rust
//! Video frames for a playback clock: decoding runs ahead on a worker thread (inline on wasm), and
|
|
//! [`VideoFeed::frame_at`] hands out the frame due at the clock's time, seeking when the clock
|
|
//! jumps (seek, loop).
|
|
|
|
use std::sync::Arc;
|
|
|
|
use crate::video::{Frame, VideoDecoder};
|
|
use crate::{Bytes, Result};
|
|
|
|
/// Frames decoded ahead of the clock.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
const AHEAD: usize = 6;
|
|
|
|
/// A jump forward larger than this (seconds) seeks instead of decoding through.
|
|
const MAX_CATCH_UP: f64 = 1.5;
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
enum Cmd {
|
|
Seek(f64, u64),
|
|
}
|
|
|
|
pub struct VideoFeed {
|
|
size: (u32, u32),
|
|
current: Option<Arc<Frame>>,
|
|
next: Option<Arc<Frame>>,
|
|
generation: u64,
|
|
ended: bool,
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
tx: std::sync::mpsc::Sender<Cmd>,
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
rx: std::sync::mpsc::Receiver<(u64, Option<Frame>)>,
|
|
#[cfg(target_arch = "wasm32")]
|
|
dec: VideoDecoder,
|
|
}
|
|
|
|
impl VideoFeed {
|
|
pub fn new(bytes: Bytes) -> Result<VideoFeed> {
|
|
let dec = VideoDecoder::open(bytes)?;
|
|
let size = dec.size();
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
{
|
|
let (tx, cmd_rx) = std::sync::mpsc::channel::<Cmd>();
|
|
let (frame_tx, rx) = std::sync::mpsc::sync_channel::<(u64, Option<Frame>)>(AHEAD);
|
|
let worker = move || worker(dec, cmd_rx, frame_tx);
|
|
if let Err(e) = std::thread::Builder::new().name("deckcraft-video-decode".into()).spawn(worker) {
|
|
return Err(crate::MediaError::Corrupt(format!("couldn't start the decoder: {e}")));
|
|
}
|
|
Ok(VideoFeed { size, current: None, next: None, generation: 0, ended: false, tx, rx })
|
|
}
|
|
#[cfg(target_arch = "wasm32")]
|
|
{
|
|
Ok(VideoFeed { size, current: None, next: None, generation: 0, ended: false, dec })
|
|
}
|
|
}
|
|
|
|
pub fn size(&self) -> (u32, u32) {
|
|
self.size
|
|
}
|
|
|
|
/// Restart decoding at `t`.
|
|
pub fn seek(&mut self, t: f64) {
|
|
self.generation += 1;
|
|
self.current = None;
|
|
self.next = None;
|
|
self.ended = false;
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
{
|
|
let _ = self.tx.send(Cmd::Seek(t, self.generation));
|
|
// Unblock the worker if it's waiting to hand over a stale frame.
|
|
while self.rx.try_recv().is_ok() {}
|
|
}
|
|
#[cfg(target_arch = "wasm32")]
|
|
self.dec.seek(t);
|
|
}
|
|
|
|
/// The next decoded frame, if one is ready (`Err(())` at the end of the stream).
|
|
fn pull(&mut self) -> std::result::Result<Option<Arc<Frame>>, ()> {
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
loop {
|
|
match self.rx.try_recv() {
|
|
Ok((g, _)) if g != self.generation => continue,
|
|
Ok((_, Some(f))) => return Ok(Some(Arc::new(f))),
|
|
Ok((_, None)) => return Err(()),
|
|
Err(std::sync::mpsc::TryRecvError::Empty) => return Ok(None),
|
|
Err(std::sync::mpsc::TryRecvError::Disconnected) => return Err(()),
|
|
}
|
|
}
|
|
#[cfg(target_arch = "wasm32")]
|
|
match self.dec.next_frame() {
|
|
Ok(Some(f)) => Ok(Some(Arc::new(f))),
|
|
_ => Err(()),
|
|
}
|
|
}
|
|
|
|
/// The frame on screen at media time `t` (seconds): the last one whose time is ≤ `t`, or the
|
|
/// first one while nothing earlier exists. `None` until the first frame has decoded.
|
|
pub fn frame_at(&mut self, t: f64) -> Option<Arc<Frame>> {
|
|
let behind = self.current.as_ref().is_some_and(|c| t + 0.02 < c.time);
|
|
let far = self.next.as_ref().or(self.current.as_ref()).is_some_and(|c| t > c.time + MAX_CATCH_UP);
|
|
if behind || far {
|
|
self.seek(t);
|
|
}
|
|
loop {
|
|
if self.next.is_none() && !self.ended {
|
|
match self.pull() {
|
|
Ok(Some(f)) => self.next = Some(f),
|
|
Ok(None) => break,
|
|
Err(()) => self.ended = true,
|
|
}
|
|
}
|
|
match &self.next {
|
|
Some(n) if n.time <= t + 1e-3 || self.current.is_none() => {
|
|
self.current = self.next.take();
|
|
}
|
|
_ => break,
|
|
}
|
|
}
|
|
self.current.clone()
|
|
}
|
|
|
|
/// The decoder has delivered its last frame.
|
|
pub fn ended(&self) -> bool {
|
|
self.ended && self.next.is_none()
|
|
}
|
|
|
|
/// Wait (up to `timeout`) for the frame at `t` — for tests and offline rendering.
|
|
pub fn frame_at_blocking(&mut self, t: f64, timeout: std::time::Duration) -> Option<Arc<Frame>> {
|
|
let mut waited = std::time::Duration::ZERO;
|
|
loop {
|
|
let f = self.frame_at(t);
|
|
let done = self.ended || self.next.is_some();
|
|
if done || waited >= timeout || !crate::THREADS {
|
|
return f;
|
|
}
|
|
std::thread::sleep(std::time::Duration::from_millis(2));
|
|
waited += std::time::Duration::from_millis(2);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn worker(mut dec: VideoDecoder, cmds: std::sync::mpsc::Receiver<Cmd>, frames: std::sync::mpsc::SyncSender<(u64, Option<Frame>)>) {
|
|
let mut generation = 0u64;
|
|
let mut at_end = false;
|
|
loop {
|
|
// Commands first; block on them once the stream has ended.
|
|
let cmd = if at_end {
|
|
match cmds.recv() {
|
|
Ok(c) => Some(c),
|
|
Err(_) => return,
|
|
}
|
|
} else {
|
|
match cmds.try_recv() {
|
|
Ok(c) => Some(c),
|
|
Err(std::sync::mpsc::TryRecvError::Empty) => None,
|
|
Err(std::sync::mpsc::TryRecvError::Disconnected) => return,
|
|
}
|
|
};
|
|
if let Some(Cmd::Seek(t, g)) = cmd {
|
|
// Only the latest seek matters.
|
|
let (mut t, mut g) = (t, g);
|
|
while let Ok(Cmd::Seek(t2, g2)) = cmds.try_recv() {
|
|
(t, g) = (t2, g2);
|
|
}
|
|
dec.seek(t);
|
|
generation = g;
|
|
}
|
|
let frame = match dec.next_frame() {
|
|
Ok(f) => f,
|
|
Err(e) => {
|
|
log::debug!("video decode stopped: {e}");
|
|
None
|
|
}
|
|
};
|
|
at_end = frame.is_none();
|
|
if frames.send((generation, frame)).is_err() {
|
|
return;
|
|
}
|
|
}
|
|
}
|