@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
@@ -1,24 +1,76 @@
1
+ import { binarySearchNearest, binarySearchFirst } from "../../utils/binary-search.js";
1
2
  import { RcFrame } from "../../model/RcFrame.js";
2
3
  const BYTES_PER_MB = 1024 * 1024;
3
4
  class VideoL1Cache {
4
5
  framesByClip = /* @__PURE__ */ new Map();
5
6
  maxMemoryBytes;
6
7
  currentBytes = 0;
8
+ windowCenter = 0;
9
+ windowSize = 35e5;
10
+ // ±3.5s
11
+ lastEvictTime = 0;
12
+ EVICT_THROTTLE_MS = 500;
7
13
  constructor(config) {
8
14
  this.maxMemoryBytes = config.maxMemoryMB * BYTES_PER_MB;
9
15
  }
16
+ /**
17
+ * Set window center and trigger eviction
18
+ * Window management helps limit memory usage for long clips
19
+ */
20
+ setWindow(centerTimeUs) {
21
+ this.windowCenter = centerTimeUs;
22
+ const now = Date.now();
23
+ if (now - this.lastEvictTime > this.EVICT_THROTTLE_MS) {
24
+ this.evictOutOfWindow();
25
+ this.lastEvictTime = now;
26
+ }
27
+ }
28
+ /**
29
+ * Evict frames outside the window
30
+ * Essential for long clips to limit memory usage to window size (±3s)
31
+ */
32
+ evictOutOfWindow() {
33
+ const minTime = Math.max(0, this.windowCenter - this.windowSize);
34
+ const maxTime = this.windowCenter + this.windowSize;
35
+ for (const [clipId, frames] of this.framesByClip) {
36
+ const toEvict = [];
37
+ const toKeep = [];
38
+ for (const frame of frames) {
39
+ const globalTime = frame.globalTimeUs;
40
+ if (globalTime === void 0) {
41
+ toKeep.push(frame);
42
+ continue;
43
+ }
44
+ if (globalTime < minTime || globalTime > maxTime) {
45
+ toEvict.push(frame);
46
+ } else {
47
+ toKeep.push(frame);
48
+ }
49
+ }
50
+ if (toEvict.length > 0) {
51
+ for (const frame of toEvict) {
52
+ this.currentBytes -= frame.sizeEstimate;
53
+ frame.close();
54
+ }
55
+ this.framesByClip.set(clipId, toKeep);
56
+ }
57
+ }
58
+ }
10
59
  get(timeUs, clipId) {
11
60
  const frames = this.framesByClip.get(clipId);
12
- if (!frames || frames.length === 0) return null;
13
- const index = this.binarySearchFrame(frames, timeUs);
14
- return index !== -1 ? frames[index] ?? null : null;
61
+ if (!frames || frames.length === 0) {
62
+ return null;
63
+ }
64
+ return binarySearchNearest(frames, timeUs, (frame) => frame.timestampUs ?? 0);
15
65
  }
16
- addFrame(frame, clipId, frameDuration, trackId) {
66
+ addFrame(frame, clipId, frameDuration, trackId, globalTimeUs) {
17
67
  const rcFrame = RcFrame.wrap(frame, {
18
68
  trackId,
19
69
  clipId,
20
70
  timestampUs: frame.timestamp ?? 0,
21
- durationUs: frame.duration ?? frameDuration
71
+ durationUs: frame.duration ?? frameDuration,
72
+ globalTimeUs
73
+ // Store global timestamp for window eviction
22
74
  });
23
75
  let frames = this.framesByClip.get(clipId);
24
76
  if (!frames) {
@@ -86,40 +138,8 @@ class VideoL1Cache {
86
138
  clipCount: this.framesByClip.size
87
139
  };
88
140
  }
89
- binarySearchFrame(frames, timeUs) {
90
- let low = 0;
91
- let high = frames.length - 1;
92
- let result = -1;
93
- while (low <= high) {
94
- const mid = Math.floor((low + high) / 2);
95
- const frame = frames[mid];
96
- if (!frame) break;
97
- const frameTime = frame.timestampUs ?? 0;
98
- const frameDuration = frame.durationUs ?? 0;
99
- if (timeUs < frameTime) {
100
- high = mid - 1;
101
- } else if (timeUs >= frameTime + frameDuration) {
102
- low = mid + 1;
103
- } else {
104
- result = mid;
105
- break;
106
- }
107
- }
108
- return result;
109
- }
110
141
  findInsertIndex(frames, timestamp) {
111
- let low = 0;
112
- let high = frames.length;
113
- while (low < high) {
114
- const mid = Math.floor((low + high) / 2);
115
- const midTs = frames[mid]?.timestampUs ?? 0;
116
- if (midTs < timestamp) {
117
- low = mid + 1;
118
- } else {
119
- high = mid;
120
- }
121
- }
122
- return low;
142
+ return binarySearchFirst(frames, (frame) => (frame.timestampUs ?? 0) >= timestamp);
123
143
  }
124
144
  }
125
145
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"VideoL1Cache.js","sources":["../../../src/cache/l1/VideoL1Cache.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport { RcFrame } from '../../model';\n\nconst BYTES_PER_MB = 1024 * 1024;\n\ninterface L1Config {\n maxMemoryMB: number;\n}\n\nexport interface L1CacheMetadata {\n size: number;\n maxSize: number;\n entries: number;\n clipCount: number;\n}\n\n/**\n * Simplified VideoL1Cache - Single-layer frame storage per clip\n *\n * Each clip stores frames in a sorted array by timestamp.\n * No GOP management - frames are directly stored and retrieved via binary search.\n */\nexport class VideoL1Cache {\n private readonly framesByClip = new Map<string, RcFrame[]>();\n private readonly maxMemoryBytes: number;\n private currentBytes = 0;\n\n constructor(config: L1Config) {\n this.maxMemoryBytes = config.maxMemoryMB * BYTES_PER_MB;\n }\n\n get(timeUs: TimeUs, clipId: string): RcFrame | null {\n const frames = this.framesByClip.get(clipId);\n if (!frames || frames.length === 0) return null;\n\n // Binary search for frame at timeUs\n const index = this.binarySearchFrame(frames, timeUs);\n return index !== -1 ? (frames[index] ?? null) : null;\n }\n\n addFrame(frame: VideoFrame, clipId: string, frameDuration: TimeUs, trackId: string): RcFrame {\n const rcFrame = RcFrame.wrap(frame, {\n trackId,\n clipId,\n timestampUs: frame.timestamp ?? 0,\n durationUs: frame.duration ?? frameDuration,\n });\n\n let frames = this.framesByClip.get(clipId);\n if (!frames) {\n frames = [];\n this.framesByClip.set(clipId, frames);\n }\n\n const timestamp = rcFrame.timestampUs ?? 0;\n const insertIndex = this.findInsertIndex(frames, timestamp);\n\n // Check for duplicate timestamp\n if (insertIndex < frames.length && (frames[insertIndex]?.timestampUs ?? 0) === timestamp) {\n const oldFrame = frames[insertIndex];\n frames[insertIndex] = rcFrame;\n this.currentBytes -= oldFrame?.sizeEstimate ?? 0;\n oldFrame?.close?.();\n } else {\n frames.splice(insertIndex, 0, rcFrame);\n }\n\n this.currentBytes += rcFrame.sizeEstimate;\n return rcFrame;\n }\n\n invalidateClip(clipId: string): void {\n const frames = this.framesByClip.get(clipId);\n if (!frames) return;\n\n for (const frame of frames) {\n this.currentBytes -= frame.sizeEstimate;\n frame?.close?.();\n }\n\n this.framesByClip.delete(clipId);\n }\n\n hasClip(clipId: string): boolean {\n const frames = this.framesByClip.get(clipId);\n return !!frames && frames.length > 0;\n }\n\n getClipFrameCount(clipId: string, startTimeUs?: TimeUs): number {\n const frames = this.framesByClip.get(clipId);\n if (!frames) return 0;\n\n if (startTimeUs === undefined) {\n return frames.length;\n }\n\n // Count frames >= startTimeUs\n let count = 0;\n for (const frame of frames) {\n if ((frame.timestampUs ?? 0) >= startTimeUs) {\n count++;\n }\n }\n return count;\n }\n\n clear(): void {\n for (const frames of this.framesByClip.values()) {\n for (const frame of frames) {\n frame?.close?.();\n }\n }\n this.framesByClip.clear();\n this.currentBytes = 0;\n }\n\n getMetadata(): L1CacheMetadata {\n let totalFrames = 0;\n for (const frames of this.framesByClip.values()) {\n totalFrames += frames.length;\n }\n\n return {\n size: this.currentBytes,\n maxSize: this.maxMemoryBytes,\n entries: totalFrames,\n clipCount: this.framesByClip.size,\n };\n }\n\n private binarySearchFrame(frames: RcFrame[], timeUs: TimeUs): number {\n let low = 0;\n let high = frames.length - 1;\n let result = -1;\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const frame = frames[mid];\n if (!frame) break;\n\n const frameTime = frame.timestampUs ?? 0;\n const frameDuration = frame.durationUs ?? 0;\n\n if (timeUs < frameTime) {\n high = mid - 1;\n } else if (timeUs >= frameTime + frameDuration) {\n low = mid + 1;\n } else {\n result = mid;\n break;\n }\n }\n\n return result;\n }\n\n private findInsertIndex(frames: RcFrame[], timestamp: TimeUs): number {\n let low = 0;\n let high = frames.length;\n\n while (low < high) {\n const mid = Math.floor((low + high) / 2);\n const midTs = frames[mid]?.timestampUs ?? 0;\n\n if (midTs < timestamp) {\n low = mid + 1;\n } else {\n high = mid;\n }\n }\n\n return low;\n }\n}\n"],"names":[],"mappings":";AAGA,MAAM,eAAe,OAAO;AAmBrB,MAAM,aAAa;AAAA,EACP,mCAAmB,IAAA;AAAA,EACnB;AAAA,EACT,eAAe;AAAA,EAEvB,YAAY,QAAkB;AAC5B,SAAK,iBAAiB,OAAO,cAAc;AAAA,EAC7C;AAAA,EAEA,IAAI,QAAgB,QAAgC;AAClD,UAAM,SAAS,KAAK,aAAa,IAAI,MAAM;AAC3C,QAAI,CAAC,UAAU,OAAO,WAAW,EAAG,QAAO;AAG3C,UAAM,QAAQ,KAAK,kBAAkB,QAAQ,MAAM;AACnD,WAAO,UAAU,KAAM,OAAO,KAAK,KAAK,OAAQ;AAAA,EAClD;AAAA,EAEA,SAAS,OAAmB,QAAgB,eAAuB,SAA0B;AAC3F,UAAM,UAAU,QAAQ,KAAK,OAAO;AAAA,MAClC;AAAA,MACA;AAAA,MACA,aAAa,MAAM,aAAa;AAAA,MAChC,YAAY,MAAM,YAAY;AAAA,IAAA,CAC/B;AAED,QAAI,SAAS,KAAK,aAAa,IAAI,MAAM;AACzC,QAAI,CAAC,QAAQ;AACX,eAAS,CAAA;AACT,WAAK,aAAa,IAAI,QAAQ,MAAM;AAAA,IACtC;AAEA,UAAM,YAAY,QAAQ,eAAe;AACzC,UAAM,cAAc,KAAK,gBAAgB,QAAQ,SAAS;AAG1D,QAAI,cAAc,OAAO,WAAW,OAAO,WAAW,GAAG,eAAe,OAAO,WAAW;AACxF,YAAM,WAAW,OAAO,WAAW;AACnC,aAAO,WAAW,IAAI;AACtB,WAAK,gBAAgB,UAAU,gBAAgB;AAC/C,gBAAU,QAAA;AAAA,IACZ,OAAO;AACL,aAAO,OAAO,aAAa,GAAG,OAAO;AAAA,IACvC;AAEA,SAAK,gBAAgB,QAAQ;AAC7B,WAAO;AAAA,EACT;AAAA,EAEA,eAAe,QAAsB;AACnC,UAAM,SAAS,KAAK,aAAa,IAAI,MAAM;AAC3C,QAAI,CAAC,OAAQ;AAEb,eAAW,SAAS,QAAQ;AAC1B,WAAK,gBAAgB,MAAM;AAC3B,aAAO,QAAA;AAAA,IACT;AAEA,SAAK,aAAa,OAAO,MAAM;AAAA,EACjC;AAAA,EAEA,QAAQ,QAAyB;AAC/B,UAAM,SAAS,KAAK,aAAa,IAAI,MAAM;AAC3C,WAAO,CAAC,CAAC,UAAU,OAAO,SAAS;AAAA,EACrC;AAAA,EAEA,kBAAkB,QAAgB,aAA8B;AAC9D,UAAM,SAAS,KAAK,aAAa,IAAI,MAAM;AAC3C,QAAI,CAAC,OAAQ,QAAO;AAEpB,QAAI,gBAAgB,QAAW;AAC7B,aAAO,OAAO;AAAA,IAChB;AAGA,QAAI,QAAQ;AACZ,eAAW,SAAS,QAAQ;AAC1B,WAAK,MAAM,eAAe,MAAM,aAAa;AAC3C;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EAEA,QAAc;AACZ,eAAW,UAAU,KAAK,aAAa,OAAA,GAAU;AAC/C,iBAAW,SAAS,QAAQ;AAC1B,eAAO,QAAA;AAAA,MACT;AAAA,IACF;AACA,SAAK,aAAa,MAAA;AAClB,SAAK,eAAe;AAAA,EACtB;AAAA,EAEA,cAA+B;AAC7B,QAAI,cAAc;AAClB,eAAW,UAAU,KAAK,aAAa,OAAA,GAAU;AAC/C,qBAAe,OAAO;AAAA,IACxB;AAEA,WAAO;AAAA,MACL,MAAM,KAAK;AAAA,MACX,SAAS,KAAK;AAAA,MACd,SAAS;AAAA,MACT,WAAW,KAAK,aAAa;AAAA,IAAA;AAAA,EAEjC;AAAA,EAEQ,kBAAkB,QAAmB,QAAwB;AACnE,QAAI,MAAM;AACV,QAAI,OAAO,OAAO,SAAS;AAC3B,QAAI,SAAS;AAEb,WAAO,OAAO,MAAM;AAClB,YAAM,MAAM,KAAK,OAAO,MAAM,QAAQ,CAAC;AACvC,YAAM,QAAQ,OAAO,GAAG;AACxB,UAAI,CAAC,MAAO;AAEZ,YAAM,YAAY,MAAM,eAAe;AACvC,YAAM,gBAAgB,MAAM,cAAc;AAE1C,UAAI,SAAS,WAAW;AACtB,eAAO,MAAM;AAAA,MACf,WAAW,UAAU,YAAY,eAAe;AAC9C,cAAM,MAAM;AAAA,MACd,OAAO;AACL,iBAAS;AACT;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,gBAAgB,QAAmB,WAA2B;AACpE,QAAI,MAAM;AACV,QAAI,OAAO,OAAO;AAElB,WAAO,MAAM,MAAM;AACjB,YAAM,MAAM,KAAK,OAAO,MAAM,QAAQ,CAAC;AACvC,YAAM,QAAQ,OAAO,GAAG,GAAG,eAAe;AAE1C,UAAI,QAAQ,WAAW;AACrB,cAAM,MAAM;AAAA,MACd,OAAO;AACL,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;"}
1
+ {"version":3,"file":"VideoL1Cache.js","sources":["../../../src/cache/l1/VideoL1Cache.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport { RcFrame } from '../../model';\nimport { binarySearchNearest, binarySearchFirst } from '../../utils/binary-search';\n\nconst BYTES_PER_MB = 1024 * 1024;\n\ninterface L1Config {\n maxMemoryMB: number;\n}\n\nexport interface L1CacheMetadata {\n size: number;\n maxSize: number;\n entries: number;\n clipCount: number;\n}\n\n/**\n * Simplified VideoL1Cache - Single-layer frame storage per clip\n *\n * Each clip stores frames in a sorted array by timestamp.\n * No GOP management - frames are directly stored and retrieved via binary search.\n */\nexport class VideoL1Cache {\n private readonly framesByClip = new Map<string, RcFrame[]>();\n private readonly maxMemoryBytes: number;\n private currentBytes = 0;\n private windowCenter: TimeUs = 0;\n private windowSize: TimeUs = 3_500_000; // ±3.5s\n private lastEvictTime = 0;\n private readonly EVICT_THROTTLE_MS = 500;\n\n constructor(config: L1Config) {\n this.maxMemoryBytes = config.maxMemoryMB * BYTES_PER_MB;\n }\n\n /**\n * Set window center and trigger eviction\n * Window management helps limit memory usage for long clips\n */\n setWindow(centerTimeUs: TimeUs): void {\n this.windowCenter = centerTimeUs;\n\n // Throttle eviction to avoid excessive processing\n const now = Date.now();\n if (now - this.lastEvictTime > this.EVICT_THROTTLE_MS) {\n this.evictOutOfWindow();\n this.lastEvictTime = now;\n }\n }\n\n /**\n * Evict frames outside the window\n * Essential for long clips to limit memory usage to window size (±3s)\n */\n private evictOutOfWindow(): void {\n const minTime = Math.max(0, this.windowCenter - this.windowSize);\n const maxTime = this.windowCenter + this.windowSize;\n\n for (const [clipId, frames] of this.framesByClip) {\n const toEvict: RcFrame[] = [];\n const toKeep: RcFrame[] = [];\n\n for (const frame of frames) {\n const globalTime = frame.globalTimeUs;\n\n // Frames without globalTimeUs (legacy) are kept (will be evicted by clip-level logic)\n if (globalTime === undefined) {\n toKeep.push(frame);\n continue;\n }\n\n // Evict frames outside window\n if (globalTime < minTime || globalTime > maxTime) {\n // console.log('[VideoL1Cache] Evicting frame outside window:', {\n // globalTime,\n // windowCenter: this.windowCenter,\n // windowSize: this.windowSize,\n // minTime,\n // maxTime,\n // });\n toEvict.push(frame);\n } else {\n toKeep.push(frame);\n }\n }\n\n // Release evicted frames\n if (toEvict.length > 0) {\n for (const frame of toEvict) {\n this.currentBytes -= frame.sizeEstimate;\n frame.close();\n }\n\n this.framesByClip.set(clipId, toKeep);\n }\n }\n }\n\n get(timeUs: TimeUs, clipId: string): RcFrame | null {\n const frames = this.framesByClip.get(clipId);\n if (!frames || frames.length === 0) {\n return null;\n }\n\n // Use nearest frame search (instead of exact range match)\n return binarySearchNearest(frames, timeUs, (frame) => frame.timestampUs ?? 0);\n }\n\n addFrame(\n frame: VideoFrame,\n clipId: string,\n frameDuration: TimeUs,\n trackId: string,\n globalTimeUs?: TimeUs\n ): RcFrame {\n const rcFrame = RcFrame.wrap(frame, {\n trackId,\n clipId,\n timestampUs: frame.timestamp ?? 0,\n durationUs: frame.duration ?? frameDuration,\n globalTimeUs, // Store global timestamp for window eviction\n });\n\n let frames = this.framesByClip.get(clipId);\n if (!frames) {\n frames = [];\n this.framesByClip.set(clipId, frames);\n }\n\n const timestamp = rcFrame.timestampUs ?? 0;\n const insertIndex = this.findInsertIndex(frames, timestamp);\n\n // Check for duplicate timestamp\n if (insertIndex < frames.length && (frames[insertIndex]?.timestampUs ?? 0) === timestamp) {\n const oldFrame = frames[insertIndex];\n frames[insertIndex] = rcFrame;\n this.currentBytes -= oldFrame?.sizeEstimate ?? 0;\n oldFrame?.close?.();\n } else {\n frames.splice(insertIndex, 0, rcFrame);\n }\n\n this.currentBytes += rcFrame.sizeEstimate;\n return rcFrame;\n }\n\n invalidateClip(clipId: string): void {\n const frames = this.framesByClip.get(clipId);\n if (!frames) return;\n\n for (const frame of frames) {\n this.currentBytes -= frame.sizeEstimate;\n frame?.close?.();\n }\n\n this.framesByClip.delete(clipId);\n }\n\n hasClip(clipId: string): boolean {\n const frames = this.framesByClip.get(clipId);\n return !!frames && frames.length > 0;\n }\n\n getClipFrameCount(clipId: string, startTimeUs?: TimeUs): number {\n const frames = this.framesByClip.get(clipId);\n if (!frames) return 0;\n\n if (startTimeUs === undefined) {\n return frames.length;\n }\n\n // Count frames >= startTimeUs\n let count = 0;\n for (const frame of frames) {\n if ((frame.timestampUs ?? 0) >= startTimeUs) {\n count++;\n }\n }\n return count;\n }\n\n clear(): void {\n for (const frames of this.framesByClip.values()) {\n for (const frame of frames) {\n frame?.close?.();\n }\n }\n this.framesByClip.clear();\n this.currentBytes = 0;\n }\n\n getMetadata(): L1CacheMetadata {\n let totalFrames = 0;\n for (const frames of this.framesByClip.values()) {\n totalFrames += frames.length;\n }\n\n return {\n size: this.currentBytes,\n maxSize: this.maxMemoryBytes,\n entries: totalFrames,\n clipCount: this.framesByClip.size,\n };\n }\n\n private findInsertIndex(frames: RcFrame[], timestamp: TimeUs): number {\n return binarySearchFirst(frames, (frame) => (frame.timestampUs ?? 0) >= timestamp);\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"L2Cache.d.ts","sourceRoot":"","sources":["../../../src/cache/l2/L2Cache.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"L2Cache.d.ts","sourceRoot":"","sources":["../../../src/cache/l2/L2Cache.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { binarySearchRange } from "../../utils/binary-search.js";
2
- import { OPFSStore } from "./OPFSStore.js";
3
- import { IndexedDBStore } from "./IndexedDBStore.js";
2
+ import { L2OPFSStore } from "./L2OPFSStore.js";
3
+ import { ChunkRecordStore } from "../storage/indexeddb/ChunkRecordStore.js";
4
4
  class L2Cache {
5
5
  opfsStore;
6
6
  dbStore;
@@ -10,8 +10,8 @@ class L2Cache {
10
10
  constructor(config) {
11
11
  this.maxSize = config.maxSizeMB * 1024 * 1024;
12
12
  this.projectId = config.projectId;
13
- this.opfsStore = new OPFSStore();
14
- this.dbStore = new IndexedDBStore();
13
+ this.opfsStore = new L2OPFSStore();
14
+ this.dbStore = new ChunkRecordStore();
15
15
  }
16
16
  async init() {
17
17
  if (this.initPromise) return this.initPromise;
@@ -24,7 +24,7 @@ class L2Cache {
24
24
  await this.init();
25
25
  const records = await this.dbStore.collectRecordsByClipId(this.projectId, clipId);
26
26
  for (const record of records) {
27
- const batch = binarySearchRange(record.batches, timeUs, (b) => ({
27
+ const batch = binarySearchRange(record.batches, timeUs, (b, _index) => ({
28
28
  start: b.startUs,
29
29
  end: b.startUs + b.durationUs
30
30
  }));
@@ -1 +1 @@
1
- {"version":3,"file":"L2Cache.js","sources":["../../../src/cache/l2/L2Cache.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport { binarySearchRange } from '../../utils/binary-search';\nimport { OPFSStore } from './OPFSStore';\nimport { IndexedDBStore, type ChunkRecord } from './IndexedDBStore';\n\ninterface L2Config {\n maxSizeMB: number;\n projectId: string;\n}\n\n/**\n * L2 Cache - High-level cache coordinator\n * Uses OPFSStore for file storage and IndexedDBStore for metadata\n */\nexport class L2Cache {\n private readonly opfsStore: OPFSStore;\n private readonly dbStore: IndexedDBStore;\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 this.opfsStore = new OPFSStore();\n this.dbStore = new IndexedDBStore();\n }\n\n async init(): Promise<void> {\n if (this.initPromise) return this.initPromise;\n\n this.initPromise = (async () => {\n await Promise.all([this.opfsStore.init(), this.dbStore.init()]);\n })();\n\n return this.initPromise;\n }\n\n async get(timeUs: TimeUs, clipId: string): Promise<EncodedVideoChunk | EncodedAudioChunk | null> {\n await this.init();\n\n // Query IndexedDB for chunk metadata\n const records = await this.dbStore.collectRecordsByClipId(this.projectId, 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.opfsStore.read(record.fileName, batch, this.projectId);\n if (!chunkData) continue;\n\n await this.dbStore.updateLastAccess(this.projectId, record.clipId, record.track);\n\n return this.createChunk(chunkData, timeUs, record.track, batch.type, batch.durationUs);\n }\n\n return null;\n }\n\n async put(\n clipId: string,\n chunks: Array<EncodedVideoChunk | EncodedAudioChunk>,\n track: 'video' | 'audio',\n options?: {\n isComplete?: boolean;\n expectedDurationUs?: number;\n metadata?: any;\n }\n ): Promise<void> {\n await this.init();\n\n if (chunks.length === 0) return;\n\n const fileName = `clip-${clipId}-${track[0]}1.${track === 'video' ? 'webm' : 'm4a'}`;\n\n // Step 1: Read existing record\n let existingRecord = await this.dbStore.getRecord(this.projectId, clipId, track);\n\n // Step 2: Validate consistency - if IndexedDB has record but OPFS file missing, delete the record\n if (existingRecord) {\n const fileExists = await this.opfsStore.fileExists(existingRecord.fileName, this.projectId);\n if (!fileExists) {\n await this.deleteEntry(clipId, track);\n existingRecord = null;\n }\n }\n\n // Step 3: Deduplicate based on timestamp\n let chunksToWrite = chunks;\n if (existingRecord && existingRecord.batches.length > 0) {\n const lastBatch = existingRecord.batches[existingRecord.batches.length - 1];\n if (lastBatch) {\n const lastTimestamp = lastBatch.startUs;\n // Filter out chunks with timestamp <= lastTimestamp\n chunksToWrite = chunks.filter((chunk) => chunk.timestamp > lastTimestamp);\n\n if (chunksToWrite.length === 0) {\n return;\n }\n }\n }\n\n // Step 4: Write to OPFS\n const newBatches = await this.opfsStore.append(\n fileName,\n chunksToWrite,\n existingRecord?.batches,\n this.projectId\n );\n\n // Step 5: Update IndexedDB\n const record: ChunkRecord = {\n projectId: this.projectId,\n clipId,\n track,\n fileName,\n batches: existingRecord?.batches ? [...existingRecord.batches, ...newBatches] : newBatches,\n lastAccess: Date.now(),\n totalBytes:\n (existingRecord?.totalBytes || 0) + newBatches.reduce((sum, b) => sum + b.byteLength, 0),\n isComplete: options?.isComplete ?? existingRecord?.isComplete ?? false,\n expectedDurationUs: options?.expectedDurationUs ?? existingRecord?.expectedDurationUs,\n metadata: options?.metadata ?? existingRecord?.metadata,\n };\n\n await this.dbStore.putRecord(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 // Get records to check\n let records: ChunkRecord[];\n if (clipId) {\n records = await this.dbStore.collectRecordsByClipId(this.projectId, clipId);\n } else {\n // Need all records for current project\n const allRecords = await this.dbStore.getAllRecords();\n records = allRecords.filter((r) => r.projectId === this.projectId);\n }\n\n // Check overlaps and delete\n for (const record of records) {\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 await this.deleteEntry(record.clipId, record.track);\n }\n }\n }\n\n /**\n * Check if clip has cached data in L2\n */\n async hasClip(clipId: string, track: 'video' | 'audio'): Promise<boolean> {\n await this.init();\n\n const record = await this.dbStore.getRecord(this.projectId, clipId, track);\n return record !== null && record.batches && record.batches.length > 0;\n }\n\n /**\n * Check if clip has complete cached data in L2\n */\n async hasCompleteClip(clipId: string, track: 'video' | 'audio'): Promise<boolean> {\n await this.init();\n\n const record = await this.dbStore.getRecord(this.projectId, clipId, track);\n return record?.isComplete === true;\n }\n\n /**\n * Mark clip as complete in L2 cache\n */\n async markComplete(clipId: string, track: 'video' | 'audio'): Promise<void> {\n await this.init();\n\n const record = await this.dbStore.getRecord(this.projectId, clipId, track);\n if (record) {\n record.isComplete = true;\n record.lastAccess = Date.now();\n await this.dbStore.putRecord(record);\n }\n }\n\n async invalidateClip(clipId: string): Promise<void> {\n await this.init();\n\n // Collect records to delete\n const records = await this.dbStore.collectRecordsByClipId(this.projectId, clipId);\n\n // Delete each record\n for (const record of records) {\n await this.deleteEntry(record.clipId, record.track);\n }\n }\n\n /**\n * Create a readable stream of encoded chunks for export\n * Reads chunks in timestamp order from OPFS\n */\n async createReadStream(\n clipId: string,\n track: 'video' | 'audio'\n ): Promise<ReadableStream<EncodedVideoChunk | EncodedAudioChunk> | null> {\n await this.init();\n\n // Get chunk record\n const record = await this.dbStore.getRecord(this.projectId, clipId, track);\n\n if (!record || record.batches.length === 0) {\n return null;\n }\n\n // Clone batches array for stream iteration\n const batches = [...record.batches];\n let batchIndex = 0;\n\n return new ReadableStream<EncodedVideoChunk | EncodedAudioChunk>({\n pull: async (controller) => {\n if (batchIndex >= batches.length) {\n controller.close();\n return;\n }\n\n const batch = batches[batchIndex];\n if (!batch) {\n controller.close();\n return;\n }\n\n try {\n // Read chunk data from OPFS\n const chunkData = await this.opfsStore.read(record.fileName, batch, this.projectId);\n if (!chunkData) {\n controller.close();\n return;\n }\n\n // Create encoded chunk with correct type and duration\n const chunk = this.createChunk(\n chunkData,\n batch.startUs,\n track,\n batch.type,\n batch.durationUs\n );\n controller.enqueue(chunk);\n\n batchIndex++;\n } catch (error) {\n // File not found or read error - close stream gracefully\n if (error instanceof DOMException && error.name === 'NotFoundError') {\n controller.close();\n } else {\n controller.error(error);\n }\n }\n },\n });\n }\n\n async clear(): Promise<void> {\n await this.init();\n\n // Clear IndexedDB\n try {\n await this.dbStore.clear();\n } catch (error) {\n console.error('[L2Cache] Failed to clear IndexedDB:', error);\n throw error;\n }\n\n // Clear OPFS files\n try {\n await this.opfsStore.clear(this.projectId);\n } catch (error) {\n if ((error as any)?.name !== 'NotFoundError') {\n console.warn('[L2Cache] Failed to clear OPFS:', error);\n }\n }\n }\n\n private createChunk(\n data: ArrayBuffer,\n timeUs: TimeUs,\n track: 'video' | 'audio',\n chunkType: 'key' | 'delta' = 'key',\n durationUs: TimeUs = 0\n ): EncodedVideoChunk | EncodedAudioChunk {\n if (track === 'video') {\n return new EncodedVideoChunk({\n type: chunkType,\n timestamp: timeUs,\n duration: durationUs,\n data,\n });\n } else {\n return new EncodedAudioChunk({\n type: chunkType,\n timestamp: timeUs,\n duration: durationUs,\n data,\n });\n }\n }\n\n private async deleteEntry(clipId: string, track: string, projectId?: string): Promise<void> {\n const targetProjectId = projectId ?? this.projectId;\n\n // Step 1: Get record info\n const record = await this.dbStore.getRecord(targetProjectId, clipId, track);\n\n // Step 2: Delete OPFS file\n if (record) {\n await this.opfsStore.deleteFile(record.fileName, targetProjectId);\n }\n\n // Step 3: Delete IndexedDB record\n await this.dbStore.deleteRecord(targetProjectId, 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 console.warn(\n `[L2Cache] Quota exceeded! Deleting oldest entries: usage=${usage}, maxSize=${this.maxSize}`\n );\n\n const toDelete = usage - this.maxSize;\n let bytesDeleted = 0;\n\n // Get all records sorted by lastAccess\n const records = await this.dbStore.getAllRecordsSortedByAccess();\n\n // Step 1: Delete oldest entries from OTHER projects (protect current project)\n for (const record of records) {\n if (bytesDeleted >= toDelete) break;\n\n if (record.projectId !== this.projectId) {\n await this.deleteEntry(record.clipId, record.track, record.projectId);\n bytesDeleted += record.totalBytes;\n }\n }\n\n // Step 2: If still over quota, delete oldest from current project\n if (bytesDeleted < toDelete) {\n for (const record of records) {\n if (bytesDeleted >= toDelete) break;\n\n if (record.projectId === this.projectId) {\n await this.deleteEntry(record.clipId, record.track);\n bytesDeleted += record.totalBytes;\n }\n }\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 /**\n * Get chunk metadata (decoderConfig) for a specific clip\n */\n async getClipMetadata(clipId: string, track: 'video' | 'audio'): Promise<any | null> {\n await this.init();\n\n const record = await this.dbStore.getRecord(this.projectId, clipId, track);\n return record?.metadata || null;\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 await this.init();\n\n const records = await this.dbStore.getAllRecords();\n const projects = new Map<\n string,\n { totalBytes: number; clipCount: number; lastAccess: number }\n >();\n\n // Aggregate stats per project\n for (const record of records) {\n const existing = projects.get(record.projectId) || {\n totalBytes: 0,\n clipCount: 0,\n lastAccess: 0,\n };\n\n projects.set(record.projectId, {\n totalBytes: existing.totalBytes + record.totalBytes,\n clipCount: existing.clipCount + 1,\n lastAccess: Math.max(existing.lastAccess, record.lastAccess),\n });\n }\n\n return Array.from(projects.entries()).map(([projectId, stats]) => ({\n projectId,\n ...stats,\n }));\n }\n\n /**\n * Clear all cache data for a specific project\n */\n async clearProject(targetProjectId: string): Promise<void> {\n await this.init();\n\n // 1. Collect all records for this project\n const records = await this.dbStore.getRecordsByProjectId(targetProjectId);\n\n // 2. Delete OPFS files and IndexedDB records\n for (const record of records) {\n await this.deleteEntry(record.clipId, record.track, targetProjectId);\n }\n\n // 3. Delete project directory (recursive, even if not empty)\n await this.opfsStore.deleteProjectDirectory(targetProjectId);\n }\n\n /**\n * Get cache size for a specific project\n */\n async getProjectSize(projectId: string): Promise<number> {\n await this.init();\n\n const records = await this.dbStore.getRecordsByProjectId(projectId);\n return records.reduce((sum, r) => sum + r.totalBytes, 0);\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"L2Cache.js","sources":["../../../src/cache/l2/L2Cache.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport { binarySearchRange } from '../../utils/binary-search';\nimport { L2OPFSStore } from './L2OPFSStore';\nimport { ChunkRecordStore, type ChunkRecord } from '../storage/indexeddb/ChunkRecordStore';\n\ninterface L2Config {\n maxSizeMB: number;\n projectId: string;\n}\n\n/**\n * L2 Cache - High-level cache coordinator\n * Uses OPFSStore for file storage and IndexedDBStore for metadata\n */\nexport class L2Cache {\n private readonly opfsStore: L2OPFSStore;\n private readonly dbStore: ChunkRecordStore;\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 this.opfsStore = new L2OPFSStore();\n this.dbStore = new ChunkRecordStore();\n }\n\n async init(): Promise<void> {\n if (this.initPromise) return this.initPromise;\n\n this.initPromise = (async () => {\n await Promise.all([this.opfsStore.init(), this.dbStore.init()]);\n })();\n\n return this.initPromise;\n }\n\n async get(timeUs: TimeUs, clipId: string): Promise<EncodedVideoChunk | EncodedAudioChunk | null> {\n await this.init();\n\n // Query IndexedDB for chunk metadata\n const records = await this.dbStore.collectRecordsByClipId(this.projectId, clipId);\n\n for (const record of records) {\n const batch = binarySearchRange(record.batches, timeUs, (b, _index) => ({\n start: b.startUs,\n end: b.startUs + b.durationUs,\n }));\n\n if (!batch) {\n continue;\n }\n\n const chunkData = await this.opfsStore.read(record.fileName, batch, this.projectId);\n if (!chunkData) continue;\n\n await this.dbStore.updateLastAccess(this.projectId, record.clipId, record.track);\n\n return this.createChunk(chunkData, timeUs, record.track, batch.type, batch.durationUs);\n }\n\n return null;\n }\n\n async put(\n clipId: string,\n chunks: Array<EncodedVideoChunk | EncodedAudioChunk>,\n track: 'video' | 'audio',\n options?: {\n isComplete?: boolean;\n expectedDurationUs?: number;\n metadata?: any;\n }\n ): Promise<void> {\n await this.init();\n\n if (chunks.length === 0) return;\n\n const fileName = `clip-${clipId}-${track[0]}1.${track === 'video' ? 'webm' : 'm4a'}`;\n\n // Step 1: Read existing record\n let existingRecord = await this.dbStore.getRecord(this.projectId, clipId, track);\n\n // Step 2: Validate consistency - if IndexedDB has record but OPFS file missing, delete the record\n if (existingRecord) {\n const fileExists = await this.opfsStore.fileExists(existingRecord.fileName, this.projectId);\n if (!fileExists) {\n await this.deleteEntry(clipId, track);\n existingRecord = null;\n }\n }\n\n // Step 3: Deduplicate based on timestamp\n let chunksToWrite = chunks;\n if (existingRecord && existingRecord.batches.length > 0) {\n const lastBatch = existingRecord.batches[existingRecord.batches.length - 1];\n if (lastBatch) {\n const lastTimestamp = lastBatch.startUs;\n // Filter out chunks with timestamp <= lastTimestamp\n chunksToWrite = chunks.filter((chunk) => chunk.timestamp > lastTimestamp);\n\n if (chunksToWrite.length === 0) {\n return;\n }\n }\n }\n\n // Step 4: Write to OPFS\n const newBatches = await this.opfsStore.append(\n fileName,\n chunksToWrite,\n existingRecord?.batches,\n this.projectId\n );\n\n // Step 5: Update IndexedDB\n const record: ChunkRecord = {\n projectId: this.projectId,\n clipId,\n track,\n fileName,\n batches: existingRecord?.batches ? [...existingRecord.batches, ...newBatches] : newBatches,\n lastAccess: Date.now(),\n totalBytes:\n (existingRecord?.totalBytes || 0) + newBatches.reduce((sum, b) => sum + b.byteLength, 0),\n isComplete: options?.isComplete ?? existingRecord?.isComplete ?? false,\n expectedDurationUs: options?.expectedDurationUs ?? existingRecord?.expectedDurationUs,\n metadata: options?.metadata ?? existingRecord?.metadata,\n };\n\n await this.dbStore.putRecord(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 // Get records to check\n let records: ChunkRecord[];\n if (clipId) {\n records = await this.dbStore.collectRecordsByClipId(this.projectId, clipId);\n } else {\n // Need all records for current project\n const allRecords = await this.dbStore.getAllRecords();\n records = allRecords.filter((r) => r.projectId === this.projectId);\n }\n\n // Check overlaps and delete\n for (const record of records) {\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 await this.deleteEntry(record.clipId, record.track);\n }\n }\n }\n\n /**\n * Check if clip has cached data in L2\n */\n async hasClip(clipId: string, track: 'video' | 'audio'): Promise<boolean> {\n await this.init();\n\n const record = await this.dbStore.getRecord(this.projectId, clipId, track);\n return record !== null && record.batches && record.batches.length > 0;\n }\n\n /**\n * Check if clip has complete cached data in L2\n */\n async hasCompleteClip(clipId: string, track: 'video' | 'audio'): Promise<boolean> {\n await this.init();\n\n const record = await this.dbStore.getRecord(this.projectId, clipId, track);\n return record?.isComplete === true;\n }\n\n /**\n * Mark clip as complete in L2 cache\n */\n async markComplete(clipId: string, track: 'video' | 'audio'): Promise<void> {\n await this.init();\n\n const record = await this.dbStore.getRecord(this.projectId, clipId, track);\n if (record) {\n record.isComplete = true;\n record.lastAccess = Date.now();\n await this.dbStore.putRecord(record);\n }\n }\n\n async invalidateClip(clipId: string): Promise<void> {\n await this.init();\n\n // Collect records to delete\n const records = await this.dbStore.collectRecordsByClipId(this.projectId, clipId);\n\n // Delete each record\n for (const record of records) {\n await this.deleteEntry(record.clipId, record.track);\n }\n }\n\n /**\n * Create a readable stream of encoded chunks for export\n * Reads chunks in timestamp order from OPFS\n */\n async createReadStream(\n clipId: string,\n track: 'video' | 'audio'\n ): Promise<ReadableStream<EncodedVideoChunk | EncodedAudioChunk> | null> {\n await this.init();\n\n // Get chunk record\n const record = await this.dbStore.getRecord(this.projectId, clipId, track);\n\n if (!record || record.batches.length === 0) {\n return null;\n }\n\n // Clone batches array for stream iteration\n const batches = [...record.batches];\n let batchIndex = 0;\n\n return new ReadableStream<EncodedVideoChunk | EncodedAudioChunk>({\n pull: async (controller) => {\n if (batchIndex >= batches.length) {\n controller.close();\n return;\n }\n\n const batch = batches[batchIndex];\n if (!batch) {\n controller.close();\n return;\n }\n\n try {\n // Read chunk data from OPFS\n const chunkData = await this.opfsStore.read(record.fileName, batch, this.projectId);\n if (!chunkData) {\n controller.close();\n return;\n }\n\n // Create encoded chunk with correct type and duration\n const chunk = this.createChunk(\n chunkData,\n batch.startUs,\n track,\n batch.type,\n batch.durationUs\n );\n controller.enqueue(chunk);\n\n batchIndex++;\n } catch (error) {\n // File not found or read error - close stream gracefully\n if (error instanceof DOMException && error.name === 'NotFoundError') {\n controller.close();\n } else {\n controller.error(error);\n }\n }\n },\n });\n }\n\n async clear(): Promise<void> {\n await this.init();\n\n // Clear IndexedDB\n try {\n await this.dbStore.clear();\n } catch (error) {\n console.error('[L2Cache] Failed to clear IndexedDB:', error);\n throw error;\n }\n\n // Clear OPFS files\n try {\n await this.opfsStore.clear(this.projectId);\n } catch (error) {\n if ((error as any)?.name !== 'NotFoundError') {\n console.warn('[L2Cache] Failed to clear OPFS:', error);\n }\n }\n }\n\n private createChunk(\n data: ArrayBuffer,\n timeUs: TimeUs,\n track: 'video' | 'audio',\n chunkType: 'key' | 'delta' = 'key',\n durationUs: TimeUs = 0\n ): EncodedVideoChunk | EncodedAudioChunk {\n if (track === 'video') {\n return new EncodedVideoChunk({\n type: chunkType,\n timestamp: timeUs,\n duration: durationUs,\n data,\n });\n } else {\n return new EncodedAudioChunk({\n type: chunkType,\n timestamp: timeUs,\n duration: durationUs,\n data,\n });\n }\n }\n\n private async deleteEntry(clipId: string, track: string, projectId?: string): Promise<void> {\n const targetProjectId = projectId ?? this.projectId;\n\n // Step 1: Get record info\n const record = await this.dbStore.getRecord(targetProjectId, clipId, track);\n\n // Step 2: Delete OPFS file\n if (record) {\n await this.opfsStore.deleteFile(record.fileName, targetProjectId);\n }\n\n // Step 3: Delete IndexedDB record\n await this.dbStore.deleteRecord(targetProjectId, 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 console.warn(\n `[L2Cache] Quota exceeded! Deleting oldest entries: usage=${usage}, maxSize=${this.maxSize}`\n );\n\n const toDelete = usage - this.maxSize;\n let bytesDeleted = 0;\n\n // Get all records sorted by lastAccess\n const records = await this.dbStore.getAllRecordsSortedByAccess();\n\n // Step 1: Delete oldest entries from OTHER projects (protect current project)\n for (const record of records) {\n if (bytesDeleted >= toDelete) break;\n\n if (record.projectId !== this.projectId) {\n await this.deleteEntry(record.clipId, record.track, record.projectId);\n bytesDeleted += record.totalBytes;\n }\n }\n\n // Step 2: If still over quota, delete oldest from current project\n if (bytesDeleted < toDelete) {\n for (const record of records) {\n if (bytesDeleted >= toDelete) break;\n\n if (record.projectId === this.projectId) {\n await this.deleteEntry(record.clipId, record.track);\n bytesDeleted += record.totalBytes;\n }\n }\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 /**\n * Get chunk metadata (decoderConfig) for a specific clip\n */\n async getClipMetadata(clipId: string, track: 'video' | 'audio'): Promise<any | null> {\n await this.init();\n\n const record = await this.dbStore.getRecord(this.projectId, clipId, track);\n return record?.metadata || null;\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 await this.init();\n\n const records = await this.dbStore.getAllRecords();\n const projects = new Map<\n string,\n { totalBytes: number; clipCount: number; lastAccess: number }\n >();\n\n // Aggregate stats per project\n for (const record of records) {\n const existing = projects.get(record.projectId) || {\n totalBytes: 0,\n clipCount: 0,\n lastAccess: 0,\n };\n\n projects.set(record.projectId, {\n totalBytes: existing.totalBytes + record.totalBytes,\n clipCount: existing.clipCount + 1,\n lastAccess: Math.max(existing.lastAccess, record.lastAccess),\n });\n }\n\n return Array.from(projects.entries()).map(([projectId, stats]) => ({\n projectId,\n ...stats,\n }));\n }\n\n /**\n * Clear all cache data for a specific project\n */\n async clearProject(targetProjectId: string): Promise<void> {\n await this.init();\n\n // 1. Collect all records for this project\n const records = await this.dbStore.getRecordsByProjectId(targetProjectId);\n\n // 2. Delete OPFS files and IndexedDB records\n for (const record of records) {\n await this.deleteEntry(record.clipId, record.track, targetProjectId);\n }\n\n // 3. Delete project directory (recursive, even if not empty)\n await this.opfsStore.deleteProjectDirectory(targetProjectId);\n }\n\n /**\n * Get cache size for a specific project\n */\n async getProjectSize(projectId: string): Promise<number> {\n await this.init();\n\n const records = await this.dbStore.getRecordsByProjectId(projectId);\n return records.reduce((sum, r) => sum + r.totalBytes, 0);\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import { ChunkBatch } from '../storage/opfs/types';
2
+
3
+ /**
4
+ * L2-specific OPFS wrapper
5
+ * Adapts OPFSManager for L2 cache chunk operations
6
+ */
7
+ export declare class L2OPFSStore {
8
+ private opfsManager;
9
+ constructor();
10
+ init(): Promise<void>;
11
+ /**
12
+ * Read chunk data from OPFS file
13
+ */
14
+ read(fileName: string, batch: ChunkBatch, projectId: string): Promise<ArrayBuffer | null>;
15
+ /**
16
+ * Append chunks to OPFS file (or create new file)
17
+ */
18
+ append(fileName: string, chunks: Array<EncodedVideoChunk | EncodedAudioChunk>, existingBatches: ChunkBatch[] | undefined, projectId: string): Promise<ChunkBatch[]>;
19
+ /**
20
+ * Delete a file from OPFS
21
+ */
22
+ deleteFile(fileName: string, projectId: string): Promise<void>;
23
+ /**
24
+ * Delete entire project directory
25
+ */
26
+ deleteProjectDirectory(projectId: string): Promise<void>;
27
+ /**
28
+ * Check if a file exists in OPFS
29
+ */
30
+ fileExists(fileName: string, projectId: string): Promise<boolean>;
31
+ /**
32
+ * Clear all OPFS data for current project
33
+ */
34
+ clear(projectId: string): Promise<void>;
35
+ private chunkToArrayBuffer;
36
+ }
37
+ //# sourceMappingURL=L2OPFSStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"L2OPFSStore.d.ts","sourceRoot":"","sources":["../../../src/cache/l2/L2OPFSStore.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAExD;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,WAAW,CAAc;;IAM3B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B;;OAEG;IACG,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAe/F;;OAEG;IACG,MAAM,CACV,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,KAAK,CAAC,iBAAiB,GAAG,iBAAiB,CAAC,EACpD,eAAe,EAAE,UAAU,EAAE,GAAG,SAAS,EACzC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,UAAU,EAAE,CAAC;IAkCxB;;OAEG;IACG,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpE;;OAEG;IACG,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9D;;OAEG;IACG,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIvE;;OAEG;IACG,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAI/B,kBAAkB;CAOjC"}
@@ -0,0 +1,89 @@
1
+ import { OPFSManager } from "../storage/opfs/OPFSManager.js";
2
+ class L2OPFSStore {
3
+ opfsManager;
4
+ constructor() {
5
+ this.opfsManager = new OPFSManager();
6
+ }
7
+ async init() {
8
+ await this.opfsManager.init();
9
+ }
10
+ /**
11
+ * Read chunk data from OPFS file
12
+ */
13
+ async read(fileName, batch, projectId) {
14
+ try {
15
+ return await this.opfsManager.readRange(
16
+ { projectId, prefix: "l2", fileName },
17
+ batch.byteOffset,
18
+ batch.byteOffset + batch.byteLength
19
+ );
20
+ } catch (error) {
21
+ if (error?.name === "NotFoundError") {
22
+ return null;
23
+ }
24
+ throw error;
25
+ }
26
+ }
27
+ /**
28
+ * Append chunks to OPFS file (or create new file)
29
+ */
30
+ async append(fileName, chunks, existingBatches, projectId) {
31
+ const path = { projectId, prefix: "l2", fileName };
32
+ let offset = 0;
33
+ if (existingBatches && existingBatches.length > 0) {
34
+ const lastBatch = existingBatches[existingBatches.length - 1];
35
+ if (lastBatch) {
36
+ offset = lastBatch.byteOffset + lastBatch.byteLength;
37
+ }
38
+ }
39
+ const writable = await this.opfsManager.createWritableStream(path);
40
+ const batches = [];
41
+ for (const chunk of chunks) {
42
+ const data = await this.chunkToArrayBuffer(chunk);
43
+ await writable.write({ type: "write", position: offset, data });
44
+ batches.push({
45
+ startUs: chunk.timestamp,
46
+ durationUs: chunk.duration || 0,
47
+ byteOffset: offset,
48
+ byteLength: data.byteLength,
49
+ type: chunk.type
50
+ });
51
+ offset += data.byteLength;
52
+ }
53
+ await writable.close();
54
+ return batches;
55
+ }
56
+ /**
57
+ * Delete a file from OPFS
58
+ */
59
+ async deleteFile(fileName, projectId) {
60
+ await this.opfsManager.deleteFile({ projectId, prefix: "l2", fileName });
61
+ }
62
+ /**
63
+ * Delete entire project directory
64
+ */
65
+ async deleteProjectDirectory(projectId) {
66
+ await this.opfsManager.deleteProjectDirectory(projectId, "l2");
67
+ }
68
+ /**
69
+ * Check if a file exists in OPFS
70
+ */
71
+ async fileExists(fileName, projectId) {
72
+ return await this.opfsManager.exists({ projectId, prefix: "l2", fileName });
73
+ }
74
+ /**
75
+ * Clear all OPFS data for current project
76
+ */
77
+ async clear(projectId) {
78
+ await this.deleteProjectDirectory(projectId);
79
+ }
80
+ async chunkToArrayBuffer(chunk) {
81
+ const buffer = new ArrayBuffer(chunk.byteLength);
82
+ chunk.copyTo(buffer);
83
+ return buffer;
84
+ }
85
+ }
86
+ export {
87
+ L2OPFSStore
88
+ };
89
+ //# sourceMappingURL=L2OPFSStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"L2OPFSStore.js","sources":["../../../src/cache/l2/L2OPFSStore.ts"],"sourcesContent":["import { OPFSManager } from '../storage/opfs/OPFSManager';\nimport type { ChunkBatch } from '../storage/opfs/types';\n\n/**\n * L2-specific OPFS wrapper\n * Adapts OPFSManager for L2 cache chunk operations\n */\nexport class L2OPFSStore {\n private opfsManager: OPFSManager;\n\n constructor() {\n this.opfsManager = new OPFSManager();\n }\n\n async init(): Promise<void> {\n await this.opfsManager.init();\n }\n\n /**\n * Read chunk data from OPFS file\n */\n async read(fileName: string, batch: ChunkBatch, projectId: string): Promise<ArrayBuffer | null> {\n try {\n return await this.opfsManager.readRange(\n { projectId, prefix: 'l2', fileName },\n batch.byteOffset,\n batch.byteOffset + batch.byteLength\n );\n } catch (error) {\n if ((error as any)?.name === 'NotFoundError') {\n return null;\n }\n throw error;\n }\n }\n\n /**\n * Append chunks to OPFS file (or create new file)\n */\n async append(\n fileName: string,\n chunks: Array<EncodedVideoChunk | EncodedAudioChunk>,\n existingBatches: ChunkBatch[] | undefined,\n projectId: string\n ): Promise<ChunkBatch[]> {\n const path = { projectId, prefix: 'l2' as const, fileName };\n\n // Calculate starting offset from existing batches\n let offset = 0;\n if (existingBatches && existingBatches.length > 0) {\n const lastBatch = existingBatches[existingBatches.length - 1];\n if (lastBatch) {\n offset = lastBatch.byteOffset + lastBatch.byteLength;\n }\n }\n\n const writable = await this.opfsManager.createWritableStream(path);\n const batches: ChunkBatch[] = [];\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 type: chunk.type,\n });\n\n offset += data.byteLength;\n }\n\n await writable.close();\n return batches;\n }\n\n /**\n * Delete a file from OPFS\n */\n async deleteFile(fileName: string, projectId: string): Promise<void> {\n await this.opfsManager.deleteFile({ projectId, prefix: 'l2', fileName });\n }\n\n /**\n * Delete entire project directory\n */\n async deleteProjectDirectory(projectId: string): Promise<void> {\n await this.opfsManager.deleteProjectDirectory(projectId, 'l2');\n }\n\n /**\n * Check if a file exists in OPFS\n */\n async fileExists(fileName: string, projectId: string): Promise<boolean> {\n return await this.opfsManager.exists({ projectId, prefix: 'l2', fileName });\n }\n\n /**\n * Clear all OPFS data for current project\n */\n async clear(projectId: string): Promise<void> {\n await this.deleteProjectDirectory(projectId);\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"],"names":[],"mappings":";AAOO,MAAM,YAAY;AAAA,EACf;AAAA,EAER,cAAc;AACZ,SAAK,cAAc,IAAI,YAAA;AAAA,EACzB;AAAA,EAEA,MAAM,OAAsB;AAC1B,UAAM,KAAK,YAAY,KAAA;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,KAAK,UAAkB,OAAmB,WAAgD;AAC9F,QAAI;AACF,aAAO,MAAM,KAAK,YAAY;AAAA,QAC5B,EAAE,WAAW,QAAQ,MAAM,SAAA;AAAA,QAC3B,MAAM;AAAA,QACN,MAAM,aAAa,MAAM;AAAA,MAAA;AAAA,IAE7B,SAAS,OAAO;AACd,UAAK,OAAe,SAAS,iBAAiB;AAC5C,eAAO;AAAA,MACT;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OACJ,UACA,QACA,iBACA,WACuB;AACvB,UAAM,OAAO,EAAE,WAAW,QAAQ,MAAe,SAAA;AAGjD,QAAI,SAAS;AACb,QAAI,mBAAmB,gBAAgB,SAAS,GAAG;AACjD,YAAM,YAAY,gBAAgB,gBAAgB,SAAS,CAAC;AAC5D,UAAI,WAAW;AACb,iBAAS,UAAU,aAAa,UAAU;AAAA,MAC5C;AAAA,IACF;AAEA,UAAM,WAAW,MAAM,KAAK,YAAY,qBAAqB,IAAI;AACjE,UAAM,UAAwB,CAAA;AAE9B,eAAW,SAAS,QAAQ;AAC1B,YAAM,OAAO,MAAM,KAAK,mBAAmB,KAAK;AAChD,YAAM,SAAS,MAAM,EAAE,MAAM,SAAS,UAAU,QAAQ,MAAM;AAE9D,cAAQ,KAAK;AAAA,QACX,SAAS,MAAM;AAAA,QACf,YAAY,MAAM,YAAY;AAAA,QAC9B,YAAY;AAAA,QACZ,YAAY,KAAK;AAAA,QACjB,MAAM,MAAM;AAAA,MAAA,CACb;AAED,gBAAU,KAAK;AAAA,IACjB;AAEA,UAAM,SAAS,MAAA;AACf,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAAW,UAAkB,WAAkC;AACnE,UAAM,KAAK,YAAY,WAAW,EAAE,WAAW,QAAQ,MAAM,UAAU;AAAA,EACzE;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,uBAAuB,WAAkC;AAC7D,UAAM,KAAK,YAAY,uBAAuB,WAAW,IAAI;AAAA,EAC/D;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAAW,UAAkB,WAAqC;AACtE,WAAO,MAAM,KAAK,YAAY,OAAO,EAAE,WAAW,QAAQ,MAAM,UAAU;AAAA,EAC5E;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,MAAM,WAAkC;AAC5C,UAAM,KAAK,uBAAuB,SAAS;AAAA,EAC7C;AAAA,EAEA,MAAc,mBACZ,OACsB;AACtB,UAAM,SAAS,IAAI,YAAY,MAAM,UAAU;AAC/C,UAAM,OAAO,MAAM;AACnB,WAAO;AAAA,EACT;AACF;"}
@@ -0,0 +1,52 @@
1
+ import { TimeUs } from '../../model/types';
2
+
3
+ export interface AudioSampleRecord {
4
+ resourceId: string;
5
+ samples: EncodedAudioChunk[];
6
+ metadata: AudioDecoderConfig;
7
+ totalBytes: number;
8
+ durationUs: TimeUs;
9
+ }
10
+ /**
11
+ * AudioSampleCache - Resource-level audio sample cache
12
+ *
13
+ * Caches all encoded audio samples from MP4 in memory
14
+ * Audio data is small (~100KB for 30s AAC, ~6MB for 30min)
15
+ *
16
+ * Strategy:
17
+ * - Extract all audio samples during moov parsing (one-time cost)
18
+ * - Keep encoded chunks in memory (compressed)
19
+ * - Decode to PCM on-demand when clip is played
20
+ */
21
+ export declare class AudioSampleCache {
22
+ private cache;
23
+ /**
24
+ * Set audio samples for a resource
25
+ */
26
+ set(resourceId: string, samples: EncodedAudioChunk[], metadata: AudioDecoderConfig): void;
27
+ /**
28
+ * Get audio record for a resource
29
+ */
30
+ get(resourceId: string): AudioSampleRecord | null;
31
+ /**
32
+ * Check if resource has audio samples
33
+ */
34
+ has(resourceId: string): boolean;
35
+ /**
36
+ * Get samples in time range (for clip trimming)
37
+ */
38
+ getSamplesInRange(resourceId: string, startUs: TimeUs, endUs: TimeUs): EncodedAudioChunk[];
39
+ /**
40
+ * Delete audio samples for a resource
41
+ */
42
+ delete(resourceId: string): void;
43
+ /**
44
+ * Clear all cached audio samples
45
+ */
46
+ clear(): void;
47
+ /**
48
+ * Get total memory usage
49
+ */
50
+ getTotalBytes(): number;
51
+ }
52
+ //# sourceMappingURL=AudioSampleCache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AudioSampleCache.d.ts","sourceRoot":"","sources":["../../../src/cache/resource/AudioSampleCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,KAAK,CAAwC;IAErD;;OAEG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,EAAE,EAAE,QAAQ,EAAE,kBAAkB,GAAG,IAAI;IAgBzF;;OAEG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAIjD;;OAEG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAIhC;;OAEG;IACH,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,iBAAiB,EAAE;IAW1F;;OAEG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAShC;;OAEG;IACH,KAAK,IAAI,IAAI;IAMb;;OAEG;IACH,aAAa,IAAI,MAAM;CAOxB"}
@@ -0,0 +1,69 @@
1
+ class AudioSampleCache {
2
+ cache = /* @__PURE__ */ new Map();
3
+ /**
4
+ * Set audio samples for a resource
5
+ */
6
+ set(resourceId, samples, metadata) {
7
+ const totalBytes = samples.reduce((sum, s) => sum + s.byteLength, 0);
8
+ const durationUs = samples.length > 0 ? samples[samples.length - 1].timestamp + (samples[samples.length - 1].duration ?? 0) : 0;
9
+ this.cache.set(resourceId, {
10
+ resourceId,
11
+ samples,
12
+ metadata,
13
+ totalBytes,
14
+ durationUs
15
+ });
16
+ }
17
+ /**
18
+ * Get audio record for a resource
19
+ */
20
+ get(resourceId) {
21
+ return this.cache.get(resourceId) || null;
22
+ }
23
+ /**
24
+ * Check if resource has audio samples
25
+ */
26
+ has(resourceId) {
27
+ return this.cache.has(resourceId);
28
+ }
29
+ /**
30
+ * Get samples in time range (for clip trimming)
31
+ */
32
+ getSamplesInRange(resourceId, startUs, endUs) {
33
+ const record = this.cache.get(resourceId);
34
+ if (!record) return [];
35
+ return record.samples.filter((s) => {
36
+ const sampleEndUs = s.timestamp + (s.duration ?? 0);
37
+ return s.timestamp < endUs && sampleEndUs > startUs;
38
+ });
39
+ }
40
+ /**
41
+ * Delete audio samples for a resource
42
+ */
43
+ delete(resourceId) {
44
+ const record = this.cache.get(resourceId);
45
+ if (record) {
46
+ this.cache.delete(resourceId);
47
+ }
48
+ }
49
+ /**
50
+ * Clear all cached audio samples
51
+ */
52
+ clear() {
53
+ this.cache.clear();
54
+ }
55
+ /**
56
+ * Get total memory usage
57
+ */
58
+ getTotalBytes() {
59
+ let total = 0;
60
+ for (const record of this.cache.values()) {
61
+ total += record.totalBytes;
62
+ }
63
+ return total;
64
+ }
65
+ }
66
+ export {
67
+ AudioSampleCache
68
+ };
69
+ //# sourceMappingURL=AudioSampleCache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AudioSampleCache.js","sources":["../../../src/cache/resource/AudioSampleCache.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\n\nexport interface AudioSampleRecord {\n resourceId: string;\n samples: EncodedAudioChunk[];\n metadata: AudioDecoderConfig;\n totalBytes: number;\n durationUs: TimeUs;\n}\n\n/**\n * AudioSampleCache - Resource-level audio sample cache\n *\n * Caches all encoded audio samples from MP4 in memory\n * Audio data is small (~100KB for 30s AAC, ~6MB for 30min)\n *\n * Strategy:\n * - Extract all audio samples during moov parsing (one-time cost)\n * - Keep encoded chunks in memory (compressed)\n * - Decode to PCM on-demand when clip is played\n */\nexport class AudioSampleCache {\n private cache = new Map<string, AudioSampleRecord>();\n\n /**\n * Set audio samples for a resource\n */\n set(resourceId: string, samples: EncodedAudioChunk[], metadata: AudioDecoderConfig): void {\n const totalBytes = samples.reduce((sum, s) => sum + s.byteLength, 0);\n const durationUs =\n samples.length > 0\n ? samples[samples.length - 1]!.timestamp + (samples[samples.length - 1]!.duration ?? 0)\n : 0;\n\n this.cache.set(resourceId, {\n resourceId,\n samples,\n metadata,\n totalBytes,\n durationUs,\n });\n }\n\n /**\n * Get audio record for a resource\n */\n get(resourceId: string): AudioSampleRecord | null {\n return this.cache.get(resourceId) || null;\n }\n\n /**\n * Check if resource has audio samples\n */\n has(resourceId: string): boolean {\n return this.cache.has(resourceId);\n }\n\n /**\n * Get samples in time range (for clip trimming)\n */\n getSamplesInRange(resourceId: string, startUs: TimeUs, endUs: TimeUs): EncodedAudioChunk[] {\n const record = this.cache.get(resourceId);\n if (!record) return [];\n\n return record.samples.filter((s) => {\n const sampleEndUs = s.timestamp + (s.duration ?? 0);\n // Include sample if it overlaps with range\n return s.timestamp < endUs && sampleEndUs > startUs;\n });\n }\n\n /**\n * Delete audio samples for a resource\n */\n delete(resourceId: string): void {\n const record = this.cache.get(resourceId);\n if (record) {\n // Note: EncodedAudioChunk doesn't have close() method\n // Chunks will be garbage collected automatically\n this.cache.delete(resourceId);\n }\n }\n\n /**\n * Clear all cached audio samples\n */\n clear(): void {\n // Note: EncodedAudioChunk doesn't have close() method\n // Chunks will be garbage collected automatically\n this.cache.clear();\n }\n\n /**\n * Get total memory usage\n */\n getTotalBytes(): number {\n let total = 0;\n for (const record of this.cache.values()) {\n total += record.totalBytes;\n }\n return total;\n }\n}\n"],"names":[],"mappings":"AAqBO,MAAM,iBAAiB;AAAA,EACpB,4BAAY,IAAA;AAAA;AAAA;AAAA;AAAA,EAKpB,IAAI,YAAoB,SAA8B,UAAoC;AACxF,UAAM,aAAa,QAAQ,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,YAAY,CAAC;AACnE,UAAM,aACJ,QAAQ,SAAS,IACb,QAAQ,QAAQ,SAAS,CAAC,EAAG,aAAa,QAAQ,QAAQ,SAAS,CAAC,EAAG,YAAY,KACnF;AAEN,SAAK,MAAM,IAAI,YAAY;AAAA,MACzB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA,CACD;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,YAA8C;AAChD,WAAO,KAAK,MAAM,IAAI,UAAU,KAAK;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,YAA6B;AAC/B,WAAO,KAAK,MAAM,IAAI,UAAU;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA,EAKA,kBAAkB,YAAoB,SAAiB,OAAoC;AACzF,UAAM,SAAS,KAAK,MAAM,IAAI,UAAU;AACxC,QAAI,CAAC,OAAQ,QAAO,CAAA;AAEpB,WAAO,OAAO,QAAQ,OAAO,CAAC,MAAM;AAClC,YAAM,cAAc,EAAE,aAAa,EAAE,YAAY;AAEjD,aAAO,EAAE,YAAY,SAAS,cAAc;AAAA,IAC9C,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,YAA0B;AAC/B,UAAM,SAAS,KAAK,MAAM,IAAI,UAAU;AACxC,QAAI,QAAQ;AAGV,WAAK,MAAM,OAAO,UAAU;AAAA,IAC9B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AAGZ,SAAK,MAAM,MAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAwB;AACtB,QAAI,QAAQ;AACZ,eAAW,UAAU,KAAK,MAAM,OAAA,GAAU;AACxC,eAAS,OAAO;AAAA,IAClB;AACA,WAAO;AAAA,EACT;AACF;"}