Skip to main content
Technology & EngineeringVideo Services283 lines

Mux Video

"Mux: video hosting, HLS streaming, upload API, playback URLs, thumbnails, analytics, Mux Player, Next.js integration"

Quick Summary32 lines
Mux is an API-first video platform built for developers. It handles the full video pipeline — ingest, encoding, storage, and delivery — so you never manage transcoding infrastructure. Every video uploaded becomes an adaptive bitrate HLS stream with a simple playback URL. Mux provides first-class SDKs, a drop-in player component, and a data analytics product that tracks quality of experience metrics. Design around Mux's asset lifecycle: create an asset, get a playback ID, use that ID everywhere. Lean on webhooks for async status updates rather than polling. Use signed URLs for access control and direct uploads for client-side ingest to avoid proxying large files through your server.

## Key Points

- Use direct uploads for client-side video ingest to avoid proxying large files through your API server.
- Set `encoding_tier: "smart"` to let Mux optimize encoding cost versus quality automatically.
- Always handle webhooks for asset status changes rather than polling the API.
- Store the `playback_id` in your database, not the full URL, since URL structures may evolve.
- Use signed playback for any content that requires access control or monetization.
- Pass `metadata` to Mux Player for accurate analytics attribution per viewer and video.
- Set `max_resolution_tier` to avoid encoding higher resolutions than your content warrants.
- Use `thumbnail.jpg?time=N` to pick a representative frame rather than defaulting to the first frame.
- Verify webhook signatures before processing any event to prevent spoofed requests.
- Do not proxy video uploads through your server; use direct uploads to Mux's endpoint.
- Do not poll the asset status endpoint in a loop; rely on `video.asset.ready` webhooks.
- Do not hardcode playback URLs with `.m3u8`; use Mux Player which handles HLS negotiation.

## Quick Example

```typescript
// package.json dependencies:
// "@mux/mux-node": "^8.0.0"
// "@mux/mux-player-react": "^2.0.0"
```

```
MUX_TOKEN_ID=your-token-id
MUX_TOKEN_SECRET=your-token-secret
MUX_WEBHOOK_SECRET=your-webhook-signing-secret
```
skilldb get video-services-skills/Mux VideoFull skill: 283 lines
Paste into your CLAUDE.md or agent config

Mux Video

Core Philosophy

Mux is an API-first video platform built for developers. It handles the full video pipeline — ingest, encoding, storage, and delivery — so you never manage transcoding infrastructure. Every video uploaded becomes an adaptive bitrate HLS stream with a simple playback URL. Mux provides first-class SDKs, a drop-in player component, and a data analytics product that tracks quality of experience metrics. Design around Mux's asset lifecycle: create an asset, get a playback ID, use that ID everywhere. Lean on webhooks for async status updates rather than polling. Use signed URLs for access control and direct uploads for client-side ingest to avoid proxying large files through your server.

Setup

Install the Node SDK and configure credentials:

import Mux from "@mux/mux-node";

const mux = new Mux({
  tokenId: process.env.MUX_TOKEN_ID!,
  tokenSecret: process.env.MUX_TOKEN_SECRET!,
});

For Next.js projects, add the Mux Player component:

// package.json dependencies:
// "@mux/mux-node": "^8.0.0"
// "@mux/mux-player-react": "^2.0.0"

Environment variables required:

MUX_TOKEN_ID=your-token-id
MUX_TOKEN_SECRET=your-token-secret
MUX_WEBHOOK_SECRET=your-webhook-signing-secret

Key Techniques

Creating Assets from a URL

async function createAssetFromUrl(videoUrl: string) {
  const asset = await mux.video.assets.create({
    input: [{ url: videoUrl }],
    playback_policy: ["public"],
    encoding_tier: "smart",
    max_resolution_tier: "1080p",
  });

  return {
    assetId: asset.id,
    playbackId: asset.playback_ids?.[0]?.id,
    status: asset.status, // "preparing" initially
  };
}

Direct Uploads (Client-Side Ingest)

Create an upload URL server-side, then upload from the browser:

// Server: create a direct upload
async function createDirectUpload() {
  const upload = await mux.video.uploads.create({
    new_asset_settings: {
      playback_policy: ["public"],
      encoding_tier: "smart",
    },
    cors_origin: process.env.NEXT_PUBLIC_APP_URL,
  });

  return { uploadId: upload.id, uploadUrl: upload.url };
}

// Client: use Mux Uploader or fetch
import MuxUploader from "@mux/mux-uploader-react";

function VideoUploader({ uploadUrl }: { uploadUrl: string }) {
  return (
    <MuxUploader
      endpoint={uploadUrl}
      onSuccess={() => console.log("Upload complete")}
      onError={(e) => console.error("Upload failed", e)}
    />
  );
}

Playback URLs and Thumbnails

function getPlaybackUrls(playbackId: string) {
  const base = `https://stream.mux.com/${playbackId}`;

  return {
    hls: `${base}.m3u8`,
    thumbnail: `https://image.mux.com/${playbackId}/thumbnail.jpg`,
    animatedGif: `https://image.mux.com/${playbackId}/animated.gif`,
    storyboard: `https://image.mux.com/${playbackId}/storyboard.vtt`,
    // Thumbnail at specific time
    thumbnailAt10s: `https://image.mux.com/${playbackId}/thumbnail.jpg?time=10`,
    // Custom dimensions
    thumbnailSized: `https://image.mux.com/${playbackId}/thumbnail.jpg?width=640&height=360`,
  };
}

Mux Player in Next.js

import MuxPlayer from "@mux/mux-player-react";

interface VideoPlayerProps {
  playbackId: string;
  title: string;
  accentColor?: string;
}

function VideoPlayer({ playbackId, title, accentColor = "#FF5733" }: VideoPlayerProps) {
  return (
    <MuxPlayer
      playbackId={playbackId}
      metadata={{
        video_title: title,
        viewer_user_id: "user-abc-123",
      }}
      accentColor={accentColor}
      autoPlay="muted"
      streamType="on-demand"
      thumbnailTime={5}
      style={{ aspectRatio: "16/9", width: "100%" }}
    />
  );
}

Signed (Private) Playback

import jwt from "jsonwebtoken";

function createSignedPlaybackUrl(
  playbackId: string,
  signingKeyId: string,
  signingPrivateKey: string,
  expiresInSeconds = 3600
) {
  const now = Math.floor(Date.now() / 1000);

  const token = jwt.sign(
    {
      sub: playbackId,
      aud: "v",
      exp: now + expiresInSeconds,
      kid: signingKeyId,
    },
    Buffer.from(signingPrivateKey, "base64"),
    { algorithm: "RS256" }
  );

  return `https://stream.mux.com/${playbackId}.m3u8?token=${token}`;
}

Webhook Handling

import { buffer } from "micro";
import Mux from "@mux/mux-node";
import type { NextApiRequest, NextApiResponse } from "next";

export const config = { api: { bodyParser: false } };

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const rawBody = (await buffer(req)).toString("utf8");
  const signature = req.headers["mux-signature"] as string;

  try {
    Mux.Webhooks.verifySignature(rawBody, {
      "mux-signature": signature,
    }, process.env.MUX_WEBHOOK_SECRET!);
  } catch {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const event = JSON.parse(rawBody);

  switch (event.type) {
    case "video.asset.ready":
      await handleAssetReady(event.data);
      break;
    case "video.asset.errored":
      await handleAssetError(event.data);
      break;
    case "video.upload.asset_created":
      await handleUploadComplete(event.data);
      break;
  }

  res.status(200).json({ received: true });
}

async function handleAssetReady(data: { id: string; playback_ids: Array<{ id: string }> }) {
  const playbackId = data.playback_ids[0]?.id;
  // Update your database with the ready asset
  await db.video.update({
    where: { muxAssetId: data.id },
    data: { status: "ready", playbackId },
  });
}

Analytics with Mux Data

async function getVideoViewMetrics(videoId: string) {
  const views = await mux.data.videoViews.list({
    filters: [`video_id:${videoId}`],
    timeframe: ["7:days"],
  });

  const metrics = await mux.data.metrics.breakdown("aggregate_startup_time", {
    group_by: "browser",
    timeframe: ["7:days"],
    filters: [`video_id:${videoId}`],
  });

  return { views: views.data, metrics: metrics.data };
}

Asset Management

async function manageAssets() {
  // List assets
  const assets = await mux.video.assets.list({ limit: 25 });

  // Get specific asset
  const asset = await mux.video.assets.retrieve("asset-id");

  // Add captions/subtitles
  await mux.video.assets.createTrack("asset-id", {
    url: "https://example.com/captions.vtt",
    type: "text",
    text_type: "subtitles",
    language_code: "en",
    name: "English",
  });

  // Delete asset
  await mux.video.assets.delete("asset-id");
}

Best Practices

  • Use direct uploads for client-side video ingest to avoid proxying large files through your API server.
  • Set encoding_tier: "smart" to let Mux optimize encoding cost versus quality automatically.
  • Always handle webhooks for asset status changes rather than polling the API.
  • Store the playback_id in your database, not the full URL, since URL structures may evolve.
  • Use signed playback for any content that requires access control or monetization.
  • Pass metadata to Mux Player for accurate analytics attribution per viewer and video.
  • Set max_resolution_tier to avoid encoding higher resolutions than your content warrants.
  • Use thumbnail.jpg?time=N to pick a representative frame rather than defaulting to the first frame.
  • Verify webhook signatures before processing any event to prevent spoofed requests.

Anti-Patterns

  • Do not proxy video uploads through your server; use direct uploads to Mux's endpoint.
  • Do not poll the asset status endpoint in a loop; rely on video.asset.ready webhooks.
  • Do not hardcode playback URLs with .m3u8; use Mux Player which handles HLS negotiation.
  • Do not skip webhook signature verification in production, even if it works without it in dev.
  • Do not create assets with playback_policy: ["signed"] unless you have signing key infrastructure set up, or playback will silently fail.
  • Do not assume the asset is ready immediately after upload; always wait for the ready status.
  • Do not ignore the video.asset.errored webhook; surface ingest failures to users.
  • Do not embed Mux token credentials in client-side code; keep them server-side only.

Install this skill directly: skilldb add video-services-skills

Get CLI access →