├── .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 |
33 | 49 |
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 |
32 | 35 | 36 | 37 | 42 | 43 |
44 | ); 45 | }; 46 | 47 | const UndoIcon: React.FC = () => { 48 | return ( 49 | 50 | 54 | 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 | 18 | 19 | 20 | 21 | 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 |
14 |
15 |
19 |
20 |
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 | 29 | {new Array(lines).fill(true).map((_, index) => { 30 | return ( 31 | 42 | ); 43 | })} 44 | 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 | 19 | 23 | 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 | 59 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 81 | 87 | 88 | 95 | 96 | 97 | 105 | 106 | 107 | 108 | 116 | 117 | 118 | 119 | 120 | 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 |