├── .env.example
├── .gitignore
├── .npmrc
├── .prettierrc
├── README.md
├── config.mjs
├── deploy.mjs
├── eslint.config.mjs
├── next.config.js
├── package.json
├── public
├── favicon.ico
└── vercel.svg
├── remotion.config.ts
├── src
├── components
│ ├── AlignEnd.tsx
│ ├── Button
│ │ ├── Button.tsx
│ │ └── styles.module.css
│ ├── Container.tsx
│ ├── DownloadButton.tsx
│ ├── Error.tsx
│ ├── Input.tsx
│ ├── ProgressBar.tsx
│ ├── RenderControls.tsx
│ ├── Spacing.tsx
│ ├── Spinner
│ │ ├── Spinner.tsx
│ │ └── styles.module.css
│ └── Tips
│ │ ├── Tips.tsx
│ │ └── styles.module.css
├── helpers
│ ├── api-response.ts
│ └── use-rendering.ts
├── lambda
│ └── api.ts
├── pages
│ ├── _app.tsx
│ ├── api
│ │ └── lambda
│ │ │ ├── progress.ts
│ │ │ └── render.ts
│ └── index.tsx
└── remotion
│ ├── MyComp
│ ├── Main.tsx
│ ├── NextLogo.tsx
│ ├── Rings.tsx
│ └── TextFade.tsx
│ ├── Root.tsx
│ └── index.ts
├── styles
└── global.css
├── tsconfig.json
├── types
├── constants.ts
└── schema.ts
└── vercel.json
/.env.example:
--------------------------------------------------------------------------------
1 | REMOTION_AWS_ACCESS_KEY_ID=""
2 | REMOTION_AWS_SECRET_ACCESS_KEY=""
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 | out/
38 | .env
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | auto-install-peers=true
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": false,
3 | "bracketSpacing": true,
4 | "tabWidth": 2
5 | }
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | This is a Next.js template for building programmatic video apps, with [`@remotion/player`](https://remotion.dev/player) and [`@remotion/lambda`](https://remotion.dev/lambda) built in.
6 |
7 | This template uses the Next.js Pages directory. There is a [App directory version](https://github.com/remotion-dev/template-next-app-dir) of this template available.
8 |
9 |
10 |
11 | ## Getting Started
12 |
13 | [Use this template](https://github.com/new?template_name=template-next-pages-dir&template_owner=remotion-dev) to clone it into your GitHub account. Run
14 |
15 | ```
16 | npm i
17 | ```
18 |
19 | afterwards. Alternatively, use this command to scaffold a project:
20 |
21 | ```
22 | npx create-video@latest --next-pages-dir
23 | ```
24 |
25 | ## Commands
26 |
27 | Start the Next.js dev server:
28 |
29 | ```
30 | npm run dev
31 | ```
32 |
33 | Open the Remotion Studio:
34 |
35 | ```
36 | npx remotion studio
37 | ```
38 |
39 | Render a video locally:
40 |
41 | ```
42 | npx remotion render
43 | ```
44 |
45 | Upgrade Remotion:
46 |
47 | ```
48 | npx remotion upgrade
49 | ```
50 |
51 | The following script will set up your Remotion Bundle and Lambda function on AWS:
52 |
53 | ```
54 | node deploy.mjs
55 | ```
56 |
57 | You should run this script after:
58 |
59 | - changing the video template
60 | - changing `config.mjs`
61 | - upgrading Remotion to a newer version
62 |
63 | ## Set up rendering on AWS Lambda
64 |
65 | This template supports rendering the videos via [Remotion Lambda](https://remotion.dev/lambda).
66 |
67 | 1. Copy the `.env.example` file to `.env` and fill in the values.
68 | Complete the [Lambda setup guide](https://www.remotion.dev/docs/lambda/setup) to get your AWS credentials.
69 |
70 | 1. Edit the `config.mjs` file to your desired Lambda settings.
71 |
72 | 1. Run `node deploy.mjs` to deploy your Lambda function and Remotion Bundle.
73 |
74 | ## Docs
75 |
76 | Get started with Remotion by reading the [fundamentals page](https://www.remotion.dev/docs/the-fundamentals).
77 |
78 | ## Help
79 |
80 | We provide help on our [Discord server](https://remotion.dev/discord).
81 |
82 | ## Issues
83 |
84 | Found an issue with Remotion? [File an issue here](https://remotion.dev/issue).
85 |
86 | ## License
87 |
88 | Note that for some entities a company license is needed. [Read the terms here](https://github.com/remotion-dev/remotion/blob/main/LICENSE.md).
89 |
--------------------------------------------------------------------------------
/config.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * Use autocomplete to get a list of available regions.
3 | * @type {import('@remotion/lambda').AwsRegion}
4 | */
5 | export const REGION = "us-east-1";
6 |
7 | export const SITE_NAME = "my-next-app";
8 | export const RAM = 3009;
9 | export const DISK = 10240;
10 | export const TIMEOUT = 240;
11 |
--------------------------------------------------------------------------------
/deploy.mjs:
--------------------------------------------------------------------------------
1 | import {
2 | deployFunction,
3 | deploySite,
4 | getOrCreateBucket,
5 | } from "@remotion/lambda";
6 | import dotenv from "dotenv";
7 | import path from "path";
8 | import { DISK, RAM, REGION, SITE_NAME, TIMEOUT } from "./config.mjs";
9 |
10 | console.log("Selected region:", REGION);
11 | dotenv.config();
12 |
13 | if (!process.env.AWS_ACCESS_KEY_ID && !process.env.REMOTION_AWS_ACCESS_KEY_ID) {
14 | console.log(
15 | 'The environment variable "REMOTION_AWS_ACCESS_KEY_ID" is not set.',
16 | );
17 | console.log("Lambda renders were not set up.");
18 | console.log(
19 | "Complete the Lambda setup: at https://www.remotion.dev/docs/lambda/setup",
20 | );
21 | process.exit(0);
22 | }
23 | if (
24 | !process.env.AWS_SECRET_ACCESS_KEY &&
25 | !process.env.REMOTION_AWS_SECRET_ACCESS_KEY
26 | ) {
27 | console.log(
28 | 'The environment variable "REMOTION_REMOTION_AWS_SECRET_ACCESS_KEY" is not set.',
29 | );
30 | console.log("Lambda renders were not set up.");
31 | console.log(
32 | "Complete the Lambda setup: at https://www.remotion.dev/docs/lambda/setup",
33 | );
34 | process.exit(0);
35 | }
36 |
37 | process.stdout.write("Deploying Lambda function... ");
38 |
39 | const { functionName, alreadyExisted: functionAlreadyExisted } =
40 | await deployFunction({
41 | createCloudWatchLogGroup: true,
42 | memorySizeInMb: RAM,
43 | region: REGION,
44 | timeoutInSeconds: TIMEOUT,
45 | diskSizeInMb: DISK,
46 | });
47 | console.log(
48 | functionName,
49 | functionAlreadyExisted ? "(already existed)" : "(created)",
50 | );
51 |
52 | process.stdout.write("Ensuring bucket... ");
53 | const { bucketName, alreadyExisted: bucketAlreadyExisted } =
54 | await getOrCreateBucket({
55 | region: REGION,
56 | });
57 | console.log(
58 | bucketName,
59 | bucketAlreadyExisted ? "(already existed)" : "(created)",
60 | );
61 |
62 | process.stdout.write("Deploying site... ");
63 | const { siteName } = await deploySite({
64 | bucketName,
65 | entryPoint: path.join(process.cwd(), "src", "remotion", "index.ts"),
66 | siteName: SITE_NAME,
67 | region: REGION,
68 | });
69 |
70 | console.log(siteName);
71 |
72 | console.log();
73 | console.log("You now have everything you need to render videos!");
74 | console.log("Re-run this command when:");
75 | console.log(" 1) you changed the video template");
76 | console.log(" 2) you changed config.mjs");
77 | console.log(" 3) you upgraded Remotion to a newer version");
78 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 | import remotion from "@remotion/eslint-plugin";
5 |
6 | const __filename = fileURLToPath(import.meta.url);
7 | const __dirname = dirname(__filename);
8 |
9 | const compat = new FlatCompat({
10 | baseDirectory: __dirname,
11 | });
12 |
13 | const [nextPlugin] = compat.extends("plugin:@next/next/recommended");
14 |
15 | const eslintConfig = [
16 | ...compat.extends("next/typescript"),
17 | nextPlugin,
18 | {
19 | ...remotion.flatPlugin,
20 | rules: {
21 | ...remotion.flatPlugin.rules,
22 | ...Object.entries(nextPlugin.rules).reduce((acc, [key]) => {
23 | return { ...acc, [key]: "off" };
24 | }, {}),
25 | },
26 | files: ["src/remotion/**"],
27 | },
28 | ];
29 |
30 | export default eslintConfig;
31 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | };
5 |
6 | module.exports = nextConfig;
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "template-next-pages",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "eslint .",
10 | "remotion": "remotion studio",
11 | "render": "remotion render",
12 | "deploy": "node deploy.mjs"
13 | },
14 | "dependencies": {
15 | "@remotion/cli": "^4.0.0",
16 | "@remotion/google-fonts": "^4.0.0",
17 | "@remotion/bundler": "^4.0.0",
18 | "@remotion/lambda": "^4.0.0",
19 | "@remotion/paths": "^4.0.0",
20 | "@remotion/player": "^4.0.0",
21 | "@remotion/shapes": "^4.0.0",
22 | "next": "15.2.4",
23 | "react": "19.0.0",
24 | "react-dom": "19.0.0",
25 | "remotion": "^4.0.0",
26 | "zod": "3.22.3"
27 | },
28 | "devDependencies": {
29 | "dotenv": "16.0.3",
30 | "@remotion/eslint-plugin": "^4.0.0",
31 | "@eslint/eslintrc": "3.1.0",
32 | "@types/node": "20.12.14",
33 | "@types/react": "19.0.0",
34 | "@types/react-dom": "19.0.0",
35 | "@types/web": "0.0.166",
36 | "eslint": "9.19.0",
37 | "@next/eslint-plugin-next": "15.1.6",
38 | "eslint-config-next": "15.1.6",
39 | "typescript": "5.8.2",
40 | "prettier": "3.3.3"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remotion-dev/template-next-pages-dir/8d3f0e22ba620c6bb20c5fc5aed07ce7653c07a1/public/favicon.ico
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/remotion.config.ts:
--------------------------------------------------------------------------------
1 | // See all configuration options: https://remotion.dev/docs/config
2 | // Each option also is available as a CLI flag: https://remotion.dev/docs/cli
3 |
4 | // Note: When using the Node.JS APIs, the config file doesn't apply. Instead, pass options directly to the APIs
5 |
6 | import { Config } from "@remotion/cli/config";
7 |
8 | Config.setVideoImageFormat("jpeg");
9 |
--------------------------------------------------------------------------------
/src/components/AlignEnd.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const container: React.CSSProperties = {
4 | alignSelf: "flex-end",
5 | };
6 |
7 | export const AlignEnd: React.FC<{
8 | children: React.ReactNode;
9 | }> = ({ children }) => {
10 | return
{children}
;
11 | };
12 |
--------------------------------------------------------------------------------
/src/components/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from "react";
2 | import { Spacing } from "../Spacing";
3 | import { Spinner } from "../Spinner/Spinner";
4 | import styles from "./styles.module.css";
5 |
6 | const ButtonForward: React.ForwardRefRenderFunction<
7 | HTMLButtonElement,
8 | {
9 | onClick?: () => void;
10 | disabled?: boolean;
11 | children: React.ReactNode;
12 | loading?: boolean;
13 | secondary?: boolean;
14 | }
15 | > = ({ onClick, disabled, children, loading, secondary }, ref) => {
16 | return (
17 |
34 | );
35 | };
36 |
37 | export const Button = forwardRef(ButtonForward);
38 |
--------------------------------------------------------------------------------
/src/components/Button/styles.module.css:
--------------------------------------------------------------------------------
1 | .button {
2 | border: 1px solid var(--foreground);
3 | border-radius: var(--geist-border-radius);
4 | background-color: var(--foreground);
5 | color: var(--background);
6 | padding-left: var(--geist-half-pad);
7 | padding-right: var(--geist-half-pad);
8 | height: 40px;
9 | font-family: var(--geist-font);
10 | font-weight: 500;
11 | transition: all 0.15s ease;
12 | display: inline-flex;
13 | flex-direction: row;
14 | align-items: center;
15 | appearance: none;
16 | font-size: 14px;
17 | }
18 |
19 | .secondarybutton {
20 | background-color: var(--background);
21 | color: var(--foreground);
22 | border-color: var(--unfocused-border-color);
23 | }
24 |
25 | .button:hover {
26 | background-color: var(--background);
27 | cursor: pointer;
28 | color: var(--foreground);
29 | border-color: var(--focused-border-color);
30 | }
31 |
32 | .button:disabled {
33 | background-color: var(--button-disabled-color);
34 | color: var(--disabled-text-color);
35 | border-color: var(--unfocused-border-color);
36 | cursor: not-allowed;
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/Container.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const inputContainer: React.CSSProperties = {
4 | border: "1px solid var(--unfocused-border-color)",
5 | padding: "var(--geist-pad)",
6 | borderRadius: "var(--geist-border-radius)",
7 | backgroundColor: "var(--background)",
8 | display: "flex",
9 | flexDirection: "column",
10 | };
11 |
12 | export const InputContainer: React.FC<{
13 | children: React.ReactNode;
14 | }> = ({ children }) => {
15 | return {children}
;
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/DownloadButton.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import React from "react";
3 | import { State } from "../helpers/use-rendering";
4 | import { Button } from "./Button/Button";
5 | import { Spacing } from "./Spacing";
6 |
7 | const light: React.CSSProperties = {
8 | opacity: 0.6,
9 | };
10 |
11 | const link: React.CSSProperties = {
12 | textDecoration: "none",
13 | };
14 |
15 | const row: React.CSSProperties = {
16 | display: "flex",
17 | flexDirection: "row",
18 | };
19 |
20 | const Megabytes: React.FC<{
21 | sizeInBytes: number;
22 | }> = ({ sizeInBytes }) => {
23 | const megabytes = Intl.NumberFormat("en", {
24 | notation: "compact",
25 | style: "unit",
26 | unit: "byte",
27 | unitDisplay: "narrow",
28 | }).format(sizeInBytes);
29 | return {megabytes};
30 | };
31 |
32 | export const DownloadButton: React.FC<{
33 | state: State;
34 | undo: () => void;
35 | }> = ({ state, undo }) => {
36 | if (state.status === "rendering") {
37 | return ;
38 | }
39 |
40 | if (state.status !== "done") {
41 | throw new Error("Download button should not be rendered when not done");
42 | }
43 |
44 | return (
45 |
46 |
49 |
50 |
51 |
56 |
57 |
58 | );
59 | };
60 |
61 | const UndoIcon: React.FC = () => {
62 | return (
63 |
69 | );
70 | };
71 |
--------------------------------------------------------------------------------
/src/components/Error.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const container: React.CSSProperties = {
4 | color: "var(--geist-error)",
5 | fontFamily: "var(--geist-font)",
6 | paddingTop: "var(--geist-half-pad)",
7 | paddingBottom: "var(--geist-half-pad)",
8 | };
9 |
10 | const icon: React.CSSProperties = {
11 | height: 20,
12 | verticalAlign: "text-bottom",
13 | marginRight: 6,
14 | };
15 |
16 | export const ErrorComp: React.FC<{
17 | message: string;
18 | }> = ({ message }) => {
19 | return (
20 |
21 |
35 |
Error: {message}
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/Input.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from "react";
2 |
3 | const textarea: React.CSSProperties = {
4 | resize: "none",
5 | lineHeight: 1.7,
6 | display: "block",
7 | width: "100%",
8 | borderRadius: "var(--geist-border-radius)",
9 | backgroundColor: "var(--background)",
10 | padding: "var(--geist-half-pad)",
11 | color: "var(--foreground)",
12 | fontSize: 14,
13 | };
14 |
15 | export const Input: React.FC<{
16 | text: string;
17 | setText: React.Dispatch>;
18 | disabled?: boolean;
19 | }> = ({ text, setText, disabled }) => {
20 | const onChange: React.ChangeEventHandler = useCallback(
21 | (e) => {
22 | setText(e.currentTarget.value);
23 | },
24 | [setText],
25 | );
26 |
27 | return (
28 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/src/components/ProgressBar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 |
3 | export const ProgressBar: React.FC<{
4 | progress: number;
5 | }> = ({ progress }) => {
6 | const style: React.CSSProperties = useMemo(() => {
7 | return {
8 | width: "100%",
9 | height: 10,
10 | borderRadius: 5,
11 | appearance: "none",
12 | backgroundColor: "var(--unfocused-border-color)",
13 | marginTop: 10,
14 | marginBottom: 25,
15 | };
16 | }, []);
17 |
18 | const fill: React.CSSProperties = useMemo(() => {
19 | return {
20 | backgroundColor: "var(--foreground)",
21 | height: 10,
22 | borderRadius: 5,
23 | transition: "width 0.1s ease-in-out",
24 | width: `${progress * 100}%`,
25 | };
26 | }, [progress]);
27 |
28 | return (
29 |
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/src/components/RenderControls.tsx:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { useRendering } from "../helpers/use-rendering";
3 | import { AlignEnd } from "./AlignEnd";
4 | import { Button } from "./Button/Button";
5 | import { InputContainer } from "./Container";
6 | import { DownloadButton } from "./DownloadButton";
7 | import { ErrorComp } from "./Error";
8 | import { Input } from "./Input";
9 | import { ProgressBar } from "./ProgressBar";
10 | import { Spacing } from "./Spacing";
11 | import { COMP_NAME, CompositionProps } from "../../types/constants";
12 |
13 | export const RenderControls: React.FC<{
14 | text: string;
15 | setText: React.Dispatch>;
16 | inputProps: z.infer;
17 | }> = ({ text, setText, inputProps }) => {
18 | const { renderMedia, state, undo } = useRendering(COMP_NAME, inputProps);
19 |
20 | return (
21 |
22 | {state.status === "init" ||
23 | state.status === "invoking" ||
24 | state.status === "error" ? (
25 | <>
26 |
31 |
32 |
33 |
40 |
41 | {state.status === "error" ? (
42 |
43 | ) : null}
44 | >
45 | ) : null}
46 | {state.status === "rendering" || state.status === "done" ? (
47 | <>
48 |
51 |
52 |
53 |
54 |
55 | >
56 | ) : null}
57 |
58 | );
59 | };
60 |
--------------------------------------------------------------------------------
/src/components/Spacing.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const Spacing: React.FC = () => {
4 | return (
5 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/Spinner/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 | import { makeRect } from "@remotion/shapes";
3 | import { translatePath } from "@remotion/paths";
4 | import styles from "./styles.module.css";
5 |
6 | const viewBox = 100;
7 | const lines = 12;
8 | const width = viewBox * 0.08;
9 |
10 | const { path } = makeRect({
11 | height: viewBox * 0.24,
12 | width,
13 | cornerRadius: width / 2,
14 | });
15 |
16 | const translated = translatePath(path, viewBox / 2 - width / 2, viewBox * 0.03);
17 |
18 | export const Spinner: React.FC<{
19 | size: number;
20 | }> = ({ size }) => {
21 | const style = useMemo(() => {
22 | return {
23 | width: size,
24 | height: size,
25 | };
26 | }, [size]);
27 |
28 | return (
29 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/src/components/Spinner/styles.module.css:
--------------------------------------------------------------------------------
1 | @keyframes spinner {
2 | 0% {
3 | opacity: 1;
4 | }
5 | 100% {
6 | opacity: 0.15;
7 | }
8 | }
9 |
10 | .line {
11 | animation: spinner 1.2s linear infinite;
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/Tips/Tips.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "./styles.module.css";
3 |
4 | const titlerow: React.CSSProperties = {
5 | display: "flex",
6 | flexDirection: "row",
7 | alignItems: "center",
8 | justifyContent: "flex-start",
9 | };
10 |
11 | const titlestyle: React.CSSProperties = {
12 | marginBottom: "0.75em",
13 | marginTop: "0.75em",
14 | color: "var(--foreground)",
15 | };
16 |
17 | const a: React.CSSProperties = {
18 | textDecoration: "none",
19 | color: "inherit",
20 | flex: 1,
21 | };
22 |
23 | const Tip: React.FC<{
24 | title: React.ReactNode;
25 | description: React.ReactNode;
26 | href: string;
27 | }> = ({ title, description, href }) => {
28 | return (
29 |
30 |
31 |
32 |
{title}
33 |
34 |
40 |
41 |
{description}
42 |
43 |
44 | );
45 | };
46 |
47 | export const Tips: React.FC = () => {
48 | return (
49 |
50 |
55 |
60 |
65 |
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/src/components/Tips/styles.module.css:
--------------------------------------------------------------------------------
1 | .row {
2 | display: flex;
3 | flex-direction: row;
4 | font-family: Inter;
5 | }
6 |
7 | .item {
8 | cursor: pointer;
9 | transition: transform 0.2s ease-in-out;
10 | padding: 10px;
11 | }
12 |
13 | .icon {
14 | opacity: 0;
15 | transition: opacity 0.2s ease-in-out;
16 | }
17 |
18 | .item:hover {
19 | transform: translateY(-2px);
20 | .icon {
21 | opacity: 1;
22 | }
23 | }
24 |
25 | .p {
26 | font-size: 14px;
27 | margin-top: 0;
28 | line-height: 1.5;
29 | color: var(--subtitle);
30 | }
31 |
32 | .flex {
33 | flex: 1;
34 | }
35 |
36 | @media (max-width: 768px) {
37 | .row {
38 | flex-direction: column;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/helpers/api-response.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 | import { z, ZodType } from "zod";
3 |
4 | export type ApiResponse =
5 | | {
6 | type: "error";
7 | message: string;
8 | }
9 | | {
10 | type: "success";
11 | data: Res;
12 | };
13 |
14 | export const executeApi =
15 | (
16 | schema: Req,
17 | handler: (
18 | req: NextApiRequest,
19 | body: z.infer,
20 | res: NextApiResponse>,
21 | ) => Promise,
22 | ) =>
23 | async (req: NextApiRequest, res: NextApiResponse>) => {
24 | try {
25 | const parsed = schema.parse(req.body);
26 | const data = await handler(req, parsed, res);
27 | res.status(200).json({
28 | type: "success",
29 | data: data,
30 | });
31 | } catch (err) {
32 | res.status(500).json({ type: "error", message: (err as Error).message });
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/src/helpers/use-rendering.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { useCallback, useMemo, useState } from "react";
3 | import { getProgress, renderVideo } from "../lambda/api";
4 | import { CompositionProps } from "../../types/constants";
5 |
6 | export type State =
7 | | {
8 | status: "init";
9 | }
10 | | {
11 | status: "invoking";
12 | }
13 | | {
14 | renderId: string;
15 | bucketName: string;
16 | progress: number;
17 | status: "rendering";
18 | }
19 | | {
20 | renderId: string | null;
21 | status: "error";
22 | error: Error;
23 | }
24 | | {
25 | url: string;
26 | size: number;
27 | status: "done";
28 | };
29 |
30 | const wait = async (milliSeconds: number) => {
31 | await new Promise((resolve) => {
32 | setTimeout(() => {
33 | resolve();
34 | }, milliSeconds);
35 | });
36 | };
37 |
38 | export const useRendering = (
39 | id: string,
40 | inputProps: z.infer,
41 | ) => {
42 | const [state, setState] = useState({
43 | status: "init",
44 | });
45 |
46 | const renderMedia = useCallback(async () => {
47 | setState({
48 | status: "invoking",
49 | });
50 | try {
51 | const { renderId, bucketName } = await renderVideo({ id, inputProps });
52 | setState({
53 | status: "rendering",
54 | progress: 0,
55 | renderId: renderId,
56 | bucketName: bucketName,
57 | });
58 |
59 | let pending = true;
60 |
61 | while (pending) {
62 | const result = await getProgress({
63 | id: renderId,
64 | bucketName: bucketName,
65 | });
66 | switch (result.type) {
67 | case "error": {
68 | setState({
69 | status: "error",
70 | renderId: renderId,
71 | error: new Error(result.message),
72 | });
73 | pending = false;
74 | break;
75 | }
76 | case "done": {
77 | setState({
78 | size: result.size,
79 | url: result.url,
80 | status: "done",
81 | });
82 | pending = false;
83 | break;
84 | }
85 | case "progress": {
86 | setState({
87 | status: "rendering",
88 | bucketName: bucketName,
89 | progress: result.progress,
90 | renderId: renderId,
91 | });
92 | await wait(1000);
93 | }
94 | }
95 | }
96 | } catch (err) {
97 | setState({
98 | status: "error",
99 | error: err as Error,
100 | renderId: null,
101 | });
102 | }
103 | }, [id, inputProps]);
104 |
105 | const undo = useCallback(() => {
106 | setState({ status: "init" });
107 | }, []);
108 |
109 | return useMemo(() => {
110 | return {
111 | renderMedia,
112 | state,
113 | undo,
114 | };
115 | }, [renderMedia, state, undo]);
116 | };
117 |
--------------------------------------------------------------------------------
/src/lambda/api.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import type { RenderMediaOnLambdaOutput } from "@remotion/lambda/client";
3 | import { ApiResponse } from "../helpers/api-response";
4 | import { CompositionProps } from "../../types/constants";
5 | import {
6 | RenderRequest,
7 | ProgressRequest,
8 | ProgressResponse,
9 | } from "../../types/schema";
10 |
11 | const makeRequest = async (
12 | endpoint: string,
13 | body: unknown,
14 | ): Promise => {
15 | const result = await fetch(endpoint, {
16 | method: "post",
17 | body: JSON.stringify(body),
18 | headers: {
19 | "content-type": "application/json",
20 | },
21 | });
22 | const json = (await result.json()) as ApiResponse;
23 | if (json.type === "error") {
24 | throw new Error(json.message);
25 | }
26 |
27 | return json.data;
28 | };
29 |
30 | export const renderVideo = async ({
31 | id,
32 | inputProps,
33 | }: {
34 | id: string;
35 | inputProps: z.infer;
36 | }) => {
37 | const body: z.infer = {
38 | id,
39 | inputProps,
40 | };
41 |
42 | return makeRequest("/api/lambda/render", body);
43 | };
44 |
45 | export const getProgress = async ({
46 | id,
47 | bucketName,
48 | }: {
49 | id: string;
50 | bucketName: string;
51 | }) => {
52 | const body: z.infer = {
53 | id,
54 | bucketName,
55 | };
56 |
57 | return makeRequest("/api/lambda/progress", body);
58 | };
59 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "../../styles/global.css";
2 | import type { AppProps } from "next/app";
3 |
4 | function MyApp({ Component, pageProps }: AppProps) {
5 | return ;
6 | }
7 |
8 | export default MyApp;
9 |
--------------------------------------------------------------------------------
/src/pages/api/lambda/progress.ts:
--------------------------------------------------------------------------------
1 | import {
2 | speculateFunctionName,
3 | AwsRegion,
4 | getRenderProgress,
5 | } from "@remotion/lambda/client";
6 | import { DISK, RAM, REGION, TIMEOUT } from "../../../../config.mjs";
7 | import { executeApi } from "../../../helpers/api-response";
8 | import { ProgressResponse, ProgressRequest } from "../../../../types/schema";
9 |
10 | const progress = executeApi(
11 | ProgressRequest,
12 | async (req, body) => {
13 | if (req.method !== "POST") {
14 | throw new Error("Only POST requests are allowed");
15 | }
16 |
17 | const renderProgress = await getRenderProgress({
18 | bucketName: body.bucketName,
19 | functionName: speculateFunctionName({
20 | diskSizeInMb: DISK,
21 | memorySizeInMb: RAM,
22 | timeoutInSeconds: TIMEOUT,
23 | }),
24 | region: REGION as AwsRegion,
25 | renderId: body.id,
26 | });
27 |
28 | if (renderProgress.fatalErrorEncountered) {
29 | return {
30 | type: "error",
31 | message: renderProgress.errors[0].message,
32 | };
33 | }
34 |
35 | if (renderProgress.done) {
36 | return {
37 | type: "done",
38 | url: renderProgress.outputFile as string,
39 | size: renderProgress.outputSizeInBytes as number,
40 | };
41 | }
42 |
43 | return {
44 | type: "progress",
45 | progress: Math.max(0.03, renderProgress.overallProgress),
46 | };
47 | },
48 | );
49 |
50 | export default progress;
51 |
--------------------------------------------------------------------------------
/src/pages/api/lambda/render.ts:
--------------------------------------------------------------------------------
1 | import { AwsRegion, RenderMediaOnLambdaOutput } from "@remotion/lambda/client";
2 | import {
3 | renderMediaOnLambda,
4 | speculateFunctionName,
5 | } from "@remotion/lambda/client";
6 | import { DISK, RAM, REGION, SITE_NAME, TIMEOUT } from "../../../../config.mjs";
7 | import { executeApi } from "../../../helpers/api-response";
8 | import { RenderRequest } from "../../../../types/schema";
9 |
10 | const render = executeApi(
11 | RenderRequest,
12 | async (req, body) => {
13 | if (req.method !== "POST") {
14 | throw new Error("Only POST requests are allowed");
15 | }
16 |
17 | if (
18 | !process.env.AWS_ACCESS_KEY_ID &&
19 | !process.env.REMOTION_AWS_ACCESS_KEY_ID
20 | ) {
21 | throw new TypeError(
22 | "Set up Remotion Lambda to render videos. See the README.md for how to do so.",
23 | );
24 | }
25 | if (
26 | !process.env.AWS_SECRET_ACCESS_KEY &&
27 | !process.env.REMOTION_AWS_SECRET_ACCESS_KEY
28 | ) {
29 | throw new TypeError(
30 | "The environment variable REMOTION_AWS_SECRET_ACCESS_KEY is missing. Add it to your .env file.",
31 | );
32 | }
33 |
34 | const result = await renderMediaOnLambda({
35 | codec: "h264",
36 | functionName: speculateFunctionName({
37 | diskSizeInMb: DISK,
38 | memorySizeInMb: RAM,
39 | timeoutInSeconds: TIMEOUT,
40 | }),
41 | region: REGION as AwsRegion,
42 | serveUrl: SITE_NAME,
43 | composition: body.id,
44 | inputProps: body.inputProps,
45 | framesPerLambda: 10,
46 | downloadBehavior: {
47 | type: "download",
48 | fileName: "video.mp4",
49 | },
50 | });
51 |
52 | return result;
53 | },
54 | );
55 |
56 | export default render;
57 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { Player } from "@remotion/player";
2 | import type { NextPage } from "next";
3 | import Head from "next/head";
4 | import React, { useMemo, useState } from "react";
5 | import { Main } from "../remotion/MyComp/Main";
6 | import {
7 | CompositionProps,
8 | defaultMyCompProps,
9 | DURATION_IN_FRAMES,
10 | VIDEO_FPS,
11 | VIDEO_HEIGHT,
12 | VIDEO_WIDTH,
13 | } from "../../types/constants";
14 | import { z } from "zod";
15 | import { RenderControls } from "../components/RenderControls";
16 | import { Tips } from "../components/Tips/Tips";
17 | import { Spacing } from "../components/Spacing";
18 |
19 | const container: React.CSSProperties = {
20 | maxWidth: 768,
21 | margin: "auto",
22 | marginBottom: 20,
23 | };
24 |
25 | const outer: React.CSSProperties = {
26 | borderRadius: "var(--geist-border-radius)",
27 | overflow: "hidden",
28 | boxShadow: "0 0 200px rgba(0, 0, 0, 0.15)",
29 | marginBottom: 40,
30 | marginTop: 60,
31 | };
32 |
33 | const player: React.CSSProperties = {
34 | width: "100%",
35 | };
36 |
37 | const Home: NextPage = () => {
38 | const [text, setText] = useState(defaultMyCompProps.title);
39 |
40 | const inputProps: z.infer = useMemo(() => {
41 | return {
42 | title: text,
43 | };
44 | }, [text]);
45 |
46 | return (
47 |
48 |
49 |
Remotion and Next.js
50 |
51 |
55 |
56 |
57 |
58 |
72 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | );
85 | };
86 |
87 | export default Home;
88 |
--------------------------------------------------------------------------------
/src/remotion/MyComp/Main.tsx:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import {
3 | AbsoluteFill,
4 | Sequence,
5 | spring,
6 | useCurrentFrame,
7 | useVideoConfig,
8 | } from "remotion";
9 | import { NextLogo } from "./NextLogo";
10 | import { loadFont, fontFamily } from "@remotion/google-fonts/Inter";
11 | import React, { useMemo } from "react";
12 | import { Rings } from "./Rings";
13 | import { TextFade } from "./TextFade";
14 | import { CompositionProps } from "../../../types/constants";
15 |
16 | loadFont("normal", {
17 | subsets: ["latin"],
18 | weights: ["400", "700"],
19 | });
20 |
21 | const container: React.CSSProperties = {
22 | backgroundColor: "white",
23 | };
24 |
25 | const logo: React.CSSProperties = {
26 | justifyContent: "center",
27 | alignItems: "center",
28 | };
29 |
30 | export const Main = ({ title }: z.infer) => {
31 | const frame = useCurrentFrame();
32 | const { fps } = useVideoConfig();
33 |
34 | const transitionStart = 2 * fps;
35 | const transitionDuration = 1 * fps;
36 |
37 | const logoOut = spring({
38 | fps,
39 | frame,
40 | config: {
41 | damping: 200,
42 | },
43 | durationInFrames: transitionDuration,
44 | delay: transitionStart,
45 | });
46 |
47 | const titleStyle: React.CSSProperties = useMemo(() => {
48 | return { fontFamily, fontSize: 70 };
49 | }, []);
50 |
51 | return (
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {title}
62 |
63 |
64 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/src/remotion/MyComp/NextLogo.tsx:
--------------------------------------------------------------------------------
1 | import { evolvePath } from "@remotion/paths";
2 | import React, { useMemo } from "react";
3 | import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
4 |
5 | const mask: React.CSSProperties = {
6 | maskType: "alpha",
7 | };
8 |
9 | const nStroke =
10 | "M149.508 157.52L69.142 54H54V125.97H66.1136V69.3836L139.999 164.845C143.333 162.614 146.509 160.165 149.508 157.52Z";
11 |
12 | export const NextLogo: React.FC<{
13 | outProgress: number;
14 | }> = ({ outProgress }) => {
15 | const { fps } = useVideoConfig();
16 | const frame = useCurrentFrame();
17 |
18 | const evolve1 = spring({
19 | fps,
20 | frame,
21 | config: {
22 | damping: 200,
23 | },
24 | });
25 | const evolve2 = spring({
26 | fps,
27 | frame: frame - 15,
28 | config: {
29 | damping: 200,
30 | },
31 | });
32 | const evolve3 = spring({
33 | fps,
34 | frame: frame - 30,
35 | config: {
36 | damping: 200,
37 | mass: 3,
38 | },
39 | durationInFrames: 30,
40 | });
41 |
42 | const style: React.CSSProperties = useMemo(() => {
43 | return {
44 | height: 140,
45 | borderRadius: 70,
46 | scale: String(1 - outProgress),
47 | };
48 | }, [outProgress]);
49 |
50 | const firstPath = `M 60.0568 54 v 71.97`;
51 | const secondPath = `M 63.47956 56.17496 L 144.7535 161.1825`;
52 | const thirdPath = `M 121 54 L 121 126`;
53 |
54 | const evolution1 = evolvePath(evolve1, firstPath);
55 | const evolution2 = evolvePath(evolve2, secondPath);
56 | const evolution3 = evolvePath(
57 | interpolate(evolve3, [0, 1], [0, 0.7]),
58 | thirdPath,
59 | );
60 |
61 | return (
62 |
118 | );
119 | };
120 |
--------------------------------------------------------------------------------
/src/remotion/MyComp/Rings.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { AbsoluteFill, interpolateColors, useVideoConfig } from "remotion";
3 |
4 | const RadialGradient: React.FC<{
5 | radius: number;
6 | color: string;
7 | }> = ({ radius, color }) => {
8 | const height = radius * 2;
9 | const width = radius * 2;
10 |
11 | return (
12 |
18 |
28 |
29 | );
30 | };
31 |
32 | export const Rings: React.FC<{
33 | outProgress: number;
34 | }> = ({ outProgress }) => {
35 | const scale = 1 / (1 - outProgress);
36 | const { height } = useVideoConfig();
37 |
38 | return (
39 |
44 | {new Array(5)
45 | .fill(true)
46 | .map((_, i) => {
47 | return (
48 |
53 | );
54 | })
55 | .reverse()}
56 |
57 | );
58 | };
59 |
--------------------------------------------------------------------------------
/src/remotion/MyComp/TextFade.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 | import {
3 | AbsoluteFill,
4 | interpolate,
5 | spring,
6 | useCurrentFrame,
7 | useVideoConfig,
8 | } from "remotion";
9 |
10 | const outer: React.CSSProperties = {};
11 |
12 | export const TextFade: React.FC<{
13 | children: React.ReactNode;
14 | }> = ({ children }) => {
15 | const { fps } = useVideoConfig();
16 | const frame = useCurrentFrame();
17 |
18 | const progress = spring({
19 | fps,
20 | frame,
21 | config: {
22 | damping: 200,
23 | },
24 | durationInFrames: 80,
25 | });
26 |
27 | const rightStop = interpolate(progress, [0, 1], [200, 0]);
28 |
29 | const leftStop = Math.max(0, rightStop - 60);
30 |
31 | const maskImage = `linear-gradient(-45deg, transparent ${leftStop}%, black ${rightStop}%)`;
32 |
33 | const container: React.CSSProperties = useMemo(() => {
34 | return {
35 | justifyContent: "center",
36 | alignItems: "center",
37 | };
38 | }, []);
39 |
40 | const content: React.CSSProperties = useMemo(() => {
41 | return {
42 | maskImage,
43 | WebkitMaskImage: maskImage,
44 | };
45 | }, [maskImage]);
46 |
47 | return (
48 |
49 |
50 | {children}
51 |
52 |
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/src/remotion/Root.tsx:
--------------------------------------------------------------------------------
1 | import { Composition } from "remotion";
2 | import { Main } from "./MyComp/Main";
3 | import {
4 | COMP_NAME,
5 | defaultMyCompProps,
6 | DURATION_IN_FRAMES,
7 | VIDEO_FPS,
8 | VIDEO_HEIGHT,
9 | VIDEO_WIDTH,
10 | } from "../../types/constants";
11 | import { NextLogo } from "./MyComp/NextLogo";
12 |
13 | export const RemotionRoot: React.FC = () => {
14 | return (
15 | <>
16 |
25 |
36 | >
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/remotion/index.ts:
--------------------------------------------------------------------------------
1 | import { registerRoot } from "remotion";
2 | import { RemotionRoot } from "./Root";
3 |
4 | registerRoot(RemotionRoot);
5 |
--------------------------------------------------------------------------------
/styles/global.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --background: #fff;
3 | --foreground: #000;
4 | --unfocused-border-color: #eaeaea;
5 | --focused-border-color: #666;
6 |
7 | --button-disabled-color: #fafafa;
8 | --disabled-text-color: #999;
9 |
10 | --geist-border-radius: 5px;
11 | --geist-quarter-pad: 6px;
12 | --geist-half-pad: 12px;
13 | --geist-pad: 24px;
14 | --geist-font: "Inter";
15 |
16 | --geist-error: #e00;
17 |
18 | --subtitle: #666;
19 | }
20 |
21 | * {
22 | box-sizing: border-box;
23 | }
24 |
25 | .cinematics {
26 | box-shadow: 0 0 200px rgba(0, 0, 0, 0.15);
27 | }
28 |
29 | @media (prefers-color-scheme: dark) {
30 | :root {
31 | --background: #000000;
32 | --unfocused-border-color: #333;
33 | --focused-border-color: #888;
34 | --foreground: #fff;
35 | --button-disabled-color: #111;
36 | --geist-error: red;
37 | --subtitle: #8d8d8d;
38 | }
39 | .cinematics {
40 | box-shadow: 0 0 200px rgba(255, 255, 255, 0.15);
41 | }
42 | }
43 |
44 | body {
45 | background-color: var(--background);
46 | }
47 |
48 | input {
49 | border: 1px solid var(--unfocused-border-color);
50 | transition: border-color 0.15s ease;
51 | outline: none;
52 | }
53 |
54 | input:focus {
55 | border-color: var(--focused-border-color);
56 | }
57 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve",
20 | "noUnusedLocals": true,
21 | "incremental": true
22 | },
23 | "include": [
24 | "next-env.d.ts",
25 | "**/*.ts",
26 | "**/*.tsx"
27 | ],
28 | "exclude": [
29 | "node_modules",
30 | "remotion.config.ts"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/types/constants.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | export const COMP_NAME = "MyComp";
3 |
4 | export const CompositionProps = z.object({
5 | title: z.string(),
6 | });
7 |
8 | export const defaultMyCompProps: z.infer = {
9 | title: "Next.js and Remotion",
10 | };
11 |
12 | export const DURATION_IN_FRAMES = 200;
13 | export const VIDEO_WIDTH = 1280;
14 | export const VIDEO_HEIGHT = 720;
15 | export const VIDEO_FPS = 30;
16 |
--------------------------------------------------------------------------------
/types/schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { CompositionProps } from "./constants";
3 |
4 | export const RenderRequest = z.object({
5 | id: z.string(),
6 | inputProps: CompositionProps,
7 | });
8 |
9 | export const ProgressRequest = z.object({
10 | bucketName: z.string(),
11 | id: z.string(),
12 | });
13 |
14 | export type ProgressResponse =
15 | | {
16 | type: "error";
17 | message: string;
18 | }
19 | | {
20 | type: "progress";
21 | progress: number;
22 | }
23 | | {
24 | type: "done";
25 | url: string;
26 | size: number;
27 | };
28 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "buildCommand": "node deploy.mjs && next build"
3 | }
4 |
--------------------------------------------------------------------------------