@meframe/core 0.0.39 → 0.0.41
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/cache/CacheManager.d.ts +1 -0
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +2 -1
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/cache/l1/AudioL1Cache.d.ts.map +1 -1
- package/dist/cache/l1/AudioL1Cache.js +16 -5
- package/dist/cache/l1/AudioL1Cache.js.map +1 -1
- package/dist/cache/resource/MP4IndexCache.d.ts +4 -0
- package/dist/cache/resource/MP4IndexCache.d.ts.map +1 -1
- package/dist/cache/resource/MP4IndexCache.js +6 -0
- package/dist/cache/resource/MP4IndexCache.js.map +1 -1
- package/dist/cache/resource/ResourceCache.d.ts +3 -8
- package/dist/cache/resource/ResourceCache.d.ts.map +1 -1
- package/dist/cache/resource/ResourceCache.js +26 -11
- package/dist/cache/resource/ResourceCache.js.map +1 -1
- package/dist/cache/storage/opfs/OPFSManager.d.ts +28 -4
- package/dist/cache/storage/opfs/OPFSManager.d.ts.map +1 -1
- package/dist/cache/storage/opfs/OPFSManager.js +110 -4
- package/dist/cache/storage/opfs/OPFSManager.js.map +1 -1
- package/dist/cache/storage/opfs/types.d.ts +5 -0
- package/dist/cache/storage/opfs/types.d.ts.map +1 -1
- package/dist/config/defaults.js +1 -1
- package/dist/config/defaults.js.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.d.ts +5 -0
- package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.js +45 -57
- package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +2 -1
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.d.ts.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.js +3 -1
- package/dist/stages/compose/OfflineAudioMixer.js.map +1 -1
- package/dist/stages/demux/MP4IndexParser.d.ts +1 -0
- package/dist/stages/demux/MP4IndexParser.d.ts.map +1 -1
- package/dist/stages/demux/MP4IndexParser.js +20 -10
- package/dist/stages/demux/MP4IndexParser.js.map +1 -1
- package/dist/stages/load/ResourceLoader.d.ts +3 -1
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +45 -4
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/stages/load/TaskManager.d.ts +7 -1
- package/dist/stages/load/TaskManager.d.ts.map +1 -1
- package/dist/stages/load/TaskManager.js +40 -2
- package/dist/stages/load/TaskManager.js.map +1 -1
- package/dist/utils/errors.d.ts +27 -0
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +32 -0
- package/dist/utils/errors.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CacheManager.d.ts","sourceRoot":"","sources":["../../src/cache/CacheManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAIvD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEtD,UAAU,kBAAkB;IAC1B,EAAE,EAAE;QACF,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,QAAQ,EAAE;QACR,SAAS,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"CacheManager.d.ts","sourceRoot":"","sources":["../../src/cache/CacheManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,7 +17,8 @@ class CacheManager {
|
|
|
17
17
|
constructor(config, eventBus) {
|
|
18
18
|
this.videoL1Cache = new VideoL1Cache(config.l1);
|
|
19
19
|
this.audioL1Cache = new AudioL1Cache();
|
|
20
|
-
const
|
|
20
|
+
const maxSizeMB = config.resource.maxSizeMB ?? 5120;
|
|
21
|
+
const opfsManager = new OPFSManager(maxSizeMB);
|
|
21
22
|
this.resourceCache = new ResourceCache(opfsManager, config.resource.projectId);
|
|
22
23
|
this.mp4IndexCache = new MP4IndexCache();
|
|
23
24
|
this.audioSampleCache = new AudioSampleCache();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CacheManager.js","sources":["../../src/cache/CacheManager.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\nimport { RcFrame } from '../model';\nimport { VideoL1Cache } from './l1/VideoL1Cache';\nimport { MeframeEvent } from '../event/events';\nimport type { EventBus } from '../event/EventBus';\nimport type { EventPayloadMap } from '../event/events';\nimport { AudioL1Cache } from './l1/AudioL1Cache';\nimport { WaiterReplacedError } from '../utils/errors';\nimport { OPFSManager } from './storage/opfs/OPFSManager';\nimport { ResourceCache } from './resource/ResourceCache';\nimport { MP4IndexCache } from './resource/MP4IndexCache';\nimport { AudioSampleCache } from './resource/AudioSampleCache';\nimport type { MP4Index } from '../stages/demux/types';\n\ninterface CacheManagerConfig {\n l1: {\n maxMemoryMB: number;\n maxGOPs?: number;\n gopIntervalUs?: number;\n };\n resource: {\n projectId: string;\n };\n}\n\ninterface ClipReadyWaiter {\n clipId: string;\n minFrameCount: number;\n startTimeUs: TimeUs;\n currentCount: number;\n resolve: (ready: boolean) => void;\n reject: (reason?: unknown) => void;\n timeoutId?: ReturnType<typeof setTimeout>;\n}\n\n/**\n * CacheManager for Window Cache Architecture\n *\n * Core features:\n * - L1 (VRAM) for composed VideoFrames (window cache ±3s)\n * - ResourceCache (OPFS) for original MP4 files\n * - MP4IndexCache (RAM) for GOP/Sample indexes\n * - AudioSampleCache (RAM) for extracted audio chunks\n */\nexport class CacheManager {\n readonly videoL1Cache: VideoL1Cache;\n private readonly audioL1Cache: AudioL1Cache;\n readonly resourceCache: ResourceCache;\n readonly mp4IndexCache: MP4IndexCache;\n readonly audioSampleCache: AudioSampleCache;\n private clipReadyWaiters = new Map<string, ClipReadyWaiter>();\n private eventBus?: EventBus<EventPayloadMap>;\n\n constructor(config: CacheManagerConfig, eventBus?: EventBus<EventPayloadMap>) {\n this.videoL1Cache = new VideoL1Cache(config.l1);\n this.audioL1Cache = new AudioL1Cache();\n\n // Initialize resource cache\n const opfsManager = new OPFSManager();\n this.resourceCache = new ResourceCache(opfsManager, config.resource.projectId);\n this.mp4IndexCache = new MP4IndexCache();\n this.audioSampleCache = new AudioSampleCache();\n this.eventBus = eventBus;\n }\n\n async init(): Promise<void> {\n await this.resourceCache.init();\n }\n\n /**\n * Set window center for L1 cache management (unified for video and audio)\n * L1 cache uses center ±3.5s window for video, ±5s for audio\n */\n setWindow(centerTimeUs: TimeUs): void {\n this.videoL1Cache.setWindow(centerTimeUs);\n this.audioL1Cache.setWindow(centerTimeUs);\n }\n\n /**\n * Check if resource exists in OPFS cache\n */\n async hasResourceInCache(resourceId: string): Promise<boolean> {\n return await this.resourceCache.hasResource(resourceId);\n }\n\n /**\n * Read byte range from resource (for GOP-level access)\n */\n async readResourceRange(resourceId: string, start: number, end: number): Promise<ArrayBuffer> {\n return await this.resourceCache.readRange(resourceId, start, end);\n }\n\n /**\n * Get MP4 index for a resource\n */\n getMP4Index(resourceId: string): MP4Index | null {\n return this.mp4IndexCache.get(resourceId);\n }\n\n putClipAudioData(\n clipId: string,\n audioData: AudioData,\n clipDurationUs: TimeUs,\n globalTimeUs?: TimeUs\n ): void {\n this.audioL1Cache.putClipAudioData(clipId, audioData, clipDurationUs, globalTimeUs);\n }\n\n getClipPCMWithMetadata(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs\n ): { planes: Float32Array[]; sampleRate: number; numberOfChannels: number } | null {\n return this.audioL1Cache.getPCMWithMetadata(clipId, startUs, endUs);\n }\n\n hasClipPCM(clipId: string): boolean {\n return this.audioL1Cache.hasClipPCM(clipId);\n }\n\n /**\n * Check if sufficient PCM data exists for the requested time window\n * Returns true only if at least 80% of requested duration is available\n */\n hasWindowPCM(clipId: string, startUs: TimeUs, endUs: TimeUs): boolean {\n return this.audioL1Cache.hasWindowData(clipId, startUs, endUs);\n }\n\n clearAudioCache(): void {\n this.audioL1Cache.clear();\n }\n\n clearClipAudioData(clipId: string): void {\n this.audioL1Cache.clearClipPCM(clipId);\n }\n\n /**\n * Add a frame to L1 cache\n * Handles event notifications (CacheCover, ComposeFrameReady)\n * @param frame - VideoFrame to add\n * @param clipId - Clip identifier\n * @param frameDuration - Frame duration in microseconds\n * @param trackId - Track identifier\n * @param globalTimeUs - Global timestamp for event emission and window management\n */\n addFrame(\n frame: VideoFrame,\n clipId: string,\n frameDuration: TimeUs,\n trackId: string,\n globalTimeUs: TimeUs\n ): RcFrame {\n const rcFrame = this.videoL1Cache.addFrame(frame, clipId, frameDuration, trackId, globalTimeUs);\n\n const relativeTimeUs = frame.timestamp ?? 0;\n\n // Check and notify clip ready\n this.checkAndNotifyClipReady(clipId, relativeTimeUs);\n\n // Emit cover event for first frame (globalTimeUs = 0 in composition)\n if (globalTimeUs === 0) {\n this.eventBus?.emit(MeframeEvent.CacheCover, {\n timeUs: globalTimeUs,\n clipId,\n level: 'L1',\n size: rcFrame.sizeEstimate ?? 0,\n });\n }\n\n // Emit frame ready event\n this.eventBus?.emit(MeframeEvent.ComposeFrameReady, {\n timeUs: globalTimeUs,\n frameNumber: Math.floor(globalTimeUs / frameDuration),\n renderTimeMs: 0,\n trackId,\n clipId,\n });\n\n return rcFrame;\n }\n\n /**\n * Get frame from L1 cache\n * @param timeUs - Clip-relative timestamp (0-based)\n * @param clipId - Clip identifier\n */\n getFrame(timeUs: TimeUs, clipId: string): RcFrame | null {\n return this.videoL1Cache.get(timeUs, clipId);\n }\n\n async invalidateClip(clipId: string): Promise<void> {\n this.videoL1Cache.invalidateClip(clipId);\n }\n\n /**\n * Evict a clip from L1 cache\n */\n invalidateClipInL1(clipId: string): void {\n this.videoL1Cache.invalidateClip(clipId);\n }\n\n /**\n * Check if a clip is cached in L1\n */\n hasClipInL1(clipId: string): boolean {\n return this.videoL1Cache.hasClip(clipId);\n }\n\n /**\n * Wait for a clip to have minimum frames cached\n * Used by PlaybackController for buffering state\n * Only one waiter per clip - new waiter replaces old one\n */\n waitForClipReady(\n clipId: string,\n options: { minFrameCount?: number; timeoutMs?: number; startTimeUs?: TimeUs } = {}\n ): Promise<boolean> {\n const minFrameCount = options.minFrameCount ?? 30;\n const startTimeUs = options.startTimeUs ?? 0;\n\n // Check if already have enough frames\n const currentFrameCount = this.videoL1Cache.getClipFrameCount(clipId, startTimeUs);\n if (currentFrameCount >= minFrameCount) {\n return Promise.resolve(true);\n }\n\n // Cancel previous waiter if exists\n const oldWaiter = this.clipReadyWaiters.get(clipId);\n if (oldWaiter) {\n if (oldWaiter.timeoutId) {\n clearTimeout(oldWaiter.timeoutId);\n }\n oldWaiter.reject(new WaiterReplacedError(clipId));\n }\n\n return new Promise<boolean>((resolve, reject) => {\n const waiter: ClipReadyWaiter = {\n clipId,\n minFrameCount,\n startTimeUs,\n currentCount: currentFrameCount,\n resolve,\n reject,\n };\n\n this.clipReadyWaiters.set(clipId, waiter);\n\n if (options.timeoutMs && options.timeoutMs > 0) {\n waiter.timeoutId = setTimeout(() => {\n if (this.clipReadyWaiters.get(clipId) === waiter) {\n this.clipReadyWaiters.delete(clipId);\n }\n resolve(false);\n }, options.timeoutMs);\n }\n });\n }\n\n clearL1Cache(): void {\n this.videoL1Cache.clear();\n }\n\n async clear(): Promise<void> {\n this.clearL1Cache();\n this.clearAudioCache();\n }\n\n getMetadata() {\n return {\n l1: this.videoL1Cache.getMetadata(),\n };\n }\n\n /**\n * Check if incoming frame satisfies clip ready condition\n * O(1) complexity - only checks single waiter and increments counter\n */\n checkAndNotifyClipReady(clipId: string, frameTimestampUs: TimeUs): void {\n const waiter = this.clipReadyWaiters.get(clipId);\n if (!waiter) {\n return;\n }\n\n // Count all frames if startTimeUs is 0 (buffering scenario)\n // Otherwise only count frames at or after the target start time\n const shouldCount = waiter.startTimeUs === 0 || frameTimestampUs >= waiter.startTimeUs;\n\n if (shouldCount) {\n waiter.currentCount++;\n\n if (waiter.currentCount >= waiter.minFrameCount) {\n if (waiter.timeoutId) {\n clearTimeout(waiter.timeoutId);\n }\n waiter.resolve(true);\n this.clipReadyWaiters.delete(clipId);\n }\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CacheManager.js","sources":["../../src/cache/CacheManager.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\nimport { RcFrame } from '../model';\nimport { VideoL1Cache } from './l1/VideoL1Cache';\nimport { MeframeEvent } from '../event/events';\nimport type { EventBus } from '../event/EventBus';\nimport type { EventPayloadMap } from '../event/events';\nimport { AudioL1Cache } from './l1/AudioL1Cache';\nimport { WaiterReplacedError } from '../utils/errors';\nimport { OPFSManager } from './storage/opfs/OPFSManager';\nimport { ResourceCache } from './resource/ResourceCache';\nimport { MP4IndexCache } from './resource/MP4IndexCache';\nimport { AudioSampleCache } from './resource/AudioSampleCache';\nimport type { MP4Index } from '../stages/demux/types';\n\ninterface CacheManagerConfig {\n l1: {\n maxMemoryMB: number;\n maxGOPs?: number;\n gopIntervalUs?: number;\n };\n resource: {\n projectId: string;\n maxSizeMB?: number;\n };\n}\n\ninterface ClipReadyWaiter {\n clipId: string;\n minFrameCount: number;\n startTimeUs: TimeUs;\n currentCount: number;\n resolve: (ready: boolean) => void;\n reject: (reason?: unknown) => void;\n timeoutId?: ReturnType<typeof setTimeout>;\n}\n\n/**\n * CacheManager for Window Cache Architecture\n *\n * Core features:\n * - L1 (VRAM) for composed VideoFrames (window cache ±3s)\n * - ResourceCache (OPFS) for original MP4 files\n * - MP4IndexCache (RAM) for GOP/Sample indexes\n * - AudioSampleCache (RAM) for extracted audio chunks\n */\nexport class CacheManager {\n readonly videoL1Cache: VideoL1Cache;\n private readonly audioL1Cache: AudioL1Cache;\n readonly resourceCache: ResourceCache;\n readonly mp4IndexCache: MP4IndexCache;\n readonly audioSampleCache: AudioSampleCache;\n private clipReadyWaiters = new Map<string, ClipReadyWaiter>();\n private eventBus?: EventBus<EventPayloadMap>;\n\n constructor(config: CacheManagerConfig, eventBus?: EventBus<EventPayloadMap>) {\n this.videoL1Cache = new VideoL1Cache(config.l1);\n this.audioL1Cache = new AudioL1Cache();\n\n const maxSizeMB = config.resource.maxSizeMB ?? 5120;\n const opfsManager = new OPFSManager(maxSizeMB);\n this.resourceCache = new ResourceCache(opfsManager, config.resource.projectId);\n this.mp4IndexCache = new MP4IndexCache();\n this.audioSampleCache = new AudioSampleCache();\n this.eventBus = eventBus;\n }\n\n async init(): Promise<void> {\n await this.resourceCache.init();\n }\n\n /**\n * Set window center for L1 cache management (unified for video and audio)\n * L1 cache uses center ±3.5s window for video, ±5s for audio\n */\n setWindow(centerTimeUs: TimeUs): void {\n this.videoL1Cache.setWindow(centerTimeUs);\n this.audioL1Cache.setWindow(centerTimeUs);\n }\n\n /**\n * Check if resource exists in OPFS cache\n */\n async hasResourceInCache(resourceId: string): Promise<boolean> {\n return await this.resourceCache.hasResource(resourceId);\n }\n\n /**\n * Read byte range from resource (for GOP-level access)\n */\n async readResourceRange(resourceId: string, start: number, end: number): Promise<ArrayBuffer> {\n return await this.resourceCache.readRange(resourceId, start, end);\n }\n\n /**\n * Get MP4 index for a resource\n */\n getMP4Index(resourceId: string): MP4Index | null {\n return this.mp4IndexCache.get(resourceId);\n }\n\n putClipAudioData(\n clipId: string,\n audioData: AudioData,\n clipDurationUs: TimeUs,\n globalTimeUs?: TimeUs\n ): void {\n this.audioL1Cache.putClipAudioData(clipId, audioData, clipDurationUs, globalTimeUs);\n }\n\n getClipPCMWithMetadata(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs\n ): { planes: Float32Array[]; sampleRate: number; numberOfChannels: number } | null {\n return this.audioL1Cache.getPCMWithMetadata(clipId, startUs, endUs);\n }\n\n hasClipPCM(clipId: string): boolean {\n return this.audioL1Cache.hasClipPCM(clipId);\n }\n\n /**\n * Check if sufficient PCM data exists for the requested time window\n * Returns true only if at least 80% of requested duration is available\n */\n hasWindowPCM(clipId: string, startUs: TimeUs, endUs: TimeUs): boolean {\n return this.audioL1Cache.hasWindowData(clipId, startUs, endUs);\n }\n\n clearAudioCache(): void {\n this.audioL1Cache.clear();\n }\n\n clearClipAudioData(clipId: string): void {\n this.audioL1Cache.clearClipPCM(clipId);\n }\n\n /**\n * Add a frame to L1 cache\n * Handles event notifications (CacheCover, ComposeFrameReady)\n * @param frame - VideoFrame to add\n * @param clipId - Clip identifier\n * @param frameDuration - Frame duration in microseconds\n * @param trackId - Track identifier\n * @param globalTimeUs - Global timestamp for event emission and window management\n */\n addFrame(\n frame: VideoFrame,\n clipId: string,\n frameDuration: TimeUs,\n trackId: string,\n globalTimeUs: TimeUs\n ): RcFrame {\n const rcFrame = this.videoL1Cache.addFrame(frame, clipId, frameDuration, trackId, globalTimeUs);\n\n const relativeTimeUs = frame.timestamp ?? 0;\n\n // Check and notify clip ready\n this.checkAndNotifyClipReady(clipId, relativeTimeUs);\n\n // Emit cover event for first frame (globalTimeUs = 0 in composition)\n if (globalTimeUs === 0) {\n this.eventBus?.emit(MeframeEvent.CacheCover, {\n timeUs: globalTimeUs,\n clipId,\n level: 'L1',\n size: rcFrame.sizeEstimate ?? 0,\n });\n }\n\n // Emit frame ready event\n this.eventBus?.emit(MeframeEvent.ComposeFrameReady, {\n timeUs: globalTimeUs,\n frameNumber: Math.floor(globalTimeUs / frameDuration),\n renderTimeMs: 0,\n trackId,\n clipId,\n });\n\n return rcFrame;\n }\n\n /**\n * Get frame from L1 cache\n * @param timeUs - Clip-relative timestamp (0-based)\n * @param clipId - Clip identifier\n */\n getFrame(timeUs: TimeUs, clipId: string): RcFrame | null {\n return this.videoL1Cache.get(timeUs, clipId);\n }\n\n async invalidateClip(clipId: string): Promise<void> {\n this.videoL1Cache.invalidateClip(clipId);\n }\n\n /**\n * Evict a clip from L1 cache\n */\n invalidateClipInL1(clipId: string): void {\n this.videoL1Cache.invalidateClip(clipId);\n }\n\n /**\n * Check if a clip is cached in L1\n */\n hasClipInL1(clipId: string): boolean {\n return this.videoL1Cache.hasClip(clipId);\n }\n\n /**\n * Wait for a clip to have minimum frames cached\n * Used by PlaybackController for buffering state\n * Only one waiter per clip - new waiter replaces old one\n */\n waitForClipReady(\n clipId: string,\n options: { minFrameCount?: number; timeoutMs?: number; startTimeUs?: TimeUs } = {}\n ): Promise<boolean> {\n const minFrameCount = options.minFrameCount ?? 30;\n const startTimeUs = options.startTimeUs ?? 0;\n\n // Check if already have enough frames\n const currentFrameCount = this.videoL1Cache.getClipFrameCount(clipId, startTimeUs);\n if (currentFrameCount >= minFrameCount) {\n return Promise.resolve(true);\n }\n\n // Cancel previous waiter if exists\n const oldWaiter = this.clipReadyWaiters.get(clipId);\n if (oldWaiter) {\n if (oldWaiter.timeoutId) {\n clearTimeout(oldWaiter.timeoutId);\n }\n oldWaiter.reject(new WaiterReplacedError(clipId));\n }\n\n return new Promise<boolean>((resolve, reject) => {\n const waiter: ClipReadyWaiter = {\n clipId,\n minFrameCount,\n startTimeUs,\n currentCount: currentFrameCount,\n resolve,\n reject,\n };\n\n this.clipReadyWaiters.set(clipId, waiter);\n\n if (options.timeoutMs && options.timeoutMs > 0) {\n waiter.timeoutId = setTimeout(() => {\n if (this.clipReadyWaiters.get(clipId) === waiter) {\n this.clipReadyWaiters.delete(clipId);\n }\n resolve(false);\n }, options.timeoutMs);\n }\n });\n }\n\n clearL1Cache(): void {\n this.videoL1Cache.clear();\n }\n\n async clear(): Promise<void> {\n this.clearL1Cache();\n this.clearAudioCache();\n }\n\n getMetadata() {\n return {\n l1: this.videoL1Cache.getMetadata(),\n };\n }\n\n /**\n * Check if incoming frame satisfies clip ready condition\n * O(1) complexity - only checks single waiter and increments counter\n */\n checkAndNotifyClipReady(clipId: string, frameTimestampUs: TimeUs): void {\n const waiter = this.clipReadyWaiters.get(clipId);\n if (!waiter) {\n return;\n }\n\n // Count all frames if startTimeUs is 0 (buffering scenario)\n // Otherwise only count frames at or after the target start time\n const shouldCount = waiter.startTimeUs === 0 || frameTimestampUs >= waiter.startTimeUs;\n\n if (shouldCount) {\n waiter.currentCount++;\n\n if (waiter.currentCount >= waiter.minFrameCount) {\n if (waiter.timeoutId) {\n clearTimeout(waiter.timeoutId);\n }\n waiter.resolve(true);\n this.clipReadyWaiters.delete(clipId);\n }\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioL1Cache.d.ts","sourceRoot":"","sources":["../../../src/cache/l1/AudioL1Cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAIhD,UAAU,aAAa;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B,qBAAa,YAAY;IAEvB,OAAO,CAAC,eAAe,CAAsC;IAI7D,OAAO,CAAC,YAAY,CAAa;IAGjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAa;IAC3C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAO;IACzC,OAAO,CAAC,aAAa,CAAK;IAE1B,gBAAgB,CACd,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,EACpB,eAAe,EAAE,MAAM,EACvB,YAAY,CAAC,EAAE,MAAM,GACpB,IAAI;
|
|
1
|
+
{"version":3,"file":"AudioL1Cache.d.ts","sourceRoot":"","sources":["../../../src/cache/l1/AudioL1Cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAIhD,UAAU,aAAa;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B,qBAAa,YAAY;IAEvB,OAAO,CAAC,eAAe,CAAsC;IAI7D,OAAO,CAAC,YAAY,CAAa;IAGjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAa;IAC3C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAO;IACzC,OAAO,CAAC,aAAa,CAAK;IAE1B,gBAAgB,CACd,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,EACpB,eAAe,EAAE,MAAM,EACvB,YAAY,CAAC,EAAE,MAAM,GACpB,IAAI;IAiEP,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,GAAG,IAAI;IAmBxF,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,EAAE,GAAG,IAAI;IA8F7E,kBAAkB,CAChB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ;QAAE,MAAM,EAAE,YAAY,EAAE,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAoBlF,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAInC;;;OAGG;IACH,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAoCtE,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAIlC;;;OAGG;IACH,SAAS,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;IAKvC,OAAO,CAAC,aAAa;IAQrB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IA+BxB;;OAEG;IACH,OAAO,CAAC,eAAe;IAIvB,KAAK,IAAI,IAAI;IAIb,KAAK,IAAI,IAAI;IAMb,OAAO,IAAI,IAAI;CAGhB"}
|
|
@@ -37,11 +37,22 @@ class AudioL1Cache {
|
|
|
37
37
|
this.audioDataByClip.set(clipId, slots);
|
|
38
38
|
}
|
|
39
39
|
const insertIndex = this.findInsertIndex(slots, audioTimestampUs);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
const DUPLICATE_THRESHOLD_US = 0.5 * audioDurationUs;
|
|
41
|
+
if (insertIndex < slots.length) {
|
|
42
|
+
const existingSlot = slots[insertIndex];
|
|
43
|
+
const timeDiff = Math.abs(existingSlot.timestampUs - audioTimestampUs);
|
|
44
|
+
if (timeDiff < DUPLICATE_THRESHOLD_US) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (insertIndex > 0) {
|
|
49
|
+
const prevSlot = slots[insertIndex - 1];
|
|
50
|
+
const timeDiff = Math.abs(prevSlot.timestampUs - audioTimestampUs);
|
|
51
|
+
if (timeDiff < DUPLICATE_THRESHOLD_US) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
44
54
|
}
|
|
55
|
+
slots.splice(insertIndex, 0, slot);
|
|
45
56
|
}
|
|
46
57
|
getSlotsInWindow(clipId, startUs, endUs) {
|
|
47
58
|
const slots = this.audioDataByClip.get(clipId);
|
|
@@ -69,6 +80,7 @@ class AudioL1Cache {
|
|
|
69
80
|
if (overlappingSlots.length === 0) {
|
|
70
81
|
return null;
|
|
71
82
|
}
|
|
83
|
+
const requestedDurationUs = endUs - startUs;
|
|
72
84
|
const firstSlot = overlappingSlots[0];
|
|
73
85
|
const uniformSampleRate = firstSlot.sampleRate;
|
|
74
86
|
const uniformChannels = firstSlot.numberOfChannels;
|
|
@@ -86,7 +98,6 @@ class AudioL1Cache {
|
|
|
86
98
|
);
|
|
87
99
|
return null;
|
|
88
100
|
}
|
|
89
|
-
const requestedDurationUs = endUs - startUs;
|
|
90
101
|
const totalFrames = Math.ceil(requestedDurationUs / 1e6 * uniformSampleRate);
|
|
91
102
|
const result = Array.from(
|
|
92
103
|
{ length: uniformChannels },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioL1Cache.js","sources":["../../../src/cache/l1/AudioL1Cache.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport { binarySearchFirst, binarySearchOverlapping } from '../../utils/binary-search';\nimport { extractPlanesFromAudioData } from '../../utils/audio-data';\n\ninterface AudioDataSlot {\n timestampUs: TimeUs; // Clip-relative timestamp\n durationUs: TimeUs;\n planes: Float32Array[]; // PCM data for this slot\n sampleRate: number;\n numberOfChannels: number;\n globalTimeUs?: TimeUs; // Global timeline time (for window management)\n}\n\nexport type { AudioDataSlot };\n\nexport class AudioL1Cache {\n // Aligned with VideoL1Cache: array of discrete audio data slots per clip\n private audioDataByClip = new Map<string, AudioDataSlot[]>();\n\n // Unified window management (aligned with VideoL1Cache)\n // All clips share the same global window center\n private windowCenter: TimeUs = 0;\n\n // Window radius aligned with video (±3.5s, but we use 5s for audio safety margin)\n private readonly WINDOW_RADIUS = 5_000_000; // ±5s\n private readonly EVICT_THROTTLE_MS = 500;\n private lastEvictTime = 0;\n\n putClipAudioData(\n clipId: string,\n audioData: AudioData,\n _clipDurationUs: TimeUs,\n globalTimeUs?: TimeUs\n ): void {\n const numberOfChannels = audioData.numberOfChannels ?? 2;\n const numberOfFrames = audioData.numberOfFrames ?? 0;\n const sampleRate = audioData.sampleRate ?? 48_000;\n const audioTimestampUs = audioData.timestamp ?? 0;\n const audioDurationUs =\n audioData.duration ?? Math.round((numberOfFrames / sampleRate) * 1_000_000);\n\n if (!numberOfChannels || !numberOfFrames) {\n audioData.close();\n return;\n }\n\n // Extract PCM data\n const planes = extractPlanesFromAudioData(audioData, numberOfChannels, numberOfFrames);\n audioData.close();\n\n // Create audio data slot (aligned with video architecture)\n const slot: AudioDataSlot = {\n timestampUs: audioTimestampUs,\n durationUs: audioDurationUs,\n planes,\n sampleRate,\n numberOfChannels,\n globalTimeUs,\n };\n // Get or create slots array for this clip\n let slots = this.audioDataByClip.get(clipId);\n if (!slots) {\n slots = [];\n this.audioDataByClip.set(clipId, slots);\n }\n\n // Insert slot in sorted order (aligned with VideoL1Cache.addFrame)\n const insertIndex = this.findInsertIndex(slots, audioTimestampUs);\n\n // Check for duplicate timestamp - replace if exists\n if (insertIndex < slots.length && slots[insertIndex]!.timestampUs === audioTimestampUs) {\n // Replace existing slot at same timestamp\n slots[insertIndex] = slot;\n } else {\n // Insert new slot\n slots.splice(insertIndex, 0, slot);\n }\n }\n\n getSlotsInWindow(clipId: string, startUs: TimeUs, endUs: TimeUs): AudioDataSlot[] | null {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots || slots.length === 0) {\n return null;\n }\n\n // Use binary search to find overlapping slots\n const overlappingSlots = binarySearchOverlapping(slots, startUs, endUs, (slot) => ({\n start: slot.timestampUs,\n end: slot.timestampUs + slot.durationUs,\n }));\n\n if (overlappingSlots.length === 0) {\n return null;\n }\n\n return overlappingSlots;\n }\n\n getPCM(clipId: string, startUs: TimeUs, endUs: TimeUs): Float32Array[] | null {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots || slots.length === 0) {\n return null;\n }\n\n // Use binary search to find overlapping slots (O(log n + k) vs O(n))\n // Aligned with video GOP/frame search\n const overlappingSlots = binarySearchOverlapping(slots, startUs, endUs, (slot) => ({\n start: slot.timestampUs,\n end: slot.timestampUs + slot.durationUs,\n }));\n\n if (overlappingSlots.length === 0) {\n return null;\n }\n\n // Validate sample rate consistency across all slots\n const firstSlot = overlappingSlots[0]!;\n const uniformSampleRate = firstSlot.sampleRate;\n const uniformChannels = firstSlot.numberOfChannels;\n\n // Check if all slots have the same sample rate and channel count\n const hasUniformRate = overlappingSlots.every(\n (s) => s.sampleRate === uniformSampleRate && s.numberOfChannels === uniformChannels\n );\n\n if (!hasUniformRate) {\n console.error(\n `[AudioL1Cache] Inconsistent sample rates detected for clip ${clipId}:`,\n overlappingSlots.map((s) => ({\n timestamp: s.timestampUs,\n sampleRate: s.sampleRate,\n channels: s.numberOfChannels,\n }))\n );\n // Return null to avoid corrupted audio data\n // This will trigger re-decode with correct sample rate\n return null;\n }\n\n // Calculate total frame count needed\n const requestedDurationUs = endUs - startUs;\n const totalFrames = Math.ceil((requestedDurationUs / 1_000_000) * uniformSampleRate);\n\n // Initialize result arrays\n const result: Float32Array[] = Array.from(\n { length: uniformChannels },\n () => new Float32Array(totalFrames)\n );\n\n // Copy data from each overlapping slot\n for (const slot of overlappingSlots) {\n const slotStartUs = slot.timestampUs;\n const slotEndUs = slotStartUs + slot.durationUs;\n\n // Calculate intersection with requested range\n const copyStartUs = Math.max(slotStartUs, startUs);\n const copyEndUs = Math.min(slotEndUs, endUs);\n\n if (copyStartUs >= copyEndUs) continue;\n\n // Convert time to frame indices (all slots have same sample rate after validation)\n const srcOffsetFrames = Math.floor(\n ((copyStartUs - slotStartUs) / 1_000_000) * uniformSampleRate\n );\n const dstOffsetFrames = Math.floor(((copyStartUs - startUs) / 1_000_000) * uniformSampleRate);\n const copyFrameCount = Math.ceil(((copyEndUs - copyStartUs) / 1_000_000) * uniformSampleRate);\n\n // Copy each channel\n for (let ch = 0; ch < uniformChannels; ch++) {\n const srcPlane = slot.planes[ch];\n const dstPlane = result[ch];\n if (!srcPlane || !dstPlane) continue;\n\n // Boundary check\n const actualCopyFrames = Math.min(\n copyFrameCount,\n srcPlane.length - srcOffsetFrames,\n dstPlane.length - dstOffsetFrames\n );\n\n if (actualCopyFrames > 0) {\n const srcSlice = srcPlane.subarray(srcOffsetFrames, srcOffsetFrames + actualCopyFrames);\n dstPlane.set(srcSlice, dstOffsetFrames);\n }\n }\n }\n\n return result;\n }\n\n getPCMWithMetadata(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs\n ): { planes: Float32Array[]; sampleRate: number; numberOfChannels: number } | null {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots || slots.length === 0) {\n return null;\n }\n\n const planes = this.getPCM(clipId, startUs, endUs);\n if (!planes) {\n return null;\n }\n\n // Use first slot's metadata\n const firstSlot = slots[0]!;\n return {\n planes,\n sampleRate: firstSlot.sampleRate,\n numberOfChannels: firstSlot.numberOfChannels,\n };\n }\n\n hasClipPCM(clipId: string): boolean {\n return this.audioDataByClip.has(clipId);\n }\n\n /**\n * Check if sufficient PCM data exists for the requested time window\n * Returns true only if at least 80% of requested duration is available\n */\n hasWindowData(clipId: string, startUs: TimeUs, endUs: TimeUs): boolean {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots || slots.length === 0) {\n return false;\n }\n\n // Use binary search to find overlapping slots (performance optimization)\n const overlappingSlots = binarySearchOverlapping(slots, startUs, endUs, (slot) => ({\n start: slot.timestampUs,\n end: slot.timestampUs + slot.durationUs,\n }));\n\n if (overlappingSlots.length === 0) {\n return false;\n }\n\n // Calculate total duration covered\n let coveredDurationUs = 0;\n const requestedDurationUs = endUs - startUs;\n\n for (const slot of overlappingSlots) {\n const slotEndUs = slot.timestampUs + slot.durationUs;\n\n // Calculate overlap with requested range\n const overlapStart = Math.max(slot.timestampUs, startUs);\n const overlapEnd = Math.min(slotEndUs, endUs);\n\n if (overlapStart < overlapEnd) {\n coveredDurationUs += overlapEnd - overlapStart;\n }\n }\n\n // Consider window data sufficient if we have at least 80% coverage\n return coveredDurationUs >= requestedDurationUs * 0.8;\n }\n\n clearClipPCM(clipId: string): void {\n this.audioDataByClip.delete(clipId);\n }\n\n /**\n * Update window center (unified global window)\n * Aligned with VideoL1Cache strategy: maintains a window of ±RADIUS around center\n */\n setWindow(centerGlobalUs: TimeUs): void {\n this.windowCenter = centerGlobalUs;\n this.checkEviction();\n }\n\n private checkEviction(): void {\n const now = Date.now();\n if (now - this.lastEvictTime > this.EVICT_THROTTLE_MS) {\n this.evictOutOfWindow();\n this.lastEvictTime = now;\n }\n }\n\n /**\n * Evict audio slots outside the global window (aligned with VideoL1Cache)\n * Skip if eviction is disabled (e.g., during export)\n */\n private evictOutOfWindow(): void {\n const windowStart = Math.max(0, this.windowCenter - this.WINDOW_RADIUS);\n const windowEnd = this.windowCenter + this.WINDOW_RADIUS;\n\n for (const [clipId, slots] of this.audioDataByClip) {\n const toKeep: AudioDataSlot[] = [];\n\n for (const slot of slots) {\n const globalTime = slot.globalTimeUs;\n\n // Slots without globalTimeUs are kept (legacy)\n if (globalTime === undefined) {\n toKeep.push(slot);\n continue;\n }\n\n // Keep slots within window\n if (globalTime >= windowStart && globalTime <= windowEnd) {\n toKeep.push(slot);\n }\n // Slots outside window are discarded (no close needed for Float32Array)\n }\n\n if (toKeep.length > 0) {\n this.audioDataByClip.set(clipId, toKeep);\n } else {\n this.audioDataByClip.delete(clipId);\n }\n }\n }\n\n /**\n * Find insertion index for a new slot (aligned with VideoL1Cache)\n */\n private findInsertIndex(slots: AudioDataSlot[], timestamp: TimeUs): number {\n return binarySearchFirst(slots, (slot) => slot.timestampUs >= timestamp);\n }\n\n flush(): void {\n this.audioDataByClip.clear();\n }\n\n clear(): void {\n this.flush();\n this.audioDataByClip.clear();\n this.windowCenter = 0;\n }\n\n dispose(): void {\n this.clear();\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AudioL1Cache.js","sources":["../../../src/cache/l1/AudioL1Cache.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport { binarySearchFirst, binarySearchOverlapping } from '../../utils/binary-search';\nimport { extractPlanesFromAudioData } from '../../utils/audio-data';\n\ninterface AudioDataSlot {\n timestampUs: TimeUs; // Clip-relative timestamp\n durationUs: TimeUs;\n planes: Float32Array[]; // PCM data for this slot\n sampleRate: number;\n numberOfChannels: number;\n globalTimeUs?: TimeUs; // Global timeline time (for window management)\n}\n\nexport type { AudioDataSlot };\n\nexport class AudioL1Cache {\n // Aligned with VideoL1Cache: array of discrete audio data slots per clip\n private audioDataByClip = new Map<string, AudioDataSlot[]>();\n\n // Unified window management (aligned with VideoL1Cache)\n // All clips share the same global window center\n private windowCenter: TimeUs = 0;\n\n // Window radius aligned with video (±3.5s, but we use 5s for audio safety margin)\n private readonly WINDOW_RADIUS = 5_000_000; // ±5s\n private readonly EVICT_THROTTLE_MS = 500;\n private lastEvictTime = 0;\n\n putClipAudioData(\n clipId: string,\n audioData: AudioData,\n _clipDurationUs: TimeUs,\n globalTimeUs?: TimeUs\n ): void {\n const numberOfChannels = audioData.numberOfChannels ?? 2;\n const numberOfFrames = audioData.numberOfFrames ?? 0;\n const sampleRate = audioData.sampleRate ?? 48_000;\n const audioTimestampUs = audioData.timestamp ?? 0;\n const audioDurationUs =\n audioData.duration ?? Math.round((numberOfFrames / sampleRate) * 1_000_000);\n\n if (!numberOfChannels || !numberOfFrames) {\n audioData.close();\n return;\n }\n\n // Extract PCM data\n const planes = extractPlanesFromAudioData(audioData, numberOfChannels, numberOfFrames);\n audioData.close();\n\n // Create audio data slot (aligned with video architecture)\n const slot: AudioDataSlot = {\n timestampUs: audioTimestampUs,\n durationUs: audioDurationUs,\n planes,\n sampleRate,\n numberOfChannels,\n globalTimeUs,\n };\n // Get or create slots array for this clip\n let slots = this.audioDataByClip.get(clipId);\n if (!slots) {\n slots = [];\n this.audioDataByClip.set(clipId, slots);\n }\n\n // Insert slot in sorted order (aligned with VideoL1Cache.addFrame)\n const insertIndex = this.findInsertIndex(slots, audioTimestampUs);\n\n // Deduplication: Check for duplicate or near-duplicate timestamp\n // Tolerance of 1ms to handle floating-point precision and concurrent decode attempts\n const DUPLICATE_THRESHOLD_US = 0.5 * audioDurationUs;\n\n if (insertIndex < slots.length) {\n const existingSlot = slots[insertIndex]!;\n const timeDiff = Math.abs(existingSlot.timestampUs - audioTimestampUs);\n\n if (timeDiff < DUPLICATE_THRESHOLD_US) {\n // Near-duplicate detected, skip insertion\n return;\n }\n }\n\n // Also check previous slot to catch overlapping duplicates\n if (insertIndex > 0) {\n const prevSlot = slots[insertIndex - 1]!;\n const timeDiff = Math.abs(prevSlot.timestampUs - audioTimestampUs);\n\n if (timeDiff < DUPLICATE_THRESHOLD_US) {\n // Near-duplicate detected, skip insertion\n return;\n }\n }\n\n // Insert new slot\n slots.splice(insertIndex, 0, slot);\n }\n\n getSlotsInWindow(clipId: string, startUs: TimeUs, endUs: TimeUs): AudioDataSlot[] | null {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots || slots.length === 0) {\n return null;\n }\n\n // Use binary search to find overlapping slots\n const overlappingSlots = binarySearchOverlapping(slots, startUs, endUs, (slot) => ({\n start: slot.timestampUs,\n end: slot.timestampUs + slot.durationUs,\n }));\n\n if (overlappingSlots.length === 0) {\n return null;\n }\n\n return overlappingSlots;\n }\n\n getPCM(clipId: string, startUs: TimeUs, endUs: TimeUs): Float32Array[] | null {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots || slots.length === 0) {\n return null;\n }\n\n // Use binary search to find overlapping slots (O(log n + k) vs O(n))\n // Aligned with video GOP/frame search\n const overlappingSlots = binarySearchOverlapping(slots, startUs, endUs, (slot) => ({\n start: slot.timestampUs,\n end: slot.timestampUs + slot.durationUs,\n }));\n\n if (overlappingSlots.length === 0) {\n return null;\n }\n\n const requestedDurationUs = endUs - startUs;\n\n // Validate sample rate consistency across all slots\n const firstSlot = overlappingSlots[0]!;\n const uniformSampleRate = firstSlot.sampleRate;\n const uniformChannels = firstSlot.numberOfChannels;\n\n // Check if all slots have the same sample rate and channel count\n const hasUniformRate = overlappingSlots.every(\n (s) => s.sampleRate === uniformSampleRate && s.numberOfChannels === uniformChannels\n );\n\n if (!hasUniformRate) {\n console.error(\n `[AudioL1Cache] Inconsistent sample rates detected for clip ${clipId}:`,\n overlappingSlots.map((s) => ({\n timestamp: s.timestampUs,\n sampleRate: s.sampleRate,\n channels: s.numberOfChannels,\n }))\n );\n // Return null to avoid corrupted audio data\n // This will trigger re-decode with correct sample rate\n return null;\n }\n\n // Calculate total frame count needed\n const totalFrames = Math.ceil((requestedDurationUs / 1_000_000) * uniformSampleRate);\n\n // Initialize result arrays\n const result: Float32Array[] = Array.from(\n { length: uniformChannels },\n () => new Float32Array(totalFrames)\n );\n\n // Copy data from each overlapping slot\n // Note: Slots are guaranteed non-overlapping by putClipAudioData deduplication\n for (const slot of overlappingSlots) {\n const slotStartUs = slot.timestampUs;\n const slotEndUs = slotStartUs + slot.durationUs;\n\n // Calculate intersection with requested range\n const copyStartUs = Math.max(slotStartUs, startUs);\n const copyEndUs = Math.min(slotEndUs, endUs);\n\n if (copyStartUs >= copyEndUs) continue;\n\n // Convert time to frame indices (all slots have same sample rate after validation)\n const srcOffsetFrames = Math.floor(\n ((copyStartUs - slotStartUs) / 1_000_000) * uniformSampleRate\n );\n const dstOffsetFrames = Math.floor(((copyStartUs - startUs) / 1_000_000) * uniformSampleRate);\n const copyFrameCount = Math.ceil(((copyEndUs - copyStartUs) / 1_000_000) * uniformSampleRate);\n\n // Copy each channel\n for (let ch = 0; ch < uniformChannels; ch++) {\n const srcPlane = slot.planes[ch];\n const dstPlane = result[ch];\n if (!srcPlane || !dstPlane) continue;\n\n // Boundary check\n const actualCopyFrames = Math.min(\n copyFrameCount,\n srcPlane.length - srcOffsetFrames,\n dstPlane.length - dstOffsetFrames\n );\n\n if (actualCopyFrames > 0) {\n const srcSlice = srcPlane.subarray(srcOffsetFrames, srcOffsetFrames + actualCopyFrames);\n dstPlane.set(srcSlice, dstOffsetFrames);\n }\n }\n }\n\n return result;\n }\n\n getPCMWithMetadata(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs\n ): { planes: Float32Array[]; sampleRate: number; numberOfChannels: number } | null {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots || slots.length === 0) {\n return null;\n }\n\n const planes = this.getPCM(clipId, startUs, endUs);\n if (!planes) {\n return null;\n }\n\n // Use first slot's metadata\n const firstSlot = slots[0]!;\n return {\n planes,\n sampleRate: firstSlot.sampleRate,\n numberOfChannels: firstSlot.numberOfChannels,\n };\n }\n\n hasClipPCM(clipId: string): boolean {\n return this.audioDataByClip.has(clipId);\n }\n\n /**\n * Check if sufficient PCM data exists for the requested time window\n * Returns true only if at least 80% of requested duration is available\n */\n hasWindowData(clipId: string, startUs: TimeUs, endUs: TimeUs): boolean {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots || slots.length === 0) {\n return false;\n }\n\n // Use binary search to find overlapping slots (performance optimization)\n const overlappingSlots = binarySearchOverlapping(slots, startUs, endUs, (slot) => ({\n start: slot.timestampUs,\n end: slot.timestampUs + slot.durationUs,\n }));\n\n if (overlappingSlots.length === 0) {\n return false;\n }\n\n // Calculate total duration covered\n let coveredDurationUs = 0;\n const requestedDurationUs = endUs - startUs;\n\n for (const slot of overlappingSlots) {\n const slotEndUs = slot.timestampUs + slot.durationUs;\n\n // Calculate overlap with requested range\n const overlapStart = Math.max(slot.timestampUs, startUs);\n const overlapEnd = Math.min(slotEndUs, endUs);\n\n if (overlapStart < overlapEnd) {\n coveredDurationUs += overlapEnd - overlapStart;\n }\n }\n\n // Consider window data sufficient if we have at least 80% coverage\n return coveredDurationUs >= requestedDurationUs * 0.8;\n }\n\n clearClipPCM(clipId: string): void {\n this.audioDataByClip.delete(clipId);\n }\n\n /**\n * Update window center (unified global window)\n * Aligned with VideoL1Cache strategy: maintains a window of ±RADIUS around center\n */\n setWindow(centerGlobalUs: TimeUs): void {\n this.windowCenter = centerGlobalUs;\n this.checkEviction();\n }\n\n private checkEviction(): void {\n const now = Date.now();\n if (now - this.lastEvictTime > this.EVICT_THROTTLE_MS) {\n this.evictOutOfWindow();\n this.lastEvictTime = now;\n }\n }\n\n /**\n * Evict audio slots outside the global window (aligned with VideoL1Cache)\n * Skip if eviction is disabled (e.g., during export)\n */\n private evictOutOfWindow(): void {\n const windowStart = Math.max(0, this.windowCenter - this.WINDOW_RADIUS);\n const windowEnd = this.windowCenter + this.WINDOW_RADIUS;\n\n for (const [clipId, slots] of this.audioDataByClip) {\n const toKeep: AudioDataSlot[] = [];\n\n for (const slot of slots) {\n const globalTime = slot.globalTimeUs;\n\n // Slots without globalTimeUs are kept (legacy)\n if (globalTime === undefined) {\n toKeep.push(slot);\n continue;\n }\n\n // Keep slots within window\n if (globalTime >= windowStart && globalTime <= windowEnd) {\n toKeep.push(slot);\n }\n // Slots outside window are discarded (no close needed for Float32Array)\n }\n\n if (toKeep.length > 0) {\n this.audioDataByClip.set(clipId, toKeep);\n } else {\n this.audioDataByClip.delete(clipId);\n }\n }\n }\n\n /**\n * Find insertion index for a new slot (aligned with VideoL1Cache)\n */\n private findInsertIndex(slots: AudioDataSlot[], timestamp: TimeUs): number {\n return binarySearchFirst(slots, (slot) => slot.timestampUs >= timestamp);\n }\n\n flush(): void {\n this.audioDataByClip.clear();\n }\n\n clear(): void {\n this.flush();\n this.audioDataByClip.clear();\n this.windowCenter = 0;\n }\n\n dispose(): void {\n this.clear();\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -44,5 +44,9 @@ export declare class MP4IndexCache {
|
|
|
44
44
|
* Remove index for a specific resource
|
|
45
45
|
*/
|
|
46
46
|
remove(resourceId: string): void;
|
|
47
|
+
/**
|
|
48
|
+
* Delete index for a resource (alias for remove)
|
|
49
|
+
*/
|
|
50
|
+
delete(resourceId: string): void;
|
|
47
51
|
}
|
|
48
52
|
//# sourceMappingURL=MP4IndexCache.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MP4IndexCache.d.ts","sourceRoot":"","sources":["../../../src/cache/resource/MP4IndexCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAC3E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAGhD;;;;;;;GAOG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAA+B;IAE9C;;OAEG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,IAAI;IAI9C;;OAEG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IAIxC;;OAEG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAIhC;;;OAGG;IACH,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IA0CvE;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAclE;;OAEG;IACH,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAwBrE;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;CAGjC"}
|
|
1
|
+
{"version":3,"file":"MP4IndexCache.d.ts","sourceRoot":"","sources":["../../../src/cache/resource/MP4IndexCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAC3E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAGhD;;;;;;;GAOG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAA+B;IAE9C;;OAEG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,IAAI;IAI9C;;OAEG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IAIxC;;OAEG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAIhC;;;OAGG;IACH,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IA0CvE;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAclE;;OAEG;IACH,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAwBrE;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAIhC;;OAEG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;CAGjC"}
|
|
@@ -97,6 +97,12 @@ class MP4IndexCache {
|
|
|
97
97
|
remove(resourceId) {
|
|
98
98
|
this.indexes.delete(resourceId);
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Delete index for a resource (alias for remove)
|
|
102
|
+
*/
|
|
103
|
+
delete(resourceId) {
|
|
104
|
+
this.indexes.delete(resourceId);
|
|
105
|
+
}
|
|
100
106
|
}
|
|
101
107
|
export {
|
|
102
108
|
MP4IndexCache
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MP4IndexCache.js","sources":["../../../src/cache/resource/MP4IndexCache.ts"],"sourcesContent":["import type { MP4Index, Sample, GOPRange } from '../../stages/demux/types';\nimport type { TimeUs } from '../../model/types';\nimport { binarySearchRange, binarySearchFirst } from '../../utils/binary-search';\n\n/**\n * MP4IndexCache - In-memory cache for MP4 indexes\n *\n * Provides fast lookup for:\n * - Sample at specific timestamp\n * - GOP range for time range\n * - Nearest keyframe\n */\nexport class MP4IndexCache {\n private indexes = new Map<string, MP4Index>();\n\n /**\n * Set index for a resource\n */\n set(resourceId: string, index: MP4Index): void {\n this.indexes.set(resourceId, index);\n }\n\n /**\n * Get index for a resource\n */\n get(resourceId: string): MP4Index | null {\n return this.indexes.get(resourceId) || null;\n }\n\n /**\n * Check if index exists for a resource\n */\n has(resourceId: string): boolean {\n return this.indexes.has(resourceId);\n }\n\n /**\n * Get GOP range (byte range) for a specific time\n * Returns the GOP that contains the target time\n */\n getGOPRangeForTime(resourceId: string, timeUs: TimeUs): GOPRange | null {\n const index = this.indexes.get(resourceId);\n if (!index?.tracks.video) return null;\n\n const { samples, gopIndex } = index.tracks.video;\n\n // Find GOP that contains this timestamp using binary search\n const targetGOP = binarySearchRange(gopIndex, timeUs, (gop, idx) => {\n const nextGOP = gopIndex[idx + 1];\n return {\n start: gop.startTimeUs,\n end: nextGOP ? nextGOP.startTimeUs : Infinity,\n };\n });\n\n if (!targetGOP) {\n console.warn('[MP4IndexCache] No GOP found for timeUs:', timeUs);\n return null;\n }\n\n // Calculate byte range for this GOP\n const startSample = samples[targetGOP.keyframeSampleIndex];\n const endSampleIndex = targetGOP.keyframeSampleIndex + targetGOP.sampleCount - 1;\n const endSample = samples[endSampleIndex];\n\n if (!startSample || !endSample) {\n console.error('[MP4IndexCache] Missing samples for GOP:', {\n keyframeSampleIndex: targetGOP.keyframeSampleIndex,\n sampleCount: targetGOP.sampleCount,\n totalSamples: samples.length,\n });\n return null;\n }\n\n return {\n byteStart: startSample.byteOffset,\n byteEnd: endSample.byteOffset + endSample.byteLength,\n startTimeUs: startSample.timestamp,\n endTimeUs: endSample.timestamp + endSample.duration,\n };\n }\n\n /**\n * Get sample at specific timestamp\n */\n getSampleAtTime(resourceId: string, timeUs: TimeUs): Sample | null {\n const index = this.indexes.get(resourceId);\n if (!index?.tracks.video) return null;\n\n const { samples } = index.tracks.video;\n\n const result = binarySearchRange(samples, timeUs, (sample, _index) => ({\n start: sample.timestamp,\n end: sample.timestamp + sample.duration,\n }));\n\n return result || null;\n }\n\n /**\n * Get nearest keyframe before or at the target time\n */\n getNearestKeyframe(resourceId: string, timeUs: TimeUs): Sample | null {\n const index = this.indexes.get(resourceId);\n if (!index?.tracks.video) return null;\n\n const { samples, gopIndex } = index.tracks.video;\n\n // Find the first GOP that starts after timeUs using binary search\n const firstGOPAfterIndex = binarySearchFirst(gopIndex, (gop) => gop.startTimeUs > timeUs);\n\n // The nearest keyframe is in the GOP before firstGOPAfterIndex\n const targetGOPIndex = firstGOPAfterIndex - 1;\n\n if (targetGOPIndex < 0) {\n // timeUs is before the first GOP\n return null;\n }\n\n const targetGOP = gopIndex[targetGOPIndex];\n if (!targetGOP) return null;\n\n // Return the keyframe sample of this GOP\n return samples[targetGOP.keyframeSampleIndex] ?? null;\n }\n\n /**\n * Clear all cached indexes\n */\n clear(): void {\n this.indexes.clear();\n }\n\n /**\n * Remove index for a specific resource\n */\n remove(resourceId: string): void {\n this.indexes.delete(resourceId);\n }\n}\n"],"names":[],"mappings":";AAYO,MAAM,cAAc;AAAA,EACjB,8BAAc,IAAA;AAAA;AAAA;AAAA;AAAA,EAKtB,IAAI,YAAoB,OAAuB;AAC7C,SAAK,QAAQ,IAAI,YAAY,KAAK;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,YAAqC;AACvC,WAAO,KAAK,QAAQ,IAAI,UAAU,KAAK;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,YAA6B;AAC/B,WAAO,KAAK,QAAQ,IAAI,UAAU;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,mBAAmB,YAAoB,QAAiC;AACtE,UAAM,QAAQ,KAAK,QAAQ,IAAI,UAAU;AACzC,QAAI,CAAC,OAAO,OAAO,MAAO,QAAO;AAEjC,UAAM,EAAE,SAAS,SAAA,IAAa,MAAM,OAAO;AAG3C,UAAM,YAAY,kBAAkB,UAAU,QAAQ,CAAC,KAAK,QAAQ;AAClE,YAAM,UAAU,SAAS,MAAM,CAAC;AAChC,aAAO;AAAA,QACL,OAAO,IAAI;AAAA,QACX,KAAK,UAAU,QAAQ,cAAc;AAAA,MAAA;AAAA,IAEzC,CAAC;AAED,QAAI,CAAC,WAAW;AACd,cAAQ,KAAK,4CAA4C,MAAM;AAC/D,aAAO;AAAA,IACT;AAGA,UAAM,cAAc,QAAQ,UAAU,mBAAmB;AACzD,UAAM,iBAAiB,UAAU,sBAAsB,UAAU,cAAc;AAC/E,UAAM,YAAY,QAAQ,cAAc;AAExC,QAAI,CAAC,eAAe,CAAC,WAAW;AAC9B,cAAQ,MAAM,4CAA4C;AAAA,QACxD,qBAAqB,UAAU;AAAA,QAC/B,aAAa,UAAU;AAAA,QACvB,cAAc,QAAQ;AAAA,MAAA,CACvB;AACD,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL,WAAW,YAAY;AAAA,MACvB,SAAS,UAAU,aAAa,UAAU;AAAA,MAC1C,aAAa,YAAY;AAAA,MACzB,WAAW,UAAU,YAAY,UAAU;AAAA,IAAA;AAAA,EAE/C;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,YAAoB,QAA+B;AACjE,UAAM,QAAQ,KAAK,QAAQ,IAAI,UAAU;AACzC,QAAI,CAAC,OAAO,OAAO,MAAO,QAAO;AAEjC,UAAM,EAAE,QAAA,IAAY,MAAM,OAAO;AAEjC,UAAM,SAAS,kBAAkB,SAAS,QAAQ,CAAC,QAAQ,YAAY;AAAA,MACrE,OAAO,OAAO;AAAA,MACd,KAAK,OAAO,YAAY,OAAO;AAAA,IAAA,EAC/B;AAEF,WAAO,UAAU;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAmB,YAAoB,QAA+B;AACpE,UAAM,QAAQ,KAAK,QAAQ,IAAI,UAAU;AACzC,QAAI,CAAC,OAAO,OAAO,MAAO,QAAO;AAEjC,UAAM,EAAE,SAAS,SAAA,IAAa,MAAM,OAAO;AAG3C,UAAM,qBAAqB,kBAAkB,UAAU,CAAC,QAAQ,IAAI,cAAc,MAAM;AAGxF,UAAM,iBAAiB,qBAAqB;AAE5C,QAAI,iBAAiB,GAAG;AAEtB,aAAO;AAAA,IACT;AAEA,UAAM,YAAY,SAAS,cAAc;AACzC,QAAI,CAAC,UAAW,QAAO;AAGvB,WAAO,QAAQ,UAAU,mBAAmB,KAAK;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,SAAK,QAAQ,MAAA;AAAA,EACf;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,YAA0B;AAC/B,SAAK,QAAQ,OAAO,UAAU;AAAA,EAChC;AACF;"}
|
|
1
|
+
{"version":3,"file":"MP4IndexCache.js","sources":["../../../src/cache/resource/MP4IndexCache.ts"],"sourcesContent":["import type { MP4Index, Sample, GOPRange } from '../../stages/demux/types';\nimport type { TimeUs } from '../../model/types';\nimport { binarySearchRange, binarySearchFirst } from '../../utils/binary-search';\n\n/**\n * MP4IndexCache - In-memory cache for MP4 indexes\n *\n * Provides fast lookup for:\n * - Sample at specific timestamp\n * - GOP range for time range\n * - Nearest keyframe\n */\nexport class MP4IndexCache {\n private indexes = new Map<string, MP4Index>();\n\n /**\n * Set index for a resource\n */\n set(resourceId: string, index: MP4Index): void {\n this.indexes.set(resourceId, index);\n }\n\n /**\n * Get index for a resource\n */\n get(resourceId: string): MP4Index | null {\n return this.indexes.get(resourceId) || null;\n }\n\n /**\n * Check if index exists for a resource\n */\n has(resourceId: string): boolean {\n return this.indexes.has(resourceId);\n }\n\n /**\n * Get GOP range (byte range) for a specific time\n * Returns the GOP that contains the target time\n */\n getGOPRangeForTime(resourceId: string, timeUs: TimeUs): GOPRange | null {\n const index = this.indexes.get(resourceId);\n if (!index?.tracks.video) return null;\n\n const { samples, gopIndex } = index.tracks.video;\n\n // Find GOP that contains this timestamp using binary search\n const targetGOP = binarySearchRange(gopIndex, timeUs, (gop, idx) => {\n const nextGOP = gopIndex[idx + 1];\n return {\n start: gop.startTimeUs,\n end: nextGOP ? nextGOP.startTimeUs : Infinity,\n };\n });\n\n if (!targetGOP) {\n console.warn('[MP4IndexCache] No GOP found for timeUs:', timeUs);\n return null;\n }\n\n // Calculate byte range for this GOP\n const startSample = samples[targetGOP.keyframeSampleIndex];\n const endSampleIndex = targetGOP.keyframeSampleIndex + targetGOP.sampleCount - 1;\n const endSample = samples[endSampleIndex];\n\n if (!startSample || !endSample) {\n console.error('[MP4IndexCache] Missing samples for GOP:', {\n keyframeSampleIndex: targetGOP.keyframeSampleIndex,\n sampleCount: targetGOP.sampleCount,\n totalSamples: samples.length,\n });\n return null;\n }\n\n return {\n byteStart: startSample.byteOffset,\n byteEnd: endSample.byteOffset + endSample.byteLength,\n startTimeUs: startSample.timestamp,\n endTimeUs: endSample.timestamp + endSample.duration,\n };\n }\n\n /**\n * Get sample at specific timestamp\n */\n getSampleAtTime(resourceId: string, timeUs: TimeUs): Sample | null {\n const index = this.indexes.get(resourceId);\n if (!index?.tracks.video) return null;\n\n const { samples } = index.tracks.video;\n\n const result = binarySearchRange(samples, timeUs, (sample, _index) => ({\n start: sample.timestamp,\n end: sample.timestamp + sample.duration,\n }));\n\n return result || null;\n }\n\n /**\n * Get nearest keyframe before or at the target time\n */\n getNearestKeyframe(resourceId: string, timeUs: TimeUs): Sample | null {\n const index = this.indexes.get(resourceId);\n if (!index?.tracks.video) return null;\n\n const { samples, gopIndex } = index.tracks.video;\n\n // Find the first GOP that starts after timeUs using binary search\n const firstGOPAfterIndex = binarySearchFirst(gopIndex, (gop) => gop.startTimeUs > timeUs);\n\n // The nearest keyframe is in the GOP before firstGOPAfterIndex\n const targetGOPIndex = firstGOPAfterIndex - 1;\n\n if (targetGOPIndex < 0) {\n // timeUs is before the first GOP\n return null;\n }\n\n const targetGOP = gopIndex[targetGOPIndex];\n if (!targetGOP) return null;\n\n // Return the keyframe sample of this GOP\n return samples[targetGOP.keyframeSampleIndex] ?? null;\n }\n\n /**\n * Clear all cached indexes\n */\n clear(): void {\n this.indexes.clear();\n }\n\n /**\n * Remove index for a specific resource\n */\n remove(resourceId: string): void {\n this.indexes.delete(resourceId);\n }\n\n /**\n * Delete index for a resource (alias for remove)\n */\n delete(resourceId: string): void {\n this.indexes.delete(resourceId);\n }\n}\n"],"names":[],"mappings":";AAYO,MAAM,cAAc;AAAA,EACjB,8BAAc,IAAA;AAAA;AAAA;AAAA;AAAA,EAKtB,IAAI,YAAoB,OAAuB;AAC7C,SAAK,QAAQ,IAAI,YAAY,KAAK;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,YAAqC;AACvC,WAAO,KAAK,QAAQ,IAAI,UAAU,KAAK;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,YAA6B;AAC/B,WAAO,KAAK,QAAQ,IAAI,UAAU;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,mBAAmB,YAAoB,QAAiC;AACtE,UAAM,QAAQ,KAAK,QAAQ,IAAI,UAAU;AACzC,QAAI,CAAC,OAAO,OAAO,MAAO,QAAO;AAEjC,UAAM,EAAE,SAAS,SAAA,IAAa,MAAM,OAAO;AAG3C,UAAM,YAAY,kBAAkB,UAAU,QAAQ,CAAC,KAAK,QAAQ;AAClE,YAAM,UAAU,SAAS,MAAM,CAAC;AAChC,aAAO;AAAA,QACL,OAAO,IAAI;AAAA,QACX,KAAK,UAAU,QAAQ,cAAc;AAAA,MAAA;AAAA,IAEzC,CAAC;AAED,QAAI,CAAC,WAAW;AACd,cAAQ,KAAK,4CAA4C,MAAM;AAC/D,aAAO;AAAA,IACT;AAGA,UAAM,cAAc,QAAQ,UAAU,mBAAmB;AACzD,UAAM,iBAAiB,UAAU,sBAAsB,UAAU,cAAc;AAC/E,UAAM,YAAY,QAAQ,cAAc;AAExC,QAAI,CAAC,eAAe,CAAC,WAAW;AAC9B,cAAQ,MAAM,4CAA4C;AAAA,QACxD,qBAAqB,UAAU;AAAA,QAC/B,aAAa,UAAU;AAAA,QACvB,cAAc,QAAQ;AAAA,MAAA,CACvB;AACD,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL,WAAW,YAAY;AAAA,MACvB,SAAS,UAAU,aAAa,UAAU;AAAA,MAC1C,aAAa,YAAY;AAAA,MACzB,WAAW,UAAU,YAAY,UAAU;AAAA,IAAA;AAAA,EAE/C;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,YAAoB,QAA+B;AACjE,UAAM,QAAQ,KAAK,QAAQ,IAAI,UAAU;AACzC,QAAI,CAAC,OAAO,OAAO,MAAO,QAAO;AAEjC,UAAM,EAAE,QAAA,IAAY,MAAM,OAAO;AAEjC,UAAM,SAAS,kBAAkB,SAAS,QAAQ,CAAC,QAAQ,YAAY;AAAA,MACrE,OAAO,OAAO;AAAA,MACd,KAAK,OAAO,YAAY,OAAO;AAAA,IAAA,EAC/B;AAEF,WAAO,UAAU;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAmB,YAAoB,QAA+B;AACpE,UAAM,QAAQ,KAAK,QAAQ,IAAI,UAAU;AACzC,QAAI,CAAC,OAAO,OAAO,MAAO,QAAO;AAEjC,UAAM,EAAE,SAAS,SAAA,IAAa,MAAM,OAAO;AAG3C,UAAM,qBAAqB,kBAAkB,UAAU,CAAC,QAAQ,IAAI,cAAc,MAAM;AAGxF,UAAM,iBAAiB,qBAAqB;AAE5C,QAAI,iBAAiB,GAAG;AAEtB,aAAO;AAAA,IACT;AAEA,UAAM,YAAY,SAAS,cAAc;AACzC,QAAI,CAAC,UAAW,QAAO;AAGvB,WAAO,QAAQ,UAAU,mBAAmB,KAAK;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,SAAK,QAAQ,MAAA;AAAA,EACf;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,YAA0B;AAC/B,SAAK,QAAQ,OAAO,UAAU;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,YAA0B;AAC/B,SAAK,QAAQ,OAAO,UAAU;AAAA,EAChC;AACF;"}
|
|
@@ -4,7 +4,7 @@ import { OPFSManager } from '../storage/opfs/OPFSManager';
|
|
|
4
4
|
* ResourceCache - Original video resource OPFS cache
|
|
5
5
|
*
|
|
6
6
|
* Stores original MP4/video files for on-demand decoding
|
|
7
|
-
*
|
|
7
|
+
* Project-level quota management handled by OPFSManager
|
|
8
8
|
*/
|
|
9
9
|
export declare class ResourceCache {
|
|
10
10
|
readonly opfsManager: OPFSManager;
|
|
@@ -13,6 +13,7 @@ export declare class ResourceCache {
|
|
|
13
13
|
init(): Promise<void>;
|
|
14
14
|
/**
|
|
15
15
|
* Write resource to OPFS (streaming)
|
|
16
|
+
* OPFSManager handles quota management automatically
|
|
16
17
|
*/
|
|
17
18
|
writeResource(resourceId: string, stream: ReadableStream<Uint8Array>): Promise<void>;
|
|
18
19
|
/**
|
|
@@ -20,7 +21,7 @@ export declare class ResourceCache {
|
|
|
20
21
|
*/
|
|
21
22
|
readRange(resourceId: string, start: number, end: number): Promise<ArrayBuffer>;
|
|
22
23
|
/**
|
|
23
|
-
* Check if resource exists in OPFS
|
|
24
|
+
* Check if resource exists in OPFS and has content
|
|
24
25
|
*/
|
|
25
26
|
hasResource(resourceId: string): Promise<boolean>;
|
|
26
27
|
/**
|
|
@@ -35,12 +36,6 @@ export declare class ResourceCache {
|
|
|
35
36
|
* Clear all resources for current project
|
|
36
37
|
*/
|
|
37
38
|
clear(): Promise<void>;
|
|
38
|
-
/**
|
|
39
|
-
* LRU eviction to free up space
|
|
40
|
-
* Note: This is a simplified implementation
|
|
41
|
-
* Full implementation would track access times in IndexedDB
|
|
42
|
-
*/
|
|
43
|
-
evictLRU(_targetBytes: number): Promise<void>;
|
|
44
39
|
private getFileName;
|
|
45
40
|
}
|
|
46
41
|
//# sourceMappingURL=ResourceCache.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResourceCache.d.ts","sourceRoot":"","sources":["../../../src/cache/resource/ResourceCache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"ResourceCache.d.ts","sourceRoot":"","sources":["../../../src/cache/resource/ResourceCache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAI1D;;;;;GAKG;AACH,qBAAa,aAAa;IACxB,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM;IAKjD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B;;;OAGG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAyB1F;;OAEG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAWrF;;OAEG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqBvD;;OAEG;IACG,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAW1D;;OAEG;IACG,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvD;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B,OAAO,CAAC,WAAW;CAGpB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OPFSQuotaExceededError } from "../../utils/errors.js";
|
|
1
2
|
class ResourceCache {
|
|
2
3
|
opfsManager;
|
|
3
4
|
projectId;
|
|
@@ -10,6 +11,7 @@ class ResourceCache {
|
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
13
|
* Write resource to OPFS (streaming)
|
|
14
|
+
* OPFSManager handles quota management automatically
|
|
13
15
|
*/
|
|
14
16
|
async writeResource(resourceId, stream) {
|
|
15
17
|
const fileName = this.getFileName(resourceId);
|
|
@@ -18,7 +20,21 @@ class ResourceCache {
|
|
|
18
20
|
prefix: "resource",
|
|
19
21
|
fileName
|
|
20
22
|
};
|
|
21
|
-
|
|
23
|
+
try {
|
|
24
|
+
await this.opfsManager.writeFile(path, stream);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error instanceof OPFSQuotaExceededError) {
|
|
27
|
+
if (error.retryable) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Failed to write resource ${resourceId}: ${error.message} Please retry with a fresh stream.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Failed to write resource ${resourceId}: No space available and no old projects to evict.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
22
38
|
}
|
|
23
39
|
/**
|
|
24
40
|
* Read byte range from resource (for GOP-level access)
|
|
@@ -33,7 +49,7 @@ class ResourceCache {
|
|
|
33
49
|
return await this.opfsManager.readRange(path, start, end);
|
|
34
50
|
}
|
|
35
51
|
/**
|
|
36
|
-
* Check if resource exists in OPFS
|
|
52
|
+
* Check if resource exists in OPFS and has content
|
|
37
53
|
*/
|
|
38
54
|
async hasResource(resourceId) {
|
|
39
55
|
const fileName = this.getFileName(resourceId);
|
|
@@ -42,7 +58,14 @@ class ResourceCache {
|
|
|
42
58
|
prefix: "resource",
|
|
43
59
|
fileName
|
|
44
60
|
};
|
|
45
|
-
|
|
61
|
+
const exists = await this.opfsManager.exists(path);
|
|
62
|
+
if (!exists) return false;
|
|
63
|
+
try {
|
|
64
|
+
const size = await this.opfsManager.getFileSize(path);
|
|
65
|
+
return size > 0;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
46
69
|
}
|
|
47
70
|
/**
|
|
48
71
|
* Get resource file size
|
|
@@ -74,14 +97,6 @@ class ResourceCache {
|
|
|
74
97
|
async clear() {
|
|
75
98
|
await this.opfsManager.deleteProjectDirectory(this.projectId, "resource");
|
|
76
99
|
}
|
|
77
|
-
/**
|
|
78
|
-
* LRU eviction to free up space
|
|
79
|
-
* Note: This is a simplified implementation
|
|
80
|
-
* Full implementation would track access times in IndexedDB
|
|
81
|
-
*/
|
|
82
|
-
async evictLRU(_targetBytes) {
|
|
83
|
-
console.warn("[ResourceCache] LRU eviction not yet implemented");
|
|
84
|
-
}
|
|
85
100
|
getFileName(resourceId) {
|
|
86
101
|
return `${resourceId}.mp4`;
|
|
87
102
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResourceCache.js","sources":["../../../src/cache/resource/ResourceCache.ts"],"sourcesContent":["import { OPFSManager } from '../storage/opfs/OPFSManager';\nimport type { OPFSPath } from '../storage/opfs/types';\n\n/**\n * ResourceCache - Original video resource OPFS cache\n *\n * Stores original MP4/video files for on-demand decoding\n *
|
|
1
|
+
{"version":3,"file":"ResourceCache.js","sources":["../../../src/cache/resource/ResourceCache.ts"],"sourcesContent":["import { OPFSManager } from '../storage/opfs/OPFSManager';\nimport type { OPFSPath } from '../storage/opfs/types';\nimport { OPFSQuotaExceededError } from '../../utils/errors';\n\n/**\n * ResourceCache - Original video resource OPFS cache\n *\n * Stores original MP4/video files for on-demand decoding\n * Project-level quota management handled by OPFSManager\n */\nexport class ResourceCache {\n readonly opfsManager: OPFSManager;\n readonly projectId: string;\n\n constructor(opfsManager: OPFSManager, projectId: string) {\n this.opfsManager = opfsManager;\n this.projectId = projectId;\n }\n\n async init(): Promise<void> {\n await this.opfsManager.init();\n }\n\n /**\n * Write resource to OPFS (streaming)\n * OPFSManager handles quota management automatically\n */\n async writeResource(resourceId: string, stream: ReadableStream<Uint8Array>): Promise<void> {\n const fileName = this.getFileName(resourceId);\n const path: OPFSPath = {\n projectId: this.projectId,\n prefix: 'resource',\n fileName,\n };\n\n try {\n await this.opfsManager.writeFile(path, stream);\n } catch (error) {\n if (error instanceof OPFSQuotaExceededError) {\n if (error.retryable) {\n throw new Error(\n `Failed to write resource ${resourceId}: ${error.message} Please retry with a fresh stream.`\n );\n }\n throw new Error(\n `Failed to write resource ${resourceId}: No space available and no old projects to evict.`\n );\n }\n throw error;\n }\n }\n\n /**\n * Read byte range from resource (for GOP-level access)\n */\n async readRange(resourceId: string, start: number, end: number): Promise<ArrayBuffer> {\n const fileName = this.getFileName(resourceId);\n const path: OPFSPath = {\n projectId: this.projectId,\n prefix: 'resource',\n fileName,\n };\n\n return await this.opfsManager.readRange(path, start, end);\n }\n\n /**\n * Check if resource exists in OPFS and has content\n */\n async hasResource(resourceId: string): Promise<boolean> {\n const fileName = this.getFileName(resourceId);\n const path: OPFSPath = {\n projectId: this.projectId,\n prefix: 'resource',\n fileName,\n };\n\n // Check if file exists\n const exists = await this.opfsManager.exists(path);\n if (!exists) return false;\n\n // Check file size to avoid empty files (race condition protection)\n try {\n const size = await this.opfsManager.getFileSize(path);\n return size > 0;\n } catch {\n return false;\n }\n }\n\n /**\n * Get resource file size\n */\n async getResourceSize(resourceId: string): Promise<number> {\n const fileName = this.getFileName(resourceId);\n const path: OPFSPath = {\n projectId: this.projectId,\n prefix: 'resource',\n fileName,\n };\n\n return await this.opfsManager.getFileSize(path);\n }\n\n /**\n * Delete a resource from OPFS\n */\n async deleteResource(resourceId: string): Promise<void> {\n const fileName = this.getFileName(resourceId);\n const path: OPFSPath = {\n projectId: this.projectId,\n prefix: 'resource',\n fileName,\n };\n\n await this.opfsManager.deleteFile(path);\n }\n\n /**\n * Clear all resources for current project\n */\n async clear(): Promise<void> {\n await this.opfsManager.deleteProjectDirectory(this.projectId, 'resource');\n }\n\n private getFileName(resourceId: string): string {\n return `${resourceId}.mp4`;\n }\n}\n"],"names":[],"mappings":";AAUO,MAAM,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,EAET,YAAY,aAA0B,WAAmB;AACvD,SAAK,cAAc;AACnB,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,MAAM,OAAsB;AAC1B,UAAM,KAAK,YAAY,KAAA;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAAc,YAAoB,QAAmD;AACzF,UAAM,WAAW,KAAK,YAAY,UAAU;AAC5C,UAAM,OAAiB;AAAA,MACrB,WAAW,KAAK;AAAA,MAChB,QAAQ;AAAA,MACR;AAAA,IAAA;AAGF,QAAI;AACF,YAAM,KAAK,YAAY,UAAU,MAAM,MAAM;AAAA,IAC/C,SAAS,OAAO;AACd,UAAI,iBAAiB,wBAAwB;AAC3C,YAAI,MAAM,WAAW;AACnB,gBAAM,IAAI;AAAA,YACR,4BAA4B,UAAU,KAAK,MAAM,OAAO;AAAA,UAAA;AAAA,QAE5D;AACA,cAAM,IAAI;AAAA,UACR,4BAA4B,UAAU;AAAA,QAAA;AAAA,MAE1C;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAU,YAAoB,OAAe,KAAmC;AACpF,UAAM,WAAW,KAAK,YAAY,UAAU;AAC5C,UAAM,OAAiB;AAAA,MACrB,WAAW,KAAK;AAAA,MAChB,QAAQ;AAAA,MACR;AAAA,IAAA;AAGF,WAAO,MAAM,KAAK,YAAY,UAAU,MAAM,OAAO,GAAG;AAAA,EAC1D;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAY,YAAsC;AACtD,UAAM,WAAW,KAAK,YAAY,UAAU;AAC5C,UAAM,OAAiB;AAAA,MACrB,WAAW,KAAK;AAAA,MAChB,QAAQ;AAAA,MACR;AAAA,IAAA;AAIF,UAAM,SAAS,MAAM,KAAK,YAAY,OAAO,IAAI;AACjD,QAAI,CAAC,OAAQ,QAAO;AAGpB,QAAI;AACF,YAAM,OAAO,MAAM,KAAK,YAAY,YAAY,IAAI;AACpD,aAAO,OAAO;AAAA,IAChB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,gBAAgB,YAAqC;AACzD,UAAM,WAAW,KAAK,YAAY,UAAU;AAC5C,UAAM,OAAiB;AAAA,MACrB,WAAW,KAAK;AAAA,MAChB,QAAQ;AAAA,MACR;AAAA,IAAA;AAGF,WAAO,MAAM,KAAK,YAAY,YAAY,IAAI;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,eAAe,YAAmC;AACtD,UAAM,WAAW,KAAK,YAAY,UAAU;AAC5C,UAAM,OAAiB;AAAA,MACrB,WAAW,KAAK;AAAA,MAChB,QAAQ;AAAA,MACR;AAAA,IAAA;AAGF,UAAM,KAAK,YAAY,WAAW,IAAI;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AAC3B,UAAM,KAAK,YAAY,uBAAuB,KAAK,WAAW,UAAU;AAAA,EAC1E;AAAA,EAEQ,YAAY,YAA4B;AAC9C,WAAO,GAAG,UAAU;AAAA,EACtB;AACF;"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OPFSPrefix, OPFSPath } from './types';
|
|
1
|
+
import { OPFSPrefix, OPFSPath, ProjectMetadata } from './types';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* OPFSManager - Unified OPFS management infrastructure
|
|
@@ -6,12 +6,15 @@ import { OPFSPrefix, OPFSPath } from './types';
|
|
|
6
6
|
* Supports:
|
|
7
7
|
* - Directory isolation by prefix (l2/resource)
|
|
8
8
|
* - Range reads for on-demand decoding
|
|
9
|
-
* - Streaming writes
|
|
10
|
-
* -
|
|
9
|
+
* - Streaming writes with automatic quota management
|
|
10
|
+
* - Project-level LRU eviction
|
|
11
11
|
*/
|
|
12
12
|
export declare class OPFSManager {
|
|
13
13
|
private opfsRoot;
|
|
14
14
|
private initPromise;
|
|
15
|
+
readonly maxSizeBytes: number;
|
|
16
|
+
readonly quotaThreshold: number;
|
|
17
|
+
constructor(maxSizeMB?: number, quotaThresholdPercent?: number);
|
|
15
18
|
init(): Promise<void>;
|
|
16
19
|
private initStorage;
|
|
17
20
|
/**
|
|
@@ -19,9 +22,20 @@ export declare class OPFSManager {
|
|
|
19
22
|
*/
|
|
20
23
|
getProjectDir(projectId: string, prefix: OPFSPrefix): Promise<FileSystemDirectoryHandle>;
|
|
21
24
|
/**
|
|
22
|
-
* Write file
|
|
25
|
+
* Write file with automatic quota management
|
|
26
|
+
* Proactively checks quota before write; evicts old projects if threshold exceeded
|
|
23
27
|
*/
|
|
24
28
|
writeFile(path: OPFSPath, data: ArrayBuffer | ReadableStream<Uint8Array>): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Ensure quota is within threshold before write
|
|
31
|
+
* Proactively evicts old projects if usage exceeds threshold
|
|
32
|
+
*/
|
|
33
|
+
private ensureQuota;
|
|
34
|
+
/**
|
|
35
|
+
* Get total size of all projects for a prefix
|
|
36
|
+
*/
|
|
37
|
+
getTotalSize(prefix: OPFSPrefix): Promise<number>;
|
|
38
|
+
private writeFileInternal;
|
|
25
39
|
/**
|
|
26
40
|
* Read entire file
|
|
27
41
|
*/
|
|
@@ -50,5 +64,15 @@ export declare class OPFSManager {
|
|
|
50
64
|
* Delete entire project directory
|
|
51
65
|
*/
|
|
52
66
|
deleteProjectDirectory(projectId: string, prefix: OPFSPrefix): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* List all projects with size and lastModified metadata
|
|
69
|
+
* Used for LRU eviction
|
|
70
|
+
*/
|
|
71
|
+
listProjectsWithMetadata(prefix: OPFSPrefix): Promise<ProjectMetadata[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Evict oldest projects (by lastModified) excluding current project
|
|
74
|
+
* Returns number of projects evicted
|
|
75
|
+
*/
|
|
76
|
+
evictOldestProjects(currentProjectId: string, prefix: OPFSPrefix, count: number): Promise<number>;
|
|
53
77
|
}
|
|
54
78
|
//# sourceMappingURL=OPFSManager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OPFSManager.d.ts","sourceRoot":"","sources":["../../../../src/cache/storage/opfs/OPFSManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"OPFSManager.d.ts","sourceRoot":"","sources":["../../../../src/cache/storage/opfs/OPFSManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAOrE;;;;;;;;GAQG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAA0C;IAC1D,OAAO,CAAC,WAAW,CAA8B;IACjD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;gBAEpB,SAAS,GAAE,MAAa,EAAE,qBAAqB,GAAE,MAAa;IAKpE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAOb,WAAW;IAIzB;;OAEG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,yBAAyB,CAAC;IAS9F;;;OAGG;IACG,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,GAAG,cAAc,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAwB9F;;;OAGG;YACW,WAAW;IAgBzB;;OAEG;IACG,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;YAKzC,iBAAiB;IAgC/B;;OAEG;IACG,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,WAAW,CAAC;IAOpD;;OAEG;IACG,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAQjF;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAW/C;;OAEG;IACG,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAa9C;;OAEG;IACG,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;IAOlD;;OAEG;IACG,oBAAoB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,4BAA4B,CAAC;IAMjF;;OAEG;IACG,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAalF;;;OAGG;IACG,wBAAwB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAkC9E;;;OAGG;IACG,mBAAmB,CACvB,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC;CA+BnB"}
|