Example Next.JS app and docs for rendering on Next.JS
After 3.0, have a more thorough example with Remotion + Player + SSR, and also have some docs about it. I'll take this one.
I just did Remotion in a (local only for now) NextJS project. <Player> was very easy to set up. In fact, I'd recommend NextJS to newbies to try Remotion player in a 'real' web page/app.
Taking this template as a starter: https://github.com/karelnagel/remotion-next-example
Not officially released as an official template, but https://github.com/remotion-dev/template-next is a good start
Hi @JonnyBurger
I'm exploring the use of remotion with Next.js and was curious about some details regarding this example.
I've been able to get the player working client side in a next JS app fine where I can then add some parameterizations to control the video. However I'm now trying to understand how I can have a button that sends a request to the server to then render this parameterized video (by the next js server itself, not a lambda function). What would be the workflow?
I've been looking at the SSR docs here, but I'm a bit confused of how the parameters from the client be communicated to the server that would render this. Does the bundle figure this out?
Thanks!
Hi @NickGeneva!
Rendering on Next.js is not recommended, because it will not work on Vercel Serverless function. This is a common question, so I have written a documentation page for it now!
https://www.remotion.dev/docs/miscellaneous/vercel-functions
Hi @JonnyBurger
Really appreciate the super fast response and info. Quick follow up, I'm planning on deploying this next JS on my own machine with sufficient specs / hardware.
If you don't deploy to Vercel, it is possible to render videos in API routes using the server-side rendering primitives.
Regarding this point here, how would this be approached with a parameterized video? Would the client send an api call with a JSON of these parameters to the server that would then injest these values using inputProps? Is there a clean way to extract these input props from a existing composition (or do I need to manually build/parse this)?
Looking at github workflow example, I think this is correct but want to confirm I'm on the right track.
Many thanks!
Hmmmm seems a bit more tricky to get this to work with next JS API routes, Next.js complains about the bundler using components that need to be client side:
You're importing a component that needs useReducer. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
Learn more: https://nextjs.org/docs/getting-started/react-essentials
,-[.../node_modules/remotion/dist/esm/index.mjs:1:1]
1 | import React, { createContext, useState, useMemo, useLayoutEffect, useContext, useEffect, forwardRef, Children, isValidElement, useRef, useCallback, createRef, useImperativeHandle, useReducer, Suspense } from 'react';
: ^^^^^^^^^^
2 | import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3 | import { createPortal } from 'react-dom';
`----
The error was caused by importing '@remotion/bundler/dist/index.js' in './src/app/api/render/bundler.ts'.
Maybe one of these should be marked as a client entry with "use client":
Switching it to client side viause client produces:
- error Error: Attempted to call bundle() from the server but bundle is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.
at Object.defineProperties.$$typeof.value (webpack-internal:///(rsc)/./node_modules/next/dist/build/webpack/loaders/next-flight-loader/module-proxy.js:151:23)
```
@NickGeneva Seems like you get multiple problems:
- Next.JS bundles each API route.
@remotion/bundlercannot be bundled because it calls Webpack and Webpack cannot be bundled with Webpack (limitation of its own). -> You can create a Remotion bundle outside of the API route and use it in the API route afterwards. There is no need to bundle more than once anyway. - The problem with client components seems like a false positive for me. This should be a pure backend routine, so it should not care about React components. -> App Router is experimental and known to have bugs. Maybe with Pages router you get a better experience.
Overall, the Next.js environment makes it complicated to execute server-side renders.
I updated https://www.remotion.dev/docs/miscellaneous/vercel-functions to mention the problems you encountered and recommending either Lambda or a pure Node.js environment.
Hi @NickGeneva, last year I was with the same use case to render a video dinamically with NextJS in server-side. I've used NextJS API Route to handle it. Probally this code need few reworks, but here is part of it (I just removed/changed some sensitive parts):
import type { NextApiRequest, NextApiResponse } from 'next';
import fs from 'fs';
import path from 'path';
import { bundle } from '@remotion/bundler';
import { getCompositions, renderMedia } from '@remotion/renderer';
type Data = Record<string, unknown>;
const cache = new Map<string, string>();
const renderingList = new Map<string, boolean>();
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
// Used to avoid double render with same params and allow background rendering
const showStatus = req.query?.status === '1';
const renderOnly = req.query?.render === '1';
const compositionId = 'Main';
const compositionPath = path.join(
__dirname,
'../../components/Video/index.tsx'
);
const videoProps = {
// Your custom data goes here, you can get it from an API or from request object
customData: {
username: '...',
// ...
},
useStaticAssets: true,
};
const sendFile = (file: string) => {
res.setHeader('content-type', 'video/mp4');
res.setHeader(
'Content-Disposition',
`attachment; filename="${videoProps.customData.username}.mp4"`
);
fs.createReadStream(file)
.pipe(res)
.on('close', () => {
res.end();
});
};
try {
const cacheKey = '...'; // Used to avoid double render with same params and allow background rendering
const videoFromCache = cache.get(cacheKey);
const isVideoRendering = renderingList.get(cacheKey);
if (showStatus) {
return res.status(200).json({
ready: !!videoFromCache,
rendering: !!isVideoRendering,
});
}
if (videoFromCache) {
sendFile(videoFromCache as string);
return;
}
renderingList.set(cacheKey, true);
if (renderOnly) {
res.status(204).end();
}
const bundleLocation = await bundle(compositionPath);
const comps = await getCompositions(bundleLocation, {
inputProps: videoProps,
});
const composition = comps.find((c) => c.id === compositionId);
if (!composition) {
throw new Error(`No video called ${compositionId}`);
}
const tmpDir = await fs.promises.mkdtemp(
path.join(__dirname, 'remotion-')
);
const finalOutput = path.join(tmpDir, `${videoProps.customData.username}-out.mp4`);
await renderMedia({
composition,
serveUrl: bundleLocation,
codec: 'h264',
outputLocation: finalOutput,
inputProps: videoProps,
envVariables: {
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || '',
},
});
cache.set(cacheKey, finalOutput);
renderingList.set(cacheKey, false);
if (!renderOnly) sendFile(finalOutput);
console.log('Video rendered and sent!');
} catch (err) {
console.error(err);
res.status(200).json({ error: true });
}
}
I didn't used Vercel's cloud for this, but maybe this code helps in any case @JonnyBurger.
Cheers
Hi @JonnyBurger
Thanks! I switched to page router and now I can get a video rendered server-side using a API call. A simple modified version of the example in the server-side rendering primitives docs works. Definitely a Next.JS app router issue.
Hi @pedrosodre
Wow ! Thanks so much for the sample code, this really helps a lot for understanding how to check the status of a given render thats running in the background. Greatly appreciate this! :)