UNPKG

@meframe/core

Version:

Next generation media processing framework based on WebCodecs

484 lines (483 loc) 14.9 kB
var WorkerMessageType = /* @__PURE__ */ ((WorkerMessageType2) => { WorkerMessageType2["Ready"] = "ready"; WorkerMessageType2["Error"] = "error"; WorkerMessageType2["Dispose"] = "dispose"; WorkerMessageType2["Configure"] = "configure"; WorkerMessageType2["LoadResource"] = "load_resource"; WorkerMessageType2["ResourceLoaded"] = "resource_loaded"; WorkerMessageType2["ResourceProgress"] = "resource_progress"; WorkerMessageType2["ConfigureDemux"] = "configure_demux"; WorkerMessageType2["AppendBuffer"] = "append_buffer"; WorkerMessageType2["DemuxSamples"] = "demux_samples"; WorkerMessageType2["FlushDemux"] = "flush_demux"; WorkerMessageType2["ConfigureDecode"] = "configure_decode"; WorkerMessageType2["DecodeChunk"] = "decode_chunk"; WorkerMessageType2["DecodedFrame"] = "decoded_frame"; WorkerMessageType2["SeekGop"] = "seek_gop"; WorkerMessageType2["SetComposition"] = "set_composition"; WorkerMessageType2["ApplyPatch"] = "apply_patch"; WorkerMessageType2["RenderFrame"] = "render_frame"; WorkerMessageType2["ComposeFrameReady"] = "compose_frame_ready"; WorkerMessageType2["ConfigureEncode"] = "configure_encode"; WorkerMessageType2["EncodeFrame"] = "encode_frame"; WorkerMessageType2["EncodeAudio"] = "encode_audio"; WorkerMessageType2["EncodedChunk"] = "encoded_chunk"; WorkerMessageType2["FlushEncode"] = "flush_encode"; WorkerMessageType2["AddChunk"] = "add_chunk"; WorkerMessageType2["PerformanceStats"] = "performance_stats"; WorkerMessageType2["RenderWindow"] = "renderWindow"; WorkerMessageType2["AudioTrackAdd"] = "audio_track:add"; WorkerMessageType2["AudioTrackRemove"] = "audio_track:remove"; WorkerMessageType2["AudioTrackUpdate"] = "audio_track:update"; return WorkerMessageType2; })(WorkerMessageType || {}); var WorkerState = /* @__PURE__ */ ((WorkerState2) => { WorkerState2["Idle"] = "idle"; WorkerState2["Initializing"] = "initializing"; WorkerState2["Ready"] = "ready"; WorkerState2["Processing"] = "processing"; WorkerState2["Error"] = "error"; WorkerState2["Disposed"] = "disposed"; return WorkerState2; })(WorkerState || {}); const defaultRetryConfig = { maxRetries: 3, initialDelay: 100, maxDelay: 5e3, backoffFactor: 2, retryableErrors: ["TIMEOUT", "NETWORK_ERROR", "WORKER_BUSY"] }; function calculateRetryDelay(attempt, config) { const { initialDelay = 100, maxDelay = 5e3, backoffFactor = 2 } = config; const delay = initialDelay * Math.pow(backoffFactor, attempt - 1); return Math.min(delay, maxDelay); } function isRetryableError(error, config) { const { retryableErrors = defaultRetryConfig.retryableErrors } = config; if (!error) return false; const errorCode = error.code || error.name; if (errorCode && retryableErrors.includes(errorCode)) { return true; } const message = error.message || ""; if (message.includes("timeout") || message.includes("Timeout")) { return true; } return false; } async function withRetry(fn, config) { const { maxRetries } = config; let lastError; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error; if (!isRetryableError(error, config)) { throw error; } if (attempt === maxRetries) { throw error; } const delay = calculateRetryDelay(attempt, config); await sleep(delay); } } throw lastError || new Error("Retry failed"); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function isTransferable(obj) { 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; } function findTransferables(obj, transferables) { if (!obj || typeof obj !== "object") { return; } if (isTransferable(obj)) { transferables.push(obj); return; } if (obj instanceof VideoFrame) { transferables.push(obj); return; } if (typeof AudioData !== "undefined" && obj instanceof AudioData) { transferables.push(obj); return; } if (typeof EncodedVideoChunk !== "undefined" && obj instanceof EncodedVideoChunk || typeof EncodedAudioChunk !== "undefined" && obj instanceof EncodedAudioChunk) { return; } if (Array.isArray(obj)) { for (const item of obj) { findTransferables(item, transferables); } } else { for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { findTransferables(obj[key], transferables); } } } } function extractTransferables(payload) { const transferables = []; findTransferables(payload, transferables); return transferables; } class WorkerChannel { name; port; pendingRequests = /* @__PURE__ */ new Map(); messageHandlers = {}; state = WorkerState.Idle; defaultTimeout; defaultMaxRetries; constructor(port, config) { this.name = config.name; this.port = port; this.defaultTimeout = config.timeout ?? 3e4; this.defaultMaxRetries = config.maxRetries ?? 3; this.setupMessageHandler(); this.state = WorkerState.Ready; } /** * Send a message and wait for response with retry support */ async send(type, payload, options) { const maxRetries = options?.maxRetries ?? this.defaultMaxRetries; const retryConfig = { ...defaultRetryConfig, maxRetries, ...options?.retryConfig }; return withRetry(() => this.sendOnce(type, payload, options), retryConfig); } /** * Send a message once (without retry) */ async sendOnce(type, payload, options) { const id = this.generateMessageId(); const timeout = options?.timeout ?? this.defaultTimeout; const message = { type, id, payload, timestamp: Date.now() }; return new Promise((resolve, reject) => { const request = { id, type, timestamp: Date.now(), timeout, resolve, reject }; this.pendingRequests.set(id, request); const timeoutId = setTimeout(() => { const pending = this.pendingRequests.get(id); if (pending) { this.pendingRequests.delete(id); const error = new Error(`Request timeout: ${id} ${type} (${timeout}ms)`); error.code = "TIMEOUT"; pending.reject(error); } }, timeout); request.timeoutId = timeoutId; if (options?.transfer) { this.port.postMessage(message, options.transfer); } else { this.port.postMessage(message); } }); } /** * Send a message without waiting for response */ post(type, payload, transfer) { const message = { type, id: this.generateMessageId(), payload, timestamp: Date.now() }; if (transfer) { this.port.postMessage(message, transfer); } else { this.port.postMessage(message); } } /** * Register a message handler */ on(type, handler) { this.messageHandlers[type] = handler; } /** * Unregister a message handler */ off(type) { delete this.messageHandlers[type]; } /** * Dispose the channel */ dispose() { this.state = WorkerState.Disposed; for (const [, request] of this.pendingRequests) { if (request.timeoutId) { clearTimeout(request.timeoutId); } request.reject(new Error("Channel disposed")); } this.pendingRequests.clear(); this.port.onmessage = null; } /** * Setup message handler for incoming messages */ setupMessageHandler() { this.port.onmessage = async (event) => { const data = event.data; if (this.isResponse(data)) { this.handleResponse(data); return; } if (this.isRequest(data)) { await this.handleRequest(data); return; } }; } /** * Handle incoming request */ async handleRequest(message) { const handler = this.messageHandlers[message.type]; if (!handler) { this.sendResponse(message.id, false, null, { code: "NO_HANDLER", message: `No handler registered for message type: ${message.type}` }); return; } this.state = WorkerState.Processing; Promise.resolve().then(() => handler(message.payload, message.transfer)).then((result) => { this.sendResponse(message.id, true, result); this.state = WorkerState.Ready; }).catch((error) => { const workerError = { code: "HANDLER_ERROR", message: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : void 0 }; this.sendResponse(message.id, false, null, workerError); this.state = WorkerState.Ready; }); } /** * Handle incoming response */ handleResponse(response) { const request = this.pendingRequests.get(response.id); if (!request) { return; } this.pendingRequests.delete(response.id); if (request.timeoutId) { clearTimeout(request.timeoutId); } if (response.success) { request.resolve(response.result); } else { const error = new Error(response.error?.message || "Unknown error"); if (response.error) { Object.assign(error, response.error); } request.reject(error); } } /** * Send a response message */ sendResponse(id, success, result, error) { let transfer = []; if (isTransferable(result)) { transfer.push(result); } const response = { id, success, result, error, timestamp: Date.now() }; this.port.postMessage(response, transfer); } /** * Check if message is a response */ isResponse(data) { return data && typeof data === "object" && "id" in data && "success" in data && !("type" in data); } /** * Check if message is a request */ isRequest(data) { return data && typeof data === "object" && "id" in data && "type" in data; } /** * Generate unique message ID */ generateMessageId() { return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; } /** * Send a notification message without waiting for response * Alias for post() method for compatibility */ notify(type, payload, transfer) { this.post(type, payload, transfer); } /** * Register a message handler * Alias for on() method for compatibility */ registerHandler(type, handler) { this.on(type, handler); } /** * Send a ReadableStream to another worker * Automatically handles transferable streams vs chunk-by-chunk fallback */ async sendStream(stream, metadata) { const streamId = metadata?.streamId || this.generateMessageId(); if (isTransferable(stream)) { this.port.postMessage( { type: "stream_transfer", ...metadata, stream, streamId }, [stream] // Transfer ownership ); } else { await this.streamChunks(stream, streamId, metadata); } } /** * Stream chunks from a ReadableStream (fallback when transfer is not supported) */ async streamChunks(stream, streamId, metadata) { const reader = stream.getReader(); this.post("stream_start", { streamId, ...metadata, mode: "chunk_transfer" }); try { while (true) { const { done, value } = await reader.read(); if (done) { this.post("stream_end", { streamId, ...metadata }); break; } const transfer = []; if (value instanceof ArrayBuffer) { transfer.push(value); } else if (value instanceof Uint8Array) { transfer.push(value.buffer); } else if (typeof AudioData !== "undefined" && value instanceof AudioData) { transfer.push(value); } else if (typeof VideoFrame !== "undefined" && value instanceof VideoFrame) { transfer.push(value); } else if (typeof value === "object" && value !== null) { const extracted = extractTransferables(value); transfer.push(...extracted); } this.post( "stream_chunk", { streamId, chunk: value, ...metadata }, transfer ); } } catch (error) { this.post("stream_error", { streamId, error: error instanceof Error ? error.message : String(error), ...metadata }); throw error; } finally { reader.releaseLock(); } } /** * Receive a stream from another worker * Handles both transferable streams and chunk-by-chunk reconstruction */ async receiveStream(onStream) { const chunkedStreams = /* @__PURE__ */ new Map(); const prev = this.port.onmessage; const handler = (event) => { const raw = event.data; const envelopeType = raw?.type; const hasPayload = raw && typeof raw === "object" && "payload" in raw; const payload = hasPayload ? raw.payload : raw; if (envelopeType === "stream_transfer" && payload?.stream) { onStream(payload.stream, payload); return; } if (envelopeType === "stream_start" && payload?.streamId) { const stream = new ReadableStream({ start(controller) { chunkedStreams.set(payload.streamId, { controller, metadata: payload }); } }); onStream(stream, payload); return; } if (envelopeType === "stream_chunk" && payload?.streamId && chunkedStreams.has(payload.streamId)) { const s = chunkedStreams.get(payload.streamId); if (s) s.controller.enqueue(payload.chunk); return; } if (envelopeType === "stream_end" && payload?.streamId && chunkedStreams.has(payload.streamId)) { const s = chunkedStreams.get(payload.streamId); if (s) { s.controller.close(); chunkedStreams.delete(payload.streamId); } return; } if (envelopeType === "stream_error" && payload?.streamId && chunkedStreams.has(payload.streamId)) { const s = chunkedStreams.get(payload.streamId); if (s) { s.controller.error(new Error(String(payload.error || "stream error"))); chunkedStreams.delete(payload.streamId); } return; } if (typeof prev === "function") prev.call(this.port, event); }; this.port.onmessage = handler; } } export { WorkerChannel as W, WorkerMessageType as a, WorkerState as b }; //# sourceMappingURL=WorkerChannel.js.map