Skip to content
MotionActor
Esc
↑↓navigate↵open⌘Jpreview
On this page

Capture and export

Read finished frames as pixels, encode stills, and write a video file from a mounted composition.

Capture asks a mounted composition for one frame, waits until the frame is ready, and reads its pixels while nothing else may paint. Export captures every frame of a range that way and feeds the pixels to a video encoder. Both run on the same page a viewer previews, so the file shows exactly what the preview shows.

Render a file

renderHeadless from @motionactor/engine/node is the whole pipeline in one call. It builds the composition into a page, opens that page in headless Chromium, captures each frame and writes the video:

import { function resolve(...paths: string[]): string
The right-most parameter is considered {to}. Other parameters are considered an array of {from}. Starting from leftmost {from} parameter, resolves {to} to an absolute path. If {to} isn't already absolute, {from} arguments are prepended in right to left order, until an absolute path is found. If after using all {from} paths still no absolute path is found, the current working directory is used as well. The resulting path is normalized, and trailing slashes are removed unless the path gets resolved to the root directory.
@parampaths A sequence of paths or path segments.@throws{TypeError} if any of the arguments is not a string.
resolve
} from "node:path";
import {
const exportFormats: {
    readonly mp4H264: {
        readonly id: "mp4H264";
        readonly label: "H.264 MP4";
        readonly container: "mp4";
        readonly codec: "avc";
        readonly alpha: "discard";
        readonly extension: "mp4";
    };
    readonly prores4444: {
        readonly id: "prores4444";
        readonly label: "ProRes 4444 MOV";
        readonly container: "mov";
        readonly codec: "prores";
        readonly alpha: "keep";
        readonly extension: "mov";
    };
    readonly webmVp9: {
        readonly id: "webmVp9";
        readonly label: "VP9 WebM";
        readonly container: "webm";
        readonly codec: "vp9";
        readonly alpha: "discard";
        readonly extension: "webm";
    };
    readonly webmVp9Alpha: {
        readonly id: "webmVp9Alpha";
        ... 4 more ...;
        readonly extension: "webm";
    };
}
Every format `exportComposition` writes. A host lists these (label, extension, `alpha`) to build a save dialog and passes one back as `format`.
exportFormats
} from "@motionactor/engine/export";
import { function renderHeadless(options: RenderHeadlessOptions): Promise<RenderHeadlessResult>
Render a composition to a video file, and optionally a JPEG poster: a session, the poster's still, the export, and the session closed. Both are written beside their targets as `.<name>.<uuid>.staged`, then replace them together (`replaceTogether`): on abort, or on failure of any step including either final rename, the new files are removed and the prior video renamed back, so `output` and `poster.output` hold what they held. Limits, each keeping the prior video rather than losing it: a failed rename back leaves it at `.<name>.<uuid>.prior`; a failed removal of it after both new files land rejects with them in place; a process killed between moving it aside and placing the poster leaves it at that name. A staged file whose removal fails stays beside its target, and that failure is what the call rejects with.
renderHeadless
} from "@motionactor/engine/node";
const const result: RenderHeadlessResultresult = await function renderHeadless(options: RenderHeadlessOptions): Promise<RenderHeadlessResult>
Render a composition to a video file, and optionally a JPEG poster: a session, the poster's still, the export, and the session closed. Both are written beside their targets as `.<name>.<uuid>.staged`, then replace them together (`replaceTogether`): on abort, or on failure of any step including either final rename, the new files are removed and the prior video renamed back, so `output` and `poster.output` hold what they held. Limits, each keeping the prior video rather than losing it: a failed rename back leaves it at `.<name>.<uuid>.prior`; a failed removal of it after both new files land rejects with them in place; a process killed between moving it aside and placing the poster leaves it at that name. A staged file whose removal fails stays beside its target, and that failure is what the call rejects with.
renderHeadless
({
RenderHeadlessOptions.entry: CompositionEntry
The composition, as `bundleComposition`/`buildCompositionPreview` take it.
entry
: {
CompositionEntry.entry: stringentry: "./src/title-card.tsx", CompositionEntry.export: stringexport: "titleCard", CompositionEntry.props?: Record<string, unknown> | undefinedprops: { title: stringtitle: "Hello" }, }, RenderHeadlessOptions.format: ExportFormat
Container, codec and alpha. The viewport, fps and duration come from the composition.
format
:
const exportFormats: {
    readonly mp4H264: {
        readonly id: "mp4H264";
        readonly label: "H.264 MP4";
        readonly container: "mp4";
        readonly codec: "avc";
        readonly alpha: "discard";
        readonly extension: "mp4";
    };
    readonly prores4444: {
        readonly id: "prores4444";
        readonly label: "ProRes 4444 MOV";
        readonly container: "mov";
        readonly codec: "prores";
        readonly alpha: "keep";
        readonly extension: "mov";
    };
    readonly webmVp9: {
        readonly id: "webmVp9";
        readonly label: "VP9 WebM";
        readonly container: "webm";
        readonly codec: "vp9";
        readonly alpha: "discard";
        readonly extension: "webm";
    };
    readonly webmVp9Alpha: {
        readonly id: "webmVp9Alpha";
        ... 4 more ...;
        readonly extension: "webm";
    };
}
Every format `exportComposition` writes. A host lists these (label, extension, `alpha`) to build a save dialog and passes one back as `format`.
exportFormats
.
mp4H264: {
    readonly id: "mp4H264";
    readonly label: "H.264 MP4";
    readonly container: "mp4";
    readonly codec: "avc";
    readonly alpha: "discard";
    readonly extension: "mp4";
}
mp4H264
,
RenderHeadlessOptions.output: string
Absolute path of the video. Written beside itself as a partial file and renamed into place.
output
: function resolve(...paths: string[]): string
The right-most parameter is considered {to}. Other parameters are considered an array of {from}. Starting from leftmost {from} parameter, resolves {to} to an absolute path. If {to} isn't already absolute, {from} arguments are prepended in right to left order, until an absolute path is found. If after using all {from} paths still no absolute path is found, the current working directory is used as well. The resulting path is normalized, and trailing slashes are removed unless the path gets resolved to the root directory.
@parampaths A sequence of paths or path segments.@throws{TypeError} if any of the arguments is not a string.
resolve
("out/title-card.mp4"),
RenderHeadlessOptions.poster?: RenderHeadlessPoster | undefinedposter: { RenderHeadlessPoster.frame: number
Composition frame to write as the poster.
frame
: 45, RenderHeadlessPoster.output: string
Absolute path of the JPEG.
output
: function resolve(...paths: string[]): string
The right-most parameter is considered {to}. Other parameters are considered an array of {from}. Starting from leftmost {from} parameter, resolves {to} to an absolute path. If {to} isn't already absolute, {from} arguments are prepended in right to left order, until an absolute path is found. If after using all {from} paths still no absolute path is found, the current working directory is used as well. The resulting path is normalized, and trailing slashes are removed unless the path gets resolved to the root directory.
@parampaths A sequence of paths or path segments.@throws{TypeError} if any of the arguments is not a string.
resolve
("out/title-card.jpg") },
RenderHeadlessOptions.onProgress?(progress: ExportProgress): void | Promise<void>
See `ExportCompositionOptions.onProgress`: a returned promise holds the next frame.
onProgress
: ({ done: numberdone, total: numbertotal }) => var console: Consoleconsole.Console.log(...data: any[]): void
The **`console.log()`** static method outputs a message to the console. [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(`${done: numberdone}/${total: numbertotal}`),
}); const result: RenderHeadlessResultresult.RenderHeadlessResult.frames: numberframes; // 60 const result: RenderHeadlessResultresult.RenderHeadlessResult.durationSeconds: numberdurationSeconds; // 2
  • It needs two optional peers: playwright-core, with a browser installed by playwright-core install chromium --only-shell, and @mediabunny/server to encode in Node.
  • The video is written beside output under a temporary name and renamed when it is complete. On a failure or an abort through signal, the browser is closed and neither the video nor the poster is left behind.
  • Each frame has frameTimeoutMs (5000 by default) to render, settle and be read. A frame that misses it fails the render with “Frame 12 did not settle within 5000ms”.
  • gl: "angle", the default, draws WebGL on the GPU where there is one and in software where there is not. gl: "swangle" always draws in software, which gives the same pixels on every machine. Software GL compiles each shader on first use, so a heavy WebGL scene may need a longer frameTimeoutMs.
  • Exported video has no sound yet. Audio from Audio and Video actors plays in previews but is not mixed into the file.

Stills and several exports

openHeadlessSession keeps one browser and page open on a composition. A still reads only its frame, and further exports reuse the page without building it again. renderHeadless is a session, a still for the poster, an export and close.

import { function resolve(...paths: string[]): string
The right-most parameter is considered {to}. Other parameters are considered an array of {from}. Starting from leftmost {from} parameter, resolves {to} to an absolute path. If {to} isn't already absolute, {from} arguments are prepended in right to left order, until an absolute path is found. If after using all {from} paths still no absolute path is found, the current working directory is used as well. The resulting path is normalized, and trailing slashes are removed unless the path gets resolved to the root directory.
@parampaths A sequence of paths or path segments.@throws{TypeError} if any of the arguments is not a string.
resolve
} from "node:path";
import {
const exportFormats: {
    readonly mp4H264: {
        readonly id: "mp4H264";
        readonly label: "H.264 MP4";
        readonly container: "mp4";
        readonly codec: "avc";
        readonly alpha: "discard";
        readonly extension: "mp4";
    };
    readonly prores4444: {
        readonly id: "prores4444";
        readonly label: "ProRes 4444 MOV";
        readonly container: "mov";
        readonly codec: "prores";
        readonly alpha: "keep";
        readonly extension: "mov";
    };
    readonly webmVp9: {
        readonly id: "webmVp9";
        readonly label: "VP9 WebM";
        readonly container: "webm";
        readonly codec: "vp9";
        readonly alpha: "discard";
        readonly extension: "webm";
    };
    readonly webmVp9Alpha: {
        readonly id: "webmVp9Alpha";
        ... 4 more ...;
        readonly extension: "webm";
    };
}
Every format `exportComposition` writes. A host lists these (label, extension, `alpha`) to build a save dialog and passes one back as `format`.
exportFormats
} from "@motionactor/engine/export";
import { function openHeadlessSession(options: HeadlessSessionOptions): Promise<HeadlessSession>
Open a composition in headless Chromium. The page is the one Studio previews (`buildCompositionPreview`), served from loopback; frames come out through CDP screenshots (`@motionactor/engine/capture/cdp`). An abort or a failure while opening releases everything it had started, the browser included.
openHeadlessSession
} from "@motionactor/engine/node";
const const session: HeadlessSessionsession = await function openHeadlessSession(options: HeadlessSessionOptions): Promise<HeadlessSession>
Open a composition in headless Chromium. The page is the one Studio previews (`buildCompositionPreview`), served from loopback; frames come out through CDP screenshots (`@motionactor/engine/capture/cdp`). An abort or a failure while opening releases everything it had started, the browser included.
openHeadlessSession
({
HeadlessSessionOptions.entry: CompositionEntry
The composition, as `bundleComposition`/`buildCompositionPreview` take it.
entry
: { CompositionEntry.entry: stringentry: "./src/title-card.tsx", CompositionEntry.export: stringexport: "titleCard" },
}); try { const const jpeg: Uint8Array<ArrayBufferLike>jpeg = await const session: HeadlessSessionsession.HeadlessSession.still(frame: number, options?: HeadlessStillOptions): Promise<Uint8Array>
A JPEG of `frame`. Captures only that frame.
still
(45); // reads frame 45 and nothing else
const jpeg: Uint8Array<ArrayBufferLike>jpeg.Uint8Array<ArrayBufferLike>.byteLength: number
The length in bytes of the array.
byteLength
;
await const session: HeadlessSessionsession.HeadlessSession.export(options: HeadlessExportOptions): Promise<HeadlessExportResult>
Encodes the range to `output`; on abort or failure nothing is left there.
export
({
HeadlessExportOptions.format: ExportFormat
Container, codec and alpha. The viewport and fps come from the composition.
format
:
const exportFormats: {
    readonly mp4H264: {
        readonly id: "mp4H264";
        readonly label: "H.264 MP4";
        readonly container: "mp4";
        readonly codec: "avc";
        readonly alpha: "discard";
        readonly extension: "mp4";
    };
    readonly prores4444: {
        readonly id: "prores4444";
        readonly label: "ProRes 4444 MOV";
        readonly container: "mov";
        readonly codec: "prores";
        readonly alpha: "keep";
        readonly extension: "mov";
    };
    readonly webmVp9: {
        readonly id: "webmVp9";
        readonly label: "VP9 WebM";
        readonly container: "webm";
        readonly codec: "vp9";
        readonly alpha: "discard";
        readonly extension: "webm";
    };
    readonly webmVp9Alpha: {
        readonly id: "webmVp9Alpha";
        ... 4 more ...;
        readonly extension: "webm";
    };
}
Every format `exportComposition` writes. A host lists these (label, extension, `alpha`) to build a save dialog and passes one back as `format`.
exportFormats
.
mp4H264: {
    readonly id: "mp4H264";
    readonly label: "H.264 MP4";
    readonly container: "mp4";
    readonly codec: "avc";
    readonly alpha: "discard";
    readonly extension: "mp4";
}
mp4H264
,
HeadlessExportOptions.output: string
Absolute path of the video. Written beside itself as a partial file and renamed into place.
output
: function resolve(...paths: string[]): string
The right-most parameter is considered {to}. Other parameters are considered an array of {from}. Starting from leftmost {from} parameter, resolves {to} to an absolute path. If {to} isn't already absolute, {from} arguments are prepended in right to left order, until an absolute path is found. If after using all {from} paths still no absolute path is found, the current working directory is used as well. The resulting path is normalized, and trailing slashes are removed unless the path gets resolved to the root directory.
@parampaths A sequence of paths or path segments.@throws{TypeError} if any of the arguments is not a string.
resolve
("out/opening.mp4"),
HeadlessExportOptions.range?: {
    readonly start: number;
    readonly end: number;
} | undefined
Composition frames `[start, end)`. Default: every frame.
range
: { start: numberstart: 0, end: numberend: 30 },
}); } finally { await const session: HeadlessSessionsession.HeadlessSession.close(): Promise<void>
Waits for the running operation, then releases the browser, server and build. Idempotent.
close
();
}
  • Stills and exports run one at a time, in call order: the page holds one frame at a time.
  • signal on openHeadlessSession cancels the build, the launch and the page load. On still or export it cancels that call only, and the session stays open.
  • An open that fails or is aborted closes the browser before it rejects. close waits for the running call, then releases the browser, the server and the build. Calling it again does nothing.

Formats

exportFormats. File Codec Alpha
mp4H264 .mp4 H.264 Discarded
prores4444 .mov ProRes 4444 Kept
webmVp9 .webm VP9 Discarded
webmVp9Alpha .webm VP9 Kept

Each format has a label (“ProRes 4444 MOV”) and an extension, so a save dialog can list them directly. A format that keeps alpha captures over a transparent page, so only what the scene paints is opaque. Those captures are PNG screenshots, which are slower than the JPEG ones an opaque export reads.

Capture

Under renderHeadless are two pieces you can use on their own. A capture host reads pixels. A capture controller reserves a frame so that no seek, prop update or other capture can paint over it while the host reads. mountComposition returns the controller as preview.capture.

import {
  function captureStill(capture: CompositionCapture, frame: number, options?: CaptureFrameOptions): StillTaskcaptureStill,
  function createCompositionCapture({ controller, viewport, host, timeoutMs }: CompositionCaptureOptions): CompositionCapture
Serial frame capture over a mounted composition. The controller reserves the frame through commit, readiness and read against every other controller user. An abort/timeout settles the caller; unfinished work retains its reservation until it settles, so a late read never races the next frame.
createCompositionCapture
,
} from "@motionactor/engine/capture"; // Whatever can read this page's pixels. declare function function readStage(signal: AbortSignal): Promise<ImageBitmap>readStage(signal: AbortSignalsignal: AbortSignal): interface Promise<T>
Represents the completion of an asynchronous operation
Promise
<ImageBitmap>;
const const capture: CompositionCapturecapture = function createCompositionCapture({ controller, viewport, host, timeoutMs }: CompositionCaptureOptions): CompositionCapture
Serial frame capture over a mounted composition. The controller reserves the frame through commit, readiness and read against every other controller user. An abort/timeout settles the caller; unfinished work retains its reservation until it settles, so a late read never races the next frame.
createCompositionCapture
({
CompositionCaptureOptions.controller: CaptureControllercontroller:
const preview: {
    play: () => Promise<void>;
    pause: () => void;
    dispose: (reason?: unknown) => void;
    seek(frame: number, mode?: PresentMode): Promise<void>;
    getSnapshot: () => PreviewSnapshot;
    subscribe(listener: () => void): () => void;
    readonly compiled: CompiledScene<{
        heading: Title;
    }, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>;
    readonly mediaPlan: MediaPlan;
    readonly composition: CompositionDefinition<HostInputArgs<{
        readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
    }>, {
        heading: Title;
    }>;
    ... 4 more ...;
    updateEditor(next: unknown): Promise<...>;
}
preview
.
capture: {
    withFrame: <T>({ frame, signal: requestedSignal, read }: FrameReadRequest<T>) => Promise<T>;
    commitFrame: (frame: number) => Promise<void>;
    settle: () => Promise<void>;
    outstandingHolds: () => readonly string[];
}
Capture reserves the controller through the host's pixel read. Low-level commit/settle remain available to transports that own their page exclusively.
capture
,
CompositionCaptureOptions.viewport: FrameViewportviewport:
const preview: {
    play: () => Promise<void>;
    pause: () => void;
    dispose: (reason?: unknown) => void;
    seek(frame: number, mode?: PresentMode): Promise<void>;
    getSnapshot: () => PreviewSnapshot;
    subscribe(listener: () => void): () => void;
    readonly compiled: CompiledScene<{
        heading: Title;
    }, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>;
    readonly mediaPlan: MediaPlan;
    readonly composition: CompositionDefinition<HostInputArgs<{
        readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
    }>, {
        heading: Title;
    }>;
    ... 4 more ...;
    updateEditor(next: unknown): Promise<...>;
}
preview
.
viewport: {
    width: number;
    height: number;
    fps: number;
    durationInFrames: number;
}
viewport
,
CompositionCaptureOptions.host: CaptureHosthost: {
CaptureHost.frameSize: {
    readonly width: number;
    readonly height: number;
}
frameSize
: { width: numberwidth: 1920, height: numberheight: 1080 },
CaptureHost.read(signal: AbortSignal): Promise<CapturedFrame>
Composite what the engine has settled and return it. Must not seek or relayout.
read
: async (signal: AbortSignalsignal) => ({
kind: "image"kind: "image", image: ImageBitmap | OffscreenCanvas | HTMLCanvasElementimage: await function readStage(signal: AbortSignal): Promise<ImageBitmap>readStage(signal: AbortSignalsignal), }), CaptureHost.dispose(reason?: unknown): voiddispose() {}, }, }); const const poster: StillTaskposter = function captureStill(capture: CompositionCapture, frame: number, options?: CaptureFrameOptions): StillTaskcaptureStill(const capture: CompositionCapturecapture, 45); const const jpeg: Uint8Array<ArrayBufferLike>jpeg = await const poster: StillTaskposter.StillTask.jpeg(quality?: number): Promise<Uint8Array>jpeg(0.85); const const png: Uint8Array<ArrayBufferLike>png = await const poster: StillTaskposter.StillTask.png(): Promise<Uint8Array>png(); // the same capture, encoded again

read is the one part that depends on where the page runs. It returns either an image the browser can draw (ImageBitmap, OffscreenCanvas, a canvas element) or raw RGBA or BGRA bytes. A host that returns bytes also supplies encode for stills.

@motionactor/engine/capture/cdp has the host renderHeadless uses. createCdpCaptureHost takes a Chrome DevTools Protocol session, which Electron’s webContents.debugger already is and a Playwright CDPSession becomes through a small adapter, and reads each frame as a Page.captureScreenshot. Call emulateCapturePage first so that frames come out in composition pixels on a high-density display.

createCompositionCapture throws when the host’s frame size differs from the composition’s. A capture that runs out of time throws a CaptureError with code timeout and the labels of the holds that never released in error.holds, so the message names the font, video or shader that kept the frame waiting.

Export

exportComposition from @motionactor/engine/export captures a half-open range of frames and encodes them into a Mediabunny target. In a browser the encoder is WebCodecs; in Node, call registerMediabunnyServer() from @mediabunny/server first.

import { class BufferTarget
A target that writes data directly into an ArrayBuffer in memory. Great for performance, but not suitable for very large files. The buffer will be available once the output has been finalized.
@groupOutput targets@public
BufferTarget
} from "mediabunny";
import { function frameStateKey(state: KeyedFrame): string
A key equal on two frames only when they draw the same: every live actor's resolved state, the layers and the camera. Time reaches pixels as resolved state (a clock's fields, an effect's `params.time`), so a paused clock repeats and a running one does not. An actor whose blueprint declares `static frameDriven = true` draws from the frame number itself (a `Component`'s `render({ frame })`, a playing `Video`), so while one is live the frame is part of the key and no frame repeats.
frameStateKey
} from "@motionactor/core";
import { function exportComposition(options: ExportCompositionOptions): Promise<ExportResult>
Capture every frame of the range and encode it, in order, in `format`. Captures pull frames lowest-first (Remotion's `getNextFrame`) and run ahead of the encoder by at most one frame each plus one, so capture and encode overlap and finished frames wait in order for their turn. A capture with `stateKey` skips any frame whose key repeats the previous frame's; the encoder writes the previous picture again. Encoders come from WebCodecs in a browser; a Node host registers them first (`registerMediabunnyServer()` from `@mediabunny/server`). On abort or any failure every capture is stopped and settled, the output is cancelled and the reason rethrown; the host removes a partly written file.
exportComposition
,
const exportFormats: {
    readonly mp4H264: {
        readonly id: "mp4H264";
        readonly label: "H.264 MP4";
        readonly container: "mp4";
        readonly codec: "avc";
        readonly alpha: "discard";
        readonly extension: "mp4";
    };
    readonly prores4444: {
        readonly id: "prores4444";
        readonly label: "ProRes 4444 MOV";
        readonly container: "mov";
        readonly codec: "prores";
        readonly alpha: "keep";
        readonly extension: "mov";
    };
    readonly webmVp9: {
        readonly id: "webmVp9";
        readonly label: "VP9 WebM";
        readonly container: "webm";
        readonly codec: "vp9";
        readonly alpha: "discard";
        readonly extension: "webm";
    };
    readonly webmVp9Alpha: {
        readonly id: "webmVp9Alpha";
        ... 4 more ...;
        readonly extension: "webm";
    };
}
Every format `exportComposition` writes. A host lists these (label, extension, `alpha`) to build a save dialog and passes one back as `format`.
exportFormats
,
} from "@motionactor/engine/export"; const const target: BufferTargettarget = new new BufferTarget(options?: BufferTargetOptions): BufferTarget
Creates a new {@link BufferTarget } . The buffer holding the data will be created and managed internally.
BufferTarget
();
const const result: ExportResultresult = await function exportComposition(options: ExportCompositionOptions): Promise<ExportResult>
Capture every frame of the range and encode it, in order, in `format`. Captures pull frames lowest-first (Remotion's `getNextFrame`) and run ahead of the encoder by at most one frame each plus one, so capture and encode overlap and finished frames wait in order for their turn. A capture with `stateKey` skips any frame whose key repeats the previous frame's; the encoder writes the previous picture again. Encoders come from WebCodecs in a browser; a Node host registers them first (`registerMediabunnyServer()` from `@mediabunny/server`). On abort or any failure every capture is stopped and settled, the output is cancelled and the reason rethrown; the host removes a partly written file.
exportComposition
({
ExportCompositionOptions.captures: readonly [ExportCapture, ...ExportCapture[]]
One or more mounted copies of the same page, all with the same viewport. Each pulls the lowest frame not yet taken, so N copies capture N frames at once; one copy still captures the next frame while this one encodes.
captures
: [
{ ...const capture: CompositionCapturecapture,
stateKey?(frame: number, options: {
    readonly signal: AbortSignal;
}): Promise<string>
The frame's state key (`frameStateKey` from `@motionactor/core`), resolved without painting. A frame whose key equals the previous frame's is not captured: the encoder repeats the previous picture. Keys are compared only within one capture, so identity-keyed values need only be stable per page.
stateKey
: async (frame: numberframe) =>
function frameStateKey(state: KeyedFrame): string
A key equal on two frames only when they draw the same: every live actor's resolved state, the layers and the camera. Time reaches pixels as resolved state (a clock's fields, an effect's `params.time`), so a paused clock repeats and a running one does not. An actor whose blueprint declares `static frameDriven = true` draws from the frame number itself (a `Component`'s `render({ frame })`, a playing `Video`), so while one is live the frame is part of the key and no frame repeats.
frameStateKey
(
const preview: {
    play: () => Promise<void>;
    pause: () => void;
    dispose: (reason?: unknown) => void;
    seek(frame: number, mode?: PresentMode): Promise<void>;
    getSnapshot: () => PreviewSnapshot;
    subscribe(listener: () => void): () => void;
    readonly compiled: CompiledScene<{
        heading: Title;
    }, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>;
    readonly mediaPlan: MediaPlan;
    readonly composition: CompositionDefinition<HostInputArgs<{
        readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
    }>, {
        heading: Title;
    }>;
    ... 4 more ...;
    updateEditor(next: unknown): Promise<...>;
}
preview
.
compiled: CompiledScene<{
    heading: Title;
}, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>
compiled
.
CompiledScene<{ heading: Title; }, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
    heading: Title;
}>
at
(frame: numberframe)),
}, ],
ExportCompositionOptions.frames: {
    readonly start: number;
    readonly end: number;
}
Composition frames, half-open: `[start, end)`.
frames
: { start: numberstart: 0, end: numberend:
const preview: {
    play: () => Promise<void>;
    pause: () => void;
    dispose: (reason?: unknown) => void;
    seek(frame: number, mode?: PresentMode): Promise<void>;
    getSnapshot: () => PreviewSnapshot;
    subscribe(listener: () => void): () => void;
    readonly compiled: CompiledScene<{
        heading: Title;
    }, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>;
    readonly mediaPlan: MediaPlan;
    readonly composition: CompositionDefinition<HostInputArgs<{
        readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
    }>, {
        heading: Title;
    }>;
    ... 4 more ...;
    updateEditor(next: unknown): Promise<...>;
}
preview
.
viewport: {
    width: number;
    height: number;
    fps: number;
    durationInFrames: number;
}
viewport
.durationInFrames: numberdurationInFrames },
ExportCompositionOptions.fps: numberfps:
const preview: {
    play: () => Promise<void>;
    pause: () => void;
    dispose: (reason?: unknown) => void;
    seek(frame: number, mode?: PresentMode): Promise<void>;
    getSnapshot: () => PreviewSnapshot;
    subscribe(listener: () => void): () => void;
    readonly compiled: CompiledScene<{
        heading: Title;
    }, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>;
    readonly mediaPlan: MediaPlan;
    readonly composition: CompositionDefinition<HostInputArgs<{
        readonly title: ValueInput<DataType<"text", ZodString>, string, false>;
    }>, {
        heading: Title;
    }>;
    ... 4 more ...;
    updateEditor(next: unknown): Promise<...>;
}
preview
.
viewport: {
    width: number;
    height: number;
    fps: number;
    durationInFrames: number;
}
viewport
.fps: numberfps,
ExportCompositionOptions.target: Target
Where the file goes: a `FilePathTarget` in Node, a `BufferTarget` or `StreamTarget` anywhere.
target
,
ExportCompositionOptions.format?: ExportFormat | undefined
Container, codec and alpha. Default `exportFormats.mp4H264`.
format
:
const exportFormats: {
    readonly mp4H264: {
        readonly id: "mp4H264";
        readonly label: "H.264 MP4";
        readonly container: "mp4";
        readonly codec: "avc";
        readonly alpha: "discard";
        readonly extension: "mp4";
    };
    readonly prores4444: {
        readonly id: "prores4444";
        readonly label: "ProRes 4444 MOV";
        readonly container: "mov";
        readonly codec: "prores";
        readonly alpha: "keep";
        readonly extension: "mov";
    };
    readonly webmVp9: {
        readonly id: "webmVp9";
        readonly label: "VP9 WebM";
        readonly container: "webm";
        readonly codec: "vp9";
        readonly alpha: "discard";
        readonly extension: "webm";
    };
    readonly webmVp9Alpha: {
        readonly id: "webmVp9Alpha";
        ... 4 more ...;
        readonly extension: "webm";
    };
}
Every format `exportComposition` writes. A host lists these (label, extension, `alpha`) to build a save dialog and passes one back as `format`.
exportFormats
.
webmVp9: {
    readonly id: "webmVp9";
    readonly label: "VP9 WebM";
    readonly container: "webm";
    readonly codec: "vp9";
    readonly alpha: "discard";
    readonly extension: "webm";
}
webmVp9
,
}); const result: ExportResultresult.ExportResult.repeatedFrames: number
Frames encoded from the previous picture because their state key repeated.
repeatedFrames
; // 44: the title is still from frame 15 on
const const file: Blobfile = new var Blob: new (blobParts?: BlobPart[], options?: BlobPropertyBag) => Blob
The **`Blob`** interface represents a blob, which is a file-like object of immutable, raw data; they can be read as text or binary data, or converted into a ReadableStream so its methods can be used for processing the data. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob)
Blob
([const target: BufferTargettarget.BufferTarget.buffer: ArrayBuffer | null
Stores the final output buffer. Until the output is finalized, this will be `null`.
buffer
!], { BlobPropertyBag.type?: string | undefinedtype: "video/webm" });
  • frames is [start, end). An empty range throws an ExportError with code empty-range, and a missing encoder throws one with code no-encoder before any frame is captured.
  • Pass several captures of the same page, mounted separately, to capture several frames at once. Each takes the lowest frame nobody has taken, and finished frames wait in order for the encoder. One capture still reads the next frame while the encoder works.
  • stateKey lets export skip frames that draw nothing new. frameStateKey from @motionactor/core turns a resolved frame into a string that is equal only when the actors, layers and camera are. When a frame’s key matches the one before it, the encoder writes the previous picture again.
  • signal aborts. On an abort or any failure every capture is stopped, the output is cancelled, and the reason is thrown. Removing a partly written file is the caller’s job.

A renderer that draws from the frame number rather than from actor state, such as a noise shader keyed to time, makes two frames with equal state look different. Its class declares static frameDriven = true, which puts the frame number into the key.

Browser requirements

DOM effects of the glShader and canvasPaint kinds draw HTML into a canvas, which needs a Chromium feature that is off by default. captureBlinkFeatures lists it. A browser that captures has to be launched with it:

import { const captureBlinkFeatures: readonly ["CanvasDrawElement"]
Blink features every capture browser enables: HTML-in-canvas (`layoutsubtree`, `requestPaint`, `drawElementImage`), which the dom backend's canvas effects (`glShader`/`canvasPaint`) paint through. A host applies them to its browser: Electron with `app.commandLine.appendSwitch("enable-blink-features", …join(","))`, Chromium with `--enable-blink-features=…`. Without them canvas effects fail to mount.
captureBlinkFeatures
} from "@motionactor/engine/capture";
const const flag: stringflag = `--enable-blink-features=${const captureBlinkFeatures: readonly ["CanvasDrawElement"]
Blink features every capture browser enables: HTML-in-canvas (`layoutsubtree`, `requestPaint`, `drawElementImage`), which the dom backend's canvas effects (`glShader`/`canvasPaint`) paint through. A host applies them to its browser: Electron with `app.commandLine.appendSwitch("enable-blink-features", …join(","))`, Chromium with `--enable-blink-features=…`. Without them canvas effects fail to mount.
captureBlinkFeatures
.ReadonlyArray<"CanvasDrawElement">.join(separator?: string): string
Adds all the elements of an array separated by the specified separator string.
@paramseparator A string used to separate one element of an array from the next in the resulting String. If omitted, the array elements are separated with a comma.
join
(",")}`;
// "--enable-blink-features=CanvasDrawElement"

renderHeadless launches Chromium with it already, along with an sRGB colour profile so captured colours equal the CSS colours on any display.

Was this page helpful?