animot-presenter 0.5.13 → 0.5.15

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.
@@ -187,6 +187,44 @@
187
187
  let menuVisible = $state(true);
188
188
  let mouseIdleTimer: ReturnType<typeof setTimeout> | null = null;
189
189
 
190
+ // Single <audio> element for per-slide narration playback. Only used
191
+ // when `project.settings.narrationEnabled` is true. Lazy-allocated so
192
+ // decks without narration don't pay for it.
193
+ 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
+ function playNarrationForSlide(index: number) {
200
+ if (!project?.settings?.narrationEnabled) return;
201
+ const slide = project?.slides?.[index];
202
+ if (narrationAudio) {
203
+ narrationAudio.pause();
204
+ narrationAudio.currentTime = 0;
205
+ }
206
+ const src = slide?.narration?.src;
207
+ if (!src) return;
208
+ if (!narrationAudio) narrationAudio = new Audio();
209
+ 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
+ );
217
+ }
218
+ function tryUnlockNarrationOnGesture() {
219
+ if (narrationPendingGesture) {
220
+ playNarrationForSlide(currentSlideIndex);
221
+ }
222
+ }
223
+ function stopNarration() {
224
+ if (narrationAudio) { narrationAudio.pause(); narrationAudio = null; }
225
+ narrationPendingGesture = false;
226
+ }
227
+
190
228
  const slides = $derived(project?.slides ?? []);
191
229
  const currentSlide = $derived(slides[currentSlideIndex]);
192
230
  const isCinemaMode = $derived(project?.mode === 'cinema');
@@ -547,6 +585,12 @@
547
585
  const targetSlide = slides[targetIndex];
548
586
  clearAllTypewriterAnimations();
549
587
  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);
550
594
  const transition = targetSlide.transition;
551
595
  const duration = durationOverride ?? transition.duration;
552
596
  transitionDurationMs = duration;
@@ -1106,6 +1150,11 @@
1106
1150
  await loadCodeHighlights();
1107
1151
  loading = false;
1108
1152
  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);
1109
1158
  if (autoplay) isAutoplay = true;
1110
1159
  } catch (e: any) { error = e.message; loading = false; }
1111
1160
  }
@@ -1123,7 +1172,24 @@
1123
1172
  });
1124
1173
  if (containerEl) resizeObserver.observe(containerEl);
1125
1174
  resetMouseIdleTimer();
1126
- return () => { resizeObserver?.disconnect(); clearAutoplayTimer(); clearAllTypewriterAnimations(); if (mouseIdleTimer) clearTimeout(mouseIdleTimer); };
1175
+
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
+ return () => {
1185
+ resizeObserver?.disconnect();
1186
+ clearAutoplayTimer();
1187
+ clearAllTypewriterAnimations();
1188
+ if (mouseIdleTimer) clearTimeout(mouseIdleTimer);
1189
+ stopNarration();
1190
+ document.removeEventListener('pointerdown', tryUnlockNarrationOnGesture, { capture: true });
1191
+ document.removeEventListener('keydown', tryUnlockNarrationOnGesture, { capture: true });
1192
+ };
1127
1193
  });
1128
1194
 
1129
1195
  // Watch for prop changes