@meframe/core 0.3.6 → 0.3.7
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/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/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.BYttrqTQ.js} +872 -217
- package/dist/workers/stages/export/export.worker.BYttrqTQ.js.map +1 -0
- package/dist/workers/worker-manifest.json +1 -3
- package/package.json +1 -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
|
@@ -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,229 @@ 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
|
+
if (this.encoder?.state === "configured") {
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
const isSupported = await this.isConfigSupported(this.config);
|
|
2329
|
+
if (!isSupported.supported) {
|
|
2330
|
+
throw new Error(`Codec not supported: ${JSON.stringify(this.config)}`);
|
|
2331
|
+
}
|
|
2332
|
+
this.encoder = this.createEncoder({
|
|
2333
|
+
output: this.handleOutput.bind(this),
|
|
2334
|
+
error: this.handleError.bind(this)
|
|
2335
|
+
});
|
|
2336
|
+
this.encoder.configure(this.config);
|
|
2337
|
+
}
|
|
2338
|
+
async reconfigure(config) {
|
|
2339
|
+
if (!config || Object.keys(config).length === 0) {
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
const nextConfig = { ...this.config, ...config };
|
|
2343
|
+
if (this.configsEqual(this.config, nextConfig)) {
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
if (!this.encoder) {
|
|
2347
|
+
this.config = nextConfig;
|
|
2348
|
+
await this.initialize();
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
if (this.encoder.state === "configured") {
|
|
2352
|
+
await this.encoder.flush();
|
|
2353
|
+
}
|
|
2354
|
+
const isSupported = await this.isConfigSupported(nextConfig);
|
|
2355
|
+
if (!isSupported.supported) {
|
|
2356
|
+
throw new Error(`New configuration not supported: ${nextConfig.codec}`);
|
|
2357
|
+
}
|
|
2358
|
+
this.config = nextConfig;
|
|
2359
|
+
this.encoder.configure(this.config);
|
|
2360
|
+
}
|
|
2361
|
+
async flush() {
|
|
2362
|
+
if (!this.encoder) {
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2365
|
+
await this.encoder.flush();
|
|
2366
|
+
}
|
|
2367
|
+
async reset() {
|
|
2368
|
+
if (!this.encoder) {
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
this.encoder.reset();
|
|
2372
|
+
this.onReset();
|
|
2373
|
+
}
|
|
2374
|
+
async close() {
|
|
2375
|
+
if (!this.encoder) {
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
if (this.encoder.state === "configured") {
|
|
2379
|
+
await this.encoder.flush();
|
|
2380
|
+
}
|
|
2381
|
+
this.encoder.close();
|
|
2382
|
+
this.encoder = void 0;
|
|
2383
|
+
}
|
|
2384
|
+
get isReady() {
|
|
2385
|
+
return this.encoder?.state === "configured";
|
|
2386
|
+
}
|
|
2387
|
+
get queueSize() {
|
|
2388
|
+
return this.encoder?.encodeQueueSize ?? 0;
|
|
2389
|
+
}
|
|
2390
|
+
handleOutput(chunk, metadata) {
|
|
2391
|
+
if (this.controller) {
|
|
2392
|
+
try {
|
|
2393
|
+
this.controller.enqueue({ chunk, metadata });
|
|
2394
|
+
} catch (error) {
|
|
2395
|
+
if (!(error instanceof TypeError && error.message.includes("closed"))) {
|
|
2396
|
+
throw error;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
handleError(error) {
|
|
2402
|
+
console.error(`[${this.getEncoderType()}Encoder] Encode error:`, {
|
|
2403
|
+
name: error.name,
|
|
2404
|
+
message: error.message,
|
|
2405
|
+
encoderState: this.encoder?.state,
|
|
2406
|
+
queueSize: this.queueSize,
|
|
2407
|
+
platform: typeof navigator !== "undefined" ? navigator.platform : "unknown"
|
|
2408
|
+
});
|
|
2409
|
+
this.controller?.error(error);
|
|
2410
|
+
}
|
|
2411
|
+
// Hook for subclasses to handle reset
|
|
2412
|
+
onReset() {
|
|
2413
|
+
}
|
|
2414
|
+
/**
|
|
2415
|
+
* Create transform stream for encoding
|
|
2416
|
+
* Implements common stream logic with backpressure handling
|
|
2417
|
+
*/
|
|
2418
|
+
createStream() {
|
|
2419
|
+
return new TransformStream(
|
|
2420
|
+
{
|
|
2421
|
+
start: async (controller) => {
|
|
2422
|
+
this.controller = controller;
|
|
2423
|
+
if (!this.isReady) {
|
|
2424
|
+
await this.initialize();
|
|
2425
|
+
}
|
|
2426
|
+
},
|
|
2427
|
+
transform: async (input) => {
|
|
2428
|
+
if (!this.encoder || this.encoder.state !== "configured") {
|
|
2429
|
+
throw new Error("Encoder not configured");
|
|
2430
|
+
}
|
|
2431
|
+
if (this.encoder.encodeQueueSize >= this.encodeQueueThreshold) {
|
|
2432
|
+
await new Promise((resolve) => {
|
|
2433
|
+
const check = () => {
|
|
2434
|
+
if (!this.encoder || this.encoder.encodeQueueSize < this.encodeQueueThreshold - 1) {
|
|
2435
|
+
resolve();
|
|
2436
|
+
} else {
|
|
2437
|
+
setTimeout(check, 10);
|
|
2438
|
+
}
|
|
2439
|
+
};
|
|
2440
|
+
check();
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
const frame = input.frame || input;
|
|
2444
|
+
this.encode(frame);
|
|
2445
|
+
},
|
|
2446
|
+
flush: async () => {
|
|
2447
|
+
await this.flush();
|
|
2448
|
+
}
|
|
2449
|
+
},
|
|
2450
|
+
// Queuing strategy with backpressure configuration
|
|
2451
|
+
{
|
|
2452
|
+
highWaterMark: this.highWaterMark,
|
|
2453
|
+
size: () => 1
|
|
2454
|
+
// Count-based
|
|
2455
|
+
}
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
class VideoChunkEncoder extends BaseEncoder {
|
|
2460
|
+
static DEFAULT_HIGH_WATER_MARK = 2;
|
|
2461
|
+
static DEFAULT_ENCODE_QUEUE_THRESHOLD = 8;
|
|
2462
|
+
highWaterMark;
|
|
2463
|
+
encodeQueueThreshold;
|
|
2464
|
+
frameCount = 0;
|
|
2465
|
+
// Default 1 second at 30fps for better social media compatibility
|
|
2466
|
+
keyFrameInterval = 30;
|
|
2467
|
+
constructor(config) {
|
|
2468
|
+
super(config);
|
|
2469
|
+
this.highWaterMark = config.backpressure?.highWaterMark ?? VideoChunkEncoder.DEFAULT_HIGH_WATER_MARK;
|
|
2470
|
+
this.encodeQueueThreshold = config.backpressure?.encodeQueueThreshold ?? VideoChunkEncoder.DEFAULT_ENCODE_QUEUE_THRESHOLD;
|
|
2471
|
+
if (config.keyFrameInterval !== void 0) {
|
|
2472
|
+
this.keyFrameInterval = Math.max(1, config.keyFrameInterval);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
async isConfigSupported(config) {
|
|
2476
|
+
const result = await VideoEncoder.isConfigSupported(config);
|
|
2477
|
+
return { supported: result.supported ?? false };
|
|
2478
|
+
}
|
|
2479
|
+
createEncoder(init) {
|
|
2480
|
+
return new VideoEncoder(init);
|
|
2481
|
+
}
|
|
2482
|
+
getEncoderType() {
|
|
2483
|
+
return "Video";
|
|
2484
|
+
}
|
|
2485
|
+
onReset() {
|
|
2486
|
+
this.frameCount = 0;
|
|
2487
|
+
}
|
|
2488
|
+
encode(frame) {
|
|
2489
|
+
const keyFrame = this.shouldGenerateKeyFrame();
|
|
2490
|
+
const encodeOptions = {
|
|
2491
|
+
keyFrame
|
|
2492
|
+
};
|
|
2493
|
+
this.encoder.encode(frame, encodeOptions);
|
|
2494
|
+
this.frameCount++;
|
|
2495
|
+
frame.close();
|
|
2496
|
+
}
|
|
2497
|
+
setKeyFrameInterval(interval) {
|
|
2498
|
+
this.keyFrameInterval = Math.max(1, interval);
|
|
2499
|
+
}
|
|
2500
|
+
shouldGenerateKeyFrame() {
|
|
2501
|
+
if (this.frameCount === 0) {
|
|
2502
|
+
return true;
|
|
2503
|
+
}
|
|
2504
|
+
return this.frameCount % this.keyFrameInterval === 0;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
1736
2507
|
const MICROSECONDS_PER_SECOND = 1e6;
|
|
1737
2508
|
const DEFAULT_FPS = 30;
|
|
1738
2509
|
function normalizeFps(value) {
|
|
@@ -2203,217 +2974,109 @@ function computeAnimationState(animation, globalTimeUs) {
|
|
|
2203
2974
|
visible: true
|
|
2204
2975
|
};
|
|
2205
2976
|
}
|
|
2206
|
-
class
|
|
2977
|
+
class ExportWorker {
|
|
2207
2978
|
channel;
|
|
2208
2979
|
composer = null;
|
|
2209
|
-
|
|
2210
|
-
sessionId = null;
|
|
2211
|
-
downstreamPort = null;
|
|
2212
|
-
upstreamPort = null;
|
|
2980
|
+
encoder = null;
|
|
2213
2981
|
instructions = null;
|
|
2214
2982
|
imageMap = /* @__PURE__ */ new Map();
|
|
2215
2983
|
constructor() {
|
|
2216
2984
|
this.channel = new WorkerChannel(self, {
|
|
2217
|
-
name: "
|
|
2218
|
-
timeout:
|
|
2985
|
+
name: "ExportWorker",
|
|
2986
|
+
timeout: 6e4
|
|
2219
2987
|
});
|
|
2220
2988
|
this.setupHandlers();
|
|
2221
2989
|
}
|
|
2222
2990
|
setupHandlers() {
|
|
2223
2991
|
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
2992
|
this.channel.registerHandler("install_instructions", this.handleInstallInstructions.bind(this));
|
|
2228
2993
|
this.channel.registerHandler("receive_image", this.handleReceiveImage.bind(this));
|
|
2229
2994
|
this.channel.registerHandler("dispose_clip", this.handleDisposeClip.bind(this));
|
|
2995
|
+
this.channel.registerHandler("flush", this.handleFlush.bind(this));
|
|
2230
2996
|
this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));
|
|
2231
2997
|
this.channel.receiveStream(this.handleReceiveStream.bind(this));
|
|
2232
2998
|
}
|
|
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
2999
|
async handleConfigure(payload) {
|
|
2259
|
-
const {
|
|
2260
|
-
|
|
2261
|
-
const hasValidFps = config.fps > 0;
|
|
2262
|
-
if (!hasValidDimensions || !hasValidFps) {
|
|
3000
|
+
const { compose, encode } = payload;
|
|
3001
|
+
if (compose.width <= 0 || compose.height <= 0 || compose.fps <= 0) {
|
|
2263
3002
|
throw new Error(
|
|
2264
|
-
`
|
|
3003
|
+
`ExportWorker: invalid config width=${compose.width}, height=${compose.height}, fps=${compose.fps}`
|
|
2265
3004
|
);
|
|
2266
3005
|
}
|
|
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);
|
|
3006
|
+
if (this.composer) {
|
|
3007
|
+
this.composer.dispose();
|
|
2278
3008
|
}
|
|
2279
|
-
this.
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
return {
|
|
2284
|
-
success: true,
|
|
2285
|
-
config: this.composer.config
|
|
2286
|
-
};
|
|
3009
|
+
this.composer = new VideoComposer(compose);
|
|
3010
|
+
this.encoder = new VideoChunkEncoder(encode);
|
|
3011
|
+
await this.encoder.initialize();
|
|
3012
|
+
this.channel.state = WorkerState.Ready;
|
|
3013
|
+
return { success: true };
|
|
2287
3014
|
}
|
|
2288
3015
|
async handleReceiveStream(stream, metadata) {
|
|
2289
|
-
if (!this.composer) {
|
|
2290
|
-
console.error("[
|
|
3016
|
+
if (!this.composer || !this.encoder) {
|
|
3017
|
+
console.error("[ExportWorker] Not configured");
|
|
2291
3018
|
return;
|
|
2292
3019
|
}
|
|
2293
3020
|
if (metadata?.instructions) {
|
|
2294
|
-
|
|
3021
|
+
this.handleInstallInstructions(metadata.instructions);
|
|
2295
3022
|
}
|
|
2296
3023
|
if (!this.instructions) {
|
|
2297
|
-
console.error("[
|
|
3024
|
+
console.error("[ExportWorker] No instructions");
|
|
2298
3025
|
return;
|
|
2299
3026
|
}
|
|
2300
3027
|
const mainLayer = this.instructions.layers.find(
|
|
2301
3028
|
(l) => l.type === "video" && !l.payload.attachmentId
|
|
2302
3029
|
);
|
|
2303
|
-
const
|
|
3030
|
+
const clipTrimStartUs = mainLayer?.type === "video" ? mainLayer.payload.trimStartUs ?? 0 : 0;
|
|
2304
3031
|
const timeline = this.instructions.baseConfig.timeline;
|
|
2305
|
-
const
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
3032
|
+
const fps = timeline?.compositionFps ?? 30;
|
|
3033
|
+
const windowStartUs = metadata?.windowStartUs ?? clipTrimStartUs;
|
|
3034
|
+
const windowEndUs = metadata?.windowEndUs ?? clipTrimStartUs + (timeline?.clipDurationUs ?? Infinity);
|
|
3035
|
+
const windowDurationUs = windowEndUs - windowStartUs;
|
|
3036
|
+
const windowToClipOffsetUs = windowStartUs - clipTrimStartUs;
|
|
3037
|
+
const fpsConverter = new FrameRateConverter(fps, windowDurationUs, windowStartUs);
|
|
2310
3038
|
const cfrStream = stream.pipeThrough(fpsConverter.createStream());
|
|
2311
|
-
const
|
|
3039
|
+
const composeRequestStream = cfrStream.pipeThrough(
|
|
2312
3040
|
new TransformStream({
|
|
2313
3041
|
transform: (frame, controller) => {
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
frame.
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
3042
|
+
let composeFrame = frame;
|
|
3043
|
+
if (windowToClipOffsetUs > 0) {
|
|
3044
|
+
composeFrame = new VideoFrame(frame, {
|
|
3045
|
+
timestamp: (frame.timestamp ?? 0) + windowToClipOffsetUs
|
|
3046
|
+
});
|
|
3047
|
+
frame.close();
|
|
3048
|
+
}
|
|
3049
|
+
const request = this.buildComposeRequest(this.instructions, composeFrame);
|
|
3050
|
+
if (!request) {
|
|
3051
|
+
composeFrame.close();
|
|
3052
|
+
return;
|
|
2325
3053
|
}
|
|
3054
|
+
controller.enqueue(request);
|
|
2326
3055
|
}
|
|
2327
3056
|
})
|
|
2328
3057
|
);
|
|
2329
3058
|
const composeStream = this.composer.createStreams();
|
|
2330
|
-
|
|
2331
|
-
|
|
3059
|
+
const encodingTransform = this.encoder.createStream();
|
|
3060
|
+
composeRequestStream.pipeTo(composeStream.writable).catch((error) => {
|
|
3061
|
+
console.error("[ExportWorker] compose pipe error:", error);
|
|
3062
|
+
});
|
|
3063
|
+
const encodedStream = composeStream.readable.pipeThrough(encodingTransform);
|
|
3064
|
+
this.channel.sendStream(encodedStream, {
|
|
3065
|
+
streamType: "video"
|
|
2332
3066
|
});
|
|
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
3067
|
}
|
|
2394
|
-
|
|
2395
|
-
const {
|
|
2396
|
-
if (!this.sessionId) {
|
|
2397
|
-
this.sessionId = clipId;
|
|
2398
|
-
}
|
|
3068
|
+
handleInstallInstructions(payload) {
|
|
3069
|
+
const { revision } = payload;
|
|
2399
3070
|
if (this.instructions && this.instructions.revision > revision) {
|
|
2400
3071
|
return { success: false };
|
|
2401
3072
|
}
|
|
2402
3073
|
this.instructions = payload;
|
|
2403
3074
|
return { success: true };
|
|
2404
3075
|
}
|
|
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
3076
|
async handleReceiveImage(payload) {
|
|
2411
|
-
const { resourceId,
|
|
2412
|
-
if (!this.sessionId) {
|
|
2413
|
-
this.sessionId = sessionId;
|
|
2414
|
-
}
|
|
3077
|
+
const { resourceId, imageBitmap, instructions } = payload;
|
|
2415
3078
|
if (instructions) {
|
|
2416
|
-
|
|
3079
|
+
this.handleInstallInstructions(instructions);
|
|
2417
3080
|
}
|
|
2418
3081
|
const existing = this.imageMap.get(resourceId);
|
|
2419
3082
|
if (existing) {
|
|
@@ -2429,33 +3092,19 @@ class VideoComposeWorker {
|
|
|
2429
3092
|
}
|
|
2430
3093
|
return { success: true };
|
|
2431
3094
|
}
|
|
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
3095
|
async startImageFrameStream() {
|
|
2443
|
-
if (!this.instructions || !this.composer) {
|
|
3096
|
+
if (!this.instructions || !this.composer || !this.encoder) {
|
|
2444
3097
|
return;
|
|
2445
3098
|
}
|
|
2446
3099
|
const timeline = this.instructions.baseConfig.timeline;
|
|
2447
|
-
if (!timeline)
|
|
2448
|
-
return;
|
|
2449
|
-
}
|
|
3100
|
+
if (!timeline) return;
|
|
2450
3101
|
const mainLayer = this.instructions.layers.find((l) => !l.payload.attachmentId);
|
|
2451
3102
|
if (!mainLayer) return;
|
|
2452
3103
|
const mainResourceId = mainLayer.payload.resourceId;
|
|
2453
3104
|
const imageBitmap = this.imageMap.get(mainResourceId);
|
|
2454
|
-
if (!imageBitmap)
|
|
2455
|
-
console.warn("[VideoComposeWorker] Main track ImageBitmap not found:", mainResourceId);
|
|
2456
|
-
return;
|
|
2457
|
-
}
|
|
3105
|
+
if (!imageBitmap) return;
|
|
2458
3106
|
const composeStream = this.composer.createStreams();
|
|
3107
|
+
const encodingTransform = this.encoder.createStream();
|
|
2459
3108
|
const { clipDurationUs, compositionFps } = timeline;
|
|
2460
3109
|
let currentTimeUs = 0;
|
|
2461
3110
|
const readableStream = new ReadableStream({
|
|
@@ -2468,36 +3117,49 @@ class VideoComposeWorker {
|
|
|
2468
3117
|
timestamp: currentTimeUs,
|
|
2469
3118
|
duration: frameDurationFromFps(compositionFps)
|
|
2470
3119
|
});
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
} else {
|
|
2476
|
-
videoFrame.close();
|
|
2477
|
-
}
|
|
2478
|
-
currentTimeUs += frameDurationFromFps(compositionFps);
|
|
2479
|
-
} catch (error) {
|
|
3120
|
+
const request = this.buildComposeRequest(this.instructions, videoFrame);
|
|
3121
|
+
if (request) {
|
|
3122
|
+
controller.enqueue(request);
|
|
3123
|
+
} else {
|
|
2480
3124
|
videoFrame.close();
|
|
2481
|
-
throw error;
|
|
2482
3125
|
}
|
|
3126
|
+
currentTimeUs += frameDurationFromFps(compositionFps);
|
|
2483
3127
|
}
|
|
2484
3128
|
});
|
|
2485
3129
|
readableStream.pipeTo(composeStream.writable).catch((error) => {
|
|
2486
|
-
console.error("[
|
|
3130
|
+
console.error("[ExportWorker] image frame stream error:", error);
|
|
2487
3131
|
});
|
|
2488
|
-
const
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
};
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
3132
|
+
const encodedStream = composeStream.readable.pipeThrough(encodingTransform);
|
|
3133
|
+
this.channel.sendStream(encodedStream, {
|
|
3134
|
+
streamType: "video"
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
3137
|
+
async handleDisposeClip() {
|
|
3138
|
+
this.instructions = null;
|
|
3139
|
+
this.imageMap.forEach((bitmap) => bitmap.close());
|
|
3140
|
+
this.imageMap.clear();
|
|
3141
|
+
return { success: true };
|
|
3142
|
+
}
|
|
3143
|
+
async handleFlush() {
|
|
3144
|
+
if (this.encoder) {
|
|
3145
|
+
await this.encoder.flush();
|
|
3146
|
+
}
|
|
3147
|
+
return { success: true };
|
|
3148
|
+
}
|
|
3149
|
+
async handleDispose() {
|
|
3150
|
+
if (this.composer) {
|
|
3151
|
+
this.composer.dispose();
|
|
3152
|
+
this.composer = null;
|
|
2500
3153
|
}
|
|
3154
|
+
if (this.encoder) {
|
|
3155
|
+
await this.encoder.close();
|
|
3156
|
+
this.encoder = null;
|
|
3157
|
+
}
|
|
3158
|
+
this.imageMap.forEach((bitmap) => bitmap.close());
|
|
3159
|
+
this.imageMap.clear();
|
|
3160
|
+
this.instructions = null;
|
|
3161
|
+
this.channel.state = WorkerState.Disposed;
|
|
3162
|
+
return { success: true };
|
|
2501
3163
|
}
|
|
2502
3164
|
buildComposeRequest(instruction, frame) {
|
|
2503
3165
|
const clipRelativeTime = frame.timestamp ?? 0;
|
|
@@ -2506,20 +3168,16 @@ class VideoComposeWorker {
|
|
|
2506
3168
|
return null;
|
|
2507
3169
|
}
|
|
2508
3170
|
const activeLayers = resolveActiveLayers(instruction.layers, clipRelativeTime);
|
|
2509
|
-
if (!activeLayers.length)
|
|
2510
|
-
return null;
|
|
2511
|
-
}
|
|
3171
|
+
if (!activeLayers.length) return null;
|
|
2512
3172
|
const clipStartUs = instruction.baseConfig.timeline?.clipStartUs ?? 0;
|
|
2513
3173
|
const globalTimeUs = clipStartUs + clipRelativeTime;
|
|
2514
3174
|
const layers = activeLayers.map((layer) => materializeLayer(layer, frame, this.imageMap, globalTimeUs)).filter((layer) => layer !== null);
|
|
2515
|
-
if (!layers.length)
|
|
2516
|
-
return null;
|
|
2517
|
-
}
|
|
3175
|
+
if (!layers.length) return null;
|
|
2518
3176
|
return {
|
|
2519
3177
|
timeUs: clipRelativeTime,
|
|
2520
3178
|
globalTimeUs,
|
|
2521
3179
|
layers,
|
|
2522
|
-
transition:
|
|
3180
|
+
transition: ExportWorker.buildTransition(
|
|
2523
3181
|
instruction.transitions,
|
|
2524
3182
|
clipRelativeTime,
|
|
2525
3183
|
instruction.baseConfig.timeline
|
|
@@ -2531,9 +3189,7 @@ class VideoComposeWorker {
|
|
|
2531
3189
|
const { startUs, endUs } = transition.range;
|
|
2532
3190
|
return timeUs >= startUs && timeUs < endUs;
|
|
2533
3191
|
});
|
|
2534
|
-
if (!entry)
|
|
2535
|
-
return void 0;
|
|
2536
|
-
}
|
|
3192
|
+
if (!entry) return void 0;
|
|
2537
3193
|
const durationUs = entry.range.endUs - entry.range.startUs;
|
|
2538
3194
|
const progress = durationUs > 0 ? (timeUs - entry.range.startUs) / durationUs : 0;
|
|
2539
3195
|
return {
|
|
@@ -2545,13 +3201,12 @@ class VideoComposeWorker {
|
|
|
2545
3201
|
};
|
|
2546
3202
|
}
|
|
2547
3203
|
}
|
|
2548
|
-
const worker = new
|
|
3204
|
+
const worker = new ExportWorker();
|
|
2549
3205
|
self.addEventListener("beforeunload", () => {
|
|
2550
3206
|
worker["handleDispose"]();
|
|
2551
3207
|
});
|
|
2552
|
-
const
|
|
3208
|
+
const export_worker = null;
|
|
2553
3209
|
export {
|
|
2554
|
-
|
|
2555
|
-
videoCompose_worker as default
|
|
3210
|
+
export_worker as default
|
|
2556
3211
|
};
|
|
2557
|
-
//# sourceMappingURL=
|
|
3212
|
+
//# sourceMappingURL=export.worker.BYttrqTQ.js.map
|