Skip to content

Commit 5f04397

Browse files
decepulismachour
andauthored
feat(mux): Add Mux Video example (#441)
Co-authored-by: Mehdi Achour <[email protected]>
1 parent 7e7507b commit 5f04397

15 files changed

+540
-0
lines changed

mux-video/.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
MUX_TOKEN_ID=
2+
MUX_TOKEN_SECRET=

mux-video/.eslintrc.cjs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* This is intended to be a basic starting point for linting in your app.
3+
* It relies on recommended configs out of the box for simplicity, but you can
4+
* and should modify this configuration to best suit your team's needs.
5+
*/
6+
7+
/** @type {import('eslint').Linter.Config} */
8+
module.exports = {
9+
root: true,
10+
parserOptions: {
11+
ecmaVersion: "latest",
12+
sourceType: "module",
13+
ecmaFeatures: {
14+
jsx: true,
15+
},
16+
},
17+
env: {
18+
browser: true,
19+
commonjs: true,
20+
es6: true,
21+
},
22+
ignorePatterns: ["!**/.server", "!**/.client"],
23+
24+
// Base config
25+
extends: ["eslint:recommended"],
26+
27+
overrides: [
28+
// React
29+
{
30+
files: ["**/*.{js,jsx,ts,tsx}"],
31+
plugins: ["react", "jsx-a11y"],
32+
extends: [
33+
"plugin:react/recommended",
34+
"plugin:react/jsx-runtime",
35+
"plugin:react-hooks/recommended",
36+
"plugin:jsx-a11y/recommended",
37+
],
38+
settings: {
39+
react: {
40+
version: "detect",
41+
},
42+
formComponents: ["Form"],
43+
linkComponents: [
44+
{ name: "Link", linkAttribute: "to" },
45+
{ name: "NavLink", linkAttribute: "to" },
46+
],
47+
"import/resolver": {
48+
typescript: {},
49+
},
50+
},
51+
},
52+
53+
// Typescript
54+
{
55+
files: ["**/*.{ts,tsx}"],
56+
plugins: ["@typescript-eslint", "import"],
57+
parser: "@typescript-eslint/parser",
58+
settings: {
59+
"import/internal-regex": "^~/",
60+
"import/resolver": {
61+
node: {
62+
extensions: [".ts", ".tsx"],
63+
},
64+
typescript: {
65+
alwaysTryTypes: true,
66+
},
67+
},
68+
},
69+
extends: [
70+
"plugin:@typescript-eslint/recommended",
71+
"plugin:import/recommended",
72+
"plugin:import/typescript",
73+
],
74+
},
75+
76+
// Node
77+
{
78+
files: [".eslintrc.cjs"],
79+
env: {
80+
node: true,
81+
},
82+
},
83+
],
84+
};

mux-video/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
3+
/.cache
4+
/build
5+
.env

mux-video/README.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Mux Video
2+
3+
This example uses Mux Video, an API-first platform for video. The example features video uploading and playback in a Remix.js application.
4+
5+
This example is useful if you want to build a platform that supports user-uploaded videos. For example:
6+
- Enabling user profile videos
7+
- Accepting videos for a video contest promotion
8+
- Allowing customers to upload screencasts that help with troubleshooting a bug
9+
- Or even the next Youtube, TikTok, or Instagram
10+
11+
## Preview
12+
13+
Open this example on [CodeSandbox](https://codesandbox.com):
14+
15+
[![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/remix-run/examples/tree/main/mux-video)
16+
17+
## How to use
18+
19+
### Step 1. Create a Remix app with this example
20+
21+
```bash
22+
npx create-remix@latest --template remix-run/examples/mux-video
23+
```
24+
25+
### Step 2. Create an account in Mux
26+
27+
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).
28+
29+
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.
30+
31+
### Step 3. Set up environment variables
32+
33+
Copy the `.env.example` file in this directory to `.env` (which will be ignored by Git):
34+
35+
```bash
36+
cp .env.example .env
37+
```
38+
39+
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`:
40+
41+
- `MUX_TOKEN_ID` should be the `TOKEN ID` of your new token
42+
- `MUX_TOKEN_SECRET` should be `TOKEN SECRET`
43+
44+
At this point, you're good to `npm run dev`.
45+
46+
## How it works
47+
48+
Uploading and viewing a video takes four steps:
49+
50+
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.
51+
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.
52+
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)
53+
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)
54+
55+
These steps correspond to the following routes:
56+
57+
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...
58+
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...
59+
3. [`playback.$playbackId.tsx`](app/routes/playback.$playbackId.tsx) plays the video.
60+
61+
## Preparing for Production
62+
63+
### Set the cors_origin
64+
65+
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.
66+
67+
### Consider webhooks
68+
69+
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.
70+
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.
71+

mux-video/app/lib/mux.server.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Mux from "@mux/mux-node";
2+
3+
const mux = new Mux();
4+
5+
export default mux;

mux-video/app/root.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {
2+
Links,
3+
Meta,
4+
Outlet,
5+
Scripts,
6+
ScrollRestoration,
7+
} from "@remix-run/react";
8+
9+
export function Layout({ children }: { children: React.ReactNode }) {
10+
return (
11+
<html lang="en">
12+
<head>
13+
<meta charSet="utf-8" />
14+
<meta name="viewport" content="width=device-width, initial-scale=1" />
15+
<Meta />
16+
<Links />
17+
</head>
18+
<body>
19+
{children}
20+
<ScrollRestoration />
21+
<Scripts />
22+
</body>
23+
</html>
24+
);
25+
}
26+
27+
export default function App() {
28+
return <Outlet />;
29+
}

mux-video/app/routes/_index.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import MuxUploader from "@mux/mux-uploader-react";
2+
import { type ActionFunctionArgs, json, redirect } from "@remix-run/node";
3+
import { Form, useActionData, useLoaderData } from "@remix-run/react";
4+
import { useState } from "react";
5+
6+
import mux from "~/lib/mux.server";
7+
8+
export const loader = async () => {
9+
// Create an endpoint for MuxUploader to upload to
10+
const upload = await mux.video.uploads.create({
11+
new_asset_settings: {
12+
playback_policy: ["public"],
13+
encoding_tier: "baseline",
14+
},
15+
// in production, you'll want to change this origin to your-domain.com
16+
cors_origin: "*",
17+
});
18+
return json({ id: upload.id, url: upload.url });
19+
};
20+
21+
export const action = async ({ request }: ActionFunctionArgs) => {
22+
const formData = await request.formData();
23+
const uploadId = formData.get("uploadId");
24+
if (typeof uploadId !== "string") {
25+
throw new Error("No uploadId found");
26+
}
27+
28+
// when the upload is complete,
29+
// the upload will have an assetId associated with it
30+
// we'll use that assetId to view the video status
31+
const upload = await mux.video.uploads.retrieve(uploadId);
32+
if (upload.asset_id) {
33+
return redirect(`/status/${upload.asset_id}`);
34+
}
35+
36+
// while onSuccess is a strong indicator that Mux has received the file
37+
// and created the asset, this isn't a guarantee.
38+
// In production, you might write an api route
39+
// to listen for the`video.upload.asset_created` webhook
40+
// https://docs.mux.com/guides/listen-for-webhooks
41+
// However, to keep things simple here,
42+
// we'll just ask the user to push the button again.
43+
// This should rarely happen.
44+
return json({ message: "Upload has no asset yet. Try again." });
45+
};
46+
47+
export default function UploadPage() {
48+
const loaderData = useLoaderData<typeof loader>();
49+
const actionData = useActionData<typeof action>();
50+
const [isUploadSuccess, setIsUploadSuccess] = useState(false);
51+
52+
const { id, url } = loaderData;
53+
const { message } = actionData ?? {};
54+
55+
return (
56+
<Form method="post">
57+
<MuxUploader endpoint={url} onSuccess={() => setIsUploadSuccess(true)} />
58+
<input type="hidden" name="uploadId" value={id} />
59+
{/*
60+
you might have other fields here, like name and description,
61+
that you'll save in your CMS alongside the uploadId and assetId
62+
*/}
63+
<button
64+
type="submit"
65+
disabled={!isUploadSuccess}
66+
>
67+
{isUploadSuccess ? "Watch video" : "Waiting for upload..."}
68+
</button>
69+
{message ? <p>{message}</p> : null}
70+
</Form>
71+
);
72+
}

mux-video/app/routes/mux.webhook.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { json, type ActionFunctionArgs } from "@remix-run/node";
2+
3+
import mux from "~/lib/mux.server";
4+
5+
// while this isn't called anywhere in this example,
6+
// I thought it might be helpful to see what a mux webhook handler looks like.
7+
8+
// Mux webhooks POST, so let's use an action
9+
export const action = async ({ request }: ActionFunctionArgs) => {
10+
if (request.method !== "POST") {
11+
return new Response("Method not allowed", { status: 405 });
12+
}
13+
14+
const body = await request.text();
15+
// mux.webhooks.unwrap will validate that the given payload was sent by Mux and parse the payload.
16+
// It will also provide type-safe access to the payload.
17+
// Generate MUX_WEBHOOK_SIGNING_SECRET in the Mux dashboard
18+
// https://dashboard.mux.com/settings/webhooks
19+
const event = mux.webhooks.unwrap(
20+
body,
21+
request.headers,
22+
process.env.MUX_WEBHOOK_SIGNING_SECRET
23+
);
24+
25+
// you can also unwrap the payload yourself:
26+
// const event = await request.json();
27+
switch (event.type) {
28+
case "video.upload.asset_created":
29+
// we might use this to know that an upload has been completed
30+
// and we can save its assetId to our database
31+
break;
32+
case "video.asset.ready":
33+
// we might use this to know that a video has been encoded
34+
// and we can save its playbackId to our database
35+
break;
36+
// there are many more Mux webhook events
37+
// check them out at https://docs.mux.com/webhook-reference
38+
default:
39+
break;
40+
}
41+
42+
return json({ message: "ok" })
43+
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import MuxPlayer from "@mux/mux-player-react";
2+
import type { MetaFunction } from "@remix-run/node";
3+
import { useParams, Link } from "@remix-run/react";
4+
5+
const title = "View this video created with Mux + Remix";
6+
const description =
7+
"This video was uploaded and processed by Mux in an example Remix application.";
8+
export const meta: MetaFunction = ({ params }) => {
9+
const { playbackId } = params;
10+
return [
11+
{ name: "description", content: description },
12+
{ property: "og:type", content: "video" },
13+
{ property: "og:title", content: title },
14+
{ property: "og:description", content: description },
15+
{
16+
property: "og:image",
17+
content: `https://image.mux.com/${playbackId}/thumbnail.png?width=1200&height=630&fit_mode=pad`,
18+
},
19+
{ property: "og:image:width", content: "1200" },
20+
{ property: "og:image:height", content: "630" },
21+
{ property: "twitter:card", content: "summary_large_image" },
22+
{ property: "twitter:title", content: title },
23+
{ property: "twitter:description", content: description },
24+
{
25+
property: "twitter:image",
26+
content: `https://image.mux.com/${playbackId}/thumbnail.png?width=1200&height=600&fit_mode=pad`,
27+
},
28+
{ property: "twitter:image:width", content: "1200" },
29+
{ property: "twitter:image:height", content: "600" },
30+
// These tags should be sufficient for social sharing.
31+
// However, if you're really committed video SEO, I'd suggest adding ld+json, as well.
32+
// https://developers.google.com/search/docs/appearance/structured-data/video
33+
];
34+
};
35+
36+
export default function Page() {
37+
const { playbackId } = useParams();
38+
return (
39+
<>
40+
<p>
41+
This video is ready for playback and sharing
42+
</p>
43+
<MuxPlayer
44+
style={{ width: '100%', height: 'auto', aspectRatio: '16/9', marginBottom: '2rem' }}
45+
playbackId={playbackId}
46+
metadata={{ player_name: "remix/examples/mux-video" }}
47+
accentColor="rgb(37 99 235)"
48+
/>
49+
<p>
50+
Go <Link to="/">back home</Link> to upload another video.
51+
</p>
52+
</>
53+
);
54+
}

0 commit comments

Comments
 (0)