@meframe/core 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/cache/AudioMixBlockCache.d.ts +18 -0
  2. package/dist/cache/AudioMixBlockCache.d.ts.map +1 -0
  3. package/dist/cache/AudioMixBlockCache.js +57 -0
  4. package/dist/cache/AudioMixBlockCache.js.map +1 -0
  5. package/dist/cache/CacheManager.d.ts +1 -1
  6. package/dist/cache/CacheManager.d.ts.map +1 -1
  7. package/dist/cache/CacheManager.js +1 -2
  8. package/dist/cache/CacheManager.js.map +1 -1
  9. package/dist/cache/l1/AudioL1Cache.d.ts +0 -15
  10. package/dist/cache/l1/AudioL1Cache.d.ts.map +1 -1
  11. package/dist/cache/l1/AudioL1Cache.js +57 -65
  12. package/dist/cache/l1/AudioL1Cache.js.map +1 -1
  13. package/dist/controllers/PlaybackController.d.ts +0 -1
  14. package/dist/controllers/PlaybackController.d.ts.map +1 -1
  15. package/dist/controllers/PlaybackController.js +16 -35
  16. package/dist/controllers/PlaybackController.js.map +1 -1
  17. package/dist/controllers/PlaybackStateMachine.d.ts.map +1 -1
  18. package/dist/controllers/PlaybackStateMachine.js +22 -2
  19. package/dist/controllers/PlaybackStateMachine.js.map +1 -1
  20. package/dist/controllers/types.d.ts +3 -0
  21. package/dist/controllers/types.d.ts.map +1 -1
  22. package/dist/controllers/types.js +1 -0
  23. package/dist/controllers/types.js.map +1 -1
  24. package/dist/orchestrator/GlobalAudioSession.d.ts +27 -7
  25. package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
  26. package/dist/orchestrator/GlobalAudioSession.js +292 -103
  27. package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
  28. package/dist/orchestrator/OnDemandVideoSession.d.ts.map +1 -1
  29. package/dist/orchestrator/OnDemandVideoSession.js +13 -0
  30. package/dist/orchestrator/OnDemandVideoSession.js.map +1 -1
  31. package/dist/orchestrator/Orchestrator.d.ts +1 -1
  32. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  33. package/dist/orchestrator/Orchestrator.js +44 -17
  34. package/dist/orchestrator/Orchestrator.js.map +1 -1
  35. package/dist/stages/compose/OfflineAudioMixer.d.ts.map +1 -1
  36. package/dist/stages/compose/OfflineAudioMixer.js +4 -1
  37. package/dist/stages/compose/OfflineAudioMixer.js.map +1 -1
  38. package/dist/stages/load/ResourceLoader.d.ts +6 -5
  39. package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
  40. package/dist/stages/load/ResourceLoader.js +53 -39
  41. package/dist/stages/load/ResourceLoader.js.map +1 -1
  42. package/dist/stages/load/TaskManager.d.ts +5 -12
  43. package/dist/stages/load/TaskManager.d.ts.map +1 -1
  44. package/dist/stages/load/TaskManager.js +60 -46
  45. package/dist/stages/load/TaskManager.js.map +1 -1
  46. package/dist/stages/mux/MP4Muxer.d.ts.map +1 -1
  47. package/dist/stages/mux/MP4Muxer.js +3 -0
  48. package/dist/stages/mux/MP4Muxer.js.map +1 -1
  49. package/dist/utils/platform-utils.d.ts +1 -0
  50. package/dist/utils/platform-utils.d.ts.map +1 -1
  51. package/dist/utils/platform-utils.js +19 -6
  52. package/dist/utils/platform-utils.js.map +1 -1
  53. package/dist/workers/stages/decode/video-decode.worker.BnWVUkng.js.map +1 -1
  54. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"GlobalAudioSession.js","sources":["../../src/orchestrator/GlobalAudioSession.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\nimport { OfflineAudioMixer } from '../stages/compose/OfflineAudioMixer';\nimport type { CompositionModel } from '../model';\nimport type { WorkerPool } from '../worker/WorkerPool';\nimport type { ResourceLoader } from '../stages/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 '../stages/encode/AudioChunkEncoder';\nimport { AudioChunkDecoder } from '../stages/decode/AudioChunkDecoder';\nimport { isAudioClip, hasResourceId } from '../model/types';\nimport type { RequestMode } from './types';\n\ninterface AudioDataMessage {\n sessionId: string;\n audioData: AudioData;\n clipStartUs: TimeUs;\n clipDurationUs: TimeUs;\n}\n\ninterface AudioSessionDeps {\n cacheManager: CacheManager;\n workerPool: WorkerPool;\n resourceLoader: ResourceLoader;\n eventBus: EventBus<EventPayloadMap>;\n buildWorkerConfigs: () => any;\n}\n\nexport class GlobalAudioSession {\n private mixer: OfflineAudioMixer;\n private activeClips = new Set<string>();\n private deps: AudioSessionDeps;\n private model: CompositionModel | null = null;\n private audioContext: AudioContext | null = null;\n private volume = 1.0;\n private playbackRate = 1.0;\n private isPlaying = false;\n\n // Lookahead scheduling state\n private nextScheduleTime = 0; // Next AudioContext time to schedule\n private nextContentTimeUs = 0; // Next timeline position (Us)\n private scheduledSources = new Set<AudioBufferSourceNode>();\n private readonly LOOKAHEAD_TIME = 0.2; // 200ms lookahead\n private readonly CHUNK_DURATION = 0.1; // 100ms chunks\n\n constructor(deps: AudioSessionDeps) {\n this.deps = deps;\n this.mixer = new OfflineAudioMixer(deps.cacheManager, () => this.model);\n }\n\n setModel(model: CompositionModel): void {\n this.model = model;\n }\n\n onAudioData(message: AudioDataMessage): void {\n const { sessionId, audioData, clipStartUs, clipDurationUs } = message;\n const globalTimeUs = clipStartUs + (audioData.timestamp ?? 0);\n this.deps.cacheManager.putClipAudioData(sessionId, audioData, clipDurationUs, globalTimeUs);\n }\n\n async ensureAudioForTime(timeUs: TimeUs, options?: { mode?: RequestMode }): Promise<void> {\n if (!this.model) return;\n\n const mode = options?.mode ?? 'blocking';\n const WINDOW_DURATION = 3_000_000; // 3s preheat window\n const windowEndUs = Math.min(this.model.durationUs, timeUs + WINDOW_DURATION);\n\n if (mode === 'probe') {\n void this.ensureAudioForTimeRange(timeUs, windowEndUs, { mode, loadResource: true });\n return;\n }\n await this.ensureAudioForTimeRange(timeUs, windowEndUs, { mode, loadResource: true });\n }\n\n /**\n * Fast readiness probe for preview playback.\n *\n * This is intentionally synchronous and lightweight:\n * - Only checks resource-level readiness (download + MP4 index parsing).\n * - If any relevant resource isn't ready yet, return false.\n * - Does NOT require audio samples / PCM window coverage (probe is resource-level only).\n *\n * Note: This probe does NOT gate on PCM coverage to avoid frequent buffering oscillation.\n * PCM is prepared incrementally by scheduleAudio() / ensureAudioForTimeRange().\n */\n isAudioResourceWindowReady(startUs: TimeUs, endUs: TimeUs): boolean {\n const model = this.model;\n if (!model) return true;\n\n const activeClips = model.getActiveClips(startUs, endUs);\n\n for (const clip of activeClips) {\n if (clip.trackKind !== 'audio' && clip.trackKind !== 'video') continue;\n if (!hasResourceId(clip)) continue;\n\n const resource = model.getResource(clip.resourceId);\n if (resource?.state !== 'ready') {\n return false;\n }\n }\n\n return true;\n }\n\n async activateAllAudioClips(): Promise<void> {\n const model = this.model;\n if (!model) {\n return;\n }\n\n const audioTracks = model.tracks.filter((track) => track.kind === 'audio');\n if (audioTracks.length === 0) return;\n\n // Find maximum clip count across all audio tracks\n const maxClipCount = Math.max(...audioTracks.map((track) => track.clips.length));\n\n // Horizontal loading: activate clip[0] from all tracks, then clip[1], etc.\n for (let clipIndex = 0; clipIndex < maxClipCount; clipIndex++) {\n for (const track of audioTracks) {\n const clip = track.clips[clipIndex];\n if (!clip || this.activeClips.has(clip.id)) continue;\n\n if (!isAudioClip(clip)) {\n throw new Error(`Clip ${clip.id} in audio track is not an audio clip`);\n }\n\n // Preview: Use main-thread parsing → AudioSampleCache → on-demand decode\n // Check if we have cached samples (already parsed in ResourceLoader)\n if (this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n // Already parsed, mark as active\n this.activeClips.add(clip.id);\n this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });\n continue;\n }\n\n // Ensure resource is loaded (will be parsed and cached in main thread)\n await this.deps.resourceLoader.load(clip.resourceId, {\n isPreload: false,\n clipId: clip.id,\n trackId: track.id,\n });\n\n // Mark as active\n this.activeClips.add(clip.id);\n this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });\n }\n }\n }\n\n async deactivateClip(clipId: string): Promise<void> {\n if (!this.activeClips.has(clipId)) {\n return;\n }\n\n this.activeClips.delete(clipId);\n this.deps.cacheManager.clearClipAudioData(clipId);\n }\n\n async startPlayback(timeUs: TimeUs, audioContext: AudioContext): Promise<void> {\n this.audioContext = audioContext;\n\n // Resume AudioContext if suspended (required by modern browsers)\n if (audioContext.state === 'suspended') {\n await audioContext.resume();\n }\n\n // Ensure audio is decoded and ready (blocking mode for startup)\n await this.ensureAudioForTime(timeUs, { mode: 'blocking' });\n\n this.isPlaying = true;\n // Reset playback states when starting to initialize scheduling from current time\n this.resetPlaybackStates();\n\n // Immediately fill lookahead buffer to avoid initial silence\n // This is critical after seek - ensures audio starts immediately\n await this.scheduleAudio(timeUs, audioContext);\n }\n\n stopPlayback(): void {\n this.isPlaying = false;\n this.stopAllAudioSources();\n }\n\n updateTime(_timeUs: TimeUs): void {\n // Kept for compatibility\n }\n\n /**\n * Schedule audio chunks ahead of playback cursor\n * Uses OfflineAudioMixer for proper mixing, then plays the result\n */\n async scheduleAudio(currentTimelineUs: TimeUs, audioContext: AudioContext): Promise<void> {\n if (!this.isPlaying || !this.model || !this.audioContext) {\n return;\n }\n\n const lookaheadTime = audioContext.currentTime + this.LOOKAHEAD_TIME;\n\n // Initialize on first call\n if (this.nextScheduleTime === 0) {\n this.nextScheduleTime = audioContext.currentTime + 0.01;\n this.nextContentTimeUs = currentTimelineUs;\n }\n\n // Schedule chunks until we reach lookahead limit\n while (this.nextScheduleTime < lookaheadTime) {\n // Detect and fix time drift (caused by slow async operations in CPU-throttled environments)\n if (this.nextScheduleTime < audioContext.currentTime) {\n const timeDrift = audioContext.currentTime - this.nextScheduleTime;\n\n // Large drift (>20ms): skip to avoid audio glitches\n if (timeDrift > 0.02) {\n this.nextScheduleTime = audioContext.currentTime + 0.02;\n const skippedUs = Math.round(timeDrift * 1_000_000);\n this.nextContentTimeUs += skippedUs;\n } else {\n // Small drift: adjust timing without skipping\n this.nextScheduleTime = audioContext.currentTime + 0.01;\n }\n }\n\n const chunkDurationUs = Math.round(this.CHUNK_DURATION * 1_000_000);\n const startUs = this.nextContentTimeUs;\n const endUs = startUs + chunkDurationUs;\n\n // Check if we need audio for this time range\n if (endUs > this.model.durationUs) {\n break; // Reached end of composition\n }\n\n try {\n // Ensure audio for all clips in the mixing window (not just clips at current time point)\n // This fixes the issue where boundary clips are missed by getClipsAtTime()\n await this.ensureAudioForTimeRange(startUs, endUs, {\n mode: 'blocking',\n loadResource: true,\n });\n\n // Mix audio using OfflineAudioMixer (handles resampling + mixing)\n const mixedBuffer = await this.mixer.mix(startUs, endUs);\n\n // Check again after mix - the slow operation may have caused time drift\n if (this.nextScheduleTime < audioContext.currentTime) {\n const timeDrift = audioContext.currentTime - this.nextScheduleTime;\n\n // Large drift (>20ms): skip chunk to avoid audio glitches\n if (timeDrift > 0.02) {\n console.warn(\n `[Audio] Skip chunk due to time drift: ${(timeDrift * 1000).toFixed(1)}ms`\n );\n\n this.nextScheduleTime = audioContext.currentTime + 0.02;\n this.nextContentTimeUs += chunkDurationUs;\n continue;\n }\n\n // Small drift (<20ms): adjust timing without skipping\n this.nextScheduleTime = audioContext.currentTime + 0.01;\n }\n\n // Create source and play\n const source = audioContext.createBufferSource();\n source.buffer = mixedBuffer;\n source.playbackRate.value = this.playbackRate;\n\n const gainNode = audioContext.createGain();\n gainNode.gain.value = this.volume;\n\n source.connect(gainNode);\n gainNode.connect(audioContext.destination);\n\n source.start(this.nextScheduleTime);\n this.scheduledSources.add(source);\n\n source.onended = () => {\n source.disconnect();\n gainNode.disconnect();\n this.scheduledSources.delete(source);\n };\n\n // Advance scheduling state\n const actualDuration = mixedBuffer.duration;\n this.nextScheduleTime += actualDuration;\n this.nextContentTimeUs += chunkDurationUs;\n } catch (error) {\n console.warn('[GlobalAudioSession] Mix error, skipping chunk:', error);\n // Skip this chunk and continue\n this.nextScheduleTime += this.CHUNK_DURATION;\n this.nextContentTimeUs += chunkDurationUs;\n }\n }\n }\n\n /**\n * Reset playback states (called on seek)\n */\n resetPlaybackStates(): void {\n this.stopAllAudioSources();\n this.nextScheduleTime = 0;\n this.nextContentTimeUs = 0;\n }\n\n setVolume(volume: number): void {\n this.volume = volume;\n // Note: We can't easily update volume of already scheduled sources in this lookahead model\n // without keeping track of gain nodes. For now, volume changes will apply to next chunks.\n // If immediate volume change is needed, we'd need to store GainNodes in SchedulingState.\n }\n\n setPlaybackRate(rate: number): void {\n this.playbackRate = rate;\n // Playback rate change requires reset of scheduling to avoid pitch shift artifacts on existing nodes\n // or complicated time mapping updates.\n this.resetPlaybackStates();\n }\n\n reset(): void {\n this.stopAllAudioSources();\n this.deps.cacheManager.clearAudioCache();\n this.activeClips.clear();\n }\n\n /**\n * Mix and encode audio for a specific segment (used by ExportScheduler)\n */\n async mixAndEncodeSegment(\n startUs: TimeUs,\n endUs: TimeUs,\n onChunk: (chunk: EncodedAudioChunk, metadata?: EncodedAudioChunkMetadata) => void\n ): Promise<void> {\n // Wait for audio clips in this time range to be ready (on-demand wait)\n await this.ensureAudioForSegment(startUs, endUs);\n\n const mixedBuffer = await this.mixer.mix(startUs, endUs);\n const audioData = this.audioBufferToAudioData(mixedBuffer, startUs);\n\n if (!audioData) return;\n\n if (!this.exportEncoder) {\n this.exportEncoder = new AudioChunkEncoder();\n await this.exportEncoder.initialize();\n this.exportEncoderStream = this.exportEncoder.createStream();\n this.exportEncoderWriter = this.exportEncoderStream.writable.getWriter();\n\n // Start reader immediately (but don't await - it's a long-running loop)\n void this.startExportEncoderReader(this.exportEncoderStream.readable, onChunk);\n\n // Wait a bit to ensure reader is ready before first write\n await new Promise((resolve) => setTimeout(resolve, 10));\n }\n\n await this.exportEncoderWriter?.write(audioData);\n }\n\n /**\n * Ensure audio clips in time range are decoded (for export)\n * Decodes from AudioSampleCache (replaces Worker pipeline)\n */\n private async ensureAudioForSegment(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n // Export mode: don't load resources (they should already be loaded), only decode cached samples\n // Use strictMode=true to ensure 99% coverage for high-quality export\n await this.ensureAudioForTimeRange(startUs, endUs, {\n mode: 'blocking',\n loadResource: false,\n strictMode: true,\n });\n }\n\n private exportEncoder: AudioChunkEncoder | null = null;\n private exportEncoderStream: TransformStream<\n AudioData,\n { chunk: EncodedAudioChunk; metadata: any }\n > | null = null;\n private exportEncoderWriter: WritableStreamDefaultWriter<AudioData> | null = null;\n\n private async startExportEncoderReader(\n stream: ReadableStream<{ chunk: EncodedAudioChunk; metadata: any }>,\n onChunk: (chunk: EncodedAudioChunk, metadata?: EncodedAudioChunkMetadata) => void\n ) {\n const reader = stream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n onChunk(value.chunk, value.metadata);\n }\n }\n } catch (e) {\n console.error('Export encoder reader error', e);\n }\n }\n\n async finalizeExportAudio(): Promise<void> {\n if (this.exportEncoderWriter) {\n await this.exportEncoderWriter.close();\n this.exportEncoderWriter = null;\n }\n this.exportEncoder = null;\n this.exportEncoderStream = null;\n }\n\n private stopAllAudioSources(): void {\n for (const source of this.scheduledSources) {\n try {\n // Disconnect first to prevent residual audio\n source.disconnect();\n // Use stop(0) for immediate stop\n source.stop(0);\n } catch {\n // Source may not be started yet, ignore\n }\n }\n this.scheduledSources.clear();\n }\n\n /**\n * Core method to ensure audio for all clips in a time range\n * Unified implementation used by ensureAudioForTime, scheduleAudio, and export\n */\n private async ensureAudioForTimeRange(\n startUs: TimeUs,\n endUs: TimeUs,\n options: { mode?: RequestMode; loadResource?: boolean; strictMode?: boolean }\n ): Promise<void> {\n const model = this.model;\n if (!model) return;\n\n const { mode = 'blocking', loadResource = true, strictMode = false } = options;\n\n // Find all clips that overlap with [startUs, endUs]\n const activeClips = model.getActiveClips(startUs, endUs);\n\n const ensurePromises = activeClips.map(async (clip) => {\n // Only process audio and video clips\n if (clip.trackKind !== 'audio' && clip.trackKind !== 'video') return;\n if (!hasResourceId(clip)) return;\n\n // Skip clips without audio track (performance optimization)\n // If AudioSampleCache doesn't have this resource, and resource is ready,\n // it means the resource has no audio track (e.g., video-only or image)\n const resource = model.getResource(clip.resourceId);\n if (\n resource?.state === 'ready' &&\n !this.deps.cacheManager.audioSampleCache.has(clip.resourceId)\n ) {\n // Resource is ready but has no audio samples - skip\n return;\n }\n\n // Ensure AudioSampleCache has data\n if (!this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n if (!loadResource) {\n // Export mode: skip clips without cached samples\n return;\n }\n\n const resource = model.getResource(clip.resourceId);\n if (resource?.state !== 'ready') {\n // Resource not yet loaded - wait for it\n await this.deps.resourceLoader.load(clip.resourceId, {\n isPreload: false,\n clipId: clip.id,\n trackId: clip.trackId,\n });\n }\n }\n\n // Calculate clip-relative time range\n const clipRelativeStartUs = Math.max(0, startUs - clip.startUs);\n const clipRelativeEndUs = Math.min(clip.durationUs, endUs - clip.startUs);\n\n // Convert to resource time (aligned with video architecture)\n // This ensures correct filtering of audio samples and cache queries\n const trimStartUs = clip.trimStartUs ?? 0;\n const resourceStartUs = clipRelativeStartUs + trimStartUs;\n const resourceEndUs = clipRelativeEndUs + trimStartUs;\n\n // Ensure audio window using resource time coordinates\n await this.ensureAudioWindow(clip.id, resourceStartUs, resourceEndUs, strictMode);\n });\n\n if (mode === 'probe') {\n void Promise.all(ensurePromises);\n return;\n }\n await Promise.all(ensurePromises);\n }\n\n /**\n * Ensure audio window for a clip (aligned with video architecture)\n *\n * Note: Unlike video getFrame(), this method doesn't need a 'preheat' parameter\n * Why: Audio cache check is window-level (range query) via hasWindowPCM()\n * It verifies the entire window has ≥95% data (preview) or ≥99% (export)\n * This naturally prevents premature return during preheating\n */\n async ensureAudioWindow(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs,\n strictMode: boolean = false\n ): Promise<void> {\n // Check L1 cache - window-level verification (not point-level like video)\n if (this.deps.cacheManager.hasWindowPCM(clipId, startUs, endUs, strictMode)) {\n return; // Entire window has sufficient data\n }\n\n await this.decodeAudioWindow(clipId, startUs, endUs);\n }\n\n /**\n * Decode audio window for a clip (aligned with video architecture)\n * Incremental decoding strategy with smart fallback:\n * - High coverage (≥80%): Skip decoding\n * - Low coverage (<30%): Full decode (avoid fragmentation)\n * - Medium coverage (30%-80%): Incremental decode\n */\n private async decodeAudioWindow(clipId: string, startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const clip = this.model?.findClip(clipId);\n if (!clip || !hasResourceId(clip)) {\n return;\n }\n\n // Get audio samples from AudioSampleCache\n const audioRecord = this.deps.cacheManager.audioSampleCache.get(clip.resourceId);\n if (!audioRecord) {\n // Resource has no audio track (common for some video files)\n return;\n }\n\n // Filter chunks within window (aligned with video GOP filtering)\n const windowChunks = audioRecord.samples.filter((s) => {\n const sampleEndUs = s.timestamp + (s.duration ?? 0);\n return s.timestamp < endUs && sampleEndUs > startUs;\n });\n\n if (windowChunks.length === 0) {\n return;\n }\n\n // Incremental decoding with smart threshold strategy\n // SKIP threshold should be high to avoid audio gaps (e.g., 95% means max 5% missing data)\n const INCREMENTAL_THRESHOLD = 0.95; // 95% coverage: skip decode (was 0.8, increased to avoid audio gaps)\n const FULL_FALLBACK_THRESHOLD = 0.3; // <30% coverage: full decode to avoid fragmentation\n\n // Check window-level coverage\n const coverage = this.deps.cacheManager.getAudioRangeCoverage(\n clipId,\n startUs,\n endUs,\n INCREMENTAL_THRESHOLD\n );\n\n // Strategy 1: High coverage - skip decode entirely\n if (coverage.covered) {\n return;\n }\n\n // Strategy 2: Very low coverage - full decode (avoid fragmentation overhead)\n if (coverage.coverageRatio < FULL_FALLBACK_THRESHOLD) {\n await this.decodeAudioSamples(\n clipId,\n windowChunks,\n audioRecord.metadata,\n clip.durationUs,\n clip.startUs\n );\n return;\n }\n\n // Strategy 3: Medium coverage - incremental decode (30%-95%)\n // Filter out chunks that are already well-covered in L1 Cache\n const chunksToDecode = windowChunks.filter((chunk) => {\n const chunkEndUs = chunk.timestamp + (chunk.duration ?? 0);\n const chunkCoverage = this.deps.cacheManager.getAudioRangeCoverage(\n clipId,\n chunk.timestamp,\n chunkEndUs,\n 0.95 // Stricter threshold for individual chunks\n );\n return !chunkCoverage.covered;\n });\n\n if (chunksToDecode.length === 0) {\n return;\n }\n\n // Decode only missing chunks\n await this.decodeAudioSamples(\n clipId,\n chunksToDecode,\n audioRecord.metadata,\n clip.durationUs,\n clip.startUs\n );\n }\n\n /**\n * Decode audio samples to PCM and cache\n * Uses AudioChunkDecoder for consistency with project architecture\n * Resamples to AudioContext sample rate if needed for better quality\n */\n private async decodeAudioSamples(\n clipId: string,\n samples: EncodedAudioChunk[],\n config: AudioDecoderConfig,\n clipDurationUs: number,\n clipStartUs: TimeUs\n ): Promise<void> {\n // Use AudioChunkDecoder for consistency with project architecture\n // Convert description to ArrayBuffer if needed for type compatibility\n let description: ArrayBuffer | undefined;\n if (config.description) {\n if (config.description instanceof ArrayBuffer) {\n description = config.description;\n } else if (ArrayBuffer.isView(config.description)) {\n // Convert TypedArray to ArrayBuffer\n const view = config.description as Uint8Array;\n // Create a new ArrayBuffer and copy data to ensure proper type\n const newBuffer = new ArrayBuffer(view.byteLength);\n new Uint8Array(newBuffer).set(\n new Uint8Array(view.buffer, view.byteOffset, view.byteLength)\n );\n description = newBuffer;\n }\n }\n\n const decoderConfig = {\n codec: config.codec,\n sampleRate: config.sampleRate,\n numberOfChannels: config.numberOfChannels,\n description,\n };\n const decoder = new AudioChunkDecoder(`audio-${clipId}`, decoderConfig);\n\n try {\n // Create chunk stream\n const chunkStream = new ReadableStream<EncodedAudioChunk>({\n start(controller) {\n for (const sample of samples) {\n controller.enqueue(sample);\n }\n controller.close();\n },\n });\n\n // Decode through stream\n const audioDataStream = chunkStream.pipeThrough(decoder.createStream());\n const reader = audioDataStream.getReader();\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n if (value) {\n // Store original sample rate - OfflineAudioMixer will handle resampling\n const globalTimeUs = clipStartUs + (value.timestamp ?? 0);\n this.deps.cacheManager.putClipAudioData(clipId, value, clipDurationUs, globalTimeUs);\n }\n }\n } finally {\n reader.releaseLock();\n }\n } catch (error) {\n console.error(`[GlobalAudioSession] Decoder error for clip ${clipId}:`, error);\n throw error;\n } finally {\n await decoder.close();\n }\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"],"names":["resource"],"mappings":"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"}
1
+ {"version":3,"file":"GlobalAudioSession.js","sources":["../../src/orchestrator/GlobalAudioSession.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\nimport { OfflineAudioMixer } from '../stages/compose/OfflineAudioMixer';\nimport type { CompositionModel } from '../model';\nimport type { WorkerPool } from '../worker/WorkerPool';\nimport type { ResourceLoader } from '../stages/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 { AudioMixBlockCache } from '../cache/AudioMixBlockCache';\nimport { AudioChunkEncoder } from '../stages/encode/AudioChunkEncoder';\nimport { AudioChunkDecoder } from '../stages/decode/AudioChunkDecoder';\nimport { isAudioClip, hasResourceId } from '../model/types';\nimport type { RequestMode } from './types';\n\ninterface AudioDataMessage {\n sessionId: string;\n audioData: AudioData;\n clipStartUs: TimeUs;\n clipDurationUs: TimeUs;\n}\n\ninterface AudioSessionDeps {\n cacheManager: CacheManager;\n workerPool: WorkerPool;\n resourceLoader: ResourceLoader;\n eventBus: EventBus<EventPayloadMap>;\n buildWorkerConfigs: () => any;\n}\n\nexport class GlobalAudioSession {\n private mixer: OfflineAudioMixer;\n private activeClips = new Set<string>();\n private deps: AudioSessionDeps;\n private model: CompositionModel | null = null;\n private audioContext: AudioContext | null = null;\n private volume = 1.0;\n private playbackRate = 1.0;\n private isPlaying = false;\n\n // Preview strategy (unified):\n // - Always schedule audio in fixed 60s \"mix blocks\"\n // - Cache 2~3 mixed AudioBuffer blocks (LRU) to accelerate seek\n // - Schedule ahead using AudioContext clock to avoid underrun\n private readonly PREVIEW_BLOCK_DURATION_US: TimeUs = 60 * 1_000_000; // 60s\n private readonly PREVIEW_BLOCK_CACHE_SIZE = 3;\n private readonly PREVIEW_SCHEDULE_AHEAD_SEC = 12.0; // keep enough scheduled audio to hide mixing latency\n private readonly PREVIEW_BUFFER_GUARD_US: TimeUs = 2_000_000; // if next block isn't ready near boundary -> buffering\n private readonly PREVIEW_BLOCK_FADE_SEC = 0.01; // 10ms fade-in/out to avoid click at boundaries\n\n private previewBlockCache = new AudioMixBlockCache(this.PREVIEW_BLOCK_CACHE_SIZE);\n\n private previewScheduleTask: Promise<void> | null = null;\n private previewScheduleToken = 0;\n private previewMixToken = 0;\n private previewNextBlockIndex = 0;\n private previewNextScheduleTime = 0; // AudioContext time\n private previewFirstBlockOffsetUs: TimeUs = 0; // seek offset within the first block\n private previewLastTimelineUs: TimeUs = 0;\n private previewLastStallWarnAt = 0; // AudioContext time (sec)\n\n private previewScheduledSources = new Set<{ source: AudioBufferSourceNode; gain: GainNode }>();\n private previewMixQueue: Promise<unknown> = Promise.resolve();\n\n constructor(deps: AudioSessionDeps) {\n this.deps = deps;\n this.mixer = new OfflineAudioMixer(deps.cacheManager, () => this.model);\n }\n\n private enqueuePreviewMix<T>(work: () => Promise<T>, token: number): Promise<T | null> {\n const run = () => work();\n const next = this.previewMixQueue.then(\n () => {\n // If a seek/reset happened since this task was enqueued, drop it.\n if (this.previewMixToken !== token) {\n return null;\n }\n return run();\n },\n () => {\n if (this.previewMixToken !== token) {\n return null;\n }\n return run();\n }\n );\n // Keep queue alive even if a task fails.\n this.previewMixQueue = next.then(\n () => undefined,\n () => undefined\n );\n return next as Promise<T | null>;\n }\n\n setModel(model: CompositionModel): void {\n this.model = model;\n }\n\n onAudioData(message: AudioDataMessage): void {\n const { sessionId, audioData, clipStartUs, clipDurationUs } = message;\n const globalTimeUs = clipStartUs + (audioData.timestamp ?? 0);\n this.deps.cacheManager.putClipAudioData(sessionId, audioData, clipDurationUs, globalTimeUs);\n }\n\n async ensureAudioForTime(timeUs: TimeUs, options?: { mode?: RequestMode }): Promise<void> {\n const model = this.model;\n if (!model) return;\n\n const mode = options?.mode ?? 'blocking';\n\n // Preview contract:\n // - blocking: ensure the current 60s mixed block is ready (may be slow -> should be wrapped by buffering UI)\n // - probe: best-effort preheat current and next block without blocking\n const blockIndex = Math.floor(Math.max(0, timeUs) / this.PREVIEW_BLOCK_DURATION_US);\n\n if (mode === 'probe') {\n // Default: only preheat the current block.\n // Preheating next block too early makes scrubbing heavier than needed.\n void this.getOrCreateMixedBlock(blockIndex);\n\n // If we're close enough to boundary (within scheduling lookahead), also preheat next block.\n const blockEndUs = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;\n const remainingToBoundaryUs = blockEndUs - Math.max(0, timeUs);\n const lookaheadUs = Math.floor(this.PREVIEW_SCHEDULE_AHEAD_SEC * 1_000_000);\n if (remainingToBoundaryUs > 0 && remainingToBoundaryUs <= lookaheadUs) {\n void this.getOrCreateMixedBlock(blockIndex + 1);\n }\n return;\n }\n\n await this.getOrCreateMixedBlock(blockIndex);\n\n // If we're very close to the block boundary, also ensure the next block.\n // This supports the buffering flow: when we enter buffering near boundary,\n // EnsureAudio(blocking) will actually make the next block ready.\n const blockEndUs = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;\n const remainingToBoundaryUs = blockEndUs - Math.max(0, timeUs);\n if (remainingToBoundaryUs > 0 && remainingToBoundaryUs <= this.PREVIEW_BUFFER_GUARD_US) {\n await this.getOrCreateMixedBlock(blockIndex + 1);\n }\n }\n\n isPreviewMixBlockCached(timeUs: TimeUs): boolean {\n const blockIndex = Math.floor(Math.max(0, timeUs) / this.PREVIEW_BLOCK_DURATION_US);\n return this.previewBlockCache.get(blockIndex) !== null;\n }\n\n shouldEnterBufferingForUpcomingPreviewAudio(timeUs: TimeUs): boolean {\n const model = this.model;\n if (!model) return false;\n\n const clampedUs = Math.max(0, timeUs);\n const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);\n const nextBlockStartUs = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;\n if (nextBlockStartUs >= model.durationUs) return false;\n\n const remainingToBoundaryUs = nextBlockStartUs - clampedUs;\n if (remainingToBoundaryUs > this.PREVIEW_BUFFER_GUARD_US) return false;\n\n // Probe next block readiness by checking cache at next block start time.\n return !this.isPreviewMixBlockCached(nextBlockStartUs);\n }\n\n /**\n * Fast readiness probe for preview playback.\n *\n * This is intentionally synchronous and lightweight:\n * - Only checks resource-level readiness (download + MP4 index parsing).\n * - If any relevant resource isn't ready yet, return false.\n * - Does NOT require audio samples / PCM window coverage (probe is resource-level only).\n *\n * Note: This probe does NOT gate on PCM coverage to avoid frequent buffering oscillation.\n * PCM is prepared incrementally by scheduleAudio() / ensureAudioForTimeRange().\n */\n isAudioResourceWindowReady(startUs: TimeUs, endUs: TimeUs): boolean {\n const model = this.model;\n if (!model) return true;\n\n const activeClips = model.getActiveClips(startUs, endUs);\n\n for (const clip of activeClips) {\n if (clip.trackKind !== 'audio' && clip.trackKind !== 'video') continue;\n if (!hasResourceId(clip)) continue;\n\n const resource = model.getResource(clip.resourceId);\n if (resource?.state !== 'ready') {\n return false;\n }\n }\n\n return true;\n }\n\n async activateAllAudioClips(): Promise<void> {\n const model = this.model;\n if (!model) {\n return;\n }\n\n const audioTracks = model.tracks.filter((track) => track.kind === 'audio');\n if (audioTracks.length === 0) return;\n\n // Find maximum clip count across all audio tracks\n const maxClipCount = Math.max(...audioTracks.map((track) => track.clips.length));\n\n // Horizontal loading: activate clip[0] from all tracks, then clip[1], etc.\n for (let clipIndex = 0; clipIndex < maxClipCount; clipIndex++) {\n for (const track of audioTracks) {\n const clip = track.clips[clipIndex];\n if (!clip || this.activeClips.has(clip.id)) continue;\n\n if (!isAudioClip(clip)) {\n throw new Error(`Clip ${clip.id} in audio track is not an audio clip`);\n }\n\n // Preview: Use main-thread parsing → AudioSampleCache → on-demand decode\n // Check if we have cached samples (already parsed in ResourceLoader)\n if (this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n // Already parsed, mark as active\n this.activeClips.add(clip.id);\n this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });\n continue;\n }\n\n // Ensure resource is loaded (will be parsed and cached in main thread)\n await this.deps.resourceLoader.load(clip.resourceId, {\n isPreload: false,\n clipId: clip.id,\n trackId: track.id,\n });\n\n // Mark as active\n this.activeClips.add(clip.id);\n this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });\n }\n }\n }\n\n async deactivateClip(clipId: string): Promise<void> {\n if (!this.activeClips.has(clipId)) {\n return;\n }\n\n this.activeClips.delete(clipId);\n this.deps.cacheManager.clearClipAudioData(clipId);\n }\n\n async startPlayback(timeUs: TimeUs, audioContext: AudioContext): Promise<void> {\n this.audioContext = audioContext;\n\n // Resume AudioContext if suspended (required by modern browsers)\n if (audioContext.state === 'suspended') {\n await audioContext.resume();\n }\n\n // Ensure audio is decoded and ready (blocking mode for startup).\n // We do this before mixing to reduce \"missing PCM\" risk.\n await this.ensureAudioForTime(timeUs, { mode: 'blocking' });\n\n this.isPlaying = true;\n\n // Unified block scheduling: align to block index and schedule immediately.\n this.startPreviewBlockScheduling(timeUs, audioContext);\n // Schedule first block in blocking mode to avoid initial silence.\n await this.scheduleNextPreviewBlock(audioContext, this.previewScheduleToken);\n // Then keep scheduling in background.\n void this.scheduleAudio(timeUs, audioContext);\n }\n\n stopPlayback(): void {\n this.isPlaying = false;\n this.stopAllPreviewSources();\n this.cancelPreviewBlockScheduling();\n this.previewMixToken += 1;\n }\n\n updateTime(_timeUs: TimeUs): void {\n // Kept for compatibility\n }\n\n /**\n * Schedule audio chunks ahead of playback cursor\n * Uses OfflineAudioMixer for proper mixing, then plays the result\n */\n async scheduleAudio(currentTimelineUs: TimeUs, audioContext: AudioContext): Promise<void> {\n if (!this.isPlaying || !this.model || !this.audioContext) {\n return;\n }\n\n this.previewLastTimelineUs = currentTimelineUs;\n\n // Keep scheduling in the background to avoid blocking the render loop.\n if (this.previewScheduleTask) return;\n\n // Initialize if needed (e.g. after model switch without explicit startPlayback).\n if (this.previewNextScheduleTime === 0) {\n this.startPreviewBlockScheduling(currentTimelineUs, audioContext);\n }\n\n const token = this.previewScheduleToken;\n this.previewScheduleTask = this.runPreviewBlockSchedulingLoop(audioContext, token).finally(\n () => {\n if (this.previewScheduleToken === token) {\n this.previewScheduleTask = null;\n }\n }\n );\n }\n\n /**\n * Reset playback states (called on seek)\n */\n resetPlaybackStates(): void {\n this.stopAllPreviewSources();\n this.cancelPreviewBlockScheduling();\n this.previewMixToken += 1;\n }\n\n setVolume(volume: number): void {\n this.volume = volume;\n const audioContext = this.audioContext;\n if (!audioContext) return;\n\n // Apply volume to already scheduled nodes (small ramp to avoid click).\n const t = audioContext.currentTime;\n for (const { gain } of this.previewScheduledSources) {\n try {\n gain.gain.cancelScheduledValues(t);\n gain.gain.setValueAtTime(gain.gain.value, t);\n gain.gain.linearRampToValueAtTime(volume, t + 0.01);\n } catch {\n // ignore\n }\n }\n }\n\n setPlaybackRate(rate: number): void {\n this.playbackRate = rate;\n // Playback rate change requires restarting scheduling to keep sync with the timeline clock.\n this.resetPlaybackStates();\n }\n\n private startPreviewBlockScheduling(startTimelineUs: TimeUs, audioContext: AudioContext): void {\n this.cancelPreviewBlockScheduling();\n this.stopAllPreviewSources();\n\n const clampedUs = Math.max(0, startTimelineUs);\n const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);\n const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;\n\n this.previewFirstBlockOffsetUs = clampedUs - blockStartUs;\n this.previewNextBlockIndex = blockIndex;\n this.previewNextScheduleTime = audioContext.currentTime + 0.02;\n this.previewLastTimelineUs = startTimelineUs;\n }\n\n private cancelPreviewBlockScheduling(): void {\n this.previewScheduleToken += 1;\n this.previewScheduleTask = null;\n this.previewNextBlockIndex = 0;\n this.previewNextScheduleTime = 0;\n this.previewFirstBlockOffsetUs = 0;\n this.previewLastTimelineUs = 0;\n }\n\n private realignPreviewSchedulingToTimeline(audioContext: AudioContext): void {\n const model = this.model;\n if (!model) return;\n\n const clampedUs = Math.max(0, this.previewLastTimelineUs);\n const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);\n const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;\n if (blockStartUs >= model.durationUs) return;\n\n this.stopAllPreviewSources();\n this.previewFirstBlockOffsetUs = clampedUs - blockStartUs;\n this.previewNextBlockIndex = blockIndex;\n this.previewNextScheduleTime = audioContext.currentTime + 0.02;\n }\n\n private async runPreviewBlockSchedulingLoop(\n audioContext: AudioContext,\n token: number\n ): Promise<void> {\n while (this.isPlaying && this.previewScheduleToken === token) {\n const model = this.model;\n if (!model) return;\n\n // End-of-timeline: nothing more to schedule. Mark scheduling as \"far enough\" so future\n // scheduleAudio() calls won't spin.\n const nextBlockStartUs = this.previewNextBlockIndex * this.PREVIEW_BLOCK_DURATION_US;\n if (nextBlockStartUs >= model.durationUs) {\n this.previewNextScheduleTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;\n return;\n }\n\n const scheduleAheadTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;\n if (this.previewNextScheduleTime >= scheduleAheadTime) return;\n\n const prevBlockIndex = this.previewNextBlockIndex;\n const prevScheduleTime = this.previewNextScheduleTime;\n\n await this.scheduleNextPreviewBlock(audioContext, token);\n\n // If scheduling made no progress, bail out to avoid a tight loop that can freeze UI.\n // The next RAF tick will call scheduleAudio() again after some time has advanced.\n if (\n this.previewScheduleToken === token &&\n this.previewNextBlockIndex === prevBlockIndex &&\n this.previewNextScheduleTime === prevScheduleTime\n ) {\n // Throttled diagnostic: log at most once per second to avoid console spam.\n const now = audioContext.currentTime;\n if (now - this.previewLastStallWarnAt >= 1) {\n this.previewLastStallWarnAt = now;\n console.warn(\n '[GlobalAudioSession][preview] scheduling stalled; stop loop to avoid spin',\n {\n prevBlockIndex,\n prevScheduleTime,\n now,\n }\n );\n }\n return;\n }\n }\n }\n\n private async getOrCreateMixedBlock(blockIndex: number): Promise<AudioBuffer | null> {\n const model = this.model;\n if (!model) return null;\n\n const token = this.previewMixToken;\n return await this.previewBlockCache.getOrCreate(blockIndex, async () => {\n return await this.enqueuePreviewMix(async () => {\n const startUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;\n const endUs = Math.min(startUs + this.PREVIEW_BLOCK_DURATION_US, model.durationUs);\n await this.ensureAudioForTimeRange(startUs, endUs, {\n mode: 'blocking',\n loadResource: true,\n });\n const mixed = await this.mixer.mix(startUs, endUs);\n\n // Preview uses mixed AudioBuffer blocks as the primary cache. PCM in AudioL1Cache is a transient\n // intermediate and can be cleared after each block to keep memory bounded (same idea as export).\n this.deps.cacheManager.clearAudioCache();\n\n return mixed;\n }, token);\n });\n }\n\n private async scheduleNextPreviewBlock(audioContext: AudioContext, token: number): Promise<void> {\n const model = this.model;\n if (!this.isPlaying || !model) return;\n if (this.previewScheduleToken !== token) return;\n\n // If we're behind the audio clock, resync to avoid attempting to schedule in the past.\n if (this.previewNextScheduleTime < audioContext.currentTime + 0.01) {\n // Important: when we fall behind (e.g. mixing slower than expected), we must re-align\n // blockIndex + offset to current timeline; otherwise we'd restart the block from its beginning\n // and cause A/V desync.\n this.realignPreviewSchedulingToTimeline(audioContext);\n }\n\n const blockIndex = this.previewNextBlockIndex;\n const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;\n if (blockStartUs >= model.durationUs) {\n // End-of-timeline: advance state so scheduling loop can finish without stalling.\n this.previewFirstBlockOffsetUs = 0;\n this.previewNextBlockIndex = blockIndex + 1;\n this.previewNextScheduleTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;\n return;\n }\n\n const buffer = await this.getOrCreateMixedBlock(blockIndex);\n if (!buffer) {\n // Could be cancelled (seek/reset) or an empty-range guard; advance to avoid stalling.\n this.previewFirstBlockOffsetUs = 0;\n this.previewNextBlockIndex = blockIndex + 1;\n this.previewNextScheduleTime = audioContext.currentTime + 0.02;\n return;\n }\n if (this.previewScheduleToken !== token) return;\n\n const rate = this.playbackRate || 1.0;\n const offsetUs = this.previewFirstBlockOffsetUs;\n let startTime = this.previewNextScheduleTime;\n let offsetSec = offsetUs > 0 ? offsetUs / 1_000_000 : 0;\n\n // Mixing can be slow; after await, startTime might already be in the past.\n // If so, resync startTime to \"now\" and advance offset accordingly to preserve A/V sync.\n const now = audioContext.currentTime;\n if (startTime < now + 0.01) {\n const targetStartTime = now + 0.02;\n const skippedSec = Math.max(0, (targetStartTime - startTime) * rate);\n startTime = targetStartTime;\n offsetSec += skippedSec;\n }\n\n // If the computed offset already exceeds this block's buffer duration, it means the timeline\n // has moved past this block while we were preparing. Skipping the block is required to:\n // - avoid scheduling a zero-length source\n // - avoid a tight scheduling loop (remainingSec <= 0 would return without advancing state)\n if (offsetSec >= buffer.duration) {\n this.previewFirstBlockOffsetUs = 0;\n this.previewNextBlockIndex = blockIndex + 1;\n this.previewNextScheduleTime = startTime;\n return;\n }\n\n const remainingSec = Math.max(0, buffer.duration - offsetSec);\n if (remainingSec <= 0) return;\n\n const source = audioContext.createBufferSource();\n source.buffer = buffer;\n source.playbackRate.value = rate;\n\n const gainNode = audioContext.createGain();\n const volume = this.volume;\n\n // Fade in/out to avoid click at block boundaries (no overlap in timeline; preserves A/V timing).\n const fadeSec = Math.min(this.PREVIEW_BLOCK_FADE_SEC, remainingSec / 2);\n gainNode.gain.setValueAtTime(0, startTime);\n gainNode.gain.linearRampToValueAtTime(volume, startTime + fadeSec);\n gainNode.gain.setValueAtTime(volume, startTime + Math.max(fadeSec, remainingSec - fadeSec));\n gainNode.gain.linearRampToValueAtTime(0, startTime + remainingSec);\n\n source.connect(gainNode);\n gainNode.connect(audioContext.destination);\n source.start(startTime, offsetSec);\n\n const entry = { source, gain: gainNode };\n this.previewScheduledSources.add(entry);\n\n source.onended = () => {\n try {\n source.disconnect();\n gainNode.disconnect();\n } catch {\n // ignore\n }\n this.previewScheduledSources.delete(entry);\n };\n\n // Advance to next block\n this.previewFirstBlockOffsetUs = 0;\n this.previewNextBlockIndex = blockIndex + 1;\n this.previewNextScheduleTime = startTime + remainingSec / rate;\n }\n\n private stopAllPreviewSources(): void {\n for (const { source, gain } of this.previewScheduledSources) {\n // Best-effort cleanup:\n // AudioNode.stop/disconnect can throw depending on node state (not started yet, already stopped, etc).\n // We intentionally isolate each call so one failure doesn't prevent other nodes from being stopped.\n try {\n source.disconnect();\n } catch {\n // ignore\n }\n try {\n // stop(0) for immediate stop; may throw if not started yet\n source.stop(0);\n } catch {\n // ignore\n }\n try {\n gain.disconnect();\n } catch {\n // ignore\n }\n }\n this.previewScheduledSources.clear();\n }\n\n reset(): void {\n this.stopAllPreviewSources();\n this.deps.cacheManager.clearAudioCache();\n this.activeClips.clear();\n this.previewBlockCache.clear();\n this.cancelPreviewBlockScheduling();\n this.previewMixToken += 1;\n }\n\n /**\n * Mix and encode audio for a specific segment (used by ExportScheduler)\n */\n async mixAndEncodeSegment(\n startUs: TimeUs,\n endUs: TimeUs,\n onChunk: (chunk: EncodedAudioChunk, metadata?: EncodedAudioChunkMetadata) => void\n ): Promise<void> {\n // Wait for audio clips in this time range to be ready (on-demand wait)\n await this.ensureAudioForSegment(startUs, endUs);\n\n const mixedBuffer = await this.mixer.mix(startUs, endUs);\n const audioData = this.audioBufferToAudioData(mixedBuffer, startUs);\n\n if (!audioData) return;\n\n if (!this.exportEncoder) {\n this.exportEncoder = new AudioChunkEncoder();\n await this.exportEncoder.initialize();\n this.exportEncoderStream = this.exportEncoder.createStream();\n this.exportEncoderWriter = this.exportEncoderStream.writable.getWriter();\n\n // Start reader immediately (but don't await - it's a long-running loop)\n void this.startExportEncoderReader(this.exportEncoderStream.readable, onChunk);\n\n // Wait a bit to ensure reader is ready before first write\n await new Promise((resolve) => setTimeout(resolve, 10));\n }\n\n await this.exportEncoderWriter?.write(audioData);\n }\n\n /**\n * Ensure audio clips in time range are decoded (for export)\n * Decodes from AudioSampleCache (replaces Worker pipeline)\n */\n private async ensureAudioForSegment(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n // Export mode: don't load resources (they should already be loaded), only decode cached samples\n // Use strictMode=true to ensure 99% coverage for high-quality export\n await this.ensureAudioForTimeRange(startUs, endUs, {\n mode: 'blocking',\n loadResource: false,\n strictMode: true,\n });\n }\n\n private exportEncoder: AudioChunkEncoder | null = null;\n private exportEncoderStream: TransformStream<\n AudioData,\n { chunk: EncodedAudioChunk; metadata: any }\n > | null = null;\n private exportEncoderWriter: WritableStreamDefaultWriter<AudioData> | null = null;\n\n private async startExportEncoderReader(\n stream: ReadableStream<{ chunk: EncodedAudioChunk; metadata: any }>,\n onChunk: (chunk: EncodedAudioChunk, metadata?: EncodedAudioChunkMetadata) => void\n ) {\n const reader = stream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n onChunk(value.chunk, value.metadata);\n }\n }\n } catch (e) {\n console.error('Export encoder reader error', e);\n }\n }\n\n async finalizeExportAudio(): Promise<void> {\n if (this.exportEncoderWriter) {\n await this.exportEncoderWriter.close();\n this.exportEncoderWriter = null;\n }\n this.exportEncoder = null;\n this.exportEncoderStream = null;\n }\n\n // Preview source scheduling is managed by stopAllPreviewSources()/previewScheduledSources.\n\n /**\n * Core method to ensure audio for all clips in a time range\n * Unified implementation used by ensureAudioForTime, scheduleAudio, and export\n */\n private async ensureAudioForTimeRange(\n startUs: TimeUs,\n endUs: TimeUs,\n options: { mode?: RequestMode; loadResource?: boolean; strictMode?: boolean }\n ): Promise<void> {\n const model = this.model;\n if (!model) return;\n\n const { mode = 'blocking', loadResource = true, strictMode = false } = options;\n\n // Find all clips that overlap with [startUs, endUs]\n const activeClips = model.getActiveClips(startUs, endUs);\n\n const ensurePromises = activeClips.map(async (clip) => {\n // Only process audio and video clips\n if (clip.trackKind !== 'audio' && clip.trackKind !== 'video') return;\n if (!hasResourceId(clip)) return;\n\n // Skip clips without audio track (performance optimization)\n // If AudioSampleCache doesn't have this resource, and resource is ready,\n // it means the resource has no audio track (e.g., video-only or image)\n const resource = model.getResource(clip.resourceId);\n if (\n resource?.state === 'ready' &&\n !this.deps.cacheManager.audioSampleCache.has(clip.resourceId)\n ) {\n // Resource is ready but has no audio samples - skip\n return;\n }\n\n // Ensure AudioSampleCache has data\n if (!this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n if (!loadResource) {\n // Export mode: skip clips without cached samples\n return;\n }\n\n const resource = model.getResource(clip.resourceId);\n if (resource?.state !== 'ready') {\n // Resource not yet loaded - wait for it\n await this.deps.resourceLoader.load(clip.resourceId, {\n isPreload: false,\n clipId: clip.id,\n trackId: clip.trackId,\n });\n }\n }\n\n // Calculate clip-relative time range\n const clipRelativeStartUs = Math.max(0, startUs - clip.startUs);\n const clipRelativeEndUs = Math.min(clip.durationUs, endUs - clip.startUs);\n\n // Convert to resource time (aligned with video architecture)\n // This ensures correct filtering of audio samples and cache queries\n const trimStartUs = clip.trimStartUs ?? 0;\n const resourceStartUs = clipRelativeStartUs + trimStartUs;\n const resourceEndUs = clipRelativeEndUs + trimStartUs;\n\n // Ensure audio window using resource time coordinates\n await this.ensureAudioWindow(clip.id, resourceStartUs, resourceEndUs, strictMode);\n });\n\n if (mode === 'probe') {\n void Promise.all(ensurePromises);\n return;\n }\n await Promise.all(ensurePromises);\n }\n\n /**\n * Ensure audio window for a clip (aligned with video architecture)\n *\n * Note: Unlike video getFrame(), this method doesn't need a 'preheat' parameter\n * Why: Audio cache check is window-level (range query) via hasWindowPCM()\n * It verifies the entire window has ≥95% data (preview) or ≥99% (export)\n * This naturally prevents premature return during preheating\n */\n async ensureAudioWindow(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs,\n strictMode: boolean = false\n ): Promise<void> {\n // Check L1 cache - window-level verification (not point-level like video)\n if (this.deps.cacheManager.hasWindowPCM(clipId, startUs, endUs, strictMode)) {\n return; // Entire window has sufficient data\n }\n\n await this.decodeAudioWindow(clipId, startUs, endUs);\n }\n\n /**\n * Decode audio window for a clip (aligned with video architecture)\n * Incremental decoding strategy with smart fallback:\n * - High coverage (≥80%): Skip decoding\n * - Low coverage (<30%): Full decode (avoid fragmentation)\n * - Medium coverage (30%-80%): Incremental decode\n */\n private async decodeAudioWindow(clipId: string, startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const clip = this.model?.findClip(clipId);\n if (!clip || !hasResourceId(clip)) {\n return;\n }\n\n // Get audio samples from AudioSampleCache\n const audioRecord = this.deps.cacheManager.audioSampleCache.get(clip.resourceId);\n if (!audioRecord) {\n // Resource has no audio track (common for some video files)\n return;\n }\n\n // Filter chunks within window (aligned with video GOP filtering)\n const windowChunks = audioRecord.samples.filter((s) => {\n const sampleEndUs = s.timestamp + (s.duration ?? 0);\n return s.timestamp < endUs && sampleEndUs > startUs;\n });\n\n if (windowChunks.length === 0) {\n return;\n }\n\n // Incremental decoding with smart threshold strategy\n // SKIP threshold should be high to avoid audio gaps (e.g., 95% means max 5% missing data)\n const INCREMENTAL_THRESHOLD = 0.95; // 95% coverage: skip decode (was 0.8, increased to avoid audio gaps)\n const FULL_FALLBACK_THRESHOLD = 0.3; // <30% coverage: full decode to avoid fragmentation\n\n // Check window-level coverage\n const coverage = this.deps.cacheManager.getAudioRangeCoverage(\n clipId,\n startUs,\n endUs,\n INCREMENTAL_THRESHOLD\n );\n\n // Strategy 1: High coverage - skip decode entirely\n if (coverage.covered) {\n return;\n }\n\n // Strategy 2: Very low coverage - full decode (avoid fragmentation overhead)\n if (coverage.coverageRatio < FULL_FALLBACK_THRESHOLD) {\n await this.decodeAudioSamples(\n clipId,\n windowChunks,\n audioRecord.metadata,\n clip.durationUs,\n clip.startUs\n );\n return;\n }\n\n // Strategy 3: Medium coverage - incremental decode (30%-95%)\n // Filter out chunks that are already well-covered in L1 Cache\n const chunksToDecode = windowChunks.filter((chunk) => {\n const chunkEndUs = chunk.timestamp + (chunk.duration ?? 0);\n const chunkCoverage = this.deps.cacheManager.getAudioRangeCoverage(\n clipId,\n chunk.timestamp,\n chunkEndUs,\n 0.95 // Stricter threshold for individual chunks\n );\n return !chunkCoverage.covered;\n });\n\n if (chunksToDecode.length === 0) {\n return;\n }\n\n // Decode only missing chunks\n await this.decodeAudioSamples(\n clipId,\n chunksToDecode,\n audioRecord.metadata,\n clip.durationUs,\n clip.startUs\n );\n }\n\n /**\n * Decode audio samples to PCM and cache\n * Uses AudioChunkDecoder for consistency with project architecture\n * Resamples to AudioContext sample rate if needed for better quality\n */\n private async decodeAudioSamples(\n clipId: string,\n samples: EncodedAudioChunk[],\n config: AudioDecoderConfig,\n clipDurationUs: number,\n clipStartUs: TimeUs\n ): Promise<void> {\n // Use AudioChunkDecoder for consistency with project architecture\n // Convert description to ArrayBuffer if needed for type compatibility\n let description: ArrayBuffer | undefined;\n if (config.description) {\n if (config.description instanceof ArrayBuffer) {\n description = config.description;\n } else if (ArrayBuffer.isView(config.description)) {\n // Convert TypedArray to ArrayBuffer\n const view = config.description as Uint8Array;\n // Create a new ArrayBuffer and copy data to ensure proper type\n const newBuffer = new ArrayBuffer(view.byteLength);\n new Uint8Array(newBuffer).set(\n new Uint8Array(view.buffer, view.byteOffset, view.byteLength)\n );\n description = newBuffer;\n }\n }\n\n const decoderConfig = {\n codec: config.codec,\n sampleRate: config.sampleRate,\n numberOfChannels: config.numberOfChannels,\n description,\n };\n const decoder = new AudioChunkDecoder(`audio-${clipId}`, decoderConfig);\n\n try {\n // Create chunk stream\n const chunkStream = new ReadableStream<EncodedAudioChunk>({\n start(controller) {\n for (const sample of samples) {\n controller.enqueue(sample);\n }\n controller.close();\n },\n });\n\n // Decode through stream\n const audioDataStream = chunkStream.pipeThrough(decoder.createStream());\n const reader = audioDataStream.getReader();\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n if (value) {\n // Store original sample rate - OfflineAudioMixer will handle resampling\n const globalTimeUs = clipStartUs + (value.timestamp ?? 0);\n this.deps.cacheManager.putClipAudioData(clipId, value, clipDurationUs, globalTimeUs);\n }\n }\n } finally {\n reader.releaseLock();\n }\n } catch (error) {\n console.error(`[GlobalAudioSession] Decoder error for clip ${clipId}:`, error);\n throw error;\n } finally {\n await decoder.close();\n }\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-planar',\n sampleRate,\n numberOfFrames,\n numberOfChannels,\n timestamp: timestampUs,\n data: this.packPlanarF32Data(planes),\n });\n }\n\n private packPlanarF32Data(planes: Float32Array[]): ArrayBuffer {\n const numberOfChannels = planes.length;\n const numberOfFrames = planes[0]?.length ?? 0;\n const totalSamples = numberOfChannels * numberOfFrames;\n const packed = new Float32Array(totalSamples);\n\n // f32-planar layout: [ch0 frames][ch1 frames]...[chN frames]\n for (let channel = 0; channel < numberOfChannels; channel++) {\n const plane = planes[channel];\n if (!plane) continue;\n packed.set(plane, channel * numberOfFrames);\n }\n\n return packed.buffer;\n }\n}\n"],"names":["blockEndUs","remainingToBoundaryUs","resource"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"OnDemandVideoSession.d.ts","sourceRoot":"","sources":["../../src/orchestrator/OnDemandVideoSession.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,QAAQ,EAAO,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,MAAM,EAAY,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AASpE,UAAU,0BAA0B;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,YAAY,CAAC;IAC3B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,cAAc,EAAE,cAAc,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,oBAAoB;IAC/B;;;OAGG;WACU,wBAAwB,CACnC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,iBAAiB,EAAE,EAC3B,KAAK,EAAE,QAAQ,EACf,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,YAAY,EAC1B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC;IAoChB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmB;IACpD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAiB;IAChD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IAEtC,OAAO,CAAC,OAAO,CAA6B;IAC5C,UAAU,UAAS;IACnB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,aAAa,CAAoB;IAEzC,OAAO;WAYM,MAAM,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAAC,oBAAoB,CAAC;YAMxE,IAAI;IAIlB;;OAEG;IACG,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBjE;;OAEG;YACW,mBAAmB;IAsBjC;;OAEG;YACW,mBAAmB;IAsCjC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAO5B,OAAO,CAAC,2BAA2B;IA8CnC;;;;;;OAMG;YACW,YAAY;YA0DZ,YAAY;IAiB1B;;OAEG;IACH,OAAO,CAAC,UAAU;YAcJ,kBAAkB;IA8BhC;;;OAGG;IACG,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAyClE;;;OAGG;IACG,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;IA+FxF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAoB/B"}
1
+ {"version":3,"file":"OnDemandVideoSession.d.ts","sourceRoot":"","sources":["../../src/orchestrator/OnDemandVideoSession.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,QAAQ,EAAO,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,MAAM,EAAY,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AASpE,UAAU,0BAA0B;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,YAAY,CAAC;IAC3B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,cAAc,EAAE,cAAc,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,oBAAoB;IAC/B;;;OAGG;WACU,wBAAwB,CACnC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,iBAAiB,EAAE,EAC3B,KAAK,EAAE,QAAQ,EACf,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,YAAY,EAC1B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC;IAoChB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmB;IACpD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAiB;IAChD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IAEtC,OAAO,CAAC,OAAO,CAA6B;IAC5C,UAAU,UAAS;IACnB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,aAAa,CAAoB;IAEzC,OAAO;WAYM,MAAM,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAAC,oBAAoB,CAAC;YAMxE,IAAI;IAIlB;;OAEG;IACG,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BjE;;OAEG;YACW,mBAAmB;IAsBjC;;OAEG;YACW,mBAAmB;IAsCjC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAO5B,OAAO,CAAC,2BAA2B;IA8CnC;;;;;;OAMG;YACW,YAAY;YA0DZ,YAAY;IAiB1B;;OAEG;IACH,OAAO,CAAC,UAAU;YAcJ,kBAAkB;IA8BhC;;;OAGG;IACG,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAyClE;;;OAGG;IACG,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;IA+FxF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAoB/B"}
@@ -74,6 +74,19 @@ class OnDemandVideoSession {
74
74
  }
75
75
  const resource = this.compositionModel.getResource(this.resourceId);
76
76
  if (!resource) {
77
+ console.warn("[OnDemandVideoSession] Resource not found in composition model:", {
78
+ resourceId: this.resourceId,
79
+ clipId: this.clipId,
80
+ startUs,
81
+ endUs,
82
+ model: {
83
+ fps: this.compositionModel.fps,
84
+ durationUs: this.compositionModel.durationUs,
85
+ mainTrackId: this.compositionModel.mainTrackId,
86
+ trackCount: this.compositionModel.tracks.length,
87
+ resourcesSize: this.compositionModel.resources.size
88
+ }
89
+ });
77
90
  throw new Error(`Resource not found: ${this.resourceId}`);
78
91
  }
79
92
  if (resource.type === "image") {
@@ -1 +1 @@
1
- {"version":3,"file":"OnDemandVideoSession.js","sources":["../../src/orchestrator/OnDemandVideoSession.ts"],"sourcesContent":["import type { CacheManager } from '../cache/CacheManager';\nimport type { MP4IndexCache } from '../cache/resource/MP4IndexCache';\nimport type { MP4Index, GOP } from '../stages/demux/types';\nimport type { TimeUs, Resource } from '../model/types';\nimport type { CompositionModel, Clip } from '../model';\nimport { binarySearchOverlapping } from '../utils/binary-search';\nimport type { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { decodeChunksWithoutFlush } from '../utils/video-decoder-helpers';\n\ninterface GOPWindowResult {\n gops: GOP[];\n byteStart: number;\n byteEnd: number;\n}\n\ninterface OnDemandVideoSessionConfig {\n clipId: string;\n resourceId: string;\n targetTimeUs: TimeUs;\n globalTimeUs: TimeUs;\n mp4IndexCache: MP4IndexCache;\n cacheManager: CacheManager;\n compositionModel: CompositionModel;\n resourceLoader: ResourceLoader;\n fps: number;\n}\n\n/**\n * OnDemandVideoSession - Main-thread on-demand decoder\n *\n * Strategy:\n * 1. Read GOP range from OPFS\n * 2. Demux using MP4Box (main thread)\n * 3. Decode using VideoDecoder (main thread)\n * 4. Write RAW VideoFrames (YUV) to L1 cache\n * 5. Dispose immediately after window completes\n *\n * Why main thread?\n * - Window is small (±2s, ~60-120 frames)\n * - Worker overhead (10-50ms) is significant for small workloads\n * - Decode is fast enough\n */\nexport class OnDemandVideoSession {\n /**\n * Static method to decode and cache first frame from extracted GOP chunks\n * Used by ResourceLoader during streaming download for fast cover rendering\n */\n static async decodeAndCacheFirstFrame(\n _resourceId: string,\n chunks: EncodedVideoChunk[],\n index: MP4Index,\n clip: Clip,\n cacheManager: CacheManager,\n fps: number\n ): Promise<void> {\n if (chunks.length === 0) return;\n\n const videoTrack = index.tracks.video;\n if (!videoTrack) return;\n\n // Verify first chunk is keyframe\n const firstChunk = chunks[0];\n if (!firstChunk || firstChunk.type !== 'key') {\n return;\n }\n\n try {\n const result = await decodeChunksWithoutFlush(chunks, {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n });\n\n // Cache all decoded frames\n const frameDuration = Math.round(1_000_000 / fps);\n for (const frame of result.frames) {\n const frameGlobalTime = clip.startUs + frame.timestamp;\n cacheManager.addFrame(\n frame,\n clip.id,\n frameDuration,\n clip.trackId ?? 'main',\n frameGlobalTime\n );\n }\n } catch (error) {\n // Don't throw - this is a best-effort optimization\n }\n }\n private readonly clipId: string;\n private readonly resourceId: string;\n private readonly mp4IndexCache: MP4IndexCache;\n private readonly cacheManager: CacheManager;\n private readonly compositionModel: CompositionModel;\n private readonly resourceLoader: ResourceLoader;\n private readonly fps: number;\n private readonly globalTimeUs: TimeUs;\n private readonly targetTimeUs: TimeUs;\n\n private decoder: VideoDecoder | null = null;\n isDisposed = false;\n private aborted = false;\n private decodedFrames: VideoFrame[] = [];\n\n private constructor(config: OnDemandVideoSessionConfig) {\n this.clipId = config.clipId;\n this.resourceId = config.resourceId;\n this.mp4IndexCache = config.mp4IndexCache;\n this.cacheManager = config.cacheManager;\n this.resourceLoader = config.resourceLoader;\n this.compositionModel = config.compositionModel;\n this.fps = config.fps;\n this.globalTimeUs = config.globalTimeUs;\n this.targetTimeUs = config.targetTimeUs;\n }\n\n static async create(config: OnDemandVideoSessionConfig): Promise<OnDemandVideoSession> {\n const session = new OnDemandVideoSession(config);\n await session.init();\n return session;\n }\n\n private async init(): Promise<void> {\n // No special initialization needed for now\n }\n\n /**\n * Decode a window range, write raw frames to L1 cache\n */\n async decodeWindow(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n if (this.isDisposed) {\n throw new Error('Session already disposed');\n }\n\n const resource = this.compositionModel.getResource(this.resourceId);\n if (!resource) {\n throw new Error(`Resource not found: ${this.resourceId}`);\n }\n\n if (resource.type === 'image') {\n await this.handleImageResource(resource, startUs, endUs);\n return;\n }\n\n await this.handleVideoResource(startUs, endUs);\n }\n\n /**\n * Handle image resource by creating a VideoFrame from ImageBitmap\n */\n private async handleImageResource(\n resource: Resource,\n startUs: TimeUs,\n endUs: TimeUs\n ): Promise<void> {\n const image = await this.resourceLoader.loadImage(resource);\n if (!image) return;\n\n const frame = new VideoFrame(image, {\n timestamp: startUs,\n duration: endUs - startUs,\n });\n\n this.cacheManager.addFrame(\n frame,\n this.clipId,\n endUs - startUs,\n this.compositionModel.mainTrackId,\n this.globalTimeUs\n );\n }\n\n /**\n * Handle video resource by decoding from OPFS\n */\n private async handleVideoResource(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index) {\n throw new Error(`No index found for resource ${this.resourceId}`);\n }\n\n // Calculate GOP ranges needed\n const gopWindow = this.calculateGOPRangesForWindow(index, startUs, endUs);\n if (gopWindow.gops.length === 0) {\n return;\n }\n\n // Read GOP data from OPFS\n const gopData = await this.cacheManager.readResourceRange(\n this.resourceId,\n gopWindow.byteStart,\n gopWindow.byteEnd\n );\n\n if (this.aborted) return;\n\n // Extract chunks from GOP data\n const chunks = await this.demuxGOPData(gopData, index, gopWindow);\n if (this.aborted) return;\n\n // Decode chunks to frames\n await this.decodeChunks(chunks, index);\n\n // Check abort and cleanup if needed\n if (this.aborted) {\n this.releaseDecodedFrames();\n return;\n }\n\n // Write frames to L1 cache\n await this.cacheDecodedFrames(startUs, endUs);\n }\n\n /**\n * Release all decoded frames without caching\n */\n private releaseDecodedFrames(): void {\n for (const frame of this.decodedFrames) {\n frame.close();\n }\n this.decodedFrames = [];\n }\n\n private calculateGOPRangesForWindow(\n index: MP4Index,\n startUs: TimeUs,\n endUs: TimeUs\n ): GOPWindowResult {\n if (!index.tracks.video) {\n return { gops: [], byteStart: 0, byteEnd: 0 };\n }\n\n const { gopIndex, samples } = index.tracks.video;\n\n // Find GOP containing startUs (or the nearest keyframe before it)\n const nearestKeyframe = this.mp4IndexCache.getNearestKeyframe(this.resourceId, startUs);\n const decodeStartUs = nearestKeyframe?.timestamp ?? startUs;\n\n // Use binary search to find all overlapping GOPs\n const overlappingGOPs = binarySearchOverlapping(gopIndex, decodeStartUs, endUs, (gop, idx) => {\n const nextGOP = gopIndex[idx + 1];\n return {\n start: gop.startTimeUs,\n end: nextGOP ? nextGOP.startTimeUs : Infinity,\n };\n });\n\n if (overlappingGOPs.length === 0) {\n return { gops: [], byteStart: 0, byteEnd: 0 };\n }\n\n // Calculate merged byte range for OPFS read\n let byteStart = Infinity;\n let byteEnd = 0;\n\n for (const gop of overlappingGOPs) {\n const startSample = samples[gop.keyframeSampleIndex];\n const endSampleIndex = gop.keyframeSampleIndex + gop.sampleCount - 1;\n const endSample = samples[endSampleIndex];\n\n if (startSample && endSample) {\n byteStart = Math.min(byteStart, startSample.byteOffset);\n byteEnd = Math.max(byteEnd, endSample.byteOffset + endSample.byteLength);\n }\n }\n\n return { gops: overlappingGOPs, byteStart, byteEnd };\n }\n\n /**\n * Extract video chunks from GOP data\n *\n * Directly use GOP sample indices to slice the samples array.\n * This is O(k) where k is the number of samples in the window,\n * vs O(n×m) for the old approach (n=all samples, m=GOP count).\n */\n private async demuxGOPData(\n data: ArrayBuffer,\n index: MP4Index,\n gopWindow: GOPWindowResult\n ): Promise<EncodedVideoChunk[]> {\n const videoTrack = index.tracks.video;\n if (!videoTrack) {\n throw new Error('No video track in index');\n }\n\n const { samples } = videoTrack;\n const chunks: EncodedVideoChunk[] = [];\n const dataView = new Uint8Array(data);\n const baseByteOffset = gopWindow.byteStart;\n\n // Direct sample index slicing - iterate only samples in the window\n for (const gop of gopWindow.gops) {\n const startIdx = gop.keyframeSampleIndex;\n const endIdx = startIdx + gop.sampleCount;\n\n // Extract samples for this GOP (direct array slice)\n for (let i = startIdx; i < endIdx; i++) {\n const sample = samples[i];\n if (!sample) continue;\n\n // Calculate relative offset within the data buffer\n const relativeOffset = sample.byteOffset - baseByteOffset;\n\n // Validate offset is within buffer\n if (relativeOffset < 0 || relativeOffset + sample.byteLength > data.byteLength) {\n console.warn('[OnDemandVideoSession] Sample outside buffer:', {\n sampleOffset: sample.byteOffset,\n sampleLength: sample.byteLength,\n baseOffset: baseByteOffset,\n relativeOffset,\n bufferLength: data.byteLength,\n });\n continue;\n }\n\n // Extract sample data\n const sampleData = dataView.slice(relativeOffset, relativeOffset + sample.byteLength);\n\n // Create EncodedVideoChunk\n const chunk = new EncodedVideoChunk({\n type: sample.isKeyframe ? 'key' : 'delta',\n timestamp: sample.timestamp,\n duration: sample.duration,\n data: sampleData,\n });\n\n chunks.push(chunk);\n }\n }\n\n return chunks;\n }\n\n private async decodeChunks(chunks: EncodedVideoChunk[], index: MP4Index): Promise<void> {\n const videoTrack = index.tracks.video;\n if (!videoTrack) {\n throw new Error('No video track in index');\n }\n\n const result = await decodeChunksWithoutFlush(chunks, {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n });\n\n // Store frames for caching\n this.decodedFrames = result.frames;\n }\n\n /**\n * Cache a single frame to L1 with proper timestamp calculations\n */\n private cacheFrame(frame: VideoFrame, globalTimeOffset: TimeUs = 0): void {\n const frameDuration = frame.duration ?? Math.round(1_000_000 / this.fps);\n const frameGlobalTime =\n this.globalTimeUs + (frame.timestamp - this.targetTimeUs) + globalTimeOffset;\n\n this.cacheManager.addFrame(\n frame,\n this.clipId,\n frameDuration,\n this.compositionModel.mainTrackId,\n frameGlobalTime\n );\n }\n\n private async cacheDecodedFrames(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const framesToCache: VideoFrame[] = [];\n const framesToDiscard: VideoFrame[] = [];\n\n // Partition frames into cacheable and discardable\n for (const frame of this.decodedFrames) {\n if (frame.timestamp >= startUs && frame.timestamp < endUs) {\n framesToCache.push(frame);\n } else {\n framesToDiscard.push(frame);\n }\n }\n\n // Cache frames within window\n for (const frame of framesToCache) {\n try {\n this.cacheFrame(frame);\n } catch (error) {\n frame.close();\n }\n }\n\n // Release frames outside window\n for (const frame of framesToDiscard) {\n frame.close();\n }\n\n this.decodedFrames = [];\n }\n\n /**\n * Fast decode single keyframe for immediate seek preview\n * Returns the decoded keyframe timestamp\n */\n async decodeKeyframe(targetTimeUs: TimeUs): Promise<TimeUs | null> {\n if (this.isDisposed || this.aborted) return null;\n\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index || !index.tracks.video) return null;\n\n // Find nearest keyframe\n const keyframeSample = this.mp4IndexCache.getNearestKeyframe(this.resourceId, targetTimeUs);\n if (!keyframeSample) return null;\n\n // Read only the keyframe bytes from OPFS\n const keyframeData = await this.cacheManager.readResourceRange(\n this.resourceId,\n keyframeSample.byteOffset,\n keyframeSample.byteOffset + keyframeSample.byteLength\n );\n\n if (this.aborted) return null;\n\n // Create and decode keyframe chunk\n const chunk = new EncodedVideoChunk({\n type: 'key',\n timestamp: keyframeSample.timestamp,\n duration: keyframeSample.duration,\n data: keyframeData,\n });\n\n await this.decodeChunks([chunk], index);\n\n if (this.aborted || this.decodedFrames.length === 0) return null;\n\n // Cache the first (and only) decoded frame\n const frame = this.decodedFrames[0];\n if (!frame) return null;\n\n this.cacheFrame(frame);\n this.decodedFrames = [];\n\n return keyframeSample.timestamp;\n }\n\n /**\n * Decode entire time range to VideoFrame stream (for export)\n * Does NOT cache to L1 - outputs frames directly for Worker pipeline\n */\n async decodeRangeToStream(startUs: TimeUs, endUs: TimeUs): Promise<ReadableStream<VideoFrame>> {\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index?.tracks.video) {\n throw new Error(`[OnDemandVideoSession] No video track index for ${this.resourceId}`);\n }\n\n const videoTrack = index.tracks.video;\n const gopWindow = this.calculateGOPRangesForWindow(index, startUs, endUs);\n\n return new ReadableStream<VideoFrame>({\n start: async (controller) => {\n try {\n if (gopWindow.gops.length === 0) {\n console.warn('[OnDemandVideoSession] No GOPs found for range');\n controller.close();\n return;\n }\n\n // Process GOPs in batches (10 GOPs at a time for memory control)\n const batchSize = 10;\n for (let i = 0; i < gopWindow.gops.length; i += batchSize) {\n if (this.aborted) {\n controller.close();\n return;\n }\n\n const batchGOPs = gopWindow.gops.slice(\n i,\n Math.min(i + batchSize, gopWindow.gops.length)\n );\n\n // Calculate byte range for this batch\n let batchByteStart = Infinity;\n let batchByteEnd = 0;\n for (const gop of batchGOPs) {\n const startSample = videoTrack.samples[gop.keyframeSampleIndex];\n const endSampleIdx = gop.keyframeSampleIndex + gop.sampleCount - 1;\n const endSample = videoTrack.samples[endSampleIdx];\n if (startSample && endSample) {\n batchByteStart = Math.min(batchByteStart, startSample.byteOffset);\n batchByteEnd = Math.max(batchByteEnd, endSample.byteOffset + endSample.byteLength);\n }\n }\n\n // Read GOP batch data from OPFS\n const gopData = await this.cacheManager.readResourceRange(\n this.resourceId,\n batchByteStart,\n batchByteEnd\n );\n\n if (this.aborted) {\n controller.close();\n return;\n }\n\n // Extract chunks from GOP batch\n const batchChunks = await this.demuxGOPData(gopData, index, {\n gops: batchGOPs,\n byteStart: batchByteStart,\n byteEnd: batchByteEnd,\n });\n\n // Decode chunks to frames\n await this.decodeChunks(batchChunks, index);\n\n if (this.aborted) {\n this.releaseDecodedFrames();\n controller.close();\n return;\n }\n\n // Enqueue decoded frames (DO NOT cache to L1)\n for (const frame of this.decodedFrames) {\n controller.enqueue(frame);\n }\n\n // Clear frames array (ownership transferred to stream)\n this.decodedFrames = [];\n }\n\n controller.close();\n } catch (error) {\n console.error('[OnDemandVideoSession] decodeRangeToStream error:', error);\n this.releaseDecodedFrames();\n controller.error(error);\n }\n },\n cancel: () => {\n this.aborted = true;\n this.releaseDecodedFrames();\n },\n });\n }\n\n async dispose(): Promise<void> {\n if (this.isDisposed) return;\n\n this.aborted = true;\n\n // Clean up decoder if exists\n if (this.decoder) {\n try {\n this.decoder.close();\n } catch {\n // Ignore close errors during dispose\n }\n this.decoder = null;\n }\n\n // Release all decoded frames\n this.releaseDecodedFrames();\n\n this.isDisposed = true;\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"OnDemandVideoSession.js","sources":["../../src/orchestrator/OnDemandVideoSession.ts"],"sourcesContent":["import type { CacheManager } from '../cache/CacheManager';\nimport type { MP4IndexCache } from '../cache/resource/MP4IndexCache';\nimport type { MP4Index, GOP } from '../stages/demux/types';\nimport type { TimeUs, Resource } from '../model/types';\nimport type { CompositionModel, Clip } from '../model';\nimport { binarySearchOverlapping } from '../utils/binary-search';\nimport type { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { decodeChunksWithoutFlush } from '../utils/video-decoder-helpers';\n\ninterface GOPWindowResult {\n gops: GOP[];\n byteStart: number;\n byteEnd: number;\n}\n\ninterface OnDemandVideoSessionConfig {\n clipId: string;\n resourceId: string;\n targetTimeUs: TimeUs;\n globalTimeUs: TimeUs;\n mp4IndexCache: MP4IndexCache;\n cacheManager: CacheManager;\n compositionModel: CompositionModel;\n resourceLoader: ResourceLoader;\n fps: number;\n}\n\n/**\n * OnDemandVideoSession - Main-thread on-demand decoder\n *\n * Strategy:\n * 1. Read GOP range from OPFS\n * 2. Demux using MP4Box (main thread)\n * 3. Decode using VideoDecoder (main thread)\n * 4. Write RAW VideoFrames (YUV) to L1 cache\n * 5. Dispose immediately after window completes\n *\n * Why main thread?\n * - Window is small (±2s, ~60-120 frames)\n * - Worker overhead (10-50ms) is significant for small workloads\n * - Decode is fast enough\n */\nexport class OnDemandVideoSession {\n /**\n * Static method to decode and cache first frame from extracted GOP chunks\n * Used by ResourceLoader during streaming download for fast cover rendering\n */\n static async decodeAndCacheFirstFrame(\n _resourceId: string,\n chunks: EncodedVideoChunk[],\n index: MP4Index,\n clip: Clip,\n cacheManager: CacheManager,\n fps: number\n ): Promise<void> {\n if (chunks.length === 0) return;\n\n const videoTrack = index.tracks.video;\n if (!videoTrack) return;\n\n // Verify first chunk is keyframe\n const firstChunk = chunks[0];\n if (!firstChunk || firstChunk.type !== 'key') {\n return;\n }\n\n try {\n const result = await decodeChunksWithoutFlush(chunks, {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n });\n\n // Cache all decoded frames\n const frameDuration = Math.round(1_000_000 / fps);\n for (const frame of result.frames) {\n const frameGlobalTime = clip.startUs + frame.timestamp;\n cacheManager.addFrame(\n frame,\n clip.id,\n frameDuration,\n clip.trackId ?? 'main',\n frameGlobalTime\n );\n }\n } catch (error) {\n // Don't throw - this is a best-effort optimization\n }\n }\n private readonly clipId: string;\n private readonly resourceId: string;\n private readonly mp4IndexCache: MP4IndexCache;\n private readonly cacheManager: CacheManager;\n private readonly compositionModel: CompositionModel;\n private readonly resourceLoader: ResourceLoader;\n private readonly fps: number;\n private readonly globalTimeUs: TimeUs;\n private readonly targetTimeUs: TimeUs;\n\n private decoder: VideoDecoder | null = null;\n isDisposed = false;\n private aborted = false;\n private decodedFrames: VideoFrame[] = [];\n\n private constructor(config: OnDemandVideoSessionConfig) {\n this.clipId = config.clipId;\n this.resourceId = config.resourceId;\n this.mp4IndexCache = config.mp4IndexCache;\n this.cacheManager = config.cacheManager;\n this.resourceLoader = config.resourceLoader;\n this.compositionModel = config.compositionModel;\n this.fps = config.fps;\n this.globalTimeUs = config.globalTimeUs;\n this.targetTimeUs = config.targetTimeUs;\n }\n\n static async create(config: OnDemandVideoSessionConfig): Promise<OnDemandVideoSession> {\n const session = new OnDemandVideoSession(config);\n await session.init();\n return session;\n }\n\n private async init(): Promise<void> {\n // No special initialization needed for now\n }\n\n /**\n * Decode a window range, write raw frames to L1 cache\n */\n async decodeWindow(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n if (this.isDisposed) {\n throw new Error('Session already disposed');\n }\n\n const resource = this.compositionModel.getResource(this.resourceId);\n if (!resource) {\n console.warn('[OnDemandVideoSession] Resource not found in composition model:', {\n resourceId: this.resourceId,\n clipId: this.clipId,\n startUs,\n endUs,\n model: {\n fps: this.compositionModel.fps,\n durationUs: this.compositionModel.durationUs,\n mainTrackId: this.compositionModel.mainTrackId,\n trackCount: this.compositionModel.tracks.length,\n resourcesSize: this.compositionModel.resources.size,\n },\n });\n throw new Error(`Resource not found: ${this.resourceId}`);\n }\n\n if (resource.type === 'image') {\n await this.handleImageResource(resource, startUs, endUs);\n return;\n }\n\n await this.handleVideoResource(startUs, endUs);\n }\n\n /**\n * Handle image resource by creating a VideoFrame from ImageBitmap\n */\n private async handleImageResource(\n resource: Resource,\n startUs: TimeUs,\n endUs: TimeUs\n ): Promise<void> {\n const image = await this.resourceLoader.loadImage(resource);\n if (!image) return;\n\n const frame = new VideoFrame(image, {\n timestamp: startUs,\n duration: endUs - startUs,\n });\n\n this.cacheManager.addFrame(\n frame,\n this.clipId,\n endUs - startUs,\n this.compositionModel.mainTrackId,\n this.globalTimeUs\n );\n }\n\n /**\n * Handle video resource by decoding from OPFS\n */\n private async handleVideoResource(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index) {\n throw new Error(`No index found for resource ${this.resourceId}`);\n }\n\n // Calculate GOP ranges needed\n const gopWindow = this.calculateGOPRangesForWindow(index, startUs, endUs);\n if (gopWindow.gops.length === 0) {\n return;\n }\n\n // Read GOP data from OPFS\n const gopData = await this.cacheManager.readResourceRange(\n this.resourceId,\n gopWindow.byteStart,\n gopWindow.byteEnd\n );\n\n if (this.aborted) return;\n\n // Extract chunks from GOP data\n const chunks = await this.demuxGOPData(gopData, index, gopWindow);\n if (this.aborted) return;\n\n // Decode chunks to frames\n await this.decodeChunks(chunks, index);\n\n // Check abort and cleanup if needed\n if (this.aborted) {\n this.releaseDecodedFrames();\n return;\n }\n\n // Write frames to L1 cache\n await this.cacheDecodedFrames(startUs, endUs);\n }\n\n /**\n * Release all decoded frames without caching\n */\n private releaseDecodedFrames(): void {\n for (const frame of this.decodedFrames) {\n frame.close();\n }\n this.decodedFrames = [];\n }\n\n private calculateGOPRangesForWindow(\n index: MP4Index,\n startUs: TimeUs,\n endUs: TimeUs\n ): GOPWindowResult {\n if (!index.tracks.video) {\n return { gops: [], byteStart: 0, byteEnd: 0 };\n }\n\n const { gopIndex, samples } = index.tracks.video;\n\n // Find GOP containing startUs (or the nearest keyframe before it)\n const nearestKeyframe = this.mp4IndexCache.getNearestKeyframe(this.resourceId, startUs);\n const decodeStartUs = nearestKeyframe?.timestamp ?? startUs;\n\n // Use binary search to find all overlapping GOPs\n const overlappingGOPs = binarySearchOverlapping(gopIndex, decodeStartUs, endUs, (gop, idx) => {\n const nextGOP = gopIndex[idx + 1];\n return {\n start: gop.startTimeUs,\n end: nextGOP ? nextGOP.startTimeUs : Infinity,\n };\n });\n\n if (overlappingGOPs.length === 0) {\n return { gops: [], byteStart: 0, byteEnd: 0 };\n }\n\n // Calculate merged byte range for OPFS read\n let byteStart = Infinity;\n let byteEnd = 0;\n\n for (const gop of overlappingGOPs) {\n const startSample = samples[gop.keyframeSampleIndex];\n const endSampleIndex = gop.keyframeSampleIndex + gop.sampleCount - 1;\n const endSample = samples[endSampleIndex];\n\n if (startSample && endSample) {\n byteStart = Math.min(byteStart, startSample.byteOffset);\n byteEnd = Math.max(byteEnd, endSample.byteOffset + endSample.byteLength);\n }\n }\n\n return { gops: overlappingGOPs, byteStart, byteEnd };\n }\n\n /**\n * Extract video chunks from GOP data\n *\n * Directly use GOP sample indices to slice the samples array.\n * This is O(k) where k is the number of samples in the window,\n * vs O(n×m) for the old approach (n=all samples, m=GOP count).\n */\n private async demuxGOPData(\n data: ArrayBuffer,\n index: MP4Index,\n gopWindow: GOPWindowResult\n ): Promise<EncodedVideoChunk[]> {\n const videoTrack = index.tracks.video;\n if (!videoTrack) {\n throw new Error('No video track in index');\n }\n\n const { samples } = videoTrack;\n const chunks: EncodedVideoChunk[] = [];\n const dataView = new Uint8Array(data);\n const baseByteOffset = gopWindow.byteStart;\n\n // Direct sample index slicing - iterate only samples in the window\n for (const gop of gopWindow.gops) {\n const startIdx = gop.keyframeSampleIndex;\n const endIdx = startIdx + gop.sampleCount;\n\n // Extract samples for this GOP (direct array slice)\n for (let i = startIdx; i < endIdx; i++) {\n const sample = samples[i];\n if (!sample) continue;\n\n // Calculate relative offset within the data buffer\n const relativeOffset = sample.byteOffset - baseByteOffset;\n\n // Validate offset is within buffer\n if (relativeOffset < 0 || relativeOffset + sample.byteLength > data.byteLength) {\n console.warn('[OnDemandVideoSession] Sample outside buffer:', {\n sampleOffset: sample.byteOffset,\n sampleLength: sample.byteLength,\n baseOffset: baseByteOffset,\n relativeOffset,\n bufferLength: data.byteLength,\n });\n continue;\n }\n\n // Extract sample data\n const sampleData = dataView.slice(relativeOffset, relativeOffset + sample.byteLength);\n\n // Create EncodedVideoChunk\n const chunk = new EncodedVideoChunk({\n type: sample.isKeyframe ? 'key' : 'delta',\n timestamp: sample.timestamp,\n duration: sample.duration,\n data: sampleData,\n });\n\n chunks.push(chunk);\n }\n }\n\n return chunks;\n }\n\n private async decodeChunks(chunks: EncodedVideoChunk[], index: MP4Index): Promise<void> {\n const videoTrack = index.tracks.video;\n if (!videoTrack) {\n throw new Error('No video track in index');\n }\n\n const result = await decodeChunksWithoutFlush(chunks, {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n });\n\n // Store frames for caching\n this.decodedFrames = result.frames;\n }\n\n /**\n * Cache a single frame to L1 with proper timestamp calculations\n */\n private cacheFrame(frame: VideoFrame, globalTimeOffset: TimeUs = 0): void {\n const frameDuration = frame.duration ?? Math.round(1_000_000 / this.fps);\n const frameGlobalTime =\n this.globalTimeUs + (frame.timestamp - this.targetTimeUs) + globalTimeOffset;\n\n this.cacheManager.addFrame(\n frame,\n this.clipId,\n frameDuration,\n this.compositionModel.mainTrackId,\n frameGlobalTime\n );\n }\n\n private async cacheDecodedFrames(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const framesToCache: VideoFrame[] = [];\n const framesToDiscard: VideoFrame[] = [];\n\n // Partition frames into cacheable and discardable\n for (const frame of this.decodedFrames) {\n if (frame.timestamp >= startUs && frame.timestamp < endUs) {\n framesToCache.push(frame);\n } else {\n framesToDiscard.push(frame);\n }\n }\n\n // Cache frames within window\n for (const frame of framesToCache) {\n try {\n this.cacheFrame(frame);\n } catch (error) {\n frame.close();\n }\n }\n\n // Release frames outside window\n for (const frame of framesToDiscard) {\n frame.close();\n }\n\n this.decodedFrames = [];\n }\n\n /**\n * Fast decode single keyframe for immediate seek preview\n * Returns the decoded keyframe timestamp\n */\n async decodeKeyframe(targetTimeUs: TimeUs): Promise<TimeUs | null> {\n if (this.isDisposed || this.aborted) return null;\n\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index || !index.tracks.video) return null;\n\n // Find nearest keyframe\n const keyframeSample = this.mp4IndexCache.getNearestKeyframe(this.resourceId, targetTimeUs);\n if (!keyframeSample) return null;\n\n // Read only the keyframe bytes from OPFS\n const keyframeData = await this.cacheManager.readResourceRange(\n this.resourceId,\n keyframeSample.byteOffset,\n keyframeSample.byteOffset + keyframeSample.byteLength\n );\n\n if (this.aborted) return null;\n\n // Create and decode keyframe chunk\n const chunk = new EncodedVideoChunk({\n type: 'key',\n timestamp: keyframeSample.timestamp,\n duration: keyframeSample.duration,\n data: keyframeData,\n });\n\n await this.decodeChunks([chunk], index);\n\n if (this.aborted || this.decodedFrames.length === 0) return null;\n\n // Cache the first (and only) decoded frame\n const frame = this.decodedFrames[0];\n if (!frame) return null;\n\n this.cacheFrame(frame);\n this.decodedFrames = [];\n\n return keyframeSample.timestamp;\n }\n\n /**\n * Decode entire time range to VideoFrame stream (for export)\n * Does NOT cache to L1 - outputs frames directly for Worker pipeline\n */\n async decodeRangeToStream(startUs: TimeUs, endUs: TimeUs): Promise<ReadableStream<VideoFrame>> {\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index?.tracks.video) {\n throw new Error(`[OnDemandVideoSession] No video track index for ${this.resourceId}`);\n }\n\n const videoTrack = index.tracks.video;\n const gopWindow = this.calculateGOPRangesForWindow(index, startUs, endUs);\n\n return new ReadableStream<VideoFrame>({\n start: async (controller) => {\n try {\n if (gopWindow.gops.length === 0) {\n console.warn('[OnDemandVideoSession] No GOPs found for range');\n controller.close();\n return;\n }\n\n // Process GOPs in batches (10 GOPs at a time for memory control)\n const batchSize = 10;\n for (let i = 0; i < gopWindow.gops.length; i += batchSize) {\n if (this.aborted) {\n controller.close();\n return;\n }\n\n const batchGOPs = gopWindow.gops.slice(\n i,\n Math.min(i + batchSize, gopWindow.gops.length)\n );\n\n // Calculate byte range for this batch\n let batchByteStart = Infinity;\n let batchByteEnd = 0;\n for (const gop of batchGOPs) {\n const startSample = videoTrack.samples[gop.keyframeSampleIndex];\n const endSampleIdx = gop.keyframeSampleIndex + gop.sampleCount - 1;\n const endSample = videoTrack.samples[endSampleIdx];\n if (startSample && endSample) {\n batchByteStart = Math.min(batchByteStart, startSample.byteOffset);\n batchByteEnd = Math.max(batchByteEnd, endSample.byteOffset + endSample.byteLength);\n }\n }\n\n // Read GOP batch data from OPFS\n const gopData = await this.cacheManager.readResourceRange(\n this.resourceId,\n batchByteStart,\n batchByteEnd\n );\n\n if (this.aborted) {\n controller.close();\n return;\n }\n\n // Extract chunks from GOP batch\n const batchChunks = await this.demuxGOPData(gopData, index, {\n gops: batchGOPs,\n byteStart: batchByteStart,\n byteEnd: batchByteEnd,\n });\n\n // Decode chunks to frames\n await this.decodeChunks(batchChunks, index);\n\n if (this.aborted) {\n this.releaseDecodedFrames();\n controller.close();\n return;\n }\n\n // Enqueue decoded frames (DO NOT cache to L1)\n for (const frame of this.decodedFrames) {\n controller.enqueue(frame);\n }\n\n // Clear frames array (ownership transferred to stream)\n this.decodedFrames = [];\n }\n\n controller.close();\n } catch (error) {\n console.error('[OnDemandVideoSession] decodeRangeToStream error:', error);\n this.releaseDecodedFrames();\n controller.error(error);\n }\n },\n cancel: () => {\n this.aborted = true;\n this.releaseDecodedFrames();\n },\n });\n }\n\n async dispose(): Promise<void> {\n if (this.isDisposed) return;\n\n this.aborted = true;\n\n // Clean up decoder if exists\n if (this.decoder) {\n try {\n this.decoder.close();\n } catch {\n // Ignore close errors during dispose\n }\n this.decoder = null;\n }\n\n // Release all decoded frames\n this.releaseDecodedFrames();\n\n this.isDisposed = true;\n }\n}\n"],"names":[],"mappings":"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"}
@@ -25,10 +25,10 @@ export declare class Orchestrator implements IOrchestrator {
25
25
  private config;
26
26
  private ensureCacheDebounceTimer;
27
27
  private activeOnDemandSession;
28
+ private modelToken;
28
29
  readonly events: Pick<EventBus<EventPayloadMap>, 'on' | 'off' | 'once'>;
29
30
  constructor(config: OrchestratorConfig);
30
31
  private setupResourceFirstFrameHandler;
31
- private setupPreloadHandlers;
32
32
  initialize(): Promise<void>;
33
33
  on<K extends keyof EventPayloadMap>(event: K, handler: (payload: EventPayloadMap[K]) => void): void;
34
34
  off<K extends keyof EventPayloadMap>(event: K, handler: (payload: EventPayloadMap[K]) => void): void;
@@ -1 +1 @@
1
- {"version":3,"file":"Orchestrator.d.ts","sourceRoot":"","sources":["../../src/orchestrator/Orchestrator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Orchestrator.d.ts","sourceRoot":"","sources":["../../src/orchestrator/Orchestrator.ts"],"names":[],"mappings":"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"}
@@ -26,6 +26,7 @@ class Orchestrator {
26
26
  config = ConfigLoader.getInstance().getConfig();
27
27
  ensureCacheDebounceTimer = null;
28
28
  activeOnDemandSession = null;
29
+ modelToken = 0;
29
30
  events;
30
31
  constructor(config) {
31
32
  this.eventBus = config.eventBus || new EventBus();
@@ -86,7 +87,6 @@ class Orchestrator {
86
87
  eventBus: this.eventBus
87
88
  });
88
89
  this.setupResourceFirstFrameHandler();
89
- this.setupPreloadHandlers();
90
90
  }
91
91
  setupResourceFirstFrameHandler() {
92
92
  this.eventBus.on(MeframeEvent.ResourceFirstFrameReady, async (payload) => {
@@ -107,17 +107,6 @@ class Orchestrator {
107
107
  }
108
108
  });
109
109
  }
110
- setupPreloadHandlers() {
111
- this.eventBus.on(MeframeEvent.PlaybackPlay, () => {
112
- this.resourceLoader.setPreloadingEnabled(false);
113
- });
114
- this.eventBus.on(MeframeEvent.PlaybackPause, () => {
115
- this.resourceLoader.setPreloadingEnabled(true);
116
- });
117
- this.eventBus.on(MeframeEvent.PlaybackStop, () => {
118
- this.resourceLoader.setPreloadingEnabled(true);
119
- });
120
- }
121
110
  async initialize() {
122
111
  if (this.isInitialized) return;
123
112
  await this.cacheManager.init();
@@ -182,11 +171,15 @@ class Orchestrator {
182
171
  }
183
172
  }
184
173
  async setCompositionModel(model) {
185
- this.compositionModel = model;
186
174
  this.cacheManager.clear();
175
+ this.cancelActiveDecoding();
176
+ this.audioSession.stopPlayback();
177
+ this.modelToken++;
178
+ this.compositionModel = model;
187
179
  this.planner.setModel(model);
188
- await this.resourceLoader.setModel(model);
180
+ const resourceLoaderSetModelPromise = this.resourceLoader.setModel(model);
189
181
  this.audioSession.setModel(model);
182
+ await resourceLoaderSetModelPromise;
190
183
  this.eventBus.emit(MeframeEvent.ModelSet, model);
191
184
  this.eventBus.emit(MeframeEvent.CompositionUpdated, {
192
185
  trackCount: model.tracks.length,
@@ -265,6 +258,8 @@ class Orchestrator {
265
258
  if (!hasResourceId(clip)) return null;
266
259
  const resourceId = clip.resourceId;
267
260
  const trimStartUs = clip.trimStartUs ?? 0;
261
+ const startModelToken = this.modelToken;
262
+ const startModel = this.compositionModel;
268
263
  const resource = this.compositionModel?.getResource(resourceId);
269
264
  const isReady = resource?.state === "ready";
270
265
  const fetchOptions = {
@@ -278,6 +273,18 @@ class Orchestrator {
278
273
  return null;
279
274
  }
280
275
  await this.resourceLoader.load(resourceId, fetchOptions);
276
+ if (this.modelToken !== startModelToken || this.compositionModel !== startModel) {
277
+ console.warn("[Orchestrator] Model switched during decodeFromResource:", {
278
+ startModelToken,
279
+ currentModelToken: this.modelToken,
280
+ clipId: clip.id,
281
+ resourceId,
282
+ globalTimeUs,
283
+ resourceTimeUs,
284
+ mode
285
+ });
286
+ return null;
287
+ }
281
288
  this.cancelActiveDecoding();
282
289
  const session = await OnDemandVideoSession.create({
283
290
  clipId: clip.id,
@@ -453,19 +460,28 @@ class Orchestrator {
453
460
  * Returns layers ready for VideoComposer
454
461
  */
455
462
  async getRenderState(timeUs, options) {
456
- if (!this.compositionModel) {
463
+ const startModelToken = this.modelToken;
464
+ const startModel = this.compositionModel;
465
+ if (!startModel) {
457
466
  return null;
458
467
  }
459
468
  const frame = await this.getFrame(timeUs, options);
469
+ if (this.modelToken !== startModelToken || this.compositionModel !== startModel) {
470
+ return null;
471
+ }
460
472
  if (!frame) {
461
473
  return null;
462
474
  }
463
- const clip = this.compositionModel.getClipsAtTime(timeUs, this.compositionModel.mainTrackId)[0];
475
+ const clip = startModel.getClipsAtTime(timeUs, startModel.mainTrackId)[0];
464
476
  if (!clip) {
465
477
  return null;
466
478
  }
467
479
  const clipRelativeTimeUs = timeUs - clip.startUs;
468
- const resourceTimeUs = clipRelativeTimeUs + (clip.trimStartUs ?? 0);
480
+ const trimStartUs = clip.trimStartUs ?? 0;
481
+ let resourceTimeUs = clipRelativeTimeUs + trimStartUs;
482
+ if (clip.durationUs > 0) {
483
+ resourceTimeUs = Math.min(resourceTimeUs, trimStartUs + clip.durationUs - 1);
484
+ }
469
485
  const instructions = this.planner.getInstructions(clip.id);
470
486
  if (!instructions) {
471
487
  return null;
@@ -483,6 +499,17 @@ class Orchestrator {
483
499
  );
484
500
  });
485
501
  for (const layerPlan of activeLayers) {
502
+ if (layerPlan.type === "video" && !layerPlan.payload.attachmentId) {
503
+ layers.push({
504
+ id: layerPlan.layerId,
505
+ type: "video",
506
+ zIndex: layerPlan.zIndex ?? 0,
507
+ visible: true,
508
+ opacity: layerPlan.opacity ?? 1,
509
+ rcFrame: frame
510
+ });
511
+ continue;
512
+ }
486
513
  const layer = await this.materializeLayer(layerPlan, clip, resourceTimeUs, timeUs);
487
514
  if (layer) {
488
515
  layers.push(layer);