@meframe/core 0.0.13 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Meframe.d.ts.map +1 -1
- package/dist/Meframe.js +3 -3
- package/dist/Meframe.js.map +1 -1
- package/dist/cache/CacheManager.d.ts +8 -21
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +132 -156
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/cache/L2Cache.d.ts.map +1 -1
- package/dist/cache/L2Cache.js +21 -13
- package/dist/cache/L2Cache.js.map +1 -1
- package/dist/cache/l1/VideoL1Cache.d.ts +4 -5
- package/dist/cache/l1/VideoL1Cache.d.ts.map +1 -1
- package/dist/cache/l1/VideoL1Cache.js +26 -33
- package/dist/cache/l1/VideoL1Cache.js.map +1 -1
- package/dist/controllers/PlaybackController.d.ts +3 -1
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +43 -24
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/controllers/PreRenderService.d.ts.map +1 -1
- package/dist/controllers/PreRenderService.js +6 -5
- package/dist/controllers/PreRenderService.js.map +1 -1
- package/dist/model/CompositionModel.d.ts +8 -0
- package/dist/model/CompositionModel.d.ts.map +1 -1
- package/dist/model/CompositionModel.js +61 -14
- package/dist/model/CompositionModel.js.map +1 -1
- package/dist/model/dirty-range.d.ts.map +1 -1
- package/dist/model/patch.d.ts.map +1 -1
- package/dist/model/patch.js +69 -14
- package/dist/model/patch.js.map +1 -1
- package/dist/model/types.d.ts +23 -3
- package/dist/model/types.d.ts.map +1 -1
- package/dist/model/types.js +15 -0
- package/dist/model/types.js.map +1 -0
- package/dist/model/validation.d.ts.map +1 -1
- package/dist/model/validation.js +21 -4
- package/dist/model/validation.js.map +1 -1
- package/dist/orchestrator/ClipSessionManager.d.ts +1 -1
- package/dist/orchestrator/ClipSessionManager.d.ts.map +1 -1
- package/dist/orchestrator/ClipSessionManager.js +3 -4
- package/dist/orchestrator/ClipSessionManager.js.map +1 -1
- package/dist/orchestrator/CompositionPlanner.d.ts.map +1 -1
- package/dist/orchestrator/CompositionPlanner.js +9 -3
- package/dist/orchestrator/CompositionPlanner.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts +6 -2
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +41 -34
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts +3 -1
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +42 -27
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/orchestrator/types.d.ts +1 -0
- package/dist/orchestrator/types.d.ts.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.d.ts.map +1 -1
- package/dist/stages/compose/GlobalAudioSession.js +10 -2
- package/dist/stages/compose/GlobalAudioSession.js.map +1 -1
- package/dist/stages/decode/BaseDecoder.d.ts.map +1 -1
- package/dist/stages/decode/BaseDecoder.js +1 -1
- package/dist/stages/decode/BaseDecoder.js.map +1 -1
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +15 -17
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/utils/errors.d.ts +12 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +11 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/worker/WorkerPool.d.ts +2 -2
- package/dist/worker/WorkerPool.d.ts.map +1 -1
- package/dist/worker/WorkerPool.js +7 -3
- package/dist/worker/WorkerPool.js.map +1 -1
- package/dist/workers/BaseDecoder.js +1 -1
- package/dist/workers/BaseDecoder.js.map +1 -1
- package/dist/workers/stages/compose/video-compose.worker.js.map +1 -1
- package/dist/workers/stages/demux/video-demux.worker.js +1 -1
- package/dist/workers/stages/demux/video-demux.worker.js.map +1 -1
- package/package.json +1 -1
|
@@ -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 './L2Cache';\nimport { MeframeEvent } from '../event/events';\nimport type { EventBus } from '../event/EventBus';\nimport type { EventPayloadMap } from '../event/events';\nimport { AudioL1Cache } from './l1/AudioL1Cache';\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 WaitForFrameOptions {\n signal?: AbortSignal;\n timeoutMs?: number;\n toleranceUs?: number;\n}\n\ninterface FrameWaiter {\n requestKey: string;\n clipId: string;\n targetTimeUs: TimeUs;\n resolve: (result: WaitForFrameResult) => void;\n reject: (reason?: unknown) => void;\n toleranceUs: number;\n timeoutId?: ReturnType<typeof setTimeout>;\n abortCleanup?: () => void;\n}\n\ninterface ClipReadyWaiter {\n clipId: string;\n minFrameCount: number;\n resolve: (ready: boolean) => void;\n reject: (reason?: unknown) => void;\n timeoutId?: ReturnType<typeof setTimeout>;\n}\n\nconst DEFAULT_WAIT_TOLERANCE_US = 33_333; // ≈1 frame @30fps\n\nexport interface WaitForFrameResult {\n frame: RcFrame | null;\n source: 'l1' | 'wait';\n timestampUs: TimeUs;\n clipId: string;\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 pendingFramePromises = new Map<string, Promise<WaitForFrameResult>>();\n private frameWaiters = new Map<string, Set<FrameWaiter>>();\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 | { frame: VideoFrame; metadata?: any }>,\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 as any).frame || value;\n const metadata = (value as any).metadata;\n const gopSerial = metadata?.gopSerial;\n const isKeyframe = metadata?.isKeyframe;\n\n const timestamp = frame.timestamp ?? 0;\n\n const rcFrame = this.videoL1Cache.addFrame(\n frame,\n params.clipId,\n frameDuration,\n params.trackId,\n gopSerial,\n isKeyframe\n );\n if (!rcFrame) {\n await process();\n return;\n }\n\n this.notifyFrameWaiters(params.clipId, timestamp, frameDuration, rcFrame);\n\n // Calculate global time for event emission\n const globalTimeUs = (params.clipStartUs ?? 0) + timestamp;\n\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 throw 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 throw error;\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(\n clipId: string,\n audioData: AudioData,\n clipStartUs: TimeUs,\n clipDurationUs: TimeUs\n ): void {\n this.audioL1Cache.putClipAudioData(clipId, audioData, clipStartUs, 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.reset();\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 console.log(`[CacheManager] invalidateClip(${clipId}) - clearing L1 and L2`);\n this.videoL1Cache.invalidateRange(0, Infinity, clipId);\n await this.l2Cache.invalidateClip(clipId);\n }\n\n /**\n * Evict a clip from L1 cache\n */\n evictClip(clipId: string): void {\n this.videoL1Cache.evictClip(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 */\n waitForClipReady(\n clipId: string,\n options: { minFrameCount?: number; timeoutMs?: number } = {}\n ): Promise<boolean> {\n const minFrameCount = options.minFrameCount ?? 30;\n const currentFrameCount = this.videoL1Cache.getClipFrameCount(clipId);\n\n if (currentFrameCount >= minFrameCount) {\n return Promise.resolve(true);\n }\n\n return new Promise<boolean>((resolve, reject) => {\n const waiter: ClipReadyWaiter = {\n clipId,\n minFrameCount,\n resolve,\n reject,\n };\n\n const waiters = this.clipReadyWaiters.get(clipId) || [];\n waiters.push(waiter);\n this.clipReadyWaiters.set(clipId, waiters);\n\n if (options.timeoutMs && options.timeoutMs > 0) {\n waiter.timeoutId = setTimeout(() => {\n const waiters = this.clipReadyWaiters.get(clipId);\n if (waiters) {\n const remaining = waiters.filter((w) => w !== waiter);\n if (remaining.length === 0) {\n this.clipReadyWaiters.delete(clipId);\n } else {\n this.clipReadyWaiters.set(clipId, remaining);\n }\n }\n resolve(false);\n }, options.timeoutMs);\n }\n });\n }\n\n async clear(): Promise<void> {\n this.videoL1Cache.clear();\n await this.l2Cache.clear();\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 private notifyFrameWaiters(\n clipId: string,\n timestampUs: TimeUs,\n frameDurationUs: TimeUs,\n frame: RcFrame\n ): void {\n const waiters = this.frameWaiters.get(clipId);\n if (!waiters || waiters.size === 0) {\n return;\n }\n\n const resolved: FrameWaiter[] = [];\n\n for (const waiter of waiters) {\n const matches = this.matchesTimestamp(\n waiter.targetTimeUs,\n timestampUs,\n frameDurationUs,\n waiter.toleranceUs\n );\n\n if (!matches) continue;\n\n resolved.push(waiter);\n this.cleanupWaiter(waiter);\n\n waiter.resolve({\n frame,\n source: 'wait',\n timestampUs,\n clipId,\n });\n }\n\n for (const waiter of resolved) {\n waiters.delete(waiter);\n }\n\n if (waiters.size === 0) {\n this.frameWaiters.delete(clipId);\n }\n\n this.notifyClipReadyWaiters(clipId);\n }\n\n private notifyClipReadyWaiters(clipId: string): void {\n const waiters = this.clipReadyWaiters.get(clipId);\n if (!waiters || waiters.length === 0) {\n return;\n }\n\n const frameCount = this.videoL1Cache.getClipFrameCount(clipId);\n const resolved: ClipReadyWaiter[] = [];\n\n for (const waiter of waiters) {\n if (frameCount >= waiter.minFrameCount) {\n resolved.push(waiter);\n if (waiter.timeoutId) {\n clearTimeout(waiter.timeoutId);\n }\n waiter.resolve(true);\n }\n }\n\n const remaining = waiters.filter((w) => !resolved.includes(w));\n if (remaining.length === 0) {\n this.clipReadyWaiters.delete(clipId);\n } else {\n this.clipReadyWaiters.set(clipId, remaining);\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":["waiters"],"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 './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 | { frame: VideoFrame; metadata?: any }>,\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 as any).frame || value;\n const metadata = (value as any).metadata;\n const gopSerial = metadata?.gopSerial;\n const isKeyframe = metadata?.isKeyframe;\n\n const timestamp = frame.timestamp ?? 0;\n\n const rcFrame = this.videoL1Cache.addFrame(\n frame,\n params.clipId,\n frameDuration,\n params.trackId,\n gopSerial,\n isKeyframe\n );\n if (!rcFrame) {\n await process();\n return;\n }\n\n // Calculate global time for event emission\n const globalTimeUs = (params.clipStartUs ?? 0) + timestamp;\n\n this.checkAndNotifyClipReady(params.clipId, timestamp);\n\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 throw 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 throw error;\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(\n clipId: string,\n audioData: AudioData,\n clipStartUs: TimeUs,\n clipDurationUs: TimeUs\n ): void {\n this.audioL1Cache.putClipAudioData(clipId, audioData, clipStartUs, 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.reset();\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 async clear(): Promise<void> {\n this.videoL1Cache.clear();\n await this.l2Cache.clear();\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 * 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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"L2Cache.d.ts","sourceRoot":"","sources":["../../src/cache/L2Cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAoC7C,UAAU,QAAQ;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,OAAO;IAClB,OAAO,CAAC,EAAE,CAA4B;IACtC,OAAO,CAAC,QAAQ,CAA0C;IAC1D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,WAAW,CAA8B;gBAErC,MAAM,EAAE,QAAQ;IAKtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAOrB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,iBAAiB,GAAG,IAAI,CAAC;IA+B1F,GAAG,CACP,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,KAAK,CAAC,iBAAiB,GAAG,iBAAiB,CAAC,EACpD,KAAK,EAAE,OAAO,GAAG,OAAO,EACxB,OAAO,CAAC,EAAE;QACR,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,EAAE,GAAG,CAAC;KAChB,GACA,OAAO,CAAC,IAAI,CAAC;IA0FV,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8CrF;;OAEG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAYzE;;OAEG;IACG,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAgBjF;;OAEG;IACG,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAwBrE,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBnD;;;OAGG;IACG,gBAAgB,CACpB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,GAAG,OAAO,GACvB,OAAO,CAAC,cAAc,CAAC,iBAAiB,GAAG,iBAAiB,CAAC,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"L2Cache.d.ts","sourceRoot":"","sources":["../../src/cache/L2Cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAoC7C,UAAU,QAAQ;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,OAAO;IAClB,OAAO,CAAC,EAAE,CAA4B;IACtC,OAAO,CAAC,QAAQ,CAA0C;IAC1D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,WAAW,CAA8B;gBAErC,MAAM,EAAE,QAAQ;IAKtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAOrB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,iBAAiB,GAAG,IAAI,CAAC;IA+B1F,GAAG,CACP,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,KAAK,CAAC,iBAAiB,GAAG,iBAAiB,CAAC,EACpD,KAAK,EAAE,OAAO,GAAG,OAAO,EACxB,OAAO,CAAC,EAAE;QACR,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,EAAE,GAAG,CAAC;KAChB,GACA,OAAO,CAAC,IAAI,CAAC;IA0FV,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8CrF;;OAEG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAYzE;;OAEG;IACG,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAgBjF;;OAEG;IACG,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAwBrE,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBnD;;;OAGG;IACG,gBAAgB,CACpB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,GAAG,OAAO,GACvB,OAAO,CAAC,cAAc,CAAC,iBAAiB,GAAG,iBAAiB,CAAC,GAAG,IAAI,CAAC;IA8DlE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAkCd,WAAW;YA8BX,YAAY;IAY1B;;;OAGG;YACW,YAAY;YA4CZ,kBAAkB;IAQhC,OAAO,CAAC,WAAW;YAwBL,gBAAgB;YAahB,WAAW;YAgCX,YAAY;YAsCZ,cAAc;IAsB5B,OAAO,CAAC,gBAAgB;IAOxB,WAAW,IAAI;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;KACjB;IAWK,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWzD;;OAEG;IACG,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;CAWrF"}
|
package/dist/cache/L2Cache.js
CHANGED
|
@@ -224,20 +224,28 @@ class L2Cache {
|
|
|
224
224
|
controller.close();
|
|
225
225
|
return;
|
|
226
226
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
227
|
+
try {
|
|
228
|
+
const chunkData = await this.readFromOPFS(record.fileName, batch);
|
|
229
|
+
if (!chunkData) {
|
|
230
|
+
controller.close();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const chunk = this.createChunk(
|
|
234
|
+
chunkData,
|
|
235
|
+
batch.startUs,
|
|
236
|
+
track,
|
|
237
|
+
batch.type,
|
|
238
|
+
batch.durationUs
|
|
239
|
+
);
|
|
240
|
+
controller.enqueue(chunk);
|
|
241
|
+
batchIndex++;
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error instanceof DOMException && error.name === "NotFoundError") {
|
|
244
|
+
controller.close();
|
|
245
|
+
} else {
|
|
246
|
+
controller.error(error);
|
|
247
|
+
}
|
|
231
248
|
}
|
|
232
|
-
const chunk = this.createChunk(
|
|
233
|
-
chunkData,
|
|
234
|
-
batch.startUs,
|
|
235
|
-
track,
|
|
236
|
-
batch.type,
|
|
237
|
-
batch.durationUs
|
|
238
|
-
);
|
|
239
|
-
controller.enqueue(chunk);
|
|
240
|
-
batchIndex++;
|
|
241
249
|
}
|
|
242
250
|
});
|
|
243
251
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"L2Cache.js","sources":["../../src/cache/L2Cache.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\n// import type { ClipMetadata } from './types';\nimport { binarySearchRange } from '../utils/binary-search';\n\ninterface ChunkBatch {\n startUs: TimeUs;\n durationUs: TimeUs;\n byteOffset: number;\n byteLength: number;\n type: 'key' | 'delta'; // Store chunk type for correct reconstruction\n}\n\ninterface ChunkRecord {\n clipId: string; // IndexedDB keyPath uses clipId\n track: 'video' | 'audio';\n fileName: string;\n batches: ChunkBatch[];\n lastAccess: number;\n totalBytes: number;\n isComplete: boolean; // Mark if clip is fully rendered\n expectedDurationUs?: number; // Expected duration for validation\n metadata?: {\n codec?: string;\n description?: Uint8Array;\n codedWidth?: number;\n codedHeight?: number;\n displayAspectWidth?: number;\n displayAspectHeight?: number;\n colorSpace?: VideoColorSpaceInit;\n hardwareAcceleration?: HardwareAcceleration;\n optimizeForLatency?: boolean;\n sampleRate?: number;\n numberOfChannels?: number;\n };\n}\n\ninterface L2Config {\n maxSizeMB: number;\n projectId: string;\n}\n\nexport class L2Cache {\n private db: IDBDatabase | null = null;\n private opfsRoot: FileSystemDirectoryHandle | null = null;\n readonly maxSize: number;\n readonly projectId: string;\n private initPromise: Promise<void> | null = null;\n\n constructor(config: L2Config) {\n this.maxSize = config.maxSizeMB * 1024 * 1024;\n this.projectId = config.projectId;\n }\n\n async init(): Promise<void> {\n if (this.initPromise) return this.initPromise;\n\n this.initPromise = this.initStorage();\n return this.initPromise;\n }\n\n async get(timeUs: TimeUs, clipId: string): Promise<EncodedVideoChunk | EncodedAudioChunk | null> {\n await this.init();\n\n if (!this.db) return null;\n\n // Query IndexedDB for chunk metadata\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n const records = await this.collectRecords(store, clipId);\n\n for (const record of records) {\n const batch = binarySearchRange(record.batches, timeUs, (b) => ({\n start: b.startUs,\n end: b.startUs + b.durationUs,\n }));\n\n if (!batch) {\n continue;\n }\n\n const chunkData = await this.readFromOPFS(record.fileName, batch);\n if (!chunkData) continue;\n\n this.updateLastAccess(record.clipId, record.track);\n\n return this.createChunk(chunkData, timeUs, record.track, 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 (!this.db || !this.opfsRoot) {\n console.warn(`[L2Cache] put aborted: db=${!!this.db}, opfsRoot=${!!this.opfsRoot}`);\n return;\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 (separate transaction)\n let existingRecord: ChunkRecord | undefined;\n {\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n existingRecord = await this.promisifyRequest<ChunkRecord>(store.get([clipId, track]));\n }\n\n // Step 2: Validate consistency - if IndexedDB has record but OPFS file missing, delete the record\n if (existingRecord) {\n const projectDir = await this.opfsRoot.getDirectoryHandle(\n `meframe-project-${this.projectId}`,\n {\n create: true,\n }\n );\n try {\n await projectDir.getFileHandle(existingRecord.fileName, { create: false });\n } catch (error) {\n if ((error as any)?.name === 'NotFoundError') {\n await this.deleteEntry(clipId, track);\n existingRecord = undefined;\n }\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 3: Write to OPFS (no active transaction)\n const newBatches = await this.appendToOPFS(fileName, chunksToWrite, existingRecord?.batches);\n\n // Step 4: Update IndexedDB (new transaction)\n {\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n\n const record: ChunkRecord = {\n clipId,\n track,\n fileName,\n batches: 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 store.put(record);\n // Wait for transaction to complete, not just the request\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => {\n resolve();\n };\n tx.onerror = () => {\n console.error(`[L2Cache] Transaction error for ${clipId} ${track}:`, tx.error);\n reject(tx.error);\n };\n });\n }\n\n // Check and enforce quota\n await this.enforceQuota();\n }\n\n async invalidateRange(startUs: TimeUs, endUs: TimeUs, clipId?: string): Promise<void> {\n await this.init();\n\n if (!this.db) return;\n\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n const keysToDelete: Array<[string, string]> = [];\n\n // Iterate through all records\n const cursor = store.openCursor();\n await new Promise<void>((resolve) => {\n cursor.onsuccess = (event) => {\n const cursor = (event.target as IDBRequest).result;\n if (!cursor) {\n resolve();\n return;\n }\n\n const record: ChunkRecord = cursor.value;\n\n if (clipId && record.clipId !== clipId) {\n cursor.continue();\n return;\n }\n\n // Check if any batch overlaps with invalidation range\n const hasOverlap = record.batches.some((batch) => {\n const batchEnd = batch.startUs + batch.durationUs;\n return batch.startUs < endUs && batchEnd > startUs;\n });\n\n if (hasOverlap) {\n keysToDelete.push([record.clipId, record.track]);\n }\n\n cursor.continue();\n };\n });\n\n // Delete invalidated entries\n for (const key of keysToDelete) {\n await this.deleteEntry(key[0], key[1]);\n }\n }\n\n /**\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 if (!this.db) return false;\n\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n const record = await this.promisifyRequest<ChunkRecord>(store.get([clipId, track]));\n\n return record !== undefined && 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 if (!this.db) {\n console.warn(`[L2Cache] hasCompleteClip: db not initialized`);\n return false;\n }\n\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n const record = await this.promisifyRequest<ChunkRecord>(store.get([clipId, track]));\n\n const result = record?.isComplete === true;\n return result;\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 if (!this.db) return;\n\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n const record = await this.promisifyRequest<ChunkRecord>(store.get([clipId, track]));\n\n if (record) {\n record.isComplete = true;\n record.lastAccess = Date.now();\n store.put(record);\n // Wait for transaction to complete\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n console.log(`[L2Cache] markComplete(${clipId}, ${track}): marked successfully`);\n } else {\n console.warn(`[L2Cache] markComplete(${clipId}, ${track}): no record found, cannot mark`);\n }\n }\n\n async invalidateClip(clipId: string): Promise<void> {\n await this.init();\n\n if (!this.db) return;\n\n // Collect records to delete\n const recordsToDelete: ChunkRecord[] = [];\n {\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n recordsToDelete.push(...(await this.collectRecords(store, clipId)));\n }\n\n // Delete each record\n for (const record of recordsToDelete) {\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 if (!this.db || !this.opfsRoot) return null;\n\n // Get chunk record\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n const record = await this.promisifyRequest<ChunkRecord>(store.get([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 // Read chunk data from OPFS\n const chunkData = await this.readFromOPFS(record.fileName, batch);\n if (!chunkData) {\n controller.error(new Error(`Failed to read chunk at index ${batchIndex}`));\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 },\n });\n }\n\n async clear(): Promise<void> {\n await this.init();\n\n if (!this.db || !this.opfsRoot) {\n console.warn('[L2Cache] clear() called but db or opfsRoot not available');\n return;\n }\n\n // Clear IndexedDB\n try {\n const tx = this.db.transaction(['chunks', 'meta'], 'readwrite');\n await this.promisifyRequest(tx.objectStore('chunks').clear());\n await this.promisifyRequest(tx.objectStore('meta').clear());\n } catch (error) {\n console.error('[L2Cache] Failed to clear IndexedDB:', error);\n throw error;\n }\n\n // Clear OPFS files\n try {\n const projectDir = await this.opfsRoot.getDirectoryHandle(\n `meframe-project-${this.projectId}`,\n {\n create: false,\n }\n );\n await this.opfsRoot.removeEntry(projectDir.name, { recursive: true });\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 async initStorage(): Promise<void> {\n // Initialize OPFS\n this.opfsRoot = await navigator.storage.getDirectory();\n\n // Initialize IndexedDB\n const request = indexedDB.open('meframe_cache', 1);\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result;\n\n // chunks store with composite key [clipId, track]\n if (!db.objectStoreNames.contains('chunks')) {\n const store = db.createObjectStore('chunks', {\n keyPath: ['clipId', 'track'],\n });\n store.createIndex('lastAccess', 'lastAccess');\n }\n\n // meta store\n if (!db.objectStoreNames.contains('meta')) {\n db.createObjectStore('meta', { keyPath: 'projectId' });\n }\n };\n\n this.db = await new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n }\n\n private async readFromOPFS(fileName: string, batch: ChunkBatch): Promise<ArrayBuffer | null> {\n if (!this.opfsRoot) return null;\n\n const projectDir = await this.opfsRoot.getDirectoryHandle(`meframe-project-${this.projectId}`, {\n create: false,\n });\n const fileHandle = await projectDir.getFileHandle(fileName);\n const file = await fileHandle.getFile();\n const slice = file.slice(batch.byteOffset, batch.byteOffset + batch.byteLength);\n return await slice.arrayBuffer();\n }\n\n /**\n * Append chunks to OPFS file (or create new file)\n * Supports incremental writing for streaming scenarios\n */\n private async appendToOPFS(\n fileName: string,\n chunks: Array<EncodedVideoChunk | EncodedAudioChunk>,\n existingBatches?: ChunkBatch[]\n ): Promise<ChunkBatch[]> {\n if (!this.opfsRoot) return [];\n\n const projectDir = await this.opfsRoot.getDirectoryHandle(`meframe-project-${this.projectId}`, {\n create: true,\n });\n const fileHandle = await projectDir.getFileHandle(fileName, { create: true });\n\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 fileHandle.createWritable({ keepExistingData: true });\n\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 private async chunkToArrayBuffer(\n chunk: EncodedVideoChunk | EncodedAudioChunk\n ): Promise<ArrayBuffer> {\n const buffer = new ArrayBuffer(chunk.byteLength);\n chunk.copyTo(buffer);\n return buffer;\n }\n\n private createChunk(\n data: ArrayBuffer,\n timeUs: TimeUs,\n track: 'video' | 'audio',\n 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 updateLastAccess(clipId: string, track: string): Promise<void> {\n if (!this.db) return;\n\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n const record = await this.promisifyRequest<ChunkRecord>(store.get([clipId, track]));\n\n if (record) {\n record.lastAccess = Date.now();\n await this.promisifyRequest(store.put(record));\n }\n }\n\n private async deleteEntry(clipId: string, track: string): Promise<void> {\n if (!this.db) return;\n\n // Step 1: Get record info\n let record: ChunkRecord | undefined;\n {\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n record = await this.promisifyRequest<ChunkRecord>(store.get([clipId, track]));\n }\n\n // Step 2: Delete OPFS file (outside transaction)\n if (record && this.opfsRoot) {\n try {\n const projectDir = await this.opfsRoot.getDirectoryHandle(\n `meframe-project-${this.projectId}`,\n { create: false }\n );\n await projectDir.removeEntry(record.fileName);\n } catch (error) {\n console.warn(`[L2Cache] Failed to delete OPFS file ${record.fileName}:`, error);\n }\n }\n\n // Step 3: Delete IndexedDB record (new transaction)\n {\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n await this.promisifyRequest(store.delete([clipId, track]));\n }\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 if (!this.db) return;\n\n // Delete oldest entries until under quota\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n const index = store.index('lastAccess');\n\n let bytesDeleted = 0;\n const toDelete = usage - this.maxSize;\n\n const cursor = index.openCursor();\n await new Promise<void>((resolve) => {\n cursor.onsuccess = async (event) => {\n const cursor = (event.target as IDBRequest).result;\n if (!cursor || bytesDeleted >= toDelete) {\n resolve();\n return;\n }\n\n const record: ChunkRecord = cursor.value;\n await this.deleteEntry(record.clipId, record.track);\n bytesDeleted += record.totalBytes;\n\n cursor.continue();\n };\n });\n }\n\n private async collectRecords(store: IDBObjectStore, clipId: string): Promise<ChunkRecord[]> {\n const records: ChunkRecord[] = [];\n const cursor = store.openCursor();\n await new Promise<void>((resolve) => {\n cursor.onsuccess = (event) => {\n const cursor = (event.target as IDBRequest).result;\n if (!cursor) {\n resolve();\n return;\n }\n\n const record: ChunkRecord = cursor.value;\n if (record.clipId === clipId) {\n records.push(record);\n }\n\n cursor.continue();\n };\n });\n return records;\n }\n\n private promisifyRequest<T>(request: IDBRequest): Promise<T> {\n return new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n }\n\n getMetadata(): {\n maxSizeMB: number;\n usedSizeMB: number;\n entries: number;\n hitRate: number;\n } {\n // This is a simplified implementation\n // In a real implementation, we would track actual usage\n return {\n maxSizeMB: this.maxSize / (1024 * 1024),\n usedSizeMB: 0, // Would need to track actual usage\n entries: 0, // Would need to track actual entries\n hitRate: 0, // Would need to track hits and misses\n };\n }\n\n async hasAvailableQuota(sizeMB: number): Promise<boolean> {\n if (typeof navigator === 'undefined' || !navigator.storage?.estimate) {\n // L2Cache requires storage API to function\n throw new Error('Storage API not available');\n }\n\n const estimate = await navigator.storage.estimate();\n const availableMB = ((estimate.quota || 0) - (estimate.usage || 0)) / (1024 * 1024);\n return availableMB >= sizeMB;\n }\n\n /**\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 if (!this.db) return null;\n\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n const record = await this.promisifyRequest<ChunkRecord>(store.get([clipId, track]));\n\n return record?.metadata || null;\n }\n}\n"],"names":["cursor"],"mappings":";AAyCO,MAAM,QAAQ;AAAA,EACX,KAAyB;AAAA,EACzB,WAA6C;AAAA,EAC5C;AAAA,EACA;AAAA,EACD,cAAoC;AAAA,EAE5C,YAAY,QAAkB;AAC5B,SAAK,UAAU,OAAO,YAAY,OAAO;AACzC,SAAK,YAAY,OAAO;AAAA,EAC1B;AAAA,EAEA,MAAM,OAAsB;AAC1B,QAAI,KAAK,YAAa,QAAO,KAAK;AAElC,SAAK,cAAc,KAAK,YAAA;AACxB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAM,IAAI,QAAgB,QAAuE;AAC/F,UAAM,KAAK,KAAA;AAEX,QAAI,CAAC,KAAK,GAAI,QAAO;AAGrB,UAAM,KAAK,KAAK,GAAG,YAAY,UAAU,UAAU;AACnD,UAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,UAAM,UAAU,MAAM,KAAK,eAAe,OAAO,MAAM;AAEvD,eAAW,UAAU,SAAS;AAC5B,YAAM,QAAQ,kBAAkB,OAAO,SAAS,QAAQ,CAAC,OAAO;AAAA,QAC9D,OAAO,EAAE;AAAA,QACT,KAAK,EAAE,UAAU,EAAE;AAAA,MAAA,EACnB;AAEF,UAAI,CAAC,OAAO;AACV;AAAA,MACF;AAEA,YAAM,YAAY,MAAM,KAAK,aAAa,OAAO,UAAU,KAAK;AAChE,UAAI,CAAC,UAAW;AAEhB,WAAK,iBAAiB,OAAO,QAAQ,OAAO,KAAK;AAEjD,aAAO,KAAK,YAAY,WAAW,QAAQ,OAAO,OAAO,MAAM,MAAM,MAAM,UAAU;AAAA,IACvF;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,IACJ,QACA,QACA,OACA,SAKe;AACf,UAAM,KAAK,KAAA;AAEX,QAAI,CAAC,KAAK,MAAM,CAAC,KAAK,UAAU;AAC9B,cAAQ,KAAK,6BAA6B,CAAC,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,KAAK,QAAQ,EAAE;AAClF;AAAA,IACF;AACA,QAAI,OAAO,WAAW,EAAG;AAEzB,UAAM,WAAW,QAAQ,MAAM,IAAI,MAAM,CAAC,CAAC,KAAK,UAAU,UAAU,SAAS,KAAK;AAGlF,QAAI;AACJ;AACE,YAAM,KAAK,KAAK,GAAG,YAAY,UAAU,UAAU;AACnD,YAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,uBAAiB,MAAM,KAAK,iBAA8B,MAAM,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC;AAAA,IACtF;AAGA,QAAI,gBAAgB;AAClB,YAAM,aAAa,MAAM,KAAK,SAAS;AAAA,QACrC,mBAAmB,KAAK,SAAS;AAAA,QACjC;AAAA,UACE,QAAQ;AAAA,QAAA;AAAA,MACV;AAEF,UAAI;AACF,cAAM,WAAW,cAAc,eAAe,UAAU,EAAE,QAAQ,OAAO;AAAA,MAC3E,SAAS,OAAO;AACd,YAAK,OAAe,SAAS,iBAAiB;AAC5C,gBAAM,KAAK,YAAY,QAAQ,KAAK;AACpC,2BAAiB;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AAGA,QAAI,gBAAgB;AACpB,QAAI,kBAAkB,eAAe,QAAQ,SAAS,GAAG;AACvD,YAAM,YAAY,eAAe,QAAQ,eAAe,QAAQ,SAAS,CAAC;AAC1E,UAAI,WAAW;AACb,cAAM,gBAAgB,UAAU;AAEhC,wBAAgB,OAAO,OAAO,CAAC,UAAU,MAAM,YAAY,aAAa;AAExE,YAAI,cAAc,WAAW,GAAG;AAC9B;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,UAAM,aAAa,MAAM,KAAK,aAAa,UAAU,eAAe,gBAAgB,OAAO;AAG3F;AACE,YAAM,KAAK,KAAK,GAAG,YAAY,UAAU,WAAW;AACpD,YAAM,QAAQ,GAAG,YAAY,QAAQ;AAErC,YAAM,SAAsB;AAAA,QAC1B;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,gBAAgB,UAAU,CAAC,GAAG,eAAe,SAAS,GAAG,UAAU,IAAI;AAAA,QAChF,YAAY,KAAK,IAAA;AAAA,QACjB,aACG,gBAAgB,cAAc,KAAK,WAAW,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,YAAY,CAAC;AAAA,QACzF,YAAY,SAAS,cAAc,gBAAgB,cAAc;AAAA,QACjE,oBAAoB,SAAS,sBAAsB,gBAAgB;AAAA,QACnE,UAAU,SAAS,YAAY,gBAAgB;AAAA,MAAA;AAGjD,YAAM,IAAI,MAAM;AAEhB,YAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,WAAG,aAAa,MAAM;AACpB,kBAAA;AAAA,QACF;AACA,WAAG,UAAU,MAAM;AACjB,kBAAQ,MAAM,mCAAmC,MAAM,IAAI,KAAK,KAAK,GAAG,KAAK;AAC7E,iBAAO,GAAG,KAAK;AAAA,QACjB;AAAA,MACF,CAAC;AAAA,IACH;AAGA,UAAM,KAAK,aAAA;AAAA,EACb;AAAA,EAEA,MAAM,gBAAgB,SAAiB,OAAe,QAAgC;AACpF,UAAM,KAAK,KAAA;AAEX,QAAI,CAAC,KAAK,GAAI;AAEd,UAAM,KAAK,KAAK,GAAG,YAAY,UAAU,WAAW;AACpD,UAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,UAAM,eAAwC,CAAA;AAG9C,UAAM,SAAS,MAAM,WAAA;AACrB,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,aAAO,YAAY,CAAC,UAAU;AAC5B,cAAMA,UAAU,MAAM,OAAsB;AAC5C,YAAI,CAACA,SAAQ;AACX,kBAAA;AACA;AAAA,QACF;AAEA,cAAM,SAAsBA,QAAO;AAEnC,YAAI,UAAU,OAAO,WAAW,QAAQ;AACtCA,kBAAO,SAAA;AACP;AAAA,QACF;AAGA,cAAM,aAAa,OAAO,QAAQ,KAAK,CAAC,UAAU;AAChD,gBAAM,WAAW,MAAM,UAAU,MAAM;AACvC,iBAAO,MAAM,UAAU,SAAS,WAAW;AAAA,QAC7C,CAAC;AAED,YAAI,YAAY;AACd,uBAAa,KAAK,CAAC,OAAO,QAAQ,OAAO,KAAK,CAAC;AAAA,QACjD;AAEAA,gBAAO,SAAA;AAAA,MACT;AAAA,IACF,CAAC;AAGD,eAAW,OAAO,cAAc;AAC9B,YAAM,KAAK,YAAY,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;AAAA,IACvC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAQ,QAAgB,OAA4C;AACxE,UAAM,KAAK,KAAA;AAEX,QAAI,CAAC,KAAK,GAAI,QAAO;AAErB,UAAM,KAAK,KAAK,GAAG,YAAY,UAAU,UAAU;AACnD,UAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,UAAM,SAAS,MAAM,KAAK,iBAA8B,MAAM,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC;AAElF,WAAO,WAAW,UAAa,OAAO,WAAW,OAAO,QAAQ,SAAS;AAAA,EAC3E;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,gBAAgB,QAAgB,OAA4C;AAChF,UAAM,KAAK,KAAA;AAEX,QAAI,CAAC,KAAK,IAAI;AACZ,cAAQ,KAAK,+CAA+C;AAC5D,aAAO;AAAA,IACT;AAEA,UAAM,KAAK,KAAK,GAAG,YAAY,UAAU,UAAU;AACnD,UAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,UAAM,SAAS,MAAM,KAAK,iBAA8B,MAAM,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC;AAElF,UAAM,SAAS,QAAQ,eAAe;AACtC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,QAAgB,OAAyC;AAC1E,UAAM,KAAK,KAAA;AAEX,QAAI,CAAC,KAAK,GAAI;AAEd,UAAM,KAAK,KAAK,GAAG,YAAY,UAAU,WAAW;AACpD,UAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,UAAM,SAAS,MAAM,KAAK,iBAA8B,MAAM,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC;AAElF,QAAI,QAAQ;AACV,aAAO,aAAa;AACpB,aAAO,aAAa,KAAK,IAAA;AACzB,YAAM,IAAI,MAAM;AAEhB,YAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,WAAG,aAAa,MAAM,QAAA;AACtB,WAAG,UAAU,MAAM,OAAO,GAAG,KAAK;AAAA,MACpC,CAAC;AACD,cAAQ,IAAI,0BAA0B,MAAM,KAAK,KAAK,wBAAwB;AAAA,IAChF,OAAO;AACL,cAAQ,KAAK,0BAA0B,MAAM,KAAK,KAAK,iCAAiC;AAAA,IAC1F;AAAA,EACF;AAAA,EAEA,MAAM,eAAe,QAA+B;AAClD,UAAM,KAAK,KAAA;AAEX,QAAI,CAAC,KAAK,GAAI;AAGd,UAAM,kBAAiC,CAAA;AACvC;AACE,YAAM,KAAK,KAAK,GAAG,YAAY,UAAU,UAAU;AACnD,YAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,sBAAgB,KAAK,GAAI,MAAM,KAAK,eAAe,OAAO,MAAM,CAAE;AAAA,IACpE;AAGA,eAAW,UAAU,iBAAiB;AACpC,YAAM,KAAK,YAAY,OAAO,QAAQ,OAAO,KAAK;AAAA,IACpD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,iBACJ,QACA,OACuE;AACvE,UAAM,KAAK,KAAA;AAEX,QAAI,CAAC,KAAK,MAAM,CAAC,KAAK,SAAU,QAAO;AAGvC,UAAM,KAAK,KAAK,GAAG,YAAY,UAAU,UAAU;AACnD,UAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,UAAM,SAAS,MAAM,KAAK,iBAA8B,MAAM,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC;AAElF,QAAI,CAAC,UAAU,OAAO,QAAQ,WAAW,GAAG;AAC1C,aAAO;AAAA,IACT;AAGA,UAAM,UAAU,CAAC,GAAG,OAAO,OAAO;AAClC,QAAI,aAAa;AAEjB,WAAO,IAAI,eAAsD;AAAA,MAC/D,MAAM,OAAO,eAAe;AAC1B,YAAI,cAAc,QAAQ,QAAQ;AAChC,qBAAW,MAAA;AACX;AAAA,QACF;AAEA,cAAM,QAAQ,QAAQ,UAAU;AAChC,YAAI,CAAC,OAAO;AACV,qBAAW,MAAA;AACX;AAAA,QACF;AAGA,cAAM,YAAY,MAAM,KAAK,aAAa,OAAO,UAAU,KAAK;AAChE,YAAI,CAAC,WAAW;AACd,qBAAW,MAAM,IAAI,MAAM,iCAAiC,UAAU,EAAE,CAAC;AACzE;AAAA,QACF;AAGA,cAAM,QAAQ,KAAK;AAAA,UACjB;AAAA,UACA,MAAM;AAAA,UACN;AAAA,UACA,MAAM;AAAA,UACN,MAAM;AAAA,QAAA;AAER,mBAAW,QAAQ,KAAK;AAExB;AAAA,MACF;AAAA,IAAA,CACD;AAAA,EACH;AAAA,EAEA,MAAM,QAAuB;AAC3B,UAAM,KAAK,KAAA;AAEX,QAAI,CAAC,KAAK,MAAM,CAAC,KAAK,UAAU;AAC9B,cAAQ,KAAK,2DAA2D;AACxE;AAAA,IACF;AAGA,QAAI;AACF,YAAM,KAAK,KAAK,GAAG,YAAY,CAAC,UAAU,MAAM,GAAG,WAAW;AAC9D,YAAM,KAAK,iBAAiB,GAAG,YAAY,QAAQ,EAAE,OAAO;AAC5D,YAAM,KAAK,iBAAiB,GAAG,YAAY,MAAM,EAAE,OAAO;AAAA,IAC5D,SAAS,OAAO;AACd,cAAQ,MAAM,wCAAwC,KAAK;AAC3D,YAAM;AAAA,IACR;AAGA,QAAI;AACF,YAAM,aAAa,MAAM,KAAK,SAAS;AAAA,QACrC,mBAAmB,KAAK,SAAS;AAAA,QACjC;AAAA,UACE,QAAQ;AAAA,QAAA;AAAA,MACV;AAEF,YAAM,KAAK,SAAS,YAAY,WAAW,MAAM,EAAE,WAAW,MAAM;AAAA,IACtE,SAAS,OAAO;AACd,UAAK,OAAe,SAAS,iBAAiB;AAC5C,gBAAQ,KAAK,mCAAmC,KAAK;AAAA,MACvD;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,cAA6B;AAEzC,SAAK,WAAW,MAAM,UAAU,QAAQ,aAAA;AAGxC,UAAM,UAAU,UAAU,KAAK,iBAAiB,CAAC;AAEjD,YAAQ,kBAAkB,CAAC,UAAU;AACnC,YAAM,KAAM,MAAM,OAA4B;AAG9C,UAAI,CAAC,GAAG,iBAAiB,SAAS,QAAQ,GAAG;AAC3C,cAAM,QAAQ,GAAG,kBAAkB,UAAU;AAAA,UAC3C,SAAS,CAAC,UAAU,OAAO;AAAA,QAAA,CAC5B;AACD,cAAM,YAAY,cAAc,YAAY;AAAA,MAC9C;AAGA,UAAI,CAAC,GAAG,iBAAiB,SAAS,MAAM,GAAG;AACzC,WAAG,kBAAkB,QAAQ,EAAE,SAAS,aAAa;AAAA,MACvD;AAAA,IACF;AAEA,SAAK,KAAK,MAAM,IAAI,QAAQ,CAAC,SAAS,WAAW;AAC/C,cAAQ,YAAY,MAAM,QAAQ,QAAQ,MAAM;AAChD,cAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,IAC9C,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,aAAa,UAAkB,OAAgD;AAC3F,QAAI,CAAC,KAAK,SAAU,QAAO;AAE3B,UAAM,aAAa,MAAM,KAAK,SAAS,mBAAmB,mBAAmB,KAAK,SAAS,IAAI;AAAA,MAC7F,QAAQ;AAAA,IAAA,CACT;AACD,UAAM,aAAa,MAAM,WAAW,cAAc,QAAQ;AAC1D,UAAM,OAAO,MAAM,WAAW,QAAA;AAC9B,UAAM,QAAQ,KAAK,MAAM,MAAM,YAAY,MAAM,aAAa,MAAM,UAAU;AAC9E,WAAO,MAAM,MAAM,YAAA;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,aACZ,UACA,QACA,iBACuB;AACvB,QAAI,CAAC,KAAK,SAAU,QAAO,CAAA;AAE3B,UAAM,aAAa,MAAM,KAAK,SAAS,mBAAmB,mBAAmB,KAAK,SAAS,IAAI;AAAA,MAC7F,QAAQ;AAAA,IAAA,CACT;AACD,UAAM,aAAa,MAAM,WAAW,cAAc,UAAU,EAAE,QAAQ,MAAM;AAG5E,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,WAAW,eAAe,EAAE,kBAAkB,MAAM;AAE3E,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,EAEA,MAAc,mBACZ,OACsB;AACtB,UAAM,SAAS,IAAI,YAAY,MAAM,UAAU;AAC/C,UAAM,OAAO,MAAM;AACnB,WAAO;AAAA,EACT;AAAA,EAEQ,YACN,MACA,QACA,OACA,YAA6B,OAC7B,aAAqB,GACkB;AACvC,QAAI,UAAU,SAAS;AACrB,aAAO,IAAI,kBAAkB;AAAA,QAC3B,MAAM;AAAA,QACN,WAAW;AAAA,QACX,UAAU;AAAA,QACV;AAAA,MAAA,CACD;AAAA,IACH,OAAO;AACL,aAAO,IAAI,kBAAkB;AAAA,QAC3B,MAAM;AAAA,QACN,WAAW;AAAA,QACX,UAAU;AAAA,QACV;AAAA,MAAA,CACD;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAc,iBAAiB,QAAgB,OAA8B;AAC3E,QAAI,CAAC,KAAK,GAAI;AAEd,UAAM,KAAK,KAAK,GAAG,YAAY,UAAU,WAAW;AACpD,UAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,UAAM,SAAS,MAAM,KAAK,iBAA8B,MAAM,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC;AAElF,QAAI,QAAQ;AACV,aAAO,aAAa,KAAK,IAAA;AACzB,YAAM,KAAK,iBAAiB,MAAM,IAAI,MAAM,CAAC;AAAA,IAC/C;AAAA,EACF;AAAA,EAEA,MAAc,YAAY,QAAgB,OAA8B;AACtE,QAAI,CAAC,KAAK,GAAI;AAGd,QAAI;AACJ;AACE,YAAM,KAAK,KAAK,GAAG,YAAY,UAAU,UAAU;AACnD,YAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,eAAS,MAAM,KAAK,iBAA8B,MAAM,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC;AAAA,IAC9E;AAGA,QAAI,UAAU,KAAK,UAAU;AAC3B,UAAI;AACF,cAAM,aAAa,MAAM,KAAK,SAAS;AAAA,UACrC,mBAAmB,KAAK,SAAS;AAAA,UACjC,EAAE,QAAQ,MAAA;AAAA,QAAM;AAElB,cAAM,WAAW,YAAY,OAAO,QAAQ;AAAA,MAC9C,SAAS,OAAO;AACd,gBAAQ,KAAK,wCAAwC,OAAO,QAAQ,KAAK,KAAK;AAAA,MAChF;AAAA,IACF;AAGA;AACE,YAAM,KAAK,KAAK,GAAG,YAAY,UAAU,WAAW;AACpD,YAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,YAAM,KAAK,iBAAiB,MAAM,OAAO,CAAC,QAAQ,KAAK,CAAC,CAAC;AAAA,IAC3D;AAAA,EACF;AAAA,EAEA,MAAc,eAA8B;AAC1C,UAAM,WAAW,MAAM,UAAU,QAAQ,SAAA;AACzC,UAAM,QAAQ,SAAS,SAAS;AAEhC,QAAI,SAAS,KAAK,QAAS;AAE3B,YAAQ;AAAA,MACN,4DAA4D,KAAK,aAAa,KAAK,OAAO;AAAA,IAAA;AAG5F,QAAI,CAAC,KAAK,GAAI;AAGd,UAAM,KAAK,KAAK,GAAG,YAAY,UAAU,WAAW;AACpD,UAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,UAAM,QAAQ,MAAM,MAAM,YAAY;AAEtC,QAAI,eAAe;AACnB,UAAM,WAAW,QAAQ,KAAK;AAE9B,UAAM,SAAS,MAAM,WAAA;AACrB,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,aAAO,YAAY,OAAO,UAAU;AAClC,cAAMA,UAAU,MAAM,OAAsB;AAC5C,YAAI,CAACA,WAAU,gBAAgB,UAAU;AACvC,kBAAA;AACA;AAAA,QACF;AAEA,cAAM,SAAsBA,QAAO;AACnC,cAAM,KAAK,YAAY,OAAO,QAAQ,OAAO,KAAK;AAClD,wBAAgB,OAAO;AAEvBA,gBAAO,SAAA;AAAA,MACT;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,eAAe,OAAuB,QAAwC;AAC1F,UAAM,UAAyB,CAAA;AAC/B,UAAM,SAAS,MAAM,WAAA;AACrB,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,aAAO,YAAY,CAAC,UAAU;AAC5B,cAAMA,UAAU,MAAM,OAAsB;AAC5C,YAAI,CAACA,SAAQ;AACX,kBAAA;AACA;AAAA,QACF;AAEA,cAAM,SAAsBA,QAAO;AACnC,YAAI,OAAO,WAAW,QAAQ;AAC5B,kBAAQ,KAAK,MAAM;AAAA,QACrB;AAEAA,gBAAO,SAAA;AAAA,MACT;AAAA,IACF,CAAC;AACD,WAAO;AAAA,EACT;AAAA,EAEQ,iBAAoB,SAAiC;AAC3D,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,cAAQ,YAAY,MAAM,QAAQ,QAAQ,MAAM;AAChD,cAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,IAC9C,CAAC;AAAA,EACH;AAAA,EAEA,cAKE;AAGA,WAAO;AAAA,MACL,WAAW,KAAK,WAAW,OAAO;AAAA,MAClC,YAAY;AAAA;AAAA,MACZ,SAAS;AAAA;AAAA,MACT,SAAS;AAAA;AAAA,IAAA;AAAA,EAEb;AAAA,EAEA,MAAM,kBAAkB,QAAkC;AACxD,QAAI,OAAO,cAAc,eAAe,CAAC,UAAU,SAAS,UAAU;AAEpE,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AAEA,UAAM,WAAW,MAAM,UAAU,QAAQ,SAAA;AACzC,UAAM,gBAAgB,SAAS,SAAS,MAAM,SAAS,SAAS,OAAO,OAAO;AAC9E,WAAO,eAAe;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,gBAAgB,QAAgB,OAA+C;AACnF,UAAM,KAAK,KAAA;AAEX,QAAI,CAAC,KAAK,GAAI,QAAO;AAErB,UAAM,KAAK,KAAK,GAAG,YAAY,UAAU,UAAU;AACnD,UAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,UAAM,SAAS,MAAM,KAAK,iBAA8B,MAAM,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC;AAElF,WAAO,QAAQ,YAAY;AAAA,EAC7B;AACF;"}
|
|
1
|
+
{"version":3,"file":"L2Cache.js","sources":["../../src/cache/L2Cache.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\n// import type { ClipMetadata } from './types';\nimport { binarySearchRange } from '../utils/binary-search';\n\ninterface ChunkBatch {\n startUs: TimeUs;\n durationUs: TimeUs;\n byteOffset: number;\n byteLength: number;\n type: 'key' | 'delta'; // Store chunk type for correct reconstruction\n}\n\ninterface ChunkRecord {\n clipId: string; // IndexedDB keyPath uses clipId\n track: 'video' | 'audio';\n fileName: string;\n batches: ChunkBatch[];\n lastAccess: number;\n totalBytes: number;\n isComplete: boolean; // Mark if clip is fully rendered\n expectedDurationUs?: number; // Expected duration for validation\n metadata?: {\n codec?: string;\n description?: Uint8Array;\n codedWidth?: number;\n codedHeight?: number;\n displayAspectWidth?: number;\n displayAspectHeight?: number;\n colorSpace?: VideoColorSpaceInit;\n hardwareAcceleration?: HardwareAcceleration;\n optimizeForLatency?: boolean;\n sampleRate?: number;\n numberOfChannels?: number;\n };\n}\n\ninterface L2Config {\n maxSizeMB: number;\n projectId: string;\n}\n\nexport class L2Cache {\n private db: IDBDatabase | null = null;\n private opfsRoot: FileSystemDirectoryHandle | null = null;\n readonly maxSize: number;\n readonly projectId: string;\n private initPromise: Promise<void> | null = null;\n\n constructor(config: L2Config) {\n this.maxSize = config.maxSizeMB * 1024 * 1024;\n this.projectId = config.projectId;\n }\n\n async init(): Promise<void> {\n if (this.initPromise) return this.initPromise;\n\n this.initPromise = this.initStorage();\n return this.initPromise;\n }\n\n async get(timeUs: TimeUs, clipId: string): Promise<EncodedVideoChunk | EncodedAudioChunk | null> {\n await this.init();\n\n if (!this.db) return null;\n\n // Query IndexedDB for chunk metadata\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n const records = await this.collectRecords(store, clipId);\n\n for (const record of records) {\n const batch = binarySearchRange(record.batches, timeUs, (b) => ({\n start: b.startUs,\n end: b.startUs + b.durationUs,\n }));\n\n if (!batch) {\n continue;\n }\n\n const chunkData = await this.readFromOPFS(record.fileName, batch);\n if (!chunkData) continue;\n\n this.updateLastAccess(record.clipId, record.track);\n\n return this.createChunk(chunkData, timeUs, record.track, 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 (!this.db || !this.opfsRoot) {\n console.warn(`[L2Cache] put aborted: db=${!!this.db}, opfsRoot=${!!this.opfsRoot}`);\n return;\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 (separate transaction)\n let existingRecord: ChunkRecord | undefined;\n {\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n existingRecord = await this.promisifyRequest<ChunkRecord>(store.get([clipId, track]));\n }\n\n // Step 2: Validate consistency - if IndexedDB has record but OPFS file missing, delete the record\n if (existingRecord) {\n const projectDir = await this.opfsRoot.getDirectoryHandle(\n `meframe-project-${this.projectId}`,\n {\n create: true,\n }\n );\n try {\n await projectDir.getFileHandle(existingRecord.fileName, { create: false });\n } catch (error) {\n if ((error as any)?.name === 'NotFoundError') {\n await this.deleteEntry(clipId, track);\n existingRecord = undefined;\n }\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 3: Write to OPFS (no active transaction)\n const newBatches = await this.appendToOPFS(fileName, chunksToWrite, existingRecord?.batches);\n\n // Step 4: Update IndexedDB (new transaction)\n {\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n\n const record: ChunkRecord = {\n clipId,\n track,\n fileName,\n batches: 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 store.put(record);\n // Wait for transaction to complete, not just the request\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => {\n resolve();\n };\n tx.onerror = () => {\n console.error(`[L2Cache] Transaction error for ${clipId} ${track}:`, tx.error);\n reject(tx.error);\n };\n });\n }\n\n // Check and enforce quota\n await this.enforceQuota();\n }\n\n async invalidateRange(startUs: TimeUs, endUs: TimeUs, clipId?: string): Promise<void> {\n await this.init();\n\n if (!this.db) return;\n\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n const keysToDelete: Array<[string, string]> = [];\n\n // Iterate through all records\n const cursor = store.openCursor();\n await new Promise<void>((resolve) => {\n cursor.onsuccess = (event) => {\n const cursor = (event.target as IDBRequest).result;\n if (!cursor) {\n resolve();\n return;\n }\n\n const record: ChunkRecord = cursor.value;\n\n if (clipId && record.clipId !== clipId) {\n cursor.continue();\n return;\n }\n\n // Check if any batch overlaps with invalidation range\n const hasOverlap = record.batches.some((batch) => {\n const batchEnd = batch.startUs + batch.durationUs;\n return batch.startUs < endUs && batchEnd > startUs;\n });\n\n if (hasOverlap) {\n keysToDelete.push([record.clipId, record.track]);\n }\n\n cursor.continue();\n };\n });\n\n // Delete invalidated entries\n for (const key of keysToDelete) {\n await this.deleteEntry(key[0], key[1]);\n }\n }\n\n /**\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 if (!this.db) return false;\n\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n const record = await this.promisifyRequest<ChunkRecord>(store.get([clipId, track]));\n\n return record !== undefined && 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 if (!this.db) {\n console.warn(`[L2Cache] hasCompleteClip: db not initialized`);\n return false;\n }\n\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n const record = await this.promisifyRequest<ChunkRecord>(store.get([clipId, track]));\n\n const result = record?.isComplete === true;\n return result;\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 if (!this.db) return;\n\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n const record = await this.promisifyRequest<ChunkRecord>(store.get([clipId, track]));\n\n if (record) {\n record.isComplete = true;\n record.lastAccess = Date.now();\n store.put(record);\n // Wait for transaction to complete\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n console.log(`[L2Cache] markComplete(${clipId}, ${track}): marked successfully`);\n } else {\n console.warn(`[L2Cache] markComplete(${clipId}, ${track}): no record found, cannot mark`);\n }\n }\n\n async invalidateClip(clipId: string): Promise<void> {\n await this.init();\n\n if (!this.db) return;\n\n // Collect records to delete\n const recordsToDelete: ChunkRecord[] = [];\n {\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n recordsToDelete.push(...(await this.collectRecords(store, clipId)));\n }\n\n // Delete each record\n for (const record of recordsToDelete) {\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 if (!this.db || !this.opfsRoot) return null;\n\n // Get chunk record\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n const record = await this.promisifyRequest<ChunkRecord>(store.get([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.readFromOPFS(record.fileName, batch);\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 if (!this.db || !this.opfsRoot) {\n console.warn('[L2Cache] clear() called but db or opfsRoot not available');\n return;\n }\n\n // Clear IndexedDB\n try {\n const tx = this.db.transaction(['chunks', 'meta'], 'readwrite');\n await this.promisifyRequest(tx.objectStore('chunks').clear());\n await this.promisifyRequest(tx.objectStore('meta').clear());\n } catch (error) {\n console.error('[L2Cache] Failed to clear IndexedDB:', error);\n throw error;\n }\n\n // Clear OPFS files\n try {\n const projectDir = await this.opfsRoot.getDirectoryHandle(\n `meframe-project-${this.projectId}`,\n {\n create: false,\n }\n );\n await this.opfsRoot.removeEntry(projectDir.name, { recursive: true });\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 async initStorage(): Promise<void> {\n // Initialize OPFS\n this.opfsRoot = await navigator.storage.getDirectory();\n\n // Initialize IndexedDB\n const request = indexedDB.open('meframe_cache', 1);\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result;\n\n // chunks store with composite key [clipId, track]\n if (!db.objectStoreNames.contains('chunks')) {\n const store = db.createObjectStore('chunks', {\n keyPath: ['clipId', 'track'],\n });\n store.createIndex('lastAccess', 'lastAccess');\n }\n\n // meta store\n if (!db.objectStoreNames.contains('meta')) {\n db.createObjectStore('meta', { keyPath: 'projectId' });\n }\n };\n\n this.db = await new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n }\n\n private async readFromOPFS(fileName: string, batch: ChunkBatch): Promise<ArrayBuffer | null> {\n if (!this.opfsRoot) return null;\n\n const projectDir = await this.opfsRoot.getDirectoryHandle(`meframe-project-${this.projectId}`, {\n create: false,\n });\n const fileHandle = await projectDir.getFileHandle(fileName);\n const file = await fileHandle.getFile();\n const slice = file.slice(batch.byteOffset, batch.byteOffset + batch.byteLength);\n return await slice.arrayBuffer();\n }\n\n /**\n * Append chunks to OPFS file (or create new file)\n * Supports incremental writing for streaming scenarios\n */\n private async appendToOPFS(\n fileName: string,\n chunks: Array<EncodedVideoChunk | EncodedAudioChunk>,\n existingBatches?: ChunkBatch[]\n ): Promise<ChunkBatch[]> {\n if (!this.opfsRoot) return [];\n\n const projectDir = await this.opfsRoot.getDirectoryHandle(`meframe-project-${this.projectId}`, {\n create: true,\n });\n const fileHandle = await projectDir.getFileHandle(fileName, { create: true });\n\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 fileHandle.createWritable({ keepExistingData: true });\n\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 private async chunkToArrayBuffer(\n chunk: EncodedVideoChunk | EncodedAudioChunk\n ): Promise<ArrayBuffer> {\n const buffer = new ArrayBuffer(chunk.byteLength);\n chunk.copyTo(buffer);\n return buffer;\n }\n\n private createChunk(\n data: ArrayBuffer,\n timeUs: TimeUs,\n track: 'video' | 'audio',\n 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 updateLastAccess(clipId: string, track: string): Promise<void> {\n if (!this.db) return;\n\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n const record = await this.promisifyRequest<ChunkRecord>(store.get([clipId, track]));\n\n if (record) {\n record.lastAccess = Date.now();\n await this.promisifyRequest(store.put(record));\n }\n }\n\n private async deleteEntry(clipId: string, track: string): Promise<void> {\n if (!this.db) return;\n\n // Step 1: Get record info\n let record: ChunkRecord | undefined;\n {\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n record = await this.promisifyRequest<ChunkRecord>(store.get([clipId, track]));\n }\n\n // Step 2: Delete OPFS file (outside transaction)\n if (record && this.opfsRoot) {\n try {\n const projectDir = await this.opfsRoot.getDirectoryHandle(\n `meframe-project-${this.projectId}`,\n { create: false }\n );\n await projectDir.removeEntry(record.fileName);\n } catch (error) {\n console.warn(`[L2Cache] Failed to delete OPFS file ${record.fileName}:`, error);\n }\n }\n\n // Step 3: Delete IndexedDB record (new transaction)\n {\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n await this.promisifyRequest(store.delete([clipId, track]));\n }\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 if (!this.db) return;\n\n // Delete oldest entries until under quota\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n const index = store.index('lastAccess');\n\n let bytesDeleted = 0;\n const toDelete = usage - this.maxSize;\n\n const cursor = index.openCursor();\n await new Promise<void>((resolve) => {\n cursor.onsuccess = async (event) => {\n const cursor = (event.target as IDBRequest).result;\n if (!cursor || bytesDeleted >= toDelete) {\n resolve();\n return;\n }\n\n const record: ChunkRecord = cursor.value;\n await this.deleteEntry(record.clipId, record.track);\n bytesDeleted += record.totalBytes;\n\n cursor.continue();\n };\n });\n }\n\n private async collectRecords(store: IDBObjectStore, clipId: string): Promise<ChunkRecord[]> {\n const records: ChunkRecord[] = [];\n const cursor = store.openCursor();\n await new Promise<void>((resolve) => {\n cursor.onsuccess = (event) => {\n const cursor = (event.target as IDBRequest).result;\n if (!cursor) {\n resolve();\n return;\n }\n\n const record: ChunkRecord = cursor.value;\n if (record.clipId === clipId) {\n records.push(record);\n }\n\n cursor.continue();\n };\n });\n return records;\n }\n\n private promisifyRequest<T>(request: IDBRequest): Promise<T> {\n return new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n }\n\n getMetadata(): {\n maxSizeMB: number;\n usedSizeMB: number;\n entries: number;\n hitRate: number;\n } {\n // This is a simplified implementation\n // In a real implementation, we would track actual usage\n return {\n maxSizeMB: this.maxSize / (1024 * 1024),\n usedSizeMB: 0, // Would need to track actual usage\n entries: 0, // Would need to track actual entries\n hitRate: 0, // Would need to track hits and misses\n };\n }\n\n async hasAvailableQuota(sizeMB: number): Promise<boolean> {\n if (typeof navigator === 'undefined' || !navigator.storage?.estimate) {\n // L2Cache requires storage API to function\n throw new Error('Storage API not available');\n }\n\n const estimate = await navigator.storage.estimate();\n const availableMB = ((estimate.quota || 0) - (estimate.usage || 0)) / (1024 * 1024);\n return availableMB >= sizeMB;\n }\n\n /**\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 if (!this.db) return null;\n\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n const record = await this.promisifyRequest<ChunkRecord>(store.get([clipId, track]));\n\n return record?.metadata || null;\n }\n}\n"],"names":["cursor"],"mappings":"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"}
|
|
@@ -33,23 +33,23 @@ export declare class VideoL1Cache {
|
|
|
33
33
|
/**
|
|
34
34
|
* Evict all cache entries for a specific clip
|
|
35
35
|
*/
|
|
36
|
-
|
|
36
|
+
invalidateClip(clipId: string): void;
|
|
37
37
|
/**
|
|
38
38
|
* Check if a clip has any cached entries
|
|
39
39
|
*/
|
|
40
40
|
hasClip(clipId: string): boolean;
|
|
41
41
|
/**
|
|
42
42
|
* Get total frame count for a clip
|
|
43
|
+
* @param clipId - Clip identifier
|
|
44
|
+
* @param startTimeUs - Optional start time to count frames from (inclusive)
|
|
43
45
|
*/
|
|
44
|
-
getClipFrameCount(clipId: string): number;
|
|
45
|
-
invalidateRange(startUs: TimeUs, endUs: TimeUs, clipId?: string): void;
|
|
46
|
+
getClipFrameCount(clipId: string, startTimeUs?: TimeUs): number;
|
|
46
47
|
clear(): void;
|
|
47
48
|
getMetadata(): L1CacheMetadata;
|
|
48
49
|
private registerEntry;
|
|
49
50
|
private createEntry;
|
|
50
51
|
private mergeFrames;
|
|
51
52
|
private insertFrame;
|
|
52
|
-
private removeEntry;
|
|
53
53
|
private closeFrames;
|
|
54
54
|
private composeKey;
|
|
55
55
|
private wrapFrame;
|
|
@@ -57,7 +57,6 @@ export declare class VideoL1Cache {
|
|
|
57
57
|
private getEntryExact;
|
|
58
58
|
private findEntryByTime;
|
|
59
59
|
private countEntries;
|
|
60
|
-
private iterateEntries;
|
|
61
60
|
private ensureClipEntries;
|
|
62
61
|
private findInsertIndex;
|
|
63
62
|
private findFrameInsertIndex;
|
|
@@ -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,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAMtC,UAAU,QAAQ;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAYD,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;;;;;;;GAOG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqC;IACnE,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,YAAY,CAAK;gBAEb,MAAM,EAAE,QAAQ;IAK5B,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI;IA6BnD,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAwBvD,QAAQ,CACN,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,EAClB,UAAU,CAAC,EAAE,OAAO,GACnB,OAAO,GAAG,IAAI;IA0BjB;;OAEG;IACH,
|
|
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,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAMtC,UAAU,QAAQ;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAYD,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;;;;;;;GAOG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqC;IACnE,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,YAAY,CAAK;gBAEb,MAAM,EAAE,QAAQ;IAK5B,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI;IA6BnD,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAwBvD,QAAQ,CACN,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,EAClB,UAAU,CAAC,EAAE,OAAO,GACnB,OAAO,GAAG,IAAI;IA0BjB;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAYpC;;OAEG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAKhC;;;;OAIG;IACH,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM;IAqC/D,KAAK,IAAI,IAAI;IAUb,WAAW,IAAI,eAAe;IAS9B,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,WAAW;IAyBnB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,WAAW;IAqBnB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,UAAU;IAmBlB,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,eAAe;IAqBvB,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,eAAe;IAevB,OAAO,CAAC,oBAAoB;IAe5B,OAAO,CAAC,eAAe;IAgBvB,OAAO,CAAC,gBAAgB;IAQxB,OAAO,CAAC,sBAAsB;CAM/B"}
|
|
@@ -77,7 +77,7 @@ class VideoL1Cache {
|
|
|
77
77
|
/**
|
|
78
78
|
* Evict all cache entries for a specific clip
|
|
79
79
|
*/
|
|
80
|
-
|
|
80
|
+
invalidateClip(clipId) {
|
|
81
81
|
const entries = this.entriesByClip.get(clipId);
|
|
82
82
|
if (!entries) return;
|
|
83
83
|
for (const entry of entries) {
|
|
@@ -95,23 +95,37 @@ class VideoL1Cache {
|
|
|
95
95
|
}
|
|
96
96
|
/**
|
|
97
97
|
* Get total frame count for a clip
|
|
98
|
+
* @param clipId - Clip identifier
|
|
99
|
+
* @param startTimeUs - Optional start time to count frames from (inclusive)
|
|
98
100
|
*/
|
|
99
|
-
getClipFrameCount(clipId) {
|
|
101
|
+
getClipFrameCount(clipId, startTimeUs) {
|
|
100
102
|
const entries = this.entriesByClip.get(clipId);
|
|
101
103
|
if (!entries) return 0;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
104
|
+
if (startTimeUs === void 0) {
|
|
105
|
+
return entries.reduce((sum, entry) => sum + entry.frames.length, 0);
|
|
106
|
+
}
|
|
107
|
+
let count = 0;
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
for (const frame of entry.frames) {
|
|
110
|
+
const frameTime = frame.timestampUs ?? entry.gopStartUs;
|
|
111
|
+
if (frameTime >= startTimeUs) {
|
|
112
|
+
count++;
|
|
113
|
+
}
|
|
112
114
|
}
|
|
113
115
|
}
|
|
116
|
+
return count;
|
|
114
117
|
}
|
|
118
|
+
// invalidateRange(startUs: TimeUs, endUs: TimeUs, clipId?: string): void {
|
|
119
|
+
// for (const entry of this.iterateEntries()) {
|
|
120
|
+
// if (clipId && entry.clipId !== clipId) {
|
|
121
|
+
// continue;
|
|
122
|
+
// }
|
|
123
|
+
// const gopEnd = entry.gopStartUs + entry.durationUs;
|
|
124
|
+
// if (entry.gopStartUs < endUs && gopEnd > startUs) {
|
|
125
|
+
// this.removeEntry(entry);
|
|
126
|
+
// }
|
|
127
|
+
// }
|
|
128
|
+
// }
|
|
115
129
|
clear() {
|
|
116
130
|
for (const entries of this.entriesByClip.values()) {
|
|
117
131
|
for (const entry of entries) {
|
|
@@ -173,20 +187,6 @@ class VideoL1Cache {
|
|
|
173
187
|
const duration = frame.durationUs || fallbackDuration;
|
|
174
188
|
entry.durationUs = Math.max(entry.durationUs, timestamp + duration - entry.gopStartUs);
|
|
175
189
|
}
|
|
176
|
-
removeEntry(entry) {
|
|
177
|
-
const clipEntries = this.entriesByClip.get(entry.clipId);
|
|
178
|
-
if (clipEntries) {
|
|
179
|
-
const index = clipEntries.findIndex((item) => item.gopIndex === entry.gopIndex);
|
|
180
|
-
if (index !== -1) {
|
|
181
|
-
clipEntries.splice(index, 1);
|
|
182
|
-
}
|
|
183
|
-
if (clipEntries.length === 0) {
|
|
184
|
-
this.entriesByClip.delete(entry.clipId);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
this.closeFrames(entry);
|
|
188
|
-
this.currentBytes -= entry.size;
|
|
189
|
-
}
|
|
190
190
|
closeFrames(entry) {
|
|
191
191
|
for (const frame of entry.frames) {
|
|
192
192
|
frame?.close?.();
|
|
@@ -245,13 +245,6 @@ class VideoL1Cache {
|
|
|
245
245
|
}
|
|
246
246
|
return count;
|
|
247
247
|
}
|
|
248
|
-
iterateEntries() {
|
|
249
|
-
const entries = [];
|
|
250
|
-
for (const clipEntries of this.entriesByClip.values()) {
|
|
251
|
-
entries.push(...clipEntries);
|
|
252
|
-
}
|
|
253
|
-
return entries;
|
|
254
|
-
}
|
|
255
248
|
ensureClipEntries(clipId) {
|
|
256
249
|
let entries = this.entriesByClip.get(clipId);
|
|
257
250
|
if (!entries) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoL1Cache.js","sources":["../../../src/cache/l1/VideoL1Cache.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport type { GOP } from '../types';\nimport { RcFrame } from '../../model';\nimport { findFrameIndex, findGopIndex } from './gop-utils';\n\nconst DEFAULT_GOP_INTERVAL_US = 2_000_000;\nconst BYTES_PER_MB = 1024 * 1024;\n\ninterface L1Config {\n maxMemoryMB: number;\n maxGOPs?: number;\n gopIntervalUs?: number;\n}\n\ninterface L1CacheEntry {\n key: string;\n clipId: string;\n gopIndex: number;\n gopStartUs: TimeUs;\n durationUs: TimeUs;\n frames: RcFrame[];\n size: number;\n}\n\nexport interface L1CacheMetadata {\n size: number;\n maxSize: number;\n entries: number;\n clipCount: number;\n}\n\n/**\n * Simplified VideoL1Cache for 2-Clip strategy\n *\n * Clip lifecycle is managed by ClipSessionManager:\n * - No LRU eviction (clips evicted explicitly)\n * - No capacity limits (fixed 3 clips)\n * - Simple GOP storage per clip\n */\nexport class VideoL1Cache {\n private readonly entriesByClip = new Map<string, L1CacheEntry[]>();\n private maxMemoryBytes: number;\n private gopIntervalUs: number;\n private currentBytes = 0;\n\n constructor(config: L1Config) {\n this.maxMemoryBytes = config.maxMemoryMB * BYTES_PER_MB;\n this.gopIntervalUs = config.gopIntervalUs ?? DEFAULT_GOP_INTERVAL_US;\n }\n\n get(timeUs: TimeUs, clipId: string): RcFrame | null {\n const clipEntries = this.entriesByClip.get(clipId);\n if (!clipEntries || clipEntries.length === 0) {\n return null;\n }\n\n const entry = this.findEntryByTime(clipEntries, timeUs);\n if (!entry) {\n return null;\n }\n\n const frameIndex = findFrameIndex(\n {\n index: entry.gopIndex,\n startUs: entry.gopStartUs,\n durationUs: entry.durationUs,\n frames: entry.frames,\n isKeyframe: true,\n clipId: entry.clipId,\n },\n timeUs\n );\n if (frameIndex === -1) {\n return null;\n }\n\n return entry.frames[frameIndex] ?? null;\n }\n\n putGOP(gop: GOP, clipId: string, trackId: string): void {\n const gopIndex = gop.index ?? gop.startUs;\n const existing = this.getEntryExact(clipId, gopIndex);\n\n const frames = this.wrapFrames(gop.frames, clipId, gop.durationUs, trackId);\n\n if (existing) {\n existing.gopStartUs = gop.startUs;\n this.mergeFrames(existing, frames, gop.durationUs);\n return;\n }\n\n const entry = this.createEntry(clipId, {\n gopIndex,\n startUs: gop.startUs,\n durationUs: gop.durationUs,\n frames,\n isKeyframe: gop.isKeyframe,\n });\n\n this.registerEntry(entry);\n this.currentBytes += entry.size;\n }\n\n addFrame(\n frame: VideoFrame,\n clipId: string,\n frameDuration: TimeUs,\n trackId: string,\n gopSerial?: number,\n isKeyframe?: boolean\n ): RcFrame | null {\n const timestamp = frame.timestamp ?? 0;\n const gopIndex =\n typeof gopSerial === 'number' ? gopSerial : findGopIndex(timestamp, this.gopIntervalUs);\n const existing = this.getEntryExact(clipId, gopIndex);\n\n const rcFrame = this.wrapFrame(frame, clipId, frameDuration, trackId, gopSerial, isKeyframe);\n\n if (existing) {\n this.mergeFrames(existing, [rcFrame], frameDuration);\n return rcFrame;\n }\n\n const entry = this.createEntry(clipId, {\n gopIndex,\n startUs: timestamp,\n durationUs: frameDuration,\n frames: [rcFrame],\n isKeyframe: isKeyframe ?? true,\n });\n\n this.registerEntry(entry);\n this.currentBytes += entry.size;\n return rcFrame;\n }\n\n /**\n * Evict all cache entries for a specific clip\n */\n evictClip(clipId: string): void {\n const entries = this.entriesByClip.get(clipId);\n if (!entries) return;\n\n for (const entry of entries) {\n this.closeFrames(entry);\n this.currentBytes -= entry.size;\n }\n\n this.entriesByClip.delete(clipId);\n }\n\n /**\n * Check if a clip has any cached entries\n */\n hasClip(clipId: string): boolean {\n const entries = this.entriesByClip.get(clipId);\n return !!entries && entries.length > 0;\n }\n\n /**\n * Get total frame count for a clip\n */\n getClipFrameCount(clipId: string): number {\n const entries = this.entriesByClip.get(clipId);\n if (!entries) return 0;\n return entries.reduce((sum, entry) => sum + entry.frames.length, 0);\n }\n\n invalidateRange(startUs: TimeUs, endUs: TimeUs, clipId?: string): void {\n for (const entry of this.iterateEntries()) {\n if (clipId && entry.clipId !== clipId) {\n continue;\n }\n\n const gopEnd = entry.gopStartUs + entry.durationUs;\n if (entry.gopStartUs < endUs && gopEnd > startUs) {\n this.removeEntry(entry);\n }\n }\n }\n\n clear(): void {\n for (const entries of this.entriesByClip.values()) {\n for (const entry of entries) {\n this.closeFrames(entry);\n }\n }\n this.entriesByClip.clear();\n this.currentBytes = 0;\n }\n\n getMetadata(): L1CacheMetadata {\n return {\n size: this.currentBytes,\n maxSize: this.maxMemoryBytes,\n entries: this.countEntries(),\n clipCount: this.entriesByClip.size,\n };\n }\n\n private registerEntry(entry: L1CacheEntry): void {\n const entries = this.ensureClipEntries(entry.clipId);\n const insertIndex = this.findInsertIndex(entries, entry.gopIndex);\n entries.splice(insertIndex, 0, entry);\n }\n\n private createEntry(\n clipId: string,\n gop: {\n gopIndex: number;\n startUs: TimeUs;\n durationUs: TimeUs;\n frames: RcFrame[];\n isKeyframe: boolean;\n }\n ): L1CacheEntry {\n const frames = this.normalizeFrames(gop.frames);\n const entry: L1CacheEntry = {\n key: this.composeKey(clipId, gop.gopIndex),\n clipId,\n gopIndex: gop.gopIndex,\n gopStartUs: gop.startUs,\n durationUs: gop.durationUs,\n frames,\n size: 0,\n };\n\n this.updateEntryStats(entry, this.deriveFallbackDuration(gop.durationUs, frames.length));\n return entry;\n }\n\n private mergeFrames(entry: L1CacheEntry, frames: RcFrame[], fallbackDuration: TimeUs): void {\n const durationFallback = this.deriveFallbackDuration(\n fallbackDuration,\n entry.frames.length + frames.length\n );\n for (const rcFrame of frames) {\n this.insertFrame(entry, rcFrame, durationFallback);\n }\n this.updateEntryStats(entry, durationFallback);\n }\n\n private insertFrame(entry: L1CacheEntry, frame: RcFrame, fallbackDuration: TimeUs): void {\n const timestamp = frame.timestampUs ?? entry.gopStartUs;\n const frames = entry.frames;\n const insertIndex = this.findFrameInsertIndex(frames, timestamp);\n\n if (\n insertIndex < frames.length &&\n (frames[insertIndex]?.timestampUs ?? entry.gopStartUs) === timestamp\n ) {\n const oldFrame = frames[insertIndex];\n frames[insertIndex] = frame;\n oldFrame?.close?.();\n } else {\n frames.splice(insertIndex, 0, frame);\n }\n\n entry.size += frame.sizeEstimate;\n const duration = frame.durationUs || fallbackDuration;\n entry.durationUs = Math.max(entry.durationUs, timestamp + duration - entry.gopStartUs);\n }\n\n private removeEntry(entry: L1CacheEntry): void {\n const clipEntries = this.entriesByClip.get(entry.clipId);\n if (clipEntries) {\n const index = clipEntries.findIndex((item) => item.gopIndex === entry.gopIndex);\n if (index !== -1) {\n clipEntries.splice(index, 1);\n }\n if (clipEntries.length === 0) {\n this.entriesByClip.delete(entry.clipId);\n }\n }\n\n this.closeFrames(entry);\n this.currentBytes -= entry.size;\n }\n\n private closeFrames(entry: L1CacheEntry): void {\n for (const frame of entry.frames) {\n frame?.close?.();\n }\n }\n\n private composeKey(clipId: string, gopIndex: number): string {\n return `${clipId}:${gopIndex}`;\n }\n\n private wrapFrame(\n frame: VideoFrame,\n clipId: string,\n frameDuration: TimeUs,\n trackId: string,\n gopSerial?: number,\n isKeyframe?: boolean\n ): RcFrame {\n return RcFrame.wrap(frame, {\n trackId,\n clipId,\n timestampUs: frame.timestamp ?? 0,\n durationUs: frame.duration ?? frameDuration,\n gopSerial,\n isKeyframe,\n });\n }\n\n private wrapFrames(\n frames: (RcFrame | VideoFrame)[],\n clipId: string,\n fallbackDuration: TimeUs,\n trackId: string\n ): RcFrame[] {\n const wrapped = frames.map((frame) =>\n frame instanceof RcFrame\n ? frame\n : this.wrapFrame(\n frame as VideoFrame,\n clipId,\n fallbackDuration / Math.max(frames.length, 1),\n trackId\n )\n );\n return this.normalizeFrames(wrapped);\n }\n\n private getEntryExact(clipId: string, gopIndex: number): L1CacheEntry | undefined {\n const entries = this.entriesByClip.get(clipId);\n if (!entries) return undefined;\n return entries.find((e) => e.gopIndex === gopIndex);\n }\n\n private findEntryByTime(entries: L1CacheEntry[], timeUs: TimeUs): L1CacheEntry | undefined {\n let low = 0;\n let high = entries.length - 1;\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const entry = entries[mid];\n if (!entry) break;\n\n if (timeUs < entry.gopStartUs) {\n high = mid - 1;\n } else if (timeUs >= entry.gopStartUs + entry.durationUs) {\n low = mid + 1;\n } else {\n return entry;\n }\n }\n\n return undefined;\n }\n\n private countEntries(): number {\n let count = 0;\n for (const clipEntries of this.entriesByClip.values()) {\n count += clipEntries.length;\n }\n return count;\n }\n\n private iterateEntries(): Iterable<L1CacheEntry> {\n const entries: L1CacheEntry[] = [];\n for (const clipEntries of this.entriesByClip.values()) {\n entries.push(...clipEntries);\n }\n return entries;\n }\n\n private ensureClipEntries(clipId: string): L1CacheEntry[] {\n let entries = this.entriesByClip.get(clipId);\n if (!entries) {\n entries = [];\n this.entriesByClip.set(clipId, entries);\n }\n return entries;\n }\n\n private findInsertIndex(entries: L1CacheEntry[], gopIndex: number): number {\n let low = 0;\n let high = entries.length;\n while (low < high) {\n const mid = Math.floor((low + high) / 2);\n const entry = entries[mid];\n if (entry && entry.gopIndex < gopIndex) {\n low = mid + 1;\n } else {\n high = mid;\n }\n }\n return low;\n }\n\n private findFrameInsertIndex(frames: RcFrame[], timestamp: TimeUs): number {\n let low = 0;\n let high = frames.length;\n while (low < high) {\n const mid = Math.floor((low + high) / 2);\n const midTs = frames[mid]?.timestampUs ?? 0;\n if (midTs < timestamp) {\n low = mid + 1;\n } else {\n high = mid;\n }\n }\n return low;\n }\n\n private normalizeFrames(frames: RcFrame[]): RcFrame[] {\n const seen = new Set<number>();\n const sorted = [...frames].sort((a, b) => (a.timestampUs ?? 0) - (b.timestampUs ?? 0));\n const result: RcFrame[] = [];\n for (const frame of sorted) {\n const ts = frame.timestampUs ?? 0;\n if (seen.has(ts)) {\n frame?.close?.();\n } else {\n seen.add(ts);\n result.push(frame);\n }\n }\n return result;\n }\n\n private updateEntryStats(entry: L1CacheEntry, fallbackDuration: TimeUs): void {\n entry.size = entry.frames.reduce((acc, frame) => acc + frame.sizeEstimate, 0);\n entry.durationUs = entry.frames.reduce((acc, frame) => {\n const duration = frame.durationUs || fallbackDuration;\n return Math.max(acc, (frame.timestampUs ?? entry.gopStartUs) + duration - entry.gopStartUs);\n }, entry.durationUs);\n }\n\n private deriveFallbackDuration(durationUs: TimeUs, frameCount: number): TimeUs {\n if (frameCount <= 1) {\n return durationUs || this.gopIntervalUs;\n }\n return Math.max(durationUs / frameCount, this.gopIntervalUs / frameCount);\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VideoL1Cache.js","sources":["../../../src/cache/l1/VideoL1Cache.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport type { GOP } from '../types';\nimport { RcFrame } from '../../model';\nimport { findFrameIndex, findGopIndex } from './gop-utils';\n\nconst DEFAULT_GOP_INTERVAL_US = 2_000_000;\nconst BYTES_PER_MB = 1024 * 1024;\n\ninterface L1Config {\n maxMemoryMB: number;\n maxGOPs?: number;\n gopIntervalUs?: number;\n}\n\ninterface L1CacheEntry {\n key: string;\n clipId: string;\n gopIndex: number;\n gopStartUs: TimeUs;\n durationUs: TimeUs;\n frames: RcFrame[];\n size: number;\n}\n\nexport interface L1CacheMetadata {\n size: number;\n maxSize: number;\n entries: number;\n clipCount: number;\n}\n\n/**\n * Simplified VideoL1Cache for 2-Clip strategy\n *\n * Clip lifecycle is managed by ClipSessionManager:\n * - No LRU eviction (clips evicted explicitly)\n * - No capacity limits (fixed 3 clips)\n * - Simple GOP storage per clip\n */\nexport class VideoL1Cache {\n private readonly entriesByClip = new Map<string, L1CacheEntry[]>();\n private maxMemoryBytes: number;\n private gopIntervalUs: number;\n private currentBytes = 0;\n\n constructor(config: L1Config) {\n this.maxMemoryBytes = config.maxMemoryMB * BYTES_PER_MB;\n this.gopIntervalUs = config.gopIntervalUs ?? DEFAULT_GOP_INTERVAL_US;\n }\n\n get(timeUs: TimeUs, clipId: string): RcFrame | null {\n const clipEntries = this.entriesByClip.get(clipId);\n if (!clipEntries || clipEntries.length === 0) {\n return null;\n }\n\n const entry = this.findEntryByTime(clipEntries, timeUs);\n if (!entry) {\n return null;\n }\n\n const frameIndex = findFrameIndex(\n {\n index: entry.gopIndex,\n startUs: entry.gopStartUs,\n durationUs: entry.durationUs,\n frames: entry.frames,\n isKeyframe: true,\n clipId: entry.clipId,\n },\n timeUs\n );\n if (frameIndex === -1) {\n return null;\n }\n\n return entry.frames[frameIndex] ?? null;\n }\n\n putGOP(gop: GOP, clipId: string, trackId: string): void {\n const gopIndex = gop.index ?? gop.startUs;\n const existing = this.getEntryExact(clipId, gopIndex);\n\n const frames = this.wrapFrames(gop.frames, clipId, gop.durationUs, trackId);\n\n if (existing) {\n existing.gopStartUs = gop.startUs;\n this.mergeFrames(existing, frames, gop.durationUs);\n return;\n }\n\n const entry = this.createEntry(clipId, {\n gopIndex,\n startUs: gop.startUs,\n durationUs: gop.durationUs,\n frames,\n isKeyframe: gop.isKeyframe,\n });\n\n this.registerEntry(entry);\n this.currentBytes += entry.size;\n }\n\n addFrame(\n frame: VideoFrame,\n clipId: string,\n frameDuration: TimeUs,\n trackId: string,\n gopSerial?: number,\n isKeyframe?: boolean\n ): RcFrame | null {\n const timestamp = frame.timestamp ?? 0;\n const gopIndex =\n typeof gopSerial === 'number' ? gopSerial : findGopIndex(timestamp, this.gopIntervalUs);\n const existing = this.getEntryExact(clipId, gopIndex);\n\n const rcFrame = this.wrapFrame(frame, clipId, frameDuration, trackId, gopSerial, isKeyframe);\n\n if (existing) {\n this.mergeFrames(existing, [rcFrame], frameDuration);\n return rcFrame;\n }\n\n const entry = this.createEntry(clipId, {\n gopIndex,\n startUs: timestamp,\n durationUs: frameDuration,\n frames: [rcFrame],\n isKeyframe: isKeyframe ?? true,\n });\n\n this.registerEntry(entry);\n this.currentBytes += entry.size;\n return rcFrame;\n }\n\n /**\n * Evict all cache entries for a specific clip\n */\n invalidateClip(clipId: string): void {\n const entries = this.entriesByClip.get(clipId);\n if (!entries) return;\n\n for (const entry of entries) {\n this.closeFrames(entry);\n this.currentBytes -= entry.size;\n }\n\n this.entriesByClip.delete(clipId);\n }\n\n /**\n * Check if a clip has any cached entries\n */\n hasClip(clipId: string): boolean {\n const entries = this.entriesByClip.get(clipId);\n return !!entries && entries.length > 0;\n }\n\n /**\n * Get total frame count for a clip\n * @param clipId - Clip identifier\n * @param startTimeUs - Optional start time to count frames from (inclusive)\n */\n getClipFrameCount(clipId: string, startTimeUs?: TimeUs): number {\n const entries = this.entriesByClip.get(clipId);\n if (!entries) return 0;\n\n // If no startTimeUs specified, return total frame count (backward compatible)\n if (startTimeUs === undefined) {\n return entries.reduce((sum, entry) => sum + entry.frames.length, 0);\n }\n\n // Count only frames at or after startTimeUs\n // Always check individual frame timestamps since gopSerial might not split correctly\n let count = 0;\n for (const entry of entries) {\n for (const frame of entry.frames) {\n const frameTime = frame.timestampUs ?? entry.gopStartUs;\n if (frameTime >= startTimeUs) {\n count++;\n }\n }\n }\n\n return count;\n }\n\n // invalidateRange(startUs: TimeUs, endUs: TimeUs, clipId?: string): void {\n // for (const entry of this.iterateEntries()) {\n // if (clipId && entry.clipId !== clipId) {\n // continue;\n // }\n\n // const gopEnd = entry.gopStartUs + entry.durationUs;\n // if (entry.gopStartUs < endUs && gopEnd > startUs) {\n // this.removeEntry(entry);\n // }\n // }\n // }\n\n clear(): void {\n for (const entries of this.entriesByClip.values()) {\n for (const entry of entries) {\n this.closeFrames(entry);\n }\n }\n this.entriesByClip.clear();\n this.currentBytes = 0;\n }\n\n getMetadata(): L1CacheMetadata {\n return {\n size: this.currentBytes,\n maxSize: this.maxMemoryBytes,\n entries: this.countEntries(),\n clipCount: this.entriesByClip.size,\n };\n }\n\n private registerEntry(entry: L1CacheEntry): void {\n const entries = this.ensureClipEntries(entry.clipId);\n const insertIndex = this.findInsertIndex(entries, entry.gopIndex);\n entries.splice(insertIndex, 0, entry);\n }\n\n private createEntry(\n clipId: string,\n gop: {\n gopIndex: number;\n startUs: TimeUs;\n durationUs: TimeUs;\n frames: RcFrame[];\n isKeyframe: boolean;\n }\n ): L1CacheEntry {\n const frames = this.normalizeFrames(gop.frames);\n const entry: L1CacheEntry = {\n key: this.composeKey(clipId, gop.gopIndex),\n clipId,\n gopIndex: gop.gopIndex,\n gopStartUs: gop.startUs,\n durationUs: gop.durationUs,\n frames,\n size: 0,\n };\n\n this.updateEntryStats(entry, this.deriveFallbackDuration(gop.durationUs, frames.length));\n return entry;\n }\n\n private mergeFrames(entry: L1CacheEntry, frames: RcFrame[], fallbackDuration: TimeUs): void {\n const durationFallback = this.deriveFallbackDuration(\n fallbackDuration,\n entry.frames.length + frames.length\n );\n for (const rcFrame of frames) {\n this.insertFrame(entry, rcFrame, durationFallback);\n }\n this.updateEntryStats(entry, durationFallback);\n }\n\n private insertFrame(entry: L1CacheEntry, frame: RcFrame, fallbackDuration: TimeUs): void {\n const timestamp = frame.timestampUs ?? entry.gopStartUs;\n const frames = entry.frames;\n const insertIndex = this.findFrameInsertIndex(frames, timestamp);\n\n if (\n insertIndex < frames.length &&\n (frames[insertIndex]?.timestampUs ?? entry.gopStartUs) === timestamp\n ) {\n const oldFrame = frames[insertIndex];\n frames[insertIndex] = frame;\n oldFrame?.close?.();\n } else {\n frames.splice(insertIndex, 0, frame);\n }\n\n entry.size += frame.sizeEstimate;\n const duration = frame.durationUs || fallbackDuration;\n entry.durationUs = Math.max(entry.durationUs, timestamp + duration - entry.gopStartUs);\n }\n\n private closeFrames(entry: L1CacheEntry): void {\n for (const frame of entry.frames) {\n frame?.close?.();\n }\n }\n\n private composeKey(clipId: string, gopIndex: number): string {\n return `${clipId}:${gopIndex}`;\n }\n\n private wrapFrame(\n frame: VideoFrame,\n clipId: string,\n frameDuration: TimeUs,\n trackId: string,\n gopSerial?: number,\n isKeyframe?: boolean\n ): RcFrame {\n return RcFrame.wrap(frame, {\n trackId,\n clipId,\n timestampUs: frame.timestamp ?? 0,\n durationUs: frame.duration ?? frameDuration,\n gopSerial,\n isKeyframe,\n });\n }\n\n private wrapFrames(\n frames: (RcFrame | VideoFrame)[],\n clipId: string,\n fallbackDuration: TimeUs,\n trackId: string\n ): RcFrame[] {\n const wrapped = frames.map((frame) =>\n frame instanceof RcFrame\n ? frame\n : this.wrapFrame(\n frame as VideoFrame,\n clipId,\n fallbackDuration / Math.max(frames.length, 1),\n trackId\n )\n );\n return this.normalizeFrames(wrapped);\n }\n\n private getEntryExact(clipId: string, gopIndex: number): L1CacheEntry | undefined {\n const entries = this.entriesByClip.get(clipId);\n if (!entries) return undefined;\n return entries.find((e) => e.gopIndex === gopIndex);\n }\n\n private findEntryByTime(entries: L1CacheEntry[], timeUs: TimeUs): L1CacheEntry | undefined {\n let low = 0;\n let high = entries.length - 1;\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const entry = entries[mid];\n if (!entry) break;\n\n if (timeUs < entry.gopStartUs) {\n high = mid - 1;\n } else if (timeUs >= entry.gopStartUs + entry.durationUs) {\n low = mid + 1;\n } else {\n return entry;\n }\n }\n\n return undefined;\n }\n\n private countEntries(): number {\n let count = 0;\n for (const clipEntries of this.entriesByClip.values()) {\n count += clipEntries.length;\n }\n return count;\n }\n\n private ensureClipEntries(clipId: string): L1CacheEntry[] {\n let entries = this.entriesByClip.get(clipId);\n if (!entries) {\n entries = [];\n this.entriesByClip.set(clipId, entries);\n }\n return entries;\n }\n\n private findInsertIndex(entries: L1CacheEntry[], gopIndex: number): number {\n let low = 0;\n let high = entries.length;\n while (low < high) {\n const mid = Math.floor((low + high) / 2);\n const entry = entries[mid];\n if (entry && entry.gopIndex < gopIndex) {\n low = mid + 1;\n } else {\n high = mid;\n }\n }\n return low;\n }\n\n private findFrameInsertIndex(frames: RcFrame[], timestamp: TimeUs): number {\n let low = 0;\n let high = frames.length;\n while (low < high) {\n const mid = Math.floor((low + high) / 2);\n const midTs = frames[mid]?.timestampUs ?? 0;\n if (midTs < timestamp) {\n low = mid + 1;\n } else {\n high = mid;\n }\n }\n return low;\n }\n\n private normalizeFrames(frames: RcFrame[]): RcFrame[] {\n const seen = new Set<number>();\n const sorted = [...frames].sort((a, b) => (a.timestampUs ?? 0) - (b.timestampUs ?? 0));\n const result: RcFrame[] = [];\n for (const frame of sorted) {\n const ts = frame.timestampUs ?? 0;\n if (seen.has(ts)) {\n frame?.close?.();\n } else {\n seen.add(ts);\n result.push(frame);\n }\n }\n return result;\n }\n\n private updateEntryStats(entry: L1CacheEntry, fallbackDuration: TimeUs): void {\n entry.size = entry.frames.reduce((acc, frame) => acc + frame.sizeEstimate, 0);\n entry.durationUs = entry.frames.reduce((acc, frame) => {\n const duration = frame.durationUs || fallbackDuration;\n return Math.max(acc, (frame.timestampUs ?? entry.gopStartUs) + duration - entry.gopStartUs);\n }, entry.durationUs);\n }\n\n private deriveFallbackDuration(durationUs: TimeUs, frameCount: number): TimeUs {\n if (frameCount <= 1) {\n return durationUs || this.gopIntervalUs;\n }\n return Math.max(durationUs / frameCount, this.gopIntervalUs / frameCount);\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -24,6 +24,8 @@ export declare class PlaybackController implements IPlaybackController, PreviewH
|
|
|
24
24
|
private audioContext;
|
|
25
25
|
private audioSession;
|
|
26
26
|
private isBuffering;
|
|
27
|
+
private currentSeekId;
|
|
28
|
+
private wasPlayingBeforeSeek;
|
|
27
29
|
constructor(orchestrator: Orchestrator, eventBus: IEventBus, options: PlaybackOptions);
|
|
28
30
|
play(): void;
|
|
29
31
|
private startPlayback;
|
|
@@ -43,7 +45,7 @@ export declare class PlaybackController implements IPlaybackController, PreviewH
|
|
|
43
45
|
private setupListeners;
|
|
44
46
|
private playbackLoop;
|
|
45
47
|
private updateTime;
|
|
46
|
-
|
|
48
|
+
renderCurrentFrame(timeUs: TimeUs, immediate?: boolean): Promise<void>;
|
|
47
49
|
private handlePlaybackBuffering;
|
|
48
50
|
private clampTime;
|
|
49
51
|
dispose(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlaybackController.d.ts","sourceRoot":"","sources":["../../src/controllers/PlaybackController.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EAEnB,eAAe,EACf,SAAS,EACT,aAAa,EACb,MAAM,EACP,MAAM,SAAS,CAAC;AAGjB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAC/E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"PlaybackController.d.ts","sourceRoot":"","sources":["../../src/controllers/PlaybackController.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EAEnB,eAAe,EACf,SAAS,EACT,aAAa,EACb,MAAM,EACP,MAAM,SAAS,CAAC;AAGjB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAC/E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD;;;GAGG;AACH,qBAAa,kBAAmB,YAAW,mBAAmB,EAAE,aAAa;IAC3E,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAY;IAC5B,OAAO,CAAC,MAAM,CAAsC;IACpD,OAAO,CAAC,GAAG,CAA+D;IAG1E,aAAa,EAAE,MAAM,CAAK;IAC1B,OAAO,CAAC,KAAK,CAAyB;IACtC,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,MAAM,CAAO;IACrB,OAAO,CAAC,IAAI,CAAS;IAGrB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,SAAS,CAAK;IAGtB,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,YAAY,CAAmC;IAGvD,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,oBAAoB,CAAS;gBAEzB,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe;IAyCrF,IAAI,IAAI,IAAI;YAOE,aAAa;IA6B3B,KAAK,IAAI,IAAI;IAgBb,IAAI,IAAI,IAAI;IAiBN,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8CzC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAW3B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAO/B,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAQ7B,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAI5B,IAAI,QAAQ,IAAI,MAAM,CAOrB;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,eAAe,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI;IAKlD,MAAM,IAAI,IAAI;IAId,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAIxD,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAIzD,OAAO,CAAC,cAAc;IAatB,OAAO,CAAC,YAAY;IA2CpB,OAAO,CAAC,UAAU;IAuBZ,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,UAAO,GAAG,OAAO,CAAC,IAAI,CAAC;YAsB3D,uBAAuB;IAsDrC,OAAO,CAAC,SAAS;IAKjB,OAAO,IAAI,IAAI;YAID,kBAAkB;CAOjC"}
|