Skip to content

Commit

Permalink
Add Next.js and Remix.js examples (#213)
Browse files Browse the repository at this point in the history
Co-authored-by: Dave Kiss <[email protected]>
  • Loading branch information
decepulis and davekiss authored Apr 9, 2024
1 parent 3b85335 commit 56b0145
Show file tree
Hide file tree
Showing 20 changed files with 476 additions and 0 deletions.
1 change: 1 addition & 0 deletions nextjs-uploader-and-player/README.md
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)
2 changes: 2 additions & 0 deletions remixjs-uploader-and-player/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
MUX_TOKEN_ID=
MUX_TOKEN_SECRET=
4 changes: 4 additions & 0 deletions remixjs-uploader-and-player/.eslintrc.js
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"],
};
6 changes: 6 additions & 0 deletions remixjs-uploader-and-player/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules

/.cache
/build
/public/build
.env
69 changes: 69 additions & 0 deletions remixjs-uploader-and-player/README.md
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.
17 changes: 17 additions & 0 deletions remixjs-uploader-and-player/app/components/Link.tsx
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;
5 changes: 5 additions & 0 deletions remixjs-uploader-and-player/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;
36 changes: 36 additions & 0 deletions remixjs-uploader-and-player/app/root.tsx
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>
);
}
72 changes: 72 additions & 0 deletions remixjs-uploader-and-player/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 { 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 remixjs-uploader-and-player/app/routes/mux.webhook.ts
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 remixjs-uploader-and-player/app/routes/playback.$playbackId.tsx
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>
</>
);
}
Loading

0 comments on commit 56b0145

Please sign in to comment.