Retour à l'accueil

WebRTC DataChannel : fichiers sans erreurs de tampon

L'article analyse les problèmes de production du transfert de fichiers via WebRTC DataChannel : gestion du tampon SCTP, adaptation des chunks au relais, backpressure sur le récepteur. Exemples de code d'overlapped I/O, timeouts dynamiques et mécanismes ACK pour la fiabilité sont fournis.

Fichiers dans WebRTC : comment éviter la troncature sur relais
Advertisement 728x90

# Transfert de fichiers WebRTC DataChannel : Gestion des tampons et fiabilité en production

Dans WebRTC DataChannel, la méthode dc.send() met les données en file d'attente dans le tampon SCTP de l'émetteur, sans garantir leur livraison au récepteur. Sur des connexions relais lentes, le tampon se remplit de mégaoctets de données, la barre de progression atteint 100 %, et le fichier se retrouve tronqué. Une configuration typique de partage P2P inclut un serveur de signalisation pour la coordination, des connexions directes pair-à-pair comme voie principale, et des relais TURN en secours. Le serveur ne stocke pas le contenu mais gère les états de transfert : de la création du partage aux files d'attente des destinataires.

Défis principaux : contre-pression du tampon, perte de l'objet File au rechargement de page, désynchronisation de la préparation entre pairs, et reconnexions. Cette analyse s'appuie sur une implémentation en production avec limites de relais et support TURN personnalisé.

Architecture sans stockage de fichiers côté serveur

Le serveur coordonne via API REST et WebSocket : il crée les partages, signe les connexions avec ECDSA, et suit les états. Le cycle de vie d'un partage — actif → apparié → transfert → actif — permet à l'émetteur de réutiliser pour plusieurs destinataires. Les files d'attente distribuent automatiquement aux nouveaux participants.

Google AdInline article slot

États terminaux : expiré, annulé. Les états Échoué se récupèrent via réactiver. Le contenu est chiffré de bout en bout avec DTLS, et les relais proxyfient le trafic sans accès aux données.

Problème 1 : Recours TURN et taille optimale des chunks

Le P2P n'est pas toujours possible à cause des NAT symétriques, pare-feu ou réseaux mobiles. Les types de connexion sont déterminés à partir des candidats ICE : p2p, relais_serveur (coturn avec quotas), relais_personnalisé (TURN personnalisé sans limites).

La taille des chunks s'adapte pour minimiser les délais de retransmission :

Google AdInline article slot
function getOptimalChunkSize(classification: string): number {
  switch (classification) {
    case 'p2p':          return 1024 * 1024;  // 1 Mo
    case 'relais_personnalisé': return 512 * 1024;   // 512 Ko
    case 'relais_serveur': return 64 * 1024;    // 64 Ko
    default:             return 512 * 1024;
  }
}

Des chunks plus petits sur relais_serveur accélèrent la récupération : un paquet perdu se retransmet entièrement, avec des délais proportionnels à la taille. La pré-vérification TURN via une RTCPeerConnection dédiée avec iceTransportPolicy: 'relay' prend environ 5 secondes.

Problème 2 : Contre-pression et E/S chevauchées dans la boucle d'envoi

Une boucle naïve envoie les chunks séquentiellement, en attendant file.slice().arrayBuffer(). L'implémentation en production utilise un double tamponnage et des seuils haut/bas :

const BUFFER_THRESHOLD = 8 * 1024 * 1024; // 8 Mo
const BUFFER_LOW       = 6 * 1024 * 1024; // 6 Mo

dc.bufferedAmountLowThreshold = BUFFER_LOW;
let nextBuffer = await file.slice(0, chunkSize).arrayBuffer();

while (offset < file.size) {
  const buffer = nextBuffer;
  const readPromise = offset + buffer.byteLength < file.size
    ? file.slice(offset + buffer.byteLength, nextEnd).arrayBuffer()
    : null;

  if (dc.bufferedAmount >= BUFFER_THRESHOLD) {
    await waitForBufferDrain(dc, computeDrainTimeout(dc.bufferedAmount, speed));
  }

  dc.send(buffer);
  offset += buffer.byteLength;

  const effectiveSent = Math.max(0, offset - dc.bufferedAmount);
  const pct = Math.min((effectiveSent / file.size) * 100, isLast ? 100 : 99);

  nextBuffer = readPromise ? await readPromise : null;
}
  • E/S chevauchées : Lit le chunk suivant en parallèle de l'envoi du courant.
  • Timeout dynamique : estimatedDrainMs = (bufferedAmount / speed) * 1000, plafonné à 5–60 secondes.
  • Progression honnête : offset - dc.bufferedAmount, jamais au-delà de 99 % tant que non vidé.

La finalisation exige vidange du tampon + transfer_ack du récepteur avant transfer_done.

Google AdInline article slot

Problème 3 : Contre-pression côté récepteur et disque

Le récepteur copie ArrayBuffer pour éviter la réutilisation du tampon DataChannel :

const chunk = event.data.slice(0);  // Copie
writeChain = writeChain.then(() => writer.write(chunk));

if (bytesReceived === fileInfo.size) {
  dc.send(JSON.stringify({ type: 'transfer_ack' }));
}

writeChain est une chaîne de promesses séquentielle. Les disques lents (clés USB) génèrent une contre-pression remontant la pile, limitant l'entrée SCTP.

Défis supplémentaires en production

  • Perte de fichiers : Les objets File ne se sérialisent pas ; stockez en ArrayBuffer ou Blob dans IndexedDB.
  • Reconnexions : La signalisation peut lâcher avant DataChannel ; traitez peer_left comme normal.
  • Spécificités Safari : Gérez modes ordonnés/désordonnés et tampons séparément.

Points clés :

  • dc.send() écrit dans le tampon SCTP, pas sur le réseau ; surveillez toujours bufferedAmount.
  • Adaptez les tailles de chunks : 1 Mo pour P2P, 64 Ko pour relais_serveur afin d'optimiser les retransmissions.
  • Attendez transfer_ack après vidange avant de signaler transfer_done.
  • Utilisez E/S chevauchées et timeouts dynamiques pour des performances optimales.
  • Vérifiez les quotas relais bilatéralement ; lancez des tests TURN en amont.

— Editorial Team

Advertisement 728x90

Lire ensuite