1#![allow(dead_code)]
9
10use crate::{Result, TranscodeError};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct FrameTimecode {
19 pub frame: u64,
21 pub timestamp_ms: u64,
23}
24
25impl FrameTimecode {
26 #[must_use]
32 pub fn from_frame(frame: u64, fps_num: u32, fps_den: u32) -> Option<Self> {
33 if fps_den == 0 || fps_num == 0 {
34 return None;
35 }
36 let timestamp_ms = frame * 1_000 * u64::from(fps_den) / u64::from(fps_num);
37 Some(Self {
38 frame,
39 timestamp_ms,
40 })
41 }
42
43 #[must_use]
48 pub fn from_ms(timestamp_ms: u64, fps_num: u32, fps_den: u32) -> Option<Self> {
49 if fps_den == 0 || fps_num == 0 {
50 return None;
51 }
52 let frame = timestamp_ms * u64::from(fps_num) / (1_000 * u64::from(fps_den));
53 let snapped_ms = frame * 1_000 * u64::from(fps_den) / u64::from(fps_num);
55 Some(Self {
56 frame,
57 timestamp_ms: snapped_ms,
58 })
59 }
60
61 #[must_use]
65 pub fn duration_to(&self, other: &Self) -> Option<u64> {
66 other.timestamp_ms.checked_sub(self.timestamp_ms)
67 }
68
69 #[must_use]
73 pub fn frames_to(&self, other: &Self) -> Option<u64> {
74 other.frame.checked_sub(self.frame)
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum TrimPoint {
83 Frame(u64),
85 Milliseconds(u64),
87}
88
89impl TrimPoint {
90 pub fn resolve(&self, fps_num: u32, fps_den: u32) -> Result<FrameTimecode> {
96 match self {
97 Self::Frame(f) => FrameTimecode::from_frame(*f, fps_num, fps_den).ok_or_else(|| {
98 TranscodeError::ValidationError(crate::ValidationError::Unsupported(
99 "Invalid frame rate: fps_num or fps_den is zero".into(),
100 ))
101 }),
102 Self::Milliseconds(ms) => {
103 FrameTimecode::from_ms(*ms, fps_num, fps_den).ok_or_else(|| {
104 TranscodeError::ValidationError(crate::ValidationError::Unsupported(
105 "Invalid frame rate: fps_num or fps_den is zero".into(),
106 ))
107 })
108 }
109 }
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct TrimRange {
121 pub in_point: TrimPoint,
123 pub out_point: TrimPoint,
125}
126
127impl TrimRange {
128 #[must_use]
130 pub fn frames(in_frame: u64, out_frame: u64) -> Self {
131 Self {
132 in_point: TrimPoint::Frame(in_frame),
133 out_point: TrimPoint::Frame(out_frame),
134 }
135 }
136
137 #[must_use]
139 pub fn milliseconds(in_ms: u64, out_ms: u64) -> Self {
140 Self {
141 in_point: TrimPoint::Milliseconds(in_ms),
142 out_point: TrimPoint::Milliseconds(out_ms),
143 }
144 }
145
146 pub fn validate(&self, fps_num: u32, fps_den: u32, total_frames: Option<u64>) -> Result<()> {
153 let in_tc = self.in_point.resolve(fps_num, fps_den)?;
154 let out_tc = self.out_point.resolve(fps_num, fps_den)?;
155
156 if in_tc.frame >= out_tc.frame {
157 return Err(TranscodeError::ValidationError(
158 crate::ValidationError::Unsupported(format!(
159 "Trim in-point frame {} must be less than out-point frame {}",
160 in_tc.frame, out_tc.frame
161 )),
162 ));
163 }
164
165 if let Some(total) = total_frames {
166 if out_tc.frame >= total {
167 return Err(TranscodeError::ValidationError(
168 crate::ValidationError::Unsupported(format!(
169 "Trim out-point frame {} exceeds total frames {}",
170 out_tc.frame, total
171 )),
172 ));
173 }
174 }
175
176 Ok(())
177 }
178
179 pub fn resolve(&self, fps_num: u32, fps_den: u32) -> Result<ResolvedTrimRange> {
185 let in_tc = self.in_point.resolve(fps_num, fps_den)?;
186 let out_tc = self.out_point.resolve(fps_num, fps_den)?;
187 Ok(ResolvedTrimRange {
188 in_point: in_tc,
189 out_point: out_tc,
190 })
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub struct ResolvedTrimRange {
197 pub in_point: FrameTimecode,
199 pub out_point: FrameTimecode,
201}
202
203impl ResolvedTrimRange {
204 #[must_use]
206 pub fn frame_count(&self) -> u64 {
207 self.out_point.frame.saturating_sub(self.in_point.frame) + 1
208 }
209
210 #[must_use]
212 pub fn duration_ms(&self) -> u64 {
213 self.out_point
214 .timestamp_ms
215 .saturating_sub(self.in_point.timestamp_ms)
216 }
217
218 #[must_use]
220 pub fn contains_frame(&self, frame: u64) -> bool {
221 frame >= self.in_point.frame && frame <= self.out_point.frame
222 }
223}
224
225#[derive(Debug, Clone)]
232pub struct FrameTrimConfig {
233 pub fps_num: u32,
235 pub fps_den: u32,
237 pub total_source_frames: Option<u64>,
239 pub ranges: Vec<TrimRange>,
241}
242
243impl FrameTrimConfig {
244 #[must_use]
246 pub fn new(fps_num: u32, fps_den: u32) -> Self {
247 Self {
248 fps_num,
249 fps_den,
250 total_source_frames: None,
251 ranges: Vec::new(),
252 }
253 }
254
255 #[must_use]
257 pub fn total_frames(mut self, n: u64) -> Self {
258 self.total_source_frames = Some(n);
259 self
260 }
261
262 #[must_use]
264 pub fn add_range(mut self, range: TrimRange) -> Self {
265 self.ranges.push(range);
266 self
267 }
268
269 pub fn validate_and_resolve(&self) -> Result<Vec<ResolvedTrimRange>> {
275 if self.ranges.is_empty() {
276 return Err(TranscodeError::ValidationError(
277 crate::ValidationError::Unsupported("Trim config has no ranges".into()),
278 ));
279 }
280
281 let mut resolved: Vec<ResolvedTrimRange> = self
282 .ranges
283 .iter()
284 .map(|r| {
285 r.validate(self.fps_num, self.fps_den, self.total_source_frames)?;
286 r.resolve(self.fps_num, self.fps_den)
287 })
288 .collect::<Result<Vec<_>>>()?;
289
290 resolved.sort_by_key(|r| r.in_point.frame);
292
293 for pair in resolved.windows(2) {
295 let a = &pair[0];
296 let b = &pair[1];
297 if b.in_point.frame <= a.out_point.frame {
298 return Err(TranscodeError::ValidationError(
299 crate::ValidationError::Unsupported(format!(
300 "Trim ranges overlap: [{}, {}] and [{}, {}]",
301 a.in_point.frame, a.out_point.frame, b.in_point.frame, b.out_point.frame
302 )),
303 ));
304 }
305 }
306
307 Ok(resolved)
308 }
309
310 pub fn total_output_frames(&self) -> Result<u64> {
316 let resolved = self.validate_and_resolve()?;
317 Ok(resolved.iter().map(|r| r.frame_count()).sum())
318 }
319
320 pub fn total_output_duration_ms(&self) -> Result<u64> {
326 let resolved = self.validate_and_resolve()?;
327 Ok(resolved.iter().map(|r| r.duration_ms()).sum())
328 }
329
330 #[must_use]
334 pub fn should_include_frame(&self, frame: u64) -> bool {
335 match self.validate_and_resolve() {
336 Ok(resolved) => resolved.iter().any(|r| r.contains_frame(frame)),
337 Err(_) => false,
338 }
339 }
340}
341
342#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn test_frame_timecode_from_frame_30fps() {
350 let tc = FrameTimecode::from_frame(30, 30, 1).expect("valid");
351 assert_eq!(tc.frame, 30);
352 assert_eq!(tc.timestamp_ms, 1000);
353 }
354
355 #[test]
356 fn test_frame_timecode_from_ms_30fps() {
357 let tc = FrameTimecode::from_ms(1000, 30, 1).expect("valid");
358 assert_eq!(tc.frame, 30);
359 assert_eq!(tc.timestamp_ms, 1000);
360 }
361
362 #[test]
363 fn test_frame_timecode_zero_fps_returns_none() {
364 assert!(FrameTimecode::from_frame(5, 0, 1).is_none());
365 assert!(FrameTimecode::from_frame(5, 30, 0).is_none());
366 }
367
368 #[test]
369 fn test_frame_timecode_duration_to() {
370 let a = FrameTimecode::from_frame(0, 30, 1).expect("valid");
371 let b = FrameTimecode::from_frame(30, 30, 1).expect("valid");
372 assert_eq!(a.duration_to(&b), Some(1000));
373 }
374
375 #[test]
376 fn test_frame_timecode_duration_to_reverse_is_none() {
377 let a = FrameTimecode::from_frame(30, 30, 1).expect("valid");
378 let b = FrameTimecode::from_frame(0, 30, 1).expect("valid");
379 assert_eq!(a.duration_to(&b), None);
380 }
381
382 #[test]
383 fn test_trim_range_validate_ok() {
384 let range = TrimRange::frames(0, 59);
385 assert!(range.validate(30, 1, Some(120)).is_ok());
386 }
387
388 #[test]
389 fn test_trim_range_validate_in_ge_out_fails() {
390 let range = TrimRange::frames(60, 30);
391 assert!(range.validate(30, 1, None).is_err());
392 }
393
394 #[test]
395 fn test_trim_range_validate_out_exceeds_total_fails() {
396 let range = TrimRange::frames(0, 200);
397 assert!(range.validate(30, 1, Some(100)).is_err());
398 }
399
400 #[test]
401 fn test_trim_range_resolve_ms() {
402 let range = TrimRange::milliseconds(0, 1000);
403 let resolved = range.resolve(30, 1).expect("valid");
404 assert_eq!(resolved.in_point.frame, 0);
405 assert_eq!(resolved.out_point.frame, 30);
406 }
407
408 #[test]
409 fn test_resolved_range_frame_count() {
410 let range = TrimRange::frames(10, 19);
411 let r = range.resolve(30, 1).expect("valid");
412 assert_eq!(r.frame_count(), 10);
413 }
414
415 #[test]
416 fn test_resolved_range_contains_frame() {
417 let range = TrimRange::frames(10, 19);
418 let r = range.resolve(30, 1).expect("valid");
419 assert!(r.contains_frame(10));
420 assert!(r.contains_frame(15));
421 assert!(r.contains_frame(19));
422 assert!(!r.contains_frame(9));
423 assert!(!r.contains_frame(20));
424 }
425
426 #[test]
427 fn test_trim_config_total_output_frames() {
428 let cfg = FrameTrimConfig::new(30, 1)
429 .total_frames(300)
430 .add_range(TrimRange::frames(0, 29)) .add_range(TrimRange::frames(60, 89)); assert_eq!(cfg.total_output_frames().expect("valid"), 60);
433 }
434
435 #[test]
436 fn test_trim_config_overlapping_ranges_fails() {
437 let cfg = FrameTrimConfig::new(30, 1)
438 .add_range(TrimRange::frames(0, 59))
439 .add_range(TrimRange::frames(30, 89)); assert!(cfg.validate_and_resolve().is_err());
441 }
442
443 #[test]
444 fn test_trim_config_no_ranges_fails() {
445 let cfg = FrameTrimConfig::new(30, 1);
446 assert!(cfg.validate_and_resolve().is_err());
447 }
448
449 #[test]
450 fn test_should_include_frame() {
451 let cfg = FrameTrimConfig::new(30, 1)
452 .add_range(TrimRange::frames(10, 19))
453 .add_range(TrimRange::frames(30, 39));
454
455 assert!(cfg.should_include_frame(10));
456 assert!(cfg.should_include_frame(15));
457 assert!(cfg.should_include_frame(19));
458 assert!(cfg.should_include_frame(30));
459 assert!(!cfg.should_include_frame(9));
460 assert!(!cfg.should_include_frame(20));
461 assert!(!cfg.should_include_frame(29));
462 assert!(!cfg.should_include_frame(40));
463 }
464
465 #[test]
466 fn test_total_output_duration_ms() {
467 let cfg = FrameTrimConfig::new(30, 1)
469 .add_range(TrimRange::milliseconds(0, 1000))
470 .add_range(TrimRange::milliseconds(2000, 3000));
471 let total_ms = cfg.total_output_duration_ms().expect("valid");
472 assert_eq!(total_ms, 2000);
473 }
474}