use crate::convert::adapter_error;
use alpha2_media::{CancelToken, DecodeError};
use rsmpeg::{error::RsmpegError, ffi};
use std::time::{Duration, Instant};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum DecodePolicy {
#[default]
Strict,
Tolerant,
}
impl DecodePolicy {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Strict => "strict",
Self::Tolerant => "tolerant",
}
}
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
[Self::Strict, Self::Tolerant]
.into_iter()
.find(|policy| policy.as_str() == value)
}
pub fn from_env() -> Result<Self, String> {
match std::env::var("ALPHA2_DECODE_POLICY") {
Err(std::env::VarError::NotPresent) => Some(Self::Strict),
Ok(value) => Self::parse(&value),
Err(std::env::VarError::NotUnicode(_)) => None,
}
.ok_or_else(|| "ALPHA2_DECODE_POLICY must be strict or tolerant".to_owned())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct DecodeDiagnosticCounts {
pub corrupt_packets: u64,
pub video_gaps: u64,
pub concealed_audio_samples: u64,
pub timestamp_gap_samples: u64,
pub dropped_audio_samples: u64,
}
impl DecodeDiagnosticCounts {
pub fn record(&mut self, diagnostic: &DecodeDiagnostic) {
let samples = |start: i64, end: i64| u64::try_from(end.saturating_sub(start)).unwrap_or(0);
match diagnostic {
DecodeDiagnostic::Corruption(_) => {
self.corrupt_packets = self.corrupt_packets.saturating_add(1);
}
DecodeDiagnostic::VideoGap(_) => self.video_gaps = self.video_gaps.saturating_add(1),
DecodeDiagnostic::AudioGap(gap) => {
let count = samples(gap.start_sample, gap.end_sample);
let total = if gap.reason == "timestamp gap" {
&mut self.timestamp_gap_samples
} else {
&mut self.concealed_audio_samples
};
*total = total.saturating_add(count);
}
DecodeDiagnostic::AudioOverlap(overlap) => {
self.dropped_audio_samples = self
.dropped_audio_samples
.saturating_add(samples(overlap.start_sample, overlap.end_sample));
}
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
*self == Self::default()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CorruptPacket {
pub stage: &'static str,
pub pts: Option<i64>,
pub time_base: (i32, i32),
}
const MAX_AUDIO_RECORDS: usize = 1024;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConcealedAudioGap {
pub start_sample: i64,
pub end_sample: i64,
pub sample_rate: u32,
pub reason: &'static str,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DroppedAudioOverlap {
pub start_sample: i64,
pub end_sample: i64,
pub sample_rate: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VideoRecoveryGap {
pub previous_pts: Option<i64>,
pub recovered_pts: Option<i64>,
pub time_base: (i32, i32),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct DecodeDiagnostics {
pub corrupt_packets: Vec<CorruptPacket>,
pub audio_gaps: Vec<ConcealedAudioGap>,
pub audio_overlaps: Vec<DroppedAudioOverlap>,
pub video_gaps: Vec<VideoRecoveryGap>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DecodeDiagnostic {
Corruption(CorruptPacket),
AudioGap(ConcealedAudioGap),
AudioOverlap(DroppedAudioOverlap),
VideoGap(VideoRecoveryGap),
}
#[derive(Default)]
pub(crate) struct Recovery {
pub policy: DecodePolicy,
pub reporter: Option<Box<dyn Fn(DecodeDiagnostic) + Send + Sync>>,
pub diagnostics: DecodeDiagnostics,
consecutive: usize,
last_pts: Option<i64>,
stalled: usize,
video_damage: bool,
time_base: (i32, i32),
healthy_frames: usize,
hardware: bool,
}
impl Recovery {
pub fn new(time_base: rsmpeg::avutil::AVRational) -> Self {
Self {
time_base: (time_base.num, time_base.den),
..Self::default()
}
}
#[cfg_attr(not(feature = "hwdecode"), allow(dead_code))]
pub fn hardware(time_base: rsmpeg::avutil::AVRational) -> Self {
Self {
hardware: true,
..Self::new(time_base)
}
}
pub fn finish_video_gap(&mut self, recovered_pts: Option<i64>) {
if !self.video_damage {
return;
}
self.video_damage = false;
let gap = VideoRecoveryGap {
previous_pts: self.last_pts,
recovered_pts,
time_base: self.time_base,
};
self.diagnostics.video_gaps.push(gap.clone());
if let Some(report) = &self.reporter {
report(DecodeDiagnostic::VideoGap(gap));
}
}
pub fn error(
&mut self,
error: RsmpegError,
stage: &'static str,
pts: Option<i64>,
) -> Result<(), DecodeError> {
let damaged = match error.raw_error() {
Some(ffi::AVERROR_INVALIDDATA) => true,
Some(ffi::AVERROR_UNKNOWN | ffi::AVERROR_EXTERNAL) => {
self.hardware && self.policy == DecodePolicy::Tolerant
}
_ => false,
};
if !damaged {
return Err(adapter_error(error));
}
self.corrupt(stage, pts)
}
pub fn corrupt(&mut self, stage: &'static str, pts: Option<i64>) -> Result<(), DecodeError> {
if self.diagnostics.corrupt_packets.len() >= 1024 {
return Err(DecodeError::Adapter(
"corruption diagnostic budget exceeded".into(),
));
}
self.diagnostics.corrupt_packets.push(CorruptPacket {
stage,
pts: pts.filter(|pts| *pts != ffi::AV_NOPTS_VALUE),
time_base: self.time_base,
});
if let Some(report) = &self.reporter {
report(DecodeDiagnostic::Corruption(
self.diagnostics.corrupt_packets.last().unwrap().clone(),
));
}
self.video_damage |= stage.starts_with("video");
self.consecutive += 1;
if self.policy == DecodePolicy::Strict {
return Err(DecodeError::Adapter(format!(
"corrupt input at {stage} (strict decode policy)"
)));
}
if self.consecutive > 32 {
return Err(DecodeError::Adapter(
"consecutive corruption budget exceeded".into(),
));
}
Ok(())
}
pub fn video_eof(&mut self) -> Result<(), DecodeError> {
self.finish_video_gap(None);
if self.consecutive != 0 {
self.damaged_tail("video")?;
}
Ok(())
}
pub fn damaged_tail(&self, kind: &str) -> Result<(), DecodeError> {
if self.healthy_frames == 0 {
return Err(DecodeError::Adapter(format!(
"damaged {kind} has no recoverable frames"
)));
}
Ok(())
}
pub fn progress(&mut self, pts: i64) -> Result<(), DecodeError> {
self.healthy_frames = self.healthy_frames.saturating_add(1);
self.finish_video_gap((pts != ffi::AV_NOPTS_VALUE).then_some(pts));
self.consecutive = 0;
if pts != ffi::AV_NOPTS_VALUE && self.last_pts.is_none_or(|previous| pts > previous) {
self.last_pts = Some(pts);
self.stalled = 0;
} else {
self.stalled += 1;
}
if self.stalled > 256 {
return Err(DecodeError::Adapter(
"decoder PTS progress budget exceeded".into(),
));
}
Ok(())
}
pub fn reset_progress(&mut self) {
self.finish_video_gap(None);
self.last_pts = None;
self.healthy_frames = 0;
self.stalled = 0;
self.consecutive = 0;
}
pub fn audio_gap(
&mut self,
start_sample: i64,
end_sample: i64,
sample_rate: u32,
reason: &'static str,
) -> Result<(), DecodeError> {
if self.diagnostics.audio_gaps.len() >= MAX_AUDIO_RECORDS {
return Err(DecodeError::Adapter(
"audio gap diagnostic budget exceeded".into(),
));
}
self.timestamp_audio_gap(start_sample, end_sample, sample_rate, reason);
Ok(())
}
pub fn timestamp_gap(&mut self, start_sample: i64, end_sample: i64, sample_rate: u32) {
self.timestamp_audio_gap(start_sample, end_sample, sample_rate, "timestamp gap");
}
fn timestamp_audio_gap(
&mut self,
start_sample: i64,
end_sample: i64,
sample_rate: u32,
reason: &'static str,
) {
let gap = ConcealedAudioGap {
start_sample,
end_sample,
sample_rate,
reason,
};
if let Some(report) = &self.reporter {
report(DecodeDiagnostic::AudioGap(gap.clone()));
}
if self.diagnostics.audio_gaps.len() < MAX_AUDIO_RECORDS {
self.diagnostics.audio_gaps.push(gap);
}
}
pub fn timestamp_overlap(&mut self, start_sample: i64, end_sample: i64, sample_rate: u32) {
let overlap = DroppedAudioOverlap {
start_sample,
end_sample,
sample_rate,
};
if let Some(report) = &self.reporter {
report(DecodeDiagnostic::AudioOverlap(overlap.clone()));
}
if self.diagnostics.audio_overlaps.len() < MAX_AUDIO_RECORDS {
self.diagnostics.audio_overlaps.push(overlap);
}
}
}
pub const DECODE_WORK_LIMIT: Duration = Duration::from_secs(10);
const DECODE_WORK_STEPS: usize = 65_536;
thread_local! {
static DECODE_PROGRESS_WATCH: std::cell::RefCell<Option<Box<dyn Fn()>>> =
const { std::cell::RefCell::new(None) };
}
#[cfg(test)]
thread_local! {
pub(crate) static WORK_LIMIT: std::cell::Cell<Duration> =
std::cell::Cell::new(test_work_limit(std::env::var("ALPHA2_TEST_DECODE_WORK_LIMIT_MS").ok().as_deref()));
pub(crate) static WORK_STEPS: std::cell::Cell<usize> =
const { std::cell::Cell::new(DECODE_WORK_STEPS) };
pub(crate) static FRAME_DELAY: std::cell::Cell<Duration> =
const { std::cell::Cell::new(Duration::ZERO) };
pub(crate) static VIRTUAL_CLOCK: std::cell::Cell<Option<Duration>> =
const { std::cell::Cell::new(None) };
}
#[cfg(test)]
fn test_work_limit(setting: Option<&str>) -> Duration {
setting
.and_then(|value| value.parse::<u64>().ok())
.filter(|millis| *millis > 0)
.map_or(DECODE_WORK_LIMIT, Duration::from_millis)
}
#[cfg(test)]
pub(crate) fn take_frame_delay() {
let delay = FRAME_DELAY.with(std::cell::Cell::get);
match VIRTUAL_CLOCK.with(std::cell::Cell::get) {
Some(now) => VIRTUAL_CLOCK.with(|clock| clock.set(Some(now.saturating_add(delay)))),
None => std::thread::sleep(delay),
}
}
#[must_use = "the watch ends when the guard drops"]
pub fn watch_decode_progress(progressed: impl Fn() + 'static) -> DecodeProgressWatch {
let previous = DECODE_PROGRESS_WATCH.with(|watch| watch.replace(Some(Box::new(progressed))));
DecodeProgressWatch {
previous,
thread: std::marker::PhantomData,
}
}
pub(crate) fn report_decode_progress() {
DECODE_PROGRESS_WATCH.with(|watch| {
if let Some(progressed) = watch.borrow().as_ref() {
progressed();
}
});
}
pub struct DecodeProgressWatch {
previous: Option<Box<dyn Fn()>>,
thread: std::marker::PhantomData<*const ()>,
}
impl Drop for DecodeProgressWatch {
fn drop(&mut self) {
let previous = self.previous.take();
DECODE_PROGRESS_WATCH.with(|watch| {
watch.replace(previous);
});
}
}
pub(crate) struct WorkBudget {
started: Instant,
#[cfg(test)]
started_virtual: Option<Duration>,
steps: usize,
}
impl WorkBudget {
pub fn new() -> Self {
Self {
started: Instant::now(),
#[cfg(test)]
started_virtual: VIRTUAL_CLOCK.with(std::cell::Cell::get),
steps: 0,
}
}
fn elapsed(&self) -> Duration {
#[cfg(test)]
if let (Some(now), Some(started)) = (
VIRTUAL_CLOCK.with(std::cell::Cell::get),
self.started_virtual,
) {
return now.saturating_sub(started);
}
self.started.elapsed()
}
pub fn step(&mut self, cancel: &CancelToken) -> Result<(), DecodeError> {
if cancel.is_cancelled() {
return Err(DecodeError::Cancelled);
}
self.steps += 1;
#[cfg(test)]
let (limit, steps) = (
WORK_LIMIT.with(std::cell::Cell::get),
WORK_STEPS.with(std::cell::Cell::get),
);
#[cfg(not(test))]
let (limit, steps) = (DECODE_WORK_LIMIT, DECODE_WORK_STEPS);
if self.steps > steps || self.elapsed() > limit {
return Err(DecodeError::Adapter(
"decode progress budget exceeded".into(),
));
}
Ok(())
}
pub fn progressed(&mut self) {
self.started = Instant::now();
#[cfg(test)]
{
self.started_virtual = VIRTUAL_CLOCK.with(std::cell::Cell::get);
}
self.steps = 0;
report_decode_progress();
}
}
pub(crate) fn submit_packet(
mut step: impl FnMut(bool) -> Result<bool, DecodeError>,
budget: &mut WorkBudget,
cancel: &CancelToken,
) -> Result<(), DecodeError> {
for _ in 0..64 {
budget.step(cancel)?;
if step(false)? {
return Ok(());
}
budget.step(cancel)?;
if !step(true)? {
return Err(DecodeError::Adapter(
"decoder send/receive made no progress".into(),
));
}
}
Err(DecodeError::Adapter("packet retry budget exceeded".into()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_policy_is_named_as_the_host_names_it() {
for policy in [DecodePolicy::Strict, DecodePolicy::Tolerant] {
assert_eq!(DecodePolicy::parse(policy.as_str()), Some(policy));
}
assert_eq!(DecodePolicy::Tolerant.as_str(), "tolerant");
assert_eq!(DecodePolicy::parse("Tolerant"), None);
assert_eq!(DecodePolicy::parse(""), None);
}
#[test]
fn counts_tell_concealed_damage_from_a_timestamp_gap() {
let mut counts = DecodeDiagnosticCounts::default();
assert!(counts.is_empty());
let gap = |start_sample, end_sample, reason| {
DecodeDiagnostic::AudioGap(ConcealedAudioGap {
start_sample,
end_sample,
sample_rate: 48_000,
reason,
})
};
for diagnostic in [
DecodeDiagnostic::Corruption(CorruptPacket {
stage: "audio send",
pts: None,
time_base: (1, 48_000),
}),
gap(1024, 2048, "corrupt input"),
gap(4096, 4196, "timestamp gap"),
DecodeDiagnostic::AudioOverlap(DroppedAudioOverlap {
start_sample: 10,
end_sample: 15,
sample_rate: 48_000,
}),
DecodeDiagnostic::VideoGap(VideoRecoveryGap {
previous_pts: Some(1),
recovered_pts: None,
time_base: (1, 10),
}),
] {
counts.record(&diagnostic);
}
assert_eq!(
counts,
DecodeDiagnosticCounts {
corrupt_packets: 1,
video_gaps: 1,
concealed_audio_samples: 1024,
timestamp_gap_samples: 100,
dropped_audio_samples: 5,
}
);
assert!(!counts.is_empty());
}
#[test]
fn a_hardware_sessions_picture_failure_is_damage_only_when_tolerant() {
let base = rsmpeg::avutil::ra(1, 30);
for code in [ffi::AVERROR_UNKNOWN, ffi::AVERROR_EXTERNAL] {
let mut tolerant = Recovery::hardware(base);
tolerant.policy = DecodePolicy::Tolerant;
assert!(
tolerant
.error(RsmpegError::AVError(code), "video send", Some(3))
.is_ok()
);
assert_eq!(tolerant.diagnostics.corrupt_packets.len(), 1);
let mut strict = Recovery::hardware(base);
assert!(
strict
.error(RsmpegError::AVError(code), "video send", None)
.is_err()
);
let mut software = Recovery::new(base);
software.policy = DecodePolicy::Tolerant;
assert!(
software
.error(RsmpegError::AVError(code), "video send", None)
.is_err()
);
}
}
#[test]
fn strict_tolerant_and_fatal_errors() {
let mut state = Recovery::default();
assert!(state.corrupt("send", Some(12)).is_err());
state.policy = DecodePolicy::Tolerant;
state.reset_progress();
assert!(
state
.error(
RsmpegError::AVError(ffi::AVERROR_INVALIDDATA),
"send",
Some(13)
)
.is_ok()
);
assert!(
state
.error(RsmpegError::AVError(-12), "send", None)
.is_err()
);
assert_eq!(state.diagnostics.corrupt_packets.len(), 2);
for _ in 0..31 {
state.corrupt("receive", None).unwrap();
}
assert!(state.corrupt("receive", None).is_err());
}
#[test]
fn progress_and_cancellation_are_bounded() {
let mut state = Recovery::default();
state.progress(10).unwrap();
for _ in 0..256 {
state.progress(10).unwrap();
}
assert!(state.progress(10).is_err());
state.reset_progress();
state.progress(0).unwrap();
let cancel = CancelToken::default();
let mut budget = WorkBudget::new();
for _ in 0..65_536 {
budget.step(&cancel).unwrap();
}
assert!(budget.step(&cancel).is_err());
WORK_LIMIT.with(|limit| limit.set(DECODE_WORK_LIMIT));
let mut budget = WorkBudget::new();
budget.started -= Duration::from_secs(11);
assert!(budget.step(&cancel).is_err());
cancel.cancel();
assert_eq!(WorkBudget::new().step(&cancel), Err(DecodeError::Cancelled));
}
#[test]
fn eagain_drains_and_retries_the_same_packet_until_accepted() {
let mut actions = Vec::new();
let mut sends = 0;
submit_packet(
|drain| {
actions.push(drain);
if drain {
return Ok(true);
}
sends += 1;
Ok(sends == 3)
},
&mut WorkBudget::new(),
&CancelToken::default(),
)
.unwrap();
assert_eq!(actions, [false, true, false, true, false]);
assert!(
submit_packet(
|_| Ok(false),
&mut WorkBudget::new(),
&CancelToken::default()
)
.is_err()
);
assert!(submit_packet(Ok, &mut WorkBudget::new(), &CancelToken::default()).is_err());
let cancel = CancelToken::default();
let result = submit_packet(
|drain| {
if drain {
cancel.cancel();
}
Ok(drain)
},
&mut WorkBudget::new(),
&cancel,
);
assert_eq!(result, Err(DecodeError::Cancelled));
}
}