framewebworker 0.1.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/LICENSE +21 -0
- package/README.md +224 -0
- package/dist/index.cjs +572 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +144 -0
- package/dist/index.d.ts +144 -0
- package/dist/index.js +568 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +120 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +119 -0
- package/dist/react/index.d.ts +119 -0
- package/dist/react/index.js +117 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +83 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __esm = (fn, res) => function __init() {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
};
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/backends/ffmpeg.ts
|
|
14
|
+
var ffmpeg_exports = {};
|
|
15
|
+
__export(ffmpeg_exports, {
|
|
16
|
+
FFmpegBackend: () => exports.FFmpegBackend,
|
|
17
|
+
createFFmpegBackend: () => createFFmpegBackend
|
|
18
|
+
});
|
|
19
|
+
function createFFmpegBackend() {
|
|
20
|
+
return new exports.FFmpegBackend();
|
|
21
|
+
}
|
|
22
|
+
exports.FFmpegBackend = void 0;
|
|
23
|
+
var init_ffmpeg = __esm({
|
|
24
|
+
"src/backends/ffmpeg.ts"() {
|
|
25
|
+
exports.FFmpegBackend = class {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.name = "ffmpeg.wasm";
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
this.ffmpeg = null;
|
|
30
|
+
this.initialized = false;
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
this._fetchFile = null;
|
|
33
|
+
}
|
|
34
|
+
async init() {
|
|
35
|
+
if (this.initialized) return;
|
|
36
|
+
const { FFmpeg } = await import('@ffmpeg/ffmpeg').catch(() => {
|
|
37
|
+
throw new Error(
|
|
38
|
+
"[FrameWorker] @ffmpeg/ffmpeg is required. Install it: npm install @ffmpeg/ffmpeg @ffmpeg/util"
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
const { fetchFile, toBlobURL } = await import('@ffmpeg/util');
|
|
42
|
+
const ffmpeg = new FFmpeg();
|
|
43
|
+
const baseURL = "https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm";
|
|
44
|
+
await ffmpeg.load({
|
|
45
|
+
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"),
|
|
46
|
+
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm")
|
|
47
|
+
});
|
|
48
|
+
this._fetchFile = fetchFile;
|
|
49
|
+
this.ffmpeg = ffmpeg;
|
|
50
|
+
this.initialized = true;
|
|
51
|
+
}
|
|
52
|
+
async encode(frames, options) {
|
|
53
|
+
await this.init();
|
|
54
|
+
const { ffmpeg, _fetchFile: fetchFile } = this;
|
|
55
|
+
const { fps, width, height, onProgress, signal } = options;
|
|
56
|
+
const total = frames.length;
|
|
57
|
+
for (let i = 0; i < total; i++) {
|
|
58
|
+
if (signal?.aborted) throw new DOMException("Render cancelled", "AbortError");
|
|
59
|
+
const frame = frames[i];
|
|
60
|
+
const offscreen = new OffscreenCanvas(width, height);
|
|
61
|
+
const ctx = offscreen.getContext("2d");
|
|
62
|
+
ctx.putImageData(frame.imageData, 0, 0);
|
|
63
|
+
const blob = await offscreen.convertToBlob({ type: "image/png" });
|
|
64
|
+
const data2 = await fetchFile(blob);
|
|
65
|
+
await ffmpeg.writeFile(`frame${String(i).padStart(6, "0")}.png`, data2);
|
|
66
|
+
onProgress?.(i / total * 0.8);
|
|
67
|
+
}
|
|
68
|
+
await ffmpeg.exec([
|
|
69
|
+
"-framerate",
|
|
70
|
+
String(fps),
|
|
71
|
+
"-i",
|
|
72
|
+
"frame%06d.png",
|
|
73
|
+
"-c:v",
|
|
74
|
+
"libx264",
|
|
75
|
+
"-pix_fmt",
|
|
76
|
+
"yuv420p",
|
|
77
|
+
"-preset",
|
|
78
|
+
"fast",
|
|
79
|
+
"-crf",
|
|
80
|
+
"23",
|
|
81
|
+
"-movflags",
|
|
82
|
+
"+faststart",
|
|
83
|
+
"output.mp4"
|
|
84
|
+
]);
|
|
85
|
+
onProgress?.(0.95);
|
|
86
|
+
const data = await ffmpeg.readFile("output.mp4");
|
|
87
|
+
for (let i = 0; i < total; i++) {
|
|
88
|
+
await ffmpeg.deleteFile(`frame${String(i).padStart(6, "0")}.png`).catch(() => {
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
await ffmpeg.deleteFile("output.mp4").catch(() => {
|
|
92
|
+
});
|
|
93
|
+
onProgress?.(1);
|
|
94
|
+
return new Blob([data.slice().buffer], { type: "video/mp4" });
|
|
95
|
+
}
|
|
96
|
+
async concat(blobs, options) {
|
|
97
|
+
await this.init();
|
|
98
|
+
const { ffmpeg, _fetchFile: fetchFile } = this;
|
|
99
|
+
const { onProgress } = options;
|
|
100
|
+
const listLines = [];
|
|
101
|
+
for (let i = 0; i < blobs.length; i++) {
|
|
102
|
+
const name = `clip${i}.mp4`;
|
|
103
|
+
const data = await fetchFile(blobs[i]);
|
|
104
|
+
await ffmpeg.writeFile(name, data);
|
|
105
|
+
listLines.push(`file '${name}'`);
|
|
106
|
+
onProgress?.(i / blobs.length * 0.6);
|
|
107
|
+
}
|
|
108
|
+
const encoder = new TextEncoder();
|
|
109
|
+
await ffmpeg.writeFile("concat.txt", encoder.encode(listLines.join("\n")));
|
|
110
|
+
await ffmpeg.exec([
|
|
111
|
+
"-f",
|
|
112
|
+
"concat",
|
|
113
|
+
"-safe",
|
|
114
|
+
"0",
|
|
115
|
+
"-i",
|
|
116
|
+
"concat.txt",
|
|
117
|
+
"-c",
|
|
118
|
+
"copy",
|
|
119
|
+
"stitched.mp4"
|
|
120
|
+
]);
|
|
121
|
+
onProgress?.(0.9);
|
|
122
|
+
const out = await ffmpeg.readFile("stitched.mp4");
|
|
123
|
+
for (let i = 0; i < blobs.length; i++) {
|
|
124
|
+
await ffmpeg.deleteFile(`clip${i}.mp4`).catch(() => {
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
await ffmpeg.deleteFile("concat.txt").catch(() => {
|
|
128
|
+
});
|
|
129
|
+
await ffmpeg.deleteFile("stitched.mp4").catch(() => {
|
|
130
|
+
});
|
|
131
|
+
onProgress?.(1);
|
|
132
|
+
return new Blob([out.slice().buffer], { type: "video/mp4" });
|
|
133
|
+
}
|
|
134
|
+
async destroy() {
|
|
135
|
+
if (this.ffmpeg) {
|
|
136
|
+
await this.ffmpeg.terminate?.();
|
|
137
|
+
this.ffmpeg = null;
|
|
138
|
+
this.initialized = false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// src/captions.ts
|
|
146
|
+
var STYLE_PRESETS = {
|
|
147
|
+
hormozi: {
|
|
148
|
+
preset: "hormozi",
|
|
149
|
+
fontFamily: 'Impact, "Arial Black", sans-serif',
|
|
150
|
+
fontSize: 64,
|
|
151
|
+
fontWeight: "900",
|
|
152
|
+
color: "#FFFFFF",
|
|
153
|
+
strokeColor: "#000000",
|
|
154
|
+
strokeWidth: 4,
|
|
155
|
+
backgroundColor: "transparent",
|
|
156
|
+
backgroundPadding: 0,
|
|
157
|
+
backgroundRadius: 0,
|
|
158
|
+
position: "bottom",
|
|
159
|
+
textAlign: "center",
|
|
160
|
+
lineHeight: 1.1,
|
|
161
|
+
maxWidth: 0.9,
|
|
162
|
+
shadow: true,
|
|
163
|
+
shadowColor: "rgba(0,0,0,0.9)",
|
|
164
|
+
shadowBlur: 6,
|
|
165
|
+
shadowOffsetX: 2,
|
|
166
|
+
shadowOffsetY: 2,
|
|
167
|
+
uppercase: true,
|
|
168
|
+
wordHighlight: true,
|
|
169
|
+
wordHighlightColor: "#FFD700",
|
|
170
|
+
wordHighlightTextColor: "#000000"
|
|
171
|
+
},
|
|
172
|
+
modern: {
|
|
173
|
+
preset: "modern",
|
|
174
|
+
fontFamily: '"Inter", "Helvetica Neue", Arial, sans-serif',
|
|
175
|
+
fontSize: 42,
|
|
176
|
+
fontWeight: "700",
|
|
177
|
+
color: "#FFFFFF",
|
|
178
|
+
strokeColor: "transparent",
|
|
179
|
+
strokeWidth: 0,
|
|
180
|
+
backgroundColor: "rgba(0,0,0,0.65)",
|
|
181
|
+
backgroundPadding: 12,
|
|
182
|
+
backgroundRadius: 8,
|
|
183
|
+
position: "bottom",
|
|
184
|
+
textAlign: "center",
|
|
185
|
+
lineHeight: 1.3,
|
|
186
|
+
maxWidth: 0.85,
|
|
187
|
+
shadow: false,
|
|
188
|
+
shadowColor: "transparent",
|
|
189
|
+
shadowBlur: 0,
|
|
190
|
+
shadowOffsetX: 0,
|
|
191
|
+
shadowOffsetY: 0,
|
|
192
|
+
uppercase: false,
|
|
193
|
+
wordHighlight: false,
|
|
194
|
+
wordHighlightColor: "#3B82F6",
|
|
195
|
+
wordHighlightTextColor: "#FFFFFF"
|
|
196
|
+
},
|
|
197
|
+
minimal: {
|
|
198
|
+
preset: "minimal",
|
|
199
|
+
fontFamily: '"Helvetica Neue", Arial, sans-serif',
|
|
200
|
+
fontSize: 36,
|
|
201
|
+
fontWeight: "400",
|
|
202
|
+
color: "#FFFFFF",
|
|
203
|
+
strokeColor: "transparent",
|
|
204
|
+
strokeWidth: 0,
|
|
205
|
+
backgroundColor: "transparent",
|
|
206
|
+
backgroundPadding: 0,
|
|
207
|
+
backgroundRadius: 0,
|
|
208
|
+
position: "bottom",
|
|
209
|
+
textAlign: "center",
|
|
210
|
+
lineHeight: 1.4,
|
|
211
|
+
maxWidth: 0.8,
|
|
212
|
+
shadow: true,
|
|
213
|
+
shadowColor: "rgba(0,0,0,0.8)",
|
|
214
|
+
shadowBlur: 8,
|
|
215
|
+
shadowOffsetX: 0,
|
|
216
|
+
shadowOffsetY: 2,
|
|
217
|
+
uppercase: false,
|
|
218
|
+
wordHighlight: false,
|
|
219
|
+
wordHighlightColor: "#FFFFFF",
|
|
220
|
+
wordHighlightTextColor: "#000000"
|
|
221
|
+
},
|
|
222
|
+
bold: {
|
|
223
|
+
preset: "bold",
|
|
224
|
+
fontFamily: '"Arial Black", "Helvetica Neue", Arial, sans-serif',
|
|
225
|
+
fontSize: 56,
|
|
226
|
+
fontWeight: "900",
|
|
227
|
+
color: "#FFFF00",
|
|
228
|
+
strokeColor: "#000000",
|
|
229
|
+
strokeWidth: 5,
|
|
230
|
+
backgroundColor: "transparent",
|
|
231
|
+
backgroundPadding: 0,
|
|
232
|
+
backgroundRadius: 0,
|
|
233
|
+
position: "center",
|
|
234
|
+
textAlign: "center",
|
|
235
|
+
lineHeight: 1.2,
|
|
236
|
+
maxWidth: 0.88,
|
|
237
|
+
shadow: true,
|
|
238
|
+
shadowColor: "rgba(0,0,0,1)",
|
|
239
|
+
shadowBlur: 4,
|
|
240
|
+
shadowOffsetX: 3,
|
|
241
|
+
shadowOffsetY: 3,
|
|
242
|
+
uppercase: true,
|
|
243
|
+
wordHighlight: false,
|
|
244
|
+
wordHighlightColor: "#FF0000",
|
|
245
|
+
wordHighlightTextColor: "#FFFFFF"
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
function mergeStyle(base, overrides) {
|
|
249
|
+
return overrides ? { ...base, ...overrides } : base;
|
|
250
|
+
}
|
|
251
|
+
function getActiveCaptions(segments, currentTime) {
|
|
252
|
+
return segments.filter(
|
|
253
|
+
(seg) => currentTime >= seg.startTime && currentTime < seg.endTime
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
function wrapText(ctx, text, maxWidth) {
|
|
257
|
+
const words = text.split(" ");
|
|
258
|
+
const lines = [];
|
|
259
|
+
let current = "";
|
|
260
|
+
for (const word of words) {
|
|
261
|
+
const test = current ? `${current} ${word}` : word;
|
|
262
|
+
if (ctx.measureText(test).width > maxWidth && current) {
|
|
263
|
+
lines.push(current);
|
|
264
|
+
current = word;
|
|
265
|
+
} else {
|
|
266
|
+
current = test;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (current) lines.push(current);
|
|
270
|
+
return lines;
|
|
271
|
+
}
|
|
272
|
+
function renderCaption(ctx, segment, resolvedStyle, canvasWidth, canvasHeight) {
|
|
273
|
+
const style = resolvedStyle;
|
|
274
|
+
const text = style.uppercase ? segment.text.toUpperCase() : segment.text;
|
|
275
|
+
ctx.save();
|
|
276
|
+
const scaledFontSize = style.fontSize / 1080 * canvasHeight;
|
|
277
|
+
ctx.font = `${style.fontWeight} ${scaledFontSize}px ${style.fontFamily}`;
|
|
278
|
+
ctx.textAlign = style.textAlign;
|
|
279
|
+
ctx.textBaseline = "bottom";
|
|
280
|
+
const maxPx = style.maxWidth * canvasWidth;
|
|
281
|
+
const lines = wrapText(ctx, text, maxPx);
|
|
282
|
+
const lineH = scaledFontSize * style.lineHeight;
|
|
283
|
+
const totalH = lines.length * lineH;
|
|
284
|
+
let baseY;
|
|
285
|
+
if (style.position === "top") {
|
|
286
|
+
baseY = scaledFontSize * 1.5;
|
|
287
|
+
} else if (style.position === "center") {
|
|
288
|
+
baseY = canvasHeight / 2 - totalH / 2 + lineH;
|
|
289
|
+
} else {
|
|
290
|
+
baseY = canvasHeight - scaledFontSize * 1.2;
|
|
291
|
+
}
|
|
292
|
+
const cx = canvasWidth / 2;
|
|
293
|
+
lines.forEach((line, i) => {
|
|
294
|
+
const y = baseY + i * lineH;
|
|
295
|
+
if (style.backgroundColor && style.backgroundColor !== "transparent") {
|
|
296
|
+
const metrics = ctx.measureText(line);
|
|
297
|
+
const bw = metrics.width + style.backgroundPadding * 2;
|
|
298
|
+
const bh = lineH + style.backgroundPadding;
|
|
299
|
+
const bx = cx - bw / 2;
|
|
300
|
+
const by = y - lineH;
|
|
301
|
+
ctx.fillStyle = style.backgroundColor;
|
|
302
|
+
if (style.backgroundRadius > 0) {
|
|
303
|
+
roundRect(ctx, bx, by, bw, bh, style.backgroundRadius);
|
|
304
|
+
ctx.fill();
|
|
305
|
+
} else {
|
|
306
|
+
ctx.fillRect(bx, by, bw, bh);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (style.shadow) {
|
|
310
|
+
ctx.shadowColor = style.shadowColor;
|
|
311
|
+
ctx.shadowBlur = style.shadowBlur;
|
|
312
|
+
ctx.shadowOffsetX = style.shadowOffsetX;
|
|
313
|
+
ctx.shadowOffsetY = style.shadowOffsetY;
|
|
314
|
+
}
|
|
315
|
+
if (style.strokeWidth > 0 && style.strokeColor !== "transparent") {
|
|
316
|
+
ctx.lineWidth = style.strokeWidth;
|
|
317
|
+
ctx.strokeStyle = style.strokeColor;
|
|
318
|
+
ctx.strokeText(line, cx, y);
|
|
319
|
+
}
|
|
320
|
+
ctx.shadowColor = "transparent";
|
|
321
|
+
ctx.shadowBlur = 0;
|
|
322
|
+
ctx.fillStyle = style.color;
|
|
323
|
+
ctx.fillText(line, cx, y);
|
|
324
|
+
});
|
|
325
|
+
ctx.restore();
|
|
326
|
+
}
|
|
327
|
+
function roundRect(ctx, x, y, w, h, r) {
|
|
328
|
+
ctx.beginPath();
|
|
329
|
+
ctx.moveTo(x + r, y);
|
|
330
|
+
ctx.lineTo(x + w - r, y);
|
|
331
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
332
|
+
ctx.lineTo(x + w, y + h - r);
|
|
333
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
|
334
|
+
ctx.lineTo(x + r, y + h);
|
|
335
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
|
336
|
+
ctx.lineTo(x, y + r);
|
|
337
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
338
|
+
ctx.closePath();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/index.ts
|
|
342
|
+
init_ffmpeg();
|
|
343
|
+
|
|
344
|
+
// src/compositor.ts
|
|
345
|
+
var ASPECT_RATIO_MAP = {
|
|
346
|
+
"16:9": [16, 9],
|
|
347
|
+
"9:16": [9, 16],
|
|
348
|
+
"1:1": [1, 1],
|
|
349
|
+
"4:3": [4, 3],
|
|
350
|
+
"3:4": [3, 4],
|
|
351
|
+
original: [0, 0]
|
|
352
|
+
};
|
|
353
|
+
function resolveOutputDimensions(clip, videoWidth, videoHeight, options) {
|
|
354
|
+
const ar = clip.aspectRatio ?? "original";
|
|
355
|
+
const ratio = ASPECT_RATIO_MAP[ar] ?? [0, 0];
|
|
356
|
+
if (ratio[0] === 0) {
|
|
357
|
+
return [options.width ?? videoWidth, options.height ?? videoHeight];
|
|
358
|
+
}
|
|
359
|
+
const w = options.width ?? 1280;
|
|
360
|
+
const h = Math.round(w * (ratio[1] / ratio[0]));
|
|
361
|
+
return [w, h];
|
|
362
|
+
}
|
|
363
|
+
async function extractFrames(clip, options) {
|
|
364
|
+
const fps = options.fps ?? 30;
|
|
365
|
+
const onProgress = options.onProgress;
|
|
366
|
+
const signal = options.signal;
|
|
367
|
+
let srcUrl;
|
|
368
|
+
let needsRevoke = false;
|
|
369
|
+
if (typeof clip.source === "string") {
|
|
370
|
+
srcUrl = clip.source;
|
|
371
|
+
} else if (clip.source instanceof HTMLVideoElement) {
|
|
372
|
+
srcUrl = clip.source.src;
|
|
373
|
+
} else {
|
|
374
|
+
srcUrl = URL.createObjectURL(clip.source);
|
|
375
|
+
needsRevoke = true;
|
|
376
|
+
}
|
|
377
|
+
const video = document.createElement("video");
|
|
378
|
+
video.muted = true;
|
|
379
|
+
video.crossOrigin = "anonymous";
|
|
380
|
+
video.preload = "auto";
|
|
381
|
+
await new Promise((resolve, reject) => {
|
|
382
|
+
video.onloadedmetadata = () => resolve();
|
|
383
|
+
video.onerror = () => reject(new Error(`Failed to load video: ${srcUrl}`));
|
|
384
|
+
video.src = srcUrl;
|
|
385
|
+
});
|
|
386
|
+
const duration = video.duration;
|
|
387
|
+
const startTime = clip.startTime ?? 0;
|
|
388
|
+
const endTime = clip.endTime ?? duration;
|
|
389
|
+
const clipDuration = endTime - startTime;
|
|
390
|
+
const [outW, outH] = resolveOutputDimensions(
|
|
391
|
+
clip,
|
|
392
|
+
video.videoWidth,
|
|
393
|
+
video.videoHeight,
|
|
394
|
+
options
|
|
395
|
+
);
|
|
396
|
+
const canvas = document.createElement("canvas");
|
|
397
|
+
canvas.width = outW;
|
|
398
|
+
canvas.height = outH;
|
|
399
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
400
|
+
const totalFrames = Math.ceil(clipDuration * fps);
|
|
401
|
+
const frames = [];
|
|
402
|
+
const captionSegments = clip.captions?.segments ?? [];
|
|
403
|
+
const baseStylePreset = clip.captions?.style?.preset ?? "modern";
|
|
404
|
+
const baseStyle = mergeStyle(
|
|
405
|
+
STYLE_PRESETS[baseStylePreset],
|
|
406
|
+
clip.captions?.style
|
|
407
|
+
);
|
|
408
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
409
|
+
if (signal?.aborted) throw new DOMException("Render cancelled", "AbortError");
|
|
410
|
+
const t = startTime + i / fps;
|
|
411
|
+
await seekVideo(video, t);
|
|
412
|
+
ctx.clearRect(0, 0, outW, outH);
|
|
413
|
+
drawVideoFrame(ctx, video, clip, outW, outH);
|
|
414
|
+
if (captionSegments.length > 0) {
|
|
415
|
+
const active = getActiveCaptions(captionSegments, t - startTime);
|
|
416
|
+
for (const seg of active) {
|
|
417
|
+
const segStyle = mergeStyle(baseStyle, seg.style);
|
|
418
|
+
renderCaption(ctx, seg, segStyle, outW, outH);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const imageData = ctx.getImageData(0, 0, outW, outH);
|
|
422
|
+
frames.push({ imageData, timestamp: t - startTime, width: outW, height: outH });
|
|
423
|
+
if (onProgress) onProgress(i / totalFrames);
|
|
424
|
+
}
|
|
425
|
+
if (needsRevoke) URL.revokeObjectURL(srcUrl);
|
|
426
|
+
return frames;
|
|
427
|
+
}
|
|
428
|
+
function drawVideoFrame(ctx, video, clip, outW, outH) {
|
|
429
|
+
const vw = video.videoWidth;
|
|
430
|
+
const vh = video.videoHeight;
|
|
431
|
+
if (clip.crop) {
|
|
432
|
+
const { x, y, width, height } = clip.crop;
|
|
433
|
+
ctx.drawImage(
|
|
434
|
+
video,
|
|
435
|
+
x * vw,
|
|
436
|
+
y * vh,
|
|
437
|
+
width * vw,
|
|
438
|
+
height * vh,
|
|
439
|
+
0,
|
|
440
|
+
0,
|
|
441
|
+
outW,
|
|
442
|
+
outH
|
|
443
|
+
);
|
|
444
|
+
} else {
|
|
445
|
+
const videoAR = vw / vh;
|
|
446
|
+
const outAR = outW / outH;
|
|
447
|
+
let sx = 0, sy = 0, sw = vw, sh = vh;
|
|
448
|
+
if (videoAR > outAR) {
|
|
449
|
+
sw = vh * outAR;
|
|
450
|
+
sx = (vw - sw) / 2;
|
|
451
|
+
} else if (videoAR < outAR) {
|
|
452
|
+
sh = vw / outAR;
|
|
453
|
+
sy = (vh - sh) / 2;
|
|
454
|
+
}
|
|
455
|
+
ctx.drawImage(video, sx, sy, sw, sh, 0, 0, outW, outH);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function seekVideo(video, time) {
|
|
459
|
+
return new Promise((resolve) => {
|
|
460
|
+
if (Math.abs(video.currentTime - time) < 1e-3) {
|
|
461
|
+
resolve();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const onSeeked = () => {
|
|
465
|
+
video.removeEventListener("seeked", onSeeked);
|
|
466
|
+
resolve();
|
|
467
|
+
};
|
|
468
|
+
video.addEventListener("seeked", onSeeked);
|
|
469
|
+
video.currentTime = time;
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/stitch.ts
|
|
474
|
+
async function stitchClips(clips, backend, options) {
|
|
475
|
+
const fps = options.fps ?? 30;
|
|
476
|
+
const width = options.width ?? 1280;
|
|
477
|
+
const height = options.height ?? 720;
|
|
478
|
+
const onProgress = options.onProgress;
|
|
479
|
+
const blobs = [];
|
|
480
|
+
for (let ci = 0; ci < clips.length; ci++) {
|
|
481
|
+
const clip = clips[ci];
|
|
482
|
+
const clipProgress = (p) => {
|
|
483
|
+
onProgress?.((ci + p * 0.9) / clips.length);
|
|
484
|
+
};
|
|
485
|
+
const frames = await extractFrames(clip, {
|
|
486
|
+
...options,
|
|
487
|
+
width,
|
|
488
|
+
height,
|
|
489
|
+
fps,
|
|
490
|
+
onProgress: clipProgress
|
|
491
|
+
});
|
|
492
|
+
const blob = await backend.encode(frames, {
|
|
493
|
+
width,
|
|
494
|
+
height,
|
|
495
|
+
fps,
|
|
496
|
+
mimeType: options.mimeType ?? "video/mp4",
|
|
497
|
+
quality: options.quality ?? 0.92,
|
|
498
|
+
encoderOptions: options.encoderOptions,
|
|
499
|
+
onProgress: (p) => clipProgress(0.9 + p * 0.1),
|
|
500
|
+
signal: options.signal
|
|
501
|
+
});
|
|
502
|
+
blobs.push(blob);
|
|
503
|
+
}
|
|
504
|
+
if (blobs.length === 1) {
|
|
505
|
+
onProgress?.(1);
|
|
506
|
+
return blobs[0];
|
|
507
|
+
}
|
|
508
|
+
return backend.concat(blobs, {
|
|
509
|
+
width,
|
|
510
|
+
height,
|
|
511
|
+
fps,
|
|
512
|
+
mimeType: options.mimeType ?? "video/mp4",
|
|
513
|
+
quality: options.quality ?? 0.92,
|
|
514
|
+
onProgress: (p) => onProgress?.((clips.length - 1 + p) / clips.length),
|
|
515
|
+
signal: options.signal
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/index.ts
|
|
520
|
+
function createFrameWorker(config = {}) {
|
|
521
|
+
const fps = config.fps ?? 30;
|
|
522
|
+
const width = config.width ?? 1280;
|
|
523
|
+
const height = config.height ?? 720;
|
|
524
|
+
let _backend = config.backend ?? null;
|
|
525
|
+
async function getBackend() {
|
|
526
|
+
if (!_backend) {
|
|
527
|
+
const { createFFmpegBackend: createFFmpegBackend2 } = await Promise.resolve().then(() => (init_ffmpeg(), ffmpeg_exports));
|
|
528
|
+
_backend = createFFmpegBackend2();
|
|
529
|
+
}
|
|
530
|
+
await _backend.init();
|
|
531
|
+
return _backend;
|
|
532
|
+
}
|
|
533
|
+
async function render(clip, options = {}) {
|
|
534
|
+
const mergedOpts = { fps, width, height, ...options };
|
|
535
|
+
const backend = await getBackend();
|
|
536
|
+
const onProgress = mergedOpts.onProgress;
|
|
537
|
+
const frames = await extractFrames(clip, {
|
|
538
|
+
...mergedOpts,
|
|
539
|
+
onProgress: onProgress ? (p) => onProgress(p * 0.85) : void 0
|
|
540
|
+
});
|
|
541
|
+
return backend.encode(frames, {
|
|
542
|
+
width: mergedOpts.width ?? width,
|
|
543
|
+
height: mergedOpts.height ?? height,
|
|
544
|
+
fps: mergedOpts.fps ?? fps,
|
|
545
|
+
mimeType: mergedOpts.mimeType ?? "video/mp4",
|
|
546
|
+
quality: mergedOpts.quality ?? 0.92,
|
|
547
|
+
encoderOptions: mergedOpts.encoderOptions,
|
|
548
|
+
onProgress: onProgress ? (p) => onProgress(0.85 + p * 0.15) : void 0,
|
|
549
|
+
signal: mergedOpts.signal
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
async function renderToUrl(clip, options) {
|
|
553
|
+
const blob = await render(clip, options);
|
|
554
|
+
return URL.createObjectURL(blob);
|
|
555
|
+
}
|
|
556
|
+
async function stitch(clips, options = {}) {
|
|
557
|
+
const mergedOpts = { fps, width, height, ...options };
|
|
558
|
+
const backend = await getBackend();
|
|
559
|
+
return stitchClips(clips, backend, mergedOpts);
|
|
560
|
+
}
|
|
561
|
+
async function stitchToUrl(clips, options) {
|
|
562
|
+
const blob = await stitch(clips, options);
|
|
563
|
+
return URL.createObjectURL(blob);
|
|
564
|
+
}
|
|
565
|
+
return { render, renderToUrl, stitch, stitchToUrl };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
exports.STYLE_PRESETS = STYLE_PRESETS;
|
|
569
|
+
exports.createFFmpegBackend = createFFmpegBackend;
|
|
570
|
+
exports.createFrameWorker = createFrameWorker;
|
|
571
|
+
//# sourceMappingURL=index.cjs.map
|
|
572
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/backends/ffmpeg.ts","../src/captions.ts","../src/index.ts","../src/compositor.ts","../src/stitch.ts"],"names":["FFmpegBackend","data","createFFmpegBackend"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { RendererBackend, FrameData, EncodeOptions } from '../types.js';\n\nexport class FFmpegBackend implements RendererBackend {\n readonly name = 'ffmpeg.wasm';\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private ffmpeg: any = null;\n private initialized = false;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private _fetchFile: any = null;\n\n async init(): Promise<void> {\n if (this.initialized) return;\n\n const { FFmpeg } = await import('@ffmpeg/ffmpeg').catch(() => {\n throw new Error(\n '[FrameWorker] @ffmpeg/ffmpeg is required. Install it: npm install @ffmpeg/ffmpeg @ffmpeg/util'\n );\n });\n const { fetchFile, toBlobURL } = await import('@ffmpeg/util');\n\n const ffmpeg = new FFmpeg();\n\n const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';\n await ffmpeg.load({\n coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),\n wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),\n });\n\n this._fetchFile = fetchFile;\n this.ffmpeg = ffmpeg;\n this.initialized = true;\n }\n\n async encode(frames: FrameData[], options: EncodeOptions): Promise<Blob> {\n await this.init();\n const { ffmpeg, _fetchFile: fetchFile } = this;\n const { fps, width, height, onProgress, signal } = options;\n\n const total = frames.length;\n for (let i = 0; i < total; i++) {\n if (signal?.aborted) throw new DOMException('Render cancelled', 'AbortError');\n\n const frame = frames[i];\n const offscreen = new OffscreenCanvas(width, height);\n const ctx = offscreen.getContext('2d')!;\n ctx.putImageData(frame.imageData, 0, 0);\n const blob = await offscreen.convertToBlob({ type: 'image/png' });\n const data = await fetchFile(blob);\n await ffmpeg.writeFile(`frame${String(i).padStart(6, '0')}.png`, data);\n\n onProgress?.(i / total * 0.8);\n }\n\n await ffmpeg.exec([\n '-framerate', String(fps),\n '-i', 'frame%06d.png',\n '-c:v', 'libx264',\n '-pix_fmt', 'yuv420p',\n '-preset', 'fast',\n '-crf', '23',\n '-movflags', '+faststart',\n 'output.mp4',\n ]);\n\n onProgress?.(0.95);\n\n const data = await ffmpeg.readFile('output.mp4') as Uint8Array;\n\n for (let i = 0; i < total; i++) {\n await ffmpeg.deleteFile(`frame${String(i).padStart(6, '0')}.png`).catch(() => {});\n }\n await ffmpeg.deleteFile('output.mp4').catch(() => {});\n\n onProgress?.(1);\n\n return new Blob([data.slice().buffer], { type: 'video/mp4' });\n }\n\n async concat(blobs: Blob[], options: EncodeOptions): Promise<Blob> {\n await this.init();\n const { ffmpeg, _fetchFile: fetchFile } = this;\n const { onProgress } = options;\n\n const listLines: string[] = [];\n for (let i = 0; i < blobs.length; i++) {\n const name = `clip${i}.mp4`;\n const data = await fetchFile(blobs[i]);\n await ffmpeg.writeFile(name, data);\n listLines.push(`file '${name}'`);\n onProgress?.(i / blobs.length * 0.6);\n }\n\n const encoder = new TextEncoder();\n await ffmpeg.writeFile('concat.txt', encoder.encode(listLines.join('\\n')));\n\n await ffmpeg.exec([\n '-f', 'concat',\n '-safe', '0',\n '-i', 'concat.txt',\n '-c', 'copy',\n 'stitched.mp4',\n ]);\n\n onProgress?.(0.9);\n\n const out = await ffmpeg.readFile('stitched.mp4') as Uint8Array;\n\n for (let i = 0; i < blobs.length; i++) {\n await ffmpeg.deleteFile(`clip${i}.mp4`).catch(() => {});\n }\n await ffmpeg.deleteFile('concat.txt').catch(() => {});\n await ffmpeg.deleteFile('stitched.mp4').catch(() => {});\n\n onProgress?.(1);\n\n return new Blob([out.slice().buffer], { type: 'video/mp4' });\n }\n\n async destroy(): Promise<void> {\n if (this.ffmpeg) {\n await this.ffmpeg.terminate?.();\n this.ffmpeg = null;\n this.initialized = false;\n }\n }\n}\n\nexport function createFFmpegBackend(): FFmpegBackend {\n return new FFmpegBackend();\n}\n","import type { CaptionSegment, CaptionStyle, CaptionStylePreset } from './types.js';\n\nexport const STYLE_PRESETS: Record<CaptionStylePreset, CaptionStyle> = {\n hormozi: {\n preset: 'hormozi',\n fontFamily: 'Impact, \"Arial Black\", sans-serif',\n fontSize: 64,\n fontWeight: '900',\n color: '#FFFFFF',\n strokeColor: '#000000',\n strokeWidth: 4,\n backgroundColor: 'transparent',\n backgroundPadding: 0,\n backgroundRadius: 0,\n position: 'bottom',\n textAlign: 'center',\n lineHeight: 1.1,\n maxWidth: 0.9,\n shadow: true,\n shadowColor: 'rgba(0,0,0,0.9)',\n shadowBlur: 6,\n shadowOffsetX: 2,\n shadowOffsetY: 2,\n uppercase: true,\n wordHighlight: true,\n wordHighlightColor: '#FFD700',\n wordHighlightTextColor: '#000000',\n },\n modern: {\n preset: 'modern',\n fontFamily: '\"Inter\", \"Helvetica Neue\", Arial, sans-serif',\n fontSize: 42,\n fontWeight: '700',\n color: '#FFFFFF',\n strokeColor: 'transparent',\n strokeWidth: 0,\n backgroundColor: 'rgba(0,0,0,0.65)',\n backgroundPadding: 12,\n backgroundRadius: 8,\n position: 'bottom',\n textAlign: 'center',\n lineHeight: 1.3,\n maxWidth: 0.85,\n shadow: false,\n shadowColor: 'transparent',\n shadowBlur: 0,\n shadowOffsetX: 0,\n shadowOffsetY: 0,\n uppercase: false,\n wordHighlight: false,\n wordHighlightColor: '#3B82F6',\n wordHighlightTextColor: '#FFFFFF',\n },\n minimal: {\n preset: 'minimal',\n fontFamily: '\"Helvetica Neue\", Arial, sans-serif',\n fontSize: 36,\n fontWeight: '400',\n color: '#FFFFFF',\n strokeColor: 'transparent',\n strokeWidth: 0,\n backgroundColor: 'transparent',\n backgroundPadding: 0,\n backgroundRadius: 0,\n position: 'bottom',\n textAlign: 'center',\n lineHeight: 1.4,\n maxWidth: 0.8,\n shadow: true,\n shadowColor: 'rgba(0,0,0,0.8)',\n shadowBlur: 8,\n shadowOffsetX: 0,\n shadowOffsetY: 2,\n uppercase: false,\n wordHighlight: false,\n wordHighlightColor: '#FFFFFF',\n wordHighlightTextColor: '#000000',\n },\n bold: {\n preset: 'bold',\n fontFamily: '\"Arial Black\", \"Helvetica Neue\", Arial, sans-serif',\n fontSize: 56,\n fontWeight: '900',\n color: '#FFFF00',\n strokeColor: '#000000',\n strokeWidth: 5,\n backgroundColor: 'transparent',\n backgroundPadding: 0,\n backgroundRadius: 0,\n position: 'center',\n textAlign: 'center',\n lineHeight: 1.2,\n maxWidth: 0.88,\n shadow: true,\n shadowColor: 'rgba(0,0,0,1)',\n shadowBlur: 4,\n shadowOffsetX: 3,\n shadowOffsetY: 3,\n uppercase: true,\n wordHighlight: false,\n wordHighlightColor: '#FF0000',\n wordHighlightTextColor: '#FFFFFF',\n },\n};\n\nexport function mergeStyle(\n base: CaptionStyle,\n overrides?: Partial<CaptionStyle>\n): CaptionStyle {\n return overrides ? { ...base, ...overrides } : base;\n}\n\nexport function getActiveCaptions(\n segments: CaptionSegment[],\n currentTime: number\n): CaptionSegment[] {\n return segments.filter(\n (seg) => currentTime >= seg.startTime && currentTime < seg.endTime\n );\n}\n\nfunction wrapText(\n ctx: CanvasRenderingContext2D,\n text: string,\n maxWidth: number\n): string[] {\n const words = text.split(' ');\n const lines: string[] = [];\n let current = '';\n\n for (const word of words) {\n const test = current ? `${current} ${word}` : word;\n if (ctx.measureText(test).width > maxWidth && current) {\n lines.push(current);\n current = word;\n } else {\n current = test;\n }\n }\n if (current) lines.push(current);\n return lines;\n}\n\nexport function renderCaption(\n ctx: CanvasRenderingContext2D,\n segment: CaptionSegment,\n resolvedStyle: CaptionStyle,\n canvasWidth: number,\n canvasHeight: number\n): void {\n const style = resolvedStyle;\n const text = style.uppercase ? segment.text.toUpperCase() : segment.text;\n\n ctx.save();\n\n const scaledFontSize = (style.fontSize / 1080) * canvasHeight;\n ctx.font = `${style.fontWeight} ${scaledFontSize}px ${style.fontFamily}`;\n ctx.textAlign = style.textAlign;\n ctx.textBaseline = 'bottom';\n\n const maxPx = style.maxWidth * canvasWidth;\n const lines = wrapText(ctx, text, maxPx);\n const lineH = scaledFontSize * style.lineHeight;\n const totalH = lines.length * lineH;\n\n let baseY: number;\n if (style.position === 'top') {\n baseY = scaledFontSize * 1.5;\n } else if (style.position === 'center') {\n baseY = canvasHeight / 2 - totalH / 2 + lineH;\n } else {\n baseY = canvasHeight - scaledFontSize * 1.2;\n }\n\n const cx = canvasWidth / 2;\n\n lines.forEach((line, i) => {\n const y = baseY + i * lineH;\n\n // Background box\n if (style.backgroundColor && style.backgroundColor !== 'transparent') {\n const metrics = ctx.measureText(line);\n const bw = metrics.width + style.backgroundPadding * 2;\n const bh = lineH + style.backgroundPadding;\n const bx = cx - bw / 2;\n const by = y - lineH;\n\n ctx.fillStyle = style.backgroundColor;\n if (style.backgroundRadius > 0) {\n roundRect(ctx, bx, by, bw, bh, style.backgroundRadius);\n ctx.fill();\n } else {\n ctx.fillRect(bx, by, bw, bh);\n }\n }\n\n // Shadow\n if (style.shadow) {\n ctx.shadowColor = style.shadowColor;\n ctx.shadowBlur = style.shadowBlur;\n ctx.shadowOffsetX = style.shadowOffsetX;\n ctx.shadowOffsetY = style.shadowOffsetY;\n }\n\n // Stroke\n if (style.strokeWidth > 0 && style.strokeColor !== 'transparent') {\n ctx.lineWidth = style.strokeWidth;\n ctx.strokeStyle = style.strokeColor;\n ctx.strokeText(line, cx, y);\n }\n\n // Fill\n ctx.shadowColor = 'transparent';\n ctx.shadowBlur = 0;\n ctx.fillStyle = style.color;\n ctx.fillText(line, cx, y);\n });\n\n ctx.restore();\n}\n\nfunction roundRect(\n ctx: CanvasRenderingContext2D,\n x: number,\n y: number,\n w: number,\n h: number,\n r: number\n): void {\n ctx.beginPath();\n ctx.moveTo(x + r, y);\n ctx.lineTo(x + w - r, y);\n ctx.quadraticCurveTo(x + w, y, x + w, y + r);\n ctx.lineTo(x + w, y + h - r);\n ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);\n ctx.lineTo(x + r, y + h);\n ctx.quadraticCurveTo(x, y + h, x, y + h - r);\n ctx.lineTo(x, y + r);\n ctx.quadraticCurveTo(x, y, x + r, y);\n ctx.closePath();\n}\n","export type {\n ClipInput,\n CaptionSegment,\n CaptionStyle,\n CaptionStylePreset,\n AspectRatio,\n CropOptions,\n CaptionOptions,\n RenderOptions,\n EncodeOptions,\n FrameData,\n RendererBackend,\n FrameWorkerConfig,\n FrameWorker,\n} from './types.js';\n\nexport { STYLE_PRESETS } from './captions.js';\nexport { FFmpegBackend, createFFmpegBackend } from './backends/ffmpeg.js';\n\nimport type { ClipInput, RenderOptions, FrameWorkerConfig, FrameWorker, RendererBackend } from './types.js';\nimport { extractFrames } from './compositor.js';\nimport { stitchClips } from './stitch.js';\n\nexport function createFrameWorker(config: FrameWorkerConfig = {}): FrameWorker {\n const fps = config.fps ?? 30;\n const width = config.width ?? 1280;\n const height = config.height ?? 720;\n\n let _backend: RendererBackend | null = config.backend ?? null;\n\n async function getBackend(): Promise<RendererBackend> {\n if (!_backend) {\n const { createFFmpegBackend } = await import('./backends/ffmpeg.js');\n _backend = createFFmpegBackend();\n }\n await _backend.init();\n return _backend;\n }\n\n async function render(clip: ClipInput, options: RenderOptions = {}): Promise<Blob> {\n const mergedOpts: RenderOptions = { fps, width, height, ...options };\n const backend = await getBackend();\n\n const onProgress = mergedOpts.onProgress;\n const frames = await extractFrames(clip, {\n ...mergedOpts,\n onProgress: onProgress ? (p) => onProgress(p * 0.85) : undefined,\n });\n\n return backend.encode(frames, {\n width: mergedOpts.width ?? width,\n height: mergedOpts.height ?? height,\n fps: mergedOpts.fps ?? fps,\n mimeType: mergedOpts.mimeType ?? 'video/mp4',\n quality: mergedOpts.quality ?? 0.92,\n encoderOptions: mergedOpts.encoderOptions,\n onProgress: onProgress ? (p) => onProgress(0.85 + p * 0.15) : undefined,\n signal: mergedOpts.signal,\n });\n }\n\n async function renderToUrl(clip: ClipInput, options?: RenderOptions): Promise<string> {\n const blob = await render(clip, options);\n return URL.createObjectURL(blob);\n }\n\n async function stitch(clips: ClipInput[], options: RenderOptions = {}): Promise<Blob> {\n const mergedOpts: RenderOptions = { fps, width, height, ...options };\n const backend = await getBackend();\n return stitchClips(clips, backend, mergedOpts);\n }\n\n async function stitchToUrl(clips: ClipInput[], options?: RenderOptions): Promise<string> {\n const blob = await stitch(clips, options);\n return URL.createObjectURL(blob);\n }\n\n return { render, renderToUrl, stitch, stitchToUrl };\n}\n","import type { ClipInput, FrameData, RenderOptions } from './types.js';\nimport { STYLE_PRESETS, mergeStyle, getActiveCaptions, renderCaption } from './captions.js';\n\nconst ASPECT_RATIO_MAP: Record<string, [number, number]> = {\n '16:9': [16, 9],\n '9:16': [9, 16],\n '1:1': [1, 1],\n '4:3': [4, 3],\n '3:4': [3, 4],\n original: [0, 0],\n};\n\nfunction resolveOutputDimensions(\n clip: ClipInput,\n videoWidth: number,\n videoHeight: number,\n options: RenderOptions\n): [number, number] {\n const ar = clip.aspectRatio ?? 'original';\n const ratio = ASPECT_RATIO_MAP[ar] ?? [0, 0];\n\n if (ratio[0] === 0) {\n return [options.width ?? videoWidth, options.height ?? videoHeight];\n }\n\n const w = options.width ?? 1280;\n const h = Math.round(w * (ratio[1] / ratio[0]));\n return [w, h];\n}\n\nexport async function extractFrames(\n clip: ClipInput,\n options: RenderOptions\n): Promise<FrameData[]> {\n const fps = options.fps ?? 30;\n const onProgress = options.onProgress;\n const signal = options.signal;\n\n let srcUrl: string;\n let needsRevoke = false;\n\n if (typeof clip.source === 'string') {\n srcUrl = clip.source;\n } else if (clip.source instanceof HTMLVideoElement) {\n srcUrl = clip.source.src;\n } else {\n srcUrl = URL.createObjectURL(clip.source as Blob);\n needsRevoke = true;\n }\n\n const video = document.createElement('video');\n video.muted = true;\n video.crossOrigin = 'anonymous';\n video.preload = 'auto';\n\n await new Promise<void>((resolve, reject) => {\n video.onloadedmetadata = () => resolve();\n video.onerror = () => reject(new Error(`Failed to load video: ${srcUrl}`));\n video.src = srcUrl;\n });\n\n const duration = video.duration;\n const startTime = clip.startTime ?? 0;\n const endTime = clip.endTime ?? duration;\n const clipDuration = endTime - startTime;\n\n const [outW, outH] = resolveOutputDimensions(\n clip,\n video.videoWidth,\n video.videoHeight,\n options\n );\n\n const canvas = document.createElement('canvas');\n canvas.width = outW;\n canvas.height = outH;\n const ctx = canvas.getContext('2d', { willReadFrequently: true })!;\n\n const totalFrames = Math.ceil(clipDuration * fps);\n const frames: FrameData[] = [];\n\n const captionSegments = clip.captions?.segments ?? [];\n const baseStylePreset = clip.captions?.style?.preset ?? 'modern';\n const baseStyle = mergeStyle(\n STYLE_PRESETS[baseStylePreset],\n clip.captions?.style\n );\n\n for (let i = 0; i < totalFrames; i++) {\n if (signal?.aborted) throw new DOMException('Render cancelled', 'AbortError');\n\n const t = startTime + (i / fps);\n\n await seekVideo(video, t);\n ctx.clearRect(0, 0, outW, outH);\n\n drawVideoFrame(ctx, video, clip, outW, outH);\n\n if (captionSegments.length > 0) {\n const active = getActiveCaptions(captionSegments, t - startTime);\n for (const seg of active) {\n const segStyle = mergeStyle(baseStyle, seg.style);\n renderCaption(ctx, seg, segStyle, outW, outH);\n }\n }\n\n const imageData = ctx.getImageData(0, 0, outW, outH);\n frames.push({ imageData, timestamp: t - startTime, width: outW, height: outH });\n\n if (onProgress) onProgress(i / totalFrames);\n }\n\n if (needsRevoke) URL.revokeObjectURL(srcUrl);\n\n return frames;\n}\n\nfunction drawVideoFrame(\n ctx: CanvasRenderingContext2D,\n video: HTMLVideoElement,\n clip: ClipInput,\n outW: number,\n outH: number\n): void {\n const vw = video.videoWidth;\n const vh = video.videoHeight;\n\n if (clip.crop) {\n const { x, y, width, height } = clip.crop;\n ctx.drawImage(\n video,\n x * vw, y * vh, width * vw, height * vh,\n 0, 0, outW, outH\n );\n } else {\n const videoAR = vw / vh;\n const outAR = outW / outH;\n\n let sx = 0, sy = 0, sw = vw, sh = vh;\n if (videoAR > outAR) {\n sw = vh * outAR;\n sx = (vw - sw) / 2;\n } else if (videoAR < outAR) {\n sh = vw / outAR;\n sy = (vh - sh) / 2;\n }\n ctx.drawImage(video, sx, sy, sw, sh, 0, 0, outW, outH);\n }\n}\n\nfunction seekVideo(video: HTMLVideoElement, time: number): Promise<void> {\n return new Promise((resolve) => {\n if (Math.abs(video.currentTime - time) < 0.001) {\n resolve();\n return;\n }\n const onSeeked = () => {\n video.removeEventListener('seeked', onSeeked);\n resolve();\n };\n video.addEventListener('seeked', onSeeked);\n video.currentTime = time;\n });\n}\n","import type { ClipInput, RenderOptions, RendererBackend } from './types.js';\nimport { extractFrames } from './compositor.js';\n\nexport async function stitchClips(\n clips: ClipInput[],\n backend: RendererBackend,\n options: RenderOptions\n): Promise<Blob> {\n const fps = options.fps ?? 30;\n const width = options.width ?? 1280;\n const height = options.height ?? 720;\n const onProgress = options.onProgress;\n\n const blobs: Blob[] = [];\n\n for (let ci = 0; ci < clips.length; ci++) {\n const clip = clips[ci];\n const clipProgress = (p: number) => {\n onProgress?.(((ci + p * 0.9) / clips.length));\n };\n\n const frames = await extractFrames(clip, {\n ...options,\n width,\n height,\n fps,\n onProgress: clipProgress,\n });\n\n const blob = await backend.encode(frames, {\n width,\n height,\n fps,\n mimeType: options.mimeType ?? 'video/mp4',\n quality: options.quality ?? 0.92,\n encoderOptions: options.encoderOptions,\n onProgress: (p) => clipProgress(0.9 + p * 0.1),\n signal: options.signal,\n });\n\n blobs.push(blob);\n }\n\n if (blobs.length === 1) {\n onProgress?.(1);\n return blobs[0];\n }\n\n return backend.concat(blobs, {\n width,\n height,\n fps,\n mimeType: options.mimeType ?? 'video/mp4',\n quality: options.quality ?? 0.92,\n onProgress: (p) => onProgress?.((clips.length - 1 + p) / clips.length),\n signal: options.signal,\n });\n}\n"]}
|