Skip to main content

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

  1. Cloudflare Dashboard → Stream
  2. Get your Account ID from the sidebar
  3. 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

ComponentCost
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

MistakeImpactFix
Not using direct creator uploadsVideos route through your server — slowUse direct upload URLs
Missing requireSignedURLs for private contentAnyone with URL can viewEnable signed URLs
Not handling encoding statusPlayer shows error on unfinished videosPoll status until "ready"
Using mp4 download links for streamingNo adaptive bitrate, poor mobile experienceUse HLS manifest URL
Hardcoding account ID in clientWorks but messyUse environment variable

Choosing video infrastructure? Compare Cloudflare Stream vs Mux vs api.video on APIScout — pricing, encoding quality, and developer experience.

Comments