reMarkable Streaming Tool v2: リモートワークの効率向上
(blog.owulveryck.info)- ビデオ会議中に reMarkable 2 のスケッチを共有していたツールが、ノートPC上のローカルサービスなしで開けるようになり、発表者はブラウザだけで 即時ストリーミング を開始できるようになった
- 新しい構成では、reMarkable 内部の HTTP サーバー とブラウザの JavaScript クライアントが生画像を受け取り、
canvasに描画する方式へと単純化された - WebSocket の代替案は動作したものの、iOS の問題とサーバーのオーバーヘッドが残ったため、固定解像度の画像を
http.ResponseWriterに継続して書き込み、fetchストリームで読み取る 生ストリーム 方式に整理された - 1872x1404 の生フレームは約 2.5MB のため、ファームウェア 3.3 以降は 16 色の値を uint4 にパックして 50% 削減し、RLE により平均で約 200KB の転送量 まで抑えた
/dev/input/event*の監視により入力がないときは新しいフレーム送信を止め、クライアント接続中でも CPU 使用率は 0 に落ち、筆記中は約 10% CPU で動作する
既存ツールが使いづらかった理由
- 2021 年に作成した reMarkable ストリーミングツールは、ビデオ会議中にスケッチを共有するために使われており、ブラウザタブだけを共有すればよいため、発表内容に集中しやすかった
- 既存実装は 3 つの構成要素に分かれていた
- サーバー: reMarkable デバイス上で動作し、現在の画面の生画像を公開する
- クライアント: ノートPC上でサーバーの生画像を取得し、ブラウザが閲覧できる形式に処理する
- レンダラー: ブラウザや VLC のように HTTP MJPEG ストリームを読み取り、画面に表示する
- デバイスの CPU 使用量を減らすため、サーバーはクライアント接続時のみ画像を抽出し、通信には gRPC が使われていた
- ノートPC側クライアントは画像を繰り返し取得し、JPEG にエンコードしたうえで MJPEG ストリーム を HTTP サービスとして提供していた
- 発表環境では、reMarkable のアドレス、クライアント実行権限、レンダラーが知る必要のあるクライアント IP などのネットワーク設定が負担になっていた
ブラウザだけで開ける新構成
- 新たな目標は、どのブラウザでも reMarkable のアドレスを入力するだけでストリームにアクセスできるようにすることだった
- 別途ノートPCクライアントをなくし、reMarkable のサーバーコンポーネント内に HTTP サーバー を組み込む構成へ変更された
- ブラウザで動かすクライアントは JavaScript または WASM の形が必要だった
- 当初は Go の経験を活かすため WASM コンパイルを検討したが、大幅な修正が必要になる制約のため断念した
- 第 2 バージョンのクライアントは最終的に JavaScript で書かれた
- JavaScript のコード片や説明を得る過程で ChatGPT を使ったが、望む解決策の方向性は自分で定めた
canvas レンダリング方式
- MJPEG ストリームから離れるため、ブラウザの画像操作の基本要素である
canvasを使用した - reMarkable から受け取った生画像を
Uint8Arrayとして読み取り、ImageDataの RGBA ピクセルデータに同じ値を R/G/B として入れ、アルファ値を 255 に設定して表示する - レスポンシブ表示、回転、着色の可能性に備え、固定サイズの
fixedCanvasを非表示のまま維持する - 表示用キャンバスには
drawImageで隠しキャンバスの内容をコピーする - ブラウザウィンドウのサイズが変わると、コンテナサイズと 1872/1404 の比率を基準に表示キャンバスの幅と高さを調整する
WebSocket を捨てて生ストリームへ移行
- gRPC は Web 開発で一般的な選択肢ではないため、最初の代替実装では通信とカプセル化の手段として WebSocket を使用した
- WebSocket メッセージには生画像を載せ、ブラウザクライアントはメッセージを受け取るたびにキャンバスを更新して、ストリーミングのように見せていた
- この方式では、サーバー側のメッセージ送信頻度を調整して reMarkable のメモリおよび CPU 負荷を管理できた
- しかし iOS で問題があり、サーバー側 WebSocket 実装のオーバーヘッドも制御しづらかった
- 最終構成ではカプセル化をやめ、固定された画像サイズを利用して 生画像 をネットワークへ直接送るようにした
- Go サーバーは
http.ResponseWriterに画像を繰り返しWriteする - ブラウザクライアントは
fetch('/stream')のReadableStreamを読み取り、届いたチャンクをキャンバスデータへ反映する
- Go サーバーは
転送量の最適化
- reMarkable 2 の生画像は解像度 1872x1404 で約 2.5MB あり、このデータをフレームごとに転送する必要がある
- ファームウェア 3.3 以降、reMarkable の 16 種類の色値は uint8 の代わりに uint4 配列 で表現できる
- Go と JavaScript にはネイティブな uint4 型がない
- 2 つのピクセル値を 1 つの uint8 バイトに保存する方法で回避した
- Go では 2 つの uint4 値を上位 4 ビットと下位 4 ビットにパックし、JavaScript ではそれを再びアンパックする
- この表現によりデータ量を 50% 削減できる
- 追加圧縮には Run Length Encoding (RLE) を使用した
- RLE は同じピクセル値が連続する回数と値を一緒に送る単純なアルゴリズムである
- 例
0 0 0 0 0 0 1 1 1 0 0 0 0は6 0 3 1 4 0と表現される
- カウント値は最大で 1872*1404 まで大きくなるため、uint64 のような型が必要になる可能性があり、場合によっては圧縮結果が元データより大きくなる危険もある
- これを避けるため、カウント長を 15 に制限し、カウントとピクセル値を 1 バイトに収めるバランスを選んだ
- RLE 実装は Go の
io.Writerのように動作して再利用可能であり、必要なら RLE を 2 回適用することもできるが、現時点では不要だった - パッキングと RLE の後、平均転送量は約 200KB 水準となった
変更があるときだけフレーム送信
- 最後の最適化は、画面が変わったときだけ新しいフレームを送る方式である
- 変更有無をチェックサムで計算すると CPU 負荷が大きくなる可能性がある
- reMarkable は Linux ベースのため、ペンやタッチ入力は
/dev/input/event*に渡される - goroutine がこの入力イベントを監視し、必要な場合にのみ画像を送信する
- イベントがなければ、クライアントが接続されている状態でも CPU 使用率は 0 に落ちる
- 筆記中の CPU 使用率は約 10% 程度である
ファームウェア変更と保守負担
- このアプリケーションはハックに基づいており、核心的な課題は画像取得インターフェースとクライアント/レンダラーを効果的に分離することにある
- 以前の実装では、protobuf 定義を通じてクライアントとサーバーが完全に分離されていた
- reMarkable 3.3 ファームウェアはツールを壊し、関連内容は GitHub の issue 36 にある
- 当時の修正はクライアントコンポーネントにのみ影響した
- ファームウェア 3.6 も GitHub の issue 58 によると破壊的変更をもたらす可能性がある
- この場合、より広範な修正が必要になるかもしれない
- ただし、クライアントがサーバーに統合された自己完結型構成のため、デバイス更新はより単純になる可能性がある
- アプリケーションとソースコードは github.com/owulveryck/goMarkableStream で提供されている
1件のコメント
Hacker Newsのコメント
reMarkableタブレットの画面をノートPCへストリーミングしていた2021年のツールを再び磨き直して公開し、新しい記事ではアーキテクチャ、構成要素、ユーザー体験を改善した過程を深く扱っている
プロダクトマネージャーの視点からユーザーがどう感じるかを見ながら有効化プロセスを単純化し、ローカルサービスなしで動作するようにし、ネットワーク使用量も最適化した
Ctrl-Cの後に./goMarkableStreamで再起動するとある程度動いたが、依然としてwaiting for reMarkable screenが頻繁に表示され、サービスが不安定reMarkable2を3.5.2.1807に上げてからインストールしたが、ノートPC、シート、本の上に描いても反応がなく、
read /dev/input/event2: file already closed、read /dev/input/event1: file already closedのようなログが時々見えるhttps://192.168.8.143:2001/とhttps://10.11.99.1:2001/はいずれもHTMLとキャンバスを返し、Chrome、Firefox、Braveでも試した
ストリームごとにブラウザ1つ、IP 1つの制限があるように見えるが、どのアドレスとブラウザを使っても
waiting for reMarkable screenが出ることがある。nohup ./goMarkableStream &の後でPuTTYを閉じ、クライアントを再起動したら、すべてのブラウザが同じ状態になり、https://10.11.99.1:2001/streamを見るとtoo many requestsが返ってくる。ストリームをどう再起動すればいいのか知りたい代替としてSuperNoteを非常に満足して使っている。画面ミラーリングができるので、会議中に図を素早く描くのにとてもよい
欠点は、SuperNoteが小さなWebサーバーを立ててFirefoxで接続する方式なので、ノートPCとSuperNoteが同じネットワーク上にある必要があること。ホームオフィスでは問題ないが、会社のポリシー上ブロックされる可能性がある
RM2であれSuperNoteであれ、ペンと紙でアイデアを書くのが好きな人にはすばらしいツールで、アプリやテキスト文書とは感覚がかなり違い、ノートに落書きもできる
[0]: https://supernote.com/
ただし購入する際はGPL違反を受け入れる必要がある。完全にAndroidベースなのに、OSのソースを公開していない
今後3〜5年の間、機能アップデートや少なくともセキュリティアップデートすら受けられないデバイスを買うことになるのではと心配している
HTMLキャンバスのレンダリングは、ここで説明されているように型付き配列を使うとさらに速くできるかもしれない: https://hacks.mozilla.org/2011/12/faster-canvas-pixel-manipu...
こういう記事こそ、ここで見たいコンテンツだ。ChatGPTがあまり知らない領域の問題を学び、解決するのにどう役立ったかがよかったし、「自分が開発者で、ChatGPTがコーダーだった」という表現に共感した
シンプルさは実際には複雑だ、という言葉もその通り
JPEGを選んだのは、MJPEGに変えやすく、対応している側に渡せばデコードをほぼ無料のように処理できるからだったのだと思う。ただしそれがreMarkable CPUに大きな負担をかける要因かもしれない
JPEGは写真により適しているが、reMarkableの画面はイラストに近く、しかも白黒系だ。PNGのような別の一般的な画像形式や、単純なRLE圧縮を使うだけでもCPU負荷はもっと低くなりそう
そして独自ファイル形式はビットマップではなく、手書き入力ベースだ
プロファイリングしてみると、CPUの大半は回線経由のデータ転送に使われていたので、圧縮を入れた。今はCPU使用率は低い
フレームバッファの変更された領域だけを送信する方式は検討してみたのか気になる。そうすればデータ転送率を大きく下げられる: https://github.com/pl-semiotics/mxc_epdc_fb_damage
rM VNCプロジェクトもそうしているが、クライアント側ソフトウェアが不要なこのアプリのユーザー体験のほうが気に入っている
本当に素晴らしいし、ReMarkable 2を好きになりたいのだが、安全ではないデバイスという立場のせいで簡単ではない: https://support.remarkable.com/s/article/Does-reMarkable-off...
ネットワーク接続された安全でないデバイスと言うときによく想像される、既知のソフトウェア脆弱性の話ではない
「最初はクライアントを WASM にコンパイルしようとした。Go の開発経験を活かせるので有望に見えたが、かなりの修正が必要な複数の制約にぶつかった」という部分をもっと読んでみたい
たとえ MJPEG ストリームを生成できたとしても、それをどう表示するかも問題だった。Canvas 方式を考えたが、WASM と JS の間で大きなコピーを行わずに Canvas バックエンドへアクセスするのは難しく、サイズも 2.5MB だった
結局、WASM に依存すると、画像の回転のように JS なら標準でアクセスできる画像の基本操作を多数自前で実装する必要がありそうだった
このツールが内蔵ストリーミング、つまり画面共有機能とどう違うのか気になる
https://support.remarkable.com/s/article/Screen-Share
記事の解決策は、十分な機能を持つブラウザさえあれば Linux でも動作するように見える
reMarkable は好きだが、今後もお金を払うつもりのないサブスクリプションより、こうしたストリーミング機能に注力してほしい