use std::sync::{Arc, Mutex};
use crate::api::{Limits, Result};
use crate::codec::SeekPreviewSource;
use crate::media::{PixelFormat, Plane, VideoDimensions, VideoFrame};
use crate::timeline::FrameIndex;
#[cfg(not(target_arch = "wasm32"))]
use std::thread::{self, JoinHandle};
#[cfg(not(target_arch = "wasm32"))]
use std::time::Duration;
use crate::api::{Error, ErrorKind};
#[cfg(not(target_arch = "wasm32"))]
use crate::codec::{
CancellationToken, EncodedVideoSample, ExactFrameReader, VideoDecoderConfig,
VideoDecoderFactory,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PreviewOptions {
pub frames_per_second: u64,
pub scale: u32,
pub previews_per_second: u64,
pub budget_bytes: u64,
}
impl PreviewOptions {
pub fn for_frame_rate(frames_per_second: u64) -> Self {
Self {
frames_per_second,
scale: 4,
previews_per_second: 2,
budget_bytes: 64 << 20,
}
}
pub fn stride(&self, dimensions: &VideoDimensions, frame_count: u64) -> u64 {
let bytes = self.preview_bytes(dimensions).max(1);
let affordable = (self.budget_bytes / bytes).max(1);
let budgeted = frame_count.div_ceil(affordable);
let wanted = self
.frames_per_second
.div_ceil(self.previews_per_second.max(1));
budgeted.max(wanted).max(1)
}
pub fn preview_bytes(&self, dimensions: &VideoDimensions) -> u64 {
let scale = self.scale.max(1);
let width = u64::from(dimensions.width.div_ceil(scale).max(1));
let height = u64::from(dimensions.height.div_ceil(scale).max(1));
width * height * 4
}
}
struct Store {
stride: u64,
slots: Vec<Option<VideoFrame>>,
}
impl Store {
fn nearest(&self, frame: u64) -> Option<VideoFrame> {
self.nearest_at(frame).map(|(_, preview)| preview)
}
fn nearest_at(&self, frame: u64) -> Option<(FrameIndex, VideoFrame)> {
if self.slots.is_empty() {
return None;
}
let slot = ((frame / self.stride) as usize).min(self.slots.len() - 1);
for distance in 0..self.slots.len() {
if distance <= slot {
if let Some(preview) = self.slots[slot - distance].as_ref() {
return Some((self.frame_of(slot - distance), preview.clone()));
}
}
if distance > 0 {
if let Some(preview) = self.slots.get(slot + distance).and_then(Option::as_ref) {
return Some((self.frame_of(slot + distance), preview.clone()));
}
}
}
None
}
fn frame_of(&self, slot: usize) -> FrameIndex {
FrameIndex(slot as u64 * self.stride)
}
}
#[derive(Clone)]
pub struct PreviewStore {
store: Arc<Mutex<Store>>,
}
impl PreviewStore {
pub fn nearest(&self, frame: u64) -> Option<VideoFrame> {
self.store.lock().ok()?.nearest(frame)
}
pub fn nearest_at(&self, frame: FrameIndex) -> Option<(FrameIndex, VideoFrame)> {
self.store.lock().ok()?.nearest_at(frame.0)
}
pub fn coverage(&self) -> (usize, usize) {
match self.store.lock() {
Ok(store) => (
store.slots.iter().filter(|slot| slot.is_some()).count(),
store.slots.len(),
),
Err(_) => (0, 0),
}
}
pub fn stride(&self) -> u64 {
self.store.lock().map(|store| store.stride).unwrap_or(1)
}
}
impl SeekPreviewSource for PreviewStore {
fn nearest_at(&self, frame: FrameIndex) -> Option<(FrameIndex, VideoFrame)> {
PreviewStore::nearest_at(self, frame)
}
}
pub struct PreviewPass {
store: PreviewStore,
options: PreviewOptions,
stride: u64,
slots: usize,
next_slot: usize,
}
impl PreviewPass {
pub fn new(options: PreviewOptions, dimensions: &VideoDimensions, frame_count: u64) -> Self {
let frame_count = frame_count.max(1);
let stride = options.stride(dimensions, frame_count);
let slots = frame_count.div_ceil(stride) as usize;
Self {
store: PreviewStore {
store: Arc::new(Mutex::new(Store {
stride,
slots: vec![None; slots],
})),
},
options,
stride,
slots,
next_slot: 0,
}
}
pub fn store(&self) -> PreviewStore {
self.store.clone()
}
pub fn stride(&self) -> u64 {
self.stride
}
pub fn next_frame(&self) -> Option<FrameIndex> {
(self.next_slot < self.slots).then(|| FrameIndex(self.next_slot as u64 * self.stride))
}
pub fn accept(&mut self, picture: &VideoFrame, limits: &Limits) {
let slot = self.next_slot;
self.skip();
if slot >= self.slots {
return;
}
let Ok(preview) = shrink(picture, self.options.scale, limits) else {
return;
};
if let Ok(mut store) = self.store.store.lock() {
store.slots[slot] = Some(preview);
}
}
pub fn skip(&mut self) {
self.next_slot = self.next_slot.saturating_add(1);
}
}
#[cfg(not(target_arch = "wasm32"))]
pub struct PreviewIndex {
store: PreviewStore,
cancellation: CancellationToken,
worker: Option<JoinHandle<()>>,
}
#[cfg(not(target_arch = "wasm32"))]
impl PreviewIndex {
pub fn new(
factory: &dyn VideoDecoderFactory,
configuration: VideoDecoderConfig,
samples: Vec<EncodedVideoSample>,
limits: Limits,
options: PreviewOptions,
) -> Result<Self> {
let frame_count = samples.len() as u64;
Self::with_frame_count(
factory,
configuration,
samples,
frame_count,
limits,
options,
)
}
pub fn with_frame_count(
factory: &dyn VideoDecoderFactory,
configuration: VideoDecoderConfig,
samples: Vec<EncodedVideoSample>,
frame_count: u64,
limits: Limits,
options: PreviewOptions,
) -> Result<Self> {
let pass = PreviewPass::new(options, &configuration.coded_dimensions, frame_count);
let store = pass.store();
let reader = ExactFrameReader::new(factory, configuration, samples, limits)?;
let cancellation = CancellationToken::new();
let worker_cancellation = cancellation.clone();
let worker = thread::Builder::new()
.name("zvidlib-preview-index".to_string())
.spawn(move || {
build(reader, pass, &worker_cancellation);
})
.map_err(|error| {
Error::new(
ErrorKind::InvalidInput,
format!("could not start the preview index thread: {error}"),
)
})?;
Ok(Self {
store,
cancellation,
worker: Some(worker),
})
}
pub fn coverage(&self) -> (usize, usize) {
self.store.coverage()
}
pub fn wait_for_coverage(&self) {
while let Some(worker) = self.worker.as_ref() {
if worker.is_finished() {
return;
}
thread::sleep(Duration::from_millis(1));
}
}
pub fn nearest(&self, frame: u64) -> Option<VideoFrame> {
self.store.nearest(frame)
}
pub fn store(&self) -> PreviewStore {
self.store.clone()
}
}
#[cfg(not(target_arch = "wasm32"))]
impl SeekPreviewSource for PreviewIndex {
fn nearest_at(&self, frame: FrameIndex) -> Option<(FrameIndex, VideoFrame)> {
self.store.nearest_at(frame)
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Drop for PreviewIndex {
fn drop(&mut self) {
self.cancellation.cancel();
if let Some(worker) = self.worker.take() {
let _ = worker.join();
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn build(mut reader: ExactFrameReader, mut pass: PreviewPass, cancellation: &CancellationToken) {
let limits = Limits::default();
while let Some(frame) = pass.next_frame() {
if cancellation.is_canceled() {
return;
}
let Ok(picture) = reader.get_step(frame, cancellation) else {
pass.skip();
continue;
};
pass.accept(&picture, &limits);
}
}
pub fn shrink(frame: &VideoFrame, scale: u32, limits: &Limits) -> Result<VideoFrame> {
if frame.pixel_format != PixelFormat::Rgba8 || frame.planes.len() != 1 {
return Err(Error::new(
ErrorKind::InvalidInput,
"a preview can only be made from a single-plane RGBA frame",
));
}
let source = &frame.planes[0];
let source_width = frame.dimensions.width as usize;
let source_height = frame.dimensions.height as usize;
let scale = scale.max(1) as usize;
let width = source_width.div_ceil(scale).max(1);
let height = source_height.div_ceil(scale).max(1);
let stride = width * 4;
let mut data = vec![0_u8; stride * height];
for y in 0..height {
for x in 0..width {
let mut totals = [0_u32; 4];
let mut count = 0_u32;
for block_y in 0..scale {
let source_y = y * scale + block_y;
if source_y >= source_height {
break;
}
for block_x in 0..scale {
let source_x = x * scale + block_x;
if source_x >= source_width {
break;
}
let offset = source_y * source.stride + source_x * 4;
for (total, channel) in totals.iter_mut().zip(&source.data[offset..offset + 4])
{
*total += u32::from(*channel);
}
count += 1;
}
}
let offset = y * stride + x * 4;
for (channel, total) in data[offset..offset + 4].iter_mut().zip(totals) {
*channel = (total / count.max(1)) as u8;
}
}
}
VideoFrame::new(
VideoDimensions::new(width as u32, height as u32, limits)?,
PixelFormat::Rgba8,
frame.color_range,
vec![Plane { data, stride }],
limits,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::media::ColorRange;
const SCALE: u32 = 4;
fn options() -> PreviewOptions {
PreviewOptions::for_frame_rate(24)
}
fn rgba(width: u32, height: u32, fill: impl Fn(u32, u32) -> [u8; 4]) -> VideoFrame {
let stride = width as usize * 4;
let mut data = vec![0_u8; stride * height as usize];
for y in 0..height {
for x in 0..width {
let offset = y as usize * stride + x as usize * 4;
data[offset..offset + 4].copy_from_slice(&fill(x, y));
}
}
VideoFrame::new(
VideoDimensions::new(width, height, &Limits::default()).unwrap(),
PixelFormat::Rgba8,
ColorRange::Limited,
vec![Plane { data, stride }],
&Limits::default(),
)
.unwrap()
}
#[test]
fn a_preview_averages_the_block_it_replaces() {
let frame = rgba(SCALE * 2, SCALE, |x, _| {
if x % 2 == 0 {
[0, 0, 0, 0]
} else {
[255, 255, 255, 255]
}
});
let preview = shrink(&frame, SCALE, &Limits::default()).unwrap();
assert_eq!(preview.dimensions.width, 2);
assert_eq!(preview.dimensions.height, 1);
assert_eq!(&preview.planes[0].data[..4], &[127, 127, 127, 127]);
assert_eq!(&preview.planes[0].data[4..8], &[127, 127, 127, 127]);
}
#[test]
fn a_preview_covers_a_source_the_scale_does_not_divide() {
let frame = rgba(SCALE + 1, SCALE + 1, |_, _| [10, 20, 30, 40]);
let preview = shrink(&frame, SCALE, &Limits::default()).unwrap();
assert_eq!(preview.dimensions.width, 2);
assert_eq!(preview.dimensions.height, 2);
for pixel in preview.planes[0].data.chunks_exact(4) {
assert_eq!(pixel, [10, 20, 30, 40]);
}
}
#[test]
fn the_scale_is_the_callers() {
let frame = rgba(16, 16, |_, _| [7, 7, 7, 255]);
for scale in [1_u32, 2, 4, 8, 16] {
let preview = shrink(&frame, scale, &Limits::default()).unwrap();
assert_eq!(preview.dimensions.width, 16 / scale);
assert_eq!(preview.dimensions.height, 16 / scale);
}
assert_eq!(
shrink(&frame, 0, &Limits::default()).unwrap().dimensions,
frame.dimensions
);
}
#[test]
fn the_stride_keeps_the_index_inside_its_budget() {
let dimensions = VideoDimensions::new(1920, 1080, &Limits::default()).unwrap();
let options = options();
for frame_count in [1_u64, 768, 100_000, 10_000_000] {
let stride = options.stride(&dimensions, frame_count);
let slots = frame_count.div_ceil(stride);
assert!(
slots * options.preview_bytes(&dimensions) <= options.budget_bytes,
"{frame_count} frames at stride {stride} exceeds the budget"
);
}
assert_eq!(options.stride(&dimensions, 768), 12);
assert!(options.stride(&dimensions, 10_000_000) > 12);
}
#[test]
fn the_nearest_preview_falls_back_to_a_neighbor() {
let picture = |value: u8| rgba(4, 4, move |_, _| [value, value, value, 255]);
let store = Store {
stride: 10,
slots: vec![Some(picture(1)), None, Some(picture(3)), None, None],
};
let value = |frame| store.nearest(frame).map(|f| f.planes[0].data[0]);
assert_eq!(value(0), Some(1));
assert_eq!(
value(15),
Some(1),
"a gap falls back to the picture behind it"
);
assert_eq!(value(25), Some(3));
assert_eq!(
value(999),
Some(3),
"past the end draws the last one decoded"
);
assert_eq!(
Store {
stride: 10,
slots: vec![None]
}
.nearest(0),
None
);
}
#[test]
fn a_preview_refuses_a_format_it_cannot_read() {
let mut frame = rgba(4, 4, |_, _| [0, 0, 0, 0]);
frame.pixel_format = PixelFormat::Yuv420p8;
assert!(shrink(&frame, SCALE, &Limits::default()).is_err());
}
}