Media · Stream

Ingest and deliver video with Stream

After this lesson you'll be able to upload a video via a direct creator upload URL, embed the resulting player, stand up a basic live input, and budget correctly for both storage and delivery minutes.

Stream is Cloudflare's managed video platform: you give it a video file or a live encoder feed, and it handles encoding, storage, adaptive-bitrate delivery, and a drop-in player — without you running transcoding servers or a video CDN yourself. It covers both on-demand video (upload once, play many times) and live video (ingest an RTMPS/SRT feed, distribute it to viewers, optionally record it for later on-demand playback) through one API and one billing model.

What you're not building. Without Stream, "just host video" means running a transcoding pipeline (multiple resolutions/bitrates per upload), storing all those renditions, serving an HLS/DASH manifest, and building or licensing a player that handles adaptive bitrate switching. Stream collapses that into an upload call and an iframe.

How it works

The core object is a video, identified by a uid. You get a video into Stream one of two ways:

Once a file lands, Stream transcodes it to H.264 across multiple renditions (up to 1080p) for adaptive-bitrate streaming — the player automatically switches resolution as the viewer's bandwidth changes, the same idea as HLS/DASH elsewhere, but you don't configure the ladder yourself. When processing finishes, the video is playable via Stream's hosted player (an iframe you embed with the uid) or via direct HLS/DASH manifest URLs if you're using your own player.

Live video uses a separate object, a live input, created via API or dashboard. Creating one returns ingest credentials for two supported protocols:

Both expect closed GOPs, a stable bitrate (typically under 12 Mbps), and keyframes every 2–8 seconds — standard live-encoder settings, not Stream-specific tuning. Viewers watch the live input the same way they watch on-demand video: through the hosted player or a manifest URL, pointed at the live input's playback ID instead of a video uid. If you set the input's recording mode to automatic, the broadcast is also saved as an on-demand video the moment it ends — billed as separate stored minutes, not free just because it started as live.

Worked example: direct creator upload

Step 1 — your backend requests a one-time upload URL (never expose your API token to the browser):

curl https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/direct_upload \
  --header 'Authorization: Bearer <API_TOKEN>' \
  --header 'Content-Type: application/json' \
  --data '{"maxDurationSeconds": 3600}'

Response — save the uid, hand the uploadURL to the client:

{
  "result": {
    "uploadURL": "https://upload.videodelivery.net/f65014bc6ff5419ea86e7972a047ba22",
    "uid": "f65014bc6ff5419ea86e7972a047ba22"
  },
  "success": true
}

Step 2 — the client uploads the file straight to that URL (files under 200MB can use a plain POST; larger or unreliable-connection uploads should use the resumable tus protocol instead):

curl --request POST \
  --form file=@my_video.mp4 \
  https://upload.videodelivery.net/f65014bc6ff5419ea86e7972a047ba22

Step 3 — once Stream finishes processing (poll the video-details endpoint with the uid, or use a webhook), embed the player:

<iframe
  src="https://customer-<code>.cloudflarestream.com/f65014bc6ff5419ea86e7972a047ba22/iframe"
  style="border:none; width:100%; aspect-ratio:16/9"
  allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"
  allowfullscreen>
</iframe>

That's the entire "user-generated video" pipeline: your backend never handles the raw file, and there's no transcoding code anywhere in your app.

Pricing

Stream bills on two independent dimensions:

DimensionRate
Minutes stored$5 per 1,000 minutes stored
Minutes delivered$1 per 1,000 minutes delivered (playback, MP4 downloads, simulcasting)

Storage cost is based on video duration, not file size or resolution. Delivery is post-paid and usage-based — bandwidth itself has no separate line item; it's folded into the per-minute-delivered price. For live: ingesting and encoding the incoming feed is free — you're only billed once a live stream is delivered to a viewer or gets recorded to storage. A live stream nobody watches costs nothing beyond whatever recording gets stored. These numbers can change — confirm current rates on the Stream pricing page before quoting them to a client or building a cost model.

Pitfall: budgeting only for storage. It's easy to price out "$5 per 1,000 minutes stored," multiply by your expected video library size, and call it done. But a library of any real size is worthless if nobody watches it — and every minute watched is billed separately at $1 per 1,000 minutes delivered. A 90-minute course video watched by 10,000 students costs roughly $4.50 to store once, but roughly $900 in delivery minutes for that one cohort. Course platforms and popular user-generated clips can rack up delivery costs that dwarf storage costs by orders of magnitude — model both dimensions from day one, not just the one that sounds like "storage" on a pricing page.

Use cases

Primary source

Cloudflare Stream — Direct creator uploads is the canonical reference for the upload flow in this lesson; pair it with the Stream pricing page for current rates, since pricing is subject to change.

A course platform stores 200 hours of video and expects a few thousand students to watch it monthly. Which statement about Stream billing is correct?
Without scrolling up: what are the two ingest protocols Stream live inputs support, and what's the one thing your backend should never expose to the client during a direct creator upload?
Reveal

Live inputs accept RTMPS (URL + stream key, the classic OBS-style setup) and SRT (caller mode — the encoder initiates the connection to Cloudflare). For direct creator uploads, the backend requests a one-time upload URL from Stream's API and hands only that URL to the client — the API token itself is never exposed to the browser or app.

Anything above unclear — the storage-vs-delivery billing split, signed URLs for access control, or when to use the hosted player versus your own HLS/DASH player? Ask your AI teacher before moving on.
← Previous: Resize, optimize, and serve images at the edge Next: Build live audio/video apps with Realtime →