@meframe/core
Version:
Next generation media processing framework based on WebCodecs
484 lines (483 loc) • 14.9 kB
JavaScript
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