This site is not affiliated with or endorsed by Cloudflare, Inc. It simply showcases experiments built using Cloudflare services.
Cloudflare Experiments
Edge Platform

Stream Video Demo

Create Stream direct upload URLs and signed playback tokens

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