1use std::collections::HashMap;
8use std::fmt;
9
10use oximedia_core::Rational;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub struct SequenceId(pub u64);
19
20impl fmt::Display for SequenceId {
21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 write!(f, "seq-{}", self.0)
23 }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum ConformMethod {
34 NearestFrame,
36 FrameBlend,
38 OpticalFlow,
40}
41
42impl fmt::Display for ConformMethod {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 Self::NearestFrame => write!(f, "nearest"),
46 Self::FrameBlend => write!(f, "blend"),
47 Self::OpticalFlow => write!(f, "optical-flow"),
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct NestedSequenceRef {
55 pub sequence_id: SequenceId,
57 pub position: i64,
59 pub in_point: i64,
61 pub out_point: i64,
63 pub speed: f64,
65 pub conform: ConformMethod,
67 pub flattened: bool,
69}
70
71impl NestedSequenceRef {
72 #[must_use]
74 pub fn new(sequence_id: SequenceId, position: i64, in_point: i64, out_point: i64) -> Self {
75 Self {
76 sequence_id,
77 position,
78 in_point,
79 out_point,
80 speed: 1.0,
81 conform: ConformMethod::NearestFrame,
82 flattened: false,
83 }
84 }
85
86 #[must_use]
88 pub fn with_speed(mut self, speed: f64) -> Self {
89 self.speed = speed;
90 self
91 }
92
93 #[must_use]
95 pub fn with_conform(mut self, method: ConformMethod) -> Self {
96 self.conform = method;
97 self
98 }
99
100 #[allow(clippy::cast_precision_loss)]
105 #[allow(clippy::cast_possible_truncation)]
106 #[allow(clippy::cast_sign_loss)]
107 #[must_use]
108 pub fn duration(&self) -> i64 {
109 let source_len = (self.out_point - self.in_point).max(0);
110 if self.speed.abs() < f64::EPSILON {
111 return 0;
112 }
113 (source_len as f64 / self.speed).round().max(0.0) as i64
114 }
115
116 #[must_use]
118 pub fn end_position(&self) -> i64 {
119 self.position.saturating_add(self.duration())
120 }
121}
122
123#[derive(Debug, Clone)]
129pub struct NestedSequence {
130 pub id: SequenceId,
132 pub name: String,
134 pub frame_rate: Rational,
136 pub duration: i64,
138 pub resolution: (u32, u32),
140 pub track_count: u32,
142 pub nested_refs: Vec<NestedSequenceRef>,
144}
145
146impl NestedSequence {
147 #[must_use]
149 pub fn new(id: SequenceId, name: impl Into<String>, frame_rate: Rational) -> Self {
150 Self {
151 id,
152 name: name.into(),
153 frame_rate,
154 duration: 0,
155 resolution: (1920, 1080),
156 track_count: 0,
157 nested_refs: Vec::new(),
158 }
159 }
160
161 #[must_use]
163 pub fn with_duration(mut self, duration: i64) -> Self {
164 self.duration = duration;
165 self
166 }
167
168 #[must_use]
170 pub fn with_resolution(mut self, width: u32, height: u32) -> Self {
171 self.resolution = (width, height);
172 self
173 }
174
175 #[must_use]
177 pub fn with_track_count(mut self, count: u32) -> Self {
178 self.track_count = count;
179 self
180 }
181
182 pub fn add_ref(&mut self, r: NestedSequenceRef) {
184 self.nested_refs.push(r);
185 }
186
187 pub fn remove_ref(&mut self, child_id: SequenceId) -> usize {
189 let before = self.nested_refs.len();
190 self.nested_refs.retain(|r| r.sequence_id != child_id);
191 before - self.nested_refs.len()
192 }
193
194 #[must_use]
196 pub fn references(&self, child_id: SequenceId) -> bool {
197 self.nested_refs.iter().any(|r| r.sequence_id == child_id)
198 }
199
200 #[must_use]
225 pub fn duration_in_outer_timebase(&self, outer_rate: Rational) -> i64 {
226 if self.frame_rate.num == 0
228 || self.frame_rate.den == 0
229 || outer_rate.num == 0
230 || outer_rate.den == 0
231 {
232 return 0;
233 }
234 let numerator: i128 =
246 self.duration as i128 * outer_rate.num as i128 * self.frame_rate.den as i128;
247 let denominator: i128 = outer_rate.den as i128 * self.frame_rate.num as i128;
248 if denominator == 0 {
249 return 0;
250 }
251 let half = denominator.abs() / 2;
254 let rounded = if numerator >= 0 {
255 (numerator + half) / denominator
256 } else {
257 (numerator - half) / denominator
258 };
259 rounded as i64
260 }
261}
262
263#[derive(Debug, Clone)]
269pub struct SequenceRegistry {
270 sequences: HashMap<SequenceId, NestedSequence>,
272 next_id: u64,
274}
275
276impl SequenceRegistry {
277 #[must_use]
279 pub fn new() -> Self {
280 Self {
281 sequences: HashMap::new(),
282 next_id: 1,
283 }
284 }
285
286 pub fn create(&mut self, name: impl Into<String>, frame_rate: Rational) -> SequenceId {
288 let id = SequenceId(self.next_id);
289 self.next_id += 1;
290 let seq = NestedSequence::new(id, name, frame_rate);
291 self.sequences.insert(id, seq);
292 id
293 }
294
295 #[must_use]
297 pub fn get(&self, id: SequenceId) -> Option<&NestedSequence> {
298 self.sequences.get(&id)
299 }
300
301 pub fn get_mut(&mut self, id: SequenceId) -> Option<&mut NestedSequence> {
303 self.sequences.get_mut(&id)
304 }
305
306 pub fn remove(&mut self, id: SequenceId) -> Option<NestedSequence> {
308 let removed = self.sequences.remove(&id);
309 for seq in self.sequences.values_mut() {
311 seq.remove_ref(id);
312 }
313 removed
314 }
315
316 #[must_use]
318 pub fn count(&self) -> usize {
319 self.sequences.len()
320 }
321
322 #[must_use]
324 pub fn is_empty(&self) -> bool {
325 self.sequences.is_empty()
326 }
327
328 #[must_use]
331 pub fn would_create_cycle(&self, parent: SequenceId, child: SequenceId) -> bool {
332 if parent == child {
333 return true;
334 }
335 let mut stack = vec![child];
337 let mut visited = std::collections::HashSet::new();
338 while let Some(current) = stack.pop() {
339 if !visited.insert(current) {
340 continue;
341 }
342 if let Some(seq) = self.sequences.get(¤t) {
343 for r in &seq.nested_refs {
344 if r.sequence_id == parent {
345 return true;
346 }
347 stack.push(r.sequence_id);
348 }
349 }
350 }
351 false
352 }
353
354 #[must_use]
356 pub fn depth(&self, id: SequenceId) -> usize {
357 let seq = match self.sequences.get(&id) {
358 Some(s) => s,
359 None => return 0,
360 };
361 if seq.nested_refs.is_empty() {
362 return 1;
363 }
364 let max_child = seq
365 .nested_refs
366 .iter()
367 .map(|r| self.depth(r.sequence_id))
368 .max()
369 .unwrap_or(0);
370 1 + max_child
371 }
372}
373
374impl Default for SequenceRegistry {
375 fn default() -> Self {
376 Self::new()
377 }
378}
379
380#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
389 fn test_sequence_id_display() {
390 assert_eq!(SequenceId(7).to_string(), "seq-7");
391 }
392
393 #[test]
394 fn test_rational_frame_rate_display() {
395 let fr = Rational::new(24, 1);
397 assert_eq!(fr.to_string(), "24/1");
398 let ntsc = Rational::new(30000, 1001);
399 assert_eq!(ntsc.to_string(), "30000/1001");
400 }
401
402 #[test]
403 fn test_conform_method_display() {
404 assert_eq!(ConformMethod::NearestFrame.to_string(), "nearest");
405 assert_eq!(ConformMethod::OpticalFlow.to_string(), "optical-flow");
406 }
407
408 #[test]
409 fn test_nested_ref_duration_normal_speed() {
410 let r = NestedSequenceRef::new(SequenceId(1), 0, 0, 1000);
411 assert_eq!(r.duration(), 1000);
412 }
413
414 #[test]
415 fn test_nested_ref_duration_double_speed() {
416 let r = NestedSequenceRef::new(SequenceId(1), 0, 0, 1000).with_speed(2.0);
417 assert_eq!(r.duration(), 500);
418 }
419
420 #[test]
421 fn test_nested_ref_duration_zero_speed() {
422 let r = NestedSequenceRef::new(SequenceId(1), 0, 0, 1000).with_speed(0.0);
423 assert_eq!(r.duration(), 0);
424 }
425
426 #[test]
427 fn test_nested_ref_end_position() {
428 let r = NestedSequenceRef::new(SequenceId(1), 500, 0, 1000);
429 assert_eq!(r.end_position(), 1500);
430 }
431
432 #[test]
433 fn test_nested_ref_negative_position() {
434 let r = NestedSequenceRef::new(SequenceId(1), -24, 0, 48);
436 assert_eq!(r.position, -24_i64);
437 assert_eq!(r.end_position(), 24); }
439
440 #[test]
441 fn test_nested_sequence_add_and_remove_ref() {
442 let mut seq = NestedSequence::new(SequenceId(1), "Main", Rational::new(24, 1));
443 seq.add_ref(NestedSequenceRef::new(SequenceId(2), 0, 0, 100));
444 assert!(seq.references(SequenceId(2)));
445 let removed = seq.remove_ref(SequenceId(2));
446 assert_eq!(removed, 1);
447 assert!(!seq.references(SequenceId(2)));
448 }
449
450 #[test]
451 fn test_nested_sequence_default_resolution() {
452 let seq = NestedSequence::new(SequenceId(1), "S", Rational::new(24, 1));
453 assert_eq!(seq.resolution, (1920, 1080));
454 }
455
456 #[test]
457 fn test_nested_sequence_with_resolution() {
458 let seq = NestedSequence::new(SequenceId(1), "S", Rational::new(24, 1))
459 .with_resolution(1280, 720);
460 assert_eq!(seq.resolution, (1280, 720));
461 }
462
463 #[test]
464 fn test_registry_create_and_get() {
465 let mut reg = SequenceRegistry::new();
466 let id = reg.create("Timeline 1", Rational::new(30, 1));
467 assert!(reg.get(id).is_some());
468 assert_eq!(reg.count(), 1);
469 }
470
471 #[test]
472 fn test_registry_remove_cleans_refs() {
473 let mut reg = SequenceRegistry::new();
474 let parent = reg.create("Parent", Rational::new(24, 1));
475 let child = reg.create("Child", Rational::new(24, 1));
476 reg.get_mut(parent)
477 .expect("test expectation failed")
478 .add_ref(NestedSequenceRef::new(child, 0, 0, 100));
479 reg.remove(child);
480 assert!(!reg
481 .get(parent)
482 .expect("get should succeed")
483 .references(child));
484 }
485
486 #[test]
487 fn test_registry_cycle_detection_self() {
488 let mut reg = SequenceRegistry::new();
489 let id = reg.create("S", Rational::new(24, 1));
490 assert!(reg.would_create_cycle(id, id));
491 }
492
493 #[test]
494 fn test_registry_cycle_detection_indirect() {
495 let mut reg = SequenceRegistry::new();
496 let a = reg.create("A", Rational::new(24, 1));
497 let b = reg.create("B", Rational::new(24, 1));
498 let c = reg.create("C", Rational::new(24, 1));
499 reg.get_mut(a)
500 .expect("test expectation failed")
501 .add_ref(NestedSequenceRef::new(b, 0, 0, 100));
502 reg.get_mut(b)
503 .expect("test expectation failed")
504 .add_ref(NestedSequenceRef::new(c, 0, 0, 100));
505 assert!(reg.would_create_cycle(c, a));
507 assert!(!reg.would_create_cycle(a, c));
509 }
510
511 #[test]
512 fn test_registry_depth() {
513 let mut reg = SequenceRegistry::new();
514 let a = reg.create("A", Rational::new(24, 1));
515 let b = reg.create("B", Rational::new(24, 1));
516 reg.get_mut(a)
517 .expect("test expectation failed")
518 .add_ref(NestedSequenceRef::new(b, 0, 0, 100));
519 assert_eq!(reg.depth(a), 2);
520 assert_eq!(reg.depth(b), 1);
521 }
522
523 #[test]
524 fn test_registry_default() {
525 let reg = SequenceRegistry::default();
526 assert!(reg.is_empty());
527 }
528
529 #[test]
530 fn test_nested_sequence_builders() {
531 let seq = NestedSequence::new(SequenceId(1), "S", Rational::new(25, 1))
532 .with_duration(5000)
533 .with_track_count(4);
534 assert_eq!(seq.duration, 5000);
535 assert_eq!(seq.track_count, 4);
536 }
537
538 #[test]
539 fn test_nested_ref_with_conform() {
540 let r = NestedSequenceRef::new(SequenceId(1), 0, 0, 100)
541 .with_conform(ConformMethod::FrameBlend);
542 assert_eq!(r.conform, ConformMethod::FrameBlend);
543 }
544
545 #[test]
548 fn test_duration_in_outer_same_rate() {
549 let seq = NestedSequence::new(SequenceId(1), "S", Rational::new(25, 1)).with_duration(100);
551 assert_eq!(seq.duration_in_outer_timebase(Rational::new(25, 1)), 100);
552 }
553
554 #[test]
555 fn test_duration_24fps_to_60fps() {
556 let seq = NestedSequence::new(SequenceId(1), "S", Rational::new(24, 1)).with_duration(24);
558 assert_eq!(seq.duration_in_outer_timebase(Rational::new(60, 1)), 60);
559 }
560
561 #[test]
562 fn test_duration_60fps_to_24fps() {
563 let seq = NestedSequence::new(SequenceId(1), "S", Rational::new(60, 1)).with_duration(60);
565 assert_eq!(seq.duration_in_outer_timebase(Rational::new(24, 1)), 24);
566 }
567
568 #[test]
569 fn test_duration_ntsc_to_integer_fps() {
570 let seq = NestedSequence::new(SequenceId(1), "S", Rational::new(30000, 1001))
573 .with_duration(30000); let seq2 = NestedSequence::new(SequenceId(1), "S", Rational::new(30000, 1001))
581 .with_duration(30000);
582 assert_eq!(seq2.duration_in_outer_timebase(Rational::new(30, 1)), 30030);
584 drop(seq); }
586
587 #[test]
588 fn test_duration_zero_inner_duration() {
589 let seq = NestedSequence::new(SequenceId(1), "S", Rational::new(24, 1)).with_duration(0);
590 assert_eq!(seq.duration_in_outer_timebase(Rational::new(60, 1)), 0);
591 }
592}