Volver al inicio

WebRTC DataChannel: archivos sin errores de buffer

El artículo analiza problemas de producción de transferencia de archivos vía WebRTC DataChannel: gestión de SCTP buffer, adaptación de chunks a relay, backpressure en receptor. Se proporcionan ejemplos de código de overlapped I/O, timeouts dinámicos y mecanismos ACK para confiabilidad.

Archivos en WebRTC: cómo evitar truncamiento en relay
Advertisement 728x90

# Transferencia de archivos con WebRTC DataChannel: Gestión de buffers y fiabilidad en producción

En WebRTC DataChannel, el método dc.send() encola los datos en el buffer SCTP del emisor, pero no garantiza la entrega al receptor. En conexiones de retransmisión lentas, el buffer se llena con megabytes de datos, la barra de progreso llega al 100% y el archivo queda truncado. Una configuración típica de intercambio P2P incluye un servidor de señalización para coordinar, conexiones directas peer-to-peer como vía principal y relés TURN como alternativa. El servidor no almacena contenido, solo gestiona los estados de transferencia: desde la creación del compartir hasta las colas de receptores.

Desafíos clave: contrapresión del buffer, pérdida del objeto File al recargar la página, desajuste de preparación entre pares y reconexiones. Este análisis se basa en una implementación en producción con límites de relés y soporte TURN personalizado.

Arquitectura sin almacenamiento de archivos en servidor

El servidor coordina vía REST API y WebSocket: crea comparticiones, firma conexiones con ECDSA y rastrea estados. El ciclo de vida de una compartición —active → matched → transferring → active— permite al emisor reutilizarla para múltiples receptores. Las colas distribuyen automáticamente a nuevos participantes.

Google AdInline article slot

Estados terminales: expired, cancelled. Los estados Failed se recuperan con reactivate. El contenido se cifra de extremo a extremo con DTLS, y los relés hacen proxy del tráfico sin acceder a los datos.

Problema 1: Fallback a TURN y tamaño óptimo de chunks

El P2P no siempre es posible por NAT simétrico, firewalls o redes móviles. Los tipos de conexión se determinan de los candidatos ICE: p2p, server_relay (coturn con cuotas), custom_relay (TURN personalizado sin límites).

El tamaño de chunk se adapta para minimizar retrasos de retransmisión:

Google AdInline article slot
function getOptimalChunkSize(classification: string): number {
  switch (classification) {
    case 'p2p':          return 1024 * 1024;  // 1 MB
    case 'custom_relay': return 512 * 1024;   // 512 KB
    case 'server_relay': return 64 * 1024;    // 64 KB
    default:             return 512 * 1024;
  }
}

Chunks más pequeños en server_relay aceleran la recuperación: un paquete perdido se retransmite completo, con retrasos que escalan por tamaño. Verificar TURN previamente con una RTCPeerConnection dedicada y iceTransportPolicy: 'relay' toma unos 5 segundos.

Problema 2: Contrapresión y E/S solapada en el bucle de envío

Un bucle ingenuo envía chunks secuencialmente, inactivo en file.slice().arrayBuffer(). La implementación en producción usa doble bufferización y marcas de agua alta/baja:

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

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 solapada: Lee el siguiente chunk en paralelo al envío del actual.
  • Timeout dinámico: estimatedDrainMs = (bufferedAmount / speed) * 1000, limitado a 5–60 segundos.
  • Progreso honesto: offset - dc.bufferedAmount, nunca supera el 99% hasta vaciar completamente.

La finalización requiere vaciado del buffer + transfer_ack del receptor antes de transfer_done.

Google AdInline article slot

Problema 3: Contrapresión en receptor y disco

El receptor copia ArrayBuffer para evitar reutilización del buffer DataChannel:

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

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

writeChain es una cadena de promesas secuencial. Discos lentos (como pendrives USB) generan contrapresión hacia arriba, limitando la ingesta de datos SCTP.

Desafíos adicionales en producción

  • Pérdida de archivos: Los objetos File no se serializan; guárdalos como ArrayBuffer o Blob en IndexedDB.
  • Reconexiones: La señalización puede caer antes que DataChannel; trata peer_left como normal.
  • Rarezas de Safari: Maneja modos ordered/unordered y buffers por separado.

Lecciones clave:

  • dc.send() escribe al buffer SCTP, no a la red; monitorea siempre bufferedAmount.
  • Adapta tamaños de chunk: 1MB para P2P, 64KB para server_relay para optimizar retransmisiones.
  • Espera transfer_ack tras vaciado antes de señalar transfer_done.
  • Usa E/S solapada y timeouts dinámicos para máximo rendimiento.
  • Verifica cuotas de relés bilateralmente; ejecuta chequeos de salud TURN al inicio.

— Editorial Team

Advertisement 728x90

Leer después