1use crate::{
2 ActiveCaption, ActiveClip, ActiveComposition, ActiveEffect, ActiveIndex, ActiveTransition,
3 EvalError, FrameDesc, FrameNeed, collect_active, map_source_time, sample_visual,
4};
5use alpha2_model::{Project, TrackKind};
6use alpha2_time::{FrameRate, MediaTime, TICKS_PER_SECOND, TimeRange};
7
8pub fn evaluate<'a>(
17 project: &Project,
18 index: &'a ActiveIndex,
19 time: MediaTime,
20) -> Result<FrameDesc<'a>, EvalError> {
21 ensure_index_matches(project, index)?;
22 let aspect = f64::from(project.settings.width) / f64::from(project.settings.height.max(1));
23 let mut active = Vec::with_capacity(index.max_active);
25 if let Some(root) = index.root {
26 collect_active(&index.nodes, root, time, &mut active);
27 }
28 active.sort_unstable_by_key(|entry| (entry.track_order, entry.clip_order));
29
30 let visual_count = active
31 .iter()
32 .filter(|entry| entry.track_kind == TrackKind::Video)
33 .count();
34 let audio_count = active
35 .iter()
36 .filter(|entry| entry.track_kind == TrackKind::Audio)
37 .count();
38 let captions = index
40 .captions
41 .iter()
42 .filter_map(|track| {
43 let cues = &track.captions.cues;
44 let candidate = cues.partition_point(|cue| cue.timeline_range.start() <= time);
47 let cue = cues[..candidate].last()?;
48 cue.timeline_range.contains(time).then(|| ActiveCaption {
49 track_id: &track.track_id,
50 text: &cue.text,
51 style: &track.captions.style,
52 layers_below: active
53 .iter()
54 .filter(|entry| {
55 entry.track_kind == TrackKind::Video
56 && entry.track_order < track.track_order
57 })
58 .count(),
59 })
60 })
61 .collect();
62 let mut visual = Vec::with_capacity(visual_count);
63 let mut audio = Vec::with_capacity(audio_count);
64 let frame_mirror = crate::Mirror::frame(project.settings.frame_rate);
65 for entry in active {
66 let item = active_clip(entry, frame_mirror, time, aspect)?;
67 match entry.track_kind {
68 TrackKind::Video => visual.push(item),
69 TrackKind::Audio => audio.push(item),
70 _ => {}
71 }
72 }
73 let frame_rate = project.settings.frame_rate;
74 let active_transitions = || {
75 index
76 .transitions
77 .iter()
78 .take_while(move |transition| transition.timeline_range.start() <= time)
79 .filter(move |transition| transition.timeline_range.contains(time))
80 };
81 let mut transitions = Vec::with_capacity(active_transitions().count());
82 transitions.extend(active_transitions().map(|transition| {
83 let linear_progress = centred_progress(transition.timeline_range, frame_rate, time);
84 #[allow(clippy::cast_possible_truncation)]
86 let progress = transition.easing.apply(linear_progress).clamp(0.0, 1.0) as f32;
87 ActiveTransition {
88 from_clip: &transition.from_clip,
89 to_clip: &transition.to_clip,
90 effect: ActiveEffect::new(&transition.effect, time),
91 progress,
92 }
93 }));
94 Ok(FrameDesc {
95 time,
96 background: project.settings.background,
97 blend_space: project.settings.blend_space,
98 visual,
99 audio,
100 transitions,
101 captions,
102 })
103}
104
105fn active_clip(
108 entry: &crate::IndexedClip,
109 frame_mirror: crate::Mirror,
110 time: MediaTime,
111 aspect: f64,
112) -> Result<ActiveClip<'_>, EvalError> {
113 let source_time = map_source_time(
114 entry.timeline_range,
115 entry.source_range,
116 entry.time_map,
117 frame_mirror,
118 time,
119 )
120 .ok_or_else(|| EvalError::UnmappableClip {
121 clip: entry.clip_id.clone(),
122 })?;
123 Ok(ActiveClip {
124 clip_id: &entry.clip_id,
125 source_id: &entry.source_id,
126 track_id: &entry.track_id,
127 source_time,
128 visual: (entry.track_kind == TrackKind::Video)
129 .then(|| sample_visual(&entry.visual, time, aspect)),
130 composition: active_composition(entry, time)?,
131 blend_mode: entry.blend_mode,
132 cache: entry.cache,
133 masks: &entry.masks,
134 matte: entry.matte.as_ref(),
135 is_matte: entry.is_matte,
136 generator: entry.generator.as_ref(),
137 shapes: entry.shapes.as_deref(),
138 source_composition: active_source_composition(entry, source_time)?,
139 })
140}
141
142fn active_source_composition(
144 entry: &crate::IndexedClip,
145 source_time: MediaTime,
146) -> Result<Option<ActiveComposition<'_>>, EvalError> {
147 entry
148 .source_composition
149 .as_ref()
150 .filter(|_| entry.track_kind == TrackKind::Video)
151 .map(|(id, plan)| {
152 plan.as_ref()
153 .as_ref()
154 .map(|plan| ActiveComposition::new(plan, source_time))
155 .map_err(|reason| EvalError::IncompleteComposition {
156 clip: entry.clip_id.clone(),
157 composition: id.clone(),
158 reason: reason.clone(),
159 })
160 })
161 .transpose()
162}
163
164fn active_composition(
166 entry: &crate::IndexedClip,
167 time: MediaTime,
168) -> Result<Option<ActiveComposition<'_>>, EvalError> {
169 let Some(indexed) = entry
170 .composition
171 .as_ref()
172 .filter(|_| entry.track_kind == TrackKind::Video)
173 else {
174 return Ok(None);
175 };
176 let plan =
177 indexed
178 .plan
179 .as_ref()
180 .as_ref()
181 .map_err(|reason| EvalError::IncompleteComposition {
182 clip: entry.clip_id.clone(),
183 composition: indexed.reference.id.clone(),
184 reason: reason.clone(),
185 })?;
186 let local = indexed
187 .reference
188 .local_time(entry.timeline_range.start(), time)
189 .ok_or_else(|| EvalError::UnmappableClip {
190 clip: entry.clip_id.clone(),
191 })?;
192 Ok(Some(ActiveComposition::new(plan, local)))
193}
194
195fn centred_progress(range: TimeRange, frame_rate: FrameRate, time: MediaTime) -> f64 {
203 let elapsed = i128::from(time.ticks()) - i128::from(range.start().ticks());
204 let duration = i128::from(range.end().ticks()) - i128::from(range.start().ticks());
205 let num = i128::from(frame_rate.numerator());
208 let den = i128::from(frame_rate.denominator());
209 let numerator = 2 * num * elapsed + i128::from(TICKS_PER_SECOND) * den;
210 let denominator = 2 * num * duration;
211 #[allow(clippy::cast_precision_loss)]
212 let progress = (numerator as f64 / denominator as f64).clamp(0.0, 1.0);
213 progress
214}
215
216pub fn frame_needs(
229 project: &Project,
230 index: &ActiveIndex,
231 time: MediaTime,
232) -> Result<Vec<FrameNeed>, EvalError> {
233 let frame = evaluate(project, index, time)?;
234 let mut needs = Needs::default();
235 for item in frame
236 .visual
237 .iter()
238 .filter(|item| frame.draws(item))
239 .chain(&frame.audio)
240 {
241 if item.needs_source() {
242 needs.ask(item.clip_id, item.source_id, item.source_time, || None);
243 }
244 let plans = item
245 .composition
246 .iter()
247 .map(|composition| (composition, false))
248 .chain(
249 item.source_composition
250 .iter()
251 .map(|composition| (composition, true)),
252 );
253 for (composition, of_source) in plans {
254 let steps = &composition.plan().steps;
255 for (step, source_id, time) in composition.media() {
256 let node = &steps[step].node;
257 needs.ask(item.clip_id, source_id, time, || {
260 if of_source {
261 alpha2_model::NodeId::new(format!("/{node}")).ok()
262 } else {
263 Some(node.clone())
264 }
265 });
266 }
267 }
268 }
269 Ok(needs.list)
270}
271
272pub(crate) const SCANNED_NEEDS: usize = 32;
278
279#[cfg(test)]
280thread_local! {
281 pub(crate) static COMPARED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
284}
285
286#[derive(Default)]
288struct Needs<'a> {
289 list: Vec<FrameNeed>,
290 hashed: std::collections::HashSet<(&'a alpha2_media::SourceId, MediaTime)>,
293}
294
295impl<'a> Needs<'a> {
296 fn ask(
300 &mut self,
301 clip_id: &alpha2_model::ClipId,
302 source_id: &'a alpha2_media::SourceId,
303 media_time: MediaTime,
304 reader: impl FnOnce() -> Option<alpha2_model::NodeId>,
305 ) {
306 let scanned = &self.list[..self.list.len().min(SCANNED_NEEDS)];
307 #[cfg(test)]
308 COMPARED.with(|compared| compared.set(compared.get() + scanned.len()));
309 let repeat = scanned
310 .iter()
311 .any(|need| need.media_time == media_time && need.source_id == *source_id)
312 || (self.list.len() > SCANNED_NEEDS && self.hashed.contains(&(source_id, media_time)));
313 if repeat {
314 return;
315 }
316 if self.list.len() >= SCANNED_NEEDS {
317 self.hashed.insert((source_id, media_time));
318 }
319 self.list.push(FrameNeed {
320 clip_id: clip_id.clone(),
321 reader: reader(),
322 source_id: source_id.clone(),
323 media_time,
324 });
325 }
326}
327
328pub(crate) fn ensure_index_matches(
330 project: &Project,
331 index: &ActiveIndex,
332) -> Result<(), EvalError> {
333 if project.id == index.project_id {
334 Ok(())
335 } else {
336 Err(EvalError::IndexProjectMismatch {
337 index_project: index.project_id.clone(),
338 evaluated_project: project.id.clone(),
339 })
340 }
341}