@meframe/core 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +17 -4
  2. package/dist/Meframe.d.ts.map +1 -1
  3. package/dist/Meframe.js +0 -3
  4. package/dist/Meframe.js.map +1 -1
  5. package/dist/assets/audio-compose.worker-nGVvHD5Q.js +1537 -0
  6. package/dist/assets/audio-compose.worker-nGVvHD5Q.js.map +1 -0
  7. package/dist/assets/audio-demux.worker-xwWBtbAe.js +8299 -0
  8. package/dist/assets/audio-demux.worker-xwWBtbAe.js.map +1 -0
  9. package/dist/assets/decode.worker-DpWHsc7R.js +1291 -0
  10. package/dist/assets/decode.worker-DpWHsc7R.js.map +1 -0
  11. package/dist/assets/encode.worker-nfOb3kw6.js +1026 -0
  12. package/dist/assets/encode.worker-nfOb3kw6.js.map +1 -0
  13. package/dist/assets/mux.worker-uEMQY066.js +8019 -0
  14. package/dist/assets/mux.worker-uEMQY066.js.map +1 -0
  15. package/dist/assets/video-compose.worker-DPzsC21d.js +1683 -0
  16. package/dist/assets/video-compose.worker-DPzsC21d.js.map +1 -0
  17. package/dist/assets/video-demux.worker-D019I7GQ.js +7957 -0
  18. package/dist/assets/video-demux.worker-D019I7GQ.js.map +1 -0
  19. package/dist/cache/CacheManager.d.ts.map +1 -1
  20. package/dist/cache/CacheManager.js +8 -1
  21. package/dist/cache/CacheManager.js.map +1 -1
  22. package/dist/config/defaults.d.ts.map +1 -1
  23. package/dist/config/defaults.js +0 -8
  24. package/dist/config/defaults.js.map +1 -1
  25. package/dist/config/types.d.ts +0 -4
  26. package/dist/config/types.d.ts.map +1 -1
  27. package/dist/controllers/PlaybackController.d.ts +4 -2
  28. package/dist/controllers/PlaybackController.d.ts.map +1 -1
  29. package/dist/controllers/PlaybackController.js +7 -13
  30. package/dist/controllers/PlaybackController.js.map +1 -1
  31. package/dist/controllers/PreRenderService.d.ts +3 -2
  32. package/dist/controllers/PreRenderService.d.ts.map +1 -1
  33. package/dist/controllers/PreRenderService.js.map +1 -1
  34. package/dist/controllers/PreviewHandle.d.ts +2 -0
  35. package/dist/controllers/PreviewHandle.d.ts.map +1 -1
  36. package/dist/controllers/PreviewHandle.js +6 -0
  37. package/dist/controllers/PreviewHandle.js.map +1 -1
  38. package/dist/controllers/index.d.ts +1 -1
  39. package/dist/controllers/index.d.ts.map +1 -1
  40. package/dist/controllers/types.d.ts +2 -12
  41. package/dist/controllers/types.d.ts.map +1 -1
  42. package/dist/event/events.d.ts +5 -59
  43. package/dist/event/events.d.ts.map +1 -1
  44. package/dist/event/events.js +1 -6
  45. package/dist/event/events.js.map +1 -1
  46. package/dist/model/CompositionModel.js +1 -2
  47. package/dist/model/CompositionModel.js.map +1 -1
  48. package/dist/orchestrator/CompositionPlanner.d.ts.map +1 -1
  49. package/dist/orchestrator/CompositionPlanner.js +1 -0
  50. package/dist/orchestrator/CompositionPlanner.js.map +1 -1
  51. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  52. package/dist/orchestrator/Orchestrator.js +1 -12
  53. package/dist/orchestrator/Orchestrator.js.map +1 -1
  54. package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
  55. package/dist/orchestrator/VideoClipSession.js +4 -5
  56. package/dist/orchestrator/VideoClipSession.js.map +1 -1
  57. package/dist/orchestrator/types.d.ts +0 -1
  58. package/dist/orchestrator/types.d.ts.map +1 -1
  59. package/dist/stages/compose/GlobalAudioSession.d.ts.map +1 -1
  60. package/dist/stages/compose/GlobalAudioSession.js +3 -2
  61. package/dist/stages/compose/GlobalAudioSession.js.map +1 -1
  62. package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
  63. package/dist/stages/compose/VideoComposer.js +2 -2
  64. package/dist/stages/compose/VideoComposer.js.map +1 -1
  65. package/dist/stages/compose/audio-compose.worker.d.ts.map +1 -1
  66. package/dist/stages/compose/audio-compose.worker.js +0 -1
  67. package/dist/stages/compose/audio-compose.worker.js.map +1 -1
  68. package/dist/stages/compose/audio-compose.worker2.js +5 -0
  69. package/dist/stages/compose/audio-compose.worker2.js.map +1 -0
  70. package/dist/stages/compose/types.d.ts +1 -0
  71. package/dist/stages/compose/types.d.ts.map +1 -1
  72. package/dist/stages/compose/video-compose.worker.d.ts.map +1 -1
  73. package/dist/stages/compose/video-compose.worker.js +18 -8
  74. package/dist/stages/compose/video-compose.worker.js.map +1 -1
  75. package/dist/stages/compose/video-compose.worker2.js +5 -0
  76. package/dist/stages/compose/video-compose.worker2.js.map +1 -0
  77. package/dist/stages/decode/AudioChunkDecoder.d.ts.map +1 -1
  78. package/dist/stages/decode/AudioChunkDecoder.js +0 -1
  79. package/dist/stages/decode/AudioChunkDecoder.js.map +1 -1
  80. package/dist/stages/decode/VideoChunkDecoder.d.ts +0 -1
  81. package/dist/stages/decode/VideoChunkDecoder.d.ts.map +1 -1
  82. package/dist/stages/decode/VideoChunkDecoder.js +1 -11
  83. package/dist/stages/decode/VideoChunkDecoder.js.map +1 -1
  84. package/dist/stages/decode/decode.worker.d.ts.map +1 -1
  85. package/dist/stages/decode/decode.worker.js +3 -16
  86. package/dist/stages/decode/decode.worker.js.map +1 -1
  87. package/dist/stages/decode/decode.worker2.js +5 -0
  88. package/dist/stages/decode/decode.worker2.js.map +1 -0
  89. package/dist/stages/demux/MP4Demuxer.d.ts +2 -0
  90. package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
  91. package/dist/stages/demux/MP4Demuxer.js +13 -2
  92. package/dist/stages/demux/MP4Demuxer.js.map +1 -1
  93. package/dist/stages/demux/audio-demux.worker2.js +5 -0
  94. package/dist/stages/demux/audio-demux.worker2.js.map +1 -0
  95. package/dist/stages/demux/video-demux.worker.d.ts +6 -3
  96. package/dist/stages/demux/video-demux.worker.d.ts.map +1 -1
  97. package/dist/stages/demux/video-demux.worker.js +5 -27
  98. package/dist/stages/demux/video-demux.worker.js.map +1 -1
  99. package/dist/stages/demux/video-demux.worker2.js +5 -0
  100. package/dist/stages/demux/video-demux.worker2.js.map +1 -0
  101. package/dist/stages/encode/encode.worker.d.ts.map +1 -1
  102. package/dist/stages/encode/encode.worker.js +0 -1
  103. package/dist/stages/encode/encode.worker.js.map +1 -1
  104. package/dist/stages/encode/encode.worker2.js +5 -0
  105. package/dist/stages/encode/encode.worker2.js.map +1 -0
  106. package/dist/stages/load/EventHandlers.d.ts +2 -11
  107. package/dist/stages/load/EventHandlers.d.ts.map +1 -1
  108. package/dist/stages/load/EventHandlers.js +1 -24
  109. package/dist/stages/load/EventHandlers.js.map +1 -1
  110. package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
  111. package/dist/stages/load/ResourceLoader.js +11 -13
  112. package/dist/stages/load/ResourceLoader.js.map +1 -1
  113. package/dist/stages/load/TaskManager.d.ts +1 -1
  114. package/dist/stages/load/TaskManager.d.ts.map +1 -1
  115. package/dist/stages/load/TaskManager.js +3 -2
  116. package/dist/stages/load/TaskManager.js.map +1 -1
  117. package/dist/stages/load/types.d.ts +2 -0
  118. package/dist/stages/load/types.d.ts.map +1 -1
  119. package/dist/stages/mux/mux.worker2.js +5 -0
  120. package/dist/stages/mux/mux.worker2.js.map +1 -0
  121. package/dist/vite-plugin.d.ts +17 -0
  122. package/dist/vite-plugin.d.ts.map +1 -0
  123. package/dist/vite-plugin.js +88 -0
  124. package/dist/vite-plugin.js.map +1 -0
  125. package/dist/worker/WorkerPool.d.ts +0 -4
  126. package/dist/worker/WorkerPool.d.ts.map +1 -1
  127. package/dist/worker/WorkerPool.js +4 -17
  128. package/dist/worker/WorkerPool.js.map +1 -1
  129. package/dist/worker/worker-registry.d.ts +12 -0
  130. package/dist/worker/worker-registry.d.ts.map +1 -0
  131. package/dist/worker/worker-registry.js +20 -0
  132. package/dist/worker/worker-registry.js.map +1 -0
  133. package/package.json +7 -1
@@ -76,17 +76,7 @@ class DecodeWorker {
76
76
  * @param payload.initial - If true, initialize worker and recreate decoder instances; otherwise just update config
77
77
  */
78
78
  async handleConfigure(payload) {
79
- const {
80
- clipId,
81
- streamType,
82
- codec,
83
- width,
84
- height,
85
- sampleRate,
86
- numberOfChannels,
87
- description,
88
- range
89
- } = payload;
79
+ const { clipId, streamType, codec, width, height, sampleRate, numberOfChannels, description } = payload;
90
80
  if (clipId && streamType) {
91
81
  try {
92
82
  if (streamType === "video") {
@@ -96,8 +86,7 @@ class DecodeWorker {
96
86
  codec,
97
87
  width,
98
88
  height,
99
- description: normalizeDescription(description),
100
- ...range && { range }
89
+ description: normalizeDescription(description)
101
90
  });
102
91
  }
103
92
  } else if (streamType === "audio") {
@@ -146,7 +135,6 @@ class DecodeWorker {
146
135
  async handleReceiveStream(stream, metadata) {
147
136
  const clipId = metadata?.clipId || "default";
148
137
  const streamType = metadata?.streamType;
149
- console.log("[DecodeWorker] handleReceiveStream", streamType, clipId, metadata);
150
138
  if (streamType === "video") {
151
139
  const decoder = await this.getOrCreateDecoder("video", clipId, metadata);
152
140
  const transform = decoder.createStream();
@@ -298,8 +286,7 @@ class DecodeWorker {
298
286
  codec: metadata.codec,
299
287
  width: metadata.width,
300
288
  height: metadata.height,
301
- description: normalizeDescription(metadata.description),
302
- ...metadata.range && { range: metadata.range }
289
+ description: normalizeDescription(metadata.description)
303
290
  } : void 0
304
291
  );
305
292
  this.evictIfNeeded("video");
@@ -1 +1 @@
1
- {"version":3,"file":"decode.worker.js","sources":["../../../src/stages/decode/decode.worker.ts"],"sourcesContent":["import { WorkerChannel } from '../../worker/WorkerChannel';\nimport { WorkerMessageType, WorkerState } from '../../worker/types';\nimport { VideoChunkDecoder } from './VideoChunkDecoder';\nimport { AudioChunkDecoder } from './AudioChunkDecoder';\nimport { VideoDecoderConfig, AudioDecoderConfig } from './types';\nimport type { AudioTrackConfig } from '../compose/types';\n\ninterface TrackMetadata {\n clipId: string;\n config: AudioTrackConfig;\n sampleRate?: number;\n numberOfChannels?: number;\n type: 'bgm' | 'voice' | 'sfx' | 'other';\n}\n\nconst normalizeDescription = (desc?: ArrayBuffer | ArrayBufferView): ArrayBuffer | undefined => {\n if (!desc) return undefined;\n\n if (desc instanceof ArrayBuffer) return desc;\n\n // Handle ArrayBufferView (Uint8Array, etc.)\n const view = desc as ArrayBufferView;\n return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength) as ArrayBuffer;\n};\n\n/**\n * DecodeWorker - Third stage in the pipeline\n * Receives encoded chunks from DemuxWorkers and outputs decoded frames to ComposeWorker\n *\n * Pipeline: VideoDemuxWorker/AudioDemuxWorker → DecodeWorker → ComposeWorker\n *\n * Features:\n * - GOP-based caching for video (≤4 GOPs in memory)\n * - Stream-based processing with backpressure\n * - Direct streaming to ComposeWorker\n */\nexport class DecodeWorker {\n private channel: WorkerChannel;\n // Map of clipId -> decoder instance\n private videoDecoders = new Map<string, VideoChunkDecoder>();\n private audioDecoders = new Map<string, AudioChunkDecoder>();\n private registeredAudioTracks = new Set<string>();\n private deliveredAudioTracks = new Set<string>();\n private audioTrackMetadata = new Map<string, TrackMetadata>();\n\n /** Maximum number of active decoder pairs allowed at the same time */\n private static readonly MAX_ACTIVE_DECODERS = 8;\n\n // Cached default configs merged from orchestrator\n private defaultVideoConfig: Partial<VideoDecoderConfig> = {};\n private defaultAudioConfig: Partial<AudioDecoderConfig> = {};\n\n // Connections to other workers\n private composePorts = new Map<string, MessagePort>();\n private audioDownstreamPort: MessagePort | null = null;\n private demuxPorts = new Map<string, MessagePort>(); // Connections from demux workers\n\n constructor() {\n // Initialize WorkerChannel with MessagePort\n this.channel = new WorkerChannel(self as any, {\n name: 'DecodeWorker',\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 this.channel.registerHandler('connect' as any, this.handleConnect.bind(this));\n this.channel.registerHandler('flush', this.handleFlush.bind(this));\n this.channel.registerHandler('reset', this.handleReset.bind(this));\n this.channel.registerHandler('get_stats', this.handleGetStats.bind(this));\n this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));\n }\n\n /**\n * Connect handler used by stream pipeline\n */\n private async handleConnect(payload: {\n direction: 'upstream' | 'downstream';\n port: MessagePort;\n streamType: 'video' | 'audio';\n clipId?: string;\n clipStartUs?: number;\n clipDurationUs?: number;\n }): Promise<{ success: boolean }> {\n const { port, direction, clipId } = payload;\n if (direction === 'upstream') {\n this.demuxPorts.set(clipId || 'default', port);\n const channel = new WorkerChannel(port, {\n name: 'Demux-Decode',\n timeout: 30000,\n });\n channel.receiveStream((stream, metadata) => {\n this.handleReceiveStream(stream, {\n ...metadata,\n clipStartUs: payload.clipStartUs,\n clipDurationUs: payload.clipDurationUs,\n });\n });\n // Also register configure handler on upstream channel for codec info from demuxer\n channel.registerHandler('configure' as any, this.handleConfigure.bind(this));\n }\n if (direction === 'downstream') {\n if (payload.streamType === 'audio') {\n this.audioDownstreamPort?.close();\n this.audioDownstreamPort = port;\n } else {\n this.composePorts.set(clipId || 'default', port);\n }\n }\n return { success: true };\n }\n\n /**\n * Handle configuration message from orchestrator\n * @param payload.initial - If true, initialize worker and recreate decoder instances; otherwise just update config\n */\n private async handleConfigure(payload: {\n config?: { video?: Partial<VideoDecoderConfig>; audio?: Partial<AudioDecoderConfig> };\n // Support direct codec info from demuxer\n clipId?: string;\n streamType?: 'video' | 'audio';\n codec?: string;\n width?: number;\n height?: number;\n sampleRate?: number;\n numberOfChannels?: number;\n description?: ArrayBuffer | Uint8Array;\n range?: { start: number; end: number };\n }): Promise<{ success: boolean }> {\n // If this is codec info from demuxer (has clipId and streamType)\n const {\n clipId,\n streamType,\n codec,\n width,\n height,\n sampleRate,\n numberOfChannels,\n description,\n range,\n } = payload;\n if (clipId && streamType) {\n try {\n if (streamType === 'video') {\n const decoder = this.videoDecoders.get(clipId);\n if (decoder) {\n await decoder.updateConfig({\n codec,\n width,\n height,\n description: normalizeDescription(description),\n ...(range && { range }),\n });\n }\n } else if (streamType === 'audio') {\n const decoder = this.audioDecoders.get(clipId);\n if (decoder) {\n await decoder.updateConfig({\n codec,\n sampleRate,\n numberOfChannels,\n description: normalizeDescription(description),\n });\n }\n }\n } catch (error: any) {\n console.error('[DecodeWorker] Failed to configure decoder:', error);\n throw {\n code: 'CODEC_CONFIG_ERROR',\n message: error.message,\n };\n }\n return { success: true };\n }\n\n // Otherwise, this is a global config from orchestrator\n const { config } = payload;\n if (!config) {\n return { success: true };\n }\n\n // Ensure worker becomes ready once configured at least once\n this.channel.state = WorkerState.Ready;\n\n // Merge and cache default configs\n if (config.video) {\n Object.assign(this.defaultVideoConfig, config.video);\n }\n if (config.audio) {\n Object.assign(this.defaultAudioConfig, config.audio);\n }\n\n // Propagate updated config to existing decoders\n if (config.video) {\n for (const dec of this.videoDecoders.values()) {\n await dec.updateConfig(config.video);\n }\n }\n if (config.audio) {\n for (const dec of this.audioDecoders.values()) {\n await dec.updateConfig(config.audio);\n }\n }\n\n return { success: true };\n }\n\n private async handleReceiveStream(\n stream: ReadableStream,\n metadata?: Record<string, any>\n ): Promise<void> {\n const clipId: string = metadata?.clipId || 'default';\n const streamType = metadata?.streamType;\n\n console.log('[DecodeWorker] handleReceiveStream', streamType, clipId, metadata);\n\n if (streamType === 'video') {\n // For video, create decoder that will buffer chunks until configured\n const decoder = await this.getOrCreateDecoder('video', clipId, metadata);\n const transform = decoder.createStream();\n\n // Forward decoded frames downstream if port exists\n const composePort = this.composePorts.get(clipId);\n if (composePort) {\n const channel = new WorkerChannel(composePort, {\n name: 'Decode-Compose',\n timeout: 30000,\n });\n channel.sendStream(transform.readable as ReadableStream, {\n streamType: 'video',\n clipId,\n });\n\n // Pipe encoded chunks into decoder (will buffer if not configured)\n stream\n .pipeTo(transform.writable)\n .catch((error) =>\n console.error('[DecodeWorker] Video stream pipe error:', clipId, error)\n );\n }\n } else if (streamType === 'audio') {\n const decoder = await this.getOrCreateDecoder('audio', clipId, metadata);\n const transform = decoder.createStream();\n stream.pipeTo(transform.writable).catch((error) => {\n console.error('[DecodeWorker] Audio stream pipe error:', error);\n });\n\n const trackId = metadata?.trackId ?? clipId;\n await this.registerAudioTrack(trackId, clipId, metadata);\n\n this.channel.sendStream(transform.readable as ReadableStream<AudioData>, {\n streamType: 'audio',\n clipId,\n trackId,\n clipStartUs: metadata?.clipStartUs ?? 0,\n clipDurationUs: metadata?.clipDurationUs ?? 0,\n });\n\n this.deliveredAudioTracks.add(trackId);\n }\n }\n\n /**\n * Flush decoders\n */\n private async handleFlush(payload?: { type?: 'video' | 'audio' }): Promise<{ success: boolean }> {\n try {\n if (!payload?.type || payload.type === 'video') {\n for (const dec of this.videoDecoders.values()) {\n await dec.flush();\n }\n }\n if (!payload?.type || payload.type === 'audio') {\n for (const dec of this.audioDecoders.values()) {\n await dec.flush();\n }\n }\n\n return { success: true };\n } catch (error: any) {\n throw {\n code: 'FLUSH_ERROR',\n message: error.message,\n };\n }\n }\n\n /**\n * Reset decoders\n */\n private async handleReset(payload?: { type?: 'video' | 'audio' }): Promise<{ success: boolean }> {\n try {\n if (!payload?.type || payload.type === 'video') {\n for (const dec of this.videoDecoders.values()) {\n await dec.reset();\n }\n }\n if (!payload?.type || payload.type === 'audio') {\n for (const dec of this.audioDecoders.values()) {\n await dec.reset();\n }\n }\n\n // Notify reset complete\n this.channel.notify('reset_complete', {\n type: payload?.type || 'all',\n });\n\n return { success: true };\n } catch (error: any) {\n throw {\n code: 'RESET_ERROR',\n message: error.message,\n };\n }\n }\n\n /**\n * Get decoder statistics\n */\n private async handleGetStats(): Promise<{\n video?: any;\n audio?: any;\n }> {\n const stats: any = {};\n\n if (this.videoDecoders.size) {\n stats.video = Array.from(this.videoDecoders.entries()).map(([clipId, dec]) => ({\n clipId,\n configured: dec.isConfigured,\n queueSize: dec.queueSize,\n state: dec.state,\n }));\n }\n\n if (this.audioDecoders.size) {\n stats.audio = Array.from(this.audioDecoders.entries()).map(([clipId, dec]) => ({\n clipId,\n configured: dec.isConfigured,\n queueSize: dec.queueSize,\n state: dec.state,\n }));\n }\n\n return stats;\n }\n\n /**\n * Dispose worker and cleanup resources\n */\n private async handleDispose(): Promise<{ success: boolean }> {\n // Close all decoders\n for (const dec of this.videoDecoders.values()) {\n await dec.close();\n }\n for (const dec of this.audioDecoders.values()) {\n await dec.close();\n }\n\n this.videoDecoders.clear();\n this.audioDecoders.clear();\n\n // Close connections\n for (const port of this.composePorts.values()) {\n port.close();\n }\n this.composePorts.clear();\n\n if (this.audioDownstreamPort) {\n this.audioDownstreamPort.close();\n this.audioDownstreamPort = null;\n }\n\n this.registeredAudioTracks.clear();\n this.deliveredAudioTracks.clear();\n this.audioTrackMetadata.clear();\n\n for (const port of this.demuxPorts.values()) {\n port.close();\n }\n this.demuxPorts.clear();\n\n this.channel.state = WorkerState.Disposed;\n\n return { success: true };\n }\n\n /**\n * Get existing decoder for clip or create a new one (with LRU eviction)\n */\n private async getOrCreateDecoder(\n kind: 'video' | 'audio',\n clipId: string,\n metadata?: Record<string, any> | null\n ): Promise<VideoChunkDecoder | AudioChunkDecoder> {\n if (kind === 'video') {\n let decoder = this.videoDecoders.get(clipId);\n if (!decoder) {\n // Create decoder without configuration if metadata is null\n decoder = new VideoChunkDecoder(\n clipId,\n metadata\n ? {\n ...this.defaultVideoConfig,\n codec: metadata.codec,\n width: metadata.width,\n height: metadata.height,\n description: normalizeDescription(metadata.description),\n ...(metadata.range && { range: metadata.range }),\n }\n : undefined\n ); // Pass undefined to create unconfigured decoder\n\n this.evictIfNeeded('video');\n this.videoDecoders.set(clipId, decoder);\n }\n // refresh LRU order\n this.videoDecoders.delete(clipId);\n this.videoDecoders.set(clipId, decoder);\n return decoder;\n } else {\n let decoder = this.audioDecoders.get(clipId);\n if (!decoder) {\n decoder = new AudioChunkDecoder(\n clipId,\n metadata\n ? {\n ...this.defaultAudioConfig,\n codec: metadata.codec,\n sampleRate: metadata.sampleRate,\n numberOfChannels: metadata.numberOfChannels,\n description: normalizeDescription(metadata.description),\n }\n : undefined\n );\n\n this.evictIfNeeded('audio');\n this.audioDecoders.set(clipId, decoder);\n }\n // refresh LRU order\n this.audioDecoders.delete(clipId);\n this.audioDecoders.set(clipId, decoder);\n return decoder;\n }\n }\n\n /**\n * Evict least-recently-used decoder if we exceed MAX_ACTIVE_DECODERS.\n */\n private evictIfNeeded(kind: 'video' | 'audio'): void {\n const map = kind === 'video' ? this.videoDecoders : this.audioDecoders;\n if (map.size < DecodeWorker.MAX_ACTIVE_DECODERS) return;\n\n // Map preserves insertion order; first entry is LRU\n const [lrucId, lruDecoder] = map.entries().next().value as [string, any];\n lruDecoder.close().catch(() => undefined);\n map.delete(lrucId);\n }\n\n private async registerAudioTrack(\n trackId: string,\n clipId: string,\n metadata?: Record<string, any>\n ): Promise<void> {\n const record: TrackMetadata = {\n clipId,\n config: this.extractTrackConfig(metadata?.runtimeConfig),\n sampleRate: metadata?.sampleRate,\n numberOfChannels: metadata?.numberOfChannels,\n type: (metadata?.trackType as TrackMetadata['type']) ?? 'other',\n };\n\n this.audioTrackMetadata.set(trackId, record);\n\n if (this.registeredAudioTracks.has(trackId)) {\n await this.sendAudioTrackUpdate(trackId, record);\n return;\n }\n\n this.registeredAudioTracks.add(trackId);\n await this.sendAudioTrackAdd(trackId, record);\n }\n\n // private unregisterAudioTrack(trackId: string): void {\n // if (!this.registeredAudioTracks.delete(trackId)) {\n // return;\n // }\n\n // const record = this.audioTrackMetadata.get(trackId);\n // this.audioTrackMetadata.delete(trackId);\n\n // if (!record) {\n // return;\n // }\n\n // const channel = this.ensureComposeChannel();\n // channel\n // ?.send(WorkerMessageType.AudioTrackRemove, {\n // clipId: record.clipId,\n // trackId,\n // })\n // .catch((error) => {\n // console.warn('[DecodeWorker] Failed to notify track removal', error);\n // });\n // }\n\n private extractTrackConfig(config: any): AudioTrackConfig {\n return {\n startTimeUs: config?.startTimeUs ?? 0,\n durationUs: config?.durationUs,\n volume: config?.volume ?? 1,\n fadeIn: config?.fadeIn,\n fadeOut: config?.fadeOut,\n effects: config?.effects ?? [],\n duckingTag: config?.duckingTag,\n };\n }\n\n private async sendAudioTrackAdd(trackId: string, record: TrackMetadata): Promise<void> {\n const channel = this.ensureComposeChannel();\n if (!channel) {\n return;\n }\n\n await channel.send(WorkerMessageType.AudioTrackAdd, {\n clipId: record.clipId,\n trackId,\n config: record.config,\n sampleRate: record.sampleRate,\n numberOfChannels: record.numberOfChannels,\n type: record.type,\n });\n }\n\n private async sendAudioTrackUpdate(trackId: string, record: TrackMetadata): Promise<void> {\n const channel = this.ensureComposeChannel();\n if (!channel) {\n return;\n }\n\n if (!channel) {\n return;\n }\n\n await channel.send(WorkerMessageType.AudioTrackUpdate, {\n clipId: record.clipId,\n trackId,\n config: record.config,\n type: record.type,\n });\n }\n\n private ensureComposeChannel(): WorkerChannel | null {\n if (!this.audioDownstreamPort) {\n return null;\n }\n\n return new WorkerChannel(this.audioDownstreamPort, {\n name: 'Decode-AudioCompose',\n timeout: 30000,\n });\n }\n}\n\n// Initialize worker\nconst worker = new DecodeWorker();\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":"decode.worker.js","sources":["../../../src/stages/decode/decode.worker.ts"],"sourcesContent":["import { WorkerChannel } from '../../worker/WorkerChannel';\nimport { WorkerMessageType, WorkerState } from '../../worker/types';\nimport { VideoChunkDecoder } from './VideoChunkDecoder';\nimport { AudioChunkDecoder } from './AudioChunkDecoder';\nimport { VideoDecoderConfig, AudioDecoderConfig } from './types';\nimport type { AudioTrackConfig } from '../compose/types';\n\ninterface TrackMetadata {\n clipId: string;\n config: AudioTrackConfig;\n sampleRate?: number;\n numberOfChannels?: number;\n type: 'bgm' | 'voice' | 'sfx' | 'other';\n}\n\nconst normalizeDescription = (desc?: ArrayBuffer | ArrayBufferView): ArrayBuffer | undefined => {\n if (!desc) return undefined;\n\n if (desc instanceof ArrayBuffer) return desc;\n\n // Handle ArrayBufferView (Uint8Array, etc.)\n const view = desc as ArrayBufferView;\n return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength) as ArrayBuffer;\n};\n\n/**\n * DecodeWorker - Third stage in the pipeline\n * Receives encoded chunks from DemuxWorkers and outputs decoded frames to ComposeWorker\n *\n * Pipeline: VideoDemuxWorker/AudioDemuxWorker → DecodeWorker → ComposeWorker\n *\n * Features:\n * - GOP-based caching for video (≤4 GOPs in memory)\n * - Stream-based processing with backpressure\n * - Direct streaming to ComposeWorker\n */\nexport class DecodeWorker {\n private channel: WorkerChannel;\n // Map of clipId -> decoder instance\n private videoDecoders = new Map<string, VideoChunkDecoder>();\n private audioDecoders = new Map<string, AudioChunkDecoder>();\n private registeredAudioTracks = new Set<string>();\n private deliveredAudioTracks = new Set<string>();\n private audioTrackMetadata = new Map<string, TrackMetadata>();\n\n /** Maximum number of active decoder pairs allowed at the same time */\n private static readonly MAX_ACTIVE_DECODERS = 8;\n\n // Cached default configs merged from orchestrator\n private defaultVideoConfig: Partial<VideoDecoderConfig> = {};\n private defaultAudioConfig: Partial<AudioDecoderConfig> = {};\n\n // Connections to other workers\n private composePorts = new Map<string, MessagePort>();\n private audioDownstreamPort: MessagePort | null = null;\n private demuxPorts = new Map<string, MessagePort>(); // Connections from demux workers\n\n constructor() {\n // Initialize WorkerChannel with MessagePort\n this.channel = new WorkerChannel(self as any, {\n name: 'DecodeWorker',\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 this.channel.registerHandler('connect' as any, this.handleConnect.bind(this));\n this.channel.registerHandler('flush', this.handleFlush.bind(this));\n this.channel.registerHandler('reset', this.handleReset.bind(this));\n this.channel.registerHandler('get_stats', this.handleGetStats.bind(this));\n this.channel.registerHandler(WorkerMessageType.Dispose, this.handleDispose.bind(this));\n }\n\n /**\n * Connect handler used by stream pipeline\n */\n private async handleConnect(payload: {\n direction: 'upstream' | 'downstream';\n port: MessagePort;\n streamType: 'video' | 'audio';\n clipId?: string;\n clipStartUs?: number;\n clipDurationUs?: number;\n }): Promise<{ success: boolean }> {\n const { port, direction, clipId } = payload;\n if (direction === 'upstream') {\n this.demuxPorts.set(clipId || 'default', port);\n const channel = new WorkerChannel(port, {\n name: 'Demux-Decode',\n timeout: 30000,\n });\n channel.receiveStream((stream, metadata) => {\n this.handleReceiveStream(stream, {\n ...metadata,\n clipStartUs: payload.clipStartUs,\n clipDurationUs: payload.clipDurationUs,\n });\n });\n // Also register configure handler on upstream channel for codec info from demuxer\n channel.registerHandler('configure' as any, this.handleConfigure.bind(this));\n }\n if (direction === 'downstream') {\n if (payload.streamType === 'audio') {\n this.audioDownstreamPort?.close();\n this.audioDownstreamPort = port;\n } else {\n this.composePorts.set(clipId || 'default', port);\n }\n }\n return { success: true };\n }\n\n /**\n * Handle configuration message from orchestrator\n * @param payload.initial - If true, initialize worker and recreate decoder instances; otherwise just update config\n */\n private async handleConfigure(payload: {\n config?: { video?: Partial<VideoDecoderConfig>; audio?: Partial<AudioDecoderConfig> };\n // Support direct codec info from demuxer\n clipId?: string;\n streamType?: 'video' | 'audio';\n codec?: string;\n width?: number;\n height?: number;\n sampleRate?: number;\n numberOfChannels?: number;\n description?: ArrayBuffer | Uint8Array;\n }): Promise<{ success: boolean }> {\n // If this is codec info from demuxer (has clipId and streamType)\n const { clipId, streamType, codec, width, height, sampleRate, numberOfChannels, description } =\n payload;\n if (clipId && streamType) {\n try {\n if (streamType === 'video') {\n const decoder = this.videoDecoders.get(clipId);\n if (decoder) {\n await decoder.updateConfig({\n codec,\n width,\n height,\n description: normalizeDescription(description),\n });\n }\n } else if (streamType === 'audio') {\n const decoder = this.audioDecoders.get(clipId);\n if (decoder) {\n await decoder.updateConfig({\n codec,\n sampleRate,\n numberOfChannels,\n description: normalizeDescription(description),\n });\n }\n }\n } catch (error: any) {\n console.error('[DecodeWorker] Failed to configure decoder:', error);\n throw {\n code: 'CODEC_CONFIG_ERROR',\n message: error.message,\n };\n }\n return { success: true };\n }\n\n // Otherwise, this is a global config from orchestrator\n const { config } = payload;\n if (!config) {\n return { success: true };\n }\n\n // Ensure worker becomes ready once configured at least once\n this.channel.state = WorkerState.Ready;\n\n // Merge and cache default configs\n if (config.video) {\n Object.assign(this.defaultVideoConfig, config.video);\n }\n if (config.audio) {\n Object.assign(this.defaultAudioConfig, config.audio);\n }\n\n // Propagate updated config to existing decoders\n if (config.video) {\n for (const dec of this.videoDecoders.values()) {\n await dec.updateConfig(config.video);\n }\n }\n if (config.audio) {\n for (const dec of this.audioDecoders.values()) {\n await dec.updateConfig(config.audio);\n }\n }\n\n return { success: true };\n }\n\n private async handleReceiveStream(\n stream: ReadableStream,\n metadata?: Record<string, any>\n ): Promise<void> {\n const clipId: string = metadata?.clipId || 'default';\n const streamType = metadata?.streamType;\n\n if (streamType === 'video') {\n // For video, create decoder that will buffer chunks until configured\n const decoder = await this.getOrCreateDecoder('video', clipId, metadata);\n const transform = decoder.createStream();\n\n // Forward decoded frames downstream if port exists\n const composePort = this.composePorts.get(clipId);\n if (composePort) {\n const channel = new WorkerChannel(composePort, {\n name: 'Decode-Compose',\n timeout: 30000,\n });\n channel.sendStream(transform.readable as ReadableStream, {\n streamType: 'video',\n clipId,\n });\n\n // Pipe encoded chunks into decoder (will buffer if not configured)\n stream\n .pipeTo(transform.writable)\n .catch((error) =>\n console.error('[DecodeWorker] Video stream pipe error:', clipId, error)\n );\n }\n } else if (streamType === 'audio') {\n const decoder = await this.getOrCreateDecoder('audio', clipId, metadata);\n const transform = decoder.createStream();\n stream.pipeTo(transform.writable).catch((error) => {\n console.error('[DecodeWorker] Audio stream pipe error:', error);\n });\n\n const trackId = metadata?.trackId ?? clipId;\n await this.registerAudioTrack(trackId, clipId, metadata);\n\n this.channel.sendStream(transform.readable as ReadableStream<AudioData>, {\n streamType: 'audio',\n clipId,\n trackId,\n clipStartUs: metadata?.clipStartUs ?? 0,\n clipDurationUs: metadata?.clipDurationUs ?? 0,\n });\n\n this.deliveredAudioTracks.add(trackId);\n }\n }\n\n /**\n * Flush decoders\n */\n private async handleFlush(payload?: { type?: 'video' | 'audio' }): Promise<{ success: boolean }> {\n try {\n if (!payload?.type || payload.type === 'video') {\n for (const dec of this.videoDecoders.values()) {\n await dec.flush();\n }\n }\n if (!payload?.type || payload.type === 'audio') {\n for (const dec of this.audioDecoders.values()) {\n await dec.flush();\n }\n }\n\n return { success: true };\n } catch (error: any) {\n throw {\n code: 'FLUSH_ERROR',\n message: error.message,\n };\n }\n }\n\n /**\n * Reset decoders\n */\n private async handleReset(payload?: { type?: 'video' | 'audio' }): Promise<{ success: boolean }> {\n try {\n if (!payload?.type || payload.type === 'video') {\n for (const dec of this.videoDecoders.values()) {\n await dec.reset();\n }\n }\n if (!payload?.type || payload.type === 'audio') {\n for (const dec of this.audioDecoders.values()) {\n await dec.reset();\n }\n }\n\n // Notify reset complete\n this.channel.notify('reset_complete', {\n type: payload?.type || 'all',\n });\n\n return { success: true };\n } catch (error: any) {\n throw {\n code: 'RESET_ERROR',\n message: error.message,\n };\n }\n }\n\n /**\n * Get decoder statistics\n */\n private async handleGetStats(): Promise<{\n video?: any;\n audio?: any;\n }> {\n const stats: any = {};\n\n if (this.videoDecoders.size) {\n stats.video = Array.from(this.videoDecoders.entries()).map(([clipId, dec]) => ({\n clipId,\n configured: dec.isConfigured,\n queueSize: dec.queueSize,\n state: dec.state,\n }));\n }\n\n if (this.audioDecoders.size) {\n stats.audio = Array.from(this.audioDecoders.entries()).map(([clipId, dec]) => ({\n clipId,\n configured: dec.isConfigured,\n queueSize: dec.queueSize,\n state: dec.state,\n }));\n }\n\n return stats;\n }\n\n /**\n * Dispose worker and cleanup resources\n */\n private async handleDispose(): Promise<{ success: boolean }> {\n // Close all decoders\n for (const dec of this.videoDecoders.values()) {\n await dec.close();\n }\n for (const dec of this.audioDecoders.values()) {\n await dec.close();\n }\n\n this.videoDecoders.clear();\n this.audioDecoders.clear();\n\n // Close connections\n for (const port of this.composePorts.values()) {\n port.close();\n }\n this.composePorts.clear();\n\n if (this.audioDownstreamPort) {\n this.audioDownstreamPort.close();\n this.audioDownstreamPort = null;\n }\n\n this.registeredAudioTracks.clear();\n this.deliveredAudioTracks.clear();\n this.audioTrackMetadata.clear();\n\n for (const port of this.demuxPorts.values()) {\n port.close();\n }\n this.demuxPorts.clear();\n\n this.channel.state = WorkerState.Disposed;\n\n return { success: true };\n }\n\n /**\n * Get existing decoder for clip or create a new one (with LRU eviction)\n */\n private async getOrCreateDecoder(\n kind: 'video' | 'audio',\n clipId: string,\n metadata?: Record<string, any> | null\n ): Promise<VideoChunkDecoder | AudioChunkDecoder> {\n if (kind === 'video') {\n let decoder = this.videoDecoders.get(clipId);\n if (!decoder) {\n // Create decoder without configuration if metadata is null\n decoder = new VideoChunkDecoder(\n clipId,\n metadata\n ? {\n ...this.defaultVideoConfig,\n codec: metadata.codec,\n width: metadata.width,\n height: metadata.height,\n description: normalizeDescription(metadata.description),\n }\n : undefined\n ); // Pass undefined to create unconfigured decoder\n\n this.evictIfNeeded('video');\n this.videoDecoders.set(clipId, decoder);\n }\n // refresh LRU order\n this.videoDecoders.delete(clipId);\n this.videoDecoders.set(clipId, decoder);\n return decoder;\n } else {\n let decoder = this.audioDecoders.get(clipId);\n if (!decoder) {\n decoder = new AudioChunkDecoder(\n clipId,\n metadata\n ? {\n ...this.defaultAudioConfig,\n codec: metadata.codec,\n sampleRate: metadata.sampleRate,\n numberOfChannels: metadata.numberOfChannels,\n description: normalizeDescription(metadata.description),\n }\n : undefined\n );\n\n this.evictIfNeeded('audio');\n this.audioDecoders.set(clipId, decoder);\n }\n // refresh LRU order\n this.audioDecoders.delete(clipId);\n this.audioDecoders.set(clipId, decoder);\n return decoder;\n }\n }\n\n /**\n * Evict least-recently-used decoder if we exceed MAX_ACTIVE_DECODERS.\n */\n private evictIfNeeded(kind: 'video' | 'audio'): void {\n const map = kind === 'video' ? this.videoDecoders : this.audioDecoders;\n if (map.size < DecodeWorker.MAX_ACTIVE_DECODERS) return;\n\n // Map preserves insertion order; first entry is LRU\n const [lrucId, lruDecoder] = map.entries().next().value as [string, any];\n lruDecoder.close().catch(() => undefined);\n map.delete(lrucId);\n }\n\n private async registerAudioTrack(\n trackId: string,\n clipId: string,\n metadata?: Record<string, any>\n ): Promise<void> {\n const record: TrackMetadata = {\n clipId,\n config: this.extractTrackConfig(metadata?.runtimeConfig),\n sampleRate: metadata?.sampleRate,\n numberOfChannels: metadata?.numberOfChannels,\n type: (metadata?.trackType as TrackMetadata['type']) ?? 'other',\n };\n\n this.audioTrackMetadata.set(trackId, record);\n\n if (this.registeredAudioTracks.has(trackId)) {\n await this.sendAudioTrackUpdate(trackId, record);\n return;\n }\n\n this.registeredAudioTracks.add(trackId);\n await this.sendAudioTrackAdd(trackId, record);\n }\n\n // private unregisterAudioTrack(trackId: string): void {\n // if (!this.registeredAudioTracks.delete(trackId)) {\n // return;\n // }\n\n // const record = this.audioTrackMetadata.get(trackId);\n // this.audioTrackMetadata.delete(trackId);\n\n // if (!record) {\n // return;\n // }\n\n // const channel = this.ensureComposeChannel();\n // channel\n // ?.send(WorkerMessageType.AudioTrackRemove, {\n // clipId: record.clipId,\n // trackId,\n // })\n // .catch((error) => {\n // console.warn('[DecodeWorker] Failed to notify track removal', error);\n // });\n // }\n\n private extractTrackConfig(config: any): AudioTrackConfig {\n return {\n startTimeUs: config?.startTimeUs ?? 0,\n durationUs: config?.durationUs,\n volume: config?.volume ?? 1,\n fadeIn: config?.fadeIn,\n fadeOut: config?.fadeOut,\n effects: config?.effects ?? [],\n duckingTag: config?.duckingTag,\n };\n }\n\n private async sendAudioTrackAdd(trackId: string, record: TrackMetadata): Promise<void> {\n const channel = this.ensureComposeChannel();\n if (!channel) {\n return;\n }\n\n await channel.send(WorkerMessageType.AudioTrackAdd, {\n clipId: record.clipId,\n trackId,\n config: record.config,\n sampleRate: record.sampleRate,\n numberOfChannels: record.numberOfChannels,\n type: record.type,\n });\n }\n\n private async sendAudioTrackUpdate(trackId: string, record: TrackMetadata): Promise<void> {\n const channel = this.ensureComposeChannel();\n if (!channel) {\n return;\n }\n\n if (!channel) {\n return;\n }\n\n await channel.send(WorkerMessageType.AudioTrackUpdate, {\n clipId: record.clipId,\n trackId,\n config: record.config,\n type: record.type,\n });\n }\n\n private ensureComposeChannel(): WorkerChannel | null {\n if (!this.audioDownstreamPort) {\n return null;\n }\n\n return new WorkerChannel(this.audioDownstreamPort, {\n name: 'Decode-AudioCompose',\n timeout: 30000,\n });\n }\n}\n\n// Initialize worker\nconst worker = new DecodeWorker();\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,5 @@
1
+ const decodeWorkerUrl = "" + new URL("../../assets/decode.worker-DpWHsc7R.js", import.meta.url).href;
2
+ export {
3
+ decodeWorkerUrl as default
4
+ };
5
+ //# sourceMappingURL=decode.worker2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode.worker2.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -14,6 +14,8 @@ export declare class MP4Demuxer {
14
14
  private demuxHighWaterMark;
15
15
  private onReadyCallback?;
16
16
  private fileOffset;
17
+ private videoTimestampOffset;
18
+ private audioTimestampOffset;
17
19
  constructor(config?: DemuxConfig & {
18
20
  onReady?: () => void;
19
21
  });
@@ -1 +1 @@
1
- {"version":3,"file":"MP4Demuxer.d.ts","sourceRoot":"","sources":["../../../src/stages/demux/MP4Demuxer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAGtD;;;GAGG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAM;IACxB,MAAM,yBAAgC;IACtC,OAAO,UAAS;IAChB,OAAO,CAAC,eAAe,CAAC,CAAsD;IAC9E,OAAO,CAAC,eAAe,CAAC,CAAsD;IAC9E,OAAO,CAAC,mBAAmB,CAAsB;IACjD,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,eAAe,CAAC,CAAa;IACrC,OAAO,CAAC,UAAU,CAAK;gBAEX,MAAM,GAAE,WAAW,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;KAAO;IAY/D,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAIvC,OAAO,CAAC,aAAa;IA2BrB,OAAO,CAAC,aAAa;IA4BrB,OAAO,CAAC,cAAc;IAuCtB,OAAO,CAAC,mBAAmB;IA0B3B,OAAO,CAAC,mBAAmB;IAgB3B;;OAEG;IACH,iBAAiB,IAAI,eAAe,CAAC,UAAU,EAAE,iBAAiB,CAAC;IAuCnE;;OAEG;IACH,iBAAiB,IAAI,eAAe,CAAC,UAAU,EAAE,iBAAiB,CAAC,GAAG,IAAI;IAkC1E,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAOrC;;OAEG;IACH,IAAI,cAAc,IAAI,SAAS,GAAG,SAAS,CAE1C;IAED;;OAEG;IACH,IAAI,cAAc,IAAI,SAAS,GAAG,SAAS,CAE1C;IAED,OAAO,IAAI,IAAI;CAOhB"}
1
+ {"version":3,"file":"MP4Demuxer.d.ts","sourceRoot":"","sources":["../../../src/stages/demux/MP4Demuxer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAGtD;;;GAGG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAM;IACxB,MAAM,yBAAgC;IACtC,OAAO,UAAS;IAChB,OAAO,CAAC,eAAe,CAAC,CAAsD;IAC9E,OAAO,CAAC,eAAe,CAAC,CAAsD;IAC9E,OAAO,CAAC,mBAAmB,CAAsB;IACjD,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,eAAe,CAAC,CAAa;IACrC,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,oBAAoB,CAAuB;IACnD,OAAO,CAAC,oBAAoB,CAAuB;gBAEvC,MAAM,GAAE,WAAW,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;KAAO;IAY/D,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAIvC,OAAO,CAAC,aAAa;IA2BrB,OAAO,CAAC,aAAa;IA4BrB,OAAO,CAAC,cAAc;IAmDtB,OAAO,CAAC,mBAAmB;IA0B3B,OAAO,CAAC,mBAAmB;IAgB3B;;OAEG;IACH,iBAAiB,IAAI,eAAe,CAAC,UAAU,EAAE,iBAAiB,CAAC;IAqCnE;;OAEG;IACH,iBAAiB,IAAI,eAAe,CAAC,UAAU,EAAE,iBAAiB,CAAC,GAAG,IAAI;IAkC1E,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAOrC;;OAEG;IACH,IAAI,cAAc,IAAI,SAAS,GAAG,SAAS,CAE1C;IAED;;OAEG;IACH,IAAI,cAAc,IAAI,SAAS,GAAG,SAAS,CAE1C;IAED,OAAO,IAAI,IAAI;CAShB"}
@@ -11,6 +11,8 @@ class MP4Demuxer {
11
11
  demuxHighWaterMark;
12
12
  onReadyCallback;
13
13
  fileOffset = 0;
14
+ videoTimestampOffset = null;
15
+ audioTimestampOffset = null;
14
16
  constructor(config = {}) {
15
17
  this.mp4boxFile = mp4box_all.createFile();
16
18
  this.backpressureMonitor = new BackpressureMonitor();
@@ -69,13 +71,17 @@ class MP4Demuxer {
69
71
  if (!track) return;
70
72
  const timescale = track.timescale || 9e4;
71
73
  for (const sample of samples) {
72
- const timestamp = sample.cts * 1e6 / timescale;
74
+ const rawTimestamp = sample.cts * 1e6 / timescale;
73
75
  const duration = sample.duration * 1e6 / timescale;
74
76
  if (track.type === "video") {
75
77
  if (!this.videoController) {
76
78
  console.error("[MP4Demuxer] videoController is null when trying to output chunk!");
77
79
  return;
78
80
  }
81
+ if (this.videoTimestampOffset === null) {
82
+ this.videoTimestampOffset = rawTimestamp;
83
+ }
84
+ const timestamp = rawTimestamp - this.videoTimestampOffset;
79
85
  const chunk = new EncodedVideoChunk({
80
86
  type: sample.is_sync ? "key" : "delta",
81
87
  timestamp,
@@ -84,6 +90,10 @@ class MP4Demuxer {
84
90
  });
85
91
  this.videoController.enqueue(chunk);
86
92
  } else if (track.type === "audio" && this.audioController) {
93
+ if (this.audioTimestampOffset === null) {
94
+ this.audioTimestampOffset = rawTimestamp;
95
+ }
96
+ const timestamp = rawTimestamp - this.audioTimestampOffset;
87
97
  const chunk = new EncodedAudioChunk({
88
98
  type: "key",
89
99
  timestamp,
@@ -155,7 +165,6 @@ class MP4Demuxer {
155
165
  flush: async () => {
156
166
  this.mp4boxFile.flush();
157
167
  await new Promise((resolve) => setTimeout(resolve, 100));
158
- console.log("[MP4Demuxer] Video stream flush complete");
159
168
  }
160
169
  },
161
170
  // Queuing strategy: use configuration
@@ -219,6 +228,8 @@ class MP4Demuxer {
219
228
  this.tracks.clear();
220
229
  this.backpressureMonitor.clear();
221
230
  this.isReady = false;
231
+ this.videoTimestampOffset = null;
232
+ this.audioTimestampOffset = null;
222
233
  }
223
234
  }
224
235
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"MP4Demuxer.js","sources":["../../../src/stages/demux/MP4Demuxer.ts"],"sourcesContent":["import * as MP4Box from 'mp4box';\nimport type { DemuxConfig, TrackInfo } from './types';\nimport { BackpressureMonitor } from '../../plugins/BackpressureMonitor';\n\n/**\n * MP4 Demuxer - Extract encoded chunks from MP4 container\n * Simplified implementation following Stream API pattern\n */\nexport class MP4Demuxer {\n private mp4boxFile: any;\n tracks = new Map<number, TrackInfo>();\n isReady = false;\n private videoController?: TransformStreamDefaultController<EncodedVideoChunk>;\n private audioController?: TransformStreamDefaultController<EncodedAudioChunk>;\n private backpressureMonitor: BackpressureMonitor;\n private demuxHighWaterMark: number;\n private onReadyCallback?: () => void;\n private fileOffset = 0;\n\n constructor(config: DemuxConfig & { onReady?: () => void } = {}) {\n this.mp4boxFile = MP4Box.createFile();\n this.backpressureMonitor = new BackpressureMonitor();\n this.onReadyCallback = config.onReady;\n\n // Use provided config with local default as fallback\n const DEFAULT_HIGH_WATER_MARK = 10; // Default for video chunks\n this.demuxHighWaterMark = config.highWaterMark ?? DEFAULT_HIGH_WATER_MARK;\n\n this.setupHandlers();\n }\n\n updateConfig(config: DemuxConfig): void {\n this.demuxHighWaterMark = config.highWaterMark ?? this.demuxHighWaterMark;\n }\n\n private setupHandlers(): void {\n this.mp4boxFile.onError = (error: string) => {\n console.error('MP4Box error:', error);\n this.videoController?.error(new Error(error));\n this.audioController?.error(new Error(error));\n };\n\n this.mp4boxFile.onReady = (info: any) => {\n this.processTracks(info.tracks);\n this.isReady = true;\n\n // Call the ready callback before starting\n if (this.onReadyCallback) {\n this.onReadyCallback();\n }\n\n // Start processing samples after callback\n // Note: start() enables extraction, actual samples will be output\n // when flush() is called (by TransformStream's flush callback)\n this.mp4boxFile.start();\n };\n\n this.mp4boxFile.onSamples = (trackId: number, _user: any, samples: any[]) => {\n this.processSamples(trackId, samples);\n };\n }\n\n private processTracks(tracks: any[]): void {\n for (const track of tracks) {\n const trackInfo: TrackInfo = {\n id: track.id,\n type: track.type === 'video' ? 'video' : 'audio',\n codec: track.codec,\n timescale: track.timescale,\n };\n\n if (track.type === 'video') {\n trackInfo.width = track.video?.width;\n trackInfo.height = track.video?.height;\n trackInfo.description = this.getVideoDescription(track);\n } else if (track.type === 'audio') {\n trackInfo.sampleRate = track.audio?.sample_rate;\n trackInfo.numberOfChannels = track.audio?.channel_count;\n trackInfo.description = this.getAudioDescription(track);\n }\n\n this.tracks.set(track.id, trackInfo);\n\n // Configure extraction\n this.mp4boxFile.setExtractionOptions(track.id, track, {\n nbSamples: 30, // Batch size per callback (balance between latency and overhead)\n });\n }\n }\n\n private processSamples(trackId: number, samples: any[]): void {\n const track = this.tracks.get(trackId);\n if (!track) return;\n\n const timescale = track.timescale || 90000;\n for (const sample of samples) {\n const timestamp = (sample.cts * 1000000) / timescale;\n const duration = (sample.duration * 1000000) / timescale;\n\n if (track.type === 'video') {\n if (!this.videoController) {\n console.error('[MP4Demuxer] videoController is null when trying to output chunk!');\n // Should not happen - stream should be created before appendBuffer\n return;\n }\n\n const chunk = new EncodedVideoChunk({\n type: sample.is_sync ? 'key' : 'delta',\n timestamp,\n duration,\n data: sample.data,\n });\n this.videoController.enqueue(chunk);\n } else if (track.type === 'audio' && this.audioController) {\n const chunk = new EncodedAudioChunk({\n type: 'key',\n timestamp,\n duration,\n data: sample.data,\n });\n this.audioController.enqueue(chunk);\n }\n }\n\n const last = samples[samples.length - 1].number;\n // Release memory immediately\n this.mp4boxFile.releaseUsedSamples(trackId, last + 1);\n }\n\n private getVideoDescription(track: any): ArrayBuffer | undefined {\n try {\n const fullTrack = this.mp4boxFile.getTrackById(track.id);\n for (const entry of fullTrack.mdia.minf.stbl.stsd.entries) {\n const box = entry.avcC ?? entry.hvcC ?? entry.av1C ?? entry.vpcC;\n if (box) {\n const stream = new (MP4Box as any).DataStream(\n undefined,\n 0,\n (MP4Box as any).DataStream.BIG_ENDIAN // IMPORTANT: must be BIG_ENDIAN\n );\n box.write(stream);\n return new Uint8Array(stream.buffer.slice(8)).buffer;\n }\n }\n } catch (error) {\n console.error('Failed to get video description:', error);\n }\n return undefined;\n }\n\n // private getVideoDescription(track: any): ArrayBuffer | undefined {\n // if (!this.mp4boxFile) return undefined;\n // return getVideoDescription(this.mp4boxFile, track);\n // }\n\n private getAudioDescription(track: any): ArrayBuffer | undefined {\n try {\n const fullTrack = this.mp4boxFile.getTrackById(track.id);\n for (const entry of fullTrack.mdia.minf.stbl.stsd.entries) {\n if (entry.esds || entry.dOps) {\n const stream = new (MP4Box as any).DataStream();\n (entry.esds || entry.dOps).write(stream);\n return new Uint8Array(stream.buffer.slice(8)).buffer;\n }\n }\n } catch (error) {\n console.error('Failed to get audio description:', error);\n }\n return undefined;\n }\n\n /**\n * Create transform stream for video track\n */\n createVideoStream(): TransformStream<Uint8Array, EncodedVideoChunk> {\n // const hasVideo = Array.from(this.tracks.values()).some((t) => t.type === 'video');\n return new TransformStream<Uint8Array, EncodedVideoChunk>(\n {\n start: (controller) => {\n this.videoController = controller;\n },\n transform: (chunk, controller) => {\n // Update backpressure metrics\n // desiredSize can be null, negative (backpressure), or positive\n const desiredSize = controller.desiredSize ?? this.demuxHighWaterMark;\n this.backpressureMonitor.updateMetrics('demux-video', desiredSize);\n\n // Create a copy to avoid buffer reuse issues (required for mp4box 0.5.x)\n const chunkData = new Uint8Array(chunk);\n this.appendBuffer(chunkData);\n\n // Flush after each append to trigger sample extraction immediately\n this.mp4boxFile.flush();\n },\n flush: async () => {\n // Trigger MP4Box flush\n this.mp4boxFile.flush();\n\n // Wait for MP4Box to complete sample extraction (onSamples callbacks)\n // Give MP4Box time to process asynchronously\n await new Promise((resolve) => setTimeout(resolve, 100));\n\n console.log('[MP4Demuxer] Video stream flush complete');\n },\n },\n // Queuing strategy: use configuration\n {\n highWaterMark: this.demuxHighWaterMark,\n size: () => 1, // Count-based\n }\n );\n }\n\n /**\n * Create transform stream for audio track\n */\n createAudioStream(): TransformStream<Uint8Array, EncodedAudioChunk> | null {\n const hasAudio = Array.from(this.tracks.values()).some((t) => t.type === 'audio');\n if (!hasAudio) return null;\n\n return new TransformStream<Uint8Array, EncodedAudioChunk>(\n {\n start: (controller) => {\n this.audioController = controller;\n },\n transform: (chunk, controller) => {\n // Update backpressure metrics\n // desiredSize can be null, negative (backpressure), or positive\n const desiredSize = controller.desiredSize ?? this.demuxHighWaterMark;\n this.backpressureMonitor.updateMetrics('demux-audio', desiredSize);\n\n // Create a copy to avoid buffer reuse issues (required for mp4box 0.5.x)\n const chunkData = new Uint8Array(chunk);\n this.appendBuffer(chunkData);\n\n // Flush after each append to trigger sample extraction immediately\n this.mp4boxFile.flush();\n },\n flush: () => {\n this.mp4boxFile.flush();\n },\n },\n // Queuing strategy: use configuration\n {\n highWaterMark: this.demuxHighWaterMark,\n size: () => 1,\n }\n );\n }\n\n appendBuffer(chunk: Uint8Array): void {\n const buffer = chunk.buffer as ArrayBuffer & { fileStart: number };\n buffer.fileStart = this.fileOffset;\n this.mp4boxFile.appendBuffer(buffer);\n this.fileOffset += chunk.byteLength;\n }\n\n /**\n * Get video track info if available\n */\n get videoTrackInfo(): TrackInfo | undefined {\n return Array.from(this.tracks.values()).find((track) => track.type === 'video');\n }\n\n /**\n * Get audio track info if available\n */\n get audioTrackInfo(): TrackInfo | undefined {\n return Array.from(this.tracks.values()).find((track) => track.type === 'audio');\n }\n\n destroy(): void {\n this.mp4boxFile?.stop();\n this.mp4boxFile = null;\n this.tracks.clear();\n this.backpressureMonitor.clear();\n this.isReady = false;\n }\n}\n"],"names":["MP4Box.createFile","MP4Box.DataStream"],"mappings":"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"}
1
+ {"version":3,"file":"MP4Demuxer.js","sources":["../../../src/stages/demux/MP4Demuxer.ts"],"sourcesContent":["import * as MP4Box from 'mp4box';\nimport type { DemuxConfig, TrackInfo } from './types';\nimport { BackpressureMonitor } from '../../plugins/BackpressureMonitor';\n\n/**\n * MP4 Demuxer - Extract encoded chunks from MP4 container\n * Simplified implementation following Stream API pattern\n */\nexport class MP4Demuxer {\n private mp4boxFile: any;\n tracks = new Map<number, TrackInfo>();\n isReady = false;\n private videoController?: TransformStreamDefaultController<EncodedVideoChunk>;\n private audioController?: TransformStreamDefaultController<EncodedAudioChunk>;\n private backpressureMonitor: BackpressureMonitor;\n private demuxHighWaterMark: number;\n private onReadyCallback?: () => void;\n private fileOffset = 0;\n private videoTimestampOffset: number | null = null;\n private audioTimestampOffset: number | null = null;\n\n constructor(config: DemuxConfig & { onReady?: () => void } = {}) {\n this.mp4boxFile = MP4Box.createFile();\n this.backpressureMonitor = new BackpressureMonitor();\n this.onReadyCallback = config.onReady;\n\n // Use provided config with local default as fallback\n const DEFAULT_HIGH_WATER_MARK = 10; // Default for video chunks\n this.demuxHighWaterMark = config.highWaterMark ?? DEFAULT_HIGH_WATER_MARK;\n\n this.setupHandlers();\n }\n\n updateConfig(config: DemuxConfig): void {\n this.demuxHighWaterMark = config.highWaterMark ?? this.demuxHighWaterMark;\n }\n\n private setupHandlers(): void {\n this.mp4boxFile.onError = (error: string) => {\n console.error('MP4Box error:', error);\n this.videoController?.error(new Error(error));\n this.audioController?.error(new Error(error));\n };\n\n this.mp4boxFile.onReady = (info: any) => {\n this.processTracks(info.tracks);\n this.isReady = true;\n\n // Call the ready callback before starting\n if (this.onReadyCallback) {\n this.onReadyCallback();\n }\n\n // Start processing samples after callback\n // Note: start() enables extraction, actual samples will be output\n // when flush() is called (by TransformStream's flush callback)\n this.mp4boxFile.start();\n };\n\n this.mp4boxFile.onSamples = (trackId: number, _user: any, samples: any[]) => {\n this.processSamples(trackId, samples);\n };\n }\n\n private processTracks(tracks: any[]): void {\n for (const track of tracks) {\n const trackInfo: TrackInfo = {\n id: track.id,\n type: track.type === 'video' ? 'video' : 'audio',\n codec: track.codec,\n timescale: track.timescale,\n };\n\n if (track.type === 'video') {\n trackInfo.width = track.video?.width;\n trackInfo.height = track.video?.height;\n trackInfo.description = this.getVideoDescription(track);\n } else if (track.type === 'audio') {\n trackInfo.sampleRate = track.audio?.sample_rate;\n trackInfo.numberOfChannels = track.audio?.channel_count;\n trackInfo.description = this.getAudioDescription(track);\n }\n\n this.tracks.set(track.id, trackInfo);\n\n // Configure extraction\n this.mp4boxFile.setExtractionOptions(track.id, track, {\n nbSamples: 30, // Batch size per callback (balance between latency and overhead)\n });\n }\n }\n\n private processSamples(trackId: number, samples: any[]): void {\n const track = this.tracks.get(trackId);\n if (!track) return;\n\n const timescale = track.timescale || 90000;\n for (const sample of samples) {\n const rawTimestamp = (sample.cts * 1000000) / timescale;\n const duration = (sample.duration * 1000000) / timescale;\n\n if (track.type === 'video') {\n if (!this.videoController) {\n console.error('[MP4Demuxer] videoController is null when trying to output chunk!');\n // Should not happen - stream should be created before appendBuffer\n return;\n }\n\n // Normalize timestamp: first frame starts at 0\n if (this.videoTimestampOffset === null) {\n this.videoTimestampOffset = rawTimestamp;\n }\n const timestamp = rawTimestamp - this.videoTimestampOffset;\n\n const chunk = new EncodedVideoChunk({\n type: sample.is_sync ? 'key' : 'delta',\n timestamp,\n duration,\n data: sample.data,\n });\n this.videoController.enqueue(chunk);\n } else if (track.type === 'audio' && this.audioController) {\n // Normalize timestamp: first frame starts at 0\n if (this.audioTimestampOffset === null) {\n this.audioTimestampOffset = rawTimestamp;\n }\n const timestamp = rawTimestamp - this.audioTimestampOffset;\n\n const chunk = new EncodedAudioChunk({\n type: 'key',\n timestamp,\n duration,\n data: sample.data,\n });\n this.audioController.enqueue(chunk);\n }\n }\n\n const last = samples[samples.length - 1].number;\n // Release memory immediately\n this.mp4boxFile.releaseUsedSamples(trackId, last + 1);\n }\n\n private getVideoDescription(track: any): ArrayBuffer | undefined {\n try {\n const fullTrack = this.mp4boxFile.getTrackById(track.id);\n for (const entry of fullTrack.mdia.minf.stbl.stsd.entries) {\n const box = entry.avcC ?? entry.hvcC ?? entry.av1C ?? entry.vpcC;\n if (box) {\n const stream = new (MP4Box as any).DataStream(\n undefined,\n 0,\n (MP4Box as any).DataStream.BIG_ENDIAN // IMPORTANT: must be BIG_ENDIAN\n );\n box.write(stream);\n return new Uint8Array(stream.buffer.slice(8)).buffer;\n }\n }\n } catch (error) {\n console.error('Failed to get video description:', error);\n }\n return undefined;\n }\n\n // private getVideoDescription(track: any): ArrayBuffer | undefined {\n // if (!this.mp4boxFile) return undefined;\n // return getVideoDescription(this.mp4boxFile, track);\n // }\n\n private getAudioDescription(track: any): ArrayBuffer | undefined {\n try {\n const fullTrack = this.mp4boxFile.getTrackById(track.id);\n for (const entry of fullTrack.mdia.minf.stbl.stsd.entries) {\n if (entry.esds || entry.dOps) {\n const stream = new (MP4Box as any).DataStream();\n (entry.esds || entry.dOps).write(stream);\n return new Uint8Array(stream.buffer.slice(8)).buffer;\n }\n }\n } catch (error) {\n console.error('Failed to get audio description:', error);\n }\n return undefined;\n }\n\n /**\n * Create transform stream for video track\n */\n createVideoStream(): TransformStream<Uint8Array, EncodedVideoChunk> {\n // const hasVideo = Array.from(this.tracks.values()).some((t) => t.type === 'video');\n return new TransformStream<Uint8Array, EncodedVideoChunk>(\n {\n start: (controller) => {\n this.videoController = controller;\n },\n transform: (chunk, controller) => {\n // Update backpressure metrics\n // desiredSize can be null, negative (backpressure), or positive\n const desiredSize = controller.desiredSize ?? this.demuxHighWaterMark;\n this.backpressureMonitor.updateMetrics('demux-video', desiredSize);\n\n // Create a copy to avoid buffer reuse issues (required for mp4box 0.5.x)\n const chunkData = new Uint8Array(chunk);\n this.appendBuffer(chunkData);\n\n // Flush after each append to trigger sample extraction immediately\n this.mp4boxFile.flush();\n },\n flush: async () => {\n // Trigger MP4Box flush\n this.mp4boxFile.flush();\n\n // Wait for MP4Box to complete sample extraction (onSamples callbacks)\n // Give MP4Box time to process asynchronously\n await new Promise((resolve) => setTimeout(resolve, 100));\n },\n },\n // Queuing strategy: use configuration\n {\n highWaterMark: this.demuxHighWaterMark,\n size: () => 1, // Count-based\n }\n );\n }\n\n /**\n * Create transform stream for audio track\n */\n createAudioStream(): TransformStream<Uint8Array, EncodedAudioChunk> | null {\n const hasAudio = Array.from(this.tracks.values()).some((t) => t.type === 'audio');\n if (!hasAudio) return null;\n\n return new TransformStream<Uint8Array, EncodedAudioChunk>(\n {\n start: (controller) => {\n this.audioController = controller;\n },\n transform: (chunk, controller) => {\n // Update backpressure metrics\n // desiredSize can be null, negative (backpressure), or positive\n const desiredSize = controller.desiredSize ?? this.demuxHighWaterMark;\n this.backpressureMonitor.updateMetrics('demux-audio', desiredSize);\n\n // Create a copy to avoid buffer reuse issues (required for mp4box 0.5.x)\n const chunkData = new Uint8Array(chunk);\n this.appendBuffer(chunkData);\n\n // Flush after each append to trigger sample extraction immediately\n this.mp4boxFile.flush();\n },\n flush: () => {\n this.mp4boxFile.flush();\n },\n },\n // Queuing strategy: use configuration\n {\n highWaterMark: this.demuxHighWaterMark,\n size: () => 1,\n }\n );\n }\n\n appendBuffer(chunk: Uint8Array): void {\n const buffer = chunk.buffer as ArrayBuffer & { fileStart: number };\n buffer.fileStart = this.fileOffset;\n this.mp4boxFile.appendBuffer(buffer);\n this.fileOffset += chunk.byteLength;\n }\n\n /**\n * Get video track info if available\n */\n get videoTrackInfo(): TrackInfo | undefined {\n return Array.from(this.tracks.values()).find((track) => track.type === 'video');\n }\n\n /**\n * Get audio track info if available\n */\n get audioTrackInfo(): TrackInfo | undefined {\n return Array.from(this.tracks.values()).find((track) => track.type === 'audio');\n }\n\n destroy(): void {\n this.mp4boxFile?.stop();\n this.mp4boxFile = null;\n this.tracks.clear();\n this.backpressureMonitor.clear();\n this.isReady = false;\n this.videoTimestampOffset = null;\n this.audioTimestampOffset = null;\n }\n}\n"],"names":["MP4Box.createFile","MP4Box.DataStream"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ const audioDemuxWorkerUrl = "" + new URL("../../assets/audio-demux.worker-xwWBtbAe.js", import.meta.url).href;
2
+ export {
3
+ audioDemuxWorkerUrl as default
4
+ };
5
+ //# sourceMappingURL=audio-demux.worker2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-demux.worker2.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -4,6 +4,11 @@
4
4
  *
5
5
  * Pipeline: ResourceLoader (Main Thread) → VideoDemuxWorker → DecodeWorker
6
6
  *
7
+ * Architecture Note:
8
+ * - One VideoDemuxWorker instance per CLIP (not per resource)
9
+ * - Multiple clips can share the same resource (different workers, independent processing)
10
+ * - This enables clean 3-Clip strategy lifecycle management
11
+ *
7
12
  * Features:
8
13
  * - MP4 container demuxing with mp4box.js
9
14
  * - Stream-based processing with backpressure
@@ -12,10 +17,8 @@
12
17
  export declare class VideoDemuxWorker {
13
18
  private channel;
14
19
  private demuxer;
15
- private resourceId;
16
- private clipId?;
20
+ private clipId;
17
21
  private downstreamPort;
18
- private range?;
19
22
  constructor();
20
23
  protected setupHandlers(): void;
21
24
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"video-demux.worker.d.ts","sourceRoot":"","sources":["../../../src/stages/demux/video-demux.worker.ts"],"names":[],"mappings":"AAaA;;;;;;;;;;GAUG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,MAAM,CAAC,CAAS;IACxB,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,KAAK,CAAC,CAAiC;;IAY/C,SAAS,CAAC,aAAa,IAAI,IAAI;IAW/B;;OAEG;YACW,aAAa;IAkB3B;;;;OAIG;YACW,eAAe;IAuC7B;;;OAGG;YACW,mBAAmB;IA+DjC,OAAO,CAAC,kBAAkB;IA2B1B;;OAEG;YACW,cAAc;IAe5B;;OAEG;YACW,aAAa;CAc5B;;AASD,wBAAoB"}
1
+ {"version":3,"file":"video-demux.worker.d.ts","sourceRoot":"","sources":["../../../src/stages/demux/video-demux.worker.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,cAAc,CAA4B;;IAYlD,SAAS,CAAC,aAAa,IAAI,IAAI;IAW/B;;OAEG;YACW,aAAa;IAiB3B;;;;OAIG;YACW,eAAe;IAuC7B;;;OAGG;YACW,mBAAmB;IAmCjC,OAAO,CAAC,kBAAkB;IA0B1B;;OAEG;YACW,cAAc;IAe5B;;OAEG;YACW,aAAa;CAc5B;;AASD,wBAAoB"}
@@ -4,10 +4,8 @@ import { MP4Demuxer } from "./MP4Demuxer.js";
4
4
  class VideoDemuxWorker {
5
5
  channel;
6
6
  demuxer = null;
7
- resourceId = null;
8
- clipId;
7
+ clipId = null;
9
8
  downstreamPort = null;
10
- range;
11
9
  constructor() {
12
10
  this.channel = new WorkerChannel(self, {
13
11
  name: "VideoDemuxWorker",
@@ -32,7 +30,7 @@ class VideoDemuxWorker {
32
30
  return { success: false };
33
31
  }
34
32
  this.downstreamPort = port;
35
- this.clipId = clipId;
33
+ this.clipId = clipId || null;
36
34
  return { success: true };
37
35
  }
38
36
  /**
@@ -72,34 +70,17 @@ class VideoDemuxWorker {
72
70
  * Strategy: Stream immediately, send codec info when ready
73
71
  */
74
72
  async handleReceiveStream(stream, metadata) {
75
- console.log("[VideoDemuxWorker] handleReceiveStream", metadata?.clipId, metadata?.resourceId);
76
- const currentResourceId = metadata?.resourceId || metadata?.clipId || this.resourceId || "default";
77
- const clipId = metadata?.clipId;
78
- if (this.resourceId && this.resourceId !== currentResourceId) {
79
- throw new Error(
80
- `[VideoDemuxWorker] BUG: Resource ID changed from ${this.resourceId} to ${currentResourceId}. Each VideoDemuxWorker instance should only handle one resource.`
81
- );
82
- }
83
- if (!this.resourceId) {
84
- this.resourceId = currentResourceId;
85
- }
73
+ this.clipId = metadata?.clipId || this.clipId;
86
74
  if (!this.demuxer) {
87
75
  this.demuxer = new MP4Demuxer({
88
76
  highWaterMark: 10,
89
77
  skipAudio: true,
90
- // Video only
91
78
  onReady: () => this.handleDemuxerReady()
92
79
  });
93
80
  }
94
81
  if (!this.downstreamPort) {
95
82
  throw new Error("Decoder not connected");
96
83
  }
97
- if (clipId) {
98
- this.clipId = clipId;
99
- }
100
- if (metadata?.range) {
101
- this.range = metadata.range;
102
- }
103
84
  const videoStream = this.demuxer.createVideoStream();
104
85
  const downstreamChannel = new WorkerChannel(this.downstreamPort, {
105
86
  name: "VideoDemux-Decoder",
@@ -107,9 +88,7 @@ class VideoDemuxWorker {
107
88
  });
108
89
  downstreamChannel.sendStream(videoStream.readable, {
109
90
  streamType: "video",
110
- clipId: this.clipId,
111
- ...this.range && { range: this.range }
112
- // codec info will be sent separately when ready
91
+ clipId: this.clipId
113
92
  });
114
93
  await stream.pipeTo(videoStream.writable);
115
94
  }
@@ -132,7 +111,6 @@ class VideoDemuxWorker {
132
111
  codec: videoTrackInfo.codec,
133
112
  width: videoTrackInfo.width,
134
113
  height: videoTrackInfo.height,
135
- ...this.range && { range: this.range },
136
114
  description: videoTrackInfo.description
137
115
  });
138
116
  }
@@ -154,7 +132,7 @@ class VideoDemuxWorker {
154
132
  async handleDispose() {
155
133
  this.demuxer?.destroy();
156
134
  this.demuxer = null;
157
- this.resourceId = null;
135
+ this.clipId = null;
158
136
  this.downstreamPort?.close();
159
137
  this.downstreamPort = null;
160
138
  this.channel.state = WorkerState.Disposed;
@@ -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 direction?: 'upstream' | string;\n clipId?: string;\n resourceId?: string;\n byteStart?: number;\n byteEnd?: number;\n range?: { start: number; end: 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 * 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 resourceId: string | null = null;\n private clipId?: string;\n private downstreamPort: MessagePort | null = null;\n private range?: { start: number; end: number };\n\n constructor() {\n // Initialize WorkerChannel\n this.channel = new WorkerChannel(self as DedicatedWorkerGlobalScope, {\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 clipId?: string;\n }): Promise<{ success: boolean }> {\n const { port, clipId } = payload;\n\n if (!port) {\n return { success: false };\n }\n\n // Store connection to decoder worker\n this.downstreamPort = port;\n this.clipId = clipId;\n\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 console.log('[VideoDemuxWorker] handleReceiveStream', metadata?.clipId, metadata?.resourceId);\n\n const currentResourceId =\n metadata?.resourceId || metadata?.clipId || this.resourceId || 'default';\n const clipId = metadata?.clipId;\n\n // According to ARCHITECTURE_VNEXT, one worker should only handle one resource\n // If resource changes, it's a bug in Orchestrator\n if (this.resourceId && this.resourceId !== currentResourceId) {\n throw new Error(\n `[VideoDemuxWorker] BUG: Resource ID changed from ${this.resourceId} to ${currentResourceId}. ` +\n `Each VideoDemuxWorker instance should only handle one resource.`\n );\n }\n\n if (!this.resourceId) {\n this.resourceId = currentResourceId;\n }\n\n // Initialize demuxer on first stream (not on every stream)\n if (!this.demuxer) {\n this.demuxer = new MP4Demuxer({\n highWaterMark: 10,\n skipAudio: true, // Video only\n onReady: () => this.handleDemuxerReady(),\n });\n }\n\n if (!this.downstreamPort) {\n throw new Error('Decoder not connected');\n }\n\n if (clipId) {\n this.clipId = clipId;\n }\n\n if (metadata?.range) {\n this.range = metadata.range;\n }\n\n // Create transform stream immediately\n const videoStream = this.demuxer.createVideoStream();\n // Send stream to decoder immediately (without codec info)\n const downstreamChannel = new WorkerChannel(this.downstreamPort, {\n name: 'VideoDemux-Decoder',\n timeout: 30000,\n });\n\n // Send stream first, codec info will follow when ready\n downstreamChannel.sendStream(videoStream.readable, {\n streamType: 'video',\n clipId: this.clipId,\n ...(this.range && { range: this.range }),\n // codec info will be sent separately when ready\n });\n\n await stream.pipeTo(videoStream!.writable);\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 clipId: this.clipId,\n streamType: 'video',\n codec: videoTrackInfo.codec,\n width: videoTrackInfo.width,\n height: videoTrackInfo.height,\n ...(this.range && { range: this.range }),\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.resourceId = 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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"}
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 clipId?: 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 3-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 clipId: string | null = null;\n private downstreamPort: MessagePort | null = null;\n\n constructor() {\n // Initialize WorkerChannel\n this.channel = new WorkerChannel(self as DedicatedWorkerGlobalScope, {\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 clipId?: string;\n }): Promise<{ success: boolean }> {\n const { port, clipId } = payload;\n\n if (!port) {\n return { success: false };\n }\n\n this.downstreamPort = port;\n this.clipId = clipId || null;\n\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 clipId from metadata (only happens once per worker lifecycle)\n this.clipId = metadata?.clipId || this.clipId;\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 throw new Error('Decoder not connected');\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 clipId: this.clipId,\n });\n\n await stream.pipeTo(videoStream.writable);\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 clipId: this.clipId,\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.clipId = 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":";;;AA0BO,MAAM,iBAAiB;AAAA,EACpB;AAAA,EACA,UAA6B;AAAA,EAC7B,SAAwB;AAAA,EACxB,iBAAqC;AAAA,EAE7C,cAAc;AAEZ,SAAK,UAAU,IAAI,cAAc,MAAoC;AAAA,MACnE,MAAM;AAAA,MACN,SAAS;AAAA,IAAA,CACV;AACD,SAAK,cAAA;AAAA,EACP;AAAA;AAAA,EAGU,gBAAsB;AAE9B,SAAK,QAAQ,gBAAgB,aAAa,KAAK,gBAAgB,KAAK,IAAI,CAAC;AACzE,SAAK,QAAQ,gBAAgB,WAAW,KAAK,cAAc,KAAK,IAAI,CAAC;AACrE,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,UAAM,EAAE,MAAM,OAAA,IAAW;AAEzB,QAAI,CAAC,MAAM;AACT,aAAO,EAAE,SAAS,MAAA;AAAA,IACpB;AAEA,SAAK,iBAAiB;AACtB,SAAK,SAAS,UAAU;AAExB,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,YAAI,KAAK,SAAS;AAChB,eAAK,QAAQ,QAAA;AAAA,QACf;AAEA,aAAK,UAAU,IAAI,WAAW;AAAA,UAC5B,GAAG;AAAA,UACH,WAAW;AAAA;AAAA,UACX,SAAS,MAAM,KAAK,mBAAA;AAAA,QAAmB,CACxC;AAGD,aAAK,QAAQ,OAAO,YAAY;AAEhC,eAAO,EAAE,SAAS,KAAA;AAAA,MACpB,OAAO;AAEL,aAAK,SAAS,aAAa,MAAM;AACjC,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;AAAA,EAMA,MAAc,oBACZ,QACA,UACe;AAEf,SAAK,SAAS,UAAU,UAAU,KAAK;AAGvC,QAAI,CAAC,KAAK,SAAS;AACjB,WAAK,UAAU,IAAI,WAAW;AAAA,QAC5B,eAAe;AAAA,QACf,WAAW;AAAA,QACX,SAAS,MAAM,KAAK,mBAAA;AAAA,MAAmB,CACxC;AAAA,IACH;AAEA,QAAI,CAAC,KAAK,gBAAgB;AACxB,YAAM,IAAI,MAAM,uBAAuB;AAAA,IACzC;AAGA,UAAM,cAAc,KAAK,QAAQ,kBAAA;AACjC,UAAM,oBAAoB,IAAI,cAAc,KAAK,gBAAgB;AAAA,MAC/D,MAAM;AAAA,MACN,SAAS;AAAA,IAAA,CACV;AAED,sBAAkB,WAAW,YAAY,UAAU;AAAA,MACjD,YAAY;AAAA,MACZ,QAAQ,KAAK;AAAA,IAAA,CACd;AAED,UAAM,OAAO,OAAO,YAAY,QAAQ;AAAA,EAC1C;AAAA,EAEQ,qBAA2B;AACjC,QAAI,CAAC,KAAK,WAAW,CAAC,KAAK,gBAAgB;AACzC;AAAA,IACF;AAEA,UAAM,iBAAiB,KAAK,QAAQ;AACpC,QAAI,CAAC,gBAAgB;AACnB,cAAQ,MAAM,qDAAqD;AACnE;AAAA,IACF;AAEA,UAAM,oBAAoB,IAAI,cAAc,KAAK,gBAAgB;AAAA,MAC/D,MAAM;AAAA,MACN,SAAS;AAAA,IAAA,CACV;AAED,sBAAkB,KAAK,aAAoB;AAAA,MACzC,QAAQ,KAAK;AAAA,MACb,YAAY;AAAA,MACZ,OAAO,eAAe;AAAA,MACtB,OAAO,eAAe;AAAA,MACtB,QAAQ,eAAe;AAAA,MACvB,aAAa,eAAe;AAAA,IAAA,CAC7B;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,iBAIX;AACD,QAAI,CAAC,KAAK,SAAS;AACjB,aAAO,EAAE,OAAO,KAAK,QAAQ,MAAA;AAAA,IAC/B;AAEA,WAAO;AAAA,MACL,YAAY,MAAM,KAAK,KAAK,QAAQ,OAAO,QAAQ;AAAA,MACnD,OAAO,KAAK,QAAQ;AAAA,IAAA;AAAA,EAExB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,gBAA+C;AAE3D,SAAK,SAAS,QAAA;AACd,SAAK,UAAU;AACf,SAAK,SAAS;AAGd,SAAK,gBAAgB,MAAA;AACrB,SAAK,iBAAiB;AAEtB,SAAK,QAAQ,QAAQ,YAAY;AAEjC,WAAO,EAAE,SAAS,KAAA;AAAA,EACpB;AACF;AAEA,MAAM,SAAS,IAAI,iBAAA;AAGnB,KAAK,iBAAiB,gBAAgB,MAAM;AAC1C,SAAO,eAAe,EAAA;AACxB,CAAC;AAED,MAAA,oBAAe;"}
@@ -0,0 +1,5 @@
1
+ const videoDemuxWorkerUrl = "" + new URL("../../assets/video-demux.worker-D019I7GQ.js", import.meta.url).href;
2
+ export {
3
+ videoDemuxWorkerUrl as default
4
+ };
5
+ //# sourceMappingURL=video-demux.worker2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"video-demux.worker2.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -1 +1 @@
1
- {"version":3,"file":"encode.worker.d.ts","sourceRoot":"","sources":["../../../src/stages/encode/encode.worker.ts"],"names":[],"mappings":";AA0aA,wBAAoB"}
1
+ {"version":3,"file":"encode.worker.d.ts","sourceRoot":"","sources":["../../../src/stages/encode/encode.worker.ts"],"names":[],"mappings":";AAyaA,wBAAoB"}
@@ -107,7 +107,6 @@ class EncodeWorker {
107
107
  timeout: 3e4
108
108
  });
109
109
  composeChannel.receiveStream(async (stream, metadata) => {
110
- console.log("[EncodeWorker] receiveStream", metadata);
111
110
  if (metadata?.streamType === "video" && this.videoEncoder) {
112
111
  const reader = stream.getReader();
113
112
  try {