はじめに
NTTビジネスソリューションズの辻本です。
ブラウザだけでシミュレータ上のロボットを遠隔操作できるデモの事例を紹介します。Azure VM の headless(GUI なし)環境で動かすために行った公式クックブック(実装ガイド)からの構成変更や、複数カメラの同時配信といった独自の拡張を中心に解説します。
なお、本記事中で扱うサービス(SkyWayなど)に関する記載は2026年2月時点の情報に基づきます。また、動作結果は筆者の実行環境・設定に依存し、記事内で掲載しているコードは、理解しやすさを優先した簡略版(抜粋)です。
背景・目的
GitHubのSkyWay 関連のリポジトリを眺めていたら skyway_ros_bridge を見つけて、「なんだこれ?」と思いました。SkyWay はビデオ通話やチャット向けのプラットフォームと思っていましたが、それがロボットの世界のROSとブリッジ・・・?想定外で驚きました。SkyWayのREADME をたどると公式クックブックがあることを知り、「メッセンジャー向けの WebRTC 技術がロボットで何に使えるのか?」――その疑問から、クックブックをベースに Azure VM headless 環境での構築やオリジナルワールドの作成など、独自の要素を加えたデモを作りました。
対象読者
- ROS 2 でロボットのリモート監視・操作に興味がある方
- SkyWay(WebRTC)× ロボットの組み合わせに興味がある方
- GUI なしの headless 環境で Gazebo シミュレータを動かしたい方
前提条件
| 項目 | 内容 |
|---|---|
| VM | Azure Standard_D4s_v3 (4 vCPU / 16 GiB RAM / GPU なし) |
| OS | Ubuntu 24.04 LTS |
| SkyWay アカウント | SkyWay 公式サイトでアカウント作成済み (詳細はユーザーガイドの「はじめに」を参照) |
完成イメージ

ブラウザ上に 2 つのカメラ映像(ロボット視点 + 俯瞰)が表示され、矢印キーでロボットを操作できます。 ロボット視点のカメラでは、周りの光景が見えます。俯瞰カメラは上空から世界を見渡すイメージです。 遠隔操作ロボットが右下から動いてくる小さい点が見えます。
SkyWay とは
SkyWay は NTT ドコモビジネスが提供する WebRTC プラットフォームです。WebRTC に必要なシグナリングや TURN(中継)/STUN(経路確認)サーバーをマネージドで提供しており、インフラを自前で構築する必要がありません。また、ROS 2 と連携するため開発ドキュメントとして公式クックブックと skyway_ros_bridge(ROS 2 と SkyWay を接続するブリッジノード)が用意されています。
今回のデモで SkyWay を採用した理由を以下にまとめます。
| 項目 | 説明 |
|---|---|
| NAT 越え | TURN/STUN サーバーが利用できるため、VM にポートを開放する必要がない |
| ブラウザ完結 | 専用クライアント不要。Chrome があればどこからでもアクセスできる |
| 映像 + 操作が同一基盤 | VideoStream(カメラ映像)と DataStream(WebRTC の DataChannel に相当)を 1 つの Room でまとめて扱える |
| マルチユーザー対応 | Room 機能で複数人が同時に映像を視聴できる |
今回の Azure VM ではポート開放を一切していませんが、SkyWay 経由で映像も操作コマンドも問題なく通りました。
デモの全体像
アーキテクチャ
Docker Compose で 4 つのサービスを構成しています。

データフロー
映像と操作コマンドが SkyWay を介して双方向に流れます。

技術スタック
| カテゴリ | 技術 |
|---|---|
| VM | Azure VM / Ubuntu 24.04 |
| ロボット OS | ROS 2 Jazzy |
| シミュレータ | Gazebo Harmonic(headless) |
| WebRTC | SkyWay(Linux SDK / JavaScript SDK) |
| ROS 2 ↔ SkyWay ブリッジ | skyway_ros_bridge |
| コンテナ | Docker Compose(4 サービス) |
クックブックから headless 環境への構成変更
公式クックブックは GUI ありの環境を前提としています。今回は Docker Compose による headless 環境で構築したため、いくつかの構成変更が必要でした。以下、クックブックとの主な構成差分と、それぞれの対応方法を紹介します。
| 項目 | クックブック | 本デモ |
|---|---|---|
| コンテナ構成 | 1 コンテナ内で全て実行 | 4 コンテナに分離 |
| Gazebo | GUI あり | headless(-s フラグ) |
| カメラモデル | デフォルト(wideanglecamera) | 軽量版(camera, 160x120, 1 Hz) |
ipc: host |
不要(単一コンテナ) | 必須(Fast DDS 共有メモリ) |
| DataStream 購読 | ターミナルから手動でサービスコール | シェルスクリプトで自動購読 |
| 起動方法 | docker compose exec ros bash で中に入り手動実行 |
docker compose up で全自動起動 |
1. カメラセンサーの軽量化
今回使用した Azure VM には GPU が搭載されていないため、Gazebo のレンダリングは Ogre2 のソフトウェアレンダリングで処理されます。起動してみると Real-Time Factor(RTF)が 0.0015(実時間の 0.15%)まで低下し、シミュレーションがほぼ停止しました。RTF 0.0015 を見たときは、カクカクとさえ動かず、GPU なしでのコンテナ化は難しいと感じました。
原因は TurtleBot3 デフォルトモデルの wideanglecamera センサーにありました。このセンサーは内部でキューブマップ(6 面レンダリング)を行うため、GPU なしの環境では処理が追いつきません。解像度だけを下げても RTF はほとんど改善しなかったため、カメラタイプ自体を camera(通常レンダリング)に変更しました。
軽量版のモデル SDF を作成し volume mount で差し替えました。SDF(Simulation Description Format)は Gazebo のロボットやワールドを記述する XML 形式です。
| パラメータ | デフォルト | 軽量版 |
|---|---|---|
| カメラタイプ | wideanglecamera(キューブマップ) |
camera(通常レンダリング) |
| 解像度 | 320x240 | 160x120 |
| フレームレート | 30 Hz | 1 Hz |
| LiDAR | 5 Hz | 1 Hz |
| IMU | 200 Hz | 50 Hz |
SDF での変更箇所は主にカメラセンサーの定義部分です。
<!-- 変更前:wideanglecamera(キューブマップ) --> <sensor name="camera" type="wideanglecamera"> <camera> <image><width>320</width><height>240</height></image> </camera> <update_rate>30</update_rate> </sensor> <!-- 変更後:camera(通常レンダリング) --> <sensor name="camera" type="camera"> <camera> <image><width>160</width><height>120</height></image> </camera> <update_rate>1</update_rate> </sensor>
この変更で RTF が 0.0015 → 0.978 に改善しました。GPU なしの headless 環境で Gazebo を動かす場合、VM スペックを上げる前に、まずセンサーの負荷を見直すのがお勧めです。
2. コンテナ間の DDS 通信設定
クックブックでは 1 つの Docker コンテナ内で全てを実行するため、DDS の通信設定を意識する必要がありません。今回は 4 コンテナに分離したことで、追加の設定が必要になりました。
ROS 2 Jazzy のデフォルト DDS 実装は Fast DDS です。Fast DDS は同一ホスト内の通信に共有メモリ(/dev/shm)を使います。Docker コンテナはデフォルトで /dev/shm が隔離されるため、DDS Discovery(UDP マルチキャスト)は成功しますが、データ転送(共有メモリ)が機能しない状態になります。
docker-compose.yml の全コンテナに ipc: host を追加しました。
services: gazebo: network_mode: host ipc: host # Fast DDS 共有メモリ通信に必要
ipc: host によりコンテナがホストの /dev/shm を共有し、Fast DDS の共有メモリ通信が正常に動作します。複数コンテナで ROS 2 を動かす場合に必要な設定です。
3. ICE 接続の設定
ブラウザ側で SkyWay Room を作成する際、FindOrCreate() に type: 'p2p' を指定すると ICE candidate が生成されず、映像が届きません。
// NG: type を指定すると ICE candidate が生成されない room = await SkyWayRoom.FindOrCreate(context, { type: 'p2p', name: roomName, }); // OK: type を指定しない(クックブックと同じ) room = await SkyWayRoom.FindOrCreate(context, { name: roomName, });
クックブックのコードでは type を指定していないため、クックブック通りに実装すれば問題ありません。独自に Room の type を指定する場合は注意が必要です。
複数カメラの同時配信
クックブックでは skyway_ros_bridge を 1 つ起動して 1 カメラを配信する構成です。今回は俯瞰カメラを追加し、2 つの映像を同時配信する拡張を行いました。
対応方法はシンプルで、skyway_ros_bridge を名前空間を変えてもう 1 つ起動するだけです。ビデオ通話に 2 人目が参加してカメラをオンにするのと同じ要領で、skyway_ros_bridge のソースコード改修は不要です。
# docker-compose.yml(抜粋) skyway-bridge-robot: environment: - CAMERA_TOPIC=/camera/image_raw command: ros2 run skyway_ros_bridge skyway skyway-bridge-overhead: environment: - CAMERA_TOPIC=/overhead/image_raw - MEMBER_NAME=ros_overhead command: ros2 run skyway_ros_bridge skyway --ros-args -r __ns:=/overhead
2 つのノードが同じ SkyWay Room に異なるメンバー名で参加し、それぞれのカメラ映像を VideoStream として配信します。ブラウザ側では配信元のメンバー名でどちらのカメラかを判別します。
async function subscribeIfVideo(publication) { if (publication.contentType !== 'video') return; const { stream } = await me.subscribe(publication.id); const name = publication.publisher.name || ''; if (name.includes('overhead')) { stream.attach(overheadVideo); // 俯瞰カメラ } else { stream.attach(robotVideo); // ロボットカメラ } }
操作コマンドの送受信
ブラウザからの操作コマンドは SkyWay の DataStream で送信します。DataStream は文字列しか送れないため、ROS 2 の geometry_msgs/Twist(前進速度と回転速度をフィールドに持つメッセージ型)を直接扱えません。そこで Gazebo の TriggeredPublisher プラグインで、"forward" のような文字列を受け取ったら対応する Twist の値に変換しています。
<plugin filename="gz-sim-triggered-publisher-system" name="gz::sim::systems::TriggeredPublisher"> <input type="gz.msgs.StringMsg" topic="/cmd_vel_string"> <match field="data">"forward"</match> </input> <output type="gz.msgs.Twist" topic="/cmd_vel"> linear: {x: 0.5} angular: {z: 0.0} </output> </plugin>
NTT ワールド
俯瞰カメラを追加したことで、上から見たときに面白いワールドを作りたいと思いました。デモ用のオリジナル Gazebo ワールドとして、「N」「T」「T」の形にボックスを配置した「NTT ワールド」を SDF で作成しました。なお、最初に生成した SDF では「N」の斜め線が水平になり「H」に見えてしまうアクシデントもありましたが、調整して「N」に近づけました。


俯瞰カメラはワールド上空 15m に固定し、真下を向いています。そのため「NTT」の文字配置を一望できます。一方、ロボットカメラは地上 0.16m(ロボット本体の高さ)にあり、目の前の壁しか映りません。ロボット視点だと色付きの壁が並ぶ迷路のように見えていますが、自分の位置が移動によって変化することが分かるように壁の色替えをしています。俯瞰カメラと組み合わせることで、ロボットの現在位置を把握しながら操作する「監視 + 操作」の構成にしました。
おわりに
この記事では、SkyWay の公式クックブックをベースに、Azure VM headless 環境でシミュレータ上のロボットをブラウザから操作するデモを構築した事例を紹介しました。
headless 環境への移行で最もインパクトが大きかったのはカメラセンサーの軽量化で、RTF が 0.0015 から 0.978 に大きく改善しました。ipc: host の追加や ICE 接続の設定など、コンテナ分離に伴う落とし穴もありましたが、いずれも原因が分かれば数行の修正で解決できるものでした。クックブックの存在が大きく、ゼロからの構築と比べると少ない労力で動くデモを作ることができました。
SkyWay の NAT 越えとブラウザ完結という特徴は、ロボットのリモート監視・操作の用途でも有効です。今後は Nav2(Navigation2: ROS 2 の自律移動フレームワーク)などの自律移動と組み合わせて、ブラウザから走行状況を監視するような構成に発展させたいと考えています。
参考リンク
- SkyWay 公式サイト
- SkyWay × ROS 2 クックブック
- skyway_ros_bridge(GitHub)
- SkyWay JS SDK ドキュメント
- ROS 2 Jazzy ドキュメント
- Gazebo Harmonic ドキュメント
- TurtleBot3 Simulations(GitHub)
執筆者
辻本傑(NTTビジネスソリューションズ株式会社 バリューデザイン部 システム開発部門) コミュニケーションサービスの開発・運用に携わっています。C#やクラウドが好きで、最近は生成AIの活用にも関心があります。 認定スクラムマスター(CSM)
商標
- SkyWay は NTT ドコモビジネス株式会社が提供するサービスです。
- ROS 2 および Gazebo は、Open Robotics の商標または登録商標です。
- Microsoft Azure は、米国 Microsoft Corporation の商標または登録商標です。
- Docker は、Docker, Inc. の商標または登録商標です。
- Ubuntu は、Canonical Ltd. の商標または登録商標です。
- TurtleBot3 は、ROBOTIS Co., Ltd. の商標です。
- Google Chrome は、Google LLC の商標です。
- その他、本文中に記載されている会社名・製品名・サービス名等は、各社の商標または登録商標である場合があります。