- La herramienta que se usaba para compartir bocetos de reMarkable 2 durante videollamadas cambió para poder abrirse sin un servicio local en la laptop, así que el presentador ahora puede iniciar streaming al instante solo con el navegador
- La nueva arquitectura se simplifica con un servidor HTTP dentro del reMarkable y un cliente JavaScript en el navegador que recibe imágenes crudas y las dibuja en
canvas - La alternativa con WebSocket funcionaba, pero seguían existiendo problemas en iOS y sobrecarga del servidor, así que se consolidó un enfoque de stream crudo que escribe imágenes de resolución fija de forma continua con
http.ResponseWritery las lee con un stream defetch - Como cada frame crudo de 1872x1404 pesa unos 2.5 MB, desde el firmware 3.3 los valores de 16 colores se empaquetan como uint4 para reducirlos 50%, y con RLE el volumen promedio baja hasta unos 200 KB por transmisión
- Al monitorear
/dev/input/event*, si no hay entrada se deja de enviar nuevos frames; incluso con clientes conectados, el uso de CPU cae a 0, y mientras se escribe funciona con cerca de 10% de CPU
Por qué la herramienta anterior resultaba incómoda
- La herramienta de streaming para reMarkable creada en 2021 se usaba para compartir bocetos durante videollamadas, y como bastaba con compartir una pestaña del navegador, era más fácil concentrarse en la presentación
- La implementación anterior estaba dividida en tres componentes
- Servidor: se ejecuta en el dispositivo reMarkable y expone la imagen cruda de la pantalla actual
- Cliente: en la laptop, obtiene la imagen cruda del servidor y la procesa a un formato que el navegador pueda visualizar
- Renderizador: muestra en pantalla un stream HTTP MJPEG, como en el navegador o VLC
- Para reducir el uso de CPU en el dispositivo, el servidor extraía imágenes solo cuando había un cliente conectado, y para la comunicación se usaba gRPC
- El cliente en la laptop obtenía imágenes repetidamente, las codificaba en JPEG y luego ofrecía un stream MJPEG mediante un servicio HTTP
- En entornos de presentación, la configuración de red era una carga: la dirección del reMarkable, los permisos para ejecutar el cliente y la IP del cliente que el renderizador debía conocer
Nueva arquitectura que se abre solo con el navegador
- El nuevo objetivo era permitir acceso al stream desde cualquier navegador con solo ingresar la dirección del reMarkable
- Se eliminó el cliente separado en la laptop y se cambió a una estructura que integra un servidor HTTP dentro del componente servidor del reMarkable
- El cliente que se ejecuta en el navegador necesitaba ser JavaScript o WASM
- Al principio se evaluó compilar a WASM para aprovechar la experiencia con Go, pero se descartó por las limitaciones que exigían modificaciones considerables
- La segunda versión del cliente terminó escrita en JavaScript
- Se usó ChatGPT para obtener fragmentos de código JavaScript y explicaciones, pero la dirección de la solución deseada se definió directamente
Método de renderizado con canvas
- Para salir del stream MJPEG, se utilizó
canvas, el elemento base del navegador para manipulación de imágenes - La imagen cruda recibida desde el reMarkable se lee como
Uint8Array, se coloca el mismo valor en R/G/B dentro de los datos RGBA deImageData, y se fija el valor alfa en 255 para mostrarla - Para permitir visualización responsiva, rotación y posibilidades de coloreado, se mantiene oculto un
fixedCanvasde tamaño fijo - En el canvas de visualización, el contenido del canvas oculto se copia con
drawImage - Cuando cambia el tamaño de la ventana del navegador, se ajustan el ancho y la altura del canvas visible según el tamaño del contenedor y la relación 1872/1404
Abandonar WebSocket y pasar a stream crudo
- Como gRPC no es una opción común en desarrollo web, la primera implementación alternativa usó WebSocket como medio de comunicación y encapsulación
- Los mensajes de WebSocket contenían imágenes crudas, y el cliente del navegador actualizaba el canvas cada vez que recibía un mensaje para que pareciera streaming
- Este enfoque permitía gestionar la carga de memoria y CPU del reMarkable ajustando la frecuencia de envío de mensajes del lado del servidor
- Pero había problemas en iOS, y también era difícil controlar la sobrecarga de la implementación de WebSocket en el servidor
- La estructura final eliminó la encapsulación y aprovechó el tamaño fijo de la imagen para transmitir directamente la imagen cruda por la red
- El servidor en Go hace
Writerepetidamente sobrehttp.ResponseWriter - El cliente del navegador lee el
ReadableStreamdefetch('/stream')y refleja los fragmentos recibidos en los datos del canvas
- El servidor en Go hace
Optimización del volumen de transferencia
- La imagen cruda de reMarkable 2 pesa alrededor de 2.5 MB con resolución 1872x1404, y esos datos deben transmitirse en cada frame
- Desde el firmware 3.3, los 16 valores de color del reMarkable pueden representarse como un arreglo uint4 en vez de uint8
- Ni Go ni JavaScript tienen un tipo uint4 nativo
- Se resolvió guardando dos valores de píxel en un solo byte uint8
- En Go, los dos valores uint4 se empaquetan en los 4 bits altos y los 4 bits bajos, y en JavaScript luego se desempaquetan
- Esta representación permite reducir el volumen de datos en 50%
- Para compresión adicional se usó Run Length Encoding (RLE)
- RLE es un algoritmo simple que transmite juntos la cantidad de repeticiones consecutivas y el valor del mismo píxel
- Por ejemplo,
0 0 0 0 0 0 1 1 1 0 0 0 0se representa como6 0 3 1 4 0
- Como el valor del conteo puede crecer hasta 1872*1404, podrían hacer falta tipos como uint64, y en algunos casos existe el riesgo de que el resultado comprimido sea más grande que el original
- Para evitarlo, se eligió un punto de equilibrio limitando la longitud del conteo a 15 y guardando en un solo byte tanto el conteo como el valor del píxel
- La implementación de RLE funciona como
io.Writerde Go, así que puede reutilizarse; incluso sería posible aplicar RLE dos veces si hiciera falta, aunque por ahora no fue necesario - Después del empaquetado y RLE, el volumen promedio de transmisión queda en unos 200 KB
Enviar frames solo cuando hay cambios
- La última optimización consiste en enviar un frame nuevo solo cuando cambia la pantalla
- Calcular si hubo cambios mediante checksum podría imponer una carga alta de CPU
- Como reMarkable está basado en Linux, las entradas del lápiz o del tacto llegan por
/dev/input/event* - Una goroutine monitorea esos eventos de entrada y transmite imágenes solo cuando hace falta
- Si no hay eventos, el uso de CPU cae a 0 incluso con clientes conectados
- Mientras se escribe, el uso de CPU se mantiene alrededor de 10%
Cambios de firmware y carga de mantenimiento
- Esta aplicación se basa en hacking, y el desafío central es separar de forma efectiva la interfaz para obtener imágenes del cliente/renderizador
- La implementación anterior separaba completamente cliente y servidor mediante definiciones protobuf
- El firmware reMarkable 3.3 rompió la herramienta, y hay detalles en el issue 36 de GitHub
- En ese momento, la corrección afectó solo al componente cliente
- Según el issue 58 de GitHub, el firmware 3.6 también puede traer cambios incompatibles
- En ese caso podrían hacer falta modificaciones más amplias
- Aun así, como el cliente está integrado en una estructura autocontenida dentro del servidor, las actualizaciones del dispositivo podrían simplificarse
- La aplicación y el código fuente están disponibles en github.com/owulveryck/goMarkableStream
1 comentarios
Opiniones en Hacker News
Se volvió a pulir y publicar una herramienta de 2021 para transmitir la pantalla de una tablet reMarkable a una laptop, y el nuevo artículo profundiza en la arquitectura, los componentes y el proceso de mejora de la experiencia de usuario.
Desde la perspectiva de un gerente de producto, se simplificó el proceso de activación observando cómo se sentían los usuarios, se hizo que funcionara sin un servicio local y también se optimizó el uso de la red.
Ctrl-Cy reiniciarlo con./goMarkableStream, funcionó hasta cierto punto, pero el servicio sigue siendo inestable y aparece con frecuenciawaiting for reMarkable screen.Lo instalé después de actualizar mi reMarkable2 a 3.5.2.1807, pero no respondió al dibujar sobre una laptop, una hoja o un libro, y a veces veo logs como
read /dev/input/event2: file already closedyread /dev/input/event1: file already closed.Tanto https://192.168.8.143:2001/ como https://10.11.99.1:2001/ sirven HTML y canvas, y también lo probé en Chrome, Firefox y Brave.
Parece haber un límite de un navegador y una IP por stream, pero a veces aparece
waiting for reMarkable screensin importar qué dirección o navegador use. Después denohup ./goMarkableStream &, cerré PuTTY y reinicié el cliente, todos los navegadores quedaron en el mismo estado, y al ver https://10.11.99.1:2001/stream devuelvetoo many requests. Me pregunto cómo habría que reiniciar el stream.Como alternativa, estoy usando SuperNote con muchísima satisfacción. Permite duplicar la pantalla, así que es muy bueno para dibujar diagramas rápidamente durante reuniones.
La desventaja es que SuperNote levanta un pequeño servidor web y uno se conecta con Firefox, así que la laptop y la SuperNote tienen que estar en la misma red. En la oficina en casa no es un problema, pero podría estar bloqueado por políticas de la empresa.
Tanto la RM2 como la SuperNote son herramientas excelentes para quienes disfrutan escribir ideas con lápiz y papel; se sienten bastante distintas de una app o un documento de texto, y también se puede garabatear en las notas.
[0]: https://supernote.com/
Eso sí, al comprarla hay que aceptar las violaciones a la GPL. Aunque está completamente basada en Android, no publican el código fuente del sistema operativo.
Me preocupa terminar comprando un dispositivo que no reciba actualizaciones de funciones, o al menos de seguridad, durante los próximos 3 a 5 años.
El renderizado en canvas de HTML podría volverse más rápido si se usan arrays tipados, como se explica aquí: https://hacks.mozilla.org/2011/12/faster-canvas-pixel-manipu...
Este es exactamente el tipo de contenido que quiero ver aquí. Me gustó cómo ChatGPT ayudó a aprender y resolver problemas en un área que no conoce bien, y me identifiqué con la frase “yo era el desarrollador y ChatGPT era el programador”.
También es cierto eso de que la simplicidad en realidad es compleja.
Supongo que eligieron JPEG porque es fácil convertirlo a MJPEG y, si se lo pasas a algo que lo soporte, la decodificación sale casi gratis. Pero eso podría ser un factor que le meta bastante carga a la CPU de reMarkable.
JPEG es más adecuado para fotos, pero la pantalla de reMarkable se parece más a una ilustración y, además, es en escala de blanco y negro. Creo que con otro formato de imagen común, como PNG, o incluso una compresión RLE simple, la carga de CPU podría ser menor.
Además, el formato de archivo propietario no es un bitmap, sino que se basa en la entrada manuscrita.
Al perfilarlo, vi que la mayor parte de la CPU se usaba en transferir datos por el cable, así que agregué compresión. Ahora el uso de CPU es bajo.
Me pregunto si evaluaron enviar solo las áreas modificadas del framebuffer. Eso podría reducir mucho la tasa de transferencia de datos: https://github.com/pl-semiotics/mxc_epdc_fb_damage
El proyecto rM VNC también lo hace así, pero me gusta más la experiencia de usuario de esta app, que no requiere software del lado del cliente.
Es realmente genial y quisiera que me gustara la ReMarkable 2, pero no es fácil por su postura de que es un dispositivo no seguro: https://support.remarkable.com/s/article/Does-reMarkable-off...
No se trata de las vulnerabilidades de software conocidas en las que uno suele pensar cuando se habla de un dispositivo inseguro conectado a la red.
Me gustaría leer más sobre la parte que dice: “Al principio intenté compilar el cliente a WASM. Parecía prometedor porque podía aprovechar mi experiencia desarrollando en Go, pero me topé con varias limitaciones que requerían cambios considerables”.
Incluso si se generaba un stream MJPEG, también estaba el problema de cómo mostrarlo. Pensé en usar canvas, pero era difícil acceder al backend de canvas sin hacer copias grandes entre WASM y JS, y además pesaba 2.5 MB.
Al final, depender de WASM parecía implicar que tendría que implementar por mi cuenta muchas operaciones básicas de imagen, como la rotación, que en JS están disponibles de forma nativa.
Me da curiosidad en qué se diferencia esta herramienta del streaming integrado, es decir, la función de compartir pantalla.
https://support.remarkable.com/s/article/Screen-Share
La solución del artículo parece funcionar también en Linux siempre que se tenga un navegador con suficientes funciones.
Me gusta reMarkable, pero preferiría que se enfocaran en esta función de streaming en lugar de en una suscripción por la que no pienso pagar ni ahora ni en el futuro.