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 |
Install
Section titled “Install”npm install @alpha2-sh/editor alpha2pnpm add @alpha2-sh/editor alpha2yarn add @alpha2-sh/editor alpha2Documents
Section titled “Documents”An editor document wraps an engine project:
document.projectis the engine’s project.document.assetsdescribes each asset in the bin (id,kind,name,contentRevision, and optionallylocator,fingerprint,missing,authoring).document.metadataholds 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 givenconst json = serializeDocument(document); // portable JSONconst reopened = parseDocument(json); // also reads a plain engine projectcreateEmptyDocument(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.
Sessions and commands
Section titled “Sessions and commands”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
transactionapplies several commands and is checked once, by the document it ends with. It is one undo step. canExecute(command)returns the same result asdispatchwithout applying it.- Times are timeline ticks (120,000 per second) unless a command says otherwise.
- A refusal is an
EditorError. Branch on itscode(invalid_command,locked, and others);operationandentityIdsay where it happened.
The commands
Section titled “The commands”| 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.
Drag gestures
Section titled “Drag gestures”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.draftgesture.update({ type: "clip.move", id: "clip-2", at: 300_000 });gesture.commit(); // one undo step; gesture.cancel() adds noneEach 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.
Watch for changes
Section titled “Watch for changes”subscribe(listener)callslistenerafter 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 withuseSyncExternalStore.subscribeSelector(selector, listener, equal?)callslisteneronly 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).
Preview, play and export in the browser
Section titled “Preview, play and export in the browser”@alpha2-sh/editor/browser connects a session to the alpha2 engine.
- Make a bridge with
createBrowserBridge({ editor, runtimeAssets }).runtimeAssetsgives the URLs your bundler serves foralpha2/wasm/alpha2_wasm_bg.wasm(wasmUrl),alpha2/wasm/alpha2_audio_worklet.wasm(audioWasmUrl) and thealpha2/audio-worklet-processormodule (workletUrl). - Bind a canvas with
bindPreview(canvas). - Import files, place them with commands, and
seek,playorpause. - 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();Import
Section titled “Import”importFiles(files)checks each file with the engine (video, audio, images, fonts,.cubeLUTs) 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.
Preview and playback
Section titled “Preview and playback”bindPreview(canvas)draws on a canvas and resolves with a binding youdispose().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.rendereris"auto"(default),"gpu"or"cpu", as in the browser guide.audio: falsepreviews 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). |
Export
Section titled “Export”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:
EditorProvidermakes 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.EditorPreviewbinds 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> );}Export on a server
Section titled “Export on a server”@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.
- Browser: the engine the bridge runs.
- Node.js: export the same documents natively.
- Validation and errors: what the engine refuses and why.
