Add Video Streaming with Mux in 2026
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.readyinstead 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
| Metric | What It Tells You |
|---|---|
| Total views | Reach |
| Unique viewers | Audience size |
| Watch time | Engagement depth |
| Rebuffering rate | Quality of experience |
| Startup time | Player performance |
| View drop-off | Where 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
| Mistake | Impact | Fix |
|---|---|---|
| Not using direct upload | Video passes through your server — slow, expensive | Use Mux's direct upload URLs |
| Polling too frequently for status | Rate limited | Use webhooks; poll every 5s max if you must |
| Missing playback policy | Videos not accessible | Set playback_policy: ['public'] |
| No fallback for encoding | Users see broken player | Show "Processing..." until status is "ready" |
| Exposing token secret | Account compromise | Server-side only |
| Not verifying webhook signatures | Spoofed events | Always 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.