remotion icon indicating copy to clipboard operation
remotion copied to clipboard

Example Next.JS app and docs for rendering on Next.JS

Open JonnyBurger opened this issue 3 years ago • 3 comments

After 3.0, have a more thorough example with Remotion + Player + SSR, and also have some docs about it. I'll take this one.

JonnyBurger avatar Jan 31 '22 09:01 JonnyBurger

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.

tomByrer avatar Feb 01 '22 02:02 tomByrer

Taking this template as a starter: https://github.com/karelnagel/remotion-next-example

JonnyBurger avatar Oct 11 '22 15:10 JonnyBurger

Not officially released as an official template, but https://github.com/remotion-dev/template-next is a good start

JonnyBurger avatar Dec 27 '22 11:12 JonnyBurger

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!

NickGeneva avatar Aug 07 '23 00:08 NickGeneva

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

JonnyBurger avatar Aug 07 '23 07:08 JonnyBurger

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!

NickGeneva avatar Aug 08 '23 02:08 NickGeneva

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 avatar Aug 08 '23 07:08 NickGeneva

@NickGeneva Seems like you get multiple problems:

  • Next.JS bundles each API route. @remotion/bundler cannot 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.

JonnyBurger avatar Aug 08 '23 08:08 JonnyBurger

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.

JonnyBurger avatar Aug 08 '23 08:08 JonnyBurger

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

pedrosodre avatar Aug 08 '23 23:08 pedrosodre

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! :)

NickGeneva avatar Aug 09 '23 06:08 NickGeneva