Skip to content

[Nom de la Configuration Cloudflare Stream]

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]

[Description concise du rîle de cette configuration Stream dans l’architecture du site]

  • [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]
  • 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.]
[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]
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
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

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 :
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)

Section titled “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)
// 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 });
},
};
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,
};
};
// 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,
};
};
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',
};
},
},
});
{
"_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"
}
}
}
}
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>
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}
/>
---
// 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}
/>
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)

Section titled “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}`;
};
  • ✅ 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)
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
  • ✅ 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
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
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
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
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]"
  • ✅ 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)
  • ✅ 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Ă©
  • ⚠ 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
  • [Évolution 1 : ex. Support des sous-titres (VTT)]
  • [Évolution 2 : ex. Analytics avancĂ©s (views, engagement)]
  • [Évolution 3 : ex. Watermarking dynamique]
  • ❌ 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)
  • 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
  • Fichiers concernĂ©s :
    • services/media-upload/src/index.ts
    • sanity-studio/schemas/fields/video.ts
    • src/components/ui/VideoPlayer.astro
    • Configuration Dashboard Cloudflare → Stream
Date Version Description Auteur
YYYY-MM-DD 1.0 Création initiale [Nom]
YYYY-MM-DD 1.1 [Modification] [Nom]
---
**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)
Edit page

Last updated: