3 ポイント 投稿者 GN⁺ 2023-08-21 | 1件のコメント | WhatsAppで共有
  • ビデオ会議中に 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 を読み取り、届いたチャンクをキャンバスデータへ反映する

転送量の最適化

  • 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 06 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件のコメント

 
GN⁺ 2023-08-21
Hacker Newsのコメント
  • reMarkableタブレットの画面をノートPCへストリーミングしていた2021年のツールを再び磨き直して公開し、新しい記事ではアーキテクチャ、構成要素、ユーザー体験を改善した過程を深く扱っている
    プロダクトマネージャーの視点からユーザーがどう感じるかを見ながら有効化プロセスを単純化し、ローカルサービスなしで動作するようにし、ネットワーク使用量も最適化した

    • すばらしいプロジェクトに見える。Ctrl-Cの後に./goMarkableStreamで再起動するとある程度動いたが、依然としてwaiting for reMarkable screenが頻繁に表示され、サービスが不安定
      reMarkable2を3.5.2.1807に上げてからインストールしたが、ノートPC、シート、本の上に描いても反応がなく、read /dev/input/event2: file already closedread /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が返ってくる。ストリームをどう再起動すればいいのか知りたい
    • USB接続でも実装できるのか気になる
  • 代替としてSuperNoteを非常に満足して使っている。画面ミラーリングができるので、会議中に図を素早く描くのにとてもよい
    欠点は、SuperNoteが小さなWebサーバーを立ててFirefoxで接続する方式なので、ノートPCとSuperNoteが同じネットワーク上にある必要があること。ホームオフィスでは問題ないが、会社のポリシー上ブロックされる可能性がある
    RM2であれSuperNoteであれ、ペンと紙でアイデアを書くのが好きな人にはすばらしいツールで、アプリやテキスト文書とは感覚がかなり違い、ノートに落書きもできる
    [0]: https://supernote.com/

    • Onyx Boox Noteもうまく動作し、5年以上経った後でもアップデートを出している
      ただし購入する際はGPL違反を受け入れる必要がある。完全にAndroidベースなのに、OSのソースを公開していない
    • 電子書籍リーダー兼ノート用の電子インクタブレットを探していたので、推薦は助かる。Remarkable 2とBooxの間で悩んでいて、SuperNoteのソフトウェアアップデート体験がどうなのか気になる
      今後3〜5年の間、機能アップデートや少なくともセキュリティアップデートすら受けられないデバイスを買うことになるのではと心配している
    • 記憶では、SuperNoteは同梱して配布しているソフトウェアのGPLを順守していなかったが、立場が変わったのか気になる
    • まだ「同じネットワーク上にある必要がある」問題は経験していないが、ネイティブのNgrok機能を入れるのは数分で済む簡単な作業に見える。そうすればインターネット経由でストリーミングできる
    • SuperNoteの書き心地がRM2と比べてどうなのか気になる
  • HTMLキャンバスのレンダリングは、ここで説明されているように型付き配列を使うとさらに速くできるかもしれない: https://hacks.mozilla.org/2011/12/faster-canvas-pixel-manipu...

    • ありがとう、見てみる
  • こういう記事こそ、ここで見たいコンテンツだ。ChatGPTがあまり知らない領域の問題を学び、解決するのにどう役立ったかがよかったし、「自分が開発者で、ChatGPTがコーダーだった」という表現に共感した
    シンプルさは実際には複雑だ、という言葉もその通り

  • JPEGを選んだのは、MJPEGに変えやすく、対応している側に渡せばデコードをほぼ無料のように処理できるからだったのだと思う。ただしそれがreMarkable CPUに大きな負担をかける要因かもしれない
    JPEGは写真により適しているが、reMarkableの画面はイラストに近く、しかも白黒系だ。PNGのような別の一般的な画像形式や、単純なRLE圧縮を使うだけでもCPU負荷はもっと低くなりそう

    • 厳密に言うとreMarkableは単色ではなくグレースケールで、記憶では16段階のグレーに対応している。連携アプリでは、ペンは青・赤、蛍光ペンは黄・緑のように見えるカラーインクもある
      そして独自ファイル形式はビットマップではなく、手書き入力ベースだ
    • 最初にJPEGを選んだ理由はその通り。ただ、そのためにクライアント/サーバー構成を採用し、エンコードはタブレットではなくクライアントであるノートPC側で実行した
      プロファイリングしてみると、CPUの大半は回線経由のデータ転送に使われていたので、圧縮を入れた。今はCPU使用率は低い
  • フレームバッファの変更された領域だけを送信する方式は検討してみたのか気になる。そうすればデータ転送率を大きく下げられる: https://github.com/pl-semiotics/mxc_epdc_fb_damage
    rM VNCプロジェクトもそうしているが、クライアント側ソフトウェアが不要なこのアプリのユーザー体験のほうが気に入っている

    • その方式はデバイス上である程度の解析が必要になるのが問題で、コードはできるだけ低侵襲にしておきたい。安価に処理する方法があるか見てみる
  • 本当に素晴らしいし、ReMarkable 2を好きになりたいのだが、安全ではないデバイスという立場のせいで簡単ではない: https://support.remarkable.com/s/article/Does-reMarkable-off...

    • リンク先の内容は、このデバイスが置き換えようとしている紙と同程度の物理的セキュリティしか持たない、という意味。つまり、誰かがデバイスにアクセスすれば読めてしまう
      ネットワーク接続された安全でないデバイスと言うときによく想像される、既知のソフトウェア脆弱性の話ではない
    • 非公式には gocryptfs ベースのホームディレクトリ暗号化が可能: https://github.com/RedTeamPentesting/remarkable-encryption
    • ソフトウェアも現時点ではかなり制限されている。公式にマーケットプレイスや拡張機能をデバイスで使えるようにしていない点が惜しい
    • フルディスク暗号化を提供している電子書籍リーダーはあるのだろうか?
  • 「最初はクライアントを WASM にコンパイルしようとした。Go の開発経験を活かせるので有望に見えたが、かなりの修正が必要な複数の制約にぶつかった」という部分をもっと読んでみたい

    • 核心的な問題は gRPC ライブラリで、現在のサポートは非常に限られていた。また Go での JPEG 圧縮は遅く、CPU を多く使う
      たとえ MJPEG ストリームを生成できたとしても、それをどう表示するかも問題だった。Canvas 方式を考えたが、WASM と JS の間で大きなコピーを行わずに Canvas バックエンドへアクセスするのは難しく、サイズも 2.5MB だった
      結局、WASM に依存すると、画像の回転のように JS なら標準でアクセスできる画像の基本操作を多数自前で実装する必要がありそうだった
  • このツールが内蔵ストリーミング、つまり画面共有機能とどう違うのか気になる

    • 内蔵機能を使うにはデスクトップアプリをインストールする必要があり、知る限り Linux 版はない
      https://support.remarkable.com/s/article/Screen-Share
      記事の解決策は、十分な機能を持つブラウザさえあれば Linux でも動作するように見える
    • 最大の違いは、今ではクライアントのインストールが不要という点。ブラウザに reMarkable のアドレスを入力するだけで内容を見られる
    • すでにこの機能はできると思っていた。画面共有は十分に良く、ライブストリーミングにも使っている
  • reMarkable は好きだが、今後もお金を払うつもりのないサブスクリプションより、こうしたストリーミング機能に注力してほしい