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::ShapesPlan;
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,
113 shapes: Option<Arc<ShapesPlan>>,
115 },
116 Effect {
118 input: usize,
120 inputs: Vec<(String, usize)>,
122 effect: ClipEffect,
124 },
125 Transition {
127 from: usize,
129 to: usize,
131 effect: ClipEffect,
133 progress: Animated<f32>,
135 },
136 Transform {
138 input: usize,
140 transform: VisualProperties,
142 },
143 Merge {
145 layers: Vec<StepLayer>,
147 },
148 Matte {
150 input: usize,
152 matte: usize,
154 mode: MatteMode,
156 },
157 ShapeMask {
159 input: usize,
161 masks: Vec<ShapeMask>,
163 },
164}
165
166#[derive(Clone, Debug, PartialEq)]
168pub struct StepLayer {
169 pub input: usize,
171 pub blend_mode: ClipBlendMode,
173 pub opacity: Animated<f32>,
175}
176
177#[derive(Clone, Debug, PartialEq, Eq)]
179pub enum PlanError {
180 NoOutput,
182 MissingInput {
184 node: NodeId,
186 input: String,
189 },
190 UnboundGroupInput {
193 node: NodeId,
195 },
196 ClipWithoutClip {
198 node: NodeId,
200 },
201 NestedTooDeep {
205 node: NodeId,
207 },
208 TooLarge,
212}
213
214#[derive(Debug)]
217pub(crate) struct PlanBudget {
218 left: u64,
219 exceeded: bool,
220}
221
222impl PlanBudget {
223 pub(crate) const fn new(bytes: u64) -> Self {
224 Self {
225 left: bytes,
226 exceeded: false,
227 }
228 }
229
230 pub(crate) const fn exceeded(&self) -> bool {
232 self.exceeded
233 }
234}
235
236pub(crate) struct Lookup<'a> {
239 compositions: HashMap<&'a CompositionId, &'a Composition>,
240 sources: HashMap<&'a SourceId, &'a MediaSource>,
241 reads: CompositionReads<'a>,
242}
243
244impl<'a> Lookup<'a> {
245 pub(crate) fn new(compositions: &'a [Composition], sources: &'a [MediaSource]) -> Self {
246 let compositions = compositions
247 .iter()
248 .map(|composition| (&composition.id, composition))
249 .collect();
250 let reads = CompositionReads::new(&compositions);
251 Self {
252 compositions,
253 sources: sources.iter().map(|source| (&source.id, source)).collect(),
254 reads,
255 }
256 }
257
258 pub(crate) const fn reads(&self) -> &CompositionReads<'a> {
260 &self.reads
261 }
262}
263
264pub fn plan_composition(
280 composition: &Composition,
281 compositions: &[Composition],
282 sources: &[MediaSource],
283) -> Result<CompositionPlan, PlanError> {
284 plan(
285 composition,
286 &Lookup::new(compositions, sources),
287 true,
288 &mut PlanBudget::new(MAX_PLAN_BYTES),
289 )
290}
291
292pub(crate) fn plan_composition_within(
295 composition: &Composition,
296 lookup: &Lookup<'_>,
297 budget: &mut PlanBudget,
298) -> Result<CompositionPlan, PlanError> {
299 plan(composition, lookup, true, budget)
300}
301
302pub fn plan_source_composition(
310 composition: &Composition,
311 compositions: &[Composition],
312 sources: &[MediaSource],
313) -> Result<CompositionPlan, PlanError> {
314 plan(
315 composition,
316 &Lookup::new(compositions, sources),
317 false,
318 &mut PlanBudget::new(MAX_PLAN_BYTES),
319 )
320}
321
322pub(crate) fn plan_source_composition_within(
325 composition: &Composition,
326 lookup: &Lookup<'_>,
327 budget: &mut PlanBudget,
328) -> Result<CompositionPlan, PlanError> {
329 plan(composition, lookup, false, budget)
330}
331
332fn plan<'a>(
333 composition: &'a Composition,
334 lookup: &Lookup<'a>,
335 clip_allowed: bool,
336 budget: &mut PlanBudget,
337) -> Result<CompositionPlan, PlanError> {
338 let mut planner = Planner {
339 lookup,
340 steps: Vec::new(),
341 budget,
342 };
343 let scope = Scope {
344 composition,
345 inputs: Vec::new(),
346 controls: HashMap::new(),
347 offset: MediaTime::ZERO,
348 prefix: String::new(),
349 clip_allowed,
350 group_inputs_bound: false,
351 depth: 1,
352 };
353 let output = planner.output(&scope)?;
354 Ok(CompositionPlan {
355 id: composition.id.clone(),
356 steps: planner.steps,
357 output,
358 })
359}
360
361struct Scope<'a> {
363 composition: &'a Composition,
364 inputs: Vec<Option<usize>>,
367 controls: HashMap<(&'a NodeId, &'a str), &'a Animated<EffectValue>>,
369 offset: MediaTime,
371 prefix: String,
373 clip_allowed: bool,
375 group_inputs_bound: bool,
377 depth: usize,
379}
380
381struct Planner<'a, 'b> {
382 lookup: &'b Lookup<'a>,
383 steps: Vec<Step>,
384 budget: &'b mut PlanBudget,
385}
386
387impl<'a> Planner<'a, '_> {
388 fn output(&mut self, scope: &Scope<'a>) -> Result<usize, PlanError> {
390 let output = scope
391 .composition
392 .output
393 .as_ref()
394 .ok_or(PlanError::NoOutput)?;
395 let nodes: HashMap<&NodeId, &'a alpha2_model::CompositionNode> = scope
396 .composition
397 .nodes
398 .iter()
399 .map(|node| (&node.id, node))
400 .collect();
401 self.resolve(scope, &nodes, output)
402 }
403
404 fn resolve(
414 &mut self,
415 scope: &Scope<'a>,
416 nodes: &HashMap<&NodeId, &'a alpha2_model::CompositionNode>,
417 id: &'a NodeId,
418 ) -> Result<usize, PlanError> {
419 let mut done: HashMap<&NodeId, usize> = HashMap::new();
420 let mut pending = vec![(id, false)];
422 while let Some((current, expanded)) = pending.pop() {
423 if done.contains_key(current) {
424 continue;
425 }
426 let node = nodes[current];
427 let needed = needed_inputs(self.lookup.reads(), &scope.prefix, node)?;
428 if !expanded {
429 pending.push((current, true));
430 pending.extend(
431 needed
432 .iter()
433 .rev()
434 .filter(|input| !done.contains_key(**input))
435 .map(|input| (*input, false)),
436 );
437 continue;
438 }
439 let step = if node.bypass {
440 done[needed[0]]
441 } else {
442 self.node_step(scope, node, &done)?
443 };
444 done.insert(current, step);
445 }
446 Ok(done[id])
447 }
448
449 fn node_step(
451 &mut self,
452 scope: &Scope<'a>,
453 node: &'a alpha2_model::CompositionNode,
454 done: &HashMap<&NodeId, usize>,
455 ) -> Result<usize, PlanError> {
456 let at = |input: &Option<NodeId>| {
457 input
458 .as_ref()
459 .map(|id| done[id])
460 .expect("needed inputs are connected and planned")
461 };
462 let id = || scoped(&scope.prefix, &node.id);
463 let op = match &node.op {
464 NodeOp::Clip {} => {
465 if !scope.clip_allowed {
466 return Err(PlanError::ClipWithoutClip { node: id() });
467 }
468 StepOp::Clip
469 }
470 NodeOp::GroupInput { index } => {
471 return usize::try_from(*index)
473 .ok()
474 .and_then(|index| scope.inputs.get(index).copied().flatten())
475 .filter(|_| scope.group_inputs_bound)
476 .ok_or_else(|| PlanError::UnboundGroupInput { node: id() });
477 }
478 NodeOp::Group {
479 composition,
480 inputs,
481 controls,
482 } => {
483 let reads = self.lookup.reads();
485 let inputs = inputs
486 .iter()
487 .enumerate()
488 .map(|(port, input)| reads.reads_port(composition, port).then(|| at(input)))
489 .collect();
490 return self.group(scope, &id(), composition, inputs, controls);
491 }
492 NodeOp::Media { source_id, start } => {
493 let source = self.lookup.sources.get(source_id).copied();
494 if let Some(composition) = source.and_then(|source| source.composition.as_ref()) {
495 return self.composition_source(scope, &id(), composition, *start);
496 }
497 let generator = source
498 .filter(|source| source.kind == MediaKind::Generated)
499 .and_then(|source| source.generator.clone());
500 generator.map_or_else(
501 || StepOp::Media {
502 source_id: source_id.clone(),
503 start: *start,
504 },
505 |generator| generator_step(generator, *start),
506 )
507 }
508 NodeOp::Generator { generator } => generator_step(generator.clone(), MediaTime::ZERO),
509 NodeOp::Effect {
510 input,
511 inputs,
512 effect,
513 } => StepOp::Effect {
514 input: at(input),
515 inputs: named_steps(inputs, &at),
516 effect: controlled(scope, &node.id, effect),
517 },
518 NodeOp::Transition {
519 from,
520 to,
521 effect,
522 progress,
523 } => StepOp::Transition {
524 from: at(from),
525 to: at(to),
526 effect: controlled(scope, &node.id, effect),
527 progress: progress.clone(),
528 },
529 NodeOp::Transform { input, transform } => StepOp::Transform {
530 input: at(input),
531 transform: transform.clone(),
532 },
533 NodeOp::Merge { layers } => StepOp::Merge {
534 layers: layers
535 .iter()
536 .map(|layer| StepLayer {
537 input: at(&layer.input),
538 blend_mode: layer.blend_mode,
539 opacity: layer.opacity.clone(),
540 })
541 .collect(),
542 },
543 NodeOp::Matte { input, matte, mode } => StepOp::Matte {
544 input: at(input),
545 matte: at(matte),
546 mode: *mode,
547 },
548 NodeOp::ShapeMask { input, masks } => StepOp::ShapeMask {
549 input: at(input),
550 masks: masks.clone(),
551 },
552 };
553 self.push(Step {
554 node: id(),
555 op,
556 offset: scope.offset,
557 })
558 }
559
560 fn push(&mut self, step: Step) -> Result<usize, PlanError> {
563 if self.steps.len() >= MAX_INLINED_NODES {
564 return Err(PlanError::TooLarge);
565 }
566 let bytes = step_bytes(&step);
567 if bytes > self.budget.left {
568 self.budget.exceeded = true;
569 return Err(PlanError::TooLarge);
570 }
571 self.budget.left -= bytes;
572 self.steps.push(step);
573 Ok(self.steps.len() - 1)
574 }
575
576 fn group(
579 &mut self,
580 scope: &Scope<'a>,
581 node: &NodeId,
582 composition: &CompositionId,
583 inputs: Vec<Option<usize>>,
584 controls: &'a std::collections::BTreeMap<String, Animated<EffectValue>>,
585 ) -> Result<usize, PlanError> {
586 let target = self.composition(composition);
587 let depth = nested(scope, node)?;
588 let controls = target
589 .controls
590 .iter()
591 .filter_map(|control| {
592 controls
593 .get(&control.name)
594 .map(|value| ((&control.node, control.param.as_str()), value))
595 })
596 .collect();
597 self.output(&Scope {
598 composition: target,
599 inputs,
600 controls,
601 offset: scope.offset,
602 prefix: format!("{node}/"),
603 clip_allowed: scope.clip_allowed,
604 group_inputs_bound: true,
605 depth,
606 })
607 }
608
609 fn composition_source(
612 &mut self,
613 scope: &Scope<'a>,
614 node: &NodeId,
615 composition: &CompositionId,
616 start: MediaTime,
617 ) -> Result<usize, PlanError> {
618 let depth = nested(scope, node)?;
619 self.output(&Scope {
620 composition: self.composition(composition),
621 inputs: Vec::new(),
622 controls: HashMap::new(),
623 offset: MediaTime(scope.offset.ticks().saturating_add(start.ticks())),
624 prefix: format!("{node}/"),
625 clip_allowed: false,
626 group_inputs_bound: false,
627 depth,
628 })
629 }
630
631 fn composition(&self, id: &CompositionId) -> &'a Composition {
632 self.lookup
633 .compositions
634 .get(id)
635 .copied()
636 .expect("validation guarantees referenced compositions exist")
637 }
638}
639
640fn nested(scope: &Scope<'_>, node: &NodeId) -> Result<usize, PlanError> {
643 let depth = scope.depth + 1;
644 if depth > MAX_COMPOSITION_DEPTH {
645 return Err(PlanError::NestedTooDeep { node: node.clone() });
646 }
647 Ok(depth)
648}
649
650fn scoped(prefix: &str, id: &NodeId) -> NodeId {
652 if prefix.is_empty() {
653 return id.clone();
654 }
655 NodeId::new(format!("{prefix}{id}")).expect("a prefixed id is not empty")
656}
657
658fn named_steps(
661 inputs: &BTreeMap<String, Option<NodeId>>,
662 at: &dyn Fn(&Option<NodeId>) -> usize,
663) -> Vec<(String, usize)> {
664 inputs
665 .iter()
666 .map(|(name, node)| (name.clone(), at(node)))
667 .collect()
668}
669
670fn controlled(scope: &Scope<'_>, node: &NodeId, effect: &ClipEffect) -> ClipEffect {
672 let mut effect = effect.clone();
673 for ((target, param), value) in &scope.controls {
674 if *target == node {
675 effect.params.insert((*param).to_owned(), (*value).clone());
676 }
677 }
678 effect
679}
680
681fn step_bytes(step: &Step) -> u64 {
683 let heap = match &step.op {
684 StepOp::Clip | StepOp::Matte { .. } => 0,
685 StepOp::Media { source_id, .. } => source_id.as_str().len(),
686 StepOp::Generator { generator, .. } => generator_bytes(generator),
687 StepOp::Effect { effect, inputs, .. } => {
688 effect_bytes(effect)
689 + inputs
690 .iter()
691 .map(|(name, _)| name.len() + size_of::<(String, usize)>())
692 .sum::<usize>()
693 }
694 StepOp::Transition {
695 effect, progress, ..
696 } => effect_bytes(effect) + keyframe_bytes(progress),
697 StepOp::Transform { transform, .. } => transform_bytes(transform),
698 StepOp::Merge { layers } => layers
699 .iter()
700 .map(|layer| size_of::<StepLayer>() + keyframe_bytes(&layer.opacity))
701 .sum(),
702 StepOp::ShapeMask { masks, .. } => masks.iter().map(mask_bytes).sum(),
703 };
704 u64::try_from(size_of::<Step>() + step.node.as_str().len() + heap).unwrap_or(u64::MAX)
705}
706
707fn keyframe_bytes<T>(value: &Animated<T>) -> usize {
709 match value {
710 Animated::Constant { .. } => 0,
711 Animated::Keyframes { keyframes } => keyframes.len() * size_of::<Keyframe<T>>(),
712 }
713}
714
715fn effect_bytes(effect: &ClipEffect) -> usize {
716 effect.id.len()
717 + effect
718 .params
719 .iter()
720 .map(|(name, value)| {
721 name.len() + size_of::<Animated<EffectValue>>() + keyframe_bytes(value)
722 })
723 .sum::<usize>()
724 + effect
725 .resources
726 .iter()
727 .map(|(name, id)| name.len() + id.as_str().len())
728 .sum::<usize>()
729}
730
731fn transform_bytes(transform: &VisualProperties) -> usize {
732 point_bytes(&transform.position)
733 + keyframe_bytes(&transform.scale)
734 + keyframe_bytes(&transform.rotation_degrees)
735 + keyframe_bytes(&transform.opacity)
736}
737
738fn mask_bytes(mask: &ShapeMask) -> usize {
739 size_of::<ShapeMask>() + geometry_bytes(&mask.shape) + keyframe_bytes(&mask.feather)
740}
741
742fn point_bytes(point: &AnimatedPoint) -> usize {
743 match point {
744 AnimatedPoint::Constant { .. } => 0,
745 AnimatedPoint::Keyframes { keyframes } => {
746 keyframes.len() * size_of::<alpha2_model::PointKeyframe>()
747 }
748 }
749}
750
751fn path_bytes(path: &Animated<alpha2_model::Path>) -> usize {
752 let one = |path: &alpha2_model::Path| {
753 path.subpaths.len() * size_of::<alpha2_model::Subpath>()
754 + path.vertex_count() * size_of::<alpha2_model::PathVertex>()
755 };
756 match path {
757 Animated::Constant { value } => one(value),
758 Animated::Keyframes { keyframes } => keyframes
759 .iter()
760 .map(|keyframe| size_of::<Keyframe<alpha2_model::Path>>() + one(&keyframe.value))
761 .sum(),
762 }
763}
764
765fn geometry_bytes(geometry: &alpha2_model::ShapeGeometry) -> usize {
766 use alpha2_model::ShapeGeometry;
767 match geometry {
768 ShapeGeometry::Rectangle(rectangle) => {
769 point_bytes(&rectangle.center)
770 + keyframe_bytes(&rectangle.size)
771 + keyframe_bytes(&rectangle.corner_radius)
772 + keyframe_bytes(&rectangle.rotation_degrees)
773 }
774 ShapeGeometry::Ellipse(ellipse) => {
775 point_bytes(&ellipse.center)
776 + keyframe_bytes(&ellipse.size)
777 + keyframe_bytes(&ellipse.rotation_degrees)
778 }
779 ShapeGeometry::Polystar(star) => {
780 point_bytes(&star.center)
781 + keyframe_bytes(&star.points)
782 + keyframe_bytes(&star.outer_radius)
783 + keyframe_bytes(&star.inner_radius)
784 + keyframe_bytes(&star.outer_roundness)
785 + keyframe_bytes(&star.inner_roundness)
786 + keyframe_bytes(&star.rotation_degrees)
787 }
788 ShapeGeometry::Path(path) => path_bytes(&path.path),
789 }
790}
791
792fn paint_bytes(paint: &Paint) -> usize {
793 match paint {
794 Paint::Solid { .. } => 0,
795 Paint::LinearGradient { stops, .. }
796 | Paint::RadialGradient { stops, .. }
797 | Paint::ConicGradient { stops, .. } => {
798 stops.len() * size_of::<alpha2_model::GradientStop>()
799 }
800 }
801}
802
803fn animated_paint_bytes(paint: &Animated<Paint>) -> usize {
804 match paint {
805 Animated::Constant { value } => paint_bytes(value),
806 Animated::Keyframes { keyframes } => keyframes
807 .iter()
808 .map(|keyframe| size_of::<Keyframe<Paint>>() + paint_bytes(&keyframe.value))
809 .sum(),
810 }
811}
812
813fn shape_items_bytes(items: &[alpha2_model::ShapeItem]) -> usize {
816 use alpha2_model::{ShapeGeometry, ShapeItem};
817 items
818 .iter()
819 .map(|item| {
820 size_of::<ShapeItem>()
821 + match item {
822 ShapeItem::Group { items, transform } => {
823 shape_items_bytes(items)
824 + keyframe_bytes(&transform.anchor)
825 + point_bytes(&transform.position)
826 + keyframe_bytes(&transform.scale)
827 + keyframe_bytes(&transform.rotation_degrees)
828 + keyframe_bytes(&transform.opacity)
829 }
830 ShapeItem::Rectangle(geometry) => {
831 geometry_bytes(&ShapeGeometry::Rectangle(geometry.clone()))
832 }
833 ShapeItem::Ellipse(geometry) => {
834 geometry_bytes(&ShapeGeometry::Ellipse(geometry.clone()))
835 }
836 ShapeItem::Polystar(geometry) => {
837 geometry_bytes(&ShapeGeometry::Polystar(geometry.clone()))
838 }
839 ShapeItem::Path(geometry) => path_bytes(&geometry.path),
840 ShapeItem::Fill { paint, opacity, .. } => {
841 animated_paint_bytes(paint) + keyframe_bytes(opacity)
842 }
843 ShapeItem::Stroke(stroke) => {
844 animated_paint_bytes(&stroke.paint)
845 + keyframe_bytes(&stroke.width)
846 + keyframe_bytes(&stroke.opacity)
847 + stroke.dash.as_ref().map_or(0, |dash| {
848 dash.pattern
849 .iter()
850 .map(|entry| size_of::<Animated<f32>>() + keyframe_bytes(entry))
851 .sum::<usize>()
852 + keyframe_bytes(&dash.offset)
853 })
854 }
855 ShapeItem::Trim(trim) => {
856 keyframe_bytes(&trim.start)
857 + keyframe_bytes(&trim.end)
858 + keyframe_bytes(&trim.offset)
859 }
860 }
861 })
862 .sum()
863}
864
865fn generator_step(generator: Generator, start: MediaTime) -> StepOp {
867 let shapes = match &generator {
868 Generator::Shapes { items } => Some(Arc::new(ShapesPlan::new(items))),
869 _ => None,
870 };
871 StepOp::Generator {
872 generator,
873 start,
874 shapes,
875 }
876}
877
878fn generator_bytes(generator: &Generator) -> usize {
879 match generator {
880 Generator::Fill { paint } => paint_bytes(paint),
881 Generator::Shapes { items } => 2 * shape_items_bytes(items),
883 Generator::Text { text, style } => {
884 text.len() + style.font_family.as_ref().map_or(0, String::len)
885 }
886 }
887}
888
889fn needed_inputs<'a>(
894 reads: &CompositionReads<'_>,
895 prefix: &str,
896 node: &'a alpha2_model::CompositionNode,
897) -> Result<Vec<&'a NodeId>, PlanError> {
898 let needed = reads.needed_inputs(node);
899 if node.bypass && needed.is_empty() {
900 return Err(PlanError::MissingInput {
902 node: scoped(prefix, &node.id),
903 input: "layers[0]".into(),
904 });
905 }
906 needed
907 .into_iter()
908 .map(|input| {
909 input.node().ok_or_else(|| PlanError::MissingInput {
910 node: scoped(prefix, &node.id),
911 input: input.name(),
912 })
913 })
914 .collect()
915}
916
917#[derive(Clone, Debug)]
919pub(crate) struct IndexedComposition {
920 pub(crate) plan: Arc<Result<CompositionPlan, PlanError>>,
921 pub(crate) reference: alpha2_model::ClipComposition,
922}
923
924#[derive(Clone, Copy, Debug, PartialEq)]
926pub struct ActiveComposition<'a> {
927 plan: &'a CompositionPlan,
928 time: MediaTime,
929}
930
931impl<'a> ActiveComposition<'a> {
932 #[must_use]
934 pub const fn new(plan: &'a CompositionPlan, time: MediaTime) -> Self {
935 Self { plan, time }
936 }
937
938 #[must_use]
940 pub const fn plan(&self) -> &'a CompositionPlan {
941 self.plan
942 }
943
944 #[must_use]
946 pub const fn time(&self) -> MediaTime {
947 self.time
948 }
949
950 pub fn media(&self) -> impl Iterator<Item = (usize, &'a SourceId, MediaTime)> + 'a {
953 let time = self.time;
954 self.plan
955 .steps
956 .iter()
957 .enumerate()
958 .filter_map(move |(index, step)| match &step.op {
959 StepOp::Media { source_id, start } => Some((
960 index,
961 source_id,
962 MediaTime(start.ticks().saturating_add(step.time(time).ticks())),
963 )),
964 _ => None,
965 })
966 }
967}
968
969#[cfg(test)]
970#[path = "composition_tests.rs"]
971mod tests;