Programmatic Video with Remotion and AI: A Batch Marketing Video Production Guide
This guide explains when programmatic video is suitable for marketing, how Remotion turns frames into deterministic video components, and how to produce videos in batches from data-driven templates. It covers environment setup, font loading, composition code, rendering commands, secure distribution, AI-assisted code generation, tool selection, cost estimation, troubleshooting, and compliance considerations.
When to Use Programmatic Video: Define the Fit First
Programmatic video works best for structured, template-driven, batch-oriented marketing videos, such as personalized ad variants, data dashboard updates, localized versions for multiple languages, and A/B test assets. It is not a good fit for highly bespoke creative films, live-action compositing, detailed color grading, or nonlinear narrative editing. In those cases, manual design often has a lower marginal cost.
Before choosing this approach, ask three questions:
- Is the video mainly composed of titles, cards, numbers, buttons, and logos?
- Can many versions be generated by replacing copy, images, languages, or color themes?
- Do you need the rendered output to be reproducible, auditable, and easy to integrate into a pipeline?
If the answer is yes, programmatic video is a good fit.
Core Principle: A Frame Is a Function
Traditional video editing tools focus on layers along a timeline. Programmatic video focuses on a different question: Given frame N, what should the screen look like?
The workflow can be divided into three steps:
- Get the data: Read the content from JSON, spreadsheets, databases, or APIs.
- Write the components: Define layouts and animations with declarative components, using the current frame number as input.
- Render the output: Generate images frame by frame at the target frame rate, then encode them into standard video files such as MP4 or WebM.
A simplified frame-driven example looks like this:
function IntroFrame({ frame, fps }) {const progress = Math.min(1, frame / (fps * 2));const opacity = progress;const offsetX = (1 - progress) * 100;return (<Layout><Titletext="Product Introduction"style={{ opacity, transform: `translateX(${offsetX}px)` }}/></Layout>);}
The key idea is that the visual is not a manually arranged static result; it is a function of the frame number. To change the animation, change the formula. To change the content, change the data. That is what makes the approach reusable and batch-friendly.
Setting Up Remotion
This guide uses Remotion as the example. Remotion is based on React and is frame-driven, making it suitable for templating marketing videos.
The commands below use Remotion 4.x as the reference. If your team is using an older version, first check the local version:
npm ls remotion @remotion/cli
You can also use npx remotion versions to check whether component and CLI versions match. If that command is not available in your version, refer to package.json and the official documentation.
# 1. Runtime: Node.js 18 or newer; Node.js 20 LTS is recommendednode -v && npm -v# 2. Create the projectnpx create-video@latest my-product-videocd my-product-videonpm install# 3. Check versions and dependenciesnpm ls remotion @remotion/clinpx remotion versions # Available in Remotion 4.x; otherwise use the previous commandnpx remotion browser ensure # Ensure headless Chromium is availablenpx remotion compositions # List registered compositions and verify that the project can be parsednpx remotion studio # Open the local preview and inspect the layout frame by frame
A few rendering-side details are worth noting:
- Chromium: Remotion manages the required headless Chromium on demand. You do not need to install Chrome manually.
- Encoding: The built-in renderer is usually used. If you integrate with system FFmpeg, make sure
ffmpeg -versionworks and includes an H.264 encoder. - Chinese fonts: They are not automatically inherited from the system. They must be loaded explicitly.
- Linux containers: Debian/Ubuntu-based images are recommended. Install dependencies such as
libnss3,libatk-bridge2.0-0, andfonts-noto-cjk. Chromium can be started with--no-sandbox. Set concurrency according to the available CPU and memory quota.
Loading Fonts Explicitly
In Remotion, fonts must be registered through loadFont. The following example shows the full parameter set, including family, url, weight, and style.
Create src/font.ts:
import { loadFont, staticFile } from "remotion";export const FONT_FAMILY = "NotoSansSC";// Regular weightloadFont({family: FONT_FAMILY,url: staticFile("fonts/NotoSansSC-Regular.woff2"),weight: "400",style: "normal",});// Bold weightloadFont({family: FONT_FAMILY,url: staticFile("fonts/NotoSansSC-Bold.woff2"),weight: "700",style: "normal",});
Use the font inside a component:
const titleStyle = {fontFamily: FONT_FAMILY,fontWeight: 700,} as const;<h1 style={titleStyle}>Quarterly Launch</h1>
A useful self-check command is:
fc-list :lang=zh
If fc-list cannot list the target font while rendering inside a container, first check whether public/fonts/ exists, whether the file path is correct, and whether the font license allows embedding.
Data-Driven Directory Structure
It is recommended to separate the presentation layer from the data layer. Components are written once and reused in many places; the data layer handles variation.
product-video/├─ src/│ ├─ Root.tsx # Register compositions│ ├─ ProductIntro.tsx # Main component and scenes│ ├─ font.ts # Font loading│ ├─ components/ # Title / Card / CtaButton│ └─ theme.ts # Colors, font sizes, safe areas├─ public/│ ├─ fonts/ # Chinese font files; verify licensing│ └─ assets/ # Images and logos├─ data/│ └─ campaign.json # Data layer└─ package.json
Example data in data/campaign.json:
{"title": "Quarterly Launch","cards": [{ "icon": "bolt", "headline": "Fast Output", "desc": "Template-based generation" },{ "icon": "globe", "headline": "Multilingual", "desc": "Only data changes" },{ "icon": "shield", "headline": "Auditable", "desc": "Reproducible rendering results" }],"cta": { "text": "Try Now", "url": "https://example.com" }}
The field mapping rules should be documented in the team template guide:
titlemaps to the title component.cards[i]maps to the i-th card. Each card enters with a default interval of 15 frames.ctamaps to the closing button. It is displayed during the last 2 seconds by default.
Example Components and Render Commands
1. Composition Configuration
src/Root.tsx:
import { Composition } from "remotion";import { ProductIntro, FPS, DURATION } from "./ProductIntro";export const RemotionRoot = () => (<Compositionid="product-intro"component={ProductIntro}durationInFrames={DURATION}fps={FPS}width={1080}height={1920}defaultProps={{title: "Quarterly Launch",cards: [],cta: { text: "Try Now", url: "" },}}/>);
2. Main Component
src/ProductIntro.tsx:
import {AbsoluteFill,Sequence,useCurrentFrame,useVideoConfig,interpolate,spring,} from "remotion";import { FONT_FAMILY } from "./font";export const FPS = 30;export const VIDEO_SECONDS = 8;export const DURATION = FPS * VIDEO_SECONDS;type Card = {icon?: string;headline: string;desc: string;};export type Props = {title: string;cards: Card[];cta: { text: string; url: string };};const Title = ({ text }: { text: string }) => {const frame = useCurrentFrame();const { fps } = useVideoConfig();const opacity = interpolate(frame,[0, fps * 0.6],[0, 1],{ extrapolateRight: "clamp" });const x = interpolate(frame,[0, fps * 0.6],[60, 0],{ extrapolateRight: "clamp" });return (<h1style={{opacity,transform: `translateX(${x}px)`,fontFamily: FONT_FAMILY,fontSize: 88,color: "#fff",}}>{text}</h1>);};const CardItem = ({ card, index }: { card: Card; index: number }) => {const frame = useCurrentFrame();const { fps } = useVideoConfig();// Inside a Sequence, frames are relative by default, so there is no need to subtract `from`.const local = frame - index * 15;const x = interpolate(local,[0, fps * 0.5],[-240, 0],{extrapolateLeft: "clamp",extrapolateRight: "clamp",});const opacity = interpolate(local,[0, fps * 0.4],[0, 1],{extrapolateLeft: "clamp",extrapolateRight: "clamp",});return (<divstyle={{opacity,transform: `translateX(${x}px)`,width: 880,padding: 32,background: "#1f2937",borderRadius: 24,fontFamily: FONT_FAMILY,}}><div style={{ fontSize: 48, color: "#fff", fontWeight: 700 }}>{card.headline}</div><div style={{ fontSize: 32, color: "#9ca3af" }}>{card.desc}</div></div>);};const Cta = ({ text }: { text: string }) => {const frame = useCurrentFrame();const { fps } = useVideoConfig();// The Cta component is inside a Sequence. `useCurrentFrame` is relative by default,// so calling `spring({ frame, fps })` directly is sufficient.const scale = spring({frame,fps,config: { damping: 200 },});return (<buttonstyle={{transform: `scale(${scale})`,fontFamily: FONT_FAMILY,fontSize: 52,padding: "24px 64px",borderRadius: 999,}}>{text}</button>);};export const ProductIntro: React.FC<Props> = ({ title, cards, cta }) => {return (<AbsoluteFillstyle={{background: "#0b1020",justifyContent: "center",alignItems: "center",gap: 48,}}><Sequence durationInFrames={FPS * 2}><Title text={title} /></Sequence><Sequence from={FPS * 3} durationInFrames={FPS * 3}><AbsoluteFillstyle={{justifyContent: "center",alignItems: "center",gap: 32,}}>{cards.map((card, index) => (<CardItem key={card.headline} card={card} index={index} />))}</AbsoluteFill></Sequence><Sequence from={FPS * 6} durationInFrames={FPS * 2}><AbsoluteFillstyle={{justifyContent: "flex-end",alignItems: "center",paddingBottom: 200,}}><Cta text={cta.text} /></AbsoluteFill></Sequence></AbsoluteFill>);};
If you truly need absolute frames, you can use:
const absoluteFrame = useCurrentFrame({ relativeToSequence: false });
Then calculate based on the absolute frame. The examples in this article use relative-frame logic inside Sequence to avoid the CTA having no animation.
3. Render the Output
# Render the full video with JSON datanpx remotion render product-intro out/intro-en.mp4 \--props=./data/campaign.json \--codec=h264 \--crf=20 \--concurrency=4 \--log=verbose# Render only the first 2 seconds for a quick checknpx remotion render product-intro out/preview.mp4 \--frames=0-60# Render a single frame to inspect layout and fontsnpx remotion still product-intro out/frame-90.png \--frame=90
Parameter recommendations:
- A lower
--crfvalue means higher quality. The common range is 18 to 23. --concurrencyincreases parallelism, but it is limited by memory and CPU quotas.- Vertical social media assets commonly use 1080×1920 at 30fps.
- If the platform supports it, 720×1280 can reduce rendering and upload time.
Upload and Access Control: Keep Buckets Private
Rendered outputs are usually uploaded to object storage and then distributed. This step is a common security weak point.
Recommended practices:
- Keep the bucket private. Do not enable public read access.
- Use time-limited presigned URLs or CDN authentication for distribution.
- Pass access credentials through environment variables or a secret management service. AK/SK must never be committed to the repository.
- Organize assets by business directory and configure lifecycle rules to clean up intermediate files.
- Record the template version, data snapshot, and asset license source for each video.
- Validate filenames with a whitelist in the upload script to prevent path traversal.
Integrating AI Coding Assistants
Programmatic video still has a learning curve for developers, including the component model, interpolation, frame synchronization, and rendering configuration. An AI coding assistant can understand project conventions through a project conventions bundle, such as the directory structure, component patterns, and timeline rules. It can then translate natural language into project code.
An example natural-language instruction is:
Generate an 8-second, 1080×1920 vertical feature video: fade in the title at the first second, slide in three cards from the left starting at the third second, and show the CTA button with a scale-up during the final 2 seconds.
A more reliable workflow is:
- Have the AI assistant first produce a storyboard.
- Manually confirm the time ranges, elements, and animation types.
- Generate the component code.
- Use single-frame rendering to verify the layout.
- Use short-clip rendering to verify the animation.
- Render in batches at the end.
Output quality depends on the precision of the requirement description, the quality of the conventions bundle, and whether the fonts and assets are complete.
How to Choose Between Remotion, Motion Canvas, and Manim
There is no universal best option. Choose according to team constraints:
| Option | Better Fit | Notes |
|---|---|---|
| Remotion | React/TypeScript teams producing marketing videos in batches | Complete rendering pipeline; low migration cost for frontend teams |
| Motion Canvas | Animation expression and technical demonstrations | General-purpose batch video capabilities need to be added separately |
| Manim | Mathematical formulas and geometric explanations | Moderate cost-performance for marketing assets |
| After Effects + scripting | High-precision output in small quantities | Automation and engineering costs are relatively high |
| Headless browser + FFmpeg | Fully self-built pipelines | You must maintain fonts, concurrency, retries, and failure recovery |
Five selection criteria are useful: existing team stack, render time per video, dependency complexity, deployment method, and whether interactive preview and parameter tuning are needed. A practical approach is to run the same 8-second composition in each candidate option, record the render time and file size, and then decide.
How to Calculate Cost
Programmatic video can usually reduce manual work per video, but total cost depends on template complexity, render specifications, compute unit price, storage and bandwidth, asset licensing, and template development amortization.
A more rigorous formula is:
Cost per video =render instance hours × instance price+ storage and traffic cost+ asset license amortization+ template development cost ÷ expected number of outputs
If you use function compute or on-demand GPUs, billing is based on actual execution time, memory, CPU, and GPU resources. Do not divide wall-clock render time directly by concurrency. Concurrency affects throughput, but it does not mean the bill decreases proportionally.
Example calculation:
| Item | Assumption | Notes |
|---|---|---|
| Video length | 8 seconds | 240 frames at 30fps |
| Resolution | 1080×1920 | Vertical |
| Instance type | 4 vCPU / 8GB | Example |
| Instance price | 0.80 CNY/hour | Example |
| Render instance hours per video | 0.10 hours | Measured or estimated |
| Render cost | 0.08 CNY | 0.10 × 0.80 |
| Storage and traffic | 0.05 CNY | Example |
| Asset license amortization | 0.10 CNY | Amortized over the license period |
| Template development amortization | 0.05 CNY | Template cost ÷ expected number of videos |
| Total cost per video | 0.28 CNY | Example only |
Actual cost should be replaced with your own render time, instance price, asset licensing, and template amortization.
Common Errors and Troubleshooting Checklist
| Symptom | What to Check First |
|---|---|
| Black or blank screen | Check the Composition id and whether the component is registered. Use npx remotion compositions and --log=verbose to inspect errors. |
| Chinese or CJK glyphs become boxes | Check whether the font is explicitly loaded. Run fc-list :lang=zh. Confirm the family, url, weight, and style values in loadFont. |
| Missing assets or 404 errors | Check whether staticFile() paths match the relative location inside public/. Watch out for case-sensitive filenames. |
| Render timeout or OOM | Reduce --concurrency and --scale. Render in frame segments and stitch them together. Check container memory limits. |
| Audio/video out of sync | Check whether the audio and video track frame counts match. Confirm that the frame rate was not changed during rendering. |
| File too large | Increase --crf, reduce resolution or frame rate. Check platform upload limits. |
| Container fails to start | Check glibc and graphics libraries. Confirm that Chromium --no-sandbox is effective. |
| Animation jitter | Check whether progress is clamped. Confirm that interpolation uses extrapolateLeft and extrapolateRight. |
| CTA missing or not animating | Check whether it is inside a Sequence. Confirm whether useCurrentFrame returns relative or absolute frames. |
| Intermittent CI failures | Check whether the cache directory is persisted. Check memory and concurrency settings. |
Common troubleshooting commands:
npm ls remotion @remotion/clifc-list :lang=zhnpx remotion still product-intro out/frame-90.png --frame=90npx remotion render product-intro out/preview.mp4 --frames=0-60 --log=verbose
Compliance and Best Practices
Low cost does not mean exemption from licensing. Once programmatic videos are produced in batches, asset compliance issues are amplified.
Key checks include:
- Font licensing: Confirm whether commercial use, redistribution, and embedding for rendering are allowed.
- Icons and images: Confirm the license scope for each asset type and keep license records.
- Music and sound effects: Confirm commercial platform licensing and regional restrictions.
- Portrait rights: Real-person likenesses require authorization.
- Trademarks and logos: Avoid implying endorsement or using them beyond the allowed scope.
- Data sources: Use of marketing data and user data must comply with applicable regulations.
- AI-generated content labeling: Add labels as required by the relevant platform and region.
- Fact-checking: AI-generated copy still requires human review to avoid absolute claims and false advertising.
Engineering best practices:
- Build templates first and move variables into the data layer.
- Render according to delivery specifications rather than always using the highest specification.
- Put templates, data, and font lists under version control.
- Orchestrate generation, rendering, upload, and distribution as a pipeline.
- Containerize rendering nodes to obtain consistent environments and elastic compute capacity.
Conclusion
Programmatic video moves the creation entry point from a graphical interface to code. AI coding assistants move it further toward natural language. Together, they make structured marketing videos template-driven and batch-ready, shifting cost from manual labor hours to more predictable compute expenditure.
What is most worth accumulating is not one component, but this mindset: frame-driven rendering, data-driven templates, and automated pipelines. This mindset determines whether marketing assets can move from handcrafted artifacts to reproducible production capacity.
