use crate::{
AudioBuffer, Codec, ColorRange, Error, ErrorKind, FrameIndex, FrameSource, Limits, PixelFormat,
Plane, Result, VideoDimensions, VideoFrame,
};
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
pub type EncoderFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + 'a>>;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TrackKind {
Video,
Audio,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SampleDependency {
pub is_leading: u8,
pub depends_on: u8,
pub is_depended_on: u8,
pub has_redundancy: u8,
}
impl SampleDependency {
pub const INDEPENDENT: Self = Self {
is_leading: 0,
depends_on: 2,
is_depended_on: 0,
has_redundancy: 0,
};
pub const DEPENDENT: Self = Self {
is_leading: 0,
depends_on: 1,
is_depended_on: 0,
has_redundancy: 0,
};
#[doc(hidden)]
pub fn to_sdtp(self) -> u8 {
((self.is_leading & 3) << 6)
| ((self.depends_on & 3) << 4)
| ((self.is_depended_on & 3) << 2)
| (self.has_redundancy & 3)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EncodedSample {
pub data: Vec<u8>,
pub dts: i64,
pub pts: i64,
pub duration: u32,
pub is_sync: bool,
pub dependency: SampleDependency,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EncoderConfig {
pub codec: Codec,
pub timescale: u32,
pub decoder_config: Vec<u8>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct VideoEncoderFormat {
pub dimensions: VideoDimensions,
pub pixel_format: PixelFormat,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AudioEncoderFormat {
pub sample_rate: u32,
pub channels: u16,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct AudioGapless {
pub priming: u32,
pub padding: u32,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct AudioDrain {
pub samples: Vec<EncodedSample>,
pub gapless: AudioGapless,
}
pub trait VideoEncoder {
fn config(&self) -> &EncoderConfig;
fn format(&self) -> VideoEncoderFormat;
fn implementation(&self) -> CodecImplementation {
CodecImplementation::Software
}
fn backend_name(&self) -> &str {
"zvidlib"
}
fn encode<'a>(
&'a mut self,
index: FrameIndex,
frame: FrameSource<'a>,
) -> EncoderFuture<'a, Vec<EncodedSample>>;
fn finish<'a>(&'a mut self) -> EncoderFuture<'a, Vec<EncodedSample>>;
}
pub trait AudioEncoder {
fn config(&self) -> &EncoderConfig;
fn format(&self) -> AudioEncoderFormat;
fn encode<'a>(
&'a mut self,
index: FrameIndex,
buffer: AudioBuffer,
) -> EncoderFuture<'a, Vec<EncodedSample>>;
fn finish<'a>(&'a mut self) -> EncoderFuture<'a, AudioDrain>;
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum CodecProfile {
UncompressedGray8,
HevcMain,
HevcMain10,
Av1Main,
Av1High,
Av1Professional,
Vp8,
Vp9Profile0,
Vp9Profile1,
Vp9Profile2,
Vp9Profile3,
AacLowComplexity,
Opus,
Vorbis,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum HardwarePreference {
Require,
Prefer,
Avoid,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum CodecImplementation {
Software,
Hardware,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum CodecSupport {
Supported { implementation: CodecImplementation },
UnsupportedCodec,
UnsupportedProfile,
InvalidConfiguration { reason: String },
HardwareUnavailable,
}
impl CodecSupport {
pub const fn is_supported(&self) -> bool {
matches!(self, Self::Supported { .. })
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VideoDecoderConfig {
pub codec: Codec,
pub profile: CodecProfile,
pub coded_dimensions: VideoDimensions,
pub output_format: PixelFormat,
pub color_range: ColorRange,
pub hardware: HardwarePreference,
pub configuration: Vec<u8>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VideoEncoderConfig {
pub codec: Codec,
pub profile: CodecProfile,
pub coded_dimensions: VideoDimensions,
pub input_format: PixelFormat,
pub color_range: ColorRange,
pub hardware: HardwarePreference,
pub timescale: u32,
pub frame_duration: u32,
pub configuration: Vec<u8>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EncodedVideoSample {
pub presentation_index: FrameIndex,
pub random_access: bool,
pub data: Vec<u8>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DecodedVideoFrame {
pub presentation_index: FrameIndex,
pub frame: VideoFrame,
}
#[derive(Clone, Debug, Default)]
pub struct CancellationToken {
cancelled: Arc<AtomicBool>,
}
impl CancellationToken {
pub fn new() -> Self {
Self::default()
}
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::Release);
}
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::Acquire)
}
#[doc(hidden)]
pub fn check(&self) -> Result<()> {
if self.is_cancelled() {
Err(Error::new(
ErrorKind::Cancelled,
"codec operation cancelled",
))
} else {
Ok(())
}
}
}
pub trait VideoDecoder: Send {
fn submit(
&mut self,
sample: &EncodedVideoSample,
cancellation: &CancellationToken,
) -> Result<Vec<DecodedVideoFrame>>;
fn drain(&mut self, cancellation: &CancellationToken) -> Result<Vec<DecodedVideoFrame>>;
fn reset(&mut self) -> Result<()>;
fn set_output_wanted(&mut self, _wanted: bool) {}
}
pub trait VideoDecoderFactory {
fn capability(&self, configuration: &VideoDecoderConfig) -> CodecSupport;
fn create(
&self,
configuration: &VideoDecoderConfig,
limits: &Limits,
) -> Result<Box<dyn VideoDecoder>>;
}
pub trait VideoEncoderFactory {
fn capability(&self, configuration: &VideoEncoderConfig) -> CodecSupport;
fn create(
&self,
configuration: &VideoEncoderConfig,
limits: &Limits,
) -> Result<Box<dyn VideoEncoder>>;
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AudioEncoderConfig {
pub codec: Codec,
pub profile: CodecProfile,
pub sample_rate: u32,
pub channels: u16,
pub timescale: u32,
pub configuration: Vec<u8>,
}
pub trait AudioEncoderFactory {
fn capability(&self, configuration: &AudioEncoderConfig) -> CodecSupport;
fn create(
&self,
configuration: &AudioEncoderConfig,
limits: &Limits,
) -> Result<Box<dyn AudioEncoder>>;
}
#[doc(hidden)]
pub fn parse_audio_bit_rate(configuration: &[u8]) -> Option<Option<u32>> {
match configuration {
[] => Some(None),
[a, b, c, d] => match u32::from_be_bytes([*a, *b, *c, *d]) {
0 => None,
bits_per_second => Some(Some(bits_per_second)),
},
_ => None,
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct DecodeStatistics {
pub samples_submitted: u64,
pub samples_skipped: u64,
pub cache_hits: u64,
pub resets: u64,
pub drains: u64,
}
pub struct ExactFrameReader {
configuration: VideoDecoderConfig,
decoder: Box<dyn VideoDecoder>,
samples: Vec<EncodedVideoSample>,
decode_position_by_presentation: HashMap<FrameIndex, usize>,
cache: BTreeMap<FrameIndex, VideoFrame>,
lru: VecDeque<FrameIndex>,
published_since_reset: HashSet<FrameIndex>,
suppressed_since_reset: HashSet<FrameIndex>,
in_flight_since_reset: HashSet<FrameIndex>,
output_wanted: bool,
next_decode_position: Option<usize>,
session_start: Option<usize>,
seek_previews: Option<Arc<dyn SeekPreviewSource>>,
limits: Limits,
statistics: DecodeStatistics,
}
pub const SEEK_LATENCY_BUDGET: Duration = Duration::from_millis(50);
pub trait SeekPreviewSource: Send + Sync {
fn nearest_at(&self, frame: FrameIndex) -> Option<(FrameIndex, VideoFrame)>;
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Seek {
Exact(VideoFrame),
Preview {
picture: VideoFrame,
frame: FrameIndex,
},
Pending,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Request {
Destination,
Step,
}
impl Request {
fn cache_tail(self, limits: &Limits) -> u64 {
match self {
Self::Destination => u64::from(limits.max_cached_frames),
Self::Step => 0,
}
}
}
impl ExactFrameReader {
pub fn new(
factory: &dyn VideoDecoderFactory,
configuration: VideoDecoderConfig,
samples: Vec<EncodedVideoSample>,
limits: Limits,
) -> Result<Self> {
if limits.max_cached_frames == 0 || limits.max_decode_samples_per_seek == 0 {
return Err(Error::new(
ErrorKind::ResourceLimit,
"exact-frame limits must permit cached frames and decode work",
));
}
let capability = factory.capability(&configuration);
if !capability.is_supported() {
return Err(capability_error(capability));
}
if samples.is_empty() {
return Err(Error::new(
ErrorKind::InvalidInput,
"an exact-frame reader requires at least one sample",
));
}
if !samples[0].random_access {
return Err(Error::new(
ErrorKind::InvalidInput,
"the first decode-order sample must be a random-access point",
));
}
let mut positions = HashMap::with_capacity(samples.len());
for (position, sample) in samples.iter().enumerate() {
if positions
.insert(sample.presentation_index, position)
.is_some()
{
return Err(Error::new(
ErrorKind::InvalidInput,
"presentation frame identities must be unique",
));
}
}
let decoder = factory.create(&configuration, &limits)?;
Ok(Self {
configuration,
decoder,
samples,
decode_position_by_presentation: positions,
cache: BTreeMap::new(),
lru: VecDeque::new(),
published_since_reset: HashSet::new(),
suppressed_since_reset: HashSet::new(),
in_flight_since_reset: HashSet::new(),
output_wanted: true,
next_decode_position: None,
session_start: None,
seek_previews: None,
limits,
statistics: DecodeStatistics::default(),
})
}
pub fn set_seek_previews(&mut self, previews: Option<Arc<dyn SeekPreviewSource>>) {
self.seek_previews = previews;
}
pub fn seek(&mut self, presentation_index: FrameIndex) -> Seek {
if let Some(frame) = self.cache.get(&presentation_index).cloned() {
self.statistics.cache_hits = self.statistics.cache_hits.saturating_add(1);
self.touch(presentation_index);
return Seek::Exact(frame);
}
match self
.seek_previews
.as_ref()
.and_then(|previews| previews.nearest_at(presentation_index))
{
Some((frame, picture)) => Seek::Preview { picture, frame },
None => Seek::Pending,
}
}
pub fn get(
&mut self,
presentation_index: FrameIndex,
cancellation: &CancellationToken,
) -> Result<VideoFrame> {
self.get_request(presentation_index, Request::Destination, cancellation)
}
pub fn get_step(
&mut self,
presentation_index: FrameIndex,
cancellation: &CancellationToken,
) -> Result<VideoFrame> {
self.get_request(presentation_index, Request::Step, cancellation)
}
fn get_request(
&mut self,
presentation_index: FrameIndex,
request: Request,
cancellation: &CancellationToken,
) -> Result<VideoFrame> {
cancellation.check()?;
if let Some(frame) = self.cache.get(&presentation_index).cloned() {
self.statistics.cache_hits = self.statistics.cache_hits.saturating_add(1);
self.touch(presentation_index);
return Ok(frame);
}
let target_position = *self
.decode_position_by_presentation
.get(&presentation_index)
.ok_or_else(|| {
Error::new(ErrorKind::InvalidInput, "presentation frame is not indexed")
})?;
let random_access_position = self.nearest_random_access(target_position);
let can_reuse = self
.next_decode_position
.is_some_and(|position| position >= random_access_position)
&& self
.session_start
.is_some_and(|start| start <= random_access_position)
&& !self.published_since_reset.contains(&presentation_index)
&& !self.suppressed_since_reset.contains(&presentation_index);
if !can_reuse {
self.decoder.reset()?;
self.statistics.resets = self.statistics.resets.saturating_add(1);
self.published_since_reset.clear();
self.suppressed_since_reset.clear();
self.in_flight_since_reset.clear();
self.next_decode_position = Some(random_access_position);
self.session_start = Some(random_access_position);
}
let mut work = 0_u32;
while let Some(position) = self.next_decode_position {
cancellation.check()?;
if work >= self.limits.max_decode_samples_per_seek {
return Err(Error::new(
ErrorKind::ResourceLimit,
"exact-frame request exceeded the configured decode-work limit",
));
}
if position == self.samples.len() {
self.set_output_wanted(true);
self.drain_internal(cancellation)?;
break;
}
let cache_tail = request.cache_tail(&self.limits);
let wanted = position >= target_position
|| self.samples[position].presentation_index.0
>= presentation_index.0.saturating_sub(cache_tail);
self.set_output_wanted(wanted);
let suppressed = !self.output_wanted;
let outputs = self.decoder.submit(&self.samples[position], cancellation)?;
self.statistics.samples_submitted = self.statistics.samples_submitted.saturating_add(1);
if suppressed {
self.statistics.samples_skipped = self.statistics.samples_skipped.saturating_add(1);
self.suppressed_since_reset
.insert(self.samples[position].presentation_index);
} else {
self.in_flight_since_reset
.insert(self.samples[position].presentation_index);
}
self.next_decode_position = Some(position + 1);
work += 1;
self.publish(outputs)?;
if let Some(frame) = self.cache.get(&presentation_index).cloned() {
self.touch(presentation_index);
return Ok(frame);
}
}
if let Some(frame) = self.cache.get(&presentation_index).cloned() {
self.touch(presentation_index);
Ok(frame)
} else {
Err(Error::new(
ErrorKind::MalformedMedia,
"decoder did not produce the requested presentation frame",
))
}
}
pub fn drain(&mut self, cancellation: &CancellationToken) -> Result<usize> {
cancellation.check()?;
self.set_output_wanted(true);
self.drain_internal(cancellation)
}
fn set_output_wanted(&mut self, wanted: bool) {
if self.output_wanted == wanted {
return;
}
if !wanted {
for frame in self.in_flight_since_reset.drain() {
self.suppressed_since_reset.insert(frame);
}
}
self.decoder.set_output_wanted(wanted);
self.output_wanted = wanted;
}
pub fn reset(&mut self) -> Result<()> {
self.set_output_wanted(true);
self.decoder.reset()?;
self.statistics.resets = self.statistics.resets.saturating_add(1);
self.next_decode_position = None;
self.session_start = None;
self.cache.clear();
self.lru.clear();
self.published_since_reset.clear();
self.suppressed_since_reset.clear();
self.in_flight_since_reset.clear();
Ok(())
}
pub const fn statistics(&self) -> DecodeStatistics {
self.statistics
}
pub fn cached_frames(&self) -> usize {
self.cache.len()
}
fn nearest_random_access(&self, target_position: usize) -> usize {
let target = self.samples[target_position].presentation_index;
(0..=target_position)
.rev()
.find(|position| {
let sample = &self.samples[*position];
sample.random_access && sample.presentation_index <= target
})
.unwrap_or(0)
}
fn drain_internal(&mut self, cancellation: &CancellationToken) -> Result<usize> {
let outputs = self.decoder.drain(cancellation)?;
let count = outputs.len();
self.statistics.drains = self.statistics.drains.saturating_add(1);
self.next_decode_position = Some(self.samples.len());
self.publish(outputs)?;
Ok(count)
}
fn publish(&mut self, outputs: Vec<DecodedVideoFrame>) -> Result<()> {
for output in outputs {
self.suppressed_since_reset
.remove(&output.presentation_index);
self.in_flight_since_reset
.remove(&output.presentation_index);
self.published_since_reset.insert(output.presentation_index);
if !self
.decode_position_by_presentation
.contains_key(&output.presentation_index)
{
return Err(Error::new(
ErrorKind::MalformedMedia,
"decoder produced an unindexed presentation frame",
));
}
if output.frame.dimensions != self.configuration.coded_dimensions
|| output.frame.pixel_format != self.configuration.output_format
|| output.frame.color_range != self.configuration.color_range
{
return Err(Error::new(
ErrorKind::MalformedMedia,
"decoder output does not match its normalized configuration",
));
}
self.insert_cache(output.presentation_index, output.frame);
}
Ok(())
}
fn insert_cache(&mut self, index: FrameIndex, frame: VideoFrame) {
self.cache.insert(index, frame);
self.touch(index);
while self.cache.len() > self.limits.max_cached_frames as usize {
if let Some(evicted) = self.lru.pop_front() {
self.cache.remove(&evicted);
}
}
}
fn touch(&mut self, index: FrameIndex) {
self.lru.retain(|candidate| *candidate != index);
self.lru.push_back(index);
}
}
fn capability_error(capability: CodecSupport) -> Error {
let (kind, message) = match capability {
CodecSupport::UnsupportedCodec => (
ErrorKind::Unsupported,
"decoder does not support the requested codec".into(),
),
CodecSupport::UnsupportedProfile => (
ErrorKind::Unsupported,
"decoder does not support the requested profile".into(),
),
CodecSupport::InvalidConfiguration { reason } => (ErrorKind::InvalidInput, reason),
CodecSupport::HardwareUnavailable => (
ErrorKind::Unsupported,
"requested hardware decoder is unavailable".into(),
),
CodecSupport::Supported { .. } => (
ErrorKind::Internal,
"decoder capability changed unexpectedly".into(),
),
};
Error::new(kind, message)
}
pub fn uncompressed_video_decoder_factory() -> impl VideoDecoderFactory {
UncompressedVideoDecoderFactory
}
struct UncompressedVideoDecoderFactory;
impl VideoDecoderFactory for UncompressedVideoDecoderFactory {
fn capability(&self, configuration: &VideoDecoderConfig) -> CodecSupport {
if configuration.codec != Codec::UncompressedVideo {
return CodecSupport::UnsupportedCodec;
}
if configuration.profile != CodecProfile::UncompressedGray8 {
return CodecSupport::UnsupportedProfile;
}
if configuration.output_format != PixelFormat::Gray8
|| !configuration.configuration.is_empty()
{
return CodecSupport::InvalidConfiguration {
reason: "uncompressed Gray8 requires Gray8 output and no configuration bytes"
.into(),
};
}
if configuration.hardware == HardwarePreference::Require {
return CodecSupport::HardwareUnavailable;
}
CodecSupport::Supported {
implementation: CodecImplementation::Software,
}
}
fn create(
&self,
configuration: &VideoDecoderConfig,
limits: &Limits,
) -> Result<Box<dyn VideoDecoder>> {
let capability = self.capability(configuration);
if !capability.is_supported() {
return Err(capability_error(capability));
}
if configuration.coded_dimensions.width > limits.max_width
|| configuration.coded_dimensions.height > limits.max_height
{
return Err(Error::new(
ErrorKind::ResourceLimit,
"decoded dimensions exceed the configured limits",
));
}
let required = u64::from(configuration.coded_dimensions.width)
.checked_mul(u64::from(configuration.coded_dimensions.height))
.ok_or_else(|| Error::new(ErrorKind::ResourceLimit, "decoded frame size overflow"))?;
if required > limits.max_allocation_bytes {
return Err(Error::new(
ErrorKind::ResourceLimit,
"decoded frame exceeds the configured allocation limit",
));
}
Ok(Box::new(UncompressedVideoDecoder {
configuration: configuration.clone(),
limits: *limits,
}))
}
}
struct UncompressedVideoDecoder {
configuration: VideoDecoderConfig,
limits: Limits,
}
impl VideoDecoder for UncompressedVideoDecoder {
fn submit(
&mut self,
sample: &EncodedVideoSample,
cancellation: &CancellationToken,
) -> Result<Vec<DecodedVideoFrame>> {
cancellation.check()?;
let width = usize::try_from(self.configuration.coded_dimensions.width)
.map_err(|_| Error::new(ErrorKind::ResourceLimit, "decoded width is too large"))?;
let height = usize::try_from(self.configuration.coded_dimensions.height)
.map_err(|_| Error::new(ErrorKind::ResourceLimit, "decoded height is too large"))?;
let length = width
.checked_mul(height)
.ok_or_else(|| Error::new(ErrorKind::ResourceLimit, "decoded frame size overflow"))?;
if sample.data.len() != length {
return Err(Error::new(
ErrorKind::MalformedMedia,
"uncompressed Gray8 packet size does not match coded dimensions",
));
}
let frame = VideoFrame::new(
self.configuration.coded_dimensions,
PixelFormat::Gray8,
self.configuration.color_range,
vec![Plane {
data: sample.data.clone(),
stride: width,
}],
&self.limits,
)?;
Ok(vec![DecodedVideoFrame {
presentation_index: sample.presentation_index,
frame,
}])
}
fn drain(&mut self, cancellation: &CancellationToken) -> Result<Vec<DecodedVideoFrame>> {
cancellation.check()?;
Ok(Vec::new())
}
fn reset(&mut self) -> Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
fn config() -> VideoDecoderConfig {
VideoDecoderConfig {
codec: Codec::UncompressedVideo,
profile: CodecProfile::UncompressedGray8,
coded_dimensions: VideoDimensions::new(1, 1, &Limits::default()).unwrap(),
output_format: PixelFormat::Gray8,
color_range: ColorRange::Full,
hardware: HardwarePreference::Avoid,
configuration: Vec::new(),
}
}
fn sample(index: u64, value: u8, random_access: bool) -> EncodedVideoSample {
EncodedVideoSample {
presentation_index: FrameIndex(index),
random_access,
data: vec![value],
}
}
fn value(frame: &VideoFrame) -> u8 {
frame.planes[0].data[0]
}
fn single_group_reader(frames: u64) -> ExactFrameReader {
let samples = (0..frames)
.map(|index| sample(index, (index % 251) as u8, index == 0))
.collect();
ExactFrameReader::new(
&uncompressed_video_decoder_factory(),
config(),
samples,
Limits::default(),
)
.unwrap()
}
struct StridedPreviews {
stride: u64,
frames: u64,
}
impl SeekPreviewSource for StridedPreviews {
fn nearest_at(&self, frame: FrameIndex) -> Option<(FrameIndex, VideoFrame)> {
if self.frames == 0 {
return None;
}
let at = (frame.0 / self.stride * self.stride).min(self.frames - 1);
let picture = VideoFrame::new(
config().coded_dimensions,
PixelFormat::Gray8,
ColorRange::Full,
vec![Plane {
data: vec![(at % 251) as u8],
stride: 1,
}],
&Limits::default(),
)
.ok()?;
Some((FrameIndex(at), picture))
}
}
#[test]
fn a_seek_with_nothing_decoded_is_pending_and_submits_no_samples() {
let mut reader = single_group_reader(512);
assert_eq!(reader.seek(FrameIndex(511)), Seek::Pending);
assert_eq!(reader.statistics().samples_submitted, 0);
assert_eq!(reader.statistics().resets, 0);
}
#[test]
fn a_seek_into_the_cache_answers_exactly() {
let mut reader = single_group_reader(64);
let cancellation = CancellationToken::new();
reader.get(FrameIndex(40), &cancellation).unwrap();
let submitted = reader.statistics().samples_submitted;
let Seek::Exact(frame) = reader.seek(FrameIndex(40)) else {
panic!("a cached frame seeks exactly");
};
assert_eq!(value(&frame), 40);
assert_eq!(
reader.statistics().samples_submitted,
submitted,
"an exact seek decodes nothing"
);
}
#[test]
fn a_seek_anywhere_on_a_single_group_track_decodes_nothing() {
let mut reader = single_group_reader(512);
reader.set_seek_previews(Some(Arc::new(StridedPreviews {
stride: 12,
frames: 512,
})));
for target in [0_u64, 1, 255, 400, 511] {
match reader.seek(FrameIndex(target)) {
Seek::Preview { frame, .. } => assert!(
frame.0.abs_diff(target) < 12,
"frame {target} answered by {frame:?}, further than one stride away"
),
Seek::Exact(_) => {}
Seek::Pending => panic!("frame {target} had no answer"),
}
}
assert_eq!(
reader.statistics().samples_submitted,
0,
"seeking is not decoding"
);
assert_eq!(reader.statistics().resets, 0);
}
#[test]
fn a_seek_to_any_position_answers_inside_the_latency_budget() {
let frames = 768;
let mut reader = single_group_reader(frames);
reader.set_seek_previews(Some(Arc::new(StridedPreviews { stride: 12, frames })));
let mut worst = Duration::ZERO;
for step in 0..=100_u64 {
let target = FrameIndex(step * (frames - 1) / 100);
let started = std::time::Instant::now();
let answer = reader.seek(target);
worst = worst.max(started.elapsed());
assert_ne!(answer, Seek::Pending, "frame {} had no answer", target.0);
}
assert!(
worst < SEEK_LATENCY_BUDGET,
"the worst seek took {worst:?}, over the {SEEK_LATENCY_BUDGET:?} budget"
);
}
#[test]
fn detaching_the_preview_source_leaves_only_the_cache() {
let mut reader = single_group_reader(64);
reader.set_seek_previews(Some(Arc::new(StridedPreviews {
stride: 12,
frames: 64,
})));
assert!(matches!(reader.seek(FrameIndex(63)), Seek::Preview { .. }));
reader.set_seek_previews(None);
assert_eq!(reader.seek(FrameIndex(63)), Seek::Pending);
}
#[test]
fn a_leading_picture_is_decoded_from_the_random_access_point_before_it() {
let samples = vec![
sample(0, 10, true),
sample(1, 11, false),
sample(2, 12, false),
sample(4, 14, true),
sample(3, 13, false),
sample(5, 15, false),
];
let mut reader = ExactFrameReader::new(
&uncompressed_video_decoder_factory(),
config(),
samples.clone(),
Limits::default(),
)
.unwrap();
let cancellation = CancellationToken::new();
assert_eq!(
value(&reader.get(FrameIndex(3), &cancellation).unwrap()),
13
);
assert_eq!(reader.statistics().samples_submitted, 5);
let mut reader = ExactFrameReader::new(
&uncompressed_video_decoder_factory(),
config(),
samples,
Limits::default(),
)
.unwrap();
assert_eq!(
value(&reader.get(FrameIndex(5), &cancellation).unwrap()),
15
);
assert_eq!(reader.statistics().samples_submitted, 3);
}
#[test]
fn capability_distinguishes_codec_profile_configuration_and_hardware() {
let factory = uncompressed_video_decoder_factory();
assert!(factory.capability(&config()).is_supported());
let mut candidate = config();
candidate.codec = Codec::Av1;
assert_eq!(
factory.capability(&candidate),
CodecSupport::UnsupportedCodec
);
candidate = config();
candidate.profile = CodecProfile::Av1Main;
assert_eq!(
factory.capability(&candidate),
CodecSupport::UnsupportedProfile
);
candidate = config();
candidate.output_format = PixelFormat::Rgba8;
assert!(matches!(
factory.capability(&candidate),
CodecSupport::InvalidConfiguration { .. }
));
candidate = config();
candidate.hardware = HardwarePreference::Require;
assert_eq!(
factory.capability(&candidate),
CodecSupport::HardwareUnavailable
);
candidate = config();
candidate.output_format = PixelFormat::Rgba8;
assert_eq!(
factory
.create(&candidate, &Limits::default())
.err()
.unwrap()
.kind(),
ErrorKind::InvalidInput
);
let restrictive = Limits {
max_width: 0,
..Limits::default()
};
assert_eq!(
factory
.create(&config(), &restrictive)
.err()
.unwrap()
.kind(),
ErrorKind::ResourceLimit
);
}
#[test]
fn a_request_before_the_open_session_start_resets_the_decoder() {
let samples = (0..4)
.map(|index| sample(index, index as u8 + 10, true))
.collect();
let mut reader = ExactFrameReader::new(
&uncompressed_video_decoder_factory(),
config(),
samples,
Limits {
max_cached_frames: 1,
..Limits::default()
},
)
.unwrap();
let cancellation = CancellationToken::new();
assert_eq!(
value(&reader.get(FrameIndex(2), &cancellation).unwrap()),
12
);
assert_eq!(
value(&reader.get(FrameIndex(0), &cancellation).unwrap()),
10
);
assert_eq!(reader.statistics().resets, 2);
}
#[test]
fn conformance_vector_reorders_exact_frames_and_reuses_bounded_state() {
let samples = vec![
sample(0, 10, true),
sample(3, 40, false),
sample(1, 20, false),
sample(2, 30, false),
sample(4, 50, true),
sample(6, 70, false),
sample(5, 60, false),
];
let factory = uncompressed_video_decoder_factory();
let mut reader = ExactFrameReader::new(
&factory,
config(),
samples,
Limits {
max_cached_frames: 3,
..Limits::default()
},
)
.unwrap();
let cancellation = CancellationToken::new();
assert_eq!(
value(&reader.get(FrameIndex(1), &cancellation).unwrap()),
20
);
let first = reader.statistics();
assert_eq!(
value(&reader.get(FrameIndex(2), &cancellation).unwrap()),
30
);
assert_eq!(reader.statistics().resets, first.resets);
assert_eq!(
value(&reader.get(FrameIndex(3), &cancellation).unwrap()),
40
);
assert!(reader.statistics().cache_hits > 0);
assert_eq!(
value(&reader.get(FrameIndex(5), &cancellation).unwrap()),
60
);
assert_eq!(
value(&reader.get(FrameIndex(0), &cancellation).unwrap()),
10
);
assert!(reader.statistics().resets > first.resets);
assert!(reader.cached_frames() <= 3);
}
struct HintFactory {
wanted: Arc<Mutex<Vec<(u64, bool)>>>,
}
struct HintDecoder {
configuration: VideoDecoderConfig,
limits: Limits,
wanted: Arc<Mutex<Vec<(u64, bool)>>>,
output_wanted: bool,
}
impl VideoDecoderFactory for HintFactory {
fn capability(&self, _: &VideoDecoderConfig) -> CodecSupport {
CodecSupport::Supported {
implementation: CodecImplementation::Software,
}
}
fn create(
&self,
configuration: &VideoDecoderConfig,
limits: &Limits,
) -> Result<Box<dyn VideoDecoder>> {
Ok(Box::new(HintDecoder {
configuration: configuration.clone(),
limits: *limits,
wanted: Arc::clone(&self.wanted),
output_wanted: true,
}))
}
}
impl VideoDecoder for HintDecoder {
fn submit(
&mut self,
sample: &EncodedVideoSample,
cancellation: &CancellationToken,
) -> Result<Vec<DecodedVideoFrame>> {
cancellation.check()?;
self.wanted
.lock()
.expect("hint log poisoned")
.push((sample.presentation_index.0, self.output_wanted));
if !self.output_wanted {
return Ok(Vec::new());
}
Ok(vec![DecodedVideoFrame {
presentation_index: sample.presentation_index,
frame: VideoFrame::new(
self.configuration.coded_dimensions,
PixelFormat::Gray8,
ColorRange::Full,
vec![Plane {
data: sample.data.clone(),
stride: 1,
}],
&self.limits,
)?,
}])
}
fn drain(&mut self, _: &CancellationToken) -> Result<Vec<DecodedVideoFrame>> {
Ok(Vec::new())
}
fn reset(&mut self) -> Result<()> {
Ok(())
}
fn set_output_wanted(&mut self, wanted: bool) {
self.output_wanted = wanted;
}
}
struct ReplayFactory;
struct ReplayDecoder {
configuration: VideoDecoderConfig,
limits: Limits,
previous: Option<(FrameIndex, u8)>,
}
impl ReplayDecoder {
fn produce(&self, presentation_index: FrameIndex, value: u8) -> Result<DecodedVideoFrame> {
Ok(DecodedVideoFrame {
presentation_index,
frame: VideoFrame::new(
self.configuration.coded_dimensions,
PixelFormat::Gray8,
ColorRange::Full,
vec![Plane {
data: vec![value],
stride: 1,
}],
&self.limits,
)?,
})
}
}
impl VideoDecoderFactory for ReplayFactory {
fn capability(&self, _: &VideoDecoderConfig) -> CodecSupport {
CodecSupport::Supported {
implementation: CodecImplementation::Software,
}
}
fn create(
&self,
configuration: &VideoDecoderConfig,
limits: &Limits,
) -> Result<Box<dyn VideoDecoder>> {
Ok(Box::new(ReplayDecoder {
configuration: configuration.clone(),
limits: *limits,
previous: None,
}))
}
}
impl VideoDecoder for ReplayDecoder {
fn submit(
&mut self,
sample: &EncodedVideoSample,
cancellation: &CancellationToken,
) -> Result<Vec<DecodedVideoFrame>> {
cancellation.check()?;
let value = sample.data[0];
let mut outputs = Vec::new();
if let Some((index, previous)) = self.previous {
outputs.push(self.produce(index, previous)?);
}
outputs.push(self.produce(sample.presentation_index, value)?);
self.previous = Some((sample.presentation_index, value));
Ok(outputs)
}
fn drain(&mut self, _: &CancellationToken) -> Result<Vec<DecodedVideoFrame>> {
Ok(Vec::new())
}
fn reset(&mut self) -> Result<()> {
self.previous = None;
Ok(())
}
}
#[test]
fn a_frame_the_decoder_emits_twice_is_cached_again_rather_than_refused() {
let mut reader = ExactFrameReader::new(
&ReplayFactory,
config(),
single_group_of_pictures(),
Limits::default(),
)
.unwrap();
let cancellation = CancellationToken::new();
assert_eq!(
value(&reader.get(FrameIndex(3), &cancellation).unwrap()),
30
);
assert_eq!(
value(&reader.get(FrameIndex(6), &cancellation).unwrap()),
60
);
let resets = reader.statistics().resets;
let hits = reader.statistics().cache_hits;
assert_eq!(
value(&reader.get(FrameIndex(5), &cancellation).unwrap()),
50
);
assert_eq!(reader.statistics().resets, resets);
assert!(reader.statistics().cache_hits > hits);
}
fn single_group_of_pictures() -> Vec<EncodedVideoSample> {
(0..10)
.map(|index| sample(index, (index * 10) as u8, index == 0))
.collect()
}
fn small_cache() -> Limits {
Limits {
max_cached_frames: 2,
..Limits::default()
}
}
#[test]
fn walking_to_a_distant_frame_does_not_ask_for_the_pictures_it_passes() {
let log = Arc::new(Mutex::new(Vec::new()));
let factory = HintFactory {
wanted: Arc::clone(&log),
};
let mut reader = ExactFrameReader::new(
&factory,
config(),
single_group_of_pictures(),
small_cache(),
)
.unwrap();
let cancellation = CancellationToken::new();
assert_eq!(
value(&reader.get(FrameIndex(8), &cancellation).unwrap()),
80
);
assert_eq!(
*log.lock().unwrap(),
vec![
(0, false),
(1, false),
(2, false),
(3, false),
(4, false),
(5, false),
(6, true),
(7, true),
(8, true),
],
"only the target and the tail the cache can hold are produced"
);
let statistics = reader.statistics();
assert_eq!(statistics.samples_submitted, 9);
assert_eq!(statistics.samples_skipped, 6);
assert_eq!(statistics.resets, 1, "the walk itself is one decode");
}
#[test]
fn a_frame_the_walk_went_past_is_decoded_again_rather_than_reported_missing() {
let log = Arc::new(Mutex::new(Vec::new()));
let factory = HintFactory {
wanted: Arc::clone(&log),
};
let mut reader = ExactFrameReader::new(
&factory,
config(),
single_group_of_pictures(),
small_cache(),
)
.unwrap();
let cancellation = CancellationToken::new();
reader.get(FrameIndex(8), &cancellation).unwrap();
let resets = reader.statistics().resets;
assert_eq!(
value(&reader.get(FrameIndex(4), &cancellation).unwrap()),
40
);
assert_eq!(reader.statistics().resets, resets + 1);
assert_eq!(
value(&reader.get(FrameIndex(4), &cancellation).unwrap()),
40
);
assert_eq!(reader.statistics().resets, resets + 1);
}
#[test]
fn a_reordered_frame_at_or_past_the_target_is_kept_for_the_request_that_follows() {
let samples = vec![
sample(0, 0, true),
sample(4, 40, false),
sample(2, 20, false),
sample(1, 10, false),
sample(3, 30, false),
sample(8, 80, false),
sample(6, 60, false),
sample(5, 50, false),
sample(7, 70, false),
];
let log = Arc::new(Mutex::new(Vec::new()));
let factory = HintFactory {
wanted: Arc::clone(&log),
};
let mut reader = ExactFrameReader::new(&factory, config(), samples, small_cache()).unwrap();
let cancellation = CancellationToken::new();
assert_eq!(
value(&reader.get(FrameIndex(4), &cancellation).unwrap()),
40
);
let resets = reader.statistics().resets;
for (index, expected) in [(1_u64, 10_u8), (2, 20), (3, 30)] {
assert_eq!(
value(&reader.get(FrameIndex(index), &cancellation).unwrap()),
expected,
"frame {index} is still available"
);
}
assert_eq!(
reader.statistics().resets,
resets,
"a frame after the target in decode order is not skipped"
);
assert_eq!(
reader.statistics().samples_skipped,
1,
"only frame 0, the random-access point the walk starts from, is behind frame 4"
);
}
#[test]
fn a_walk_leaves_the_frames_behind_its_target_cached_so_stepping_back_is_free() {
let log = Arc::new(Mutex::new(Vec::new()));
let factory = HintFactory {
wanted: Arc::clone(&log),
};
let samples: Vec<_> = (0..40)
.map(|index| sample(index, index as u8, index == 0))
.collect();
let mut reader = ExactFrameReader::new(
&factory,
config(),
samples,
Limits {
max_cached_frames: 4,
..Limits::default()
},
)
.unwrap();
let cancellation = CancellationToken::new();
reader.get(FrameIndex(32), &cancellation).unwrap();
let after_walk = reader.statistics();
assert_eq!(after_walk.samples_skipped, 28, "frames 0 to 27 are skipped");
for index in [31_u64, 30, 29] {
assert_eq!(
value(&reader.get(FrameIndex(index), &cancellation).unwrap()),
index as u8
);
}
assert_eq!(
reader.statistics().resets,
after_walk.resets,
"the tail the walk kept is in the cache, so stepping back decodes nothing"
);
assert_eq!(
reader.statistics().samples_submitted,
after_walk.samples_submitted
);
}
#[test]
fn a_step_converts_its_own_picture_and_no_tail_behind_it() {
let log = Arc::new(Mutex::new(Vec::new()));
let factory = HintFactory {
wanted: Arc::clone(&log),
};
let samples: Vec<_> = (0..40)
.map(|index| sample(index, index as u8, index == 0))
.collect();
let mut reader = ExactFrameReader::new(
&factory,
config(),
samples,
Limits {
max_cached_frames: 4,
..Limits::default()
},
)
.unwrap();
let cancellation = CancellationToken::new();
assert_eq!(
value(&reader.get_step(FrameIndex(32), &cancellation).unwrap()),
32
);
let after_step = reader.statistics();
assert_eq!(
after_step.samples_skipped, 32,
"frames 0 to 31 are all passed through, tail included"
);
assert_eq!(
reader.cached_frames(),
1,
"only the picture the step asked for was converted"
);
}
#[test]
fn a_walk_of_steps_converts_what_it_asks_for_rather_than_what_it_passes() {
let log = Arc::new(Mutex::new(Vec::new()));
let factory = HintFactory {
wanted: Arc::clone(&log),
};
let samples: Vec<_> = (0..64)
.map(|index| sample(index, index as u8, index == 0))
.collect();
let mut reader = ExactFrameReader::new(
&factory,
config(),
samples,
Limits {
max_cached_frames: 8,
..Limits::default()
},
)
.unwrap();
let cancellation = CancellationToken::new();
let steps: Vec<u64> = (4..60).step_by(4).collect();
for step in &steps {
reader.get_step(FrameIndex(*step), &cancellation).unwrap();
}
let converted = reader.statistics().samples_submitted - reader.statistics().samples_skipped;
assert_eq!(
converted,
steps.len() as u64,
"one conversion per published step, not one per frame passed"
);
assert_eq!(
reader.statistics().resets,
1,
"only the cold start resets; a forward walk of steps never starts over again"
);
}
#[test]
fn a_step_walk_that_ends_in_a_destination_still_leaves_a_cache_tail_to_step_back_through() {
let log = Arc::new(Mutex::new(Vec::new()));
let factory = HintFactory {
wanted: Arc::clone(&log),
};
let samples: Vec<_> = (0..40)
.map(|index| sample(index, index as u8, index == 0))
.collect();
let mut reader = ExactFrameReader::new(
&factory,
config(),
samples,
Limits {
max_cached_frames: 4,
..Limits::default()
},
)
.unwrap();
let cancellation = CancellationToken::new();
for step in [8_u64, 16, 24] {
reader.get_step(FrameIndex(step), &cancellation).unwrap();
}
reader.get(FrameIndex(32), &cancellation).unwrap();
let after_walk = reader.statistics();
for index in [31_u64, 30, 29] {
assert_eq!(
value(&reader.get(FrameIndex(index), &cancellation).unwrap()),
index as u8
);
}
assert_eq!(
reader.statistics().resets,
after_walk.resets,
"the destination kept its tail, so stepping back decodes nothing"
);
assert_eq!(
reader.statistics().samples_submitted,
after_walk.samples_submitted
);
}
struct LateFactory;
struct LateDecoder {
configuration: VideoDecoderConfig,
limits: Limits,
held: Option<EncodedVideoSample>,
output_wanted: bool,
}
impl VideoDecoderFactory for LateFactory {
fn capability(&self, _: &VideoDecoderConfig) -> CodecSupport {
CodecSupport::Supported {
implementation: CodecImplementation::Software,
}
}
fn create(
&self,
configuration: &VideoDecoderConfig,
limits: &Limits,
) -> Result<Box<dyn VideoDecoder>> {
Ok(Box::new(LateDecoder {
configuration: configuration.clone(),
limits: *limits,
held: None,
output_wanted: true,
}))
}
}
impl LateDecoder {
fn release(&self, sample: EncodedVideoSample) -> Result<Vec<DecodedVideoFrame>> {
if !self.output_wanted {
return Ok(Vec::new());
}
Ok(vec![DecodedVideoFrame {
presentation_index: sample.presentation_index,
frame: VideoFrame::new(
self.configuration.coded_dimensions,
PixelFormat::Gray8,
ColorRange::Full,
vec![Plane {
data: sample.data,
stride: 1,
}],
&self.limits,
)?,
}])
}
}
impl VideoDecoder for LateDecoder {
fn submit(
&mut self,
sample: &EncodedVideoSample,
cancellation: &CancellationToken,
) -> Result<Vec<DecodedVideoFrame>> {
cancellation.check()?;
match self.held.replace(sample.clone()) {
Some(previous) => self.release(previous),
None => Ok(Vec::new()),
}
}
fn drain(&mut self, _: &CancellationToken) -> Result<Vec<DecodedVideoFrame>> {
match self.held.take() {
Some(held) => self.release(held),
None => Ok(Vec::new()),
}
}
fn reset(&mut self) -> Result<()> {
self.held = None;
Ok(())
}
fn set_output_wanted(&mut self, wanted: bool) {
self.output_wanted = wanted;
}
}
#[test]
fn a_frame_still_inside_a_reordering_decoder_when_output_stops_is_written_off_too() {
let mut reader = ExactFrameReader::new(
&LateFactory,
config(),
single_group_of_pictures(),
small_cache(),
)
.unwrap();
let cancellation = CancellationToken::new();
assert_eq!(
value(&reader.get(FrameIndex(4), &cancellation).unwrap()),
40
);
assert_eq!(
value(&reader.get(FrameIndex(9), &cancellation).unwrap()),
90
);
assert_eq!(
value(&reader.get(FrameIndex(5), &cancellation).unwrap()),
50,
"the frame that was dropped in flight is decoded again, not reported missing"
);
}
#[test]
fn a_decoder_that_ignores_the_hint_still_answers_every_frame() {
let factory = uncompressed_video_decoder_factory();
let mut reader = ExactFrameReader::new(
&factory,
config(),
single_group_of_pictures(),
small_cache(),
)
.unwrap();
let cancellation = CancellationToken::new();
reader.get(FrameIndex(8), &cancellation).unwrap();
assert_eq!(reader.statistics().samples_skipped, 6);
for index in 0..9_u64 {
assert_eq!(
value(&reader.get(FrameIndex(index), &cancellation).unwrap()),
(index * 10) as u8,
"frame {index} came back from the decoder that ignored the hint"
);
}
}
#[test]
fn malformed_packets_and_cancellation_are_reported() {
let factory = uncompressed_video_decoder_factory();
let mut reader = ExactFrameReader::new(
&factory,
config(),
vec![EncodedVideoSample {
presentation_index: FrameIndex(0),
random_access: true,
data: Vec::new(),
}],
Limits::default(),
)
.unwrap();
assert_eq!(
reader
.get(FrameIndex(0), &CancellationToken::new())
.unwrap_err()
.kind(),
ErrorKind::MalformedMedia
);
let cancelled = CancellationToken::new();
cancelled.cancel();
assert_eq!(
reader.get(FrameIndex(0), &cancelled).unwrap_err().kind(),
ErrorKind::Cancelled
);
}
#[test]
fn random_access_decode_work_obeys_the_configured_bound() {
let factory = uncompressed_video_decoder_factory();
let mut reader = ExactFrameReader::new(
&factory,
config(),
vec![sample(0, 1, true), sample(1, 2, false), sample(2, 3, false)],
Limits {
max_decode_samples_per_seek: 2,
..Limits::default()
},
)
.unwrap();
let error = reader
.get(FrameIndex(2), &CancellationToken::new())
.unwrap_err();
assert_eq!(error.kind(), ErrorKind::ResourceLimit);
assert_eq!(reader.statistics().samples_submitted, 2);
}
struct DelayedFactory;
impl VideoDecoderFactory for DelayedFactory {
fn capability(&self, _: &VideoDecoderConfig) -> CodecSupport {
CodecSupport::Supported {
implementation: CodecImplementation::Software,
}
}
fn create(
&self,
configuration: &VideoDecoderConfig,
limits: &Limits,
) -> Result<Box<dyn VideoDecoder>> {
Ok(Box::new(DelayedDecoder {
configuration: configuration.clone(),
limits: *limits,
delayed: None,
}))
}
}
struct DelayedDecoder {
configuration: VideoDecoderConfig,
limits: Limits,
delayed: Option<EncodedVideoSample>,
}
impl DelayedDecoder {
fn decode(&self, sample: EncodedVideoSample) -> Result<DecodedVideoFrame> {
Ok(DecodedVideoFrame {
presentation_index: sample.presentation_index,
frame: VideoFrame::new(
self.configuration.coded_dimensions,
PixelFormat::Gray8,
ColorRange::Full,
vec![Plane {
data: sample.data,
stride: 1,
}],
&self.limits,
)?,
})
}
}
impl VideoDecoder for DelayedDecoder {
fn submit(
&mut self,
sample: &EncodedVideoSample,
cancellation: &CancellationToken,
) -> Result<Vec<DecodedVideoFrame>> {
cancellation.check()?;
let previous = self.delayed.replace(sample.clone());
previous
.map(|sample| self.decode(sample).map(|frame| vec![frame]))
.unwrap_or(Ok(Vec::new()))
}
fn drain(&mut self, cancellation: &CancellationToken) -> Result<Vec<DecodedVideoFrame>> {
cancellation.check()?;
self.delayed
.take()
.map(|sample| self.decode(sample).map(|frame| vec![frame]))
.unwrap_or(Ok(Vec::new()))
}
fn reset(&mut self) -> Result<()> {
self.delayed = None;
Ok(())
}
}
#[test]
fn delayed_output_drains_and_reset_clears_state() {
let mut reader = ExactFrameReader::new(
&DelayedFactory,
config(),
vec![sample(0, 11, true), sample(1, 22, false)],
Limits::default(),
)
.unwrap();
let cancellation = CancellationToken::new();
assert_eq!(
value(&reader.get(FrameIndex(1), &cancellation).unwrap()),
22
);
assert_eq!(reader.statistics().drains, 1);
reader.reset().unwrap();
assert_eq!(reader.cached_frames(), 0);
assert_eq!(
value(&reader.get(FrameIndex(0), &cancellation).unwrap()),
11
);
}
#[test]
fn sequential_playback_through_a_pipelined_decoder_does_not_reset_every_frame() {
let mut reader = ExactFrameReader::new(
&DelayedFactory,
config(),
vec![
sample(0, 11, true),
sample(1, 22, false),
sample(2, 33, false),
sample(3, 44, false),
sample(4, 55, false),
],
Limits::default(),
)
.unwrap();
let cancellation = CancellationToken::new();
assert_eq!(
value(&reader.get(FrameIndex(0), &cancellation).unwrap()),
11
);
assert_eq!(reader.statistics().resets, 1);
for (index, expected) in [(1, 22_u8), (2, 33), (3, 44), (4, 55)] {
let frame = reader.get(FrameIndex(index), &cancellation).unwrap();
assert_eq!(value(&frame), expected);
assert_eq!(
reader.statistics().resets,
1,
"sequential forward playback must not reset once decoding has started"
);
}
assert_eq!(reader.statistics().samples_submitted, 5);
assert_eq!(reader.statistics().drains, 1);
}
struct MalformedOutputFactory;
impl VideoDecoderFactory for MalformedOutputFactory {
fn capability(&self, _: &VideoDecoderConfig) -> CodecSupport {
CodecSupport::Supported {
implementation: CodecImplementation::Software,
}
}
fn create(
&self,
configuration: &VideoDecoderConfig,
limits: &Limits,
) -> Result<Box<dyn VideoDecoder>> {
let factory = UncompressedVideoDecoderFactory;
factory.create(configuration, limits).map(|decoder| {
Box::new(MalformedOutputDecoder {
decoder,
previous: None,
}) as Box<dyn VideoDecoder>
})
}
}
struct MalformedOutputDecoder {
decoder: Box<dyn VideoDecoder>,
previous: Option<FrameIndex>,
}
impl VideoDecoder for MalformedOutputDecoder {
fn submit(
&mut self,
sample: &EncodedVideoSample,
cancellation: &CancellationToken,
) -> Result<Vec<DecodedVideoFrame>> {
let mut output = self.decoder.submit(sample, cancellation)?;
if let (Some(previous), Some(frame)) = (self.previous, output.first_mut()) {
frame.presentation_index = previous;
}
self.previous = Some(sample.presentation_index);
Ok(output)
}
fn drain(&mut self, cancellation: &CancellationToken) -> Result<Vec<DecodedVideoFrame>> {
self.decoder.drain(cancellation)
}
fn reset(&mut self) -> Result<()> {
self.previous = None;
self.decoder.reset()
}
}
#[test]
fn malformed_backend_output_is_rejected() {
let mut reader = ExactFrameReader::new(
&MalformedOutputFactory,
config(),
vec![sample(0, 1, true), sample(1, 2, false)],
Limits::default(),
)
.unwrap();
let error = reader
.get(FrameIndex(1), &CancellationToken::new())
.unwrap_err();
assert_eq!(error.kind(), ErrorKind::MalformedMedia);
}
}