use crate::api::{ErrorKind, Limits, Result};
use crate::codec::CancellationToken;
use crate::media::{ColorRange, PixelFormat, Plane, VideoDimensions, VideoFrame};
use crate::previews::{PreviewOptions, PreviewPass, PreviewStore};
use crate::timeline::FrameIndex;
use crate::web_decoder::WebVideoDecodeSession;
pub struct WebPreviewIndex {
session: WebVideoDecodeSession,
pass: PreviewPass,
store: PreviewStore,
limits: Limits,
}
impl WebPreviewIndex {
pub async fn open(
bytes: &[u8],
track_index: u32,
limits: &Limits,
options: PreviewOptions,
) -> Result<Self> {
let session = WebVideoDecodeSession::open(bytes, track_index, limits).await?;
let pass = PreviewPass::new(options, &session.dimensions(), session.frame_count());
let store = pass.store();
Ok(Self {
session,
pass,
store,
limits: *limits,
})
}
pub fn store(&self) -> PreviewStore {
self.store.clone()
}
pub fn coverage(&self) -> (usize, usize) {
self.store.coverage()
}
pub fn stride(&self) -> u64 {
self.pass.stride()
}
pub fn next_frame(&self) -> Option<FrameIndex> {
self.pass.next_frame()
}
pub async fn step(&mut self, cancellation: &CancellationToken) -> Result<bool> {
let Some(frame) = self.pass.next_frame() else {
return Ok(false);
};
match self.session.get(frame, cancellation).await {
Ok((dimensions, rgba)) => match picture(dimensions, rgba, &self.limits) {
Ok(picture) => self.pass.accept(&picture, &self.limits),
Err(_) => self.pass.skip(),
},
Err(error) if error.kind() == ErrorKind::Canceled => return Err(error),
Err(_) => self.pass.skip(),
}
Ok(self.pass.next_frame().is_some())
}
}
fn picture(dimensions: VideoDimensions, rgba: Vec<u8>, limits: &Limits) -> Result<VideoFrame> {
let stride = dimensions.width as usize * 4;
VideoFrame::new(
dimensions,
PixelFormat::Rgba8,
ColorRange::Full,
vec![Plane { data: rgba, stride }],
limits,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codec::{
CodecProfile, EncodedVideoSample, ExactFrameReader, HardwarePreference,
SEEK_LATENCY_BUDGET, Seek, VideoDecoderConfig, uncompressed_video_decoder_factory,
};
use crate::media::Codec;
use std::sync::Arc;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
const FRAMES: u64 = 768;
fn dimensions(width: u32, height: u32) -> VideoDimensions {
VideoDimensions::new(width, height, &Limits::default()).unwrap()
}
fn rgba(width: u32, height: u32, value: u8) -> VideoFrame {
let stride = width as usize * 4;
picture(
dimensions(width, height),
vec![value; stride * height as usize],
&Limits::default(),
)
.unwrap()
}
fn filled_pass(frames: u64) -> PreviewPass {
let mut pass = PreviewPass::new(
PreviewOptions::for_frame_rate(24),
&dimensions(1920, 1080),
frames,
);
let mut value = 0_u8;
while pass.next_frame().is_some() {
pass.accept(&rgba(64, 64, value), &Limits::default());
value = value.wrapping_add(1);
}
pass
}
fn reader() -> ExactFrameReader {
let configuration = VideoDecoderConfig {
codec: Codec::UncompressedVideo,
profile: CodecProfile::UncompressedGray8,
coded_dimensions: dimensions(1, 1),
output_format: PixelFormat::Gray8,
color_range: ColorRange::Full,
hardware: HardwarePreference::Avoid,
configuration: Vec::new(),
};
let samples = (0..FRAMES)
.map(|index| EncodedVideoSample {
presentation_index: FrameIndex(index),
random_access: index == 0,
data: vec![0],
})
.collect();
ExactFrameReader::new(
&uncompressed_video_decoder_factory(),
configuration,
samples,
Limits::default(),
)
.unwrap()
}
#[wasm_bindgen_test]
fn a_browser_seek_is_answered_by_a_preview_rather_than_left_pending() {
let mut reader = reader();
assert_eq!(
reader.seek(FrameIndex(FRAMES - 1)),
Seek::Pending,
"with no source attached there is nothing to answer from"
);
let pass = filled_pass(FRAMES);
reader.set_seek_previews(Some(Arc::new(pass.store())));
match reader.seek(FrameIndex(FRAMES - 1)) {
Seek::Preview { frame, picture } => {
assert!(frame.0 <= FRAMES - 1);
assert!(FRAMES - 1 - frame.0 < pass.stride());
assert_eq!(picture.pixel_format, PixelFormat::Rgba8);
}
other => panic!("a browser seek must be answered by a preview, got {other:?}"),
}
assert_eq!(
reader.statistics().samples_submitted,
0,
"a seek answered from the tier decodes nothing"
);
}
#[wasm_bindgen_test]
fn a_partly_filled_pass_still_answers_the_far_end_of_the_track() {
let mut pass = PreviewPass::new(
PreviewOptions::for_frame_rate(24),
&dimensions(1920, 1080),
FRAMES,
);
assert_eq!(pass.store().nearest(0), None);
pass.accept(&rgba(64, 64, 7), &Limits::default());
let (frame, picture) = pass
.store()
.nearest_at(FrameIndex(FRAMES - 1))
.expect("the picture behind the pointer");
assert_eq!(frame, FrameIndex(0));
assert_eq!(picture.planes[0].data[0], 7);
}
#[wasm_bindgen_test(async)]
async fn one_step_of_the_browser_pass_fills_one_position_and_moves_on() {
const SAMPLE: &[u8] = include_bytes!("../examples/media/BigBuckBunny.mp4");
let limits = Limits::default();
let mut index =
match WebPreviewIndex::open(SAMPLE, 0, &limits, PreviewOptions::for_frame_rate(24))
.await
{
Ok(index) => index,
Err(error) => {
assert_eq!(error.kind(), ErrorKind::Unsupported);
return;
}
};
assert_eq!(index.coverage().0, 0, "nothing is decoded before a step");
assert_eq!(index.next_frame(), Some(FrameIndex(0)));
let more = index
.step(&CancellationToken::new())
.await
.expect("a step over a decodable track");
assert!(more, "768 frames do not fit in one preview position");
assert_eq!(
index.next_frame(),
Some(FrameIndex(index.stride())),
"the pass advanced exactly one position"
);
match index.store().nearest_at(FrameIndex(index.stride() * 4)) {
Some((frame, picture)) => {
assert_eq!(frame, FrameIndex(0));
assert_eq!(picture.pixel_format, PixelFormat::Rgba8);
assert!(
picture.dimensions.width < 1920,
"a preview is the frame shrunk, not the frame"
);
console_log!(
"one browser preview step stored a {}x{} picture of frame {}",
picture.dimensions.width,
picture.dimensions.height,
frame.0
);
}
None => console_log!("this browser decoded no picture for the first preview position"),
}
}
#[wasm_bindgen_test]
fn a_browser_preview_answers_any_position_inside_the_seek_budget() {
let pass = filled_pass(FRAMES);
let store = pass.store();
let (filled, total) = store.coverage();
assert_eq!(filled, total);
let performance = web_sys::window()
.and_then(|window| window.performance())
.expect("performance.now()");
let mut worst_ms = 0.0_f64;
for step in 0..=100_u64 {
let frame = step * (FRAMES - 1) / 100;
let started = performance.now();
let preview = store
.nearest_at(FrameIndex(frame))
.expect("a preview for every position");
worst_ms = worst_ms.max(performance.now() - started);
assert_eq!(preview.1.pixel_format, PixelFormat::Rgba8);
}
let budget_ms = SEEK_LATENCY_BUDGET.as_secs_f64() * 1_000.0;
console_log!(
"browser preview index over {FRAMES} frames, {total} positions; worst lookup {worst_ms:.3} ms"
);
assert!(
worst_ms < budget_ms,
"a browser preview lookup took {worst_ms} ms, over the {budget_ms} ms budget"
);
}
}