Media: the playing clip decides whether it carries over (#994)

The carry-over check looked the clip up again on the previous slide. If
that slide held the same media under the same id without Play Across
Slides, the voice stopped one slide later. A slide without the id fell
back to the first match in the deck. Use the clip the voice was started
with; the next slide is only checked for another clip under that id.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Faabyy
2026-10-18 20:20:29 +00:00
co-authored by Claude
parent ad4e3f0e4c
commit 0957b58185
2 changed files with 21 additions and 18 deletions
+19 -16
View File
@@ -176,6 +176,11 @@ impl MediaHost {
self.owners.get(&id).map(|o| o.owner)
}
/// The clip voice `id` was started with.
pub fn clip(&self, id: ShapeId) -> Option<&MediaClip> {
self.owners.get(&id).map(|o| &o.clip)
}
/// Shapes with a voice (playing, paused or ended).
pub fn ids(&self) -> Vec<ShapeId> {
self.owners.keys().copied().collect()
@@ -353,11 +358,10 @@ fn walk_clip(shapes: &[deckcraft_model::Shape], id: ShapeId) -> Option<MediaClip
})
}
/// Whether the voice `id`, playing on slide `from`, keeps playing on slide `to`: its clip plays
/// across slides and `to` doesn't hold another clip under the same id.
pub fn carries_over(doc: &Presentation, id: ShapeId, from: Option<usize>, to: usize) -> bool {
let Some(clip) = find_clip(doc, id, from).filter(|c| c.play_across_slides) else { return false };
clip_on(doc, to, id).is_none_or(|c| c.media == clip.media)
/// Whether the voice `id`, playing `clip` (the clip it was started with), keeps playing on slide
/// `to`: the clip plays across slides and `to` doesn't hold another clip under the same id.
pub fn carries_over(clip: &MediaClip, doc: &Presentation, id: ShapeId, to: usize) -> bool {
clip.play_across_slides && clip_on(doc, to, id).is_none_or(|c| c.media == clip.media)
}
/// Media shapes directly on slide `index`: (id, box in slide points, clip).
@@ -650,23 +654,22 @@ mod tests {
#[test]
fn a_clip_across_slides_stops_where_the_next_slide_reuses_its_id() {
let (mut doc, id, first) = deck(4000, wav_level(0.05, 0.5));
if let ShapeKind::Media(m) = &mut Arc::make_mut(&mut doc.slides[0]).shapes.last_mut().expect("shape").kind {
m.play_across_slides = true;
}
let playing = MediaClip { play_across_slides: true, ..first.clone() };
let mut plain = (*doc.slides[0]).clone();
plain.shapes.clear();
doc.slides.push(Arc::new(plain));
assert!(carries_over(&doc, id, Some(0), 1), "a slide without that id");
let mut same = (*doc.slides[0]).clone();
if let ShapeKind::Media(m) = &mut same.shapes.last_mut().expect("shape").kind {
m.play_across_slides = false;
}
doc.slides.push(Arc::new(same));
assert!(carries_over(&doc, id, Some(0), 2), "the same media under that id");
assert!(carries_over(&playing, &doc, id, 1), "a slide without that id");
// Slide 0 holds the same media under that id, without Play Across Slides: the voice that
// came from elsewhere goes on (its own clip decides), here and on the next slide.
assert!(carries_over(&playing, &doc, id, 0), "the same media under that id");
assert!(!carries_over(&first, &doc, id, 1), "a clip that doesn't play across slides");
let other = doc.add_media("two.wav", "audio/wav", wav_level(0.05, 0.25)).expect("media");
let mut reused = (*doc.slides[0]).clone();
reused.shapes.last_mut().expect("shape").kind = ShapeKind::Media(MediaClip { media: other, volume: 1.0, autoplay: true, ..first.clone() });
doc.slides.push(Arc::new(reused));
assert!(!carries_over(&doc, id, Some(0), 3), "another clip under that id");
assert!(!carries_over(&playing, &doc, id, 2), "another clip under that id");
let mut host = MediaHost::new(None);
host.play(1, &doc, id, &playing, None, Owner::Show);
assert_eq!(host.clip(id), Some(&playing), "the voice keeps the clip it was started with");
}
}
+2 -2
View File
@@ -1070,7 +1070,7 @@ fn show_media(app: &mut SlideApp, show: &mut Show, doc_uid: u64, doc: &Presentat
if show.media_slide == cur {
return;
}
let previous = std::mem::replace(&mut show.media_slide, cur);
show.media_slide = cur;
let trans = cur.and_then(|i| doc.slides.get(i)).and_then(|s| s.transition.as_ref());
let sound = trans.and_then(|t| t.sound);
let sound_loop = trans.is_some_and(|t| t.sound_loop);
@@ -1081,7 +1081,7 @@ fn show_media(app: &mut SlideApp, show: &mut Show, doc_uid: u64, doc: &Presentat
.media
.ids()
.into_iter()
.filter(|id| crate::media::carries_over(doc, *id, previous, i))
.filter(|id| app.media.clip(*id).is_some_and(|c| crate::media::carries_over(c, doc, *id, i)))
// A transition sound plays to its end, as in PowerPoint, unless the new slide has its own.
.chain((sound.is_none() && !stop_previous).then_some(TRANSITION_SOUND).filter(|id| app.media.is_playing(*id)))
.collect(),