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