-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Next.js and Remix.js examples (#213)
Co-authored-by: Dave Kiss <[email protected]>
- Loading branch information
Showing
20 changed files
with
476 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Check out our full example in the official Next.js Examples repository: [with-mux-video](https://github.com/vercel/next.js/tree/canary/examples/with-mux-video) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
MUX_TOKEN_ID= | ||
MUX_TOKEN_SECRET= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/** @type {import('eslint').Linter.Config} */ | ||
module.exports = { | ||
extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
node_modules | ||
|
||
/.cache | ||
/build | ||
/public/build | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
# Mux Video | ||
|
||
> [!NOTE] | ||
> This example will be removed from this repository once it is accepted and merged into the official Remix examples: [remix-run/examples#441](https://github.com/remix-run/examples/pull/441). | ||
This example features video uploading and playback in a Remix.js application. | ||
|
||
This example is useful if you want to build a platform that supports user-uploaded videos. For example: | ||
|
||
- Enabling user profile videos | ||
- Accepting videos for a video contest promotion | ||
- Allowing customers to upload screencasts that help with troubleshooting a bug | ||
- Or even the next Youtube, TikTok, or Instagram | ||
|
||
## Preview | ||
|
||
Open this example on [CodeSandbox](https://codesandbox.com): | ||
|
||
[![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/remix-run/examples/tree/main/mux-video) | ||
|
||
## How to use | ||
|
||
### Step 1. Create an account in Mux | ||
|
||
All you need to run this example is a [Mux account](https://www.mux.com?utm_source=remix-examples&utm_medium=mux-video&utm_campaign=remix-examples). 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. | ||
|
||
### Step 2. Set up environment variables | ||
|
||
Copy the `.env.example` file in this directory to `.env` (which will be ignored by Git): | ||
|
||
```bash | ||
cp .env.example .env | ||
``` | ||
|
||
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` | ||
|
||
At this point, you're good to `npm run dev`. | ||
|
||
## How it works | ||
|
||
Uploading and viewing a video takes four steps: | ||
|
||
1. **Upload a video**: Use the Mux [Direct Uploads API](https://docs.mux.com/api-reference#video/tag/direct-uploads?utm_source=remix-examples&utm_medium=mux-video&utm_campaign=remix-examples) to create an endpoint for [Mux Uploader React](https://docs.mux.com/guides/mux-uploader?utm_source=remix-examples&utm_medium=mux-video&utm_campaign=remix-examples). The user can then use Mux Uploader to upload a video. | ||
1. **Exchange the `upload.id` for an `asset.id`**: Once the upload is complete, it will have a Mux asset associated with it. We can use the [Direct Uploads API](https://docs.mux.com/api-reference#video/tag/direct-uploads?utm_source=remix-examples&utm_medium=mux-video&utm_campaign=remix-examples) to check for that asset. | ||
1. **Use the `asset.id` to check if the asset is ready** by polling the [Asset API](https://docs.mux.com/api-reference#video/tag/assets?utm_source=remix-examples&utm_medium=mux-video&utm_campaign=remix-examples) | ||
1. **Play back the video with [Mux Player React](https://docs.mux.com/guides/mux-player-web?utm_source=remix-examples&utm_medium=mux-video&utm_campaign=remix-examples)** (on a page that uses the [Mux Image API](https://docs.mux.com/guides/get-images-from-a-video) to provide og images) | ||
|
||
These steps correspond to the following routes: | ||
|
||
1. [`_index.tsx`](app/routes/_index.tsx) creates the upload in a loader, and exchanges the `upload.id` for an `asset.id` in an action which redirects to... | ||
2. [`status.$assetId.tsx`](app/routes/status.$assetId.tsx) polls the Mux API to see if the asset is ready. When it is, we redirect to... | ||
3. [`playback.$playbackId.tsx`](app/routes/playback.$playbackId.tsx) plays the video. | ||
|
||
## Preparing for Production | ||
|
||
### Set the `cors_origin` | ||
|
||
When creating uploads, this demo sets `cors_origin: "*"` in the [`app/routes/_index.tsx`](app/routes/_index.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. | ||
|
||
### Consider webhooks | ||
|
||
In this example, we poll the Mux API to see if our asset is ready. In production, you'll likely have a database where you can store the `upload.id` and `asset.id`, and you can use [Mux Webhooks](https://docs.mux.com/guides/listen-for-webhooks) to get notified when your upload is complete, and when your asset is ready. | ||
|
||
See [`app/routes/mux.webhook.ts`](app/routes/mux.webhook.ts) for an example of how you might handle a Mux webhook. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { Link as LibLink } from "@remix-run/react"; | ||
|
||
/** | ||
* | ||
* @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-blue-600 ${className}`} | ||
{...rest} | ||
/> | ||
); | ||
|
||
export default Link; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import Mux from "@mux/mux-node"; | ||
|
||
const mux = new Mux(); | ||
|
||
export default mux; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import type { MetaFunction, LinksFunction } from "@remix-run/node"; | ||
import { | ||
Links, | ||
LiveReload, | ||
Meta, | ||
Outlet, | ||
Scripts, | ||
ScrollRestoration, | ||
} from "@remix-run/react"; | ||
|
||
import styles from "./tailwind.css"; | ||
|
||
export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }]; | ||
|
||
export const meta: MetaFunction = () => ({ | ||
charset: "utf-8", | ||
title: "New Remix App", | ||
viewport: "width=device-width,initial-scale=1", | ||
}); | ||
|
||
export default function App() { | ||
return ( | ||
<html lang="en"> | ||
<head> | ||
<Meta /> | ||
<Links /> | ||
</head> | ||
<body> | ||
<Outlet /> | ||
<ScrollRestoration /> | ||
<Scripts /> | ||
<LiveReload /> | ||
</body> | ||
</html> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import MuxUploader from "@mux/mux-uploader-react"; | ||
import { ActionFunctionArgs, json, redirect } from "@remix-run/node"; | ||
import { Form, useActionData, useLoaderData } from "@remix-run/react"; | ||
import { useState } from "react"; | ||
import mux from "~/lib/mux.server"; | ||
|
||
export const loader = async () => { | ||
// Create an endpoint for MuxUploader to upload to | ||
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 json({ id: upload.id, url: upload.url }); | ||
}; | ||
|
||
export const action = async ({ request }: ActionFunctionArgs) => { | ||
const formData = await request.formData(); | ||
const uploadId = formData.get("uploadId"); | ||
if (typeof uploadId !== "string") { | ||
throw new Error("No uploadId found"); | ||
} | ||
|
||
// when the upload is complete, | ||
// the upload will have an assetId associated with it | ||
// we'll use that assetId to view the video status | ||
const upload = await mux.video.uploads.retrieve(uploadId); | ||
if (upload.asset_id) { | ||
return redirect(`/status/${upload.asset_id}`); | ||
} | ||
|
||
// 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 write an api route | ||
// to listen for the`video.upload.asset_created` webhook | ||
// https://docs.mux.com/guides/listen-for-webhooks | ||
// However, to keep things simple here, | ||
// we'll just ask the user to push the button again. | ||
// This should rarely happen. | ||
return json({ message: "Upload has no asset yet. Try again." }); | ||
}; | ||
|
||
export default function UploadPage() { | ||
const loaderData = useLoaderData<typeof loader>(); | ||
const actionData = useActionData<typeof action>(); | ||
const [isUploadSuccess, setIsUploadSuccess] = useState(false); | ||
|
||
const { id, url } = loaderData; | ||
const { message } = actionData ?? {}; | ||
|
||
return ( | ||
<Form method="post"> | ||
<MuxUploader endpoint={url} onSuccess={() => setIsUploadSuccess(true)} /> | ||
<input type="hidden" name="uploadId" value={id} /> | ||
{/* | ||
you might have other fields here, like name and description, | ||
that you'll save in your CMS alongside the uploadId and assetId | ||
*/} | ||
<button | ||
type="submit" | ||
className="my-4 p-4 py-2 rounded border border-blue-600 text-blue-600 disabled:border-gray-400 disabled:text-gray-400" | ||
disabled={!isUploadSuccess} | ||
> | ||
{isUploadSuccess ? "Watch video" : "Waiting for upload..."} | ||
</button> | ||
{message && <p>{message}</p>} | ||
</Form> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { json, type ActionFunctionArgs } from "@remix-run/node"; | ||
import mux from "~/lib/mux.server"; | ||
|
||
// while this isn't called anywhere in this example, | ||
// I thought it might be helpful to see what a mux webhook handler looks like. | ||
|
||
// Mux webhooks POST, so let's use an action | ||
export const action = async ({ request }: ActionFunctionArgs) => { | ||
if (request.method !== "POST") { | ||
return new Response("Method not allowed", { status: 405 }); | ||
} | ||
|
||
const body = await request.text(); | ||
// mux.webhooks.unwrap will validate that the given payload was sent by Mux and parse the payload. | ||
// It will also provide type-safe access to the payload. | ||
// Generate MUX_WEBHOOK_SIGNING_SECRET in the Mux dashboard | ||
// https://dashboard.mux.com/settings/webhooks | ||
const event = mux.webhooks.unwrap( | ||
body, | ||
request.headers, | ||
process.env.MUX_WEBHOOK_SIGNING_SECRET | ||
); | ||
|
||
// you can also unwrap the payload yourself: | ||
// const event = await request.json(); | ||
switch (event.type) { | ||
case "video.upload.asset_created": | ||
// we might use this to know that an upload has been completed | ||
// and we can save its assetId to our database | ||
break; | ||
case "video.asset.ready": | ||
// we might use this to know that a video has been encoded | ||
// and we can save its playbackId to our database | ||
break; | ||
// there are many more Mux webhook events | ||
// check them out at https://docs.mux.com/webhook-reference | ||
default: | ||
break; | ||
} | ||
|
||
return json({ message: "ok" }) | ||
}; |
57 changes: 57 additions & 0 deletions
57
remixjs-uploader-and-player/app/routes/playback.$playbackId.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import MuxPlayer from "@mux/mux-player-react"; | ||
import { MetaFunction } from "@remix-run/node"; | ||
import { useParams } from "@remix-run/react"; | ||
import Link from "~/components/Link"; | ||
|
||
const title = "View this video created with Mux + Remix"; | ||
const description = | ||
"This video was uploaded and processed by Mux in an example Remix application."; | ||
export const meta: MetaFunction = ({ params }) => { | ||
const { playbackId } = params; | ||
return [ | ||
{ name: "description", content: description }, | ||
{ property: "og:type", content: "video" }, | ||
{ property: "og:title", content: title }, | ||
{ property: "og:description", content: description }, | ||
{ | ||
property: "og:image", | ||
content: `https://image.mux.com/${playbackId}/thumbnail.png?width=1200&height=630&fit_mode=pad`, | ||
}, | ||
{ property: "og:image:width", content: "1200" }, | ||
{ property: "og:image:height", content: "630" }, | ||
{ property: "twitter:card", content: "summary_large_image" }, | ||
{ property: "twitter:title", content: title }, | ||
{ property: "twitter:description", content: description }, | ||
{ | ||
property: "twitter:image", | ||
content: `https://image.mux.com/${playbackId}/thumbnail.png?width=1200&height=600&fit_mode=pad`, | ||
}, | ||
{ property: "twitter:image:width", content: "1200" }, | ||
{ property: "twitter:image:height", content: "600" }, | ||
// These tags should be sufficient for social sharing. | ||
// However, if you're really committed video SEO, I'd suggest adding ld+json, as well. | ||
// https://developers.google.com/search/docs/appearance/structured-data/video | ||
]; | ||
}; | ||
|
||
export default function Page() { | ||
const { playbackId } = useParams(); | ||
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: "remix/examples/mux-video" }} | ||
accentColor="rgb(37 99 235)" | ||
/> | ||
</div> | ||
<p> | ||
Go <Link to="/">back home</Link> to upload another video. | ||
</p> | ||
</> | ||
); | ||
} |
Oops, something went wrong.