[Nom de la Configuration Cloudflare Stream]
[Nom de la Configuration Cloudflare Stream]
Documentation technique
Type de ressource : Configuration Cloudflare Stream (Direct Upload / Lecture / Métadonnées)
Fichier source :services/media-upload/src/index.tsou configuration associée
Statut : [Actif / Déprécié / En développement]
1. Vue d’overview
1.1. Description
[Description concise du rôle de cette configuration Stream dans l’architecture du site]
1.2. Objectif
- [Objectif principal : ex. héberger les vidéos du portfolio avec transcoding automatique]
- [Bénéfice pour l’architecture : ex. adaptive bitrate sans gestion serveur]
1.3. Périmètre concerné
- Type de contenu : [Portfolio / Blog / Pages vitrine / Les deux]
- Format source : [MP4 / MOV / WebM / Les deux]
- Accès : [Public / Privé / Signé / Les deux]
- Durée max : [3600s (1h) / 7200s (2h) / etc.]
2. Architecture et flux
2.1. Diagramme de flux
[Sanity Studio] │ (demande d'upload vidéo) ▼[Worker media-upload (L3)] │ • Génération Direct Upload Token │ • Retourne uploadURL + uid ▼[Sanity Studio] │ • Upload direct vers Cloudflare Stream │ • Stocke l'uid dans le document ▼[Cloudflare Stream] │ • Transcoding automatique (multi-bitrate) │ • Génération des thumbnails │ • Création du player ▼[Astro / Site] │ • Iframe ou SDK Stream ▼[Utilisateur final]2.2. Composants impliqués
| Composant | Rôle | Couche |
|---|---|---|
| Bibliothèque Stream | Stockage + transcoding + distribution | Infrastructure |
Worker media-upload |
Génération de tokens Direct Upload | L3 |
| Sanity | Stockage de l’UID Stream | L1 |
| Astro | Intégration du player (iframe/SDK) | L2 |
| Cloudflare CDN | Distribution Edge | Infrastructure |
3. Configuration
3.1. Variables d’environnement requises
| Variable | Description | Obligatoire | Valeur par défaut |
|---|---|---|---|
CLOUDFLARE_ACCOUNT_ID |
ID du compte Cloudflare | ✅ | - |
CLOUDFLARE_STREAM_API_TOKEN |
Token API Stream (avec permissions stream:write) |
✅ | - |
STREAM_PUBLIC_DOMAIN |
Domaine public pour les vidéos (ex: videos.votredomaine.com) |
❌ | iframe.videodelivery.net |
3.2. Configuration du token API1/2
Dans le Dashboard Cloudflare :
- My Profile → API Tokens → Create Token
- Template : “Create Custom Token”
- Permissions :
- Account → Cloudflare Stream → Edit
- Account Resources : Votre compte
- Copier le token et l’injecter via Wrangler :
3.2. Configuration du token API2/2
wrangler secret put CLOUDFLARE_STREAM_API_TOKEN --env productionwrangler secret put CLOUDFLARE_STREAM_API_TOKEN --env staging3.3. Configuration du domaine personnalisé (optionnel)
Dans le Dashboard Cloudflare :
- Stream → Settings → Custom Domains
- Add Domain :
videos.votredomaine.com - Cloudflare proxy activé (orange cloud)
4. Upload de vidéos
4.1. Flux d’upload via Direct Upload Token1/2
4.1. Flux d’upload via Direct Upload Token2/2
// services/media-upload/src/index.ts (extrait)export default { async fetch(request: Request, env: Env): Promise<Response> { const { type, filename, maxDuration } = await request.json();
if (type !== 'video') { return new Response('Type non supporté', { status: 400 }); }
// Génération du Direct Upload Token const streamRes = await fetch( `https://api.cloudflare.com/client/v4/accounts/${env.CLOUDFLARE_ACCOUNT_ID}/stream/direct_upload`, { method: 'POST', headers: { 'Authorization': `Bearer ${env.CLOUDFLARE_STREAM_API_TOKEN}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ maxDurationSeconds: maxDuration || 3600, allowedOrigins: ['https://votresite.com', 'https://studio.votresite.com'], requireSignedURLs: false, watermarks: [], }), } );
const streamData = await streamRes.json();
return new Response(JSON.stringify({ uploadUrl: streamData.result.uploadURL, uid: streamData.result.uid, stagingVideo: streamData.result.stagingVideo, }), { status: 200 }); },};4.2. Upload depuis Sanity Studio
const handleVideoUpload = async (file: File) => { // 1. Demander un token d'upload au Worker const res = await fetch('https://api.votredomaine.com/media-upload', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'video', filename: file.name, maxDuration: 3600, }), });
const { uploadUrl, uid } = await res.json();
// 2. Upload direct vers Cloudflare Stream (sans transiter par le Worker) const formData = new FormData(); formData.append('file', file);
await fetch(uploadUrl, { method: 'POST', body: formData, });
// 3. Retourner l'UID à Sanity pour stockage return { _type: 'video', streamUid: uid, title: file.name.replace(/\.[^/.]+$/, ''), originalFilename: file.name, };};4.3. Vérification du statut de traitement
// Vérifier si la vidéo est prête (transcoding terminé)const checkVideoStatus = async (uid: string, env: Env) => { const res = await fetch( `https://api.cloudflare.com/client/v4/accounts/${env.CLOUDFLARE_ACCOUNT_ID}/stream/${uid}`, { headers: { 'Authorization': `Bearer ${env.CLOUDFLARE_STREAM_API_TOKEN}`, }, } );
const data = await res.json(); return { ready: data.result.ready, status: data.result.status.state, // 'queued', 'inprogress', 'ready', 'error' thumbnail: data.result.thumbnail, duration: data.result.duration, playback: data.result.playback.hls, };};5. Intégration dans Sanity
5.1. Schéma de champ vidéo1/2
5.1. Schéma de champ vidéo2/2
import { defineField } from 'sanity';
export const videoField = defineField({ name: 'video', title: 'Vidéo', type: 'object', fields: [ { name: 'streamUid', title: 'UID Stream', type: 'string', description: 'UID de la vidéo dans Cloudflare Stream (généré automatiquement)', readOnly: true, }, { name: 'title', title: 'Titre', type: 'string', validation: Rule => Rule.required(), }, { name: 'description', title: 'Description', type: 'text', }, { name: 'poster', title: 'Image de couverture', type: 'image', options: { hotspot: true }, description: 'Générée automatiquement par Stream, mais peut être remplacée', }, ], preview: { select: { title: 'title', uid: 'streamUid', }, prepare({ title, uid }) { return { title, subtitle: uid ? `UID: ${uid}` : 'Non uploadée', }; }, },});5.2. Stockage dans les documents
{ "_type": "project", "title": "Projet Alpha", "video": { "_type": "video", "streamUid": "abc123def456", "title": "Présentation du projet", "description": "Vidéo de démonstration du projet Alpha", "poster": { "_type": "image", "asset": { "_type": "reference", "_ref": "image-xyz789" } } }}6. Intégration dans Astro
6.1. Composant VideoPlayer1/2
6.1. Composant VideoPlayer2/2
---interface Props { uid: string; title: string; poster?: string; autoplay?: boolean; muted?: boolean; controls?: boolean;}
const { uid, title, poster, autoplay = false, muted = false, controls = true} = Astro.props;
const iframeSrc = new URL(`https://iframe.videodelivery.net/${uid}`);iframeSrc.searchParams.set('poster', poster || '');iframeSrc.searchParams.set('autoplay', autoplay.toString());iframeSrc.searchParams.set('muted', muted.toString());iframeSrc.searchParams.set('controls', controls.toString());iframeSrc.searchParams.set('loop', 'false');iframeSrc.searchParams.set('preload', 'metadata');---
<div class="video-container"> <iframe src={iframeSrc.toString()} title={title} allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture" allowfullscreen loading="lazy" ></iframe></div>
<style> .video-container { position: relative; width: 100%; padding-bottom: 56.25%; /* 16:9 */ height: 0; overflow: hidden; }
.video-container iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0; }</style>6.2. Utilisation dans une page
---import { getSanityClient } from '../../lib/sanity';import VideoPlayer from '../../components/ui/VideoPlayer.astro';
const project = await getSanityClient(Astro).fetch(`*[_type == "project" && slug.current == $slug][0]{ title, video { streamUid, title, poster { url } }}`, { slug: Astro.params.slug });---
<VideoPlayer uid={project.video.streamUid} title={project.video.title} poster={project.video.poster?.url} controls={true}/>6.3. Utilisation du SDK Stream (optionnel)
---// Pour un contrôle plus fin, utiliser le SDK Streamimport { StreamPlayer } from '@cloudflare/stream-react';---
<StreamPlayer src={project.video.streamUid} controls={true} autoplay={false} muted={false} poster={project.video.poster?.url} responsive={true}/>7. Sécurité et accès
7.1. Types d’accès
| Type | Usage | Configuration |
|---|---|---|
| Public | Vidéos du portfolio, démos | requireSignedURLs: false |
| Signé | Vidéos privées, previews | requireSignedURLs: true + génération de tokens |
| Restreint par domaine | Vidéos accessibles uniquement depuis votre site | allowedOrigins: ['https://votresite.com'] |
7.2. Génération d’URLs signées (si nécessaire)
// Générer une URL signée pour une vidéo privéeconst generateSignedUrl = async (uid: string, env: Env, expiration: number = 3600) => { const res = await fetch( `https://api.cloudflare.com/client/v4/accounts/${env.CLOUDFLARE_ACCOUNT_ID}/stream/${uid}/token`, { method: 'POST', headers: { 'Authorization': `Bearer ${env.CLOUDFLARE_STREAM_API_TOKEN}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ exp: Math.floor(Date.now() / 1000) + expiration, accessRules: [ { type: 'ip.src', country: ['FR', 'US'], // Optionnel : restreindre par pays }, ], }), } );
const data = await res.json(); return `https://iframe.videodelivery.net/${uid}?token=${data.result.token}`;};7.3. Règles de sécurité
- ✅ Le token API Stream ne doit jamais être exposé côté client
- ✅ Utiliser
allowedOriginspour restreindre l’embed aux domaines autorisés - ✅ Activer
requireSignedURLspour les vidéos sensibles - ✅ Définir des durées d’expiration courtes pour les tokens signés (1h max)
- ✅ Ne pas stocker les vidéos sensibles dans Stream (utiliser R2 avec URLs signées)
8. Performance et optimisation
8.1. Optimisation automatique par Stream
| Fonctionnalité | Description | Gain |
|---|---|---|
| Transcoding automatique | Génération de multiples bitrates (240p à 1080p) | Adaptive bitrate |
| Compression | H.264 + AAC optimisés | Réduction de taille |
| Thumbnails | Génération automatique d’images de couverture | UX améliorée |
| HLS + DASH | Protocoles adaptatifs | Compatibilité maximale |
| CDN Edge | Distribution mondiale | Latence réduite |
8.2. Bonnes pratiques
- ✅ Uploader des vidéos de qualité raisonnable (1080p max, bitrate 5-10 Mbps)
- ✅ Limiter la durée à 1 heure (3600s) pour les vidéos courtes
- ✅ Utiliser le format MP4 (H.264 + AAC) pour une compatibilité maximale
- ✅ Fournir un poster personnalisé si l’automatique n’est pas pertinent
- ✅ Utiliser
preload="metadata"pour éviter le chargement complet au chargement de la page - ✅ Activer
loading="lazy"sur les iframes
8.3. Limites et quotas
| Limite | Valeur | Rationale |
|---|---|---|
| Durée max | 3600s (1h) par vidéo | Plan Free/Pro |
| Taille max | 12 GB par vidéo | Limite upload |
| Stockage | 100h (Free) / Illimité (Pro) | Quota Stream |
| Lecture | 1000 min/mois (Free) / Illimité (Pro) | Quota Stream |
| Uploads simultanés | 10 par compte | Limite API |
9. Débogage et troubleshooting
9.1. Problèmes courants
| Symptôme | Cause probable | Solution |
|---|---|---|
| Vidéo ne se charge pas | UID incorrect ou vidéo non prête | Vérifier l’UID et le statut via l’API |
| “Video not found” | UID invalide ou vidéo supprimée | Vérifier l’UID dans Sanity et Stream |
| Lecture bloquée | CORS ou allowedOrigins incorrect | Ajouter le domaine dans allowedOrigins |
| Qualité faible | Upload d’une vidéo basse qualité | Uploader en 1080p minimum |
| Transcoding long | Vidéo très longue ou lourde | Attendre ou optimiser la source |
| Player ne s’affiche pas | Iframe bloquée par CSP | Ajouter iframe.videodelivery.net dans CSP |
9.2. Logs et monitoring
# Vérifier le statut d'une vidéo via l'APIcurl -H "Authorization: Bearer [token]" \ "https://api.cloudflare.com/client/v4/accounts/[account-id]/stream/[uid]"
# Vérifier les logs d'upload dans le Dashboard Stream# Cloudflare Dashboard → Stream → Videos → [Video] → Analytics
# Vérifier les logs du Worker media-uploadwrangler tail media-upload9.3. Tests manuels
# Test de génération de token d'uploadcurl -X POST "https://api.votredomaine.com/media-upload" \ -H "Content-Type: application/json" \ -d '{"type": "video", "filename": "test.mp4", "maxDuration": 3600}'
# Test d'upload direct vers Streamcurl -X POST "[upload-url]" \ -F "file=@test.mp4"
# Test de vérification du statutcurl -H "Authorization: Bearer [token]" \ "https://api.cloudflare.com/client/v4/accounts/[account-id]/stream/[uid]"10. Bonnes pratiques
10.1. Pour les développeurs
- ✅ Toujours vérifier le statut
readyavant d’afficher une vidéo - ✅ Gérer les erreurs de transcoding (statut
error) - ✅ Utiliser des URLs signées pour les vidéos sensibles
- ✅ Logger les uploads et les erreurs pour le debugging
- ✅ Tester les vidéos sur différents appareils (mobile, desktop, tablet)
10.2. Pour les contributeurs
- ✅ Optimiser les vidéos avant upload (1080p, H.264, bitrate raisonnable)
- ✅ Utiliser des titres descriptifs pour les vidéos
- ✅ Vérifier le rendu sur différents appareils avant publication
- ✅ Ne pas uploader de vidéos de plus d’1 heure
- ✅ Fournir une description pour l’accessibilité
10.3. Considérations de coût
- ⚠️ Stream facture le stockage + le transcoding + la lecture
- ✅ Supprimer les vidéos obsolètes régulièrement
- ✅ Utiliser des durées courtes pour les démos (< 5 min)
- ✅ Optimiser les vidéos source pour réduire le transcoding
- ✅ Surveiller les quotas dans le Dashboard Cloudflare
11. Évolutions et roadmap
11.1. Améliorations planifiées
- [Évolution 1 : ex. Support des sous-titres (VTT)]
- [Évolution 2 : ex. Analytics avancés (views, engagement)]
- [Évolution 3 : ex. Watermarking dynamique]
11.2. Limitations connues
- ❌ Stream ne supporte pas nativement les vidéos 4K
- ❌ Les vidéos ne peuvent pas être téléchargées (streaming uniquement)
- ❌ Le transcoding peut prendre plusieurs minutes pour les vidéos longues
- ❌ Les URLs signées ont une durée de vie limitée (pas de partage long terme)
12. Ressources liées1/2
- Documentation Cloudflare Stream : Getting Started
- Documentation Cloudflare Stream : Direct Upload
- Documentation Cloudflare Stream : Signed URLs
- DAT L1 : §4.8 - Gestion des Médias (Cloudflare R2 + Stream)
- DAT L2 : §5.4 - Consommation et optimisation des médias
- DAT L3 : §5.3 - Worker
media-upload
12. Ressources liées2/2
- Fichiers concernés :
services/media-upload/src/index.tssanity-studio/schemas/fields/video.tssrc/components/ui/VideoPlayer.astro- Configuration Dashboard Cloudflare → Stream
13. Historique des modifications1/2
| Date | Version | Description | Auteur |
|---|---|---|---|
| YYYY-MM-DD | 1.0 | Création initiale | [Nom] |
| YYYY-MM-DD | 1.1 | [Modification] | [Nom] |
13. Historique des modifications2/2
---
**Points clés de ce template :**- Documente le flux complet d'upload (Sanity → Worker → Stream)- Détaille l'intégration avec Sanity (schéma, stockage UID)- Fournit l'intégration avec Astro (composant VideoPlayer, iframe, SDK)- Couvre la sécurité (accès public/signé, allowedOrigins)- Inclut les bonnes pratiques de performance et d'optimisation- Sépare clairement les responsabilités (développeurs vs contributeurs)