Skip to content

[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.ts ou 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 :

  1. My Profile → API Tokens → Create Token
  2. Template : “Create Custom Token”
  3. Permissions :
    • Account → Cloudflare Stream → Edit
  4. Account Resources : Votre compte
  5. Copier le token et l’injecter via Wrangler :

3.2. Configuration du token API2/2

Terminal window
wrangler secret put CLOUDFLARE_STREAM_API_TOKEN --env production
wrangler secret put CLOUDFLARE_STREAM_API_TOKEN --env staging

3.3. Configuration du domaine personnalisé (optionnel)

Dans le Dashboard Cloudflare :

  1. Stream → Settings → Custom Domains
  2. Add Domain : videos.votredomaine.com
  3. 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

sanity-studio/plugins/video-upload/input.tsx
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

sanity-studio/schemas/fields/video.ts
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

src/components/ui/VideoPlayer.astro
---
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

src/pages/portfolio/[slug].astro
---
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 Stream
import { 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ée
const 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 allowedOrigins pour restreindre l’embed aux domaines autorisés
  • ✅ Activer requireSignedURLs pour 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

Terminal window
# Vérifier le statut d'une vidéo via l'API
curl -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-upload
wrangler tail media-upload

9.3. Tests manuels

Terminal window
# Test de génération de token d'upload
curl -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 Stream
curl -X POST "[upload-url]" \
-F "file=@test.mp4"
# Test de vérification du statut
curl -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 ready avant 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.ts
    • sanity-studio/schemas/fields/video.ts
    • src/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)
Switch to normal mode

Contents