Douze minutes. C’est le temps qu’a mis Cloudflare Stream à encoder une vidéo de conférence de dix-huit minutes en cinq résolutions différentes, lors d’un test mené sur un site associatif qui publie chaque semaine l’enregistrement de ses ateliers. Douze minutes pendant lesquelles un rédacteur, s’il attendait la fin du traitement pour publier son article, resterait bloqué devant un écran de chargement. C’est exactement le problème que pose l’intégration naïve d’un service d’encodage vidéo à un WordPress headless : traiter la vidéo de façon synchrone, dans la même requête que la création du contenu.
La bonne architecture sépare les deux préoccupations. WordPress reçoit d’abord les métadonnées de l’article et un identifiant de vidéo encore en cours de traitement ; Cloudflare Stream prévient ensuite, via un webhook, que l’encodage est terminé et que la vidéo est prête à être diffusée. Cet article détaille cette architecture, du côté back-end uniquement : la façon dont le lecteur vidéo est ensuite affiché côté front n’est pas traitée ici, elle mérite un article à part entière.
Le problème du traitement synchrone
Sur un site headless classique, un article est créé via l’API REST WordPress ou WPGraphQL, avec son contenu, ses métadonnées ACF et, potentiellement, un fichier vidéo à associer. Si l’upload et l’encodage de la vidéo se font dans le même cycle de requête que la création du post, plusieurs problèmes surgissent : le délai d’exécution PHP peut être dépassé, la connexion HTTP peut être coupée par un proxy intermédiaire avant la fin du traitement, et l’expérience de l’éditeur devient franchement désagréable pour un contenu qui pèse plusieurs centaines de mégaoctets.
Cloudflare Stream propose justement un mode d’ingestion asynchrone : on envoie la vidéo brute via son API (ou on lui donne une URL à récupérer), il renvoie immédiatement un identifiant unique, et il notifie un webhook lorsque l’encodage multi-résolution est terminé. C’est ce webhook qu’il faut brancher sur WordPress pour fermer la boucle.
Architecture retenue : trois étapes découplées
L’arborescence logique du traitement ressemble à ceci :
1. Éditeur crée l'article (statut "en attente de vidéo")
└── WordPress appelle l'API Cloudflare Stream (upload direct ou par URL)
└── Cloudflare renvoie un "uid" de vidéo, stocké en meta post
2. Cloudflare encode la vidéo en tâche de fond (jusqu'à 15 min)
└── Aucune action WordPress requise pendant cette phase
3. Cloudflare Stream notifie la fin de traitement
└── POST vers /wp-json/monsite/v1/video-webhook
└── Vérification de la signature webhook-signature
└── Mise à jour de la meta "video_status" à "ready"
└── Le post passe automatiquement en statut "publish"

Cette séparation en trois étapes a un avantage direct pour l’éditeur : il peut enregistrer son brouillon, quitter l’écran, et revenir plus tard constater que l’article est passé en ligne tout seul, sans avoir eu à surveiller une barre de progression.
Ce que reçoit l’endpoint et comment il le traite
L’endpoint REST personnalisé s’enregistre avec register_rest_route(), sur un espace de noms dédié, en méthode POST uniquement :
add_action( 'rest_api_init', function () {
register_rest_route( 'monsite/v1', '/video-webhook', array(
'methods' => 'POST',
'callback' => 'monsite_handle_stream_webhook',
'permission_callback' => 'monsite_verify_stream_signature',
) );
} );
Le corps de la requête envoyée par Cloudflare Stream contient, entre autres, l’identifiant de la vidéo (uid), son statut (ready ou error) et sa durée. La fonction de traitement retrouve le post WordPress correspondant grâce à une requête WP_Query filtrée par meta, puis met à jour son statut :
- Recherche du post via
meta_querysur la clé_stream_video_uid. - Mise à jour de la meta
_stream_video_statusà la valeur reçue. - Passage du post de
draftàpublishsi le statut estready, viawp_update_post(). - Journalisation de l’événement pour pouvoir diagnostiquer un échec d’encodage plus tard.
Sécuriser le webhook entrant
Un webhook exposé publiquement est une porte ouverte si rien ne vérifie que l’appelant est bien Cloudflare. Le service signe chaque requête avec un secret partagé, transmis dans l’en-tête Webhook-Signature. La fonction de permission recalcule cette signature côté WordPress à partir du corps brut de la requête et compare les deux chaînes avec hash_equals(), pour éviter une comparaison vulnérable aux attaques temporelles :
function monsite_verify_stream_signature( $request ) {
$signature = $request->get_header( 'webhook-signature' );
$body = $request->get_body();
$secret = getenv( 'STREAM_WEBHOOK_SECRET' );
$computed = hash_hmac( 'sha256', $body, $secret );
return hash_equals( $computed, $signature );
}
Sur ce type d’intégration, mieux vaut toujours partir du principe qu’un tiers finira, un jour, par appeler l’endpoint sans y être invité. Le tester avec un corps de requête vide, une signature fausse, et une signature valide mais rejouée deux fois, fait partie de la checklist minimale avant mise en production.
Gérer les cas d’échec d’encodage
Cloudflare Stream peut aussi notifier un échec, par exemple si le fichier source est corrompu ou dans un format non supporté. Dans ce cas, le post ne doit surtout pas passer en publish : il reste en brouillon, avec une meta _stream_video_status à error, et une notification est envoyée à l’éditeur (par e-mail ou via une alerte dans l’interface d’administration). Sans ce garde-fou, un article vide de vidéo peut se retrouver publié silencieusement, ce qui est pire qu’un simple retard de publication.
Notre verdict
Découpler l’encodage vidéo de la publication d’un contenu n’est pas un luxe d’architecture, c’est une nécessité dès que les vidéos dépassent quelques minutes. Le webhook Cloudflare Stream, correctement signé et vérifié, permet de fermer la boucle sans jamais bloquer l’éditeur ni exposer un endpoint REST à n’importe quel appelant. La suite logique de ce chantier, à savoir la façon dont le front headless affiche ensuite le lecteur vidéo en tenant compte des différentes résolutions disponibles, reste un sujet distinct, avec ses propres arbitrages de performance.