@meframe/core 0.0.4 → 0.0.6
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.
- package/dist/Meframe.d.ts +16 -7
- package/dist/Meframe.d.ts.map +1 -1
- package/dist/Meframe.js +106 -86
- package/dist/Meframe.js.map +1 -1
- package/dist/cache/CacheManager.d.ts +27 -10
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +92 -30
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/cache/L2Cache.d.ts +31 -2
- package/dist/cache/L2Cache.d.ts.map +1 -1
- package/dist/cache/L2Cache.js +242 -44
- package/dist/cache/L2Cache.js.map +1 -1
- package/dist/cache/l1/VideoL1Cache.d.ts +1 -1
- package/dist/cache/l1/VideoL1Cache.js.map +1 -1
- package/dist/controllers/PreRenderService.d.ts +31 -4
- package/dist/controllers/PreRenderService.d.ts.map +1 -1
- package/dist/controllers/PreRenderService.js +130 -10
- package/dist/controllers/PreRenderService.js.map +1 -1
- package/dist/event/events.d.ts +12 -3
- package/dist/event/events.d.ts.map +1 -1
- package/dist/event/events.js +1 -0
- package/dist/event/events.js.map +1 -1
- package/dist/model/CompositionModel.d.ts +1 -1
- package/dist/model/CompositionModel.js +1 -1
- package/dist/model/CompositionModel.js.map +1 -1
- package/dist/model/patch.d.ts +1 -1
- package/dist/model/patch.js.map +1 -1
- package/dist/node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js +1858 -0
- package/dist/node_modules/.pnpm/mp4-muxer@5.2.2/node_modules/mp4-muxer/build/mp4-muxer.js.map +1 -0
- package/dist/orchestrator/ClipSessionManager.d.ts +1 -2
- package/dist/orchestrator/ClipSessionManager.d.ts.map +1 -1
- package/dist/orchestrator/ClipSessionManager.js +1 -0
- package/dist/orchestrator/ClipSessionManager.js.map +1 -1
- package/dist/orchestrator/CompositionPlanner.d.ts +1 -1
- package/dist/orchestrator/CompositionPlanner.js +1 -1
- package/dist/orchestrator/CompositionPlanner.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts +10 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +83 -38
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts +11 -4
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +70 -28
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.d.ts +28 -1
- package/dist/stages/compose/GlobalAudioSession.d.ts.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.js +133 -5
- package/dist/stages/compose/GlobalAudioSession.js.map +1 -1
- package/dist/stages/compose/VideoComposer.d.ts +1 -0
- package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
- package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
- package/dist/stages/encode/AudioChunkEncoder.d.ts +2 -1
- package/dist/stages/encode/AudioChunkEncoder.d.ts.map +1 -1
- package/dist/stages/encode/AudioChunkEncoder.js +41 -0
- package/dist/stages/encode/AudioChunkEncoder.js.map +1 -0
- package/dist/stages/encode/BaseEncoder.d.ts +7 -3
- package/dist/stages/encode/BaseEncoder.d.ts.map +1 -1
- package/dist/stages/encode/BaseEncoder.js +173 -0
- package/dist/stages/encode/BaseEncoder.js.map +1 -0
- package/dist/stages/encode/ClipEncoderManager.d.ts +64 -0
- package/dist/stages/encode/ClipEncoderManager.d.ts.map +1 -0
- package/dist/stages/encode/index.d.ts +1 -1
- package/dist/stages/encode/index.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +17 -12
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/stages/load/TaskManager.d.ts +1 -1
- package/dist/stages/load/TaskManager.d.ts.map +1 -1
- package/dist/stages/load/TaskManager.js +2 -2
- package/dist/stages/load/TaskManager.js.map +1 -1
- package/dist/stages/load/types.d.ts +2 -2
- package/dist/stages/load/types.d.ts.map +1 -1
- package/dist/stages/mux/MP4Muxer.d.ts +19 -38
- package/dist/stages/mux/MP4Muxer.d.ts.map +1 -1
- package/dist/stages/mux/MP4Muxer.js +60 -0
- package/dist/stages/mux/MP4Muxer.js.map +1 -0
- package/dist/stages/mux/MuxManager.d.ts +27 -0
- package/dist/stages/mux/MuxManager.d.ts.map +1 -0
- package/dist/stages/mux/MuxManager.js +148 -0
- package/dist/stages/mux/MuxManager.js.map +1 -0
- package/dist/stages/mux/index.d.ts +1 -0
- package/dist/stages/mux/index.d.ts.map +1 -1
- package/dist/stages/mux/types.d.ts +1 -0
- package/dist/stages/mux/types.d.ts.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/worker/WorkerPool.d.ts.map +1 -1
- package/dist/worker/WorkerPool.js +2 -4
- package/dist/worker/WorkerPool.js.map +1 -1
- package/dist/worker/types.d.ts +1 -4
- package/dist/worker/types.d.ts.map +1 -1
- package/dist/worker/types.js +0 -3
- package/dist/worker/types.js.map +1 -1
- package/dist/worker/worker-event-whitelist.d.ts.map +1 -1
- package/dist/workers/MP4Demuxer.js +7049 -6
- package/dist/workers/MP4Demuxer.js.map +1 -1
- package/dist/workers/WorkerChannel.js +0 -3
- package/dist/workers/WorkerChannel.js.map +1 -1
- package/dist/workers/stages/compose/video-compose.worker.js +25 -14
- package/dist/workers/stages/compose/video-compose.worker.js.map +1 -1
- package/dist/workers/stages/decode/decode.worker.js +37 -28
- package/dist/workers/stages/decode/decode.worker.js.map +1 -1
- package/dist/workers/stages/demux/audio-demux.worker.js +4 -4
- package/dist/workers/stages/demux/audio-demux.worker.js.map +1 -1
- package/dist/workers/stages/demux/video-demux.worker.js +9 -7
- package/dist/workers/stages/demux/video-demux.worker.js.map +1 -1
- package/dist/workers/stages/encode/encode.worker.js +192 -196
- package/dist/workers/stages/encode/encode.worker.js.map +1 -1
- package/package.json +2 -1
- package/dist/stages/encode/EncoderPool.d.ts +0 -28
- package/dist/stages/encode/EncoderPool.d.ts.map +0 -1
- package/dist/workers/mp4box.all.js +0 -7049
- package/dist/workers/mp4box.all.js.map +0 -1
- package/dist/workers/stages/mux/mux.worker.js +0 -501
- package/dist/workers/stages/mux/mux.worker.js.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
class VideoClipSession {
|
|
2
2
|
clipId;
|
|
3
|
+
sessionId;
|
|
3
4
|
planner;
|
|
4
5
|
workerPool;
|
|
5
6
|
cacheManager;
|
|
@@ -22,6 +23,7 @@ class VideoClipSession {
|
|
|
22
23
|
}
|
|
23
24
|
constructor(config) {
|
|
24
25
|
this.clipId = config.clipId;
|
|
26
|
+
this.sessionId = config.sessionId ?? config.clipId;
|
|
25
27
|
this.planner = config.planner;
|
|
26
28
|
this.workerPool = config.workerPool;
|
|
27
29
|
this.cacheManager = config.cacheManager;
|
|
@@ -30,28 +32,34 @@ class VideoClipSession {
|
|
|
30
32
|
this.callbacks = config.callbacks;
|
|
31
33
|
}
|
|
32
34
|
async activate() {
|
|
33
|
-
console.log(">>>>>>>>>>>> activate", this.clipId);
|
|
34
35
|
if (this.isActive || this.isDisposed) return;
|
|
35
36
|
const clip = this.getClip();
|
|
36
37
|
const resource = this.getResource();
|
|
37
38
|
if (!clip || !resource) {
|
|
39
|
+
console.warn("[VideoClipSession] activate: clip or resource not found", this.sessionId);
|
|
38
40
|
return;
|
|
39
41
|
}
|
|
40
42
|
if (resource.type === "video") {
|
|
41
43
|
await this.setupVideoPipeline(clip, resource);
|
|
42
44
|
} else if (resource.type === "image") {
|
|
43
45
|
await this.setupImagePipeline(clip);
|
|
46
|
+
} else {
|
|
47
|
+
console.warn(
|
|
48
|
+
"[VideoClipSession] Unknown resource type:",
|
|
49
|
+
resource.type,
|
|
50
|
+
"for",
|
|
51
|
+
this.sessionId
|
|
52
|
+
);
|
|
44
53
|
}
|
|
45
54
|
await this.ensureInstructions(clip);
|
|
46
55
|
this.isActive = true;
|
|
47
56
|
if (this.callbacks.onPipelineReady) {
|
|
48
|
-
await this.callbacks.onPipelineReady(
|
|
57
|
+
await this.callbacks.onPipelineReady();
|
|
49
58
|
}
|
|
50
59
|
}
|
|
51
60
|
async deactivate() {
|
|
52
61
|
if (!this.isActive || this.isDisposed) return;
|
|
53
62
|
this.isActive = false;
|
|
54
|
-
await this.invalidateClipCache();
|
|
55
63
|
await this.releasePipeline();
|
|
56
64
|
}
|
|
57
65
|
async dispose() {
|
|
@@ -78,7 +86,6 @@ class VideoClipSession {
|
|
|
78
86
|
return;
|
|
79
87
|
}
|
|
80
88
|
if (this.isActive) {
|
|
81
|
-
console.log(">>>>>>>>>>>> restartPipeline", this.clipId);
|
|
82
89
|
await this.restartPipeline(clip, resource, instructions);
|
|
83
90
|
} else {
|
|
84
91
|
await this.activate();
|
|
@@ -104,13 +111,13 @@ class VideoClipSession {
|
|
|
104
111
|
}
|
|
105
112
|
async setupImagePipeline(clip) {
|
|
106
113
|
await this.acquireWorkers(clip, false);
|
|
107
|
-
await this.connectImagePipeline(
|
|
114
|
+
await this.connectImagePipeline();
|
|
108
115
|
}
|
|
109
|
-
async connectImagePipeline(
|
|
116
|
+
async connectImagePipeline() {
|
|
110
117
|
if (!this.visualWorker || !this.encodeWorker) {
|
|
111
118
|
throw new Error("Pipeline workers not ready");
|
|
112
119
|
}
|
|
113
|
-
const
|
|
120
|
+
const sessionId = this.sessionId;
|
|
114
121
|
this.visualToEncodeChannel = new MessageChannel();
|
|
115
122
|
await this.visualWorker.send(
|
|
116
123
|
"connect",
|
|
@@ -118,7 +125,7 @@ class VideoClipSession {
|
|
|
118
125
|
direction: "downstream",
|
|
119
126
|
port: this.visualToEncodeChannel.port1,
|
|
120
127
|
streamType: "video",
|
|
121
|
-
|
|
128
|
+
sessionId
|
|
122
129
|
},
|
|
123
130
|
{ transfer: [this.visualToEncodeChannel.port1] }
|
|
124
131
|
);
|
|
@@ -128,28 +135,46 @@ class VideoClipSession {
|
|
|
128
135
|
direction: "upstream",
|
|
129
136
|
port: this.visualToEncodeChannel.port2,
|
|
130
137
|
streamType: "video",
|
|
131
|
-
|
|
138
|
+
sessionId
|
|
132
139
|
},
|
|
133
140
|
{ transfer: [this.visualToEncodeChannel.port2] }
|
|
134
141
|
);
|
|
135
142
|
this.visualWorker.receiveStream((stream) => {
|
|
136
143
|
const visualConfig = this.workerConfigs.videoCompose ?? {};
|
|
137
144
|
const fps = this.compositionModel.fps ?? visualConfig.fps ?? 30;
|
|
138
|
-
this.callbacks.
|
|
145
|
+
this.callbacks.onComposedStreamReady(stream, fps);
|
|
146
|
+
});
|
|
147
|
+
this.encodeWorker.receiveStream((stream, metadata) => {
|
|
148
|
+
const streamSessionId = metadata?.sessionId;
|
|
149
|
+
if (streamSessionId !== this.sessionId) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (metadata?.streamType === "video") {
|
|
153
|
+
this.callbacks.onEncodedStreamReady(
|
|
154
|
+
stream,
|
|
155
|
+
"video"
|
|
156
|
+
);
|
|
157
|
+
} else if (metadata?.streamType === "audio") {
|
|
158
|
+
this.callbacks.onEncodedStreamReady(
|
|
159
|
+
stream,
|
|
160
|
+
"audio"
|
|
161
|
+
);
|
|
162
|
+
}
|
|
139
163
|
});
|
|
140
164
|
}
|
|
141
165
|
async setupVideoPipeline(clip, resource) {
|
|
142
|
-
console.log(">>>>>>>>>>>> setupVideoPipeline", this.clipId);
|
|
143
166
|
await this.acquireWorkers(clip, true);
|
|
144
|
-
await this.connectVideoPipeline(
|
|
167
|
+
await this.connectVideoPipeline(resource.id);
|
|
145
168
|
}
|
|
146
169
|
async acquireWorkers(clip, isVideo) {
|
|
147
170
|
if (isVideo) {
|
|
148
171
|
this.decodeWorker = await this.workerPool.get("decode");
|
|
149
|
-
this.videoDemuxWorker = await this.workerPool.get("videoDemux",
|
|
172
|
+
this.videoDemuxWorker = await this.workerPool.get("videoDemux", this.sessionId, {
|
|
173
|
+
lazy: true
|
|
174
|
+
});
|
|
150
175
|
}
|
|
151
176
|
this.encodeWorker = await this.workerPool.get("encode");
|
|
152
|
-
this.visualWorker = await this.workerPool.get("videoCompose",
|
|
177
|
+
this.visualWorker = await this.workerPool.get("videoCompose", this.sessionId, { lazy: true });
|
|
153
178
|
const visualConfig = this.workerConfigs.videoCompose ?? {};
|
|
154
179
|
const renderOverrides = this.compositionModel.renderConfig ?? {};
|
|
155
180
|
const timeline = {
|
|
@@ -164,11 +189,11 @@ class VideoClipSession {
|
|
|
164
189
|
config: { ...visualConfig, ...renderOverrides, timeline }
|
|
165
190
|
});
|
|
166
191
|
}
|
|
167
|
-
async connectVideoPipeline(
|
|
192
|
+
async connectVideoPipeline(resourceId) {
|
|
168
193
|
if (!this.visualWorker || !this.decodeWorker || !this.encodeWorker || !this.videoDemuxWorker) {
|
|
169
194
|
throw new Error("Pipeline workers not ready");
|
|
170
195
|
}
|
|
171
|
-
const
|
|
196
|
+
const sessionId = this.sessionId;
|
|
172
197
|
this.visualToEncodeChannel = new MessageChannel();
|
|
173
198
|
await this.visualWorker.send(
|
|
174
199
|
"connect",
|
|
@@ -176,7 +201,7 @@ class VideoClipSession {
|
|
|
176
201
|
direction: "downstream",
|
|
177
202
|
port: this.visualToEncodeChannel.port1,
|
|
178
203
|
streamType: "video",
|
|
179
|
-
|
|
204
|
+
sessionId
|
|
180
205
|
},
|
|
181
206
|
{ transfer: [this.visualToEncodeChannel.port1] }
|
|
182
207
|
);
|
|
@@ -186,7 +211,7 @@ class VideoClipSession {
|
|
|
186
211
|
direction: "upstream",
|
|
187
212
|
port: this.visualToEncodeChannel.port2,
|
|
188
213
|
streamType: "video",
|
|
189
|
-
|
|
214
|
+
sessionId
|
|
190
215
|
},
|
|
191
216
|
{ transfer: [this.visualToEncodeChannel.port2] }
|
|
192
217
|
);
|
|
@@ -197,7 +222,7 @@ class VideoClipSession {
|
|
|
197
222
|
direction: "downstream",
|
|
198
223
|
port: this.decodeToVisualChannel.port1,
|
|
199
224
|
streamType: "video",
|
|
200
|
-
|
|
225
|
+
sessionId
|
|
201
226
|
},
|
|
202
227
|
{ transfer: [this.decodeToVisualChannel.port1] }
|
|
203
228
|
);
|
|
@@ -207,7 +232,7 @@ class VideoClipSession {
|
|
|
207
232
|
direction: "upstream",
|
|
208
233
|
port: this.decodeToVisualChannel.port2,
|
|
209
234
|
streamType: "video",
|
|
210
|
-
|
|
235
|
+
sessionId
|
|
211
236
|
},
|
|
212
237
|
{ transfer: [this.decodeToVisualChannel.port2] }
|
|
213
238
|
);
|
|
@@ -218,7 +243,7 @@ class VideoClipSession {
|
|
|
218
243
|
direction: "downstream",
|
|
219
244
|
port: this.demuxToDecodeChannel.port1,
|
|
220
245
|
streamType: "video",
|
|
221
|
-
|
|
246
|
+
sessionId
|
|
222
247
|
},
|
|
223
248
|
{ transfer: [this.demuxToDecodeChannel.port1] }
|
|
224
249
|
);
|
|
@@ -228,7 +253,7 @@ class VideoClipSession {
|
|
|
228
253
|
direction: "upstream",
|
|
229
254
|
port: this.demuxToDecodeChannel.port2,
|
|
230
255
|
streamType: "video",
|
|
231
|
-
|
|
256
|
+
sessionId
|
|
232
257
|
},
|
|
233
258
|
{ transfer: [this.demuxToDecodeChannel.port2] }
|
|
234
259
|
);
|
|
@@ -236,13 +261,30 @@ class VideoClipSession {
|
|
|
236
261
|
await this.videoDemuxWorker.send("configure", {
|
|
237
262
|
initial: true,
|
|
238
263
|
resourceId,
|
|
239
|
-
|
|
264
|
+
sessionId,
|
|
240
265
|
config: demuxConfig
|
|
241
266
|
});
|
|
242
267
|
this.visualWorker.receiveStream((stream) => {
|
|
243
268
|
const visualConfig = this.workerConfigs.videoCompose ?? {};
|
|
244
269
|
const fps = this.compositionModel.fps ?? visualConfig.fps ?? 30;
|
|
245
|
-
this.callbacks.
|
|
270
|
+
this.callbacks.onComposedStreamReady(stream, fps);
|
|
271
|
+
});
|
|
272
|
+
this.encodeWorker.receiveStream((stream, metadata) => {
|
|
273
|
+
const streamSessionId = metadata?.sessionId;
|
|
274
|
+
if (streamSessionId !== this.sessionId) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (metadata?.streamType === "video") {
|
|
278
|
+
this.callbacks.onEncodedStreamReady(
|
|
279
|
+
stream,
|
|
280
|
+
"video"
|
|
281
|
+
);
|
|
282
|
+
} else if (metadata?.streamType === "audio") {
|
|
283
|
+
this.callbacks.onEncodedStreamReady(
|
|
284
|
+
stream,
|
|
285
|
+
"audio"
|
|
286
|
+
);
|
|
287
|
+
}
|
|
246
288
|
});
|
|
247
289
|
}
|
|
248
290
|
async installInstructions(instructions) {
|
|
@@ -267,7 +309,7 @@ class VideoClipSession {
|
|
|
267
309
|
async releasePipeline() {
|
|
268
310
|
if (this.visualWorker && this.instructionContext) {
|
|
269
311
|
await this.visualWorker.notify("dispose_clip", {
|
|
270
|
-
|
|
312
|
+
sessionId: this.sessionId,
|
|
271
313
|
revision: this.instructionContext.revision
|
|
272
314
|
});
|
|
273
315
|
}
|
|
@@ -281,15 +323,15 @@ class VideoClipSession {
|
|
|
281
323
|
this.decodeToVisualChannel = null;
|
|
282
324
|
this.demuxToDecodeChannel = null;
|
|
283
325
|
if (this.visualStream && this.callbacks.onStreamDisposed) {
|
|
284
|
-
this.callbacks.onStreamDisposed(
|
|
326
|
+
this.callbacks.onStreamDisposed();
|
|
285
327
|
}
|
|
286
328
|
this.visualStream = null;
|
|
287
329
|
if (this.videoDemuxWorker) {
|
|
288
|
-
this.workerPool.terminate("videoDemux", this.
|
|
330
|
+
this.workerPool.terminate("videoDemux", this.sessionId);
|
|
289
331
|
this.videoDemuxWorker = null;
|
|
290
332
|
}
|
|
291
333
|
if (this.visualWorker) {
|
|
292
|
-
this.workerPool.terminate("videoCompose", this.
|
|
334
|
+
this.workerPool.terminate("videoCompose", this.sessionId);
|
|
293
335
|
this.visualWorker = null;
|
|
294
336
|
}
|
|
295
337
|
this.decodeWorker = null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoClipSession.js","sources":["../../src/orchestrator/VideoClipSession.ts"],"sourcesContent":["import type { WorkerPool } from '../worker/WorkerPool';\nimport type { CacheManager } from '../cache/CacheManager';\nimport type { CompositionModel, Clip } from '../model';\nimport type { CompositionPlanner, ClipUpdateResult } from './CompositionPlanner';\nimport type { ClipInstructionSet } from '../stages/compose/instructions';\nimport type { BaseWorker } from '../worker/BaseWorker';\nimport type { WorkerType } from '../worker/types';\n\ninterface VideoClipSessionCallbacks {\n onStreamReady(clipId: string, stream: ReadableStream<VideoFrame>, fps: number): void;\n onStreamDisposed?(clipId: string): void;\n onPipelineReady?(clipId: string): Promise<void>;\n}\n\ninterface VideoClipSessionConfig {\n clipId: string;\n planner: CompositionPlanner;\n workerPool: WorkerPool;\n cacheManager: CacheManager;\n compositionModel: CompositionModel;\n workerConfigs: Record<WorkerType, any>;\n callbacks: VideoClipSessionCallbacks;\n}\n\ninterface InstructionContext {\n revision: number;\n instructions: ClipInstructionSet;\n status: ClipInstructionSet['status'];\n}\n\nexport class VideoClipSession {\n private readonly clipId: string;\n private readonly planner: CompositionPlanner;\n private readonly workerPool: WorkerPool;\n private readonly cacheManager: CacheManager;\n private readonly compositionModel: CompositionModel;\n private readonly workerConfigs: Record<WorkerType, any>;\n private readonly callbacks: VideoClipSessionCallbacks;\n\n private instructionContext: InstructionContext | null = null;\n private visualWorker: BaseWorker | null = null;\n private decodeWorker: BaseWorker | null = null;\n private encodeWorker: BaseWorker | null = null;\n private videoDemuxWorker: BaseWorker | null = null;\n private visualStream: ReadableStream<VideoFrame> | null = null;\n private visualToEncodeChannel: MessageChannel | null = null;\n private decodeToVisualChannel: MessageChannel | null = null;\n private demuxToDecodeChannel: MessageChannel | null = null;\n private isActive = false;\n private isDisposed = false;\n\n static async create(config: VideoClipSessionConfig): Promise<VideoClipSession> {\n return new VideoClipSession(config);\n }\n\n private constructor(config: VideoClipSessionConfig) {\n this.clipId = config.clipId;\n this.planner = config.planner;\n this.workerPool = config.workerPool;\n this.cacheManager = config.cacheManager;\n this.compositionModel = config.compositionModel;\n this.workerConfigs = config.workerConfigs;\n this.callbacks = config.callbacks;\n }\n\n async activate(): Promise<void> {\n console.log('>>>>>>>>>>>> activate', this.clipId);\n if (this.isActive || this.isDisposed) return;\n\n const clip = this.getClip();\n const resource = this.getResource();\n if (!clip || !resource) {\n return;\n }\n\n if (resource.type === 'video') {\n await this.setupVideoPipeline(clip, resource);\n } else if (resource.type === 'image') {\n await this.setupImagePipeline(clip);\n }\n\n await this.ensureInstructions(clip);\n\n this.isActive = true;\n\n // Notify that pipeline is ready - triggers resource loading\n if (this.callbacks.onPipelineReady) {\n await this.callbacks.onPipelineReady(this.clipId);\n }\n }\n\n async deactivate(): Promise<void> {\n if (!this.isActive || this.isDisposed) return;\n this.isActive = false;\n\n await this.invalidateClipCache();\n await this.releasePipeline();\n }\n\n async dispose(): Promise<void> {\n if (this.isDisposed) return;\n await this.deactivate();\n this.planner.releaseClip(this.clipId);\n this.isDisposed = true;\n }\n\n async handlePlannerUpdate(update: ClipUpdateResult): Promise<void> {\n if (this.isDisposed) {\n return;\n }\n\n if (update.type === 'remove') {\n await this.dispose();\n return;\n }\n\n const instructions = update.instructions;\n if (!instructions) {\n return;\n }\n\n const clip = this.getClip();\n const resource = this.getResource();\n if (!clip || !resource || (resource.type !== 'video' && resource.type !== 'image')) {\n return;\n }\n\n // Any update requires pipeline restart (stream is closed after cache eviction)\n if (this.isActive) {\n console.log('>>>>>>>>>>>> restartPipeline', this.clipId);\n await this.restartPipeline(clip, resource, instructions);\n } else {\n await this.activate();\n }\n }\n\n get visualWorkerHandle(): BaseWorker | null {\n return this.visualWorker;\n }\n\n private getClip(): Clip | null {\n return this.compositionModel?.findClip?.(this.clipId) ?? null;\n }\n\n private getResource() {\n const clip = this.getClip();\n if (!clip) return null;\n return this.compositionModel.resources.get(clip.resourceId) || null;\n }\n\n private async ensureInstructions(_clip: Clip): Promise<void> {\n const instructions = this.planner.getInstructions(this.clipId);\n if (!instructions) {\n throw new Error(`No instructions for clip ${this.clipId}`);\n }\n await this.installInstructions(instructions);\n }\n\n private async setupImagePipeline(clip: Clip): Promise<void> {\n await this.acquireWorkers(clip, false);\n await this.connectImagePipeline(clip);\n }\n\n private async connectImagePipeline(clip: Clip): Promise<void> {\n if (!this.visualWorker || !this.encodeWorker) {\n throw new Error('Pipeline workers not ready');\n }\n\n const clipId = clip.id;\n\n this.visualToEncodeChannel = new MessageChannel();\n await this.visualWorker.send(\n 'connect',\n {\n direction: 'downstream',\n port: this.visualToEncodeChannel.port1,\n streamType: 'video',\n clipId,\n },\n { transfer: [this.visualToEncodeChannel.port1] }\n );\n await this.encodeWorker.send(\n 'connect',\n {\n direction: 'upstream',\n port: this.visualToEncodeChannel.port2,\n streamType: 'video',\n clipId,\n },\n { transfer: [this.visualToEncodeChannel.port2] }\n );\n\n this.visualWorker.receiveStream((stream) => {\n const visualConfig = this.workerConfigs.videoCompose ?? {};\n const fps = this.compositionModel.fps ?? visualConfig.fps ?? 30;\n this.callbacks.onStreamReady(this.clipId, stream, fps);\n });\n }\n\n private async setupVideoPipeline(clip: Clip, resource: { id: string }): Promise<void> {\n console.log('>>>>>>>>>>>> setupVideoPipeline', this.clipId);\n await this.acquireWorkers(clip, true);\n await this.connectVideoPipeline(clip, resource.id);\n }\n\n private async acquireWorkers(clip: Clip, isVideo: boolean): Promise<void> {\n if (isVideo) {\n this.decodeWorker = await this.workerPool.get('decode');\n this.videoDemuxWorker = await this.workerPool.get('videoDemux', clip.id, { lazy: true });\n }\n\n this.encodeWorker = await this.workerPool.get('encode');\n this.visualWorker = await this.workerPool.get('videoCompose', clip.id, { lazy: true });\n\n const visualConfig = this.workerConfigs.videoCompose ?? {};\n const renderOverrides = this.compositionModel.renderConfig ?? {};\n const timeline = {\n clipId: clip.id,\n clipStartUs: clip.startUs,\n clipEndUs: clip.startUs + clip.durationUs,\n compositionFps: this.compositionModel.fps ?? visualConfig.fps ?? 30,\n };\n await this.visualWorker.send('configure', {\n initial: true,\n clipId: clip.id,\n config: { ...visualConfig, ...renderOverrides, timeline },\n });\n }\n\n private async connectVideoPipeline(clip: Clip, resourceId: string): Promise<void> {\n if (!this.visualWorker || !this.decodeWorker || !this.encodeWorker || !this.videoDemuxWorker) {\n throw new Error('Pipeline workers not ready');\n }\n\n const clipId = clip.id;\n\n this.visualToEncodeChannel = new MessageChannel();\n await this.visualWorker.send(\n 'connect',\n {\n direction: 'downstream',\n port: this.visualToEncodeChannel.port1,\n streamType: 'video',\n clipId,\n },\n { transfer: [this.visualToEncodeChannel.port1] }\n );\n await this.encodeWorker.send(\n 'connect',\n {\n direction: 'upstream',\n port: this.visualToEncodeChannel.port2,\n streamType: 'video',\n clipId,\n },\n { transfer: [this.visualToEncodeChannel.port2] }\n );\n\n this.decodeToVisualChannel = new MessageChannel();\n await this.decodeWorker.send(\n 'connect',\n {\n direction: 'downstream',\n port: this.decodeToVisualChannel.port1,\n streamType: 'video',\n clipId,\n },\n { transfer: [this.decodeToVisualChannel.port1] }\n );\n await this.visualWorker.send(\n 'connect',\n {\n direction: 'upstream',\n port: this.decodeToVisualChannel.port2,\n streamType: 'video',\n clipId,\n },\n { transfer: [this.decodeToVisualChannel.port2] }\n );\n\n this.demuxToDecodeChannel = new MessageChannel();\n await this.videoDemuxWorker.send(\n 'connect',\n {\n direction: 'downstream',\n port: this.demuxToDecodeChannel.port1,\n streamType: 'video',\n clipId,\n },\n { transfer: [this.demuxToDecodeChannel.port1] }\n );\n await this.decodeWorker.send(\n 'connect',\n {\n direction: 'upstream',\n port: this.demuxToDecodeChannel.port2,\n streamType: 'video',\n clipId,\n },\n { transfer: [this.demuxToDecodeChannel.port2] }\n );\n\n const demuxConfig = this.workerConfigs.videoDemux ?? {};\n await this.videoDemuxWorker.send('configure', {\n initial: true,\n resourceId,\n clipId,\n config: demuxConfig,\n });\n\n this.visualWorker.receiveStream((stream) => {\n const visualConfig = this.workerConfigs.videoCompose ?? {};\n const fps = this.compositionModel.fps ?? visualConfig.fps ?? 30;\n this.callbacks.onStreamReady(this.clipId, stream, fps);\n });\n }\n\n private async installInstructions(instructions: ClipInstructionSet): Promise<void> {\n this.instructionContext = {\n revision: instructions.revision,\n instructions,\n status: instructions.status,\n };\n\n if (this.visualWorker) {\n await this.visualWorker.send('install_instructions', instructions);\n }\n }\n\n private async restartPipeline(\n clip: Clip,\n resource: { id: string; type: string },\n instructions: ClipInstructionSet\n ): Promise<void> {\n await this.releasePipeline();\n\n if (resource.type === 'video') {\n await this.setupVideoPipeline(clip, resource);\n } else if (resource.type === 'image') {\n await this.setupImagePipeline(clip);\n }\n\n await this.installInstructions(instructions);\n }\n\n private async releasePipeline(): Promise<void> {\n if (this.visualWorker && this.instructionContext) {\n await this.visualWorker.notify('dispose_clip', {\n clipId: this.clipId,\n revision: this.instructionContext.revision,\n });\n }\n\n this.visualToEncodeChannel?.port1.close();\n this.visualToEncodeChannel?.port2.close();\n this.decodeToVisualChannel?.port1.close();\n this.decodeToVisualChannel?.port2.close();\n this.demuxToDecodeChannel?.port1.close();\n this.demuxToDecodeChannel?.port2.close();\n\n this.visualToEncodeChannel = null;\n this.decodeToVisualChannel = null;\n this.demuxToDecodeChannel = null;\n\n if (this.visualStream && this.callbacks.onStreamDisposed) {\n this.callbacks.onStreamDisposed(this.clipId);\n }\n this.visualStream = null;\n\n if (this.videoDemuxWorker) {\n this.workerPool.terminate('videoDemux', this.clipId);\n this.videoDemuxWorker = null;\n }\n\n if (this.visualWorker) {\n this.workerPool.terminate('videoCompose', this.clipId);\n this.visualWorker = null;\n }\n\n // Shared workers are not terminated here (decode / encode)\n this.decodeWorker = null;\n this.encodeWorker = null;\n }\n\n private async invalidateClipCache(): Promise<void> {\n await this.cacheManager.invalidateClip(this.clipId);\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VideoClipSession.js","sources":["../../src/orchestrator/VideoClipSession.ts"],"sourcesContent":["import type { WorkerPool } from '../worker/WorkerPool';\nimport type { CacheManager } from '../cache/CacheManager';\nimport type { CompositionModel, Clip } from '../model';\nimport type { CompositionPlanner, ClipUpdateResult } from './CompositionPlanner';\nimport type { ClipInstructionSet } from '../stages/compose/instructions';\nimport type { BaseWorker } from '../worker/BaseWorker';\nimport type { WorkerType } from '../worker/types';\n\ninterface VideoClipSessionCallbacks {\n onComposedStreamReady(stream: ReadableStream<VideoFrame>, fps: number): void;\n onEncodedStreamReady(\n stream: ReadableStream<{ chunk: EncodedVideoChunk; metadata: EncodedVideoChunkMetadata }>,\n track: 'video' | 'audio'\n ): void;\n onStreamDisposed?(): void;\n onPipelineReady?(): Promise<void>;\n}\n\ninterface VideoClipSessionConfig {\n clipId: string;\n sessionId?: string;\n forL2Only?: boolean;\n planner: CompositionPlanner;\n workerPool: WorkerPool;\n cacheManager: CacheManager;\n compositionModel: CompositionModel;\n workerConfigs: Record<WorkerType, any>;\n callbacks: VideoClipSessionCallbacks;\n}\n\ninterface InstructionContext {\n revision: number;\n instructions: ClipInstructionSet;\n status: ClipInstructionSet['status'];\n}\n\nexport class VideoClipSession {\n private readonly clipId: string;\n private readonly sessionId: string;\n private readonly planner: CompositionPlanner;\n private readonly workerPool: WorkerPool;\n private readonly cacheManager: CacheManager;\n private readonly compositionModel: CompositionModel;\n private readonly workerConfigs: Record<WorkerType, any>;\n private readonly callbacks: VideoClipSessionCallbacks;\n\n private instructionContext: InstructionContext | null = null;\n private visualWorker: BaseWorker | null = null;\n private decodeWorker: BaseWorker | null = null;\n private encodeWorker: BaseWorker | null = null;\n private videoDemuxWorker: BaseWorker | null = null;\n private visualStream: ReadableStream<VideoFrame> | null = null;\n private visualToEncodeChannel: MessageChannel | null = null;\n private decodeToVisualChannel: MessageChannel | null = null;\n private demuxToDecodeChannel: MessageChannel | null = null;\n private isActive = false;\n private isDisposed = false;\n\n static async create(config: VideoClipSessionConfig): Promise<VideoClipSession> {\n return new VideoClipSession(config);\n }\n\n private constructor(config: VideoClipSessionConfig) {\n this.clipId = config.clipId;\n this.sessionId = config.sessionId ?? config.clipId;\n this.planner = config.planner;\n this.workerPool = config.workerPool;\n this.cacheManager = config.cacheManager;\n this.compositionModel = config.compositionModel;\n this.workerConfigs = config.workerConfigs;\n this.callbacks = config.callbacks;\n }\n\n async activate(): Promise<void> {\n if (this.isActive || this.isDisposed) return;\n\n const clip = this.getClip();\n const resource = this.getResource();\n if (!clip || !resource) {\n console.warn('[VideoClipSession] activate: clip or resource not found', this.sessionId);\n return;\n }\n\n if (resource.type === 'video') {\n await this.setupVideoPipeline(clip, resource);\n } else if (resource.type === 'image') {\n await this.setupImagePipeline(clip);\n } else {\n console.warn(\n '[VideoClipSession] Unknown resource type:',\n resource.type,\n 'for',\n this.sessionId\n );\n }\n\n await this.ensureInstructions(clip);\n\n this.isActive = true;\n\n // Notify that pipeline is ready - triggers resource loading\n if (this.callbacks.onPipelineReady) {\n await this.callbacks.onPipelineReady();\n }\n }\n\n async deactivate(): Promise<void> {\n if (!this.isActive || this.isDisposed) return;\n this.isActive = false;\n\n await this.releasePipeline();\n }\n\n async dispose(): Promise<void> {\n if (this.isDisposed) return;\n await this.deactivate();\n this.planner.releaseClip(this.clipId);\n this.isDisposed = true;\n }\n\n async handlePlannerUpdate(update: ClipUpdateResult): Promise<void> {\n if (this.isDisposed) {\n return;\n }\n\n if (update.type === 'remove') {\n await this.dispose();\n return;\n }\n\n const instructions = update.instructions;\n if (!instructions) {\n return;\n }\n\n const clip = this.getClip();\n const resource = this.getResource();\n if (!clip || !resource || (resource.type !== 'video' && resource.type !== 'image')) {\n return;\n }\n\n // Any update requires pipeline restart (stream is closed after cache eviction)\n if (this.isActive) {\n await this.restartPipeline(clip, resource, instructions);\n } else {\n await this.activate();\n }\n }\n\n get visualWorkerHandle(): BaseWorker | null {\n return this.visualWorker;\n }\n\n private getClip(): Clip | null {\n return this.compositionModel?.findClip?.(this.clipId) ?? null;\n }\n\n private getResource() {\n const clip = this.getClip();\n if (!clip) return null;\n return this.compositionModel.resources.get(clip.resourceId) || null;\n }\n\n private async ensureInstructions(_clip: Clip): Promise<void> {\n const instructions = this.planner.getInstructions(this.clipId);\n if (!instructions) {\n throw new Error(`No instructions for clip ${this.clipId}`);\n }\n await this.installInstructions(instructions);\n }\n\n private async setupImagePipeline(clip: Clip): Promise<void> {\n await this.acquireWorkers(clip, false);\n await this.connectImagePipeline();\n }\n\n private async connectImagePipeline(): Promise<void> {\n if (!this.visualWorker || !this.encodeWorker) {\n throw new Error('Pipeline workers not ready');\n }\n\n // Use sessionId for worker communication\n const sessionId = this.sessionId;\n\n this.visualToEncodeChannel = new MessageChannel();\n await this.visualWorker.send(\n 'connect',\n {\n direction: 'downstream',\n port: this.visualToEncodeChannel.port1,\n streamType: 'video',\n sessionId,\n },\n { transfer: [this.visualToEncodeChannel.port1] }\n );\n await this.encodeWorker.send(\n 'connect',\n {\n direction: 'upstream',\n port: this.visualToEncodeChannel.port2,\n streamType: 'video',\n sessionId,\n },\n { transfer: [this.visualToEncodeChannel.port2] }\n );\n\n this.visualWorker.receiveStream((stream) => {\n const visualConfig = this.workerConfigs.videoCompose ?? {};\n const fps = this.compositionModel.fps ?? visualConfig.fps ?? 30;\n this.callbacks.onComposedStreamReady(stream, fps);\n });\n\n // Receive encoded chunks from EncodeWorker and forward to callback\n // Note: encodeWorker is shared across sessions\n // Each session registers its own callback with sessionId filtering\n this.encodeWorker.receiveStream((stream, metadata) => {\n // Filter: only process streams for this session\n const streamSessionId = (metadata as any)?.sessionId;\n\n if (streamSessionId !== this.sessionId) {\n return;\n }\n\n if (metadata?.streamType === 'video') {\n this.callbacks.onEncodedStreamReady(\n stream as ReadableStream<{\n chunk: EncodedVideoChunk;\n metadata: EncodedVideoChunkMetadata;\n }>,\n 'video'\n );\n } else if (metadata?.streamType === 'audio') {\n this.callbacks.onEncodedStreamReady(\n stream as ReadableStream<{\n chunk: EncodedVideoChunk;\n metadata: EncodedVideoChunkMetadata;\n }>,\n 'audio'\n );\n }\n });\n }\n\n private async setupVideoPipeline(clip: Clip, resource: { id: string }): Promise<void> {\n await this.acquireWorkers(clip, true);\n await this.connectVideoPipeline(resource.id);\n }\n\n private async acquireWorkers(clip: Clip, isVideo: boolean): Promise<void> {\n if (isVideo) {\n this.decodeWorker = await this.workerPool.get('decode');\n this.videoDemuxWorker = await this.workerPool.get('videoDemux', this.sessionId, {\n lazy: true,\n });\n }\n\n this.encodeWorker = await this.workerPool.get('encode');\n this.visualWorker = await this.workerPool.get('videoCompose', this.sessionId, { lazy: true });\n\n const visualConfig = this.workerConfigs.videoCompose ?? {};\n const renderOverrides = this.compositionModel.renderConfig ?? {};\n const timeline = {\n clipId: clip.id,\n clipStartUs: clip.startUs,\n clipEndUs: clip.startUs + clip.durationUs,\n compositionFps: this.compositionModel.fps ?? visualConfig.fps ?? 30,\n };\n await this.visualWorker.send('configure', {\n initial: true,\n clipId: clip.id,\n config: { ...visualConfig, ...renderOverrides, timeline },\n });\n }\n\n private async connectVideoPipeline(resourceId: string): Promise<void> {\n if (!this.visualWorker || !this.decodeWorker || !this.encodeWorker || !this.videoDemuxWorker) {\n throw new Error('Pipeline workers not ready');\n }\n\n // Use sessionId for worker communication\n const sessionId = this.sessionId;\n\n this.visualToEncodeChannel = new MessageChannel();\n await this.visualWorker.send(\n 'connect',\n {\n direction: 'downstream',\n port: this.visualToEncodeChannel.port1,\n streamType: 'video',\n sessionId,\n },\n { transfer: [this.visualToEncodeChannel.port1] }\n );\n await this.encodeWorker.send(\n 'connect',\n {\n direction: 'upstream',\n port: this.visualToEncodeChannel.port2,\n streamType: 'video',\n sessionId,\n },\n { transfer: [this.visualToEncodeChannel.port2] }\n );\n\n this.decodeToVisualChannel = new MessageChannel();\n await this.decodeWorker.send(\n 'connect',\n {\n direction: 'downstream',\n port: this.decodeToVisualChannel.port1,\n streamType: 'video',\n sessionId,\n },\n { transfer: [this.decodeToVisualChannel.port1] }\n );\n await this.visualWorker.send(\n 'connect',\n {\n direction: 'upstream',\n port: this.decodeToVisualChannel.port2,\n streamType: 'video',\n sessionId,\n },\n { transfer: [this.decodeToVisualChannel.port2] }\n );\n\n this.demuxToDecodeChannel = new MessageChannel();\n await this.videoDemuxWorker.send(\n 'connect',\n {\n direction: 'downstream',\n port: this.demuxToDecodeChannel.port1,\n streamType: 'video',\n sessionId,\n },\n { transfer: [this.demuxToDecodeChannel.port1] }\n );\n await this.decodeWorker.send(\n 'connect',\n {\n direction: 'upstream',\n port: this.demuxToDecodeChannel.port2,\n streamType: 'video',\n sessionId,\n },\n { transfer: [this.demuxToDecodeChannel.port2] }\n );\n\n const demuxConfig = this.workerConfigs.videoDemux ?? {};\n await this.videoDemuxWorker.send('configure', {\n initial: true,\n resourceId,\n sessionId,\n config: demuxConfig,\n });\n\n this.visualWorker.receiveStream((stream) => {\n const visualConfig = this.workerConfigs.videoCompose ?? {};\n const fps = this.compositionModel.fps ?? visualConfig.fps ?? 30;\n this.callbacks.onComposedStreamReady(stream, fps);\n });\n\n // Receive encoded chunks from EncodeWorker and forward to callback\n // Note: encodeWorker is shared across sessions\n // Each session registers its own callback with sessionId filtering\n this.encodeWorker.receiveStream((stream, metadata) => {\n // Filter: only process streams for this session\n const streamSessionId = (metadata as any)?.sessionId;\n\n if (streamSessionId !== this.sessionId) {\n return;\n }\n\n if (metadata?.streamType === 'video') {\n this.callbacks.onEncodedStreamReady(\n stream as ReadableStream<{\n chunk: EncodedVideoChunk;\n metadata: EncodedVideoChunkMetadata;\n }>,\n 'video'\n );\n } else if (metadata?.streamType === 'audio') {\n this.callbacks.onEncodedStreamReady(\n stream as ReadableStream<{\n chunk: EncodedVideoChunk;\n metadata: EncodedVideoChunkMetadata;\n }>,\n 'audio'\n );\n }\n });\n }\n\n private async installInstructions(instructions: ClipInstructionSet): Promise<void> {\n this.instructionContext = {\n revision: instructions.revision,\n instructions,\n status: instructions.status,\n };\n\n if (this.visualWorker) {\n await this.visualWorker.send('install_instructions', instructions);\n }\n }\n\n private async restartPipeline(\n clip: Clip,\n resource: { id: string; type: string },\n instructions: ClipInstructionSet\n ): Promise<void> {\n await this.releasePipeline();\n\n if (resource.type === 'video') {\n await this.setupVideoPipeline(clip, resource);\n } else if (resource.type === 'image') {\n await this.setupImagePipeline(clip);\n }\n\n await this.installInstructions(instructions);\n }\n\n private async releasePipeline(): Promise<void> {\n if (this.visualWorker && this.instructionContext) {\n await this.visualWorker.notify('dispose_clip', {\n sessionId: this.sessionId,\n revision: this.instructionContext.revision,\n });\n }\n\n this.visualToEncodeChannel?.port1.close();\n this.visualToEncodeChannel?.port2.close();\n this.decodeToVisualChannel?.port1.close();\n this.decodeToVisualChannel?.port2.close();\n this.demuxToDecodeChannel?.port1.close();\n this.demuxToDecodeChannel?.port2.close();\n\n this.visualToEncodeChannel = null;\n this.decodeToVisualChannel = null;\n this.demuxToDecodeChannel = null;\n\n if (this.visualStream && this.callbacks.onStreamDisposed) {\n this.callbacks.onStreamDisposed();\n }\n this.visualStream = null;\n\n if (this.videoDemuxWorker) {\n this.workerPool.terminate('videoDemux', this.sessionId);\n this.videoDemuxWorker = null;\n }\n\n if (this.visualWorker) {\n this.workerPool.terminate('videoCompose', this.sessionId);\n this.visualWorker = null;\n }\n\n // Shared workers are not terminated here (decode / encode)\n this.decodeWorker = null;\n this.encodeWorker = null;\n }\n\n async invalidateClipCache(): Promise<void> {\n await this.cacheManager.invalidateClip(this.clipId);\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -7,7 +7,7 @@ import { EventPayloadMap } from '../../event/events';
|
|
|
7
7
|
import { CacheManager } from '../../cache/CacheManager';
|
|
8
8
|
|
|
9
9
|
interface AudioDataMessage {
|
|
10
|
-
|
|
10
|
+
sessionId: string;
|
|
11
11
|
audioData: AudioData;
|
|
12
12
|
clipStartUs: TimeUs;
|
|
13
13
|
clipDurationUs: TimeUs;
|
|
@@ -37,6 +37,33 @@ export declare class GlobalAudioSession {
|
|
|
37
37
|
activateAllAudioClips(): Promise<void>;
|
|
38
38
|
handleAudioStream(stream: ReadableStream<AudioData>, metadata: Record<string, any>): void;
|
|
39
39
|
reset(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Create export encoded audio stream with metadata callback
|
|
42
|
+
* Mixes all audio clips and encodes to EncodedAudioChunk for export
|
|
43
|
+
* Extracts metadata from first chunk and invokes callback
|
|
44
|
+
*/
|
|
45
|
+
createExportEncodedStream(config?: Partial<AudioEncoderConfig>, onFirstMetadata?: (metadata: EncodedAudioChunkMetadata) => void): Promise<ReadableStream<EncodedAudioChunk> | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Create export audio stream
|
|
48
|
+
* Mixes all audio clips for the entire composition and outputs AudioData stream
|
|
49
|
+
*/
|
|
50
|
+
createExportAudioStream(): Promise<ReadableStream<AudioData> | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Wait for audio clips to be decoded and cached
|
|
53
|
+
*/
|
|
54
|
+
private waitForAudioClipsReady;
|
|
55
|
+
/**
|
|
56
|
+
* Wait for clip PCM data to be available
|
|
57
|
+
*/
|
|
58
|
+
private waitForClipPCM;
|
|
59
|
+
/**
|
|
60
|
+
* Convert AudioBuffer to AudioData for encoding
|
|
61
|
+
*/
|
|
62
|
+
private audioBufferToAudioData;
|
|
63
|
+
/**
|
|
64
|
+
* Interleave planar audio data for AudioData constructor
|
|
65
|
+
*/
|
|
66
|
+
private interleavePlanarData;
|
|
40
67
|
private setupAudioPipeline;
|
|
41
68
|
private alignToWindow;
|
|
42
69
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GlobalAudioSession.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/GlobalAudioSession.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,KAAK,EAAE,gBAAgB,EAAQ,MAAM,aAAa,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"GlobalAudioSession.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/GlobalAudioSession.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,KAAK,EAAE,gBAAgB,EAAQ,MAAM,aAAa,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D,UAAU,gBAAgB;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,WAAW,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,gBAAgB;IACxB,YAAY,EAAE,YAAY,CAAC;IAC3B,OAAO,EAAE,UAAU,CAAC;IACpB,cAAc,EAAE,cAAc,CAAC;IAC/B,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IACpC,QAAQ,EAAE,MAAM,gBAAgB,GAAG,IAAI,CAAC;IACxC,kBAAkB,EAAE,MAAM,GAAG,CAAC;CAC/B;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,IAAI,CAAmB;gBAEnB,IAAI,EAAE,gBAAgB;IAKlC,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAKtC,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAwB5D,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAkB3E,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B5C,iBAAiB,CAAC,MAAM,EAAE,cAAc,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IA+BzF,KAAK,IAAI,IAAI;IAKb;;;;OAIG;IACG,yBAAyB,CAC7B,MAAM,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,EACpC,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,yBAAyB,KAAK,IAAI,GAC9D,OAAO,CAAC,cAAc,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAAC;IA6BpD;;;OAGG;IACG,uBAAuB,IAAI,OAAO,CAAC,cAAc,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;IAuC1E;;OAEG;YACW,sBAAsB;IAgBpC;;OAEG;IACH,OAAO,CAAC,cAAc;IAwBtB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAqB9B;;OAEG;IACH,OAAO,CAAC,oBAAoB;YAgBd,kBAAkB;IAiChC,OAAO,CAAC,aAAa;CAGtB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { OfflineAudioMixer } from "./OfflineAudioMixer.js";
|
|
2
2
|
import { MeframeEvent } from "../../event/events.js";
|
|
3
|
+
import { AudioChunkEncoder } from "../encode/AudioChunkEncoder.js";
|
|
3
4
|
class GlobalAudioSession {
|
|
4
5
|
mixWindowUs = 3e6;
|
|
5
6
|
mixer;
|
|
@@ -10,8 +11,8 @@ class GlobalAudioSession {
|
|
|
10
11
|
this.mixer = new OfflineAudioMixer(deps.cacheManager, deps.getModel);
|
|
11
12
|
}
|
|
12
13
|
onAudioData(message) {
|
|
13
|
-
const {
|
|
14
|
-
this.deps.cacheManager.putClipAudioData(
|
|
14
|
+
const { sessionId, audioData, clipStartUs, clipDurationUs } = message;
|
|
15
|
+
this.deps.cacheManager.putClipAudioData(sessionId, audioData, clipStartUs, clipDurationUs);
|
|
15
16
|
}
|
|
16
17
|
async ensureMixedPCM(startUs) {
|
|
17
18
|
const model = this.deps.getModel();
|
|
@@ -61,7 +62,7 @@ class GlobalAudioSession {
|
|
|
61
62
|
this.activeClips.add(clip.id);
|
|
62
63
|
await this.deps.resourceLoader.fetch(clip.resourceId, {
|
|
63
64
|
priority: "high",
|
|
64
|
-
|
|
65
|
+
sessionId: clip.id,
|
|
65
66
|
trackId: track.id
|
|
66
67
|
});
|
|
67
68
|
this.deps.eventBus.emit(MeframeEvent.ClipActivated, { clipId: clip.id });
|
|
@@ -70,7 +71,7 @@ class GlobalAudioSession {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
handleAudioStream(stream, metadata) {
|
|
73
|
-
const
|
|
74
|
+
const sessionId = metadata.sessionId || "unknown";
|
|
74
75
|
const clipStartUs = metadata.clipStartUs ?? 0;
|
|
75
76
|
const clipDurationUs = metadata.clipDurationUs ?? 0;
|
|
76
77
|
const reader = stream.getReader();
|
|
@@ -82,7 +83,7 @@ class GlobalAudioSession {
|
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
85
|
this.onAudioData({
|
|
85
|
-
|
|
86
|
+
sessionId,
|
|
86
87
|
audioData: value,
|
|
87
88
|
clipStartUs,
|
|
88
89
|
clipDurationUs
|
|
@@ -99,6 +100,133 @@ class GlobalAudioSession {
|
|
|
99
100
|
this.deps.cacheManager.resetAudioCache();
|
|
100
101
|
this.activeClips.clear();
|
|
101
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Create export encoded audio stream with metadata callback
|
|
105
|
+
* Mixes all audio clips and encodes to EncodedAudioChunk for export
|
|
106
|
+
* Extracts metadata from first chunk and invokes callback
|
|
107
|
+
*/
|
|
108
|
+
async createExportEncodedStream(config, onFirstMetadata) {
|
|
109
|
+
const audioDataStream = await this.createExportAudioStream();
|
|
110
|
+
if (!audioDataStream) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const encoder = new AudioChunkEncoder(config);
|
|
114
|
+
await encoder.initialize();
|
|
115
|
+
const encodingTransform = encoder.createStream();
|
|
116
|
+
const encodedStream = audioDataStream.pipeThrough(encodingTransform);
|
|
117
|
+
let firstMetadataExtracted = false;
|
|
118
|
+
return encodedStream.pipeThrough(
|
|
119
|
+
new TransformStream({
|
|
120
|
+
transform(encoderChunk, controller) {
|
|
121
|
+
if (!firstMetadataExtracted && onFirstMetadata) {
|
|
122
|
+
onFirstMetadata(encoderChunk.metadata);
|
|
123
|
+
firstMetadataExtracted = true;
|
|
124
|
+
}
|
|
125
|
+
controller.enqueue(encoderChunk.chunk);
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Create export audio stream
|
|
132
|
+
* Mixes all audio clips for the entire composition and outputs AudioData stream
|
|
133
|
+
*/
|
|
134
|
+
async createExportAudioStream() {
|
|
135
|
+
const model = this.deps.getModel();
|
|
136
|
+
if (!model) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const totalDurationUs = model.durationUs;
|
|
140
|
+
await this.activateAllAudioClips();
|
|
141
|
+
await this.waitForAudioClipsReady();
|
|
142
|
+
return new ReadableStream({
|
|
143
|
+
start: async (controller) => {
|
|
144
|
+
const windowSize = 3e6;
|
|
145
|
+
let currentUs = 0;
|
|
146
|
+
while (currentUs < totalDurationUs) {
|
|
147
|
+
const windowEndUs = Math.min(currentUs + windowSize, totalDurationUs);
|
|
148
|
+
const mixedBuffer = await this.mixer.mix(currentUs, windowEndUs);
|
|
149
|
+
const audioData = this.audioBufferToAudioData(mixedBuffer, currentUs);
|
|
150
|
+
if (audioData) {
|
|
151
|
+
controller.enqueue(audioData);
|
|
152
|
+
}
|
|
153
|
+
currentUs = windowEndUs;
|
|
154
|
+
}
|
|
155
|
+
controller.close();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Wait for audio clips to be decoded and cached
|
|
161
|
+
*/
|
|
162
|
+
async waitForAudioClipsReady() {
|
|
163
|
+
const model = this.deps.getModel();
|
|
164
|
+
if (!model) return;
|
|
165
|
+
const audioClips = model.tracks.filter((track) => track.kind === "audio").flatMap((track) => track.clips);
|
|
166
|
+
const waitPromises = audioClips.map(
|
|
167
|
+
(clip) => this.waitForClipPCM(clip.id, 1e3)
|
|
168
|
+
// Wait up to 1s per clip
|
|
169
|
+
);
|
|
170
|
+
await Promise.allSettled(waitPromises);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Wait for clip PCM data to be available
|
|
174
|
+
*/
|
|
175
|
+
waitForClipPCM(clipId, timeoutMs) {
|
|
176
|
+
return new Promise((resolve) => {
|
|
177
|
+
const checkInterval = 50;
|
|
178
|
+
let elapsed = 0;
|
|
179
|
+
const check = () => {
|
|
180
|
+
if (this.deps.cacheManager.hasClipPCM(clipId)) {
|
|
181
|
+
resolve(true);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
elapsed += checkInterval;
|
|
185
|
+
if (elapsed >= timeoutMs) {
|
|
186
|
+
resolve(false);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
setTimeout(check, checkInterval);
|
|
190
|
+
};
|
|
191
|
+
check();
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Convert AudioBuffer to AudioData for encoding
|
|
196
|
+
*/
|
|
197
|
+
audioBufferToAudioData(buffer, timestampUs) {
|
|
198
|
+
const sampleRate = buffer.sampleRate;
|
|
199
|
+
const numberOfChannels = buffer.numberOfChannels;
|
|
200
|
+
const numberOfFrames = buffer.length;
|
|
201
|
+
const planes = [];
|
|
202
|
+
for (let channel = 0; channel < numberOfChannels; channel++) {
|
|
203
|
+
planes.push(buffer.getChannelData(channel));
|
|
204
|
+
}
|
|
205
|
+
return new AudioData({
|
|
206
|
+
format: "f32",
|
|
207
|
+
// interleaved format
|
|
208
|
+
sampleRate,
|
|
209
|
+
numberOfFrames,
|
|
210
|
+
numberOfChannels,
|
|
211
|
+
timestamp: timestampUs,
|
|
212
|
+
data: this.interleavePlanarData(planes)
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Interleave planar audio data for AudioData constructor
|
|
217
|
+
*/
|
|
218
|
+
interleavePlanarData(planes) {
|
|
219
|
+
const numberOfChannels = planes.length;
|
|
220
|
+
const numberOfFrames = planes[0]?.length ?? 0;
|
|
221
|
+
const totalSamples = numberOfChannels * numberOfFrames;
|
|
222
|
+
const interleaved = new Float32Array(totalSamples);
|
|
223
|
+
for (let frame = 0; frame < numberOfFrames; frame++) {
|
|
224
|
+
for (let channel = 0; channel < numberOfChannels; channel++) {
|
|
225
|
+
interleaved[frame * numberOfChannels + channel] = planes[channel][frame];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return interleaved.buffer;
|
|
229
|
+
}
|
|
102
230
|
async setupAudioPipeline(clip) {
|
|
103
231
|
const { id: clipId, resourceId, startUs, durationUs } = clip;
|
|
104
232
|
const audioDemuxWorker = await this.deps.workers.get("audioDemux", clipId, { lazy: true });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GlobalAudioSession.js","sources":["../../../src/stages/compose/GlobalAudioSession.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport { OfflineAudioMixer } from './OfflineAudioMixer';\nimport type { CompositionModel, Clip } from '../../model';\nimport type { WorkerPool } from '../../worker/WorkerPool';\nimport type { ResourceLoader } from '../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';\n\ninterface AudioDataMessage {\n clipId: string;\n audioData: AudioData;\n clipStartUs: TimeUs;\n clipDurationUs: TimeUs;\n}\n\nexport interface AudioPlaybackInfo {\n buffer: AudioBuffer;\n offsetSeconds: number;\n durationSeconds: number;\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 mixWindowUs = 3_000_000;\n private mixer: OfflineAudioMixer;\n private activeClips = new Set<string>();\n private deps: AudioSessionDeps;\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 { clipId, audioData, clipStartUs, clipDurationUs } = message;\n this.deps.cacheManager.putClipAudioData(clipId, audioData, clipStartUs, clipDurationUs);\n }\n\n async ensureMixedPCM(startUs: TimeUs): Promise<AudioBuffer | null> {\n const model = this.deps.getModel();\n if (!model) {\n return null;\n }\n\n const windowStartUs = this.alignToWindow(startUs);\n const windowEndUs = windowStartUs + this.mixWindowUs;\n\n const cached = this.deps.cacheManager.getMixedAudio(windowStartUs, windowEndUs);\n if (cached) {\n return cached;\n }\n\n try {\n const mixedBuffer = await this.mixer.mix(windowStartUs, windowEndUs);\n this.deps.cacheManager.putMixedAudio(windowStartUs, windowEndUs, mixedBuffer);\n return mixedBuffer;\n } catch (error) {\n console.error('[GlobalAudioSession] Mix failed:', error);\n return null;\n }\n }\n\n async prepareAudioForPlayback(startUs: TimeUs): Promise<AudioPlaybackInfo | null> {\n const buffer = await this.ensureMixedPCM(startUs);\n if (!buffer) {\n return null;\n }\n\n const windowStartUs = this.alignToWindow(startUs);\n const offsetUs = startUs - windowStartUs;\n const offsetSeconds = offsetUs / 1_000_000;\n const durationSeconds = buffer.duration - offsetSeconds;\n\n return {\n buffer,\n offsetSeconds,\n durationSeconds,\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 await this.setupAudioPipeline(clip);\n this.activeClips.add(clip.id);\n\n await this.deps.resourceLoader.fetch(clip.resourceId, {\n priority: 'high',\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 handleAudioStream(stream: ReadableStream<AudioData>, metadata: Record<string, any>): void {\n const clipId = metadata.clipId || '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 reader.releaseLock();\n return;\n }\n\n this.onAudioData({\n clipId,\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 reset(): void {\n this.deps.cacheManager.resetAudioCache();\n this.activeClips.clear();\n }\n\n private async setupAudioPipeline(clip: Clip): Promise<void> {\n const { id: clipId, resourceId, startUs, durationUs } = clip;\n const audioDemuxWorker = await this.deps.workers.get('audioDemux', clipId, { lazy: true });\n const decodeWorker = await this.deps.workers.get('decode');\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 decodeWorker.send(\n 'connect',\n {\n direction: 'upstream',\n port: demuxToDecodeChannel.port2,\n streamType: 'audio',\n clipId,\n clipStartUs: startUs || 0,\n clipDurationUs: durationUs || 0,\n },\n { transfer: [demuxToDecodeChannel.port2] }\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 private alignToWindow(timeUs: TimeUs): TimeUs {\n return Math.floor(timeUs / this.mixWindowUs) * this.mixWindowUs;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GlobalAudioSession.js","sources":["../../../src/stages/compose/GlobalAudioSession.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport { OfflineAudioMixer } from './OfflineAudioMixer';\nimport type { CompositionModel, Clip } from '../../model';\nimport type { WorkerPool } from '../../worker/WorkerPool';\nimport type { ResourceLoader } from '../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 '../encode/AudioChunkEncoder';\n\ninterface AudioDataMessage {\n sessionId: string;\n audioData: AudioData;\n clipStartUs: TimeUs;\n clipDurationUs: TimeUs;\n}\n\nexport interface AudioPlaybackInfo {\n buffer: AudioBuffer;\n offsetSeconds: number;\n durationSeconds: number;\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 mixWindowUs = 3_000_000;\n private mixer: OfflineAudioMixer;\n private activeClips = new Set<string>();\n private deps: AudioSessionDeps;\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, clipStartUs, clipDurationUs } = message;\n this.deps.cacheManager.putClipAudioData(sessionId, audioData, clipStartUs, clipDurationUs);\n }\n\n async ensureMixedPCM(startUs: TimeUs): Promise<AudioBuffer | null> {\n const model = this.deps.getModel();\n if (!model) {\n return null;\n }\n\n const windowStartUs = this.alignToWindow(startUs);\n const windowEndUs = windowStartUs + this.mixWindowUs;\n\n const cached = this.deps.cacheManager.getMixedAudio(windowStartUs, windowEndUs);\n if (cached) {\n return cached;\n }\n\n try {\n const mixedBuffer = await this.mixer.mix(windowStartUs, windowEndUs);\n this.deps.cacheManager.putMixedAudio(windowStartUs, windowEndUs, mixedBuffer);\n return mixedBuffer;\n } catch (error) {\n console.error('[GlobalAudioSession] Mix failed:', error);\n return null;\n }\n }\n\n async prepareAudioForPlayback(startUs: TimeUs): Promise<AudioPlaybackInfo | null> {\n const buffer = await this.ensureMixedPCM(startUs);\n if (!buffer) {\n return null;\n }\n\n const windowStartUs = this.alignToWindow(startUs);\n const offsetUs = startUs - windowStartUs;\n const offsetSeconds = offsetUs / 1_000_000;\n const durationSeconds = buffer.duration - offsetSeconds;\n\n return {\n buffer,\n offsetSeconds,\n durationSeconds,\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 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 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 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 reset(): void {\n this.deps.cacheManager.resetAudioCache();\n this.activeClips.clear();\n }\n\n /**\n * Create export encoded audio stream with metadata callback\n * Mixes all audio clips and encodes to EncodedAudioChunk for export\n * Extracts metadata from first chunk and invokes callback\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 // Extract chunk from EncoderChunk and capture first metadata\n return encodedStream.pipeThrough(\n new TransformStream({\n transform(encoderChunk, controller) {\n // Extract metadata from first chunk\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 * Mixes all audio clips for the entire composition and outputs AudioData 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 // Ensure all audio clips are active and cached\n await this.activateAllAudioClips();\n\n // Wait for PCM data to be available\n await this.waitForAudioClipsReady();\n\n return new ReadableStream<AudioData>({\n start: async (controller) => {\n const windowSize = 3_000_000; // 3 seconds per chunk\n let currentUs = 0;\n\n while (currentUs < totalDurationUs) {\n const windowEndUs = Math.min(currentUs + windowSize, totalDurationUs);\n\n // Mix this window\n const mixedBuffer = await this.mixer.mix(currentUs, windowEndUs);\n\n // Convert AudioBuffer to AudioData\n const audioData = this.audioBufferToAudioData(mixedBuffer, currentUs);\n if (audioData) {\n controller.enqueue(audioData);\n }\n\n currentUs = windowEndUs;\n }\n\n controller.close();\n },\n });\n }\n\n /**\n * Wait for audio clips to be decoded and cached\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 // Wait for each clip to have PCM data\n const waitPromises = audioClips.map(\n (clip) => this.waitForClipPCM(clip.id, 1000) // Wait up to 1s per clip\n );\n\n await Promise.allSettled(waitPromises);\n }\n\n /**\n * Wait for clip PCM data to be available\n */\n private waitForClipPCM(clipId: string, timeoutMs: number): Promise<boolean> {\n return new Promise((resolve) => {\n const checkInterval = 50;\n let elapsed = 0;\n\n const check = () => {\n if (this.deps.cacheManager.hasClipPCM(clipId)) {\n resolve(true);\n return;\n }\n\n elapsed += checkInterval;\n if (elapsed >= timeoutMs) {\n resolve(false);\n return;\n }\n\n setTimeout(check, checkInterval);\n };\n\n check();\n });\n }\n\n /**\n * Convert AudioBuffer to AudioData for encoding\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 // Interleave channels into planar format\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 /**\n * Interleave planar audio data for AudioData constructor\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: Clip): Promise<void> {\n const { id: clipId, resourceId, startUs, durationUs } = clip;\n const audioDemuxWorker = await this.deps.workers.get('audioDemux', clipId, { lazy: true });\n const decodeWorker = await this.deps.workers.get('decode');\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 decodeWorker.send(\n 'connect',\n {\n direction: 'upstream',\n port: demuxToDecodeChannel.port2,\n streamType: 'audio',\n clipId,\n clipStartUs: startUs || 0,\n clipDurationUs: durationUs || 0,\n },\n { transfer: [demuxToDecodeChannel.port2] }\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 private alignToWindow(timeUs: TimeUs): TimeUs {\n return Math.floor(timeUs / this.mixWindowUs) * this.mixWindowUs;\n }\n}\n"],"names":[],"mappings":"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"}
|