Files
deckcraft/crates/media/src/feed.rs
T

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;
}
}
}