@meframe/core
Version:
Next generation media processing framework based on WebCodecs
313 lines (312 loc) • 9.6 kB
JavaScript
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