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:

    1. Is the video mainly composed of titles, cards, numbers, buttons, and logos?
    2. Can many versions be generated by replacing copy, images, languages, or color themes?
    3. 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:

    1. Get the data: Read the content from JSON, spreadsheets, databases, or APIs.
    2. Write the components: Define layouts and animations with declarative components, using the current frame number as input.
    3. 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:

    1. function IntroFrame({ frame, fps }) {
    2. const progress = Math.min(1, frame / (fps * 2));
    3. const opacity = progress;
    4. const offsetX = (1 - progress) * 100;
    5. return (
    6. <Layout>
    7. <Title
    8. text="Product Introduction"
    9. style={{ opacity, transform: `translateX(${offsetX}px)` }}
    10. />
    11. </Layout>
    12. );
    13. }

    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:

    1. 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. # 1. Runtime: Node.js 18 or newer; Node.js 20 LTS is recommended
    2. node -v && npm -v
    3. # 2. Create the project
    4. npx create-video@latest my-product-video
    5. cd my-product-video
    6. npm install
    7. # 3. Check versions and dependencies
    8. npm ls remotion @remotion/cli
    9. npx remotion versions # Available in Remotion 4.x; otherwise use the previous command
    10. npx remotion browser ensure # Ensure headless Chromium is available
    11. npx remotion compositions # List registered compositions and verify that the project can be parsed
    12. npx 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 -version works 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, and fonts-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:

    1. import { loadFont, staticFile } from "remotion";
    2. export const FONT_FAMILY = "NotoSansSC";
    3. // Regular weight
    4. loadFont({
    5. family: FONT_FAMILY,
    6. url: staticFile("fonts/NotoSansSC-Regular.woff2"),
    7. weight: "400",
    8. style: "normal",
    9. });
    10. // Bold weight
    11. loadFont({
    12. family: FONT_FAMILY,
    13. url: staticFile("fonts/NotoSansSC-Bold.woff2"),
    14. weight: "700",
    15. style: "normal",
    16. });

    Use the font inside a component:

    1. const titleStyle = {
    2. fontFamily: FONT_FAMILY,
    3. fontWeight: 700,
    4. } as const;
    5. <h1 style={titleStyle}>Quarterly Launch</h1>

    A useful self-check command is:

    1. 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.

    1. product-video/
    2. ├─ src/
    3. │ ├─ Root.tsx # Register compositions
    4. │ ├─ ProductIntro.tsx # Main component and scenes
    5. │ ├─ font.ts # Font loading
    6. │ ├─ components/ # Title / Card / CtaButton
    7. │ └─ theme.ts # Colors, font sizes, safe areas
    8. ├─ public/
    9. │ ├─ fonts/ # Chinese font files; verify licensing
    10. │ └─ assets/ # Images and logos
    11. ├─ data/
    12. │ └─ campaign.json # Data layer
    13. └─ package.json

    Example data in data/campaign.json:

    1. {
    2. "title": "Quarterly Launch",
    3. "cards": [
    4. { "icon": "bolt", "headline": "Fast Output", "desc": "Template-based generation" },
    5. { "icon": "globe", "headline": "Multilingual", "desc": "Only data changes" },
    6. { "icon": "shield", "headline": "Auditable", "desc": "Reproducible rendering results" }
    7. ],
    8. "cta": { "text": "Try Now", "url": "https://example.com" }
    9. }

    The field mapping rules should be documented in the team template guide:

    • title maps to the title component.
    • cards[i] maps to the i-th card. Each card enters with a default interval of 15 frames.
    • cta maps 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:

    1. import { Composition } from "remotion";
    2. import { ProductIntro, FPS, DURATION } from "./ProductIntro";
    3. export const RemotionRoot = () => (
    4. <Composition
    5. id="product-intro"
    6. component={ProductIntro}
    7. durationInFrames={DURATION}
    8. fps={FPS}
    9. width={1080}
    10. height={1920}
    11. defaultProps={{
    12. title: "Quarterly Launch",
    13. cards: [],
    14. cta: { text: "Try Now", url: "" },
    15. }}
    16. />
    17. );

    2. Main Component

    src/ProductIntro.tsx:

    1. import {
    2. AbsoluteFill,
    3. Sequence,
    4. useCurrentFrame,
    5. useVideoConfig,
    6. interpolate,
    7. spring,
    8. } from "remotion";
    9. import { FONT_FAMILY } from "./font";
    10. export const FPS = 30;
    11. export const VIDEO_SECONDS = 8;
    12. export const DURATION = FPS * VIDEO_SECONDS;
    13. type Card = {
    14. icon?: string;
    15. headline: string;
    16. desc: string;
    17. };
    18. export type Props = {
    19. title: string;
    20. cards: Card[];
    21. cta: { text: string; url: string };
    22. };
    23. const Title = ({ text }: { text: string }) => {
    24. const frame = useCurrentFrame();
    25. const { fps } = useVideoConfig();
    26. const opacity = interpolate(
    27. frame,
    28. [0, fps * 0.6],
    29. [0, 1],
    30. { extrapolateRight: "clamp" }
    31. );
    32. const x = interpolate(
    33. frame,
    34. [0, fps * 0.6],
    35. [60, 0],
    36. { extrapolateRight: "clamp" }
    37. );
    38. return (
    39. <h1
    40. style={{
    41. opacity,
    42. transform: `translateX(${x}px)`,
    43. fontFamily: FONT_FAMILY,
    44. fontSize: 88,
    45. color: "#fff",
    46. }}
    47. >
    48. {text}
    49. </h1>
    50. );
    51. };
    52. const CardItem = ({ card, index }: { card: Card; index: number }) => {
    53. const frame = useCurrentFrame();
    54. const { fps } = useVideoConfig();
    55. // Inside a Sequence, frames are relative by default, so there is no need to subtract `from`.
    56. const local = frame - index * 15;
    57. const x = interpolate(
    58. local,
    59. [0, fps * 0.5],
    60. [-240, 0],
    61. {
    62. extrapolateLeft: "clamp",
    63. extrapolateRight: "clamp",
    64. }
    65. );
    66. const opacity = interpolate(
    67. local,
    68. [0, fps * 0.4],
    69. [0, 1],
    70. {
    71. extrapolateLeft: "clamp",
    72. extrapolateRight: "clamp",
    73. }
    74. );
    75. return (
    76. <div
    77. style={{
    78. opacity,
    79. transform: `translateX(${x}px)`,
    80. width: 880,
    81. padding: 32,
    82. background: "#1f2937",
    83. borderRadius: 24,
    84. fontFamily: FONT_FAMILY,
    85. }}
    86. >
    87. <div style={{ fontSize: 48, color: "#fff", fontWeight: 700 }}>
    88. {card.headline}
    89. </div>
    90. <div style={{ fontSize: 32, color: "#9ca3af" }}>{card.desc}</div>
    91. </div>
    92. );
    93. };
    94. const Cta = ({ text }: { text: string }) => {
    95. const frame = useCurrentFrame();
    96. const { fps } = useVideoConfig();
    97. // The Cta component is inside a Sequence. `useCurrentFrame` is relative by default,
    98. // so calling `spring({ frame, fps })` directly is sufficient.
    99. const scale = spring({
    100. frame,
    101. fps,
    102. config: { damping: 200 },
    103. });
    104. return (
    105. <button
    106. style={{
    107. transform: `scale(${scale})`,
    108. fontFamily: FONT_FAMILY,
    109. fontSize: 52,
    110. padding: "24px 64px",
    111. borderRadius: 999,
    112. }}
    113. >
    114. {text}
    115. </button>
    116. );
    117. };
    118. export const ProductIntro: React.FC<Props> = ({ title, cards, cta }) => {
    119. return (
    120. <AbsoluteFill
    121. style={{
    122. background: "#0b1020",
    123. justifyContent: "center",
    124. alignItems: "center",
    125. gap: 48,
    126. }}
    127. >
    128. <Sequence durationInFrames={FPS * 2}>
    129. <Title text={title} />
    130. </Sequence>
    131. <Sequence from={FPS * 3} durationInFrames={FPS * 3}>
    132. <AbsoluteFill
    133. style={{
    134. justifyContent: "center",
    135. alignItems: "center",
    136. gap: 32,
    137. }}
    138. >
    139. {cards.map((card, index) => (
    140. <CardItem key={card.headline} card={card} index={index} />
    141. ))}
    142. </AbsoluteFill>
    143. </Sequence>
    144. <Sequence from={FPS * 6} durationInFrames={FPS * 2}>
    145. <AbsoluteFill
    146. style={{
    147. justifyContent: "flex-end",
    148. alignItems: "center",
    149. paddingBottom: 200,
    150. }}
    151. >
    152. <Cta text={cta.text} />
    153. </AbsoluteFill>
    154. </Sequence>
    155. </AbsoluteFill>
    156. );
    157. };

    If you truly need absolute frames, you can use:

    1. 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

    1. # Render the full video with JSON data
    2. npx remotion render product-intro out/intro-en.mp4 \
    3. --props=./data/campaign.json \
    4. --codec=h264 \
    5. --crf=20 \
    6. --concurrency=4 \
    7. --log=verbose
    8. # Render only the first 2 seconds for a quick check
    9. npx remotion render product-intro out/preview.mp4 \
    10. --frames=0-60
    11. # Render a single frame to inspect layout and fonts
    12. npx remotion still product-intro out/frame-90.png \
    13. --frame=90

    Parameter recommendations:

    • A lower --crf value means higher quality. The common range is 18 to 23.
    • --concurrency increases 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:

    1. Have the AI assistant first produce a storyboard.
    2. Manually confirm the time ranges, elements, and animation types.
    3. Generate the component code.
    4. Use single-frame rendering to verify the layout.
    5. Use short-clip rendering to verify the animation.
    6. 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:

    1. Cost per video =
    2. render instance hours × instance price
    3. + storage and traffic cost
    4. + asset license amortization
    5. + 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:

    1. npm ls remotion @remotion/cli
    2. fc-list :lang=zh
    3. npx remotion still product-intro out/frame-90.png --frame=90
    4. npx 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.