1use alpha2_media::SourceId;
21use alpha2_model::{
22 Animated, ClipBlendMode, ClipEffect, Composition, CompositionId, CompositionReads, EffectValue,
23 Generator, Keyframe, MAX_COMPOSITION_DEPTH, MAX_INLINED_NODES, MaskShape, MatteMode, MediaKind,
24 MediaSource, NodeId, NodeOp, Paint, ShapeMask, VisualProperties,
25};
26use alpha2_time::MediaTime;
27use std::collections::HashMap;
28use std::sync::Arc;
29
30pub const MAX_PLAN_BYTES: u64 = 256 * 1024 * 1024;
39
40#[derive(Clone, Debug, PartialEq)]
42pub struct CompositionPlan {
43 pub id: CompositionId,
45 pub steps: Vec<Step>,
47 pub output: usize,
49}
50
51impl CompositionPlan {
52 pub fn effects(&self) -> impl Iterator<Item = &ClipEffect> {
54 self.steps.iter().filter_map(|step| match &step.op {
55 StepOp::Effect { effect, .. } | StepOp::Transition { effect, .. } => Some(effect),
56 _ => None,
57 })
58 }
59
60 #[must_use]
62 pub fn uses_clip(&self) -> bool {
63 self.steps
64 .iter()
65 .any(|step| matches!(step.op, StepOp::Clip))
66 }
67}
68
69#[derive(Clone, Debug, PartialEq)]
71pub struct Step {
72 pub node: NodeId,
75 pub op: StepOp,
77 pub offset: MediaTime,
80}
81
82impl Step {
83 #[must_use]
86 pub fn time(&self, time: MediaTime) -> MediaTime {
87 MediaTime(time.ticks().saturating_add(self.offset.ticks()))
88 }
89}
90
91#[derive(Clone, Debug, PartialEq)]
93pub enum StepOp {
94 Clip,
96 Media {
98 source_id: SourceId,
100 start: MediaTime,
102 },
103 Generator(Generator),
106 Effect {
108 input: usize,
110 effect: ClipEffect,
112 },
113 Transition {
115 from: usize,
117 to: usize,
119 effect: ClipEffect,
121 progress: Animated<f32>,
123 },
124 Transform {
126 input: usize,
128 transform: VisualProperties,
130 },
131 Merge {
133 layers: Vec<StepLayer>,
135 },
136 Matte {
138 input: usize,
140 matte: usize,
142 mode: MatteMode,
144 },
145 ShapeMask {
147 input: usize,
149 masks: Vec<ShapeMask>,
151 },
152}
153
154#[derive(Clone, Debug, PartialEq)]
156pub struct StepLayer {
157 pub input: usize,
159 pub blend_mode: ClipBlendMode,
161 pub opacity: Animated<f32>,
163}
164
165#[derive(Clone, Debug, PartialEq, Eq)]
167pub enum PlanError {
168 NoOutput,
170 MissingInput {
172 node: NodeId,
174 input: String,
177 },
178 UnboundGroupInput {
181 node: NodeId,
183 },
184 ClipWithoutClip {
186 node: NodeId,
188 },
189 NestedTooDeep {
193 node: NodeId,
195 },
196 TooLarge,
200}
201
202#[derive(Debug)]
205pub(crate) struct PlanBudget {
206 left: u64,
207 exceeded: bool,
208}
209
210impl PlanBudget {
211 pub(crate) const fn new(bytes: u64) -> Self {
212 Self {
213 left: bytes,
214 exceeded: false,
215 }
216 }
217
218 pub(crate) const fn exceeded(&self) -> bool {
220 self.exceeded
221 }
222}
223
224pub(crate) struct Lookup<'a> {
227 compositions: HashMap<&'a CompositionId, &'a Composition>,
228 sources: HashMap<&'a SourceId, &'a MediaSource>,
229 reads: CompositionReads<'a>,
230}
231
232impl<'a> Lookup<'a> {
233 pub(crate) fn new(compositions: &'a [Composition], sources: &'a [MediaSource]) -> Self {
234 let compositions = compositions
235 .iter()
236 .map(|composition| (&composition.id, composition))
237 .collect();
238 let reads = CompositionReads::new(&compositions);
239 Self {
240 compositions,
241 sources: sources.iter().map(|source| (&source.id, source)).collect(),
242 reads,
243 }
244 }
245
246 pub(crate) const fn reads(&self) -> &CompositionReads<'a> {
248 &self.reads
249 }
250}
251
252pub fn plan_composition(
268 composition: &Composition,
269 compositions: &[Composition],
270 sources: &[MediaSource],
271) -> Result<CompositionPlan, PlanError> {
272 plan(
273 composition,
274 &Lookup::new(compositions, sources),
275 true,
276 &mut PlanBudget::new(MAX_PLAN_BYTES),
277 )
278}
279
280pub(crate) fn plan_composition_within(
283 composition: &Composition,
284 lookup: &Lookup<'_>,
285 budget: &mut PlanBudget,
286) -> Result<CompositionPlan, PlanError> {
287 plan(composition, lookup, true, budget)
288}
289
290pub fn plan_source_composition(
298 composition: &Composition,
299 compositions: &[Composition],
300 sources: &[MediaSource],
301) -> Result<CompositionPlan, PlanError> {
302 plan(
303 composition,
304 &Lookup::new(compositions, sources),
305 false,
306 &mut PlanBudget::new(MAX_PLAN_BYTES),
307 )
308}
309
310pub(crate) fn plan_source_composition_within(
313 composition: &Composition,
314 lookup: &Lookup<'_>,
315 budget: &mut PlanBudget,
316) -> Result<CompositionPlan, PlanError> {
317 plan(composition, lookup, false, budget)
318}
319
320fn plan<'a>(
321 composition: &'a Composition,
322 lookup: &Lookup<'a>,
323 clip_allowed: bool,
324 budget: &mut PlanBudget,
325) -> Result<CompositionPlan, PlanError> {
326 let mut planner = Planner {
327 lookup,
328 steps: Vec::new(),
329 budget,
330 };
331 let scope = Scope {
332 composition,
333 inputs: Vec::new(),
334 controls: HashMap::new(),
335 offset: MediaTime::ZERO,
336 prefix: String::new(),
337 clip_allowed,
338 group_inputs_bound: false,
339 depth: 1,
340 };
341 let output = planner.output(&scope)?;
342 Ok(CompositionPlan {
343 id: composition.id.clone(),
344 steps: planner.steps,
345 output,
346 })
347}
348
349struct Scope<'a> {
351 composition: &'a Composition,
352 inputs: Vec<Option<usize>>,
355 controls: HashMap<(&'a NodeId, &'a str), &'a Animated<EffectValue>>,
357 offset: MediaTime,
359 prefix: String,
361 clip_allowed: bool,
363 group_inputs_bound: bool,
365 depth: usize,
367}
368
369struct Planner<'a, 'b> {
370 lookup: &'b Lookup<'a>,
371 steps: Vec<Step>,
372 budget: &'b mut PlanBudget,
373}
374
375impl<'a> Planner<'a, '_> {
376 fn output(&mut self, scope: &Scope<'a>) -> Result<usize, PlanError> {
378 let output = scope
379 .composition
380 .output
381 .as_ref()
382 .ok_or(PlanError::NoOutput)?;
383 let nodes: HashMap<&NodeId, &'a alpha2_model::CompositionNode> = scope
384 .composition
385 .nodes
386 .iter()
387 .map(|node| (&node.id, node))
388 .collect();
389 self.resolve(scope, &nodes, output)
390 }
391
392 fn resolve(
402 &mut self,
403 scope: &Scope<'a>,
404 nodes: &HashMap<&NodeId, &'a alpha2_model::CompositionNode>,
405 id: &'a NodeId,
406 ) -> Result<usize, PlanError> {
407 let mut done: HashMap<&NodeId, usize> = HashMap::new();
408 let mut pending = vec![(id, false)];
410 while let Some((current, expanded)) = pending.pop() {
411 if done.contains_key(current) {
412 continue;
413 }
414 let node = nodes[current];
415 let needed = needed_inputs(self.lookup.reads(), &scope.prefix, node)?;
416 if !expanded {
417 pending.push((current, true));
418 pending.extend(
419 needed
420 .iter()
421 .rev()
422 .filter(|input| !done.contains_key(**input))
423 .map(|input| (*input, false)),
424 );
425 continue;
426 }
427 let step = if node.bypass {
428 done[needed[0]]
429 } else {
430 self.node_step(scope, node, &done)?
431 };
432 done.insert(current, step);
433 }
434 Ok(done[id])
435 }
436
437 fn node_step(
439 &mut self,
440 scope: &Scope<'a>,
441 node: &'a alpha2_model::CompositionNode,
442 done: &HashMap<&NodeId, usize>,
443 ) -> Result<usize, PlanError> {
444 let at = |input: &Option<NodeId>| {
445 input
446 .as_ref()
447 .map(|id| done[id])
448 .expect("needed inputs are connected and planned")
449 };
450 let id = || scoped(&scope.prefix, &node.id);
451 let op = match &node.op {
452 NodeOp::Clip {} => {
453 if !scope.clip_allowed {
454 return Err(PlanError::ClipWithoutClip { node: id() });
455 }
456 StepOp::Clip
457 }
458 NodeOp::GroupInput { index } => {
459 return usize::try_from(*index)
461 .ok()
462 .and_then(|index| scope.inputs.get(index).copied().flatten())
463 .filter(|_| scope.group_inputs_bound)
464 .ok_or_else(|| PlanError::UnboundGroupInput { node: id() });
465 }
466 NodeOp::Group {
467 composition,
468 inputs,
469 controls,
470 } => {
471 let reads = self.lookup.reads();
473 let inputs = inputs
474 .iter()
475 .enumerate()
476 .map(|(port, input)| reads.reads_port(composition, port).then(|| at(input)))
477 .collect();
478 return self.group(scope, &id(), composition, inputs, controls);
479 }
480 NodeOp::Media { source_id, start } => {
481 let source = self.lookup.sources.get(source_id).copied();
482 if let Some(composition) = source.and_then(|source| source.composition.as_ref()) {
483 return self.composition_source(scope, &id(), composition, *start);
484 }
485 let generator = source
486 .filter(|source| source.kind == MediaKind::Generated)
487 .and_then(|source| source.generator.clone());
488 generator.map_or_else(
489 || StepOp::Media {
490 source_id: source_id.clone(),
491 start: *start,
492 },
493 StepOp::Generator,
494 )
495 }
496 NodeOp::Generator { generator } => StepOp::Generator(generator.clone()),
497 NodeOp::Effect { input, effect } => StepOp::Effect {
498 input: at(input),
499 effect: controlled(scope, &node.id, effect),
500 },
501 NodeOp::Transition {
502 from,
503 to,
504 effect,
505 progress,
506 } => StepOp::Transition {
507 from: at(from),
508 to: at(to),
509 effect: controlled(scope, &node.id, effect),
510 progress: progress.clone(),
511 },
512 NodeOp::Transform { input, transform } => StepOp::Transform {
513 input: at(input),
514 transform: transform.clone(),
515 },
516 NodeOp::Merge { layers } => StepOp::Merge {
517 layers: layers
518 .iter()
519 .map(|layer| StepLayer {
520 input: at(&layer.input),
521 blend_mode: layer.blend_mode,
522 opacity: layer.opacity.clone(),
523 })
524 .collect(),
525 },
526 NodeOp::Matte { input, matte, mode } => StepOp::Matte {
527 input: at(input),
528 matte: at(matte),
529 mode: *mode,
530 },
531 NodeOp::ShapeMask { input, masks } => StepOp::ShapeMask {
532 input: at(input),
533 masks: masks.clone(),
534 },
535 };
536 self.push(Step {
537 node: id(),
538 op,
539 offset: scope.offset,
540 })
541 }
542
543 fn push(&mut self, step: Step) -> Result<usize, PlanError> {
546 if self.steps.len() >= MAX_INLINED_NODES {
547 return Err(PlanError::TooLarge);
548 }
549 let bytes = step_bytes(&step);
550 if bytes > self.budget.left {
551 self.budget.exceeded = true;
552 return Err(PlanError::TooLarge);
553 }
554 self.budget.left -= bytes;
555 self.steps.push(step);
556 Ok(self.steps.len() - 1)
557 }
558
559 fn group(
562 &mut self,
563 scope: &Scope<'a>,
564 node: &NodeId,
565 composition: &CompositionId,
566 inputs: Vec<Option<usize>>,
567 controls: &'a std::collections::BTreeMap<String, Animated<EffectValue>>,
568 ) -> Result<usize, PlanError> {
569 let target = self.composition(composition);
570 let depth = nested(scope, node)?;
571 let controls = target
572 .controls
573 .iter()
574 .filter_map(|control| {
575 controls
576 .get(&control.name)
577 .map(|value| ((&control.node, control.param.as_str()), value))
578 })
579 .collect();
580 self.output(&Scope {
581 composition: target,
582 inputs,
583 controls,
584 offset: scope.offset,
585 prefix: format!("{node}/"),
586 clip_allowed: scope.clip_allowed,
587 group_inputs_bound: true,
588 depth,
589 })
590 }
591
592 fn composition_source(
595 &mut self,
596 scope: &Scope<'a>,
597 node: &NodeId,
598 composition: &CompositionId,
599 start: MediaTime,
600 ) -> Result<usize, PlanError> {
601 let depth = nested(scope, node)?;
602 self.output(&Scope {
603 composition: self.composition(composition),
604 inputs: Vec::new(),
605 controls: HashMap::new(),
606 offset: MediaTime(scope.offset.ticks().saturating_add(start.ticks())),
607 prefix: format!("{node}/"),
608 clip_allowed: false,
609 group_inputs_bound: false,
610 depth,
611 })
612 }
613
614 fn composition(&self, id: &CompositionId) -> &'a Composition {
615 self.lookup
616 .compositions
617 .get(id)
618 .copied()
619 .expect("validation guarantees referenced compositions exist")
620 }
621}
622
623fn nested(scope: &Scope<'_>, node: &NodeId) -> Result<usize, PlanError> {
626 let depth = scope.depth + 1;
627 if depth > MAX_COMPOSITION_DEPTH {
628 return Err(PlanError::NestedTooDeep { node: node.clone() });
629 }
630 Ok(depth)
631}
632
633fn scoped(prefix: &str, id: &NodeId) -> NodeId {
635 if prefix.is_empty() {
636 return id.clone();
637 }
638 NodeId::new(format!("{prefix}{id}")).expect("a prefixed id is not empty")
639}
640
641fn controlled(scope: &Scope<'_>, node: &NodeId, effect: &ClipEffect) -> ClipEffect {
643 let mut effect = effect.clone();
644 for ((target, param), value) in &scope.controls {
645 if *target == node {
646 effect.params.insert((*param).to_owned(), (*value).clone());
647 }
648 }
649 effect
650}
651
652fn step_bytes(step: &Step) -> u64 {
654 let heap = match &step.op {
655 StepOp::Clip | StepOp::Matte { .. } => 0,
656 StepOp::Media { source_id, .. } => source_id.as_str().len(),
657 StepOp::Generator(generator) => generator_bytes(generator),
658 StepOp::Effect { effect, .. } => effect_bytes(effect),
659 StepOp::Transition {
660 effect, progress, ..
661 } => effect_bytes(effect) + keyframe_bytes(progress),
662 StepOp::Transform { transform, .. } => transform_bytes(transform),
663 StepOp::Merge { layers } => layers
664 .iter()
665 .map(|layer| size_of::<StepLayer>() + keyframe_bytes(&layer.opacity))
666 .sum(),
667 StepOp::ShapeMask { masks, .. } => masks.iter().map(mask_bytes).sum(),
668 };
669 u64::try_from(size_of::<Step>() + step.node.as_str().len() + heap).unwrap_or(u64::MAX)
670}
671
672fn keyframe_bytes<T>(value: &Animated<T>) -> usize {
674 match value {
675 Animated::Constant { .. } => 0,
676 Animated::Keyframes { keyframes } => keyframes.len() * size_of::<Keyframe<T>>(),
677 }
678}
679
680fn effect_bytes(effect: &ClipEffect) -> usize {
681 effect.id.len()
682 + effect
683 .params
684 .iter()
685 .map(|(name, value)| {
686 name.len() + size_of::<Animated<EffectValue>>() + keyframe_bytes(value)
687 })
688 .sum::<usize>()
689 + effect
690 .resources
691 .iter()
692 .map(|(name, id)| name.len() + id.as_str().len())
693 .sum::<usize>()
694}
695
696fn transform_bytes(transform: &VisualProperties) -> usize {
697 keyframe_bytes(&transform.position)
698 + keyframe_bytes(&transform.scale)
699 + keyframe_bytes(&transform.rotation_degrees)
700 + keyframe_bytes(&transform.opacity)
701}
702
703fn mask_bytes(mask: &ShapeMask) -> usize {
704 let corner = match &mask.shape {
705 MaskShape::Rectangle { corner_radius } => keyframe_bytes(corner_radius),
706 MaskShape::Ellipse => 0,
707 };
708 size_of::<ShapeMask>()
709 + keyframe_bytes(&mask.center)
710 + keyframe_bytes(&mask.size)
711 + keyframe_bytes(&mask.rotation_degrees)
712 + keyframe_bytes(&mask.feather)
713 + corner
714}
715
716fn generator_bytes(generator: &Generator) -> usize {
717 let paint = |paint: &Paint| match paint {
718 Paint::Solid { .. } => 0,
719 Paint::LinearGradient { stops, .. }
720 | Paint::RadialGradient { stops, .. }
721 | Paint::ConicGradient { stops, .. } => {
722 stops.len() * size_of::<alpha2_model::GradientStop>()
723 }
724 };
725 match generator {
726 Generator::Fill { paint: fill } => paint(fill),
727 Generator::Shape { fill, stroke, .. } => {
728 fill.as_ref().map_or(0, paint)
729 + stroke.as_ref().map_or(0, |stroke| paint(&stroke.paint))
730 }
731 Generator::Text { text, style } => {
732 text.len() + style.font_family.as_ref().map_or(0, String::len)
733 }
734 }
735}
736
737fn needed_inputs<'a>(
742 reads: &CompositionReads<'_>,
743 prefix: &str,
744 node: &'a alpha2_model::CompositionNode,
745) -> Result<Vec<&'a NodeId>, PlanError> {
746 let needed = reads.needed_inputs(node);
747 if node.bypass && needed.is_empty() {
748 return Err(PlanError::MissingInput {
750 node: scoped(prefix, &node.id),
751 input: "layers[0]".into(),
752 });
753 }
754 needed
755 .into_iter()
756 .map(|input| {
757 input.node().ok_or_else(|| PlanError::MissingInput {
758 node: scoped(prefix, &node.id),
759 input: input.name(),
760 })
761 })
762 .collect()
763}
764
765#[derive(Clone, Debug)]
767pub(crate) struct IndexedComposition {
768 pub(crate) plan: Arc<Result<CompositionPlan, PlanError>>,
769 pub(crate) reference: alpha2_model::ClipComposition,
770}
771
772#[derive(Clone, Copy, Debug, PartialEq)]
774pub struct ActiveComposition<'a> {
775 plan: &'a CompositionPlan,
776 time: MediaTime,
777}
778
779impl<'a> ActiveComposition<'a> {
780 #[must_use]
782 pub const fn new(plan: &'a CompositionPlan, time: MediaTime) -> Self {
783 Self { plan, time }
784 }
785
786 #[must_use]
788 pub const fn plan(&self) -> &'a CompositionPlan {
789 self.plan
790 }
791
792 #[must_use]
794 pub const fn time(&self) -> MediaTime {
795 self.time
796 }
797
798 pub fn media(&self) -> impl Iterator<Item = (usize, &'a SourceId, MediaTime)> + 'a {
801 let time = self.time;
802 self.plan
803 .steps
804 .iter()
805 .enumerate()
806 .filter_map(move |(index, step)| match &step.op {
807 StepOp::Media { source_id, start } => Some((
808 index,
809 source_id,
810 MediaTime(start.ticks().saturating_add(step.time(time).ticks())),
811 )),
812 _ => None,
813 })
814 }
815}
816
817#[cfg(test)]
818#[path = "composition_tests.rs"]
819mod tests;