├── .env.example
├── .gitignore
├── .npmrc
├── .prettierrc
├── README.md
├── config.mjs
├── deploy.mjs
├── eslint.config.mjs
├── next.config.js
├── package.json
├── postcss.config.mjs
├── remotion.config.ts
├── src
├── app
│ ├── api
│ │ └── lambda
│ │ │ ├── progress
│ │ │ └── route.ts
│ │ │ └── render
│ │ │ └── route.ts
│ ├── favicon.ico
│ ├── layout.tsx
│ └── page.tsx
├── components
│ ├── AlignEnd.tsx
│ ├── Button.tsx
│ ├── Container.tsx
│ ├── DownloadButton.tsx
│ ├── Error.tsx
│ ├── Input.tsx
│ ├── ProgressBar.tsx
│ ├── RenderControls.tsx
│ ├── Spacing.tsx
│ ├── Spinner.tsx
│ └── Tips.tsx
├── helpers
│ ├── api-response.ts
│ └── use-rendering.ts
├── lambda
│ └── api.ts
├── lib
│ └── utils.ts
└── remotion
│ ├── MyComp
│ ├── Main.tsx
│ ├── NextLogo.tsx
│ ├── Rings.tsx
│ └── TextFade.tsx
│ ├── Root.tsx
│ ├── index.ts
│ └── webpack-override.mjs
├── 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
39 | build
--------------------------------------------------------------------------------
/.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 App directory, with TailwindCSS. There is a [Non-TailwindCSS version](https://github.com/remotion-dev/template-next-app-dir), and a [Pages directory version](https://github.com/remotion-dev/template-next-pages-dir) of this template available.
8 |
9 |
10 |
11 | ## Getting Started
12 |
13 | [Use this template](https://github.com/new?template_name=template-next-app-dir-tailwind&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-tailwind
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 | import { VERSION } from "remotion/version";
2 |
3 | /**
4 | * Use autocomplete to get a list of available regions.
5 | * @type {import('@remotion/lambda').AwsRegion}
6 | */
7 | export const REGION = "us-east-1";
8 |
9 | export const SITE_NAME = "my-next-app";
10 | export const RAM = 3009;
11 | export const DISK = 10240;
12 | export const TIMEOUT = 240;
13 |
--------------------------------------------------------------------------------
/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 | import { webpackOverride } from "./src/remotion/webpack-override.mjs";
10 |
11 | console.log("Selected region:", REGION);
12 | dotenv.config();
13 |
14 | if (!process.env.AWS_ACCESS_KEY_ID && !process.env.REMOTION_AWS_ACCESS_KEY_ID) {
15 | console.log(
16 | 'The environment variable "REMOTION_AWS_ACCESS_KEY_ID" is not set.',
17 | );
18 | console.log("Lambda renders were not set up.");
19 | console.log(
20 | "Complete the Lambda setup: at https://www.remotion.dev/docs/lambda/setup",
21 | );
22 | process.exit(0);
23 | }
24 | if (
25 | !process.env.AWS_SECRET_ACCESS_KEY &&
26 | !process.env.REMOTION_AWS_SECRET_ACCESS_KEY
27 | ) {
28 | console.log(
29 | 'The environment variable "REMOTION_REMOTION_AWS_SECRET_ACCESS_KEY" is not set.',
30 | );
31 | console.log("Lambda renders were not set up.");
32 | console.log(
33 | "Complete the Lambda setup: at https://www.remotion.dev/docs/lambda/setup",
34 | );
35 | process.exit(0);
36 | }
37 |
38 | process.stdout.write("Deploying Lambda function... ");
39 |
40 | const { functionName, alreadyExisted: functionAlreadyExisted } =
41 | await deployFunction({
42 | createCloudWatchLogGroup: true,
43 | memorySizeInMb: RAM,
44 | region: REGION,
45 | timeoutInSeconds: TIMEOUT,
46 | diskSizeInMb: DISK,
47 | });
48 | console.log(
49 | functionName,
50 | functionAlreadyExisted ? "(already existed)" : "(created)",
51 | );
52 |
53 | process.stdout.write("Ensuring bucket... ");
54 | const { bucketName, alreadyExisted: bucketAlreadyExisted } =
55 | await getOrCreateBucket({
56 | region: REGION,
57 | });
58 | console.log(
59 | bucketName,
60 | bucketAlreadyExisted ? "(already existed)" : "(created)",
61 | );
62 |
63 | process.stdout.write("Deploying site... ");
64 | const { siteName } = await deploySite({
65 | bucketName,
66 | entryPoint: path.join(process.cwd(), "src", "remotion", "index.ts"),
67 | siteName: SITE_NAME,
68 | region: REGION,
69 | options: { webpackOverride },
70 | });
71 |
72 | console.log(siteName);
73 |
74 | console.log();
75 | console.log("You now have everything you need to render videos!");
76 | console.log("Re-run this command when:");
77 | console.log(" 1) you changed the video template");
78 | console.log(" 2) you changed config.mjs");
79 | console.log(" 3) you upgraded Remotion to a newer version");
80 |
--------------------------------------------------------------------------------
/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-app-tailwind",
3 | "version": "0.1.0",
4 | "private": true,
5 | "author": {
6 | "name": "Mohit Yadav",
7 | "email": "mohit@mohitya.dev",
8 | "url": "https://mohitya.dev"
9 | },
10 | "scripts": {
11 | "dev": "next dev",
12 | "build": "next build",
13 | "start": "next start",
14 | "lint": "next lint",
15 | "remotion": "remotion studio",
16 | "render": "remotion render",
17 | "deploy": "node deploy.mjs"
18 | },
19 | "dependencies": {
20 | "@remotion/bundler": "^4.0.0",
21 | "@remotion/cli": "^4.0.0",
22 | "@remotion/google-fonts": "^4.0.0",
23 | "@remotion/lambda": "^4.0.0",
24 | "@remotion/paths": "^4.0.0",
25 | "@remotion/player": "^4.0.0",
26 | "@remotion/shapes": "^4.0.0",
27 | "@remotion/tailwind-v4": "^4.0.0",
28 | "clsx": "2.1.1",
29 | "next": "15.2.4",
30 | "react": "19.0.0",
31 | "react-dom": "19.0.0",
32 | "remotion": "^4.0.0",
33 | "tailwind-merge": "3.0.1",
34 | "zod": "3.22.3"
35 | },
36 | "devDependencies": {
37 | "@remotion/eslint-plugin": "^4.0.0",
38 | "@eslint/eslintrc": "3.1.0",
39 | "@types/node": "20.12.14",
40 | "@types/react": "19.0.0",
41 | "@types/react-dom": "19.0.0",
42 | "@types/web": "0.0.166",
43 | "autoprefixer": "10.4.20",
44 | "dotenv": "16.0.3",
45 | "eslint": "9.19.0",
46 | "@next/eslint-plugin-next": "15.1.6",
47 | "eslint-config-next": "15.1.6",
48 | "postcss": "8.4.47",
49 | "prettier": "3.3.3",
50 | "tailwindcss": "4.0.3",
51 | "typescript": "5.8.2"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: {
3 | "@tailwindcss/postcss": {},
4 | },
5 | };
6 | export default config;
7 |
--------------------------------------------------------------------------------
/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 | import { webpackOverride } from "./src/remotion/webpack-override.mjs";
8 |
9 | Config.setVideoImageFormat("jpeg");
10 |
11 | Config.overrideWebpackConfig(webpackOverride);
12 |
--------------------------------------------------------------------------------
/src/app/api/lambda/progress/route.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 { ProgressResponse, ProgressRequest } from "../../../../../types/schema";
8 | import { executeApi } from "../../../../helpers/api-response";
9 |
10 | export const POST = executeApi(
11 | ProgressRequest,
12 | async (req, body) => {
13 | const renderProgress = await getRenderProgress({
14 | bucketName: body.bucketName,
15 | functionName: speculateFunctionName({
16 | diskSizeInMb: DISK,
17 | memorySizeInMb: RAM,
18 | timeoutInSeconds: TIMEOUT,
19 | }),
20 | region: REGION as AwsRegion,
21 | renderId: body.id,
22 | });
23 |
24 | if (renderProgress.fatalErrorEncountered) {
25 | return {
26 | type: "error",
27 | message: renderProgress.errors[0].message,
28 | };
29 | }
30 |
31 | if (renderProgress.done) {
32 | return {
33 | type: "done",
34 | url: renderProgress.outputFile as string,
35 | size: renderProgress.outputSizeInBytes as number,
36 | };
37 | }
38 |
39 | return {
40 | type: "progress",
41 | progress: Math.max(0.03, renderProgress.overallProgress),
42 | };
43 | },
44 | );
45 |
--------------------------------------------------------------------------------
/src/app/api/lambda/render/route.ts:
--------------------------------------------------------------------------------
1 | import { AwsRegion, RenderMediaOnLambdaOutput } from "@remotion/lambda/client";
2 | import {
3 | renderMediaOnLambda,
4 | speculateFunctionName,
5 | } from "@remotion/lambda/client";
6 | import {
7 | DISK,
8 | RAM,
9 | REGION,
10 | SITE_NAME,
11 | TIMEOUT,
12 | } from "../../../../../config.mjs";
13 | import { RenderRequest } from "../../../../../types/schema";
14 | import { executeApi } from "../../../../helpers/api-response";
15 |
16 | export const POST = executeApi(
17 | RenderRequest,
18 | async (req, body) => {
19 | if (
20 | !process.env.AWS_ACCESS_KEY_ID &&
21 | !process.env.REMOTION_AWS_ACCESS_KEY_ID
22 | ) {
23 | throw new TypeError(
24 | "Set up Remotion Lambda to render videos. See the README.md for how to do so.",
25 | );
26 | }
27 | if (
28 | !process.env.AWS_SECRET_ACCESS_KEY &&
29 | !process.env.REMOTION_AWS_SECRET_ACCESS_KEY
30 | ) {
31 | throw new TypeError(
32 | "The environment variable REMOTION_AWS_SECRET_ACCESS_KEY is missing. Add it to your .env file.",
33 | );
34 | }
35 |
36 | const result = await renderMediaOnLambda({
37 | codec: "h264",
38 | functionName: speculateFunctionName({
39 | diskSizeInMb: DISK,
40 | memorySizeInMb: RAM,
41 | timeoutInSeconds: TIMEOUT,
42 | }),
43 | region: REGION as AwsRegion,
44 | serveUrl: SITE_NAME,
45 | composition: body.id,
46 | inputProps: body.inputProps,
47 | framesPerLambda: 10,
48 | downloadBehavior: {
49 | type: "download",
50 | fileName: "video.mp4",
51 | },
52 | });
53 |
54 | return result;
55 | },
56 | );
57 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remotion-dev/template-next-app-dir-tailwind/ee1e8606f476f06a42ca5d32317b3c0500ac4b86/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "../../styles/global.css";
2 | import { Metadata, Viewport } from "next";
3 |
4 | export const metadata: Metadata = {
5 | title: "Remotion and Next.js",
6 | description: "Remotion and Next.js",
7 | };
8 |
9 | export const viewport: Viewport = {
10 | width: "device-width",
11 | initialScale: 1,
12 | maximumScale: 1,
13 | };
14 |
15 | export default function RootLayout({
16 | children,
17 | }: {
18 | children: React.ReactNode;
19 | }) {
20 | return (
21 |
22 | {children}
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Player } from "@remotion/player";
4 | import type { NextPage } from "next";
5 | import React, { useMemo, useState } from "react";
6 | import { z } from "zod";
7 | import {
8 | defaultMyCompProps,
9 | CompositionProps,
10 | DURATION_IN_FRAMES,
11 | VIDEO_FPS,
12 | VIDEO_HEIGHT,
13 | VIDEO_WIDTH,
14 | } from "../../types/constants";
15 | import { RenderControls } from "../components/RenderControls";
16 | import { Spacing } from "../components/Spacing";
17 | import { Tips } from "../components/Tips";
18 | import { Main } from "../remotion/MyComp/Main";
19 |
20 | const Home: NextPage = () => {
21 | const [text, setText] = useState(defaultMyCompProps.title);
22 |
23 | const inputProps: z.infer = useMemo(() => {
24 | return {
25 | title: text,
26 | };
27 | }, [text]);
28 |
29 | return (
30 |
31 |
32 |
50 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | export default Home;
66 |
--------------------------------------------------------------------------------
/src/components/AlignEnd.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const AlignEnd: React.FC<{
4 | children: React.ReactNode;
5 | }> = ({ children }) => {
6 | return {children}
;
7 | };
8 |
--------------------------------------------------------------------------------
/src/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from "react";
2 | import { Spacing } from "./Spacing";
3 | import { Spinner } from "./Spinner";
4 | import { cn } from "../lib/utils";
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 |
36 | );
37 | };
38 |
39 | export const Button = forwardRef(ButtonForward);
40 |
--------------------------------------------------------------------------------
/src/components/Container.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const InputContainer: React.FC<{
4 | children: React.ReactNode;
5 | }> = ({ children }) => {
6 | return (
7 |
8 | {children}
9 |
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/src/components/DownloadButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { State } from "../helpers/use-rendering";
3 | import { Button } from "./Button";
4 | import { Spacing } from "./Spacing";
5 |
6 | const Megabytes: React.FC<{
7 | sizeInBytes: number;
8 | }> = ({ sizeInBytes }) => {
9 | const megabytes = Intl.NumberFormat("en", {
10 | notation: "compact",
11 | style: "unit",
12 | unit: "byte",
13 | unitDisplay: "narrow",
14 | }).format(sizeInBytes);
15 | return {megabytes};
16 | };
17 |
18 | export const DownloadButton: React.FC<{
19 | state: State;
20 | undo: () => void;
21 | }> = ({ state, undo }) => {
22 | if (state.status === "rendering") {
23 | return ;
24 | }
25 |
26 | if (state.status !== "done") {
27 | throw new Error("Download button should not be rendered when not done");
28 | }
29 |
30 | return (
31 |
44 | );
45 | };
46 |
47 | const UndoIcon: React.FC = () => {
48 | return (
49 |
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/src/components/Error.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const ErrorComp: React.FC<{
4 | message: string;
5 | }> = ({ message }) => {
6 | return (
7 |
8 |
22 |
Error: {message}
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/components/Input.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from "react";
2 |
3 | export const Input: React.FC<{
4 | text: string;
5 | setText: React.Dispatch>;
6 | disabled?: boolean;
7 | }> = ({ text, setText, disabled }) => {
8 | const onChange: React.ChangeEventHandler = useCallback(
9 | (e) => {
10 | setText(e.currentTarget.value);
11 | },
12 | [setText],
13 | );
14 |
15 | return (
16 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/ProgressBar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 |
3 | export const ProgressBar: React.FC<{
4 | progress: number;
5 | }> = ({ progress }) => {
6 | const fill: React.CSSProperties = useMemo(() => {
7 | return {
8 | width: `${progress * 100}%`,
9 | };
10 | }, [progress]);
11 |
12 | return (
13 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/RenderControls.tsx:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { AlignEnd } from "./AlignEnd";
3 | import { Button } from "./Button";
4 | import { InputContainer } from "./Container";
5 | import { DownloadButton } from "./DownloadButton";
6 | import { ErrorComp } from "./Error";
7 | import { Input } from "./Input";
8 | import { ProgressBar } from "./ProgressBar";
9 | import { Spacing } from "./Spacing";
10 | import { COMP_NAME, CompositionProps } from "../../types/constants";
11 | import { useRendering } from "../helpers/use-rendering";
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 | };
6 |
--------------------------------------------------------------------------------
/src/components/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 | import { makeRect } from "@remotion/shapes";
3 | import { translatePath } from "@remotion/paths";
4 |
5 | const viewBox = 100;
6 | const lines = 12;
7 | const width = viewBox * 0.08;
8 |
9 | const { path } = makeRect({
10 | height: viewBox * 0.24,
11 | width,
12 | cornerRadius: width / 2,
13 | });
14 |
15 | const translated = translatePath(path, viewBox / 2 - width / 2, viewBox * 0.03);
16 |
17 | export const Spinner: React.FC<{
18 | size: number;
19 | }> = ({ size }) => {
20 | const style = useMemo(() => {
21 | return {
22 | width: size,
23 | height: size,
24 | };
25 | }, [size]);
26 |
27 | return (
28 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/src/components/Tips.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Tip: React.FC<{
4 | title: React.ReactNode;
5 | description: React.ReactNode;
6 | href: string;
7 | }> = ({ title, description, href }) => {
8 | return (
9 |
10 |
11 |
12 |
{title}
13 |
14 |
24 |
25 |
26 | {description}
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export const Tips: React.FC = () => {
34 | return (
35 |
36 |
41 |
46 |
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/src/helpers/api-response.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
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: (req: Request, body: z.infer) => Promise,
18 | ) =>
19 | async (req: Request) => {
20 | try {
21 | const payload = await req.json();
22 | const parsed = schema.parse(payload);
23 | const data = await handler(req, parsed);
24 | return NextResponse.json({
25 | type: "success",
26 | data: data,
27 | });
28 | } catch (err) {
29 | return NextResponse.json(
30 | { type: "error", message: (err as Error).message },
31 | {
32 | status: 500,
33 | },
34 | );
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/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 {
4 | ProgressRequest,
5 | ProgressResponse,
6 | RenderRequest,
7 | } from "../../types/schema";
8 | import { CompositionProps } from "../../types/constants";
9 | import { ApiResponse } from "../helpers/api-response";
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/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/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 { CompositionProps } from "../../../types/constants";
10 | import { NextLogo } from "./NextLogo";
11 | import { loadFont, fontFamily } from "@remotion/google-fonts/Inter";
12 | import React from "react";
13 | import { Rings } from "./Rings";
14 | import { TextFade } from "./TextFade";
15 |
16 | loadFont("normal", {
17 | subsets: ["latin"],
18 | weights: ["400", "700"],
19 | });
20 | export const Main = ({ title }: z.infer) => {
21 | const frame = useCurrentFrame();
22 | const { fps } = useVideoConfig();
23 |
24 | const transitionStart = 2 * fps;
25 | const transitionDuration = 1 * fps;
26 |
27 | const logoOut = spring({
28 | fps,
29 | frame,
30 | config: {
31 | damping: 200,
32 | },
33 | durationInFrames: transitionDuration,
34 | delay: transitionStart,
35 | });
36 |
37 | return (
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
53 | {title}
54 |
55 |
56 |
57 |
58 | );
59 | };
60 |
--------------------------------------------------------------------------------
/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 nStroke =
6 | "M149.508 157.52L69.142 54H54V125.97H66.1136V69.3836L139.999 164.845C143.333 162.614 146.509 160.165 149.508 157.52Z";
7 |
8 | export const NextLogo: React.FC<{
9 | outProgress: number;
10 | }> = ({ outProgress }) => {
11 | const { fps } = useVideoConfig();
12 | const frame = useCurrentFrame();
13 |
14 | const evolve1 = spring({
15 | fps,
16 | frame,
17 | config: {
18 | damping: 200,
19 | },
20 | });
21 | const evolve2 = spring({
22 | fps,
23 | frame: frame - 15,
24 | config: {
25 | damping: 200,
26 | },
27 | });
28 | const evolve3 = spring({
29 | fps,
30 | frame: frame - 30,
31 | config: {
32 | damping: 200,
33 | mass: 3,
34 | },
35 | durationInFrames: 30,
36 | });
37 |
38 | const style: React.CSSProperties = useMemo(() => {
39 | return {
40 | height: 140,
41 | borderRadius: 70,
42 | scale: String(1 - outProgress),
43 | };
44 | }, [outProgress]);
45 |
46 | const firstPath = `M 60.0568 54 v 71.97`;
47 | const secondPath = `M 63.47956 56.17496 L 144.7535 161.1825`;
48 | const thirdPath = `M 121 54 L 121 126`;
49 |
50 | const evolution1 = evolvePath(evolve1, firstPath);
51 | const evolution2 = evolvePath(evolve2, secondPath);
52 | const evolution3 = evolvePath(
53 | interpolate(evolve3, [0, 1], [0, 0.7]),
54 | thirdPath,
55 | );
56 |
57 | return (
58 |
121 | );
122 | };
123 |
--------------------------------------------------------------------------------
/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 |
26 |
27 | );
28 | };
29 |
30 | export const Rings: React.FC<{
31 | outProgress: number;
32 | }> = ({ outProgress }) => {
33 | const scale = 1 / (1 - outProgress);
34 | const { height } = useVideoConfig();
35 |
36 | return (
37 |
42 | {new Array(5)
43 | .fill(true)
44 | .map((_, i) => {
45 | return (
46 |
51 | );
52 | })
53 | .reverse()}
54 |
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/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 | export const TextFade: React.FC<{
11 | children: React.ReactNode;
12 | }> = ({ children }) => {
13 | const { fps } = useVideoConfig();
14 | const frame = useCurrentFrame();
15 |
16 | const progress = spring({
17 | fps,
18 | frame,
19 | config: {
20 | damping: 200,
21 | },
22 | durationInFrames: 80,
23 | });
24 |
25 | const rightStop = interpolate(progress, [0, 1], [200, 0]);
26 |
27 | const leftStop = Math.max(0, rightStop - 60);
28 |
29 | const maskImage = `linear-gradient(-45deg, transparent ${leftStop}%, black ${rightStop}%)`;
30 |
31 | const content: React.CSSProperties = useMemo(() => {
32 | return {
33 | maskImage,
34 | WebkitMaskImage: maskImage,
35 | };
36 | }, [maskImage]);
37 |
38 | return (
39 |
40 |
41 | {children}
42 |
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/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 | import "../../styles/global.css";
4 |
5 | registerRoot(RemotionRoot);
6 |
--------------------------------------------------------------------------------
/src/remotion/webpack-override.mjs:
--------------------------------------------------------------------------------
1 | import { enableTailwind } from "@remotion/tailwind-v4";
2 |
3 | /**
4 | * @param {import('webpack').Configuration} currentConfig
5 | */
6 | export const webpackOverride = (currentConfig) => {
7 | return enableTailwind(currentConfig);
8 | };
9 |
--------------------------------------------------------------------------------
/styles/global.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
3 | :root {
4 | --background: #fff;
5 | --foreground: #000;
6 | --unfocused-border-color: #eaeaea;
7 | --focused-border-color: #666;
8 |
9 | --button-disabled-color: #fafafa;
10 | --disabled-text-color: #999;
11 |
12 | --geist-border-radius: 5px;
13 | --geist-quarter-pad: 6px;
14 | --geist-half-pad: 12px;
15 | --geist-pad: 24px;
16 | --geist-font: "Inter";
17 |
18 | --geist-error: #e00;
19 |
20 | --subtitle: #666;
21 | }
22 |
23 | @media (prefers-color-scheme: dark) {
24 | :root {
25 | --background: #000000;
26 | --unfocused-border-color: #333;
27 | --focused-border-color: #888;
28 | --foreground: #fff;
29 | --button-disabled-color: #111;
30 | --geist-error: red;
31 | --subtitle: #8d8d8d;
32 | }
33 | }
34 |
35 | @theme {
36 | --color-foreground: var(--foreground);
37 | --color-background: var(--background);
38 | --color-unfocused-border-color: var(--unfocused-border-color);
39 | --color-focused-border-color: var(--focused-border-color);
40 | --color-button-disabled-color: var(--button-disabled-color);
41 | --color-disabled-text-color: var(--disabled-text-color);
42 | --color-geist-error: var(--geist-error);
43 | --color-subtitle: var(--subtitle);
44 | --padding-geist-quarter: var(--geist-quarter-pad);
45 | --padding-geist-half: var(--geist-half-pad);
46 | --padding-geist: var(--geist-pad);
47 | --spacing-geist: var(--geist-pad);
48 | --spacing-geist-half: var(--geist-half-pad);
49 | --spacing-geist-quarter: var(--geist-quarter-pad);
50 | --radius-geist: var(--geist-border-radius);
51 | --font-geist: var(--geist-font);
52 | --animate-spinner: spinner 1.2s linear infinite;
53 |
54 | @keyframes spinner {
55 | 0% {
56 | opacity: 1;
57 | }
58 | 100% {
59 | opacity: 0.15;
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/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 | "plugins": [
22 | {
23 | "name": "next"
24 | }
25 | ],
26 | "incremental": true
27 | },
28 | "include": [
29 | "next-env.d.ts",
30 | "**/*.ts",
31 | "**/*.tsx",
32 | ".next/types/**/*.ts",
33 | "remotion/webpack-override.mjs"
34 | ],
35 | "exclude": [
36 | "node_modules",
37 | "remotion.config.ts"
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------