Skip to main content

Render videos programmatically from a dataset

You can use Remotion to do a batch render to create many videos based on a dataset. In the following example, we are going to turn a JSON dataset into a series of videos.

We'll start by creating a blank Remotion project:

bash
npm init video --blank
bash
npm init video --blank

Sample dataset

JSON is the most convenient format to import in Remotion. If your dataset is in a different format, you can convert it using one of many available libraries on NPM.

my-data.ts
ts
export const data = [
{
name: "React",
repo: "facebook/react",
logo: "https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg",
},
{
name: "Remotion",
repo: "remotion-dev/remotion",
logo: "https://github.com/remotion-dev/logo/raw/main/withouttitle/element-0.png",
},
];
my-data.ts
ts
export const data = [
{
name: "React",
repo: "facebook/react",
logo: "https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg",
},
{
name: "Remotion",
repo: "remotion-dev/remotion",
logo: "https://github.com/remotion-dev/logo/raw/main/withouttitle/element-0.png",
},
];

Sample component

This component will animate a title, subtitle and image using Remotion. Replace the contents of the src/Composition.tsx file with the following:

Composition.tsx
tsx
import React from "react";
import {
AbsoluteFill,
Img,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
interface Props {
name: string;
logo: string;
repo: string;
}
export const MyComposition: React.FC<Props> = ({ name, repo, logo }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scale = spring({
fps,
frame: frame - 10,
config: {
damping: 100,
},
});
const opacity = interpolate(frame, [30, 40], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const moveY = interpolate(frame, [20, 30], [10, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<AbsoluteFill
style={{
scale: String(scale),
backgroundColor: "white",
fontWeight: "bold",
justifyContent: "center",
alignItems: "center",
}}
>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 20,
}}
>
<Img
src={logo}
style={{
height: 80,
}}
/>
<div
style={{
display: "flex",
flexDirection: "column",
}}
>
<div
style={{
fontSize: 40,
transform: `translateY(${moveY}px)`,
lineHeight: 1,
}}
>
{name}
</div>
<div
style={{
fontSize: 20,
opacity,
lineHeight: 1.25,
}}
>
{repo}
</div>
</div>
</div>
</AbsoluteFill>
);
};
Composition.tsx
tsx
import React from "react";
import {
AbsoluteFill,
Img,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
interface Props {
name: string;
logo: string;
repo: string;
}
export const MyComposition: React.FC<Props> = ({ name, repo, logo }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scale = spring({
fps,
frame: frame - 10,
config: {
damping: 100,
},
});
const opacity = interpolate(frame, [30, 40], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const moveY = interpolate(frame, [20, 30], [10, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<AbsoluteFill
style={{
scale: String(scale),
backgroundColor: "white",
fontWeight: "bold",
justifyContent: "center",
alignItems: "center",
}}
>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 20,
}}
>
<Img
src={logo}
style={{
height: 80,
}}
/>
<div
style={{
display: "flex",
flexDirection: "column",
}}
>
<div
style={{
fontSize: 40,
transform: `translateY(${moveY}px)`,
lineHeight: 1,
}}
>
{name}
</div>
<div
style={{
fontSize: 20,
opacity,
lineHeight: 1.25,
}}
>
{repo}
</div>
</div>
</div>
</AbsoluteFill>
);
};
Remotion
remotion-dev/remotion
0:00 / 0:02

Writing the script

In order to render our videos, we'll first need to bundle our project using Webpack and prepare it for rendering. This can be done by using the bundle() function from the @remotion/bundler package. Make sure to include the webpack override in the bundle if you have one.

ts
import { bundle } from "@remotion/bundler";
import { webpackOverride } from "./webpack-override";
 
const bundleLocation = await bundle({
entryPoint: "./src/index.ts",
// If you have a webpack override, don't forget to add it
webpackOverride: webpackOverride,
});
ts
import { bundle } from "@remotion/bundler";
import { webpackOverride } from "./webpack-override";
 
const bundleLocation = await bundle({
entryPoint: "./src/index.ts",
// If you have a webpack override, don't forget to add it
webpackOverride: webpackOverride,
});

Getting the composition

We can use getCompositions() to extract all the defined compositions. Select the composition by searching for the composition ID that is defined in src/Root.tsx - by default MyComp:

tsx
import { getCompositions } from "@remotion/renderer";
 
const compositionId = "MyComp";
const allCompositions = await getCompositions(bundleLocation);
 
const composition = allCompositions.find((c) => c.id === compositionId);
 
if (!composition) {
throw new Error(`No composition with the ID ${compositionId} found.`);
}
tsx
import { getCompositions } from "@remotion/renderer";
 
const compositionId = "MyComp";
const allCompositions = await getCompositions(bundleLocation);
 
const composition = allCompositions.find((c) => c.id === compositionId);
 
if (!composition) {
throw new Error(`No composition with the ID ${compositionId} found.`);
}

By throwing an error if the composition does not exist, we tell TypeScript that we are sure that composition is not undefined.

note

If the duration or dimensions of your video are dependent on your data, pass the data to getCompositions() as well. See here for an example.

Rendering videos

Import the dataset and loop over each entry. Trigger a render using renderMedia() and pass the data entry as inputProps. This will pass the object as React props to the component above.

ts
import { renderMedia } from "@remotion/renderer";
import { data } from "./dataset";
 
for (const entry of data) {
await renderMedia({
composition,
serveUrl: bundleLocation,
codec: "h264",
outputLocation: `out/${entry.name}.mp4`,
inputProps: entry,
});
}
ts
import { renderMedia } from "@remotion/renderer";
import { data } from "./dataset";
 
for (const entry of data) {
await renderMedia({
composition,
serveUrl: bundleLocation,
codec: "h264",
outputLocation: `out/${entry.name}.mp4`,
inputProps: entry,
});
}
note

It is not recommended to render more than one video at once.

Full script

Currently, top level await is not enable by default in Node, so all asynchronous functions were wrapped in an async function and which is immediately called.

render.ts
ts
import { getCompositions, renderMedia } from "@remotion/renderer";
import { webpackOverride } from "./webpack-override";
import { bundle } from "@remotion/bundler";
import { data } from "./dataset";
 
const compositionId = "MyComp";
 
const start = async () => {
const bundleLocation = await bundle({
entryPoint: "./src/index.ts",
// If you have a webpack override, don't forget to add it
webpackOverride: webpackOverride,
});
 
const allCompositions = await getCompositions(bundleLocation);
 
const composition = allCompositions.find((c) => c.id === compositionId);
 
if (!composition) {
throw new Error(`No composition with the ID ${compositionId} found.`);
}
 
for (const entry of data) {
await renderMedia({
composition,
serveUrl: bundleLocation,
codec: "h264",
outputLocation: `out/${entry.name}.mp4`,
inputProps: entry,
});
}
};
 
start()
.then(() => {
console.log("Rendered all videos");
})
.catch((err) => {
console.log("Error occurred:", err);
});
render.ts
ts
import { getCompositions, renderMedia } from "@remotion/renderer";
import { webpackOverride } from "./webpack-override";
import { bundle } from "@remotion/bundler";
import { data } from "./dataset";
 
const compositionId = "MyComp";
 
const start = async () => {
const bundleLocation = await bundle({
entryPoint: "./src/index.ts",
// If you have a webpack override, don't forget to add it
webpackOverride: webpackOverride,
});
 
const allCompositions = await getCompositions(bundleLocation);
 
const composition = allCompositions.find((c) => c.id === compositionId);
 
if (!composition) {
throw new Error(`No composition with the ID ${compositionId} found.`);
}
 
for (const entry of data) {
await renderMedia({
composition,
serveUrl: bundleLocation,
codec: "h264",
outputLocation: `out/${entry.name}.mp4`,
inputProps: entry,
});
}
};
 
start()
.then(() => {
console.log("Rendered all videos");
})
.catch((err) => {
console.log("Error occurred:", err);
});

Running the script

To help us in running the render, we need to install ts-node from npm.

bash
npm i ts-node
bash
npm i ts-node

You can then run the script using

bash
npx ts-node render.ts
bash
npx ts-node render.ts

Rendering videos from a CSV dataset

Use a package like csv2json to convert your dataset into a JSON.

Change duration, width or height dynamically

To make the duration or the dimensions dependent on your data, call getCompositions() with the inputProps option. For this you need to move getCompositions() into the for loop like this:

render.ts
ts
for (const entry of data) {
const allCompositions = await getCompositions(bundleLocation, {
// Add input props to getCompositions() as well.
inputProps: entry,
});
 
const composition = allCompositions.find((c) => c.id === compositionId);
 
if (!composition) {
throw new Error(`No composition with the ID ${compositionId} found.`);
}
 
await renderMedia({
composition,
serveUrl: bundleLocation,
codec: "h264",
outputLocation: `out/${entry.name}.mp4`,
inputProps: entry,
});
}
render.ts
ts
for (const entry of data) {
const allCompositions = await getCompositions(bundleLocation, {
// Add input props to getCompositions() as well.
inputProps: entry,
});
 
const composition = allCompositions.find((c) => c.id === compositionId);
 
if (!composition) {
throw new Error(`No composition with the ID ${compositionId} found.`);
}
 
await renderMedia({
composition,
serveUrl: bundleLocation,
codec: "h264",
outputLocation: `out/${entry.name}.mp4`,
inputProps: entry,
});
}

Learn how to change the metadata based on the input props here.

Credits

Authored by Alex Fernandez and ThePerfectSystem, edited by Jonny Burger.

See also