1use alpha2_media::SourceId;
12use alpha2_model::{
13 Animated, AnimatedPoint, ClipBlendMode, ClipEffect, Composition, CompositionId,
14 CompositionReads, EffectValue, Generator, Keyframe, MAX_COMPOSITION_DEPTH, MAX_INLINED_NODES,
15 MatteMode, MediaKind, MediaSource, NodeId, NodeOp, Paint, ShapeMask, VisualProperties,
16};
17use alpha2_time::MediaTime;
18use std::collections::{BTreeMap, HashMap};
19use std::sync::Arc;
20
21use crate::GeneratorPlan;
22
23pub const MAX_PLAN_BYTES: u64 = 256 * 1024 * 1024;
26
27#[derive(Clone, Debug, PartialEq)]
29pub struct CompositionPlan {
30 pub id: CompositionId,
32 pub steps: Vec<Step>,
34 pub output: usize,
36}
37
38impl CompositionPlan {
39 pub fn effects(&self) -> impl Iterator<Item = &ClipEffect> {
41 self.steps.iter().filter_map(|step| match &step.op {
42 StepOp::Effect { effect, .. } | StepOp::Transition { effect, .. } => Some(effect),
43 _ => None,
44 })
45 }
46
47 #[must_use]
49 pub fn uses_clip(&self) -> bool {
50 self.steps
51 .iter()
52 .any(|step| matches!(step.op, StepOp::Clip))
53 }
54}
55
56#[derive(Clone, Debug, PartialEq)]
58pub struct Step {
59 pub node: NodeId,
61 pub op: StepOp,
63 pub offset: MediaTime,
65}
66
67impl Step {
68 #[must_use]
70 pub fn time(&self, time: MediaTime) -> MediaTime {
71 MediaTime(time.ticks().saturating_add(self.offset.ticks()))
72 }
73}
74
75#[derive(Clone, Debug, PartialEq)]
77pub enum StepOp {
78 Clip,
80 Media {
82 source_id: SourceId,
84 start: MediaTime,
86 },
87 Generator {
89 generator: Generator,
91 start: MediaTime,
93 plan: Option<Arc<GeneratorPlan>>,
95 },
96 Effect {
98 input: usize,
100 inputs: Vec<(String, usize)>,
102 effect: ClipEffect,
104 },
105 Transition {
107 from: usize,
109 to: usize,
111 effect: ClipEffect,
113 progress: Animated<f32>,
115 },
116 Transform {
118 input: usize,
120 transform: VisualProperties,
122 },
123 Merge {
125 layers: Vec<StepLayer>,
127 },
128 Matte {
130 input: usize,
132 matte: usize,
134 mode: MatteMode,
136 },
137 ShapeMask {
139 input: usize,
141 masks: Vec<ShapeMask>,
143 },
144}
145
146#[derive(Clone, Debug, PartialEq)]
148pub struct StepLayer {
149 pub input: usize,
151 pub blend_mode: ClipBlendMode,
153 pub opacity: Animated<f32>,
155}
156
157#[derive(Clone, Debug, PartialEq, Eq)]
159pub enum PlanError {
160 NoOutput,
162 MissingInput {
164 node: NodeId,
166 input: String,
168 },
169 UnboundGroupInput {
171 node: NodeId,
173 },
174 ClipWithoutClip {
176 node: NodeId,
178 },
179 NestedTooDeep {
181 node: NodeId,
183 },
184 TooLarge,
187}
188
189#[derive(Debug)]
191pub(crate) struct PlanBudget {
192 left: u64,
193 exceeded: bool,
194}
195
196impl PlanBudget {
197 pub(crate) const fn new(bytes: u64) -> Self {
198 Self {
199 left: bytes,
200 exceeded: false,
201 }
202 }
203
204 pub(crate) const fn exceeded(&self) -> bool {
206 self.exceeded
207 }
208}
209
210pub(crate) struct Lookup<'a> {
212 compositions: HashMap<&'a CompositionId, &'a Composition>,
213 sources: HashMap<&'a SourceId, &'a MediaSource>,
214 reads: CompositionReads<'a>,
215 canvas: (u32, u32),
217}
218
219impl<'a> Lookup<'a> {
220 pub(crate) fn new(
221 compositions: &'a [Composition],
222 sources: &'a [MediaSource],
223 canvas: (u32, u32),
224 ) -> Self {
225 let compositions = compositions
226 .iter()
227 .map(|composition| (&composition.id, composition))
228 .collect();
229 let reads = CompositionReads::new(&compositions);
230 Self {
231 compositions,
232 sources: sources.iter().map(|source| (&source.id, source)).collect(),
233 reads,
234 canvas,
235 }
236 }
237
238 pub(crate) const fn reads(&self) -> &CompositionReads<'a> {
240 &self.reads
241 }
242}
243
244pub fn plan_composition(
253 composition: &Composition,
254 compositions: &[Composition],
255 sources: &[MediaSource],
256 canvas: (u32, u32),
257) -> Result<CompositionPlan, PlanError> {
258 plan(
259 composition,
260 &Lookup::new(compositions, sources, canvas),
261 true,
262 &mut PlanBudget::new(MAX_PLAN_BYTES),
263 )
264}
265
266pub(crate) fn plan_composition_within(
268 composition: &Composition,
269 lookup: &Lookup<'_>,
270 budget: &mut PlanBudget,
271) -> Result<CompositionPlan, PlanError> {
272 plan(composition, lookup, true, budget)
273}
274
275pub fn plan_source_composition(
281 composition: &Composition,
282 compositions: &[Composition],
283 sources: &[MediaSource],
284 canvas: (u32, u32),
285) -> Result<CompositionPlan, PlanError> {
286 plan(
287 composition,
288 &Lookup::new(compositions, sources, canvas),
289 false,
290 &mut PlanBudget::new(MAX_PLAN_BYTES),
291 )
292}
293
294pub(crate) fn plan_source_composition_within(
296 composition: &Composition,
297 lookup: &Lookup<'_>,
298 budget: &mut PlanBudget,
299) -> Result<CompositionPlan, PlanError> {
300 plan(composition, lookup, false, budget)
301}
302
303fn plan<'a>(
304 composition: &'a Composition,
305 lookup: &Lookup<'a>,
306 clip_allowed: bool,
307 budget: &mut PlanBudget,
308) -> Result<CompositionPlan, PlanError> {
309 let mut planner = Planner {
310 lookup,
311 steps: Vec::new(),
312 budget,
313 };
314 let scope = Scope {
315 composition,
316 inputs: Vec::new(),
317 controls: HashMap::new(),
318 offset: MediaTime::ZERO,
319 prefix: String::new(),
320 clip_allowed,
321 group_inputs_bound: false,
322 depth: 1,
323 };
324 let output = planner.output(&scope)?;
325 Ok(CompositionPlan {
326 id: composition.id.clone(),
327 steps: planner.steps,
328 output,
329 })
330}
331
332struct Scope<'a> {
334 composition: &'a Composition,
335 inputs: Vec<Option<usize>>,
337 controls: HashMap<(&'a NodeId, &'a str), &'a Animated<EffectValue>>,
339 offset: MediaTime,
341 prefix: String,
343 clip_allowed: bool,
345 group_inputs_bound: bool,
347 depth: usize,
349}
350
351struct Planner<'a, 'b> {
352 lookup: &'b Lookup<'a>,
353 steps: Vec<Step>,
354 budget: &'b mut PlanBudget,
355}
356
357impl<'a> Planner<'a, '_> {
358 fn output(&mut self, scope: &Scope<'a>) -> Result<usize, PlanError> {
360 let output = scope
361 .composition
362 .output
363 .as_ref()
364 .ok_or(PlanError::NoOutput)?;
365 let nodes: HashMap<&NodeId, &'a alpha2_model::CompositionNode> = scope
366 .composition
367 .nodes
368 .iter()
369 .map(|node| (&node.id, node))
370 .collect();
371 self.resolve(scope, &nodes, output)
372 }
373
374 fn resolve(
380 &mut self,
381 scope: &Scope<'a>,
382 nodes: &HashMap<&NodeId, &'a alpha2_model::CompositionNode>,
383 id: &'a NodeId,
384 ) -> Result<usize, PlanError> {
385 let mut done: HashMap<&NodeId, usize> = HashMap::new();
386 let mut pending = vec![(id, false)];
388 while let Some((current, expanded)) = pending.pop() {
389 if done.contains_key(current) {
390 continue;
391 }
392 let node = nodes[current];
393 let needed = needed_inputs(self.lookup.reads(), &scope.prefix, node)?;
394 if !expanded {
395 pending.push((current, true));
396 pending.extend(
397 needed
398 .iter()
399 .rev()
400 .filter(|input| !done.contains_key(**input))
401 .map(|input| (*input, false)),
402 );
403 continue;
404 }
405 let step = if node.bypass {
406 done[needed[0]]
407 } else {
408 self.node_step(scope, node, &done)?
409 };
410 done.insert(current, step);
411 }
412 Ok(done[id])
413 }
414
415 fn generator_step(&self, generator: &Generator, start: MediaTime) -> StepOp {
416 generator_step(generator.clone(), start, self.lookup.canvas)
417 }
418
419 fn node_step(
420 &mut self,
421 scope: &Scope<'a>,
422 node: &'a alpha2_model::CompositionNode,
423 done: &HashMap<&NodeId, usize>,
424 ) -> Result<usize, PlanError> {
425 let at = |input: &Option<NodeId>| {
426 input
427 .as_ref()
428 .map(|id| done[id])
429 .expect("needed inputs are connected and planned")
430 };
431 let id = || scoped(&scope.prefix, &node.id);
432 let op = match &node.op {
433 NodeOp::Clip {} => {
434 if !scope.clip_allowed {
435 return Err(PlanError::ClipWithoutClip { node: id() });
436 }
437 StepOp::Clip
438 }
439 NodeOp::GroupInput { index } => {
440 return usize::try_from(*index)
442 .ok()
443 .and_then(|index| scope.inputs.get(index).copied().flatten())
444 .filter(|_| scope.group_inputs_bound)
445 .ok_or_else(|| PlanError::UnboundGroupInput { node: id() });
446 }
447 NodeOp::Group {
448 composition,
449 inputs,
450 controls,
451 } => {
452 let reads = self.lookup.reads();
454 let inputs = inputs
455 .iter()
456 .enumerate()
457 .map(|(port, input)| reads.reads_port(composition, port).then(|| at(input)))
458 .collect();
459 return self.group(scope, &id(), composition, inputs, controls);
460 }
461 NodeOp::Media { source_id, start } => {
462 let source = self.lookup.sources.get(source_id).copied();
463 if let Some(composition) = source.and_then(|source| source.composition.as_ref()) {
464 return self.composition_source(scope, &id(), composition, *start);
465 }
466 let generator = source
467 .filter(|source| source.kind == MediaKind::Generated)
468 .and_then(|source| source.generator.as_ref());
469 generator.map_or_else(
470 || StepOp::Media {
471 source_id: source_id.clone(),
472 start: *start,
473 },
474 |generator| self.generator_step(generator, *start),
475 )
476 }
477 NodeOp::Generator { generator } => self.generator_step(generator, MediaTime::ZERO),
478 NodeOp::Effect {
479 input,
480 inputs,
481 effect,
482 } => StepOp::Effect {
483 input: at(input),
484 inputs: named_steps(inputs, &at),
485 effect: controlled(scope, &node.id, effect),
486 },
487 NodeOp::Transition {
488 from,
489 to,
490 effect,
491 progress,
492 } => StepOp::Transition {
493 from: at(from),
494 to: at(to),
495 effect: controlled(scope, &node.id, effect),
496 progress: progress.clone(),
497 },
498 NodeOp::Transform { input, transform } => StepOp::Transform {
499 input: at(input),
500 transform: transform.clone(),
501 },
502 NodeOp::Merge { layers } => StepOp::Merge {
503 layers: layers
504 .iter()
505 .map(|layer| StepLayer {
506 input: at(&layer.input),
507 blend_mode: layer.blend_mode,
508 opacity: layer.opacity.clone(),
509 })
510 .collect(),
511 },
512 NodeOp::Matte { input, matte, mode } => StepOp::Matte {
513 input: at(input),
514 matte: at(matte),
515 mode: *mode,
516 },
517 NodeOp::ShapeMask { input, masks } => StepOp::ShapeMask {
518 input: at(input),
519 masks: masks.clone(),
520 },
521 };
522 self.push(Step {
523 node: id(),
524 op,
525 offset: scope.offset,
526 })
527 }
528
529 fn push(&mut self, step: Step) -> Result<usize, PlanError> {
531 if self.steps.len() >= MAX_INLINED_NODES {
532 return Err(PlanError::TooLarge);
533 }
534 let bytes = step_bytes(&step);
535 if bytes > self.budget.left {
536 self.budget.exceeded = true;
537 return Err(PlanError::TooLarge);
538 }
539 self.budget.left -= bytes;
540 self.steps.push(step);
541 Ok(self.steps.len() - 1)
542 }
543
544 fn group(
546 &mut self,
547 scope: &Scope<'a>,
548 node: &NodeId,
549 composition: &CompositionId,
550 inputs: Vec<Option<usize>>,
551 controls: &'a std::collections::BTreeMap<String, Animated<EffectValue>>,
552 ) -> Result<usize, PlanError> {
553 let target = self.composition(composition);
554 let depth = nested(scope, node)?;
555 let controls = target
556 .controls
557 .iter()
558 .filter_map(|control| {
559 controls
560 .get(&control.name)
561 .map(|value| ((&control.node, control.param.as_str()), value))
562 })
563 .collect();
564 self.output(&Scope {
565 composition: target,
566 inputs,
567 controls,
568 offset: scope.offset,
569 prefix: format!("{node}/"),
570 clip_allowed: scope.clip_allowed,
571 group_inputs_bound: true,
572 depth,
573 })
574 }
575
576 fn composition_source(
578 &mut self,
579 scope: &Scope<'a>,
580 node: &NodeId,
581 composition: &CompositionId,
582 start: MediaTime,
583 ) -> Result<usize, PlanError> {
584 let depth = nested(scope, node)?;
585 self.output(&Scope {
586 composition: self.composition(composition),
587 inputs: Vec::new(),
588 controls: HashMap::new(),
589 offset: MediaTime(scope.offset.ticks().saturating_add(start.ticks())),
590 prefix: format!("{node}/"),
591 clip_allowed: false,
592 group_inputs_bound: false,
593 depth,
594 })
595 }
596
597 fn composition(&self, id: &CompositionId) -> &'a Composition {
598 self.lookup
599 .compositions
600 .get(id)
601 .copied()
602 .expect("validation guarantees referenced compositions exist")
603 }
604}
605
606fn nested(scope: &Scope<'_>, node: &NodeId) -> Result<usize, PlanError> {
607 let depth = scope.depth + 1;
608 if depth > MAX_COMPOSITION_DEPTH {
609 return Err(PlanError::NestedTooDeep { node: node.clone() });
610 }
611 Ok(depth)
612}
613
614fn scoped(prefix: &str, id: &NodeId) -> NodeId {
615 if prefix.is_empty() {
616 return id.clone();
617 }
618 NodeId::new(format!("{prefix}{id}")).expect("a prefixed id is not empty")
619}
620
621fn named_steps(
622 inputs: &BTreeMap<String, Option<NodeId>>,
623 at: &dyn Fn(&Option<NodeId>) -> usize,
624) -> Vec<(String, usize)> {
625 inputs
626 .iter()
627 .map(|(name, node)| (name.clone(), at(node)))
628 .collect()
629}
630
631fn controlled(scope: &Scope<'_>, node: &NodeId, effect: &ClipEffect) -> ClipEffect {
632 let mut effect = effect.clone();
633 for ((target, param), value) in &scope.controls {
634 if *target == node {
635 effect.params.insert((*param).to_owned(), (*value).clone());
636 }
637 }
638 effect
639}
640
641fn step_bytes(step: &Step) -> u64 {
643 let heap = match &step.op {
644 StepOp::Clip | StepOp::Matte { .. } => 0,
645 StepOp::Media { source_id, .. } => source_id.as_str().len(),
646 StepOp::Generator { generator, .. } => generator_bytes(generator),
647 StepOp::Effect { effect, inputs, .. } => {
648 effect_bytes(effect)
649 + inputs
650 .iter()
651 .map(|(name, _)| name.len() + size_of::<(String, usize)>())
652 .sum::<usize>()
653 }
654 StepOp::Transition {
655 effect, progress, ..
656 } => effect_bytes(effect) + keyframe_bytes(progress),
657 StepOp::Transform { transform, .. } => transform_bytes(transform),
658 StepOp::Merge { layers } => layers
659 .iter()
660 .map(|layer| size_of::<StepLayer>() + keyframe_bytes(&layer.opacity))
661 .sum(),
662 StepOp::ShapeMask { masks, .. } => masks.iter().map(mask_bytes).sum(),
663 };
664 u64::try_from(size_of::<Step>() + step.node.as_str().len() + heap).unwrap_or(u64::MAX)
665}
666
667fn keyframe_bytes<T>(value: &Animated<T>) -> usize {
669 match value {
670 Animated::Constant { .. } => 0,
671 Animated::Keyframes { keyframes } => keyframes.len() * size_of::<Keyframe<T>>(),
672 }
673}
674
675fn effect_bytes(effect: &ClipEffect) -> usize {
676 effect.id.len()
677 + effect
678 .params
679 .iter()
680 .map(|(name, value)| {
681 name.len() + size_of::<Animated<EffectValue>>() + keyframe_bytes(value)
682 })
683 .sum::<usize>()
684 + effect
685 .resources
686 .iter()
687 .map(|(name, id)| name.len() + id.as_str().len())
688 .sum::<usize>()
689}
690
691fn transform_bytes(transform: &VisualProperties) -> usize {
692 point_bytes(&transform.position)
693 + keyframe_bytes(&transform.scale)
694 + keyframe_bytes(&transform.rotation_degrees)
695 + keyframe_bytes(&transform.opacity)
696}
697
698fn mask_bytes(mask: &ShapeMask) -> usize {
699 size_of::<ShapeMask>() + geometry_bytes(&mask.shape) + keyframe_bytes(&mask.feather)
700}
701
702fn point_bytes(point: &AnimatedPoint) -> usize {
703 match point {
704 AnimatedPoint::Constant { .. } => 0,
705 AnimatedPoint::Keyframes { keyframes } => {
706 keyframes.len() * size_of::<alpha2_model::PointKeyframe>()
707 }
708 }
709}
710
711fn path_bytes(path: &Animated<alpha2_model::Path>) -> usize {
712 let one = |path: &alpha2_model::Path| {
713 path.subpaths.len() * size_of::<alpha2_model::Subpath>()
714 + path.vertex_count() * size_of::<alpha2_model::PathVertex>()
715 };
716 match path {
717 Animated::Constant { value } => one(value),
718 Animated::Keyframes { keyframes } => keyframes
719 .iter()
720 .map(|keyframe| size_of::<Keyframe<alpha2_model::Path>>() + one(&keyframe.value))
721 .sum(),
722 }
723}
724
725fn geometry_bytes(geometry: &alpha2_model::ShapeGeometry) -> usize {
726 use alpha2_model::ShapeGeometry;
727 match geometry {
728 ShapeGeometry::Rectangle(rectangle) => {
729 point_bytes(&rectangle.center)
730 + keyframe_bytes(&rectangle.size)
731 + keyframe_bytes(&rectangle.corner_radius)
732 + keyframe_bytes(&rectangle.rotation_degrees)
733 }
734 ShapeGeometry::Ellipse(ellipse) => {
735 point_bytes(&ellipse.center)
736 + keyframe_bytes(&ellipse.size)
737 + keyframe_bytes(&ellipse.rotation_degrees)
738 }
739 ShapeGeometry::Polystar(star) => {
740 point_bytes(&star.center)
741 + keyframe_bytes(&star.points)
742 + keyframe_bytes(&star.outer_radius)
743 + keyframe_bytes(&star.inner_radius)
744 + keyframe_bytes(&star.outer_roundness)
745 + keyframe_bytes(&star.inner_roundness)
746 + keyframe_bytes(&star.rotation_degrees)
747 }
748 ShapeGeometry::Path(path) => path_bytes(&path.path),
749 }
750}
751
752fn paint_bytes(paint: &Paint) -> usize {
753 match paint {
754 Paint::Solid { .. } => 0,
755 Paint::LinearGradient { stops, .. }
756 | Paint::RadialGradient { stops, .. }
757 | Paint::ConicGradient { stops, .. } => {
758 stops.len() * size_of::<alpha2_model::GradientStop>()
759 }
760 }
761}
762
763fn animated_paint_bytes(paint: &Animated<Paint>) -> usize {
764 match paint {
765 Animated::Constant { value } => paint_bytes(value),
766 Animated::Keyframes { keyframes } => keyframes
767 .iter()
768 .map(|keyframe| size_of::<Keyframe<Paint>>() + paint_bytes(&keyframe.value))
769 .sum(),
770 }
771}
772
773fn shape_items_bytes(items: &[alpha2_model::ShapeItem]) -> usize {
775 use alpha2_model::{ShapeGeometry, ShapeItem};
776 items
777 .iter()
778 .map(|item| {
779 size_of::<ShapeItem>()
780 + match item {
781 ShapeItem::Group { items, transform } => {
782 shape_items_bytes(items)
783 + keyframe_bytes(&transform.anchor)
784 + point_bytes(&transform.position)
785 + keyframe_bytes(&transform.scale)
786 + keyframe_bytes(&transform.rotation_degrees)
787 + keyframe_bytes(&transform.opacity)
788 }
789 ShapeItem::Rectangle(geometry) => {
790 geometry_bytes(&ShapeGeometry::Rectangle(geometry.clone()))
791 }
792 ShapeItem::Ellipse(geometry) => {
793 geometry_bytes(&ShapeGeometry::Ellipse(geometry.clone()))
794 }
795 ShapeItem::Polystar(geometry) => {
796 geometry_bytes(&ShapeGeometry::Polystar(geometry.clone()))
797 }
798 ShapeItem::Path(geometry) => path_bytes(&geometry.path),
799 ShapeItem::Fill { paint, opacity, .. } => {
800 animated_paint_bytes(paint) + keyframe_bytes(opacity)
801 }
802 ShapeItem::Stroke(stroke) => {
803 animated_paint_bytes(&stroke.paint)
804 + keyframe_bytes(&stroke.width)
805 + keyframe_bytes(&stroke.opacity)
806 + stroke.dash.as_ref().map_or(0, |dash| {
807 dash.pattern
808 .iter()
809 .map(|entry| size_of::<Animated<f32>>() + keyframe_bytes(entry))
810 .sum::<usize>()
811 + keyframe_bytes(&dash.offset)
812 })
813 }
814 ShapeItem::Trim(trim) => {
815 keyframe_bytes(&trim.start)
816 + keyframe_bytes(&trim.end)
817 + keyframe_bytes(&trim.offset)
818 }
819 }
820 })
821 .sum()
822}
823
824fn generator_step(generator: Generator, start: MediaTime, canvas: (u32, u32)) -> StepOp {
825 let plan = GeneratorPlan::new(&generator, canvas).map(Arc::new);
826 StepOp::Generator {
827 generator,
828 start,
829 plan,
830 }
831}
832
833fn generator_bytes(generator: &Generator) -> usize {
834 match generator {
835 Generator::Fill { paint } => paint_bytes(paint),
836 Generator::Shapes { items } => 2 * shape_items_bytes(items),
838 Generator::Text { text, style } => {
839 text.len() + style.font_family.as_ref().map_or(0, String::len)
840 }
841 Generator::Layout { root } => 2 * crate::layout::layout_bytes(root, &shape_items_bytes),
843 }
844}
845
846fn needed_inputs<'a>(
849 reads: &CompositionReads<'_>,
850 prefix: &str,
851 node: &'a alpha2_model::CompositionNode,
852) -> Result<Vec<&'a NodeId>, PlanError> {
853 let needed = reads.needed_inputs(node);
854 if node.bypass && needed.is_empty() {
855 return Err(PlanError::MissingInput {
857 node: scoped(prefix, &node.id),
858 input: "layers[0]".into(),
859 });
860 }
861 needed
862 .into_iter()
863 .map(|input| {
864 input.node().ok_or_else(|| PlanError::MissingInput {
865 node: scoped(prefix, &node.id),
866 input: input.name(),
867 })
868 })
869 .collect()
870}
871
872#[derive(Clone, Debug)]
874pub(crate) struct IndexedComposition {
875 pub(crate) plan: Arc<Result<CompositionPlan, PlanError>>,
876 pub(crate) reference: alpha2_model::ClipComposition,
877}
878
879#[derive(Clone, Copy, Debug, PartialEq)]
881pub struct ActiveComposition<'a> {
882 plan: &'a CompositionPlan,
883 time: MediaTime,
884}
885
886impl<'a> ActiveComposition<'a> {
887 #[must_use]
889 pub const fn new(plan: &'a CompositionPlan, time: MediaTime) -> Self {
890 Self { plan, time }
891 }
892
893 #[must_use]
895 pub const fn plan(&self) -> &'a CompositionPlan {
896 self.plan
897 }
898
899 #[must_use]
901 pub const fn time(&self) -> MediaTime {
902 self.time
903 }
904
905 pub fn media(&self) -> impl Iterator<Item = (usize, &'a SourceId, MediaTime)> + 'a {
907 let time = self.time;
908 self.plan
909 .steps
910 .iter()
911 .enumerate()
912 .filter_map(move |(index, step)| match &step.op {
913 StepOp::Media { source_id, start } => Some((
914 index,
915 source_id,
916 MediaTime(start.ticks().saturating_add(step.time(time).ticks())),
917 )),
918 _ => None,
919 })
920 }
921}
922
923#[cfg(test)]
924#[path = "composition_tests.rs"]
925mod tests;