@meframe/core 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/cache/CacheManager.d.ts +1 -1
  2. package/dist/cache/CacheManager.d.ts.map +1 -1
  3. package/dist/cache/CacheManager.js +1 -1
  4. package/dist/cache/CacheManager.js.map +1 -1
  5. package/dist/cache/l1/AudioL1Cache.d.ts +0 -8
  6. package/dist/cache/l1/AudioL1Cache.d.ts.map +1 -1
  7. package/dist/cache/l1/AudioL1Cache.js +16 -25
  8. package/dist/cache/l1/AudioL1Cache.js.map +1 -1
  9. package/dist/controllers/PlaybackController.d.ts.map +1 -1
  10. package/dist/controllers/PlaybackController.js +2 -10
  11. package/dist/controllers/PlaybackController.js.map +1 -1
  12. package/dist/controllers/PlaybackStateMachine.d.ts.map +1 -1
  13. package/dist/controllers/PlaybackStateMachine.js +20 -2
  14. package/dist/controllers/PlaybackStateMachine.js.map +1 -1
  15. package/dist/controllers/types.d.ts +3 -0
  16. package/dist/controllers/types.d.ts.map +1 -1
  17. package/dist/controllers/types.js +1 -0
  18. package/dist/controllers/types.js.map +1 -1
  19. package/dist/orchestrator/GlobalAudioSession.d.ts +1 -1
  20. package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
  21. package/dist/orchestrator/GlobalAudioSession.js +9 -10
  22. package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
  23. package/dist/orchestrator/OnDemandVideoSession.d.ts.map +1 -1
  24. package/dist/orchestrator/OnDemandVideoSession.js +13 -0
  25. package/dist/orchestrator/OnDemandVideoSession.js.map +1 -1
  26. package/dist/orchestrator/Orchestrator.d.ts +1 -1
  27. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  28. package/dist/orchestrator/Orchestrator.js +44 -17
  29. package/dist/orchestrator/Orchestrator.js.map +1 -1
  30. package/dist/stages/load/ResourceLoader.d.ts +6 -5
  31. package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
  32. package/dist/stages/load/ResourceLoader.js +53 -39
  33. package/dist/stages/load/ResourceLoader.js.map +1 -1
  34. package/dist/stages/load/TaskManager.d.ts +5 -12
  35. package/dist/stages/load/TaskManager.d.ts.map +1 -1
  36. package/dist/stages/load/TaskManager.js +60 -46
  37. package/dist/stages/load/TaskManager.js.map +1 -1
  38. package/dist/stages/mux/MP4Muxer.d.ts.map +1 -1
  39. package/dist/stages/mux/MP4Muxer.js +3 -0
  40. package/dist/stages/mux/MP4Muxer.js.map +1 -1
  41. package/dist/utils/platform-utils.d.ts +1 -0
  42. package/dist/utils/platform-utils.d.ts.map +1 -1
  43. package/dist/utils/platform-utils.js +19 -6
  44. package/dist/utils/platform-utils.js.map +1 -1
  45. package/dist/workers/stages/decode/video-decode.worker.BnWVUkng.js.map +1 -1
  46. package/package.json +1 -1
@@ -20,11 +20,11 @@ class ResourceLoader {
20
20
  // Track in-progress OPFS writes
21
21
  // Preloading state
22
22
  isPreloadingEnabled = true;
23
- preloadQueue = [];
24
23
  constructor(options) {
25
24
  const config = options.config || {};
26
25
  const maxConcurrent = config.maxConcurrent ?? 3;
27
- this.taskManager = new TaskManager(maxConcurrent);
26
+ const preloadConcurrency = config.preloadConcurrency ?? 2;
27
+ this.taskManager = new TaskManager(maxConcurrent, preloadConcurrency);
28
28
  this.streamFactory = new StreamFactory(options.onProgress, config);
29
29
  this.eventBus = options.eventBus;
30
30
  this.onStateChange = options.onStateChange;
@@ -68,22 +68,10 @@ class ResourceLoader {
68
68
  }
69
69
  }
70
70
  }
71
- processPreloadQueue() {
72
- if (!this.isPreloadingEnabled || this.preloadQueue.length === 0) return;
73
- while (this.preloadQueue.length > 0) {
74
- const resourceId = this.preloadQueue.shift();
75
- if (!resourceId) break;
76
- this.load(resourceId, { isPreload: true }).finally(() => {
77
- this.processPreloadQueue();
78
- });
79
- }
80
- }
81
71
  enqueueLoad(resource, isPreload = false, sessionId, clipId, trackId) {
82
72
  const existingTask = this.taskManager.getActiveTask(resource.id);
83
73
  if (existingTask) {
84
- if (existingTask.isPreload && !isPreload) {
85
- existingTask.isPreload = false;
86
- }
74
+ this.taskManager.enqueue(resource, isPreload, sessionId, clipId, trackId);
87
75
  return existingTask;
88
76
  }
89
77
  const task = this.taskManager.enqueue(resource, isPreload, sessionId, clipId, trackId);
@@ -117,20 +105,22 @@ class ResourceLoader {
117
105
  }
118
106
  }
119
107
  /**
120
- * Load image resource (download + cache or read from cache)
121
- * Returns ImageBitmap for caller to use (no worker transfer)
108
+ * Ensure image blob is cached (download + cache or reuse cache).
109
+ *
110
+ * Important: preload should NOT decode ImageBitmap.
111
+ * createImageBitmap can be slow / flaky under heavy main-thread load (playback + export),
112
+ * and decoding isn't required for caching or for later loadImage()/getImageBitmap() calls.
122
113
  */
123
- async loadImageResource(task) {
114
+ async ensureImageBlob(task) {
124
115
  let blob = this.blobCache.get(task.resourceId);
125
116
  if (!blob) {
126
117
  if (task.controller) {
127
118
  blob = await this.fetchBlob(task.resource.uri, task.controller.signal);
128
119
  this.blobCache.set(task.resourceId, blob);
129
120
  } else {
130
- return null;
121
+ return;
131
122
  }
132
123
  }
133
- return await createImageBitmapFromBlob(blob);
134
124
  }
135
125
  /**
136
126
  * Get cached ImageBitmap (for already loaded resources)
@@ -161,10 +151,7 @@ class ResourceLoader {
161
151
  task.controller = new AbortController();
162
152
  switch (task.resource.type) {
163
153
  case "image": {
164
- const image = await this.loadImageResource(task);
165
- if (image) {
166
- image.close();
167
- }
154
+ await this.ensureImageBlob(task);
168
155
  break;
169
156
  }
170
157
  case "video":
@@ -368,6 +355,11 @@ class ResourceLoader {
368
355
  }
369
356
  }
370
357
  async loadImage(resource) {
358
+ const existingBlob = this.blobCache.get(resource.id);
359
+ if (existingBlob) {
360
+ const imageBitmap = await createImageBitmapFromBlob(existingBlob);
361
+ return imageBitmap;
362
+ }
371
363
  const task = {
372
364
  resourceId: resource.id,
373
365
  resource,
@@ -380,10 +372,10 @@ class ResourceLoader {
380
372
  let loadError;
381
373
  try {
382
374
  this.updateResourceState(resource.id, "loading");
383
- const imageBitmap = await this.loadImageResource(task);
384
- if (!imageBitmap) {
385
- throw new Error(`Failed to load image ${resource.id}`);
386
- }
375
+ await this.ensureImageBlob(task);
376
+ const blob = this.blobCache.get(resource.id);
377
+ if (!blob) throw new Error(`Failed to load image ${resource.id}`);
378
+ const imageBitmap = await createImageBitmapFromBlob(blob);
387
379
  this.updateResourceState(resource.id, "ready");
388
380
  return imageBitmap;
389
381
  } catch (error) {
@@ -432,24 +424,42 @@ class ResourceLoader {
432
424
  if (!resourceId) {
433
425
  return;
434
426
  }
435
- const resource = this.model?.resources.get(resourceId);
427
+ const resource = this.model?.getResource(resourceId);
436
428
  if (!resource) {
437
- console.warn(`Resource ${resourceId} not found in model`);
429
+ const m = this.model;
430
+ console.warn("[ResourceLoader] Resource not found in model:", {
431
+ resourceId,
432
+ options: {
433
+ isPreload: options?.isPreload ?? false,
434
+ sessionId: options?.sessionId,
435
+ clipId: options?.clipId,
436
+ trackId: options?.trackId,
437
+ isMainTrack: options?.isMainTrack
438
+ },
439
+ model: m ? {
440
+ fps: m.fps,
441
+ durationUs: m.durationUs,
442
+ mainTrackId: m.mainTrackId,
443
+ trackCount: m.tracks.length,
444
+ resourcesSize: m.resources.size
445
+ } : null
446
+ });
438
447
  return;
439
448
  }
440
449
  const isPreload = options?.isPreload ?? false;
441
- if (!isPreload) {
442
- this.taskManager.pausePreloadTasks();
443
- }
444
450
  if (resource.state === "ready") {
445
451
  return;
446
452
  }
447
453
  if (resource.state === "loading") {
448
454
  const existingTask2 = this.taskManager.getActiveTask(resourceId);
449
455
  if (existingTask2) {
450
- if (existingTask2.isPreload && !isPreload) {
451
- existingTask2.isPreload = false;
452
- }
456
+ this.taskManager.enqueue(
457
+ resource,
458
+ isPreload,
459
+ options?.sessionId,
460
+ options?.clipId,
461
+ options?.trackId
462
+ );
453
463
  if (!options?.sessionId || existingTask2.sessionId === options.sessionId) {
454
464
  return existingTask2.promise;
455
465
  }
@@ -457,9 +467,13 @@ class ResourceLoader {
457
467
  }
458
468
  const existingTask = this.taskManager.getActiveTask(resourceId);
459
469
  if (existingTask) {
460
- if (existingTask.isPreload && !isPreload) {
461
- existingTask.isPreload = false;
462
- }
470
+ this.taskManager.enqueue(
471
+ resource,
472
+ isPreload,
473
+ options?.sessionId,
474
+ options?.clipId,
475
+ options?.trackId
476
+ );
463
477
  return existingTask.promise;
464
478
  }
465
479
  const task = this.enqueueLoad(
@@ -1 +1 @@
1
- {"version":3,"file":"ResourceLoader.js","sources":["../../../src/stages/load/ResourceLoader.ts"],"sourcesContent":["import { type Resource, type CompositionModel, hasResourceId } from '../../model';\nimport type { ResourceLoadOptions, LoadTask, ResourceLoaderOptions } from './types';\nimport { TaskManager } from './TaskManager';\nimport { StreamFactory } from './StreamFactory';\nimport { EventPayloadMap, MeframeEvent } from '../../event/events';\nimport { EventBus } from '../../event/EventBus';\nimport { createImageBitmapFromBlob } from '../../utils/image-utils';\nimport { MP4IndexParser, type MP4ParseResult } from '../demux/MP4IndexParser';\nimport { MP3FrameParser } from '../demux/MP3FrameParser';\nimport type { CacheManager } from '../../cache/CacheManager';\nimport {\n EmptyStreamError,\n ResourceCorruptedError,\n OPFSQuotaExceededError,\n} from '../../utils/errors';\n\nexport class ResourceConflictError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ResourceConflictError';\n }\n}\n\nexport class ResourceLoader {\n private cacheManager: CacheManager;\n private model?: CompositionModel;\n private taskManager: TaskManager;\n private streamFactory: StreamFactory;\n private eventBus?: EventBus<EventPayloadMap>;\n private onStateChange?: (resourceId: string, state: Resource['state']) => void;\n private blobCache = new Map<string, Blob>();\n private parsingIndexes = new Set<string>(); // Track in-progress index parsing\n private writingResources = new Set<string>(); // Track in-progress OPFS writes\n\n // Preloading state\n private isPreloadingEnabled = true;\n private preloadQueue: string[] = [];\n\n constructor(options: ResourceLoaderOptions) {\n const config = options.config || {};\n const maxConcurrent = config.maxConcurrent ?? 3;\n\n this.taskManager = new TaskManager(maxConcurrent);\n this.streamFactory = new StreamFactory(options.onProgress, config);\n this.eventBus = options.eventBus;\n this.onStateChange = options.onStateChange;\n this.cacheManager = options.cacheManager;\n }\n\n async setModel(model: CompositionModel): Promise<void> {\n this.model = model;\n const mainTrack = model.tracks.find((track) => track.id === (model.mainTrackId || 'main'));\n if (mainTrack?.clips?.[0] && hasResourceId(mainTrack.clips[0])) {\n await this.load(mainTrack.clips[0].resourceId, {\n isPreload: false,\n clipId: mainTrack.clips[0].id,\n trackId: mainTrack.id,\n });\n }\n this.startPreloading();\n }\n\n setPreloadingEnabled(enabled: boolean): void {\n this.isPreloadingEnabled = enabled;\n if (enabled) {\n this.startPreloading();\n }\n }\n\n startPreloading(): void {\n if (!this.model || !this.isPreloadingEnabled) return;\n\n // Collect all video/audio tracks for horizontal loading\n const tracks = this.model.tracks.filter(\n (track) => track.kind === 'video' || track.kind === 'audio'\n );\n if (tracks.length === 0) return;\n\n // Find maximum clip count across all tracks\n const maxClipCount = Math.max(...tracks.map((track) => track.clips.length));\n\n // Horizontal loading: load clip[0] from all tracks, then clip[1], etc.\n for (let clipIndex = 0; clipIndex < maxClipCount; clipIndex++) {\n for (const track of tracks) {\n const clip = track.clips[clipIndex];\n if (!clip || !hasResourceId(clip)) continue;\n\n const resource = this.model.getResource(clip.resourceId);\n if (!resource) continue;\n\n // Skip if already ready, loading, or error\n if (\n resource.state === 'ready' ||\n resource.state === 'loading' ||\n resource.state === 'error'\n ) {\n continue;\n }\n\n this.load(resource.id, { isPreload: true });\n }\n }\n }\n\n private processPreloadQueue(): void {\n if (!this.isPreloadingEnabled || this.preloadQueue.length === 0) return;\n\n while (this.preloadQueue.length > 0) {\n const resourceId = this.preloadQueue.shift();\n if (!resourceId) break;\n\n // Use isPreload flag for background preloading\n this.load(resourceId, { isPreload: true }).finally(() => {\n // Continue processing queue\n this.processPreloadQueue();\n });\n }\n }\n\n private enqueueLoad(\n resource: Resource,\n isPreload: boolean = false,\n sessionId?: string,\n clipId?: string,\n trackId?: string\n ): LoadTask {\n // Check if task is already active\n const existingTask = this.taskManager.getActiveTask(resource.id);\n if (existingTask) {\n // Upgrade preload task to active task if needed\n if (existingTask.isPreload && !isPreload) {\n existingTask.isPreload = false;\n }\n return existingTask;\n }\n\n // Create new task and enqueue\n const task = this.taskManager.enqueue(resource, isPreload, sessionId, clipId, trackId);\n this.processQueue();\n return task;\n }\n\n private processQueue(): void {\n while (this.taskManager.canProcess) {\n const task = this.taskManager.getNextTask();\n if (!task) break;\n this.startLoad(task);\n }\n }\n\n /**\n * Load video resource (download + cache or read from cache)\n */\n private async loadVideoResource(task: LoadTask): Promise<void> {\n const cached = await this.cacheManager.hasResourceInCache(task.resourceId);\n\n if (cached) {\n // Resource already in OPFS - ensure index is parsed\n await this.ensureIndexParsed(task.resourceId);\n\n // Note: Export now uses IndexedVideoSource directly from OPFS\n // No need to transfer stream to worker anymore\n } else {\n // Not cached - download and cache to OPFS\n await this.loadWithOPFSCache(task);\n }\n }\n\n /**\n * Load audio resource (download + parse or reuse cache)\n */\n private async loadAudioResource(task: LoadTask): Promise<void> {\n if (!this.cacheManager.audioSampleCache.has(task.resourceId)) {\n // Not cached - download and parse\n await this.loadAndParseAudioFile(task);\n }\n // If already cached, do nothing (reuse existing cache)\n }\n\n /**\n * Load image resource (download + cache or read from cache)\n * Returns ImageBitmap for caller to use (no worker transfer)\n */\n private async loadImageResource(task: LoadTask): Promise<ImageBitmap | null> {\n // Check cache first\n let blob = this.blobCache.get(task.resourceId);\n\n if (!blob) {\n // Not cached: download and cache\n if (task.controller) {\n blob = await this.fetchBlob(task.resource.uri, task.controller.signal);\n this.blobCache.set(task.resourceId, blob);\n } else {\n return null;\n }\n }\n\n // Create ImageBitmap and return (caller decides what to do with it)\n return await createImageBitmapFromBlob(blob);\n }\n\n /**\n * Get cached ImageBitmap (for already loaded resources)\n * Used by VideoClipSession to batch transfer attachments\n */\n async getImageBitmap(resourceId: string): Promise<ImageBitmap | null> {\n const blob = this.blobCache.get(resourceId);\n if (!blob) return null;\n return await createImageBitmapFromBlob(blob);\n }\n\n /**\n * Load text resource (json/text)\n */\n private async loadTextResource(task: LoadTask): Promise<void> {\n if (task.controller) {\n await this.fetchBlob(task.resource.uri, task.controller.signal);\n }\n }\n\n /**\n * Start loading a resource\n * Handles state management (loading → ready/error) for all resource types\n */\n private async startLoad(task: LoadTask): Promise<void> {\n this.taskManager.activateTask(task);\n let loadError: Error | undefined;\n\n try {\n this.updateResourceState(task.resourceId, 'loading');\n task.controller = new AbortController();\n\n // Route to specific handlers based on resource type\n switch (task.resource.type) {\n case 'image': {\n const image = await this.loadImageResource(task);\n if (image) {\n image.close(); // Close if not transferred\n }\n break;\n }\n\n case 'video':\n await this.loadVideoResource(task);\n break;\n\n case 'audio':\n await this.loadAudioResource(task);\n break;\n\n case 'json':\n case 'text':\n await this.loadTextResource(task);\n break;\n }\n\n // Unified state update for all resource types\n this.updateResourceState(task.resourceId, 'ready');\n } catch (error) {\n task.error = error as Error;\n loadError = error as Error;\n this.updateResourceState(task.resourceId, 'error');\n } finally {\n this.taskManager.completeTask(task.resourceId, loadError);\n this.processQueue();\n }\n }\n\n /**\n * Ensure MP4 index is parsed for a cached resource\n */\n async ensureIndexParsed(resourceId: string): Promise<void> {\n // Check if index already exists\n if (this.cacheManager.mp4IndexCache.has(resourceId)) {\n return;\n }\n\n // Wait for OPFS write to complete\n while (this.writingResources.has(resourceId)) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n\n // Check if already parsing (avoid duplicate parsing for same resource)\n if (this.parsingIndexes.has(resourceId)) {\n // Wait for the in-progress parsing to complete\n while (this.parsingIndexes.has(resourceId)) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n return;\n }\n\n // Mark as parsing\n this.parsingIndexes.add(resourceId);\n\n try {\n // Validate file exists and has content\n const cached = await this.cacheManager.hasResourceInCache(resourceId);\n if (!cached) {\n throw new ResourceCorruptedError(resourceId, 'File not found in OPFS or is empty');\n }\n\n // Parse from OPFS file\n const stream = await this.createOPFSReadStream(resourceId);\n // Create minimal task for parsing (no clipId since this is a background index parse)\n const parseTask: LoadTask = {\n resourceId,\n resource: { id: resourceId, type: 'video', uri: '' },\n bytesLoaded: 0,\n totalBytes: 0,\n startTime: Date.now(),\n isPreload: false,\n };\n await this.parseIndexFromStream(parseTask, stream);\n } catch (error) {\n // Handle empty stream error by clearing corrupted cache\n if (error instanceof EmptyStreamError || error instanceof ResourceCorruptedError) {\n console.warn(`[ResourceLoader] Corrupted cache detected for ${resourceId}, clearing...`);\n try {\n await this.cacheManager.resourceCache.deleteResource(resourceId);\n this.cacheManager.mp4IndexCache.delete(resourceId);\n } catch (deleteError) {\n console.error(`[ResourceLoader] Failed to clear corrupted cache:`, deleteError);\n }\n }\n throw error;\n } finally {\n // Remove from parsing set\n this.parsingIndexes.delete(resourceId);\n }\n }\n\n /**\n * Create ReadableStream from OPFS file\n * Reuses OPFSManager's underlying file access\n */\n private async createOPFSReadStream(resourceId: string): Promise<ReadableStream<Uint8Array>> {\n const opfsManager = this.cacheManager.resourceCache.opfsManager;\n const projectId = this.cacheManager.resourceCache.projectId;\n\n // Get file handle from OPFS\n const dir = await opfsManager.getProjectDir(projectId, 'resource');\n const fileName = `${resourceId}.mp4`;\n const fileHandle = await dir.getFileHandle(fileName);\n const file = await fileHandle.getFile();\n\n // Return native stream\n return file.stream();\n }\n\n /**\n * Load resource and cache to OPFS + parse moov + extract first frame\n *\n * Strategy: Parallel tee() approach for fast first frame\n * - One stream writes to OPFS (background)\n * - Another stream parses moov and extracts first GOP\n * - First frame is decoded and cached immediately\n */\n private async loadWithOPFSCache(task: LoadTask): Promise<void> {\n const stream = await this.streamFactory.createRegularStream(task);\n if (!stream) {\n throw new Error(`Failed to create stream for ${task.resourceId}`);\n }\n\n // Export mode: only 2-way split (OPFS + parsing)\n // IndexedVideoSource will read directly from OPFS later\n const [opfsStream, parseStream] = stream.tee();\n\n // Parallel execution: write to OPFS and parse index\n await Promise.all([\n this.writeToOPFS(task.resourceId, opfsStream, task),\n this.parseIndexFromStream(task, parseStream),\n ]);\n }\n\n /**\n * Write resource stream to OPFS with retry on quota exceeded\n * If quota is exceeded and old projects are evicted, fetches a fresh stream and retries\n */\n private async writeToOPFS(\n resourceId: string,\n stream: ReadableStream<Uint8Array>,\n task?: LoadTask\n ): Promise<void> {\n this.writingResources.add(resourceId);\n try {\n await this.cacheManager.resourceCache.writeResource(resourceId, stream);\n } catch (error) {\n if (error instanceof OPFSQuotaExceededError && error.retryable && task) {\n console.log(\n `[ResourceLoader] OPFS quota exceeded for ${resourceId}, retrying with fresh stream...`\n );\n\n // Create fresh stream for retry\n const retryStream = await this.streamFactory.createRegularStream(task);\n if (!retryStream) {\n throw new Error(`Failed to create retry stream for ${resourceId}`);\n }\n\n // Retry write with fresh stream\n await this.cacheManager.resourceCache.writeResource(resourceId, retryStream);\n } else {\n throw error;\n }\n } finally {\n this.writingResources.delete(resourceId);\n }\n }\n\n /**\n * Load and parse audio file (MP3/WAV) in main thread\n * Extract EncodedAudioChunk and cache to AudioSampleCache\n * Aligned with video audio track extraction (unified architecture)\n */\n private async loadAndParseAudioFile(task: LoadTask): Promise<void> {\n const { resourceId } = task;\n\n try {\n // TODO: Streaming download and parse?\n const blob = await this.fetchBlob(task.resource.uri, task.controller!.signal);\n\n // Convert blob to ArrayBuffer\n const arrayBuffer = await blob.arrayBuffer();\n const uint8Array = new Uint8Array(arrayBuffer);\n\n // Parse MP3 frames using MP3FrameParser\n const parser = new MP3FrameParser();\n const { frames, config } = parser.push(uint8Array);\n const remainingFrames = parser.flush();\n const allFrames = [...frames, ...remainingFrames];\n\n if (!config) {\n throw new Error(`Failed to parse audio config for ${resourceId}`);\n }\n\n // Convert MP3Frame to EncodedAudioChunk\n const audioChunks: EncodedAudioChunk[] = allFrames.map((frame) => {\n return new EncodedAudioChunk({\n type: 'key', // MP3 frames are all key frames\n timestamp: frame.timestampUs,\n duration: frame.durationUs,\n data: frame.data,\n });\n });\n\n // Build AudioDecoderConfig from MP3Config\n const audioConfig: AudioDecoderConfig = {\n codec: 'mp3',\n sampleRate: config.sampleRate,\n numberOfChannels: config.channels,\n };\n\n // Cache to AudioSampleCache\n this.cacheManager.audioSampleCache.set(resourceId, audioChunks, audioConfig);\n } catch (error) {\n console.error(`[ResourceLoader] Failed to parse audio file ${resourceId}:`, error);\n throw error;\n }\n }\n\n /**\n * Parse moov from stream and cache index + audio samples + decode first frame\n */\n private async parseIndexFromStream(\n task: LoadTask,\n stream: ReadableStream<Uint8Array>\n ): Promise<void> {\n const { resourceId, clipId } = task;\n\n try {\n const parser = new MP4IndexParser();\n\n // Only enable first GOP extraction for clips that start at time 0 (cover clips)\n const shouldExtractFirstGOP = clipId ? this.shouldExtractCover(clipId) : false;\n\n const result: MP4ParseResult = await parser.parseFromStream(stream, {\n resourceId: resourceId,\n onFirstFrameReady: shouldExtractFirstGOP\n ? async (index, chunks) => {\n // Set resourceId on index\n index.resourceId = resourceId;\n\n // Cache index immediately\n this.cacheManager.mp4IndexCache.set(resourceId, index);\n\n // Emit event with chunks for Orchestrator to handle\n this.eventBus?.emit(MeframeEvent.ResourceFirstFrameReady, {\n resourceId,\n clipId: clipId!,\n index,\n chunks,\n });\n }\n : undefined,\n });\n\n result.index.resourceId = resourceId;\n\n // Cache index (if not already cached by onFirstFrameReady)\n if (!this.cacheManager.mp4IndexCache.has(resourceId)) {\n this.cacheManager.mp4IndexCache.set(resourceId, result.index);\n }\n\n // Cache audio samples if present\n if (result.audioSamples && result.audioMetadata) {\n this.cacheManager.audioSampleCache.set(\n resourceId,\n result.audioSamples,\n result.audioMetadata\n );\n }\n } catch (error) {\n console.error(`[ResourceLoader] Failed to parse MP4 index for ${resourceId}:`, error);\n // Rethrow error to ensure resource is marked as failed\n // In the new architecture (Window Cache + AudioSampleCache), index parsing is critical.\n throw error;\n }\n }\n\n async loadImage(resource: Resource): Promise<ImageBitmap> {\n const task: LoadTask = {\n resourceId: resource.id,\n resource: resource,\n bytesLoaded: 0,\n totalBytes: 0,\n startTime: Date.now(),\n isPreload: false,\n controller: new AbortController(),\n };\n\n // loadImage() bypasses startLoad(), so it must manage resource state by itself.\n let loadError: Error | undefined;\n try {\n this.updateResourceState(resource.id, 'loading');\n const imageBitmap = await this.loadImageResource(task);\n if (!imageBitmap) {\n throw new Error(`Failed to load image ${resource.id}`);\n }\n this.updateResourceState(resource.id, 'ready');\n return imageBitmap;\n } catch (error) {\n loadError = error as Error;\n this.updateResourceState(resource.id, 'error');\n throw loadError;\n }\n }\n\n /**\n * Fetch resource as blob (for images, json, etc.)\n */\n private async fetchBlob(uri: string, signal: AbortSignal): Promise<Blob> {\n const response = await fetch(uri, { signal });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n return response.blob();\n }\n\n private updateResourceState(resourceId: string, state: Resource['state']): void {\n const resource = this.model?.resources.get(resourceId);\n if (resource) {\n const oldState = resource.state;\n resource.state = state;\n this.eventBus?.emit(MeframeEvent.ResourceStageChange, {\n type: MeframeEvent.ResourceStageChange,\n resourceId,\n oldState,\n newState: state,\n });\n }\n\n this.onStateChange?.(resourceId, state);\n }\n\n /**\n * Fetch a resource and wait for loading + parsing to complete\n *\n * Returns a Promise that resolves when:\n * - Resource is fully loaded, parsed, and cached (state='ready')\n * - Or rejects if loading/parsing fails\n *\n * Promise lifecycle:\n * 1. enqueueLoad() creates LoadTask with promise/resolve/reject (or reuses existing)\n * 2. processQueue() → startLoad() executes async in background\n * 3. startLoad() completes → finally → completeTask() → task.resolve()/reject()\n */\n async load(resourceId?: string, options?: ResourceLoadOptions): Promise<void> {\n if (!resourceId) {\n return;\n }\n\n const resource = this.model?.resources.get(resourceId);\n if (!resource) {\n console.warn(`Resource ${resourceId} not found in model`);\n return;\n }\n\n const isPreload = options?.isPreload ?? false;\n\n // If this is an active load (not preload), pause all preload tasks\n if (!isPreload) {\n this.taskManager.pausePreloadTasks();\n }\n\n // First check: if resource is already ready, nothing to do\n if (resource.state === 'ready') {\n return;\n }\n\n // Second check: if resource is being loaded\n if (resource.state === 'loading') {\n const existingTask = this.taskManager.getActiveTask(resourceId);\n if (existingTask) {\n // Upgrade preload task to active task\n if (existingTask.isPreload && !isPreload) {\n existingTask.isPreload = false;\n }\n\n // If sessionId matches or no sessionId required, reuse existing task\n if (!options?.sessionId || existingTask.sessionId === options.sessionId) {\n return existingTask.promise;\n }\n }\n }\n\n // Third path: check if already has active task\n const existingTask = this.taskManager.getActiveTask(resourceId);\n if (existingTask) {\n // Upgrade preload task to active task\n if (existingTask.isPreload && !isPreload) {\n existingTask.isPreload = false;\n }\n\n // Reuse existing task\n return existingTask.promise;\n }\n\n // Create new task\n const task = this.enqueueLoad(\n resource,\n isPreload,\n options?.sessionId,\n options?.clipId,\n options?.trackId\n );\n\n // Wait for task completion\n return task.promise;\n }\n\n cancel(resourceId: string): void {\n this.taskManager.cancelTask(resourceId);\n this.processQueue();\n }\n\n /**\n * Check if a resource is currently being loaded\n */\n isResourceLoading(resourceId: string): boolean {\n return this.taskManager.hasActiveTask(resourceId);\n }\n\n pause(resourceId: string): void {\n const task = this.taskManager.getActiveTask(resourceId);\n if (task) {\n task.controller?.abort();\n }\n }\n\n async resume(resourceId: string, options?: ResourceLoadOptions): Promise<void> {\n const resource = this.model?.getResource(resourceId);\n if (!resource) {\n throw new Error(`Resource ${resourceId} not found`);\n }\n\n const pausedTask = this.taskManager.getActiveTask(resourceId);\n\n if (pausedTask?.pausedAt !== undefined) {\n this.enqueueLoad(\n resource,\n options?.isPreload ?? false,\n options?.sessionId,\n options?.clipId,\n options?.trackId\n );\n } else {\n await this.load(resourceId, options);\n }\n }\n\n get activeTasks(): Map<string, LoadTask> {\n return this.taskManager.activeTasks;\n }\n\n get taskQueue(): LoadTask[] {\n return this.taskManager.taskQueue;\n }\n\n dispose(): void {\n this.taskManager.clear();\n this.blobCache.clear();\n }\n\n /**\n * Check if a clip needs cover extraction (first GOP decode)\n * Only clips starting at time 0 need fast cover rendering\n */\n private shouldExtractCover(clipId: string): boolean {\n if (!this.model) return false;\n\n const clip = this.model.findClip(clipId);\n return clip?.startUs === 0;\n }\n}\n"],"names":["existingTask"],"mappings":"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"}
1
+ {"version":3,"file":"ResourceLoader.js","sources":["../../../src/stages/load/ResourceLoader.ts"],"sourcesContent":["import { type Resource, type CompositionModel, hasResourceId } from '../../model';\nimport type { ResourceLoadOptions, LoadTask, ResourceLoaderOptions } from './types';\nimport { TaskManager } from './TaskManager';\nimport { StreamFactory } from './StreamFactory';\nimport { EventPayloadMap, MeframeEvent } from '../../event/events';\nimport { EventBus } from '../../event/EventBus';\nimport { createImageBitmapFromBlob } from '../../utils/image-utils';\nimport { MP4IndexParser, type MP4ParseResult } from '../demux/MP4IndexParser';\nimport { MP3FrameParser } from '../demux/MP3FrameParser';\nimport type { CacheManager } from '../../cache/CacheManager';\nimport {\n EmptyStreamError,\n ResourceCorruptedError,\n OPFSQuotaExceededError,\n} from '../../utils/errors';\n\nexport class ResourceConflictError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ResourceConflictError';\n }\n}\n\nexport class ResourceLoader {\n private cacheManager: CacheManager;\n private model?: CompositionModel;\n private taskManager: TaskManager;\n private streamFactory: StreamFactory;\n private eventBus?: EventBus<EventPayloadMap>;\n private onStateChange?: (resourceId: string, state: Resource['state']) => void;\n private blobCache = new Map<string, Blob>();\n private parsingIndexes = new Set<string>(); // Track in-progress index parsing\n private writingResources = new Set<string>(); // Track in-progress OPFS writes\n\n // Preloading state\n private isPreloadingEnabled = true;\n\n constructor(options: ResourceLoaderOptions) {\n const config = options.config || {};\n const maxConcurrent = config.maxConcurrent ?? 3;\n const preloadConcurrency = config.preloadConcurrency ?? 2;\n\n this.taskManager = new TaskManager(maxConcurrent, preloadConcurrency);\n this.streamFactory = new StreamFactory(options.onProgress, config);\n this.eventBus = options.eventBus;\n this.onStateChange = options.onStateChange;\n this.cacheManager = options.cacheManager;\n }\n\n async setModel(model: CompositionModel): Promise<void> {\n // Resource IDs are globally stable across models (same id implies same uri/content),\n // so in-flight load tasks remain valid across model switching.\n this.model = model;\n const mainTrack = model.tracks.find((track) => track.id === (model.mainTrackId || 'main'));\n if (mainTrack?.clips?.[0] && hasResourceId(mainTrack.clips[0])) {\n await this.load(mainTrack.clips[0].resourceId, {\n isPreload: false,\n clipId: mainTrack.clips[0].id,\n trackId: mainTrack.id,\n });\n }\n this.startPreloading();\n }\n\n setPreloadingEnabled(enabled: boolean): void {\n this.isPreloadingEnabled = enabled;\n if (enabled) {\n this.startPreloading();\n }\n }\n\n startPreloading(): void {\n if (!this.model || !this.isPreloadingEnabled) return;\n\n // Collect all video/audio tracks for horizontal loading\n const tracks = this.model.tracks.filter(\n (track) => track.kind === 'video' || track.kind === 'audio'\n );\n if (tracks.length === 0) return;\n\n // Find maximum clip count across all tracks\n const maxClipCount = Math.max(...tracks.map((track) => track.clips.length));\n\n // Horizontal loading: load clip[0] from all tracks, then clip[1], etc.\n for (let clipIndex = 0; clipIndex < maxClipCount; clipIndex++) {\n for (const track of tracks) {\n const clip = track.clips[clipIndex];\n if (!clip || !hasResourceId(clip)) continue;\n\n const resource = this.model.getResource(clip.resourceId);\n if (!resource) continue;\n\n // Skip if already ready, loading, or error\n if (\n resource.state === 'ready' ||\n resource.state === 'loading' ||\n resource.state === 'error'\n ) {\n continue;\n }\n\n this.load(resource.id, { isPreload: true });\n }\n }\n }\n\n private enqueueLoad(\n resource: Resource,\n isPreload: boolean = false,\n sessionId?: string,\n clipId?: string,\n trackId?: string\n ): LoadTask {\n // Check if task is already active\n const existingTask = this.taskManager.getActiveTask(resource.id);\n if (existingTask) {\n // Upgrade preload task to foreground if needed (go through TaskManager to keep counters correct)\n this.taskManager.enqueue(resource, isPreload, sessionId, clipId, trackId);\n return existingTask;\n }\n\n // Create new task and enqueue\n const task = this.taskManager.enqueue(resource, isPreload, sessionId, clipId, trackId);\n this.processQueue();\n return task;\n }\n\n private processQueue(): void {\n while (this.taskManager.canProcess) {\n const task = this.taskManager.getNextTask();\n if (!task) break;\n this.startLoad(task);\n }\n }\n\n /**\n * Load video resource (download + cache or read from cache)\n */\n private async loadVideoResource(task: LoadTask): Promise<void> {\n const cached = await this.cacheManager.hasResourceInCache(task.resourceId);\n\n if (cached) {\n // Resource already in OPFS - ensure index is parsed\n await this.ensureIndexParsed(task.resourceId);\n\n // Note: Export now uses IndexedVideoSource directly from OPFS\n // No need to transfer stream to worker anymore\n } else {\n // Not cached - download and cache to OPFS\n await this.loadWithOPFSCache(task);\n }\n }\n\n /**\n * Load audio resource (download + parse or reuse cache)\n */\n private async loadAudioResource(task: LoadTask): Promise<void> {\n if (!this.cacheManager.audioSampleCache.has(task.resourceId)) {\n // Not cached - download and parse\n await this.loadAndParseAudioFile(task);\n }\n // If already cached, do nothing (reuse existing cache)\n }\n\n /**\n * Ensure image blob is cached (download + cache or reuse cache).\n *\n * Important: preload should NOT decode ImageBitmap.\n * createImageBitmap can be slow / flaky under heavy main-thread load (playback + export),\n * and decoding isn't required for caching or for later loadImage()/getImageBitmap() calls.\n */\n private async ensureImageBlob(task: LoadTask): Promise<void> {\n // Check cache first\n let blob = this.blobCache.get(task.resourceId);\n\n if (!blob) {\n // Not cached: download and cache\n if (task.controller) {\n blob = await this.fetchBlob(task.resource.uri, task.controller.signal);\n this.blobCache.set(task.resourceId, blob);\n } else {\n return;\n }\n }\n }\n\n /**\n * Get cached ImageBitmap (for already loaded resources)\n * Used by VideoClipSession to batch transfer attachments\n */\n async getImageBitmap(resourceId: string): Promise<ImageBitmap | null> {\n const blob = this.blobCache.get(resourceId);\n if (!blob) return null;\n return await createImageBitmapFromBlob(blob);\n }\n\n /**\n * Load text resource (json/text)\n */\n private async loadTextResource(task: LoadTask): Promise<void> {\n if (task.controller) {\n await this.fetchBlob(task.resource.uri, task.controller.signal);\n }\n }\n\n /**\n * Start loading a resource\n * Handles state management (loading → ready/error) for all resource types\n */\n private async startLoad(task: LoadTask): Promise<void> {\n this.taskManager.activateTask(task);\n let loadError: Error | undefined;\n\n try {\n this.updateResourceState(task.resourceId, 'loading');\n task.controller = new AbortController();\n\n // Route to specific handlers based on resource type\n switch (task.resource.type) {\n case 'image': {\n // Preload path: only cache blob, avoid createImageBitmap during export/playback concurrency.\n await this.ensureImageBlob(task);\n break;\n }\n\n case 'video':\n await this.loadVideoResource(task);\n break;\n\n case 'audio':\n await this.loadAudioResource(task);\n break;\n\n case 'json':\n case 'text':\n await this.loadTextResource(task);\n break;\n }\n\n // Unified state update for all resource types\n this.updateResourceState(task.resourceId, 'ready');\n } catch (error) {\n task.error = error as Error;\n loadError = error as Error;\n this.updateResourceState(task.resourceId, 'error');\n } finally {\n this.taskManager.completeTask(task.resourceId, loadError);\n this.processQueue();\n }\n }\n\n /**\n * Ensure MP4 index is parsed for a cached resource\n */\n async ensureIndexParsed(resourceId: string): Promise<void> {\n // Check if index already exists\n if (this.cacheManager.mp4IndexCache.has(resourceId)) {\n return;\n }\n\n // Wait for OPFS write to complete\n while (this.writingResources.has(resourceId)) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n\n // Check if already parsing (avoid duplicate parsing for same resource)\n if (this.parsingIndexes.has(resourceId)) {\n // Wait for the in-progress parsing to complete\n while (this.parsingIndexes.has(resourceId)) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n return;\n }\n\n // Mark as parsing\n this.parsingIndexes.add(resourceId);\n\n try {\n // Validate file exists and has content\n const cached = await this.cacheManager.hasResourceInCache(resourceId);\n if (!cached) {\n throw new ResourceCorruptedError(resourceId, 'File not found in OPFS or is empty');\n }\n\n // Parse from OPFS file\n const stream = await this.createOPFSReadStream(resourceId);\n // Create minimal task for parsing (no clipId since this is a background index parse)\n const parseTask: LoadTask = {\n resourceId,\n resource: { id: resourceId, type: 'video', uri: '' },\n bytesLoaded: 0,\n totalBytes: 0,\n startTime: Date.now(),\n isPreload: false,\n };\n await this.parseIndexFromStream(parseTask, stream);\n } catch (error) {\n // Handle empty stream error by clearing corrupted cache\n if (error instanceof EmptyStreamError || error instanceof ResourceCorruptedError) {\n console.warn(`[ResourceLoader] Corrupted cache detected for ${resourceId}, clearing...`);\n try {\n await this.cacheManager.resourceCache.deleteResource(resourceId);\n this.cacheManager.mp4IndexCache.delete(resourceId);\n } catch (deleteError) {\n console.error(`[ResourceLoader] Failed to clear corrupted cache:`, deleteError);\n }\n }\n throw error;\n } finally {\n // Remove from parsing set\n this.parsingIndexes.delete(resourceId);\n }\n }\n\n /**\n * Create ReadableStream from OPFS file\n * Reuses OPFSManager's underlying file access\n */\n private async createOPFSReadStream(resourceId: string): Promise<ReadableStream<Uint8Array>> {\n const opfsManager = this.cacheManager.resourceCache.opfsManager;\n const projectId = this.cacheManager.resourceCache.projectId;\n\n // Get file handle from OPFS\n const dir = await opfsManager.getProjectDir(projectId, 'resource');\n const fileName = `${resourceId}.mp4`;\n const fileHandle = await dir.getFileHandle(fileName);\n const file = await fileHandle.getFile();\n\n // Return native stream\n return file.stream();\n }\n\n /**\n * Load resource and cache to OPFS + parse moov + extract first frame\n *\n * Strategy: Parallel tee() approach for fast first frame\n * - One stream writes to OPFS (background)\n * - Another stream parses moov and extracts first GOP\n * - First frame is decoded and cached immediately\n */\n private async loadWithOPFSCache(task: LoadTask): Promise<void> {\n const stream = await this.streamFactory.createRegularStream(task);\n if (!stream) {\n throw new Error(`Failed to create stream for ${task.resourceId}`);\n }\n\n // Export mode: only 2-way split (OPFS + parsing)\n // IndexedVideoSource will read directly from OPFS later\n const [opfsStream, parseStream] = stream.tee();\n\n // Parallel execution: write to OPFS and parse index\n await Promise.all([\n this.writeToOPFS(task.resourceId, opfsStream, task),\n this.parseIndexFromStream(task, parseStream),\n ]);\n }\n\n /**\n * Write resource stream to OPFS with retry on quota exceeded\n * If quota is exceeded and old projects are evicted, fetches a fresh stream and retries\n */\n private async writeToOPFS(\n resourceId: string,\n stream: ReadableStream<Uint8Array>,\n task?: LoadTask\n ): Promise<void> {\n this.writingResources.add(resourceId);\n try {\n await this.cacheManager.resourceCache.writeResource(resourceId, stream);\n } catch (error) {\n if (error instanceof OPFSQuotaExceededError && error.retryable && task) {\n console.log(\n `[ResourceLoader] OPFS quota exceeded for ${resourceId}, retrying with fresh stream...`\n );\n\n // Create fresh stream for retry\n const retryStream = await this.streamFactory.createRegularStream(task);\n if (!retryStream) {\n throw new Error(`Failed to create retry stream for ${resourceId}`);\n }\n\n // Retry write with fresh stream\n await this.cacheManager.resourceCache.writeResource(resourceId, retryStream);\n } else {\n throw error;\n }\n } finally {\n this.writingResources.delete(resourceId);\n }\n }\n\n /**\n * Load and parse audio file (MP3/WAV) in main thread\n * Extract EncodedAudioChunk and cache to AudioSampleCache\n * Aligned with video audio track extraction (unified architecture)\n */\n private async loadAndParseAudioFile(task: LoadTask): Promise<void> {\n const { resourceId } = task;\n\n try {\n // TODO: Streaming download and parse?\n const blob = await this.fetchBlob(task.resource.uri, task.controller!.signal);\n\n // Convert blob to ArrayBuffer\n const arrayBuffer = await blob.arrayBuffer();\n const uint8Array = new Uint8Array(arrayBuffer);\n\n // Parse MP3 frames using MP3FrameParser\n const parser = new MP3FrameParser();\n const { frames, config } = parser.push(uint8Array);\n const remainingFrames = parser.flush();\n const allFrames = [...frames, ...remainingFrames];\n\n if (!config) {\n throw new Error(`Failed to parse audio config for ${resourceId}`);\n }\n\n // Convert MP3Frame to EncodedAudioChunk\n const audioChunks: EncodedAudioChunk[] = allFrames.map((frame) => {\n return new EncodedAudioChunk({\n type: 'key', // MP3 frames are all key frames\n timestamp: frame.timestampUs,\n duration: frame.durationUs,\n data: frame.data,\n });\n });\n\n // Build AudioDecoderConfig from MP3Config\n const audioConfig: AudioDecoderConfig = {\n codec: 'mp3',\n sampleRate: config.sampleRate,\n numberOfChannels: config.channels,\n };\n\n // Cache to AudioSampleCache\n this.cacheManager.audioSampleCache.set(resourceId, audioChunks, audioConfig);\n } catch (error) {\n console.error(`[ResourceLoader] Failed to parse audio file ${resourceId}:`, error);\n throw error;\n }\n }\n\n /**\n * Parse moov from stream and cache index + audio samples + decode first frame\n */\n private async parseIndexFromStream(\n task: LoadTask,\n stream: ReadableStream<Uint8Array>\n ): Promise<void> {\n const { resourceId, clipId } = task;\n\n try {\n const parser = new MP4IndexParser();\n\n // Only enable first GOP extraction for clips that start at time 0 (cover clips)\n const shouldExtractFirstGOP = clipId ? this.shouldExtractCover(clipId) : false;\n\n const result: MP4ParseResult = await parser.parseFromStream(stream, {\n resourceId: resourceId,\n onFirstFrameReady: shouldExtractFirstGOP\n ? async (index, chunks) => {\n // Set resourceId on index\n index.resourceId = resourceId;\n\n // Cache index immediately\n this.cacheManager.mp4IndexCache.set(resourceId, index);\n\n // Emit event with chunks for Orchestrator to handle\n this.eventBus?.emit(MeframeEvent.ResourceFirstFrameReady, {\n resourceId,\n clipId: clipId!,\n index,\n chunks,\n });\n }\n : undefined,\n });\n\n result.index.resourceId = resourceId;\n\n // Cache index (if not already cached by onFirstFrameReady)\n if (!this.cacheManager.mp4IndexCache.has(resourceId)) {\n this.cacheManager.mp4IndexCache.set(resourceId, result.index);\n }\n\n // Cache audio samples if present\n if (result.audioSamples && result.audioMetadata) {\n this.cacheManager.audioSampleCache.set(\n resourceId,\n result.audioSamples,\n result.audioMetadata\n );\n }\n } catch (error) {\n console.error(`[ResourceLoader] Failed to parse MP4 index for ${resourceId}:`, error);\n // Rethrow error to ensure resource is marked as failed\n // In the new architecture (Window Cache + AudioSampleCache), index parsing is critical.\n throw error;\n }\n }\n\n async loadImage(resource: Resource): Promise<ImageBitmap> {\n // If we already have the blob in memory, decoding to ImageBitmap is NOT resource loading.\n // Do not flip resource.state to 'loading' during playback; it will cause buffering oscillation\n // because PlaybackController gates on resource.state === 'ready'.\n const existingBlob = this.blobCache.get(resource.id);\n if (existingBlob) {\n const imageBitmap = await createImageBitmapFromBlob(existingBlob);\n return imageBitmap;\n }\n\n const task: LoadTask = {\n resourceId: resource.id,\n resource: resource,\n bytesLoaded: 0,\n totalBytes: 0,\n startTime: Date.now(),\n isPreload: false,\n controller: new AbortController(),\n };\n\n // loadImage() bypasses startLoad(), so it must manage resource state by itself.\n let loadError: Error | undefined;\n try {\n this.updateResourceState(resource.id, 'loading');\n // Ensure blob exists, then decode to ImageBitmap.\n await this.ensureImageBlob(task);\n const blob = this.blobCache.get(resource.id);\n if (!blob) throw new Error(`Failed to load image ${resource.id}`);\n const imageBitmap = await createImageBitmapFromBlob(blob);\n this.updateResourceState(resource.id, 'ready');\n return imageBitmap;\n } catch (error) {\n loadError = error as Error;\n this.updateResourceState(resource.id, 'error');\n throw loadError;\n }\n }\n\n /**\n * Fetch resource as blob (for images, json, etc.)\n */\n private async fetchBlob(uri: string, signal: AbortSignal): Promise<Blob> {\n const response = await fetch(uri, { signal });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n return response.blob();\n }\n\n private updateResourceState(resourceId: string, state: Resource['state']): void {\n const resource = this.model?.resources.get(resourceId);\n if (resource) {\n const oldState = resource.state;\n resource.state = state;\n this.eventBus?.emit(MeframeEvent.ResourceStageChange, {\n type: MeframeEvent.ResourceStageChange,\n resourceId,\n oldState,\n newState: state,\n });\n }\n\n this.onStateChange?.(resourceId, state);\n }\n\n /**\n * Fetch a resource and wait for loading + parsing to complete\n *\n * Returns a Promise that resolves when:\n * - Resource is fully loaded, parsed, and cached (state='ready')\n * - Or rejects if loading/parsing fails\n *\n * Promise lifecycle:\n * 1. enqueueLoad() creates LoadTask with promise/resolve/reject (or reuses existing)\n * 2. processQueue() → startLoad() executes async in background\n * 3. startLoad() completes → finally → completeTask() → task.resolve()/reject()\n */\n async load(resourceId?: string, options?: ResourceLoadOptions): Promise<void> {\n if (!resourceId) {\n return;\n }\n\n const resource = this.model?.getResource(resourceId);\n if (!resource) {\n const m = this.model;\n console.warn('[ResourceLoader] Resource not found in model:', {\n resourceId,\n options: {\n isPreload: options?.isPreload ?? false,\n sessionId: options?.sessionId,\n clipId: options?.clipId,\n trackId: options?.trackId,\n isMainTrack: options?.isMainTrack,\n },\n model: m\n ? {\n fps: m.fps,\n durationUs: m.durationUs,\n mainTrackId: m.mainTrackId,\n trackCount: m.tracks.length,\n resourcesSize: m.resources.size,\n }\n : null,\n });\n return;\n }\n\n const isPreload = options?.isPreload ?? false;\n\n // First check: if resource is already ready, nothing to do\n if (resource.state === 'ready') {\n return;\n }\n\n // Second check: if resource is being loaded\n if (resource.state === 'loading') {\n const existingTask = this.taskManager.getActiveTask(resourceId);\n if (existingTask) {\n // Upgrade preload -> foreground through TaskManager to keep counters correct\n // (do NOT mutate existingTask.isPreload here).\n this.taskManager.enqueue(\n resource,\n isPreload,\n options?.sessionId,\n options?.clipId,\n options?.trackId\n );\n\n // If sessionId matches or no sessionId required, reuse existing task\n if (!options?.sessionId || existingTask.sessionId === options.sessionId) {\n return existingTask.promise;\n }\n }\n }\n\n // Third path: check if already has active task\n const existingTask = this.taskManager.getActiveTask(resourceId);\n if (existingTask) {\n // Upgrade preload -> foreground through TaskManager to keep counters correct\n // (do NOT mutate existingTask.isPreload here).\n this.taskManager.enqueue(\n resource,\n isPreload,\n options?.sessionId,\n options?.clipId,\n options?.trackId\n );\n\n // Reuse existing task\n return existingTask.promise;\n }\n\n // Create new task\n const task = this.enqueueLoad(\n resource,\n isPreload,\n options?.sessionId,\n options?.clipId,\n options?.trackId\n );\n\n // Wait for task completion\n return task.promise;\n }\n\n cancel(resourceId: string): void {\n this.taskManager.cancelTask(resourceId);\n this.processQueue();\n }\n\n /**\n * Check if a resource is currently being loaded\n */\n isResourceLoading(resourceId: string): boolean {\n return this.taskManager.hasActiveTask(resourceId);\n }\n\n pause(resourceId: string): void {\n const task = this.taskManager.getActiveTask(resourceId);\n if (task) {\n task.controller?.abort();\n }\n }\n\n async resume(resourceId: string, options?: ResourceLoadOptions): Promise<void> {\n const resource = this.model?.getResource(resourceId);\n if (!resource) {\n throw new Error(`Resource ${resourceId} not found`);\n }\n\n const pausedTask = this.taskManager.getActiveTask(resourceId);\n\n if (pausedTask?.pausedAt !== undefined) {\n this.enqueueLoad(\n resource,\n options?.isPreload ?? false,\n options?.sessionId,\n options?.clipId,\n options?.trackId\n );\n } else {\n await this.load(resourceId, options);\n }\n }\n\n get activeTasks(): Map<string, LoadTask> {\n return this.taskManager.activeTasks;\n }\n\n get taskQueue(): LoadTask[] {\n return this.taskManager.taskQueue;\n }\n\n dispose(): void {\n this.taskManager.clear();\n this.blobCache.clear();\n }\n\n /**\n * Check if a clip needs cover extraction (first GOP decode)\n * Only clips starting at time 0 need fast cover rendering\n */\n private shouldExtractCover(clipId: string): boolean {\n if (!this.model) return false;\n\n const clip = this.model.findClip(clipId);\n return clip?.startUs === 0;\n }\n}\n"],"names":["existingTask"],"mappings":"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"}
@@ -7,10 +7,12 @@ import { Resource } from '../../model';
7
7
  export declare class TaskManager {
8
8
  activeTasks: Map<string, LoadTask>;
9
9
  taskQueue: LoadTask[];
10
- private pausedPreloadTasks;
10
+ private queuedTasks;
11
11
  private concurrentCount;
12
+ private concurrentPreloadCount;
12
13
  private maxConcurrent;
13
- constructor(maxConcurrent?: number);
14
+ private maxPreloadConcurrent;
15
+ constructor(maxConcurrent?: number, preloadConcurrency?: number);
14
16
  /**
15
17
  * Check if a resource is already being loaded
16
18
  */
@@ -51,16 +53,7 @@ export declare class TaskManager {
51
53
  * Check if can process more tasks
52
54
  */
53
55
  get canProcess(): boolean;
54
- /**
55
- * Pause all preload tasks (when active load starts)
56
- * Moves preload tasks to paused queue instead of discarding them
57
- */
58
- pausePreloadTasks(): void;
59
- /**
60
- * Resume preload tasks when no active normal tasks
61
- * Called automatically after task completion
62
- */
63
- resumePreloadTasks(): void;
56
+ private mergeTaskMeta;
64
57
  /**
65
58
  * Clear all tasks
66
59
  */
@@ -1 +1 @@
1
- {"version":3,"file":"TaskManager.d.ts","sourceRoot":"","sources":["../../../src/stages/load/TaskManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C;;GAEG;AACH,qBAAa,WAAW;IACtB,WAAW,wBAA+B;IAC1C,SAAS,EAAE,QAAQ,EAAE,CAAM;IAC3B,OAAO,CAAC,kBAAkB,CAAkB;IAC5C,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,aAAa,CAAS;gBAElB,aAAa,GAAE,MAAU;IAIrC;;OAEG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAI1C;;OAEG;IACH,uBAAuB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO;IAYxE;;OAEG;IACH,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS;IAM7D;;OAEG;IACH,OAAO,CACL,QAAQ,EAAE,QAAQ,EAClB,SAAS,GAAE,OAAe,EAC1B,SAAS,CAAC,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,MAAM,GACf,QAAQ;IA8BX;;OAEG;IACH,WAAW,IAAI,QAAQ,GAAG,IAAI;IAQ9B;;OAEG;IACH,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAKlC;;OAEG;IACH,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI;IAkBrD;;OAEG;IACH,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAkBvC;;OAEG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAIvD;;OAEG;IACH,IAAI,UAAU,IAAI,OAAO,CAExB;IAED;;;OAGG;IACH,iBAAiB,IAAI,IAAI;IAmBzB;;;OAGG;IACH,kBAAkB,IAAI,IAAI;IA2B1B;;OAEG;IACH,KAAK,IAAI,IAAI;CAWd"}
1
+ {"version":3,"file":"TaskManager.d.ts","sourceRoot":"","sources":["../../../src/stages/load/TaskManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C;;GAEG;AACH,qBAAa,WAAW;IACtB,WAAW,wBAA+B;IAC1C,SAAS,EAAE,QAAQ,EAAE,CAAM;IAC3B,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,sBAAsB,CAAK;IACnC,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,oBAAoB,CAAS;gBAEzB,aAAa,GAAE,MAAU,EAAE,kBAAkB,GAAE,MAAU;IAKrE;;OAEG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAI1C;;OAEG;IACH,uBAAuB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO;IAYxE;;OAEG;IACH,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS;IAM7D;;OAEG;IACH,OAAO,CACL,QAAQ,EAAE,QAAQ,EAClB,SAAS,GAAE,OAAe,EAC1B,SAAS,CAAC,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,MAAM,GACf,QAAQ;IAuDX;;OAEG;IACH,WAAW,IAAI,QAAQ,GAAG,IAAI;IAyB9B;;OAEG;IACH,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAQlC;;OAEG;IACH,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI;IAkBrD;;OAEG;IACH,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAqBvC;;OAEG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAIvD;;OAEG;IACH,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,OAAO,CAAC,aAAa;IAoBrB;;OAEG;IACH,KAAK,IAAI,IAAI;CAYd"}
@@ -1,11 +1,14 @@
1
1
  class TaskManager {
2
2
  activeTasks = /* @__PURE__ */ new Map();
3
3
  taskQueue = [];
4
- pausedPreloadTasks = [];
4
+ queuedTasks = /* @__PURE__ */ new Map();
5
5
  concurrentCount = 0;
6
+ concurrentPreloadCount = 0;
6
7
  maxConcurrent;
7
- constructor(maxConcurrent = 3) {
8
+ maxPreloadConcurrent;
9
+ constructor(maxConcurrent = 3, preloadConcurrency = 2) {
8
10
  this.maxConcurrent = maxConcurrent;
11
+ this.maxPreloadConcurrent = Math.max(0, Math.min(preloadConcurrency, maxConcurrent));
9
12
  }
10
13
  /**
11
14
  * Check if a resource is already being loaded
@@ -35,6 +38,14 @@ class TaskManager {
35
38
  * Create and enqueue a new task
36
39
  */
37
40
  enqueue(resource, isPreload = false, sessionId, clipId, trackId) {
41
+ const existingActive = this.activeTasks.get(resource.id);
42
+ if (existingActive) {
43
+ return this.mergeTaskMeta(existingActive, { isPreload, sessionId, clipId, trackId });
44
+ }
45
+ const existingQueued = this.queuedTasks.get(resource.id);
46
+ if (existingQueued) {
47
+ return this.mergeTaskMeta(existingQueued, { isPreload, sessionId, clipId, trackId });
48
+ }
38
49
  let resolve;
39
50
  let reject;
40
51
  const promise = new Promise((res, rej) => {
@@ -55,7 +66,17 @@ class TaskManager {
55
66
  resolve,
56
67
  reject
57
68
  };
58
- this.taskQueue.push(task);
69
+ if (task.isPreload) {
70
+ this.taskQueue.push(task);
71
+ } else {
72
+ const firstPreloadIndex = this.taskQueue.findIndex((t) => t.isPreload);
73
+ if (firstPreloadIndex < 0) {
74
+ this.taskQueue.push(task);
75
+ } else {
76
+ this.taskQueue.splice(firstPreloadIndex, 0, task);
77
+ }
78
+ }
79
+ this.queuedTasks.set(task.resourceId, task);
59
80
  return task;
60
81
  }
61
82
  /**
@@ -65,7 +86,20 @@ class TaskManager {
65
86
  if (this.taskQueue.length === 0 || this.concurrentCount >= this.maxConcurrent) {
66
87
  return null;
67
88
  }
68
- return this.taskQueue.shift() || null;
89
+ const normalIndex = this.taskQueue.findIndex((t) => !t.isPreload);
90
+ if (normalIndex >= 0) {
91
+ const [task2] = this.taskQueue.splice(normalIndex, 1);
92
+ if (!task2) return null;
93
+ this.queuedTasks.delete(task2.resourceId);
94
+ return task2;
95
+ }
96
+ if (this.concurrentPreloadCount >= this.maxPreloadConcurrent) {
97
+ return null;
98
+ }
99
+ const task = this.taskQueue.shift() || null;
100
+ if (!task) return null;
101
+ this.queuedTasks.delete(task.resourceId);
102
+ return task;
69
103
  }
70
104
  /**
71
105
  * Mark task as active
@@ -73,6 +107,9 @@ class TaskManager {
73
107
  activateTask(task) {
74
108
  this.activeTasks.set(task.resourceId, task);
75
109
  this.concurrentCount++;
110
+ if (task.isPreload) {
111
+ this.concurrentPreloadCount++;
112
+ }
76
113
  }
77
114
  /**
78
115
  * Mark task as completed
@@ -87,8 +124,10 @@ class TaskManager {
87
124
  }
88
125
  this.activeTasks.delete(resourceId);
89
126
  this.concurrentCount--;
127
+ if (task.isPreload) {
128
+ this.concurrentPreloadCount = Math.max(0, this.concurrentPreloadCount - 1);
129
+ }
90
130
  }
91
- this.resumePreloadTasks();
92
131
  }
93
132
  /**
94
133
  * Cancel a task
@@ -102,7 +141,9 @@ class TaskManager {
102
141
  }
103
142
  const index = this.taskQueue.findIndex((t) => t.resourceId === resourceId);
104
143
  if (index >= 0) {
105
- this.taskQueue.splice(index, 1);
144
+ const [removed] = this.taskQueue.splice(index, 1);
145
+ this.queuedTasks.delete(resourceId);
146
+ removed?.resolve?.();
106
147
  return true;
107
148
  }
108
149
  return false;
@@ -119,47 +160,19 @@ class TaskManager {
119
160
  get canProcess() {
120
161
  return this.concurrentCount < this.maxConcurrent;
121
162
  }
122
- /**
123
- * Pause all preload tasks (when active load starts)
124
- * Moves preload tasks to paused queue instead of discarding them
125
- */
126
- pausePreloadTasks() {
127
- const normalTasks = [];
128
- const pausedIds = new Set(this.pausedPreloadTasks.map((t) => t.resourceId));
129
- for (const task of this.taskQueue) {
130
- if (task.isPreload) {
131
- if (!pausedIds.has(task.resourceId)) {
132
- this.pausedPreloadTasks.push(task);
133
- pausedIds.add(task.resourceId);
134
- }
135
- } else {
136
- normalTasks.push(task);
137
- }
138
- }
139
- this.taskQueue = normalTasks;
140
- }
141
- /**
142
- * Resume preload tasks when no active normal tasks
143
- * Called automatically after task completion
144
- */
145
- resumePreloadTasks() {
146
- if (this.pausedPreloadTasks.length === 0) return;
147
- for (const task of this.activeTasks.values()) {
148
- if (!task.isPreload) return;
149
- }
150
- const skipIds = /* @__PURE__ */ new Set();
151
- for (const task of this.taskQueue) {
152
- skipIds.add(task.resourceId);
153
- }
154
- for (const id of this.activeTasks.keys()) {
155
- skipIds.add(id);
156
- }
157
- for (const task of this.pausedPreloadTasks) {
158
- if (!skipIds.has(task.resourceId)) {
159
- this.taskQueue.push(task);
163
+ mergeTaskMeta(task, meta) {
164
+ if (task.isPreload && !meta.isPreload) {
165
+ const wasActive = this.activeTasks.get(task.resourceId) === task;
166
+ if (wasActive) {
167
+ this.concurrentPreloadCount = Math.max(0, this.concurrentPreloadCount - 1);
160
168
  }
169
+ task.isPreload = false;
161
170
  }
162
- this.pausedPreloadTasks = [];
171
+ if (task.sessionId === void 0 && meta.sessionId !== void 0)
172
+ task.sessionId = meta.sessionId;
173
+ if (task.clipId === void 0 && meta.clipId !== void 0) task.clipId = meta.clipId;
174
+ if (task.trackId === void 0 && meta.trackId !== void 0) task.trackId = meta.trackId;
175
+ return task;
163
176
  }
164
177
  /**
165
178
  * Clear all tasks
@@ -170,8 +183,9 @@ class TaskManager {
170
183
  }
171
184
  this.activeTasks.clear();
172
185
  this.taskQueue = [];
173
- this.pausedPreloadTasks = [];
186
+ this.queuedTasks.clear();
174
187
  this.concurrentCount = 0;
188
+ this.concurrentPreloadCount = 0;
175
189
  }
176
190
  }
177
191
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"TaskManager.js","sources":["../../../src/stages/load/TaskManager.ts"],"sourcesContent":["import type { LoadTask } from './types';\nimport type { Resource } from '../../model';\n\n/**\n * Manages resource loading tasks and queue\n */\nexport class TaskManager {\n activeTasks = new Map<string, LoadTask>();\n taskQueue: LoadTask[] = [];\n private pausedPreloadTasks: LoadTask[] = [];\n private concurrentCount = 0;\n private maxConcurrent: number;\n\n constructor(maxConcurrent: number = 3) {\n this.maxConcurrent = maxConcurrent;\n }\n\n /**\n * Check if a resource is already being loaded\n */\n hasActiveTask(resourceId: string): boolean {\n return this.activeTasks.has(resourceId);\n }\n\n /**\n * Check if a resource is being loaded for a specific session\n */\n hasActiveTaskForSession(resourceId: string, sessionId?: string): boolean {\n const task = this.activeTasks.get(resourceId);\n if (!task) return false;\n\n // If sessionId is provided, check if it matches\n if (sessionId !== undefined) {\n return task.sessionId === sessionId;\n }\n\n return true;\n }\n\n /**\n * Get task promise (task must already exist)\n */\n getTaskPromise(resourceId: string): Promise<void> | undefined {\n const task =\n this.activeTasks.get(resourceId) || this.taskQueue.find((t) => t.resourceId === resourceId);\n return task?.promise;\n }\n\n /**\n * Create and enqueue a new task\n */\n enqueue(\n resource: Resource,\n isPreload: boolean = false,\n sessionId?: string,\n clipId?: string,\n trackId?: string\n ): LoadTask {\n // Create promise for this task\n let resolve: (() => void) | undefined;\n let reject: ((error: Error) => void) | undefined;\n const promise = new Promise<void>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n\n const task: LoadTask = {\n resourceId: resource.id,\n resource,\n bytesLoaded: 0,\n totalBytes: 0,\n startTime: Date.now(),\n isPreload,\n sessionId,\n clipId,\n trackId,\n promise,\n resolve,\n reject,\n };\n\n // Add to end of queue (no priority-based ordering)\n this.taskQueue.push(task);\n\n return task;\n }\n\n /**\n * Get next task from queue if under concurrent limit\n */\n getNextTask(): LoadTask | null {\n if (this.taskQueue.length === 0 || this.concurrentCount >= this.maxConcurrent) {\n return null;\n }\n\n return this.taskQueue.shift() || null;\n }\n\n /**\n * Mark task as active\n */\n activateTask(task: LoadTask): void {\n this.activeTasks.set(task.resourceId, task);\n this.concurrentCount++;\n }\n\n /**\n * Mark task as completed\n */\n completeTask(resourceId: string, error?: Error): void {\n const task = this.activeTasks.get(resourceId);\n if (task) {\n // Resolve or reject task's promise\n if (error) {\n task.reject?.(error);\n } else {\n task.resolve?.();\n }\n\n this.activeTasks.delete(resourceId);\n this.concurrentCount--;\n }\n\n // Try to resume preload tasks after completion\n this.resumePreloadTasks();\n }\n\n /**\n * Cancel a task\n */\n cancelTask(resourceId: string): boolean {\n const task = this.activeTasks.get(resourceId);\n if (task) {\n task.controller?.abort();\n this.completeTask(resourceId);\n return true;\n }\n\n // Also remove from queue\n const index = this.taskQueue.findIndex((t) => t.resourceId === resourceId);\n if (index >= 0) {\n this.taskQueue.splice(index, 1);\n return true;\n }\n\n return false;\n }\n\n /**\n * Find an active task\n */\n getActiveTask(resourceId: string): LoadTask | undefined {\n return this.activeTasks.get(resourceId);\n }\n\n /**\n * Check if can process more tasks\n */\n get canProcess(): boolean {\n return this.concurrentCount < this.maxConcurrent;\n }\n\n /**\n * Pause all preload tasks (when active load starts)\n * Moves preload tasks to paused queue instead of discarding them\n */\n pausePreloadTasks(): void {\n // Single-pass: split queue and deduplicate\n const normalTasks: LoadTask[] = [];\n const pausedIds = new Set(this.pausedPreloadTasks.map((t) => t.resourceId));\n\n for (const task of this.taskQueue) {\n if (task.isPreload) {\n if (!pausedIds.has(task.resourceId)) {\n this.pausedPreloadTasks.push(task);\n pausedIds.add(task.resourceId);\n }\n } else {\n normalTasks.push(task);\n }\n }\n\n this.taskQueue = normalTasks;\n }\n\n /**\n * Resume preload tasks when no active normal tasks\n * Called automatically after task completion\n */\n resumePreloadTasks(): void {\n if (this.pausedPreloadTasks.length === 0) return;\n\n // Check if any active normal tasks exist\n for (const task of this.activeTasks.values()) {\n if (!task.isPreload) return;\n }\n\n // Build skip set (already in queue or active)\n const skipIds = new Set<string>();\n for (const task of this.taskQueue) {\n skipIds.add(task.resourceId);\n }\n for (const id of this.activeTasks.keys()) {\n skipIds.add(id);\n }\n\n // Restore valid tasks\n for (const task of this.pausedPreloadTasks) {\n if (!skipIds.has(task.resourceId)) {\n this.taskQueue.push(task);\n }\n }\n\n this.pausedPreloadTasks = [];\n }\n\n /**\n * Clear all tasks\n */\n clear(): void {\n // Cancel all active tasks\n for (const task of this.activeTasks.values()) {\n task.controller?.abort();\n }\n\n this.activeTasks.clear();\n this.taskQueue = [];\n this.pausedPreloadTasks = [];\n this.concurrentCount = 0;\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TaskManager.js","sources":["../../../src/stages/load/TaskManager.ts"],"sourcesContent":["import type { LoadTask } from './types';\nimport type { Resource } from '../../model';\n\n/**\n * Manages resource loading tasks and queue\n */\nexport class TaskManager {\n activeTasks = new Map<string, LoadTask>();\n taskQueue: LoadTask[] = [];\n private queuedTasks = new Map<string, LoadTask>();\n private concurrentCount = 0;\n private concurrentPreloadCount = 0;\n private maxConcurrent: number;\n private maxPreloadConcurrent: number;\n\n constructor(maxConcurrent: number = 3, preloadConcurrency: number = 2) {\n this.maxConcurrent = maxConcurrent;\n this.maxPreloadConcurrent = Math.max(0, Math.min(preloadConcurrency, maxConcurrent));\n }\n\n /**\n * Check if a resource is already being loaded\n */\n hasActiveTask(resourceId: string): boolean {\n return this.activeTasks.has(resourceId);\n }\n\n /**\n * Check if a resource is being loaded for a specific session\n */\n hasActiveTaskForSession(resourceId: string, sessionId?: string): boolean {\n const task = this.activeTasks.get(resourceId);\n if (!task) return false;\n\n // If sessionId is provided, check if it matches\n if (sessionId !== undefined) {\n return task.sessionId === sessionId;\n }\n\n return true;\n }\n\n /**\n * Get task promise (task must already exist)\n */\n getTaskPromise(resourceId: string): Promise<void> | undefined {\n const task =\n this.activeTasks.get(resourceId) || this.taskQueue.find((t) => t.resourceId === resourceId);\n return task?.promise;\n }\n\n /**\n * Create and enqueue a new task\n */\n enqueue(\n resource: Resource,\n isPreload: boolean = false,\n sessionId?: string,\n clipId?: string,\n trackId?: string\n ): LoadTask {\n // Deduplicate by resourceId across active + queued tasks.\n // This prevents the race where two concurrent callers enqueue the same resourceId\n // while state is still 'pending', causing activeTasks.set(resourceId, task) to overwrite\n // and orphan the previous promise (export preload can hang at ~0-40%).\n const existingActive = this.activeTasks.get(resource.id);\n if (existingActive) {\n return this.mergeTaskMeta(existingActive, { isPreload, sessionId, clipId, trackId });\n }\n\n const existingQueued = this.queuedTasks.get(resource.id);\n if (existingQueued) {\n return this.mergeTaskMeta(existingQueued, { isPreload, sessionId, clipId, trackId });\n }\n\n // Create promise for this task\n let resolve: (() => void) | undefined;\n let reject: ((error: Error) => void) | undefined;\n const promise = new Promise<void>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n\n const task: LoadTask = {\n resourceId: resource.id,\n resource,\n bytesLoaded: 0,\n totalBytes: 0,\n startTime: Date.now(),\n isPreload,\n sessionId,\n clipId,\n trackId,\n promise,\n resolve,\n reject,\n };\n\n // Add to end of queue (no priority-based ordering)\n if (task.isPreload) {\n this.taskQueue.push(task);\n } else {\n // Insert before the first preload task to preserve FIFO among normal tasks.\n const firstPreloadIndex = this.taskQueue.findIndex((t) => t.isPreload);\n if (firstPreloadIndex < 0) {\n this.taskQueue.push(task);\n } else {\n this.taskQueue.splice(firstPreloadIndex, 0, task);\n }\n }\n this.queuedTasks.set(task.resourceId, task);\n\n return task;\n }\n\n /**\n * Get next task from queue if under concurrent limit\n */\n getNextTask(): LoadTask | null {\n if (this.taskQueue.length === 0 || this.concurrentCount >= this.maxConcurrent) {\n return null;\n }\n\n // Priority: always prefer normal (non-preload) tasks.\n const normalIndex = this.taskQueue.findIndex((t) => !t.isPreload);\n if (normalIndex >= 0) {\n const [task] = this.taskQueue.splice(normalIndex, 1);\n if (!task) return null;\n this.queuedTasks.delete(task.resourceId);\n return task;\n }\n\n // All remaining tasks are preloads: cap preload concurrency so foreground loads can proceed.\n if (this.concurrentPreloadCount >= this.maxPreloadConcurrent) {\n return null;\n }\n\n const task = this.taskQueue.shift() || null;\n if (!task) return null;\n this.queuedTasks.delete(task.resourceId);\n return task;\n }\n\n /**\n * Mark task as active\n */\n activateTask(task: LoadTask): void {\n this.activeTasks.set(task.resourceId, task);\n this.concurrentCount++;\n if (task.isPreload) {\n this.concurrentPreloadCount++;\n }\n }\n\n /**\n * Mark task as completed\n */\n completeTask(resourceId: string, error?: Error): void {\n const task = this.activeTasks.get(resourceId);\n if (task) {\n // Resolve or reject task's promise\n if (error) {\n task.reject?.(error);\n } else {\n task.resolve?.();\n }\n\n this.activeTasks.delete(resourceId);\n this.concurrentCount--;\n if (task.isPreload) {\n this.concurrentPreloadCount = Math.max(0, this.concurrentPreloadCount - 1);\n }\n }\n }\n\n /**\n * Cancel a task\n */\n cancelTask(resourceId: string): boolean {\n const task = this.activeTasks.get(resourceId);\n if (task) {\n task.controller?.abort();\n this.completeTask(resourceId);\n return true;\n }\n\n // Also remove from queue\n const index = this.taskQueue.findIndex((t) => t.resourceId === resourceId);\n if (index >= 0) {\n const [removed] = this.taskQueue.splice(index, 1);\n this.queuedTasks.delete(resourceId);\n // Keep previous semantics: cancel resolves (does not reject) awaiting callers.\n removed?.resolve?.();\n return true;\n }\n\n return false;\n }\n\n /**\n * Find an active task\n */\n getActiveTask(resourceId: string): LoadTask | undefined {\n return this.activeTasks.get(resourceId);\n }\n\n /**\n * Check if can process more tasks\n */\n get canProcess(): boolean {\n return this.concurrentCount < this.maxConcurrent;\n }\n\n private mergeTaskMeta(\n task: LoadTask,\n meta: { isPreload: boolean; sessionId?: string; clipId?: string; trackId?: string }\n ): LoadTask {\n // Upgrade preload to normal if needed.\n if (task.isPreload && !meta.isPreload) {\n const wasActive = this.activeTasks.get(task.resourceId) === task;\n if (wasActive) {\n this.concurrentPreloadCount = Math.max(0, this.concurrentPreloadCount - 1);\n }\n task.isPreload = false;\n }\n // Best-effort fill missing metadata (do not overwrite existing).\n if (task.sessionId === undefined && meta.sessionId !== undefined)\n task.sessionId = meta.sessionId;\n if (task.clipId === undefined && meta.clipId !== undefined) task.clipId = meta.clipId;\n if (task.trackId === undefined && meta.trackId !== undefined) task.trackId = meta.trackId;\n return task;\n }\n\n /**\n * Clear all tasks\n */\n clear(): void {\n // Cancel all active tasks\n for (const task of this.activeTasks.values()) {\n task.controller?.abort();\n }\n\n this.activeTasks.clear();\n this.taskQueue = [];\n this.queuedTasks.clear();\n this.concurrentCount = 0;\n this.concurrentPreloadCount = 0;\n }\n}\n"],"names":["task"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"MP4Muxer.d.ts","sourceRoot":"","sources":["../../../src/stages/mux/MP4Muxer.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,KAAK,CAA2B;IACxC,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,cAAc,CAAa;gBAEvB,MAAM,EAAE;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,SAAS,CAAC,EAAE,KAAK,GAAG,WAAW,GAAG,YAAY,CAAC;QAC/C,cAAc,CAAC,EAAE,GAAG,CAAC;QACrB,cAAc,CAAC,EAAE,GAAG,CAAC;KACtB;IA2CD,OAAO,CAAC,eAAe,CAAK;IAE5B,eAAe,CAAC,KAAK,EAAE,iBAAiB,EAAE,QAAQ,CAAC,EAAE,yBAAyB,GAAG,IAAI;IAwBrF,OAAO,CAAC,eAAe,CAAK;IAE5B,eAAe,CAAC,KAAK,EAAE,iBAAiB,EAAE,QAAQ,CAAC,EAAE,yBAAyB,GAAG,IAAI;IAmBrF,QAAQ,IAAI,IAAI;CAKjB"}
1
+ {"version":3,"file":"MP4Muxer.d.ts","sourceRoot":"","sources":["../../../src/stages/mux/MP4Muxer.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,KAAK,CAA2B;IACxC,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,cAAc,CAAa;gBAEvB,MAAM,EAAE;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,SAAS,CAAC,EAAE,KAAK,GAAG,WAAW,GAAG,YAAY,CAAC;QAC/C,cAAc,CAAC,EAAE,GAAG,CAAC;QACrB,cAAc,CAAC,EAAE,GAAG,CAAC;KACtB;IA2CD,OAAO,CAAC,eAAe,CAAK;IAE5B,eAAe,CAAC,KAAK,EAAE,iBAAiB,EAAE,QAAQ,CAAC,EAAE,yBAAyB,GAAG,IAAI;IAwBrF,OAAO,CAAC,eAAe,CAAK;IAE5B,eAAe,CAAC,KAAK,EAAE,iBAAiB,EAAE,QAAQ,CAAC,EAAE,yBAAyB,GAAG,IAAI;IA0BrF,QAAQ,IAAI,IAAI;CAKjB"}
@@ -46,6 +46,9 @@ class MP4Muxer {
46
46
  }
47
47
  audioChunkCount = 0;
48
48
  writeAudioChunk(chunk, metadata) {
49
+ if (chunk.byteLength <= 16) {
50
+ return;
51
+ }
49
52
  let meta;
50
53
  if (this.firstAudioChunk) {
51
54
  if (metadata && metadata.decoderConfig) {