Skip to content

API ​

The functions, components and options you call from code: the player in the browser, the export you drive from Node, and the kit entry points both build on.

@cutcraft/player ​

tsx
import { Player, type PlayerRef } from "@cutcraft/player";
import "@cutcraft/player/fonts.css";
import { collectModule, resolve } from "@cutcraft/kit";
import * as file from "./video";

const state = resolve(collectModule(file), manifest);
const ref = useRef<PlayerRef>(null);

<Player
  ref={ref}
  state={state}
  element={file.default(file.defaults)}
  media={{ founder: blob, music: "/music.ogg" }}
  controls
/>;

Import @cutcraft/player/fonts.css once in the app. It loads the bundled font families, so the preview sets text in the same faces as the export.

<Player> props ​

PropTypeDefault
stateResolvedStateThe resolved composition, from resolve(). A new state is pushed into the running player.
mediaMediaResolver | Record<string, Blob | string>Where each asset's bytes come from. A map takes a Blob or a URL per asset id.
elementReactNodeThe evaluated file element, file.default(props). Needed only to render the children of each <Layer>.
controlsbooleanShow the control bar: play and pause, scrubber, time, mute and rate.
loopbooleanfalseStart again from 0 at the end.
autoPlaybooleanStart playing once the player is ready.
initiallyMutedbooleanfalseStart muted.
playbackRatenumber1Playback speed, clamped to 0.25 to 4.
fit"contain" | "none""contain"contain scales the composition into the box; none renders at composition pixels.
styleCSSPropertiesMerged into the root element's style.
classNamestringClass of the root element.
audioContextAudioContextUse this context instead of creating one.
onError(e: PlayerErrorEvent) => voidCalled on the error event.
onFrame(e: { time: number; frame: number }) => voidCalled on the frameupdate event.
coreOptionsPartial<PlayerOptions>Options passed through to PlayerCore; meant for tests.

A new PlayerCore is created when media or audioContext changes, so keep both stable between renders. The root element carries data-time and data-frame, and every element carries data-el and data-kind.

PlayerRef ​

The ref of <Player> exposes part of PlayerCore.

MemberType
play()Promise<void>Start playback. From the end, it starts again at 0.
pause()voidPause.
toggle()Promise<void>Play when paused, pause when playing.
seekTo(time)Promise<void>Jump to time seconds and draw its exact frame before resolving.
seekToFrame(frame)Promise<void>Same, by frame index.
getCurrentTime()numberComposition seconds.
getCurrentFrame()numberFrame index at the current time.
isPlaying()booleanWhether it is playing.
addEventListener(type, fn)() => voidListen to an event; returns a function that removes the listener.
removeEventListener(type, fn)voidRemove a listener.
renderFrame(time)Promise<void>Draw time without moving the play head.
captureSheet(times, options?)Promise<HTMLCanvasElement | OffscreenCanvas>A contact sheet of the canvas at times. See captureSheet.
durationnumberComposition length in seconds. Read only.
stateResolvedStateThe current state. Read only.
playbackRatenumberRead and write. Clamped to 0.25 to 4.
loopbooleanRead and write.
mutedbooleanRead and write.
volumenumberRead and write. Clamped to 0 to 1.
corePlayerCoreThe underlying player.
canvasHTMLCanvasElement | nullThe canvas the media is drawn on.

Hooks ​

These work only inside a <Player>; outside one they throw.

HookReturns
usePlayer()PlayerCoreThe player the component sits in.
useCompositionTime()numberComposition seconds. Re-renders once per painted frame and on every seek.
useCompositionFrame()numberFrame index. Updates like useCompositionTime().
useLayerT()numberProgress through the enclosing <Layer>, 0 to 1. Throws outside a <Layer>.

Inside a file, use the kit's useT() and useLayer() instead. A mounted <Player> installs them, and they read the same clock.

PlayerCore ​

The player without React. <Player> is a thin wrapper over it.

ts
import { PlayerCore } from "@cutcraft/player";

const core = new PlayerCore({ state, media: { founder: blob } });
core.attachCanvas(canvas);
await core.seekTo(2);
await core.play();

Constructor options (PlayerOptions):

OptionTypeDefault
stateResolvedStateThe resolved composition.
mediaMediaResolver | Record<string, Blob | string>Where each asset's bytes come from.
audioContextAudioContexta new oneWhen omitted, a context is created at the composition's sampleRate, or 48000.
loopbooleanfalseStart again from 0 at the end.
initiallyMutedbooleanfalseStart muted.
playbackRatenumber1Clamped to 0.25 to 4.
resolveNestedResolveNestedHow a <Nested> sequence is resolved; by default from the state itself.

The remaining options (clock, audioOutput, toContextTime, frameSourceFactory, audioSourceFactory, createCanvas, raf, cancelRaf, setInterval, clearInterval, setTimeout, clearTimeout) are test seams. In a browser the defaults are the audio context clock, Web Audio and mediabunny.

Methods and properties beyond those in PlayerRef:

MemberType
attachCanvas(canvas)voidDraw on this canvas (HTMLCanvasElement, OffscreenCanvas or null). It is sized to the composition.
setState(state)voidPush a new resolved state, for example on every drag step in an editor. A paused player paints at once and fetches the exact frame 120 ms after the last call.
getTime(){ time: number; frame: number }The last painted time and frame.
subscribeTime(fn)() => voidCalled once per painted frame and on every seek. Fits useSyncExternalStore.
resolveMedia(assetId)Promise<MediaHandle>The media handle for an asset id.
dispose()voidPause and release decoders, audio and listeners.

Events ​

Listen with addEventListener(type, fn). The event types are in PlayerEvents.

EventPayload
frameupdate{ time, frame }A new frame was painted.
play{ time }Playback started.
pause{ time }Playback paused.
seeked{ time }A seek finished and its frame is drawn.
ended{ time }Playback reached the end without loop.
ratechange{ rate }playbackRate changed.
volumechange{ volume, muted }volume or muted changed.
errorPlayerErrorEventAn asset failed. { id?, asset?, error: Error, fatal: boolean }.

captureSheet ​

captureSheet(times, options?) draws each time with renderFrame into one canvas, columns cells wide, with a timecode label in each cell. It returns the canvas. It shows only the canvas plane: DOM overlays such as titles, shapes and <Layer> interiors are not on it.

OptionTypeDefault
columnsnumber4Cells per row, never more than the number of times.
cellWidthnumbercomposition width, at most 320Cell width in pixels. Height follows the aspect ratio.
labelsbooleantrueDraw an mm:ss:ff label in each cell.

MediaResolver ​

Pass a resolver as media when a plain map of Blobs and URLs is not enough.

ts
interface MediaResolver {
  resolve(assetId: string): Promise<MediaHandle>;
}

type MediaHandle =
  | { kind: "blob"; blob: Blob }
  | { kind: "url"; url: string }
  | { kind: "path"; path: string }; // a file on disk: the export's server reads it; the player needs a URL

A map passed as media is turned into a resolver that answers a string with url and a Blob with blob, and rejects an id it does not hold.

@cutcraft/render ​

Renders a file to a video, a soundtrack, a still image or a layout report, from Node, through the player in headless Chrome.

ts
import { directoryResolver, loadVideo, renderVideo } from "@cutcraft/render";

const video = await loadVideo("video.tsx", { manifest: "manifest.json" });
const result = await renderVideo({
  state: video.state,
  source: video.source,
  media: directoryResolver("media", video.manifest),
  out: "video.mp4",
  onProgress: ({ frame, frames }) => console.log(frame, frames),
});

See Exporting for the command line and what the export draws.

loadVideo ​

loadVideo(file: string, options?: LoadOptions): Promise<LoadedVideo>

Evaluates a cut file the way the editor does, with the kit's loadModule, and resolves it against its manifest. A file that imports something the host does not offer fails here as it would in the editor.

OptionTypeDefault
manifeststring | AssetManifestthe file's manifest exportA manifest file path, or the manifest itself. Without either, loading fails.
propsRecord<string, unknown>Props for the file's default component, over its defaults export.
importsRecord<string, string>Modules the file may import beyond react, zod and the kit, by the name it imports them as. Each value is a package name or a path relative to the file's folder.

LoadedVideo:

FieldType
stateResolvedStateThe resolved composition.
manifestAssetManifestThe manifest it was resolved against.
sourceExportSourceThe file's text, manifest and props, for renderVideo, renderStill and checkLayout: the export page loads the file itself.

directoryResolver ​

directoryResolver(dir: string, manifest: AssetManifest, overrides?: Record<string, string>): MediaResolver

Finds each asset's file in dir: <dir>/<id> with no extension or with one of the usual media extensions (.webm, .mp4, .mov, .mkv, .m4v, .ogg, .opus, .mp3, .wav, .m4a, .aac, .flac, .png, .jpg, .jpeg, .webp, .gif, .svg). An entry in overrides gives the path for that asset id instead.

renderVideo ​

renderVideo(options: RenderOptions): Promise<RenderResult>

Renders the composition to a file in headless Chrome: the real player draws each frame, and the browser encodes the pictures and the soundtrack. Node joins the pieces into the file. The first call downloads Chrome for Testing unless one is cached or CUTCRAFT_CHROMIUM names one (findChrome, installChrome).

OptionTypeDefault
stateResolvedStateThe resolved composition.
sourceExportSourceFrom loadVideo: { text, manifest, props?, executablePath? }. executablePath names the Chrome to run.
mediaMediaResolverWhere each asset's file is. Usually directoryResolver.
outstringThe file to write. Its extension picks the container unless container is given.
container"mp4" | "webm" | "mp3" | "wav"from outBy extension. MP3 and WAV hold only the soundtrack. Any other extension is an error.
videoCodec"avc" | "vp9" | "av1"AVC in MP4, VP9 in WebMThe video codec. It must fit the container: AVC or AV1 in MP4, VP9 or AV1 in WebM.
bitratenumberabout 0.1 bits per pixel per frameVideo bits per second.
quality"very-low" | "low" | "medium" | "high" | "very-high"An encoder quality preset, in place of bitrate.
keyFrameIntervalnumber2Seconds between keyframes.
fromnumber0Start of the part to render, in composition seconds.
tonumberthe composition's durationEnd of the part to render, in composition seconds.
fontsFontFile[]Fonts beyond the bundled ones and those the file declares, picked by a title's font prop. { name, data: Buffer | ArrayBuffer, weight?, style?: "normal" | "italic" }.
tabsnumber1Pages of the browser that render pieces at once.
smartCopybooleanCopy encoded pictures from the source where a stretch shows only one file (VP9, at the composition's size and frame rate, no overlay or effect), and render only the ends.
silentbooleanSkip the soundtrack.
audioBitratenumber128000Audio bits per second.
onProgress(p: { frame: number; frames: number }) => voidCalled as pieces are written.
onLog(message: string) => voidProgress while Chrome downloads, on the first export.
signalAbortSignalStops the render when aborted.
onAssetError"throw" | "continue""throw"When an asset cannot be opened or decoded: stop, or draw the grey placeholder and go on.

RenderResult:

FieldType
framesnumberPictures written; 0 for MP3 or WAV.
audiobooleanWhether the file has an audio track.
fps, width, heightnumberThe composition's frame rate and size.
wallMsnumberTime the render took, starting the browser included.
tabsnumberPages that rendered pieces at once.
copiedFramesnumberFrames copied from a source instead of rendered.
warningsstring[]What was left out and why, and asset errors under onAssetError: "continue".

containerOf(out) returns the container a file name asks for, by the same rule as container.

renderStill ​

renderStill(options: StillOptions): Promise<StillResult>

Draws one frame the way renderVideo does, to a PNG or a JPEG.

OptionTypeDefault
stateResolvedStateThe resolved composition.
sourceExportSourceAs for renderVideo.
mediaMediaResolverWhere each asset's file is.
outstringThe image to write. .jpg or .jpeg writes a JPEG, anything else a PNG.
timenumberComposition seconds. The frame showing at that time is drawn. It must be within the composition.
fontsFontFile[]As for renderVideo.
onAssetError"throw" | "continue"As for renderVideo.
onLog(message: string) => voidAs for renderVideo.

StillResult has frame, width, height and warnings.

checkLayout ​

checkLayout(options: LayoutCheckOptions): Promise<LayoutCheckResult>

Samples the composition in the export page, drawing each overlay alone to measure its ink, and reports every overlay (titles, shapes, graphics, stickers, layers and captions) that leaves the frame, leaves the safe zone, covers captions, or covers the subject. Each issue is a run of consecutive samples. Overlays beneath the footage, ones whose ink fills the frame, and moments where an element is fainter than 10% are not checked.

OptionTypeDefault
stateResolvedStateThe resolved composition.
mediaMediaResolverWhere each asset's file is.
sourceExportSource & { file?: string }As for renderVideo, plus the file's path, so each issue points at its place in the JSX.
fontsFontFile[]As for renderVideo.
safeZoneSafeZoneName | Insets"title-safe"A named zone from SAFE_ZONES, or your own margins.
subject{ x, y, w, h }Where the subject is, as fractions of the frame, while footage shows whose manifest entry has no subjects.
stepnumberone frameSeconds between samples.
fromnumber0Start, in composition seconds.
tonumberthe composition's durationEnd, in composition seconds.

LayoutCheckResult has summary (one line on what was checked and found), issues, samples and warnings. Each LayoutIssue has:

FieldType
rule"frame-edge" | "safe-zone" | "captions" | "subject"The rule it breaks.
id, kindstringThe element.
withstringWhat it runs into: the captions' id, subject, or the safe zone's name (custom for insets).
from, tonumberFirst and last sample that break the rule, in seconds.
atnumberThe worst moment.
box, area{ x, y, w, h }The element's ink at at, and what it breaks, in composition pixels.
messagestringWhat is wrong, in one sentence.
fixstringThe smallest change in props that clears it at at.
sourceSourceSpotWhere to fix it in the file, when source was given: file, line, column, code, and helper or loop when the element was generated.

formatLayout(result) prints the result as a compiler does: file:line:column, the rule and message, the line of JSX with a caret, and the fix for each issue, then the summary.

SAFE_ZONES holds the named margins, as fractions of the frame's height (top, bottom) and width (left, right):

NameTopBottomLeftRight
tiktok240/1920660/1920120/1080120/1080
reels0.140.350.060.06
shorts288/1920672/192048/1080192/1080
title-safe0.050.050.050.05
action-safe0.0350.0350.0350.035

addAsset ​

ts
const { entry, manifest } = await addAsset("media/talk.mp4", { manifest: "manifest.json" });

What cutcraft add does: probes the media (kind, duration, fps, size, sound), reads the transcript beside it (talk.json, talk.vtt, talk.srt) or transcript, and writes the entry into the manifest. Options: manifest, id, transcript. The result also has transcript (the file read), estimated and warnings.

Constants ​

QUALITIES maps each quality name (very-low, low, medium, high, very-high) to mediabunny's preset. Its keys are the values of quality.

CHROME_MILESTONE is the Chrome for Testing milestone the export downloads; browserCacheDir() is where. findChrome() returns the binary the export would run (downloading it if needed), and installChrome() downloads it.

Lower-level exports ​

Plumbing for the CLI and other packages: mixBlock, mixPlan, envelopeAt, loadFonts, subjectRects.

@cutcraft/kit ​

The kit has no DOM. It turns a file into a resolved state, edits it with actions, and writes edits back to the text. See Editing for how these fit together and Actions for every action.

From @cutcraft/kit:

ExportSignature
collectModule(mod: VideoModule, overrides?: Record<string, unknown>, options?: CollectOptions) => CollectedTreeCalls the file's default component with defaults and overrides and collects the element tree.
resolve(tree: CollectedTree, manifest: AssetManifest, log?: PatchLog) => ResolvedStateAbsolute placements by id, with the patch log applied.
Executornew Executor({ tree, manifest, log?, history? })Runs actions: execute(input), undo(), redo(), state(), log(), history(), beginGroup(), endGroup(), replaceTree(tree).
useT() => numberProgress through the enclosing <Layer>, 0 to 1. Needs a mounted player.
useLayer() => LayerClockThe layer's clock: t, elapsed, remaining, duration, time, fps. Needs a mounted player.
interpolate(input, inputRange, outputRange, options?: { easing?, extrapolateLeft?, extrapolateRight? }) => numberPiecewise map from one range onto another. Extends past the ends unless "clamp".
spring({ time, from?, to?, mass?, damping?, stiffness?, overshootClamping? }) => numberA physical spring at time seconds. Defaults: from 0, to 1, mass 1, damping 10, stiffness 100.
random(seed: string | number) => numberA number in [0, 1) fixed by seed, the same every frame in the preview and the export.
parseTranscript(text: string, format?: "srt" | "vtt" | "json") => { words, estimated }Reads SRT, WebVTT or speech-to-text JSON (OpenAI, whisper, whisper.cpp, ElevenLabs) into the manifest's word list (Getting a transcript).
subtitles(state: ResolvedState, { format: "srt" | "vtt", from?, to? }) => stringThe captions the cut plays, as a subtitle file timed from from (Subtitle files).
effect<P extends ZodObject>({ name, params?: P, shader } | { name, params?: P, passes: string[] }) => EffectRef<P>Declares an effect drawn by a GLSL vec4 effect(vec2 uv), or by several run in order (passes, each reading the one before as image and the element as original), that reads each param as params.<name>. Returns the definition as a value, with no side effect, for <Effect type>, which then checks params against P. The player finds it on the element that uses it or among the file's exports, so each file's effects are its own (Effects and transitions of your own).
transition<P extends ZodObject>({ name, params?: P, shader }) => TransitionRef<P>Declares a transition drawn by a GLSL vec4 transition(vec2 uv) that reads from, to, progress and params.<name>. Returns the definition as a value for <Transition type>, which then checks params against P.
BUILT_IN_SHADERS, builtInShaderreadonly ShaderDefinition[], (type) => ShaderDefinition | undefinedThe built-in effects drawn by a shader (levels, lut), with the schema of their params.
parseCube(text: string) => { size, domainMin, domainMax, data, title? }Reads a 3D .cube LUT; throws naming the line that is wrong (A look from a LUT).
font({ family, src: "asset:<id>", weight?, style? }) => FontRefDeclares a face from a font file and returns it as a value that stands for its family wherever a string goes (Fonts). The faces a file uses or exports are on state.fonts.
googleFont(family, { weights?, styles? }?) => FontRefDeclares a Google Fonts family, one face per weight and style, and returns it as a value that stands for its family. The export downloads it once into a local cache.

From @cutcraft/kit/source:

ExportSignature
loadModule(source: string, options?: { imports?: Record<string, unknown> }) => CutModuleEvaluates a file's text. It may import react, zod and @cutcraft/kit, plus imports.
lint(text: string, options?: { manifest?, rules? }) => Diagnostic[]Static checks that keep a file editable. Each Diagnostic has rule, severity, message, range, and id and prop when known.
foldToSource(text: string, log: PatchLog, state: ResolvedState, maxRounds?: number) => FoldResultWrites the patch log's net effect back into the text. FoldResult has text, edits, baked and skipped.
buildSourceIndex(text: string) => SourceIndexWhere each element, prop and helper sits in the text.

MIT licensed.