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[]): stringThe 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.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: CompositionEntryThe 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: ExportFormatContainer, 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: stringAbsolute path of the video. Written beside itself as a partial file and renamed into place.output: function resolve(...paths: string[]): stringThe 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.resolve("out/title-card.mp4"),
RenderHeadlessOptions.poster?: RenderHeadlessPoster | undefinedposter: { RenderHeadlessPoster.frame: numberComposition frame to write as the poster.frame: 45, RenderHeadlessPoster.output: stringAbsolute path of the JPEG.output: function resolve(...paths: string[]): stringThe 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.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[]): voidThe **`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 byplaywright-core install chromium --only-shell, and@mediabunny/serverto encode in Node. - The video is written beside
outputunder a temporary name and renamed when it is complete. On a failure or an abort throughsignal, 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 longerframeTimeoutMs.- Exported video has no sound yet. Audio from
AudioandVideoactors 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[]): stringThe 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.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: CompositionEntryThe 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: numberThe 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: ExportFormatContainer, 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: stringAbsolute path of the video. Written beside itself as a partial file and renamed into place.output: function resolve(...paths: string[]): stringThe 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.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.
signalonopenHeadlessSessioncancels the build, the launch and the page load. Onstillorexportit cancels that call only, and the session stays open.- An open that fails or is aborted closes the browser before it rejects.
closewaits 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): CompositionCaptureSerial 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 operationPromise<ImageBitmap>;
const const capture: CompositionCapturecapture = function createCompositionCapture({ controller, viewport, host, timeoutMs }: CompositionCaptureOptions): CompositionCaptureSerial 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 BufferTargetA 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.BufferTarget } from "mediabunny";
import { function frameStateKey(state: KeyedFrame): stringA 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): BufferTargetCreates 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): stringA 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: TargetWhere the file goes: a `FilePathTarget` in Node, a `BufferTarget` or `StreamTarget` anywhere.target,
ExportCompositionOptions.format?: ExportFormat | undefinedContainer, 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: numberFrames 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) => BlobThe **`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 | nullStores the final output buffer. Until the output is finalized, this will be `null`.buffer!], { BlobPropertyBag.type?: string | undefinedtype: "video/webm" });
framesis[start, end). An empty range throws anExportErrorwith codeempty-range, and a missing encoder throws one with codeno-encoderbefore 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.
stateKeylets export skip frames that draw nothing new.frameStateKeyfrom@motionactor/coreturns 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.signalaborts. 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): stringAdds all the elements of an array separated by the specified separator string.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.