@meframe/core 0.0.10 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Meframe.d.ts.map +1 -1
- package/dist/Meframe.js +3 -0
- package/dist/Meframe.js.map +1 -1
- package/dist/cache/CacheManager.d.ts +12 -0
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +16 -4
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/controllers/PlaybackController.d.ts.map +1 -1
- package/dist/controllers/PlaybackController.js +3 -0
- package/dist/controllers/PlaybackController.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +13 -4
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/types.d.ts +1 -0
- package/dist/orchestrator/types.d.ts.map +1 -1
- package/dist/stages/mux/MP4Muxer.js +1 -1
- package/dist/stages/mux/MP4Muxer.js.map +1 -1
- package/dist/stages/mux/MuxManager.d.ts.map +1 -1
- package/dist/stages/mux/MuxManager.js +36 -5
- package/dist/stages/mux/MuxManager.js.map +1 -1
- package/dist/workers/stages/compose/video-compose.worker.js +7 -56
- package/dist/workers/stages/compose/video-compose.worker.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Orchestrator.js","sources":["../../src/orchestrator/Orchestrator.ts"],"sourcesContent":["import { EventBus } from '../event/EventBus';\nimport { WorkerPool } from '../worker/WorkerPool';\nimport { applyPatch as applyModelPatch } from '../model/patch';\nimport { ResourceConflictError, ResourceLoader } from '../stages/load/ResourceLoader';\nimport { CacheManager } from '../cache/CacheManager';\nimport { ConfigLoader } from '../config/ConfigLoader';\nimport type { IOrchestrator, OrchestratorConfig, RenderFrameOptions } from './types';\nimport { WorkerStatus, WorkerType } from '../worker/types';\nimport { CompositionModel, CompositionPatch, Resource, TimeUs, RcFrame } from '../model';\nimport { MeframeEvent, type EventPayloadMap } from '../event/events';\nimport { CompositionPlanner } from './CompositionPlanner';\nimport { VideoClipSession } from './VideoClipSession';\nimport { ClipSessionManager } from './ClipSessionManager';\nimport { GlobalAudioSession } from '../stages/compose/GlobalAudioSession';\nimport { MuxManager } from '../stages/mux/MuxManager';\nimport { ExportOptions } from '@/types';\n\nexport class Orchestrator implements IOrchestrator {\n workers: WorkerPool;\n eventBus: EventBus<EventPayloadMap>;\n compositionModel: CompositionModel | null = null;\n resourceLoader: ResourceLoader;\n cacheManager: CacheManager;\n planner: CompositionPlanner;\n audioSession: GlobalAudioSession;\n muxManager: MuxManager;\n\n private activeClips = new Set<string>();\n private isInitialized = false;\n private config = ConfigLoader.getInstance().getConfig();\n private clipSessionManager: ClipSessionManager;\n private currentClipId: string | null = null;\n readonly events: Pick<EventBus<EventPayloadMap>, 'on' | 'off' | 'once'>;\n\n constructor(config: OrchestratorConfig) {\n // Use provided eventBus or create a new one\n this.eventBus = config.eventBus || new EventBus<EventPayloadMap>();\n this.events = this.eventBus.asReadonly();\n\n // Initialize config first\n this.config = ConfigLoader.getInstance().getConfig();\n\n const workerConfigs = this.buildWorkerConfigs();\n\n // Initialize WorkerPool with worker path from config\n this.workers = new WorkerPool({\n eventBus: this.eventBus,\n workerConfigs,\n workerPath: config.workerPath,\n workerExtension: config.workerExtension,\n });\n\n this.resourceLoader = new ResourceLoader({\n orchestrator: this as any,\n eventBus: this.eventBus,\n config: {\n maxConcurrent: config.maxWorkers || (this.config.load as any)?.retry?.maxAttempts || 4,\n },\n onStateChange: (resourceId, state) => this.handleResourceStateChange(resourceId, state),\n });\n\n this.planner = new CompositionPlanner();\n\n const cacheConfig = config.cacheConfig || this.config.cache;\n this.cacheManager = new CacheManager(\n {\n l1: {\n maxMemoryMB:\n (cacheConfig as any)?.l1Size || (cacheConfig as any)?.l1?.maxMemoryMB || 1024,\n maxGOPs: (this.config.decode as any)?.video?.maxGOPs || 4,\n },\n l2: {\n maxSizeMB: (cacheConfig as any)?.l2Size || (cacheConfig as any)?.l2?.maxSizeMB || 2048,\n projectId: 'default',\n },\n },\n this.eventBus\n );\n\n this.clipSessionManager = new ClipSessionManager({\n maxConcurrent: 2,\n factory: {\n createSession: (clipId) => this.createSession(clipId),\n },\n model: () => this.compositionModel,\n cacheManager: this.cacheManager,\n });\n\n this.audioSession = new GlobalAudioSession({\n cacheManager: this.cacheManager,\n workers: this.workers,\n resourceLoader: this.resourceLoader,\n eventBus: this.eventBus,\n getModel: () => this.compositionModel,\n buildWorkerConfigs: () => this.buildWorkerConfigs(),\n });\n\n this.muxManager = new MuxManager(\n this.cacheManager,\n this.audioSession,\n this.config.encode.audio as AudioEncoderConfig\n );\n }\n\n get workerStatus(): WorkerStatus {\n const status = this.workers.status;\n const result: WorkerStatus = {} as WorkerStatus;\n\n const workerTypes: WorkerType[] = [\n 'videoDemux',\n 'audioDemux',\n 'videoDecode',\n 'audioDecode',\n 'videoCompose',\n 'audioCompose',\n 'videoEncode',\n ];\n\n for (const type of workerTypes) {\n result[type] = status[type] || {\n state: 'idle',\n taskCount: 0,\n };\n }\n\n return result;\n }\n\n async initialize(): Promise<void> {\n if (this.isInitialized) return;\n\n await this.cacheManager.init();\n\n this.isInitialized = true;\n }\n\n // Event methods - forward to eventBus\n on<K extends keyof EventPayloadMap>(\n event: K,\n handler: (payload: EventPayloadMap[K]) => void\n ): void {\n this.eventBus.on(event, handler);\n }\n\n off<K extends keyof EventPayloadMap>(\n event: K,\n handler: (payload: EventPayloadMap[K]) => void\n ): void {\n this.eventBus.off(event, handler);\n }\n\n once<K extends keyof EventPayloadMap>(\n event: K,\n handler: (payload: EventPayloadMap[K]) => void\n ): void {\n this.eventBus.once(event, handler);\n }\n\n async setCompositionModel(model: CompositionModel): Promise<void> {\n this.compositionModel = model;\n this.planner.setModel(model);\n this.currentClipId = null;\n\n this.eventBus.emit(MeframeEvent.ModelSet, model);\n\n this.eventBus.emit(MeframeEvent.CompositionUpdated, {\n trackCount: model.tracks.length,\n clipCount: model.tracks.reduce((acc: number, track: any) => acc + track.clips.length, 0),\n durationUs: model.durationUs,\n });\n\n await this.audioSession.activateAllAudioClips();\n\n this.ensureClipCache(0);\n }\n\n async applyPatch(patch: CompositionPatch): Promise<void> {\n if (!this.compositionModel) {\n throw new Error('No composition model set');\n }\n\n // Apply patch and get affected clip IDs (simplified for 2-Clip strategy)\n const affectedClipIds = applyModelPatch(this.compositionModel, patch);\n const clipUpdates = this.planner.applyPatch(patch, affectedClipIds);\n\n this.eventBus.emit(MeframeEvent.PatchApplied, {\n operations: patch.operations.length,\n affectedClips: Array.from(affectedClipIds),\n });\n\n // Get currently active clips in the 2-Clip window\n const activeClipIds = new Set(\n Array.from(this.clipSessionManager['entries'].keys()).filter((clipId) =>\n this.clipSessionManager.isClipActive(clipId)\n )\n );\n\n // Process clip updates\n for (const update of clipUpdates) {\n if (update.type === 'remove') {\n this.activeClips.delete(update.clipId);\n this.cacheManager.evictClip(update.clipId);\n }\n\n // Notify ClipSessionManager to handle the update\n // This will install new instructions or restart pipeline as needed\n await this.clipSessionManager.handlePlannerUpdate(update.clipId, update);\n\n // Only evict cache for active clips (in current 2-Clip window)\n // Non-active clips will be regenerated when they enter the window\n if (activeClipIds.has(update.clipId) && update.type !== 'remove') {\n this.cacheManager.evictClip(update.clipId);\n }\n }\n }\n\n private handleResourceStateChange(resourceId: string, state: Resource['state']): void {\n if (!this.compositionModel) {\n return;\n }\n\n this.compositionModel.updateResourceState(resourceId, state ?? 'pending');\n\n if (state !== 'ready') {\n return;\n }\n\n const resource = this.compositionModel.getResource(resourceId);\n if (!resource) {\n return;\n }\n\n // Main video/audio resources: data will flow naturally into pipeline\n if (resource.type === 'video' || resource.type === 'audio') {\n return;\n }\n\n // Attachment resources (fonts, images): update instructions for active clips\n const clipIds = this.compositionModel.getClipIdsByResourceId(resourceId);\n for (const clipId of clipIds) {\n // Only update active clips (in 2-Clip window)\n if (!this.clipSessionManager.isClipActive(clipId)) {\n continue;\n }\n\n const clip = this.compositionModel.findClip(clipId);\n if (!clip) {\n continue;\n }\n\n // Rebuild instructions with updated resource status\n const instructions = this.planner.getInstructions(clipId);\n if (!instructions) {\n continue;\n }\n\n // Send updated instructions to worker (no pipeline restart needed)\n const session = this.clipSessionManager.getSession(clipId);\n const visualWorker = session?.visualWorkerHandle;\n if (visualWorker) {\n visualWorker.send('install_instructions', instructions);\n }\n }\n }\n\n async restartWorker(type: WorkerType, clipId?: string): Promise<void> {\n const clipLocalTypes: WorkerType[] = [\n 'videoDemux',\n 'audioDemux',\n 'videoDecode',\n 'audioDecode',\n 'videoCompose',\n 'videoEncode',\n ];\n\n if (clipLocalTypes.includes(type) && !clipId) {\n throw new Error(`clipId required for restarting ${type} worker`);\n }\n\n this.workers.terminate(type, clipId);\n const worker = await this.workers.get(type, clipId);\n\n this.eventBus.emit(MeframeEvent.WorkerRestarted, {\n type,\n workerId: worker.getWorkerId(),\n reason: 'Manual restart',\n });\n\n if (clipId) {\n const session = this.clipSessionManager.getSession(clipId);\n if (session) {\n await session.activate();\n }\n }\n }\n\n async renderFrame(timeUs: TimeUs, options?: RenderFrameOptions): Promise<RcFrame | null> {\n const signal = options?.signal;\n\n if (!this.compositionModel) {\n throw new Error('No composition model set');\n }\n\n const clip = this.compositionModel.getClipsAtTime(timeUs, this.compositionModel.mainTrackId)[0];\n if (!clip) {\n return null;\n }\n\n // Detect clip change and proactively ensure cache for prev/current/next\n if (this.currentClipId !== clip.id) {\n this.currentClipId = clip.id;\n void this.ensureClipCache(timeUs);\n }\n\n const cachedFrame = await this.cacheManager.getFrame(timeUs, clip.id);\n // console.log('>>>>>>>>>>>> renderFrame', timeUs, clip.id, cachedFrame);\n if (cachedFrame) {\n this.eventBus.emit(MeframeEvent.CacheHit, {\n timeUs,\n level: 'L1',\n key: `${clip.id}-${timeUs}`,\n });\n return cachedFrame;\n }\n // Cache miss - also ensure cache (defensive, already called on clip change)\n void this.ensureClipCache(timeUs);\n\n this.eventBus.emit(MeframeEvent.CacheMiss, {\n timeUs,\n level: 'L1',\n key: `${clip.id}-${timeUs}`,\n });\n\n if (signal?.aborted) {\n throw new DOMException('Render aborted', 'AbortError');\n }\n\n // Return null immediately instead of waiting\n // This allows PlaybackController to detect miss and trigger buffering\n return null;\n }\n\n /**\n * Ensure clips are cached using 2-Clip strategy\n * Called by PlaybackController\n */\n async ensureClipCache(timeUs: TimeUs): Promise<void> {\n if (!this.compositionModel) {\n return;\n }\n\n await this.clipSessionManager.ensureClips(timeUs);\n }\n\n /**\n * Wait for clip cache to be ready for playback\n * Returns true if minimum cache is ready, false if timeout\n */\n async waitForClipReady(\n timeUs: TimeUs,\n options?: { minFrameCount?: number; timeoutMs?: number }\n ): Promise<boolean> {\n if (!this.compositionModel) {\n return false;\n }\n\n const clips = this.compositionModel.getClipsAtTime(timeUs, this.compositionModel.mainTrackId);\n if (clips.length === 0) {\n return true;\n }\n\n const currentClip = clips[0];\n if (!currentClip) {\n return true;\n }\n\n return this.cacheManager.waitForClipReady(currentClip.id, {\n minFrameCount: options?.minFrameCount ?? 30,\n timeoutMs: options?.timeoutMs ?? 5_000,\n });\n }\n\n /**\n * Render a clip completely for L2 cache (bypass ClipSessionManager)\n * Returns a promise that resolves when encoding is complete\n */\n async renderClipForL2(clipId: string): Promise<boolean> {\n const sessionId = `${clipId}#l2`;\n return new Promise<boolean>((resolve, reject) => {\n this.createSession(sessionId, {\n forL2Only: true,\n clipId: clipId,\n onL2Complete: () => {\n resolve(true);\n },\n onL2Error: (error) => {\n console.error('[Orchestrator] L2 rendering failed for', clipId, error);\n reject(error);\n },\n })\n .then((session) => session.activate())\n .catch((error) => {\n if (error instanceof ResourceConflictError) {\n resolve(false);\n } else {\n reject(error);\n }\n });\n });\n }\n\n /**\n * Create a new session for a clip\n */\n private async createSession(\n sessionId: string,\n options?: {\n forL2Only?: boolean;\n clipId?: string;\n onL2Complete?: () => void;\n onL2Error?: (error: Error) => void;\n }\n ): Promise<VideoClipSession> {\n const clipId = options?.clipId ?? sessionId;\n const clip = this.compositionModel?.findClip(clipId);\n if (!clip) {\n throw new Error(`Clip ${clipId} not found`);\n }\n\n const session = await VideoClipSession.create({\n clipId,\n sessionId,\n forL2Only: options?.forL2Only ?? false,\n planner: this.planner,\n workerPool: this.workers,\n cacheManager: this.cacheManager,\n compositionModel: this.compositionModel!,\n workerConfigs: this.buildWorkerConfigs(),\n callbacks: {\n onComposedStreamReady: (stream, fps) => {\n if (options?.forL2Only) {\n // L2 channel: don't need L1, cancel stream\n stream.cancel();\n } else {\n // Preview channel: fill L1\n this.cacheManager.receiveComposedFrames(stream, {\n clipId: clipId,\n trackId: this.compositionModel!.mainTrackId,\n fps,\n onFrame: () => {},\n });\n }\n },\n onEncodedStreamReady: (stream, track) => {\n if (options?.forL2Only) {\n // L2 channel: write to L2 using clipId, notify on complete\n this.cacheManager.receiveEncodedChunks(stream, clipId, track, {\n onComplete: () => {\n session.dispose();\n // Only notify completion for video track (audio is optional)\n if (track === 'video' && options.onL2Complete) {\n options.onL2Complete();\n }\n },\n onError: (error) => {\n console.error(\n `[Orchestrator] L2 encode stream error for ${clipId} ${track}:`,\n error\n );\n session.dispose();\n if (options.onL2Error) {\n options.onL2Error(error);\n }\n },\n });\n } else {\n // Preview channel: don't write to L2, cancel stream\n stream.cancel();\n }\n },\n onPipelineReady: async () => {\n const clip = this.compositionModel?.findClip(clipId);\n if (clip?.resourceId) {\n await this.resourceLoader.fetch(clip.resourceId, {\n priority: options?.forL2Only ? 'low' : 'high',\n sessionId: sessionId,\n trackId: clip.trackId,\n });\n }\n },\n },\n });\n\n this.activeClips.add(sessionId);\n return session;\n }\n\n async dispose(): Promise<void> {\n this.resourceLoader.dispose();\n await this.clipSessionManager.dispose();\n await this.cacheManager.clear();\n\n this.currentClipId = null;\n this.activeClips.clear();\n\n this.workers.terminateAll();\n this.compositionModel = null;\n this.eventBus.dispose();\n }\n\n private buildWorkerConfigs(): Record<WorkerType, any> {\n const config = this.config as any;\n const defaultCanvasWidth = config.global?.defaultCanvasWidth ?? 720;\n const defaultCanvasHeight = config.global?.defaultCanvasHeight ?? 1280;\n const defaultFps = config.global?.defaultFps ?? 30;\n return {\n videoDemux: {\n highWaterMark: config.demux?.backpressure?.highWaterMark ?? 10,\n },\n audioDemux: {\n highWaterMark: config.demux?.backpressure?.highWaterMark ?? 10,\n },\n videoDecode: config.decode?.video,\n audioDecode: config.decode?.audio,\n videoCompose: {\n width: config.compose?.canvas?.width ?? defaultCanvasWidth,\n height: config.compose?.canvas?.height ?? defaultCanvasHeight,\n fps: config.global?.defaultFps ?? defaultFps,\n backgroundColor: config.compose?.canvas?.backgroundColor ?? '#000000',\n enableSmoothing: config.compose?.visual?.enableSmoothing ?? true,\n enableHardwareAcceleration: config.compose?.visual?.enableHardwareAcceleration ?? true,\n },\n audioCompose: {\n ducking: config.compose?.audio?.ducking,\n mixing: config.compose?.audio?.mixing,\n },\n videoEncode: {\n codec: 'avc1.42002A',\n width: config.compose?.canvas?.width || defaultCanvasWidth,\n height: config.compose?.canvas?.height || defaultCanvasHeight,\n bitrate: config.encode?.video?.bitrateKbps\n ? config.encode.video.bitrateKbps * 1000\n : 12_000_000,\n framerate: config.encode?.video?.framerate || defaultFps,\n latencyMode: 'quality',\n bitrateMode: 'variable',\n hardwareAcceleration: 'no-preference',\n ...(config.encode?.video as any),\n },\n };\n }\n\n async export(model: CompositionModel, options: ExportOptions): Promise<Blob> {\n return this.muxManager.export(model, options);\n }\n}\n"],"names":["applyModelPatch","clip"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Orchestrator.js","sources":["../../src/orchestrator/Orchestrator.ts"],"sourcesContent":["import { EventBus } from '../event/EventBus';\nimport { WorkerPool } from '../worker/WorkerPool';\nimport { applyPatch as applyModelPatch } from '../model/patch';\nimport { ResourceConflictError, ResourceLoader } from '../stages/load/ResourceLoader';\nimport { CacheManager } from '../cache/CacheManager';\nimport { ConfigLoader } from '../config/ConfigLoader';\nimport type { IOrchestrator, OrchestratorConfig, RenderFrameOptions } from './types';\nimport { WorkerStatus, WorkerType } from '../worker/types';\nimport { CompositionModel, CompositionPatch, Resource, TimeUs, RcFrame } from '../model';\nimport { MeframeEvent, type EventPayloadMap } from '../event/events';\nimport { CompositionPlanner } from './CompositionPlanner';\nimport { VideoClipSession } from './VideoClipSession';\nimport { ClipSessionManager } from './ClipSessionManager';\nimport { GlobalAudioSession } from '../stages/compose/GlobalAudioSession';\nimport { MuxManager } from '../stages/mux/MuxManager';\nimport { ExportOptions } from '@/types';\n\nexport class Orchestrator implements IOrchestrator {\n workers: WorkerPool;\n eventBus: EventBus<EventPayloadMap>;\n compositionModel: CompositionModel | null = null;\n resourceLoader: ResourceLoader;\n cacheManager: CacheManager;\n planner: CompositionPlanner;\n audioSession: GlobalAudioSession;\n muxManager: MuxManager;\n\n private activeClips = new Set<string>();\n private isInitialized = false;\n private config = ConfigLoader.getInstance().getConfig();\n private clipSessionManager: ClipSessionManager;\n private currentClipId: string | null = null;\n readonly events: Pick<EventBus<EventPayloadMap>, 'on' | 'off' | 'once'>;\n\n constructor(config: OrchestratorConfig) {\n // Use provided eventBus or create a new one\n this.eventBus = config.eventBus || new EventBus<EventPayloadMap>();\n this.events = this.eventBus.asReadonly();\n\n // Initialize config first\n this.config = ConfigLoader.getInstance().getConfig();\n\n const workerConfigs = this.buildWorkerConfigs();\n\n // Initialize WorkerPool with worker path from config\n this.workers = new WorkerPool({\n eventBus: this.eventBus,\n workerConfigs,\n workerPath: config.workerPath,\n workerExtension: config.workerExtension,\n });\n\n this.resourceLoader = new ResourceLoader({\n orchestrator: this as any,\n eventBus: this.eventBus,\n config: {\n maxConcurrent: config.maxWorkers || (this.config.load as any)?.retry?.maxAttempts || 4,\n },\n onStateChange: (resourceId, state) => this.handleResourceStateChange(resourceId, state),\n });\n\n this.planner = new CompositionPlanner();\n\n const cacheConfig = config.cacheConfig || this.config.cache;\n this.cacheManager = new CacheManager(\n {\n l1: {\n maxMemoryMB:\n (cacheConfig as any)?.l1Size || (cacheConfig as any)?.l1?.maxMemoryMB || 1024,\n maxGOPs: (this.config.decode as any)?.video?.maxGOPs || 4,\n },\n l2: {\n maxSizeMB: (cacheConfig as any)?.l2Size || (cacheConfig as any)?.l2?.maxSizeMB || 2048,\n projectId: 'default',\n },\n },\n this.eventBus\n );\n\n this.clipSessionManager = new ClipSessionManager({\n maxConcurrent: 2,\n factory: {\n createSession: (clipId) => this.createSession(clipId),\n },\n model: () => this.compositionModel,\n cacheManager: this.cacheManager,\n });\n\n this.audioSession = new GlobalAudioSession({\n cacheManager: this.cacheManager,\n workers: this.workers,\n resourceLoader: this.resourceLoader,\n eventBus: this.eventBus,\n getModel: () => this.compositionModel,\n buildWorkerConfigs: () => this.buildWorkerConfigs(),\n });\n\n this.muxManager = new MuxManager(\n this.cacheManager,\n this.audioSession,\n this.config.encode.audio as AudioEncoderConfig\n );\n }\n\n get workerStatus(): WorkerStatus {\n const status = this.workers.status;\n const result: WorkerStatus = {} as WorkerStatus;\n\n const workerTypes: WorkerType[] = [\n 'videoDemux',\n 'audioDemux',\n 'videoDecode',\n 'audioDecode',\n 'videoCompose',\n 'audioCompose',\n 'videoEncode',\n ];\n\n for (const type of workerTypes) {\n result[type] = status[type] || {\n state: 'idle',\n taskCount: 0,\n };\n }\n\n return result;\n }\n\n async initialize(): Promise<void> {\n if (this.isInitialized) return;\n\n await this.cacheManager.init();\n\n this.isInitialized = true;\n }\n\n // Event methods - forward to eventBus\n on<K extends keyof EventPayloadMap>(\n event: K,\n handler: (payload: EventPayloadMap[K]) => void\n ): void {\n this.eventBus.on(event, handler);\n }\n\n off<K extends keyof EventPayloadMap>(\n event: K,\n handler: (payload: EventPayloadMap[K]) => void\n ): void {\n this.eventBus.off(event, handler);\n }\n\n once<K extends keyof EventPayloadMap>(\n event: K,\n handler: (payload: EventPayloadMap[K]) => void\n ): void {\n this.eventBus.once(event, handler);\n }\n\n async setCompositionModel(model: CompositionModel): Promise<void> {\n this.compositionModel = model;\n this.planner.setModel(model);\n this.currentClipId = null;\n\n this.eventBus.emit(MeframeEvent.ModelSet, model);\n\n this.eventBus.emit(MeframeEvent.CompositionUpdated, {\n trackCount: model.tracks.length,\n clipCount: model.tracks.reduce((acc: number, track: any) => acc + track.clips.length, 0),\n durationUs: model.durationUs,\n });\n\n await this.audioSession.activateAllAudioClips();\n\n this.ensureClipCache(0);\n }\n\n async applyPatch(patch: CompositionPatch): Promise<void> {\n if (!this.compositionModel) {\n throw new Error('No composition model set');\n }\n\n // Apply patch and get affected clip IDs (simplified for 2-Clip strategy)\n const affectedClipIds = applyModelPatch(this.compositionModel, patch);\n const clipUpdates = this.planner.applyPatch(patch, affectedClipIds);\n\n this.eventBus.emit(MeframeEvent.PatchApplied, {\n operations: patch.operations.length,\n affectedClips: Array.from(affectedClipIds),\n });\n\n // Get currently active clips in the 2-Clip window\n const activeClipIds = new Set(\n Array.from(this.clipSessionManager['entries'].keys()).filter((clipId) =>\n this.clipSessionManager.isClipActive(clipId)\n )\n );\n\n // Process clip updates\n for (const update of clipUpdates) {\n if (update.type === 'remove') {\n this.activeClips.delete(update.clipId);\n this.cacheManager.evictClip(update.clipId);\n }\n\n // Notify ClipSessionManager to handle the update\n // This will install new instructions or restart pipeline as needed\n await this.clipSessionManager.handlePlannerUpdate(update.clipId, update);\n\n // Only evict cache for active clips (in current 2-Clip window)\n // Non-active clips will be regenerated when they enter the window\n if (activeClipIds.has(update.clipId) && update.type !== 'remove') {\n this.cacheManager.evictClip(update.clipId);\n }\n }\n }\n\n private handleResourceStateChange(resourceId: string, state: Resource['state']): void {\n if (!this.compositionModel) {\n return;\n }\n\n this.compositionModel.updateResourceState(resourceId, state ?? 'pending');\n\n if (state !== 'ready') {\n return;\n }\n\n const resource = this.compositionModel.getResource(resourceId);\n if (!resource) {\n return;\n }\n\n // Main video/audio resources: data will flow naturally into pipeline\n if (resource.type === 'video' || resource.type === 'audio') {\n return;\n }\n\n // Attachment resources (fonts, images): update instructions for active clips\n const clipIds = this.compositionModel.getClipIdsByResourceId(resourceId);\n for (const clipId of clipIds) {\n // Only update active clips (in 2-Clip window)\n if (!this.clipSessionManager.isClipActive(clipId)) {\n continue;\n }\n\n const clip = this.compositionModel.findClip(clipId);\n if (!clip) {\n continue;\n }\n\n // Rebuild instructions with updated resource status\n const instructions = this.planner.getInstructions(clipId);\n if (!instructions) {\n continue;\n }\n\n // Send updated instructions to worker (no pipeline restart needed)\n const session = this.clipSessionManager.getSession(clipId);\n const visualWorker = session?.visualWorkerHandle;\n if (visualWorker) {\n visualWorker.send('install_instructions', instructions);\n }\n }\n }\n\n async restartWorker(type: WorkerType, clipId?: string): Promise<void> {\n const clipLocalTypes: WorkerType[] = [\n 'videoDemux',\n 'audioDemux',\n 'videoDecode',\n 'audioDecode',\n 'videoCompose',\n 'videoEncode',\n ];\n\n if (clipLocalTypes.includes(type) && !clipId) {\n throw new Error(`clipId required for restarting ${type} worker`);\n }\n\n this.workers.terminate(type, clipId);\n const worker = await this.workers.get(type, clipId);\n\n this.eventBus.emit(MeframeEvent.WorkerRestarted, {\n type,\n workerId: worker.getWorkerId(),\n reason: 'Manual restart',\n });\n\n if (clipId) {\n const session = this.clipSessionManager.getSession(clipId);\n if (session) {\n await session.activate();\n }\n }\n }\n\n async renderFrame(timeUs: TimeUs, options?: RenderFrameOptions): Promise<RcFrame | null> {\n const signal = options?.signal;\n\n if (!this.compositionModel) {\n throw new Error('No composition model set');\n }\n\n const clip = this.compositionModel.getClipsAtTime(timeUs, this.compositionModel.mainTrackId)[0];\n if (!clip) {\n return null;\n }\n\n // Detect clip change and proactively ensure cache for prev/current/next\n if (this.currentClipId !== clip.id) {\n this.currentClipId = clip.id;\n void this.ensureClipCache(timeUs);\n }\n\n // Calculate clip-relative time for cache lookup (global time - clip start time)\n const relativeTimeUs = options?.relativeTimeUs ?? timeUs - clip.startUs;\n const cachedFrame = await this.cacheManager.getFrame(relativeTimeUs, clip.id);\n // console.log('>>>>>>>>>>>> renderFrame', timeUs, clip.id, cachedFrame);\n if (cachedFrame) {\n this.eventBus.emit(MeframeEvent.CacheHit, {\n timeUs,\n level: 'L1',\n key: `${clip.id}-${relativeTimeUs}`,\n });\n return cachedFrame;\n }\n // Cache miss - also ensure cache (defensive, already called on clip change)\n void this.ensureClipCache(timeUs);\n\n this.eventBus.emit(MeframeEvent.CacheMiss, {\n timeUs,\n level: 'L1',\n key: `${clip.id}-${relativeTimeUs}`,\n });\n\n if (signal?.aborted) {\n throw new DOMException('Render aborted', 'AbortError');\n }\n\n // Return null immediately instead of waiting\n // This allows PlaybackController to detect miss and trigger buffering\n return null;\n }\n\n /**\n * Ensure clips are cached using 2-Clip strategy\n * Called by PlaybackController\n */\n async ensureClipCache(timeUs: TimeUs): Promise<void> {\n if (!this.compositionModel) {\n return;\n }\n\n await this.clipSessionManager.ensureClips(timeUs);\n }\n\n /**\n * Wait for clip cache to be ready for playback\n * Returns true if minimum cache is ready, false if timeout\n */\n async waitForClipReady(\n timeUs: TimeUs,\n options?: { minFrameCount?: number; timeoutMs?: number }\n ): Promise<boolean> {\n if (!this.compositionModel) {\n return false;\n }\n\n const clips = this.compositionModel.getClipsAtTime(timeUs, this.compositionModel.mainTrackId);\n if (clips.length === 0) {\n return true;\n }\n\n const currentClip = clips[0];\n if (!currentClip) {\n return true;\n }\n\n return this.cacheManager.waitForClipReady(currentClip.id, {\n minFrameCount: options?.minFrameCount ?? 30,\n timeoutMs: options?.timeoutMs ?? 5_000,\n });\n }\n\n /**\n * Render a clip completely for L2 cache (bypass ClipSessionManager)\n * Returns a promise that resolves when encoding is complete\n */\n async renderClipForL2(clipId: string): Promise<boolean> {\n const sessionId = `${clipId}#l2`;\n let session: VideoClipSession | null = null;\n\n return new Promise<boolean>((resolve, reject) => {\n this.createSession(sessionId, {\n forL2Only: true,\n clipId: clipId,\n onL2Complete: () => {\n resolve(true);\n },\n onL2Error: (error) => {\n console.error('[Orchestrator] L2 rendering failed for', clipId, error);\n reject(error);\n },\n })\n .then((s) => {\n session = s;\n return session.activate();\n })\n .catch(async (error) => {\n // Clean up partial session on any error to avoid worker state pollution\n if (session) {\n await session.dispose();\n }\n\n if (error instanceof ResourceConflictError) {\n resolve(false);\n } else {\n reject(error);\n }\n });\n });\n }\n\n /**\n * Create a new session for a clip\n */\n private async createSession(\n sessionId: string,\n options?: {\n forL2Only?: boolean;\n clipId?: string;\n onL2Complete?: () => void;\n onL2Error?: (error: Error) => void;\n }\n ): Promise<VideoClipSession> {\n const clipId = options?.clipId ?? sessionId;\n const clip = this.compositionModel?.findClip(clipId);\n if (!clip) {\n throw new Error(`Clip ${clipId} not found`);\n }\n\n const session = await VideoClipSession.create({\n clipId,\n sessionId,\n forL2Only: options?.forL2Only ?? false,\n planner: this.planner,\n workerPool: this.workers,\n cacheManager: this.cacheManager,\n compositionModel: this.compositionModel!,\n workerConfigs: this.buildWorkerConfigs(),\n callbacks: {\n onComposedStreamReady: (stream, fps) => {\n if (options?.forL2Only) {\n // L2 channel: don't need L1, cancel stream\n stream.cancel();\n } else {\n // Preview channel: fill L1\n this.cacheManager.receiveComposedFrames(stream, {\n clipId: clipId,\n trackId: this.compositionModel!.mainTrackId,\n fps,\n clipStartUs: clip.startUs,\n onFrame: () => {},\n });\n }\n },\n onEncodedStreamReady: (stream, track) => {\n if (options?.forL2Only) {\n // L2 channel: write to L2 using clipId, notify on complete\n this.cacheManager.receiveEncodedChunks(stream, clipId, track, {\n onComplete: () => {\n session.dispose();\n // Only notify completion for video track (audio is optional)\n if (track === 'video' && options.onL2Complete) {\n options.onL2Complete();\n }\n },\n onError: (error) => {\n console.error(\n `[Orchestrator] L2 encode stream error for ${clipId} ${track}:`,\n error\n );\n session.dispose();\n if (options.onL2Error) {\n options.onL2Error(error);\n }\n },\n });\n } else {\n // Preview channel: don't write to L2, cancel stream\n stream.cancel();\n }\n },\n onPipelineReady: async () => {\n const clip = this.compositionModel?.findClip(clipId);\n if (clip?.resourceId) {\n await this.resourceLoader.fetch(clip.resourceId, {\n priority: options?.forL2Only ? 'low' : 'high',\n sessionId: sessionId,\n trackId: clip.trackId,\n });\n }\n },\n },\n });\n\n this.activeClips.add(sessionId);\n return session;\n }\n\n async dispose(): Promise<void> {\n this.resourceLoader.dispose();\n await this.clipSessionManager.dispose();\n await this.cacheManager.clear();\n\n this.currentClipId = null;\n this.activeClips.clear();\n\n this.workers.terminateAll();\n this.compositionModel = null;\n this.eventBus.dispose();\n }\n\n private buildWorkerConfigs(): Record<WorkerType, any> {\n const config = this.config as any;\n const defaultCanvasWidth = config.global?.defaultCanvasWidth ?? 720;\n const defaultCanvasHeight = config.global?.defaultCanvasHeight ?? 1280;\n const defaultFps = config.global?.defaultFps ?? 30;\n return {\n videoDemux: {\n highWaterMark: config.demux?.backpressure?.highWaterMark ?? 10,\n },\n audioDemux: {\n highWaterMark: config.demux?.backpressure?.highWaterMark ?? 10,\n },\n videoDecode: config.decode?.video,\n audioDecode: config.decode?.audio,\n videoCompose: {\n width: config.compose?.canvas?.width ?? defaultCanvasWidth,\n height: config.compose?.canvas?.height ?? defaultCanvasHeight,\n fps: config.global?.defaultFps ?? defaultFps,\n backgroundColor: config.compose?.canvas?.backgroundColor ?? '#000000',\n enableSmoothing: config.compose?.visual?.enableSmoothing ?? true,\n enableHardwareAcceleration: config.compose?.visual?.enableHardwareAcceleration ?? true,\n },\n audioCompose: {\n ducking: config.compose?.audio?.ducking,\n mixing: config.compose?.audio?.mixing,\n },\n videoEncode: {\n codec: 'avc1.42002A',\n width: config.compose?.canvas?.width || defaultCanvasWidth,\n height: config.compose?.canvas?.height || defaultCanvasHeight,\n bitrate: config.encode?.video?.bitrateKbps\n ? config.encode.video.bitrateKbps * 1000\n : 12_000_000,\n framerate: config.encode?.video?.framerate || defaultFps,\n latencyMode: 'quality',\n bitrateMode: 'variable',\n hardwareAcceleration: 'no-preference',\n ...(config.encode?.video as any),\n },\n };\n }\n\n async export(model: CompositionModel, options: ExportOptions): Promise<Blob> {\n return this.muxManager.export(model, options);\n }\n}\n"],"names":["applyModelPatch","clip"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/orchestrator/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,MAAM,EACN,OAAO,EACR,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QACZ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC;IACF,QAAQ,CAAC,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,IAAI,GAAG,KAAK,GAAG,MAAM,CAAC,CAAC;IACxE,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACnD,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IAEpC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnF,UAAU,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACnF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,IAAI,IAAI,CAAC;IACd,MAAM,IAAI,IAAI,CAAC;IACf,IAAI,IAAI,IAAI,CAAC;IACb,gBAAgB,IAAI,MAAM,CAAC;IAC3B,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3C,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;CAC7C;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/orchestrator/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,MAAM,EACN,OAAO,EACR,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QACZ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC;IACF,QAAQ,CAAC,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,IAAI,GAAG,KAAK,GAAG,MAAM,CAAC,CAAC;IACxE,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACnD,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IAEpC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnF,UAAU,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACnF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,IAAI,IAAI,CAAC;IACd,MAAM,IAAI,IAAI,CAAC;IACf,IAAI,IAAI,IAAI,CAAC;IACb,gBAAgB,IAAI,MAAM,CAAC;IAC3B,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3C,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;CAC7C;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MP4Muxer.js","sources":["../../../src/stages/mux/MP4Muxer.ts"],"sourcesContent":["import { Muxer, ArrayBufferTarget } from 'mp4-muxer';\n\n/**\n * MP4Muxer - MP4 container multiplexer using mp4-muxer library\n * Supports video and audio track export\n */\nexport class MP4Muxer {\n private muxer: Muxer<ArrayBufferTarget>;\n private firstVideoChunk = true;\n private firstAudioChunk = true;\n private videoChunkMeta: any = null;\n private audioChunkMeta: any = null;\n\n constructor(config: {\n width: number;\n height: number;\n fps: number;\n fastStart?: false | 'in-memory' | 'fragmented';\n videoChunkMeta?: any;\n audioChunkMeta?: any;\n }) {\n this.videoChunkMeta = config.videoChunkMeta;\n this.audioChunkMeta = config.audioChunkMeta;\n\n const muxerConfig: any = {\n target: new ArrayBufferTarget(),\n video: {\n codec: 'avc',\n width: config.width,\n height: config.height,\n frameRate: config.fps,\n },\n fastStart: config.fastStart ?? 'in-memory',\n firstTimestampBehavior: '
|
|
1
|
+
{"version":3,"file":"MP4Muxer.js","sources":["../../../src/stages/mux/MP4Muxer.ts"],"sourcesContent":["import { Muxer, ArrayBufferTarget } from 'mp4-muxer';\n\n/**\n * MP4Muxer - MP4 container multiplexer using mp4-muxer library\n * Supports video and audio track export\n */\nexport class MP4Muxer {\n private muxer: Muxer<ArrayBufferTarget>;\n private firstVideoChunk = true;\n private firstAudioChunk = true;\n private videoChunkMeta: any = null;\n private audioChunkMeta: any = null;\n\n constructor(config: {\n width: number;\n height: number;\n fps: number;\n fastStart?: false | 'in-memory' | 'fragmented';\n videoChunkMeta?: any;\n audioChunkMeta?: any;\n }) {\n this.videoChunkMeta = config.videoChunkMeta;\n this.audioChunkMeta = config.audioChunkMeta;\n\n const muxerConfig: any = {\n target: new ArrayBufferTarget(),\n video: {\n codec: 'avc',\n width: config.width,\n height: config.height,\n frameRate: config.fps,\n },\n fastStart: config.fastStart ?? 'in-memory',\n firstTimestampBehavior: 'offset',\n };\n\n // Add audio configuration if provided\n if (this.audioChunkMeta) {\n muxerConfig.audio = {\n codec: 'aac',\n sampleRate: this.audioChunkMeta.sampleRate || 48000,\n numberOfChannels: this.audioChunkMeta.numberOfChannels || 2,\n };\n }\n\n this.muxer = new Muxer(muxerConfig);\n }\n\n private videoChunkCount = 0;\n\n writeVideoChunk(chunk: EncodedVideoChunk): void {\n let meta: EncodedVideoChunkMetadata | undefined;\n if (this.firstVideoChunk && chunk.type === 'key' && this.videoChunkMeta) {\n meta = { decoderConfig: this.videoChunkMeta };\n this.firstVideoChunk = false;\n }\n\n this.videoChunkCount++;\n this.muxer.addVideoChunk(chunk, meta);\n }\n\n private audioChunkCount = 0;\n\n writeAudioChunk(chunk: EncodedAudioChunk): void {\n let meta: EncodedAudioChunkMetadata | undefined;\n if (this.firstAudioChunk && this.audioChunkMeta) {\n meta = { decoderConfig: this.audioChunkMeta };\n this.firstAudioChunk = false;\n }\n\n this.audioChunkCount++;\n this.muxer.addAudioChunk(chunk, meta);\n }\n\n finalize(): Blob {\n this.muxer.finalize();\n const buffer = (this.muxer.target as ArrayBufferTarget).buffer;\n return new Blob([buffer], { type: 'video/mp4' });\n }\n}\n"],"names":[],"mappings":";AAMO,MAAM,SAAS;AAAA,EACZ;AAAA,EACA,kBAAkB;AAAA,EAClB,kBAAkB;AAAA,EAClB,iBAAsB;AAAA,EACtB,iBAAsB;AAAA,EAE9B,YAAY,QAOT;AACD,SAAK,iBAAiB,OAAO;AAC7B,SAAK,iBAAiB,OAAO;AAE7B,UAAM,cAAmB;AAAA,MACvB,QAAQ,IAAI,kBAAA;AAAA,MACZ,OAAO;AAAA,QACL,OAAO;AAAA,QACP,OAAO,OAAO;AAAA,QACd,QAAQ,OAAO;AAAA,QACf,WAAW,OAAO;AAAA,MAAA;AAAA,MAEpB,WAAW,OAAO,aAAa;AAAA,MAC/B,wBAAwB;AAAA,IAAA;AAI1B,QAAI,KAAK,gBAAgB;AACvB,kBAAY,QAAQ;AAAA,QAClB,OAAO;AAAA,QACP,YAAY,KAAK,eAAe,cAAc;AAAA,QAC9C,kBAAkB,KAAK,eAAe,oBAAoB;AAAA,MAAA;AAAA,IAE9D;AAEA,SAAK,QAAQ,IAAI,MAAM,WAAW;AAAA,EACpC;AAAA,EAEQ,kBAAkB;AAAA,EAE1B,gBAAgB,OAAgC;AAC9C,QAAI;AACJ,QAAI,KAAK,mBAAmB,MAAM,SAAS,SAAS,KAAK,gBAAgB;AACvE,aAAO,EAAE,eAAe,KAAK,eAAA;AAC7B,WAAK,kBAAkB;AAAA,IACzB;AAEA,SAAK;AACL,SAAK,MAAM,cAAc,OAAO,IAAI;AAAA,EACtC;AAAA,EAEQ,kBAAkB;AAAA,EAE1B,gBAAgB,OAAgC;AAC9C,QAAI;AACJ,QAAI,KAAK,mBAAmB,KAAK,gBAAgB;AAC/C,aAAO,EAAE,eAAe,KAAK,eAAA;AAC7B,WAAK,kBAAkB;AAAA,IACzB;AAEA,SAAK;AACL,SAAK,MAAM,cAAc,OAAO,IAAI;AAAA,EACtC;AAAA,EAEA,WAAiB;AACf,SAAK,MAAM,SAAA;AACX,UAAM,SAAU,KAAK,MAAM,OAA6B;AACxD,WAAO,IAAI,KAAK,CAAC,MAAM,GAAG,EAAE,MAAM,aAAa;AAAA,EACjD;AACF;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MuxManager.d.ts","sourceRoot":"","sources":["../../../src/stages/mux/MuxManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAEnE,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpD;;;GAGG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,kBAAkB,CAAkD;IAC5E,OAAO,CAAC,eAAe,CAAkC;gBAGvD,YAAY,EAAE,YAAY,EAC1B,YAAY,EAAE,kBAAkB,EAChC,kBAAkB,EAAE,kBAAkB;IAOlC,MAAM,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IA+C5E;;;OAGG;YACW,iBAAiB;YAoCjB,gBAAgB;
|
|
1
|
+
{"version":3,"file":"MuxManager.d.ts","sourceRoot":"","sources":["../../../src/stages/mux/MuxManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAEnE,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpD;;;GAGG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,kBAAkB,CAAkD;IAC5E,OAAO,CAAC,eAAe,CAAkC;gBAGvD,YAAY,EAAE,YAAY,EAC1B,YAAY,EAAE,kBAAkB,EAChC,kBAAkB,EAAE,kBAAkB;IAOlC,MAAM,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IA+C5E;;;OAGG;YACW,iBAAiB;YAoCjB,gBAAgB;YAkDhB,gBAAgB;YAwDhB,eAAe;CAkB9B"}
|
|
@@ -78,6 +78,7 @@ class MuxManager {
|
|
|
78
78
|
}
|
|
79
79
|
async writeVideoChunks(muxer, sortedClips) {
|
|
80
80
|
let firstKeyframeWritten = false;
|
|
81
|
+
let exportTimeUs = 0;
|
|
81
82
|
for (const clip of sortedClips) {
|
|
82
83
|
const stream = await this.cacheManager.createL2ReadStream(clip.id, "video");
|
|
83
84
|
if (!stream) {
|
|
@@ -89,15 +90,25 @@ class MuxManager {
|
|
|
89
90
|
while (true) {
|
|
90
91
|
const { done, value } = await reader.read();
|
|
91
92
|
if (done) break;
|
|
92
|
-
const
|
|
93
|
+
const originalChunk = value;
|
|
94
|
+
const buffer = new ArrayBuffer(originalChunk.byteLength);
|
|
95
|
+
originalChunk.copyTo(buffer);
|
|
96
|
+
const remappedChunk = new EncodedVideoChunk({
|
|
97
|
+
type: originalChunk.type,
|
|
98
|
+
timestamp: exportTimeUs,
|
|
99
|
+
duration: originalChunk.duration ?? void 0,
|
|
100
|
+
data: buffer
|
|
101
|
+
});
|
|
93
102
|
if (!firstKeyframeWritten) {
|
|
94
|
-
if (
|
|
103
|
+
if (remappedChunk.type !== "key") {
|
|
95
104
|
console.warn(`[MuxManager] Skipping non-keyframe at start of clip ${clip.id}`);
|
|
105
|
+
exportTimeUs += originalChunk.duration || 0;
|
|
96
106
|
continue;
|
|
97
107
|
}
|
|
98
108
|
firstKeyframeWritten = true;
|
|
99
109
|
}
|
|
100
|
-
muxer.writeVideoChunk(
|
|
110
|
+
muxer.writeVideoChunk(remappedChunk);
|
|
111
|
+
exportTimeUs += originalChunk.duration || 0;
|
|
101
112
|
}
|
|
102
113
|
} finally {
|
|
103
114
|
reader.releaseLock();
|
|
@@ -109,15 +120,35 @@ class MuxManager {
|
|
|
109
120
|
console.warn("[MuxManager] No audio stream available");
|
|
110
121
|
return;
|
|
111
122
|
}
|
|
123
|
+
let exportTimeUs = 0;
|
|
112
124
|
if (this.audioFirstChunk) {
|
|
113
|
-
|
|
125
|
+
const buffer = new ArrayBuffer(this.audioFirstChunk.byteLength);
|
|
126
|
+
this.audioFirstChunk.copyTo(buffer);
|
|
127
|
+
const remappedChunk = new EncodedAudioChunk({
|
|
128
|
+
type: this.audioFirstChunk.type,
|
|
129
|
+
timestamp: exportTimeUs,
|
|
130
|
+
duration: this.audioFirstChunk.duration ?? void 0,
|
|
131
|
+
data: buffer
|
|
132
|
+
});
|
|
133
|
+
muxer.writeAudioChunk(remappedChunk);
|
|
134
|
+
exportTimeUs += this.audioFirstChunk.duration || 0;
|
|
114
135
|
}
|
|
115
136
|
const reader = this.audioEncodedStream.getReader();
|
|
116
137
|
try {
|
|
117
138
|
while (true) {
|
|
118
139
|
const { done, value } = await reader.read();
|
|
119
140
|
if (done) break;
|
|
120
|
-
|
|
141
|
+
const originalChunk = value;
|
|
142
|
+
const buffer = new ArrayBuffer(originalChunk.byteLength);
|
|
143
|
+
originalChunk.copyTo(buffer);
|
|
144
|
+
const remappedChunk = new EncodedAudioChunk({
|
|
145
|
+
type: originalChunk.type,
|
|
146
|
+
timestamp: exportTimeUs,
|
|
147
|
+
duration: originalChunk.duration ?? void 0,
|
|
148
|
+
data: buffer
|
|
149
|
+
});
|
|
150
|
+
muxer.writeAudioChunk(remappedChunk);
|
|
151
|
+
exportTimeUs += originalChunk.duration || 0;
|
|
121
152
|
}
|
|
122
153
|
} finally {
|
|
123
154
|
reader.releaseLock();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MuxManager.js","sources":["../../../src/stages/mux/MuxManager.ts"],"sourcesContent":["import type { CompositionModel } from '../../model/CompositionModel';\nimport type { ExportOptions } from '../../types';\nimport { GlobalAudioSession } from '../compose/GlobalAudioSession';\nimport { MP4Muxer } from './MP4Muxer';\nimport { CacheManager } from '@/cache/CacheManager';\n\n/**\n * MuxManager: Main thread muxing service\n * Reads encoded chunks from L2 cache and muxes into final video file\n */\nexport class MuxManager {\n private cacheManager: CacheManager;\n private audioSession: GlobalAudioSession;\n private audioEncoderConfig: AudioEncoderConfig;\n private audioEncodedStream: ReadableStream<EncodedAudioChunk> | null = null;\n private audioFirstChunk: EncodedAudioChunk | null = null;\n\n constructor(\n cacheManager: CacheManager,\n audioSession: GlobalAudioSession,\n audioEncoderConfig: AudioEncoderConfig\n ) {\n this.cacheManager = cacheManager;\n this.audioSession = audioSession;\n this.audioEncoderConfig = audioEncoderConfig;\n }\n\n async export(model: CompositionModel, options: ExportOptions): Promise<Blob> {\n const videoTrack = model.tracks.find((t) => t.kind === 'video');\n if (!videoTrack || videoTrack.clips.length === 0) {\n throw new Error('No video clips in composition');\n }\n\n const sortedClips = [...videoTrack.clips].sort((a, b) => a.startUs - b.startUs);\n await this.checkL2Coverage(sortedClips);\n\n const width = options.width || (model as any).renderConfig?.width || 720;\n const height = options.height || (model as any).renderConfig?.height || 1280;\n const fps = options.fps || model.fps || 30;\n\n // Get video metadata from L2 cache (first clip)\n const videoChunkMeta = sortedClips[0]\n ? await this.cacheManager.getL2Metadata(sortedClips[0].id, 'video')\n : null;\n\n if (!videoChunkMeta) {\n console.warn('[MuxManager] No videoChunkMeta available, export may fail');\n }\n\n const audioTrack = model.tracks.find((t) => t.kind === 'audio');\n\n // Get audio metadata from first chunk if audio exists\n let audioChunkMeta: any = undefined;\n if (audioTrack?.clips.length) {\n audioChunkMeta = await this.getAudioChunkMeta();\n }\n\n const muxer = new MP4Muxer({\n width,\n height,\n fps,\n fastStart: 'in-memory',\n videoChunkMeta,\n audioChunkMeta,\n });\n\n await this.writeVideoChunks(muxer, sortedClips);\n if (audioTrack?.clips.length) {\n await this.writeAudioChunks(muxer);\n }\n\n return muxer.finalize();\n }\n\n /**\n * Get audio chunk metadata by reading first chunk from encoder\n * Caches the stream and first chunk for later use\n */\n private async getAudioChunkMeta(): Promise<any> {\n const audioEncoderConfig = this.audioEncoderConfig;\n let metadata: any = null;\n\n this.audioEncodedStream = await this.audioSession.createExportEncodedStream(\n audioEncoderConfig,\n (meta) => {\n // Extract decoderConfig from first chunk metadata\n if (meta.decoderConfig) {\n metadata = meta.decoderConfig;\n }\n }\n );\n\n if (!this.audioEncodedStream) {\n return null;\n }\n\n // Read first chunk to trigger metadata extraction and cache it\n const reader = this.audioEncodedStream.getReader();\n try {\n const { done, value } = await reader.read();\n if (!done && value) {\n this.audioFirstChunk = value;\n }\n reader.releaseLock();\n } catch (error) {\n console.error('[MuxManager] Failed to read first audio chunk:', error);\n reader.releaseLock();\n this.audioEncodedStream = null;\n return null;\n }\n\n return metadata;\n }\n\n private async writeVideoChunks(muxer: MP4Muxer, sortedClips: any[]): Promise<void> {\n let firstKeyframeWritten = false;\n\n for (const clip of sortedClips) {\n const stream = await this.cacheManager.createL2ReadStream(clip.id, 'video');\n if (!stream) {\n console.warn(`[MuxManager] No video stream for clip ${clip.id}`);\n continue;\n }\n\n const reader = stream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n const chunk = value as EncodedVideoChunk;\n\n // Ensure first chunk in export is a keyframe\n if (!firstKeyframeWritten) {\n if (chunk.type !== 'key') {\n console.warn(`[MuxManager] Skipping non-keyframe at start of clip ${clip.id}`);\n continue;\n }\n firstKeyframeWritten = true;\n }\n\n muxer.writeVideoChunk(chunk);\n }\n } finally {\n reader.releaseLock();\n }\n }\n }\n\n private async writeAudioChunks(muxer: MP4Muxer): Promise<void> {\n // Use cached stream from getAudioChunkMeta()\n if (!this.audioEncodedStream) {\n console.warn('[MuxManager] No audio stream available');\n return;\n }\n\n // Write the cached first chunk\n if (this.audioFirstChunk) {\n muxer.writeAudioChunk(this.audioFirstChunk);\n }\n\n // Continue reading remaining chunks\n const reader = this.audioEncodedStream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n muxer.writeAudioChunk(value);\n }\n } finally {\n reader.releaseLock();\n // Clean up cached state\n this.audioEncodedStream = null;\n this.audioFirstChunk = null;\n }\n }\n\n private async checkL2Coverage(clips: any[]): Promise<void> {\n const missingClips: string[] = [];\n for (const clip of clips) {\n const inL2 = await this.cacheManager.hasClipInL2(clip.id, 'video');\n if (!inL2) {\n missingClips.push(clip.id);\n }\n }\n\n if (missingClips.length > 0) {\n const clipList = missingClips.slice(0, 3).join(', ');\n const moreText = missingClips.length > 3 ? ` and ${missingClips.length - 3} more` : '';\n throw new Error(\n `Export failed: ${missingClips.length} clip(s) not cached (${clipList}${moreText}). ` +\n `Please start PreRenderService and wait for background caching to complete.`\n );\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MuxManager.js","sources":["../../../src/stages/mux/MuxManager.ts"],"sourcesContent":["import type { CompositionModel } from '../../model/CompositionModel';\nimport type { ExportOptions } from '../../types';\nimport { GlobalAudioSession } from '../compose/GlobalAudioSession';\nimport { MP4Muxer } from './MP4Muxer';\nimport { CacheManager } from '@/cache/CacheManager';\n\n/**\n * MuxManager: Main thread muxing service\n * Reads encoded chunks from L2 cache and muxes into final video file\n */\nexport class MuxManager {\n private cacheManager: CacheManager;\n private audioSession: GlobalAudioSession;\n private audioEncoderConfig: AudioEncoderConfig;\n private audioEncodedStream: ReadableStream<EncodedAudioChunk> | null = null;\n private audioFirstChunk: EncodedAudioChunk | null = null;\n\n constructor(\n cacheManager: CacheManager,\n audioSession: GlobalAudioSession,\n audioEncoderConfig: AudioEncoderConfig\n ) {\n this.cacheManager = cacheManager;\n this.audioSession = audioSession;\n this.audioEncoderConfig = audioEncoderConfig;\n }\n\n async export(model: CompositionModel, options: ExportOptions): Promise<Blob> {\n const videoTrack = model.tracks.find((t) => t.kind === 'video');\n if (!videoTrack || videoTrack.clips.length === 0) {\n throw new Error('No video clips in composition');\n }\n\n const sortedClips = [...videoTrack.clips].sort((a, b) => a.startUs - b.startUs);\n await this.checkL2Coverage(sortedClips);\n\n const width = options.width || (model as any).renderConfig?.width || 720;\n const height = options.height || (model as any).renderConfig?.height || 1280;\n const fps = options.fps || model.fps || 30;\n\n // Get video metadata from L2 cache (first clip)\n const videoChunkMeta = sortedClips[0]\n ? await this.cacheManager.getL2Metadata(sortedClips[0].id, 'video')\n : null;\n\n if (!videoChunkMeta) {\n console.warn('[MuxManager] No videoChunkMeta available, export may fail');\n }\n\n const audioTrack = model.tracks.find((t) => t.kind === 'audio');\n\n // Get audio metadata from first chunk if audio exists\n let audioChunkMeta: any = undefined;\n if (audioTrack?.clips.length) {\n audioChunkMeta = await this.getAudioChunkMeta();\n }\n\n const muxer = new MP4Muxer({\n width,\n height,\n fps,\n fastStart: 'in-memory',\n videoChunkMeta,\n audioChunkMeta,\n });\n\n await this.writeVideoChunks(muxer, sortedClips);\n if (audioTrack?.clips.length) {\n await this.writeAudioChunks(muxer);\n }\n\n return muxer.finalize();\n }\n\n /**\n * Get audio chunk metadata by reading first chunk from encoder\n * Caches the stream and first chunk for later use\n */\n private async getAudioChunkMeta(): Promise<any> {\n const audioEncoderConfig = this.audioEncoderConfig;\n let metadata: any = null;\n\n this.audioEncodedStream = await this.audioSession.createExportEncodedStream(\n audioEncoderConfig,\n (meta) => {\n // Extract decoderConfig from first chunk metadata\n if (meta.decoderConfig) {\n metadata = meta.decoderConfig;\n }\n }\n );\n\n if (!this.audioEncodedStream) {\n return null;\n }\n\n // Read first chunk to trigger metadata extraction and cache it\n const reader = this.audioEncodedStream.getReader();\n try {\n const { done, value } = await reader.read();\n if (!done && value) {\n this.audioFirstChunk = value;\n }\n reader.releaseLock();\n } catch (error) {\n console.error('[MuxManager] Failed to read first audio chunk:', error);\n reader.releaseLock();\n this.audioEncodedStream = null;\n return null;\n }\n\n return metadata;\n }\n\n private async writeVideoChunks(muxer: MP4Muxer, sortedClips: any[]): Promise<void> {\n let firstKeyframeWritten = false;\n let exportTimeUs = 0;\n\n for (const clip of sortedClips) {\n const stream = await this.cacheManager.createL2ReadStream(clip.id, 'video');\n if (!stream) {\n console.warn(`[MuxManager] No video stream for clip ${clip.id}`);\n continue;\n }\n\n const reader = stream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n const originalChunk = value as EncodedVideoChunk;\n\n // Copy chunk data\n const buffer = new ArrayBuffer(originalChunk.byteLength);\n originalChunk.copyTo(buffer);\n\n // Rewrite timestamp to export timeline (tight concatenation)\n const remappedChunk = new EncodedVideoChunk({\n type: originalChunk.type,\n timestamp: exportTimeUs,\n duration: originalChunk.duration ?? undefined,\n data: buffer,\n });\n\n // Ensure first chunk in export is a keyframe\n if (!firstKeyframeWritten) {\n if (remappedChunk.type !== 'key') {\n console.warn(`[MuxManager] Skipping non-keyframe at start of clip ${clip.id}`);\n exportTimeUs += originalChunk.duration || 0;\n continue;\n }\n firstKeyframeWritten = true;\n }\n\n muxer.writeVideoChunk(remappedChunk);\n exportTimeUs += originalChunk.duration || 0;\n }\n } finally {\n reader.releaseLock();\n }\n }\n }\n\n private async writeAudioChunks(muxer: MP4Muxer): Promise<void> {\n // Use cached stream from getAudioChunkMeta()\n if (!this.audioEncodedStream) {\n console.warn('[MuxManager] No audio stream available');\n return;\n }\n\n let exportTimeUs = 0;\n\n // Write the cached first chunk with remapped timestamp\n if (this.audioFirstChunk) {\n const buffer = new ArrayBuffer(this.audioFirstChunk.byteLength);\n this.audioFirstChunk.copyTo(buffer);\n\n const remappedChunk = new EncodedAudioChunk({\n type: this.audioFirstChunk.type,\n timestamp: exportTimeUs,\n duration: this.audioFirstChunk.duration ?? undefined,\n data: buffer,\n });\n muxer.writeAudioChunk(remappedChunk);\n exportTimeUs += this.audioFirstChunk.duration || 0;\n }\n\n // Continue reading remaining chunks\n const reader = this.audioEncodedStream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n const originalChunk = value as EncodedAudioChunk;\n\n // Copy chunk data\n const buffer = new ArrayBuffer(originalChunk.byteLength);\n originalChunk.copyTo(buffer);\n\n // Rewrite timestamp to export timeline\n const remappedChunk = new EncodedAudioChunk({\n type: originalChunk.type,\n timestamp: exportTimeUs,\n duration: originalChunk.duration ?? undefined,\n data: buffer,\n });\n\n muxer.writeAudioChunk(remappedChunk);\n exportTimeUs += originalChunk.duration || 0;\n }\n } finally {\n reader.releaseLock();\n // Clean up cached state\n this.audioEncodedStream = null;\n this.audioFirstChunk = null;\n }\n }\n\n private async checkL2Coverage(clips: any[]): Promise<void> {\n const missingClips: string[] = [];\n for (const clip of clips) {\n const inL2 = await this.cacheManager.hasClipInL2(clip.id, 'video');\n if (!inL2) {\n missingClips.push(clip.id);\n }\n }\n\n if (missingClips.length > 0) {\n const clipList = missingClips.slice(0, 3).join(', ');\n const moreText = missingClips.length > 3 ? ` and ${missingClips.length - 3} more` : '';\n throw new Error(\n `Export failed: ${missingClips.length} clip(s) not cached (${clipList}${moreText}). ` +\n `Please start PreRenderService and wait for background caching to complete.`\n );\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -12,30 +12,6 @@ function frameDurationFromFps(fps) {
|
|
|
12
12
|
const duration = MICROSECONDS_PER_SECOND / normalized;
|
|
13
13
|
return Math.max(Math.round(duration), 1);
|
|
14
14
|
}
|
|
15
|
-
function frameIndexFromTimestamp(baseTimestampUs, timestampUs, fps, strategy = "nearest") {
|
|
16
|
-
const frameDurationUs = frameDurationFromFps(fps);
|
|
17
|
-
if (frameDurationUs <= 0) {
|
|
18
|
-
return 0;
|
|
19
|
-
}
|
|
20
|
-
const delta = timestampUs - baseTimestampUs;
|
|
21
|
-
const rawIndex = delta / frameDurationUs;
|
|
22
|
-
if (!Number.isFinite(rawIndex)) {
|
|
23
|
-
return 0;
|
|
24
|
-
}
|
|
25
|
-
switch (strategy) {
|
|
26
|
-
case "floor":
|
|
27
|
-
return Math.floor(rawIndex);
|
|
28
|
-
case "ceil":
|
|
29
|
-
return Math.ceil(rawIndex);
|
|
30
|
-
default:
|
|
31
|
-
return Math.round(rawIndex);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function quantizeTimestampToFrame(timestampUs, baseTimestampUs, fps, strategy = "nearest") {
|
|
35
|
-
const frameDurationUs = frameDurationFromFps(fps);
|
|
36
|
-
const index = frameIndexFromTimestamp(baseTimestampUs, timestampUs, fps, strategy);
|
|
37
|
-
return baseTimestampUs + index * frameDurationUs;
|
|
38
|
-
}
|
|
39
15
|
class LayerRenderer {
|
|
40
16
|
ctx;
|
|
41
17
|
width;
|
|
@@ -1164,21 +1140,18 @@ class VideoComposeWorker {
|
|
|
1164
1140
|
}
|
|
1165
1141
|
}
|
|
1166
1142
|
buildComposeRequest(instruction, frame) {
|
|
1167
|
-
const
|
|
1168
|
-
const clipStartUs = instruction.baseConfig.timeline?.clipStartUs ?? 0;
|
|
1143
|
+
const clipRelativeTime = this.computeTimelineTimestamp(frame, instruction.baseConfig);
|
|
1169
1144
|
const clipDurationUs = instruction.baseConfig.timeline?.clipDurationUs ?? Infinity;
|
|
1170
|
-
|
|
1171
|
-
if (normalizedTime < clipStartUs || normalizedTime >= clipEndUs) {
|
|
1145
|
+
if (clipRelativeTime < 0 || clipRelativeTime >= clipDurationUs) {
|
|
1172
1146
|
return null;
|
|
1173
1147
|
}
|
|
1174
|
-
const clipRelativeTime = normalizedTime - clipStartUs;
|
|
1175
1148
|
const activeLayers = resolveActiveLayers(instruction.layers, clipRelativeTime);
|
|
1176
1149
|
if (!activeLayers.length) {
|
|
1177
1150
|
return null;
|
|
1178
1151
|
}
|
|
1179
1152
|
const layers = activeLayers.map((layer) => materializeLayer(layer, frame));
|
|
1180
1153
|
return {
|
|
1181
|
-
timeUs:
|
|
1154
|
+
timeUs: clipRelativeTime,
|
|
1182
1155
|
layers,
|
|
1183
1156
|
transition: VideoComposeWorker.buildTransition(
|
|
1184
1157
|
instruction.transitions,
|
|
@@ -1208,7 +1181,6 @@ class VideoComposeWorker {
|
|
|
1208
1181
|
computeTimelineTimestamp(frame, config) {
|
|
1209
1182
|
if (!this.streamState) {
|
|
1210
1183
|
this.streamState = {
|
|
1211
|
-
baseTimestamp: null,
|
|
1212
1184
|
lastSourceTimestamp: null,
|
|
1213
1185
|
nextFrameIndex: 0
|
|
1214
1186
|
};
|
|
@@ -1219,42 +1191,21 @@ class VideoComposeWorker {
|
|
|
1219
1191
|
this.streamState.lastSourceTimestamp = frame.timestamp ?? null;
|
|
1220
1192
|
return ts;
|
|
1221
1193
|
}
|
|
1222
|
-
const {
|
|
1194
|
+
const { compositionFps } = timeline;
|
|
1223
1195
|
const sourceTimestamp = frame.timestamp ?? null;
|
|
1224
1196
|
if (sourceTimestamp !== null && this.streamState.lastSourceTimestamp !== null && sourceTimestamp < this.streamState.lastSourceTimestamp) {
|
|
1225
|
-
this.streamState.baseTimestamp = null;
|
|
1226
1197
|
this.streamState.nextFrameIndex = 0;
|
|
1227
1198
|
}
|
|
1228
|
-
if (this.streamState.baseTimestamp === null) {
|
|
1229
|
-
this.streamState.baseTimestamp = sourceTimestamp ?? 0;
|
|
1230
|
-
this.streamState.nextFrameIndex = 0;
|
|
1231
|
-
if (this.streamState.baseTimestamp > 1e3) {
|
|
1232
|
-
console.warn(
|
|
1233
|
-
`[VideoComposeWorker] First frame timestamp is ${this.streamState.baseTimestamp}us, expected ~0. Check MP4Demuxer normalization.`
|
|
1234
|
-
);
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
1199
|
const frameDuration = frameDurationFromFps(compositionFps);
|
|
1238
1200
|
let frameIndex = this.streamState.nextFrameIndex;
|
|
1239
1201
|
if (sourceTimestamp !== null) {
|
|
1240
|
-
const approxIndex =
|
|
1241
|
-
this.streamState.baseTimestamp,
|
|
1242
|
-
sourceTimestamp,
|
|
1243
|
-
compositionFps,
|
|
1244
|
-
"nearest"
|
|
1245
|
-
);
|
|
1202
|
+
const approxIndex = Math.round(sourceTimestamp / frameDuration);
|
|
1246
1203
|
frameIndex = Math.max(frameIndex, approxIndex);
|
|
1247
1204
|
}
|
|
1248
|
-
const
|
|
1249
|
-
const timelineTime = quantizeTimestampToFrame(
|
|
1250
|
-
rawTimeline,
|
|
1251
|
-
clipStartUs,
|
|
1252
|
-
compositionFps,
|
|
1253
|
-
"nearest"
|
|
1254
|
-
);
|
|
1205
|
+
const relativeTimeUs = frameIndex * frameDuration;
|
|
1255
1206
|
this.streamState.nextFrameIndex = frameIndex + 1;
|
|
1256
1207
|
this.streamState.lastSourceTimestamp = sourceTimestamp;
|
|
1257
|
-
return
|
|
1208
|
+
return relativeTimeUs;
|
|
1258
1209
|
}
|
|
1259
1210
|
}
|
|
1260
1211
|
const worker = new VideoComposeWorker();
|