use alpha2_media::SourceId;
use alpha2_model::{
Animated, ClipBlendMode, ClipEffect, Composition, CompositionId, CompositionReads, EffectValue,
Generator, Keyframe, MAX_COMPOSITION_DEPTH, MAX_INLINED_NODES, MaskShape, MatteMode, MediaKind,
MediaSource, NodeId, NodeOp, Paint, ShapeMask, VisualProperties,
};
use alpha2_time::MediaTime;
use std::collections::HashMap;
use std::sync::Arc;
pub const MAX_PLAN_BYTES: u64 = 256 * 1024 * 1024;
#[derive(Clone, Debug, PartialEq)]
pub struct CompositionPlan {
pub id: CompositionId,
pub steps: Vec<Step>,
pub output: usize,
}
impl CompositionPlan {
pub fn effects(&self) -> impl Iterator<Item = &ClipEffect> {
self.steps.iter().filter_map(|step| match &step.op {
StepOp::Effect { effect, .. } | StepOp::Transition { effect, .. } => Some(effect),
_ => None,
})
}
#[must_use]
pub fn uses_clip(&self) -> bool {
self.steps
.iter()
.any(|step| matches!(step.op, StepOp::Clip))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Step {
pub node: NodeId,
pub op: StepOp,
pub offset: MediaTime,
}
impl Step {
#[must_use]
pub fn time(&self, time: MediaTime) -> MediaTime {
MediaTime(time.ticks().saturating_add(self.offset.ticks()))
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum StepOp {
Clip,
Media {
source_id: SourceId,
start: MediaTime,
},
Generator(Generator),
Effect {
input: usize,
effect: ClipEffect,
},
Transition {
from: usize,
to: usize,
effect: ClipEffect,
progress: Animated<f32>,
},
Transform {
input: usize,
transform: VisualProperties,
},
Merge {
layers: Vec<StepLayer>,
},
Matte {
input: usize,
matte: usize,
mode: MatteMode,
},
ShapeMask {
input: usize,
masks: Vec<ShapeMask>,
},
}
#[derive(Clone, Debug, PartialEq)]
pub struct StepLayer {
pub input: usize,
pub blend_mode: ClipBlendMode,
pub opacity: Animated<f32>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PlanError {
NoOutput,
MissingInput {
node: NodeId,
input: String,
},
UnboundGroupInput {
node: NodeId,
},
ClipWithoutClip {
node: NodeId,
},
NestedTooDeep {
node: NodeId,
},
TooLarge,
}
#[derive(Debug)]
pub(crate) struct PlanBudget {
left: u64,
exceeded: bool,
}
impl PlanBudget {
pub(crate) const fn new(bytes: u64) -> Self {
Self {
left: bytes,
exceeded: false,
}
}
pub(crate) const fn exceeded(&self) -> bool {
self.exceeded
}
}
pub(crate) struct Lookup<'a> {
compositions: HashMap<&'a CompositionId, &'a Composition>,
sources: HashMap<&'a SourceId, &'a MediaSource>,
reads: CompositionReads<'a>,
}
impl<'a> Lookup<'a> {
pub(crate) fn new(compositions: &'a [Composition], sources: &'a [MediaSource]) -> Self {
let compositions = compositions
.iter()
.map(|composition| (&composition.id, composition))
.collect();
let reads = CompositionReads::new(&compositions);
Self {
compositions,
sources: sources.iter().map(|source| (&source.id, source)).collect(),
reads,
}
}
pub(crate) const fn reads(&self) -> &CompositionReads<'a> {
&self.reads
}
}
pub fn plan_composition(
composition: &Composition,
compositions: &[Composition],
sources: &[MediaSource],
) -> Result<CompositionPlan, PlanError> {
plan(
composition,
&Lookup::new(compositions, sources),
true,
&mut PlanBudget::new(MAX_PLAN_BYTES),
)
}
pub(crate) fn plan_composition_within(
composition: &Composition,
lookup: &Lookup<'_>,
budget: &mut PlanBudget,
) -> Result<CompositionPlan, PlanError> {
plan(composition, lookup, true, budget)
}
pub fn plan_source_composition(
composition: &Composition,
compositions: &[Composition],
sources: &[MediaSource],
) -> Result<CompositionPlan, PlanError> {
plan(
composition,
&Lookup::new(compositions, sources),
false,
&mut PlanBudget::new(MAX_PLAN_BYTES),
)
}
pub(crate) fn plan_source_composition_within(
composition: &Composition,
lookup: &Lookup<'_>,
budget: &mut PlanBudget,
) -> Result<CompositionPlan, PlanError> {
plan(composition, lookup, false, budget)
}
fn plan<'a>(
composition: &'a Composition,
lookup: &Lookup<'a>,
clip_allowed: bool,
budget: &mut PlanBudget,
) -> Result<CompositionPlan, PlanError> {
let mut planner = Planner {
lookup,
steps: Vec::new(),
budget,
};
let scope = Scope {
composition,
inputs: Vec::new(),
controls: HashMap::new(),
offset: MediaTime::ZERO,
prefix: String::new(),
clip_allowed,
group_inputs_bound: false,
depth: 1,
};
let output = planner.output(&scope)?;
Ok(CompositionPlan {
id: composition.id.clone(),
steps: planner.steps,
output,
})
}
struct Scope<'a> {
composition: &'a Composition,
inputs: Vec<Option<usize>>,
controls: HashMap<(&'a NodeId, &'a str), &'a Animated<EffectValue>>,
offset: MediaTime,
prefix: String,
clip_allowed: bool,
group_inputs_bound: bool,
depth: usize,
}
struct Planner<'a, 'b> {
lookup: &'b Lookup<'a>,
steps: Vec<Step>,
budget: &'b mut PlanBudget,
}
impl<'a> Planner<'a, '_> {
fn output(&mut self, scope: &Scope<'a>) -> Result<usize, PlanError> {
let output = scope
.composition
.output
.as_ref()
.ok_or(PlanError::NoOutput)?;
let nodes: HashMap<&NodeId, &'a alpha2_model::CompositionNode> = scope
.composition
.nodes
.iter()
.map(|node| (&node.id, node))
.collect();
self.resolve(scope, &nodes, output)
}
fn resolve(
&mut self,
scope: &Scope<'a>,
nodes: &HashMap<&NodeId, &'a alpha2_model::CompositionNode>,
id: &'a NodeId,
) -> Result<usize, PlanError> {
let mut done: HashMap<&NodeId, usize> = HashMap::new();
let mut pending = vec![(id, false)];
while let Some((current, expanded)) = pending.pop() {
if done.contains_key(current) {
continue;
}
let node = nodes[current];
let needed = needed_inputs(self.lookup.reads(), &scope.prefix, node)?;
if !expanded {
pending.push((current, true));
pending.extend(
needed
.iter()
.rev()
.filter(|input| !done.contains_key(**input))
.map(|input| (*input, false)),
);
continue;
}
let step = if node.bypass {
done[needed[0]]
} else {
self.node_step(scope, node, &done)?
};
done.insert(current, step);
}
Ok(done[id])
}
fn node_step(
&mut self,
scope: &Scope<'a>,
node: &'a alpha2_model::CompositionNode,
done: &HashMap<&NodeId, usize>,
) -> Result<usize, PlanError> {
let at = |input: &Option<NodeId>| {
input
.as_ref()
.map(|id| done[id])
.expect("needed inputs are connected and planned")
};
let id = || scoped(&scope.prefix, &node.id);
let op = match &node.op {
NodeOp::Clip {} => {
if !scope.clip_allowed {
return Err(PlanError::ClipWithoutClip { node: id() });
}
StepOp::Clip
}
NodeOp::GroupInput { index } => {
return usize::try_from(*index)
.ok()
.and_then(|index| scope.inputs.get(index).copied().flatten())
.filter(|_| scope.group_inputs_bound)
.ok_or_else(|| PlanError::UnboundGroupInput { node: id() });
}
NodeOp::Group {
composition,
inputs,
controls,
} => {
let reads = self.lookup.reads();
let inputs = inputs
.iter()
.enumerate()
.map(|(port, input)| reads.reads_port(composition, port).then(|| at(input)))
.collect();
return self.group(scope, &id(), composition, inputs, controls);
}
NodeOp::Media { source_id, start } => {
let source = self.lookup.sources.get(source_id).copied();
if let Some(composition) = source.and_then(|source| source.composition.as_ref()) {
return self.composition_source(scope, &id(), composition, *start);
}
let generator = source
.filter(|source| source.kind == MediaKind::Generated)
.and_then(|source| source.generator.clone());
generator.map_or_else(
|| StepOp::Media {
source_id: source_id.clone(),
start: *start,
},
StepOp::Generator,
)
}
NodeOp::Generator { generator } => StepOp::Generator(generator.clone()),
NodeOp::Effect { input, effect } => StepOp::Effect {
input: at(input),
effect: controlled(scope, &node.id, effect),
},
NodeOp::Transition {
from,
to,
effect,
progress,
} => StepOp::Transition {
from: at(from),
to: at(to),
effect: controlled(scope, &node.id, effect),
progress: progress.clone(),
},
NodeOp::Transform { input, transform } => StepOp::Transform {
input: at(input),
transform: transform.clone(),
},
NodeOp::Merge { layers } => StepOp::Merge {
layers: layers
.iter()
.map(|layer| StepLayer {
input: at(&layer.input),
blend_mode: layer.blend_mode,
opacity: layer.opacity.clone(),
})
.collect(),
},
NodeOp::Matte { input, matte, mode } => StepOp::Matte {
input: at(input),
matte: at(matte),
mode: *mode,
},
NodeOp::ShapeMask { input, masks } => StepOp::ShapeMask {
input: at(input),
masks: masks.clone(),
},
};
self.push(Step {
node: id(),
op,
offset: scope.offset,
})
}
fn push(&mut self, step: Step) -> Result<usize, PlanError> {
if self.steps.len() >= MAX_INLINED_NODES {
return Err(PlanError::TooLarge);
}
let bytes = step_bytes(&step);
if bytes > self.budget.left {
self.budget.exceeded = true;
return Err(PlanError::TooLarge);
}
self.budget.left -= bytes;
self.steps.push(step);
Ok(self.steps.len() - 1)
}
fn group(
&mut self,
scope: &Scope<'a>,
node: &NodeId,
composition: &CompositionId,
inputs: Vec<Option<usize>>,
controls: &'a std::collections::BTreeMap<String, Animated<EffectValue>>,
) -> Result<usize, PlanError> {
let target = self.composition(composition);
let depth = nested(scope, node)?;
let controls = target
.controls
.iter()
.filter_map(|control| {
controls
.get(&control.name)
.map(|value| ((&control.node, control.param.as_str()), value))
})
.collect();
self.output(&Scope {
composition: target,
inputs,
controls,
offset: scope.offset,
prefix: format!("{node}/"),
clip_allowed: scope.clip_allowed,
group_inputs_bound: true,
depth,
})
}
fn composition_source(
&mut self,
scope: &Scope<'a>,
node: &NodeId,
composition: &CompositionId,
start: MediaTime,
) -> Result<usize, PlanError> {
let depth = nested(scope, node)?;
self.output(&Scope {
composition: self.composition(composition),
inputs: Vec::new(),
controls: HashMap::new(),
offset: MediaTime(scope.offset.ticks().saturating_add(start.ticks())),
prefix: format!("{node}/"),
clip_allowed: false,
group_inputs_bound: false,
depth,
})
}
fn composition(&self, id: &CompositionId) -> &'a Composition {
self.lookup
.compositions
.get(id)
.copied()
.expect("validation guarantees referenced compositions exist")
}
}
fn nested(scope: &Scope<'_>, node: &NodeId) -> Result<usize, PlanError> {
let depth = scope.depth + 1;
if depth > MAX_COMPOSITION_DEPTH {
return Err(PlanError::NestedTooDeep { node: node.clone() });
}
Ok(depth)
}
fn scoped(prefix: &str, id: &NodeId) -> NodeId {
if prefix.is_empty() {
return id.clone();
}
NodeId::new(format!("{prefix}{id}")).expect("a prefixed id is not empty")
}
fn controlled(scope: &Scope<'_>, node: &NodeId, effect: &ClipEffect) -> ClipEffect {
let mut effect = effect.clone();
for ((target, param), value) in &scope.controls {
if *target == node {
effect.params.insert((*param).to_owned(), (*value).clone());
}
}
effect
}
fn step_bytes(step: &Step) -> u64 {
let heap = match &step.op {
StepOp::Clip | StepOp::Matte { .. } => 0,
StepOp::Media { source_id, .. } => source_id.as_str().len(),
StepOp::Generator(generator) => generator_bytes(generator),
StepOp::Effect { effect, .. } => effect_bytes(effect),
StepOp::Transition {
effect, progress, ..
} => effect_bytes(effect) + keyframe_bytes(progress),
StepOp::Transform { transform, .. } => transform_bytes(transform),
StepOp::Merge { layers } => layers
.iter()
.map(|layer| size_of::<StepLayer>() + keyframe_bytes(&layer.opacity))
.sum(),
StepOp::ShapeMask { masks, .. } => masks.iter().map(mask_bytes).sum(),
};
u64::try_from(size_of::<Step>() + step.node.as_str().len() + heap).unwrap_or(u64::MAX)
}
fn keyframe_bytes<T>(value: &Animated<T>) -> usize {
match value {
Animated::Constant { .. } => 0,
Animated::Keyframes { keyframes } => keyframes.len() * size_of::<Keyframe<T>>(),
}
}
fn effect_bytes(effect: &ClipEffect) -> usize {
effect.id.len()
+ effect
.params
.iter()
.map(|(name, value)| {
name.len() + size_of::<Animated<EffectValue>>() + keyframe_bytes(value)
})
.sum::<usize>()
+ effect
.resources
.iter()
.map(|(name, id)| name.len() + id.as_str().len())
.sum::<usize>()
}
fn transform_bytes(transform: &VisualProperties) -> usize {
keyframe_bytes(&transform.position)
+ keyframe_bytes(&transform.scale)
+ keyframe_bytes(&transform.rotation_degrees)
+ keyframe_bytes(&transform.opacity)
}
fn mask_bytes(mask: &ShapeMask) -> usize {
let corner = match &mask.shape {
MaskShape::Rectangle { corner_radius } => keyframe_bytes(corner_radius),
MaskShape::Ellipse => 0,
};
size_of::<ShapeMask>()
+ keyframe_bytes(&mask.center)
+ keyframe_bytes(&mask.size)
+ keyframe_bytes(&mask.rotation_degrees)
+ keyframe_bytes(&mask.feather)
+ corner
}
fn generator_bytes(generator: &Generator) -> usize {
let paint = |paint: &Paint| match paint {
Paint::Solid { .. } => 0,
Paint::LinearGradient { stops, .. }
| Paint::RadialGradient { stops, .. }
| Paint::ConicGradient { stops, .. } => {
stops.len() * size_of::<alpha2_model::GradientStop>()
}
};
match generator {
Generator::Fill { paint: fill } => paint(fill),
Generator::Shape { fill, stroke, .. } => {
fill.as_ref().map_or(0, paint)
+ stroke.as_ref().map_or(0, |stroke| paint(&stroke.paint))
}
Generator::Text { text, style } => {
text.len() + style.font_family.as_ref().map_or(0, String::len)
}
}
}
fn needed_inputs<'a>(
reads: &CompositionReads<'_>,
prefix: &str,
node: &'a alpha2_model::CompositionNode,
) -> Result<Vec<&'a NodeId>, PlanError> {
let needed = reads.needed_inputs(node);
if node.bypass && needed.is_empty() {
return Err(PlanError::MissingInput {
node: scoped(prefix, &node.id),
input: "layers[0]".into(),
});
}
needed
.into_iter()
.map(|input| {
input.node().ok_or_else(|| PlanError::MissingInput {
node: scoped(prefix, &node.id),
input: input.name(),
})
})
.collect()
}
#[derive(Clone, Debug)]
pub(crate) struct IndexedComposition {
pub(crate) plan: Arc<Result<CompositionPlan, PlanError>>,
pub(crate) reference: alpha2_model::ClipComposition,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ActiveComposition<'a> {
plan: &'a CompositionPlan,
time: MediaTime,
}
impl<'a> ActiveComposition<'a> {
#[must_use]
pub const fn new(plan: &'a CompositionPlan, time: MediaTime) -> Self {
Self { plan, time }
}
#[must_use]
pub const fn plan(&self) -> &'a CompositionPlan {
self.plan
}
#[must_use]
pub const fn time(&self) -> MediaTime {
self.time
}
pub fn media(&self) -> impl Iterator<Item = (usize, &'a SourceId, MediaTime)> + 'a {
let time = self.time;
self.plan
.steps
.iter()
.enumerate()
.filter_map(move |(index, step)| match &step.op {
StepOp::Media { source_id, start } => Some((
index,
source_id,
MediaTime(start.ticks().saturating_add(step.time(time).ticks())),
)),
_ => None,
})
}
}
#[cfg(test)]
#[path = "composition_tests.rs"]
mod tests;