@meframe/core 0.0.29 → 0.0.30-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/dist/Meframe.d.ts +2 -13
  2. package/dist/Meframe.d.ts.map +1 -1
  3. package/dist/Meframe.js +6 -100
  4. package/dist/Meframe.js.map +1 -1
  5. package/dist/cache/CacheManager.d.ts +35 -19
  6. package/dist/cache/CacheManager.d.ts.map +1 -1
  7. package/dist/cache/CacheManager.js +223 -134
  8. package/dist/cache/CacheManager.js.map +1 -1
  9. package/dist/cache/l1/VideoL1Cache.d.ts +15 -2
  10. package/dist/cache/l1/VideoL1Cache.d.ts.map +1 -1
  11. package/dist/cache/l1/VideoL1Cache.js +58 -38
  12. package/dist/cache/l1/VideoL1Cache.js.map +1 -1
  13. package/dist/cache/l2/L2Cache.d.ts.map +1 -1
  14. package/dist/cache/l2/L2Cache.js +5 -5
  15. package/dist/cache/l2/L2Cache.js.map +1 -1
  16. package/dist/cache/l2/L2OPFSStore.d.ts +37 -0
  17. package/dist/cache/l2/L2OPFSStore.d.ts.map +1 -0
  18. package/dist/cache/l2/L2OPFSStore.js +89 -0
  19. package/dist/cache/l2/L2OPFSStore.js.map +1 -0
  20. package/dist/cache/resource/AudioSampleCache.d.ts +52 -0
  21. package/dist/cache/resource/AudioSampleCache.d.ts.map +1 -0
  22. package/dist/cache/resource/AudioSampleCache.js +69 -0
  23. package/dist/cache/resource/AudioSampleCache.js.map +1 -0
  24. package/dist/cache/resource/ImageBitmapCache.d.ts +65 -0
  25. package/dist/cache/resource/ImageBitmapCache.d.ts.map +1 -0
  26. package/dist/cache/resource/ImageBitmapCache.js +101 -0
  27. package/dist/cache/resource/ImageBitmapCache.js.map +1 -0
  28. package/dist/cache/resource/MP4IndexCache.d.ts +48 -0
  29. package/dist/cache/resource/MP4IndexCache.d.ts.map +1 -0
  30. package/dist/cache/resource/MP4IndexCache.js +104 -0
  31. package/dist/cache/resource/MP4IndexCache.js.map +1 -0
  32. package/dist/cache/resource/ResourceCache.d.ts +46 -0
  33. package/dist/cache/resource/ResourceCache.d.ts.map +1 -0
  34. package/dist/cache/resource/ResourceCache.js +92 -0
  35. package/dist/cache/resource/ResourceCache.js.map +1 -0
  36. package/dist/cache/storage/indexeddb/ChunkRecordStore.d.ts +75 -0
  37. package/dist/cache/storage/indexeddb/ChunkRecordStore.d.ts.map +1 -0
  38. package/dist/cache/{l2/IndexedDBStore.js → storage/indexeddb/ChunkRecordStore.js} +3 -3
  39. package/dist/cache/storage/indexeddb/ChunkRecordStore.js.map +1 -0
  40. package/dist/cache/storage/opfs/OPFSManager.d.ts +54 -0
  41. package/dist/cache/storage/opfs/OPFSManager.d.ts.map +1 -0
  42. package/dist/cache/storage/opfs/OPFSManager.js +133 -0
  43. package/dist/cache/storage/opfs/OPFSManager.js.map +1 -0
  44. package/dist/cache/storage/opfs/types.d.ts +16 -0
  45. package/dist/cache/storage/opfs/types.d.ts.map +1 -0
  46. package/dist/config/defaults.d.ts.map +1 -1
  47. package/dist/config/defaults.js +21 -2
  48. package/dist/config/defaults.js.map +1 -1
  49. package/dist/config/types.d.ts +28 -0
  50. package/dist/config/types.d.ts.map +1 -1
  51. package/dist/controllers/ExportController.d.ts +16 -0
  52. package/dist/controllers/ExportController.d.ts.map +1 -0
  53. package/dist/controllers/ExportController.js +44 -0
  54. package/dist/controllers/ExportController.js.map +1 -0
  55. package/dist/controllers/PlaybackController.d.ts +28 -4
  56. package/dist/controllers/PlaybackController.d.ts.map +1 -1
  57. package/dist/controllers/PlaybackController.js +116 -51
  58. package/dist/controllers/PlaybackController.js.map +1 -1
  59. package/dist/controllers/index.d.ts +2 -3
  60. package/dist/controllers/index.d.ts.map +1 -1
  61. package/dist/controllers/types.d.ts +0 -28
  62. package/dist/controllers/types.d.ts.map +1 -1
  63. package/dist/event/events.d.ts +8 -0
  64. package/dist/event/events.d.ts.map +1 -1
  65. package/dist/event/events.js +1 -0
  66. package/dist/event/events.js.map +1 -1
  67. package/dist/model/CompositionModel.d.ts.map +1 -1
  68. package/dist/model/CompositionModel.js +11 -6
  69. package/dist/model/CompositionModel.js.map +1 -1
  70. package/dist/model/RcFrame.d.ts +2 -0
  71. package/dist/model/RcFrame.d.ts.map +1 -1
  72. package/dist/model/RcFrame.js +3 -0
  73. package/dist/model/RcFrame.js.map +1 -1
  74. package/dist/orchestrator/ExportScheduler.d.ts +35 -0
  75. package/dist/orchestrator/ExportScheduler.d.ts.map +1 -0
  76. package/dist/orchestrator/ExportScheduler.js +241 -0
  77. package/dist/orchestrator/ExportScheduler.js.map +1 -0
  78. package/dist/orchestrator/GlobalAudioSession.d.ts +21 -7
  79. package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
  80. package/dist/orchestrator/GlobalAudioSession.js +132 -140
  81. package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
  82. package/dist/orchestrator/OnDemandVideoSession.d.ts +73 -0
  83. package/dist/orchestrator/OnDemandVideoSession.d.ts.map +1 -0
  84. package/dist/orchestrator/OnDemandVideoSession.js +281 -0
  85. package/dist/orchestrator/OnDemandVideoSession.js.map +1 -0
  86. package/dist/orchestrator/Orchestrator.d.ts +22 -17
  87. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  88. package/dist/orchestrator/Orchestrator.js +231 -297
  89. package/dist/orchestrator/Orchestrator.js.map +1 -1
  90. package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
  91. package/dist/orchestrator/VideoClipSession.js +3 -15
  92. package/dist/orchestrator/VideoClipSession.js.map +1 -1
  93. package/dist/orchestrator/index.d.ts +0 -1
  94. package/dist/orchestrator/index.d.ts.map +1 -1
  95. package/dist/orchestrator/types.d.ts +4 -4
  96. package/dist/orchestrator/types.d.ts.map +1 -1
  97. package/dist/stages/compose/FilterProcessor.d.ts +1 -1
  98. package/dist/stages/compose/FilterProcessor.d.ts.map +1 -1
  99. package/dist/stages/compose/FilterProcessor.js +226 -0
  100. package/dist/stages/compose/FilterProcessor.js.map +1 -0
  101. package/dist/stages/compose/LayerRenderer.d.ts +1 -1
  102. package/dist/stages/compose/LayerRenderer.d.ts.map +1 -1
  103. package/dist/stages/compose/LayerRenderer.js +270 -0
  104. package/dist/stages/compose/LayerRenderer.js.map +1 -0
  105. package/dist/stages/compose/TransitionProcessor.d.ts +1 -1
  106. package/dist/stages/compose/TransitionProcessor.d.ts.map +1 -1
  107. package/dist/stages/compose/TransitionProcessor.js +189 -0
  108. package/dist/stages/compose/TransitionProcessor.js.map +1 -0
  109. package/dist/stages/compose/VideoComposer.d.ts +4 -2
  110. package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
  111. package/dist/stages/compose/VideoComposer.js +229 -0
  112. package/dist/stages/compose/VideoComposer.js.map +1 -0
  113. package/dist/stages/compose/text-renderers/animation-utils.js +76 -0
  114. package/dist/stages/compose/text-renderers/animation-utils.js.map +1 -0
  115. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts +2 -2
  116. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts.map +1 -1
  117. package/dist/stages/compose/text-renderers/basic-text-renderer.js +93 -0
  118. package/dist/stages/compose/text-renderers/basic-text-renderer.js.map +1 -0
  119. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts +1 -1
  120. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts.map +1 -1
  121. package/dist/stages/compose/text-renderers/character-ktv-renderer.js +132 -0
  122. package/dist/stages/compose/text-renderers/character-ktv-renderer.js.map +1 -0
  123. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts +1 -1
  124. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts.map +1 -1
  125. package/dist/stages/compose/text-renderers/word-by-word-renderer.js +128 -0
  126. package/dist/stages/compose/text-renderers/word-by-word-renderer.js.map +1 -0
  127. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts +1 -1
  128. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts.map +1 -1
  129. package/dist/stages/compose/text-renderers/word-fancy-renderer.js +135 -0
  130. package/dist/stages/compose/text-renderers/word-fancy-renderer.js.map +1 -0
  131. package/dist/stages/compose/text-utils/locale-detector.js +16 -0
  132. package/dist/stages/compose/text-utils/locale-detector.js.map +1 -0
  133. package/dist/stages/compose/text-utils/text-metrics.js +21 -0
  134. package/dist/stages/compose/text-utils/text-metrics.js.map +1 -0
  135. package/dist/stages/compose/text-utils/text-wrapper.js +225 -0
  136. package/dist/stages/compose/text-utils/text-wrapper.js.map +1 -0
  137. package/dist/stages/compose/types.d.ts +2 -1
  138. package/dist/stages/compose/types.d.ts.map +1 -1
  139. package/dist/stages/decode/BaseDecoder.js +0 -3
  140. package/dist/stages/decode/BaseDecoder.js.map +1 -1
  141. package/dist/stages/demux/MP4Demuxer.d.ts +5 -0
  142. package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
  143. package/dist/stages/demux/MP4Demuxer.js +281 -0
  144. package/dist/stages/demux/MP4Demuxer.js.map +1 -0
  145. package/dist/stages/demux/MP4IndexParser.d.ts +71 -0
  146. package/dist/stages/demux/MP4IndexParser.d.ts.map +1 -0
  147. package/dist/stages/demux/MP4IndexParser.js +416 -0
  148. package/dist/stages/demux/MP4IndexParser.js.map +1 -0
  149. package/dist/stages/demux/types.d.ts +48 -0
  150. package/dist/stages/demux/types.d.ts.map +1 -1
  151. package/dist/stages/load/ResourceLoader.d.ts +44 -2
  152. package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
  153. package/dist/stages/load/ResourceLoader.js +281 -37
  154. package/dist/stages/load/ResourceLoader.js.map +1 -1
  155. package/dist/stages/load/TaskManager.d.ts +6 -2
  156. package/dist/stages/load/TaskManager.d.ts.map +1 -1
  157. package/dist/stages/load/TaskManager.js +27 -4
  158. package/dist/stages/load/TaskManager.js.map +1 -1
  159. package/dist/stages/load/types.d.ts +7 -0
  160. package/dist/stages/load/types.d.ts.map +1 -1
  161. package/dist/stages/mux/MP4Muxer.d.ts +2 -2
  162. package/dist/stages/mux/MP4Muxer.d.ts.map +1 -1
  163. package/dist/stages/mux/MP4Muxer.js +24 -13
  164. package/dist/stages/mux/MP4Muxer.js.map +1 -1
  165. package/dist/stages/mux/MuxManager.d.ts +10 -21
  166. package/dist/stages/mux/MuxManager.d.ts.map +1 -1
  167. package/dist/stages/mux/MuxManager.js +21 -162
  168. package/dist/stages/mux/MuxManager.js.map +1 -1
  169. package/dist/stages/mux/index.d.ts +0 -1
  170. package/dist/stages/mux/index.d.ts.map +1 -1
  171. package/dist/utils/binary-search.d.ts +12 -4
  172. package/dist/utils/binary-search.d.ts.map +1 -1
  173. package/dist/utils/binary-search.js +52 -6
  174. package/dist/utils/binary-search.js.map +1 -1
  175. package/dist/workers/{BaseDecoder.BWYu1W0B.js → BaseDecoder.CTW-vr29.js} +1 -4
  176. package/dist/workers/BaseDecoder.CTW-vr29.js.map +1 -0
  177. package/dist/workers/{MP4Demuxer.lMOUMWFh.js → MP4Demuxer.BEa6PLJm.js} +9 -2
  178. package/dist/workers/{MP4Demuxer.lMOUMWFh.js.map → MP4Demuxer.BEa6PLJm.js.map} +1 -1
  179. package/dist/workers/stages/compose/{video-compose.worker.CIeEIJO7.js → video-compose.worker.DHQ8B105.js} +59 -31
  180. package/dist/workers/stages/compose/video-compose.worker.DHQ8B105.js.map +1 -0
  181. package/dist/workers/stages/decode/{audio-decode.worker.DnS17GD9.js → audio-decode.worker.CP8bXXa4.js} +2 -2
  182. package/dist/workers/stages/decode/{audio-decode.worker.DnS17GD9.js.map → audio-decode.worker.CP8bXXa4.js.map} +1 -1
  183. package/dist/workers/stages/decode/{video-decode.worker.BEYsjOXp.js → video-decode.worker.BIspTxgV.js} +2 -2
  184. package/dist/workers/stages/decode/{video-decode.worker.BEYsjOXp.js.map → video-decode.worker.BIspTxgV.js.map} +1 -1
  185. package/dist/workers/stages/demux/{audio-demux.worker.DcurGC8i.js → audio-demux.worker._VRQdLdv.js} +2 -2
  186. package/dist/workers/stages/demux/{audio-demux.worker.DcurGC8i.js.map → audio-demux.worker._VRQdLdv.js.map} +1 -1
  187. package/dist/workers/stages/demux/{video-demux.worker.B1_wntU4.js → video-demux.worker.CSkxGtmx.js} +3 -19
  188. package/dist/workers/stages/demux/video-demux.worker.CSkxGtmx.js.map +1 -0
  189. package/dist/workers/worker-manifest.json +5 -5
  190. package/package.json +1 -1
  191. package/dist/cache/l2/IndexedDBStore.js.map +0 -1
  192. package/dist/cache/l2/OPFSStore.js +0 -131
  193. package/dist/cache/l2/OPFSStore.js.map +0 -1
  194. package/dist/controllers/PreRenderService.d.ts +0 -59
  195. package/dist/controllers/PreRenderService.d.ts.map +0 -1
  196. package/dist/controllers/PreRenderService.js +0 -185
  197. package/dist/controllers/PreRenderService.js.map +0 -1
  198. package/dist/controllers/PreRenderTaskQueue.d.ts +0 -21
  199. package/dist/controllers/PreRenderTaskQueue.d.ts.map +0 -1
  200. package/dist/orchestrator/ClipSessionManager.d.ts +0 -70
  201. package/dist/orchestrator/ClipSessionManager.d.ts.map +0 -1
  202. package/dist/orchestrator/ClipSessionManager.js +0 -158
  203. package/dist/orchestrator/ClipSessionManager.js.map +0 -1
  204. package/dist/stages/decode/AudioChunkDecoder.js +0 -169
  205. package/dist/stages/decode/AudioChunkDecoder.js.map +0 -1
  206. package/dist/stages/mux/OPFSWriter.d.ts +0 -46
  207. package/dist/stages/mux/OPFSWriter.d.ts.map +0 -1
  208. package/dist/utils/BackpressureAdapter.d.ts +0 -26
  209. package/dist/utils/BackpressureAdapter.d.ts.map +0 -1
  210. package/dist/workers/BaseDecoder.BWYu1W0B.js.map +0 -1
  211. package/dist/workers/stages/compose/video-compose.worker.CIeEIJO7.js.map +0 -1
  212. package/dist/workers/stages/demux/video-demux.worker.B1_wntU4.js.map +0 -1
@@ -3,156 +3,215 @@ import { L2Cache } from "./l2/L2Cache.js";
3
3
  import { MeframeEvent } from "../event/events.js";
4
4
  import { AudioL1Cache } from "./l1/AudioL1Cache.js";
5
5
  import { WaiterReplacedError } from "../utils/errors.js";
6
+ import { OPFSManager } from "./storage/opfs/OPFSManager.js";
7
+ import { ResourceCache } from "./resource/ResourceCache.js";
8
+ import { MP4IndexCache } from "./resource/MP4IndexCache.js";
9
+ import { AudioSampleCache } from "./resource/AudioSampleCache.js";
10
+ import { ImageBitmapCache } from "./resource/ImageBitmapCache.js";
6
11
  class CacheManager {
7
12
  videoL1Cache;
8
13
  audioL1Cache;
9
14
  l2Cache;
15
+ resourceCache;
16
+ mp4IndexCache;
17
+ audioSampleCache;
18
+ imageBitmapCache;
10
19
  clipReadyWaiters = /* @__PURE__ */ new Map();
11
20
  eventBus;
12
21
  constructor(config, eventBus) {
13
22
  this.videoL1Cache = new VideoL1Cache(config.l1);
14
23
  this.l2Cache = new L2Cache(config.l2);
15
24
  this.audioL1Cache = new AudioL1Cache();
25
+ const opfsManager = new OPFSManager();
26
+ this.resourceCache = new ResourceCache(opfsManager, config.l2.projectId);
27
+ this.mp4IndexCache = new MP4IndexCache();
28
+ this.audioSampleCache = new AudioSampleCache();
29
+ this.imageBitmapCache = new ImageBitmapCache(100);
16
30
  this.eventBus = eventBus;
17
31
  }
18
32
  async init() {
19
- await this.l2Cache.init();
33
+ await Promise.all([this.l2Cache.init(), this.resourceCache.init()]);
20
34
  }
21
- async receiveComposedFrames(stream, params) {
22
- const reader = stream.getReader();
23
- const process = async () => {
24
- const { done, value } = await reader.read();
25
- if (done) {
26
- reader.releaseLock();
27
- return;
28
- }
29
- if (value) {
30
- const fps = params.fps > 0 ? params.fps : 30;
31
- const frameDuration = Math.round(1e6 / fps);
32
- const frame = value;
33
- const timestamp = frame.timestamp ?? 0;
34
- const rcFrame = this.videoL1Cache.addFrame(
35
- frame,
36
- params.clipId,
37
- frameDuration,
38
- params.trackId
39
- );
40
- const globalTimeUs = (params.clipStartUs ?? 0) + timestamp;
41
- this.checkAndNotifyClipReady(params.clipId, timestamp);
42
- if (globalTimeUs === 0) {
43
- this.eventBus?.emit(MeframeEvent.CacheCover, {
44
- timeUs: globalTimeUs,
45
- clipId: params.clipId,
46
- level: "L1",
47
- size: rcFrame.sizeEstimate ?? 0
48
- });
49
- }
50
- const info = { clipId: params.clipId, timeUs: timestamp };
51
- this.eventBus?.emit(MeframeEvent.ComposeFrameReady, {
52
- timeUs: globalTimeUs,
53
- frameNumber: Math.floor(globalTimeUs / frameDuration),
54
- renderTimeMs: 0,
55
- trackId: params.trackId,
56
- clipId: params.clipId
57
- });
58
- params.onFrame(info);
59
- }
60
- await process();
61
- };
62
- try {
63
- await process();
64
- } catch (error) {
65
- this.eventBus?.emit(MeframeEvent.ComposeFrameDropped, {
66
- timeUs: 0,
67
- reason: "compose_slow"
68
- });
69
- reader.releaseLock();
70
- console.error("[CacheManager] receiveComposedFrames error: ", error);
71
- }
35
+ /**
36
+ * Set window center for L1 cache management
37
+ * L1 cache uses center ±3s window (defined in VideoL1Cache)
38
+ */
39
+ setWindow(centerTimeUs) {
40
+ this.videoL1Cache.setWindow(centerTimeUs);
72
41
  }
73
- async receiveEncodedChunks(stream, clipId, track, options) {
74
- const reader = stream.getReader();
75
- const chunks = [];
76
- const batchSize = track === "video" ? 30 : 50;
77
- let decoderConfig = null;
78
- let metadata;
79
- const process = async () => {
80
- const { done, value } = await reader.read();
81
- const { chunk, metadata: chunkMetadata } = value ?? {};
82
- if (chunkMetadata) {
83
- metadata = chunkMetadata;
84
- if (!decoderConfig && chunkMetadata.decoderConfig) {
85
- decoderConfig = chunkMetadata.decoderConfig;
86
- }
87
- }
88
- if (done) {
89
- if (chunks.length > 0) {
90
- await this.l2Cache.put(clipId, chunks, track, {
91
- isComplete: true,
92
- metadata: decoderConfig
93
- });
94
- const firstChunk = chunks[0];
95
- if (firstChunk) {
96
- this.eventBus?.emit(MeframeEvent.CacheWrite, {
97
- clipId,
98
- timeUs: firstChunk.timestamp,
99
- level: "L2",
100
- size: chunks.reduce((sum, c) => sum + c.byteLength, 0)
101
- });
102
- }
103
- } else {
104
- await this.l2Cache.markComplete(clipId, track);
105
- }
106
- reader.releaseLock();
107
- if (options?.onComplete) {
108
- options.onComplete(metadata);
109
- }
110
- return;
111
- }
112
- if (chunk) {
113
- chunks.push(chunk);
114
- this.eventBus?.emit(MeframeEvent.EncodeChunkReady, {
115
- timeUs: chunk.timestamp,
116
- durationUs: chunk.duration ?? 0,
117
- track,
118
- size: chunk.byteLength
119
- });
120
- if (chunks.length >= batchSize) {
121
- const batchToWrite = chunks.splice(0);
122
- if (batchToWrite.length > 0) {
123
- await this.l2Cache.put(clipId, batchToWrite, track, {
124
- metadata: decoderConfig
125
- });
126
- this.eventBus?.emit(MeframeEvent.CacheWrite, {
127
- clipId,
128
- timeUs: chunk.timestamp,
129
- level: "L2",
130
- size: batchToWrite.reduce((sum, c) => sum + c.byteLength, 0)
131
- });
132
- }
133
- }
134
- }
135
- await process();
136
- };
137
- try {
138
- await process();
139
- } catch (error) {
140
- if (chunks.length > 0) {
141
- await this.l2Cache.put(clipId, chunks, track, {
142
- metadata: decoderConfig
143
- });
144
- }
145
- this.eventBus?.emit(MeframeEvent.EncodeChunkError, {
146
- timeUs: 0,
147
- track,
148
- error
149
- });
150
- reader.releaseLock();
151
- if (options?.onError) {
152
- options.onError(error);
153
- }
154
- }
42
+ /**
43
+ * Check if resource exists in OPFS cache
44
+ */
45
+ async hasResourceInCache(resourceId) {
46
+ return await this.resourceCache.hasResource(resourceId);
47
+ }
48
+ /**
49
+ * Get MP4 index for a resource
50
+ */
51
+ getMP4Index(resourceId) {
52
+ return this.mp4IndexCache.get(resourceId);
155
53
  }
54
+ // async receiveComposedFrames(
55
+ // stream: ReadableStream<VideoFrame>,
56
+ // params: {
57
+ // clipId: string;
58
+ // trackId: string;
59
+ // fps: number;
60
+ // clipStartUs?: TimeUs;
61
+ // onFrame: (info: { clipId: string; timeUs: TimeUs }) => void;
62
+ // }
63
+ // ): Promise<void> {
64
+ // const reader = stream.getReader();
65
+ // const process = async (): Promise<void> => {
66
+ // const { done, value } = await reader.read();
67
+ // if (done) {
68
+ // reader.releaseLock();
69
+ // return;
70
+ // }
71
+ // if (value) {
72
+ // const fps = params.fps > 0 ? params.fps : 30;
73
+ // const frameDuration = Math.round(1_000_000 / fps);
74
+ // const frame = value;
75
+ // const timestamp = frame.timestamp ?? 0;
76
+ // const rcFrame = this.videoL1Cache.addFrame(
77
+ // frame,
78
+ // params.clipId,
79
+ // frameDuration,
80
+ // params.trackId
81
+ // );
82
+ // // Calculate global time for event emission
83
+ // const globalTimeUs = (params.clipStartUs ?? 0) + timestamp;
84
+ // this.checkAndNotifyClipReady(params.clipId, timestamp);
85
+ // // Emit cover event only for the first frame of the composition (global time = 0)
86
+ // if (globalTimeUs === 0) {
87
+ // this.eventBus?.emit(MeframeEvent.CacheCover, {
88
+ // timeUs: globalTimeUs,
89
+ // clipId: params.clipId,
90
+ // level: 'L1',
91
+ // size: rcFrame.sizeEstimate ?? 0,
92
+ // });
93
+ // }
94
+ // const info = { clipId: params.clipId, timeUs: timestamp };
95
+ // this.eventBus?.emit(MeframeEvent.ComposeFrameReady, {
96
+ // timeUs: globalTimeUs,
97
+ // frameNumber: Math.floor(globalTimeUs / frameDuration),
98
+ // renderTimeMs: 0,
99
+ // trackId: params.trackId,
100
+ // clipId: params.clipId,
101
+ // });
102
+ // params.onFrame(info);
103
+ // }
104
+ // await process();
105
+ // };
106
+ // try {
107
+ // await process();
108
+ // } catch (error) {
109
+ // this.eventBus?.emit(MeframeEvent.ComposeFrameDropped, {
110
+ // timeUs: 0,
111
+ // reason: 'compose_slow',
112
+ // });
113
+ // reader.releaseLock();
114
+ // console.error('[CacheManager] receiveComposedFrames error: ', error);
115
+ // }
116
+ // }
117
+ // async receiveEncodedChunks(
118
+ // stream: ReadableStream<{ chunk: EncodedVideoChunk; metadata: EncodedVideoChunkMetadata }>,
119
+ // clipId: string,
120
+ // track: 'video' | 'audio',
121
+ // options?: {
122
+ // onComplete?: (metadata: EncodedVideoChunkMetadata) => void;
123
+ // onError?: (error: Error) => void;
124
+ // }
125
+ // ): Promise<void> {
126
+ // const reader = stream.getReader();
127
+ // const chunks: Array<EncodedVideoChunk | EncodedAudioChunk> = [];
128
+ // const batchSize = track === 'video' ? 30 : 50;
129
+ // let decoderConfig: any = null;
130
+ // let metadata: EncodedVideoChunkMetadata | EncodedAudioChunkMetadata | undefined;
131
+ // const process = async (): Promise<void> => {
132
+ // const { done, value } = await reader.read();
133
+ // const { chunk, metadata: chunkMetadata } = value ?? {};
134
+ // if (chunkMetadata) {
135
+ // metadata = chunkMetadata;
136
+ // // Extract decoderConfig from first chunk's metadata
137
+ // if (!decoderConfig && chunkMetadata.decoderConfig) {
138
+ // decoderConfig = chunkMetadata.decoderConfig;
139
+ // }
140
+ // }
141
+ // if (done) {
142
+ // // Flush remaining chunks on stream end
143
+ // if (chunks.length > 0) {
144
+ // await this.l2Cache.put(clipId, chunks, track, {
145
+ // isComplete: true,
146
+ // metadata: decoderConfig,
147
+ // });
148
+ // const firstChunk = chunks[0];
149
+ // if (firstChunk) {
150
+ // this.eventBus?.emit(MeframeEvent.CacheWrite, {
151
+ // clipId,
152
+ // timeUs: firstChunk.timestamp,
153
+ // level: 'L2',
154
+ // size: chunks.reduce((sum, c) => sum + c.byteLength, 0),
155
+ // });
156
+ // }
157
+ // } else {
158
+ // // Mark as complete even if no chunks in final batch
159
+ // await this.l2Cache.markComplete(clipId, track);
160
+ // }
161
+ // reader.releaseLock();
162
+ // // Notify completion
163
+ // if (options?.onComplete) {
164
+ // options.onComplete(metadata!);
165
+ // }
166
+ // return;
167
+ // }
168
+ // if (chunk) {
169
+ // chunks.push(chunk);
170
+ // this.eventBus?.emit(MeframeEvent.EncodeChunkReady, {
171
+ // timeUs: chunk.timestamp,
172
+ // durationUs: chunk.duration ?? 0,
173
+ // track,
174
+ // size: chunk.byteLength,
175
+ // });
176
+ // // Batch write to L2 when buffer is full
177
+ // if (chunks.length >= batchSize) {
178
+ // const batchToWrite = chunks.splice(0);
179
+ // if (batchToWrite.length > 0) {
180
+ // await this.l2Cache.put(clipId, batchToWrite, track, {
181
+ // metadata: decoderConfig,
182
+ // });
183
+ // this.eventBus?.emit(MeframeEvent.CacheWrite, {
184
+ // clipId,
185
+ // timeUs: chunk.timestamp,
186
+ // level: 'L2',
187
+ // size: batchToWrite.reduce((sum, c) => sum + c.byteLength, 0),
188
+ // });
189
+ // }
190
+ // }
191
+ // }
192
+ // await process();
193
+ // };
194
+ // try {
195
+ // await process();
196
+ // } catch (error) {
197
+ // // Flush any accumulated chunks before throwing
198
+ // if (chunks.length > 0) {
199
+ // await this.l2Cache.put(clipId, chunks, track, {
200
+ // metadata: decoderConfig,
201
+ // });
202
+ // }
203
+ // this.eventBus?.emit(MeframeEvent.EncodeChunkError, {
204
+ // timeUs: 0,
205
+ // track,
206
+ // error: error as Error,
207
+ // });
208
+ // reader.releaseLock();
209
+ // // Notify error
210
+ // if (options?.onError) {
211
+ // options.onError(error as Error);
212
+ // }
213
+ // }
214
+ // }
156
215
  acceptMixedAudio(stream, metadata) {
157
216
  this.audioL1Cache.attachStream(stream, metadata);
158
217
  }
@@ -174,6 +233,36 @@ class CacheManager {
174
233
  clearClipAudioData(clipId) {
175
234
  this.audioL1Cache.clearClipPCM(clipId);
176
235
  }
236
+ /**
237
+ * Add a frame to L1 cache
238
+ * Handles event notifications (CacheCover, ComposeFrameReady)
239
+ * @param frame - VideoFrame to add
240
+ * @param clipId - Clip identifier
241
+ * @param frameDuration - Frame duration in microseconds
242
+ * @param trackId - Track identifier
243
+ * @param globalTimeUs - Global timestamp for event emission and window management
244
+ */
245
+ addFrame(frame, clipId, frameDuration, trackId, globalTimeUs) {
246
+ const rcFrame = this.videoL1Cache.addFrame(frame, clipId, frameDuration, trackId, globalTimeUs);
247
+ const relativeTimeUs = frame.timestamp ?? 0;
248
+ this.checkAndNotifyClipReady(clipId, relativeTimeUs);
249
+ if (globalTimeUs === 0) {
250
+ this.eventBus?.emit(MeframeEvent.CacheCover, {
251
+ timeUs: globalTimeUs,
252
+ clipId,
253
+ level: "L1",
254
+ size: rcFrame.sizeEstimate ?? 0
255
+ });
256
+ }
257
+ this.eventBus?.emit(MeframeEvent.ComposeFrameReady, {
258
+ timeUs: globalTimeUs,
259
+ frameNumber: Math.floor(globalTimeUs / frameDuration),
260
+ renderTimeMs: 0,
261
+ trackId,
262
+ clipId
263
+ });
264
+ return rcFrame;
265
+ }
177
266
  /**
178
267
  * Get frame from L1 cache
179
268
  * @param timeUs - Clip-relative timestamp (0-based)
@@ -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 { L2Cache } from './l2/L2Cache';\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';\n\ninterface CacheManagerConfig {\n l1: {\n maxMemoryMB: number;\n maxGOPs?: number;\n gopIntervalUs?: number;\n };\n l2: {\n maxSizeMB: number;\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 * Simplified CacheManager for 2-Clip strategy\n *\n * Core features:\n * - L1 (VRAM) for composed VideoFrames\n * - L2 (IndexedDB/OPFS) for encoded chunks\n * - Clip-level cache management\n */\nexport class CacheManager {\n private readonly videoL1Cache: VideoL1Cache;\n private readonly audioL1Cache: AudioL1Cache;\n readonly l2Cache: L2Cache;\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.l2Cache = new L2Cache(config.l2);\n this.audioL1Cache = new AudioL1Cache();\n this.eventBus = eventBus;\n }\n\n async init(): Promise<void> {\n await this.l2Cache.init();\n }\n\n async receiveComposedFrames(\n stream: ReadableStream<VideoFrame>,\n params: {\n clipId: string;\n trackId: string;\n fps: number;\n clipStartUs?: TimeUs;\n onFrame: (info: { clipId: string; timeUs: TimeUs }) => void;\n }\n ): Promise<void> {\n const reader = stream.getReader();\n const process = async (): Promise<void> => {\n const { done, value } = await reader.read();\n if (done) {\n reader.releaseLock();\n return;\n }\n if (value) {\n const fps = params.fps > 0 ? params.fps : 30;\n const frameDuration = Math.round(1_000_000 / fps);\n\n const frame = value;\n const timestamp = frame.timestamp ?? 0;\n\n const rcFrame = this.videoL1Cache.addFrame(\n frame,\n params.clipId,\n frameDuration,\n params.trackId\n );\n\n // Calculate global time for event emission\n const globalTimeUs = (params.clipStartUs ?? 0) + timestamp;\n\n this.checkAndNotifyClipReady(params.clipId, timestamp);\n // Emit cover event only for the first frame of the composition (global time = 0)\n if (globalTimeUs === 0) {\n this.eventBus?.emit(MeframeEvent.CacheCover, {\n timeUs: globalTimeUs,\n clipId: params.clipId,\n level: 'L1',\n size: rcFrame.sizeEstimate ?? 0,\n });\n }\n\n const info = { clipId: params.clipId, timeUs: timestamp };\n this.eventBus?.emit(MeframeEvent.ComposeFrameReady, {\n timeUs: globalTimeUs,\n frameNumber: Math.floor(globalTimeUs / frameDuration),\n renderTimeMs: 0,\n trackId: params.trackId,\n clipId: params.clipId,\n });\n\n params.onFrame(info);\n }\n\n await process();\n };\n\n try {\n await process();\n } catch (error) {\n this.eventBus?.emit(MeframeEvent.ComposeFrameDropped, {\n timeUs: 0,\n reason: 'compose_slow',\n });\n reader.releaseLock();\n console.error('[CacheManager] receiveComposedFrames error: ', error);\n }\n }\n\n async receiveEncodedChunks(\n stream: ReadableStream<{ chunk: EncodedVideoChunk; metadata: EncodedVideoChunkMetadata }>,\n clipId: string,\n track: 'video' | 'audio',\n options?: {\n onComplete?: (metadata: EncodedVideoChunkMetadata) => void;\n onError?: (error: Error) => void;\n }\n ): Promise<void> {\n const reader = stream.getReader();\n const chunks: Array<EncodedVideoChunk | EncodedAudioChunk> = [];\n const batchSize = track === 'video' ? 30 : 50;\n let decoderConfig: any = null;\n let metadata: EncodedVideoChunkMetadata | EncodedAudioChunkMetadata | undefined;\n const process = async (): Promise<void> => {\n const { done, value } = await reader.read();\n const { chunk, metadata: chunkMetadata } = value ?? {};\n if (chunkMetadata) {\n metadata = chunkMetadata;\n // Extract decoderConfig from first chunk's metadata\n if (!decoderConfig && chunkMetadata.decoderConfig) {\n decoderConfig = chunkMetadata.decoderConfig;\n }\n }\n if (done) {\n // Flush remaining chunks on stream end\n if (chunks.length > 0) {\n await this.l2Cache.put(clipId, chunks, track, {\n isComplete: true,\n metadata: decoderConfig,\n });\n const firstChunk = chunks[0];\n if (firstChunk) {\n this.eventBus?.emit(MeframeEvent.CacheWrite, {\n clipId,\n timeUs: firstChunk.timestamp,\n level: 'L2',\n size: chunks.reduce((sum, c) => sum + c.byteLength, 0),\n });\n }\n } else {\n // Mark as complete even if no chunks in final batch\n await this.l2Cache.markComplete(clipId, track);\n }\n reader.releaseLock();\n\n // Notify completion\n if (options?.onComplete) {\n options.onComplete(metadata!);\n }\n return;\n }\n\n if (chunk) {\n chunks.push(chunk);\n\n this.eventBus?.emit(MeframeEvent.EncodeChunkReady, {\n timeUs: chunk.timestamp,\n durationUs: chunk.duration ?? 0,\n track,\n size: chunk.byteLength,\n });\n\n // Batch write to L2 when buffer is full\n if (chunks.length >= batchSize) {\n const batchToWrite = chunks.splice(0);\n if (batchToWrite.length > 0) {\n await this.l2Cache.put(clipId, batchToWrite, track, {\n metadata: decoderConfig,\n });\n this.eventBus?.emit(MeframeEvent.CacheWrite, {\n clipId,\n timeUs: chunk.timestamp,\n level: 'L2',\n size: batchToWrite.reduce((sum, c) => sum + c.byteLength, 0),\n });\n }\n }\n }\n\n await process();\n };\n\n try {\n await process();\n } catch (error) {\n // Flush any accumulated chunks before throwing\n if (chunks.length > 0) {\n await this.l2Cache.put(clipId, chunks, track, {\n metadata: decoderConfig,\n });\n }\n this.eventBus?.emit(MeframeEvent.EncodeChunkError, {\n timeUs: 0,\n track,\n error: error as Error,\n });\n reader.releaseLock();\n\n // Notify error\n if (options?.onError) {\n options.onError(error as Error);\n }\n }\n }\n\n acceptMixedAudio(\n stream: ReadableStream<AudioData>,\n metadata: { sampleRate: number; numberOfChannels: number }\n ): void {\n this.audioL1Cache.attachStream(stream, metadata);\n }\n\n putClipAudioData(clipId: string, audioData: AudioData, clipDurationUs: TimeUs): void {\n this.audioL1Cache.putClipAudioData(clipId, audioData, clipDurationUs);\n }\n\n getClipPCM(clipId: string, startUs: TimeUs, endUs: TimeUs): Float32Array[] | null {\n return this.audioL1Cache.getPCM(clipId, startUs, endUs);\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 resetAudioCache(): void {\n this.audioL1Cache.clear();\n }\n\n clearClipAudioData(clipId: string): void {\n this.audioL1Cache.clearClipPCM(clipId);\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 /**\n * Wait for frame to be available in cache\n * @param timeUs - Clip-relative timestamp (0-based)\n * @param clipId - Clip identifier\n * @param options - Wait options (timeout, tolerance, etc.)\n */\n // waitForFrame(\n // timeUs: TimeUs,\n // clipId: string,\n // options: WaitForFrameOptions = {}\n // ): Promise<WaitForFrameResult> {\n // const existing = this.videoL1Cache.get(timeUs, clipId);\n // if (existing) {\n // return Promise.resolve({ frame: existing, source: 'l1', timestampUs: timeUs, clipId });\n // }\n\n // const requestKey = this.makeRequestKey(clipId, timeUs);\n // const existingPromise = this.pendingFramePromises.get(requestKey);\n // if (existingPromise) {\n // return existingPromise;\n // }\n\n // const promise = new Promise<WaitForFrameResult>((resolve, reject) => {\n // const toleranceUs = Math.max(\n // options.toleranceUs ?? DEFAULT_WAIT_TOLERANCE_US,\n // DEFAULT_WAIT_TOLERANCE_US\n // );\n\n // const waiter: FrameWaiter = {\n // requestKey,\n // clipId,\n // targetTimeUs: timeUs,\n // resolve,\n // reject,\n // toleranceUs,\n // };\n\n // let waiters = this.frameWaiters.get(clipId);\n // if (!waiters) {\n // waiters = new Set();\n // this.frameWaiters.set(clipId, waiters);\n // }\n // waiters.add(waiter);\n\n // const signal = options.signal;\n // if (signal) {\n // const onAbort = (): void => {\n // this.removeWaiter(waiter);\n // this.cleanupWaiter(waiter);\n // reject(new DOMException('Render aborted', 'AbortError'));\n // };\n\n // if (signal.aborted) {\n // onAbort();\n // return;\n // }\n\n // signal.addEventListener('abort', onAbort, { once: true });\n // waiter.abortCleanup = () => {\n // signal.removeEventListener('abort', onAbort);\n // };\n // }\n\n // if (options.timeoutMs && options.timeoutMs > 0) {\n // waiter.timeoutId = setTimeout(() => {\n // this.removeWaiter(waiter);\n // this.cleanupWaiter(waiter);\n // reject(new Error('waitForFrame timeout'));\n // }, options.timeoutMs);\n // }\n // });\n\n // const trackedPromise = promise.finally(() => {\n // this.pendingFramePromises.delete(requestKey);\n // });\n\n // this.pendingFramePromises.set(requestKey, trackedPromise);\n // return trackedPromise;\n // }\n\n async invalidateClip(clipId: string): Promise<void> {\n this.videoL1Cache.invalidateClip(clipId);\n await this.l2Cache.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 this.audioL1Cache.clear();\n }\n\n async clearL2Cache(): Promise<void> {\n await this.l2Cache.clear();\n }\n\n async clear(): Promise<void> {\n this.clearL1Cache();\n await this.clearL2Cache();\n }\n\n getMetadata() {\n return {\n l1: this.videoL1Cache.getMetadata(),\n l2: this.l2Cache.getMetadata(),\n };\n }\n\n /**\n * Create read stream from L2 cache for export\n */\n async createL2ReadStream(\n clipId: string,\n track: 'video' | 'audio'\n ): Promise<ReadableStream<EncodedVideoChunk | EncodedAudioChunk> | null> {\n return this.l2Cache.createReadStream(clipId, track);\n }\n\n /**\n * Check if clip is fully cached in L2\n * Returns true only if the clip is marked as complete\n */\n async hasClipInL2(clipId: string, track: 'video' | 'audio'): Promise<boolean> {\n const result = await this.l2Cache.hasCompleteClip(clipId, track);\n return result;\n }\n\n /**\n * Mark clip as complete in L2 cache\n */\n async markClipComplete(clipId: string, track: 'video' | 'audio'): Promise<void> {\n await this.l2Cache.markComplete(clipId, track);\n }\n\n /**\n * Get chunk metadata (decoderConfig) from L2 cache\n */\n async getL2Metadata(clipId: string, track: 'video' | 'audio'): Promise<any | null> {\n return this.l2Cache.getClipMetadata(clipId, track);\n }\n\n /**\n * List all cached projects\n */\n async listProjects(): Promise<\n Array<{ projectId: string; totalBytes: number; clipCount: number; lastAccess: number }>\n > {\n return this.l2Cache.listProjects();\n }\n\n /**\n * Clear all cache data for a specific project\n */\n async clearProject(projectId: string): Promise<void> {\n return this.l2Cache.clearProject(projectId);\n }\n\n /**\n * Get cache size for a specific project (in bytes)\n */\n async getProjectSize(projectId: string): Promise<number> {\n return this.l2Cache.getProjectSize(projectId);\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 private 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 // private cleanupWaiter(waiter: FrameWaiter): void {\n // if (waiter.timeoutId) {\n // clearTimeout(waiter.timeoutId);\n // waiter.timeoutId = undefined;\n // }\n\n // if (waiter.abortCleanup) {\n // waiter.abortCleanup();\n // waiter.abortCleanup = undefined;\n // }\n // }\n\n // private removeWaiter(waiter: FrameWaiter): void {\n // const waiters = this.frameWaiters.get(waiter.clipId);\n // if (!waiters) return;\n\n // waiters.delete(waiter);\n // if (waiters.size === 0) {\n // this.frameWaiters.delete(waiter.clipId);\n // }\n // }\n\n // private matchesTimestamp(\n // targetTimeUs: TimeUs,\n // actualTimeUs: TimeUs,\n // frameDurationUs: TimeUs,\n // toleranceUs: TimeUs\n // ): boolean {\n // if (targetTimeUs === actualTimeUs) return true;\n\n // const delta = Math.abs(targetTimeUs - actualTimeUs);\n // if (delta <= toleranceUs) {\n // return true;\n // }\n\n // if (actualTimeUs >= targetTimeUs && actualTimeUs < targetTimeUs + frameDurationUs) {\n // return true;\n // }\n\n // return false;\n // }\n\n // private makeRequestKey(clipId: string, timeUs: TimeUs): string {\n // return `${clipId}:${timeUs}`;\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 { L2Cache } from './l2/L2Cache';\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 { ImageBitmapCache } from './resource/ImageBitmapCache';\nimport type { MP4Index } from '../stages/demux/types';\n\ninterface CacheManagerConfig {\n l1: {\n maxMemoryMB: number;\n maxGOPs?: number;\n gopIntervalUs?: number;\n };\n l2: {\n maxSizeMB: number;\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 * Simplified CacheManager for 2-Clip strategy\n *\n * Core features:\n * - L1 (VRAM) for composed VideoFrames\n * - L2 (IndexedDB/OPFS) for encoded chunks\n * - Clip-level cache management\n */\nexport class CacheManager {\n readonly videoL1Cache: VideoL1Cache;\n private readonly audioL1Cache: AudioL1Cache;\n readonly l2Cache: L2Cache;\n readonly resourceCache: ResourceCache;\n readonly mp4IndexCache: MP4IndexCache;\n readonly audioSampleCache: AudioSampleCache;\n readonly imageBitmapCache: ImageBitmapCache;\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.l2Cache = new L2Cache(config.l2);\n this.audioL1Cache = new AudioL1Cache();\n\n // Initialize resource cache\n const opfsManager = new OPFSManager();\n this.resourceCache = new ResourceCache(opfsManager, config.l2.projectId);\n this.mp4IndexCache = new MP4IndexCache();\n this.audioSampleCache = new AudioSampleCache();\n this.imageBitmapCache = new ImageBitmapCache(100); // 100MB default\n\n this.eventBus = eventBus;\n }\n\n async init(): Promise<void> {\n await Promise.all([this.l2Cache.init(), this.resourceCache.init()]);\n }\n\n /**\n * Set window center for L1 cache management\n * L1 cache uses center ±3s window (defined in VideoL1Cache)\n */\n setWindow(centerTimeUs: TimeUs): void {\n this.videoL1Cache.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 * Get MP4 index for a resource\n */\n getMP4Index(resourceId: string): MP4Index | null {\n return this.mp4IndexCache.get(resourceId);\n }\n\n // async receiveComposedFrames(\n // stream: ReadableStream<VideoFrame>,\n // params: {\n // clipId: string;\n // trackId: string;\n // fps: number;\n // clipStartUs?: TimeUs;\n // onFrame: (info: { clipId: string; timeUs: TimeUs }) => void;\n // }\n // ): Promise<void> {\n // const reader = stream.getReader();\n // const process = async (): Promise<void> => {\n // const { done, value } = await reader.read();\n // if (done) {\n // reader.releaseLock();\n // return;\n // }\n // if (value) {\n // const fps = params.fps > 0 ? params.fps : 30;\n // const frameDuration = Math.round(1_000_000 / fps);\n\n // const frame = value;\n // const timestamp = frame.timestamp ?? 0;\n\n // const rcFrame = this.videoL1Cache.addFrame(\n // frame,\n // params.clipId,\n // frameDuration,\n // params.trackId\n // );\n\n // // Calculate global time for event emission\n // const globalTimeUs = (params.clipStartUs ?? 0) + timestamp;\n\n // this.checkAndNotifyClipReady(params.clipId, timestamp);\n // // Emit cover event only for the first frame of the composition (global time = 0)\n // if (globalTimeUs === 0) {\n // this.eventBus?.emit(MeframeEvent.CacheCover, {\n // timeUs: globalTimeUs,\n // clipId: params.clipId,\n // level: 'L1',\n // size: rcFrame.sizeEstimate ?? 0,\n // });\n // }\n\n // const info = { clipId: params.clipId, timeUs: timestamp };\n // this.eventBus?.emit(MeframeEvent.ComposeFrameReady, {\n // timeUs: globalTimeUs,\n // frameNumber: Math.floor(globalTimeUs / frameDuration),\n // renderTimeMs: 0,\n // trackId: params.trackId,\n // clipId: params.clipId,\n // });\n\n // params.onFrame(info);\n // }\n\n // await process();\n // };\n\n // try {\n // await process();\n // } catch (error) {\n // this.eventBus?.emit(MeframeEvent.ComposeFrameDropped, {\n // timeUs: 0,\n // reason: 'compose_slow',\n // });\n // reader.releaseLock();\n // console.error('[CacheManager] receiveComposedFrames error: ', error);\n // }\n // }\n\n // async receiveEncodedChunks(\n // stream: ReadableStream<{ chunk: EncodedVideoChunk; metadata: EncodedVideoChunkMetadata }>,\n // clipId: string,\n // track: 'video' | 'audio',\n // options?: {\n // onComplete?: (metadata: EncodedVideoChunkMetadata) => void;\n // onError?: (error: Error) => void;\n // }\n // ): Promise<void> {\n // const reader = stream.getReader();\n // const chunks: Array<EncodedVideoChunk | EncodedAudioChunk> = [];\n // const batchSize = track === 'video' ? 30 : 50;\n // let decoderConfig: any = null;\n // let metadata: EncodedVideoChunkMetadata | EncodedAudioChunkMetadata | undefined;\n // const process = async (): Promise<void> => {\n // const { done, value } = await reader.read();\n // const { chunk, metadata: chunkMetadata } = value ?? {};\n // if (chunkMetadata) {\n // metadata = chunkMetadata;\n // // Extract decoderConfig from first chunk's metadata\n // if (!decoderConfig && chunkMetadata.decoderConfig) {\n // decoderConfig = chunkMetadata.decoderConfig;\n // }\n // }\n // if (done) {\n // // Flush remaining chunks on stream end\n // if (chunks.length > 0) {\n // await this.l2Cache.put(clipId, chunks, track, {\n // isComplete: true,\n // metadata: decoderConfig,\n // });\n // const firstChunk = chunks[0];\n // if (firstChunk) {\n // this.eventBus?.emit(MeframeEvent.CacheWrite, {\n // clipId,\n // timeUs: firstChunk.timestamp,\n // level: 'L2',\n // size: chunks.reduce((sum, c) => sum + c.byteLength, 0),\n // });\n // }\n // } else {\n // // Mark as complete even if no chunks in final batch\n // await this.l2Cache.markComplete(clipId, track);\n // }\n // reader.releaseLock();\n\n // // Notify completion\n // if (options?.onComplete) {\n // options.onComplete(metadata!);\n // }\n // return;\n // }\n\n // if (chunk) {\n // chunks.push(chunk);\n\n // this.eventBus?.emit(MeframeEvent.EncodeChunkReady, {\n // timeUs: chunk.timestamp,\n // durationUs: chunk.duration ?? 0,\n // track,\n // size: chunk.byteLength,\n // });\n\n // // Batch write to L2 when buffer is full\n // if (chunks.length >= batchSize) {\n // const batchToWrite = chunks.splice(0);\n // if (batchToWrite.length > 0) {\n // await this.l2Cache.put(clipId, batchToWrite, track, {\n // metadata: decoderConfig,\n // });\n // this.eventBus?.emit(MeframeEvent.CacheWrite, {\n // clipId,\n // timeUs: chunk.timestamp,\n // level: 'L2',\n // size: batchToWrite.reduce((sum, c) => sum + c.byteLength, 0),\n // });\n // }\n // }\n // }\n\n // await process();\n // };\n\n // try {\n // await process();\n // } catch (error) {\n // // Flush any accumulated chunks before throwing\n // if (chunks.length > 0) {\n // await this.l2Cache.put(clipId, chunks, track, {\n // metadata: decoderConfig,\n // });\n // }\n // this.eventBus?.emit(MeframeEvent.EncodeChunkError, {\n // timeUs: 0,\n // track,\n // error: error as Error,\n // });\n // reader.releaseLock();\n\n // // Notify error\n // if (options?.onError) {\n // options.onError(error as Error);\n // }\n // }\n // }\n\n acceptMixedAudio(\n stream: ReadableStream<AudioData>,\n metadata: { sampleRate: number; numberOfChannels: number }\n ): void {\n this.audioL1Cache.attachStream(stream, metadata);\n }\n\n putClipAudioData(clipId: string, audioData: AudioData, clipDurationUs: TimeUs): void {\n this.audioL1Cache.putClipAudioData(clipId, audioData, clipDurationUs);\n }\n\n getClipPCM(clipId: string, startUs: TimeUs, endUs: TimeUs): Float32Array[] | null {\n return this.audioL1Cache.getPCM(clipId, startUs, endUs);\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 resetAudioCache(): 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 /**\n * Wait for frame to be available in cache\n * @param timeUs - Clip-relative timestamp (0-based)\n * @param clipId - Clip identifier\n * @param options - Wait options (timeout, tolerance, etc.)\n */\n // waitForFrame(\n // timeUs: TimeUs,\n // clipId: string,\n // options: WaitForFrameOptions = {}\n // ): Promise<WaitForFrameResult> {\n // const existing = this.videoL1Cache.get(timeUs, clipId);\n // if (existing) {\n // return Promise.resolve({ frame: existing, source: 'l1', timestampUs: timeUs, clipId });\n // }\n\n // const requestKey = this.makeRequestKey(clipId, timeUs);\n // const existingPromise = this.pendingFramePromises.get(requestKey);\n // if (existingPromise) {\n // return existingPromise;\n // }\n\n // const promise = new Promise<WaitForFrameResult>((resolve, reject) => {\n // const toleranceUs = Math.max(\n // options.toleranceUs ?? DEFAULT_WAIT_TOLERANCE_US,\n // DEFAULT_WAIT_TOLERANCE_US\n // );\n\n // const waiter: FrameWaiter = {\n // requestKey,\n // clipId,\n // targetTimeUs: timeUs,\n // resolve,\n // reject,\n // toleranceUs,\n // };\n\n // let waiters = this.frameWaiters.get(clipId);\n // if (!waiters) {\n // waiters = new Set();\n // this.frameWaiters.set(clipId, waiters);\n // }\n // waiters.add(waiter);\n\n // const signal = options.signal;\n // if (signal) {\n // const onAbort = (): void => {\n // this.removeWaiter(waiter);\n // this.cleanupWaiter(waiter);\n // reject(new DOMException('Render aborted', 'AbortError'));\n // };\n\n // if (signal.aborted) {\n // onAbort();\n // return;\n // }\n\n // signal.addEventListener('abort', onAbort, { once: true });\n // waiter.abortCleanup = () => {\n // signal.removeEventListener('abort', onAbort);\n // };\n // }\n\n // if (options.timeoutMs && options.timeoutMs > 0) {\n // waiter.timeoutId = setTimeout(() => {\n // this.removeWaiter(waiter);\n // this.cleanupWaiter(waiter);\n // reject(new Error('waitForFrame timeout'));\n // }, options.timeoutMs);\n // }\n // });\n\n // const trackedPromise = promise.finally(() => {\n // this.pendingFramePromises.delete(requestKey);\n // });\n\n // this.pendingFramePromises.set(requestKey, trackedPromise);\n // return trackedPromise;\n // }\n\n async invalidateClip(clipId: string): Promise<void> {\n this.videoL1Cache.invalidateClip(clipId);\n await this.l2Cache.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 this.audioL1Cache.clear();\n }\n\n async clearL2Cache(): Promise<void> {\n await this.l2Cache.clear();\n }\n\n async clear(): Promise<void> {\n this.clearL1Cache();\n await this.clearL2Cache();\n }\n\n getMetadata() {\n return {\n l1: this.videoL1Cache.getMetadata(),\n l2: this.l2Cache.getMetadata(),\n };\n }\n\n /**\n * Create read stream from L2 cache for export\n */\n async createL2ReadStream(\n clipId: string,\n track: 'video' | 'audio'\n ): Promise<ReadableStream<EncodedVideoChunk | EncodedAudioChunk> | null> {\n return this.l2Cache.createReadStream(clipId, track);\n }\n\n /**\n * Check if clip is fully cached in L2\n * Returns true only if the clip is marked as complete\n */\n async hasClipInL2(clipId: string, track: 'video' | 'audio'): Promise<boolean> {\n const result = await this.l2Cache.hasCompleteClip(clipId, track);\n return result;\n }\n\n /**\n * Mark clip as complete in L2 cache\n */\n async markClipComplete(clipId: string, track: 'video' | 'audio'): Promise<void> {\n await this.l2Cache.markComplete(clipId, track);\n }\n\n /**\n * Get chunk metadata (decoderConfig) from L2 cache\n */\n async getL2Metadata(clipId: string, track: 'video' | 'audio'): Promise<any | null> {\n return this.l2Cache.getClipMetadata(clipId, track);\n }\n\n /**\n * List all cached projects\n */\n async listProjects(): Promise<\n Array<{ projectId: string; totalBytes: number; clipCount: number; lastAccess: number }>\n > {\n return this.l2Cache.listProjects();\n }\n\n /**\n * Clear all cache data for a specific project\n */\n async clearProject(projectId: string): Promise<void> {\n return this.l2Cache.clearProject(projectId);\n }\n\n /**\n * Get cache size for a specific project (in bytes)\n */\n async getProjectSize(projectId: string): Promise<number> {\n return this.l2Cache.getProjectSize(projectId);\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 // private cleanupWaiter(waiter: FrameWaiter): void {\n // if (waiter.timeoutId) {\n // clearTimeout(waiter.timeoutId);\n // waiter.timeoutId = undefined;\n // }\n\n // if (waiter.abortCleanup) {\n // waiter.abortCleanup();\n // waiter.abortCleanup = undefined;\n // }\n // }\n\n // private removeWaiter(waiter: FrameWaiter): void {\n // const waiters = this.frameWaiters.get(waiter.clipId);\n // if (!waiters) return;\n\n // waiters.delete(waiter);\n // if (waiters.size === 0) {\n // this.frameWaiters.delete(waiter.clipId);\n // }\n // }\n\n // private matchesTimestamp(\n // targetTimeUs: TimeUs,\n // actualTimeUs: TimeUs,\n // frameDurationUs: TimeUs,\n // toleranceUs: TimeUs\n // ): boolean {\n // if (targetTimeUs === actualTimeUs) return true;\n\n // const delta = Math.abs(targetTimeUs - actualTimeUs);\n // if (delta <= toleranceUs) {\n // return true;\n // }\n\n // if (actualTimeUs >= targetTimeUs && actualTimeUs < targetTimeUs + frameDurationUs) {\n // return true;\n // }\n\n // return false;\n // }\n\n // private makeRequestKey(clipId: string, timeUs: TimeUs): string {\n // return `${clipId}:${timeUs}`;\n // }\n}\n"],"names":[],"mappings":"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"}
@@ -20,15 +20,28 @@ export declare class VideoL1Cache {
20
20
  private readonly framesByClip;
21
21
  private readonly maxMemoryBytes;
22
22
  private currentBytes;
23
+ private windowCenter;
24
+ private windowSize;
25
+ private lastEvictTime;
26
+ private readonly EVICT_THROTTLE_MS;
23
27
  constructor(config: L1Config);
28
+ /**
29
+ * Set window center and trigger eviction
30
+ * Window management helps limit memory usage for long clips
31
+ */
32
+ setWindow(centerTimeUs: TimeUs): void;
33
+ /**
34
+ * Evict frames outside the window
35
+ * Essential for long clips to limit memory usage to window size (±3s)
36
+ */
37
+ private evictOutOfWindow;
24
38
  get(timeUs: TimeUs, clipId: string): RcFrame | null;
25
- addFrame(frame: VideoFrame, clipId: string, frameDuration: TimeUs, trackId: string): RcFrame;
39
+ addFrame(frame: VideoFrame, clipId: string, frameDuration: TimeUs, trackId: string, globalTimeUs?: TimeUs): RcFrame;
26
40
  invalidateClip(clipId: string): void;
27
41
  hasClip(clipId: string): boolean;
28
42
  getClipFrameCount(clipId: string, startTimeUs?: TimeUs): number;
29
43
  clear(): void;
30
44
  getMetadata(): L1CacheMetadata;
31
- private binarySearchFrame;
32
45
  private findInsertIndex;
33
46
  }
34
47
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"VideoL1Cache.d.ts","sourceRoot":"","sources":["../../../src/cache/l1/VideoL1Cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAItC,UAAU,QAAQ;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAgC;IAC7D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,YAAY,CAAK;gBAEb,MAAM,EAAE,QAAQ;IAI5B,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI;IASnD,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO;IA+B5F,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAYpC,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAKhC,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM;IAkB/D,KAAK,IAAI,IAAI;IAUb,WAAW,IAAI,eAAe;IAc9B,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,eAAe;CAiBxB"}
1
+ {"version":3,"file":"VideoL1Cache.d.ts","sourceRoot":"","sources":["../../../src/cache/l1/VideoL1Cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAKtC,UAAU,QAAQ;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAgC;IAC7D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,YAAY,CAAa;IACjC,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAO;gBAE7B,MAAM,EAAE,QAAQ;IAI5B;;;OAGG;IACH,SAAS,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI;IAWrC;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IA4CxB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI;IAUnD,QAAQ,CACN,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO;IAgCV,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAYpC,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAKhC,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM;IAkB/D,KAAK,IAAI,IAAI;IAUb,WAAW,IAAI,eAAe;IAc9B,OAAO,CAAC,eAAe;CAGxB"}