@meframe/core 0.0.43 → 0.0.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Meframe.d.ts.map +1 -1
- package/dist/Meframe.js +1 -0
- package/dist/Meframe.js.map +1 -1
- package/dist/cache/CacheManager.d.ts +5 -2
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +8 -3
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/cache/l1/AudioL1Cache.d.ts +5 -1
- package/dist/cache/l1/AudioL1Cache.d.ts.map +1 -1
- package/dist/cache/l1/AudioL1Cache.js +19 -2
- package/dist/cache/l1/AudioL1Cache.js.map +1 -1
- package/dist/cache/storage/opfs/OPFSManager.d.ts +7 -3
- package/dist/cache/storage/opfs/OPFSManager.d.ts.map +1 -1
- package/dist/cache/storage/opfs/OPFSManager.js +29 -14
- package/dist/cache/storage/opfs/OPFSManager.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +1 -6
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/types.d.ts +2 -8
- package/dist/config/types.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.d.ts +1 -1
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +3 -4
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/model/CompositionModel.d.ts.map +1 -1
- package/dist/model/CompositionModel.js +0 -4
- package/dist/model/CompositionModel.js.map +1 -1
- package/dist/orchestrator/ExportScheduler.d.ts +1 -0
- package/dist/orchestrator/ExportScheduler.d.ts.map +1 -1
- package/dist/orchestrator/ExportScheduler.js +12 -18
- package/dist/orchestrator/ExportScheduler.js.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.js +1 -0
- package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
- package/dist/orchestrator/OnDemandVideoSession.d.ts +5 -0
- package/dist/orchestrator/OnDemandVideoSession.d.ts.map +1 -1
- package/dist/orchestrator/OnDemandVideoSession.js +78 -0
- package/dist/orchestrator/OnDemandVideoSession.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +5 -6
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts +8 -9
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +112 -175
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/stages/compose/FrameRateConverter.d.ts +4 -4
- package/dist/stages/compose/FrameRateConverter.d.ts.map +1 -1
- package/dist/stages/compose/LayerRenderer.d.ts +9 -0
- package/dist/stages/compose/LayerRenderer.d.ts.map +1 -1
- package/dist/stages/compose/LayerRenderer.js +41 -35
- package/dist/stages/compose/LayerRenderer.js.map +1 -1
- package/dist/stages/compose/font-system/font-templates.js +1 -1
- package/dist/stages/compose/font-system/font-templates.js.map +1 -1
- package/dist/stages/load/ResourceLoader.d.ts +5 -4
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +14 -59
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/worker/WorkerChannel.d.ts.map +1 -1
- package/dist/worker/WorkerChannel.js +11 -2
- package/dist/worker/WorkerChannel.js.map +1 -1
- package/dist/worker/WorkerPool.d.ts.map +1 -1
- package/dist/worker/WorkerPool.js +5 -2
- package/dist/worker/WorkerPool.js.map +1 -1
- package/dist/worker/transferable-helper.js +22 -0
- package/dist/worker/transferable-helper.js.map +1 -1
- package/dist/worker/types.d.ts +1 -1
- package/dist/worker/types.d.ts.map +1 -1
- package/dist/worker/types.js.map +1 -1
- package/dist/workers/{WorkerChannel.CE5euh3R.js → WorkerChannel.DjBEVvEA.js} +31 -2
- package/dist/workers/WorkerChannel.DjBEVvEA.js.map +1 -0
- package/dist/workers/stages/compose/{audio-compose.worker.rW63uN6z.js → audio-compose.worker.CiM_KP27.js} +2 -2
- package/dist/workers/stages/compose/{audio-compose.worker.rW63uN6z.js.map → audio-compose.worker.CiM_KP27.js.map} +1 -1
- package/dist/workers/stages/compose/{video-compose.worker.D_542rY_.js → video-compose.worker.CeT5le4s.js} +55 -63
- package/dist/workers/stages/compose/video-compose.worker.CeT5le4s.js.map +1 -0
- package/dist/workers/stages/decode/{audio-decode.worker.B__6tqsy.js → audio-decode.worker.CpjkrZtT.js} +2 -2
- package/dist/workers/stages/decode/{audio-decode.worker.B__6tqsy.js.map → audio-decode.worker.CpjkrZtT.js.map} +1 -1
- package/dist/workers/stages/decode/{video-decode.worker.tOv-QR2f.js → video-decode.worker.BQtw6eWn.js} +2 -2
- package/dist/workers/stages/decode/video-decode.worker.BQtw6eWn.js.map +1 -0
- package/dist/workers/stages/demux/{audio-demux.worker.DgvvQVXU.js → audio-demux.worker.C4V11GQi.js} +2 -2
- package/dist/workers/stages/demux/{audio-demux.worker.DgvvQVXU.js.map → audio-demux.worker.C4V11GQi.js.map} +1 -1
- package/dist/workers/stages/demux/{video-demux.worker.DhG3CRix.js → video-demux.worker.5pJr0Ij-.js} +2 -2
- package/dist/workers/stages/demux/video-demux.worker.5pJr0Ij-.js.map +1 -0
- package/dist/workers/stages/encode/{video-encode.worker.D8pfFber.js → video-encode.worker.CX2_3YhQ.js} +2 -2
- package/dist/workers/stages/encode/{video-encode.worker.D8pfFber.js.map → video-encode.worker.CX2_3YhQ.js.map} +1 -1
- package/dist/workers/worker-manifest.json +7 -7
- package/package.json +1 -1
- package/dist/workers/WorkerChannel.CE5euh3R.js.map +0 -1
- package/dist/workers/stages/compose/video-compose.worker.D_542rY_.js.map +0 -1
- package/dist/workers/stages/decode/video-decode.worker.tOv-QR2f.js.map +0 -1
- package/dist/workers/stages/demux/video-demux.worker.DhG3CRix.js.map +0 -1
package/dist/Meframe.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Meframe.d.ts","sourceRoot":"","sources":["../src/Meframe.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC1F,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACpF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAK5D,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEpE,qBAAa,OAAO;IAClB,wDAAwD;IACxD,KAAK,EAAE,YAAY,CAAU;IAC7B,mDAAmD;IACnD,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,4DAA4D;IAC5D,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAQ;IACtC,oCAAoC;IACpC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,kDAAkD;IAClD,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,IAAI,GAAG,KAAK,GAAG,MAAM,CAAC,CAAC;IAExE,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,kBAAkB,CAAmC;IAC7D,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,MAAM,CAAC,CAAsC;IAErD,OAAO;IAoBP;;OAEG;WACU,MAAM,CAAC,MAAM,GAAE,aAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAQjE;;OAEG;YACW,UAAU;IAyBxB;;OAEG;IACG,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBxF;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAcxD;;OAEG;IACH,YAAY,CAAC,OAAO,CAAC,EAAE;QACrB,MAAM,CAAC,EAAE,iBAAiB,GAAG,eAAe,CAAC;QAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB,GAAG,aAAa;
|
|
1
|
+
{"version":3,"file":"Meframe.d.ts","sourceRoot":"","sources":["../src/Meframe.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC1F,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACpF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAK5D,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEpE,qBAAa,OAAO;IAClB,wDAAwD;IACxD,KAAK,EAAE,YAAY,CAAU;IAC7B,mDAAmD;IACnD,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,4DAA4D;IAC5D,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAQ;IACtC,oCAAoC;IACpC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,kDAAkD;IAClD,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,IAAI,GAAG,KAAK,GAAG,MAAM,CAAC,CAAC;IAExE,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,kBAAkB,CAAmC;IAC7D,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,MAAM,CAAC,CAAsC;IAErD,OAAO;IAoBP;;OAEG;WACU,MAAM,CAAC,MAAM,GAAE,aAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAQjE;;OAEG;YACW,UAAU;IAyBxB;;OAEG;IACG,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBxF;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAcxD;;OAEG;IACH,YAAY,CAAC,OAAO,CAAC,EAAE;QACrB,MAAM,CAAC,EAAE,iBAAiB,GAAG,eAAe,CAAC;QAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB,GAAG,aAAa;IA6BjB;;;OAGG;IACG,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IA+CnD;;OAEG;IACH,cAAc,IAAI,MAAM;IAIxB;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAYjC;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB9B;;OAEG;IACH,OAAO,CAAC,QAAQ;IAYhB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAM1B;;OAEG;IACH,OAAO,CAAC,WAAW;CAWpB"}
|
package/dist/Meframe.js
CHANGED
package/dist/Meframe.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Meframe.js","sources":["../src/Meframe.ts"],"sourcesContent":["/**\n * Meframe - Main entry point for the media processing framework\n */\n\nimport type { MeframeConfig, ResolvedConfig, MeframeState, ExportOptions } from './types';\nimport type { CompositionModelData, CompositionPatch, TimeUs } from './model/types';\nimport type { PreviewHandle } from './controllers/types';\nimport type { Plugin } from './plugins/types';\nimport { CompositionModel } from './model/CompositionModel';\nimport { loadConfig } from './config';\nimport { Orchestrator } from './orchestrator/Orchestrator';\nimport { PlaybackController } from './controllers/PlaybackController';\nimport { ExportController } from './controllers/ExportController';\nimport { PluginManager } from './plugins/PluginManager';\nimport { EventBus } from './event/EventBus';\nimport { MeframeEvent, type EventPayloadMap } from './event/events';\n\nexport class Meframe {\n /** Current state - managed internally via setState() */\n state: MeframeState = 'idle';\n /** Configuration - immutable after construction */\n readonly config: ResolvedConfig;\n /** Composition model - managed via setCompositionModel() */\n model: CompositionModel | null = null;\n /** Plugin manager for extensions */\n readonly pluginManager: PluginManager;\n /** Event bus for subscribing to Meframe events */\n readonly events: Pick<EventBus<EventPayloadMap>, 'on' | 'off' | 'once'>;\n\n private orchestrator: Orchestrator;\n private eventBus: EventBus<EventPayloadMap>;\n private playbackController: PlaybackController | null = null;\n private exportController: ExportController;\n private canvas?: HTMLCanvasElement | OffscreenCanvas;\n\n private constructor(config: ResolvedConfig) {\n this.config = config;\n this.eventBus = new EventBus<EventPayloadMap>();\n this.events = this.eventBus.asReadonly();\n this.pluginManager = new PluginManager(this);\n\n // Initialize orchestrator with configuration and shared eventBus\n // Worker paths are passed via config\n this.orchestrator = new Orchestrator({\n projectId: config.global.projectId,\n maxWorkers: (config as any).maxWorkers,\n cacheConfig: config.cache as any,\n eventBus: this.eventBus,\n workerPath: config.global.workerPath,\n workerExtension: config.global.workerExtension,\n });\n\n this.exportController = new ExportController(this.orchestrator);\n }\n\n /**\n * Create a new Meframe instance\n */\n static async create(config: MeframeConfig = {}): Promise<Meframe> {\n // Load and resolve configuration using ConfigLoader\n const resolvedConfig = await loadConfig({ override: config });\n const instance = new Meframe(resolvedConfig);\n await instance.initialize();\n return instance;\n }\n\n /**\n * Initialize the core engine\n */\n private async initialize(): Promise<void> {\n this.setState('loading');\n\n try {\n // Initialize orchestrator (sets up workers and cache)\n await this.orchestrator.initialize();\n\n // Initialize plugins if provided\n const plugins = (this.config as any).plugins;\n if (plugins && Array.isArray(plugins)) {\n for (const plugin of plugins) {\n // Ensure plugin type matches the Plugin interface\n if (plugin && typeof plugin === 'object' && 'name' in plugin && 'install' in plugin) {\n this.pluginManager.register(plugin as Plugin);\n }\n }\n }\n\n this.setState('idle');\n } catch (error) {\n this.setState('error');\n throw error;\n }\n }\n\n /**\n * Set the composition model\n */\n async setCompositionModel(model: CompositionModel | CompositionModelData): Promise<void> {\n this.ensureNotDestroyed();\n\n // Convert plain object to CompositionModel instance if needed\n const compositionModel =\n model instanceof CompositionModel ? model : new CompositionModel(model);\n\n // Set the model in orchestrator\n await this.orchestrator.setCompositionModel(compositionModel);\n this.model = compositionModel;\n\n this.setState('ready');\n this.eventBus.emit(MeframeEvent.Ready, {\n trackCount: model.tracks.length,\n clipCount: model.tracks.reduce(\n (acc: number, track: any) => acc + track.clips?.length || 0,\n 0\n ),\n durationUs: model.durationUs,\n });\n // await this.playbackController?.renderCover();\n }\n\n /**\n * Apply a patch to the composition model\n */\n async applyPatch(patch: CompositionPatch): Promise<void> {\n this.ensureNotDestroyed();\n\n if (!this.model) {\n throw new Error('No composition model set');\n }\n\n // Apply patch through orchestrator\n await this.orchestrator.applyPatch(patch);\n\n // Patch is applied to the model by orchestrator\n this.model = this.orchestrator.compositionModel!;\n }\n\n /**\n * Start preview and return a handle for control\n */\n startPreview(options?: {\n canvas?: HTMLCanvasElement | OffscreenCanvas;\n startUs?: TimeUs;\n autoStart?: boolean;\n }): PreviewHandle {\n this.ensureReady();\n\n // Use provided canvas or the one from config\n const canvas = options?.canvas || this.canvas;\n if (!canvas) {\n throw new Error('Canvas is required for preview');\n }\n\n // Store canvas for later use\n this.canvas = canvas;\n\n // Create playback controller\n if (!this.playbackController) {\n this.playbackController = new PlaybackController(this.orchestrator as any, this.eventBus, {\n canvas,\n startUs: options?.startUs,\n autoStart: options?.autoStart,\n });\n }\n\n // Render initial frame\n this.playbackController.renderCover();\n\n // Return preview handle\n return this.playbackController;\n }\n\n /**\n * Export the composition\n * Uses ExportController for direct export\n */\n async export(options: ExportOptions): Promise<Blob> {\n this.ensureReady();\n this.playbackController?.pause();\n this.setState('exporting');\n\n try {\n const model = this.model;\n if (!model) {\n throw new Error('No composition model set');\n }\n\n const width = options.width || (model as any).renderConfig?.width || 720;\n const height = options.height || (model as any).renderConfig?.height || 1280;\n const fps = options.fps || model.fps || 30;\n\n this.eventBus.emit(MeframeEvent.ExportStart, {\n format: options.format || 'mp4',\n width,\n height,\n fps,\n durationUs: model.durationUs,\n });\n\n // Delegate to ExportController\n const blob = await this.exportController.export(model, options);\n\n this.setState('ready');\n this.eventBus.emit(MeframeEvent.ExportComplete, {\n size: blob.size,\n durationMs: model.durationUs / 1000,\n format: options.format || 'mp4',\n });\n this.eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 1,\n });\n\n return blob;\n } catch (error) {\n this.setState('error');\n this.eventBus.emit(MeframeEvent.ExportError, {\n error: error as Error,\n stage: 'export',\n });\n throw error;\n }\n }\n\n /**\n * Get current playback time\n */\n getCurrentTime(): number {\n return this.playbackController?.currentTimeUs || 0;\n }\n\n /**\n * Clear all L1/L2 cache data\n * Useful for debugging or forcing re-render\n */\n async clearCache(): Promise<void> {\n this.ensureReady();\n\n // Stop playback first\n this.playbackController?.stop();\n\n // Clear cache through CacheManager\n await this.orchestrator.cacheManager.clear();\n\n console.log('[Meframe] Cache cleared successfully');\n }\n\n /**\n * Clean up and destroy the instance\n */\n async dispose(): Promise<void> {\n if (this.state === 'destroyed') return;\n\n // Stop playback\n this.playbackController?.stop();\n this.playbackController?.dispose();\n this.playbackController = null;\n\n // Cleanup plugins\n await this.pluginManager.disposeAll();\n\n // Dispose orchestrator (stops workers and clears cache)\n await this.orchestrator.dispose();\n\n // Clear event bus\n this.eventBus.dispose();\n\n this.setState('destroyed');\n }\n\n /**\n * Set internal state\n */\n private setState(state: MeframeState): void {\n const oldState = this.state;\n this.state = state;\n\n // Emit state change event\n // Use a generic event for now, can be added to MeframeEvent enum later\n this.eventBus.emit('state:changed' as any, {\n oldState,\n newState: state,\n });\n }\n\n /**\n * Ensure the instance is not destroyed\n */\n private ensureNotDestroyed(): void {\n if (this.state === 'destroyed') {\n throw new Error('Core instance is destroyed');\n }\n }\n\n /**\n * Ensure the instance is ready\n */\n private ensureReady(): void {\n this.ensureNotDestroyed();\n\n if (!this.model) {\n throw new Error('No composition model set');\n }\n\n if (this.state === 'loading' || this.state === 'idle') {\n throw new Error('Core is not ready');\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Meframe.js","sources":["../src/Meframe.ts"],"sourcesContent":["/**\n * Meframe - Main entry point for the media processing framework\n */\n\nimport type { MeframeConfig, ResolvedConfig, MeframeState, ExportOptions } from './types';\nimport type { CompositionModelData, CompositionPatch, TimeUs } from './model/types';\nimport type { PreviewHandle } from './controllers/types';\nimport type { Plugin } from './plugins/types';\nimport { CompositionModel } from './model/CompositionModel';\nimport { loadConfig } from './config';\nimport { Orchestrator } from './orchestrator/Orchestrator';\nimport { PlaybackController } from './controllers/PlaybackController';\nimport { ExportController } from './controllers/ExportController';\nimport { PluginManager } from './plugins/PluginManager';\nimport { EventBus } from './event/EventBus';\nimport { MeframeEvent, type EventPayloadMap } from './event/events';\n\nexport class Meframe {\n /** Current state - managed internally via setState() */\n state: MeframeState = 'idle';\n /** Configuration - immutable after construction */\n readonly config: ResolvedConfig;\n /** Composition model - managed via setCompositionModel() */\n model: CompositionModel | null = null;\n /** Plugin manager for extensions */\n readonly pluginManager: PluginManager;\n /** Event bus for subscribing to Meframe events */\n readonly events: Pick<EventBus<EventPayloadMap>, 'on' | 'off' | 'once'>;\n\n private orchestrator: Orchestrator;\n private eventBus: EventBus<EventPayloadMap>;\n private playbackController: PlaybackController | null = null;\n private exportController: ExportController;\n private canvas?: HTMLCanvasElement | OffscreenCanvas;\n\n private constructor(config: ResolvedConfig) {\n this.config = config;\n this.eventBus = new EventBus<EventPayloadMap>();\n this.events = this.eventBus.asReadonly();\n this.pluginManager = new PluginManager(this);\n\n // Initialize orchestrator with configuration and shared eventBus\n // Worker paths are passed via config\n this.orchestrator = new Orchestrator({\n projectId: config.global.projectId,\n maxWorkers: (config as any).maxWorkers,\n cacheConfig: config.cache as any,\n eventBus: this.eventBus,\n workerPath: config.global.workerPath,\n workerExtension: config.global.workerExtension,\n });\n\n this.exportController = new ExportController(this.orchestrator);\n }\n\n /**\n * Create a new Meframe instance\n */\n static async create(config: MeframeConfig = {}): Promise<Meframe> {\n // Load and resolve configuration using ConfigLoader\n const resolvedConfig = await loadConfig({ override: config });\n const instance = new Meframe(resolvedConfig);\n await instance.initialize();\n return instance;\n }\n\n /**\n * Initialize the core engine\n */\n private async initialize(): Promise<void> {\n this.setState('loading');\n\n try {\n // Initialize orchestrator (sets up workers and cache)\n await this.orchestrator.initialize();\n\n // Initialize plugins if provided\n const plugins = (this.config as any).plugins;\n if (plugins && Array.isArray(plugins)) {\n for (const plugin of plugins) {\n // Ensure plugin type matches the Plugin interface\n if (plugin && typeof plugin === 'object' && 'name' in plugin && 'install' in plugin) {\n this.pluginManager.register(plugin as Plugin);\n }\n }\n }\n\n this.setState('idle');\n } catch (error) {\n this.setState('error');\n throw error;\n }\n }\n\n /**\n * Set the composition model\n */\n async setCompositionModel(model: CompositionModel | CompositionModelData): Promise<void> {\n this.ensureNotDestroyed();\n\n // Convert plain object to CompositionModel instance if needed\n const compositionModel =\n model instanceof CompositionModel ? model : new CompositionModel(model);\n\n // Set the model in orchestrator\n await this.orchestrator.setCompositionModel(compositionModel);\n this.model = compositionModel;\n\n this.setState('ready');\n this.eventBus.emit(MeframeEvent.Ready, {\n trackCount: model.tracks.length,\n clipCount: model.tracks.reduce(\n (acc: number, track: any) => acc + track.clips?.length || 0,\n 0\n ),\n durationUs: model.durationUs,\n });\n // await this.playbackController?.renderCover();\n }\n\n /**\n * Apply a patch to the composition model\n */\n async applyPatch(patch: CompositionPatch): Promise<void> {\n this.ensureNotDestroyed();\n\n if (!this.model) {\n throw new Error('No composition model set');\n }\n\n // Apply patch through orchestrator\n await this.orchestrator.applyPatch(patch);\n\n // Patch is applied to the model by orchestrator\n this.model = this.orchestrator.compositionModel!;\n }\n\n /**\n * Start preview and return a handle for control\n */\n startPreview(options?: {\n canvas?: HTMLCanvasElement | OffscreenCanvas;\n startUs?: TimeUs;\n autoStart?: boolean;\n }): PreviewHandle {\n this.ensureReady();\n\n // Use provided canvas or the one from config\n const canvas = options?.canvas || this.canvas;\n if (!canvas) {\n throw new Error('Canvas is required for preview');\n }\n\n // Store canvas for later use\n this.canvas = canvas;\n\n // Create playback controller\n if (!this.playbackController) {\n this.playbackController = new PlaybackController(this.orchestrator as any, this.eventBus, {\n canvas,\n startUs: options?.startUs,\n autoStart: options?.autoStart,\n });\n }\n\n // Render initial frame\n this.playbackController.renderCover();\n this.playbackController.preheatNextWindow();\n\n // Return preview handle\n return this.playbackController;\n }\n\n /**\n * Export the composition\n * Uses ExportController for direct export\n */\n async export(options: ExportOptions): Promise<Blob> {\n this.ensureReady();\n this.playbackController?.pause();\n this.setState('exporting');\n\n try {\n const model = this.model;\n if (!model) {\n throw new Error('No composition model set');\n }\n\n const width = options.width || (model as any).renderConfig?.width || 720;\n const height = options.height || (model as any).renderConfig?.height || 1280;\n const fps = options.fps || model.fps || 30;\n\n this.eventBus.emit(MeframeEvent.ExportStart, {\n format: options.format || 'mp4',\n width,\n height,\n fps,\n durationUs: model.durationUs,\n });\n\n // Delegate to ExportController\n const blob = await this.exportController.export(model, options);\n\n this.setState('ready');\n this.eventBus.emit(MeframeEvent.ExportComplete, {\n size: blob.size,\n durationMs: model.durationUs / 1000,\n format: options.format || 'mp4',\n });\n this.eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 1,\n });\n\n return blob;\n } catch (error) {\n this.setState('error');\n this.eventBus.emit(MeframeEvent.ExportError, {\n error: error as Error,\n stage: 'export',\n });\n throw error;\n }\n }\n\n /**\n * Get current playback time\n */\n getCurrentTime(): number {\n return this.playbackController?.currentTimeUs || 0;\n }\n\n /**\n * Clear all L1/L2 cache data\n * Useful for debugging or forcing re-render\n */\n async clearCache(): Promise<void> {\n this.ensureReady();\n\n // Stop playback first\n this.playbackController?.stop();\n\n // Clear cache through CacheManager\n await this.orchestrator.cacheManager.clear();\n\n console.log('[Meframe] Cache cleared successfully');\n }\n\n /**\n * Clean up and destroy the instance\n */\n async dispose(): Promise<void> {\n if (this.state === 'destroyed') return;\n\n // Stop playback\n this.playbackController?.stop();\n this.playbackController?.dispose();\n this.playbackController = null;\n\n // Cleanup plugins\n await this.pluginManager.disposeAll();\n\n // Dispose orchestrator (stops workers and clears cache)\n await this.orchestrator.dispose();\n\n // Clear event bus\n this.eventBus.dispose();\n\n this.setState('destroyed');\n }\n\n /**\n * Set internal state\n */\n private setState(state: MeframeState): void {\n const oldState = this.state;\n this.state = state;\n\n // Emit state change event\n // Use a generic event for now, can be added to MeframeEvent enum later\n this.eventBus.emit('state:changed' as any, {\n oldState,\n newState: state,\n });\n }\n\n /**\n * Ensure the instance is not destroyed\n */\n private ensureNotDestroyed(): void {\n if (this.state === 'destroyed') {\n throw new Error('Core instance is destroyed');\n }\n }\n\n /**\n * Ensure the instance is ready\n */\n private ensureReady(): void {\n this.ensureNotDestroyed();\n\n if (!this.model) {\n throw new Error('No composition model set');\n }\n\n if (this.state === 'loading' || this.state === 'idle') {\n throw new Error('Core is not ready');\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -16,7 +16,6 @@ interface CacheManagerConfig {
|
|
|
16
16
|
};
|
|
17
17
|
resource: {
|
|
18
18
|
projectId: string;
|
|
19
|
-
maxSizeMB?: number;
|
|
20
19
|
};
|
|
21
20
|
}
|
|
22
21
|
/**
|
|
@@ -62,9 +61,13 @@ export declare class CacheManager {
|
|
|
62
61
|
numberOfChannels: number;
|
|
63
62
|
} | null;
|
|
64
63
|
hasClipPCM(clipId: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Check if a specific audio chunk is already cached (by timestamp)
|
|
66
|
+
*/
|
|
67
|
+
hasAudioSlotAt(clipId: string, timestampUs: TimeUs): boolean;
|
|
65
68
|
/**
|
|
66
69
|
* Check if sufficient PCM data exists for the requested time window
|
|
67
|
-
* Returns true only if at least 80% of requested duration is available
|
|
70
|
+
* Returns true only if at least threshold (default 80%) of requested duration is available
|
|
68
71
|
*/
|
|
69
72
|
hasWindowPCM(clipId: string, startUs: TimeUs, endUs: TimeUs): boolean;
|
|
70
73
|
clearAudioCache(): void;
|
|
@@ -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,8 +17,7 @@ class CacheManager {
|
|
|
17
17
|
constructor(config, eventBus) {
|
|
18
18
|
this.videoL1Cache = new VideoL1Cache(config.l1);
|
|
19
19
|
this.audioL1Cache = new AudioL1Cache();
|
|
20
|
-
const
|
|
21
|
-
const opfsManager = new OPFSManager(maxSizeMB);
|
|
20
|
+
const opfsManager = new OPFSManager();
|
|
22
21
|
this.resourceCache = new ResourceCache(opfsManager, config.resource.projectId);
|
|
23
22
|
this.mp4IndexCache = new MP4IndexCache();
|
|
24
23
|
this.audioSampleCache = new AudioSampleCache();
|
|
@@ -62,9 +61,15 @@ class CacheManager {
|
|
|
62
61
|
hasClipPCM(clipId) {
|
|
63
62
|
return this.audioL1Cache.hasClipPCM(clipId);
|
|
64
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Check if a specific audio chunk is already cached (by timestamp)
|
|
66
|
+
*/
|
|
67
|
+
hasAudioSlotAt(clipId, timestampUs) {
|
|
68
|
+
return this.audioL1Cache.hasSlotAt(clipId, timestampUs);
|
|
69
|
+
}
|
|
65
70
|
/**
|
|
66
71
|
* Check if sufficient PCM data exists for the requested time window
|
|
67
|
-
* Returns true only if at least 80% of requested duration is available
|
|
72
|
+
* Returns true only if at least threshold (default 80%) of requested duration is available
|
|
68
73
|
*/
|
|
69
74
|
hasWindowPCM(clipId, startUs, endUs) {
|
|
70
75
|
return this.audioL1Cache.hasWindowData(clipId, startUs, endUs);
|
|
@@ -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 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
|
+
{"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 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 a specific audio chunk is already cached (by timestamp)\n */\n hasAudioSlotAt(clipId: string, timestampUs: TimeUs): boolean {\n return this.audioL1Cache.hasSlotAt(clipId, timestampUs);\n }\n\n /**\n * Check if sufficient PCM data exists for the requested time window\n * Returns true only if at least threshold (default 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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"}
|
|
@@ -24,9 +24,13 @@ export declare class AudioL1Cache {
|
|
|
24
24
|
numberOfChannels: number;
|
|
25
25
|
} | null;
|
|
26
26
|
hasClipPCM(clipId: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Check if a slot with specific timestamp exists (used for incremental decoding)
|
|
29
|
+
*/
|
|
30
|
+
hasSlotAt(clipId: string, timestampUs: TimeUs, toleranceUs?: number): boolean;
|
|
27
31
|
/**
|
|
28
32
|
* Check if sufficient PCM data exists for the requested time window
|
|
29
|
-
* Returns true only if at least
|
|
33
|
+
* Returns true only if at least 99% of requested duration is available
|
|
30
34
|
*/
|
|
31
35
|
hasWindowData(clipId: string, startUs: TimeUs, endUs: TimeUs): boolean;
|
|
32
36
|
clearClipPCM(clipId: string): void;
|
|
@@ -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;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;
|
|
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;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,GAAE,MAAa,GAAG,OAAO;IAmBnF;;;OAGG;IACH,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAqCtE,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"}
|
|
@@ -150,9 +150,26 @@ class AudioL1Cache {
|
|
|
150
150
|
hasClipPCM(clipId) {
|
|
151
151
|
return this.audioDataByClip.has(clipId);
|
|
152
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Check if a slot with specific timestamp exists (used for incremental decoding)
|
|
155
|
+
*/
|
|
156
|
+
hasSlotAt(clipId, timestampUs, toleranceUs = 1e3) {
|
|
157
|
+
const slots = this.audioDataByClip.get(clipId);
|
|
158
|
+
if (!slots) return false;
|
|
159
|
+
const index = this.findInsertIndex(slots, timestampUs);
|
|
160
|
+
if (index < slots.length) {
|
|
161
|
+
const slot = slots[index];
|
|
162
|
+
if (slot && Math.abs(slot.timestampUs - timestampUs) < toleranceUs) return true;
|
|
163
|
+
}
|
|
164
|
+
if (index > 0) {
|
|
165
|
+
const slot = slots[index - 1];
|
|
166
|
+
if (slot && Math.abs(slot.timestampUs - timestampUs) < toleranceUs) return true;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
153
170
|
/**
|
|
154
171
|
* Check if sufficient PCM data exists for the requested time window
|
|
155
|
-
* Returns true only if at least
|
|
172
|
+
* Returns true only if at least 99% of requested duration is available
|
|
156
173
|
*/
|
|
157
174
|
hasWindowData(clipId, startUs, endUs) {
|
|
158
175
|
const slots = this.audioDataByClip.get(clipId);
|
|
@@ -176,7 +193,7 @@ class AudioL1Cache {
|
|
|
176
193
|
coveredDurationUs += overlapEnd - overlapStart;
|
|
177
194
|
}
|
|
178
195
|
}
|
|
179
|
-
return coveredDurationUs >= requestedDurationUs * 0.
|
|
196
|
+
return coveredDurationUs >= requestedDurationUs * 0.99;
|
|
180
197
|
}
|
|
181
198
|
clearClipPCM(clipId) {
|
|
182
199
|
this.audioDataByClip.delete(clipId);
|
|
@@ -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 // 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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"}
|
|
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 a slot with specific timestamp exists (used for incremental decoding)\n */\n hasSlotAt(clipId: string, timestampUs: TimeUs, toleranceUs: number = 1000): boolean {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots) return false;\n\n // Since slots are sorted, we can use binary search\n const index = this.findInsertIndex(slots, timestampUs);\n\n // Check index and index-1 (in case findInsertIndex lands after)\n if (index < slots.length) {\n const slot = slots[index];\n if (slot && Math.abs(slot.timestampUs - timestampUs) < toleranceUs) return true;\n }\n if (index > 0) {\n const slot = slots[index - 1];\n if (slot && Math.abs(slot.timestampUs - timestampUs) < toleranceUs) return true;\n }\n return false;\n }\n\n /**\n * Check if sufficient PCM data exists for the requested time window\n * Returns true only if at least 99% 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 99% coverage\n // We use 0.99 instead of 1.0 to tolerate minor floating-point precision errors\n return coveredDurationUs >= requestedDurationUs * 0.99;\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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"}
|
|
@@ -12,9 +12,9 @@ import { OPFSPrefix, OPFSPath, ProjectMetadata } from './types';
|
|
|
12
12
|
export declare class OPFSManager {
|
|
13
13
|
private opfsRoot;
|
|
14
14
|
private initPromise;
|
|
15
|
-
|
|
15
|
+
maxSizeBytes: number;
|
|
16
16
|
readonly quotaThreshold: number;
|
|
17
|
-
constructor(
|
|
17
|
+
constructor(quotaThresholdPercent?: number);
|
|
18
18
|
init(): Promise<void>;
|
|
19
19
|
private initStorage;
|
|
20
20
|
/**
|
|
@@ -26,6 +26,10 @@ export declare class OPFSManager {
|
|
|
26
26
|
* Proactively checks quota before write; evicts old projects if threshold exceeded
|
|
27
27
|
*/
|
|
28
28
|
writeFile(path: OPFSPath, data: ArrayBuffer | ReadableStream<Uint8Array>): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Check if project is locked by any tab (actively being used)
|
|
31
|
+
*/
|
|
32
|
+
private isProjectLocked;
|
|
29
33
|
/**
|
|
30
34
|
* Ensure quota is within threshold before write
|
|
31
35
|
* Proactively evicts old projects if usage exceeds threshold
|
|
@@ -70,7 +74,7 @@ export declare class OPFSManager {
|
|
|
70
74
|
*/
|
|
71
75
|
listProjectsWithMetadata(prefix: OPFSPrefix): Promise<ProjectMetadata[]>;
|
|
72
76
|
/**
|
|
73
|
-
* Evict oldest projects (by lastModified) excluding current project
|
|
77
|
+
* Evict oldest projects (by lastModified) excluding current project and locked projects
|
|
74
78
|
* Returns number of projects evicted
|
|
75
79
|
*/
|
|
76
80
|
evictOldestProjects(currentProjectId: string, prefix: OPFSPrefix, count: number): Promise<number>;
|
|
@@ -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,eAAe,EAAE,MAAM,SAAS,CAAC;AAOrE;;;;;;;;GAQG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAA0C;IAC1D,OAAO,CAAC,WAAW,CAA8B;IACjD,
|
|
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,YAAY,EAAE,MAAM,CAAY;IAChC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;gBAEpB,qBAAqB,GAAE,MAAa;IAI1C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAOb,WAAW;IAWzB;;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;;OAEG;YACW,eAAe;IAU7B;;;OAGG;YACW,WAAW;IAYzB;;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;CAiCnB"}
|
|
@@ -5,10 +5,9 @@ function isDOMException(error, name) {
|
|
|
5
5
|
class OPFSManager {
|
|
6
6
|
opfsRoot = null;
|
|
7
7
|
initPromise = null;
|
|
8
|
-
maxSizeBytes;
|
|
8
|
+
maxSizeBytes = Infinity;
|
|
9
9
|
quotaThreshold;
|
|
10
|
-
constructor(
|
|
11
|
-
this.maxSizeBytes = maxSizeMB * 1024 * 1024;
|
|
10
|
+
constructor(quotaThresholdPercent = 0.85) {
|
|
12
11
|
this.quotaThreshold = quotaThresholdPercent;
|
|
13
12
|
}
|
|
14
13
|
async init() {
|
|
@@ -18,6 +17,9 @@ class OPFSManager {
|
|
|
18
17
|
}
|
|
19
18
|
async initStorage() {
|
|
20
19
|
this.opfsRoot = await navigator.storage.getDirectory();
|
|
20
|
+
const estimate = await navigator.storage.estimate();
|
|
21
|
+
const quota = estimate.quota || 5 * 1024 * 1024 * 1024;
|
|
22
|
+
this.maxSizeBytes = Math.floor(quota * 0.8);
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* Get project directory by prefix
|
|
@@ -52,6 +54,17 @@ class OPFSManager {
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Check if project is locked by any tab (actively being used)
|
|
59
|
+
*/
|
|
60
|
+
async isProjectLocked(projectId, prefix) {
|
|
61
|
+
if (!navigator.locks) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const lockName = `meframe-${prefix}-${projectId}`;
|
|
65
|
+
const locks = await navigator.locks.query();
|
|
66
|
+
return locks.held?.some((lock) => lock.name === lockName) ?? false;
|
|
67
|
+
}
|
|
55
68
|
/**
|
|
56
69
|
* Ensure quota is within threshold before write
|
|
57
70
|
* Proactively evicts old projects if usage exceeds threshold
|
|
@@ -62,9 +75,6 @@ class OPFSManager {
|
|
|
62
75
|
if (usagePercent > this.quotaThreshold) {
|
|
63
76
|
const needToFreeBytes = totalSize - this.maxSizeBytes * this.quotaThreshold;
|
|
64
77
|
const projectsToEvict = Math.ceil(needToFreeBytes / (totalSize / 10));
|
|
65
|
-
console.log(
|
|
66
|
-
`[OPFSManager] Quota usage ${(usagePercent * 100).toFixed(1)}% exceeds threshold ${(this.quotaThreshold * 100).toFixed(0)}%, evicting old projects`
|
|
67
|
-
);
|
|
68
78
|
await this.evictOldestProjects(currentProjectId, prefix, Math.max(1, projectsToEvict));
|
|
69
79
|
}
|
|
70
80
|
}
|
|
@@ -210,27 +220,32 @@ class OPFSManager {
|
|
|
210
220
|
return projects;
|
|
211
221
|
}
|
|
212
222
|
/**
|
|
213
|
-
* Evict oldest projects (by lastModified) excluding current project
|
|
223
|
+
* Evict oldest projects (by lastModified) excluding current project and locked projects
|
|
214
224
|
* Returns number of projects evicted
|
|
215
225
|
*/
|
|
216
226
|
async evictOldestProjects(currentProjectId, prefix, count) {
|
|
217
227
|
const projects = await this.listProjectsWithMetadata(prefix);
|
|
218
|
-
const candidates =
|
|
228
|
+
const candidates = [];
|
|
229
|
+
for (const project of projects) {
|
|
230
|
+
if (project.projectId === currentProjectId) continue;
|
|
231
|
+
const isLocked = await this.isProjectLocked(project.projectId, prefix);
|
|
232
|
+
if (isLocked) continue;
|
|
233
|
+
candidates.push(project);
|
|
234
|
+
}
|
|
219
235
|
if (candidates.length === 0) {
|
|
220
236
|
return 0;
|
|
221
237
|
}
|
|
238
|
+
candidates.sort((a, b) => a.lastModified - b.lastModified);
|
|
239
|
+
const toEvict = candidates.slice(0, count);
|
|
222
240
|
let freedBytes = 0;
|
|
223
|
-
for (const project of
|
|
241
|
+
for (const project of toEvict) {
|
|
224
242
|
await this.deleteProjectDirectory(project.projectId, prefix);
|
|
225
243
|
freedBytes += project.size;
|
|
226
|
-
console.log(
|
|
227
|
-
`[OPFSManager] Evicted ${prefix} project ${project.projectId} (${(project.size / 1024 / 1024).toFixed(2)}MB, last modified: ${new Date(project.lastModified).toLocaleString()})`
|
|
228
|
-
);
|
|
229
244
|
}
|
|
230
245
|
console.log(
|
|
231
|
-
`[OPFSManager]
|
|
246
|
+
`[OPFSManager] Evicted ${toEvict.length} project(s), freed ${(freedBytes / 1024 / 1024).toFixed(0)}MB`
|
|
232
247
|
);
|
|
233
|
-
return
|
|
248
|
+
return toEvict.length;
|
|
234
249
|
}
|
|
235
250
|
}
|
|
236
251
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OPFSManager.js","sources":["../../../../src/cache/storage/opfs/OPFSManager.ts"],"sourcesContent":["import type { OPFSPrefix, OPFSPath, ProjectMetadata } from './types';\nimport { OPFSQuotaExceededError } from '../../../utils/errors';\n\nfunction isDOMException(error: unknown, name: string): boolean {\n return error instanceof Error && 'name' in error && error.name === name;\n}\n\n/**\n * OPFSManager - Unified OPFS management infrastructure\n *\n * Supports:\n * - Directory isolation by prefix (l2/resource)\n * - Range reads for on-demand decoding\n * - Streaming writes with automatic quota management\n * - Project-level LRU eviction\n */\nexport class OPFSManager {\n private opfsRoot: FileSystemDirectoryHandle | null = null;\n private initPromise: Promise<void> | null = null;\n readonly maxSizeBytes: number;\n readonly quotaThreshold: number;\n\n constructor(maxSizeMB: number = 5120, quotaThresholdPercent: number = 0.85) {\n this.maxSizeBytes = maxSizeMB * 1024 * 1024;\n this.quotaThreshold = quotaThresholdPercent;\n }\n\n async init(): Promise<void> {\n if (this.initPromise) return this.initPromise;\n\n this.initPromise = this.initStorage();\n return this.initPromise;\n }\n\n private async initStorage(): Promise<void> {\n this.opfsRoot = await navigator.storage.getDirectory();\n }\n\n /**\n * Get project directory by prefix\n */\n async getProjectDir(projectId: string, prefix: OPFSPrefix): Promise<FileSystemDirectoryHandle> {\n if (!this.opfsRoot) {\n throw new Error('[OPFSManager] Not initialized');\n }\n\n const dirName = `meframe-${prefix}-${projectId}`;\n return this.opfsRoot.getDirectoryHandle(dirName, { create: true });\n }\n\n /**\n * Write file with automatic quota management\n * Proactively checks quota before write; evicts old projects if threshold exceeded\n */\n async writeFile(path: OPFSPath, data: ArrayBuffer | ReadableStream<Uint8Array>): Promise<void> {\n await this.ensureQuota(path.projectId, path.prefix);\n\n try {\n await this.writeFileInternal(path, data);\n } catch (error) {\n if (isDOMException(error, 'QuotaExceededError')) {\n const evictedCount = await this.evictOldestProjects(path.projectId, path.prefix, 1);\n\n if (evictedCount === 0) {\n throw new OPFSQuotaExceededError(path.projectId, path.prefix, false);\n }\n\n if (data instanceof ReadableStream) {\n throw new OPFSQuotaExceededError(path.projectId, path.prefix, true);\n }\n\n await this.writeFileInternal(path, data);\n } else {\n throw error;\n }\n }\n }\n\n /**\n * Ensure quota is within threshold before write\n * Proactively evicts old projects if usage exceeds threshold\n */\n private async ensureQuota(currentProjectId: string, prefix: OPFSPrefix): Promise<void> {\n const totalSize = await this.getTotalSize(prefix);\n const usagePercent = totalSize / this.maxSizeBytes;\n\n if (usagePercent > this.quotaThreshold) {\n const needToFreeBytes = totalSize - this.maxSizeBytes * this.quotaThreshold;\n const projectsToEvict = Math.ceil(needToFreeBytes / (totalSize / 10));\n\n console.log(\n `[OPFSManager] Quota usage ${(usagePercent * 100).toFixed(1)}% exceeds threshold ${(this.quotaThreshold * 100).toFixed(0)}%, evicting old projects`\n );\n\n await this.evictOldestProjects(currentProjectId, prefix, Math.max(1, projectsToEvict));\n }\n }\n\n /**\n * Get total size of all projects for a prefix\n */\n async getTotalSize(prefix: OPFSPrefix): Promise<number> {\n const projects = await this.listProjectsWithMetadata(prefix);\n return projects.reduce((sum, p) => sum + p.size, 0);\n }\n\n private async writeFileInternal(\n path: OPFSPath,\n data: ArrayBuffer | ReadableStream<Uint8Array>\n ): Promise<void> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: true });\n const writable = await fileHandle.createWritable();\n\n if (data instanceof ArrayBuffer) {\n await writable.write(data);\n } else {\n const reader = data.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n const buffer = value.buffer.slice(\n value.byteOffset,\n value.byteOffset + value.byteLength\n );\n await writable.write(buffer as ArrayBuffer);\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n\n await writable.close();\n }\n\n /**\n * Read entire file\n */\n async readFile(path: OPFSPath): Promise<ArrayBuffer> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n return await file.arrayBuffer();\n }\n\n /**\n * Read byte range from file (for on-demand decoding)\n */\n async readRange(path: OPFSPath, start: number, end: number): Promise<ArrayBuffer> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n const slice = file.slice(start, end);\n return await slice.arrayBuffer();\n }\n\n /**\n * Delete file\n */\n async deleteFile(path: OPFSPath): Promise<void> {\n try {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n await projectDir.removeEntry(path.fileName);\n } catch (error) {\n if (!isDOMException(error, 'NotFoundError')) {\n console.warn(`[OPFSManager] Failed to delete file ${path.fileName}:`, error);\n }\n }\n }\n\n /**\n * Check if file exists\n */\n async exists(path: OPFSPath): Promise<boolean> {\n try {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n await projectDir.getFileHandle(path.fileName, { create: false });\n return true;\n } catch (error) {\n if (isDOMException(error, 'NotFoundError')) {\n return false;\n }\n throw error;\n }\n }\n\n /**\n * Get file size\n */\n async getFileSize(path: OPFSPath): Promise<number> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n return file.size;\n }\n\n /**\n * Create writable stream for streaming writes\n */\n async createWritableStream(path: OPFSPath): Promise<FileSystemWritableFileStream> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: true });\n return await fileHandle.createWritable();\n }\n\n /**\n * Delete entire project directory\n */\n async deleteProjectDirectory(projectId: string, prefix: OPFSPrefix): Promise<void> {\n if (!this.opfsRoot) return;\n\n try {\n const dirName = `meframe-${prefix}-${projectId}`;\n await this.opfsRoot.removeEntry(dirName, { recursive: true });\n } catch (error) {\n if (!isDOMException(error, 'NotFoundError')) {\n console.warn(`[OPFSManager] Failed to remove directory ${prefix}-${projectId}:`, error);\n }\n }\n }\n\n /**\n * List all projects with size and lastModified metadata\n * Used for LRU eviction\n */\n async listProjectsWithMetadata(prefix: OPFSPrefix): Promise<ProjectMetadata[]> {\n if (!this.opfsRoot) {\n throw new Error('[OPFSManager] Not initialized');\n }\n\n const projects: ProjectMetadata[] = [];\n const searchPrefix = `meframe-${prefix}-`;\n\n // @ts-expect-error - AsyncIterator type not well-supported\n for await (const [name, handle] of this.opfsRoot.entries()) {\n if (handle.kind === 'directory' && name.startsWith(searchPrefix)) {\n const projectId = name.slice(searchPrefix.length);\n const projectDir = handle as FileSystemDirectoryHandle;\n let totalSize = 0;\n let maxLastModified = 0;\n\n // @ts-expect-error - AsyncIterator type not well-supported\n for await (const [_fileName, fileHandle] of projectDir.entries()) {\n if (fileHandle.kind === 'file') {\n const file = await (fileHandle as FileSystemFileHandle).getFile();\n totalSize += file.size;\n maxLastModified = Math.max(maxLastModified, file.lastModified);\n }\n }\n\n if (totalSize > 0) {\n projects.push({ projectId, size: totalSize, lastModified: maxLastModified });\n }\n }\n }\n\n return projects;\n }\n\n /**\n * Evict oldest projects (by lastModified) excluding current project\n * Returns number of projects evicted\n */\n async evictOldestProjects(\n currentProjectId: string,\n prefix: OPFSPrefix,\n count: number\n ): Promise<number> {\n const projects = await this.listProjectsWithMetadata(prefix);\n const candidates = projects\n .filter((p) => p.projectId !== currentProjectId)\n .sort((a, b) => a.lastModified - b.lastModified)\n .slice(0, count);\n\n if (candidates.length === 0) {\n return 0;\n }\n\n let freedBytes = 0;\n\n for (const project of candidates) {\n await this.deleteProjectDirectory(project.projectId, prefix);\n freedBytes += project.size;\n\n console.log(\n `[OPFSManager] Evicted ${prefix} project ${project.projectId} ` +\n `(${(project.size / 1024 / 1024).toFixed(2)}MB, ` +\n `last modified: ${new Date(project.lastModified).toLocaleString()})`\n );\n }\n\n console.log(\n `[OPFSManager] Freed ${(freedBytes / 1024 / 1024).toFixed(2)}MB ` +\n `by evicting ${candidates.length} old project(s)`\n );\n\n return candidates.length;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OPFSManager.js","sources":["../../../../src/cache/storage/opfs/OPFSManager.ts"],"sourcesContent":["import type { OPFSPrefix, OPFSPath, ProjectMetadata } from './types';\nimport { OPFSQuotaExceededError } from '../../../utils/errors';\n\nfunction isDOMException(error: unknown, name: string): boolean {\n return error instanceof Error && 'name' in error && error.name === name;\n}\n\n/**\n * OPFSManager - Unified OPFS management infrastructure\n *\n * Supports:\n * - Directory isolation by prefix (l2/resource)\n * - Range reads for on-demand decoding\n * - Streaming writes with automatic quota management\n * - Project-level LRU eviction\n */\nexport class OPFSManager {\n private opfsRoot: FileSystemDirectoryHandle | null = null;\n private initPromise: Promise<void> | null = null;\n maxSizeBytes: number = Infinity;\n readonly quotaThreshold: number;\n\n constructor(quotaThresholdPercent: number = 0.85) {\n this.quotaThreshold = quotaThresholdPercent;\n }\n\n async init(): Promise<void> {\n if (this.initPromise) return this.initPromise;\n\n this.initPromise = this.initStorage();\n return this.initPromise;\n }\n\n private async initStorage(): Promise<void> {\n this.opfsRoot = await navigator.storage.getDirectory();\n\n // Dynamic quota detection\n const estimate = await navigator.storage.estimate();\n const quota = estimate.quota || 5 * 1024 * 1024 * 1024;\n\n // Aggressive strategy: use 80% of browser quota\n this.maxSizeBytes = Math.floor(quota * 0.8);\n }\n\n /**\n * Get project directory by prefix\n */\n async getProjectDir(projectId: string, prefix: OPFSPrefix): Promise<FileSystemDirectoryHandle> {\n if (!this.opfsRoot) {\n throw new Error('[OPFSManager] Not initialized');\n }\n\n const dirName = `meframe-${prefix}-${projectId}`;\n return this.opfsRoot.getDirectoryHandle(dirName, { create: true });\n }\n\n /**\n * Write file with automatic quota management\n * Proactively checks quota before write; evicts old projects if threshold exceeded\n */\n async writeFile(path: OPFSPath, data: ArrayBuffer | ReadableStream<Uint8Array>): Promise<void> {\n await this.ensureQuota(path.projectId, path.prefix);\n\n try {\n await this.writeFileInternal(path, data);\n } catch (error) {\n if (isDOMException(error, 'QuotaExceededError')) {\n const evictedCount = await this.evictOldestProjects(path.projectId, path.prefix, 1);\n\n if (evictedCount === 0) {\n throw new OPFSQuotaExceededError(path.projectId, path.prefix, false);\n }\n\n if (data instanceof ReadableStream) {\n throw new OPFSQuotaExceededError(path.projectId, path.prefix, true);\n }\n\n await this.writeFileInternal(path, data);\n } else {\n throw error;\n }\n }\n }\n\n /**\n * Check if project is locked by any tab (actively being used)\n */\n private async isProjectLocked(projectId: string, prefix: OPFSPrefix): Promise<boolean> {\n if (!navigator.locks) {\n return false;\n }\n\n const lockName = `meframe-${prefix}-${projectId}`;\n const locks = await navigator.locks.query();\n return locks.held?.some((lock) => lock.name === lockName) ?? false;\n }\n\n /**\n * Ensure quota is within threshold before write\n * Proactively evicts old projects if usage exceeds threshold\n */\n private async ensureQuota(currentProjectId: string, prefix: OPFSPrefix): Promise<void> {\n const totalSize = await this.getTotalSize(prefix);\n const usagePercent = totalSize / this.maxSizeBytes;\n\n if (usagePercent > this.quotaThreshold) {\n const needToFreeBytes = totalSize - this.maxSizeBytes * this.quotaThreshold;\n const projectsToEvict = Math.ceil(needToFreeBytes / (totalSize / 10));\n\n await this.evictOldestProjects(currentProjectId, prefix, Math.max(1, projectsToEvict));\n }\n }\n\n /**\n * Get total size of all projects for a prefix\n */\n async getTotalSize(prefix: OPFSPrefix): Promise<number> {\n const projects = await this.listProjectsWithMetadata(prefix);\n return projects.reduce((sum, p) => sum + p.size, 0);\n }\n\n private async writeFileInternal(\n path: OPFSPath,\n data: ArrayBuffer | ReadableStream<Uint8Array>\n ): Promise<void> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: true });\n const writable = await fileHandle.createWritable();\n\n if (data instanceof ArrayBuffer) {\n await writable.write(data);\n } else {\n const reader = data.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n const buffer = value.buffer.slice(\n value.byteOffset,\n value.byteOffset + value.byteLength\n );\n await writable.write(buffer as ArrayBuffer);\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n\n await writable.close();\n }\n\n /**\n * Read entire file\n */\n async readFile(path: OPFSPath): Promise<ArrayBuffer> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n return await file.arrayBuffer();\n }\n\n /**\n * Read byte range from file (for on-demand decoding)\n */\n async readRange(path: OPFSPath, start: number, end: number): Promise<ArrayBuffer> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n const slice = file.slice(start, end);\n return await slice.arrayBuffer();\n }\n\n /**\n * Delete file\n */\n async deleteFile(path: OPFSPath): Promise<void> {\n try {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n await projectDir.removeEntry(path.fileName);\n } catch (error) {\n if (!isDOMException(error, 'NotFoundError')) {\n console.warn(`[OPFSManager] Failed to delete file ${path.fileName}:`, error);\n }\n }\n }\n\n /**\n * Check if file exists\n */\n async exists(path: OPFSPath): Promise<boolean> {\n try {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n await projectDir.getFileHandle(path.fileName, { create: false });\n return true;\n } catch (error) {\n if (isDOMException(error, 'NotFoundError')) {\n return false;\n }\n throw error;\n }\n }\n\n /**\n * Get file size\n */\n async getFileSize(path: OPFSPath): Promise<number> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n return file.size;\n }\n\n /**\n * Create writable stream for streaming writes\n */\n async createWritableStream(path: OPFSPath): Promise<FileSystemWritableFileStream> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: true });\n return await fileHandle.createWritable();\n }\n\n /**\n * Delete entire project directory\n */\n async deleteProjectDirectory(projectId: string, prefix: OPFSPrefix): Promise<void> {\n if (!this.opfsRoot) return;\n\n try {\n const dirName = `meframe-${prefix}-${projectId}`;\n await this.opfsRoot.removeEntry(dirName, { recursive: true });\n } catch (error) {\n if (!isDOMException(error, 'NotFoundError')) {\n console.warn(`[OPFSManager] Failed to remove directory ${prefix}-${projectId}:`, error);\n }\n }\n }\n\n /**\n * List all projects with size and lastModified metadata\n * Used for LRU eviction\n */\n async listProjectsWithMetadata(prefix: OPFSPrefix): Promise<ProjectMetadata[]> {\n if (!this.opfsRoot) {\n throw new Error('[OPFSManager] Not initialized');\n }\n\n const projects: ProjectMetadata[] = [];\n const searchPrefix = `meframe-${prefix}-`;\n\n // @ts-expect-error - AsyncIterator type not well-supported\n for await (const [name, handle] of this.opfsRoot.entries()) {\n if (handle.kind === 'directory' && name.startsWith(searchPrefix)) {\n const projectId = name.slice(searchPrefix.length);\n const projectDir = handle as FileSystemDirectoryHandle;\n let totalSize = 0;\n let maxLastModified = 0;\n\n // @ts-expect-error - AsyncIterator type not well-supported\n for await (const [_fileName, fileHandle] of projectDir.entries()) {\n if (fileHandle.kind === 'file') {\n const file = await (fileHandle as FileSystemFileHandle).getFile();\n totalSize += file.size;\n maxLastModified = Math.max(maxLastModified, file.lastModified);\n }\n }\n\n if (totalSize > 0) {\n projects.push({ projectId, size: totalSize, lastModified: maxLastModified });\n }\n }\n }\n\n return projects;\n }\n\n /**\n * Evict oldest projects (by lastModified) excluding current project and locked projects\n * Returns number of projects evicted\n */\n async evictOldestProjects(\n currentProjectId: string,\n prefix: OPFSPrefix,\n count: number\n ): Promise<number> {\n const projects = await this.listProjectsWithMetadata(prefix);\n\n // Filter candidates: exclude current project and locked projects\n const candidates = [];\n for (const project of projects) {\n if (project.projectId === currentProjectId) continue;\n\n const isLocked = await this.isProjectLocked(project.projectId, prefix);\n if (isLocked) continue;\n\n candidates.push(project);\n }\n\n if (candidates.length === 0) {\n return 0;\n }\n\n candidates.sort((a, b) => a.lastModified - b.lastModified);\n const toEvict = candidates.slice(0, count);\n\n let freedBytes = 0;\n for (const project of toEvict) {\n await this.deleteProjectDirectory(project.projectId, prefix);\n freedBytes += project.size;\n }\n\n console.log(\n `[OPFSManager] Evicted ${toEvict.length} project(s), freed ${(freedBytes / 1024 / 1024).toFixed(0)}MB`\n );\n\n return toEvict.length;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAU9C;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,EAAE,
|
|
1
|
+
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAU9C;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,EAAE,cAiF5B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc;IACzB;;;OAGG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA8BH;;;OAGG;;;;;;;;;;;;;;;;;;;;;;;;;qCAyBmC,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE;;;;;;;;;;;;;;IAezD;;;OAGG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BK,CAAC"}
|
package/dist/config/defaults.js
CHANGED
|
@@ -76,14 +76,9 @@ const DEFAULT_CONFIG = {
|
|
|
76
76
|
opfs: {
|
|
77
77
|
enabled: true,
|
|
78
78
|
resource: {
|
|
79
|
-
maxSizeMB: 5120,
|
|
80
|
-
// 5GB for original resources
|
|
81
79
|
evictionPolicy: "lru"
|
|
82
80
|
},
|
|
83
|
-
l2: {
|
|
84
|
-
maxSizeMB: 2048
|
|
85
|
-
// 2GB for encoded chunks
|
|
86
|
-
}
|
|
81
|
+
l2: {}
|
|
87
82
|
}
|
|
88
83
|
},
|
|
89
84
|
mux: {}
|