Skip to content

Commit

Permalink
feat(mux): Add Mux Video example (#441)
Browse files Browse the repository at this point in the history
Co-authored-by: Mehdi Achour <machour@gmail.com>
  • Loading branch information
decepulis and machour committed May 30, 2024
1 parent 7e7507b commit 5f04397
Show file tree
Hide file tree
Showing 15 changed files with 540 additions and 0 deletions.
2 changes: 2 additions & 0 deletions mux-video/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
MUX_TOKEN_ID=
MUX_TOKEN_SECRET=
84 changes: 84 additions & 0 deletions mux-video/.eslintrc.cjs
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,
},
},
],
};
5 changes: 5 additions & 0 deletions mux-video/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules

/.cache
/build
.env
71 changes: 71 additions & 0 deletions mux-video/README.md
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.

5 changes: 5 additions & 0 deletions mux-video/app/lib/mux.server.ts
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;
29 changes: 29 additions & 0 deletions mux-video/app/root.tsx
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 />;
}
72 changes: 72 additions & 0 deletions mux-video/app/routes/_index.tsx
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>
);
}
43 changes: 43 additions & 0 deletions mux-video/app/routes/mux.webhook.ts
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" })
};
54 changes: 54 additions & 0 deletions mux-video/app/routes/playback.$playbackId.tsx
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>
</>
);
}
Loading

0 comments on commit 5f04397

Please sign in to comment.