1use crate::{EvalError, IndexedCaptions, IndexedClip, IndexedTransition, IntervalNode, build_tree};
8use alpha2_media::SourceId;
9use alpha2_model::{CompositionId, Project, ProjectId};
10use alpha2_time::TimeRange;
11
12#[derive(Clone, Debug)]
14pub struct ActiveIndex {
15 pub(crate) project_id: ProjectId,
16 pub(crate) nodes: Vec<IntervalNode>,
17 pub(crate) root: Option<usize>,
18 pub(crate) transitions: Vec<IndexedTransition>,
19 pub(crate) captions: Vec<IndexedCaptions>,
21 pub(crate) max_active: usize,
23}
24
25impl ActiveIndex {
26 #[allow(clippy::too_many_lines)]
32 pub fn build(project: &Project) -> Result<Self, EvalError> {
33 project.validate().map_err(EvalError::InvalidProject)?;
34 let mut entries = Vec::new();
35 let mut transitions = Vec::new();
36 let matte_sources = project.timeline.matte_sources();
37 let lookup = crate::composition::Lookup::new(
38 &project.compositions,
39 &project.sources,
40 (project.settings.width, project.settings.height),
41 );
42 let (plans, source_plans) = compile_plans(project, &lookup);
43 let sources = project
44 .sources
45 .iter()
46 .map(|source| (&source.id, source))
47 .collect::<std::collections::HashMap<_, _>>();
48 let generators = generator_plans(project);
49 for (track_order, track) in project.timeline.tracks.iter().enumerate() {
50 for (clip_order, clip) in track.clips.iter().enumerate() {
51 let source = sources.get(&clip.source_id).copied();
53 let source_is_audible = source.is_some_and(alpha2_model::MediaSource::is_audible);
54 let generator = source.and_then(|source| source.generator.clone());
55 let drawn = clip
58 .composition
59 .as_ref()
60 .is_none_or(|reference| lookup.reads().draws_clip(&reference.id));
61 let source_composition = source
62 .filter(|_| drawn)
63 .and_then(|source| source_composition_of(source, &source_plans));
64 entries.push(IndexedClip {
65 clip_id: clip.id.clone(),
66 source_id: clip.source_id.clone(),
67 track_id: track.id.clone(),
68 track_kind: track.kind,
69 timeline_range: clip.timeline_range,
70 source_range: clip.source_range,
71 time_map: clip.time_map,
72 visual: clip.visual.clone(),
73 composition: clip.composition.as_ref().and_then(|reference| {
75 plans
76 .get(&reference.id)
77 .map(|plan| crate::IndexedComposition {
78 plan: std::sync::Arc::clone(plan),
79 reference: reference.clone(),
80 })
81 }),
82 blend_mode: clip.blend_mode,
83 cache: clip.cache,
84 masks: clip.masks.clone(),
85 matte: clip.matte.clone(),
86 is_matte: matte_sources.contains(&clip.id),
87 audio: clip.audio.clone(),
88 source_is_audible,
89 generator,
90 plan: generators.get(&clip.source_id).cloned(),
91 source_composition,
92 track_order,
93 clip_order,
94 });
95 }
96 transitions.extend(
97 track
98 .transitions
99 .iter()
100 .map(|transition| IndexedTransition {
101 from_clip: transition.from_clip.clone(),
102 to_clip: transition.to_clip.clone(),
103 timeline_range: transition.timeline_range,
104 easing: transition.easing,
105 effect: transition.effect.clone(),
106 track_order,
107 }),
108 );
109 }
110 let captions = project
111 .timeline
112 .tracks
113 .iter()
114 .enumerate()
115 .filter_map(|(track_order, track)| {
116 track.captions.as_ref().map(|captions| IndexedCaptions {
117 track_id: track.id.clone(),
118 track_order,
119 captions: captions.clone(),
120 })
121 })
122 .collect();
123 entries.sort_unstable_by_key(|entry| {
124 (
125 entry.timeline_range.start(),
126 entry.track_order,
127 entry.clip_order,
128 )
129 });
130 let max_active = deepest_overlap(&entries);
131 let (nodes, root) = build_tree(entries);
132 transitions.sort_unstable_by_key(|transition| {
133 (transition.timeline_range.start(), transition.track_order)
134 });
135 Ok(Self {
136 project_id: project.id.clone(),
137 nodes,
138 root,
139 transitions,
140 captions,
141 max_active,
142 })
143 }
144
145 #[must_use]
147 pub fn project_id(&self) -> &ProjectId {
148 &self.project_id
149 }
150
151 #[must_use]
153 pub fn len(&self) -> usize {
154 self.nodes.len()
155 }
156
157 #[must_use]
159 pub fn is_empty(&self) -> bool {
160 self.nodes.is_empty()
161 }
162
163 pub fn effects(&self) -> impl Iterator<Item = &alpha2_model::ClipEffect> {
170 let mut seen = std::collections::HashSet::new();
171 let plans = self
172 .nodes
173 .iter()
174 .flat_map(|node| clip_plans(node.entry()))
175 .flatten()
176 .filter(|slot| seen.insert(std::sync::Arc::as_ptr(slot)))
177 .filter_map(|slot| slot.as_ref().as_ref().ok())
178 .collect::<Vec<_>>();
179 plans
180 .into_iter()
181 .flat_map(crate::CompositionPlan::effects)
182 .chain(self.transitions.iter().map(|transition| &transition.effect))
183 }
184
185 #[must_use]
197 pub fn peak_source_reads(
198 &self,
199 range: TimeRange,
200 mut counted: impl FnMut(&SourceId) -> bool,
201 ) -> usize {
202 let mut plan_reads = std::collections::HashMap::new();
204 let mut events = Vec::new();
205 for entry in self.nodes.iter().map(IntervalNode::entry) {
206 let Some(span) = entry.timeline_range.intersection(range) else {
207 continue;
208 };
209 let [composition, source_composition] = clip_plans(entry);
210 let mut reads = usize::from(draws_own_source(entry) && counted(&entry.source_id));
211 for slot in [composition, source_composition].into_iter().flatten() {
212 reads += *plan_reads
213 .entry(std::sync::Arc::as_ptr(slot))
214 .or_insert_with(|| {
215 slot.as_ref().as_ref().map_or(0, |plan| {
216 plan.steps
217 .iter()
218 .filter(|step| match &step.op {
219 crate::StepOp::Media { source_id, .. } => counted(source_id),
220 _ => false,
221 })
222 .count()
223 })
224 });
225 }
226 if reads > 0 {
227 let delta = isize::try_from(reads).unwrap_or(isize::MAX);
228 events.extend([(span.start(), delta), (span.end(), -delta)]);
229 }
230 }
231 events.sort_unstable_by_key(|&(time, delta)| (time, delta.signum()));
233 let mut active = 0_usize;
234 let mut peak = 0;
235 for (_, delta) in events {
236 active = active.saturating_add_signed(delta);
237 peak = peak.max(active);
238 }
239 peak
240 }
241
242 #[must_use]
254 pub fn source_reads(
255 &self,
256 range: TimeRange,
257 mut counted: impl FnMut(&SourceId) -> bool,
258 ) -> Vec<SourceRead> {
259 let mut reads = Vec::new();
260 for entry in self.nodes.iter().map(IntervalNode::entry) {
261 let Some(span) = entry.timeline_range.intersection(range) else {
262 continue;
263 };
264 let read = |source_id: &SourceId, reader| SourceRead {
265 source_id: source_id.clone(),
266 clip_id: entry.clip_id.clone(),
267 reader,
268 span,
269 };
270 if draws_own_source(entry) && counted(&entry.source_id) {
271 reads.push(read(&entry.source_id, None));
272 }
273 let [composition, source_composition] = clip_plans(entry);
274 for (slot, of_source) in [(composition, false), (source_composition, true)] {
275 let Some(Ok(plan)) = slot.map(|slot| slot.as_ref().as_ref()) else {
276 continue;
277 };
278 for step in &plan.steps {
279 let crate::StepOp::Media { source_id, .. } = &step.op else {
280 continue;
281 };
282 if !counted(source_id) {
283 continue;
284 }
285 let reader = if of_source {
286 alpha2_model::NodeId::new(format!("/{}", step.node)).ok()
287 } else {
288 Some(step.node.clone())
289 };
290 reads.push(read(source_id, reader));
291 }
292 }
293 }
294 reads
295 }
296}
297
298#[derive(Clone, Debug, Eq, PartialEq)]
300pub struct SourceRead {
301 pub source_id: SourceId,
303 pub clip_id: alpha2_model::ClipId,
305 pub reader: Option<alpha2_model::NodeId>,
308 pub span: TimeRange,
310}
311
312fn draws_own_source(entry: &IndexedClip) -> bool {
315 let [composition, source_composition] = clip_plans(entry);
316 entry.generator.is_none()
317 && source_composition.is_none()
318 && composition.is_none_or(|slot| {
319 slot.as_ref()
320 .as_ref()
321 .is_ok_and(crate::CompositionPlan::uses_clip)
322 })
323}
324
325fn clip_plans(entry: &IndexedClip) -> [Option<&PlanSlot>; 2] {
328 [
329 entry
330 .composition
331 .as_ref()
332 .map(|composition| &composition.plan),
333 entry.source_composition.as_ref().map(|(_, plan)| plan),
334 ]
335}
336
337type Plans<'a> = std::collections::HashMap<&'a CompositionId, PlanSlot>;
340
341fn compile_plans<'a>(
347 project: &'a Project,
348 lookup: &crate::composition::Lookup<'a>,
349) -> (Plans<'a>, Plans<'a>) {
350 let shown = project
351 .timeline
352 .tracks
353 .iter()
354 .flat_map(|track| &track.clips)
355 .filter_map(|clip| clip.composition.as_ref().map(|reference| &reference.id))
356 .collect::<std::collections::HashSet<_>>();
357 let sourced = project
359 .sources
360 .iter()
361 .filter_map(|source| source.composition.as_ref())
362 .collect::<std::collections::HashSet<_>>();
363 let mut budget = crate::composition::PlanBudget::new(crate::MAX_PLAN_BYTES);
364 let mut plans = Plans::new();
365 let mut source_plans = Plans::new();
366 for composition in &project.compositions {
367 if shown.contains(&composition.id) {
368 let plan =
369 crate::composition::plan_composition_within(composition, lookup, &mut budget);
370 plans.insert(&composition.id, std::sync::Arc::new(plan));
371 }
372 if sourced.contains(&composition.id) {
373 let plan = crate::composition::plan_source_composition_within(
374 composition,
375 lookup,
376 &mut budget,
377 );
378 source_plans.insert(&composition.id, std::sync::Arc::new(plan));
379 }
380 }
381 if budget.exceeded() {
382 let refused = std::sync::Arc::new(Err(crate::PlanError::TooLarge));
383 for plan in plans.values_mut().chain(source_plans.values_mut()) {
384 *plan = std::sync::Arc::clone(&refused);
385 }
386 }
387 (plans, source_plans)
388}
389
390fn source_composition_of(
392 source: &alpha2_model::MediaSource,
393 plans: &Plans<'_>,
394) -> Option<(CompositionId, PlanSlot)> {
395 let id = source.composition.as_ref()?;
396 Some((id.clone(), std::sync::Arc::clone(plans.get(id)?)))
397}
398
399pub(crate) type PlanSlot = std::sync::Arc<Result<crate::CompositionPlan, crate::PlanError>>;
401
402fn deepest_overlap(entries: &[IndexedClip]) -> usize {
404 let mut edges: Vec<_> = entries
407 .iter()
408 .flat_map(|entry| {
409 [
410 (entry.timeline_range.start(), 1_i8),
411 (entry.timeline_range.end(), -1),
412 ]
413 })
414 .collect();
415 edges.sort_unstable();
416 let mut depth = 0_usize;
417 let mut deepest = 0;
418 for (_, step) in edges {
419 depth = depth.saturating_add_signed(isize::from(step));
420 deepest = deepest.max(depth);
421 }
422 deepest
423}
424
425fn generator_plans(
428 project: &Project,
429) -> std::collections::HashMap<&SourceId, std::sync::Arc<crate::GeneratorPlan>> {
430 project
431 .sources
432 .iter()
433 .filter_map(|source| {
434 let plan = crate::GeneratorPlan::new(
435 source.generator.as_ref()?,
436 (project.settings.width, project.settings.height),
437 )?;
438 Some((&source.id, std::sync::Arc::new(plan)))
439 })
440 .collect()
441}