@meframe/core 0.0.34 → 0.0.36

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 (39) hide show
  1. package/dist/Meframe.d.ts +1 -1
  2. package/dist/Meframe.d.ts.map +1 -1
  3. package/dist/Meframe.js +1 -1
  4. package/dist/Meframe.js.map +1 -1
  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 +2 -1
  8. package/dist/cache/CacheManager.js.map +1 -1
  9. package/dist/config/defaults.d.ts +1 -1
  10. package/dist/config/defaults.js +2 -2
  11. package/dist/config/defaults.js.map +1 -1
  12. package/dist/config/presets.js +4 -4
  13. package/dist/config/presets.js.map +1 -1
  14. package/dist/controllers/PlaybackController.d.ts.map +1 -1
  15. package/dist/controllers/PlaybackController.js +29 -4
  16. package/dist/controllers/PlaybackController.js.map +1 -1
  17. package/dist/orchestrator/ExportScheduler.d.ts.map +1 -1
  18. package/dist/orchestrator/ExportScheduler.js +11 -6
  19. package/dist/orchestrator/ExportScheduler.js.map +1 -1
  20. package/dist/orchestrator/GlobalAudioSession.js +1 -1
  21. package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
  22. package/dist/orchestrator/OnDemandVideoSession.d.ts +7 -1
  23. package/dist/orchestrator/OnDemandVideoSession.d.ts.map +1 -1
  24. package/dist/orchestrator/OnDemandVideoSession.js +65 -2
  25. package/dist/orchestrator/OnDemandVideoSession.js.map +1 -1
  26. package/dist/orchestrator/Orchestrator.d.ts +12 -0
  27. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  28. package/dist/orchestrator/Orchestrator.js +80 -3
  29. package/dist/orchestrator/Orchestrator.js.map +1 -1
  30. package/dist/orchestrator/types.d.ts +2 -0
  31. package/dist/orchestrator/types.d.ts.map +1 -1
  32. package/dist/stages/encode/VideoChunkEncoder.d.ts.map +1 -1
  33. package/dist/stages/encode/types.d.ts +1 -0
  34. package/dist/stages/encode/types.d.ts.map +1 -1
  35. package/dist/workers/stages/encode/{video-encode.worker.u2o7iXCT.js → video-encode.worker.BtxXzPR3.js} +6 -3
  36. package/dist/workers/stages/encode/video-encode.worker.BtxXzPR3.js.map +1 -0
  37. package/dist/workers/worker-manifest.json +1 -1
  38. package/package.json +1 -1
  39. package/dist/workers/stages/encode/video-encode.worker.u2o7iXCT.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ExportScheduler.js","sources":["../../src/orchestrator/ExportScheduler.ts"],"sourcesContent":["import { CompositionModel } from '../model';\nimport { ExportOptions } from '../types';\nimport { WorkerPool } from '../worker/WorkerPool';\nimport { CompositionPlanner } from './CompositionPlanner';\nimport { CacheManager } from '../cache/CacheManager';\nimport { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { MuxManager } from '../stages/mux/MuxManager';\nimport { GlobalAudioSession } from './GlobalAudioSession';\nimport { VideoClipSession } from './VideoClipSession';\nimport { WorkerType } from '../worker/types';\nimport { hasResourceId, type TimeUs } from '../model/types';\nimport type { ExportController } from '../controllers/ExportController';\nimport { EventBus } from '../event/EventBus';\nimport { MeframeEvent, EventPayloadMap } from '../event/events';\n\ninterface ExportSchedulerDeps {\n workerPool: WorkerPool;\n planner: CompositionPlanner;\n cacheManager: CacheManager;\n resourceLoader: ResourceLoader;\n muxManager: MuxManager;\n audioSession: GlobalAudioSession;\n workerConfigsProvider: () => Record<WorkerType, any>;\n eventBus: EventBus<EventPayloadMap>;\n}\n\ninterface ExtendedExportOptions extends ExportOptions {\n signal?: AbortSignal;\n controller?: ExportController;\n}\n\nexport class ExportScheduler {\n private deps: ExportSchedulerDeps;\n\n constructor(deps: ExportSchedulerDeps) {\n this.deps = deps;\n }\n\n async execute(model: CompositionModel, options: ExtendedExportOptions): Promise<Blob> {\n const { muxManager, audioSession, eventBus, resourceLoader } = this.deps;\n const signal = options.signal;\n const controller = options.controller;\n\n const checkStatus = async () => {\n if (signal?.aborted) {\n throw new DOMException('Export aborted', 'AbortError');\n }\n if (controller?.isPaused()) {\n // Wait until resumed\n while (controller.isPaused()) {\n if (signal?.aborted) throw new DOMException('Export aborted', 'AbortError');\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n }\n };\n\n const width = options.width || model.renderConfig?.width || 720;\n const height = options.height || model.renderConfig?.height || 1280;\n const fps = options.fps || model.fps || 30;\n\n eventBus.emit(MeframeEvent.ExportStart, {\n format: options.format || 'mp4',\n width,\n height,\n fps,\n durationUs: model.durationUs,\n });\n\n try {\n // 1. Preload and parse all resources (0-40%)\n await this.preloadResources(model, resourceLoader, eventBus, checkStatus);\n\n // 2. Start Muxer\n muxManager.start({\n width,\n height,\n fps,\n });\n\n // 3. Process Video and Audio\n const mainTrack = model.tracks.find((t) => t.id === model.mainTrackId);\n if (mainTrack && mainTrack.clips.length > 0) {\n // Process audio with 60s windows (balance quality and memory)\n const audioPromise = this.processAudioInWindows(\n model.durationUs,\n audioSession,\n muxManager,\n checkStatus\n );\n\n // Process video clips sequentially\n await this.processVideoClipsSequentially(mainTrack.clips, muxManager, model, checkStatus);\n\n // Wait for audio encoding to complete\n await audioPromise;\n } else {\n console.warn('[ExportScheduler] No video clips found');\n }\n\n // Finalize audio session (close encoder)\n await audioSession.finalizeExportAudio();\n\n if (signal?.aborted) {\n throw new DOMException('Export aborted', 'AbortError');\n }\n\n // 4. Finalize\n const blob = await muxManager.finalize();\n\n eventBus.emit(MeframeEvent.ExportComplete, {\n size: blob.size,\n durationMs: model.durationUs / 1000,\n format: options.format || 'mp4',\n });\n\n return blob;\n } catch (error) {\n eventBus.emit(MeframeEvent.ExportError, {\n error: error instanceof Error ? error : new Error(String(error)),\n stage: 'export',\n });\n throw error;\n }\n }\n\n /**\n * Preload all resources (0-40% progress)\n */\n private async preloadResources(\n model: CompositionModel,\n resourceLoader: ResourceLoader,\n eventBus: EventBus<EventPayloadMap>,\n checkStatus: () => Promise<void>\n ): Promise<void> {\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 0,\n stage: 'preparing',\n message: 'Loading and parsing resources...',\n });\n\n // Collect all resources to load\n const resourcesToLoad: string[] = [];\n for (const track of model.tracks) {\n if (!['video', 'audio'].includes(track.kind)) continue;\n for (const clip of track.clips) {\n if (hasResourceId(clip)) {\n resourcesToLoad.push(clip.resourceId);\n }\n }\n }\n\n // Load resources with progress updates\n for (let i = 0; i < resourcesToLoad.length; i++) {\n await checkStatus();\n\n const resourceId = resourcesToLoad[i];\n await resourceLoader.load(resourceId, { priority: 'high' });\n\n // Update progress: 0-40%\n const progress = ((i + 1) / resourcesToLoad.length) * 0.4;\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress,\n stage: 'preparing',\n message: `Loading resources... (${i + 1}/${resourcesToLoad.length})`,\n });\n }\n }\n\n /**\n * Process audio in 60-second windows\n * - Videos ≤60s: Single pass (zero boundaries)\n * - Videos >60s: 60s windows (minimal boundaries, ~23MB per window)\n */\n private async processAudioInWindows(\n totalDurationUs: TimeUs,\n audioSession: GlobalAudioSession,\n muxManager: MuxManager,\n checkStatus: () => Promise<void>\n ): Promise<void> {\n const WINDOW_DURATION_US = 5 * 60 * 1_000_000; // 5 minutes\n let currentUs = 0;\n\n while (currentUs < totalDurationUs) {\n await checkStatus();\n\n const endUs = Math.min(currentUs + WINDOW_DURATION_US, totalDurationUs);\n\n await audioSession.mixAndEncodeSegment(currentUs, endUs, (chunk, meta) =>\n muxManager.writeAudioChunk(chunk, meta)\n );\n\n currentUs = endUs;\n }\n }\n\n private async processVideoClipsSequentially(\n clips: any[],\n muxManager: MuxManager,\n model: CompositionModel,\n checkStatus: () => Promise<void>\n ) {\n let accumulatedDurationUs = 0;\n\n for (let i = 0; i < clips.length; i++) {\n const clip = clips[i];\n // const nextClip = clips[i + 1];\n\n await checkStatus(); // Check before starting new clip\n\n const sessionId = `${clip.id}-export`;\n let streamFinishedResolver: () => void;\n let streamFinishedRejecter: (err: any) => void;\n const streamFinishedPromise = new Promise<void>((resolve, reject) => {\n streamFinishedResolver = resolve;\n streamFinishedRejecter = reject;\n });\n\n const session = await VideoClipSession.create({\n clipId: clip.id,\n sessionId,\n forL2Only: true,\n planner: this.deps.planner,\n workerPool: this.deps.workerPool,\n cacheManager: this.deps.cacheManager,\n compositionModel: model,\n workerConfigs: this.deps.workerConfigsProvider(),\n callbacks: {\n onComposedStreamReady: async () => {},\n onEncodedStreamReady: async (stream, track) => {\n if (track === 'video') {\n const reader = stream.getReader();\n try {\n while (true) {\n await checkStatus();\n\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n const originalChunk = value.chunk;\n const metadata = value.metadata;\n\n const buffer = new ArrayBuffer(originalChunk.byteLength);\n originalChunk.copyTo(buffer);\n\n const remappedChunk = new EncodedVideoChunk({\n type: originalChunk.type,\n timestamp: originalChunk.timestamp + accumulatedDurationUs,\n duration: originalChunk.duration ?? undefined,\n data: buffer,\n });\n\n muxManager.writeVideoChunk(remappedChunk, metadata);\n\n // Emit progress: 40-100%\n const encodingProgress =\n (accumulatedDurationUs + originalChunk.timestamp) / model.durationUs;\n const totalProgress = 0.4 + encodingProgress * 0.6; // 40% + (0-60%)\n\n this.deps.eventBus.emit(MeframeEvent.ExportProgress, {\n progress: Math.min(1.0, totalProgress),\n stage: 'encoding',\n timeUs: accumulatedDurationUs + originalChunk.timestamp,\n });\n }\n }\n streamFinishedResolver();\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') {\n streamFinishedRejecter(error);\n } else {\n console.error(`[ExportScheduler] Stream error for clip ${clip.id}:`, error);\n streamFinishedRejecter(error);\n }\n } finally {\n reader.releaseLock();\n }\n }\n },\n onPipelineReady: async (attachmentResourceIds) => {\n // Load Main Track Resource\n if (hasResourceId(clip)) {\n await this.deps.resourceLoader.load(clip.resourceId, {\n priority: 'high',\n sessionId,\n clipId: clip.id,\n trackId: clip.trackId,\n isMainTrack: true,\n });\n }\n\n // Load Attachment Resources\n if (attachmentResourceIds) {\n for (const rid of attachmentResourceIds) {\n await this.deps.resourceLoader.load(rid, {\n priority: 'normal',\n sessionId,\n clipId: clip.id,\n isMainTrack: false,\n });\n }\n }\n },\n },\n });\n\n try {\n await session.activate();\n await streamFinishedPromise;\n } finally {\n await session.dispose();\n }\n\n accumulatedDurationUs += clip.durationUs;\n }\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ExportScheduler.js","sources":["../../src/orchestrator/ExportScheduler.ts"],"sourcesContent":["import { CompositionModel } from '../model';\nimport { ExportOptions } from '../types';\nimport { WorkerPool } from '../worker/WorkerPool';\nimport { CompositionPlanner } from './CompositionPlanner';\nimport { CacheManager } from '../cache/CacheManager';\nimport { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { MuxManager } from '../stages/mux/MuxManager';\nimport { GlobalAudioSession } from './GlobalAudioSession';\nimport { VideoClipSession } from './VideoClipSession';\nimport { WorkerType } from '../worker/types';\nimport { hasResourceId, type TimeUs } from '../model/types';\nimport type { ExportController } from '../controllers/ExportController';\nimport { EventBus } from '../event/EventBus';\nimport { MeframeEvent, EventPayloadMap } from '../event/events';\n\ninterface ExportSchedulerDeps {\n workerPool: WorkerPool;\n planner: CompositionPlanner;\n cacheManager: CacheManager;\n resourceLoader: ResourceLoader;\n muxManager: MuxManager;\n audioSession: GlobalAudioSession;\n workerConfigsProvider: () => Record<WorkerType, any>;\n eventBus: EventBus<EventPayloadMap>;\n}\n\ninterface ExtendedExportOptions extends ExportOptions {\n signal?: AbortSignal;\n controller?: ExportController;\n}\n\nexport class ExportScheduler {\n private deps: ExportSchedulerDeps;\n\n constructor(deps: ExportSchedulerDeps) {\n this.deps = deps;\n }\n\n async execute(model: CompositionModel, options: ExtendedExportOptions): Promise<Blob> {\n const { muxManager, audioSession, eventBus, resourceLoader } = this.deps;\n const signal = options.signal;\n const controller = options.controller;\n\n const checkStatus = async () => {\n if (signal?.aborted) {\n throw new DOMException('Export aborted', 'AbortError');\n }\n if (controller?.isPaused()) {\n // Wait until resumed\n while (controller.isPaused()) {\n if (signal?.aborted) throw new DOMException('Export aborted', 'AbortError');\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n }\n };\n\n const width = options.width || model.renderConfig?.width || 720;\n const height = options.height || model.renderConfig?.height || 1280;\n const fps = options.fps || model.fps || 30;\n\n eventBus.emit(MeframeEvent.ExportStart, {\n format: options.format || 'mp4',\n width,\n height,\n fps,\n durationUs: model.durationUs,\n });\n\n try {\n // 1. Preload and parse all resources (0-40%)\n await this.preloadResources(model, resourceLoader, eventBus, checkStatus);\n\n // 2. Start Muxer\n muxManager.start({\n width,\n height,\n fps,\n });\n\n // 3. Process Video and Audio\n const mainTrack = model.tracks.find((t) => t.id === model.mainTrackId);\n if (mainTrack && mainTrack.clips.length > 0) {\n // Process audio with 60s windows (balance quality and memory)\n const audioPromise = this.processAudioInWindows(\n model.durationUs,\n audioSession,\n muxManager,\n checkStatus\n );\n\n // Process video clips sequentially\n await this.processVideoClipsSequentially(mainTrack.clips, muxManager, model, checkStatus);\n\n // Wait for audio encoding to complete\n await audioPromise;\n } else {\n console.warn('[ExportScheduler] No video clips found');\n }\n\n // Finalize audio session (close encoder)\n await audioSession.finalizeExportAudio();\n\n if (signal?.aborted) {\n throw new DOMException('Export aborted', 'AbortError');\n }\n\n // 4. Finalize\n const blob = await muxManager.finalize();\n\n eventBus.emit(MeframeEvent.ExportComplete, {\n size: blob.size,\n durationMs: model.durationUs / 1000,\n format: options.format || 'mp4',\n });\n\n return blob;\n } catch (error) {\n eventBus.emit(MeframeEvent.ExportError, {\n error: error instanceof Error ? error : new Error(String(error)),\n stage: 'export',\n });\n throw error;\n }\n }\n\n /**\n * Preload all resources (0-40% progress)\n */\n private async preloadResources(\n model: CompositionModel,\n resourceLoader: ResourceLoader,\n eventBus: EventBus<EventPayloadMap>,\n checkStatus: () => Promise<void>\n ): Promise<void> {\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 0,\n stage: 'preparing',\n message: 'Loading and parsing resources...',\n });\n\n // Collect all resources to load\n const resourcesToLoad: string[] = [];\n for (const track of model.tracks) {\n if (!['video', 'audio'].includes(track.kind)) continue;\n for (const clip of track.clips) {\n if (hasResourceId(clip)) {\n resourcesToLoad.push(clip.resourceId);\n }\n }\n }\n\n // Load resources with progress updates\n for (let i = 0; i < resourcesToLoad.length; i++) {\n await checkStatus();\n\n const resourceId = resourcesToLoad[i];\n await resourceLoader.load(resourceId, { priority: 'high' });\n\n // Update progress: 0-40%\n const progress = ((i + 1) / resourcesToLoad.length) * 0.4;\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress,\n stage: 'preparing',\n message: `Loading resources... (${i + 1}/${resourcesToLoad.length})`,\n });\n }\n }\n\n /**\n * Process audio in 60-second windows\n * - Videos ≤60s: Single pass (zero boundaries)\n * - Videos >60s: 60s windows (minimal boundaries, ~23MB per window)\n */\n private async processAudioInWindows(\n totalDurationUs: TimeUs,\n audioSession: GlobalAudioSession,\n muxManager: MuxManager,\n checkStatus: () => Promise<void>\n ): Promise<void> {\n const WINDOW_DURATION_US = 5 * 60 * 1_000_000; // 5 minutes\n let currentUs = 0;\n\n while (currentUs < totalDurationUs) {\n await checkStatus();\n\n const endUs = Math.min(currentUs + WINDOW_DURATION_US, totalDurationUs);\n\n await audioSession.mixAndEncodeSegment(currentUs, endUs, (chunk, meta) =>\n muxManager.writeAudioChunk(chunk, meta)\n );\n\n currentUs = endUs;\n }\n }\n\n private async processVideoClipsSequentially(\n clips: any[],\n muxManager: MuxManager,\n model: CompositionModel,\n checkStatus: () => Promise<void>\n ) {\n // Use actual last written timestamp + duration as offset for next clip\n // This avoids duplicate PTS when mp4-muxer rounds microseconds to timescale\n let nextClipStartUs = 0;\n // Track last chunk's end time (timestamp + duration) for precise offset calculation\n let lastChunkEndUs = 0;\n\n for (let i = 0; i < clips.length; i++) {\n const clip = clips[i];\n const currentClipOffsetUs = nextClipStartUs;\n\n await checkStatus(); // Check before starting new clip\n\n const sessionId = `${clip.id}-export`;\n let streamFinishedResolver: () => void;\n let streamFinishedRejecter: (err: any) => void;\n const streamFinishedPromise = new Promise<void>((resolve, reject) => {\n streamFinishedResolver = resolve;\n streamFinishedRejecter = reject;\n });\n\n const session = await VideoClipSession.create({\n clipId: clip.id,\n sessionId,\n forL2Only: true,\n planner: this.deps.planner,\n workerPool: this.deps.workerPool,\n cacheManager: this.deps.cacheManager,\n compositionModel: model,\n workerConfigs: this.deps.workerConfigsProvider(),\n callbacks: {\n onComposedStreamReady: async () => {},\n onEncodedStreamReady: async (stream, track) => {\n if (track === 'video') {\n const reader = stream.getReader();\n try {\n while (true) {\n await checkStatus();\n\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n const originalChunk = value.chunk;\n const metadata = value.metadata;\n const chunkDuration = originalChunk.duration ?? 33333; // Default ~30fps\n\n const remappedTimestamp = originalChunk.timestamp + currentClipOffsetUs;\n\n const buffer = new ArrayBuffer(originalChunk.byteLength);\n originalChunk.copyTo(buffer);\n\n const remappedChunk = new EncodedVideoChunk({\n type: originalChunk.type,\n timestamp: remappedTimestamp,\n duration: chunkDuration,\n data: buffer,\n });\n\n muxManager.writeVideoChunk(remappedChunk, metadata);\n\n // Track end time for next clip's offset\n lastChunkEndUs = remappedTimestamp + chunkDuration;\n\n // Emit progress: 40-100%\n const encodingProgress = remappedTimestamp / model.durationUs;\n const totalProgress = 0.4 + encodingProgress * 0.6; // 40% + (0-60%)\n\n this.deps.eventBus.emit(MeframeEvent.ExportProgress, {\n progress: Math.min(1.0, totalProgress),\n stage: 'encoding',\n timeUs: remappedTimestamp,\n });\n }\n }\n streamFinishedResolver();\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') {\n streamFinishedRejecter(error);\n } else {\n console.error(`[ExportScheduler] Stream error for clip ${clip.id}:`, error);\n streamFinishedRejecter(error);\n }\n } finally {\n reader.releaseLock();\n }\n }\n },\n onPipelineReady: async (attachmentResourceIds) => {\n // Load Main Track Resource\n if (hasResourceId(clip)) {\n await this.deps.resourceLoader.load(clip.resourceId, {\n priority: 'high',\n sessionId,\n clipId: clip.id,\n trackId: clip.trackId,\n isMainTrack: true,\n });\n }\n\n // Load Attachment Resources\n if (attachmentResourceIds) {\n for (const rid of attachmentResourceIds) {\n await this.deps.resourceLoader.load(rid, {\n priority: 'normal',\n sessionId,\n clipId: clip.id,\n isMainTrack: false,\n });\n }\n }\n },\n },\n });\n\n try {\n await session.activate();\n await streamFinishedPromise;\n } finally {\n await session.dispose();\n }\n\n // Use actual last chunk end time as next clip's start\n // This ensures no timestamp overlap even after muxer rounding\n nextClipStartUs = lastChunkEndUs;\n }\n }\n}\n"],"names":[],"mappings":"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"}
@@ -185,7 +185,7 @@ class GlobalAudioSession {
185
185
  }
186
186
  reset() {
187
187
  this.stopAllAudioSources();
188
- this.deps.cacheManager.resetAudioCache();
188
+ this.deps.cacheManager.clearAudioCache();
189
189
  this.activeClips.clear();
190
190
  }
191
191
  /**
@@ -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, Clip } 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';\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 void this.activateAllAudioClips();\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?: { immediate?: boolean }): Promise<void> {\n const model = this.model;\n if (!model) return;\n\n const immediate = options?.immediate ?? false;\n\n // 1. Find all audio/video clips active at this time\n const activeClips: Clip[] = [];\n for (const track of model.tracks) {\n if (track.kind !== 'audio' && track.kind !== 'video') continue;\n const clips = model.getClipsAtTime(timeUs, track.id);\n activeClips.push(...clips);\n }\n\n // 2. Ensure audio window for each clip (using window-based strategy)\n const ensureAudioWindowPromises = activeClips.map(async (clip) => {\n if (!hasResourceId(clip)) return;\n\n const resource = model.getResource(clip.resourceId);\n if (!resource) return;\n\n // All audio (both audio clips and video audio) use AudioSampleCache → window decode\n // Check if resource samples are cached\n if (!this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n // Check if resource is already loaded (but has no audio track)\n const resource = model.getResource(clip.resourceId);\n if (resource?.state === 'ready') {\n // Resource loaded but no audio track - skip this clip (video files may not have audio)\n return;\n }\n\n // Resource not yet loaded\n // Blocking: wait for resource to load\n await this.deps.resourceLoader.load(clip.resourceId, {\n priority: 'high',\n clipId: clip.id,\n trackId: clip.trackId,\n });\n }\n\n const clipRelativeTimeUs = timeUs - clip.startUs;\n const WINDOW_DURATION = 3_000_000;\n const startUs = clipRelativeTimeUs;\n const endUs = Math.min(clip.durationUs, startUs + WINDOW_DURATION);\n // Now decode window from AudioSampleCache\n await this.ensureAudioWindow(clip.id, startUs, endUs);\n });\n\n if (immediate) {\n void Promise.all(ensureAudioWindowPromises);\n } else {\n await Promise.all(ensureAudioWindowPromises);\n }\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\n for (const track of audioTracks) {\n for (const clip of track.clips) {\n if (!this.activeClips.has(clip.id)) {\n if (!isAudioClip(clip)) {\n throw new Error(`Clip ${clip.id} in audio track is not an audio clip`);\n }\n\n // 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 priority: 'high',\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\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, { immediate: false });\n\n this.isPlaying = true;\n // Reset playback states when starting to initialize scheduling from current time\n this.resetPlaybackStates();\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 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 // Mix audio using OfflineAudioMixer (handles resampling + mixing)\n const mixedBuffer = await this.mixer.mix(startUs, endUs);\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.resetAudioCache();\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 const model = this.model;\n if (!model) return;\n\n // Find all clips (audio + video with audio tracks) that overlap with [startUs, endUs]\n const clips: Clip[] = [];\n for (const track of model.tracks) {\n if (track.kind !== 'audio' && track.kind !== 'video') continue;\n\n for (const clip of track.clips) {\n const clipEndUs = clip.startUs + clip.durationUs;\n // Check if clip overlaps with segment\n if (clip.startUs < endUs && clipEndUs > startUs) {\n clips.push(clip);\n }\n }\n }\n\n // Decode audio window for each clip from AudioSampleCache\n for (const clip of clips) {\n if (!hasResourceId(clip)) continue;\n\n // Skip if no audio track in resource\n if (!this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n continue;\n }\n\n // Calculate clip-relative window (decoder expects clip-relative time)\n const clipRelativeStartUs = Math.max(0, startUs - clip.startUs);\n const clipRelativeEndUs = Math.min(clip.durationUs, endUs - clip.startUs);\n\n // Decode window from AudioSampleCache (main thread)\n await this.ensureAudioWindow(clip.id, clipRelativeStartUs, clipRelativeEndUs);\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 source.stop();\n source.disconnect();\n } catch {\n // Ignore\n }\n }\n this.scheduledSources.clear();\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 ≥80% data, not just a single point\n * This naturally prevents premature return during preheating\n */\n async ensureAudioWindow(clipId: string, startUs: TimeUs, endUs: TimeUs): Promise<void> {\n // Check L1 cache - window-level verification (not point-level like video)\n if (this.deps.cacheManager.hasWindowPCM(clipId, startUs, endUs)) {\n return; // Entire window has sufficient data (≥80%)\n }\n\n await this.decodeAudioWindow(clipId, startUs, endUs);\n }\n\n /**\n * Decode audio window for a clip (aligned with video architecture)\n * Simple strategy: decode entire window range, cache handles duplicates\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 // Decode chunks to AudioL1Cache (cache will handle append/merge)\n await this.decodeAudioSamples(\n clipId,\n windowChunks,\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, Clip } 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';\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 void this.activateAllAudioClips();\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?: { immediate?: boolean }): Promise<void> {\n const model = this.model;\n if (!model) return;\n\n const immediate = options?.immediate ?? false;\n\n // 1. Find all audio/video clips active at this time\n const activeClips: Clip[] = [];\n for (const track of model.tracks) {\n if (track.kind !== 'audio' && track.kind !== 'video') continue;\n const clips = model.getClipsAtTime(timeUs, track.id);\n activeClips.push(...clips);\n }\n\n // 2. Ensure audio window for each clip (using window-based strategy)\n const ensureAudioWindowPromises = activeClips.map(async (clip) => {\n if (!hasResourceId(clip)) return;\n\n const resource = model.getResource(clip.resourceId);\n if (!resource) return;\n\n // All audio (both audio clips and video audio) use AudioSampleCache → window decode\n // Check if resource samples are cached\n if (!this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n // Check if resource is already loaded (but has no audio track)\n const resource = model.getResource(clip.resourceId);\n if (resource?.state === 'ready') {\n // Resource loaded but no audio track - skip this clip (video files may not have audio)\n return;\n }\n\n // Resource not yet loaded\n // Blocking: wait for resource to load\n await this.deps.resourceLoader.load(clip.resourceId, {\n priority: 'high',\n clipId: clip.id,\n trackId: clip.trackId,\n });\n }\n\n const clipRelativeTimeUs = timeUs - clip.startUs;\n const WINDOW_DURATION = 3_000_000;\n const startUs = clipRelativeTimeUs;\n const endUs = Math.min(clip.durationUs, startUs + WINDOW_DURATION);\n // Now decode window from AudioSampleCache\n await this.ensureAudioWindow(clip.id, startUs, endUs);\n });\n\n if (immediate) {\n void Promise.all(ensureAudioWindowPromises);\n } else {\n await Promise.all(ensureAudioWindowPromises);\n }\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\n for (const track of audioTracks) {\n for (const clip of track.clips) {\n if (!this.activeClips.has(clip.id)) {\n if (!isAudioClip(clip)) {\n throw new Error(`Clip ${clip.id} in audio track is not an audio clip`);\n }\n\n // 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 priority: 'high',\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\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, { immediate: false });\n\n this.isPlaying = true;\n // Reset playback states when starting to initialize scheduling from current time\n this.resetPlaybackStates();\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 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 // Mix audio using OfflineAudioMixer (handles resampling + mixing)\n const mixedBuffer = await this.mixer.mix(startUs, endUs);\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 const model = this.model;\n if (!model) return;\n\n // Find all clips (audio + video with audio tracks) that overlap with [startUs, endUs]\n const clips: Clip[] = [];\n for (const track of model.tracks) {\n if (track.kind !== 'audio' && track.kind !== 'video') continue;\n\n for (const clip of track.clips) {\n const clipEndUs = clip.startUs + clip.durationUs;\n // Check if clip overlaps with segment\n if (clip.startUs < endUs && clipEndUs > startUs) {\n clips.push(clip);\n }\n }\n }\n\n // Decode audio window for each clip from AudioSampleCache\n for (const clip of clips) {\n if (!hasResourceId(clip)) continue;\n\n // Skip if no audio track in resource\n if (!this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n continue;\n }\n\n // Calculate clip-relative window (decoder expects clip-relative time)\n const clipRelativeStartUs = Math.max(0, startUs - clip.startUs);\n const clipRelativeEndUs = Math.min(clip.durationUs, endUs - clip.startUs);\n\n // Decode window from AudioSampleCache (main thread)\n await this.ensureAudioWindow(clip.id, clipRelativeStartUs, clipRelativeEndUs);\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 source.stop();\n source.disconnect();\n } catch {\n // Ignore\n }\n }\n this.scheduledSources.clear();\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 ≥80% data, not just a single point\n * This naturally prevents premature return during preheating\n */\n async ensureAudioWindow(clipId: string, startUs: TimeUs, endUs: TimeUs): Promise<void> {\n // Check L1 cache - window-level verification (not point-level like video)\n if (this.deps.cacheManager.hasWindowPCM(clipId, startUs, endUs)) {\n return; // Entire window has sufficient data (≥80%)\n }\n\n await this.decodeAudioWindow(clipId, startUs, endUs);\n }\n\n /**\n * Decode audio window for a clip (aligned with video architecture)\n * Simple strategy: decode entire window range, cache handles duplicates\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 // Decode chunks to AudioL1Cache (cache will handle append/merge)\n await this.decodeAudioSamples(\n clipId,\n windowChunks,\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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"}
@@ -47,7 +47,8 @@ export declare class OnDemandVideoSession {
47
47
  private readonly globalTimeUs;
48
48
  private readonly targetTimeUs;
49
49
  private decoder;
50
- private isDisposed;
50
+ isDisposed: boolean;
51
+ private aborted;
51
52
  private decodedFrames;
52
53
  private constructor();
53
54
  static create(config: OnDemandVideoSessionConfig): Promise<OnDemandVideoSession>;
@@ -67,6 +68,11 @@ export declare class OnDemandVideoSession {
67
68
  private demuxGOPData;
68
69
  private decodeChunks;
69
70
  private cacheDecodedFrames;
71
+ /**
72
+ * Fast decode single keyframe for immediate seek preview
73
+ * Returns the decoded keyframe timestamp
74
+ */
75
+ decodeKeyframe(targetTimeUs: TimeUs): Promise<TimeUs | null>;
70
76
  dispose(): Promise<void>;
71
77
  }
72
78
  export {};
@@ -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,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAGvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAQpE,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,UAAU,EAAE,MAAM,EAClB,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;IAuEhB,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,CAAkC;IACjD,OAAO,CAAC,UAAU,CAAS;IAC3B,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;IA0DjE,OAAO,CAAC,2BAA2B;IAgDnC;;;;;;OAMG;YACW,YAAY;YA0DZ,YAAY;YA4CZ,kBAAkB;IA4C1B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAiB/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,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAGvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAQpE,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,UAAU,EAAE,MAAM,EAClB,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;IAuEhB,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,CAAkC;IACjD,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;IAsEjE,OAAO,CAAC,2BAA2B;IAgDnC;;;;;;OAMG;YACW,YAAY;YA0DZ,YAAY;YAqDZ,kBAAkB;IA4ChC;;;OAGG;IACG,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAsD5D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAuB/B"}
@@ -71,6 +71,7 @@ class OnDemandVideoSession {
71
71
  targetTimeUs;
72
72
  decoder = null;
73
73
  isDisposed = false;
74
+ aborted = false;
74
75
  decodedFrames = [];
75
76
  constructor(config) {
76
77
  this.clipId = config.clipId;
@@ -129,8 +130,17 @@ class OnDemandVideoSession {
129
130
  gopWindow.byteStart,
130
131
  gopWindow.byteEnd
131
132
  );
133
+ if (this.aborted) return;
132
134
  const chunks = await this.demuxGOPData(gopData, index, gopWindow);
135
+ if (this.aborted) return;
133
136
  await this.decodeChunks(chunks, index);
137
+ if (this.aborted) {
138
+ for (const frame of this.decodedFrames) {
139
+ frame.close();
140
+ }
141
+ this.decodedFrames = [];
142
+ return;
143
+ }
134
144
  await this.cacheDecodedFrames(startUs, endUs);
135
145
  }
136
146
  calculateGOPRangesForWindow(index, startUs, endUs) {
@@ -226,6 +236,10 @@ class OnDemandVideoSession {
226
236
  const chunkStream = new ReadableStream({
227
237
  start: (controller) => {
228
238
  for (const chunk of chunks) {
239
+ if (this.aborted) {
240
+ controller.close();
241
+ return;
242
+ }
229
243
  controller.enqueue(chunk);
230
244
  }
231
245
  controller.close();
@@ -235,12 +249,16 @@ class OnDemandVideoSession {
235
249
  const reader = frameStream.getReader();
236
250
  try {
237
251
  while (true) {
252
+ if (this.aborted) break;
238
253
  const { done, value } = await reader.read();
239
254
  if (done) break;
240
255
  if (value) {
241
256
  this.decodedFrames.push(value);
242
257
  }
243
258
  }
259
+ } catch (error) {
260
+ if (this.aborted) return;
261
+ throw error;
244
262
  } finally {
245
263
  reader.releaseLock();
246
264
  }
@@ -273,10 +291,55 @@ class OnDemandVideoSession {
273
291
  }
274
292
  this.decodedFrames = [];
275
293
  }
294
+ /**
295
+ * Fast decode single keyframe for immediate seek preview
296
+ * Returns the decoded keyframe timestamp
297
+ */
298
+ async decodeKeyframe(targetTimeUs) {
299
+ if (this.isDisposed || this.aborted) return null;
300
+ const index = this.mp4IndexCache.get(this.resourceId);
301
+ if (!index || !index.tracks.video) return null;
302
+ const keyframeSample = this.mp4IndexCache.getNearestKeyframe(this.resourceId, targetTimeUs);
303
+ if (!keyframeSample) return null;
304
+ const keyframeData = await this.cacheManager.readResourceRange(
305
+ this.resourceId,
306
+ keyframeSample.byteOffset,
307
+ keyframeSample.byteOffset + keyframeSample.byteLength
308
+ );
309
+ if (this.aborted) return null;
310
+ const chunk = new EncodedVideoChunk({
311
+ type: "key",
312
+ timestamp: keyframeSample.timestamp,
313
+ duration: keyframeSample.duration,
314
+ data: keyframeData
315
+ });
316
+ if (this.aborted) return null;
317
+ await this.decodeChunks([chunk], index);
318
+ if (this.aborted || this.decodedFrames.length === 0) return null;
319
+ const frame = this.decodedFrames[0];
320
+ if (!frame) return null;
321
+ const frameDuration = frame.duration ?? Math.round(1e6 / this.fps);
322
+ const frameGlobalTime = this.globalTimeUs + (frame.timestamp - this.targetTimeUs);
323
+ this.cacheManager.addFrame(
324
+ frame,
325
+ this.clipId,
326
+ frameDuration,
327
+ this.compositionModel.mainTrackId,
328
+ frameGlobalTime
329
+ );
330
+ this.decodedFrames = [];
331
+ return keyframeSample.timestamp;
332
+ }
276
333
  async dispose() {
277
334
  if (this.isDisposed) return;
278
- if (this.decoder && this.decoder.state !== "closed") {
279
- await this.decoder.close();
335
+ this.aborted = true;
336
+ if (this.decoder) {
337
+ if (this.decoder.state === "configured") {
338
+ await this.decoder.reset();
339
+ }
340
+ if (this.decoder.state !== "closed") {
341
+ await this.decoder.close();
342
+ }
280
343
  this.decoder = null;
281
344
  }
282
345
  for (const frame of this.decodedFrames) {
@@ -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 } from '../model/types';\nimport type { CompositionModel, Clip } from '../model';\nimport { VideoChunkDecoder } from '../stages/decode/VideoChunkDecoder';\nimport { binarySearchOverlapping } from '../utils/binary-search';\nimport type { ResourceLoader } from '../stages/load/ResourceLoader';\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 console.warn('[OnDemandVideoSession] First chunk is not a keyframe, skipping');\n return;\n }\n\n try {\n // Create temporary decoder\n const decoder = new VideoChunkDecoder(`first-frame-${resourceId}`, {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n hardwareAcceleration: 'no-preference' as HardwareAcceleration,\n thread: 'main',\n });\n\n try {\n // Create chunk stream with ALL chunks from first GOP\n const chunkStream = new ReadableStream<EncodedVideoChunk>({\n start(controller) {\n for (const chunk of chunks) {\n controller.enqueue(chunk);\n }\n controller.close();\n },\n });\n\n // Decode all frames\n const frameStream = chunkStream.pipeThrough(decoder.createStream());\n const reader = frameStream.getReader();\n\n try {\n const frameDuration = Math.round(1_000_000 / fps);\n\n // Read and cache all decoded frames\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n if (value) {\n // Calculate global time for this frame\n const frameGlobalTime = clip.startUs + value.timestamp;\n\n cacheManager.addFrame(\n value,\n clip.id,\n frameDuration,\n clip.trackId ?? 'main',\n frameGlobalTime\n );\n }\n }\n } finally {\n reader.releaseLock();\n }\n } finally {\n await decoder.close();\n }\n } catch (error) {\n console.warn('[OnDemandVideoSession] Failed to decode first GOP:', 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: VideoChunkDecoder | null = null;\n private isDisposed = 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 // Check if resource is an image (no MP4 index)\n const resource = this.compositionModel.getResource(this.resourceId);\n if (resource?.type === 'image') {\n // Image clip: handled by direct ImageBitmap access in PlaybackController\n // No need to decode anything here\n const image = await this.resourceLoader.loadImage(resource);\n if (image) {\n const frame = new VideoFrame(image, {\n timestamp: startUs,\n duration: endUs - startUs,\n });\n this.cacheManager.addFrame(\n frame,\n this.clipId,\n endUs - startUs,\n this.compositionModel.mainTrackId,\n this.globalTimeUs\n );\n }\n return;\n }\n\n // Video clip: decode from OPFS\n // Get MP4 index\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 (with binary search optimization)\n const gopWindow = this.calculateGOPRangesForWindow(index, startUs, endUs);\n if (gopWindow.gops.length === 0) {\n console.warn('[OnDemandVideoSession] No GOP ranges for window', startUs, endUs);\n return;\n }\n\n // Read GOP data from OPFS (merged byte range)\n const gopData = await this.cacheManager.readResourceRange(\n this.resourceId,\n gopWindow.byteStart,\n gopWindow.byteEnd\n );\n\n // Extract chunks from GOP data (direct sample index slicing)\n const chunks = await this.demuxGOPData(gopData, index, gopWindow);\n\n // Decode chunks to frames\n await this.decodeChunks(chunks, index);\n\n // Write frames to L1\n await this.cacheDecodedFrames(startUs, endUs);\n }\n\n private calculateGOPRangesForWindow(\n index: MP4Index,\n startUs: TimeUs,\n endUs: TimeUs\n ): GOPWindowResult {\n if (!index.tracks.video) {\n console.warn('[OnDemandVideoSession] No video track in index');\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 console.warn('[OnDemandVideoSession] No GOP ranges for window', startUs, endUs);\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 // Create decoder (reuse VideoChunkDecoder like decodeFromL2)\n this.decoder = new VideoChunkDecoder(`ondemand-${this.clipId}`, {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n hardwareAcceleration: 'no-preference' as HardwareAcceleration,\n thread: 'main',\n });\n\n // Create stream from chunks\n const chunkStream = new ReadableStream<EncodedVideoChunk>({\n start: (controller) => {\n for (const chunk of chunks) {\n controller.enqueue(chunk);\n }\n controller.close();\n },\n });\n\n // Pipe through decoder\n const frameStream = chunkStream.pipeThrough(this.decoder.createStream());\n\n // Collect decoded frames\n const reader = frameStream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n this.decodedFrames.push(value);\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n\n private async cacheDecodedFrames(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n // Filter frames within window (include boundary frames)\n // Use original timestamp (no quantization)\n const framesToCache = this.decodedFrames.filter((f) => {\n // Exclude frames at window end boundary\n return f.timestamp >= startUs && f.timestamp < endUs;\n });\n\n for (const frame of framesToCache) {\n try {\n // Use original timestamp and duration (preserve source frame rate)\n const originalTimestamp = frame.timestamp;\n const originalDuration = frame.duration ?? Math.round(1_000_000 / this.fps);\n\n // Calculate global timestamp using original value\n const frameGlobalTime = this.globalTimeUs + (originalTimestamp - this.targetTimeUs);\n\n // Write RAW video frame to L1 with original timestamp, duration and global time\n // NOTE: addComposedFrame name is legacy, but now we store raw frames\n this.cacheManager.addFrame(\n frame,\n this.clipId,\n originalDuration,\n this.compositionModel.mainTrackId,\n frameGlobalTime\n );\n\n // Don't close frame here - it's now owned by L1 cache\n // (CacheManager.addComposedFrame clones it or takes ownership)\n } catch (error) {\n console.error('[OnDemandVideoSession] Cache error:', error);\n frame.close();\n }\n }\n\n // Clean up decoded frames that weren't cached\n for (const frame of this.decodedFrames) {\n if (!framesToCache.includes(frame)) {\n frame.close();\n }\n }\n this.decodedFrames = [];\n }\n\n async dispose(): Promise<void> {\n if (this.isDisposed) return;\n\n // Close decoder (check state to avoid closing already-closed decoder)\n if (this.decoder && this.decoder.state !== 'closed') {\n await this.decoder.close();\n this.decoder = null;\n }\n\n // Clean up any remaining frames\n for (const frame of this.decodedFrames) {\n frame.close();\n }\n this.decodedFrames = [];\n\n this.isDisposed = true;\n }\n}\n"],"names":[],"mappings":";;AA0CO,MAAM,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA,EAKhC,aAAa,yBACX,YACA,QACA,OACA,MACA,cACA,KACe;AACf,QAAI,OAAO,WAAW,EAAG;AAEzB,UAAM,aAAa,MAAM,OAAO;AAChC,QAAI,CAAC,WAAY;AAGjB,UAAM,aAAa,OAAO,CAAC;AAC3B,QAAI,CAAC,cAAc,WAAW,SAAS,OAAO;AAC5C,cAAQ,KAAK,gEAAgE;AAC7E;AAAA,IACF;AAEA,QAAI;AAEF,YAAM,UAAU,IAAI,kBAAkB,eAAe,UAAU,IAAI;AAAA,QACjE,OAAO,WAAW;AAAA,QAClB,OAAO,WAAW;AAAA,QAClB,QAAQ,WAAW;AAAA,QACnB,aAAa,WAAW;AAAA,QACxB,sBAAsB;AAAA,QACtB,QAAQ;AAAA,MAAA,CACT;AAED,UAAI;AAEF,cAAM,cAAc,IAAI,eAAkC;AAAA,UACxD,MAAM,YAAY;AAChB,uBAAW,SAAS,QAAQ;AAC1B,yBAAW,QAAQ,KAAK;AAAA,YAC1B;AACA,uBAAW,MAAA;AAAA,UACb;AAAA,QAAA,CACD;AAGD,cAAM,cAAc,YAAY,YAAY,QAAQ,cAAc;AAClE,cAAM,SAAS,YAAY,UAAA;AAE3B,YAAI;AACF,gBAAM,gBAAgB,KAAK,MAAM,MAAY,GAAG;AAGhD,iBAAO,MAAM;AACX,kBAAM,EAAE,MAAM,MAAA,IAAU,MAAM,OAAO,KAAA;AACrC,gBAAI,KAAM;AAEV,gBAAI,OAAO;AAET,oBAAM,kBAAkB,KAAK,UAAU,MAAM;AAE7C,2BAAa;AAAA,gBACX;AAAA,gBACA,KAAK;AAAA,gBACL;AAAA,gBACA,KAAK,WAAW;AAAA,gBAChB;AAAA,cAAA;AAAA,YAEJ;AAAA,UACF;AAAA,QACF,UAAA;AACE,iBAAO,YAAA;AAAA,QACT;AAAA,MACF,UAAA;AACE,cAAM,QAAQ,MAAA;AAAA,MAChB;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,KAAK,sDAAsD,KAAK;AAAA,IAE1E;AAAA,EACF;AAAA,EACiB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,UAAoC;AAAA,EACpC,aAAa;AAAA,EACb,gBAA8B,CAAA;AAAA,EAE9B,YAAY,QAAoC;AACtD,SAAK,SAAS,OAAO;AACrB,SAAK,aAAa,OAAO;AACzB,SAAK,gBAAgB,OAAO;AAC5B,SAAK,eAAe,OAAO;AAC3B,SAAK,iBAAiB,OAAO;AAC7B,SAAK,mBAAmB,OAAO;AAC/B,SAAK,MAAM,OAAO;AAClB,SAAK,eAAe,OAAO;AAC3B,SAAK,eAAe,OAAO;AAAA,EAC7B;AAAA,EAEA,aAAa,OAAO,QAAmE;AACrF,UAAM,UAAU,IAAI,qBAAqB,MAAM;AAC/C,UAAM,QAAQ,KAAA;AACd,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,OAAsB;AAAA,EAEpC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,SAAiB,OAA8B;AAChE,QAAI,KAAK,YAAY;AACnB,YAAM,IAAI,MAAM,0BAA0B;AAAA,IAC5C;AAGA,UAAM,WAAW,KAAK,iBAAiB,YAAY,KAAK,UAAU;AAClE,QAAI,UAAU,SAAS,SAAS;AAG9B,YAAM,QAAQ,MAAM,KAAK,eAAe,UAAU,QAAQ;AAC1D,UAAI,OAAO;AACT,cAAM,QAAQ,IAAI,WAAW,OAAO;AAAA,UAClC,WAAW;AAAA,UACX,UAAU,QAAQ;AAAA,QAAA,CACnB;AACD,aAAK,aAAa;AAAA,UAChB;AAAA,UACA,KAAK;AAAA,UACL,QAAQ;AAAA,UACR,KAAK,iBAAiB;AAAA,UACtB,KAAK;AAAA,QAAA;AAAA,MAET;AACA;AAAA,IACF;AAIA,UAAM,QAAQ,KAAK,cAAc,IAAI,KAAK,UAAU;AACpD,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,MAAM,+BAA+B,KAAK,UAAU,EAAE;AAAA,IAClE;AAGA,UAAM,YAAY,KAAK,4BAA4B,OAAO,SAAS,KAAK;AACxE,QAAI,UAAU,KAAK,WAAW,GAAG;AAC/B,cAAQ,KAAK,mDAAmD,SAAS,KAAK;AAC9E;AAAA,IACF;AAGA,UAAM,UAAU,MAAM,KAAK,aAAa;AAAA,MACtC,KAAK;AAAA,MACL,UAAU;AAAA,MACV,UAAU;AAAA,IAAA;AAIZ,UAAM,SAAS,MAAM,KAAK,aAAa,SAAS,OAAO,SAAS;AAGhE,UAAM,KAAK,aAAa,QAAQ,KAAK;AAGrC,UAAM,KAAK,mBAAmB,SAAS,KAAK;AAAA,EAC9C;AAAA,EAEQ,4BACN,OACA,SACA,OACiB;AACjB,QAAI,CAAC,MAAM,OAAO,OAAO;AACvB,cAAQ,KAAK,gDAAgD;AAC7D,aAAO,EAAE,MAAM,CAAA,GAAI,WAAW,GAAG,SAAS,EAAA;AAAA,IAC5C;AAEA,UAAM,EAAE,UAAU,QAAA,IAAY,MAAM,OAAO;AAG3C,UAAM,kBAAkB,KAAK,cAAc,mBAAmB,KAAK,YAAY,OAAO;AACtF,UAAM,gBAAgB,iBAAiB,aAAa;AAGpD,UAAM,kBAAkB,wBAAwB,UAAU,eAAe,OAAO,CAAC,KAAK,QAAQ;AAC5F,YAAM,UAAU,SAAS,MAAM,CAAC;AAChC,aAAO;AAAA,QACL,OAAO,IAAI;AAAA,QACX,KAAK,UAAU,QAAQ,cAAc;AAAA,MAAA;AAAA,IAEzC,CAAC;AAED,QAAI,gBAAgB,WAAW,GAAG;AAChC,cAAQ,KAAK,mDAAmD,SAAS,KAAK;AAC9E,aAAO,EAAE,MAAM,CAAA,GAAI,WAAW,GAAG,SAAS,EAAA;AAAA,IAC5C;AAGA,QAAI,YAAY;AAChB,QAAI,UAAU;AAEd,eAAW,OAAO,iBAAiB;AACjC,YAAM,cAAc,QAAQ,IAAI,mBAAmB;AACnD,YAAM,iBAAiB,IAAI,sBAAsB,IAAI,cAAc;AACnE,YAAM,YAAY,QAAQ,cAAc;AAExC,UAAI,eAAe,WAAW;AAC5B,oBAAY,KAAK,IAAI,WAAW,YAAY,UAAU;AACtD,kBAAU,KAAK,IAAI,SAAS,UAAU,aAAa,UAAU,UAAU;AAAA,MACzE;AAAA,IACF;AAEA,WAAO,EAAE,MAAM,iBAAiB,WAAW,QAAA;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,aACZ,MACA,OACA,WAC8B;AAC9B,UAAM,aAAa,MAAM,OAAO;AAChC,QAAI,CAAC,YAAY;AACf,YAAM,IAAI,MAAM,yBAAyB;AAAA,IAC3C;AAEA,UAAM,EAAE,YAAY;AACpB,UAAM,SAA8B,CAAA;AACpC,UAAM,WAAW,IAAI,WAAW,IAAI;AACpC,UAAM,iBAAiB,UAAU;AAGjC,eAAW,OAAO,UAAU,MAAM;AAChC,YAAM,WAAW,IAAI;AACrB,YAAM,SAAS,WAAW,IAAI;AAG9B,eAAS,IAAI,UAAU,IAAI,QAAQ,KAAK;AACtC,cAAM,SAAS,QAAQ,CAAC;AACxB,YAAI,CAAC,OAAQ;AAGb,cAAM,iBAAiB,OAAO,aAAa;AAG3C,YAAI,iBAAiB,KAAK,iBAAiB,OAAO,aAAa,KAAK,YAAY;AAC9E,kBAAQ,KAAK,iDAAiD;AAAA,YAC5D,cAAc,OAAO;AAAA,YACrB,cAAc,OAAO;AAAA,YACrB,YAAY;AAAA,YACZ;AAAA,YACA,cAAc,KAAK;AAAA,UAAA,CACpB;AACD;AAAA,QACF;AAGA,cAAM,aAAa,SAAS,MAAM,gBAAgB,iBAAiB,OAAO,UAAU;AAGpF,cAAM,QAAQ,IAAI,kBAAkB;AAAA,UAClC,MAAM,OAAO,aAAa,QAAQ;AAAA,UAClC,WAAW,OAAO;AAAA,UAClB,UAAU,OAAO;AAAA,UACjB,MAAM;AAAA,QAAA,CACP;AAED,eAAO,KAAK,KAAK;AAAA,MACnB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,aAAa,QAA6B,OAAgC;AACtF,UAAM,aAAa,MAAM,OAAO;AAChC,QAAI,CAAC,YAAY;AACf,YAAM,IAAI,MAAM,yBAAyB;AAAA,IAC3C;AAGA,SAAK,UAAU,IAAI,kBAAkB,YAAY,KAAK,MAAM,IAAI;AAAA,MAC9D,OAAO,WAAW;AAAA,MAClB,OAAO,WAAW;AAAA,MAClB,QAAQ,WAAW;AAAA,MACnB,aAAa,WAAW;AAAA,MACxB,sBAAsB;AAAA,MACtB,QAAQ;AAAA,IAAA,CACT;AAGD,UAAM,cAAc,IAAI,eAAkC;AAAA,MACxD,OAAO,CAAC,eAAe;AACrB,mBAAW,SAAS,QAAQ;AAC1B,qBAAW,QAAQ,KAAK;AAAA,QAC1B;AACA,mBAAW,MAAA;AAAA,MACb;AAAA,IAAA,CACD;AAGD,UAAM,cAAc,YAAY,YAAY,KAAK,QAAQ,cAAc;AAGvE,UAAM,SAAS,YAAY,UAAA;AAC3B,QAAI;AACF,aAAO,MAAM;AACX,cAAM,EAAE,MAAM,MAAA,IAAU,MAAM,OAAO,KAAA;AACrC,YAAI,KAAM;AACV,YAAI,OAAO;AACT,eAAK,cAAc,KAAK,KAAK;AAAA,QAC/B;AAAA,MACF;AAAA,IACF,UAAA;AACE,aAAO,YAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAc,mBAAmB,SAAiB,OAA8B;AAG9E,UAAM,gBAAgB,KAAK,cAAc,OAAO,CAAC,MAAM;AAErD,aAAO,EAAE,aAAa,WAAW,EAAE,YAAY;AAAA,IACjD,CAAC;AAED,eAAW,SAAS,eAAe;AACjC,UAAI;AAEF,cAAM,oBAAoB,MAAM;AAChC,cAAM,mBAAmB,MAAM,YAAY,KAAK,MAAM,MAAY,KAAK,GAAG;AAG1E,cAAM,kBAAkB,KAAK,gBAAgB,oBAAoB,KAAK;AAItE,aAAK,aAAa;AAAA,UAChB;AAAA,UACA,KAAK;AAAA,UACL;AAAA,UACA,KAAK,iBAAiB;AAAA,UACtB;AAAA,QAAA;AAAA,MAKJ,SAAS,OAAO;AACd,gBAAQ,MAAM,uCAAuC,KAAK;AAC1D,cAAM,MAAA;AAAA,MACR;AAAA,IACF;AAGA,eAAW,SAAS,KAAK,eAAe;AACtC,UAAI,CAAC,cAAc,SAAS,KAAK,GAAG;AAClC,cAAM,MAAA;AAAA,MACR;AAAA,IACF;AACA,SAAK,gBAAgB,CAAA;AAAA,EACvB;AAAA,EAEA,MAAM,UAAyB;AAC7B,QAAI,KAAK,WAAY;AAGrB,QAAI,KAAK,WAAW,KAAK,QAAQ,UAAU,UAAU;AACnD,YAAM,KAAK,QAAQ,MAAA;AACnB,WAAK,UAAU;AAAA,IACjB;AAGA,eAAW,SAAS,KAAK,eAAe;AACtC,YAAM,MAAA;AAAA,IACR;AACA,SAAK,gBAAgB,CAAA;AAErB,SAAK,aAAa;AAAA,EACpB;AACF;"}
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 } from '../model/types';\nimport type { CompositionModel, Clip } from '../model';\nimport { VideoChunkDecoder } from '../stages/decode/VideoChunkDecoder';\nimport { binarySearchOverlapping } from '../utils/binary-search';\nimport type { ResourceLoader } from '../stages/load/ResourceLoader';\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 console.warn('[OnDemandVideoSession] First chunk is not a keyframe, skipping');\n return;\n }\n\n try {\n // Create temporary decoder\n const decoder = new VideoChunkDecoder(`first-frame-${resourceId}`, {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n hardwareAcceleration: 'no-preference' as HardwareAcceleration,\n thread: 'main',\n });\n\n try {\n // Create chunk stream with ALL chunks from first GOP\n const chunkStream = new ReadableStream<EncodedVideoChunk>({\n start(controller) {\n for (const chunk of chunks) {\n controller.enqueue(chunk);\n }\n controller.close();\n },\n });\n\n // Decode all frames\n const frameStream = chunkStream.pipeThrough(decoder.createStream());\n const reader = frameStream.getReader();\n\n try {\n const frameDuration = Math.round(1_000_000 / fps);\n\n // Read and cache all decoded frames\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n if (value) {\n // Calculate global time for this frame\n const frameGlobalTime = clip.startUs + value.timestamp;\n\n cacheManager.addFrame(\n value,\n clip.id,\n frameDuration,\n clip.trackId ?? 'main',\n frameGlobalTime\n );\n }\n }\n } finally {\n reader.releaseLock();\n }\n } finally {\n await decoder.close();\n }\n } catch (error) {\n console.warn('[OnDemandVideoSession] Failed to decode first GOP:', 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: VideoChunkDecoder | 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 // Check if resource is an image (no MP4 index)\n const resource = this.compositionModel.getResource(this.resourceId);\n if (resource?.type === 'image') {\n // Image clip: handled by direct ImageBitmap access in PlaybackController\n // No need to decode anything here\n const image = await this.resourceLoader.loadImage(resource);\n if (image) {\n const frame = new VideoFrame(image, {\n timestamp: startUs,\n duration: endUs - startUs,\n });\n this.cacheManager.addFrame(\n frame,\n this.clipId,\n endUs - startUs,\n this.compositionModel.mainTrackId,\n this.globalTimeUs\n );\n }\n return;\n }\n\n // Video clip: decode from OPFS\n // Get MP4 index\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 (with binary search optimization)\n const gopWindow = this.calculateGOPRangesForWindow(index, startUs, endUs);\n if (gopWindow.gops.length === 0) {\n console.warn('[OnDemandVideoSession] No GOP ranges for window', startUs, endUs);\n return;\n }\n\n // Read GOP data from OPFS (merged byte range)\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 (direct sample index slicing)\n const chunks = await this.demuxGOPData(gopData, index, gopWindow);\n\n if (this.aborted) return;\n\n // Decode chunks to frames\n await this.decodeChunks(chunks, index);\n\n if (this.aborted) {\n for (const frame of this.decodedFrames) {\n frame.close();\n }\n this.decodedFrames = [];\n return;\n }\n\n // Write frames to L1\n await this.cacheDecodedFrames(startUs, endUs);\n }\n\n private calculateGOPRangesForWindow(\n index: MP4Index,\n startUs: TimeUs,\n endUs: TimeUs\n ): GOPWindowResult {\n if (!index.tracks.video) {\n console.warn('[OnDemandVideoSession] No video track in index');\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 console.warn('[OnDemandVideoSession] No GOP ranges for window', startUs, endUs);\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 // Create decoder (reuse VideoChunkDecoder like decodeFromL2)\n this.decoder = new VideoChunkDecoder(`ondemand-${this.clipId}`, {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n hardwareAcceleration: 'no-preference' as HardwareAcceleration,\n thread: 'main',\n });\n\n // Create stream from chunks\n const chunkStream = new ReadableStream<EncodedVideoChunk>({\n start: (controller) => {\n for (const chunk of chunks) {\n if (this.aborted) {\n controller.close();\n return;\n }\n controller.enqueue(chunk);\n }\n controller.close();\n },\n });\n\n // Pipe through decoder\n const frameStream = chunkStream.pipeThrough(this.decoder.createStream());\n\n // Collect decoded frames\n const reader = frameStream.getReader();\n try {\n while (true) {\n if (this.aborted) break;\n\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n this.decodedFrames.push(value);\n }\n }\n } catch (error) {\n if (this.aborted) return;\n throw error;\n } finally {\n reader.releaseLock();\n }\n }\n\n private async cacheDecodedFrames(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n // Filter frames within window (include boundary frames)\n // Use original timestamp (no quantization)\n const framesToCache = this.decodedFrames.filter((f) => {\n // Exclude frames at window end boundary\n return f.timestamp >= startUs && f.timestamp < endUs;\n });\n\n for (const frame of framesToCache) {\n try {\n // Use original timestamp and duration (preserve source frame rate)\n const originalTimestamp = frame.timestamp;\n const originalDuration = frame.duration ?? Math.round(1_000_000 / this.fps);\n\n // Calculate global timestamp using original value\n const frameGlobalTime = this.globalTimeUs + (originalTimestamp - this.targetTimeUs);\n\n // Write RAW video frame to L1 with original timestamp, duration and global time\n // NOTE: addComposedFrame name is legacy, but now we store raw frames\n this.cacheManager.addFrame(\n frame,\n this.clipId,\n originalDuration,\n this.compositionModel.mainTrackId,\n frameGlobalTime\n );\n\n // Don't close frame here - it's now owned by L1 cache\n // (CacheManager.addComposedFrame clones it or takes ownership)\n } catch (error) {\n console.error('[OnDemandVideoSession] Cache error:', error);\n frame.close();\n }\n }\n\n // Clean up decoded frames that weren't cached\n for (const frame of this.decodedFrames) {\n if (!framesToCache.includes(frame)) {\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 // Extract keyframe chunk\n const chunk = new EncodedVideoChunk({\n type: 'key',\n timestamp: keyframeSample.timestamp,\n duration: keyframeSample.duration,\n data: keyframeData,\n });\n\n if (this.aborted) return null;\n\n // Decode single keyframe\n await this.decodeChunks([chunk], index);\n\n if (this.aborted || this.decodedFrames.length === 0) return null;\n\n // Cache the keyframe\n const frame = this.decodedFrames[0];\n if (!frame) return null;\n\n const frameDuration = frame.duration ?? Math.round(1_000_000 / this.fps);\n const frameGlobalTime = this.globalTimeUs + (frame.timestamp - this.targetTimeUs);\n\n this.cacheManager.addFrame(\n frame,\n this.clipId,\n frameDuration,\n this.compositionModel.mainTrackId,\n frameGlobalTime\n );\n\n this.decodedFrames = [];\n\n return keyframeSample.timestamp;\n }\n\n async dispose(): Promise<void> {\n if (this.isDisposed) return;\n\n this.aborted = true;\n\n if (this.decoder) {\n if (this.decoder.state === 'configured') {\n await this.decoder.reset();\n }\n\n if (this.decoder.state !== 'closed') {\n await this.decoder.close();\n }\n this.decoder = null;\n }\n\n for (const frame of this.decodedFrames) {\n frame.close();\n }\n this.decodedFrames = [];\n\n this.isDisposed = true;\n }\n}\n"],"names":[],"mappings":"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"}
@@ -24,6 +24,7 @@ export declare class Orchestrator implements IOrchestrator {
24
24
  private isInitialized;
25
25
  private config;
26
26
  private ensureCacheDebounceTimer;
27
+ private activeOnDemandSession;
27
28
  readonly events: Pick<EventBus<EventPayloadMap>, 'on' | 'off' | 'once'>;
28
29
  constructor(config: OrchestratorConfig);
29
30
  private setupResourceFirstFrameHandler;
@@ -32,6 +33,12 @@ export declare class Orchestrator implements IOrchestrator {
32
33
  on<K extends keyof EventPayloadMap>(event: K, handler: (payload: EventPayloadMap[K]) => void): void;
33
34
  off<K extends keyof EventPayloadMap>(event: K, handler: (payload: EventPayloadMap[K]) => void): void;
34
35
  once<K extends keyof EventPayloadMap>(event: K, handler: (payload: EventPayloadMap[K]) => void): void;
36
+ cancelActiveDecoding(): void;
37
+ /**
38
+ * Decode and render nearest keyframe for fast seek preview
39
+ * Returns the keyframe timestamp if successfully decoded, null otherwise
40
+ */
41
+ tryRenderKeyframe(timeUs: TimeUs): Promise<TimeUs | null>;
35
42
  setCompositionModel(model: CompositionModel): Promise<void>;
36
43
  applyPatch(patch: CompositionPatch): Promise<void>;
37
44
  private handleResourceStateChange;
@@ -51,6 +58,11 @@ export declare class Orchestrator implements IOrchestrator {
51
58
  }): Promise<boolean>;
52
59
  dispose(): Promise<void>;
53
60
  private buildWorkerConfigs;
61
+ /**
62
+ * Calculate default video bitrate based on resolution
63
+ * Optimized for social media platforms (YouTube, TikTok, WeChat, etc.)
64
+ */
65
+ private calculateDefaultBitrate;
54
66
  export(model: CompositionModel, options: ExportOptions): Promise<Blob>;
55
67
  /**
56
68
  * Preheat a specific clip's window range
@@ -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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"}