@bendyline/squisq-video-react 1.1.1 → 1.2.0
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/index.d.ts +171 -19
- package/dist/index.js +921 -21
- package/dist/index.js.map +1 -1
- package/dist/workers/encode.worker.d.ts +2 -13
- package/dist/workers/encode.worker.js +258 -304
- package/dist/workers/encode.worker.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/encodeParams.test.ts +33 -0
- package/src/workers/encode.worker.ts +1 -13
- package/src/workers/encodeParams.ts +24 -0
- package/dist/VideoExportButton.d.ts +0 -28
- package/dist/VideoExportButton.d.ts.map +0 -1
- package/dist/VideoExportButton.js +0 -18
- package/dist/VideoExportButton.js.map +0 -1
- package/dist/VideoExportModal.d.ts +0 -26
- package/dist/VideoExportModal.d.ts.map +0 -1
- package/dist/VideoExportModal.js +0 -164
- package/dist/VideoExportModal.js.map +0 -1
- package/dist/hooks/useFrameCapture.d.ts +0 -27
- package/dist/hooks/useFrameCapture.d.ts.map +0 -1
- package/dist/hooks/useFrameCapture.js +0 -211
- package/dist/hooks/useFrameCapture.js.map +0 -1
- package/dist/hooks/useVideoExport.d.ts +0 -75
- package/dist/hooks/useVideoExport.d.ts.map +0 -1
- package/dist/hooks/useVideoExport.js +0 -266
- package/dist/hooks/useVideoExport.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/mainThreadEncoder.d.ts +0 -44
- package/dist/mainThreadEncoder.d.ts.map +0 -1
- package/dist/mainThreadEncoder.js +0 -123
- package/dist/mainThreadEncoder.js.map +0 -1
- package/dist/mp4Mux.d.ts +0 -22
- package/dist/mp4Mux.d.ts.map +0 -1
- package/dist/mp4Mux.js +0 -32
- package/dist/mp4Mux.js.map +0 -1
- package/dist/workerEncoder.d.ts +0 -17
- package/dist/workerEncoder.d.ts.map +0 -1
- package/dist/workerEncoder.js +0 -103
- package/dist/workerEncoder.js.map +0 -1
- package/dist/workers/encode.worker.d.ts.map +0 -1
- package/dist/workers/workerTypes.d.ts +0 -63
- package/dist/workers/workerTypes.d.ts.map +0 -1
- package/dist/workers/workerTypes.js +0 -8
- package/dist/workers/workerTypes.js.map +0 -1
|
@@ -1,341 +1,295 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
import {
|
|
2
|
+
createMp4Muxer
|
|
3
|
+
} from "../chunk-LZYWP4HB.js";
|
|
4
|
+
|
|
5
|
+
// src/workers/encodeParams.ts
|
|
6
|
+
function bitrateForQuality(quality, width, height) {
|
|
7
|
+
const pixels = width * height;
|
|
8
|
+
const baseBitrate = pixels * 4;
|
|
9
|
+
switch (quality) {
|
|
10
|
+
case "draft":
|
|
11
|
+
return Math.round(baseBitrate * 0.5);
|
|
12
|
+
case "high":
|
|
13
|
+
return Math.round(baseBitrate * 2);
|
|
14
|
+
default:
|
|
15
|
+
return baseBitrate;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/workers/encode.worker.ts
|
|
20
|
+
var backend = null;
|
|
21
|
+
var cancelled = false;
|
|
22
|
+
var videoEncoder = null;
|
|
23
|
+
var muxer = null;
|
|
24
|
+
var ffmpegInstance = null;
|
|
25
|
+
var ffmpegFrames = [];
|
|
26
|
+
var ffmpegConfig = null;
|
|
27
|
+
var totalFramesReceived = 0;
|
|
28
|
+
var _totalFramesEncoded = 0;
|
|
27
29
|
function post(msg, transfer) {
|
|
28
|
-
|
|
30
|
+
self.postMessage(msg, { transfer: transfer ?? [] });
|
|
29
31
|
}
|
|
30
32
|
function postProgress(percent, phase) {
|
|
31
|
-
|
|
33
|
+
post({ type: "progress", percent, phase });
|
|
32
34
|
}
|
|
33
35
|
function postError(message) {
|
|
34
|
-
|
|
36
|
+
post({ type: "error", message });
|
|
35
37
|
}
|
|
36
|
-
// ── Feature Detection ──────────────────────────────────────────────
|
|
37
38
|
function hasWebCodecs() {
|
|
38
|
-
|
|
39
|
+
return typeof VideoEncoder !== "undefined" && typeof VideoFrame !== "undefined";
|
|
39
40
|
}
|
|
40
41
|
function hasSharedArrayBuffer() {
|
|
41
|
-
|
|
42
|
+
return typeof SharedArrayBuffer !== "undefined";
|
|
42
43
|
}
|
|
43
|
-
/**
|
|
44
|
-
* Check whether the browser's WebCodecs implementation actually supports
|
|
45
|
-
* the H.264 Baseline profile we use. Linux Chromium ships without the
|
|
46
|
-
* proprietary H.264 encoder, so `VideoEncoder` exists but `configure()`
|
|
47
|
-
* for `avc1.*` fails asynchronously.
|
|
48
|
-
*/
|
|
49
44
|
async function supportsWebCodecsH264(config) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
framerate: config.fps,
|
|
59
|
-
});
|
|
60
|
-
return support.supported === true;
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
// ── WebCodecs Backend ──────────────────────────────────────────────
|
|
67
|
-
function initWebCodecs(config) {
|
|
68
|
-
muxer = createMp4Muxer({
|
|
69
|
-
width: config.width,
|
|
70
|
-
height: config.height,
|
|
71
|
-
fps: config.fps,
|
|
72
|
-
});
|
|
73
|
-
videoEncoder = new VideoEncoder({
|
|
74
|
-
output(chunk, meta) {
|
|
75
|
-
if (cancelled)
|
|
76
|
-
return;
|
|
77
|
-
muxer.addVideoChunk(chunk, meta ?? undefined);
|
|
78
|
-
_totalFramesEncoded++;
|
|
79
|
-
},
|
|
80
|
-
error(err) {
|
|
81
|
-
postError(`WebCodecs encoder error: ${err.message}`);
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
// Use H.264 Baseline for maximum compatibility
|
|
85
|
-
videoEncoder.configure({
|
|
86
|
-
codec: 'avc1.42001f',
|
|
87
|
-
width: config.width,
|
|
88
|
-
height: config.height,
|
|
89
|
-
bitrate: bitrateForQuality(config.quality, config.width, config.height),
|
|
90
|
-
framerate: config.fps,
|
|
45
|
+
if (!hasWebCodecs()) return false;
|
|
46
|
+
try {
|
|
47
|
+
const support = await VideoEncoder.isConfigSupported({
|
|
48
|
+
codec: "avc1.42001f",
|
|
49
|
+
width: config.width,
|
|
50
|
+
height: config.height,
|
|
51
|
+
bitrate: bitrateForQuality(config.quality, config.width, config.height),
|
|
52
|
+
framerate: config.fps
|
|
91
53
|
});
|
|
54
|
+
return support.supported === true;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
92
58
|
}
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
59
|
+
function initWebCodecs(config) {
|
|
60
|
+
muxer = createMp4Muxer({
|
|
61
|
+
width: config.width,
|
|
62
|
+
height: config.height,
|
|
63
|
+
fps: config.fps
|
|
64
|
+
});
|
|
65
|
+
videoEncoder = new VideoEncoder({
|
|
66
|
+
output(chunk, meta) {
|
|
67
|
+
if (cancelled) return;
|
|
68
|
+
muxer.addVideoChunk(chunk, meta ?? void 0);
|
|
69
|
+
_totalFramesEncoded++;
|
|
70
|
+
},
|
|
71
|
+
error(err) {
|
|
72
|
+
postError(`WebCodecs encoder error: ${err.message}`);
|
|
103
73
|
}
|
|
74
|
+
});
|
|
75
|
+
videoEncoder.configure({
|
|
76
|
+
codec: "avc1.42001f",
|
|
77
|
+
width: config.width,
|
|
78
|
+
height: config.height,
|
|
79
|
+
bitrate: bitrateForQuality(config.quality, config.width, config.height),
|
|
80
|
+
framerate: config.fps
|
|
81
|
+
});
|
|
104
82
|
}
|
|
105
83
|
async function encodeFrameWebCodecs(msg) {
|
|
106
|
-
|
|
107
|
-
msg.bitmap.close();
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
const frame = new VideoFrame(msg.bitmap, { timestamp: msg.timestamp });
|
|
111
|
-
const keyFrame = msg.frameIndex % 30 === 0; // Key frame every 30 frames
|
|
112
|
-
videoEncoder.encode(frame, { keyFrame });
|
|
113
|
-
frame.close();
|
|
84
|
+
if (!videoEncoder || cancelled) {
|
|
114
85
|
msg.bitmap.close();
|
|
115
|
-
|
|
116
|
-
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const frame = new VideoFrame(msg.bitmap, { timestamp: msg.timestamp });
|
|
89
|
+
const keyFrame = msg.frameIndex % 30 === 0;
|
|
90
|
+
videoEncoder.encode(frame, { keyFrame });
|
|
91
|
+
frame.close();
|
|
92
|
+
msg.bitmap.close();
|
|
93
|
+
totalFramesReceived++;
|
|
94
|
+
postProgress(
|
|
95
|
+
Math.round(totalFramesReceived / (totalFramesReceived + 1) * 50),
|
|
96
|
+
`Encoding frame ${totalFramesReceived}`
|
|
97
|
+
);
|
|
117
98
|
}
|
|
118
99
|
async function finalizeWebCodecs() {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
post({ type: 'complete', data: buffer, size: buffer.byteLength }, [buffer]);
|
|
100
|
+
if (!videoEncoder || !muxer || cancelled) return;
|
|
101
|
+
postProgress(50, "Flushing encoder\u2026");
|
|
102
|
+
await videoEncoder.flush();
|
|
103
|
+
videoEncoder.close();
|
|
104
|
+
videoEncoder = null;
|
|
105
|
+
postProgress(90, "Finalizing MP4\u2026");
|
|
106
|
+
const buffer = muxer.finalize();
|
|
107
|
+
muxer = null;
|
|
108
|
+
post({ type: "complete", data: buffer, size: buffer.byteLength }, [buffer]);
|
|
129
109
|
}
|
|
130
|
-
// ── ffmpeg.wasm Backend ────────────────────────────────────────────
|
|
131
110
|
async function initFfmpegWasm(config) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
'Cross-Origin-Isolation headers (COOP/COEP) are set.');
|
|
147
|
-
}
|
|
111
|
+
ffmpegConfig = config;
|
|
112
|
+
ffmpegFrames = [];
|
|
113
|
+
totalFramesReceived = 0;
|
|
114
|
+
try {
|
|
115
|
+
const { FFmpeg } = await import("@ffmpeg/ffmpeg");
|
|
116
|
+
const ffmpeg = new FFmpeg();
|
|
117
|
+
await ffmpeg.load();
|
|
118
|
+
ffmpegInstance = ffmpeg;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
121
|
+
postError(
|
|
122
|
+
`Failed to load ffmpeg.wasm: ${message}. Ensure @ffmpeg/ffmpeg and @ffmpeg/util are installed and Cross-Origin-Isolation headers (COOP/COEP) are set.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
148
125
|
}
|
|
149
126
|
async function encodeFrameFfmpeg(msg) {
|
|
150
|
-
|
|
151
|
-
msg.bitmap.close();
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
// Convert ImageBitmap to PNG bytes via OffscreenCanvas
|
|
155
|
-
const canvas = new OffscreenCanvas(msg.bitmap.width, msg.bitmap.height);
|
|
156
|
-
const ctx = canvas.getContext('2d');
|
|
157
|
-
if (!ctx) {
|
|
158
|
-
msg.bitmap.close();
|
|
159
|
-
postError('Failed to create OffscreenCanvas 2D context');
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
ctx.drawImage(msg.bitmap, 0, 0);
|
|
127
|
+
if (cancelled) {
|
|
163
128
|
msg.bitmap.close();
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const canvas = new OffscreenCanvas(msg.bitmap.width, msg.bitmap.height);
|
|
132
|
+
const ctx = canvas.getContext("2d");
|
|
133
|
+
if (!ctx) {
|
|
134
|
+
msg.bitmap.close();
|
|
135
|
+
postError("Failed to create OffscreenCanvas 2D context");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
ctx.drawImage(msg.bitmap, 0, 0);
|
|
139
|
+
msg.bitmap.close();
|
|
140
|
+
const blob = await canvas.convertToBlob({ type: "image/png" });
|
|
141
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
142
|
+
ffmpegFrames.push({ data: new Uint8Array(arrayBuffer), index: msg.frameIndex });
|
|
143
|
+
totalFramesReceived++;
|
|
144
|
+
postProgress(
|
|
145
|
+
Math.round(totalFramesReceived / (totalFramesReceived + 1) * 40),
|
|
146
|
+
`Collecting frame ${totalFramesReceived}`
|
|
147
|
+
);
|
|
148
|
+
const batchSize = (ffmpegConfig?.fps ?? 30) * 10;
|
|
149
|
+
if (ffmpegFrames.length >= batchSize) {
|
|
150
|
+
await encodeFfmpegBatch();
|
|
151
|
+
}
|
|
174
152
|
}
|
|
175
|
-
|
|
176
|
-
const ffmpegSegments = [];
|
|
153
|
+
var ffmpegSegments = [];
|
|
177
154
|
async function encodeFfmpegBatch() {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
155
|
+
if (!ffmpegInstance || ffmpegFrames.length === 0 || cancelled) return;
|
|
156
|
+
const ffmpeg = ffmpegInstance;
|
|
157
|
+
const config = ffmpegConfig;
|
|
158
|
+
const batchIndex = ffmpegSegments.length;
|
|
159
|
+
postProgress(40 + batchIndex * 5, `Encoding batch ${batchIndex + 1}\u2026`);
|
|
160
|
+
for (const frame of ffmpegFrames) {
|
|
161
|
+
const paddedIndex = String(frame.index).padStart(6, "0");
|
|
162
|
+
await ffmpeg.writeFile(`frame_${paddedIndex}.png`, frame.data);
|
|
163
|
+
}
|
|
164
|
+
const firstIndex = ffmpegFrames[0].index;
|
|
165
|
+
const segmentName = `segment_${batchIndex}.mp4`;
|
|
166
|
+
await ffmpeg.exec([
|
|
167
|
+
"-framerate",
|
|
168
|
+
String(config.fps),
|
|
169
|
+
"-start_number",
|
|
170
|
+
String(firstIndex),
|
|
171
|
+
"-i",
|
|
172
|
+
`frame_%06d.png`,
|
|
173
|
+
"-frames:v",
|
|
174
|
+
String(ffmpegFrames.length),
|
|
175
|
+
"-c:v",
|
|
176
|
+
"libx264",
|
|
177
|
+
"-pix_fmt",
|
|
178
|
+
"yuv420p",
|
|
179
|
+
"-preset",
|
|
180
|
+
config.quality === "draft" ? "ultrafast" : config.quality === "high" ? "slow" : "medium",
|
|
181
|
+
"-crf",
|
|
182
|
+
config.quality === "draft" ? "28" : config.quality === "high" ? "18" : "23",
|
|
183
|
+
segmentName
|
|
184
|
+
]);
|
|
185
|
+
const segmentData = await ffmpeg.readFile(segmentName);
|
|
186
|
+
ffmpegSegments.push(segmentData);
|
|
187
|
+
for (const frame of ffmpegFrames) {
|
|
188
|
+
const paddedIndex = String(frame.index).padStart(6, "0");
|
|
189
|
+
await ffmpeg.deleteFile(`frame_${paddedIndex}.png`);
|
|
190
|
+
}
|
|
191
|
+
await ffmpeg.deleteFile(segmentName);
|
|
192
|
+
ffmpegFrames = [];
|
|
193
|
+
}
|
|
194
|
+
async function finalizeFfmpeg() {
|
|
195
|
+
if (!ffmpegInstance || cancelled) return;
|
|
196
|
+
await encodeFfmpegBatch();
|
|
197
|
+
const ffmpeg = ffmpegInstance;
|
|
198
|
+
postProgress(85, "Concatenating segments\u2026");
|
|
199
|
+
let finalData;
|
|
200
|
+
if (ffmpegSegments.length === 1) {
|
|
201
|
+
finalData = ffmpegSegments[0];
|
|
202
|
+
} else if (ffmpegSegments.length > 1) {
|
|
203
|
+
const concatList = [];
|
|
204
|
+
for (let i = 0; i < ffmpegSegments.length; i++) {
|
|
205
|
+
const name = `seg_${i}.mp4`;
|
|
206
|
+
await ffmpeg.writeFile(name, ffmpegSegments[i]);
|
|
207
|
+
concatList.push(`file '${name}'`);
|
|
189
208
|
}
|
|
190
|
-
|
|
191
|
-
const segmentName = `segment_${batchIndex}.mp4`;
|
|
192
|
-
// Encode batch to MP4 segment
|
|
209
|
+
await ffmpeg.writeFile("concat.txt", new TextEncoder().encode(concatList.join("\n")));
|
|
193
210
|
await ffmpeg.exec([
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
'libx264',
|
|
204
|
-
'-pix_fmt',
|
|
205
|
-
'yuv420p',
|
|
206
|
-
'-preset',
|
|
207
|
-
config.quality === 'draft' ? 'ultrafast' : config.quality === 'high' ? 'slow' : 'medium',
|
|
208
|
-
'-crf',
|
|
209
|
-
config.quality === 'draft' ? '28' : config.quality === 'high' ? '18' : '23',
|
|
210
|
-
segmentName,
|
|
211
|
+
"-f",
|
|
212
|
+
"concat",
|
|
213
|
+
"-safe",
|
|
214
|
+
"0",
|
|
215
|
+
"-i",
|
|
216
|
+
"concat.txt",
|
|
217
|
+
"-c",
|
|
218
|
+
"copy",
|
|
219
|
+
"output.mp4"
|
|
211
220
|
]);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// Clean up frame files
|
|
216
|
-
for (const frame of ffmpegFrames) {
|
|
217
|
-
const paddedIndex = String(frame.index).padStart(6, '0');
|
|
218
|
-
await ffmpeg.deleteFile(`frame_${paddedIndex}.png`);
|
|
221
|
+
finalData = await ffmpeg.readFile("output.mp4");
|
|
222
|
+
for (let i = 0; i < ffmpegSegments.length; i++) {
|
|
223
|
+
await ffmpeg.deleteFile(`seg_${i}.mp4`);
|
|
219
224
|
}
|
|
220
|
-
await ffmpeg.deleteFile(
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
await ffmpeg.deleteFile("concat.txt");
|
|
226
|
+
await ffmpeg.deleteFile("output.mp4");
|
|
227
|
+
} else {
|
|
228
|
+
postError("No frames were encoded");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
postProgress(95, "Preparing download\u2026");
|
|
232
|
+
const sliced = new Uint8Array(finalData).buffer;
|
|
233
|
+
post({ type: "complete", data: sliced, size: sliced.byteLength }, [sliced]);
|
|
234
|
+
ffmpegInstance = null;
|
|
235
|
+
ffmpegSegments.length = 0;
|
|
223
236
|
}
|
|
224
|
-
async
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
concatList.push(`file '${name}'`);
|
|
237
|
+
self.onmessage = async (event) => {
|
|
238
|
+
const msg = event.data;
|
|
239
|
+
try {
|
|
240
|
+
switch (msg.type) {
|
|
241
|
+
case "init": {
|
|
242
|
+
cancelled = false;
|
|
243
|
+
totalFramesReceived = 0;
|
|
244
|
+
_totalFramesEncoded = 0;
|
|
245
|
+
if (await supportsWebCodecsH264(msg)) {
|
|
246
|
+
backend = "webcodecs";
|
|
247
|
+
initWebCodecs(msg);
|
|
248
|
+
} else if (hasSharedArrayBuffer()) {
|
|
249
|
+
backend = "ffmpeg-wasm";
|
|
250
|
+
await initFfmpegWasm(msg);
|
|
251
|
+
} else {
|
|
252
|
+
postError(
|
|
253
|
+
"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)."
|
|
254
|
+
);
|
|
255
|
+
return;
|
|
244
256
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
'copy',
|
|
255
|
-
'output.mp4',
|
|
256
|
-
]);
|
|
257
|
-
finalData = await ffmpeg.readFile('output.mp4');
|
|
258
|
-
// Clean up
|
|
259
|
-
for (let i = 0; i < ffmpegSegments.length; i++) {
|
|
260
|
-
await ffmpeg.deleteFile(`seg_${i}.mp4`);
|
|
257
|
+
post({ type: "capabilities", backend });
|
|
258
|
+
postProgress(0, "Encoder ready");
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
case "frame": {
|
|
262
|
+
if (backend === "webcodecs") {
|
|
263
|
+
await encodeFrameWebCodecs(msg);
|
|
264
|
+
} else if (backend === "ffmpeg-wasm") {
|
|
265
|
+
await encodeFrameFfmpeg(msg);
|
|
261
266
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
postProgress(95, 'Preparing download…');
|
|
270
|
-
// Convert Uint8Array to a transferable ArrayBuffer
|
|
271
|
-
const sliced = new Uint8Array(finalData).buffer;
|
|
272
|
-
post({ type: 'complete', data: sliced, size: sliced.byteLength }, [sliced]);
|
|
273
|
-
// Clean up
|
|
274
|
-
ffmpegInstance = null;
|
|
275
|
-
ffmpegSegments.length = 0;
|
|
276
|
-
}
|
|
277
|
-
// ── Message Handler ────────────────────────────────────────────────
|
|
278
|
-
self.onmessage = async (event) => {
|
|
279
|
-
const msg = event.data;
|
|
280
|
-
try {
|
|
281
|
-
switch (msg.type) {
|
|
282
|
-
case 'init': {
|
|
283
|
-
cancelled = false;
|
|
284
|
-
totalFramesReceived = 0;
|
|
285
|
-
_totalFramesEncoded = 0;
|
|
286
|
-
if (await supportsWebCodecsH264(msg)) {
|
|
287
|
-
backend = 'webcodecs';
|
|
288
|
-
initWebCodecs(msg);
|
|
289
|
-
}
|
|
290
|
-
else if (hasSharedArrayBuffer()) {
|
|
291
|
-
backend = 'ffmpeg-wasm';
|
|
292
|
-
await initFfmpegWasm(msg);
|
|
293
|
-
}
|
|
294
|
-
else {
|
|
295
|
-
postError('No video encoding support available. ' +
|
|
296
|
-
'WebCodecs H.264 is unavailable (typical on Linux Chromium without proprietary codecs) ' +
|
|
297
|
-
'and ffmpeg.wasm requires SharedArrayBuffer (Cross-Origin-Isolation headers).');
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
post({ type: 'capabilities', backend });
|
|
301
|
-
postProgress(0, 'Encoder ready');
|
|
302
|
-
break;
|
|
303
|
-
}
|
|
304
|
-
case 'frame': {
|
|
305
|
-
if (backend === 'webcodecs') {
|
|
306
|
-
await encodeFrameWebCodecs(msg);
|
|
307
|
-
}
|
|
308
|
-
else if (backend === 'ffmpeg-wasm') {
|
|
309
|
-
await encodeFrameFfmpeg(msg);
|
|
310
|
-
}
|
|
311
|
-
break;
|
|
312
|
-
}
|
|
313
|
-
case 'finalize': {
|
|
314
|
-
if (backend === 'webcodecs') {
|
|
315
|
-
await finalizeWebCodecs();
|
|
316
|
-
}
|
|
317
|
-
else if (backend === 'ffmpeg-wasm') {
|
|
318
|
-
await finalizeFfmpeg();
|
|
319
|
-
}
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
case 'cancel': {
|
|
323
|
-
cancelled = true;
|
|
324
|
-
if (videoEncoder && videoEncoder.state !== 'closed') {
|
|
325
|
-
videoEncoder.close();
|
|
326
|
-
videoEncoder = null;
|
|
327
|
-
}
|
|
328
|
-
muxer = null;
|
|
329
|
-
ffmpegInstance = null;
|
|
330
|
-
ffmpegFrames = [];
|
|
331
|
-
ffmpegSegments.length = 0;
|
|
332
|
-
break;
|
|
333
|
-
}
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
case "finalize": {
|
|
270
|
+
if (backend === "webcodecs") {
|
|
271
|
+
await finalizeWebCodecs();
|
|
272
|
+
} else if (backend === "ffmpeg-wasm") {
|
|
273
|
+
await finalizeFfmpeg();
|
|
334
274
|
}
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case "cancel": {
|
|
278
|
+
cancelled = true;
|
|
279
|
+
if (videoEncoder && videoEncoder.state !== "closed") {
|
|
280
|
+
videoEncoder.close();
|
|
281
|
+
videoEncoder = null;
|
|
282
|
+
}
|
|
283
|
+
muxer = null;
|
|
284
|
+
ffmpegInstance = null;
|
|
285
|
+
ffmpegFrames = [];
|
|
286
|
+
ffmpegSegments.length = 0;
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
335
289
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
290
|
+
} catch (err) {
|
|
291
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
292
|
+
postError(message);
|
|
293
|
+
}
|
|
340
294
|
};
|
|
341
295
|
//# sourceMappingURL=encode.worker.js.map
|