@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,1291 @@
|
|
|
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
|
+
class BaseDecoder {
|
|
482
|
+
decoder;
|
|
483
|
+
config;
|
|
484
|
+
controller = null;
|
|
485
|
+
constructor(config) {
|
|
486
|
+
this.config = config;
|
|
487
|
+
}
|
|
488
|
+
async initialize() {
|
|
489
|
+
if (this.decoder?.state === "configured") {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const isSupported = await this.isConfigSupported(this.config);
|
|
493
|
+
if (!isSupported.supported) {
|
|
494
|
+
throw new Error(
|
|
495
|
+
`Codec not supported: ${this.config.codecString || this.config.codec}`
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
this.decoder = this.createDecoder({
|
|
499
|
+
output: this.handleOutput.bind(this),
|
|
500
|
+
error: this.handleError.bind(this)
|
|
501
|
+
});
|
|
502
|
+
await this.configureDecoder(this.config);
|
|
503
|
+
}
|
|
504
|
+
async reconfigure(config) {
|
|
505
|
+
this.config = { ...this.config, ...config };
|
|
506
|
+
if (!this.decoder) {
|
|
507
|
+
await this.initialize();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (this.decoder.state === "configured") {
|
|
511
|
+
await this.decoder.flush();
|
|
512
|
+
}
|
|
513
|
+
const isSupported = await this.isConfigSupported(this.config);
|
|
514
|
+
if (!isSupported.supported) {
|
|
515
|
+
throw new Error(
|
|
516
|
+
`New configuration not supported: ${this.config.codecString || this.config.codec}`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
await this.configureDecoder(this.config);
|
|
520
|
+
}
|
|
521
|
+
async flush() {
|
|
522
|
+
if (!this.decoder) return;
|
|
523
|
+
await this.decoder.flush();
|
|
524
|
+
}
|
|
525
|
+
async reset() {
|
|
526
|
+
if (!this.decoder) return;
|
|
527
|
+
this.decoder.reset();
|
|
528
|
+
this.onReset();
|
|
529
|
+
}
|
|
530
|
+
async close() {
|
|
531
|
+
if (!this.decoder) return;
|
|
532
|
+
if (this.decoder.state === "configured") {
|
|
533
|
+
await this.decoder.flush();
|
|
534
|
+
}
|
|
535
|
+
this.decoder.close();
|
|
536
|
+
this.decoder = void 0;
|
|
537
|
+
}
|
|
538
|
+
get isReady() {
|
|
539
|
+
return this.decoder?.state === "configured";
|
|
540
|
+
}
|
|
541
|
+
get queueSize() {
|
|
542
|
+
return this.decoder?.decodeQueueSize ?? 0;
|
|
543
|
+
}
|
|
544
|
+
handleOutput(data) {
|
|
545
|
+
if (!this.controller) {
|
|
546
|
+
this.closeIfPossible(data);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
this.controller.enqueue(data);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
if (error instanceof TypeError && /Cannot enqueue a chunk into a readable stream that is closed/.test(error.message)) {
|
|
553
|
+
this.closeIfPossible(data);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
handleError(error) {
|
|
560
|
+
console.error(`${this.getDecoderType()} decoder error:`, error);
|
|
561
|
+
this.controller?.error(error);
|
|
562
|
+
}
|
|
563
|
+
closeIfPossible(data) {
|
|
564
|
+
data?.close?.();
|
|
565
|
+
data?.frame?.close?.();
|
|
566
|
+
}
|
|
567
|
+
onReset() {
|
|
568
|
+
}
|
|
569
|
+
createStream() {
|
|
570
|
+
return new TransformStream(
|
|
571
|
+
{
|
|
572
|
+
start: async (controller) => {
|
|
573
|
+
this.controller = controller;
|
|
574
|
+
if (!this.isReady) {
|
|
575
|
+
await this.initialize();
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
transform: async (input) => {
|
|
579
|
+
if (!this.decoder || this.decoder.state !== "configured") {
|
|
580
|
+
throw new Error("Decoder not configured");
|
|
581
|
+
}
|
|
582
|
+
if (this.decoder.decodeQueueSize >= this.decodeQueueThreshold) {
|
|
583
|
+
await new Promise((resolve) => {
|
|
584
|
+
const check = () => {
|
|
585
|
+
if (!this.decoder || this.decoder.decodeQueueSize < this.decodeQueueThreshold - 1) {
|
|
586
|
+
resolve();
|
|
587
|
+
} else {
|
|
588
|
+
setTimeout(check, 10);
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
check();
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
this.decode(input);
|
|
595
|
+
},
|
|
596
|
+
flush: async () => {
|
|
597
|
+
await this.flush();
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
highWaterMark: this.highWaterMark,
|
|
602
|
+
size: () => 1
|
|
603
|
+
}
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
class VideoChunkDecoder extends BaseDecoder {
|
|
608
|
+
static DEFAULT_HIGH_WATER_MARK = 4;
|
|
609
|
+
static DEFAULT_DECODE_QUEUE_THRESHOLD = 16;
|
|
610
|
+
trackId;
|
|
611
|
+
highWaterMark;
|
|
612
|
+
decodeQueueThreshold;
|
|
613
|
+
// GOP tracking (serial is derived from keyframe timestamp for idempotency)
|
|
614
|
+
currentGopSerial = -1;
|
|
615
|
+
isCurrentFrameKeyframe = false;
|
|
616
|
+
// Buffering support for delayed configuration
|
|
617
|
+
bufferedChunks = [];
|
|
618
|
+
isProcessingBuffer = false;
|
|
619
|
+
constructor(trackId, config) {
|
|
620
|
+
super(config || {});
|
|
621
|
+
this.trackId = trackId;
|
|
622
|
+
this.highWaterMark = config?.backpressure?.highWaterMark ?? VideoChunkDecoder.DEFAULT_HIGH_WATER_MARK;
|
|
623
|
+
this.decodeQueueThreshold = config?.backpressure?.decodeQueueThreshold ?? VideoChunkDecoder.DEFAULT_DECODE_QUEUE_THRESHOLD;
|
|
624
|
+
}
|
|
625
|
+
// Computed properties
|
|
626
|
+
get isConfigured() {
|
|
627
|
+
return this.isReady;
|
|
628
|
+
}
|
|
629
|
+
get state() {
|
|
630
|
+
return this.decoder?.state || "unconfigured";
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Update configuration - can be called before or after initialization
|
|
634
|
+
*/
|
|
635
|
+
async updateConfig(config) {
|
|
636
|
+
if (!this.isReady && config.codec) {
|
|
637
|
+
await this.configure(config);
|
|
638
|
+
await this.processBufferedChunks();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// Override createStream to handle GOP tracking and buffering
|
|
642
|
+
// Always create new stream for each clip (ReadableStreams can only be consumed once)
|
|
643
|
+
createStream() {
|
|
644
|
+
return new TransformStream(
|
|
645
|
+
{
|
|
646
|
+
start: async (controller) => {
|
|
647
|
+
this.controller = controller;
|
|
648
|
+
if (this.config?.codec && this.config?.description && !this.isReady) {
|
|
649
|
+
await this.initialize();
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
transform: async (chunk) => {
|
|
653
|
+
if (!this.isReady) {
|
|
654
|
+
this.bufferedChunks.push(chunk);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (this.isProcessingBuffer) {
|
|
658
|
+
this.bufferedChunks.push(chunk);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
await this.processChunk(chunk);
|
|
662
|
+
},
|
|
663
|
+
flush: async () => {
|
|
664
|
+
if (this.isReady) {
|
|
665
|
+
await this.flush();
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
highWaterMark: this.highWaterMark,
|
|
671
|
+
size: () => 1
|
|
672
|
+
}
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Process a single chunk (extracted from transform for reuse)
|
|
677
|
+
*/
|
|
678
|
+
async processChunk(chunk) {
|
|
679
|
+
if (!this.decoder) {
|
|
680
|
+
throw new Error("Decoder not initialized");
|
|
681
|
+
}
|
|
682
|
+
if (this.decoder.state !== "configured") {
|
|
683
|
+
console.error("[VideoChunkDecoder] Decoder in unexpected state:", this.decoder.state);
|
|
684
|
+
throw new Error(`Decoder not configured, state: ${this.decoder.state}`);
|
|
685
|
+
}
|
|
686
|
+
if (chunk.type === "key") {
|
|
687
|
+
this.handleKeyFrame(chunk.timestamp);
|
|
688
|
+
}
|
|
689
|
+
if (this.decoder.decodeQueueSize >= this.decodeQueueThreshold) {
|
|
690
|
+
await new Promise((resolve) => {
|
|
691
|
+
const check = () => {
|
|
692
|
+
if (!this.decoder || this.decoder.decodeQueueSize < this.decodeQueueThreshold - 1) {
|
|
693
|
+
resolve();
|
|
694
|
+
} else {
|
|
695
|
+
setTimeout(check, 20);
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
check();
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
this.decode(chunk);
|
|
703
|
+
} catch (error) {
|
|
704
|
+
console.error(`[VideoChunkDecoder] decode error:`, error);
|
|
705
|
+
throw error;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
// Override handleOutput to attach GOP metadata
|
|
709
|
+
handleOutput(frame) {
|
|
710
|
+
const wrappedFrame = {
|
|
711
|
+
frame,
|
|
712
|
+
gopSerial: this.currentGopSerial,
|
|
713
|
+
isKeyframe: this.isCurrentFrameKeyframe,
|
|
714
|
+
timestamp: frame.timestamp
|
|
715
|
+
};
|
|
716
|
+
this.isCurrentFrameKeyframe = false;
|
|
717
|
+
super.handleOutput(wrappedFrame);
|
|
718
|
+
}
|
|
719
|
+
// Implement abstract methods
|
|
720
|
+
async isConfigSupported(config) {
|
|
721
|
+
const result = await VideoDecoder.isConfigSupported({
|
|
722
|
+
codec: config.codec,
|
|
723
|
+
codedWidth: config.width,
|
|
724
|
+
codedHeight: config.height
|
|
725
|
+
});
|
|
726
|
+
return { supported: result.supported ?? false };
|
|
727
|
+
}
|
|
728
|
+
createDecoder(init) {
|
|
729
|
+
return new VideoDecoder(init);
|
|
730
|
+
}
|
|
731
|
+
getDecoderType() {
|
|
732
|
+
return "Video";
|
|
733
|
+
}
|
|
734
|
+
async configureDecoder(config) {
|
|
735
|
+
if (!this.decoder) return;
|
|
736
|
+
const decoderConfig = {
|
|
737
|
+
codec: config.codec,
|
|
738
|
+
codedWidth: config.width,
|
|
739
|
+
codedHeight: config.height,
|
|
740
|
+
hardwareAcceleration: config.hardwareAcceleration || "prefer-hardware",
|
|
741
|
+
optimizeForLatency: false,
|
|
742
|
+
...config.description && { description: config.description },
|
|
743
|
+
...config.displayAspectWidth && { displayAspectWidth: config.displayAspectWidth },
|
|
744
|
+
...config.displayAspectHeight && { displayAspectHeight: config.displayAspectHeight }
|
|
745
|
+
};
|
|
746
|
+
this.decoder.configure(decoderConfig);
|
|
747
|
+
}
|
|
748
|
+
decode(chunk) {
|
|
749
|
+
this.decoder?.decode(chunk);
|
|
750
|
+
}
|
|
751
|
+
// Override reset to clear GOP data
|
|
752
|
+
onReset() {
|
|
753
|
+
this.currentGopSerial = -1;
|
|
754
|
+
this.isCurrentFrameKeyframe = false;
|
|
755
|
+
}
|
|
756
|
+
// GOP management methods
|
|
757
|
+
handleKeyFrame(timestamp) {
|
|
758
|
+
this.currentGopSerial = timestamp;
|
|
759
|
+
this.isCurrentFrameKeyframe = true;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Configure the decoder with codec info (can be called after creation)
|
|
763
|
+
*/
|
|
764
|
+
async configure(config) {
|
|
765
|
+
if (this.isReady) {
|
|
766
|
+
await this.reconfigure(config);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
this.config = config;
|
|
770
|
+
await this.initialize();
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Process any buffered chunks after configuration
|
|
774
|
+
*/
|
|
775
|
+
async processBufferedChunks() {
|
|
776
|
+
if (!this.isReady || this.bufferedChunks.length === 0) {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
this.isProcessingBuffer = true;
|
|
780
|
+
const chunks = [...this.bufferedChunks];
|
|
781
|
+
this.bufferedChunks = [];
|
|
782
|
+
for (const chunk of chunks) {
|
|
783
|
+
try {
|
|
784
|
+
await this.processChunk(chunk);
|
|
785
|
+
} catch (error) {
|
|
786
|
+
console.error("[VideoChunkDecoder] Error processing buffered chunk:", error);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
this.isProcessingBuffer = false;
|
|
790
|
+
if (this.bufferedChunks.length > 0) {
|
|
791
|
+
await this.processBufferedChunks();
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// Override close to clean up GOP data
|
|
795
|
+
async close() {
|
|
796
|
+
this.bufferedChunks = [];
|
|
797
|
+
this.onReset();
|
|
798
|
+
await super.close();
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
class AudioChunkDecoder extends BaseDecoder {
|
|
802
|
+
// Default values
|
|
803
|
+
static DEFAULT_HIGH_WATER_MARK = 20;
|
|
804
|
+
static DEFAULT_DECODE_QUEUE_THRESHOLD = 16;
|
|
805
|
+
// Exposed properties
|
|
806
|
+
trackId;
|
|
807
|
+
// Backpressure configuration
|
|
808
|
+
highWaterMark;
|
|
809
|
+
decodeQueueThreshold;
|
|
810
|
+
constructor(trackId, config) {
|
|
811
|
+
super(config);
|
|
812
|
+
this.trackId = trackId;
|
|
813
|
+
this.highWaterMark = config?.backpressure?.highWaterMark ?? AudioChunkDecoder.DEFAULT_HIGH_WATER_MARK;
|
|
814
|
+
this.decodeQueueThreshold = AudioChunkDecoder.DEFAULT_DECODE_QUEUE_THRESHOLD;
|
|
815
|
+
}
|
|
816
|
+
// Computed properties
|
|
817
|
+
get isConfigured() {
|
|
818
|
+
return this.isReady;
|
|
819
|
+
}
|
|
820
|
+
get state() {
|
|
821
|
+
return this.decoder?.state || "unconfigured";
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Update configuration - can be called before or after initialization
|
|
825
|
+
*/
|
|
826
|
+
async updateConfig(config) {
|
|
827
|
+
if (!this.isReady && config.codec) {
|
|
828
|
+
await this.configure(config);
|
|
829
|
+
await this.processBufferedChunks();
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
// Implement abstract methods
|
|
834
|
+
async isConfigSupported(config) {
|
|
835
|
+
const result = await AudioDecoder.isConfigSupported({
|
|
836
|
+
codec: config.codec,
|
|
837
|
+
sampleRate: config.sampleRate,
|
|
838
|
+
numberOfChannels: config.numberOfChannels
|
|
839
|
+
});
|
|
840
|
+
return { supported: result.supported ?? false };
|
|
841
|
+
}
|
|
842
|
+
createDecoder(init) {
|
|
843
|
+
return new AudioDecoder(init);
|
|
844
|
+
}
|
|
845
|
+
getDecoderType() {
|
|
846
|
+
return "Audio";
|
|
847
|
+
}
|
|
848
|
+
async configureDecoder(config) {
|
|
849
|
+
if (!this.decoder) return;
|
|
850
|
+
await this.decoder.configure({
|
|
851
|
+
codec: config.codec,
|
|
852
|
+
sampleRate: config.sampleRate,
|
|
853
|
+
numberOfChannels: config.numberOfChannels,
|
|
854
|
+
...config.description && { description: config.description }
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
decode(chunk) {
|
|
858
|
+
this.decoder?.decode(chunk);
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Configure the decoder with codec info (can be called after creation)
|
|
862
|
+
*/
|
|
863
|
+
async configure(config) {
|
|
864
|
+
if (this.isReady) {
|
|
865
|
+
await this.reconfigure(config);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
this.config = config;
|
|
869
|
+
await this.initialize();
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Process any buffered chunks after configuration
|
|
873
|
+
* Note: Audio doesn't buffer in current implementation, but keeping for interface consistency
|
|
874
|
+
*/
|
|
875
|
+
async processBufferedChunks() {
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
const normalizeDescription = (desc) => {
|
|
879
|
+
if (!desc) return void 0;
|
|
880
|
+
if (desc instanceof ArrayBuffer) return desc;
|
|
881
|
+
const view = desc;
|
|
882
|
+
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
|
|
883
|
+
};
|
|
884
|
+
class DecodeWorker {
|
|
885
|
+
channel;
|
|
886
|
+
// Map of clipId -> decoder instance
|
|
887
|
+
videoDecoders = /* @__PURE__ */ new Map();
|
|
888
|
+
audioDecoders = /* @__PURE__ */ new Map();
|
|
889
|
+
registeredAudioTracks = /* @__PURE__ */ new Set();
|
|
890
|
+
deliveredAudioTracks = /* @__PURE__ */ new Set();
|
|
891
|
+
audioTrackMetadata = /* @__PURE__ */ new Map();
|
|
892
|
+
/** Maximum number of active decoder pairs allowed at the same time */
|
|
893
|
+
static MAX_ACTIVE_DECODERS = 8;
|
|
894
|
+
// Cached default configs merged from orchestrator
|
|
895
|
+
defaultVideoConfig = {};
|
|
896
|
+
defaultAudioConfig = {};
|
|
897
|
+
// Connections to other workers
|
|
898
|
+
composePorts = /* @__PURE__ */ new Map();
|
|
899
|
+
audioDownstreamPort = null;
|
|
900
|
+
demuxPorts = /* @__PURE__ */ new Map();
|
|
901
|
+
// Connections from demux workers
|
|
902
|
+
constructor() {
|
|
903
|
+
this.channel = new WorkerChannel(self, {
|
|
904
|
+
name: "DecodeWorker",
|
|
905
|
+
timeout: 3e4
|
|
906
|
+
});
|
|
907
|
+
this.setupHandlers();
|
|
908
|
+
}
|
|
909
|
+
setupHandlers() {
|
|
910
|
+
this.channel.registerHandler("configure", this.handleConfigure.bind(this));
|
|
911
|
+
this.channel.registerHandler("connect", this.handleConnect.bind(this));
|
|
912
|
+
this.channel.registerHandler("flush", this.handleFlush.bind(this));
|
|
913
|
+
this.channel.registerHandler("reset", this.handleReset.bind(this));
|
|
914
|
+
this.channel.registerHandler("get_stats", this.handleGetStats.bind(this));
|
|
915
|
+
this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Connect handler used by stream pipeline
|
|
919
|
+
*/
|
|
920
|
+
async handleConnect(payload) {
|
|
921
|
+
const { port, direction, clipId } = payload;
|
|
922
|
+
if (direction === "upstream") {
|
|
923
|
+
this.demuxPorts.set(clipId || "default", port);
|
|
924
|
+
const channel = new WorkerChannel(port, {
|
|
925
|
+
name: "Demux-Decode",
|
|
926
|
+
timeout: 3e4
|
|
927
|
+
});
|
|
928
|
+
channel.receiveStream((stream, metadata) => {
|
|
929
|
+
this.handleReceiveStream(stream, {
|
|
930
|
+
...metadata,
|
|
931
|
+
clipStartUs: payload.clipStartUs,
|
|
932
|
+
clipDurationUs: payload.clipDurationUs
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
channel.registerHandler("configure", this.handleConfigure.bind(this));
|
|
936
|
+
}
|
|
937
|
+
if (direction === "downstream") {
|
|
938
|
+
if (payload.streamType === "audio") {
|
|
939
|
+
this.audioDownstreamPort?.close();
|
|
940
|
+
this.audioDownstreamPort = port;
|
|
941
|
+
} else {
|
|
942
|
+
this.composePorts.set(clipId || "default", port);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return { success: true };
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Handle configuration message from orchestrator
|
|
949
|
+
* @param payload.initial - If true, initialize worker and recreate decoder instances; otherwise just update config
|
|
950
|
+
*/
|
|
951
|
+
async handleConfigure(payload) {
|
|
952
|
+
const { clipId, streamType, codec, width, height, sampleRate, numberOfChannels, description } = payload;
|
|
953
|
+
if (clipId && streamType) {
|
|
954
|
+
try {
|
|
955
|
+
if (streamType === "video") {
|
|
956
|
+
const decoder = this.videoDecoders.get(clipId);
|
|
957
|
+
if (decoder) {
|
|
958
|
+
await decoder.updateConfig({
|
|
959
|
+
codec,
|
|
960
|
+
width,
|
|
961
|
+
height,
|
|
962
|
+
description: normalizeDescription(description)
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
} else if (streamType === "audio") {
|
|
966
|
+
const decoder = this.audioDecoders.get(clipId);
|
|
967
|
+
if (decoder) {
|
|
968
|
+
await decoder.updateConfig({
|
|
969
|
+
codec,
|
|
970
|
+
sampleRate,
|
|
971
|
+
numberOfChannels,
|
|
972
|
+
description: normalizeDescription(description)
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
} catch (error) {
|
|
977
|
+
console.error("[DecodeWorker] Failed to configure decoder:", error);
|
|
978
|
+
throw {
|
|
979
|
+
code: "CODEC_CONFIG_ERROR",
|
|
980
|
+
message: error.message
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
return { success: true };
|
|
984
|
+
}
|
|
985
|
+
const { config } = payload;
|
|
986
|
+
if (!config) {
|
|
987
|
+
return { success: true };
|
|
988
|
+
}
|
|
989
|
+
this.channel.state = WorkerState.Ready;
|
|
990
|
+
if (config.video) {
|
|
991
|
+
Object.assign(this.defaultVideoConfig, config.video);
|
|
992
|
+
}
|
|
993
|
+
if (config.audio) {
|
|
994
|
+
Object.assign(this.defaultAudioConfig, config.audio);
|
|
995
|
+
}
|
|
996
|
+
if (config.video) {
|
|
997
|
+
for (const dec of this.videoDecoders.values()) {
|
|
998
|
+
await dec.updateConfig(config.video);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if (config.audio) {
|
|
1002
|
+
for (const dec of this.audioDecoders.values()) {
|
|
1003
|
+
await dec.updateConfig(config.audio);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return { success: true };
|
|
1007
|
+
}
|
|
1008
|
+
async handleReceiveStream(stream, metadata) {
|
|
1009
|
+
const clipId = metadata?.clipId || "default";
|
|
1010
|
+
const streamType = metadata?.streamType;
|
|
1011
|
+
if (streamType === "video") {
|
|
1012
|
+
const decoder = await this.getOrCreateDecoder("video", clipId, metadata);
|
|
1013
|
+
const transform = decoder.createStream();
|
|
1014
|
+
const composePort = this.composePorts.get(clipId);
|
|
1015
|
+
if (composePort) {
|
|
1016
|
+
const channel = new WorkerChannel(composePort, {
|
|
1017
|
+
name: "Decode-Compose",
|
|
1018
|
+
timeout: 3e4
|
|
1019
|
+
});
|
|
1020
|
+
channel.sendStream(transform.readable, {
|
|
1021
|
+
streamType: "video",
|
|
1022
|
+
clipId
|
|
1023
|
+
});
|
|
1024
|
+
stream.pipeTo(transform.writable).catch(
|
|
1025
|
+
(error) => console.error("[DecodeWorker] Video stream pipe error:", clipId, error)
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
} else if (streamType === "audio") {
|
|
1029
|
+
const decoder = await this.getOrCreateDecoder("audio", clipId, metadata);
|
|
1030
|
+
const transform = decoder.createStream();
|
|
1031
|
+
stream.pipeTo(transform.writable).catch((error) => {
|
|
1032
|
+
console.error("[DecodeWorker] Audio stream pipe error:", error);
|
|
1033
|
+
});
|
|
1034
|
+
const trackId = metadata?.trackId ?? clipId;
|
|
1035
|
+
await this.registerAudioTrack(trackId, clipId, metadata);
|
|
1036
|
+
this.channel.sendStream(transform.readable, {
|
|
1037
|
+
streamType: "audio",
|
|
1038
|
+
clipId,
|
|
1039
|
+
trackId,
|
|
1040
|
+
clipStartUs: metadata?.clipStartUs ?? 0,
|
|
1041
|
+
clipDurationUs: metadata?.clipDurationUs ?? 0
|
|
1042
|
+
});
|
|
1043
|
+
this.deliveredAudioTracks.add(trackId);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Flush decoders
|
|
1048
|
+
*/
|
|
1049
|
+
async handleFlush(payload) {
|
|
1050
|
+
try {
|
|
1051
|
+
if (!payload?.type || payload.type === "video") {
|
|
1052
|
+
for (const dec of this.videoDecoders.values()) {
|
|
1053
|
+
await dec.flush();
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (!payload?.type || payload.type === "audio") {
|
|
1057
|
+
for (const dec of this.audioDecoders.values()) {
|
|
1058
|
+
await dec.flush();
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return { success: true };
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
throw {
|
|
1064
|
+
code: "FLUSH_ERROR",
|
|
1065
|
+
message: error.message
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Reset decoders
|
|
1071
|
+
*/
|
|
1072
|
+
async handleReset(payload) {
|
|
1073
|
+
try {
|
|
1074
|
+
if (!payload?.type || payload.type === "video") {
|
|
1075
|
+
for (const dec of this.videoDecoders.values()) {
|
|
1076
|
+
await dec.reset();
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
if (!payload?.type || payload.type === "audio") {
|
|
1080
|
+
for (const dec of this.audioDecoders.values()) {
|
|
1081
|
+
await dec.reset();
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
this.channel.notify("reset_complete", {
|
|
1085
|
+
type: payload?.type || "all"
|
|
1086
|
+
});
|
|
1087
|
+
return { success: true };
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
throw {
|
|
1090
|
+
code: "RESET_ERROR",
|
|
1091
|
+
message: error.message
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Get decoder statistics
|
|
1097
|
+
*/
|
|
1098
|
+
async handleGetStats() {
|
|
1099
|
+
const stats = {};
|
|
1100
|
+
if (this.videoDecoders.size) {
|
|
1101
|
+
stats.video = Array.from(this.videoDecoders.entries()).map(([clipId, dec]) => ({
|
|
1102
|
+
clipId,
|
|
1103
|
+
configured: dec.isConfigured,
|
|
1104
|
+
queueSize: dec.queueSize,
|
|
1105
|
+
state: dec.state
|
|
1106
|
+
}));
|
|
1107
|
+
}
|
|
1108
|
+
if (this.audioDecoders.size) {
|
|
1109
|
+
stats.audio = Array.from(this.audioDecoders.entries()).map(([clipId, dec]) => ({
|
|
1110
|
+
clipId,
|
|
1111
|
+
configured: dec.isConfigured,
|
|
1112
|
+
queueSize: dec.queueSize,
|
|
1113
|
+
state: dec.state
|
|
1114
|
+
}));
|
|
1115
|
+
}
|
|
1116
|
+
return stats;
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Dispose worker and cleanup resources
|
|
1120
|
+
*/
|
|
1121
|
+
async handleDispose() {
|
|
1122
|
+
for (const dec of this.videoDecoders.values()) {
|
|
1123
|
+
await dec.close();
|
|
1124
|
+
}
|
|
1125
|
+
for (const dec of this.audioDecoders.values()) {
|
|
1126
|
+
await dec.close();
|
|
1127
|
+
}
|
|
1128
|
+
this.videoDecoders.clear();
|
|
1129
|
+
this.audioDecoders.clear();
|
|
1130
|
+
for (const port of this.composePorts.values()) {
|
|
1131
|
+
port.close();
|
|
1132
|
+
}
|
|
1133
|
+
this.composePorts.clear();
|
|
1134
|
+
if (this.audioDownstreamPort) {
|
|
1135
|
+
this.audioDownstreamPort.close();
|
|
1136
|
+
this.audioDownstreamPort = null;
|
|
1137
|
+
}
|
|
1138
|
+
this.registeredAudioTracks.clear();
|
|
1139
|
+
this.deliveredAudioTracks.clear();
|
|
1140
|
+
this.audioTrackMetadata.clear();
|
|
1141
|
+
for (const port of this.demuxPorts.values()) {
|
|
1142
|
+
port.close();
|
|
1143
|
+
}
|
|
1144
|
+
this.demuxPorts.clear();
|
|
1145
|
+
this.channel.state = WorkerState.Disposed;
|
|
1146
|
+
return { success: true };
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Get existing decoder for clip or create a new one (with LRU eviction)
|
|
1150
|
+
*/
|
|
1151
|
+
async getOrCreateDecoder(kind, clipId, metadata) {
|
|
1152
|
+
if (kind === "video") {
|
|
1153
|
+
let decoder = this.videoDecoders.get(clipId);
|
|
1154
|
+
if (!decoder) {
|
|
1155
|
+
decoder = new VideoChunkDecoder(
|
|
1156
|
+
clipId,
|
|
1157
|
+
metadata ? {
|
|
1158
|
+
...this.defaultVideoConfig,
|
|
1159
|
+
codec: metadata.codec,
|
|
1160
|
+
width: metadata.width,
|
|
1161
|
+
height: metadata.height,
|
|
1162
|
+
description: normalizeDescription(metadata.description)
|
|
1163
|
+
} : void 0
|
|
1164
|
+
);
|
|
1165
|
+
this.evictIfNeeded("video");
|
|
1166
|
+
this.videoDecoders.set(clipId, decoder);
|
|
1167
|
+
}
|
|
1168
|
+
this.videoDecoders.delete(clipId);
|
|
1169
|
+
this.videoDecoders.set(clipId, decoder);
|
|
1170
|
+
return decoder;
|
|
1171
|
+
} else {
|
|
1172
|
+
let decoder = this.audioDecoders.get(clipId);
|
|
1173
|
+
if (!decoder) {
|
|
1174
|
+
decoder = new AudioChunkDecoder(
|
|
1175
|
+
clipId,
|
|
1176
|
+
metadata ? {
|
|
1177
|
+
...this.defaultAudioConfig,
|
|
1178
|
+
codec: metadata.codec,
|
|
1179
|
+
sampleRate: metadata.sampleRate,
|
|
1180
|
+
numberOfChannels: metadata.numberOfChannels,
|
|
1181
|
+
description: normalizeDescription(metadata.description)
|
|
1182
|
+
} : void 0
|
|
1183
|
+
);
|
|
1184
|
+
this.evictIfNeeded("audio");
|
|
1185
|
+
this.audioDecoders.set(clipId, decoder);
|
|
1186
|
+
}
|
|
1187
|
+
this.audioDecoders.delete(clipId);
|
|
1188
|
+
this.audioDecoders.set(clipId, decoder);
|
|
1189
|
+
return decoder;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Evict least-recently-used decoder if we exceed MAX_ACTIVE_DECODERS.
|
|
1194
|
+
*/
|
|
1195
|
+
evictIfNeeded(kind) {
|
|
1196
|
+
const map = kind === "video" ? this.videoDecoders : this.audioDecoders;
|
|
1197
|
+
if (map.size < DecodeWorker.MAX_ACTIVE_DECODERS) return;
|
|
1198
|
+
const [lrucId, lruDecoder] = map.entries().next().value;
|
|
1199
|
+
lruDecoder.close().catch(() => void 0);
|
|
1200
|
+
map.delete(lrucId);
|
|
1201
|
+
}
|
|
1202
|
+
async registerAudioTrack(trackId, clipId, metadata) {
|
|
1203
|
+
const record = {
|
|
1204
|
+
clipId,
|
|
1205
|
+
config: this.extractTrackConfig(metadata?.runtimeConfig),
|
|
1206
|
+
sampleRate: metadata?.sampleRate,
|
|
1207
|
+
numberOfChannels: metadata?.numberOfChannels,
|
|
1208
|
+
type: metadata?.trackType ?? "other"
|
|
1209
|
+
};
|
|
1210
|
+
this.audioTrackMetadata.set(trackId, record);
|
|
1211
|
+
if (this.registeredAudioTracks.has(trackId)) {
|
|
1212
|
+
await this.sendAudioTrackUpdate(trackId, record);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
this.registeredAudioTracks.add(trackId);
|
|
1216
|
+
await this.sendAudioTrackAdd(trackId, record);
|
|
1217
|
+
}
|
|
1218
|
+
// private unregisterAudioTrack(trackId: string): void {
|
|
1219
|
+
// if (!this.registeredAudioTracks.delete(trackId)) {
|
|
1220
|
+
// return;
|
|
1221
|
+
// }
|
|
1222
|
+
// const record = this.audioTrackMetadata.get(trackId);
|
|
1223
|
+
// this.audioTrackMetadata.delete(trackId);
|
|
1224
|
+
// if (!record) {
|
|
1225
|
+
// return;
|
|
1226
|
+
// }
|
|
1227
|
+
// const channel = this.ensureComposeChannel();
|
|
1228
|
+
// channel
|
|
1229
|
+
// ?.send(WorkerMessageType.AudioTrackRemove, {
|
|
1230
|
+
// clipId: record.clipId,
|
|
1231
|
+
// trackId,
|
|
1232
|
+
// })
|
|
1233
|
+
// .catch((error) => {
|
|
1234
|
+
// console.warn('[DecodeWorker] Failed to notify track removal', error);
|
|
1235
|
+
// });
|
|
1236
|
+
// }
|
|
1237
|
+
extractTrackConfig(config) {
|
|
1238
|
+
return {
|
|
1239
|
+
startTimeUs: config?.startTimeUs ?? 0,
|
|
1240
|
+
durationUs: config?.durationUs,
|
|
1241
|
+
volume: config?.volume ?? 1,
|
|
1242
|
+
fadeIn: config?.fadeIn,
|
|
1243
|
+
fadeOut: config?.fadeOut,
|
|
1244
|
+
effects: config?.effects ?? [],
|
|
1245
|
+
duckingTag: config?.duckingTag
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
async sendAudioTrackAdd(trackId, record) {
|
|
1249
|
+
const channel = this.ensureComposeChannel();
|
|
1250
|
+
if (!channel) {
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
await channel.send(WorkerMessageType.AudioTrackAdd, {
|
|
1254
|
+
clipId: record.clipId,
|
|
1255
|
+
trackId,
|
|
1256
|
+
config: record.config,
|
|
1257
|
+
sampleRate: record.sampleRate,
|
|
1258
|
+
numberOfChannels: record.numberOfChannels,
|
|
1259
|
+
type: record.type
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
async sendAudioTrackUpdate(trackId, record) {
|
|
1263
|
+
const channel = this.ensureComposeChannel();
|
|
1264
|
+
if (!channel) {
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
if (!channel) {
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
await channel.send(WorkerMessageType.AudioTrackUpdate, {
|
|
1271
|
+
clipId: record.clipId,
|
|
1272
|
+
trackId,
|
|
1273
|
+
config: record.config,
|
|
1274
|
+
type: record.type
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
ensureComposeChannel() {
|
|
1278
|
+
if (!this.audioDownstreamPort) {
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
return new WorkerChannel(this.audioDownstreamPort, {
|
|
1282
|
+
name: "Decode-AudioCompose",
|
|
1283
|
+
timeout: 3e4
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
const worker = new DecodeWorker();
|
|
1288
|
+
self.addEventListener("beforeunload", () => {
|
|
1289
|
+
worker["handleDispose"]();
|
|
1290
|
+
});
|
|
1291
|
+
//# sourceMappingURL=decode.worker-DpWHsc7R.js.map
|