@bendyline/squisq-video-react 2.0.2 → 2.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/COPYING.GPL-2.0.txt +339 -0
- package/LICENSE +21 -0
- package/NOTICE.md +39 -0
- package/README.md +34 -7
- package/dist/chunk-KQG6DZ7G.js +549 -0
- package/dist/chunk-VI5AIVOQ.js +1951 -0
- package/dist/chunk-VILZP3GL.js +856 -0
- package/dist/chunk-YZN7D4IC.js +300 -0
- package/dist/components/index.d.ts +90 -0
- package/dist/components/index.js +11 -0
- package/dist/encoder/index.d.ts +95 -0
- package/dist/encoder/index.js +13 -0
- package/dist/hooks/index.d.ts +32 -0
- package/dist/hooks/index.js +10 -0
- package/dist/index.d.ts +8 -277
- package/dist/index.js +12 -1503
- package/dist/useVideoExport-NtqZVgaz.d.ts +115 -0
- package/dist/workers/encode.worker.js +37 -20
- package/dist/workers/ffmpeg.class-worker.js +0 -1
- package/package.json +26 -8
- package/dist/chunk-6DKLHXX3.js +0 -47
- package/dist/chunk-6DKLHXX3.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/workers/encode.worker.js.map +0 -1
- package/dist/workers/ffmpeg.class-worker.js.map +0 -1
- package/src/VideoExportButton.tsx +0 -87
- package/src/VideoExportModal.tsx +0 -553
- package/src/__tests__/audioTrack.test.ts +0 -108
- package/src/__tests__/gifTranscode.test.ts +0 -118
- package/src/__tests__/mp4MuxAudio.test.ts +0 -72
- package/src/__tests__/useVideoExportGif.test.ts +0 -155
- package/src/__tests__/videoExportProps.test.tsx +0 -148
- package/src/__tests__/workerEncoder.test.ts +0 -143
- package/src/audioTrack.ts +0 -332
- package/src/gifTranscode.ts +0 -75
- package/src/hooks/useFrameCapture.ts +0 -268
- package/src/hooks/useVideoExport.ts +0 -647
- package/src/index.ts +0 -40
- package/src/mainThreadEncoder.ts +0 -158
- package/src/mp4Mux.ts +0 -123
- package/src/workerEncoder.ts +0 -175
- package/src/workers/encode.worker.ts +0 -439
- package/src/workers/ffmpeg.class-worker.ts +0 -11
- package/src/workers/workerTypes.ts +0 -89
package/src/mainThreadEncoder.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Main-thread WebCodecs encoder.
|
|
3
|
-
*
|
|
4
|
-
* Encodes video frames to MP4 using the WebCodecs API and mp4-muxer,
|
|
5
|
-
* running directly on the main thread. This is simpler and avoids
|
|
6
|
-
* worker module-resolution issues with bundlers. Since frame capture
|
|
7
|
-
* via html2canvas (~100-200ms per frame) is the bottleneck — not
|
|
8
|
-
* encoding (~1ms per frame with hardware-accelerated WebCodecs) —
|
|
9
|
-
* worker offloading provides minimal benefit.
|
|
10
|
-
*
|
|
11
|
-
* Requirements: Chrome 94+ / Edge 94+ (WebCodecs support).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { bitrateForQuality, validateVideoExportOptions } from '@bendyline/squisq-video';
|
|
15
|
-
|
|
16
|
-
import { createMp4Muxer, type Mp4MuxerHandle } from './mp4Mux.js';
|
|
17
|
-
|
|
18
|
-
export interface EncoderConfig {
|
|
19
|
-
width: number;
|
|
20
|
-
height: number;
|
|
21
|
-
fps: number;
|
|
22
|
-
quality: 'draft' | 'normal' | 'high';
|
|
23
|
-
/**
|
|
24
|
-
* When present, the underlying muxer is configured with an AAC audio track
|
|
25
|
-
* and {@link MainThreadEncoder.addAudioChunk} becomes usable. Absent → the
|
|
26
|
-
* encoder produces a video-only MP4 exactly as before.
|
|
27
|
-
*/
|
|
28
|
-
audio?: {
|
|
29
|
-
numberOfChannels: number;
|
|
30
|
-
sampleRate: number;
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface MainThreadEncoder {
|
|
35
|
-
/** Encode a single frame. The bitmap is closed after encoding. */
|
|
36
|
-
encodeFrame(bitmap: ImageBitmap, frameIndex: number): Promise<void>;
|
|
37
|
-
/**
|
|
38
|
-
* Hand an encoded audio chunk (from a WebCodecs `AudioEncoder`) to the muxer.
|
|
39
|
-
* Only valid when the encoder was created with an `audio` config; otherwise a
|
|
40
|
-
* no-op. Must be called before {@link finalize}.
|
|
41
|
-
*/
|
|
42
|
-
addAudioChunk?(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata): void;
|
|
43
|
-
/** Flush pending frames and finalize the MP4. Returns the MP4 ArrayBuffer. */
|
|
44
|
-
finalize(): Promise<ArrayBuffer>;
|
|
45
|
-
/** Close the encoder without producing output (e.g., on cancel). */
|
|
46
|
-
close(): void;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Check whether the browser supports WebCodecs video encoding.
|
|
51
|
-
*/
|
|
52
|
-
export function supportsWebCodecs(): boolean {
|
|
53
|
-
return typeof VideoEncoder !== 'undefined' && typeof VideoFrame !== 'undefined';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Probe whether the WebCodecs encoder actually supports the H.264 profile
|
|
58
|
-
* we use. The `VideoEncoder` global can exist while the specific codec is
|
|
59
|
-
* unavailable — this is the case on Linux Chromium, which ships without
|
|
60
|
-
* the proprietary H.264 encoder.
|
|
61
|
-
*/
|
|
62
|
-
export async function supportsWebCodecsH264(config: EncoderConfig): Promise<boolean> {
|
|
63
|
-
if (!supportsWebCodecs()) return false;
|
|
64
|
-
try {
|
|
65
|
-
const support = await VideoEncoder.isConfigSupported({
|
|
66
|
-
codec: 'avc1.640028',
|
|
67
|
-
width: config.width,
|
|
68
|
-
height: config.height,
|
|
69
|
-
bitrate: bitrateForQuality(config.quality, config.width, config.height),
|
|
70
|
-
framerate: config.fps,
|
|
71
|
-
});
|
|
72
|
-
return support.supported === true;
|
|
73
|
-
} catch {
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Create a main-thread WebCodecs encoder.
|
|
80
|
-
*
|
|
81
|
-
* Throws if WebCodecs is not available.
|
|
82
|
-
*/
|
|
83
|
-
export function createEncoder(config: EncoderConfig): MainThreadEncoder {
|
|
84
|
-
validateVideoExportOptions(config);
|
|
85
|
-
if (!supportsWebCodecs()) {
|
|
86
|
-
throw new Error(
|
|
87
|
-
'WebCodecs is not available in this browser. ' +
|
|
88
|
-
'Video export requires Chrome 94+, Edge 94+, or another Chromium-based browser.',
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const muxer: Mp4MuxerHandle = createMp4Muxer({
|
|
93
|
-
width: config.width,
|
|
94
|
-
height: config.height,
|
|
95
|
-
fps: config.fps,
|
|
96
|
-
...(config.audio ? { audio: config.audio } : {}),
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
let closed = false;
|
|
100
|
-
const frameDuration = 1_000_000 / config.fps; // microseconds per frame
|
|
101
|
-
|
|
102
|
-
const encoder = new VideoEncoder({
|
|
103
|
-
output(chunk, meta) {
|
|
104
|
-
if (closed) return;
|
|
105
|
-
muxer.addVideoChunk(chunk, meta ?? undefined);
|
|
106
|
-
},
|
|
107
|
-
error(err) {
|
|
108
|
-
console.error('WebCodecs encoder error:', err.message);
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
encoder.configure({
|
|
113
|
-
// Deliberate profile split from the fallback worker (avc1.42001f, Baseline):
|
|
114
|
-
// this primary WebCodecs path targets H.264 High@4.0 for better quality up
|
|
115
|
-
// to 1080p; the wasm-fallback worker uses Baseline for max decoder compat.
|
|
116
|
-
codec: 'avc1.640028', // H.264 High profile, level 4.0 (supports up to 1080p)
|
|
117
|
-
width: config.width,
|
|
118
|
-
height: config.height,
|
|
119
|
-
bitrate: bitrateForQuality(config.quality, config.width, config.height),
|
|
120
|
-
framerate: config.fps,
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
async encodeFrame(bitmap: ImageBitmap, frameIndex: number): Promise<void> {
|
|
125
|
-
if (closed) {
|
|
126
|
-
bitmap.close();
|
|
127
|
-
throw new Error('Encoder already closed');
|
|
128
|
-
}
|
|
129
|
-
const timestamp = Math.round(frameIndex * frameDuration);
|
|
130
|
-
const frame = new VideoFrame(bitmap, { timestamp });
|
|
131
|
-
const keyFrame = frameIndex % 30 === 0;
|
|
132
|
-
encoder.encode(frame, { keyFrame });
|
|
133
|
-
frame.close();
|
|
134
|
-
bitmap.close();
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
addAudioChunk(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata) {
|
|
138
|
-
if (closed || !muxer.hasAudioTrack) return;
|
|
139
|
-
muxer.addAudioChunk(chunk, meta);
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
async finalize(): Promise<ArrayBuffer> {
|
|
143
|
-
if (closed) throw new Error('Encoder already closed');
|
|
144
|
-
await encoder.flush();
|
|
145
|
-
encoder.close();
|
|
146
|
-
closed = true;
|
|
147
|
-
return muxer.finalize();
|
|
148
|
-
},
|
|
149
|
-
|
|
150
|
-
close() {
|
|
151
|
-
if (closed) return;
|
|
152
|
-
closed = true;
|
|
153
|
-
if (encoder.state !== 'closed') {
|
|
154
|
-
encoder.close();
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
}
|
package/src/mp4Mux.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* mp4Mux — Thin wrapper around mp4-muxer for WebCodecs encoding.
|
|
3
|
-
*
|
|
4
|
-
* Creates a Muxer instance configured for H.264 video (and, optionally, an
|
|
5
|
-
* AAC audio track), accumulates encoded chunks, and produces a final MP4
|
|
6
|
-
* ArrayBuffer. When no `audio` option is supplied the muxer configuration is
|
|
7
|
-
* byte-identical to the video-only path.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { Muxer, ArrayBufferTarget } from 'mp4-muxer';
|
|
11
|
-
|
|
12
|
-
export interface Mp4MuxerOptions {
|
|
13
|
-
width: number;
|
|
14
|
-
height: number;
|
|
15
|
-
fps: number;
|
|
16
|
-
/**
|
|
17
|
-
* When present, declares an AAC audio track alongside the video track.
|
|
18
|
-
* Feed encoded audio via {@link Mp4MuxerHandle.addAudioChunk} (from a
|
|
19
|
-
* WebCodecs `AudioEncoder`) or {@link Mp4MuxerHandle.addAudioChunkRaw}.
|
|
20
|
-
*/
|
|
21
|
-
audio?: {
|
|
22
|
-
numberOfChannels: number;
|
|
23
|
-
sampleRate: number;
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface Mp4MuxerHandle {
|
|
28
|
-
/** Add an encoded video chunk to the muxer. */
|
|
29
|
-
addVideoChunk(chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata): void;
|
|
30
|
-
/**
|
|
31
|
-
* Add a raw video sample (bytes + timing) without an `EncodedVideoChunk`.
|
|
32
|
-
* Useful where no `VideoEncoder` instance is available (e.g. Node tests).
|
|
33
|
-
*/
|
|
34
|
-
addVideoChunkRaw(
|
|
35
|
-
data: Uint8Array,
|
|
36
|
-
type: 'key' | 'delta',
|
|
37
|
-
timestampMicros: number,
|
|
38
|
-
durationMicros: number,
|
|
39
|
-
meta?: EncodedVideoChunkMetadata,
|
|
40
|
-
): void;
|
|
41
|
-
/** Add an encoded audio chunk (from a WebCodecs `AudioEncoder`). */
|
|
42
|
-
addAudioChunk(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata): void;
|
|
43
|
-
/**
|
|
44
|
-
* Add a raw audio sample (bytes + timing) without an `EncodedAudioChunk`.
|
|
45
|
-
* Useful where no `AudioEncoder` instance is available (e.g. Node tests).
|
|
46
|
-
*/
|
|
47
|
-
addAudioChunkRaw(
|
|
48
|
-
data: Uint8Array,
|
|
49
|
-
type: 'key' | 'delta',
|
|
50
|
-
timestampMicros: number,
|
|
51
|
-
durationMicros: number,
|
|
52
|
-
meta?: EncodedAudioChunkMetadata,
|
|
53
|
-
): void;
|
|
54
|
-
/** Whether this muxer was created with an audio track. */
|
|
55
|
-
readonly hasAudioTrack: boolean;
|
|
56
|
-
/** Finalize and return the MP4 as an ArrayBuffer. */
|
|
57
|
-
finalize(): ArrayBuffer;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Create an MP4 muxer configured for H.264 video, plus an optional AAC track.
|
|
62
|
-
*/
|
|
63
|
-
export function createMp4Muxer(options: Mp4MuxerOptions): Mp4MuxerHandle {
|
|
64
|
-
const target = new ArrayBufferTarget();
|
|
65
|
-
|
|
66
|
-
const muxer = new Muxer({
|
|
67
|
-
target,
|
|
68
|
-
video: {
|
|
69
|
-
codec: 'avc',
|
|
70
|
-
width: options.width,
|
|
71
|
-
height: options.height,
|
|
72
|
-
},
|
|
73
|
-
// Only declare the audio track when requested so the video-only
|
|
74
|
-
// configuration stays byte-identical to the historical output.
|
|
75
|
-
...(options.audio
|
|
76
|
-
? {
|
|
77
|
-
audio: {
|
|
78
|
-
codec: 'aac' as const,
|
|
79
|
-
numberOfChannels: options.audio.numberOfChannels,
|
|
80
|
-
sampleRate: options.audio.sampleRate,
|
|
81
|
-
},
|
|
82
|
-
}
|
|
83
|
-
: {}),
|
|
84
|
-
fastStart: 'in-memory',
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
hasAudioTrack: options.audio !== undefined,
|
|
89
|
-
|
|
90
|
-
addVideoChunk(chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata) {
|
|
91
|
-
muxer.addVideoChunk(chunk, meta);
|
|
92
|
-
},
|
|
93
|
-
|
|
94
|
-
addVideoChunkRaw(
|
|
95
|
-
data: Uint8Array,
|
|
96
|
-
type: 'key' | 'delta',
|
|
97
|
-
timestampMicros: number,
|
|
98
|
-
durationMicros: number,
|
|
99
|
-
meta?: EncodedVideoChunkMetadata,
|
|
100
|
-
) {
|
|
101
|
-
muxer.addVideoChunkRaw(data, type, timestampMicros, durationMicros, meta);
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
addAudioChunk(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata) {
|
|
105
|
-
muxer.addAudioChunk(chunk, meta);
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
addAudioChunkRaw(
|
|
109
|
-
data: Uint8Array,
|
|
110
|
-
type: 'key' | 'delta',
|
|
111
|
-
timestampMicros: number,
|
|
112
|
-
durationMicros: number,
|
|
113
|
-
meta?: EncodedAudioChunkMetadata,
|
|
114
|
-
) {
|
|
115
|
-
muxer.addAudioChunkRaw(data, type, timestampMicros, durationMicros, meta);
|
|
116
|
-
},
|
|
117
|
-
|
|
118
|
-
finalize(): ArrayBuffer {
|
|
119
|
-
muxer.finalize();
|
|
120
|
-
return target.buffer;
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
}
|
package/src/workerEncoder.ts
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Worker-backed video encoder.
|
|
3
|
-
*
|
|
4
|
-
* Spawns `workers/encode.worker.ts` and exposes the same shape as
|
|
5
|
-
* `MainThreadEncoder` so `useVideoExport` can treat both backends
|
|
6
|
-
* uniformly. Used as a fallback when WebCodecs H.264 isn't supported
|
|
7
|
-
* (typical on Linux Chromium): the worker auto-selects its ffmpeg.wasm
|
|
8
|
-
* path in that case.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { MainThreadEncoder, EncoderConfig } from './mainThreadEncoder.js';
|
|
12
|
-
import type { MainToWorkerMessage, WorkerToMainMessage } from './workers/workerTypes.js';
|
|
13
|
-
import type { FfmpegWasmLoadConfig } from '@bendyline/squisq-video';
|
|
14
|
-
import { validateVideoExportOptions } from '@bendyline/squisq-video';
|
|
15
|
-
|
|
16
|
-
/** Resolves once the worker has reported which backend it picked. */
|
|
17
|
-
export interface WorkerEncoder extends MainThreadEncoder {
|
|
18
|
-
/** Backend the worker selected ('webcodecs' or 'ffmpeg-wasm'). */
|
|
19
|
-
readonly ready: Promise<'webcodecs' | 'ffmpeg-wasm'>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function createWorkerEncoder(
|
|
23
|
-
config: EncoderConfig & { ffmpegWasm?: FfmpegWasmLoadConfig },
|
|
24
|
-
): WorkerEncoder {
|
|
25
|
-
validateVideoExportOptions(config);
|
|
26
|
-
|
|
27
|
-
const worker = new Worker(new URL('./workers/encode.worker.js', import.meta.url), {
|
|
28
|
-
type: 'module',
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
let state: 'open' | 'finalizing' | 'closed' = 'open';
|
|
32
|
-
let fatalError: Error | null = null;
|
|
33
|
-
let finalizeResolve: ((buffer: ArrayBuffer) => void) | null = null;
|
|
34
|
-
let finalizeReject: ((err: Error) => void) | null = null;
|
|
35
|
-
let readyResolve: ((backend: 'webcodecs' | 'ffmpeg-wasm') => void) | null = null;
|
|
36
|
-
let readyReject: ((err: Error) => void) | null = null;
|
|
37
|
-
let readySettled = false;
|
|
38
|
-
const frameWaiters = new Map<
|
|
39
|
-
number,
|
|
40
|
-
{ promise: Promise<void>; resolve: () => void; reject: (err: Error) => void }
|
|
41
|
-
>();
|
|
42
|
-
|
|
43
|
-
const ready = new Promise<'webcodecs' | 'ffmpeg-wasm'>((resolve, reject) => {
|
|
44
|
-
readyResolve = resolve;
|
|
45
|
-
readyReject = reject;
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const frameDuration = 1_000_000 / config.fps; // microseconds per frame
|
|
49
|
-
|
|
50
|
-
function post(msg: MainToWorkerMessage, transfer?: Transferable[]) {
|
|
51
|
-
worker.postMessage(msg, transfer ?? []);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Keep async lifecycle checks observable to TypeScript: event handlers can
|
|
55
|
-
// change state while finalize() is awaiting pending frame acknowledgements.
|
|
56
|
-
const currentState = () => state;
|
|
57
|
-
|
|
58
|
-
worker.onmessage = (event: MessageEvent<WorkerToMainMessage>) => {
|
|
59
|
-
const msg = event.data;
|
|
60
|
-
switch (msg.type) {
|
|
61
|
-
case 'capabilities':
|
|
62
|
-
readySettled = true;
|
|
63
|
-
readyResolve?.(msg.backend);
|
|
64
|
-
readyResolve = readyReject = null;
|
|
65
|
-
break;
|
|
66
|
-
case 'frame-complete': {
|
|
67
|
-
const waiter = frameWaiters.get(msg.frameIndex);
|
|
68
|
-
waiter?.resolve();
|
|
69
|
-
frameWaiters.delete(msg.frameIndex);
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
case 'complete':
|
|
73
|
-
state = 'closed';
|
|
74
|
-
finalizeResolve?.(msg.data);
|
|
75
|
-
finalizeResolve = finalizeReject = null;
|
|
76
|
-
worker.terminate();
|
|
77
|
-
break;
|
|
78
|
-
case 'error': {
|
|
79
|
-
const err = new Error(msg.message);
|
|
80
|
-
fatalError = err;
|
|
81
|
-
state = 'closed';
|
|
82
|
-
readySettled = true;
|
|
83
|
-
readyReject?.(err);
|
|
84
|
-
finalizeReject?.(err);
|
|
85
|
-
for (const waiter of frameWaiters.values()) waiter.reject(err);
|
|
86
|
-
frameWaiters.clear();
|
|
87
|
-
readyResolve = readyReject = null;
|
|
88
|
-
finalizeResolve = finalizeReject = null;
|
|
89
|
-
worker.terminate();
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
// 'progress' is informational; ignored at this layer
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
worker.onerror = (event) => {
|
|
97
|
-
const err = new Error(event.message || 'Worker error');
|
|
98
|
-
fatalError = err;
|
|
99
|
-
state = 'closed';
|
|
100
|
-
readySettled = true;
|
|
101
|
-
readyReject?.(err);
|
|
102
|
-
finalizeReject?.(err);
|
|
103
|
-
for (const waiter of frameWaiters.values()) waiter.reject(err);
|
|
104
|
-
frameWaiters.clear();
|
|
105
|
-
readyResolve = readyReject = null;
|
|
106
|
-
finalizeResolve = finalizeReject = null;
|
|
107
|
-
worker.terminate();
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
post({
|
|
111
|
-
type: 'init',
|
|
112
|
-
width: config.width,
|
|
113
|
-
height: config.height,
|
|
114
|
-
fps: config.fps,
|
|
115
|
-
quality: config.quality,
|
|
116
|
-
...(config.ffmpegWasm ? { ffmpegWasm: config.ffmpegWasm } : {}),
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
ready,
|
|
121
|
-
|
|
122
|
-
encodeFrame(bitmap: ImageBitmap, frameIndex: number): Promise<void> {
|
|
123
|
-
if (state !== 'open' || fatalError) {
|
|
124
|
-
bitmap.close();
|
|
125
|
-
return Promise.reject(fatalError ?? new Error('Encoder is not accepting frames'));
|
|
126
|
-
}
|
|
127
|
-
if (frameWaiters.has(frameIndex)) {
|
|
128
|
-
bitmap.close();
|
|
129
|
-
return Promise.reject(new Error(`Frame ${frameIndex} was submitted more than once`));
|
|
130
|
-
}
|
|
131
|
-
let resolveFrame!: () => void;
|
|
132
|
-
let rejectFrame!: (err: Error) => void;
|
|
133
|
-
const promise = new Promise<void>((resolve, reject) => {
|
|
134
|
-
resolveFrame = resolve;
|
|
135
|
-
rejectFrame = reject;
|
|
136
|
-
});
|
|
137
|
-
frameWaiters.set(frameIndex, { promise, resolve: resolveFrame, reject: rejectFrame });
|
|
138
|
-
const timestamp = Math.round(frameIndex * frameDuration);
|
|
139
|
-
post({ type: 'frame', bitmap, frameIndex, timestamp }, [bitmap]);
|
|
140
|
-
return promise;
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
async finalize(): Promise<ArrayBuffer> {
|
|
144
|
-
if (state !== 'open') throw new Error('Encoder already closed or finalizing');
|
|
145
|
-
if (fatalError) throw fatalError;
|
|
146
|
-
state = 'finalizing';
|
|
147
|
-
await Promise.all(Array.from(frameWaiters.values(), (waiter) => waiter.promise));
|
|
148
|
-
if (currentState() === 'closed') {
|
|
149
|
-
throw fatalError ?? new Error('Encoder closed during finalization');
|
|
150
|
-
}
|
|
151
|
-
return new Promise<ArrayBuffer>((resolve, reject) => {
|
|
152
|
-
finalizeResolve = resolve;
|
|
153
|
-
finalizeReject = reject;
|
|
154
|
-
post({ type: 'finalize' });
|
|
155
|
-
});
|
|
156
|
-
},
|
|
157
|
-
|
|
158
|
-
close() {
|
|
159
|
-
if (state === 'closed') return;
|
|
160
|
-
state = 'closed';
|
|
161
|
-
const err = new Error('Encoder closed');
|
|
162
|
-
if (!readySettled) {
|
|
163
|
-
readySettled = true;
|
|
164
|
-
readyReject?.(err);
|
|
165
|
-
}
|
|
166
|
-
finalizeReject?.(err);
|
|
167
|
-
for (const waiter of frameWaiters.values()) waiter.reject(err);
|
|
168
|
-
frameWaiters.clear();
|
|
169
|
-
readyResolve = readyReject = null;
|
|
170
|
-
finalizeResolve = finalizeReject = null;
|
|
171
|
-
post({ type: 'cancel' });
|
|
172
|
-
worker.terminate();
|
|
173
|
-
},
|
|
174
|
-
};
|
|
175
|
-
}
|