Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions crates/project/examples/transition-mapping-benchmark.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use cap_project::{ClipTransition, ClipTransitionType, TimelineConfiguration};
use std::{
collections::hash_map::DefaultHasher,
error::Error,
hash::{Hash, Hasher},
hint::black_box,
time::Instant,
};

fn main() -> Result<(), Box<dyn Error>> {
println!("clips,stride,sample,ns_per_mapping,fingerprint");
for (clips, stride) in [(20, 0), (20, 1), (20, 4), (100, 1), (100, 4), (1_000, 1)] {
let segments: Vec<_> = (0..clips)
.map(|index| {
let start = (index % 3) as f64 * 0.125;
serde_json::json!({
"start": start,
"end": start + 2.0 + (index % 7) as f64 * 0.5,
"timescale": ([1.0, 0.5, 2.0][index % 3]),
"recordingSegment": index % 3,
})
})
.collect();
let mut timeline: TimelineConfiguration = serde_json::from_value(serde_json::json!({
"segments": segments,
"zoomSegments": [],
}))?;
if stride != 0 {
timeline.transitions = (1..clips)
.filter(|index| index % stride == 0)
.map(|index| ClipTransition {
segment_index: index as u32,
kind: if index % 2 == 0 {
ClipTransitionType::CrossFade
} else {
ClipTransitionType::FadeThroughBlack
},
duration: 0.25,
})
.collect();
}
let duration = timeline.duration();
let times: Vec<_> = (0..1_024)
.map(|index| duration * index as f64 / 1_024.0)
.collect();
let mut output = vec![None; times.len()];
for iteration in 0..28 {
let started = Instant::now();
for (result, time) in output.iter_mut().zip(&times) {
*result = black_box(black_box(&timeline).get_frame_mapping(black_box(*time)));
}
let elapsed_ns = started.elapsed().as_nanos() as f64 / times.len() as f64;
let mut hash = DefaultHasher::new();
format!("{output:?}").hash(&mut hash);
assert!(output.iter().all(Option::is_some));
if iteration >= 3 {
println!(
"{clips},{stride},{},{elapsed_ns:.6},{}",
iteration - 3,
hash.finish()
);
}
}
}
Ok(())
}
100 changes: 99 additions & 1 deletion crates/project/src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2089,9 +2089,9 @@ impl TimelineConfiguration {
}

let mut segment_start = 0.0;
let mut incoming: Option<ClipTransition> = None;

for (segment_index, segment) in self.segments.iter().enumerate() {
let incoming = self.effective_transition(segment_index);
let incoming_duration = incoming.as_ref().map_or(0.0, |value| value.duration);

if let Some(transition) = incoming {
Expand Down Expand Up @@ -2141,6 +2141,7 @@ impl TimelineConfiguration {
}

segment_start += segment.duration() - next_duration;
incoming = next_transition;
}

None
Expand Down Expand Up @@ -4058,6 +4059,103 @@ mod tests {
));
}

#[test]
fn adjacent_transition_mapping_matches_independent_clip_offsets() {
let mut timeline = timeline_with_transitions(Vec::new());
let template = timeline.segments[0].clone();
timeline.segments = (0..8)
.map(|index| {
let timescale = [0.5, 1.0, 2.0][index % 3];
TimelineSegment {
recording_clip: index as u32,
start: index as f64 * 10.0,
end: index as f64 * 10.0 + 4.0 * timescale,
timescale,
..template.clone()
}
})
.collect();
timeline.transitions = [1.0, 0.5, 999.0, f64::NAN, 0.0, 1.0, f64::INFINITY, 1.5, 1.0]
.into_iter()
.enumerate()
.map(|(index, duration)| ClipTransition {
segment_index: index as u32,
duration,
kind: if index % 2 == 0 {
ClipTransitionType::CrossFade
} else {
ClipTransitionType::FadeThroughBlack
},
})
.collect();
let offsets = crate::caption_timing::clip_timeline_offsets(&timeline);
for (index, segment) in timeline.segments.iter().enumerate() {
let start = offsets[index];
let incoming = timeline.effective_transition(index);
let next = timeline.effective_transition(index + 1);
let single_start = start + incoming.map_or(0.0, |transition| transition.duration);
let end =
start + segment.duration() - next.map_or(0.0, |transition| transition.duration);
for time in [single_start, (single_start + end) / 2.0, end.next_down()] {
let Some(TimelineFrameMapping::Single { source, output_end }) =
timeline.get_frame_mapping(time)
else {
panic!("expected single source at {time}");
};
assert_eq!(source.segment_index, index);
assert!(std::ptr::eq(source.segment, segment));
assert_eq!(
source.source_time.to_bits(),
(segment.start + (time - start) * segment.timescale).to_bits()
);
assert_eq!(output_end.to_bits(), end.to_bits());
}
if let Some(expected) = incoming {
for elapsed in [0.0, expected.duration / 2.0, expected.duration.next_down()] {
let time = start + elapsed;
if time >= start + expected.duration {
continue;
}
let Some(TimelineFrameMapping::Transition {
outgoing,
incoming,
kind,
progress,
duration,
output_end,
}) = timeline.get_frame_mapping(time)
else {
panic!("expected transition at {time}");
};
let previous = &timeline.segments[index - 1];
let elapsed = time - start;
assert_eq!(
(outgoing.segment_index, incoming.segment_index),
(index - 1, index)
);
assert!(std::ptr::eq(outgoing.segment, previous));
assert!(std::ptr::eq(incoming.segment, segment));
assert_eq!(
outgoing.source_time.to_bits(),
(previous.end - (expected.duration - elapsed) * previous.timescale)
.to_bits()
);
assert_eq!(
incoming.source_time.to_bits(),
(segment.start + elapsed * segment.timescale).to_bits()
);
assert_eq!(kind, expected.kind);
assert_eq!(duration.to_bits(), expected.duration.to_bits());
assert_eq!(progress.to_bits(), (elapsed / duration).to_bits());
assert_eq!(output_end.to_bits(), (start + duration).to_bits());
}
}
}
for time in [-1.0, timeline.duration(), f64::INFINITY, f64::NAN] {
assert!(timeline.get_frame_mapping(time).is_none());
}
}

#[test]
fn timeline_segment_volume_is_backward_compatible_and_bounded() {
let mut segment: TimelineSegment = serde_json::from_value(serde_json::json!({
Expand Down
Loading