Cloudflare Stream
"Cloudflare Stream: video hosting, adaptive streaming, direct upload, watermarks, signed URLs, Workers integration"
Cloudflare Stream is a video-on-demand and live streaming platform integrated into the Cloudflare ecosystem. It eliminates transcoding complexity — you upload a video, and Stream produces adaptive bitrate HLS/DASH outputs delivered over Cloudflare's edge network. Stream is opinionated: no codec knobs, no bitrate ladders, no storage buckets to configure. You get a video UID and an embed code. This simplicity is the point. Use Stream when you need reliable video delivery without building encoding pipelines. Pair it with Workers for custom access logic, signed URLs for gated content, and the Stream API for programmatic control. Direct creator uploads let end users upload without touching your origin. ## Key Points - Use direct creator uploads with tus protocol for reliable, resumable client-side uploads. - Enable `requireSignedURLs` on any video that needs access gating; public URLs cannot be revoked. - Store the video `uid` in your database, not full URLs, since URL formats may change. - Set `allowedOrigins` to restrict which domains can embed your videos. - Use Workers KV or D1 to store access control rules close to the edge for low-latency auth checks. - Create watermark profiles once and reuse them across uploads rather than recreating per video. - Use the `meta` field to store your own identifiers and metadata on each video for easy cross-referencing. - Handle the `error` state in webhooks; not all uploads succeed, and users need feedback. - Do not proxy video uploads through your origin server; use direct upload URLs from Stream. - Do not try to configure encoding settings, codecs, or bitrate ladders; Stream handles this automatically. - Do not poll the API for video readiness; configure webhooks and react to status changes. - Do not store signed tokens long-term; generate them on demand with short expiration times. ## Quick Example ``` CLOUDFLARE_API_TOKEN=your-api-token-with-stream-permissions CLOUDFLARE_ACCOUNT_ID=your-account-id CLOUDFLARE_STREAM_SIGNING_KEY=your-signing-key-id CLOUDFLARE_STREAM_SIGNING_PEM=your-pem-private-key ```
skilldb get video-services-skills/Cloudflare StreamFull skill: 358 linesCloudflare Stream
Core Philosophy
Cloudflare Stream is a video-on-demand and live streaming platform integrated into the Cloudflare ecosystem. It eliminates transcoding complexity — you upload a video, and Stream produces adaptive bitrate HLS/DASH outputs delivered over Cloudflare's edge network. Stream is opinionated: no codec knobs, no bitrate ladders, no storage buckets to configure. You get a video UID and an embed code. This simplicity is the point. Use Stream when you need reliable video delivery without building encoding pipelines. Pair it with Workers for custom access logic, signed URLs for gated content, and the Stream API for programmatic control. Direct creator uploads let end users upload without touching your origin.
Setup
Stream is accessed via the Cloudflare API. Install the SDK and configure credentials:
import Cloudflare from "cloudflare";
const cf = new Cloudflare({
apiToken: process.env.CLOUDFLARE_API_TOKEN!,
});
const ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID!;
Environment variables required:
CLOUDFLARE_API_TOKEN=your-api-token-with-stream-permissions
CLOUDFLARE_ACCOUNT_ID=your-account-id
CLOUDFLARE_STREAM_SIGNING_KEY=your-signing-key-id
CLOUDFLARE_STREAM_SIGNING_PEM=your-pem-private-key
Ensure your API token has Stream:Edit and Stream:Read permissions.
Key Techniques
Uploading Video via URL
async function uploadFromUrl(videoUrl: string, name: string) {
const response = await cf.stream.copy.create({
account_id: ACCOUNT_ID,
url: videoUrl,
meta: {
name,
},
allowedOrigins: ["example.com", "*.example.com"],
});
return {
uid: response.uid,
status: response.status,
playback: {
hls: `https://customer-${ACCOUNT_ID}.cloudflarestream.com/${response.uid}/manifest/video.m3u8`,
dash: `https://customer-${ACCOUNT_ID}.cloudflarestream.com/${response.uid}/manifest/video.mpd`,
iframe: `https://customer-${ACCOUNT_ID}.cloudflarestream.com/${response.uid}/iframe`,
},
};
}
Direct Creator Uploads (Client-Side)
Generate a one-time upload URL on the server, then upload from the browser:
// Server: create a direct upload URL
async function createDirectUploadUrl(maxDurationSeconds = 3600) {
const response = await cf.stream.directUpload.create({
account_id: ACCOUNT_ID,
maxDurationSeconds,
allowedOrigins: ["https://example.com"],
meta: {
uploadedBy: "user-123",
},
requireSignedURLs: false,
});
return { uploadUrl: response.uploadURL, uid: response.uid };
}
// Client: upload using tus-js-client for resumable uploads
import * as tus from "tus-js-client";
function uploadVideo(file: File, uploadUrl: string, onProgress: (pct: number) => void) {
return new Promise<void>((resolve, reject) => {
const upload = new tus.Upload(file, {
endpoint: uploadUrl,
chunkSize: 50 * 1024 * 1024, // 50 MB chunks
retryDelays: [0, 3000, 5000, 10000],
metadata: {
name: file.name,
filetype: file.type,
},
onError: reject,
onProgress: (bytesUploaded, bytesTotal) => {
onProgress(Math.round((bytesUploaded / bytesTotal) * 100));
},
onSuccess: () => resolve(),
});
upload.start();
});
}
Signed URLs for Access Control
import { webcrypto } from "crypto";
async function createSignedToken(
videoUid: string,
signingKeyId: string,
pemKey: string,
expiresInSeconds = 3600
) {
const encoder = new TextEncoder();
const expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;
const header = { alg: "RS256", kid: signingKeyId };
const payload = {
sub: videoUid,
kid: signingKeyId,
exp: expiresAt,
accessRules: [
{ type: "any", action: "allow" },
],
};
const headerB64 = btoa(JSON.stringify(header));
const payloadB64 = btoa(JSON.stringify(payload));
const signingInput = `${headerB64}.${payloadB64}`;
const key = await webcrypto.subtle.importKey(
"pkcs8",
pemToBuffer(pemKey),
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
false,
["sign"]
);
const signature = await webcrypto.subtle.sign(
"RSASSA-PKCS1-v1_5",
key,
encoder.encode(signingInput)
);
const signatureB64 = btoa(String.fromCharCode(...new Uint8Array(signature)));
return `${signingInput}.${signatureB64}`;
}
function pemToBuffer(pem: string): ArrayBuffer {
const stripped = pem.replace(/-----[^-]+-----/g, "").replace(/\s/g, "");
const binary = atob(stripped);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return bytes.buffer;
}
Watermarks
async function createWatermarkProfile(imageUrl: string) {
const watermark = await cf.stream.watermarks.create({
account_id: ACCOUNT_ID,
url: imageUrl,
name: "brand-logo",
position: "upperRight",
scale: 0.1,
opacity: 0.8,
});
return watermark.uid;
}
async function uploadWithWatermark(videoUrl: string, watermarkUid: string) {
const response = await cf.stream.copy.create({
account_id: ACCOUNT_ID,
url: videoUrl,
watermark: { uid: watermarkUid },
});
return response.uid;
}
Workers Integration
Use a Cloudflare Worker to gate access or transform stream responses:
// Cloudflare Worker script
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const videoId = url.pathname.split("/")[2];
if (!videoId) {
return new Response("Video ID required", { status: 400 });
}
// Check auth
const authHeader = request.headers.get("Authorization");
const user = await validateToken(authHeader, env);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
// Check if user has access to this video
const hasAccess = await env.VIDEO_ACCESS.get(`${user.id}:${videoId}`);
if (!hasAccess) {
return new Response("Forbidden", { status: 403 });
}
// Generate signed URL and redirect
const token = await createSignedStreamToken(videoId, env);
const streamUrl = `https://customer-${env.ACCOUNT_ID}.cloudflarestream.com/${token}/manifest/video.m3u8`;
return Response.redirect(streamUrl, 302);
},
};
interface Env {
ACCOUNT_ID: string;
STREAM_SIGNING_KEY_ID: string;
STREAM_SIGNING_PEM: string;
VIDEO_ACCESS: KVNamespace;
}
Video Management
async function manageVideos() {
// List videos
const videos = await cf.stream.list({
account_id: ACCOUNT_ID,
status: "ready",
});
// Get video details
const video = await cf.stream.get(VIDEO_UID, {
account_id: ACCOUNT_ID,
});
// Update video metadata
await cf.stream.update(VIDEO_UID, {
account_id: ACCOUNT_ID,
meta: { title: "Updated Title", category: "tutorial" },
});
// Clip a video
const clip = await cf.stream.clip.create({
account_id: ACCOUNT_ID,
clippedFromVideoUID: VIDEO_UID,
startTimeSeconds: 30,
endTimeSeconds: 90,
});
// Delete a video
await cf.stream.delete(VIDEO_UID, {
account_id: ACCOUNT_ID,
});
}
Embedding the Stream Player
interface StreamPlayerProps {
videoUid: string;
signedToken?: string;
}
function StreamPlayer({ videoUid, signedToken }: StreamPlayerProps) {
const src = signedToken || videoUid;
return (
<div style={{ position: "relative", paddingTop: "56.25%" }}>
<iframe
src={`https://customer-${process.env.NEXT_PUBLIC_CF_ACCOUNT_ID}.cloudflarestream.com/${src}/iframe`}
style={{
border: "none",
position: "absolute",
top: 0,
left: 0,
height: "100%",
width: "100%",
}}
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture"
allowFullScreen
/>
</div>
);
}
Webhook Processing
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const signature = req.headers["webhook-signature"] as string;
// Verify signature using your webhook signing secret
if (!verifyWebhookSignature(req.body, signature, process.env.CF_WEBHOOK_SECRET!)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = req.body;
if (event.status?.state === "ready") {
await db.video.update({
where: { streamUid: event.uid },
data: {
status: "ready",
duration: event.duration,
thumbnailUrl: event.thumbnail,
},
});
} else if (event.status?.state === "error") {
await db.video.update({
where: { streamUid: event.uid },
data: { status: "error", errorMessage: event.status.errorReasonText },
});
}
res.status(200).json({ ok: true });
}
Best Practices
- Use direct creator uploads with tus protocol for reliable, resumable client-side uploads.
- Enable
requireSignedURLson any video that needs access gating; public URLs cannot be revoked. - Store the video
uidin your database, not full URLs, since URL formats may change. - Set
allowedOriginsto restrict which domains can embed your videos. - Use Workers KV or D1 to store access control rules close to the edge for low-latency auth checks.
- Create watermark profiles once and reuse them across uploads rather than recreating per video.
- Use the
metafield to store your own identifiers and metadata on each video for easy cross-referencing. - Handle the
errorstate in webhooks; not all uploads succeed, and users need feedback.
Anti-Patterns
- Do not proxy video uploads through your origin server; use direct upload URLs from Stream.
- Do not try to configure encoding settings, codecs, or bitrate ladders; Stream handles this automatically.
- Do not poll the API for video readiness; configure webhooks and react to status changes.
- Do not store signed tokens long-term; generate them on demand with short expiration times.
- Do not embed Stream videos without setting
allowedOrigins; anyone can embed your content otherwise. - Do not assume uploads are instant; large files take time to process even after upload completes.
- Do not use Stream for audio-only content; it is optimized for video and charges per minute of video stored.
- Do not hardcode the customer subdomain; use environment variables so it works across accounts.
Install this skill directly: skilldb add video-services-skills
Related Skills
Amazon IVS
"Amazon Interactive Video Service: low-latency live streaming, real-time stages, chat, stream recording, channel management, viewer analytics"
Api.video
"api.video: video hosting API, upload, live streaming, player customization, analytics, webhooks"
Bunny Stream
"Bunny.net Stream: video hosting, HLS delivery, direct uploads, video collections, thumbnail generation, token authentication, webhook events"
LiveKit
"LiveKit: real-time video/audio, WebRTC, rooms, tracks, screen sharing, recording, React components"
Mux Video
"Mux: video hosting, HLS streaming, upload API, playback URLs, thumbnails, analytics, Mux Player, Next.js integration"
Vimeo OTT
"Vimeo OTT API: video-on-demand platform, subscription management, customer auth, content libraries, embed players, analytics, webhook events"