forked from remix-run/examples
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(mux): Add Mux Video example (remix-run#441)
Co-authored-by: Mehdi Achour <machour@gmail.com>
- Loading branch information
Showing
15 changed files
with
540 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,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,84 @@ | ||
/** | ||
* This is intended to be a basic starting point for linting in your app. | ||
* It relies on recommended configs out of the box for simplicity, but you can | ||
* and should modify this configuration to best suit your team's needs. | ||
*/ | ||
|
||
/** @type {import('eslint').Linter.Config} */ | ||
module.exports = { | ||
root: true, | ||
parserOptions: { | ||
ecmaVersion: "latest", | ||
sourceType: "module", | ||
ecmaFeatures: { | ||
jsx: true, | ||
}, | ||
}, | ||
env: { | ||
browser: true, | ||
commonjs: true, | ||
es6: true, | ||
}, | ||
ignorePatterns: ["!**/.server", "!**/.client"], | ||
|
||
// Base config | ||
extends: ["eslint:recommended"], | ||
|
||
overrides: [ | ||
// React | ||
{ | ||
files: ["**/*.{js,jsx,ts,tsx}"], | ||
plugins: ["react", "jsx-a11y"], | ||
extends: [ | ||
"plugin:react/recommended", | ||
"plugin:react/jsx-runtime", | ||
"plugin:react-hooks/recommended", | ||
"plugin:jsx-a11y/recommended", | ||
], | ||
settings: { | ||
react: { | ||
version: "detect", | ||
}, | ||
formComponents: ["Form"], | ||
linkComponents: [ | ||
{ name: "Link", linkAttribute: "to" }, | ||
{ name: "NavLink", linkAttribute: "to" }, | ||
], | ||
"import/resolver": { | ||
typescript: {}, | ||
}, | ||
}, | ||
}, | ||
|
||
// Typescript | ||
{ | ||
files: ["**/*.{ts,tsx}"], | ||
plugins: ["@typescript-eslint", "import"], | ||
parser: "@typescript-eslint/parser", | ||
settings: { | ||
"import/internal-regex": "^~/", | ||
"import/resolver": { | ||
node: { | ||
extensions: [".ts", ".tsx"], | ||
}, | ||
typescript: { | ||
alwaysTryTypes: true, | ||
}, | ||
}, | ||
}, | ||
extends: [ | ||
"plugin:@typescript-eslint/recommended", | ||
"plugin:import/recommended", | ||
"plugin:import/typescript", | ||
], | ||
}, | ||
|
||
// Node | ||
{ | ||
files: [".eslintrc.cjs"], | ||
env: { | ||
node: true, | ||
}, | ||
}, | ||
], | ||
}; |
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 @@ | ||
node_modules | ||
|
||
/.cache | ||
/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,71 @@ | ||
# Mux Video | ||
|
||
This example uses Mux Video, an API-first platform for video. The 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 a Remix app with this example | ||
|
||
```bash | ||
npx create-remix@latest --template remix-run/examples/mux-video | ||
``` | ||
|
||
### Step 2. 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). | ||
|
||
Before entering a credit card on your Mux account, all videos are in “test mode” which means they are watermarked and clipped to 10 seconds. | ||
|
||
### Step 3. 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** with "Mux Video Read" and "Mux Video Write" permissions. 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. | ||
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. | ||
|
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,29 @@ | ||
import { | ||
Links, | ||
Meta, | ||
Outlet, | ||
Scripts, | ||
ScrollRestoration, | ||
} from "@remix-run/react"; | ||
|
||
export function Layout({ children }: { children: React.ReactNode }) { | ||
return ( | ||
<html lang="en"> | ||
<head> | ||
<meta charSet="utf-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
<Meta /> | ||
<Links /> | ||
</head> | ||
<body> | ||
{children} | ||
<ScrollRestoration /> | ||
<Scripts /> | ||
</body> | ||
</html> | ||
); | ||
} | ||
|
||
export default function App() { | ||
return <Outlet />; | ||
} |
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 { type 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" | ||
disabled={!isUploadSuccess} | ||
> | ||
{isUploadSuccess ? "Watch video" : "Waiting for upload..."} | ||
</button> | ||
{message ? <p>{message}</p> : null} | ||
</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,43 @@ | ||
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" }) | ||
}; |
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,54 @@ | ||
import MuxPlayer from "@mux/mux-player-react"; | ||
import type { MetaFunction } from "@remix-run/node"; | ||
import { useParams, Link } from "@remix-run/react"; | ||
|
||
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 ( | ||
<> | ||
<p> | ||
This video is ready for playback and sharing | ||
</p> | ||
<MuxPlayer | ||
style={{ width: '100%', height: 'auto', aspectRatio: '16/9', marginBottom: '2rem' }} | ||
playbackId={playbackId} | ||
metadata={{ player_name: "remix/examples/mux-video" }} | ||
accentColor="rgb(37 99 235)" | ||
/> | ||
<p> | ||
Go <Link to="/">back home</Link> to upload another video. | ||
</p> | ||
</> | ||
); | ||
} |
Oops, something went wrong.