Media, assets and fonts
How a scene names files, how hosts load them, and how video and audio play frame by frame.
A scene never holds bytes. Its signals hold asset references, small JSON values that name a file. A host turns each reference into a URL through a resolver, loads what the composition uses before showing frame 0, and holds every later frame until its images are decoded and its videos show the right picture.
Asset references
image, video, audio and font from @motionactor/schema each make references of
their type:
import { function assetKey(ref: AssetRef): stringWhat a ref loads, independent of its type: equal keys load the same bytes.assetKey, const image: AssetType<"image", "image">image, const video: AssetType<"video", "video">video } from "@motionactor/schema";
const const hero: AssetRef<"image">hero = const image: AssetType<"image", "image">image.AssetType<"image", "image">.file(src: string, options?: {
mimeType?: string;
}): AssetRef<"image">
file("photos/hero.jpg");
// { type: "image", kind: "file", src: "photos/hero.jpg" }
const const trailer: AssetRef<"video">trailer = const video: AssetType<"video", "video">video.AssetType<"video", "video">.url(src: string, options?: {
mimeType?: string;
}): AssetRef<"video">
url("https://cdn.example.com/trailer.mp4");
function assetKey(ref: AssetRef): stringWhat a ref loads, independent of its type: equal keys load the same bytes.assetKey(const trailer: AssetRef<"video">trailer); // "url:https://cdn.example.com/trailer.mp4"
| Constructor | Names | Resolves to |
|---|---|---|
.file(src) |
A file the host serves | src under the resolver’s publicBase |
.url(src) |
An absolute URL | src itself |
.bytes(data, { mimeType }) |
Bytes the scene carries | An object URL made from the bytes |
.blob(hash, { mimeType }) |
Bytes a store keeps by SHA-256 | blobBase followed by the hash |
A signal declared with signal.of(image) accepts only image references. A reference with a
mimeType outside its type’s range throws when it is made:
image.file("a.png", { mimeType: "video/mp4" }) fails with “Invalid image ref: mimeType must
match image/*”.
Resolvers
An AssetResolver has url(ref), which must answer synchronously because views render
synchronously, and bytes(ref) for decoders. An optional prepare(refs) runs once with
every reference the composition uses, and a promise it returns holds the first frame.
That includes each reference a derived or bound field resolves to over the timeline. A reference
a host will publish on a channel later is not known yet, so prepare cannot see it.
import { function createDefaultResolver(options?: DefaultResolverOptions): AssetResolver`file` → `publicBase` + src (an absolute src, such as a bundled import's URL,
stays as is), `url` → src, `blob` → a blob URL of its payload, else
`blobBase` + hash. `prepare` registers payloads of `blob` refs by hash, so a
ref that crossed a serialization boundary still resolves once the scene that
made it ran here.createDefaultResolver } from "@motionactor/engine/react";
import { const image: AssetType<"image", "image">image } from "@motionactor/schema";
const const resolver: AssetResolverresolver = function createDefaultResolver(options?: DefaultResolverOptions): AssetResolver`file` → `publicBase` + src (an absolute src, such as a bundled import's URL,
stays as is), `url` → src, `blob` → a blob URL of its payload, else
`blobBase` + hash. `prepare` registers payloads of `blob` refs by hash, so a
ref that crossed a serialization boundary still resolves once the scene that
made it ran here.createDefaultResolver({
DefaultResolverOptions.publicBase?: string | URL | ((path: string) => string) | undefinedWhere `file` refs are served from: an absolute base, or a host's own
mapping of a normalized public path (Remotion's `staticFile`). Defaults to
the origin root.publicBase: "https://assets.example.com/reel/",
DefaultResolverOptions.blobBase?: string | URL | ((hash: string) => string) | undefinedWhere stored `blob` refs are served from, as `blobBase` + hash: an absolute
base, or a host's own mapping of the hash. A `blob` whose payload this
process holds loads from that payload instead.blobBase: "https://store.example.com/blobs/",
});
const resolver: AssetResolverresolver.AssetResolver.url(ref: AssetRef): stringA loadable URL for any ref; sync because views render sync.url(const image: AssetType<"image", "image">image.AssetType<"image", "image">.file(src: string, options?: {
mimeType?: string;
}): AssetRef<"image">
file("photos/hero.jpg"));
// "https://assets.example.com/reel/photos/hero.jpg"
locate(ref) comes first when you supply it: return a URL to take over a reference, such as
one in a cache, or undefined to fall through to the rules above. Each host has a default
for publicBase. The native preview resolves against the page’s base URL, Remotion uses
staticFile, and HyperFrames pages use their generated assets directory.
Video and audio
Video, Image, Audio, AudioTrack and AudioManager come from @motionactor/media.
Video and audio play from one signal: sourceTime, the seconds into the source at each
frame. The commands are tweens on it, so playback seeks, scrubs and exports exactly.
import { function createScene<TSceneExports, const I extends SceneInputs = Readonly<Record<never, SceneInput>>, const L extends SceneLayers = Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<I, L, TSceneExports>, options?: SceneDefinitionOptions<I, L>): SceneDefinition<I, L, TSceneExports>Declare a scene. Nothing compiles until a host builds it:
`scene.build({ viewport, fps, inputs })` runs the script as
`(ctx, inputs, viewport, layers)` and returns the compiled scene.
`ctx.spawn` places a root actor in the default layer; `layers.<key>.spawn`
places it in a declared one. `ctx.stage` stages another scene on this
scene's clock, and a staged scene may stage again.createScene, const Easing: {
readonly linear: EasingFunction;
readonly inQuad: EasingFunction;
readonly outQuad: EasingFunction;
readonly inOutQuad: EasingFunction;
readonly inCubic: EasingFunction;
readonly outCubic: EasingFunction;
readonly inOutCubic: EasingFunction;
readonly inQuart: EasingFunction;
readonly outQuart: EasingFunction;
readonly inOutQuart: EasingFunction;
readonly bezier: (x1: number, y1: number, x2: number, y2: number) => EasingFunction;
readonly wiggle: (count: number, type?: WiggleType, amplitude?: number) => EasingFunction;
readonly in: (easing: EasingFunction) => EasingFunction;
readonly out: (easing: EasingFunction) => EasingFunction;
readonly inOut: (easing: EasingFunction) => EasingFunction;
}
Easing, const input: {
of: {
<T extends AnyType>(type: T): ValueInput<T, TypeValue<T>, true>;
<T extends AnyType>(type: T, defaultValue: null): ValueInput<T, TypeValue<T> | null, false>;
<T extends AnyType>(type: T, defaultValue: TypeValue<T>): ValueInput<T, TypeValue<T>, false>;
<TInstance extends object>(blueprint: ActorClass<TInstance>): ActorInput<TInstance>;
};
}
input } from "@motionactor/core";
import { class AudioAudio, class VideoVideo } from "@motionactor/media";
import { const audio: AssetType<"audio", "audio">audio, const video: AssetType<"video", "video">video } from "@motionactor/schema";
import {
const createRenderRuntime: (input: CreateRenderRuntimeInput<ReactBackendHost, React.ReactNode>) => ReactRenderRuntimeThe floor's `createRenderRuntime` for React: every backend's host is a
React component and its stages wrap React nodes, so mixed backends (each
with its own host props) compose without naming a type.createRenderRuntime,
const defineComposition: DefineComposition<ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>A composition of a `createScene` definition (`scene`), painted by `render`,
or by default by `SceneRenderer` over `background`.defineComposition,
} from "@motionactor/renderers/react";
import { const dom: StagedBackend<React.FC<ReactBackendHostProps>, "dom", React.ReactNode>The React DOM backend, and the `dom` binding code names: `dom.render(Box, …)`,
`createRenderRuntime({ backends: [dom] })`.dom } from "@motionactor/dom/react";
const const reelScene: SceneDefinition<{
readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>;
readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>;
}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
clip: Video;
}>
reelScene = createScene<{
clip: Video;
}, {
readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>;
readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>;
}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<{
readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>;
readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>;
}, Readonly<...>, {
clip: Video;
}>, options?: SceneDefinitionOptions<...> | undefined): SceneDefinition<...>
Declare a scene. Nothing compiles until a host builds it:
`scene.build({ viewport, fps, inputs })` runs the script as
`(ctx, inputs, viewport, layers)` and returns the compiled scene.
`ctx.spawn` places a root actor in the default layer; `layers.<key>.spawn`
places it in a declared one. `ctx.stage` stages another scene on this
scene's clock, and a staged scene may stage again.createScene(
function* (ctx: SceneContextctx, { footage: AssetRef<"video">footage, score: AssetRef<"audio">score }) {
const const clip: Videoclip = ctx: SceneContextctx.CompileContext.spawn<Video>(definition: ActorClass<Video>, overrides: {
content: {
source: SignalInputValue<AssetRef<"video">>;
posterSource?: SignalInputValue<AssetRef<"image"> | null> | undefined;
fit?: SignalInputValue<MediaFit> | undefined;
focalPointX?: SignalInputValue<number> | undefined;
focalPointY?: SignalInputValue<number> | undefined;
};
id?: undefined;
localLayout?: undefined;
worldLayout?: undefined;
compositing?: {
...;
} | undefined;
... 31 more ...;
rampTo?: undefined;
}, options?: {
...;
} | undefined): Video (+1 overload)
spawn(class VideoVideo, {
content: {
source: SignalInputValue<AssetRef<"video">>;
posterSource?: SignalInputValue<AssetRef<"image"> | null> | undefined;
fit?: SignalInputValue<MediaFit> | undefined;
focalPointX?: SignalInputValue<number> | undefined;
focalPointY?: SignalInputValue<number> | undefined;
}
content: { source: SignalInputValue<AssetRef<"video">>source: footage: AssetRef<"video">footage },
layout?: {
width?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>> | undefined;
height?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>> | undefined;
x?: SignalInputValue<LayoutValue> | undefined;
y?: SignalInputValue<LayoutValue> | undefined;
} | undefined
layout: { width?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>> | undefined
width: 1280, height?: Readonly<{
kind: "layout.percent";
value: number;
}> | SignalInputValue<number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>> | undefined
height: 720 },
});
const const music: Audiomusic = ctx: SceneContextctx.CompileContext.spawn<Audio>(definition: ActorClass<Audio>, overrides: {
content: {
source: SignalInputValue<AssetRef<"audio">>;
};
id?: undefined;
cursor?: undefined;
cursorSeconds?: undefined;
sceneFps?: undefined;
onInit?: undefined;
onEnter?: undefined;
onExit?: undefined;
enter?: undefined;
exit?: undefined;
parent?: undefined;
play?: undefined;
timing?: {
start?: SignalInputValue<number> | undefined;
end?: SignalInputValue<number> | undefined;
} | undefined;
sourceTime?: SignalInputValue<number> | undefined;
... 9 more ...;
fade?: {
...;
} | undefined;
}, options?: {
...;
} | undefined): Audio (+1 overload)
spawn(class AudioAudio, {
content: {
source: SignalInputValue<AssetRef<"audio">>;
}
content: { source: SignalInputValue<AssetRef<"audio">>source: score: AssetRef<"audio">score },
volume?: SignalInputValue<number> | undefinedvolume: 0.6,
fade?: {
out?: SignalInputValue<number> | undefined;
in?: SignalInputValue<number> | undefined;
} | undefined
fade: { in?: SignalInputValue<number> | undefinedin: 0.5, out?: SignalInputValue<number> | undefinedout: 1 },
});
const music: Audiomusic.Audio.loop(params?: MediaLoopParams): voidloop();
yield* const clip: Videoclip.Video.play(params?: MediaPlayParams): Generator<number, void, unknown>play({ from?: number | undefinedfrom: 4, to?: number | undefinedto: 6 });
yield* const clip: Videoclip.Video.hold(duration: number): Generator<number, void, unknown>hold(0.5);
yield* const clip: Videoclip.Video.rewind(params: MediaScrubParams): Generator<number, void, unknown>rewind({ to: numberto: 4, duration: numberduration: 1, easing?: EasingFunction | undefinedeasing: const Easing: {
readonly linear: EasingFunction;
readonly inQuad: EasingFunction;
readonly outQuad: EasingFunction;
readonly inOutQuad: EasingFunction;
readonly inCubic: EasingFunction;
readonly outCubic: EasingFunction;
readonly inOutCubic: EasingFunction;
readonly inQuart: EasingFunction;
readonly outQuart: EasingFunction;
readonly inOutQuart: EasingFunction;
readonly bezier: (x1: number, y1: number, x2: number, y2: number) => EasingFunction;
readonly wiggle: (count: number, type?: WiggleType, amplitude?: number) => EasingFunction;
readonly in: (easing: EasingFunction) => EasingFunction;
readonly out: (easing: EasingFunction) => EasingFunction;
readonly inOut: (easing: EasingFunction) => EasingFunction;
}
Easing.inOutCubic: EasingFunctioninOutCubic });
yield* const clip: Videoclip.Video.play(params?: MediaPlayParams): Generator<number, void, unknown>play({ rate?: number | undefinedrate: 2 });
return { clip: Videoclip };
},
{ SceneDefinitionOptions<{ readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>; readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<...>, true>; }, Readonly<...>>.inputs?: {
readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>;
readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>;
} | undefined
inputs: { footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>footage: const input: {
of: {
<T extends AnyType>(type: T): ValueInput<T, TypeValue<T>, true>;
<T extends AnyType>(type: T, defaultValue: null): ValueInput<T, TypeValue<T> | null, false>;
<T extends AnyType>(type: T, defaultValue: TypeValue<T>): ValueInput<T, TypeValue<T>, false>;
<TInstance extends object>(blueprint: ActorClass<TInstance>): ActorInput<TInstance>;
};
}
input.of: <AssetType<"video", "video">>(type: AssetType<"video", "video">) => ValueInput<AssetType<"video", "video">, AssetRef<"video">, true> (+3 overloads)Declare a scene input. Over a named type from `@motionactor/schema`,
`input.of(image)` is required; `input.of(text, "Launch day")` and
`input.of(image, null)` are not. Over an actor blueprint,
`input.of(Narrator)` takes a `Narrator` from the scene that stages it,
and has no default.of(const video: AssetType<"video", "video">video), score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>score: const input: {
of: {
<T extends AnyType>(type: T): ValueInput<T, TypeValue<T>, true>;
<T extends AnyType>(type: T, defaultValue: null): ValueInput<T, TypeValue<T> | null, false>;
<T extends AnyType>(type: T, defaultValue: TypeValue<T>): ValueInput<T, TypeValue<T>, false>;
<TInstance extends object>(blueprint: ActorClass<TInstance>): ActorInput<TInstance>;
};
}
input.of: <AssetType<"audio", "audio">>(type: AssetType<"audio", "audio">) => ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true> (+3 overloads)Declare a scene input. Over a named type from `@motionactor/schema`,
`input.of(image)` is required; `input.of(text, "Launch day")` and
`input.of(image, null)` are not. Over an actor blueprint,
`input.of(Narrator)` takes a `Narrator` from the scene that stages it,
and has no default.of(const audio: AssetType<"audio", "audio">audio) } },
);
export const const reel: CompositionDefinition<HostInputArgs<{
readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>;
readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>;
}>, {
clip: Video;
}, ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>
reel = defineComposition<SceneDefinition<{
readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>;
readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>;
}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
clip: Video;
}>>(definition: SceneCompositionInput<SceneDefinition<{
readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>;
readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>;
}, Readonly<...>, {
clip: Video;
}>, ReactBackendHost, React.ReactNode, React.ReactElement<...>>): CompositionDefinition<...> (+1 overload)
`scene`'s inputs are the props.defineComposition({
id: stringid: "Reel",
width: numberwidth: 1920,
height: numberheight: 1080,
fps: numberfps: 30,
defaultProps: {
footage: AssetRef<"video">;
score: AssetRef<"audio">;
} & {}
defaultProps: {
footage: AssetRef<"video">footage: const video: AssetType<"video", "video">video.AssetType<"video", "video">.file(src: string, options?: {
mimeType?: string;
}): AssetRef<"video">
file("media/surf.mp4"),
score: AssetRef<"audio">score: const audio: AssetType<"audio", "audio">audio.AssetType<"audio", "audio">.file(src: string, options?: {
mimeType?: string;
}): AssetRef<"audio">
file("media/theme.mp3"),
},
runtime: RenderRuntime<ReactBackendHost, React.ReactNode>runtime: function createRenderRuntime(input: CreateRenderRuntimeInput<ReactBackendHost, React.ReactNode>): ReactRenderRuntimeThe floor's `createRenderRuntime` for React: every backend's host is a
React component and its stages wrap React nodes, so mixed backends (each
with its own host props) compose without naming a type.createRenderRuntime({ backends: readonly AnyBackend<ReactBackendHost, React.ReactNode>[]backends: [const dom: StagedBackend<React.FC<ReactBackendHostProps>, "dom", React.ReactNode>The React DOM backend, and the `dom` binding code names: `dom.render(Box, …)`,
`createRenderRuntime({ backends: [dom] })`.dom] }),
scene: SceneDefinition<{
readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>;
readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>;
}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
clip: Video;
}>
scene: const reelScene: SceneDefinition<{
readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>;
readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>;
}, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
clip: Video;
}>
reelScene,
});
| Command | sourceTime does |
Scene waits |
|---|---|---|
play({ from?, to?, rate? }) |
Runs from from (or where it is) to to (or the end) at rate |
Yes |
loop({ window?, rate? }) |
Repeats a window until another command takes over | No |
hold(seconds) |
Stays put: a freeze frame | Yes |
seekTo(time) |
Jumps | Yes, zero frames |
fastForward / rewind({ to, duration, easing? }) |
Tweens to to, in either direction |
Yes |
rampTo({ to, rate, duration, fromRate? }) |
Eases its rate from fromRate to rate, then plays on to to |
Yes |
timing: { start, end } selects a window of the source, with end: -1 meaning its end, and
sourceTime counts from start. Video is muted by default, and its sound joins the mix
when you set muted: false. Audio has volume, muted and fade: { in, out } in seconds.
Timed sound cues and sound packs are on Audio.
Before playback, the host turns each clip’s sourceTime curve into segments. A run of frames
with a steady positive slope becomes one segment played at that rate. A hold, a rewind or the
wrap of a loop becomes a seek on each frame. In this reel, frames 0 to 60 are one segment at
rate 1 and frames 105 to 224 one segment at rate 2; the hold and the rewind between them are
seeks.
Lengths a scene needs
play() with no to and loop() with no window need the source’s length, and so does
any timing.end of -1. The scene cannot read a file, so the first build stops with a
MediaDurationRequired naming the reference. A host catches it, measures the file in the
browser, and builds again with the length known. Outside a host, pass the lengths yourself,
keyed by assetKey:
import { function buildComposition<P extends Record<string, unknown>, T, THost, TNode, TRender>(composition: CompositionDefinition<P, T, THost, TNode, TRender>, props: P, options?: CompositionBuildOptions): CompositionBuild<T>buildComposition } from "@motionactor/renderers";
import { function assetKey(ref: AssetRef): stringWhat a ref loads, independent of its type: equal keys load the same bytes.assetKey } from "@motionactor/schema";
import { const reel: CompositionDefinition<HostInputArgs<{
readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>;
readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>;
}>, {
clip: Video;
}, ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>
reel } from "./reel";
const { const footage: AssetRef<"video">footage, const score: AssetRef<"audio">score } = const reel: CompositionDefinition<HostInputArgs<{
readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>;
readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>;
}>, {
clip: Video;
}, ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>
reel.CompositionDefinition<HostInputArgs<{ readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>; readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<...>, true>; }>, { ...; }, ReactBackendHost, React.ReactNode, React.ReactElement<...>>.defaultProps: HostInputArgs<{
readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>;
readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>;
}>
The props a host does not pass. Always present on a defined composition.defaultProps;
using using build: CompositionBuild<{
clip: Video;
}>
build = buildComposition<HostInputArgs<{
readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>;
readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>;
}>, {
clip: Video;
}, ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>(composition: CompositionDefinition<HostInputArgs<{
readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>;
readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>;
}>, {
clip: Video;
}, ReactBackendHost, React.ReactNode, React.ReactElement<...>>, props: HostInputArgs<...>, options?: CompositionBuildOptions): CompositionBuild<...>
buildComposition(const reel: CompositionDefinition<HostInputArgs<{
readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>;
readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>;
}>, {
clip: Video;
}, ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>
reel, const reel: CompositionDefinition<HostInputArgs<{
readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>;
readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>;
}>, {
clip: Video;
}, ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>
reel.CompositionDefinition<HostInputArgs<{ readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>; readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<...>, true>; }>, { ...; }, ReactBackendHost, React.ReactNode, React.ReactElement<...>>.defaultProps: HostInputArgs<{
readonly footage: ValueInput<AssetType<"video", "video">, AssetRef<"video">, true>;
readonly score: ValueInput<AssetType<"audio", "audio">, AssetRef<"audio">, true>;
}>
The props a host does not pass. Always present on a defined composition.defaultProps, {
CompositionBuildOptions.mediaDurations?: Readonly<Record<string, number>> | undefinedmediaDurations: { [function assetKey(ref: AssetRef): stringWhat a ref loads, independent of its type: equal keys load the same bytes.assetKey(const footage: AssetRef<"video">footage)]: 12, [function assetKey(ref: AssetRef): stringWhat a ref loads, independent of its type: equal keys load the same bytes.assetKey(const score: AssetRef<"audio">score)]: 30 },
});
using build: CompositionBuild<{
clip: Video;
}>
build.SceneBuild<{ ...; }>.compiled: CompiledScene<{
clip: Video;
}, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>
compiled.CompiledScene<TSceneExports = void | SceneExportShape, TLayers extends CompiledLayers = Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>.totalDuration: numbertotalDuration; // 225
using build: CompositionBuild<{
clip: Video;
}>
build.SceneBuild<{ ...; }>.compiled: CompiledScene<{
clip: Video;
}, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>
compiled.CompiledScene<{ ...; }, Readonly<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
clip: Video;
}>
at(90).FrameState<{ ...; }>.scene: {
clip: ResolvedActorOfInstance<Video> | undefined;
}
scene.clip: ResolvedActorOfInstance<Video> | undefinedclip?.sourceTime: number | undefinedSeconds into the selected source window.sourceTime; // 5, halfway back
A host can only catch the error during a build it runs. That is why reelScene takes its
media as inputs, so the host builds it. A scene built when its module loads asks for the
length before any host is there to measure it.
Drawing media
mediaRendererRegistry from @motionactor/media/react holds the DOM renderers for the media
actors and the collector that finds every clip a scene plays. Every host (the native preview,
HyperFrames and Remotion) adds it after the composition’s own renderers with
withMediaFamily, so reel lists none. The views are DOM renderers, so Image and Video
actors belong in a DOM layer.
For a designed variant, extend the actor and reuse its view. style is merged over the
view’s own box:
import { function signal<T>(): RequiredSignal<T> (+1 overload)
module signal
A timeline value on an actor, and an input to every spawn of it.
Without an initial value the signal is required: `ctx.spawn` must supply it.signal } from "@motionactor/core";
import { class ImageImage } from "@motionactor/media";
import { const ImageView: React.FC<ImageViewProps>ImageView } from "@motionactor/media/react";
import { const dom: StagedBackend<React.FC<ReactBackendHostProps>, "dom", React.ReactNode>The React DOM backend, and the `dom` binding code names: `dom.render(Box, …)`,
`createRenderRuntime({ backends: [dom] })`.dom } from "@motionactor/dom/react";
export class class PhotoPhoto extends class ImageImage {
Photo.radius: ActorFieldSignal<number>radius = signal<number>(initial: number): ActorFieldSignal<number> (+1 overload)A timeline value on an actor, and an input to every spawn of it.
Without an initial value the signal is required: `ctx.spawn` must supply it.signal(24);
}
export const const photo: BackendRendererDefinition<readonly [typeof Photo], "dom">photo = const dom: StagedBackend<React.FC<ReactBackendHostProps>, "dom", React.ReactNode>The React DOM backend, and the `dom` binding code names: `dom.render(Box, …)`,
`createRenderRuntime({ backends: [dom] })`.dom.render: <typeof Photo>(blueprint: typeof Photo, render: ActorRendererForBlueprints<readonly [typeof Photo], "dom">, options?: ActorRendererRegistrationOptions) => BackendRendererDefinition<readonly [typeof Photo], "dom"> (+1 overload)render(class PhotoPhoto, ({ actor: ResolvedActor & Omit<ResolvedActor, "signals" | "channels"> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
x: LayoutValue;
y: LayoutValue;
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | ... 1 more ... | Readonly<...>;
};
... 8 more ...;
objectPosition: string;
} & {
...;
}
The actor. Reading it makes the renderer depend on all of it.actor }) => (
<const ImageView: React.FC<ImageViewProps>ImageView
actor: Omit<ResolvedActor, "signals" | "channels"> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
x: LayoutValue;
y: LayoutValue;
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | ... 1 more ... | Readonly<...>;
};
... 7 more ...;
objectPosition: string;
} & {
...;
}
actor={actor: ResolvedActor & Omit<ResolvedActor, "signals" | "channels"> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
x: LayoutValue;
y: LayoutValue;
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | ... 1 more ... | Readonly<...>;
};
... 8 more ...;
objectPosition: string;
} & {
...;
}
The actor. Reading it makes the renderer depend on all of it.actor}
style?: React.CSSProperties | undefinedMerged over the frame's own box: how a designed subclass adds chrome.style={{ StandardShorthandProperties<string | number, string & {}>.borderRadius?: Property.BorderRadius<string | number> | undefinedThis feature is well established and works across many devices and browser versions. It’s been available across browsers since July 2015.
**Syntax**: `<length-percentage [0,∞]>{1,4} [ / <length-percentage [0,∞]>{1,4} ]?`
| Chrome | Firefox | Safari | Edge | IE |
| :-----: | :-----: | :-----: | :----: | :---: |
| **4** | **4** | **5** | **12** | **9** |
| 1 _-x-_ | | 3 _-x-_ | | |borderRadius: actor: ResolvedActor & Omit<ResolvedActor, "signals" | "channels"> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
x: LayoutValue;
y: LayoutValue;
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | ... 1 more ... | Readonly<...>;
};
... 8 more ...;
objectPosition: string;
} & {
...;
}
The actor. Reading it makes the renderer depend on all of it.actor.signals: Readonly<Record<string, unknown>> & {
compositing: {
blend: "source-over" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus-lighter";
isolate: boolean;
clip: number | null;
};
measuredBox: Rect | undefined;
layout: {
x: LayoutValue;
y: LayoutValue;
width: number | `${number}%` | Readonly<{
kind: "layout.percent";
value: number;
}>;
height: number | ... 1 more ... | Readonly<...>;
};
... 8 more ...;
objectPosition: string;
}
signals.radius: numberradius }}
/>
));
List photo in the runtime’s renderers. The media family comes after them, so Photo gets
this renderer and a plain Image still gets the built-in one.
An image holds its frame until the browser has decoded it, so the first frame that shows it already shows all of it.
Fonts
A composition lists the font faces it draws with. The host loads each one through the
FontFace API and holds the first frame until all of them are ready, so text measures and
paints in the right face from frame 0.
import { const font: AssetType<"font", "font">font } from "@motionactor/schema";
import { const defineComposition: DefineComposition<ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>A composition of a `createScene` definition (`scene`), painted by `render`,
or by default by `SceneRenderer` over `background`.defineComposition } from "@motionactor/renderers/react";
export const const titleCard: CompositionDefinition<HostInputArgs<Readonly<Record<never, SceneInput>>>, void, ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>titleCard = defineComposition<SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, void>>(definition: SceneCompositionInput<SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, void>, ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>): CompositionDefinition<...> (+1 overload)`scene`'s inputs are the props.defineComposition({
id: stringid: "TitleCard",
width: numberwidth: 1920,
height: numberheight: 1080,
fps: numberfps: 30,
runtime: RenderRuntime<ReactBackendHost, React.ReactNode>runtime,
fonts?: readonly CompositionFont[] | undefinedfonts: [
{ CompositionFont.family: stringfamily: "Inter", CompositionFont.source: AssetRef<"font">source: const font: AssetType<"font", "font">font.AssetType<"font", "font">.file(src: string, options?: {
mimeType?: string;
}): AssetRef<"font">
file("fonts/Inter.woff2") },
{
CompositionFont.family: stringfamily: "Inter",
CompositionFont.source: AssetRef<"font">source: const font: AssetType<"font", "font">font.AssetType<"font", "font">.file(src: string, options?: {
mimeType?: string;
}): AssetRef<"font">
file("fonts/Inter-Bold.woff2"),
CompositionFont.weight?: string | undefinedweight: "700",
},
],
scene: SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, void>scene: const titleScene: SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, void>titleScene,
render?(args: CompositionRenderArgs<HostInputArgs<Readonly<Record<never, SceneInput>>>, void>): React.ReactElement<unknown, string | React.JSXElementConstructor<any>>render: ({ compiled: CompiledScene<void, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>compiled, frame: numberframe, width: numberwidth, height: numberheight }) => (
<const SceneRenderer: <TSceneExports>({ compiled, frame, width, height, sceneMeta, style, children, runtime, }: {
compiled: CompiledScene<TSceneExports>;
frame: number;
width: number;
height: number;
sceneMeta?: unknown;
style?: React.CSSProperties;
children?: React.ReactNode;
runtime?: ReactRenderRuntime;
}) => JSX.Element
Frame-passive renderer for a CompiledScene: resolves
the frame and paints its layers (`SceneLayers`) on a stage of `width` by
`height`. Each layer is painted by the host its backend registers on the
runtime; the default layer by the runtime's default backend. `children` are
page markup: they render in the scope of the scene's DOM layer, where
`<actor.h1 of={compiled.scene.card.title}>` binds a node of it to an actor
(`SceneLayers`).SceneRenderer
compiled: CompiledScene<void, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>compiled={compiled: CompiledScene<void, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>compiled}
frame: numberframe={frame: numberframe}
width: numberwidth={width: numberwidth}
height: numberheight={height: numberheight}
/>
),
});
Each face takes the @font-face descriptors: weight, style, stretch, unicodeRange
and featureSettings. A face that has not loaded after 15 seconds fails the frame. Loaded
faces are cached per document, so remounting a composition does not fetch them again.