@bendyline/squisq-video-react 1.2.2 → 2.0.1
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/README.md +63 -22
- package/dist/index.d.ts +33 -13
- package/dist/index.js +511 -212
- package/dist/index.js.map +1 -1
- package/dist/workers/encode.worker.js +110 -80
- package/dist/workers/encode.worker.js.map +1 -1
- package/dist/workers/ffmpeg.class-worker.d.ts +2 -0
- package/dist/workers/ffmpeg.class-worker.js +180 -0
- package/dist/workers/ffmpeg.class-worker.js.map +1 -0
- package/package.json +7 -5
- package/src/VideoExportButton.tsx +12 -5
- package/src/VideoExportModal.tsx +219 -62
- package/src/__tests__/gifTranscode.test.ts +118 -0
- package/src/__tests__/useVideoExportGif.test.ts +155 -0
- package/src/__tests__/videoExportProps.test.tsx +82 -7
- package/src/__tests__/workerEncoder.test.ts +143 -0
- package/src/audioTrack.ts +18 -9
- package/src/gifTranscode.ts +75 -0
- package/src/hooks/useFrameCapture.ts +26 -21
- package/src/hooks/useVideoExport.ts +104 -15
- package/src/index.ts +2 -0
- package/src/mainThreadEncoder.ts +5 -10
- package/src/workerEncoder.ts +66 -14
- package/src/workers/encode.worker.ts +134 -86
- package/src/workers/ffmpeg.class-worker.ts +11 -0
- package/src/workers/workerTypes.ts +9 -1
|
@@ -21,6 +21,14 @@ function postProgress(percent, phase) {
|
|
|
21
21
|
function postError(message) {
|
|
22
22
|
post({ type: "error", message });
|
|
23
23
|
}
|
|
24
|
+
function disposeFfmpeg() {
|
|
25
|
+
const instance = ffmpegInstance;
|
|
26
|
+
try {
|
|
27
|
+
instance?.terminate?.();
|
|
28
|
+
} finally {
|
|
29
|
+
ffmpegInstance = null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
24
32
|
function hasWebCodecs() {
|
|
25
33
|
return typeof VideoEncoder !== "undefined" && typeof VideoFrame !== "undefined";
|
|
26
34
|
}
|
|
@@ -95,15 +103,21 @@ async function finalizeWebCodecs() {
|
|
|
95
103
|
async function initFfmpegWasm(config) {
|
|
96
104
|
ffmpegConfig = config;
|
|
97
105
|
ffmpegFrames = [];
|
|
106
|
+
ffmpegSegments.length = 0;
|
|
98
107
|
totalFramesReceived = 0;
|
|
108
|
+
let ffmpeg = null;
|
|
99
109
|
try {
|
|
100
110
|
const { FFmpeg } = await import("@ffmpeg/ffmpeg");
|
|
101
|
-
|
|
102
|
-
await ffmpeg.load(
|
|
111
|
+
ffmpeg = new FFmpeg();
|
|
112
|
+
await ffmpeg.load({
|
|
113
|
+
...config.ffmpegWasm,
|
|
114
|
+
classWorkerURL: config.ffmpegWasm?.classWorkerURL ?? new URL("./ffmpeg.class-worker.js", import.meta.url).href
|
|
115
|
+
});
|
|
103
116
|
ffmpegInstance = ffmpeg;
|
|
104
117
|
} catch (err) {
|
|
118
|
+
ffmpeg?.terminate();
|
|
105
119
|
const message = err instanceof Error ? err.message : String(err);
|
|
106
|
-
|
|
120
|
+
throw new Error(
|
|
107
121
|
`Failed to load ffmpeg.wasm: ${message}. Ensure @ffmpeg/ffmpeg and @ffmpeg/util are installed and Cross-Origin-Isolation headers (COOP/COEP) are set.`
|
|
108
122
|
);
|
|
109
123
|
}
|
|
@@ -136,6 +150,12 @@ async function encodeFrameFfmpeg(msg) {
|
|
|
136
150
|
}
|
|
137
151
|
}
|
|
138
152
|
var ffmpegSegments = [];
|
|
153
|
+
async function execOrThrow(ffmpeg, args, operation) {
|
|
154
|
+
const exitCode = await ffmpeg.exec(args);
|
|
155
|
+
if (exitCode !== 0) {
|
|
156
|
+
throw new Error(`${operation} failed with ffmpeg exit code ${exitCode}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
139
159
|
async function encodeFfmpegBatch() {
|
|
140
160
|
if (!ffmpegInstance || ffmpegFrames.length === 0 || cancelled) return;
|
|
141
161
|
const ffmpeg = ffmpegInstance;
|
|
@@ -148,22 +168,26 @@ async function encodeFfmpegBatch() {
|
|
|
148
168
|
}
|
|
149
169
|
const firstIndex = ffmpegFrames[0].index;
|
|
150
170
|
const segmentName = `segment_${batchIndex}.mp4`;
|
|
151
|
-
await
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
171
|
+
await execOrThrow(
|
|
172
|
+
ffmpeg,
|
|
173
|
+
[
|
|
174
|
+
"-framerate",
|
|
175
|
+
String(config.fps),
|
|
176
|
+
"-start_number",
|
|
177
|
+
String(firstIndex),
|
|
178
|
+
"-i",
|
|
179
|
+
`frame_%06d.png`,
|
|
180
|
+
"-frames:v",
|
|
181
|
+
String(ffmpegFrames.length),
|
|
182
|
+
"-c:v",
|
|
183
|
+
"libx264",
|
|
184
|
+
"-pix_fmt",
|
|
185
|
+
"yuv420p",
|
|
186
|
+
...ffmpegVideoQualityArgs(config.quality),
|
|
187
|
+
segmentName
|
|
188
|
+
],
|
|
189
|
+
`Encoding video segment ${batchIndex + 1}`
|
|
190
|
+
);
|
|
167
191
|
const segmentData = await ffmpeg.readFile(segmentName);
|
|
168
192
|
ffmpegSegments.push(segmentData);
|
|
169
193
|
for (const frame of ffmpegFrames) {
|
|
@@ -189,17 +213,11 @@ async function finalizeFfmpeg() {
|
|
|
189
213
|
concatList.push(`file '${name}'`);
|
|
190
214
|
}
|
|
191
215
|
await ffmpeg.writeFile("concat.txt", new TextEncoder().encode(concatList.join("\n")));
|
|
192
|
-
await
|
|
193
|
-
|
|
194
|
-
"concat",
|
|
195
|
-
"
|
|
196
|
-
|
|
197
|
-
"-i",
|
|
198
|
-
"concat.txt",
|
|
199
|
-
"-c",
|
|
200
|
-
"copy",
|
|
201
|
-
"output.mp4"
|
|
202
|
-
]);
|
|
216
|
+
await execOrThrow(
|
|
217
|
+
ffmpeg,
|
|
218
|
+
["-f", "concat", "-safe", "0", "-i", "concat.txt", "-c", "copy", "output.mp4"],
|
|
219
|
+
"Concatenating video segments"
|
|
220
|
+
);
|
|
203
221
|
finalData = await ffmpeg.readFile("output.mp4");
|
|
204
222
|
for (let i = 0; i < ffmpegSegments.length; i++) {
|
|
205
223
|
await ffmpeg.deleteFile(`seg_${i}.mp4`);
|
|
@@ -213,64 +231,76 @@ async function finalizeFfmpeg() {
|
|
|
213
231
|
postProgress(95, "Preparing download\u2026");
|
|
214
232
|
const sliced = new Uint8Array(finalData).buffer;
|
|
215
233
|
post({ type: "complete", data: sliced, size: sliced.byteLength }, [sliced]);
|
|
216
|
-
|
|
234
|
+
disposeFfmpeg();
|
|
217
235
|
ffmpegSegments.length = 0;
|
|
218
236
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
237
|
+
async function handleMessage(msg) {
|
|
238
|
+
switch (msg.type) {
|
|
239
|
+
case "init": {
|
|
240
|
+
cancelled = false;
|
|
241
|
+
totalFramesReceived = 0;
|
|
242
|
+
if (await supportsWebCodecsH264(msg)) {
|
|
243
|
+
backend = "webcodecs";
|
|
244
|
+
initWebCodecs(msg);
|
|
245
|
+
} else if (hasSharedArrayBuffer()) {
|
|
246
|
+
backend = "ffmpeg-wasm";
|
|
247
|
+
await initFfmpegWasm(msg);
|
|
248
|
+
} else {
|
|
249
|
+
throw new Error(
|
|
250
|
+
"No video encoding support available. WebCodecs H.264 is unavailable (typical on Linux Chromium without proprietary codecs) and ffmpeg.wasm requires SharedArrayBuffer (Cross-Origin-Isolation headers)."
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
post({ type: "capabilities", backend });
|
|
254
|
+
postProgress(0, "Encoder ready");
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case "frame": {
|
|
258
|
+
if (cancelled) {
|
|
259
|
+
msg.bitmap.close();
|
|
260
|
+
return;
|
|
241
261
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
262
|
+
if (backend === "webcodecs") {
|
|
263
|
+
await encodeFrameWebCodecs(msg);
|
|
264
|
+
} else if (backend === "ffmpeg-wasm") {
|
|
265
|
+
await encodeFrameFfmpeg(msg);
|
|
266
|
+
} else {
|
|
267
|
+
msg.bitmap.close();
|
|
268
|
+
throw new Error("Encoder received a frame before it was initialized");
|
|
249
269
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
270
|
+
post({ type: "frame-complete", frameIndex: msg.frameIndex });
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
case "finalize": {
|
|
274
|
+
if (backend === "webcodecs") {
|
|
275
|
+
await finalizeWebCodecs();
|
|
276
|
+
} else if (backend === "ffmpeg-wasm") {
|
|
277
|
+
await finalizeFfmpeg();
|
|
278
|
+
} else {
|
|
279
|
+
throw new Error("Encoder cannot finalize before it is initialized");
|
|
257
280
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
muxer = null;
|
|
265
|
-
ffmpegInstance = null;
|
|
266
|
-
ffmpegFrames = [];
|
|
267
|
-
ffmpegSegments.length = 0;
|
|
268
|
-
break;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case "cancel": {
|
|
284
|
+
if (videoEncoder && videoEncoder.state !== "closed") {
|
|
285
|
+
videoEncoder.close();
|
|
286
|
+
videoEncoder = null;
|
|
269
287
|
}
|
|
288
|
+
muxer = null;
|
|
289
|
+
disposeFfmpeg();
|
|
290
|
+
ffmpegFrames = [];
|
|
291
|
+
ffmpegSegments.length = 0;
|
|
292
|
+
backend = null;
|
|
293
|
+
break;
|
|
270
294
|
}
|
|
271
|
-
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
var messageQueue = Promise.resolve();
|
|
298
|
+
self.onmessage = (event) => {
|
|
299
|
+
const msg = event.data;
|
|
300
|
+
if (msg.type === "cancel") cancelled = true;
|
|
301
|
+
messageQueue = messageQueue.then(() => handleMessage(msg)).catch((err) => {
|
|
272
302
|
const message = err instanceof Error ? err.message : String(err);
|
|
273
303
|
postError(message);
|
|
274
|
-
}
|
|
304
|
+
});
|
|
275
305
|
};
|
|
276
306
|
//# sourceMappingURL=encode.worker.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/workers/encode.worker.ts"],"sourcesContent":["/**\n * Video Encoding Web Worker\n *\n * Encodes video frames to MP4 using one of two backends:\n * - **WebCodecs** (preferred): Streaming H.264 encoding via VideoEncoder + mp4-muxer.\n * Each frame is encoded as it arrives — minimal memory footprint.\n * - **ffmpeg.wasm** (fallback): Batch encoding in ~10-second segments.\n * Used when WebCodecs is unavailable (older browsers).\n *\n * Frames arrive as ImageBitmap (zero-copy transfer from main thread).\n */\n\nimport type {\n MainToWorkerMessage,\n WorkerToMainMessage,\n InitMessage,\n FrameMessage,\n} from './workerTypes.js';\nimport { bitrateForQuality, ffmpegVideoQualityArgs } from '@bendyline/squisq-video';\n\nimport { createMp4Muxer, type Mp4MuxerHandle } from '../mp4Mux.js';\n\n// ── State ──────────────────────────────────────────────────────────\n\nlet backend: 'webcodecs' | 'ffmpeg-wasm' | null = null;\nlet cancelled = false;\n\n// WebCodecs state\nlet videoEncoder: VideoEncoder | null = null;\nlet muxer: Mp4MuxerHandle | null = null;\n\n// ffmpeg.wasm state\nlet ffmpegInstance: unknown = null;\nlet ffmpegFrames: Array<{ data: Uint8Array; index: number }> = [];\nlet ffmpegConfig: InitMessage | null = null;\n\n// Frame tracking\nlet totalFramesReceived = 0;\n\n// ── Helpers ────────────────────────────────────────────────────────\n\nfunction post(msg: WorkerToMainMessage, transfer?: Transferable[]) {\n self.postMessage(msg, { transfer: transfer ?? [] });\n}\n\nfunction postProgress(percent: number, phase: string) {\n post({ type: 'progress', percent, phase });\n}\n\nfunction postError(message: string) {\n post({ type: 'error', message });\n}\n\n// ── Feature Detection ──────────────────────────────────────────────\n\nfunction hasWebCodecs(): boolean {\n return typeof VideoEncoder !== 'undefined' && typeof VideoFrame !== 'undefined';\n}\n\nfunction hasSharedArrayBuffer(): boolean {\n return typeof SharedArrayBuffer !== 'undefined';\n}\n\n/**\n * Check whether the browser's WebCodecs implementation actually supports\n * the H.264 Baseline profile we use. Linux Chromium ships without the\n * proprietary H.264 encoder, so `VideoEncoder` exists but `configure()`\n * for `avc1.*` fails asynchronously.\n */\nasync function supportsWebCodecsH264(config: InitMessage): Promise<boolean> {\n if (!hasWebCodecs()) return false;\n try {\n const support = await VideoEncoder.isConfigSupported({\n codec: 'avc1.42001f',\n width: config.width,\n height: config.height,\n bitrate: bitrateForQuality(config.quality, config.width, config.height),\n framerate: config.fps,\n });\n return support.supported === true;\n } catch {\n return false;\n }\n}\n\n// ── WebCodecs Backend ──────────────────────────────────────────────\n\nfunction initWebCodecs(config: InitMessage) {\n muxer = createMp4Muxer({\n width: config.width,\n height: config.height,\n fps: config.fps,\n });\n\n videoEncoder = new VideoEncoder({\n output(chunk, meta) {\n if (cancelled) return;\n muxer!.addVideoChunk(chunk, meta ?? undefined);\n },\n error(err) {\n postError(`WebCodecs encoder error: ${err.message}`);\n },\n });\n\n // Deliberate profile split from mainThreadEncoder (avc1.640028, High@4.0):\n // this worker is the max-compatibility fallback path, so it uses H.264\n // Baseline (avc1.42001f) — widest decoder support at a small quality cost.\n videoEncoder.configure({\n codec: 'avc1.42001f',\n width: config.width,\n height: config.height,\n bitrate: bitrateForQuality(config.quality, config.width, config.height),\n framerate: config.fps,\n });\n}\n\nasync function encodeFrameWebCodecs(msg: FrameMessage) {\n if (!videoEncoder || cancelled) {\n msg.bitmap.close();\n return;\n }\n\n const frame = new VideoFrame(msg.bitmap, { timestamp: msg.timestamp });\n const keyFrame = msg.frameIndex % 30 === 0; // Key frame every 30 frames\n videoEncoder.encode(frame, { keyFrame });\n frame.close();\n msg.bitmap.close();\n totalFramesReceived++;\n\n postProgress(\n Math.round((totalFramesReceived / (totalFramesReceived + 1)) * 50),\n `Encoding frame ${totalFramesReceived}`,\n );\n}\n\nasync function finalizeWebCodecs() {\n if (!videoEncoder || !muxer || cancelled) return;\n\n postProgress(50, 'Flushing encoder…');\n await videoEncoder.flush();\n videoEncoder.close();\n videoEncoder = null;\n\n postProgress(90, 'Finalizing MP4…');\n const buffer = muxer.finalize();\n muxer = null;\n\n post({ type: 'complete', data: buffer, size: buffer.byteLength }, [buffer]);\n}\n\n// ── ffmpeg.wasm Backend ────────────────────────────────────────────\n\nasync function initFfmpegWasm(config: InitMessage) {\n ffmpegConfig = config;\n ffmpegFrames = [];\n totalFramesReceived = 0;\n\n // Lazy-load ffmpeg.wasm\n try {\n const { FFmpeg } = await import('@ffmpeg/ffmpeg');\n const ffmpeg = new FFmpeg();\n await ffmpeg.load();\n ffmpegInstance = ffmpeg;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n postError(\n `Failed to load ffmpeg.wasm: ${message}. ` +\n 'Ensure @ffmpeg/ffmpeg and @ffmpeg/util are installed and ' +\n 'Cross-Origin-Isolation headers (COOP/COEP) are set.',\n );\n }\n}\n\nasync function encodeFrameFfmpeg(msg: FrameMessage) {\n if (cancelled) {\n msg.bitmap.close();\n return;\n }\n\n // Convert ImageBitmap to PNG bytes via OffscreenCanvas\n const canvas = new OffscreenCanvas(msg.bitmap.width, msg.bitmap.height);\n const ctx = canvas.getContext('2d');\n if (!ctx) {\n msg.bitmap.close();\n postError('Failed to create OffscreenCanvas 2D context');\n return;\n }\n ctx.drawImage(msg.bitmap, 0, 0);\n msg.bitmap.close();\n\n const blob = await canvas.convertToBlob({ type: 'image/png' });\n const arrayBuffer = await blob.arrayBuffer();\n ffmpegFrames.push({ data: new Uint8Array(arrayBuffer), index: msg.frameIndex });\n totalFramesReceived++;\n\n postProgress(\n Math.round((totalFramesReceived / (totalFramesReceived + 1)) * 40),\n `Collecting frame ${totalFramesReceived}`,\n );\n\n // Batch encode every ~10 seconds worth of frames\n const batchSize = (ffmpegConfig?.fps ?? 30) * 10;\n if (ffmpegFrames.length >= batchSize) {\n await encodeFfmpegBatch();\n }\n}\n\n/** Segments already encoded as MP4 data. */\nconst ffmpegSegments: Uint8Array[] = [];\n\nasync function encodeFfmpegBatch() {\n if (!ffmpegInstance || ffmpegFrames.length === 0 || cancelled) return;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const ffmpeg = ffmpegInstance as any;\n const config = ffmpegConfig!;\n const batchIndex = ffmpegSegments.length;\n\n postProgress(40 + batchIndex * 5, `Encoding batch ${batchIndex + 1}…`);\n\n // Write frames to virtual filesystem\n for (const frame of ffmpegFrames) {\n const paddedIndex = String(frame.index).padStart(6, '0');\n await ffmpeg.writeFile(`frame_${paddedIndex}.png`, frame.data);\n }\n\n const firstIndex = ffmpegFrames[0].index;\n const segmentName = `segment_${batchIndex}.mp4`;\n\n // Encode batch to MP4 segment\n await ffmpeg.exec([\n '-framerate',\n String(config.fps),\n '-start_number',\n String(firstIndex),\n '-i',\n `frame_%06d.png`,\n '-frames:v',\n String(ffmpegFrames.length),\n '-c:v',\n 'libx264',\n '-pix_fmt',\n 'yuv420p',\n ...ffmpegVideoQualityArgs(config.quality),\n segmentName,\n ]);\n\n // Read segment and clean up frames\n const segmentData = await ffmpeg.readFile(segmentName);\n ffmpegSegments.push(segmentData);\n\n // Clean up frame files\n for (const frame of ffmpegFrames) {\n const paddedIndex = String(frame.index).padStart(6, '0');\n await ffmpeg.deleteFile(`frame_${paddedIndex}.png`);\n }\n await ffmpeg.deleteFile(segmentName);\n\n // Free batch memory\n ffmpegFrames = [];\n}\n\nasync function finalizeFfmpeg() {\n if (!ffmpegInstance || cancelled) return;\n\n // Encode any remaining frames\n await encodeFfmpegBatch();\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const ffmpeg = ffmpegInstance as any;\n\n postProgress(85, 'Concatenating segments…');\n\n let finalData: Uint8Array;\n\n if (ffmpegSegments.length === 1) {\n // Single segment — no concat needed\n finalData = ffmpegSegments[0];\n } else if (ffmpegSegments.length > 1) {\n // Write segments and concat list\n const concatList: string[] = [];\n for (let i = 0; i < ffmpegSegments.length; i++) {\n const name = `seg_${i}.mp4`;\n await ffmpeg.writeFile(name, ffmpegSegments[i]);\n concatList.push(`file '${name}'`);\n }\n await ffmpeg.writeFile('concat.txt', new TextEncoder().encode(concatList.join('\\n')));\n\n await ffmpeg.exec([\n '-f',\n 'concat',\n '-safe',\n '0',\n '-i',\n 'concat.txt',\n '-c',\n 'copy',\n 'output.mp4',\n ]);\n\n finalData = await ffmpeg.readFile('output.mp4');\n\n // Clean up\n for (let i = 0; i < ffmpegSegments.length; i++) {\n await ffmpeg.deleteFile(`seg_${i}.mp4`);\n }\n await ffmpeg.deleteFile('concat.txt');\n await ffmpeg.deleteFile('output.mp4');\n } else {\n postError('No frames were encoded');\n return;\n }\n\n postProgress(95, 'Preparing download…');\n\n // Convert Uint8Array to a transferable ArrayBuffer\n const sliced = new Uint8Array(finalData).buffer as ArrayBuffer;\n post({ type: 'complete', data: sliced, size: sliced.byteLength }, [sliced]);\n\n // Clean up\n ffmpegInstance = null;\n ffmpegSegments.length = 0;\n}\n\n// ── Message Handler ────────────────────────────────────────────────\n\nself.onmessage = async (event: MessageEvent<MainToWorkerMessage>) => {\n const msg = event.data;\n\n try {\n switch (msg.type) {\n case 'init': {\n cancelled = false;\n totalFramesReceived = 0;\n\n if (await supportsWebCodecsH264(msg)) {\n backend = 'webcodecs';\n initWebCodecs(msg);\n } else if (hasSharedArrayBuffer()) {\n backend = 'ffmpeg-wasm';\n await initFfmpegWasm(msg);\n } else {\n postError(\n 'No video encoding support available. ' +\n 'WebCodecs H.264 is unavailable (typical on Linux Chromium without proprietary codecs) ' +\n 'and ffmpeg.wasm requires SharedArrayBuffer (Cross-Origin-Isolation headers).',\n );\n return;\n }\n\n post({ type: 'capabilities', backend });\n postProgress(0, 'Encoder ready');\n break;\n }\n\n case 'frame': {\n if (backend === 'webcodecs') {\n await encodeFrameWebCodecs(msg);\n } else if (backend === 'ffmpeg-wasm') {\n await encodeFrameFfmpeg(msg);\n }\n break;\n }\n\n case 'finalize': {\n if (backend === 'webcodecs') {\n await finalizeWebCodecs();\n } else if (backend === 'ffmpeg-wasm') {\n await finalizeFfmpeg();\n }\n break;\n }\n\n case 'cancel': {\n cancelled = true;\n if (videoEncoder && videoEncoder.state !== 'closed') {\n videoEncoder.close();\n videoEncoder = null;\n }\n muxer = null;\n ffmpegInstance = null;\n ffmpegFrames = [];\n ffmpegSegments.length = 0;\n break;\n }\n }\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n postError(message);\n }\n};\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/workers/encode.worker.ts"],"sourcesContent":["/**\n * Video Encoding Web Worker\n *\n * Encodes video frames to MP4 using one of two backends:\n * - **WebCodecs** (preferred): Streaming H.264 encoding via VideoEncoder + mp4-muxer.\n * Each frame is encoded as it arrives — minimal memory footprint.\n * - **ffmpeg.wasm** (fallback): Batch encoding in ~10-second segments.\n * Used when WebCodecs is unavailable (older browsers).\n *\n * Frames arrive as ImageBitmap (zero-copy transfer from main thread).\n */\n\nimport type {\n MainToWorkerMessage,\n WorkerToMainMessage,\n InitMessage,\n FrameMessage,\n} from './workerTypes.js';\nimport { bitrateForQuality, ffmpegVideoQualityArgs } from '@bendyline/squisq-video';\nimport type { FfmpegWasmLoadConfig } from '@bendyline/squisq-video';\n\nimport { createMp4Muxer, type Mp4MuxerHandle } from '../mp4Mux.js';\n\n// ── State ──────────────────────────────────────────────────────────\n\nlet backend: 'webcodecs' | 'ffmpeg-wasm' | null = null;\nlet cancelled = false;\n\n// WebCodecs state\nlet videoEncoder: VideoEncoder | null = null;\nlet muxer: Mp4MuxerHandle | null = null;\n\n// ffmpeg.wasm state\nlet ffmpegInstance: unknown = null;\nlet ffmpegFrames: Array<{ data: Uint8Array; index: number }> = [];\nlet ffmpegConfig: InitMessage | null = null;\n\n// Frame tracking\nlet totalFramesReceived = 0;\n\n// ── Helpers ────────────────────────────────────────────────────────\n\nfunction post(msg: WorkerToMainMessage, transfer?: Transferable[]) {\n self.postMessage(msg, { transfer: transfer ?? [] });\n}\n\nfunction postProgress(percent: number, phase: string) {\n post({ type: 'progress', percent, phase });\n}\n\nfunction postError(message: string) {\n post({ type: 'error', message });\n}\n\nfunction disposeFfmpeg(): void {\n const instance = ffmpegInstance as { terminate?: () => void } | null;\n try {\n instance?.terminate?.();\n } finally {\n ffmpegInstance = null;\n }\n}\n\n// ── Feature Detection ──────────────────────────────────────────────\n\nfunction hasWebCodecs(): boolean {\n return typeof VideoEncoder !== 'undefined' && typeof VideoFrame !== 'undefined';\n}\n\nfunction hasSharedArrayBuffer(): boolean {\n return typeof SharedArrayBuffer !== 'undefined';\n}\n\n/**\n * Check whether the browser's WebCodecs implementation actually supports\n * the H.264 Baseline profile we use. Linux Chromium ships without the\n * proprietary H.264 encoder, so `VideoEncoder` exists but `configure()`\n * for `avc1.*` fails asynchronously.\n */\nasync function supportsWebCodecsH264(config: InitMessage): Promise<boolean> {\n if (!hasWebCodecs()) return false;\n try {\n const support = await VideoEncoder.isConfigSupported({\n codec: 'avc1.42001f',\n width: config.width,\n height: config.height,\n bitrate: bitrateForQuality(config.quality, config.width, config.height),\n framerate: config.fps,\n });\n return support.supported === true;\n } catch {\n return false;\n }\n}\n\n// ── WebCodecs Backend ──────────────────────────────────────────────\n\nfunction initWebCodecs(config: InitMessage) {\n muxer = createMp4Muxer({\n width: config.width,\n height: config.height,\n fps: config.fps,\n });\n\n videoEncoder = new VideoEncoder({\n output(chunk, meta) {\n if (cancelled) return;\n muxer!.addVideoChunk(chunk, meta ?? undefined);\n },\n error(err) {\n postError(`WebCodecs encoder error: ${err.message}`);\n },\n });\n\n // Deliberate profile split from mainThreadEncoder (avc1.640028, High@4.0):\n // this worker is the max-compatibility fallback path, so it uses H.264\n // Baseline (avc1.42001f) — widest decoder support at a small quality cost.\n videoEncoder.configure({\n codec: 'avc1.42001f',\n width: config.width,\n height: config.height,\n bitrate: bitrateForQuality(config.quality, config.width, config.height),\n framerate: config.fps,\n });\n}\n\nasync function encodeFrameWebCodecs(msg: FrameMessage) {\n if (!videoEncoder || cancelled) {\n msg.bitmap.close();\n return;\n }\n\n const frame = new VideoFrame(msg.bitmap, { timestamp: msg.timestamp });\n const keyFrame = msg.frameIndex % 30 === 0; // Key frame every 30 frames\n videoEncoder.encode(frame, { keyFrame });\n frame.close();\n msg.bitmap.close();\n totalFramesReceived++;\n\n postProgress(\n Math.round((totalFramesReceived / (totalFramesReceived + 1)) * 50),\n `Encoding frame ${totalFramesReceived}`,\n );\n}\n\nasync function finalizeWebCodecs() {\n if (!videoEncoder || !muxer || cancelled) return;\n\n postProgress(50, 'Flushing encoder…');\n await videoEncoder.flush();\n videoEncoder.close();\n videoEncoder = null;\n\n postProgress(90, 'Finalizing MP4…');\n const buffer = muxer.finalize();\n muxer = null;\n\n post({ type: 'complete', data: buffer, size: buffer.byteLength }, [buffer]);\n}\n\n// ── ffmpeg.wasm Backend ────────────────────────────────────────────\n\nasync function initFfmpegWasm(config: InitMessage) {\n ffmpegConfig = config;\n ffmpegFrames = [];\n ffmpegSegments.length = 0;\n totalFramesReceived = 0;\n\n // Lazy-load ffmpeg.wasm\n let ffmpeg: {\n load: (config?: FfmpegWasmLoadConfig) => Promise<unknown>;\n terminate: () => void;\n } | null = null;\n try {\n const { FFmpeg } = await import('@ffmpeg/ffmpeg');\n ffmpeg = new FFmpeg();\n await ffmpeg.load({\n ...config.ffmpegWasm,\n classWorkerURL:\n config.ffmpegWasm?.classWorkerURL ??\n new URL('./ffmpeg.class-worker.js', import.meta.url).href,\n });\n ffmpegInstance = ffmpeg;\n } catch (err: unknown) {\n ffmpeg?.terminate();\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(\n `Failed to load ffmpeg.wasm: ${message}. ` +\n 'Ensure @ffmpeg/ffmpeg and @ffmpeg/util are installed and ' +\n 'Cross-Origin-Isolation headers (COOP/COEP) are set.',\n );\n }\n}\n\nasync function encodeFrameFfmpeg(msg: FrameMessage) {\n if (cancelled) {\n msg.bitmap.close();\n return;\n }\n\n // Convert ImageBitmap to PNG bytes via OffscreenCanvas\n const canvas = new OffscreenCanvas(msg.bitmap.width, msg.bitmap.height);\n const ctx = canvas.getContext('2d');\n if (!ctx) {\n msg.bitmap.close();\n postError('Failed to create OffscreenCanvas 2D context');\n return;\n }\n ctx.drawImage(msg.bitmap, 0, 0);\n msg.bitmap.close();\n\n const blob = await canvas.convertToBlob({ type: 'image/png' });\n const arrayBuffer = await blob.arrayBuffer();\n ffmpegFrames.push({ data: new Uint8Array(arrayBuffer), index: msg.frameIndex });\n totalFramesReceived++;\n\n postProgress(\n Math.round((totalFramesReceived / (totalFramesReceived + 1)) * 40),\n `Collecting frame ${totalFramesReceived}`,\n );\n\n // Batch encode every ~10 seconds worth of frames\n const batchSize = (ffmpegConfig?.fps ?? 30) * 10;\n if (ffmpegFrames.length >= batchSize) {\n await encodeFfmpegBatch();\n }\n}\n\n/** Segments already encoded as MP4 data. */\nconst ffmpegSegments: Uint8Array[] = [];\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nasync function execOrThrow(ffmpeg: any, args: string[], operation: string): Promise<void> {\n const exitCode = await ffmpeg.exec(args);\n if (exitCode !== 0) {\n throw new Error(`${operation} failed with ffmpeg exit code ${exitCode}`);\n }\n}\n\nasync function encodeFfmpegBatch() {\n if (!ffmpegInstance || ffmpegFrames.length === 0 || cancelled) return;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const ffmpeg = ffmpegInstance as any;\n const config = ffmpegConfig!;\n const batchIndex = ffmpegSegments.length;\n\n postProgress(40 + batchIndex * 5, `Encoding batch ${batchIndex + 1}…`);\n\n // Write frames to virtual filesystem\n for (const frame of ffmpegFrames) {\n const paddedIndex = String(frame.index).padStart(6, '0');\n await ffmpeg.writeFile(`frame_${paddedIndex}.png`, frame.data);\n }\n\n const firstIndex = ffmpegFrames[0].index;\n const segmentName = `segment_${batchIndex}.mp4`;\n\n // Encode batch to MP4 segment\n await execOrThrow(\n ffmpeg,\n [\n '-framerate',\n String(config.fps),\n '-start_number',\n String(firstIndex),\n '-i',\n `frame_%06d.png`,\n '-frames:v',\n String(ffmpegFrames.length),\n '-c:v',\n 'libx264',\n '-pix_fmt',\n 'yuv420p',\n ...ffmpegVideoQualityArgs(config.quality),\n segmentName,\n ],\n `Encoding video segment ${batchIndex + 1}`,\n );\n\n // Read segment and clean up frames\n const segmentData = await ffmpeg.readFile(segmentName);\n ffmpegSegments.push(segmentData);\n\n // Clean up frame files\n for (const frame of ffmpegFrames) {\n const paddedIndex = String(frame.index).padStart(6, '0');\n await ffmpeg.deleteFile(`frame_${paddedIndex}.png`);\n }\n await ffmpeg.deleteFile(segmentName);\n\n // Free batch memory\n ffmpegFrames = [];\n}\n\nasync function finalizeFfmpeg() {\n if (!ffmpegInstance || cancelled) return;\n\n // Encode any remaining frames\n await encodeFfmpegBatch();\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const ffmpeg = ffmpegInstance as any;\n\n postProgress(85, 'Concatenating segments…');\n\n let finalData: Uint8Array;\n\n if (ffmpegSegments.length === 1) {\n // Single segment — no concat needed\n finalData = ffmpegSegments[0];\n } else if (ffmpegSegments.length > 1) {\n // Write segments and concat list\n const concatList: string[] = [];\n for (let i = 0; i < ffmpegSegments.length; i++) {\n const name = `seg_${i}.mp4`;\n await ffmpeg.writeFile(name, ffmpegSegments[i]);\n concatList.push(`file '${name}'`);\n }\n await ffmpeg.writeFile('concat.txt', new TextEncoder().encode(concatList.join('\\n')));\n\n await execOrThrow(\n ffmpeg,\n ['-f', 'concat', '-safe', '0', '-i', 'concat.txt', '-c', 'copy', 'output.mp4'],\n 'Concatenating video segments',\n );\n\n finalData = await ffmpeg.readFile('output.mp4');\n\n // Clean up\n for (let i = 0; i < ffmpegSegments.length; i++) {\n await ffmpeg.deleteFile(`seg_${i}.mp4`);\n }\n await ffmpeg.deleteFile('concat.txt');\n await ffmpeg.deleteFile('output.mp4');\n } else {\n postError('No frames were encoded');\n return;\n }\n\n postProgress(95, 'Preparing download…');\n\n // Convert Uint8Array to a transferable ArrayBuffer\n const sliced = new Uint8Array(finalData).buffer as ArrayBuffer;\n post({ type: 'complete', data: sliced, size: sliced.byteLength }, [sliced]);\n\n // Clean up\n disposeFfmpeg();\n ffmpegSegments.length = 0;\n}\n\n// ── Message Handler ────────────────────────────────────────────────\n\nasync function handleMessage(msg: MainToWorkerMessage): Promise<void> {\n switch (msg.type) {\n case 'init': {\n cancelled = false;\n totalFramesReceived = 0;\n\n if (await supportsWebCodecsH264(msg)) {\n backend = 'webcodecs';\n initWebCodecs(msg);\n } else if (hasSharedArrayBuffer()) {\n backend = 'ffmpeg-wasm';\n await initFfmpegWasm(msg);\n } else {\n throw new Error(\n 'No video encoding support available. ' +\n 'WebCodecs H.264 is unavailable (typical on Linux Chromium without proprietary codecs) ' +\n 'and ffmpeg.wasm requires SharedArrayBuffer (Cross-Origin-Isolation headers).',\n );\n }\n\n post({ type: 'capabilities', backend });\n postProgress(0, 'Encoder ready');\n break;\n }\n\n case 'frame': {\n if (cancelled) {\n msg.bitmap.close();\n return;\n }\n if (backend === 'webcodecs') {\n await encodeFrameWebCodecs(msg);\n } else if (backend === 'ffmpeg-wasm') {\n await encodeFrameFfmpeg(msg);\n } else {\n msg.bitmap.close();\n throw new Error('Encoder received a frame before it was initialized');\n }\n post({ type: 'frame-complete', frameIndex: msg.frameIndex });\n break;\n }\n\n case 'finalize': {\n if (backend === 'webcodecs') {\n await finalizeWebCodecs();\n } else if (backend === 'ffmpeg-wasm') {\n await finalizeFfmpeg();\n } else {\n throw new Error('Encoder cannot finalize before it is initialized');\n }\n break;\n }\n\n case 'cancel': {\n if (videoEncoder && videoEncoder.state !== 'closed') {\n videoEncoder.close();\n videoEncoder = null;\n }\n muxer = null;\n disposeFfmpeg();\n ffmpegFrames = [];\n ffmpegSegments.length = 0;\n backend = null;\n break;\n }\n }\n}\n\n// MessageEvent callbacks are not implicitly serialized: an async frame handler\n// can still be running when finalize arrives. Keep one queue so frame ownership,\n// ffmpeg's virtual filesystem, and finalization have a single ordering boundary.\nlet messageQueue: Promise<void> = Promise.resolve();\n\nself.onmessage = (event: MessageEvent<MainToWorkerMessage>) => {\n const msg = event.data;\n // Cancellation must become visible to the currently-running operation without\n // waiting behind all queued frames. Cleanup itself remains ordered.\n if (msg.type === 'cancel') cancelled = true;\n\n messageQueue = messageQueue\n .then(() => handleMessage(msg))\n .catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n postError(message);\n });\n};\n"],"mappings":";;;;;AAkBA,SAAS,mBAAmB,8BAA8B;AAO1D,IAAI,UAA8C;AAClD,IAAI,YAAY;AAGhB,IAAI,eAAoC;AACxC,IAAI,QAA+B;AAGnC,IAAI,iBAA0B;AAC9B,IAAI,eAA2D,CAAC;AAChE,IAAI,eAAmC;AAGvC,IAAI,sBAAsB;AAI1B,SAAS,KAAK,KAA0B,UAA2B;AACjE,OAAK,YAAY,KAAK,EAAE,UAAU,YAAY,CAAC,EAAE,CAAC;AACpD;AAEA,SAAS,aAAa,SAAiB,OAAe;AACpD,OAAK,EAAE,MAAM,YAAY,SAAS,MAAM,CAAC;AAC3C;AAEA,SAAS,UAAU,SAAiB;AAClC,OAAK,EAAE,MAAM,SAAS,QAAQ,CAAC;AACjC;AAEA,SAAS,gBAAsB;AAC7B,QAAM,WAAW;AACjB,MAAI;AACF,cAAU,YAAY;AAAA,EACxB,UAAE;AACA,qBAAiB;AAAA,EACnB;AACF;AAIA,SAAS,eAAwB;AAC/B,SAAO,OAAO,iBAAiB,eAAe,OAAO,eAAe;AACtE;AAEA,SAAS,uBAAgC;AACvC,SAAO,OAAO,sBAAsB;AACtC;AAQA,eAAe,sBAAsB,QAAuC;AAC1E,MAAI,CAAC,aAAa,EAAG,QAAO;AAC5B,MAAI;AACF,UAAM,UAAU,MAAM,aAAa,kBAAkB;AAAA,MACnD,OAAO;AAAA,MACP,OAAO,OAAO;AAAA,MACd,QAAQ,OAAO;AAAA,MACf,SAAS,kBAAkB,OAAO,SAAS,OAAO,OAAO,OAAO,MAAM;AAAA,MACtE,WAAW,OAAO;AAAA,IACpB,CAAC;AACD,WAAO,QAAQ,cAAc;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAIA,SAAS,cAAc,QAAqB;AAC1C,UAAQ,eAAe;AAAA,IACrB,OAAO,OAAO;AAAA,IACd,QAAQ,OAAO;AAAA,IACf,KAAK,OAAO;AAAA,EACd,CAAC;AAED,iBAAe,IAAI,aAAa;AAAA,IAC9B,OAAO,OAAO,MAAM;AAClB,UAAI,UAAW;AACf,YAAO,cAAc,OAAO,QAAQ,MAAS;AAAA,IAC/C;AAAA,IACA,MAAM,KAAK;AACT,gBAAU,4BAA4B,IAAI,OAAO,EAAE;AAAA,IACrD;AAAA,EACF,CAAC;AAKD,eAAa,UAAU;AAAA,IACrB,OAAO;AAAA,IACP,OAAO,OAAO;AAAA,IACd,QAAQ,OAAO;AAAA,IACf,SAAS,kBAAkB,OAAO,SAAS,OAAO,OAAO,OAAO,MAAM;AAAA,IACtE,WAAW,OAAO;AAAA,EACpB,CAAC;AACH;AAEA,eAAe,qBAAqB,KAAmB;AACrD,MAAI,CAAC,gBAAgB,WAAW;AAC9B,QAAI,OAAO,MAAM;AACjB;AAAA,EACF;AAEA,QAAM,QAAQ,IAAI,WAAW,IAAI,QAAQ,EAAE,WAAW,IAAI,UAAU,CAAC;AACrE,QAAM,WAAW,IAAI,aAAa,OAAO;AACzC,eAAa,OAAO,OAAO,EAAE,SAAS,CAAC;AACvC,QAAM,MAAM;AACZ,MAAI,OAAO,MAAM;AACjB;AAEA;AAAA,IACE,KAAK,MAAO,uBAAuB,sBAAsB,KAAM,EAAE;AAAA,IACjE,kBAAkB,mBAAmB;AAAA,EACvC;AACF;AAEA,eAAe,oBAAoB;AACjC,MAAI,CAAC,gBAAgB,CAAC,SAAS,UAAW;AAE1C,eAAa,IAAI,wBAAmB;AACpC,QAAM,aAAa,MAAM;AACzB,eAAa,MAAM;AACnB,iBAAe;AAEf,eAAa,IAAI,sBAAiB;AAClC,QAAM,SAAS,MAAM,SAAS;AAC9B,UAAQ;AAER,OAAK,EAAE,MAAM,YAAY,MAAM,QAAQ,MAAM,OAAO,WAAW,GAAG,CAAC,MAAM,CAAC;AAC5E;AAIA,eAAe,eAAe,QAAqB;AACjD,iBAAe;AACf,iBAAe,CAAC;AAChB,iBAAe,SAAS;AACxB,wBAAsB;AAGtB,MAAI,SAGO;AACX,MAAI;AACF,UAAM,EAAE,OAAO,IAAI,MAAM,OAAO,gBAAgB;AAChD,aAAS,IAAI,OAAO;AACpB,UAAM,OAAO,KAAK;AAAA,MAChB,GAAG,OAAO;AAAA,MACV,gBACE,OAAO,YAAY,kBACnB,IAAI,IAAI,4BAA4B,YAAY,GAAG,EAAE;AAAA,IACzD,CAAC;AACD,qBAAiB;AAAA,EACnB,SAAS,KAAc;AACrB,YAAQ,UAAU;AAClB,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,UAAM,IAAI;AAAA,MACR,+BAA+B,OAAO;AAAA,IAGxC;AAAA,EACF;AACF;AAEA,eAAe,kBAAkB,KAAmB;AAClD,MAAI,WAAW;AACb,QAAI,OAAO,MAAM;AACjB;AAAA,EACF;AAGA,QAAM,SAAS,IAAI,gBAAgB,IAAI,OAAO,OAAO,IAAI,OAAO,MAAM;AACtE,QAAM,MAAM,OAAO,WAAW,IAAI;AAClC,MAAI,CAAC,KAAK;AACR,QAAI,OAAO,MAAM;AACjB,cAAU,6CAA6C;AACvD;AAAA,EACF;AACA,MAAI,UAAU,IAAI,QAAQ,GAAG,CAAC;AAC9B,MAAI,OAAO,MAAM;AAEjB,QAAM,OAAO,MAAM,OAAO,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7D,QAAM,cAAc,MAAM,KAAK,YAAY;AAC3C,eAAa,KAAK,EAAE,MAAM,IAAI,WAAW,WAAW,GAAG,OAAO,IAAI,WAAW,CAAC;AAC9E;AAEA;AAAA,IACE,KAAK,MAAO,uBAAuB,sBAAsB,KAAM,EAAE;AAAA,IACjE,oBAAoB,mBAAmB;AAAA,EACzC;AAGA,QAAM,aAAa,cAAc,OAAO,MAAM;AAC9C,MAAI,aAAa,UAAU,WAAW;AACpC,UAAM,kBAAkB;AAAA,EAC1B;AACF;AAGA,IAAM,iBAA+B,CAAC;AAGtC,eAAe,YAAY,QAAa,MAAgB,WAAkC;AACxF,QAAM,WAAW,MAAM,OAAO,KAAK,IAAI;AACvC,MAAI,aAAa,GAAG;AAClB,UAAM,IAAI,MAAM,GAAG,SAAS,iCAAiC,QAAQ,EAAE;AAAA,EACzE;AACF;AAEA,eAAe,oBAAoB;AACjC,MAAI,CAAC,kBAAkB,aAAa,WAAW,KAAK,UAAW;AAG/D,QAAM,SAAS;AACf,QAAM,SAAS;AACf,QAAM,aAAa,eAAe;AAElC,eAAa,KAAK,aAAa,GAAG,kBAAkB,aAAa,CAAC,QAAG;AAGrE,aAAW,SAAS,cAAc;AAChC,UAAM,cAAc,OAAO,MAAM,KAAK,EAAE,SAAS,GAAG,GAAG;AACvD,UAAM,OAAO,UAAU,SAAS,WAAW,QAAQ,MAAM,IAAI;AAAA,EAC/D;AAEA,QAAM,aAAa,aAAa,CAAC,EAAE;AACnC,QAAM,cAAc,WAAW,UAAU;AAGzC,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,MACE;AAAA,MACA,OAAO,OAAO,GAAG;AAAA,MACjB;AAAA,MACA,OAAO,UAAU;AAAA,MACjB;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,aAAa,MAAM;AAAA,MAC1B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,GAAG,uBAAuB,OAAO,OAAO;AAAA,MACxC;AAAA,IACF;AAAA,IACA,0BAA0B,aAAa,CAAC;AAAA,EAC1C;AAGA,QAAM,cAAc,MAAM,OAAO,SAAS,WAAW;AACrD,iBAAe,KAAK,WAAW;AAG/B,aAAW,SAAS,cAAc;AAChC,UAAM,cAAc,OAAO,MAAM,KAAK,EAAE,SAAS,GAAG,GAAG;AACvD,UAAM,OAAO,WAAW,SAAS,WAAW,MAAM;AAAA,EACpD;AACA,QAAM,OAAO,WAAW,WAAW;AAGnC,iBAAe,CAAC;AAClB;AAEA,eAAe,iBAAiB;AAC9B,MAAI,CAAC,kBAAkB,UAAW;AAGlC,QAAM,kBAAkB;AAGxB,QAAM,SAAS;AAEf,eAAa,IAAI,8BAAyB;AAE1C,MAAI;AAEJ,MAAI,eAAe,WAAW,GAAG;AAE/B,gBAAY,eAAe,CAAC;AAAA,EAC9B,WAAW,eAAe,SAAS,GAAG;AAEpC,UAAM,aAAuB,CAAC;AAC9B,aAAS,IAAI,GAAG,IAAI,eAAe,QAAQ,KAAK;AAC9C,YAAM,OAAO,OAAO,CAAC;AACrB,YAAM,OAAO,UAAU,MAAM,eAAe,CAAC,CAAC;AAC9C,iBAAW,KAAK,SAAS,IAAI,GAAG;AAAA,IAClC;AACA,UAAM,OAAO,UAAU,cAAc,IAAI,YAAY,EAAE,OAAO,WAAW,KAAK,IAAI,CAAC,CAAC;AAEpF,UAAM;AAAA,MACJ;AAAA,MACA,CAAC,MAAM,UAAU,SAAS,KAAK,MAAM,cAAc,MAAM,QAAQ,YAAY;AAAA,MAC7E;AAAA,IACF;AAEA,gBAAY,MAAM,OAAO,SAAS,YAAY;AAG9C,aAAS,IAAI,GAAG,IAAI,eAAe,QAAQ,KAAK;AAC9C,YAAM,OAAO,WAAW,OAAO,CAAC,MAAM;AAAA,IACxC;AACA,UAAM,OAAO,WAAW,YAAY;AACpC,UAAM,OAAO,WAAW,YAAY;AAAA,EACtC,OAAO;AACL,cAAU,wBAAwB;AAClC;AAAA,EACF;AAEA,eAAa,IAAI,0BAAqB;AAGtC,QAAM,SAAS,IAAI,WAAW,SAAS,EAAE;AACzC,OAAK,EAAE,MAAM,YAAY,MAAM,QAAQ,MAAM,OAAO,WAAW,GAAG,CAAC,MAAM,CAAC;AAG1E,gBAAc;AACd,iBAAe,SAAS;AAC1B;AAIA,eAAe,cAAc,KAAyC;AACpE,UAAQ,IAAI,MAAM;AAAA,IAChB,KAAK,QAAQ;AACX,kBAAY;AACZ,4BAAsB;AAEtB,UAAI,MAAM,sBAAsB,GAAG,GAAG;AACpC,kBAAU;AACV,sBAAc,GAAG;AAAA,MACnB,WAAW,qBAAqB,GAAG;AACjC,kBAAU;AACV,cAAM,eAAe,GAAG;AAAA,MAC1B,OAAO;AACL,cAAM,IAAI;AAAA,UACR;AAAA,QAGF;AAAA,MACF;AAEA,WAAK,EAAE,MAAM,gBAAgB,QAAQ,CAAC;AACtC,mBAAa,GAAG,eAAe;AAC/B;AAAA,IACF;AAAA,IAEA,KAAK,SAAS;AACZ,UAAI,WAAW;AACb,YAAI,OAAO,MAAM;AACjB;AAAA,MACF;AACA,UAAI,YAAY,aAAa;AAC3B,cAAM,qBAAqB,GAAG;AAAA,MAChC,WAAW,YAAY,eAAe;AACpC,cAAM,kBAAkB,GAAG;AAAA,MAC7B,OAAO;AACL,YAAI,OAAO,MAAM;AACjB,cAAM,IAAI,MAAM,oDAAoD;AAAA,MACtE;AACA,WAAK,EAAE,MAAM,kBAAkB,YAAY,IAAI,WAAW,CAAC;AAC3D;AAAA,IACF;AAAA,IAEA,KAAK,YAAY;AACf,UAAI,YAAY,aAAa;AAC3B,cAAM,kBAAkB;AAAA,MAC1B,WAAW,YAAY,eAAe;AACpC,cAAM,eAAe;AAAA,MACvB,OAAO;AACL,cAAM,IAAI,MAAM,kDAAkD;AAAA,MACpE;AACA;AAAA,IACF;AAAA,IAEA,KAAK,UAAU;AACb,UAAI,gBAAgB,aAAa,UAAU,UAAU;AACnD,qBAAa,MAAM;AACnB,uBAAe;AAAA,MACjB;AACA,cAAQ;AACR,oBAAc;AACd,qBAAe,CAAC;AAChB,qBAAe,SAAS;AACxB,gBAAU;AACV;AAAA,IACF;AAAA,EACF;AACF;AAKA,IAAI,eAA8B,QAAQ,QAAQ;AAElD,KAAK,YAAY,CAAC,UAA6C;AAC7D,QAAM,MAAM,MAAM;AAGlB,MAAI,IAAI,SAAS,SAAU,aAAY;AAEvC,iBAAe,aACZ,KAAK,MAAM,cAAc,GAAG,CAAC,EAC7B,MAAM,CAAC,QAAiB;AACvB,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,cAAU,OAAO;AAAA,EACnB,CAAC;AACL;","names":[]}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// ../../node_modules/@ffmpeg/ffmpeg/dist/esm/const.js
|
|
2
|
+
var CORE_VERSION = "0.12.9";
|
|
3
|
+
var CORE_URL = `https://unpkg.com/@ffmpeg/core@${CORE_VERSION}/dist/umd/ffmpeg-core.js`;
|
|
4
|
+
var FFMessageType;
|
|
5
|
+
(function(FFMessageType2) {
|
|
6
|
+
FFMessageType2["LOAD"] = "LOAD";
|
|
7
|
+
FFMessageType2["EXEC"] = "EXEC";
|
|
8
|
+
FFMessageType2["FFPROBE"] = "FFPROBE";
|
|
9
|
+
FFMessageType2["WRITE_FILE"] = "WRITE_FILE";
|
|
10
|
+
FFMessageType2["READ_FILE"] = "READ_FILE";
|
|
11
|
+
FFMessageType2["DELETE_FILE"] = "DELETE_FILE";
|
|
12
|
+
FFMessageType2["RENAME"] = "RENAME";
|
|
13
|
+
FFMessageType2["CREATE_DIR"] = "CREATE_DIR";
|
|
14
|
+
FFMessageType2["LIST_DIR"] = "LIST_DIR";
|
|
15
|
+
FFMessageType2["DELETE_DIR"] = "DELETE_DIR";
|
|
16
|
+
FFMessageType2["ERROR"] = "ERROR";
|
|
17
|
+
FFMessageType2["DOWNLOAD"] = "DOWNLOAD";
|
|
18
|
+
FFMessageType2["PROGRESS"] = "PROGRESS";
|
|
19
|
+
FFMessageType2["LOG"] = "LOG";
|
|
20
|
+
FFMessageType2["MOUNT"] = "MOUNT";
|
|
21
|
+
FFMessageType2["UNMOUNT"] = "UNMOUNT";
|
|
22
|
+
})(FFMessageType || (FFMessageType = {}));
|
|
23
|
+
|
|
24
|
+
// ../../node_modules/@ffmpeg/ffmpeg/dist/esm/errors.js
|
|
25
|
+
var ERROR_UNKNOWN_MESSAGE_TYPE = new Error("unknown message type");
|
|
26
|
+
var ERROR_NOT_LOADED = new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first");
|
|
27
|
+
var ERROR_TERMINATED = new Error("called FFmpeg.terminate()");
|
|
28
|
+
var ERROR_IMPORT_FAILURE = new Error("failed to import ffmpeg-core.js");
|
|
29
|
+
|
|
30
|
+
// ../../node_modules/@ffmpeg/ffmpeg/dist/esm/worker.js
|
|
31
|
+
var ffmpeg;
|
|
32
|
+
var load = async ({ coreURL: _coreURL, wasmURL: _wasmURL, workerURL: _workerURL }) => {
|
|
33
|
+
const first = !ffmpeg;
|
|
34
|
+
try {
|
|
35
|
+
if (!_coreURL)
|
|
36
|
+
_coreURL = CORE_URL;
|
|
37
|
+
importScripts(_coreURL);
|
|
38
|
+
} catch {
|
|
39
|
+
if (!_coreURL || _coreURL === CORE_URL)
|
|
40
|
+
_coreURL = CORE_URL.replace("/umd/", "/esm/");
|
|
41
|
+
self.createFFmpegCore = (await import(
|
|
42
|
+
/* @vite-ignore */
|
|
43
|
+
_coreURL
|
|
44
|
+
)).default;
|
|
45
|
+
if (!self.createFFmpegCore) {
|
|
46
|
+
throw ERROR_IMPORT_FAILURE;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const coreURL = _coreURL;
|
|
50
|
+
const wasmURL = _wasmURL ? _wasmURL : _coreURL.replace(/.js$/g, ".wasm");
|
|
51
|
+
const workerURL = _workerURL ? _workerURL : _coreURL.replace(/.js$/g, ".worker.js");
|
|
52
|
+
ffmpeg = await self.createFFmpegCore({
|
|
53
|
+
// Fix `Overload resolution failed.` when using multi-threaded ffmpeg-core.
|
|
54
|
+
// Encoded wasmURL and workerURL in the URL as a hack to fix locateFile issue.
|
|
55
|
+
mainScriptUrlOrBlob: `${coreURL}#${btoa(JSON.stringify({ wasmURL, workerURL }))}`
|
|
56
|
+
});
|
|
57
|
+
ffmpeg.setLogger((data) => self.postMessage({ type: FFMessageType.LOG, data }));
|
|
58
|
+
ffmpeg.setProgress((data) => self.postMessage({
|
|
59
|
+
type: FFMessageType.PROGRESS,
|
|
60
|
+
data
|
|
61
|
+
}));
|
|
62
|
+
return first;
|
|
63
|
+
};
|
|
64
|
+
var exec = ({ args, timeout = -1 }) => {
|
|
65
|
+
ffmpeg.setTimeout(timeout);
|
|
66
|
+
ffmpeg.exec(...args);
|
|
67
|
+
const ret = ffmpeg.ret;
|
|
68
|
+
ffmpeg.reset();
|
|
69
|
+
return ret;
|
|
70
|
+
};
|
|
71
|
+
var ffprobe = ({ args, timeout = -1 }) => {
|
|
72
|
+
ffmpeg.setTimeout(timeout);
|
|
73
|
+
ffmpeg.ffprobe(...args);
|
|
74
|
+
const ret = ffmpeg.ret;
|
|
75
|
+
ffmpeg.reset();
|
|
76
|
+
return ret;
|
|
77
|
+
};
|
|
78
|
+
var writeFile = ({ path, data }) => {
|
|
79
|
+
ffmpeg.FS.writeFile(path, data);
|
|
80
|
+
return true;
|
|
81
|
+
};
|
|
82
|
+
var readFile = ({ path, encoding }) => ffmpeg.FS.readFile(path, { encoding });
|
|
83
|
+
var deleteFile = ({ path }) => {
|
|
84
|
+
ffmpeg.FS.unlink(path);
|
|
85
|
+
return true;
|
|
86
|
+
};
|
|
87
|
+
var rename = ({ oldPath, newPath }) => {
|
|
88
|
+
ffmpeg.FS.rename(oldPath, newPath);
|
|
89
|
+
return true;
|
|
90
|
+
};
|
|
91
|
+
var createDir = ({ path }) => {
|
|
92
|
+
ffmpeg.FS.mkdir(path);
|
|
93
|
+
return true;
|
|
94
|
+
};
|
|
95
|
+
var listDir = ({ path }) => {
|
|
96
|
+
const names = ffmpeg.FS.readdir(path);
|
|
97
|
+
const nodes = [];
|
|
98
|
+
for (const name of names) {
|
|
99
|
+
const stat = ffmpeg.FS.stat(`${path}/${name}`);
|
|
100
|
+
const isDir = ffmpeg.FS.isDir(stat.mode);
|
|
101
|
+
nodes.push({ name, isDir });
|
|
102
|
+
}
|
|
103
|
+
return nodes;
|
|
104
|
+
};
|
|
105
|
+
var deleteDir = ({ path }) => {
|
|
106
|
+
ffmpeg.FS.rmdir(path);
|
|
107
|
+
return true;
|
|
108
|
+
};
|
|
109
|
+
var mount = ({ fsType, options, mountPoint }) => {
|
|
110
|
+
const str = fsType;
|
|
111
|
+
const fs = ffmpeg.FS.filesystems[str];
|
|
112
|
+
if (!fs)
|
|
113
|
+
return false;
|
|
114
|
+
ffmpeg.FS.mount(fs, options, mountPoint);
|
|
115
|
+
return true;
|
|
116
|
+
};
|
|
117
|
+
var unmount = ({ mountPoint }) => {
|
|
118
|
+
ffmpeg.FS.unmount(mountPoint);
|
|
119
|
+
return true;
|
|
120
|
+
};
|
|
121
|
+
self.onmessage = async ({ data: { id, type, data: _data } }) => {
|
|
122
|
+
const trans = [];
|
|
123
|
+
let data;
|
|
124
|
+
try {
|
|
125
|
+
if (type !== FFMessageType.LOAD && !ffmpeg)
|
|
126
|
+
throw ERROR_NOT_LOADED;
|
|
127
|
+
switch (type) {
|
|
128
|
+
case FFMessageType.LOAD:
|
|
129
|
+
data = await load(_data);
|
|
130
|
+
break;
|
|
131
|
+
case FFMessageType.EXEC:
|
|
132
|
+
data = exec(_data);
|
|
133
|
+
break;
|
|
134
|
+
case FFMessageType.FFPROBE:
|
|
135
|
+
data = ffprobe(_data);
|
|
136
|
+
break;
|
|
137
|
+
case FFMessageType.WRITE_FILE:
|
|
138
|
+
data = writeFile(_data);
|
|
139
|
+
break;
|
|
140
|
+
case FFMessageType.READ_FILE:
|
|
141
|
+
data = readFile(_data);
|
|
142
|
+
break;
|
|
143
|
+
case FFMessageType.DELETE_FILE:
|
|
144
|
+
data = deleteFile(_data);
|
|
145
|
+
break;
|
|
146
|
+
case FFMessageType.RENAME:
|
|
147
|
+
data = rename(_data);
|
|
148
|
+
break;
|
|
149
|
+
case FFMessageType.CREATE_DIR:
|
|
150
|
+
data = createDir(_data);
|
|
151
|
+
break;
|
|
152
|
+
case FFMessageType.LIST_DIR:
|
|
153
|
+
data = listDir(_data);
|
|
154
|
+
break;
|
|
155
|
+
case FFMessageType.DELETE_DIR:
|
|
156
|
+
data = deleteDir(_data);
|
|
157
|
+
break;
|
|
158
|
+
case FFMessageType.MOUNT:
|
|
159
|
+
data = mount(_data);
|
|
160
|
+
break;
|
|
161
|
+
case FFMessageType.UNMOUNT:
|
|
162
|
+
data = unmount(_data);
|
|
163
|
+
break;
|
|
164
|
+
default:
|
|
165
|
+
throw ERROR_UNKNOWN_MESSAGE_TYPE;
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
self.postMessage({
|
|
169
|
+
id,
|
|
170
|
+
type: FFMessageType.ERROR,
|
|
171
|
+
data: e.toString()
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (data instanceof Uint8Array) {
|
|
176
|
+
trans.push(data.buffer);
|
|
177
|
+
}
|
|
178
|
+
self.postMessage({ id, type, data }, trans);
|
|
179
|
+
};
|
|
180
|
+
//# sourceMappingURL=ffmpeg.class-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../node_modules/@ffmpeg/ffmpeg/dist/esm/const.js","../../../../node_modules/@ffmpeg/ffmpeg/dist/esm/errors.js","../../../../node_modules/@ffmpeg/ffmpeg/dist/esm/worker.js"],"sourcesContent":["export const MIME_TYPE_JAVASCRIPT = \"text/javascript\";\nexport const MIME_TYPE_WASM = \"application/wasm\";\nexport const CORE_VERSION = \"0.12.9\";\nexport const CORE_URL = `https://unpkg.com/@ffmpeg/core@${CORE_VERSION}/dist/umd/ffmpeg-core.js`;\nexport var FFMessageType;\n(function (FFMessageType) {\n FFMessageType[\"LOAD\"] = \"LOAD\";\n FFMessageType[\"EXEC\"] = \"EXEC\";\n FFMessageType[\"FFPROBE\"] = \"FFPROBE\";\n FFMessageType[\"WRITE_FILE\"] = \"WRITE_FILE\";\n FFMessageType[\"READ_FILE\"] = \"READ_FILE\";\n FFMessageType[\"DELETE_FILE\"] = \"DELETE_FILE\";\n FFMessageType[\"RENAME\"] = \"RENAME\";\n FFMessageType[\"CREATE_DIR\"] = \"CREATE_DIR\";\n FFMessageType[\"LIST_DIR\"] = \"LIST_DIR\";\n FFMessageType[\"DELETE_DIR\"] = \"DELETE_DIR\";\n FFMessageType[\"ERROR\"] = \"ERROR\";\n FFMessageType[\"DOWNLOAD\"] = \"DOWNLOAD\";\n FFMessageType[\"PROGRESS\"] = \"PROGRESS\";\n FFMessageType[\"LOG\"] = \"LOG\";\n FFMessageType[\"MOUNT\"] = \"MOUNT\";\n FFMessageType[\"UNMOUNT\"] = \"UNMOUNT\";\n})(FFMessageType || (FFMessageType = {}));\n","export const ERROR_UNKNOWN_MESSAGE_TYPE = new Error(\"unknown message type\");\nexport const ERROR_NOT_LOADED = new Error(\"ffmpeg is not loaded, call `await ffmpeg.load()` first\");\nexport const ERROR_TERMINATED = new Error(\"called FFmpeg.terminate()\");\nexport const ERROR_IMPORT_FAILURE = new Error(\"failed to import ffmpeg-core.js\");\n","/// <reference no-default-lib=\"true\" />\n/// <reference lib=\"esnext\" />\n/// <reference lib=\"webworker\" />\nimport { CORE_URL, FFMessageType } from \"./const.js\";\nimport { ERROR_UNKNOWN_MESSAGE_TYPE, ERROR_NOT_LOADED, ERROR_IMPORT_FAILURE, } from \"./errors.js\";\nlet ffmpeg;\nconst load = async ({ coreURL: _coreURL, wasmURL: _wasmURL, workerURL: _workerURL, }) => {\n const first = !ffmpeg;\n try {\n if (!_coreURL)\n _coreURL = CORE_URL;\n // when web worker type is `classic`.\n importScripts(_coreURL);\n }\n catch {\n if (!_coreURL || _coreURL === CORE_URL)\n _coreURL = CORE_URL.replace('/umd/', '/esm/');\n // when web worker type is `module`.\n self.createFFmpegCore = (await import(\n /* @vite-ignore */ _coreURL)).default;\n if (!self.createFFmpegCore) {\n throw ERROR_IMPORT_FAILURE;\n }\n }\n const coreURL = _coreURL;\n const wasmURL = _wasmURL ? _wasmURL : _coreURL.replace(/.js$/g, \".wasm\");\n const workerURL = _workerURL\n ? _workerURL\n : _coreURL.replace(/.js$/g, \".worker.js\");\n ffmpeg = await self.createFFmpegCore({\n // Fix `Overload resolution failed.` when using multi-threaded ffmpeg-core.\n // Encoded wasmURL and workerURL in the URL as a hack to fix locateFile issue.\n mainScriptUrlOrBlob: `${coreURL}#${btoa(JSON.stringify({ wasmURL, workerURL }))}`,\n });\n ffmpeg.setLogger((data) => self.postMessage({ type: FFMessageType.LOG, data }));\n ffmpeg.setProgress((data) => self.postMessage({\n type: FFMessageType.PROGRESS,\n data,\n }));\n return first;\n};\nconst exec = ({ args, timeout = -1 }) => {\n ffmpeg.setTimeout(timeout);\n ffmpeg.exec(...args);\n const ret = ffmpeg.ret;\n ffmpeg.reset();\n return ret;\n};\nconst ffprobe = ({ args, timeout = -1 }) => {\n ffmpeg.setTimeout(timeout);\n ffmpeg.ffprobe(...args);\n const ret = ffmpeg.ret;\n ffmpeg.reset();\n return ret;\n};\nconst writeFile = ({ path, data }) => {\n ffmpeg.FS.writeFile(path, data);\n return true;\n};\nconst readFile = ({ path, encoding }) => ffmpeg.FS.readFile(path, { encoding });\n// TODO: check if deletion works.\nconst deleteFile = ({ path }) => {\n ffmpeg.FS.unlink(path);\n return true;\n};\nconst rename = ({ oldPath, newPath }) => {\n ffmpeg.FS.rename(oldPath, newPath);\n return true;\n};\n// TODO: check if creation works.\nconst createDir = ({ path }) => {\n ffmpeg.FS.mkdir(path);\n return true;\n};\nconst listDir = ({ path }) => {\n const names = ffmpeg.FS.readdir(path);\n const nodes = [];\n for (const name of names) {\n const stat = ffmpeg.FS.stat(`${path}/${name}`);\n const isDir = ffmpeg.FS.isDir(stat.mode);\n nodes.push({ name, isDir });\n }\n return nodes;\n};\n// TODO: check if deletion works.\nconst deleteDir = ({ path }) => {\n ffmpeg.FS.rmdir(path);\n return true;\n};\nconst mount = ({ fsType, options, mountPoint }) => {\n const str = fsType;\n const fs = ffmpeg.FS.filesystems[str];\n if (!fs)\n return false;\n ffmpeg.FS.mount(fs, options, mountPoint);\n return true;\n};\nconst unmount = ({ mountPoint }) => {\n ffmpeg.FS.unmount(mountPoint);\n return true;\n};\nself.onmessage = async ({ data: { id, type, data: _data }, }) => {\n const trans = [];\n let data;\n try {\n if (type !== FFMessageType.LOAD && !ffmpeg)\n throw ERROR_NOT_LOADED; // eslint-disable-line\n switch (type) {\n case FFMessageType.LOAD:\n data = await load(_data);\n break;\n case FFMessageType.EXEC:\n data = exec(_data);\n break;\n case FFMessageType.FFPROBE:\n data = ffprobe(_data);\n break;\n case FFMessageType.WRITE_FILE:\n data = writeFile(_data);\n break;\n case FFMessageType.READ_FILE:\n data = readFile(_data);\n break;\n case FFMessageType.DELETE_FILE:\n data = deleteFile(_data);\n break;\n case FFMessageType.RENAME:\n data = rename(_data);\n break;\n case FFMessageType.CREATE_DIR:\n data = createDir(_data);\n break;\n case FFMessageType.LIST_DIR:\n data = listDir(_data);\n break;\n case FFMessageType.DELETE_DIR:\n data = deleteDir(_data);\n break;\n case FFMessageType.MOUNT:\n data = mount(_data);\n break;\n case FFMessageType.UNMOUNT:\n data = unmount(_data);\n break;\n default:\n throw ERROR_UNKNOWN_MESSAGE_TYPE;\n }\n }\n catch (e) {\n self.postMessage({\n id,\n type: FFMessageType.ERROR,\n data: e.toString(),\n });\n return;\n }\n if (data instanceof Uint8Array) {\n trans.push(data.buffer);\n }\n self.postMessage({ id, type, data }, trans);\n};\n"],"mappings":"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","names":["FFMessageType"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/squisq-video-react",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "React components for browser-based
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "React components for browser-based MP4 and animated-GIF export of Squisq documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bendyline",
|
|
7
7
|
"repository": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"squisq",
|
|
15
15
|
"video",
|
|
16
16
|
"mp4",
|
|
17
|
+
"gif",
|
|
17
18
|
"react",
|
|
18
19
|
"webcodecs",
|
|
19
20
|
"export"
|
|
@@ -45,9 +46,10 @@
|
|
|
45
46
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
|
-
"@bendyline/squisq": "
|
|
49
|
-
"@bendyline/squisq-video": "
|
|
50
|
-
"@bendyline/squisq-react": "
|
|
49
|
+
"@bendyline/squisq": "2.0.1",
|
|
50
|
+
"@bendyline/squisq-video": "2.0.1",
|
|
51
|
+
"@bendyline/squisq-react": "2.0.1",
|
|
52
|
+
"@ffmpeg/core": "0.12.9",
|
|
51
53
|
"@ffmpeg/ffmpeg": "0.12.15",
|
|
52
54
|
"@ffmpeg/util": "0.12.2",
|
|
53
55
|
"html2canvas": "1.4.1",
|
|
@@ -28,11 +28,14 @@ export interface VideoExportButtonProps {
|
|
|
28
28
|
/** Pre-collected audio map */
|
|
29
29
|
audio?: Map<string, ArrayBuffer>;
|
|
30
30
|
/**
|
|
31
|
-
* Seeds the modal's initial export settings and is merged
|
|
32
|
-
* config passed to the export hook. Forwarded to
|
|
31
|
+
* Seeds the modal's initial output format and export settings and is merged
|
|
32
|
+
* as a base into the config passed to the export hook. Forwarded to
|
|
33
|
+
* {@link VideoExportModal}.
|
|
33
34
|
*/
|
|
34
35
|
defaultConfig?: Partial<VideoExportConfig>;
|
|
35
|
-
/**
|
|
36
|
+
/** Visual color scheme forwarded to the portaled modal. Defaults to light. */
|
|
37
|
+
colorScheme?: 'light' | 'dark';
|
|
38
|
+
/** Button label (defaults to "Export Video", or "Export GIF" for a GIF default config) */
|
|
36
39
|
label?: string;
|
|
37
40
|
/** Additional inline styles for the button */
|
|
38
41
|
style?: React.CSSProperties;
|
|
@@ -47,11 +50,14 @@ export function VideoExportButton({
|
|
|
47
50
|
images,
|
|
48
51
|
audio,
|
|
49
52
|
defaultConfig,
|
|
50
|
-
|
|
53
|
+
colorScheme,
|
|
54
|
+
label,
|
|
51
55
|
style,
|
|
52
56
|
disabled,
|
|
53
57
|
}: VideoExportButtonProps) {
|
|
54
58
|
const [showModal, setShowModal] = useState(false);
|
|
59
|
+
const resolvedLabel =
|
|
60
|
+
label ?? (defaultConfig?.outputFormat === 'gif' ? 'Export GIF' : 'Export Video');
|
|
55
61
|
|
|
56
62
|
const handleOpen = useCallback(() => setShowModal(true), []);
|
|
57
63
|
const handleClose = useCallback(() => setShowModal(false), []);
|
|
@@ -59,7 +65,7 @@ export function VideoExportButton({
|
|
|
59
65
|
return (
|
|
60
66
|
<>
|
|
61
67
|
<button onClick={handleOpen} disabled={disabled} style={style}>
|
|
62
|
-
{
|
|
68
|
+
{resolvedLabel}
|
|
63
69
|
</button>
|
|
64
70
|
|
|
65
71
|
{showModal &&
|
|
@@ -71,6 +77,7 @@ export function VideoExportButton({
|
|
|
71
77
|
images={images}
|
|
72
78
|
audio={audio}
|
|
73
79
|
defaultConfig={defaultConfig}
|
|
80
|
+
colorScheme={colorScheme}
|
|
74
81
|
onClose={handleClose}
|
|
75
82
|
/>,
|
|
76
83
|
document.body,
|