- Helix は、クラウド上で自律的に動作するコーディングエージェントの画面をユーザーに見せる AIプラットフォーム であり、安定したリモート画面転送が中核となる
- 企業ネットワークの UDP遮断とファイアウォール制約 により WebRTC ベースのストリーミングが失敗したため、チームは WebSocketベースの H.264 パイプライン を構築したが、不安定な Wi-Fi 環境では遅延が深刻に発生した
- 複雑なエンコード・デコード構造の代わりに、単純に JPEGスクリーンショットを HTTP で定期的に送信 する方式のほうが、はるかに安定していて効率的であることを発見した
- この方式は 帯域幅使用量が少なく、破損フレームの復旧が不要で、ネットワーク品質に応じて 自動的に画質とフレームレートを調整 する
- 結果として Helix は、良好な接続では H.264、悪い接続では JPEGポーリングへ切り替えるハイブリッド構成 を採用し、シンプルだが実用的なリモートストリーミングシステムを完成させた
Helixのストリーミング問題と制約
- Helix は、クラウドサンドボックスで動作する AIコーディングエージェントの画面をリアルタイムで共有 しなければならないプラットフォーム
- ユーザーは、まるでリモートデスクトップのように AI がコードを書く過程を視聴する
- 当初は WebRTC を使用していたが、企業ネットワークでの UDP 遮断 によって接続に失敗した
- TURN サーバー、STUN/ICE、カスタムポートなどはすべてファイアウォールポリシーによって遮断された
- そのため、HTTPS(443番ポート)のみを使う WebSocket ベースの H.264 ストリーミングパイプライン を自前で実装した
- GStreamer + VA-API によるハードウェアエンコード、WebCodecs によるブラウザーデコード
- 60fps、40Mbps、100ms 未満の遅延を達成
ネットワーク遅延と性能低下
- カフェなどの不安定なネットワーク環境で、映像が止まったり数十秒遅延 したりする問題が発生
- TCP ベースの WebSocket はパケットロス時にフレームが順番に遅延し、リアルタイム性が崩壊 する
- ビットレートを下げても遅延は解決せず、画質だけが低下した
- キーフレームのみを送る方式も試したが、Moonlight プロトコル が P フレームを要求するため失敗した
JPEGスクリーンショット方式の発見
- デバッグ中に
/screenshot?format=jpeg&quality=70 エンドポイントを呼び出すと、即座に鮮明な画像が読み込まれた
- 150KB サイズの JPEG 1枚が遅延なく表示された
- 単純に HTTP リクエストを繰り返してスクリーンショットを更新すると、5fps 程度の滑らかな画面更新 が可能になった
- 最終的に複雑なビデオパイプラインの代わりに、定期的な JPEG リクエスト(fetch ループ) 方式へ切り替えた
JPEG方式の利点
- H.264 との主な比較項目
- 帯域幅: H.264 は 40Mbps 固定、JPEG は 100〜500Kbps で変動
- 状態管理: H.264 は状態依存、JPEG は 完全に独立したフレーム
- 復旧性: H.264 はキーフレーム待ちが必要、JPEG は次のフレームで即座に復旧
- 複雑さ: H.264 は数か月の開発、JPEG は
fetch() ループ数行で実装
- ネットワーク品質が悪いほど、単純な JPEG 方式のほうが より安定していて効率的 だった
ハイブリッド切り替え構成
- Helix は 2つの方式を RTT(往復遅延時間) を基準に自動切り替えする
- RTT < 150ms → H.264 ストリーミング
- RTT > 150ms → JPEG ポーリング
- 接続回復時はユーザーがクリックして再切り替え
- 入力イベント(キーボード・マウス)は WebSocket で継続して送信されるため、対話性を維持 できる
- サーバーは
{"set_video_enabled": false} メッセージでビデオ送信を停止し、スクリーンショットモードへ切り替える
切り替えの不安定化(oscillation)問題と解決
- 送信停止後に WebSocket トラフィックが減ると遅延が下がり、自動的に再びビデオモードへ戻る無限ループ が発生
- 解決策: スクリーンショットモードに入った後は、ユーザーがクリックするまで固定維持 する
- UI に「帯域幅節約のためビデオが一時停止されました」というメッセージを表示
JPEG対応問題とビルド過程
- Wayland 用スクリーンショットツール grim は Ubuntu の標準パッケージで JPEG サポートが無効化されている
grim -t jpeg 実行時に “jpeg support disabled” エラーが発生
- これを解決するため、Dockerfile で libjpeg-turbo8-dev を含めて grim をソースから直接ビルド した
最終アーキテクチャ
- 良好な接続: 60fps H.264、ハードウェアアクセラレーション
- 悪い接続: 2〜10fps JPEG ポーリング、完全な信頼性
- スクリーンショット品質は送信時間に応じて自動調整
- 500ms 超なら品質 -10%、300ms 未満なら +5%、最低 2fps を維持
主な教訓
- シンプルな解決策は複雑なシステムより優れている — 3か月の H.264 開発より、2時間の JPEG ハックのほうが実用的だった
- 優雅な性能劣化(graceful degradation) がユーザー体験の鍵
- WebSocket は入力転送に最適 であり、映像転送に必須ではない
- Ubuntu パッケージには機能欠落の可能性 がある — 必要なら自前でビルド
- 最適化の前に計測が必須 — 複雑なストリーミングが唯一の解決策とは限らない
オープンソース公開
- Helix はオープンソースとして提供されており、主要実装は次のとおり
api/cmd/screenshot-server/main.go — スクリーンショットサーバー
MoonlightStreamViewer.tsx — 適応型クライアントロジック
websocket-stream.ts — ビデオ切り替え制御
- Helix は、実環境でも動作する AI インフラ を目標に開発が進められている
1件のコメント
Hacker News の意見
ネットワークが悪いときに JPEG がうまく縮むのは、UDP のせいではなく TCP の実装方式によるものだという指摘
JPEG はバッファリングや輻輳制御の問題を解決しない。おそらくフレーム送信を最小化する構造で実装した可能性が高い
h.264 は JPEG より符号化効率が高い。同じサイズなら h.264 の IDR フレームのほうがより良い品質を出せる
根本的な問題は帯域幅推定の欠如にある。TCP 環境でも初期帯域幅プローブや送信遅延の検知を通じてビットレートを調整できる
可能なら WebRTC を使うほうがよく、ファイアウォール回避用には WebSocket が向いている
文章の形式上の問題や LLM っぽさを脇に置いても、内容全体に間違いが多い
10Mbps なら静的な画面には十分なはずで、問題はエンコード設定が悪いかエンコーダ品質が低いことだ
「キーフレームだけ送る」という発想は非効率で、代わりに短い keyframe 間隔を設定すればよい
結局のところ問題は、TCP の単一接続にストリーム全体を押し込む構造にある。こうした状況向けの DASH のような解決策はすでに存在する
VNC が 1998 年からやってきた方式を参考にするとよさそうだ
クライアントプル型のモデルを維持しつつ、フレームバッファをタイル単位に分割し、変更された部分だけを送る構造だ
静的なコーディング画面では帯域幅を大きく減らせる。スクロール検知も加えればさらに効率化できるだろう
以前ビデオエンコーディングを扱っていたが、40Mbps はBlu-ray 級の品質だ
単純なテキストのストリーミングには過剰だ。Claude と話した結果、30FPS、GOP 2 秒、平均 1Mbps 程度で十分という結論になった
最悪の場合でも 1.2Mbps あれば十分に安定した品質を維持できる
この文章の核心的な問題は、h.264 の最低帯域幅を高く設定しすぎたことだ
H.264 は JPEG よりはるかに効率的だ。1Mbps から始めて調整すべきだった
キーフレームだけを使うのはむしろ非効率だ
私ならまったく別のアプローチを取る
10Mbps は過剰で、YouTube のコーディング動画は 1080p でも0.6Mbps程度だ。それでも十分鮮明だ
むしろ 1fps に落とすか keyframe 間隔を調整するほうがよいと思う
ブラウザでリアルタイム動画をストリーミングするのは本当に骨が折れる
JPEG スクリーンショットがうまく機能するなら、そのままにしておくのがよい
gstreamer や Moonlight のようなスタックは、バックプレッシャーとエラー伝播を理解していないとデバッグが地獄になる
NVIDIA Video Codec SDK + WebSocket + MediaSource Extensions の組み合わせが現実的な代案だ
ただし、この文章が LLM 生成物だとしたら、著者にはそうした内部構造を理解する意思がないかもしれない
昔、5 秒ごとにスクリーンショットを撮るプログラムを使っていたが、ハードディスクがすぐいっぱいになった
画像の大半が同じだと気づき、変更された部分だけ保存するアルゴリズムを考え始めたが、
結局、自分がビデオ圧縮を再発明しているだけだと気づいた
ffmpeg の 1 行で解決し、保存容量を 98% 節約できた
LLM がタイピングする映像を 40Mbps でストリーミングするのは異常なほど過剰な帯域幅だ
HN で良い回答を得る唯一の方法は、間違った文章を投稿することだ
間違ってはいても興味深い記事こそ、議論を引き出す完璧なバランスを持った例だと思う