@meframe/core 0.0.1
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/CHANGELOG.md +42 -0
- package/LICENSE +22 -0
- package/README.md +396 -0
- package/dist/Meframe.d.ts +82 -0
- package/dist/Meframe.d.ts.map +1 -0
- package/dist/Meframe.js +290 -0
- package/dist/Meframe.js.map +1 -0
- package/dist/_virtual/mp4box.all.js +5 -0
- package/dist/_virtual/mp4box.all.js.map +1 -0
- package/dist/cache/BatchWriter.d.ts +25 -0
- package/dist/cache/BatchWriter.d.ts.map +1 -0
- package/dist/cache/CacheManager.d.ts +115 -0
- package/dist/cache/CacheManager.d.ts.map +1 -0
- package/dist/cache/CacheManager.js +388 -0
- package/dist/cache/CacheManager.js.map +1 -0
- package/dist/cache/CacheStatsDecorator.d.ts +27 -0
- package/dist/cache/CacheStatsDecorator.d.ts.map +1 -0
- package/dist/cache/L2Cache.d.ts +39 -0
- package/dist/cache/L2Cache.d.ts.map +1 -0
- package/dist/cache/L2Cache.js +282 -0
- package/dist/cache/L2Cache.js.map +1 -0
- package/dist/cache/index.d.ts +7 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/l1/AudioL1Cache.d.ts +30 -0
- package/dist/cache/l1/AudioL1Cache.d.ts.map +1 -0
- package/dist/cache/l1/AudioL1Cache.js +306 -0
- package/dist/cache/l1/AudioL1Cache.js.map +1 -0
- package/dist/cache/l1/MixedAudioL1Cache.d.ts +13 -0
- package/dist/cache/l1/MixedAudioL1Cache.d.ts.map +1 -0
- package/dist/cache/l1/MixedAudioL1Cache.js +52 -0
- package/dist/cache/l1/MixedAudioL1Cache.js.map +1 -0
- package/dist/cache/l1/VideoL1Cache.d.ts +69 -0
- package/dist/cache/l1/VideoL1Cache.d.ts.map +1 -0
- package/dist/cache/l1/VideoL1Cache.js +318 -0
- package/dist/cache/l1/VideoL1Cache.js.map +1 -0
- package/dist/cache/l1/gop-utils.d.ts +10 -0
- package/dist/cache/l1/gop-utils.d.ts.map +1 -0
- package/dist/cache/l1/gop-utils.js +78 -0
- package/dist/cache/l1/gop-utils.js.map +1 -0
- package/dist/cache/l1/index.d.ts +4 -0
- package/dist/cache/l1/index.d.ts.map +1 -0
- package/dist/cache/l1/types.d.ts +17 -0
- package/dist/cache/l1/types.d.ts.map +1 -0
- package/dist/cache/types.d.ts +93 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/config/ConfigLoader.d.ts +69 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -0
- package/dist/config/ConfigLoader.js +133 -0
- package/dist/config/ConfigLoader.js.map +1 -0
- package/dist/config/defaults.d.ts +125 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +191 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/presets.d.ts +32 -0
- package/dist/config/presets.d.ts.map +1 -0
- package/dist/config/presets.js +11 -0
- package/dist/config/presets.js.map +1 -0
- package/dist/config/types.d.ts +199 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/validation.d.ts +19 -0
- package/dist/config/validation.d.ts.map +1 -0
- package/dist/config/validation.js +232 -0
- package/dist/config/validation.js.map +1 -0
- package/dist/controllers/PlaybackController.d.ts +55 -0
- package/dist/controllers/PlaybackController.d.ts.map +1 -0
- package/dist/controllers/PlaybackController.js +369 -0
- package/dist/controllers/PlaybackController.js.map +1 -0
- package/dist/controllers/PreRenderService.d.ts +34 -0
- package/dist/controllers/PreRenderService.d.ts.map +1 -0
- package/dist/controllers/PreRenderService.js +83 -0
- package/dist/controllers/PreRenderService.js.map +1 -0
- package/dist/controllers/PreRenderTaskQueue.d.ts +21 -0
- package/dist/controllers/PreRenderTaskQueue.d.ts.map +1 -0
- package/dist/controllers/PreviewHandle.d.ts +23 -0
- package/dist/controllers/PreviewHandle.d.ts.map +1 -0
- package/dist/controllers/PreviewHandle.js +39 -0
- package/dist/controllers/PreviewHandle.js.map +1 -0
- package/dist/controllers/index.d.ts +8 -0
- package/dist/controllers/index.d.ts.map +1 -0
- package/dist/controllers/types.d.ts +102 -0
- package/dist/controllers/types.d.ts.map +1 -0
- package/dist/event/EventBus.d.ts +42 -0
- package/dist/event/EventBus.d.ts.map +1 -0
- package/dist/event/EventBus.js +94 -0
- package/dist/event/EventBus.js.map +1 -0
- package/dist/event/events.d.ts +371 -0
- package/dist/event/events.d.ts.map +1 -0
- package/dist/event/events.js +71 -0
- package/dist/event/events.js.map +1 -0
- package/dist/event/index.d.ts +4 -0
- package/dist/event/index.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/model/CompositionModel.d.ts +48 -0
- package/dist/model/CompositionModel.d.ts.map +1 -0
- package/dist/model/CompositionModel.js +197 -0
- package/dist/model/CompositionModel.js.map +1 -0
- package/dist/model/RcFrame.d.ts +34 -0
- package/dist/model/RcFrame.d.ts.map +1 -0
- package/dist/model/RcFrame.js +97 -0
- package/dist/model/RcFrame.js.map +1 -0
- package/dist/model/dirty-range.d.ts +5 -0
- package/dist/model/dirty-range.d.ts.map +1 -0
- package/dist/model/dirty-range.js +220 -0
- package/dist/model/dirty-range.js.map +1 -0
- package/dist/model/index.d.ts +7 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/patch.d.ts +5 -0
- package/dist/model/patch.d.ts.map +1 -0
- package/dist/model/patch.js +250 -0
- package/dist/model/patch.js.map +1 -0
- package/dist/model/types.d.ts +135 -0
- package/dist/model/types.d.ts.map +1 -0
- package/dist/model/types.js +5 -0
- package/dist/model/types.js.map +1 -0
- package/dist/model/validation.d.ts +15 -0
- package/dist/model/validation.d.ts.map +1 -0
- package/dist/model/validation.js +74 -0
- package/dist/model/validation.js.map +1 -0
- package/dist/node_modules/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js +7046 -0
- package/dist/node_modules/.pnpm/mp4box@0.5.4/node_modules/mp4box/dist/mp4box.all.js.map +1 -0
- package/dist/orchestrator/ClipSessionManager.d.ts +75 -0
- package/dist/orchestrator/ClipSessionManager.d.ts.map +1 -0
- package/dist/orchestrator/ClipSessionManager.js +160 -0
- package/dist/orchestrator/ClipSessionManager.js.map +1 -0
- package/dist/orchestrator/CompositionPlanner.d.ts +55 -0
- package/dist/orchestrator/CompositionPlanner.d.ts.map +1 -0
- package/dist/orchestrator/CompositionPlanner.js +411 -0
- package/dist/orchestrator/CompositionPlanner.js.map +1 -0
- package/dist/orchestrator/Orchestrator.d.ts +59 -0
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -0
- package/dist/orchestrator/Orchestrator.js +390 -0
- package/dist/orchestrator/Orchestrator.js.map +1 -0
- package/dist/orchestrator/VideoClipSession.d.ts +64 -0
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -0
- package/dist/orchestrator/VideoClipSession.js +309 -0
- package/dist/orchestrator/VideoClipSession.js.map +1 -0
- package/dist/orchestrator/index.d.ts +5 -0
- package/dist/orchestrator/index.d.ts.map +1 -0
- package/dist/orchestrator/types.d.ts +64 -0
- package/dist/orchestrator/types.d.ts.map +1 -0
- package/dist/plugins/BackpressureMonitor.d.ts +33 -0
- package/dist/plugins/BackpressureMonitor.d.ts.map +1 -0
- package/dist/plugins/BackpressureMonitor.js +62 -0
- package/dist/plugins/BackpressureMonitor.js.map +1 -0
- package/dist/plugins/PluginManager.d.ts +37 -0
- package/dist/plugins/PluginManager.d.ts.map +1 -0
- package/dist/plugins/PluginManager.js +66 -0
- package/dist/plugins/PluginManager.js.map +1 -0
- package/dist/plugins/types.d.ts +60 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/stages/compose/AudioDucker.d.ts +59 -0
- package/dist/stages/compose/AudioDucker.d.ts.map +1 -0
- package/dist/stages/compose/AudioDucker.js +161 -0
- package/dist/stages/compose/AudioDucker.js.map +1 -0
- package/dist/stages/compose/AudioMixer.d.ts +29 -0
- package/dist/stages/compose/AudioMixer.d.ts.map +1 -0
- package/dist/stages/compose/AudioMixer.js +373 -0
- package/dist/stages/compose/AudioMixer.js.map +1 -0
- package/dist/stages/compose/FilterProcessor.d.ts +41 -0
- package/dist/stages/compose/FilterProcessor.d.ts.map +1 -0
- package/dist/stages/compose/FilterProcessor.js +226 -0
- package/dist/stages/compose/FilterProcessor.js.map +1 -0
- package/dist/stages/compose/GlobalAudioSession.d.ts +38 -0
- package/dist/stages/compose/GlobalAudioSession.d.ts.map +1 -0
- package/dist/stages/compose/GlobalAudioSession.js +122 -0
- package/dist/stages/compose/GlobalAudioSession.js.map +1 -0
- package/dist/stages/compose/LayerRenderer.d.ts +30 -0
- package/dist/stages/compose/LayerRenderer.d.ts.map +1 -0
- package/dist/stages/compose/LayerRenderer.js +215 -0
- package/dist/stages/compose/LayerRenderer.js.map +1 -0
- package/dist/stages/compose/OfflineAudioMixer.d.ts +14 -0
- package/dist/stages/compose/OfflineAudioMixer.d.ts.map +1 -0
- package/dist/stages/compose/OfflineAudioMixer.js +68 -0
- package/dist/stages/compose/OfflineAudioMixer.js.map +1 -0
- package/dist/stages/compose/TransitionProcessor.d.ts +30 -0
- package/dist/stages/compose/TransitionProcessor.d.ts.map +1 -0
- package/dist/stages/compose/TransitionProcessor.js +189 -0
- package/dist/stages/compose/TransitionProcessor.js.map +1 -0
- package/dist/stages/compose/VideoComposer.d.ts +30 -0
- package/dist/stages/compose/VideoComposer.d.ts.map +1 -0
- package/dist/stages/compose/VideoComposer.js +186 -0
- package/dist/stages/compose/VideoComposer.js.map +1 -0
- package/dist/stages/compose/audio-compose.worker.d.ts +79 -0
- package/dist/stages/compose/audio-compose.worker.d.ts.map +1 -0
- package/dist/stages/compose/audio-compose.worker.js +541 -0
- package/dist/stages/compose/audio-compose.worker.js.map +1 -0
- package/dist/stages/compose/instructions.d.ts +95 -0
- package/dist/stages/compose/instructions.d.ts.map +1 -0
- package/dist/stages/compose/types.d.ts +245 -0
- package/dist/stages/compose/types.d.ts.map +1 -0
- package/dist/stages/compose/video-compose.worker.d.ts +60 -0
- package/dist/stages/compose/video-compose.worker.d.ts.map +1 -0
- package/dist/stages/compose/video-compose.worker.js +369 -0
- package/dist/stages/compose/video-compose.worker.js.map +1 -0
- package/dist/stages/decode/AudioChunkDecoder.d.ts +41 -0
- package/dist/stages/decode/AudioChunkDecoder.d.ts.map +1 -0
- package/dist/stages/decode/AudioChunkDecoder.js +83 -0
- package/dist/stages/decode/AudioChunkDecoder.js.map +1 -0
- package/dist/stages/decode/BaseDecoder.d.ts +35 -0
- package/dist/stages/decode/BaseDecoder.d.ts.map +1 -0
- package/dist/stages/decode/BaseDecoder.js +130 -0
- package/dist/stages/decode/BaseDecoder.js.map +1 -0
- package/dist/stages/decode/VideoChunkDecoder.d.ts +54 -0
- package/dist/stages/decode/VideoChunkDecoder.d.ts.map +1 -0
- package/dist/stages/decode/VideoChunkDecoder.js +209 -0
- package/dist/stages/decode/VideoChunkDecoder.js.map +1 -0
- package/dist/stages/decode/decode.worker.d.ts +70 -0
- package/dist/stages/decode/decode.worker.d.ts.map +1 -0
- package/dist/stages/decode/decode.worker.js +436 -0
- package/dist/stages/decode/decode.worker.js.map +1 -0
- package/dist/stages/decode/index.d.ts +5 -0
- package/dist/stages/decode/index.d.ts.map +1 -0
- package/dist/stages/decode/types.d.ts +108 -0
- package/dist/stages/decode/types.d.ts.map +1 -0
- package/dist/stages/demux/MP3FrameParser.d.ts +33 -0
- package/dist/stages/demux/MP3FrameParser.d.ts.map +1 -0
- package/dist/stages/demux/MP3FrameParser.js +186 -0
- package/dist/stages/demux/MP3FrameParser.js.map +1 -0
- package/dist/stages/demux/MP4Demuxer.d.ts +45 -0
- package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -0
- package/dist/stages/demux/MP4Demuxer.js +227 -0
- package/dist/stages/demux/MP4Demuxer.js.map +1 -0
- package/dist/stages/demux/aac-esds-extractor.d.ts +7 -0
- package/dist/stages/demux/aac-esds-extractor.d.ts.map +1 -0
- package/dist/stages/demux/audio-demux.worker.d.ts +51 -0
- package/dist/stages/demux/audio-demux.worker.d.ts.map +1 -0
- package/dist/stages/demux/audio-demux.worker.js +312 -0
- package/dist/stages/demux/audio-demux.worker.js.map +1 -0
- package/dist/stages/demux/types.d.ts +77 -0
- package/dist/stages/demux/types.d.ts.map +1 -0
- package/dist/stages/demux/video-demux.worker.d.ts +48 -0
- package/dist/stages/demux/video-demux.worker.d.ts.map +1 -0
- package/dist/stages/demux/video-demux.worker.js +173 -0
- package/dist/stages/demux/video-demux.worker.js.map +1 -0
- package/dist/stages/encode/AudioChunkEncoder.d.ts +21 -0
- package/dist/stages/encode/AudioChunkEncoder.d.ts.map +1 -0
- package/dist/stages/encode/AudioChunkEncoder.js +37 -0
- package/dist/stages/encode/AudioChunkEncoder.js.map +1 -0
- package/dist/stages/encode/BaseEncoder.d.ts +44 -0
- package/dist/stages/encode/BaseEncoder.d.ts.map +1 -0
- package/dist/stages/encode/BaseEncoder.js +164 -0
- package/dist/stages/encode/BaseEncoder.js.map +1 -0
- package/dist/stages/encode/EncoderPool.d.ts +28 -0
- package/dist/stages/encode/EncoderPool.d.ts.map +1 -0
- package/dist/stages/encode/VideoChunkEncoder.d.ts +26 -0
- package/dist/stages/encode/VideoChunkEncoder.d.ts.map +1 -0
- package/dist/stages/encode/VideoChunkEncoder.js +50 -0
- package/dist/stages/encode/VideoChunkEncoder.js.map +1 -0
- package/dist/stages/encode/encode.worker.d.ts +3 -0
- package/dist/stages/encode/encode.worker.d.ts.map +1 -0
- package/dist/stages/encode/encode.worker.js +318 -0
- package/dist/stages/encode/encode.worker.js.map +1 -0
- package/dist/stages/encode/index.d.ts +6 -0
- package/dist/stages/encode/index.d.ts.map +1 -0
- package/dist/stages/encode/types.d.ts +127 -0
- package/dist/stages/encode/types.d.ts.map +1 -0
- package/dist/stages/load/EventHandlers.d.ts +35 -0
- package/dist/stages/load/EventHandlers.d.ts.map +1 -0
- package/dist/stages/load/EventHandlers.js +65 -0
- package/dist/stages/load/EventHandlers.js.map +1 -0
- package/dist/stages/load/ResourceLoader.d.ts +36 -0
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -0
- package/dist/stages/load/ResourceLoader.js +184 -0
- package/dist/stages/load/ResourceLoader.js.map +1 -0
- package/dist/stages/load/StreamFactory.d.ts +42 -0
- package/dist/stages/load/StreamFactory.d.ts.map +1 -0
- package/dist/stages/load/StreamFactory.js +201 -0
- package/dist/stages/load/StreamFactory.js.map +1 -0
- package/dist/stages/load/TaskManager.d.ts +50 -0
- package/dist/stages/load/TaskManager.d.ts.map +1 -0
- package/dist/stages/load/TaskManager.js +103 -0
- package/dist/stages/load/TaskManager.js.map +1 -0
- package/dist/stages/load/WindowByteRangeResolver.d.ts +47 -0
- package/dist/stages/load/WindowByteRangeResolver.d.ts.map +1 -0
- package/dist/stages/load/WindowByteRangeResolver.js +270 -0
- package/dist/stages/load/WindowByteRangeResolver.js.map +1 -0
- package/dist/stages/load/index.d.ts +11 -0
- package/dist/stages/load/index.d.ts.map +1 -0
- package/dist/stages/load/types.d.ts +177 -0
- package/dist/stages/load/types.d.ts.map +1 -0
- package/dist/stages/mux/MP4Muxer.d.ts +44 -0
- package/dist/stages/mux/MP4Muxer.d.ts.map +1 -0
- package/dist/stages/mux/MP4Muxer.js +262 -0
- package/dist/stages/mux/MP4Muxer.js.map +1 -0
- package/dist/stages/mux/OPFSWriter.d.ts +46 -0
- package/dist/stages/mux/OPFSWriter.d.ts.map +1 -0
- package/dist/stages/mux/index.d.ts +5 -0
- package/dist/stages/mux/index.d.ts.map +1 -0
- package/dist/stages/mux/mux.worker.d.ts +65 -0
- package/dist/stages/mux/mux.worker.d.ts.map +1 -0
- package/dist/stages/mux/mux.worker.js +219 -0
- package/dist/stages/mux/mux.worker.js.map +1 -0
- package/dist/stages/mux/types.d.ts +95 -0
- package/dist/stages/mux/types.d.ts.map +1 -0
- package/dist/stages/mux/utils.d.ts +32 -0
- package/dist/stages/mux/utils.d.ts.map +1 -0
- package/dist/stages/mux/utils.js +34 -0
- package/dist/stages/mux/utils.js.map +1 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/BackpressureAdapter.d.ts +26 -0
- package/dist/utils/BackpressureAdapter.d.ts.map +1 -0
- package/dist/utils/binary-search.d.ts +33 -0
- package/dist/utils/binary-search.d.ts.map +1 -0
- package/dist/utils/binary-search.js +62 -0
- package/dist/utils/binary-search.js.map +1 -0
- package/dist/utils/canvas-utils.d.ts +96 -0
- package/dist/utils/canvas-utils.d.ts.map +1 -0
- package/dist/utils/canvas-utils.js +58 -0
- package/dist/utils/canvas-utils.js.map +1 -0
- package/dist/utils/object-utils.d.ts +34 -0
- package/dist/utils/object-utils.d.ts.map +1 -0
- package/dist/utils/object-utils.js +22 -0
- package/dist/utils/object-utils.js.map +1 -0
- package/dist/utils/time-utils.d.ts +10 -0
- package/dist/utils/time-utils.d.ts.map +1 -0
- package/dist/utils/time-utils.js +60 -0
- package/dist/utils/time-utils.js.map +1 -0
- package/dist/worker/BaseWorker.d.ts +44 -0
- package/dist/worker/BaseWorker.d.ts.map +1 -0
- package/dist/worker/BaseWorker.js +98 -0
- package/dist/worker/BaseWorker.js.map +1 -0
- package/dist/worker/WorkerChannel.d.ts +105 -0
- package/dist/worker/WorkerChannel.d.ts.map +1 -0
- package/dist/worker/WorkerChannel.js +355 -0
- package/dist/worker/WorkerChannel.js.map +1 -0
- package/dist/worker/WorkerPool.d.ts +52 -0
- package/dist/worker/WorkerPool.d.ts.map +1 -0
- package/dist/worker/WorkerPool.js +124 -0
- package/dist/worker/WorkerPool.js.map +1 -0
- package/dist/worker/index.d.ts +11 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/transferable-helper.d.ts +89 -0
- package/dist/worker/transferable-helper.d.ts.map +1 -0
- package/dist/worker/transferable-helper.js +44 -0
- package/dist/worker/transferable-helper.js.map +1 -0
- package/dist/worker/types.d.ts +179 -0
- package/dist/worker/types.d.ts.map +1 -0
- package/dist/worker/types.js +50 -0
- package/dist/worker/types.js.map +1 -0
- package/dist/worker/worker-event-whitelist.d.ts +23 -0
- package/dist/worker/worker-event-whitelist.d.ts.map +1 -0
- package/dist/worker/worker-retry.d.ts +36 -0
- package/dist/worker/worker-retry.d.ts.map +1 -0
- package/dist/worker/worker-retry.js +55 -0
- package/dist/worker/worker-retry.js.map +1 -0
- package/package.json +105 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { binarySearchRange } from "../utils/binary-search.js";
|
|
2
|
+
class L2Cache {
|
|
3
|
+
db = null;
|
|
4
|
+
opfsRoot = null;
|
|
5
|
+
maxSize;
|
|
6
|
+
projectId;
|
|
7
|
+
initPromise = null;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.maxSize = config.maxSizeMB * 1024 * 1024;
|
|
10
|
+
this.projectId = config.projectId;
|
|
11
|
+
}
|
|
12
|
+
async init() {
|
|
13
|
+
if (this.initPromise) return this.initPromise;
|
|
14
|
+
this.initPromise = this.initStorage();
|
|
15
|
+
return this.initPromise;
|
|
16
|
+
}
|
|
17
|
+
async get(timeUs, clipId) {
|
|
18
|
+
await this.init();
|
|
19
|
+
if (!this.db) return null;
|
|
20
|
+
const tx = this.db.transaction("chunks", "readonly");
|
|
21
|
+
const store = tx.objectStore("chunks");
|
|
22
|
+
const records = await this.collectRecords(store, clipId);
|
|
23
|
+
for (const record of records) {
|
|
24
|
+
const batch = binarySearchRange(record.batches, timeUs, (b) => ({
|
|
25
|
+
start: b.startUs,
|
|
26
|
+
end: b.startUs + b.durationUs
|
|
27
|
+
}));
|
|
28
|
+
if (!batch) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const chunkData = await this.readFromOPFS(record.fileName, batch);
|
|
32
|
+
if (!chunkData) continue;
|
|
33
|
+
this.updateLastAccess(record.clipId, record.track);
|
|
34
|
+
return this.createChunk(chunkData, timeUs, record.track);
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
async put(clipId, chunks, track) {
|
|
39
|
+
await this.init();
|
|
40
|
+
if (!this.db || !this.opfsRoot) return;
|
|
41
|
+
const fileName = `clip-${clipId}-${track[0]}1.${track === "video" ? "webm" : "m4a"}`;
|
|
42
|
+
const batches = await this.writeToOPFS(fileName, chunks);
|
|
43
|
+
const totalBytes = batches.reduce((sum, b) => sum + b.byteLength, 0);
|
|
44
|
+
const tx = this.db.transaction("chunks", "readwrite");
|
|
45
|
+
const store = tx.objectStore("chunks");
|
|
46
|
+
const record = {
|
|
47
|
+
clipId,
|
|
48
|
+
track,
|
|
49
|
+
fileName,
|
|
50
|
+
batches,
|
|
51
|
+
lastAccess: Date.now(),
|
|
52
|
+
totalBytes
|
|
53
|
+
};
|
|
54
|
+
await this.promisifyRequest(store.put(record));
|
|
55
|
+
await this.enforceQuota();
|
|
56
|
+
}
|
|
57
|
+
async invalidateRange(startUs, endUs, clipId) {
|
|
58
|
+
await this.init();
|
|
59
|
+
if (!this.db) return;
|
|
60
|
+
const tx = this.db.transaction("chunks", "readwrite");
|
|
61
|
+
const store = tx.objectStore("chunks");
|
|
62
|
+
const keysToDelete = [];
|
|
63
|
+
const cursor = store.openCursor();
|
|
64
|
+
await new Promise((resolve) => {
|
|
65
|
+
cursor.onsuccess = (event) => {
|
|
66
|
+
const cursor2 = event.target.result;
|
|
67
|
+
if (!cursor2) {
|
|
68
|
+
resolve();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const record = cursor2.value;
|
|
72
|
+
if (clipId && record.clipId !== clipId) {
|
|
73
|
+
cursor2.continue();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const hasOverlap = record.batches.some((batch) => {
|
|
77
|
+
const batchEnd = batch.startUs + batch.durationUs;
|
|
78
|
+
return batch.startUs < endUs && batchEnd > startUs;
|
|
79
|
+
});
|
|
80
|
+
if (hasOverlap) {
|
|
81
|
+
keysToDelete.push([record.clipId, record.track]);
|
|
82
|
+
}
|
|
83
|
+
cursor2.continue();
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
for (const key of keysToDelete) {
|
|
87
|
+
await this.deleteEntry(key[0], key[1]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async invalidateClip(clipId) {
|
|
91
|
+
await this.init();
|
|
92
|
+
if (!this.db) return;
|
|
93
|
+
const tx = this.db.transaction("chunks", "readwrite");
|
|
94
|
+
const store = tx.objectStore("chunks");
|
|
95
|
+
const records = await this.collectRecords(store, clipId);
|
|
96
|
+
for (const record of records) {
|
|
97
|
+
await this.deleteEntry(record.clipId, record.track);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async clear() {
|
|
101
|
+
await this.init();
|
|
102
|
+
if (!this.db || !this.opfsRoot) return;
|
|
103
|
+
const tx = this.db.transaction(["chunks", "meta"], "readwrite");
|
|
104
|
+
await this.promisifyRequest(tx.objectStore("chunks").clear());
|
|
105
|
+
await this.promisifyRequest(tx.objectStore("meta").clear());
|
|
106
|
+
const projectDir = await this.opfsRoot.getDirectoryHandle(`meframe-project-${this.projectId}`, {
|
|
107
|
+
create: false
|
|
108
|
+
});
|
|
109
|
+
await this.opfsRoot.removeEntry(projectDir.name, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
async initStorage() {
|
|
112
|
+
this.opfsRoot = await navigator.storage.getDirectory();
|
|
113
|
+
const request = indexedDB.open("meframe_cache", 1);
|
|
114
|
+
request.onupgradeneeded = (event) => {
|
|
115
|
+
const db = event.target.result;
|
|
116
|
+
if (!db.objectStoreNames.contains("chunks")) {
|
|
117
|
+
const store = db.createObjectStore("chunks", {
|
|
118
|
+
keyPath: ["clipId", "track"]
|
|
119
|
+
});
|
|
120
|
+
store.createIndex("lastAccess", "lastAccess");
|
|
121
|
+
}
|
|
122
|
+
if (!db.objectStoreNames.contains("meta")) {
|
|
123
|
+
db.createObjectStore("meta", { keyPath: "projectId" });
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
this.db = await new Promise((resolve, reject) => {
|
|
127
|
+
request.onsuccess = () => resolve(request.result);
|
|
128
|
+
request.onerror = () => reject(request.error);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async readFromOPFS(fileName, batch) {
|
|
132
|
+
if (!this.opfsRoot) return null;
|
|
133
|
+
const projectDir = await this.opfsRoot.getDirectoryHandle(`meframe-project-${this.projectId}`, {
|
|
134
|
+
create: false
|
|
135
|
+
});
|
|
136
|
+
const fileHandle = await projectDir.getFileHandle(fileName);
|
|
137
|
+
const file = await fileHandle.getFile();
|
|
138
|
+
const slice = file.slice(batch.byteOffset, batch.byteOffset + batch.byteLength);
|
|
139
|
+
return await slice.arrayBuffer();
|
|
140
|
+
}
|
|
141
|
+
async writeToOPFS(fileName, chunks) {
|
|
142
|
+
if (!this.opfsRoot) return [];
|
|
143
|
+
const projectDir = await this.opfsRoot.getDirectoryHandle(`meframe-project-${this.projectId}`, {
|
|
144
|
+
create: true
|
|
145
|
+
});
|
|
146
|
+
const fileHandle = await projectDir.getFileHandle(fileName, { create: true });
|
|
147
|
+
const writable = await fileHandle.createWritable();
|
|
148
|
+
const batches = [];
|
|
149
|
+
let offset = 0;
|
|
150
|
+
for (const chunk of chunks) {
|
|
151
|
+
const data = await this.chunkToArrayBuffer(chunk);
|
|
152
|
+
await writable.write({ type: "write", position: offset, data });
|
|
153
|
+
batches.push({
|
|
154
|
+
startUs: chunk.timestamp,
|
|
155
|
+
durationUs: chunk.duration || 0,
|
|
156
|
+
byteOffset: offset,
|
|
157
|
+
byteLength: data.byteLength
|
|
158
|
+
});
|
|
159
|
+
offset += data.byteLength;
|
|
160
|
+
}
|
|
161
|
+
await writable.close();
|
|
162
|
+
return batches;
|
|
163
|
+
}
|
|
164
|
+
async chunkToArrayBuffer(chunk) {
|
|
165
|
+
const buffer = new ArrayBuffer(chunk.byteLength);
|
|
166
|
+
chunk.copyTo(buffer);
|
|
167
|
+
return buffer;
|
|
168
|
+
}
|
|
169
|
+
createChunk(data, timeUs, track) {
|
|
170
|
+
if (track === "video") {
|
|
171
|
+
return new EncodedVideoChunk({
|
|
172
|
+
type: "key",
|
|
173
|
+
// Should be determined from metadata
|
|
174
|
+
timestamp: timeUs,
|
|
175
|
+
data
|
|
176
|
+
});
|
|
177
|
+
} else {
|
|
178
|
+
return new EncodedAudioChunk({
|
|
179
|
+
type: "key",
|
|
180
|
+
timestamp: timeUs,
|
|
181
|
+
data
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async updateLastAccess(clipId, track) {
|
|
186
|
+
if (!this.db) return;
|
|
187
|
+
const tx = this.db.transaction("chunks", "readwrite");
|
|
188
|
+
const store = tx.objectStore("chunks");
|
|
189
|
+
const record = await this.promisifyRequest(store.get([clipId, track]));
|
|
190
|
+
if (record) {
|
|
191
|
+
record.lastAccess = Date.now();
|
|
192
|
+
await this.promisifyRequest(store.put(record));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async deleteEntry(clipId, track) {
|
|
196
|
+
if (!this.db) return;
|
|
197
|
+
const tx = this.db.transaction("chunks", "readwrite");
|
|
198
|
+
const store = tx.objectStore("chunks");
|
|
199
|
+
const record = await this.promisifyRequest(store.get([clipId, track]));
|
|
200
|
+
if (record && this.opfsRoot) {
|
|
201
|
+
const projectDir = await this.opfsRoot.getDirectoryHandle(
|
|
202
|
+
`meframe-project-${this.projectId}`,
|
|
203
|
+
{ create: false }
|
|
204
|
+
);
|
|
205
|
+
await projectDir.removeEntry(record.fileName);
|
|
206
|
+
}
|
|
207
|
+
await this.promisifyRequest(store.delete([clipId, track]));
|
|
208
|
+
}
|
|
209
|
+
async enforceQuota() {
|
|
210
|
+
const estimate = await navigator.storage.estimate();
|
|
211
|
+
const usage = estimate.usage || 0;
|
|
212
|
+
if (usage <= this.maxSize) return;
|
|
213
|
+
if (!this.db) return;
|
|
214
|
+
const tx = this.db.transaction("chunks", "readwrite");
|
|
215
|
+
const store = tx.objectStore("chunks");
|
|
216
|
+
const index = store.index("lastAccess");
|
|
217
|
+
let bytesDeleted = 0;
|
|
218
|
+
const toDelete = usage - this.maxSize;
|
|
219
|
+
const cursor = index.openCursor();
|
|
220
|
+
await new Promise((resolve) => {
|
|
221
|
+
cursor.onsuccess = async (event) => {
|
|
222
|
+
const cursor2 = event.target.result;
|
|
223
|
+
if (!cursor2 || bytesDeleted >= toDelete) {
|
|
224
|
+
resolve();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const record = cursor2.value;
|
|
228
|
+
await this.deleteEntry(record.clipId, record.track);
|
|
229
|
+
bytesDeleted += record.totalBytes;
|
|
230
|
+
cursor2.continue();
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
async collectRecords(store, clipId) {
|
|
235
|
+
const records = [];
|
|
236
|
+
const cursor = store.openCursor();
|
|
237
|
+
await new Promise((resolve) => {
|
|
238
|
+
cursor.onsuccess = (event) => {
|
|
239
|
+
const cursor2 = event.target.result;
|
|
240
|
+
if (!cursor2) {
|
|
241
|
+
resolve();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const record = cursor2.value;
|
|
245
|
+
if (record.clipId === clipId) {
|
|
246
|
+
records.push(record);
|
|
247
|
+
}
|
|
248
|
+
cursor2.continue();
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
return records;
|
|
252
|
+
}
|
|
253
|
+
promisifyRequest(request) {
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
request.onsuccess = () => resolve(request.result);
|
|
256
|
+
request.onerror = () => reject(request.error);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
getMetadata() {
|
|
260
|
+
return {
|
|
261
|
+
maxSizeMB: this.maxSize / (1024 * 1024),
|
|
262
|
+
usedSizeMB: 0,
|
|
263
|
+
// Would need to track actual usage
|
|
264
|
+
entries: 0,
|
|
265
|
+
// Would need to track actual entries
|
|
266
|
+
hitRate: 0
|
|
267
|
+
// Would need to track hits and misses
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
async hasAvailableQuota(sizeMB) {
|
|
271
|
+
if (typeof navigator === "undefined" || !navigator.storage?.estimate) {
|
|
272
|
+
throw new Error("Storage API not available");
|
|
273
|
+
}
|
|
274
|
+
const estimate = await navigator.storage.estimate();
|
|
275
|
+
const availableMB = ((estimate.quota || 0) - (estimate.usage || 0)) / (1024 * 1024);
|
|
276
|
+
return availableMB >= sizeMB;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
export {
|
|
280
|
+
L2Cache
|
|
281
|
+
};
|
|
282
|
+
//# sourceMappingURL=L2Cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"L2Cache.js","sources":["../../src/cache/L2Cache.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\n// import type { ClipMetadata } from './types';\nimport { binarySearchRange } from '../utils/binary-search';\n\ninterface ChunkBatch {\n startUs: TimeUs;\n durationUs: TimeUs;\n byteOffset: number;\n byteLength: number;\n}\n\ninterface ChunkRecord {\n clipId: string;\n track: 'video' | 'audio';\n fileName: string;\n batches: ChunkBatch[];\n lastAccess: number;\n totalBytes: number;\n}\n\ninterface L2Config {\n maxSizeMB: number;\n projectId: string;\n}\n\nexport class L2Cache {\n private db: IDBDatabase | null = null;\n private opfsRoot: FileSystemDirectoryHandle | null = null;\n readonly maxSize: number;\n readonly projectId: string;\n private initPromise: Promise<void> | null = null;\n\n constructor(config: L2Config) {\n this.maxSize = config.maxSizeMB * 1024 * 1024;\n this.projectId = config.projectId;\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 async get(timeUs: TimeUs, clipId: string): Promise<EncodedVideoChunk | EncodedAudioChunk | null> {\n await this.init();\n\n if (!this.db) return null;\n\n // Query IndexedDB for chunk metadata\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n const records = await this.collectRecords(store, clipId);\n\n for (const record of records) {\n const batch = binarySearchRange(record.batches, timeUs, (b) => ({\n start: b.startUs,\n end: b.startUs + b.durationUs,\n }));\n\n if (!batch) {\n continue;\n }\n\n const chunkData = await this.readFromOPFS(record.fileName, batch);\n if (!chunkData) continue;\n\n this.updateLastAccess(record.clipId, record.track);\n\n return this.createChunk(chunkData, timeUs, record.track);\n }\n\n return null;\n }\n\n async put(\n clipId: string,\n chunks: Array<EncodedVideoChunk | EncodedAudioChunk>,\n track: 'video' | 'audio'\n ): Promise<void> {\n await this.init();\n\n if (!this.db || !this.opfsRoot) return;\n\n const fileName = `clip-${clipId}-${track[0]}1.${track === 'video' ? 'webm' : 'm4a'}`;\n\n // Write chunks to OPFS\n const batches = await this.writeToOPFS(fileName, chunks);\n\n // Calculate total size\n const totalBytes = batches.reduce((sum, b) => sum + b.byteLength, 0);\n\n // Update IndexedDB index\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n\n const record: ChunkRecord = {\n clipId,\n track,\n fileName,\n batches,\n lastAccess: Date.now(),\n totalBytes,\n };\n\n await this.promisifyRequest(store.put(record));\n\n // Check and enforce quota\n await this.enforceQuota();\n }\n\n async invalidateRange(startUs: TimeUs, endUs: TimeUs, clipId?: string): Promise<void> {\n await this.init();\n\n if (!this.db) return;\n\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n const keysToDelete: Array<[string, string]> = [];\n\n // Iterate through all records\n const cursor = store.openCursor();\n await new Promise<void>((resolve) => {\n cursor.onsuccess = (event) => {\n const cursor = (event.target as IDBRequest).result;\n if (!cursor) {\n resolve();\n return;\n }\n\n const record: ChunkRecord = cursor.value;\n\n if (clipId && record.clipId !== clipId) {\n cursor.continue();\n return;\n }\n\n // Check if any batch overlaps with invalidation range\n const hasOverlap = record.batches.some((batch) => {\n const batchEnd = batch.startUs + batch.durationUs;\n return batch.startUs < endUs && batchEnd > startUs;\n });\n\n if (hasOverlap) {\n keysToDelete.push([record.clipId, record.track]);\n }\n\n cursor.continue();\n };\n });\n\n // Delete invalidated entries\n for (const key of keysToDelete) {\n await this.deleteEntry(key[0], key[1]);\n }\n }\n\n async invalidateClip(clipId: string): Promise<void> {\n await this.init();\n\n if (!this.db) return;\n\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n const records = await this.collectRecords(store, clipId);\n\n for (const record of records) {\n await this.deleteEntry(record.clipId, record.track);\n }\n }\n\n async clear(): Promise<void> {\n await this.init();\n\n if (!this.db || !this.opfsRoot) return;\n\n // Clear IndexedDB\n const tx = this.db.transaction(['chunks', 'meta'], 'readwrite');\n await this.promisifyRequest(tx.objectStore('chunks').clear());\n await this.promisifyRequest(tx.objectStore('meta').clear());\n\n // Clear OPFS files\n const projectDir = await this.opfsRoot.getDirectoryHandle(`meframe-project-${this.projectId}`, {\n create: false,\n });\n await this.opfsRoot.removeEntry(projectDir.name, { recursive: true });\n }\n\n private async initStorage(): Promise<void> {\n // Initialize OPFS\n this.opfsRoot = await navigator.storage.getDirectory();\n\n // Initialize IndexedDB\n const request = indexedDB.open('meframe_cache', 1);\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result;\n\n // chunks store with composite key\n if (!db.objectStoreNames.contains('chunks')) {\n const store = db.createObjectStore('chunks', {\n keyPath: ['clipId', 'track'],\n });\n store.createIndex('lastAccess', 'lastAccess');\n }\n\n // meta store\n if (!db.objectStoreNames.contains('meta')) {\n db.createObjectStore('meta', { keyPath: 'projectId' });\n }\n };\n\n this.db = await new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n }\n\n private async readFromOPFS(fileName: string, batch: ChunkBatch): Promise<ArrayBuffer | null> {\n if (!this.opfsRoot) return null;\n\n const projectDir = await this.opfsRoot.getDirectoryHandle(`meframe-project-${this.projectId}`, {\n create: false,\n });\n const fileHandle = await projectDir.getFileHandle(fileName);\n const file = await fileHandle.getFile();\n const slice = file.slice(batch.byteOffset, batch.byteOffset + batch.byteLength);\n return await slice.arrayBuffer();\n }\n\n private async writeToOPFS(\n fileName: string,\n chunks: Array<EncodedVideoChunk | EncodedAudioChunk>\n ): Promise<ChunkBatch[]> {\n if (!this.opfsRoot) return [];\n\n const projectDir = await this.opfsRoot.getDirectoryHandle(`meframe-project-${this.projectId}`, {\n create: true,\n });\n const fileHandle = await projectDir.getFileHandle(fileName, { create: true });\n const writable = await fileHandle.createWritable();\n\n const batches: ChunkBatch[] = [];\n let offset = 0;\n\n for (const chunk of chunks) {\n const data = await this.chunkToArrayBuffer(chunk);\n await writable.write({ type: 'write', position: offset, data });\n\n batches.push({\n startUs: chunk.timestamp,\n durationUs: chunk.duration || 0,\n byteOffset: offset,\n byteLength: data.byteLength,\n });\n\n offset += data.byteLength;\n }\n\n await writable.close();\n return batches;\n }\n\n private async chunkToArrayBuffer(\n chunk: EncodedVideoChunk | EncodedAudioChunk\n ): Promise<ArrayBuffer> {\n const buffer = new ArrayBuffer(chunk.byteLength);\n chunk.copyTo(buffer);\n return buffer;\n }\n\n private createChunk(\n data: ArrayBuffer,\n timeUs: TimeUs,\n track: 'video' | 'audio'\n ): EncodedVideoChunk | EncodedAudioChunk {\n if (track === 'video') {\n return new EncodedVideoChunk({\n type: 'key', // Should be determined from metadata\n timestamp: timeUs,\n data,\n });\n } else {\n return new EncodedAudioChunk({\n type: 'key',\n timestamp: timeUs,\n data,\n });\n }\n }\n\n private async updateLastAccess(clipId: string, track: string): Promise<void> {\n if (!this.db) return;\n\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n const record = await this.promisifyRequest<ChunkRecord>(store.get([clipId, track]));\n\n if (record) {\n record.lastAccess = Date.now();\n await this.promisifyRequest(store.put(record));\n }\n }\n\n private async deleteEntry(clipId: string, track: string): Promise<void> {\n if (!this.db) return;\n\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n const record = await this.promisifyRequest<ChunkRecord>(store.get([clipId, track]));\n\n if (record && this.opfsRoot) {\n // Delete OPFS file\n const projectDir = await this.opfsRoot.getDirectoryHandle(\n `meframe-project-${this.projectId}`,\n { create: false }\n );\n await projectDir.removeEntry(record.fileName);\n }\n\n // Delete IndexedDB record\n await this.promisifyRequest(store.delete([clipId, track]));\n }\n\n private async enforceQuota(): Promise<void> {\n const estimate = await navigator.storage.estimate();\n const usage = estimate.usage || 0;\n\n if (usage <= this.maxSize) return;\n\n if (!this.db) return;\n\n // Delete oldest entries until under quota\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n const index = store.index('lastAccess');\n\n let bytesDeleted = 0;\n const toDelete = usage - this.maxSize;\n\n const cursor = index.openCursor();\n await new Promise<void>((resolve) => {\n cursor.onsuccess = async (event) => {\n const cursor = (event.target as IDBRequest).result;\n if (!cursor || bytesDeleted >= toDelete) {\n resolve();\n return;\n }\n\n const record: ChunkRecord = cursor.value;\n await this.deleteEntry(record.clipId, record.track);\n bytesDeleted += record.totalBytes;\n\n cursor.continue();\n };\n });\n }\n\n private async collectRecords(store: IDBObjectStore, clipId: string): Promise<ChunkRecord[]> {\n const records: ChunkRecord[] = [];\n const cursor = store.openCursor();\n await new Promise<void>((resolve) => {\n cursor.onsuccess = (event) => {\n const cursor = (event.target as IDBRequest).result;\n if (!cursor) {\n resolve();\n return;\n }\n\n const record: ChunkRecord = cursor.value;\n if (record.clipId === clipId) {\n records.push(record);\n }\n\n cursor.continue();\n };\n });\n return records;\n }\n\n private promisifyRequest<T>(request: IDBRequest): Promise<T> {\n return new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n }\n\n getMetadata(): {\n maxSizeMB: number;\n usedSizeMB: number;\n entries: number;\n hitRate: number;\n } {\n // This is a simplified implementation\n // In a real implementation, we would track actual usage\n return {\n maxSizeMB: this.maxSize / (1024 * 1024),\n usedSizeMB: 0, // Would need to track actual usage\n entries: 0, // Would need to track actual entries\n hitRate: 0, // Would need to track hits and misses\n };\n }\n\n async hasAvailableQuota(sizeMB: number): Promise<boolean> {\n if (typeof navigator === 'undefined' || !navigator.storage?.estimate) {\n // L2Cache requires storage API to function\n throw new Error('Storage API not available');\n }\n\n const estimate = await navigator.storage.estimate();\n const availableMB = ((estimate.quota || 0) - (estimate.usage || 0)) / (1024 * 1024);\n return availableMB >= sizeMB;\n }\n}\n"],"names":["cursor"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { VideoL1Cache } from './l1/VideoL1Cache';
|
|
2
|
+
export { L2Cache } from './L2Cache';
|
|
3
|
+
export { CacheManager } from './CacheManager';
|
|
4
|
+
export { BatchWriter } from './BatchWriter';
|
|
5
|
+
export { CacheStatsDecorator } from './CacheStatsDecorator';
|
|
6
|
+
export type { CacheConfig, CacheStats, CacheEntry, GOP, ClipMetadata } from './types';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE5D,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AudioMetadata, AudioSlot } from './types';
|
|
2
|
+
import { TimeUs } from '../../model/types';
|
|
3
|
+
|
|
4
|
+
export declare class AudioL1Cache {
|
|
5
|
+
private slots;
|
|
6
|
+
private clipPCM;
|
|
7
|
+
private metadata;
|
|
8
|
+
private volume;
|
|
9
|
+
private muted;
|
|
10
|
+
private maxClips;
|
|
11
|
+
attachStream(stream: ReadableStream<AudioData>, metadata: AudioMetadata): void;
|
|
12
|
+
putClipAudioData(clipId: string, audioData: AudioData, clipStartUs: TimeUs, clipDurationUs: TimeUs): void;
|
|
13
|
+
getPCM(clipId: string, startUs: TimeUs, endUs: TimeUs): Float32Array[] | null;
|
|
14
|
+
hasClipPCM(clipId: string): boolean;
|
|
15
|
+
clearClipPCM(clipId: string): void;
|
|
16
|
+
private evictLRU;
|
|
17
|
+
private extractPlanesFromAudioData;
|
|
18
|
+
addAudio(audio: AudioData): void;
|
|
19
|
+
getClosest(timeUs: number): (AudioSlot & {
|
|
20
|
+
metadata: AudioMetadata;
|
|
21
|
+
}) | null;
|
|
22
|
+
flush(): void;
|
|
23
|
+
reset(): void;
|
|
24
|
+
setPlaybackRate(_rate: number): void;
|
|
25
|
+
setVolume(volume: number): void;
|
|
26
|
+
setMute(muted: boolean): void;
|
|
27
|
+
dispose(): void;
|
|
28
|
+
private applyGain;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=AudioL1Cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AudioL1Cache.d.ts","sourceRoot":"","sources":["../../../src/cache/l1/AudioL1Cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAYhD,qBAAa,YAAY;IACvB,OAAO,CAAC,KAAK,CAAmB;IAChC,OAAO,CAAC,OAAO,CAAmC;IAClD,OAAO,CAAC,QAAQ,CAA8D;IAC9E,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,QAAQ,CAAM;IAEtB,YAAY,CAAC,MAAM,EAAE,cAAc,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI;IAqB9E,gBAAgB,CACd,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,EACpB,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,GACrB,IAAI;IA6CP,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,EAAE,GAAG,IAAI;IAqC7E,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAInC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAIlC,OAAO,CAAC,QAAQ;IAgBhB,OAAO,CAAC,0BAA0B;IAqIlC,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI;IA6BhC,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,SAAS,GAAG;QAAE,QAAQ,EAAE,aAAa,CAAA;KAAE,CAAC,GAAG,IAAI;IAkC5E,KAAK,IAAI,IAAI;IAIb,KAAK,IAAI,IAAI;IAQb,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIpC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI/B,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAI7B,OAAO,IAAI,IAAI;IAIf,OAAO,CAAC,SAAS;CAiBlB"}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
class AudioL1Cache {
|
|
2
|
+
slots = [];
|
|
3
|
+
clipPCM = /* @__PURE__ */ new Map();
|
|
4
|
+
metadata = { sampleRate: 48e3, numberOfChannels: 2 };
|
|
5
|
+
volume = 1;
|
|
6
|
+
muted = false;
|
|
7
|
+
maxClips = 20;
|
|
8
|
+
attachStream(stream, metadata) {
|
|
9
|
+
this.metadata = metadata;
|
|
10
|
+
const reader = stream.getReader();
|
|
11
|
+
const pump = async () => {
|
|
12
|
+
const { done, value } = await reader.read();
|
|
13
|
+
if (done) {
|
|
14
|
+
reader.releaseLock();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
this.addAudio(value);
|
|
18
|
+
await pump();
|
|
19
|
+
};
|
|
20
|
+
pump().catch((error) => {
|
|
21
|
+
console.error("[AudioL1Cache] stream error", error);
|
|
22
|
+
reader.releaseLock();
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
putClipAudioData(clipId, audioData, clipStartUs, clipDurationUs) {
|
|
26
|
+
const numberOfChannels = audioData.numberOfChannels ?? this.metadata.numberOfChannels;
|
|
27
|
+
const numberOfFrames = audioData.numberOfFrames ?? 0;
|
|
28
|
+
const sampleRate = audioData.sampleRate ?? this.metadata.sampleRate;
|
|
29
|
+
if (!numberOfChannels || !numberOfFrames) {
|
|
30
|
+
audioData.close();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const planes = this.extractPlanesFromAudioData(audioData, numberOfChannels, numberOfFrames);
|
|
34
|
+
audioData.close();
|
|
35
|
+
let entry = this.clipPCM.get(clipId);
|
|
36
|
+
if (!entry) {
|
|
37
|
+
entry = {
|
|
38
|
+
clipId,
|
|
39
|
+
sampleRate,
|
|
40
|
+
numberOfChannels,
|
|
41
|
+
planes: Array.from({ length: numberOfChannels }, () => new Float32Array(0)),
|
|
42
|
+
startUs: clipStartUs,
|
|
43
|
+
durationUs: clipDurationUs,
|
|
44
|
+
lastAccessedAt: Date.now()
|
|
45
|
+
};
|
|
46
|
+
this.clipPCM.set(clipId, entry);
|
|
47
|
+
if (this.clipPCM.size > this.maxClips) {
|
|
48
|
+
this.evictLRU();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (let channel = 0; channel < numberOfChannels; channel++) {
|
|
52
|
+
const existingPlane = entry.planes[channel];
|
|
53
|
+
const newPlane = planes[channel];
|
|
54
|
+
if (!existingPlane || !newPlane) continue;
|
|
55
|
+
const combined = new Float32Array(existingPlane.length + newPlane.length);
|
|
56
|
+
combined.set(existingPlane, 0);
|
|
57
|
+
combined.set(newPlane, existingPlane.length);
|
|
58
|
+
entry.planes[channel] = combined;
|
|
59
|
+
}
|
|
60
|
+
entry.lastAccessedAt = Date.now();
|
|
61
|
+
}
|
|
62
|
+
getPCM(clipId, startUs, endUs) {
|
|
63
|
+
const entry = this.clipPCM.get(clipId);
|
|
64
|
+
if (!entry) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
entry.lastAccessedAt = Date.now();
|
|
68
|
+
const offsetUs = Math.max(0, startUs - entry.startUs);
|
|
69
|
+
const durationUs = Math.min(endUs - startUs, entry.durationUs - offsetUs);
|
|
70
|
+
if (durationUs <= 0) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const offsetFrames = Math.floor(offsetUs / 1e6 * entry.sampleRate);
|
|
74
|
+
const frameCount = Math.ceil(durationUs / 1e6 * entry.sampleRate);
|
|
75
|
+
const result = [];
|
|
76
|
+
for (let channel = 0; channel < entry.numberOfChannels; channel++) {
|
|
77
|
+
const plane = entry.planes[channel];
|
|
78
|
+
if (!plane) {
|
|
79
|
+
result.push(new Float32Array(frameCount));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const channelData = new Float32Array(frameCount);
|
|
83
|
+
const copyLength = Math.min(frameCount, plane.length - offsetFrames);
|
|
84
|
+
for (let i = 0; i < copyLength; i++) {
|
|
85
|
+
channelData[i] = plane[offsetFrames + i] ?? 0;
|
|
86
|
+
}
|
|
87
|
+
result.push(channelData);
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
hasClipPCM(clipId) {
|
|
92
|
+
return this.clipPCM.has(clipId);
|
|
93
|
+
}
|
|
94
|
+
clearClipPCM(clipId) {
|
|
95
|
+
this.clipPCM.delete(clipId);
|
|
96
|
+
}
|
|
97
|
+
evictLRU() {
|
|
98
|
+
let oldestClipId = null;
|
|
99
|
+
let oldestTime = Date.now();
|
|
100
|
+
for (const [clipId, entry] of this.clipPCM) {
|
|
101
|
+
if (entry.lastAccessedAt < oldestTime) {
|
|
102
|
+
oldestTime = entry.lastAccessedAt;
|
|
103
|
+
oldestClipId = clipId;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (oldestClipId) {
|
|
107
|
+
this.clipPCM.delete(oldestClipId);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
extractPlanesFromAudioData(audioData, numberOfChannels, numberOfFrames) {
|
|
111
|
+
const planes = Array.from(
|
|
112
|
+
{ length: numberOfChannels },
|
|
113
|
+
() => new Float32Array(numberOfFrames)
|
|
114
|
+
);
|
|
115
|
+
const toFloat = (value) => value / 32768;
|
|
116
|
+
const fillInterleaved = (format) => {
|
|
117
|
+
const samples = format === "f32" ? new Float32Array(numberOfFrames * numberOfChannels) : new Int16Array(numberOfFrames * numberOfChannels);
|
|
118
|
+
try {
|
|
119
|
+
audioData.copyTo(samples, { format, planeIndex: 0 });
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
for (let frame = 0; frame < numberOfFrames; frame += 1) {
|
|
124
|
+
const offset = frame * numberOfChannels;
|
|
125
|
+
for (let channel = 0; channel < numberOfChannels; channel += 1) {
|
|
126
|
+
const plane = planes[channel];
|
|
127
|
+
if (!plane) continue;
|
|
128
|
+
if (format === "f32") {
|
|
129
|
+
plane[frame] = samples[offset + channel] ?? 0;
|
|
130
|
+
} else {
|
|
131
|
+
plane[frame] = toFloat(samples[offset + channel] ?? 0);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
};
|
|
137
|
+
const fillPlanar = (format) => {
|
|
138
|
+
try {
|
|
139
|
+
if (format === "f32-planar") {
|
|
140
|
+
for (let channel = 0; channel < numberOfChannels; channel += 1) {
|
|
141
|
+
const plane = planes[channel];
|
|
142
|
+
if (!plane) continue;
|
|
143
|
+
audioData.copyTo(plane, { planeIndex: channel, format: "f32-planar" });
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
const tmp = new Int16Array(numberOfFrames);
|
|
148
|
+
for (let channel = 0; channel < numberOfChannels; channel += 1) {
|
|
149
|
+
const plane = planes[channel];
|
|
150
|
+
if (!plane) continue;
|
|
151
|
+
audioData.copyTo(tmp, { planeIndex: channel, format: "s16-planar" });
|
|
152
|
+
for (let i = 0; i < numberOfFrames; i += 1) {
|
|
153
|
+
plane[i] = toFloat(tmp[i] ?? 0);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const fillFallback = () => {
|
|
162
|
+
try {
|
|
163
|
+
for (let channel = 0; channel < numberOfChannels; channel += 1) {
|
|
164
|
+
const plane = planes[channel];
|
|
165
|
+
if (!plane) continue;
|
|
166
|
+
audioData.copyTo(plane, { planeIndex: channel });
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const reportedFormat = audioData.format;
|
|
174
|
+
const attempts = [];
|
|
175
|
+
const scheduled = /* @__PURE__ */ new Set();
|
|
176
|
+
const scheduleAttempt = (token, attempt) => {
|
|
177
|
+
if (!scheduled.has(token)) {
|
|
178
|
+
scheduled.add(token);
|
|
179
|
+
attempts.push(attempt);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
if (reportedFormat) {
|
|
183
|
+
switch (reportedFormat) {
|
|
184
|
+
case "f32":
|
|
185
|
+
scheduleAttempt("f32", () => fillInterleaved("f32"));
|
|
186
|
+
break;
|
|
187
|
+
case "s16":
|
|
188
|
+
scheduleAttempt("s16", () => fillInterleaved("s16"));
|
|
189
|
+
break;
|
|
190
|
+
case "f32-planar":
|
|
191
|
+
scheduleAttempt("f32-planar", () => fillPlanar("f32-planar"));
|
|
192
|
+
break;
|
|
193
|
+
case "s16-planar":
|
|
194
|
+
scheduleAttempt("s16-planar", () => fillPlanar("s16-planar"));
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
scheduleAttempt("f32", () => fillInterleaved("f32"));
|
|
199
|
+
scheduleAttempt("f32-planar", () => fillPlanar("f32-planar"));
|
|
200
|
+
scheduleAttempt("s16", () => fillInterleaved("s16"));
|
|
201
|
+
scheduleAttempt("s16-planar", () => fillPlanar("s16-planar"));
|
|
202
|
+
let filled = false;
|
|
203
|
+
for (const attempt of attempts) {
|
|
204
|
+
if (attempt()) {
|
|
205
|
+
filled = true;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (!filled) {
|
|
210
|
+
filled = fillFallback();
|
|
211
|
+
}
|
|
212
|
+
if (!filled) {
|
|
213
|
+
throw new Error("AudioL1Cache: unsupported AudioData format");
|
|
214
|
+
}
|
|
215
|
+
return planes;
|
|
216
|
+
}
|
|
217
|
+
addAudio(audio) {
|
|
218
|
+
const numberOfChannels = audio.numberOfChannels ?? this.metadata.numberOfChannels;
|
|
219
|
+
const numberOfFrames = audio.numberOfFrames ?? 0;
|
|
220
|
+
if (!numberOfChannels || !numberOfFrames) {
|
|
221
|
+
audio.close();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (audio.sampleRate && audio.sampleRate > 0 && audio.sampleRate !== this.metadata.sampleRate) {
|
|
225
|
+
this.metadata = { ...this.metadata, sampleRate: audio.sampleRate };
|
|
226
|
+
}
|
|
227
|
+
if (numberOfChannels !== this.metadata.numberOfChannels) {
|
|
228
|
+
this.metadata = { ...this.metadata, numberOfChannels };
|
|
229
|
+
}
|
|
230
|
+
const planes = this.extractPlanesFromAudioData(audio, numberOfChannels, numberOfFrames);
|
|
231
|
+
this.slots.push({
|
|
232
|
+
timestampUs: audio.timestamp ?? 0,
|
|
233
|
+
durationUs: audio.duration ?? Math.round(numberOfFrames / this.metadata.sampleRate * 1e6),
|
|
234
|
+
planes
|
|
235
|
+
});
|
|
236
|
+
audio.close();
|
|
237
|
+
}
|
|
238
|
+
getClosest(timeUs) {
|
|
239
|
+
if (this.slots.length === 0) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
let closest = null;
|
|
243
|
+
let minDelta = Number.MAX_SAFE_INTEGER;
|
|
244
|
+
for (const slot of this.slots) {
|
|
245
|
+
const start = slot.timestampUs;
|
|
246
|
+
const end = start + slot.durationUs;
|
|
247
|
+
if (timeUs >= start && timeUs <= end) {
|
|
248
|
+
closest = slot;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
const delta = Math.min(Math.abs(timeUs - start), Math.abs(timeUs - end));
|
|
252
|
+
if (delta < minDelta) {
|
|
253
|
+
closest = slot;
|
|
254
|
+
minDelta = delta;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!closest) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
...closest,
|
|
262
|
+
planes: this.applyGain(closest.planes),
|
|
263
|
+
metadata: this.metadata
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
flush() {
|
|
267
|
+
this.slots = [];
|
|
268
|
+
}
|
|
269
|
+
reset() {
|
|
270
|
+
this.flush();
|
|
271
|
+
this.clipPCM.clear();
|
|
272
|
+
this.metadata = { sampleRate: 48e3, numberOfChannels: 2 };
|
|
273
|
+
this.volume = 1;
|
|
274
|
+
this.muted = false;
|
|
275
|
+
}
|
|
276
|
+
setPlaybackRate(_rate) {
|
|
277
|
+
}
|
|
278
|
+
setVolume(volume) {
|
|
279
|
+
this.volume = Math.max(0, Math.min(1, volume));
|
|
280
|
+
}
|
|
281
|
+
setMute(muted) {
|
|
282
|
+
this.muted = muted;
|
|
283
|
+
}
|
|
284
|
+
dispose() {
|
|
285
|
+
this.reset();
|
|
286
|
+
}
|
|
287
|
+
applyGain(planes) {
|
|
288
|
+
if (this.muted || this.volume === 0) {
|
|
289
|
+
return planes.map((plane) => new Float32Array(plane.length));
|
|
290
|
+
}
|
|
291
|
+
if (this.volume === 1) {
|
|
292
|
+
return planes.map((plane) => plane.slice());
|
|
293
|
+
}
|
|
294
|
+
return planes.map((plane) => {
|
|
295
|
+
const scaled = new Float32Array(plane.length);
|
|
296
|
+
for (let i = 0; i < plane.length; i += 1) {
|
|
297
|
+
scaled[i] = (plane[i] ?? 0) * this.volume;
|
|
298
|
+
}
|
|
299
|
+
return scaled;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
export {
|
|
304
|
+
AudioL1Cache
|
|
305
|
+
};
|
|
306
|
+
//# sourceMappingURL=AudioL1Cache.js.map
|