Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

with-mux-video: move to app router and update packages #62297

Merged
merged 20 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 29 additions & 21 deletions examples/with-mux-video/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

This example uses Mux Video, an API-first platform for video. The example features video uploading and playback in a Next.js application.

## Demo
## Try it out

### [https://with-mux-video.vercel.app/](https://with-mux-video.vercel.app/)

### This project was used to create [stream.new](https://stream.new/)
- [https://with-mux-video.vercel.app/](https://with-mux-video.vercel.app/)
- This project was used to create [stream.new](https://stream.new/)

## Deploy your own

Expand All @@ -16,6 +15,8 @@ Deploy the example using [Vercel](https://vercel.com/home):

## How to use

### Step 1. Create a Next app with this example

Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:

```bash
Expand All @@ -30,38 +31,32 @@ yarn create next-app --example with-mux-video with-mux-video-app
pnpm create next-app --example with-mux-video with-mux-video-app
```

## Note

**Important:** When creating uploads, this demo sets `cors_origin: "*"` in the [`pages/api/upload.js`](pages/api/upload.js) file. For extra security, you should update this value to be something like `cors_origin: 'https://your-app.com'`, to restrict uploads to only be allowed from your application.

This example uses:

- [SWR](https://swr.vercel.app/) — dynamically changing the `refreshInterval` depending on if the client should be polling for updates or not
- [`/pages/api`](pages/api) routes — a couple endpoints for making authenticated requests to the Mux API.
- Dynamic routes using [`getStaticPaths` and `fallback: true`](https://nextjs.org/docs/basic-features/data-fetching/get-static-paths), as well as dynamic API routes.

## Configuration
```bash
bunx create-next-app --example with-mux-video with-mux-video-app
```

### Step 1. Create an account in Mux
### Step 2. Create an account in Mux

All you need to set this up is a [Mux account](https://mux.com). You can sign up for free and pricing is pay-as-you-go. There are no upfront charges, you get billed monthly only for what you use.
All you need to run this example is a [Mux account](https://www.mux.com?utm_source=create-next-app&utm_medium=with-mux-video&utm_campaign=create-next-app). You can sign up for free. There are no upfront charges -- you get billed monthly only for what you use.

Without entering a credit card on your Mux account all videos are in “test mode” which means they are watermarked and clipped to 10 seconds. If you enter a credit card all limitations are lifted and you get \$20 of free credit. The free credit should be plenty for you to test out and play around with everything before you are charged.
Without entering a credit card on your Mux account all videos are in “test mode” which means they are watermarked and clipped to 10 seconds. If you enter a credit card all limitations are lifted and you get \$20 of free credit. The free credit should be plenty for you to test out and play around with everything.

### Step 2. Set up environment variables
### Step 3. Set up environment variables

Copy the `.env.local.example` file in this directory to `.env.local` (which will be ignored by Git):

```bash
cp .env.local.example .env.local
```

Then, go to the [settings page](https://dashboard.mux.com/settings/access-tokens) in your Mux dashboard set each variable on `.env.local`, get a new **API Access Token** and set each variable in `.env.local`:
Then, go to the [settings page](https://dashboard.mux.com/settings/access-tokens) in your Mux dashboard, get a new **API Access Token**. Use that token to set the variables in `.env.local`:

- `MUX_TOKEN_ID` should be the `TOKEN ID` of your new token
- `MUX_TOKEN_SECRET` should be `TOKEN SECRET`

### Step 3. Deploy on Vercel
At this point, you're good to `npm run dev` or `yarn dev` or `pnpm dev`. However, if you want to deploy, read on:

### Step 4. Deploy on Vercel

You can deploy this app to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).

Expand All @@ -75,3 +70,16 @@ vercel secrets add next_example_mux_token_secret <MUX_TOKEN_SECRET>
```

Then push the project to GitHub/GitLab/Bitbucket and [import to Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) to deploy.

## Notes

### Preparing for Production

**Important:** When creating uploads, this demo sets `cors_origin: "*"` in the [`app/(upload)/page.tsx`](<app/(upload)/page.tsx>) file. For extra security, you should update this value to be something like `cors_origin: 'https://your-app.com'`, to restrict uploads to only be allowed from your application.

### How it works

1. Users land on the home page, `app/(upload)/page.tsx`. The Mux [Direct Uploads API](https://docs.mux.com/api-reference#video/tag/direct-uploads?utm_source=create-next-app&utm_medium=with-mux-video&utm_campaign=create-next-app) provides an endpoint to [Mux Uploader React](https://docs.mux.com/guides/mux-uploader?utm_source=create-next-app&utm_medium=with-mux-video&utm_campaign=create-next-app).
1. The user uploads a video with Mux Uploader. When their upload is complete, Mux Uploader calls a [server action](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations) that redirects to...
1. `app/(upload)/asset/[assetId]/page.tsx`, which polls the [Asset API](https://docs.mux.com/api-reference#video/tag/assets?utm_source=create-next-app&utm_medium=with-mux-video&utm_campaign=create-next-app) via server action, waiting for the asset to be ready. Once the asset is ready, it redirects to...
1. `app/v/[assetId]/page.tsx`, where users can watch their video using [Mux Player React](https://docs.mux.com/guides/mux-player-web?utm_source=create-next-app&utm_medium=with-mux-video&utm_campaign=create-next-app). This page uses the [Mux Image API](https://docs.mux.com/guides/get-images-from-a-video) and the [Next.js Metadata API](https://nextjs.org/docs/app/building-your-application/optimizing/metadata) to provide an og images tailored to each video.
13 changes: 13 additions & 0 deletions examples/with-mux-video/app/(upload)/MuxUploader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"use client";

import { type ComponentPropsWithoutRef } from "react";

import LibMuxUploader from "@mux/mux-uploader-react";

type Props = {
endpoint: ComponentPropsWithoutRef<typeof LibMuxUploader>["endpoint"];
onSuccess: () => void;
};
export default function MuxUploader({ endpoint, onSuccess }: Props) {
return <LibMuxUploader endpoint={endpoint} onSuccess={() => onSuccess()} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use client";

import { useEffect, useState } from "react";

import { Status } from "./types";
import Link from "@/app/_components/Link";

const Oops = () => (
<p>
This is awkward. Let's <Link href="/">refresh</Link> and try again.
</p>
);

type Props = {
initialStatus: Status;
checkAssetStatus: () => Promise<Status>;
};
export default function AssetStatusPoll({
initialStatus,
checkAssetStatus,
}: Props) {
const [{ status, errors }, setStatus] = useState<Status>(() => initialStatus);

useEffect(() => {
const poll = async () => setStatus(await checkAssetStatus());
const interval = setInterval(poll, 1000);
return () => clearInterval(interval);
}, [checkAssetStatus]);

switch (status) {
case "preparing":
return <p className="animate-pulse">Asset is preparing...</p>;
case "errored":
return (
<div>
<p className="mb-4">Asset encountered an error.</p>
{Array.isArray(errors) && (
<ul className="mb-4">
{errors.map((error, key) => (
<li key={key}>{JSON.stringify(error)}</li>
))}
</ul>
)}
<Oops />
</div>
);
case "ready":
return (
<div>
<p className="mb-4">
Asset is ready. The app really should've redirected you to it by
now.
</p>
<Oops />
</div>
);
default:
return (
<div>
<p className="mb-4">Asset is in an unknown state.</p>
<pre className="mb-4">{JSON.stringify({ status, errors })}</pre>
<Oops />
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Layout({ children }: { children: React.ReactNode }) {
return <div className="font-mono text-sm">{children}</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Loading() {
return <div>Checking asset status...</div>;
}
51 changes: 51 additions & 0 deletions examples/with-mux-video/app/(upload)/asset/[assetId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Mux from "@mux/mux-node";
import { redirect } from "next/navigation";
import { Status } from "./types";
import AssetStatusPoll from "./AssetStatusPoll";

// reads MUX_TOKEN_ID and MUX_TOKEN_SECRET from your environment
const mux = new Mux();

const checkAssetStatus = async (assetId: string): Promise<Status> => {
const asset = await mux.video.assets.retrieve(assetId);

// if the asset is ready and it has a public playback ID,
// (which it should, considering the upload settings we used)
// redirect to its playback page
if (asset.status === "ready") {
const playbackIds = asset.playback_ids;
if (Array.isArray(playbackIds)) {
const playbackId = playbackIds.find((id) => id.policy === "public");
if (playbackId) {
redirect(`/v/${playbackId.id}`);
}
}
}

return {
status: asset.status,
errors: asset.errors,
};
};

// For better performance, we could cache and use a Mux webhook to invalidate the cache.
// https://docs.mux.com/guides/listen-for-webhooks
// For this example, calling the Mux API on each request and then polling is sufficient.
export const dynamic = "force-dynamic";

export default async function Page({
params: { assetId },
}: {
params: { assetId: string };
}) {
const initialStatus = await checkAssetStatus(assetId);
return (
<AssetStatusPoll
initialStatus={initialStatus}
checkAssetStatus={async () => {
"use server";
return await checkAssetStatus(assetId);
}}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type Mux from "@mux/mux-node";

export type Status = {
status: Mux.Video.Assets.Asset["status"];
errors: Mux.Video.Assets.Asset["errors"];
};
57 changes: 57 additions & 0 deletions examples/with-mux-video/app/(upload)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Link from "../_components/Link";
import { MUX_HOME_PAGE_URL } from "../constants";

export default function Layout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<>
<header className="mb-8">
<h1 className="font-bold text-4xl lg:text-5xl mb-2">
Welcome to Mux + Next.js
</h1>
<p className="italic">Get started by uploading a video</p>
</header>
<p className="mb-4">
<Link
href={MUX_HOME_PAGE_URL}
target="_blank"
rel="noopener noreferrer"
>
Mux
</Link>{" "}
provides APIs for developers working with video.
<br />
This example is useful if you want to build:
</p>
<ul className="list-disc pl-8 mb-4">
<li>A video on demand service like Youtube or Netflix</li>
<li>
A platform that supports user uploaded videos like TikTok or Instagram
</li>
<li>Video into your custom CMS</li>
</ul>
<p className="mb-4">
Uploading a video uses the Mux{" "}
<Link href="https://docs.mux.com/docs/direct-upload">
direct upload API
</Link>
. When the upload is complete your video will be processed by Mux and
available for playback on a sharable URL.
</p>
<p>
To learn more,{" "}
<Link
href="https://github.com/vercel/next.js/tree/canary/examples/with-mux-video"
target="_blank"
rel="noopener noreferrer"
>
check out the source code on GitHub
</Link>
.
</p>
<hr className="my-8 bg-gray-500" />
{children}
</>
);
}
3 changes: 3 additions & 0 deletions examples/with-mux-video/app/(upload)/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Loading() {
return <div>Preparing upload...</div>;
}
60 changes: 60 additions & 0 deletions examples/with-mux-video/app/(upload)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Mux from "@mux/mux-node";
import MuxUploader from "./MuxUploader";
import { redirect } from "next/navigation";

// reads MUX_TOKEN_ID and MUX_TOKEN_SECRET from your environment
const mux = new Mux();

const createUpload = async () => {
const upload = await mux.video.uploads.create({
new_asset_settings: {
playback_policy: ["public"],
encoding_tier: "baseline",
},
// in production, you'll want to change this origin to your-domain.com
cors_origin: "*",
});

return upload;
};

const waitForThreeSeconds = () =>
new Promise((resolve) => setTimeout(resolve, 3000));

const redirectToAsset = async (uploadId: string) => {
let attempts = 0;
while (attempts <= 10) {
const upload = await mux.video.uploads.retrieve(uploadId);
if (upload.asset_id) {
redirect(`/asset/${upload.asset_id}`);
} else {
// while onSuccess is a strong indicator that Mux has received the file
// and created the asset, this isn't a guarantee.
// In production, you might listen for the video.upload.asset_created webhook
// https://docs.mux.com/guides/listen-for-webhooks
// To keep things simple here,
// we'll just poll the API at an interval for a few seconds.
await waitForThreeSeconds();
attempts++;
}
}
throw new Error("No asset_id found for upload");
};

// since we want to create a new upload for each visitor,
// we disable caching
export const dynamic = "force-dynamic";

export default async function Page() {
const upload = await createUpload();

return (
<MuxUploader
onSuccess={async () => {
"use server";
await redirectToAsset(upload.id);
}}
endpoint={upload.url}
/>
);
}
14 changes: 14 additions & 0 deletions examples/with-mux-video/app/_components/Link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import LibLink from "next/link";

/**
*
* @param className this component does not merge className with the default classes -- it only appends -- so beware of duplicates
*/
const Link = ({ className, ...rest }: React.ComponentProps<typeof LibLink>) => (
<LibLink
className={`underline hover:no-underline focus-visible:no-underline text-red-600 ${className}`}
{...rest}
/>
);

export default Link;
Binary file added examples/with-mux-video/app/apple-icon.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions examples/with-mux-video/app/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const MUX_HOME_PAGE_URL =
"https://www.mux.com?utm_source=create-next-app&utm_medium=with-mux-video&utm_campaign=create-next-app";
Binary file added examples/with-mux-video/app/favicon.ico
Binary file not shown.
3 changes: 3 additions & 0 deletions examples/with-mux-video/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
1 change: 1 addition & 0 deletions examples/with-mux-video/app/icon.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.