@meframe/core 0.0.32 → 0.0.34

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 (47) hide show
  1. package/dist/controllers/PlaybackController.d.ts +1 -1
  2. package/dist/controllers/PlaybackController.d.ts.map +1 -1
  3. package/dist/controllers/PlaybackController.js +22 -10
  4. package/dist/controllers/PlaybackController.js.map +1 -1
  5. package/dist/event/events.d.ts +1 -1
  6. package/dist/event/events.d.ts.map +1 -1
  7. package/dist/event/events.js.map +1 -1
  8. package/dist/model/CompositionModel.d.ts +8 -0
  9. package/dist/model/CompositionModel.d.ts.map +1 -1
  10. package/dist/model/CompositionModel.js +18 -0
  11. package/dist/model/CompositionModel.js.map +1 -1
  12. package/dist/orchestrator/ExportScheduler.d.ts +4 -0
  13. package/dist/orchestrator/ExportScheduler.d.ts.map +1 -1
  14. package/dist/orchestrator/ExportScheduler.js +38 -34
  15. package/dist/orchestrator/ExportScheduler.js.map +1 -1
  16. package/dist/orchestrator/GlobalAudioSession.js +2 -2
  17. package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
  18. package/dist/orchestrator/OnDemandVideoSession.d.ts.map +1 -1
  19. package/dist/orchestrator/OnDemandVideoSession.js +25 -14
  20. package/dist/orchestrator/OnDemandVideoSession.js.map +1 -1
  21. package/dist/orchestrator/Orchestrator.d.ts +10 -2
  22. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  23. package/dist/orchestrator/Orchestrator.js +41 -20
  24. package/dist/orchestrator/Orchestrator.js.map +1 -1
  25. package/dist/stages/demux/MP4Demuxer.d.ts +5 -0
  26. package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
  27. package/dist/stages/demux/MP4Demuxer.js +38 -2
  28. package/dist/stages/demux/MP4Demuxer.js.map +1 -1
  29. package/dist/stages/demux/MP4IndexParser.d.ts.map +1 -1
  30. package/dist/stages/demux/MP4IndexParser.js +19 -5
  31. package/dist/stages/demux/MP4IndexParser.js.map +1 -1
  32. package/dist/stages/load/ResourceLoader.d.ts +34 -21
  33. package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
  34. package/dist/stages/load/ResourceLoader.js +197 -80
  35. package/dist/stages/load/ResourceLoader.js.map +1 -1
  36. package/dist/stages/load/TaskManager.d.ts +4 -0
  37. package/dist/stages/load/TaskManager.d.ts.map +1 -1
  38. package/dist/stages/load/TaskManager.js +11 -0
  39. package/dist/stages/load/TaskManager.js.map +1 -1
  40. package/dist/workers/{MP4Demuxer.DxMpB08B.js → MP4Demuxer.DfWiwyjB.js} +37 -2
  41. package/dist/workers/{MP4Demuxer.DxMpB08B.js.map → MP4Demuxer.DfWiwyjB.js.map} +1 -1
  42. package/dist/workers/stages/demux/{audio-demux.worker.Fd8sRTYi.js → audio-demux.worker.DgvvQVXU.js} +2 -2
  43. package/dist/workers/stages/demux/{audio-demux.worker.Fd8sRTYi.js.map → audio-demux.worker.DgvvQVXU.js.map} +1 -1
  44. package/dist/workers/stages/demux/{video-demux.worker.DqFOe12v.js → video-demux.worker.DhG3CRix.js} +2 -2
  45. package/dist/workers/stages/demux/{video-demux.worker.DqFOe12v.js.map → video-demux.worker.DhG3CRix.js.map} +1 -1
  46. package/dist/workers/worker-manifest.json +2 -2
  47. package/package.json +1 -1
@@ -32,22 +32,7 @@ class ExportScheduler {
32
32
  durationUs: model.durationUs
33
33
  });
34
34
  try {
35
- eventBus.emit(MeframeEvent.ExportProgress, {
36
- progress: 0,
37
- stage: "preparing",
38
- message: "Loading and parsing resources..."
39
- });
40
- const allResourceIds = /* @__PURE__ */ new Set();
41
- for (const track of model.tracks) {
42
- for (const clip of track.clips) {
43
- if (hasResourceId(clip)) {
44
- allResourceIds.add(clip.resourceId);
45
- }
46
- }
47
- }
48
- await Promise.all(
49
- Array.from(allResourceIds).map((rid) => resourceLoader.fetch(rid, { priority: "high" }))
50
- );
35
+ await this.preloadResources(model, resourceLoader, eventBus, checkStatus);
51
36
  muxManager.start({
52
37
  width,
53
38
  height,
@@ -85,13 +70,43 @@ class ExportScheduler {
85
70
  throw error;
86
71
  }
87
72
  }
73
+ /**
74
+ * Preload all resources (0-40% progress)
75
+ */
76
+ async preloadResources(model, resourceLoader, eventBus, checkStatus) {
77
+ eventBus.emit(MeframeEvent.ExportProgress, {
78
+ progress: 0,
79
+ stage: "preparing",
80
+ message: "Loading and parsing resources..."
81
+ });
82
+ const resourcesToLoad = [];
83
+ for (const track of model.tracks) {
84
+ if (!["video", "audio"].includes(track.kind)) continue;
85
+ for (const clip of track.clips) {
86
+ if (hasResourceId(clip)) {
87
+ resourcesToLoad.push(clip.resourceId);
88
+ }
89
+ }
90
+ }
91
+ for (let i = 0; i < resourcesToLoad.length; i++) {
92
+ await checkStatus();
93
+ const resourceId = resourcesToLoad[i];
94
+ await resourceLoader.load(resourceId, { priority: "high" });
95
+ const progress = (i + 1) / resourcesToLoad.length * 0.4;
96
+ eventBus.emit(MeframeEvent.ExportProgress, {
97
+ progress,
98
+ stage: "preparing",
99
+ message: `Loading resources... (${i + 1}/${resourcesToLoad.length})`
100
+ });
101
+ }
102
+ }
88
103
  /**
89
104
  * Process audio in 60-second windows
90
105
  * - Videos ≤60s: Single pass (zero boundaries)
91
106
  * - Videos >60s: 60s windows (minimal boundaries, ~23MB per window)
92
107
  */
93
108
  async processAudioInWindows(totalDurationUs, audioSession, muxManager, checkStatus) {
94
- const WINDOW_DURATION_US = 6e7;
109
+ const WINDOW_DURATION_US = 5 * 60 * 1e6;
95
110
  let currentUs = 0;
96
111
  while (currentUs < totalDurationUs) {
97
112
  await checkStatus();
@@ -108,16 +123,7 @@ class ExportScheduler {
108
123
  let accumulatedDurationUs = 0;
109
124
  for (let i = 0; i < clips.length; i++) {
110
125
  const clip = clips[i];
111
- const nextClip = clips[i + 1];
112
126
  await checkStatus();
113
- if (nextClip && hasResourceId(nextClip)) {
114
- this.deps.resourceLoader.fetch(nextClip.resourceId, {
115
- priority: "normal",
116
- clipId: nextClip.id,
117
- trackId: nextClip.trackId,
118
- isMainTrack: true
119
- });
120
- }
121
127
  const sessionId = `${clip.id}-export`;
122
128
  let streamFinishedResolver;
123
129
  let streamFinishedRejecter;
@@ -157,13 +163,11 @@ class ExportScheduler {
157
163
  data: buffer
158
164
  });
159
165
  muxManager.writeVideoChunk(remappedChunk, metadata);
160
- const progress = Math.min(
161
- 1,
162
- (accumulatedDurationUs + originalChunk.timestamp) / model.durationUs
163
- );
166
+ const encodingProgress = (accumulatedDurationUs + originalChunk.timestamp) / model.durationUs;
167
+ const totalProgress = 0.4 + encodingProgress * 0.6;
164
168
  this.deps.eventBus.emit(MeframeEvent.ExportProgress, {
165
- progress,
166
- stage: "muxing",
169
+ progress: Math.min(1, totalProgress),
170
+ stage: "encoding",
167
171
  timeUs: accumulatedDurationUs + originalChunk.timestamp
168
172
  });
169
173
  }
@@ -183,7 +187,7 @@ class ExportScheduler {
183
187
  },
184
188
  onPipelineReady: async (attachmentResourceIds) => {
185
189
  if (hasResourceId(clip)) {
186
- await this.deps.resourceLoader.fetch(clip.resourceId, {
190
+ await this.deps.resourceLoader.load(clip.resourceId, {
187
191
  priority: "high",
188
192
  sessionId,
189
193
  clipId: clip.id,
@@ -193,7 +197,7 @@ class ExportScheduler {
193
197
  }
194
198
  if (attachmentResourceIds) {
195
199
  for (const rid of attachmentResourceIds) {
196
- await this.deps.resourceLoader.fetch(rid, {
200
+ await this.deps.resourceLoader.load(rid, {
197
201
  priority: "normal",
198
202
  sessionId,
199
203
  clipId: clip.id,
@@ -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 (ensures OPFS + AudioSampleCache + MP4IndexCache)\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 0,\n stage: 'preparing',\n message: 'Loading and parsing resources...',\n });\n\n const allResourceIds = new Set<string>();\n for (const track of model.tracks) {\n for (const clip of track.clips) {\n if (hasResourceId(clip)) {\n allResourceIds.add(clip.resourceId);\n }\n }\n }\n\n // Parallel loading and parsing\n // fetch() now returns Promise that resolves after parsing completes\n await Promise.all(\n Array.from(allResourceIds).map((rid) => resourceLoader.fetch(rid, { priority: 'high' }))\n );\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 * 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 = 60_000_000; // 60 seconds\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 // --- Preload Next Clip ---\n // Start downloading next clip's resource while current clip is processing\n if (nextClip && hasResourceId(nextClip)) {\n // Fire and forget - ResourceLoader handles deduping\n this.deps.resourceLoader.fetch(nextClip.resourceId, {\n priority: 'normal',\n clipId: nextClip.id,\n trackId: nextClip.trackId,\n isMainTrack: true,\n });\n }\n // -------------------------\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\n const progress = Math.min(\n 1.0,\n (accumulatedDurationUs + originalChunk.timestamp) / model.durationUs\n );\n this.deps.eventBus.emit(MeframeEvent.ExportProgress, {\n progress,\n stage: 'muxing',\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.fetch(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.fetch(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 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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"}
@@ -54,7 +54,7 @@ class GlobalAudioSession {
54
54
  if (resource2?.state === "ready") {
55
55
  return;
56
56
  }
57
- await this.deps.resourceLoader.fetch(clip.resourceId, {
57
+ await this.deps.resourceLoader.load(clip.resourceId, {
58
58
  priority: "high",
59
59
  clipId: clip.id,
60
60
  trackId: clip.trackId
@@ -89,7 +89,7 @@ class GlobalAudioSession {
89
89
  this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });
90
90
  continue;
91
91
  }
92
- await this.deps.resourceLoader.fetch(clip.resourceId, {
92
+ await this.deps.resourceLoader.load(clip.resourceId, {
93
93
  priority: "high",
94
94
  clipId: clip.id,
95
95
  trackId: track.id
@@ -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.fetch(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.fetch(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.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 +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;IAwDhB,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,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"}
@@ -9,13 +9,18 @@ class OnDemandVideoSession {
9
9
  if (chunks.length === 0) return;
10
10
  const videoTrack = index.tracks.video;
11
11
  if (!videoTrack) return;
12
+ const firstChunk = chunks[0];
13
+ if (!firstChunk || firstChunk.type !== "key") {
14
+ console.warn("[OnDemandVideoSession] First chunk is not a keyframe, skipping");
15
+ return;
16
+ }
12
17
  try {
13
18
  const decoder = new VideoChunkDecoder(`first-frame-${resourceId}`, {
14
19
  codec: videoTrack.codec,
15
20
  width: videoTrack.width,
16
21
  height: videoTrack.height,
17
22
  description: videoTrack.description,
18
- hardwareAcceleration: "prefer-hardware",
23
+ hardwareAcceleration: "no-preference",
19
24
  thread: "main"
20
25
  });
21
26
  try {
@@ -29,24 +34,30 @@ class OnDemandVideoSession {
29
34
  });
30
35
  const frameStream = chunkStream.pipeThrough(decoder.createStream());
31
36
  const reader = frameStream.getReader();
32
- const { value } = await reader.read();
33
- reader.releaseLock();
34
- if (value) {
37
+ try {
35
38
  const frameDuration = Math.round(1e6 / fps);
36
- cacheManager.addFrame(
37
- value,
38
- clip.id,
39
- frameDuration,
40
- clip.trackId ?? "main",
41
- clip.startUs
42
- // globalTimeUs = clipStartUs (first frame, relativeTime=0)
43
- );
39
+ while (true) {
40
+ const { done, value } = await reader.read();
41
+ if (done) break;
42
+ if (value) {
43
+ const frameGlobalTime = clip.startUs + value.timestamp;
44
+ cacheManager.addFrame(
45
+ value,
46
+ clip.id,
47
+ frameDuration,
48
+ clip.trackId ?? "main",
49
+ frameGlobalTime
50
+ );
51
+ }
52
+ }
53
+ } finally {
54
+ reader.releaseLock();
44
55
  }
45
56
  } finally {
46
57
  await decoder.close();
47
58
  }
48
59
  } catch (error) {
49
- console.warn("[OnDemandVideoSession] Failed to decode first frame:", error);
60
+ console.warn("[OnDemandVideoSession] Failed to decode first GOP:", error);
50
61
  }
51
62
  }
52
63
  clipId;
@@ -209,7 +220,7 @@ class OnDemandVideoSession {
209
220
  width: videoTrack.width,
210
221
  height: videoTrack.height,
211
222
  description: videoTrack.description,
212
- hardwareAcceleration: "prefer-hardware",
223
+ hardwareAcceleration: "no-preference",
213
224
  thread: "main"
214
225
  });
215
226
  const chunkStream = new ReadableStream({
@@ -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 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: 'prefer-hardware',\n thread: 'main',\n });\n\n try {\n // Create chunk stream\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\n const frameStream = chunkStream.pipeThrough(decoder.createStream());\n\n // Read first frame only\n const reader = frameStream.getReader();\n const { value } = await reader.read();\n reader.releaseLock();\n\n if (value) {\n // Use CacheManager's unified interface to ensure event notification\n const frameDuration = Math.round(1_000_000 / fps);\n\n cacheManager.addFrame(\n value,\n clip.id,\n frameDuration,\n clip.trackId ?? 'main',\n clip.startUs // globalTimeUs = clipStartUs (first frame, relativeTime=0)\n );\n }\n } finally {\n await decoder.close();\n }\n } catch (error) {\n console.warn('[OnDemandVideoSession] Failed to decode first frame:', 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: 'prefer-hardware',\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":"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"}
1
+ {"version":3,"file":"OnDemandVideoSession.js","sources":["../../src/orchestrator/OnDemandVideoSession.ts"],"sourcesContent":["import type { CacheManager } from '../cache/CacheManager';\nimport type { MP4IndexCache } from '../cache/resource/MP4IndexCache';\nimport type { MP4Index, GOP } from '../stages/demux/types';\nimport type { TimeUs } 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":"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"}
@@ -24,7 +24,6 @@ export declare class Orchestrator implements IOrchestrator {
24
24
  private isInitialized;
25
25
  private config;
26
26
  private ensureCacheDebounceTimer;
27
- private currentClipId;
28
27
  readonly events: Pick<EventBus<EventPayloadMap>, 'on' | 'off' | 'once'>;
29
28
  constructor(config: OrchestratorConfig);
30
29
  private setupResourceFirstFrameHandler;
@@ -37,7 +36,6 @@ export declare class Orchestrator implements IOrchestrator {
37
36
  applyPatch(patch: CompositionPatch): Promise<void>;
38
37
  private handleResourceStateChange;
39
38
  getFrame(timeUs: TimeUs, options?: RenderFrameOptions): Promise<RcFrame | null>;
40
- private preheatNextClip;
41
39
  /**
42
40
  * Compose frame from OPFS resource (on-demand decoding)
43
41
  * This is the new path for long clips with window caching
@@ -54,6 +52,16 @@ export declare class Orchestrator implements IOrchestrator {
54
52
  dispose(): Promise<void>;
55
53
  private buildWorkerConfigs;
56
54
  export(model: CompositionModel, options: ExportOptions): Promise<Blob>;
55
+ /**
56
+ * Preheat a specific clip's window range
57
+ * Used by PlaybackController for cross-clip window preheating
58
+ *
59
+ * @param clipId - Clip identifier
60
+ * @param clipRelativeStart - Start time relative to clip (microseconds)
61
+ * @param clipRelativeEnd - End time relative to clip (microseconds)
62
+ * @param globalTimeUs - Global timeline position (for globalTimeUs in cache)
63
+ */
64
+ preheatClipWindow(clipId: string, clipRelativeStart: TimeUs, clipRelativeEnd: TimeUs, globalTimeUs: TimeUs): Promise<void>;
57
65
  /**
58
66
  * Get render state for real-time composition
59
67
  * Returns layers ready for VideoComposer
@@ -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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"}