with-mux-video: move to app router and update packages (#62297)
## App Router Moves the `with-mux-video` example to idiomatic App Router, with goodies like * server component data fetching * server actions * layouts * route groups * loading UI ## Mux Dependencies * @mux/mux-node 7 -> 8 * @mux/mux-player-react 1 -> 2 * @mux/upchunk + custom UI -> @mux/mux-uploader ## In other news... * Fleshed out the README * Updated imagery * Moved from styled jsx to tailwind and lightly updated styles ## Contributor Checklist * [x] The "examples guidelines" are followed from our contributing doc https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md * [x] Make sure the linting passes by running `pnpm build && pnpm lint`. See https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md --------- Co-authored-by: Sam Ko <sam@vercel.com>
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -30,25 +31,17 @@ 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
|
||||
```bash
|
||||
bunx create-next-app --example with-mux-video with-mux-video-app
|
||||
```
|
||||
|
||||
**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.
|
||||
### Step 2. Create an account in Mux
|
||||
|
||||
This example uses:
|
||||
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.
|
||||
|
||||
- [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.
|
||||
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.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Step 1. 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.
|
||||
|
||||
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.
|
||||
|
||||
### 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):
|
||||
|
||||
|
@ -56,12 +49,14 @@ Copy the `.env.local.example` file in this directory to `.env.local` (which will
|
|||
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)).
|
||||
|
||||
|
@ -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
examples/with-mux-video/app/(upload)/MuxUploader.tsx
Normal file
|
@ -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()} />;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return <div className="font-mono text-sm">{children}</div>;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export default function Loading() {
|
||||
return <div>Checking asset status...</div>;
|
||||
}
|
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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
examples/with-mux-video/app/(upload)/layout.tsx
Normal file
|
@ -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
examples/with-mux-video/app/(upload)/loading.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function Loading() {
|
||||
return <div>Preparing upload...</div>;
|
||||
}
|
60
examples/with-mux-video/app/(upload)/page.tsx
Normal file
|
@ -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
examples/with-mux-video/app/_components/Link.tsx
Normal file
|
@ -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;
|
BIN
examples/with-mux-video/app/apple-icon.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
2
examples/with-mux-video/app/constants.ts
Normal file
|
@ -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";
|
BIN
examples/with-mux-video/app/favicon.ico
Normal file
After Width: | Height: | Size: 625 B |
3
examples/with-mux-video/app/globals.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
1
examples/with-mux-video/app/icon.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="#FA50B5" fill-rule="evenodd" d="M16 0H0v16h16V0Zm-4.56 2.439A1.5 1.5 0 0 1 14 3.499V12.5a1.5 1.5 0 0 1-3 0V7.12L9.06 9.06a1.5 1.5 0 0 1-2.12 0L5 7.12v5.38a1.5 1.5 0 0 1-3 0v-9a1.5 1.5 0 0 1 2.56-1.061L8 5.879l3.44-3.44Zm.316 10.061a.744.744 0 1 0 1.488 0 .744.744 0 0 0-1.488 0Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 414 B |
57
examples/with-mux-video/app/layout.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import type { Metadata } from "next";
|
||||
import { DM_Sans, JetBrains_Mono } from "next/font/google";
|
||||
import Image from "next/image";
|
||||
|
||||
import { MUX_HOME_PAGE_URL } from "./constants";
|
||||
import MuxLogo from "./mux.svg";
|
||||
|
||||
import "./globals.css";
|
||||
|
||||
const dmSans = DM_Sans({ subsets: ["latin"], variable: "--sans" });
|
||||
const jetBrainsMono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--mono",
|
||||
});
|
||||
|
||||
const title = "Mux + Next.js";
|
||||
const description = "Upload and view a video with Mux and Next.js";
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: process.env.VERCEL_URL
|
||||
? new URL(`https://${process.env.VERCEL_URL}`)
|
||||
: new URL(`http://localhost:${process.env.PORT || 3000}`),
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description,
|
||||
card: "summary_large_image",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`min-h-screen flex flex-col font-sans ${dmSans.variable} ${jetBrainsMono.variable}`}
|
||||
>
|
||||
<main className="px-4 py-8 sm:px-8 lg:py-16 w-full max-w-2xl mx-auto">
|
||||
{children}
|
||||
</main>
|
||||
<footer className="mt-auto w-full h-24 border-t border-gray-500 flex items-center justify-center">
|
||||
Powered by
|
||||
<a href={MUX_HOME_PAGE_URL} target="_blank" rel="noopener noreferrer">
|
||||
<Image src={MuxLogo} alt="Mux" className="ml-2 w-16" />
|
||||
</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
examples/with-mux-video/app/opengraph-image.png
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
examples/with-mux-video/app/twitter-image.png
Normal file
After Width: | Height: | Size: 43 KiB |
3
examples/with-mux-video/app/v/[playbackId]/MuxPlayer.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
"use client";
|
||||
|
||||
export { default } from "@mux/mux-player-react";
|
122
examples/with-mux-video/app/v/[playbackId]/page.tsx
Normal file
|
@ -0,0 +1,122 @@
|
|||
import Link from "@/app/_components/Link";
|
||||
import MuxPlayer from "./MuxPlayer";
|
||||
import { MUX_HOME_PAGE_URL } from "@/app/constants";
|
||||
import Script from "next/script";
|
||||
|
||||
type Params = {
|
||||
playbackId: string;
|
||||
};
|
||||
|
||||
const title = "View this video created with Mux + Next.js";
|
||||
export const generateMetadata = ({ params }: { params: Params }) => ({
|
||||
title,
|
||||
description: undefined,
|
||||
openGraph: {
|
||||
title,
|
||||
images: [
|
||||
{
|
||||
url: `https://image.mux.com/${params.playbackId}/thumbnail.png?width=1200&height=630&fit_mode=pad`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
card: "summary_large_image",
|
||||
images: [
|
||||
{
|
||||
url: `https://image.mux.com/${params.playbackId}/thumbnail.png?width=1200&height=600&fit_mode=pad`,
|
||||
width: 1200,
|
||||
height: 600,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const Code = ({ children }: { children: React.ReactNode }) => (
|
||||
<code className="text-mono text-sm bg-red-500/10 px-1 py-0.5 rounded-sm">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
|
||||
// this function communicates with no external services and relies on no Node APIs
|
||||
// it's perfect for the edge
|
||||
export const runtime = "edge";
|
||||
|
||||
export default function Page({ params: { playbackId } }: { params: Params }) {
|
||||
return (
|
||||
<>
|
||||
<div className="px-8 py-4 mb-8 text-center bg-green-500/30 rounded-full">
|
||||
This video is ready for playback
|
||||
</div>
|
||||
<div className="bg-black aspect-video mb-8 -mx-4 flex">
|
||||
<MuxPlayer
|
||||
className="w-full"
|
||||
playbackId={playbackId}
|
||||
metadata={{ player_name: "with-mux-video" }}
|
||||
accentColor="rgb(220 38 38)"
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
Go <Link href="/">back home</Link> to upload another video.
|
||||
</p>
|
||||
<hr className="my-8 bg-gray-500" />
|
||||
<p className="mb-4">
|
||||
This video was uploaded and processed by{" "}
|
||||
<Link
|
||||
href={MUX_HOME_PAGE_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Mux
|
||||
</Link>
|
||||
. This page was rendered with{" "}
|
||||
<Link
|
||||
href="https://nextjs.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Next.js
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<p className="mb-4">
|
||||
Thanks to the Next.js{" "}
|
||||
<Link href="https://nextjs.org/docs/app/building-your-application/optimizing/metadata">
|
||||
Metadata API
|
||||
</Link>
|
||||
, this page is easily sharable on social and has an{" "}
|
||||
<Code>og:image</Code> thumbnail generated by Mux. Try clicking the
|
||||
Twitter button below to share:
|
||||
</p>
|
||||
<p className="mb-4 min-h-7">
|
||||
<Link
|
||||
className="twitter-share-button"
|
||||
data-size="large"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://twitter.com/intent/tweet?text=Check%20out%20the%20video%20I%20uploaded%20with%20Next.js%2C%20%40Vercel%2C%20and%20%40muxhq%20"
|
||||
>
|
||||
Tweet this
|
||||
</Link>
|
||||
</p>
|
||||
<Script
|
||||
strategy="lazyOnload"
|
||||
async
|
||||
src="https://platform.twitter.com/widgets.js"
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function Button({ children, ...otherProps }: ButtonProps) {
|
||||
return (
|
||||
<>
|
||||
<button {...otherProps}>{children}</button>
|
||||
<style jsx>{`
|
||||
button {
|
||||
padding: 16px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 22px;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
rgb(255, 61, 48),
|
||||
rgb(255, 43, 97)
|
||||
);
|
||||
color: white;
|
||||
line-height: 22px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
background-image: linear-gradient(
|
||||
to left,
|
||||
rgb(253, 95, 85),
|
||||
rgb(255, 85, 128)
|
||||
);
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
interface ErrorMessageProps {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export default function ErrorMessage({ message }: ErrorMessageProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="message">{message || "Unknown error"}</div>
|
||||
<style jsx>{`
|
||||
.message {
|
||||
color: #650303;
|
||||
background-color: #ffb2b2;
|
||||
padding: 10px 6px;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,195 +0,0 @@
|
|||
import Head from "next/head";
|
||||
import { MUX_HOME_PAGE_URL } from "../constants";
|
||||
|
||||
interface LayoutProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
metaTitle?: string;
|
||||
metaDescription?: string;
|
||||
image?: string;
|
||||
children: React.ReactNode;
|
||||
loadTwitterWidget?: boolean;
|
||||
}
|
||||
|
||||
export default function Layout({
|
||||
title,
|
||||
description,
|
||||
metaTitle = "Mux + Next.js",
|
||||
metaDescription,
|
||||
image = "https://with-mux-video.vercel.app/mux-nextjs-og-image.png",
|
||||
children,
|
||||
loadTwitterWidget,
|
||||
}: LayoutProps) {
|
||||
return (
|
||||
<div className="container">
|
||||
<Head>
|
||||
<title>Mux + Next.js</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
{metaTitle && <meta property="og:title" content={metaTitle} />}
|
||||
{metaTitle && <meta property="twitter:title" content={metaTitle} />}
|
||||
{metaDescription && (
|
||||
<meta property="og:description" content={description} />
|
||||
)}
|
||||
{metaDescription && (
|
||||
<meta property="twitter:description" content={description} />
|
||||
)}
|
||||
{image && <meta property="og:image" content={image} />}
|
||||
{image && (
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
)}
|
||||
{image && <meta property="twitter:image" content={image} />}
|
||||
{loadTwitterWidget && (
|
||||
<script
|
||||
type="text/javascript"
|
||||
async
|
||||
src="https://platform.twitter.com/widgets.js"
|
||||
></script>
|
||||
)}
|
||||
</Head>
|
||||
|
||||
<main>
|
||||
<h1 className="title">{title}</h1>
|
||||
<p className="description">{description}</p>
|
||||
<div className="grid">{children}</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<a href={MUX_HOME_PAGE_URL} target="_blank" rel="noopener noreferrer">
|
||||
Powered by <img src="/mux.svg" alt="Mux Logo" className="logo" />
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
<style jsx>{`
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
padding: 0 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1rem 0 5rem 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
border-top: 1px solid #eaeaea;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
footer img {
|
||||
margin-left: 0.5rem;
|
||||
width: 71px;
|
||||
}
|
||||
|
||||
footer a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title a {
|
||||
color: #0070f3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title a:hover,
|
||||
.title a:focus,
|
||||
.title a:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
line-height: 1.15;
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.title,
|
||||
.description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
line-height: 1.5;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #fafafa;
|
||||
border-radius: 5px;
|
||||
padding: 0.75rem;
|
||||
font-size: 1.1rem;
|
||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono,
|
||||
DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
max-width: 800px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.grid {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
.title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
footer {
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<style jsx global>{`
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
||||
Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #ff2b61;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.4rem;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
interface SpinnerProps {
|
||||
size?: Number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export default function Spinner({ size = 6, color = "#999" }: SpinnerProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="spinner" />
|
||||
<style jsx>{`
|
||||
.spinner,
|
||||
.spinner:after {
|
||||
border-radius: 50%;
|
||||
width: 10em;
|
||||
height: 10em;
|
||||
}
|
||||
.spinner {
|
||||
margin: 60px auto;
|
||||
font-size: ${size}px;
|
||||
position: relative;
|
||||
text-indent: -9999em;
|
||||
border-top: 1.1em solid ${color};
|
||||
border-right: 1.1em solid ${color};
|
||||
border-bottom: 1.1em solid ${color};
|
||||
border-left: 1.1em solid transparent;
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
-webkit-animation: load8 1.1s infinite linear;
|
||||
animation: load8 1.1s infinite linear;
|
||||
}
|
||||
@-webkit-keyframes load8 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes load8 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import Router from "next/router";
|
||||
import * as UpChunk from "@mux/upchunk";
|
||||
import useSwr from "swr";
|
||||
import Button from "./button";
|
||||
import Spinner from "./spinner";
|
||||
import ErrorMessage from "./error-message";
|
||||
|
||||
const fetcher = (url: string) => {
|
||||
return fetch(url).then((res) => res.json());
|
||||
};
|
||||
|
||||
const UploadForm = () => {
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [isPreparing, setIsPreparing] = useState(false);
|
||||
const [uploadId, setUploadId] = useState(null);
|
||||
const [progress, setProgress] = useState<Number | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { data, error } = useSwr(
|
||||
() => (isPreparing ? `/api/upload/${uploadId}` : null),
|
||||
fetcher,
|
||||
{ refreshInterval: 5000 },
|
||||
);
|
||||
|
||||
const upload = data && data.upload;
|
||||
|
||||
useEffect(() => {
|
||||
if (upload && upload.asset_id) {
|
||||
Router.push({
|
||||
pathname: `/asset/${upload.asset_id}`,
|
||||
});
|
||||
}
|
||||
}, [upload]);
|
||||
|
||||
if (error) return <ErrorMessage message="Error fetching api" />;
|
||||
if (data && data.error) return <ErrorMessage message={data.error} />;
|
||||
|
||||
const createUpload = async () => {
|
||||
try {
|
||||
return fetch("/api/upload", {
|
||||
method: "POST",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ id, url }) => {
|
||||
setUploadId(id);
|
||||
return url;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error in createUpload", e);
|
||||
setErrorMessage("Error creating upload");
|
||||
}
|
||||
};
|
||||
|
||||
const startUpload = () => {
|
||||
setIsUploading(true);
|
||||
|
||||
const files = inputRef.current?.files;
|
||||
if (!files) {
|
||||
setErrorMessage("An unexpected issue occurred");
|
||||
return;
|
||||
}
|
||||
|
||||
const upload = UpChunk.createUpload({
|
||||
endpoint: createUpload,
|
||||
file: files[0],
|
||||
});
|
||||
|
||||
upload.on("error", (err: any) => {
|
||||
setErrorMessage(err.detail.message);
|
||||
});
|
||||
|
||||
upload.on("progress", (progress: any) => {
|
||||
setProgress(Math.floor(progress.detail));
|
||||
});
|
||||
|
||||
upload.on("success", () => {
|
||||
setIsPreparing(true);
|
||||
});
|
||||
};
|
||||
|
||||
if (errorMessage) return <ErrorMessage message={errorMessage} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container">
|
||||
{isUploading ? (
|
||||
<>
|
||||
{isPreparing ? (
|
||||
<div>Preparing..</div>
|
||||
) : (
|
||||
<div>Uploading...{progress ? `${progress}%` : ""}</div>
|
||||
)}
|
||||
<Spinner />
|
||||
</>
|
||||
) : (
|
||||
<label>
|
||||
<Button type="button" onClick={() => inputRef.current?.click()}>
|
||||
Select a video file
|
||||
</Button>
|
||||
<input type="file" onChange={startUpload} ref={inputRef} />
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<style jsx>{`
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadForm;
|
|
@ -1,72 +0,0 @@
|
|||
import Layout from "./layout";
|
||||
import { MUX_HOME_PAGE_URL } from "../constants";
|
||||
|
||||
interface UploadPageProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function UploadPage({ children }: UploadPageProps) {
|
||||
return (
|
||||
<Layout
|
||||
title="Welcome to Mux + Next.js"
|
||||
description="Get started by uploading a video"
|
||||
>
|
||||
<div className="wrapper">
|
||||
<div className="about-mux">
|
||||
<p>
|
||||
<a
|
||||
href={MUX_HOME_PAGE_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Mux
|
||||
</a>{" "}
|
||||
provides APIs for developers working with video. This example is
|
||||
useful if you want to build:
|
||||
</p>
|
||||
<ul>
|
||||
<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>
|
||||
Uploading a video uses the Mux{" "}
|
||||
<a href="https://docs.mux.com/docs/direct-upload">
|
||||
direct upload API
|
||||
</a>
|
||||
. 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,{" "}
|
||||
<a
|
||||
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
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
<div className="children">{children}</div>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
.about-mux {
|
||||
padding: 0 1rem 1.5rem 1rem;
|
||||
max-width: 600px;
|
||||
}
|
||||
.about-mux {
|
||||
line-height: 1.4rem;
|
||||
}
|
||||
.children {
|
||||
text-align: center;
|
||||
min-height: 230px;
|
||||
}
|
||||
`}</style>
|
||||
</Layout>
|
||||
);
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export const MUX_HOME_PAGE_URL =
|
||||
"https://mux.com?utm_source=create-next-app&utm_medium=with-mux-video&utm_campaign=create-next-app";
|
4
examples/with-mux-video/next.config.mjs
Normal file
|
@ -0,0 +1,4 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
|
@ -6,18 +6,21 @@
|
|||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mux/mux-node": "^7.0.0",
|
||||
"@mux/mux-player-react": "^1",
|
||||
"@mux/upchunk": "^3",
|
||||
"@mux/mux-node": "^8",
|
||||
"@mux/mux-player-react": "^2",
|
||||
"@mux/mux-uploader-react": "^1.0.0-beta.15",
|
||||
"next": "latest",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"swr": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.10",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"typescript": "^4.9.3"
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"typescript": "^5",
|
||||
"autoprefixer": "^10",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import Mux from "@mux/mux-node";
|
||||
const { Video } = new Mux();
|
||||
|
||||
export default async function assetHandler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
case "GET":
|
||||
try {
|
||||
const asset = await Video.Assets.get(req.query.id as string);
|
||||
res.json({
|
||||
asset: {
|
||||
id: asset.id,
|
||||
status: asset.status,
|
||||
errors: asset.errors,
|
||||
playback_id: asset.playback_ids![0].id,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Request error", e);
|
||||
res.status(500).json({ error: "Error getting upload/asset" });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
res.setHeader("Allow", ["GET"]);
|
||||
res.status(405).end(`Method ${method} Not Allowed`);
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import Mux from "@mux/mux-node";
|
||||
const { Video } = new Mux();
|
||||
|
||||
export default async function uploadHandler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
case "POST":
|
||||
try {
|
||||
const upload = await Video.Uploads.create({
|
||||
new_asset_settings: { playback_policy: "public" },
|
||||
cors_origin: "*",
|
||||
});
|
||||
res.json({
|
||||
id: upload.id,
|
||||
url: upload.url,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Request error", e);
|
||||
res.status(500).json({ error: "Error creating upload" });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
res.setHeader("Allow", ["POST"]);
|
||||
res.status(405).end(`Method ${method} Not Allowed`);
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import Mux from "@mux/mux-node";
|
||||
const { Video } = new Mux();
|
||||
|
||||
export default async function uploadHandler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
case "GET":
|
||||
try {
|
||||
const upload = await Video.Uploads.get(req.query.id as string);
|
||||
res.json({
|
||||
upload: {
|
||||
status: upload.status,
|
||||
url: upload.url,
|
||||
asset_id: upload.asset_id,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Request error", e);
|
||||
res.status(500).json({ error: "Error getting upload/asset" });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
res.setHeader("Allow", ["GET"]);
|
||||
res.status(405).end(`Method ${method} Not Allowed`);
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
import { useEffect } from "react";
|
||||
import Router, { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import useSwr from "swr";
|
||||
import Spinner from "../../components/spinner";
|
||||
import ErrorMessage from "../../components/error-message";
|
||||
import UploadPage from "../../components/upload-page";
|
||||
|
||||
const fetcher = (url: string) => {
|
||||
return fetch(url).then((res) => res.json());
|
||||
};
|
||||
|
||||
export default function Asset() {
|
||||
const router = useRouter();
|
||||
|
||||
const { data, error } = useSwr(
|
||||
() => (router.query.id ? `/api/asset/${router.query.id}` : null),
|
||||
fetcher,
|
||||
{ refreshInterval: 5000 },
|
||||
);
|
||||
|
||||
const asset = data && data.asset;
|
||||
|
||||
useEffect(() => {
|
||||
if (asset && asset.playback_id && asset.status === "ready") {
|
||||
Router.push(`/v/${asset.playback_id}`);
|
||||
}
|
||||
}, [asset]);
|
||||
|
||||
let errorMessage: string = "";
|
||||
|
||||
if (error) {
|
||||
errorMessage = "Error fetching api";
|
||||
}
|
||||
|
||||
if (data && data.error) {
|
||||
errorMessage = data.error;
|
||||
}
|
||||
|
||||
if (asset && asset.status === "errored") {
|
||||
const message = asset.errors && asset.errors.messages[0];
|
||||
errorMessage = `Error creating this asset: ${message}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<UploadPage>
|
||||
{errorMessage ? (
|
||||
<>
|
||||
<ErrorMessage message={errorMessage} />
|
||||
<p>
|
||||
Go <Link href="/">back home</Link> to upload another video.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div>Preparing...</div>
|
||||
<Spinner />
|
||||
</>
|
||||
)}
|
||||
</UploadPage>
|
||||
);
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import UploadForm from "../components/upload-form";
|
||||
import UploadPage from "../components/upload-page";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<UploadPage>
|
||||
<UploadForm />
|
||||
</UploadPage>
|
||||
);
|
||||
}
|
|
@ -1,144 +0,0 @@
|
|||
import type {
|
||||
InferGetStaticPropsType,
|
||||
GetStaticProps,
|
||||
GetStaticPaths,
|
||||
} from "next";
|
||||
import MuxPlayer from "@mux/mux-player-react";
|
||||
import Link from "next/link";
|
||||
import Layout from "../../components/layout";
|
||||
import Spinner from "../../components/spinner";
|
||||
import { MUX_HOME_PAGE_URL } from "../../constants";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
type Params = {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ params }) => {
|
||||
const { id: playbackId } = params as Params;
|
||||
const poster = `https://image.mux.com/${playbackId}/thumbnail.png`;
|
||||
|
||||
return { props: { playbackId, poster } };
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = () => {
|
||||
return {
|
||||
paths: [],
|
||||
fallback: true,
|
||||
};
|
||||
};
|
||||
|
||||
type CodeProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const Code = ({ children }: CodeProps) => (
|
||||
<>
|
||||
<span className="code">{children}</span>
|
||||
<style jsx>{`
|
||||
.code {
|
||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono,
|
||||
DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace,
|
||||
serif;
|
||||
color: #ff2b61;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
|
||||
export default function Playback({
|
||||
playbackId,
|
||||
poster,
|
||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const router = useRouter();
|
||||
|
||||
if (router.isFallback) {
|
||||
return (
|
||||
<Layout>
|
||||
<Spinner />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
metaTitle="View this video created with Mux + Next.js"
|
||||
image={poster}
|
||||
loadTwitterWidget
|
||||
>
|
||||
<div className="flash-message">This video is ready for playback</div>
|
||||
<MuxPlayer
|
||||
style={{ width: "100%" }}
|
||||
playbackId={playbackId}
|
||||
metadata={{ player_name: "with-mux-video" }}
|
||||
/>
|
||||
<p>
|
||||
Go{" "}
|
||||
<Link href="/" legacyBehavior>
|
||||
<a>back home</a>
|
||||
</Link>{" "}
|
||||
to upload another video.
|
||||
</p>
|
||||
<div className="about-playback">
|
||||
<p>
|
||||
This video was uploaded and processed by{" "}
|
||||
<a href={MUX_HOME_PAGE_URL} target="_blank" rel="noopener noreferrer">
|
||||
Mux
|
||||
</a>
|
||||
. This page was pre-rendered with{" "}
|
||||
<a
|
||||
href="https://nextjs.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Next.js
|
||||
</a>{" "}
|
||||
using <Code>`getStaticPaths`</Code> and <Code>`getStaticProps`</Code>.
|
||||
</p>
|
||||
<p>
|
||||
Thanks to pre-rendering this page is easily sharable on social and has
|
||||
an <Code>`og:image`</Code> thumbnail generated by Mux. Try clicking
|
||||
the Twitter button below to share:
|
||||
</p>
|
||||
<div className="share-button">
|
||||
<a
|
||||
className="twitter-share-button"
|
||||
data-size="large"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={`https://twitter.com/intent/tweet?text=Check%20out%20the%20video%20I%20uploaded%20with%20Next.js%2C%20%40Vercel%2C%20and%20%40muxhq%20`}
|
||||
>
|
||||
Tweet this
|
||||
</a>
|
||||
</div>
|
||||
<p>
|
||||
To learn more,{" "}
|
||||
<a
|
||||
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
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
.flash-message {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
background-color: #c1dcc1;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
.share-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 40px 0;
|
||||
}
|
||||
`}</style>
|
||||
</Layout>
|
||||
);
|
||||
}
|
6
examples/with-mux-video/postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 446 KiB |
12
examples/with-mux-video/tailwind.config.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./app/**/*.{js,ts,jsx,tsx,mdx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
fontFamily: {
|
||||
sans: ["var(--sans)", "sans-serif"],
|
||||
mono: ["var(--mono)", "monospace"],
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
|
@ -1,20 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|