@meframe/core 0.0.12 → 0.0.14
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/Meframe.d.ts.map +1 -1
- package/dist/Meframe.js +3 -3
- package/dist/Meframe.js.map +1 -1
- package/dist/cache/CacheManager.d.ts +10 -24
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +134 -182
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/cache/l1/VideoL1Cache.d.ts +4 -2
- package/dist/cache/l1/VideoL1Cache.d.ts.map +1 -1
- package/dist/cache/l1/VideoL1Cache.js +17 -3
- package/dist/cache/l1/VideoL1Cache.js.map +1 -1
- package/dist/controllers/PlaybackController.d.ts +3 -1
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +43 -25
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/controllers/PreRenderService.d.ts.map +1 -1
- package/dist/controllers/PreRenderService.js +6 -5
- package/dist/controllers/PreRenderService.js.map +1 -1
- package/dist/model/CompositionModel.d.ts.map +1 -1
- package/dist/model/CompositionModel.js +23 -14
- package/dist/model/CompositionModel.js.map +1 -1
- package/dist/model/dirty-range.d.ts.map +1 -1
- package/dist/model/patch.d.ts.map +1 -1
- package/dist/model/patch.js +60 -14
- package/dist/model/patch.js.map +1 -1
- package/dist/model/types.d.ts +23 -3
- package/dist/model/types.d.ts.map +1 -1
- package/dist/model/types.js +15 -0
- package/dist/model/types.js.map +1 -0
- package/dist/model/validation.d.ts.map +1 -1
- package/dist/model/validation.js +21 -4
- package/dist/model/validation.js.map +1 -1
- package/dist/orchestrator/ClipSessionManager.d.ts +2 -6
- package/dist/orchestrator/ClipSessionManager.d.ts.map +1 -1
- package/dist/orchestrator/ClipSessionManager.js +3 -9
- package/dist/orchestrator/ClipSessionManager.js.map +1 -1
- package/dist/orchestrator/CompositionPlanner.d.ts.map +1 -1
- package/dist/orchestrator/CompositionPlanner.js +9 -3
- package/dist/orchestrator/CompositionPlanner.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts +7 -2
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +82 -14
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts +3 -0
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +31 -14
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/orchestrator/types.d.ts +1 -0
- package/dist/orchestrator/types.d.ts.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.d.ts.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.js +10 -2
- package/dist/stages/compose/GlobalAudioSession.js.map +1 -1
- package/dist/stages/decode/BaseDecoder.js +130 -0
- package/dist/stages/decode/BaseDecoder.js.map +1 -0
- package/dist/stages/decode/VideoChunkDecoder.js +199 -0
- package/dist/stages/decode/VideoChunkDecoder.js.map +1 -0
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +15 -17
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/utils/errors.d.ts +12 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +11 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/worker/WorkerPool.d.ts +2 -2
- package/dist/worker/WorkerPool.d.ts.map +1 -1
- package/dist/worker/WorkerPool.js +7 -3
- package/dist/worker/WorkerPool.js.map +1 -1
- package/dist/workers/stages/compose/video-compose.worker.js.map +1 -1
- package/dist/workers/stages/demux/video-demux.worker.js +1 -1
- package/dist/workers/stages/demux/video-demux.worker.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GlobalAudioSession.js","sources":["../../../src/stages/compose/GlobalAudioSession.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport { OfflineAudioMixer } from './OfflineAudioMixer';\nimport type { CompositionModel, Clip } from '../../model';\nimport type { WorkerPool } from '../../worker/WorkerPool';\nimport type { ResourceLoader } from '../load/ResourceLoader';\nimport type { EventBus } from '../../event/EventBus';\nimport type { EventPayloadMap } from '../../event/events';\nimport { MeframeEvent } from '../../event/events';\nimport type { CacheManager } from '../../cache/CacheManager';\nimport { AudioChunkEncoder } from '../encode/AudioChunkEncoder';\n\ninterface AudioDataMessage {\n sessionId: string;\n audioData: AudioData;\n clipStartUs: TimeUs;\n clipDurationUs: TimeUs;\n}\n\ninterface AudioSessionDeps {\n cacheManager: CacheManager;\n workers: WorkerPool;\n resourceLoader: ResourceLoader;\n eventBus: EventBus<EventPayloadMap>;\n getModel: () => CompositionModel | null;\n buildWorkerConfigs: () => any;\n}\n\nexport class GlobalAudioSession {\n private mixer: OfflineAudioMixer;\n private activeClips = new Set<string>();\n private streamEndedClips = new Set<string>();\n private deps: AudioSessionDeps;\n private audioContext: AudioContext | null = null;\n private audioSources: AudioBufferSourceNode[] = [];\n private audioGainNodes: GainNode[] = [];\n private volume = 1.0;\n private playbackRate = 1.0;\n private isPlaying = false;\n\n constructor(deps: AudioSessionDeps) {\n this.deps = deps;\n this.mixer = new OfflineAudioMixer(deps.cacheManager, deps.getModel);\n }\n\n onAudioData(message: AudioDataMessage): void {\n const { sessionId, audioData, clipStartUs, clipDurationUs } = message;\n this.deps.cacheManager.putClipAudioData(sessionId, audioData, clipStartUs, clipDurationUs);\n }\n\n async activateAllAudioClips(): Promise<void> {\n const model = this.deps.getModel();\n if (!model) {\n return;\n }\n\n const audioTracks = model.tracks.filter((track) => track.kind === 'audio');\n\n for (const track of audioTracks) {\n for (const clip of track.clips) {\n if (!this.activeClips.has(clip.id)) {\n await this.setupAudioPipeline(clip);\n this.activeClips.add(clip.id);\n\n await this.deps.resourceLoader.fetch(clip.resourceId, {\n priority: 'high',\n sessionId: clip.id,\n trackId: track.id,\n });\n\n this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });\n }\n }\n }\n }\n\n handleAudioStream(stream: ReadableStream<AudioData>, metadata: Record<string, any>): void {\n const sessionId = metadata.sessionId || 'unknown';\n const clipStartUs = metadata.clipStartUs ?? 0;\n const clipDurationUs = metadata.clipDurationUs ?? 0;\n\n const reader = stream.getReader();\n const pump = async (): Promise<void> => {\n try {\n const { done, value } = await reader.read();\n if (done) {\n this.streamEndedClips.add(sessionId);\n reader.releaseLock();\n return;\n }\n\n this.onAudioData({\n sessionId,\n audioData: value,\n clipStartUs,\n clipDurationUs,\n });\n\n await pump();\n } catch (error) {\n console.error('[GlobalAudioSession] Audio stream error:', error);\n reader.releaseLock();\n }\n };\n\n pump();\n }\n\n async startPlayback(timeUs: TimeUs, audioContext: AudioContext): Promise<void> {\n this.audioContext = audioContext;\n this.isPlaying = true;\n this.startAllActiveClips(timeUs);\n }\n\n stopPlayback(): void {\n this.isPlaying = false;\n this.stopAllAudioSources();\n }\n\n updateTime(timeUs: TimeUs): void {\n if (!this.isPlaying) {\n return;\n }\n this.checkAndStartNewClips(timeUs);\n }\n\n setVolume(volume: number): void {\n this.volume = volume;\n for (const gainNode of this.audioGainNodes) {\n gainNode.gain.value = this.volume;\n }\n }\n\n setPlaybackRate(rate: number): void {\n this.playbackRate = rate;\n for (const source of this.audioSources) {\n source.playbackRate.value = this.playbackRate;\n }\n }\n\n reset(): void {\n this.stopAllAudioSources();\n this.deps.cacheManager.resetAudioCache();\n this.activeClips.clear();\n this.streamEndedClips.clear();\n }\n\n /**\n * Create export encoded audio stream\n */\n async createExportEncodedStream(\n config?: Partial<AudioEncoderConfig>,\n onFirstMetadata?: (metadata: EncodedAudioChunkMetadata) => void\n ): Promise<ReadableStream<EncodedAudioChunk> | null> {\n const audioDataStream = await this.createExportAudioStream();\n if (!audioDataStream) {\n return null;\n }\n\n const encoder = new AudioChunkEncoder(config);\n await encoder.initialize();\n\n const encodingTransform = encoder.createStream();\n const encodedStream = audioDataStream.pipeThrough(encodingTransform);\n\n let firstMetadataExtracted = false;\n\n return encodedStream.pipeThrough(\n new TransformStream({\n transform(encoderChunk, controller) {\n if (!firstMetadataExtracted && onFirstMetadata) {\n onFirstMetadata(encoderChunk.metadata as EncodedAudioChunkMetadata);\n firstMetadataExtracted = true;\n }\n controller.enqueue(encoderChunk.chunk as EncodedAudioChunk);\n },\n })\n );\n }\n\n /**\n * Create export audio stream\n */\n async createExportAudioStream(): Promise<ReadableStream<AudioData> | null> {\n const model = this.deps.getModel();\n if (!model) {\n return null;\n }\n\n const totalDurationUs = model.durationUs;\n\n await this.activateAllAudioClips();\n await this.waitForAudioClipsReady();\n\n return new ReadableStream<AudioData>({\n start: async (controller) => {\n const windowSize = 5_000_000;\n let currentUs = 0;\n\n while (currentUs < totalDurationUs) {\n const windowEndUs = Math.min(currentUs + windowSize, totalDurationUs);\n const mixedBuffer = await this.mixer.mix(currentUs, windowEndUs);\n const audioData = this.audioBufferToAudioData(mixedBuffer, currentUs);\n if (audioData) {\n controller.enqueue(audioData);\n }\n currentUs = windowEndUs;\n }\n\n controller.close();\n },\n });\n }\n\n private async waitForAudioClipsReady(): Promise<void> {\n const model = this.deps.getModel();\n if (!model) return;\n\n const audioClips = model.tracks\n .filter((track) => track.kind === 'audio')\n .flatMap((track) => track.clips);\n\n const waitPromises = audioClips.map((clip) => this.waitForClipPCM(clip.id, 10000)); // 10s timeout\n await Promise.allSettled(waitPromises);\n }\n\n private waitForClipPCM(clipId: string, timeoutMs: number): Promise<boolean> {\n return new Promise((resolve) => {\n const checkInterval = 100;\n let elapsed = 0;\n let lastFrameCount = 0;\n let stableCount = 0;\n let streamEndDetected = false;\n\n const check = () => {\n const pcm = this.deps.cacheManager.getClipPCM(clipId, 0, Number.MAX_SAFE_INTEGER);\n\n if (pcm && pcm.length > 0) {\n const currentFrameCount = pcm[0]?.length ?? 0;\n\n // Check if we have received stream end signal\n if (this.streamEndedClips.has(clipId)) {\n streamEndDetected = true;\n }\n\n // If stream has ended, we're done\n if (streamEndDetected) {\n resolve(true);\n return;\n }\n\n // Otherwise, check if frame count is stable (no new data for 500ms)\n if (currentFrameCount === lastFrameCount) {\n stableCount++;\n if (stableCount >= 5) {\n // 5 * 100ms = 500ms\n resolve(true);\n return;\n }\n } else {\n stableCount = 0;\n lastFrameCount = currentFrameCount;\n }\n }\n\n elapsed += checkInterval;\n if (elapsed >= timeoutMs) {\n console.warn('[GlobalAudioSession] Timeout waiting for clip', clipId, {\n frames: lastFrameCount,\n elapsed,\n });\n resolve(false);\n return;\n }\n\n setTimeout(check, checkInterval);\n };\n\n check();\n });\n }\n\n private startAllActiveClips(timeUs: TimeUs): void {\n if (!this.audioContext) {\n return;\n }\n\n const currentClips = this.getActiveAudioClips(timeUs);\n\n for (const clip of currentClips) {\n this.startClipPlayback(clip, timeUs);\n }\n }\n\n private checkAndStartNewClips(timeUs: TimeUs): void {\n if (!this.audioContext) {\n return;\n }\n\n const currentClips = this.getActiveAudioClips(timeUs);\n const activeClipIds = new Set(\n this.audioSources.map((source) => (source as any)._meframeClipId).filter(Boolean)\n );\n\n for (const clip of currentClips) {\n // Check if clip should be playing at current time\n const clipEndUs = clip.startUs + clip.durationUs;\n if (timeUs >= clipEndUs) {\n // Clip has already ended, skip\n continue;\n }\n\n if (!activeClipIds.has(clip.id)) {\n this.startClipPlayback(clip, timeUs);\n }\n }\n }\n\n private startClipPlayback(clip: Clip, currentTimeUs: TimeUs): void {\n if (!this.audioContext) {\n return;\n }\n\n const clipPCMData = this.deps.cacheManager.getClipPCMWithMetadata(\n clip.id,\n clip.startUs,\n clip.startUs + clip.durationUs\n );\n\n if (!clipPCMData || clipPCMData.planes.length === 0) {\n // No data yet, will retry later via checkAndStartNewClips\n return;\n }\n\n const buffer = this.pcmToAudioBuffer(clipPCMData.planes, clipPCMData.sampleRate);\n\n const source = this.audioContext.createBufferSource();\n source.buffer = buffer;\n source.playbackRate.value = this.playbackRate;\n (source as any)._meframeClipId = clip.id;\n (source as any)._playedToUs = clip.startUs + buffer.duration * 1_000_000; // Track where it played to\n\n const gainNode = this.audioContext.createGain();\n gainNode.gain.value = this.volume;\n\n source.connect(gainNode);\n gainNode.connect(this.audioContext.destination);\n\n const offsetUs = Math.max(0, currentTimeUs - clip.startUs);\n const offsetSeconds = offsetUs / 1_000_000;\n\n // Use actual buffer duration instead of clip.durationUs\n const actualDurationSeconds = buffer.duration - offsetSeconds;\n\n if (actualDurationSeconds <= 0) {\n return;\n }\n\n source.start(0, offsetSeconds, actualDurationSeconds);\n\n source.onended = () => {\n const index = this.audioSources.indexOf(source);\n if (index >= 0) {\n this.audioSources.splice(index, 1);\n this.audioGainNodes.splice(index, 1);\n }\n\n // Check if more data has arrived and continue playing\n const playedToUs = (source as any)._playedToUs;\n const clipEndUs = clip.startUs + clip.durationUs;\n\n if (playedToUs < clipEndUs && this.isPlaying) {\n // There might be more data, try to continue\n setTimeout(() => {\n if (this.isPlaying) {\n this.continueClipPlayback(clip, playedToUs);\n }\n }, 50);\n }\n };\n\n this.audioSources.push(source);\n this.audioGainNodes.push(gainNode);\n }\n\n private continueClipPlayback(clip: Clip, fromUs: TimeUs): void {\n if (!this.audioContext) {\n return;\n }\n\n // Check if there's new data\n const clipPCMData = this.deps.cacheManager.getClipPCMWithMetadata(\n clip.id,\n clip.startUs,\n clip.startUs + clip.durationUs\n );\n\n if (!clipPCMData || clipPCMData.planes.length === 0) {\n return;\n }\n\n const buffer = this.pcmToAudioBuffer(clipPCMData.planes, clipPCMData.sampleRate);\n const bufferEndUs = clip.startUs + buffer.duration * 1_000_000;\n\n // Check if there's new data beyond where we played to\n if (bufferEndUs <= fromUs + 100_000) {\n // 100ms tolerance\n // No significant new data\n return;\n }\n\n // Continue playback from where it left off\n const source = this.audioContext.createBufferSource();\n source.buffer = buffer;\n source.playbackRate.value = this.playbackRate;\n (source as any)._meframeClipId = clip.id;\n (source as any)._playedToUs = bufferEndUs;\n\n const gainNode = this.audioContext.createGain();\n gainNode.gain.value = this.volume;\n\n source.connect(gainNode);\n gainNode.connect(this.audioContext.destination);\n\n const offsetUs = Math.max(0, fromUs - clip.startUs);\n const offsetSeconds = offsetUs / 1_000_000;\n const actualDurationSeconds = buffer.duration - offsetSeconds;\n\n if (actualDurationSeconds <= 0) {\n return;\n }\n\n source.start(0, offsetSeconds, actualDurationSeconds);\n\n source.onended = () => {\n const index = this.audioSources.indexOf(source);\n if (index >= 0) {\n this.audioSources.splice(index, 1);\n this.audioGainNodes.splice(index, 1);\n }\n\n // Check if more data has arrived\n const playedToUs = (source as any)._playedToUs;\n const clipEndUs = clip.startUs + clip.durationUs;\n\n if (playedToUs < clipEndUs && this.isPlaying) {\n setTimeout(() => {\n if (this.isPlaying) {\n this.continueClipPlayback(clip, playedToUs);\n }\n }, 50);\n }\n };\n\n this.audioSources.push(source);\n this.audioGainNodes.push(gainNode);\n }\n\n private stopAllAudioSources(): void {\n for (const source of this.audioSources) {\n try {\n source.stop();\n source.disconnect();\n } catch (error) {\n // Ignore\n }\n }\n\n for (const gainNode of this.audioGainNodes) {\n try {\n gainNode.disconnect();\n } catch (error) {\n // Ignore\n }\n }\n\n this.audioSources = [];\n this.audioGainNodes = [];\n }\n\n private getActiveAudioClips(timeUs: TimeUs): Clip[] {\n const model = this.deps.getModel();\n if (!model) {\n return [];\n }\n\n const audioTracks = model.getTracksByKind('audio');\n const clips: Clip[] = [];\n\n for (const track of audioTracks) {\n const trackClips = model.getClipsAtTime(timeUs, track.id);\n if (trackClips.length > 0) {\n clips.push(...trackClips);\n }\n }\n\n return clips;\n }\n\n private pcmToAudioBuffer(planes: Float32Array[], sampleRate: number): AudioBuffer {\n const numberOfChannels = planes.length;\n const numberOfFrames = planes[0]?.length ?? 0;\n\n const ctx = new OfflineAudioContext(numberOfChannels, 1, sampleRate);\n const buffer = ctx.createBuffer(numberOfChannels, numberOfFrames, sampleRate);\n\n for (let channel = 0; channel < numberOfChannels; channel++) {\n const plane = planes[channel];\n if (plane) {\n const channelData = buffer.getChannelData(channel);\n channelData.set(plane);\n }\n }\n\n return buffer;\n }\n\n private audioBufferToAudioData(buffer: AudioBuffer, timestampUs: TimeUs): AudioData | null {\n const sampleRate = buffer.sampleRate;\n const numberOfChannels = buffer.numberOfChannels;\n const numberOfFrames = buffer.length;\n\n const planes: Float32Array[] = [];\n for (let channel = 0; channel < numberOfChannels; channel++) {\n planes.push(buffer.getChannelData(channel));\n }\n\n return new AudioData({\n format: 'f32', // interleaved format\n sampleRate,\n numberOfFrames,\n numberOfChannels,\n timestamp: timestampUs,\n data: this.interleavePlanarData(planes),\n });\n }\n\n private interleavePlanarData(planes: Float32Array[]): ArrayBuffer {\n const numberOfChannels = planes.length;\n const numberOfFrames = planes[0]?.length ?? 0;\n const totalSamples = numberOfChannels * numberOfFrames;\n\n const interleaved = new Float32Array(totalSamples);\n\n for (let frame = 0; frame < numberOfFrames; frame++) {\n for (let channel = 0; channel < numberOfChannels; channel++) {\n interleaved[frame * numberOfChannels + channel] = planes[channel]![frame]!;\n }\n }\n\n return interleaved.buffer;\n }\n\n private async setupAudioPipeline(clip: Clip): Promise<void> {\n const { id: clipId, resourceId, startUs, durationUs } = clip;\n const audioDemuxWorker = await this.deps.workers.get('audioDemux', clipId, { lazy: true });\n const audioDecodeWorker = await this.deps.workers.get('audioDecode', clipId, { lazy: true });\n\n const demuxToDecodeChannel = new MessageChannel();\n await audioDemuxWorker.send(\n 'connect',\n { direction: 'downstream', port: demuxToDecodeChannel.port1, streamType: 'audio', clipId },\n { transfer: [demuxToDecodeChannel.port1] }\n );\n await audioDecodeWorker.send(\n 'connect',\n {\n direction: 'upstream',\n port: demuxToDecodeChannel.port2,\n streamType: 'audio',\n sessionId: clipId,\n clipStartUs: startUs || 0,\n clipDurationUs: durationUs || 0,\n },\n { transfer: [demuxToDecodeChannel.port2] }\n );\n\n audioDecodeWorker.receiveStream((stream, metadata) => {\n this.handleAudioStream(stream as ReadableStream<AudioData>, {\n sessionId: clipId,\n clipStartUs: startUs || 0,\n clipDurationUs: durationUs || 0,\n ...metadata,\n });\n });\n\n const demuxConfig = this.deps.buildWorkerConfigs().audioDemux;\n await audioDemuxWorker.send('configure', {\n initial: true,\n resourceId,\n clipId,\n config: demuxConfig,\n });\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GlobalAudioSession.js","sources":["../../../src/stages/compose/GlobalAudioSession.ts"],"sourcesContent":["import type { TimeUs, AudioClip } from '../../model/types';\nimport { OfflineAudioMixer } from './OfflineAudioMixer';\nimport type { CompositionModel, Clip } from '../../model';\nimport type { WorkerPool } from '../../worker/WorkerPool';\nimport type { ResourceLoader } from '../load/ResourceLoader';\nimport type { EventBus } from '../../event/EventBus';\nimport type { EventPayloadMap } from '../../event/events';\nimport { MeframeEvent } from '../../event/events';\nimport type { CacheManager } from '../../cache/CacheManager';\nimport { AudioChunkEncoder } from '../encode/AudioChunkEncoder';\nimport { isAudioClip } from '../../model/types';\n\ninterface AudioDataMessage {\n sessionId: string;\n audioData: AudioData;\n clipStartUs: TimeUs;\n clipDurationUs: TimeUs;\n}\n\ninterface AudioSessionDeps {\n cacheManager: CacheManager;\n workers: WorkerPool;\n resourceLoader: ResourceLoader;\n eventBus: EventBus<EventPayloadMap>;\n getModel: () => CompositionModel | null;\n buildWorkerConfigs: () => any;\n}\n\nexport class GlobalAudioSession {\n private mixer: OfflineAudioMixer;\n private activeClips = new Set<string>();\n private streamEndedClips = new Set<string>();\n private deps: AudioSessionDeps;\n private audioContext: AudioContext | null = null;\n private audioSources: AudioBufferSourceNode[] = [];\n private audioGainNodes: GainNode[] = [];\n private volume = 1.0;\n private playbackRate = 1.0;\n private isPlaying = false;\n\n constructor(deps: AudioSessionDeps) {\n this.deps = deps;\n this.mixer = new OfflineAudioMixer(deps.cacheManager, deps.getModel);\n }\n\n onAudioData(message: AudioDataMessage): void {\n const { sessionId, audioData, clipStartUs, clipDurationUs } = message;\n this.deps.cacheManager.putClipAudioData(sessionId, audioData, clipStartUs, clipDurationUs);\n }\n\n async activateAllAudioClips(): Promise<void> {\n const model = this.deps.getModel();\n if (!model) {\n return;\n }\n\n const audioTracks = model.tracks.filter((track) => track.kind === 'audio');\n\n for (const track of audioTracks) {\n for (const clip of track.clips) {\n if (!this.activeClips.has(clip.id)) {\n if (!isAudioClip(clip)) {\n throw new Error(`Clip ${clip.id} in audio track is not an audio clip`);\n }\n\n await this.setupAudioPipeline(clip);\n this.activeClips.add(clip.id);\n\n await this.deps.resourceLoader.fetch(clip.resourceId, {\n priority: 'high',\n sessionId: clip.id,\n trackId: track.id,\n });\n\n this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });\n }\n }\n }\n }\n\n handleAudioStream(stream: ReadableStream<AudioData>, metadata: Record<string, any>): void {\n const sessionId = metadata.sessionId || 'unknown';\n const clipStartUs = metadata.clipStartUs ?? 0;\n const clipDurationUs = metadata.clipDurationUs ?? 0;\n\n const reader = stream.getReader();\n const pump = async (): Promise<void> => {\n try {\n const { done, value } = await reader.read();\n if (done) {\n this.streamEndedClips.add(sessionId);\n reader.releaseLock();\n return;\n }\n\n this.onAudioData({\n sessionId,\n audioData: value,\n clipStartUs,\n clipDurationUs,\n });\n\n await pump();\n } catch (error) {\n console.error('[GlobalAudioSession] Audio stream error:', error);\n reader.releaseLock();\n }\n };\n\n pump();\n }\n\n async startPlayback(timeUs: TimeUs, audioContext: AudioContext): Promise<void> {\n this.audioContext = audioContext;\n this.isPlaying = true;\n this.startAllActiveClips(timeUs);\n }\n\n stopPlayback(): void {\n this.isPlaying = false;\n this.stopAllAudioSources();\n }\n\n updateTime(timeUs: TimeUs): void {\n if (!this.isPlaying) {\n return;\n }\n this.checkAndStartNewClips(timeUs);\n }\n\n setVolume(volume: number): void {\n this.volume = volume;\n for (const gainNode of this.audioGainNodes) {\n gainNode.gain.value = this.volume;\n }\n }\n\n setPlaybackRate(rate: number): void {\n this.playbackRate = rate;\n for (const source of this.audioSources) {\n source.playbackRate.value = this.playbackRate;\n }\n }\n\n reset(): void {\n this.stopAllAudioSources();\n this.deps.cacheManager.resetAudioCache();\n this.activeClips.clear();\n this.streamEndedClips.clear();\n }\n\n /**\n * Create export encoded audio stream\n */\n async createExportEncodedStream(\n config?: Partial<AudioEncoderConfig>,\n onFirstMetadata?: (metadata: EncodedAudioChunkMetadata) => void\n ): Promise<ReadableStream<EncodedAudioChunk> | null> {\n const audioDataStream = await this.createExportAudioStream();\n if (!audioDataStream) {\n return null;\n }\n\n const encoder = new AudioChunkEncoder(config);\n await encoder.initialize();\n\n const encodingTransform = encoder.createStream();\n const encodedStream = audioDataStream.pipeThrough(encodingTransform);\n\n let firstMetadataExtracted = false;\n\n return encodedStream.pipeThrough(\n new TransformStream({\n transform(encoderChunk, controller) {\n if (!firstMetadataExtracted && onFirstMetadata) {\n onFirstMetadata(encoderChunk.metadata as EncodedAudioChunkMetadata);\n firstMetadataExtracted = true;\n }\n controller.enqueue(encoderChunk.chunk as EncodedAudioChunk);\n },\n })\n );\n }\n\n /**\n * Create export audio stream\n */\n async createExportAudioStream(): Promise<ReadableStream<AudioData> | null> {\n const model = this.deps.getModel();\n if (!model) {\n return null;\n }\n\n const totalDurationUs = model.durationUs;\n\n await this.activateAllAudioClips();\n await this.waitForAudioClipsReady();\n\n return new ReadableStream<AudioData>({\n start: async (controller) => {\n const windowSize = 5_000_000;\n let currentUs = 0;\n\n while (currentUs < totalDurationUs) {\n const windowEndUs = Math.min(currentUs + windowSize, totalDurationUs);\n const mixedBuffer = await this.mixer.mix(currentUs, windowEndUs);\n const audioData = this.audioBufferToAudioData(mixedBuffer, currentUs);\n if (audioData) {\n controller.enqueue(audioData);\n }\n currentUs = windowEndUs;\n }\n\n controller.close();\n },\n });\n }\n\n private async waitForAudioClipsReady(): Promise<void> {\n const model = this.deps.getModel();\n if (!model) return;\n\n const audioClips = model.tracks\n .filter((track) => track.kind === 'audio')\n .flatMap((track) => track.clips);\n\n const waitPromises = audioClips.map((clip) => this.waitForClipPCM(clip.id, 10000)); // 10s timeout\n await Promise.allSettled(waitPromises);\n }\n\n private waitForClipPCM(clipId: string, timeoutMs: number): Promise<boolean> {\n return new Promise((resolve) => {\n const checkInterval = 100;\n let elapsed = 0;\n let lastFrameCount = 0;\n let stableCount = 0;\n let streamEndDetected = false;\n\n const check = () => {\n const pcm = this.deps.cacheManager.getClipPCM(clipId, 0, Number.MAX_SAFE_INTEGER);\n\n if (pcm && pcm.length > 0) {\n const currentFrameCount = pcm[0]?.length ?? 0;\n\n // Check if we have received stream end signal\n if (this.streamEndedClips.has(clipId)) {\n streamEndDetected = true;\n }\n\n // If stream has ended, we're done\n if (streamEndDetected) {\n resolve(true);\n return;\n }\n\n // Otherwise, check if frame count is stable (no new data for 500ms)\n if (currentFrameCount === lastFrameCount) {\n stableCount++;\n if (stableCount >= 5) {\n // 5 * 100ms = 500ms\n resolve(true);\n return;\n }\n } else {\n stableCount = 0;\n lastFrameCount = currentFrameCount;\n }\n }\n\n elapsed += checkInterval;\n if (elapsed >= timeoutMs) {\n console.warn('[GlobalAudioSession] Timeout waiting for clip', clipId, {\n frames: lastFrameCount,\n elapsed,\n });\n resolve(false);\n return;\n }\n\n setTimeout(check, checkInterval);\n };\n\n check();\n });\n }\n\n private startAllActiveClips(timeUs: TimeUs): void {\n if (!this.audioContext) {\n return;\n }\n\n const currentClips = this.getActiveAudioClips(timeUs);\n\n for (const clip of currentClips) {\n this.startClipPlayback(clip, timeUs);\n }\n }\n\n private checkAndStartNewClips(timeUs: TimeUs): void {\n if (!this.audioContext) {\n return;\n }\n\n const currentClips = this.getActiveAudioClips(timeUs);\n const activeClipIds = new Set(\n this.audioSources.map((source) => (source as any)._meframeClipId).filter(Boolean)\n );\n\n for (const clip of currentClips) {\n // Check if clip should be playing at current time\n const clipEndUs = clip.startUs + clip.durationUs;\n if (timeUs >= clipEndUs) {\n // Clip has already ended, skip\n continue;\n }\n\n if (!activeClipIds.has(clip.id)) {\n this.startClipPlayback(clip, timeUs);\n }\n }\n }\n\n private startClipPlayback(clip: Clip, currentTimeUs: TimeUs): void {\n if (!this.audioContext) {\n return;\n }\n\n const clipPCMData = this.deps.cacheManager.getClipPCMWithMetadata(\n clip.id,\n clip.startUs,\n clip.startUs + clip.durationUs\n );\n\n if (!clipPCMData || clipPCMData.planes.length === 0) {\n // No data yet, will retry later via checkAndStartNewClips\n return;\n }\n\n const buffer = this.pcmToAudioBuffer(clipPCMData.planes, clipPCMData.sampleRate);\n\n const source = this.audioContext.createBufferSource();\n source.buffer = buffer;\n source.playbackRate.value = this.playbackRate;\n (source as any)._meframeClipId = clip.id;\n (source as any)._playedToUs = clip.startUs + buffer.duration * 1_000_000; // Track where it played to\n\n const gainNode = this.audioContext.createGain();\n gainNode.gain.value = this.volume;\n\n source.connect(gainNode);\n gainNode.connect(this.audioContext.destination);\n\n const offsetUs = Math.max(0, currentTimeUs - clip.startUs);\n const offsetSeconds = offsetUs / 1_000_000;\n\n // Use actual buffer duration instead of clip.durationUs\n const actualDurationSeconds = buffer.duration - offsetSeconds;\n\n if (actualDurationSeconds <= 0) {\n return;\n }\n\n source.start(0, offsetSeconds, actualDurationSeconds);\n\n source.onended = () => {\n const index = this.audioSources.indexOf(source);\n if (index >= 0) {\n this.audioSources.splice(index, 1);\n this.audioGainNodes.splice(index, 1);\n }\n\n // Check if more data has arrived and continue playing\n const playedToUs = (source as any)._playedToUs;\n const clipEndUs = clip.startUs + clip.durationUs;\n\n if (playedToUs < clipEndUs && this.isPlaying) {\n // There might be more data, try to continue\n setTimeout(() => {\n if (this.isPlaying) {\n this.continueClipPlayback(clip, playedToUs);\n }\n }, 50);\n }\n };\n\n this.audioSources.push(source);\n this.audioGainNodes.push(gainNode);\n }\n\n private continueClipPlayback(clip: Clip, fromUs: TimeUs): void {\n if (!this.audioContext) {\n return;\n }\n\n // Check if there's new data\n const clipPCMData = this.deps.cacheManager.getClipPCMWithMetadata(\n clip.id,\n clip.startUs,\n clip.startUs + clip.durationUs\n );\n\n if (!clipPCMData || clipPCMData.planes.length === 0) {\n return;\n }\n\n const buffer = this.pcmToAudioBuffer(clipPCMData.planes, clipPCMData.sampleRate);\n const bufferEndUs = clip.startUs + buffer.duration * 1_000_000;\n\n // Check if there's new data beyond where we played to\n if (bufferEndUs <= fromUs + 100_000) {\n // 100ms tolerance\n // No significant new data\n return;\n }\n\n // Continue playback from where it left off\n const source = this.audioContext.createBufferSource();\n source.buffer = buffer;\n source.playbackRate.value = this.playbackRate;\n (source as any)._meframeClipId = clip.id;\n (source as any)._playedToUs = bufferEndUs;\n\n const gainNode = this.audioContext.createGain();\n gainNode.gain.value = this.volume;\n\n source.connect(gainNode);\n gainNode.connect(this.audioContext.destination);\n\n const offsetUs = Math.max(0, fromUs - clip.startUs);\n const offsetSeconds = offsetUs / 1_000_000;\n const actualDurationSeconds = buffer.duration - offsetSeconds;\n\n if (actualDurationSeconds <= 0) {\n return;\n }\n\n source.start(0, offsetSeconds, actualDurationSeconds);\n\n source.onended = () => {\n const index = this.audioSources.indexOf(source);\n if (index >= 0) {\n this.audioSources.splice(index, 1);\n this.audioGainNodes.splice(index, 1);\n }\n\n // Check if more data has arrived\n const playedToUs = (source as any)._playedToUs;\n const clipEndUs = clip.startUs + clip.durationUs;\n\n if (playedToUs < clipEndUs && this.isPlaying) {\n setTimeout(() => {\n if (this.isPlaying) {\n this.continueClipPlayback(clip, playedToUs);\n }\n }, 50);\n }\n };\n\n this.audioSources.push(source);\n this.audioGainNodes.push(gainNode);\n }\n\n private stopAllAudioSources(): void {\n for (const source of this.audioSources) {\n try {\n source.stop();\n source.disconnect();\n } catch (error) {\n // Ignore\n }\n }\n\n for (const gainNode of this.audioGainNodes) {\n try {\n gainNode.disconnect();\n } catch (error) {\n // Ignore\n }\n }\n\n this.audioSources = [];\n this.audioGainNodes = [];\n }\n\n private getActiveAudioClips(timeUs: TimeUs): Clip[] {\n const model = this.deps.getModel();\n if (!model) {\n return [];\n }\n\n const audioTracks = model.getTracksByKind('audio');\n const clips: Clip[] = [];\n\n for (const track of audioTracks) {\n const trackClips = model.getClipsAtTime(timeUs, track.id);\n if (trackClips.length > 0) {\n clips.push(...trackClips);\n }\n }\n\n return clips;\n }\n\n private pcmToAudioBuffer(planes: Float32Array[], sampleRate: number): AudioBuffer {\n const numberOfChannels = planes.length;\n const numberOfFrames = planes[0]?.length ?? 0;\n\n const ctx = new OfflineAudioContext(numberOfChannels, 1, sampleRate);\n const buffer = ctx.createBuffer(numberOfChannels, numberOfFrames, sampleRate);\n\n for (let channel = 0; channel < numberOfChannels; channel++) {\n const plane = planes[channel];\n if (plane) {\n const channelData = buffer.getChannelData(channel);\n channelData.set(plane);\n }\n }\n\n return buffer;\n }\n\n private audioBufferToAudioData(buffer: AudioBuffer, timestampUs: TimeUs): AudioData | null {\n const sampleRate = buffer.sampleRate;\n const numberOfChannels = buffer.numberOfChannels;\n const numberOfFrames = buffer.length;\n\n const planes: Float32Array[] = [];\n for (let channel = 0; channel < numberOfChannels; channel++) {\n planes.push(buffer.getChannelData(channel));\n }\n\n return new AudioData({\n format: 'f32', // interleaved format\n sampleRate,\n numberOfFrames,\n numberOfChannels,\n timestamp: timestampUs,\n data: this.interleavePlanarData(planes),\n });\n }\n\n private interleavePlanarData(planes: Float32Array[]): ArrayBuffer {\n const numberOfChannels = planes.length;\n const numberOfFrames = planes[0]?.length ?? 0;\n const totalSamples = numberOfChannels * numberOfFrames;\n\n const interleaved = new Float32Array(totalSamples);\n\n for (let frame = 0; frame < numberOfFrames; frame++) {\n for (let channel = 0; channel < numberOfChannels; channel++) {\n interleaved[frame * numberOfChannels + channel] = planes[channel]![frame]!;\n }\n }\n\n return interleaved.buffer;\n }\n\n private async setupAudioPipeline(clip: AudioClip): Promise<void> {\n const { id: clipId, resourceId, startUs, durationUs } = clip;\n const audioDemuxWorker = await this.deps.workers.getOrCreate('audioDemux', clipId, {\n lazy: true,\n });\n const audioDecodeWorker = await this.deps.workers.getOrCreate('audioDecode', clipId, {\n lazy: true,\n });\n\n const demuxToDecodeChannel = new MessageChannel();\n await audioDemuxWorker.send(\n 'connect',\n { direction: 'downstream', port: demuxToDecodeChannel.port1, streamType: 'audio', clipId },\n { transfer: [demuxToDecodeChannel.port1] }\n );\n await audioDecodeWorker.send(\n 'connect',\n {\n direction: 'upstream',\n port: demuxToDecodeChannel.port2,\n streamType: 'audio',\n sessionId: clipId,\n clipStartUs: startUs || 0,\n clipDurationUs: durationUs || 0,\n },\n { transfer: [demuxToDecodeChannel.port2] }\n );\n\n audioDecodeWorker.receiveStream((stream, metadata) => {\n this.handleAudioStream(stream as ReadableStream<AudioData>, {\n sessionId: clipId,\n clipStartUs: startUs || 0,\n clipDurationUs: durationUs || 0,\n ...metadata,\n });\n });\n\n const demuxConfig = this.deps.buildWorkerConfigs().audioDemux;\n await audioDemuxWorker.send('configure', {\n initial: true,\n resourceId,\n clipId,\n config: demuxConfig,\n });\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
class BaseDecoder {
|
|
2
|
+
decoder;
|
|
3
|
+
config;
|
|
4
|
+
controller = null;
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
}
|
|
8
|
+
async initialize() {
|
|
9
|
+
if (this.decoder?.state === "configured") {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const isSupported = await this.isConfigSupported(this.config);
|
|
13
|
+
if (!isSupported.supported) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`Codec not supported: ${this.config.codecString || this.config.codec}`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
this.decoder = this.createDecoder({
|
|
19
|
+
output: this.handleOutput.bind(this),
|
|
20
|
+
error: this.handleError.bind(this)
|
|
21
|
+
});
|
|
22
|
+
await this.configureDecoder(this.config);
|
|
23
|
+
}
|
|
24
|
+
async reconfigure(config) {
|
|
25
|
+
this.config = { ...this.config, ...config };
|
|
26
|
+
if (!this.decoder) {
|
|
27
|
+
await this.initialize();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (this.decoder.state === "configured") {
|
|
31
|
+
await this.decoder.flush();
|
|
32
|
+
}
|
|
33
|
+
const isSupported = await this.isConfigSupported(this.config);
|
|
34
|
+
if (!isSupported.supported) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`New configuration not supported: ${this.config.codecString || this.config.codec}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
await this.configureDecoder(this.config);
|
|
40
|
+
}
|
|
41
|
+
async flush() {
|
|
42
|
+
if (!this.decoder) return;
|
|
43
|
+
await this.decoder.flush();
|
|
44
|
+
}
|
|
45
|
+
async reset() {
|
|
46
|
+
if (!this.decoder) return;
|
|
47
|
+
this.decoder.reset();
|
|
48
|
+
this.onReset();
|
|
49
|
+
}
|
|
50
|
+
async close() {
|
|
51
|
+
if (!this.decoder) return;
|
|
52
|
+
if (this.decoder.state === "configured") {
|
|
53
|
+
await this.decoder.flush();
|
|
54
|
+
}
|
|
55
|
+
this.decoder.close();
|
|
56
|
+
this.decoder = void 0;
|
|
57
|
+
}
|
|
58
|
+
get isReady() {
|
|
59
|
+
return this.decoder?.state === "configured";
|
|
60
|
+
}
|
|
61
|
+
get queueSize() {
|
|
62
|
+
return this.decoder?.decodeQueueSize ?? 0;
|
|
63
|
+
}
|
|
64
|
+
handleOutput(data) {
|
|
65
|
+
if (!this.controller) {
|
|
66
|
+
this.closeIfPossible(data);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
this.controller.enqueue(data);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error instanceof TypeError && /Cannot enqueue a chunk into a readable stream that is closed/.test(error.message)) {
|
|
73
|
+
this.closeIfPossible(data);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
handleError(error) {
|
|
80
|
+
console.error(`${this.getDecoderType()} decoder error:`, error);
|
|
81
|
+
this.controller?.error(error);
|
|
82
|
+
}
|
|
83
|
+
closeIfPossible(data) {
|
|
84
|
+
data?.close?.();
|
|
85
|
+
data?.frame?.close?.();
|
|
86
|
+
}
|
|
87
|
+
onReset() {
|
|
88
|
+
}
|
|
89
|
+
createStream() {
|
|
90
|
+
return new TransformStream(
|
|
91
|
+
{
|
|
92
|
+
start: async (controller) => {
|
|
93
|
+
this.controller = controller;
|
|
94
|
+
if (!this.isReady) {
|
|
95
|
+
await this.initialize();
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
transform: async (input) => {
|
|
99
|
+
if (!this.decoder || this.decoder.state !== "configured") {
|
|
100
|
+
throw new Error("Decoder not configured");
|
|
101
|
+
}
|
|
102
|
+
if (this.decoder.decodeQueueSize >= this.decodeQueueThreshold) {
|
|
103
|
+
await new Promise((resolve) => {
|
|
104
|
+
const check = () => {
|
|
105
|
+
if (!this.decoder || this.decoder.decodeQueueSize < this.decodeQueueThreshold - 1) {
|
|
106
|
+
resolve();
|
|
107
|
+
} else {
|
|
108
|
+
setTimeout(check, 10);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
check();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
this.decode(input);
|
|
115
|
+
},
|
|
116
|
+
flush: async () => {
|
|
117
|
+
await this.flush();
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
highWaterMark: this.highWaterMark,
|
|
122
|
+
size: () => 1
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export {
|
|
128
|
+
BaseDecoder
|
|
129
|
+
};
|
|
130
|
+
//# sourceMappingURL=BaseDecoder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BaseDecoder.js","sources":["../../../src/stages/decode/BaseDecoder.ts"],"sourcesContent":["/**\n * Base decoder class abstracting common WebCodecs decoder workflow.\n * Mirrors BaseEncoder but for decoding path.\n */\nimport { AudioDecoderConfig, DecodedVideoFrame, VideoDecoderConfig } from './types';\n\nexport abstract class BaseDecoder<\n TDecoder extends VideoDecoder | AudioDecoder,\n TConfig extends VideoDecoderConfig | AudioDecoderConfig,\n TInput extends EncodedVideoChunk | EncodedAudioChunk,\n TOutput extends VideoFrame | AudioData | DecodedVideoFrame,\n> {\n protected decoder?: TDecoder;\n protected config: TConfig;\n protected controller: TransformStreamDefaultController<TOutput> | null = null;\n\n constructor(config: TConfig) {\n this.config = config;\n }\n\n async initialize(): Promise<void> {\n if (this.decoder?.state === 'configured') {\n return;\n }\n const isSupported = await this.isConfigSupported(this.config);\n if (!isSupported.supported) {\n throw new Error(\n `Codec not supported: ${(this.config as any).codecString || (this.config as any).codec}`\n );\n }\n\n this.decoder = this.createDecoder({\n output: this.handleOutput.bind(this),\n error: this.handleError.bind(this),\n });\n\n await this.configureDecoder(this.config);\n }\n\n async reconfigure(config: Partial<TConfig>): Promise<void> {\n this.config = { ...this.config, ...config } as TConfig;\n\n if (!this.decoder) {\n await this.initialize();\n return;\n }\n\n // Flush before reconfiguring\n if (this.decoder.state === 'configured') {\n await this.decoder.flush();\n }\n\n const isSupported = await this.isConfigSupported(this.config);\n if (!isSupported.supported) {\n throw new Error(\n `New configuration not supported: ${(this.config as any).codecString || (this.config as any).codec}`\n );\n }\n\n await this.configureDecoder(this.config);\n }\n\n async flush(): Promise<void> {\n if (!this.decoder) return;\n await this.decoder.flush();\n }\n\n async reset(): Promise<void> {\n if (!this.decoder) return;\n this.decoder.reset();\n this.onReset();\n }\n\n async close(): Promise<void> {\n if (!this.decoder) return;\n\n if (this.decoder.state === 'configured') {\n await this.decoder.flush();\n }\n\n this.decoder.close();\n this.decoder = undefined;\n }\n\n get isReady(): boolean {\n return this.decoder?.state === 'configured';\n }\n\n get queueSize(): number {\n return (this.decoder as any)?.decodeQueueSize ?? 0;\n }\n\n protected handleOutput(data: TOutput): void {\n if (!this.controller) {\n this.closeIfPossible(data);\n return;\n }\n\n try {\n this.controller.enqueue(data);\n } catch (error) {\n if (\n error instanceof TypeError &&\n /Cannot enqueue a chunk into a readable stream that is closed/.test(error.message)\n ) {\n this.closeIfPossible(data);\n return;\n }\n\n throw error;\n }\n }\n\n protected handleError(error: DOMException): void {\n console.error(`${this.getDecoderType()} decoder error:`, error);\n this.controller?.error(error);\n }\n\n private closeIfPossible(data: TOutput): void {\n (data as any)?.close?.();\n (data as DecodedVideoFrame)?.frame?.close?.();\n }\n\n // Abstracts\n protected abstract isConfigSupported(config: TConfig): Promise<{ supported: boolean }>;\n protected abstract createDecoder(init: DecoderInit): TDecoder;\n protected abstract configureDecoder(config: TConfig): Promise<void>;\n protected abstract getDecoderType(): string;\n protected abstract decode(input: TInput): void;\n protected onReset(): void {}\n\n // Backpressure\n protected abstract readonly highWaterMark: number;\n protected abstract readonly decodeQueueThreshold: number;\n\n createStream(): TransformStream<TInput, TOutput> {\n return new TransformStream<TInput, TOutput>(\n {\n start: async (controller) => {\n this.controller = controller;\n if (!this.isReady) {\n await this.initialize();\n }\n },\n transform: async (input) => {\n if (!this.decoder || this.decoder.state !== 'configured') {\n throw new Error('Decoder not configured');\n }\n // backpressure\n if ((this.decoder as any).decodeQueueSize >= this.decodeQueueThreshold) {\n await new Promise<void>((resolve) => {\n const check = () => {\n if (\n !this.decoder ||\n (this.decoder as any).decodeQueueSize < this.decodeQueueThreshold - 1\n ) {\n resolve();\n } else {\n setTimeout(check, 10);\n }\n };\n check();\n });\n }\n this.decode(input);\n },\n flush: async () => {\n await this.flush();\n },\n },\n {\n highWaterMark: this.highWaterMark,\n size: () => 1,\n }\n );\n }\n}\n\ninterface DecoderInit {\n output: (data: any) => void;\n error: (error: DOMException) => void;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { BaseDecoder } from "./BaseDecoder.js";
|
|
2
|
+
class VideoChunkDecoder extends BaseDecoder {
|
|
3
|
+
static DEFAULT_HIGH_WATER_MARK = 4;
|
|
4
|
+
static DEFAULT_DECODE_QUEUE_THRESHOLD = 16;
|
|
5
|
+
trackId;
|
|
6
|
+
highWaterMark;
|
|
7
|
+
decodeQueueThreshold;
|
|
8
|
+
// GOP tracking (serial is derived from keyframe timestamp for idempotency)
|
|
9
|
+
currentGopSerial = -1;
|
|
10
|
+
isCurrentFrameKeyframe = false;
|
|
11
|
+
// Buffering support for delayed configuration
|
|
12
|
+
bufferedChunks = [];
|
|
13
|
+
isProcessingBuffer = false;
|
|
14
|
+
constructor(trackId, config) {
|
|
15
|
+
super(config || {});
|
|
16
|
+
this.trackId = trackId;
|
|
17
|
+
this.highWaterMark = config?.backpressure?.highWaterMark ?? VideoChunkDecoder.DEFAULT_HIGH_WATER_MARK;
|
|
18
|
+
this.decodeQueueThreshold = config?.backpressure?.decodeQueueThreshold ?? VideoChunkDecoder.DEFAULT_DECODE_QUEUE_THRESHOLD;
|
|
19
|
+
}
|
|
20
|
+
// Computed properties
|
|
21
|
+
get isConfigured() {
|
|
22
|
+
return this.isReady;
|
|
23
|
+
}
|
|
24
|
+
get state() {
|
|
25
|
+
return this.decoder?.state || "unconfigured";
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Update configuration - can be called before or after initialization
|
|
29
|
+
*/
|
|
30
|
+
async updateConfig(config) {
|
|
31
|
+
if (!this.isReady && config.codec) {
|
|
32
|
+
await this.configure(config);
|
|
33
|
+
await this.processBufferedChunks();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Override createStream to handle GOP tracking and buffering
|
|
37
|
+
// Always create new stream for each clip (ReadableStreams can only be consumed once)
|
|
38
|
+
createStream() {
|
|
39
|
+
return new TransformStream(
|
|
40
|
+
{
|
|
41
|
+
start: async (controller) => {
|
|
42
|
+
this.controller = controller;
|
|
43
|
+
if (this.config?.codec && this.config?.description && !this.isReady) {
|
|
44
|
+
await this.initialize();
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
transform: async (chunk) => {
|
|
48
|
+
if (!this.isReady) {
|
|
49
|
+
this.bufferedChunks.push(chunk);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (this.isProcessingBuffer) {
|
|
53
|
+
this.bufferedChunks.push(chunk);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await this.processChunk(chunk);
|
|
57
|
+
},
|
|
58
|
+
flush: async () => {
|
|
59
|
+
if (this.isReady) {
|
|
60
|
+
await this.flush();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
highWaterMark: this.highWaterMark,
|
|
66
|
+
size: () => 1
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Process a single chunk (extracted from transform for reuse)
|
|
72
|
+
*/
|
|
73
|
+
async processChunk(chunk) {
|
|
74
|
+
if (!this.decoder) {
|
|
75
|
+
throw new Error("Decoder not initialized");
|
|
76
|
+
}
|
|
77
|
+
if (this.decoder.state !== "configured") {
|
|
78
|
+
console.error("[VideoChunkDecoder] Decoder in unexpected state:", this.decoder.state);
|
|
79
|
+
throw new Error(`Decoder not configured, state: ${this.decoder.state}`);
|
|
80
|
+
}
|
|
81
|
+
if (chunk.type === "key") {
|
|
82
|
+
this.handleKeyFrame(chunk.timestamp);
|
|
83
|
+
}
|
|
84
|
+
if (this.decoder.decodeQueueSize >= this.decodeQueueThreshold) {
|
|
85
|
+
await new Promise((resolve) => {
|
|
86
|
+
const check = () => {
|
|
87
|
+
if (!this.decoder || this.decoder.decodeQueueSize < this.decodeQueueThreshold - 1) {
|
|
88
|
+
resolve();
|
|
89
|
+
} else {
|
|
90
|
+
setTimeout(check, 20);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
check();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
this.decode(chunk);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(`[VideoChunkDecoder] decode error:`, error);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Override handleOutput to attach GOP metadata
|
|
104
|
+
handleOutput(frame) {
|
|
105
|
+
const wrappedFrame = {
|
|
106
|
+
frame,
|
|
107
|
+
gopSerial: this.currentGopSerial,
|
|
108
|
+
isKeyframe: this.isCurrentFrameKeyframe,
|
|
109
|
+
timestamp: frame.timestamp
|
|
110
|
+
};
|
|
111
|
+
this.isCurrentFrameKeyframe = false;
|
|
112
|
+
super.handleOutput(wrappedFrame);
|
|
113
|
+
}
|
|
114
|
+
// Implement abstract methods
|
|
115
|
+
async isConfigSupported(config) {
|
|
116
|
+
const result = await VideoDecoder.isConfigSupported({
|
|
117
|
+
codec: config.codec,
|
|
118
|
+
codedWidth: config.width,
|
|
119
|
+
codedHeight: config.height
|
|
120
|
+
});
|
|
121
|
+
return { supported: result.supported ?? false };
|
|
122
|
+
}
|
|
123
|
+
createDecoder(init) {
|
|
124
|
+
return new VideoDecoder(init);
|
|
125
|
+
}
|
|
126
|
+
getDecoderType() {
|
|
127
|
+
return "Video";
|
|
128
|
+
}
|
|
129
|
+
async configureDecoder(config) {
|
|
130
|
+
if (!this.decoder) return;
|
|
131
|
+
const decoderConfig = {
|
|
132
|
+
codec: config.codec,
|
|
133
|
+
codedWidth: config.width,
|
|
134
|
+
codedHeight: config.height,
|
|
135
|
+
hardwareAcceleration: config.hardwareAcceleration || "no-preference",
|
|
136
|
+
optimizeForLatency: false,
|
|
137
|
+
...config.description && { description: config.description },
|
|
138
|
+
...config.displayAspectWidth && { displayAspectWidth: config.displayAspectWidth },
|
|
139
|
+
...config.displayAspectHeight && { displayAspectHeight: config.displayAspectHeight }
|
|
140
|
+
};
|
|
141
|
+
this.decoder.configure(decoderConfig);
|
|
142
|
+
}
|
|
143
|
+
decode(chunk) {
|
|
144
|
+
this.decoder?.decode(chunk);
|
|
145
|
+
}
|
|
146
|
+
// Override reset to clear GOP data
|
|
147
|
+
onReset() {
|
|
148
|
+
this.currentGopSerial = -1;
|
|
149
|
+
this.isCurrentFrameKeyframe = false;
|
|
150
|
+
}
|
|
151
|
+
// GOP management methods
|
|
152
|
+
handleKeyFrame(timestamp) {
|
|
153
|
+
this.currentGopSerial = timestamp;
|
|
154
|
+
this.isCurrentFrameKeyframe = true;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Configure the decoder with codec info (can be called after creation)
|
|
158
|
+
*/
|
|
159
|
+
async configure(config) {
|
|
160
|
+
if (this.isReady) {
|
|
161
|
+
await this.reconfigure(config);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
this.config = config;
|
|
165
|
+
await this.initialize();
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Process any buffered chunks after configuration
|
|
169
|
+
*/
|
|
170
|
+
async processBufferedChunks() {
|
|
171
|
+
if (!this.isReady || this.bufferedChunks.length === 0) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
this.isProcessingBuffer = true;
|
|
175
|
+
const chunks = [...this.bufferedChunks];
|
|
176
|
+
this.bufferedChunks = [];
|
|
177
|
+
for (const chunk of chunks) {
|
|
178
|
+
try {
|
|
179
|
+
await this.processChunk(chunk);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error("[VideoChunkDecoder] Error processing buffered chunk:", error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
this.isProcessingBuffer = false;
|
|
185
|
+
if (this.bufferedChunks.length > 0) {
|
|
186
|
+
await this.processBufferedChunks();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Override close to clean up GOP data
|
|
190
|
+
async close() {
|
|
191
|
+
this.bufferedChunks = [];
|
|
192
|
+
this.onReset();
|
|
193
|
+
await super.close();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
export {
|
|
197
|
+
VideoChunkDecoder
|
|
198
|
+
};
|
|
199
|
+
//# sourceMappingURL=VideoChunkDecoder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VideoChunkDecoder.js","sources":["../../../src/stages/decode/VideoChunkDecoder.ts"],"sourcesContent":["import { VideoDecoderConfig } from './types';\nimport { BaseDecoder } from './BaseDecoder';\n\n/**\n * Video decoder with GOP tracking\n * Tracks keyframe boundaries and attaches GOP metadata to decoded frames\n */\nexport class VideoChunkDecoder extends BaseDecoder<\n VideoDecoder,\n VideoDecoderConfig,\n EncodedVideoChunk,\n VideoFrame\n> {\n private static readonly DEFAULT_HIGH_WATER_MARK = 4;\n private static readonly DEFAULT_DECODE_QUEUE_THRESHOLD = 16;\n\n readonly trackId: string;\n\n protected readonly highWaterMark: number;\n protected readonly decodeQueueThreshold: number;\n\n // GOP tracking (serial is derived from keyframe timestamp for idempotency)\n private currentGopSerial = -1;\n private isCurrentFrameKeyframe = false;\n\n // Buffering support for delayed configuration\n private bufferedChunks: EncodedVideoChunk[] = [];\n private isProcessingBuffer: boolean = false;\n\n constructor(trackId: string, config?: Partial<VideoDecoderConfig>) {\n super((config || {}) as VideoDecoderConfig);\n\n this.trackId = trackId;\n this.highWaterMark =\n config?.backpressure?.highWaterMark ?? VideoChunkDecoder.DEFAULT_HIGH_WATER_MARK;\n this.decodeQueueThreshold =\n config?.backpressure?.decodeQueueThreshold ??\n VideoChunkDecoder.DEFAULT_DECODE_QUEUE_THRESHOLD;\n }\n\n // Computed properties\n get isConfigured(): boolean {\n return this.isReady;\n }\n\n get state(): string {\n return this.decoder?.state || 'unconfigured';\n }\n\n /**\n * Update configuration - can be called before or after initialization\n */\n async updateConfig(config: Partial<VideoDecoderConfig>): Promise<void> {\n if (!this.isReady && config.codec) {\n await this.configure(config as VideoDecoderConfig);\n await this.processBufferedChunks();\n }\n }\n\n // Override createStream to handle GOP tracking and buffering\n // Always create new stream for each clip (ReadableStreams can only be consumed once)\n override createStream(): TransformStream<EncodedVideoChunk, VideoFrame> {\n return new TransformStream<EncodedVideoChunk, VideoFrame>(\n {\n start: async (controller) => {\n this.controller = controller;\n // Don't initialize if no codec config yet\n if (this.config?.codec && this.config?.description && !this.isReady) {\n await this.initialize();\n }\n },\n\n transform: async (chunk) => {\n // If not configured yet, buffer the chunk\n if (!this.isReady) {\n this.bufferedChunks.push(chunk);\n return; // Don't process yet\n }\n\n // If we're processing buffered chunks, add to buffer to maintain order\n if (this.isProcessingBuffer) {\n this.bufferedChunks.push(chunk);\n return;\n }\n\n // Normal processing\n await this.processChunk(chunk);\n },\n\n flush: async () => {\n if (this.isReady) {\n await this.flush();\n }\n },\n },\n {\n highWaterMark: this.highWaterMark,\n size: () => 1,\n }\n );\n }\n\n /**\n * Process a single chunk (extracted from transform for reuse)\n */\n private async processChunk(chunk: EncodedVideoChunk): Promise<void> {\n if (!this.decoder) {\n throw new Error('Decoder not initialized');\n }\n\n if (this.decoder.state !== 'configured') {\n console.error('[VideoChunkDecoder] Decoder in unexpected state:', this.decoder.state);\n throw new Error(`Decoder not configured, state: ${this.decoder.state}`);\n }\n\n // Track GOP boundaries\n if (chunk.type === 'key') {\n this.handleKeyFrame(chunk.timestamp);\n }\n\n // Check decoder queue pressure\n if (this.decoder.decodeQueueSize >= this.decodeQueueThreshold) {\n await new Promise<void>((resolve) => {\n const check = () => {\n if (!this.decoder || this.decoder.decodeQueueSize < this.decodeQueueThreshold - 1) {\n resolve();\n } else {\n setTimeout(check, 20);\n }\n };\n check();\n });\n }\n\n // Decode the chunk\n try {\n this.decode(chunk);\n } catch (error) {\n console.error(`[VideoChunkDecoder] decode error:`, error);\n throw error; // Re-throw to propagate\n }\n }\n\n // Override handleOutput to attach GOP metadata\n protected override handleOutput(frame: VideoFrame): void {\n // Wrap frame with GOP metadata as a plain object\n // The frame itself will be transferred, but metadata travels separately\n const wrappedFrame = {\n frame,\n gopSerial: this.currentGopSerial,\n isKeyframe: this.isCurrentFrameKeyframe,\n timestamp: frame.timestamp,\n };\n\n // Reset keyframe flag for subsequent frames\n this.isCurrentFrameKeyframe = false;\n\n // Call parent to enqueue wrapped frame\n super.handleOutput(wrappedFrame as any);\n }\n\n // Implement abstract methods\n protected async isConfigSupported(config: VideoDecoderConfig): Promise<{ supported: boolean }> {\n const result = await VideoDecoder.isConfigSupported({\n codec: config.codec,\n codedWidth: config.width,\n codedHeight: config.height,\n });\n return { supported: result.supported ?? false };\n }\n\n protected createDecoder(init: {\n output: (frame: VideoFrame) => void;\n error: (error: DOMException) => void;\n }): VideoDecoder {\n return new VideoDecoder(init);\n }\n\n protected getDecoderType(): string {\n return 'Video';\n }\n\n protected async configureDecoder(config: VideoDecoderConfig): Promise<void> {\n if (!this.decoder) return;\n\n const decoderConfig = {\n codec: config.codec,\n codedWidth: config.width,\n codedHeight: config.height,\n hardwareAcceleration: config.hardwareAcceleration || 'no-preference',\n optimizeForLatency: false,\n ...(config.description && { description: config.description }),\n ...(config.displayAspectWidth && { displayAspectWidth: config.displayAspectWidth }),\n ...(config.displayAspectHeight && { displayAspectHeight: config.displayAspectHeight }),\n };\n\n this.decoder.configure(decoderConfig as any);\n // console.log('[VideoChunkDecoder] Decoder configured, state:', this.decoder.state);\n }\n\n protected decode(chunk: EncodedVideoChunk): void {\n this.decoder?.decode(chunk);\n }\n\n // Override reset to clear GOP data\n protected override onReset(): void {\n this.currentGopSerial = -1;\n this.isCurrentFrameKeyframe = false;\n }\n\n // GOP management methods\n private handleKeyFrame(timestamp: number): void {\n // Use timestamp as gopSerial for idempotency (same keyframe = same serial)\n this.currentGopSerial = timestamp;\n this.isCurrentFrameKeyframe = true;\n }\n\n /**\n * Configure the decoder with codec info (can be called after creation)\n */\n async configure(config: VideoDecoderConfig): Promise<void> {\n // console.log('[VideoChunkDecoder] Configuring with:', config);\n\n if (this.isReady) {\n // If already configured, reconfigure\n await this.reconfigure(config);\n return;\n }\n\n this.config = config as any;\n\n // Initialize decoder with new config\n await this.initialize();\n }\n\n /**\n * Process any buffered chunks after configuration\n */\n async processBufferedChunks(): Promise<void> {\n if (!this.isReady || this.bufferedChunks.length === 0) {\n return;\n }\n\n // console.log('[VideoChunkDecoder] Processing', this.bufferedChunks.length, 'buffered chunks');\n this.isProcessingBuffer = true;\n\n // Process buffered chunks in order\n const chunks = [...this.bufferedChunks];\n this.bufferedChunks = [];\n\n for (const chunk of chunks) {\n try {\n await this.processChunk(chunk);\n } catch (error) {\n console.error('[VideoChunkDecoder] Error processing buffered chunk:', error);\n }\n }\n\n this.isProcessingBuffer = false;\n\n // Process any new chunks that arrived while processing buffer\n if (this.bufferedChunks.length > 0) {\n await this.processBufferedChunks();\n }\n }\n\n // Override close to clean up GOP data\n override async close(): Promise<void> {\n // Clear buffered chunks\n this.bufferedChunks = [];\n\n // Clean up GOP data first\n this.onReset();\n\n // Call parent close\n await super.close();\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResourceLoader.d.ts","sourceRoot":"","sources":["../../../src/stages/load/ResourceLoader.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,QAAQ,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AASlG,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,YAAY,CAAC,CAAe;IACpC,OAAO,CAAC,KAAK,CAAC,CAAmB;IACjC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,aAAa,CAAC,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAC,CAA4B;IAC7C,OAAO,CAAC,aAAa,CAAC,CAAyD;IAC/E,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,gBAAgB,CAA+B;gBAE3C,OAAO,CAAC,EAAE,qBAAqB;IAa3C;;OAEG;IACH,IAAI,CAAC,YAAY,EAAE,YAAY,GAAG,IAAI;IAWtC;;OAEG;IACH,MAAM,IAAI,IAAI;IAMd,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,WAAW;IAoCnB,OAAO,CAAC,uBAAuB;IAU/B,OAAO,CAAC,YAAY;IAQpB;;;OAGG;YACW,SAAS;IAiCvB;;;OAGG;YACW,gBAAgB;IAM9B;;;OAGG;YACW,eAAe;IAyB7B;;OAEG;YACW,SAAS;IAUvB;;OAEG;YACW,qBAAqB;
|
|
1
|
+
{"version":3,"file":"ResourceLoader.d.ts","sourceRoot":"","sources":["../../../src/stages/load/ResourceLoader.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,QAAQ,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AASlG,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,YAAY,CAAC,CAAe;IACpC,OAAO,CAAC,KAAK,CAAC,CAAmB;IACjC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,aAAa,CAAC,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAC,CAA4B;IAC7C,OAAO,CAAC,aAAa,CAAC,CAAyD;IAC/E,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,gBAAgB,CAA+B;gBAE3C,OAAO,CAAC,EAAE,qBAAqB;IAa3C;;OAEG;IACH,IAAI,CAAC,YAAY,EAAE,YAAY,GAAG,IAAI;IAWtC;;OAEG;IACH,MAAM,IAAI,IAAI;IAMd,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,WAAW;IAoCnB,OAAO,CAAC,uBAAuB;IAU/B,OAAO,CAAC,YAAY;IAQpB;;;OAGG;YACW,SAAS;IAiCvB;;;OAGG;YACW,gBAAgB;IAM9B;;;OAGG;YACW,eAAe;IAyB7B;;OAEG;YACW,SAAS;IAUvB;;OAEG;YACW,qBAAqB;IAsBnC;;;OAGG;YACW,mBAAmB;YAuBnB,qBAAqB;IAyBnC,OAAO,CAAC,mBAAmB;IAkBrB,KAAK,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB9E,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAKhC;;OAEG;IACH,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAI9C,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAOzB,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB9E,IAAI,WAAW,IAAI,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAEvC;IAED,IAAI,SAAS,IAAI,QAAQ,EAAE,CAE1B;IAED,OAAO,IAAI,IAAI;CAOhB"}
|
|
@@ -170,10 +170,8 @@ class ResourceLoader {
|
|
|
170
170
|
`[ResourceLoader] sessionId required for resource ${task.resourceId}. In Clip-based architecture, use fetch(resourceId, { sessionId })`
|
|
171
171
|
);
|
|
172
172
|
}
|
|
173
|
-
const composeWorker = await this.orchestrator.workers.get("videoCompose", task.sessionId
|
|
174
|
-
|
|
175
|
-
});
|
|
176
|
-
await composeWorker.send(
|
|
173
|
+
const composeWorker = await this.orchestrator.workers.get("videoCompose", task.sessionId);
|
|
174
|
+
await composeWorker?.send?.(
|
|
177
175
|
"receive_image",
|
|
178
176
|
{
|
|
179
177
|
resourceId: task.resourceId,
|
|
@@ -192,10 +190,8 @@ class ResourceLoader {
|
|
|
192
190
|
if (!blob || !sessionId) return;
|
|
193
191
|
const imageBitmap = await createImageBitmapFromBlob(blob);
|
|
194
192
|
if (!this.orchestrator) return;
|
|
195
|
-
const composeWorker = await this.orchestrator.workers.get("videoCompose", sessionId
|
|
196
|
-
|
|
197
|
-
});
|
|
198
|
-
await composeWorker.send(
|
|
193
|
+
const composeWorker = await this.orchestrator.workers.get("videoCompose", sessionId);
|
|
194
|
+
await composeWorker?.send?.(
|
|
199
195
|
"receive_image",
|
|
200
196
|
{
|
|
201
197
|
resourceId,
|
|
@@ -213,15 +209,17 @@ class ResourceLoader {
|
|
|
213
209
|
);
|
|
214
210
|
}
|
|
215
211
|
const workerType = task.resource.type === "video" ? "videoDemux" : "audioDemux";
|
|
216
|
-
const demuxWorker = await this.orchestrator.workers.get(workerType, task.sessionId
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
212
|
+
const demuxWorker = await this.orchestrator.workers.get(workerType, task.sessionId);
|
|
213
|
+
if (demuxWorker) {
|
|
214
|
+
await demuxWorker.sendStream(task.stream, {
|
|
215
|
+
sessionId: task.sessionId,
|
|
216
|
+
...task.metadata,
|
|
217
|
+
...task.range && { range: task.range },
|
|
218
|
+
...task.trackId && { trackId: task.trackId }
|
|
219
|
+
});
|
|
220
|
+
} else {
|
|
221
|
+
task.stream.cancel();
|
|
222
|
+
}
|
|
225
223
|
}
|
|
226
224
|
updateResourceState(resourceId, state) {
|
|
227
225
|
const resource = this.model?.resources.get(resourceId);
|