- Das Tool, mit dem während Videokonferenzen Skizzen vom reMarkable 2 geteilt wurden, wurde so umgestellt, dass es sich ohne lokalen Dienst auf dem Laptop öffnen lässt, sodass Vortragende spontan mit dem Streaming allein im Browser beginnen können
- Die neue Architektur wurde vereinfacht: Ein interner HTTP-Server auf dem reMarkable und ein JavaScript-Client im Browser empfangen Rohbilder und zeichnen sie auf ein
canvas - Eine WebSocket-Alternative funktionierte zwar, ließ aber iOS-Probleme und Server-Overhead offen; daher fiel die Wahl schließlich auf einen Rohdaten-Stream, bei dem Bilder mit fester Auflösung fortlaufend über
http.ResponseWritergeschrieben und perfetch-Stream gelesen werden - Ein Rohbild mit 1872x1404 Pixeln ist etwa 2,5 MB groß; seit Firmware 3.3 werden 16 Farbwerte als uint4 gepackt, was die Größe um 50 % reduziert, und mit RLE sinkt das durchschnittliche Übertragungsvolumen auf etwa 200 KB
- Durch Überwachung von
/dev/input/event*wird das Senden neuer Frames gestoppt, wenn keine Eingaben vorliegen; selbst bei verbundenen Clients fällt die CPU-Auslastung dann auf 0, und beim Schreiben liegt sie bei etwa 10 % CPU
Warum das bisherige Tool unpraktisch war
- Das 2021 entwickelte reMarkable-Streaming-Tool wurde genutzt, um während Videokonferenzen Skizzen zu teilen, und da nur ein Browser-Tab geteilt werden musste, konnte man sich leichter auf die Präsentation konzentrieren
- Die bisherige Implementierung bestand aus drei Komponenten
- Server: Läuft auf dem reMarkable-Gerät und stellt das Rohbild des aktuellen Bildschirms bereit
- Client: Holt auf dem Laptop die Rohbilder vom Server und verarbeitet sie in ein vom Browser darstellbares Format
- Renderer: Liest einen HTTP-MJPEG-Stream, etwa im Browser oder in VLC, und zeigt ihn an
- Um die CPU-Auslastung des Geräts zu senken, extrahierte der Server Bilder nur dann, wenn ein Client verbunden war; für die Kommunikation wurde gRPC verwendet
- Der Laptop-Client holte die Bilder wiederholt ab, kodierte sie als JPEG und stellte dann per HTTP einen MJPEG-Stream bereit
- In Präsentationsumgebungen waren Netzwerkeinstellungen wie die reMarkable-Adresse, Ausführungsrechte für den Client und die Client-IP, die der Renderer kennen musste, eine Belastung
Die neue Architektur: nur noch der Browser
- Das neue Ziel war, den Zugriff auf den Stream in jedem Browser allein durch Eingabe der reMarkable-Adresse zu ermöglichen
- Der separate Laptop-Client wurde entfernt und stattdessen ein HTTP-Server direkt in die Server-Komponente des reMarkable integriert
- Der im Browser laufende Client musste als JavaScript oder WASM umgesetzt werden
- Zunächst wurde geprüft, den vorhandenen Go-Erfahrungsschatz für eine WASM-Kompilierung zu nutzen, doch wegen Einschränkungen, die umfangreiche Änderungen erfordert hätten, wurde das verworfen
- Der Client der zweiten Version wurde schließlich in JavaScript geschrieben
- Beim Erzeugen von JavaScript-Codefragmenten und Erklärungen wurde ChatGPT verwendet, die Richtung der gewünschten Lösung wurde jedoch selbst festgelegt
Rendering mit canvas
- Um sich vom MJPEG-Stream zu lösen, wurde das grundlegende Browser-Element zur Bildmanipulation verwendet:
canvas - Das vom reMarkable empfangene Rohbild wird als
Uint8Arraygelesen; anschließend werden dieselben Werte in die RGBA-Pixeldaten vonImageDatafür R/G/B eingetragen und der Alphawert auf 255 gesetzt - Für responsives Anzeigen, Rotation und mögliche Einfärbung wird ein
fixedCanvasmit fester Größe verborgen vorgehalten - In das sichtbare Canvas wird der Inhalt des verborgenen Canvas per
drawImagekopiert - Wenn sich die Fenstergröße des Browsers ändert, werden Breite und Höhe des Anzeigecanvas anhand der Containergröße und des Verhältnisses 1872/1404 angepasst
WebSocket verworfen, Umstieg auf Rohdaten-Streaming
- Da gRPC in der Webentwicklung keine übliche Wahl ist, nutzte die erste alternative Implementierung WebSocket als Kommunikations- und Kapselungsmittel
- Die WebSocket-Nachrichten enthielten Rohbilder, und der Browser-Client aktualisierte das Canvas bei jeder empfangenen Nachricht, sodass es wie Streaming wirkte
- Mit diesem Ansatz ließ sich serverseitig die Versandfrequenz der Nachrichten steuern, um Speicher- und CPU-Last des reMarkable zu verwalten
- Allerdings gab es Probleme unter iOS, und auch der Overhead der serverseitigen WebSocket-Implementierung ließ sich nur schwer kontrollieren
- In der finalen Architektur wurde auf Kapselung verzichtet und stattdessen die feste Bildgröße genutzt, um Rohbilder direkt über das Netzwerk zu übertragen
- Der Go-Server schreibt die Bilder wiederholt per
Writeaufhttp.ResponseWriter - Der Browser-Client liest den
ReadableStreamvonfetch('/stream')und überträgt eingehende Chunks in Canvas-Daten
- Der Go-Server schreibt die Bilder wiederholt per
Optimierung des Übertragungsvolumens
- Das Rohbild des reMarkable 2 ist bei einer Auflösung von 1872x1404 etwa 2,5 MB groß, und diese Daten müssen für jeden Frame übertragen werden
- Seit Firmware 3.3 lassen sich die 16 Farbwerte des reMarkable als uint4-Array statt als uint8 darstellen
- Weder Go noch JavaScript haben einen nativen uint4-Typ
- Daher werden je zwei Pixelwerte in einem uint8-Byte gespeichert
- In Go werden zwei uint4-Werte in die oberen und unteren 4 Bit gepackt, in JavaScript wieder entpackt
- Diese Darstellung reduziert die Datenmenge um 50 %
- Für zusätzliche Kompression wird Run Length Encoding (RLE) verwendet
- RLE ist ein einfacher Algorithmus, der die Anzahl aufeinanderfolgender gleicher Pixelwerte zusammen mit dem Wert überträgt
- Beispiel:
0 0 0 0 0 0 1 1 1 0 0 0 0wird als6 0 3 1 4 0dargestellt
- Der Zählwert kann bis auf 1872*1404 anwachsen, was Typen wie uint64 erfordern könnte; zudem besteht das Risiko, dass das komprimierte Ergebnis in manchen Fällen größer als das Original wird
- Um das zu vermeiden, wurde die Zähllänge auf 15 begrenzt und ein Kompromiss gewählt, bei dem Zählwert und Pixelwert in einem Byte untergebracht werden
- Die RLE-Implementierung verhält sich wie ein
io.Writerin Go und ist dadurch wiederverwendbar; bei Bedarf könnte RLE sogar zweimal angewendet werden, was hier aber nicht nötig war - Nach Packing und RLE liegt das durchschnittliche Übertragungsvolumen bei etwa 200 KB
Frames nur bei Änderungen senden
- Die letzte Optimierung besteht darin, neue Frames nur dann zu senden, wenn sich der Bildschirm ändert
- Eine Prüfung per Checksumme könnte die CPU unnötig belasten
- Das reMarkable basiert auf Linux, daher werden Stift- oder Berührungseingaben über
/dev/input/event*geliefert - Eine Goroutine überwacht diese Eingabeereignisse und sendet Bilder nur bei Bedarf
- Wenn keine Ereignisse auftreten, fällt die CPU-Auslastung selbst bei verbundenen Clients auf 0
- Während des Schreibens liegt die CPU-Auslastung bei etwa 10 %
Firmware-Änderungen und Wartungsaufwand
- Diese Anwendung basiert auf Hacking; die zentrale Herausforderung ist, die Schnittstelle zum Abrufen von Bildern sowie Client und Renderer wirksam voneinander zu trennen
- In der früheren Implementierung waren Client und Server über Protobuf-Definitionen vollständig getrennt
- Die Firmware 3.3 des reMarkable machte das Tool funktionsunfähig; Details dazu stehen im GitHub-Issue 36
- Die damalige Korrektur betraf nur die Client-Komponente
- Auch Firmware 3.6 könnte laut GitHub-Issue 58 inkompatible Änderungen mit sich bringen
- In diesem Fall könnten umfassendere Anpassungen nötig sein
- Da der Client jedoch in den Server integriert ist und die Struktur in sich geschlossen ist, könnten Updates auf dem Gerät einfacher werden
- Die Anwendung und der Quellcode sind unter github.com/owulveryck/goMarkableStream verfügbar
1 Kommentare
Hacker-News-Kommentare
Das Tool von 2021, mit dem der Bildschirm eines reMarkable-Tablets auf einen Laptop gestreamt wurde, wurde überarbeitet und erneut veröffentlicht; der neue Beitrag behandelt ausführlich Architektur, Komponenten und den Prozess zur Verbesserung der User Experience.
Aus Sicht eines Produktmanagers wurde anhand des Nutzererlebnisses der Aktivierungsprozess vereinfacht, der Betrieb ohne lokalen Dienst ermöglicht und auch die Netzwerknutzung optimiert.
Ctrl-Cund einem Neustart mit./goMarkableStreamfunktionierte es einigermaßen, aber weiterhin erscheint häufigwaiting for reMarkable screen, und der Dienst ist instabil.Ich habe es installiert, nachdem ich das reMarkable2 auf 3.5.2.1807 aktualisiert hatte, aber beim Zeichnen auf Laptop, Blatt oder Buch gab es keine Reaktion; gelegentlich sehe ich Logs wie
read /dev/input/event2: file already closedundread /dev/input/event1: file already closed.Sowohl https://192.168.8.143:2001/ als auch https://10.11.99.1:2001/ liefern HTML und Canvas aus, und ich habe es auch mit Chrome, Firefox und Brave versucht.
Es wirkt so, als gäbe es eine Beschränkung auf einen Browser und eine IP pro Stream, aber egal welche Adresse und welchen Browser ich verwende, manchmal erscheint
waiting for reMarkable screen. Nachdem ichnohup ./goMarkableStream &ausgeführt, PuTTY geschlossen und den Client neu gestartet hatte, waren alle Browser im selben Zustand; bei https://10.11.99.1:2001/stream kommttoo many requestszurück. Ich frage mich, wie man den Stream neu starten soll.Als Alternative nutze ich SuperNote und bin sehr zufrieden damit. Screen Mirroring ist möglich, was sehr praktisch ist, um während Meetings schnell Diagramme zu zeichnen.
Der Nachteil ist, dass SuperNote einen kleinen Webserver startet und man mit Firefox darauf zugreift; Laptop und SuperNote müssen also im selben Netzwerk sein. Im Homeoffice ist das kein Problem, könnte aber durch Unternehmensrichtlinien blockiert werden.
Ob RM2 oder SuperNote: Für Leute, die Ideen gern mit Stift und Papier festhalten, sind es großartige Werkzeuge; es fühlt sich ziemlich anders an als Apps oder Textdokumente, und man kann auch in Notizen herumkritzeln.
[0]: https://supernote.com/
Beim Kauf muss man allerdings GPL-Verstöße in Kauf nehmen. Obwohl es vollständig auf Android basiert, wird der Betriebssystem-Quellcode nicht veröffentlicht.
Ich mache mir Sorgen, am Ende ein Gerät zu kaufen, das in den nächsten 3 bis 5 Jahren keine Funktionsupdates oder zumindest Sicherheitsupdates mehr bekommt.
Das Rendering auf dem HTML-Canvas kann schneller werden, wenn man wie hier beschrieben Typed Arrays verwendet: https://hacks.mozilla.org/2011/12/faster-canvas-pixel-manipu...
Genau solche Beiträge möchte ich hier sehen. Mir gefiel, wie ChatGPT dabei geholfen hat, ein Problem in einem Bereich zu verstehen und zu lösen, den es nicht gut kennt, und ich konnte mich mit der Formulierung „ich war der Entwickler und ChatGPT war der Coder“ identifizieren.
Auch die Aussage, dass Einfachheit in Wirklichkeit komplex ist, stimmt.
JPEG wurde vermutlich gewählt, weil es sich leicht in MJPEG umwandeln lässt und man die Decodierung quasi kostenlos bekommt, wenn man sie an eine unterstützende Seite weiterreicht. Allerdings könnte genau das die reMarkable-CPU stark belasten.
JPEG ist eher für Fotos geeignet, der reMarkable-Bildschirm ähnelt aber eher Illustrationen und ist zudem in Schwarzweiß-/Graustufen gehalten. Andere gängige Bildformate wie PNG oder auch nur einfache RLE-Komprimierung dürften die CPU weniger belasten.
Und das proprietäre Dateiformat basiert nicht auf Bitmaps, sondern auf Stifteingaben.
Beim Profiling zeigte sich, dass der Großteil der CPU-Zeit für die Datenübertragung über die Leitung draufging, daher habe ich Kompression eingebaut. Jetzt ist die CPU-Auslastung niedrig.
Ich frage mich, ob ihr einen Ansatz geprüft habt, bei dem nur die geänderten Bereiche des Framebuffers übertragen werden. Damit ließe sich die Datenrate deutlich senken: https://github.com/pl-semiotics/mxc_epdc_fb_damage
Das rM-VNC-Projekt macht es ebenfalls so, aber mir gefällt die User Experience dieser App besser, weil auf Client-Seite keine Software nötig ist.
Wirklich cool, und ich würde das ReMarkable 2 gern mögen, aber wegen der Haltung, dass es ein unsicheres Gerät ist, fällt mir das nicht leicht: https://support.remarkable.com/s/article/Does-reMarkable-off...
Es geht nicht um bekannte Software-Schwachstellen, an die man typischerweise denkt, wenn von einem unsicheren, vernetzten Gerät die Rede ist.
Ich würde gern mehr über die Stelle lesen: „Anfangs versuchte ich, den Client nach WASM zu kompilieren. Das sah vielversprechend aus, weil ich meine Go-Entwicklungserfahrung nutzen konnte, aber ich stieß auf mehrere Einschränkungen, die erhebliche Anpassungen erfordert hätten.“
Selbst wenn man einen MJPEG-Stream erzeugen würde, bliebe die Frage, wie man ihn anzeigt. Ich dachte an einen Canvas-Ansatz, aber ohne große Kopien zwischen WASM und JS war der Zugriff auf das Canvas-Backend schwierig, und die Größe lag bei 2,5 MB.
Am Ende sah es so aus, als müsste man bei einer Abhängigkeit von WASM viele grundlegende Bildoperationen, die in JS nativ zugänglich sind, etwa Bildrotation, selbst implementieren.
Ich frage mich, wie sich dieses Tool vom eingebauten Streaming, also der Screen-Sharing-Funktion, unterscheidet.
https://support.remarkable.com/s/article/Screen-Share
Die im Artikel beschriebene Lösung scheint auch unter Linux zu funktionieren, solange man nur einen ausreichend leistungsfähigen Browser hat.
Ich mag reMarkable, aber ich wünschte, sie würden sich auf solche Streaming-Funktionen konzentrieren statt auf ein Abo, für das ich auch in Zukunft nicht zu zahlen gedenke.