Skip to main content

Add Video Streaming with Mux in 2026

·APIScout Team
Share:

Add Video Streaming with Mux in 2026

Mux handles the hard parts of video: encoding, hosting, adaptive streaming, and analytics. Upload a video file, get a playback URL. This guide covers the full integration: uploads, player setup, live streaming, tracking engagement, image APIs, webhook status handling, and a detailed cost breakdown to help you budget.

TL;DR

  • Mux handles transcoding, adaptive bitrate streaming (HLS), global CDN delivery, and quality-of-experience analytics so you don't have to build or operate a video pipeline
  • Use direct browser uploads (PUT to a signed URL) — never route video through your own server
  • Mux Data is included and tracks rebuffering ratio, startup time, view drop-off, and more automatically when you use Mux Player
  • Webhooks beat polling: listen for video.asset.ready instead of polling the upload status endpoint
  • Pricing for a typical platform with 100 videos × 10 min × 1,000 monthly views lands around $320–$380/month — understand the three billing dimensions before you launch

Why Mux?

Before you write a single line of code, it's worth understanding what Mux actually replaces. Building video infrastructure from scratch means running FFmpeg on every upload to transcode raw files into multiple bitrate/resolution renditions, deploying those renditions to a CDN, implementing HLS manifest generation, dealing with codec compatibility across iOS Safari, Chrome, Firefox, and Android, and then operating all of it reliably at scale. That's a multi-month infrastructure project, and it needs ongoing maintenance as browser codec support evolves.

Mux is an API that handles every layer of that pipeline. You upload a source file — any format, any codec — and Mux returns a playback ID. Behind that ID are multiple renditions encoded at different quality levels (360p through 4K depending on the source), a global CDN distribution network, and an adaptive bitrate player that automatically selects the best rendition for each viewer's connection speed. The whole encoding pipeline typically completes within a few minutes of upload.

The comparison to alternatives is useful. Cloudflare Stream is the closest competitor and is significantly cheaper — Stream charges $1 per 1,000 minutes of video stored and $1 per 1,000 minutes delivered, versus Mux's $0.007 per stored minute and $0.00059 per delivered minute (Mux is roughly 2–3× more expensive for delivery). Cloudflare Stream wins on price for high-volume delivery. Mux wins on analytics depth, live streaming latency options, and developer experience. If you need real-time quality metrics — rebuffering events, startup failures, bandwidth-based quality scoring — Mux Data is significantly more capable than anything Cloudflare includes. See our detailed comparison of Mux vs Cloudflare Stream and our video streaming guide for more context.

Pick Mux when analytics and live streaming quality matter. Pick Cloudflare Stream when you're cost-sensitive and primarily hosting on-demand content.

What You'll Build

  • Video upload (direct from browser)
  • Adaptive bitrate streaming (HLS)
  • Video player with Mux Player
  • Upload progress tracking
  • Video analytics (views, watch time, engagement)

Prerequisites: Node.js 18+, Mux account (free: no upfront cost, pay per minute).

1. Setup

Install

npm install @mux/mux-node @mux/mux-player-react

Initialize

// lib/mux.ts
import Mux from '@mux/mux-node';

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

Environment Variables

# .env.local
MUX_TOKEN_ID=your_token_id
MUX_TOKEN_SECRET=your_token_secret

2. Upload Videos

Create Upload URL

// app/api/upload/route.ts
import { NextResponse } from 'next/server';
import { mux } from '@/lib/mux';

export async function POST() {
  const upload = await mux.video.uploads.create({
    new_asset_settings: {
      playback_policy: ['public'],
      encoding_tier: 'baseline', // or 'smart' for better quality
    },
    cors_origin: process.env.NEXT_PUBLIC_URL,
  });

  return NextResponse.json({
    uploadId: upload.id,
    uploadUrl: upload.url,
  });
}

Direct Browser Upload

// components/VideoUpload.tsx
'use client';
import { useState } from 'react';

export function VideoUpload({ onComplete }: {
  onComplete: (uploadId: string) => void;
}) {
  const [progress, setProgress] = useState(0);
  const [uploading, setUploading] = useState(false);

  const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    setUploading(true);

    // 1. Get upload URL from your server
    const res = await fetch('/api/upload', { method: 'POST' });
    const { uploadId, uploadUrl } = await res.json();

    // 2. Upload directly to Mux
    const xhr = new XMLHttpRequest();
    xhr.upload.onprogress = (e) => {
      if (e.lengthComputable) {
        setProgress(Math.round((e.loaded / e.total) * 100));
      }
    };
    xhr.onload = () => {
      setUploading(false);
      onComplete(uploadId);
    };
    xhr.open('PUT', uploadUrl);
    xhr.send(file);
  };

  return (
    <div>
      <input
        type="file"
        accept="video/*"
        onChange={handleUpload}
        disabled={uploading}
      />
      {uploading && (
        <div>
          <progress value={progress} max={100} />
          <span>{progress}%</span>
        </div>
      )}
    </div>
  );
}

3. Check Upload Status

Poll for Asset Ready

// app/api/upload/[id]/route.ts
import { NextResponse } from 'next/server';
import { mux } from '@/lib/mux';

export async function GET(
  req: Request,
  { params }: { params: { id: string } }
) {
  const upload = await mux.video.uploads.retrieve(params.id);

  if (upload.status === 'asset_created' && upload.asset_id) {
    const asset = await mux.video.assets.retrieve(upload.asset_id);

    return NextResponse.json({
      status: asset.status, // 'preparing' | 'ready' | 'errored'
      playbackId: asset.playback_ids?.[0]?.id,
      duration: asset.duration,
      aspectRatio: asset.aspect_ratio,
    });
  }

  return NextResponse.json({
    status: upload.status, // 'waiting' | 'asset_created'
  });
}

4. Play Videos

Mux Player (React)

// components/VideoPlayer.tsx
'use client';
import MuxPlayer from '@mux/mux-player-react';

export function VideoPlayer({ playbackId, title }: {
  playbackId: string;
  title?: string;
}) {
  return (
    <MuxPlayer
      playbackId={playbackId}
      metadata={{
        video_title: title,
        viewer_user_id: 'user-123', // For analytics
      }}
      streamType="on-demand"
      style={{ width: '100%', maxWidth: '800px' }}
    />
  );
}

Thumbnail and Poster Images

Mux auto-generates thumbnails:

// Thumbnail at specific time
const thumbnailUrl = `https://image.mux.com/${playbackId}/thumbnail.jpg?time=10`;

// Animated GIF preview
const gifUrl = `https://image.mux.com/${playbackId}/animated.gif?start=5&end=10`;

// Storyboard for scrubbing
const storyboardUrl = `https://image.mux.com/${playbackId}/storyboard.vtt`;

5. Live Streaming

Create Live Stream

// app/api/live/route.ts
import { NextResponse } from 'next/server';
import { mux } from '@/lib/mux';

export async function POST() {
  const liveStream = await mux.video.liveStreams.create({
    playback_policy: ['public'],
    new_asset_settings: {
      playback_policy: ['public'],
    },
    latency_mode: 'low', // or 'standard' or 'reduced'
  });

  return NextResponse.json({
    streamKey: liveStream.stream_key,
    playbackId: liveStream.playback_ids?.[0]?.id,
    rtmpUrl: 'rtmps://global-live.mux.com:443/app',
  });
}

Stream from OBS/Software

Point OBS or any RTMP software to:

  • Server: rtmps://global-live.mux.com:443/app
  • Stream Key: The key from the API response

Play Live Stream

<MuxPlayer
  playbackId={livePlaybackId}
  streamType="live"
  metadata={{ video_title: 'My Live Stream' }}
/>

6. Video Analytics

Mux Data (Built-In)

Mux Player automatically tracks engagement. Query analytics via API:

// Get video views for an asset
const views = await mux.data.videoViews.list({
  filters: [`video_id:${assetId}`],
  timeframe: ['7:days'],
});

// Get overall metrics
const metrics = await mux.data.metrics.breakdown('views', {
  group_by: 'video_title',
  timeframe: ['30:days'],
});

Key Metrics

MetricWhat It Tells You
Total viewsReach
Unique viewersAudience size
Watch timeEngagement depth
Rebuffering rateQuality of experience
Startup timePlayer performance
View drop-offWhere users stop watching

Mux Data: Video Quality Analytics

Mux Data is included free with every Mux Video account, and it is more sophisticated than basic view counting. The platform tracks Quality of Experience (QoE) metrics — the signals that tell you whether viewers are actually having a good experience watching your content, not just whether they started playing it.

The four metrics that matter most for QoE are rebuffering ratio (the percentage of playback time spent stalling — anything above 0.5% starts to noticeably hurt completion rates), startup time (how long from play button press to first frame — above 2 seconds causes meaningful abandonment), average bitrate (which rendition viewers are actually getting, a proxy for CDN performance), and video quality score (Mux's composite 1–10 score combining multiple signals). When you pass viewer_user_id to the Mux Player metadata, every metric becomes user-attributable, so you can identify whether a quality problem is global or affecting specific users or regions.

Mux Data exposes all of this through its dashboard and through a REST API, so you can pull metrics into your own analytics pipeline or alert when rebuffering ratio spikes above a threshold. The API returns timeseries data that you can plot in Grafana or ingest into a data warehouse.

Mux Image APIs

Mux generates derivative images from every video asset without any additional setup. These are served from image.mux.com and are parameterized via query string, making them trivial to integrate into any UI.

For static thumbnails, thumbnail.png (or .jpg or .webp) extracts a frame from the video. The time parameter specifies the offset in seconds — ?time=30 grabs the frame at 30 seconds. If you don't specify a time, Mux picks a representative frame automatically. You can control the output dimensions with width and height parameters while maintaining aspect ratio. This is the right approach for video card thumbnails in a grid layout.

For hover previews, animated.gif generates a short animated preview. The start and end parameters define the clip range in seconds. Keep animated previews short (3–5 seconds) since GIF file sizes grow quickly. animated.webp produces smaller files for browsers that support it.

For player scrubbing, Mux generates a storyboard.vtt file — a WebVTT file that references a sprite sheet of thumbnail frames. The Mux Player uses this automatically for the seek preview feature (the small thumbnail that appears when you hover over the scrubber). If you're building a custom player, the storyboard URL is https://image.mux.com/{playbackId}/storyboard.vtt and the sprite sheet is at https://image.mux.com/{playbackId}/storyboard.jpg.

All image API endpoints are CDN-cached and served from Mux's global edge network, so they perform well without any additional caching layer on your end.

Webhooks for Video Status

The upload status polling approach shown in section 3 works, but it has problems at scale. Polling every 5 seconds for each upload creates unnecessary API calls, adds latency to the user experience (you might poll right before encoding finishes and wait another 5 seconds), and clutters your server logs.

The production approach is webhooks. Mux sends a video.asset.ready event to your webhook endpoint when an asset finishes encoding and is available for playback. Configure your webhook endpoint in the Mux Dashboard under Settings → Webhooks.

// app/api/webhooks/mux/route.ts
import { NextResponse } from 'next/server';
import { mux } from '@/lib/mux';

export async function POST(req: Request) {
  const body = await req.text();

  // Verify webhook signature
  const signature = req.headers.get('mux-signature') ?? '';
  const isValid = mux.webhooks.verifySignature(
    body,
    { 'mux-signature': signature },
    process.env.MUX_WEBHOOK_SIGNING_SECRET!
  );

  if (!isValid) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  const event = JSON.parse(body);

  if (event.type === 'video.asset.ready') {
    const assetId = event.data.id;
    const playbackId = event.data.playback_ids?.[0]?.id;

    // Mark the video as ready in your database
    await db.video.update({
      where: { muxAssetId: assetId },
      data: {
        status: 'ready',
        playbackId,
        duration: event.data.duration,
        aspectRatio: event.data.aspect_ratio,
      },
    });
  }

  if (event.type === 'video.asset.errored') {
    const assetId = event.data.id;
    await db.video.update({
      where: { muxAssetId: assetId },
      data: { status: 'errored' },
    });
  }

  return NextResponse.json({ received: true });
}

The key events to handle are video.asset.ready (encoding complete, safe to show the player), video.asset.errored (encoding failed — alert someone), video.upload.asset_created (upload received, encoding started), and video.live_stream.active / video.live_stream.idle for live stream lifecycle management.

The polling vs webhook tradeoff: polling is simpler to implement in prototypes and acceptable when upload volume is low. Webhooks are the right choice for production because they eliminate latency, reduce API calls, and give you reliable event-driven state transitions in your database. For a thorough treatment of webhook reliability, see our guide on building webhooks that don't break.

Mux Pricing Deep Dive

Mux charges across three independent dimensions: encoding minutes, storage minutes, and delivery minutes. Understanding each is essential for accurate cost estimation.

Encoding is a one-time charge per uploaded video. Baseline tier costs $0.015 per source minute — a 10-minute video costs $0.15 to encode. Smart tier costs $0.035 per source minute and produces better quality output with more renditions. Encoding is charged once regardless of how many times the video is viewed.

Storage is $0.007 per stored minute per month. A 10-minute video costs $0.07 per month to store. This compounds with library size — 1,000 videos × 10 minutes × $0.007 = $70/month in storage alone.

Delivery is $0.00059 per minute of video delivered to viewers. A viewer watching your full 10-minute video generates $0.0059 in delivery charges. At 1,000 views, that video costs $5.90 in delivery. This is where most platforms see the majority of their Mux bill.

Real cost example for a platform with 100 videos × 10 minutes × 1,000 monthly views:

  • Encoding (one-time): 100 videos × 10 min × $0.015 = $15 one-time
  • Storage: 100 × 10 × $0.007 = $7/month
  • Delivery: 100 videos × 1,000 views × 10 min × $0.00059 = $590/month
  • Total ongoing: ~$597/month

Compare this to Cloudflare Stream: storage at $5/1,000 minutes stored ($5/month for the same library) and delivery at $1/1,000 minutes delivered ($1,000 views × 10 min × 100 videos / 1,000 × $1 = $1,000/month). Cloudflare Stream is actually more expensive at this volume in the delivery dimension. The math reverses at higher view counts — Stream's flat per-thousand-minute pricing versus Mux's per-minute pricing means results vary by usage pattern. Run the numbers for your specific access patterns. Mux's free tier includes 10 encoding hours per month, which covers initial development and testing.

Common Mistakes

MistakeImpactFix
Not using direct uploadVideo passes through your server — slow, expensiveUse Mux's direct upload URLs
Polling too frequently for statusRate limitedUse webhooks; poll every 5s max if you must
Missing playback policyVideos not accessibleSet playback_policy: ['public']
No fallback for encodingUsers see broken playerShow "Processing..." until status is "ready"
Exposing token secretAccount compromiseServer-side only
Not verifying webhook signaturesSpoofed eventsAlways verify mux-signature header

Building with video? Explore video APIs on APIScout — pricing, features, and encoding quality. Also see our API caching strategies guide and API monitoring guide for building production-ready infrastructure around your video platform.

The API Integration Checklist (Free PDF)

Step-by-step checklist: auth setup, rate limit handling, error codes, SDK evaluation, and pricing comparison for 50+ APIs. Used by 200+ developers.

Join 200+ developers. Unsubscribe in one click.