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