How to Stream Video with Cloudflare Stream
·APIScout Team
cloudflare streamvideo streamingvideo apitutorialapi integration
How to Stream Video with Cloudflare Stream
Cloudflare Stream handles video encoding, storage, and adaptive bitrate delivery. Upload a video, get an embed code. No transcoding pipelines, no CDN configuration, no player headaches. This guide covers uploads, playback, access control, and live streaming.
What You'll Build
- Video upload (direct and via URL)
- Embeddable player with adaptive streaming
- Signed URLs for private video access
- Live streaming with RTMP
- Thumbnail generation
Prerequisites: Cloudflare account with Stream enabled ($5 minimum, pay-per-use).
1. Setup
Get API Credentials
- Cloudflare Dashboard → Stream
- Get your Account ID from the sidebar
- Create an API token with Stream permissions
CLOUDFLARE_ACCOUNT_ID=your_account_id
CLOUDFLARE_API_TOKEN=your_api_token
API Helper
// lib/cloudflare-stream.ts
const ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID!;
const API_TOKEN = process.env.CLOUDFLARE_API_TOKEN!;
const BASE_URL = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/stream`;
async function streamApi(path: string, options?: RequestInit) {
const res = await fetch(`${BASE_URL}${path}`, {
...options,
headers: {
Authorization: `Bearer ${API_TOKEN}`,
...options?.headers,
},
});
return res.json();
}
2. Upload Videos
Direct Upload (from server)
export async function uploadFromUrl(videoUrl: string, name: string) {
const data = await streamApi('/copy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: videoUrl,
meta: { name },
}),
});
return {
uid: data.result.uid,
playbackUrl: `https://customer-${ACCOUNT_ID}.cloudflarestream.com/${data.result.uid}/manifest/video.m3u8`,
embedUrl: `https://customer-${ACCOUNT_ID}.cloudflarestream.com/${data.result.uid}/iframe`,
};
}
Direct Creator Upload (browser-to-Cloudflare)
// 1. Server: Create upload URL
export async function createUploadUrl() {
const data = await streamApi('/direct_upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
maxDurationSeconds: 3600,
requireSignedURLs: false,
}),
});
return {
uploadUrl: data.result.uploadURL,
uid: data.result.uid,
};
}
// 2. Client: Upload directly to Cloudflare
async function uploadVideo(file: File) {
const { uploadUrl, uid } = await fetch('/api/stream/upload', {
method: 'POST',
}).then(r => r.json());
const formData = new FormData();
formData.append('file', file);
await fetch(uploadUrl, {
method: 'POST',
body: formData,
});
return uid;
}
3. Embed Player
iframe Embed
<iframe
src="https://customer-ACCOUNT_ID.cloudflarestream.com/VIDEO_UID/iframe"
style="border: none; width: 100%; aspect-ratio: 16/9;"
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture"
allowfullscreen
></iframe>
Stream Player Web Component
<script src="https://embed.cloudflarestream.com/embed/sdk.latest.js"></script>
<stream src="VIDEO_UID" controls autoplay muted></stream>
React Component
'use client';
export function VideoPlayer({ uid, title }: { uid: string; title?: string }) {
return (
<div style={{ position: 'relative', paddingTop: '56.25%' }}>
<iframe
src={`https://customer-${process.env.NEXT_PUBLIC_CF_ACCOUNT_ID}.cloudflarestream.com/${uid}/iframe`}
title={title}
style={{
border: 'none',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
}}
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture"
allowFullScreen
/>
</div>
);
}
4. Signed URLs (Private Videos)
Create Signing Keys
const keys = await streamApi('/keys', { method: 'POST' });
const signingKey = keys.result;
// Save signingKey.id and signingKey.pem securely
Generate Signed Token
import jwt from 'jsonwebtoken';
export function createSignedUrl(videoUid: string, expiresInHours: number = 1) {
const token = jwt.sign(
{
sub: videoUid,
kid: process.env.CF_STREAM_KEY_ID,
exp: Math.floor(Date.now() / 1000) + expiresInHours * 3600,
accessRules: [
{ type: 'any', action: 'allow' },
],
},
process.env.CF_STREAM_SIGNING_KEY!,
{ algorithm: 'RS256' }
);
return `https://customer-${process.env.CLOUDFLARE_ACCOUNT_ID}.cloudflarestream.com/${token}/manifest/video.m3u8`;
}
5. Live Streaming
Create Live Input
export async function createLiveStream() {
const data = await streamApi('/live_inputs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
meta: { name: 'My Live Stream' },
recording: { mode: 'automatic' },
}),
});
return {
uid: data.result.uid,
rtmpUrl: data.result.rtmps.url,
rtmpKey: data.result.rtmps.streamKey,
srtUrl: data.result.srt.url,
playbackUrl: `https://customer-${ACCOUNT_ID}.cloudflarestream.com/${data.result.uid}/manifest/video.m3u8`,
};
}
Stream from OBS
- Server: Use the RTMPS URL from the API response
- Stream Key: Use the stream key from the API response
6. Thumbnails and Previews
// Static thumbnail at a specific time
const thumbnailUrl = `https://customer-${ACCOUNT_ID}.cloudflarestream.com/${uid}/thumbnails/thumbnail.jpg?time=10s&width=640`;
// Animated GIF
const gifUrl = `https://customer-${ACCOUNT_ID}.cloudflarestream.com/${uid}/thumbnails/thumbnail.gif?start=5s&end=10s&width=320`;
7. Video Analytics
// Get analytics via GraphQL
const analytics = await fetch(
`https://api.cloudflare.com/client/v4/graphql`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `
query {
viewer {
accounts(filter: { accountTag: "${ACCOUNT_ID}" }) {
streamMinutesViewedAdaptiveGroups(
filter: { date_gt: "2026-03-01" }
limit: 100
) {
sum { minutesViewed }
dimensions { uid }
}
}
}
}
`,
}),
}
).then(r => r.json());
Pricing
| Component | Cost |
|---|---|
| Storage | $5/1,000 minutes stored |
| Delivery | $1/1,000 minutes viewed |
| Live streaming | $0.75/1,000 minutes input |
| Minimum | $5/month |
Example: 100 videos × 5 min each (500 min stored) + 10,000 views × 5 min each (50,000 min delivered):
- Storage: $2.50
- Delivery: $50.00
- Total: $52.50/month
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Not using direct creator uploads | Videos route through your server — slow | Use direct upload URLs |
Missing requireSignedURLs for private content | Anyone with URL can view | Enable signed URLs |
| Not handling encoding status | Player shows error on unfinished videos | Poll status until "ready" |
| Using mp4 download links for streaming | No adaptive bitrate, poor mobile experience | Use HLS manifest URL |
| Hardcoding account ID in client | Works but messy | Use environment variable |
Choosing video infrastructure? Compare Cloudflare Stream vs Mux vs api.video on APIScout — pricing, encoding quality, and developer experience.