1use oximedia_core::Rational;
6use std::collections::{HashMap, HashSet};
7
8use crate::clip::{Clip, ClipId, ClipSelection, ClipType};
9use crate::error::{EditError, EditResult};
10use crate::group::{GroupManager, LinkManager};
11use crate::magnetic_snap::{MagneticSnapConfig, MagneticSnapEngine};
12use crate::marker::{InOutPoints, MarkerManager, RegionManager};
13use crate::transition::TransitionManager;
14
15#[derive(Debug)]
17pub struct Timeline {
18 pub tracks: Vec<Track>,
20 pub timebase: Rational,
22 pub frame_rate: Rational,
24 pub duration: i64,
26 pub playhead: i64,
28 pub selection: ClipSelection,
30 pub transitions: TransitionManager,
32 pub markers: MarkerManager,
34 pub regions: RegionManager,
36 pub in_out: InOutPoints,
38 pub groups: GroupManager,
40 pub links: LinkManager,
42 pub next_clip_id: u64,
44 pub clip_map: HashMap<ClipId, (usize, usize)>, pub snap_engine: Option<MagneticSnapEngine>,
48}
49
50impl Timeline {
51 #[must_use]
53 pub fn new(timebase: Rational, frame_rate: Rational) -> Self {
54 Self {
55 tracks: Vec::new(),
56 timebase,
57 frame_rate,
58 duration: 0,
59 playhead: 0,
60 selection: ClipSelection::new(),
61 transitions: TransitionManager::new(),
62 markers: MarkerManager::new(),
63 regions: RegionManager::new(),
64 in_out: InOutPoints::new(),
65 groups: GroupManager::new(),
66 links: LinkManager::new(),
67 next_clip_id: 1,
68 clip_map: HashMap::new(),
69 snap_engine: None,
70 }
71 }
72
73 #[must_use]
75 pub fn with_magnetic_snap(mut self, config: MagneticSnapConfig) -> Self {
76 self.snap_engine = Some(MagneticSnapEngine::new(config));
77 self
78 }
79
80 #[must_use]
82 pub fn default_settings() -> Self {
83 Self::new(Rational::new(1, 1000), Rational::new(30, 1))
84 }
85
86 pub fn add_track(&mut self, track_type: TrackType) -> usize {
88 let index = self.tracks.len();
89 self.tracks.push(Track::new(index, track_type));
90 index
91 }
92
93 pub fn remove_track(&mut self, index: usize) -> EditResult<Track> {
95 if index >= self.tracks.len() {
96 return Err(EditError::InvalidTrackIndex(index, self.tracks.len()));
97 }
98
99 let track = &self.tracks[index];
101 for clip in &track.clips {
102 self.clip_map.remove(&clip.id);
103 }
104
105 let track = self.tracks.remove(index);
106
107 for (i, t) in self.tracks.iter_mut().enumerate() {
109 t.index = i;
110 }
111
112 self.rebuild_clip_map();
114
115 Ok(track)
116 }
117
118 #[must_use]
120 pub fn get_track(&self, index: usize) -> Option<&Track> {
121 self.tracks.get(index)
122 }
123
124 pub fn get_track_mut(&mut self, index: usize) -> Option<&mut Track> {
126 self.tracks.get_mut(index)
127 }
128
129 pub fn add_clip(&mut self, track_index: usize, mut clip: Clip) -> EditResult<ClipId> {
134 if track_index >= self.tracks.len() {
135 return Err(EditError::InvalidTrackIndex(track_index, self.tracks.len()));
136 }
137
138 let track_type = self.tracks[track_index].track_type;
140 if !track_type.matches_clip(clip.clip_type) {
141 return Err(EditError::TrackTypeMismatch {
142 expected: track_type.expected_clip_type(),
143 got: clip.clip_type,
144 });
145 }
146
147 clip.id = self.next_clip_id;
149 self.next_clip_id += 1;
150 let clip_id = clip.id;
151
152 let track = &self.tracks[track_index];
154 for existing in &track.clips {
155 if existing.overlaps(clip.timeline_start, clip.timeline_end()) {
156 return Err(EditError::ClipOverlap(clip.timeline_start, track_index));
157 }
158 }
159
160 let track = &mut self.tracks[track_index];
162 track.clips.push(clip);
163 track.sort_clips();
164
165 let clip_index = track
167 .clips
168 .iter()
169 .position(|c| c.id == clip_id)
170 .ok_or_else(|| {
171 EditError::InvalidEdit("clip was just inserted but not found".to_string())
172 })?;
173 self.clip_map.insert(clip_id, (track_index, clip_index));
174
175 self.update_duration();
177
178 Ok(clip_id)
179 }
180
181 pub fn remove_clip(&mut self, clip_id: ClipId) -> EditResult<Clip> {
187 let (track_index, _) = self
188 .clip_map
189 .get(&clip_id)
190 .copied()
191 .ok_or(EditError::ClipNotFound(clip_id))?;
192
193 let track = &mut self.tracks[track_index];
194 let clip_index = track
195 .clips
196 .iter()
197 .position(|c| c.id == clip_id)
198 .ok_or(EditError::ClipNotFound(clip_id))?;
199
200 let clip = track.clips.remove(clip_index);
201 self.clip_map.remove(&clip_id);
202
203 self.links.remove_clip_links(clip_id);
206
207 self.rebuild_clip_map();
208 self.update_duration();
209
210 Ok(clip)
211 }
212
213 #[must_use]
215 pub fn get_clip(&self, clip_id: ClipId) -> Option<&Clip> {
216 let (track_index, clip_index) = self.clip_map.get(&clip_id)?;
217 self.tracks.get(*track_index)?.clips.get(*clip_index)
218 }
219
220 pub fn get_clip_mut(&mut self, clip_id: ClipId) -> Option<&mut Clip> {
222 let (track_index, clip_index) = self.clip_map.get(&clip_id).copied()?;
223 self.tracks.get_mut(track_index)?.clips.get_mut(clip_index)
224 }
225
226 pub fn move_clip(&mut self, clip_id: ClipId, requested_start: i64) -> EditResult<()> {
236 let old_start = self
238 .get_clip(clip_id)
239 .ok_or(EditError::ClipNotFound(clip_id))?
240 .timeline_start;
241
242 let snapped_start = if let Some(ref engine) = self.snap_engine {
244 let mut cfg = engine.config.clone();
247 if !cfg.excluded_clips.contains(&clip_id) {
248 cfg.excluded_clips.push(clip_id);
249 }
250 let transient = MagneticSnapEngine::new(cfg);
251 let result = transient.snap_on_timeline(requested_start, self);
252 if result.snapped {
253 result.position
254 } else {
255 requested_start
256 }
257 } else {
258 requested_start
259 };
260
261 let delta = snapped_start - old_start;
262 if delta == 0 {
263 return Ok(());
264 }
265
266 let mut pending: Vec<(ClipId, i64, i64)> = Vec::new();
269 let mut visited: HashSet<ClipId> = HashSet::new();
270 let mut queue: Vec<ClipId> = vec![clip_id];
271 visited.insert(clip_id);
272
273 while let Some(id) = queue.pop() {
274 let this_old = if id == clip_id {
275 old_start
276 } else {
277 match self.get_clip(id) {
279 Some(c) => c.timeline_start,
280 None => continue,
281 }
282 };
283 pending.push((id, this_old, this_old + delta));
284
285 let linked: Vec<ClipId> = self
287 .links
288 .get_clip_links(id)
289 .into_iter()
290 .filter(|l| l.active)
291 .filter_map(|l| l.other_clip(id))
292 .collect();
293 for linked_id in linked {
294 if visited.insert(linked_id) {
295 queue.push(linked_id);
296 }
297 }
298 }
299
300 let batch_new: HashMap<ClipId, i64> = pending.iter().map(|&(id, _, ns)| (id, ns)).collect();
304
305 for &(moving_id, _, new_s) in &pending {
306 let (track_idx, dur) = {
307 let (ti, _) = self
308 .clip_map
309 .get(&moving_id)
310 .copied()
311 .ok_or(EditError::ClipNotFound(moving_id))?;
312 let dur = self.tracks[ti]
313 .clips
314 .iter()
315 .find(|c| c.id == moving_id)
316 .map(|c| c.timeline_duration)
317 .ok_or(EditError::ClipNotFound(moving_id))?;
318 (ti, dur)
319 };
320 let new_end = new_s + dur;
321
322 for existing in &self.tracks[track_idx].clips {
323 if existing.id == moving_id {
324 continue;
325 }
326 let ex_start = batch_new
328 .get(&existing.id)
329 .copied()
330 .unwrap_or(existing.timeline_start);
331 let ex_end = ex_start + existing.timeline_duration;
332 if !(new_end <= ex_start || new_s >= ex_end) {
334 return Err(EditError::ClipOverlap(new_s, track_idx));
335 }
336 }
337 }
338
339 for &(id, _, new_s) in &pending {
341 if let Some(clip) = self.get_clip_mut(id) {
342 clip.timeline_start = new_s;
343 }
344 }
345 let affected_tracks: HashSet<usize> = pending
347 .iter()
348 .filter_map(|&(id, _, _)| self.clip_map.get(&id).map(|&(ti, _)| ti))
349 .collect();
350 for ti in affected_tracks {
351 self.tracks[ti].sort_clips();
352 }
353 self.rebuild_clip_map();
354 self.update_duration();
355
356 Ok(())
357 }
358
359 pub fn move_clip_to_track(&mut self, clip_id: ClipId, target_track: usize) -> EditResult<()> {
361 if target_track >= self.tracks.len() {
362 return Err(EditError::InvalidTrackIndex(
363 target_track,
364 self.tracks.len(),
365 ));
366 }
367
368 let clip = self.remove_clip(clip_id)?;
369 self.add_clip(target_track, clip)?;
370 Ok(())
371 }
372
373 #[must_use]
375 pub fn get_clips_at(&self, position: i64) -> Vec<(usize, &Clip)> {
376 let mut result = Vec::new();
377 for track in &self.tracks {
378 if let Some(clip) = track.get_clip_at(position) {
379 result.push((track.index, clip));
380 }
381 }
382 result
383 }
384
385 #[must_use]
387 pub fn get_clips_in_range(&self, start: i64, end: i64) -> Vec<(usize, &Clip)> {
388 let mut result = Vec::new();
389 for track in &self.tracks {
390 for clip in &track.clips {
391 if clip.overlaps(start, end) {
392 result.push((track.index, clip));
393 }
394 }
395 }
396 result
397 }
398
399 pub fn rebuild_clip_map(&mut self) {
401 self.clip_map.clear();
402 for (track_index, track) in self.tracks.iter().enumerate() {
403 for (clip_index, clip) in track.clips.iter().enumerate() {
404 self.clip_map.insert(clip.id, (track_index, clip_index));
405 }
406 }
407 }
408
409 fn update_duration(&mut self) {
411 let mut max_end = 0i64;
412 for track in &self.tracks {
413 if let Some(clip) = track.clips.last() {
414 max_end = max_end.max(clip.timeline_end());
415 }
416 }
417 self.duration = max_end;
418 }
419
420 pub fn set_playhead(&mut self, position: i64) {
422 self.playhead = position.clamp(0, self.duration);
423 }
424
425 pub fn move_playhead(&mut self, delta: i64) {
427 self.set_playhead(self.playhead + delta);
428 }
429
430 pub fn seek_to_start(&mut self) {
432 self.playhead = 0;
433 }
434
435 pub fn seek_to_end(&mut self) {
437 self.playhead = self.duration;
438 }
439
440 #[must_use]
442 pub fn duration_seconds(&self) -> f64 {
443 let timestamp = oximedia_core::Timestamp::new(self.duration, self.timebase);
444 timestamp.to_seconds()
445 }
446
447 #[must_use]
449 pub fn video_tracks(&self) -> Vec<&Track> {
450 self.tracks
451 .iter()
452 .filter(|t| matches!(t.track_type, TrackType::Video))
453 .collect()
454 }
455
456 #[must_use]
458 pub fn audio_tracks(&self) -> Vec<&Track> {
459 self.tracks
460 .iter()
461 .filter(|t| matches!(t.track_type, TrackType::Audio))
462 .collect()
463 }
464
465 #[must_use]
467 pub fn subtitle_tracks(&self) -> Vec<&Track> {
468 self.tracks
469 .iter()
470 .filter(|t| matches!(t.track_type, TrackType::Subtitle))
471 .collect()
472 }
473
474 #[must_use]
476 pub fn clip_count(&self) -> usize {
477 self.tracks.iter().map(|t| t.clips.len()).sum()
478 }
479
480 pub fn clear(&mut self) {
482 self.tracks.clear();
483 self.clip_map.clear();
484 self.selection.clear();
485 self.transitions.clear();
486 self.markers.clear();
487 self.regions.clear();
488 self.in_out.clear();
489 self.groups.clear();
490 self.links.clear();
491 self.playhead = 0;
492 self.duration = 0;
493 self.next_clip_id = 1;
494 }
495}
496
497impl Default for Timeline {
498 fn default() -> Self {
499 Self::default_settings()
500 }
501}
502
503#[derive(Clone, Debug)]
505pub struct Track {
506 pub index: usize,
508 pub track_type: TrackType,
510 pub clips: Vec<Clip>,
512 pub muted: bool,
514 pub solo: bool,
516 pub locked: bool,
518 pub height: u32,
520 pub name: Option<String>,
522 pub color: Option<String>,
524}
525
526impl Track {
527 #[must_use]
529 pub fn new(index: usize, track_type: TrackType) -> Self {
530 Self {
531 index,
532 track_type,
533 clips: Vec::new(),
534 muted: false,
535 solo: false,
536 locked: false,
537 height: 60,
538 name: None,
539 color: None,
540 }
541 }
542
543 pub fn sort_clips(&mut self) {
545 self.clips.sort_by_key(|c| c.timeline_start);
546 }
547
548 #[must_use]
550 pub fn get_clip_at(&self, position: i64) -> Option<&Clip> {
551 self.clips.iter().find(|c| c.contains(position))
552 }
553
554 pub fn get_clip_at_mut(&mut self, position: i64) -> Option<&mut Clip> {
556 self.clips.iter_mut().find(|c| c.contains(position))
557 }
558
559 #[must_use]
561 pub fn get_clips_in_range(&self, start: i64, end: i64) -> Vec<&Clip> {
562 self.clips
563 .iter()
564 .filter(|c| c.overlaps(start, end))
565 .collect()
566 }
567
568 #[must_use]
570 pub fn is_video(&self) -> bool {
571 matches!(self.track_type, TrackType::Video)
572 }
573
574 #[must_use]
576 pub fn is_audio(&self) -> bool {
577 matches!(self.track_type, TrackType::Audio)
578 }
579
580 #[must_use]
582 pub fn is_subtitle(&self) -> bool {
583 matches!(self.track_type, TrackType::Subtitle)
584 }
585
586 #[must_use]
588 pub fn duration(&self) -> i64 {
589 self.clips.last().map_or(0, super::clip::Clip::timeline_end)
590 }
591
592 #[must_use]
594 pub fn is_empty(&self) -> bool {
595 self.clips.is_empty()
596 }
597
598 #[must_use]
600 pub fn clip_count(&self) -> usize {
601 self.clips.len()
602 }
603}
604
605#[derive(Clone, Copy, Debug, PartialEq, Eq)]
607pub enum TrackType {
608 Video,
610 Audio,
612 Subtitle,
614}
615
616impl TrackType {
617 #[must_use]
619 pub fn matches_clip(&self, clip_type: ClipType) -> bool {
620 matches!(
621 (self, clip_type),
622 (Self::Video, ClipType::Video)
623 | (Self::Audio, ClipType::Audio)
624 | (Self::Subtitle, ClipType::Subtitle)
625 )
626 }
627
628 #[must_use]
630 pub fn expected_clip_type(&self) -> ClipType {
631 match self {
632 Self::Video => ClipType::Video,
633 Self::Audio => ClipType::Audio,
634 Self::Subtitle => ClipType::Subtitle,
635 }
636 }
637}
638
639#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
641pub enum PlaybackState {
642 #[default]
644 Stopped,
645 Playing,
647 Paused,
649 Seeking,
651}
652
653#[derive(Clone, Debug)]
655pub struct TimelineConfig {
656 pub timebase: Rational,
658 pub frame_rate: Rational,
660 pub width: u32,
662 pub height: u32,
664 pub sample_rate: u32,
666 pub channels: u32,
668}
669
670impl Default for TimelineConfig {
671 fn default() -> Self {
672 Self {
673 timebase: Rational::new(1, 1000),
674 frame_rate: Rational::new(30, 1),
675 width: 1920,
676 height: 1080,
677 sample_rate: 48000,
678 channels: 2,
679 }
680 }
681}
682
683impl TimelineConfig {
684 #[must_use]
686 pub fn hd_1080p_30() -> Self {
687 Self {
688 width: 1920,
689 height: 1080,
690 frame_rate: Rational::new(30, 1),
691 ..Default::default()
692 }
693 }
694
695 #[must_use]
697 pub fn hd_1080p_60() -> Self {
698 Self {
699 width: 1920,
700 height: 1080,
701 frame_rate: Rational::new(60, 1),
702 ..Default::default()
703 }
704 }
705
706 #[must_use]
708 pub fn uhd_4k_30() -> Self {
709 Self {
710 width: 3840,
711 height: 2160,
712 frame_rate: Rational::new(30, 1),
713 ..Default::default()
714 }
715 }
716
717 #[must_use]
719 pub fn uhd_4k_60() -> Self {
720 Self {
721 width: 3840,
722 height: 2160,
723 frame_rate: Rational::new(60, 1),
724 ..Default::default()
725 }
726 }
727}