animot-presenter 0.5.14 → 0.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -190,6 +190,13 @@
190
190
  // Single <audio> element for per-slide narration playback. Only used
191
191
  // when `project.settings.narrationEnabled` is true. Lazy-allocated so
192
192
  // decks without narration don't pay for it.
193
+ //
194
+ // Narration is bound to the deck's PLAY state (`isAutoplay`), not to
195
+ // arbitrary page clicks: the user pressing the play button is what
196
+ // starts narration, the pause button stops it. This keeps multiple
197
+ // decks on a page from playing each other's audio when one is clicked,
198
+ // and keeps autoplay-blocked browsers from silently doing nothing —
199
+ // the user's click on the play control is itself the unlocking gesture.
193
200
  let narrationAudio: HTMLAudioElement | null = null;
194
201
  function playNarrationForSlide(index: number) {
195
202
  if (!project?.settings?.narrationEnabled) return;
@@ -202,11 +209,11 @@
202
209
  if (!src) return;
203
210
  if (!narrationAudio) narrationAudio = new Audio();
204
211
  narrationAudio.src = src;
205
- // First play() may reject under autoplay policy; the player's first
206
- // user gesture (pressing play / clicking the deck) unlocks the audio
207
- // context and subsequent slides will work.
208
212
  narrationAudio.play().catch(() => {});
209
213
  }
214
+ function pauseNarration() {
215
+ if (narrationAudio) narrationAudio.pause();
216
+ }
210
217
  function stopNarration() {
211
218
  if (narrationAudio) { narrationAudio.pause(); narrationAudio = null; }
212
219
  }
@@ -571,12 +578,10 @@
571
578
  const targetSlide = slides[targetIndex];
572
579
  clearAllTypewriterAnimations();
573
580
  cancelMotionPathLoops();
574
- // Trigger per-slide narration. No-op when `narrationEnabled` is off
575
- // or the slide has no narration clip. First call may be blocked by
576
- // the browser's autoplay policy; the deck consumer should suppress
577
- // `autoplay` when narration is on so the viewer's first nav-click
578
- // (which gets us here) acts as the unlocking gesture.
579
- playNarrationForSlide(targetIndex);
581
+ // Trigger per-slide narration only while the deck is actively
582
+ // playing. Manual nav while paused stays silent narration is
583
+ // bound to the play button, not to every interaction.
584
+ if (isAutoplay) playNarrationForSlide(targetIndex);
580
585
  const transition = targetSlide.transition;
581
586
  const duration = durationOverride ?? transition.duration;
582
587
  transitionDurationMs = duration;
@@ -1136,11 +1141,9 @@
1136
1141
  await loadCodeHighlights();
1137
1142
  loading = false;
1138
1143
  if (currentSlide) setTimeout(() => animateMotionPaths(currentSlide!), 300);
1139
- // First-slide narration. Browsers may block this until the
1140
- // viewer interacts; downstream code (e.g. share-link page)
1141
- // should already disable autoplay when narration is on so the
1142
- // click that starts the deck doubles as the audio unlock.
1143
- playNarrationForSlide(currentSlideIndex);
1144
+ // Narration starts via the play-state effect below — not on
1145
+ // mount. That way the user's click on Play is the gesture
1146
+ // that unlocks audio, and a paused deck stays silent.
1144
1147
  if (autoplay) isAutoplay = true;
1145
1148
  } catch (e: any) { error = e.message; loading = false; }
1146
1149
  }
@@ -1158,7 +1161,28 @@
1158
1161
  });
1159
1162
  if (containerEl) resizeObserver.observe(containerEl);
1160
1163
  resetMouseIdleTimer();
1161
- return () => { resizeObserver?.disconnect(); clearAutoplayTimer(); clearAllTypewriterAnimations(); if (mouseIdleTimer) clearTimeout(mouseIdleTimer); stopNarration(); };
1164
+
1165
+ return () => {
1166
+ resizeObserver?.disconnect();
1167
+ clearAutoplayTimer();
1168
+ clearAllTypewriterAnimations();
1169
+ if (mouseIdleTimer) clearTimeout(mouseIdleTimer);
1170
+ stopNarration();
1171
+ };
1172
+ });
1173
+
1174
+ // Narration follows the deck's play/pause state. The play button click
1175
+ // flips `isAutoplay` true → this effect fires → audio starts. The
1176
+ // click itself is the user gesture that unlocks the browser audio
1177
+ // context. Pause/stop turns it back off. Each presenter instance
1178
+ // scopes its own audio, so multiple decks on a page never overlap.
1179
+ $effect(() => {
1180
+ if (!project?.settings?.narrationEnabled) return;
1181
+ if (isAutoplay) {
1182
+ playNarrationForSlide(currentSlideIndex);
1183
+ } else {
1184
+ pauseNarration();
1185
+ }
1162
1186
  });
1163
1187
 
1164
1188
  // Watch for prop changes