update skills

This commit is contained in:
2026-03-17 16:53:22 -07:00
parent 0b0783ef8e
commit f9a530667e
389 changed files with 54512 additions and 1 deletions

View File

@@ -0,0 +1,114 @@
# Cloudflare Stream
Serverless live and on-demand video streaming platform with one API.
## Overview
Cloudflare Stream provides video upload, storage, encoding, and delivery without managing infrastructure. Runs on Cloudflare's global network.
### Key Features
- **On-demand video**: Upload, encode, store, deliver
- **Live streaming**: RTMPS/SRT ingestion with ABR
- **Direct creator uploads**: End users upload without API keys
- **Signed URLs**: Token-based access control
- **Analytics**: Server-side metrics via GraphQL
- **Webhooks**: Processing notifications
- **Captions**: Upload or AI-generate subtitles
- **Watermarks**: Apply branding to videos
- **Downloads**: Enable MP4 offline viewing
## Core Concepts
### Video Upload Methods
1. **API Upload (TUS protocol)**: Direct server upload
2. **Upload from URL**: Import from external source
3. **Direct Creator Uploads**: User-generated content (recommended)
### Playback Options
1. **Stream Player (iframe)**: Built-in, optimized player
2. **Custom Player (HLS/DASH)**: Video.js, HLS.js integration
3. **Thumbnails**: Static or animated previews
### Access Control
- **Public**: No restrictions
- **requireSignedURLs**: Token-based access
- **allowedOrigins**: Domain restrictions
- **Access Rules**: Geo/IP restrictions in tokens
### Live Streaming
- RTMPS/SRT ingest from OBS, FFmpeg
- Automatic recording to on-demand
- Simulcast to YouTube, Twitch, etc.
- WebRTC support for browser streaming
## Quick Start
**Upload video via API**
```bash
curl -X POST \
"https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/copy" \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/video.mp4"}'
```
**Embed player**
```html
<iframe
src="https://customer-<CODE>.cloudflarestream.com/<VIDEO_ID>/iframe"
style="border: none;"
height="720" width="1280"
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"
allowfullscreen="true"
></iframe>
```
**Create live input**
```bash
curl -X POST \
"https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/live_inputs" \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"recording": {"mode": "automatic"}}'
```
## Limits
- Max file size: 30 GB
- Max frame rate: 60 fps (recommended)
- Supported formats: MP4, MKV, MOV, AVI, FLV, MPEG-2 TS/PS, MXF, LXF, GXF, 3GP, WebM, MPG, QuickTime
## Pricing
- $5/1000 min stored
- $1/1000 min delivered
## Resources
- Dashboard: https://dash.cloudflare.com/?to=/:account/stream
- API Docs: https://developers.cloudflare.com/api/resources/stream/
- Stream Docs: https://developers.cloudflare.com/stream/
## Reading Order
| Order | File | Purpose | When to Use |
|-------|------|---------|-------------|
| 1 | [configuration.md](./configuration.md) | Setup SDKs, env vars, signing keys | Starting new project |
| 2 | [api.md](./api.md) | On-demand video APIs | Implementing uploads/playback |
| 3 | [api-live.md](./api-live.md) | Live streaming APIs | Building live streaming |
| 4 | [patterns.md](./patterns.md) | Full-stack flows, TUS, JWT signing | Implementing workflows |
| 5 | [gotchas.md](./gotchas.md) | Errors, limits, troubleshooting | Debugging issues |
## In This Reference
- [configuration.md](./configuration.md) - Setup, environment variables, wrangler config
- [api.md](./api.md) - On-demand video upload, playback, management APIs
- [api-live.md](./api-live.md) - Live streaming (RTMPS/SRT/WebRTC), simulcast
- [patterns.md](./patterns.md) - Full-stack flows, state management, best practices
- [gotchas.md](./gotchas.md) - Error codes, troubleshooting, limits
## See Also
- [workers](../workers/) - Deploy Stream APIs in Workers
- [pages](../pages/) - Integrate Stream with Pages
- [workers-ai](../workers-ai/) - AI-generate captions

View File

@@ -0,0 +1,195 @@
# Stream Live Streaming API
Live input creation, status checking, simulcast, and WebRTC streaming.
## Create Live Input
### Using Cloudflare SDK
```typescript
import Cloudflare from 'cloudflare';
const client = new Cloudflare({ apiToken: env.CF_API_TOKEN });
const liveInput = await client.stream.liveInputs.create({
account_id: env.CF_ACCOUNT_ID,
recording: { mode: 'automatic', timeoutSeconds: 30 },
deleteRecordingAfterDays: 30
});
// Returns: { uid, rtmps, srt, webRTC }
```
### Raw fetch API
```typescript
async function createLiveInput(accountId: string, apiToken: string) {
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/live_inputs`,
{
method: 'POST',
headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
recording: { mode: 'automatic', timeoutSeconds: 30 },
deleteRecordingAfterDays: 30
})
}
);
const { result } = await response.json();
return {
uid: result.uid,
rtmps: { url: result.rtmps.url, streamKey: result.rtmps.streamKey },
srt: { url: result.srt.url, streamId: result.srt.streamId, passphrase: result.srt.passphrase },
webRTC: result.webRTC
};
}
```
## Check Live Status
```typescript
async function getLiveStatus(accountId: string, liveInputId: string, apiToken: string) {
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/live_inputs/${liveInputId}`,
{ headers: { 'Authorization': `Bearer ${apiToken}` } }
);
const { result } = await response.json();
return {
isLive: result.status?.current?.state === 'connected',
recording: result.recording,
status: result.status
};
}
```
## Simulcast (Live Outputs)
### Create Output
```typescript
async function createLiveOutput(
accountId: string, liveInputId: string, apiToken: string,
outputUrl: string, streamKey: string
) {
return fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/live_inputs/${liveInputId}/outputs`,
{
method: 'POST',
headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
url: `${outputUrl}/${streamKey}`,
enabled: true,
streamKey // For platforms like YouTube, Twitch
})
}
).then(r => r.json());
}
```
### Example: Simulcast to YouTube + Twitch
```typescript
const liveInput = await createLiveInput(accountId, apiToken);
// Add YouTube output
await createLiveOutput(
accountId, liveInput.uid, apiToken,
'rtmp://a.rtmp.youtube.com/live2',
'your-youtube-stream-key'
);
// Add Twitch output
await createLiveOutput(
accountId, liveInput.uid, apiToken,
'rtmp://live.twitch.tv/app',
'your-twitch-stream-key'
);
```
## WebRTC Streaming (WHIP/WHEP)
### Browser to Stream (WHIP)
```typescript
async function startWebRTCBroadcast(liveInputId: string) {
const pc = new RTCPeerConnection();
// Add local media tracks
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
stream.getTracks().forEach(track => pc.addTrack(track, stream));
// Create offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Send to Stream via WHIP
const response = await fetch(
`https://customer-<CODE>.cloudflarestream.com/${liveInputId}/webRTC/publish`,
{
method: 'POST',
headers: { 'Content-Type': 'application/sdp' },
body: offer.sdp
}
);
const answer = await response.text();
await pc.setRemoteDescription({ type: 'answer', sdp: answer });
}
```
### Stream to Browser (WHEP)
```typescript
async function playWebRTCStream(videoId: string) {
const pc = new RTCPeerConnection();
pc.addTransceiver('video', { direction: 'recvonly' });
pc.addTransceiver('audio', { direction: 'recvonly' });
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const response = await fetch(
`https://customer-<CODE>.cloudflarestream.com/${videoId}/webRTC/play`,
{
method: 'POST',
headers: { 'Content-Type': 'application/sdp' },
body: offer.sdp
}
);
const answer = await response.text();
await pc.setRemoteDescription({ type: 'answer', sdp: answer });
return pc;
}
```
## Recording Settings
| Mode | Behavior |
|------|----------|
| `automatic` | Record all live streams |
| `off` | No recording |
| `timeoutSeconds` | Stop recording after N seconds of inactivity |
```typescript
const recordingConfig = {
mode: 'automatic',
timeoutSeconds: 30, // Auto-stop 30s after stream ends
requireSignedURLs: true, // Require token for VOD playback
allowedOrigins: ['https://yourdomain.com']
};
```
## In This Reference
- [README.md](./README.md) - Overview and quick start
- [api.md](./api.md) - On-demand video APIs
- [configuration.md](./configuration.md) - Setup and config
- [patterns.md](./patterns.md) - Full-stack flows, best practices
- [gotchas.md](./gotchas.md) - Error codes, troubleshooting
## See Also
- [workers](../workers/) - Deploy live APIs in Workers

View File

@@ -0,0 +1,199 @@
# Stream API Reference
Upload, playback, live streaming, and management APIs.
## Upload APIs
### Direct Creator Upload (Recommended)
**Backend: Create upload URL (SDK)**
```typescript
import Cloudflare from 'cloudflare';
const client = new Cloudflare({ apiToken: env.CF_API_TOKEN });
const uploadData = await client.stream.directUpload.create({
account_id: env.CF_ACCOUNT_ID,
maxDurationSeconds: 3600,
requireSignedURLs: true,
meta: { creator: 'user-123' }
});
// Returns: { uploadURL: string, uid: string }
```
**Frontend: Upload file**
```typescript
async function uploadVideo(file: File, uploadURL: string) {
const formData = new FormData();
formData.append('file', file);
return fetch(uploadURL, { method: 'POST', body: formData }).then(r => r.json());
}
```
### Upload from URL
```typescript
const video = await client.stream.copy.create({
account_id: env.CF_ACCOUNT_ID,
url: 'https://example.com/video.mp4',
meta: { name: 'My Video' },
requireSignedURLs: false
});
```
## Playback APIs
### Embed Player (iframe)
```html
<iframe
src="https://customer-<CODE>.cloudflarestream.com/<VIDEO_ID>/iframe?autoplay=true&muted=true"
style="border: none;" height="720" width="1280"
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"
allowfullscreen="true"
></iframe>
```
### HLS/DASH Manifest URLs
```typescript
// HLS
const hlsUrl = `https://customer-<CODE>.cloudflarestream.com/${videoId}/manifest/video.m3u8`;
// DASH
const dashUrl = `https://customer-<CODE>.cloudflarestream.com/${videoId}/manifest/video.mpd`;
```
### Thumbnails
```typescript
// At specific time (seconds)
const thumb = `https://customer-<CODE>.cloudflarestream.com/${videoId}/thumbnails/thumbnail.jpg?time=10s`;
// By percentage
const thumbPct = `https://customer-<CODE>.cloudflarestream.com/${videoId}/thumbnails/thumbnail.jpg?time=50%`;
// Animated GIF
const gif = `https://customer-<CODE>.cloudflarestream.com/${videoId}/thumbnails/thumbnail.gif`;
```
## Signed URLs
```typescript
// Low volume (<1k/day): Use API
async function getSignedToken(accountId: string, videoId: string, apiToken: string) {
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/${videoId}/token`,
{
method: 'POST',
headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
exp: Math.floor(Date.now() / 1000) + 3600,
accessRules: [{ type: 'ip.geoip.country', action: 'allow', country: ['US'] }]
})
}
);
return (await response.json()).result.token;
}
// High volume: Self-sign with RS256 JWT (see "Self-Sign JWT" in patterns.md)
```
## Captions & Clips
### Upload Captions
```typescript
async function uploadCaption(
accountId: string, videoId: string, apiToken: string,
language: string, captionFile: File
) {
const formData = new FormData();
formData.append('file', captionFile);
return fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/${videoId}/captions/${language}`,
{
method: 'PUT',
headers: { 'Authorization': `Bearer ${apiToken}` },
body: formData
}
).then(r => r.json());
}
```
### Generate AI Captions
```typescript
// TODO: Requires Workers AI integration - see workers-ai reference
async function generateAICaptions(accountId: string, videoId: string, apiToken: string) {
return fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/${videoId}/captions/generate`,
{
method: 'POST',
headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ language: 'en' })
}
).then(r => r.json());
}
```
### Clip Video
```typescript
async function clipVideo(
accountId: string, videoId: string, apiToken: string,
startTime: number, endTime: number
) {
return fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/clip`,
{
method: 'POST',
headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
clippedFromVideoUID: videoId,
startTimeSeconds: startTime,
endTimeSeconds: endTime
})
}
).then(r => r.json());
}
```
## Video Management
```typescript
// List videos
const videos = await client.stream.videos.list({
account_id: env.CF_ACCOUNT_ID,
search: 'keyword' // optional
});
// Get video details
const video = await client.stream.videos.get(videoId, {
account_id: env.CF_ACCOUNT_ID
});
// Update video
await client.stream.videos.update(videoId, {
account_id: env.CF_ACCOUNT_ID,
meta: { title: 'New Title' },
requireSignedURLs: true
});
// Delete video
await client.stream.videos.delete(videoId, {
account_id: env.CF_ACCOUNT_ID
});
```
## In This Reference
- [README.md](./README.md) - Overview and quick start
- [configuration.md](./configuration.md) - Setup and config
- [api-live.md](./api-live.md) - Live streaming APIs (RTMPS/SRT/WebRTC)
- [patterns.md](./patterns.md) - Full-stack flows, best practices
- [gotchas.md](./gotchas.md) - Error codes, troubleshooting
## See Also
- [workers](../workers/) - Deploy Stream APIs in Workers

View File

@@ -0,0 +1,141 @@
# Stream Configuration
Setup, environment variables, and wrangler configuration.
## Installation
```bash
# Official Cloudflare SDK (Node.js, Workers, Pages)
npm install cloudflare
# React component library
npm install @cloudflare/stream-react
# TUS resumable uploads (large files)
npm install tus-js-client
```
## Environment Variables
```bash
# Required
CF_ACCOUNT_ID=your-account-id
CF_API_TOKEN=your-api-token
# For signed URLs (high volume)
STREAM_KEY_ID=your-key-id
STREAM_JWK=base64-encoded-jwk
# For webhooks
WEBHOOK_SECRET=your-webhook-secret
# Customer subdomain (from dashboard)
STREAM_CUSTOMER_CODE=your-customer-code
```
## Wrangler Configuration
```jsonc
{
"name": "stream-worker",
"main": "src/index.ts",
"compatibility_date": "2025-01-01", // Use current date for new projects
"vars": {
"CF_ACCOUNT_ID": "your-account-id"
}
// Store secrets: wrangler secret put CF_API_TOKEN
// wrangler secret put STREAM_KEY_ID
// wrangler secret put STREAM_JWK
// wrangler secret put WEBHOOK_SECRET
}
```
## Signing Keys (High Volume)
Create once for self-signing tokens (thousands of daily users).
**Create key**
```bash
curl -X POST \
"https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/keys" \
-H "Authorization: Bearer <API_TOKEN>"
# Save `id` and `jwk` (base64) from response
```
**Store in secrets**
```bash
wrangler secret put STREAM_KEY_ID
wrangler secret put STREAM_JWK
```
## Webhooks
**Setup webhook URL**
```bash
curl -X PUT \
"https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/webhook" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"notificationUrl": "https://your-worker.workers.dev/webhook"}'
# Save the returned `secret` for signature verification
```
**Store secret**
```bash
wrangler secret put WEBHOOK_SECRET
```
## Direct Upload / Live / Watermark Config
```typescript
// Direct upload
const uploadConfig = {
maxDurationSeconds: 3600,
expiry: new Date(Date.now() + 3600000).toISOString(),
requireSignedURLs: true,
allowedOrigins: ['https://yourdomain.com'],
meta: { creator: 'user-123' }
};
// Live input
const liveConfig = {
recording: { mode: 'automatic', timeoutSeconds: 30 },
deleteRecordingAfterDays: 30
};
// Watermark
const watermark = {
name: 'Logo', opacity: 0.7, padding: 20,
position: 'lowerRight', scale: 0.15
};
```
## Access Rules & Player Config
```typescript
// Access rules: allow US/CA, block CN/RU, or IP allowlist
const geoRestrict = [
{ type: 'ip.geoip.country', action: 'allow', country: ['US', 'CA'] },
{ type: 'any', action: 'block' }
];
// Player params for iframe
const playerParams = new URLSearchParams({
autoplay: 'true', muted: 'true', preload: 'auto', defaultTextTrack: 'en'
});
```
## In This Reference
- [README.md](./README.md) - Overview and quick start
- [api.md](./api.md) - On-demand video APIs
- [api-live.md](./api-live.md) - Live streaming APIs
- [patterns.md](./patterns.md) - Full-stack flows, best practices
- [gotchas.md](./gotchas.md) - Error codes, troubleshooting
## See Also
- [wrangler](../wrangler/) - Wrangler CLI and configuration
- [workers](../workers/) - Deploy Stream APIs in Workers

View File

@@ -0,0 +1,130 @@
# Stream Gotchas
## Common Errors
### "ERR_NON_VIDEO"
**Cause:** Uploaded file is not a valid video format
**Solution:** Ensure file is in supported format (MP4, MKV, MOV, AVI, FLV, MPEG-2 TS/PS, MXF, LXF, GXF, 3GP, WebM, MPG, QuickTime)
### "ERR_DURATION_EXCEED_CONSTRAINT"
**Cause:** Video duration exceeds `maxDurationSeconds` constraint
**Solution:** Increase `maxDurationSeconds` in direct upload config or trim video before upload
### "ERR_FETCH_ORIGIN_ERROR"
**Cause:** Failed to download video from URL (upload from URL)
**Solution:** Ensure URL is publicly accessible, uses HTTPS, and video file is available
### "ERR_MALFORMED_VIDEO"
**Cause:** Video file is corrupted or improperly encoded
**Solution:** Re-encode video using FFmpeg or check source file integrity
### "ERR_DURATION_TOO_SHORT"
**Cause:** Video must be at least 0.1 seconds long
**Solution:** Ensure video has valid duration (not a single frame)
## Troubleshooting
### Video stuck in "inprogress" state
- **Cause**: Processing large/complex video
- **Solution**: Wait up to 5 minutes for processing; use webhooks instead of polling
### Signed URL returns 403
- **Cause**: Token expired or invalid signature
- **Solution**: Check expiration timestamp, verify JWK is correct, ensure clock sync
### Live stream not connecting
- **Cause**: Invalid RTMPS URL or stream key
- **Solution**: Use exact URL/key from API, ensure firewall allows outbound 443
### Webhook signature verification fails
- **Cause**: Incorrect secret or timestamp window
- **Solution**: Use exact secret from webhook setup, allow 5-minute timestamp drift
### Video uploads but isn't visible
- **Cause**: `requireSignedURLs` enabled without providing token
- **Solution**: Generate signed token or set `requireSignedURLs: false` for public videos
### Player shows infinite loading
- **Cause**: CORS issue with allowedOrigins
- **Solution**: Add your domain to `allowedOrigins` array
## Limits
| Resource | Limit |
|----------|-------|
| Max file size | 30 GB |
| Max frame rate | 60 fps (recommended) |
| Max duration per direct upload | Configurable via `maxDurationSeconds` |
| Token generation (API endpoint) | 1,000/day recommended (use signing keys for higher) |
| Live input outputs (simulcast) | 5 per live input |
| Webhook retry attempts | 5 (exponential backoff) |
| Webhook timeout | 30 seconds |
| Caption file size | 5 MB |
| Watermark image size | 2 MB |
| Metadata keys per video | Unlimited |
| Search results per page | Max 1,000 |
## Performance Issues
### Upload is slow
- **Cause**: Large file size or network constraints
- **Solution**: Use TUS resumable upload, compress video before upload, check bandwidth
### Playback buffering
- **Cause**: Network congestion or low bandwidth
- **Solution**: Use ABR (adaptive bitrate) with HLS/DASH, reduce max bitrate
### High processing time
- **Cause**: Complex video codec, high resolution
- **Solution**: Pre-encode with H.264 (most efficient), reduce resolution
## Type Safety
```typescript
// Error response type
interface StreamError {
success: false;
errors: Array<{
code: number;
message: string;
}>;
}
// Handle errors
async function uploadWithErrorHandling(url: string, file: File) {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(url, { method: 'POST', body: formData });
const result = await response.json();
if (!result.success) {
throw new Error(result.errors[0]?.message || 'Upload failed');
}
return result;
}
```
## Security Gotchas
1. **Never expose API token in frontend** - Use direct creator uploads
2. **Always verify webhook signatures** - Prevent spoofed notifications
3. **Set appropriate token expiration** - Short-lived for security
4. **Use requireSignedURLs for private content** - Prevent unauthorized access
5. **Whitelist allowedOrigins** - Prevent hotlinking/embedding on unauthorized sites
## 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
- [patterns.md](./patterns.md) - Full-stack flows, best practices
## See Also
- [workers](../workers/) - Deploy Stream APIs securely

View File

@@ -0,0 +1,184 @@
# 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 <Stream controls src={token ? `${videoId}?token=${token}` : videoId} responsive />;
}
```
## Full-Stack Upload Flow
**Backend API (Workers/Pages)**
```typescript
import Cloudflare from 'cloudflare';
export default {
async fetch(request: Request, env: Env): Promise<Response> {
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 (
<div>
<input type="file" accept="video/*" onChange={(e) => e.target.files?.[0] && handleUpload(e.target.files[0])} disabled={uploading} />
{uploading && <progress value={progress} max={100} />}
</div>
);
}
```
## 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<string>((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<Response> {
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<boolean> {
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