@meframe/core 0.1.7 → 0.1.8
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/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +3 -2
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/cache/l1/AudioL1Cache.d.ts +3 -0
- package/dist/cache/l1/AudioL1Cache.d.ts.map +1 -1
- package/dist/cache/l1/AudioL1Cache.js +27 -33
- package/dist/cache/l1/AudioL1Cache.js.map +1 -1
- package/dist/controllers/PlaybackController.d.ts +2 -0
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +82 -13
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/controllers/PlaybackStateMachine.d.ts.map +1 -1
- package/dist/controllers/PlaybackStateMachine.js +13 -8
- package/dist/controllers/PlaybackStateMachine.js.map +1 -1
- package/dist/controllers/types.d.ts +10 -3
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/controllers/types.js +1 -0
- package/dist/controllers/types.js.map +1 -1
- package/dist/model/types.d.ts +0 -1
- package/dist/model/types.d.ts.map +1 -1
- package/dist/model/types.js.map +1 -1
- package/dist/orchestrator/ExportScheduler.d.ts.map +1 -1
- package/dist/orchestrator/ExportScheduler.js +22 -19
- package/dist/orchestrator/ExportScheduler.js.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.d.ts +14 -1
- package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.js +39 -10
- package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +8 -3
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +4 -2
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/orchestrator/types.d.ts +7 -1
- package/dist/orchestrator/types.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +13 -6
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/worker/BaseWorker.d.ts +2 -0
- package/dist/worker/BaseWorker.d.ts.map +1 -1
- package/dist/worker/BaseWorker.js.map +1 -1
- package/dist/worker/WorkerChannel.d.ts +2 -0
- package/dist/worker/WorkerChannel.d.ts.map +1 -1
- package/dist/worker/WorkerChannel.js +17 -1
- package/dist/worker/WorkerChannel.js.map +1 -1
- package/dist/workers/{WorkerChannel.DjBEVvEA.js → WorkerChannel.DQK8rAab.js} +18 -2
- package/dist/workers/{WorkerChannel.DjBEVvEA.js.map → WorkerChannel.DQK8rAab.js.map} +1 -1
- package/dist/workers/stages/compose/{audio-compose.worker.CiM_KP27.js → audio-compose.worker.B4Io5w9i.js} +2 -2
- package/dist/workers/stages/compose/{audio-compose.worker.CiM_KP27.js.map → audio-compose.worker.B4Io5w9i.js.map} +1 -1
- package/dist/workers/stages/compose/{video-compose.worker.CQwmNfXT.js → video-compose.worker.CA2_Kpg-.js} +2 -2
- package/dist/workers/stages/compose/{video-compose.worker.CQwmNfXT.js.map → video-compose.worker.CA2_Kpg-.js.map} +1 -1
- package/dist/workers/stages/decode/{audio-decode.worker.CpjkrZtT.js → audio-decode.worker.-DGlQrJD.js} +2 -2
- package/dist/workers/stages/decode/{audio-decode.worker.CpjkrZtT.js.map → audio-decode.worker.-DGlQrJD.js.map} +1 -1
- package/dist/workers/stages/decode/{video-decode.worker.BQtw6eWn.js → video-decode.worker.BnWVUkng.js} +2 -2
- package/dist/workers/stages/decode/{video-decode.worker.BQtw6eWn.js.map → video-decode.worker.BnWVUkng.js.map} +1 -1
- package/dist/workers/stages/demux/{audio-demux.worker.C4V11GQi.js → audio-demux.worker.D-_LoVqW.js} +2 -2
- package/dist/workers/stages/demux/{audio-demux.worker.C4V11GQi.js.map → audio-demux.worker.D-_LoVqW.js.map} +1 -1
- package/dist/workers/stages/demux/{video-demux.worker.5pJr0Ij-.js → video-demux.worker.BWDrLGni.js} +2 -2
- package/dist/workers/stages/demux/{video-demux.worker.5pJr0Ij-.js.map → video-demux.worker.BWDrLGni.js.map} +1 -1
- package/dist/workers/stages/encode/{video-encode.worker.CX2_3YhQ.js → video-encode.worker.D6aB_rF9.js} +2 -2
- package/dist/workers/stages/encode/{video-encode.worker.CX2_3YhQ.js.map → video-encode.worker.D6aB_rF9.js.map} +1 -1
- package/dist/workers/worker-manifest.json +7 -7
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CacheManager.d.ts","sourceRoot":"","sources":["../../src/cache/CacheManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"CacheManager.d.ts","sourceRoot":"","sources":["../../src/cache/CacheManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { VideoL1Cache } from "./l1/VideoL1Cache.js";
|
|
2
2
|
import { MeframeEvent } from "../event/events.js";
|
|
3
|
-
import { AudioL1Cache } from "./l1/AudioL1Cache.js";
|
|
3
|
+
import { AudioL1Cache, EXPORT_PCM_COVERAGE_THRESHOLD, PREVIEW_PCM_COVERAGE_THRESHOLD } from "./l1/AudioL1Cache.js";
|
|
4
4
|
import { WaiterReplacedError } from "../utils/errors.js";
|
|
5
5
|
import { OPFSManager } from "./storage/opfs/OPFSManager.js";
|
|
6
6
|
import { ResourceCache } from "./resource/ResourceCache.js";
|
|
@@ -72,7 +72,8 @@ class CacheManager {
|
|
|
72
72
|
* Returns true only if at least threshold (default 80%) of requested duration is available
|
|
73
73
|
*/
|
|
74
74
|
hasWindowPCM(clipId, startUs, endUs, strictMode) {
|
|
75
|
-
|
|
75
|
+
const threshold = strictMode ? EXPORT_PCM_COVERAGE_THRESHOLD : PREVIEW_PCM_COVERAGE_THRESHOLD;
|
|
76
|
+
return this.audioL1Cache.getAudioRangeCoverage(clipId, startUs, endUs, threshold).covered;
|
|
76
77
|
}
|
|
77
78
|
/**
|
|
78
79
|
* Get audio range coverage information for incremental decoding
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CacheManager.js","sources":["../../src/cache/CacheManager.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\nimport { RcFrame } from '../model';\nimport { VideoL1Cache } from './l1/VideoL1Cache';\nimport { MeframeEvent } from '../event/events';\nimport type { EventBus } from '../event/EventBus';\nimport type { EventPayloadMap } from '../event/events';\nimport { AudioL1Cache } from './l1/AudioL1Cache';\nimport { WaiterReplacedError } from '../utils/errors';\nimport { OPFSManager } from './storage/opfs/OPFSManager';\nimport { ResourceCache } from './resource/ResourceCache';\nimport { MP4IndexCache } from './resource/MP4IndexCache';\nimport { AudioSampleCache } from './resource/AudioSampleCache';\nimport type { MP4Index } from '../stages/demux/types';\n\ninterface CacheManagerConfig {\n l1: {\n maxMemoryMB: number;\n maxGOPs?: number;\n gopIntervalUs?: number;\n };\n resource: {\n projectId: string;\n };\n}\n\ninterface ClipReadyWaiter {\n clipId: string;\n minFrameCount: number;\n startTimeUs: TimeUs;\n currentCount: number;\n resolve: (ready: boolean) => void;\n reject: (reason?: unknown) => void;\n timeoutId?: ReturnType<typeof setTimeout>;\n}\n\n/**\n * CacheManager for Window Cache Architecture\n *\n * Core features:\n * - L1 (VRAM) for composed VideoFrames (window cache ±3s)\n * - ResourceCache (OPFS) for original MP4 files\n * - MP4IndexCache (RAM) for GOP/Sample indexes\n * - AudioSampleCache (RAM) for extracted audio chunks\n */\nexport class CacheManager {\n readonly videoL1Cache: VideoL1Cache;\n private readonly audioL1Cache: AudioL1Cache;\n readonly resourceCache: ResourceCache;\n readonly mp4IndexCache: MP4IndexCache;\n readonly audioSampleCache: AudioSampleCache;\n private clipReadyWaiters = new Map<string, ClipReadyWaiter>();\n private eventBus?: EventBus<EventPayloadMap>;\n\n constructor(config: CacheManagerConfig, eventBus?: EventBus<EventPayloadMap>) {\n this.videoL1Cache = new VideoL1Cache(config.l1);\n this.audioL1Cache = new AudioL1Cache();\n\n const opfsManager = new OPFSManager();\n this.resourceCache = new ResourceCache(opfsManager, config.resource.projectId);\n this.mp4IndexCache = new MP4IndexCache();\n this.audioSampleCache = new AudioSampleCache();\n this.eventBus = eventBus;\n }\n\n async init(): Promise<void> {\n await this.resourceCache.init();\n }\n\n /**\n * Set window center for L1 cache management (unified for video and audio)\n * L1 cache uses center ±3.5s window for video, ±5s for audio\n */\n setWindow(centerTimeUs: TimeUs): void {\n this.videoL1Cache.setWindow(centerTimeUs);\n this.audioL1Cache.setWindow(centerTimeUs);\n }\n\n /**\n * Check if resource exists in OPFS cache\n */\n async hasResourceInCache(resourceId: string): Promise<boolean> {\n return await this.resourceCache.hasResource(resourceId);\n }\n\n /**\n * Read byte range from resource (for GOP-level access)\n */\n async readResourceRange(resourceId: string, start: number, end: number): Promise<ArrayBuffer> {\n return await this.resourceCache.readRange(resourceId, start, end);\n }\n\n /**\n * Get MP4 index for a resource\n */\n getMP4Index(resourceId: string): MP4Index | null {\n return this.mp4IndexCache.get(resourceId);\n }\n\n putClipAudioData(\n clipId: string,\n audioData: AudioData,\n clipDurationUs: TimeUs,\n globalTimeUs?: TimeUs\n ): void {\n this.audioL1Cache.putClipAudioData(clipId, audioData, clipDurationUs, globalTimeUs);\n }\n\n getClipPCMWithMetadata(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs\n ): { planes: Float32Array[]; sampleRate: number; numberOfChannels: number } | null {\n return this.audioL1Cache.getPCMWithMetadata(clipId, startUs, endUs);\n }\n\n hasClipPCM(clipId: string): boolean {\n return this.audioL1Cache.hasClipPCM(clipId);\n }\n\n /**\n * Check if a specific audio chunk is already cached (by timestamp)\n */\n hasAudioSlotAt(clipId: string, timestampUs: TimeUs): boolean {\n return this.audioL1Cache.hasSlotAt(clipId, timestampUs);\n }\n\n /**\n * Check if sufficient PCM data exists for the requested time window\n * Returns true only if at least threshold (default 80%) of requested duration is available\n */\n hasWindowPCM(clipId: string, startUs: TimeUs, endUs: TimeUs, strictMode?: boolean): boolean {\n return this.audioL1Cache.hasWindowData(clipId, startUs, endUs, strictMode);\n }\n\n /**\n * Get audio range coverage information for incremental decoding\n * @param clipId - Clip identifier\n * @param startUs - Range start time in microseconds\n * @param endUs - Range end time in microseconds\n * @param threshold - Coverage threshold (0-1)\n * @returns Coverage info { covered: boolean, coverageRatio: number }\n */\n getAudioRangeCoverage(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs,\n threshold?: number\n ): { covered: boolean; coverageRatio: number } {\n return this.audioL1Cache.getAudioRangeCoverage(clipId, startUs, endUs, threshold);\n }\n\n clearAudioCache(): void {\n this.audioL1Cache.clear();\n }\n\n clearClipAudioData(clipId: string): void {\n this.audioL1Cache.clearClipPCM(clipId);\n }\n\n /**\n * Add a frame to L1 cache\n * Handles event notifications (CacheCover, ComposeFrameReady)\n * @param frame - VideoFrame to add\n * @param clipId - Clip identifier\n * @param frameDuration - Frame duration in microseconds\n * @param trackId - Track identifier\n * @param globalTimeUs - Global timestamp for event emission and window management\n */\n addFrame(\n frame: VideoFrame,\n clipId: string,\n frameDuration: TimeUs,\n trackId: string,\n globalTimeUs: TimeUs\n ): RcFrame {\n const rcFrame = this.videoL1Cache.addFrame(frame, clipId, frameDuration, trackId, globalTimeUs);\n\n const relativeTimeUs = frame.timestamp ?? 0;\n\n // Check and notify clip ready\n this.checkAndNotifyClipReady(clipId, relativeTimeUs);\n\n // Emit cover event for first frame (globalTimeUs = 0 in composition)\n if (globalTimeUs === 0) {\n this.eventBus?.emit(MeframeEvent.CacheCover, {\n timeUs: globalTimeUs,\n clipId,\n level: 'L1',\n size: rcFrame.sizeEstimate ?? 0,\n });\n }\n\n // Emit frame ready event\n this.eventBus?.emit(MeframeEvent.ComposeFrameReady, {\n timeUs: globalTimeUs,\n frameNumber: Math.floor(globalTimeUs / frameDuration),\n renderTimeMs: 0,\n trackId,\n clipId,\n });\n\n return rcFrame;\n }\n\n /**\n * Get frame from L1 cache\n * @param timeUs - Clip-relative timestamp (0-based)\n * @param clipId - Clip identifier\n */\n getFrame(timeUs: TimeUs, clipId: string): RcFrame | null {\n return this.videoL1Cache.get(timeUs, clipId);\n }\n\n async invalidateClip(clipId: string): Promise<void> {\n this.videoL1Cache.invalidateClip(clipId);\n }\n\n /**\n * Evict a clip from L1 cache\n */\n invalidateClipInL1(clipId: string): void {\n this.videoL1Cache.invalidateClip(clipId);\n }\n\n /**\n * Check if a clip is cached in L1\n */\n hasClipInL1(clipId: string): boolean {\n return this.videoL1Cache.hasClip(clipId);\n }\n\n /**\n * Wait for a clip to have minimum frames cached\n * Used by PlaybackController for buffering state\n * Only one waiter per clip - new waiter replaces old one\n */\n waitForClipReady(\n clipId: string,\n options: { minFrameCount?: number; timeoutMs?: number; startTimeUs?: TimeUs } = {}\n ): Promise<boolean> {\n const minFrameCount = options.minFrameCount ?? 30;\n const startTimeUs = options.startTimeUs ?? 0;\n\n // Check if already have enough frames\n const currentFrameCount = this.videoL1Cache.getClipFrameCount(clipId, startTimeUs);\n if (currentFrameCount >= minFrameCount) {\n return Promise.resolve(true);\n }\n\n // Cancel previous waiter if exists\n const oldWaiter = this.clipReadyWaiters.get(clipId);\n if (oldWaiter) {\n if (oldWaiter.timeoutId) {\n clearTimeout(oldWaiter.timeoutId);\n }\n oldWaiter.reject(new WaiterReplacedError(clipId));\n }\n\n return new Promise<boolean>((resolve, reject) => {\n const waiter: ClipReadyWaiter = {\n clipId,\n minFrameCount,\n startTimeUs,\n currentCount: currentFrameCount,\n resolve,\n reject,\n };\n\n this.clipReadyWaiters.set(clipId, waiter);\n\n if (options.timeoutMs && options.timeoutMs > 0) {\n waiter.timeoutId = setTimeout(() => {\n if (this.clipReadyWaiters.get(clipId) === waiter) {\n this.clipReadyWaiters.delete(clipId);\n }\n resolve(false);\n }, options.timeoutMs);\n }\n });\n }\n\n clearVideoCache(): void {\n this.videoL1Cache.clear();\n }\n\n async clear(): Promise<void> {\n this.clearVideoCache();\n this.clearAudioCache();\n }\n\n getMetadata() {\n return {\n l1: this.videoL1Cache.getMetadata(),\n };\n }\n\n /**\n * Check if incoming frame satisfies clip ready condition\n * O(1) complexity - only checks single waiter and increments counter\n */\n checkAndNotifyClipReady(clipId: string, frameTimestampUs: TimeUs): void {\n const waiter = this.clipReadyWaiters.get(clipId);\n if (!waiter) {\n return;\n }\n\n // Count all frames if startTimeUs is 0 (buffering scenario)\n // Otherwise only count frames at or after the target start time\n const shouldCount = waiter.startTimeUs === 0 || frameTimestampUs >= waiter.startTimeUs;\n\n if (shouldCount) {\n waiter.currentCount++;\n\n if (waiter.currentCount >= waiter.minFrameCount) {\n if (waiter.timeoutId) {\n clearTimeout(waiter.timeoutId);\n }\n waiter.resolve(true);\n this.clipReadyWaiters.delete(clipId);\n }\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CacheManager.js","sources":["../../src/cache/CacheManager.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\nimport { RcFrame } from '../model';\nimport { VideoL1Cache } from './l1/VideoL1Cache';\nimport { MeframeEvent } from '../event/events';\nimport type { EventBus } from '../event/EventBus';\nimport type { EventPayloadMap } from '../event/events';\nimport {\n AudioL1Cache,\n EXPORT_PCM_COVERAGE_THRESHOLD,\n PREVIEW_PCM_COVERAGE_THRESHOLD,\n} from './l1/AudioL1Cache';\nimport { WaiterReplacedError } from '../utils/errors';\nimport { OPFSManager } from './storage/opfs/OPFSManager';\nimport { ResourceCache } from './resource/ResourceCache';\nimport { MP4IndexCache } from './resource/MP4IndexCache';\nimport { AudioSampleCache } from './resource/AudioSampleCache';\nimport type { MP4Index } from '../stages/demux/types';\n\ninterface CacheManagerConfig {\n l1: {\n maxMemoryMB: number;\n maxGOPs?: number;\n gopIntervalUs?: number;\n };\n resource: {\n projectId: string;\n };\n}\n\ninterface ClipReadyWaiter {\n clipId: string;\n minFrameCount: number;\n startTimeUs: TimeUs;\n currentCount: number;\n resolve: (ready: boolean) => void;\n reject: (reason?: unknown) => void;\n timeoutId?: ReturnType<typeof setTimeout>;\n}\n\n/**\n * CacheManager for Window Cache Architecture\n *\n * Core features:\n * - L1 (VRAM) for composed VideoFrames (window cache ±3s)\n * - ResourceCache (OPFS) for original MP4 files\n * - MP4IndexCache (RAM) for GOP/Sample indexes\n * - AudioSampleCache (RAM) for extracted audio chunks\n */\nexport class CacheManager {\n readonly videoL1Cache: VideoL1Cache;\n private readonly audioL1Cache: AudioL1Cache;\n readonly resourceCache: ResourceCache;\n readonly mp4IndexCache: MP4IndexCache;\n readonly audioSampleCache: AudioSampleCache;\n private clipReadyWaiters = new Map<string, ClipReadyWaiter>();\n private eventBus?: EventBus<EventPayloadMap>;\n\n constructor(config: CacheManagerConfig, eventBus?: EventBus<EventPayloadMap>) {\n this.videoL1Cache = new VideoL1Cache(config.l1);\n this.audioL1Cache = new AudioL1Cache();\n\n const opfsManager = new OPFSManager();\n this.resourceCache = new ResourceCache(opfsManager, config.resource.projectId);\n this.mp4IndexCache = new MP4IndexCache();\n this.audioSampleCache = new AudioSampleCache();\n this.eventBus = eventBus;\n }\n\n async init(): Promise<void> {\n await this.resourceCache.init();\n }\n\n /**\n * Set window center for L1 cache management (unified for video and audio)\n * L1 cache uses center ±3.5s window for video, ±5s for audio\n */\n setWindow(centerTimeUs: TimeUs): void {\n this.videoL1Cache.setWindow(centerTimeUs);\n this.audioL1Cache.setWindow(centerTimeUs);\n }\n\n /**\n * Check if resource exists in OPFS cache\n */\n async hasResourceInCache(resourceId: string): Promise<boolean> {\n return await this.resourceCache.hasResource(resourceId);\n }\n\n /**\n * Read byte range from resource (for GOP-level access)\n */\n async readResourceRange(resourceId: string, start: number, end: number): Promise<ArrayBuffer> {\n return await this.resourceCache.readRange(resourceId, start, end);\n }\n\n /**\n * Get MP4 index for a resource\n */\n getMP4Index(resourceId: string): MP4Index | null {\n return this.mp4IndexCache.get(resourceId);\n }\n\n putClipAudioData(\n clipId: string,\n audioData: AudioData,\n clipDurationUs: TimeUs,\n globalTimeUs?: TimeUs\n ): void {\n this.audioL1Cache.putClipAudioData(clipId, audioData, clipDurationUs, globalTimeUs);\n }\n\n getClipPCMWithMetadata(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs\n ): { planes: Float32Array[]; sampleRate: number; numberOfChannels: number } | null {\n return this.audioL1Cache.getPCMWithMetadata(clipId, startUs, endUs);\n }\n\n hasClipPCM(clipId: string): boolean {\n return this.audioL1Cache.hasClipPCM(clipId);\n }\n\n /**\n * Check if a specific audio chunk is already cached (by timestamp)\n */\n hasAudioSlotAt(clipId: string, timestampUs: TimeUs): boolean {\n return this.audioL1Cache.hasSlotAt(clipId, timestampUs);\n }\n\n /**\n * Check if sufficient PCM data exists for the requested time window\n * Returns true only if at least threshold (default 80%) of requested duration is available\n */\n hasWindowPCM(clipId: string, startUs: TimeUs, endUs: TimeUs, strictMode?: boolean): boolean {\n const threshold = strictMode ? EXPORT_PCM_COVERAGE_THRESHOLD : PREVIEW_PCM_COVERAGE_THRESHOLD;\n return this.audioL1Cache.getAudioRangeCoverage(clipId, startUs, endUs, threshold).covered;\n }\n\n /**\n * Get audio range coverage information for incremental decoding\n * @param clipId - Clip identifier\n * @param startUs - Range start time in microseconds\n * @param endUs - Range end time in microseconds\n * @param threshold - Coverage threshold (0-1)\n * @returns Coverage info { covered: boolean, coverageRatio: number }\n */\n getAudioRangeCoverage(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs,\n threshold?: number\n ): { covered: boolean; coverageRatio: number } {\n return this.audioL1Cache.getAudioRangeCoverage(clipId, startUs, endUs, threshold);\n }\n\n clearAudioCache(): void {\n this.audioL1Cache.clear();\n }\n\n clearClipAudioData(clipId: string): void {\n this.audioL1Cache.clearClipPCM(clipId);\n }\n\n /**\n * Add a frame to L1 cache\n * Handles event notifications (CacheCover, ComposeFrameReady)\n * @param frame - VideoFrame to add\n * @param clipId - Clip identifier\n * @param frameDuration - Frame duration in microseconds\n * @param trackId - Track identifier\n * @param globalTimeUs - Global timestamp for event emission and window management\n */\n addFrame(\n frame: VideoFrame,\n clipId: string,\n frameDuration: TimeUs,\n trackId: string,\n globalTimeUs: TimeUs\n ): RcFrame {\n const rcFrame = this.videoL1Cache.addFrame(frame, clipId, frameDuration, trackId, globalTimeUs);\n\n const relativeTimeUs = frame.timestamp ?? 0;\n\n // Check and notify clip ready\n this.checkAndNotifyClipReady(clipId, relativeTimeUs);\n\n // Emit cover event for first frame (globalTimeUs = 0 in composition)\n if (globalTimeUs === 0) {\n this.eventBus?.emit(MeframeEvent.CacheCover, {\n timeUs: globalTimeUs,\n clipId,\n level: 'L1',\n size: rcFrame.sizeEstimate ?? 0,\n });\n }\n\n // Emit frame ready event\n this.eventBus?.emit(MeframeEvent.ComposeFrameReady, {\n timeUs: globalTimeUs,\n frameNumber: Math.floor(globalTimeUs / frameDuration),\n renderTimeMs: 0,\n trackId,\n clipId,\n });\n\n return rcFrame;\n }\n\n /**\n * Get frame from L1 cache\n * @param timeUs - Clip-relative timestamp (0-based)\n * @param clipId - Clip identifier\n */\n getFrame(timeUs: TimeUs, clipId: string): RcFrame | null {\n return this.videoL1Cache.get(timeUs, clipId);\n }\n\n async invalidateClip(clipId: string): Promise<void> {\n this.videoL1Cache.invalidateClip(clipId);\n }\n\n /**\n * Evict a clip from L1 cache\n */\n invalidateClipInL1(clipId: string): void {\n this.videoL1Cache.invalidateClip(clipId);\n }\n\n /**\n * Check if a clip is cached in L1\n */\n hasClipInL1(clipId: string): boolean {\n return this.videoL1Cache.hasClip(clipId);\n }\n\n /**\n * Wait for a clip to have minimum frames cached\n * Used by PlaybackController for buffering state\n * Only one waiter per clip - new waiter replaces old one\n */\n waitForClipReady(\n clipId: string,\n options: { minFrameCount?: number; timeoutMs?: number; startTimeUs?: TimeUs } = {}\n ): Promise<boolean> {\n const minFrameCount = options.minFrameCount ?? 30;\n const startTimeUs = options.startTimeUs ?? 0;\n\n // Check if already have enough frames\n const currentFrameCount = this.videoL1Cache.getClipFrameCount(clipId, startTimeUs);\n if (currentFrameCount >= minFrameCount) {\n return Promise.resolve(true);\n }\n\n // Cancel previous waiter if exists\n const oldWaiter = this.clipReadyWaiters.get(clipId);\n if (oldWaiter) {\n if (oldWaiter.timeoutId) {\n clearTimeout(oldWaiter.timeoutId);\n }\n oldWaiter.reject(new WaiterReplacedError(clipId));\n }\n\n return new Promise<boolean>((resolve, reject) => {\n const waiter: ClipReadyWaiter = {\n clipId,\n minFrameCount,\n startTimeUs,\n currentCount: currentFrameCount,\n resolve,\n reject,\n };\n\n this.clipReadyWaiters.set(clipId, waiter);\n\n if (options.timeoutMs && options.timeoutMs > 0) {\n waiter.timeoutId = setTimeout(() => {\n if (this.clipReadyWaiters.get(clipId) === waiter) {\n this.clipReadyWaiters.delete(clipId);\n }\n resolve(false);\n }, options.timeoutMs);\n }\n });\n }\n\n clearVideoCache(): void {\n this.videoL1Cache.clear();\n }\n\n async clear(): Promise<void> {\n this.clearVideoCache();\n this.clearAudioCache();\n }\n\n getMetadata() {\n return {\n l1: this.videoL1Cache.getMetadata(),\n };\n }\n\n /**\n * Check if incoming frame satisfies clip ready condition\n * O(1) complexity - only checks single waiter and increments counter\n */\n checkAndNotifyClipReady(clipId: string, frameTimestampUs: TimeUs): void {\n const waiter = this.clipReadyWaiters.get(clipId);\n if (!waiter) {\n return;\n }\n\n // Count all frames if startTimeUs is 0 (buffering scenario)\n // Otherwise only count frames at or after the target start time\n const shouldCount = waiter.startTimeUs === 0 || frameTimestampUs >= waiter.startTimeUs;\n\n if (shouldCount) {\n waiter.currentCount++;\n\n if (waiter.currentCount >= waiter.minFrameCount) {\n if (waiter.timeoutId) {\n clearTimeout(waiter.timeoutId);\n }\n waiter.resolve(true);\n this.clipReadyWaiters.delete(clipId);\n }\n }\n }\n}\n"],"names":[],"mappings":";;;;;;;;AAgDO,MAAM,aAAa;AAAA,EACf;AAAA,EACQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACD,uCAAuB,IAAA;AAAA,EACvB;AAAA,EAER,YAAY,QAA4B,UAAsC;AAC5E,SAAK,eAAe,IAAI,aAAa,OAAO,EAAE;AAC9C,SAAK,eAAe,IAAI,aAAA;AAExB,UAAM,cAAc,IAAI,YAAA;AACxB,SAAK,gBAAgB,IAAI,cAAc,aAAa,OAAO,SAAS,SAAS;AAC7E,SAAK,gBAAgB,IAAI,cAAA;AACzB,SAAK,mBAAmB,IAAI,iBAAA;AAC5B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAM,OAAsB;AAC1B,UAAM,KAAK,cAAc,KAAA;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAU,cAA4B;AACpC,SAAK,aAAa,UAAU,YAAY;AACxC,SAAK,aAAa,UAAU,YAAY;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,mBAAmB,YAAsC;AAC7D,WAAO,MAAM,KAAK,cAAc,YAAY,UAAU;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,kBAAkB,YAAoB,OAAe,KAAmC;AAC5F,WAAO,MAAM,KAAK,cAAc,UAAU,YAAY,OAAO,GAAG;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,YAAqC;AAC/C,WAAO,KAAK,cAAc,IAAI,UAAU;AAAA,EAC1C;AAAA,EAEA,iBACE,QACA,WACA,gBACA,cACM;AACN,SAAK,aAAa,iBAAiB,QAAQ,WAAW,gBAAgB,YAAY;AAAA,EACpF;AAAA,EAEA,uBACE,QACA,SACA,OACiF;AACjF,WAAO,KAAK,aAAa,mBAAmB,QAAQ,SAAS,KAAK;AAAA,EACpE;AAAA,EAEA,WAAW,QAAyB;AAClC,WAAO,KAAK,aAAa,WAAW,MAAM;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,QAAgB,aAA8B;AAC3D,WAAO,KAAK,aAAa,UAAU,QAAQ,WAAW;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa,QAAgB,SAAiB,OAAe,YAA+B;AAC1F,UAAM,YAAY,aAAa,gCAAgC;AAC/D,WAAO,KAAK,aAAa,sBAAsB,QAAQ,SAAS,OAAO,SAAS,EAAE;AAAA,EACpF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,sBACE,QACA,SACA,OACA,WAC6C;AAC7C,WAAO,KAAK,aAAa,sBAAsB,QAAQ,SAAS,OAAO,SAAS;AAAA,EAClF;AAAA,EAEA,kBAAwB;AACtB,SAAK,aAAa,MAAA;AAAA,EACpB;AAAA,EAEA,mBAAmB,QAAsB;AACvC,SAAK,aAAa,aAAa,MAAM;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,SACE,OACA,QACA,eACA,SACA,cACS;AACT,UAAM,UAAU,KAAK,aAAa,SAAS,OAAO,QAAQ,eAAe,SAAS,YAAY;AAE9F,UAAM,iBAAiB,MAAM,aAAa;AAG1C,SAAK,wBAAwB,QAAQ,cAAc;AAGnD,QAAI,iBAAiB,GAAG;AACtB,WAAK,UAAU,KAAK,aAAa,YAAY;AAAA,QAC3C,QAAQ;AAAA,QACR;AAAA,QACA,OAAO;AAAA,QACP,MAAM,QAAQ,gBAAgB;AAAA,MAAA,CAC/B;AAAA,IACH;AAGA,SAAK,UAAU,KAAK,aAAa,mBAAmB;AAAA,MAClD,QAAQ;AAAA,MACR,aAAa,KAAK,MAAM,eAAe,aAAa;AAAA,MACpD,cAAc;AAAA,MACd;AAAA,MACA;AAAA,IAAA,CACD;AAED,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,SAAS,QAAgB,QAAgC;AACvD,WAAO,KAAK,aAAa,IAAI,QAAQ,MAAM;AAAA,EAC7C;AAAA,EAEA,MAAM,eAAe,QAA+B;AAClD,SAAK,aAAa,eAAe,MAAM;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAmB,QAAsB;AACvC,SAAK,aAAa,eAAe,MAAM;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,QAAyB;AACnC,WAAO,KAAK,aAAa,QAAQ,MAAM;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBACE,QACA,UAAgF,IAC9D;AAClB,UAAM,gBAAgB,QAAQ,iBAAiB;AAC/C,UAAM,cAAc,QAAQ,eAAe;AAG3C,UAAM,oBAAoB,KAAK,aAAa,kBAAkB,QAAQ,WAAW;AACjF,QAAI,qBAAqB,eAAe;AACtC,aAAO,QAAQ,QAAQ,IAAI;AAAA,IAC7B;AAGA,UAAM,YAAY,KAAK,iBAAiB,IAAI,MAAM;AAClD,QAAI,WAAW;AACb,UAAI,UAAU,WAAW;AACvB,qBAAa,UAAU,SAAS;AAAA,MAClC;AACA,gBAAU,OAAO,IAAI,oBAAoB,MAAM,CAAC;AAAA,IAClD;AAEA,WAAO,IAAI,QAAiB,CAAC,SAAS,WAAW;AAC/C,YAAM,SAA0B;AAAA,QAC9B;AAAA,QACA;AAAA,QACA;AAAA,QACA,cAAc;AAAA,QACd;AAAA,QACA;AAAA,MAAA;AAGF,WAAK,iBAAiB,IAAI,QAAQ,MAAM;AAExC,UAAI,QAAQ,aAAa,QAAQ,YAAY,GAAG;AAC9C,eAAO,YAAY,WAAW,MAAM;AAClC,cAAI,KAAK,iBAAiB,IAAI,MAAM,MAAM,QAAQ;AAChD,iBAAK,iBAAiB,OAAO,MAAM;AAAA,UACrC;AACA,kBAAQ,KAAK;AAAA,QACf,GAAG,QAAQ,SAAS;AAAA,MACtB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,kBAAwB;AACtB,SAAK,aAAa,MAAA;AAAA,EACpB;AAAA,EAEA,MAAM,QAAuB;AAC3B,SAAK,gBAAA;AACL,SAAK,gBAAA;AAAA,EACP;AAAA,EAEA,cAAc;AACZ,WAAO;AAAA,MACL,IAAI,KAAK,aAAa,YAAA;AAAA,IAAY;AAAA,EAEtC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,wBAAwB,QAAgB,kBAAgC;AACtE,UAAM,SAAS,KAAK,iBAAiB,IAAI,MAAM;AAC/C,QAAI,CAAC,QAAQ;AACX;AAAA,IACF;AAIA,UAAM,cAAc,OAAO,gBAAgB,KAAK,oBAAoB,OAAO;AAE3E,QAAI,aAAa;AACf,aAAO;AAEP,UAAI,OAAO,gBAAgB,OAAO,eAAe;AAC/C,YAAI,OAAO,WAAW;AACpB,uBAAa,OAAO,SAAS;AAAA,QAC/B;AACA,eAAO,QAAQ,IAAI;AACnB,aAAK,iBAAiB,OAAO,MAAM;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AACF;"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { TimeUs } from '../../model/types';
|
|
2
2
|
|
|
3
|
+
export declare const PREVIEW_PCM_COVERAGE_THRESHOLD = 0.95;
|
|
4
|
+
export declare const EXPORT_PCM_COVERAGE_THRESHOLD = 0.99;
|
|
3
5
|
interface AudioDataSlot {
|
|
4
6
|
timestampUs: TimeUs;
|
|
5
7
|
durationUs: TimeUs;
|
|
@@ -48,6 +50,7 @@ export declare class AudioL1Cache {
|
|
|
48
50
|
covered: boolean;
|
|
49
51
|
coverageRatio: number;
|
|
50
52
|
};
|
|
53
|
+
private computeRangeCoverageDuration;
|
|
51
54
|
clearClipPCM(clipId: string): void;
|
|
52
55
|
/**
|
|
53
56
|
* Update window center (unified global window)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioL1Cache.d.ts","sourceRoot":"","sources":["../../../src/cache/l1/AudioL1Cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAIhD,UAAU,aAAa;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B,qBAAa,YAAY;IAEvB,OAAO,CAAC,eAAe,CAAsC;IAI7D,OAAO,CAAC,YAAY,CAAa;IAGjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAa;IAC3C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAO;IACzC,OAAO,CAAC,aAAa,CAAK;IAE1B,gBAAgB,CACd,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,EACpB,eAAe,EAAE,MAAM,EACvB,YAAY,CAAC,EAAE,MAAM,GACpB,IAAI;IAiEP,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,GAAG,IAAI;IAmBxF,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,EAAE,GAAG,IAAI;IA8F7E,kBAAkB,CAChB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ;QAAE,MAAM,EAAE,YAAY,EAAE,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAoBlF,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAInC;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,GAAE,MAAa,GAAG,OAAO;IAmBnF;;;;;;OAMG;IACH,aAAa,CACX,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,UAAU,GAAE,OAAe,GAC1B,OAAO;
|
|
1
|
+
{"version":3,"file":"AudioL1Cache.d.ts","sourceRoot":"","sources":["../../../src/cache/l1/AudioL1Cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAIhD,eAAO,MAAM,8BAA8B,OAAO,CAAC;AACnD,eAAO,MAAM,6BAA6B,OAAO,CAAC;AAElD,UAAU,aAAa;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B,qBAAa,YAAY;IAEvB,OAAO,CAAC,eAAe,CAAsC;IAI7D,OAAO,CAAC,YAAY,CAAa;IAGjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAa;IAC3C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAO;IACzC,OAAO,CAAC,aAAa,CAAK;IAE1B,gBAAgB,CACd,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,EACpB,eAAe,EAAE,MAAM,EACvB,YAAY,CAAC,EAAE,MAAM,GACpB,IAAI;IAiEP,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,GAAG,IAAI;IAmBxF,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,EAAE,GAAG,IAAI;IA8F7E,kBAAkB,CAChB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ;QAAE,MAAM,EAAE,YAAY,EAAE,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAoBlF,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAInC;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,GAAE,MAAa,GAAG,OAAO;IAmBnF;;;;;;OAMG;IACH,aAAa,CACX,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,UAAU,GAAE,OAAe,GAC1B,OAAO;IAKV;;;;;;;OAOG;IACH,qBAAqB,CACnB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,SAAS,SAAiC,GACzC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE;IAa9C,OAAO,CAAC,4BAA4B;IAsCpC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAIlC;;;OAGG;IACH,SAAS,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;IAKvC,OAAO,CAAC,aAAa;IAQrB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IA+BxB;;OAEG;IACH,OAAO,CAAC,eAAe;IAIvB,KAAK,IAAI,IAAI;IAIb,KAAK,IAAI,IAAI;IAMb,OAAO,IAAI,IAAI;CAGhB"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { binarySearchOverlapping, binarySearchFirst } from "../../utils/binary-search.js";
|
|
2
2
|
import { extractPlanesFromAudioData } from "../../utils/audio-data.js";
|
|
3
|
+
const PREVIEW_PCM_COVERAGE_THRESHOLD = 0.95;
|
|
4
|
+
const EXPORT_PCM_COVERAGE_THRESHOLD = 0.99;
|
|
3
5
|
class AudioL1Cache {
|
|
4
6
|
// Aligned with VideoL1Cache: array of discrete audio data slots per clip
|
|
5
7
|
audioDataByClip = /* @__PURE__ */ new Map();
|
|
@@ -175,29 +177,8 @@ class AudioL1Cache {
|
|
|
175
177
|
* @param strictMode - If true, require 99% coverage (export). If false, accept 95% (preview)
|
|
176
178
|
*/
|
|
177
179
|
hasWindowData(clipId, startUs, endUs, strictMode = false) {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
const overlappingSlots = binarySearchOverlapping(slots, startUs, endUs, (slot) => ({
|
|
183
|
-
start: slot.timestampUs,
|
|
184
|
-
end: slot.timestampUs + slot.durationUs
|
|
185
|
-
}));
|
|
186
|
-
if (overlappingSlots.length === 0) {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
let coveredDurationUs = 0;
|
|
190
|
-
const requestedDurationUs = endUs - startUs;
|
|
191
|
-
for (const slot of overlappingSlots) {
|
|
192
|
-
const slotEndUs = slot.timestampUs + slot.durationUs;
|
|
193
|
-
const overlapStart = Math.max(slot.timestampUs, startUs);
|
|
194
|
-
const overlapEnd = Math.min(slotEndUs, endUs);
|
|
195
|
-
if (overlapStart < overlapEnd) {
|
|
196
|
-
coveredDurationUs += overlapEnd - overlapStart;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
const threshold = strictMode ? 0.99 : 0.95;
|
|
200
|
-
return coveredDurationUs >= requestedDurationUs * threshold;
|
|
180
|
+
const threshold = strictMode ? EXPORT_PCM_COVERAGE_THRESHOLD : PREVIEW_PCM_COVERAGE_THRESHOLD;
|
|
181
|
+
return this.getAudioRangeCoverage(clipId, startUs, endUs, threshold).covered;
|
|
201
182
|
}
|
|
202
183
|
/**
|
|
203
184
|
* Get audio range coverage information for incremental decoding
|
|
@@ -207,20 +188,35 @@ class AudioL1Cache {
|
|
|
207
188
|
* @param threshold - Coverage threshold (0-1), default 0.95
|
|
208
189
|
* @returns Coverage info { covered: boolean, coverageRatio: number }
|
|
209
190
|
*/
|
|
210
|
-
getAudioRangeCoverage(clipId, startUs, endUs, threshold =
|
|
191
|
+
getAudioRangeCoverage(clipId, startUs, endUs, threshold = PREVIEW_PCM_COVERAGE_THRESHOLD) {
|
|
192
|
+
const { coveredDurationUs, requestedDurationUs } = this.computeRangeCoverageDuration(
|
|
193
|
+
clipId,
|
|
194
|
+
startUs,
|
|
195
|
+
endUs
|
|
196
|
+
);
|
|
197
|
+
const coverageRatio = requestedDurationUs > 0 ? coveredDurationUs / requestedDurationUs : 0;
|
|
198
|
+
return {
|
|
199
|
+
covered: coverageRatio >= threshold,
|
|
200
|
+
coverageRatio
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
computeRangeCoverageDuration(clipId, startUs, endUs) {
|
|
204
|
+
const requestedDurationUs = Math.max(0, endUs - startUs);
|
|
205
|
+
if (requestedDurationUs === 0) {
|
|
206
|
+
return { coveredDurationUs: 0, requestedDurationUs: 0 };
|
|
207
|
+
}
|
|
211
208
|
const slots = this.audioDataByClip.get(clipId);
|
|
212
209
|
if (!slots || slots.length === 0) {
|
|
213
|
-
return {
|
|
210
|
+
return { coveredDurationUs: 0, requestedDurationUs };
|
|
214
211
|
}
|
|
215
212
|
const overlappingSlots = binarySearchOverlapping(slots, startUs, endUs, (slot) => ({
|
|
216
213
|
start: slot.timestampUs,
|
|
217
214
|
end: slot.timestampUs + slot.durationUs
|
|
218
215
|
}));
|
|
219
216
|
if (overlappingSlots.length === 0) {
|
|
220
|
-
return {
|
|
217
|
+
return { coveredDurationUs: 0, requestedDurationUs };
|
|
221
218
|
}
|
|
222
219
|
let coveredDurationUs = 0;
|
|
223
|
-
const requestedDurationUs = endUs - startUs;
|
|
224
220
|
for (const slot of overlappingSlots) {
|
|
225
221
|
const slotEndUs = slot.timestampUs + slot.durationUs;
|
|
226
222
|
const overlapStart = Math.max(startUs, slot.timestampUs);
|
|
@@ -229,11 +225,7 @@ class AudioL1Cache {
|
|
|
229
225
|
coveredDurationUs += overlapEnd - overlapStart;
|
|
230
226
|
}
|
|
231
227
|
}
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
covered: coverageRatio >= threshold,
|
|
235
|
-
coverageRatio
|
|
236
|
-
};
|
|
228
|
+
return { coveredDurationUs, requestedDurationUs };
|
|
237
229
|
}
|
|
238
230
|
clearClipPCM(clipId) {
|
|
239
231
|
this.audioDataByClip.delete(clipId);
|
|
@@ -298,6 +290,8 @@ class AudioL1Cache {
|
|
|
298
290
|
}
|
|
299
291
|
}
|
|
300
292
|
export {
|
|
301
|
-
AudioL1Cache
|
|
293
|
+
AudioL1Cache,
|
|
294
|
+
EXPORT_PCM_COVERAGE_THRESHOLD,
|
|
295
|
+
PREVIEW_PCM_COVERAGE_THRESHOLD
|
|
302
296
|
};
|
|
303
297
|
//# sourceMappingURL=AudioL1Cache.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioL1Cache.js","sources":["../../../src/cache/l1/AudioL1Cache.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport { binarySearchFirst, binarySearchOverlapping } from '../../utils/binary-search';\nimport { extractPlanesFromAudioData } from '../../utils/audio-data';\n\ninterface AudioDataSlot {\n timestampUs: TimeUs; // Resource timestamp (aligned with VideoL1Cache)\n durationUs: TimeUs;\n planes: Float32Array[]; // PCM data for this slot\n sampleRate: number;\n numberOfChannels: number;\n globalTimeUs?: TimeUs; // Global timeline time (for window management)\n}\n\nexport type { AudioDataSlot };\n\nexport class AudioL1Cache {\n // Aligned with VideoL1Cache: array of discrete audio data slots per clip\n private audioDataByClip = new Map<string, AudioDataSlot[]>();\n\n // Unified window management (aligned with VideoL1Cache)\n // All clips share the same global window center\n private windowCenter: TimeUs = 0;\n\n // Window radius aligned with video (±3.5s, but we use 5s for audio safety margin)\n private readonly WINDOW_RADIUS = 5_000_000; // ±5s\n private readonly EVICT_THROTTLE_MS = 500;\n private lastEvictTime = 0;\n\n putClipAudioData(\n clipId: string,\n audioData: AudioData,\n _clipDurationUs: TimeUs,\n globalTimeUs?: TimeUs\n ): void {\n const numberOfChannels = audioData.numberOfChannels ?? 2;\n const numberOfFrames = audioData.numberOfFrames ?? 0;\n const sampleRate = audioData.sampleRate ?? 48_000;\n const audioTimestampUs = audioData.timestamp ?? 0;\n const audioDurationUs =\n audioData.duration ?? Math.round((numberOfFrames / sampleRate) * 1_000_000);\n\n if (!numberOfChannels || !numberOfFrames) {\n audioData.close();\n return;\n }\n\n // Extract PCM data\n const planes = extractPlanesFromAudioData(audioData, numberOfChannels, numberOfFrames);\n audioData.close();\n\n // Create audio data slot (aligned with video architecture)\n const slot: AudioDataSlot = {\n timestampUs: audioTimestampUs,\n durationUs: audioDurationUs,\n planes,\n sampleRate,\n numberOfChannels,\n globalTimeUs,\n };\n // Get or create slots array for this clip\n let slots = this.audioDataByClip.get(clipId);\n if (!slots) {\n slots = [];\n this.audioDataByClip.set(clipId, slots);\n }\n\n // Insert slot in sorted order (aligned with VideoL1Cache.addFrame)\n const insertIndex = this.findInsertIndex(slots, audioTimestampUs);\n\n // Deduplication: Check for duplicate or near-duplicate timestamp\n // Tolerance of 1ms to handle floating-point precision and concurrent decode attempts\n const DUPLICATE_THRESHOLD_US = 0.5 * audioDurationUs;\n\n if (insertIndex < slots.length) {\n const existingSlot = slots[insertIndex]!;\n const timeDiff = Math.abs(existingSlot.timestampUs - audioTimestampUs);\n\n if (timeDiff < DUPLICATE_THRESHOLD_US) {\n // Near-duplicate detected, skip insertion\n return;\n }\n }\n\n // Also check previous slot to catch overlapping duplicates\n if (insertIndex > 0) {\n const prevSlot = slots[insertIndex - 1]!;\n const timeDiff = Math.abs(prevSlot.timestampUs - audioTimestampUs);\n\n if (timeDiff < DUPLICATE_THRESHOLD_US) {\n // Near-duplicate detected, skip insertion\n return;\n }\n }\n\n // Insert new slot\n slots.splice(insertIndex, 0, slot);\n }\n\n getSlotsInWindow(clipId: string, startUs: TimeUs, endUs: TimeUs): AudioDataSlot[] | null {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots || slots.length === 0) {\n return null;\n }\n\n // Use binary search to find overlapping slots\n const overlappingSlots = binarySearchOverlapping(slots, startUs, endUs, (slot) => ({\n start: slot.timestampUs,\n end: slot.timestampUs + slot.durationUs,\n }));\n\n if (overlappingSlots.length === 0) {\n return null;\n }\n\n return overlappingSlots;\n }\n\n getPCM(clipId: string, startUs: TimeUs, endUs: TimeUs): Float32Array[] | null {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots || slots.length === 0) {\n return null;\n }\n\n // Use binary search to find overlapping slots (O(log n + k) vs O(n))\n // Aligned with video GOP/frame search\n const overlappingSlots = binarySearchOverlapping(slots, startUs, endUs, (slot) => ({\n start: slot.timestampUs,\n end: slot.timestampUs + slot.durationUs,\n }));\n\n if (overlappingSlots.length === 0) {\n return null;\n }\n\n const requestedDurationUs = endUs - startUs;\n\n // Validate sample rate consistency across all slots\n const firstSlot = overlappingSlots[0]!;\n const uniformSampleRate = firstSlot.sampleRate;\n const uniformChannels = firstSlot.numberOfChannels;\n\n // Check if all slots have the same sample rate and channel count\n const hasUniformRate = overlappingSlots.every(\n (s) => s.sampleRate === uniformSampleRate && s.numberOfChannels === uniformChannels\n );\n\n if (!hasUniformRate) {\n console.error(\n `[AudioL1Cache] Inconsistent sample rates detected for clip ${clipId}:`,\n overlappingSlots.map((s) => ({\n timestamp: s.timestampUs,\n sampleRate: s.sampleRate,\n channels: s.numberOfChannels,\n }))\n );\n // Return null to avoid corrupted audio data\n // This will trigger re-decode with correct sample rate\n return null;\n }\n\n // Calculate total frame count needed\n const totalFrames = Math.ceil((requestedDurationUs / 1_000_000) * uniformSampleRate);\n\n // Initialize result arrays\n const result: Float32Array[] = Array.from(\n { length: uniformChannels },\n () => new Float32Array(totalFrames)\n );\n\n // Copy data from each overlapping slot\n // Note: Slots are guaranteed non-overlapping by putClipAudioData deduplication\n for (const slot of overlappingSlots) {\n const slotStartUs = slot.timestampUs;\n const slotEndUs = slotStartUs + slot.durationUs;\n\n // Calculate intersection with requested range\n const copyStartUs = Math.max(slotStartUs, startUs);\n const copyEndUs = Math.min(slotEndUs, endUs);\n\n if (copyStartUs >= copyEndUs) continue;\n\n // Convert time to frame indices (all slots have same sample rate after validation)\n const srcOffsetFrames = Math.floor(\n ((copyStartUs - slotStartUs) / 1_000_000) * uniformSampleRate\n );\n const dstOffsetFrames = Math.floor(((copyStartUs - startUs) / 1_000_000) * uniformSampleRate);\n const copyFrameCount = Math.ceil(((copyEndUs - copyStartUs) / 1_000_000) * uniformSampleRate);\n\n // Copy each channel\n for (let ch = 0; ch < uniformChannels; ch++) {\n const srcPlane = slot.planes[ch];\n const dstPlane = result[ch];\n if (!srcPlane || !dstPlane) continue;\n\n // Boundary check\n const actualCopyFrames = Math.min(\n copyFrameCount,\n srcPlane.length - srcOffsetFrames,\n dstPlane.length - dstOffsetFrames\n );\n\n if (actualCopyFrames > 0) {\n const srcSlice = srcPlane.subarray(srcOffsetFrames, srcOffsetFrames + actualCopyFrames);\n dstPlane.set(srcSlice, dstOffsetFrames);\n }\n }\n }\n\n return result;\n }\n\n getPCMWithMetadata(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs\n ): { planes: Float32Array[]; sampleRate: number; numberOfChannels: number } | null {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots || slots.length === 0) {\n return null;\n }\n\n const planes = this.getPCM(clipId, startUs, endUs);\n if (!planes) {\n return null;\n }\n\n // Use first slot's metadata\n const firstSlot = slots[0]!;\n return {\n planes,\n sampleRate: firstSlot.sampleRate,\n numberOfChannels: firstSlot.numberOfChannels,\n };\n }\n\n hasClipPCM(clipId: string): boolean {\n return this.audioDataByClip.has(clipId);\n }\n\n /**\n * Check if a slot with specific timestamp exists (used for incremental decoding)\n */\n hasSlotAt(clipId: string, timestampUs: TimeUs, toleranceUs: number = 1000): boolean {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots) return false;\n\n // Since slots are sorted, we can use binary search\n const index = this.findInsertIndex(slots, timestampUs);\n\n // Check index and index-1 (in case findInsertIndex lands after)\n if (index < slots.length) {\n const slot = slots[index];\n if (slot && Math.abs(slot.timestampUs - timestampUs) < toleranceUs) return true;\n }\n if (index > 0) {\n const slot = slots[index - 1];\n if (slot && Math.abs(slot.timestampUs - timestampUs) < toleranceUs) return true;\n }\n return false;\n }\n\n /**\n * Check if sufficient PCM data exists for the requested time window\n * @param clipId - Clip identifier\n * @param startUs - Window start time\n * @param endUs - Window end time\n * @param strictMode - If true, require 99% coverage (export). If false, accept 95% (preview)\n */\n hasWindowData(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs,\n strictMode: boolean = false\n ): boolean {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots || slots.length === 0) {\n return false;\n }\n\n // Use binary search to find overlapping slots (performance optimization)\n const overlappingSlots = binarySearchOverlapping(slots, startUs, endUs, (slot) => ({\n start: slot.timestampUs,\n end: slot.timestampUs + slot.durationUs,\n }));\n\n if (overlappingSlots.length === 0) {\n return false;\n }\n\n // Calculate total duration covered\n let coveredDurationUs = 0;\n const requestedDurationUs = endUs - startUs;\n\n for (const slot of overlappingSlots) {\n const slotEndUs = slot.timestampUs + slot.durationUs;\n\n // Calculate overlap with requested range\n const overlapStart = Math.max(slot.timestampUs, startUs);\n const overlapEnd = Math.min(slotEndUs, endUs);\n\n if (overlapStart < overlapEnd) {\n coveredDurationUs += overlapEnd - overlapStart;\n }\n }\n\n // Adaptive threshold based on mode:\n // - Export (strictMode=true): 99% - ensures complete audio, critical for final output\n // - Preview (strictMode=false): 95% - balances performance and quality, tolerates chunk misalignment\n const threshold = strictMode ? 0.99 : 0.95;\n return coveredDurationUs >= requestedDurationUs * threshold;\n }\n\n /**\n * Get audio range coverage information for incremental decoding\n * @param clipId - Clip identifier\n * @param startUs - Range start time in microseconds\n * @param endUs - Range end time in microseconds\n * @param threshold - Coverage threshold (0-1), default 0.95\n * @returns Coverage info { covered: boolean, coverageRatio: number }\n */\n getAudioRangeCoverage(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs,\n threshold = 0.95\n ): { covered: boolean; coverageRatio: number } {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots || slots.length === 0) {\n return { covered: false, coverageRatio: 0 };\n }\n\n // Use binary search to find overlapping slots\n const overlappingSlots = binarySearchOverlapping(slots, startUs, endUs, (slot) => ({\n start: slot.timestampUs,\n end: slot.timestampUs + slot.durationUs,\n }));\n\n if (overlappingSlots.length === 0) {\n return { covered: false, coverageRatio: 0 };\n }\n\n // Calculate covered duration within [startUs, endUs]\n let coveredDurationUs = 0;\n const requestedDurationUs = endUs - startUs;\n\n for (const slot of overlappingSlots) {\n const slotEndUs = slot.timestampUs + slot.durationUs;\n\n // Calculate overlap with requested range\n const overlapStart = Math.max(startUs, slot.timestampUs);\n const overlapEnd = Math.min(endUs, slotEndUs);\n\n if (overlapStart < overlapEnd) {\n coveredDurationUs += overlapEnd - overlapStart;\n }\n }\n\n const coverageRatio = requestedDurationUs > 0 ? coveredDurationUs / requestedDurationUs : 0;\n return {\n covered: coverageRatio >= threshold,\n coverageRatio,\n };\n }\n\n clearClipPCM(clipId: string): void {\n this.audioDataByClip.delete(clipId);\n }\n\n /**\n * Update window center (unified global window)\n * Aligned with VideoL1Cache strategy: maintains a window of ±RADIUS around center\n */\n setWindow(centerGlobalUs: TimeUs): void {\n this.windowCenter = centerGlobalUs;\n this.checkEviction();\n }\n\n private checkEviction(): void {\n const now = Date.now();\n if (now - this.lastEvictTime > this.EVICT_THROTTLE_MS) {\n this.evictOutOfWindow();\n this.lastEvictTime = now;\n }\n }\n\n /**\n * Evict audio slots outside the global window (aligned with VideoL1Cache)\n * Skip if eviction is disabled (e.g., during export)\n */\n private evictOutOfWindow(): void {\n const windowStart = Math.max(0, this.windowCenter - this.WINDOW_RADIUS);\n const windowEnd = this.windowCenter + this.WINDOW_RADIUS;\n\n for (const [clipId, slots] of this.audioDataByClip) {\n const toKeep: AudioDataSlot[] = [];\n\n for (const slot of slots) {\n const globalTime = slot.globalTimeUs;\n\n // Slots without globalTimeUs are kept (legacy)\n if (globalTime === undefined) {\n toKeep.push(slot);\n continue;\n }\n\n // Keep slots within window\n if (globalTime >= windowStart && globalTime <= windowEnd) {\n toKeep.push(slot);\n }\n // Slots outside window are discarded (no close needed for Float32Array)\n }\n\n if (toKeep.length > 0) {\n this.audioDataByClip.set(clipId, toKeep);\n } else {\n this.audioDataByClip.delete(clipId);\n }\n }\n }\n\n /**\n * Find insertion index for a new slot (aligned with VideoL1Cache)\n */\n private findInsertIndex(slots: AudioDataSlot[], timestamp: TimeUs): number {\n return binarySearchFirst(slots, (slot) => slot.timestampUs >= timestamp);\n }\n\n flush(): void {\n this.audioDataByClip.clear();\n }\n\n clear(): void {\n this.flush();\n this.audioDataByClip.clear();\n this.windowCenter = 0;\n }\n\n dispose(): void {\n this.clear();\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AudioL1Cache.js","sources":["../../../src/cache/l1/AudioL1Cache.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport { binarySearchFirst, binarySearchOverlapping } from '../../utils/binary-search';\nimport { extractPlanesFromAudioData } from '../../utils/audio-data';\n\nexport const PREVIEW_PCM_COVERAGE_THRESHOLD = 0.95;\nexport const EXPORT_PCM_COVERAGE_THRESHOLD = 0.99;\n\ninterface AudioDataSlot {\n timestampUs: TimeUs; // Resource timestamp (aligned with VideoL1Cache)\n durationUs: TimeUs;\n planes: Float32Array[]; // PCM data for this slot\n sampleRate: number;\n numberOfChannels: number;\n globalTimeUs?: TimeUs; // Global timeline time (for window management)\n}\n\nexport type { AudioDataSlot };\n\nexport class AudioL1Cache {\n // Aligned with VideoL1Cache: array of discrete audio data slots per clip\n private audioDataByClip = new Map<string, AudioDataSlot[]>();\n\n // Unified window management (aligned with VideoL1Cache)\n // All clips share the same global window center\n private windowCenter: TimeUs = 0;\n\n // Window radius aligned with video (±3.5s, but we use 5s for audio safety margin)\n private readonly WINDOW_RADIUS = 5_000_000; // ±5s\n private readonly EVICT_THROTTLE_MS = 500;\n private lastEvictTime = 0;\n\n putClipAudioData(\n clipId: string,\n audioData: AudioData,\n _clipDurationUs: TimeUs,\n globalTimeUs?: TimeUs\n ): void {\n const numberOfChannels = audioData.numberOfChannels ?? 2;\n const numberOfFrames = audioData.numberOfFrames ?? 0;\n const sampleRate = audioData.sampleRate ?? 48_000;\n const audioTimestampUs = audioData.timestamp ?? 0;\n const audioDurationUs =\n audioData.duration ?? Math.round((numberOfFrames / sampleRate) * 1_000_000);\n\n if (!numberOfChannels || !numberOfFrames) {\n audioData.close();\n return;\n }\n\n // Extract PCM data\n const planes = extractPlanesFromAudioData(audioData, numberOfChannels, numberOfFrames);\n audioData.close();\n\n // Create audio data slot (aligned with video architecture)\n const slot: AudioDataSlot = {\n timestampUs: audioTimestampUs,\n durationUs: audioDurationUs,\n planes,\n sampleRate,\n numberOfChannels,\n globalTimeUs,\n };\n // Get or create slots array for this clip\n let slots = this.audioDataByClip.get(clipId);\n if (!slots) {\n slots = [];\n this.audioDataByClip.set(clipId, slots);\n }\n\n // Insert slot in sorted order (aligned with VideoL1Cache.addFrame)\n const insertIndex = this.findInsertIndex(slots, audioTimestampUs);\n\n // Deduplication: Check for duplicate or near-duplicate timestamp\n // Tolerance of 1ms to handle floating-point precision and concurrent decode attempts\n const DUPLICATE_THRESHOLD_US = 0.5 * audioDurationUs;\n\n if (insertIndex < slots.length) {\n const existingSlot = slots[insertIndex]!;\n const timeDiff = Math.abs(existingSlot.timestampUs - audioTimestampUs);\n\n if (timeDiff < DUPLICATE_THRESHOLD_US) {\n // Near-duplicate detected, skip insertion\n return;\n }\n }\n\n // Also check previous slot to catch overlapping duplicates\n if (insertIndex > 0) {\n const prevSlot = slots[insertIndex - 1]!;\n const timeDiff = Math.abs(prevSlot.timestampUs - audioTimestampUs);\n\n if (timeDiff < DUPLICATE_THRESHOLD_US) {\n // Near-duplicate detected, skip insertion\n return;\n }\n }\n\n // Insert new slot\n slots.splice(insertIndex, 0, slot);\n }\n\n getSlotsInWindow(clipId: string, startUs: TimeUs, endUs: TimeUs): AudioDataSlot[] | null {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots || slots.length === 0) {\n return null;\n }\n\n // Use binary search to find overlapping slots\n const overlappingSlots = binarySearchOverlapping(slots, startUs, endUs, (slot) => ({\n start: slot.timestampUs,\n end: slot.timestampUs + slot.durationUs,\n }));\n\n if (overlappingSlots.length === 0) {\n return null;\n }\n\n return overlappingSlots;\n }\n\n getPCM(clipId: string, startUs: TimeUs, endUs: TimeUs): Float32Array[] | null {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots || slots.length === 0) {\n return null;\n }\n\n // Use binary search to find overlapping slots (O(log n + k) vs O(n))\n // Aligned with video GOP/frame search\n const overlappingSlots = binarySearchOverlapping(slots, startUs, endUs, (slot) => ({\n start: slot.timestampUs,\n end: slot.timestampUs + slot.durationUs,\n }));\n\n if (overlappingSlots.length === 0) {\n return null;\n }\n\n const requestedDurationUs = endUs - startUs;\n\n // Validate sample rate consistency across all slots\n const firstSlot = overlappingSlots[0]!;\n const uniformSampleRate = firstSlot.sampleRate;\n const uniformChannels = firstSlot.numberOfChannels;\n\n // Check if all slots have the same sample rate and channel count\n const hasUniformRate = overlappingSlots.every(\n (s) => s.sampleRate === uniformSampleRate && s.numberOfChannels === uniformChannels\n );\n\n if (!hasUniformRate) {\n console.error(\n `[AudioL1Cache] Inconsistent sample rates detected for clip ${clipId}:`,\n overlappingSlots.map((s) => ({\n timestamp: s.timestampUs,\n sampleRate: s.sampleRate,\n channels: s.numberOfChannels,\n }))\n );\n // Return null to avoid corrupted audio data\n // This will trigger re-decode with correct sample rate\n return null;\n }\n\n // Calculate total frame count needed\n const totalFrames = Math.ceil((requestedDurationUs / 1_000_000) * uniformSampleRate);\n\n // Initialize result arrays\n const result: Float32Array[] = Array.from(\n { length: uniformChannels },\n () => new Float32Array(totalFrames)\n );\n\n // Copy data from each overlapping slot\n // Note: Slots are guaranteed non-overlapping by putClipAudioData deduplication\n for (const slot of overlappingSlots) {\n const slotStartUs = slot.timestampUs;\n const slotEndUs = slotStartUs + slot.durationUs;\n\n // Calculate intersection with requested range\n const copyStartUs = Math.max(slotStartUs, startUs);\n const copyEndUs = Math.min(slotEndUs, endUs);\n\n if (copyStartUs >= copyEndUs) continue;\n\n // Convert time to frame indices (all slots have same sample rate after validation)\n const srcOffsetFrames = Math.floor(\n ((copyStartUs - slotStartUs) / 1_000_000) * uniformSampleRate\n );\n const dstOffsetFrames = Math.floor(((copyStartUs - startUs) / 1_000_000) * uniformSampleRate);\n const copyFrameCount = Math.ceil(((copyEndUs - copyStartUs) / 1_000_000) * uniformSampleRate);\n\n // Copy each channel\n for (let ch = 0; ch < uniformChannels; ch++) {\n const srcPlane = slot.planes[ch];\n const dstPlane = result[ch];\n if (!srcPlane || !dstPlane) continue;\n\n // Boundary check\n const actualCopyFrames = Math.min(\n copyFrameCount,\n srcPlane.length - srcOffsetFrames,\n dstPlane.length - dstOffsetFrames\n );\n\n if (actualCopyFrames > 0) {\n const srcSlice = srcPlane.subarray(srcOffsetFrames, srcOffsetFrames + actualCopyFrames);\n dstPlane.set(srcSlice, dstOffsetFrames);\n }\n }\n }\n\n return result;\n }\n\n getPCMWithMetadata(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs\n ): { planes: Float32Array[]; sampleRate: number; numberOfChannels: number } | null {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots || slots.length === 0) {\n return null;\n }\n\n const planes = this.getPCM(clipId, startUs, endUs);\n if (!planes) {\n return null;\n }\n\n // Use first slot's metadata\n const firstSlot = slots[0]!;\n return {\n planes,\n sampleRate: firstSlot.sampleRate,\n numberOfChannels: firstSlot.numberOfChannels,\n };\n }\n\n hasClipPCM(clipId: string): boolean {\n return this.audioDataByClip.has(clipId);\n }\n\n /**\n * Check if a slot with specific timestamp exists (used for incremental decoding)\n */\n hasSlotAt(clipId: string, timestampUs: TimeUs, toleranceUs: number = 1000): boolean {\n const slots = this.audioDataByClip.get(clipId);\n if (!slots) return false;\n\n // Since slots are sorted, we can use binary search\n const index = this.findInsertIndex(slots, timestampUs);\n\n // Check index and index-1 (in case findInsertIndex lands after)\n if (index < slots.length) {\n const slot = slots[index];\n if (slot && Math.abs(slot.timestampUs - timestampUs) < toleranceUs) return true;\n }\n if (index > 0) {\n const slot = slots[index - 1];\n if (slot && Math.abs(slot.timestampUs - timestampUs) < toleranceUs) return true;\n }\n return false;\n }\n\n /**\n * Check if sufficient PCM data exists for the requested time window\n * @param clipId - Clip identifier\n * @param startUs - Window start time\n * @param endUs - Window end time\n * @param strictMode - If true, require 99% coverage (export). If false, accept 95% (preview)\n */\n hasWindowData(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs,\n strictMode: boolean = false\n ): boolean {\n const threshold = strictMode ? EXPORT_PCM_COVERAGE_THRESHOLD : PREVIEW_PCM_COVERAGE_THRESHOLD;\n return this.getAudioRangeCoverage(clipId, startUs, endUs, threshold).covered;\n }\n\n /**\n * Get audio range coverage information for incremental decoding\n * @param clipId - Clip identifier\n * @param startUs - Range start time in microseconds\n * @param endUs - Range end time in microseconds\n * @param threshold - Coverage threshold (0-1), default 0.95\n * @returns Coverage info { covered: boolean, coverageRatio: number }\n */\n getAudioRangeCoverage(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs,\n threshold = PREVIEW_PCM_COVERAGE_THRESHOLD\n ): { covered: boolean; coverageRatio: number } {\n const { coveredDurationUs, requestedDurationUs } = this.computeRangeCoverageDuration(\n clipId,\n startUs,\n endUs\n );\n const coverageRatio = requestedDurationUs > 0 ? coveredDurationUs / requestedDurationUs : 0;\n return {\n covered: coverageRatio >= threshold,\n coverageRatio,\n };\n }\n\n private computeRangeCoverageDuration(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs\n ): { coveredDurationUs: TimeUs; requestedDurationUs: TimeUs } {\n const requestedDurationUs = Math.max(0, endUs - startUs);\n if (requestedDurationUs === 0) {\n return { coveredDurationUs: 0, requestedDurationUs: 0 };\n }\n\n const slots = this.audioDataByClip.get(clipId);\n if (!slots || slots.length === 0) {\n return { coveredDurationUs: 0, requestedDurationUs };\n }\n\n // Use binary search to find overlapping slots (O(log n + k))\n const overlappingSlots = binarySearchOverlapping(slots, startUs, endUs, (slot) => ({\n start: slot.timestampUs,\n end: slot.timestampUs + slot.durationUs,\n }));\n\n if (overlappingSlots.length === 0) {\n return { coveredDurationUs: 0, requestedDurationUs };\n }\n\n let coveredDurationUs = 0;\n for (const slot of overlappingSlots) {\n const slotEndUs = slot.timestampUs + slot.durationUs;\n const overlapStart = Math.max(startUs, slot.timestampUs);\n const overlapEnd = Math.min(endUs, slotEndUs);\n if (overlapStart < overlapEnd) {\n coveredDurationUs += overlapEnd - overlapStart;\n }\n }\n\n return { coveredDurationUs, requestedDurationUs };\n }\n\n clearClipPCM(clipId: string): void {\n this.audioDataByClip.delete(clipId);\n }\n\n /**\n * Update window center (unified global window)\n * Aligned with VideoL1Cache strategy: maintains a window of ±RADIUS around center\n */\n setWindow(centerGlobalUs: TimeUs): void {\n this.windowCenter = centerGlobalUs;\n this.checkEviction();\n }\n\n private checkEviction(): void {\n const now = Date.now();\n if (now - this.lastEvictTime > this.EVICT_THROTTLE_MS) {\n this.evictOutOfWindow();\n this.lastEvictTime = now;\n }\n }\n\n /**\n * Evict audio slots outside the global window (aligned with VideoL1Cache)\n * Skip if eviction is disabled (e.g., during export)\n */\n private evictOutOfWindow(): void {\n const windowStart = Math.max(0, this.windowCenter - this.WINDOW_RADIUS);\n const windowEnd = this.windowCenter + this.WINDOW_RADIUS;\n\n for (const [clipId, slots] of this.audioDataByClip) {\n const toKeep: AudioDataSlot[] = [];\n\n for (const slot of slots) {\n const globalTime = slot.globalTimeUs;\n\n // Slots without globalTimeUs are kept (legacy)\n if (globalTime === undefined) {\n toKeep.push(slot);\n continue;\n }\n\n // Keep slots within window\n if (globalTime >= windowStart && globalTime <= windowEnd) {\n toKeep.push(slot);\n }\n // Slots outside window are discarded (no close needed for Float32Array)\n }\n\n if (toKeep.length > 0) {\n this.audioDataByClip.set(clipId, toKeep);\n } else {\n this.audioDataByClip.delete(clipId);\n }\n }\n }\n\n /**\n * Find insertion index for a new slot (aligned with VideoL1Cache)\n */\n private findInsertIndex(slots: AudioDataSlot[], timestamp: TimeUs): number {\n return binarySearchFirst(slots, (slot) => slot.timestampUs >= timestamp);\n }\n\n flush(): void {\n this.audioDataByClip.clear();\n }\n\n clear(): void {\n this.flush();\n this.audioDataByClip.clear();\n this.windowCenter = 0;\n }\n\n dispose(): void {\n this.clear();\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -26,6 +26,7 @@ export declare class PlaybackController implements IPlaybackController, PreviewH
|
|
|
26
26
|
private fsm;
|
|
27
27
|
private windowEnd;
|
|
28
28
|
private readonly WINDOW_DURATION;
|
|
29
|
+
private readonly AUDIO_READY_PROBE_WINDOW;
|
|
29
30
|
private readonly PREHEAT_DISTANCE;
|
|
30
31
|
private preheatInProgress;
|
|
31
32
|
constructor(orchestrator: Orchestrator, eventBus: IEventBus, options: PlaybackOptions);
|
|
@@ -63,5 +64,6 @@ export declare class PlaybackController implements IPlaybackController, PreviewH
|
|
|
63
64
|
private onCacheCover;
|
|
64
65
|
private onModelSet;
|
|
65
66
|
private setupEventListeners;
|
|
67
|
+
private isVideoResourceReadyAtTime;
|
|
66
68
|
}
|
|
67
69
|
//# sourceMappingURL=PlaybackController.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlaybackController.d.ts","sourceRoot":"","sources":["../../src/controllers/PlaybackController.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,eAAe,EACf,SAAS,EACT,aAAa,EACb,MAAM,EACP,MAAM,SAAS,CAAC;AAWjB,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,EACnB,eAAe,EACf,SAAS,EACT,aAAa,EACb,MAAM,EACP,MAAM,SAAS,CAAC;AAWjB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAQpD;;;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,aAAa,CAA8B;IAGnD,aAAa,EAAE,MAAM,CAAK;IAC1B,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,MAAM,CAAO;IACrB,OAAO,CAAC,IAAI,CAAS;IAGrB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,WAAW,CAAa;IAGhC,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,GAAG,CAAK;IAGhB,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAW;IAGnD,OAAO,CAAC,GAAG,CAA8B;IAGzC,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAa;IAC7C,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAW;IACpD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAa;IAC9C,OAAO,CAAC,iBAAiB,CAAS;gBAEtB,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe;IAoC/E,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAMlC,IAAI,IAAI,IAAI;IAIZ,KAAK,IAAI,IAAI;IAIb,IAAI,IAAI,IAAI;IAIN,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASzC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAW3B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAM/B,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAU7B,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAI5B,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,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;IAMzD,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,eAAe;IAKvB,OAAO,CAAC,UAAU;IAkBlB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,cAAc;IAoMtB,OAAO,CAAC,SAAS;IAOjB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,iBAAiB;YAMX,SAAS;IAiEvB,OAAO,CAAC,SAAS;IAUjB,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,qBAAqB;IAgBvB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;YAoC1B,kBAAkB;YAwBlB,OAAO;IAYrB,OAAO,CAAC,WAAW;IAYnB,OAAO,IAAI,IAAI;IAUf,OAAO,CAAC,YAAY,CAIlB;IAEF,OAAO,CAAC,UAAU,CAchB;IAEF,OAAO,CAAC,mBAAmB;IAK3B,OAAO,CAAC,0BAA0B;CAUnC"}
|
|
@@ -34,6 +34,8 @@ class PlaybackController {
|
|
|
34
34
|
windowEnd = 0;
|
|
35
35
|
WINDOW_DURATION = 3e6;
|
|
36
36
|
// 3s decode window
|
|
37
|
+
AUDIO_READY_PROBE_WINDOW = 5e5;
|
|
38
|
+
// 500ms: gate buffering only for near-future audio
|
|
37
39
|
PREHEAT_DISTANCE = 1e6;
|
|
38
40
|
// 1s preheat trigger distance
|
|
39
41
|
preheatInProgress = false;
|
|
@@ -68,7 +70,7 @@ class PlaybackController {
|
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
async renderCover() {
|
|
71
|
-
await this.renderCurrentFrame(0, {
|
|
73
|
+
await this.renderCurrentFrame(0, { mode: "blocking" });
|
|
72
74
|
}
|
|
73
75
|
// ========= Public API =========
|
|
74
76
|
play() {
|
|
@@ -173,9 +175,23 @@ class PlaybackController {
|
|
|
173
175
|
if (command.ignoreWaiterReplacedError && error instanceof WaiterReplacedError) return;
|
|
174
176
|
if (command.logPrefix) console.error(command.logPrefix, error);
|
|
175
177
|
const onErrorDone = command.onError ? this.dispatch(command.onError).done : void 0;
|
|
178
|
+
const normalizeError = (e) => {
|
|
179
|
+
if (e instanceof Error) return e;
|
|
180
|
+
return new Error(typeof e === "string" ? e : JSON.stringify(e));
|
|
181
|
+
};
|
|
176
182
|
const emit = () => {
|
|
177
183
|
if (command.emitPlaybackError) {
|
|
178
|
-
|
|
184
|
+
const err = normalizeError(error);
|
|
185
|
+
this.eventBus.emit(MeframeEvent.PlaybackError, err);
|
|
186
|
+
this.eventBus.emit(MeframeEvent.Error, {
|
|
187
|
+
source: "playback",
|
|
188
|
+
error: err,
|
|
189
|
+
context: {
|
|
190
|
+
command: command.logPrefix,
|
|
191
|
+
onError: command.onError?.type
|
|
192
|
+
},
|
|
193
|
+
recoverable: false
|
|
194
|
+
});
|
|
179
195
|
}
|
|
180
196
|
};
|
|
181
197
|
if (onErrorDone) {
|
|
@@ -256,7 +272,7 @@ class PlaybackController {
|
|
|
256
272
|
}
|
|
257
273
|
case PlaybackCommandType.RenderFrame: {
|
|
258
274
|
return this.renderCurrentFrame(command.timeUs, {
|
|
259
|
-
|
|
275
|
+
mode: command.mode,
|
|
260
276
|
relativeTimeUs: command.relativeTimeUs
|
|
261
277
|
});
|
|
262
278
|
}
|
|
@@ -265,7 +281,7 @@ class PlaybackController {
|
|
|
265
281
|
if (!this.isCurrentToken(token)) return;
|
|
266
282
|
if (keyframeTimeUs === null) return;
|
|
267
283
|
return this.orchestrator.getRenderState(command.timeUs, {
|
|
268
|
-
|
|
284
|
+
mode: "probe",
|
|
269
285
|
relativeTimeUs: keyframeTimeUs
|
|
270
286
|
}).then((keyframeRenderState) => {
|
|
271
287
|
if (!this.isCurrentToken(token)) return;
|
|
@@ -276,18 +292,49 @@ class PlaybackController {
|
|
|
276
292
|
}
|
|
277
293
|
case PlaybackCommandType.EnsureAudio: {
|
|
278
294
|
return this.audioSession.ensureAudioForTime(command.timeUs, {
|
|
279
|
-
|
|
295
|
+
mode: command.mode
|
|
280
296
|
});
|
|
281
297
|
}
|
|
282
298
|
case PlaybackCommandType.GetFrame: {
|
|
283
299
|
return this.orchestrator.getFrame(command.timeUs, {
|
|
284
|
-
|
|
300
|
+
mode: command.mode,
|
|
285
301
|
preheat: command.preheat
|
|
286
|
-
}).then(() =>
|
|
302
|
+
}).then((frame) => {
|
|
303
|
+
if (!frame && command.mode === "blocking") {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`[PlaybackController] GetFrame miss in blocking mode at ${command.timeUs}`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
287
309
|
}
|
|
288
310
|
case PlaybackCommandType.StartAudioPlayback: {
|
|
289
311
|
return this.audioSession.startPlayback(command.timeUs, this.audioContext);
|
|
290
312
|
}
|
|
313
|
+
case PlaybackCommandType.ProbeStartReady: {
|
|
314
|
+
const audioWindowEnd = Math.min(
|
|
315
|
+
this.duration,
|
|
316
|
+
command.timeUs + this.AUDIO_READY_PROBE_WINDOW
|
|
317
|
+
);
|
|
318
|
+
const audioReady = this.audioSession.isAudioResourceWindowReady(
|
|
319
|
+
command.timeUs,
|
|
320
|
+
audioWindowEnd
|
|
321
|
+
);
|
|
322
|
+
const videoReady = this.isVideoResourceReadyAtTime(command.timeUs);
|
|
323
|
+
if (audioReady && videoReady) return;
|
|
324
|
+
if (!audioReady) {
|
|
325
|
+
void this.audioSession.ensureAudioForTime(command.timeUs, { mode: "probe" });
|
|
326
|
+
}
|
|
327
|
+
if (!videoReady) {
|
|
328
|
+
void this.orchestrator.getFrame(command.timeUs, { mode: "probe" });
|
|
329
|
+
}
|
|
330
|
+
this.dispatch({
|
|
331
|
+
type: PlaybackActionType.EnterBuffering,
|
|
332
|
+
timeUs: command.timeUs,
|
|
333
|
+
bumpToken: true,
|
|
334
|
+
reason: "startup"
|
|
335
|
+
});
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
291
338
|
case PlaybackCommandType.StartRafLoop: {
|
|
292
339
|
this.startPlaybackLoop(token);
|
|
293
340
|
return;
|
|
@@ -323,13 +370,22 @@ class PlaybackController {
|
|
|
323
370
|
if (!this.isCurrentToken(token) || this.fsm.snapshot.state !== PlaybackState.Playing) {
|
|
324
371
|
return;
|
|
325
372
|
}
|
|
373
|
+
const audioWindowEnd = Math.min(
|
|
374
|
+
this.duration,
|
|
375
|
+
this.currentTimeUs + this.AUDIO_READY_PROBE_WINDOW
|
|
376
|
+
);
|
|
377
|
+
if (!this.audioSession.isAudioResourceWindowReady(this.currentTimeUs, audioWindowEnd)) {
|
|
378
|
+
void this.audioSession.ensureAudioForTime(this.currentTimeUs, { mode: "probe" });
|
|
379
|
+
this.dispatch({ type: PlaybackActionType.EnterBuffering, timeUs: this.currentTimeUs });
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
326
382
|
if (this.currentTimeUs - this.lastAudioScheduleTime >= this.AUDIO_SCHEDULE_INTERVAL) {
|
|
327
383
|
await this.audioSession.scheduleAudio(this.currentTimeUs, this.audioContext);
|
|
328
384
|
if (!this.isCurrentToken(token) || this.fsm.snapshot.state !== PlaybackState.Playing) return;
|
|
329
385
|
this.lastAudioScheduleTime = this.currentTimeUs;
|
|
330
386
|
}
|
|
331
387
|
const renderState = await this.orchestrator.getRenderState(this.currentTimeUs, {
|
|
332
|
-
|
|
388
|
+
mode: "probe"
|
|
333
389
|
});
|
|
334
390
|
if (!this.isCurrentToken(token) || this.fsm.snapshot.state !== PlaybackState.Playing) {
|
|
335
391
|
return;
|
|
@@ -391,7 +447,7 @@ class PlaybackController {
|
|
|
391
447
|
this.orchestrator.preheatClipWindow(clip.id, clipWindowStart, clipWindowEnd, windowStart)
|
|
392
448
|
);
|
|
393
449
|
}
|
|
394
|
-
preheatPromises.push(this.audioSession.ensureAudioForTime(windowStart, {
|
|
450
|
+
preheatPromises.push(this.audioSession.ensureAudioForTime(windowStart, { mode: "blocking" }));
|
|
395
451
|
await Promise.all(preheatPromises);
|
|
396
452
|
this.windowEnd = windowEnd;
|
|
397
453
|
} catch (error) {
|
|
@@ -406,10 +462,13 @@ class PlaybackController {
|
|
|
406
462
|
return;
|
|
407
463
|
}
|
|
408
464
|
const renderState = await this.orchestrator.getRenderState(timeUs, {
|
|
409
|
-
|
|
465
|
+
mode: options.mode,
|
|
410
466
|
relativeTimeUs: options.relativeTimeUs
|
|
411
467
|
});
|
|
412
468
|
if (!renderState) {
|
|
469
|
+
if (options.mode === "blocking") {
|
|
470
|
+
throw new Error(`[PlaybackController] RenderFrame miss in blocking mode at ${timeUs}`);
|
|
471
|
+
}
|
|
413
472
|
return;
|
|
414
473
|
}
|
|
415
474
|
await this.compose(timeUs, renderState);
|
|
@@ -440,7 +499,7 @@ class PlaybackController {
|
|
|
440
499
|
}
|
|
441
500
|
onCacheCover = () => {
|
|
442
501
|
if (this.fsm.snapshot.state === PlaybackState.Idle && this.currentTimeUs === 0) {
|
|
443
|
-
void this.renderCurrentFrame(0, {
|
|
502
|
+
void this.renderCurrentFrame(0, { mode: "blocking" });
|
|
444
503
|
}
|
|
445
504
|
};
|
|
446
505
|
onModelSet = () => {
|
|
@@ -452,13 +511,23 @@ class PlaybackController {
|
|
|
452
511
|
fps: model.fps || 30,
|
|
453
512
|
backgroundColor: model.renderConfig?.backgroundColor || "#000"
|
|
454
513
|
});
|
|
455
|
-
void this.audioSession.ensureAudioForTime(this.currentTimeUs, {
|
|
456
|
-
void this.renderCurrentFrame(this.currentTimeUs, {
|
|
514
|
+
void this.audioSession.ensureAudioForTime(this.currentTimeUs, { mode: "blocking" });
|
|
515
|
+
void this.renderCurrentFrame(this.currentTimeUs, { mode: "blocking" });
|
|
457
516
|
};
|
|
458
517
|
setupEventListeners() {
|
|
459
518
|
this.eventBus.on(MeframeEvent.CacheCover, this.onCacheCover);
|
|
460
519
|
this.eventBus.on(MeframeEvent.ModelSet, this.onModelSet);
|
|
461
520
|
}
|
|
521
|
+
isVideoResourceReadyAtTime(timeUs) {
|
|
522
|
+
const model = this.orchestrator.compositionModel;
|
|
523
|
+
if (!model) return true;
|
|
524
|
+
const clip = model.getClipsAtTime(timeUs, model.mainTrackId)[0];
|
|
525
|
+
if (!clip || !("resourceId" in clip) || typeof clip.resourceId !== "string")
|
|
526
|
+
return true;
|
|
527
|
+
const resourceId = clip.resourceId;
|
|
528
|
+
const resource = model.getResource(resourceId);
|
|
529
|
+
return resource?.state === "ready";
|
|
530
|
+
}
|
|
462
531
|
}
|
|
463
532
|
export {
|
|
464
533
|
PlaybackController
|