3 pontos por GN⁺ 2023-08-21 | 1 comentários | Compartilhar no WhatsApp
  • A ferramenta usada para compartilhar esboços do reMarkable 2 durante videoconferências foi reformulada para abrir sem um serviço local no notebook, permitindo que o apresentador inicie streaming instantâneo apenas com o navegador
  • A nova arquitetura foi simplificada: um servidor HTTP dentro do reMarkable e um cliente JavaScript no navegador recebem imagens brutas e as desenham em um canvas
  • A alternativa com WebSocket funcionava, mas ainda apresentava problemas no iOS e overhead no servidor, então a solução final adotou um stream bruto: imagens em resolução fixa escritas continuamente com http.ResponseWriter e lidas por um stream de fetch
  • Como cada frame bruto em 1872x1404 tem cerca de 2,5 MB, após o firmware 3.3 os 16 valores de cor passaram a ser empacotados em uint4, reduzindo 50% do volume, e com RLE a média caiu para cerca de 200 KB por transmissão
  • Ao monitorar /dev/input/event*, o envio de novos frames para quando não há entrada; mesmo com clientes conectados, o uso de CPU cai para 0, e durante a escrita fica em cerca de 10% de CPU

Por que a ferramenta antiga era incômoda

  • A ferramenta de streaming para reMarkable criada em 2021 era usada para compartilhar esboços em videoconferências, e bastava compartilhar a aba do navegador para facilitar o foco no conteúdo da apresentação
  • A implementação anterior era dividida em três componentes
    • Servidor: executado no dispositivo reMarkable e expunha a imagem bruta da tela atual
    • Cliente: no notebook, buscava a imagem bruta do servidor e a processava em um formato visível no navegador
    • Renderizador: lia um stream HTTP MJPEG e exibia a tela, como no navegador ou no VLC
  • Para reduzir o uso de CPU no dispositivo, o servidor extraía imagens apenas quando havia cliente conectado, e a comunicação usava gRPC
  • O cliente no notebook buscava repetidamente as imagens, codificava em JPEG e então oferecia um serviço HTTP com stream MJPEG
  • Em ambientes de apresentação, a configuração de rede era um incômodo: endereço do reMarkable, permissão para executar o cliente e o IP do cliente que o renderizador precisava conhecer

Nova arquitetura aberta só com o navegador

  • O novo objetivo era permitir acesso ao stream em qualquer navegador apenas informando o endereço do reMarkable
  • O cliente separado no notebook foi eliminado, e a estrutura passou a incluir um servidor HTTP dentro do componente de servidor no próprio reMarkable
  • O cliente a ser executado no navegador precisava estar em JavaScript ou WASM
    • A princípio foi considerada a compilação para WASM para aproveitar a experiência com Go, mas a ideia foi abandonada por exigir muitas adaptações devido às limitações
    • A segunda versão do cliente acabou sendo escrita em JavaScript
  • ChatGPT foi usado no processo de obtenção de trechos de código JavaScript e explicações, mas a direção da solução desejada foi definida diretamente pelo autor

Forma de renderização com canvas

  • Para sair do stream MJPEG, foi usado o canvas, elemento básico de manipulação de imagens no navegador
  • A imagem bruta recebida do reMarkable é lida como Uint8Array, os mesmos valores são colocados nos canais R/G/B dos pixels RGBA de ImageData, e o canal alfa é definido como 255 para exibição
  • Para possibilitar exibição responsiva, rotação e potencial colorização, um fixedCanvas de tamanho fixo é mantido oculto
  • O conteúdo do canvas oculto é copiado para o canvas de exibição com drawImage
  • Quando o tamanho da janela do navegador muda, a largura e a altura do canvas de exibição são ajustadas com base no tamanho do contêiner e na proporção 1872/1404

Abandono do WebSocket em favor de stream bruto

  • Como gRPC não é uma escolha comum no desenvolvimento web, a primeira implementação substituta usou WebSocket como meio de comunicação e encapsulamento
  • As mensagens WebSocket continham imagens brutas, e o cliente no navegador atualizava o canvas a cada mensagem recebida para parecer streaming
  • Essa abordagem permitia controlar, no lado do servidor, a frequência de envio das mensagens para gerenciar carga de memória e CPU no reMarkable
  • Mas havia problemas no iOS, e também era difícil controlar o overhead da implementação de WebSocket no servidor
  • A estrutura final removeu o encapsulamento e passou a transmitir a imagem bruta diretamente pela rede, aproveitando o tamanho fixo da imagem
    • O servidor em Go faz Write repetidamente da imagem em http.ResponseWriter
    • O cliente no navegador lê o ReadableStream de fetch('/stream') e aplica os chunks recebidos aos dados do canvas

Otimização do volume de transmissão

  • A imagem bruta do reMarkable 2 tem resolução 1872x1404 e tamanho de cerca de 2,5 MB, e esses dados precisam ser transmitidos a cada frame
  • Depois do firmware 3.3, os 16 valores de cor do reMarkable puderam ser representados como um array uint4 em vez de uint8
    • Go e JavaScript não têm tipo uint4 nativo
    • A solução foi armazenar dois valores de pixel em um único byte uint8
    • Em Go, os dois valores uint4 são empacotados nos 4 bits mais altos e nos 4 bits mais baixos; em JavaScript, eles são desempacotados novamente
    • Essa representação reduz o volume de dados em 50%
  • Para compressão adicional, foi usado Run Length Encoding (RLE)
    • RLE é um algoritmo simples que transmite juntos a quantidade de repetições consecutivas de um mesmo valor de pixel e o próprio valor
    • Por exemplo, 0 0 0 0 0 0 1 1 1 0 0 0 0 pode ser representado como 6 0 3 1 4 0
  • O valor de contagem pode chegar a até 1872*1404, o que exigiria tipos como uint64, e em alguns casos o resultado comprimido poderia até ficar maior que o original
  • Para evitar isso, foi escolhido um ponto de equilíbrio limitando o comprimento da contagem a 15 e armazenando contagem e valor do pixel em um único byte
  • A implementação de RLE funciona como o io.Writer do Go, podendo ser reutilizada; em tese seria possível aplicar RLE duas vezes, mas isso não foi necessário
  • Após o empacotamento e o RLE, o volume médio de transmissão ficou em cerca de 200 KB

Envio de frames apenas quando há mudanças

  • A última otimização foi enviar novos frames apenas quando a tela muda
  • Calcular se houve mudança por checksum poderia gerar carga excessiva de CPU
  • O reMarkable é baseado em Linux, então entradas de caneta ou toque chegam por /dev/input/event*
  • Uma goroutine monitora esses eventos de entrada e envia a imagem apenas quando necessário
  • Sem eventos, mesmo com o cliente conectado, o uso de CPU cai para 0
  • Durante a escrita, o uso de CPU fica em cerca de 10%

Mudanças de firmware e custo de manutenção

  • Esta aplicação é baseada em hacking, e o desafio central é separar de forma eficaz a interface de captura de imagem do cliente/renderizador
  • Na implementação anterior, cliente e servidor estavam totalmente separados por meio de definições protobuf
  • O firmware reMarkable 3.3 quebrou a ferramenta, e os detalhes estão na issue 36 do GitHub
    • Naquele momento, a correção afetou apenas o componente cliente
  • Segundo a issue 58 do GitHub, o firmware 3.6 também pode trazer mudanças incompatíveis
    • Nesse caso, correções mais amplas podem ser necessárias
    • Ainda assim, como o cliente está integrado ao servidor em uma estrutura autocontida, a atualização no dispositivo pode ficar mais simples
  • A aplicação e o código-fonte estão disponíveis em github.com/owulveryck/goMarkableStream

1 comentários

 
GN⁺ 2023-08-21
Opiniões no Hacker News
  • A ferramenta de 2021 que fazia streaming da tela do tablet reMarkable para um notebook foi refinada e relançada, e o novo post se aprofunda em arquitetura, componentes e no processo de melhoria da experiência do usuário
    Do ponto de vista de um gerente de produto, simplificou-se o processo de ativação observando como o usuário se sentia, fez-se a ferramenta funcionar sem serviço local e também se otimizou o uso de rede

    • Parece um projeto bacana. Depois de Ctrl-C e reiniciar com ./goMarkableStream, funcionou em certa medida, mas ainda aparece waiting for reMarkable screen com frequência, e o serviço está instável
      Instalei depois de atualizar o reMarkable2 para 3.5.2.1807, mas não houve reação ao desenhar sobre o notebook, uma folha ou um livro, e às vezes aparecem logs como read /dev/input/event2: file already closed e read /dev/input/event1: file already closed
      Tanto https://192.168.8.143:2001/ quanto https://10.11.99.1:2001/ servem HTML e canvas, e também tentei no Chrome, Firefox e Brave
      Parece haver uma limitação de um navegador e um IP por stream, mas, seja qual for o endereço e o navegador usados, às vezes aparece waiting for reMarkable screen. Depois de nohup ./goMarkableStream &, fechei o PuTTY e reiniciei o cliente, e todos os navegadores ficaram no mesmo estado; ao acessar https://10.11.99.1:2001/stream, retorna too many requests. Gostaria de saber como reiniciar o stream
    • Fico curioso se também daria para implementar por conexão USB
  • Como alternativa, uso o SuperNote com muita satisfação. Ele permite espelhamento de tela, então é ótimo para desenhar diagramas rapidamente durante reuniões
    A desvantagem é que o SuperNote sobe um pequeno servidor web e você acessa pelo Firefox, então o notebook e o SuperNote precisam estar na mesma rede. No home office isso não é problema, mas pode ser bloqueado por políticas da empresa
    Seja o RM2 ou o SuperNote, para quem gosta de escrever ideias com caneta e papel, são ferramentas excelentes; a sensação é bem diferente de apps ou documentos de texto, e também dá para rabiscar nas notas
    [0]: https://supernote.com/

    • O Onyx Boox Note também funciona bem, e continua recebendo atualizações mesmo depois de mais de 5 anos
      Porém, ao comprar, é preciso aceitar a violação da GPL. Embora seja totalmente baseado em Android, eles não publicam o código-fonte do sistema operacional
    • Eu estava procurando um tablet de e-ink para usar como leitor de e-books e para anotações, e a recomendação ajudou. Estava em dúvida entre o Remarkable 2 e o Boox, e queria saber como é a experiência de atualizações de software do SuperNote
      Tenho receio de acabar comprando um dispositivo que não receba atualizações de recursos — ou ao menos atualizações de segurança — nos próximos 3 a 5 anos
    • Pelo que me lembro, o SuperNote não cumpria a GPL do software que distribuía junto; fico curioso se a postura mudou
    • Ainda não enfrentei o problema de “precisar estar na mesma rede”, mas incluir uma função nativa de Ngrok parece algo simples, de poucos minutos. Assim daria para fazer streaming pela internet
    • Fico curioso sobre como a sensação de escrita do SuperNote se compara à do RM2
  • A renderização em canvas HTML pode ficar mais rápida usando arrays tipados, como explicado aqui: https://hacks.mozilla.org/2011/12/faster-canvas-pixel-manipu...

    • Obrigado, vou dar uma olhada
  • Esse é exatamente o tipo de conteúdo que quero ver por aqui. Gostei de como o ChatGPT ajudou a aprender e resolver problemas em uma área que ele não conhece bem, e me identifiquei com a frase “eu era o desenvolvedor e o ChatGPT era o programador
    Também é verdade que simplicidade, na prática, é complexa

  • Imagino que a escolha por JPEG tenha sido porque é fácil transformar em MJPEG e, ao entregar para algo que suporte o formato, a decodificação sai quase “de graça”. Mas isso pode ser um fator que pesa bastante na CPU do reMarkable
    JPEG é mais adequado para fotos, enquanto a tela do reMarkable se parece mais com ilustrações e, além disso, é em tons de preto e branco. Usar outro formato comum de imagem, como PNG, ou apenas uma compressão RLE simples, provavelmente reduziria a carga de CPU

    • Tecnicamente, o reMarkable não é monocromático, e sim em escala de cinza; se não me engano, suporta 16 níveis de cinza. No app complementar também há tintas coloridas, como caneta em azul e vermelho e marca-texto em amarelo e verde
      Além disso, o formato de arquivo próprio não é bitmap, mas baseado na entrada da escrita à mão
    • O motivo inicial para escolher JPEG foi esse mesmo. Só que, por isso, adotamos uma estrutura cliente/servidor, e a codificação era feita no notebook cliente, não no tablet
      Ao fazer profiling, vi que a maior parte da CPU era usada na transferência de dados pelo fio, então adicionei compressão. Agora o uso de CPU está baixo
  • Fico curioso se vocês chegaram a avaliar uma abordagem de enviar apenas as áreas alteradas do framebuffer. Isso poderia reduzir bastante a taxa de transferência de dados: https://github.com/pl-semiotics/mxc_epdc_fb_damage
    O projeto rM VNC também faz isso, mas gosto mais da experiência de usuário deste app, que não exige software no lado do cliente

    • O problema desse método é que exige algum nível de análise no dispositivo, e quero manter o código o mais pouco invasivo possível. Vou ver se há alguma forma barata de fazer isso
  • É muito legal e eu gostaria de gostar do ReMarkable 2, mas não é fácil por causa da postura de que é um dispositivo inseguro: https://support.remarkable.com/s/article/Does-reMarkable-off...

    • O conteúdo do link significa que este dispositivo tem apenas o mesmo nível de segurança física do papel que ele tenta substituir. Ou seja, se alguém tiver acesso ao dispositivo, consegue ler
      Não se trata daquelas vulnerabilidades de software conhecidas que normalmente vêm à mente quando se fala em um dispositivo inseguro conectado à rede
    • Extraoficialmente, é possível usar criptografia do diretório home baseada em gocryptfs: https://github.com/RedTeamPentesting/remarkable-encryption
    • O software também é muito limitado por enquanto. É uma pena que oficialmente não permitam usar um marketplace ou extensões no dispositivo
    • Existe algum leitor de e-books que ofereça criptografia de disco completo?
  • Gostaria de ler mais sobre a parte: “No início, tentei compilar o cliente para WASM. Parecia promissor, pois poderia aproveitar minha experiência de desenvolvimento em Go, mas esbarrei em várias limitações que exigiriam modificações consideráveis”

    • O principal problema era a biblioteca gRPC, cujo suporte atualmente era muito limitado. Além disso, a compressão JPEG em Go é lenta e consome muita CPU
      Mesmo que eu gerasse um stream MJPEG, ainda havia a questão de como exibi-lo. Pensei em usar canvas, mas era difícil acessar o backend do canvas sem fazer grandes cópias entre WASM e JS, e o tamanho também era de 2,5 MB
      No fim, ao depender de WASM, parecia que eu teria de implementar diretamente muitas operações básicas de imagem que em JS já são acessíveis de forma nativa, como rotação de imagem
  • Fico curioso para saber como esta ferramenta difere do streaming integrado, ou seja, da função de compartilhamento de tela

    • Para usar o recurso integrado, é preciso instalar o app desktop e, pelo que sei, não há versão para Linux
      https://support.remarkable.com/s/article/Screen-Share
      A solução do artigo parece funcionar também no Linux, desde que haja um navegador com recursos suficientes
    • A maior diferença é que agora não é necessário instalar um cliente. Basta inserir o endereço do reMarkable no navegador para ver o conteúdo
    • Eu achava que esse recurso já existia. O compartilhamento de tela é bom o suficiente e eu também o uso para livestreaming
  • Gosto do reMarkable, mas gostaria que eles se concentrassem mais em recursos de streaming como este do que em uma assinatura pela qual não pretendo pagar no futuro