animot-presenter 0.5.13 → 0.5.14

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,30 @@
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
+ function playNarrationForSlide(index: number) {
195
+ if (!project?.settings?.narrationEnabled) return;
196
+ const slide = project?.slides?.[index];
197
+ if (narrationAudio) {
198
+ narrationAudio.pause();
199
+ narrationAudio.currentTime = 0;
200
+ }
201
+ const src = slide?.narration?.src;
202
+ if (!src) return;
203
+ if (!narrationAudio) narrationAudio = new Audio();
204
+ 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
+ narrationAudio.play().catch(() => {});
209
+ }
210
+ function stopNarration() {
211
+ if (narrationAudio) { narrationAudio.pause(); narrationAudio = null; }
212
+ }
213
+
190
214
  const slides = $derived(project?.slides ?? []);
191
215
  const currentSlide = $derived(slides[currentSlideIndex]);
192
216
  const isCinemaMode = $derived(project?.mode === 'cinema');
@@ -547,6 +571,12 @@
547
571
  const targetSlide = slides[targetIndex];
548
572
  clearAllTypewriterAnimations();
549
573
  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);
550
580
  const transition = targetSlide.transition;
551
581
  const duration = durationOverride ?? transition.duration;
552
582
  transitionDurationMs = duration;
@@ -1106,6 +1136,11 @@
1106
1136
  await loadCodeHighlights();
1107
1137
  loading = false;
1108
1138
  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);
1109
1144
  if (autoplay) isAutoplay = true;
1110
1145
  } catch (e: any) { error = e.message; loading = false; }
1111
1146
  }
@@ -1123,7 +1158,7 @@
1123
1158
  });
1124
1159
  if (containerEl) resizeObserver.observe(containerEl);
1125
1160
  resetMouseIdleTimer();
1126
- return () => { resizeObserver?.disconnect(); clearAutoplayTimer(); clearAllTypewriterAnimations(); if (mouseIdleTimer) clearTimeout(mouseIdleTimer); };
1161
+ return () => { resizeObserver?.disconnect(); clearAutoplayTimer(); clearAllTypewriterAnimations(); if (mouseIdleTimer) clearTimeout(mouseIdleTimer); stopNarration(); };
1127
1162
  });
1128
1163
 
1129
1164
  // Watch for prop changes