npx skills add imbilawork/2nth-skills@cloudflare-stream
WHAT IS CLOUDFLARE STREAM
Cloudflare Stream is a fully managed video platform — upload a video file, get back an encoding pipeline, global CDN delivery, an embeddable player, and a playback API. It handles on-demand VOD and real-time live streaming in the same product.
For AI agents, Stream is the right tool whenever a workflow involves video: storing customer walkthroughs, live-streaming product launches, transcribing support calls, or building AI-powered media pipelines on Cloudflare Workers.
Supported Upload Formats
MP4, MKV, MOV, AVI, FLV, MPEG-2 TS, MPEG-2 PS, MXF, LXF, GXF, 3GP, WebM, MPG, QuickTime. Recommended: MP4 container · H.264 video · AAC audio · ≤60 FPS.
HOW STREAM FITS IN
Stream sits between your application and your viewers. You push video in via the API; Stream encodes, stores, and delivers it. Your Workers or AI agents talk to the API to manage assets and generate secure playback URLs.
file / URL / webcam / encoder
Upload API / RTMPS / SRT
H.264 · ABR · 360-1080p
Cloudflare edge PoPs
HLS / DASH / MP4
API calls · signed URLs
manage assets · live inputs
transcribe · caption · search
<stream> · HLS player
GETTING VIDEO INTO STREAM
Four distinct upload paths depending on where the video originates and who is uploading it.
Method 1 — Direct Upload (server-side)
POST a multipart form or a tus resumable upload from your backend. Ideal for server-initiated workflows — processing pipelines, content ingestion, automated archiving.
# Upload via multipart form (small files < 200 MB)
POST https://api.cloudflare.com/client/v4/accounts/{account_id}/stream
Authorization: Bearer {token}
Content-Type: multipart/form-data
--boundary
Content-Disposition: form-data; name="file"; filename="video.mp4"
Content-Type: video/mp4
[binary data]
--boundary--
# Response includes uid, status, thumbnail, playback URLs
{
"result": {
"uid": "ea95132c15732419d526b5cae4c20c56",
"status": { "state": "queued" },
"thumbnail": "https://videodelivery.net/ea95.../thumbnails/thumbnail.jpg",
"playback": {
"hls": "https://videodelivery.net/ea95.../manifest/video.m3u8",
"dash": "https://videodelivery.net/ea95.../manifest/video.mpd"
}
}
}
Method 2 — Upload by URL (pull from external source)
Pass a publicly accessible URL and Stream fetches and encodes it asynchronously. Useful when pulling from S3, a CMS asset library, or a third-party CDN.
POST https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/copy
Authorization: Bearer {token}
Content-Type: application/json
{
"url": "https://your-bucket.s3.amazonaws.com/source/video.mp4",
"meta": {
"name": "Product Walkthrough Q1 2026"
},
"requireSignedURLs": false,
"allowedOrigins": ["yourdomain.com"]
}
Method 3 — Direct Creator Uploads (client-side, one-time URL)
Your backend mints a single-use upload URL and passes it to the browser. The user uploads directly to Cloudflare — your API token never touches the client. This is the correct pattern for user-generated content platforms.
# Step 1: Backend mints the upload URL
POST https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/direct_upload
Authorization: Bearer {token}
Content-Type: application/json
{
"maxDurationSeconds": 3600,
"expiry": "2026-04-01T00:00:00Z",
"meta": { "uploadedBy": "user_123" },
"requireSignedURLs": true
}
# Response
{
"result": {
"uid": "2f01...",
"uploadURL": "https://upload.videodelivery.net/2f01..."
}
}
# Step 2: Client POSTs directly to uploadURL (tus protocol)
# No auth header needed — URL is the credential
For files over 200 MB use the tus protocol (tus-js-client or uppy). Set endpoint to https://api.cloudflare.com/client/v4/accounts/{account_id}/stream with the Authorization header as tus metadata.
Upload API Fields Reference
| Field | Type | Description |
|---|---|---|
uid | string | Unique video identifier — used in all playback URLs |
meta.name | string | Human-readable label for the video |
requireSignedURLs | bool | Restrict playback to signed tokens only |
allowedOrigins | string[] | CORS-style origin whitelist for the player |
scheduledDeletion | datetime | Auto-delete at this UTC timestamp |
maxDurationSeconds | int | Hard cap on video length (creator uploads) |
status.state | string | queued → inprogress → ready / error |
Video Management
# List all videos
GET /accounts/{account_id}/stream?limit=50&status=ready&search=walkthrough
# Get single video
GET /accounts/{account_id}/stream/{video_uid}
# Update metadata
POST /accounts/{account_id}/stream/{video_uid}
{ "meta": { "name": "Updated Title" }, "requireSignedURLs": true }
# Delete
DELETE /accounts/{account_id}/stream/{video_uid}
STREAM LIVE
Stream Live accepts RTMPS and SRT ingest. Every live broadcast is automatically recorded — the VOD asset becomes available within 60 seconds of the stream ending.
Create a Live Input
POST https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/live_inputs
Authorization: Bearer {token}
Content-Type: application/json
{
"meta": { "name": "Weekly Webinar — Product Updates" },
"recording": {
"mode": "automatic",
"timeoutSeconds": 300
}
}
# Response — store rtmps.url + rtmps.streamKey for your encoder
{
"result": {
"uid": "66c...",
"rtmps": {
"url": "rtmps://live.cloudflare.com:443/live/",
"streamKey": "66c...abc"
},
"srt": {
"url": "srt://live.cloudflare.com:778",
"streamId": "66c...",
"passphrase": "xyz..."
},
"playback": {
"hls": "https://videodelivery.net/66c.../manifest/video.m3u8",
"dash": "https://videodelivery.net/66c.../manifest/video.mpd"
}
}
}
Live Input Management
# List live inputs
GET /accounts/{account_id}/stream/live_inputs
# Get live input + all recorded VODs
GET /accounts/{account_id}/stream/live_inputs/{live_input_uid}/videos
# Delete live input
DELETE /accounts/{account_id}/stream/live_inputs/{live_input_uid}
OBS / FFmpeg Setup
| Field | OBS Value |
|---|---|
| Service | Custom |
| Server | rtmps://live.cloudflare.com:443/live/ |
| Stream Key | {streamKey} from live input response |
| Encoder | x264 · CBR · 6000 kbps max · Keyframe 2s |
# FFmpeg RTMPS ingest (requires automatic reconnect flags)
ffmpeg -re -i input.mp4 \
-c:v libx264 -preset veryfast -b:v 4000k -maxrate 4000k -bufsize 8000k \
-g 60 -keyint_min 60 \
-c:a aac -b:a 128k \
-f flv "rtmps://live.cloudflare.com:443/live/{streamKey}"
OBS reconnects automatically. FFmpeg does not — wrap in a shell loop or use -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 30 flags. Cloudflare holds the stream open for timeoutSeconds after the last packet before marking it ended.
Adaptive Bitrate Behaviour
Stream Live calculates ABR renditions from the actual ingest bitrate — not a fixed ladder. Low-motion content (slides, talking heads) will produce fewer/lower renditions. High-motion content (sports, gaming) triggers higher estimates. Plan encoder bitrate accordingly.
SERVING VIDEO TO VIEWERS
Stream Player (Embed)
The fastest path. Cloudflare's own player handles adaptive streaming, captions, thumbnails, and analytics out of the box.
<!-- Load the SDK (once per page) -->
<script src="https://embed.cloudflarestream.com/embed/sdk.latest.js"></script>
<!-- Embed the player -->
<stream
src="ea95132c15732419d526b5cae4c20c56"
controls
autoplay
muted
loop
width="100%"
poster="https://videodelivery.net/ea95.../thumbnails/thumbnail.jpg?time=5s&height=360"
></stream>
<!-- JavaScript API -->
<script>
const player = Stream(document.querySelector('stream'));
player.addEventListener('play', () => console.log('playing'));
player.play();
player.pause();
player.currentTime = 30;
player.volume = 0.5;
</script>
HLS / DASH — Custom Player
Use any player (Video.js, Shaka Player, hls.js) by pointing it at the manifest URL.
# HLS manifest
https://videodelivery.net/{video_uid}/manifest/video.m3u8
# DASH manifest
https://videodelivery.net/{video_uid}/manifest/video.mpd
# Direct MP4 (for download or simple playback)
https://videodelivery.net/{video_uid}/downloads/default.mp4
# Example with hls.js
const hls = new Hls();
hls.loadSource('https://videodelivery.net/{video_uid}/manifest/video.m3u8');
hls.attachMedia(videoElement);
Thumbnails
# Default thumbnail (at 0:00)
https://videodelivery.net/{video_uid}/thumbnails/thumbnail.jpg
# Thumbnail at specific time
https://videodelivery.net/{video_uid}/thumbnails/thumbnail.jpg?time=15s
# Resize
https://videodelivery.net/{video_uid}/thumbnails/thumbnail.jpg?height=360&width=640&fit=crop
# Animated GIF preview (first 3 seconds by default)
https://videodelivery.net/{video_uid}/thumbnails/thumbnail.gif?duration=5s
SIGNED URLS & RESTRICTIONS
When requireSignedURLs: true is set on a video, all direct playback URLs return 401. Viewers must present a signed token issued by your backend.
Step 1 — Enable on a Video
POST /accounts/{account_id}/stream/{video_uid}
{ "requireSignedURLs": true }
Step 2 — Generate a Signing Key
POST /accounts/{account_id}/stream/keys
# Returns a pemKey (RSA private key) — store securely, never expose to clients
{
"result": {
"id": "key_abc123",
"pemKey": "-----BEGIN RSA PRIVATE KEY-----\n..."
}
}
Step 3 — Issue Tokens in Your Worker
// Cloudflare Worker — sign a playback token
import { SignJWT, importPKCS8 } from 'jose';
export async function signStreamToken(videoUID, pemKey, keyId, expiresIn = '1h') {
const privateKey = await importPKCS8(pemKey, 'RS256');
return new SignJWT({})
.setProtectedHeader({ alg: 'RS256', kid: keyId })
.setSubject(videoUID)
.setIssuedAt()
.setExpirationTime(expiresIn)
.sign(privateKey);
}
// Usage — append token to any Stream URL
const token = await signStreamToken(uid, pemKey, keyId, '2h');
const signedHLS = `https://videodelivery.net/${token}/manifest/video.m3u8`;
const signedEmbed = `<stream src="${token}" controls></stream>`;
JWT sub must be the video UID. Optional claims: accessRules to restrict by IP range, country, or any custom claim in the viewer's JWT. Tokens are stateless — no database lookups at playback time.
Origin Restrictions (No Auth Required)
# Restrict playback to specific domains without signed URLs
POST /accounts/{account_id}/stream/{video_uid}
{ "allowedOrigins": ["yourdomain.com", "app.yourdomain.com"] }
STREAM EVENTS
Subscribe to Stream webhook events to trigger downstream workflows — transcription jobs, CMS updates, notifications, or AI processing pipelines.
Register a Webhook Endpoint
PUT /accounts/{account_id}/stream/webhook
Authorization: Bearer {token}
Content-Type: application/json
{
"notificationUrl": "https://your-worker.workers.dev/stream-webhook"
}
Event Types
| Event | When It Fires |
|---|---|
stream.video.ready | Video finished encoding and is ready for playback |
stream.video.errored | Encoding failed — check status.errReasonCode |
stream.live_input.connected | RTMPS/SRT encoder connected and stream is live |
stream.live_input.disconnected | Encoder disconnected; recording will save after timeout |
stream.live_input.recording_started | Recording is being written to storage |
stream.live_input.recording_ready | Post-stream VOD is ready and can be played back |
Webhook Payload & Verification
// Worker receiving a webhook
export default {
async fetch(request) {
const body = await request.json();
const { uid, status, meta } = body.result;
// Cloudflare signs the request — verify with HMAC-SHA256
const signature = request.headers.get('Webhook-Signature');
// signature = 'time=1234;sig1=...'
if (status.state === 'ready') {
// Trigger downstream: transcription, CMS update, AI summary
await triggerTranscription(uid);
}
return new Response('OK');
}
};
AI-POWERED VIDEO WORKFLOWS
Combine Stream with Workers AI to build intelligent video pipelines without leaving Cloudflare's network. Common patterns: auto-transcription, caption generation, content moderation, and semantic video search.
Auto-Transcription on Ready
// workers/stream-pipeline.js
// Triggered by stream.video.ready webhook
export default {
async fetch(request, env) {
const event = await request.json();
const { uid } = event.result;
// 1. Download the audio track from Stream
const audioResp = await fetch(
`https://videodelivery.net/${uid}/downloads/default.mp3`
);
const audioBuffer = await audioResp.arrayBuffer();
// 2. Transcribe with Whisper via Workers AI
const transcription = await env.AI.run('@cf/openai/whisper', {
audio: [...new Uint8Array(audioBuffer)],
});
// 3. Store transcript (D1, KV, R2...)
await env.DB.prepare(
'INSERT INTO transcripts (video_uid, text, language) VALUES (?, ?, ?)'
).bind(uid, transcription.text, transcription.language).run();
// 4. Generate VTT captions and attach to video
const vtt = buildVTT(transcription.words);
await uploadCaptions(uid, vtt, env.CF_API_TOKEN, env.CF_ACCOUNT_ID);
return new Response('Pipeline complete');
}
};
Upload VTT Captions to Stream
async function uploadCaptions(videoUID, vttContent, token, accountId) {
const formData = new FormData();
formData.append('file', new Blob([vttContent], { type: 'text/vtt' }), 'captions.vtt');
return fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/${videoUID}/captions/en`,
{
method: 'PUT',
headers: { Authorization: `Bearer ${token}` },
body: formData,
}
);
}
AI-Powered Summary Generation
// After transcription, generate a structured summary
const summary = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
messages: [
{
role: 'system',
content: 'You are a video content analyst. Extract key topics, action items, and a brief summary from the transcript.'
},
{
role: 'user',
content: `Transcript:\n${transcription.text}`
}
]
});
// Store alongside the video in your CMS or D1 database
Stream webhook → Worker receives stream.video.ready → Download audio → Whisper transcription → LLaMA summary → Store in D1 → Attach VTT captions back to video → Update search index → Notify user. All within Cloudflare's network, zero egress fees.
Typical AI Workflow Patterns
| Pattern | Models | Output |
|---|---|---|
| Auto-transcription | @cf/openai/whisper | Full transcript with word timestamps |
| Auto-captioning | Whisper → VTT formatter | VTT file attached to video |
| Meeting summary | Whisper + LLaMA 3.1 | Structured JSON: topics, action items |
| Content moderation | Custom classifier | Flag/approve workflow |
| Semantic search | Whisper + embeddings + Vectorize | Natural language video search |
| Chapter generation | Whisper + LLaMA | Timestamped chapter markers |
AI AGENT TOOL SET
When an AI agent has the Cloudflare Stream skill installed, it has access to the following tool definitions for managing videos programmatically.
Example Agent Conversations
status.state until it hits ready.rtmps://live.cloudflare.com:443/live/, Stream Key → [key]. Use x264, CBR, 4000 kbps, 60fps, 2-second keyframe interval. The HLS playback URL for your site is ready immediately — the stream goes live as soon as OBS connects.en. I'll batch these to avoid rate limits — estimated 3-5 minutes for the full set.STREAM COSTS
Stream pricing is based on minutes stored and minutes delivered. There is no charge for encoding, transcoding, or packaging.
| Dimension | Cost | Notes |
|---|---|---|
| Storage (minutes) | $5 / 1,000 min stored | Measured at encoded output duration |
| Delivery (minutes) | $1 / 1,000 min delivered | Per viewer-minute of playback |
| Live encoding | Included | No extra charge for RTMPS/SRT ingest |
| Recording | $5 / 1,000 min recorded | Live streams auto-recorded |
| Captions | Included | VTT upload + delivery at no extra cost |
| Signed URLs | Included | No charge for token issuance |
For AI transcription pipelines: Workers AI Whisper costs apply separately (charged per audio second). Budget ~$0.005 per minute of audio. Schedule transcription jobs after stream.video.ready to avoid wasted calls on failed encodes.
COMMON ISSUES & FIXES
| Issue | Cause | Fix |
|---|---|---|
Video stuck in inprogress |
Large file still encoding | Poll GET /stream/{uid} every 5s; hook stream.video.ready webhook instead of polling |
| Playback returns 401 | requireSignedURLs is true but no token passed |
Pass signed JWT as the src in <stream> or append ?token=... to HLS URL |
| Live stream drops and doesn't reconnect | FFmpeg doesn't auto-reconnect on RTMPS disconnect | Add -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 30 to FFmpeg flags |
| Player not showing on page | SDK script loaded after <stream> element |
Load sdk.latest.js in <head> or before the <stream> tag |
| ABR quality lower than expected | Stream calculates renditions from ingest bitrate | Increase encoder bitrate — Stream won't upscale. Target ≥6 Mbps for 1080p output |
| Thumbnail returns wrong frame | Default is 0:00 which may be a black frame | Set ?time=5s or specify thumbnailTimestampPct during upload |
| Creator upload URL expired | expiry field set too short |
Set expiry to at least 30 minutes from now; mint a new URL if expired |
| Multi-channel audio mixed down | Stream encodes to stereo by design | Expected behaviour — pre-mix to stereo before upload if mix matters |
ADD THIS SKILL
Install the Cloudflare Stream skill into your AI agent or Claude Code project. Once installed, the agent has access to all API patterns, code examples, and MCP tool definitions on this page.
npx skills add imbilawork/2nth-skills@cloudflare-stream
Related Skills
Cloudflare Stream integrates naturally with these other 2nth.ai skills:
- Shopify AI — embed product demo videos and live launch streams directly in Shopify storefronts
- Sage X3 AI Integration — attach training videos to ERP onboarding workflows and knowledge bases
Official Cloudflare Stream documentation: developers.cloudflare.com/stream