use std::collections::VecDeque;
use std::sync::{Arc, Condvar, Mutex};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use zvidlib::{
CancellationToken, EncodedVideoSample, Error, ErrorKind, ExactFrameReader, FrameIndex, Limits,
PlaybackVideoSource, Result, VideoDecoderConfig, VideoDecoderFactory, VideoFrame,
};
const DELIVERY_DEPTH: usize = 3;
pub fn target_frame(fraction: f32, frame_count: u64) -> u64 {
let maximum = frame_count.saturating_sub(1);
(f64::from(fraction.clamp(0.0, 1.0)) * maximum as f64).round() as u64
}
pub fn frame_is_at_playhead(index: u64, playhead: u64, tolerance: u64) -> bool {
index.abs_diff(playhead) <= tolerance
}
const AUDIO_SCRUB_INTERVAL: Duration = Duration::from_millis(100);
#[derive(Default)]
pub struct AudioScrubCadence {
last: Option<Instant>,
}
impl AudioScrubCadence {
pub fn due(&mut self, now: Instant) -> bool {
if self
.last
.is_some_and(|last| now.duration_since(last) < AUDIO_SCRUB_INTERVAL)
{
return false;
}
self.last = Some(now);
true
}
pub fn rearm(&mut self) {
self.last = None;
}
}
#[derive(Default)]
struct Queue {
target: Option<u64>,
cursor: Option<u64>,
in_flight: Option<CancellationToken>,
delivered: VecDeque<(u64, VideoFrame)>,
failure: Option<Error>,
shutdown: bool,
}
impl Queue {
fn retarget(&mut self, frame: u64) -> bool {
if self.target == Some(frame) {
return false;
}
self.target = Some(frame);
if self.cursor.is_some_and(|cursor| cursor > frame) {
if let Some(in_flight) = self.in_flight.as_ref() {
in_flight.cancel();
}
}
true
}
fn take_exact(&mut self, frame: u64) -> Option<VideoFrame> {
let position = self
.delivered
.iter()
.position(|(index, _)| *index == frame)?;
self.delivered.drain(..position);
self.delivered.front().map(|(_, frame)| frame.clone())
}
}
pub struct FrameService {
queue: Arc<(Mutex<Queue>, Condvar)>,
worker: Option<JoinHandle<()>>,
}
impl FrameService {
pub fn new(
factory: &dyn VideoDecoderFactory,
configuration: VideoDecoderConfig,
samples: Vec<EncodedVideoSample>,
limits: Limits,
) -> Result<Self> {
let reader = ExactFrameReader::new(factory, configuration, samples, limits)?;
let queue = Arc::new((Mutex::new(Queue::default()), Condvar::new()));
let worker_queue = Arc::clone(&queue);
let worker = thread::Builder::new()
.name("zvidlib-frame-service".to_string())
.spawn(move || decode_frames(&worker_queue, reader))
.map_err(|error| {
Error::new(
ErrorKind::InvalidInput,
format!("could not start the frame decode thread: {error}"),
)
})?;
Ok(Self {
queue,
worker: Some(worker),
})
}
pub fn source(&self) -> FrameServiceSource {
FrameServiceSource {
queue: Arc::clone(&self.queue),
}
}
pub fn request(&mut self, frame: u64) -> bool {
let (lock, condvar) = &*self.queue;
let retargeted = lock.lock().expect("frame queue poisoned").retarget(frame);
if retargeted {
condvar.notify_one();
}
retargeted
}
pub fn take_latest(&mut self) -> Option<(u64, VideoFrame)> {
let (lock, _) = &*self.queue;
let mut state = lock.lock().expect("frame queue poisoned");
let newest = state.delivered.pop_back();
state.delivered.clear();
newest
}
}
impl Drop for FrameService {
fn drop(&mut self) {
{
let (lock, condvar) = &*self.queue;
let mut state = lock.lock().expect("frame queue poisoned");
state.shutdown = true;
if let Some(in_flight) = state.in_flight.as_ref() {
in_flight.cancel();
}
condvar.notify_all();
}
if let Some(worker) = self.worker.take() {
let _ = worker.join();
}
}
}
pub struct FrameServiceSource {
queue: Arc<(Mutex<Queue>, Condvar)>,
}
impl PlaybackVideoSource for FrameServiceSource {
fn get_exact(&mut self, frame: FrameIndex, _: &CancellationToken) -> Result<VideoFrame> {
let (lock, condvar) = &*self.queue;
let mut state = lock.lock().expect("frame queue poisoned");
if let Some(decoded) = state.take_exact(frame.0) {
return Ok(decoded);
}
if let Some(failure) = state.failure.take() {
return Err(failure);
}
if state.retarget(frame.0) {
condvar.notify_one();
}
Err(Error::new(
ErrorKind::WouldBlock,
"the requested frame is still decoding",
))
}
fn reset(&mut self) -> Result<()> {
Ok(())
}
}
fn decode_frames(queue: &Arc<(Mutex<Queue>, Condvar)>, mut reader: ExactFrameReader) {
let (lock, condvar) = &**queue;
loop {
let (mut target, mut cancellation) = {
let mut state = lock.lock().expect("frame queue poisoned");
loop {
if state.shutdown {
return;
}
if let Some(target) = state.target {
let cancellation = CancellationToken::new();
state.in_flight = Some(cancellation.clone());
break (target, cancellation);
}
state = condvar.wait(state).expect("frame queue poisoned");
}
};
loop {
lock.lock().expect("frame queue poisoned").cursor = Some(target);
let decoded = reader.get(FrameIndex(target), &cancellation);
let mut state = lock.lock().expect("frame queue poisoned");
if state.shutdown {
return;
}
let mut reached = false;
match decoded {
Ok(frame) => {
reached = true;
while state.delivered.len() >= DELIVERY_DEPTH {
state.delivered.pop_front();
}
state.delivered.push_back((target, frame));
}
Err(error) if error.kind() == ErrorKind::Canceled => {}
Err(error) => {
state.failure = Some(error);
state.target = None;
}
}
let Some(current) = state.target else {
state.cursor = None;
state.in_flight = None;
break;
};
if current != target {
target = current;
} else if reached {
state.target = None;
state.cursor = None;
state.in_flight = None;
break;
}
if cancellation.is_canceled() {
cancellation = CancellationToken::new();
state.in_flight = Some(cancellation.clone());
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, Instant};
use zvidlib::{
CodecProfile, CodecSupport, ColorRange, DecodedVideoFrame, PixelFormat, Plane,
VideoDecoder, VideoDimensions,
};
fn samples(count: u64, keyframe_every: u64) -> Vec<EncodedVideoSample> {
(0..count)
.map(|index| EncodedVideoSample {
presentation_index: FrameIndex(index),
random_access: index % keyframe_every == 0,
data: vec![index as u8],
})
.collect()
}
#[test]
fn a_timeline_fraction_selects_the_frame_it_points_at() {
assert_eq!(target_frame(0.0, 101), 0);
assert_eq!(target_frame(0.5, 101), 50);
assert_eq!(target_frame(1.0, 101), 100);
assert_eq!(target_frame(-1.0, 101), 0);
assert_eq!(target_frame(2.0, 101), 100);
assert_eq!(target_frame(0.5, 0), 0);
}
type Gate = Arc<(Mutex<u32>, Condvar)>;
fn gate(permits: u32) -> Gate {
Arc::new((Mutex::new(permits), Condvar::new()))
}
fn grant(gate: &Gate, permits: u32) {
*gate.0.lock().expect("gate poisoned") += permits;
gate.1.notify_all();
}
struct IndexedDecoder {
gate: Option<Gate>,
}
impl VideoDecoder for IndexedDecoder {
fn submit(
&mut self,
sample: &EncodedVideoSample,
_: &CancellationToken,
) -> Result<Vec<DecodedVideoFrame>> {
if let Some(gate) = self.gate.as_ref() {
let (lock, condvar) = &**gate;
let mut permits = lock.lock().expect("gate poisoned");
while *permits == 0 {
permits = condvar.wait(permits).expect("gate poisoned");
}
*permits -= 1;
}
Ok(vec![DecodedVideoFrame {
presentation_index: sample.presentation_index,
frame: VideoFrame::new(
VideoDimensions::new(1, 1, &Limits::default())?,
PixelFormat::Rgba8,
ColorRange::Limited,
vec![Plane {
data: vec![sample.presentation_index.0 as u8, 0, 0, 255],
stride: 4,
}],
&Limits::default(),
)?,
}])
}
fn drain(&mut self, _: &CancellationToken) -> Result<Vec<DecodedVideoFrame>> {
Ok(Vec::new())
}
fn reset(&mut self) -> Result<()> {
Ok(())
}
}
struct IndexedFactory {
gate: Option<Gate>,
}
impl VideoDecoderFactory for IndexedFactory {
fn capability(&self, _: &VideoDecoderConfig) -> CodecSupport {
CodecSupport::Supported {
implementation: zvidlib::CodecImplementation::Software,
}
}
fn create(&self, _: &VideoDecoderConfig, _: &Limits) -> Result<Box<dyn VideoDecoder>> {
Ok(Box::new(IndexedDecoder {
gate: self.gate.clone(),
}))
}
}
fn configuration() -> VideoDecoderConfig {
VideoDecoderConfig {
codec: zvidlib::Codec::Hevc,
profile: CodecProfile::HevcMain,
coded_dimensions: VideoDimensions::new(1, 1, &Limits::default()).unwrap(),
output_format: PixelFormat::Rgba8,
color_range: ColorRange::Limited,
hardware: zvidlib::HardwarePreference::Avoid,
configuration: Vec::new(),
}
}
fn open(gate: Option<Gate>, samples: Vec<EncodedVideoSample>) -> FrameService {
FrameService::new(
&IndexedFactory { gate },
configuration(),
samples,
Limits::default(),
)
.unwrap()
}
fn wait_for_a_moment<T>(mut poll: impl FnMut() -> Option<T>) -> Option<T> {
let deadline = Instant::now() + Duration::from_millis(100);
while Instant::now() < deadline {
if let Some(value) = poll() {
return Some(value);
}
thread::sleep(Duration::from_millis(2));
}
None
}
fn wait_for<T>(mut poll: impl FnMut() -> Option<T>) -> Option<T> {
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
if let Some(value) = poll() {
return Some(value);
}
thread::sleep(Duration::from_millis(2));
}
None
}
#[test]
fn requesting_a_frame_does_not_wait_for_it_and_the_newest_target_wins() {
let gate = gate(0);
let mut frames = open(Some(Arc::clone(&gate)), samples(12, 4));
assert!(frames.request(0));
assert!(frames.take_latest().is_none());
assert!(!frames.request(0));
assert!(frames.request(8));
grant(&gate, u32::MAX / 2);
let drawn = wait_for(|| {
frames
.take_latest()
.map(|(_, frame)| frame.planes[0].data[0])
.filter(|index| *index == 8)
});
assert_eq!(
drawn,
Some(8),
"the newest requested frame is the one drawn"
);
}
#[test]
fn playback_is_told_a_frame_is_still_decoding_rather_than_being_made_to_wait() {
let gate = gate(0);
let frames = open(Some(Arc::clone(&gate)), samples(12, 4));
let mut source = frames.source();
let token = CancellationToken::new();
let start = Instant::now();
let pending = source.get_exact(FrameIndex(4), &token);
assert!(start.elapsed() < Duration::from_secs(1), "get_exact waited");
assert_eq!(
pending.map(|_| ()).unwrap_err().kind(),
ErrorKind::WouldBlock
);
grant(&gate, u32::MAX / 2);
let frame = wait_for(|| source.get_exact(FrameIndex(4), &token).ok());
assert_eq!(
frame.map(|frame| frame.planes[0].data[0]),
Some(4),
"the frame is delivered once it has decoded"
);
}
#[test]
fn an_exact_request_draws_its_own_frame_and_nothing_it_passed() {
let mut single_gop = samples(12, 1);
for sample in single_gop.iter_mut().skip(1) {
sample.random_access = false;
}
let gate = gate(0);
let frames = open(Some(Arc::clone(&gate)), single_gop);
let mut source = frames.source();
let cancellation = CancellationToken::new();
assert_eq!(
source
.get_exact(FrameIndex(9), &cancellation)
.unwrap_err()
.kind(),
ErrorKind::WouldBlock
);
grant(&gate, 8);
assert!(
wait_for_a_moment(|| source.get_exact(FrameIndex(9), &cancellation).ok()).is_none(),
"a frame an exact request passed is not drawn"
);
grant(&gate, u32::MAX / 2);
let arrived = wait_for(|| source.get_exact(FrameIndex(9), &cancellation).ok());
assert_eq!(
arrived.map(|frame| frame.planes[0].data[0]),
Some(9),
"the frame that was asked for is the one drawn"
);
}
#[test]
fn a_drag_asks_for_the_frame_under_the_pointer_and_publishes_nothing_it_passed() {
let mut single_gop = samples(12, 1);
for sample in single_gop.iter_mut().skip(1) {
sample.random_access = false;
}
let gate = gate(0);
let mut frames = open(Some(Arc::clone(&gate)), single_gop);
frames.request(9);
grant(&gate, 8);
assert!(
wait_for_a_moment(|| frames.take_latest()).is_none(),
"a frame the drag's walk passed is published to nobody"
);
grant(&gate, u32::MAX / 2);
let arrived = wait_for(|| {
frames
.take_latest()
.map(|(index, frame)| (index, frame.planes[0].data[0]))
});
assert_eq!(
arrived,
Some((9, 9)),
"the frame under the pointer is the one that is published"
);
}
#[test]
fn only_a_picture_at_the_playhead_is_drawn() {
assert!(!frame_is_at_playhead(0, 600, 7));
assert!(!frame_is_at_playhead(15, 600, 7));
assert!(!frame_is_at_playhead(30, 600, 7));
assert!(frame_is_at_playhead(600, 600, 7));
assert!(frame_is_at_playhead(593, 600, 7));
assert!(frame_is_at_playhead(607, 600, 7));
assert!(!frame_is_at_playhead(592, 600, 7));
assert!(!frame_is_at_playhead(608, 600, 7));
}
#[test]
fn a_drag_moves_the_audio_on_a_cadence_rather_than_on_every_pointer_move() {
let start = Instant::now();
let mut cadence = AudioScrubCadence::default();
assert!(cadence.due(start));
assert!(!cadence.due(start + Duration::from_millis(16)));
assert!(!cadence.due(start + Duration::from_millis(99)));
assert!(cadence.due(start + AUDIO_SCRUB_INTERVAL));
assert!(!cadence.due(start + AUDIO_SCRUB_INTERVAL + Duration::from_millis(16)));
cadence.rearm();
assert!(cadence.due(start + AUDIO_SCRUB_INTERVAL + Duration::from_millis(17)));
}
}