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(&project.compositions, &project.sources);
38 let (plans, source_plans) = compile_plans(project, &lookup);
39 let sources = project
40 .sources
41 .iter()
42 .map(|source| (&source.id, source))
43 .collect::<std::collections::HashMap<_, _>>();
44 let shapes = shape_plans(project);
45 for (track_order, track) in project.timeline.tracks.iter().enumerate() {
46 for (clip_order, clip) in track.clips.iter().enumerate() {
47 let source = sources.get(&clip.source_id).copied();
49 let source_is_audible = source.is_some_and(alpha2_model::MediaSource::is_audible);
50 let generator = source.and_then(|source| source.generator.clone());
51 let drawn = clip
54 .composition
55 .as_ref()
56 .is_none_or(|reference| lookup.reads().draws_clip(&reference.id));
57 let source_composition = source
58 .filter(|_| drawn)
59 .and_then(|source| source_composition_of(source, &source_plans));
60 entries.push(IndexedClip {
61 clip_id: clip.id.clone(),
62 source_id: clip.source_id.clone(),
63 track_id: track.id.clone(),
64 track_kind: track.kind,
65 timeline_range: clip.timeline_range,
66 source_range: clip.source_range,
67 time_map: clip.time_map,
68 visual: clip.visual.clone(),
69 composition: clip.composition.as_ref().and_then(|reference| {
71 plans
72 .get(&reference.id)
73 .map(|plan| crate::IndexedComposition {
74 plan: std::sync::Arc::clone(plan),
75 reference: reference.clone(),
76 })
77 }),
78 blend_mode: clip.blend_mode,
79 cache: clip.cache,
80 masks: clip.masks.clone(),
81 matte: clip.matte.clone(),
82 is_matte: matte_sources.contains(&clip.id),
83 audio: clip.audio.clone(),
84 source_is_audible,
85 generator,
86 shapes: shapes.get(&clip.source_id).cloned(),
87 source_composition,
88 track_order,
89 clip_order,
90 });
91 }
92 transitions.extend(
93 track
94 .transitions
95 .iter()
96 .map(|transition| IndexedTransition {
97 from_clip: transition.from_clip.clone(),
98 to_clip: transition.to_clip.clone(),
99 timeline_range: transition.timeline_range,
100 easing: transition.easing,
101 effect: transition.effect.clone(),
102 track_order,
103 }),
104 );
105 }
106 let captions = project
107 .timeline
108 .tracks
109 .iter()
110 .enumerate()
111 .filter_map(|(track_order, track)| {
112 track.captions.as_ref().map(|captions| IndexedCaptions {
113 track_id: track.id.clone(),
114 track_order,
115 captions: 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> {
166 let mut seen = std::collections::HashSet::new();
167 let plans = self
168 .nodes
169 .iter()
170 .flat_map(|node| clip_plans(node.entry()))
171 .flatten()
172 .filter(|slot| seen.insert(std::sync::Arc::as_ptr(slot)))
173 .filter_map(|slot| slot.as_ref().as_ref().ok())
174 .collect::<Vec<_>>();
175 plans
176 .into_iter()
177 .flat_map(crate::CompositionPlan::effects)
178 .chain(self.transitions.iter().map(|transition| &transition.effect))
179 }
180
181 #[must_use]
193 pub fn peak_source_reads(
194 &self,
195 range: TimeRange,
196 mut counted: impl FnMut(&SourceId) -> bool,
197 ) -> usize {
198 let mut plan_reads = std::collections::HashMap::new();
200 let mut events = Vec::new();
201 for entry in self.nodes.iter().map(IntervalNode::entry) {
202 let Some(span) = entry.timeline_range.intersection(range) else {
203 continue;
204 };
205 let [composition, source_composition] = clip_plans(entry);
206 let draws_source = entry.generator.is_none()
207 && source_composition.is_none()
208 && composition.is_none_or(|slot| {
209 slot.as_ref()
210 .as_ref()
211 .is_ok_and(crate::CompositionPlan::uses_clip)
212 });
213 let mut reads = usize::from(draws_source && counted(&entry.source_id));
214 for slot in [composition, source_composition].into_iter().flatten() {
215 reads += *plan_reads
216 .entry(std::sync::Arc::as_ptr(slot))
217 .or_insert_with(|| {
218 slot.as_ref().as_ref().map_or(0, |plan| {
219 plan.steps
220 .iter()
221 .filter(|step| match &step.op {
222 crate::StepOp::Media { source_id, .. } => counted(source_id),
223 _ => false,
224 })
225 .count()
226 })
227 });
228 }
229 if reads > 0 {
230 let delta = isize::try_from(reads).unwrap_or(isize::MAX);
231 events.extend([(span.start(), delta), (span.end(), -delta)]);
232 }
233 }
234 events.sort_unstable_by_key(|&(time, delta)| (time, delta.signum()));
236 let mut active = 0_usize;
237 let mut peak = 0;
238 for (_, delta) in events {
239 active = active.saturating_add_signed(delta);
240 peak = peak.max(active);
241 }
242 peak
243 }
244}
245
246fn clip_plans(entry: &IndexedClip) -> [Option<&PlanSlot>; 2] {
249 [
250 entry
251 .composition
252 .as_ref()
253 .map(|composition| &composition.plan),
254 entry.source_composition.as_ref().map(|(_, plan)| plan),
255 ]
256}
257
258type Plans<'a> = std::collections::HashMap<&'a CompositionId, PlanSlot>;
261
262fn compile_plans<'a>(
268 project: &'a Project,
269 lookup: &crate::composition::Lookup<'a>,
270) -> (Plans<'a>, Plans<'a>) {
271 let shown = project
272 .timeline
273 .tracks
274 .iter()
275 .flat_map(|track| &track.clips)
276 .filter_map(|clip| clip.composition.as_ref().map(|reference| &reference.id))
277 .collect::<std::collections::HashSet<_>>();
278 let sourced = project
280 .sources
281 .iter()
282 .filter_map(|source| source.composition.as_ref())
283 .collect::<std::collections::HashSet<_>>();
284 let mut budget = crate::composition::PlanBudget::new(crate::MAX_PLAN_BYTES);
285 let mut plans = Plans::new();
286 let mut source_plans = Plans::new();
287 for composition in &project.compositions {
288 if shown.contains(&composition.id) {
289 let plan =
290 crate::composition::plan_composition_within(composition, lookup, &mut budget);
291 plans.insert(&composition.id, std::sync::Arc::new(plan));
292 }
293 if sourced.contains(&composition.id) {
294 let plan = crate::composition::plan_source_composition_within(
295 composition,
296 lookup,
297 &mut budget,
298 );
299 source_plans.insert(&composition.id, std::sync::Arc::new(plan));
300 }
301 }
302 if budget.exceeded() {
303 let refused = std::sync::Arc::new(Err(crate::PlanError::TooLarge));
304 for plan in plans.values_mut().chain(source_plans.values_mut()) {
305 *plan = std::sync::Arc::clone(&refused);
306 }
307 }
308 (plans, source_plans)
309}
310
311fn source_composition_of(
313 source: &alpha2_model::MediaSource,
314 plans: &Plans<'_>,
315) -> Option<(CompositionId, PlanSlot)> {
316 let id = source.composition.as_ref()?;
317 Some((id.clone(), std::sync::Arc::clone(plans.get(id)?)))
318}
319
320pub(crate) type PlanSlot = std::sync::Arc<Result<crate::CompositionPlan, crate::PlanError>>;
322
323fn deepest_overlap(entries: &[IndexedClip]) -> usize {
325 let mut edges: Vec<_> = entries
328 .iter()
329 .flat_map(|entry| {
330 [
331 (entry.timeline_range.start(), 1_i8),
332 (entry.timeline_range.end(), -1),
333 ]
334 })
335 .collect();
336 edges.sort_unstable();
337 let mut depth = 0_usize;
338 let mut deepest = 0;
339 for (_, step) in edges {
340 depth = depth.saturating_add_signed(isize::from(step));
341 deepest = deepest.max(depth);
342 }
343 deepest
344}
345
346fn shape_plans(
348 project: &Project,
349) -> std::collections::HashMap<&SourceId, std::sync::Arc<crate::ShapesPlan>> {
350 project
351 .sources
352 .iter()
353 .filter_map(|source| match &source.generator {
354 Some(alpha2_model::Generator::Shapes { items }) => Some((
355 &source.id,
356 std::sync::Arc::new(crate::ShapesPlan::new(items)),
357 )),
358 _ => None,
359 })
360 .collect()
361}