UNPKG

@meframe/core

Version:

Next generation media processing framework based on WebCodecs

313 lines (312 loc) 9.6 kB
import { CompositionModel } from "./model/CompositionModel.js"; import { loadConfig } from "./config/ConfigLoader.js"; import { Orchestrator } from "./orchestrator/Orchestrator.js"; import { PlaybackController } from "./controllers/PlaybackController.js"; import { PreRenderService } from "./controllers/PreRenderService.js"; import { PluginManager } from "./plugins/PluginManager.js"; import { EventBus } from "./event/EventBus.js"; import { MeframeEvent } from "./event/events.js"; class Meframe { /** Current state - managed internally via setState() */ state = "idle"; /** Configuration - immutable after construction */ config; /** Composition model - managed via setCompositionTree() */ model = null; /** Plugin manager for extensions */ pluginManager; /** Event bus for subscribing to Meframe events */ events; orchestrator; eventBus; playbackController = null; preRenderService = null; canvas; constructor(config) { this.config = config; this.eventBus = new EventBus(); this.events = this.eventBus.asReadonly(); this.pluginManager = new PluginManager(this); this.orchestrator = new Orchestrator({ maxWorkers: config.maxWorkers, cacheConfig: config.cache, eventBus: this.eventBus, workerPath: config.global.workerPath, workerExtension: config.global.workerExtension }); } /** * Create a new Meframe instance */ static async create(config = {}) { const resolvedConfig = await loadConfig({ override: config }); const instance = new Meframe(resolvedConfig); await instance.initialize(); return instance; } /** * Initialize the core engine */ async initialize() { this.setState("loading"); try { await this.orchestrator.initialize(); const plugins = this.config.plugins; if (plugins && Array.isArray(plugins)) { for (const plugin of plugins) { if (plugin && typeof plugin === "object" && "name" in plugin && "install" in plugin) { this.pluginManager.register(plugin); } } } this.setState("idle"); } catch (error) { this.setState("error"); throw error; } } /** * Set the composition model */ async setCompositionModel(model) { this.ensureNotDestroyed(); const compositionModel = model instanceof CompositionModel ? model : new CompositionModel(model); await this.orchestrator.setCompositionModel(compositionModel); this.model = compositionModel; this.setState("ready"); this.eventBus.emit(MeframeEvent.Ready, { trackCount: model.tracks.length, clipCount: model.tracks.reduce( (acc, track) => acc + track.clips?.length || 0, 0 ), durationUs: model.durationUs }); } /** * Apply a patch to the composition model */ async applyPatch(patch) { this.ensureNotDestroyed(); if (!this.model) { throw new Error("No composition model set"); } await this.orchestrator.applyPatch(patch); this.model = this.orchestrator.compositionModel; } /** * Start preview and return a handle for control */ startPreview(options) { this.ensureReady(); const canvas = options?.canvas || this.canvas; if (!canvas) { throw new Error("Canvas is required for preview"); } this.canvas = canvas; if (!this.playbackController) { this.playbackController = new PlaybackController(this.orchestrator, this.eventBus, { canvas, startUs: options?.startUs }); this.playbackController.setAudioSession(this.orchestrator.audioSession); } if (!this.preRenderService) { this.preRenderService = new PreRenderService(this.orchestrator, this.events); this.preRenderService.start(); } this.playbackController.on(MeframeEvent.PlaybackTimeUpdate, (payload) => { this.preRenderService?.updatePlaybackTime(payload.timeUs); }); this.playbackController.on(MeframeEvent.PlaybackPlay, () => { this.preRenderService?.setPlaybackActive(true); }); this.playbackController.on(MeframeEvent.PlaybackPause, () => { this.preRenderService?.setPlaybackActive(false); }); this.playbackController.on(MeframeEvent.PlaybackStop, () => { this.preRenderService?.setPlaybackActive(false); }); this.playbackController?.renderCurrentFrame(0); return this.playbackController; } /** * Export the composition * Uses L2 cache for fast export, muxes in main thread */ async export(options) { this.ensureReady(); this.setState("exporting"); try { const model = this.model; if (!model) { throw new Error("No composition model set"); } const readiness = await this.getExportReadiness(); if (!readiness.ready) { this.eventBus.emit(MeframeEvent.ExportPreparing, { totalClips: readiness.totalClips, cachedClips: readiness.cachedClips, missingClips: readiness.missingClips }); if (!this.preRenderService) { this.preRenderService = new PreRenderService(this.orchestrator, this.events); this.preRenderService.start(); } const abortController = new AbortController(); const timeoutMs = 5 * 60 * 1e3; const timeoutId = setTimeout(() => { abortController.abort(); }, timeoutMs); try { await this.preRenderService.ensureClipsInL2(readiness.missingClips, { onProgress: (completed, total) => { this.eventBus.emit(MeframeEvent.ExportProgress, { progress: completed / total, stage: "preparing", message: `Preparing cache: ${completed}/${total} clips` }); }, signal: abortController.signal }); } catch (error) { if (error instanceof DOMException && error.name === "AbortError") { throw new Error(`Export preparation timeout after ${timeoutMs / 1e3}s`); } throw error; } finally { clearTimeout(timeoutId); } } const width = options.width || model.renderConfig?.width || 720; const height = options.height || model.renderConfig?.height || 1280; const fps = options.fps || model.fps || 30; this.eventBus.emit(MeframeEvent.ExportStart, { format: options.format || "mp4", width, height, fps, durationUs: model.durationUs }); const blob = await this.orchestrator.export(model, options); this.setState("ready"); this.eventBus.emit(MeframeEvent.ExportComplete, { size: blob.size, durationMs: model.durationUs / 1e3, format: options.format || "mp4" }); this.eventBus.emit(MeframeEvent.ExportProgress, { progress: 1 }); return blob; } catch (error) { this.setState("error"); this.eventBus.emit(MeframeEvent.ExportError, { error, stage: "export" }); throw error; } } /** * Check export readiness * Returns coverage info for L2 cache */ async getExportReadiness() { if (!this.model) { return { ready: false, totalClips: 0, cachedClips: 0, missingClips: [], coverage: 0 }; } const mainTrack = this.model.findTrack(this.model.mainTrackId); const allClips = mainTrack?.clips ?? []; const missingClips = []; let cachedCount = 0; for (const clip of allClips) { const inL2 = await this.orchestrator.cacheManager.hasClipInL2(clip.id, "video"); if (inL2) { cachedCount++; } else { missingClips.push(clip.id); } } return { ready: missingClips.length === 0, totalClips: allClips.length, cachedClips: cachedCount, missingClips, coverage: allClips.length > 0 ? cachedCount / allClips.length : 0 }; } /** * Get current playback time */ getCurrentTime() { return this.playbackController?.currentTimeUs || 0; } /** * Clear all L1/L2 cache data * Useful for debugging or forcing re-render */ async clearCache() { this.ensureReady(); this.playbackController?.stop(); this.preRenderService?.stop(); await this.orchestrator.cacheManager.clear(); console.log("[Meframe] Cache cleared successfully"); } /** * Clean up and destroy the instance */ async dispose() { if (this.state === "destroyed") return; this.playbackController?.stop(); this.playbackController?.dispose(); this.playbackController = null; this.preRenderService?.stop(); this.preRenderService = null; await this.pluginManager.disposeAll(); await this.orchestrator.dispose(); this.eventBus.dispose(); this.setState("destroyed"); } /** * Set internal state */ setState(state) { const oldState = this.state; this.state = state; this.eventBus.emit("state:changed", { oldState, newState: state }); } /** * Ensure the instance is not destroyed */ ensureNotDestroyed() { if (this.state === "destroyed") { throw new Error("Core instance is destroyed"); } } /** * Ensure the instance is ready */ ensureReady() { this.ensureNotDestroyed(); if (!this.model) { throw new Error("No composition model set"); } if (this.state === "loading" || this.state === "idle") { throw new Error("Core is not ready"); } } } export { Meframe }; //# sourceMappingURL=Meframe.js.map