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]
24 pub fn extent(&self) -> Option<TimeRange> {
25 let mut ranges = self.tracks.iter().flat_map(|track| {
26 track.clips.iter().map(|clip| clip.timeline_range).chain(
27 track
28 .captions
29 .iter()
30 .flat_map(|captions| captions.cues.iter().map(|cue| cue.timeline_range)),
31 )
32 });
33 let first = ranges.next()?;
34 let (start, end) = ranges.fold((first.start(), first.end()), |(start, end), range| {
35 (start.min(range.start()), end.max(range.end()))
36 });
37 TimeRange::new(start, end).ok()
38 }
39
40 #[must_use]
43 pub fn any_solo(&self) -> bool {
44 self.tracks.iter().any(|track| track.audio.solo)
45 }
46
47 #[must_use]
51 pub fn matte_sources(&self) -> std::collections::HashSet<&ClipId> {
52 self.tracks
53 .iter()
54 .flat_map(|track| &track.clips)
55 .filter_map(|clip| clip.matte.as_ref().map(|matte| &matte.source))
56 .collect()
57 }
58}
59
60#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
62#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
63#[serde(deny_unknown_fields)]
64#[non_exhaustive]
65pub struct Track {
66 pub id: TrackId,
68 pub name: String,
70 pub kind: TrackKind,
72 #[serde(default)]
75 pub clips: Vec<Clip>,
76 #[serde(default)]
78 pub transitions: Vec<ClipTransition>,
79 #[serde(default)]
81 pub audio: TrackAudio,
82 #[serde(default)]
84 pub captions: Option<Captions>,
85}
86
87impl Track {
88 #[must_use]
90 pub fn new(id: TrackId, name: impl Into<String>, kind: TrackKind) -> Self {
91 Self {
92 id,
93 name: name.into(),
94 kind,
95 clips: Vec::new(),
96 transitions: Vec::new(),
97 audio: TrackAudio::default(),
98 captions: (kind == TrackKind::Caption).then(Captions::default),
99 }
100 }
101}
102
103#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
105#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
106#[serde(rename_all = "snake_case")]
107#[non_exhaustive]
108pub enum TrackKind {
109 Video,
111 Audio,
113 Caption,
115}
116
117impl TrackKind {
118 pub(crate) const fn accepts(self, source: MediaKind) -> bool {
119 matches!(
120 (self, source),
121 (
122 Self::Video,
123 MediaKind::Video | MediaKind::Image | MediaKind::Generated | MediaKind::Composition
124 ) | (Self::Audio, MediaKind::Audio)
125 )
126 }
127}
128
129#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
131#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
132#[serde(deny_unknown_fields)]
133#[non_exhaustive]
134pub struct Clip {
135 pub id: ClipId,
137 pub source_id: SourceId,
139 pub timeline_range: TimeRange,
141 pub source_range: TimeRange,
143 #[serde(default)]
145 pub visual: VisualProperties,
146 #[serde(default)]
150 pub composition: Option<ClipComposition>,
151 #[serde(default)]
153 pub blend_mode: ClipBlendMode,
154 #[serde(default)]
158 pub masks: Vec<ShapeMask>,
159 #[serde(default)]
161 pub matte: Option<Matte>,
162 #[serde(default)]
164 pub time_map: TimeMap,
165 #[serde(default)]
167 pub audio: AudioProperties,
168 #[serde(default, skip_serializing_if = "CacheIntent::is_auto")]
172 pub cache: CacheIntent,
173}
174
175#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
177#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
178#[serde(rename_all = "snake_case")]
179pub enum CacheIntent {
180 #[default]
182 Auto,
183 Pin,
186 Bypass,
189}
190
191impl CacheIntent {
192 #[must_use]
194 pub const fn is_auto(&self) -> bool {
195 matches!(self, Self::Auto)
196 }
197}
198
199impl Clip {
200 #[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}