animot-presenter 0.5.15 → 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,12 +190,14 @@
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
- // Set when the most recent play() was rejected by the browser autoplay
195
- // policy. The first user gesture clears it and replays the current
196
- // slide's clip — without this a single-slide deck would never play
197
- // narration, since there's no navigation event to retry on.
198
- let narrationPendingGesture = false;
199
201
  function playNarrationForSlide(index: number) {
200
202
  if (!project?.settings?.narrationEnabled) return;
201
203
  const slide = project?.slides?.[index];
@@ -207,22 +209,13 @@
207
209
  if (!src) return;
208
210
  if (!narrationAudio) narrationAudio = new Audio();
209
211
  narrationAudio.src = src;
210
- // First play() may reject under autoplay policy. We track the
211
- // rejection so a subsequent user gesture (handled below) retries
212
- // once instead of silently giving up.
213
- narrationAudio.play().then(
214
- () => { narrationPendingGesture = false; },
215
- () => { narrationPendingGesture = true; }
216
- );
212
+ narrationAudio.play().catch(() => {});
217
213
  }
218
- function tryUnlockNarrationOnGesture() {
219
- if (narrationPendingGesture) {
220
- playNarrationForSlide(currentSlideIndex);
221
- }
214
+ function pauseNarration() {
215
+ if (narrationAudio) narrationAudio.pause();
222
216
  }
223
217
  function stopNarration() {
224
218
  if (narrationAudio) { narrationAudio.pause(); narrationAudio = null; }
225
- narrationPendingGesture = false;
226
219
  }
227
220
 
228
221
  const slides = $derived(project?.slides ?? []);
@@ -585,12 +578,10 @@
585
578
  const targetSlide = slides[targetIndex];
586
579
  clearAllTypewriterAnimations();
587
580
  cancelMotionPathLoops();
588
- // Trigger per-slide narration. No-op when `narrationEnabled` is off
589
- // or the slide has no narration clip. First call may be blocked by
590
- // the browser's autoplay policy; the deck consumer should suppress
591
- // `autoplay` when narration is on so the viewer's first nav-click
592
- // (which gets us here) acts as the unlocking gesture.
593
- 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);
594
585
  const transition = targetSlide.transition;
595
586
  const duration = durationOverride ?? transition.duration;
596
587
  transitionDurationMs = duration;
@@ -1150,11 +1141,9 @@
1150
1141
  await loadCodeHighlights();
1151
1142
  loading = false;
1152
1143
  if (currentSlide) setTimeout(() => animateMotionPaths(currentSlide!), 300);
1153
- // First-slide narration. Browsers may block this until the
1154
- // viewer interacts; downstream code (e.g. share-link page)
1155
- // should already disable autoplay when narration is on so the
1156
- // click that starts the deck doubles as the audio unlock.
1157
- 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.
1158
1147
  if (autoplay) isAutoplay = true;
1159
1148
  } catch (e: any) { error = e.message; loading = false; }
1160
1149
  }
@@ -1173,25 +1162,29 @@
1173
1162
  if (containerEl) resizeObserver.observe(containerEl);
1174
1163
  resetMouseIdleTimer();
1175
1164
 
1176
- // First-gesture retry for narration playback. Browsers block
1177
- // audio.play() until the user has interacted; without this hook
1178
- // a single-slide deck would never play its narration since there's
1179
- // no slide change to retry on. Attached on document so any click
1180
- // or key press in the page acts as the unlocking gesture.
1181
- document.addEventListener('pointerdown', tryUnlockNarrationOnGesture, { capture: true });
1182
- document.addEventListener('keydown', tryUnlockNarrationOnGesture, { capture: true });
1183
-
1184
1165
  return () => {
1185
1166
  resizeObserver?.disconnect();
1186
1167
  clearAutoplayTimer();
1187
1168
  clearAllTypewriterAnimations();
1188
1169
  if (mouseIdleTimer) clearTimeout(mouseIdleTimer);
1189
1170
  stopNarration();
1190
- document.removeEventListener('pointerdown', tryUnlockNarrationOnGesture, { capture: true });
1191
- document.removeEventListener('keydown', tryUnlockNarrationOnGesture, { capture: true });
1192
1171
  };
1193
1172
  });
1194
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
+ }
1186
+ });
1187
+
1195
1188
  // Watch for prop changes
1196
1189
  $effect(() => { if (data) { project = data; } });
1197
1190
  </script>