@meframe/core 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/medeo-fe/node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js.map +1 -0
- package/dist/{node_modules → medeo-fe/node_modules}/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js +2 -2
- package/dist/medeo-fe/node_modules/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js.map +1 -0
- package/dist/orchestrator/ExportScheduler.d.ts +9 -7
- package/dist/orchestrator/ExportScheduler.d.ts.map +1 -1
- package/dist/orchestrator/ExportScheduler.js +182 -80
- package/dist/orchestrator/ExportScheduler.js.map +1 -1
- package/dist/orchestrator/Orchestrator.js +22 -22
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoWindowDecodeSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoWindowDecodeSession.js +15 -3
- package/dist/orchestrator/VideoWindowDecodeSession.js.map +1 -1
- package/dist/stages/compose/VideoComposer.d.ts +2 -0
- package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
- package/dist/stages/compose/VideoComposer.js +41 -2
- package/dist/stages/compose/VideoComposer.js.map +1 -1
- package/dist/stages/decode/video-decoder.d.ts.map +1 -1
- package/dist/stages/decode/video-decoder.js +5 -3
- package/dist/stages/decode/video-decoder.js.map +1 -1
- package/dist/stages/encode/BaseEncoder.d.ts.map +1 -1
- package/dist/stages/encode/BaseEncoder.js +34 -3
- package/dist/stages/encode/BaseEncoder.js.map +1 -1
- package/dist/stages/encode/VideoChunkEncoder.d.ts.map +1 -1
- package/dist/stages/mux/MP4Muxer.js +1 -1
- package/dist/utils/mp4box.js +1 -1
- package/dist/utils/time-utils.d.ts +15 -0
- package/dist/utils/time-utils.d.ts.map +1 -1
- package/dist/utils/time-utils.js +33 -0
- package/dist/utils/time-utils.js.map +1 -0
- package/dist/worker/WorkerChannel.d.ts.map +1 -1
- package/dist/worker/WorkerChannel.js +3 -15
- package/dist/worker/WorkerChannel.js.map +1 -1
- package/dist/worker/WorkerPool.d.ts.map +1 -1
- package/dist/worker/WorkerPool.js +4 -12
- package/dist/worker/WorkerPool.js.map +1 -1
- package/dist/worker/types.d.ts +1 -1
- package/dist/worker/types.d.ts.map +1 -1
- package/dist/worker/types.js.map +1 -1
- package/dist/worker/worker-event-whitelist.d.ts.map +1 -1
- package/dist/workers/stages/{compose/video-compose.worker.KMZjuJuY.js → export/export.worker.SahP9aVK.js} +915 -217
- package/dist/workers/stages/export/export.worker.SahP9aVK.js.map +1 -0
- package/dist/workers/worker-manifest.json +1 -3
- package/package.json +1 -1
- package/dist/node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js.map +0 -1
- package/dist/node_modules/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js.map +0 -1
- package/dist/orchestrator/VideoClipSession.d.ts +0 -80
- package/dist/orchestrator/VideoClipSession.d.ts.map +0 -1
- package/dist/orchestrator/VideoClipSession.js +0 -361
- package/dist/orchestrator/VideoClipSession.js.map +0 -1
- package/dist/workers/WorkerChannel.DQK8rAab.js +0 -528
- package/dist/workers/WorkerChannel.DQK8rAab.js.map +0 -1
- package/dist/workers/stages/compose/audio-compose.worker.B4Io5w9i.js +0 -1063
- package/dist/workers/stages/compose/audio-compose.worker.B4Io5w9i.js.map +0 -1
- package/dist/workers/stages/compose/video-compose.worker.KMZjuJuY.js.map +0 -1
- package/dist/workers/stages/encode/video-encode.worker.D6aB_rF9.js +0 -334
- package/dist/workers/stages/encode/video-encode.worker.D6aB_rF9.js.map +0 -1
- /package/dist/{node_modules → medeo-fe/node_modules}/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js +0 -0
|
@@ -1,4 +1,513 @@
|
|
|
1
|
-
|
|
1
|
+
var WorkerMessageType = /* @__PURE__ */ ((WorkerMessageType2) => {
|
|
2
|
+
WorkerMessageType2["Ready"] = "ready";
|
|
3
|
+
WorkerMessageType2["Error"] = "error";
|
|
4
|
+
WorkerMessageType2["Dispose"] = "dispose";
|
|
5
|
+
WorkerMessageType2["Configure"] = "configure";
|
|
6
|
+
WorkerMessageType2["LoadResource"] = "load_resource";
|
|
7
|
+
WorkerMessageType2["ResourceLoaded"] = "resource_loaded";
|
|
8
|
+
WorkerMessageType2["ResourceProgress"] = "resource_progress";
|
|
9
|
+
WorkerMessageType2["ConfigureDemux"] = "configure_demux";
|
|
10
|
+
WorkerMessageType2["AppendBuffer"] = "append_buffer";
|
|
11
|
+
WorkerMessageType2["DemuxSamples"] = "demux_samples";
|
|
12
|
+
WorkerMessageType2["FlushDemux"] = "flush_demux";
|
|
13
|
+
WorkerMessageType2["ConfigureDecode"] = "configure_decode";
|
|
14
|
+
WorkerMessageType2["DecodeChunk"] = "decode_chunk";
|
|
15
|
+
WorkerMessageType2["DecodedFrame"] = "decoded_frame";
|
|
16
|
+
WorkerMessageType2["SeekGop"] = "seek_gop";
|
|
17
|
+
WorkerMessageType2["SetComposition"] = "set_composition";
|
|
18
|
+
WorkerMessageType2["ApplyPatch"] = "apply_patch";
|
|
19
|
+
WorkerMessageType2["RenderFrame"] = "render_frame";
|
|
20
|
+
WorkerMessageType2["ComposeFrameReady"] = "compose_frame_ready";
|
|
21
|
+
WorkerMessageType2["ConfigureEncode"] = "configure_encode";
|
|
22
|
+
WorkerMessageType2["EncodeFrame"] = "encode_frame";
|
|
23
|
+
WorkerMessageType2["EncodeAudio"] = "encode_audio";
|
|
24
|
+
WorkerMessageType2["EncodedChunk"] = "encoded_chunk";
|
|
25
|
+
WorkerMessageType2["FlushEncode"] = "flush_encode";
|
|
26
|
+
WorkerMessageType2["AddChunk"] = "add_chunk";
|
|
27
|
+
WorkerMessageType2["PerformanceStats"] = "performance_stats";
|
|
28
|
+
WorkerMessageType2["RenderWindow"] = "renderWindow";
|
|
29
|
+
WorkerMessageType2["AudioTrackAdd"] = "audio_track:add";
|
|
30
|
+
WorkerMessageType2["AudioTrackRemove"] = "audio_track:remove";
|
|
31
|
+
WorkerMessageType2["AudioTrackUpdate"] = "audio_track:update";
|
|
32
|
+
return WorkerMessageType2;
|
|
33
|
+
})(WorkerMessageType || {});
|
|
34
|
+
var WorkerState = /* @__PURE__ */ ((WorkerState2) => {
|
|
35
|
+
WorkerState2["Idle"] = "idle";
|
|
36
|
+
WorkerState2["Initializing"] = "initializing";
|
|
37
|
+
WorkerState2["Ready"] = "ready";
|
|
38
|
+
WorkerState2["Processing"] = "processing";
|
|
39
|
+
WorkerState2["Error"] = "error";
|
|
40
|
+
WorkerState2["Disposed"] = "disposed";
|
|
41
|
+
return WorkerState2;
|
|
42
|
+
})(WorkerState || {});
|
|
43
|
+
const defaultRetryConfig = {
|
|
44
|
+
maxRetries: 3,
|
|
45
|
+
initialDelay: 100,
|
|
46
|
+
maxDelay: 5e3,
|
|
47
|
+
backoffFactor: 2,
|
|
48
|
+
retryableErrors: ["TIMEOUT", "NETWORK_ERROR", "WORKER_BUSY"]
|
|
49
|
+
};
|
|
50
|
+
function calculateRetryDelay(attempt, config) {
|
|
51
|
+
const { initialDelay = 100, maxDelay = 5e3, backoffFactor = 2 } = config;
|
|
52
|
+
const delay = initialDelay * Math.pow(backoffFactor, attempt - 1);
|
|
53
|
+
return Math.min(delay, maxDelay);
|
|
54
|
+
}
|
|
55
|
+
function isRetryableError(error, config) {
|
|
56
|
+
const { retryableErrors = defaultRetryConfig.retryableErrors } = config;
|
|
57
|
+
if (!error) return false;
|
|
58
|
+
const errorCode = error.code || error.name;
|
|
59
|
+
if (errorCode && retryableErrors.includes(errorCode)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
const message = error.message || "";
|
|
63
|
+
if (message.includes("timeout") || message.includes("Timeout")) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
async function withRetry(fn, config) {
|
|
69
|
+
const { maxRetries } = config;
|
|
70
|
+
let lastError;
|
|
71
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
72
|
+
try {
|
|
73
|
+
return await fn();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
lastError = error;
|
|
76
|
+
if (!isRetryableError(error, config)) {
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
if (attempt === maxRetries) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
const delay = calculateRetryDelay(attempt, config);
|
|
83
|
+
await sleep(delay);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
throw lastError || new Error("Retry failed");
|
|
87
|
+
}
|
|
88
|
+
function sleep(ms) {
|
|
89
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
90
|
+
}
|
|
91
|
+
function isTransferable(obj) {
|
|
92
|
+
return obj instanceof ArrayBuffer || obj instanceof MessagePort || typeof ImageBitmap !== "undefined" && obj instanceof ImageBitmap || typeof OffscreenCanvas !== "undefined" && obj instanceof OffscreenCanvas || typeof ReadableStream !== "undefined" && obj instanceof ReadableStream || typeof WritableStream !== "undefined" && obj instanceof WritableStream || typeof TransformStream !== "undefined" && obj instanceof TransformStream;
|
|
93
|
+
}
|
|
94
|
+
function findTransferables(obj, transferables) {
|
|
95
|
+
if (!obj || typeof obj !== "object") {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (isTransferable(obj)) {
|
|
99
|
+
transferables.push(obj);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (obj instanceof VideoFrame) {
|
|
103
|
+
transferables.push(obj);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (typeof AudioData !== "undefined" && obj instanceof AudioData) {
|
|
107
|
+
transferables.push(obj);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (typeof EncodedVideoChunk !== "undefined" && obj instanceof EncodedVideoChunk || typeof EncodedAudioChunk !== "undefined" && obj instanceof EncodedAudioChunk) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(obj)) {
|
|
114
|
+
for (const item of obj) {
|
|
115
|
+
findTransferables(item, transferables);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
for (const key in obj) {
|
|
119
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
120
|
+
findTransferables(obj[key], transferables);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function extractTransferables(payload) {
|
|
126
|
+
const transferables = [];
|
|
127
|
+
findTransferables(payload, transferables);
|
|
128
|
+
return transferables;
|
|
129
|
+
}
|
|
130
|
+
function encodedVideoChunkToTransferable(chunk) {
|
|
131
|
+
const data = new ArrayBuffer(chunk.byteLength);
|
|
132
|
+
chunk.copyTo(new Uint8Array(data));
|
|
133
|
+
return {
|
|
134
|
+
data,
|
|
135
|
+
type: chunk.type,
|
|
136
|
+
timestamp: chunk.timestamp,
|
|
137
|
+
duration: chunk.duration
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function encodedAudioChunkToTransferable(chunk) {
|
|
141
|
+
const data = new ArrayBuffer(chunk.byteLength);
|
|
142
|
+
chunk.copyTo(new Uint8Array(data));
|
|
143
|
+
return {
|
|
144
|
+
data,
|
|
145
|
+
type: chunk.type,
|
|
146
|
+
timestamp: chunk.timestamp,
|
|
147
|
+
duration: chunk.duration
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
class WorkerChannel {
|
|
151
|
+
name;
|
|
152
|
+
port;
|
|
153
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
154
|
+
messageHandlers = {};
|
|
155
|
+
state = WorkerState.Idle;
|
|
156
|
+
defaultTimeout;
|
|
157
|
+
defaultMaxRetries;
|
|
158
|
+
constructor(port, config) {
|
|
159
|
+
this.name = config.name;
|
|
160
|
+
this.port = port;
|
|
161
|
+
this.defaultTimeout = config.timeout ?? 3e4;
|
|
162
|
+
this.defaultMaxRetries = config.maxRetries ?? 3;
|
|
163
|
+
this.setupMessageHandler();
|
|
164
|
+
this.state = WorkerState.Ready;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Send a message and wait for response with retry support
|
|
168
|
+
*/
|
|
169
|
+
async send(type, payload, options) {
|
|
170
|
+
const maxRetries = options?.maxRetries ?? this.defaultMaxRetries;
|
|
171
|
+
const retryConfig = {
|
|
172
|
+
...defaultRetryConfig,
|
|
173
|
+
maxRetries,
|
|
174
|
+
...options?.retryConfig
|
|
175
|
+
};
|
|
176
|
+
return withRetry(() => this.sendOnce(type, payload, options), retryConfig);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Send a message once (without retry)
|
|
180
|
+
*/
|
|
181
|
+
async sendOnce(type, payload, options) {
|
|
182
|
+
const id = this.generateMessageId();
|
|
183
|
+
const timeout = options?.timeout ?? this.defaultTimeout;
|
|
184
|
+
const message = {
|
|
185
|
+
type,
|
|
186
|
+
id,
|
|
187
|
+
payload,
|
|
188
|
+
timestamp: Date.now()
|
|
189
|
+
};
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
const request = {
|
|
192
|
+
id,
|
|
193
|
+
type,
|
|
194
|
+
timestamp: Date.now(),
|
|
195
|
+
timeout,
|
|
196
|
+
resolve,
|
|
197
|
+
reject
|
|
198
|
+
};
|
|
199
|
+
this.pendingRequests.set(id, request);
|
|
200
|
+
const timeoutId = setTimeout(() => {
|
|
201
|
+
const pending = this.pendingRequests.get(id);
|
|
202
|
+
if (pending) {
|
|
203
|
+
this.pendingRequests.delete(id);
|
|
204
|
+
const error = new Error(`Request timeout: ${id} ${type} (${timeout}ms)`);
|
|
205
|
+
error.code = "TIMEOUT";
|
|
206
|
+
pending.reject(error);
|
|
207
|
+
}
|
|
208
|
+
}, timeout);
|
|
209
|
+
request.timeoutId = timeoutId;
|
|
210
|
+
if (options?.transfer) {
|
|
211
|
+
this.port.postMessage(message, options.transfer);
|
|
212
|
+
} else {
|
|
213
|
+
this.port.postMessage(message);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Send a message without waiting for response
|
|
219
|
+
*/
|
|
220
|
+
post(type, payload, transfer) {
|
|
221
|
+
const message = {
|
|
222
|
+
type,
|
|
223
|
+
id: this.generateMessageId(),
|
|
224
|
+
payload,
|
|
225
|
+
timestamp: Date.now()
|
|
226
|
+
};
|
|
227
|
+
if (transfer) {
|
|
228
|
+
this.port.postMessage(message, transfer);
|
|
229
|
+
} else {
|
|
230
|
+
this.port.postMessage(message);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Register a message handler
|
|
235
|
+
*/
|
|
236
|
+
on(type, handler) {
|
|
237
|
+
this.messageHandlers[type] = handler;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Unregister a message handler
|
|
241
|
+
*/
|
|
242
|
+
off(type) {
|
|
243
|
+
delete this.messageHandlers[type];
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Dispose the channel
|
|
247
|
+
*/
|
|
248
|
+
dispose() {
|
|
249
|
+
this.state = WorkerState.Disposed;
|
|
250
|
+
for (const [, request] of this.pendingRequests) {
|
|
251
|
+
if (request.timeoutId) {
|
|
252
|
+
clearTimeout(request.timeoutId);
|
|
253
|
+
}
|
|
254
|
+
request.reject(new Error("Channel disposed"));
|
|
255
|
+
}
|
|
256
|
+
this.pendingRequests.clear();
|
|
257
|
+
this.port.onmessage = null;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Setup message handler for incoming messages
|
|
261
|
+
*/
|
|
262
|
+
setupMessageHandler() {
|
|
263
|
+
this.port.onmessage = async (event) => {
|
|
264
|
+
const data = event.data;
|
|
265
|
+
if (this.isResponse(data)) {
|
|
266
|
+
this.handleResponse(data);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (this.isRequest(data)) {
|
|
270
|
+
await this.handleRequest(data);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Handle incoming request
|
|
277
|
+
*/
|
|
278
|
+
async handleRequest(message) {
|
|
279
|
+
const handler = this.messageHandlers[message.type];
|
|
280
|
+
if (!handler) {
|
|
281
|
+
this.sendResponse(message.id, false, null, {
|
|
282
|
+
code: "NO_HANDLER",
|
|
283
|
+
message: `No handler registered for message type: ${message.type}`
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
this.state = WorkerState.Processing;
|
|
288
|
+
Promise.resolve().then(() => handler(message.payload, message.transfer)).then((result) => {
|
|
289
|
+
this.sendResponse(message.id, true, result);
|
|
290
|
+
this.state = WorkerState.Ready;
|
|
291
|
+
}).catch((error) => {
|
|
292
|
+
const workerError = {
|
|
293
|
+
code: "HANDLER_ERROR",
|
|
294
|
+
message: error instanceof Error ? error.message : String(error),
|
|
295
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
296
|
+
};
|
|
297
|
+
this.sendResponse(message.id, false, null, workerError);
|
|
298
|
+
this.state = WorkerState.Ready;
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Handle incoming response
|
|
303
|
+
*/
|
|
304
|
+
handleResponse(response) {
|
|
305
|
+
const request = this.pendingRequests.get(response.id);
|
|
306
|
+
if (!request) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
this.pendingRequests.delete(response.id);
|
|
310
|
+
if (request.timeoutId) {
|
|
311
|
+
clearTimeout(request.timeoutId);
|
|
312
|
+
}
|
|
313
|
+
if (response.success) {
|
|
314
|
+
request.resolve(response.result);
|
|
315
|
+
} else {
|
|
316
|
+
const error = new Error(response.error?.message || "Unknown error");
|
|
317
|
+
if (response.error) {
|
|
318
|
+
Object.assign(error, response.error);
|
|
319
|
+
}
|
|
320
|
+
request.reject(error);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Send a response message
|
|
325
|
+
*/
|
|
326
|
+
sendResponse(id, success, result, error) {
|
|
327
|
+
let transfer = [];
|
|
328
|
+
if (isTransferable(result)) {
|
|
329
|
+
transfer.push(result);
|
|
330
|
+
}
|
|
331
|
+
const response = {
|
|
332
|
+
id,
|
|
333
|
+
success,
|
|
334
|
+
result,
|
|
335
|
+
error,
|
|
336
|
+
timestamp: Date.now()
|
|
337
|
+
};
|
|
338
|
+
this.port.postMessage(response, transfer);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Check if message is a response
|
|
342
|
+
*/
|
|
343
|
+
isResponse(data) {
|
|
344
|
+
return data && typeof data === "object" && "id" in data && "success" in data && !("type" in data);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Check if message is a request
|
|
348
|
+
*/
|
|
349
|
+
isRequest(data) {
|
|
350
|
+
return data && typeof data === "object" && "id" in data && "type" in data;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Generate unique message ID
|
|
354
|
+
*/
|
|
355
|
+
generateMessageId() {
|
|
356
|
+
return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Send a notification message without waiting for response
|
|
360
|
+
* Alias for post() method for compatibility
|
|
361
|
+
*/
|
|
362
|
+
notify(type, payload, transfer) {
|
|
363
|
+
this.post(type, payload, transfer);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Register a message handler
|
|
367
|
+
* Alias for on() method for compatibility
|
|
368
|
+
*/
|
|
369
|
+
registerHandler(type, handler) {
|
|
370
|
+
this.on(type, handler);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Send a ReadableStream to another worker
|
|
374
|
+
* Automatically handles transferable streams vs chunk-by-chunk fallback
|
|
375
|
+
*/
|
|
376
|
+
async sendStream(stream, metadata) {
|
|
377
|
+
const streamId = metadata?.streamId || this.generateMessageId();
|
|
378
|
+
const forceChunkTransfer = metadata?.forceChunkTransfer === true || metadata?.streamType === "video" || metadata?.streamType === "audio";
|
|
379
|
+
if (!forceChunkTransfer && isTransferable(stream)) {
|
|
380
|
+
this.port.postMessage(
|
|
381
|
+
{
|
|
382
|
+
type: "stream_transfer",
|
|
383
|
+
...metadata,
|
|
384
|
+
stream,
|
|
385
|
+
streamId
|
|
386
|
+
},
|
|
387
|
+
[stream]
|
|
388
|
+
// Transfer ownership
|
|
389
|
+
);
|
|
390
|
+
} else {
|
|
391
|
+
await this.streamChunks(stream, streamId, metadata);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Stream chunks from a ReadableStream (fallback when transfer is not supported)
|
|
396
|
+
*/
|
|
397
|
+
async streamChunks(stream, streamId, metadata) {
|
|
398
|
+
const reader = stream.getReader();
|
|
399
|
+
this.post("stream_start", {
|
|
400
|
+
streamId,
|
|
401
|
+
...metadata,
|
|
402
|
+
mode: "chunk_transfer"
|
|
403
|
+
});
|
|
404
|
+
try {
|
|
405
|
+
while (true) {
|
|
406
|
+
const { done, value } = await reader.read();
|
|
407
|
+
if (done) {
|
|
408
|
+
this.post("stream_end", { streamId });
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
const transfer = [];
|
|
412
|
+
let chunkValue = value;
|
|
413
|
+
if (value instanceof ArrayBuffer) {
|
|
414
|
+
transfer.push(value);
|
|
415
|
+
} else if (value instanceof Uint8Array) {
|
|
416
|
+
transfer.push(value.buffer);
|
|
417
|
+
} else if (typeof AudioData !== "undefined" && value instanceof AudioData) {
|
|
418
|
+
transfer.push(value);
|
|
419
|
+
} else if (typeof VideoFrame !== "undefined" && value instanceof VideoFrame) {
|
|
420
|
+
transfer.push(value);
|
|
421
|
+
} else if (typeof EncodedVideoChunk !== "undefined" && value instanceof EncodedVideoChunk) {
|
|
422
|
+
const serialized = encodedVideoChunkToTransferable(value);
|
|
423
|
+
transfer.push(serialized.data);
|
|
424
|
+
chunkValue = serialized;
|
|
425
|
+
} else if (typeof EncodedAudioChunk !== "undefined" && value instanceof EncodedAudioChunk) {
|
|
426
|
+
const serialized = encodedAudioChunkToTransferable(value);
|
|
427
|
+
transfer.push(serialized.data);
|
|
428
|
+
chunkValue = serialized;
|
|
429
|
+
} else if (typeof value === "object" && value !== null) {
|
|
430
|
+
const extracted = extractTransferables(value);
|
|
431
|
+
transfer.push(...extracted);
|
|
432
|
+
}
|
|
433
|
+
this.post("stream_chunk", { streamId, chunk: chunkValue }, transfer);
|
|
434
|
+
for (const t of transfer) {
|
|
435
|
+
if (typeof VideoFrame !== "undefined" && t instanceof VideoFrame) {
|
|
436
|
+
try {
|
|
437
|
+
t.close();
|
|
438
|
+
} catch {
|
|
439
|
+
}
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (typeof AudioData !== "undefined" && t instanceof AudioData) {
|
|
443
|
+
try {
|
|
444
|
+
t.close();
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} catch (error) {
|
|
451
|
+
this.post("stream_error", {
|
|
452
|
+
streamId,
|
|
453
|
+
error: error instanceof Error ? error.message : String(error)
|
|
454
|
+
});
|
|
455
|
+
throw error;
|
|
456
|
+
} finally {
|
|
457
|
+
reader.releaseLock();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Receive a stream from another worker
|
|
462
|
+
* Handles both transferable streams and chunk-by-chunk reconstruction
|
|
463
|
+
*/
|
|
464
|
+
async receiveStream(onStream) {
|
|
465
|
+
const chunkedStreams = /* @__PURE__ */ new Map();
|
|
466
|
+
const prev = this.port.onmessage;
|
|
467
|
+
const handler = (event) => {
|
|
468
|
+
const raw = event.data;
|
|
469
|
+
const envelopeType = raw?.type;
|
|
470
|
+
const hasPayload = raw && typeof raw === "object" && "payload" in raw;
|
|
471
|
+
const payload = hasPayload ? raw.payload : raw;
|
|
472
|
+
if (envelopeType === "stream_transfer" && payload?.stream) {
|
|
473
|
+
onStream(payload.stream, payload);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (envelopeType === "stream_start" && payload?.streamId) {
|
|
477
|
+
const stream = new ReadableStream({
|
|
478
|
+
start(controller) {
|
|
479
|
+
chunkedStreams.set(payload.streamId, { controller, metadata: payload });
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
onStream(stream, payload);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (envelopeType === "stream_chunk" && payload?.streamId && chunkedStreams.has(payload.streamId)) {
|
|
486
|
+
const s = chunkedStreams.get(payload.streamId);
|
|
487
|
+
if (s) s.controller.enqueue(payload.chunk);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (envelopeType === "stream_end" && payload?.streamId && chunkedStreams.has(payload.streamId)) {
|
|
491
|
+
const s = chunkedStreams.get(payload.streamId);
|
|
492
|
+
if (s) {
|
|
493
|
+
s.controller.close();
|
|
494
|
+
chunkedStreams.delete(payload.streamId);
|
|
495
|
+
}
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (envelopeType === "stream_error" && payload?.streamId && chunkedStreams.has(payload.streamId)) {
|
|
499
|
+
const s = chunkedStreams.get(payload.streamId);
|
|
500
|
+
if (s) {
|
|
501
|
+
s.controller.error(new Error(String(payload.error || "stream error")));
|
|
502
|
+
chunkedStreams.delete(payload.streamId);
|
|
503
|
+
}
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (typeof prev === "function") prev.call(this.port, event);
|
|
507
|
+
};
|
|
508
|
+
this.port.onmessage = handler;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
2
511
|
function measureTextWidth(ctx, text, fontSize, fontFamily, fontWeight = 400) {
|
|
3
512
|
ctx.save();
|
|
4
513
|
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
@@ -1515,6 +2024,7 @@ class VideoComposer {
|
|
|
1515
2024
|
// Cached frame duration
|
|
1516
2025
|
fontsLoadingPromise = null;
|
|
1517
2026
|
loadedFonts = /* @__PURE__ */ new Set();
|
|
2027
|
+
static FONT_LOAD_TIMEOUT_MS = 1e4;
|
|
1518
2028
|
constructor(config) {
|
|
1519
2029
|
this.config = this.applyDefaults(config);
|
|
1520
2030
|
this.frameDurationUs = Math.round(1e6 / this.config.fps);
|
|
@@ -1602,7 +2112,10 @@ class VideoComposer {
|
|
|
1602
2112
|
}
|
|
1603
2113
|
async composeFrame(request) {
|
|
1604
2114
|
if (this.fontsLoadingPromise) {
|
|
1605
|
-
|
|
2115
|
+
try {
|
|
2116
|
+
await this.withTimeout(this.fontsLoadingPromise, VideoComposer.FONT_LOAD_TIMEOUT_MS);
|
|
2117
|
+
} catch {
|
|
2118
|
+
}
|
|
1606
2119
|
this.fontsLoadingPromise = null;
|
|
1607
2120
|
}
|
|
1608
2121
|
if (request.layers.length > this.config.maxLayers) {
|
|
@@ -1702,7 +2215,10 @@ class VideoComposer {
|
|
|
1702
2215
|
}
|
|
1703
2216
|
try {
|
|
1704
2217
|
const fontFace = new FontFace(font.family, `url(${font.url})`);
|
|
1705
|
-
const loadedFont = await
|
|
2218
|
+
const loadedFont = await this.withTimeout(
|
|
2219
|
+
fontFace.load(),
|
|
2220
|
+
VideoComposer.FONT_LOAD_TIMEOUT_MS
|
|
2221
|
+
);
|
|
1706
2222
|
if ("fonts" in self) {
|
|
1707
2223
|
self.fonts.add(loadedFont);
|
|
1708
2224
|
}
|
|
@@ -1711,6 +2227,38 @@ class VideoComposer {
|
|
|
1711
2227
|
}
|
|
1712
2228
|
}
|
|
1713
2229
|
}
|
|
2230
|
+
async withTimeout(promise, timeoutMs) {
|
|
2231
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
2232
|
+
return promise;
|
|
2233
|
+
}
|
|
2234
|
+
let timeoutId = null;
|
|
2235
|
+
try {
|
|
2236
|
+
const safe = promise.then(
|
|
2237
|
+
(value) => ({ ok: true, value }),
|
|
2238
|
+
(error) => ({ ok: false, error })
|
|
2239
|
+
);
|
|
2240
|
+
const result = await Promise.race([
|
|
2241
|
+
safe,
|
|
2242
|
+
new Promise((_, reject) => {
|
|
2243
|
+
timeoutId = setTimeout(
|
|
2244
|
+
() => reject(new Error(`Timeout after ${timeoutMs}ms`)),
|
|
2245
|
+
timeoutMs
|
|
2246
|
+
);
|
|
2247
|
+
})
|
|
2248
|
+
]);
|
|
2249
|
+
if (result?.ok === true) {
|
|
2250
|
+
return result.value;
|
|
2251
|
+
}
|
|
2252
|
+
if (result?.ok === false) {
|
|
2253
|
+
throw result.error;
|
|
2254
|
+
}
|
|
2255
|
+
return result;
|
|
2256
|
+
} finally {
|
|
2257
|
+
if (timeoutId !== null) {
|
|
2258
|
+
clearTimeout(timeoutId);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
1714
2262
|
updateConfig(config) {
|
|
1715
2263
|
Object.assign(this.config, this.applyDefaults({ ...this.config, ...config }));
|
|
1716
2264
|
if (config.fps !== void 0) {
|
|
@@ -1733,6 +2281,272 @@ class VideoComposer {
|
|
|
1733
2281
|
this.filterProcessor.clearCache();
|
|
1734
2282
|
}
|
|
1735
2283
|
}
|
|
2284
|
+
class BaseEncoder {
|
|
2285
|
+
encoder;
|
|
2286
|
+
config;
|
|
2287
|
+
controller = null;
|
|
2288
|
+
constructor(config) {
|
|
2289
|
+
this.config = config;
|
|
2290
|
+
}
|
|
2291
|
+
getConfig() {
|
|
2292
|
+
return { ...this.config };
|
|
2293
|
+
}
|
|
2294
|
+
get currentConfig() {
|
|
2295
|
+
return this.config;
|
|
2296
|
+
}
|
|
2297
|
+
shouldReconfigure(partial) {
|
|
2298
|
+
const next = { ...this.config, ...partial };
|
|
2299
|
+
const keys = Object.keys(partial ?? {});
|
|
2300
|
+
for (const key of keys) {
|
|
2301
|
+
if (partial[key] !== void 0 && next[key] !== this.config[key]) {
|
|
2302
|
+
return true;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
return false;
|
|
2306
|
+
}
|
|
2307
|
+
hasConfigChanged(next) {
|
|
2308
|
+
const currentEntries = Object.entries(this.config);
|
|
2309
|
+
for (const [key, value] of currentEntries) {
|
|
2310
|
+
if (next[key] !== value) {
|
|
2311
|
+
return true;
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
for (const key of Object.keys(next)) {
|
|
2315
|
+
if (this.config[key] !== next[key]) {
|
|
2316
|
+
return true;
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
return false;
|
|
2320
|
+
}
|
|
2321
|
+
configsEqual(a, b) {
|
|
2322
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
2323
|
+
}
|
|
2324
|
+
async initialize() {
|
|
2325
|
+
console.info("Encoder is initializing...");
|
|
2326
|
+
if (this.encoder?.state === "configured") {
|
|
2327
|
+
console.warn("Encoder is already initialized, skipping initialization");
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
const isSupported = await this.isConfigSupported(this.config);
|
|
2331
|
+
if (!isSupported.supported) {
|
|
2332
|
+
console.warn("Codec not supported:", JSON.stringify(this.config));
|
|
2333
|
+
throw new Error(`Codec not supported: ${JSON.stringify(this.config)}`);
|
|
2334
|
+
}
|
|
2335
|
+
this.encoder = this.createEncoder({
|
|
2336
|
+
output: this.handleOutput.bind(this),
|
|
2337
|
+
error: this.handleError.bind(this)
|
|
2338
|
+
});
|
|
2339
|
+
this.encoder.configure(this.config);
|
|
2340
|
+
console.info("Encoder is initialized");
|
|
2341
|
+
}
|
|
2342
|
+
async reconfigure(config) {
|
|
2343
|
+
console.info("Encoder is reconfiguring...");
|
|
2344
|
+
if (!config || Object.keys(config).length === 0) {
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
const nextConfig = { ...this.config, ...config };
|
|
2348
|
+
if (this.configsEqual(this.config, nextConfig)) {
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
if (!this.encoder) {
|
|
2352
|
+
this.config = nextConfig;
|
|
2353
|
+
await this.initialize();
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
if (this.encoder.state === "configured") {
|
|
2357
|
+
await this.encoder.flush();
|
|
2358
|
+
}
|
|
2359
|
+
const isSupported = await this.isConfigSupported(nextConfig);
|
|
2360
|
+
if (!isSupported.supported) {
|
|
2361
|
+
throw new Error(`New configuration not supported: ${nextConfig.codec}`);
|
|
2362
|
+
}
|
|
2363
|
+
this.config = nextConfig;
|
|
2364
|
+
this.encoder.configure(this.config);
|
|
2365
|
+
console.info("Encoder is reconfigured");
|
|
2366
|
+
}
|
|
2367
|
+
async flush() {
|
|
2368
|
+
if (!this.encoder) {
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
await this.encoder.flush();
|
|
2372
|
+
}
|
|
2373
|
+
async reset() {
|
|
2374
|
+
console.info("Encoder is resetting...");
|
|
2375
|
+
if (!this.encoder) {
|
|
2376
|
+
console.warn("Encoder is not initialized, skipping reset");
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
this.encoder.reset();
|
|
2380
|
+
this.onReset();
|
|
2381
|
+
console.info("Encoder is resetted");
|
|
2382
|
+
}
|
|
2383
|
+
async close() {
|
|
2384
|
+
console.info("Encoder is closing...");
|
|
2385
|
+
if (!this.encoder) {
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
if (this.encoder.state === "configured") {
|
|
2389
|
+
await this.encoder.flush();
|
|
2390
|
+
}
|
|
2391
|
+
this.encoder.close();
|
|
2392
|
+
this.encoder = void 0;
|
|
2393
|
+
console.info("Encoder is closed");
|
|
2394
|
+
}
|
|
2395
|
+
get isReady() {
|
|
2396
|
+
return this.encoder?.state === "configured";
|
|
2397
|
+
}
|
|
2398
|
+
get queueSize() {
|
|
2399
|
+
return this.encoder?.encodeQueueSize ?? 0;
|
|
2400
|
+
}
|
|
2401
|
+
handleOutput(chunk, metadata) {
|
|
2402
|
+
if (this.controller) {
|
|
2403
|
+
try {
|
|
2404
|
+
this.controller.enqueue({ chunk, metadata });
|
|
2405
|
+
} catch (error) {
|
|
2406
|
+
console.error("Encoder output error:", error);
|
|
2407
|
+
if (!(error instanceof TypeError && error.message.includes("closed"))) {
|
|
2408
|
+
throw error;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
handleError(error) {
|
|
2414
|
+
if (error.message.includes("reclaimed")) {
|
|
2415
|
+
console.warn("Encoder reclaimed by browser due to inactivity, skipping error handling");
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
console.error(`[${this.getEncoderType()}Encoder] Encode error:`, {
|
|
2419
|
+
name: error.name,
|
|
2420
|
+
message: error.message,
|
|
2421
|
+
encoderState: this.encoder?.state,
|
|
2422
|
+
queueSize: this.queueSize,
|
|
2423
|
+
platform: typeof navigator !== "undefined" ? navigator.platform : "unknown"
|
|
2424
|
+
});
|
|
2425
|
+
this.controller?.error(error);
|
|
2426
|
+
}
|
|
2427
|
+
// Hook for subclasses to handle reset
|
|
2428
|
+
onReset() {
|
|
2429
|
+
}
|
|
2430
|
+
/**
|
|
2431
|
+
* Create transform stream for encoding
|
|
2432
|
+
* Implements common stream logic with backpressure handling
|
|
2433
|
+
*/
|
|
2434
|
+
createStream() {
|
|
2435
|
+
return new TransformStream(
|
|
2436
|
+
{
|
|
2437
|
+
start: async (controller) => {
|
|
2438
|
+
this.controller = controller;
|
|
2439
|
+
},
|
|
2440
|
+
transform: async (input) => {
|
|
2441
|
+
if (!this.isReady) {
|
|
2442
|
+
console.warn("Encoder is not ready, initializing...");
|
|
2443
|
+
await this.initialize();
|
|
2444
|
+
}
|
|
2445
|
+
if (!this.encoder || this.encoder.state !== "configured") {
|
|
2446
|
+
console.error("Encoder not configured, throwing error");
|
|
2447
|
+
throw new Error("Encoder not configured");
|
|
2448
|
+
}
|
|
2449
|
+
if (this.encoder.encodeQueueSize >= this.encodeQueueThreshold) {
|
|
2450
|
+
await new Promise((resolve) => {
|
|
2451
|
+
const check = () => {
|
|
2452
|
+
if (!this.encoder || this.encoder.encodeQueueSize < this.encodeQueueThreshold - 1) {
|
|
2453
|
+
resolve();
|
|
2454
|
+
} else {
|
|
2455
|
+
setTimeout(check, 10);
|
|
2456
|
+
}
|
|
2457
|
+
};
|
|
2458
|
+
check();
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2461
|
+
const frame = input.frame || input;
|
|
2462
|
+
try {
|
|
2463
|
+
this.encode(frame);
|
|
2464
|
+
} catch (err) {
|
|
2465
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2466
|
+
console.error("Encoder error:", msg);
|
|
2467
|
+
if (err instanceof DOMException && msg.includes("reclaimed")) {
|
|
2468
|
+
console.warn("Codec reclaimed due to inactivity, reset encoder...");
|
|
2469
|
+
this.encoder = void 0;
|
|
2470
|
+
await this.initialize();
|
|
2471
|
+
this.encode(frame);
|
|
2472
|
+
} else {
|
|
2473
|
+
throw err;
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
},
|
|
2477
|
+
flush: async () => {
|
|
2478
|
+
await this.flush();
|
|
2479
|
+
}
|
|
2480
|
+
},
|
|
2481
|
+
// Queuing strategy with backpressure configuration
|
|
2482
|
+
{
|
|
2483
|
+
highWaterMark: this.highWaterMark,
|
|
2484
|
+
size: () => 1
|
|
2485
|
+
// Count-based
|
|
2486
|
+
}
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
class VideoChunkEncoder extends BaseEncoder {
|
|
2491
|
+
static DEFAULT_HIGH_WATER_MARK = 2;
|
|
2492
|
+
static DEFAULT_ENCODE_QUEUE_THRESHOLD = 8;
|
|
2493
|
+
highWaterMark;
|
|
2494
|
+
encodeQueueThreshold;
|
|
2495
|
+
frameCount = 0;
|
|
2496
|
+
// Default 1 second at 30fps for better social media compatibility
|
|
2497
|
+
keyFrameInterval = 30;
|
|
2498
|
+
constructor(config) {
|
|
2499
|
+
super(config);
|
|
2500
|
+
this.highWaterMark = config.backpressure?.highWaterMark ?? VideoChunkEncoder.DEFAULT_HIGH_WATER_MARK;
|
|
2501
|
+
this.encodeQueueThreshold = config.backpressure?.encodeQueueThreshold ?? VideoChunkEncoder.DEFAULT_ENCODE_QUEUE_THRESHOLD;
|
|
2502
|
+
if (config.keyFrameInterval !== void 0) {
|
|
2503
|
+
this.keyFrameInterval = Math.max(1, config.keyFrameInterval);
|
|
2504
|
+
}
|
|
2505
|
+
console.info(
|
|
2506
|
+
"VideoChunkEncoder initialized with encodeQueueThreshold:",
|
|
2507
|
+
this.encodeQueueThreshold,
|
|
2508
|
+
" highWaterMark:",
|
|
2509
|
+
this.highWaterMark,
|
|
2510
|
+
" keyFrameInterval:",
|
|
2511
|
+
this.keyFrameInterval
|
|
2512
|
+
);
|
|
2513
|
+
}
|
|
2514
|
+
async isConfigSupported(config) {
|
|
2515
|
+
const result = await VideoEncoder.isConfigSupported(config);
|
|
2516
|
+
return { supported: result.supported ?? false };
|
|
2517
|
+
}
|
|
2518
|
+
createEncoder(init) {
|
|
2519
|
+
return new VideoEncoder(init);
|
|
2520
|
+
}
|
|
2521
|
+
getEncoderType() {
|
|
2522
|
+
return "Video";
|
|
2523
|
+
}
|
|
2524
|
+
onReset() {
|
|
2525
|
+
this.frameCount = 0;
|
|
2526
|
+
}
|
|
2527
|
+
encode(frame) {
|
|
2528
|
+
try {
|
|
2529
|
+
const keyFrame = this.shouldGenerateKeyFrame();
|
|
2530
|
+
const encodeOptions = {
|
|
2531
|
+
keyFrame
|
|
2532
|
+
};
|
|
2533
|
+
this.encoder.encode(frame, encodeOptions);
|
|
2534
|
+
this.frameCount++;
|
|
2535
|
+
} finally {
|
|
2536
|
+
frame.close();
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
setKeyFrameInterval(interval) {
|
|
2540
|
+
this.keyFrameInterval = Math.max(1, interval);
|
|
2541
|
+
console.info("Key frame interval set to:", this.keyFrameInterval);
|
|
2542
|
+
}
|
|
2543
|
+
shouldGenerateKeyFrame() {
|
|
2544
|
+
if (this.frameCount === 0) {
|
|
2545
|
+
return true;
|
|
2546
|
+
}
|
|
2547
|
+
return this.frameCount % this.keyFrameInterval === 0;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
1736
2550
|
const MICROSECONDS_PER_SECOND = 1e6;
|
|
1737
2551
|
const DEFAULT_FPS = 30;
|
|
1738
2552
|
function normalizeFps(value) {
|
|
@@ -2203,217 +3017,109 @@ function computeAnimationState(animation, globalTimeUs) {
|
|
|
2203
3017
|
visible: true
|
|
2204
3018
|
};
|
|
2205
3019
|
}
|
|
2206
|
-
class
|
|
3020
|
+
class ExportWorker {
|
|
2207
3021
|
channel;
|
|
2208
3022
|
composer = null;
|
|
2209
|
-
|
|
2210
|
-
sessionId = null;
|
|
2211
|
-
downstreamPort = null;
|
|
2212
|
-
upstreamPort = null;
|
|
3023
|
+
encoder = null;
|
|
2213
3024
|
instructions = null;
|
|
2214
3025
|
imageMap = /* @__PURE__ */ new Map();
|
|
2215
3026
|
constructor() {
|
|
2216
3027
|
this.channel = new WorkerChannel(self, {
|
|
2217
|
-
name: "
|
|
2218
|
-
timeout:
|
|
3028
|
+
name: "ExportWorker",
|
|
3029
|
+
timeout: 6e4
|
|
2219
3030
|
});
|
|
2220
3031
|
this.setupHandlers();
|
|
2221
3032
|
}
|
|
2222
3033
|
setupHandlers() {
|
|
2223
3034
|
this.channel.registerHandler("configure", this.handleConfigure.bind(this));
|
|
2224
|
-
this.channel.registerHandler("connect", this.handleConnect.bind(this));
|
|
2225
|
-
this.channel.registerHandler("flush", this.handleFlush.bind(this));
|
|
2226
|
-
this.channel.registerHandler("get_stats", this.handleGetStats.bind(this));
|
|
2227
3035
|
this.channel.registerHandler("install_instructions", this.handleInstallInstructions.bind(this));
|
|
2228
3036
|
this.channel.registerHandler("receive_image", this.handleReceiveImage.bind(this));
|
|
2229
3037
|
this.channel.registerHandler("dispose_clip", this.handleDisposeClip.bind(this));
|
|
3038
|
+
this.channel.registerHandler("flush", this.handleFlush.bind(this));
|
|
2230
3039
|
this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));
|
|
2231
3040
|
this.channel.receiveStream(this.handleReceiveStream.bind(this));
|
|
2232
3041
|
}
|
|
2233
|
-
/**
|
|
2234
|
-
* Unified connect handler used by stream pipeline
|
|
2235
|
-
*/
|
|
2236
|
-
async handleConnect(payload) {
|
|
2237
|
-
const { port, direction, sessionId } = payload;
|
|
2238
|
-
if (sessionId && !this.sessionId) {
|
|
2239
|
-
this.sessionId = sessionId;
|
|
2240
|
-
}
|
|
2241
|
-
if (direction === "upstream") {
|
|
2242
|
-
this.upstreamPort = port;
|
|
2243
|
-
const channel = new WorkerChannel(port, {
|
|
2244
|
-
name: "VideoCompose-Decode",
|
|
2245
|
-
timeout: 3e4
|
|
2246
|
-
});
|
|
2247
|
-
channel.receiveStream(this.handleReceiveStream.bind(this));
|
|
2248
|
-
}
|
|
2249
|
-
if (direction === "downstream") {
|
|
2250
|
-
this.downstreamPort = port;
|
|
2251
|
-
}
|
|
2252
|
-
return { success: true };
|
|
2253
|
-
}
|
|
2254
|
-
/**
|
|
2255
|
-
* Configure composer
|
|
2256
|
-
* According to docs/impl/14-config, only reinitialize when initial=true
|
|
2257
|
-
*/
|
|
2258
3042
|
async handleConfigure(payload) {
|
|
2259
|
-
const {
|
|
2260
|
-
|
|
2261
|
-
const hasValidFps = config.fps > 0;
|
|
2262
|
-
if (!hasValidDimensions || !hasValidFps) {
|
|
3043
|
+
const { compose, encode } = payload;
|
|
3044
|
+
if (compose.width <= 0 || compose.height <= 0 || compose.fps <= 0) {
|
|
2263
3045
|
throw new Error(
|
|
2264
|
-
`
|
|
3046
|
+
`ExportWorker: invalid config width=${compose.width}, height=${compose.height}, fps=${compose.fps}`
|
|
2265
3047
|
);
|
|
2266
3048
|
}
|
|
2267
|
-
if (
|
|
2268
|
-
this.
|
|
2269
|
-
}
|
|
2270
|
-
if (initial || !this.composer) {
|
|
2271
|
-
if (this.composer) {
|
|
2272
|
-
this.composer.dispose();
|
|
2273
|
-
this.composeStream = null;
|
|
2274
|
-
}
|
|
2275
|
-
this.composer = new VideoComposer(config);
|
|
2276
|
-
} else {
|
|
2277
|
-
this.composer.updateConfig(config);
|
|
3049
|
+
if (this.composer) {
|
|
3050
|
+
this.composer.dispose();
|
|
2278
3051
|
}
|
|
2279
|
-
this.
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
return {
|
|
2284
|
-
success: true,
|
|
2285
|
-
config: this.composer.config
|
|
2286
|
-
};
|
|
3052
|
+
this.composer = new VideoComposer(compose);
|
|
3053
|
+
this.encoder = new VideoChunkEncoder(encode);
|
|
3054
|
+
await this.encoder.initialize();
|
|
3055
|
+
this.channel.state = WorkerState.Ready;
|
|
3056
|
+
return { success: true };
|
|
2287
3057
|
}
|
|
2288
3058
|
async handleReceiveStream(stream, metadata) {
|
|
2289
|
-
if (!this.composer) {
|
|
2290
|
-
console.error("[
|
|
3059
|
+
if (!this.composer || !this.encoder) {
|
|
3060
|
+
console.error("[ExportWorker] Not configured");
|
|
2291
3061
|
return;
|
|
2292
3062
|
}
|
|
2293
3063
|
if (metadata?.instructions) {
|
|
2294
|
-
|
|
3064
|
+
this.handleInstallInstructions(metadata.instructions);
|
|
2295
3065
|
}
|
|
2296
3066
|
if (!this.instructions) {
|
|
2297
|
-
console.error("[
|
|
3067
|
+
console.error("[ExportWorker] No instructions");
|
|
2298
3068
|
return;
|
|
2299
3069
|
}
|
|
2300
3070
|
const mainLayer = this.instructions.layers.find(
|
|
2301
3071
|
(l) => l.type === "video" && !l.payload.attachmentId
|
|
2302
3072
|
);
|
|
2303
|
-
const
|
|
3073
|
+
const clipTrimStartUs = mainLayer?.type === "video" ? mainLayer.payload.trimStartUs ?? 0 : 0;
|
|
2304
3074
|
const timeline = this.instructions.baseConfig.timeline;
|
|
2305
|
-
const
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
3075
|
+
const fps = timeline?.compositionFps ?? 30;
|
|
3076
|
+
const windowStartUs = metadata?.windowStartUs ?? clipTrimStartUs;
|
|
3077
|
+
const windowEndUs = metadata?.windowEndUs ?? clipTrimStartUs + (timeline?.clipDurationUs ?? Infinity);
|
|
3078
|
+
const windowDurationUs = windowEndUs - windowStartUs;
|
|
3079
|
+
const windowToClipOffsetUs = windowStartUs - clipTrimStartUs;
|
|
3080
|
+
const fpsConverter = new FrameRateConverter(fps, windowDurationUs, windowStartUs);
|
|
2310
3081
|
const cfrStream = stream.pipeThrough(fpsConverter.createStream());
|
|
2311
|
-
const
|
|
3082
|
+
const composeRequestStream = cfrStream.pipeThrough(
|
|
2312
3083
|
new TransformStream({
|
|
2313
3084
|
transform: (frame, controller) => {
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
frame.
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
3085
|
+
let composeFrame = frame;
|
|
3086
|
+
if (windowToClipOffsetUs > 0) {
|
|
3087
|
+
composeFrame = new VideoFrame(frame, {
|
|
3088
|
+
timestamp: (frame.timestamp ?? 0) + windowToClipOffsetUs
|
|
3089
|
+
});
|
|
3090
|
+
frame.close();
|
|
3091
|
+
}
|
|
3092
|
+
const request = this.buildComposeRequest(this.instructions, composeFrame);
|
|
3093
|
+
if (!request) {
|
|
3094
|
+
composeFrame.close();
|
|
3095
|
+
return;
|
|
2325
3096
|
}
|
|
3097
|
+
controller.enqueue(request);
|
|
2326
3098
|
}
|
|
2327
3099
|
})
|
|
2328
3100
|
);
|
|
2329
3101
|
const composeStream = this.composer.createStreams();
|
|
2330
|
-
|
|
2331
|
-
|
|
3102
|
+
const encodingTransform = this.encoder.createStream();
|
|
3103
|
+
composeRequestStream.pipeTo(composeStream.writable).catch((error) => {
|
|
3104
|
+
console.error("[ExportWorker] compose pipe error:", error);
|
|
3105
|
+
});
|
|
3106
|
+
const encodedStream = composeStream.readable.pipeThrough(encodingTransform);
|
|
3107
|
+
this.channel.sendStream(encodedStream, {
|
|
3108
|
+
streamType: "video"
|
|
2332
3109
|
});
|
|
2333
|
-
const meta = {
|
|
2334
|
-
...metadata,
|
|
2335
|
-
streamType: "video",
|
|
2336
|
-
sessionId: this.sessionId
|
|
2337
|
-
};
|
|
2338
|
-
if (this.downstreamPort) {
|
|
2339
|
-
const encodeChannel = new WorkerChannel(this.downstreamPort, {
|
|
2340
|
-
name: "VideoCompose-Encode",
|
|
2341
|
-
timeout: 3e4
|
|
2342
|
-
});
|
|
2343
|
-
encodeChannel.sendStream(composeStream.readable, meta);
|
|
2344
|
-
} else {
|
|
2345
|
-
this.channel.sendStream(composeStream.readable, meta);
|
|
2346
|
-
}
|
|
2347
|
-
}
|
|
2348
|
-
// private handleGetStream(): ReadableStream<VideoFrame> | undefined {
|
|
2349
|
-
// return this.composer?.createStreams()?.cacheStream;
|
|
2350
|
-
// }
|
|
2351
|
-
/**
|
|
2352
|
-
* Flush the composition pipeline
|
|
2353
|
-
*/
|
|
2354
|
-
async handleFlush() {
|
|
2355
|
-
try {
|
|
2356
|
-
this.channel.notify("flush_complete", {});
|
|
2357
|
-
return { success: true };
|
|
2358
|
-
} catch (error) {
|
|
2359
|
-
throw {
|
|
2360
|
-
code: "FLUSH_ERROR",
|
|
2361
|
-
message: error.message
|
|
2362
|
-
};
|
|
2363
|
-
}
|
|
2364
|
-
}
|
|
2365
|
-
/**
|
|
2366
|
-
* Get composer statistics
|
|
2367
|
-
*/
|
|
2368
|
-
async handleGetStats() {
|
|
2369
|
-
return {
|
|
2370
|
-
configured: this.composer !== null,
|
|
2371
|
-
config: this.composer?.config,
|
|
2372
|
-
streaming: this.composeStream !== null
|
|
2373
|
-
};
|
|
2374
|
-
}
|
|
2375
|
-
/**
|
|
2376
|
-
* Dispose worker and cleanup resources
|
|
2377
|
-
*/
|
|
2378
|
-
async handleDispose() {
|
|
2379
|
-
if (this.composer) {
|
|
2380
|
-
this.composer.dispose();
|
|
2381
|
-
this.composer = null;
|
|
2382
|
-
}
|
|
2383
|
-
this.composeStream = null;
|
|
2384
|
-
this.downstreamPort?.close();
|
|
2385
|
-
this.upstreamPort?.close();
|
|
2386
|
-
this.downstreamPort = null;
|
|
2387
|
-
this.upstreamPort = null;
|
|
2388
|
-
this.imageMap.forEach((bitmap) => bitmap.close());
|
|
2389
|
-
this.imageMap.clear();
|
|
2390
|
-
this.instructions = null;
|
|
2391
|
-
this.channel.state = WorkerState.Disposed;
|
|
2392
|
-
return { success: true };
|
|
2393
3110
|
}
|
|
2394
|
-
|
|
2395
|
-
const {
|
|
2396
|
-
if (!this.sessionId) {
|
|
2397
|
-
this.sessionId = clipId;
|
|
2398
|
-
}
|
|
3111
|
+
handleInstallInstructions(payload) {
|
|
3112
|
+
const { revision } = payload;
|
|
2399
3113
|
if (this.instructions && this.instructions.revision > revision) {
|
|
2400
3114
|
return { success: false };
|
|
2401
3115
|
}
|
|
2402
3116
|
this.instructions = payload;
|
|
2403
3117
|
return { success: true };
|
|
2404
3118
|
}
|
|
2405
|
-
/**
|
|
2406
|
-
* Receive image data with instructions (aligned with video pipeline)
|
|
2407
|
-
* Note: ImageBitmap is required because VideoFrame constructor in Worker context
|
|
2408
|
-
* only accepts ImageBitmap/OffscreenCanvas, not HTMLImageElement or Blob
|
|
2409
|
-
*/
|
|
2410
3119
|
async handleReceiveImage(payload) {
|
|
2411
|
-
const { resourceId,
|
|
2412
|
-
if (!this.sessionId) {
|
|
2413
|
-
this.sessionId = sessionId;
|
|
2414
|
-
}
|
|
3120
|
+
const { resourceId, imageBitmap, instructions } = payload;
|
|
2415
3121
|
if (instructions) {
|
|
2416
|
-
|
|
3122
|
+
this.handleInstallInstructions(instructions);
|
|
2417
3123
|
}
|
|
2418
3124
|
const existing = this.imageMap.get(resourceId);
|
|
2419
3125
|
if (existing) {
|
|
@@ -2429,33 +3135,19 @@ class VideoComposeWorker {
|
|
|
2429
3135
|
}
|
|
2430
3136
|
return { success: true };
|
|
2431
3137
|
}
|
|
2432
|
-
async handleDisposeClip() {
|
|
2433
|
-
this.instructions = null;
|
|
2434
|
-
this.downstreamPort?.close();
|
|
2435
|
-
this.upstreamPort?.close();
|
|
2436
|
-
this.downstreamPort = null;
|
|
2437
|
-
this.upstreamPort = null;
|
|
2438
|
-
this.imageMap.forEach((bitmap) => bitmap.close());
|
|
2439
|
-
this.imageMap.clear();
|
|
2440
|
-
return { success: true };
|
|
2441
|
-
}
|
|
2442
3138
|
async startImageFrameStream() {
|
|
2443
|
-
if (!this.instructions || !this.composer) {
|
|
3139
|
+
if (!this.instructions || !this.composer || !this.encoder) {
|
|
2444
3140
|
return;
|
|
2445
3141
|
}
|
|
2446
3142
|
const timeline = this.instructions.baseConfig.timeline;
|
|
2447
|
-
if (!timeline)
|
|
2448
|
-
return;
|
|
2449
|
-
}
|
|
3143
|
+
if (!timeline) return;
|
|
2450
3144
|
const mainLayer = this.instructions.layers.find((l) => !l.payload.attachmentId);
|
|
2451
3145
|
if (!mainLayer) return;
|
|
2452
3146
|
const mainResourceId = mainLayer.payload.resourceId;
|
|
2453
3147
|
const imageBitmap = this.imageMap.get(mainResourceId);
|
|
2454
|
-
if (!imageBitmap)
|
|
2455
|
-
console.warn("[VideoComposeWorker] Main track ImageBitmap not found:", mainResourceId);
|
|
2456
|
-
return;
|
|
2457
|
-
}
|
|
3148
|
+
if (!imageBitmap) return;
|
|
2458
3149
|
const composeStream = this.composer.createStreams();
|
|
3150
|
+
const encodingTransform = this.encoder.createStream();
|
|
2459
3151
|
const { clipDurationUs, compositionFps } = timeline;
|
|
2460
3152
|
let currentTimeUs = 0;
|
|
2461
3153
|
const readableStream = new ReadableStream({
|
|
@@ -2468,36 +3160,49 @@ class VideoComposeWorker {
|
|
|
2468
3160
|
timestamp: currentTimeUs,
|
|
2469
3161
|
duration: frameDurationFromFps(compositionFps)
|
|
2470
3162
|
});
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
} else {
|
|
2476
|
-
videoFrame.close();
|
|
2477
|
-
}
|
|
2478
|
-
currentTimeUs += frameDurationFromFps(compositionFps);
|
|
2479
|
-
} catch (error) {
|
|
3163
|
+
const request = this.buildComposeRequest(this.instructions, videoFrame);
|
|
3164
|
+
if (request) {
|
|
3165
|
+
controller.enqueue(request);
|
|
3166
|
+
} else {
|
|
2480
3167
|
videoFrame.close();
|
|
2481
|
-
throw error;
|
|
2482
3168
|
}
|
|
3169
|
+
currentTimeUs += frameDurationFromFps(compositionFps);
|
|
2483
3170
|
}
|
|
2484
3171
|
});
|
|
2485
3172
|
readableStream.pipeTo(composeStream.writable).catch((error) => {
|
|
2486
|
-
console.error("[
|
|
3173
|
+
console.error("[ExportWorker] image frame stream error:", error);
|
|
2487
3174
|
});
|
|
2488
|
-
const
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
};
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
3175
|
+
const encodedStream = composeStream.readable.pipeThrough(encodingTransform);
|
|
3176
|
+
this.channel.sendStream(encodedStream, {
|
|
3177
|
+
streamType: "video"
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
async handleDisposeClip() {
|
|
3181
|
+
this.instructions = null;
|
|
3182
|
+
this.imageMap.forEach((bitmap) => bitmap.close());
|
|
3183
|
+
this.imageMap.clear();
|
|
3184
|
+
return { success: true };
|
|
3185
|
+
}
|
|
3186
|
+
async handleFlush() {
|
|
3187
|
+
if (this.encoder) {
|
|
3188
|
+
await this.encoder.flush();
|
|
3189
|
+
}
|
|
3190
|
+
return { success: true };
|
|
3191
|
+
}
|
|
3192
|
+
async handleDispose() {
|
|
3193
|
+
if (this.composer) {
|
|
3194
|
+
this.composer.dispose();
|
|
3195
|
+
this.composer = null;
|
|
2500
3196
|
}
|
|
3197
|
+
if (this.encoder) {
|
|
3198
|
+
await this.encoder.close();
|
|
3199
|
+
this.encoder = null;
|
|
3200
|
+
}
|
|
3201
|
+
this.imageMap.forEach((bitmap) => bitmap.close());
|
|
3202
|
+
this.imageMap.clear();
|
|
3203
|
+
this.instructions = null;
|
|
3204
|
+
this.channel.state = WorkerState.Disposed;
|
|
3205
|
+
return { success: true };
|
|
2501
3206
|
}
|
|
2502
3207
|
buildComposeRequest(instruction, frame) {
|
|
2503
3208
|
const clipRelativeTime = frame.timestamp ?? 0;
|
|
@@ -2506,20 +3211,16 @@ class VideoComposeWorker {
|
|
|
2506
3211
|
return null;
|
|
2507
3212
|
}
|
|
2508
3213
|
const activeLayers = resolveActiveLayers(instruction.layers, clipRelativeTime);
|
|
2509
|
-
if (!activeLayers.length)
|
|
2510
|
-
return null;
|
|
2511
|
-
}
|
|
3214
|
+
if (!activeLayers.length) return null;
|
|
2512
3215
|
const clipStartUs = instruction.baseConfig.timeline?.clipStartUs ?? 0;
|
|
2513
3216
|
const globalTimeUs = clipStartUs + clipRelativeTime;
|
|
2514
3217
|
const layers = activeLayers.map((layer) => materializeLayer(layer, frame, this.imageMap, globalTimeUs)).filter((layer) => layer !== null);
|
|
2515
|
-
if (!layers.length)
|
|
2516
|
-
return null;
|
|
2517
|
-
}
|
|
3218
|
+
if (!layers.length) return null;
|
|
2518
3219
|
return {
|
|
2519
3220
|
timeUs: clipRelativeTime,
|
|
2520
3221
|
globalTimeUs,
|
|
2521
3222
|
layers,
|
|
2522
|
-
transition:
|
|
3223
|
+
transition: ExportWorker.buildTransition(
|
|
2523
3224
|
instruction.transitions,
|
|
2524
3225
|
clipRelativeTime,
|
|
2525
3226
|
instruction.baseConfig.timeline
|
|
@@ -2531,9 +3232,7 @@ class VideoComposeWorker {
|
|
|
2531
3232
|
const { startUs, endUs } = transition.range;
|
|
2532
3233
|
return timeUs >= startUs && timeUs < endUs;
|
|
2533
3234
|
});
|
|
2534
|
-
if (!entry)
|
|
2535
|
-
return void 0;
|
|
2536
|
-
}
|
|
3235
|
+
if (!entry) return void 0;
|
|
2537
3236
|
const durationUs = entry.range.endUs - entry.range.startUs;
|
|
2538
3237
|
const progress = durationUs > 0 ? (timeUs - entry.range.startUs) / durationUs : 0;
|
|
2539
3238
|
return {
|
|
@@ -2545,13 +3244,12 @@ class VideoComposeWorker {
|
|
|
2545
3244
|
};
|
|
2546
3245
|
}
|
|
2547
3246
|
}
|
|
2548
|
-
const worker = new
|
|
3247
|
+
const worker = new ExportWorker();
|
|
2549
3248
|
self.addEventListener("beforeunload", () => {
|
|
2550
3249
|
worker["handleDispose"]();
|
|
2551
3250
|
});
|
|
2552
|
-
const
|
|
3251
|
+
const export_worker = null;
|
|
2553
3252
|
export {
|
|
2554
|
-
|
|
2555
|
-
videoCompose_worker as default
|
|
3253
|
+
export_worker as default
|
|
2556
3254
|
};
|
|
2557
|
-
//# sourceMappingURL=
|
|
3255
|
+
//# sourceMappingURL=export.worker.SahP9aVK.js.map
|