@meframe/core 0.0.30 → 0.0.32
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 +2 -2
- package/dist/Meframe.d.ts.map +1 -1
- package/dist/Meframe.js +3 -3
- package/dist/Meframe.js.map +1 -1
- package/dist/_virtual/_commonjsHelpers.js +7 -0
- package/dist/_virtual/_commonjsHelpers.js.map +1 -0
- package/dist/cache/CacheManager.d.ts +12 -17
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +18 -281
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/cache/l1/AudioL1Cache.d.ts +36 -19
- package/dist/cache/l1/AudioL1Cache.d.ts.map +1 -1
- package/dist/cache/l1/AudioL1Cache.js +182 -282
- package/dist/cache/l1/AudioL1Cache.js.map +1 -1
- package/dist/controllers/PlaybackController.d.ts +5 -2
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +60 -13
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/medeo-fe/node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js.map +1 -0
- package/dist/{node_modules → medeo-fe/node_modules}/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js +7 -2
- package/dist/medeo-fe/node_modules/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js.map +1 -0
- package/dist/model/types.d.ts +0 -4
- package/dist/model/types.d.ts.map +1 -1
- package/dist/model/types.js.map +1 -1
- package/dist/orchestrator/ExportScheduler.d.ts +6 -0
- package/dist/orchestrator/ExportScheduler.d.ts.map +1 -1
- package/dist/orchestrator/ExportScheduler.js +45 -66
- package/dist/orchestrator/ExportScheduler.js.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.d.ts +35 -28
- package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.js +212 -421
- package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
- package/dist/orchestrator/OnDemandVideoSession.d.ts +3 -3
- package/dist/orchestrator/OnDemandVideoSession.d.ts.map +1 -1
- package/dist/orchestrator/OnDemandVideoSession.js +4 -4
- package/dist/orchestrator/OnDemandVideoSession.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts +1 -2
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +34 -48
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts +0 -2
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +0 -49
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.d.ts.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.js +13 -18
- package/dist/stages/compose/OfflineAudioMixer.js.map +1 -1
- package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
- package/dist/stages/compose/VideoComposer.js +4 -0
- package/dist/stages/compose/VideoComposer.js.map +1 -1
- package/dist/stages/decode/AudioChunkDecoder.js +169 -0
- package/dist/stages/decode/AudioChunkDecoder.js.map +1 -0
- package/dist/stages/demux/MP3FrameParser.js +186 -0
- package/dist/stages/demux/MP3FrameParser.js.map +1 -0
- package/dist/stages/demux/MP4Demuxer.js +6 -7
- package/dist/stages/demux/MP4Demuxer.js.map +1 -1
- package/dist/stages/demux/MP4IndexParser.js +3 -4
- package/dist/stages/demux/MP4IndexParser.js.map +1 -1
- package/dist/stages/load/ResourceLoader.d.ts +20 -10
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +92 -139
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/stages/load/index.d.ts +0 -1
- package/dist/stages/load/index.d.ts.map +1 -1
- package/dist/stages/load/types.d.ts +1 -0
- package/dist/stages/load/types.d.ts.map +1 -1
- package/dist/stages/mux/MP4Muxer.js +1 -1
- package/dist/utils/audio-data.d.ts +16 -0
- package/dist/utils/audio-data.d.ts.map +1 -0
- package/dist/utils/audio-data.js +111 -0
- package/dist/utils/audio-data.js.map +1 -0
- package/dist/utils/mp4box.d.ts +4 -0
- package/dist/utils/mp4box.d.ts.map +1 -0
- package/dist/utils/mp4box.js +17 -0
- package/dist/utils/mp4box.js.map +1 -0
- package/dist/workers/{MP4Demuxer.BEa6PLJm.js → MP4Demuxer.DxMpB08B.js} +49 -11
- package/dist/workers/MP4Demuxer.DxMpB08B.js.map +1 -0
- package/dist/workers/stages/compose/{video-compose.worker.DHQ8B105.js → video-compose.worker.BhpN-lxf.js} +5 -1
- package/dist/workers/stages/compose/video-compose.worker.BhpN-lxf.js.map +1 -0
- package/dist/workers/stages/demux/{audio-demux.worker._VRQdLdv.js → audio-demux.worker.Fd8sRTYi.js} +2 -2
- package/dist/workers/stages/demux/{audio-demux.worker._VRQdLdv.js.map → audio-demux.worker.Fd8sRTYi.js.map} +1 -1
- package/dist/workers/stages/demux/{video-demux.worker.CSkxGtmx.js → video-demux.worker.DqFOe12v.js} +2 -2
- package/dist/workers/stages/demux/{video-demux.worker.CSkxGtmx.js.map → video-demux.worker.DqFOe12v.js.map} +1 -1
- package/dist/workers/worker-manifest.json +3 -3
- package/package.json +1 -1
- package/dist/cache/resource/ImageBitmapCache.d.ts +0 -65
- package/dist/cache/resource/ImageBitmapCache.d.ts.map +0 -1
- package/dist/cache/resource/ImageBitmapCache.js +0 -101
- package/dist/cache/resource/ImageBitmapCache.js.map +0 -1
- package/dist/node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js.map +0 -1
- package/dist/node_modules/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js.map +0 -1
- package/dist/stages/load/WindowByteRangeResolver.d.ts +0 -47
- package/dist/stages/load/WindowByteRangeResolver.d.ts.map +0 -1
- package/dist/stages/load/WindowByteRangeResolver.js +0 -270
- package/dist/stages/load/WindowByteRangeResolver.js.map +0 -1
- package/dist/workers/MP4Demuxer.BEa6PLJm.js.map +0 -1
- package/dist/workers/stages/compose/video-compose.worker.DHQ8B105.js.map +0 -1
- /package/dist/{node_modules → medeo-fe/node_modules}/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MP4IndexParser.js","sources":["../../../src/stages/demux/MP4IndexParser.ts"],"sourcesContent":["import * as MP4Box from 'mp4box';\nimport type { MP4BoxFile } from 'mp4box';\nimport type { MP4Index, VideoTrackIndex, AudioTrackIndex, Sample, GOP } from './types';\nimport type { TimeUs } from '../../model/types';\nimport { MP4Demuxer } from './MP4Demuxer';\n\nexport interface MP4ParseResult {\n index: MP4Index;\n audioSamples?: EncodedAudioChunk[];\n audioMetadata?: AudioDecoderConfig;\n}\n\nexport interface ParseStreamOptions {\n onFirstFrameReady?: (index: MP4Index, firstGOPChunks: EncodedVideoChunk[]) => void;\n}\n\n/**\n * MP4IndexParser - Parse MP4 moov box and build time→byte index\n *\n * Features:\n * - Stream-based moov parsing (stop after moov found)\n * - Build sample table with byte offsets\n * - Build GOP index for keyframe positions\n * - Extract all audio samples (encoded) for memory caching\n * - Extract first GOP for fast cover rendering\n */\nexport class MP4IndexParser {\n /**\n * Parse from streaming download\n * Returns video index + all audio samples\n * Can optionally extract first GOP for fast cover rendering\n */\n async parseFromStream(\n stream: ReadableStream<Uint8Array>,\n options?: ParseStreamOptions\n ): Promise<MP4ParseResult> {\n const mp4boxFile = MP4Box.createFile();\n let resolveResult: ((result: MP4ParseResult) => void) | null = null;\n let rejectResult: ((error: Error) => void) | null = null;\n\n const audioChunks: EncodedAudioChunk[] = [];\n let audioConfig: AudioDecoderConfig | undefined;\n let audioTrackId: number | null = null;\n let expectedAudioSamples = 0;\n let savedInfo: any = null;\n\n const resultPromise = new Promise<MP4ParseResult>((resolve, reject) => {\n resolveResult = resolve;\n rejectResult = reject;\n });\n\n // First GOP extraction state\n const firstGOPState = {\n byteEnd: 0,\n extracted: !options?.onFirstFrameReady,\n index: null as MP4Index | null,\n buffer: new Uint8Array(0),\n };\n\n const streamState = {\n fileOffset: 0,\n moovParsed: false,\n audioComplete: false,\n };\n\n mp4boxFile.onError = (error: string) => {\n rejectResult?.(new Error(`MP4Box error: ${error}`));\n };\n\n mp4boxFile.onReady = (info: any) => {\n try {\n // Set moovParsed immediately\n streamState.moovParsed = true;\n\n // Save info for later use in audio extraction\n savedInfo = info;\n\n // Build video index\n const index = this.buildIndex(mp4boxFile, info);\n firstGOPState.index = index;\n\n // Check if this is a fast-start format (moov at beginning)\n const isFastStart = info.isProgressive === true;\n\n // Only extract first GOP for fast-start videos (moov-at-end videos will read from OPFS)\n if (isFastStart && options?.onFirstFrameReady && index.tracks.video?.gopIndex[0]) {\n const firstGOP = index.tracks.video.gopIndex[0];\n const samples = index.tracks.video.samples;\n const startIdx = firstGOP.keyframeSampleIndex;\n const endIdx = startIdx + firstGOP.sampleCount;\n\n const endSample = samples[endIdx - 1];\n if (endSample) {\n firstGOPState.byteEnd = endSample.byteOffset + endSample.byteLength;\n }\n }\n\n // Setup audio extraction if audio track exists\n const { config, trackId, expectedSamples, isComplete } = this.setupAudioExtraction(\n mp4boxFile,\n info\n );\n audioConfig = config;\n audioTrackId = trackId ?? null;\n expectedAudioSamples = expectedSamples;\n streamState.audioComplete = isComplete;\n\n if (isComplete) {\n // No audio track, resolve immediately if firstGOP extracted or not needed\n // Use setTimeout to allow stack to unwind and ensure consistency\n setTimeout(() => {\n if (streamState.moovParsed && streamState.audioComplete && firstGOPState.extracted) {\n resolveResult?.({ index });\n }\n }, 0);\n }\n } catch (error) {\n rejectResult?.(error as Error);\n }\n };\n\n // Handle audio sample extraction\n mp4boxFile.onSamples = (trackId: number, _user: any, samples: any[]) => {\n if (trackId === audioTrackId && audioConfig) {\n const timescale = audioConfig.sampleRate;\n\n // Process samples if any\n if (samples && samples.length > 0) {\n for (const sample of samples) {\n try {\n const chunk = new EncodedAudioChunk({\n type: sample.is_sync ? 'key' : 'delta',\n timestamp: Math.round((sample.cts / timescale) * 1_000_000),\n duration: Math.round((sample.duration / timescale) * 1_000_000),\n data: sample.data,\n });\n audioChunks.push(chunk);\n } catch (error) {\n console.warn('[MP4IndexParser] Failed to create audio chunk:', error);\n }\n }\n }\n\n // Check if we have all samples\n if (expectedAudioSamples !== Infinity && audioChunks.length >= expectedAudioSamples) {\n streamState.audioComplete = true;\n }\n }\n };\n\n await this.processStreamData(stream, mp4boxFile, options, firstGOPState, streamState);\n\n // Flush remaining data\n mp4boxFile.flush();\n\n // Wait for MP4Box to complete sample extraction (onReady/onSamples are async)\n // Reference: MP4Demuxer.ts line 302\n await new Promise((resolve) => setTimeout(resolve, 100));\n\n // If audio extraction finished normally (or there was no audio), resolve now\n if (streamState.moovParsed && streamState.audioComplete) {\n const index = firstGOPState.index || this.buildIndex(mp4boxFile, savedInfo);\n resolveResult!({\n index,\n audioSamples: audioChunks,\n audioMetadata: audioConfig,\n });\n }\n // If moov is parsed but audio extraction hasn't completed yet, force complete\n else if (streamState.moovParsed && !streamState.audioComplete && savedInfo) {\n streamState.audioComplete = true;\n const index = this.buildIndex(mp4boxFile, savedInfo);\n\n // Check if we have audio chunks\n if (audioChunks.length > 0 && audioConfig) {\n resolveResult!({\n index,\n audioSamples: audioChunks,\n audioMetadata: audioConfig,\n });\n } else {\n // No audio or extraction failed, just return index\n resolveResult!({ index });\n }\n }\n\n // Wait for result with timeout (5s max)\n const parseTimeout = new Promise<never>((_, reject) => {\n setTimeout(() => {\n reject(\n new Error(\n `MP4Box parsing timeout after reading ${streamState.fileOffset} bytes. ` +\n `moovParsed=${streamState.moovParsed}, audioExtractionComplete=${streamState.audioComplete}`\n )\n );\n }, 5000);\n });\n\n // Wait for either resultPromise or timeout\n return await Promise.race([resultPromise, parseTimeout]);\n }\n\n private async processStreamData(\n stream: ReadableStream<Uint8Array>,\n mp4boxFile: MP4BoxFile,\n options: ParseStreamOptions | undefined,\n firstGOPState: any,\n streamState: any\n ): Promise<void> {\n const reader = stream.getReader();\n\n try {\n let hasData = false;\n\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n if (value) {\n hasData = true;\n const buffer = this.prepareBuffer(value, streamState.fileOffset);\n\n this.handleFirstGOPExtraction(buffer, value, options, firstGOPState, streamState);\n\n mp4boxFile.appendBuffer(buffer);\n streamState.fileOffset += buffer.byteLength;\n\n if (streamState.moovParsed && streamState.audioComplete && firstGOPState.extracted) {\n break;\n }\n }\n }\n\n if (!hasData) {\n throw new Error('Empty stream received');\n }\n } finally {\n reader.releaseLock();\n }\n }\n\n private prepareBuffer(value: Uint8Array, fileOffset: number): ArrayBuffer {\n let buffer: ArrayBuffer;\n if (value.byteOffset === 0 && value.byteLength === value.buffer.byteLength) {\n buffer = value.buffer as ArrayBuffer;\n } else {\n buffer = value.buffer.slice(\n value.byteOffset,\n value.byteOffset + value.byteLength\n ) as ArrayBuffer;\n }\n (buffer as any).fileStart = fileOffset;\n return buffer;\n }\n\n private handleFirstGOPExtraction(\n buffer: ArrayBuffer,\n value: Uint8Array,\n options: ParseStreamOptions | undefined,\n firstGOPState: any,\n streamState: any\n ): void {\n if (!firstGOPState.extracted && options?.onFirstFrameReady) {\n // Safety: Cap accumulation to 10MB\n if (firstGOPState.buffer.length + value.length < 10 * 1024 * 1024) {\n const newBuffer = new Uint8Array(firstGOPState.buffer.length + value.length);\n newBuffer.set(firstGOPState.buffer);\n newBuffer.set(value, firstGOPState.buffer.length);\n firstGOPState.buffer = newBuffer;\n } else if (!streamState.moovParsed) {\n firstGOPState.extracted = true;\n firstGOPState.buffer = new Uint8Array(0);\n }\n\n if (\n streamState.moovParsed &&\n firstGOPState.byteEnd > 0 &&\n firstGOPState.index &&\n streamState.fileOffset + buffer.byteLength >= firstGOPState.byteEnd\n ) {\n try {\n const currentIndex = firstGOPState.index as MP4Index;\n const videoTrack = currentIndex.tracks.video;\n\n if (videoTrack) {\n const chunks = this.extractFirstGOP(firstGOPState.buffer, videoTrack, 0);\n options.onFirstFrameReady?.(currentIndex, chunks);\n }\n\n firstGOPState.extracted = true;\n firstGOPState.buffer = new Uint8Array(0);\n } catch (error) {\n console.warn('[MP4IndexParser] Failed to extract first GOP:', error);\n firstGOPState.extracted = true;\n firstGOPState.buffer = new Uint8Array(0);\n }\n }\n }\n }\n\n private setupAudioExtraction(\n mp4boxFile: MP4BoxFile,\n info: any\n ): {\n config?: AudioDecoderConfig;\n trackId?: number;\n expectedSamples: number;\n isComplete: boolean;\n } {\n const audioTrack = info.tracks.find((t: any) => t.type === 'audio');\n if (audioTrack) {\n const trackId: number = audioTrack.id;\n const config = {\n codec: audioTrack.codec.startsWith('mp4a') ? 'mp4a.40.2' : audioTrack.codec,\n sampleRate: audioTrack.audio.sample_rate || audioTrack.timescale,\n numberOfChannels: audioTrack.audio.channel_count,\n };\n\n // Extract all audio samples\n mp4boxFile.setExtractionOptions(trackId, null as any, {\n nbSamples: Infinity, // Extract all samples\n });\n mp4boxFile.start();\n\n return {\n config,\n trackId,\n expectedSamples: audioTrack.nb_samples || Infinity,\n isComplete: false,\n };\n }\n\n return { expectedSamples: 0, isComplete: true };\n }\n\n /**\n * Parse from file/ArrayBuffer (for already cached resources)\n */\n async parseFromFile(data: File | ArrayBuffer): Promise<MP4Index> {\n const mp4boxFile = MP4Box.createFile();\n\n const indexPromise = new Promise<MP4Index>((resolve, reject) => {\n mp4boxFile.onError = (error: string) => {\n reject(new Error(`MP4Box error: ${error}`));\n };\n\n mp4boxFile.onReady = (info: any) => {\n try {\n const index = this.buildIndex(mp4boxFile, info);\n resolve(index);\n } catch (error) {\n reject(error);\n }\n };\n });\n\n // Read file data\n let buffer: ArrayBuffer;\n if (data instanceof File) {\n buffer = await data.arrayBuffer();\n } else {\n buffer = data;\n }\n\n (buffer as any).fileStart = 0;\n mp4boxFile.appendBuffer(buffer);\n mp4boxFile.flush();\n\n return indexPromise;\n }\n\n /**\n * Build MP4Index from mp4box.js parsed data\n */\n private buildIndex(mp4boxFile: MP4BoxFile, info: any): MP4Index {\n const index: MP4Index = {\n resourceId: '', // Will be set by caller\n moovOffset: 0, // mp4box doesn't expose this directly\n moovSize: 0,\n durationUs: (info.duration / info.timescale) * 1_000_000,\n tracks: {},\n };\n\n for (const trackInfo of info.tracks) {\n if (trackInfo.type === 'video') {\n index.tracks.video = this.buildVideoTrackIndex(mp4boxFile, trackInfo);\n } else if (trackInfo.type === 'audio') {\n index.tracks.audio = this.buildAudioTrackIndex(mp4boxFile, trackInfo);\n }\n }\n\n return index;\n }\n\n /**\n * Build video track index with sample table and GOP boundaries\n */\n private buildVideoTrackIndex(mp4boxFile: MP4BoxFile, trackInfo: any): VideoTrackIndex {\n const samples = this.buildSampleTable(mp4boxFile, trackInfo.id, trackInfo.timescale);\n const gopIndex = this.buildGOPIndex(samples);\n const description = this.getVideoDescription(mp4boxFile, trackInfo);\n\n return {\n trackId: trackInfo.id,\n codec: trackInfo.codec,\n width: trackInfo.track_width || trackInfo.video?.width || 0,\n height: trackInfo.track_height || trackInfo.video?.height || 0,\n timescale: trackInfo.timescale,\n description,\n samples,\n gopIndex,\n };\n }\n\n /**\n * Build audio track index\n */\n private buildAudioTrackIndex(mp4boxFile: MP4BoxFile, trackInfo: any): AudioTrackIndex {\n const samples = this.buildSampleTable(mp4boxFile, trackInfo.id, trackInfo.timescale);\n\n return {\n trackId: trackInfo.id,\n codec: trackInfo.codec,\n sampleRate: trackInfo.audio?.sample_rate || 48000,\n numberOfChannels: trackInfo.audio?.channel_count || 2,\n timescale: trackInfo.timescale,\n samples,\n };\n }\n\n /**\n * Build sample table from mp4box track samples\n *\n * IMPORTANT: Keep samples in DTS (decode) order, not PTS (presentation) order!\n * VideoDecoder requires chunks in decode order. It will output frames in PTS order.\n */\n private buildSampleTable(mp4boxFile: MP4BoxFile, trackId: number, timescale: number): Sample[] {\n const samples: Sample[] = [];\n\n // Get track box\n const trak = mp4boxFile.getTrackById(trackId);\n if (!trak) return samples;\n\n // Access sample table (already in DTS order)\n const sampleTable = trak.samples || [];\n\n // Calculate PTS from CTS and normalize to start at 0\n let timestampOffset: number | null = null;\n\n for (const sample of sampleTable) {\n const durationUs = ((sample.duration || 0) / timescale) * 1_000_000;\n\n // Use CTS (Composition Time Stamp = PTS) for display timestamp\n const rawTimestampUs = ((sample.cts || 0) / timescale) * 1_000_000;\n\n // Normalize: first frame starts at 0 (like MP4Demuxer does)\n if (timestampOffset === null) {\n timestampOffset = rawTimestampUs;\n }\n const timestampUs = rawTimestampUs - timestampOffset;\n\n samples.push({\n timestamp: timestampUs, // Normalized PTS (display timestamp)\n duration: durationUs,\n byteOffset: sample.offset || 0,\n byteLength: sample.size || 0,\n isKeyframe: sample.is_sync || false,\n });\n }\n\n // DO NOT SORT! Samples must stay in DTS (decode) order for VideoDecoder\n // Decoder will output frames in PTS order automatically\n\n return samples;\n }\n\n /**\n * Extract video description (avcC/hvcC/etc) for VideoDecoder\n * Reuses MP4Demuxer.extractVideoDescription for consistency\n */\n private getVideoDescription(mp4boxFile: MP4BoxFile, trackInfo: any): ArrayBuffer | undefined {\n return MP4Demuxer.extractVideoDescription(mp4boxFile, trackInfo.id);\n }\n\n /**\n * Build GOP index from samples\n * GOP = Group of Pictures, starts with a keyframe\n */\n private buildGOPIndex(samples: Sample[]): GOP[] {\n const gopIndex: GOP[] = [];\n let currentGOP: {\n startTimeUs: TimeUs;\n keyframeSampleIndex: number;\n sampleCount: number;\n } | null = null;\n\n for (let i = 0; i < samples.length; i++) {\n const sample = samples[i];\n if (!sample) continue;\n\n if (sample.isKeyframe) {\n // Save previous GOP if exists\n if (currentGOP) {\n gopIndex.push(currentGOP);\n }\n\n // Start new GOP\n currentGOP = {\n startTimeUs: sample.timestamp,\n keyframeSampleIndex: i,\n sampleCount: 1,\n };\n } else if (currentGOP) {\n // Add sample to current GOP\n currentGOP.sampleCount++;\n }\n }\n\n // Save last GOP\n if (currentGOP) {\n gopIndex.push(currentGOP);\n }\n\n return gopIndex;\n }\n\n /**\n * Extract first GOP chunks from accumulated buffer\n * Used for fast cover rendering during streaming download\n */\n private extractFirstGOP(\n buffer: Uint8Array,\n videoTrack: VideoTrackIndex,\n byteStart: number\n ): EncodedVideoChunk[] {\n const chunks: EncodedVideoChunk[] = [];\n const firstGOP = videoTrack.gopIndex[0];\n if (!firstGOP) {\n return chunks;\n }\n\n const samples = videoTrack.samples;\n\n for (let i = 0; i < firstGOP.sampleCount; i++) {\n const sampleIdx = firstGOP.keyframeSampleIndex + i;\n const sample = samples[sampleIdx];\n if (!sample) continue;\n\n const relativeOffset = sample.byteOffset - byteStart;\n\n // Validate offset is within buffer\n if (relativeOffset < 0 || relativeOffset + sample.byteLength > buffer.length) {\n console.warn('[MP4IndexParser] Sample outside buffer:', {\n sampleOffset: sample.byteOffset,\n sampleLength: sample.byteLength,\n byteStart,\n relativeOffset,\n bufferLength: buffer.length,\n });\n continue;\n }\n\n const sampleData = buffer.slice(relativeOffset, relativeOffset + sample.byteLength);\n\n try {\n chunks.push(\n new EncodedVideoChunk({\n type: sample.isKeyframe ? 'key' : 'delta',\n timestamp: sample.timestamp,\n duration: sample.duration,\n data: sampleData,\n })\n );\n } catch (error) {\n console.warn('[MP4IndexParser] Failed to create EncodedVideoChunk:', error);\n }\n }\n\n return chunks;\n }\n}\n"],"names":["MP4Box.createFile"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MP4IndexParser.js","sources":["../../../src/stages/demux/MP4IndexParser.ts"],"sourcesContent":["import { MP4Box } from '../../utils/mp4box';\nimport type { MP4BoxFile } from 'mp4box';\nimport type { MP4Index, VideoTrackIndex, AudioTrackIndex, Sample, GOP } from './types';\nimport type { TimeUs } from '../../model/types';\nimport { MP4Demuxer } from './MP4Demuxer';\n\nexport interface MP4ParseResult {\n index: MP4Index;\n audioSamples?: EncodedAudioChunk[];\n audioMetadata?: AudioDecoderConfig;\n}\n\nexport interface ParseStreamOptions {\n onFirstFrameReady?: (index: MP4Index, firstGOPChunks: EncodedVideoChunk[]) => void;\n}\n\n/**\n * MP4IndexParser - Parse MP4 moov box and build time→byte index\n *\n * Features:\n * - Stream-based moov parsing (stop after moov found)\n * - Build sample table with byte offsets\n * - Build GOP index for keyframe positions\n * - Extract all audio samples (encoded) for memory caching\n * - Extract first GOP for fast cover rendering\n */\nexport class MP4IndexParser {\n /**\n * Parse from streaming download\n * Returns video index + all audio samples\n * Can optionally extract first GOP for fast cover rendering\n */\n async parseFromStream(\n stream: ReadableStream<Uint8Array>,\n options?: ParseStreamOptions\n ): Promise<MP4ParseResult> {\n const mp4boxFile = MP4Box.createFile();\n let resolveResult: ((result: MP4ParseResult) => void) | null = null;\n let rejectResult: ((error: Error) => void) | null = null;\n\n const audioChunks: EncodedAudioChunk[] = [];\n let audioConfig: AudioDecoderConfig | undefined;\n let audioTrackId: number | null = null;\n let expectedAudioSamples = 0;\n let savedInfo: any = null;\n\n const resultPromise = new Promise<MP4ParseResult>((resolve, reject) => {\n resolveResult = resolve;\n rejectResult = reject;\n });\n\n // First GOP extraction state\n const firstGOPState = {\n byteEnd: 0,\n extracted: !options?.onFirstFrameReady,\n index: null as MP4Index | null,\n buffer: new Uint8Array(0),\n };\n\n const streamState = {\n fileOffset: 0,\n moovParsed: false,\n audioComplete: false,\n };\n\n mp4boxFile.onError = (error: string) => {\n rejectResult?.(new Error(`MP4Box error: ${error}`));\n };\n\n mp4boxFile.onReady = (info: any) => {\n try {\n // Set moovParsed immediately\n streamState.moovParsed = true;\n\n // Save info for later use in audio extraction\n savedInfo = info;\n\n // Build video index\n const index = this.buildIndex(mp4boxFile, info);\n firstGOPState.index = index;\n\n // Check if this is a fast-start format (moov at beginning)\n const isFastStart = info.isProgressive === true;\n\n // Only extract first GOP for fast-start videos (moov-at-end videos will read from OPFS)\n if (isFastStart && options?.onFirstFrameReady && index.tracks.video?.gopIndex[0]) {\n const firstGOP = index.tracks.video.gopIndex[0];\n const samples = index.tracks.video.samples;\n const startIdx = firstGOP.keyframeSampleIndex;\n const endIdx = startIdx + firstGOP.sampleCount;\n\n const endSample = samples[endIdx - 1];\n if (endSample) {\n firstGOPState.byteEnd = endSample.byteOffset + endSample.byteLength;\n }\n }\n\n // Setup audio extraction if audio track exists\n const { config, trackId, expectedSamples, isComplete } = this.setupAudioExtraction(\n mp4boxFile,\n info\n );\n audioConfig = config;\n audioTrackId = trackId ?? null;\n expectedAudioSamples = expectedSamples;\n streamState.audioComplete = isComplete;\n\n if (isComplete) {\n // No audio track, resolve immediately if firstGOP extracted or not needed\n // Use setTimeout to allow stack to unwind and ensure consistency\n setTimeout(() => {\n if (streamState.moovParsed && streamState.audioComplete && firstGOPState.extracted) {\n resolveResult?.({ index });\n }\n }, 0);\n }\n } catch (error) {\n rejectResult?.(error as Error);\n }\n };\n\n // Handle audio sample extraction\n mp4boxFile.onSamples = (trackId: number, _user: any, samples: any[]) => {\n if (trackId === audioTrackId && audioConfig) {\n const timescale = audioConfig.sampleRate;\n\n // Process samples if any\n if (samples && samples.length > 0) {\n for (const sample of samples) {\n try {\n const chunk = new EncodedAudioChunk({\n type: sample.is_sync ? 'key' : 'delta',\n timestamp: Math.round((sample.cts / timescale) * 1_000_000),\n duration: Math.round((sample.duration / timescale) * 1_000_000),\n data: sample.data,\n });\n audioChunks.push(chunk);\n } catch (error) {\n console.warn('[MP4IndexParser] Failed to create audio chunk:', error);\n }\n }\n }\n\n // Check if we have all samples\n if (expectedAudioSamples !== Infinity && audioChunks.length >= expectedAudioSamples) {\n streamState.audioComplete = true;\n }\n }\n };\n\n await this.processStreamData(stream, mp4boxFile, options, firstGOPState, streamState);\n\n // Flush remaining data\n mp4boxFile.flush();\n\n // Wait for MP4Box to complete sample extraction (onReady/onSamples are async)\n // Reference: MP4Demuxer.ts line 302\n await new Promise((resolve) => setTimeout(resolve, 100));\n\n // If audio extraction finished normally (or there was no audio), resolve now\n if (streamState.moovParsed && streamState.audioComplete) {\n const index = firstGOPState.index || this.buildIndex(mp4boxFile, savedInfo);\n resolveResult!({\n index,\n audioSamples: audioChunks,\n audioMetadata: audioConfig,\n });\n }\n // If moov is parsed but audio extraction hasn't completed yet, force complete\n else if (streamState.moovParsed && !streamState.audioComplete && savedInfo) {\n streamState.audioComplete = true;\n const index = this.buildIndex(mp4boxFile, savedInfo);\n\n // Check if we have audio chunks\n if (audioChunks.length > 0 && audioConfig) {\n resolveResult!({\n index,\n audioSamples: audioChunks,\n audioMetadata: audioConfig,\n });\n } else {\n // No audio or extraction failed, just return index\n resolveResult!({ index });\n }\n }\n\n // Wait for result with timeout (5s max)\n const parseTimeout = new Promise<never>((_, reject) => {\n setTimeout(() => {\n reject(\n new Error(\n `MP4Box parsing timeout after reading ${streamState.fileOffset} bytes. ` +\n `moovParsed=${streamState.moovParsed}, audioExtractionComplete=${streamState.audioComplete}`\n )\n );\n }, 5000);\n });\n\n // Wait for either resultPromise or timeout\n return await Promise.race([resultPromise, parseTimeout]);\n }\n\n private async processStreamData(\n stream: ReadableStream<Uint8Array>,\n mp4boxFile: MP4BoxFile,\n options: ParseStreamOptions | undefined,\n firstGOPState: any,\n streamState: any\n ): Promise<void> {\n const reader = stream.getReader();\n\n try {\n let hasData = false;\n\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n if (value) {\n hasData = true;\n const buffer = this.prepareBuffer(value, streamState.fileOffset);\n\n this.handleFirstGOPExtraction(buffer, value, options, firstGOPState, streamState);\n\n mp4boxFile.appendBuffer(buffer);\n streamState.fileOffset += buffer.byteLength;\n\n if (streamState.moovParsed && streamState.audioComplete && firstGOPState.extracted) {\n break;\n }\n }\n }\n\n if (!hasData) {\n throw new Error('Empty stream received');\n }\n } finally {\n reader.releaseLock();\n }\n }\n\n private prepareBuffer(value: Uint8Array, fileOffset: number): ArrayBuffer {\n let buffer: ArrayBuffer;\n if (value.byteOffset === 0 && value.byteLength === value.buffer.byteLength) {\n buffer = value.buffer as ArrayBuffer;\n } else {\n buffer = value.buffer.slice(\n value.byteOffset,\n value.byteOffset + value.byteLength\n ) as ArrayBuffer;\n }\n (buffer as any).fileStart = fileOffset;\n return buffer;\n }\n\n private handleFirstGOPExtraction(\n buffer: ArrayBuffer,\n value: Uint8Array,\n options: ParseStreamOptions | undefined,\n firstGOPState: any,\n streamState: any\n ): void {\n if (!firstGOPState.extracted && options?.onFirstFrameReady) {\n // Safety: Cap accumulation to 10MB\n if (firstGOPState.buffer.length + value.length < 10 * 1024 * 1024) {\n const newBuffer = new Uint8Array(firstGOPState.buffer.length + value.length);\n newBuffer.set(firstGOPState.buffer);\n newBuffer.set(value, firstGOPState.buffer.length);\n firstGOPState.buffer = newBuffer;\n } else if (!streamState.moovParsed) {\n firstGOPState.extracted = true;\n firstGOPState.buffer = new Uint8Array(0);\n }\n\n if (\n streamState.moovParsed &&\n firstGOPState.byteEnd > 0 &&\n firstGOPState.index &&\n streamState.fileOffset + buffer.byteLength >= firstGOPState.byteEnd\n ) {\n try {\n const currentIndex = firstGOPState.index as MP4Index;\n const videoTrack = currentIndex.tracks.video;\n\n if (videoTrack) {\n const chunks = this.extractFirstGOP(firstGOPState.buffer, videoTrack, 0);\n options.onFirstFrameReady?.(currentIndex, chunks);\n }\n\n firstGOPState.extracted = true;\n firstGOPState.buffer = new Uint8Array(0);\n } catch (error) {\n console.warn('[MP4IndexParser] Failed to extract first GOP:', error);\n firstGOPState.extracted = true;\n firstGOPState.buffer = new Uint8Array(0);\n }\n }\n }\n }\n\n private setupAudioExtraction(\n mp4boxFile: MP4BoxFile,\n info: any\n ): {\n config?: AudioDecoderConfig;\n trackId?: number;\n expectedSamples: number;\n isComplete: boolean;\n } {\n const audioTrack = info.tracks.find((t: any) => t.type === 'audio');\n if (audioTrack) {\n const trackId: number = audioTrack.id;\n const config = {\n codec: audioTrack.codec.startsWith('mp4a') ? 'mp4a.40.2' : audioTrack.codec,\n sampleRate: audioTrack.audio.sample_rate || audioTrack.timescale,\n numberOfChannels: audioTrack.audio.channel_count,\n };\n\n // Extract all audio samples\n mp4boxFile.setExtractionOptions(trackId, null as any, {\n nbSamples: Infinity, // Extract all samples\n });\n mp4boxFile.start();\n\n return {\n config,\n trackId,\n expectedSamples: audioTrack.nb_samples || Infinity,\n isComplete: false,\n };\n }\n\n return { expectedSamples: 0, isComplete: true };\n }\n\n /**\n * Parse from file/ArrayBuffer (for already cached resources)\n */\n async parseFromFile(data: File | ArrayBuffer): Promise<MP4Index> {\n const mp4boxFile = MP4Box.createFile();\n\n const indexPromise = new Promise<MP4Index>((resolve, reject) => {\n mp4boxFile.onError = (error: string) => {\n reject(new Error(`MP4Box error: ${error}`));\n };\n\n mp4boxFile.onReady = (info: any) => {\n try {\n const index = this.buildIndex(mp4boxFile, info);\n resolve(index);\n } catch (error) {\n reject(error);\n }\n };\n });\n\n // Read file data\n let buffer: ArrayBuffer;\n if (data instanceof File) {\n buffer = await data.arrayBuffer();\n } else {\n buffer = data;\n }\n\n (buffer as any).fileStart = 0;\n mp4boxFile.appendBuffer(buffer);\n mp4boxFile.flush();\n\n return indexPromise;\n }\n\n /**\n * Build MP4Index from mp4box.js parsed data\n */\n private buildIndex(mp4boxFile: MP4BoxFile, info: any): MP4Index {\n const index: MP4Index = {\n resourceId: '', // Will be set by caller\n moovOffset: 0, // mp4box doesn't expose this directly\n moovSize: 0,\n durationUs: (info.duration / info.timescale) * 1_000_000,\n tracks: {},\n };\n\n for (const trackInfo of info.tracks) {\n if (trackInfo.type === 'video') {\n index.tracks.video = this.buildVideoTrackIndex(mp4boxFile, trackInfo);\n } else if (trackInfo.type === 'audio') {\n index.tracks.audio = this.buildAudioTrackIndex(mp4boxFile, trackInfo);\n }\n }\n\n return index;\n }\n\n /**\n * Build video track index with sample table and GOP boundaries\n */\n private buildVideoTrackIndex(mp4boxFile: MP4BoxFile, trackInfo: any): VideoTrackIndex {\n const samples = this.buildSampleTable(mp4boxFile, trackInfo.id, trackInfo.timescale);\n const gopIndex = this.buildGOPIndex(samples);\n const description = this.getVideoDescription(mp4boxFile, trackInfo);\n\n return {\n trackId: trackInfo.id,\n codec: trackInfo.codec,\n width: trackInfo.track_width || trackInfo.video?.width || 0,\n height: trackInfo.track_height || trackInfo.video?.height || 0,\n timescale: trackInfo.timescale,\n description,\n samples,\n gopIndex,\n };\n }\n\n /**\n * Build audio track index\n */\n private buildAudioTrackIndex(mp4boxFile: MP4BoxFile, trackInfo: any): AudioTrackIndex {\n const samples = this.buildSampleTable(mp4boxFile, trackInfo.id, trackInfo.timescale);\n\n return {\n trackId: trackInfo.id,\n codec: trackInfo.codec,\n sampleRate: trackInfo.audio?.sample_rate || 48000,\n numberOfChannels: trackInfo.audio?.channel_count || 2,\n timescale: trackInfo.timescale,\n samples,\n };\n }\n\n /**\n * Build sample table from mp4box track samples\n *\n * IMPORTANT: Keep samples in DTS (decode) order, not PTS (presentation) order!\n * VideoDecoder requires chunks in decode order. It will output frames in PTS order.\n */\n private buildSampleTable(mp4boxFile: MP4BoxFile, trackId: number, timescale: number): Sample[] {\n const samples: Sample[] = [];\n\n // Get track box\n const trak = mp4boxFile.getTrackById(trackId);\n if (!trak) return samples;\n\n // Access sample table (already in DTS order)\n const sampleTable = trak.samples || [];\n\n // Calculate PTS from CTS and normalize to start at 0\n let timestampOffset: number | null = null;\n\n for (const sample of sampleTable) {\n const durationUs = ((sample.duration || 0) / timescale) * 1_000_000;\n\n // Use CTS (Composition Time Stamp = PTS) for display timestamp\n const rawTimestampUs = ((sample.cts || 0) / timescale) * 1_000_000;\n\n // Normalize: first frame starts at 0 (like MP4Demuxer does)\n if (timestampOffset === null) {\n timestampOffset = rawTimestampUs;\n }\n const timestampUs = rawTimestampUs - timestampOffset;\n\n samples.push({\n timestamp: timestampUs, // Normalized PTS (display timestamp)\n duration: durationUs,\n byteOffset: sample.offset || 0,\n byteLength: sample.size || 0,\n isKeyframe: sample.is_sync || false,\n });\n }\n\n // DO NOT SORT! Samples must stay in DTS (decode) order for VideoDecoder\n // Decoder will output frames in PTS order automatically\n\n return samples;\n }\n\n /**\n * Extract video description (avcC/hvcC/etc) for VideoDecoder\n * Reuses MP4Demuxer.extractVideoDescription for consistency\n */\n private getVideoDescription(mp4boxFile: MP4BoxFile, trackInfo: any): ArrayBuffer | undefined {\n return MP4Demuxer.extractVideoDescription(mp4boxFile, trackInfo.id);\n }\n\n /**\n * Build GOP index from samples\n * GOP = Group of Pictures, starts with a keyframe\n */\n private buildGOPIndex(samples: Sample[]): GOP[] {\n const gopIndex: GOP[] = [];\n let currentGOP: {\n startTimeUs: TimeUs;\n keyframeSampleIndex: number;\n sampleCount: number;\n } | null = null;\n\n for (let i = 0; i < samples.length; i++) {\n const sample = samples[i];\n if (!sample) continue;\n\n if (sample.isKeyframe) {\n // Save previous GOP if exists\n if (currentGOP) {\n gopIndex.push(currentGOP);\n }\n\n // Start new GOP\n currentGOP = {\n startTimeUs: sample.timestamp,\n keyframeSampleIndex: i,\n sampleCount: 1,\n };\n } else if (currentGOP) {\n // Add sample to current GOP\n currentGOP.sampleCount++;\n }\n }\n\n // Save last GOP\n if (currentGOP) {\n gopIndex.push(currentGOP);\n }\n\n return gopIndex;\n }\n\n /**\n * Extract first GOP chunks from accumulated buffer\n * Used for fast cover rendering during streaming download\n */\n private extractFirstGOP(\n buffer: Uint8Array,\n videoTrack: VideoTrackIndex,\n byteStart: number\n ): EncodedVideoChunk[] {\n const chunks: EncodedVideoChunk[] = [];\n const firstGOP = videoTrack.gopIndex[0];\n if (!firstGOP) {\n return chunks;\n }\n\n const samples = videoTrack.samples;\n\n for (let i = 0; i < firstGOP.sampleCount; i++) {\n const sampleIdx = firstGOP.keyframeSampleIndex + i;\n const sample = samples[sampleIdx];\n if (!sample) continue;\n\n const relativeOffset = sample.byteOffset - byteStart;\n\n // Validate offset is within buffer\n if (relativeOffset < 0 || relativeOffset + sample.byteLength > buffer.length) {\n console.warn('[MP4IndexParser] Sample outside buffer:', {\n sampleOffset: sample.byteOffset,\n sampleLength: sample.byteLength,\n byteStart,\n relativeOffset,\n bufferLength: buffer.length,\n });\n continue;\n }\n\n const sampleData = buffer.slice(relativeOffset, relativeOffset + sample.byteLength);\n\n try {\n chunks.push(\n new EncodedVideoChunk({\n type: sample.isKeyframe ? 'key' : 'delta',\n timestamp: sample.timestamp,\n duration: sample.duration,\n data: sampleData,\n })\n );\n } catch (error) {\n console.warn('[MP4IndexParser] Failed to create EncodedVideoChunk:', error);\n }\n }\n\n return chunks;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CompositionModel } from '../../model';
|
|
1
|
+
import { Resource, CompositionModel } from '../../model';
|
|
2
2
|
import { ResourceLoadOptions, LoadTask, ResourceLoaderOptions } from './types';
|
|
3
3
|
|
|
4
4
|
export declare class ResourceConflictError extends Error {
|
|
@@ -12,21 +12,16 @@ export declare class ResourceLoader {
|
|
|
12
12
|
private streamFactory;
|
|
13
13
|
private eventBus?;
|
|
14
14
|
private onStateChange?;
|
|
15
|
-
private byteRangeResolver;
|
|
16
15
|
private blobCache;
|
|
17
|
-
private pendingTransfers;
|
|
18
16
|
private parsingIndexes;
|
|
19
17
|
private isPreloadingEnabled;
|
|
20
18
|
private preloadQueue;
|
|
21
|
-
private activePreloads;
|
|
22
|
-
private readonly IDLE_PRELOAD_CONCURRENCY;
|
|
23
19
|
constructor(options: ResourceLoaderOptions);
|
|
24
20
|
setModel(model: CompositionModel): Promise<void>;
|
|
25
21
|
setPreloadingEnabled(enabled: boolean): void;
|
|
26
22
|
startPreloading(): void;
|
|
27
23
|
private processPreloadQueue;
|
|
28
24
|
private enqueueLoad;
|
|
29
|
-
private registerPendingTransfer;
|
|
30
25
|
private processQueue;
|
|
31
26
|
/**
|
|
32
27
|
* Start loading a resource
|
|
@@ -55,6 +50,12 @@ export declare class ResourceLoader {
|
|
|
55
50
|
* Write resource stream to OPFS
|
|
56
51
|
*/
|
|
57
52
|
private writeToOPFS;
|
|
53
|
+
/**
|
|
54
|
+
* Load and parse audio file (MP3/WAV) in main thread
|
|
55
|
+
* Extract EncodedAudioChunk and cache to AudioSampleCache
|
|
56
|
+
* Aligned with video audio track extraction (unified architecture)
|
|
57
|
+
*/
|
|
58
|
+
private loadAndParseAudioFile;
|
|
58
59
|
/**
|
|
59
60
|
* Parse moov from stream and cache index + audio samples + decode first frame
|
|
60
61
|
*/
|
|
@@ -64,6 +65,7 @@ export declare class ResourceLoader {
|
|
|
64
65
|
* Just download the content - state management is handled by startLoad()
|
|
65
66
|
*/
|
|
66
67
|
private loadTextResource;
|
|
68
|
+
loadImage(resource: Resource): Promise<ImageBitmap>;
|
|
67
69
|
/**
|
|
68
70
|
* Load image resource: fetch blob → create ImageBitmap → cache in CacheManager
|
|
69
71
|
* Note: Images don't need streaming (typically < 5MB)
|
|
@@ -78,10 +80,6 @@ export declare class ResourceLoader {
|
|
|
78
80
|
* Fetch resource as blob (for images, json, etc.)
|
|
79
81
|
*/
|
|
80
82
|
private fetchBlob;
|
|
81
|
-
/**
|
|
82
|
-
* Transfer ImageBitmap to VideoComposeWorker
|
|
83
|
-
* Legacy: Not used in window cache architecture (images accessed via CacheManager)
|
|
84
|
-
*/
|
|
85
83
|
/**
|
|
86
84
|
* Transfer cached image to a session
|
|
87
85
|
* Creates new ImageBitmap from cached Blob and transfers to worker
|
|
@@ -92,6 +90,18 @@ export declare class ResourceLoader {
|
|
|
92
90
|
*/
|
|
93
91
|
private transferToDemuxWorker;
|
|
94
92
|
private updateResourceState;
|
|
93
|
+
/**
|
|
94
|
+
* Fetch a resource and wait for loading + parsing to complete
|
|
95
|
+
*
|
|
96
|
+
* Returns a Promise that resolves when:
|
|
97
|
+
* - Resource is fully loaded, parsed, and cached (state='ready')
|
|
98
|
+
* - Or rejects if loading/parsing fails
|
|
99
|
+
*
|
|
100
|
+
* Promise lifecycle:
|
|
101
|
+
* 1. enqueueLoad() creates LoadTask with promise/resolve/reject (or reuses existing)
|
|
102
|
+
* 2. processQueue() → startLoad() executes async in background
|
|
103
|
+
* 3. startLoad() completes → finally → completeTask() → task.resolve()/reject()
|
|
104
|
+
*/
|
|
95
105
|
fetch(resourceId?: string, options?: ResourceLoadOptions): Promise<void>;
|
|
96
106
|
cancel(resourceId: string): void;
|
|
97
107
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResourceLoader.d.ts","sourceRoot":"","sources":["../../../src/stages/load/ResourceLoader.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"ResourceLoader.d.ts","sourceRoot":"","sources":["../../../src/stages/load/ResourceLoader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,gBAAgB,EAAiB,MAAM,aAAa,CAAC;AAClF,OAAO,KAAK,EAAE,mBAAmB,EAAE,QAAQ,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAWpF,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,KAAK,CAAC,CAAmB;IACjC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,QAAQ,CAAC,CAA4B;IAC7C,OAAO,CAAC,aAAa,CAAC,CAAyD;IAC/E,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,cAAc,CAAqB;IAG3C,OAAO,CAAC,mBAAmB,CAAQ;IACnC,OAAO,CAAC,YAAY,CAAgB;gBAExB,OAAO,EAAE,qBAAqB;IAYpC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAatD,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAO5C,eAAe,IAAI,IAAI;IA0BvB,OAAO,CAAC,mBAAmB;IAe3B,OAAO,CAAC,WAAW;IAmBnB,OAAO,CAAC,YAAY;IAQpB;;;OAGG;YACW,SAAS;IAsDvB;;OAEG;IACG,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqC1D;;;OAGG;YACW,oBAAoB;IAclC;;;;;;;OAOG;YACW,iBAAiB;IA6C/B;;OAEG;YACW,WAAW;IAIzB;;;;OAIG;YACW,qBAAqB;IA8CnC;;OAEG;YACW,oBAAoB;IA8DlC;;;OAGG;YACW,gBAAgB;IAMxB,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,WAAW,CAAC;IAazD;;;;;;;;OAQG;YACW,eAAe;IAe7B;;OAEG;YACW,SAAS;IAUvB;;;OAGG;YACW,mBAAmB;IAsBjC;;OAEG;YACW,qBAAqB;IAyBnC,OAAO,CAAC,mBAAmB;IAgB3B;;;;;;;;;;;OAWG;IACG,KAAK,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAwB9E,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;IAqB9E,IAAI,WAAW,IAAI,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAEvC;IAED,IAAI,SAAS,IAAI,QAAQ,EAAE,CAE1B;IAED,OAAO,IAAI,IAAI;CAIhB"}
|
|
@@ -2,15 +2,9 @@ import { hasResourceId } from "../../model/types.js";
|
|
|
2
2
|
import { TaskManager } from "./TaskManager.js";
|
|
3
3
|
import { StreamFactory } from "./StreamFactory.js";
|
|
4
4
|
import { MeframeEvent } from "../../event/events.js";
|
|
5
|
-
import { WindowByteRangeResolver } from "./WindowByteRangeResolver.js";
|
|
6
5
|
import { createImageBitmapFromBlob } from "../../utils/image-utils.js";
|
|
7
6
|
import { MP4IndexParser } from "../demux/MP4IndexParser.js";
|
|
8
|
-
|
|
9
|
-
constructor(message) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.name = "ResourceConflictError";
|
|
12
|
-
}
|
|
13
|
-
}
|
|
7
|
+
import { MP3FrameParser } from "../demux/MP3FrameParser.js";
|
|
14
8
|
class ResourceLoader {
|
|
15
9
|
cacheManager;
|
|
16
10
|
workerPool;
|
|
@@ -19,24 +13,19 @@ class ResourceLoader {
|
|
|
19
13
|
streamFactory;
|
|
20
14
|
eventBus;
|
|
21
15
|
onStateChange;
|
|
22
|
-
byteRangeResolver;
|
|
23
16
|
blobCache = /* @__PURE__ */ new Map();
|
|
24
|
-
pendingTransfers = /* @__PURE__ */ new Map();
|
|
25
17
|
parsingIndexes = /* @__PURE__ */ new Set();
|
|
26
18
|
// Track in-progress index parsing
|
|
27
19
|
// Preloading state
|
|
28
20
|
isPreloadingEnabled = true;
|
|
29
21
|
preloadQueue = [];
|
|
30
|
-
activePreloads = /* @__PURE__ */ new Set();
|
|
31
|
-
// TODO: make this configurable
|
|
32
|
-
IDLE_PRELOAD_CONCURRENCY = 2;
|
|
33
22
|
constructor(options) {
|
|
34
|
-
const
|
|
23
|
+
const config = options.config || {};
|
|
24
|
+
const maxConcurrent = config.maxConcurrent ?? 2;
|
|
35
25
|
this.taskManager = new TaskManager(maxConcurrent);
|
|
36
|
-
this.streamFactory = new StreamFactory(options.onProgress,
|
|
26
|
+
this.streamFactory = new StreamFactory(options.onProgress, config);
|
|
37
27
|
this.eventBus = options.eventBus;
|
|
38
28
|
this.onStateChange = options.onStateChange;
|
|
39
|
-
this.byteRangeResolver = new WindowByteRangeResolver();
|
|
40
29
|
this.cacheManager = options.cacheManager;
|
|
41
30
|
this.workerPool = options.workerPool;
|
|
42
31
|
}
|
|
@@ -56,8 +45,6 @@ class ResourceLoader {
|
|
|
56
45
|
this.isPreloadingEnabled = enabled;
|
|
57
46
|
if (enabled) {
|
|
58
47
|
this.startPreloading();
|
|
59
|
-
} else {
|
|
60
|
-
this.preloadQueue = [];
|
|
61
48
|
}
|
|
62
49
|
}
|
|
63
50
|
startPreloading() {
|
|
@@ -66,7 +53,6 @@ class ResourceLoader {
|
|
|
66
53
|
(track) => track.id === (this.model?.mainTrackId || "main")
|
|
67
54
|
);
|
|
68
55
|
if (!mainTrack) return;
|
|
69
|
-
const newQueue = [];
|
|
70
56
|
for (const clip of mainTrack.clips) {
|
|
71
57
|
if (!hasResourceId(clip)) continue;
|
|
72
58
|
const resource = this.model.getResource(clip.resourceId);
|
|
@@ -74,56 +60,27 @@ class ResourceLoader {
|
|
|
74
60
|
if (!resource || resource.state === "ready" || resource.state === "loading" || resource.state === "error") {
|
|
75
61
|
continue;
|
|
76
62
|
}
|
|
77
|
-
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
newQueue.push(resource.id);
|
|
63
|
+
this.fetch(resource.id, { priority: "low" });
|
|
81
64
|
}
|
|
82
|
-
this.preloadQueue = newQueue;
|
|
83
|
-
this.processPreloadQueue();
|
|
84
65
|
}
|
|
85
66
|
processPreloadQueue() {
|
|
86
67
|
if (!this.isPreloadingEnabled || this.preloadQueue.length === 0) return;
|
|
87
|
-
while (this.
|
|
68
|
+
while (this.preloadQueue.length > 0) {
|
|
88
69
|
const resourceId = this.preloadQueue.shift();
|
|
89
70
|
if (!resourceId) break;
|
|
90
|
-
this.activePreloads.add(resourceId);
|
|
91
71
|
this.fetch(resourceId, { priority: "low" }).finally(() => {
|
|
92
|
-
this.activePreloads.delete(resourceId);
|
|
93
72
|
this.processPreloadQueue();
|
|
94
73
|
});
|
|
95
74
|
}
|
|
96
75
|
}
|
|
97
|
-
enqueueLoad(resource, priority = "normal", sessionId, clipId, trackId
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
throw new ResourceConflictError(
|
|
102
|
-
`Resource ${resource.id} is being loaded by another session. Preview channel has priority.`
|
|
103
|
-
);
|
|
104
|
-
} else {
|
|
105
|
-
if (this.blobCache.has(resource.id)) {
|
|
106
|
-
void this.transferCachedImage(resource.id, sessionId);
|
|
107
|
-
return;
|
|
108
|
-
} else {
|
|
109
|
-
this.registerPendingTransfer(resource.id, sessionId);
|
|
110
|
-
console.debug(
|
|
111
|
-
`[ResourceLoader] Attachment resource ${resource.id} loading, registered for pending transfer`
|
|
112
|
-
);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
76
|
+
enqueueLoad(resource, priority = "normal", sessionId, clipId, trackId) {
|
|
77
|
+
const existingTask = this.taskManager.getActiveTask(resource.id);
|
|
78
|
+
if (existingTask) {
|
|
79
|
+
return existingTask;
|
|
116
80
|
}
|
|
117
|
-
this.taskManager.enqueue(resource, priority, sessionId, clipId, trackId);
|
|
81
|
+
const task = this.taskManager.enqueue(resource, priority, sessionId, clipId, trackId);
|
|
118
82
|
this.processQueue();
|
|
119
|
-
|
|
120
|
-
registerPendingTransfer(resourceId, sessionId) {
|
|
121
|
-
if (!sessionId) return;
|
|
122
|
-
const pending = this.pendingTransfers.get(resourceId) || [];
|
|
123
|
-
if (!pending.includes(sessionId)) {
|
|
124
|
-
pending.push(sessionId);
|
|
125
|
-
this.pendingTransfers.set(resourceId, pending);
|
|
126
|
-
}
|
|
83
|
+
return task;
|
|
127
84
|
}
|
|
128
85
|
processQueue() {
|
|
129
86
|
while (this.taskManager.canProcess) {
|
|
@@ -144,6 +101,9 @@ class ResourceLoader {
|
|
|
144
101
|
task.controller = new AbortController();
|
|
145
102
|
if (task.resource.type === "image") {
|
|
146
103
|
await this.loadImageBitmap(task);
|
|
104
|
+
if (task.sessionId) {
|
|
105
|
+
this.transferCachedImage(task);
|
|
106
|
+
}
|
|
147
107
|
} else if (task.resource.type === "video") {
|
|
148
108
|
const cached = await this.cacheManager.hasResourceInCache(task.resourceId);
|
|
149
109
|
if (cached) {
|
|
@@ -157,12 +117,9 @@ class ResourceLoader {
|
|
|
157
117
|
await this.loadWithOPFSCache(task);
|
|
158
118
|
}
|
|
159
119
|
} else if (task.resource.type === "audio") {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
throw new Error(`Failed to create stream for ${task.resourceId}`);
|
|
120
|
+
if (!this.cacheManager.audioSampleCache.has(task.resourceId)) {
|
|
121
|
+
await this.loadAndParseAudioFile(task);
|
|
163
122
|
}
|
|
164
|
-
task.stream = stream;
|
|
165
|
-
await this.transferToDemuxWorker(task);
|
|
166
123
|
} else if (task.resource.type === "json" || task.resource.type === "text") {
|
|
167
124
|
await this.loadTextResource(task);
|
|
168
125
|
}
|
|
@@ -261,6 +218,44 @@ class ResourceLoader {
|
|
|
261
218
|
async writeToOPFS(resourceId, stream) {
|
|
262
219
|
await this.cacheManager.resourceCache.writeResource(resourceId, stream);
|
|
263
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Load and parse audio file (MP3/WAV) in main thread
|
|
223
|
+
* Extract EncodedAudioChunk and cache to AudioSampleCache
|
|
224
|
+
* Aligned with video audio track extraction (unified architecture)
|
|
225
|
+
*/
|
|
226
|
+
async loadAndParseAudioFile(task) {
|
|
227
|
+
const { resourceId } = task;
|
|
228
|
+
try {
|
|
229
|
+
const blob = await this.fetchBlob(task.resource.uri, task.controller.signal);
|
|
230
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
231
|
+
const uint8Array = new Uint8Array(arrayBuffer);
|
|
232
|
+
const parser = new MP3FrameParser();
|
|
233
|
+
const { frames, config } = parser.push(uint8Array);
|
|
234
|
+
const remainingFrames = parser.flush();
|
|
235
|
+
const allFrames = [...frames, ...remainingFrames];
|
|
236
|
+
if (!config) {
|
|
237
|
+
throw new Error(`Failed to parse audio config for ${resourceId}`);
|
|
238
|
+
}
|
|
239
|
+
const audioChunks = allFrames.map((frame) => {
|
|
240
|
+
return new EncodedAudioChunk({
|
|
241
|
+
type: "key",
|
|
242
|
+
// MP3 frames are all key frames
|
|
243
|
+
timestamp: frame.timestampUs,
|
|
244
|
+
duration: frame.durationUs,
|
|
245
|
+
data: frame.data
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
const audioConfig = {
|
|
249
|
+
codec: "mp3",
|
|
250
|
+
sampleRate: config.sampleRate,
|
|
251
|
+
numberOfChannels: config.channels
|
|
252
|
+
};
|
|
253
|
+
this.cacheManager.audioSampleCache.set(resourceId, audioChunks, audioConfig);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.error(`[ResourceLoader] Failed to parse audio file ${resourceId}:`, error);
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
264
259
|
/**
|
|
265
260
|
* Parse moov from stream and cache index + audio samples + decode first frame
|
|
266
261
|
*/
|
|
@@ -306,6 +301,18 @@ class ResourceLoader {
|
|
|
306
301
|
async loadTextResource(task) {
|
|
307
302
|
await this.fetchBlob(task.resource.uri, task.controller.signal);
|
|
308
303
|
}
|
|
304
|
+
async loadImage(resource) {
|
|
305
|
+
const task = {
|
|
306
|
+
resourceId: resource.id,
|
|
307
|
+
resource,
|
|
308
|
+
bytesLoaded: 0,
|
|
309
|
+
totalBytes: 0,
|
|
310
|
+
startTime: Date.now(),
|
|
311
|
+
priority: "normal",
|
|
312
|
+
controller: new AbortController()
|
|
313
|
+
};
|
|
314
|
+
return this.loadImageBitmap(task);
|
|
315
|
+
}
|
|
309
316
|
/**
|
|
310
317
|
* Load image resource: fetch blob → create ImageBitmap → cache in CacheManager
|
|
311
318
|
* Note: Images don't need streaming (typically < 5MB)
|
|
@@ -322,7 +329,7 @@ class ResourceLoader {
|
|
|
322
329
|
this.blobCache.set(task.resourceId, blob);
|
|
323
330
|
}
|
|
324
331
|
const imageBitmap = await createImageBitmapFromBlob(blob);
|
|
325
|
-
|
|
332
|
+
return imageBitmap;
|
|
326
333
|
}
|
|
327
334
|
/**
|
|
328
335
|
* Fetch resource as blob (for images, json, etc.)
|
|
@@ -334,34 +341,12 @@ class ResourceLoader {
|
|
|
334
341
|
}
|
|
335
342
|
return response.blob();
|
|
336
343
|
}
|
|
337
|
-
/**
|
|
338
|
-
* Transfer ImageBitmap to VideoComposeWorker
|
|
339
|
-
* Legacy: Not used in window cache architecture (images accessed via CacheManager)
|
|
340
|
-
*/
|
|
341
|
-
// private async transferImageToWorker(task: LoadTask, imageBitmap: ImageBitmap): Promise<void> {
|
|
342
|
-
// if (!this.orchestrator) return;
|
|
343
|
-
// if (!task.sessionId) {
|
|
344
|
-
// throw new Error(
|
|
345
|
-
// `[ResourceLoader] sessionId required for resource ${task.resourceId}. ` +
|
|
346
|
-
// `In Clip-based architecture, use fetch(resourceId, { sessionId })`
|
|
347
|
-
// );
|
|
348
|
-
// }
|
|
349
|
-
// const composeWorker = await this.orchestrator.workers.get('videoCompose', task.sessionId);
|
|
350
|
-
// await composeWorker?.send?.(
|
|
351
|
-
// 'receive_image',
|
|
352
|
-
// {
|
|
353
|
-
// resourceId: task.resourceId,
|
|
354
|
-
// sessionId: task.sessionId,
|
|
355
|
-
// imageBitmap,
|
|
356
|
-
// },
|
|
357
|
-
// { transfer: [imageBitmap] }
|
|
358
|
-
// );
|
|
359
|
-
// }
|
|
360
344
|
/**
|
|
361
345
|
* Transfer cached image to a session
|
|
362
346
|
* Creates new ImageBitmap from cached Blob and transfers to worker
|
|
363
347
|
*/
|
|
364
|
-
async transferCachedImage(
|
|
348
|
+
async transferCachedImage(task) {
|
|
349
|
+
const { resourceId, sessionId } = task;
|
|
365
350
|
const blob = this.blobCache.get(resourceId);
|
|
366
351
|
if (!blob || !sessionId) return;
|
|
367
352
|
const imageBitmap = await createImageBitmapFromBlob(blob);
|
|
@@ -382,9 +367,8 @@ class ResourceLoader {
|
|
|
382
367
|
async transferToDemuxWorker(task) {
|
|
383
368
|
if (!task.stream) return;
|
|
384
369
|
if (!task.sessionId) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
);
|
|
370
|
+
task.stream.cancel();
|
|
371
|
+
return;
|
|
388
372
|
}
|
|
389
373
|
const workerType = task.resource.type === "video" ? "videoDemux" : "audioDemux";
|
|
390
374
|
const demuxWorker = await this.workerPool.get(workerType, task.sessionId);
|
|
@@ -413,6 +397,18 @@ class ResourceLoader {
|
|
|
413
397
|
}
|
|
414
398
|
this.onStateChange?.(resourceId, state);
|
|
415
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Fetch a resource and wait for loading + parsing to complete
|
|
402
|
+
*
|
|
403
|
+
* Returns a Promise that resolves when:
|
|
404
|
+
* - Resource is fully loaded, parsed, and cached (state='ready')
|
|
405
|
+
* - Or rejects if loading/parsing fails
|
|
406
|
+
*
|
|
407
|
+
* Promise lifecycle:
|
|
408
|
+
* 1. enqueueLoad() creates LoadTask with promise/resolve/reject (or reuses existing)
|
|
409
|
+
* 2. processQueue() → startLoad() executes async in background
|
|
410
|
+
* 3. startLoad() completes → finally → completeTask() → task.resolve()/reject()
|
|
411
|
+
*/
|
|
416
412
|
async fetch(resourceId, options) {
|
|
417
413
|
if (!resourceId) {
|
|
418
414
|
return;
|
|
@@ -422,54 +418,14 @@ class ResourceLoader {
|
|
|
422
418
|
console.warn(`Resource ${resourceId} not found in model`);
|
|
423
419
|
return;
|
|
424
420
|
}
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
bytesLoaded: 0,
|
|
434
|
-
totalBytes: 0,
|
|
435
|
-
startTime: Date.now(),
|
|
436
|
-
priority: "normal"
|
|
437
|
-
};
|
|
438
|
-
await this.transferToDemuxWorker(task);
|
|
439
|
-
} else if (type === "image") {
|
|
440
|
-
await this.transferCachedImage(rId, sId);
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
if (resource.state === "ready") {
|
|
444
|
-
if (options?.sessionId) {
|
|
445
|
-
await transferResourceToWorker(resourceId, options.sessionId, resource.type);
|
|
446
|
-
}
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
let taskPromise = this.taskManager.getTaskPromise(resourceId);
|
|
450
|
-
let isCoveredByTask = false;
|
|
451
|
-
if (taskPromise) {
|
|
452
|
-
const activeTask = this.taskManager.activeTasks.get(resourceId) || this.taskManager.taskQueue.find((t) => t.resourceId === resourceId);
|
|
453
|
-
if (activeTask && activeTask.sessionId === options?.sessionId) {
|
|
454
|
-
isCoveredByTask = true;
|
|
455
|
-
}
|
|
456
|
-
} else {
|
|
457
|
-
this.enqueueLoad(
|
|
458
|
-
resource,
|
|
459
|
-
options?.priority || "normal",
|
|
460
|
-
options?.sessionId,
|
|
461
|
-
options?.clipId,
|
|
462
|
-
options?.trackId,
|
|
463
|
-
options?.isMainTrack ?? false
|
|
464
|
-
);
|
|
465
|
-
taskPromise = this.taskManager.getTaskPromise(resourceId);
|
|
466
|
-
isCoveredByTask = true;
|
|
467
|
-
}
|
|
468
|
-
await taskPromise;
|
|
469
|
-
const updatedResource = this.model?.resources.get(resourceId);
|
|
470
|
-
if (!isCoveredByTask && updatedResource?.state === "ready" && options?.sessionId) {
|
|
471
|
-
await transferResourceToWorker(resourceId, options.sessionId, resource.type);
|
|
472
|
-
}
|
|
421
|
+
const task = this.enqueueLoad(
|
|
422
|
+
resource,
|
|
423
|
+
options?.priority || "normal",
|
|
424
|
+
options?.sessionId,
|
|
425
|
+
options?.clipId,
|
|
426
|
+
options?.trackId
|
|
427
|
+
);
|
|
428
|
+
return task.promise;
|
|
473
429
|
}
|
|
474
430
|
cancel(resourceId) {
|
|
475
431
|
this.taskManager.cancelTask(resourceId);
|
|
@@ -513,13 +469,10 @@ class ResourceLoader {
|
|
|
513
469
|
}
|
|
514
470
|
dispose() {
|
|
515
471
|
this.taskManager.clear();
|
|
516
|
-
this.byteRangeResolver.dispose();
|
|
517
472
|
this.blobCache.clear();
|
|
518
|
-
this.pendingTransfers.clear();
|
|
519
473
|
}
|
|
520
474
|
}
|
|
521
475
|
export {
|
|
522
|
-
ResourceConflictError,
|
|
523
476
|
ResourceLoader
|
|
524
477
|
};
|
|
525
478
|
//# sourceMappingURL=ResourceLoader.js.map
|