mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 06:11:27 -07:00
update skills
This commit is contained in:
184
.agents/skills/cloudflare-deploy/references/stream/patterns.md
Normal file
184
.agents/skills/cloudflare-deploy/references/stream/patterns.md
Normal 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
|
||||
Reference in New Issue
Block a user