# Stream Patterns Common workflows, full-stack flows, and best practices. ## React Stream Player `npm install @cloudflare/stream-react` ```tsx import { Stream } from '@cloudflare/stream-react'; export function VideoPlayer({ videoId, token }: { videoId: string; token?: string }) { return ; } ``` ## Full-Stack Upload Flow **Backend API (Workers/Pages)** ```typescript import Cloudflare from 'cloudflare'; export default { async fetch(request: Request, env: Env): Promise { const { videoName } = await request.json(); const client = new Cloudflare({ apiToken: env.CF_API_TOKEN }); const { uploadURL, uid } = await client.stream.directUpload.create({ account_id: env.CF_ACCOUNT_ID, maxDurationSeconds: 3600, requireSignedURLs: true, meta: { name: videoName } }); return Response.json({ uploadURL, uid }); } }; ``` **Frontend component** ```tsx import { useState } from 'react'; export function VideoUploader() { const [uploading, setUploading] = useState(false); const [progress, setProgress] = useState(0); async function handleUpload(file: File) { setUploading(true); const { uploadURL, uid } = await fetch('/api/upload-url', { method: 'POST', body: JSON.stringify({ videoName: file.name }) }).then(r => r.json()); const xhr = new XMLHttpRequest(); xhr.upload.onprogress = (e) => setProgress((e.loaded / e.total) * 100); xhr.onload = () => { setUploading(false); window.location.href = `/videos/${uid}`; }; xhr.open('POST', uploadURL); const formData = new FormData(); formData.append('file', file); xhr.send(formData); } return (
e.target.files?.[0] && handleUpload(e.target.files[0])} disabled={uploading} /> {uploading && }
); } ``` ## TUS Resumable Upload For large files (>500MB). `npm install tus-js-client` ```typescript import * as tus from 'tus-js-client'; async function uploadWithTUS(file: File, uploadURL: string, onProgress?: (pct: number) => void) { return new Promise((resolve, reject) => { const upload = new tus.Upload(file, { endpoint: uploadURL, retryDelays: [0, 3000, 5000, 10000, 20000], chunkSize: 50 * 1024 * 1024, metadata: { filename: file.name, filetype: file.type }, onError: reject, onProgress: (up, total) => onProgress?.((up / total) * 100), onSuccess: () => resolve(upload.url?.split('/').pop() || '') }); upload.start(); }); } ``` ## Video State Polling ```typescript async function waitForVideoReady(client: Cloudflare, accountId: string, videoId: string) { for (let i = 0; i < 60; i++) { const video = await client.stream.videos.get(videoId, { account_id: accountId }); if (video.readyToStream || video.status.state === 'error') return video; await new Promise(resolve => setTimeout(resolve, 5000)); } throw new Error('Video processing timeout'); } ``` ## Webhook Handler ```typescript export default { async fetch(request: Request, env: Env): Promise { const signature = request.headers.get('Webhook-Signature'); const body = await request.text(); if (!signature || !await verifyWebhook(signature, body, env.WEBHOOK_SECRET)) { return new Response('Unauthorized', { status: 401 }); } const payload = JSON.parse(body); if (payload.readyToStream) console.log(`Video ${payload.uid} ready`); return new Response('OK'); } }; async function verifyWebhook(sig: string, body: string, secret: string): Promise { const parts = Object.fromEntries(sig.split(',').map(p => p.split('='))); const timestamp = parseInt(parts.time || '0', 10); if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false; const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); const computed = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(`${timestamp}.${body}`)); const hex = Array.from(new Uint8Array(computed), b => b.toString(16).padStart(2, '0')).join(''); return hex === parts.sig1; } ``` ## Self-Sign JWT (High Volume Tokens) For >1k tokens/day. Prerequisites: Create signing key (see configuration.md). ```typescript async function selfSignToken(keyId: string, jwkBase64: string, videoId: string, expiresIn = 3600) { const key = await crypto.subtle.importKey( 'jwk', JSON.parse(atob(jwkBase64)), { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['sign'] ); const now = Math.floor(Date.now() / 1000); const header = btoa(JSON.stringify({ alg: 'RS256', kid: keyId })).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); const payload = btoa(JSON.stringify({ sub: videoId, kid: keyId, exp: now + expiresIn, nbf: now })) .replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); const message = `${header}.${payload}`; const sig = await crypto.subtle.sign('RSASSA-PKCS1-v1_5', key, new TextEncoder().encode(message)); const b64Sig = btoa(String.fromCharCode(...new Uint8Array(sig))).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); return `${message}.${b64Sig}`; } // With access rules (geo-restriction) const payloadWithRules = { sub: videoId, kid: keyId, exp: now + 3600, nbf: now, accessRules: [{ type: 'ip.geoip.country', action: 'allow', country: ['US'] }] }; ``` ## Best Practices - **Use Direct Creator Uploads** - Avoid proxying through servers - **Enable requireSignedURLs** - Control private content access - **Self-sign tokens at scale** - Use signing keys for >1k/day - **Set allowedOrigins** - Prevent hotlinking - **Use webhooks over polling** - Efficient status updates - **Set maxDurationSeconds** - Prevent abuse - **Enable live recordings** - Auto VOD after stream ## In This Reference - [README.md](./README.md) - Overview and quick start - [configuration.md](./configuration.md) - Setup and config - [api.md](./api.md) - On-demand video APIs - [api-live.md](./api-live.md) - Live streaming APIs - [gotchas.md](./gotchas.md) - Error codes, troubleshooting ## See Also - [workers](../workers/) - Deploy Stream APIs in Workers - [pages](../pages/) - Integrate Stream with Pages