@meframe/core 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache/CacheManager.d.ts +5 -3
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +3 -10
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/cache/l1/AudioL1Cache.d.ts +5 -0
- package/dist/cache/l1/AudioL1Cache.d.ts.map +1 -1
- package/dist/cache/l1/AudioL1Cache.js +31 -3
- package/dist/cache/l1/AudioL1Cache.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +1 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/types.d.ts +1 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.d.ts +0 -4
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +11 -54
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts +0 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +25 -37
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts +1 -0
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +114 -92
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.d.ts +20 -25
- package/dist/stages/compose/GlobalAudioSession.d.ts.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.js +252 -67
- package/dist/stages/compose/GlobalAudioSession.js.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.d.ts.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.js +26 -8
- package/dist/stages/compose/OfflineAudioMixer.js.map +1 -1
- package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +1 -1
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/worker/WorkerPool.d.ts.map +1 -1
- package/dist/worker/WorkerPool.js +6 -2
- package/dist/worker/WorkerPool.js.map +1 -1
- package/dist/worker/types.d.ts +1 -1
- package/dist/worker/types.d.ts.map +1 -1
- package/dist/worker/types.js.map +1 -1
- package/dist/worker/worker-event-whitelist.d.ts.map +1 -1
- package/dist/workers/BaseDecoder.js +130 -0
- package/dist/workers/BaseDecoder.js.map +1 -0
- package/dist/workers/MP4Demuxer.js +4 -3
- package/dist/workers/MP4Demuxer.js.map +1 -1
- package/dist/workers/WorkerChannel.js.map +1 -1
- package/dist/workers/stages/compose/video-compose.worker.js +13 -9
- package/dist/workers/stages/compose/video-compose.worker.js.map +1 -1
- package/dist/workers/stages/decode/audio-decode.worker.js +243 -0
- package/dist/workers/stages/decode/audio-decode.worker.js.map +1 -0
- package/dist/workers/stages/decode/video-decode.worker.js +346 -0
- package/dist/workers/stages/decode/video-decode.worker.js.map +1 -0
- package/dist/workers/stages/demux/audio-demux.worker.js +28 -18
- package/dist/workers/stages/demux/audio-demux.worker.js.map +1 -1
- package/dist/workers/stages/demux/video-demux.worker.js +5 -2
- package/dist/workers/stages/demux/video-demux.worker.js.map +1 -1
- package/dist/workers/stages/encode/video-encode.worker.js +325 -0
- package/dist/workers/stages/encode/video-encode.worker.js.map +1 -0
- package/package.json +1 -1
- package/dist/cache/l1/MixedAudioL1Cache.d.ts +0 -13
- package/dist/cache/l1/MixedAudioL1Cache.d.ts.map +0 -1
- package/dist/cache/l1/MixedAudioL1Cache.js +0 -52
- package/dist/cache/l1/MixedAudioL1Cache.js.map +0 -1
- package/dist/workers/stages/decode/decode.worker.js +0 -826
- package/dist/workers/stages/decode/decode.worker.js.map +0 -1
- package/dist/workers/stages/encode/encode.worker.js +0 -547
- package/dist/workers/stages/encode/encode.worker.js.map +0 -1
|
@@ -302,35 +302,45 @@ class AudioDemuxWorker {
|
|
|
302
302
|
await this.handleConfigure({
|
|
303
303
|
config: { container: "mp4", highWaterMark: 10 },
|
|
304
304
|
initial: true
|
|
305
|
+
}).catch((error) => {
|
|
306
|
+
console.error("[AudioDemuxWorker] Configure error:", metadata?.sessionId, error);
|
|
307
|
+
return;
|
|
305
308
|
});
|
|
306
309
|
}
|
|
307
310
|
if (!this.decoderPort) {
|
|
308
|
-
|
|
311
|
+
console.error("[AudioDemuxWorker] Decoder not connected");
|
|
312
|
+
return;
|
|
309
313
|
}
|
|
310
314
|
const decoderChannel = new WorkerChannel(this.decoderPort, {
|
|
311
315
|
name: "AudioDemux-Decoder",
|
|
312
316
|
timeout: 3e4
|
|
313
317
|
});
|
|
314
318
|
if (this.currentFormat === "mp3") {
|
|
315
|
-
await this.pipeMp3Stream(stream, decoderChannel, metadata)
|
|
316
|
-
|
|
317
|
-
const transformed = stream.pipeThrough(this.audioStream);
|
|
318
|
-
const trackInfo = Array.from(this.demuxer.tracks.values())[0];
|
|
319
|
-
await decoderChannel.sendStream(transformed, {
|
|
320
|
-
streamType: "audio",
|
|
321
|
-
sessionId: metadata?.sessionId ?? "default",
|
|
322
|
-
trackId: metadata?.trackId ?? "main",
|
|
323
|
-
codec: trackInfo?.codec ?? "mp4a.40.2",
|
|
324
|
-
sampleRate: trackInfo?.sampleRate ?? 44100,
|
|
325
|
-
numberOfChannels: trackInfo?.numberOfChannels ?? 2,
|
|
326
|
-
description: trackInfo?.description
|
|
327
|
-
});
|
|
328
|
-
this.channel.notify("demux_complete", {
|
|
329
|
-
tracksProcessed: this.demuxer?.tracks.size || 0
|
|
319
|
+
await this.pipeMp3Stream(stream, decoderChannel, metadata).catch((error) => {
|
|
320
|
+
console.error("[AudioDemuxWorker] MP3 stream error:", metadata?.sessionId, error);
|
|
330
321
|
});
|
|
331
|
-
|
|
332
|
-
throw new Error("Audio demuxer not initialized");
|
|
322
|
+
return;
|
|
333
323
|
}
|
|
324
|
+
if (!this.demuxer || !this.audioStream) {
|
|
325
|
+
console.error("[AudioDemuxWorker] Audio demuxer not initialized");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const transformed = stream.pipeThrough(this.audioStream);
|
|
329
|
+
const trackInfo = Array.from(this.demuxer.tracks.values())[0];
|
|
330
|
+
await decoderChannel.sendStream(transformed, {
|
|
331
|
+
streamType: "audio",
|
|
332
|
+
sessionId: metadata?.sessionId ?? "default",
|
|
333
|
+
trackId: metadata?.trackId ?? "main",
|
|
334
|
+
codec: trackInfo?.codec ?? "mp4a.40.2",
|
|
335
|
+
sampleRate: trackInfo?.sampleRate ?? 44100,
|
|
336
|
+
numberOfChannels: trackInfo?.numberOfChannels ?? 2,
|
|
337
|
+
description: trackInfo?.description
|
|
338
|
+
}).catch((error) => {
|
|
339
|
+
console.error("[AudioDemuxWorker] Send stream error:", metadata?.sessionId, error);
|
|
340
|
+
});
|
|
341
|
+
this.channel.notify("demux_complete", {
|
|
342
|
+
tracksProcessed: this.demuxer?.tracks.size || 0
|
|
343
|
+
});
|
|
334
344
|
}
|
|
335
345
|
async pipeMp3Stream(stream, decoderChannel, metadata) {
|
|
336
346
|
if (!this.mp3Parser) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio-demux.worker.js","sources":["../../../../src/stages/demux/MP3FrameParser.ts","../../../../src/stages/demux/audio-demux.worker.ts"],"sourcesContent":["const BITRATE_TABLE: Record<string, number[]> = {\n '1-3': [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0],\n '1-2': [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0],\n '1-1': [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0],\n '2-3': [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0],\n '2-2': [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0],\n '2-1': [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0],\n};\n\nconst SAMPLE_RATE_TABLE: Record<number, number[]> = {\n 1: [44100, 48000, 32000],\n 2: [22050, 24000, 16000],\n 2.5: [11025, 12000, 8000],\n};\n\nexport interface MP3Config {\n codec: 'mp3';\n sampleRate: number;\n channels: number;\n bitrateKbps: number | null;\n samplesPerFrame: number;\n}\n\nexport interface MP3Frame {\n data: Uint8Array;\n timestampUs: number;\n durationUs: number;\n}\n\ninterface ParseResult {\n frames: MP3Frame[];\n config?: MP3Config;\n}\n\nexport class MP3FrameParser {\n private buffer = new Uint8Array(0);\n private config: MP3Config | null = null;\n private timestampUs = 0;\n\n push(chunk: Uint8Array): ParseResult {\n this.appendToBuffer(chunk);\n this.skipId3Headers();\n\n const frames: MP3Frame[] = [];\n let configEmitted: MP3Config | undefined;\n\n let offset = 0;\n while (offset <= this.buffer.length - 4) {\n const header = this.parseHeader(this.buffer, offset);\n if (!header) {\n offset += 1;\n continue;\n }\n\n if (offset + header.frameSize > this.buffer.length) {\n break;\n }\n\n const frameBytes = this.buffer.slice(offset, offset + header.frameSize);\n const durationUs = Math.round((header.samplesPerFrame * 1_000_000) / header.sampleRate);\n\n if (!this.config) {\n this.config = {\n codec: 'mp3',\n sampleRate: header.sampleRate,\n channels: header.channels,\n bitrateKbps: header.bitrateKbps,\n samplesPerFrame: header.samplesPerFrame,\n };\n configEmitted = this.config;\n }\n\n frames.push({\n data: frameBytes,\n timestampUs: this.timestampUs,\n durationUs,\n });\n\n this.timestampUs += durationUs;\n offset += header.frameSize;\n }\n\n if (offset > 0) {\n this.buffer = this.buffer.slice(offset);\n }\n\n return { frames, config: configEmitted };\n }\n\n flush(): MP3Frame[] {\n const { frames } = this.push(new Uint8Array(0));\n const remainder = this.buffer;\n this.buffer = new Uint8Array(0);\n if (remainder.length) {\n console.warn('[MP3FrameParser] Remaining unparsed bytes:', remainder.length);\n }\n return frames;\n }\n\n private appendToBuffer(chunk: Uint8Array): void {\n if (chunk.length === 0) {\n return;\n }\n const combined = new Uint8Array(this.buffer.length + chunk.length);\n combined.set(this.buffer, 0);\n combined.set(chunk, this.buffer.length);\n this.buffer = combined;\n }\n\n private skipId3Headers(): void {\n let offset = 0;\n while (this.buffer.length - offset >= 10) {\n if (\n this.buffer[offset] === 0x49 &&\n this.buffer[offset + 1] === 0x44 &&\n this.buffer[offset + 2] === 0x33\n ) {\n const size = this.readSynchsafeInteger(this.buffer.subarray(offset + 6, offset + 10));\n const total = size + 10;\n if (offset + total <= this.buffer.length) {\n offset += total;\n continue;\n }\n }\n break;\n }\n if (offset > 0) {\n this.buffer = this.buffer.slice(offset);\n }\n }\n\n private parseHeader(\n buffer: Uint8Array,\n offset: number\n ): {\n frameSize: number;\n sampleRate: number;\n channels: number;\n bitrateKbps: number | null;\n samplesPerFrame: number;\n } | null {\n if (offset + 3 >= buffer.length) {\n return null;\n }\n if (buffer[offset] !== 0xff || ((buffer[offset + 1] ?? 0) & 0xe0) !== 0xe0) {\n return null;\n }\n\n const versionBits = ((buffer[offset + 1] ?? 0) >> 3) & 0x03;\n const layerBits = ((buffer[offset + 1] ?? 0) >> 1) & 0x03;\n const bitrateIndex = ((buffer[offset + 2] ?? 0) >> 4) & 0x0f;\n const sampleRateIndex = ((buffer[offset + 2] ?? 0) >> 2) & 0x03;\n const paddingBit = ((buffer[offset + 2] ?? 0) >> 1) & 0x01;\n const channelMode = ((buffer[offset + 3] ?? 0) >> 6) & 0x03;\n\n const version = this.getVersion(versionBits);\n const layer = this.getLayer(layerBits);\n if (!version || !layer) {\n return null;\n }\n\n const sampleRate = SAMPLE_RATE_TABLE[version]?.[sampleRateIndex] ?? null;\n if (!sampleRate) {\n return null;\n }\n\n const bitrateKey = `${version === 1 ? 1 : 2}-${layer}`;\n const bitrateKbps = BITRATE_TABLE[bitrateKey]?.[bitrateIndex] ?? null;\n if (bitrateKbps === null) {\n return null;\n }\n\n const samplesPerFrame = this.getSamplesPerFrame(version, layer);\n const frameSize = this.calculateFrameSize(layer, bitrateKbps, sampleRate, paddingBit);\n if (!frameSize || frameSize < 24) {\n return null;\n }\n\n const channels = channelMode === 3 ? 1 : 2;\n\n return {\n frameSize,\n sampleRate,\n channels,\n bitrateKbps: bitrateKbps || null,\n samplesPerFrame,\n };\n }\n\n private getVersion(bits: number): 1 | 2 | 2.5 | null {\n switch (bits) {\n case 0b11:\n return 1;\n case 0b10:\n return 2;\n case 0b00:\n return 2.5;\n default:\n return null;\n }\n }\n\n private getLayer(bits: number): 1 | 2 | 3 | null {\n switch (bits) {\n case 0b11:\n return 1;\n case 0b10:\n return 2;\n case 0b01:\n return 3;\n default:\n return null;\n }\n }\n\n private getSamplesPerFrame(version: 1 | 2 | 2.5, layer: 1 | 2 | 3): number {\n if (layer === 1) {\n return 384;\n }\n if (layer === 2) {\n return 1152;\n }\n return version === 1 ? 1152 : 576;\n }\n\n private calculateFrameSize(\n layer: 1 | 2 | 3,\n bitrateKbps: number,\n sampleRate: number,\n padding: number\n ): number {\n if (bitrateKbps <= 0) {\n return 0;\n }\n\n if (layer === 1) {\n return ((12 * bitrateKbps * 1000) / sampleRate + padding) * 4;\n }\n\n return Math.floor((144 * bitrateKbps * 1000) / sampleRate + padding);\n }\n\n private readSynchsafeInteger(bytes: Uint8Array): number {\n if (bytes.length !== 4) {\n return 0;\n }\n return (\n (((bytes[0] ?? 0) & 0x7f) << 21) |\n (((bytes[1] ?? 0) & 0x7f) << 14) |\n (((bytes[2] ?? 0) & 0x7f) << 7) |\n ((bytes[3] ?? 0) & 0x7f)\n );\n }\n}\n","import { WorkerChannel } from '../../worker/WorkerChannel';\nimport { WorkerMessageType, WorkerState } from '../../worker/types';\nimport { MP4Demuxer } from './MP4Demuxer';\nimport { MP3FrameParser, type MP3Config, type MP3Frame } from './MP3FrameParser';\nimport type { DemuxConfig } from './types';\n\n/**\n * AudioDemuxWorker - First stage for audio processing\n * Extracts audio tracks from various container formats (MP3, MP4/M4A, etc.)\n *\n * Pipeline: ResourceLoader (Main Thread) → AudioDemuxWorker → DecodeWorker\n *\n * Features:\n * - Multi-format support (MP3, AAC in MP4/M4A)\n * - Stream-based processing with backpressure\n * - Direct streaming to DecodeWorker\n */\nexport class AudioDemuxWorker {\n private channel: WorkerChannel;\n private demuxer: MP4Demuxer | null = null;\n private audioStream: TransformStream<Uint8Array, EncodedAudioChunk> | null = null;\n private mp3Parser: MP3FrameParser | null = null;\n private currentFormat: 'mp3' | 'mp4' | null = null;\n\n // Connection to decoder worker\n private decoderPort: MessagePort | null = null;\n\n constructor() {\n // Initialize WorkerChannel\n this.channel = new WorkerChannel(self as any, {\n name: 'AudioDemuxWorker',\n timeout: 30000,\n });\n\n this.setupHandlers();\n }\n\n private setupHandlers(): void {\n // Register message handlers\n this.channel.registerHandler('configure', this.handleConfigure.bind(this));\n // Unified stream connect (feature-flagged)\n this.channel.registerHandler('connect' as any, this.handleConnect.bind(this));\n // Unified stream connect only\n this.channel.registerHandler('get_stats', this.handleGetStats.bind(this));\n this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));\n\n // Setup stream receiver from ResourceLoader (main thread)\n this.channel.receiveStream(this.handleReceiveStream.bind(this));\n }\n\n /**\n * Unified connect handler used by stream pipeline\n */\n private async handleConnect(payload: {\n direction: 'upstream';\n port: MessagePort;\n streamType: 'video' | 'audio' | 'frame' | 'chunk';\n }): Promise<{ success: boolean }> {\n this.decoderPort = payload.port;\n // Demux connects upstream to decoder\n return { success: true };\n }\n\n /**\n * Configure demuxer with format settings\n * @param payload.config - Demuxer configuration\n * @param payload.initial - If true, initialize worker state; otherwise just update config\n */\n private async handleConfigure(payload: {\n config: DemuxConfig;\n initial?: boolean;\n }): Promise<{ success: boolean; tracks?: any[] }> {\n const { config, initial = false } = payload;\n\n try {\n if (initial) {\n // Initial setup - set worker state to ready\n this.channel.state = WorkerState.Ready;\n\n // Detect format and create appropriate demuxer\n const format = config.container || this.detectFormat(config);\n\n if (format === 'mp3') {\n this.mp3Parser = new MP3FrameParser();\n this.demuxer?.destroy();\n this.demuxer = null;\n this.audioStream = null;\n this.currentFormat = 'mp3';\n\n const mp3Track = { id: 1, type: 'audio', codec: 'mp3' };\n this.channel.notify('configured', {\n tracks: [mp3Track],\n format,\n codec: 'mp3',\n });\n\n return { success: true, tracks: [mp3Track] };\n } else if (format === 'mp4' || format === 'm4a') {\n if (this.demuxer) {\n this.demuxer.destroy();\n }\n\n this.demuxer = new MP4Demuxer({\n ...config,\n skipVideo: true, // Audio only\n });\n this.audioStream = this.demuxer.createAudioStream();\n this.mp3Parser = null;\n this.currentFormat = 'mp4';\n } else {\n throw {\n code: 'UNSUPPORTED_FORMAT',\n message: `Unsupported audio format: ${format}`,\n };\n }\n\n if (!this.audioStream) {\n throw {\n code: 'NO_AUDIO_TRACK',\n message: 'No audio track found in container',\n };\n }\n\n const tracks = Array.from(this.demuxer.tracks.values());\n\n // Notify configuration complete\n this.channel.notify('configured', {\n tracks,\n format,\n codec: tracks[0]?.codec,\n });\n\n return { success: true, tracks };\n } else {\n // Update configuration only (e.g., backpressure settings)\n if (!this.demuxer) {\n throw {\n code: 'NOT_INITIALIZED',\n message: 'Demuxer not initialized. Call configure with initial=true first',\n };\n }\n\n // Demuxers don't support runtime config updates\n // Would need initial=true for changes\n\n return { success: true };\n }\n } catch (error: any) {\n throw {\n code: error.code || 'CONFIG_ERROR',\n message: error.message,\n };\n }\n }\n\n /**\n * Detect audio format from configuration\n */\n private detectFormat(config: DemuxConfig): string {\n // Simple format detection based on codec or file extension\n if (config.codec?.includes('mp3')) return 'mp3';\n if (config.codec?.includes('aac')) return 'mp4';\n if (config.codec?.includes('opus')) return 'webm';\n\n return 'mp3';\n }\n\n /**\n * Handle input stream from ResourceLoader (main thread)\n */\n private async handleReceiveStream(\n stream: ReadableStream<Uint8Array>,\n metadata?: Record<string, any>\n ): Promise<void> {\n // Initialize demuxer if not configured\n if (this.currentFormat === 'mp3' && !this.mp3Parser) {\n this.mp3Parser = new MP3FrameParser();\n }\n\n if (this.currentFormat === 'mp4' && (!this.demuxer || !this.audioStream)) {\n await this.handleConfigure({\n config: { container: 'mp4', highWaterMark: 10 },\n initial: true,\n });\n }\n\n if (!this.decoderPort) {\n throw new Error('Decoder not connected');\n }\n\n // Setup channel to decoder\n const decoderChannel = new WorkerChannel(this.decoderPort, {\n name: 'AudioDemux-Decoder',\n timeout: 30000,\n });\n\n if (this.currentFormat === 'mp3') {\n await this.pipeMp3Stream(stream, decoderChannel, metadata);\n } else if (this.demuxer && this.audioStream) {\n const transformed = stream.pipeThrough(this.audioStream);\n const trackInfo = Array.from(this.demuxer.tracks.values())[0];\n\n await decoderChannel.sendStream(transformed, {\n streamType: 'audio',\n sessionId: metadata?.sessionId ?? 'default',\n trackId: metadata?.trackId ?? 'main',\n codec: trackInfo?.codec ?? 'mp4a.40.2',\n sampleRate: trackInfo?.sampleRate ?? 44_100,\n numberOfChannels: trackInfo?.numberOfChannels ?? 2,\n description: trackInfo?.description,\n });\n\n this.channel.notify('demux_complete', {\n tracksProcessed: this.demuxer?.tracks.size || 0,\n });\n } else {\n throw new Error('Audio demuxer not initialized');\n }\n }\n\n private async pipeMp3Stream(\n stream: ReadableStream<Uint8Array>,\n decoderChannel: WorkerChannel,\n metadata?: Record<string, any>\n ): Promise<void> {\n if (!this.mp3Parser) {\n this.mp3Parser = new MP3FrameParser();\n }\n\n const reader = stream.getReader();\n let currentConfig: MP3Config | null = null;\n let configured = false;\n const bufferedFrames: MP3Frame[] = [];\n let writer: WritableStreamDefaultWriter<EncodedAudioChunk> | null = null;\n\n const ensureWriter = async (): Promise<WritableStreamDefaultWriter<EncodedAudioChunk>> => {\n if (!writer) {\n const cfg = currentConfig;\n if (!cfg) {\n throw new Error('MP3 config missing while creating writer');\n }\n const transform = new TransformStream<EncodedAudioChunk, EncodedAudioChunk>();\n writer = transform.writable.getWriter();\n await decoderChannel.sendStream(transform.readable, {\n streamType: 'audio',\n sessionId: metadata?.sessionId ?? 'default',\n trackId: metadata?.trackId ?? 'main',\n codec: 'mp3',\n sampleRate: cfg.sampleRate,\n numberOfChannels: cfg.channels,\n description: undefined,\n });\n }\n return writer!;\n };\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n\n if (!value) {\n continue;\n }\n\n const { frames, config } = this.mp3Parser.push(value);\n\n if (config) {\n currentConfig = config;\n }\n\n if (config && !configured) {\n await decoderChannel.send('configure' as any, {\n streamType: 'audio',\n clipId: metadata?.clipId ?? 'default',\n codec: 'mp3',\n sampleRate: config.sampleRate,\n numberOfChannels: config.channels,\n description: undefined,\n });\n this.channel.notify('demux_configured', {\n clipId: metadata?.clipId ?? 'default',\n codec: 'mp3',\n sampleRate: config.sampleRate,\n numberOfChannels: config.channels,\n });\n configured = true;\n }\n\n if (config) {\n this.currentFormat = 'mp3';\n }\n\n if (!configured) {\n bufferedFrames.push(...frames);\n continue;\n }\n\n const targetWriter = await ensureWriter();\n const readyFrames = bufferedFrames.splice(0, bufferedFrames.length);\n readyFrames.push(...frames);\n for (const frame of readyFrames) {\n const chunk = new EncodedAudioChunk({\n type: 'key',\n timestamp: frame.timestampUs,\n duration: frame.durationUs,\n data: frame.data.buffer.slice(\n frame.data.byteOffset,\n frame.data.byteOffset + frame.data.byteLength\n ),\n });\n await targetWriter.write(chunk);\n }\n }\n\n const remaining = this.mp3Parser.flush();\n if (remaining.length) {\n if (!configured) {\n throw new Error('MP3 stream ended before configuration');\n }\n const targetWriter = await ensureWriter();\n for (const frame of remaining) {\n const chunk = new EncodedAudioChunk({\n type: 'key',\n timestamp: frame.timestampUs,\n duration: frame.durationUs,\n data: frame.data.buffer.slice(\n frame.data.byteOffset,\n frame.data.byteOffset + frame.data.byteLength\n ),\n });\n await targetWriter.write(chunk);\n }\n }\n\n if (writer) {\n await (writer as WritableStreamDefaultWriter<EncodedAudioChunk>).close();\n }\n\n this.channel.notify('demux_complete', {\n tracksProcessed: 1,\n });\n } finally {\n reader.releaseLock();\n }\n }\n\n /**\n * Get demuxer statistics\n */\n private async handleGetStats(): Promise<{\n queueSize?: number;\n tracksInfo?: any[];\n format?: string;\n state?: WorkerState;\n }> {\n if (this.currentFormat === 'mp3') {\n return {\n tracksInfo: this.mp3Parser ? [{ id: 1, type: 'audio', codec: 'mp3' }] : [],\n format: 'mp3',\n state: this.channel.state,\n };\n }\n\n if (this.demuxer) {\n return {\n tracksInfo: Array.from(this.demuxer.tracks.values()),\n format: 'mp4',\n state: this.channel.state,\n };\n }\n\n return { state: this.channel.state };\n }\n\n /**\n * Dispose worker and cleanup resources\n */\n private async handleDispose(): Promise<{ success: boolean }> {\n if (this.demuxer) {\n this.demuxer.destroy();\n }\n this.demuxer = null;\n this.audioStream = null;\n this.mp3Parser = null;\n this.currentFormat = null;\n\n this.decoderPort?.close();\n this.decoderPort = null;\n\n this.channel.state = WorkerState.Disposed;\n\n return { success: true };\n }\n}\n\n// Initialize worker\nconst worker = new AudioDemuxWorker();\n\n// Handle worker termination\nself.addEventListener('beforeunload', () => {\n worker['handleDispose']();\n});\n\nexport default null; // Required for TypeScript worker compilation\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"audio-demux.worker.js","sources":["../../../../src/stages/demux/MP3FrameParser.ts","../../../../src/stages/demux/audio-demux.worker.ts"],"sourcesContent":["const BITRATE_TABLE: Record<string, number[]> = {\n '1-3': [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0],\n '1-2': [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0],\n '1-1': [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0],\n '2-3': [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0],\n '2-2': [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0],\n '2-1': [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0],\n};\n\nconst SAMPLE_RATE_TABLE: Record<number, number[]> = {\n 1: [44100, 48000, 32000],\n 2: [22050, 24000, 16000],\n 2.5: [11025, 12000, 8000],\n};\n\nexport interface MP3Config {\n codec: 'mp3';\n sampleRate: number;\n channels: number;\n bitrateKbps: number | null;\n samplesPerFrame: number;\n}\n\nexport interface MP3Frame {\n data: Uint8Array;\n timestampUs: number;\n durationUs: number;\n}\n\ninterface ParseResult {\n frames: MP3Frame[];\n config?: MP3Config;\n}\n\nexport class MP3FrameParser {\n private buffer = new Uint8Array(0);\n private config: MP3Config | null = null;\n private timestampUs = 0;\n\n push(chunk: Uint8Array): ParseResult {\n this.appendToBuffer(chunk);\n this.skipId3Headers();\n\n const frames: MP3Frame[] = [];\n let configEmitted: MP3Config | undefined;\n\n let offset = 0;\n while (offset <= this.buffer.length - 4) {\n const header = this.parseHeader(this.buffer, offset);\n if (!header) {\n offset += 1;\n continue;\n }\n\n if (offset + header.frameSize > this.buffer.length) {\n break;\n }\n\n const frameBytes = this.buffer.slice(offset, offset + header.frameSize);\n const durationUs = Math.round((header.samplesPerFrame * 1_000_000) / header.sampleRate);\n\n if (!this.config) {\n this.config = {\n codec: 'mp3',\n sampleRate: header.sampleRate,\n channels: header.channels,\n bitrateKbps: header.bitrateKbps,\n samplesPerFrame: header.samplesPerFrame,\n };\n configEmitted = this.config;\n }\n\n frames.push({\n data: frameBytes,\n timestampUs: this.timestampUs,\n durationUs,\n });\n\n this.timestampUs += durationUs;\n offset += header.frameSize;\n }\n\n if (offset > 0) {\n this.buffer = this.buffer.slice(offset);\n }\n\n return { frames, config: configEmitted };\n }\n\n flush(): MP3Frame[] {\n const { frames } = this.push(new Uint8Array(0));\n const remainder = this.buffer;\n this.buffer = new Uint8Array(0);\n if (remainder.length) {\n console.warn('[MP3FrameParser] Remaining unparsed bytes:', remainder.length);\n }\n return frames;\n }\n\n private appendToBuffer(chunk: Uint8Array): void {\n if (chunk.length === 0) {\n return;\n }\n const combined = new Uint8Array(this.buffer.length + chunk.length);\n combined.set(this.buffer, 0);\n combined.set(chunk, this.buffer.length);\n this.buffer = combined;\n }\n\n private skipId3Headers(): void {\n let offset = 0;\n while (this.buffer.length - offset >= 10) {\n if (\n this.buffer[offset] === 0x49 &&\n this.buffer[offset + 1] === 0x44 &&\n this.buffer[offset + 2] === 0x33\n ) {\n const size = this.readSynchsafeInteger(this.buffer.subarray(offset + 6, offset + 10));\n const total = size + 10;\n if (offset + total <= this.buffer.length) {\n offset += total;\n continue;\n }\n }\n break;\n }\n if (offset > 0) {\n this.buffer = this.buffer.slice(offset);\n }\n }\n\n private parseHeader(\n buffer: Uint8Array,\n offset: number\n ): {\n frameSize: number;\n sampleRate: number;\n channels: number;\n bitrateKbps: number | null;\n samplesPerFrame: number;\n } | null {\n if (offset + 3 >= buffer.length) {\n return null;\n }\n if (buffer[offset] !== 0xff || ((buffer[offset + 1] ?? 0) & 0xe0) !== 0xe0) {\n return null;\n }\n\n const versionBits = ((buffer[offset + 1] ?? 0) >> 3) & 0x03;\n const layerBits = ((buffer[offset + 1] ?? 0) >> 1) & 0x03;\n const bitrateIndex = ((buffer[offset + 2] ?? 0) >> 4) & 0x0f;\n const sampleRateIndex = ((buffer[offset + 2] ?? 0) >> 2) & 0x03;\n const paddingBit = ((buffer[offset + 2] ?? 0) >> 1) & 0x01;\n const channelMode = ((buffer[offset + 3] ?? 0) >> 6) & 0x03;\n\n const version = this.getVersion(versionBits);\n const layer = this.getLayer(layerBits);\n if (!version || !layer) {\n return null;\n }\n\n const sampleRate = SAMPLE_RATE_TABLE[version]?.[sampleRateIndex] ?? null;\n if (!sampleRate) {\n return null;\n }\n\n const bitrateKey = `${version === 1 ? 1 : 2}-${layer}`;\n const bitrateKbps = BITRATE_TABLE[bitrateKey]?.[bitrateIndex] ?? null;\n if (bitrateKbps === null) {\n return null;\n }\n\n const samplesPerFrame = this.getSamplesPerFrame(version, layer);\n const frameSize = this.calculateFrameSize(layer, bitrateKbps, sampleRate, paddingBit);\n if (!frameSize || frameSize < 24) {\n return null;\n }\n\n const channels = channelMode === 3 ? 1 : 2;\n\n return {\n frameSize,\n sampleRate,\n channels,\n bitrateKbps: bitrateKbps || null,\n samplesPerFrame,\n };\n }\n\n private getVersion(bits: number): 1 | 2 | 2.5 | null {\n switch (bits) {\n case 0b11:\n return 1;\n case 0b10:\n return 2;\n case 0b00:\n return 2.5;\n default:\n return null;\n }\n }\n\n private getLayer(bits: number): 1 | 2 | 3 | null {\n switch (bits) {\n case 0b11:\n return 1;\n case 0b10:\n return 2;\n case 0b01:\n return 3;\n default:\n return null;\n }\n }\n\n private getSamplesPerFrame(version: 1 | 2 | 2.5, layer: 1 | 2 | 3): number {\n if (layer === 1) {\n return 384;\n }\n if (layer === 2) {\n return 1152;\n }\n return version === 1 ? 1152 : 576;\n }\n\n private calculateFrameSize(\n layer: 1 | 2 | 3,\n bitrateKbps: number,\n sampleRate: number,\n padding: number\n ): number {\n if (bitrateKbps <= 0) {\n return 0;\n }\n\n if (layer === 1) {\n return ((12 * bitrateKbps * 1000) / sampleRate + padding) * 4;\n }\n\n return Math.floor((144 * bitrateKbps * 1000) / sampleRate + padding);\n }\n\n private readSynchsafeInteger(bytes: Uint8Array): number {\n if (bytes.length !== 4) {\n return 0;\n }\n return (\n (((bytes[0] ?? 0) & 0x7f) << 21) |\n (((bytes[1] ?? 0) & 0x7f) << 14) |\n (((bytes[2] ?? 0) & 0x7f) << 7) |\n ((bytes[3] ?? 0) & 0x7f)\n );\n }\n}\n","import { WorkerChannel } from '../../worker/WorkerChannel';\nimport { WorkerMessageType, WorkerState } from '../../worker/types';\nimport { MP4Demuxer } from './MP4Demuxer';\nimport { MP3FrameParser, type MP3Config, type MP3Frame } from './MP3FrameParser';\nimport type { DemuxConfig } from './types';\n\n/**\n * AudioDemuxWorker - First stage for audio processing\n * Extracts audio tracks from various container formats (MP3, MP4/M4A, etc.)\n *\n * Pipeline: ResourceLoader (Main Thread) → AudioDemuxWorker → DecodeWorker\n *\n * Features:\n * - Multi-format support (MP3, AAC in MP4/M4A)\n * - Stream-based processing with backpressure\n * - Direct streaming to DecodeWorker\n */\nexport class AudioDemuxWorker {\n private channel: WorkerChannel;\n private demuxer: MP4Demuxer | null = null;\n private audioStream: TransformStream<Uint8Array, EncodedAudioChunk> | null = null;\n private mp3Parser: MP3FrameParser | null = null;\n private currentFormat: 'mp3' | 'mp4' | null = null;\n\n // Connection to decoder worker\n private decoderPort: MessagePort | null = null;\n\n constructor() {\n // Initialize WorkerChannel\n this.channel = new WorkerChannel(self as any, {\n name: 'AudioDemuxWorker',\n timeout: 30000,\n });\n\n this.setupHandlers();\n }\n\n private setupHandlers(): void {\n // Register message handlers\n this.channel.registerHandler('configure', this.handleConfigure.bind(this));\n // Unified stream connect (feature-flagged)\n this.channel.registerHandler('connect' as any, this.handleConnect.bind(this));\n // Unified stream connect only\n this.channel.registerHandler('get_stats', this.handleGetStats.bind(this));\n this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));\n\n // Setup stream receiver from ResourceLoader (main thread)\n this.channel.receiveStream(this.handleReceiveStream.bind(this));\n }\n\n /**\n * Unified connect handler used by stream pipeline\n */\n private async handleConnect(payload: {\n direction: 'upstream';\n port: MessagePort;\n streamType: 'video' | 'audio' | 'frame' | 'chunk';\n }): Promise<{ success: boolean }> {\n this.decoderPort = payload.port;\n // Demux connects upstream to decoder\n return { success: true };\n }\n\n /**\n * Configure demuxer with format settings\n * @param payload.config - Demuxer configuration\n * @param payload.initial - If true, initialize worker state; otherwise just update config\n */\n private async handleConfigure(payload: {\n config: DemuxConfig;\n initial?: boolean;\n }): Promise<{ success: boolean; tracks?: any[] }> {\n const { config, initial = false } = payload;\n\n try {\n if (initial) {\n // Initial setup - set worker state to ready\n this.channel.state = WorkerState.Ready;\n\n // Detect format and create appropriate demuxer\n const format = config.container || this.detectFormat(config);\n\n if (format === 'mp3') {\n this.mp3Parser = new MP3FrameParser();\n this.demuxer?.destroy();\n this.demuxer = null;\n this.audioStream = null;\n this.currentFormat = 'mp3';\n\n const mp3Track = { id: 1, type: 'audio', codec: 'mp3' };\n this.channel.notify('configured', {\n tracks: [mp3Track],\n format,\n codec: 'mp3',\n });\n\n return { success: true, tracks: [mp3Track] };\n } else if (format === 'mp4' || format === 'm4a') {\n if (this.demuxer) {\n this.demuxer.destroy();\n }\n\n this.demuxer = new MP4Demuxer({\n ...config,\n skipVideo: true, // Audio only\n });\n this.audioStream = this.demuxer.createAudioStream();\n this.mp3Parser = null;\n this.currentFormat = 'mp4';\n } else {\n throw {\n code: 'UNSUPPORTED_FORMAT',\n message: `Unsupported audio format: ${format}`,\n };\n }\n\n if (!this.audioStream) {\n throw {\n code: 'NO_AUDIO_TRACK',\n message: 'No audio track found in container',\n };\n }\n\n const tracks = Array.from(this.demuxer.tracks.values());\n\n // Notify configuration complete\n this.channel.notify('configured', {\n tracks,\n format,\n codec: tracks[0]?.codec,\n });\n\n return { success: true, tracks };\n } else {\n // Update configuration only (e.g., backpressure settings)\n if (!this.demuxer) {\n throw {\n code: 'NOT_INITIALIZED',\n message: 'Demuxer not initialized. Call configure with initial=true first',\n };\n }\n\n // Demuxers don't support runtime config updates\n // Would need initial=true for changes\n\n return { success: true };\n }\n } catch (error: any) {\n throw {\n code: error.code || 'CONFIG_ERROR',\n message: error.message,\n };\n }\n }\n\n /**\n * Detect audio format from configuration\n */\n private detectFormat(config: DemuxConfig): string {\n // Simple format detection based on codec or file extension\n if (config.codec?.includes('mp3')) return 'mp3';\n if (config.codec?.includes('aac')) return 'mp4';\n if (config.codec?.includes('opus')) return 'webm';\n\n return 'mp3';\n }\n\n /**\n * Handle input stream from ResourceLoader (main thread)\n */\n private async handleReceiveStream(\n stream: ReadableStream<Uint8Array>,\n metadata?: Record<string, any>\n ): Promise<void> {\n // Initialize demuxer if not configured\n if (this.currentFormat === 'mp3' && !this.mp3Parser) {\n this.mp3Parser = new MP3FrameParser();\n }\n\n if (this.currentFormat === 'mp4' && (!this.demuxer || !this.audioStream)) {\n await this.handleConfigure({\n config: { container: 'mp4', highWaterMark: 10 },\n initial: true,\n }).catch((error) => {\n console.error('[AudioDemuxWorker] Configure error:', metadata?.sessionId, error);\n return;\n });\n }\n\n if (!this.decoderPort) {\n console.error('[AudioDemuxWorker] Decoder not connected');\n return;\n }\n\n // Setup channel to decoder\n const decoderChannel = new WorkerChannel(this.decoderPort, {\n name: 'AudioDemux-Decoder',\n timeout: 30000,\n });\n\n if (this.currentFormat === 'mp3') {\n await this.pipeMp3Stream(stream, decoderChannel, metadata).catch((error) => {\n console.error('[AudioDemuxWorker] MP3 stream error:', metadata?.sessionId, error);\n });\n return;\n }\n\n if (!this.demuxer || !this.audioStream) {\n console.error('[AudioDemuxWorker] Audio demuxer not initialized');\n return;\n }\n\n const transformed = stream.pipeThrough(this.audioStream);\n const trackInfo = Array.from(this.demuxer.tracks.values())[0];\n\n await decoderChannel\n .sendStream(transformed, {\n streamType: 'audio',\n sessionId: metadata?.sessionId ?? 'default',\n trackId: metadata?.trackId ?? 'main',\n codec: trackInfo?.codec ?? 'mp4a.40.2',\n sampleRate: trackInfo?.sampleRate ?? 44_100,\n numberOfChannels: trackInfo?.numberOfChannels ?? 2,\n description: trackInfo?.description,\n })\n .catch((error) => {\n console.error('[AudioDemuxWorker] Send stream error:', metadata?.sessionId, error);\n });\n\n this.channel.notify('demux_complete', {\n tracksProcessed: this.demuxer?.tracks.size || 0,\n });\n }\n\n private async pipeMp3Stream(\n stream: ReadableStream<Uint8Array>,\n decoderChannel: WorkerChannel,\n metadata?: Record<string, any>\n ): Promise<void> {\n if (!this.mp3Parser) {\n this.mp3Parser = new MP3FrameParser();\n }\n\n const reader = stream.getReader();\n let currentConfig: MP3Config | null = null;\n let configured = false;\n const bufferedFrames: MP3Frame[] = [];\n let writer: WritableStreamDefaultWriter<EncodedAudioChunk> | null = null;\n\n const ensureWriter = async (): Promise<WritableStreamDefaultWriter<EncodedAudioChunk>> => {\n if (!writer) {\n const cfg = currentConfig;\n if (!cfg) {\n throw new Error('MP3 config missing while creating writer');\n }\n const transform = new TransformStream<EncodedAudioChunk, EncodedAudioChunk>();\n writer = transform.writable.getWriter();\n await decoderChannel.sendStream(transform.readable, {\n streamType: 'audio',\n sessionId: metadata?.sessionId ?? 'default',\n trackId: metadata?.trackId ?? 'main',\n codec: 'mp3',\n sampleRate: cfg.sampleRate,\n numberOfChannels: cfg.channels,\n description: undefined,\n });\n }\n return writer!;\n };\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n\n if (!value) {\n continue;\n }\n\n const { frames, config } = this.mp3Parser.push(value);\n\n if (config) {\n currentConfig = config;\n }\n\n if (config && !configured) {\n await decoderChannel.send('configure' as any, {\n streamType: 'audio',\n clipId: metadata?.clipId ?? 'default',\n codec: 'mp3',\n sampleRate: config.sampleRate,\n numberOfChannels: config.channels,\n description: undefined,\n });\n this.channel.notify('demux_configured', {\n clipId: metadata?.clipId ?? 'default',\n codec: 'mp3',\n sampleRate: config.sampleRate,\n numberOfChannels: config.channels,\n });\n configured = true;\n }\n\n if (config) {\n this.currentFormat = 'mp3';\n }\n\n if (!configured) {\n bufferedFrames.push(...frames);\n continue;\n }\n\n const targetWriter = await ensureWriter();\n const readyFrames = bufferedFrames.splice(0, bufferedFrames.length);\n readyFrames.push(...frames);\n for (const frame of readyFrames) {\n const chunk = new EncodedAudioChunk({\n type: 'key',\n timestamp: frame.timestampUs,\n duration: frame.durationUs,\n data: frame.data.buffer.slice(\n frame.data.byteOffset,\n frame.data.byteOffset + frame.data.byteLength\n ),\n });\n await targetWriter.write(chunk);\n }\n }\n\n const remaining = this.mp3Parser.flush();\n if (remaining.length) {\n if (!configured) {\n throw new Error('MP3 stream ended before configuration');\n }\n const targetWriter = await ensureWriter();\n for (const frame of remaining) {\n const chunk = new EncodedAudioChunk({\n type: 'key',\n timestamp: frame.timestampUs,\n duration: frame.durationUs,\n data: frame.data.buffer.slice(\n frame.data.byteOffset,\n frame.data.byteOffset + frame.data.byteLength\n ),\n });\n await targetWriter.write(chunk);\n }\n }\n\n if (writer) {\n await (writer as WritableStreamDefaultWriter<EncodedAudioChunk>).close();\n }\n\n this.channel.notify('demux_complete', {\n tracksProcessed: 1,\n });\n } finally {\n reader.releaseLock();\n }\n }\n\n /**\n * Get demuxer statistics\n */\n private async handleGetStats(): Promise<{\n queueSize?: number;\n tracksInfo?: any[];\n format?: string;\n state?: WorkerState;\n }> {\n if (this.currentFormat === 'mp3') {\n return {\n tracksInfo: this.mp3Parser ? [{ id: 1, type: 'audio', codec: 'mp3' }] : [],\n format: 'mp3',\n state: this.channel.state,\n };\n }\n\n if (this.demuxer) {\n return {\n tracksInfo: Array.from(this.demuxer.tracks.values()),\n format: 'mp4',\n state: this.channel.state,\n };\n }\n\n return { state: this.channel.state };\n }\n\n /**\n * Dispose worker and cleanup resources\n */\n private async handleDispose(): Promise<{ success: boolean }> {\n if (this.demuxer) {\n this.demuxer.destroy();\n }\n this.demuxer = null;\n this.audioStream = null;\n this.mp3Parser = null;\n this.currentFormat = null;\n\n this.decoderPort?.close();\n this.decoderPort = null;\n\n this.channel.state = WorkerState.Disposed;\n\n return { success: true };\n }\n}\n\n// Initialize worker\nconst worker = new AudioDemuxWorker();\n\n// Handle worker termination\nself.addEventListener('beforeunload', () => {\n worker['handleDispose']();\n});\n\nexport default null; // Required for TypeScript worker compilation\n"],"names":[],"mappings":";;AAAA,MAAM,gBAA0C;AAAA,EAC9C,OAAO,CAAC,GAAG,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,CAAC;AAAA,EAC3E,OAAO,CAAC,GAAG,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,CAAC;AAAA,EAC5E,OAAO,CAAC,GAAG,IAAI,IAAI,IAAI,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,CAAC;AAAA,EAC/E,OAAO,CAAC,GAAG,GAAG,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,KAAK,KAAK,KAAK,CAAC;AAAA,EACvE,OAAO,CAAC,GAAG,GAAG,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,KAAK,KAAK,KAAK,CAAC;AAAA,EACvE,OAAO,CAAC,GAAG,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,CAAC;AAC9E;AAEA,MAAM,oBAA8C;AAAA,EAClD,GAAG,CAAC,OAAO,MAAO,IAAK;AAAA,EACvB,GAAG,CAAC,OAAO,MAAO,IAAK;AAAA,EACvB,KAAK,CAAC,OAAO,MAAO,GAAI;AAC1B;AAqBO,MAAM,eAAe;AAAA,EAClB,SAAS,IAAI,WAAW,CAAC;AAAA,EACzB,SAA2B;AAAA,EAC3B,cAAc;AAAA,EAEtB,KAAK,OAAgC;AACnC,SAAK,eAAe,KAAK;AACzB,SAAK,eAAA;AAEL,UAAM,SAAqB,CAAA;AAC3B,QAAI;AAEJ,QAAI,SAAS;AACb,WAAO,UAAU,KAAK,OAAO,SAAS,GAAG;AACvC,YAAM,SAAS,KAAK,YAAY,KAAK,QAAQ,MAAM;AACnD,UAAI,CAAC,QAAQ;AACX,kBAAU;AACV;AAAA,MACF;AAEA,UAAI,SAAS,OAAO,YAAY,KAAK,OAAO,QAAQ;AAClD;AAAA,MACF;AAEA,YAAM,aAAa,KAAK,OAAO,MAAM,QAAQ,SAAS,OAAO,SAAS;AACtE,YAAM,aAAa,KAAK,MAAO,OAAO,kBAAkB,MAAa,OAAO,UAAU;AAEtF,UAAI,CAAC,KAAK,QAAQ;AAChB,aAAK,SAAS;AAAA,UACZ,OAAO;AAAA,UACP,YAAY,OAAO;AAAA,UACnB,UAAU,OAAO;AAAA,UACjB,aAAa,OAAO;AAAA,UACpB,iBAAiB,OAAO;AAAA,QAAA;AAE1B,wBAAgB,KAAK;AAAA,MACvB;AAEA,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN,aAAa,KAAK;AAAA,QAClB;AAAA,MAAA,CACD;AAED,WAAK,eAAe;AACpB,gBAAU,OAAO;AAAA,IACnB;AAEA,QAAI,SAAS,GAAG;AACd,WAAK,SAAS,KAAK,OAAO,MAAM,MAAM;AAAA,IACxC;AAEA,WAAO,EAAE,QAAQ,QAAQ,cAAA;AAAA,EAC3B;AAAA,EAEA,QAAoB;AAClB,UAAM,EAAE,WAAW,KAAK,KAAK,IAAI,WAAW,CAAC,CAAC;AAC9C,UAAM,YAAY,KAAK;AACvB,SAAK,SAAS,IAAI,WAAW,CAAC;AAC9B,QAAI,UAAU,QAAQ;AACpB,cAAQ,KAAK,8CAA8C,UAAU,MAAM;AAAA,IAC7E;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,eAAe,OAAyB;AAC9C,QAAI,MAAM,WAAW,GAAG;AACtB;AAAA,IACF;AACA,UAAM,WAAW,IAAI,WAAW,KAAK,OAAO,SAAS,MAAM,MAAM;AACjE,aAAS,IAAI,KAAK,QAAQ,CAAC;AAC3B,aAAS,IAAI,OAAO,KAAK,OAAO,MAAM;AACtC,SAAK,SAAS;AAAA,EAChB;AAAA,EAEQ,iBAAuB;AAC7B,QAAI,SAAS;AACb,WAAO,KAAK,OAAO,SAAS,UAAU,IAAI;AACxC,UACE,KAAK,OAAO,MAAM,MAAM,MACxB,KAAK,OAAO,SAAS,CAAC,MAAM,MAC5B,KAAK,OAAO,SAAS,CAAC,MAAM,IAC5B;AACA,cAAM,OAAO,KAAK,qBAAqB,KAAK,OAAO,SAAS,SAAS,GAAG,SAAS,EAAE,CAAC;AACpF,cAAM,QAAQ,OAAO;AACrB,YAAI,SAAS,SAAS,KAAK,OAAO,QAAQ;AACxC,oBAAU;AACV;AAAA,QACF;AAAA,MACF;AACA;AAAA,IACF;AACA,QAAI,SAAS,GAAG;AACd,WAAK,SAAS,KAAK,OAAO,MAAM,MAAM;AAAA,IACxC;AAAA,EACF;AAAA,EAEQ,YACN,QACA,QAOO;AACP,QAAI,SAAS,KAAK,OAAO,QAAQ;AAC/B,aAAO;AAAA,IACT;AACA,QAAI,OAAO,MAAM,MAAM,SAAU,OAAO,SAAS,CAAC,KAAK,KAAK,SAAU,KAAM;AAC1E,aAAO;AAAA,IACT;AAEA,UAAM,eAAgB,OAAO,SAAS,CAAC,KAAK,MAAM,IAAK;AACvD,UAAM,aAAc,OAAO,SAAS,CAAC,KAAK,MAAM,IAAK;AACrD,UAAM,gBAAiB,OAAO,SAAS,CAAC,KAAK,MAAM,IAAK;AACxD,UAAM,mBAAoB,OAAO,SAAS,CAAC,KAAK,MAAM,IAAK;AAC3D,UAAM,cAAe,OAAO,SAAS,CAAC,KAAK,MAAM,IAAK;AACtD,UAAM,eAAgB,OAAO,SAAS,CAAC,KAAK,MAAM,IAAK;AAEvD,UAAM,UAAU,KAAK,WAAW,WAAW;AAC3C,UAAM,QAAQ,KAAK,SAAS,SAAS;AACrC,QAAI,CAAC,WAAW,CAAC,OAAO;AACtB,aAAO;AAAA,IACT;AAEA,UAAM,aAAa,kBAAkB,OAAO,IAAI,eAAe,KAAK;AACpE,QAAI,CAAC,YAAY;AACf,aAAO;AAAA,IACT;AAEA,UAAM,aAAa,GAAG,YAAY,IAAI,IAAI,CAAC,IAAI,KAAK;AACpD,UAAM,cAAc,cAAc,UAAU,IAAI,YAAY,KAAK;AACjE,QAAI,gBAAgB,MAAM;AACxB,aAAO;AAAA,IACT;AAEA,UAAM,kBAAkB,KAAK,mBAAmB,SAAS,KAAK;AAC9D,UAAM,YAAY,KAAK,mBAAmB,OAAO,aAAa,YAAY,UAAU;AACpF,QAAI,CAAC,aAAa,YAAY,IAAI;AAChC,aAAO;AAAA,IACT;AAEA,UAAM,WAAW,gBAAgB,IAAI,IAAI;AAEzC,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA,aAAa,eAAe;AAAA,MAC5B;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEQ,WAAW,MAAkC;AACnD,YAAQ,MAAA;AAAA,MACN,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT;AACE,eAAO;AAAA,IAAA;AAAA,EAEb;AAAA,EAEQ,SAAS,MAAgC;AAC/C,YAAQ,MAAA;AAAA,MACN,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT;AACE,eAAO;AAAA,IAAA;AAAA,EAEb;AAAA,EAEQ,mBAAmB,SAAsB,OAA0B;AACzE,QAAI,UAAU,GAAG;AACf,aAAO;AAAA,IACT;AACA,QAAI,UAAU,GAAG;AACf,aAAO;AAAA,IACT;AACA,WAAO,YAAY,IAAI,OAAO;AAAA,EAChC;AAAA,EAEQ,mBACN,OACA,aACA,YACA,SACQ;AACR,QAAI,eAAe,GAAG;AACpB,aAAO;AAAA,IACT;AAEA,QAAI,UAAU,GAAG;AACf,cAAS,KAAK,cAAc,MAAQ,aAAa,WAAW;AAAA,IAC9D;AAEA,WAAO,KAAK,MAAO,MAAM,cAAc,MAAQ,aAAa,OAAO;AAAA,EACrE;AAAA,EAEQ,qBAAqB,OAA2B;AACtD,QAAI,MAAM,WAAW,GAAG;AACtB,aAAO;AAAA,IACT;AACA,aACK,MAAM,CAAC,KAAK,KAAK,QAAS,OAC1B,MAAM,CAAC,KAAK,KAAK,QAAS,OAC1B,MAAM,CAAC,KAAK,KAAK,QAAS,KAC3B,MAAM,CAAC,KAAK,KAAK;AAAA,EAEvB;AACF;AC5OO,MAAM,iBAAiB;AAAA,EACpB;AAAA,EACA,UAA6B;AAAA,EAC7B,cAAqE;AAAA,EACrE,YAAmC;AAAA,EACnC,gBAAsC;AAAA;AAAA,EAGtC,cAAkC;AAAA,EAE1C,cAAc;AAEZ,SAAK,UAAU,IAAI,cAAc,MAAa;AAAA,MAC5C,MAAM;AAAA,MACN,SAAS;AAAA,IAAA,CACV;AAED,SAAK,cAAA;AAAA,EACP;AAAA,EAEQ,gBAAsB;AAE5B,SAAK,QAAQ,gBAAgB,aAAa,KAAK,gBAAgB,KAAK,IAAI,CAAC;AAEzE,SAAK,QAAQ,gBAAgB,WAAkB,KAAK,cAAc,KAAK,IAAI,CAAC;AAE5E,SAAK,QAAQ,gBAAgB,aAAa,KAAK,eAAe,KAAK,IAAI,CAAC;AACxE,SAAK,QAAQ,gBAAgB,kBAAkB,SAAS,KAAK,cAAc,KAAK,IAAI,CAAC;AAGrF,SAAK,QAAQ,cAAc,KAAK,oBAAoB,KAAK,IAAI,CAAC;AAAA,EAChE;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,cAAc,SAIM;AAChC,SAAK,cAAc,QAAQ;AAE3B,WAAO,EAAE,SAAS,KAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,gBAAgB,SAGoB;AAChD,UAAM,EAAE,QAAQ,UAAU,MAAA,IAAU;AAEpC,QAAI;AACF,UAAI,SAAS;AAEX,aAAK,QAAQ,QAAQ,YAAY;AAGjC,cAAM,SAAS,OAAO,aAAa,KAAK,aAAa,MAAM;AAE3D,YAAI,WAAW,OAAO;AACpB,eAAK,YAAY,IAAI,eAAA;AACrB,eAAK,SAAS,QAAA;AACd,eAAK,UAAU;AACf,eAAK,cAAc;AACnB,eAAK,gBAAgB;AAErB,gBAAM,WAAW,EAAE,IAAI,GAAG,MAAM,SAAS,OAAO,MAAA;AAChD,eAAK,QAAQ,OAAO,cAAc;AAAA,YAChC,QAAQ,CAAC,QAAQ;AAAA,YACjB;AAAA,YACA,OAAO;AAAA,UAAA,CACR;AAED,iBAAO,EAAE,SAAS,MAAM,QAAQ,CAAC,QAAQ,EAAA;AAAA,QAC3C,WAAW,WAAW,SAAS,WAAW,OAAO;AAC/C,cAAI,KAAK,SAAS;AAChB,iBAAK,QAAQ,QAAA;AAAA,UACf;AAEA,eAAK,UAAU,IAAI,WAAW;AAAA,YAC5B,GAAG;AAAA,YACH,WAAW;AAAA;AAAA,UAAA,CACZ;AACD,eAAK,cAAc,KAAK,QAAQ,kBAAA;AAChC,eAAK,YAAY;AACjB,eAAK,gBAAgB;AAAA,QACvB,OAAO;AACL,gBAAM;AAAA,YACJ,MAAM;AAAA,YACN,SAAS,6BAA6B,MAAM;AAAA,UAAA;AAAA,QAEhD;AAEA,YAAI,CAAC,KAAK,aAAa;AACrB,gBAAM;AAAA,YACJ,MAAM;AAAA,YACN,SAAS;AAAA,UAAA;AAAA,QAEb;AAEA,cAAM,SAAS,MAAM,KAAK,KAAK,QAAQ,OAAO,QAAQ;AAGtD,aAAK,QAAQ,OAAO,cAAc;AAAA,UAChC;AAAA,UACA;AAAA,UACA,OAAO,OAAO,CAAC,GAAG;AAAA,QAAA,CACnB;AAED,eAAO,EAAE,SAAS,MAAM,OAAA;AAAA,MAC1B,OAAO;AAEL,YAAI,CAAC,KAAK,SAAS;AACjB,gBAAM;AAAA,YACJ,MAAM;AAAA,YACN,SAAS;AAAA,UAAA;AAAA,QAEb;AAKA,eAAO,EAAE,SAAS,KAAA;AAAA,MACpB;AAAA,IACF,SAAS,OAAY;AACnB,YAAM;AAAA,QACJ,MAAM,MAAM,QAAQ;AAAA,QACpB,SAAS,MAAM;AAAA,MAAA;AAAA,IAEnB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,aAAa,QAA6B;AAEhD,QAAI,OAAO,OAAO,SAAS,KAAK,EAAG,QAAO;AAC1C,QAAI,OAAO,OAAO,SAAS,KAAK,EAAG,QAAO;AAC1C,QAAI,OAAO,OAAO,SAAS,MAAM,EAAG,QAAO;AAE3C,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,oBACZ,QACA,UACe;AAEf,QAAI,KAAK,kBAAkB,SAAS,CAAC,KAAK,WAAW;AACnD,WAAK,YAAY,IAAI,eAAA;AAAA,IACvB;AAEA,QAAI,KAAK,kBAAkB,UAAU,CAAC,KAAK,WAAW,CAAC,KAAK,cAAc;AACxE,YAAM,KAAK,gBAAgB;AAAA,QACzB,QAAQ,EAAE,WAAW,OAAO,eAAe,GAAA;AAAA,QAC3C,SAAS;AAAA,MAAA,CACV,EAAE,MAAM,CAAC,UAAU;AAClB,gBAAQ,MAAM,uCAAuC,UAAU,WAAW,KAAK;AAC/E;AAAA,MACF,CAAC;AAAA,IACH;AAEA,QAAI,CAAC,KAAK,aAAa;AACrB,cAAQ,MAAM,0CAA0C;AACxD;AAAA,IACF;AAGA,UAAM,iBAAiB,IAAI,cAAc,KAAK,aAAa;AAAA,MACzD,MAAM;AAAA,MACN,SAAS;AAAA,IAAA,CACV;AAED,QAAI,KAAK,kBAAkB,OAAO;AAChC,YAAM,KAAK,cAAc,QAAQ,gBAAgB,QAAQ,EAAE,MAAM,CAAC,UAAU;AAC1E,gBAAQ,MAAM,wCAAwC,UAAU,WAAW,KAAK;AAAA,MAClF,CAAC;AACD;AAAA,IACF;AAEA,QAAI,CAAC,KAAK,WAAW,CAAC,KAAK,aAAa;AACtC,cAAQ,MAAM,kDAAkD;AAChE;AAAA,IACF;AAEA,UAAM,cAAc,OAAO,YAAY,KAAK,WAAW;AACvD,UAAM,YAAY,MAAM,KAAK,KAAK,QAAQ,OAAO,QAAQ,EAAE,CAAC;AAE5D,UAAM,eACH,WAAW,aAAa;AAAA,MACvB,YAAY;AAAA,MACZ,WAAW,UAAU,aAAa;AAAA,MAClC,SAAS,UAAU,WAAW;AAAA,MAC9B,OAAO,WAAW,SAAS;AAAA,MAC3B,YAAY,WAAW,cAAc;AAAA,MACrC,kBAAkB,WAAW,oBAAoB;AAAA,MACjD,aAAa,WAAW;AAAA,IAAA,CACzB,EACA,MAAM,CAAC,UAAU;AAChB,cAAQ,MAAM,yCAAyC,UAAU,WAAW,KAAK;AAAA,IACnF,CAAC;AAEH,SAAK,QAAQ,OAAO,kBAAkB;AAAA,MACpC,iBAAiB,KAAK,SAAS,OAAO,QAAQ;AAAA,IAAA,CAC/C;AAAA,EACH;AAAA,EAEA,MAAc,cACZ,QACA,gBACA,UACe;AACf,QAAI,CAAC,KAAK,WAAW;AACnB,WAAK,YAAY,IAAI,eAAA;AAAA,IACvB;AAEA,UAAM,SAAS,OAAO,UAAA;AACtB,QAAI,gBAAkC;AACtC,QAAI,aAAa;AACjB,UAAM,iBAA6B,CAAA;AACnC,QAAI,SAAgE;AAEpE,UAAM,eAAe,YAAqE;AACxF,UAAI,CAAC,QAAQ;AACX,cAAM,MAAM;AACZ,YAAI,CAAC,KAAK;AACR,gBAAM,IAAI,MAAM,0CAA0C;AAAA,QAC5D;AACA,cAAM,YAAY,IAAI,gBAAA;AACtB,iBAAS,UAAU,SAAS,UAAA;AAC5B,cAAM,eAAe,WAAW,UAAU,UAAU;AAAA,UAClD,YAAY;AAAA,UACZ,WAAW,UAAU,aAAa;AAAA,UAClC,SAAS,UAAU,WAAW;AAAA,UAC9B,OAAO;AAAA,UACP,YAAY,IAAI;AAAA,UAChB,kBAAkB,IAAI;AAAA,UACtB,aAAa;AAAA,QAAA,CACd;AAAA,MACH;AACA,aAAO;AAAA,IACT;AAEA,QAAI;AACF,aAAO,MAAM;AACX,cAAM,EAAE,MAAM,MAAA,IAAU,MAAM,OAAO,KAAA;AACrC,YAAI,MAAM;AACR;AAAA,QACF;AAEA,YAAI,CAAC,OAAO;AACV;AAAA,QACF;AAEA,cAAM,EAAE,QAAQ,OAAA,IAAW,KAAK,UAAU,KAAK,KAAK;AAEpD,YAAI,QAAQ;AACV,0BAAgB;AAAA,QAClB;AAEA,YAAI,UAAU,CAAC,YAAY;AACzB,gBAAM,eAAe,KAAK,aAAoB;AAAA,YAC5C,YAAY;AAAA,YACZ,QAAQ,UAAU,UAAU;AAAA,YAC5B,OAAO;AAAA,YACP,YAAY,OAAO;AAAA,YACnB,kBAAkB,OAAO;AAAA,YACzB,aAAa;AAAA,UAAA,CACd;AACD,eAAK,QAAQ,OAAO,oBAAoB;AAAA,YACtC,QAAQ,UAAU,UAAU;AAAA,YAC5B,OAAO;AAAA,YACP,YAAY,OAAO;AAAA,YACnB,kBAAkB,OAAO;AAAA,UAAA,CAC1B;AACD,uBAAa;AAAA,QACf;AAEA,YAAI,QAAQ;AACV,eAAK,gBAAgB;AAAA,QACvB;AAEA,YAAI,CAAC,YAAY;AACf,yBAAe,KAAK,GAAG,MAAM;AAC7B;AAAA,QACF;AAEA,cAAM,eAAe,MAAM,aAAA;AAC3B,cAAM,cAAc,eAAe,OAAO,GAAG,eAAe,MAAM;AAClE,oBAAY,KAAK,GAAG,MAAM;AAC1B,mBAAW,SAAS,aAAa;AAC/B,gBAAM,QAAQ,IAAI,kBAAkB;AAAA,YAClC,MAAM;AAAA,YACN,WAAW,MAAM;AAAA,YACjB,UAAU,MAAM;AAAA,YAChB,MAAM,MAAM,KAAK,OAAO;AAAA,cACtB,MAAM,KAAK;AAAA,cACX,MAAM,KAAK,aAAa,MAAM,KAAK;AAAA,YAAA;AAAA,UACrC,CACD;AACD,gBAAM,aAAa,MAAM,KAAK;AAAA,QAChC;AAAA,MACF;AAEA,YAAM,YAAY,KAAK,UAAU,MAAA;AACjC,UAAI,UAAU,QAAQ;AACpB,YAAI,CAAC,YAAY;AACf,gBAAM,IAAI,MAAM,uCAAuC;AAAA,QACzD;AACA,cAAM,eAAe,MAAM,aAAA;AAC3B,mBAAW,SAAS,WAAW;AAC7B,gBAAM,QAAQ,IAAI,kBAAkB;AAAA,YAClC,MAAM;AAAA,YACN,WAAW,MAAM;AAAA,YACjB,UAAU,MAAM;AAAA,YAChB,MAAM,MAAM,KAAK,OAAO;AAAA,cACtB,MAAM,KAAK;AAAA,cACX,MAAM,KAAK,aAAa,MAAM,KAAK;AAAA,YAAA;AAAA,UACrC,CACD;AACD,gBAAM,aAAa,MAAM,KAAK;AAAA,QAChC;AAAA,MACF;AAEA,UAAI,QAAQ;AACV,cAAO,OAA0D,MAAA;AAAA,MACnE;AAEA,WAAK,QAAQ,OAAO,kBAAkB;AAAA,QACpC,iBAAiB;AAAA,MAAA,CAClB;AAAA,IACH,UAAA;AACE,aAAO,YAAA;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,iBAKX;AACD,QAAI,KAAK,kBAAkB,OAAO;AAChC,aAAO;AAAA,QACL,YAAY,KAAK,YAAY,CAAC,EAAE,IAAI,GAAG,MAAM,SAAS,OAAO,MAAA,CAAO,IAAI,CAAA;AAAA,QACxE,QAAQ;AAAA,QACR,OAAO,KAAK,QAAQ;AAAA,MAAA;AAAA,IAExB;AAEA,QAAI,KAAK,SAAS;AAChB,aAAO;AAAA,QACL,YAAY,MAAM,KAAK,KAAK,QAAQ,OAAO,QAAQ;AAAA,QACnD,QAAQ;AAAA,QACR,OAAO,KAAK,QAAQ;AAAA,MAAA;AAAA,IAExB;AAEA,WAAO,EAAE,OAAO,KAAK,QAAQ,MAAA;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,gBAA+C;AAC3D,QAAI,KAAK,SAAS;AAChB,WAAK,QAAQ,QAAA;AAAA,IACf;AACA,SAAK,UAAU;AACf,SAAK,cAAc;AACnB,SAAK,YAAY;AACjB,SAAK,gBAAgB;AAErB,SAAK,aAAa,MAAA;AAClB,SAAK,cAAc;AAEnB,SAAK,QAAQ,QAAQ,YAAY;AAEjC,WAAO,EAAE,SAAS,KAAA;AAAA,EACpB;AACF;AAGA,MAAM,SAAS,IAAI,iBAAA;AAGnB,KAAK,iBAAiB,gBAAgB,MAAM;AAC1C,SAAO,eAAe,EAAA;AACxB,CAAC;AAED,MAAA,oBAAe;"}
|
|
@@ -80,7 +80,8 @@ class VideoDemuxWorker {
|
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
if (!this.downstreamPort) {
|
|
83
|
-
|
|
83
|
+
console.error("[VideoDemuxWorker] Decoder not connected");
|
|
84
|
+
return;
|
|
84
85
|
}
|
|
85
86
|
const videoStream = this.demuxer.createVideoStream();
|
|
86
87
|
const downstreamChannel = new WorkerChannel(this.downstreamPort, {
|
|
@@ -91,7 +92,9 @@ class VideoDemuxWorker {
|
|
|
91
92
|
streamType: "video",
|
|
92
93
|
sessionId: this.sessionId
|
|
93
94
|
});
|
|
94
|
-
await stream.pipeTo(videoStream.writable)
|
|
95
|
+
await stream.pipeTo(videoStream.writable).catch((error) => {
|
|
96
|
+
console.error("[VideoDemuxWorker] Stream error:", this.sessionId, error);
|
|
97
|
+
});
|
|
95
98
|
}
|
|
96
99
|
handleDemuxerReady() {
|
|
97
100
|
if (!this.demuxer || !this.downstreamPort) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video-demux.worker.js","sources":["../../../../src/stages/demux/video-demux.worker.ts"],"sourcesContent":["import { WorkerChannel } from '../../worker/WorkerChannel';\nimport { WorkerMessageType, WorkerState } from '../../worker/types';\nimport { MP4Demuxer } from './MP4Demuxer';\nimport type { DemuxConfig } from './types';\n\ninterface LoaderStreamMetadata {\n sessionId?: string;\n byteStart?: number;\n byteEnd?: number;\n}\n/**\n * VideoDemuxWorker - First stage for video processing\n * Extracts video tracks from container formats (MP4, etc.)\n *\n * Pipeline: ResourceLoader (Main Thread) → VideoDemuxWorker → DecodeWorker\n *\n * Architecture Note:\n * - One VideoDemuxWorker instance per CLIP (not per resource)\n * - Multiple clips can share the same resource (different workers, independent processing)\n * - This enables clean 2-Clip strategy lifecycle management\n *\n * Features:\n * - MP4 container demuxing with mp4box.js\n * - Stream-based processing with backpressure\n * - Direct streaming to DecodeWorker\n */\nexport class VideoDemuxWorker {\n private channel: WorkerChannel;\n private demuxer: MP4Demuxer | null = null;\n private sessionId: string | null = null;\n private downstreamPort: MessagePort | null = null;\n\n constructor() {\n // Initialize WorkerChannel\n this.channel = new WorkerChannel(self as any, {\n name: 'VideoDemuxWorker',\n timeout: 30000,\n });\n this.setupHandlers();\n }\n\n /* @better-ai.mdc For test visibility */\n protected setupHandlers(): void {\n // Register message handlers\n this.channel.registerHandler('configure', this.handleConfigure.bind(this));\n this.channel.registerHandler('connect', this.handleConnect.bind(this));\n this.channel.registerHandler('get_stats', this.handleGetStats.bind(this));\n this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));\n\n // Setup stream receiver from ResourceLoader (main thread)\n this.channel.receiveStream(this.handleReceiveStream.bind(this));\n }\n\n /**\n * Handle connection from orchestrator\n */\n private async handleConnect(payload: {\n port?: MessagePort;\n streamType?: string;\n sessionId?: string;\n direction?: string;\n }): Promise<{ success: boolean }> {\n const { port, sessionId, direction } = payload;\n\n if (!port) {\n return { success: false };\n }\n\n this.downstreamPort = port;\n this.sessionId = sessionId || null;\n return { success: true };\n }\n\n /**\n * Configure demuxer with format settings\n * @param payload.config - Demuxer configuration\n * @param payload.initial - If true, initialize worker state; otherwise just update config\n */\n private async handleConfigure(payload: {\n config: DemuxConfig;\n initial?: boolean;\n }): Promise<{ success: boolean; tracks?: any[] }> {\n const { config, initial = false } = payload;\n\n try {\n if (initial) {\n // Initial setup - set worker state to ready\n this.channel.state = WorkerState.Ready;\n\n // Create new demuxer instance\n if (this.demuxer) {\n this.demuxer.destroy();\n }\n\n this.demuxer = new MP4Demuxer({\n ...config,\n skipAudio: true, // Video only\n onReady: () => this.handleDemuxerReady(),\n });\n\n // Notify configuration complete\n this.channel.notify('configured');\n\n return { success: true };\n } else {\n // Update configuration only (e.g., backpressure settings)\n this.demuxer?.updateConfig(config);\n return { success: true };\n }\n } catch (error: any) {\n throw {\n code: error.code || 'CONFIG_ERROR',\n message: error.message,\n };\n }\n }\n\n /**\n * Handle input stream from ResourceLoader (main thread)\n * Strategy: Stream immediately, send codec info when ready\n */\n private async handleReceiveStream(\n stream: ReadableStream<Uint8Array | ArrayBuffer>,\n metadata?: LoaderStreamMetadata\n ): Promise<void> {\n // Store sessionId from metadata (only happens once per worker lifecycle)\n // Note: Do NOT override if already set by handleConnect (which has correct sessionId)\n if (!this.sessionId) {\n this.sessionId = metadata?.sessionId || this.sessionId;\n }\n\n // Initialize demuxer on first stream\n if (!this.demuxer) {\n this.demuxer = new MP4Demuxer({\n highWaterMark: 10,\n skipAudio: true,\n onReady: () => this.handleDemuxerReady(),\n });\n }\n\n if (!this.downstreamPort) {\n
|
|
1
|
+
{"version":3,"file":"video-demux.worker.js","sources":["../../../../src/stages/demux/video-demux.worker.ts"],"sourcesContent":["import { WorkerChannel } from '../../worker/WorkerChannel';\nimport { WorkerMessageType, WorkerState } from '../../worker/types';\nimport { MP4Demuxer } from './MP4Demuxer';\nimport type { DemuxConfig } from './types';\n\ninterface LoaderStreamMetadata {\n sessionId?: string;\n byteStart?: number;\n byteEnd?: number;\n}\n/**\n * VideoDemuxWorker - First stage for video processing\n * Extracts video tracks from container formats (MP4, etc.)\n *\n * Pipeline: ResourceLoader (Main Thread) → VideoDemuxWorker → DecodeWorker\n *\n * Architecture Note:\n * - One VideoDemuxWorker instance per CLIP (not per resource)\n * - Multiple clips can share the same resource (different workers, independent processing)\n * - This enables clean 2-Clip strategy lifecycle management\n *\n * Features:\n * - MP4 container demuxing with mp4box.js\n * - Stream-based processing with backpressure\n * - Direct streaming to DecodeWorker\n */\nexport class VideoDemuxWorker {\n private channel: WorkerChannel;\n private demuxer: MP4Demuxer | null = null;\n private sessionId: string | null = null;\n private downstreamPort: MessagePort | null = null;\n\n constructor() {\n // Initialize WorkerChannel\n this.channel = new WorkerChannel(self as any, {\n name: 'VideoDemuxWorker',\n timeout: 30000,\n });\n this.setupHandlers();\n }\n\n /* @better-ai.mdc For test visibility */\n protected setupHandlers(): void {\n // Register message handlers\n this.channel.registerHandler('configure', this.handleConfigure.bind(this));\n this.channel.registerHandler('connect', this.handleConnect.bind(this));\n this.channel.registerHandler('get_stats', this.handleGetStats.bind(this));\n this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));\n\n // Setup stream receiver from ResourceLoader (main thread)\n this.channel.receiveStream(this.handleReceiveStream.bind(this));\n }\n\n /**\n * Handle connection from orchestrator\n */\n private async handleConnect(payload: {\n port?: MessagePort;\n streamType?: string;\n sessionId?: string;\n direction?: string;\n }): Promise<{ success: boolean }> {\n const { port, sessionId, direction } = payload;\n\n if (!port) {\n return { success: false };\n }\n\n this.downstreamPort = port;\n this.sessionId = sessionId || null;\n return { success: true };\n }\n\n /**\n * Configure demuxer with format settings\n * @param payload.config - Demuxer configuration\n * @param payload.initial - If true, initialize worker state; otherwise just update config\n */\n private async handleConfigure(payload: {\n config: DemuxConfig;\n initial?: boolean;\n }): Promise<{ success: boolean; tracks?: any[] }> {\n const { config, initial = false } = payload;\n\n try {\n if (initial) {\n // Initial setup - set worker state to ready\n this.channel.state = WorkerState.Ready;\n\n // Create new demuxer instance\n if (this.demuxer) {\n this.demuxer.destroy();\n }\n\n this.demuxer = new MP4Demuxer({\n ...config,\n skipAudio: true, // Video only\n onReady: () => this.handleDemuxerReady(),\n });\n\n // Notify configuration complete\n this.channel.notify('configured');\n\n return { success: true };\n } else {\n // Update configuration only (e.g., backpressure settings)\n this.demuxer?.updateConfig(config);\n return { success: true };\n }\n } catch (error: any) {\n throw {\n code: error.code || 'CONFIG_ERROR',\n message: error.message,\n };\n }\n }\n\n /**\n * Handle input stream from ResourceLoader (main thread)\n * Strategy: Stream immediately, send codec info when ready\n */\n private async handleReceiveStream(\n stream: ReadableStream<Uint8Array | ArrayBuffer>,\n metadata?: LoaderStreamMetadata\n ): Promise<void> {\n // Store sessionId from metadata (only happens once per worker lifecycle)\n // Note: Do NOT override if already set by handleConnect (which has correct sessionId)\n if (!this.sessionId) {\n this.sessionId = metadata?.sessionId || this.sessionId;\n }\n\n // Initialize demuxer on first stream\n if (!this.demuxer) {\n this.demuxer = new MP4Demuxer({\n highWaterMark: 10,\n skipAudio: true,\n onReady: () => this.handleDemuxerReady(),\n });\n }\n\n if (!this.downstreamPort) {\n console.error('[VideoDemuxWorker] Decoder not connected');\n return;\n }\n\n // Create and send stream to decoder\n const videoStream = this.demuxer.createVideoStream();\n const downstreamChannel = new WorkerChannel(this.downstreamPort, {\n name: 'VideoDemux-Decoder',\n timeout: 30000,\n });\n\n downstreamChannel.sendStream(videoStream.readable, {\n streamType: 'video',\n sessionId: this.sessionId,\n });\n\n await stream.pipeTo(videoStream.writable).catch((error) => {\n console.error('[VideoDemuxWorker] Stream error:', this.sessionId, error);\n });\n }\n\n private handleDemuxerReady(): void {\n if (!this.demuxer || !this.downstreamPort) {\n return;\n }\n\n const videoTrackInfo = this.demuxer.videoTrackInfo;\n if (!videoTrackInfo) {\n console.error('[VideoDemuxWorker] No video track found after ready');\n return;\n }\n\n const downstreamChannel = new WorkerChannel(this.downstreamPort, {\n name: 'VideoDemux-Decoder',\n timeout: 30000,\n });\n\n downstreamChannel.send('configure' as any, {\n sessionId: this.sessionId,\n streamType: 'video',\n codec: videoTrackInfo.codec,\n width: videoTrackInfo.width,\n height: videoTrackInfo.height,\n description: videoTrackInfo.description,\n });\n }\n\n /**\n * Get demuxer statistics\n */\n private async handleGetStats(): Promise<{\n queueSize?: number;\n tracksInfo?: any[];\n state?: WorkerState;\n }> {\n if (!this.demuxer) {\n return { state: this.channel.state };\n }\n\n return {\n tracksInfo: Array.from(this.demuxer.tracks.values()),\n state: this.channel.state,\n };\n }\n\n /**\n * Dispose worker and cleanup resources\n */\n private async handleDispose(): Promise<{ success: boolean }> {\n // Destroy demuxer\n this.demuxer?.destroy();\n this.demuxer = null;\n this.sessionId = null;\n\n // Close connections\n this.downstreamPort?.close();\n this.downstreamPort = null;\n\n this.channel.state = WorkerState.Disposed;\n\n return { success: true };\n }\n}\n// Initialize worker\nconst worker = new VideoDemuxWorker();\n\n// Handle worker termination\nself.addEventListener('beforeunload', () => {\n worker['handleDispose']();\n});\n\nexport default null; // Required for TypeScript worker compilation\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { W as WorkerChannel, a as WorkerMessageType, b as WorkerState } from "../../WorkerChannel.js";
|
|
2
|
+
class BaseEncoder {
|
|
3
|
+
encoder;
|
|
4
|
+
config;
|
|
5
|
+
controller = null;
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
getConfig() {
|
|
10
|
+
return { ...this.config };
|
|
11
|
+
}
|
|
12
|
+
get currentConfig() {
|
|
13
|
+
return this.config;
|
|
14
|
+
}
|
|
15
|
+
shouldReconfigure(partial) {
|
|
16
|
+
const next = { ...this.config, ...partial };
|
|
17
|
+
const keys = Object.keys(partial ?? {});
|
|
18
|
+
for (const key of keys) {
|
|
19
|
+
if (partial[key] !== void 0 && next[key] !== this.config[key]) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
hasConfigChanged(next) {
|
|
26
|
+
const currentEntries = Object.entries(this.config);
|
|
27
|
+
for (const [key, value] of currentEntries) {
|
|
28
|
+
if (next[key] !== value) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const key of Object.keys(next)) {
|
|
33
|
+
if (this.config[key] !== next[key]) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
configsEqual(a, b) {
|
|
40
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
41
|
+
}
|
|
42
|
+
async initialize() {
|
|
43
|
+
if (this.encoder?.state === "configured") {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const isSupported = await this.isConfigSupported(this.config);
|
|
47
|
+
if (!isSupported.supported) {
|
|
48
|
+
throw new Error(`Codec not supported: ${JSON.stringify(this.config)}`);
|
|
49
|
+
}
|
|
50
|
+
this.encoder = this.createEncoder({
|
|
51
|
+
output: this.handleOutput.bind(this),
|
|
52
|
+
error: this.handleError.bind(this)
|
|
53
|
+
});
|
|
54
|
+
this.encoder.configure(this.config);
|
|
55
|
+
}
|
|
56
|
+
async reconfigure(config) {
|
|
57
|
+
if (!config || Object.keys(config).length === 0) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const nextConfig = { ...this.config, ...config };
|
|
61
|
+
if (this.configsEqual(this.config, nextConfig)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!this.encoder) {
|
|
65
|
+
this.config = nextConfig;
|
|
66
|
+
await this.initialize();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (this.encoder.state === "configured") {
|
|
70
|
+
await this.encoder.flush();
|
|
71
|
+
}
|
|
72
|
+
const isSupported = await this.isConfigSupported(nextConfig);
|
|
73
|
+
if (!isSupported.supported) {
|
|
74
|
+
throw new Error(`New configuration not supported: ${nextConfig.codec}`);
|
|
75
|
+
}
|
|
76
|
+
this.config = nextConfig;
|
|
77
|
+
this.encoder.configure(this.config);
|
|
78
|
+
}
|
|
79
|
+
async flush() {
|
|
80
|
+
if (!this.encoder) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
await this.encoder.flush();
|
|
84
|
+
}
|
|
85
|
+
async reset() {
|
|
86
|
+
if (!this.encoder) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.encoder.reset();
|
|
90
|
+
this.onReset();
|
|
91
|
+
}
|
|
92
|
+
async close() {
|
|
93
|
+
if (!this.encoder) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (this.encoder.state === "configured") {
|
|
97
|
+
await this.encoder.flush();
|
|
98
|
+
}
|
|
99
|
+
this.encoder.close();
|
|
100
|
+
this.encoder = void 0;
|
|
101
|
+
}
|
|
102
|
+
get isReady() {
|
|
103
|
+
return this.encoder?.state === "configured";
|
|
104
|
+
}
|
|
105
|
+
get queueSize() {
|
|
106
|
+
return this.encoder?.encodeQueueSize ?? 0;
|
|
107
|
+
}
|
|
108
|
+
handleOutput(chunk, metadata) {
|
|
109
|
+
if (this.controller) {
|
|
110
|
+
try {
|
|
111
|
+
this.controller.enqueue({ chunk, metadata });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (!(error instanceof TypeError && error.message.includes("closed"))) {
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
handleError(error) {
|
|
120
|
+
console.error(`${this.getEncoderType()} encoder error:`, error);
|
|
121
|
+
this.controller?.error(error);
|
|
122
|
+
}
|
|
123
|
+
// Hook for subclasses to handle reset
|
|
124
|
+
onReset() {
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Create transform stream for encoding
|
|
128
|
+
* Implements common stream logic with backpressure handling
|
|
129
|
+
*/
|
|
130
|
+
createStream() {
|
|
131
|
+
return new TransformStream(
|
|
132
|
+
{
|
|
133
|
+
start: async (controller) => {
|
|
134
|
+
this.controller = controller;
|
|
135
|
+
if (!this.isReady) {
|
|
136
|
+
await this.initialize();
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
transform: async (input) => {
|
|
140
|
+
if (!this.encoder || this.encoder.state !== "configured") {
|
|
141
|
+
throw new Error("Encoder not configured");
|
|
142
|
+
}
|
|
143
|
+
if (this.encoder.encodeQueueSize >= this.encodeQueueThreshold) {
|
|
144
|
+
await new Promise((resolve) => {
|
|
145
|
+
const check = () => {
|
|
146
|
+
if (!this.encoder || this.encoder.encodeQueueSize < this.encodeQueueThreshold - 1) {
|
|
147
|
+
resolve();
|
|
148
|
+
} else {
|
|
149
|
+
setTimeout(check, 10);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
check();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const frame = input.frame || input;
|
|
156
|
+
this.encode(frame);
|
|
157
|
+
},
|
|
158
|
+
flush: async () => {
|
|
159
|
+
await this.flush();
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
// Queuing strategy with backpressure configuration
|
|
163
|
+
{
|
|
164
|
+
highWaterMark: this.highWaterMark,
|
|
165
|
+
size: () => 1
|
|
166
|
+
// Count-based
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
class VideoChunkEncoder extends BaseEncoder {
|
|
172
|
+
static DEFAULT_HIGH_WATER_MARK = 2;
|
|
173
|
+
static DEFAULT_ENCODE_QUEUE_THRESHOLD = 8;
|
|
174
|
+
highWaterMark;
|
|
175
|
+
encodeQueueThreshold;
|
|
176
|
+
frameCount = 0;
|
|
177
|
+
keyFrameInterval = 60;
|
|
178
|
+
// 2 seconds at 30fps
|
|
179
|
+
constructor(config) {
|
|
180
|
+
super(config);
|
|
181
|
+
this.highWaterMark = config.backpressure?.highWaterMark ?? VideoChunkEncoder.DEFAULT_HIGH_WATER_MARK;
|
|
182
|
+
this.encodeQueueThreshold = config.backpressure?.encodeQueueThreshold ?? VideoChunkEncoder.DEFAULT_ENCODE_QUEUE_THRESHOLD;
|
|
183
|
+
}
|
|
184
|
+
async isConfigSupported(config) {
|
|
185
|
+
const result = await VideoEncoder.isConfigSupported(config);
|
|
186
|
+
return { supported: result.supported ?? false };
|
|
187
|
+
}
|
|
188
|
+
createEncoder(init) {
|
|
189
|
+
return new VideoEncoder(init);
|
|
190
|
+
}
|
|
191
|
+
getEncoderType() {
|
|
192
|
+
return "Video";
|
|
193
|
+
}
|
|
194
|
+
onReset() {
|
|
195
|
+
this.frameCount = 0;
|
|
196
|
+
}
|
|
197
|
+
encode(frame) {
|
|
198
|
+
const keyFrame = this.shouldGenerateKeyFrame();
|
|
199
|
+
const encodeOptions = {
|
|
200
|
+
keyFrame
|
|
201
|
+
};
|
|
202
|
+
this.encoder.encode(frame, encodeOptions);
|
|
203
|
+
this.frameCount++;
|
|
204
|
+
frame.close();
|
|
205
|
+
}
|
|
206
|
+
setKeyFrameInterval(interval) {
|
|
207
|
+
this.keyFrameInterval = Math.max(1, interval);
|
|
208
|
+
}
|
|
209
|
+
shouldGenerateKeyFrame() {
|
|
210
|
+
if (this.frameCount === 0) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
return this.frameCount % this.keyFrameInterval === 0;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
class VideoEncodeWorker {
|
|
217
|
+
channel;
|
|
218
|
+
encoder = null;
|
|
219
|
+
clipId = "";
|
|
220
|
+
upstreamPort = null;
|
|
221
|
+
constructor() {
|
|
222
|
+
this.channel = new WorkerChannel(self, {
|
|
223
|
+
name: "VideoEncodeWorker",
|
|
224
|
+
timeout: 3e4
|
|
225
|
+
});
|
|
226
|
+
this.setupHandlers();
|
|
227
|
+
}
|
|
228
|
+
setupHandlers() {
|
|
229
|
+
this.channel.registerHandler("configure", this.handleConfigure.bind(this));
|
|
230
|
+
this.channel.registerHandler("connect", this.handleConnect.bind(this));
|
|
231
|
+
this.channel.registerHandler("flush", this.handleFlush.bind(this));
|
|
232
|
+
this.channel.registerHandler("reset", this.handleReset.bind(this));
|
|
233
|
+
this.channel.registerHandler("get_stats", this.handleGetStats.bind(this));
|
|
234
|
+
this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));
|
|
235
|
+
}
|
|
236
|
+
async handleConnect(payload) {
|
|
237
|
+
const { port, streamType, sessionId, direction } = payload;
|
|
238
|
+
if (direction === "upstream" && streamType === "video") {
|
|
239
|
+
return this.handleConnectComposer({ port, sessionId });
|
|
240
|
+
}
|
|
241
|
+
return { success: true };
|
|
242
|
+
}
|
|
243
|
+
async handleConfigure(payload) {
|
|
244
|
+
const { config, initial = false } = payload;
|
|
245
|
+
if (initial) {
|
|
246
|
+
this.channel.state = WorkerState.Ready;
|
|
247
|
+
this.encoder = new VideoChunkEncoder(config);
|
|
248
|
+
this.encoder.initialize();
|
|
249
|
+
}
|
|
250
|
+
return { success: true };
|
|
251
|
+
}
|
|
252
|
+
async handleConnectComposer(payload) {
|
|
253
|
+
const { port, sessionId } = payload;
|
|
254
|
+
this.upstreamPort = port;
|
|
255
|
+
this.clipId = sessionId || "default";
|
|
256
|
+
const composeChannel = new WorkerChannel(port, {
|
|
257
|
+
name: "Encode-VideoCompose",
|
|
258
|
+
timeout: 3e4
|
|
259
|
+
});
|
|
260
|
+
composeChannel.receiveStream(async (stream, metadata) => {
|
|
261
|
+
const streamSessionId = metadata?.sessionId || sessionId || "unknown";
|
|
262
|
+
if (metadata?.streamType === "video") {
|
|
263
|
+
if (!this.encoder) {
|
|
264
|
+
console.error("[VideoEncoderWorker] error: no encoder");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const encodingTransform = this.encoder.createStream();
|
|
268
|
+
const encodedStream = stream.pipeThrough(
|
|
269
|
+
encodingTransform
|
|
270
|
+
);
|
|
271
|
+
this.channel.sendStream(encodedStream, {
|
|
272
|
+
streamType: "video",
|
|
273
|
+
track: "video",
|
|
274
|
+
sessionId: streamSessionId
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
return { success: true };
|
|
279
|
+
}
|
|
280
|
+
async handleFlush() {
|
|
281
|
+
if (this.encoder) {
|
|
282
|
+
await this.encoder.flush();
|
|
283
|
+
}
|
|
284
|
+
return { success: true };
|
|
285
|
+
}
|
|
286
|
+
async handleReset() {
|
|
287
|
+
if (this.encoder) {
|
|
288
|
+
await this.encoder.reset();
|
|
289
|
+
}
|
|
290
|
+
this.channel.notify("reset_complete", {
|
|
291
|
+
type: "video"
|
|
292
|
+
});
|
|
293
|
+
return { success: true };
|
|
294
|
+
}
|
|
295
|
+
async handleGetStats() {
|
|
296
|
+
if (!this.encoder) {
|
|
297
|
+
return {};
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
video: {
|
|
301
|
+
clipId: this.clipId,
|
|
302
|
+
configured: true
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
async handleDispose() {
|
|
307
|
+
if (this.encoder) {
|
|
308
|
+
await this.encoder.close();
|
|
309
|
+
this.encoder = null;
|
|
310
|
+
}
|
|
311
|
+
this.upstreamPort?.close();
|
|
312
|
+
this.upstreamPort = null;
|
|
313
|
+
this.channel.state = WorkerState.Disposed;
|
|
314
|
+
return { success: true };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const worker = new VideoEncodeWorker();
|
|
318
|
+
self.addEventListener("beforeunload", () => {
|
|
319
|
+
worker["handleDispose"]();
|
|
320
|
+
});
|
|
321
|
+
const videoEncode_worker = null;
|
|
322
|
+
export {
|
|
323
|
+
videoEncode_worker as default
|
|
324
|
+
};
|
|
325
|
+
//# sourceMappingURL=video-encode.worker.js.map
|