@meframe/core 0.0.31 → 0.0.33
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 +2 -2
- package/dist/Meframe.d.ts.map +1 -1
- package/dist/Meframe.js +3 -2
- package/dist/Meframe.js.map +1 -1
- package/dist/cache/CacheManager.d.ts +12 -17
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +18 -280
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/cache/l1/AudioL1Cache.d.ts +36 -19
- package/dist/cache/l1/AudioL1Cache.d.ts.map +1 -1
- package/dist/cache/l1/AudioL1Cache.js +182 -282
- package/dist/cache/l1/AudioL1Cache.js.map +1 -1
- package/dist/controllers/PlaybackController.d.ts +5 -3
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +58 -16
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/event/events.d.ts +1 -1
- package/dist/event/events.d.ts.map +1 -1
- package/dist/event/events.js.map +1 -1
- package/dist/model/CompositionModel.d.ts +8 -0
- package/dist/model/CompositionModel.d.ts.map +1 -1
- package/dist/model/CompositionModel.js +18 -0
- package/dist/model/CompositionModel.js.map +1 -1
- package/dist/model/types.d.ts +0 -4
- package/dist/model/types.d.ts.map +1 -1
- package/dist/model/types.js.map +1 -1
- package/dist/orchestrator/ExportScheduler.d.ts +10 -0
- package/dist/orchestrator/ExportScheduler.d.ts.map +1 -1
- package/dist/orchestrator/ExportScheduler.js +66 -83
- package/dist/orchestrator/ExportScheduler.js.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.d.ts +35 -28
- package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.js +213 -422
- package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
- package/dist/orchestrator/OnDemandVideoSession.d.ts +3 -3
- package/dist/orchestrator/OnDemandVideoSession.d.ts.map +1 -1
- package/dist/orchestrator/OnDemandVideoSession.js +4 -4
- package/dist/orchestrator/OnDemandVideoSession.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts +11 -4
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +75 -68
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts +0 -2
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +0 -49
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.d.ts.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.js +13 -18
- package/dist/stages/compose/OfflineAudioMixer.js.map +1 -1
- package/dist/stages/decode/AudioChunkDecoder.js +169 -0
- package/dist/stages/decode/AudioChunkDecoder.js.map +1 -0
- package/dist/stages/demux/MP3FrameParser.js +186 -0
- package/dist/stages/demux/MP3FrameParser.js.map +1 -0
- package/dist/stages/load/ResourceLoader.d.ts +49 -30
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +255 -189
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/stages/load/TaskManager.d.ts +4 -0
- package/dist/stages/load/TaskManager.d.ts.map +1 -1
- package/dist/stages/load/TaskManager.js +11 -0
- package/dist/stages/load/TaskManager.js.map +1 -1
- package/dist/stages/load/types.d.ts +1 -0
- package/dist/stages/load/types.d.ts.map +1 -1
- package/dist/utils/audio-data.d.ts +16 -0
- package/dist/utils/audio-data.d.ts.map +1 -0
- package/dist/utils/audio-data.js +111 -0
- package/dist/utils/audio-data.js.map +1 -0
- package/package.json +1 -1
- package/dist/cache/resource/ImageBitmapCache.d.ts +0 -65
- package/dist/cache/resource/ImageBitmapCache.d.ts.map +0 -1
- package/dist/cache/resource/ImageBitmapCache.js +0 -101
- package/dist/cache/resource/ImageBitmapCache.js.map +0 -1
|
@@ -4,12 +4,7 @@ import { StreamFactory } from "./StreamFactory.js";
|
|
|
4
4
|
import { MeframeEvent } from "../../event/events.js";
|
|
5
5
|
import { createImageBitmapFromBlob } from "../../utils/image-utils.js";
|
|
6
6
|
import { MP4IndexParser } from "../demux/MP4IndexParser.js";
|
|
7
|
-
|
|
8
|
-
constructor(message) {
|
|
9
|
-
super(message);
|
|
10
|
-
this.name = "ResourceConflictError";
|
|
11
|
-
}
|
|
12
|
-
}
|
|
7
|
+
import { MP3FrameParser } from "../demux/MP3FrameParser.js";
|
|
13
8
|
class ResourceLoader {
|
|
14
9
|
cacheManager;
|
|
15
10
|
workerPool;
|
|
@@ -19,19 +14,16 @@ class ResourceLoader {
|
|
|
19
14
|
eventBus;
|
|
20
15
|
onStateChange;
|
|
21
16
|
blobCache = /* @__PURE__ */ new Map();
|
|
22
|
-
pendingTransfers = /* @__PURE__ */ new Map();
|
|
23
17
|
parsingIndexes = /* @__PURE__ */ new Set();
|
|
24
18
|
// Track in-progress index parsing
|
|
25
19
|
// Preloading state
|
|
26
20
|
isPreloadingEnabled = true;
|
|
27
21
|
preloadQueue = [];
|
|
28
|
-
activePreloads = /* @__PURE__ */ new Set();
|
|
29
|
-
// TODO: make this configurable
|
|
30
|
-
IDLE_PRELOAD_CONCURRENCY = 2;
|
|
31
22
|
constructor(options) {
|
|
32
|
-
const
|
|
23
|
+
const config = options.config || {};
|
|
24
|
+
const maxConcurrent = config.maxConcurrent ?? 2;
|
|
33
25
|
this.taskManager = new TaskManager(maxConcurrent);
|
|
34
|
-
this.streamFactory = new StreamFactory(options.onProgress,
|
|
26
|
+
this.streamFactory = new StreamFactory(options.onProgress, config);
|
|
35
27
|
this.eventBus = options.eventBus;
|
|
36
28
|
this.onStateChange = options.onStateChange;
|
|
37
29
|
this.cacheManager = options.cacheManager;
|
|
@@ -41,7 +33,7 @@ class ResourceLoader {
|
|
|
41
33
|
this.model = model;
|
|
42
34
|
const mainTrack = model.tracks.find((track) => track.id === (model.mainTrackId || "main"));
|
|
43
35
|
if (mainTrack?.clips?.[0] && hasResourceId(mainTrack.clips[0])) {
|
|
44
|
-
await this.
|
|
36
|
+
await this.load(mainTrack.clips[0].resourceId, {
|
|
45
37
|
priority: "high",
|
|
46
38
|
clipId: mainTrack.clips[0].id,
|
|
47
39
|
trackId: mainTrack.id
|
|
@@ -53,8 +45,6 @@ class ResourceLoader {
|
|
|
53
45
|
this.isPreloadingEnabled = enabled;
|
|
54
46
|
if (enabled) {
|
|
55
47
|
this.startPreloading();
|
|
56
|
-
} else {
|
|
57
|
-
this.preloadQueue = [];
|
|
58
48
|
}
|
|
59
49
|
}
|
|
60
50
|
startPreloading() {
|
|
@@ -63,7 +53,6 @@ class ResourceLoader {
|
|
|
63
53
|
(track) => track.id === (this.model?.mainTrackId || "main")
|
|
64
54
|
);
|
|
65
55
|
if (!mainTrack) return;
|
|
66
|
-
const newQueue = [];
|
|
67
56
|
for (const clip of mainTrack.clips) {
|
|
68
57
|
if (!hasResourceId(clip)) continue;
|
|
69
58
|
const resource = this.model.getResource(clip.resourceId);
|
|
@@ -71,56 +60,27 @@ class ResourceLoader {
|
|
|
71
60
|
if (!resource || resource.state === "ready" || resource.state === "loading" || resource.state === "error") {
|
|
72
61
|
continue;
|
|
73
62
|
}
|
|
74
|
-
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
newQueue.push(resource.id);
|
|
63
|
+
this.load(resource.id, { priority: "low" });
|
|
78
64
|
}
|
|
79
|
-
this.preloadQueue = newQueue;
|
|
80
|
-
this.processPreloadQueue();
|
|
81
65
|
}
|
|
82
66
|
processPreloadQueue() {
|
|
83
67
|
if (!this.isPreloadingEnabled || this.preloadQueue.length === 0) return;
|
|
84
|
-
while (this.
|
|
68
|
+
while (this.preloadQueue.length > 0) {
|
|
85
69
|
const resourceId = this.preloadQueue.shift();
|
|
86
70
|
if (!resourceId) break;
|
|
87
|
-
this.
|
|
88
|
-
this.fetch(resourceId, { priority: "low" }).finally(() => {
|
|
89
|
-
this.activePreloads.delete(resourceId);
|
|
71
|
+
this.load(resourceId, { priority: "low" }).finally(() => {
|
|
90
72
|
this.processPreloadQueue();
|
|
91
73
|
});
|
|
92
74
|
}
|
|
93
75
|
}
|
|
94
|
-
enqueueLoad(resource, priority = "normal", sessionId, clipId, trackId
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
throw new ResourceConflictError(
|
|
99
|
-
`Resource ${resource.id} is being loaded by another session. Preview channel has priority.`
|
|
100
|
-
);
|
|
101
|
-
} else {
|
|
102
|
-
if (this.blobCache.has(resource.id)) {
|
|
103
|
-
void this.transferCachedImage(resource.id, sessionId);
|
|
104
|
-
return;
|
|
105
|
-
} else {
|
|
106
|
-
this.registerPendingTransfer(resource.id, sessionId);
|
|
107
|
-
console.debug(
|
|
108
|
-
`[ResourceLoader] Attachment resource ${resource.id} loading, registered for pending transfer`
|
|
109
|
-
);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
76
|
+
enqueueLoad(resource, priority = "normal", sessionId, clipId, trackId) {
|
|
77
|
+
const existingTask = this.taskManager.getActiveTask(resource.id);
|
|
78
|
+
if (existingTask) {
|
|
79
|
+
return existingTask;
|
|
113
80
|
}
|
|
114
|
-
this.taskManager.enqueue(resource, priority, sessionId, clipId, trackId);
|
|
81
|
+
const task = this.taskManager.enqueue(resource, priority, sessionId, clipId, trackId);
|
|
115
82
|
this.processQueue();
|
|
116
|
-
|
|
117
|
-
registerPendingTransfer(resourceId, sessionId) {
|
|
118
|
-
if (!sessionId) return;
|
|
119
|
-
const pending = this.pendingTransfers.get(resourceId) || [];
|
|
120
|
-
if (!pending.includes(sessionId)) {
|
|
121
|
-
pending.push(sessionId);
|
|
122
|
-
this.pendingTransfers.set(resourceId, pending);
|
|
123
|
-
}
|
|
83
|
+
return task;
|
|
124
84
|
}
|
|
125
85
|
processQueue() {
|
|
126
86
|
while (this.taskManager.canProcess) {
|
|
@@ -129,6 +89,103 @@ class ResourceLoader {
|
|
|
129
89
|
this.startLoad(task);
|
|
130
90
|
}
|
|
131
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Check if resource is cached and ready (without loading)
|
|
94
|
+
*/
|
|
95
|
+
async isResourceCached(resourceId, type) {
|
|
96
|
+
switch (type) {
|
|
97
|
+
case "video": {
|
|
98
|
+
const hasOPFS = await this.cacheManager.hasResourceInCache(resourceId);
|
|
99
|
+
const hasIndex = this.cacheManager.mp4IndexCache.has(resourceId);
|
|
100
|
+
return hasOPFS && hasIndex;
|
|
101
|
+
}
|
|
102
|
+
case "audio":
|
|
103
|
+
return this.cacheManager.audioSampleCache.has(resourceId);
|
|
104
|
+
case "image":
|
|
105
|
+
return this.blobCache.has(resourceId);
|
|
106
|
+
case "json":
|
|
107
|
+
case "text":
|
|
108
|
+
return this.blobCache.has(resourceId);
|
|
109
|
+
default:
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Transfer video stream to worker
|
|
115
|
+
*/
|
|
116
|
+
async transferVideoToWorker(resourceId, sessionId) {
|
|
117
|
+
const stream = await this.createOPFSReadStream(resourceId);
|
|
118
|
+
const demuxWorker = await this.workerPool.get("videoDemux", sessionId);
|
|
119
|
+
if (demuxWorker) {
|
|
120
|
+
await demuxWorker.sendStream(stream, {
|
|
121
|
+
sessionId,
|
|
122
|
+
resourceId
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
stream.cancel();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Transfer image to worker
|
|
130
|
+
*/
|
|
131
|
+
async transferImageToWorker(resourceId, sessionId, imageBitmap) {
|
|
132
|
+
const composeWorker = await this.workerPool.get("videoCompose", sessionId);
|
|
133
|
+
await composeWorker?.send?.(
|
|
134
|
+
"receive_image",
|
|
135
|
+
{ resourceId, sessionId, imageBitmap },
|
|
136
|
+
{ transfer: [imageBitmap] }
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Load video resource (download + cache or read from cache)
|
|
141
|
+
*/
|
|
142
|
+
async loadVideoResource(task) {
|
|
143
|
+
const cached = await this.cacheManager.hasResourceInCache(task.resourceId);
|
|
144
|
+
if (cached) {
|
|
145
|
+
await this.ensureIndexParsed(task.resourceId);
|
|
146
|
+
if (task.sessionId) {
|
|
147
|
+
await this.transferVideoToWorker(task.resourceId, task.sessionId);
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
await this.loadWithOPFSCache(task);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Load audio resource (download + parse or reuse cache)
|
|
155
|
+
*/
|
|
156
|
+
async loadAudioResource(task) {
|
|
157
|
+
if (!this.cacheManager.audioSampleCache.has(task.resourceId)) {
|
|
158
|
+
await this.loadAndParseAudioFile(task);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Load image resource (download + cache or read from cache) and transfer to worker
|
|
163
|
+
*/
|
|
164
|
+
async loadImageResource(task) {
|
|
165
|
+
let blob = this.blobCache.get(task.resourceId);
|
|
166
|
+
if (!blob) {
|
|
167
|
+
if (task.controller) {
|
|
168
|
+
blob = await this.fetchBlob(task.resource.uri, task.controller.signal);
|
|
169
|
+
this.blobCache.set(task.resourceId, blob);
|
|
170
|
+
} else {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const imageBitmap = await createImageBitmapFromBlob(blob);
|
|
175
|
+
if (task.sessionId) {
|
|
176
|
+
await this.transferImageToWorker(task.resourceId, task.sessionId, imageBitmap);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
return imageBitmap;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Load text resource (json/text)
|
|
183
|
+
*/
|
|
184
|
+
async loadTextResource(task) {
|
|
185
|
+
if (task.controller) {
|
|
186
|
+
await this.fetchBlob(task.resource.uri, task.controller.signal);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
132
189
|
/**
|
|
133
190
|
* Start loading a resource
|
|
134
191
|
* Handles state management (loading → ready/error) for all resource types
|
|
@@ -139,29 +196,24 @@ class ResourceLoader {
|
|
|
139
196
|
try {
|
|
140
197
|
this.updateResourceState(task.resourceId, "loading");
|
|
141
198
|
task.controller = new AbortController();
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
await this.ensureIndexParsed(task.resourceId);
|
|
148
|
-
if (task.sessionId) {
|
|
149
|
-
const stream = await this.createOPFSReadStream(task.resourceId);
|
|
150
|
-
task.stream = stream;
|
|
151
|
-
await this.transferToDemuxWorker(task);
|
|
199
|
+
switch (task.resource.type) {
|
|
200
|
+
case "image": {
|
|
201
|
+
const image = await this.loadImageResource(task);
|
|
202
|
+
if (image) {
|
|
203
|
+
image.close();
|
|
152
204
|
}
|
|
153
|
-
|
|
154
|
-
await this.loadWithOPFSCache(task);
|
|
155
|
-
}
|
|
156
|
-
} else if (task.resource.type === "audio") {
|
|
157
|
-
const stream = await this.streamFactory.createRegularStream(task);
|
|
158
|
-
if (!stream) {
|
|
159
|
-
throw new Error(`Failed to create stream for ${task.resourceId}`);
|
|
205
|
+
break;
|
|
160
206
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
207
|
+
case "video":
|
|
208
|
+
await this.loadVideoResource(task);
|
|
209
|
+
break;
|
|
210
|
+
case "audio":
|
|
211
|
+
await this.loadAudioResource(task);
|
|
212
|
+
break;
|
|
213
|
+
case "json":
|
|
214
|
+
case "text":
|
|
215
|
+
await this.loadTextResource(task);
|
|
216
|
+
break;
|
|
165
217
|
}
|
|
166
218
|
this.updateResourceState(task.resourceId, "ready");
|
|
167
219
|
} catch (error) {
|
|
@@ -258,6 +310,44 @@ class ResourceLoader {
|
|
|
258
310
|
async writeToOPFS(resourceId, stream) {
|
|
259
311
|
await this.cacheManager.resourceCache.writeResource(resourceId, stream);
|
|
260
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Load and parse audio file (MP3/WAV) in main thread
|
|
315
|
+
* Extract EncodedAudioChunk and cache to AudioSampleCache
|
|
316
|
+
* Aligned with video audio track extraction (unified architecture)
|
|
317
|
+
*/
|
|
318
|
+
async loadAndParseAudioFile(task) {
|
|
319
|
+
const { resourceId } = task;
|
|
320
|
+
try {
|
|
321
|
+
const blob = await this.fetchBlob(task.resource.uri, task.controller.signal);
|
|
322
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
323
|
+
const uint8Array = new Uint8Array(arrayBuffer);
|
|
324
|
+
const parser = new MP3FrameParser();
|
|
325
|
+
const { frames, config } = parser.push(uint8Array);
|
|
326
|
+
const remainingFrames = parser.flush();
|
|
327
|
+
const allFrames = [...frames, ...remainingFrames];
|
|
328
|
+
if (!config) {
|
|
329
|
+
throw new Error(`Failed to parse audio config for ${resourceId}`);
|
|
330
|
+
}
|
|
331
|
+
const audioChunks = allFrames.map((frame) => {
|
|
332
|
+
return new EncodedAudioChunk({
|
|
333
|
+
type: "key",
|
|
334
|
+
// MP3 frames are all key frames
|
|
335
|
+
timestamp: frame.timestampUs,
|
|
336
|
+
duration: frame.durationUs,
|
|
337
|
+
data: frame.data
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
const audioConfig = {
|
|
341
|
+
codec: "mp3",
|
|
342
|
+
sampleRate: config.sampleRate,
|
|
343
|
+
numberOfChannels: config.channels
|
|
344
|
+
};
|
|
345
|
+
this.cacheManager.audioSampleCache.set(resourceId, audioChunks, audioConfig);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
console.error(`[ResourceLoader] Failed to parse audio file ${resourceId}:`, error);
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
261
351
|
/**
|
|
262
352
|
* Parse moov from stream and cache index + audio samples + decode first frame
|
|
263
353
|
*/
|
|
@@ -296,30 +386,21 @@ class ResourceLoader {
|
|
|
296
386
|
throw error;
|
|
297
387
|
}
|
|
298
388
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
* - OnDemandComposeSession retrieves from cache for composition
|
|
313
|
-
* - No longer transfer to Worker (composition is in main thread)
|
|
314
|
-
*/
|
|
315
|
-
async loadImageBitmap(task) {
|
|
316
|
-
let blob = this.blobCache.get(task.resourceId);
|
|
317
|
-
if (!blob) {
|
|
318
|
-
blob = await this.fetchBlob(task.resource.uri, task.controller.signal);
|
|
319
|
-
this.blobCache.set(task.resourceId, blob);
|
|
389
|
+
async loadImage(resource) {
|
|
390
|
+
const task = {
|
|
391
|
+
resourceId: resource.id,
|
|
392
|
+
resource,
|
|
393
|
+
bytesLoaded: 0,
|
|
394
|
+
totalBytes: 0,
|
|
395
|
+
startTime: Date.now(),
|
|
396
|
+
priority: "normal",
|
|
397
|
+
controller: new AbortController()
|
|
398
|
+
};
|
|
399
|
+
const imageBitmap = await this.loadImageResource(task);
|
|
400
|
+
if (!imageBitmap) {
|
|
401
|
+
throw new Error(`Failed to load image ${resource.id}`);
|
|
320
402
|
}
|
|
321
|
-
|
|
322
|
-
this.cacheManager.imageBitmapCache.set(task.resourceId, imageBitmap);
|
|
403
|
+
return imageBitmap;
|
|
323
404
|
}
|
|
324
405
|
/**
|
|
325
406
|
* Fetch resource as blob (for images, json, etc.)
|
|
@@ -331,57 +412,14 @@ class ResourceLoader {
|
|
|
331
412
|
}
|
|
332
413
|
return response.blob();
|
|
333
414
|
}
|
|
334
|
-
/**
|
|
335
|
-
* Transfer ImageBitmap to VideoComposeWorker
|
|
336
|
-
* Legacy: Not used in window cache architecture (images accessed via CacheManager)
|
|
337
|
-
*/
|
|
338
|
-
// private async transferImageToWorker(task: LoadTask, imageBitmap: ImageBitmap): Promise<void> {
|
|
339
|
-
// if (!this.orchestrator) return;
|
|
340
|
-
// if (!task.sessionId) {
|
|
341
|
-
// throw new Error(
|
|
342
|
-
// `[ResourceLoader] sessionId required for resource ${task.resourceId}. ` +
|
|
343
|
-
// `In Clip-based architecture, use fetch(resourceId, { sessionId })`
|
|
344
|
-
// );
|
|
345
|
-
// }
|
|
346
|
-
// const composeWorker = await this.orchestrator.workers.get('videoCompose', task.sessionId);
|
|
347
|
-
// await composeWorker?.send?.(
|
|
348
|
-
// 'receive_image',
|
|
349
|
-
// {
|
|
350
|
-
// resourceId: task.resourceId,
|
|
351
|
-
// sessionId: task.sessionId,
|
|
352
|
-
// imageBitmap,
|
|
353
|
-
// },
|
|
354
|
-
// { transfer: [imageBitmap] }
|
|
355
|
-
// );
|
|
356
|
-
// }
|
|
357
|
-
/**
|
|
358
|
-
* Transfer cached image to a session
|
|
359
|
-
* Creates new ImageBitmap from cached Blob and transfers to worker
|
|
360
|
-
*/
|
|
361
|
-
async transferCachedImage(resourceId, sessionId) {
|
|
362
|
-
const blob = this.blobCache.get(resourceId);
|
|
363
|
-
if (!blob || !sessionId) return;
|
|
364
|
-
const imageBitmap = await createImageBitmapFromBlob(blob);
|
|
365
|
-
const composeWorker = await this.workerPool.get("videoCompose", sessionId);
|
|
366
|
-
await composeWorker?.send?.(
|
|
367
|
-
"receive_image",
|
|
368
|
-
{
|
|
369
|
-
resourceId,
|
|
370
|
-
sessionId,
|
|
371
|
-
imageBitmap
|
|
372
|
-
},
|
|
373
|
-
{ transfer: [imageBitmap] }
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
415
|
/**
|
|
377
416
|
* Transfer stream to demux worker (for audio files)
|
|
378
417
|
*/
|
|
379
418
|
async transferToDemuxWorker(task) {
|
|
380
419
|
if (!task.stream) return;
|
|
381
420
|
if (!task.sessionId) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
);
|
|
421
|
+
task.stream.cancel();
|
|
422
|
+
return;
|
|
385
423
|
}
|
|
386
424
|
const workerType = task.resource.type === "video" ? "videoDemux" : "audioDemux";
|
|
387
425
|
const demuxWorker = await this.workerPool.get(workerType, task.sessionId);
|
|
@@ -410,7 +448,19 @@ class ResourceLoader {
|
|
|
410
448
|
}
|
|
411
449
|
this.onStateChange?.(resourceId, state);
|
|
412
450
|
}
|
|
413
|
-
|
|
451
|
+
/**
|
|
452
|
+
* Fetch a resource and wait for loading + parsing to complete
|
|
453
|
+
*
|
|
454
|
+
* Returns a Promise that resolves when:
|
|
455
|
+
* - Resource is fully loaded, parsed, and cached (state='ready')
|
|
456
|
+
* - Or rejects if loading/parsing fails
|
|
457
|
+
*
|
|
458
|
+
* Promise lifecycle:
|
|
459
|
+
* 1. enqueueLoad() creates LoadTask with promise/resolve/reject (or reuses existing)
|
|
460
|
+
* 2. processQueue() → startLoad() executes async in background
|
|
461
|
+
* 3. startLoad() completes → finally → completeTask() → task.resolve()/reject()
|
|
462
|
+
*/
|
|
463
|
+
async load(resourceId, options) {
|
|
414
464
|
if (!resourceId) {
|
|
415
465
|
return;
|
|
416
466
|
}
|
|
@@ -419,54 +469,72 @@ class ResourceLoader {
|
|
|
419
469
|
console.warn(`Resource ${resourceId} not found in model`);
|
|
420
470
|
return;
|
|
421
471
|
}
|
|
422
|
-
const transferResourceToWorker = async (rId, sId, type) => {
|
|
423
|
-
if (type === "video") {
|
|
424
|
-
const stream = await this.createOPFSReadStream(rId);
|
|
425
|
-
const task = {
|
|
426
|
-
resourceId: rId,
|
|
427
|
-
sessionId: sId,
|
|
428
|
-
resource: this.model?.resources.get(rId),
|
|
429
|
-
stream,
|
|
430
|
-
bytesLoaded: 0,
|
|
431
|
-
totalBytes: 0,
|
|
432
|
-
startTime: Date.now(),
|
|
433
|
-
priority: "normal"
|
|
434
|
-
};
|
|
435
|
-
await this.transferToDemuxWorker(task);
|
|
436
|
-
} else if (type === "image") {
|
|
437
|
-
await this.transferCachedImage(rId, sId);
|
|
438
|
-
}
|
|
439
|
-
};
|
|
440
472
|
if (resource.state === "ready") {
|
|
441
|
-
if (options?.sessionId) {
|
|
442
|
-
|
|
473
|
+
if (!options?.sessionId) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const isCached = await this.isResourceCached(resourceId, resource.type);
|
|
477
|
+
const hasActiveTaskForSession = this.taskManager.hasActiveTaskForSession(
|
|
478
|
+
resourceId,
|
|
479
|
+
options.sessionId
|
|
480
|
+
);
|
|
481
|
+
if (isCached && !hasActiveTaskForSession) {
|
|
482
|
+
switch (resource.type) {
|
|
483
|
+
case "video":
|
|
484
|
+
await this.transferVideoToWorker(resourceId, options.sessionId);
|
|
485
|
+
break;
|
|
486
|
+
case "image": {
|
|
487
|
+
const blob = this.blobCache.get(resourceId);
|
|
488
|
+
if (blob) {
|
|
489
|
+
const imageBitmap = await createImageBitmapFromBlob(blob);
|
|
490
|
+
await this.transferImageToWorker(resourceId, options.sessionId, imageBitmap);
|
|
491
|
+
}
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return;
|
|
443
496
|
}
|
|
444
|
-
return;
|
|
445
497
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
498
|
+
if (resource.state === "loading") {
|
|
499
|
+
const existingTask2 = this.taskManager.getActiveTask(resourceId);
|
|
500
|
+
if (existingTask2) {
|
|
501
|
+
if (!options?.sessionId || existingTask2.sessionId === options.sessionId) {
|
|
502
|
+
return existingTask2.promise;
|
|
503
|
+
}
|
|
452
504
|
}
|
|
453
|
-
} else {
|
|
454
|
-
this.enqueueLoad(
|
|
455
|
-
resource,
|
|
456
|
-
options?.priority || "normal",
|
|
457
|
-
options?.sessionId,
|
|
458
|
-
options?.clipId,
|
|
459
|
-
options?.trackId,
|
|
460
|
-
options?.isMainTrack ?? false
|
|
461
|
-
);
|
|
462
|
-
taskPromise = this.taskManager.getTaskPromise(resourceId);
|
|
463
|
-
isCoveredByTask = true;
|
|
464
505
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
506
|
+
const existingTask = this.taskManager.getActiveTask(resourceId);
|
|
507
|
+
if (existingTask) {
|
|
508
|
+
if (options?.sessionId && !existingTask.sessionId) {
|
|
509
|
+
await existingTask.promise;
|
|
510
|
+
const isCached = await this.isResourceCached(resourceId, resource.type);
|
|
511
|
+
if (isCached) {
|
|
512
|
+
switch (resource.type) {
|
|
513
|
+
case "video":
|
|
514
|
+
await this.transferVideoToWorker(resourceId, options.sessionId);
|
|
515
|
+
break;
|
|
516
|
+
case "image": {
|
|
517
|
+
const blob = this.blobCache.get(resourceId);
|
|
518
|
+
if (blob) {
|
|
519
|
+
const imageBitmap = await createImageBitmapFromBlob(blob);
|
|
520
|
+
await this.transferImageToWorker(resourceId, options.sessionId, imageBitmap);
|
|
521
|
+
}
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
return existingTask.promise;
|
|
469
529
|
}
|
|
530
|
+
const task = this.enqueueLoad(
|
|
531
|
+
resource,
|
|
532
|
+
options?.priority || "normal",
|
|
533
|
+
options?.sessionId,
|
|
534
|
+
options?.clipId,
|
|
535
|
+
options?.trackId
|
|
536
|
+
);
|
|
537
|
+
return task.promise;
|
|
470
538
|
}
|
|
471
539
|
cancel(resourceId) {
|
|
472
540
|
this.taskManager.cancelTask(resourceId);
|
|
@@ -499,7 +567,7 @@ class ResourceLoader {
|
|
|
499
567
|
options?.trackId
|
|
500
568
|
);
|
|
501
569
|
} else {
|
|
502
|
-
await this.
|
|
570
|
+
await this.load(resourceId, options);
|
|
503
571
|
}
|
|
504
572
|
}
|
|
505
573
|
get activeTasks() {
|
|
@@ -511,11 +579,9 @@ class ResourceLoader {
|
|
|
511
579
|
dispose() {
|
|
512
580
|
this.taskManager.clear();
|
|
513
581
|
this.blobCache.clear();
|
|
514
|
-
this.pendingTransfers.clear();
|
|
515
582
|
}
|
|
516
583
|
}
|
|
517
584
|
export {
|
|
518
|
-
ResourceConflictError,
|
|
519
585
|
ResourceLoader
|
|
520
586
|
};
|
|
521
587
|
//# sourceMappingURL=ResourceLoader.js.map
|