@meframe/core 0.1.2 → 0.1.4

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.
@@ -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 { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { CacheManager } from '../cache/CacheManager';\nimport { ConfigLoader } from '../config/ConfigLoader';\nimport type { IOrchestrator, OrchestratorConfig, RenderFrameOptions } from './types';\nimport { WorkerType } from '../worker/types';\nimport { CompositionModel, CompositionPatch, Resource, TimeUs, RcFrame, Clip } from '../model';\nimport { hasResourceId } from '../model/types';\nimport { MeframeEvent, type EventPayloadMap } from '../event/events';\nimport { CompositionPlanner } from './CompositionPlanner';\nimport { GlobalAudioSession } from './GlobalAudioSession';\nimport { MuxManager } from '../stages/mux/MuxManager';\nimport { ExportOptions } from '../types';\nimport { OnDemandVideoSession } from './OnDemandVideoSession';\nimport { ExportScheduler } from './ExportScheduler';\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 exportScheduler: ExportScheduler;\n\n private isInitialized = false;\n private config = ConfigLoader.getInstance().getConfig();\n private ensureCacheDebounceTimer: number | null = null;\n private activeOnDemandSession: OnDemandVideoSession | 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 const maxMemoryMB = config.cacheConfig?.l1Size || this.config.cache?.l1?.maxMemoryMB || 1024;\n const maxGOPs = this.config.decode?.video?.maxGOPs || 4;\n\n this.cacheManager = new CacheManager(\n {\n l1: {\n maxMemoryMB,\n maxGOPs,\n },\n resource: {\n projectId: config.projectId || this.config.global.projectId || 'default',\n },\n },\n this.eventBus\n );\n\n this.resourceLoader = new ResourceLoader({\n cacheManager: this.cacheManager,\n eventBus: this.eventBus,\n config: {\n maxConcurrent: this.config.load.maxConcurrent,\n preloadConcurrency: 2, // Fixed preload concurrency for idle background loading\n },\n onStateChange: (resourceId, state) => this.handleResourceStateChange(resourceId, state),\n });\n\n this.planner = new CompositionPlanner();\n\n this.audioSession = new GlobalAudioSession({\n cacheManager: this.cacheManager,\n workerPool: this.workers,\n resourceLoader: this.resourceLoader,\n eventBus: this.eventBus,\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 this.exportScheduler = new ExportScheduler({\n workerPool: this.workers,\n planner: this.planner,\n cacheManager: this.cacheManager,\n resourceLoader: this.resourceLoader,\n muxManager: this.muxManager,\n audioSession: this.audioSession,\n workerConfigsProvider: () => this.buildWorkerConfigs(),\n eventBus: this.eventBus,\n });\n\n this.setupResourceFirstFrameHandler();\n this.setupPreloadHandlers();\n }\n\n private setupResourceFirstFrameHandler(): void {\n this.eventBus.on(MeframeEvent.ResourceFirstFrameReady, async (payload) => {\n const { resourceId, clipId, index, chunks } = payload;\n\n if (!this.compositionModel) return;\n\n // Find the specific clip\n const clip = this.compositionModel.findClip(clipId);\n if (!clip || !clip.trackId) return;\n\n // Only decode first frame for clips that start at composition time 0\n // (these clips need the resource's first frame as cover)\n if (clip.startUs === 0) {\n const fps = this.compositionModel.fps ?? 30;\n await OnDemandVideoSession.decodeAndCacheFirstFrame(\n resourceId,\n chunks,\n index,\n clip,\n this.cacheManager,\n fps\n );\n }\n });\n }\n\n private setupPreloadHandlers(): void {\n // Stop preloading when playback starts\n this.eventBus.on(MeframeEvent.PlaybackPlay, () => {\n this.resourceLoader.setPreloadingEnabled(false);\n });\n\n // Enable preloading when playback pauses/stops\n this.eventBus.on(MeframeEvent.PlaybackPause, () => {\n this.resourceLoader.setPreloadingEnabled(true);\n });\n\n this.eventBus.on(MeframeEvent.PlaybackStop, () => {\n this.resourceLoader.setPreloadingEnabled(true);\n });\n\n // Note: ModelSet and PatchApplied are handled internally in ResourceLoader via handleModelSet\n // and direct calls isn't needed as ResourceLoader listens to updateResourceState via onStateChange?\n // Wait, ResourceLoader handles ModelSet via eventHandlers.\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 cancelActiveDecoding(): void {\n if (this.activeOnDemandSession) {\n void this.activeOnDemandSession.dispose();\n this.activeOnDemandSession = null;\n }\n }\n\n /**\n * Decode and render nearest keyframe for fast seek preview\n * Returns the keyframe timestamp if successfully decoded, null otherwise\n */\n async tryRenderKeyframe(timeUs: TimeUs): Promise<TimeUs | null> {\n if (!this.compositionModel) return null;\n\n const clip = this.compositionModel.getClipsAtTime(timeUs, this.compositionModel.mainTrackId)[0];\n if (!clip || !hasResourceId(clip)) return null;\n\n // Use resource-relative time (same as frame.timestamp from decoder)\n const resourceTimeUs = timeUs - clip.startUs + (clip.trimStartUs ?? 0);\n const resourceId = clip.resourceId;\n\n // Check if keyframe is already in L1 cache\n const keyframeSample = this.cacheManager.mp4IndexCache.getNearestKeyframe(\n resourceId,\n resourceTimeUs\n );\n\n if (!keyframeSample) return null;\n\n const cachedKeyframe = this.cacheManager.getFrame(keyframeSample.timestamp, clip.id);\n if (cachedKeyframe) {\n return keyframeSample.timestamp;\n }\n\n // Resource must be ready to decode keyframe\n const resource = this.compositionModel.getResource(resourceId);\n if (resource?.state !== 'ready') return null;\n\n // Create temporary session to decode keyframe\n const session = await OnDemandVideoSession.create({\n clipId: clip.id,\n resourceId,\n targetTimeUs: resourceTimeUs,\n globalTimeUs: timeUs,\n resourceLoader: this.resourceLoader,\n mp4IndexCache: this.cacheManager.mp4IndexCache,\n cacheManager: this.cacheManager,\n compositionModel: this.compositionModel,\n fps: this.compositionModel.fps ?? 30,\n });\n\n try {\n const keyframeTimeUs = await session.decodeKeyframe(resourceTimeUs);\n return keyframeTimeUs;\n } catch (error) {\n console.warn('[Orchestrator] Fast keyframe decode failed:', error);\n return null;\n } finally {\n await session.dispose();\n }\n }\n\n async setCompositionModel(model: CompositionModel): Promise<void> {\n this.compositionModel = model;\n this.cacheManager.clear();\n this.planner.setModel(model);\n // ensure the cover resource is preloaded before audio is activated\n await this.resourceLoader.setModel(model);\n this.audioSession.setModel(model);\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\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 // Note: addTrack/removeTrack already call buildIndexes() in patch.ts\n const affectedClipIds = applyModelPatch(this.compositionModel, patch);\n this.planner.applyPatch(patch, affectedClipIds);\n this.eventBus.emit(MeframeEvent.PatchApplied, {\n operations: patch.operations.length,\n affectedClips: Array.from(affectedClipIds),\n });\n\n // Process clip updates\n for (const clipId of affectedClipIds) {\n this.cacheManager.invalidateClip(clipId);\n }\n\n // Reactivate updated audio clips\n const reactivatedAudioClips: string[] = [];\n const reactivatedVideoClips: string[] = [];\n for (const clipId of affectedClipIds) {\n const clip = this.compositionModel.findClip(clipId);\n if (clip?.trackKind === 'audio') {\n await this.audioSession.deactivateClip(clipId);\n reactivatedAudioClips.push(clipId);\n } else if (clip?.trackKind === 'video') {\n reactivatedVideoClips.push(clipId);\n }\n }\n\n // Activate all audio clips (including reactivated ones)\n await this.audioSession.activateAllAudioClips();\n\n // Note: No need to restart per-clip playback in new architecture\n // scheduleAudio() uses OfflineAudioMixer which automatically includes all active clips\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 // For preview, simple cache invalidation or triggering re-render might be enough\n // if we were caching instructions. But PlaybackController pulls instructions every frame.\n // So just updating Model state is enough.\n }\n\n async getFrame(timeUs: TimeUs, options?: RenderFrameOptions): Promise<RcFrame | null> {\n const signal = options?.signal;\n const preheat = options?.preheat ?? false;\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 const trimStartUs = clip.trimStartUs ?? 0;\n\n // Calculate resource-relative time (same as frame.timestamp from decoder)\n // resourceTimeUs = clipRelativeTime + trimStartUs\n let resourceTimeUs = options?.relativeTimeUs\n ? options.relativeTimeUs + trimStartUs\n : timeUs - clip.startUs + trimStartUs;\n\n // Clamp to valid range: [trimStartUs, trimStartUs + durationUs)\n resourceTimeUs = Math.min(resourceTimeUs, trimStartUs + clip.durationUs - 1);\n\n // 1. Check L1 window cache\n // Note: preheat mode skips cache check to force decoding the entire window\n // Why: Video cache is frame-level (point query), not window-level (range query)\n // A single cached frame doesn't guarantee the entire window is cached\n // Without preheat flag, preheating would return early on first frame hit\n if (!preheat) {\n const cachedFrame = this.cacheManager.getFrame(resourceTimeUs, clip.id);\n if (cachedFrame) {\n return cachedFrame;\n }\n }\n\n if (signal?.aborted) {\n return null;\n }\n\n // 2. Try decode from OPFS resource (on-demand path)\n const resourceFrame = await this.decodeFromResource(clip, resourceTimeUs, timeUs, options);\n return resourceFrame;\n }\n\n /**\n * Compose frame from OPFS resource (on-demand decoding)\n * This is the new path for long clips with window caching\n *\n * @param clip - The clip to decode\n * @param resourceTimeUs - Time in resource-relative coordinates (same as frame.timestamp)\n * @param globalTimeUs - Time in composition timeline\n */\n private async decodeFromResource(\n clip: Clip,\n resourceTimeUs: TimeUs,\n globalTimeUs: TimeUs,\n options?: RenderFrameOptions\n ): Promise<RcFrame | null> {\n if (!hasResourceId(clip)) return null;\n\n const resourceId = clip.resourceId;\n const trimStartUs = clip.trimStartUs ?? 0;\n\n // Check resource state\n const resource = this.compositionModel?.getResource(resourceId);\n const isReady = resource?.state === 'ready';\n\n const fetchOptions = {\n isPreload: false,\n clipId: clip.id,\n trackId: clip.trackId,\n };\n\n // In immediate mode, if not ready, trigger fetch in background and return null\n if (options?.immediate && !isReady) {\n // Fire and forget fetch to start loading\n this.resourceLoader.load(resourceId, fetchOptions);\n return null;\n }\n\n // Normal mode: wait for download\n await this.resourceLoader.load(resourceId, fetchOptions);\n\n this.cancelActiveDecoding();\n\n // Create temporary on-demand video session\n const session = await OnDemandVideoSession.create({\n clipId: clip.id,\n resourceId,\n targetTimeUs: resourceTimeUs,\n globalTimeUs,\n resourceLoader: this.resourceLoader,\n mp4IndexCache: this.cacheManager.mp4IndexCache,\n cacheManager: this.cacheManager,\n compositionModel: this.compositionModel!,\n fps: this.compositionModel?.fps ?? 30,\n });\n\n this.activeOnDemandSession = session;\n\n try {\n // Decode window: from target position to target + 3s\n // Window bounds are in resource time: [trimStartUs, trimStartUs + durationUs)\n const DECODE_WINDOW_SIZE = 3_000_000;\n\n const windowStart = resourceTimeUs;\n const windowEnd = Math.min(\n trimStartUs + clip.durationUs,\n resourceTimeUs + DECODE_WINDOW_SIZE\n );\n\n await session.decodeWindow(windowStart, windowEnd);\n // Return target frame from L1 cache\n return this.cacheManager.getFrame(resourceTimeUs, clip.id);\n } catch (error) {\n if (session.isDisposed) {\n return null;\n }\n console.error('[Orchestrator] Error composing from resource:', error);\n return null;\n } finally {\n if (this.activeOnDemandSession === session) {\n this.activeOnDemandSession = null;\n }\n await session.dispose();\n }\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 ?? 5,\n timeoutMs: options?.timeoutMs ?? 5_000,\n });\n }\n\n async dispose(): Promise<void> {\n if (this.ensureCacheDebounceTimer !== null) {\n clearTimeout(this.ensureCacheDebounceTimer);\n this.ensureCacheDebounceTimer = null;\n }\n\n this.resourceLoader.dispose();\n await this.cacheManager.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;\n const defaultCanvasWidth = config.global.defaultCanvasWidth;\n const defaultCanvasHeight = config.global.defaultCanvasHeight;\n const defaultFps = config.global.defaultFps;\n\n const targetFps = this.compositionModel?.fps ?? defaultFps;\n\n return {\n // videoDemux: { // DEPRECATED: Removed - replaced by IndexedVideoSource\n // highWaterMark: config.demux.backpressure.highWaterMark,\n // },\n audioDemux: {\n highWaterMark: config.demux.backpressure.highWaterMark,\n },\n // videoDecode: config.decode.video, // DEPRECATED: Removed - replaced by OnDemandVideoSession\n audioDecode: config.decode.audio,\n videoCompose: {\n width: defaultCanvasWidth,\n height: defaultCanvasHeight,\n fps: targetFps,\n backgroundColor: '#000000',\n enableSmoothing: true,\n enableHardwareAcceleration: true,\n fonts: config.global.fonts,\n },\n audioCompose: {\n enableDucking: config.compose.audio.enableDucking,\n },\n videoEncode: {\n // Main Profile Level 4.1 - better compression efficiency for social media\n codec: 'avc1.4D0029',\n width: defaultCanvasWidth,\n height: defaultCanvasHeight,\n bitrate: config.encode.video.bitrateKbps\n ? config.encode.video.bitrateKbps * 1000\n : this.calculateDefaultBitrate(defaultCanvasWidth, defaultCanvasHeight),\n framerate: targetFps,\n latencyMode: 'quality',\n bitrateMode: 'variable',\n hardwareAcceleration: 'no-preference',\n // Default 1 second keyframe interval for better social media compatibility\n keyFrameInterval: config.encode.video.keyIntervalS\n ? Math.round(config.encode.video.keyIntervalS * targetFps)\n : targetFps,\n ...config.encode.video,\n },\n };\n }\n\n /**\n * Calculate default video bitrate based on resolution\n * Optimized for social media platforms (YouTube, TikTok, WeChat, etc.)\n */\n private calculateDefaultBitrate(width: number, height: number): number {\n const pixels = width * height;\n\n // Bitrate recommendations for H.264 Main Profile VBR:\n // - 720p (921,600 px): 5 Mbps\n // - 1080p (2,073,600 px): 8 Mbps\n // - 4K (8,294,400 px): 25 Mbps\n if (pixels <= 921_600) {\n // 720p and below\n return 5_000_000;\n } else if (pixels <= 2_073_600) {\n // 1080p\n return 8_000_000;\n } else if (pixels <= 3_686_400) {\n // 1440p (2K)\n return 16_000_000;\n } else {\n // 4K and above\n return 25_000_000;\n }\n }\n\n async export(model: CompositionModel, options: ExportOptions): Promise<Blob> {\n return this.exportScheduler.execute(model, options);\n }\n\n /**\n * Preheat a specific clip's window range\n * Used by PlaybackController for cross-clip window preheating\n *\n * @param clipId - Clip identifier\n * @param clipRelativeStart - Start time relative to clip (microseconds), 0 = clip start\n * @param clipRelativeEnd - End time relative to clip (microseconds)\n * @param globalTimeUs - Global timeline position (for globalTimeUs in cache)\n */\n async preheatClipWindow(\n clipId: string,\n clipRelativeStart: TimeUs,\n clipRelativeEnd: TimeUs,\n globalTimeUs: TimeUs\n ): Promise<void> {\n if (!this.compositionModel) return;\n\n const clip = this.compositionModel.findClip(clipId);\n if (!clip || !hasResourceId(clip)) return;\n\n const resourceId = clip.resourceId;\n const trimStartUs = clip.trimStartUs ?? 0;\n\n // Convert clip-relative time to resource time\n const resourceStart = clipRelativeStart + trimStartUs;\n const resourceEnd = clipRelativeEnd + trimStartUs;\n\n // Ensure resource is downloaded\n await this.resourceLoader.load(resourceId, {\n isPreload: false,\n clipId: clip.id,\n trackId: clip.trackId,\n });\n\n // Create temporary on-demand session for this window\n const session = await OnDemandVideoSession.create({\n clipId: clip.id,\n resourceId,\n targetTimeUs: resourceStart,\n globalTimeUs,\n resourceLoader: this.resourceLoader,\n mp4IndexCache: this.cacheManager.mp4IndexCache,\n cacheManager: this.cacheManager,\n compositionModel: this.compositionModel,\n fps: this.compositionModel.fps ?? 30,\n });\n\n try {\n // Decode the entire window range for this clip (using resource time)\n await session.decodeWindow(resourceStart, resourceEnd);\n } catch (error) {\n console.warn(`[Orchestrator] Preheat clip ${clipId} window failed:`, error);\n // Non-critical, don't throw\n } finally {\n await session.dispose();\n }\n }\n\n /**\n * Get render state for real-time composition\n * Returns layers ready for VideoComposer\n */\n async getRenderState(\n timeUs: TimeUs,\n options?: RenderFrameOptions\n ): Promise<{ layers: any[]; transition?: any } | null> {\n if (!this.compositionModel) {\n return null;\n }\n\n // Ensure frame/resource is ready (this populates L1 if needed)\n const frame = await this.getFrame(timeUs, options);\n\n // If immediate mode and no frame, return null to trigger buffering\n if (!frame) {\n return null;\n }\n\n const clip = this.compositionModel.getClipsAtTime(timeUs, this.compositionModel.mainTrackId)[0];\n if (!clip) {\n return null;\n }\n\n const clipRelativeTimeUs = timeUs - clip.startUs;\n const resourceTimeUs = clipRelativeTimeUs + (clip.trimStartUs ?? 0);\n\n // Get instructions from planner\n const instructions = this.planner.getInstructions(clip.id);\n if (!instructions) {\n return null;\n }\n\n // Build layers array\n const layers: any[] = [];\n\n // 1. Filter active layers at this timestamp (uses clip-relative time for activeRanges)\n const activeLayers = instructions.layers.filter((layer: any) => {\n if (!layer.payload.attachmentId) {\n // Main track layer is always active\n return true;\n }\n if (layer.status !== 'ready') {\n return false;\n }\n // Check if layer is active at current timestamp\n return layer.activeRanges.some(\n (range: any) => clipRelativeTimeUs >= range.startUs && clipRelativeTimeUs < range.endUs\n );\n });\n\n // 2. Materialize layers\n for (const layerPlan of activeLayers) {\n const layer = await this.materializeLayer(layerPlan, clip, resourceTimeUs, timeUs);\n if (layer) {\n layers.push(layer);\n }\n }\n\n return { layers };\n }\n\n /**\n * Materialize a serialized layer plan into concrete Layer\n */\n private async materializeLayer(\n layerPlan: any,\n clip: Clip,\n resourceTimeUs: TimeUs,\n globalTimeUs: TimeUs\n ): Promise<any | null> {\n const baseLayer: any = {\n id: layerPlan.layerId,\n type: layerPlan.type,\n zIndex: layerPlan.zIndex ?? 0,\n visible: true,\n opacity: layerPlan.opacity ?? 1,\n };\n\n // Video layer - fetch raw VideoFrame from L1 (RcFrame wrapper)\n if (layerPlan.type === 'video' && !layerPlan.payload.attachmentId) {\n const rcFrame = this.cacheManager.getFrame(resourceTimeUs, clip.id);\n if (!rcFrame) {\n console.warn('[Orchestrator] Video frame not found in L1:', clip.id, resourceTimeUs);\n return null;\n }\n\n return {\n ...baseLayer,\n type: 'video',\n rcFrame: rcFrame,\n };\n }\n\n // Text layer\n if (layerPlan.type === 'text') {\n const payload = layerPlan.payload;\n return {\n ...baseLayer,\n type: 'text',\n text: payload.text,\n localeCode: payload.localeCode,\n fontConfig: payload.fontConfig,\n animation: payload.animation,\n wordTimings: payload.wordTimings,\n letterCase: payload.letterCase,\n };\n }\n\n // Image layer\n if (layerPlan.type === 'image') {\n const payload = layerPlan.payload;\n const resource = this.compositionModel?.getResource(payload.resourceId);\n if (!resource) {\n return null;\n }\n\n const source = await this.resourceLoader.loadImage(resource);\n const imageLayer: any = {\n ...baseLayer,\n type: 'image',\n source,\n attachmentId: payload.attachmentId,\n renderConfig: payload.renderConfig,\n };\n\n // Handle animation (overlays)\n if (payload.animation) {\n const { position, keyframes, overlayClipStartUs } = payload.animation;\n\n // Calculate time relative to overlay clip start\n const relativeTimeUs = globalTimeUs - overlayClipStartUs;\n\n // If outside keyframe range, hide\n if (relativeTimeUs < 0 || relativeTimeUs > keyframes[keyframes.length - 1].time) {\n return null; // Not visible at this time\n }\n\n const rotationRad = 0; // TODO: interpolate from keyframes\n\n imageLayer.transform = {\n x: position.x,\n y: position.y,\n scaleX: 1,\n scaleY: 1,\n rotation: rotationRad,\n anchorX: 0.5,\n anchorY: 0.5,\n };\n }\n\n return imageLayer;\n }\n\n return baseLayer;\n }\n}\n"],"names":["applyModelPatch"],"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 { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { CacheManager } from '../cache/CacheManager';\nimport { ConfigLoader } from '../config/ConfigLoader';\nimport type { IOrchestrator, OrchestratorConfig, RenderFrameOptions } from './types';\nimport { WorkerType } from '../worker/types';\nimport { CompositionModel, CompositionPatch, Resource, TimeUs, RcFrame, Clip } from '../model';\nimport { hasResourceId } from '../model/types';\nimport { MeframeEvent, type EventPayloadMap } from '../event/events';\nimport { CompositionPlanner } from './CompositionPlanner';\nimport { GlobalAudioSession } from './GlobalAudioSession';\nimport { MuxManager } from '../stages/mux/MuxManager';\nimport { ExportOptions } from '../types';\nimport { OnDemandVideoSession } from './OnDemandVideoSession';\nimport { ExportScheduler } from './ExportScheduler';\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 exportScheduler: ExportScheduler;\n\n private isInitialized = false;\n private config = ConfigLoader.getInstance().getConfig();\n private ensureCacheDebounceTimer: number | null = null;\n private activeOnDemandSession: OnDemandVideoSession | 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 const maxMemoryMB = config.cacheConfig?.l1Size || this.config.cache?.l1?.maxMemoryMB || 1024;\n const maxGOPs = this.config.decode?.video?.maxGOPs || 4;\n\n this.cacheManager = new CacheManager(\n {\n l1: {\n maxMemoryMB,\n maxGOPs,\n },\n resource: {\n projectId: config.projectId || this.config.global.projectId || 'default',\n },\n },\n this.eventBus\n );\n\n this.resourceLoader = new ResourceLoader({\n cacheManager: this.cacheManager,\n eventBus: this.eventBus,\n config: {\n maxConcurrent: this.config.load.maxConcurrent,\n preloadConcurrency: 2, // Fixed preload concurrency for idle background loading\n },\n onStateChange: (resourceId, state) => this.handleResourceStateChange(resourceId, state),\n });\n\n this.planner = new CompositionPlanner();\n\n this.audioSession = new GlobalAudioSession({\n cacheManager: this.cacheManager,\n workerPool: this.workers,\n resourceLoader: this.resourceLoader,\n eventBus: this.eventBus,\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 this.exportScheduler = new ExportScheduler({\n workerPool: this.workers,\n planner: this.planner,\n cacheManager: this.cacheManager,\n resourceLoader: this.resourceLoader,\n muxManager: this.muxManager,\n audioSession: this.audioSession,\n workerConfigsProvider: () => this.buildWorkerConfigs(),\n eventBus: this.eventBus,\n });\n\n this.setupResourceFirstFrameHandler();\n this.setupPreloadHandlers();\n }\n\n private setupResourceFirstFrameHandler(): void {\n this.eventBus.on(MeframeEvent.ResourceFirstFrameReady, async (payload) => {\n const { resourceId, clipId, index, chunks } = payload;\n\n if (!this.compositionModel) return;\n\n // Find the specific clip\n const clip = this.compositionModel.findClip(clipId);\n if (!clip || !clip.trackId) return;\n\n // Only decode first frame for clips that start at composition time 0\n // (these clips need the resource's first frame as cover)\n if (clip.startUs === 0) {\n const fps = this.compositionModel.fps ?? 30;\n await OnDemandVideoSession.decodeAndCacheFirstFrame(\n resourceId,\n chunks,\n index,\n clip,\n this.cacheManager,\n fps\n );\n }\n });\n }\n\n private setupPreloadHandlers(): void {\n // Stop preloading when playback starts\n this.eventBus.on(MeframeEvent.PlaybackPlay, () => {\n this.resourceLoader.setPreloadingEnabled(false);\n });\n\n // Enable preloading when playback pauses/stops\n this.eventBus.on(MeframeEvent.PlaybackPause, () => {\n this.resourceLoader.setPreloadingEnabled(true);\n });\n\n this.eventBus.on(MeframeEvent.PlaybackStop, () => {\n this.resourceLoader.setPreloadingEnabled(true);\n });\n\n // Note: ModelSet and PatchApplied are handled internally in ResourceLoader via handleModelSet\n // and direct calls isn't needed as ResourceLoader listens to updateResourceState via onStateChange?\n // Wait, ResourceLoader handles ModelSet via eventHandlers.\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 cancelActiveDecoding(): void {\n if (this.activeOnDemandSession) {\n void this.activeOnDemandSession.dispose();\n this.activeOnDemandSession = null;\n }\n }\n\n /**\n * Decode and render nearest keyframe for fast seek preview\n * Returns the keyframe timestamp if successfully decoded, null otherwise\n */\n async tryRenderKeyframe(timeUs: TimeUs): Promise<TimeUs | null> {\n if (!this.compositionModel) return null;\n\n const clip = this.compositionModel.getClipsAtTime(timeUs, this.compositionModel.mainTrackId)[0];\n if (!clip || !hasResourceId(clip)) return null;\n\n // Use resource-relative time (same as frame.timestamp from decoder)\n const resourceTimeUs = timeUs - clip.startUs + (clip.trimStartUs ?? 0);\n const resourceId = clip.resourceId;\n\n // Check if keyframe is already in L1 cache\n const keyframeSample = this.cacheManager.mp4IndexCache.getNearestKeyframe(\n resourceId,\n resourceTimeUs\n );\n\n if (!keyframeSample) return null;\n\n const cachedKeyframe = this.cacheManager.getFrame(keyframeSample.timestamp, clip.id);\n if (cachedKeyframe) {\n return keyframeSample.timestamp;\n }\n\n // Resource must be ready to decode keyframe\n const resource = this.compositionModel.getResource(resourceId);\n if (resource?.state !== 'ready') return null;\n\n // Create temporary session to decode keyframe\n const session = await OnDemandVideoSession.create({\n clipId: clip.id,\n resourceId,\n targetTimeUs: resourceTimeUs,\n globalTimeUs: timeUs,\n resourceLoader: this.resourceLoader,\n mp4IndexCache: this.cacheManager.mp4IndexCache,\n cacheManager: this.cacheManager,\n compositionModel: this.compositionModel,\n fps: this.compositionModel.fps ?? 30,\n });\n\n try {\n const keyframeTimeUs = await session.decodeKeyframe(resourceTimeUs);\n return keyframeTimeUs;\n } catch (error) {\n console.warn('[Orchestrator] Fast keyframe decode failed:', error);\n return null;\n } finally {\n await session.dispose();\n }\n }\n\n async setCompositionModel(model: CompositionModel): Promise<void> {\n this.compositionModel = model;\n this.cacheManager.clear();\n this.planner.setModel(model);\n // ensure the cover resource is preloaded before audio is activated\n await this.resourceLoader.setModel(model);\n this.audioSession.setModel(model);\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\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 // Note: addTrack/removeTrack already call buildIndexes() in patch.ts\n const affectedClipIds = applyModelPatch(this.compositionModel, patch);\n this.planner.applyPatch(patch, affectedClipIds);\n this.eventBus.emit(MeframeEvent.PatchApplied, {\n operations: patch.operations.length,\n affectedClips: Array.from(affectedClipIds),\n });\n\n // Process clip updates\n for (const clipId of affectedClipIds) {\n this.cacheManager.invalidateClip(clipId);\n }\n\n // Reactivate updated audio clips\n const reactivatedAudioClips: string[] = [];\n const reactivatedVideoClips: string[] = [];\n for (const clipId of affectedClipIds) {\n const clip = this.compositionModel.findClip(clipId);\n if (clip?.trackKind === 'audio') {\n await this.audioSession.deactivateClip(clipId);\n reactivatedAudioClips.push(clipId);\n } else if (clip?.trackKind === 'video') {\n reactivatedVideoClips.push(clipId);\n }\n }\n\n // Activate all audio clips (including reactivated ones)\n await this.audioSession.activateAllAudioClips();\n\n // Note: No need to restart per-clip playback in new architecture\n // scheduleAudio() uses OfflineAudioMixer which automatically includes all active clips\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 // For preview, simple cache invalidation or triggering re-render might be enough\n // if we were caching instructions. But PlaybackController pulls instructions every frame.\n // So just updating Model state is enough.\n }\n\n async getFrame(timeUs: TimeUs, options?: RenderFrameOptions): Promise<RcFrame | null> {\n const signal = options?.signal;\n const preheat = options?.preheat ?? false;\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 const trimStartUs = clip.trimStartUs ?? 0;\n\n // Calculate resource-relative time (same as frame.timestamp from decoder)\n // resourceTimeUs = clipRelativeTime + trimStartUs\n let resourceTimeUs = options?.relativeTimeUs\n ? options.relativeTimeUs + trimStartUs\n : timeUs - clip.startUs + trimStartUs;\n\n // Clamp to valid range: [trimStartUs, trimStartUs + durationUs)\n resourceTimeUs = Math.min(resourceTimeUs, trimStartUs + clip.durationUs - 1);\n\n // 1. Check L1 window cache\n // Note: preheat mode skips cache check to force decoding the entire window\n // Why: Video cache is frame-level (point query), not window-level (range query)\n // A single cached frame doesn't guarantee the entire window is cached\n // Without preheat flag, preheating would return early on first frame hit\n if (!preheat) {\n const cachedFrame = this.cacheManager.getFrame(resourceTimeUs, clip.id);\n if (cachedFrame) {\n return cachedFrame;\n }\n }\n\n if (signal?.aborted) {\n return null;\n }\n\n // 2. Try decode from OPFS resource (on-demand path)\n const resourceFrame = await this.decodeFromResource(clip, resourceTimeUs, timeUs, options);\n return resourceFrame;\n }\n\n /**\n * Compose frame from OPFS resource (on-demand decoding)\n * This is the new path for long clips with window caching\n *\n * @param clip - The clip to decode\n * @param resourceTimeUs - Time in resource-relative coordinates (same as frame.timestamp)\n * @param globalTimeUs - Time in composition timeline\n */\n private async decodeFromResource(\n clip: Clip,\n resourceTimeUs: TimeUs,\n globalTimeUs: TimeUs,\n options?: RenderFrameOptions\n ): Promise<RcFrame | null> {\n if (!hasResourceId(clip)) return null;\n\n const resourceId = clip.resourceId;\n const trimStartUs = clip.trimStartUs ?? 0;\n\n // Check resource state\n const resource = this.compositionModel?.getResource(resourceId);\n const isReady = resource?.state === 'ready';\n\n const fetchOptions = {\n isPreload: false,\n clipId: clip.id,\n trackId: clip.trackId,\n };\n\n // In immediate mode, if not ready, trigger fetch in background and return null\n if (options?.immediate && !isReady) {\n // Fire and forget fetch to start loading\n this.resourceLoader.load(resourceId, fetchOptions);\n return null;\n }\n\n // Normal mode: wait for download\n await this.resourceLoader.load(resourceId, fetchOptions);\n\n this.cancelActiveDecoding();\n\n // Create temporary on-demand video session\n const session = await OnDemandVideoSession.create({\n clipId: clip.id,\n resourceId,\n targetTimeUs: resourceTimeUs,\n globalTimeUs,\n resourceLoader: this.resourceLoader,\n mp4IndexCache: this.cacheManager.mp4IndexCache,\n cacheManager: this.cacheManager,\n compositionModel: this.compositionModel!,\n fps: this.compositionModel?.fps ?? 30,\n });\n\n this.activeOnDemandSession = session;\n\n try {\n // Decode window: from target position to target + 3s\n // Window bounds are in resource time: [trimStartUs, trimStartUs + durationUs)\n const DECODE_WINDOW_SIZE = 3_000_000;\n\n const windowStart = resourceTimeUs;\n const windowEnd = Math.min(\n trimStartUs + clip.durationUs,\n resourceTimeUs + DECODE_WINDOW_SIZE\n );\n\n await session.decodeWindow(windowStart, windowEnd);\n // Return target frame from L1 cache\n return this.cacheManager.getFrame(resourceTimeUs, clip.id);\n } catch (error) {\n if (session.isDisposed) {\n return null;\n }\n console.error('[Orchestrator] Error composing from resource:', error);\n return null;\n } finally {\n if (this.activeOnDemandSession === session) {\n this.activeOnDemandSession = null;\n }\n await session.dispose();\n }\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 ?? 5,\n timeoutMs: options?.timeoutMs ?? 5_000,\n });\n }\n\n async dispose(): Promise<void> {\n if (this.ensureCacheDebounceTimer !== null) {\n clearTimeout(this.ensureCacheDebounceTimer);\n this.ensureCacheDebounceTimer = null;\n }\n\n this.resourceLoader.dispose();\n await this.cacheManager.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;\n const defaultCanvasWidth = config.global.defaultCanvasWidth;\n const defaultCanvasHeight = config.global.defaultCanvasHeight;\n const defaultFps = config.global.defaultFps;\n\n const targetFps = this.compositionModel?.fps ?? defaultFps;\n\n return {\n // videoDemux: { // DEPRECATED: Removed - replaced by IndexedVideoSource\n // highWaterMark: config.demux.backpressure.highWaterMark,\n // },\n audioDemux: {\n highWaterMark: config.demux.backpressure.highWaterMark,\n },\n // videoDecode: config.decode.video, // DEPRECATED: Removed - replaced by OnDemandVideoSession\n audioDecode: config.decode.audio,\n videoCompose: {\n width: defaultCanvasWidth,\n height: defaultCanvasHeight,\n fps: targetFps,\n backgroundColor: '#000000',\n enableSmoothing: true,\n enableHardwareAcceleration: true,\n fonts: config.global.fonts,\n },\n audioCompose: {\n enableDucking: config.compose.audio.enableDucking,\n },\n videoEncode: {\n // Main Profile Level 4.1 - better compression efficiency for social media\n codec: 'avc1.4D0029',\n width: defaultCanvasWidth,\n height: defaultCanvasHeight,\n bitrate: config.encode.video.bitrateKbps\n ? config.encode.video.bitrateKbps * 1000\n : this.calculateDefaultBitrate(defaultCanvasWidth, defaultCanvasHeight),\n framerate: targetFps,\n latencyMode: 'quality',\n bitrateMode: 'variable',\n hardwareAcceleration: 'no-preference',\n // Default 1 second keyframe interval for better social media compatibility\n keyFrameInterval: config.encode.video.keyIntervalS\n ? Math.round(config.encode.video.keyIntervalS * targetFps)\n : targetFps,\n ...config.encode.video,\n },\n };\n }\n\n /**\n * Calculate default video bitrate based on resolution\n * Optimized for social media platforms (YouTube, TikTok, WeChat, etc.)\n */\n private calculateDefaultBitrate(width: number, height: number): number {\n const pixels = width * height;\n\n // Bitrate recommendations for H.264 Main Profile VBR:\n // - 720p (921,600 px): 5 Mbps\n // - 1080p (2,073,600 px): 8 Mbps\n // - 4K (8,294,400 px): 25 Mbps\n if (pixels <= 921_600) {\n // 720p and below\n return 5_000_000;\n } else if (pixels <= 2_073_600) {\n // 1080p\n return 8_000_000;\n } else if (pixels <= 3_686_400) {\n // 1440p (2K)\n return 16_000_000;\n } else {\n // 4K and above\n return 25_000_000;\n }\n }\n\n async export(model: CompositionModel, options: ExportOptions): Promise<Blob> {\n return this.exportScheduler.execute(model, options);\n }\n\n /**\n * Preheat a specific clip's window range\n * Used by PlaybackController for cross-clip window preheating\n *\n * @param clipId - Clip identifier\n * @param clipRelativeStart - Start time relative to clip (microseconds), 0 = clip start\n * @param clipRelativeEnd - End time relative to clip (microseconds)\n * @param globalTimeUs - Global timeline position (for globalTimeUs in cache)\n */\n async preheatClipWindow(\n clipId: string,\n clipRelativeStart: TimeUs,\n clipRelativeEnd: TimeUs,\n globalTimeUs: TimeUs\n ): Promise<void> {\n if (!this.compositionModel) return;\n\n const clip = this.compositionModel.findClip(clipId);\n if (!clip || !hasResourceId(clip)) return;\n\n const resourceId = clip.resourceId;\n const trimStartUs = clip.trimStartUs ?? 0;\n\n // Convert clip-relative time to resource time\n const resourceStart = clipRelativeStart + trimStartUs;\n const resourceEnd = clipRelativeEnd + trimStartUs;\n\n // Ensure resource is downloaded\n await this.resourceLoader.load(resourceId, {\n isPreload: false,\n clipId: clip.id,\n trackId: clip.trackId,\n });\n\n // Create temporary on-demand session for this window\n const session = await OnDemandVideoSession.create({\n clipId: clip.id,\n resourceId,\n targetTimeUs: resourceStart,\n globalTimeUs,\n resourceLoader: this.resourceLoader,\n mp4IndexCache: this.cacheManager.mp4IndexCache,\n cacheManager: this.cacheManager,\n compositionModel: this.compositionModel,\n fps: this.compositionModel.fps ?? 30,\n });\n\n try {\n // Decode the entire window range for this clip (using resource time)\n await session.decodeWindow(resourceStart, resourceEnd);\n } catch (error) {\n console.warn(`[Orchestrator] Preheat clip ${clipId} window failed:`, error);\n // Non-critical, don't throw\n } finally {\n await session.dispose();\n }\n }\n\n /**\n * Get render state for real-time composition\n * Returns layers ready for VideoComposer\n */\n async getRenderState(\n timeUs: TimeUs,\n options?: RenderFrameOptions\n ): Promise<{ layers: any[]; transition?: any } | null> {\n if (!this.compositionModel) {\n return null;\n }\n\n // Ensure frame/resource is ready (this populates L1 if needed)\n const frame = await this.getFrame(timeUs, options);\n\n // If immediate mode and no frame, return null to trigger buffering\n if (!frame) {\n return null;\n }\n\n const clip = this.compositionModel.getClipsAtTime(timeUs, this.compositionModel.mainTrackId)[0];\n if (!clip) {\n return null;\n }\n\n const clipRelativeTimeUs = timeUs - clip.startUs;\n const resourceTimeUs = clipRelativeTimeUs + (clip.trimStartUs ?? 0);\n\n // Get instructions from planner\n const instructions = this.planner.getInstructions(clip.id);\n if (!instructions) {\n return null;\n }\n\n // Build layers array\n const layers: any[] = [];\n\n // 1. Filter active layers at this timestamp (uses clip-relative time for activeRanges)\n const activeLayers = instructions.layers.filter((layer: any) => {\n if (!layer.payload.attachmentId) {\n // Main track layer is always active\n return true;\n }\n if (layer.status !== 'ready') {\n return false;\n }\n // Check if layer is active at current timestamp\n return layer.activeRanges.some(\n (range: any) => clipRelativeTimeUs >= range.startUs && clipRelativeTimeUs < range.endUs\n );\n });\n\n // 2. Materialize layers\n for (const layerPlan of activeLayers) {\n const layer = await this.materializeLayer(layerPlan, clip, resourceTimeUs, timeUs);\n if (layer) {\n layers.push(layer);\n }\n }\n\n return { layers };\n }\n\n /**\n * Materialize a serialized layer plan into concrete Layer\n */\n private async materializeLayer(\n layerPlan: any,\n clip: Clip,\n resourceTimeUs: TimeUs,\n globalTimeUs: TimeUs\n ): Promise<any | null> {\n const baseLayer: any = {\n id: layerPlan.layerId,\n type: layerPlan.type,\n zIndex: layerPlan.zIndex ?? 0,\n visible: true,\n opacity: layerPlan.opacity ?? 1,\n };\n\n // Video layer - fetch raw VideoFrame from L1 (RcFrame wrapper)\n if (layerPlan.type === 'video' && !layerPlan.payload.attachmentId) {\n const rcFrame = this.cacheManager.getFrame(resourceTimeUs, clip.id);\n if (!rcFrame) {\n console.warn('[Orchestrator] Video frame not found in L1:', clip.id, resourceTimeUs);\n return null;\n }\n\n return {\n ...baseLayer,\n type: 'video',\n rcFrame: rcFrame,\n };\n }\n\n // Text layer\n if (layerPlan.type === 'text') {\n const payload = layerPlan.payload;\n return {\n ...baseLayer,\n type: 'text',\n text: payload.text,\n localeCode: payload.localeCode,\n fontConfig: payload.fontConfig,\n animation: payload.animation,\n wordTimings: payload.wordTimings,\n letterCase: payload.letterCase,\n };\n }\n\n // Image layer\n if (layerPlan.type === 'image') {\n const payload = layerPlan.payload;\n const resource = this.compositionModel?.getResource(payload.resourceId);\n if (!resource) {\n return null;\n }\n\n const source = await this.resourceLoader.loadImage(resource);\n const imageLayer: any = {\n ...baseLayer,\n type: 'image',\n source,\n attachmentId: payload.attachmentId,\n };\n\n // Add renderConfig fields if present\n if (payload.renderConfig) {\n imageLayer.renderConfig = {\n ...(payload.renderConfig.width !== undefined && { width: payload.renderConfig.width }),\n ...(payload.renderConfig.height !== undefined && { height: payload.renderConfig.height }),\n };\n }\n\n // Handle animation (overlays)\n if (payload.animation) {\n const { position, keyframes, overlayClipStartUs } = payload.animation;\n\n // Calculate time relative to overlay clip start\n const relativeTimeUs = globalTimeUs - overlayClipStartUs;\n\n // If outside keyframe range, hide\n if (relativeTimeUs < 0 || relativeTimeUs > keyframes[keyframes.length - 1].time) {\n return null; // Not visible at this time\n }\n\n const rotationRad = 0; // TODO: interpolate from keyframes\n\n imageLayer.transform = {\n x: position.x,\n y: position.y,\n scaleX: 1,\n scaleY: 1,\n rotation: rotationRad,\n anchorX: 0.5,\n anchorY: 0.5,\n };\n }\n\n return imageLayer;\n }\n\n return baseLayer;\n }\n}\n"],"names":["applyModelPatch"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"LayerRenderer.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/LayerRenderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAA8D,MAAM,SAAS,CAAC;AAMjG;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,GAAG,CAA+D;IAC1E,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,YAAY,CAAa;IACjC,OAAO,CAAC,GAAG,CAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAO;gBAGpC,GAAG,EAAE,iCAAiC,GAAG,wBAAwB,EACjE,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,MAAW;IASlB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIpC,OAAO,CAAC,0BAA0B;IAKlC;;OAEG;IACH,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAkD/B,OAAO,CAAC,cAAc;IAqBtB;;;OAGG;IACH,OAAO,CAAC,6BAA6B;IA8BrC,OAAO,CAAC,kBAAkB;IAmB1B;;;;;;OAMG;IACH,OAAO,CAAC,yBAAyB;IAkCjC,OAAO,CAAC,cAAc;IA2BtB,OAAO,CAAC,gBAAgB;IA+CxB,OAAO,CAAC,gBAAgB;IAgExB,OAAO,CAAC,eAAe;IAuCvB,OAAO,CAAC,SAAS;IAkBjB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;CAKtD"}
1
+ {"version":3,"file":"LayerRenderer.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/LayerRenderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAA8D,MAAM,SAAS,CAAC;AAMjG;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,GAAG,CAA+D;IAC1E,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,YAAY,CAAa;IACjC,OAAO,CAAC,GAAG,CAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAO;gBAGpC,GAAG,EAAE,iCAAiC,GAAG,wBAAwB,EACjE,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,MAAW;IASlB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIpC,OAAO,CAAC,0BAA0B;IAKlC;;OAEG;IACH,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAkD/B,OAAO,CAAC,cAAc;IAqBtB;;;OAGG;IACH,OAAO,CAAC,6BAA6B;IA8BrC,OAAO,CAAC,kBAAkB;IAyC1B;;;;;;OAMG;IACH,OAAO,CAAC,yBAAyB;IAkCjC,OAAO,CAAC,cAAc;IA2BtB,OAAO,CAAC,gBAAgB;IAoDxB,OAAO,CAAC,gBAAgB;IA2ExB,OAAO,CAAC,eAAe;IAuCvB,OAAO,CAAC,SAAS;IAkBjB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;CAKtD"}
@@ -106,14 +106,22 @@ class LayerRenderer {
106
106
  if (imageLayer.source) {
107
107
  const imgWidth = imageLayer.source.width;
108
108
  const imgHeight = imageLayer.source.height;
109
- return this.calculateDimensionsFromConfig(imgWidth, imgHeight, imageLayer.renderConfig);
109
+ const hasValidRenderConfig = !!(imageLayer.renderConfig?.width !== void 0 || imageLayer.renderConfig?.height !== void 0);
110
+ if (hasValidRenderConfig) {
111
+ return this.calculateDimensionsFromConfig(imgWidth, imgHeight, imageLayer.renderConfig);
112
+ }
113
+ return { width: imgWidth, height: imgHeight };
110
114
  }
111
115
  } else if (layer.type === "video") {
112
116
  const videoLayer = layer;
113
117
  const videoFrame = videoLayer.videoFrame;
114
118
  const videoWidth = videoFrame.displayWidth || videoFrame.codedWidth;
115
119
  const videoHeight = videoFrame.displayHeight || videoFrame.codedHeight;
116
- return this.calculateDimensionsFromConfig(videoWidth, videoHeight, videoLayer.renderConfig);
120
+ const hasValidRenderConfig = !!(videoLayer.renderConfig?.width !== void 0 || videoLayer.renderConfig?.height !== void 0);
121
+ if (hasValidRenderConfig) {
122
+ return this.calculateDimensionsFromConfig(videoWidth, videoHeight, videoLayer.renderConfig);
123
+ }
124
+ return { width: videoWidth, height: videoHeight };
117
125
  }
118
126
  return { width: this.width, height: this.height };
119
127
  }
@@ -165,7 +173,8 @@ class LayerRenderer {
165
173
  let renderY;
166
174
  let renderWidth;
167
175
  let renderHeight;
168
- if (renderConfig) {
176
+ const hasValidRenderConfig = !!(renderConfig?.width !== void 0 || renderConfig?.height !== void 0);
177
+ if (hasValidRenderConfig) {
169
178
  const dimensions = this.calculateDimensionsFromConfig(videoWidth, videoHeight, renderConfig);
170
179
  renderWidth = dimensions.width;
171
180
  renderHeight = dimensions.height;
@@ -196,7 +205,7 @@ class LayerRenderer {
196
205
  }
197
206
  }
198
207
  renderImageLayer(layer) {
199
- const { source, crop, renderConfig } = layer;
208
+ const { source, crop, renderConfig, attachmentId } = layer;
200
209
  if (source instanceof ImageData) {
201
210
  if (crop) {
202
211
  const tempCanvas = new OffscreenCanvas(crop.width, crop.height);
@@ -215,12 +224,18 @@ class LayerRenderer {
215
224
  let renderY;
216
225
  let renderWidth;
217
226
  let renderHeight;
218
- if (renderConfig) {
227
+ const hasValidRenderConfig = !!(renderConfig?.width !== void 0 || renderConfig?.height !== void 0);
228
+ if (hasValidRenderConfig) {
219
229
  const dimensions = this.calculateDimensionsFromConfig(imgWidth, imgHeight, renderConfig);
220
230
  renderWidth = dimensions.width;
221
231
  renderHeight = dimensions.height;
222
232
  renderX = 0;
223
233
  renderY = 0;
234
+ } else if (attachmentId) {
235
+ renderWidth = imgWidth;
236
+ renderHeight = imgHeight;
237
+ renderX = 0;
238
+ renderY = 0;
224
239
  } else {
225
240
  const naturalScale = this.width / imgWidth;
226
241
  const dimensions = this.calculateRenderDimensions(imgWidth, imgHeight, naturalScale);
@@ -1 +1 @@
1
- {"version":3,"file":"LayerRenderer.js","sources":["../../../src/stages/compose/LayerRenderer.ts"],"sourcesContent":["import type { Layer, VideoLayer, ImageLayer, TextLayer, Transform2D, MaskConfig } from './types';\nimport { renderBasicText, renderTextWithEntrance } from './text-renderers/basic-text-renderer';\nimport { renderWordByWord } from './text-renderers/word-by-word-renderer';\nimport { renderCharacterKTV } from './text-renderers/character-ktv-renderer';\nimport { renderWordByWordFancy } from './text-renderers/word-fancy-renderer';\n\n/**\n * LayerRenderer - Handles rendering of individual layers\n * Single responsibility: Draw a single layer to the canvas context\n */\nexport class LayerRenderer {\n private ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;\n private width: number;\n private height: number;\n private currentFrame: number = 0;\n private fps: number = 30;\n private readonly FILL_THRESHOLD = 0.9;\n\n constructor(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n width: number,\n height: number,\n fps: number = 30\n ) {\n this.ctx = ctx;\n this.width = width;\n this.height = height;\n this.fps = fps;\n this.ensureHighQualityRendering();\n }\n\n setCurrentFrame(frame: number): void {\n this.currentFrame = frame;\n }\n\n private ensureHighQualityRendering(): void {\n this.ctx.imageSmoothingEnabled = true;\n this.ctx.imageSmoothingQuality = 'high';\n }\n\n /**\n * Render a single layer with all its properties\n */\n renderLayer(layer: Layer): void {\n if (!layer.visible || layer.opacity <= 0) return;\n\n // Only save/restore context if layer has properties that need it\n const needsStateManagement =\n layer.opacity !== 1 || layer.blendMode || layer.transform || layer.mask;\n\n if (needsStateManagement) {\n this.ctx.save();\n }\n\n try {\n // Apply layer properties only when needed\n if (layer.opacity !== 1) {\n this.ctx.globalAlpha = layer.opacity;\n }\n\n if (layer.blendMode) {\n this.ctx.globalCompositeOperation = layer.blendMode;\n }\n\n if (layer.transform) {\n // Get layer dimensions for transform anchor calculation\n const layerDimensions = this.getLayerDimensions(layer);\n this.applyTransform(layer.transform, layerDimensions);\n }\n // Render based on layer type\n switch (layer.type) {\n case 'video':\n this.renderVideoLayer(layer as VideoLayer);\n break;\n case 'image':\n this.renderImageLayer(layer as ImageLayer);\n break;\n case 'text':\n this.renderTextLayer(layer as TextLayer);\n break;\n }\n\n // Apply mask if present\n if (layer.mask) {\n this.applyMask(layer.mask);\n }\n } finally {\n if (needsStateManagement) {\n this.ctx.restore();\n }\n }\n }\n\n private parseDimension(\n value: number | string | undefined,\n canvasSize: number\n ): number | undefined {\n if (value === undefined) return undefined;\n if (typeof value === 'number') return value;\n\n // value is string at this point\n const strValue = value as string;\n\n // Parse percentage string like \"5%\"\n if (strValue.includes('%')) {\n const numValue = parseFloat(strValue);\n return isNaN(numValue) ? undefined : (numValue / 100) * canvasSize;\n }\n\n // Parse as pixel value\n const parsed = parseFloat(strValue);\n return isNaN(parsed) ? undefined : parsed;\n }\n\n /**\n * Calculate dimensions from renderConfig\n * Returns dimensions maintaining aspect ratio when only one dimension is specified\n */\n private calculateDimensionsFromConfig(\n sourceWidth: number,\n sourceHeight: number,\n renderConfig: { width?: number | string; height?: number | string } | undefined\n ): { width: number; height: number } {\n if (!renderConfig) {\n return { width: sourceWidth, height: sourceHeight };\n }\n\n const width = this.parseDimension(renderConfig.width, this.width);\n const height = this.parseDimension(renderConfig.height, this.height);\n\n if (width && height) {\n return { width, height };\n } else if (width) {\n return {\n width,\n height: Math.round((sourceHeight / sourceWidth) * width),\n };\n } else if (height) {\n return {\n width: Math.round((sourceWidth / sourceHeight) * height),\n height,\n };\n } else {\n // renderConfig exists but empty, use original size\n return { width: sourceWidth, height: sourceHeight };\n }\n }\n\n private getLayerDimensions(layer: Layer): { width: number; height: number } {\n if (layer.type === 'image') {\n const imageLayer = layer as ImageLayer;\n if (imageLayer.source) {\n const imgWidth = imageLayer.source.width;\n const imgHeight = imageLayer.source.height;\n return this.calculateDimensionsFromConfig(imgWidth, imgHeight, imageLayer.renderConfig);\n }\n } else if (layer.type === 'video') {\n const videoLayer = layer as VideoLayer;\n const videoFrame = videoLayer.videoFrame;\n const videoWidth = videoFrame.displayWidth || videoFrame.codedWidth;\n const videoHeight = videoFrame.displayHeight || videoFrame.codedHeight;\n return this.calculateDimensionsFromConfig(videoWidth, videoHeight, videoLayer.renderConfig);\n }\n // Default to canvas dimensions\n return { width: this.width, height: this.height };\n }\n\n /**\n * Calculate render dimensions with smart fill logic\n * @param sourceWidth Source width\n * @param sourceHeight Source height\n * @param naturalScale Natural scale factor (scaleY for video, scaleX for image)\n * @returns Render dimensions and position\n */\n private calculateRenderDimensions(\n sourceWidth: number,\n sourceHeight: number,\n naturalScale: number\n ): { width: number; height: number; x: number; y: number } {\n const scaledWidth = sourceWidth * naturalScale;\n const scaledHeight = sourceHeight * naturalScale;\n\n // Smart fill: when scaled size is close to container (>90%), use cover mode\n const shouldFill =\n scaledWidth / this.width > this.FILL_THRESHOLD &&\n scaledHeight / this.height > this.FILL_THRESHOLD;\n\n let renderWidth: number;\n let renderHeight: number;\n\n if (shouldFill) {\n // Cover mode: use Math.max to ensure entire canvas is covered while maintaining aspect ratio\n const coverScale = Math.max(this.width / sourceWidth, this.height / sourceHeight);\n renderWidth = Math.round(sourceWidth * coverScale);\n renderHeight = Math.round(sourceHeight * coverScale);\n } else {\n // Natural scale mode: use scaled dimensions\n renderWidth = Math.round(scaledWidth);\n renderHeight = Math.round(scaledHeight);\n }\n\n // Center the content\n const renderX = Math.round((this.width - renderWidth) / 2);\n const renderY = Math.round((this.height - renderHeight) / 2);\n\n return { width: renderWidth, height: renderHeight, x: renderX, y: renderY };\n }\n\n private applyTransform(\n transform: Transform2D,\n layerDimensions: { width: number; height: number }\n ): void {\n // Use layer dimensions (not canvas dimensions) for anchor calculation\n const anchorX = transform.anchorX ?? 0.5;\n const anchorY = transform.anchorY ?? 0.5;\n const centerX = layerDimensions.width * anchorX;\n const centerY = layerDimensions.height * anchorY;\n\n // Move to the layer position + anchor offset\n this.ctx.translate(transform.x + centerX, transform.y + centerY);\n\n if (transform.rotation) {\n this.ctx.rotate(transform.rotation);\n }\n\n this.ctx.scale(transform.scaleX, transform.scaleY);\n\n if (transform.skewX || transform.skewY) {\n this.ctx.transform(1, transform.skewY ?? 0, transform.skewX ?? 0, 1, 0, 0);\n }\n\n // Move back by anchor offset\n this.ctx.translate(-centerX, -centerY);\n }\n\n private renderVideoLayer(layer: VideoLayer): void {\n const { videoFrame, crop, renderConfig } = layer;\n\n const videoWidth = videoFrame.displayWidth || videoFrame.codedWidth;\n const videoHeight = videoFrame.displayHeight || videoFrame.codedHeight;\n\n let renderX: number;\n let renderY: number;\n let renderWidth: number;\n let renderHeight: number;\n\n if (renderConfig) {\n // Has renderConfig: explicit dimensions\n const dimensions = this.calculateDimensionsFromConfig(videoWidth, videoHeight, renderConfig);\n renderWidth = dimensions.width;\n renderHeight = dimensions.height;\n // Center the video\n renderX = Math.round((this.width - renderWidth) / 2);\n renderY = Math.round((this.height - renderHeight) / 2);\n } else {\n // No renderConfig: legacy smart fill (height-based)\n const naturalScale = this.height / videoHeight;\n const dimensions = this.calculateRenderDimensions(videoWidth, videoHeight, naturalScale);\n renderX = dimensions.x;\n renderY = dimensions.y;\n renderWidth = dimensions.width;\n renderHeight = dimensions.height;\n }\n\n if (crop) {\n this.ctx.drawImage(\n videoFrame,\n crop.x,\n crop.y,\n crop.width,\n crop.height,\n renderX,\n renderY,\n renderWidth,\n renderHeight\n );\n } else {\n this.ctx.drawImage(videoFrame, renderX, renderY, renderWidth, renderHeight);\n }\n // NOTE: Do not close videoFrame - it's managed by RcFrame wrapper\n }\n\n private renderImageLayer(layer: ImageLayer): void {\n const { source, crop, renderConfig } = layer;\n\n // Handle ImageData by putting it on canvas first\n if (source instanceof ImageData) {\n if (crop) {\n // For ImageData with crop, we need to extract the cropped region\n const tempCanvas = new OffscreenCanvas(crop.width, crop.height);\n const tempCtx = tempCanvas.getContext('2d')!;\n tempCtx.putImageData(source, -crop.x, -crop.y);\n this.ctx.drawImage(tempCanvas, 0, 0, this.width, this.height);\n } else {\n // Put ImageData directly\n this.ctx.putImageData(source, 0, 0);\n }\n return;\n }\n\n if (!source) return;\n\n const imgWidth = source.width;\n const imgHeight = source.height;\n\n let renderX: number;\n let renderY: number;\n let renderWidth: number;\n let renderHeight: number;\n\n if (renderConfig) {\n // Has renderConfig: explicit dimensions\n const dimensions = this.calculateDimensionsFromConfig(imgWidth, imgHeight, renderConfig);\n renderWidth = dimensions.width;\n renderHeight = dimensions.height;\n // Images with renderConfig start at origin (for overlay positioning)\n renderX = 0;\n renderY = 0;\n } else {\n // No renderConfig: legacy smart fill (width-based, main track)\n const naturalScale = this.width / imgWidth;\n const dimensions = this.calculateRenderDimensions(imgWidth, imgHeight, naturalScale);\n renderWidth = dimensions.width;\n renderHeight = dimensions.height;\n // Center the image\n renderX = Math.round((this.width - renderWidth) / 2);\n renderY = Math.round((this.height - renderHeight) / 2);\n }\n\n if (crop) {\n this.ctx.drawImage(\n source,\n crop.x,\n crop.y,\n crop.width,\n crop.height,\n renderX,\n renderY,\n renderWidth,\n renderHeight\n );\n } else {\n this.ctx.drawImage(source, renderX, renderY, renderWidth, renderHeight);\n }\n }\n\n private renderTextLayer(layer: TextLayer): void {\n const animationType = layer.animation?.type;\n const hasWordTimings = layer.wordTimings && layer.wordTimings.length > 0;\n\n const needsWordTimings = ['wordByWord', 'characterKTV', 'wordByWordFancy'].includes(\n animationType || ''\n );\n\n if (needsWordTimings && !hasWordTimings) {\n renderBasicText(this.ctx, layer, this.width, this.height, this.currentFrame);\n return;\n }\n\n switch (animationType) {\n case 'wordByWord':\n renderWordByWord(this.ctx, layer, this.width, this.height, this.currentFrame, this.fps);\n break;\n case 'characterKTV':\n renderCharacterKTV(this.ctx, layer, this.width, this.height, this.currentFrame, this.fps);\n break;\n case 'wordByWordFancy':\n renderWordByWordFancy(\n this.ctx,\n layer,\n this.width,\n this.height,\n this.currentFrame,\n this.fps\n );\n break;\n case 'fade':\n renderTextWithEntrance(this.ctx, layer, this.width, this.height, this.currentFrame);\n break;\n default:\n renderBasicText(this.ctx, layer, this.width, this.height, this.currentFrame);\n break;\n }\n }\n\n private applyMask(mask: MaskConfig): void {\n this.ctx.globalCompositeOperation = mask.invert ? 'source-out' : 'destination-in';\n\n if (mask.source) {\n this.ctx.drawImage(mask.source, 0, 0, this.width, this.height);\n } else if (mask.shape === 'circle') {\n this.ctx.beginPath();\n this.ctx.arc(\n this.width / 2,\n this.height / 2,\n Math.min(this.width, this.height) / 2,\n 0,\n Math.PI * 2\n );\n this.ctx.fill();\n }\n }\n\n updateDimensions(width: number, height: number): void {\n this.width = width;\n this.height = height;\n this.ensureHighQualityRendering();\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"LayerRenderer.js","sources":["../../../src/stages/compose/LayerRenderer.ts"],"sourcesContent":["import type { Layer, VideoLayer, ImageLayer, TextLayer, Transform2D, MaskConfig } from './types';\nimport { renderBasicText, renderTextWithEntrance } from './text-renderers/basic-text-renderer';\nimport { renderWordByWord } from './text-renderers/word-by-word-renderer';\nimport { renderCharacterKTV } from './text-renderers/character-ktv-renderer';\nimport { renderWordByWordFancy } from './text-renderers/word-fancy-renderer';\n\n/**\n * LayerRenderer - Handles rendering of individual layers\n * Single responsibility: Draw a single layer to the canvas context\n */\nexport class LayerRenderer {\n private ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;\n private width: number;\n private height: number;\n private currentFrame: number = 0;\n private fps: number = 30;\n private readonly FILL_THRESHOLD = 0.9;\n\n constructor(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n width: number,\n height: number,\n fps: number = 30\n ) {\n this.ctx = ctx;\n this.width = width;\n this.height = height;\n this.fps = fps;\n this.ensureHighQualityRendering();\n }\n\n setCurrentFrame(frame: number): void {\n this.currentFrame = frame;\n }\n\n private ensureHighQualityRendering(): void {\n this.ctx.imageSmoothingEnabled = true;\n this.ctx.imageSmoothingQuality = 'high';\n }\n\n /**\n * Render a single layer with all its properties\n */\n renderLayer(layer: Layer): void {\n if (!layer.visible || layer.opacity <= 0) return;\n\n // Only save/restore context if layer has properties that need it\n const needsStateManagement =\n layer.opacity !== 1 || layer.blendMode || layer.transform || layer.mask;\n\n if (needsStateManagement) {\n this.ctx.save();\n }\n\n try {\n // Apply layer properties only when needed\n if (layer.opacity !== 1) {\n this.ctx.globalAlpha = layer.opacity;\n }\n\n if (layer.blendMode) {\n this.ctx.globalCompositeOperation = layer.blendMode;\n }\n\n if (layer.transform) {\n // Get layer dimensions for transform anchor calculation\n const layerDimensions = this.getLayerDimensions(layer);\n this.applyTransform(layer.transform, layerDimensions);\n }\n // Render based on layer type\n switch (layer.type) {\n case 'video':\n this.renderVideoLayer(layer as VideoLayer);\n break;\n case 'image':\n this.renderImageLayer(layer as ImageLayer);\n break;\n case 'text':\n this.renderTextLayer(layer as TextLayer);\n break;\n }\n\n // Apply mask if present\n if (layer.mask) {\n this.applyMask(layer.mask);\n }\n } finally {\n if (needsStateManagement) {\n this.ctx.restore();\n }\n }\n }\n\n private parseDimension(\n value: number | string | undefined,\n canvasSize: number\n ): number | undefined {\n if (value === undefined) return undefined;\n if (typeof value === 'number') return value;\n\n // value is string at this point\n const strValue = value as string;\n\n // Parse percentage string like \"5%\"\n if (strValue.includes('%')) {\n const numValue = parseFloat(strValue);\n return isNaN(numValue) ? undefined : (numValue / 100) * canvasSize;\n }\n\n // Parse as pixel value\n const parsed = parseFloat(strValue);\n return isNaN(parsed) ? undefined : parsed;\n }\n\n /**\n * Calculate dimensions from renderConfig\n * Returns dimensions maintaining aspect ratio when only one dimension is specified\n */\n private calculateDimensionsFromConfig(\n sourceWidth: number,\n sourceHeight: number,\n renderConfig: { width?: number | string; height?: number | string } | undefined\n ): { width: number; height: number } {\n if (!renderConfig) {\n return { width: sourceWidth, height: sourceHeight };\n }\n\n const width = this.parseDimension(renderConfig.width, this.width);\n const height = this.parseDimension(renderConfig.height, this.height);\n\n if (width && height) {\n return { width, height };\n } else if (width) {\n return {\n width,\n height: Math.round((sourceHeight / sourceWidth) * width),\n };\n } else if (height) {\n return {\n width: Math.round((sourceWidth / sourceHeight) * height),\n height,\n };\n } else {\n // renderConfig exists but empty, use original size\n return { width: sourceWidth, height: sourceHeight };\n }\n }\n\n private getLayerDimensions(layer: Layer): { width: number; height: number } {\n if (layer.type === 'image') {\n const imageLayer = layer as ImageLayer;\n if (imageLayer.source) {\n const imgWidth = imageLayer.source.width;\n const imgHeight = imageLayer.source.height;\n\n // Check if has valid renderConfig\n const hasValidRenderConfig = !!(\n imageLayer.renderConfig?.width !== undefined ||\n imageLayer.renderConfig?.height !== undefined\n );\n\n if (hasValidRenderConfig) {\n return this.calculateDimensionsFromConfig(imgWidth, imgHeight, imageLayer.renderConfig);\n }\n // No valid renderConfig: return original dimensions\n return { width: imgWidth, height: imgHeight };\n }\n } else if (layer.type === 'video') {\n const videoLayer = layer as VideoLayer;\n const videoFrame = videoLayer.videoFrame;\n const videoWidth = videoFrame.displayWidth || videoFrame.codedWidth;\n const videoHeight = videoFrame.displayHeight || videoFrame.codedHeight;\n\n // Check if has valid renderConfig\n const hasValidRenderConfig = !!(\n videoLayer.renderConfig?.width !== undefined ||\n videoLayer.renderConfig?.height !== undefined\n );\n\n if (hasValidRenderConfig) {\n return this.calculateDimensionsFromConfig(videoWidth, videoHeight, videoLayer.renderConfig);\n }\n // No valid renderConfig: return original dimensions\n return { width: videoWidth, height: videoHeight };\n }\n // Default to canvas dimensions\n return { width: this.width, height: this.height };\n }\n\n /**\n * Calculate render dimensions with smart fill logic\n * @param sourceWidth Source width\n * @param sourceHeight Source height\n * @param naturalScale Natural scale factor (scaleY for video, scaleX for image)\n * @returns Render dimensions and position\n */\n private calculateRenderDimensions(\n sourceWidth: number,\n sourceHeight: number,\n naturalScale: number\n ): { width: number; height: number; x: number; y: number } {\n const scaledWidth = sourceWidth * naturalScale;\n const scaledHeight = sourceHeight * naturalScale;\n\n // Smart fill: when scaled size is close to container (>90%), use cover mode\n const shouldFill =\n scaledWidth / this.width > this.FILL_THRESHOLD &&\n scaledHeight / this.height > this.FILL_THRESHOLD;\n\n let renderWidth: number;\n let renderHeight: number;\n\n if (shouldFill) {\n // Cover mode: use Math.max to ensure entire canvas is covered while maintaining aspect ratio\n const coverScale = Math.max(this.width / sourceWidth, this.height / sourceHeight);\n renderWidth = Math.round(sourceWidth * coverScale);\n renderHeight = Math.round(sourceHeight * coverScale);\n } else {\n // Natural scale mode: use scaled dimensions\n renderWidth = Math.round(scaledWidth);\n renderHeight = Math.round(scaledHeight);\n }\n\n // Center the content\n const renderX = Math.round((this.width - renderWidth) / 2);\n const renderY = Math.round((this.height - renderHeight) / 2);\n\n return { width: renderWidth, height: renderHeight, x: renderX, y: renderY };\n }\n\n private applyTransform(\n transform: Transform2D,\n layerDimensions: { width: number; height: number }\n ): void {\n // Use layer dimensions (not canvas dimensions) for anchor calculation\n const anchorX = transform.anchorX ?? 0.5;\n const anchorY = transform.anchorY ?? 0.5;\n const centerX = layerDimensions.width * anchorX;\n const centerY = layerDimensions.height * anchorY;\n\n // Move to the layer position + anchor offset\n this.ctx.translate(transform.x + centerX, transform.y + centerY);\n\n if (transform.rotation) {\n this.ctx.rotate(transform.rotation);\n }\n\n this.ctx.scale(transform.scaleX, transform.scaleY);\n\n if (transform.skewX || transform.skewY) {\n this.ctx.transform(1, transform.skewY ?? 0, transform.skewX ?? 0, 1, 0, 0);\n }\n\n // Move back by anchor offset\n this.ctx.translate(-centerX, -centerY);\n }\n\n private renderVideoLayer(layer: VideoLayer): void {\n const { videoFrame, crop, renderConfig } = layer;\n\n const videoWidth = videoFrame.displayWidth || videoFrame.codedWidth;\n const videoHeight = videoFrame.displayHeight || videoFrame.codedHeight;\n\n let renderX: number;\n let renderY: number;\n let renderWidth: number;\n let renderHeight: number;\n\n // Check if has valid renderConfig (with actual width or height values)\n const hasValidRenderConfig = !!(\n renderConfig?.width !== undefined || renderConfig?.height !== undefined\n );\n\n if (hasValidRenderConfig) {\n // Has valid renderConfig: explicit dimensions\n const dimensions = this.calculateDimensionsFromConfig(videoWidth, videoHeight, renderConfig);\n renderWidth = dimensions.width;\n renderHeight = dimensions.height;\n // Center the video\n renderX = Math.round((this.width - renderWidth) / 2);\n renderY = Math.round((this.height - renderHeight) / 2);\n } else {\n // No valid renderConfig: legacy smart fill (height-based)\n const naturalScale = this.height / videoHeight;\n const dimensions = this.calculateRenderDimensions(videoWidth, videoHeight, naturalScale);\n renderX = dimensions.x;\n renderY = dimensions.y;\n renderWidth = dimensions.width;\n renderHeight = dimensions.height;\n }\n\n if (crop) {\n this.ctx.drawImage(\n videoFrame,\n crop.x,\n crop.y,\n crop.width,\n crop.height,\n renderX,\n renderY,\n renderWidth,\n renderHeight\n );\n } else {\n this.ctx.drawImage(videoFrame, renderX, renderY, renderWidth, renderHeight);\n }\n // NOTE: Do not close videoFrame - it's managed by RcFrame wrapper\n }\n\n private renderImageLayer(layer: ImageLayer): void {\n const { source, crop, renderConfig, attachmentId } = layer;\n\n // Handle ImageData by putting it on canvas first\n if (source instanceof ImageData) {\n if (crop) {\n // For ImageData with crop, we need to extract the cropped region\n const tempCanvas = new OffscreenCanvas(crop.width, crop.height);\n const tempCtx = tempCanvas.getContext('2d')!;\n tempCtx.putImageData(source, -crop.x, -crop.y);\n this.ctx.drawImage(tempCanvas, 0, 0, this.width, this.height);\n } else {\n // Put ImageData directly\n this.ctx.putImageData(source, 0, 0);\n }\n return;\n }\n\n if (!source) return;\n\n const imgWidth = source.width;\n const imgHeight = source.height;\n\n let renderX: number;\n let renderY: number;\n let renderWidth: number;\n let renderHeight: number;\n\n // Check if has valid renderConfig (with actual width or height values)\n const hasValidRenderConfig = !!(\n renderConfig?.width !== undefined || renderConfig?.height !== undefined\n );\n\n if (hasValidRenderConfig) {\n // Has valid renderConfig: explicit dimensions\n const dimensions = this.calculateDimensionsFromConfig(imgWidth, imgHeight, renderConfig);\n renderWidth = dimensions.width;\n renderHeight = dimensions.height;\n // Images with renderConfig start at origin (for overlay positioning via transform)\n renderX = 0;\n renderY = 0;\n } else if (attachmentId) {\n // Attachment without valid renderConfig: use original size, start at origin\n renderWidth = imgWidth;\n renderHeight = imgHeight;\n renderX = 0;\n renderY = 0;\n } else {\n // No renderConfig and not attachment: legacy smart fill (width-based, main track)\n const naturalScale = this.width / imgWidth;\n const dimensions = this.calculateRenderDimensions(imgWidth, imgHeight, naturalScale);\n renderWidth = dimensions.width;\n renderHeight = dimensions.height;\n // Center the image\n renderX = Math.round((this.width - renderWidth) / 2);\n renderY = Math.round((this.height - renderHeight) / 2);\n }\n\n if (crop) {\n this.ctx.drawImage(\n source,\n crop.x,\n crop.y,\n crop.width,\n crop.height,\n renderX,\n renderY,\n renderWidth,\n renderHeight\n );\n } else {\n this.ctx.drawImage(source, renderX, renderY, renderWidth, renderHeight);\n }\n }\n\n private renderTextLayer(layer: TextLayer): void {\n const animationType = layer.animation?.type;\n const hasWordTimings = layer.wordTimings && layer.wordTimings.length > 0;\n\n const needsWordTimings = ['wordByWord', 'characterKTV', 'wordByWordFancy'].includes(\n animationType || ''\n );\n\n if (needsWordTimings && !hasWordTimings) {\n renderBasicText(this.ctx, layer, this.width, this.height, this.currentFrame);\n return;\n }\n\n switch (animationType) {\n case 'wordByWord':\n renderWordByWord(this.ctx, layer, this.width, this.height, this.currentFrame, this.fps);\n break;\n case 'characterKTV':\n renderCharacterKTV(this.ctx, layer, this.width, this.height, this.currentFrame, this.fps);\n break;\n case 'wordByWordFancy':\n renderWordByWordFancy(\n this.ctx,\n layer,\n this.width,\n this.height,\n this.currentFrame,\n this.fps\n );\n break;\n case 'fade':\n renderTextWithEntrance(this.ctx, layer, this.width, this.height, this.currentFrame);\n break;\n default:\n renderBasicText(this.ctx, layer, this.width, this.height, this.currentFrame);\n break;\n }\n }\n\n private applyMask(mask: MaskConfig): void {\n this.ctx.globalCompositeOperation = mask.invert ? 'source-out' : 'destination-in';\n\n if (mask.source) {\n this.ctx.drawImage(mask.source, 0, 0, this.width, this.height);\n } else if (mask.shape === 'circle') {\n this.ctx.beginPath();\n this.ctx.arc(\n this.width / 2,\n this.height / 2,\n Math.min(this.width, this.height) / 2,\n 0,\n Math.PI * 2\n );\n this.ctx.fill();\n }\n }\n\n updateDimensions(width: number, height: number): void {\n this.width = width;\n this.height = height;\n this.ensureHighQualityRendering();\n }\n}\n"],"names":[],"mappings":"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"}
@@ -875,14 +875,22 @@ class LayerRenderer {
875
875
  if (imageLayer.source) {
876
876
  const imgWidth = imageLayer.source.width;
877
877
  const imgHeight = imageLayer.source.height;
878
- return this.calculateDimensionsFromConfig(imgWidth, imgHeight, imageLayer.renderConfig);
878
+ const hasValidRenderConfig = !!(imageLayer.renderConfig?.width !== void 0 || imageLayer.renderConfig?.height !== void 0);
879
+ if (hasValidRenderConfig) {
880
+ return this.calculateDimensionsFromConfig(imgWidth, imgHeight, imageLayer.renderConfig);
881
+ }
882
+ return { width: imgWidth, height: imgHeight };
879
883
  }
880
884
  } else if (layer.type === "video") {
881
885
  const videoLayer = layer;
882
886
  const videoFrame = videoLayer.videoFrame;
883
887
  const videoWidth = videoFrame.displayWidth || videoFrame.codedWidth;
884
888
  const videoHeight = videoFrame.displayHeight || videoFrame.codedHeight;
885
- return this.calculateDimensionsFromConfig(videoWidth, videoHeight, videoLayer.renderConfig);
889
+ const hasValidRenderConfig = !!(videoLayer.renderConfig?.width !== void 0 || videoLayer.renderConfig?.height !== void 0);
890
+ if (hasValidRenderConfig) {
891
+ return this.calculateDimensionsFromConfig(videoWidth, videoHeight, videoLayer.renderConfig);
892
+ }
893
+ return { width: videoWidth, height: videoHeight };
886
894
  }
887
895
  return { width: this.width, height: this.height };
888
896
  }
@@ -934,7 +942,8 @@ class LayerRenderer {
934
942
  let renderY;
935
943
  let renderWidth;
936
944
  let renderHeight;
937
- if (renderConfig) {
945
+ const hasValidRenderConfig = !!(renderConfig?.width !== void 0 || renderConfig?.height !== void 0);
946
+ if (hasValidRenderConfig) {
938
947
  const dimensions = this.calculateDimensionsFromConfig(videoWidth, videoHeight, renderConfig);
939
948
  renderWidth = dimensions.width;
940
949
  renderHeight = dimensions.height;
@@ -965,7 +974,7 @@ class LayerRenderer {
965
974
  }
966
975
  }
967
976
  renderImageLayer(layer) {
968
- const { source, crop, renderConfig } = layer;
977
+ const { source, crop, renderConfig, attachmentId } = layer;
969
978
  if (source instanceof ImageData) {
970
979
  if (crop) {
971
980
  const tempCanvas = new OffscreenCanvas(crop.width, crop.height);
@@ -984,12 +993,18 @@ class LayerRenderer {
984
993
  let renderY;
985
994
  let renderWidth;
986
995
  let renderHeight;
987
- if (renderConfig) {
996
+ const hasValidRenderConfig = !!(renderConfig?.width !== void 0 || renderConfig?.height !== void 0);
997
+ if (hasValidRenderConfig) {
988
998
  const dimensions = this.calculateDimensionsFromConfig(imgWidth, imgHeight, renderConfig);
989
999
  renderWidth = dimensions.width;
990
1000
  renderHeight = dimensions.height;
991
1001
  renderX = 0;
992
1002
  renderY = 0;
1003
+ } else if (attachmentId) {
1004
+ renderWidth = imgWidth;
1005
+ renderHeight = imgHeight;
1006
+ renderX = 0;
1007
+ renderY = 0;
993
1008
  } else {
994
1009
  const naturalScale = this.width / imgWidth;
995
1010
  const dimensions = this.calculateRenderDimensions(imgWidth, imgHeight, naturalScale);
@@ -2029,12 +2044,18 @@ function materializeLayer(layer, frame, imageMap, globalTimeUs) {
2029
2044
  };
2030
2045
  if (layer.type === "video") {
2031
2046
  const payload = layer.payload;
2032
- return {
2047
+ const videoLayer = {
2033
2048
  ...baseLayer,
2034
2049
  type: "video",
2035
- videoFrame: frame,
2036
- renderConfig: payload.renderConfig
2050
+ videoFrame: frame
2037
2051
  };
2052
+ if (payload.renderConfig) {
2053
+ videoLayer.renderConfig = {
2054
+ ...payload.renderConfig.width !== void 0 && { width: payload.renderConfig.width },
2055
+ ...payload.renderConfig.height !== void 0 && { height: payload.renderConfig.height }
2056
+ };
2057
+ }
2058
+ return videoLayer;
2038
2059
  }
2039
2060
  if (layer.type === "text") {
2040
2061
  const payload = layer.payload;
@@ -2057,9 +2078,14 @@ function materializeLayer(layer, frame, imageMap, globalTimeUs) {
2057
2078
  ...baseLayer,
2058
2079
  type: "image",
2059
2080
  source,
2060
- attachmentId: payload.attachmentId,
2061
- renderConfig: payload.renderConfig
2081
+ attachmentId: payload.attachmentId
2062
2082
  };
2083
+ if (payload.renderConfig) {
2084
+ imageLayer.renderConfig = {
2085
+ ...payload.renderConfig.width !== void 0 && { width: payload.renderConfig.width },
2086
+ ...payload.renderConfig.height !== void 0 && { height: payload.renderConfig.height }
2087
+ };
2088
+ }
2063
2089
  if (payload.animation && globalTimeUs !== void 0) {
2064
2090
  const animState = computeAnimationState(payload.animation, globalTimeUs);
2065
2091
  if (!animState.visible) {
@@ -2461,4 +2487,4 @@ export {
2461
2487
  VideoComposeWorker,
2462
2488
  videoCompose_worker as default
2463
2489
  };
2464
- //# sourceMappingURL=video-compose.worker.C8728Oi3.js.map
2490
+ //# sourceMappingURL=video-compose.worker.Dieo8Oun.js.map