@meframe/core 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -4
- package/dist/Meframe.d.ts.map +1 -1
- package/dist/Meframe.js +0 -3
- package/dist/Meframe.js.map +1 -1
- package/dist/assets/audio-compose.worker-nGVvHD5Q.js +1537 -0
- package/dist/assets/audio-compose.worker-nGVvHD5Q.js.map +1 -0
- package/dist/assets/audio-demux.worker-xwWBtbAe.js +8299 -0
- package/dist/assets/audio-demux.worker-xwWBtbAe.js.map +1 -0
- package/dist/assets/decode.worker-DpWHsc7R.js +1291 -0
- package/dist/assets/decode.worker-DpWHsc7R.js.map +1 -0
- package/dist/assets/encode.worker-nfOb3kw6.js +1026 -0
- package/dist/assets/encode.worker-nfOb3kw6.js.map +1 -0
- package/dist/assets/mux.worker-uEMQY066.js +8019 -0
- package/dist/assets/mux.worker-uEMQY066.js.map +1 -0
- package/dist/assets/video-compose.worker-DPzsC21d.js +1683 -0
- package/dist/assets/video-compose.worker-DPzsC21d.js.map +1 -0
- package/dist/assets/video-demux.worker-D019I7GQ.js +7957 -0
- package/dist/assets/video-demux.worker-D019I7GQ.js.map +1 -0
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +8 -1
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +0 -8
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/types.d.ts +0 -4
- package/dist/config/types.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.d.ts +4 -2
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +7 -13
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/controllers/PreRenderService.d.ts +3 -2
- package/dist/controllers/PreRenderService.d.ts.map +1 -1
- package/dist/controllers/PreRenderService.js.map +1 -1
- package/dist/controllers/PreviewHandle.d.ts +2 -0
- package/dist/controllers/PreviewHandle.d.ts.map +1 -1
- package/dist/controllers/PreviewHandle.js +6 -0
- package/dist/controllers/PreviewHandle.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.d.ts.map +1 -1
- package/dist/controllers/types.d.ts +2 -12
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/event/events.d.ts +5 -59
- package/dist/event/events.d.ts.map +1 -1
- package/dist/event/events.js +1 -6
- package/dist/event/events.js.map +1 -1
- package/dist/model/CompositionModel.js +1 -2
- package/dist/model/CompositionModel.js.map +1 -1
- package/dist/orchestrator/CompositionPlanner.d.ts.map +1 -1
- package/dist/orchestrator/CompositionPlanner.js +1 -0
- package/dist/orchestrator/CompositionPlanner.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +1 -12
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +4 -5
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/orchestrator/types.d.ts +0 -1
- package/dist/orchestrator/types.d.ts.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.d.ts.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.js +3 -2
- package/dist/stages/compose/GlobalAudioSession.js.map +1 -1
- package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
- package/dist/stages/compose/VideoComposer.js +2 -2
- package/dist/stages/compose/VideoComposer.js.map +1 -1
- package/dist/stages/compose/audio-compose.worker.d.ts.map +1 -1
- package/dist/stages/compose/audio-compose.worker.js +0 -1
- package/dist/stages/compose/audio-compose.worker.js.map +1 -1
- package/dist/stages/compose/audio-compose.worker2.js +5 -0
- package/dist/stages/compose/audio-compose.worker2.js.map +1 -0
- package/dist/stages/compose/types.d.ts +1 -0
- package/dist/stages/compose/types.d.ts.map +1 -1
- package/dist/stages/compose/video-compose.worker.d.ts.map +1 -1
- package/dist/stages/compose/video-compose.worker.js +18 -8
- package/dist/stages/compose/video-compose.worker.js.map +1 -1
- package/dist/stages/compose/video-compose.worker2.js +5 -0
- package/dist/stages/compose/video-compose.worker2.js.map +1 -0
- package/dist/stages/decode/AudioChunkDecoder.d.ts.map +1 -1
- package/dist/stages/decode/AudioChunkDecoder.js +0 -1
- package/dist/stages/decode/AudioChunkDecoder.js.map +1 -1
- package/dist/stages/decode/VideoChunkDecoder.d.ts +0 -1
- package/dist/stages/decode/VideoChunkDecoder.d.ts.map +1 -1
- package/dist/stages/decode/VideoChunkDecoder.js +1 -11
- package/dist/stages/decode/VideoChunkDecoder.js.map +1 -1
- package/dist/stages/decode/decode.worker.d.ts.map +1 -1
- package/dist/stages/decode/decode.worker.js +3 -16
- package/dist/stages/decode/decode.worker.js.map +1 -1
- package/dist/stages/decode/decode.worker2.js +5 -0
- package/dist/stages/decode/decode.worker2.js.map +1 -0
- package/dist/stages/demux/MP4Demuxer.d.ts +2 -0
- package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
- package/dist/stages/demux/MP4Demuxer.js +13 -2
- package/dist/stages/demux/MP4Demuxer.js.map +1 -1
- package/dist/stages/demux/audio-demux.worker2.js +5 -0
- package/dist/stages/demux/audio-demux.worker2.js.map +1 -0
- package/dist/stages/demux/video-demux.worker.d.ts +6 -3
- package/dist/stages/demux/video-demux.worker.d.ts.map +1 -1
- package/dist/stages/demux/video-demux.worker.js +5 -27
- package/dist/stages/demux/video-demux.worker.js.map +1 -1
- package/dist/stages/demux/video-demux.worker2.js +5 -0
- package/dist/stages/demux/video-demux.worker2.js.map +1 -0
- package/dist/stages/encode/encode.worker.d.ts.map +1 -1
- package/dist/stages/encode/encode.worker.js +0 -1
- package/dist/stages/encode/encode.worker.js.map +1 -1
- package/dist/stages/encode/encode.worker2.js +5 -0
- package/dist/stages/encode/encode.worker2.js.map +1 -0
- package/dist/stages/load/EventHandlers.d.ts +2 -11
- package/dist/stages/load/EventHandlers.d.ts.map +1 -1
- package/dist/stages/load/EventHandlers.js +1 -24
- package/dist/stages/load/EventHandlers.js.map +1 -1
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +11 -13
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/stages/load/TaskManager.d.ts +1 -1
- package/dist/stages/load/TaskManager.d.ts.map +1 -1
- package/dist/stages/load/TaskManager.js +3 -2
- package/dist/stages/load/TaskManager.js.map +1 -1
- package/dist/stages/load/types.d.ts +2 -0
- package/dist/stages/load/types.d.ts.map +1 -1
- package/dist/stages/mux/mux.worker2.js +5 -0
- package/dist/stages/mux/mux.worker2.js.map +1 -0
- package/dist/vite-plugin.d.ts +17 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +88 -0
- package/dist/vite-plugin.js.map +1 -0
- package/dist/worker/WorkerPool.d.ts +0 -4
- package/dist/worker/WorkerPool.d.ts.map +1 -1
- package/dist/worker/WorkerPool.js +4 -17
- package/dist/worker/WorkerPool.js.map +1 -1
- package/dist/worker/worker-registry.d.ts +12 -0
- package/dist/worker/worker-registry.d.ts.map +1 -0
- package/dist/worker/worker-registry.js +20 -0
- package/dist/worker/worker-registry.js.map +1 -0
- package/package.json +7 -1
|
@@ -0,0 +1,1683 @@
|
|
|
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["ConfigureMux"] = "configure_mux";
|
|
27
|
+
WorkerMessageType2["AddChunk"] = "add_chunk";
|
|
28
|
+
WorkerMessageType2["FinishMux"] = "finish_mux";
|
|
29
|
+
WorkerMessageType2["MuxComplete"] = "mux_complete";
|
|
30
|
+
WorkerMessageType2["PerformanceStats"] = "performance_stats";
|
|
31
|
+
WorkerMessageType2["RenderWindow"] = "renderWindow";
|
|
32
|
+
WorkerMessageType2["AudioTrackAdd"] = "audio_track:add";
|
|
33
|
+
WorkerMessageType2["AudioTrackRemove"] = "audio_track:remove";
|
|
34
|
+
WorkerMessageType2["AudioTrackUpdate"] = "audio_track:update";
|
|
35
|
+
return WorkerMessageType2;
|
|
36
|
+
})(WorkerMessageType || {});
|
|
37
|
+
var WorkerState = /* @__PURE__ */ ((WorkerState2) => {
|
|
38
|
+
WorkerState2["Idle"] = "idle";
|
|
39
|
+
WorkerState2["Initializing"] = "initializing";
|
|
40
|
+
WorkerState2["Ready"] = "ready";
|
|
41
|
+
WorkerState2["Processing"] = "processing";
|
|
42
|
+
WorkerState2["Error"] = "error";
|
|
43
|
+
WorkerState2["Disposed"] = "disposed";
|
|
44
|
+
return WorkerState2;
|
|
45
|
+
})(WorkerState || {});
|
|
46
|
+
const defaultRetryConfig = {
|
|
47
|
+
maxRetries: 3,
|
|
48
|
+
initialDelay: 100,
|
|
49
|
+
maxDelay: 5e3,
|
|
50
|
+
backoffFactor: 2,
|
|
51
|
+
retryableErrors: ["TIMEOUT", "NETWORK_ERROR", "WORKER_BUSY"]
|
|
52
|
+
};
|
|
53
|
+
function calculateRetryDelay(attempt, config) {
|
|
54
|
+
const { initialDelay = 100, maxDelay = 5e3, backoffFactor = 2 } = config;
|
|
55
|
+
const delay = initialDelay * Math.pow(backoffFactor, attempt - 1);
|
|
56
|
+
return Math.min(delay, maxDelay);
|
|
57
|
+
}
|
|
58
|
+
function isRetryableError(error, config) {
|
|
59
|
+
const { retryableErrors = defaultRetryConfig.retryableErrors } = config;
|
|
60
|
+
if (!error) return false;
|
|
61
|
+
const errorCode = error.code || error.name;
|
|
62
|
+
if (errorCode && retryableErrors.includes(errorCode)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
const message = error.message || "";
|
|
66
|
+
if (message.includes("timeout") || message.includes("Timeout")) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
async function withRetry(fn, config) {
|
|
72
|
+
const { maxRetries } = config;
|
|
73
|
+
let lastError;
|
|
74
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
75
|
+
try {
|
|
76
|
+
return await fn();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
lastError = error;
|
|
79
|
+
if (!isRetryableError(error, config)) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
if (attempt === maxRetries) {
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
const delay = calculateRetryDelay(attempt, config);
|
|
86
|
+
await sleep(delay);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
throw lastError || new Error("Retry failed");
|
|
90
|
+
}
|
|
91
|
+
function sleep(ms) {
|
|
92
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
93
|
+
}
|
|
94
|
+
function isTransferable(obj) {
|
|
95
|
+
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;
|
|
96
|
+
}
|
|
97
|
+
function findTransferables(obj, transferables) {
|
|
98
|
+
if (!obj || typeof obj !== "object") {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (isTransferable(obj)) {
|
|
102
|
+
transferables.push(obj);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (obj instanceof VideoFrame) {
|
|
106
|
+
transferables.push(obj);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (typeof AudioData !== "undefined" && obj instanceof AudioData) {
|
|
110
|
+
transferables.push(obj);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (typeof EncodedVideoChunk !== "undefined" && obj instanceof EncodedVideoChunk || typeof EncodedAudioChunk !== "undefined" && obj instanceof EncodedAudioChunk) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (Array.isArray(obj)) {
|
|
117
|
+
for (const item of obj) {
|
|
118
|
+
findTransferables(item, transferables);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
for (const key in obj) {
|
|
122
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
123
|
+
findTransferables(obj[key], transferables);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function extractTransferables(payload) {
|
|
129
|
+
const transferables = [];
|
|
130
|
+
findTransferables(payload, transferables);
|
|
131
|
+
return transferables;
|
|
132
|
+
}
|
|
133
|
+
class WorkerChannel {
|
|
134
|
+
name;
|
|
135
|
+
port;
|
|
136
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
137
|
+
messageHandlers = {};
|
|
138
|
+
state = WorkerState.Idle;
|
|
139
|
+
defaultTimeout;
|
|
140
|
+
defaultMaxRetries;
|
|
141
|
+
constructor(port, config) {
|
|
142
|
+
this.name = config.name;
|
|
143
|
+
this.port = port;
|
|
144
|
+
this.defaultTimeout = config.timeout ?? 3e4;
|
|
145
|
+
this.defaultMaxRetries = config.maxRetries ?? 3;
|
|
146
|
+
this.setupMessageHandler();
|
|
147
|
+
this.state = WorkerState.Ready;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Send a message and wait for response with retry support
|
|
151
|
+
*/
|
|
152
|
+
async send(type, payload, options) {
|
|
153
|
+
const maxRetries = options?.maxRetries ?? this.defaultMaxRetries;
|
|
154
|
+
const retryConfig = {
|
|
155
|
+
...defaultRetryConfig,
|
|
156
|
+
maxRetries,
|
|
157
|
+
...options?.retryConfig
|
|
158
|
+
};
|
|
159
|
+
return withRetry(() => this.sendOnce(type, payload, options), retryConfig);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Send a message once (without retry)
|
|
163
|
+
*/
|
|
164
|
+
async sendOnce(type, payload, options) {
|
|
165
|
+
const id = this.generateMessageId();
|
|
166
|
+
const timeout = options?.timeout ?? this.defaultTimeout;
|
|
167
|
+
const message = {
|
|
168
|
+
type,
|
|
169
|
+
id,
|
|
170
|
+
payload,
|
|
171
|
+
timestamp: Date.now()
|
|
172
|
+
};
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
const request = {
|
|
175
|
+
id,
|
|
176
|
+
type,
|
|
177
|
+
timestamp: Date.now(),
|
|
178
|
+
timeout,
|
|
179
|
+
resolve,
|
|
180
|
+
reject
|
|
181
|
+
};
|
|
182
|
+
this.pendingRequests.set(id, request);
|
|
183
|
+
const timeoutId = setTimeout(() => {
|
|
184
|
+
const pending = this.pendingRequests.get(id);
|
|
185
|
+
if (pending) {
|
|
186
|
+
this.pendingRequests.delete(id);
|
|
187
|
+
const error = new Error(`Request timeout: ${id} ${type} (${timeout}ms)`);
|
|
188
|
+
error.code = "TIMEOUT";
|
|
189
|
+
pending.reject(error);
|
|
190
|
+
}
|
|
191
|
+
}, timeout);
|
|
192
|
+
request.timeoutId = timeoutId;
|
|
193
|
+
if (options?.transfer) {
|
|
194
|
+
this.port.postMessage(message, options.transfer);
|
|
195
|
+
} else {
|
|
196
|
+
this.port.postMessage(message);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Send a message without waiting for response
|
|
202
|
+
*/
|
|
203
|
+
post(type, payload, transfer) {
|
|
204
|
+
const message = {
|
|
205
|
+
type,
|
|
206
|
+
id: this.generateMessageId(),
|
|
207
|
+
payload,
|
|
208
|
+
timestamp: Date.now()
|
|
209
|
+
};
|
|
210
|
+
if (transfer) {
|
|
211
|
+
this.port.postMessage(message, transfer);
|
|
212
|
+
} else {
|
|
213
|
+
this.port.postMessage(message);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Register a message handler
|
|
218
|
+
*/
|
|
219
|
+
on(type, handler) {
|
|
220
|
+
this.messageHandlers[type] = handler;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Unregister a message handler
|
|
224
|
+
*/
|
|
225
|
+
off(type) {
|
|
226
|
+
delete this.messageHandlers[type];
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Dispose the channel
|
|
230
|
+
*/
|
|
231
|
+
dispose() {
|
|
232
|
+
this.state = WorkerState.Disposed;
|
|
233
|
+
for (const [, request] of this.pendingRequests) {
|
|
234
|
+
if (request.timeoutId) {
|
|
235
|
+
clearTimeout(request.timeoutId);
|
|
236
|
+
}
|
|
237
|
+
request.reject(new Error("Channel disposed"));
|
|
238
|
+
}
|
|
239
|
+
this.pendingRequests.clear();
|
|
240
|
+
this.port.onmessage = null;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Setup message handler for incoming messages
|
|
244
|
+
*/
|
|
245
|
+
setupMessageHandler() {
|
|
246
|
+
this.port.onmessage = async (event) => {
|
|
247
|
+
const data = event.data;
|
|
248
|
+
if (this.isResponse(data)) {
|
|
249
|
+
this.handleResponse(data);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (this.isRequest(data)) {
|
|
253
|
+
await this.handleRequest(data);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Handle incoming request
|
|
260
|
+
*/
|
|
261
|
+
async handleRequest(message) {
|
|
262
|
+
const handler = this.messageHandlers[message.type];
|
|
263
|
+
if (!handler) {
|
|
264
|
+
this.sendResponse(message.id, false, null, {
|
|
265
|
+
code: "NO_HANDLER",
|
|
266
|
+
message: `No handler registered for message type: ${message.type}`
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
this.state = WorkerState.Processing;
|
|
271
|
+
Promise.resolve().then(() => handler(message.payload, message.transfer)).then((result) => {
|
|
272
|
+
this.sendResponse(message.id, true, result);
|
|
273
|
+
this.state = WorkerState.Ready;
|
|
274
|
+
}).catch((error) => {
|
|
275
|
+
const workerError = {
|
|
276
|
+
code: "HANDLER_ERROR",
|
|
277
|
+
message: error instanceof Error ? error.message : String(error),
|
|
278
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
279
|
+
};
|
|
280
|
+
this.sendResponse(message.id, false, null, workerError);
|
|
281
|
+
this.state = WorkerState.Ready;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Handle incoming response
|
|
286
|
+
*/
|
|
287
|
+
handleResponse(response) {
|
|
288
|
+
const request = this.pendingRequests.get(response.id);
|
|
289
|
+
if (!request) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
this.pendingRequests.delete(response.id);
|
|
293
|
+
if (request.timeoutId) {
|
|
294
|
+
clearTimeout(request.timeoutId);
|
|
295
|
+
}
|
|
296
|
+
if (response.success) {
|
|
297
|
+
request.resolve(response.result);
|
|
298
|
+
} else {
|
|
299
|
+
const error = new Error(response.error?.message || "Unknown error");
|
|
300
|
+
if (response.error) {
|
|
301
|
+
Object.assign(error, response.error);
|
|
302
|
+
}
|
|
303
|
+
request.reject(error);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Send a response message
|
|
308
|
+
*/
|
|
309
|
+
sendResponse(id, success, result, error) {
|
|
310
|
+
let transfer = [];
|
|
311
|
+
if (isTransferable(result)) {
|
|
312
|
+
transfer.push(result);
|
|
313
|
+
}
|
|
314
|
+
const response = {
|
|
315
|
+
id,
|
|
316
|
+
success,
|
|
317
|
+
result,
|
|
318
|
+
error,
|
|
319
|
+
timestamp: Date.now()
|
|
320
|
+
};
|
|
321
|
+
this.port.postMessage(response, transfer);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Check if message is a response
|
|
325
|
+
*/
|
|
326
|
+
isResponse(data) {
|
|
327
|
+
return data && typeof data === "object" && "id" in data && "success" in data && !("type" in data);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Check if message is a request
|
|
331
|
+
*/
|
|
332
|
+
isRequest(data) {
|
|
333
|
+
return data && typeof data === "object" && "id" in data && "type" in data;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Generate unique message ID
|
|
337
|
+
*/
|
|
338
|
+
generateMessageId() {
|
|
339
|
+
return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Send a notification message without waiting for response
|
|
343
|
+
* Alias for post() method for compatibility
|
|
344
|
+
*/
|
|
345
|
+
notify(type, payload, transfer) {
|
|
346
|
+
this.post(type, payload, transfer);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Register a message handler
|
|
350
|
+
* Alias for on() method for compatibility
|
|
351
|
+
*/
|
|
352
|
+
registerHandler(type, handler) {
|
|
353
|
+
this.on(type, handler);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Send a ReadableStream to another worker
|
|
357
|
+
* Automatically handles transferable streams vs chunk-by-chunk fallback
|
|
358
|
+
*/
|
|
359
|
+
async sendStream(stream, metadata) {
|
|
360
|
+
const streamId = metadata?.streamId || this.generateMessageId();
|
|
361
|
+
if (isTransferable(stream)) {
|
|
362
|
+
this.port.postMessage(
|
|
363
|
+
{
|
|
364
|
+
type: "stream_transfer",
|
|
365
|
+
...metadata,
|
|
366
|
+
stream,
|
|
367
|
+
streamId
|
|
368
|
+
},
|
|
369
|
+
[stream]
|
|
370
|
+
// Transfer ownership
|
|
371
|
+
);
|
|
372
|
+
} else {
|
|
373
|
+
await this.streamChunks(stream, streamId, metadata);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Stream chunks from a ReadableStream (fallback when transfer is not supported)
|
|
378
|
+
*/
|
|
379
|
+
async streamChunks(stream, streamId, metadata) {
|
|
380
|
+
const reader = stream.getReader();
|
|
381
|
+
this.post("stream_start", {
|
|
382
|
+
streamId,
|
|
383
|
+
...metadata,
|
|
384
|
+
mode: "chunk_transfer"
|
|
385
|
+
});
|
|
386
|
+
try {
|
|
387
|
+
while (true) {
|
|
388
|
+
const { done, value } = await reader.read();
|
|
389
|
+
if (done) {
|
|
390
|
+
this.post("stream_end", {
|
|
391
|
+
streamId,
|
|
392
|
+
...metadata
|
|
393
|
+
});
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
const transfer = [];
|
|
397
|
+
if (value instanceof ArrayBuffer) {
|
|
398
|
+
transfer.push(value);
|
|
399
|
+
} else if (value instanceof Uint8Array) {
|
|
400
|
+
transfer.push(value.buffer);
|
|
401
|
+
} else if (typeof AudioData !== "undefined" && value instanceof AudioData) {
|
|
402
|
+
transfer.push(value);
|
|
403
|
+
} else if (typeof VideoFrame !== "undefined" && value instanceof VideoFrame) {
|
|
404
|
+
transfer.push(value);
|
|
405
|
+
} else if (typeof value === "object" && value !== null) {
|
|
406
|
+
const extracted = extractTransferables(value);
|
|
407
|
+
transfer.push(...extracted);
|
|
408
|
+
}
|
|
409
|
+
this.post(
|
|
410
|
+
"stream_chunk",
|
|
411
|
+
{
|
|
412
|
+
streamId,
|
|
413
|
+
chunk: value,
|
|
414
|
+
...metadata
|
|
415
|
+
},
|
|
416
|
+
transfer
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
} catch (error) {
|
|
420
|
+
this.post("stream_error", {
|
|
421
|
+
streamId,
|
|
422
|
+
error: error instanceof Error ? error.message : String(error),
|
|
423
|
+
...metadata
|
|
424
|
+
});
|
|
425
|
+
throw error;
|
|
426
|
+
} finally {
|
|
427
|
+
reader.releaseLock();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Receive a stream from another worker
|
|
432
|
+
* Handles both transferable streams and chunk-by-chunk reconstruction
|
|
433
|
+
*/
|
|
434
|
+
async receiveStream(onStream) {
|
|
435
|
+
const chunkedStreams = /* @__PURE__ */ new Map();
|
|
436
|
+
const prev = this.port.onmessage;
|
|
437
|
+
const handler = (event) => {
|
|
438
|
+
const raw = event.data;
|
|
439
|
+
const envelopeType = raw?.type;
|
|
440
|
+
const hasPayload = raw && typeof raw === "object" && "payload" in raw;
|
|
441
|
+
const payload = hasPayload ? raw.payload : raw;
|
|
442
|
+
if (envelopeType === "stream_transfer" && payload?.stream) {
|
|
443
|
+
onStream(payload.stream, payload);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (envelopeType === "stream_start" && payload?.streamId) {
|
|
447
|
+
const stream = new ReadableStream({
|
|
448
|
+
start(controller) {
|
|
449
|
+
chunkedStreams.set(payload.streamId, { controller, metadata: payload });
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
onStream(stream, payload);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (envelopeType === "stream_chunk" && payload?.streamId && chunkedStreams.has(payload.streamId)) {
|
|
456
|
+
const s = chunkedStreams.get(payload.streamId);
|
|
457
|
+
if (s) s.controller.enqueue(payload.chunk);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (envelopeType === "stream_end" && payload?.streamId && chunkedStreams.has(payload.streamId)) {
|
|
461
|
+
const s = chunkedStreams.get(payload.streamId);
|
|
462
|
+
if (s) {
|
|
463
|
+
s.controller.close();
|
|
464
|
+
chunkedStreams.delete(payload.streamId);
|
|
465
|
+
}
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (envelopeType === "stream_error" && payload?.streamId && chunkedStreams.has(payload.streamId)) {
|
|
469
|
+
const s = chunkedStreams.get(payload.streamId);
|
|
470
|
+
if (s) {
|
|
471
|
+
s.controller.error(new Error(String(payload.error || "stream error")));
|
|
472
|
+
chunkedStreams.delete(payload.streamId);
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (typeof prev === "function") prev.call(this.port, event);
|
|
477
|
+
};
|
|
478
|
+
this.port.onmessage = handler;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const MICROSECONDS_PER_SECOND = 1e6;
|
|
482
|
+
const DEFAULT_FPS = 30;
|
|
483
|
+
function normalizeFps(value) {
|
|
484
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
485
|
+
return DEFAULT_FPS;
|
|
486
|
+
}
|
|
487
|
+
return value;
|
|
488
|
+
}
|
|
489
|
+
function frameDurationFromFps(fps) {
|
|
490
|
+
const normalized = normalizeFps(fps);
|
|
491
|
+
const duration = MICROSECONDS_PER_SECOND / normalized;
|
|
492
|
+
return Math.max(Math.round(duration), 1);
|
|
493
|
+
}
|
|
494
|
+
function frameIndexFromTimestamp(baseTimestampUs, timestampUs, fps, strategy = "nearest") {
|
|
495
|
+
const frameDurationUs = frameDurationFromFps(fps);
|
|
496
|
+
if (frameDurationUs <= 0) {
|
|
497
|
+
return 0;
|
|
498
|
+
}
|
|
499
|
+
const delta = timestampUs - baseTimestampUs;
|
|
500
|
+
const rawIndex = delta / frameDurationUs;
|
|
501
|
+
if (!Number.isFinite(rawIndex)) {
|
|
502
|
+
return 0;
|
|
503
|
+
}
|
|
504
|
+
switch (strategy) {
|
|
505
|
+
case "floor":
|
|
506
|
+
return Math.floor(rawIndex);
|
|
507
|
+
case "ceil":
|
|
508
|
+
return Math.ceil(rawIndex);
|
|
509
|
+
default:
|
|
510
|
+
return Math.round(rawIndex);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function quantizeTimestampToFrame(timestampUs, baseTimestampUs, fps, strategy = "nearest") {
|
|
514
|
+
const frameDurationUs = frameDurationFromFps(fps);
|
|
515
|
+
const index = frameIndexFromTimestamp(baseTimestampUs, timestampUs, fps, strategy);
|
|
516
|
+
return baseTimestampUs + index * frameDurationUs;
|
|
517
|
+
}
|
|
518
|
+
class LayerRenderer {
|
|
519
|
+
ctx;
|
|
520
|
+
width;
|
|
521
|
+
height;
|
|
522
|
+
constructor(ctx, width, height) {
|
|
523
|
+
this.ctx = ctx;
|
|
524
|
+
this.width = width;
|
|
525
|
+
this.height = height;
|
|
526
|
+
this.ensureHighQualityRendering();
|
|
527
|
+
}
|
|
528
|
+
ensureHighQualityRendering() {
|
|
529
|
+
this.ctx.imageSmoothingEnabled = true;
|
|
530
|
+
this.ctx.imageSmoothingQuality = "high";
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Render a single layer with all its properties
|
|
534
|
+
*/
|
|
535
|
+
async renderLayer(layer) {
|
|
536
|
+
if (!layer.visible || layer.opacity <= 0) return;
|
|
537
|
+
this.ctx.save();
|
|
538
|
+
try {
|
|
539
|
+
this.ensureHighQualityRendering();
|
|
540
|
+
this.ctx.globalAlpha = layer.opacity;
|
|
541
|
+
if (layer.blendMode) {
|
|
542
|
+
this.ctx.globalCompositeOperation = layer.blendMode;
|
|
543
|
+
}
|
|
544
|
+
if (layer.transform) {
|
|
545
|
+
this.applyTransform(layer.transform);
|
|
546
|
+
}
|
|
547
|
+
switch (layer.type) {
|
|
548
|
+
case "video":
|
|
549
|
+
await this.renderVideoLayer(layer);
|
|
550
|
+
break;
|
|
551
|
+
case "image":
|
|
552
|
+
await this.renderImageLayer(layer);
|
|
553
|
+
break;
|
|
554
|
+
case "text":
|
|
555
|
+
await this.renderTextLayer(layer);
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
if (layer.mask) {
|
|
559
|
+
this.applyMask(layer.mask);
|
|
560
|
+
}
|
|
561
|
+
} finally {
|
|
562
|
+
this.ctx.restore();
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
applyTransform(transform) {
|
|
566
|
+
const centerX = this.width * (transform.anchorX ?? 0.5);
|
|
567
|
+
const centerY = this.height * (transform.anchorY ?? 0.5);
|
|
568
|
+
this.ctx.translate(transform.x + centerX, transform.y + centerY);
|
|
569
|
+
if (transform.rotation) {
|
|
570
|
+
this.ctx.rotate(transform.rotation);
|
|
571
|
+
}
|
|
572
|
+
this.ctx.scale(transform.scaleX, transform.scaleY);
|
|
573
|
+
if (transform.skewX || transform.skewY) {
|
|
574
|
+
this.ctx.transform(1, transform.skewY ?? 0, transform.skewX ?? 0, 1, 0, 0);
|
|
575
|
+
}
|
|
576
|
+
this.ctx.translate(-centerX, -centerY);
|
|
577
|
+
}
|
|
578
|
+
async renderVideoLayer(layer) {
|
|
579
|
+
const { videoFrame, crop } = layer;
|
|
580
|
+
const videoWidth = videoFrame.displayWidth || videoFrame.codedWidth;
|
|
581
|
+
const videoHeight = videoFrame.displayHeight || videoFrame.codedHeight;
|
|
582
|
+
const scaleX = this.width / videoWidth;
|
|
583
|
+
const scaleY = this.height / videoHeight;
|
|
584
|
+
const scale = Math.min(scaleX, scaleY);
|
|
585
|
+
const renderWidth = Math.round(videoWidth * scale);
|
|
586
|
+
const renderHeight = Math.round(videoHeight * scale);
|
|
587
|
+
const renderX = Math.round((this.width - renderWidth) / 2);
|
|
588
|
+
const renderY = Math.round((this.height - renderHeight) / 2);
|
|
589
|
+
if (crop) {
|
|
590
|
+
this.ctx.drawImage(
|
|
591
|
+
videoFrame,
|
|
592
|
+
crop.x,
|
|
593
|
+
crop.y,
|
|
594
|
+
crop.width,
|
|
595
|
+
crop.height,
|
|
596
|
+
renderX,
|
|
597
|
+
renderY,
|
|
598
|
+
renderWidth,
|
|
599
|
+
renderHeight
|
|
600
|
+
);
|
|
601
|
+
} else {
|
|
602
|
+
this.ctx.drawImage(videoFrame, renderX, renderY, renderWidth, renderHeight);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async renderImageLayer(layer) {
|
|
606
|
+
const { source, crop } = layer;
|
|
607
|
+
if (source instanceof ImageData) {
|
|
608
|
+
if (crop) {
|
|
609
|
+
const tempCanvas = new OffscreenCanvas(crop.width, crop.height);
|
|
610
|
+
const tempCtx = tempCanvas.getContext("2d");
|
|
611
|
+
tempCtx.putImageData(source, -crop.x, -crop.y);
|
|
612
|
+
this.ctx.drawImage(tempCanvas, 0, 0, this.width, this.height);
|
|
613
|
+
} else {
|
|
614
|
+
this.ctx.putImageData(source, 0, 0);
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
if (!source) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (crop) {
|
|
621
|
+
this.ctx.drawImage(
|
|
622
|
+
source,
|
|
623
|
+
crop.x,
|
|
624
|
+
crop.y,
|
|
625
|
+
crop.width,
|
|
626
|
+
crop.height,
|
|
627
|
+
0,
|
|
628
|
+
0,
|
|
629
|
+
this.width,
|
|
630
|
+
this.height
|
|
631
|
+
);
|
|
632
|
+
} else {
|
|
633
|
+
this.ctx.drawImage(source, 0, 0, this.width, this.height);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
async renderTextLayer(layer) {
|
|
638
|
+
const fontSize = layer.fontSize ?? 16;
|
|
639
|
+
const fontFamily = layer.fontFamily ?? "sans-serif";
|
|
640
|
+
const fontWeight = layer.fontWeight ?? "normal";
|
|
641
|
+
const fontStyle = layer.fontStyle ?? "normal";
|
|
642
|
+
this.ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
643
|
+
this.ctx.fillStyle = layer.color ?? "#000000";
|
|
644
|
+
this.ctx.textAlign = layer.textAlign ?? "left";
|
|
645
|
+
this.ctx.textBaseline = layer.verticalAlign ?? "top";
|
|
646
|
+
if (layer.letterSpacing && typeof this.ctx.letterSpacing !== "undefined") {
|
|
647
|
+
this.ctx.letterSpacing = `${layer.letterSpacing}px`;
|
|
648
|
+
}
|
|
649
|
+
this.ensureHighQualityRendering();
|
|
650
|
+
const baseX = this.calculateTextX(layer.textAlign);
|
|
651
|
+
const baseY = this.calculateTextY(layer.verticalAlign, fontSize);
|
|
652
|
+
const x = Math.round(baseX) + 0.5;
|
|
653
|
+
const y = Math.round(baseY) + 0.5;
|
|
654
|
+
if (layer.shadow) {
|
|
655
|
+
this.ctx.shadowColor = layer.shadow.color;
|
|
656
|
+
this.ctx.shadowOffsetX = layer.shadow.offsetX;
|
|
657
|
+
this.ctx.shadowOffsetY = layer.shadow.offsetY;
|
|
658
|
+
this.ctx.shadowBlur = layer.shadow.blur;
|
|
659
|
+
}
|
|
660
|
+
if (layer.strokeColor && layer.strokeWidth && layer.strokeWidth > 0) {
|
|
661
|
+
this.drawEnhancedStroke(layer.text, x, y, layer.strokeColor, layer.strokeWidth);
|
|
662
|
+
}
|
|
663
|
+
this.ctx.fillText(layer.text, x, y);
|
|
664
|
+
if (layer.shadow) {
|
|
665
|
+
this.ctx.shadowColor = "transparent";
|
|
666
|
+
this.ctx.shadowOffsetX = 0;
|
|
667
|
+
this.ctx.shadowOffsetY = 0;
|
|
668
|
+
this.ctx.shadowBlur = 0;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Draw enhanced multi-layer stroke for better text visibility
|
|
673
|
+
*/
|
|
674
|
+
drawEnhancedStroke(text, x, y, strokeColor, strokeWidth) {
|
|
675
|
+
this.ctx.save();
|
|
676
|
+
this.ctx.strokeStyle = strokeColor;
|
|
677
|
+
this.ctx.lineJoin = "round";
|
|
678
|
+
this.ctx.lineCap = "round";
|
|
679
|
+
this.ctx.miterLimit = 2;
|
|
680
|
+
const layers = [1.1, 1];
|
|
681
|
+
layers.forEach((multiplier) => {
|
|
682
|
+
this.ctx.lineWidth = strokeWidth * multiplier;
|
|
683
|
+
this.ctx.strokeText(text, x, y);
|
|
684
|
+
});
|
|
685
|
+
this.ctx.restore();
|
|
686
|
+
}
|
|
687
|
+
calculateTextX(align) {
|
|
688
|
+
switch (align) {
|
|
689
|
+
case "center":
|
|
690
|
+
return this.width / 2;
|
|
691
|
+
case "right":
|
|
692
|
+
return this.width;
|
|
693
|
+
default:
|
|
694
|
+
return 0;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
calculateTextY(align, fontSize = 16) {
|
|
698
|
+
switch (align) {
|
|
699
|
+
case "middle":
|
|
700
|
+
return this.height / 2;
|
|
701
|
+
case "bottom":
|
|
702
|
+
return this.height * 0.85;
|
|
703
|
+
default:
|
|
704
|
+
return fontSize;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
applyMask(mask) {
|
|
708
|
+
this.ctx.globalCompositeOperation = mask.invert ? "source-out" : "destination-in";
|
|
709
|
+
if (mask.source) {
|
|
710
|
+
this.ctx.drawImage(mask.source, 0, 0, this.width, this.height);
|
|
711
|
+
} else if (mask.shape === "circle") {
|
|
712
|
+
this.ctx.beginPath();
|
|
713
|
+
this.ctx.arc(
|
|
714
|
+
this.width / 2,
|
|
715
|
+
this.height / 2,
|
|
716
|
+
Math.min(this.width, this.height) / 2,
|
|
717
|
+
0,
|
|
718
|
+
Math.PI * 2
|
|
719
|
+
);
|
|
720
|
+
this.ctx.fill();
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
updateDimensions(width, height) {
|
|
724
|
+
this.width = width;
|
|
725
|
+
this.height = height;
|
|
726
|
+
this.ensureHighQualityRendering();
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
class TransitionProcessor {
|
|
730
|
+
width;
|
|
731
|
+
height;
|
|
732
|
+
constructor(width, height) {
|
|
733
|
+
this.width = width;
|
|
734
|
+
this.height = height;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Apply transition effect to the canvas context
|
|
738
|
+
* Returns true if transition was applied, false if not needed
|
|
739
|
+
*/
|
|
740
|
+
applyTransition(ctx, transition) {
|
|
741
|
+
if (!transition || transition.progress <= 0) return false;
|
|
742
|
+
const progress = this.calculateEasedProgress(transition.progress, transition.easing);
|
|
743
|
+
switch (transition.type) {
|
|
744
|
+
case "fade":
|
|
745
|
+
return this.applyFade(ctx, progress);
|
|
746
|
+
case "slide":
|
|
747
|
+
return this.applySlide(ctx, progress, transition.direction);
|
|
748
|
+
case "wipe":
|
|
749
|
+
return this.applyWipe(ctx, progress, transition.direction);
|
|
750
|
+
case "zoom":
|
|
751
|
+
return this.applyZoom(ctx, progress, transition.direction);
|
|
752
|
+
case "rotate":
|
|
753
|
+
return this.applyRotate(ctx, progress);
|
|
754
|
+
case "dissolve":
|
|
755
|
+
return this.applyDissolve(ctx, progress);
|
|
756
|
+
default:
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
calculateEasedProgress(progress, easing) {
|
|
761
|
+
switch (easing) {
|
|
762
|
+
case "ease-in":
|
|
763
|
+
return progress * progress;
|
|
764
|
+
case "ease-out":
|
|
765
|
+
return 1 - (1 - progress) * (1 - progress);
|
|
766
|
+
case "ease-in-out":
|
|
767
|
+
return progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;
|
|
768
|
+
default:
|
|
769
|
+
return progress;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
applyFade(ctx, progress) {
|
|
773
|
+
ctx.globalAlpha = progress;
|
|
774
|
+
return true;
|
|
775
|
+
}
|
|
776
|
+
applySlide(ctx, progress, direction) {
|
|
777
|
+
const distance = 1 - progress;
|
|
778
|
+
switch (direction) {
|
|
779
|
+
case "left":
|
|
780
|
+
ctx.translate(-this.width * distance, 0);
|
|
781
|
+
break;
|
|
782
|
+
case "right":
|
|
783
|
+
ctx.translate(this.width * distance, 0);
|
|
784
|
+
break;
|
|
785
|
+
case "up":
|
|
786
|
+
ctx.translate(0, -this.height * distance);
|
|
787
|
+
break;
|
|
788
|
+
case "down":
|
|
789
|
+
ctx.translate(0, this.height * distance);
|
|
790
|
+
break;
|
|
791
|
+
default:
|
|
792
|
+
ctx.translate(-this.width * distance, 0);
|
|
793
|
+
}
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
applyWipe(ctx, progress, direction) {
|
|
797
|
+
ctx.save();
|
|
798
|
+
ctx.beginPath();
|
|
799
|
+
switch (direction) {
|
|
800
|
+
case "left":
|
|
801
|
+
ctx.rect(0, 0, this.width * progress, this.height);
|
|
802
|
+
break;
|
|
803
|
+
case "right":
|
|
804
|
+
ctx.rect(this.width * (1 - progress), 0, this.width * progress, this.height);
|
|
805
|
+
break;
|
|
806
|
+
case "up":
|
|
807
|
+
ctx.rect(0, 0, this.width, this.height * progress);
|
|
808
|
+
break;
|
|
809
|
+
case "down":
|
|
810
|
+
ctx.rect(0, this.height * (1 - progress), this.width, this.height * progress);
|
|
811
|
+
break;
|
|
812
|
+
default:
|
|
813
|
+
ctx.rect(0, 0, this.width * progress, this.height);
|
|
814
|
+
}
|
|
815
|
+
ctx.clip();
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
applyZoom(ctx, progress, direction) {
|
|
819
|
+
const scale = direction === "out" ? 1 + (1 - progress) : progress;
|
|
820
|
+
const centerX = this.width / 2;
|
|
821
|
+
const centerY = this.height / 2;
|
|
822
|
+
ctx.translate(centerX, centerY);
|
|
823
|
+
ctx.scale(scale, scale);
|
|
824
|
+
ctx.translate(-centerX, -centerY);
|
|
825
|
+
if (direction === "out") {
|
|
826
|
+
ctx.globalAlpha = progress;
|
|
827
|
+
}
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
applyRotate(ctx, progress) {
|
|
831
|
+
const rotation = (1 - progress) * Math.PI * 2;
|
|
832
|
+
const centerX = this.width / 2;
|
|
833
|
+
const centerY = this.height / 2;
|
|
834
|
+
ctx.translate(centerX, centerY);
|
|
835
|
+
ctx.rotate(rotation);
|
|
836
|
+
ctx.translate(-centerX, -centerY);
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
applyDissolve(ctx, progress) {
|
|
840
|
+
ctx.globalAlpha = progress;
|
|
841
|
+
ctx.globalCompositeOperation = "multiply";
|
|
842
|
+
return true;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Create a transition mask for advanced effects
|
|
846
|
+
*/
|
|
847
|
+
createTransitionMask(transition, canvas) {
|
|
848
|
+
const ctx = canvas.getContext("2d");
|
|
849
|
+
if (!ctx) return null;
|
|
850
|
+
const imageData = ctx.createImageData(this.width, this.height);
|
|
851
|
+
const data = imageData.data;
|
|
852
|
+
const progress = this.calculateEasedProgress(transition.progress, transition.easing);
|
|
853
|
+
for (let y = 0; y < this.height; y++) {
|
|
854
|
+
for (let x = 0; x < this.width; x++) {
|
|
855
|
+
const index = (y * this.width + x) * 4;
|
|
856
|
+
let alpha = 255;
|
|
857
|
+
switch (transition.type) {
|
|
858
|
+
case "wipe":
|
|
859
|
+
alpha = this.calculateWipeAlpha(x, y, progress, transition.direction);
|
|
860
|
+
break;
|
|
861
|
+
case "dissolve":
|
|
862
|
+
alpha = Math.random() < progress ? 255 : 0;
|
|
863
|
+
break;
|
|
864
|
+
default:
|
|
865
|
+
alpha = Math.floor(255 * progress);
|
|
866
|
+
}
|
|
867
|
+
data[index] = 255;
|
|
868
|
+
data[index + 1] = 255;
|
|
869
|
+
data[index + 2] = 255;
|
|
870
|
+
data[index + 3] = alpha;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return imageData;
|
|
874
|
+
}
|
|
875
|
+
calculateWipeAlpha(x, y, progress, direction) {
|
|
876
|
+
let position = 0;
|
|
877
|
+
switch (direction) {
|
|
878
|
+
case "left":
|
|
879
|
+
position = x / this.width;
|
|
880
|
+
break;
|
|
881
|
+
case "right":
|
|
882
|
+
position = 1 - x / this.width;
|
|
883
|
+
break;
|
|
884
|
+
case "up":
|
|
885
|
+
position = y / this.height;
|
|
886
|
+
break;
|
|
887
|
+
case "down":
|
|
888
|
+
position = 1 - y / this.height;
|
|
889
|
+
break;
|
|
890
|
+
case "in": {
|
|
891
|
+
const cx = x - this.width / 2;
|
|
892
|
+
const cy = y - this.height / 2;
|
|
893
|
+
const maxDist = Math.sqrt(this.width * this.width + this.height * this.height) / 2;
|
|
894
|
+
position = Math.sqrt(cx * cx + cy * cy) / maxDist;
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
case "out": {
|
|
898
|
+
const cx2 = x - this.width / 2;
|
|
899
|
+
const cy2 = y - this.height / 2;
|
|
900
|
+
const maxDist2 = Math.sqrt(this.width * this.width + this.height * this.height) / 2;
|
|
901
|
+
position = 1 - Math.sqrt(cx2 * cx2 + cy2 * cy2) / maxDist2;
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
default:
|
|
905
|
+
position = x / this.width;
|
|
906
|
+
}
|
|
907
|
+
return position < progress ? 255 : 0;
|
|
908
|
+
}
|
|
909
|
+
updateDimensions(width, height) {
|
|
910
|
+
this.width = width;
|
|
911
|
+
this.height = height;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
class FilterProcessor {
|
|
915
|
+
filterCache = /* @__PURE__ */ new Map();
|
|
916
|
+
/**
|
|
917
|
+
* Apply filters to canvas context
|
|
918
|
+
* Combines multiple filters into a single CSS filter string for performance
|
|
919
|
+
*/
|
|
920
|
+
applyFilters(ctx, filters) {
|
|
921
|
+
if (!filters || filters.length === 0) {
|
|
922
|
+
ctx.filter = "none";
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
const cacheKey = this.generateCacheKey(filters);
|
|
926
|
+
let filterString = this.filterCache.get(cacheKey);
|
|
927
|
+
if (!filterString) {
|
|
928
|
+
filterString = this.buildFilterString(filters);
|
|
929
|
+
this.filterCache.set(cacheKey, filterString);
|
|
930
|
+
}
|
|
931
|
+
ctx.filter = filterString;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Build CSS filter string from filter array
|
|
935
|
+
*/
|
|
936
|
+
buildFilterString(filters) {
|
|
937
|
+
const filterStrings = [];
|
|
938
|
+
for (const filter of filters) {
|
|
939
|
+
const filterStr = this.buildSingleFilter(filter);
|
|
940
|
+
if (filterStr) {
|
|
941
|
+
filterStrings.push(filterStr);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return filterStrings.length > 0 ? filterStrings.join(" ") : "none";
|
|
945
|
+
}
|
|
946
|
+
buildSingleFilter(filter) {
|
|
947
|
+
switch (filter.type) {
|
|
948
|
+
case "blur":
|
|
949
|
+
return `blur(${filter.value ?? 0}px)`;
|
|
950
|
+
case "brightness":
|
|
951
|
+
return `brightness(${filter.value ?? 1})`;
|
|
952
|
+
case "contrast":
|
|
953
|
+
return `contrast(${filter.value ?? 1})`;
|
|
954
|
+
case "grayscale":
|
|
955
|
+
return `grayscale(${filter.value ?? 0})`;
|
|
956
|
+
case "hue-rotate":
|
|
957
|
+
return `hue-rotate(${filter.value ?? 0}deg)`;
|
|
958
|
+
case "saturate":
|
|
959
|
+
return `saturate(${filter.value ?? 1})`;
|
|
960
|
+
case "sepia":
|
|
961
|
+
return `sepia(${filter.value ?? 0})`;
|
|
962
|
+
case "custom":
|
|
963
|
+
return this.buildCustomFilter(filter);
|
|
964
|
+
default:
|
|
965
|
+
console.warn(`Unknown filter type: ${filter.type}`);
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Build custom filter from params
|
|
971
|
+
*/
|
|
972
|
+
buildCustomFilter(filter) {
|
|
973
|
+
if (!filter.params) return null;
|
|
974
|
+
const { type, ...params } = filter.params;
|
|
975
|
+
switch (type) {
|
|
976
|
+
case "drop-shadow":
|
|
977
|
+
return `drop-shadow(${params.offsetX}px ${params.offsetY}px ${params.blur}px ${params.color})`;
|
|
978
|
+
case "opacity":
|
|
979
|
+
return `opacity(${params.value})`;
|
|
980
|
+
case "invert":
|
|
981
|
+
return `invert(${params.value})`;
|
|
982
|
+
default:
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Apply color matrix transformation for advanced effects
|
|
988
|
+
* This allows for more complex color manipulations than CSS filters
|
|
989
|
+
*/
|
|
990
|
+
applyColorMatrix(imageData, matrix) {
|
|
991
|
+
if (matrix.length !== 20) {
|
|
992
|
+
throw new Error("Color matrix must have 20 values (4x5 matrix)");
|
|
993
|
+
}
|
|
994
|
+
const data = imageData.data;
|
|
995
|
+
const length = data.length;
|
|
996
|
+
for (let i = 0; i < length; i += 4) {
|
|
997
|
+
const r = data[i];
|
|
998
|
+
const g = data[i + 1];
|
|
999
|
+
const b = data[i + 2];
|
|
1000
|
+
const a = data[i + 3];
|
|
1001
|
+
const m = matrix;
|
|
1002
|
+
data[i] = this.clamp(r * m[0] + g * m[1] + b * m[2] + a * m[3] + m[4] * 255);
|
|
1003
|
+
data[i + 1] = this.clamp(r * m[5] + g * m[6] + b * m[7] + a * m[8] + m[9] * 255);
|
|
1004
|
+
data[i + 2] = this.clamp(r * m[10] + g * m[11] + b * m[12] + a * m[13] + m[14] * 255);
|
|
1005
|
+
data[i + 3] = this.clamp(r * m[15] + g * m[16] + b * m[17] + a * m[18] + m[19] * 255);
|
|
1006
|
+
}
|
|
1007
|
+
return imageData;
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Predefined color matrices for common effects
|
|
1011
|
+
*/
|
|
1012
|
+
getPresetMatrix(preset) {
|
|
1013
|
+
switch (preset) {
|
|
1014
|
+
case "vintage":
|
|
1015
|
+
return [
|
|
1016
|
+
0.393,
|
|
1017
|
+
0.769,
|
|
1018
|
+
0.189,
|
|
1019
|
+
0,
|
|
1020
|
+
0,
|
|
1021
|
+
0.349,
|
|
1022
|
+
0.686,
|
|
1023
|
+
0.168,
|
|
1024
|
+
0,
|
|
1025
|
+
0,
|
|
1026
|
+
0.272,
|
|
1027
|
+
0.534,
|
|
1028
|
+
0.131,
|
|
1029
|
+
0,
|
|
1030
|
+
0,
|
|
1031
|
+
0,
|
|
1032
|
+
0,
|
|
1033
|
+
0,
|
|
1034
|
+
1,
|
|
1035
|
+
0
|
|
1036
|
+
];
|
|
1037
|
+
case "noir":
|
|
1038
|
+
return [
|
|
1039
|
+
0.25,
|
|
1040
|
+
0.25,
|
|
1041
|
+
0.25,
|
|
1042
|
+
0,
|
|
1043
|
+
0,
|
|
1044
|
+
0.25,
|
|
1045
|
+
0.25,
|
|
1046
|
+
0.25,
|
|
1047
|
+
0,
|
|
1048
|
+
0,
|
|
1049
|
+
0.25,
|
|
1050
|
+
0.25,
|
|
1051
|
+
0.25,
|
|
1052
|
+
0,
|
|
1053
|
+
0,
|
|
1054
|
+
0,
|
|
1055
|
+
0,
|
|
1056
|
+
0,
|
|
1057
|
+
1,
|
|
1058
|
+
0
|
|
1059
|
+
];
|
|
1060
|
+
case "cool":
|
|
1061
|
+
return [0.8, 0, 0, 0, 0, 0, 0.9, 0, 0, 0, 0, 0, 1.2, 0, 0, 0, 0, 0, 1, 0];
|
|
1062
|
+
case "warm":
|
|
1063
|
+
return [1.2, 0, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 0, 0.8, 0, 0, 0, 0, 0, 1, 0];
|
|
1064
|
+
default:
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Apply Gaussian blur manually (for cases where CSS filter is not enough)
|
|
1070
|
+
*/
|
|
1071
|
+
applyGaussianBlur(imageData, radius) {
|
|
1072
|
+
const output = new ImageData(
|
|
1073
|
+
new Uint8ClampedArray(imageData.data),
|
|
1074
|
+
imageData.width,
|
|
1075
|
+
imageData.height
|
|
1076
|
+
);
|
|
1077
|
+
const width = imageData.width;
|
|
1078
|
+
const height = imageData.height;
|
|
1079
|
+
const data = imageData.data;
|
|
1080
|
+
const outData = output.data;
|
|
1081
|
+
for (let y = 0; y < height; y++) {
|
|
1082
|
+
for (let x = 0; x < width; x++) {
|
|
1083
|
+
let r = 0, g = 0, b = 0, a = 0;
|
|
1084
|
+
let count = 0;
|
|
1085
|
+
for (let dx = -radius; dx <= radius; dx++) {
|
|
1086
|
+
const nx = Math.min(Math.max(x + dx, 0), width - 1);
|
|
1087
|
+
const idx2 = (y * width + nx) * 4;
|
|
1088
|
+
r += data[idx2];
|
|
1089
|
+
g += data[idx2 + 1];
|
|
1090
|
+
b += data[idx2 + 2];
|
|
1091
|
+
a += data[idx2 + 3];
|
|
1092
|
+
count++;
|
|
1093
|
+
}
|
|
1094
|
+
const idx = (y * width + x) * 4;
|
|
1095
|
+
outData[idx] = r / count;
|
|
1096
|
+
outData[idx + 1] = g / count;
|
|
1097
|
+
outData[idx + 2] = b / count;
|
|
1098
|
+
outData[idx + 3] = a / count;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
for (let x = 0; x < width; x++) {
|
|
1102
|
+
for (let y = 0; y < height; y++) {
|
|
1103
|
+
let r = 0, g = 0, b = 0, a = 0;
|
|
1104
|
+
let count = 0;
|
|
1105
|
+
for (let dy = -radius; dy <= radius; dy++) {
|
|
1106
|
+
const ny = Math.min(Math.max(y + dy, 0), height - 1);
|
|
1107
|
+
const idx2 = (ny * width + x) * 4;
|
|
1108
|
+
r += outData[idx2];
|
|
1109
|
+
g += outData[idx2 + 1];
|
|
1110
|
+
b += outData[idx2 + 2];
|
|
1111
|
+
a += outData[idx2 + 3];
|
|
1112
|
+
count++;
|
|
1113
|
+
}
|
|
1114
|
+
const idx = (y * width + x) * 4;
|
|
1115
|
+
data[idx] = r / count;
|
|
1116
|
+
data[idx + 1] = g / count;
|
|
1117
|
+
data[idx + 2] = b / count;
|
|
1118
|
+
data[idx + 3] = a / count;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return imageData;
|
|
1122
|
+
}
|
|
1123
|
+
clamp(value) {
|
|
1124
|
+
return Math.min(255, Math.max(0, Math.round(value)));
|
|
1125
|
+
}
|
|
1126
|
+
generateCacheKey(filters) {
|
|
1127
|
+
return filters.map((f) => `${f.type}:${f.value ?? "default"}`).join("|");
|
|
1128
|
+
}
|
|
1129
|
+
clearCache() {
|
|
1130
|
+
this.filterCache.clear();
|
|
1131
|
+
}
|
|
1132
|
+
getCacheSize() {
|
|
1133
|
+
return this.filterCache.size;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
class VideoComposer {
|
|
1137
|
+
config;
|
|
1138
|
+
canvas;
|
|
1139
|
+
ctx;
|
|
1140
|
+
layerRenderer;
|
|
1141
|
+
transitionProcessor;
|
|
1142
|
+
filterProcessor;
|
|
1143
|
+
timelineContext;
|
|
1144
|
+
constructor(config) {
|
|
1145
|
+
this.config = this.applyDefaults(config);
|
|
1146
|
+
this.canvas = new OffscreenCanvas(this.config.width, this.config.height);
|
|
1147
|
+
const ctx = this.canvas.getContext("2d", {
|
|
1148
|
+
alpha: true,
|
|
1149
|
+
desynchronized: true,
|
|
1150
|
+
willReadFrequently: false,
|
|
1151
|
+
colorSpace: "srgb"
|
|
1152
|
+
});
|
|
1153
|
+
if (!ctx) {
|
|
1154
|
+
throw new Error("Failed to create 2D rendering context");
|
|
1155
|
+
}
|
|
1156
|
+
this.ctx = ctx;
|
|
1157
|
+
this.ctx.imageSmoothingEnabled = this.config.enableSmoothing;
|
|
1158
|
+
this.ctx.imageSmoothingQuality = "high";
|
|
1159
|
+
this.layerRenderer = new LayerRenderer(ctx, this.config.width, this.config.height);
|
|
1160
|
+
this.transitionProcessor = new TransitionProcessor(this.config.width, this.config.height);
|
|
1161
|
+
this.filterProcessor = new FilterProcessor();
|
|
1162
|
+
this.timelineContext = this.config.timeline;
|
|
1163
|
+
}
|
|
1164
|
+
applyDefaults(config) {
|
|
1165
|
+
return {
|
|
1166
|
+
width: config.width || 720,
|
|
1167
|
+
height: config.height || 1280,
|
|
1168
|
+
fps: config.fps || 30,
|
|
1169
|
+
backgroundColor: config.backgroundColor ?? "#000",
|
|
1170
|
+
renderer: config.renderer ?? "canvas2d",
|
|
1171
|
+
enableSmoothing: config.enableSmoothing ?? true,
|
|
1172
|
+
enableHardwareAcceleration: config.enableHardwareAcceleration ?? true,
|
|
1173
|
+
revision: config.revision ?? 0,
|
|
1174
|
+
inputHighWaterMark: config.inputHighWaterMark ?? 3,
|
|
1175
|
+
outputHighWaterMark: config.outputHighWaterMark ?? 1,
|
|
1176
|
+
maxLayers: config.maxLayers ?? 100,
|
|
1177
|
+
timeline: config.timeline ?? {
|
|
1178
|
+
clipId: "default",
|
|
1179
|
+
trackId: "main",
|
|
1180
|
+
clipStartUs: 0,
|
|
1181
|
+
clipDurationUs: Infinity,
|
|
1182
|
+
compositionFps: 30
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
createStreams(_instruction) {
|
|
1187
|
+
if (_instruction?.baseConfig.timeline) {
|
|
1188
|
+
this.timelineContext = _instruction.baseConfig.timeline;
|
|
1189
|
+
}
|
|
1190
|
+
const stream = new TransformStream(
|
|
1191
|
+
{
|
|
1192
|
+
transform: async (request, controller) => {
|
|
1193
|
+
const result = await this.composeFrame(request);
|
|
1194
|
+
controller.enqueue({
|
|
1195
|
+
frame: result.frame,
|
|
1196
|
+
metadata: result.metadata
|
|
1197
|
+
});
|
|
1198
|
+
},
|
|
1199
|
+
flush: async () => {
|
|
1200
|
+
this.filterProcessor.clearCache();
|
|
1201
|
+
}
|
|
1202
|
+
},
|
|
1203
|
+
{
|
|
1204
|
+
highWaterMark: this.config.inputHighWaterMark
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
highWaterMark: this.config.outputHighWaterMark
|
|
1208
|
+
}
|
|
1209
|
+
);
|
|
1210
|
+
return {
|
|
1211
|
+
composeStream: stream.writable,
|
|
1212
|
+
cacheStream: stream.readable
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
async composeFrame(request) {
|
|
1216
|
+
if (request.layers.length > this.config.maxLayers) {
|
|
1217
|
+
throw new Error(`Too many layers: ${request.layers.length} > ${this.config.maxLayers}`);
|
|
1218
|
+
}
|
|
1219
|
+
this.clearCanvas();
|
|
1220
|
+
if (request.transition) {
|
|
1221
|
+
this.ctx.save();
|
|
1222
|
+
this.transitionProcessor.applyTransition(this.ctx, request.transition);
|
|
1223
|
+
}
|
|
1224
|
+
for (const layer of request.layers) {
|
|
1225
|
+
if (!layer.visible || layer.opacity <= 0) {
|
|
1226
|
+
if (layer.type === "video") {
|
|
1227
|
+
const vf = layer.videoFrame;
|
|
1228
|
+
vf?.close?.();
|
|
1229
|
+
}
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
try {
|
|
1233
|
+
if (layer.filters && layer.filters.length > 0) {
|
|
1234
|
+
this.ctx.save();
|
|
1235
|
+
this.filterProcessor.applyFilters(this.ctx, layer.filters);
|
|
1236
|
+
}
|
|
1237
|
+
await this.layerRenderer.renderLayer(layer);
|
|
1238
|
+
if (layer.filters && layer.filters.length > 0) {
|
|
1239
|
+
this.ctx.restore();
|
|
1240
|
+
}
|
|
1241
|
+
} finally {
|
|
1242
|
+
if (layer.type === "video") {
|
|
1243
|
+
const vf = layer.videoFrame;
|
|
1244
|
+
vf?.close?.();
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
if (request.transition) {
|
|
1249
|
+
this.ctx.restore();
|
|
1250
|
+
}
|
|
1251
|
+
const frame = await this.createOutputFrame(request.timeUs);
|
|
1252
|
+
const gopSerial = request.gopSerial;
|
|
1253
|
+
const isKeyframe = request.isKeyframe;
|
|
1254
|
+
return {
|
|
1255
|
+
frame,
|
|
1256
|
+
timeUs: request.timeUs,
|
|
1257
|
+
metadata: {
|
|
1258
|
+
layerCount: request.layers.length,
|
|
1259
|
+
renderTime: 0,
|
|
1260
|
+
gpuAccelerated: this.config.enableHardwareAcceleration && this.config.renderer !== "canvas2d",
|
|
1261
|
+
...typeof gopSerial === "number" && { gopSerial },
|
|
1262
|
+
...typeof isKeyframe === "boolean" && { isKeyframe }
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
async composeTransition(fromRequest, toRequest, transition) {
|
|
1267
|
+
await this.composeFrame(fromRequest);
|
|
1268
|
+
const toFrameRequest = {
|
|
1269
|
+
...toRequest,
|
|
1270
|
+
transition
|
|
1271
|
+
};
|
|
1272
|
+
return this.composeFrame(toFrameRequest);
|
|
1273
|
+
}
|
|
1274
|
+
clearCanvas() {
|
|
1275
|
+
if (this.config.backgroundColor) {
|
|
1276
|
+
this.ctx.fillStyle = this.config.backgroundColor;
|
|
1277
|
+
this.ctx.fillRect(0, 0, this.config.width, this.config.height);
|
|
1278
|
+
} else {
|
|
1279
|
+
this.ctx.clearRect(0, 0, this.config.width, this.config.height);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
// private sortLayers(layers: Layer[]): Layer[] {
|
|
1283
|
+
// return [...layers].sort((a, b) => a.zIndex - b.zIndex);
|
|
1284
|
+
// }
|
|
1285
|
+
async createOutputFrame(timeUs) {
|
|
1286
|
+
const duration = frameDurationFromFps(this.timelineContext.compositionFps);
|
|
1287
|
+
const frame = new VideoFrame(this.canvas, {
|
|
1288
|
+
timestamp: timeUs,
|
|
1289
|
+
duration,
|
|
1290
|
+
alpha: "discard",
|
|
1291
|
+
visibleRect: { x: 0, y: 0, width: this.canvas.width, height: this.canvas.height }
|
|
1292
|
+
});
|
|
1293
|
+
return frame;
|
|
1294
|
+
}
|
|
1295
|
+
updateConfig(config) {
|
|
1296
|
+
Object.assign(this.config, this.applyDefaults({ ...this.config, ...config }));
|
|
1297
|
+
if (config.width || config.height) {
|
|
1298
|
+
this.canvas.width = this.config.width;
|
|
1299
|
+
this.canvas.height = this.config.height;
|
|
1300
|
+
this.layerRenderer.updateDimensions(this.config.width, this.config.height);
|
|
1301
|
+
this.transitionProcessor.updateDimensions(this.config.width, this.config.height);
|
|
1302
|
+
}
|
|
1303
|
+
if (config.enableSmoothing !== void 0) {
|
|
1304
|
+
this.ctx.imageSmoothingEnabled = this.config.enableSmoothing;
|
|
1305
|
+
}
|
|
1306
|
+
if (config.timeline) {
|
|
1307
|
+
this.timelineContext = config.timeline;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
dispose() {
|
|
1311
|
+
this.filterProcessor.clearCache();
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
function resolveActiveLayers(layers, timestamp, _frame) {
|
|
1315
|
+
return layers.filter((layer) => {
|
|
1316
|
+
if (layer.status !== "ready") {
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1319
|
+
return layer.activeRanges.some(
|
|
1320
|
+
(range) => timestamp >= range.startUs && timestamp < range.endUs
|
|
1321
|
+
);
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
function materializeLayer(layer, frame) {
|
|
1325
|
+
const baseLayer = {
|
|
1326
|
+
id: layer.layerId,
|
|
1327
|
+
type: layer.type,
|
|
1328
|
+
zIndex: layer.zIndex ?? 0,
|
|
1329
|
+
visible: true,
|
|
1330
|
+
opacity: layer.opacity ?? 1
|
|
1331
|
+
};
|
|
1332
|
+
if (layer.type === "video") {
|
|
1333
|
+
return {
|
|
1334
|
+
...baseLayer,
|
|
1335
|
+
type: "video",
|
|
1336
|
+
videoFrame: frame
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
if (layer.type === "text") {
|
|
1340
|
+
const payload = layer.payload;
|
|
1341
|
+
return {
|
|
1342
|
+
...baseLayer,
|
|
1343
|
+
type: "text",
|
|
1344
|
+
text: payload.text,
|
|
1345
|
+
fontFamily: payload.fontFamily,
|
|
1346
|
+
fontSize: payload.fontSize,
|
|
1347
|
+
fontWeight: payload.fontWeight,
|
|
1348
|
+
color: payload.color,
|
|
1349
|
+
strokeColor: payload.strokeColor,
|
|
1350
|
+
strokeWidth: payload.strokeWidth,
|
|
1351
|
+
lineHeight: payload.lineHeight,
|
|
1352
|
+
textAlign: payload.align,
|
|
1353
|
+
verticalAlign: "bottom"
|
|
1354
|
+
// Subtitles positioned at bottom
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
if (layer.type === "image") {
|
|
1358
|
+
const payload = layer.payload;
|
|
1359
|
+
const source = payload.bitmapHandle ?? null;
|
|
1360
|
+
if (source) {
|
|
1361
|
+
return {
|
|
1362
|
+
...baseLayer,
|
|
1363
|
+
type: "image",
|
|
1364
|
+
source
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return baseLayer;
|
|
1369
|
+
}
|
|
1370
|
+
class VideoComposeWorker {
|
|
1371
|
+
channel;
|
|
1372
|
+
composer = null;
|
|
1373
|
+
composeStream = null;
|
|
1374
|
+
downstreamPorts = /* @__PURE__ */ new Map();
|
|
1375
|
+
upstreamPorts = /* @__PURE__ */ new Map();
|
|
1376
|
+
instructionRegistry = /* @__PURE__ */ new Map();
|
|
1377
|
+
pendingReplay = /* @__PURE__ */ new Map();
|
|
1378
|
+
streamState = /* @__PURE__ */ new Map();
|
|
1379
|
+
constructor() {
|
|
1380
|
+
this.channel = new WorkerChannel(self, {
|
|
1381
|
+
name: "VideoComposeWorker",
|
|
1382
|
+
timeout: 3e4
|
|
1383
|
+
});
|
|
1384
|
+
this.setupHandlers();
|
|
1385
|
+
}
|
|
1386
|
+
setupHandlers() {
|
|
1387
|
+
this.channel.registerHandler("configure", this.handleConfigure.bind(this));
|
|
1388
|
+
this.channel.registerHandler("connect", this.handleConnect.bind(this));
|
|
1389
|
+
this.channel.registerHandler("flush", this.handleFlush.bind(this));
|
|
1390
|
+
this.channel.registerHandler("get_stats", this.handleGetStats.bind(this));
|
|
1391
|
+
this.channel.registerHandler("install_instructions", this.handleInstallInstructions.bind(this));
|
|
1392
|
+
this.channel.registerHandler("sync_clip", this.handleSyncClip.bind(this));
|
|
1393
|
+
this.channel.registerHandler("dispose_clip", this.handleDisposeClip.bind(this));
|
|
1394
|
+
this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Unified connect handler used by stream pipeline
|
|
1398
|
+
*/
|
|
1399
|
+
async handleConnect(payload) {
|
|
1400
|
+
const { port, direction, clipId = "default" } = payload;
|
|
1401
|
+
if (direction === "upstream") {
|
|
1402
|
+
this.upstreamPorts.set(clipId, port);
|
|
1403
|
+
const channel = new WorkerChannel(port, {
|
|
1404
|
+
name: "VideoCompose-Decode",
|
|
1405
|
+
timeout: 3e4
|
|
1406
|
+
});
|
|
1407
|
+
channel.receiveStream(this.handleReceiveStream.bind(this));
|
|
1408
|
+
}
|
|
1409
|
+
if (direction === "downstream") {
|
|
1410
|
+
this.downstreamPorts.set(clipId, port);
|
|
1411
|
+
}
|
|
1412
|
+
return { success: true };
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Configure composer
|
|
1416
|
+
* According to docs/impl/14-config, only reinitialize when initial=true
|
|
1417
|
+
*/
|
|
1418
|
+
async handleConfigure(payload) {
|
|
1419
|
+
const { config, initial } = payload;
|
|
1420
|
+
const hasValidDimensions = config.width > 0 && config.height > 0;
|
|
1421
|
+
const hasValidFps = config.fps > 0;
|
|
1422
|
+
if (!hasValidDimensions || !hasValidFps) {
|
|
1423
|
+
throw new Error(
|
|
1424
|
+
`VideoComposeWorker: invalid canvas config width=${config.width}, height=${config.height}, fps=${config.fps}`
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
if (initial) {
|
|
1428
|
+
this.channel.state = WorkerState.Ready;
|
|
1429
|
+
}
|
|
1430
|
+
if (initial || !this.composer) {
|
|
1431
|
+
if (this.composer) {
|
|
1432
|
+
this.composer.dispose();
|
|
1433
|
+
this.composeStream = null;
|
|
1434
|
+
}
|
|
1435
|
+
this.composer = new VideoComposer(config);
|
|
1436
|
+
} else {
|
|
1437
|
+
this.composer.updateConfig(config);
|
|
1438
|
+
}
|
|
1439
|
+
this.channel.notify("configured", {
|
|
1440
|
+
config: this.composer.config,
|
|
1441
|
+
initialized: initial || false
|
|
1442
|
+
});
|
|
1443
|
+
return {
|
|
1444
|
+
success: true,
|
|
1445
|
+
config: this.composer.config
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
async handleReceiveStream(stream, metadata) {
|
|
1449
|
+
const { clipId = "default" } = metadata || {};
|
|
1450
|
+
if (!this.composer) {
|
|
1451
|
+
console.error("[VideoComposeWorker] Composer not configured");
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
const instruction = this.instructionRegistry.get(clipId);
|
|
1455
|
+
if (!instruction) {
|
|
1456
|
+
console.warn("[VideoComposeWorker] No instructions for clip", clipId);
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
const filteredStream = stream.pipeThrough(
|
|
1460
|
+
new TransformStream({
|
|
1461
|
+
transform: (wrappedFrame, controller) => {
|
|
1462
|
+
try {
|
|
1463
|
+
const frame = wrappedFrame.frame || wrappedFrame;
|
|
1464
|
+
const gopSerial = wrappedFrame.gopSerial;
|
|
1465
|
+
const isKeyframe = wrappedFrame.isKeyframe;
|
|
1466
|
+
const timestamp = frame.timestamp ?? 0;
|
|
1467
|
+
if (this.shouldSkipFrame(clipId, timestamp)) {
|
|
1468
|
+
frame.close();
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
const request = this.buildComposeRequest(clipId, instruction, frame, timestamp);
|
|
1472
|
+
if (!request) {
|
|
1473
|
+
frame.close();
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
request.gopSerial = gopSerial;
|
|
1477
|
+
request.isKeyframe = isKeyframe;
|
|
1478
|
+
controller.enqueue(request);
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
const frame = wrappedFrame.frame || wrappedFrame;
|
|
1481
|
+
frame?.close?.();
|
|
1482
|
+
throw error;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
})
|
|
1486
|
+
);
|
|
1487
|
+
const { composeStream, cacheStream } = this.composer.createStreams();
|
|
1488
|
+
this.channel.sendStream(cacheStream, metadata);
|
|
1489
|
+
filteredStream.pipeTo(composeStream).catch((error) => {
|
|
1490
|
+
console.error("[VideoComposeWorker] compose stream error", clipId, error);
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
// private handleGetStream(): ReadableStream<VideoFrame> | undefined {
|
|
1494
|
+
// return this.composer?.createStreams()?.cacheStream;
|
|
1495
|
+
// }
|
|
1496
|
+
/**
|
|
1497
|
+
* Flush the composition pipeline
|
|
1498
|
+
*/
|
|
1499
|
+
async handleFlush() {
|
|
1500
|
+
try {
|
|
1501
|
+
this.channel.notify("flush_complete", {});
|
|
1502
|
+
return { success: true };
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
throw {
|
|
1505
|
+
code: "FLUSH_ERROR",
|
|
1506
|
+
message: error.message
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Get composer statistics
|
|
1512
|
+
*/
|
|
1513
|
+
async handleGetStats() {
|
|
1514
|
+
return {
|
|
1515
|
+
configured: this.composer !== null,
|
|
1516
|
+
config: this.composer?.config,
|
|
1517
|
+
streaming: this.composeStream !== null
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Dispose worker and cleanup resources
|
|
1522
|
+
*/
|
|
1523
|
+
async handleDispose() {
|
|
1524
|
+
if (this.composer) {
|
|
1525
|
+
this.composer.dispose();
|
|
1526
|
+
this.composer = null;
|
|
1527
|
+
}
|
|
1528
|
+
this.composeStream = null;
|
|
1529
|
+
this.downstreamPorts.get("default")?.close();
|
|
1530
|
+
this.upstreamPorts.get("default")?.close();
|
|
1531
|
+
this.downstreamPorts.clear();
|
|
1532
|
+
this.upstreamPorts.clear();
|
|
1533
|
+
this.channel.state = WorkerState.Disposed;
|
|
1534
|
+
return { success: true };
|
|
1535
|
+
}
|
|
1536
|
+
async handleInstallInstructions(data) {
|
|
1537
|
+
const { clipId, revision } = data;
|
|
1538
|
+
const current = this.instructionRegistry.get(clipId);
|
|
1539
|
+
if (current && current.revision > revision) {
|
|
1540
|
+
return { success: false };
|
|
1541
|
+
}
|
|
1542
|
+
this.instructionRegistry.set(clipId, data);
|
|
1543
|
+
return { success: true };
|
|
1544
|
+
}
|
|
1545
|
+
async handleSyncClip(payload) {
|
|
1546
|
+
const { clipId, revision, range } = payload;
|
|
1547
|
+
const current = this.instructionRegistry.get(clipId);
|
|
1548
|
+
if (!current || current.revision > revision) {
|
|
1549
|
+
return { success: false };
|
|
1550
|
+
}
|
|
1551
|
+
this.pendingReplay.set(clipId, { ...range, revision });
|
|
1552
|
+
this.channel.notify("sync_ack", { clipId, revision });
|
|
1553
|
+
return { success: true };
|
|
1554
|
+
}
|
|
1555
|
+
async handleDisposeClip(payload) {
|
|
1556
|
+
const { clipId } = payload;
|
|
1557
|
+
this.instructionRegistry.delete(clipId);
|
|
1558
|
+
this.pendingReplay.delete(clipId);
|
|
1559
|
+
this.downstreamPorts.get(clipId)?.close();
|
|
1560
|
+
this.upstreamPorts.get(clipId)?.close();
|
|
1561
|
+
this.downstreamPorts.delete(clipId);
|
|
1562
|
+
this.upstreamPorts.delete(clipId);
|
|
1563
|
+
return { success: true };
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Check if frame should be skipped (outside dirty range)
|
|
1567
|
+
* Returns true if frame is NOT in the dirty range and should use cached version
|
|
1568
|
+
*/
|
|
1569
|
+
shouldSkipFrame(clipId, timestamp) {
|
|
1570
|
+
const dirtyRange = this.pendingReplay.get(clipId);
|
|
1571
|
+
if (!dirtyRange) {
|
|
1572
|
+
return false;
|
|
1573
|
+
}
|
|
1574
|
+
if (timestamp >= dirtyRange.startUs && timestamp <= dirtyRange.endUs) {
|
|
1575
|
+
return false;
|
|
1576
|
+
}
|
|
1577
|
+
if (timestamp > dirtyRange.endUs) {
|
|
1578
|
+
this.pendingReplay.delete(clipId);
|
|
1579
|
+
}
|
|
1580
|
+
return true;
|
|
1581
|
+
}
|
|
1582
|
+
buildComposeRequest(clipId, instruction, frame, _timestamp) {
|
|
1583
|
+
const normalizedTime = this.computeTimelineTimestamp(clipId, frame, instruction.baseConfig);
|
|
1584
|
+
const clipStartUs = instruction.baseConfig.timeline?.clipStartUs ?? 0;
|
|
1585
|
+
const clipDurationUs = instruction.baseConfig.timeline?.clipDurationUs ?? Infinity;
|
|
1586
|
+
const clipEndUs = clipStartUs + clipDurationUs;
|
|
1587
|
+
if (normalizedTime < clipStartUs || normalizedTime >= clipEndUs) {
|
|
1588
|
+
return null;
|
|
1589
|
+
}
|
|
1590
|
+
const clipRelativeTime = normalizedTime - clipStartUs;
|
|
1591
|
+
const activeLayers = resolveActiveLayers(instruction.layers, clipRelativeTime);
|
|
1592
|
+
if (!activeLayers.length) {
|
|
1593
|
+
return null;
|
|
1594
|
+
}
|
|
1595
|
+
const layers = activeLayers.map((layer) => materializeLayer(layer, frame));
|
|
1596
|
+
return {
|
|
1597
|
+
timeUs: normalizedTime,
|
|
1598
|
+
layers,
|
|
1599
|
+
transition: VideoComposeWorker.buildTransition(
|
|
1600
|
+
instruction.transitions,
|
|
1601
|
+
clipRelativeTime,
|
|
1602
|
+
instruction.baseConfig.timeline
|
|
1603
|
+
)
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
static buildTransition(transitions, timeUs, _timeline) {
|
|
1607
|
+
const entry = transitions.find((transition) => {
|
|
1608
|
+
const { startUs, endUs } = transition.range;
|
|
1609
|
+
return timeUs >= startUs && timeUs < endUs;
|
|
1610
|
+
});
|
|
1611
|
+
if (!entry) {
|
|
1612
|
+
return void 0;
|
|
1613
|
+
}
|
|
1614
|
+
const durationUs = entry.range.endUs - entry.range.startUs;
|
|
1615
|
+
const progress = durationUs > 0 ? (timeUs - entry.range.startUs) / durationUs : 0;
|
|
1616
|
+
return {
|
|
1617
|
+
type: entry.params.type,
|
|
1618
|
+
progress: Math.min(Math.max(progress, 0), 1),
|
|
1619
|
+
easing: entry.params.easing,
|
|
1620
|
+
params: entry.params.payload,
|
|
1621
|
+
direction: entry.params.payload?.direction
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
computeTimelineTimestamp(clipId, frame, config) {
|
|
1625
|
+
const key = clipId;
|
|
1626
|
+
let state = this.streamState.get(key);
|
|
1627
|
+
if (!state) {
|
|
1628
|
+
state = {
|
|
1629
|
+
baseTimestamp: null,
|
|
1630
|
+
lastSourceTimestamp: null,
|
|
1631
|
+
nextFrameIndex: 0
|
|
1632
|
+
};
|
|
1633
|
+
this.streamState.set(key, state);
|
|
1634
|
+
}
|
|
1635
|
+
const timeline = config.timeline;
|
|
1636
|
+
if (!timeline) {
|
|
1637
|
+
const ts = frame.timestamp ?? 0;
|
|
1638
|
+
state.lastSourceTimestamp = frame.timestamp ?? null;
|
|
1639
|
+
return ts;
|
|
1640
|
+
}
|
|
1641
|
+
const { clipStartUs, compositionFps } = timeline;
|
|
1642
|
+
const sourceTimestamp = frame.timestamp ?? null;
|
|
1643
|
+
if (sourceTimestamp !== null && state.lastSourceTimestamp !== null && sourceTimestamp < state.lastSourceTimestamp) {
|
|
1644
|
+
state.baseTimestamp = null;
|
|
1645
|
+
state.nextFrameIndex = 0;
|
|
1646
|
+
}
|
|
1647
|
+
if (state.baseTimestamp === null) {
|
|
1648
|
+
state.baseTimestamp = sourceTimestamp ?? 0;
|
|
1649
|
+
state.nextFrameIndex = 0;
|
|
1650
|
+
if (state.baseTimestamp > 1e3) {
|
|
1651
|
+
console.warn(
|
|
1652
|
+
`[VideoComposeWorker] First frame timestamp is ${state.baseTimestamp}us, expected ~0. Check MP4Demuxer normalization.`
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
const frameDuration = frameDurationFromFps(compositionFps);
|
|
1657
|
+
let frameIndex = state.nextFrameIndex;
|
|
1658
|
+
if (sourceTimestamp !== null) {
|
|
1659
|
+
const approxIndex = frameIndexFromTimestamp(
|
|
1660
|
+
state.baseTimestamp,
|
|
1661
|
+
sourceTimestamp,
|
|
1662
|
+
compositionFps,
|
|
1663
|
+
"nearest"
|
|
1664
|
+
);
|
|
1665
|
+
frameIndex = Math.max(frameIndex, approxIndex);
|
|
1666
|
+
}
|
|
1667
|
+
const rawTimeline = clipStartUs + frameIndex * frameDuration;
|
|
1668
|
+
const timelineTime = quantizeTimestampToFrame(
|
|
1669
|
+
rawTimeline,
|
|
1670
|
+
clipStartUs,
|
|
1671
|
+
compositionFps,
|
|
1672
|
+
"nearest"
|
|
1673
|
+
);
|
|
1674
|
+
state.nextFrameIndex = frameIndex + 1;
|
|
1675
|
+
state.lastSourceTimestamp = sourceTimestamp;
|
|
1676
|
+
return timelineTime;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
const worker = new VideoComposeWorker();
|
|
1680
|
+
self.addEventListener("beforeunload", () => {
|
|
1681
|
+
worker["handleDispose"]();
|
|
1682
|
+
});
|
|
1683
|
+
//# sourceMappingURL=video-compose.worker-DPzsC21d.js.map
|