Skip to content

Editor

@alpha2-sh/editor is an editing SDK over the engine’s project document. After reading this page you can hold a project in an editing session, change it with typed commands that validate and undo, drive a drag gesture, and connect the session to a browser preview, playback with audio and export.

The package has four entry points:

Import What it has Runs in
@alpha2-sh/editor Documents, commands, sessions, undo Browser and Node.js (no DOM, React, WebAssembly or audio)
@alpha2-sh/editor/browser createBrowserBridge: import, preview, playback, export Browser
@alpha2-sh/editor/react EditorProvider, useEditor, useEditorSelector, EditorPreview React
@alpha2-sh/editor/native-client createNativeClient: export on your own native export service Browser
Terminal window
npm install @alpha2-sh/editor alpha2

An editor document wraps an engine project:

  • document.project is the engine’s project.
  • document.assets describes each asset in the bin (id, kind, name, contentRevision, and optionally locator, fingerprint, missing, authoring).
  • document.metadata holds editor-only data: markers, the export range, track and clip metadata, graph layout.

Media bytes never live in the document.

import { createEmptyDocument, serializeDocument, parseDocument } from "@alpha2-sh/editor";
const document = createEmptyDocument({ width: 1280, height: 720 }); // 30 fps, 48 kHz unless given
const json = serializeDocument(document); // portable JSON
const reopened = parseDocument(json); // also reads a plain engine project

createEmptyDocument(settings) makes a document with no media, 1920x1080 at 30 fps and 48 kHz unless settings say otherwise. serializeDocument throws an EditorError for a document that does not validate.

createEditor({ document, historyLimit, historyByteLimit }) makes an EditorSession. Every change goes through dispatch(command), which validates the result and returns { ok: true, value } or { ok: false, error }. A refused command changes nothing.

import { createEditor, createEmptyDocument } from "@alpha2-sh/editor";
const editor = createEditor({ document: createEmptyDocument({ width: 1280, height: 720 }) });
const result = editor.dispatch({
type: "transaction",
commands: [
{
type: "asset.put",
asset: { id: "backdrop", kind: "generated", name: "Backdrop", contentRevision: 0 },
source: {
id: "backdrop",
kind: "generated",
generator: { type: "fill", paint: { type: "solid", color: { red: 0.17, green: 0.36, blue: 0.99, alpha: 1 } } },
},
},
{ type: "track.add", track: { id: "video", name: "Video", kind: "video", clips: [] } },
{
type: "clip.insert",
trackId: "video",
clip: {
id: "clip-1",
source_id: "backdrop",
timeline_range: { start: 0, end: 240_000 },
source_range: { start: 0, end: 240_000 },
},
},
],
});
if (!result.ok) throw result.error; // an EditorError with a `code`
editor.dispatch({ type: "clip.split", id: "clip-1", at: 120_000, newId: "clip-2" });
editor.undo();
editor.redo();
await editor.dispose();
  • A transaction applies several commands and is checked once, by the document it ends with. It is one undo step.
  • canExecute(command) returns the same result as dispatch without applying it.
  • Times are timeline ticks (120,000 per second) unless a command says otherwise.
  • A refusal is an EditorError. Branch on its code (invalid_command, locked, and others); operation and entityId say where it happened.
Area Commands
Project project.update, metadata.update
Tracks track.add, track.remove, track.reorder, track.update, track.metadata
Clips clip.insert, clip.place (insert, overwrite or append), clip.delete, clip.update, clip.metadata, clip.move, clips.move, clip.trim, clip.split, clip.slip, clip.slide, clip.roll, clip.retime
Effects effect.add, effect.update, effect.remove, effect.reorder
Transitions transition.add, transition.resize, transition.remove
Captions cue.add, cue.update, cue.remove, cue.split, cue.trim
Composition graphs graph.create, graph.node.add, graph.node.update, graph.node.remove, graph.connect, graph.disconnect, graph.output, graph.group, graph.ungroup, graph.port.add, graph.control.add, graph.control.remove, graph.layout, composition.add, composition.create
Assets and markers asset.put, asset.remove, marker.add, marker.update, marker.remove
Batches transaction

The clip commands are built on the edit operations in alpha2/edit. The Command type in the package’s declarations documents every field.

Every change that commits is one undo step. undo() and redo() return false when there is nothing to step to. The history keeps up to 120 steps and 64 MiB by default; set historyLimit and historyByteLimit in createEditor to change that. clearHistory() drops it.

beginGesture() previews a drag without touching the document or the history:

const gesture = editor.beginGesture();
gesture.update({ type: "clip.move", id: "clip-2", at: 360_000 }); // shown in editor.draft
gesture.update({ type: "clip.move", id: "clip-2", at: 300_000 });
gesture.commit(); // one undo step; gesture.cancel() adds none

Each update runs a command on the gesture’s starting document and, when valid, shows the result as editor.draft. commit() validates the last candidate fully and commits it. A commit, cancel, dispatch, undo, redo or new gesture ends the gesture; a later update returns stale_gesture.

  • subscribe(listener) calls listener after every change and returns the unsubscribe function.
  • getSnapshot() returns { document, draft, selection, revision, generation, canUndo, canRedo }, the same object until the next change, so it works with useSyncExternalStore.
  • subscribeSelector(selector, listener, equal?) calls listener only when the selected value changes.
const stop = editor.subscribeSelector(
(snapshot) => snapshot.document.project.timeline.tracks.length,
(count) => console.log("tracks:", count),
);

The session also keeps a selection (select(ids)) and a clipboard (copy, paste).

@alpha2-sh/editor/browser connects a session to the alpha2 engine.

  1. Make a bridge with createBrowserBridge({ editor, runtimeAssets }). runtimeAssets gives the URLs your bundler serves for alpha2/wasm/alpha2_wasm_bg.wasm (wasmUrl), alpha2/wasm/alpha2_audio_worklet.wasm (audioWasmUrl) and the alpha2/audio-worklet-processor module (workletUrl).
  2. Bind a canvas with bindPreview(canvas).
  3. Import files, place them with commands, and seek, play or pause.
  4. Export with startExport(options).
import { createEditor, createEmptyDocument } from "@alpha2-sh/editor";
import { createBrowserBridge } from "@alpha2-sh/editor/browser";
const editor = createEditor({ document: createEmptyDocument({ width: 1280, height: 720 }) });
const bridge = createBrowserBridge({
editor,
runtimeAssets: { wasmUrl, audioWasmUrl, workletUrl },
});
await bridge.bindPreview(canvas);
const { assetIds } = await bridge.importFiles(files); // from an <input type="file">
const trackId = crypto.randomUUID();
const placed = editor.dispatch({
type: "transaction",
commands: [
{ type: "track.add", track: { id: trackId, kind: "video", name: "Video", clips: [] } },
{
type: "clip.insert",
trackId,
clip: {
id: crypto.randomUUID(),
source_id: assetIds[0],
timeline_range: { start: 0, end: 120_000 },
source_range: { start: 0, end: 120_000 },
time_map: { type: "forward" },
},
},
],
});
if (!placed.ok) throw placed.error;
await bridge.seek(0);
const job = bridge.startExport({ container: "webm" });
job.subscribe((progress) => console.log(progress.phase, progress.fraction));
const artifact = await job.result;
// artifact.kind "file": a temporary file in the origin's private storage.
if (artifact.kind === "file") await artifact.release(); // once downloaded
await bridge.dispose();
await editor.dispose();
  • importFiles(files) checks each file with the engine (video, audio, images, fonts, .cube LUTs) and adds them to the document in one undoable step.
  • importCaptions(file) reads an SRT or WebVTT file into a captions asset. importSequence(files, { frameRate, missing }) imports numbered PNG or OpenEXR files as one image sequence.
  • relink(id, file) points an asset at a new file of the same media.
  • bindPreview(canvas) draws on a canvas and resolves with a binding you dispose().
  • seek(tick) moves the playhead and renders that frame. play(rate, { loop, policy }) plays from the playhead; pause() stops. With audio, playback follows the audio clock.
  • previewScale (bridge option, default 0.5) sets the preview resolution; at 1, text and effects match an export pixel for pixel. setPreviewScale(scale) changes it later.
  • renderer is "auto" (default), "gpu" or "cpu", as in the browser guide. audio: false previews picture only.

Workers take work off the page’s thread. Each is optional:

runtimeAssets field Worker entry Moves off the page
workerFactory @alpha2-sh/editor/preview-worker Preview rendering. Without it, or when the worker fails, the main thread renders.
hashWorkerFactory @alpha2-sh/editor/hash-worker Hashing imported files.
extensionsWorkerFactory @alpha2-sh/editor/extensions-worker Reading SVG, animated images, Lottie and OpenEXR (with extensionsWasmUrl).

startExport(options) returns an ExportJob with result (a promise of the file), subscribe(listener) for progress, and cancel().

Option Default
container mp4 mp4, mov, mkv or webm. Limits the codecs.
videoCodec the container’s (avc, or vp9 for WebM) avc, hevc, vp9 or av1.
audioCodec the first of the container’s this browser encodes aac or opus.
videoQuality, audioQuality high very-low, low, medium, high, very-high.
includeAudio true
frameRate the project’s An exact { num, den }.
media original proxies reads fresh proxies where there are some.
output { kind: "opfs" } A temporary file in the origin’s private storage; { kind: "file", handle } for a file the user picked; { kind: "memory", maxBytes } (default 512 MiB).
videoDecode auto software decodes on the CPU.

A codec the container does not offer, or that this browser does not encode, is refused before the export renders. Ask ahead with bridge.videoEncoders(). The job’s progress phase runs through preparing, rendering, finalizing and completed (or failed, cancelled, retrying), with fraction from 0 to 1.

@alpha2-sh/editor/react has four pieces:

  • EditorProvider makes a session available below it. The application owns the session: unmounting the provider does not dispose it.
  • useEditor() returns that session.
  • useEditorSelector(selector, equal?) re-renders when the selected value changes. Inline selectors are fine; the result is cached per snapshot.
  • EditorPreview binds a bridge’s preview to a canvas it makes, and releases it on unmount.
import { EditorPreview, EditorProvider, useEditor, useEditorSelector } from "@alpha2-sh/editor/react";
function UndoButton() {
const editor = useEditor();
const canUndo = useEditorSelector((snapshot) => snapshot.canUndo);
return <button disabled={!canUndo} onClick={() => editor.undo()}>Undo</button>;
}
function App({ editor, bridge }) {
return (
<EditorProvider editor={editor}>
<EditorPreview bridge={bridge} onError={reportError} />
<UndoButton />
</EditorProvider>
);
}

@alpha2-sh/editor/native-client exports a document on an HTTP export service that you run, for example on top of @alpha2-sh/node. createNativeClient({ baseUrl, fetch? }) returns a client whose startExport(editor, assets, settings) uploads the document’s media (from bridge.nativeAssets) and follows the job to a downloadable file. The settings are the browser export’s, plus the mpeg4-part2 video codec the native host can encode.