@bendyline/squisq-video-react 2.2.4 → 2.2.6
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.
- package/dist/{chunk-IDY64F4K.js → chunk-NZYFSB2T.js} +351 -58
- package/dist/{chunk-ULNKIV4A.js → chunk-U2TST3MN.js} +95 -9
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.js +2 -2
- package/dist/encoder/index.d.ts +2 -68
- package/dist/hooks/index.d.ts +10 -3
- package/dist/hooks/index.js +5 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +6 -2
- package/dist/mainThreadEncoder-BgcFyYvO.d.ts +68 -0
- package/dist/{useVideoExport-DAEglCva.d.ts → useVideoExport-Okwrf8gg.d.ts} +49 -3
- package/package.json +5 -5
|
@@ -26,34 +26,61 @@ var MIME_MAP = {
|
|
|
26
26
|
webp: "image/webp",
|
|
27
27
|
svg: "image/svg+xml",
|
|
28
28
|
bmp: "image/bmp",
|
|
29
|
-
avif: "image/avif"
|
|
29
|
+
avif: "image/avif",
|
|
30
|
+
mp3: "audio/mpeg",
|
|
31
|
+
wav: "audio/wav",
|
|
32
|
+
ogg: "audio/ogg",
|
|
33
|
+
mp4: "video/mp4",
|
|
34
|
+
webm: "video/webm"
|
|
30
35
|
};
|
|
31
|
-
var
|
|
36
|
+
var CAPTURE_ASSET_TIMEOUT_MS = 15e3;
|
|
37
|
+
var RENDER_TIME_EPSILON_SECONDS = 1e-6;
|
|
32
38
|
var POTENTIALLY_ANIMATED_IMAGE_URL = /(?:^data:image\/(?:gif|webp|avif)[;,]|\.(?:gif|webp|avif)(?:[?#]|$))/i;
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
async function waitForImageDecode(image) {
|
|
40
|
+
const src = image.currentSrc || image.src;
|
|
41
|
+
if (!src) return;
|
|
42
|
+
const decoded = typeof image.decode === "function" ? image.decode() : new Promise((resolve, reject) => {
|
|
43
|
+
if (image.complete) {
|
|
44
|
+
if (image.naturalWidth > 0) resolve();
|
|
45
|
+
else reject(new Error(`Image could not be decoded: ${src}`));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
image.addEventListener("load", () => resolve(), { once: true });
|
|
49
|
+
image.addEventListener(
|
|
50
|
+
"error",
|
|
51
|
+
() => reject(new Error(`Image could not be loaded: ${src}`)),
|
|
52
|
+
{
|
|
53
|
+
once: true
|
|
54
|
+
}
|
|
47
55
|
);
|
|
48
|
-
if (document.visibilityState !== "visible") return;
|
|
49
|
-
const waitForFrame = (remaining) => {
|
|
50
|
-
animationFrame = window.requestAnimationFrame(() => {
|
|
51
|
-
if (remaining <= 1) finish();
|
|
52
|
-
else waitForFrame(remaining - 1);
|
|
53
|
-
});
|
|
54
|
-
};
|
|
55
|
-
waitForFrame(Math.max(1, frameCount));
|
|
56
56
|
});
|
|
57
|
+
let timeout;
|
|
58
|
+
try {
|
|
59
|
+
await Promise.race([
|
|
60
|
+
decoded,
|
|
61
|
+
new Promise((_resolve, reject) => {
|
|
62
|
+
timeout = setTimeout(
|
|
63
|
+
() => reject(new Error(`Image did not become ready within 15s: ${src}`)),
|
|
64
|
+
CAPTURE_ASSET_TIMEOUT_MS
|
|
65
|
+
);
|
|
66
|
+
})
|
|
67
|
+
]);
|
|
68
|
+
} finally {
|
|
69
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function waitForCaptureAssets(captureRoot, decodedImages = /* @__PURE__ */ new WeakSet()) {
|
|
73
|
+
const fonts = captureRoot.ownerDocument.fonts;
|
|
74
|
+
if (fonts) await fonts.ready;
|
|
75
|
+
const pendingImages = Array.from(captureRoot.querySelectorAll("img")).filter(
|
|
76
|
+
(image) => !decodedImages.has(image)
|
|
77
|
+
);
|
|
78
|
+
await Promise.all(
|
|
79
|
+
pendingImages.map(async (image) => {
|
|
80
|
+
await waitForImageDecode(image);
|
|
81
|
+
decodedImages.add(image);
|
|
82
|
+
})
|
|
83
|
+
);
|
|
57
84
|
}
|
|
58
85
|
function createInlineProvider(images) {
|
|
59
86
|
const blobUrls = /* @__PURE__ */ new Map();
|
|
@@ -96,6 +123,159 @@ function shouldIgnoreCaptureSibling(element, captureRoot) {
|
|
|
96
123
|
function finiteMediaTime(value) {
|
|
97
124
|
return Number.isFinite(value) ? value.toFixed(6) : "unknown";
|
|
98
125
|
}
|
|
126
|
+
function coverSourceRect(sourceWidth, sourceHeight, destinationWidth, destinationHeight) {
|
|
127
|
+
const sourceRatio = sourceWidth / sourceHeight;
|
|
128
|
+
const destinationRatio = destinationWidth / destinationHeight;
|
|
129
|
+
if (sourceRatio > destinationRatio) {
|
|
130
|
+
const sw = sourceHeight * destinationRatio;
|
|
131
|
+
return { sx: (sourceWidth - sw) / 2, sy: 0, sw, sh: sourceHeight };
|
|
132
|
+
}
|
|
133
|
+
const sh = sourceWidth / destinationRatio;
|
|
134
|
+
return { sx: 0, sy: (sourceHeight - sh) / 2, sw: sourceWidth, sh };
|
|
135
|
+
}
|
|
136
|
+
function videoFrameRect(sourceWidth, sourceHeight, destinationWidth, destinationHeight, objectFit) {
|
|
137
|
+
if (objectFit === "cover") {
|
|
138
|
+
return {
|
|
139
|
+
...coverSourceRect(sourceWidth, sourceHeight, destinationWidth, destinationHeight),
|
|
140
|
+
dx: 0,
|
|
141
|
+
dy: 0,
|
|
142
|
+
dw: destinationWidth,
|
|
143
|
+
dh: destinationHeight
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (objectFit === "contain" || objectFit === "scale-down") {
|
|
147
|
+
const containScale = Math.min(destinationWidth / sourceWidth, destinationHeight / sourceHeight);
|
|
148
|
+
const scale = objectFit === "scale-down" ? Math.min(1, containScale) : containScale;
|
|
149
|
+
const dw = sourceWidth * scale;
|
|
150
|
+
const dh = sourceHeight * scale;
|
|
151
|
+
return {
|
|
152
|
+
sx: 0,
|
|
153
|
+
sy: 0,
|
|
154
|
+
sw: sourceWidth,
|
|
155
|
+
sh: sourceHeight,
|
|
156
|
+
dx: (destinationWidth - dw) / 2,
|
|
157
|
+
dy: (destinationHeight - dh) / 2,
|
|
158
|
+
dw,
|
|
159
|
+
dh
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (objectFit === "none") {
|
|
163
|
+
return {
|
|
164
|
+
sx: 0,
|
|
165
|
+
sy: 0,
|
|
166
|
+
sw: sourceWidth,
|
|
167
|
+
sh: sourceHeight,
|
|
168
|
+
dx: (destinationWidth - sourceWidth) / 2,
|
|
169
|
+
dy: (destinationHeight - sourceHeight) / 2,
|
|
170
|
+
dw: sourceWidth,
|
|
171
|
+
dh: sourceHeight
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
sx: 0,
|
|
176
|
+
sy: 0,
|
|
177
|
+
sw: sourceWidth,
|
|
178
|
+
sh: sourceHeight,
|
|
179
|
+
dx: 0,
|
|
180
|
+
dy: 0,
|
|
181
|
+
dw: destinationWidth,
|
|
182
|
+
dh: destinationHeight
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function prepareScheduledVideoClones(originalRoot, clonedRoot) {
|
|
186
|
+
const captureFamilies = [
|
|
187
|
+
{
|
|
188
|
+
original: ".doc-player__media-clips video[data-clip-id]",
|
|
189
|
+
clone: ".doc-player__media-clips canvas"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
original: ".block-layer--video video[data-clip-start]",
|
|
193
|
+
clone: ".block-layer--video canvas"
|
|
194
|
+
}
|
|
195
|
+
];
|
|
196
|
+
const pairs = captureFamilies.flatMap(({ original, clone }) => {
|
|
197
|
+
const videos = Array.from(originalRoot.querySelectorAll(original));
|
|
198
|
+
const canvases = Array.from(clonedRoot.querySelectorAll(clone));
|
|
199
|
+
return videos.flatMap((video, index) => {
|
|
200
|
+
const canvas = canvases[index];
|
|
201
|
+
return canvas ? [{ video, canvas }] : [];
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
pairs.forEach(({ video, canvas }) => {
|
|
205
|
+
canvas.className = video.className;
|
|
206
|
+
canvas.style.cssText = video.style.cssText;
|
|
207
|
+
for (const attribute of Array.from(video.attributes)) {
|
|
208
|
+
if (attribute.name.startsWith("data-")) {
|
|
209
|
+
canvas.setAttribute(attribute.name, attribute.value);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
canvas.dataset.videoCaptureClone = "true";
|
|
213
|
+
const destinationWidth = Math.round(video.clientWidth || video.offsetWidth);
|
|
214
|
+
const destinationHeight = Math.round(video.clientHeight || video.offsetHeight);
|
|
215
|
+
if (video.videoWidth <= 0 || video.videoHeight <= 0 || destinationWidth <= 0 || destinationHeight <= 0) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const view = video.ownerDocument.defaultView;
|
|
220
|
+
const objectFit = video.style.objectFit || view?.getComputedStyle(video).objectFit || "fill";
|
|
221
|
+
const frame = videoFrameRect(
|
|
222
|
+
video.videoWidth,
|
|
223
|
+
video.videoHeight,
|
|
224
|
+
destinationWidth,
|
|
225
|
+
destinationHeight,
|
|
226
|
+
objectFit
|
|
227
|
+
);
|
|
228
|
+
const stagingCanvas = canvas.ownerDocument.createElement("canvas");
|
|
229
|
+
stagingCanvas.width = destinationWidth;
|
|
230
|
+
stagingCanvas.height = destinationHeight;
|
|
231
|
+
const stagingContext = stagingCanvas.getContext("2d");
|
|
232
|
+
const context = canvas.getContext("2d");
|
|
233
|
+
if (!stagingContext || !context) return;
|
|
234
|
+
stagingContext.drawImage(
|
|
235
|
+
video,
|
|
236
|
+
frame.sx,
|
|
237
|
+
frame.sy,
|
|
238
|
+
frame.sw,
|
|
239
|
+
frame.sh,
|
|
240
|
+
frame.dx,
|
|
241
|
+
frame.dy,
|
|
242
|
+
frame.dw,
|
|
243
|
+
frame.dh
|
|
244
|
+
);
|
|
245
|
+
canvas.width = destinationWidth;
|
|
246
|
+
canvas.height = destinationHeight;
|
|
247
|
+
context.drawImage(stagingCanvas, 0, 0);
|
|
248
|
+
const foreignObject = canvas.closest("foreignObject");
|
|
249
|
+
const svg = canvas.closest("svg");
|
|
250
|
+
if (foreignObject && svg) {
|
|
251
|
+
const originalHost = video.closest(".doc-player__block") ?? originalRoot;
|
|
252
|
+
const clonedHost = svg.closest(".doc-player__block") ?? clonedRoot;
|
|
253
|
+
const videoRect = video.getBoundingClientRect();
|
|
254
|
+
const hostRect = originalHost.getBoundingClientRect();
|
|
255
|
+
const renderedWidth = videoRect.width || destinationWidth;
|
|
256
|
+
const renderedHeight = videoRect.height || destinationHeight;
|
|
257
|
+
const fallbackX = Number.parseFloat(foreignObject.getAttribute("x") ?? "0") || 0;
|
|
258
|
+
const fallbackY = Number.parseFloat(foreignObject.getAttribute("y") ?? "0") || 0;
|
|
259
|
+
const left = videoRect.width ? videoRect.left - hostRect.left : fallbackX;
|
|
260
|
+
const top = videoRect.height ? videoRect.top - hostRect.top : fallbackY;
|
|
261
|
+
foreignObject.remove();
|
|
262
|
+
if (clonedHost === clonedRoot && !clonedHost.style.position) {
|
|
263
|
+
clonedHost.style.position = "relative";
|
|
264
|
+
}
|
|
265
|
+
canvas.style.position = "absolute";
|
|
266
|
+
canvas.style.left = `${left}px`;
|
|
267
|
+
canvas.style.top = `${top}px`;
|
|
268
|
+
canvas.style.width = `${renderedWidth}px`;
|
|
269
|
+
canvas.style.height = `${renderedHeight}px`;
|
|
270
|
+
canvas.style.zIndex = "3";
|
|
271
|
+
canvas.style.margin = "0";
|
|
272
|
+
canvas.style.transform = "none";
|
|
273
|
+
clonedHost.appendChild(canvas);
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
99
279
|
function getFrameVisualStateKey(captureRoot, timelineTime) {
|
|
100
280
|
const markup = captureRoot.innerHTML;
|
|
101
281
|
let needsTimelineKey = false;
|
|
@@ -153,6 +333,7 @@ function useFrameCapture() {
|
|
|
153
333
|
const captureCanvasRef = useRef(null);
|
|
154
334
|
const lastVisualStateKeyRef = useRef(null);
|
|
155
335
|
const hasCapturedFrameRef = useRef(false);
|
|
336
|
+
const decodedImagesRef = useRef(/* @__PURE__ */ new WeakSet());
|
|
156
337
|
const dimensionsRef = useRef({ width: 1920, height: 1080 });
|
|
157
338
|
const init = useCallback(
|
|
158
339
|
async (doc, renderOptions, captionMode) => {
|
|
@@ -168,6 +349,7 @@ function useFrameCapture() {
|
|
|
168
349
|
captureCanvasRef.current = null;
|
|
169
350
|
lastVisualStateKeyRef.current = null;
|
|
170
351
|
hasCapturedFrameRef.current = false;
|
|
352
|
+
decodedImagesRef.current = /* @__PURE__ */ new WeakSet();
|
|
171
353
|
await new Promise((resolve) => {
|
|
172
354
|
setTimeout(() => {
|
|
173
355
|
if (oldRoot) oldRoot.unmount();
|
|
@@ -193,6 +375,7 @@ function useFrameCapture() {
|
|
|
193
375
|
captureCanvasRef.current = captureCanvas;
|
|
194
376
|
lastVisualStateKeyRef.current = null;
|
|
195
377
|
hasCapturedFrameRef.current = false;
|
|
378
|
+
decodedImagesRef.current = /* @__PURE__ */ new WeakSet();
|
|
196
379
|
const container = document.createElement("div");
|
|
197
380
|
container.style.cssText = `position:fixed;left:0;top:0;width:${width}px;height:${height}px;opacity:0;pointer-events:none;z-index:-1;overflow:hidden;`;
|
|
198
381
|
document.body.appendChild(container);
|
|
@@ -219,6 +402,12 @@ function useFrameCapture() {
|
|
|
219
402
|
showControls: false,
|
|
220
403
|
autoPlay: false,
|
|
221
404
|
forceViewport: { width, height, name: "export" },
|
|
405
|
+
theme: renderOptions.theme,
|
|
406
|
+
videoPresentation: renderOptions.videoPresentation,
|
|
407
|
+
pipSize: renderOptions.pipSize,
|
|
408
|
+
pipShape: renderOptions.pipShape,
|
|
409
|
+
pipPosition: renderOptions.pipPosition,
|
|
410
|
+
showCoverSlide: renderOptions.showCoverSlide,
|
|
222
411
|
captionsEnabled,
|
|
223
412
|
captionStyle,
|
|
224
413
|
onRenderAPIReady: (api) => {
|
|
@@ -246,14 +435,31 @@ function useFrameCapture() {
|
|
|
246
435
|
)
|
|
247
436
|
);
|
|
248
437
|
}, 15e3);
|
|
249
|
-
void renderAPIReady.then((api) => {
|
|
250
|
-
|
|
251
|
-
|
|
438
|
+
void renderAPIReady.then(async (api) => {
|
|
439
|
+
try {
|
|
440
|
+
const captureRoot = container.querySelector("#squisq-capture-root");
|
|
441
|
+
if (!(captureRoot instanceof HTMLElement)) {
|
|
442
|
+
throw new Error("Capture root element not found after player initialization.");
|
|
443
|
+
}
|
|
444
|
+
await waitForCaptureAssets(captureRoot, decodedImagesRef.current);
|
|
445
|
+
clearTimeout(timeout);
|
|
446
|
+
resolve(api.getDuration());
|
|
447
|
+
} catch (assetError) {
|
|
448
|
+
clearTimeout(timeout);
|
|
449
|
+
reject(assetError);
|
|
450
|
+
}
|
|
252
451
|
});
|
|
253
452
|
});
|
|
254
453
|
},
|
|
255
454
|
[]
|
|
256
455
|
);
|
|
456
|
+
const setCoverVisible = useCallback(async (visible) => {
|
|
457
|
+
const api = renderAPIRef.current;
|
|
458
|
+
if (!api) throw new Error("Frame capture not initialized \xE2\u20AC\u201D call init() first");
|
|
459
|
+
if (visible) await api.showCover();
|
|
460
|
+
else await api.hideCover();
|
|
461
|
+
lastVisualStateKeyRef.current = null;
|
|
462
|
+
}, []);
|
|
257
463
|
const captureCanvasFrame = useCallback(
|
|
258
464
|
async (time, options = {}) => {
|
|
259
465
|
const container = containerRef.current;
|
|
@@ -264,11 +470,17 @@ function useFrameCapture() {
|
|
|
264
470
|
}
|
|
265
471
|
const { width, height } = dimensionsRef.current;
|
|
266
472
|
await api.seekTo(time);
|
|
267
|
-
|
|
473
|
+
const renderedTime = api.getRenderedTime();
|
|
474
|
+
if (Math.abs(renderedTime - time) > RENDER_TIME_EPSILON_SECONDS) {
|
|
475
|
+
throw new Error(
|
|
476
|
+
`Player committed ${renderedTime.toFixed(6)}s while capture requested ${time.toFixed(6)}s.`
|
|
477
|
+
);
|
|
478
|
+
}
|
|
268
479
|
const root = container.querySelector("#squisq-capture-root");
|
|
269
480
|
if (!root) {
|
|
270
481
|
throw new Error("Capture root element not found");
|
|
271
482
|
}
|
|
483
|
+
await waitForCaptureAssets(root, decodedImagesRef.current);
|
|
272
484
|
const visualStateKey = options.reuseIfUnchanged ? getFrameVisualStateKey(root, time) : null;
|
|
273
485
|
if (visualStateKey !== null && hasCapturedFrameRef.current && lastVisualStateKeyRef.current === visualStateKey) {
|
|
274
486
|
return captureCanvas;
|
|
@@ -286,6 +498,9 @@ function useFrameCapture() {
|
|
|
286
498
|
allowTaint: true,
|
|
287
499
|
backgroundColor: "#000000",
|
|
288
500
|
logging: false,
|
|
501
|
+
onclone: (_clonedDocument, clonedRoot) => {
|
|
502
|
+
prepareScheduledVideoClones(root, clonedRoot);
|
|
503
|
+
},
|
|
289
504
|
// html2canvas starts cloning at documentElement. Do not clone the rest
|
|
290
505
|
// of the editor/site UI on every frame; only the capture root, its
|
|
291
506
|
// ancestors, descendants, and document styles can affect this render.
|
|
@@ -322,11 +537,12 @@ function useFrameCapture() {
|
|
|
322
537
|
}
|
|
323
538
|
lastVisualStateKeyRef.current = null;
|
|
324
539
|
hasCapturedFrameRef.current = false;
|
|
540
|
+
decodedImagesRef.current = /* @__PURE__ */ new WeakSet();
|
|
325
541
|
renderAPIRef.current = null;
|
|
326
542
|
}, []);
|
|
327
543
|
return useMemo(
|
|
328
|
-
() => ({ init, captureFrame, captureCanvasFrame, destroy }),
|
|
329
|
-
[init, captureFrame, captureCanvasFrame, destroy]
|
|
544
|
+
() => ({ init, setCoverVisible, captureFrame, captureCanvasFrame, destroy }),
|
|
545
|
+
[init, setCoverVisible, captureFrame, captureCanvasFrame, destroy]
|
|
330
546
|
);
|
|
331
547
|
}
|
|
332
548
|
|
|
@@ -593,14 +809,22 @@ async function transcodeMp4ToGifWithFfmpegWasm(videoMp4, options, loadConfig, si
|
|
|
593
809
|
|
|
594
810
|
// src/hooks/useVideoExport.ts
|
|
595
811
|
var MAX_EXPORT_MEDIA_FILES = 256;
|
|
596
|
-
var MAX_EXPORT_MEDIA_FILE_BYTES = 64 * 1024 * 1024;
|
|
597
|
-
var MAX_EXPORT_MEDIA_TOTAL_BYTES = 256 * 1024 * 1024;
|
|
598
812
|
var ENCODER_PROBE_TIMEOUT_MS = 5e3;
|
|
599
813
|
var ENCODER_START_TIMEOUT_MS = 6e4;
|
|
600
814
|
var FRAME_CAPTURE_TIMEOUT_MS = 6e4;
|
|
601
815
|
var FRAME_ENCODE_TIMEOUT_MS = 6e4;
|
|
602
816
|
var CAPTURE_PROGRESS_START = 7;
|
|
603
817
|
var CAPTURE_PROGRESS_END = 95;
|
|
818
|
+
var FRAME_RATE_WINDOW_SIZE = 30;
|
|
819
|
+
var DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS = 2;
|
|
820
|
+
function calculateRollingFramesPerSecond(frameBoundaryTimes) {
|
|
821
|
+
if (frameBoundaryTimes.length < 2) return null;
|
|
822
|
+
const firstIndex = Math.max(0, frameBoundaryTimes.length - (FRAME_RATE_WINDOW_SIZE + 1));
|
|
823
|
+
const elapsedMs = frameBoundaryTimes[frameBoundaryTimes.length - 1] - frameBoundaryTimes[firstIndex];
|
|
824
|
+
const completedFrames = frameBoundaryTimes.length - 1 - firstIndex;
|
|
825
|
+
if (elapsedMs <= 0 || completedFrames <= 0) return null;
|
|
826
|
+
return completedFrames * 1e3 / elapsedMs;
|
|
827
|
+
}
|
|
604
828
|
function releaseEncoderFrame(frame) {
|
|
605
829
|
if ("close" in frame) frame.close();
|
|
606
830
|
}
|
|
@@ -633,6 +857,14 @@ function settleWithin(operation, timeoutMs, timeoutMessage, onLateResult) {
|
|
|
633
857
|
function toArrayBuffer(bytes) {
|
|
634
858
|
return bytes.slice().buffer;
|
|
635
859
|
}
|
|
860
|
+
function resolveExportMediaResourcePolicy(declaredSize, policy) {
|
|
861
|
+
const knownSize = Number.isFinite(declaredSize) ? Math.max(0, declaredSize) : 0;
|
|
862
|
+
return {
|
|
863
|
+
...DEFAULT_INTERACTIVE_RESOURCE_POLICY,
|
|
864
|
+
...policy,
|
|
865
|
+
maxBytes: policy?.maxBytes ?? Math.max(DEFAULT_INTERACTIVE_RESOURCE_POLICY.maxBytes, knownSize)
|
|
866
|
+
};
|
|
867
|
+
}
|
|
636
868
|
function collectDocumentMediaReferences(doc) {
|
|
637
869
|
const references = /* @__PURE__ */ new Set();
|
|
638
870
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
@@ -672,10 +904,37 @@ async function resolveAudioBuffers(clips, sources) {
|
|
|
672
904
|
}
|
|
673
905
|
return out;
|
|
674
906
|
}
|
|
675
|
-
function
|
|
907
|
+
function resolveFrontmatterBoolean(value) {
|
|
908
|
+
if (typeof value === "boolean") return value;
|
|
909
|
+
if (typeof value !== "string") return void 0;
|
|
910
|
+
const normalized = value.trim().toLowerCase();
|
|
911
|
+
if (normalized === "true" || normalized === "yes" || normalized === "on" || normalized === "show" || normalized === "visible") {
|
|
912
|
+
return true;
|
|
913
|
+
}
|
|
914
|
+
if (normalized === "false" || normalized === "no" || normalized === "off" || normalized === "hide" || normalized === "hidden") {
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
return void 0;
|
|
918
|
+
}
|
|
919
|
+
function resolveVideoExportCover(doc, config = {}) {
|
|
920
|
+
const frontmatter = doc.frontmatter;
|
|
921
|
+
const frontmatterValue = frontmatter ? Object.prototype.hasOwnProperty.call(frontmatter, "squisq-cover-slide") ? frontmatter["squisq-cover-slide"] : frontmatter["cover-slide"] : void 0;
|
|
922
|
+
const showCoverSlide = config.showCoverSlide ?? resolveFrontmatterBoolean(frontmatterValue) ?? true;
|
|
923
|
+
const requestedPreRoll = config.coverPreRoll ?? DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS;
|
|
924
|
+
if (!Number.isFinite(requestedPreRoll) || requestedPreRoll < 0) {
|
|
925
|
+
throw new Error("Cover pre-roll must be a finite number of seconds greater than or equal to 0");
|
|
926
|
+
}
|
|
927
|
+
return {
|
|
928
|
+
showCoverSlide,
|
|
929
|
+
coverPreRoll: showCoverSlide && !!doc.startBlock ? requestedPreRoll : 0
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
function useVideoExport(options = {}) {
|
|
676
933
|
const [state, setState] = useState("idle");
|
|
677
934
|
const [progress, setProgress] = useState(0);
|
|
678
935
|
const [phase, setPhase] = useState("");
|
|
936
|
+
const [currentFrameTime, setCurrentFrameTime] = useState(null);
|
|
937
|
+
const [processingFps, setProcessingFps] = useState(null);
|
|
679
938
|
const [duration, setDuration] = useState(0);
|
|
680
939
|
const [outputFormat, setOutputFormat] = useState("mp4");
|
|
681
940
|
const [backend, setBackend] = useState(null);
|
|
@@ -692,6 +951,8 @@ function useVideoExport() {
|
|
|
692
951
|
const downloadUrlRef = useRef2(null);
|
|
693
952
|
const startTimeRef = useRef2(0);
|
|
694
953
|
const elapsedTimerRef = useRef2(null);
|
|
954
|
+
const previewOptionsRef = useRef2(options);
|
|
955
|
+
previewOptionsRef.current = options;
|
|
695
956
|
const frameCapture = useFrameCapture();
|
|
696
957
|
useEffect(() => {
|
|
697
958
|
return () => {
|
|
@@ -721,6 +982,8 @@ function useVideoExport() {
|
|
|
721
982
|
setState("idle");
|
|
722
983
|
setProgress(0);
|
|
723
984
|
setPhase("");
|
|
985
|
+
setCurrentFrameTime(null);
|
|
986
|
+
setProcessingFps(null);
|
|
724
987
|
setDuration(0);
|
|
725
988
|
setOutputFormat("mp4");
|
|
726
989
|
setBackend(null);
|
|
@@ -760,6 +1023,8 @@ function useVideoExport() {
|
|
|
760
1023
|
setAudioIncluded(false);
|
|
761
1024
|
setAudioSkippedReason(null);
|
|
762
1025
|
setError(null);
|
|
1026
|
+
setCurrentFrameTime(null);
|
|
1027
|
+
setProcessingFps(null);
|
|
763
1028
|
const quality = config.quality ?? "normal";
|
|
764
1029
|
const effectiveOutputFormat = config.outputFormat ?? "mp4";
|
|
765
1030
|
const fps = config.fps ?? (effectiveOutputFormat === "gif" ? 10 : 30);
|
|
@@ -769,6 +1034,7 @@ function useVideoExport() {
|
|
|
769
1034
|
const audioPolicy = config.audioPolicy ?? "require";
|
|
770
1035
|
setOutputFormat(effectiveOutputFormat);
|
|
771
1036
|
try {
|
|
1037
|
+
const cover = resolveVideoExportCover(doc, config);
|
|
772
1038
|
const gifDefaults = orientation === "portrait" ? { width: 540, height: 960 } : { width: 960, height: 540 };
|
|
773
1039
|
const { width, height } = resolveDimensions({
|
|
774
1040
|
orientation,
|
|
@@ -815,42 +1081,42 @@ function useVideoExport() {
|
|
|
815
1081
|
`Document references ${neededEntries.length} media files; browser export supports at most ${MAX_EXPORT_MEDIA_FILES}.`
|
|
816
1082
|
);
|
|
817
1083
|
}
|
|
818
|
-
let totalMediaBytes = 0;
|
|
819
1084
|
for (const entry of neededEntries) {
|
|
820
1085
|
if (cancelledRef.current) return;
|
|
821
|
-
if (entry.size > MAX_EXPORT_MEDIA_FILE_BYTES) {
|
|
822
|
-
throw new Error(`Media file "${entry.name}" is too large for browser video export.`);
|
|
823
|
-
}
|
|
824
1086
|
const url2 = await config.mediaProvider.resolveUrl(entry.name);
|
|
825
1087
|
const resource = await fetchResourceBytes(url2, {
|
|
826
|
-
policy:
|
|
827
|
-
...DEFAULT_INTERACTIVE_RESOURCE_POLICY,
|
|
828
|
-
...config.resourcePolicy,
|
|
829
|
-
maxBytes: Math.min(
|
|
830
|
-
config.resourcePolicy?.maxBytes ?? MAX_EXPORT_MEDIA_FILE_BYTES,
|
|
831
|
-
MAX_EXPORT_MEDIA_FILE_BYTES
|
|
832
|
-
)
|
|
833
|
-
}
|
|
1088
|
+
policy: resolveExportMediaResourcePolicy(entry.size, config.resourcePolicy)
|
|
834
1089
|
});
|
|
835
1090
|
const data = toArrayBuffer(resource.bytes);
|
|
836
|
-
totalMediaBytes += data.byteLength;
|
|
837
|
-
if (totalMediaBytes > MAX_EXPORT_MEDIA_TOTAL_BYTES) {
|
|
838
|
-
throw new Error("Referenced media exceeds the browser video export memory limit.");
|
|
839
|
-
}
|
|
840
1091
|
images.set(entry.name, data);
|
|
841
1092
|
}
|
|
842
1093
|
}
|
|
843
1094
|
const docDuration = await frameCapture.init(
|
|
844
1095
|
doc,
|
|
845
|
-
{
|
|
1096
|
+
{
|
|
1097
|
+
images,
|
|
1098
|
+
audio: config.audio,
|
|
1099
|
+
width,
|
|
1100
|
+
height,
|
|
1101
|
+
animationsEnabled,
|
|
1102
|
+
theme: config.theme,
|
|
1103
|
+
videoPresentation: config.videoPresentation,
|
|
1104
|
+
pipSize: config.pipSize,
|
|
1105
|
+
pipShape: config.pipShape,
|
|
1106
|
+
pipPosition: config.pipPosition,
|
|
1107
|
+
showCoverSlide: cover.showCoverSlide
|
|
1108
|
+
},
|
|
846
1109
|
captionMode
|
|
847
1110
|
);
|
|
848
1111
|
if (cancelledRef.current) return;
|
|
849
|
-
setDuration(docDuration);
|
|
850
1112
|
if (docDuration <= 0) {
|
|
851
1113
|
throw new Error("Document has zero duration \u2014 nothing to export");
|
|
852
1114
|
}
|
|
853
|
-
const
|
|
1115
|
+
const coverFrameCount = Math.ceil(cover.coverPreRoll * fps);
|
|
1116
|
+
const storyFrameCount = Math.ceil(docDuration * fps);
|
|
1117
|
+
const totalFrames = coverFrameCount + storyFrameCount;
|
|
1118
|
+
const exportDuration = totalFrames / fps;
|
|
1119
|
+
setDuration(exportDuration);
|
|
854
1120
|
setPhase("Checking video encoder\u2026");
|
|
855
1121
|
setProgress(5);
|
|
856
1122
|
const canUseWebCodecs = webCodecsAvailable && await settleWithin(
|
|
@@ -859,7 +1125,7 @@ function useVideoExport() {
|
|
|
859
1125
|
"The browser did not finish checking WebCodecs support."
|
|
860
1126
|
).catch(() => false);
|
|
861
1127
|
const audioBitrate = (QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.normal).audioBitrate;
|
|
862
|
-
const timeline = effectiveOutputFormat === "mp4" && audioPolicy !== "omit" ? computeAudioTimeline(doc,
|
|
1128
|
+
const timeline = effectiveOutputFormat === "mp4" && audioPolicy !== "omit" ? computeAudioTimeline(doc, coverFrameCount / fps) : [];
|
|
863
1129
|
const aacSupported = timeline.length > 0 ? await supportsWebCodecsAac(EXPORT_AUDIO_SAMPLE_RATE, EXPORT_AUDIO_CHANNELS) : false;
|
|
864
1130
|
const tierDecision = selectAudioTier({
|
|
865
1131
|
hasClips: timeline.length > 0,
|
|
@@ -894,7 +1160,7 @@ function useVideoExport() {
|
|
|
894
1160
|
} else {
|
|
895
1161
|
const totalAudioDur = timeline.reduce(
|
|
896
1162
|
(max, c) => Math.max(max, c.startSec + c.durationSec),
|
|
897
|
-
|
|
1163
|
+
exportDuration
|
|
898
1164
|
);
|
|
899
1165
|
renderedAudio = await renderAudioTimeline(
|
|
900
1166
|
timeline,
|
|
@@ -954,13 +1220,20 @@ function useVideoExport() {
|
|
|
954
1220
|
}
|
|
955
1221
|
if (cancelledRef.current) return;
|
|
956
1222
|
setProgress(CAPTURE_PROGRESS_START);
|
|
957
|
-
setPhase(`Capturing frame 1/${totalFrames}
|
|
1223
|
+
setPhase(`Capturing frame 1/${totalFrames}`);
|
|
1224
|
+
setCurrentFrameTime(0);
|
|
958
1225
|
setState("capturing");
|
|
959
1226
|
const captureStartTime = performance.now();
|
|
1227
|
+
const frameBoundaryTimes = [captureStartTime];
|
|
1228
|
+
if (coverFrameCount > 0) await frameCapture.setCoverVisible(true);
|
|
960
1229
|
for (let i = 0; i < totalFrames; i++) {
|
|
961
1230
|
if (cancelledRef.current) return;
|
|
1231
|
+
if (coverFrameCount > 0 && i === coverFrameCount) {
|
|
1232
|
+
await frameCapture.setCoverVisible(false);
|
|
1233
|
+
}
|
|
962
1234
|
const time = i / fps;
|
|
963
|
-
const
|
|
1235
|
+
const captureTime = i < coverFrameCount ? 0 : (i - coverFrameCount) / fps;
|
|
1236
|
+
const captureOperation = canUseWebCodecs ? frameCapture.captureCanvasFrame(captureTime, { reuseIfUnchanged: true }) : frameCapture.captureFrame(captureTime, { reuseIfUnchanged: true });
|
|
964
1237
|
const frame = await settleWithin(
|
|
965
1238
|
captureOperation,
|
|
966
1239
|
FRAME_CAPTURE_TIMEOUT_MS,
|
|
@@ -971,15 +1244,31 @@ function useVideoExport() {
|
|
|
971
1244
|
releaseEncoderFrame(frame);
|
|
972
1245
|
return;
|
|
973
1246
|
}
|
|
974
|
-
|
|
1247
|
+
const previewOptions = previewOptionsRef.current;
|
|
1248
|
+
const previewInterval = Math.max(1, Math.floor(previewOptions.previewEveryNFrames ?? 1));
|
|
1249
|
+
if (previewOptions.onFramePreview && (i === 0 || i === totalFrames - 1 || i % previewInterval === 0)) {
|
|
1250
|
+
try {
|
|
1251
|
+
previewOptions.onFramePreview({ source: frame, frameIndex: i, totalFrames, time });
|
|
1252
|
+
} catch {
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
setPhase(`Encoding frame ${i + 1}/${totalFrames}`);
|
|
1256
|
+
setCurrentFrameTime(time);
|
|
975
1257
|
await settleWithin(
|
|
976
1258
|
encoder.encodeFrame(frame, i),
|
|
977
1259
|
FRAME_ENCODE_TIMEOUT_MS,
|
|
978
1260
|
`Video encoding stopped responding at frame ${i + 1}/${totalFrames}.`
|
|
979
1261
|
);
|
|
980
1262
|
const completedFrames = i + 1;
|
|
1263
|
+
const completedAt = performance.now();
|
|
1264
|
+
frameBoundaryTimes.push(completedAt);
|
|
1265
|
+
if (frameBoundaryTimes.length > FRAME_RATE_WINDOW_SIZE + 1) {
|
|
1266
|
+
frameBoundaryTimes.shift();
|
|
1267
|
+
}
|
|
1268
|
+
setProcessingFps(calculateRollingFramesPerSecond(frameBoundaryTimes));
|
|
1269
|
+
setCurrentFrameTime(Math.min(completedFrames / fps, exportDuration));
|
|
981
1270
|
setPhase(
|
|
982
|
-
completedFrames < totalFrames ? `Capturing frame ${completedFrames + 1}/${totalFrames}
|
|
1271
|
+
completedFrames < totalFrames ? `Capturing frame ${completedFrames + 1}/${totalFrames}` : `Captured ${totalFrames.toLocaleString()} frames\u2026`
|
|
983
1272
|
);
|
|
984
1273
|
const captureRatio = completedFrames / totalFrames;
|
|
985
1274
|
const captureProgress = CAPTURE_PROGRESS_START + captureRatio * (CAPTURE_PROGRESS_END - CAPTURE_PROGRESS_START);
|
|
@@ -1085,6 +1374,8 @@ function useVideoExport() {
|
|
|
1085
1374
|
state,
|
|
1086
1375
|
progress,
|
|
1087
1376
|
phase,
|
|
1377
|
+
currentFrameTime,
|
|
1378
|
+
processingFps,
|
|
1088
1379
|
duration,
|
|
1089
1380
|
outputFormat,
|
|
1090
1381
|
backend,
|
|
@@ -1103,5 +1394,7 @@ function useVideoExport() {
|
|
|
1103
1394
|
|
|
1104
1395
|
export {
|
|
1105
1396
|
useFrameCapture,
|
|
1397
|
+
DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS,
|
|
1398
|
+
resolveVideoExportCover,
|
|
1106
1399
|
useVideoExport
|
|
1107
1400
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useVideoExport
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-NZYFSB2T.js";
|
|
4
4
|
|
|
5
5
|
// src/VideoExportModal.tsx
|
|
6
6
|
import { useState, useCallback, useId, useRef } from "react";
|
|
@@ -12,6 +12,31 @@ function formatDuration(seconds) {
|
|
|
12
12
|
const s = seconds % 60;
|
|
13
13
|
return `${m}m ${s}s`;
|
|
14
14
|
}
|
|
15
|
+
function formatProcessingFps(framesPerSecond) {
|
|
16
|
+
return `${framesPerSecond.toFixed(2)} fps`;
|
|
17
|
+
}
|
|
18
|
+
function formatRealtimeMultiplier(processingFps, outputFps) {
|
|
19
|
+
return `${(processingFps / outputFps).toFixed(2)}\xD7 realtime`;
|
|
20
|
+
}
|
|
21
|
+
var FRAME_PREVIEW_INTERVAL = 15;
|
|
22
|
+
var FRAME_PREVIEW_WIDTH = 480;
|
|
23
|
+
var FRAME_PREVIEW_HEIGHT = 270;
|
|
24
|
+
function drawVideoExportPreview(canvas, source) {
|
|
25
|
+
const context = canvas.getContext("2d");
|
|
26
|
+
if (!context || source.width <= 0 || source.height <= 0) return;
|
|
27
|
+
context.fillStyle = "#000000";
|
|
28
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
29
|
+
const scale = Math.min(canvas.width / source.width, canvas.height / source.height);
|
|
30
|
+
const width = source.width * scale;
|
|
31
|
+
const height = source.height * scale;
|
|
32
|
+
context.drawImage(
|
|
33
|
+
source,
|
|
34
|
+
(canvas.width - width) / 2,
|
|
35
|
+
(canvas.height - height) / 2,
|
|
36
|
+
width,
|
|
37
|
+
height
|
|
38
|
+
);
|
|
39
|
+
}
|
|
15
40
|
var VIDEO_EXPORT_PALETTES = {
|
|
16
41
|
light: {
|
|
17
42
|
overlay: "rgba(0, 0, 0, 0.5)",
|
|
@@ -139,6 +164,7 @@ function VideoExportModal({
|
|
|
139
164
|
}) {
|
|
140
165
|
const overlayRef = useRef(null);
|
|
141
166
|
const dialogRef = useRef(null);
|
|
167
|
+
const previewCanvasRef = useRef(null);
|
|
142
168
|
const titleId = useId();
|
|
143
169
|
const initialOutputFormat = defaultConfig?.outputFormat ?? "mp4";
|
|
144
170
|
const [outputFormat, setOutputFormat] = useState(initialOutputFormat);
|
|
@@ -188,11 +214,20 @@ function VideoExportModal({
|
|
|
188
214
|
color: palette.text,
|
|
189
215
|
border: `1px solid ${palette.border}`
|
|
190
216
|
};
|
|
191
|
-
const
|
|
217
|
+
const handleFramePreview = useCallback((preview) => {
|
|
218
|
+
const canvas = previewCanvasRef.current;
|
|
219
|
+
if (canvas) drawVideoExportPreview(canvas, preview.source);
|
|
220
|
+
}, []);
|
|
221
|
+
const exportHook = useVideoExport({
|
|
222
|
+
onFramePreview: handleFramePreview,
|
|
223
|
+
previewEveryNFrames: FRAME_PREVIEW_INTERVAL
|
|
224
|
+
});
|
|
192
225
|
const {
|
|
193
226
|
state,
|
|
194
227
|
progress,
|
|
195
228
|
phase,
|
|
229
|
+
currentFrameTime,
|
|
230
|
+
processingFps,
|
|
196
231
|
outputFormat: completedOutputFormat,
|
|
197
232
|
downloadUrl,
|
|
198
233
|
fileSize,
|
|
@@ -218,6 +253,12 @@ function VideoExportModal({
|
|
|
218
253
|
}
|
|
219
254
|
}, []);
|
|
220
255
|
const handleExport = useCallback(async () => {
|
|
256
|
+
const previewCanvas = previewCanvasRef.current;
|
|
257
|
+
const previewContext = previewCanvas?.getContext("2d");
|
|
258
|
+
if (previewCanvas && previewContext) {
|
|
259
|
+
previewContext.fillStyle = "#000000";
|
|
260
|
+
previewContext.fillRect(0, 0, previewCanvas.width, previewCanvas.height);
|
|
261
|
+
}
|
|
221
262
|
const config = {
|
|
222
263
|
// defaultConfig is the base; explicit props/selections win over it.
|
|
223
264
|
...defaultConfig,
|
|
@@ -439,6 +480,27 @@ function VideoExportModal({
|
|
|
439
480
|
] })
|
|
440
481
|
] }),
|
|
441
482
|
isExporting && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
483
|
+
/* @__PURE__ */ jsx(
|
|
484
|
+
"canvas",
|
|
485
|
+
{
|
|
486
|
+
ref: previewCanvasRef,
|
|
487
|
+
width: FRAME_PREVIEW_WIDTH,
|
|
488
|
+
height: FRAME_PREVIEW_HEIGHT,
|
|
489
|
+
role: "img",
|
|
490
|
+
"aria-label": "Latest rendered video frame",
|
|
491
|
+
"data-squisq-video-export-preview": true,
|
|
492
|
+
style: {
|
|
493
|
+
display: "block",
|
|
494
|
+
width: "100%",
|
|
495
|
+
height: "auto",
|
|
496
|
+
aspectRatio: "16 / 9",
|
|
497
|
+
boxSizing: "border-box",
|
|
498
|
+
background: "#000000",
|
|
499
|
+
border: `1px solid ${palette.border}`,
|
|
500
|
+
marginBottom: 12
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
),
|
|
442
504
|
/* @__PURE__ */ jsx(
|
|
443
505
|
"div",
|
|
444
506
|
{
|
|
@@ -463,16 +525,40 @@ function VideoExportModal({
|
|
|
463
525
|
)
|
|
464
526
|
}
|
|
465
527
|
),
|
|
466
|
-
/* @__PURE__ */ jsxs(
|
|
467
|
-
progress,
|
|
468
|
-
"% complete"
|
|
469
|
-
] }),
|
|
470
|
-
phase && /* @__PURE__ */ jsx(
|
|
528
|
+
/* @__PURE__ */ jsxs(
|
|
471
529
|
"p",
|
|
472
530
|
{
|
|
473
|
-
style: { fontSize:
|
|
531
|
+
style: { fontSize: 13, fontVariantNumeric: "tabular-nums", margin: "0 0 4px 0" },
|
|
532
|
+
"data-squisq-video-export-progress-label": true,
|
|
533
|
+
children: [
|
|
534
|
+
progress.toFixed(1),
|
|
535
|
+
"% complete",
|
|
536
|
+
currentFrameTime != null && `, @ ${currentFrameTime.toFixed(1)} seconds`
|
|
537
|
+
]
|
|
538
|
+
}
|
|
539
|
+
),
|
|
540
|
+
phase && /* @__PURE__ */ jsxs(
|
|
541
|
+
"p",
|
|
542
|
+
{
|
|
543
|
+
style: {
|
|
544
|
+
fontSize: 12,
|
|
545
|
+
fontVariantNumeric: "tabular-nums",
|
|
546
|
+
color: palette.label,
|
|
547
|
+
margin: "0 0 4px 0"
|
|
548
|
+
},
|
|
474
549
|
"data-squisq-video-export-phase": true,
|
|
475
|
-
children:
|
|
550
|
+
children: [
|
|
551
|
+
phase,
|
|
552
|
+
processingFps != null && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
553
|
+
" - ",
|
|
554
|
+
/* @__PURE__ */ jsxs("span", { "data-squisq-video-export-frame-metrics": true, children: [
|
|
555
|
+
formatProcessingFps(processingFps),
|
|
556
|
+
" \xB7",
|
|
557
|
+
" ",
|
|
558
|
+
formatRealtimeMultiplier(processingFps, fps)
|
|
559
|
+
] })
|
|
560
|
+
] })
|
|
561
|
+
]
|
|
476
562
|
}
|
|
477
563
|
),
|
|
478
564
|
/* @__PURE__ */ jsxs("p", { style: { fontSize: 12, color: palette.muted, margin: 0 }, children: [
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { Doc, MediaProvider } from '@bendyline/squisq/schemas';
|
|
3
|
-
import { a as VideoExportConfig } from '../useVideoExport-
|
|
3
|
+
import { a as VideoExportConfig } from '../useVideoExport-Okwrf8gg.js';
|
|
4
4
|
import '@bendyline/squisq/markdown';
|
|
5
5
|
import '@bendyline/squisq-video';
|
|
6
6
|
import '@bendyline/squisq-react';
|
|
7
|
+
import '../mainThreadEncoder-BgcFyYvO.js';
|
|
7
8
|
|
|
8
9
|
interface VideoExportModalProps {
|
|
9
10
|
/** The document to export */
|
package/dist/components/index.js
CHANGED
package/dist/encoder/index.d.ts
CHANGED
|
@@ -1,72 +1,6 @@
|
|
|
1
|
+
export { E as EncoderConfig, a as EncoderFrameSource, M as MainThreadEncoder, c as createEncoder, s as supportsWebCodecs, b as supportsWebCodecsH264 } from '../mainThreadEncoder-BgcFyYvO.js';
|
|
1
2
|
export { FfmpegWasmLoadConfig } from '@bendyline/squisq-video';
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
-
* Main-thread WebCodecs encoder.
|
|
5
|
-
*
|
|
6
|
-
* Encodes video frames to MP4 using the WebCodecs API and mp4-muxer,
|
|
7
|
-
* running directly on the main thread. This is simpler and avoids
|
|
8
|
-
* worker module-resolution issues with bundlers. Since frame capture
|
|
9
|
-
* via html2canvas (~100-200ms per frame) is the bottleneck — not
|
|
10
|
-
* encoding (~1ms per frame with hardware-accelerated WebCodecs) —
|
|
11
|
-
* worker offloading provides minimal benefit.
|
|
12
|
-
*
|
|
13
|
-
* Requirements: Chrome 94+ / Edge 94+ (WebCodecs support).
|
|
14
|
-
*/
|
|
15
|
-
interface EncoderConfig {
|
|
16
|
-
width: number;
|
|
17
|
-
height: number;
|
|
18
|
-
fps: number;
|
|
19
|
-
quality: 'draft' | 'normal' | 'high';
|
|
20
|
-
/**
|
|
21
|
-
* Total frames the caller intends to submit, when known. Unused by the
|
|
22
|
-
* main-thread encoder (the caller owns the progress bar) but forwarded by
|
|
23
|
-
* {@link createWorkerEncoder} so the worker can report real progress instead
|
|
24
|
-
* of guessing from the frames it happens to have seen.
|
|
25
|
-
*/
|
|
26
|
-
totalFrames?: number;
|
|
27
|
-
/**
|
|
28
|
-
* When present, the underlying muxer is configured with an AAC audio track
|
|
29
|
-
* and {@link MainThreadEncoder.addAudioChunk} becomes usable. Absent → the
|
|
30
|
-
* encoder produces a video-only MP4 exactly as before.
|
|
31
|
-
*/
|
|
32
|
-
audio?: {
|
|
33
|
-
numberOfChannels: number;
|
|
34
|
-
sampleRate: number;
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
type EncoderFrameSource = ImageBitmap | HTMLCanvasElement;
|
|
38
|
-
interface MainThreadEncoder {
|
|
39
|
-
/** Encode a single frame. ImageBitmap sources are closed after encoding. */
|
|
40
|
-
encodeFrame(frame: EncoderFrameSource, frameIndex: number): Promise<void>;
|
|
41
|
-
/**
|
|
42
|
-
* Hand an encoded audio chunk (from a WebCodecs `AudioEncoder`) to the muxer.
|
|
43
|
-
* Only valid when the encoder was created with an `audio` config; otherwise a
|
|
44
|
-
* no-op. Must be called before {@link finalize}.
|
|
45
|
-
*/
|
|
46
|
-
addAudioChunk?(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata): void;
|
|
47
|
-
/** Flush pending frames and finalize the MP4. Returns the MP4 ArrayBuffer. */
|
|
48
|
-
finalize(): Promise<ArrayBuffer>;
|
|
49
|
-
/** Close the encoder without producing output (e.g., on cancel). */
|
|
50
|
-
close(): void;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Check whether the browser supports WebCodecs video encoding.
|
|
54
|
-
*/
|
|
55
|
-
declare function supportsWebCodecs(): boolean;
|
|
56
|
-
/**
|
|
57
|
-
* Probe whether the WebCodecs encoder actually supports the H.264 profile
|
|
58
|
-
* we use. The `VideoEncoder` global can exist while the specific codec is
|
|
59
|
-
* unavailable — this is the case on Linux Chromium, which ships without
|
|
60
|
-
* the proprietary H.264 encoder.
|
|
61
|
-
*/
|
|
62
|
-
declare function supportsWebCodecsH264(config: EncoderConfig): Promise<boolean>;
|
|
63
|
-
/**
|
|
64
|
-
* Create a main-thread WebCodecs encoder.
|
|
65
|
-
*
|
|
66
|
-
* Throws if WebCodecs is not available.
|
|
67
|
-
*/
|
|
68
|
-
declare function createEncoder(config: EncoderConfig): MainThreadEncoder;
|
|
69
|
-
|
|
70
4
|
/**
|
|
71
5
|
* audioTrack — In-browser audio rendering + AAC encoding for MP4 export.
|
|
72
6
|
*
|
|
@@ -93,4 +27,4 @@ declare function createEncoder(config: EncoderConfig): MainThreadEncoder;
|
|
|
93
27
|
*/
|
|
94
28
|
declare function supportsWebCodecsAac(sampleRate?: number, channels?: number): Promise<boolean>;
|
|
95
29
|
|
|
96
|
-
export {
|
|
30
|
+
export { supportsWebCodecsAac };
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export { V as VideoAudioPolicy, a as VideoExportConfig, b as
|
|
1
|
+
export { D as DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS, R as ResolvedVideoExportCover, U as UseVideoExportOptions, V as VideoAudioPolicy, a as VideoExportConfig, b as VideoExportFramePreview, c as VideoExportResult, d as VideoExportState, e as VideoOutputFormat, r as resolveVideoExportCover, u as useVideoExport } from '../useVideoExport-Okwrf8gg.js';
|
|
2
2
|
import { Doc } from '@bendyline/squisq/schemas';
|
|
3
3
|
import { RenderHtmlOptions } from '@bendyline/squisq-video';
|
|
4
4
|
import { CaptionMode } from '@bendyline/squisq-react';
|
|
5
5
|
import '@bendyline/squisq/markdown';
|
|
6
|
+
import '../mainThreadEncoder-BgcFyYvO.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* useFrameCapture — Hidden div + html2canvas frame capture.
|
|
@@ -24,9 +25,15 @@ interface FrameCaptureOptions {
|
|
|
24
25
|
*/
|
|
25
26
|
reuseIfUnchanged?: boolean;
|
|
26
27
|
}
|
|
28
|
+
interface FrameCaptureRenderOptions extends Omit<RenderHtmlOptions, 'playerScript'> {
|
|
29
|
+
/** Whether the hidden player should materialize its managed cover. */
|
|
30
|
+
showCoverSlide?: boolean;
|
|
31
|
+
}
|
|
27
32
|
interface FrameCaptureHandle {
|
|
28
33
|
/** Initialize the hidden player. Returns the video duration in seconds. */
|
|
29
|
-
init: (doc: Doc, renderOptions:
|
|
34
|
+
init: (doc: Doc, renderOptions: FrameCaptureRenderOptions, captionMode?: CaptionMode) => Promise<number>;
|
|
35
|
+
/** Force the managed cover on or off before capturing export frames. */
|
|
36
|
+
setCoverVisible: (visible: boolean) => Promise<void>;
|
|
30
37
|
/** Capture a single frame at the given time (seconds). Returns an ImageBitmap. */
|
|
31
38
|
captureFrame: (time: number, options?: FrameCaptureOptions) => Promise<ImageBitmap>;
|
|
32
39
|
/**
|
|
@@ -42,4 +49,4 @@ interface FrameCaptureHandle {
|
|
|
42
49
|
*/
|
|
43
50
|
declare function useFrameCapture(): FrameCaptureHandle;
|
|
44
51
|
|
|
45
|
-
export { type FrameCaptureHandle, type FrameCaptureOptions, useFrameCapture };
|
|
52
|
+
export { type FrameCaptureHandle, type FrameCaptureOptions, type FrameCaptureRenderOptions, useFrameCapture };
|
package/dist/hooks/index.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS,
|
|
3
|
+
resolveVideoExportCover,
|
|
2
4
|
useFrameCapture,
|
|
3
5
|
useVideoExport
|
|
4
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-NZYFSB2T.js";
|
|
5
7
|
import "../chunk-ZQJBUX75.js";
|
|
6
8
|
import "../chunk-MEPETH5V.js";
|
|
7
9
|
export {
|
|
10
|
+
DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS,
|
|
11
|
+
resolveVideoExportCover,
|
|
8
12
|
useFrameCapture,
|
|
9
13
|
useVideoExport
|
|
10
14
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export { VideoExportButton, VideoExportButtonProps, VideoExportModal, VideoExportModalProps, VideoExportPalette } from './components/index.js';
|
|
2
|
-
export { V as VideoAudioPolicy, a as VideoExportConfig, b as
|
|
3
|
-
export { FrameCaptureHandle, FrameCaptureOptions, useFrameCapture } from './hooks/index.js';
|
|
4
|
-
export { EncoderConfig, EncoderFrameSource, MainThreadEncoder, createEncoder, supportsWebCodecs,
|
|
2
|
+
export { D as DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS, R as ResolvedVideoExportCover, U as UseVideoExportOptions, V as VideoAudioPolicy, a as VideoExportConfig, b as VideoExportFramePreview, c as VideoExportResult, d as VideoExportState, e as VideoOutputFormat, r as resolveVideoExportCover, u as useVideoExport } from './useVideoExport-Okwrf8gg.js';
|
|
3
|
+
export { FrameCaptureHandle, FrameCaptureOptions, FrameCaptureRenderOptions, useFrameCapture } from './hooks/index.js';
|
|
4
|
+
export { E as EncoderConfig, a as EncoderFrameSource, M as MainThreadEncoder, c as createEncoder, s as supportsWebCodecs, b as supportsWebCodecsH264 } from './mainThreadEncoder-BgcFyYvO.js';
|
|
5
5
|
export { FfmpegWasmLoadConfig } from '@bendyline/squisq-video';
|
|
6
|
+
export { supportsWebCodecsAac } from './encoder/index.js';
|
|
6
7
|
import 'react/jsx-runtime';
|
|
7
8
|
import '@bendyline/squisq/schemas';
|
|
8
9
|
import '@bendyline/squisq/markdown';
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
VideoExportButton,
|
|
3
3
|
VideoExportModal
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-U2TST3MN.js";
|
|
5
5
|
import {
|
|
6
|
+
DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS,
|
|
7
|
+
resolveVideoExportCover,
|
|
6
8
|
useFrameCapture,
|
|
7
9
|
useVideoExport
|
|
8
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-NZYFSB2T.js";
|
|
9
11
|
import {
|
|
10
12
|
createEncoder,
|
|
11
13
|
supportsWebCodecs,
|
|
@@ -14,9 +16,11 @@ import {
|
|
|
14
16
|
} from "./chunk-ZQJBUX75.js";
|
|
15
17
|
import "./chunk-MEPETH5V.js";
|
|
16
18
|
export {
|
|
19
|
+
DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS,
|
|
17
20
|
VideoExportButton,
|
|
18
21
|
VideoExportModal,
|
|
19
22
|
createEncoder,
|
|
23
|
+
resolveVideoExportCover,
|
|
20
24
|
supportsWebCodecs,
|
|
21
25
|
supportsWebCodecsAac,
|
|
22
26
|
supportsWebCodecsH264,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main-thread WebCodecs encoder.
|
|
3
|
+
*
|
|
4
|
+
* Encodes video frames to MP4 using the WebCodecs API and mp4-muxer,
|
|
5
|
+
* running directly on the main thread. This is simpler and avoids
|
|
6
|
+
* worker module-resolution issues with bundlers. Since frame capture
|
|
7
|
+
* via html2canvas (~100-200ms per frame) is the bottleneck — not
|
|
8
|
+
* encoding (~1ms per frame with hardware-accelerated WebCodecs) —
|
|
9
|
+
* worker offloading provides minimal benefit.
|
|
10
|
+
*
|
|
11
|
+
* Requirements: Chrome 94+ / Edge 94+ (WebCodecs support).
|
|
12
|
+
*/
|
|
13
|
+
interface EncoderConfig {
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
fps: number;
|
|
17
|
+
quality: 'draft' | 'normal' | 'high';
|
|
18
|
+
/**
|
|
19
|
+
* Total frames the caller intends to submit, when known. Unused by the
|
|
20
|
+
* main-thread encoder (the caller owns the progress bar) but forwarded by
|
|
21
|
+
* {@link createWorkerEncoder} so the worker can report real progress instead
|
|
22
|
+
* of guessing from the frames it happens to have seen.
|
|
23
|
+
*/
|
|
24
|
+
totalFrames?: number;
|
|
25
|
+
/**
|
|
26
|
+
* When present, the underlying muxer is configured with an AAC audio track
|
|
27
|
+
* and {@link MainThreadEncoder.addAudioChunk} becomes usable. Absent → the
|
|
28
|
+
* encoder produces a video-only MP4 exactly as before.
|
|
29
|
+
*/
|
|
30
|
+
audio?: {
|
|
31
|
+
numberOfChannels: number;
|
|
32
|
+
sampleRate: number;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
type EncoderFrameSource = ImageBitmap | HTMLCanvasElement;
|
|
36
|
+
interface MainThreadEncoder {
|
|
37
|
+
/** Encode a single frame. ImageBitmap sources are closed after encoding. */
|
|
38
|
+
encodeFrame(frame: EncoderFrameSource, frameIndex: number): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Hand an encoded audio chunk (from a WebCodecs `AudioEncoder`) to the muxer.
|
|
41
|
+
* Only valid when the encoder was created with an `audio` config; otherwise a
|
|
42
|
+
* no-op. Must be called before {@link finalize}.
|
|
43
|
+
*/
|
|
44
|
+
addAudioChunk?(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata): void;
|
|
45
|
+
/** Flush pending frames and finalize the MP4. Returns the MP4 ArrayBuffer. */
|
|
46
|
+
finalize(): Promise<ArrayBuffer>;
|
|
47
|
+
/** Close the encoder without producing output (e.g., on cancel). */
|
|
48
|
+
close(): void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check whether the browser supports WebCodecs video encoding.
|
|
52
|
+
*/
|
|
53
|
+
declare function supportsWebCodecs(): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Probe whether the WebCodecs encoder actually supports the H.264 profile
|
|
56
|
+
* we use. The `VideoEncoder` global can exist while the specific codec is
|
|
57
|
+
* unavailable — this is the case on Linux Chromium, which ships without
|
|
58
|
+
* the proprietary H.264 encoder.
|
|
59
|
+
*/
|
|
60
|
+
declare function supportsWebCodecsH264(config: EncoderConfig): Promise<boolean>;
|
|
61
|
+
/**
|
|
62
|
+
* Create a main-thread WebCodecs encoder.
|
|
63
|
+
*
|
|
64
|
+
* Throws if WebCodecs is not available.
|
|
65
|
+
*/
|
|
66
|
+
declare function createEncoder(config: EncoderConfig): MainThreadEncoder;
|
|
67
|
+
|
|
68
|
+
export { type EncoderConfig as E, type MainThreadEncoder as M, type EncoderFrameSource as a, supportsWebCodecsH264 as b, createEncoder as c, supportsWebCodecs as s };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { MediaProvider, Doc } from '@bendyline/squisq/schemas';
|
|
1
|
+
import { MediaProvider, Theme, VideoPresentation, VideoPipSize, VideoPipShape, VideoPipPosition, Doc } from '@bendyline/squisq/schemas';
|
|
2
2
|
import { ResourcePolicy } from '@bendyline/squisq/markdown';
|
|
3
3
|
import { VideoQuality, VideoOrientation, FfmpegWasmLoadConfig } from '@bendyline/squisq-video';
|
|
4
4
|
import { CaptionMode } from '@bendyline/squisq-react';
|
|
5
|
+
import { a as EncoderFrameSource } from './mainThreadEncoder-BgcFyYvO.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* useVideoExport — Main orchestration hook for browser video export.
|
|
@@ -20,6 +21,7 @@ import { CaptionMode } from '@bendyline/squisq-react';
|
|
|
20
21
|
* <button onClick={() => startExport(doc, options)}>Export</button>
|
|
21
22
|
*/
|
|
22
23
|
|
|
24
|
+
declare const DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS = 2;
|
|
23
25
|
type VideoExportState = 'idle' | 'preparing' | 'capturing' | 'encoding' | 'complete' | 'error';
|
|
24
26
|
/** Browser export container format. */
|
|
25
27
|
type VideoOutputFormat = 'mp4' | 'gif';
|
|
@@ -62,11 +64,31 @@ interface VideoExportConfig {
|
|
|
62
64
|
resourcePolicy?: ResourcePolicy;
|
|
63
65
|
/** Caption mode for the exported video (default: 'off' for MP4, 'standard' for GIF). */
|
|
64
66
|
captionMode?: CaptionMode;
|
|
67
|
+
/** Theme override for capture. Omitted values resolve from the Doc. */
|
|
68
|
+
theme?: Theme;
|
|
69
|
+
/** Player-level video placement override. Omitted values resolve from Doc frontmatter. */
|
|
70
|
+
videoPresentation?: VideoPresentation;
|
|
71
|
+
/** Picture-in-picture size override. Omitted values resolve from Doc frontmatter. */
|
|
72
|
+
pipSize?: VideoPipSize;
|
|
73
|
+
/** Picture-in-picture shape override. Omitted values resolve from Doc frontmatter. */
|
|
74
|
+
pipShape?: VideoPipShape;
|
|
75
|
+
/** Picture-in-picture corner override. Omitted values resolve from Doc frontmatter. */
|
|
76
|
+
pipPosition?: VideoPipPosition;
|
|
77
|
+
/** Managed-cover visibility override. Omitted values resolve from Doc frontmatter. */
|
|
78
|
+
showCoverSlide?: boolean;
|
|
79
|
+
/** Seconds to hold an enabled managed cover before story frame zero (default: 2). */
|
|
80
|
+
coverPreRoll?: number;
|
|
65
81
|
/** Player IIFE bundle (unused in browser export, kept for CLI/Playwright path) */
|
|
66
82
|
playerScript?: string;
|
|
67
83
|
/** Optional self-hosted ffmpeg.wasm core URLs for fallback/offline/CSP use. */
|
|
68
84
|
ffmpegWasm?: FfmpegWasmLoadConfig;
|
|
69
85
|
}
|
|
86
|
+
interface ResolvedVideoExportCover {
|
|
87
|
+
showCoverSlide: boolean;
|
|
88
|
+
coverPreRoll: number;
|
|
89
|
+
}
|
|
90
|
+
/** Resolve the cover segment shared by browser MP4/GIF capture. */
|
|
91
|
+
declare function resolveVideoExportCover(doc: Doc, config?: Pick<VideoExportConfig, 'showCoverSlide' | 'coverPreRoll'>): ResolvedVideoExportCover;
|
|
70
92
|
interface VideoExportResult {
|
|
71
93
|
/** Current export state */
|
|
72
94
|
state: VideoExportState;
|
|
@@ -74,6 +96,10 @@ interface VideoExportResult {
|
|
|
74
96
|
progress: number;
|
|
75
97
|
/** Human-readable description of the current phase */
|
|
76
98
|
phase: string;
|
|
99
|
+
/** Current timestamp being captured from the output timeline (seconds). */
|
|
100
|
+
currentFrameTime: number | null;
|
|
101
|
+
/** Rolling throughput over at most the last 30 completed frames. */
|
|
102
|
+
processingFps: number | null;
|
|
77
103
|
/** Video duration detected from the doc (seconds) */
|
|
78
104
|
duration: number;
|
|
79
105
|
/** Effective output format for the current or most recent export. */
|
|
@@ -110,6 +136,26 @@ interface VideoExportResult {
|
|
|
110
136
|
/** Reset state back to idle (e.g., after complete or error) */
|
|
111
137
|
reset: () => void;
|
|
112
138
|
}
|
|
113
|
-
|
|
139
|
+
interface VideoExportFramePreview {
|
|
140
|
+
/** The raster that is about to be submitted to the encoder. Valid only during the callback. */
|
|
141
|
+
source: EncoderFrameSource;
|
|
142
|
+
/** Zero-based frame index. */
|
|
143
|
+
frameIndex: number;
|
|
144
|
+
/** Total number of frames in the export. */
|
|
145
|
+
totalFrames: number;
|
|
146
|
+
/** Timestamp on the output timeline, in seconds. */
|
|
147
|
+
time: number;
|
|
148
|
+
}
|
|
149
|
+
interface UseVideoExportOptions {
|
|
150
|
+
/**
|
|
151
|
+
* Observe an occasional captured frame without performing another rasterization.
|
|
152
|
+
* The source may be reused or closed as soon as this synchronous callback returns.
|
|
153
|
+
* Observer errors are ignored so display-only work cannot fail an export.
|
|
154
|
+
*/
|
|
155
|
+
onFramePreview?: (preview: VideoExportFramePreview) => void;
|
|
156
|
+
/** Preview the first, last, and every Nth frame. Defaults to 1. */
|
|
157
|
+
previewEveryNFrames?: number;
|
|
158
|
+
}
|
|
159
|
+
declare function useVideoExport(options?: UseVideoExportOptions): VideoExportResult;
|
|
114
160
|
|
|
115
|
-
export { type VideoAudioPolicy as V, type VideoExportConfig as a, type
|
|
161
|
+
export { DEFAULT_VIDEO_COVER_PRE_ROLL_SECONDS as D, type ResolvedVideoExportCover as R, type UseVideoExportOptions as U, type VideoAudioPolicy as V, type VideoExportConfig as a, type VideoExportFramePreview as b, type VideoExportResult as c, type VideoExportState as d, type VideoOutputFormat as e, resolveVideoExportCover as r, useVideoExport as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/squisq-video-react",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.6",
|
|
4
4
|
"description": "React components for browser-based MP4 and animated-GIF export of Squisq documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bendyline",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"type": "module",
|
|
26
26
|
"engines": {
|
|
27
|
-
"node": "
|
|
27
|
+
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
|
28
28
|
},
|
|
29
29
|
"main": "./dist/index.js",
|
|
30
30
|
"types": "./dist/index.d.ts",
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@bendyline/squisq": "2.4.
|
|
69
|
-
"@bendyline/squisq-video": "2.2.
|
|
70
|
-
"@bendyline/squisq-react": "2.4.
|
|
68
|
+
"@bendyline/squisq": "2.4.2",
|
|
69
|
+
"@bendyline/squisq-video": "2.2.6",
|
|
70
|
+
"@bendyline/squisq-react": "2.4.2",
|
|
71
71
|
"@ffmpeg/core": "0.12.9",
|
|
72
72
|
"@ffmpeg/ffmpeg": "0.12.15",
|
|
73
73
|
"@ffmpeg/util": "0.12.2",
|