Introduction
Le streaming vidéo en PHP, est-ce vraiment une bonne idée ?
Lorsqu’on parle de diffuser des vidéos sur un site web, on pense souvent à des solutions comme YouTube ou Vimeo. Pourtant, de plus en plus de projets souhaitent héberger et gérer leurs propres vidéos directement, pour des raisons de contrôle, de branding ou de performance. Mais diffuser de la vidéo, c’est aussi gérer le média le plus lourd du web, avec tous les problèmes que cela implique : bande passante, compression, adaptation de qualité, mise en cache, et fluidité de lecture.
Imaginez par exemple que vous souhaitiez ajouter une vidéo en arrière-plan sur la page d’accueil de votre site. Vous aimeriez que, comme sur YouTube, la qualité s’adapte automatiquement au débit Internet de chaque utilisateur pour garantir une lecture fluide, sans sacrifier les performances du site.
Symfony 7 est-il prêt pour ce genre de défi ? Peut-on réellement mettre en place un streaming HLS (HTTP Live Streaming) en PHP, tout en tirant parti de Varnish pour la mise en cache, de FFMPEG pour l’encodage adaptatif, et d’un peu de JavaScript pour ajuster le buffer et la qualité en temps réel ?
Dans cet article, nous allons créer un POC (Proof of Concept) complet pour comprendre comment diffuser efficacement de la vidéo sur un site Symfony 7, sans ralentir l’expérience utilisateur.
Projet simple et moderne : Symfony 7.3, FrankenPHP, FFMPEG et Varnish
Pour ce proof of concept (POC), on va construire un projet léger mais complet basé sur :
- Symfony 7.3 comme framework principal,
- FrankenPHP pour le serveur web performant et natif PHP,
- FFMPEG pour l’encodage et la génération des segments HLS,
- Varnish en reverse proxy pour le cache,
- PostgreSQL pour la persistance (stockage des infos vidéos),
- et un frontend simple basé sur PicoCSS pour ne pas perdre de temps sur le design.
Pourquoi cette stack ?
Parce qu’on cherche une solution moderne et performante, tout en restant facile à déployer avec Docker. L’objectif n’est pas encore de gérer la sécurité (login, rôles, ACL), mais simplement de mettre en place un système de streaming adaptatif fonctionnel.
Architecture Docker
Pour que notre POC soit réaliste et facilement déployable, nous allons construire une architecture Docker modulaire.
Le cœur de l’application repose sur Symfony 7.3 exécuté avec FrankenPHP, et nous allons y ajouter tous les services nécessaires pour gérer efficacement le streaming vidéo adaptatif.
Notre docker-compose.yml
contiendra :
- Conteneur Symfony/FrankenPHP → héberge l’application Symfony, le serveur web intégré et éventuellement FFMPEG si l’on choisit de l’installer directement dans cette image.
- Conteneur FFMPEG (optionnel) → permet d’encoder et segmenter les vidéos sans surcharger le conteneur principal.
- Conteneur Varnish → agit comme reverse proxy et cache haute performance pour les segments vidéo HLS et les playlists
.m3u8
. - Conteneur PostgreSQL → stocke les métadonnées des vidéos (titre, URL HLS, formats disponibles).
- Conteneur RabbitMQ → gère le traitement asynchrone (ex. lancer un encodage vidéo en arrière-plan sans bloquer la requête HTTP).
Pourquoi RabbitMQ dans un projet vidéo ?
Dans un workflow vidéo, l’encodage HLS peut durer plusieurs secondes voire minutes selon la taille et la résolution.
Pour ne pas bloquer l’utilisateur, on envoie la tâche dans RabbitMQ depuis Symfony (via Messenger), et un worker dédié exécute le traitement FFMPEG.
Ainsi :
On prépare le terrain pour une mise à l’échelle facile (scaling horizontal).
L’utilisateur peut continuer à naviguer sur le site,
Les encodages peuvent être mis en file et traités en parallèle,
La vidéo : le souci majeur
Quand on parle de streaming, le problème numéro un, c’est la taille des fichiers vidéo. Une vidéo HD ou 4K peut peser plusieurs centaines de mégaoctets, voire plusieurs gigas.
Cela pose rapidement des problèmes : temps d’upload, ressources serveur monopolisées, risques de timeout… et c’est là qu’intervient le traitement asynchrone.
Pourquoi faire de l’async dans un POC ?
À première vue, mettre en place RabbitMQ + worker dédié pour un simple POC peut sembler overkill. Après tout, on pourrait simplement traiter la vidéo dans la même requête HTTP.
Mais ici, nous sommes en local, donc aucune contrainte de cloud ou de budget. C’est justement le moment idéal pour tester une architecture plus ambitieuse et préparer le terrain pour un vrai déploiement.
L’idée : un worker dédié à l’encodage vidéo
Puisque nous utilisons FrankenPHP, nous pouvons très bien créer un worker PHP dédié à l’encodage et au traitement des vidéos.
Voici la logique :
- L’utilisateur charge une vidéo depuis l’interface
/admin
. - Le fichier est envoyé au serveur (ici nous avons augmenté la limite d’upload PHP à 500 Mo dans
php.ini
). - Au lieu de traiter directement le fichier, Symfony envoie un message dans RabbitMQ.
- Un worker asynchrone (via Symfony Messenger) prend la tâche en charge, exécute FFMPEG pour générer les segments HLS, puis met à jour la base PostgreSQL avec l’URL finale.
Cette méthode présente plusieurs avantages :
Séparation claire des responsabilités (API → traitement → diffusion)
Pas de blocage utilisateur (l’upload se termine vite et l’encodage se fait en arrière-plan)
Scalabilité (on peut multiplier les workers si nécessaire)
Dockerfile : préparer FrankenPHP pour la vidéo
Tout ne se joue pas dans le docker-compose.yml
. Le Dockerfile
est également un point clé, surtout si l’on veut gérer l’upload multi-résolution directement dans un environnement PHP.
Par défaut, PHP est pensé pour traiter des formulaires, des images ou de petits fichiers. Mais une vidéo en 1080p, ça n’a rien à voir avec un JPEG de 300 ko : on parle facilement de plusieurs centaines de mégaoctets, avec des temps de traitement longs.
C’est pourquoi il faut adapter les limites PHP :
upload_max_filesize
etpost_max_size
poussés à 500M (sinon impossible d’uploader une vidéo).max_execution_time
etmax_input_time
augmentés à 300 secondes pour éviter les timeouts lors d’uploads/encodages.memory_limit
relevé pour absorber la charge de FFmpeg et des workers.
Ensuite, on ajoute FFmpeg directement dans l’image, puisqu’il est le moteur central de l’encodage et du découpage HLS.
Voici le Dockerfile
utilisé dans ce POC :
FROM dunglas/frankenphp:latest
WORKDIR /app
RUN install-php-extensions \
pdo_pgsql \
gd \
intl \
zip \
opcache \
amqp
# Install FFmpeg
RUN apt-get update && apt-get install -y \
ffmpeg \
&& rm -rf /var/lib/apt/lists/*
# Configure PHP for video uploads
RUN echo "upload_max_filesize = 500M" >> /usr/local/etc/php/conf.d/uploads.ini \
&& echo "post_max_size = 500M" >> /usr/local/etc/php/conf.d/uploads.ini \
&& echo "max_execution_time = 300" >> /usr/local/etc/php/conf.d/uploads.ini \
&& echo "memory_limit = 512M" >> /usr/local/etc/php/conf.d/uploads.ini \
&& echo "max_input_time = 300" >> /usr/local/etc/php/conf.d/uploads.ini
# Copy Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Copy application
COPY . .
RUN chmod +x bin/console
# FrankenPHP with Symfony configuration
ENV FRANKENPHP_CONFIG="worker /app/public/index.php"
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
ENV SERVER_NAME=localhost
EXPOSE 80 443
# Default command will be provided by the base image
Ce Dockerfile
n’est évidemment pas “parfait” : en production, il faudrait affiner, notamment sur la taille finale de l’image, la séparation des rôles (un conteneur FFmpeg à part pour l’encodage lourd, par exemple), et la gestion fine de la mémoire.
Mais pour un POC, c’est une excellente base : tout est en place pour que Symfony 7 + FrankenPHP puissent uploader, encoder et streamer de la vidéo dans de bonnes conditions.
Workers asynchrones : encodage vidéo “comme en prod”, mais en local
L’objectif du POC est d’être réaliste : on veut simuler une archi où l’upload ne bloque jamais l’utilisateur et où l’encodage HLS multi-résolutions se fait en arrière-plan via Symfony Messenger + RabbitMQ.
Le flux est simple et robuste :
Admin (EasyAdmin) → Formulaire d’upload → Event Subscriber → File RabbitMQ → Worker dédié → FFmpeg → Mise à jour BDD → Fichiers HLS (public/…)
Service worker
dans docker-compose
worker:
build:
context: .
dockerfile: Dockerfile
volumes:
- .:/app
- frankenphp_var:/app/var
environment:
APP_ENV: ${APP_ENV:-dev}
APP_SECRET: ${APP_SECRET:-change_me}
DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@database:5432/${POSTGRES_DB:-app}?serverVersion=16&charset=utf8
RABBITMQ_URL: amqp://${RABBITMQ_USER:-guest}:${RABBITMQ_PASSWORD:-guest}@rabbitmq:5672
# Optionnel : tuning Messenger
MESSENGER_CONCURRENCY: "1" # 1 par process, scale via replicas
MESSENGER_PREFETCH: "10" # messages préchargés par worker
depends_on:
database:
condition: service_healthy
rabbitmq:
condition: service_healthy
command: ["/app/bin/worker"] # wrapper vers messenger:consume
restart: unless-stopped
deploy:
replicas: 2 # 2 instances pour paralléliser
resources:
limits:
cpus: "1.0"
memory: 768M
reservations:
cpus: "0.25"
memory: 256M
healthcheck:
test: ["CMD-SHELL", "php -v >/dev/null 2>&1 || exit 1"]
interval: 15s
timeout: 3s
retries: 5
Pourquoi ces choix ?
replicas: 2
: tu parallélises les encodages sans rendre chaque process obèse. En prod, tu scales horizontalement.MESSENGER_PREFETCH
: limite l’overfetch et évite qu’un worker monopolise trop de messages.resources.limits
: garde un couvercle sur CPU/RAM quand FFmpeg s’emballe.restart: unless-stopped
+ healthcheck : résilience minimum, même en local.
Pipeline d’encodage asynchrone : upload instantané, traitement en arrière-plan
Notre POC mise sur une chaîne de traitement asynchrone pour que l’upload soit non bloquant et que l’encodage HLS se fasse hors requête HTTP. Concrètement :
- Upload côté back-office (EasyAdmin)
Le contrôleurVideosCrudController
déclenche l’encodage en dispatchant un message (ProcessVideoMessage
) dès la fin de l’upload. - Routage vers RabbitMQ
Dansconfig/packages/messenger.yaml
, le transportasync
envoie le message dans la queue RabbitMQ prévue pour l’encodage. - Traitement en worker dédié
Le scriptbin/worker
exécutemessenger:consume async
avec des limites maîtrisées (mémoire, durée, nombre de messages).
Le handlerProcessVideoMessageHandler
délègue le travail sale auVideoProcessingService
(encodage multi-résolutions, segmentation HLS, mise à jour BDD).
Résultat :
1) Upload → 2) Dispatch vers la file → 3) Worker → 4) Mise à jour du statut, pendant que la requête d’origine rend la main immédiatement. C’est le vrai asynchrone, pas du “pseudo-async”.
Détails d’implémentation
Le worker dédié
bin/worker
pilote Messenger avec des garde-fous de prod (temps, mémoire, débit) :
set -Eeuo pipefail
exec php bin/console messenger:consume async \
--limit=10 \
--memory-limit=512M \
--time-limit=3600 \
-vv
Tu peux scaler en répliques (Docker deploy.replicas: 2
) pour paralléliser plusieurs encodages.
Routage Messenger (RabbitMQ)
Dans messenger.yaml
, le message vidéo est envoyé sur le transport async
(RabbitMQ) :
framework:
messenger:
transports:
async:
dsn: '%env(RABBITMQ_URL)%'
routing:
App\Message\ProcessVideoMessage: async
Ajoute au besoin une stratégie de retry exponentiel pour encaisser les ratés d’encodage.
Handler et service d’encodage
ProcessVideoMessageHandler
récupère l’ID vidéo, met à jour le statut (PENDING
→PROCESSING
→READY
ouFAILED
), puis appelleVideoProcessingService
.VideoProcessingService
s’occupe de FFmpeg : profils 360p/720p/1080p, segments HLS (.ts
/.m4s
) et master playlist (master.m3u8
). Les fichiers sont écrits danspublic/videos/{id}/...
pour que Varnish puisse les cacher agressivement.
États et UX
Pendant l’encodage, l’admin voit l’état en temps réel dans le back-office (statut en base). Une fois l’encodage terminé, l’URL HLS (/videos/{id}/master.m3u8
) est prête pour le player.
Pourquoi c’est important
- Non-bloquant : l’utilisateur n’attend jamais que FFmpeg termine.
- Scalable : on augmente le nombre de workers quand la file grossit.
- Robuste : isolation des pannes (un échec d’encodage ne flingue pas le site).
- Cache-friendly : les segments HLS statiques sont servis vite via Varnish.

Architecture Docker en action
Une fois docker-compose up
lancé, notre environnement tourne avec l’ensemble des services nécessaires au streaming :
app-1
: l’application Symfony 7 propulsée par FrankenPHP.worker-1
etworker-2
: deux workers asynchrones qui consomment la file RabbitMQ et lancent les encodages vidéo avec FFmpeg.varnish-1
: le reverse proxy cache, qui distribue rapidement les segments HLS.database-1
: PostgreSQL pour stocker les métadonnées vidéos.rabbitmq-1
: le broker de messages, avec interface de management dispo sur le port 15672.mailer-1
: service de mail (Mailpit) pour tester les notifications.
On voit bien que tout est compartimenté : l’encodage est isolé des requêtes web, le cache est découplé du backend, et les workers peuvent être répliqués à volonté. C’est exactement ce qui permet à ce POC d’être scalable et de ressembler déjà à une architecture de production.
Résultat côté utilisateur
Après tout ce travail d’architecture (workers asynchrones, FFmpeg, segmentation HLS, cache Varnish), voici la page finale de lecture vidéo.
Ce qui frappe, c’est la simplicité. Un lecteur épuré, mais qui propose déjà :
- un choix de qualité fluide (480p / 720p / 1080p),
- une vitesse ajustable,
- un affichage du statut de la vidéo (encodage terminé ou en cours),
- des infos techniques (durée, poids, date d’upload),
- la possibilité de télécharger chaque résolution individuellement.
Derrière cette interface se cache en réalité une architecture scalable et robuste. Chaque encodage est traité en arrière-plan par un worker dédié, les vidéos sont segmentées en HLS multi-résolutions, et Varnish se charge d’accélérer la diffusion.

Conclusion
Pour cette première partie, on est allé très loin — peut-être même trop loin pour un simple POC — mais c’était nécessaire pour éviter le piège du banal CRUD vidéo. Grâce à FrankenPHP, on a pu tester rapidement une architecture à la fois moderne et puissante.
Le parcours est fluide : en HTTPS via Caddy, on accède au back-office /admin
, on upload une vidéo, et hop, elle est traitée en asynchrone par des workers dédiés via RabbitMQ. Les métadonnées sont stockées dans PostgreSQL, et côté frontend, /
affiche la liste des vidéos.
Avec Varnish greffé au pipeline, le chargement est instantané et taillé pour absorber la charge. Sur la page /show/{id}
, on profite d’un lecteur complet :
- changement de résolution (480p, 720p, 1080p),
- ajustement de la vitesse,
- téléchargement des différentes versions disponibles.
Bref, ce POC montre que Symfony 7 + FrankenPHP peuvent déjà donner naissance à une solution de streaming adaptatif sérieuse, proche de ce qu’on retrouve chez les géants du secteur.
👉 La suite ? On va ajouter la sécurité, avec gestion d’accès public/privé, tokens temporaires et même du DRM façon Netflix (oui, avec Cloudflare et d’autres outils costauds). Mais ça, ce sera pour la deuxième partie…
J’espère que cet article, dense mais passionnant, vous a plu autant que j’ai pris plaisir à le créer. On en discute en commentaires !
Cet article t’a plu ?
J’essaie le plus possible de rester objectif et de tester toujours plus d’idées sur Symfony 7 et FrankenPHP.
D’ailleurs je travaille en parallèle sur minikube, article bientôt prévu ^^
Laisser un commentaire