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 CHANGED
@@ -8,13 +8,15 @@
8
8
 
9
9
  ## Features
10
10
 
11
- - **Trim** any video to a time range
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
- - **Stitch** multiple clips into one
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, reports progress 0–1
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. If you supply your own backend you don't need them.
27
+ > `@ffmpeg/ffmpeg` and `@ffmpeg/util` are optional peer dependencies required only by the default ffmpeg.wasm backend.
26
28
 
27
- ## Quick Start
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 { createFrameWorker } from 'framewebworker';
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 fw = createFrameWorker();
75
+ const url = URL.createObjectURL(blob);
76
+ ```
77
+
78
+ ### With per-segment captions
33
79
 
34
- const blob = await fw.render({
35
- source: 'https://example.com/my-video.mp4',
36
- startTime: 5,
37
- endTime: 15,
38
- captions: {
39
- segments: [
40
- { text: 'Hello world', startTime: 0, endTime: 3 },
41
- { text: 'This is FrameWorker', startTime: 3, endTime: 5 },
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
- width: 1280,
47
- height: 720,
48
- fps: 30,
49
- onProgress: (p) => console.log(`${Math.round(p * 100)}%`),
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
- const url = URL.createObjectURL(blob);
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
- ## React Example
125
+ ---
56
126
 
57
- ```tsx
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 { useRender } from 'framewebworker/react';
133
+ import type { ClipSource } from 'framewebworker';
60
134
 
61
135
  const fw = createFrameWorker();
62
136
 
63
- export function ExportButton({ videoFile }: { videoFile: File }) {
64
- const { render, isRendering, progress, url, error } = useRender(fw);
65
-
66
- const handleExport = async () => {
67
- await render({
68
- source: videoFile,
69
- startTime: 0,
70
- endTime: 30,
71
- captions: {
72
- segments: [{ text: 'My Clip', startTime: 0, endTime: 5 }],
73
- style: { preset: 'modern' },
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={handleExport} disabled={isRendering}>
81
- {isRendering ? `Exporting… ${Math.round(progress * 100)}%` : 'Export MP4'}
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="clip.mp4">Download</a>}
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
- ## Stitch Multiple Clips
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
- const blob = await fw.stitch([
94
- { source: fileA, startTime: 0, endTime: 10 },
95
- { source: fileB, startTime: 5, endTime: 20 },
96
- { source: fileC },
97
- ], { width: 1920, height: 1080 });
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
- ## BYOB: Bring Your Own Backend
377
+ ---
123
378
 
124
- Implement the `RendererBackend` interface to use any encoder:
379
+ ## `createFrameWorker` API reference
125
380
 
126
381
  ```ts
127
- import type { RendererBackend, FrameData, EncodeOptions } from 'framewebworker';
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({ backend: myBackend });
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
- | `render` | `(clip, opts?) => Promise<Blob>` | Render one clip |
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
- ### `ClipInput`
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
- ### `RenderOptions`
411
+ ### `Segment`
179
412
 
180
413
  | Field | Type | Description |
181
414
  |-------|------|-------------|
182
- | `width` | `number` | Output width in pixels |
183
- | `height` | `number` | Output height in pixels |
184
- | `fps` | `number` | Frames per second |
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
- ### React Hooks (`framewebworker/react`)
419
+ ---
191
420
 
192
- #### `useRender(frameWorker)`
421
+ ## BYOB: Bring Your Own Backend
193
422
 
194
423
  ```ts
195
- const { progress, isRendering, error, blob, url, render, cancel, reset } = useRender(fw);
196
- ```
424
+ import type { RendererBackend, FrameData, EncodeOptions } from 'framewebworker';
197
425
 
198
- #### `useStitch(frameWorker)`
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
- ```ts
201
- const { progress, isRendering, error, blob, url, stitch, cancel, reset } = useStitch(fw);
435
+ const fw = createFrameWorker({ backend: myBackend });
202
436
  ```
203
437
 
204
- Both hooks expose:
205
- - `progress` — number 0–1
206
- - `isRendering` boolean
207
- - `error` — `Error | null`
208
- - `blob` the output `Blob | null`
209
- - `url` — `string | null` (object URL, auto-revoked on next render)
210
- - `cancel()` abort the current render
211
- - `reset()` clear state and revoke URL
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 render(videoUrl, segments, options) {
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 renderToUrl(videoUrl, segments, options) {
874
- const { blob, metrics } = await render(videoUrl, segments, options);
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 stitch(clips, options = {}) {
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 stitchToUrl(clips, options) {
921
- const { blob, metrics } = await stitch(clips, options);
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 { render: render2, renderToUrl: renderToUrl2, stitch, stitchToUrl };
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