Décoder de la vidéo en parallèle, dans un onglet
Video2Any a une contrainte qui décide de tout le reste : votre vidéo ne quitte pas votre machine. Pas d’envoi, donc pas de serveur qui travaille, donc le décodage, la comparaison et le rendu se passent tous dans un onglet qui doit en plus rester réactif.
Voici ce que cela coûte réellement, et sur quoi le reste du site est construit.
Le démultiplexeur, c’est à vous de l’écrire
WebCodecs vous donne un VideoDecoder qui transforme des morceaux encodés en images. Il ne vous donne pas les morceaux. Le navigateur possède un parseur MP4 — c’est ainsi que <video> fonctionne — et aucune API ne vous le confie : vous lisez donc le conteneur vous-même. Parcourir les boîtes, trouver moov, lire la table des échantillons, et passer au décodeur un EncodedVideoChunk à la fois.
Cela a un effet secondaire agréable. Puisque vous lisez le conteneur, vous pouvez dire pourquoi un fichier ne s’ouvrira pas avant de perdre cinq minutes à échouer : ProRes est un format de montage qu’aucun navigateur ne décode, et on l’apprend en lisant son moov, qui chez ProRes se trouve souvent à la fin du fichier plutôt qu’au début. C’est pour cela que notre sonde examine les deux extrémités.
Deux passes, à cause de la mémoire
L’approche évidente serait de décoder toutes les images et de les comparer. Un enregistrement de 92 minutes à 25 i/s, cela fait 138 000 images ; en 1080p, un onglet ne tient pas cela.
Il y a donc deux passes. Le balayage ne décode que les points d’échantillonnage et garde chacun en 160×90 : assez petit pour qu’une vidéo entière pèse quelques dizaines de mégaoctets de pixels, assez grand pour distinguer une diapositive d’une autre. Ce n’est qu’une fois le détecteur décidé que la passe de capture revient chercher les images en pleine résolution, et seulement celles-là. Un plafond de 900 échantillons borne la première passe, avec une conséquence curieuse : une vidéo de 29 minutes et une de 92 coûtent à peu près pareil, parce que le coût se compte par échantillon et non par minute.
Un worker par morceau, chacun avec son décodeur
La vidéo est découpée dans le temps et chaque morceau va à un worker avec son propre VideoDecoder. Le pool vaut min(4, cœurs / 2) : la moitié des cœurs, parce que l’onglet a encore une interface à faire tourner, et plafonné à quatre pour la raison exposée à la section suivante.
Chaque morceau commence à balayer un peu avant sa propre plage, pour que le détecteur ait de quoi juger sa première image, puis écarte ce qui tombe hors de sa plage. Les diapositives sortent au fil des captures, pas à la fin.
Ce que coûte le découpage
Deux choses, et c’est là que ça devient intéressant. Chaque morceau calibre son seuil sur sa propre tranche : la même vidéo découpée en quatre et en huit donne des présentations différentes, autrement dit le résultat dépend du nombre de cœurs de la machine. Et chaque morceau ne supprimait les doublons qu’en son sein, si bien qu’un plan sur lequel une réunion revient sans cesse survivait une fois par morceau : trois diapositives en double sur neuf pour un enregistrement de 29 minutes.
Les doublons sont réglés : la vérification est passée sur le thread principal, au fil de l’arrivée des diapositives. Le calibrage non. Nous avons écrit la version globale, et elle a dégradé le résultat : sur cette même réunion, où ffmpeg compte sept coupes réelles, le calibrage par morceau en donne six et le calibrage global quatre. Le pool reste donc à quatre, parce que davantage de morceaux va environ 25 % plus vite et change encore la réponse.
Rien ne peut être montré avant d’avoir lu la vidéo
Le seuil vient de la distribution des différences entre images sur toute la vidéo : la première diapositive ne peut donc pas être choisie avant que la dernière image ait été échantillonnée. Sur un enregistrement de 92 minutes, le balayage prend 52 secondes et la première diapositive apparaît à la 50ᵉ.
Ce n’est pas un problème de performance et aucun degré de parallélisme ne le règle. C’est un problème d’information, résolu en écrivant « Lecture de la vidéo · 45:46 sur 92:08 » là où il n’y avait qu’un pourcentage.
Le reste de la pile
Cloudflare Workers avec Hono pour l’API, D1 pour les comptes et les métadonnées, R2 pour les rares choses qui sont stockées. React 19 avec TanStack Router, Tailwind v4, better-auth. Huit langues, chacune avec son préfixe d’URL et son basepath de routeur.
Le site est une SPA prérendue plutôt que rendue côté serveur : chaque page publique est un instantané HTML statique produit au déploiement, d’où des pages marketing rapides et cacheables, et un convertisseur qui fonctionne sans serveur du tout. Nous ne dérogeons qu’à un seul endroit. Les pages /app sont des coquilles statiques : l’éditeur démarrait donc sans rien savoir de votre compte et devait demander, et commencer à travailler avant la réponse est précisément ainsi qu’un abonné payant s’est retrouvé avec la limite de trente minutes de l’offre gratuite. Le worker a déjà la session au moment de servir ces pages ; il y écrit donc la formule, et le premier rendu la connaît. Ces réponses sont privées et jamais mises en cache ; toutes les autres pages conservent leur cache public.
Pourquoi s’embêter
Un serveur ferait disparaître l’essentiel de tout cela. Il signifierait aussi que chaque cours, chaque réunion interne et chaque enregistrement médical converti passe par nos machines — et tout l’intérêt du produit est qu’ils n’y passent pas.
La contrainte est la fonctionnalité. Ce qui précède en est le prix.