De una pizarra en una clase de inglés a un experimento con deltas, compresión, audio, video y conexiones intermitentes.
Estaba en un curso de inglés por videollamada, desde la empresa donde trabajaba, con la conexión de todos los días en Cuba: lenta, alrededor de 120 kbps de trabajo, y con caídas constantes.
El audio normalmente llegaba. El video, no. La profesora podía hablar conmigo, pero enseñar a escribir una palabra o una frase en vivo era una batalla imposible. Mostrar en pantalla cómo se escribe algo, durante la clase, necesitaba demasiados datos para las condiciones existentes.
No pensé "voy a arreglar internet en Cuba", sino algo mucho más pequeño: ¿y si un dibujo pudiera cruzar esa conexión sin romperse?
Un video de compresión de datos lo puso en marcha
En YouTube apareció el video de ThePrimeagen (@ThePrimeagen en YouTube) explicando el run-length encoding (RLE): representar una secuencia de bytes repetidos como "veinte de este byte" en lugar de veinte bytes. Una idea preciosa por lo obvia que es.
Yo no estaba buscando compresión. Lo vi y pensé: espera, ¿qué pasa si aplico esta idea a una pizarra? 💡
Una pizarra en blanco y negro es, en el fondo, una cuadrícula de bits. La versión más simple de la idea es casi un chiste:
// "aaaaaaabbbb" -> [7, 'a', 4, 'b']
function rle(seq) {
const out = [];
let i = 0;
while (i < seq.length) {
let j = i;
while (j < seq.length && seq[j] === seq[i]) j++;
out.push(j - i, seq[i]);
i = j;
}
return out;
}
Canvas → arreglo de bytes → RLE → enviar. Mi primer pensamiento fue exactamente ese. Ya era una mejora enorme frente a enviar todo el canvas como información serializada: usar el arreglo de bytes (ArrayBuffer) como protocolo, en vez de un JSON enorme.
Luego descubrí que comprimía lo que no debía
Durante el desarrollo apareció la pregunta que no esperaba: ¿por qué estoy enviando el canvas completo si solo cambió una pequeña parte?
Dos personas dibujan. Una borra un trazo en la esquina. El 99% del dibujo no cambió. Comprimir el canvas entero es optimizar el envío de información que la otra persona ya tiene. Lo que de verdad quería enviar era el cambio:
{ type: 'add', shape } // nace un trazo
{ type: 'update', id, next } // un punto se mueve
{ type: 'remove', id } // un trazo se borra
En mi trabajo con conexiones malas hay una frase que vuelve siempre: la mejor optimización no es hacer una operación más barata, es no hacer la operación. Enviar el canvas completo era una operación que no hacía falta. RLE comprime bien los bytes repetidos; comprimir ochocientos bytes que podían ser tres era un problema mal planteado.
La pizarra se convirtió en un instrumento
Ahí la pizarra dejó de ser "una app" y se convirtió en el primer instrumento de una pregunta más grande: ¿cuánta información necesita realmente una experiencia compartida? 🎯
- Cada trazo es un cambio con su propio estado, y solo eso es lo que se envía con un protocolo binario propio: el dibujo se representa como cambios (add, update, remove), no como estados.
- Los cambios se codifican de forma compacta y, cuando conviene, se comprimen. Probé con gzip sobre mi protocolo binario.
- Hay un lobby para entrar y salas donde convivir.
La demo está en https://whiteboard-five-gamma.vercel.app. Conectas, dibujas, el trazo llega. La parte cómoda terminó ahí: la pizarra funcionaba justo en los casos que ya sabía resolver.
Añadí audio y conocí la parte invisible de la compresión
En ese momento la app era solo una pizarra que funcionaba muy bien como acompañamiento de una videollamada de solo audio en otra plataforma: permitía mantener el consumo de datos visuales al mínimo.
En una pizarra dibujar importa, pero una conversación se vuelve humana con la voz. Comencé a expandir la app implementando audio compartido, pero teniendo en cuenta el presupuesto de ancho de banda pensé que, antes de añadir el audio, debía agregar un sistema para registrar el consumo y el envío de datos y poder visualizarlos, para saber si realmente me mantenía dentro del presupuesto. 📊
Añadí audio compartido. Funciona. Revisando los datos que registra el sistema, en una conversación entre pocos usuarios el canal de audio ronda los 40 kbps, dejando alrededor de 80 kbps para la pizarra:
120 kbps presupuesto total
-40 kbps audio compartido
80 kbps pizarra (y todo lo que venga después)
Ahí encontré un contexto que vale la pena. Existe toda una industria de códecs pensando exactamente en este problema. Opus, por ejemplo, nació para hablar por VoIP con poco ancho de banda: su propia especificación (RFC 6716) lo sitúa en el orden de 6 a 510 kbps, y trae ideas tan buenas como dejar de transmitir cuando nadie habla (DTX) y gastar bits extra para proteger contra pérdida de paquetes cuando hace falta (FEC en banda). Con el audio entendí que la compresión es solo una esquina de un problema más rico: decidir qué transmitir, y cuándo no transmitir nada.
El video es el problema incómodo
El paso siguiente era el video, y ahí el experimento de la pizarra dejó de ser fácil.
Transmitir píxeles es carísimo. Un video no es un dibujo: cambia todo el tiempo, incluso cuando nada importante cambia, y los frames se repiten sin que el ojo lo note. "Enviar solo lo que cambia" debería ayudar, es la idea que usa VNC (RFC 6143) para refrescar solo las regiones sucias de una pantalla, pero en una conversación real las regiones que cambian están en todas partes: la cara, los labios, los ojos, los gestos. El "delta" de una cara es enorme.
La pregunta incómoda no es "¿cómo comprimo el video?", sino "¿qué parte del video es información que la otra persona necesita?".
Aquí ThePrimeagen sirvió de inspiración también: en los videos de ASCII art, particle systems y ASCII Doom encontré un montón de ideas curiosas que se pueden aplicar a este contexto. 🕹️
Estoy experimentando en dos caminos a la vez. Uno es el video de muy baja resolución, que sigue siendo caro, e incluso estoy pensando en implementar algo como un shader para enviar información usando los algoritmos que muestra ThePrimeagen en ASCII art. El otro es una representación mucho más agresiva: avatares que no transmiten píxeles sino el estado del cuerpo (cabeza, manos, postura).
// representación semántica: píxeles fuera, estado del cuerpo dentro
{ head: { x, y, z, rot }, hands: [...], posture: 'sentado' }
Es la misma intuición de la pizarra, llevada más lejos: ¿cuáles son los cambios mínimos que un humano necesita para sentir que hay otra persona enfrente? No sé dónde está el límite. Esa es, literalmente, una de las cosas que quiero medir. Actualmente el sistema de avatares ya funciona, aunque enviando apenas el mínimo de información se hace difícil transmitir los gestos de un rostro humano de forma congruente.
El problema más grande no era el ancho de banda
Y aquí está lo que el experimento me enseñó de verdad. La conexión para la que diseño no es "120 kbps sostenidos", sino una conexión que existe, desaparece, vuelve en ráfagas, se degrada y desaparece otra vez:
available → degraded → unavailable → available → burst → unavailable → …
Mi hipótesis es que el problema de ETECSA no es la velocidad sino la capacidad compartida: una antena que atiende muchos dispositivos y los sirve por tandas. No lo he verificado; lo pongo como lo que es, una observación informal con la que mi modelo encaja, no una medición. ⚠️
Cuando la conexión desaparece por minutos, ninguna cantidad de compresión arregla nada. Lo que necesitas saber es qué hacer mientras no hay canal: retener, priorizar y enviar cuando vuelva. La compresión pasó de ser la solución a ser una nota al pie del problema real.
Ahí encontré a gente que lleva décadas pensando en esto
Investigando cómo diseñar comunicación para un canal que puede desaparecer, llegué a algo que no esperaba: la comunidad DTN (Delay/Disruption Tolerant Networking, redes tolerantes a retrasos y disrupciones).
La NASA la describe como una arquitectura para redes con interrupciones, retrasos y desajustes de tasa de datos. La idea central es el store-and-forward: si no hay ruta hasta el destinatario, guardas el mensaje y sigues intentando. No es un estándar que yo haya inventado: hay RFC públicos, el Bundle Protocol v7 (RFC 9171) sobre la arquitectura del RFC 4838, y décadas de trabajo documentado.
Encontrarlo no convierte a mi pizarra en un sistema espacial 😅. Mi problema es mucho más pequeño: pocos participantes, una pizarra compartida, retrasos de segundos y no de minutos u horas. Implementar un Bundle Protocol completo aquí sería sobredimensionado. Lo que DTN me dio no fue código que copiar, fue confirmación de algo importante: la dificultad no está en que sobra o falta ancho de banda, sino en que no hay continuidad. Y hay gente que lleva décadas tomando esa distinción muy en serio.
LOW-NET y lo que todavía no sé
En ese punto dejé de pensar en esto como una pizarra. La pizarra era solo el primer experimento.
LOW-NET es el nombre que le puse a la pregunta cuando creció demasiado:
¿Cuánta información puede intercambiar una experiencia interactiva y seguir siendo útil cuando el ancho de banda es extremadamente limitado y la conectividad es intermitente?
En una frase corta: LOW-NET es un experimento en comunicar significado en lugar de comunicar estado innecesariamente. 🧪
Y ahora lo honesto, porque es la parte más interesante.
Lo que ya descubrí
- No siempre necesito transmitir una imagen completa. Para la pizarra puedo representar la interacción como cambios.
- RLE fue una buena puerta de entrada, pero no era el destino. Me hizo pensar en los datos como bytes, pero después apareció una optimización todavía más importante: no enviar datos que el receptor ya posee.
- La representación importa tanto como la compresión. Un mensaje pequeño diseñado para transportar solamente un cambio puede ser más interesante que tomar un mensaje grande y pasarlo por un compresor.
- Audio, video y una pizarra no consumen el ancho de banda de la misma manera. Cada uno obliga a decidir qué información es realmente importante.
- Para video puedo experimentar con diferentes niveles de información. Puedo intentar enviar diferencias de cámara o abandonar directamente los píxeles y enviar una representación semántica mediante un avatar.
- El video tiene muchas perillas que puedo convertir en experimentos medibles: resolución, FPS, tamaño de bloque, cuantización, keyframes y adaptación dinámica del FPS, entre otras.
- La intermitencia es un problema diferente de la compresión. Si durante un período no existe un canal útil, reducir un mensaje de 10 KB a 5 KB no lo hace atravesar una conexión que está caída.
Lo que todavía necesito averiguar
Aquí es donde empiezan los experimentos que todavía no quiero fingir que he resuelto:
- ¿Cuántos bytes produce realmente cada operación de la pizarra?
- ¿Cuánto ahorro obtengo frente a enviar el estado completo?
- ¿En qué tamaños merece la pena usar gzip y en cuáles el propio formato de compresión cuesta más de lo que ahorra?
- ¿Qué combinación de resolución, FPS, bloques, cuantización y keyframes produce una experiencia de video que siga siendo útil con un presupuesto de bytes muy pequeño?
- ¿Cuánto cuesta transmitir un avatar semántico frente a una cámara diferencial?
- ¿Cuánta información visual puede eliminarse antes de que una persona deje de percibir "hay alguien ahí"?
- ¿Qué debería conservar una sesión cuando la conexión desaparece?
- Cuando la conexión vuelve, ¿qué información merece realmente la pena enviar primero?
- ¿Cómo puedo hacer que todo esto sea medible y reproducible para que otra persona pueda ejecutar el mismo experimento y comprobar mis resultados?
El plan es medir todo eso con números de verdad, un registro de bytes por mensaje, estado vs delta, batcheo, reconexión y una cola local de store-and-forward, y publicar los resultados, sobre todo cuando sean embarazosos. Quiero que el experimento sea abierto, para que cualquiera pueda reproducir las condiciones y ver los números con sus propios ojos.
Esto acaba de empezar
Entré queriendo que un dibujo sobreviviera a una clase de inglés. Terminé preguntándome otra cosa, y todavía no sé hasta dónde lleva: ¿cuánto de una conversación puedo eliminar antes de que deje de sentirse como una conversación?
No tengo la respuesta. Esa es exactamente la razón por la que el experimento sigue en pie.
La demo está en https://whiteboard-five-gamma.vercel.app. Si esto te hace pensar en un problema que has sentido, quiero escucharlo.