Remplacer le streaming H.264 par des captures d’écran JPEG a finalement mieux fonctionné
(blog.helix.ml)- Helix est une plateforme d’IA qui montre à l’utilisateur l’écran sur lequel tournent des agents de codage autonomes dans le cloud, et la transmission distante fiable de l’écran y est essentielle
- Quand le streaming basé sur WebRTC a échoué à cause du blocage de l’UDP et des contraintes de pare-feu sur les réseaux d’entreprise, l’équipe a construit un pipeline H.264 basé sur WebSocket, mais dans des environnements Wi-Fi instables, la latence est devenue très importante
- Au lieu d’une architecture complexe d’encodage et de décodage, l’équipe a découvert qu’une approche consistant simplement à envoyer périodiquement des captures d’écran JPEG via HTTP était bien plus stable et efficace
- Cette méthode consomme moins de bande passante, n’a pas besoin de récupération de trames corrompues et ajuste automatiquement la qualité d’image et la fréquence d’images selon la qualité du réseau
- Au final, Helix a adopté une architecture hybride : H.264 sur une bonne connexion et bascule vers le polling JPEG sur une mauvaise connexion, aboutissant à un système de streaming distant simple mais pragmatique
Le problème de streaming et les contraintes de Helix
- Helix est une plateforme qui doit partager en temps réel l’écran d’un agent de codage IA exécuté dans un sandbox cloud
- L’utilisateur regarde l’IA écrire du code comme s’il s’agissait d’un bureau à distance
- Au départ, l’équipe utilisait WebRTC, mais la connexion échouait à cause du blocage de l’UDP sur les réseaux d’entreprise
- Serveurs TURN, STUN/ICE, ports personnalisés : tout était bloqué par les politiques de pare-feu
- L’équipe a donc implémenté elle-même un pipeline de streaming H.264 basé sur WebSocket n’utilisant que HTTPS (port 443)
- Encodage matériel avec GStreamer + VA-API, décodage navigateur avec WebCodecs
- 60fps, 40Mbps et moins de 100ms de latence atteints
Latence réseau et dégradation des performances
- Dans des environnements réseau instables comme les cafés, la vidéo se figeait ou prenait plusieurs dizaines de secondes de retard
- Avec WebSocket basé sur TCP, en cas de perte de paquets, les trames étaient retardées séquentiellement et le temps réel s’effondrait
- Réduire le bitrate ne résolvait pas la latence et ne faisait que dégrader la qualité d’image
- L’équipe a aussi tenté d’envoyer uniquement des keyframes, mais cela a échoué parce que le protocole Moonlight exige des P-frames
La découverte de l’approche par captures JPEG
- Pendant le débogage, l’appel à l’endpoint
/screenshot?format=jpeg&quality=70a chargé instantanément une image nette- Un JPEG unique de 150KB s’affichait sans latence
- Le simple fait de répéter des requêtes HTTP pour rafraîchir la capture permettait une mise à jour fluide de l’écran autour de 5fps
- L’équipe a donc abandonné le pipeline vidéo complexe au profit d’une méthode de requêtes JPEG périodiques (boucle
fetch())
Les avantages de l’approche JPEG
- Principaux points de comparaison face à H.264
- Bande passante : H.264 reste fixe à 40Mbps, JPEG varie entre 100 et 500Kbps
- Gestion d’état : H.264 dépend de l’état, JPEG fournit des trames totalement indépendantes
- Récupération : H.264 impose d’attendre une keyframe, JPEG récupère immédiatement à la trame suivante
- Complexité : H.264 a demandé plusieurs mois de développement, JPEG s’implémente avec quelques lignes de boucle
fetch()
- Plus la qualité du réseau est mauvaise, plus l’approche JPEG, pourtant simple, se montre stable et efficace
Architecture hybride de bascule
- Helix bascule automatiquement entre les deux méthodes selon le RTT (temps d’aller-retour)
- RTT < 150ms → streaming H.264
- RTT > 150ms → polling JPEG
- Une fois la connexion rétablie, l’utilisateur clique pour rebascule
- Les événements d’entrée (clavier, souris) continuent d’être envoyés via WebSocket, ce qui préserve l’interactivité
- Le serveur arrête l’envoi vidéo et passe en mode capture avec le message
{"set_video_enabled": false}
Problème d’oscillation et solution
- Une fois la transmission vidéo arrêtée, le trafic WebSocket diminuait, la latence baissait et cela déclenchait une boucle infinie de retour automatique en mode vidéo
- Solution : après l’entrée en mode capture, rester verrouillé jusqu’au clic de l’utilisateur
- L’interface affiche le message « Vidéo mise en pause pour économiser la bande passante »
Problème de support JPEG et processus de build
- L’outil de capture Wayland grim est livré dans les paquets Ubuntu par défaut avec le support JPEG désactivé
- La commande
grim -t jpegrenvoie l’erreur « jpeg support disabled »
- La commande
- Pour corriger cela, l’équipe a compilé directement grim depuis les sources dans le Dockerfile en incluant
libjpeg-turbo8-dev
Architecture finale
- Bonne connexion : H.264 à 60fps, accélération matérielle
- Mauvaise connexion : polling JPEG à 2~10fps, fiabilité totale
- La qualité des captures est ajustée automatiquement selon le temps de transfert
- Au-delà de 500ms : qualité -10 %, en dessous de 300ms : +5 %, avec un minimum maintenu à 2fps
Enseignements clés
- Une solution simple vaut mieux qu’un système complexe — 2 heures de bricolage JPEG ont été plus utiles que 3 mois de développement H.264
- La dégradation élégante (graceful degradation) est au cœur de l’expérience utilisateur
- WebSocket est optimal pour transmettre les entrées, mais pas indispensable pour la vidéo
- Les paquets Ubuntu peuvent manquer de fonctionnalités — il faut parfois compiler soi-même
- Mesurer avant d’optimiser est indispensable — un streaming complexe n’est pas forcément la seule solution
Publication en open source
- Helix est proposé en open source, avec notamment ces éléments clés
api/cmd/screenshot-server/main.go— serveur de captures d’écranMoonlightStreamViewer.tsx— logique client adaptativewebsocket-stream.ts— contrôle de la bascule vidéo
- Helix est développé avec l’objectif de fournir une infrastructure IA capable de fonctionner aussi en conditions réelles
1 commentaires
Avis sur Hacker News
Quand le réseau est mauvais, si le JPEG s’en sort mieux, ce n’est pas à cause de l’UDP mais de la façon dont TCP est implémenté
Le JPEG ne résout ni les problèmes de mise en tampon ni ceux de contrôle de congestion. Il est plus probable que l’implémentation réduise au minimum l’envoi des frames
h.264 a une meilleure efficacité de codage que le JPEG. À taille égale, une frame IDR h.264 peut offrir une meilleure qualité
Le vrai problème est l’absence d’estimation de la bande passante. Même dans un environnement TCP, on peut ajuster le bitrate via une sonde initiale de bande passante et la détection de la latence de transmission
Si possible, mieux vaut utiliser WebRTC, et WebSocket pour contourner les pare-feu
Même en laissant de côté les problèmes de forme ou le style LLM du billet, le fond contient beaucoup d’erreurs
10 Mbps devraient suffire pour un écran statique. Le problème vient soit de paramètres d’encodage mal réglés, soit d’un encodeur de mauvaise qualité
L’approche consistant à « n’envoyer que des keyframes » est inefficace ; il suffit plutôt de définir un intervalle de keyframes court
Au fond, le problème est une architecture qui pousse tout le flux dans une unique connexion TCP. Des solutions comme DASH existent déjà pour ce type de cas
Il serait utile de s’inspirer de ce que VNC fait depuis 1998
L’idée est de garder un modèle de client en pull, en découpant le framebuffer en tuiles et en n’envoyant que les parties modifiées
Sur un écran de code statique, cela peut réduire fortement la bande passante. Ajouter une détection du défilement le rendrait encore plus efficace
J’ai déjà travaillé sur l’encodage vidéo, et 40 Mbps, c’est une qualité de niveau Blu-ray
C’est excessif pour du simple streaming de texte. Après en avoir discuté avec Claude, la conclusion était qu’environ 30 FPS, un GOP de 2 secondes et un débit moyen d’environ 1 Mbps suffiraient
Même dans le pire des cas, 1,2 Mbps suffiraient à maintenir une qualité stable
Le problème central de ce billet, c’est d’avoir réglé bien trop haut la bande passante minimale pour h.264
H.264 est bien plus efficace que le JPEG. Il aurait fallu commencer à 1 Mbps puis ajuster
N’utiliser que des keyframes est au contraire inefficace
À ma place, j’aurais adopté une approche complètement différente
10 Mbps, c’est excessif, et les vidéos de code sur YouTube tournent autour de 0,6 Mbps même en 1080p. C’est largement assez net
Je pense qu’il vaudrait mieux descendre à 1 fps ou ajuster l’intervalle des keyframes
Diffuser de la vidéo en temps réel dans un navigateur est vraiment pénible
Si les captures d’écran JPEG fonctionnent bien, autant les garder
Des stacks comme gstreamer ou Moonlight sont un enfer à déboguer si on ne comprend pas la backpressure et la propagation des erreurs
Une combinaison NVIDIA Video Codec SDK + WebSocket + MediaSource Extensions est une alternative réaliste
Mais si le billet a été généré par LLM, l’auteur n’a probablement pas envie de comprendre ce fonctionnement interne
J’ai autrefois utilisé un programme qui prenait une capture d’écran toutes les 5 secondes, et le disque dur s’est vite rempli
En réalisant que la plupart des images étaient identiques, j’ai réfléchi à un algorithme ne stockant que les parties modifiées, puis
je me suis aperçu que j’étais tout simplement en train de réinventer la compression vidéo
Une ligne de ffmpeg a suffi à régler le problème, avec 98 % d’espace économisé
Diffuser à 40 Mbps une vidéo d’un LLM en train de taper représente une bande passante anormalement excessive
Sur HN, la seule façon d’obtenir de bonnes réponses est de publier quelque chose de faux
À mon avis, c’est justement un excellent exemple d’équilibre : un texte faux, mais intéressant, parfait pour déclencher une discussion