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:
- npm
- yarn
- pnpm
bash
npm init video --blank
bash
npm init video --blank
bash
pnpm create video --blank
bash
pnpm create video --blank
bash
yarn create video --blank
bash
yarn create 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.tsts
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.tsts
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.tsxtsx
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 (<AbsoluteFillstyle={{scale: String(scale),backgroundColor: "white",fontWeight: "bold",justifyContent: "center",alignItems: "center",}}><divstyle={{display: "flex",flexDirection: "row",alignItems: "center",gap: 20,}}><Imgsrc={logo}style={{height: 80,}}/><divstyle={{display: "flex",flexDirection: "column",}}><divstyle={{fontSize: 40,transform: `translateY(${moveY}px)`,lineHeight: 1,}}>{name}</div><divstyle={{fontSize: 20,opacity,lineHeight: 1.25,}}>{repo}</div></div></div></AbsoluteFill>);};
Composition.tsxtsx
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 (<AbsoluteFillstyle={{scale: String(scale),backgroundColor: "white",fontWeight: "bold",justifyContent: "center",alignItems: "center",}}><divstyle={{display: "flex",flexDirection: "row",alignItems: "center",gap: 20,}}><Imgsrc={logo}style={{height: 80,}}/><divstyle={{display: "flex",flexDirection: "column",}}><divstyle={{fontSize: 40,transform: `translateY(${moveY}px)`,lineHeight: 1,}}>{name}</div><divstyle={{fontSize: 20,opacity,lineHeight: 1.25,}}>{repo}</div></div></div></AbsoluteFill>);};
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";constbundleLocation = awaitbundle ({entryPoint : "./src/index.ts",// If you have a webpack override, don't forget to add itwebpackOverride :webpackOverride ,});
ts
import {bundle } from "@remotion/bundler";import {webpackOverride } from "./webpack-override";constbundleLocation = awaitbundle ({entryPoint : "./src/index.ts",// If you have a webpack override, don't forget to add itwebpackOverride :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";constcompositionId = "MyComp";constallCompositions = awaitgetCompositions (bundleLocation );constcomposition =allCompositions .find ((c ) =>c .id ===compositionId );if (!composition ) {throw newError (`No composition with the ID ${compositionId } found.`);}
tsx
import {getCompositions } from "@remotion/renderer";constcompositionId = "MyComp";constallCompositions = awaitgetCompositions (bundleLocation );constcomposition =allCompositions .find ((c ) =>c .id ===compositionId );if (!composition ) {throw newError (`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
.
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 (constentry ofdata ) {awaitrenderMedia ({composition ,serveUrl :bundleLocation ,codec : "h264",outputLocation : `out/${entry .name }.mp4`,inputProps :entry ,});}
ts
import {renderMedia } from "@remotion/renderer";import {data } from "./dataset";for (constentry ofdata ) {awaitrenderMedia ({composition ,serveUrl :bundleLocation ,codec : "h264",outputLocation : `out/${entry .name }.mp4`,inputProps :entry ,});}
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.tsts
import {getCompositions ,renderMedia } from "@remotion/renderer";import {webpackOverride } from "./webpack-override";import {bundle } from "@remotion/bundler";import {data } from "./dataset";constcompositionId = "MyComp";conststart = async () => {constbundleLocation = awaitbundle ({entryPoint : "./src/index.ts",// If you have a webpack override, don't forget to add itwebpackOverride :webpackOverride ,});constallCompositions = awaitgetCompositions (bundleLocation );constcomposition =allCompositions .find ((c ) =>c .id ===compositionId );if (!composition ) {throw newError (`No composition with the ID ${compositionId } found.`);}for (constentry ofdata ) {awaitrenderMedia ({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.tsts
import {getCompositions ,renderMedia } from "@remotion/renderer";import {webpackOverride } from "./webpack-override";import {bundle } from "@remotion/bundler";import {data } from "./dataset";constcompositionId = "MyComp";conststart = async () => {constbundleLocation = awaitbundle ({entryPoint : "./src/index.ts",// If you have a webpack override, don't forget to add itwebpackOverride :webpackOverride ,});constallCompositions = awaitgetCompositions (bundleLocation );constcomposition =allCompositions .find ((c ) =>c .id ===compositionId );if (!composition ) {throw newError (`No composition with the ID ${compositionId } found.`);}for (constentry ofdata ) {awaitrenderMedia ({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.
- npm
- yarn
- pnpm
bash
npm i ts-node
bash
npm i ts-node
bash
pnpm i ts-node
bash
pnpm i ts-node
bash
yarn add ts-node
bash
yarn add 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.tsts
for (constentry ofdata ) {constallCompositions = awaitgetCompositions (bundleLocation , {// Add input props to getCompositions() as well.inputProps :entry ,});constcomposition =allCompositions .find ((c ) =>c .id ===compositionId );if (!composition ) {throw newError (`No composition with the ID ${compositionId } found.`);}awaitrenderMedia ({composition ,serveUrl :bundleLocation ,codec : "h264",outputLocation : `out/${entry .name }.mp4`,inputProps :entry ,});}
render.tsts
for (constentry ofdata ) {constallCompositions = awaitgetCompositions (bundleLocation , {// Add input props to getCompositions() as well.inputProps :entry ,});constcomposition =allCompositions .find ((c ) =>c .id ===compositionId );if (!composition ) {throw newError (`No composition with the ID ${compositionId } found.`);}awaitrenderMedia ({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.