This site is not affiliated with or endorsed by Cloudflare, Inc. It showcases deployable experiments across most Cloudflare products.
Edge Platform

Stream Video Demo

Create Cloudflare Stream direct-upload URLs and signed playback tokens - video upload and secure playback from a Worker

Create Cloudflare Stream direct upload URLs and signed playback tokens from a Worker via the Stream binding. Locally (or without a live binding) endpoints return demo values with mode: "demo".

Features

  • Direct creator upload URL via env.STREAM.createDirectUpload
  • Signed playback token via env.STREAM.video(uid).generateToken
  • Demo mode when the Stream binding is unavailable

API Reference

POST /upload-url

Create a one-time direct upload URL.

Body (optional JSON)

FieldRequiredDescription
maxDurationSecondsNoMax video length in seconds (1–21600, default 3600)

Example Request

curl -X POST "https://your-worker.workers.dev/upload-url" \
  -H "Content-Type: application/json" \
  -d '{"maxDurationSeconds": 600}'

Success Response

{
  "uploadURL": "https://upload.cloudflarestream.com/...",
  "uid": "video-uid",
  "mode": "live",
  "maxDurationSeconds": 600
}

Error Codes

  • 400 - Invalid JSON / duration (INVALID_BODY)
  • 502 - Stream API failure (STREAM_ERROR)

GET /playback-token

Generate a signed playback token for a video UID.

uid string (required)

Stream video UID.

Example Request

curl "https://your-worker.workers.dev/playback-token?uid=VIDEO_UID"

Success Response

{
  "token": "signed-playback-token",
  "uid": "VIDEO_UID",
  "playbackUrl": "https://videodelivery.net/signed-playback-token/manifest/video.m3u8",
  "mode": "live"
}

Error Codes

  • 400 - Missing uid (INVALID_UID)
  • 502 - Stream API failure (STREAM_ERROR)

Use Cases

  • Let end users upload video without exposing Stream API tokens
  • Issue short-lived playback tokens for private videos
  • Prototype creator-upload + secure viewing flows on Workers

Limitations

  • Requires Stream enabled on the account for live mode
  • Basic POST uploads are limited to ~200 MB (use tus via REST for larger files)
  • Demo mode returns placeholder URLs/tokens

Use in your project

Copy these files into an existing Worker. Prefer Deployment to try the full experiment first. Source: apps/experiments/stream-video-demo.

DependenciesNoneBindingsNonePlatformWorkers runtime
import {  DEFAULT_MAX_DURATION_SECONDS,  DEMO_TOKEN,  DEMO_UID,  DEMO_UPLOAD_URL,  MAX_MAX_DURATION_SECONDS,  MIN_MAX_DURATION_SECONDS,  PLAYBACK_HOST,} from "../constants/defaults";import type { StreamBinding } from "../types/env";import type { PlaybackTokenResponse, UploadUrlBody, UploadUrlResponse } from "../types/stream";export function validateUid(input: string | undefined): string | null {  if (!input || typeof input !== "string") return null;  const trimmed = input.trim();  if (!trimmed) return null;  return trimmed;}/** * Parse optional POST body for /upload-url. * Returns default duration when body is empty or maxDurationSeconds is omitted. * Returns null when the body or maxDurationSeconds is invalid. */export function parseMaxDurationSeconds(body: unknown): number | null {  if (body === undefined || body === null || body === "") {    return DEFAULT_MAX_DURATION_SECONDS;  }  if (typeof body !== "object" || Array.isArray(body)) {    return null;  }  const { maxDurationSeconds } = body as UploadUrlBody;  if (maxDurationSeconds === undefined) {    return DEFAULT_MAX_DURATION_SECONDS;  }  if (typeof maxDurationSeconds !== "number" || !Number.isFinite(maxDurationSeconds)) {    return null;  }  if (    !Number.isInteger(maxDurationSeconds) ||    maxDurationSeconds < MIN_MAX_DURATION_SECONDS ||    maxDurationSeconds > MAX_MAX_DURATION_SECONDS  ) {    return null;  }  return maxDurationSeconds;}export function buildPlaybackUrl(token: string): string {  return `${PLAYBACK_HOST}/${token}/manifest/video.m3u8`;}export function demoUploadUrl(maxDurationSeconds: number): UploadUrlResponse {  return {    uploadURL: DEMO_UPLOAD_URL,    uid: DEMO_UID,    mode: "demo",    maxDurationSeconds,  };}export function demoPlaybackToken(uid: string): PlaybackTokenResponse {  return {    token: DEMO_TOKEN,    uid,    playbackUrl: buildPlaybackUrl(DEMO_TOKEN),    mode: "demo",  };}function hasCreateDirectUpload(stream: StreamBinding | undefined): stream is StreamBinding {  return typeof stream?.createDirectUpload === "function";}function hasVideo(stream: StreamBinding | undefined): stream is StreamBinding {  return typeof stream?.video === "function";}/** * Create a Stream direct upload URL via the Workers binding. * Falls back to demo values when the binding is missing, stubbed, or throws (e.g. local wrangler). */export async function createUploadUrl(  stream: StreamBinding | undefined,  maxDurationSeconds: number): Promise<UploadUrlResponse> {  if (!hasCreateDirectUpload(stream)) {    return demoUploadUrl(maxDurationSeconds);  }  try {    const result = await stream.createDirectUpload({ maxDurationSeconds });    const uid = result.uid || (result as { uploadURL: string; uid?: string; id?: string }).id || "";    if (!result.uploadURL || !uid) {      return demoUploadUrl(maxDurationSeconds);    }    return {      uploadURL: result.uploadURL,      uid,      mode: "live",      maxDurationSeconds,    };  } catch {    return demoUploadUrl(maxDurationSeconds);  }}/** * Generate a signed Stream playback token for a video uid. * Falls back to demo values when the binding is missing, stubbed, or throws. */export async function createPlaybackToken(  stream: StreamBinding | undefined,  uid: string,  exp?: number): Promise<PlaybackTokenResponse> {  if (!hasVideo(stream)) {    return demoPlaybackToken(uid);  }  try {    const handle = stream.video(uid);    if (typeof handle?.generateToken !== "function") {      return demoPlaybackToken(uid);    }    const result = await handle.generateToken(exp !== undefined ? { exp } : undefined);    if (!result?.token) {      return demoPlaybackToken(uid);    }    return {      token: result.token,      uid,      playbackUrl: buildPlaybackUrl(result.token),      mode: "live",    };  } catch {    return demoPlaybackToken(uid);  }}

Deployment

Click the deploy button

Deploy to Cloudflare Workers

Enable Stream

Ensure Stream is available on your Cloudflare account. The Worker binds STREAM via wrangler.json.

Test your deployment

curl -X POST "https://your-worker.workers.dev/upload-url"

Local Development

cd apps/experiments/stream-video-demo
npm install
npm run dev

Without a live Stream binding, responses include "mode": "demo".

Configuration

wrangler.json declares:

  • Stream binding STREAM

Cloudflare Features Used

On this page