use std::{collections::VecDeque, path::Path, time::Duration};
use crate::{Engine, Error, Result};
use super::{VideoContainerInfo, VideoContainerKind, VideoDecoder, VideoDemuxer, VideoFrame};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct VideoPlayerConfig {
pub loop_playback: bool,
pub frame_rate_override: Option<f64>,
pub start_playing: bool,
pub presentation_cache_frames: usize,
pub presentation_cache_bytes: usize,
}
impl Default for VideoPlayerConfig {
fn default() -> Self {
Self {
loop_playback: true,
frame_rate_override: None,
start_playing: true,
presentation_cache_frames: 32,
presentation_cache_bytes: 256 * 1024 * 1024,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct VideoPlayerStats {
pub presented_frames: u64,
pub decoded_packets: u64,
pub seek_resets: u64,
pub loop_restarts: u64,
pub seek_replay_frames: u64,
pub presentation_cache_hits: u64,
pub presentation_cache_misses: u64,
pub presentation_cache_resident: usize,
pub presentation_cache_capacity: usize,
}
pub struct VideoPlayer {
state: Option<PlayerState>,
}
struct PlayerState {
demuxer: VideoDemuxer,
decoder: VideoDecoder,
output: PlayerOutput,
info: VideoContainerInfo,
config: VideoPlayerConfig,
current: Option<VideoFrame>,
drained: VecDeque<VideoFrame>,
presentation_cache: VecDeque<CachedPresentation>,
presentation_cache_bytes: usize,
frame_interval: Duration,
accumulator: Duration,
playing: bool,
done: bool,
display_index: u64,
next_display_index: u64,
display_timestamps: Vec<DisplayTimestamp>,
indexed: bool,
stats: VideoPlayerStats,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum PlayerOutput {
HostPlanar,
Native,
}
struct CachedPresentation {
index: u64,
frame: VideoFrame,
bytes: usize,
}
#[derive(Clone, Copy)]
struct DisplayTimestamp {
ticks: u64,
duration: Duration,
}
impl VideoPlayer {
pub fn open(engine: &Engine, path: impl AsRef<Path>, config: VideoPlayerConfig) -> Result<Self> {
Self::open_with_output(engine, path, config, PlayerOutput::HostPlanar)
}
pub fn open_native(
engine: &Engine,
path: impl AsRef<Path>,
config: VideoPlayerConfig,
) -> Result<Self> {
Self::open_with_output(engine, path, config, PlayerOutput::Native)
}
fn open_with_output(
engine: &Engine,
path: impl AsRef<Path>,
config: VideoPlayerConfig,
output: PlayerOutput,
) -> Result<Self> {
if config
.frame_rate_override
.is_some_and(|rate| !rate.is_finite() || rate <= 0.0)
{
return Err(Error::invalid_argument(
"video player frame-rate override must be finite and positive",
));
}
let demuxer = VideoDemuxer::open(path)?;
let info = demuxer.info();
let indexed = info.kind() != VideoContainerKind::MpegTs;
let timestamp_ticks = if indexed {
demuxer.presentation_timestamps()?
} else {
Vec::new()
};
let mut display_timestamps = Vec::new();
display_timestamps
.try_reserve_exact(timestamp_ticks.len())
.map_err(|_| Error::resource_exhausted("video display index allocation failed"))?;
for ticks in timestamp_ticks {
display_timestamps.push(DisplayTimestamp {
ticks,
duration: ticks_to_duration(ticks, info)?,
});
}
let decoder = VideoDecoder::create(engine, info)?;
let rate = config
.frame_rate_override
.unwrap_or_else(|| info.frame_rate());
let rate = if rate.is_finite() && rate > 0.0 {
rate
} else {
30.0
};
let frame_interval = Duration::from_secs_f64(1.0 / rate);
let presentation_cache_capacity = if output == PlayerOutput::Native || !indexed {
0
} else {
let frame_bytes = usize::try_from(info.width())
.ok()
.and_then(|width| {
usize::try_from(info.height())
.ok()
.and_then(|height| width.checked_mul(height))
})
.and_then(|luma| luma.checked_mul(3))
.and_then(|samples| samples.checked_div(2))
.ok_or_else(|| Error::out_of_range("video player frame size overflows usize"))?;
config.presentation_cache_frames.min(
config
.presentation_cache_bytes
.checked_div(frame_bytes)
.unwrap_or(0),
)
};
let mut player = Self {
state: Some(PlayerState {
demuxer,
decoder,
output,
info,
config,
current: None,
drained: VecDeque::new(),
presentation_cache: VecDeque::new(),
presentation_cache_bytes: 0,
frame_interval,
accumulator: Duration::ZERO,
playing: config.start_playing,
done: false,
display_index: 0,
next_display_index: 0,
display_timestamps,
indexed,
stats: VideoPlayerStats {
presentation_cache_capacity,
..VideoPlayerStats::default()
},
}),
};
if !player.advance_one()? {
return Err(Error::data_loss(
"video source contains no displayable frame",
));
}
Ok(player)
}
pub fn advance(&mut self) -> Result<bool> {
self.advance_one()
}
pub fn step_backward(&mut self) -> Result<()> {
self.step_frames(-1)
}
pub fn step_frames(&mut self, delta: i32) -> Result<()> {
if delta >= 0 {
for _ in 0..delta.unsigned_abs() {
if !self.advance_one()? {
break;
}
}
return Ok(());
}
let current = self.state_ref()?.display_index;
let target = current.saturating_sub(u64::from(delta.unsigned_abs()));
if target == current {
return Ok(());
}
if !self.state_ref()?.indexed {
return Err(Error::missing_capability(
"backward stepping requires an indexed video source",
));
}
if self.present_cached(target)? {
return Ok(());
}
self.record_cache_miss()?;
self.replay_to_index(target)
}
pub fn tick(&mut self, elapsed: Duration) -> Result<u32> {
let state = self.state_mut()?;
if !state.playing || state.done {
return Ok(0);
}
state.accumulator = state
.accumulator
.checked_add(elapsed)
.ok_or_else(|| Error::out_of_range("video player clock accumulator overflowed"))?;
let mut advanced = 0_u32;
loop {
let state = self.state_mut()?;
if state.accumulator < state.frame_interval {
break;
}
state.accumulator -= state.frame_interval;
if !self.advance_one()? {
break;
}
advanced = advanced
.checked_add(1)
.ok_or_else(|| Error::resource_exhausted("video tick frame count overflowed"))?;
}
Ok(advanced)
}
pub fn seek(&mut self, timestamp: u64) -> Result<()> {
if !self.state_ref()?.indexed {
if timestamp != 0 {
return Err(Error::missing_capability(
"unindexed video sources support rewind to zero only",
));
}
let state = self.state_mut()?;
state.decoder.flush()?;
state.demuxer.seek(0)?;
state.drained.clear();
Self::clear_presentation_cache(state);
state.current = None;
state.accumulator = Duration::ZERO;
state.done = false;
state.display_index = 0;
state.next_display_index = 0;
state.stats.seek_resets = state
.stats
.seek_resets
.checked_add(1)
.ok_or_else(|| Error::resource_exhausted("video seek counter overflowed"))?;
if !self.advance_decoded_one()? {
return Err(Error::data_loss(
"video source contains no displayable frame",
));
}
return Ok(());
}
let target = {
let state = self.state_ref()?;
if timestamp >= state.info.duration() && state.info.duration() != 0 {
return Err(Error::out_of_range(
"video player seek timestamp is outside the stream duration",
));
}
state
.display_timestamps
.partition_point(|point| point.ticks < timestamp)
};
if target == self.state_ref()?.display_timestamps.len() {
return Err(Error::out_of_range(
"video seek found no display frame at the requested timestamp",
));
}
self.rebuild_to_index(target, false)
}
pub fn seek_at_or_before(&mut self, timestamp: u64) -> Result<()> {
let state = self.state_ref()?;
if !state.indexed {
return if timestamp == 0 {
self.seek(0)
} else {
Err(Error::missing_capability(
"timestamp seeking requires an indexed video source",
))
};
}
let target = display_index_at_or_before(&state.display_timestamps, timestamp)
.ok_or_else(|| Error::data_loss("video source has no indexed display frames"))?;
self.seek_frame(
u64::try_from(target).map_err(|_| Error::out_of_range("video display index exceeds u64"))?,
)
}
pub fn seek_frame(&mut self, index: u64) -> Result<()> {
if !self.state_ref()?.indexed {
return Err(Error::missing_capability(
"frame seeking requires an indexed video source",
));
}
let target =
usize::try_from(index).map_err(|_| Error::out_of_range("video frame index exceeds usize"))?;
if target >= self.state_ref()?.display_timestamps.len() {
return Err(Error::out_of_range(
"video frame index is outside the display sequence",
));
}
if self.state_ref()?.display_index == index || self.present_cached(index)? {
return Ok(());
}
self.rebuild_to_index(target, false)
}
pub fn reset(&mut self) -> Result<()> {
self.seek(0)
}
pub fn flush(&mut self) -> Result<usize> {
let state = self.state_mut()?;
let discarded = state.decoder.flush()?.len() + state.drained.len();
state.drained.clear();
Self::clear_presentation_cache(state);
state.done = true;
state.accumulator = Duration::ZERO;
Ok(discarded)
}
pub fn close(&mut self) {
if let Some(mut state) = self.state.take() {
state.decoder.close();
state.demuxer.close();
}
}
pub fn play(&mut self) -> Result<()> {
self.state_mut()?.playing = true;
Ok(())
}
pub fn pause(&mut self) -> Result<()> {
let state = self.state_mut()?;
state.playing = false;
state.accumulator = Duration::ZERO;
Ok(())
}
pub fn toggle_play(&mut self) -> Result<()> {
let state = self.state_mut()?;
state.playing = !state.playing;
if !state.playing {
state.accumulator = Duration::ZERO;
}
Ok(())
}
pub fn set_looping(&mut self, looping: bool) -> Result<()> {
self.state_mut()?.config.loop_playback = looping;
Ok(())
}
pub fn current_frame(&self) -> Result<&VideoFrame> {
self
.state_ref()?
.current
.as_ref()
.ok_or_else(|| Error::failed_precondition("VideoPlayer has no current frame"))
}
pub fn next_presentation_boundary(&self) -> Result<Option<Duration>> {
let state = self.state_ref()?;
if state.indexed {
let next = state
.display_index
.checked_add(1)
.ok_or_else(|| Error::out_of_range("video display index overflowed"))?;
let next = usize::try_from(next)
.map_err(|_| Error::out_of_range("video display index exceeds usize"))?;
return Ok(
state
.display_timestamps
.get(next)
.map(|point| point.duration),
);
}
let timing = self.current_frame()?.timing();
let duration = timing.duration().ok_or_else(|| {
Error::missing_capability(
"unindexed video requires known frame durations for clocked playback",
)
})?;
let boundary = timing
.presentation_timestamp()
.checked_add(duration)
.ok_or_else(|| Error::out_of_range("video presentation time overflowed"))?;
Ok(Some(boundary))
}
pub fn current_frame_index(&self) -> Result<u64> {
Ok(self.state_ref()?.display_index)
}
pub fn info(&self) -> Option<VideoContainerInfo> {
self.state.as_ref().map(|state| state.info)
}
pub fn stats(&self) -> Option<VideoPlayerStats> {
self.state.as_ref().map(|state| state.stats)
}
pub fn is_playing(&self) -> bool {
self.state.as_ref().is_some_and(|state| state.playing)
}
pub fn is_done(&self) -> bool {
self.state.as_ref().is_none_or(|state| state.done)
}
pub const fn is_open(&self) -> bool {
self.state.is_some()
}
fn advance_one(&mut self) -> Result<bool> {
let cached_target = {
let state = self.state_ref()?;
state
.display_index
.checked_add(1)
.filter(|next| *next < state.next_display_index)
};
if let Some(target) = cached_target {
if self.present_cached(target)? {
return Ok(true);
}
self.record_cache_miss()?;
self.replay_to_index(target)?;
return Ok(true);
}
self.advance_decoded_one()
}
fn advance_decoded_one(&mut self) -> Result<bool> {
loop {
let state = self.state_mut()?;
if let Some(frame) = state.drained.pop_front() {
Self::present_new(state, frame)?;
if state.drained.is_empty() && state.demuxer.is_eos() && !state.config.loop_playback {
state.done = true;
}
return Ok(true);
}
if state.done {
return Ok(false);
}
match state.demuxer.read_next_packet()? {
Some(packet) => {
state.stats.decoded_packets =
state.stats.decoded_packets.checked_add(1).ok_or_else(|| {
Error::resource_exhausted("video decoded-packet counter overflowed")
})?;
let frame = match state.output {
PlayerOutput::HostPlanar => state.decoder.decode(&packet)?,
PlayerOutput::Native => state.decoder.decode_native(&packet)?,
};
if let Some(frame) = frame {
Self::present_new(state, frame)?;
return Ok(true);
}
}
None => {
state.drained.extend(state.decoder.flush()?);
if !state.drained.is_empty() {
continue;
}
if !state.config.loop_playback {
state.done = true;
return Ok(false);
}
state.demuxer.seek(0)?;
state.stats.loop_restarts = state
.stats
.loop_restarts
.checked_add(1)
.ok_or_else(|| Error::resource_exhausted("video loop counter overflowed"))?;
state.stats.seek_resets = state
.stats
.seek_resets
.checked_add(1)
.ok_or_else(|| Error::resource_exhausted("video seek counter overflowed"))?;
state.display_index = 0;
state.next_display_index = 0;
Self::clear_presentation_cache(state);
}
}
}
}
fn present_new(state: &mut PlayerState, frame: VideoFrame) -> Result<()> {
if !state.indexed {
let index = state.next_display_index;
state.next_display_index = index
.checked_add(1)
.ok_or_else(|| Error::resource_exhausted("video display index overflowed"))?;
state.current = Some(frame);
state.display_index = index;
state.stats.presented_frames = state
.stats
.presented_frames
.checked_add(1)
.ok_or_else(|| Error::resource_exhausted("video presented-frame counter overflowed"))?;
return Ok(());
}
let minimum = usize::try_from(state.next_display_index)
.map_err(|_| Error::out_of_range("video display index exceeds usize"))?;
let timestamp = frame.timing().presentation_timestamp();
let remaining = state.display_timestamps.get(minimum..).ok_or_else(|| {
Error::data_loss("video decoder advanced beyond the container display index")
})?;
let relative = remaining.partition_point(|point| point.duration < timestamp);
let resolved = minimum
.checked_add(relative)
.ok_or_else(|| Error::out_of_range("video display index overflowed"))?;
let Some(point) = state.display_timestamps.get(resolved) else {
return Err(Error::data_loss(
"decoded frame timestamp is absent from the container display index",
));
};
if point.duration != timestamp {
return Err(Error::data_loss(
"decoded frame timestamp does not match the container display index",
));
}
let index = u64::try_from(resolved)
.map_err(|_| Error::out_of_range("video display index exceeds u64"))?;
state.current = Some(frame.clone());
state.display_index = index;
state.next_display_index = index
.checked_add(1)
.ok_or_else(|| Error::resource_exhausted("video display index overflowed"))?;
state.stats.presented_frames = state
.stats
.presented_frames
.checked_add(1)
.ok_or_else(|| Error::resource_exhausted("video presented-frame counter overflowed"))?;
Self::retain_presentation(state, index, frame)?;
Ok(())
}
fn present_cached(&mut self, index: u64) -> Result<bool> {
let state = self.state_mut()?;
let Some(frame) = state
.presentation_cache
.iter()
.find(|entry| entry.index == index)
.map(|entry| entry.frame.clone())
else {
return Ok(false);
};
state.current = Some(frame);
state.display_index = index;
state.done = false;
state.stats.presented_frames = state
.stats
.presented_frames
.checked_add(1)
.ok_or_else(|| Error::resource_exhausted("video presented-frame counter overflowed"))?;
state.stats.presentation_cache_hits = state
.stats
.presentation_cache_hits
.checked_add(1)
.ok_or_else(|| Error::resource_exhausted("video cache-hit counter overflowed"))?;
Ok(true)
}
fn record_cache_miss(&mut self) -> Result<()> {
let state = self.state_mut()?;
state.stats.presentation_cache_misses = state
.stats
.presentation_cache_misses
.checked_add(1)
.ok_or_else(|| Error::resource_exhausted("video cache-miss counter overflowed"))?;
Ok(())
}
fn replay_to_index(&mut self, target: u64) -> Result<()> {
let target = usize::try_from(target)
.map_err(|_| Error::out_of_range("video replay index exceeds usize"))?;
self.rebuild_to_index(target, true)
}
fn rebuild_to_index(&mut self, target: usize, replay: bool) -> Result<()> {
let timestamp = {
let state = self.state_ref()?;
let history = state.stats.presentation_cache_capacity.max(1);
let seek_start = target.saturating_add(1).saturating_sub(history);
state
.display_timestamps
.get(seek_start)
.ok_or_else(|| Error::out_of_range("video rebuild index is outside the stream"))?
.ticks
};
{
let state = self.state_mut()?;
state.decoder.flush()?;
state.demuxer.seek(timestamp)?;
state.drained.clear();
Self::clear_presentation_cache(state);
state.current = None;
state.accumulator = Duration::ZERO;
state.done = false;
state.display_index = 0;
state.next_display_index = 0;
state.stats.seek_resets = state
.stats
.seek_resets
.checked_add(1)
.ok_or_else(|| Error::resource_exhausted("video seek counter overflowed"))?;
}
loop {
if !self.advance_decoded_one()? {
return Err(Error::data_loss(
"video rebuild ended before the requested display frame",
));
}
let current = {
let state = self.state_mut()?;
if replay {
state.stats.seek_replay_frames = state
.stats
.seek_replay_frames
.checked_add(1)
.ok_or_else(|| Error::resource_exhausted("video replay counter overflowed"))?;
}
usize::try_from(state.display_index)
.map_err(|_| Error::out_of_range("video display index exceeds usize"))?
};
if current == target {
return Ok(());
}
if current > target {
return Err(Error::data_loss(
"video rebuild skipped the requested display frame",
));
}
}
}
fn retain_presentation(state: &mut PlayerState, index: u64, frame: VideoFrame) -> Result<()> {
let Some(bytes) = frame.as_yuv420p().map(<[u8]>::len) else {
return Ok(());
};
if state.stats.presentation_cache_capacity == 0
|| state.config.presentation_cache_bytes == 0
|| bytes > state.config.presentation_cache_bytes
{
return Ok(());
}
state.presentation_cache_bytes = state
.presentation_cache_bytes
.checked_add(bytes)
.ok_or_else(|| Error::resource_exhausted("video cache byte count overflowed"))?;
state.presentation_cache.push_back(CachedPresentation {
index,
frame,
bytes,
});
while state.presentation_cache.len() > state.stats.presentation_cache_capacity
|| state.presentation_cache_bytes > state.config.presentation_cache_bytes
{
let evicted = state
.presentation_cache
.pop_front()
.ok_or_else(|| Error::internal("video presentation cache accounting diverged"))?;
state.presentation_cache_bytes -= evicted.bytes;
}
state.stats.presentation_cache_resident = state.presentation_cache.len();
Ok(())
}
fn clear_presentation_cache(state: &mut PlayerState) {
state.presentation_cache.clear();
state.presentation_cache_bytes = 0;
state.stats.presentation_cache_resident = 0;
}
fn state_ref(&self) -> Result<&PlayerState> {
self
.state
.as_ref()
.ok_or_else(|| Error::failed_precondition("VideoPlayer is closed"))
}
fn state_mut(&mut self) -> Result<&mut PlayerState> {
self
.state
.as_mut()
.ok_or_else(|| Error::failed_precondition("VideoPlayer is closed"))
}
}
fn ticks_to_duration(ticks: u64, info: VideoContainerInfo) -> Result<Duration> {
let time_base = info.time_base();
let nanos = u128::from(ticks)
.checked_mul(u128::from(time_base.numerator()))
.and_then(|value| value.checked_mul(1_000_000_000))
.ok_or_else(|| Error::out_of_range("video player timestamp overflows nanoseconds"))?
/ u128::from(time_base.denominator());
let seconds = u64::try_from(nanos / 1_000_000_000)
.map_err(|_| Error::out_of_range("video player timestamp exceeds Duration"))?;
let subsec_nanos = u32::try_from(nanos % 1_000_000_000)
.map_err(|_| Error::out_of_range("video player timestamp remainder exceeds u32"))?;
Ok(Duration::new(seconds, subsec_nanos))
}
fn display_index_at_or_before(points: &[DisplayTimestamp], ticks: u64) -> Option<usize> {
let last = points.get(
points
.partition_point(|point| point.ticks <= ticks)
.saturating_sub(1),
)?;
Some(points.partition_point(|point| point.ticks < last.ticks))
}