mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-27 03:56:50 -06:00
235 lines
8.8 KiB
Rust
235 lines
8.8 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));
|
|
// pull() discards stale generations and unblocks the worker. Draining here could
|
|
// also discard the new generation's frames or EOF if the worker handles seek fast.
|
|
}
|
|
#[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;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(all(test, not(target_arch = "wasm32")))]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn seek_keeps_new_generation_frames_and_end_of_stream() {
|
|
let (tx, _commands) = std::sync::mpsc::channel();
|
|
let (frames, rx) = std::sync::mpsc::sync_channel(AHEAD);
|
|
let frame = |time| Frame { time, width: 1, height: 1, rgba: vec![0, 0, 0, 255] };
|
|
frames.send((0, Some(frame(0.3)))).expect("stale frame");
|
|
frames.send((0, None)).expect("stale end of stream");
|
|
// Model a fast worker's seek response already waiting when seek drains the queue.
|
|
frames.send((1, Some(frame(0.0)))).expect("new frame");
|
|
frames.send((1, None)).expect("new end of stream");
|
|
let mut feed = VideoFeed { size: (1, 1), current: None, next: None, generation: 0, ended: false, tx, rx };
|
|
|
|
feed.seek(0.05);
|
|
let shown = feed.frame_at(0.05).expect("frame from the new generation");
|
|
assert_eq!(shown.time, 0.0);
|
|
assert!(feed.ended());
|
|
|
|
frames.send((2, Some(frame(0.3)))).expect("superseded seek frame");
|
|
frames.send((2, None)).expect("superseded seek end of stream");
|
|
frames.send((3, Some(frame(0.0)))).expect("latest seek frame");
|
|
frames.send((3, None)).expect("latest seek end of stream");
|
|
feed.seek(0.35);
|
|
feed.seek(0.05);
|
|
assert_eq!(feed.frame_at(0.05).expect("latest seek wins").time, 0.0);
|
|
assert!(feed.ended());
|
|
}
|
|
|
|
#[test]
|
|
fn seek_unblocks_worker_when_stale_queue_is_full() {
|
|
let (tx, commands) = std::sync::mpsc::channel();
|
|
let (frames, rx) = std::sync::mpsc::sync_channel(AHEAD);
|
|
let frame = || Frame { time: 0.0, width: 1, height: 1, rgba: vec![0, 0, 0, 255] };
|
|
for _ in 0..AHEAD {
|
|
frames.send((0, Some(frame()))).expect("fill stale queue");
|
|
}
|
|
let worker = std::thread::spawn(move || {
|
|
let Cmd::Seek(_, generation) = commands.recv().expect("seek command");
|
|
frames.send((generation, Some(frame()))).expect("seek frame");
|
|
frames.send((generation, None)).expect("seek end of stream");
|
|
});
|
|
let mut feed = VideoFeed { size: (1, 1), current: None, next: None, generation: 0, ended: false, tx, rx };
|
|
|
|
feed.seek(0.05);
|
|
let shown = feed.frame_at_blocking(0.05, std::time::Duration::from_secs(5)).expect("worker unblocked");
|
|
assert_eq!(shown.time, 0.0);
|
|
assert!(feed.ended());
|
|
worker.join().expect("worker completed");
|
|
}
|
|
}
|