@meframe/core 0.0.29 → 0.0.30-beta

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 (212) hide show
  1. package/dist/Meframe.d.ts +2 -13
  2. package/dist/Meframe.d.ts.map +1 -1
  3. package/dist/Meframe.js +6 -100
  4. package/dist/Meframe.js.map +1 -1
  5. package/dist/cache/CacheManager.d.ts +35 -19
  6. package/dist/cache/CacheManager.d.ts.map +1 -1
  7. package/dist/cache/CacheManager.js +223 -134
  8. package/dist/cache/CacheManager.js.map +1 -1
  9. package/dist/cache/l1/VideoL1Cache.d.ts +15 -2
  10. package/dist/cache/l1/VideoL1Cache.d.ts.map +1 -1
  11. package/dist/cache/l1/VideoL1Cache.js +58 -38
  12. package/dist/cache/l1/VideoL1Cache.js.map +1 -1
  13. package/dist/cache/l2/L2Cache.d.ts.map +1 -1
  14. package/dist/cache/l2/L2Cache.js +5 -5
  15. package/dist/cache/l2/L2Cache.js.map +1 -1
  16. package/dist/cache/l2/L2OPFSStore.d.ts +37 -0
  17. package/dist/cache/l2/L2OPFSStore.d.ts.map +1 -0
  18. package/dist/cache/l2/L2OPFSStore.js +89 -0
  19. package/dist/cache/l2/L2OPFSStore.js.map +1 -0
  20. package/dist/cache/resource/AudioSampleCache.d.ts +52 -0
  21. package/dist/cache/resource/AudioSampleCache.d.ts.map +1 -0
  22. package/dist/cache/resource/AudioSampleCache.js +69 -0
  23. package/dist/cache/resource/AudioSampleCache.js.map +1 -0
  24. package/dist/cache/resource/ImageBitmapCache.d.ts +65 -0
  25. package/dist/cache/resource/ImageBitmapCache.d.ts.map +1 -0
  26. package/dist/cache/resource/ImageBitmapCache.js +101 -0
  27. package/dist/cache/resource/ImageBitmapCache.js.map +1 -0
  28. package/dist/cache/resource/MP4IndexCache.d.ts +48 -0
  29. package/dist/cache/resource/MP4IndexCache.d.ts.map +1 -0
  30. package/dist/cache/resource/MP4IndexCache.js +104 -0
  31. package/dist/cache/resource/MP4IndexCache.js.map +1 -0
  32. package/dist/cache/resource/ResourceCache.d.ts +46 -0
  33. package/dist/cache/resource/ResourceCache.d.ts.map +1 -0
  34. package/dist/cache/resource/ResourceCache.js +92 -0
  35. package/dist/cache/resource/ResourceCache.js.map +1 -0
  36. package/dist/cache/storage/indexeddb/ChunkRecordStore.d.ts +75 -0
  37. package/dist/cache/storage/indexeddb/ChunkRecordStore.d.ts.map +1 -0
  38. package/dist/cache/{l2/IndexedDBStore.js → storage/indexeddb/ChunkRecordStore.js} +3 -3
  39. package/dist/cache/storage/indexeddb/ChunkRecordStore.js.map +1 -0
  40. package/dist/cache/storage/opfs/OPFSManager.d.ts +54 -0
  41. package/dist/cache/storage/opfs/OPFSManager.d.ts.map +1 -0
  42. package/dist/cache/storage/opfs/OPFSManager.js +133 -0
  43. package/dist/cache/storage/opfs/OPFSManager.js.map +1 -0
  44. package/dist/cache/storage/opfs/types.d.ts +16 -0
  45. package/dist/cache/storage/opfs/types.d.ts.map +1 -0
  46. package/dist/config/defaults.d.ts.map +1 -1
  47. package/dist/config/defaults.js +21 -2
  48. package/dist/config/defaults.js.map +1 -1
  49. package/dist/config/types.d.ts +28 -0
  50. package/dist/config/types.d.ts.map +1 -1
  51. package/dist/controllers/ExportController.d.ts +16 -0
  52. package/dist/controllers/ExportController.d.ts.map +1 -0
  53. package/dist/controllers/ExportController.js +44 -0
  54. package/dist/controllers/ExportController.js.map +1 -0
  55. package/dist/controllers/PlaybackController.d.ts +28 -4
  56. package/dist/controllers/PlaybackController.d.ts.map +1 -1
  57. package/dist/controllers/PlaybackController.js +116 -51
  58. package/dist/controllers/PlaybackController.js.map +1 -1
  59. package/dist/controllers/index.d.ts +2 -3
  60. package/dist/controllers/index.d.ts.map +1 -1
  61. package/dist/controllers/types.d.ts +0 -28
  62. package/dist/controllers/types.d.ts.map +1 -1
  63. package/dist/event/events.d.ts +8 -0
  64. package/dist/event/events.d.ts.map +1 -1
  65. package/dist/event/events.js +1 -0
  66. package/dist/event/events.js.map +1 -1
  67. package/dist/model/CompositionModel.d.ts.map +1 -1
  68. package/dist/model/CompositionModel.js +11 -6
  69. package/dist/model/CompositionModel.js.map +1 -1
  70. package/dist/model/RcFrame.d.ts +2 -0
  71. package/dist/model/RcFrame.d.ts.map +1 -1
  72. package/dist/model/RcFrame.js +3 -0
  73. package/dist/model/RcFrame.js.map +1 -1
  74. package/dist/orchestrator/ExportScheduler.d.ts +35 -0
  75. package/dist/orchestrator/ExportScheduler.d.ts.map +1 -0
  76. package/dist/orchestrator/ExportScheduler.js +241 -0
  77. package/dist/orchestrator/ExportScheduler.js.map +1 -0
  78. package/dist/orchestrator/GlobalAudioSession.d.ts +21 -7
  79. package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
  80. package/dist/orchestrator/GlobalAudioSession.js +132 -140
  81. package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
  82. package/dist/orchestrator/OnDemandVideoSession.d.ts +73 -0
  83. package/dist/orchestrator/OnDemandVideoSession.d.ts.map +1 -0
  84. package/dist/orchestrator/OnDemandVideoSession.js +281 -0
  85. package/dist/orchestrator/OnDemandVideoSession.js.map +1 -0
  86. package/dist/orchestrator/Orchestrator.d.ts +22 -17
  87. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  88. package/dist/orchestrator/Orchestrator.js +231 -297
  89. package/dist/orchestrator/Orchestrator.js.map +1 -1
  90. package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
  91. package/dist/orchestrator/VideoClipSession.js +3 -15
  92. package/dist/orchestrator/VideoClipSession.js.map +1 -1
  93. package/dist/orchestrator/index.d.ts +0 -1
  94. package/dist/orchestrator/index.d.ts.map +1 -1
  95. package/dist/orchestrator/types.d.ts +4 -4
  96. package/dist/orchestrator/types.d.ts.map +1 -1
  97. package/dist/stages/compose/FilterProcessor.d.ts +1 -1
  98. package/dist/stages/compose/FilterProcessor.d.ts.map +1 -1
  99. package/dist/stages/compose/FilterProcessor.js +226 -0
  100. package/dist/stages/compose/FilterProcessor.js.map +1 -0
  101. package/dist/stages/compose/LayerRenderer.d.ts +1 -1
  102. package/dist/stages/compose/LayerRenderer.d.ts.map +1 -1
  103. package/dist/stages/compose/LayerRenderer.js +270 -0
  104. package/dist/stages/compose/LayerRenderer.js.map +1 -0
  105. package/dist/stages/compose/TransitionProcessor.d.ts +1 -1
  106. package/dist/stages/compose/TransitionProcessor.d.ts.map +1 -1
  107. package/dist/stages/compose/TransitionProcessor.js +189 -0
  108. package/dist/stages/compose/TransitionProcessor.js.map +1 -0
  109. package/dist/stages/compose/VideoComposer.d.ts +4 -2
  110. package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
  111. package/dist/stages/compose/VideoComposer.js +229 -0
  112. package/dist/stages/compose/VideoComposer.js.map +1 -0
  113. package/dist/stages/compose/text-renderers/animation-utils.js +76 -0
  114. package/dist/stages/compose/text-renderers/animation-utils.js.map +1 -0
  115. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts +2 -2
  116. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts.map +1 -1
  117. package/dist/stages/compose/text-renderers/basic-text-renderer.js +93 -0
  118. package/dist/stages/compose/text-renderers/basic-text-renderer.js.map +1 -0
  119. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts +1 -1
  120. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts.map +1 -1
  121. package/dist/stages/compose/text-renderers/character-ktv-renderer.js +132 -0
  122. package/dist/stages/compose/text-renderers/character-ktv-renderer.js.map +1 -0
  123. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts +1 -1
  124. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts.map +1 -1
  125. package/dist/stages/compose/text-renderers/word-by-word-renderer.js +128 -0
  126. package/dist/stages/compose/text-renderers/word-by-word-renderer.js.map +1 -0
  127. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts +1 -1
  128. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts.map +1 -1
  129. package/dist/stages/compose/text-renderers/word-fancy-renderer.js +135 -0
  130. package/dist/stages/compose/text-renderers/word-fancy-renderer.js.map +1 -0
  131. package/dist/stages/compose/text-utils/locale-detector.js +16 -0
  132. package/dist/stages/compose/text-utils/locale-detector.js.map +1 -0
  133. package/dist/stages/compose/text-utils/text-metrics.js +21 -0
  134. package/dist/stages/compose/text-utils/text-metrics.js.map +1 -0
  135. package/dist/stages/compose/text-utils/text-wrapper.js +225 -0
  136. package/dist/stages/compose/text-utils/text-wrapper.js.map +1 -0
  137. package/dist/stages/compose/types.d.ts +2 -1
  138. package/dist/stages/compose/types.d.ts.map +1 -1
  139. package/dist/stages/decode/BaseDecoder.js +0 -3
  140. package/dist/stages/decode/BaseDecoder.js.map +1 -1
  141. package/dist/stages/demux/MP4Demuxer.d.ts +5 -0
  142. package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
  143. package/dist/stages/demux/MP4Demuxer.js +281 -0
  144. package/dist/stages/demux/MP4Demuxer.js.map +1 -0
  145. package/dist/stages/demux/MP4IndexParser.d.ts +71 -0
  146. package/dist/stages/demux/MP4IndexParser.d.ts.map +1 -0
  147. package/dist/stages/demux/MP4IndexParser.js +416 -0
  148. package/dist/stages/demux/MP4IndexParser.js.map +1 -0
  149. package/dist/stages/demux/types.d.ts +48 -0
  150. package/dist/stages/demux/types.d.ts.map +1 -1
  151. package/dist/stages/load/ResourceLoader.d.ts +44 -2
  152. package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
  153. package/dist/stages/load/ResourceLoader.js +281 -37
  154. package/dist/stages/load/ResourceLoader.js.map +1 -1
  155. package/dist/stages/load/TaskManager.d.ts +6 -2
  156. package/dist/stages/load/TaskManager.d.ts.map +1 -1
  157. package/dist/stages/load/TaskManager.js +27 -4
  158. package/dist/stages/load/TaskManager.js.map +1 -1
  159. package/dist/stages/load/types.d.ts +7 -0
  160. package/dist/stages/load/types.d.ts.map +1 -1
  161. package/dist/stages/mux/MP4Muxer.d.ts +2 -2
  162. package/dist/stages/mux/MP4Muxer.d.ts.map +1 -1
  163. package/dist/stages/mux/MP4Muxer.js +24 -13
  164. package/dist/stages/mux/MP4Muxer.js.map +1 -1
  165. package/dist/stages/mux/MuxManager.d.ts +10 -21
  166. package/dist/stages/mux/MuxManager.d.ts.map +1 -1
  167. package/dist/stages/mux/MuxManager.js +21 -162
  168. package/dist/stages/mux/MuxManager.js.map +1 -1
  169. package/dist/stages/mux/index.d.ts +0 -1
  170. package/dist/stages/mux/index.d.ts.map +1 -1
  171. package/dist/utils/binary-search.d.ts +12 -4
  172. package/dist/utils/binary-search.d.ts.map +1 -1
  173. package/dist/utils/binary-search.js +52 -6
  174. package/dist/utils/binary-search.js.map +1 -1
  175. package/dist/workers/{BaseDecoder.BWYu1W0B.js → BaseDecoder.CTW-vr29.js} +1 -4
  176. package/dist/workers/BaseDecoder.CTW-vr29.js.map +1 -0
  177. package/dist/workers/{MP4Demuxer.lMOUMWFh.js → MP4Demuxer.BEa6PLJm.js} +9 -2
  178. package/dist/workers/{MP4Demuxer.lMOUMWFh.js.map → MP4Demuxer.BEa6PLJm.js.map} +1 -1
  179. package/dist/workers/stages/compose/{video-compose.worker.CIeEIJO7.js → video-compose.worker.DHQ8B105.js} +59 -31
  180. package/dist/workers/stages/compose/video-compose.worker.DHQ8B105.js.map +1 -0
  181. package/dist/workers/stages/decode/{audio-decode.worker.DnS17GD9.js → audio-decode.worker.CP8bXXa4.js} +2 -2
  182. package/dist/workers/stages/decode/{audio-decode.worker.DnS17GD9.js.map → audio-decode.worker.CP8bXXa4.js.map} +1 -1
  183. package/dist/workers/stages/decode/{video-decode.worker.BEYsjOXp.js → video-decode.worker.BIspTxgV.js} +2 -2
  184. package/dist/workers/stages/decode/{video-decode.worker.BEYsjOXp.js.map → video-decode.worker.BIspTxgV.js.map} +1 -1
  185. package/dist/workers/stages/demux/{audio-demux.worker.DcurGC8i.js → audio-demux.worker._VRQdLdv.js} +2 -2
  186. package/dist/workers/stages/demux/{audio-demux.worker.DcurGC8i.js.map → audio-demux.worker._VRQdLdv.js.map} +1 -1
  187. package/dist/workers/stages/demux/{video-demux.worker.B1_wntU4.js → video-demux.worker.CSkxGtmx.js} +3 -19
  188. package/dist/workers/stages/demux/video-demux.worker.CSkxGtmx.js.map +1 -0
  189. package/dist/workers/worker-manifest.json +5 -5
  190. package/package.json +1 -1
  191. package/dist/cache/l2/IndexedDBStore.js.map +0 -1
  192. package/dist/cache/l2/OPFSStore.js +0 -131
  193. package/dist/cache/l2/OPFSStore.js.map +0 -1
  194. package/dist/controllers/PreRenderService.d.ts +0 -59
  195. package/dist/controllers/PreRenderService.d.ts.map +0 -1
  196. package/dist/controllers/PreRenderService.js +0 -185
  197. package/dist/controllers/PreRenderService.js.map +0 -1
  198. package/dist/controllers/PreRenderTaskQueue.d.ts +0 -21
  199. package/dist/controllers/PreRenderTaskQueue.d.ts.map +0 -1
  200. package/dist/orchestrator/ClipSessionManager.d.ts +0 -70
  201. package/dist/orchestrator/ClipSessionManager.d.ts.map +0 -1
  202. package/dist/orchestrator/ClipSessionManager.js +0 -158
  203. package/dist/orchestrator/ClipSessionManager.js.map +0 -1
  204. package/dist/stages/decode/AudioChunkDecoder.js +0 -169
  205. package/dist/stages/decode/AudioChunkDecoder.js.map +0 -1
  206. package/dist/stages/mux/OPFSWriter.d.ts +0 -46
  207. package/dist/stages/mux/OPFSWriter.d.ts.map +0 -1
  208. package/dist/utils/BackpressureAdapter.d.ts +0 -26
  209. package/dist/utils/BackpressureAdapter.d.ts.map +0 -1
  210. package/dist/workers/BaseDecoder.BWYu1W0B.js.map +0 -1
  211. package/dist/workers/stages/compose/video-compose.worker.CIeEIJO7.js.map +0 -1
  212. package/dist/workers/stages/demux/video-demux.worker.B1_wntU4.js.map +0 -1
@@ -1,8 +1,7 @@
1
1
  import { OfflineAudioMixer } from "../stages/compose/OfflineAudioMixer.js";
2
2
  import { MeframeEvent } from "../event/events.js";
3
3
  import { AudioChunkEncoder } from "../stages/encode/AudioChunkEncoder.js";
4
- import { AudioChunkDecoder } from "../stages/decode/AudioChunkDecoder.js";
5
- import { isAudioClip, hasAudioConfig } from "../model/types.js";
4
+ import { hasResourceId, isAudioClip, hasAudioConfig } from "../model/types.js";
6
5
  class GlobalAudioSession {
7
6
  mixer;
8
7
  activeClips = /* @__PURE__ */ new Set();
@@ -15,7 +14,8 @@ class GlobalAudioSession {
15
14
  playbackRate = 1;
16
15
  isPlaying = false;
17
16
  currentPlaybackTimeUs = 0;
18
- ensuringFromL2 = /* @__PURE__ */ new Set();
17
+ ensuringClips = /* @__PURE__ */ new Set();
18
+ // Track all decoding attempts (Resource & L2)
19
19
  constructor(deps) {
20
20
  this.deps = deps;
21
21
  this.mixer = new OfflineAudioMixer(deps.cacheManager, deps.getModel);
@@ -24,6 +24,31 @@ class GlobalAudioSession {
24
24
  const { sessionId, audioData, clipDurationUs } = message;
25
25
  this.deps.cacheManager.putClipAudioData(sessionId, audioData, clipDurationUs);
26
26
  }
27
+ async ensureAudioForTime(timeUs) {
28
+ const model = this.deps.getModel();
29
+ if (!model) return;
30
+ const activeClips = [];
31
+ for (const track of model.tracks) {
32
+ if (track.kind !== "audio" && track.kind !== "video") continue;
33
+ const clips = model.getClipsAtTime(timeUs, track.id);
34
+ activeClips.push(...clips);
35
+ }
36
+ await Promise.all(
37
+ activeClips.map(async (clip) => {
38
+ if (this.deps.cacheManager.hasClipPCM(clip.id)) return;
39
+ if (!hasResourceId(clip)) return;
40
+ const resource = model.getResource(clip.resourceId);
41
+ if (!resource) return;
42
+ if (this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {
43
+ await this.ensureClipAudio(clip.id);
44
+ return;
45
+ }
46
+ if (this.activeClips.has(clip.id)) {
47
+ await this.waitForClipPCM(clip.id, 2e3);
48
+ }
49
+ })
50
+ );
51
+ }
27
52
  async activateAllAudioClips() {
28
53
  const model = this.deps.getModel();
29
54
  if (!model) {
@@ -36,11 +61,17 @@ class GlobalAudioSession {
36
61
  if (!isAudioClip(clip)) {
37
62
  throw new Error(`Clip ${clip.id} in audio track is not an audio clip`);
38
63
  }
64
+ if (this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {
65
+ await this.ensureClipAudio(clip.id);
66
+ this.activeClips.add(clip.id);
67
+ continue;
68
+ }
39
69
  await this.setupAudioPipeline(clip);
40
70
  this.activeClips.add(clip.id);
41
71
  await this.deps.resourceLoader.fetch(clip.resourceId, {
42
72
  priority: "high",
43
73
  sessionId: clip.id,
74
+ clipId: clip.id,
44
75
  trackId: track.id
45
76
  });
46
77
  this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });
@@ -96,13 +127,13 @@ class GlobalAudioSession {
96
127
  try {
97
128
  source.stop();
98
129
  source.disconnect();
99
- } catch (error) {
130
+ } catch {
100
131
  }
101
132
  }
102
133
  for (const gainNode of gainNodesToDisconnect) {
103
134
  try {
104
135
  gainNode.disconnect();
105
- } catch (error) {
136
+ } catch {
106
137
  }
107
138
  }
108
139
  }
@@ -138,6 +169,7 @@ class GlobalAudioSession {
138
169
  if (audioContext.state === "suspended") {
139
170
  await audioContext.resume();
140
171
  }
172
+ await this.ensureAudioForTime(timeUs);
141
173
  this.isPlaying = true;
142
174
  this.startAllActiveClips(timeUs);
143
175
  }
@@ -180,32 +212,47 @@ class GlobalAudioSession {
180
212
  this.deps.cacheManager.resetAudioCache();
181
213
  this.activeClips.clear();
182
214
  this.streamEndedClips.clear();
183
- this.ensuringFromL2.clear();
184
215
  }
185
216
  /**
186
- * Create export encoded audio stream
217
+ * Mix and encode audio for a specific segment (used by ExportScheduler)
187
218
  */
188
- async createExportEncodedStream(config, onFirstMetadata) {
189
- const audioDataStream = await this.createExportAudioStream();
190
- if (!audioDataStream) {
191
- return null;
192
- }
193
- const encoder = new AudioChunkEncoder(config);
194
- await encoder.initialize();
195
- const encodingTransform = encoder.createStream();
196
- const encodedStream = audioDataStream.pipeThrough(encodingTransform);
197
- let firstMetadataExtracted = false;
198
- return encodedStream.pipeThrough(
199
- new TransformStream({
200
- transform(encoderChunk, controller) {
201
- if (!firstMetadataExtracted && onFirstMetadata) {
202
- onFirstMetadata(encoderChunk.metadata);
203
- firstMetadataExtracted = true;
204
- }
205
- controller.enqueue(encoderChunk.chunk);
219
+ async mixAndEncodeSegment(startUs, endUs, onChunk) {
220
+ const mixedBuffer = await this.mixer.mix(startUs, endUs);
221
+ const audioData = this.audioBufferToAudioData(mixedBuffer, startUs);
222
+ if (!audioData) return;
223
+ if (!this.exportEncoder) {
224
+ this.exportEncoder = new AudioChunkEncoder();
225
+ await this.exportEncoder.initialize();
226
+ this.exportEncoderStream = this.exportEncoder.createStream();
227
+ this.exportEncoderWriter = this.exportEncoderStream.writable.getWriter();
228
+ this.startExportEncoderReader(this.exportEncoderStream.readable, onChunk);
229
+ }
230
+ await this.exportEncoderWriter?.write(audioData);
231
+ }
232
+ exportEncoder = null;
233
+ exportEncoderStream = null;
234
+ exportEncoderWriter = null;
235
+ async startExportEncoderReader(stream, onChunk) {
236
+ const reader = stream.getReader();
237
+ try {
238
+ while (true) {
239
+ const { done, value } = await reader.read();
240
+ if (done) break;
241
+ if (value) {
242
+ onChunk(value.chunk, value.metadata);
206
243
  }
207
- })
208
- );
244
+ }
245
+ } catch (e) {
246
+ console.error("Export encoder reader error", e);
247
+ }
248
+ }
249
+ async finalizeExportAudio() {
250
+ if (this.exportEncoderWriter) {
251
+ await this.exportEncoderWriter.close();
252
+ this.exportEncoderWriter = null;
253
+ }
254
+ this.exportEncoder = null;
255
+ this.exportEncoderStream = null;
209
256
  }
210
257
  /**
211
258
  * Create export audio stream
@@ -332,7 +379,7 @@ class GlobalAudioSession {
332
379
  // Full clip duration
333
380
  );
334
381
  if (!clipPCMData || clipPCMData.planes.length === 0) {
335
- console.warn("[GlobalAudioSession] No PCM data for clip, will retry later", clip.id);
382
+ void this.ensureClipAudio(clip.id);
336
383
  return;
337
384
  }
338
385
  const buffer = this.pcmToAudioBuffer(clipPCMData.planes, clipPCMData.sampleRate);
@@ -347,7 +394,6 @@ class GlobalAudioSession {
347
394
  source.buffer = buffer;
348
395
  source.playbackRate.value = this.playbackRate;
349
396
  source._meframeClipId = clip.id;
350
- source._playedToUs = clip.startUs + buffer.duration * 1e6;
351
397
  const gainNode = this.audioContext.createGain();
352
398
  if (hasAudioConfig(clip)) {
353
399
  const volume = clip.audioConfig?.volume ?? 1;
@@ -365,75 +411,6 @@ class GlobalAudioSession {
365
411
  this.audioSources.splice(index, 1);
366
412
  this.audioGainNodes.splice(index, 1);
367
413
  }
368
- const playedToUs = source._playedToUs;
369
- const clipEndUs = clip.startUs + clip.durationUs;
370
- if (playedToUs < clipEndUs && this.isPlaying) {
371
- setTimeout(() => {
372
- if (this.isPlaying) {
373
- this.continueClipPlayback(clip, playedToUs);
374
- }
375
- }, 50);
376
- }
377
- };
378
- this.audioSources.push(source);
379
- this.audioGainNodes.push(gainNode);
380
- }
381
- continueClipPlayback(clip, fromUs) {
382
- if (!this.audioContext) {
383
- return;
384
- }
385
- const clipPCMData = this.deps.cacheManager.getClipPCMWithMetadata(
386
- clip.id,
387
- 0,
388
- // 0-based start
389
- clip.durationUs
390
- // clip duration
391
- );
392
- if (!clipPCMData || clipPCMData.planes.length === 0) {
393
- return;
394
- }
395
- const buffer = this.pcmToAudioBuffer(clipPCMData.planes, clipPCMData.sampleRate);
396
- const bufferEndUs = clip.startUs + buffer.duration * 1e6;
397
- if (bufferEndUs <= fromUs + 1e5) {
398
- return;
399
- }
400
- const source = this.audioContext.createBufferSource();
401
- source.buffer = buffer;
402
- source.playbackRate.value = this.playbackRate;
403
- source._meframeClipId = clip.id;
404
- source._playedToUs = bufferEndUs;
405
- const gainNode = this.audioContext.createGain();
406
- if (hasAudioConfig(clip)) {
407
- const volume = clip.audioConfig?.volume ?? 1;
408
- const muted = clip.audioConfig?.muted ?? false;
409
- gainNode.gain.value = muted ? 0 : volume * this.volume;
410
- } else {
411
- gainNode.gain.value = this.volume;
412
- }
413
- source.connect(gainNode);
414
- gainNode.connect(this.audioContext.destination);
415
- const offsetUs = Math.max(0, fromUs - clip.startUs);
416
- const offsetSeconds = offsetUs / 1e6;
417
- const actualDurationSeconds = buffer.duration - offsetSeconds;
418
- if (actualDurationSeconds <= 0) {
419
- return;
420
- }
421
- source.start(0, offsetSeconds, actualDurationSeconds);
422
- source.onended = () => {
423
- const index = this.audioSources.indexOf(source);
424
- if (index >= 0) {
425
- this.audioSources.splice(index, 1);
426
- this.audioGainNodes.splice(index, 1);
427
- }
428
- const playedToUs = source._playedToUs;
429
- const clipEndUs = clip.startUs + clip.durationUs;
430
- if (playedToUs < clipEndUs && this.isPlaying) {
431
- setTimeout(() => {
432
- if (this.isPlaying) {
433
- this.continueClipPlayback(clip, playedToUs);
434
- }
435
- }, 50);
436
- }
437
414
  };
438
415
  this.audioSources.push(source);
439
416
  this.audioGainNodes.push(gainNode);
@@ -443,13 +420,13 @@ class GlobalAudioSession {
443
420
  try {
444
421
  source.stop();
445
422
  source.disconnect();
446
- } catch (error) {
423
+ } catch {
447
424
  }
448
425
  }
449
426
  for (const gainNode of this.audioGainNodes) {
450
427
  try {
451
428
  gainNode.disconnect();
452
- } catch (error) {
429
+ } catch {
453
430
  }
454
431
  }
455
432
  this.audioSources = [];
@@ -466,61 +443,76 @@ class GlobalAudioSession {
466
443
  continue;
467
444
  }
468
445
  const trackClips = model.getClipsAtTime(timeUs, track.id);
469
- for (const clip of trackClips) {
470
- if (this.deps.cacheManager.hasClipPCM(clip.id)) {
471
- clips.push(clip);
472
- }
473
- }
446
+ clips.push(...trackClips);
474
447
  }
475
448
  return clips;
476
449
  }
477
450
  /**
478
- * Ensure PCM for a clip is available by decoding from L2 encoded audio
479
- * No-op if PCM already exists or L2 lacks audio
451
+ * Ensure PCM for a clip is available
452
+ * Priority: AudioSampleCache (Resource) > L2 > None
480
453
  */
481
- async ensureClipAudioFromL2(clipId) {
454
+ async ensureClipAudio(clipId) {
482
455
  if (this.deps.cacheManager.hasClipPCM(clipId)) {
483
456
  return;
484
457
  }
485
- if (this.ensuringFromL2.has(clipId)) {
458
+ if (this.ensuringClips.has(clipId)) {
486
459
  return;
487
460
  }
461
+ this.ensuringClips.add(clipId);
462
+ try {
463
+ await this.ensureClipAudioFromResource(clipId);
464
+ } finally {
465
+ this.ensuringClips.delete(clipId);
466
+ }
467
+ }
468
+ /**
469
+ * Ensure PCM from AudioSampleCache (Resource-level)
470
+ * Returns true if successful
471
+ */
472
+ async ensureClipAudioFromResource(clipId) {
488
473
  const model = this.deps.getModel();
489
474
  const clip = model?.findClip(clipId);
490
- if (!clip) return;
491
- const l2Meta = await this.deps.cacheManager.getL2Metadata(clipId, "audio");
492
- const chunkStream = await this.deps.cacheManager.l2Cache.createReadStream(clipId, "audio");
493
- if (!l2Meta || !chunkStream) {
494
- return;
495
- }
496
- this.ensuringFromL2.add(clipId);
497
- const decoder = new AudioChunkDecoder(`l2-audio-${clipId}`, {
498
- codec: l2Meta?.codec,
499
- sampleRate: l2Meta?.sampleRate,
500
- numberOfChannels: l2Meta?.numberOfChannels,
501
- description: l2Meta?.description
502
- });
475
+ if (!clip) return false;
476
+ const resourceId = clip.resourceId;
477
+ if (!resourceId) return false;
478
+ const audioRecord = this.deps.cacheManager.audioSampleCache.get(resourceId);
479
+ if (!audioRecord) return false;
503
480
  try {
504
- const decodeStream = chunkStream.pipeThrough(decoder.createStream());
505
- const reader = decodeStream.getReader();
506
- const pump = async () => {
507
- const { done, value } = await reader.read();
508
- if (done) {
509
- reader.releaseLock();
510
- return;
511
- }
512
- if (value) {
513
- this.deps.cacheManager.putClipAudioData(clipId, value, clip.durationUs);
514
- }
515
- await pump();
516
- };
517
- await pump();
481
+ const clipSamples = audioRecord.samples.filter((s) => {
482
+ const sampleEndUs = s.timestamp + (s.duration ?? 0);
483
+ return s.timestamp < clip.durationUs && sampleEndUs > 0;
484
+ });
485
+ if (clipSamples.length === 0) {
486
+ return false;
487
+ }
488
+ await this.decodeAudioSamples(clipId, clipSamples, audioRecord.metadata, clip.durationUs);
489
+ return true;
518
490
  } catch (error) {
519
- console.error("[GlobalAudioSession] ensureClipAudioFromL2 error:", error);
520
- } finally {
521
- this.ensuringFromL2.delete(clipId);
522
- await decoder.close();
491
+ console.warn(
492
+ `[GlobalAudioSession] Failed to decode audio from resource ${resourceId}:`,
493
+ error
494
+ );
495
+ return false;
496
+ }
497
+ }
498
+ /**
499
+ * Decode audio samples to PCM and cache
500
+ */
501
+ async decodeAudioSamples(clipId, samples, config, clipDurationUs) {
502
+ const decoder = new AudioDecoder({
503
+ output: (audioData) => {
504
+ this.deps.cacheManager.putClipAudioData(clipId, audioData, clipDurationUs);
505
+ },
506
+ error: (error) => {
507
+ console.error(`[GlobalAudioSession] Decoder error for clip ${clipId}:`, error);
508
+ }
509
+ });
510
+ decoder.configure(config);
511
+ for (const sample of samples) {
512
+ decoder.decode(sample);
523
513
  }
514
+ await decoder.flush();
515
+ decoder.close();
524
516
  }
525
517
  pcmToAudioBuffer(planes, sampleRate) {
526
518
  const numberOfChannels = planes.length;
@@ -1 +1 @@
1
- {"version":3,"file":"GlobalAudioSession.js","sources":["../../src/orchestrator/GlobalAudioSession.ts"],"sourcesContent":["import type { TimeUs, AudioClip } 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, hasAudioConfig } from '../model/types';\n\ninterface AudioDataMessage {\n sessionId: string;\n audioData: AudioData;\n clipStartUs: TimeUs;\n clipDurationUs: TimeUs;\n}\n\ninterface AudioSessionDeps {\n cacheManager: CacheManager;\n workers: WorkerPool;\n resourceLoader: ResourceLoader;\n eventBus: EventBus<EventPayloadMap>;\n getModel: () => CompositionModel | null;\n buildWorkerConfigs: () => any;\n}\n\nexport class GlobalAudioSession {\n private mixer: OfflineAudioMixer;\n private activeClips = new Set<string>();\n private streamEndedClips = new Set<string>();\n private deps: AudioSessionDeps;\n private audioContext: AudioContext | null = null;\n private audioSources: AudioBufferSourceNode[] = [];\n private audioGainNodes: GainNode[] = [];\n private volume = 1.0;\n private playbackRate = 1.0;\n private isPlaying = false;\n private currentPlaybackTimeUs: TimeUs = 0;\n private ensuringFromL2 = new Set<string>();\n\n constructor(deps: AudioSessionDeps) {\n this.deps = deps;\n this.mixer = new OfflineAudioMixer(deps.cacheManager, deps.getModel);\n }\n\n onAudioData(message: AudioDataMessage): void {\n const { sessionId, audioData, clipDurationUs } = message;\n this.deps.cacheManager.putClipAudioData(sessionId, audioData, clipDurationUs);\n }\n\n async activateAllAudioClips(): Promise<void> {\n const model = this.deps.getModel();\n if (!model) {\n return;\n }\n\n const audioTracks = model.tracks.filter((track) => track.kind === 'audio');\n\n for (const track of audioTracks) {\n for (const clip of track.clips) {\n if (!this.activeClips.has(clip.id)) {\n if (!isAudioClip(clip)) {\n throw new Error(`Clip ${clip.id} in audio track is not an audio clip`);\n }\n\n await this.setupAudioPipeline(clip);\n this.activeClips.add(clip.id);\n\n await this.deps.resourceLoader.fetch(clip.resourceId, {\n priority: 'high',\n sessionId: clip.id,\n trackId: track.id,\n });\n\n this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });\n }\n }\n }\n }\n\n async deactivateClip(clipId: string): Promise<void> {\n if (!this.activeClips.has(clipId)) {\n return;\n }\n\n // Stop any playing audio sources for this clip\n this.stopClipAudioSources(clipId);\n\n this.deps.workers.terminate('audioDemux', clipId);\n this.deps.workers.terminate('audioDecode', clipId);\n\n this.activeClips.delete(clipId);\n\n this.deps.cacheManager.clearClipAudioData(clipId);\n }\n\n restartPlayingClip(clipId: string, currentTimeUs?: TimeUs): void {\n if (!this.isPlaying || !this.audioContext) {\n return;\n }\n\n const timeUs = currentTimeUs ?? this.currentPlaybackTimeUs;\n\n const model = this.deps.getModel();\n if (!model) {\n return;\n }\n\n const clip = model.findClip(clipId);\n if (!clip) {\n return;\n }\n\n // Check if clip should be playing at current time\n const clipEndUs = clip.startUs + clip.durationUs;\n if (timeUs < clip.startUs || timeUs >= clipEndUs) {\n return;\n }\n\n // Start playback from current time\n this.startClipPlayback(clip, timeUs);\n }\n\n private stopClipAudioSources(clipId: string): void {\n const sourcesToStop: AudioBufferSourceNode[] = [];\n const gainNodesToDisconnect: GainNode[] = [];\n\n for (let i = this.audioSources.length - 1; i >= 0; i--) {\n const source = this.audioSources[i];\n if (source && (source as any)._meframeClipId === clipId) {\n sourcesToStop.push(source);\n this.audioSources.splice(i, 1);\n\n const gainNode = this.audioGainNodes[i];\n if (gainNode) {\n gainNodesToDisconnect.push(gainNode);\n this.audioGainNodes.splice(i, 1);\n }\n }\n }\n\n for (const source of sourcesToStop) {\n try {\n source.stop();\n source.disconnect();\n } catch (error) {\n // Ignore - source may have already stopped\n }\n }\n\n for (const gainNode of gainNodesToDisconnect) {\n try {\n gainNode.disconnect();\n } catch (error) {\n // Ignore\n }\n }\n }\n\n handleAudioStream(stream: ReadableStream<AudioData>, metadata: Record<string, any>): void {\n const sessionId = metadata.sessionId || 'unknown';\n const clipStartUs = metadata.clipStartUs ?? 0;\n const clipDurationUs = metadata.clipDurationUs ?? 0;\n\n const reader = stream.getReader();\n const pump = async (): Promise<void> => {\n try {\n const { done, value } = await reader.read();\n if (done) {\n this.streamEndedClips.add(sessionId);\n reader.releaseLock();\n return;\n }\n\n this.onAudioData({\n sessionId,\n audioData: value,\n clipStartUs,\n clipDurationUs,\n });\n\n await pump();\n } catch (error) {\n console.error('[GlobalAudioSession] Audio stream error:', error);\n reader.releaseLock();\n }\n };\n\n pump();\n }\n\n async startPlayback(timeUs: TimeUs, audioContext: AudioContext): Promise<void> {\n this.audioContext = audioContext;\n\n // Resume AudioContext if suspended (required by modern browsers)\n if (audioContext.state === 'suspended') {\n await audioContext.resume();\n }\n\n this.isPlaying = true;\n this.startAllActiveClips(timeUs);\n }\n\n stopPlayback(): void {\n this.isPlaying = false;\n this.stopAllAudioSources();\n }\n\n updateTime(timeUs: TimeUs): void {\n this.currentPlaybackTimeUs = timeUs;\n if (!this.isPlaying) {\n return;\n }\n this.checkAndStartNewClips(timeUs);\n }\n\n setVolume(volume: number): void {\n this.volume = volume;\n\n // Update existing gain nodes with clip-level config\n for (let i = 0; i < this.audioGainNodes.length; i++) {\n const gainNode = this.audioGainNodes[i];\n const source = this.audioSources[i];\n const clipId = (source as any)._meframeClipId;\n\n if (clipId && gainNode) {\n const model = this.deps.getModel();\n const clip = model?.findClip(clipId);\n if (clip && hasAudioConfig(clip)) {\n const clipVolume = clip.audioConfig?.volume ?? 1.0;\n const muted = clip.audioConfig?.muted ?? false;\n gainNode.gain.value = muted ? 0 : clipVolume * this.volume;\n }\n }\n }\n }\n\n setPlaybackRate(rate: number): void {\n this.playbackRate = rate;\n for (const source of this.audioSources) {\n source.playbackRate.value = this.playbackRate;\n }\n }\n\n reset(): void {\n this.stopAllAudioSources();\n this.deps.cacheManager.resetAudioCache();\n this.activeClips.clear();\n this.streamEndedClips.clear();\n this.ensuringFromL2.clear();\n }\n\n /**\n * Create export encoded audio stream\n */\n async createExportEncodedStream(\n config?: Partial<AudioEncoderConfig>,\n onFirstMetadata?: (metadata: EncodedAudioChunkMetadata) => void\n ): Promise<ReadableStream<EncodedAudioChunk> | null> {\n const audioDataStream = await this.createExportAudioStream();\n if (!audioDataStream) {\n return null;\n }\n\n const encoder = new AudioChunkEncoder(config);\n await encoder.initialize();\n\n const encodingTransform = encoder.createStream();\n const encodedStream = audioDataStream.pipeThrough(encodingTransform);\n\n let firstMetadataExtracted = false;\n\n return encodedStream.pipeThrough(\n new TransformStream({\n transform(encoderChunk, controller) {\n if (!firstMetadataExtracted && onFirstMetadata) {\n onFirstMetadata(encoderChunk.metadata as EncodedAudioChunkMetadata);\n firstMetadataExtracted = true;\n }\n controller.enqueue(encoderChunk.chunk as EncodedAudioChunk);\n },\n })\n );\n }\n\n /**\n * Create export audio stream\n */\n async createExportAudioStream(): Promise<ReadableStream<AudioData> | null> {\n const model = this.deps.getModel();\n if (!model) {\n return null;\n }\n\n const totalDurationUs = model.durationUs;\n\n await this.activateAllAudioClips();\n await this.waitForAudioClipsReady();\n\n return new ReadableStream<AudioData>({\n start: async (controller) => {\n const windowSize = 5_000_000;\n let currentUs = 0;\n\n while (currentUs < totalDurationUs) {\n const windowEndUs = Math.min(currentUs + windowSize, totalDurationUs);\n const mixedBuffer = await this.mixer.mix(currentUs, windowEndUs);\n const audioData = this.audioBufferToAudioData(mixedBuffer, currentUs);\n if (audioData) {\n controller.enqueue(audioData);\n }\n currentUs = windowEndUs;\n }\n\n controller.close();\n },\n });\n }\n\n private async waitForAudioClipsReady(): Promise<void> {\n const model = this.deps.getModel();\n if (!model) return;\n\n const audioClips = model.tracks\n .filter((track) => track.kind === 'audio')\n .flatMap((track) => track.clips);\n\n const waitPromises = audioClips.map((clip) => this.waitForClipPCM(clip.id, 10000)); // 10s timeout\n await Promise.allSettled(waitPromises);\n }\n\n private waitForClipPCM(clipId: string, timeoutMs: number): Promise<boolean> {\n return new Promise((resolve) => {\n const checkInterval = 100;\n let elapsed = 0;\n let lastFrameCount = 0;\n let stableCount = 0;\n let streamEndDetected = false;\n\n const check = () => {\n const pcm = this.deps.cacheManager.getClipPCM(clipId, 0, Number.MAX_SAFE_INTEGER);\n\n if (pcm && pcm.length > 0) {\n const currentFrameCount = pcm[0]?.length ?? 0;\n\n // Check if we have received stream end signal\n if (this.streamEndedClips.has(clipId)) {\n streamEndDetected = true;\n }\n\n // If stream has ended, we're done\n if (streamEndDetected) {\n resolve(true);\n return;\n }\n\n // Otherwise, check if frame count is stable (no new data for 500ms)\n if (currentFrameCount === lastFrameCount) {\n stableCount++;\n if (stableCount >= 5) {\n // 5 * 100ms = 500ms\n resolve(true);\n return;\n }\n } else {\n stableCount = 0;\n lastFrameCount = currentFrameCount;\n }\n }\n\n elapsed += checkInterval;\n if (elapsed >= timeoutMs) {\n console.warn('[GlobalAudioSession] Timeout waiting for clip', clipId, {\n frames: lastFrameCount,\n elapsed,\n });\n resolve(false);\n return;\n }\n\n setTimeout(check, checkInterval);\n };\n\n check();\n });\n }\n\n private startAllActiveClips(timeUs: TimeUs): void {\n if (!this.audioContext) {\n return;\n }\n\n const currentClips = this.getActiveAudioClips(timeUs);\n\n for (const clip of currentClips) {\n this.startClipPlayback(clip, timeUs);\n }\n }\n\n private checkAndStartNewClips(timeUs: TimeUs): void {\n if (!this.audioContext) {\n return;\n }\n\n const currentClips = this.getActiveAudioClips(timeUs);\n const activeClipIds = new Set(\n this.audioSources.map((source) => (source as any)._meframeClipId).filter(Boolean)\n );\n\n for (const clip of currentClips) {\n // Check if clip should be playing at current time\n const clipEndUs = clip.startUs + clip.durationUs;\n if (timeUs >= clipEndUs) {\n // Clip has already ended, skip\n continue;\n }\n\n // Check if current time is before clip starts\n if (timeUs < clip.startUs) {\n // Not yet time to play this clip\n continue;\n }\n\n // Check if clip is too close to ending (avoid glitches from very short playback)\n const MIN_REMAINING_TIME_US = 30000; // 30ms in microseconds\n if (clipEndUs - timeUs < MIN_REMAINING_TIME_US) {\n // Too close to the end, skip to avoid audio glitches\n continue;\n }\n\n if (!activeClipIds.has(clip.id)) {\n this.startClipPlayback(clip, timeUs);\n }\n }\n }\n\n private startClipPlayback(clip: Clip, currentTimeUs: TimeUs): void {\n if (!this.audioContext) {\n console.warn('[GlobalAudioSession] No audioContext, cannot start playback');\n return;\n }\n\n // Use clip-relative time (0-based) like video cache\n const clipPCMData = this.deps.cacheManager.getClipPCMWithMetadata(\n clip.id,\n 0, // Start from beginning of clip (0-based)\n clip.durationUs // Full clip duration\n );\n\n if (!clipPCMData || clipPCMData.planes.length === 0) {\n // No data yet, will retry later via checkAndStartNewClips\n console.warn('[GlobalAudioSession] No PCM data for clip, will retry later', clip.id);\n return;\n }\n\n const buffer = this.pcmToAudioBuffer(clipPCMData.planes, clipPCMData.sampleRate);\n\n const offsetUs = Math.max(0, currentTimeUs - clip.startUs);\n const offsetSeconds = offsetUs / 1_000_000;\n\n // Use actual buffer duration instead of clip.durationUs\n const actualDurationSeconds = buffer.duration - offsetSeconds;\n\n // Early check: if remaining duration is too short, skip\n // (Note: checkAndStartNewClips should already filter these out)\n const MIN_PLAYBACK_DURATION_SECONDS = 0.03; // 30ms\n if (actualDurationSeconds < MIN_PLAYBACK_DURATION_SECONDS) {\n return;\n }\n\n const source = this.audioContext.createBufferSource();\n source.buffer = buffer;\n source.playbackRate.value = this.playbackRate;\n (source as any)._meframeClipId = clip.id;\n (source as any)._playedToUs = clip.startUs + buffer.duration * 1_000_000; // Track where it played to\n\n const gainNode = this.audioContext.createGain();\n\n // Apply audio config\n if (hasAudioConfig(clip)) {\n const volume = clip.audioConfig?.volume ?? 1.0;\n const muted = clip.audioConfig?.muted ?? false;\n gainNode.gain.value = muted ? 0 : volume * this.volume;\n } else {\n gainNode.gain.value = this.volume;\n }\n\n source.connect(gainNode);\n gainNode.connect(this.audioContext.destination);\n\n source.start(0, offsetSeconds, actualDurationSeconds);\n\n source.onended = () => {\n const index = this.audioSources.indexOf(source);\n if (index >= 0) {\n this.audioSources.splice(index, 1);\n this.audioGainNodes.splice(index, 1);\n }\n\n // Check if more data has arrived and continue playing\n const playedToUs = (source as any)._playedToUs;\n const clipEndUs = clip.startUs + clip.durationUs;\n\n if (playedToUs < clipEndUs && this.isPlaying) {\n // There might be more data, try to continue\n setTimeout(() => {\n if (this.isPlaying) {\n this.continueClipPlayback(clip, playedToUs);\n }\n }, 50);\n }\n };\n\n this.audioSources.push(source);\n this.audioGainNodes.push(gainNode);\n }\n\n private continueClipPlayback(clip: Clip, fromUs: TimeUs): void {\n if (!this.audioContext) {\n return;\n }\n\n // Use clip-relative time (0-based) like video cache\n const clipPCMData = this.deps.cacheManager.getClipPCMWithMetadata(\n clip.id,\n 0, // 0-based start\n clip.durationUs // clip duration\n );\n\n if (!clipPCMData || clipPCMData.planes.length === 0) {\n return;\n }\n\n const buffer = this.pcmToAudioBuffer(clipPCMData.planes, clipPCMData.sampleRate);\n const bufferEndUs = clip.startUs + buffer.duration * 1_000_000;\n\n // Check if there's new data beyond where we played to\n if (bufferEndUs <= fromUs + 100_000) {\n // 100ms tolerance\n // No significant new data\n return;\n }\n\n // Continue playback from where it left off\n const source = this.audioContext.createBufferSource();\n source.buffer = buffer;\n source.playbackRate.value = this.playbackRate;\n (source as any)._meframeClipId = clip.id;\n (source as any)._playedToUs = bufferEndUs;\n\n const gainNode = this.audioContext.createGain();\n\n // Apply audio config\n if (hasAudioConfig(clip)) {\n const volume = clip.audioConfig?.volume ?? 1.0;\n const muted = clip.audioConfig?.muted ?? false;\n gainNode.gain.value = muted ? 0 : volume * this.volume;\n } else {\n gainNode.gain.value = this.volume;\n }\n\n source.connect(gainNode);\n gainNode.connect(this.audioContext.destination);\n\n const offsetUs = Math.max(0, fromUs - clip.startUs);\n const offsetSeconds = offsetUs / 1_000_000;\n const actualDurationSeconds = buffer.duration - offsetSeconds;\n\n if (actualDurationSeconds <= 0) {\n return;\n }\n\n source.start(0, offsetSeconds, actualDurationSeconds);\n\n source.onended = () => {\n const index = this.audioSources.indexOf(source);\n if (index >= 0) {\n this.audioSources.splice(index, 1);\n this.audioGainNodes.splice(index, 1);\n }\n\n // Check if more data has arrived\n const playedToUs = (source as any)._playedToUs;\n const clipEndUs = clip.startUs + clip.durationUs;\n\n if (playedToUs < clipEndUs && this.isPlaying) {\n setTimeout(() => {\n if (this.isPlaying) {\n this.continueClipPlayback(clip, playedToUs);\n }\n }, 50);\n }\n };\n\n this.audioSources.push(source);\n this.audioGainNodes.push(gainNode);\n }\n\n private stopAllAudioSources(): void {\n for (const source of this.audioSources) {\n try {\n source.stop();\n source.disconnect();\n } catch (error) {\n // Ignore\n }\n }\n\n for (const gainNode of this.audioGainNodes) {\n try {\n gainNode.disconnect();\n } catch (error) {\n // Ignore\n }\n }\n\n this.audioSources = [];\n this.audioGainNodes = [];\n }\n\n private getActiveAudioClips(timeUs: TimeUs): Clip[] {\n const model = this.deps.getModel();\n if (!model) {\n return [];\n }\n\n const clips: Clip[] = [];\n\n // Only search audio tracks and video tracks (video clips can have audio)\n // Exclude attachment tracks (caption, fx, etc.) which don't participate in audio playback\n for (const track of model.tracks) {\n if (track.kind !== 'audio' && track.kind !== 'video') {\n continue;\n }\n\n const trackClips = model.getClipsAtTime(timeUs, track.id);\n for (const clip of trackClips) {\n // Check if this clip has audio data in cache\n if (this.deps.cacheManager.hasClipPCM(clip.id)) {\n clips.push(clip);\n }\n }\n }\n\n return clips;\n }\n\n /**\n * Ensure PCM for a clip is available by decoding from L2 encoded audio\n * No-op if PCM already exists or L2 lacks audio\n */\n async ensureClipAudioFromL2(clipId: string): Promise<void> {\n if (this.deps.cacheManager.hasClipPCM(clipId)) {\n return;\n }\n if (this.ensuringFromL2.has(clipId)) {\n return;\n }\n\n const model = this.deps.getModel();\n const clip = model?.findClip(clipId);\n if (!clip) return;\n\n const l2Meta = await this.deps.cacheManager.getL2Metadata(clipId, 'audio');\n const chunkStream = await this.deps.cacheManager.l2Cache.createReadStream(clipId, 'audio');\n if (!l2Meta || !chunkStream) {\n return;\n }\n\n this.ensuringFromL2.add(clipId);\n\n const decoder = new AudioChunkDecoder(`l2-audio-${clipId}`, {\n codec: l2Meta?.codec,\n sampleRate: l2Meta?.sampleRate,\n numberOfChannels: l2Meta?.numberOfChannels,\n description: l2Meta?.description,\n } as any);\n try {\n const decodeStream = chunkStream.pipeThrough(decoder.createStream());\n const reader = decodeStream.getReader();\n const pump = async (): Promise<void> => {\n const { done, value } = await reader.read();\n if (done) {\n reader.releaseLock();\n return;\n }\n if (value) {\n this.deps.cacheManager.putClipAudioData(clipId, value, clip.durationUs);\n }\n await pump();\n };\n await pump();\n } catch (error) {\n console.error('[GlobalAudioSession] ensureClipAudioFromL2 error:', error);\n } finally {\n this.ensuringFromL2.delete(clipId);\n await decoder.close();\n }\n }\n\n private pcmToAudioBuffer(planes: Float32Array[], sampleRate: number): AudioBuffer {\n const numberOfChannels = planes.length;\n const numberOfFrames = planes[0]?.length ?? 0;\n\n const ctx = new OfflineAudioContext(numberOfChannels, 1, sampleRate);\n const buffer = ctx.createBuffer(numberOfChannels, numberOfFrames, sampleRate);\n\n for (let channel = 0; channel < numberOfChannels; channel++) {\n const plane = planes[channel];\n if (plane) {\n const channelData = buffer.getChannelData(channel);\n channelData.set(plane);\n }\n }\n\n return buffer;\n }\n\n private audioBufferToAudioData(buffer: AudioBuffer, timestampUs: TimeUs): AudioData | null {\n const sampleRate = buffer.sampleRate;\n const numberOfChannels = buffer.numberOfChannels;\n const numberOfFrames = buffer.length;\n\n const planes: Float32Array[] = [];\n for (let channel = 0; channel < numberOfChannels; channel++) {\n planes.push(buffer.getChannelData(channel));\n }\n\n return new AudioData({\n format: 'f32', // interleaved format\n sampleRate,\n numberOfFrames,\n numberOfChannels,\n timestamp: timestampUs,\n data: this.interleavePlanarData(planes),\n });\n }\n\n private interleavePlanarData(planes: Float32Array[]): ArrayBuffer {\n const numberOfChannels = planes.length;\n const numberOfFrames = planes[0]?.length ?? 0;\n const totalSamples = numberOfChannels * numberOfFrames;\n\n const interleaved = new Float32Array(totalSamples);\n\n for (let frame = 0; frame < numberOfFrames; frame++) {\n for (let channel = 0; channel < numberOfChannels; channel++) {\n interleaved[frame * numberOfChannels + channel] = planes[channel]![frame]!;\n }\n }\n\n return interleaved.buffer;\n }\n\n private async setupAudioPipeline(clip: AudioClip): Promise<void> {\n const { id: clipId, resourceId, startUs, durationUs } = clip;\n const audioDemuxWorker = await this.deps.workers.getOrCreate('audioDemux', clipId, {\n lazy: true,\n });\n const audioDecodeWorker = await this.deps.workers.getOrCreate('audioDecode', clipId, {\n lazy: true,\n });\n\n const demuxToDecodeChannel = new MessageChannel();\n await audioDemuxWorker.send(\n 'connect',\n { direction: 'downstream', port: demuxToDecodeChannel.port1, streamType: 'audio', clipId },\n { transfer: [demuxToDecodeChannel.port1] }\n );\n await audioDecodeWorker.send(\n 'connect',\n {\n direction: 'upstream',\n port: demuxToDecodeChannel.port2,\n streamType: 'audio',\n sessionId: clipId,\n clipStartUs: startUs || 0,\n clipDurationUs: durationUs || 0,\n },\n { transfer: [demuxToDecodeChannel.port2] }\n );\n\n audioDecodeWorker.receiveStream((stream, metadata) => {\n this.handleAudioStream(stream as ReadableStream<AudioData>, {\n sessionId: clipId,\n clipStartUs: startUs || 0,\n clipDurationUs: durationUs || 0,\n ...metadata,\n });\n });\n\n const demuxConfig = this.deps.buildWorkerConfigs().audioDemux;\n await audioDemuxWorker.send('configure', {\n initial: true,\n resourceId,\n clipId,\n config: demuxConfig,\n });\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"GlobalAudioSession.js","sources":["../../src/orchestrator/GlobalAudioSession.ts"],"sourcesContent":["import type { TimeUs, AudioClip } 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 { isAudioClip, hasAudioConfig, 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 workers: WorkerPool;\n resourceLoader: ResourceLoader;\n eventBus: EventBus<EventPayloadMap>;\n getModel: () => CompositionModel | null;\n buildWorkerConfigs: () => any;\n}\n\nexport class GlobalAudioSession {\n private mixer: OfflineAudioMixer;\n private activeClips = new Set<string>();\n private streamEndedClips = new Set<string>();\n private deps: AudioSessionDeps;\n private audioContext: AudioContext | null = null;\n private audioSources: AudioBufferSourceNode[] = [];\n private audioGainNodes: GainNode[] = [];\n private volume = 1.0;\n private playbackRate = 1.0;\n private isPlaying = false;\n private currentPlaybackTimeUs: TimeUs = 0;\n private ensuringClips = new Set<string>(); // Track all decoding attempts (Resource & L2)\n\n constructor(deps: AudioSessionDeps) {\n this.deps = deps;\n this.mixer = new OfflineAudioMixer(deps.cacheManager, deps.getModel);\n }\n\n onAudioData(message: AudioDataMessage): void {\n const { sessionId, audioData, clipDurationUs } = message;\n this.deps.cacheManager.putClipAudioData(sessionId, audioData, clipDurationUs);\n }\n\n async ensureAudioForTime(timeUs: TimeUs): Promise<void> {\n const model = this.deps.getModel();\n if (!model) return;\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 for each clip\n await Promise.all(\n activeClips.map(async (clip) => {\n // If already has PCM, we are good\n if (this.deps.cacheManager.hasClipPCM(clip.id)) return;\n\n if (!hasResourceId(clip)) return;\n\n const resource = model.getResource(clip.resourceId);\n if (!resource) return;\n\n // Strategy A: Cached Samples (Video/M4A)\n if (this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n await this.ensureClipAudio(clip.id);\n return;\n }\n\n // Strategy B: Streaming (Audio Track)\n // If it's an active streaming clip, wait for it\n if (this.activeClips.has(clip.id)) {\n await this.waitForClipPCM(clip.id, 2000);\n }\n })\n );\n }\n\n async activateAllAudioClips(): Promise<void> {\n const model = this.deps.getModel();\n if (!model) {\n return;\n }\n\n const audioTracks = model.tracks.filter((track) => track.kind === 'audio');\n\n for (const track of audioTracks) {\n for (const clip of track.clips) {\n if (!this.activeClips.has(clip.id)) {\n if (!isAudioClip(clip)) {\n throw new Error(`Clip ${clip.id} in audio track is not an audio clip`);\n }\n\n // Audio track clips are always standalone audio files (MP3/WAV)\n // Video audio tracks are handled separately via AudioSampleCache\n\n // OPTIMIZATION: If we have cached samples (e.g. from video resource used as audio), skip pipeline\n if (this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {\n await this.ensureClipAudio(clip.id);\n this.activeClips.add(clip.id);\n continue;\n }\n\n await this.setupAudioPipeline(clip);\n this.activeClips.add(clip.id);\n\n await this.deps.resourceLoader.fetch(clip.resourceId, {\n priority: 'high',\n sessionId: clip.id,\n clipId: clip.id,\n trackId: track.id,\n });\n\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 // Stop any playing audio sources for this clip\n this.stopClipAudioSources(clipId);\n\n this.deps.workers.terminate('audioDemux', clipId);\n this.deps.workers.terminate('audioDecode', clipId);\n\n this.activeClips.delete(clipId);\n\n this.deps.cacheManager.clearClipAudioData(clipId);\n }\n\n restartPlayingClip(clipId: string, currentTimeUs?: TimeUs): void {\n if (!this.isPlaying || !this.audioContext) {\n return;\n }\n\n const timeUs = currentTimeUs ?? this.currentPlaybackTimeUs;\n\n const model = this.deps.getModel();\n if (!model) {\n return;\n }\n\n const clip = model.findClip(clipId);\n if (!clip) {\n return;\n }\n\n // Check if clip should be playing at current time\n const clipEndUs = clip.startUs + clip.durationUs;\n if (timeUs < clip.startUs || timeUs >= clipEndUs) {\n return;\n }\n\n // Start playback from current time\n this.startClipPlayback(clip, timeUs);\n }\n\n private stopClipAudioSources(clipId: string): void {\n const sourcesToStop: AudioBufferSourceNode[] = [];\n const gainNodesToDisconnect: GainNode[] = [];\n\n for (let i = this.audioSources.length - 1; i >= 0; i--) {\n const source = this.audioSources[i];\n if (source && (source as any)._meframeClipId === clipId) {\n sourcesToStop.push(source);\n this.audioSources.splice(i, 1);\n\n const gainNode = this.audioGainNodes[i];\n if (gainNode) {\n gainNodesToDisconnect.push(gainNode);\n this.audioGainNodes.splice(i, 1);\n }\n }\n }\n\n for (const source of sourcesToStop) {\n try {\n source.stop();\n source.disconnect();\n } catch {\n // Ignore - source may have already stopped\n }\n }\n\n for (const gainNode of gainNodesToDisconnect) {\n try {\n gainNode.disconnect();\n } catch {\n // Ignore\n }\n }\n }\n\n handleAudioStream(stream: ReadableStream<AudioData>, metadata: Record<string, any>): void {\n const sessionId = metadata.sessionId || 'unknown';\n const clipStartUs = metadata.clipStartUs ?? 0;\n const clipDurationUs = metadata.clipDurationUs ?? 0;\n\n const reader = stream.getReader();\n const pump = async (): Promise<void> => {\n try {\n const { done, value } = await reader.read();\n if (done) {\n this.streamEndedClips.add(sessionId);\n reader.releaseLock();\n return;\n }\n\n this.onAudioData({\n sessionId,\n audioData: value,\n clipStartUs,\n clipDurationUs,\n });\n\n await pump();\n } catch (error) {\n console.error('[GlobalAudioSession] Audio stream error:', error);\n reader.releaseLock();\n }\n };\n\n pump();\n }\n\n async startPlayback(timeUs: TimeUs, audioContext: AudioContext): Promise<void> {\n this.audioContext = audioContext;\n\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 (L1 cache)\n await this.ensureAudioForTime(timeUs);\n\n this.isPlaying = true;\n this.startAllActiveClips(timeUs);\n }\n\n stopPlayback(): void {\n this.isPlaying = false;\n this.stopAllAudioSources();\n }\n\n updateTime(timeUs: TimeUs): void {\n this.currentPlaybackTimeUs = timeUs;\n if (!this.isPlaying) {\n return;\n }\n this.checkAndStartNewClips(timeUs);\n }\n\n setVolume(volume: number): void {\n this.volume = volume;\n\n // Update existing gain nodes with clip-level config\n for (let i = 0; i < this.audioGainNodes.length; i++) {\n const gainNode = this.audioGainNodes[i];\n const source = this.audioSources[i];\n const clipId = (source as any)._meframeClipId;\n\n if (clipId && gainNode) {\n const model = this.deps.getModel();\n const clip = model?.findClip(clipId);\n if (clip && hasAudioConfig(clip)) {\n const clipVolume = clip.audioConfig?.volume ?? 1.0;\n const muted = clip.audioConfig?.muted ?? false;\n gainNode.gain.value = muted ? 0 : clipVolume * this.volume;\n }\n }\n }\n }\n\n setPlaybackRate(rate: number): void {\n this.playbackRate = rate;\n for (const source of this.audioSources) {\n source.playbackRate.value = this.playbackRate;\n }\n }\n\n reset(): void {\n this.stopAllAudioSources();\n this.deps.cacheManager.resetAudioCache();\n this.activeClips.clear();\n this.streamEndedClips.clear();\n }\n\n /**\n * 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 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 this.startExportEncoderReader(this.exportEncoderStream.readable, onChunk);\n }\n\n await this.exportEncoderWriter?.write(audioData);\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 /**\n * Create export audio stream\n */\n async createExportAudioStream(): Promise<ReadableStream<AudioData> | null> {\n const model = this.deps.getModel();\n if (!model) {\n return null;\n }\n\n const totalDurationUs = model.durationUs;\n\n await this.activateAllAudioClips();\n await this.waitForAudioClipsReady();\n\n return new ReadableStream<AudioData>({\n start: async (controller) => {\n const windowSize = 5_000_000;\n let currentUs = 0;\n\n while (currentUs < totalDurationUs) {\n const windowEndUs = Math.min(currentUs + windowSize, totalDurationUs);\n const mixedBuffer = await this.mixer.mix(currentUs, windowEndUs);\n const audioData = this.audioBufferToAudioData(mixedBuffer, currentUs);\n if (audioData) {\n controller.enqueue(audioData);\n }\n currentUs = windowEndUs;\n }\n\n controller.close();\n },\n });\n }\n\n private async waitForAudioClipsReady(): Promise<void> {\n const model = this.deps.getModel();\n if (!model) return;\n\n const audioClips = model.tracks\n .filter((track) => track.kind === 'audio')\n .flatMap((track) => track.clips);\n\n const waitPromises = audioClips.map((clip) => this.waitForClipPCM(clip.id, 10000)); // 10s timeout\n await Promise.allSettled(waitPromises);\n }\n\n private waitForClipPCM(clipId: string, timeoutMs: number): Promise<boolean> {\n return new Promise((resolve) => {\n const checkInterval = 100;\n let elapsed = 0;\n let lastFrameCount = 0;\n let stableCount = 0;\n let streamEndDetected = false;\n\n const check = () => {\n const pcm = this.deps.cacheManager.getClipPCM(clipId, 0, Number.MAX_SAFE_INTEGER);\n\n if (pcm && pcm.length > 0) {\n const currentFrameCount = pcm[0]?.length ?? 0;\n\n // Check if we have received stream end signal\n if (this.streamEndedClips.has(clipId)) {\n streamEndDetected = true;\n }\n\n // If stream has ended, we're done\n if (streamEndDetected) {\n resolve(true);\n return;\n }\n\n // Otherwise, check if frame count is stable (no new data for 500ms)\n if (currentFrameCount === lastFrameCount) {\n stableCount++;\n if (stableCount >= 5) {\n // 5 * 100ms = 500ms\n resolve(true);\n return;\n }\n } else {\n stableCount = 0;\n lastFrameCount = currentFrameCount;\n }\n }\n\n elapsed += checkInterval;\n if (elapsed >= timeoutMs) {\n console.warn('[GlobalAudioSession] Timeout waiting for clip', clipId, {\n frames: lastFrameCount,\n elapsed,\n });\n resolve(false);\n return;\n }\n\n setTimeout(check, checkInterval);\n };\n\n check();\n });\n }\n\n private startAllActiveClips(timeUs: TimeUs): void {\n if (!this.audioContext) {\n return;\n }\n\n const currentClips = this.getActiveAudioClips(timeUs);\n\n for (const clip of currentClips) {\n this.startClipPlayback(clip, timeUs);\n }\n }\n\n private checkAndStartNewClips(timeUs: TimeUs): void {\n if (!this.audioContext) {\n return;\n }\n\n const currentClips = this.getActiveAudioClips(timeUs);\n const activeClipIds = new Set(\n this.audioSources.map((source) => (source as any)._meframeClipId).filter(Boolean)\n );\n\n for (const clip of currentClips) {\n // Check if clip should be playing at current time\n const clipEndUs = clip.startUs + clip.durationUs;\n if (timeUs >= clipEndUs) {\n // Clip has already ended, skip\n continue;\n }\n\n // Check if current time is before clip starts\n if (timeUs < clip.startUs) {\n // Not yet time to play this clip\n continue;\n }\n\n // Check if clip is too close to ending (avoid glitches from very short playback)\n const MIN_REMAINING_TIME_US = 30000; // 30ms in microseconds\n if (clipEndUs - timeUs < MIN_REMAINING_TIME_US) {\n // Too close to the end, skip to avoid audio glitches\n continue;\n }\n\n if (!activeClipIds.has(clip.id)) {\n this.startClipPlayback(clip, timeUs);\n }\n }\n }\n\n private startClipPlayback(clip: Clip, currentTimeUs: TimeUs): void {\n if (!this.audioContext) {\n console.warn('[GlobalAudioSession] No audioContext, cannot start playback');\n return;\n }\n\n // Use clip-relative time (0-based) like video cache\n const clipPCMData = this.deps.cacheManager.getClipPCMWithMetadata(\n clip.id,\n 0, // Start from beginning of clip (0-based)\n clip.durationUs // Full clip duration\n );\n\n if (!clipPCMData || clipPCMData.planes.length === 0) {\n // No data yet, trigger decoding and retry later\n // This handles the case where natural playback reaches a new clip\n void this.ensureClipAudio(clip.id);\n return;\n }\n\n const buffer = this.pcmToAudioBuffer(clipPCMData.planes, clipPCMData.sampleRate);\n\n const offsetUs = Math.max(0, currentTimeUs - clip.startUs);\n const offsetSeconds = offsetUs / 1_000_000;\n\n // Use actual buffer duration instead of clip.durationUs\n const actualDurationSeconds = buffer.duration - offsetSeconds;\n\n // Early check: if remaining duration is too short, skip\n // (Note: checkAndStartNewClips should already filter these out)\n const MIN_PLAYBACK_DURATION_SECONDS = 0.03; // 30ms\n if (actualDurationSeconds < MIN_PLAYBACK_DURATION_SECONDS) {\n return;\n }\n\n const source = this.audioContext.createBufferSource();\n source.buffer = buffer;\n source.playbackRate.value = this.playbackRate;\n (source as any)._meframeClipId = clip.id;\n\n const gainNode = this.audioContext.createGain();\n\n // Apply audio config\n if (hasAudioConfig(clip)) {\n const volume = clip.audioConfig?.volume ?? 1.0;\n const muted = clip.audioConfig?.muted ?? false;\n gainNode.gain.value = muted ? 0 : volume * this.volume;\n } else {\n gainNode.gain.value = this.volume;\n }\n\n source.connect(gainNode);\n gainNode.connect(this.audioContext.destination);\n\n source.start(0, offsetSeconds, actualDurationSeconds);\n\n source.onended = () => {\n const index = this.audioSources.indexOf(source);\n if (index >= 0) {\n this.audioSources.splice(index, 1);\n this.audioGainNodes.splice(index, 1);\n }\n // Playback loop will automatically restart if clip should continue playing\n };\n\n this.audioSources.push(source);\n this.audioGainNodes.push(gainNode);\n }\n\n private stopAllAudioSources(): void {\n for (const source of this.audioSources) {\n try {\n source.stop();\n source.disconnect();\n } catch {\n // Ignore\n }\n }\n\n for (const gainNode of this.audioGainNodes) {\n try {\n gainNode.disconnect();\n } catch {\n // Ignore\n }\n }\n\n this.audioSources = [];\n this.audioGainNodes = [];\n }\n\n private getActiveAudioClips(timeUs: TimeUs): Clip[] {\n const model = this.deps.getModel();\n if (!model) {\n return [];\n }\n\n const clips: Clip[] = [];\n\n // Only search audio tracks and video tracks (video clips can have audio)\n // Exclude attachment tracks (caption, fx, etc.) which don't participate in audio playback\n for (const track of model.tracks) {\n if (track.kind !== 'audio' && track.kind !== 'video') {\n continue;\n }\n\n const trackClips = model.getClipsAtTime(timeUs, track.id);\n // Return ALL clips active at this time, regardless of PCM status\n // startClipPlayback will handle missing PCM by triggering decoding\n clips.push(...trackClips);\n }\n\n return clips;\n }\n\n /**\n * Ensure PCM for a clip is available\n * Priority: AudioSampleCache (Resource) > L2 > None\n */\n async ensureClipAudio(clipId: string): Promise<void> {\n // Already has PCM\n if (this.deps.cacheManager.hasClipPCM(clipId)) {\n return;\n }\n\n // Prevent concurrent decoding for the same clip\n if (this.ensuringClips.has(clipId)) {\n return;\n }\n this.ensuringClips.add(clipId);\n\n try {\n // Try from resource audio samples first\n await this.ensureClipAudioFromResource(clipId);\n } finally {\n this.ensuringClips.delete(clipId);\n }\n }\n\n /**\n * Ensure PCM from AudioSampleCache (Resource-level)\n * Returns true if successful\n */\n private async ensureClipAudioFromResource(clipId: string): Promise<boolean> {\n const model = this.deps.getModel();\n const clip = model?.findClip(clipId);\n if (!clip) return false;\n\n const resourceId = (clip as any).resourceId;\n if (!resourceId) return false;\n\n // Get audio samples from cache\n const audioRecord = this.deps.cacheManager.audioSampleCache.get(resourceId);\n if (!audioRecord) return false;\n\n try {\n // Get samples for this clip's time range\n const clipSamples = audioRecord.samples.filter((s) => {\n const sampleEndUs = s.timestamp + (s.duration ?? 0);\n return s.timestamp < clip.durationUs && sampleEndUs > 0;\n });\n\n if (clipSamples.length === 0) {\n return false;\n }\n\n // Decode samples to PCM\n await this.decodeAudioSamples(clipId, clipSamples, audioRecord.metadata, clip.durationUs);\n return true;\n } catch (error) {\n console.warn(\n `[GlobalAudioSession] Failed to decode audio from resource ${resourceId}:`,\n error\n );\n return false;\n }\n }\n\n /**\n * Decode audio samples to PCM and cache\n */\n private async decodeAudioSamples(\n clipId: string,\n samples: EncodedAudioChunk[],\n config: AudioDecoderConfig,\n clipDurationUs: number\n ): Promise<void> {\n const decoder = new AudioDecoder({\n output: (audioData) => {\n this.deps.cacheManager.putClipAudioData(clipId, audioData, clipDurationUs);\n },\n error: (error) => {\n console.error(`[GlobalAudioSession] Decoder error for clip ${clipId}:`, error);\n },\n });\n\n decoder.configure(config);\n\n for (const sample of samples) {\n decoder.decode(sample);\n }\n\n await decoder.flush();\n decoder.close();\n }\n\n private pcmToAudioBuffer(planes: Float32Array[], sampleRate: number): AudioBuffer {\n const numberOfChannels = planes.length;\n const numberOfFrames = planes[0]?.length ?? 0;\n\n const ctx = new OfflineAudioContext(numberOfChannels, 1, sampleRate);\n const buffer = ctx.createBuffer(numberOfChannels, numberOfFrames, sampleRate);\n\n for (let channel = 0; channel < numberOfChannels; channel++) {\n const plane = planes[channel];\n if (plane) {\n const channelData = buffer.getChannelData(channel);\n channelData.set(plane);\n }\n }\n\n return buffer;\n }\n\n private audioBufferToAudioData(buffer: AudioBuffer, timestampUs: TimeUs): AudioData | null {\n const sampleRate = buffer.sampleRate;\n const numberOfChannels = buffer.numberOfChannels;\n const numberOfFrames = buffer.length;\n\n const planes: Float32Array[] = [];\n for (let channel = 0; channel < numberOfChannels; channel++) {\n planes.push(buffer.getChannelData(channel));\n }\n\n return new AudioData({\n format: 'f32', // interleaved format\n sampleRate,\n numberOfFrames,\n numberOfChannels,\n timestamp: timestampUs,\n data: this.interleavePlanarData(planes),\n });\n }\n\n private interleavePlanarData(planes: Float32Array[]): ArrayBuffer {\n const numberOfChannels = planes.length;\n const numberOfFrames = planes[0]?.length ?? 0;\n const totalSamples = numberOfChannels * numberOfFrames;\n\n const interleaved = new Float32Array(totalSamples);\n\n for (let frame = 0; frame < numberOfFrames; frame++) {\n for (let channel = 0; channel < numberOfChannels; channel++) {\n interleaved[frame * numberOfChannels + channel] = planes[channel]![frame]!;\n }\n }\n\n return interleaved.buffer;\n }\n\n private async setupAudioPipeline(clip: AudioClip): Promise<void> {\n const { id: clipId, resourceId, startUs, durationUs } = clip;\n const audioDemuxWorker = await this.deps.workers.getOrCreate('audioDemux', clipId, {\n lazy: true,\n });\n const audioDecodeWorker = await this.deps.workers.getOrCreate('audioDecode', clipId, {\n lazy: true,\n });\n\n const demuxToDecodeChannel = new MessageChannel();\n await audioDemuxWorker.send(\n 'connect',\n { direction: 'downstream', port: demuxToDecodeChannel.port1, streamType: 'audio', clipId },\n { transfer: [demuxToDecodeChannel.port1] }\n );\n await audioDecodeWorker.send(\n 'connect',\n {\n direction: 'upstream',\n port: demuxToDecodeChannel.port2,\n streamType: 'audio',\n sessionId: clipId,\n clipStartUs: startUs || 0,\n clipDurationUs: durationUs || 0,\n },\n { transfer: [demuxToDecodeChannel.port2] }\n );\n\n audioDecodeWorker.receiveStream((stream, metadata) => {\n this.handleAudioStream(stream as ReadableStream<AudioData>, {\n sessionId: clipId,\n clipStartUs: startUs || 0,\n clipDurationUs: durationUs || 0,\n ...metadata,\n });\n });\n\n const demuxConfig = this.deps.buildWorkerConfigs().audioDemux;\n await audioDemuxWorker.send('configure', {\n initial: true,\n resourceId,\n clipId,\n config: demuxConfig,\n });\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ import { CacheManager } from '../cache/CacheManager';
2
+ import { ResourceCache } from '../cache/resource/ResourceCache';
3
+ import { MP4IndexCache } from '../cache/resource/MP4IndexCache';
4
+ import { MP4Index } from '../stages/demux/types';
5
+ import { TimeUs } from '../model/types';
6
+ import { CompositionModel, Clip } from '../model';
7
+
8
+ interface OnDemandVideoSessionConfig {
9
+ clipId: string;
10
+ resourceId: string;
11
+ targetTimeUs: TimeUs;
12
+ globalTimeUs: TimeUs;
13
+ resourceCache: ResourceCache;
14
+ mp4IndexCache: MP4IndexCache;
15
+ cacheManager: CacheManager;
16
+ compositionModel: CompositionModel;
17
+ fps: number;
18
+ }
19
+ /**
20
+ * OnDemandVideoSession - Main-thread on-demand decoder
21
+ *
22
+ * Strategy:
23
+ * 1. Read GOP range from OPFS
24
+ * 2. Demux using MP4Box (main thread)
25
+ * 3. Decode using VideoDecoder (main thread)
26
+ * 4. Write RAW VideoFrames (YUV) to L1 cache
27
+ * 5. Dispose immediately after window completes
28
+ *
29
+ * Why main thread?
30
+ * - Window is small (±2s, ~60-120 frames)
31
+ * - Worker overhead (10-50ms) is significant for small workloads
32
+ * - Decode is fast enough
33
+ */
34
+ export declare class OnDemandVideoSession {
35
+ /**
36
+ * Static method to decode and cache first frame from extracted GOP chunks
37
+ * Used by ResourceLoader during streaming download for fast cover rendering
38
+ */
39
+ static decodeAndCacheFirstFrame(resourceId: string, chunks: EncodedVideoChunk[], index: MP4Index, clip: Clip, cacheManager: CacheManager, fps: number): Promise<void>;
40
+ private readonly clipId;
41
+ private readonly resourceId;
42
+ private readonly resourceCache;
43
+ private readonly mp4IndexCache;
44
+ private readonly cacheManager;
45
+ private readonly compositionModel;
46
+ private readonly fps;
47
+ private readonly globalTimeUs;
48
+ private readonly targetTimeUs;
49
+ private decoder;
50
+ private isDisposed;
51
+ private decodedFrames;
52
+ private constructor();
53
+ static create(config: OnDemandVideoSessionConfig): Promise<OnDemandVideoSession>;
54
+ private init;
55
+ /**
56
+ * Decode a window range, write raw frames to L1 cache
57
+ */
58
+ decodeWindow(startUs: TimeUs, endUs: TimeUs): Promise<void>;
59
+ private calculateGOPRangesForWindow;
60
+ /**
61
+ * Extract video chunks from GOP data
62
+ *
63
+ * Directly use GOP sample indices to slice the samples array.
64
+ * This is O(k) where k is the number of samples in the window,
65
+ * vs O(n×m) for the old approach (n=all samples, m=GOP count).
66
+ */
67
+ private demuxGOPData;
68
+ private decodeChunks;
69
+ private cacheDecodedFrames;
70
+ dispose(): Promise<void>;
71
+ }
72
+ export {};
73
+ //# sourceMappingURL=OnDemandVideoSession.d.ts.map
@@ -0,0 +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,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;AAUvD,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,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,YAAY,CAAC;IAC3B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,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,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmB;IACpD,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"}