Une démo qui circule cette semaine ressemble à un tour de magie. Un téléphone affiche un flux ininterrompu de QR codes clignotants. Un second téléphone pointe sa caméra vers le premier écran. Vingt secondes plus tard, le fichier est arrivé.
Pas de Wi-Fi. Pas de Bluetooth. Aucun appairage, aucun réseau partagé, aucune application à installer. La seule chose qui traverse l'espace entre les deux appareils, c'est de la lumière.
Le projet — Decimen Optical Transfer, sous licence MIT, construit en une nuit avec Claude Code — atteint environ 129 Ko/s de débit utile, contre un plafond théorique proche de 186 Ko/s. Soit près de 30 fois plus rapide que les implémentations séquentielles de 2024, qui plafonnaient autour de 4 Ko/s. Ce n'est pas un gadget : c'est un vrai canal de données, et ce qui le rend viable est une famille de codes correcteurs que la plupart des développeurs web n'ont jamais eu l'occasion de croiser.
Le problème que pose le canal optique
Tous les protocoles réseau que vous avez écrits supposent l'existence d'une voie de retour. TCP retransmet parce que le récepteur peut dire « la trame 47 n'est jamais arrivée ». Les requêtes HTTP Range existent parce que le client peut réclamer un décalage précis.
Un écran qui montre un QR code à une caméra n'a rien de tout cela. C'est de la diffusion pure. L'émetteur ignore si le récepteur regarde, si l'éclairage est mauvais, si la caméra a perdu une trame en faisant la mise au point, ou si la lecture a commencé au milieu du flux.
L'approche naïve consiste à découper le fichier, numéroter les morceaux et boucler la séquence. C'est ce que presque tout le monde construit en premier, et cela échoue lourdement. Les caméras de téléphone perdent des trames en permanence — une trame perdue à 15 images par seconde signifie attendre une boucle complète pour récupérer ce morceau. Perdez-en quelques-uns par boucle, et le transfert ne converge jamais. Le projet txqr avait mesuré ce phénomène dès 2018 : avec des morceaux de près de 1000 octets, la perte de trame était « quasiment garantie » et le décodeur restait bloqué indéfiniment.
Impossible de corriger cela par des tentatives répétées : il n'existe aucun canal sur lequel réessayer.
Les codes fontaine : abandonner les numéros de séquence
Les codes fontaine résolvent précisément ce problème. Leur nom formel est codes correcteurs sans rendement fixe, et l'intuition tient dans la métaphore : une fontaine projette un nombre illimité de gouttes. Peu importe lesquelles vous attrapez — vous tendez un seau jusqu'à ce qu'il soit plein.
Au lieu de transmettre le morceau 1, puis le 2, puis le 3, un encodeur Luby Transform procède ainsi :
- Découper le fichier en K blocs source.
- Pour chaque trame, tirer un degré
daléatoire selon une distribution soigneusement calibrée (la distribution soliton robuste). - Choisir
dblocs source au hasard et les combiner par XOR. - Transmettre ce résultat, accompagné d'un en-tête permettant de retrouver quels blocs y sont entrés.
Chaque trame est un mélange différent du fichier entier. Aucune trame n'est « le troisième morceau ». Le récepteur collecte les trames dans n'importe quel ordre, et dès qu'il en a rassemblé environ K × 1,15 trames distinctes, il peut résoudre le système et reconstituer l'original — 15 pour cent de surcoût en échange d'une indifférence totale à l'identité des trames reçues.
Le décodage est d'une simplicité élégante. Toute trame de degré 1 est directement un bloc source. On l'élimine par XOR de toutes les autres trames qui la contenaient, ce qui réduit leur degré d'une unité, ce qui peut produire de nouvelles trames de degré 1, et ainsi de suite. C'est une réaction en chaîne.
// Encodeur LT — une trame, dérivée entièrement de son numéro de séquence
function encodeFrame(blocks, seq, sessionId) {
const rng = seededRng(sessionId ^ seq); // le récepteur reproduit ceci à l'identique
const degree = robustSoliton(rng, blocks.length);
const picked = sampleDistinct(rng, blocks.length, degree);
const payload = new Uint8Array(blocks[0].length);
for (const i of picked) {
for (let b = 0; b < payload.length; b++) payload[b] ^= blocks[i][b];
}
return concat(header(sessionId, seq, blocks.length, payload.length), payload);
}Remarquez ce que l'en-tête ne contient pas : la liste des blocs source. Il ne porte que le numéro de séquence, et le récepteur redérive le même sous-ensemble aléatoire à partir de la même graine. L'en-tête reste ainsi à environ 20 octets au lieu d'une liste d'index de longueur variable — et sur un canal où chaque octet coûte des modules de QR code, cela compte.
Cela crée aussi un piège subtil que l'auteur de Decimen a rencontré : Math.log ne retourne pas exactement les mêmes valeurs sous V8 et sous JavaScriptCore. Si votre distribution de degrés dépend de calculs en virgule flottante, un récepteur iPhone et un émetteur Android dérivent des sous-ensembles différents, et chaque trame se décode en bouillie. Une arithmétique déterministe entre moteurs n'est pas optionnelle ici.
D'où vient réellement le débit
Le débit utile d'un lien optique est le produit de quatre facteurs, et l'instinct naïf optimise le mauvais.
| Levier | Réflexe naïf | Ce qui marche vraiment |
|---|---|---|
| Cadence | Pousser à 60 images/s | S'aligner sur la cadence réelle de la caméra, pas de l'écran |
| Densité du QR | Saturer en version 40 | Version 25 à 30 — assez dense pour porter la charge, assez clair pour décoder malgré le flou |
| Correction d'erreur | Niveau H « par sécurité » | Niveau L (7 pour cent) — la couche fontaine gère déjà les pertes |
| Taille des morceaux | 1500 octets pour l'efficacité | 550 à 900 octets ; au-delà, la densité devient illisible |
La troisième ligne est la plus contre-intuitive. Les QR codes embarquent leur propre correction Reed-Solomon, et passer au niveau H consacre 30 pour cent de chaque trame à de la redondance. Or vous disposez déjà d'une couche tolérante aux pertes au-dessus. Une trame illisible n'est qu'une trame que la fontaine n'a pas livrée, et la fontaine s'en moque. Renforcer la redondance au niveau du QR, c'est la payer deux fois.
La cadence est le terrain des particularités de plateforme. Sous iOS, demander { ideal: 60 } à getUserMedia vous donne silencieusement 30 images par seconde ; il faut { exact: 60 } en largeur 1280px pour obtenir réellement 60. Et requestVideoFrameCallback est la bonne primitive de capture — une boucle setInterval échantillonnera deux fois la même image tout en en manquant d'autres.
// Récepteur — décoder chaque vraie image caméra, pas chaque tick de minuteur
function pump(video, canvas, ctx, onSymbol) {
video.requestVideoFrameCallback(() => {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const result = zxing.readBarcode(
ctx.getImageData(0, 0, canvas.width, canvas.height),
{ formats: ["QRCode"], tryHarder: false } // tryHarder coûte plus qu'il ne récupère
);
if (result?.bytes) onSymbol(result.bytes);
pump(video, canvas, ctx, onSymbol); // réarmement
});
}Deux remarques pratiques. tryHarder semble gratuit mais ne l'est pas : sur un flux où la prochaine image exploitable arrive dans 16 ms, consacrer 40 ms à sauver une image marginale est une perte nette. Et chaque requestVideoFrameCallback doit être réarmé exactement une fois ; si le flux caméra est détruit puis recréé sans annuler la chaîne, vous vous retrouvez avec des callbacks fantômes qui décodent dans un canvas mort.
Safari ajoute une difficulté supplémentaire : BarcodeDetector n'est toujours pas implémenté dans WebKit, ce qui rend une compilation WebAssembly de zxing-cpp incontournable. Si vous avez suivi la sortie de WebAssembly hors du navigateur, voici l'image inverse : Wasm comblant un manque à l'intérieur de celui-ci.
Le paysage actuel
Cette idée a été réinventée plusieurs fois, ce qui est généralement bon signe.
| Projet | Codec | Débit annoncé | Notes |
|---|---|---|---|
| Decimen Optical Transfer | LT, soliton robuste | ~129 Ko/s | Navigateur, MIT, juillet 2026 |
| RaptorQR | RaptorQ (RFC 6330) via Wasm | 180 à 254 Ko/s (labo) | Lecture QR parallèle, PWA, émetteur CLI |
| txqr | Codes fontaine, Go | ~9 Ko/s en pointe | L'original de 2018 ; Gomobile et GopherJS |
| libcimbar | Fontaine plus zstd | Élevé, fichiers jusqu'à 33 Mo | Abandonne le QR pour un format plus dense |
Les chiffres de RaptorQR proviennent de la lecture de quatre QR codes côte à côte à 30 images/s sur un iPhone 16 en éclairage contrôlé : réels, mais il s'agit d'un plafond, pas d'une promesse. RaptorQ est aussi le codec le plus sérieux — un code systématique normalisé, aux caractéristiques de surcoût bien meilleures que le LT simple, au prix d'une véritable implémentation plutôt que cinquante lignes de XOR.
libcimbar franchit le pas honnête et abandonne complètement le QR. Une fois admis que l'on construit un modem optique, la spécification QR — pensée pour être lue sur une étiquette imprimée sous un angle quelconque — devient du poids mort. Des grilles denses en couleur transportent bien davantage par image.
Les usages réellement pertinents
Il est facile de balayer tout cela d'un revers de main puisque AirDrop existe. Les usages sont réels, mais spécifiques.
Environnements isolés. Signer une transaction sur une machine qui n'a jamais touché un réseau, puis en extraire le blob signé par la caméra, est un schéma établi dans les portefeuilles matériels. Les QR animés à codes fontaine font passer le plafond de charge utile d'un code statique unique à des fichiers complets.
Zones de connectivité faible ou coupures. Dans une grande partie du MENA et de l'Afrique, la donnée mobile est facturée, coûteuse ou indisponible par intermittence. Un protocole qui ne demande aucune infrastructure — pas même un point d'accès local — se dégrade avec élégance là où la synchronisation cloud s'effondre. C'est l'instinct qui sous-tend l'architecture local-first : considérer le réseau comme un confort, pas comme un prérequis.
Transferts sous contrainte de politique interne. Beaucoup d'environnements d'entreprise ou d'administration interdisent les clés USB et bloquent les services de partage, mais personne n'a rédigé de politique contre le fait de pointer un téléphone vers un écran. Que ce soit une fonctionnalité ou une faille dépend entièrement du côté où vous vous trouvez.
Ce dernier point mérite l'avertissement que l'auteur de Decimen formule lui-même : l'isolement dont on parle ici est optique, pas adversarial. Une caméra pointée vers un écran est un chemin de données. Quiconque voit l'écran — y compris une caméra de surveillance, une fenêtre ou un reflet — reçoit exactement la même diffusion. Il n'y a ni appairage, ni authentification. Si la charge utile compte, chiffrez-la avant qu'elle ne devienne des images : le transport ne vous offre rien. La discipline applicable à tout canal non fiable s'applique ici, et pour la dimension politique, nos notes sur la souveraineté numérique couvrent le sujet.
Pour essayer par vous-même
La pile complète tient en trois morceaux : un encodeur LT ou RaptorQ, un moteur de rendu QR rapide, et un lecteur de code-barres en Wasm. RaptorQR est le point de départ le plus complet — il livre un émetteur navigateur, un récepteur PWA et une CLI. Decimen est le plus lisible si vous voulez comprendre le codec plutôt que l'utiliser.
Partez des paramètres connus pour fonctionner — QR version 25, correction niveau L, morceaux de 700 octets, capture pilotée par requestVideoFrameCallback — puis affinez. Mesurez les symboles décodés par seconde, pas les images affichées par seconde. L'écart entre ces deux nombres constitue l'intégralité de votre problème d'ingénierie.
Ce qu'il faut retenir
L'élément intéressant ici n'est pas le débit. C'est qu'une contrainte que tout le monde juge rédhibitoire — pas de voie de retour, support avec pertes, aucune poignée de main — trouve une réponse propre et vieille de quarante ans, déjà posée dans la théorie des codes. Les codes fontaine ont été conçus exactement pour cette forme de problème et restent sous-employés parce que la plupart des développeurs ne rencontrent jamais de canal sans retransmission.
Les écrans et les caméras sont les deux dispositifs d'entrée-sortie les plus universels de la planète. Chaque téléphone possède les deux. Traiter ce couple comme une couche de transport est une très bonne idée — et il aura fallu un code sans rendement fixe pour la rendre praticable.
Sources : Analyse de Decimen Optical Transfer · RaptorQR · txqr · Transfert de données par QR animé