1use crate::{
2 AudioProperties, Captions, ClipBlendMode, ClipComposition, ClipEffect, ClipId, Easing, Matte,
3 MediaKind, ShapeMask, TimeMap, TrackAudio, TrackId, VisualProperties,
4};
5use alpha2_media::SourceId;
6use alpha2_time::TimeRange;
7use serde::{Deserialize, Serialize};
8
9#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
11#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12#[serde(deny_unknown_fields)]
13pub struct Timeline {
14 pub tracks: Vec<Track>,
16}
17
18impl Timeline {
19 #[must_use]
22 pub fn extent(&self) -> Option<TimeRange> {
23 let mut ranges = self.tracks.iter().flat_map(|track| {
24 track.clips.iter().map(|clip| clip.timeline_range).chain(
25 track
26 .captions
27 .iter()
28 .flat_map(|captions| captions.cues.iter().map(|cue| cue.timeline_range)),
29 )
30 });
31 let first = ranges.next()?;
32 let (start, end) = ranges.fold((first.start(), first.end()), |(start, end), range| {
33 (start.min(range.start()), end.max(range.end()))
34 });
35 TimeRange::new(start, end).ok()
36 }
37
38 #[must_use]
41 pub fn any_solo(&self) -> bool {
42 self.tracks.iter().any(|track| track.audio.solo)
43 }
44
45 #[must_use]
48 pub fn matte_sources(&self) -> std::collections::HashSet<&ClipId> {
49 self.tracks
50 .iter()
51 .flat_map(|track| &track.clips)
52 .filter_map(|clip| clip.matte.as_ref().map(|matte| &matte.source))
53 .collect()
54 }
55}
56
57#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
59#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
60#[serde(deny_unknown_fields)]
61#[non_exhaustive]
62pub struct Track {
63 pub id: TrackId,
65 pub name: String,
67 pub kind: TrackKind,
69 #[serde(default)]
72 pub clips: Vec<Clip>,
73 #[serde(default)]
75 pub transitions: Vec<ClipTransition>,
76 #[serde(default)]
78 pub audio: TrackAudio,
79 #[serde(default)]
81 pub captions: Option<Captions>,
82}
83
84impl Track {
85 #[must_use]
87 pub fn new(id: TrackId, name: impl Into<String>, kind: TrackKind) -> Self {
88 Self {
89 id,
90 name: name.into(),
91 kind,
92 clips: Vec::new(),
93 transitions: Vec::new(),
94 audio: TrackAudio::default(),
95 captions: (kind == TrackKind::Caption).then(Captions::default),
96 }
97 }
98}
99
100#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
102#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
103#[serde(rename_all = "snake_case")]
104#[non_exhaustive]
105pub enum TrackKind {
106 Video,
108 Audio,
110 Caption,
112}
113
114impl TrackKind {
115 pub(crate) const fn accepts(self, source: MediaKind) -> bool {
116 matches!(
117 (self, source),
118 (
119 Self::Video,
120 MediaKind::Video | MediaKind::Image | MediaKind::Generated | MediaKind::Composition
121 ) | (Self::Audio, MediaKind::Audio)
122 )
123 }
124}
125
126#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
128#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
129#[serde(deny_unknown_fields)]
130#[non_exhaustive]
131pub struct Clip {
132 pub id: ClipId,
134 pub source_id: SourceId,
136 pub timeline_range: TimeRange,
138 pub source_range: TimeRange,
140 #[serde(default)]
142 pub visual: VisualProperties,
143 #[serde(default)]
146 pub composition: Option<ClipComposition>,
147 #[serde(default)]
149 pub blend_mode: ClipBlendMode,
150 #[serde(default)]
153 pub masks: Vec<ShapeMask>,
154 #[serde(default)]
156 pub matte: Option<Matte>,
157 #[serde(default)]
159 pub time_map: TimeMap,
160 #[serde(default)]
162 pub audio: AudioProperties,
163 #[serde(default, skip_serializing_if = "CacheIntent::is_auto")]
166 pub cache: CacheIntent,
167}
168
169#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
171#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
172#[serde(rename_all = "snake_case")]
173pub enum CacheIntent {
174 #[default]
176 Auto,
177 Pin,
179 Bypass,
181}
182
183impl CacheIntent {
184 #[must_use]
186 pub const fn is_auto(&self) -> bool {
187 matches!(self, Self::Auto)
188 }
189}
190
191impl Clip {
192 #[must_use]
195 pub fn stretches_audio(&self) -> bool {
196 self.audio.preserve_pitch
197 && matches!(self.time_map, TimeMap::Forward | TimeMap::Reverse)
198 && self.timeline_range.duration() != self.source_range.duration()
199 }
200
201 #[must_use]
203 pub fn new(
204 id: ClipId,
205 source_id: SourceId,
206 timeline_range: TimeRange,
207 source_range: TimeRange,
208 ) -> Self {
209 Self {
210 id,
211 source_id,
212 timeline_range,
213 source_range,
214 visual: VisualProperties::default(),
215 composition: None,
216 blend_mode: ClipBlendMode::default(),
217 masks: Vec::new(),
218 matte: None,
219 time_map: TimeMap::Forward,
220 audio: AudioProperties::default(),
221 cache: CacheIntent::Auto,
222 }
223 }
224}
225
226#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
228#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
229#[serde(deny_unknown_fields)]
230pub struct ClipTransition {
231 pub from_clip: ClipId,
233 pub to_clip: ClipId,
235 pub timeline_range: TimeRange,
237 pub easing: Easing,
240 pub effect: ClipEffect,
242}