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 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(&project.compositions, &project.sources);
37 let (plans, source_plans) = compile_plans(project, &lookup);
38 let sources = project
39 .sources
40 .iter()
41 .map(|source| (&source.id, source))
42 .collect::<std::collections::HashMap<_, _>>();
43 for (track_order, track) in project.timeline.tracks.iter().enumerate() {
44 for (clip_order, clip) in track.clips.iter().enumerate() {
45 let source = sources.get(&clip.source_id).copied();
47 let source_is_audible = source.is_some_and(alpha2_model::MediaSource::is_audible);
48 let generator = source.and_then(|source| source.generator.clone());
49 let drawn = clip
52 .composition
53 .as_ref()
54 .is_none_or(|reference| lookup.reads().draws_clip(&reference.id));
55 let source_composition = source
56 .filter(|_| drawn)
57 .and_then(|source| source_composition_of(source, &source_plans));
58 entries.push(IndexedClip {
59 clip_id: clip.id.clone(),
60 source_id: clip.source_id.clone(),
61 track_id: track.id.clone(),
62 track_kind: track.kind,
63 timeline_range: clip.timeline_range,
64 source_range: clip.source_range,
65 time_map: clip.time_map,
66 visual: clip.visual.clone(),
67 composition: clip.composition.as_ref().and_then(|reference| {
69 plans
70 .get(&reference.id)
71 .map(|plan| crate::IndexedComposition {
72 plan: std::sync::Arc::clone(plan),
73 reference: reference.clone(),
74 })
75 }),
76 blend_mode: clip.blend_mode,
77 cache: clip.cache,
78 masks: clip.masks.clone(),
79 matte: clip.matte.clone(),
80 is_matte: matte_sources.contains(&clip.id),
81 audio: clip.audio.clone(),
82 source_is_audible,
83 generator,
84 source_composition,
85 track_order,
86 clip_order,
87 });
88 }
89 transitions.extend(
90 track
91 .transitions
92 .iter()
93 .map(|transition| IndexedTransition {
94 from_clip: transition.from_clip.clone(),
95 to_clip: transition.to_clip.clone(),
96 timeline_range: transition.timeline_range,
97 easing: transition.easing,
98 effect: transition.effect.clone(),
99 track_order,
100 }),
101 );
102 }
103 let captions = project
104 .timeline
105 .tracks
106 .iter()
107 .enumerate()
108 .filter_map(|(track_order, track)| {
109 track.captions.as_ref().map(|captions| IndexedCaptions {
110 track_id: track.id.clone(),
111 track_order,
112 captions: captions.clone(),
113 })
114 })
115 .collect();
116 entries.sort_unstable_by_key(|entry| {
117 (
118 entry.timeline_range.start(),
119 entry.track_order,
120 entry.clip_order,
121 )
122 });
123 let max_active = deepest_overlap(&entries);
124 let (nodes, root) = build_tree(entries);
125 transitions.sort_unstable_by_key(|transition| {
126 (transition.timeline_range.start(), transition.track_order)
127 });
128 Ok(Self {
129 project_id: project.id.clone(),
130 nodes,
131 root,
132 transitions,
133 captions,
134 max_active,
135 })
136 }
137
138 #[must_use]
140 pub fn project_id(&self) -> &ProjectId {
141 &self.project_id
142 }
143
144 #[must_use]
146 pub fn len(&self) -> usize {
147 self.nodes.len()
148 }
149
150 #[must_use]
152 pub fn is_empty(&self) -> bool {
153 self.nodes.is_empty()
154 }
155
156 pub fn effects(&self) -> impl Iterator<Item = &alpha2_model::ClipEffect> {
163 let mut seen = std::collections::HashSet::new();
164 let plans = self
165 .nodes
166 .iter()
167 .flat_map(|node| clip_plans(node.entry()))
168 .flatten()
169 .filter(|slot| seen.insert(std::sync::Arc::as_ptr(slot)))
170 .filter_map(|slot| slot.as_ref().as_ref().ok())
171 .collect::<Vec<_>>();
172 plans
173 .into_iter()
174 .flat_map(crate::CompositionPlan::effects)
175 .chain(self.transitions.iter().map(|transition| &transition.effect))
176 }
177
178 #[must_use]
190 pub fn peak_source_reads(
191 &self,
192 range: TimeRange,
193 mut counted: impl FnMut(&SourceId) -> bool,
194 ) -> usize {
195 let mut plan_reads = std::collections::HashMap::new();
197 let mut events = Vec::new();
198 for entry in self.nodes.iter().map(IntervalNode::entry) {
199 let Some(span) = entry.timeline_range.intersection(range) else {
200 continue;
201 };
202 let [composition, source_composition] = clip_plans(entry);
203 let draws_source = entry.generator.is_none()
204 && source_composition.is_none()
205 && composition.is_none_or(|slot| {
206 slot.as_ref()
207 .as_ref()
208 .is_ok_and(crate::CompositionPlan::uses_clip)
209 });
210 let mut reads = usize::from(draws_source && 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
243fn clip_plans(entry: &IndexedClip) -> [Option<&PlanSlot>; 2] {
246 [
247 entry
248 .composition
249 .as_ref()
250 .map(|composition| &composition.plan),
251 entry.source_composition.as_ref().map(|(_, plan)| plan),
252 ]
253}
254
255type Plans<'a> = std::collections::HashMap<&'a CompositionId, PlanSlot>;
258
259fn compile_plans<'a>(
265 project: &'a Project,
266 lookup: &crate::composition::Lookup<'a>,
267) -> (Plans<'a>, Plans<'a>) {
268 let shown = project
269 .timeline
270 .tracks
271 .iter()
272 .flat_map(|track| &track.clips)
273 .filter_map(|clip| clip.composition.as_ref().map(|reference| &reference.id))
274 .collect::<std::collections::HashSet<_>>();
275 let sourced = project
277 .sources
278 .iter()
279 .filter_map(|source| source.composition.as_ref())
280 .collect::<std::collections::HashSet<_>>();
281 let mut budget = crate::composition::PlanBudget::new(crate::MAX_PLAN_BYTES);
282 let mut plans = Plans::new();
283 let mut source_plans = Plans::new();
284 for composition in &project.compositions {
285 if shown.contains(&composition.id) {
286 let plan =
287 crate::composition::plan_composition_within(composition, lookup, &mut budget);
288 plans.insert(&composition.id, std::sync::Arc::new(plan));
289 }
290 if sourced.contains(&composition.id) {
291 let plan = crate::composition::plan_source_composition_within(
292 composition,
293 lookup,
294 &mut budget,
295 );
296 source_plans.insert(&composition.id, std::sync::Arc::new(plan));
297 }
298 }
299 if budget.exceeded() {
300 let refused = std::sync::Arc::new(Err(crate::PlanError::TooLarge));
301 for plan in plans.values_mut().chain(source_plans.values_mut()) {
302 *plan = std::sync::Arc::clone(&refused);
303 }
304 }
305 (plans, source_plans)
306}
307
308fn source_composition_of(
310 source: &alpha2_model::MediaSource,
311 plans: &Plans<'_>,
312) -> Option<(CompositionId, PlanSlot)> {
313 let id = source.composition.as_ref()?;
314 Some((id.clone(), std::sync::Arc::clone(plans.get(id)?)))
315}
316
317pub(crate) type PlanSlot = std::sync::Arc<Result<crate::CompositionPlan, crate::PlanError>>;
319
320fn deepest_overlap(entries: &[IndexedClip]) -> usize {
322 let mut edges: Vec<_> = entries
325 .iter()
326 .flat_map(|entry| {
327 [
328 (entry.timeline_range.start(), 1_i8),
329 (entry.timeline_range.end(), -1),
330 ]
331 })
332 .collect();
333 edges.sort_unstable();
334 let mut depth = 0_usize;
335 let mut deepest = 0;
336 for (_, step) in edges {
337 depth = depth.saturating_add_signed(isize::from(step));
338 deepest = deepest.max(depth);
339 }
340 deepest
341}