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