@meframe/core 0.4.4 → 0.4.5
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/model/types.d.ts +6 -0
- package/dist/model/types.d.ts.map +1 -1
- package/dist/model/types.js +14 -1
- package/dist/model/types.js.map +1 -1
- package/dist/orchestrator/AudioWindowPreparer.d.ts.map +1 -1
- package/dist/orchestrator/AudioWindowPreparer.js +6 -3
- package/dist/orchestrator/AudioWindowPreparer.js.map +1 -1
- package/dist/orchestrator/CompositionPlanner.d.ts.map +1 -1
- package/dist/orchestrator/CompositionPlanner.js +4 -2
- package/dist/orchestrator/CompositionPlanner.js.map +1 -1
- package/dist/orchestrator/ExportScheduler.d.ts.map +1 -1
- package/dist/orchestrator/ExportScheduler.js +4 -2
- package/dist/orchestrator/ExportScheduler.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +18 -11
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoWindowDecodeSession.d.ts +3 -0
- package/dist/orchestrator/VideoWindowDecodeSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoWindowDecodeSession.js +41 -1
- package/dist/orchestrator/VideoWindowDecodeSession.js.map +1 -1
- package/dist/stages/compose/FrameRateConverter.d.ts +2 -1
- package/dist/stages/compose/FrameRateConverter.d.ts.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.d.ts.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.js +9 -3
- package/dist/stages/compose/OfflineAudioMixer.js.map +1 -1
- package/dist/utils/loop-utils.d.ts +2 -0
- package/dist/utils/loop-utils.d.ts.map +1 -1
- package/dist/utils/loop-utils.js +5 -2
- package/dist/utils/loop-utils.js.map +1 -1
- package/dist/workers/stages/export/{export.worker.DCStS1mL.js → export.worker.Cw9iPvkh.js} +27 -17
- package/dist/workers/stages/export/{export.worker.DCStS1mL.js.map → export.worker.Cw9iPvkh.js.map} +1 -1
- package/dist/workers/worker-manifest.json +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoWindowDecodeSession.js","sources":["../../src/orchestrator/VideoWindowDecodeSession.ts"],"sourcesContent":["import type { CacheManager } from '../cache/CacheManager';\nimport type { MP4IndexCache } from '../cache/resource/MP4IndexCache';\nimport type { MP4Index, GOP, Sample } from '../stages/demux/types';\nimport type { TimeUs, Resource } from '../model/types';\nimport type { CompositionModel, Clip } from '../model';\nimport { binarySearchOverlapping, binarySearchRange } from '../utils/binary-search';\nimport type { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { decodeChunksForScrub, decodeChunksWithoutFlush } from '../stages/decode/video-decoder';\nimport { ResourceCorruptedError } from '../utils/errors';\n\ninterface GOPWindowResult {\n gops: GOP[];\n byteStart: number;\n byteEnd: number;\n}\n\ninterface VideoWindowDecodeSessionConfig {\n clipId: string;\n resourceId: string;\n targetTimeUs: TimeUs;\n globalTimeUs: TimeUs;\n mp4IndexCache: MP4IndexCache;\n cacheManager: CacheManager;\n compositionModel: CompositionModel;\n resourceLoader: ResourceLoader;\n fps: number;\n}\n\n/**\n * Strategy:\n * 1. Read GOP range from OPFS\n * 2. Demux using MP4Box (main thread)\n * 3. Decode using VideoDecoder (main thread)\n * 4. Write RAW VideoFrames (YUV) to L1 cache\n * 5. Dispose immediately after window completes\n *\n * Why main thread?\n * - Window is small (±2s, ~60-120 frames)\n * - Worker overhead (10-50ms) is significant for small workloads\n * - Decode is fast enough\n */\nexport class VideoWindowDecodeSession {\n /**\n * Static method to decode and cache first frame from extracted GOP chunks\n * Used by ResourceLoader during streaming download for fast cover rendering\n */\n static async decodeAndCacheFirstFrame(\n _resourceId: string,\n chunks: EncodedVideoChunk[],\n index: MP4Index,\n clip: Clip,\n cacheManager: CacheManager,\n fps: number\n ): Promise<void> {\n if (chunks.length === 0) return;\n\n const videoTrack = index.tracks.video;\n if (!videoTrack) return;\n\n // Verify first chunk is keyframe\n const firstChunk = chunks[0];\n if (!firstChunk || firstChunk.type !== 'key') {\n return;\n }\n\n try {\n const result = await decodeChunksWithoutFlush(chunks, {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n });\n\n // Cache all decoded frames\n const frameDuration = Math.round(1_000_000 / fps);\n for (const frame of result.frames) {\n const frameGlobalTime = clip.startUs + frame.timestamp;\n cacheManager.addFrame(\n frame,\n clip.id,\n frameDuration,\n clip.trackId ?? 'main',\n frameGlobalTime\n );\n }\n } catch {\n // Don't throw - this is a best-effort optimization\n }\n }\n private readonly clipId: string;\n private readonly resourceId: string;\n private readonly mp4IndexCache: MP4IndexCache;\n private readonly cacheManager: CacheManager;\n private readonly compositionModel: CompositionModel;\n private readonly resourceLoader: ResourceLoader;\n private readonly fps: number;\n private readonly globalTimeUs: TimeUs;\n private readonly targetTimeUs: TimeUs;\n\n isDisposed = false;\n private aborted = false;\n private decodedFrames: VideoFrame[] = [];\n\n private constructor(config: VideoWindowDecodeSessionConfig) {\n this.clipId = config.clipId;\n this.resourceId = config.resourceId;\n this.mp4IndexCache = config.mp4IndexCache;\n this.cacheManager = config.cacheManager;\n this.resourceLoader = config.resourceLoader;\n this.compositionModel = config.compositionModel;\n this.fps = config.fps;\n this.globalTimeUs = config.globalTimeUs;\n this.targetTimeUs = config.targetTimeUs;\n }\n\n static async create(config: VideoWindowDecodeSessionConfig): Promise<VideoWindowDecodeSession> {\n const session = new VideoWindowDecodeSession(config);\n await session.init();\n return session;\n }\n\n private async init(): Promise<void> {\n // No special initialization needed for now\n }\n\n /**\n * Decode a window range, write raw frames to L1 cache\n */\n async decodeWindow(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n if (this.isDisposed) {\n throw new Error('Session already disposed');\n }\n\n const resource = this.compositionModel.getResource(this.resourceId);\n if (!resource) {\n console.warn('[VideoWindowDecodeSession] Resource not found in composition model:', {\n resourceId: this.resourceId,\n clipId: this.clipId,\n startUs,\n endUs,\n model: {\n fps: this.compositionModel.fps,\n durationUs: this.compositionModel.durationUs,\n mainTrackId: this.compositionModel.mainTrackId,\n trackCount: this.compositionModel.tracks.length,\n resourcesSize: this.compositionModel.resources.size,\n },\n });\n throw new Error(`Resource not found: ${this.resourceId}`);\n }\n\n if (resource.type === 'image') {\n await this.handleImageResource(resource, startUs, endUs);\n return;\n }\n\n await this.handleVideoResource(startUs, endUs);\n }\n\n /**\n * Scrub decode (timeline dragging):\n * Decode only the minimum frames needed to present a true frame near targetTimeUs.\n * This avoids the heavy 3s window decode used for playback preheating.\n */\n async decodeScrub(targetTimeUs: TimeUs): Promise<void> {\n if (this.isDisposed) {\n throw new Error('Session already disposed');\n }\n\n const resource = this.compositionModel.getResource(this.resourceId);\n if (!resource) {\n throw new Error(`Resource not found: ${this.resourceId}`);\n }\n\n if (resource.type === 'image') {\n const frameDuration = Math.max(1, Math.round(1_000_000 / this.fps));\n await this.handleImageResource(resource, targetTimeUs, targetTimeUs + frameDuration);\n return;\n }\n\n await this.handleVideoResourceScrub(targetTimeUs);\n }\n\n /**\n * Handle image resource by creating a VideoFrame from ImageBitmap\n */\n private async handleImageResource(\n resource: Resource,\n startUs: TimeUs,\n endUs: TimeUs\n ): Promise<void> {\n const image = await this.resourceLoader.loadImage(resource);\n if (!image) return;\n\n const frame = new VideoFrame(image, {\n timestamp: startUs,\n duration: endUs - startUs,\n });\n\n this.cacheManager.addFrame(\n frame,\n this.clipId,\n endUs - startUs,\n this.compositionModel.mainTrackId,\n this.globalTimeUs\n );\n }\n\n /**\n * Handle video resource by decoding from OPFS\n */\n private async handleVideoResource(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index) {\n throw new Error(`No index found for resource ${this.resourceId}`);\n }\n\n // Calculate GOP ranges needed\n const gopWindow = this.calculateGOPRangesForWindow(index, startUs, endUs);\n if (gopWindow.gops.length === 0) {\n console.warn('[VideoWindowDecodeSession] no GOPs found for window');\n return;\n }\n\n // Read GOP data from OPFS\n const gopData = await this.readResourceRangeWithRecovery(\n gopWindow.byteStart,\n gopWindow.byteEnd\n );\n\n if (this.aborted) {\n console.warn('[VideoWindowDecodeSession] aborted during readResourceRangeWithRecovery');\n return;\n }\n\n // Extract chunks from GOP data\n const chunks = await this.demuxGOPData(gopData, index, gopWindow);\n if (this.aborted) {\n console.warn('[VideoWindowDecodeSession] aborted during demuxGOPData');\n return;\n }\n\n // Decode chunks to frames\n await this.decodeChunks(chunks, index);\n\n // Check abort and cleanup if needed\n if (this.aborted) {\n console.warn('[VideoWindowDecodeSession] aborted during decodeWindow');\n this.releaseDecodedFrames();\n return;\n }\n\n // Write frames to L1 cache\n await this.cacheDecodedFrames(startUs, endUs);\n }\n\n private async handleVideoResourceScrub(targetTimeUs: TimeUs): Promise<void> {\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index?.tracks.video) {\n throw new Error(`No video index found for resource ${this.resourceId}`);\n }\n\n const videoTrack = index.tracks.video;\n const { gopIndex, samples } = videoTrack;\n\n const gop = binarySearchRange(gopIndex, targetTimeUs, (g, idx) => {\n const next = gopIndex[idx + 1];\n return { start: g.startTimeUs, end: next ? next.startTimeUs : Infinity };\n });\n if (!gop) {\n // Fallback: use a tiny window decode.\n const frameDuration = Math.max(1, Math.round(1_000_000 / this.fps));\n await this.handleVideoResource(targetTimeUs, targetTimeUs + frameDuration);\n return;\n }\n\n const startIdx = gop.keyframeSampleIndex;\n const endIdx = Math.min(samples.length, startIdx + gop.sampleCount);\n if (startIdx >= endIdx) return;\n\n // Compute byte range for the GOP (sample offsets are not guaranteed monotonic).\n let byteStart = Infinity;\n let byteEnd = 0;\n for (let i = startIdx; i < endIdx; i++) {\n const s = samples[i];\n if (!s) continue;\n byteStart = Math.min(byteStart, s.byteOffset);\n byteEnd = Math.max(byteEnd, s.byteOffset + s.byteLength);\n }\n if (!Number.isFinite(byteStart) || byteEnd <= byteStart) return;\n\n const gopData = await this.readResourceRangeWithRecovery(byteStart, byteEnd);\n if (this.aborted) return;\n\n const chunks = this.buildEncodedChunksFromSamples(\n gopData,\n byteStart,\n samples,\n startIdx,\n endIdx\n );\n if (chunks.length === 0) return;\n\n const timeoutMs = this.cacheManager.isExporting ? 15_000 : 2_000;\n const { before, after } = await decodeChunksForScrub(\n chunks,\n {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n },\n targetTimeUs,\n {\n timeoutMs,\n maxQueueSize: 2,\n shouldAbort: () => this.aborted,\n }\n );\n\n if (this.aborted) {\n before?.close();\n after?.close();\n return;\n }\n\n // Cache only the closest frames around target.\n for (const frame of [before, after]) {\n if (!frame) continue;\n try {\n this.cacheFrame(frame);\n } catch {\n frame.close();\n }\n }\n }\n\n private buildEncodedChunksFromSamples(\n data: ArrayBuffer,\n baseByteOffset: number,\n samples: Sample[],\n startIdx: number,\n endIdx: number\n ): EncodedVideoChunk[] {\n const chunks: EncodedVideoChunk[] = [];\n const dataView = new Uint8Array(data);\n\n for (let i = startIdx; i < endIdx; i++) {\n const sample = samples[i];\n if (!sample) continue;\n\n const relativeOffset = sample.byteOffset - baseByteOffset;\n if (relativeOffset < 0 || relativeOffset + sample.byteLength > data.byteLength) {\n continue;\n }\n\n const sampleData = dataView.slice(relativeOffset, relativeOffset + sample.byteLength);\n chunks.push(\n new EncodedVideoChunk({\n type: sample.isKeyframe ? 'key' : 'delta',\n timestamp: sample.timestamp,\n duration: sample.duration,\n data: sampleData,\n })\n );\n }\n\n return chunks;\n }\n\n /**\n * Release all decoded frames without caching\n */\n private releaseDecodedFrames(): void {\n for (const frame of this.decodedFrames) {\n frame.close();\n }\n this.decodedFrames = [];\n }\n\n private calculateGOPRangesForWindow(\n index: MP4Index,\n startUs: TimeUs,\n endUs: TimeUs\n ): GOPWindowResult {\n if (!index.tracks.video) {\n return { gops: [], byteStart: 0, byteEnd: 0 };\n }\n\n const { gopIndex, samples } = index.tracks.video;\n\n // Find GOP containing startUs (or the nearest keyframe before it)\n const nearestKeyframe = this.mp4IndexCache.getNearestKeyframe(this.resourceId, startUs);\n const decodeStartUs = nearestKeyframe?.timestamp ?? startUs;\n\n // Use binary search to find all overlapping GOPs\n const overlappingGOPs = binarySearchOverlapping(gopIndex, decodeStartUs, endUs, (gop, idx) => {\n const nextGOP = gopIndex[idx + 1];\n return {\n start: gop.startTimeUs,\n end: nextGOP ? nextGOP.startTimeUs : Infinity,\n };\n });\n\n if (overlappingGOPs.length === 0) {\n return { gops: [], byteStart: 0, byteEnd: 0 };\n }\n\n // Calculate merged byte range for OPFS read\n let byteStart = Infinity;\n let byteEnd = 0;\n\n for (const gop of overlappingGOPs) {\n const startSample = samples[gop.keyframeSampleIndex];\n const endSampleIndex = gop.keyframeSampleIndex + gop.sampleCount - 1;\n const endSample = samples[endSampleIndex];\n\n if (startSample && endSample) {\n byteStart = Math.min(byteStart, startSample.byteOffset);\n byteEnd = Math.max(byteEnd, endSample.byteOffset + endSample.byteLength);\n }\n }\n\n return { gops: overlappingGOPs, byteStart, byteEnd };\n }\n\n /**\n * Extract video chunks from GOP data\n *\n * Directly use GOP sample indices to slice the samples array.\n * This is O(k) where k is the number of samples in the window,\n * vs O(n×m) for the old approach (n=all samples, m=GOP count).\n */\n private async demuxGOPData(\n data: ArrayBuffer,\n index: MP4Index,\n gopWindow: GOPWindowResult\n ): Promise<EncodedVideoChunk[]> {\n const videoTrack = index.tracks.video;\n if (!videoTrack) {\n throw new Error('No video track in index');\n }\n\n const { samples } = videoTrack;\n const chunks: EncodedVideoChunk[] = [];\n const dataView = new Uint8Array(data);\n const baseByteOffset = gopWindow.byteStart;\n\n // Direct sample index slicing - iterate only samples in the window\n for (const gop of gopWindow.gops) {\n const startIdx = gop.keyframeSampleIndex;\n const endIdx = startIdx + gop.sampleCount;\n\n // Extract samples for this GOP (direct array slice)\n for (let i = startIdx; i < endIdx; i++) {\n const sample = samples[i];\n if (!sample) continue;\n\n // Calculate relative offset within the data buffer\n const relativeOffset = sample.byteOffset - baseByteOffset;\n\n // Validate offset is within buffer\n if (relativeOffset < 0 || relativeOffset + sample.byteLength > data.byteLength) {\n console.warn('[VideoWindowDecodeSession] Sample outside buffer:', {\n sampleOffset: sample.byteOffset,\n sampleLength: sample.byteLength,\n baseOffset: baseByteOffset,\n relativeOffset,\n bufferLength: data.byteLength,\n });\n continue;\n }\n\n // Extract sample data\n const sampleData = dataView.slice(relativeOffset, relativeOffset + sample.byteLength);\n\n // Create EncodedVideoChunk\n const chunk = new EncodedVideoChunk({\n type: sample.isKeyframe ? 'key' : 'delta',\n timestamp: sample.timestamp,\n duration: sample.duration,\n data: sampleData,\n });\n\n chunks.push(chunk);\n }\n }\n\n return chunks;\n }\n\n private async decodeChunks(chunks: EncodedVideoChunk[], index: MP4Index): Promise<void> {\n const videoTrack = index.tracks.video;\n if (!videoTrack) {\n throw new Error('No video track in index');\n }\n\n const timeoutMs = this.cacheManager.isExporting ? 15_000 : undefined;\n const result = await decodeChunksWithoutFlush(\n chunks,\n {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n },\n timeoutMs ? { timeoutMs } : undefined\n );\n\n // Store frames for caching\n this.decodedFrames = result.frames;\n }\n\n /**\n * Cache a single frame to L1 with proper timestamp calculations\n */\n private cacheFrame(frame: VideoFrame, globalTimeOffset: TimeUs = 0): void {\n const frameDuration = frame.duration ?? Math.round(1_000_000 / this.fps);\n const frameGlobalTime =\n this.globalTimeUs + (frame.timestamp - this.targetTimeUs) + globalTimeOffset;\n\n this.cacheManager.addFrame(\n frame,\n this.clipId,\n frameDuration,\n this.compositionModel.mainTrackId,\n frameGlobalTime\n );\n }\n\n private async cacheDecodedFrames(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const framesToCache: VideoFrame[] = [];\n const framesToDiscard: VideoFrame[] = [];\n\n // Partition frames into cacheable and discardable\n for (const frame of this.decodedFrames) {\n if (frame.timestamp >= startUs && frame.timestamp < endUs) {\n framesToCache.push(frame);\n } else {\n framesToDiscard.push(frame);\n }\n }\n\n // Cache frames within window\n for (const frame of framesToCache) {\n try {\n this.cacheFrame(frame);\n } catch {\n frame.close();\n }\n }\n\n // Release frames outside window\n for (const frame of framesToDiscard) {\n frame.close();\n }\n\n this.decodedFrames = [];\n }\n\n /**\n * Decode entire time range to VideoFrame stream (for export)\n * Does NOT cache to L1 - outputs frames directly for Worker pipeline\n */\n async decodeRangeToStream(startUs: TimeUs, endUs: TimeUs): Promise<ReadableStream<VideoFrame>> {\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index?.tracks.video) {\n throw new Error(`[VideoWindowDecodeSession] No video track index for ${this.resourceId}`);\n }\n\n const videoTrack = index.tracks.video;\n const gopWindow = this.calculateGOPRangesForWindow(index, startUs, endUs);\n\n return new ReadableStream<VideoFrame>({\n start: async (controller) => {\n try {\n if (gopWindow.gops.length === 0) {\n console.warn('[VideoWindowDecodeSession] No GOPs found for range');\n controller.close();\n return;\n }\n\n // Process GOPs in batches (10 GOPs at a time for memory control)\n const batchSize = 10;\n for (let i = 0; i < gopWindow.gops.length; i += batchSize) {\n if (this.aborted) {\n controller.close();\n return;\n }\n\n const batchGOPs = gopWindow.gops.slice(\n i,\n Math.min(i + batchSize, gopWindow.gops.length)\n );\n\n // Calculate byte range for this batch\n let batchByteStart = Infinity;\n let batchByteEnd = 0;\n for (const gop of batchGOPs) {\n const startSample = videoTrack.samples[gop.keyframeSampleIndex];\n const endSampleIdx = gop.keyframeSampleIndex + gop.sampleCount - 1;\n const endSample = videoTrack.samples[endSampleIdx];\n if (startSample && endSample) {\n batchByteStart = Math.min(batchByteStart, startSample.byteOffset);\n batchByteEnd = Math.max(batchByteEnd, endSample.byteOffset + endSample.byteLength);\n }\n }\n\n // Read GOP batch data from OPFS\n const gopData = await this.readResourceRangeWithRecovery(batchByteStart, batchByteEnd);\n\n if (this.aborted) {\n controller.close();\n return;\n }\n\n // Extract chunks from GOP batch\n const batchChunks = await this.demuxGOPData(gopData, index, {\n gops: batchGOPs,\n byteStart: batchByteStart,\n byteEnd: batchByteEnd,\n });\n\n // Decode chunks to frames\n await this.decodeChunks(batchChunks, index);\n\n if (this.aborted) {\n this.releaseDecodedFrames();\n controller.close();\n return;\n }\n\n // Enqueue only frames within the requested range.\n // Leading GOP frames before startUs are reference-only; discard them\n // to prevent duplicate frames across sequential windows.\n for (const frame of this.decodedFrames) {\n if (frame.timestamp >= startUs && frame.timestamp < endUs) {\n controller.enqueue(frame);\n } else {\n frame.close();\n }\n }\n this.decodedFrames = [];\n }\n\n controller.close();\n } catch (error) {\n console.error('[VideoWindowDecodeSession] decodeRangeToStream error:', error);\n this.releaseDecodedFrames();\n controller.error(error);\n }\n },\n cancel: () => {\n this.aborted = true;\n this.releaseDecodedFrames();\n },\n });\n }\n\n private async readResourceRangeWithRecovery(start: number, end: number): Promise<ArrayBuffer> {\n try {\n return await this.cacheManager.readResourceRange(this.resourceId, start, end);\n } catch (error) {\n if (!(error instanceof ResourceCorruptedError)) throw error;\n\n // Unify recovery behavior:\n // 1) Invalidate OPFS + index (ensure cache consistency)\n // 2) Reload to OPFS + rebuild index\n // 3) Retry the read once\n await this.cacheManager.resourceCache.deleteResource(this.resourceId);\n this.cacheManager.mp4IndexCache.delete(this.resourceId);\n\n await this.resourceLoader.load(this.resourceId, { isPreload: false, clipId: this.clipId });\n return await this.cacheManager.readResourceRange(this.resourceId, start, end);\n }\n }\n\n async dispose(): Promise<void> {\n if (this.isDisposed) return;\n\n this.aborted = true;\n\n // Release all decoded frames\n this.releaseDecodedFrames();\n\n this.isDisposed = true;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VideoWindowDecodeSession.js","sources":["../../src/orchestrator/VideoWindowDecodeSession.ts"],"sourcesContent":["import type { CacheManager } from '../cache/CacheManager';\nimport type { MP4IndexCache } from '../cache/resource/MP4IndexCache';\nimport type { MP4Index, GOP, Sample } from '../stages/demux/types';\nimport { type TimeUs, type Resource, isVideoClip, videoClipPlaybackRate } from '../model/types';\nimport type { CompositionModel, Clip } from '../model';\nimport { binarySearchOverlapping, binarySearchRange } from '../utils/binary-search';\nimport type { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { decodeChunksForScrub, decodeChunksWithoutFlush } from '../stages/decode/video-decoder';\nimport { ResourceCorruptedError } from '../utils/errors';\n\ninterface GOPWindowResult {\n gops: GOP[];\n byteStart: number;\n byteEnd: number;\n}\n\ninterface VideoWindowDecodeSessionConfig {\n clipId: string;\n resourceId: string;\n resourceUri: string;\n targetTimeUs: TimeUs;\n globalTimeUs: TimeUs;\n mp4IndexCache: MP4IndexCache;\n cacheManager: CacheManager;\n compositionModel: CompositionModel;\n resourceLoader: ResourceLoader;\n fps: number;\n}\n\n/**\n * Strategy:\n * 1. Read GOP range from OPFS\n * 2. Demux using MP4Box (main thread)\n * 3. Decode using VideoDecoder (main thread)\n * 4. Write RAW VideoFrames (YUV) to L1 cache\n * 5. Dispose immediately after window completes\n *\n * Why main thread?\n * - Window is small (±2s, ~60-120 frames)\n * - Worker overhead (10-50ms) is significant for small workloads\n * - Decode is fast enough\n */\nexport class VideoWindowDecodeSession {\n /**\n * Static method to decode and cache first frame from extracted GOP chunks\n * Used by ResourceLoader during streaming download for fast cover rendering\n */\n static async decodeAndCacheFirstFrame(\n _resourceId: string,\n chunks: EncodedVideoChunk[],\n index: MP4Index,\n clip: Clip,\n cacheManager: CacheManager,\n fps: number\n ): Promise<void> {\n if (chunks.length === 0) return;\n\n const videoTrack = index.tracks.video;\n if (!videoTrack) return;\n\n // Verify first chunk is keyframe\n const firstChunk = chunks[0];\n if (!firstChunk || firstChunk.type !== 'key') {\n return;\n }\n\n try {\n const result = await decodeChunksWithoutFlush(chunks, {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n });\n\n // Cache all decoded frames\n const frameDuration = Math.round(1_000_000 / fps);\n for (const frame of result.frames) {\n const frameGlobalTime = clip.startUs + frame.timestamp;\n cacheManager.addFrame(\n frame,\n clip.id,\n frameDuration,\n clip.trackId ?? 'main',\n frameGlobalTime\n );\n }\n } catch {\n // Don't throw - this is a best-effort optimization\n }\n }\n private readonly clipId: string;\n private readonly resourceId: string;\n private readonly resourceUri: string;\n private readonly mp4IndexCache: MP4IndexCache;\n private readonly cacheManager: CacheManager;\n private readonly compositionModel: CompositionModel;\n private readonly resourceLoader: ResourceLoader;\n private readonly fps: number;\n private readonly globalTimeUs: TimeUs;\n private readonly targetTimeUs: TimeUs;\n\n isDisposed = false;\n private aborted = false;\n private decodedFrames: VideoFrame[] = [];\n private loggedDecodedStreamDimensions = false;\n\n private constructor(config: VideoWindowDecodeSessionConfig) {\n this.clipId = config.clipId;\n this.resourceId = config.resourceId;\n this.resourceUri = config.resourceUri;\n this.mp4IndexCache = config.mp4IndexCache;\n this.cacheManager = config.cacheManager;\n this.resourceLoader = config.resourceLoader;\n this.compositionModel = config.compositionModel;\n this.fps = config.fps;\n this.globalTimeUs = config.globalTimeUs;\n this.targetTimeUs = config.targetTimeUs;\n }\n\n static async create(config: VideoWindowDecodeSessionConfig): Promise<VideoWindowDecodeSession> {\n const session = new VideoWindowDecodeSession(config);\n await session.init();\n return session;\n }\n\n private async init(): Promise<void> {\n // No special initialization needed for now\n }\n\n /**\n * Decode a window range, write raw frames to L1 cache\n */\n async decodeWindow(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n if (this.isDisposed) {\n throw new Error('Session already disposed');\n }\n\n const resource = this.compositionModel.getResource(this.resourceId);\n if (!resource) {\n console.warn('[VideoWindowDecodeSession] Resource not found in composition model:', {\n resourceId: this.resourceId,\n clipId: this.clipId,\n startUs,\n endUs,\n model: {\n fps: this.compositionModel.fps,\n durationUs: this.compositionModel.durationUs,\n mainTrackId: this.compositionModel.mainTrackId,\n trackCount: this.compositionModel.tracks.length,\n resourcesSize: this.compositionModel.resources.size,\n },\n });\n throw new Error(`Resource not found: ${this.resourceId}`);\n }\n\n if (resource.type === 'image') {\n await this.handleImageResource(resource, startUs, endUs);\n return;\n }\n\n await this.handleVideoResource(startUs, endUs);\n }\n\n /**\n * Scrub decode (timeline dragging):\n * Decode only the minimum frames needed to present a true frame near targetTimeUs.\n * This avoids the heavy 3s window decode used for playback preheating.\n */\n async decodeScrub(targetTimeUs: TimeUs): Promise<void> {\n if (this.isDisposed) {\n throw new Error('Session already disposed');\n }\n\n const resource = this.compositionModel.getResource(this.resourceId);\n if (!resource) {\n throw new Error(`Resource not found: ${this.resourceId}`);\n }\n\n if (resource.type === 'image') {\n const frameDuration = Math.max(1, Math.round(1_000_000 / this.fps));\n await this.handleImageResource(resource, targetTimeUs, targetTimeUs + frameDuration);\n return;\n }\n\n await this.handleVideoResourceScrub(targetTimeUs);\n }\n\n /**\n * Handle image resource by creating a VideoFrame from ImageBitmap\n */\n private async handleImageResource(\n resource: Resource,\n startUs: TimeUs,\n endUs: TimeUs\n ): Promise<void> {\n const image = await this.resourceLoader.loadImage(resource);\n if (!image) return;\n\n const frame = new VideoFrame(image, {\n timestamp: startUs,\n duration: endUs - startUs,\n });\n\n this.cacheManager.addFrame(\n frame,\n this.clipId,\n endUs - startUs,\n this.compositionModel.mainTrackId,\n this.globalTimeUs\n );\n }\n\n /**\n * Handle video resource by decoding from OPFS\n */\n private async handleVideoResource(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index) {\n throw new Error(`No index found for resource ${this.resourceId}`);\n }\n\n // Calculate GOP ranges needed\n const gopWindow = this.calculateGOPRangesForWindow(index, startUs, endUs);\n if (gopWindow.gops.length === 0) {\n console.warn('[VideoWindowDecodeSession] no GOPs found for window');\n return;\n }\n\n // Read GOP data from OPFS\n const gopData = await this.readResourceRangeWithRecovery(\n gopWindow.byteStart,\n gopWindow.byteEnd\n );\n\n if (this.aborted) {\n console.warn('[VideoWindowDecodeSession] aborted during readResourceRangeWithRecovery');\n return;\n }\n\n // Extract chunks from GOP data\n const chunks = await this.demuxGOPData(gopData, index, gopWindow);\n if (this.aborted) {\n console.warn('[VideoWindowDecodeSession] aborted during demuxGOPData');\n return;\n }\n\n // Decode chunks to frames\n await this.decodeChunks(chunks, index);\n\n // Check abort and cleanup if needed\n if (this.aborted) {\n console.warn('[VideoWindowDecodeSession] aborted during decodeWindow');\n this.releaseDecodedFrames();\n return;\n }\n\n // Write frames to L1 cache\n await this.cacheDecodedFrames(startUs, endUs);\n }\n\n private async handleVideoResourceScrub(targetTimeUs: TimeUs): Promise<void> {\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index?.tracks.video) {\n throw new Error(`No video index found for resource ${this.resourceId}`);\n }\n\n const videoTrack = index.tracks.video;\n const { gopIndex, samples } = videoTrack;\n\n const gop = binarySearchRange(gopIndex, targetTimeUs, (g, idx) => {\n const next = gopIndex[idx + 1];\n return { start: g.startTimeUs, end: next ? next.startTimeUs : Infinity };\n });\n if (!gop) {\n // Fallback: use a tiny window decode.\n const frameDuration = Math.max(1, Math.round(1_000_000 / this.fps));\n await this.handleVideoResource(targetTimeUs, targetTimeUs + frameDuration);\n return;\n }\n\n const startIdx = gop.keyframeSampleIndex;\n const endIdx = Math.min(samples.length, startIdx + gop.sampleCount);\n if (startIdx >= endIdx) return;\n\n // Compute byte range for the GOP (sample offsets are not guaranteed monotonic).\n let byteStart = Infinity;\n let byteEnd = 0;\n for (let i = startIdx; i < endIdx; i++) {\n const s = samples[i];\n if (!s) continue;\n byteStart = Math.min(byteStart, s.byteOffset);\n byteEnd = Math.max(byteEnd, s.byteOffset + s.byteLength);\n }\n if (!Number.isFinite(byteStart) || byteEnd <= byteStart) return;\n\n const gopData = await this.readResourceRangeWithRecovery(byteStart, byteEnd);\n if (this.aborted) return;\n\n const chunks = this.buildEncodedChunksFromSamples(\n gopData,\n byteStart,\n samples,\n startIdx,\n endIdx\n );\n if (chunks.length === 0) return;\n\n const timeoutMs = this.cacheManager.isExporting ? 15_000 : 2_000;\n const { before, after } = await decodeChunksForScrub(\n chunks,\n {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n },\n targetTimeUs,\n {\n timeoutMs,\n maxQueueSize: 2,\n shouldAbort: () => this.aborted,\n }\n );\n\n if (this.aborted) {\n before?.close();\n after?.close();\n return;\n }\n\n // Cache only the closest frames around target.\n for (const frame of [before, after]) {\n if (!frame) continue;\n try {\n this.cacheFrame(frame);\n } catch {\n frame.close();\n }\n }\n }\n\n private buildEncodedChunksFromSamples(\n data: ArrayBuffer,\n baseByteOffset: number,\n samples: Sample[],\n startIdx: number,\n endIdx: number\n ): EncodedVideoChunk[] {\n const chunks: EncodedVideoChunk[] = [];\n const dataView = new Uint8Array(data);\n\n for (let i = startIdx; i < endIdx; i++) {\n const sample = samples[i];\n if (!sample) continue;\n\n const relativeOffset = sample.byteOffset - baseByteOffset;\n if (relativeOffset < 0 || relativeOffset + sample.byteLength > data.byteLength) {\n continue;\n }\n\n const sampleData = dataView.slice(relativeOffset, relativeOffset + sample.byteLength);\n chunks.push(\n new EncodedVideoChunk({\n type: sample.isKeyframe ? 'key' : 'delta',\n timestamp: sample.timestamp,\n duration: sample.duration,\n data: sampleData,\n })\n );\n }\n\n return chunks;\n }\n\n /**\n * Release all decoded frames without caching\n */\n private releaseDecodedFrames(): void {\n for (const frame of this.decodedFrames) {\n frame.close();\n }\n this.decodedFrames = [];\n }\n\n private calculateGOPRangesForWindow(\n index: MP4Index,\n startUs: TimeUs,\n endUs: TimeUs\n ): GOPWindowResult {\n if (!index.tracks.video) {\n return { gops: [], byteStart: 0, byteEnd: 0 };\n }\n\n const { gopIndex, samples } = index.tracks.video;\n\n // Find GOP containing startUs (or the nearest keyframe before it)\n const nearestKeyframe = this.mp4IndexCache.getNearestKeyframe(this.resourceId, startUs);\n const decodeStartUs = nearestKeyframe?.timestamp ?? startUs;\n\n // Use binary search to find all overlapping GOPs\n const overlappingGOPs = binarySearchOverlapping(gopIndex, decodeStartUs, endUs, (gop, idx) => {\n const nextGOP = gopIndex[idx + 1];\n return {\n start: gop.startTimeUs,\n end: nextGOP ? nextGOP.startTimeUs : Infinity,\n };\n });\n\n if (overlappingGOPs.length === 0) {\n return { gops: [], byteStart: 0, byteEnd: 0 };\n }\n\n // Calculate merged byte range for OPFS read\n let byteStart = Infinity;\n let byteEnd = 0;\n\n for (const gop of overlappingGOPs) {\n const startSample = samples[gop.keyframeSampleIndex];\n const endSampleIndex = gop.keyframeSampleIndex + gop.sampleCount - 1;\n const endSample = samples[endSampleIndex];\n\n if (startSample && endSample) {\n byteStart = Math.min(byteStart, startSample.byteOffset);\n byteEnd = Math.max(byteEnd, endSample.byteOffset + endSample.byteLength);\n }\n }\n\n return { gops: overlappingGOPs, byteStart, byteEnd };\n }\n\n /**\n * Extract video chunks from GOP data\n *\n * Directly use GOP sample indices to slice the samples array.\n * This is O(k) where k is the number of samples in the window,\n * vs O(n×m) for the old approach (n=all samples, m=GOP count).\n */\n private async demuxGOPData(\n data: ArrayBuffer,\n index: MP4Index,\n gopWindow: GOPWindowResult\n ): Promise<EncodedVideoChunk[]> {\n const videoTrack = index.tracks.video;\n if (!videoTrack) {\n throw new Error('No video track in index');\n }\n\n const { samples } = videoTrack;\n const chunks: EncodedVideoChunk[] = [];\n const dataView = new Uint8Array(data);\n const baseByteOffset = gopWindow.byteStart;\n\n // Direct sample index slicing - iterate only samples in the window\n for (const gop of gopWindow.gops) {\n const startIdx = gop.keyframeSampleIndex;\n const endIdx = startIdx + gop.sampleCount;\n\n // Extract samples for this GOP (direct array slice)\n for (let i = startIdx; i < endIdx; i++) {\n const sample = samples[i];\n if (!sample) continue;\n\n // Calculate relative offset within the data buffer\n const relativeOffset = sample.byteOffset - baseByteOffset;\n\n // Validate offset is within buffer\n if (relativeOffset < 0 || relativeOffset + sample.byteLength > data.byteLength) {\n console.warn('[VideoWindowDecodeSession] Sample outside buffer:', {\n sampleOffset: sample.byteOffset,\n sampleLength: sample.byteLength,\n baseOffset: baseByteOffset,\n relativeOffset,\n bufferLength: data.byteLength,\n });\n continue;\n }\n\n // Extract sample data\n const sampleData = dataView.slice(relativeOffset, relativeOffset + sample.byteLength);\n\n // Create EncodedVideoChunk\n const chunk = new EncodedVideoChunk({\n type: sample.isKeyframe ? 'key' : 'delta',\n timestamp: sample.timestamp,\n duration: sample.duration,\n data: sampleData,\n });\n\n chunks.push(chunk);\n }\n }\n\n return chunks;\n }\n\n private async decodeChunks(chunks: EncodedVideoChunk[], index: MP4Index): Promise<void> {\n const videoTrack = index.tracks.video;\n if (!videoTrack) {\n throw new Error('No video track in index');\n }\n\n const timeoutMs = this.cacheManager.isExporting ? 15_000 : undefined;\n const result = await decodeChunksWithoutFlush(\n chunks,\n {\n codec: videoTrack.codec,\n width: videoTrack.width,\n height: videoTrack.height,\n description: videoTrack.description,\n },\n timeoutMs ? { timeoutMs } : undefined\n );\n\n // Store frames for caching\n this.decodedFrames = result.frames;\n }\n\n /**\n * Cache a single frame to L1 with proper timestamp calculations\n */\n private cacheFrame(frame: VideoFrame, globalTimeOffset: TimeUs = 0): void {\n const frameDuration = frame.duration ?? Math.round(1_000_000 / this.fps);\n const clip = this.compositionModel.findClip(this.clipId);\n let frameGlobalTime: TimeUs;\n if (clip && isVideoClip(clip)) {\n const trimStartUs = clip.trimStartUs ?? 0;\n const rate = videoClipPlaybackRate(clip);\n frameGlobalTime = clip.startUs + (frame.timestamp - trimStartUs) / rate + globalTimeOffset;\n } else {\n frameGlobalTime =\n this.globalTimeUs + (frame.timestamp - this.targetTimeUs) + globalTimeOffset;\n }\n\n this.cacheManager.addFrame(\n frame,\n this.clipId,\n frameDuration,\n this.compositionModel.mainTrackId,\n frameGlobalTime\n );\n }\n\n private async cacheDecodedFrames(startUs: TimeUs, endUs: TimeUs): Promise<void> {\n const framesToCache: VideoFrame[] = [];\n const framesToDiscard: VideoFrame[] = [];\n\n // Partition frames into cacheable and discardable\n for (const frame of this.decodedFrames) {\n if (frame.timestamp >= startUs && frame.timestamp < endUs) {\n framesToCache.push(frame);\n } else {\n framesToDiscard.push(frame);\n }\n }\n\n // Cache frames within window\n for (const frame of framesToCache) {\n try {\n this.cacheFrame(frame);\n } catch {\n frame.close();\n }\n }\n\n // Release frames outside window\n for (const frame of framesToDiscard) {\n frame.close();\n }\n\n this.decodedFrames = [];\n }\n\n /**\n * Decode entire time range to VideoFrame stream (for export)\n * Does NOT cache to L1 - outputs frames directly for Worker pipeline\n */\n async decodeRangeToStream(startUs: TimeUs, endUs: TimeUs): Promise<ReadableStream<VideoFrame>> {\n const index = this.mp4IndexCache.get(this.resourceId);\n if (!index?.tracks.video) {\n throw new Error(`[VideoWindowDecodeSession] No video track index for ${this.resourceId}`);\n }\n\n const videoTrack = index.tracks.video;\n const gopWindow = this.calculateGOPRangesForWindow(index, startUs, endUs);\n\n return new ReadableStream<VideoFrame>({\n start: async (controller) => {\n try {\n if (gopWindow.gops.length === 0) {\n console.warn('[VideoWindowDecodeSession] No GOPs found for range');\n controller.close();\n return;\n }\n\n // Process GOPs in batches (10 GOPs at a time for memory control)\n const batchSize = 10;\n for (let i = 0; i < gopWindow.gops.length; i += batchSize) {\n if (this.aborted) {\n controller.close();\n return;\n }\n\n const batchGOPs = gopWindow.gops.slice(\n i,\n Math.min(i + batchSize, gopWindow.gops.length)\n );\n\n // Calculate byte range for this batch\n let batchByteStart = Infinity;\n let batchByteEnd = 0;\n for (const gop of batchGOPs) {\n const startSample = videoTrack.samples[gop.keyframeSampleIndex];\n const endSampleIdx = gop.keyframeSampleIndex + gop.sampleCount - 1;\n const endSample = videoTrack.samples[endSampleIdx];\n if (startSample && endSample) {\n batchByteStart = Math.min(batchByteStart, startSample.byteOffset);\n batchByteEnd = Math.max(batchByteEnd, endSample.byteOffset + endSample.byteLength);\n }\n }\n\n // Read GOP batch data from OPFS\n const gopData = await this.readResourceRangeWithRecovery(batchByteStart, batchByteEnd);\n\n if (this.aborted) {\n controller.close();\n return;\n }\n\n // Extract chunks from GOP batch\n const batchChunks = await this.demuxGOPData(gopData, index, {\n gops: batchGOPs,\n byteStart: batchByteStart,\n byteEnd: batchByteEnd,\n });\n\n // Decode chunks to frames\n await this.decodeChunks(batchChunks, index);\n\n if (this.aborted) {\n this.releaseDecodedFrames();\n controller.close();\n return;\n }\n\n // Enqueue only frames within the requested range.\n // Leading GOP frames before startUs are reference-only; discard them\n // to prevent duplicate frames across sequential windows.\n for (const frame of this.decodedFrames) {\n if (frame.timestamp >= startUs && frame.timestamp < endUs) {\n if (!this.loggedDecodedStreamDimensions) {\n console.info(\n 'decodeRangeToStream:',\n 'Id:',\n this.resourceId,\n 'videoTrack:',\n videoTrack.width,\n 'x',\n videoTrack.height,\n 'displaySize:',\n frame.displayWidth,\n 'x',\n frame.displayHeight,\n 'codedSize:',\n frame.codedWidth,\n 'x',\n frame.codedHeight,\n 'startUs:',\n startUs,\n 'endUs:',\n endUs,\n 'duration:',\n frame.duration,\n 'Uri:',\n this.resourceUri\n );\n this.loggedDecodedStreamDimensions = true;\n }\n\n controller.enqueue(frame);\n } else {\n frame.close();\n }\n }\n this.decodedFrames = [];\n }\n\n controller.close();\n } catch (error) {\n console.error('[VideoWindowDecodeSession] decodeRangeToStream error:', error);\n this.releaseDecodedFrames();\n controller.error(error);\n }\n },\n cancel: () => {\n this.aborted = true;\n this.releaseDecodedFrames();\n },\n });\n }\n\n private async readResourceRangeWithRecovery(start: number, end: number): Promise<ArrayBuffer> {\n try {\n return await this.cacheManager.readResourceRange(this.resourceId, start, end);\n } catch (error) {\n if (!(error instanceof ResourceCorruptedError)) throw error;\n\n // Unify recovery behavior:\n // 1) Invalidate OPFS + index (ensure cache consistency)\n // 2) Reload to OPFS + rebuild index\n // 3) Retry the read once\n await this.cacheManager.resourceCache.deleteResource(this.resourceId);\n this.cacheManager.mp4IndexCache.delete(this.resourceId);\n\n await this.resourceLoader.load(this.resourceId, { isPreload: false, clipId: this.clipId });\n return await this.cacheManager.readResourceRange(this.resourceId, start, end);\n }\n }\n\n async dispose(): Promise<void> {\n if (this.isDisposed) return;\n\n this.aborted = true;\n\n // Release all decoded frames\n this.releaseDecodedFrames();\n\n this.isDisposed = true;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -29,12 +29,13 @@ export declare class FrameRateConverter {
|
|
|
29
29
|
private readonly clipDurationUs;
|
|
30
30
|
private readonly frameDurationUs;
|
|
31
31
|
private readonly trimStartUs;
|
|
32
|
+
private readonly playbackRate;
|
|
32
33
|
private readonly totalFrameCount;
|
|
33
34
|
private targetFrameIndex;
|
|
34
35
|
private targetFrameTimeUs;
|
|
35
36
|
private sourceFrameBuffer;
|
|
36
37
|
private maxSourceTimestampUs;
|
|
37
|
-
constructor(targetFps: number, clipDurationUs: TimeUs, trimStartUs?: TimeUs);
|
|
38
|
+
constructor(targetFps: number, clipDurationUs: TimeUs, trimStartUs?: TimeUs, playbackRate?: number);
|
|
38
39
|
/**
|
|
39
40
|
* Create a TransformStream that converts VFR frames to CFR frames
|
|
40
41
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FrameRateConverter.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/FrameRateConverter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAgB;IAGhD,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,iBAAiB,CAAa;IACtC,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,oBAAoB,CAAuB;
|
|
1
|
+
{"version":3,"file":"FrameRateConverter.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/FrameRateConverter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAgB;IAGhD,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,iBAAiB,CAAa;IACtC,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,oBAAoB,CAAuB;gBAGjD,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,WAAW,GAAE,MAAU,EACvB,YAAY,GAAE,MAAU;IAqB1B;;OAEG;IACH,YAAY,IAAI,eAAe,CAAC,UAAU,EAAE,UAAU,CAAC;IAsBvD,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,gBAAgB,CAAK;IAE7B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA2E1B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAuC5B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAoBzB,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,oBAAoB;IAO5B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAaxB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAmBxB;;;;;;OAMG;IACH,OAAO,CAAC,sBAAsB;IAY9B;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAoCxB;;OAEG;IACH,QAAQ,IAAI;QACV,gBAAgB,EAAE,MAAM,CAAC;QACzB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,UAAU,EAAE,MAAM,CAAC;QACnB,eAAe,EAAE,MAAM,CAAC;KACzB;CAQF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OfflineAudioMixer.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/OfflineAudioMixer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"OfflineAudioMixer.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/OfflineAudioMixer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAOhD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAU7D,qBAAa,iBAAiB;IAK1B,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,QAAQ;IALlB,OAAO,CAAC,UAAU,CAAU;IAC5B,OAAO,CAAC,gBAAgB,CAAK;gBAGnB,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,MAAM,gBAAgB,GAAG,IAAI;IAG3C,GAAG,CAAC,aAAa,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAgH3E,OAAO,CAAC,gBAAgB;CA2CzB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasResourceId, hasAudioConfig } from "../../model/types.js";
|
|
1
|
+
import { isVideoClip, videoClipPlaybackRate, hasResourceId, hasAudioConfig } from "../../model/types.js";
|
|
2
2
|
import { buildLoopedResourceSegments } from "../../utils/loop-utils.js";
|
|
3
3
|
class OfflineAudioMixer {
|
|
4
4
|
constructor(cacheManager, getModel) {
|
|
@@ -31,13 +31,15 @@ class OfflineAudioMixer {
|
|
|
31
31
|
const clipModel = this.getModel()?.findClip(clip.clipId);
|
|
32
32
|
const trimStartUs = clipModel?.trimStartUs ?? 0;
|
|
33
33
|
const loop = clipModel?.trackKind === "audio" && clipModel.loop === true;
|
|
34
|
+
const playbackRate = clipModel && isVideoClip(clipModel) ? videoClipPlaybackRate(clipModel) : 1;
|
|
34
35
|
const resourceDurationUs = clipModel && hasResourceId(clipModel) ? this.cacheManager.audioSampleCache.get(clipModel.resourceId)?.durationUs ?? 0 : 0;
|
|
35
36
|
let segments = buildLoopedResourceSegments({
|
|
36
37
|
clipRelativeStartUs,
|
|
37
38
|
clipRelativeEndUs,
|
|
38
39
|
trimStartUs,
|
|
39
40
|
resourceDurationUs,
|
|
40
|
-
loop
|
|
41
|
+
loop,
|
|
42
|
+
playbackRate: loop ? 1 : playbackRate
|
|
41
43
|
});
|
|
42
44
|
if (segments.length === 0 && clipRelativeEndUs > clipRelativeStartUs) {
|
|
43
45
|
segments = buildLoopedResourceSegments({
|
|
@@ -45,7 +47,8 @@ class OfflineAudioMixer {
|
|
|
45
47
|
clipRelativeEndUs,
|
|
46
48
|
trimStartUs,
|
|
47
49
|
resourceDurationUs,
|
|
48
|
-
loop: false
|
|
50
|
+
loop: false,
|
|
51
|
+
playbackRate
|
|
49
52
|
});
|
|
50
53
|
}
|
|
51
54
|
for (const seg of segments) {
|
|
@@ -70,6 +73,9 @@ class OfflineAudioMixer {
|
|
|
70
73
|
}
|
|
71
74
|
const source = ctx.createBufferSource();
|
|
72
75
|
source.buffer = buffer;
|
|
76
|
+
if (playbackRate !== 1) {
|
|
77
|
+
source.playbackRate.value = playbackRate;
|
|
78
|
+
}
|
|
73
79
|
const gainNode = ctx.createGain();
|
|
74
80
|
gainNode.gain.value = clip.volume;
|
|
75
81
|
source.connect(gainNode);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OfflineAudioMixer.js","sources":["../../../src/stages/compose/OfflineAudioMixer.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport {
|
|
1
|
+
{"version":3,"file":"OfflineAudioMixer.js","sources":["../../../src/stages/compose/OfflineAudioMixer.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport {\n hasAudioConfig,\n hasResourceId,\n isVideoClip,\n videoClipPlaybackRate,\n} from '../../model/types';\nimport type { CompositionModel } from '../../model';\nimport type { CacheManager } from '../../cache/CacheManager';\nimport { buildLoopedResourceSegments } from '../../utils/loop-utils';\n\ninterface MixClipInfo {\n clipId: string;\n startUs: TimeUs;\n durationUs: TimeUs;\n volume: number;\n}\n\nexport class OfflineAudioMixer {\n private sampleRate = 48_000;\n private numberOfChannels = 2;\n\n constructor(\n private cacheManager: CacheManager,\n private getModel: () => CompositionModel | null\n ) {}\n\n async mix(windowStartUs: TimeUs, windowEndUs: TimeUs): Promise<AudioBuffer> {\n const durationUs = windowEndUs - windowStartUs;\n // Guard against invalid/empty ranges (can happen near timeline end or after clamping).\n // OfflineAudioContext requires length >= 1.\n const frameCount = Math.max(\n 1,\n Math.ceil((Math.max(0, durationUs) / 1_000_000) * this.sampleRate)\n );\n\n const ctx = new OfflineAudioContext(this.numberOfChannels, frameCount, this.sampleRate);\n\n // Ensure the OfflineAudioContext renders the full requested length.\n // Some implementations may stop early if no sources are scheduled near the tail,\n // which would truncate trailing silence and make export audio shorter than video.\n const silent = ctx.createBuffer(1, frameCount, this.sampleRate);\n const silentSource = ctx.createBufferSource();\n silentSource.buffer = silent;\n const silentGain = ctx.createGain();\n silentGain.gain.value = 0;\n silentSource.connect(silentGain);\n silentGain.connect(ctx.destination);\n silentSource.start(0);\n\n const clips = this.getClipsInWindow(windowStartUs, windowEndUs);\n\n for (const clip of clips) {\n // Calculate clip-relative time range\n const clipIntersectStartUs = Math.max(windowStartUs, clip.startUs);\n const clipIntersectEndUs = Math.min(windowEndUs, clip.startUs + clip.durationUs);\n const clipRelativeStartUs = clipIntersectStartUs - clip.startUs;\n const clipRelativeEndUs = clipIntersectEndUs - clip.startUs;\n\n // Convert to resource time (aligned with video architecture)\n const clipModel = this.getModel()?.findClip(clip.clipId);\n const trimStartUs = clipModel?.trimStartUs ?? 0;\n const loop = clipModel?.trackKind === 'audio' && clipModel.loop === true;\n const playbackRate =\n clipModel && isVideoClip(clipModel) ? videoClipPlaybackRate(clipModel) : 1;\n const resourceDurationUs =\n clipModel && hasResourceId(clipModel)\n ? (this.cacheManager.audioSampleCache.get(clipModel.resourceId)?.durationUs ?? 0)\n : 0;\n\n let segments = buildLoopedResourceSegments({\n clipRelativeStartUs,\n clipRelativeEndUs,\n trimStartUs,\n resourceDurationUs,\n loop,\n playbackRate: loop ? 1 : playbackRate,\n });\n if (segments.length === 0 && clipRelativeEndUs > clipRelativeStartUs) {\n segments = buildLoopedResourceSegments({\n clipRelativeStartUs,\n clipRelativeEndUs,\n trimStartUs,\n resourceDurationUs,\n loop: false,\n playbackRate,\n });\n }\n\n for (const seg of segments) {\n // Get PCM data using resource time coordinates\n const pcmData = this.cacheManager.getClipPCMWithMetadata(\n clip.clipId,\n seg.resourceStartUs,\n seg.resourceEndUs\n );\n\n if (!pcmData || pcmData.planes.length === 0) {\n continue;\n }\n\n const intersectFrames = pcmData.planes[0]?.length ?? 0;\n if (intersectFrames === 0) {\n continue;\n }\n\n // Create AudioBuffer\n const buffer = ctx.createBuffer(pcmData.planes.length, intersectFrames, pcmData.sampleRate);\n\n for (let channel = 0; channel < pcmData.planes.length; channel++) {\n const plane = pcmData.planes[channel];\n if (plane) {\n // Create new Float32Array to ensure correct type (ArrayBuffer, not SharedArrayBuffer)\n buffer.copyToChannel(new Float32Array(plane), channel);\n }\n }\n\n const source = ctx.createBufferSource();\n source.buffer = buffer;\n if (playbackRate !== 1) {\n source.playbackRate.value = playbackRate;\n }\n\n const gainNode = ctx.createGain();\n gainNode.gain.value = clip.volume;\n\n source.connect(gainNode);\n gainNode.connect(ctx.destination);\n\n const segmentStartUs = clip.startUs + seg.clipRelativeStartUs;\n const startTime = (segmentStartUs - windowStartUs) / 1_000_000;\n source.start(startTime);\n }\n }\n\n const mixedBuffer = await ctx.startRendering();\n return mixedBuffer;\n }\n\n private getClipsInWindow(windowStartUs: TimeUs, windowEndUs: TimeUs): MixClipInfo[] {\n const clips: MixClipInfo[] = [];\n const model = this.getModel();\n if (!model) {\n return clips;\n }\n\n for (const track of model.tracks) {\n for (const clip of track.clips) {\n const clipEndUs = clip.startUs + clip.durationUs;\n if (clip.startUs < windowEndUs && clipEndUs > windowStartUs) {\n // Read audio config (only video/audio clips have audioConfig)\n if (hasAudioConfig(clip)) {\n const muted = clip.audioConfig?.muted ?? false;\n\n // Skip muted clips in export (performance optimization)\n if (muted) {\n continue;\n }\n\n const volume = clip.audioConfig?.volume ?? 1.0;\n\n clips.push({\n clipId: clip.id,\n startUs: clip.startUs,\n durationUs: clip.durationUs,\n volume,\n });\n } else {\n // Caption/Fx clips in audio track should not happen, but handle gracefully\n clips.push({\n clipId: clip.id,\n startUs: clip.startUs,\n durationUs: clip.durationUs,\n volume: 1.0,\n });\n }\n }\n }\n }\n\n return clips;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -12,5 +12,7 @@ export declare function buildLoopedResourceSegments(params: {
|
|
|
12
12
|
trimStartUs: TimeUs;
|
|
13
13
|
resourceDurationUs: TimeUs;
|
|
14
14
|
loop: boolean;
|
|
15
|
+
/** Applied only when loop is false (e.g. video clip timeline speed). */
|
|
16
|
+
playbackRate?: number;
|
|
15
17
|
}): LoopSegment[];
|
|
16
18
|
//# sourceMappingURL=loop-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop-utils.d.ts","sourceRoot":"","sources":["../../src/utils/loop-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAE7C,MAAM,WAAW,WAAW;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE;IAClD,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,IAAI,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"loop-utils.d.ts","sourceRoot":"","sources":["../../src/utils/loop-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAE7C,MAAM,WAAW,WAAW;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE;IAClD,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,wEAAwE;IACxE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GAAG,WAAW,EAAE,CAkDhB"}
|
package/dist/utils/loop-utils.js
CHANGED
|
@@ -6,12 +6,15 @@ function buildLoopedResourceSegments(params) {
|
|
|
6
6
|
return [];
|
|
7
7
|
}
|
|
8
8
|
if (!params.loop) {
|
|
9
|
+
const rawR = params.playbackRate ?? 1;
|
|
10
|
+
const rate = typeof rawR === "number" && Number.isFinite(rawR) && rawR > 0 ? rawR : 1;
|
|
11
|
+
const trim = params.trimStartUs ?? 0;
|
|
9
12
|
return [
|
|
10
13
|
{
|
|
11
14
|
clipRelativeStartUs: rangeStartUs,
|
|
12
15
|
durationUs: requestedDurationUs,
|
|
13
|
-
resourceStartUs:
|
|
14
|
-
resourceEndUs:
|
|
16
|
+
resourceStartUs: trim + rangeStartUs * rate,
|
|
17
|
+
resourceEndUs: trim + rangeEndUs * rate
|
|
15
18
|
}
|
|
16
19
|
];
|
|
17
20
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop-utils.js","sources":["../../src/utils/loop-utils.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\n\nexport interface LoopSegment {\n clipRelativeStartUs: TimeUs;\n durationUs: TimeUs;\n resourceStartUs: TimeUs;\n resourceEndUs: TimeUs;\n}\n\nexport function buildLoopedResourceSegments(params: {\n clipRelativeStartUs: TimeUs;\n clipRelativeEndUs: TimeUs;\n trimStartUs: TimeUs;\n resourceDurationUs: TimeUs;\n loop: boolean;\n}): LoopSegment[] {\n const rangeStartUs = Math.max(0, params.clipRelativeStartUs);\n const rangeEndUs = Math.max(rangeStartUs, params.clipRelativeEndUs);\n\n const requestedDurationUs = rangeEndUs - rangeStartUs;\n if (requestedDurationUs <= 0) {\n return [];\n }\n\n if (!params.loop) {\n return [\n {\n clipRelativeStartUs: rangeStartUs,\n durationUs: requestedDurationUs,\n resourceStartUs:
|
|
1
|
+
{"version":3,"file":"loop-utils.js","sources":["../../src/utils/loop-utils.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\n\nexport interface LoopSegment {\n clipRelativeStartUs: TimeUs;\n durationUs: TimeUs;\n resourceStartUs: TimeUs;\n resourceEndUs: TimeUs;\n}\n\nexport function buildLoopedResourceSegments(params: {\n clipRelativeStartUs: TimeUs;\n clipRelativeEndUs: TimeUs;\n trimStartUs: TimeUs;\n resourceDurationUs: TimeUs;\n loop: boolean;\n /** Applied only when loop is false (e.g. video clip timeline speed). */\n playbackRate?: number;\n}): LoopSegment[] {\n const rangeStartUs = Math.max(0, params.clipRelativeStartUs);\n const rangeEndUs = Math.max(rangeStartUs, params.clipRelativeEndUs);\n\n const requestedDurationUs = rangeEndUs - rangeStartUs;\n if (requestedDurationUs <= 0) {\n return [];\n }\n\n if (!params.loop) {\n const rawR = params.playbackRate ?? 1;\n const rate = typeof rawR === 'number' && Number.isFinite(rawR) && rawR > 0 ? rawR : 1;\n const trim = params.trimStartUs ?? 0;\n return [\n {\n clipRelativeStartUs: rangeStartUs,\n durationUs: requestedDurationUs,\n resourceStartUs: trim + rangeStartUs * rate,\n resourceEndUs: trim + rangeEndUs * rate,\n },\n ];\n }\n\n const trimStartUs = params.trimStartUs ?? 0;\n const periodUs = params.resourceDurationUs - trimStartUs;\n if (periodUs <= 0) {\n return [];\n }\n\n const segments: LoopSegment[] = [];\n let tUs = rangeStartUs;\n\n while (tUs < rangeEndUs) {\n const offsetInPeriodUs = tUs % periodUs;\n const maxLenUs = periodUs - offsetInPeriodUs;\n const lenUs = Math.min(rangeEndUs - tUs, maxLenUs);\n if (lenUs <= 0) break;\n\n const resourceStartUs = trimStartUs + offsetInPeriodUs;\n segments.push({\n clipRelativeStartUs: tUs,\n durationUs: lenUs,\n resourceStartUs,\n resourceEndUs: resourceStartUs + lenUs,\n });\n\n tUs += lenUs;\n }\n\n return segments;\n}\n"],"names":[],"mappings":"AASO,SAAS,4BAA4B,QAQ1B;AAChB,QAAM,eAAe,KAAK,IAAI,GAAG,OAAO,mBAAmB;AAC3D,QAAM,aAAa,KAAK,IAAI,cAAc,OAAO,iBAAiB;AAElE,QAAM,sBAAsB,aAAa;AACzC,MAAI,uBAAuB,GAAG;AAC5B,WAAO,CAAA;AAAA,EACT;AAEA,MAAI,CAAC,OAAO,MAAM;AAChB,UAAM,OAAO,OAAO,gBAAgB;AACpC,UAAM,OAAO,OAAO,SAAS,YAAY,OAAO,SAAS,IAAI,KAAK,OAAO,IAAI,OAAO;AACpF,UAAM,OAAO,OAAO,eAAe;AACnC,WAAO;AAAA,MACL;AAAA,QACE,qBAAqB;AAAA,QACrB,YAAY;AAAA,QACZ,iBAAiB,OAAO,eAAe;AAAA,QACvC,eAAe,OAAO,aAAa;AAAA,MAAA;AAAA,IACrC;AAAA,EAEJ;AAEA,QAAM,cAAc,OAAO,eAAe;AAC1C,QAAM,WAAW,OAAO,qBAAqB;AAC7C,MAAI,YAAY,GAAG;AACjB,WAAO,CAAA;AAAA,EACT;AAEA,QAAM,WAA0B,CAAA;AAChC,MAAI,MAAM;AAEV,SAAO,MAAM,YAAY;AACvB,UAAM,mBAAmB,MAAM;AAC/B,UAAM,WAAW,WAAW;AAC5B,UAAM,QAAQ,KAAK,IAAI,aAAa,KAAK,QAAQ;AACjD,QAAI,SAAS,EAAG;AAEhB,UAAM,kBAAkB,cAAc;AACtC,aAAS,KAAK;AAAA,MACZ,qBAAqB;AAAA,MACrB,YAAY;AAAA,MACZ;AAAA,MACA,eAAe,kBAAkB;AAAA,IAAA,CAClC;AAED,WAAO;AAAA,EACT;AAEA,SAAO;AACT;"}
|
|
@@ -2633,13 +2633,14 @@ class FrameRateConverter {
|
|
|
2633
2633
|
clipDurationUs;
|
|
2634
2634
|
frameDurationUs;
|
|
2635
2635
|
trimStartUs;
|
|
2636
|
+
playbackRate;
|
|
2636
2637
|
totalFrameCount;
|
|
2637
2638
|
// State for frame processing
|
|
2638
2639
|
targetFrameIndex = 0;
|
|
2639
2640
|
targetFrameTimeUs = 0;
|
|
2640
2641
|
sourceFrameBuffer = [];
|
|
2641
2642
|
maxSourceTimestampUs = null;
|
|
2642
|
-
constructor(targetFps, clipDurationUs, trimStartUs = 0) {
|
|
2643
|
+
constructor(targetFps, clipDurationUs, trimStartUs = 0, playbackRate = 1) {
|
|
2643
2644
|
if (targetFps <= 0) {
|
|
2644
2645
|
throw new Error(`Invalid target fps: ${targetFps}`);
|
|
2645
2646
|
}
|
|
@@ -2649,6 +2650,7 @@ class FrameRateConverter {
|
|
|
2649
2650
|
this.clipDurationUs = clipDurationUs;
|
|
2650
2651
|
this.frameDurationUs = Math.round(1e6 / targetFps);
|
|
2651
2652
|
this.trimStartUs = trimStartUs;
|
|
2653
|
+
this.playbackRate = typeof playbackRate === "number" && Number.isFinite(playbackRate) && playbackRate > 0 ? playbackRate : 1;
|
|
2652
2654
|
this.totalFrameCount = Number.isFinite(clipDurationUs) ? Math.max(1, Math.round(clipDurationUs / this.frameDurationUs)) : null;
|
|
2653
2655
|
}
|
|
2654
2656
|
/**
|
|
@@ -2699,10 +2701,11 @@ class FrameRateConverter {
|
|
|
2699
2701
|
const bufferedTs = frameToBuffer.timestamp ?? 0;
|
|
2700
2702
|
this.maxSourceTimestampUs = this.maxSourceTimestampUs === null ? bufferedTs : Math.max(this.maxSourceTimestampUs, bufferedTs);
|
|
2701
2703
|
while (this.shouldContinueOutput()) {
|
|
2702
|
-
|
|
2704
|
+
const targetSourceUs = this.targetFrameTimeUs * this.playbackRate;
|
|
2705
|
+
if (this.maxSourceTimestampUs !== null && targetSourceUs > this.maxSourceTimestampUs) {
|
|
2703
2706
|
break;
|
|
2704
2707
|
}
|
|
2705
|
-
const closestFrame = this.findClosestFrame(
|
|
2708
|
+
const closestFrame = this.findClosestFrame(targetSourceUs);
|
|
2706
2709
|
if (!closestFrame) {
|
|
2707
2710
|
break;
|
|
2708
2711
|
}
|
|
@@ -2731,10 +2734,11 @@ class FrameRateConverter {
|
|
|
2731
2734
|
*/
|
|
2732
2735
|
flushRemainingFrames(controller) {
|
|
2733
2736
|
while (this.sourceFrameBuffer.length > 0 && this.shouldContinueOutput()) {
|
|
2734
|
-
|
|
2737
|
+
const targetSourceUs = this.targetFrameTimeUs * this.playbackRate;
|
|
2738
|
+
if (this.maxSourceTimestampUs !== null && targetSourceUs > this.maxSourceTimestampUs) {
|
|
2735
2739
|
break;
|
|
2736
2740
|
}
|
|
2737
|
-
const closestFrame = this.findClosestFrame(
|
|
2741
|
+
const closestFrame = this.findClosestFrame(targetSourceUs);
|
|
2738
2742
|
if (!closestFrame) break;
|
|
2739
2743
|
if (!this.outputTargetFrame(closestFrame, controller)) break;
|
|
2740
2744
|
}
|
|
@@ -2830,7 +2834,8 @@ class FrameRateConverter {
|
|
|
2830
2834
|
shouldWaitForNextFrame(closestFrame) {
|
|
2831
2835
|
if (this.sourceFrameBuffer.length <= 1) {
|
|
2832
2836
|
const frameTimestamp = closestFrame.timestamp ?? 0;
|
|
2833
|
-
|
|
2837
|
+
const targetSourceUs = this.targetFrameTimeUs * this.playbackRate;
|
|
2838
|
+
if (frameTimestamp < targetSourceUs) {
|
|
2834
2839
|
return true;
|
|
2835
2840
|
}
|
|
2836
2841
|
}
|
|
@@ -2840,16 +2845,18 @@ class FrameRateConverter {
|
|
|
2840
2845
|
* Clean up source frames that are no longer needed
|
|
2841
2846
|
* Keep frames that might be needed for future target frames
|
|
2842
2847
|
*/
|
|
2843
|
-
cleanupOldFrames(
|
|
2844
|
-
const
|
|
2845
|
-
const
|
|
2848
|
+
cleanupOldFrames(currentTimelineTargetUs) {
|
|
2849
|
+
const nextTimelineUs = currentTimelineTargetUs + this.frameDurationUs;
|
|
2850
|
+
const prevTimelineUs = currentTimelineTargetUs - this.frameDurationUs;
|
|
2851
|
+
const nextSourceUs = nextTimelineUs * this.playbackRate;
|
|
2852
|
+
const prevSourceUs = prevTimelineUs * this.playbackRate;
|
|
2846
2853
|
let removeCount = 0;
|
|
2847
2854
|
for (let i = 0; i < this.sourceFrameBuffer.length; i++) {
|
|
2848
2855
|
const frame = this.sourceFrameBuffer[i];
|
|
2849
2856
|
if (!frame) continue;
|
|
2850
2857
|
const frameTimestamp = frame.timestamp ?? 0;
|
|
2851
|
-
const isNeededForNext = Math.abs(frameTimestamp -
|
|
2852
|
-
if (frameTimestamp <
|
|
2858
|
+
const isNeededForNext = Math.abs(frameTimestamp - nextSourceUs) < Math.abs(frameTimestamp - prevSourceUs);
|
|
2859
|
+
if (frameTimestamp < prevSourceUs && !isNeededForNext) {
|
|
2853
2860
|
try {
|
|
2854
2861
|
frame.close();
|
|
2855
2862
|
} catch {
|
|
@@ -3062,19 +3069,22 @@ class ExportWorker {
|
|
|
3062
3069
|
(l) => l.type === "video" && !l.payload.attachmentId
|
|
3063
3070
|
);
|
|
3064
3071
|
const clipTrimStartUs = mainLayer?.type === "video" ? mainLayer.payload.trimStartUs ?? 0 : 0;
|
|
3072
|
+
const rawRate = mainLayer?.type === "video" ? mainLayer.payload.playbackRate ?? 1 : 1;
|
|
3073
|
+
const playbackRate = typeof rawRate === "number" && Number.isFinite(rawRate) && rawRate > 0 ? rawRate : 1;
|
|
3065
3074
|
const timeline = this.instructions.baseConfig.timeline;
|
|
3066
3075
|
const fps = timeline?.compositionFps ?? 30;
|
|
3067
3076
|
const windowStartUs = metadata?.windowStartUs ?? clipTrimStartUs;
|
|
3068
|
-
const
|
|
3069
|
-
const
|
|
3070
|
-
const
|
|
3071
|
-
const
|
|
3077
|
+
const windowEndResourceUs = metadata?.windowEndUs ?? clipTrimStartUs + (timeline?.clipDurationUs ?? Infinity) * playbackRate;
|
|
3078
|
+
const resourceWindowUs = windowEndResourceUs - windowStartUs;
|
|
3079
|
+
const timelineWindowUs = resourceWindowUs / playbackRate;
|
|
3080
|
+
const windowToClipOffsetUs = (windowStartUs - clipTrimStartUs) / playbackRate;
|
|
3081
|
+
const fpsConverter = new FrameRateConverter(fps, timelineWindowUs, windowStartUs, playbackRate);
|
|
3072
3082
|
const cfrStream = stream.pipeThrough(fpsConverter.createStream());
|
|
3073
3083
|
const composeRequestStream = cfrStream.pipeThrough(
|
|
3074
3084
|
new TransformStream({
|
|
3075
3085
|
transform: (frame, controller) => {
|
|
3076
3086
|
let composeFrame = frame;
|
|
3077
|
-
if (windowToClipOffsetUs
|
|
3087
|
+
if (windowToClipOffsetUs !== 0) {
|
|
3078
3088
|
composeFrame = new VideoFrame(frame, {
|
|
3079
3089
|
timestamp: (frame.timestamp ?? 0) + windowToClipOffsetUs
|
|
3080
3090
|
});
|
|
@@ -3243,4 +3253,4 @@ const export_worker = null;
|
|
|
3243
3253
|
export {
|
|
3244
3254
|
export_worker as default
|
|
3245
3255
|
};
|
|
3246
|
-
//# sourceMappingURL=export.worker.
|
|
3256
|
+
//# sourceMappingURL=export.worker.Cw9iPvkh.js.map
|