@meframe/core 0.0.29 → 0.0.30

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 (218) hide show
  1. package/dist/Meframe.d.ts +2 -30
  2. package/dist/Meframe.d.ts.map +1 -1
  3. package/dist/Meframe.js +7 -118
  4. package/dist/Meframe.js.map +1 -1
  5. package/dist/cache/CacheManager.d.ts +42 -68
  6. package/dist/cache/CacheManager.d.ts.map +1 -1
  7. package/dist/cache/CacheManager.js +224 -188
  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/L2OPFSStore.d.ts +37 -0
  15. package/dist/cache/l2/L2OPFSStore.d.ts.map +1 -0
  16. package/dist/cache/resource/AudioSampleCache.d.ts +52 -0
  17. package/dist/cache/resource/AudioSampleCache.d.ts.map +1 -0
  18. package/dist/cache/resource/AudioSampleCache.js +69 -0
  19. package/dist/cache/resource/AudioSampleCache.js.map +1 -0
  20. package/dist/cache/resource/ImageBitmapCache.d.ts +65 -0
  21. package/dist/cache/resource/ImageBitmapCache.d.ts.map +1 -0
  22. package/dist/cache/resource/ImageBitmapCache.js +101 -0
  23. package/dist/cache/resource/ImageBitmapCache.js.map +1 -0
  24. package/dist/cache/resource/MP4IndexCache.d.ts +48 -0
  25. package/dist/cache/resource/MP4IndexCache.d.ts.map +1 -0
  26. package/dist/cache/resource/MP4IndexCache.js +104 -0
  27. package/dist/cache/resource/MP4IndexCache.js.map +1 -0
  28. package/dist/cache/resource/ResourceCache.d.ts +46 -0
  29. package/dist/cache/resource/ResourceCache.d.ts.map +1 -0
  30. package/dist/cache/resource/ResourceCache.js +92 -0
  31. package/dist/cache/resource/ResourceCache.js.map +1 -0
  32. package/dist/cache/storage/indexeddb/ChunkRecordStore.d.ts +75 -0
  33. package/dist/cache/storage/indexeddb/ChunkRecordStore.d.ts.map +1 -0
  34. package/dist/cache/storage/opfs/OPFSManager.d.ts +54 -0
  35. package/dist/cache/storage/opfs/OPFSManager.d.ts.map +1 -0
  36. package/dist/cache/storage/opfs/OPFSManager.js +133 -0
  37. package/dist/cache/storage/opfs/OPFSManager.js.map +1 -0
  38. package/dist/cache/storage/opfs/types.d.ts +16 -0
  39. package/dist/cache/storage/opfs/types.d.ts.map +1 -0
  40. package/dist/config/defaults.d.ts.map +1 -1
  41. package/dist/config/defaults.js +21 -2
  42. package/dist/config/defaults.js.map +1 -1
  43. package/dist/config/types.d.ts +28 -0
  44. package/dist/config/types.d.ts.map +1 -1
  45. package/dist/controllers/ExportController.d.ts +16 -0
  46. package/dist/controllers/ExportController.d.ts.map +1 -0
  47. package/dist/controllers/ExportController.js +44 -0
  48. package/dist/controllers/ExportController.js.map +1 -0
  49. package/dist/controllers/PlaybackController.d.ts +28 -4
  50. package/dist/controllers/PlaybackController.d.ts.map +1 -1
  51. package/dist/controllers/PlaybackController.js +115 -51
  52. package/dist/controllers/PlaybackController.js.map +1 -1
  53. package/dist/controllers/index.d.ts +2 -3
  54. package/dist/controllers/index.d.ts.map +1 -1
  55. package/dist/controllers/types.d.ts +0 -28
  56. package/dist/controllers/types.d.ts.map +1 -1
  57. package/dist/event/events.d.ts +8 -0
  58. package/dist/event/events.d.ts.map +1 -1
  59. package/dist/event/events.js +1 -0
  60. package/dist/event/events.js.map +1 -1
  61. package/dist/model/CompositionModel.d.ts.map +1 -1
  62. package/dist/model/CompositionModel.js +11 -6
  63. package/dist/model/CompositionModel.js.map +1 -1
  64. package/dist/model/RcFrame.d.ts +2 -0
  65. package/dist/model/RcFrame.d.ts.map +1 -1
  66. package/dist/model/RcFrame.js +3 -0
  67. package/dist/model/RcFrame.js.map +1 -1
  68. package/dist/model/types.d.ts +0 -1
  69. package/dist/model/types.d.ts.map +1 -1
  70. package/dist/model/types.js.map +1 -1
  71. package/dist/orchestrator/ExportScheduler.d.ts +35 -0
  72. package/dist/orchestrator/ExportScheduler.d.ts.map +1 -0
  73. package/dist/orchestrator/ExportScheduler.js +241 -0
  74. package/dist/orchestrator/ExportScheduler.js.map +1 -0
  75. package/dist/orchestrator/GlobalAudioSession.d.ts +24 -9
  76. package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
  77. package/dist/orchestrator/GlobalAudioSession.js +149 -152
  78. package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
  79. package/dist/orchestrator/OnDemandVideoSession.d.ts +73 -0
  80. package/dist/orchestrator/OnDemandVideoSession.d.ts.map +1 -0
  81. package/dist/orchestrator/OnDemandVideoSession.js +281 -0
  82. package/dist/orchestrator/OnDemandVideoSession.js.map +1 -0
  83. package/dist/orchestrator/Orchestrator.d.ts +22 -17
  84. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  85. package/dist/orchestrator/Orchestrator.js +244 -312
  86. package/dist/orchestrator/Orchestrator.js.map +1 -1
  87. package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
  88. package/dist/orchestrator/VideoClipSession.js +3 -15
  89. package/dist/orchestrator/VideoClipSession.js.map +1 -1
  90. package/dist/orchestrator/index.d.ts +0 -1
  91. package/dist/orchestrator/index.d.ts.map +1 -1
  92. package/dist/orchestrator/types.d.ts +4 -4
  93. package/dist/orchestrator/types.d.ts.map +1 -1
  94. package/dist/stages/compose/FilterProcessor.d.ts +1 -1
  95. package/dist/stages/compose/FilterProcessor.d.ts.map +1 -1
  96. package/dist/stages/compose/FilterProcessor.js +226 -0
  97. package/dist/stages/compose/FilterProcessor.js.map +1 -0
  98. package/dist/stages/compose/LayerRenderer.d.ts +1 -1
  99. package/dist/stages/compose/LayerRenderer.d.ts.map +1 -1
  100. package/dist/stages/compose/LayerRenderer.js +270 -0
  101. package/dist/stages/compose/LayerRenderer.js.map +1 -0
  102. package/dist/stages/compose/TransitionProcessor.d.ts +1 -1
  103. package/dist/stages/compose/TransitionProcessor.d.ts.map +1 -1
  104. package/dist/stages/compose/TransitionProcessor.js +189 -0
  105. package/dist/stages/compose/TransitionProcessor.js.map +1 -0
  106. package/dist/stages/compose/VideoComposer.d.ts +4 -2
  107. package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
  108. package/dist/stages/compose/VideoComposer.js +229 -0
  109. package/dist/stages/compose/VideoComposer.js.map +1 -0
  110. package/dist/stages/compose/text-renderers/animation-utils.js +76 -0
  111. package/dist/stages/compose/text-renderers/animation-utils.js.map +1 -0
  112. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts +2 -2
  113. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts.map +1 -1
  114. package/dist/stages/compose/text-renderers/basic-text-renderer.js +93 -0
  115. package/dist/stages/compose/text-renderers/basic-text-renderer.js.map +1 -0
  116. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts +1 -1
  117. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts.map +1 -1
  118. package/dist/stages/compose/text-renderers/character-ktv-renderer.js +132 -0
  119. package/dist/stages/compose/text-renderers/character-ktv-renderer.js.map +1 -0
  120. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts +1 -1
  121. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts.map +1 -1
  122. package/dist/stages/compose/text-renderers/word-by-word-renderer.js +128 -0
  123. package/dist/stages/compose/text-renderers/word-by-word-renderer.js.map +1 -0
  124. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts +1 -1
  125. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts.map +1 -1
  126. package/dist/stages/compose/text-renderers/word-fancy-renderer.js +135 -0
  127. package/dist/stages/compose/text-renderers/word-fancy-renderer.js.map +1 -0
  128. package/dist/stages/compose/text-utils/locale-detector.js +16 -0
  129. package/dist/stages/compose/text-utils/locale-detector.js.map +1 -0
  130. package/dist/stages/compose/text-utils/text-metrics.js +21 -0
  131. package/dist/stages/compose/text-utils/text-metrics.js.map +1 -0
  132. package/dist/stages/compose/text-utils/text-wrapper.js +225 -0
  133. package/dist/stages/compose/text-utils/text-wrapper.js.map +1 -0
  134. package/dist/stages/compose/types.d.ts +2 -1
  135. package/dist/stages/compose/types.d.ts.map +1 -1
  136. package/dist/stages/decode/BaseDecoder.js +0 -3
  137. package/dist/stages/decode/BaseDecoder.js.map +1 -1
  138. package/dist/stages/demux/MP4Demuxer.d.ts +5 -0
  139. package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
  140. package/dist/stages/demux/MP4Demuxer.js +281 -0
  141. package/dist/stages/demux/MP4Demuxer.js.map +1 -0
  142. package/dist/stages/demux/MP4IndexParser.d.ts +71 -0
  143. package/dist/stages/demux/MP4IndexParser.d.ts.map +1 -0
  144. package/dist/stages/demux/MP4IndexParser.js +416 -0
  145. package/dist/stages/demux/MP4IndexParser.js.map +1 -0
  146. package/dist/stages/demux/types.d.ts +48 -0
  147. package/dist/stages/demux/types.d.ts.map +1 -1
  148. package/dist/stages/load/ResourceLoader.d.ts +50 -15
  149. package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
  150. package/dist/stages/load/ResourceLoader.js +297 -80
  151. package/dist/stages/load/ResourceLoader.js.map +1 -1
  152. package/dist/stages/load/TaskManager.d.ts +6 -2
  153. package/dist/stages/load/TaskManager.d.ts.map +1 -1
  154. package/dist/stages/load/TaskManager.js +27 -4
  155. package/dist/stages/load/TaskManager.js.map +1 -1
  156. package/dist/stages/load/index.d.ts +0 -1
  157. package/dist/stages/load/index.d.ts.map +1 -1
  158. package/dist/stages/load/types.d.ts +9 -9
  159. package/dist/stages/load/types.d.ts.map +1 -1
  160. package/dist/stages/mux/MP4Muxer.d.ts +2 -2
  161. package/dist/stages/mux/MP4Muxer.d.ts.map +1 -1
  162. package/dist/stages/mux/MP4Muxer.js +24 -13
  163. package/dist/stages/mux/MP4Muxer.js.map +1 -1
  164. package/dist/stages/mux/MuxManager.d.ts +10 -21
  165. package/dist/stages/mux/MuxManager.d.ts.map +1 -1
  166. package/dist/stages/mux/MuxManager.js +21 -162
  167. package/dist/stages/mux/MuxManager.js.map +1 -1
  168. package/dist/stages/mux/index.d.ts +0 -1
  169. package/dist/stages/mux/index.d.ts.map +1 -1
  170. package/dist/utils/binary-search.d.ts +12 -4
  171. package/dist/utils/binary-search.d.ts.map +1 -1
  172. package/dist/utils/binary-search.js +52 -6
  173. package/dist/utils/binary-search.js.map +1 -1
  174. package/dist/workers/{BaseDecoder.BWYu1W0B.js → BaseDecoder.CTW-vr29.js} +1 -4
  175. package/dist/workers/BaseDecoder.CTW-vr29.js.map +1 -0
  176. package/dist/workers/{MP4Demuxer.lMOUMWFh.js → MP4Demuxer.BEa6PLJm.js} +9 -2
  177. package/dist/workers/{MP4Demuxer.lMOUMWFh.js.map → MP4Demuxer.BEa6PLJm.js.map} +1 -1
  178. package/dist/workers/stages/compose/{video-compose.worker.CIeEIJO7.js → video-compose.worker.DHQ8B105.js} +59 -31
  179. package/dist/workers/stages/compose/video-compose.worker.DHQ8B105.js.map +1 -0
  180. package/dist/workers/stages/decode/{audio-decode.worker.DnS17GD9.js → audio-decode.worker.CP8bXXa4.js} +2 -2
  181. package/dist/workers/stages/decode/{audio-decode.worker.DnS17GD9.js.map → audio-decode.worker.CP8bXXa4.js.map} +1 -1
  182. package/dist/workers/stages/decode/{video-decode.worker.BEYsjOXp.js → video-decode.worker.BIspTxgV.js} +2 -2
  183. package/dist/workers/stages/decode/{video-decode.worker.BEYsjOXp.js.map → video-decode.worker.BIspTxgV.js.map} +1 -1
  184. package/dist/workers/stages/demux/{audio-demux.worker.DcurGC8i.js → audio-demux.worker._VRQdLdv.js} +2 -2
  185. package/dist/workers/stages/demux/{audio-demux.worker.DcurGC8i.js.map → audio-demux.worker._VRQdLdv.js.map} +1 -1
  186. package/dist/workers/stages/demux/{video-demux.worker.B1_wntU4.js → video-demux.worker.CSkxGtmx.js} +3 -19
  187. package/dist/workers/stages/demux/video-demux.worker.CSkxGtmx.js.map +1 -0
  188. package/dist/workers/worker-manifest.json +5 -5
  189. package/package.json +1 -1
  190. package/dist/cache/l2/IndexedDBStore.js +0 -180
  191. package/dist/cache/l2/IndexedDBStore.js.map +0 -1
  192. package/dist/cache/l2/L2Cache.js +0 -329
  193. package/dist/cache/l2/L2Cache.js.map +0 -1
  194. package/dist/cache/l2/OPFSStore.js +0 -131
  195. package/dist/cache/l2/OPFSStore.js.map +0 -1
  196. package/dist/controllers/PreRenderService.d.ts +0 -59
  197. package/dist/controllers/PreRenderService.d.ts.map +0 -1
  198. package/dist/controllers/PreRenderService.js +0 -185
  199. package/dist/controllers/PreRenderService.js.map +0 -1
  200. package/dist/controllers/PreRenderTaskQueue.d.ts +0 -21
  201. package/dist/controllers/PreRenderTaskQueue.d.ts.map +0 -1
  202. package/dist/orchestrator/ClipSessionManager.d.ts +0 -70
  203. package/dist/orchestrator/ClipSessionManager.d.ts.map +0 -1
  204. package/dist/orchestrator/ClipSessionManager.js +0 -158
  205. package/dist/orchestrator/ClipSessionManager.js.map +0 -1
  206. package/dist/stages/decode/AudioChunkDecoder.js +0 -169
  207. package/dist/stages/decode/AudioChunkDecoder.js.map +0 -1
  208. package/dist/stages/load/EventHandlers.d.ts +0 -26
  209. package/dist/stages/load/EventHandlers.d.ts.map +0 -1
  210. package/dist/stages/load/EventHandlers.js +0 -42
  211. package/dist/stages/load/EventHandlers.js.map +0 -1
  212. package/dist/stages/mux/OPFSWriter.d.ts +0 -46
  213. package/dist/stages/mux/OPFSWriter.d.ts.map +0 -1
  214. package/dist/utils/BackpressureAdapter.d.ts +0 -26
  215. package/dist/utils/BackpressureAdapter.d.ts.map +0 -1
  216. package/dist/workers/BaseDecoder.BWYu1W0B.js.map +0 -1
  217. package/dist/workers/stages/compose/video-compose.worker.CIeEIJO7.js.map +0 -1
  218. package/dist/workers/stages/demux/video-demux.worker.B1_wntU4.js.map +0 -1
@@ -1,13 +1,13 @@
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();
9
8
  streamEndedClips = /* @__PURE__ */ new Set();
10
9
  deps;
10
+ model = null;
11
11
  audioContext = null;
12
12
  audioSources = [];
13
13
  audioGainNodes = [];
@@ -15,17 +15,47 @@ class GlobalAudioSession {
15
15
  playbackRate = 1;
16
16
  isPlaying = false;
17
17
  currentPlaybackTimeUs = 0;
18
- ensuringFromL2 = /* @__PURE__ */ new Set();
18
+ ensuringClips = /* @__PURE__ */ new Set();
19
+ // Track all decoding attempts (Resource & L2)
19
20
  constructor(deps) {
20
21
  this.deps = deps;
21
- this.mixer = new OfflineAudioMixer(deps.cacheManager, deps.getModel);
22
+ this.mixer = new OfflineAudioMixer(deps.cacheManager, () => this.model);
23
+ }
24
+ setModel(model) {
25
+ this.model = model;
26
+ this.activateAllAudioClips();
22
27
  }
23
28
  onAudioData(message) {
24
29
  const { sessionId, audioData, clipDurationUs } = message;
25
30
  this.deps.cacheManager.putClipAudioData(sessionId, audioData, clipDurationUs);
26
31
  }
32
+ async ensureAudioForTime(timeUs) {
33
+ const model = this.model;
34
+ if (!model) return;
35
+ const activeClips = [];
36
+ for (const track of model.tracks) {
37
+ if (track.kind !== "audio" && track.kind !== "video") continue;
38
+ const clips = model.getClipsAtTime(timeUs, track.id);
39
+ activeClips.push(...clips);
40
+ }
41
+ await Promise.all(
42
+ activeClips.map(async (clip) => {
43
+ if (this.deps.cacheManager.hasClipPCM(clip.id)) return;
44
+ if (!hasResourceId(clip)) return;
45
+ const resource = model.getResource(clip.resourceId);
46
+ if (!resource) return;
47
+ if (this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {
48
+ await this.ensureClipAudio(clip.id);
49
+ return;
50
+ }
51
+ if (this.activeClips.has(clip.id)) {
52
+ await this.waitForClipPCM(clip.id, 2e3);
53
+ }
54
+ })
55
+ );
56
+ }
27
57
  async activateAllAudioClips() {
28
- const model = this.deps.getModel();
58
+ const model = this.model;
29
59
  if (!model) {
30
60
  return;
31
61
  }
@@ -36,11 +66,17 @@ class GlobalAudioSession {
36
66
  if (!isAudioClip(clip)) {
37
67
  throw new Error(`Clip ${clip.id} in audio track is not an audio clip`);
38
68
  }
69
+ if (this.deps.cacheManager.audioSampleCache.has(clip.resourceId)) {
70
+ await this.ensureClipAudio(clip.id);
71
+ this.activeClips.add(clip.id);
72
+ continue;
73
+ }
39
74
  await this.setupAudioPipeline(clip);
40
75
  this.activeClips.add(clip.id);
41
76
  await this.deps.resourceLoader.fetch(clip.resourceId, {
42
77
  priority: "high",
43
78
  sessionId: clip.id,
79
+ clipId: clip.id,
44
80
  trackId: track.id
45
81
  });
46
82
  this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });
@@ -53,8 +89,8 @@ class GlobalAudioSession {
53
89
  return;
54
90
  }
55
91
  this.stopClipAudioSources(clipId);
56
- this.deps.workers.terminate("audioDemux", clipId);
57
- this.deps.workers.terminate("audioDecode", clipId);
92
+ this.deps.workerPool.terminate("audioDemux", clipId);
93
+ this.deps.workerPool.terminate("audioDecode", clipId);
58
94
  this.activeClips.delete(clipId);
59
95
  this.deps.cacheManager.clearClipAudioData(clipId);
60
96
  }
@@ -63,7 +99,7 @@ class GlobalAudioSession {
63
99
  return;
64
100
  }
65
101
  const timeUs = currentTimeUs ?? this.currentPlaybackTimeUs;
66
- const model = this.deps.getModel();
102
+ const model = this.model;
67
103
  if (!model) {
68
104
  return;
69
105
  }
@@ -96,13 +132,13 @@ class GlobalAudioSession {
96
132
  try {
97
133
  source.stop();
98
134
  source.disconnect();
99
- } catch (error) {
135
+ } catch {
100
136
  }
101
137
  }
102
138
  for (const gainNode of gainNodesToDisconnect) {
103
139
  try {
104
140
  gainNode.disconnect();
105
- } catch (error) {
141
+ } catch {
106
142
  }
107
143
  }
108
144
  }
@@ -138,6 +174,7 @@ class GlobalAudioSession {
138
174
  if (audioContext.state === "suspended") {
139
175
  await audioContext.resume();
140
176
  }
177
+ await this.ensureAudioForTime(timeUs);
141
178
  this.isPlaying = true;
142
179
  this.startAllActiveClips(timeUs);
143
180
  }
@@ -159,7 +196,7 @@ class GlobalAudioSession {
159
196
  const source = this.audioSources[i];
160
197
  const clipId = source._meframeClipId;
161
198
  if (clipId && gainNode) {
162
- const model = this.deps.getModel();
199
+ const model = this.model;
163
200
  const clip = model?.findClip(clipId);
164
201
  if (clip && hasAudioConfig(clip)) {
165
202
  const clipVolume = clip.audioConfig?.volume ?? 1;
@@ -180,38 +217,53 @@ class GlobalAudioSession {
180
217
  this.deps.cacheManager.resetAudioCache();
181
218
  this.activeClips.clear();
182
219
  this.streamEndedClips.clear();
183
- this.ensuringFromL2.clear();
184
220
  }
185
221
  /**
186
- * Create export encoded audio stream
222
+ * Mix and encode audio for a specific segment (used by ExportScheduler)
187
223
  */
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);
224
+ async mixAndEncodeSegment(startUs, endUs, onChunk) {
225
+ const mixedBuffer = await this.mixer.mix(startUs, endUs);
226
+ const audioData = this.audioBufferToAudioData(mixedBuffer, startUs);
227
+ if (!audioData) return;
228
+ if (!this.exportEncoder) {
229
+ this.exportEncoder = new AudioChunkEncoder();
230
+ await this.exportEncoder.initialize();
231
+ this.exportEncoderStream = this.exportEncoder.createStream();
232
+ this.exportEncoderWriter = this.exportEncoderStream.writable.getWriter();
233
+ this.startExportEncoderReader(this.exportEncoderStream.readable, onChunk);
234
+ }
235
+ await this.exportEncoderWriter?.write(audioData);
236
+ }
237
+ exportEncoder = null;
238
+ exportEncoderStream = null;
239
+ exportEncoderWriter = null;
240
+ async startExportEncoderReader(stream, onChunk) {
241
+ const reader = stream.getReader();
242
+ try {
243
+ while (true) {
244
+ const { done, value } = await reader.read();
245
+ if (done) break;
246
+ if (value) {
247
+ onChunk(value.chunk, value.metadata);
206
248
  }
207
- })
208
- );
249
+ }
250
+ } catch (e) {
251
+ console.error("Export encoder reader error", e);
252
+ }
253
+ }
254
+ async finalizeExportAudio() {
255
+ if (this.exportEncoderWriter) {
256
+ await this.exportEncoderWriter.close();
257
+ this.exportEncoderWriter = null;
258
+ }
259
+ this.exportEncoder = null;
260
+ this.exportEncoderStream = null;
209
261
  }
210
262
  /**
211
263
  * Create export audio stream
212
264
  */
213
265
  async createExportAudioStream() {
214
- const model = this.deps.getModel();
266
+ const model = this.model;
215
267
  if (!model) {
216
268
  return null;
217
269
  }
@@ -236,7 +288,7 @@ class GlobalAudioSession {
236
288
  });
237
289
  }
238
290
  async waitForAudioClipsReady() {
239
- const model = this.deps.getModel();
291
+ const model = this.model;
240
292
  if (!model) return;
241
293
  const audioClips = model.tracks.filter((track) => track.kind === "audio").flatMap((track) => track.clips);
242
294
  const waitPromises = audioClips.map((clip) => this.waitForClipPCM(clip.id, 1e4));
@@ -332,7 +384,7 @@ class GlobalAudioSession {
332
384
  // Full clip duration
333
385
  );
334
386
  if (!clipPCMData || clipPCMData.planes.length === 0) {
335
- console.warn("[GlobalAudioSession] No PCM data for clip, will retry later", clip.id);
387
+ void this.ensureClipAudio(clip.id);
336
388
  return;
337
389
  }
338
390
  const buffer = this.pcmToAudioBuffer(clipPCMData.planes, clipPCMData.sampleRate);
@@ -347,7 +399,6 @@ class GlobalAudioSession {
347
399
  source.buffer = buffer;
348
400
  source.playbackRate.value = this.playbackRate;
349
401
  source._meframeClipId = clip.id;
350
- source._playedToUs = clip.startUs + buffer.duration * 1e6;
351
402
  const gainNode = this.audioContext.createGain();
352
403
  if (hasAudioConfig(clip)) {
353
404
  const volume = clip.audioConfig?.volume ?? 1;
@@ -365,75 +416,6 @@ class GlobalAudioSession {
365
416
  this.audioSources.splice(index, 1);
366
417
  this.audioGainNodes.splice(index, 1);
367
418
  }
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
419
  };
438
420
  this.audioSources.push(source);
439
421
  this.audioGainNodes.push(gainNode);
@@ -443,20 +425,20 @@ class GlobalAudioSession {
443
425
  try {
444
426
  source.stop();
445
427
  source.disconnect();
446
- } catch (error) {
428
+ } catch {
447
429
  }
448
430
  }
449
431
  for (const gainNode of this.audioGainNodes) {
450
432
  try {
451
433
  gainNode.disconnect();
452
- } catch (error) {
434
+ } catch {
453
435
  }
454
436
  }
455
437
  this.audioSources = [];
456
438
  this.audioGainNodes = [];
457
439
  }
458
440
  getActiveAudioClips(timeUs) {
459
- const model = this.deps.getModel();
441
+ const model = this.model;
460
442
  if (!model) {
461
443
  return [];
462
444
  }
@@ -466,61 +448,76 @@ class GlobalAudioSession {
466
448
  continue;
467
449
  }
468
450
  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
- }
451
+ clips.push(...trackClips);
474
452
  }
475
453
  return clips;
476
454
  }
477
455
  /**
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
456
+ * Ensure PCM for a clip is available
457
+ * Priority: AudioSampleCache (Resource) > L2 > None
480
458
  */
481
- async ensureClipAudioFromL2(clipId) {
459
+ async ensureClipAudio(clipId) {
482
460
  if (this.deps.cacheManager.hasClipPCM(clipId)) {
483
461
  return;
484
462
  }
485
- if (this.ensuringFromL2.has(clipId)) {
463
+ if (this.ensuringClips.has(clipId)) {
486
464
  return;
487
465
  }
488
- const model = this.deps.getModel();
489
- 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;
466
+ this.ensuringClips.add(clipId);
467
+ try {
468
+ await this.ensureClipAudioFromResource(clipId);
469
+ } finally {
470
+ this.ensuringClips.delete(clipId);
495
471
  }
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
- });
472
+ }
473
+ /**
474
+ * Ensure PCM from AudioSampleCache (Resource-level)
475
+ * Returns true if successful
476
+ */
477
+ async ensureClipAudioFromResource(clipId) {
478
+ const model = this.model;
479
+ const clip = model?.findClip(clipId);
480
+ if (!clip) return false;
481
+ const resourceId = clip.resourceId;
482
+ if (!resourceId) return false;
483
+ const audioRecord = this.deps.cacheManager.audioSampleCache.get(resourceId);
484
+ if (!audioRecord) return false;
503
485
  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();
486
+ const clipSamples = audioRecord.samples.filter((s) => {
487
+ const sampleEndUs = s.timestamp + (s.duration ?? 0);
488
+ return s.timestamp < clip.durationUs && sampleEndUs > 0;
489
+ });
490
+ if (clipSamples.length === 0) {
491
+ return false;
492
+ }
493
+ await this.decodeAudioSamples(clipId, clipSamples, audioRecord.metadata, clip.durationUs);
494
+ return true;
518
495
  } catch (error) {
519
- console.error("[GlobalAudioSession] ensureClipAudioFromL2 error:", error);
520
- } finally {
521
- this.ensuringFromL2.delete(clipId);
522
- await decoder.close();
496
+ console.warn(
497
+ `[GlobalAudioSession] Failed to decode audio from resource ${resourceId}:`,
498
+ error
499
+ );
500
+ return false;
501
+ }
502
+ }
503
+ /**
504
+ * Decode audio samples to PCM and cache
505
+ */
506
+ async decodeAudioSamples(clipId, samples, config, clipDurationUs) {
507
+ const decoder = new AudioDecoder({
508
+ output: (audioData) => {
509
+ this.deps.cacheManager.putClipAudioData(clipId, audioData, clipDurationUs);
510
+ },
511
+ error: (error) => {
512
+ console.error(`[GlobalAudioSession] Decoder error for clip ${clipId}:`, error);
513
+ }
514
+ });
515
+ decoder.configure(config);
516
+ for (const sample of samples) {
517
+ decoder.decode(sample);
523
518
  }
519
+ await decoder.flush();
520
+ decoder.close();
524
521
  }
525
522
  pcmToAudioBuffer(planes, sampleRate) {
526
523
  const numberOfChannels = planes.length;
@@ -568,10 +565,10 @@ class GlobalAudioSession {
568
565
  }
569
566
  async setupAudioPipeline(clip) {
570
567
  const { id: clipId, resourceId, startUs, durationUs } = clip;
571
- const audioDemuxWorker = await this.deps.workers.getOrCreate("audioDemux", clipId, {
568
+ const audioDemuxWorker = await this.deps.workerPool.getOrCreate("audioDemux", clipId, {
572
569
  lazy: true
573
570
  });
574
- const audioDecodeWorker = await this.deps.workers.getOrCreate("audioDecode", clipId, {
571
+ const audioDecodeWorker = await this.deps.workerPool.getOrCreate("audioDecode", clipId, {
575
572
  lazy: true
576
573
  });
577
574
  const demuxToDecodeChannel = new MessageChannel();
@@ -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 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 streamEndedClips = new Set<string>();\n private deps: AudioSessionDeps;\n private model: CompositionModel | null = null;\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, () => this.model);\n }\n\n setModel(model: CompositionModel): void {\n this.model = model;\n this.activateAllAudioClips();\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.model;\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.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 // 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.workerPool.terminate('audioDemux', clipId);\n this.deps.workerPool.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.model;\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.model;\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.model;\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.model;\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.model;\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.model;\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.workerPool.getOrCreate('audioDemux', clipId, {\n lazy: true,\n });\n const audioDecodeWorker = await this.deps.workerPool.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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"}