@meframe/core 0.2.1 → 0.2.3

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":"Meframe.d.ts","sourceRoot":"","sources":["../src/Meframe.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC1F,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACpF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAK5D,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,qBAAa,OAAO;IAClB,wDAAwD;IACxD,KAAK,EAAE,YAAY,CAAU;IAC7B,mDAAmD;IACnD,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,4DAA4D;IAC5D,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAQ;IACtC,oCAAoC;IACpC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,kDAAkD;IAClD,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,IAAI,GAAG,KAAK,GAAG,MAAM,CAAC,CAAC;IAExE,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,kBAAkB,CAAmC;IAC7D,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,MAAM,CAAC,CAAoB;IAEnC;;;OAGG;IACH,MAAM,CAAC,kBAAkB,IAAI,UAAU,CAAC,OAAO,yBAAyB,CAAC;IAIzE,OAAO;IAoBP;;OAEG;WACU,MAAM,CAAC,MAAM,GAAE,aAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAcjE;;OAEG;YACW,UAAU;IAyBxB;;OAEG;IACG,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAgCxF;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAcxD;;OAEG;IACH,YAAY,CAAC,OAAO,CAAC,EAAE;QACrB,MAAM,CAAC,EAAE,iBAAiB,CAAC;QAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB,GAAG,aAAa;IA6BjB;;;OAGG;IACG,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IA+CnD;;OAEG;IACH,YAAY,IAAI,IAAI;IAIpB;;OAEG;IACH,WAAW,IAAI,IAAI;IAInB;;OAEG;IACH,YAAY,IAAI,IAAI;IAIpB;;OAEG;IACH,cAAc,IAAI,OAAO;IAIzB;;OAEG;IACH,cAAc,IAAI,MAAM;IAIxB;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAYjC;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB9B;;OAEG;IACH,OAAO,CAAC,QAAQ;IAYhB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAM1B;;OAEG;IACH,OAAO,CAAC,WAAW;CAWpB"}
1
+ {"version":3,"file":"Meframe.d.ts","sourceRoot":"","sources":["../src/Meframe.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC1F,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACpF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAK5D,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,qBAAa,OAAO;IAClB,wDAAwD;IACxD,KAAK,EAAE,YAAY,CAAU;IAC7B,mDAAmD;IACnD,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,4DAA4D;IAC5D,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAQ;IACtC,oCAAoC;IACpC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,kDAAkD;IAClD,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,IAAI,GAAG,KAAK,GAAG,MAAM,CAAC,CAAC;IAExE,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,kBAAkB,CAAmC;IAC7D,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,MAAM,CAAC,CAAoB;IAEnC;;;OAGG;IACH,MAAM,CAAC,kBAAkB,IAAI,UAAU,CAAC,OAAO,yBAAyB,CAAC;IAIzE,OAAO;IAoBP;;OAEG;WACU,MAAM,CAAC,MAAM,GAAE,aAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAcjE;;OAEG;YACW,UAAU;IAyBxB;;OAEG;IACG,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBxF;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAcxD;;OAEG;IACH,YAAY,CAAC,OAAO,CAAC,EAAE;QACrB,MAAM,CAAC,EAAE,iBAAiB,CAAC;QAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB,GAAG,aAAa;IA6BjB;;;OAGG;IACG,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IA+CnD;;OAEG;IACH,YAAY,IAAI,IAAI;IAIpB;;OAEG;IACH,WAAW,IAAI,IAAI;IAInB;;OAEG;IACH,YAAY,IAAI,IAAI;IAIpB;;OAEG;IACH,cAAc,IAAI,OAAO;IAIzB;;OAEG;IACH,cAAc,IAAI,MAAM;IAIxB;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAYjC;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB9B;;OAEG;IACH,OAAO,CAAC,QAAQ;IAYhB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAM1B;;OAEG;IACH,OAAO,CAAC,WAAW;CAWpB"}
package/dist/Meframe.js CHANGED
@@ -86,11 +86,8 @@ class Meframe {
86
86
  */
87
87
  async setCompositionModel(model) {
88
88
  this.ensureNotDestroyed();
89
+ this.playbackController?.pause();
89
90
  const compositionModel = model instanceof CompositionModel ? model : new CompositionModel(model);
90
- const wasPlaying = this.playbackController?.isPlaying ?? false;
91
- if (wasPlaying) {
92
- this.playbackController?.pause();
93
- }
94
91
  await this.orchestrator.setCompositionModel(compositionModel);
95
92
  this.model = compositionModel;
96
93
  this.setState("ready");
@@ -102,9 +99,6 @@ class Meframe {
102
99
  ),
103
100
  durationUs: model.durationUs
104
101
  });
105
- if (wasPlaying) {
106
- this.playbackController?.play();
107
- }
108
102
  }
109
103
  /**
110
104
  * Apply a patch to the composition model
@@ -1 +1 @@
1
- {"version":3,"file":"Meframe.js","sources":["../src/Meframe.ts"],"sourcesContent":["/**\n * Meframe - Main entry point for the media processing framework\n */\n\nimport type { MeframeConfig, ResolvedConfig, MeframeState, ExportOptions } from './types';\nimport type { CompositionModelData, CompositionPatch, TimeUs } from './model/types';\nimport type { PreviewHandle } from './controllers/types';\nimport type { Plugin } from './plugins/types';\nimport { CompositionModel } from './model/CompositionModel';\nimport { loadConfig } from './config';\nimport { Orchestrator } from './orchestrator/Orchestrator';\nimport { PlaybackController } from './controllers/PlaybackController';\nimport { ExportController } from './controllers/ExportController';\nimport { PluginManager } from './plugins/PluginManager';\nimport { EventBus } from './event/EventBus';\nimport { MeframeEvent, type EventPayloadMap } from './event/events';\nimport { checkBrowserCompatibility } from './utils/platform-utils';\nimport { BrowserCompatibilityError } from './utils/errors';\n\nexport class Meframe {\n /** Current state - managed internally via setState() */\n state: MeframeState = 'idle';\n /** Configuration - immutable after construction */\n readonly config: ResolvedConfig;\n /** Composition model - managed via setCompositionModel() */\n model: CompositionModel | null = null;\n /** Plugin manager for extensions */\n readonly pluginManager: PluginManager;\n /** Event bus for subscribing to Meframe events */\n readonly events: Pick<EventBus<EventPayloadMap>, 'on' | 'off' | 'once'>;\n\n private orchestrator: Orchestrator;\n private eventBus: EventBus<EventPayloadMap>;\n private playbackController: PlaybackController | null = null;\n private exportController: ExportController;\n private canvas?: HTMLCanvasElement;\n\n /**\n * Check if current browser is compatible with Meframe\n * Returns compatibility info including browser name, version, and missing features\n */\n static checkCompatibility(): ReturnType<typeof checkBrowserCompatibility> {\n return checkBrowserCompatibility();\n }\n\n private constructor(config: ResolvedConfig) {\n this.config = config;\n this.eventBus = new EventBus<EventPayloadMap>();\n this.events = this.eventBus.asReadonly();\n this.pluginManager = new PluginManager(this);\n\n // Initialize orchestrator with configuration and shared eventBus\n // Worker paths are passed via config\n this.orchestrator = new Orchestrator({\n projectId: config.global.projectId,\n maxWorkers: (config as any).maxWorkers,\n cacheConfig: config.cache as any,\n eventBus: this.eventBus,\n workerPath: config.global.workerPath,\n workerExtension: config.global.workerExtension,\n });\n\n this.exportController = new ExportController(this.orchestrator);\n }\n\n /**\n * Create a new Meframe instance\n */\n static async create(config: MeframeConfig = {}): Promise<Meframe> {\n // Check browser compatibility first\n const compatibility = checkBrowserCompatibility();\n if (!compatibility.webCodecsAvailable || !compatibility.opfsAvailable) {\n throw new BrowserCompatibilityError(compatibility);\n }\n\n // Load and resolve configuration using ConfigLoader\n const resolvedConfig = await loadConfig({ override: config });\n const instance = new Meframe(resolvedConfig);\n await instance.initialize();\n return instance;\n }\n\n /**\n * Initialize the core engine\n */\n private async initialize(): Promise<void> {\n this.setState('loading');\n\n try {\n // Initialize orchestrator (sets up workers and cache)\n await this.orchestrator.initialize();\n\n // Initialize plugins if provided\n const plugins = (this.config as any).plugins;\n if (plugins && Array.isArray(plugins)) {\n for (const plugin of plugins) {\n // Ensure plugin type matches the Plugin interface\n if (plugin && typeof plugin === 'object' && 'name' in plugin && 'install' in plugin) {\n this.pluginManager.register(plugin as Plugin);\n }\n }\n }\n\n this.setState('idle');\n } catch (error) {\n this.setState('error');\n throw error;\n }\n }\n\n /**\n * Set the composition model\n */\n async setCompositionModel(model: CompositionModel | CompositionModelData): Promise<void> {\n this.ensureNotDestroyed();\n\n // Convert plain object to CompositionModel instance if needed\n const compositionModel =\n model instanceof CompositionModel ? model : new CompositionModel(model);\n\n const wasPlaying = this.playbackController?.isPlaying ?? false;\n if (wasPlaying) {\n this.playbackController?.pause();\n }\n\n // Set the model in orchestrator\n await this.orchestrator.setCompositionModel(compositionModel);\n this.model = compositionModel;\n\n this.setState('ready');\n this.eventBus.emit(MeframeEvent.Ready, {\n trackCount: model.tracks.length,\n clipCount: model.tracks.reduce(\n (acc: number, track: any) => acc + track.clips?.length || 0,\n 0\n ),\n durationUs: model.durationUs,\n });\n // await this.playbackController?.renderCover();\n\n if (wasPlaying) {\n this.playbackController?.play();\n }\n }\n\n /**\n * Apply a patch to the composition model\n */\n async applyPatch(patch: CompositionPatch): Promise<void> {\n this.ensureNotDestroyed();\n\n if (!this.model) {\n throw new Error('No composition model set');\n }\n\n // Apply patch through orchestrator\n await this.orchestrator.applyPatch(patch);\n\n // Patch is applied to the model by orchestrator\n this.model = this.orchestrator.compositionModel!;\n }\n\n /**\n * Start preview and return a handle for control\n */\n startPreview(options?: {\n canvas?: HTMLCanvasElement;\n startUs?: TimeUs;\n autoStart?: boolean;\n }): PreviewHandle {\n this.ensureReady();\n\n // Use provided canvas or the one from config\n const canvas = options?.canvas || this.canvas;\n if (!canvas) {\n throw new Error('Canvas is required for preview');\n }\n\n // Store canvas for later use\n this.canvas = canvas;\n\n // Create playback controller\n if (!this.playbackController) {\n this.playbackController = new PlaybackController(this.orchestrator as any, this.eventBus, {\n canvas,\n startUs: options?.startUs,\n autoStart: options?.autoStart,\n });\n }\n\n // Render initial frame\n this.playbackController.renderCover();\n this.playbackController.preheatNextWindow();\n\n // Return preview handle\n return this.playbackController;\n }\n\n /**\n * Export the composition\n * Uses ExportController for direct export\n */\n async export(options: ExportOptions): Promise<Blob> {\n this.ensureReady();\n this.playbackController?.pause();\n this.setState('exporting');\n\n try {\n const model = this.model;\n if (!model) {\n throw new Error('No composition model set');\n }\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 this.eventBus.emit(MeframeEvent.ExportStart, {\n format: options.format || 'mp4',\n width,\n height,\n fps,\n durationUs: model.durationUs,\n });\n\n // Delegate to ExportController\n const blob = await this.exportController.export(model, options);\n\n this.setState('ready');\n this.eventBus.emit(MeframeEvent.ExportComplete, {\n size: blob.size,\n durationMs: model.durationUs / 1000,\n format: options.format || 'mp4',\n });\n this.eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 1,\n });\n\n return blob;\n } catch (error) {\n this.setState('error');\n this.eventBus.emit(MeframeEvent.ExportError, {\n error: error as Error,\n stage: 'export',\n });\n throw error;\n }\n }\n\n /**\n * Cancel ongoing export operation\n */\n cancelExport(): void {\n this.exportController.cancel();\n }\n\n /**\n * Pause ongoing export operation\n */\n pauseExport(): void {\n this.exportController.pause();\n }\n\n /**\n * Resume paused export operation\n */\n resumeExport(): void {\n this.exportController.resume();\n }\n\n /**\n * Check if export is paused\n */\n isExportPaused(): boolean {\n return this.exportController.isPaused();\n }\n\n /**\n * Get current playback time\n */\n getCurrentTime(): number {\n return this.playbackController?.currentTimeUs || 0;\n }\n\n /**\n * Clear all L1/L2 cache data\n * Useful for debugging or forcing re-render\n */\n async clearCache(): Promise<void> {\n this.ensureReady();\n\n // Stop playback first\n this.playbackController?.stop();\n\n // Clear cache through CacheManager\n await this.orchestrator.cacheManager.clear();\n\n console.log('[Meframe] Cache cleared successfully');\n }\n\n /**\n * Clean up and destroy the instance\n */\n async dispose(): Promise<void> {\n if (this.state === 'destroyed') return;\n\n // Stop playback\n this.playbackController?.stop();\n this.playbackController?.dispose();\n this.playbackController = null;\n\n // Cleanup plugins\n await this.pluginManager.disposeAll();\n\n // Dispose orchestrator (stops workers and clears cache)\n await this.orchestrator.dispose();\n\n // Clear event bus\n this.eventBus.dispose();\n\n this.setState('destroyed');\n }\n\n /**\n * Set internal state\n */\n private setState(state: MeframeState): void {\n const oldState = this.state;\n this.state = state;\n\n // Emit state change event\n // Use a generic event for now, can be added to MeframeEvent enum later\n this.eventBus.emit('state:changed' as any, {\n oldState,\n newState: state,\n });\n }\n\n /**\n * Ensure the instance is not destroyed\n */\n private ensureNotDestroyed(): void {\n if (this.state === 'destroyed') {\n throw new Error('Core instance is destroyed');\n }\n }\n\n /**\n * Ensure the instance is ready\n */\n private ensureReady(): void {\n this.ensureNotDestroyed();\n\n if (!this.model) {\n throw new Error('No composition model set');\n }\n\n if (this.state === 'loading' || this.state === 'idle') {\n throw new Error('Core is not ready');\n }\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Meframe.js","sources":["../src/Meframe.ts"],"sourcesContent":["/**\n * Meframe - Main entry point for the media processing framework\n */\n\nimport type { MeframeConfig, ResolvedConfig, MeframeState, ExportOptions } from './types';\nimport type { CompositionModelData, CompositionPatch, TimeUs } from './model/types';\nimport type { PreviewHandle } from './controllers/types';\nimport type { Plugin } from './plugins/types';\nimport { CompositionModel } from './model/CompositionModel';\nimport { loadConfig } from './config';\nimport { Orchestrator } from './orchestrator/Orchestrator';\nimport { PlaybackController } from './controllers/PlaybackController';\nimport { ExportController } from './controllers/ExportController';\nimport { PluginManager } from './plugins/PluginManager';\nimport { EventBus } from './event/EventBus';\nimport { MeframeEvent, type EventPayloadMap } from './event/events';\nimport { checkBrowserCompatibility } from './utils/platform-utils';\nimport { BrowserCompatibilityError } from './utils/errors';\n\nexport class Meframe {\n /** Current state - managed internally via setState() */\n state: MeframeState = 'idle';\n /** Configuration - immutable after construction */\n readonly config: ResolvedConfig;\n /** Composition model - managed via setCompositionModel() */\n model: CompositionModel | null = null;\n /** Plugin manager for extensions */\n readonly pluginManager: PluginManager;\n /** Event bus for subscribing to Meframe events */\n readonly events: Pick<EventBus<EventPayloadMap>, 'on' | 'off' | 'once'>;\n\n private orchestrator: Orchestrator;\n private eventBus: EventBus<EventPayloadMap>;\n private playbackController: PlaybackController | null = null;\n private exportController: ExportController;\n private canvas?: HTMLCanvasElement;\n\n /**\n * Check if current browser is compatible with Meframe\n * Returns compatibility info including browser name, version, and missing features\n */\n static checkCompatibility(): ReturnType<typeof checkBrowserCompatibility> {\n return checkBrowserCompatibility();\n }\n\n private constructor(config: ResolvedConfig) {\n this.config = config;\n this.eventBus = new EventBus<EventPayloadMap>();\n this.events = this.eventBus.asReadonly();\n this.pluginManager = new PluginManager(this);\n\n // Initialize orchestrator with configuration and shared eventBus\n // Worker paths are passed via config\n this.orchestrator = new Orchestrator({\n projectId: config.global.projectId,\n maxWorkers: (config as any).maxWorkers,\n cacheConfig: config.cache as any,\n eventBus: this.eventBus,\n workerPath: config.global.workerPath,\n workerExtension: config.global.workerExtension,\n });\n\n this.exportController = new ExportController(this.orchestrator);\n }\n\n /**\n * Create a new Meframe instance\n */\n static async create(config: MeframeConfig = {}): Promise<Meframe> {\n // Check browser compatibility first\n const compatibility = checkBrowserCompatibility();\n if (!compatibility.webCodecsAvailable || !compatibility.opfsAvailable) {\n throw new BrowserCompatibilityError(compatibility);\n }\n\n // Load and resolve configuration using ConfigLoader\n const resolvedConfig = await loadConfig({ override: config });\n const instance = new Meframe(resolvedConfig);\n await instance.initialize();\n return instance;\n }\n\n /**\n * Initialize the core engine\n */\n private async initialize(): Promise<void> {\n this.setState('loading');\n\n try {\n // Initialize orchestrator (sets up workers and cache)\n await this.orchestrator.initialize();\n\n // Initialize plugins if provided\n const plugins = (this.config as any).plugins;\n if (plugins && Array.isArray(plugins)) {\n for (const plugin of plugins) {\n // Ensure plugin type matches the Plugin interface\n if (plugin && typeof plugin === 'object' && 'name' in plugin && 'install' in plugin) {\n this.pluginManager.register(plugin as Plugin);\n }\n }\n }\n\n this.setState('idle');\n } catch (error) {\n this.setState('error');\n throw error;\n }\n }\n\n /**\n * Set the composition model\n */\n async setCompositionModel(model: CompositionModel | CompositionModelData): Promise<void> {\n this.ensureNotDestroyed();\n\n this.playbackController?.pause();\n\n // Convert plain object to CompositionModel instance if needed\n const compositionModel =\n model instanceof CompositionModel ? model : new CompositionModel(model);\n\n // Set the model in orchestrator\n await this.orchestrator.setCompositionModel(compositionModel);\n this.model = compositionModel;\n\n this.setState('ready');\n this.eventBus.emit(MeframeEvent.Ready, {\n trackCount: model.tracks.length,\n clipCount: model.tracks.reduce(\n (acc: number, track: any) => acc + track.clips?.length || 0,\n 0\n ),\n durationUs: model.durationUs,\n });\n // await this.playbackController?.renderCover();\n }\n\n /**\n * Apply a patch to the composition model\n */\n async applyPatch(patch: CompositionPatch): Promise<void> {\n this.ensureNotDestroyed();\n\n if (!this.model) {\n throw new Error('No composition model set');\n }\n\n // Apply patch through orchestrator\n await this.orchestrator.applyPatch(patch);\n\n // Patch is applied to the model by orchestrator\n this.model = this.orchestrator.compositionModel!;\n }\n\n /**\n * Start preview and return a handle for control\n */\n startPreview(options?: {\n canvas?: HTMLCanvasElement;\n startUs?: TimeUs;\n autoStart?: boolean;\n }): PreviewHandle {\n this.ensureReady();\n\n // Use provided canvas or the one from config\n const canvas = options?.canvas || this.canvas;\n if (!canvas) {\n throw new Error('Canvas is required for preview');\n }\n\n // Store canvas for later use\n this.canvas = canvas;\n\n // Create playback controller\n if (!this.playbackController) {\n this.playbackController = new PlaybackController(this.orchestrator as any, this.eventBus, {\n canvas,\n startUs: options?.startUs,\n autoStart: options?.autoStart,\n });\n }\n\n // Render initial frame\n this.playbackController.renderCover();\n this.playbackController.preheatNextWindow();\n\n // Return preview handle\n return this.playbackController;\n }\n\n /**\n * Export the composition\n * Uses ExportController for direct export\n */\n async export(options: ExportOptions): Promise<Blob> {\n this.ensureReady();\n this.playbackController?.pause();\n this.setState('exporting');\n\n try {\n const model = this.model;\n if (!model) {\n throw new Error('No composition model set');\n }\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 this.eventBus.emit(MeframeEvent.ExportStart, {\n format: options.format || 'mp4',\n width,\n height,\n fps,\n durationUs: model.durationUs,\n });\n\n // Delegate to ExportController\n const blob = await this.exportController.export(model, options);\n\n this.setState('ready');\n this.eventBus.emit(MeframeEvent.ExportComplete, {\n size: blob.size,\n durationMs: model.durationUs / 1000,\n format: options.format || 'mp4',\n });\n this.eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 1,\n });\n\n return blob;\n } catch (error) {\n this.setState('error');\n this.eventBus.emit(MeframeEvent.ExportError, {\n error: error as Error,\n stage: 'export',\n });\n throw error;\n }\n }\n\n /**\n * Cancel ongoing export operation\n */\n cancelExport(): void {\n this.exportController.cancel();\n }\n\n /**\n * Pause ongoing export operation\n */\n pauseExport(): void {\n this.exportController.pause();\n }\n\n /**\n * Resume paused export operation\n */\n resumeExport(): void {\n this.exportController.resume();\n }\n\n /**\n * Check if export is paused\n */\n isExportPaused(): boolean {\n return this.exportController.isPaused();\n }\n\n /**\n * Get current playback time\n */\n getCurrentTime(): number {\n return this.playbackController?.currentTimeUs || 0;\n }\n\n /**\n * Clear all L1/L2 cache data\n * Useful for debugging or forcing re-render\n */\n async clearCache(): Promise<void> {\n this.ensureReady();\n\n // Stop playback first\n this.playbackController?.stop();\n\n // Clear cache through CacheManager\n await this.orchestrator.cacheManager.clear();\n\n console.log('[Meframe] Cache cleared successfully');\n }\n\n /**\n * Clean up and destroy the instance\n */\n async dispose(): Promise<void> {\n if (this.state === 'destroyed') return;\n\n // Stop playback\n this.playbackController?.stop();\n this.playbackController?.dispose();\n this.playbackController = null;\n\n // Cleanup plugins\n await this.pluginManager.disposeAll();\n\n // Dispose orchestrator (stops workers and clears cache)\n await this.orchestrator.dispose();\n\n // Clear event bus\n this.eventBus.dispose();\n\n this.setState('destroyed');\n }\n\n /**\n * Set internal state\n */\n private setState(state: MeframeState): void {\n const oldState = this.state;\n this.state = state;\n\n // Emit state change event\n // Use a generic event for now, can be added to MeframeEvent enum later\n this.eventBus.emit('state:changed' as any, {\n oldState,\n newState: state,\n });\n }\n\n /**\n * Ensure the instance is not destroyed\n */\n private ensureNotDestroyed(): void {\n if (this.state === 'destroyed') {\n throw new Error('Core instance is destroyed');\n }\n }\n\n /**\n * Ensure the instance is ready\n */\n private ensureReady(): void {\n this.ensureNotDestroyed();\n\n if (!this.model) {\n throw new Error('No composition model set');\n }\n\n if (this.state === 'loading' || this.state === 'idle') {\n throw new Error('Core is not ready');\n }\n }\n}\n"],"names":[],"mappings":"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"}
@@ -35,7 +35,10 @@ export declare class CacheManager {
35
35
  readonly audioSampleCache: AudioSampleCache;
36
36
  private clipReadyWaiters;
37
37
  private eventBus?;
38
+ isExporting: boolean;
38
39
  constructor(config: CacheManagerConfig, eventBus?: EventBus<EventPayloadMap>);
40
+ beginExport(): void;
41
+ endExport(): void;
39
42
  init(): Promise<void>;
40
43
  /**
41
44
  * Set window center for L1 cache management (unified for video and audio)
@@ -1 +1 @@
1
- {"version":3,"file":"CacheManager.d.ts","sourceRoot":"","sources":["../../src/cache/CacheManager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CacheManager.d.ts","sourceRoot":"","sources":["../../src/cache/CacheManager.ts"],"names":[],"mappings":"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"}
@@ -14,6 +14,7 @@ class CacheManager {
14
14
  audioSampleCache;
15
15
  clipReadyWaiters = /* @__PURE__ */ new Map();
16
16
  eventBus;
17
+ isExporting = false;
17
18
  constructor(config, eventBus) {
18
19
  this.videoL1Cache = new VideoL1Cache(config.l1);
19
20
  this.audioL1Cache = new AudioL1Cache();
@@ -23,6 +24,12 @@ class CacheManager {
23
24
  this.audioSampleCache = new AudioSampleCache();
24
25
  this.eventBus = eventBus;
25
26
  }
27
+ beginExport() {
28
+ this.isExporting = true;
29
+ }
30
+ endExport() {
31
+ this.isExporting = false;
32
+ }
26
33
  async init() {
27
34
  await this.resourceCache.init();
28
35
  }
@@ -1 +1 @@
1
- {"version":3,"file":"CacheManager.js","sources":["../../src/cache/CacheManager.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\nimport { RcFrame } from '../model';\nimport { VideoL1Cache } from './l1/VideoL1Cache';\nimport { MeframeEvent } from '../event/events';\nimport type { EventBus } from '../event/EventBus';\nimport type { EventPayloadMap } from '../event/events';\nimport {\n AudioL1Cache,\n EXPORT_PCM_COVERAGE_THRESHOLD,\n PREVIEW_PCM_COVERAGE_THRESHOLD,\n} from './l1/AudioL1Cache';\nimport { WaiterReplacedError } from '../utils/errors';\nimport { OPFSManager } from './storage/opfs/OPFSManager';\nimport { ResourceCache } from './resource/ResourceCache';\nimport { MP4IndexCache } from './resource/MP4IndexCache';\nimport { AudioSampleCache } from './resource/AudioSampleCache';\nimport type { MP4Index } from '../stages/demux/types';\n\ninterface CacheManagerConfig {\n l1: {\n maxMemoryMB: number;\n maxGOPs?: number;\n gopIntervalUs?: number;\n };\n resource: {\n projectId: string;\n };\n}\n\ninterface ClipReadyWaiter {\n clipId: string;\n minFrameCount: number;\n startTimeUs: TimeUs;\n currentCount: number;\n resolve: (ready: boolean) => void;\n reject: (reason?: unknown) => void;\n timeoutId?: ReturnType<typeof setTimeout>;\n}\n\n/**\n * CacheManager for Window Cache Architecture\n *\n * Core features:\n * - L1 (VRAM) for composed VideoFrames (window cache ±3s)\n * - ResourceCache (OPFS) for original MP4 files\n * - MP4IndexCache (RAM) for GOP/Sample indexes\n * - AudioSampleCache (RAM) for extracted audio chunks\n */\nexport class CacheManager {\n readonly videoL1Cache: VideoL1Cache;\n private readonly audioL1Cache: AudioL1Cache;\n readonly resourceCache: ResourceCache;\n readonly mp4IndexCache: MP4IndexCache;\n readonly audioSampleCache: AudioSampleCache;\n private clipReadyWaiters = new Map<string, ClipReadyWaiter>();\n private eventBus?: EventBus<EventPayloadMap>;\n\n constructor(config: CacheManagerConfig, eventBus?: EventBus<EventPayloadMap>) {\n this.videoL1Cache = new VideoL1Cache(config.l1);\n this.audioL1Cache = new AudioL1Cache();\n\n const opfsManager = new OPFSManager();\n this.resourceCache = new ResourceCache(opfsManager, config.resource.projectId);\n this.mp4IndexCache = new MP4IndexCache();\n this.audioSampleCache = new AudioSampleCache();\n this.eventBus = eventBus;\n }\n\n async init(): Promise<void> {\n await this.resourceCache.init();\n }\n\n /**\n * Set window center for L1 cache management (unified for video and audio)\n * L1 cache uses center ±3.5s window for video, ±5s for audio\n */\n setWindow(centerTimeUs: TimeUs): void {\n this.videoL1Cache.setWindow(centerTimeUs);\n // this.audioL1Cache.setWindow(centerTimeUs);\n }\n\n /**\n * Check if resource exists in OPFS cache\n */\n async hasResourceInCache(resourceId: string): Promise<boolean> {\n return await this.resourceCache.hasResource(resourceId);\n }\n\n /**\n * Read byte range from resource (for GOP-level access)\n */\n async readResourceRange(resourceId: string, start: number, end: number): Promise<ArrayBuffer> {\n return await this.resourceCache.readRange(resourceId, start, end);\n }\n\n /**\n * Get MP4 index for a resource\n */\n getMP4Index(resourceId: string): MP4Index | null {\n return this.mp4IndexCache.get(resourceId);\n }\n\n putClipAudioData(\n clipId: string,\n audioData: AudioData,\n clipDurationUs: TimeUs,\n globalTimeUs?: TimeUs\n ): void {\n this.audioL1Cache.putClipAudioData(clipId, audioData, clipDurationUs, globalTimeUs);\n }\n\n getClipPCMWithMetadata(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs\n ): { planes: Float32Array[]; sampleRate: number; numberOfChannels: number } | null {\n return this.audioL1Cache.getPCMWithMetadata(clipId, startUs, endUs);\n }\n\n hasClipPCM(clipId: string): boolean {\n return this.audioL1Cache.hasClipPCM(clipId);\n }\n\n /**\n * Check if a specific audio chunk is already cached (by timestamp)\n */\n hasAudioSlotAt(clipId: string, timestampUs: TimeUs): boolean {\n return this.audioL1Cache.hasSlotAt(clipId, timestampUs);\n }\n\n /**\n * Check if sufficient PCM data exists for the requested time window\n * Returns true only if at least threshold (default 80%) of requested duration is available\n */\n hasWindowPCM(clipId: string, startUs: TimeUs, endUs: TimeUs, strictMode?: boolean): boolean {\n const threshold = strictMode ? EXPORT_PCM_COVERAGE_THRESHOLD : PREVIEW_PCM_COVERAGE_THRESHOLD;\n return this.audioL1Cache.getAudioRangeCoverage(clipId, startUs, endUs, threshold).covered;\n }\n\n /**\n * Get audio range coverage information for incremental decoding\n * @param clipId - Clip identifier\n * @param startUs - Range start time in microseconds\n * @param endUs - Range end time in microseconds\n * @param threshold - Coverage threshold (0-1)\n * @returns Coverage info { covered: boolean, coverageRatio: number }\n */\n getAudioRangeCoverage(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs,\n threshold?: number\n ): { covered: boolean; coverageRatio: number } {\n return this.audioL1Cache.getAudioRangeCoverage(clipId, startUs, endUs, threshold);\n }\n\n clearAudioCache(): void {\n this.audioL1Cache.clear();\n }\n\n clearClipAudioData(clipId: string): void {\n this.audioL1Cache.clearClipPCM(clipId);\n }\n\n /**\n * Add a frame to L1 cache\n * Handles event notifications (CacheCover, ComposeFrameReady)\n * @param frame - VideoFrame to add\n * @param clipId - Clip identifier\n * @param frameDuration - Frame duration in microseconds\n * @param trackId - Track identifier\n * @param globalTimeUs - Global timestamp for event emission and window management\n */\n addFrame(\n frame: VideoFrame,\n clipId: string,\n frameDuration: TimeUs,\n trackId: string,\n globalTimeUs: TimeUs\n ): RcFrame {\n const rcFrame = this.videoL1Cache.addFrame(frame, clipId, frameDuration, trackId, globalTimeUs);\n\n const relativeTimeUs = frame.timestamp ?? 0;\n\n // Check and notify clip ready\n this.checkAndNotifyClipReady(clipId, relativeTimeUs);\n\n // Emit cover event for first frame (globalTimeUs = 0 in composition)\n if (globalTimeUs === 0) {\n this.eventBus?.emit(MeframeEvent.CacheCover, {\n timeUs: globalTimeUs,\n clipId,\n level: 'L1',\n size: rcFrame.sizeEstimate ?? 0,\n });\n }\n\n // Emit frame ready event\n this.eventBus?.emit(MeframeEvent.ComposeFrameReady, {\n timeUs: globalTimeUs,\n frameNumber: Math.floor(globalTimeUs / frameDuration),\n renderTimeMs: 0,\n trackId,\n clipId,\n });\n\n return rcFrame;\n }\n\n /**\n * Get frame from L1 cache\n * @param timeUs - Clip-relative timestamp (0-based)\n * @param clipId - Clip identifier\n */\n getFrame(timeUs: TimeUs, clipId: string): RcFrame | null {\n return this.videoL1Cache.get(timeUs, clipId);\n }\n\n async invalidateClip(clipId: string): Promise<void> {\n this.videoL1Cache.invalidateClip(clipId);\n }\n\n /**\n * Evict a clip from L1 cache\n */\n invalidateClipInL1(clipId: string): void {\n this.videoL1Cache.invalidateClip(clipId);\n }\n\n /**\n * Check if a clip is cached in L1\n */\n hasClipInL1(clipId: string): boolean {\n return this.videoL1Cache.hasClip(clipId);\n }\n\n /**\n * Wait for a clip to have minimum frames cached\n * Used by PlaybackController for buffering state\n * Only one waiter per clip - new waiter replaces old one\n */\n waitForClipReady(\n clipId: string,\n options: { minFrameCount?: number; timeoutMs?: number; startTimeUs?: TimeUs } = {}\n ): Promise<boolean> {\n const minFrameCount = options.minFrameCount ?? 30;\n const startTimeUs = options.startTimeUs ?? 0;\n\n // Check if already have enough frames\n const currentFrameCount = this.videoL1Cache.getClipFrameCount(clipId, startTimeUs);\n if (currentFrameCount >= minFrameCount) {\n return Promise.resolve(true);\n }\n\n // Cancel previous waiter if exists\n const oldWaiter = this.clipReadyWaiters.get(clipId);\n if (oldWaiter) {\n if (oldWaiter.timeoutId) {\n clearTimeout(oldWaiter.timeoutId);\n }\n oldWaiter.reject(new WaiterReplacedError(clipId));\n }\n\n return new Promise<boolean>((resolve, reject) => {\n const waiter: ClipReadyWaiter = {\n clipId,\n minFrameCount,\n startTimeUs,\n currentCount: currentFrameCount,\n resolve,\n reject,\n };\n\n this.clipReadyWaiters.set(clipId, waiter);\n\n if (options.timeoutMs && options.timeoutMs > 0) {\n waiter.timeoutId = setTimeout(() => {\n if (this.clipReadyWaiters.get(clipId) === waiter) {\n this.clipReadyWaiters.delete(clipId);\n }\n resolve(false);\n }, options.timeoutMs);\n }\n });\n }\n\n clearVideoCache(): void {\n this.videoL1Cache.clear();\n }\n\n clear(): void {\n this.clearVideoCache();\n this.clearAudioCache();\n }\n\n getMetadata() {\n return {\n l1: this.videoL1Cache.getMetadata(),\n };\n }\n\n /**\n * Check if incoming frame satisfies clip ready condition\n * O(1) complexity - only checks single waiter and increments counter\n */\n checkAndNotifyClipReady(clipId: string, frameTimestampUs: TimeUs): void {\n const waiter = this.clipReadyWaiters.get(clipId);\n if (!waiter) {\n return;\n }\n\n // Count all frames if startTimeUs is 0 (buffering scenario)\n // Otherwise only count frames at or after the target start time\n const shouldCount = waiter.startTimeUs === 0 || frameTimestampUs >= waiter.startTimeUs;\n\n if (shouldCount) {\n waiter.currentCount++;\n\n if (waiter.currentCount >= waiter.minFrameCount) {\n if (waiter.timeoutId) {\n clearTimeout(waiter.timeoutId);\n }\n waiter.resolve(true);\n this.clipReadyWaiters.delete(clipId);\n }\n }\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CacheManager.js","sources":["../../src/cache/CacheManager.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\nimport { RcFrame } from '../model';\nimport { VideoL1Cache } from './l1/VideoL1Cache';\nimport { MeframeEvent } from '../event/events';\nimport type { EventBus } from '../event/EventBus';\nimport type { EventPayloadMap } from '../event/events';\nimport {\n AudioL1Cache,\n EXPORT_PCM_COVERAGE_THRESHOLD,\n PREVIEW_PCM_COVERAGE_THRESHOLD,\n} from './l1/AudioL1Cache';\nimport { WaiterReplacedError } from '../utils/errors';\nimport { OPFSManager } from './storage/opfs/OPFSManager';\nimport { ResourceCache } from './resource/ResourceCache';\nimport { MP4IndexCache } from './resource/MP4IndexCache';\nimport { AudioSampleCache } from './resource/AudioSampleCache';\nimport type { MP4Index } from '../stages/demux/types';\n\ninterface CacheManagerConfig {\n l1: {\n maxMemoryMB: number;\n maxGOPs?: number;\n gopIntervalUs?: number;\n };\n resource: {\n projectId: string;\n };\n}\n\ninterface ClipReadyWaiter {\n clipId: string;\n minFrameCount: number;\n startTimeUs: TimeUs;\n currentCount: number;\n resolve: (ready: boolean) => void;\n reject: (reason?: unknown) => void;\n timeoutId?: ReturnType<typeof setTimeout>;\n}\n\n/**\n * CacheManager for Window Cache Architecture\n *\n * Core features:\n * - L1 (VRAM) for composed VideoFrames (window cache ±3s)\n * - ResourceCache (OPFS) for original MP4 files\n * - MP4IndexCache (RAM) for GOP/Sample indexes\n * - AudioSampleCache (RAM) for extracted audio chunks\n */\nexport class CacheManager {\n readonly videoL1Cache: VideoL1Cache;\n private readonly audioL1Cache: AudioL1Cache;\n readonly resourceCache: ResourceCache;\n readonly mp4IndexCache: MP4IndexCache;\n readonly audioSampleCache: AudioSampleCache;\n private clipReadyWaiters = new Map<string, ClipReadyWaiter>();\n private eventBus?: EventBus<EventPayloadMap>;\n isExporting = false;\n\n constructor(config: CacheManagerConfig, eventBus?: EventBus<EventPayloadMap>) {\n this.videoL1Cache = new VideoL1Cache(config.l1);\n this.audioL1Cache = new AudioL1Cache();\n\n const opfsManager = new OPFSManager();\n this.resourceCache = new ResourceCache(opfsManager, config.resource.projectId);\n this.mp4IndexCache = new MP4IndexCache();\n this.audioSampleCache = new AudioSampleCache();\n this.eventBus = eventBus;\n }\n\n beginExport(): void {\n this.isExporting = true;\n }\n\n endExport(): void {\n this.isExporting = false;\n }\n\n async init(): Promise<void> {\n await this.resourceCache.init();\n }\n\n /**\n * Set window center for L1 cache management (unified for video and audio)\n * L1 cache uses center ±3.5s window for video, ±5s for audio\n */\n setWindow(centerTimeUs: TimeUs): void {\n this.videoL1Cache.setWindow(centerTimeUs);\n // this.audioL1Cache.setWindow(centerTimeUs);\n }\n\n /**\n * Check if resource exists in OPFS cache\n */\n async hasResourceInCache(resourceId: string): Promise<boolean> {\n return await this.resourceCache.hasResource(resourceId);\n }\n\n /**\n * Read byte range from resource (for GOP-level access)\n */\n async readResourceRange(resourceId: string, start: number, end: number): Promise<ArrayBuffer> {\n return await this.resourceCache.readRange(resourceId, start, end);\n }\n\n /**\n * Get MP4 index for a resource\n */\n getMP4Index(resourceId: string): MP4Index | null {\n return this.mp4IndexCache.get(resourceId);\n }\n\n putClipAudioData(\n clipId: string,\n audioData: AudioData,\n clipDurationUs: TimeUs,\n globalTimeUs?: TimeUs\n ): void {\n this.audioL1Cache.putClipAudioData(clipId, audioData, clipDurationUs, globalTimeUs);\n }\n\n getClipPCMWithMetadata(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs\n ): { planes: Float32Array[]; sampleRate: number; numberOfChannels: number } | null {\n return this.audioL1Cache.getPCMWithMetadata(clipId, startUs, endUs);\n }\n\n hasClipPCM(clipId: string): boolean {\n return this.audioL1Cache.hasClipPCM(clipId);\n }\n\n /**\n * Check if a specific audio chunk is already cached (by timestamp)\n */\n hasAudioSlotAt(clipId: string, timestampUs: TimeUs): boolean {\n return this.audioL1Cache.hasSlotAt(clipId, timestampUs);\n }\n\n /**\n * Check if sufficient PCM data exists for the requested time window\n * Returns true only if at least threshold (default 80%) of requested duration is available\n */\n hasWindowPCM(clipId: string, startUs: TimeUs, endUs: TimeUs, strictMode?: boolean): boolean {\n const threshold = strictMode ? EXPORT_PCM_COVERAGE_THRESHOLD : PREVIEW_PCM_COVERAGE_THRESHOLD;\n return this.audioL1Cache.getAudioRangeCoverage(clipId, startUs, endUs, threshold).covered;\n }\n\n /**\n * Get audio range coverage information for incremental decoding\n * @param clipId - Clip identifier\n * @param startUs - Range start time in microseconds\n * @param endUs - Range end time in microseconds\n * @param threshold - Coverage threshold (0-1)\n * @returns Coverage info { covered: boolean, coverageRatio: number }\n */\n getAudioRangeCoverage(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs,\n threshold?: number\n ): { covered: boolean; coverageRatio: number } {\n return this.audioL1Cache.getAudioRangeCoverage(clipId, startUs, endUs, threshold);\n }\n\n clearAudioCache(): void {\n this.audioL1Cache.clear();\n }\n\n clearClipAudioData(clipId: string): void {\n this.audioL1Cache.clearClipPCM(clipId);\n }\n\n /**\n * Add a frame to L1 cache\n * Handles event notifications (CacheCover, ComposeFrameReady)\n * @param frame - VideoFrame to add\n * @param clipId - Clip identifier\n * @param frameDuration - Frame duration in microseconds\n * @param trackId - Track identifier\n * @param globalTimeUs - Global timestamp for event emission and window management\n */\n addFrame(\n frame: VideoFrame,\n clipId: string,\n frameDuration: TimeUs,\n trackId: string,\n globalTimeUs: TimeUs\n ): RcFrame {\n const rcFrame = this.videoL1Cache.addFrame(frame, clipId, frameDuration, trackId, globalTimeUs);\n\n const relativeTimeUs = frame.timestamp ?? 0;\n\n // Check and notify clip ready\n this.checkAndNotifyClipReady(clipId, relativeTimeUs);\n\n // Emit cover event for first frame (globalTimeUs = 0 in composition)\n if (globalTimeUs === 0) {\n this.eventBus?.emit(MeframeEvent.CacheCover, {\n timeUs: globalTimeUs,\n clipId,\n level: 'L1',\n size: rcFrame.sizeEstimate ?? 0,\n });\n }\n\n // Emit frame ready event\n this.eventBus?.emit(MeframeEvent.ComposeFrameReady, {\n timeUs: globalTimeUs,\n frameNumber: Math.floor(globalTimeUs / frameDuration),\n renderTimeMs: 0,\n trackId,\n clipId,\n });\n\n return rcFrame;\n }\n\n /**\n * Get frame from L1 cache\n * @param timeUs - Clip-relative timestamp (0-based)\n * @param clipId - Clip identifier\n */\n getFrame(timeUs: TimeUs, clipId: string): RcFrame | null {\n return this.videoL1Cache.get(timeUs, clipId);\n }\n\n async invalidateClip(clipId: string): Promise<void> {\n this.videoL1Cache.invalidateClip(clipId);\n }\n\n /**\n * Evict a clip from L1 cache\n */\n invalidateClipInL1(clipId: string): void {\n this.videoL1Cache.invalidateClip(clipId);\n }\n\n /**\n * Check if a clip is cached in L1\n */\n hasClipInL1(clipId: string): boolean {\n return this.videoL1Cache.hasClip(clipId);\n }\n\n /**\n * Wait for a clip to have minimum frames cached\n * Used by PlaybackController for buffering state\n * Only one waiter per clip - new waiter replaces old one\n */\n waitForClipReady(\n clipId: string,\n options: { minFrameCount?: number; timeoutMs?: number; startTimeUs?: TimeUs } = {}\n ): Promise<boolean> {\n const minFrameCount = options.minFrameCount ?? 30;\n const startTimeUs = options.startTimeUs ?? 0;\n\n // Check if already have enough frames\n const currentFrameCount = this.videoL1Cache.getClipFrameCount(clipId, startTimeUs);\n if (currentFrameCount >= minFrameCount) {\n return Promise.resolve(true);\n }\n\n // Cancel previous waiter if exists\n const oldWaiter = this.clipReadyWaiters.get(clipId);\n if (oldWaiter) {\n if (oldWaiter.timeoutId) {\n clearTimeout(oldWaiter.timeoutId);\n }\n oldWaiter.reject(new WaiterReplacedError(clipId));\n }\n\n return new Promise<boolean>((resolve, reject) => {\n const waiter: ClipReadyWaiter = {\n clipId,\n minFrameCount,\n startTimeUs,\n currentCount: currentFrameCount,\n resolve,\n reject,\n };\n\n this.clipReadyWaiters.set(clipId, waiter);\n\n if (options.timeoutMs && options.timeoutMs > 0) {\n waiter.timeoutId = setTimeout(() => {\n if (this.clipReadyWaiters.get(clipId) === waiter) {\n this.clipReadyWaiters.delete(clipId);\n }\n resolve(false);\n }, options.timeoutMs);\n }\n });\n }\n\n clearVideoCache(): void {\n this.videoL1Cache.clear();\n }\n\n clear(): void {\n this.clearVideoCache();\n this.clearAudioCache();\n }\n\n getMetadata() {\n return {\n l1: this.videoL1Cache.getMetadata(),\n };\n }\n\n /**\n * Check if incoming frame satisfies clip ready condition\n * O(1) complexity - only checks single waiter and increments counter\n */\n checkAndNotifyClipReady(clipId: string, frameTimestampUs: TimeUs): void {\n const waiter = this.clipReadyWaiters.get(clipId);\n if (!waiter) {\n return;\n }\n\n // Count all frames if startTimeUs is 0 (buffering scenario)\n // Otherwise only count frames at or after the target start time\n const shouldCount = waiter.startTimeUs === 0 || frameTimestampUs >= waiter.startTimeUs;\n\n if (shouldCount) {\n waiter.currentCount++;\n\n if (waiter.currentCount >= waiter.minFrameCount) {\n if (waiter.timeoutId) {\n clearTimeout(waiter.timeoutId);\n }\n waiter.resolve(true);\n this.clipReadyWaiters.delete(clipId);\n }\n }\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"PlaybackController.d.ts","sourceRoot":"","sources":["../../src/controllers/PlaybackController.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,eAAe,EACf,SAAS,EACT,aAAa,EACb,MAAM,EACP,MAAM,SAAS,CAAC;AAUjB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AASpD;;;GAGG;AACH,qBAAa,kBAAmB,YAAW,mBAAmB,EAAE,aAAa;IAC3E,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAY;IAC5B,OAAO,CAAC,MAAM,CAAsC;IACpD,OAAO,CAAC,aAAa,CAA8B;IAGnD,aAAa,EAAE,MAAM,CAAK;IAC1B,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,MAAM,CAAO;IACrB,OAAO,CAAC,IAAI,CAAS;IAGrB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,WAAW,CAAa;IAGhC,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,GAAG,CAAK;IAGhB,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAW;IAGnD,OAAO,CAAC,GAAG,CAA8B;IAGzC,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAa;IAC7C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAa;IAC9C,OAAO,CAAC,iBAAiB,CAAS;gBAEtB,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe;IAoC/E,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAMlC,IAAI,IAAI,IAAI;IAIZ,KAAK,IAAI,IAAI;IAIb,IAAI,IAAI,IAAI;IAIN,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASzC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAW3B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAM/B,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAU7B,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAI5B,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,MAAM,IAAI,IAAI;IAId,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAIxD,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAMzD,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,eAAe;IAKvB,OAAO,CAAC,UAAU;IAkBlB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,cAAc;IAoLtB,OAAO,CAAC,SAAS;IAOjB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,iBAAiB;YAMX,SAAS;IAwEvB,OAAO,CAAC,SAAS;IAUjB,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,qBAAqB;IAgBvB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;YAmC1B,kBAAkB;YAqBlB,OAAO;IAYrB,OAAO,CAAC,WAAW;IAYnB,OAAO,IAAI,IAAI;IAUf,OAAO,CAAC,YAAY,CAIlB;IAEF,OAAO,CAAC,UAAU,CAkBhB;IAEF,OAAO,CAAC,mBAAmB;IAK3B,OAAO,CAAC,0BAA0B;CAUnC"}
1
+ {"version":3,"file":"PlaybackController.d.ts","sourceRoot":"","sources":["../../src/controllers/PlaybackController.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,eAAe,EACf,SAAS,EACT,aAAa,EACb,MAAM,EACP,MAAM,SAAS,CAAC;AAUjB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AASpD;;;GAGG;AACH,qBAAa,kBAAmB,YAAW,mBAAmB,EAAE,aAAa;IAC3E,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAY;IAC5B,OAAO,CAAC,MAAM,CAAsC;IACpD,OAAO,CAAC,aAAa,CAA8B;IAGnD,aAAa,EAAE,MAAM,CAAK;IAC1B,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,MAAM,CAAO;IACrB,OAAO,CAAC,IAAI,CAAS;IAGrB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,WAAW,CAAa;IAGhC,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,GAAG,CAAK;IAGhB,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAW;IAGnD,OAAO,CAAC,GAAG,CAA8B;IAGzC,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAa;IAC7C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAa;IAC9C,OAAO,CAAC,iBAAiB,CAAS;gBAEtB,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe;IAoC/E,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAMlC,IAAI,IAAI,IAAI;IAIZ,KAAK,IAAI,IAAI;IAIb,IAAI,IAAI,IAAI;IAIN,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASzC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAW3B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAM/B,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAU7B,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAI5B,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,MAAM,IAAI,IAAI;IAId,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAIxD,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAMzD,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,eAAe;IAKvB,OAAO,CAAC,UAAU;IAkBlB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,cAAc;IAoLtB,OAAO,CAAC,SAAS;IAOjB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,iBAAiB;YAMX,SAAS;IAwEvB,OAAO,CAAC,SAAS;IAUjB,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,qBAAqB;IAgBvB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;YAmC1B,kBAAkB;YAqBlB,OAAO;IAYrB,OAAO,CAAC,WAAW;IAYnB,OAAO,IAAI,IAAI;IAUf,OAAO,CAAC,YAAY,CAIlB;IAEF,OAAO,CAAC,UAAU,CAoBhB;IAEF,OAAO,CAAC,mBAAmB;IAK3B,OAAO,CAAC,0BAA0B;CAUnC"}
@@ -492,7 +492,9 @@ class PlaybackController {
492
492
  fps: model.fps || 30,
493
493
  backgroundColor: model.renderConfig?.backgroundColor || "#000"
494
494
  });
495
- void this.audioSession.ensureAudioForTime(this.currentTimeUs, { mode: "probe" });
495
+ if (!this.orchestrator.cacheManager.isExporting) {
496
+ void this.audioSession.ensureAudioForTime(this.currentTimeUs, { mode: "probe" });
497
+ }
496
498
  void this.renderCurrentFrame(this.currentTimeUs, { mode: "blocking" });
497
499
  };
498
500
  setupEventListeners() {
@@ -1 +1 @@
1
- {"version":3,"file":"PlaybackController.js","sources":["../../src/controllers/PlaybackController.ts"],"sourcesContent":["import type {\n IPlaybackController,\n PlaybackOptions,\n IEventBus,\n PreviewHandle,\n TimeUs,\n} from './types';\nimport {\n PlaybackActionType,\n PlaybackCommandType,\n PlaybackState,\n type OpToken,\n type PlaybackAction,\n type PlaybackCommand,\n} from './types';\nimport { MeframeEvent } from '../event/events';\nimport type { Orchestrator } from '../orchestrator';\nimport type { RequestMode } from '../orchestrator/types';\nimport { WaiterReplacedError } from '../utils/errors';\nimport { VideoComposer } from '../stages/compose/VideoComposer';\nimport { isVideoClip } from '../model/types';\nimport type { AudioPreviewSession } from '../orchestrator/AudioPreviewSession';\n\nimport { PlaybackStateMachine } from './PlaybackStateMachine';\n\n/**\n * Playback controller for preview\n * Internal implementation - not exposed directly to external consumers\n */\nexport class PlaybackController implements IPlaybackController, PreviewHandle {\n private orchestrator: Orchestrator;\n private eventBus: IEventBus;\n private canvas: HTMLCanvasElement | OffscreenCanvas;\n private videoComposer: VideoComposer | null = null;\n\n // Playback time (external)\n currentTimeUs: TimeUs = 0;\n private playbackRate = 1.0;\n private volume = 1.0;\n private loop = false;\n\n // Time base\n private rafId: number | null = null;\n private startTimeUs: TimeUs = 0; // AudioContext timeline origin (microseconds)\n\n // Frame stats\n private frameCount = 0;\n private lastFrameTime = 0;\n private fps = 0;\n\n // Audio\n private audioContext: AudioContext;\n private audioSession: AudioPreviewSession;\n private lastAudioScheduleTime = 0;\n private readonly AUDIO_SCHEDULE_INTERVAL = 250_000; // 250ms\n\n // State machine\n private fsm = new PlaybackStateMachine();\n\n // Unified window management for both video and audio\n private windowEnd: TimeUs = 0;\n private readonly WINDOW_DURATION = 3_000_000; // 3s decode window\n private readonly PREHEAT_DISTANCE = 1_000_000; // 1s preheat trigger distance\n private preheatInProgress = false;\n\n constructor(orchestrator: Orchestrator, eventBus: IEventBus, options: PlaybackOptions) {\n this.orchestrator = orchestrator;\n this.audioSession = orchestrator.audio.preview;\n this.eventBus = eventBus;\n this.canvas = options.canvas;\n this.audioContext = new AudioContext();\n\n const model = orchestrator.compositionModel;\n const width = model?.renderConfig?.width || this.canvas.width || 720;\n const height = model?.renderConfig?.height || this.canvas.height || 1280;\n\n this.videoComposer = new VideoComposer({\n width,\n height,\n fps: model?.fps || 30,\n backgroundColor: model?.renderConfig?.backgroundColor || '#000',\n externalCanvas: this.canvas,\n });\n\n if (options.startUs !== undefined) {\n this.currentTimeUs = options.startUs;\n }\n if (options.rate !== undefined) {\n this.playbackRate = options.rate;\n }\n if (options.loop !== undefined) {\n this.loop = options.loop;\n }\n\n this.setupEventListeners();\n\n if (options.autoStart) {\n this.play();\n }\n }\n\n async renderCover(): Promise<void> {\n await this.renderCurrentFrame(0, { mode: 'blocking' });\n }\n\n // ========= Public API =========\n\n play(): void {\n this.dispatch({ type: PlaybackActionType.Play });\n }\n\n pause(): void {\n this.dispatch({ type: PlaybackActionType.Pause });\n }\n\n stop(): void {\n this.dispatch({ type: PlaybackActionType.Stop });\n }\n\n async seek(timeUs: TimeUs): Promise<void> {\n const { done } = this.dispatch({\n type: PlaybackActionType.Seek,\n timeUs,\n durationUs: this.duration,\n });\n await done;\n }\n\n setRate(rate: number): void {\n const currentTimeUs = this.currentTimeUs;\n this.playbackRate = rate;\n\n // Keep currentTimeUs stable; update the time base for AudioContext clock mapping.\n this.startTimeUs = this.audioContext.currentTime * 1_000_000 - currentTimeUs / rate;\n this.audioSession.setPlaybackRate(this.playbackRate);\n\n this.eventBus.emit(MeframeEvent.PlaybackRateChange, { rate });\n }\n\n setVolume(volume: number): void {\n this.volume = Math.max(0, Math.min(1, volume));\n this.audioSession.setVolume(this.volume);\n this.eventBus.emit(MeframeEvent.PlaybackVolumeChange, { volume: this.volume });\n }\n\n setMute(muted: boolean): void {\n if (muted) {\n this.audioSession.stopPlayback();\n return;\n }\n if (this.fsm.snapshot.state === PlaybackState.Playing) {\n void this.audioSession.startPlayback(this.currentTimeUs, this.audioContext);\n }\n }\n\n setLoop(loop: boolean): void {\n this.loop = loop;\n }\n\n get duration(): TimeUs {\n return this.orchestrator.compositionModel?.durationUs ?? 0;\n }\n\n get isPlaying(): boolean {\n return this.fsm.snapshot.state === PlaybackState.Playing;\n }\n\n resume(): void {\n this.play();\n }\n\n on(event: string, handler: (payload: any) => void): void {\n this.eventBus.on(event as MeframeEvent, handler);\n }\n\n off(event: string, handler: (payload: any) => void): void {\n this.eventBus.off(event as MeframeEvent, handler);\n }\n\n // ========= State machine wiring =========\n\n private dispatch(action: PlaybackAction): { token: OpToken; done: Promise<void> } {\n const { token, commands } = this.fsm.dispatch(action, { currentTimeUs: this.currentTimeUs });\n const done = this.executeCommands(commands, token);\n return { token, done };\n }\n\n private executeCommands(commands: PlaybackCommand[], token: OpToken): Promise<void> {\n const maybe = this.executeSeq(commands, token, 0);\n return maybe ?? Promise.resolve();\n }\n\n private executeSeq(\n commands: PlaybackCommand[],\n token: OpToken,\n startIndex: number\n ): Promise<void> | void {\n for (let i = startIndex; i < commands.length; i++) {\n if (!this.isCurrentToken(token)) return;\n const maybe = this.executeCommand(commands[i]!, token);\n if (maybe) {\n return maybe.then(() => {\n if (!this.isCurrentToken(token)) return;\n const cont = this.executeSeq(commands, token, i + 1);\n return cont ?? Promise.resolve();\n });\n }\n }\n }\n\n private executePar(commands: PlaybackCommand[], token: OpToken): Promise<any> | void {\n const promises: Promise<void>[] = [];\n for (const c of commands) {\n if (!this.isCurrentToken(token)) return;\n const maybe = this.executeCommand(c, token);\n if (maybe) promises.push(maybe);\n }\n if (promises.length === 0) return;\n return Promise.all(promises);\n }\n\n private executeCommand(command: PlaybackCommand, token: OpToken): Promise<any> | void {\n if (!this.isCurrentToken(token)) return;\n\n switch (command.type) {\n case PlaybackCommandType.Seq:\n return this.executeSeq(command.commands, token, 0);\n case PlaybackCommandType.Par:\n return this.executePar(command.commands, token);\n case PlaybackCommandType.Try: {\n const handleError = (error: unknown): Promise<void> | void => {\n if (!this.isCurrentToken(token)) return;\n if (command.ignoreWaiterReplacedError && error instanceof WaiterReplacedError) return;\n if (command.logPrefix) console.error(command.logPrefix, error);\n const onErrorDone = command.onError ? this.dispatch(command.onError).done : undefined;\n const normalizeError = (e: unknown): Error => {\n if (e instanceof Error) return e;\n return new Error(typeof e === 'string' ? e : JSON.stringify(e));\n };\n const emit = () => {\n if (command.emitPlaybackError) {\n const err = normalizeError(error);\n // PlaybackError: direct playback channel error for advanced consumers.\n this.eventBus.emit(MeframeEvent.PlaybackError, err);\n // Error: generic error channel expected by higher-level wrappers (e.g. @meframe/axii).\n this.eventBus.emit(MeframeEvent.Error, {\n source: 'playback',\n error: err,\n context: {\n command: command.logPrefix,\n onError: command.onError?.type,\n },\n recoverable: false,\n });\n }\n };\n if (onErrorDone) {\n return onErrorDone.then(() => {\n emit();\n });\n }\n emit();\n };\n\n try {\n const maybe = this.executeCommand(command.command, token);\n if (maybe) {\n return maybe.catch(handleError);\n }\n return;\n } catch (error) {\n return handleError(error) ?? Promise.resolve();\n }\n }\n case PlaybackCommandType.Dispatch:\n return this.dispatch(command.action).done;\n case PlaybackCommandType.SetTime: {\n this.currentTimeUs = command.timeUs;\n return;\n }\n case PlaybackCommandType.SetFrozenTime:\n case PlaybackCommandType.SetWantsPlay:\n case PlaybackCommandType.SetState: {\n // managed inside fsm\n return;\n }\n case PlaybackCommandType.CancelRaf: {\n this.cancelRaf();\n return;\n }\n case PlaybackCommandType.StopAudio: {\n this.audioSession.stopPlayback();\n return;\n }\n case PlaybackCommandType.ResetAudioPlaybackStates: {\n this.audioSession.resetPlaybackStates();\n return;\n }\n case PlaybackCommandType.ResetAudioSession: {\n this.audioSession.reset();\n return;\n }\n case PlaybackCommandType.ClearCanvas: {\n this.clearCanvas();\n return;\n }\n case PlaybackCommandType.SetLastAudioScheduleTime: {\n this.lastAudioScheduleTime = command.timeUs;\n return;\n }\n case PlaybackCommandType.SetStartTimeBase: {\n this.startTimeUs = command.startTimeUs;\n return;\n }\n case PlaybackCommandType.SyncTimeBaseToAudioClock: {\n this.startTimeUs =\n this.audioContext.currentTime * 1_000_000 - command.timeUs / this.playbackRate;\n return;\n }\n case PlaybackCommandType.InitWindow: {\n this.initWindow(command.timeUs);\n return;\n }\n case PlaybackCommandType.SetCacheWindow: {\n this.orchestrator.cacheManager.setWindow(command.timeUs);\n return;\n }\n case PlaybackCommandType.Emit: {\n if (command.payload === undefined) {\n this.eventBus.emit(command.event as any);\n } else {\n this.eventBus.emit(command.event as any, command.payload);\n }\n return;\n }\n case PlaybackCommandType.RenderFrame: {\n return this.renderCurrentFrame(command.timeUs, {\n mode: command.mode,\n relativeTimeUs: command.relativeTimeUs,\n });\n }\n case PlaybackCommandType.MaybeRenderKeyframePreview: {\n return this.orchestrator.tryRenderKeyframe(command.timeUs).then((keyframeTimeUs) => {\n if (!this.isCurrentToken(token)) return;\n if (keyframeTimeUs === null) return;\n return this.orchestrator\n .getRenderState(command.timeUs, {\n mode: 'probe',\n relativeTimeUs: keyframeTimeUs,\n })\n .then((keyframeRenderState) => {\n if (!this.isCurrentToken(token)) return;\n if (!keyframeRenderState) return;\n return this.compose(command.timeUs, keyframeRenderState);\n });\n });\n }\n case PlaybackCommandType.EnsureAudio: {\n return this.audioSession.ensureAudioForTime(command.timeUs, {\n mode: command.mode,\n });\n }\n case PlaybackCommandType.GetFrame: {\n return this.orchestrator.getFrame(command.timeUs, {\n mode: command.mode,\n preheat: command.preheat,\n });\n }\n case PlaybackCommandType.StartAudioPlayback: {\n return this.audioSession.startPlayback(command.timeUs, this.audioContext);\n }\n case PlaybackCommandType.ProbeStartReady: {\n const audioReady = this.audioSession.isPreviewMixBlockCached(command.timeUs);\n const videoReady = this.isVideoResourceReadyAtTime(command.timeUs);\n\n if (audioReady && videoReady) return;\n\n // Kick background preparation (best-effort).\n if (!audioReady) {\n void this.audioSession.ensureAudioForTime(command.timeUs, { mode: 'probe' });\n }\n if (!videoReady) {\n void this.orchestrator.getFrame(command.timeUs, { mode: 'probe' });\n }\n\n // Enter buffering and bump token to cancel the remaining start sequence.\n this.dispatch({\n type: PlaybackActionType.EnterBuffering,\n timeUs: command.timeUs,\n bumpToken: true,\n reason: 'startup',\n });\n return;\n }\n case PlaybackCommandType.StartRafLoop: {\n this.startPlaybackLoop(token);\n return;\n }\n }\n }\n\n private cancelRaf(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n }\n\n private isCurrentToken(token: OpToken): boolean {\n return token === this.fsm.snapshot.token;\n }\n\n private startPlaybackLoop(token: OpToken): void {\n this.rafId = requestAnimationFrame(() => {\n void this.onRafTick(token);\n });\n }\n\n private async onRafTick(token: OpToken): Promise<void> {\n if (!this.isCurrentToken(token) || this.fsm.snapshot.state !== PlaybackState.Playing) {\n return;\n }\n\n const candidateTimeUs =\n (this.audioContext.currentTime * 1_000_000 - this.startTimeUs) * this.playbackRate;\n this.dispatch({\n type: PlaybackActionType.ClockTick,\n candidateTimeUs,\n durationUs: this.duration,\n loop: this.loop,\n audioNowUs: this.audioContext.currentTime * 1_000_000,\n });\n\n if (!this.isCurrentToken(token) || this.fsm.snapshot.state !== PlaybackState.Playing) {\n return;\n }\n\n // Audio probe: if the audio window isn't ready, enter buffering (freeze timeline)\n // and kick background audio preparation.\n // Note: preview audio readiness is driven by 60s mix-block cache inside AudioPreviewSession.\n // We intentionally avoid additional resource-window probing here to prevent buffering oscillation.\n\n // Throttle audio scheduling.\n if (this.currentTimeUs - this.lastAudioScheduleTime >= this.AUDIO_SCHEDULE_INTERVAL) {\n // Fire-and-forget: AudioPreviewSession runs its own background scheduling loop.\n // We avoid awaiting here to keep the render loop responsive.\n void this.audioSession.scheduleAudio(this.currentTimeUs, this.audioContext);\n this.lastAudioScheduleTime = this.currentTimeUs;\n }\n\n // If we're close to the 60s block boundary and the next mixed block isn't ready yet,\n // enter buffering to avoid hard silence at the boundary.\n if (this.audioSession.shouldEnterBufferingForUpcomingPreviewAudio(this.currentTimeUs)) {\n // Best-effort preheat; buffering path will call EnsureAudio(blocking).\n void this.audioSession.ensureAudioForTime(this.currentTimeUs, { mode: 'probe' });\n this.dispatch({\n type: PlaybackActionType.EnterBuffering,\n timeUs: this.currentTimeUs,\n reason: 'audio',\n });\n return;\n }\n\n const renderState = await this.orchestrator.getRenderState(this.currentTimeUs, {\n mode: 'probe',\n });\n if (!this.isCurrentToken(token) || this.fsm.snapshot.state !== PlaybackState.Playing) {\n return;\n }\n\n if (!renderState) {\n this.dispatch({ type: PlaybackActionType.EnterBuffering, timeUs: this.currentTimeUs });\n return;\n }\n\n await this.compose(this.currentTimeUs, renderState);\n if (!this.isCurrentToken(token) || this.fsm.snapshot.state !== PlaybackState.Playing) return;\n\n this.updateFps();\n this.frameCount++;\n\n // Unified cache window update.\n this.orchestrator.cacheManager.setWindow(this.currentTimeUs);\n\n this.checkAndPreheatWindow();\n if (!this.isCurrentToken(token) || this.fsm.snapshot.state !== PlaybackState.Playing) return;\n\n this.startPlaybackLoop(token);\n }\n\n private updateFps(): void {\n const now = performance.now();\n if (this.lastFrameTime > 0) {\n const deltaTime = now - this.lastFrameTime;\n const instantFps = 1000 / deltaTime;\n this.fps = this.fps > 0 ? this.fps * 0.9 + instantFps * 0.1 : instantFps;\n }\n this.lastFrameTime = now;\n }\n\n private initWindow(timeUs: TimeUs): void {\n this.windowEnd = timeUs + this.WINDOW_DURATION;\n this.preheatInProgress = false;\n this.orchestrator.cacheManager.setWindow(timeUs);\n }\n\n private checkAndPreheatWindow(): void {\n if (this.preheatInProgress || this.fsm.snapshot.state !== PlaybackState.Playing) {\n return;\n }\n\n const distanceToWindowEnd = this.windowEnd - this.currentTimeUs;\n if (distanceToWindowEnd < 0) {\n this.initWindow(this.currentTimeUs);\n return;\n }\n\n if (distanceToWindowEnd > 0 && distanceToWindowEnd <= this.PREHEAT_DISTANCE) {\n void this.preheatNextWindow();\n }\n }\n\n async preheatNextWindow(): Promise<void> {\n if (this.preheatInProgress) return;\n\n this.preheatInProgress = true;\n try {\n const windowStart = this.currentTimeUs;\n const windowEnd = windowStart + this.WINDOW_DURATION;\n\n const clipsInWindow =\n this.orchestrator.compositionModel?.getClipsInRange(windowStart, windowEnd) ?? [];\n const preheatPromises: Promise<any>[] = [];\n\n for (const clip of clipsInWindow) {\n if (!isVideoClip(clip)) continue;\n\n const clipWindowStart = Math.max(0, windowStart - clip.startUs);\n const clipWindowEnd = Math.min(clip.durationUs, windowEnd - clip.startUs);\n if (clipWindowStart >= clipWindowEnd) continue;\n\n preheatPromises.push(\n this.orchestrator.preheatClipWindow(clip.id, clipWindowStart, clipWindowEnd, windowStart)\n );\n }\n\n // Audio preheat is handled inside AudioPreviewSession via mix-block scheduling & cache.\n\n await Promise.all(preheatPromises);\n this.windowEnd = windowEnd;\n } catch (error) {\n console.warn('[PlaybackController] Preheat failed:', error);\n } finally {\n this.preheatInProgress = false;\n }\n }\n\n private async renderCurrentFrame(\n timeUs: TimeUs,\n options: { mode: RequestMode; relativeTimeUs?: TimeUs }\n ): Promise<void> {\n if (!this.videoComposer) {\n console.error('[PlaybackController] VideoComposer not initialized');\n return;\n }\n\n const renderState = await this.orchestrator.getRenderState(timeUs, {\n mode: options.mode,\n relativeTimeUs: options.relativeTimeUs,\n });\n\n if (!renderState) {\n return;\n }\n\n await this.compose(timeUs, renderState);\n }\n\n private async compose(\n timeUs: TimeUs,\n renderState: { layers: any[]; transition?: any }\n ): Promise<void> {\n if (!this.videoComposer) return;\n await this.videoComposer.composeFrame({\n timeUs,\n layers: renderState.layers,\n transition: renderState.transition,\n });\n }\n\n private clearCanvas(): void {\n const ctx = this.canvas.getContext('2d') as\n | CanvasRenderingContext2D\n | OffscreenCanvasRenderingContext2D\n | null;\n if (ctx && 'clearRect' in ctx) {\n ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n }\n }\n\n // ========= Cleanup / event handlers =========\n\n dispose(): void {\n this.stop();\n this.eventBus.off(MeframeEvent.CacheCover, this.onCacheCover);\n this.eventBus.off(MeframeEvent.ModelSet, this.onModelSet);\n if (this.videoComposer) {\n this.videoComposer.dispose();\n this.videoComposer = null;\n }\n }\n\n private onCacheCover = (): void => {\n if (this.fsm.snapshot.state === PlaybackState.Idle && this.currentTimeUs === 0) {\n void this.renderCurrentFrame(0, { mode: 'blocking' });\n }\n };\n\n private onModelSet = (): void => {\n if (!this.videoComposer || !this.orchestrator.compositionModel) return;\n\n // Model switching cancels in-flight async chains, but must NOT enter buffering or reset time/state.\n // Buffering runs a long async sequence; if cancelled mid-way it can leave the FSM stuck.\n this.dispatch({ type: PlaybackActionType.ModelSet });\n\n const model = this.orchestrator.compositionModel;\n this.videoComposer.updateConfig({\n width: model.renderConfig?.width || 720,\n height: model.renderConfig?.height || 1280,\n fps: model.fps || 30,\n backgroundColor: model.renderConfig?.backgroundColor || '#000',\n });\n\n // Best-effort background audio preheat (non-blocking).\n void this.audioSession.ensureAudioForTime(this.currentTimeUs, { mode: 'probe' });\n void this.renderCurrentFrame(this.currentTimeUs, { mode: 'blocking' });\n };\n\n private setupEventListeners(): void {\n this.eventBus.on(MeframeEvent.CacheCover, this.onCacheCover);\n this.eventBus.on(MeframeEvent.ModelSet, this.onModelSet);\n }\n\n private isVideoResourceReadyAtTime(timeUs: TimeUs): boolean {\n const model = this.orchestrator.compositionModel;\n if (!model) return true;\n const clip = model.getClipsAtTime(timeUs, model.mainTrackId)[0];\n if (!clip || !('resourceId' in clip) || typeof (clip as any).resourceId !== 'string')\n return true;\n const resourceId = (clip as any).resourceId as string;\n const resource = model.getResource(resourceId);\n return resource?.state === 'ready';\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PlaybackController.js","sources":["../../src/controllers/PlaybackController.ts"],"sourcesContent":["import type {\n IPlaybackController,\n PlaybackOptions,\n IEventBus,\n PreviewHandle,\n TimeUs,\n} from './types';\nimport {\n PlaybackActionType,\n PlaybackCommandType,\n PlaybackState,\n type OpToken,\n type PlaybackAction,\n type PlaybackCommand,\n} from './types';\nimport { MeframeEvent } from '../event/events';\nimport type { Orchestrator } from '../orchestrator';\nimport type { RequestMode } from '../orchestrator/types';\nimport { WaiterReplacedError } from '../utils/errors';\nimport { VideoComposer } from '../stages/compose/VideoComposer';\nimport { isVideoClip } from '../model/types';\nimport type { AudioPreviewSession } from '../orchestrator/AudioPreviewSession';\n\nimport { PlaybackStateMachine } from './PlaybackStateMachine';\n\n/**\n * Playback controller for preview\n * Internal implementation - not exposed directly to external consumers\n */\nexport class PlaybackController implements IPlaybackController, PreviewHandle {\n private orchestrator: Orchestrator;\n private eventBus: IEventBus;\n private canvas: HTMLCanvasElement | OffscreenCanvas;\n private videoComposer: VideoComposer | null = null;\n\n // Playback time (external)\n currentTimeUs: TimeUs = 0;\n private playbackRate = 1.0;\n private volume = 1.0;\n private loop = false;\n\n // Time base\n private rafId: number | null = null;\n private startTimeUs: TimeUs = 0; // AudioContext timeline origin (microseconds)\n\n // Frame stats\n private frameCount = 0;\n private lastFrameTime = 0;\n private fps = 0;\n\n // Audio\n private audioContext: AudioContext;\n private audioSession: AudioPreviewSession;\n private lastAudioScheduleTime = 0;\n private readonly AUDIO_SCHEDULE_INTERVAL = 250_000; // 250ms\n\n // State machine\n private fsm = new PlaybackStateMachine();\n\n // Unified window management for both video and audio\n private windowEnd: TimeUs = 0;\n private readonly WINDOW_DURATION = 3_000_000; // 3s decode window\n private readonly PREHEAT_DISTANCE = 1_000_000; // 1s preheat trigger distance\n private preheatInProgress = false;\n\n constructor(orchestrator: Orchestrator, eventBus: IEventBus, options: PlaybackOptions) {\n this.orchestrator = orchestrator;\n this.audioSession = orchestrator.audio.preview;\n this.eventBus = eventBus;\n this.canvas = options.canvas;\n this.audioContext = new AudioContext();\n\n const model = orchestrator.compositionModel;\n const width = model?.renderConfig?.width || this.canvas.width || 720;\n const height = model?.renderConfig?.height || this.canvas.height || 1280;\n\n this.videoComposer = new VideoComposer({\n width,\n height,\n fps: model?.fps || 30,\n backgroundColor: model?.renderConfig?.backgroundColor || '#000',\n externalCanvas: this.canvas,\n });\n\n if (options.startUs !== undefined) {\n this.currentTimeUs = options.startUs;\n }\n if (options.rate !== undefined) {\n this.playbackRate = options.rate;\n }\n if (options.loop !== undefined) {\n this.loop = options.loop;\n }\n\n this.setupEventListeners();\n\n if (options.autoStart) {\n this.play();\n }\n }\n\n async renderCover(): Promise<void> {\n await this.renderCurrentFrame(0, { mode: 'blocking' });\n }\n\n // ========= Public API =========\n\n play(): void {\n this.dispatch({ type: PlaybackActionType.Play });\n }\n\n pause(): void {\n this.dispatch({ type: PlaybackActionType.Pause });\n }\n\n stop(): void {\n this.dispatch({ type: PlaybackActionType.Stop });\n }\n\n async seek(timeUs: TimeUs): Promise<void> {\n const { done } = this.dispatch({\n type: PlaybackActionType.Seek,\n timeUs,\n durationUs: this.duration,\n });\n await done;\n }\n\n setRate(rate: number): void {\n const currentTimeUs = this.currentTimeUs;\n this.playbackRate = rate;\n\n // Keep currentTimeUs stable; update the time base for AudioContext clock mapping.\n this.startTimeUs = this.audioContext.currentTime * 1_000_000 - currentTimeUs / rate;\n this.audioSession.setPlaybackRate(this.playbackRate);\n\n this.eventBus.emit(MeframeEvent.PlaybackRateChange, { rate });\n }\n\n setVolume(volume: number): void {\n this.volume = Math.max(0, Math.min(1, volume));\n this.audioSession.setVolume(this.volume);\n this.eventBus.emit(MeframeEvent.PlaybackVolumeChange, { volume: this.volume });\n }\n\n setMute(muted: boolean): void {\n if (muted) {\n this.audioSession.stopPlayback();\n return;\n }\n if (this.fsm.snapshot.state === PlaybackState.Playing) {\n void this.audioSession.startPlayback(this.currentTimeUs, this.audioContext);\n }\n }\n\n setLoop(loop: boolean): void {\n this.loop = loop;\n }\n\n get duration(): TimeUs {\n return this.orchestrator.compositionModel?.durationUs ?? 0;\n }\n\n get isPlaying(): boolean {\n return this.fsm.snapshot.state === PlaybackState.Playing;\n }\n\n resume(): void {\n this.play();\n }\n\n on(event: string, handler: (payload: any) => void): void {\n this.eventBus.on(event as MeframeEvent, handler);\n }\n\n off(event: string, handler: (payload: any) => void): void {\n this.eventBus.off(event as MeframeEvent, handler);\n }\n\n // ========= State machine wiring =========\n\n private dispatch(action: PlaybackAction): { token: OpToken; done: Promise<void> } {\n const { token, commands } = this.fsm.dispatch(action, { currentTimeUs: this.currentTimeUs });\n const done = this.executeCommands(commands, token);\n return { token, done };\n }\n\n private executeCommands(commands: PlaybackCommand[], token: OpToken): Promise<void> {\n const maybe = this.executeSeq(commands, token, 0);\n return maybe ?? Promise.resolve();\n }\n\n private executeSeq(\n commands: PlaybackCommand[],\n token: OpToken,\n startIndex: number\n ): Promise<void> | void {\n for (let i = startIndex; i < commands.length; i++) {\n if (!this.isCurrentToken(token)) return;\n const maybe = this.executeCommand(commands[i]!, token);\n if (maybe) {\n return maybe.then(() => {\n if (!this.isCurrentToken(token)) return;\n const cont = this.executeSeq(commands, token, i + 1);\n return cont ?? Promise.resolve();\n });\n }\n }\n }\n\n private executePar(commands: PlaybackCommand[], token: OpToken): Promise<any> | void {\n const promises: Promise<void>[] = [];\n for (const c of commands) {\n if (!this.isCurrentToken(token)) return;\n const maybe = this.executeCommand(c, token);\n if (maybe) promises.push(maybe);\n }\n if (promises.length === 0) return;\n return Promise.all(promises);\n }\n\n private executeCommand(command: PlaybackCommand, token: OpToken): Promise<any> | void {\n if (!this.isCurrentToken(token)) return;\n\n switch (command.type) {\n case PlaybackCommandType.Seq:\n return this.executeSeq(command.commands, token, 0);\n case PlaybackCommandType.Par:\n return this.executePar(command.commands, token);\n case PlaybackCommandType.Try: {\n const handleError = (error: unknown): Promise<void> | void => {\n if (!this.isCurrentToken(token)) return;\n if (command.ignoreWaiterReplacedError && error instanceof WaiterReplacedError) return;\n if (command.logPrefix) console.error(command.logPrefix, error);\n const onErrorDone = command.onError ? this.dispatch(command.onError).done : undefined;\n const normalizeError = (e: unknown): Error => {\n if (e instanceof Error) return e;\n return new Error(typeof e === 'string' ? e : JSON.stringify(e));\n };\n const emit = () => {\n if (command.emitPlaybackError) {\n const err = normalizeError(error);\n // PlaybackError: direct playback channel error for advanced consumers.\n this.eventBus.emit(MeframeEvent.PlaybackError, err);\n // Error: generic error channel expected by higher-level wrappers (e.g. @meframe/axii).\n this.eventBus.emit(MeframeEvent.Error, {\n source: 'playback',\n error: err,\n context: {\n command: command.logPrefix,\n onError: command.onError?.type,\n },\n recoverable: false,\n });\n }\n };\n if (onErrorDone) {\n return onErrorDone.then(() => {\n emit();\n });\n }\n emit();\n };\n\n try {\n const maybe = this.executeCommand(command.command, token);\n if (maybe) {\n return maybe.catch(handleError);\n }\n return;\n } catch (error) {\n return handleError(error) ?? Promise.resolve();\n }\n }\n case PlaybackCommandType.Dispatch:\n return this.dispatch(command.action).done;\n case PlaybackCommandType.SetTime: {\n this.currentTimeUs = command.timeUs;\n return;\n }\n case PlaybackCommandType.SetFrozenTime:\n case PlaybackCommandType.SetWantsPlay:\n case PlaybackCommandType.SetState: {\n // managed inside fsm\n return;\n }\n case PlaybackCommandType.CancelRaf: {\n this.cancelRaf();\n return;\n }\n case PlaybackCommandType.StopAudio: {\n this.audioSession.stopPlayback();\n return;\n }\n case PlaybackCommandType.ResetAudioPlaybackStates: {\n this.audioSession.resetPlaybackStates();\n return;\n }\n case PlaybackCommandType.ResetAudioSession: {\n this.audioSession.reset();\n return;\n }\n case PlaybackCommandType.ClearCanvas: {\n this.clearCanvas();\n return;\n }\n case PlaybackCommandType.SetLastAudioScheduleTime: {\n this.lastAudioScheduleTime = command.timeUs;\n return;\n }\n case PlaybackCommandType.SetStartTimeBase: {\n this.startTimeUs = command.startTimeUs;\n return;\n }\n case PlaybackCommandType.SyncTimeBaseToAudioClock: {\n this.startTimeUs =\n this.audioContext.currentTime * 1_000_000 - command.timeUs / this.playbackRate;\n return;\n }\n case PlaybackCommandType.InitWindow: {\n this.initWindow(command.timeUs);\n return;\n }\n case PlaybackCommandType.SetCacheWindow: {\n this.orchestrator.cacheManager.setWindow(command.timeUs);\n return;\n }\n case PlaybackCommandType.Emit: {\n if (command.payload === undefined) {\n this.eventBus.emit(command.event as any);\n } else {\n this.eventBus.emit(command.event as any, command.payload);\n }\n return;\n }\n case PlaybackCommandType.RenderFrame: {\n return this.renderCurrentFrame(command.timeUs, {\n mode: command.mode,\n relativeTimeUs: command.relativeTimeUs,\n });\n }\n case PlaybackCommandType.MaybeRenderKeyframePreview: {\n return this.orchestrator.tryRenderKeyframe(command.timeUs).then((keyframeTimeUs) => {\n if (!this.isCurrentToken(token)) return;\n if (keyframeTimeUs === null) return;\n return this.orchestrator\n .getRenderState(command.timeUs, {\n mode: 'probe',\n relativeTimeUs: keyframeTimeUs,\n })\n .then((keyframeRenderState) => {\n if (!this.isCurrentToken(token)) return;\n if (!keyframeRenderState) return;\n return this.compose(command.timeUs, keyframeRenderState);\n });\n });\n }\n case PlaybackCommandType.EnsureAudio: {\n return this.audioSession.ensureAudioForTime(command.timeUs, {\n mode: command.mode,\n });\n }\n case PlaybackCommandType.GetFrame: {\n return this.orchestrator.getFrame(command.timeUs, {\n mode: command.mode,\n preheat: command.preheat,\n });\n }\n case PlaybackCommandType.StartAudioPlayback: {\n return this.audioSession.startPlayback(command.timeUs, this.audioContext);\n }\n case PlaybackCommandType.ProbeStartReady: {\n const audioReady = this.audioSession.isPreviewMixBlockCached(command.timeUs);\n const videoReady = this.isVideoResourceReadyAtTime(command.timeUs);\n\n if (audioReady && videoReady) return;\n\n // Kick background preparation (best-effort).\n if (!audioReady) {\n void this.audioSession.ensureAudioForTime(command.timeUs, { mode: 'probe' });\n }\n if (!videoReady) {\n void this.orchestrator.getFrame(command.timeUs, { mode: 'probe' });\n }\n\n // Enter buffering and bump token to cancel the remaining start sequence.\n this.dispatch({\n type: PlaybackActionType.EnterBuffering,\n timeUs: command.timeUs,\n bumpToken: true,\n reason: 'startup',\n });\n return;\n }\n case PlaybackCommandType.StartRafLoop: {\n this.startPlaybackLoop(token);\n return;\n }\n }\n }\n\n private cancelRaf(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n }\n\n private isCurrentToken(token: OpToken): boolean {\n return token === this.fsm.snapshot.token;\n }\n\n private startPlaybackLoop(token: OpToken): void {\n this.rafId = requestAnimationFrame(() => {\n void this.onRafTick(token);\n });\n }\n\n private async onRafTick(token: OpToken): Promise<void> {\n if (!this.isCurrentToken(token) || this.fsm.snapshot.state !== PlaybackState.Playing) {\n return;\n }\n\n const candidateTimeUs =\n (this.audioContext.currentTime * 1_000_000 - this.startTimeUs) * this.playbackRate;\n this.dispatch({\n type: PlaybackActionType.ClockTick,\n candidateTimeUs,\n durationUs: this.duration,\n loop: this.loop,\n audioNowUs: this.audioContext.currentTime * 1_000_000,\n });\n\n if (!this.isCurrentToken(token) || this.fsm.snapshot.state !== PlaybackState.Playing) {\n return;\n }\n\n // Audio probe: if the audio window isn't ready, enter buffering (freeze timeline)\n // and kick background audio preparation.\n // Note: preview audio readiness is driven by 60s mix-block cache inside AudioPreviewSession.\n // We intentionally avoid additional resource-window probing here to prevent buffering oscillation.\n\n // Throttle audio scheduling.\n if (this.currentTimeUs - this.lastAudioScheduleTime >= this.AUDIO_SCHEDULE_INTERVAL) {\n // Fire-and-forget: AudioPreviewSession runs its own background scheduling loop.\n // We avoid awaiting here to keep the render loop responsive.\n void this.audioSession.scheduleAudio(this.currentTimeUs, this.audioContext);\n this.lastAudioScheduleTime = this.currentTimeUs;\n }\n\n // If we're close to the 60s block boundary and the next mixed block isn't ready yet,\n // enter buffering to avoid hard silence at the boundary.\n if (this.audioSession.shouldEnterBufferingForUpcomingPreviewAudio(this.currentTimeUs)) {\n // Best-effort preheat; buffering path will call EnsureAudio(blocking).\n void this.audioSession.ensureAudioForTime(this.currentTimeUs, { mode: 'probe' });\n this.dispatch({\n type: PlaybackActionType.EnterBuffering,\n timeUs: this.currentTimeUs,\n reason: 'audio',\n });\n return;\n }\n\n const renderState = await this.orchestrator.getRenderState(this.currentTimeUs, {\n mode: 'probe',\n });\n if (!this.isCurrentToken(token) || this.fsm.snapshot.state !== PlaybackState.Playing) {\n return;\n }\n\n if (!renderState) {\n this.dispatch({ type: PlaybackActionType.EnterBuffering, timeUs: this.currentTimeUs });\n return;\n }\n\n await this.compose(this.currentTimeUs, renderState);\n if (!this.isCurrentToken(token) || this.fsm.snapshot.state !== PlaybackState.Playing) return;\n\n this.updateFps();\n this.frameCount++;\n\n // Unified cache window update.\n this.orchestrator.cacheManager.setWindow(this.currentTimeUs);\n\n this.checkAndPreheatWindow();\n if (!this.isCurrentToken(token) || this.fsm.snapshot.state !== PlaybackState.Playing) return;\n\n this.startPlaybackLoop(token);\n }\n\n private updateFps(): void {\n const now = performance.now();\n if (this.lastFrameTime > 0) {\n const deltaTime = now - this.lastFrameTime;\n const instantFps = 1000 / deltaTime;\n this.fps = this.fps > 0 ? this.fps * 0.9 + instantFps * 0.1 : instantFps;\n }\n this.lastFrameTime = now;\n }\n\n private initWindow(timeUs: TimeUs): void {\n this.windowEnd = timeUs + this.WINDOW_DURATION;\n this.preheatInProgress = false;\n this.orchestrator.cacheManager.setWindow(timeUs);\n }\n\n private checkAndPreheatWindow(): void {\n if (this.preheatInProgress || this.fsm.snapshot.state !== PlaybackState.Playing) {\n return;\n }\n\n const distanceToWindowEnd = this.windowEnd - this.currentTimeUs;\n if (distanceToWindowEnd < 0) {\n this.initWindow(this.currentTimeUs);\n return;\n }\n\n if (distanceToWindowEnd > 0 && distanceToWindowEnd <= this.PREHEAT_DISTANCE) {\n void this.preheatNextWindow();\n }\n }\n\n async preheatNextWindow(): Promise<void> {\n if (this.preheatInProgress) return;\n\n this.preheatInProgress = true;\n try {\n const windowStart = this.currentTimeUs;\n const windowEnd = windowStart + this.WINDOW_DURATION;\n\n const clipsInWindow =\n this.orchestrator.compositionModel?.getClipsInRange(windowStart, windowEnd) ?? [];\n const preheatPromises: Promise<any>[] = [];\n\n for (const clip of clipsInWindow) {\n if (!isVideoClip(clip)) continue;\n\n const clipWindowStart = Math.max(0, windowStart - clip.startUs);\n const clipWindowEnd = Math.min(clip.durationUs, windowEnd - clip.startUs);\n if (clipWindowStart >= clipWindowEnd) continue;\n\n preheatPromises.push(\n this.orchestrator.preheatClipWindow(clip.id, clipWindowStart, clipWindowEnd, windowStart)\n );\n }\n\n // Audio preheat is handled inside AudioPreviewSession via mix-block scheduling & cache.\n\n await Promise.all(preheatPromises);\n this.windowEnd = windowEnd;\n } catch (error) {\n console.warn('[PlaybackController] Preheat failed:', error);\n } finally {\n this.preheatInProgress = false;\n }\n }\n\n private async renderCurrentFrame(\n timeUs: TimeUs,\n options: { mode: RequestMode; relativeTimeUs?: TimeUs }\n ): Promise<void> {\n if (!this.videoComposer) {\n console.error('[PlaybackController] VideoComposer not initialized');\n return;\n }\n\n const renderState = await this.orchestrator.getRenderState(timeUs, {\n mode: options.mode,\n relativeTimeUs: options.relativeTimeUs,\n });\n\n if (!renderState) {\n return;\n }\n\n await this.compose(timeUs, renderState);\n }\n\n private async compose(\n timeUs: TimeUs,\n renderState: { layers: any[]; transition?: any }\n ): Promise<void> {\n if (!this.videoComposer) return;\n await this.videoComposer.composeFrame({\n timeUs,\n layers: renderState.layers,\n transition: renderState.transition,\n });\n }\n\n private clearCanvas(): void {\n const ctx = this.canvas.getContext('2d') as\n | CanvasRenderingContext2D\n | OffscreenCanvasRenderingContext2D\n | null;\n if (ctx && 'clearRect' in ctx) {\n ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n }\n }\n\n // ========= Cleanup / event handlers =========\n\n dispose(): void {\n this.stop();\n this.eventBus.off(MeframeEvent.CacheCover, this.onCacheCover);\n this.eventBus.off(MeframeEvent.ModelSet, this.onModelSet);\n if (this.videoComposer) {\n this.videoComposer.dispose();\n this.videoComposer = null;\n }\n }\n\n private onCacheCover = (): void => {\n if (this.fsm.snapshot.state === PlaybackState.Idle && this.currentTimeUs === 0) {\n void this.renderCurrentFrame(0, { mode: 'blocking' });\n }\n };\n\n private onModelSet = (): void => {\n if (!this.videoComposer || !this.orchestrator.compositionModel) return;\n\n // Model switching cancels in-flight async chains, but must NOT enter buffering or reset time/state.\n // Buffering runs a long async sequence; if cancelled mid-way it can leave the FSM stuck.\n this.dispatch({ type: PlaybackActionType.ModelSet });\n\n const model = this.orchestrator.compositionModel;\n this.videoComposer.updateConfig({\n width: model.renderConfig?.width || 720,\n height: model.renderConfig?.height || 1280,\n fps: model.fps || 30,\n backgroundColor: model.renderConfig?.backgroundColor || '#000',\n });\n\n // Best-effort background audio preheat (non-blocking).\n if (!this.orchestrator.cacheManager.isExporting) {\n void this.audioSession.ensureAudioForTime(this.currentTimeUs, { mode: 'probe' });\n }\n void this.renderCurrentFrame(this.currentTimeUs, { mode: 'blocking' });\n };\n\n private setupEventListeners(): void {\n this.eventBus.on(MeframeEvent.CacheCover, this.onCacheCover);\n this.eventBus.on(MeframeEvent.ModelSet, this.onModelSet);\n }\n\n private isVideoResourceReadyAtTime(timeUs: TimeUs): boolean {\n const model = this.orchestrator.compositionModel;\n if (!model) return true;\n const clip = model.getClipsAtTime(timeUs, model.mainTrackId)[0];\n if (!clip || !('resourceId' in clip) || typeof (clip as any).resourceId !== 'string')\n return true;\n const resourceId = (clip as any).resourceId as string;\n const resource = model.getResource(resourceId);\n return resource?.state === 'ready';\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"AudioPreviewSession.d.ts","sourceRoot":"","sources":["../../src/orchestrator/AudioPreviewSession.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAG7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAEjE,qBAAa,mBAAmB;IA+BlB,OAAO,CAAC,IAAI;IA9BxB,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,MAAM,CAAO;IACrB,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,SAAS,CAAS;IAM1B,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAA0B;IACpE,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAK;IAC9C,OAAO,CAAC,QAAQ,CAAC,0BAA0B,CAAQ;IACnD,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAqB;IAC7D,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAQ;IAE/C,OAAO,CAAC,iBAAiB,CAAyD;IAElF,OAAO,CAAC,mBAAmB,CAA8B;IACzD,OAAO,CAAC,oBAAoB,CAAK;IACjC,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,uBAAuB,CAAK;IACpC,OAAO,CAAC,yBAAyB,CAAa;IAC9C,OAAO,CAAC,qBAAqB,CAAa;IAC1C,OAAO,CAAC,sBAAsB,CAAK;IAEnC,OAAO,CAAC,uBAAuB,CAAgE;IAC/F,OAAO,CAAC,eAAe,CAAuC;gBAE1C,IAAI,EAAE;QAAE,YAAY,EAAE,YAAY,CAAC;QAAC,QAAQ,EAAE,mBAAmB,CAAA;KAAE;IAIvF,yBAAyB,IAAI,IAAI;IASjC,OAAO,CAAC,iBAAiB;IAyBnB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAmCzF,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAKhD,2CAA2C,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAgB9D,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAqB9E,YAAY,IAAI,IAAI;IAOpB;;OAEG;IACG,aAAa,CAAC,iBAAiB,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBzF;;OAEG;IACH,mBAAmB,IAAI,IAAI;IAM3B,KAAK,IAAI,IAAI;IAOb,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAkB/B,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAMnC,OAAO,CAAC,2BAA2B;IAcnC,OAAO,CAAC,4BAA4B;IASpC,OAAO,CAAC,kCAAkC;YAe5B,6BAA6B;YA2C7B,qBAAqB;YAwBrB,wBAAwB;IA2FtC,OAAO,CAAC,qBAAqB;CAoB9B"}
1
+ {"version":3,"file":"AudioPreviewSession.d.ts","sourceRoot":"","sources":["../../src/orchestrator/AudioPreviewSession.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAG7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAEjE,qBAAa,mBAAmB;IA+BlB,OAAO,CAAC,IAAI;IA9BxB,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,MAAM,CAAO;IACrB,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,SAAS,CAAS;IAM1B,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAA0B;IACpE,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAK;IAC9C,OAAO,CAAC,QAAQ,CAAC,0BAA0B,CAAQ;IACnD,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAqB;IAC7D,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAQ;IAE/C,OAAO,CAAC,iBAAiB,CAAyD;IAElF,OAAO,CAAC,mBAAmB,CAA8B;IACzD,OAAO,CAAC,oBAAoB,CAAK;IACjC,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,uBAAuB,CAAK;IACpC,OAAO,CAAC,yBAAyB,CAAa;IAC9C,OAAO,CAAC,qBAAqB,CAAa;IAC1C,OAAO,CAAC,sBAAsB,CAAK;IAEnC,OAAO,CAAC,uBAAuB,CAAgE;IAC/F,OAAO,CAAC,eAAe,CAAuC;gBAE1C,IAAI,EAAE;QAAE,YAAY,EAAE,YAAY,CAAC;QAAC,QAAQ,EAAE,mBAAmB,CAAA;KAAE;IAIvF,yBAAyB,IAAI,IAAI;IASjC,OAAO,CAAC,iBAAiB;IAyBnB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAqCzF,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAKhD,2CAA2C,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAgB9D,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAqB9E,YAAY,IAAI,IAAI;IAOpB;;OAEG;IACG,aAAa,CAAC,iBAAiB,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBzF;;OAEG;IACH,mBAAmB,IAAI,IAAI;IAM3B,KAAK,IAAI,IAAI;IAOb,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAkB/B,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAMnC,OAAO,CAAC,2BAA2B;IAcnC,OAAO,CAAC,4BAA4B;IASpC,OAAO,CAAC,kCAAkC;YAe5B,6BAA6B;YA2C7B,qBAAqB;YA2BrB,wBAAwB;IA2FtC,OAAO,CAAC,qBAAqB;CAoB9B"}
@@ -64,6 +64,7 @@ class AudioPreviewSession {
64
64
  return next;
65
65
  }
66
66
  async ensureAudioForTime(timeUs, options) {
67
+ if (this.deps.cacheManager.isExporting) return;
67
68
  const model = this.deps.preparer.getModel();
68
69
  if (!model) return;
69
70
  const mode = options?.mode ?? "blocking";
@@ -235,6 +236,7 @@ class AudioPreviewSession {
235
236
  const token = this.previewMixToken;
236
237
  return await this.previewBlockCache.getOrCreate(blockIndex, async () => {
237
238
  return await this.enqueuePreviewMix(async () => {
239
+ if (this.deps.cacheManager.isExporting) return null;
238
240
  const startUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;
239
241
  const endUs = Math.min(startUs + this.PREVIEW_BLOCK_DURATION_US, model.durationUs);
240
242
  await this.deps.preparer.ensureAudioForTimeRange(startUs, endUs, {
@@ -242,7 +244,7 @@ class AudioPreviewSession {
242
244
  loadResource: true
243
245
  });
244
246
  const mixed = await this.mixer.mix(startUs, endUs);
245
- this.deps.cacheManager.clearAudioCache();
247
+ if (!this.deps.cacheManager.isExporting) this.deps.cacheManager.clearAudioCache();
246
248
  return mixed;
247
249
  }, token);
248
250
  });
@@ -1 +1 @@
1
- {"version":3,"file":"AudioPreviewSession.js","sources":["../../src/orchestrator/AudioPreviewSession.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\nimport { OfflineAudioMixer } from '../stages/compose/OfflineAudioMixer';\nimport { AudioMixBlockCache } from '../cache/AudioMixBlockCache';\nimport type { CacheManager } from '../cache/CacheManager';\nimport type { RequestMode } from './types';\nimport type { AudioWindowPreparer } from './AudioWindowPreparer';\n\nexport class AudioPreviewSession {\n private mixer: OfflineAudioMixer;\n private audioContext: AudioContext | null = null;\n private volume = 1.0;\n private playbackRate = 1.0;\n private isPlaying = false;\n\n // Preview strategy (unified):\n // - Always schedule audio in fixed 60s \"mix blocks\"\n // - Cache 2~3 mixed AudioBuffer blocks (LRU) to accelerate seek\n // - Schedule ahead using AudioContext clock to avoid underrun\n private readonly PREVIEW_BLOCK_DURATION_US: TimeUs = 60 * 1_000_000; // 60s\n private readonly PREVIEW_BLOCK_CACHE_SIZE = 3;\n private readonly PREVIEW_SCHEDULE_AHEAD_SEC = 12.0; // keep enough scheduled audio to hide mixing latency\n private readonly PREVIEW_BUFFER_GUARD_US: TimeUs = 2_000_000; // if next block isn't ready near boundary -> buffering\n private readonly PREVIEW_BLOCK_FADE_SEC = 0.01; // 10ms fade-in/out to avoid click at boundaries\n\n private previewBlockCache = new AudioMixBlockCache(this.PREVIEW_BLOCK_CACHE_SIZE);\n\n private previewScheduleTask: Promise<void> | null = null;\n private previewScheduleToken = 0;\n private previewMixToken = 0;\n private previewNextBlockIndex = 0;\n private previewNextScheduleTime = 0; // AudioContext time\n private previewFirstBlockOffsetUs: TimeUs = 0; // seek offset within the first block\n private previewLastTimelineUs: TimeUs = 0;\n private previewLastStallWarnAt = 0; // AudioContext time (sec)\n\n private previewScheduledSources = new Set<{ source: AudioBufferSourceNode; gain: GainNode }>();\n private previewMixQueue: Promise<unknown> = Promise.resolve();\n\n constructor(private deps: { cacheManager: CacheManager; preparer: AudioWindowPreparer }) {\n this.mixer = new OfflineAudioMixer(deps.cacheManager, () => deps.preparer.getModel());\n }\n\n invalidatePreviewMixCache(): void {\n // Mixed AudioBuffer blocks embed per-clip audioConfig (volume/muted).\n // When model is replaced via setCompositionModel, these blocks must be invalidated,\n // otherwise preview may keep scheduling old AudioBuffers and volume changes won't take effect.\n this.previewBlockCache.clear();\n // Ensure any in-flight mix tasks are dropped.\n this.previewMixToken += 1;\n }\n\n private enqueuePreviewMix<T>(work: () => Promise<T>, token: number): Promise<T | null> {\n const run = () => work();\n const next = this.previewMixQueue.then(\n () => {\n // If a seek/reset happened since this task was enqueued, drop it.\n if (this.previewMixToken !== token) {\n return null;\n }\n return run();\n },\n () => {\n if (this.previewMixToken !== token) {\n return null;\n }\n return run();\n }\n );\n // Keep queue alive even if a task fails.\n this.previewMixQueue = next.then(\n () => undefined,\n () => undefined\n );\n return next as Promise<T | null>;\n }\n\n async ensureAudioForTime(timeUs: TimeUs, options?: { mode?: RequestMode }): Promise<void> {\n const model = this.deps.preparer.getModel();\n if (!model) return;\n\n const mode = options?.mode ?? 'blocking';\n\n // Preview contract:\n // - blocking: ensure the current 60s mixed block is ready (may be slow -> should be wrapped by buffering UI)\n // - probe: best-effort preheat current and next block without blocking\n const blockIndex = Math.floor(Math.max(0, timeUs) / this.PREVIEW_BLOCK_DURATION_US);\n\n if (mode === 'probe') {\n // Default: only preheat the current block.\n void this.getOrCreateMixedBlock(blockIndex);\n\n // If we're close enough to boundary (within scheduling lookahead), also preheat next block.\n const blockEndUs = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;\n const remainingToBoundaryUs = blockEndUs - Math.max(0, timeUs);\n const lookaheadUs = Math.floor(this.PREVIEW_SCHEDULE_AHEAD_SEC * 1_000_000);\n if (remainingToBoundaryUs > 0 && remainingToBoundaryUs <= lookaheadUs) {\n void this.getOrCreateMixedBlock(blockIndex + 1);\n }\n return;\n }\n\n await this.getOrCreateMixedBlock(blockIndex);\n\n // If we're very close to the block boundary, also ensure the next block.\n const blockEndUs = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;\n const remainingToBoundaryUs = blockEndUs - Math.max(0, timeUs);\n if (remainingToBoundaryUs > 0 && remainingToBoundaryUs <= this.PREVIEW_BUFFER_GUARD_US) {\n await this.getOrCreateMixedBlock(blockIndex + 1);\n }\n }\n\n isPreviewMixBlockCached(timeUs: TimeUs): boolean {\n const blockIndex = Math.floor(Math.max(0, timeUs) / this.PREVIEW_BLOCK_DURATION_US);\n return this.previewBlockCache.get(blockIndex) !== null;\n }\n\n shouldEnterBufferingForUpcomingPreviewAudio(timeUs: TimeUs): boolean {\n const model = this.deps.preparer.getModel();\n if (!model) return false;\n\n const clampedUs = Math.max(0, timeUs);\n const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);\n const nextBlockStartUs = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;\n if (nextBlockStartUs >= model.durationUs) return false;\n\n const remainingToBoundaryUs = nextBlockStartUs - clampedUs;\n if (remainingToBoundaryUs > this.PREVIEW_BUFFER_GUARD_US) return false;\n\n // Probe next block readiness by checking cache at next block start time.\n return !this.isPreviewMixBlockCached(nextBlockStartUs);\n }\n\n async startPlayback(timeUs: TimeUs, audioContext: AudioContext): Promise<void> {\n this.audioContext = audioContext;\n\n // Resume AudioContext if suspended (required by modern browsers)\n if (audioContext.state === 'suspended') {\n await audioContext.resume();\n }\n\n // Ensure audio is decoded and ready (blocking mode for startup).\n await this.ensureAudioForTime(timeUs, { mode: 'blocking' });\n\n this.isPlaying = true;\n\n // Unified block scheduling: align to block index and schedule immediately.\n this.startPreviewBlockScheduling(timeUs, audioContext);\n // Schedule first block in blocking mode to avoid initial silence.\n await this.scheduleNextPreviewBlock(audioContext, this.previewScheduleToken);\n // Then keep scheduling in background.\n void this.scheduleAudio(timeUs, audioContext);\n }\n\n stopPlayback(): void {\n this.isPlaying = false;\n this.stopAllPreviewSources();\n this.cancelPreviewBlockScheduling();\n this.previewMixToken += 1;\n }\n\n /**\n * Schedule audio chunks ahead of playback cursor.\n */\n async scheduleAudio(currentTimelineUs: TimeUs, audioContext: AudioContext): Promise<void> {\n if (!this.isPlaying || !this.deps.preparer.getModel() || !this.audioContext) {\n return;\n }\n\n this.previewLastTimelineUs = currentTimelineUs;\n\n // Keep scheduling in the background to avoid blocking the render loop.\n if (this.previewScheduleTask) return;\n\n // Initialize if needed (e.g. after model switch without explicit startPlayback).\n if (this.previewNextScheduleTime === 0) {\n this.startPreviewBlockScheduling(currentTimelineUs, audioContext);\n }\n\n const token = this.previewScheduleToken;\n this.previewScheduleTask = this.runPreviewBlockSchedulingLoop(audioContext, token).finally(\n () => {\n if (this.previewScheduleToken === token) {\n this.previewScheduleTask = null;\n }\n }\n );\n }\n\n /**\n * Reset playback states (called on seek)\n */\n resetPlaybackStates(): void {\n this.stopAllPreviewSources();\n this.cancelPreviewBlockScheduling();\n this.previewMixToken += 1;\n }\n\n reset(): void {\n this.stopPlayback();\n this.deps.cacheManager.clearAudioCache();\n this.previewBlockCache.clear();\n this.deps.preparer.reset();\n }\n\n setVolume(volume: number): void {\n this.volume = volume;\n const audioContext = this.audioContext;\n if (!audioContext) return;\n\n // Apply volume to already scheduled nodes (small ramp to avoid click).\n const t = audioContext.currentTime;\n for (const { gain } of this.previewScheduledSources) {\n try {\n gain.gain.cancelScheduledValues(t);\n gain.gain.setValueAtTime(gain.gain.value, t);\n gain.gain.linearRampToValueAtTime(volume, t + 0.01);\n } catch {\n // ignore\n }\n }\n }\n\n setPlaybackRate(rate: number): void {\n this.playbackRate = rate;\n // Playback rate change requires restarting scheduling to keep sync with the timeline clock.\n this.resetPlaybackStates();\n }\n\n private startPreviewBlockScheduling(startTimelineUs: TimeUs, audioContext: AudioContext): void {\n this.cancelPreviewBlockScheduling();\n this.stopAllPreviewSources();\n\n const clampedUs = Math.max(0, startTimelineUs);\n const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);\n const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;\n\n this.previewFirstBlockOffsetUs = clampedUs - blockStartUs;\n this.previewNextBlockIndex = blockIndex;\n this.previewNextScheduleTime = audioContext.currentTime + 0.02;\n this.previewLastTimelineUs = startTimelineUs;\n }\n\n private cancelPreviewBlockScheduling(): void {\n this.previewScheduleToken += 1;\n this.previewScheduleTask = null;\n this.previewNextBlockIndex = 0;\n this.previewNextScheduleTime = 0;\n this.previewFirstBlockOffsetUs = 0;\n this.previewLastTimelineUs = 0;\n }\n\n private realignPreviewSchedulingToTimeline(audioContext: AudioContext): void {\n const model = this.deps.preparer.getModel();\n if (!model) return;\n\n const clampedUs = Math.max(0, this.previewLastTimelineUs);\n const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);\n const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;\n if (blockStartUs >= model.durationUs) return;\n\n this.stopAllPreviewSources();\n this.previewFirstBlockOffsetUs = clampedUs - blockStartUs;\n this.previewNextBlockIndex = blockIndex;\n this.previewNextScheduleTime = audioContext.currentTime + 0.02;\n }\n\n private async runPreviewBlockSchedulingLoop(\n audioContext: AudioContext,\n token: number\n ): Promise<void> {\n while (this.isPlaying && this.previewScheduleToken === token) {\n const model = this.deps.preparer.getModel();\n if (!model) return;\n\n // End-of-timeline: nothing more to schedule.\n const nextBlockStartUs = this.previewNextBlockIndex * this.PREVIEW_BLOCK_DURATION_US;\n if (nextBlockStartUs >= model.durationUs) {\n this.previewNextScheduleTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;\n return;\n }\n\n const scheduleAheadTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;\n if (this.previewNextScheduleTime >= scheduleAheadTime) return;\n\n const prevBlockIndex = this.previewNextBlockIndex;\n const prevScheduleTime = this.previewNextScheduleTime;\n\n await this.scheduleNextPreviewBlock(audioContext, token);\n\n // If scheduling made no progress, bail out to avoid a tight loop that can freeze UI.\n if (\n this.previewScheduleToken === token &&\n this.previewNextBlockIndex === prevBlockIndex &&\n this.previewNextScheduleTime === prevScheduleTime\n ) {\n const now = audioContext.currentTime;\n if (now - this.previewLastStallWarnAt >= 1) {\n this.previewLastStallWarnAt = now;\n console.warn('[AudioPreviewSession] scheduling stalled; stop loop to avoid spin', {\n prevBlockIndex,\n prevScheduleTime,\n now,\n });\n }\n return;\n }\n }\n }\n\n private async getOrCreateMixedBlock(blockIndex: number): Promise<AudioBuffer | null> {\n const model = this.deps.preparer.getModel();\n if (!model) return null;\n\n const token = this.previewMixToken;\n\n return await this.previewBlockCache.getOrCreate(blockIndex, async () => {\n return await this.enqueuePreviewMix(async () => {\n const startUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;\n const endUs = Math.min(startUs + this.PREVIEW_BLOCK_DURATION_US, model.durationUs);\n await this.deps.preparer.ensureAudioForTimeRange(startUs, endUs, {\n mode: 'blocking',\n loadResource: true,\n });\n const mixed = await this.mixer.mix(startUs, endUs);\n\n // Preview uses mixed AudioBuffer blocks as the primary cache.\n this.deps.cacheManager.clearAudioCache();\n\n return mixed;\n }, token);\n });\n }\n\n private async scheduleNextPreviewBlock(audioContext: AudioContext, token: number): Promise<void> {\n const model = this.deps.preparer.getModel();\n if (!this.isPlaying || !model) return;\n if (this.previewScheduleToken !== token) return;\n\n // If we're behind the audio clock, resync to avoid attempting to schedule in the past.\n if (this.previewNextScheduleTime < audioContext.currentTime + 0.01) {\n this.realignPreviewSchedulingToTimeline(audioContext);\n }\n\n const blockIndex = this.previewNextBlockIndex;\n const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;\n if (blockStartUs >= model.durationUs) {\n // End-of-timeline: advance state so scheduling loop can finish without stalling.\n this.previewFirstBlockOffsetUs = 0;\n this.previewNextBlockIndex = blockIndex + 1;\n this.previewNextScheduleTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;\n return;\n }\n\n const buffer = await this.getOrCreateMixedBlock(blockIndex);\n if (!buffer) {\n // Could be cancelled (seek/reset) or an empty-range guard; advance to avoid stalling.\n this.previewFirstBlockOffsetUs = 0;\n this.previewNextBlockIndex = blockIndex + 1;\n this.previewNextScheduleTime = audioContext.currentTime + 0.02;\n return;\n }\n if (this.previewScheduleToken !== token) return;\n\n const rate = this.playbackRate || 1.0;\n const offsetUs = this.previewFirstBlockOffsetUs;\n let startTime = this.previewNextScheduleTime;\n let offsetSec = offsetUs > 0 ? offsetUs / 1_000_000 : 0;\n\n // Mixing can be slow; after await, startTime might already be in the past.\n const now = audioContext.currentTime;\n if (startTime < now + 0.01) {\n const targetStartTime = now + 0.02;\n const skippedSec = Math.max(0, (targetStartTime - startTime) * rate);\n startTime = targetStartTime;\n offsetSec += skippedSec;\n }\n\n if (offsetSec >= buffer.duration) {\n this.previewFirstBlockOffsetUs = 0;\n this.previewNextBlockIndex = blockIndex + 1;\n this.previewNextScheduleTime = startTime;\n return;\n }\n\n const remainingSec = Math.max(0, buffer.duration - offsetSec);\n if (remainingSec <= 0) return;\n\n const source = audioContext.createBufferSource();\n source.buffer = buffer;\n source.playbackRate.value = rate;\n\n const gainNode = audioContext.createGain();\n const volume = this.volume;\n\n // Fade in/out to avoid click at block boundaries.\n const fadeSec = Math.min(this.PREVIEW_BLOCK_FADE_SEC, remainingSec / 2);\n gainNode.gain.setValueAtTime(0, startTime);\n gainNode.gain.linearRampToValueAtTime(volume, startTime + fadeSec);\n gainNode.gain.setValueAtTime(volume, startTime + Math.max(fadeSec, remainingSec - fadeSec));\n gainNode.gain.linearRampToValueAtTime(0, startTime + remainingSec);\n\n source.connect(gainNode);\n gainNode.connect(audioContext.destination);\n source.start(startTime, offsetSec);\n\n const entry = { source, gain: gainNode };\n this.previewScheduledSources.add(entry);\n\n source.onended = () => {\n try {\n source.disconnect();\n gainNode.disconnect();\n } catch {\n // ignore\n }\n this.previewScheduledSources.delete(entry);\n };\n\n // Advance to next block\n this.previewFirstBlockOffsetUs = 0;\n this.previewNextBlockIndex = blockIndex + 1;\n this.previewNextScheduleTime = startTime + remainingSec / rate;\n }\n\n private stopAllPreviewSources(): void {\n for (const { source, gain } of this.previewScheduledSources) {\n try {\n source.disconnect();\n } catch {\n // ignore\n }\n try {\n source.stop(0);\n } catch {\n // ignore\n }\n try {\n gain.disconnect();\n } catch {\n // ignore\n }\n }\n this.previewScheduledSources.clear();\n }\n}\n"],"names":["blockEndUs","remainingToBoundaryUs"],"mappings":"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"}
1
+ {"version":3,"file":"AudioPreviewSession.js","sources":["../../src/orchestrator/AudioPreviewSession.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\nimport { OfflineAudioMixer } from '../stages/compose/OfflineAudioMixer';\nimport { AudioMixBlockCache } from '../cache/AudioMixBlockCache';\nimport type { CacheManager } from '../cache/CacheManager';\nimport type { RequestMode } from './types';\nimport type { AudioWindowPreparer } from './AudioWindowPreparer';\n\nexport class AudioPreviewSession {\n private mixer: OfflineAudioMixer;\n private audioContext: AudioContext | null = null;\n private volume = 1.0;\n private playbackRate = 1.0;\n private isPlaying = false;\n\n // Preview strategy (unified):\n // - Always schedule audio in fixed 60s \"mix blocks\"\n // - Cache 2~3 mixed AudioBuffer blocks (LRU) to accelerate seek\n // - Schedule ahead using AudioContext clock to avoid underrun\n private readonly PREVIEW_BLOCK_DURATION_US: TimeUs = 60 * 1_000_000; // 60s\n private readonly PREVIEW_BLOCK_CACHE_SIZE = 3;\n private readonly PREVIEW_SCHEDULE_AHEAD_SEC = 12.0; // keep enough scheduled audio to hide mixing latency\n private readonly PREVIEW_BUFFER_GUARD_US: TimeUs = 2_000_000; // if next block isn't ready near boundary -> buffering\n private readonly PREVIEW_BLOCK_FADE_SEC = 0.01; // 10ms fade-in/out to avoid click at boundaries\n\n private previewBlockCache = new AudioMixBlockCache(this.PREVIEW_BLOCK_CACHE_SIZE);\n\n private previewScheduleTask: Promise<void> | null = null;\n private previewScheduleToken = 0;\n private previewMixToken = 0;\n private previewNextBlockIndex = 0;\n private previewNextScheduleTime = 0; // AudioContext time\n private previewFirstBlockOffsetUs: TimeUs = 0; // seek offset within the first block\n private previewLastTimelineUs: TimeUs = 0;\n private previewLastStallWarnAt = 0; // AudioContext time (sec)\n\n private previewScheduledSources = new Set<{ source: AudioBufferSourceNode; gain: GainNode }>();\n private previewMixQueue: Promise<unknown> = Promise.resolve();\n\n constructor(private deps: { cacheManager: CacheManager; preparer: AudioWindowPreparer }) {\n this.mixer = new OfflineAudioMixer(deps.cacheManager, () => deps.preparer.getModel());\n }\n\n invalidatePreviewMixCache(): void {\n // Mixed AudioBuffer blocks embed per-clip audioConfig (volume/muted).\n // When model is replaced via setCompositionModel, these blocks must be invalidated,\n // otherwise preview may keep scheduling old AudioBuffers and volume changes won't take effect.\n this.previewBlockCache.clear();\n // Ensure any in-flight mix tasks are dropped.\n this.previewMixToken += 1;\n }\n\n private enqueuePreviewMix<T>(work: () => Promise<T>, token: number): Promise<T | null> {\n const run = () => work();\n const next = this.previewMixQueue.then(\n () => {\n // If a seek/reset happened since this task was enqueued, drop it.\n if (this.previewMixToken !== token) {\n return null;\n }\n return run();\n },\n () => {\n if (this.previewMixToken !== token) {\n return null;\n }\n return run();\n }\n );\n // Keep queue alive even if a task fails.\n this.previewMixQueue = next.then(\n () => undefined,\n () => undefined\n );\n return next as Promise<T | null>;\n }\n\n async ensureAudioForTime(timeUs: TimeUs, options?: { mode?: RequestMode }): Promise<void> {\n if (this.deps.cacheManager.isExporting) return;\n\n const model = this.deps.preparer.getModel();\n if (!model) return;\n\n const mode = options?.mode ?? 'blocking';\n\n // Preview contract:\n // - blocking: ensure the current 60s mixed block is ready (may be slow -> should be wrapped by buffering UI)\n // - probe: best-effort preheat current and next block without blocking\n const blockIndex = Math.floor(Math.max(0, timeUs) / this.PREVIEW_BLOCK_DURATION_US);\n\n if (mode === 'probe') {\n // Default: only preheat the current block.\n void this.getOrCreateMixedBlock(blockIndex);\n\n // If we're close enough to boundary (within scheduling lookahead), also preheat next block.\n const blockEndUs = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;\n const remainingToBoundaryUs = blockEndUs - Math.max(0, timeUs);\n const lookaheadUs = Math.floor(this.PREVIEW_SCHEDULE_AHEAD_SEC * 1_000_000);\n if (remainingToBoundaryUs > 0 && remainingToBoundaryUs <= lookaheadUs) {\n void this.getOrCreateMixedBlock(blockIndex + 1);\n }\n return;\n }\n\n await this.getOrCreateMixedBlock(blockIndex);\n\n // If we're very close to the block boundary, also ensure the next block.\n const blockEndUs = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;\n const remainingToBoundaryUs = blockEndUs - Math.max(0, timeUs);\n if (remainingToBoundaryUs > 0 && remainingToBoundaryUs <= this.PREVIEW_BUFFER_GUARD_US) {\n await this.getOrCreateMixedBlock(blockIndex + 1);\n }\n }\n\n isPreviewMixBlockCached(timeUs: TimeUs): boolean {\n const blockIndex = Math.floor(Math.max(0, timeUs) / this.PREVIEW_BLOCK_DURATION_US);\n return this.previewBlockCache.get(blockIndex) !== null;\n }\n\n shouldEnterBufferingForUpcomingPreviewAudio(timeUs: TimeUs): boolean {\n const model = this.deps.preparer.getModel();\n if (!model) return false;\n\n const clampedUs = Math.max(0, timeUs);\n const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);\n const nextBlockStartUs = (blockIndex + 1) * this.PREVIEW_BLOCK_DURATION_US;\n if (nextBlockStartUs >= model.durationUs) return false;\n\n const remainingToBoundaryUs = nextBlockStartUs - clampedUs;\n if (remainingToBoundaryUs > this.PREVIEW_BUFFER_GUARD_US) return false;\n\n // Probe next block readiness by checking cache at next block start time.\n return !this.isPreviewMixBlockCached(nextBlockStartUs);\n }\n\n async startPlayback(timeUs: TimeUs, audioContext: AudioContext): Promise<void> {\n this.audioContext = audioContext;\n\n // Resume AudioContext if suspended (required by modern browsers)\n if (audioContext.state === 'suspended') {\n await audioContext.resume();\n }\n\n // Ensure audio is decoded and ready (blocking mode for startup).\n await this.ensureAudioForTime(timeUs, { mode: 'blocking' });\n\n this.isPlaying = true;\n\n // Unified block scheduling: align to block index and schedule immediately.\n this.startPreviewBlockScheduling(timeUs, audioContext);\n // Schedule first block in blocking mode to avoid initial silence.\n await this.scheduleNextPreviewBlock(audioContext, this.previewScheduleToken);\n // Then keep scheduling in background.\n void this.scheduleAudio(timeUs, audioContext);\n }\n\n stopPlayback(): void {\n this.isPlaying = false;\n this.stopAllPreviewSources();\n this.cancelPreviewBlockScheduling();\n this.previewMixToken += 1;\n }\n\n /**\n * Schedule audio chunks ahead of playback cursor.\n */\n async scheduleAudio(currentTimelineUs: TimeUs, audioContext: AudioContext): Promise<void> {\n if (!this.isPlaying || !this.deps.preparer.getModel() || !this.audioContext) {\n return;\n }\n\n this.previewLastTimelineUs = currentTimelineUs;\n\n // Keep scheduling in the background to avoid blocking the render loop.\n if (this.previewScheduleTask) return;\n\n // Initialize if needed (e.g. after model switch without explicit startPlayback).\n if (this.previewNextScheduleTime === 0) {\n this.startPreviewBlockScheduling(currentTimelineUs, audioContext);\n }\n\n const token = this.previewScheduleToken;\n this.previewScheduleTask = this.runPreviewBlockSchedulingLoop(audioContext, token).finally(\n () => {\n if (this.previewScheduleToken === token) {\n this.previewScheduleTask = null;\n }\n }\n );\n }\n\n /**\n * Reset playback states (called on seek)\n */\n resetPlaybackStates(): void {\n this.stopAllPreviewSources();\n this.cancelPreviewBlockScheduling();\n this.previewMixToken += 1;\n }\n\n reset(): void {\n this.stopPlayback();\n this.deps.cacheManager.clearAudioCache();\n this.previewBlockCache.clear();\n this.deps.preparer.reset();\n }\n\n setVolume(volume: number): void {\n this.volume = volume;\n const audioContext = this.audioContext;\n if (!audioContext) return;\n\n // Apply volume to already scheduled nodes (small ramp to avoid click).\n const t = audioContext.currentTime;\n for (const { gain } of this.previewScheduledSources) {\n try {\n gain.gain.cancelScheduledValues(t);\n gain.gain.setValueAtTime(gain.gain.value, t);\n gain.gain.linearRampToValueAtTime(volume, t + 0.01);\n } catch {\n // ignore\n }\n }\n }\n\n setPlaybackRate(rate: number): void {\n this.playbackRate = rate;\n // Playback rate change requires restarting scheduling to keep sync with the timeline clock.\n this.resetPlaybackStates();\n }\n\n private startPreviewBlockScheduling(startTimelineUs: TimeUs, audioContext: AudioContext): void {\n this.cancelPreviewBlockScheduling();\n this.stopAllPreviewSources();\n\n const clampedUs = Math.max(0, startTimelineUs);\n const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);\n const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;\n\n this.previewFirstBlockOffsetUs = clampedUs - blockStartUs;\n this.previewNextBlockIndex = blockIndex;\n this.previewNextScheduleTime = audioContext.currentTime + 0.02;\n this.previewLastTimelineUs = startTimelineUs;\n }\n\n private cancelPreviewBlockScheduling(): void {\n this.previewScheduleToken += 1;\n this.previewScheduleTask = null;\n this.previewNextBlockIndex = 0;\n this.previewNextScheduleTime = 0;\n this.previewFirstBlockOffsetUs = 0;\n this.previewLastTimelineUs = 0;\n }\n\n private realignPreviewSchedulingToTimeline(audioContext: AudioContext): void {\n const model = this.deps.preparer.getModel();\n if (!model) return;\n\n const clampedUs = Math.max(0, this.previewLastTimelineUs);\n const blockIndex = Math.floor(clampedUs / this.PREVIEW_BLOCK_DURATION_US);\n const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;\n if (blockStartUs >= model.durationUs) return;\n\n this.stopAllPreviewSources();\n this.previewFirstBlockOffsetUs = clampedUs - blockStartUs;\n this.previewNextBlockIndex = blockIndex;\n this.previewNextScheduleTime = audioContext.currentTime + 0.02;\n }\n\n private async runPreviewBlockSchedulingLoop(\n audioContext: AudioContext,\n token: number\n ): Promise<void> {\n while (this.isPlaying && this.previewScheduleToken === token) {\n const model = this.deps.preparer.getModel();\n if (!model) return;\n\n // End-of-timeline: nothing more to schedule.\n const nextBlockStartUs = this.previewNextBlockIndex * this.PREVIEW_BLOCK_DURATION_US;\n if (nextBlockStartUs >= model.durationUs) {\n this.previewNextScheduleTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;\n return;\n }\n\n const scheduleAheadTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;\n if (this.previewNextScheduleTime >= scheduleAheadTime) return;\n\n const prevBlockIndex = this.previewNextBlockIndex;\n const prevScheduleTime = this.previewNextScheduleTime;\n\n await this.scheduleNextPreviewBlock(audioContext, token);\n\n // If scheduling made no progress, bail out to avoid a tight loop that can freeze UI.\n if (\n this.previewScheduleToken === token &&\n this.previewNextBlockIndex === prevBlockIndex &&\n this.previewNextScheduleTime === prevScheduleTime\n ) {\n const now = audioContext.currentTime;\n if (now - this.previewLastStallWarnAt >= 1) {\n this.previewLastStallWarnAt = now;\n console.warn('[AudioPreviewSession] scheduling stalled; stop loop to avoid spin', {\n prevBlockIndex,\n prevScheduleTime,\n now,\n });\n }\n return;\n }\n }\n }\n\n private async getOrCreateMixedBlock(blockIndex: number): Promise<AudioBuffer | null> {\n const model = this.deps.preparer.getModel();\n if (!model) return null;\n\n const token = this.previewMixToken;\n\n return await this.previewBlockCache.getOrCreate(blockIndex, async () => {\n return await this.enqueuePreviewMix(async () => {\n // If export starts while a preview mix task is already queued/running, drop it.\n if (this.deps.cacheManager.isExporting) return null;\n\n const startUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;\n const endUs = Math.min(startUs + this.PREVIEW_BLOCK_DURATION_US, model.durationUs);\n await this.deps.preparer.ensureAudioForTimeRange(startUs, endUs, {\n mode: 'blocking',\n loadResource: true,\n });\n const mixed = await this.mixer.mix(startUs, endUs);\n\n // Preview uses mixed AudioBuffer blocks as the primary cache.\n if (!this.deps.cacheManager.isExporting) this.deps.cacheManager.clearAudioCache();\n\n return mixed;\n }, token);\n });\n }\n\n private async scheduleNextPreviewBlock(audioContext: AudioContext, token: number): Promise<void> {\n const model = this.deps.preparer.getModel();\n if (!this.isPlaying || !model) return;\n if (this.previewScheduleToken !== token) return;\n\n // If we're behind the audio clock, resync to avoid attempting to schedule in the past.\n if (this.previewNextScheduleTime < audioContext.currentTime + 0.01) {\n this.realignPreviewSchedulingToTimeline(audioContext);\n }\n\n const blockIndex = this.previewNextBlockIndex;\n const blockStartUs = blockIndex * this.PREVIEW_BLOCK_DURATION_US;\n if (blockStartUs >= model.durationUs) {\n // End-of-timeline: advance state so scheduling loop can finish without stalling.\n this.previewFirstBlockOffsetUs = 0;\n this.previewNextBlockIndex = blockIndex + 1;\n this.previewNextScheduleTime = audioContext.currentTime + this.PREVIEW_SCHEDULE_AHEAD_SEC;\n return;\n }\n\n const buffer = await this.getOrCreateMixedBlock(blockIndex);\n if (!buffer) {\n // Could be cancelled (seek/reset) or an empty-range guard; advance to avoid stalling.\n this.previewFirstBlockOffsetUs = 0;\n this.previewNextBlockIndex = blockIndex + 1;\n this.previewNextScheduleTime = audioContext.currentTime + 0.02;\n return;\n }\n if (this.previewScheduleToken !== token) return;\n\n const rate = this.playbackRate || 1.0;\n const offsetUs = this.previewFirstBlockOffsetUs;\n let startTime = this.previewNextScheduleTime;\n let offsetSec = offsetUs > 0 ? offsetUs / 1_000_000 : 0;\n\n // Mixing can be slow; after await, startTime might already be in the past.\n const now = audioContext.currentTime;\n if (startTime < now + 0.01) {\n const targetStartTime = now + 0.02;\n const skippedSec = Math.max(0, (targetStartTime - startTime) * rate);\n startTime = targetStartTime;\n offsetSec += skippedSec;\n }\n\n if (offsetSec >= buffer.duration) {\n this.previewFirstBlockOffsetUs = 0;\n this.previewNextBlockIndex = blockIndex + 1;\n this.previewNextScheduleTime = startTime;\n return;\n }\n\n const remainingSec = Math.max(0, buffer.duration - offsetSec);\n if (remainingSec <= 0) return;\n\n const source = audioContext.createBufferSource();\n source.buffer = buffer;\n source.playbackRate.value = rate;\n\n const gainNode = audioContext.createGain();\n const volume = this.volume;\n\n // Fade in/out to avoid click at block boundaries.\n const fadeSec = Math.min(this.PREVIEW_BLOCK_FADE_SEC, remainingSec / 2);\n gainNode.gain.setValueAtTime(0, startTime);\n gainNode.gain.linearRampToValueAtTime(volume, startTime + fadeSec);\n gainNode.gain.setValueAtTime(volume, startTime + Math.max(fadeSec, remainingSec - fadeSec));\n gainNode.gain.linearRampToValueAtTime(0, startTime + remainingSec);\n\n source.connect(gainNode);\n gainNode.connect(audioContext.destination);\n source.start(startTime, offsetSec);\n\n const entry = { source, gain: gainNode };\n this.previewScheduledSources.add(entry);\n\n source.onended = () => {\n try {\n source.disconnect();\n gainNode.disconnect();\n } catch {\n // ignore\n }\n this.previewScheduledSources.delete(entry);\n };\n\n // Advance to next block\n this.previewFirstBlockOffsetUs = 0;\n this.previewNextBlockIndex = blockIndex + 1;\n this.previewNextScheduleTime = startTime + remainingSec / rate;\n }\n\n private stopAllPreviewSources(): void {\n for (const { source, gain } of this.previewScheduledSources) {\n try {\n source.disconnect();\n } catch {\n // ignore\n }\n try {\n source.stop(0);\n } catch {\n // ignore\n }\n try {\n gain.disconnect();\n } catch {\n // ignore\n }\n }\n this.previewScheduledSources.clear();\n }\n}\n"],"names":["blockEndUs","remainingToBoundaryUs"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ExportScheduler.d.ts","sourceRoot":"","sources":["../../src/orchestrator/ExportScheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAgB,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEhE,UAAU,mBAAmB;IAC3B,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,EAAE,kBAAkB,CAAC;IAC5B,YAAY,EAAE,YAAY,CAAC;IAC3B,cAAc,EAAE,cAAc,CAAC;IAC/B,UAAU,EAAE,UAAU,CAAC;IACvB,YAAY,EAAE,kBAAkB,CAAC;IACjC,qBAAqB,EAAE,MAAM,MAAM,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;IACrD,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;CACrC;AAED,UAAU,qBAAsB,SAAQ,aAAa;IACnD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,IAAI,CAAsB;gBAEtB,IAAI,EAAE,mBAAmB;IAI/B,OAAO,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;YAavE,eAAe;IA0F7B;;OAEG;YACW,gBAAgB;IAsD9B;;;;OAIG;YACW,qBAAqB;YA0BrB,6BAA6B;CA0G5C"}
1
+ {"version":3,"file":"ExportScheduler.d.ts","sourceRoot":"","sources":["../../src/orchestrator/ExportScheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAgB,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEhE,UAAU,mBAAmB;IAC3B,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,EAAE,kBAAkB,CAAC;IAC5B,YAAY,EAAE,YAAY,CAAC;IAC3B,cAAc,EAAE,cAAc,CAAC;IAC/B,UAAU,EAAE,UAAU,CAAC;IACvB,YAAY,EAAE,kBAAkB,CAAC;IACjC,qBAAqB,EAAE,MAAM,MAAM,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;IACrD,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;CACrC;AAED,UAAU,qBAAsB,SAAQ,aAAa;IACnD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,IAAI,CAAsB;gBAEtB,IAAI,EAAE,mBAAmB;IAI/B,OAAO,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;YAavE,eAAe;IA+F7B;;OAEG;YACW,gBAAgB;IAsD9B;;;;OAIG;YACW,qBAAqB;YA0BrB,6BAA6B;CA0G5C"}
@@ -40,6 +40,7 @@ class ExportScheduler {
40
40
  fps,
41
41
  durationUs: model.durationUs
42
42
  });
43
+ this.deps.cacheManager.beginExport();
43
44
  try {
44
45
  await this.preloadResources(model, resourceLoader, eventBus, checkStatus);
45
46
  muxManager.start({
@@ -73,6 +74,8 @@ class ExportScheduler {
73
74
  stage: "export"
74
75
  });
75
76
  throw error;
77
+ } finally {
78
+ this.deps.cacheManager.endExport();
76
79
  }
77
80
  }
78
81
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"ExportScheduler.js","sources":["../../src/orchestrator/ExportScheduler.ts"],"sourcesContent":["import { CompositionModel } from '../model';\nimport { ExportOptions } from '../types';\nimport { WorkerPool } from '../worker/WorkerPool';\nimport { CompositionPlanner } from './CompositionPlanner';\nimport { CacheManager } from '../cache/CacheManager';\nimport { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { MuxManager } from '../stages/mux/MuxManager';\nimport { AudioExportSession } from './AudioExportSession';\nimport { VideoClipSession } from './VideoClipSession';\nimport { WorkerType } from '../worker/types';\nimport { hasResourceId, type TimeUs } from '../model/types';\nimport type { ExportController } from '../controllers/ExportController';\nimport { EventBus } from '../event/EventBus';\nimport { MeframeEvent, EventPayloadMap } from '../event/events';\n\ninterface ExportSchedulerDeps {\n workerPool: WorkerPool;\n planner: CompositionPlanner;\n cacheManager: CacheManager;\n resourceLoader: ResourceLoader;\n muxManager: MuxManager;\n audioSession: AudioExportSession;\n workerConfigsProvider: () => Record<WorkerType, any>;\n eventBus: EventBus<EventPayloadMap>;\n}\n\ninterface ExtendedExportOptions extends ExportOptions {\n signal?: AbortSignal;\n controller?: ExportController;\n}\n\nexport class ExportScheduler {\n private deps: ExportSchedulerDeps;\n\n constructor(deps: ExportSchedulerDeps) {\n this.deps = deps;\n }\n\n async execute(model: CompositionModel, options: ExtendedExportOptions): Promise<Blob> {\n this.deps.cacheManager.clear();\n\n const projectId = this.deps.cacheManager.resourceCache.projectId;\n\n if (!navigator.locks) {\n return this.executeInternal(model, options);\n }\n\n const lockName = `meframe-resource-${projectId}`;\n return navigator.locks.request(lockName, () => this.executeInternal(model, options));\n }\n\n private async executeInternal(\n model: CompositionModel,\n options: ExtendedExportOptions\n ): Promise<Blob> {\n const { muxManager, audioSession, eventBus, resourceLoader } = this.deps;\n const signal = options.signal;\n const controller = options.controller;\n\n const checkStatus = async () => {\n if (signal?.aborted) {\n throw new DOMException('Export aborted', 'AbortError');\n }\n if (controller?.isPaused()) {\n // Wait until resumed\n while (controller.isPaused()) {\n if (signal?.aborted) throw new DOMException('Export aborted', 'AbortError');\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n }\n };\n\n const width = options.width || model.renderConfig?.width || 720;\n const height = options.height || model.renderConfig?.height || 1280;\n const fps = options.fps || model.fps || 30;\n\n eventBus.emit(MeframeEvent.ExportStart, {\n format: options.format || 'mp4',\n width,\n height,\n fps,\n durationUs: model.durationUs,\n });\n\n try {\n // 1. Preload and parse all resources (0-40%)\n await this.preloadResources(model, resourceLoader, eventBus, checkStatus);\n\n // 2. Start Muxer\n muxManager.start({\n width,\n height,\n fps,\n });\n\n // 3. Process Video and Audio\n const mainTrack = model.tracks.find((t) => t.id === model.mainTrackId);\n if (mainTrack && mainTrack.clips.length > 0) {\n // Skip audio pipeline entirely if no audio samples exist (common for video-only projects).\n // This reduces CPU time and avoids initializing AudioEncoder unnecessarily.\n const hasAudioSamples = this.deps.cacheManager.audioSampleCache.getTotalBytes() > 0;\n\n const audioPromise = hasAudioSamples\n ? this.processAudioInWindows(model.durationUs, audioSession, muxManager, checkStatus)\n : Promise.resolve();\n\n // Process video clips sequentially\n await this.processVideoClipsSequentially(mainTrack.clips, muxManager, model, checkStatus);\n\n // Wait for audio encoding to complete\n await audioPromise;\n } else {\n console.warn('[ExportScheduler] No video clips found');\n }\n\n // Finalize audio session (close encoder)\n await audioSession.finalizeExportAudio();\n\n if (signal?.aborted) {\n throw new DOMException('Export aborted', 'AbortError');\n }\n\n // 4. Finalize\n const blob = await muxManager.finalize();\n\n eventBus.emit(MeframeEvent.ExportComplete, {\n size: blob.size,\n durationMs: model.durationUs / 1000,\n format: options.format || 'mp4',\n });\n\n return blob;\n } catch (error) {\n eventBus.emit(MeframeEvent.ExportError, {\n error: error instanceof Error ? error : new Error(String(error)),\n stage: 'export',\n });\n throw error;\n }\n }\n\n /**\n * Preload all resources (0-40% progress)\n */\n private async preloadResources(\n model: CompositionModel,\n resourceLoader: ResourceLoader,\n eventBus: EventBus<EventPayloadMap>,\n checkStatus: () => Promise<void>\n ): Promise<void> {\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 0,\n stage: 'preparing',\n message: 'Loading and parsing resources...',\n });\n\n // Collect resources in horizontal order (clip index priority)\n const tracks = model.tracks.filter((track) => ['video', 'audio'].includes(track.kind));\n if (tracks.length === 0) return;\n\n const maxClipCount = Math.max(...tracks.map((track) => track.clips.length));\n const resourcesToLoad: string[] = [];\n const seen = new Set<string>();\n\n // Horizontal collection: clip[0] from all tracks, then clip[1], etc.\n for (let clipIndex = 0; clipIndex < maxClipCount; clipIndex++) {\n for (const track of tracks) {\n const clip = track.clips[clipIndex];\n if (clip && hasResourceId(clip)) {\n if (!seen.has(clip.resourceId)) {\n seen.add(clip.resourceId);\n resourcesToLoad.push(clip.resourceId);\n }\n }\n }\n }\n\n // Load resources with progress updates (concurrent; ResourceLoader already enforces maxConcurrent).\n const total = resourcesToLoad.length;\n let completed = 0;\n\n await Promise.all(\n resourcesToLoad.map(async (resourceId) => {\n await checkStatus();\n await resourceLoader.load(resourceId, { isPreload: false });\n completed++;\n\n // Update progress: 0-40%\n const progress = total > 0 ? (completed / total) * 0.4 : 0.4;\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress,\n stage: 'preparing',\n message: `Loading resources... (${completed}/${total})`,\n });\n })\n );\n }\n\n /**\n * Process audio in 60-second windows\n * - Videos ≤60s: Single pass (zero boundaries)\n * - Videos >60s: 60s windows (minimal boundaries, ~23MB per window)\n */\n private async processAudioInWindows(\n totalDurationUs: TimeUs,\n audioSession: AudioExportSession,\n muxManager: MuxManager,\n checkStatus: () => Promise<void>\n ): Promise<void> {\n const WINDOW_DURATION_US = 5 * 60 * 1_000_000; // 5 minutes\n let currentUs = 0;\n\n while (currentUs < totalDurationUs) {\n await checkStatus();\n\n const endUs = Math.min(currentUs + WINDOW_DURATION_US, totalDurationUs);\n\n await audioSession.mixAndEncodeSegment(currentUs, endUs, (chunk, meta) =>\n muxManager.writeAudioChunk(chunk, meta)\n );\n\n // Clear audio cache after encoding each window to prevent memory accumulation\n // This is safe because audio data is already encoded and won't be reused\n this.deps.cacheManager.clearAudioCache();\n\n currentUs = endUs;\n }\n }\n\n private async processVideoClipsSequentially(\n clips: any[],\n muxManager: MuxManager,\n model: CompositionModel,\n checkStatus: () => Promise<void>\n ) {\n // Use actual last written timestamp + duration as offset for next clip\n // This avoids duplicate PTS when mp4-muxer rounds microseconds to timescale\n let nextClipStartUs = 0;\n // Track last chunk's end time (timestamp + duration) for precise offset calculation\n let lastChunkEndUs = 0;\n\n for (let i = 0; i < clips.length; i++) {\n const clip = clips[i];\n const currentClipOffsetUs = nextClipStartUs;\n\n await checkStatus(); // Check before starting new clip\n\n const sessionId = `${clip.id}-export`;\n let streamFinishedResolver: () => void;\n let streamFinishedRejecter: (err: any) => void;\n const streamFinishedPromise = new Promise<void>((resolve, reject) => {\n streamFinishedResolver = resolve;\n streamFinishedRejecter = reject;\n });\n\n const session = await VideoClipSession.create({\n clipId: clip.id,\n sessionId,\n planner: this.deps.planner,\n workerPool: this.deps.workerPool,\n cacheManager: this.deps.cacheManager,\n compositionModel: model,\n workerConfigs: this.deps.workerConfigsProvider(),\n resourceLoader: this.deps.resourceLoader,\n callbacks: {\n onEncodedStreamReady: async (stream, track) => {\n if (track === 'video') {\n const reader = stream.getReader();\n try {\n while (true) {\n await checkStatus();\n\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n const originalChunk = value.chunk;\n const metadata = value.metadata;\n const chunkDuration = originalChunk.duration ?? 33333; // Default ~30fps\n\n const remappedTimestamp = originalChunk.timestamp + currentClipOffsetUs;\n\n const buffer = new ArrayBuffer(originalChunk.byteLength);\n originalChunk.copyTo(buffer);\n\n const remappedChunk = new EncodedVideoChunk({\n type: originalChunk.type,\n timestamp: remappedTimestamp,\n duration: chunkDuration,\n data: buffer,\n });\n\n muxManager.writeVideoChunk(remappedChunk, metadata);\n\n // Track end time for next clip's offset\n lastChunkEndUs = remappedTimestamp + chunkDuration;\n\n // Emit progress: 40-100%\n const encodingProgress = remappedTimestamp / model.durationUs;\n const totalProgress = 0.4 + encodingProgress * 0.6; // 40% + (0-60%)\n\n this.deps.eventBus.emit(MeframeEvent.ExportProgress, {\n progress: Math.min(1.0, totalProgress),\n stage: 'encoding',\n timeUs: remappedTimestamp,\n });\n }\n }\n streamFinishedResolver();\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') {\n streamFinishedRejecter(error);\n } else {\n console.error(`[ExportScheduler] Stream error for clip ${clip.id}:`, error);\n streamFinishedRejecter(error);\n }\n } finally {\n reader.releaseLock();\n }\n }\n },\n // Note: Attachment resources are loaded in VideoClipSession.connectPipeline\n // before sending video stream, ensuring watermarks appear from the first frame\n },\n });\n\n await session.activate();\n await streamFinishedPromise;\n\n await session.dispose();\n\n // Use actual last chunk end time as next clip's start\n // This ensures no timestamp overlap even after muxer rounding\n nextClipStartUs = lastChunkEndUs;\n }\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ExportScheduler.js","sources":["../../src/orchestrator/ExportScheduler.ts"],"sourcesContent":["import { CompositionModel } from '../model';\nimport { ExportOptions } from '../types';\nimport { WorkerPool } from '../worker/WorkerPool';\nimport { CompositionPlanner } from './CompositionPlanner';\nimport { CacheManager } from '../cache/CacheManager';\nimport { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { MuxManager } from '../stages/mux/MuxManager';\nimport { AudioExportSession } from './AudioExportSession';\nimport { VideoClipSession } from './VideoClipSession';\nimport { WorkerType } from '../worker/types';\nimport { hasResourceId, type TimeUs } from '../model/types';\nimport type { ExportController } from '../controllers/ExportController';\nimport { EventBus } from '../event/EventBus';\nimport { MeframeEvent, EventPayloadMap } from '../event/events';\n\ninterface ExportSchedulerDeps {\n workerPool: WorkerPool;\n planner: CompositionPlanner;\n cacheManager: CacheManager;\n resourceLoader: ResourceLoader;\n muxManager: MuxManager;\n audioSession: AudioExportSession;\n workerConfigsProvider: () => Record<WorkerType, any>;\n eventBus: EventBus<EventPayloadMap>;\n}\n\ninterface ExtendedExportOptions extends ExportOptions {\n signal?: AbortSignal;\n controller?: ExportController;\n}\n\nexport class ExportScheduler {\n private deps: ExportSchedulerDeps;\n\n constructor(deps: ExportSchedulerDeps) {\n this.deps = deps;\n }\n\n async execute(model: CompositionModel, options: ExtendedExportOptions): Promise<Blob> {\n this.deps.cacheManager.clear();\n\n const projectId = this.deps.cacheManager.resourceCache.projectId;\n\n if (!navigator.locks) {\n return this.executeInternal(model, options);\n }\n\n const lockName = `meframe-resource-${projectId}`;\n return navigator.locks.request(lockName, () => this.executeInternal(model, options));\n }\n\n private async executeInternal(\n model: CompositionModel,\n options: ExtendedExportOptions\n ): Promise<Blob> {\n const { muxManager, audioSession, eventBus, resourceLoader } = this.deps;\n const signal = options.signal;\n const controller = options.controller;\n\n const checkStatus = async () => {\n if (signal?.aborted) {\n throw new DOMException('Export aborted', 'AbortError');\n }\n // TODO: ugly\n if (controller?.isPaused()) {\n // Wait until resumed\n while (controller.isPaused()) {\n if (signal?.aborted) throw new DOMException('Export aborted', 'AbortError');\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n }\n };\n\n const width = options.width || model.renderConfig?.width || 720;\n const height = options.height || model.renderConfig?.height || 1280;\n const fps = options.fps || model.fps || 30;\n\n eventBus.emit(MeframeEvent.ExportStart, {\n format: options.format || 'mp4',\n width,\n height,\n fps,\n durationUs: model.durationUs,\n });\n\n this.deps.cacheManager.beginExport();\n\n try {\n // 1. Preload and parse all resources (0-40%)\n await this.preloadResources(model, resourceLoader, eventBus, checkStatus);\n\n // 2. Start Muxer\n muxManager.start({\n width,\n height,\n fps,\n });\n\n // 3. Process Video and Audio\n const mainTrack = model.tracks.find((t) => t.id === model.mainTrackId);\n if (mainTrack && mainTrack.clips.length > 0) {\n // Skip audio pipeline entirely if no audio samples exist (common for video-only projects).\n // This reduces CPU time and avoids initializing AudioEncoder unnecessarily.\n const hasAudioSamples = this.deps.cacheManager.audioSampleCache.getTotalBytes() > 0;\n\n const audioPromise = hasAudioSamples\n ? this.processAudioInWindows(model.durationUs, audioSession, muxManager, checkStatus)\n : Promise.resolve();\n\n // Process video clips sequentially\n await this.processVideoClipsSequentially(mainTrack.clips, muxManager, model, checkStatus);\n\n // Wait for audio encoding to complete\n await audioPromise;\n } else {\n console.warn('[ExportScheduler] No video clips found');\n }\n\n // Finalize audio session (close encoder)\n await audioSession.finalizeExportAudio();\n\n if (signal?.aborted) {\n throw new DOMException('Export aborted', 'AbortError');\n }\n\n // 4. Finalize\n const blob = await muxManager.finalize();\n\n eventBus.emit(MeframeEvent.ExportComplete, {\n size: blob.size,\n durationMs: model.durationUs / 1000,\n format: options.format || 'mp4',\n });\n\n return blob;\n } catch (error) {\n eventBus.emit(MeframeEvent.ExportError, {\n error: error instanceof Error ? error : new Error(String(error)),\n stage: 'export',\n });\n throw error;\n } finally {\n this.deps.cacheManager.endExport();\n }\n }\n\n /**\n * Preload all resources (0-40% progress)\n */\n private async preloadResources(\n model: CompositionModel,\n resourceLoader: ResourceLoader,\n eventBus: EventBus<EventPayloadMap>,\n checkStatus: () => Promise<void>\n ): Promise<void> {\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress: 0,\n stage: 'preparing',\n message: 'Loading and parsing resources...',\n });\n\n // Collect resources in horizontal order (clip index priority)\n const tracks = model.tracks.filter((track) => ['video', 'audio'].includes(track.kind));\n if (tracks.length === 0) return;\n\n const maxClipCount = Math.max(...tracks.map((track) => track.clips.length));\n const resourcesToLoad: string[] = [];\n const seen = new Set<string>();\n\n // Horizontal collection: clip[0] from all tracks, then clip[1], etc.\n for (let clipIndex = 0; clipIndex < maxClipCount; clipIndex++) {\n for (const track of tracks) {\n const clip = track.clips[clipIndex];\n if (clip && hasResourceId(clip)) {\n if (!seen.has(clip.resourceId)) {\n seen.add(clip.resourceId);\n resourcesToLoad.push(clip.resourceId);\n }\n }\n }\n }\n\n // Load resources with progress updates (concurrent; ResourceLoader already enforces maxConcurrent).\n const total = resourcesToLoad.length;\n let completed = 0;\n\n await Promise.all(\n resourcesToLoad.map(async (resourceId) => {\n await checkStatus();\n await resourceLoader.load(resourceId, { isPreload: false });\n completed++;\n\n // Update progress: 0-40%\n const progress = total > 0 ? (completed / total) * 0.4 : 0.4;\n eventBus.emit(MeframeEvent.ExportProgress, {\n progress,\n stage: 'preparing',\n message: `Loading resources... (${completed}/${total})`,\n });\n })\n );\n }\n\n /**\n * Process audio in 60-second windows\n * - Videos ≤60s: Single pass (zero boundaries)\n * - Videos >60s: 60s windows (minimal boundaries, ~23MB per window)\n */\n private async processAudioInWindows(\n totalDurationUs: TimeUs,\n audioSession: AudioExportSession,\n muxManager: MuxManager,\n checkStatus: () => Promise<void>\n ): Promise<void> {\n const WINDOW_DURATION_US = 5 * 60 * 1_000_000; // 5 minutes\n let currentUs = 0;\n\n while (currentUs < totalDurationUs) {\n await checkStatus();\n\n const endUs = Math.min(currentUs + WINDOW_DURATION_US, totalDurationUs);\n\n await audioSession.mixAndEncodeSegment(currentUs, endUs, (chunk, meta) =>\n muxManager.writeAudioChunk(chunk, meta)\n );\n\n // Clear audio cache after encoding each window to prevent memory accumulation\n // This is safe because audio data is already encoded and won't be reused\n this.deps.cacheManager.clearAudioCache();\n\n currentUs = endUs;\n }\n }\n\n private async processVideoClipsSequentially(\n clips: any[],\n muxManager: MuxManager,\n model: CompositionModel,\n checkStatus: () => Promise<void>\n ) {\n // Use actual last written timestamp + duration as offset for next clip\n // This avoids duplicate PTS when mp4-muxer rounds microseconds to timescale\n let nextClipStartUs = 0;\n // Track last chunk's end time (timestamp + duration) for precise offset calculation\n let lastChunkEndUs = 0;\n\n for (let i = 0; i < clips.length; i++) {\n const clip = clips[i];\n const currentClipOffsetUs = nextClipStartUs;\n\n await checkStatus(); // Check before starting new clip\n\n const sessionId = `${clip.id}-export`;\n let streamFinishedResolver: () => void;\n let streamFinishedRejecter: (err: any) => void;\n const streamFinishedPromise = new Promise<void>((resolve, reject) => {\n streamFinishedResolver = resolve;\n streamFinishedRejecter = reject;\n });\n\n const session = await VideoClipSession.create({\n clipId: clip.id,\n sessionId,\n planner: this.deps.planner,\n workerPool: this.deps.workerPool,\n cacheManager: this.deps.cacheManager,\n compositionModel: model,\n workerConfigs: this.deps.workerConfigsProvider(),\n resourceLoader: this.deps.resourceLoader,\n callbacks: {\n onEncodedStreamReady: async (stream, track) => {\n if (track === 'video') {\n const reader = stream.getReader();\n try {\n while (true) {\n await checkStatus();\n\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n const originalChunk = value.chunk;\n const metadata = value.metadata;\n const chunkDuration = originalChunk.duration ?? 33333; // Default ~30fps\n\n const remappedTimestamp = originalChunk.timestamp + currentClipOffsetUs;\n\n const buffer = new ArrayBuffer(originalChunk.byteLength);\n originalChunk.copyTo(buffer);\n\n const remappedChunk = new EncodedVideoChunk({\n type: originalChunk.type,\n timestamp: remappedTimestamp,\n duration: chunkDuration,\n data: buffer,\n });\n\n muxManager.writeVideoChunk(remappedChunk, metadata);\n\n // Track end time for next clip's offset\n lastChunkEndUs = remappedTimestamp + chunkDuration;\n\n // Emit progress: 40-100%\n const encodingProgress = remappedTimestamp / model.durationUs;\n const totalProgress = 0.4 + encodingProgress * 0.6; // 40% + (0-60%)\n\n this.deps.eventBus.emit(MeframeEvent.ExportProgress, {\n progress: Math.min(1.0, totalProgress),\n stage: 'encoding',\n timeUs: remappedTimestamp,\n });\n }\n }\n streamFinishedResolver();\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') {\n streamFinishedRejecter(error);\n } else {\n console.error(`[ExportScheduler] Stream error for clip ${clip.id}:`, error);\n streamFinishedRejecter(error);\n }\n } finally {\n reader.releaseLock();\n }\n }\n },\n // Note: Attachment resources are loaded in VideoClipSession.connectPipeline\n // before sending video stream, ensuring watermarks appear from the first frame\n },\n });\n\n await session.activate();\n await streamFinishedPromise;\n\n await session.dispose();\n\n // Use actual last chunk end time as next clip's start\n // This ensures no timestamp overlap even after muxer rounding\n nextClipStartUs = lastChunkEndUs;\n }\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meframe/core",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Next generation media processing framework based on WebCodecs",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",