1use crate::core::analysis::event::MetadataEvent;
10
11#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct BlackRange {
14 pub start_us: i64,
15 pub end_us: i64,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq)]
21pub struct SilenceRange {
22 pub start_us: i64,
23 pub end_us: i64,
24 pub channel: Option<usize>,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct SceneChange {
30 pub at_us: i64,
31 pub score: f64,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq)]
36pub struct CropSuggestion {
37 pub x: i32,
38 pub y: i32,
39 pub w: i32,
40 pub h: i32,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
47pub struct LoudnessReport {
48 pub integrated: Option<f64>,
49 pub lra: Option<f64>,
50 pub true_peak: Option<f64>,
51}
52
53#[derive(Debug, Clone, PartialEq, Default)]
55pub struct AnalysisReport {
56 pub black: Vec<BlackRange>,
57 pub silence: Vec<SilenceRange>,
58 pub scenes: Vec<SceneChange>,
59 pub crop: Option<CropSuggestion>,
60 pub loudness: Option<LoudnessReport>,
61}
62
63#[derive(Debug, Clone, Copy, Default)]
66pub(crate) struct FoldConfig {
67 pub black_min_duration_us: Option<i64>,
68 pub silence_min_duration_us: Option<i64>,
69}
70
71pub(crate) fn fold(events: Vec<MetadataEvent>, cfg: &FoldConfig) -> AnalysisReport {
77 let mut report = AnalysisReport::default();
78 let mut pending_black: Option<i64> = None;
79 let mut pending_silence: Vec<(Option<usize>, i64)> = Vec::new();
80 let mut video_end_us: Option<i64> = None;
81 let mut audio_end_us: Option<i64> = None;
82
83 for ev in events {
84 match ev {
85 MetadataEvent::BlackStart { at } => pending_black = Some(at.time_us),
86 MetadataEvent::BlackEnd { at, .. } => {
87 if let Some(start) = pending_black.take() {
88 report.black.push(BlackRange {
89 start_us: start,
90 end_us: at.time_us,
91 });
92 }
93 }
94 MetadataEvent::SilenceStart { at, channel_number } => {
95 pending_silence.push((channel_number, at.time_us));
96 }
97 MetadataEvent::SilenceEnd { at, channel_number, .. } => {
98 if let Some(pos) = pending_silence.iter().position(|(c, _)| *c == channel_number) {
99 let (_, start) = pending_silence.remove(pos);
100 report.silence.push(SilenceRange {
101 start_us: start,
102 end_us: at.time_us,
103 channel: channel_number,
104 });
105 }
106 }
107 MetadataEvent::SceneChange { at, score } => report.scenes.push(SceneChange {
108 at_us: at.time_us,
109 score,
110 }),
111 MetadataEvent::CropDetect { x, y, w, h, .. } => {
112 report.crop = Some(CropSuggestion { x, y, w, h });
113 }
114 MetadataEvent::R128Summary {
115 integrated,
116 lra,
117 true_peak,
118 } => {
119 report.loudness = Some(LoudnessReport {
120 integrated,
121 lra,
122 true_peak,
123 });
124 }
125 MetadataEvent::StreamEnd { at, media } => {
126 let slot = if media == ffmpeg_sys_next::AVMediaType::AVMEDIA_TYPE_AUDIO {
130 &mut audio_end_us
131 } else {
132 &mut video_end_us
133 };
134 *slot = Some(slot.map_or(at.time_us, |e| e.max(at.time_us)));
135 }
136 MetadataEvent::R128Frame { .. } => {}
137 }
138 }
139
140 if let (Some(start), Some(end)) = (pending_black, video_end_us) {
142 if keep_tail(start, end, cfg.black_min_duration_us) {
143 report.black.push(BlackRange {
144 start_us: start,
145 end_us: end,
146 });
147 }
148 }
149 if let Some(end) = audio_end_us {
151 for (channel, start) in pending_silence {
152 if keep_tail(start, end, cfg.silence_min_duration_us) {
153 report.silence.push(SilenceRange {
154 start_us: start,
155 end_us: end,
156 channel,
157 });
158 }
159 }
160 }
161
162 report
163}
164
165fn keep_tail(start_us: i64, end_us: i64, min_duration_us: Option<i64>) -> bool {
166 let duration = end_us.saturating_sub(start_us);
167 duration >= 0 && min_duration_us.is_none_or(|min| duration >= min)
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::core::analysis::event::Timestamp;
174 use ffmpeg_sys_next::AVMediaType::AVMEDIA_TYPE_VIDEO;
175
176 fn ts(us: i64) -> Timestamp {
177 Timestamp {
178 time_us: us,
179 pts: None,
180 time_base: None,
181 }
182 }
183
184 #[test]
185 fn pairs_black_start_end_into_range() {
186 let events = vec![
187 MetadataEvent::BlackStart { at: ts(1_000_000) },
188 MetadataEvent::BlackEnd {
189 at: ts(3_000_000),
190 duration_us: 2_000_000,
191 },
192 ];
193 let report = fold(events, &FoldConfig::default());
194 assert_eq!(
195 report.black,
196 vec![BlackRange {
197 start_us: 1_000_000,
198 end_us: 3_000_000
199 }]
200 );
201 }
202
203 #[test]
204 fn unpaired_start_closed_at_stream_end() {
205 let events = vec![
206 MetadataEvent::BlackStart { at: ts(1_000_000) },
207 MetadataEvent::StreamEnd {
208 media: AVMEDIA_TYPE_VIDEO,
209 at: ts(5_000_000),
210 },
211 ];
212 let report = fold(events, &FoldConfig::default());
213 assert_eq!(
214 report.black,
215 vec![BlackRange {
216 start_us: 1_000_000,
217 end_us: 5_000_000
218 }]
219 );
220 }
221
222 #[test]
223 fn short_tail_dropped_when_below_min_duration() {
224 let cfg = FoldConfig {
225 black_min_duration_us: Some(2_000_000),
226 silence_min_duration_us: None,
227 };
228 let events = vec![
230 MetadataEvent::BlackStart { at: ts(4_500_000) },
231 MetadataEvent::StreamEnd {
232 media: AVMEDIA_TYPE_VIDEO,
233 at: ts(5_000_000),
234 },
235 ];
236 assert!(fold(events, &cfg).black.is_empty());
237 }
238
239 #[test]
240 fn crop_takes_last_value() {
241 let events = vec![
242 MetadataEvent::CropDetect {
243 at: ts(0),
244 x: 0,
245 y: 0,
246 w: 100,
247 h: 100,
248 },
249 MetadataEvent::CropDetect {
250 at: ts(1),
251 x: 0,
252 y: 10,
253 w: 100,
254 h: 80,
255 },
256 ];
257 let report = fold(events, &FoldConfig::default());
258 assert_eq!(
259 report.crop,
260 Some(CropSuggestion {
261 x: 0,
262 y: 10,
263 w: 100,
264 h: 80
265 })
266 );
267 }
268}