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