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