Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mux Video example #441

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions mux-video/.env.example
@@ -0,0 +1,2 @@
MUX_TOKEN_ID=
MUX_TOKEN_SECRET=
4 changes: 4 additions & 0 deletions mux-video/.eslintrc.js
@@ -0,0 +1,4 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
};
6 changes: 6 additions & 0 deletions mux-video/.gitignore
@@ -0,0 +1,6 @@
node_modules

/.cache
/build
/public/build
.env
71 changes: 71 additions & 0 deletions mux-video/README.md
@@ -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 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). 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 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**. 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.
17 changes: 17 additions & 0 deletions mux-video/app/components/Link.tsx
@@ -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;
5 changes: 5 additions & 0 deletions mux-video/app/lib/mux.server.ts
@@ -0,0 +1,5 @@
import Mux from "@mux/mux-node";

const mux = new Mux();

export default mux;
36 changes: 36 additions & 0 deletions mux-video/app/root.tsx
@@ -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>
);
}
72 changes: 72 additions & 0 deletions mux-video/app/routes/_index.tsx
@@ -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>
);
}
42 changes: 42 additions & 0 deletions mux-video/app/routes/mux.webhook.ts
@@ -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 mux-video/app/routes/playback.$playbackId.tsx
@@ -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>
</>
);
}