1use crate::convert::adapter_error;
3use alpha2_media::{CancelToken, DecodeError};
4use rsmpeg::{error::RsmpegError, ffi};
5use std::time::{Duration, Instant};
6
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
9pub enum DecodePolicy {
10 #[default]
12 Strict,
13 Tolerant,
15}
16
17impl DecodePolicy {
18 #[must_use]
21 pub const fn as_str(self) -> &'static str {
22 match self {
23 Self::Strict => "strict",
24 Self::Tolerant => "tolerant",
25 }
26 }
27
28 #[must_use]
30 pub fn parse(value: &str) -> Option<Self> {
31 [Self::Strict, Self::Tolerant]
32 .into_iter()
33 .find(|policy| policy.as_str() == value)
34 }
35
36 pub fn from_env() -> Result<Self, String> {
42 match std::env::var("ALPHA2_DECODE_POLICY") {
43 Err(std::env::VarError::NotPresent) => Some(Self::Strict),
44 Ok(value) => Self::parse(&value),
45 Err(std::env::VarError::NotUnicode(_)) => None,
46 }
47 .ok_or_else(|| "ALPHA2_DECODE_POLICY must be strict or tolerant".to_owned())
48 }
49}
50
51#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
55pub struct DecodeDiagnosticCounts {
56 pub corrupt_packets: u64,
59 pub video_gaps: u64,
61 pub concealed_audio_samples: u64,
63 pub timestamp_gap_samples: u64,
65 pub dropped_audio_samples: u64,
67}
68
69impl DecodeDiagnosticCounts {
70 pub fn record(&mut self, diagnostic: &DecodeDiagnostic) {
72 let samples = |start: i64, end: i64| u64::try_from(end.saturating_sub(start)).unwrap_or(0);
73 match diagnostic {
74 DecodeDiagnostic::Corruption(_) => {
75 self.corrupt_packets = self.corrupt_packets.saturating_add(1);
76 }
77 DecodeDiagnostic::VideoGap(_) => self.video_gaps = self.video_gaps.saturating_add(1),
78 DecodeDiagnostic::AudioGap(gap) => {
79 let count = samples(gap.start_sample, gap.end_sample);
80 let total = if gap.reason == "timestamp gap" {
81 &mut self.timestamp_gap_samples
82 } else {
83 &mut self.concealed_audio_samples
84 };
85 *total = total.saturating_add(count);
86 }
87 DecodeDiagnostic::AudioOverlap(overlap) => {
88 self.dropped_audio_samples = self
89 .dropped_audio_samples
90 .saturating_add(samples(overlap.start_sample, overlap.end_sample));
91 }
92 }
93 }
94
95 #[must_use]
97 pub fn is_empty(&self) -> bool {
98 *self == Self::default()
99 }
100}
101
102#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct CorruptPacket {
105 pub stage: &'static str,
107 pub pts: Option<i64>,
109 pub time_base: (i32, i32),
111}
112
113const MAX_AUDIO_RECORDS: usize = 1024;
115
116#[derive(Clone, Debug, PartialEq, Eq)]
119pub struct ConcealedAudioGap {
120 pub start_sample: i64,
122 pub end_sample: i64,
124 pub sample_rate: u32,
126 pub reason: &'static str,
128}
129
130#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct DroppedAudioOverlap {
134 pub start_sample: i64,
136 pub end_sample: i64,
138 pub sample_rate: u32,
140}
141
142#[derive(Clone, Debug, PartialEq, Eq)]
144pub struct VideoRecoveryGap {
145 pub previous_pts: Option<i64>,
147 pub recovered_pts: Option<i64>,
149 pub time_base: (i32, i32),
151}
152
153#[derive(Clone, Debug, Default, PartialEq, Eq)]
155pub struct DecodeDiagnostics {
156 pub corrupt_packets: Vec<CorruptPacket>,
158 pub audio_gaps: Vec<ConcealedAudioGap>,
160 pub audio_overlaps: Vec<DroppedAudioOverlap>,
162 pub video_gaps: Vec<VideoRecoveryGap>,
164}
165
166#[derive(Clone, Debug, PartialEq, Eq)]
168pub enum DecodeDiagnostic {
169 Corruption(CorruptPacket),
171 AudioGap(ConcealedAudioGap),
173 AudioOverlap(DroppedAudioOverlap),
175 VideoGap(VideoRecoveryGap),
177}
178
179#[derive(Default)]
180pub(crate) struct Recovery {
181 pub policy: DecodePolicy,
182 pub reporter: Option<Box<dyn Fn(DecodeDiagnostic) + Send + Sync>>,
183 pub diagnostics: DecodeDiagnostics,
184 consecutive: usize,
185 last_pts: Option<i64>,
186 stalled: usize,
187 video_damage: bool,
188 time_base: (i32, i32),
189 healthy_frames: usize,
190 hardware: bool,
192}
193impl Recovery {
194 pub fn new(time_base: rsmpeg::avutil::AVRational) -> Self {
195 Self {
196 time_base: (time_base.num, time_base.den),
197 ..Self::default()
198 }
199 }
200 #[cfg_attr(not(feature = "hwdecode"), allow(dead_code))]
203 pub fn hardware(time_base: rsmpeg::avutil::AVRational) -> Self {
204 Self {
205 hardware: true,
206 ..Self::new(time_base)
207 }
208 }
209 pub fn finish_video_gap(&mut self, recovered_pts: Option<i64>) {
210 if !self.video_damage {
211 return;
212 }
213 self.video_damage = false;
214 let gap = VideoRecoveryGap {
215 previous_pts: self.last_pts,
216 recovered_pts,
217 time_base: self.time_base,
218 };
219 self.diagnostics.video_gaps.push(gap.clone());
220 if let Some(report) = &self.reporter {
221 report(DecodeDiagnostic::VideoGap(gap));
222 }
223 }
224
225 pub fn error(
226 &mut self,
227 error: RsmpegError,
228 stage: &'static str,
229 pts: Option<i64>,
230 ) -> Result<(), DecodeError> {
231 let damaged = match error.raw_error() {
232 Some(ffi::AVERROR_INVALIDDATA) => true,
233 Some(ffi::AVERROR_UNKNOWN | ffi::AVERROR_EXTERNAL) => {
238 self.hardware && self.policy == DecodePolicy::Tolerant
239 }
240 _ => false,
241 };
242 if !damaged {
243 return Err(adapter_error(error));
244 }
245 self.corrupt(stage, pts)
246 }
247 pub fn corrupt(&mut self, stage: &'static str, pts: Option<i64>) -> Result<(), DecodeError> {
248 if self.diagnostics.corrupt_packets.len() >= 1024 {
249 return Err(DecodeError::Adapter(
250 "corruption diagnostic budget exceeded".into(),
251 ));
252 }
253 self.diagnostics.corrupt_packets.push(CorruptPacket {
254 stage,
255 pts: pts.filter(|pts| *pts != ffi::AV_NOPTS_VALUE),
256 time_base: self.time_base,
257 });
258 if let Some(report) = &self.reporter {
259 report(DecodeDiagnostic::Corruption(
260 self.diagnostics.corrupt_packets.last().unwrap().clone(),
261 ));
262 }
263 self.video_damage |= stage.starts_with("video");
264 self.consecutive += 1;
265 if self.policy == DecodePolicy::Strict {
266 return Err(DecodeError::Adapter(format!(
267 "corrupt input at {stage} (strict decode policy)"
268 )));
269 }
270 if self.consecutive > 32 {
271 return Err(DecodeError::Adapter(
272 "consecutive corruption budget exceeded".into(),
273 ));
274 }
275 Ok(())
276 }
277 pub fn video_eof(&mut self) -> Result<(), DecodeError> {
278 self.finish_video_gap(None);
279 if self.consecutive != 0 {
280 self.damaged_tail("video")?;
281 }
282 Ok(())
283 }
284 pub fn damaged_tail(&self, kind: &str) -> Result<(), DecodeError> {
288 if self.healthy_frames == 0 {
289 return Err(DecodeError::Adapter(format!(
290 "damaged {kind} has no recoverable frames"
291 )));
292 }
293 Ok(())
294 }
295 pub fn progress(&mut self, pts: i64) -> Result<(), DecodeError> {
296 self.healthy_frames = self.healthy_frames.saturating_add(1);
297 self.finish_video_gap((pts != ffi::AV_NOPTS_VALUE).then_some(pts));
298 self.consecutive = 0;
299 if pts != ffi::AV_NOPTS_VALUE && self.last_pts.is_none_or(|previous| pts > previous) {
300 self.last_pts = Some(pts);
301 self.stalled = 0;
302 } else {
303 self.stalled += 1;
304 }
305 if self.stalled > 256 {
306 return Err(DecodeError::Adapter(
307 "decoder PTS progress budget exceeded".into(),
308 ));
309 }
310 Ok(())
311 }
312 pub fn reset_progress(&mut self) {
313 self.finish_video_gap(None);
314 self.last_pts = None;
315 self.healthy_frames = 0;
316 self.stalled = 0;
317 self.consecutive = 0;
318 }
319 pub fn audio_gap(
320 &mut self,
321 start_sample: i64,
322 end_sample: i64,
323 sample_rate: u32,
324 reason: &'static str,
325 ) -> Result<(), DecodeError> {
326 if self.diagnostics.audio_gaps.len() >= MAX_AUDIO_RECORDS {
327 return Err(DecodeError::Adapter(
328 "audio gap diagnostic budget exceeded".into(),
329 ));
330 }
331 self.timestamp_audio_gap(start_sample, end_sample, sample_rate, reason);
332 Ok(())
333 }
334 pub fn timestamp_gap(&mut self, start_sample: i64, end_sample: i64, sample_rate: u32) {
337 self.timestamp_audio_gap(start_sample, end_sample, sample_rate, "timestamp gap");
338 }
339 fn timestamp_audio_gap(
340 &mut self,
341 start_sample: i64,
342 end_sample: i64,
343 sample_rate: u32,
344 reason: &'static str,
345 ) {
346 let gap = ConcealedAudioGap {
347 start_sample,
348 end_sample,
349 sample_rate,
350 reason,
351 };
352 if let Some(report) = &self.reporter {
353 report(DecodeDiagnostic::AudioGap(gap.clone()));
354 }
355 if self.diagnostics.audio_gaps.len() < MAX_AUDIO_RECORDS {
356 self.diagnostics.audio_gaps.push(gap);
357 }
358 }
359 pub fn timestamp_overlap(&mut self, start_sample: i64, end_sample: i64, sample_rate: u32) {
361 let overlap = DroppedAudioOverlap {
362 start_sample,
363 end_sample,
364 sample_rate,
365 };
366 if let Some(report) = &self.reporter {
367 report(DecodeDiagnostic::AudioOverlap(overlap.clone()));
368 }
369 if self.diagnostics.audio_overlaps.len() < MAX_AUDIO_RECORDS {
370 self.diagnostics.audio_overlaps.push(overlap);
371 }
372 }
373}
374
375pub const DECODE_WORK_LIMIT: Duration = Duration::from_secs(10);
387
388const DECODE_WORK_STEPS: usize = 65_536;
390
391thread_local! {
392 static DECODE_PROGRESS_WATCH: std::cell::RefCell<Option<Box<dyn Fn()>>> =
393 const { std::cell::RefCell::new(None) };
394}
395
396#[cfg(test)]
397thread_local! {
398 pub(crate) static WORK_LIMIT: std::cell::Cell<Duration> =
403 std::cell::Cell::new(test_work_limit(std::env::var("ALPHA2_TEST_DECODE_WORK_LIMIT_MS").ok().as_deref()));
404 pub(crate) static WORK_STEPS: std::cell::Cell<usize> =
406 const { std::cell::Cell::new(DECODE_WORK_STEPS) };
407 pub(crate) static FRAME_DELAY: std::cell::Cell<Duration> =
411 const { std::cell::Cell::new(Duration::ZERO) };
412 pub(crate) static VIRTUAL_CLOCK: std::cell::Cell<Option<Duration>> =
417 const { std::cell::Cell::new(None) };
418}
419
420#[cfg(test)]
423fn test_work_limit(setting: Option<&str>) -> Duration {
424 setting
425 .and_then(|value| value.parse::<u64>().ok())
426 .filter(|millis| *millis > 0)
427 .map_or(DECODE_WORK_LIMIT, Duration::from_millis)
428}
429
430#[cfg(test)]
433pub(crate) fn take_frame_delay() {
434 let delay = FRAME_DELAY.with(std::cell::Cell::get);
435 match VIRTUAL_CLOCK.with(std::cell::Cell::get) {
436 Some(now) => VIRTUAL_CLOCK.with(|clock| clock.set(Some(now.saturating_add(delay)))),
437 None => std::thread::sleep(delay),
438 }
439}
440
441#[must_use = "the watch ends when the guard drops"]
449pub fn watch_decode_progress(progressed: impl Fn() + 'static) -> DecodeProgressWatch {
450 let previous = DECODE_PROGRESS_WATCH.with(|watch| watch.replace(Some(Box::new(progressed))));
451 DecodeProgressWatch {
452 previous,
453 thread: std::marker::PhantomData,
454 }
455}
456
457pub(crate) fn report_decode_progress() {
461 DECODE_PROGRESS_WATCH.with(|watch| {
462 if let Some(progressed) = watch.borrow().as_ref() {
463 progressed();
464 }
465 });
466}
467
468pub struct DecodeProgressWatch {
470 previous: Option<Box<dyn Fn()>>,
471 thread: std::marker::PhantomData<*const ()>,
473}
474
475impl Drop for DecodeProgressWatch {
476 fn drop(&mut self) {
477 let previous = self.previous.take();
478 DECODE_PROGRESS_WATCH.with(|watch| {
479 watch.replace(previous);
480 });
481 }
482}
483
484pub(crate) struct WorkBudget {
488 started: Instant,
489 #[cfg(test)]
491 started_virtual: Option<Duration>,
492 steps: usize,
493}
494impl WorkBudget {
495 pub fn new() -> Self {
496 Self {
497 started: Instant::now(),
498 #[cfg(test)]
499 started_virtual: VIRTUAL_CLOCK.with(std::cell::Cell::get),
500 steps: 0,
501 }
502 }
503
504 fn elapsed(&self) -> Duration {
506 #[cfg(test)]
507 if let (Some(now), Some(started)) = (
508 VIRTUAL_CLOCK.with(std::cell::Cell::get),
509 self.started_virtual,
510 ) {
511 return now.saturating_sub(started);
512 }
513 self.started.elapsed()
514 }
515 pub fn step(&mut self, cancel: &CancelToken) -> Result<(), DecodeError> {
516 if cancel.is_cancelled() {
517 return Err(DecodeError::Cancelled);
518 }
519 self.steps += 1;
520 #[cfg(test)]
521 let (limit, steps) = (
522 WORK_LIMIT.with(std::cell::Cell::get),
523 WORK_STEPS.with(std::cell::Cell::get),
524 );
525 #[cfg(not(test))]
526 let (limit, steps) = (DECODE_WORK_LIMIT, DECODE_WORK_STEPS);
527 if self.steps > steps || self.elapsed() > limit {
528 return Err(DecodeError::Adapter(
529 "decode progress budget exceeded".into(),
530 ));
531 }
532 Ok(())
533 }
534 pub fn progressed(&mut self) {
536 self.started = Instant::now();
537 #[cfg(test)]
538 {
539 self.started_virtual = VIRTUAL_CLOCK.with(std::cell::Cell::get);
540 }
541 self.steps = 0;
542 report_decode_progress();
543 }
544}
545
546pub(crate) fn submit_packet(
549 mut step: impl FnMut(bool) -> Result<bool, DecodeError>,
550 budget: &mut WorkBudget,
551 cancel: &CancelToken,
552) -> Result<(), DecodeError> {
553 for _ in 0..64 {
554 budget.step(cancel)?;
555 if step(false)? {
556 return Ok(());
557 }
558 budget.step(cancel)?;
559 if !step(true)? {
560 return Err(DecodeError::Adapter(
561 "decoder send/receive made no progress".into(),
562 ));
563 }
564 }
565 Err(DecodeError::Adapter("packet retry budget exceeded".into()))
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571
572 #[test]
574 fn a_policy_is_named_as_the_host_names_it() {
575 for policy in [DecodePolicy::Strict, DecodePolicy::Tolerant] {
576 assert_eq!(DecodePolicy::parse(policy.as_str()), Some(policy));
577 }
578 assert_eq!(DecodePolicy::Tolerant.as_str(), "tolerant");
579 assert_eq!(DecodePolicy::parse("Tolerant"), None);
580 assert_eq!(DecodePolicy::parse(""), None);
581 }
582
583 #[test]
585 fn counts_tell_concealed_damage_from_a_timestamp_gap() {
586 let mut counts = DecodeDiagnosticCounts::default();
587 assert!(counts.is_empty());
588 let gap = |start_sample, end_sample, reason| {
589 DecodeDiagnostic::AudioGap(ConcealedAudioGap {
590 start_sample,
591 end_sample,
592 sample_rate: 48_000,
593 reason,
594 })
595 };
596 for diagnostic in [
597 DecodeDiagnostic::Corruption(CorruptPacket {
598 stage: "audio send",
599 pts: None,
600 time_base: (1, 48_000),
601 }),
602 gap(1024, 2048, "corrupt input"),
603 gap(4096, 4196, "timestamp gap"),
604 DecodeDiagnostic::AudioOverlap(DroppedAudioOverlap {
605 start_sample: 10,
606 end_sample: 15,
607 sample_rate: 48_000,
608 }),
609 DecodeDiagnostic::VideoGap(VideoRecoveryGap {
610 previous_pts: Some(1),
611 recovered_pts: None,
612 time_base: (1, 10),
613 }),
614 ] {
615 counts.record(&diagnostic);
616 }
617 assert_eq!(
618 counts,
619 DecodeDiagnosticCounts {
620 corrupt_packets: 1,
621 video_gaps: 1,
622 concealed_audio_samples: 1024,
623 timestamp_gap_samples: 100,
624 dropped_audio_samples: 5,
625 }
626 );
627 assert!(!counts.is_empty());
628 }
629 #[test]
633 fn a_hardware_sessions_picture_failure_is_damage_only_when_tolerant() {
634 let base = rsmpeg::avutil::ra(1, 30);
635 for code in [ffi::AVERROR_UNKNOWN, ffi::AVERROR_EXTERNAL] {
636 let mut tolerant = Recovery::hardware(base);
637 tolerant.policy = DecodePolicy::Tolerant;
638 assert!(
639 tolerant
640 .error(RsmpegError::AVError(code), "video send", Some(3))
641 .is_ok()
642 );
643 assert_eq!(tolerant.diagnostics.corrupt_packets.len(), 1);
644 let mut strict = Recovery::hardware(base);
645 assert!(
646 strict
647 .error(RsmpegError::AVError(code), "video send", None)
648 .is_err()
649 );
650 let mut software = Recovery::new(base);
651 software.policy = DecodePolicy::Tolerant;
652 assert!(
653 software
654 .error(RsmpegError::AVError(code), "video send", None)
655 .is_err()
656 );
657 }
658 }
659
660 #[test]
661 fn strict_tolerant_and_fatal_errors() {
662 let mut state = Recovery::default();
663 assert!(state.corrupt("send", Some(12)).is_err());
664 state.policy = DecodePolicy::Tolerant;
665 state.reset_progress();
666 assert!(
667 state
668 .error(
669 RsmpegError::AVError(ffi::AVERROR_INVALIDDATA),
670 "send",
671 Some(13)
672 )
673 .is_ok()
674 );
675 assert!(
676 state
677 .error(RsmpegError::AVError(-12), "send", None)
678 .is_err()
679 );
680 assert_eq!(state.diagnostics.corrupt_packets.len(), 2);
681 for _ in 0..31 {
682 state.corrupt("receive", None).unwrap();
683 }
684 assert!(state.corrupt("receive", None).is_err());
685 }
686 #[test]
687 fn progress_and_cancellation_are_bounded() {
688 let mut state = Recovery::default();
689 state.progress(10).unwrap();
690 for _ in 0..256 {
691 state.progress(10).unwrap();
692 }
693 assert!(state.progress(10).is_err());
694 state.reset_progress();
695 state.progress(0).unwrap();
696 let cancel = CancelToken::default();
697 let mut budget = WorkBudget::new();
698 for _ in 0..65_536 {
699 budget.step(&cancel).unwrap();
700 }
701 assert!(budget.step(&cancel).is_err());
702 WORK_LIMIT.with(|limit| limit.set(DECODE_WORK_LIMIT));
704 let mut budget = WorkBudget::new();
705 budget.started -= Duration::from_secs(11);
706 assert!(budget.step(&cancel).is_err());
707 cancel.cancel();
708 assert_eq!(WorkBudget::new().step(&cancel), Err(DecodeError::Cancelled));
709 }
710 #[test]
711 fn eagain_drains_and_retries_the_same_packet_until_accepted() {
712 let mut actions = Vec::new();
713 let mut sends = 0;
714 submit_packet(
715 |drain| {
716 actions.push(drain);
717 if drain {
718 return Ok(true);
719 }
720 sends += 1;
721 Ok(sends == 3)
722 },
723 &mut WorkBudget::new(),
724 &CancelToken::default(),
725 )
726 .unwrap();
727 assert_eq!(actions, [false, true, false, true, false]);
728 assert!(
729 submit_packet(
730 |_| Ok(false),
731 &mut WorkBudget::new(),
732 &CancelToken::default()
733 )
734 .is_err()
735 );
736 assert!(submit_packet(Ok, &mut WorkBudget::new(), &CancelToken::default()).is_err());
737 let cancel = CancelToken::default();
738 let result = submit_packet(
739 |drain| {
740 if drain {
741 cancel.cancel();
742 }
743 Ok(drain)
744 },
745 &mut WorkBudget::new(),
746 &cancel,
747 );
748 assert_eq!(result, Err(DecodeError::Cancelled));
749 }
750}