ff_decode/analysis/
black_frame_detector.rs1#![allow(unsafe_code)]
4
5use std::path::{Path, PathBuf};
6use std::time::Duration;
7
8use crate::DecodeError;
9
10pub struct BlackFrameDetector {
30 input: PathBuf,
31 threshold: f64,
32}
33
34impl BlackFrameDetector {
35 pub fn new(input: impl AsRef<Path>) -> Self {
40 Self {
41 input: input.as_ref().to_path_buf(),
42 threshold: 0.1,
43 }
44 }
45
46 #[must_use]
55 pub fn threshold(self, t: f64) -> Self {
56 Self {
57 threshold: t,
58 ..self
59 }
60 }
61
62 pub fn run(self) -> Result<Vec<Duration>, DecodeError> {
70 if !(0.0..=1.0).contains(&self.threshold) {
71 return Err(DecodeError::AnalysisFailed {
72 reason: format!("threshold must be in [0.0, 1.0], got {}", self.threshold),
73 });
74 }
75 if !self.input.exists() {
76 return Err(DecodeError::AnalysisFailed {
77 reason: format!("file not found: {}", self.input.display()),
78 });
79 }
80 unsafe { super::analysis_inner::detect_black_frames_unsafe(&self.input, self.threshold) }
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn black_frame_detector_invalid_threshold_below_zero_should_return_analysis_failed() {
93 let result = BlackFrameDetector::new("irrelevant.mp4")
94 .threshold(-0.1)
95 .run();
96 assert!(
97 matches!(result, Err(DecodeError::AnalysisFailed { .. })),
98 "expected AnalysisFailed for threshold=-0.1, got {result:?}"
99 );
100 }
101
102 #[test]
103 fn black_frame_detector_invalid_threshold_above_one_should_return_analysis_failed() {
104 let result = BlackFrameDetector::new("irrelevant.mp4")
105 .threshold(1.1)
106 .run();
107 assert!(
108 matches!(result, Err(DecodeError::AnalysisFailed { .. })),
109 "expected AnalysisFailed for threshold=1.1, got {result:?}"
110 );
111 }
112
113 #[test]
114 fn black_frame_detector_missing_file_should_return_analysis_failed() {
115 let result = BlackFrameDetector::new("does_not_exist_99999.mp4").run();
116 assert!(
117 matches!(result, Err(DecodeError::AnalysisFailed { .. })),
118 "expected AnalysisFailed for missing file, got {result:?}"
119 );
120 }
121
122 #[test]
123 fn black_frame_detector_boundary_thresholds_should_be_valid() {
124 let r0 = BlackFrameDetector::new("irrelevant.mp4")
126 .threshold(0.0)
127 .run();
128 let r1 = BlackFrameDetector::new("irrelevant.mp4")
129 .threshold(1.0)
130 .run();
131 assert!(
132 matches!(r0, Err(DecodeError::AnalysisFailed { .. })),
133 "expected AnalysisFailed (file not found) for threshold=0.0, got {r0:?}"
134 );
135 assert!(
136 matches!(r1, Err(DecodeError::AnalysisFailed { .. })),
137 "expected AnalysisFailed (file not found) for threshold=1.0, got {r1:?}"
138 );
139 }
140}