@bendyline/squisq-react 2.11.6 → 2.11.8

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.
@@ -1,7 +1,11 @@
1
1
  import {
2
2
  BlockRenderer,
3
3
  LinearDocView
4
- } from "./chunk-5UGIE2QO.js";
4
+ } from "./chunk-LDXH4FOY.js";
5
+ import {
6
+ isCaptureDrivenVideo,
7
+ seekVideoToFrame
8
+ } from "./chunk-W6MKRWL6.js";
5
9
  import {
6
10
  useAudioSync,
7
11
  useDocPlayback,
@@ -13,7 +17,7 @@ import {
13
17
  } from "./chunk-TT6ENR6T.js";
14
18
  import {
15
19
  MarkdownRenderer
16
- } from "./chunk-ZY5N3JPM.js";
20
+ } from "./chunk-6PUMLLJH.js";
17
21
  import {
18
22
  useMediaProvider,
19
23
  useMediaUrl,
@@ -22,8 +26,74 @@ import {
22
26
 
23
27
  // src/MediaClipLayer.tsx
24
28
  import { useEffect, useRef } from "react";
29
+ import { serializeMediaCrop } from "@bendyline/squisq/mediaEdit";
30
+
31
+ // src/mediaClipPlayback.ts
32
+ function clipLevelAt(clip, time) {
33
+ let level = clip.gain ? Math.pow(10, clip.gain / 20) : 1;
34
+ if (clip.fadeIn && time < clip.absoluteStart + clip.fadeIn) {
35
+ level *= Math.max(0, (time - clip.absoluteStart) / clip.fadeIn);
36
+ }
37
+ if (clip.fadeOut && time > clip.absoluteEnd - clip.fadeOut) {
38
+ level *= Math.max(0, (clip.absoluteEnd - time) / clip.fadeOut);
39
+ }
40
+ return level;
41
+ }
42
+ function mountedClips(clips, time) {
43
+ const out = [];
44
+ const groups = /* @__PURE__ */ new Map();
45
+ for (const clip of clips) {
46
+ if (!clip.segment) {
47
+ out.push({ key: clip.id, clip });
48
+ continue;
49
+ }
50
+ const group = groups.get(clip.segment.groupId) ?? [];
51
+ group.push(clip);
52
+ groups.set(clip.segment.groupId, group);
53
+ }
54
+ for (const [groupId, segments] of groups) {
55
+ segments.sort((a, b) => (a.segment?.index ?? 0) - (b.segment?.index ?? 0));
56
+ let current = segments.findIndex((s) => time < s.absoluteEnd);
57
+ if (current === -1) current = segments.length - 1;
58
+ for (const index of [current, current + 1]) {
59
+ const clip = segments[index];
60
+ if (clip) out.push({ key: `${groupId}#slot${index % 2}`, clip });
61
+ }
62
+ }
63
+ return out;
64
+ }
65
+
66
+ // src/MediaClipLayer.tsx
25
67
  import { Fragment, jsx } from "react/jsx-runtime";
26
68
  var DRIFT = 0.25;
69
+ var PRESEEK = 0.75;
70
+ var boostContext = null;
71
+ var boostNodes = /* @__PURE__ */ new WeakMap();
72
+ function boostNodeFor(el) {
73
+ const existing = boostNodes.get(el);
74
+ if (existing) return existing;
75
+ if (typeof AudioContext === "undefined") return null;
76
+ try {
77
+ boostContext ?? (boostContext = new AudioContext());
78
+ const gain = boostContext.createGain();
79
+ boostContext.createMediaElementSource(el).connect(gain).connect(boostContext.destination);
80
+ boostNodes.set(el, gain);
81
+ return gain;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+ function applyLevel(el, level) {
87
+ const boost = level > 1 ? boostNodeFor(el) : boostNodes.get(el);
88
+ if (boost) {
89
+ el.volume = 1;
90
+ boost.gain.value = level;
91
+ if (boostContext?.state === "suspended") void boostContext.resume().catch(() => {
92
+ });
93
+ return;
94
+ }
95
+ el.volume = Math.min(1, Math.max(0, level));
96
+ }
27
97
  function mediaGroupStyle(presentation) {
28
98
  return {
29
99
  position: "absolute",
@@ -76,6 +146,12 @@ function mediaVideoStyle({
76
146
  ...pipCornerStyle(pipPosition)
77
147
  };
78
148
  }
149
+ function cropStyle(crop) {
150
+ if (!crop) return {};
151
+ const pct = (n) => `${(n * 100).toFixed(3)}%`;
152
+ const inset = `inset(${pct(crop.y)} ${pct(1 - crop.x - crop.w)} ${pct(1 - crop.y - crop.h)} ${pct(crop.x)})`;
153
+ return { objectViewBox: inset };
154
+ }
79
155
  function MediaClipLayer({
80
156
  schedule,
81
157
  currentTime,
@@ -93,8 +169,10 @@ function MediaClipLayer({
93
169
  }) {
94
170
  const { renderClips, activeIds } = useMediaSchedule(schedule, currentTime);
95
171
  if (renderClips.length === 0) return null;
172
+ const mounted = mountedClips(renderClips, currentTime);
96
173
  const groups = /* @__PURE__ */ new Map();
97
- for (const clip of renderClips) {
174
+ for (const entry of mounted) {
175
+ const { clip } = entry;
98
176
  const clipPresentation = honorClipPresentation && clip.kind === "video" ? clip.placement === "picture-in-picture" ? "picture-in-picture" : clip.placement === "overlay" ? "full-frame" : presentation : presentation;
99
177
  const clipPipSize = clip.pipSize ?? pipSize;
100
178
  const clipPipShape = clip.pipShape ?? pipShape;
@@ -107,7 +185,7 @@ function MediaClipLayer({
107
185
  pipPosition: clipPipPosition,
108
186
  clips: []
109
187
  };
110
- group.clips.push(clip);
188
+ group.clips.push(entry);
111
189
  groups.set(key, group);
112
190
  }
113
191
  return /* @__PURE__ */ jsx(Fragment, { children: [...groups].map(([key, group]) => /* @__PURE__ */ jsx(
@@ -120,7 +198,7 @@ function MediaClipLayer({
120
198
  "data-pip-position": group.pipPosition,
121
199
  "aria-hidden": true,
122
200
  style: mediaGroupStyle(group.presentation),
123
- children: group.clips.map((clip) => /* @__PURE__ */ jsx(
201
+ children: group.clips.map(({ key: elementKey, clip }) => /* @__PURE__ */ jsx(
124
202
  MediaClipElement,
125
203
  {
126
204
  clip,
@@ -137,7 +215,7 @@ function MediaClipLayer({
137
215
  pipOrientation,
138
216
  pipFrameStyle
139
217
  },
140
- clip.id
218
+ elementKey
141
219
  ))
142
220
  },
143
221
  key
@@ -165,10 +243,18 @@ function MediaClipElement({
165
243
  if (!el) return;
166
244
  if (!active) {
167
245
  if (!el.paused) el.pause();
246
+ const startsIn = clip.absoluteStart - currentTime;
247
+ if (!renderMode && startsIn > 0 && startsIn <= PRESEEK && !isCaptureDrivenVideo(el) && Math.abs(el.currentTime - clip.sourceIn) > DRIFT / 5) {
248
+ try {
249
+ el.currentTime = clip.sourceIn;
250
+ } catch {
251
+ }
252
+ }
168
253
  return;
169
254
  }
255
+ if (!renderMode) applyLevel(el, clipLevelAt(clip, currentTime));
170
256
  const target = Math.max(0, clip.sourceIn + (currentTime - clip.absoluteStart));
171
- if (el.dataset.captureSequential !== "true" && (renderMode || !isPlaying || Math.abs(el.currentTime - target) > DRIFT)) {
257
+ if (!isCaptureDrivenVideo(el) && (renderMode || !isPlaying || Math.abs(el.currentTime - target) > DRIFT)) {
172
258
  try {
173
259
  el.currentTime = target;
174
260
  } catch {
@@ -178,10 +264,10 @@ function MediaClipElement({
178
264
  const p = el.play();
179
265
  if (p) p.catch(() => {
180
266
  });
181
- } else if (el.dataset.captureSequential !== "true") {
267
+ } else if (!isCaptureDrivenVideo(el)) {
182
268
  el.pause();
183
269
  }
184
- }, [active, currentTime, isPlaying, renderMode, clip.sourceIn, clip.absoluteStart, src]);
270
+ }, [active, currentTime, isPlaying, renderMode, clip, src]);
185
271
  const isVideo = clip.kind === "video";
186
272
  const common = {
187
273
  ref,
@@ -200,17 +286,21 @@ function MediaClipElement({
200
286
  ...common,
201
287
  className: `doc-player__media-video${active ? " doc-player__media-video--active" : ""}`,
202
288
  "data-video-placement": clip.placement ?? "default",
203
- muted: renderMode || muted,
289
+ muted: renderMode || muted || clip.audioMuted === true,
204
290
  playsInline: true,
205
- style: mediaVideoStyle({
206
- presentation,
207
- pipSize,
208
- pipShape,
209
- pipPosition,
210
- pipOrientation,
211
- pipFrameStyle,
212
- active
213
- })
291
+ ...clip.crop ? { "data-crop": serializeMediaCrop(clip.crop) } : {},
292
+ style: {
293
+ ...mediaVideoStyle({
294
+ presentation,
295
+ pipSize,
296
+ pipShape,
297
+ pipPosition,
298
+ pipOrientation,
299
+ pipFrameStyle,
300
+ active
301
+ }),
302
+ ...cropStyle(clip.crop)
303
+ }
214
304
  }
215
305
  );
216
306
  }
@@ -2349,247 +2439,6 @@ function buildSegmentTitleMap(doc) {
2349
2439
  return map;
2350
2440
  }
2351
2441
 
2352
- // src/docPlayer/renderReadiness.ts
2353
- var DEFAULT_VIDEO_FRAME_TIMEOUT_MS = 2e3;
2354
- var VIDEO_FRAME_READINESS_POLL_MS = 16;
2355
- var VIDEO_TIME_TOLERANCE_SECONDS = 0.01;
2356
- var MAX_SEQUENTIAL_CAPTURE_STEP_SECONDS = 0.5;
2357
- var SEQUENTIAL_CAPTURE_PLAYBACK_RATE = 1;
2358
- function formatMediaTime(time) {
2359
- return Number.isFinite(time) ? `${time.toFixed(3)}s` : String(time);
2360
- }
2361
- function reachableMediaTime(video, targetTime) {
2362
- const duration = video.duration;
2363
- if (!Number.isFinite(duration)) return targetTime;
2364
- return Math.max(0, Math.min(targetTime, duration));
2365
- }
2366
- function isAtReachableMediaTime(video, targetTime) {
2367
- return Math.abs(video.currentTime - reachableMediaTime(video, targetTime)) <= VIDEO_TIME_TOLERANCE_SECONDS;
2368
- }
2369
- function isVisiblyPresented(video) {
2370
- if (!video.isConnected) return false;
2371
- const view = video.ownerDocument.defaultView;
2372
- if (!view) return false;
2373
- for (let element = video; element; element = element.parentElement) {
2374
- const style = view.getComputedStyle(element);
2375
- if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse" || Number.parseFloat(style.opacity || "1") <= 0) {
2376
- return false;
2377
- }
2378
- }
2379
- return true;
2380
- }
2381
- function createCaptureWatchdog(ownerDocument, timeoutMs, expire) {
2382
- let timeout = null;
2383
- const clear = () => {
2384
- if (timeout === null) return;
2385
- clearTimeout(timeout);
2386
- timeout = null;
2387
- };
2388
- return {
2389
- arm: () => {
2390
- clear();
2391
- if (ownerDocument.visibilityState !== "visible") return;
2392
- timeout = setTimeout(expire, timeoutMs);
2393
- },
2394
- clear
2395
- };
2396
- }
2397
- function seekVideoByAssignment(video, targetTime, timeoutMs = DEFAULT_VIDEO_FRAME_TIMEOUT_MS) {
2398
- video.pause();
2399
- return new Promise((resolve, reject) => {
2400
- let settled = false;
2401
- let videoFrameRequest = null;
2402
- const ownerDocument = video.ownerDocument;
2403
- const isInactive = () => ownerDocument.visibilityState !== "visible";
2404
- const cleanup = () => {
2405
- watchdog.clear();
2406
- clearInterval(readinessPoll);
2407
- video.removeEventListener("seeked", handleMediaReady);
2408
- video.removeEventListener("loadeddata", handleMediaReady);
2409
- video.removeEventListener("canplay", handleMediaReady);
2410
- ownerDocument.removeEventListener("visibilitychange", handleVisibilityChange);
2411
- if (videoFrameRequest !== null && typeof video.cancelVideoFrameCallback === "function") {
2412
- video.cancelVideoFrameCallback(videoFrameRequest);
2413
- }
2414
- };
2415
- const finish = () => {
2416
- if (settled) return;
2417
- settled = true;
2418
- cleanup();
2419
- resolve();
2420
- };
2421
- const fail = () => {
2422
- if (settled) return;
2423
- settled = true;
2424
- cleanup();
2425
- reject(
2426
- new Error(
2427
- `Video frame did not become ready at ${formatMediaTime(targetTime)} within ${timeoutMs}ms (currentTime=${formatMediaTime(video.currentTime)}, reachableTime=${formatMediaTime(reachableMediaTime(video, targetTime))}, duration=${formatMediaTime(video.duration)}, readyState=${video.readyState}, seeking=${String(video.seeking)}, visible=${String(isVisiblyPresented(video))}).`
2428
- )
2429
- );
2430
- };
2431
- const requestPresentedFrame = () => {
2432
- if (settled || video.seeking || video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA || !isAtReachableMediaTime(video, targetTime)) {
2433
- return;
2434
- }
2435
- if (typeof video.requestVideoFrameCallback !== "function" || !isVisiblyPresented(video)) {
2436
- finish();
2437
- return;
2438
- }
2439
- if (videoFrameRequest !== null) return;
2440
- videoFrameRequest = video.requestVideoFrameCallback(() => {
2441
- videoFrameRequest = null;
2442
- finish();
2443
- });
2444
- };
2445
- function handleMediaReady() {
2446
- requestPresentedFrame();
2447
- }
2448
- const watchdog = createCaptureWatchdog(ownerDocument, timeoutMs, fail);
2449
- const assignTargetTime = () => {
2450
- try {
2451
- video.currentTime = reachableMediaTime(video, targetTime);
2452
- queueMicrotask(requestPresentedFrame);
2453
- return true;
2454
- } catch (error) {
2455
- settled = true;
2456
- cleanup();
2457
- reject(error);
2458
- return false;
2459
- }
2460
- };
2461
- function handleVisibilityChange() {
2462
- if (settled) return;
2463
- if (isInactive()) {
2464
- watchdog.clear();
2465
- return;
2466
- }
2467
- requestPresentedFrame();
2468
- if (settled) return;
2469
- if (!video.seeking && !isAtReachableMediaTime(video, targetTime) && !assignTargetTime()) {
2470
- return;
2471
- }
2472
- watchdog.arm();
2473
- }
2474
- const readinessPoll = setInterval(requestPresentedFrame, VIDEO_FRAME_READINESS_POLL_MS);
2475
- video.addEventListener("seeked", handleMediaReady);
2476
- video.addEventListener("loadeddata", handleMediaReady);
2477
- video.addEventListener("canplay", handleMediaReady);
2478
- ownerDocument.addEventListener("visibilitychange", handleVisibilityChange);
2479
- watchdog.arm();
2480
- assignTargetTime();
2481
- });
2482
- }
2483
- function advanceSequentialCaptureVideo(video, targetTime, timeoutMs) {
2484
- return new Promise((resolve, reject) => {
2485
- let settled = false;
2486
- const ownerDocument = video.ownerDocument;
2487
- const isInactive = () => ownerDocument.visibilityState !== "visible";
2488
- const cleanup = () => {
2489
- watchdog.clear();
2490
- clearInterval(readinessPoll);
2491
- video.removeEventListener("timeupdate", check);
2492
- video.removeEventListener("loadeddata", check);
2493
- video.removeEventListener("canplay", check);
2494
- video.removeEventListener("error", fail);
2495
- ownerDocument.removeEventListener("visibilitychange", handleVisibilityChange);
2496
- };
2497
- const finish = () => {
2498
- if (!settled) {
2499
- settled = true;
2500
- cleanup();
2501
- resolve();
2502
- }
2503
- };
2504
- const fallbackToSeek = () => {
2505
- if (settled) return;
2506
- settled = true;
2507
- video.pause();
2508
- cleanup();
2509
- void seekVideoByAssignment(video, targetTime, timeoutMs).then(resolve, reject);
2510
- };
2511
- const fail = () => {
2512
- if (settled) return;
2513
- settled = true;
2514
- video.pause();
2515
- cleanup();
2516
- reject(
2517
- new Error(
2518
- `Video frame did not advance to ${formatMediaTime(targetTime)} within ${timeoutMs}ms (currentTime=${formatMediaTime(video.currentTime)}, readyState=${video.readyState}).`
2519
- )
2520
- );
2521
- };
2522
- function check() {
2523
- if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && video.currentTime >= reachableMediaTime(video, targetTime)) {
2524
- video.pause();
2525
- finish();
2526
- }
2527
- }
2528
- const handlePlaybackFailure = () => {
2529
- if (settled || isInactive()) return;
2530
- fallbackToSeek();
2531
- };
2532
- const startPlayback = () => {
2533
- if (settled || isInactive()) return;
2534
- try {
2535
- video.playbackRate = SEQUENTIAL_CAPTURE_PLAYBACK_RATE;
2536
- if (video.paused) {
2537
- void video.play().catch(handlePlaybackFailure);
2538
- }
2539
- } catch {
2540
- handlePlaybackFailure();
2541
- }
2542
- };
2543
- const watchdog = createCaptureWatchdog(ownerDocument, timeoutMs, fail);
2544
- const pauseWhileInactive = () => {
2545
- video.pause();
2546
- watchdog.clear();
2547
- };
2548
- const resumeWhenActive = () => {
2549
- if (settled || isInactive()) return;
2550
- check();
2551
- if (settled) return;
2552
- startPlayback();
2553
- watchdog.arm();
2554
- };
2555
- function handleVisibilityChange() {
2556
- if (ownerDocument.visibilityState !== "visible") {
2557
- pauseWhileInactive();
2558
- return;
2559
- }
2560
- resumeWhenActive();
2561
- }
2562
- const readinessPoll = setInterval(check, VIDEO_FRAME_READINESS_POLL_MS);
2563
- video.addEventListener("timeupdate", check);
2564
- video.addEventListener("loadeddata", check);
2565
- video.addEventListener("canplay", check);
2566
- video.addEventListener("error", fail, { once: true });
2567
- ownerDocument.addEventListener("visibilitychange", handleVisibilityChange);
2568
- resumeWhenActive();
2569
- });
2570
- }
2571
- function seekVideoToFrame(video, targetTime, timeoutMs = DEFAULT_VIDEO_FRAME_TIMEOUT_MS) {
2572
- if (video.readyState >= HTMLMediaElement.HAVE_METADATA && video.videoWidth <= 0 && video.videoHeight <= 0) {
2573
- video.pause();
2574
- return Promise.resolve();
2575
- }
2576
- const reachableTargetTime = reachableMediaTime(video, targetTime);
2577
- const step = reachableTargetTime - video.currentTime;
2578
- if (video.dataset.captureSequential === "true") {
2579
- if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && step <= VIDEO_TIME_TOLERANCE_SECONDS) {
2580
- if (step < -VIDEO_TIME_TOLERANCE_SECONDS) video.pause();
2581
- return Promise.resolve();
2582
- }
2583
- if (step > 0 && step <= MAX_SEQUENTIAL_CAPTURE_STEP_SECONDS && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
2584
- return advanceSequentialCaptureVideo(video, targetTime, timeoutMs);
2585
- }
2586
- }
2587
- video.pause();
2588
- const alreadyReady = !video.seeking && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && isAtReachableMediaTime(video, targetTime);
2589
- if (alreadyReady) return Promise.resolve();
2590
- return seekVideoByAssignment(video, targetTime, timeoutMs);
2591
- }
2592
-
2593
2442
  // src/DocPlayer.tsx
2594
2443
  import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
2595
2444
  function isDevEnvironment() {
@@ -2667,6 +2516,8 @@ function DocPlayerContent({
2667
2516
  onPlaybackStateChange,
2668
2517
  onControlsReady,
2669
2518
  onRenderAPIReady,
2519
+ renderVideoFrameSelector,
2520
+ processedAudio,
2670
2521
  isFullscreen = false,
2671
2522
  onFullscreenToggle,
2672
2523
  onBlockMarkers,
@@ -2759,8 +2610,11 @@ function DocPlayerContent({
2759
2610
  const rawSchedule = useMemo5(() => resolveMediaSchedule(doc), [doc]);
2760
2611
  const clipDurations = useMediaClipDurations(rawSchedule, basePath);
2761
2612
  const mediaSchedule = useMemo5(
2762
- () => resolveMediaSchedule(doc, { intrinsicDuration: (clip) => clipDurations.get(clip.src) }),
2763
- [doc, clipDurations]
2613
+ () => resolveMediaSchedule(doc, {
2614
+ intrinsicDuration: (clip) => clipDurations.get(clip.src),
2615
+ ...processedAudio ? { processedAudio } : {}
2616
+ }),
2617
+ [doc, clipDurations, processedAudio]
2764
2618
  );
2765
2619
  const startActiveTimelineMedia = useCallback5(() => {
2766
2620
  if (renderMode) return;
@@ -2787,6 +2641,9 @@ function DocPlayerContent({
2787
2641
  currentTimeRef.current = currentTime;
2788
2642
  const committedRenderTimeRef = useRef7(currentTime);
2789
2643
  const renderCommitWaitersRef = useRef7([]);
2644
+ const renderVideoFrameSelectorRef = useRef7(renderVideoFrameSelector);
2645
+ renderVideoFrameSelectorRef.current = renderVideoFrameSelector;
2646
+ const presentedBlocksRef = useRef7(null);
2790
2647
  const totalDurationRef = useRef7(totalDuration);
2791
2648
  totalDurationRef.current = totalDuration;
2792
2649
  const expandedBlocksLenRef = useRef7(0);
@@ -3041,6 +2898,11 @@ function DocPlayerContent({
3041
2898
  }
3042
2899
  }
3043
2900
  const elapsedMs = (time - blockStartTime) * 1e3;
2901
+ const selectVideoFrame = async (video, targetTime) => {
2902
+ const selector = renderMode ? renderVideoFrameSelectorRef.current : void 0;
2903
+ if (selector && await selector(video, targetTime)) return;
2904
+ await seekVideoToFrame(video, targetTime);
2905
+ };
3044
2906
  (root.getAnimations?.() ?? []).forEach((anim) => {
3045
2907
  const target = anim.effect?.target;
3046
2908
  if (!target) return;
@@ -3061,7 +2923,7 @@ function DocPlayerContent({
3061
2923
  const clipEnd = parseFloat(video.dataset.clipEnd || "0");
3062
2924
  const startAt = parseFloat(video.dataset.startAt || "0");
3063
2925
  const targetTime = Math.min(clipStart + Math.max(0, blockElapsed - startAt), clipEnd);
3064
- videoSeekPromises.push(seekVideoToFrame(video, targetTime));
2926
+ videoSeekPromises.push(selectVideoFrame(video, targetTime));
3065
2927
  });
3066
2928
  }
3067
2929
  root.querySelectorAll("video[data-clip-id]").forEach((el) => {
@@ -3072,10 +2934,19 @@ function DocPlayerContent({
3072
2934
  video.pause();
3073
2935
  if (time < absStart || time >= absEnd) return;
3074
2936
  const targetTime = sourceIn + (time - absStart);
3075
- videoSeekPromises.push(seekVideoToFrame(video, targetTime));
2937
+ videoSeekPromises.push(selectVideoFrame(video, targetTime));
3076
2938
  });
3077
2939
  await Promise.all(videoSeekPromises);
3078
- await waitForVisualUpdate();
2940
+ const presentedBlocks = Array.from(
2941
+ root.querySelectorAll(".doc-player__block"),
2942
+ (element) => ({ element, className: element.className })
2943
+ );
2944
+ const previousBlocks = presentedBlocksRef.current;
2945
+ presentedBlocksRef.current = presentedBlocks;
2946
+ const blocksUnchanged = previousBlocks !== null && previousBlocks.length === presentedBlocks.length && presentedBlocks.every(
2947
+ (block, index) => block.element === previousBlocks[index].element && block.className === previousBlocks[index].className
2948
+ );
2949
+ if (!renderMode || !blocksUnchanged) await waitForVisualUpdate();
3079
2950
  };
3080
2951
  const getDuration = () => {
3081
2952
  const mediaDuration = getDocPlaybackDuration(doc);
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as SquisqRenderAPI, V as VideoPresentation$1, P as PipSize, a as PipShape, b as PipPosition } from './player-BlD8zREs.js';
2
- export { B as BlockMarker, c as BlockRenderer, C as CaptionMode, d as CaptionOverlay, e as CaptionPosition, f as CaptionStyle, g as ControlsLayout, D as DisplayMode, h as DocControlsBottom, i as DocControlsOverlay, j as DocControlsSidebar, k as DocControlsSlideshow, l as DocPlayer, m as DocPlayerProps, n as DocPlayerWithSidebar, o as DocProgressBar, F as FlashcardFaceView, p as FlashcardFaceViewProps, q as FlashcardView, r as FlashcardViewProps, I as InlineAudioPlayer, s as InlineAudioPlayerProps, t as InlineVideoPlayer, u as InlineVideoPlayerProps, M as MediaClipLayer, v as MediaClipLayerProps, w as PlaybackActions, x as PlaybackState, R as RenderAudioSegmentInfo, y as RenderBlockInfo, z as RenderCaptionInfo, A as RenderChapterInfo, E as SlideNavActions, G as SocialCaptionOverlay, H as formatTime } from './player-BlD8zREs.js';
1
+ import { S as SquisqRenderAPI, V as VideoPresentation$1, P as PipSize, a as PipShape, b as PipPosition } from './player-5j10MhBq.js';
2
+ export { B as BlockMarker, c as BlockRenderer, C as CaptionMode, d as CaptionOverlay, e as CaptionPosition, f as CaptionStyle, g as ControlsLayout, D as DisplayMode, h as DocControlsBottom, i as DocControlsOverlay, j as DocControlsSidebar, k as DocControlsSlideshow, l as DocPlayer, m as DocPlayerProps, n as DocPlayerWithSidebar, o as DocProgressBar, F as FlashcardFaceView, p as FlashcardFaceViewProps, q as FlashcardView, r as FlashcardViewProps, I as InlineAudioPlayer, s as InlineAudioPlayerProps, t as InlineVideoPlayer, u as InlineVideoPlayerProps, M as MediaClipLayer, v as MediaClipLayerProps, w as PlaybackActions, x as PlaybackState, R as RenderAudioSegmentInfo, y as RenderBlockInfo, z as RenderCaptionInfo, A as RenderChapterInfo, E as RenderVideoFrameSelector, G as SlideNavActions, H as SocialCaptionOverlay, J as formatTime } from './player-5j10MhBq.js';
3
3
  import { Theme, VideoPresentation, VideoPipSize, VideoPipShape, VideoPipPosition, Doc, ScheduledClip, MediaProvider } from '@bendyline/squisq/schemas';
4
4
  import { DashboardStyleId, ViewportConfig, CoverSlideTemplate, CoverSlidePlayback } from '@bendyline/squisq/doc';
5
5
  export { getAnimationStyle, getTransitionClass } from '@bendyline/squisq/doc';
@@ -8,13 +8,14 @@ export { a as CodeBlockCopyContext, M as MarkdownRenderer, b as MarkdownRenderer
8
8
  export { FenceRendererContext, MermaidDiagram, MermaidDiagramProps, useFenceRenderers } from './markdown/index.js';
9
9
  export { CanvasSection, CanvasSectionProps, ImageDisplayMode, LinearDocView, LinearDocViewProps, PageSectionView, PageSectionViewProps, PageViewContext, PageViewContextValue, PaginatedDocPage, PaginatedDocView, PaginatedDocViewProps, usePageView } from './page/index.js';
10
10
  import * as react_jsx_runtime from 'react/jsx-runtime';
11
+ import { RefObject } from 'react';
12
+ import { AnimatedImageInfo } from '@bendyline/squisq/imageEdit';
11
13
  export { ImageLayer, MapLayer, MermaidLayer, PathLayer, ShapeLayer, TextLayer, TreeLayer, VideoLayer } from './layers/index.js';
12
14
  export { T as TableLayer, a as TableLayerContentRenderer, b as TableLayerContentRendererProps } from './TableLayer-B7ADuUGc.js';
13
15
  export { MediaContext, MediaScheduleController, ModalDialogOptions, ResourcePolicyContext, UseDocPlaybackOptions, useAudioSync, useAutoSurface, useDocPlayback, useMediaProvider, useMediaSchedule, useMediaUrl, useModalDialog, useResourcePolicy, useViewportOrientation } from './hooks/index.js';
14
16
  export { FenceRenderContext, FenceRenderer, FenceRendererMap } from '@bendyline/squisq/fence';
15
17
  export { A as AudioActions, a as AudioController, b as AudioState } from './AudioController-DwMsPe38.js';
16
18
  export { JsonFormFenceAction, JsonFormFenceRendererOptions, JsonView, JsonViewProps, createJsonFormFenceRenderer } from './json-view/index.js';
17
- import 'react';
18
19
  import '@bendyline/squisq/markdown';
19
20
  import '@bendyline/squisq/jsonForm';
20
21
 
@@ -156,6 +157,37 @@ interface DashboardViewProps {
156
157
  }
157
158
  declare function DashboardView({ doc, theme, viewport, layout, showTitle, style, documentTitle, basePath, animationsEnabled, muted, renderMode, onRenderAPIReady, className, }: DashboardViewProps): react_jsx_runtime.JSX.Element;
158
159
 
160
+ type AnimatedImageStatus = 'playing' | 'paused' | 'ended';
161
+ interface AnimatedImagePlayback {
162
+ /** Inspection result: null while pending, for sources that cannot animate,
163
+ * and when the bytes cannot be read. */
164
+ info: AnimatedImageInfo | null;
165
+ /** True once the image is known to animate. */
166
+ animated: boolean;
167
+ /** The URL to render — `resolvedUrl`, or a private copy after a replay. */
168
+ displaySrc: string;
169
+ status: AnimatedImageStatus;
170
+ /** Play from the first frame. */
171
+ play: () => void;
172
+ /** Freeze on a still frame. */
173
+ pause: () => void;
174
+ }
175
+ /**
176
+ * @param src The authored source (a relative path, URL or data URL). Its
177
+ * extension or MIME decides whether the image could animate at all.
178
+ * @param resolvedUrl The URL the image is actually loaded from, e.g. a blob
179
+ * URL from a MediaProvider. Pass '' while it is still resolving.
180
+ */
181
+ declare function useAnimatedImage(src: string, resolvedUrl: string): AnimatedImagePlayback;
182
+
183
+ interface AnimatedImageControlsProps {
184
+ playback: AnimatedImagePlayback;
185
+ /** The `<img>` showing `playback.displaySrc`. Its parent must be the
186
+ * positioned box the image fills. */
187
+ imageRef: RefObject<HTMLImageElement | null>;
188
+ }
189
+ declare function AnimatedImageControls({ playback, imageRef }: AnimatedImageControlsProps): react_jsx_runtime.JSX.Element | null;
190
+
159
191
  interface DocPlayerAppearanceOverrides {
160
192
  theme?: Theme;
161
193
  videoPresentation?: VideoPresentation$1;
@@ -206,4 +238,4 @@ declare function resolveDocPlayerAppearance(doc: Doc, overrides?: DocPlayerAppea
206
238
  */
207
239
  declare function useMediaClipDurations(schedule: ScheduledClip[], basePath: string, mediaProviderOverride?: MediaProvider | null): Map<string, number>;
208
240
 
209
- export { CodeBlockCopyHandler, DashboardView, type DashboardViewProps, type DocPlayerAppearanceOverrides, type MountOptions, PipPosition, PipShape, PipSize, type ResolvedDocPlayerAppearance, type SquisqPlayerHandle, SquisqRenderAPI, VideoPresentation$1 as VideoPresentation, resolveDocPlayerAppearance, useMediaClipDurations };
241
+ export { AnimatedImageControls, type AnimatedImageControlsProps, type AnimatedImagePlayback, type AnimatedImageStatus, CodeBlockCopyHandler, DashboardView, type DashboardViewProps, type DocPlayerAppearanceOverrides, type MountOptions, PipPosition, PipShape, PipSize, type ResolvedDocPlayerAppearance, type SquisqPlayerHandle, SquisqRenderAPI, VideoPresentation$1 as VideoPresentation, resolveDocPlayerAppearance, useAnimatedImage, useMediaClipDurations };
package/dist/index.js CHANGED
@@ -15,10 +15,10 @@ import {
15
15
  formatTime,
16
16
  resolveDocPlayerAppearance,
17
17
  useMediaClipDurations
18
- } from "./chunk-KGWT454C.js";
18
+ } from "./chunk-Z64OLGMD.js";
19
19
  import {
20
20
  PaginatedDocView
21
- } from "./chunk-7QA6EW6Y.js";
21
+ } from "./chunk-SESRQ7XH.js";
22
22
  import {
23
23
  BlockRenderer,
24
24
  CanvasSection,
@@ -26,7 +26,7 @@ import {
26
26
  PageSectionView,
27
27
  PageViewContext,
28
28
  usePageView
29
- } from "./chunk-5UGIE2QO.js";
29
+ } from "./chunk-LDXH4FOY.js";
30
30
  import {
31
31
  ImageLayer,
32
32
  MapLayer,
@@ -39,7 +39,7 @@ import {
39
39
  VideoLayer,
40
40
  getAnimationStyle,
41
41
  getTransitionClass
42
- } from "./chunk-XSOMFBH2.js";
42
+ } from "./chunk-W6MKRWL6.js";
43
43
  import {
44
44
  useModalDialog
45
45
  } from "./chunk-65POMKHR.js";
@@ -52,17 +52,19 @@ import {
52
52
  import {
53
53
  JsonView,
54
54
  createJsonFormFenceRenderer
55
- } from "./chunk-ITQ4K5N6.js";
55
+ } from "./chunk-KDTKUE53.js";
56
56
  import {
57
57
  useAutoSurface
58
58
  } from "./chunk-TT6ENR6T.js";
59
59
  import {
60
+ AnimatedImageControls,
60
61
  FenceRendererContext,
61
62
  InlineAudioPlayer,
62
63
  InlineVideoPlayer,
63
64
  MarkdownRenderer,
65
+ useAnimatedImage,
64
66
  useFenceRenderers
65
- } from "./chunk-ZY5N3JPM.js";
67
+ } from "./chunk-6PUMLLJH.js";
66
68
  import {
67
69
  MermaidDiagram
68
70
  } from "./chunk-WLUZTUNZ.js";
@@ -74,6 +76,7 @@ import {
74
76
  useResourcePolicy
75
77
  } from "./chunk-LR3AIGDD.js";
76
78
  export {
79
+ AnimatedImageControls,
77
80
  BlockRenderer,
78
81
  CanvasSection,
79
82
  CaptionOverlay,
@@ -115,6 +118,7 @@ export {
115
118
  getAnimationStyle,
116
119
  getTransitionClass,
117
120
  resolveDocPlayerAppearance,
121
+ useAnimatedImage,
118
122
  useAudioSync,
119
123
  useAutoSurface,
120
124
  useDocPlayback,
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  JsonView,
3
3
  createJsonFormFenceRenderer
4
- } from "../chunk-ITQ4K5N6.js";
4
+ } from "../chunk-KDTKUE53.js";
5
5
  import "../chunk-TT6ENR6T.js";
6
- import "../chunk-ZY5N3JPM.js";
6
+ import "../chunk-6PUMLLJH.js";
7
7
  import "../chunk-WLUZTUNZ.js";
8
8
  import "../chunk-LR3AIGDD.js";
9
9
  export {
@@ -8,7 +8,7 @@ import {
8
8
  TextLayer,
9
9
  TreeLayer,
10
10
  VideoLayer
11
- } from "../chunk-XSOMFBH2.js";
11
+ } from "../chunk-W6MKRWL6.js";
12
12
  import "../chunk-WLUZTUNZ.js";
13
13
  import "../chunk-LR3AIGDD.js";
14
14
  export {