framewebworker 0.1.4 → 0.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/README.md +357 -107
- package/dist/index.cjs +21 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -19
- package/dist/index.d.ts +41 -19
- package/dist/index.js +20 -9
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +69 -72
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +58 -39
- package/dist/react/index.d.ts +58 -39
- package/dist/react/index.js +65 -71
- package/dist/react/index.js.map +1 -1
- package/dist/render.cjs +861 -0
- package/dist/render.cjs.map +1 -0
- package/dist/render.js +855 -0
- package/dist/render.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
- **
|
|
11
|
+
- **Export segments** from a single source video with `exportClips()`
|
|
12
|
+
- **Merge clips** from multiple source videos with `mergeClips()`
|
|
12
13
|
- **Overlay captions** with built-in style presets (`hormozi`, `modern`, `minimal`, `bold`)
|
|
13
|
-
- **
|
|
14
|
+
- **Parallel rendering** via OffscreenCanvas + Web Workers — automatic on supported browsers
|
|
15
|
+
- **Timing metrics** — per-segment extraction/encoding times, overall FPS throughput
|
|
14
16
|
- **Pluggable renderer backend** (default: ffmpeg.wasm)
|
|
15
17
|
- **Framework-agnostic core** + React hooks (`framewebworker/react`)
|
|
16
18
|
- **TypeScript-first** with full type exports
|
|
17
|
-
- Respects `AbortSignal` for cancellation
|
|
19
|
+
- Respects `AbortSignal` for cancellation
|
|
18
20
|
|
|
19
21
|
## Install
|
|
20
22
|
|
|
@@ -22,81 +24,334 @@
|
|
|
22
24
|
npm install framewebworker @ffmpeg/ffmpeg @ffmpeg/util
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
> `@ffmpeg/ffmpeg` and `@ffmpeg/util` are optional peer dependencies required only by the default ffmpeg.wasm backend.
|
|
27
|
+
> `@ffmpeg/ffmpeg` and `@ffmpeg/util` are optional peer dependencies required only by the default ffmpeg.wasm backend.
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Which API should I use?
|
|
32
|
+
|
|
33
|
+
| | `exportClips()` | `mergeClips()` |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| **Source videos** | One URL, multiple time ranges | Multiple clips, each with its own source |
|
|
36
|
+
| **Best for** | Highlight reels, chapter exports, clip editors | Joining footage from different files |
|
|
37
|
+
| **Video loading** | Loads the source once, seeks per segment | Loads each source independently |
|
|
38
|
+
| **React hook** | `useExportClips(videoUrl, segments)` | `useMergeClips(fw)` |
|
|
39
|
+
|
|
40
|
+
Both produce a single concatenated MP4 `Blob` and return `RenderMetrics`.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## `exportClips()` — One video, multiple time segments
|
|
45
|
+
|
|
46
|
+
Use this when you're exporting multiple time ranges from the **same source file**.
|
|
28
47
|
|
|
29
48
|
```ts
|
|
30
|
-
import {
|
|
49
|
+
import { exportClips } from 'framewebworker';
|
|
50
|
+
import type { Segment, ExportOptions } from 'framewebworker';
|
|
51
|
+
|
|
52
|
+
const segments: Segment[] = [
|
|
53
|
+
{ start: 10, end: 25 },
|
|
54
|
+
{ start: 42, end: 58 },
|
|
55
|
+
{ start: 90, end: 110 },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const { blob, metrics } = await exportClips(
|
|
59
|
+
'https://example.com/interview.mp4',
|
|
60
|
+
segments,
|
|
61
|
+
{
|
|
62
|
+
width: 1280,
|
|
63
|
+
height: 720,
|
|
64
|
+
fps: 30,
|
|
65
|
+
onProgress: ({ overall, clips }) => {
|
|
66
|
+
console.log(`Overall: ${Math.round(overall * 100)}%`);
|
|
67
|
+
clips.forEach(c => console.log(` segment ${c.index}: ${c.status}`));
|
|
68
|
+
},
|
|
69
|
+
onComplete: (m) => {
|
|
70
|
+
console.log(`Done in ${m.totalMs.toFixed(0)}ms — ${m.framesPerSecond.toFixed(1)} fps`);
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
);
|
|
31
74
|
|
|
32
|
-
const
|
|
75
|
+
const url = URL.createObjectURL(blob);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### With per-segment captions
|
|
33
79
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
80
|
+
Caption timestamps are absolute (matching the source video timeline):
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { exportClips } from 'framewebworker';
|
|
84
|
+
import type { Segment } from 'framewebworker';
|
|
85
|
+
|
|
86
|
+
const segments: Segment[] = [
|
|
87
|
+
{
|
|
88
|
+
start: 0,
|
|
89
|
+
end: 8,
|
|
90
|
+
captions: [
|
|
91
|
+
{ text: 'Welcome back', startTime: 0, endTime: 3 },
|
|
92
|
+
{ text: 'Today we cover...', startTime: 3, endTime: 8 },
|
|
42
93
|
],
|
|
43
|
-
style: { preset: 'hormozi' },
|
|
44
94
|
},
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
95
|
+
{
|
|
96
|
+
start: 45,
|
|
97
|
+
end: 60,
|
|
98
|
+
captions: [
|
|
99
|
+
{ text: 'The key insight', startTime: 45, endTime: 52 },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const { blob } = await exportClips('https://example.com/video.mp4', segments, {
|
|
105
|
+
width: 1080,
|
|
106
|
+
height: 1920, // 9:16 portrait
|
|
50
107
|
});
|
|
108
|
+
```
|
|
51
109
|
|
|
52
|
-
|
|
110
|
+
### `exportClipsToUrl()`
|
|
111
|
+
|
|
112
|
+
Convenience wrapper that returns an object URL directly:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { exportClipsToUrl } from 'framewebworker';
|
|
116
|
+
|
|
117
|
+
const { url, metrics } = await exportClipsToUrl(
|
|
118
|
+
'https://example.com/video.mp4',
|
|
119
|
+
[{ start: 5, end: 30 }]
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
videoElement.src = url;
|
|
53
123
|
```
|
|
54
124
|
|
|
55
|
-
|
|
125
|
+
---
|
|
56
126
|
|
|
57
|
-
|
|
127
|
+
## `mergeClips()` — Multiple source videos
|
|
128
|
+
|
|
129
|
+
Use this when joining clips from **different source files** via a `FrameWorker` instance.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
58
132
|
import { createFrameWorker } from 'framewebworker';
|
|
59
|
-
import {
|
|
133
|
+
import type { ClipSource } from 'framewebworker';
|
|
60
134
|
|
|
61
135
|
const fw = createFrameWorker();
|
|
62
136
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
137
|
+
const clips: ClipSource[] = [
|
|
138
|
+
{ source: fileA, startTime: 0, endTime: 10 },
|
|
139
|
+
{ source: fileB, startTime: 5, endTime: 20 },
|
|
140
|
+
{ source: fileC, startTime: 12, endTime: 25 },
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
const { blob, metrics } = await fw.mergeClips(clips, {
|
|
144
|
+
width: 1920,
|
|
145
|
+
height: 1080,
|
|
146
|
+
onProgress: ({ overall }) => console.log(`${Math.round(overall * 100)}%`),
|
|
147
|
+
onComplete: (m) => console.log(`${m.framesPerSecond.toFixed(1)} fps`),
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### `mergeClipsToUrl()`
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const { url, metrics } = await fw.mergeClipsToUrl(clips, { width: 1280, height: 720 });
|
|
155
|
+
videoElement.src = url;
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## React hooks
|
|
161
|
+
|
|
162
|
+
Import from `framewebworker/react`.
|
|
163
|
+
|
|
164
|
+
### `useExportClips` — single video, multiple segments
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
import { useExportClips } from 'framewebworker/react';
|
|
168
|
+
import type { Segment } from 'framewebworker';
|
|
169
|
+
|
|
170
|
+
const segments: Segment[] = [
|
|
171
|
+
{ start: 10, end: 25 },
|
|
172
|
+
{ start: 60, end: 80 },
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
export function HighlightExporter({ videoUrl }: { videoUrl: string }) {
|
|
176
|
+
const { start, cancel, isRendering, progress, metrics, url, error } = useExportClips(
|
|
177
|
+
videoUrl,
|
|
178
|
+
segments,
|
|
179
|
+
{ width: 1280, height: 720, fps: 30 }
|
|
180
|
+
);
|
|
77
181
|
|
|
78
182
|
return (
|
|
79
183
|
<div>
|
|
80
|
-
<button onClick={
|
|
81
|
-
{isRendering
|
|
184
|
+
<button onClick={start} disabled={isRendering}>
|
|
185
|
+
{isRendering
|
|
186
|
+
? `Rendering… ${Math.round((progress?.overall ?? 0) * 100)}%`
|
|
187
|
+
: 'Export'}
|
|
82
188
|
</button>
|
|
189
|
+
<button onClick={cancel} disabled={!isRendering}>Cancel</button>
|
|
190
|
+
|
|
191
|
+
{metrics && (
|
|
192
|
+
<p>
|
|
193
|
+
Done in {(metrics.totalMs / 1000).toFixed(1)}s —{' '}
|
|
194
|
+
{metrics.framesPerSecond.toFixed(1)} fps
|
|
195
|
+
</p>
|
|
196
|
+
)}
|
|
83
197
|
{error && <p style={{ color: 'red' }}>{error.message}</p>}
|
|
84
|
-
{url && <a href={url} download="
|
|
198
|
+
{url && <a href={url} download="highlight.mp4">Download</a>}
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`useExportClips` signature:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
function useExportClips(
|
|
208
|
+
videoUrl: string | null,
|
|
209
|
+
segments: Segment[],
|
|
210
|
+
options?: Omit<ExportOptions, 'onProgress' | 'onComplete' | 'signal'>
|
|
211
|
+
): {
|
|
212
|
+
start: () => void;
|
|
213
|
+
cancel: () => void;
|
|
214
|
+
isRendering: boolean;
|
|
215
|
+
progress: RichProgress | null;
|
|
216
|
+
metrics: RenderMetrics | null;
|
|
217
|
+
url: string | null;
|
|
218
|
+
error: Error | null;
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Passing `null` as `videoUrl` disables the hook; `start()` is a no-op until it is set.
|
|
223
|
+
|
|
224
|
+
### `useMergeClips` — multiple source clips
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
import { createFrameWorker } from 'framewebworker';
|
|
228
|
+
import { useMergeClips } from 'framewebworker/react';
|
|
229
|
+
|
|
230
|
+
const fw = createFrameWorker();
|
|
231
|
+
|
|
232
|
+
export function MergePanel() {
|
|
233
|
+
const { mergeClips, isRendering, progress, metrics, url } = useMergeClips(fw);
|
|
234
|
+
|
|
235
|
+
const handleExport = () =>
|
|
236
|
+
mergeClips([
|
|
237
|
+
{ source: fileA, startTime: 0, endTime: 10 },
|
|
238
|
+
{ source: fileB, startTime: 5, endTime: 20 },
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div>
|
|
243
|
+
<button onClick={handleExport} disabled={isRendering}>Export</button>
|
|
244
|
+
{progress && <progress value={progress.overall} />}
|
|
245
|
+
{metrics && <p>{metrics.framesPerSecond.toFixed(1)} fps</p>}
|
|
246
|
+
{url && <a href={url} download="output.mp4">Download</a>}
|
|
85
247
|
</div>
|
|
86
248
|
);
|
|
87
249
|
}
|
|
88
250
|
```
|
|
89
251
|
|
|
90
|
-
|
|
252
|
+
### `usePreviewClip` — single clip via FrameWorker instance
|
|
253
|
+
|
|
254
|
+
For rendering a single `ClipSource` through a `FrameWorker` instance:
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
import { createFrameWorker } from 'framewebworker';
|
|
258
|
+
import { usePreviewClip } from 'framewebworker/react';
|
|
259
|
+
import type { ClipSource } from 'framewebworker';
|
|
260
|
+
|
|
261
|
+
const fw = createFrameWorker();
|
|
262
|
+
|
|
263
|
+
export function ClipPreview({ file }: { file: File }) {
|
|
264
|
+
const { render, isRendering, progress, url } = usePreviewClip(fw);
|
|
265
|
+
|
|
266
|
+
const clip: ClipSource = { source: file, startTime: 0, endTime: 30 };
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<button onClick={() => render(clip)} disabled={isRendering}>
|
|
270
|
+
{isRendering ? `${Math.round(progress * 100)}%` : 'Preview clip'}
|
|
271
|
+
</button>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## `RenderMetrics` — timing output
|
|
279
|
+
|
|
280
|
+
Both `exportClips()` and `mergeClips()` resolve with `{ blob, metrics }`. `onComplete` also receives the same object.
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
interface RenderMetrics {
|
|
284
|
+
totalMs: number; // wall-clock time for the entire operation
|
|
285
|
+
extractionMs: number; // sum of all segment/clip frame-extraction times
|
|
286
|
+
encodingMs: number; // sum of all segment/clip ffmpeg encoding times
|
|
287
|
+
stitchMs: number; // time for the final ffmpeg concat pass
|
|
288
|
+
framesPerSecond: number; // total frames / (totalMs / 1000)
|
|
289
|
+
clips: ClipMetrics[]; // one entry per segment or clip
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
interface ClipMetrics {
|
|
293
|
+
clipId: string; // segment index (as string)
|
|
294
|
+
extractionMs: number;
|
|
295
|
+
encodingMs: number;
|
|
296
|
+
totalMs: number; // extractionMs + encodingMs
|
|
297
|
+
framesExtracted: number;
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Example output for a three-segment export:
|
|
91
302
|
|
|
92
303
|
```ts
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
304
|
+
{
|
|
305
|
+
totalMs: 4820,
|
|
306
|
+
extractionMs: 3100,
|
|
307
|
+
encodingMs: 1600,
|
|
308
|
+
stitchMs: 120,
|
|
309
|
+
framesPerSecond: 94.2,
|
|
310
|
+
clips: [
|
|
311
|
+
{ clipId: '0', extractionMs: 980, encodingMs: 510, totalMs: 1490, framesExtracted: 450 },
|
|
312
|
+
{ clipId: '1', extractionMs: 1050, encodingMs: 560, totalMs: 1610, framesExtracted: 480 },
|
|
313
|
+
{ clipId: '2', extractionMs: 1070, encodingMs: 530, totalMs: 1600, framesExtracted: 510 },
|
|
314
|
+
]
|
|
315
|
+
}
|
|
98
316
|
```
|
|
99
317
|
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## `ExportOptions` / `MergeOptions`
|
|
321
|
+
|
|
322
|
+
`ExportOptions` is accepted by `exportClips()` / `exportClipsToUrl()` / `useExportClips()`.
|
|
323
|
+
`MergeOptions` is accepted by `mergeClips()` / `mergeClipsToUrl()` / `useMergeClips()`.
|
|
324
|
+
Both have identical fields:
|
|
325
|
+
|
|
326
|
+
| Field | Type | Default | Description |
|
|
327
|
+
|-------|------|---------|-------------|
|
|
328
|
+
| `width` | `number` | `1280` | Output width in pixels |
|
|
329
|
+
| `height` | `number` | `720` | Output height in pixels |
|
|
330
|
+
| `fps` | `number` | `30` | Frames per second |
|
|
331
|
+
| `mimeType` | `string` | `'video/mp4'` | Output MIME type |
|
|
332
|
+
| `quality` | `number` | `0.92` | Quality 0–1 (non-ffmpeg backends) |
|
|
333
|
+
| `encoderOptions` | `Record<string, unknown>` | — | Extra options passed to the backend |
|
|
334
|
+
| `signal` | `AbortSignal` | — | Cancellation signal |
|
|
335
|
+
| `onProgress` | `(p: RichProgress) => void` | — | Called on every frame batch |
|
|
336
|
+
| `onComplete` | `(m: RenderMetrics) => void` | — | Called once when the final blob is ready |
|
|
337
|
+
|
|
338
|
+
`RichProgress` shape:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
interface RichProgress {
|
|
342
|
+
overall: number; // 0–1 weighted average across all segments/clips
|
|
343
|
+
clips: ClipProgress[];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
interface ClipProgress {
|
|
347
|
+
index: number;
|
|
348
|
+
status: 'pending' | 'rendering' | 'encoding' | 'done' | 'error';
|
|
349
|
+
progress: number; // 0–1
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
100
355
|
## Caption Style Presets
|
|
101
356
|
|
|
102
357
|
| Preset | Description |
|
|
@@ -119,51 +374,29 @@ captions: {
|
|
|
119
374
|
}
|
|
120
375
|
```
|
|
121
376
|
|
|
122
|
-
|
|
377
|
+
---
|
|
123
378
|
|
|
124
|
-
|
|
379
|
+
## `createFrameWorker` API reference
|
|
125
380
|
|
|
126
381
|
```ts
|
|
127
|
-
import
|
|
128
|
-
|
|
129
|
-
const myBackend: RendererBackend = {
|
|
130
|
-
name: 'my-encoder',
|
|
131
|
-
async init() {
|
|
132
|
-
// load WASM, warm up workers, etc.
|
|
133
|
-
},
|
|
134
|
-
async encode(frames: FrameData[], opts: EncodeOptions): Promise<Blob> {
|
|
135
|
-
// frames is FrameData[] with .imageData (ImageData), .timestamp, .width, .height
|
|
136
|
-
// return a video Blob
|
|
137
|
-
},
|
|
138
|
-
async concat(blobs: Blob[], opts: EncodeOptions): Promise<Blob> {
|
|
139
|
-
// concatenate multiple video Blobs
|
|
140
|
-
},
|
|
141
|
-
};
|
|
382
|
+
import { createFrameWorker } from 'framewebworker';
|
|
142
383
|
|
|
143
|
-
const fw = createFrameWorker({
|
|
384
|
+
const fw = createFrameWorker({
|
|
385
|
+
backend: myBackend, // optional, defaults to ffmpeg.wasm
|
|
386
|
+
fps: 30,
|
|
387
|
+
width: 1280,
|
|
388
|
+
height: 720,
|
|
389
|
+
});
|
|
144
390
|
```
|
|
145
391
|
|
|
146
|
-
## API Reference
|
|
147
|
-
|
|
148
|
-
### `createFrameWorker(config?)`
|
|
149
|
-
|
|
150
|
-
| Option | Type | Default | Description |
|
|
151
|
-
|--------|------|---------|-------------|
|
|
152
|
-
| `backend` | `RendererBackend` | ffmpeg.wasm | Encoder backend |
|
|
153
|
-
| `fps` | `number` | `30` | Default frame rate |
|
|
154
|
-
| `width` | `number` | `1280` | Default output width |
|
|
155
|
-
| `height` | `number` | `720` | Default output height |
|
|
156
|
-
|
|
157
|
-
Returns a `FrameWorker` object:
|
|
158
|
-
|
|
159
392
|
| Method | Signature | Description |
|
|
160
393
|
|--------|-----------|-------------|
|
|
161
|
-
| `
|
|
394
|
+
| `mergeClips` | `(clips[], opts?) => Promise<{ blob, metrics }>` | Merge multiple `ClipSource`s |
|
|
395
|
+
| `mergeClipsToUrl` | `(clips[], opts?) => Promise<{ url, metrics }>` | Merge + create object URL |
|
|
396
|
+
| `render` | `(clip, opts?) => Promise<Blob>` | Render a single `ClipSource` (preview use) |
|
|
162
397
|
| `renderToUrl` | `(clip, opts?) => Promise<string>` | Render + create object URL |
|
|
163
|
-
| `stitch` | `(clips[], opts?) => Promise<Blob>` | Render + concat clips |
|
|
164
|
-
| `stitchToUrl` | `(clips[], opts?) => Promise<string>` | Stitch + create object URL |
|
|
165
398
|
|
|
166
|
-
### `
|
|
399
|
+
### `ClipSource`
|
|
167
400
|
|
|
168
401
|
| Field | Type | Description |
|
|
169
402
|
|-------|------|-------------|
|
|
@@ -175,50 +408,67 @@ Returns a `FrameWorker` object:
|
|
|
175
408
|
| `aspectRatio` | `AspectRatio` | `'16:9' \| '9:16' \| '1:1' \| '4:3' \| '3:4' \| 'original'` |
|
|
176
409
|
| `volume` | `number` | Volume multiplier 0–2 |
|
|
177
410
|
|
|
178
|
-
### `
|
|
411
|
+
### `Segment`
|
|
179
412
|
|
|
180
413
|
| Field | Type | Description |
|
|
181
414
|
|-------|------|-------------|
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
| `mimeType` | `string` | Output MIME type |
|
|
186
|
-
| `quality` | `number` | Quality 0–1 (non-ffmpeg backends) |
|
|
187
|
-
| `onProgress` | `(p: number) => void` | Progress callback 0–1 |
|
|
188
|
-
| `signal` | `AbortSignal` | Cancellation signal |
|
|
415
|
+
| `start` | `number` | Start time in seconds (absolute, within the source video) |
|
|
416
|
+
| `end` | `number` | End time in seconds |
|
|
417
|
+
| `captions` | `CaptionSegment[]` | Captions to overlay (timestamps are absolute) |
|
|
189
418
|
|
|
190
|
-
|
|
419
|
+
---
|
|
191
420
|
|
|
192
|
-
|
|
421
|
+
## BYOB: Bring Your Own Backend
|
|
193
422
|
|
|
194
423
|
```ts
|
|
195
|
-
|
|
196
|
-
```
|
|
424
|
+
import type { RendererBackend, FrameData, EncodeOptions } from 'framewebworker';
|
|
197
425
|
|
|
198
|
-
|
|
426
|
+
const myBackend: RendererBackend = {
|
|
427
|
+
name: 'my-encoder',
|
|
428
|
+
async init() { /* load WASM, warm up workers, etc. */ },
|
|
429
|
+
async encode(frames: FrameData[], opts: EncodeOptions): Promise<Blob> {
|
|
430
|
+
// frames[].imageData (ImageData), .timestamp, .width, .height
|
|
431
|
+
},
|
|
432
|
+
async concat(blobs: Blob[], opts: EncodeOptions): Promise<Blob> { /* ... */ },
|
|
433
|
+
};
|
|
199
434
|
|
|
200
|
-
|
|
201
|
-
const { progress, isRendering, error, blob, url, stitch, cancel, reset } = useStitch(fw);
|
|
435
|
+
const fw = createFrameWorker({ backend: myBackend });
|
|
202
436
|
```
|
|
203
437
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## Migration from v0.1
|
|
441
|
+
|
|
442
|
+
| v0.1 | v0.2 | Notes |
|
|
443
|
+
|------|------|-------|
|
|
444
|
+
| `render(videoUrl, segments)` | `exportClips(videoUrl, segments)` | Deprecated alias kept |
|
|
445
|
+
| `renderToUrl(videoUrl, segments)` | `exportClipsToUrl(videoUrl, segments)` | Deprecated alias kept |
|
|
446
|
+
| `fw.stitch(clips)` | `fw.mergeClips(clips)` | Deprecated alias kept on FrameWorker |
|
|
447
|
+
| `fw.stitchToUrl(clips)` | `fw.mergeClipsToUrl(clips)` | Deprecated alias kept on FrameWorker |
|
|
448
|
+
| `useRender(videoUrl, segments)` | `useExportClips(videoUrl, segments)` | Deprecated alias kept |
|
|
449
|
+
| `useStitch(fw)` | `useMergeClips(fw)` | Deprecated alias kept |
|
|
450
|
+
| `useClipRender(fw)` | `usePreviewClip(fw)` | Deprecated alias kept |
|
|
451
|
+
| `StitchOptions` | `MergeOptions` | Deprecated type alias kept |
|
|
452
|
+
| `SingleVideoRenderOptions` | `ExportOptions` | Deprecated type alias kept |
|
|
453
|
+
| `ClipInput` | `ClipSource` | Deprecated type alias kept |
|
|
454
|
+
|
|
455
|
+
All v0.1 names emit a `@deprecated` JSDoc warning in editors but continue to work. They will be removed in v0.3.
|
|
456
|
+
|
|
457
|
+
---
|
|
212
458
|
|
|
213
459
|
## Browser Requirements
|
|
214
460
|
|
|
215
|
-
- Chrome/Edge 94+ or Firefox 90+ (OffscreenCanvas, WASM)
|
|
461
|
+
- Chrome/Edge 94+ or Firefox 90+ (OffscreenCanvas, Web Workers, WASM)
|
|
216
462
|
- COOP/COEP headers required for ffmpeg.wasm SharedArrayBuffer:
|
|
217
463
|
```
|
|
218
464
|
Cross-Origin-Opener-Policy: same-origin
|
|
219
465
|
Cross-Origin-Embedder-Policy: require-corp
|
|
220
466
|
```
|
|
221
467
|
|
|
468
|
+
Browsers without `OffscreenCanvas` or `Worker` support fall back to sequential single-threaded rendering automatically.
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
222
472
|
## License
|
|
223
473
|
|
|
224
474
|
MIT © nareshipme
|
package/dist/index.cjs
CHANGED
|
@@ -528,7 +528,7 @@ var WorkerPool = class {
|
|
|
528
528
|
this.available = [];
|
|
529
529
|
this.waiters = [];
|
|
530
530
|
for (let i = 0; i < maxConcurrency; i++) {
|
|
531
|
-
const w = new Worker(new URL("./render-worker.js", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))), { type: "module" });
|
|
531
|
+
const w = new Worker(new URL("./worker/render-worker.js", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))), { type: "module" });
|
|
532
532
|
this.workers.push(w);
|
|
533
533
|
this.available.push(w);
|
|
534
534
|
}
|
|
@@ -864,16 +864,18 @@ function segmentsToClips(videoUrl, segments) {
|
|
|
864
864
|
captions: seg.captions?.length ? { segments: seg.captions } : void 0
|
|
865
865
|
}));
|
|
866
866
|
}
|
|
867
|
-
async function
|
|
867
|
+
async function exportClips(videoUrl, segments, options) {
|
|
868
868
|
const clips = segmentsToClips(videoUrl, segments);
|
|
869
869
|
const backend = createFFmpegBackend();
|
|
870
870
|
await backend.init();
|
|
871
871
|
return stitchClips(clips, backend, options ?? {});
|
|
872
872
|
}
|
|
873
|
-
async function
|
|
874
|
-
const { blob, metrics } = await
|
|
873
|
+
async function exportClipsToUrl(videoUrl, segments, options) {
|
|
874
|
+
const { blob, metrics } = await exportClips(videoUrl, segments, options);
|
|
875
875
|
return { url: URL.createObjectURL(blob), metrics };
|
|
876
876
|
}
|
|
877
|
+
var render = exportClips;
|
|
878
|
+
var renderToUrl = exportClipsToUrl;
|
|
877
879
|
|
|
878
880
|
// src/index.ts
|
|
879
881
|
function createFrameWorker(config = {}) {
|
|
@@ -912,21 +914,32 @@ function createFrameWorker(config = {}) {
|
|
|
912
914
|
const blob = await render2(clip, options);
|
|
913
915
|
return URL.createObjectURL(blob);
|
|
914
916
|
}
|
|
915
|
-
async function
|
|
917
|
+
async function mergeClips(clips, options = {}) {
|
|
916
918
|
const mergedOpts = { fps, width, height, ...options };
|
|
917
919
|
const backend = await getBackend();
|
|
918
920
|
return stitchClips(clips, backend, mergedOpts);
|
|
919
921
|
}
|
|
920
|
-
async function
|
|
921
|
-
const { blob, metrics } = await
|
|
922
|
+
async function mergeClipsToUrl(clips, options) {
|
|
923
|
+
const { blob, metrics } = await mergeClips(clips, options);
|
|
922
924
|
return { url: URL.createObjectURL(blob), metrics };
|
|
923
925
|
}
|
|
924
|
-
return {
|
|
926
|
+
return {
|
|
927
|
+
render: render2,
|
|
928
|
+
renderToUrl: renderToUrl2,
|
|
929
|
+
mergeClips,
|
|
930
|
+
mergeClipsToUrl,
|
|
931
|
+
/** @deprecated Use mergeClips() */
|
|
932
|
+
stitch: mergeClips,
|
|
933
|
+
/** @deprecated Use mergeClipsToUrl() */
|
|
934
|
+
stitchToUrl: mergeClipsToUrl
|
|
935
|
+
};
|
|
925
936
|
}
|
|
926
937
|
|
|
927
938
|
exports.STYLE_PRESETS = STYLE_PRESETS;
|
|
928
939
|
exports.createFFmpegBackend = createFFmpegBackend;
|
|
929
940
|
exports.createFrameWorker = createFrameWorker;
|
|
941
|
+
exports.exportClips = exportClips;
|
|
942
|
+
exports.exportClipsToUrl = exportClipsToUrl;
|
|
930
943
|
exports.render = render;
|
|
931
944
|
exports.renderToUrl = renderToUrl;
|
|
932
945
|
//# sourceMappingURL=index.cjs.map
|