公式WebRTC plugin for Unityのサンプルを公開しました(Android向け)

前回 ↓ からだいぶ間あいてしまいましたが、続編です。

hammmm.hatenablog.com

前回WebRTCのソースからビルドしたプラグインですが、前回以降修正が入って、より最新に近いソースがビルド可能になりました。それを動かすためのサンプルコードを今回作成しました。一応ちゃんとUnityでWebRTCが動きます!ぱちぱちぱち。

プラグイン本体はこちらからダウンロードしてください。(以前より更新しました)

https://github.com/mhama/webrtc-dev-env/releases/tag/master_20180321_085858

プロジェクトはサンプルとして公開しました。

https://github.com/mhama/WebRtcUnityPluginSample

やれること

  • Socket.IOでの簡易なシグナリングを用いて、Unityアプリとブラウザ上のWebRTCクライアントとでリアルタイム動画を送信・受信できます。
  • 簡易なサーバープログラムを作成しました。サーバーホスティング等を用意しなくてもテストができます。

f:id:hammm:20180421020955j:plain

Unity用Socket.IOのライブラリはこちらを使わせてもらいました。

https://github.com/floatinghotpot/socket.io-unity

できないこと

  • シグナリング部分がサンプルレベルなので、エラー処理、状態管理などはザルです。
    まともにするには、サーバーコード、クライアントコードともに調整する必要があると思います。
  • ルームの管理を行っていません。
    同じサーバーに3人以上接続できますが、何が起こるかわかりません。
  • Unityエディタ上でWebRTC機能は動きません。WebRTCプラグインWindows/MacOSビルドができていないためです。今の所Androidビルド(実機)でのみ動作します。

サンプルの使い方

概要

サーバー(node.js)を起動し、ブラウザとUnityアプリの2つがサーバーに接続する形で通信(シグナリング)を行います。この通信によってWebRTCでの動画通信の下準備を整えることができ、準備ができたらP2Pで直接の通信が行われます。

f:id:hammm:20180421233903p:plain

動作環境

  • ターゲット: Androidスマホ(動作確認はGalaxy S6にて行いました)
  • 開発環境: WindowsPC。Macでの動作は未確認です。

通信料金についての注意

同じLANに両方の端末がつながっていれば、映像・音声のデータはLAN内でやりとりされる可能性が高いです。ただ、場合によってはインターネットを経由する可能性もあるので、従量課金制のネットワークをお使いの場合はご注意ください。

セットアップ

Unity 2017.3以降でプロジェクトを開いてください。 node.js をインストールしてください。

サーバー起動

Windowsの場合、serverディレクトリ内の server.bat を起動してください。

Mac, Linuxでは以下を実行してください。

cd server
npm install
node index.js

成功すると、以下のようなログになります。

listening on *:3000
forwarding to global domain...
ngrok server: https://12345abcde.ngrok.io

最後に https://12345abcde.ngrok.io のようなURL が出てきますが、これがサーバーのURLですので、控えておいてください。ウィンドウはそのまま開いておいてください。閉じるとサーバーが止まります。このURLは、外部のインターネットからアクセス可能です。

外部インターネットからのアクセスは、ngrokというサービスで実現しています。無料で利用している関係で、24時間経過するとURLが無効になるので、その場合は再度 server.bat を起動してください。

ブラウザでサンプルページを開く

インターネットが使えるLANに接続し、 (カメラがついた)PCのブラウザで上記のURLを開いてください。カメラの利用を許可してください。 Start CameraしてStart Peerしてください。 カメラのリアルタイム映像が1つ表示されます。

Unityプロジェクト

Androidプラットフォームで WebRTCSample/mainシーンを Build and Runしてください。

Andoidアプリ

インターネットが使えるLANに接続しておいてください。

アプリが起動したら、URL入力ボックスのところに先程のサーバーURLを指定してください。ただし、https:// はアクセスできないので、 http:// に変更してください。

ConnectしてからOffer with Camera または Offer without Cameraを選んでください。

  • Offer with Cameraの場合
    PCのカメラ映像がスマホに表示され、スマホのカメラ映像がPCに表示されたら成功です。

  • Offer without Cameraの場合
    PCのカメラ映像がスマホに表示されたら成功です。

f:id:hammm:20180421021703j:plain

切断などは未実装です!

組み込み方法

正直、もろもろ組み込める品質に達しているとは思えませんが、一応組み込めるはずです。

  • Empty Objectを作成して、WebRtcNativeCallSample コンポーネントを追加してください。
  • ローカル側テクスチャを貼りたいGameObjectにWebRtcVideoPlayerコンポーネントを追加してください。このGameObjectを上記WebRtcNativeCallSample のLocal Playerにセットしてください。
  • リモート側テクスチャを貼りたいGameObjectにWebRtcVideoPlayerコンポーネントを追加してください。このGameObjectを上記WebRtcNativeCallSample のRemote Playerにセットしてください。
  • 上記2つのGameObjectのマテリアルに、VideoMaterialをセットしてください。これをしないと、YUVをそのままRGBとして解釈した色になってしまいます。
  • WebRtcNativeCallSample コンポーネントのServer UrlパラメータにサーバーのURLをセットしてください。
  • 相手側クライアント(このサンプルではブラウザ)のStartCamera/Start Peerを呼んでください。
  • スクリプトなどから、WebRtcNativeCallSample コンポーネントのInitWebRTC()を呼んで、OfferWithCameraまたはOfferWithoutCamera()を呼ぶようにしてください。

うまくいけばテクスチャ上で動画の再生がはじまると思います。

解説

シグナリングって結局何なのか

WebRTCではシグナリングを実装する必要がある、というように言われますが、シグナリングって何なのか、モヤッとしてよくわからない時期があったので、そのあたりを明確にする感じで書いてみます。

WebRTCでは、見ず知らずのAというクライアントとBというクライアントが最終的に動画を交換するわけですが、そのAとBを「お見合い」させて、お互いの連絡先を交換させる、みたいな相談所みたいな役割をシグナリングという仕組みでやります。

とか、WebRTCはネゴシエーションの通信手段が決められていなくて、そこは自分で実装する必要があります。

・・・みたいな雑な説明はよく聞くと思うのですが、プログラマ的にはあまりピンとこないと思います。もうちょっと具体的にしましょう。

まず前提として、以下を把握しておく必要があります。

  • シグナリングとは、動画・音声通信の通信方法についてのネゴシエーション、つまり準備のことです。動画、音声自体の通信は、準備がうまくいったらWebRTCライブラリ同士が勝手にやります。
  • シグナリングでは、通信部分以外はほとんどWebRTCのライブラリがやります。プログラムで最初のトリガーを与えたら、あとはWebRTCライブラリが「データ用意したからこれ相手に送れ」のように指示してくる感じです。送るデータ内容の変更は通常は行いません。
  • シグナリングに必要な、「相手と相互にメッセージを交換」する、といういわばチャットのような通信機能を自分で実装する必要があります。たとえば今回のサンプルでは、Socket.IOを利用して実装しました。

流れでみてみましょう。JSのAPIの例です。ネイティブでは微妙に異なりますが、似たような流れです。ここで書かれているような流れを実装する必要があります。

  • A,B: 初期化する。
    • RTCPeerConnection をnewする。
    • 自分の動画・音声ストリームがあればPeerConnectionのインスタンスAddStream/AddTrack APIで追加しておく。
  • オファー(提案)
    • A: CreateOffer API(オファー作成)を呼ぶ。すると、コールバックで「オファーSDPができた(ので相手に送れ)」と言われる
    • ※SDPには、主に動画や音声の対応フォーマット情報、および後述のICEの情報などが入ります。
    • A: このオファーSDPSetLocalDescription API で「自分(A)側の設定データ」としてセットしておく
    • A: なんらかの通信手段でAからBにオファーSDPを送る
      • A ===> B
    • B: 受け取ったオファーSDPSetRemoteDescription API で「相手(A)側の設定データ」としてセットしておく
  • アンサー(回答)
    • B: CreateAnswer API(アンサー作成)を呼ぶ。するとコールバックで「アンサーSDPができた(ので相手に送れ)」と言われる。
    • B: このアンサーSDPSetLocalDescription API で「自分(B)の設定データ」としてセットしておく
    • B: なんらかの通信手段でBからAにアンサーSDPを送る
      • B <=== A
    • A: 受け取ったアンサーSDPSetRemoteDescription API で「相手(B)の設定データ」としてセットしておく
  • ICE(=Intaractive Connectivity Establishment: 相互通信路構築)
    • A: WebRTCから「ICE Candidate(通信路候補)ができた(ので相手に送れ)」と言われる
    • A: なんらかの通信手段でAからBにICE Candidateを送る
      • A ===> B
    • B: 受け取ったICE CandidateAddIceCandidate API につっこむ。
    • 同様に、A <=== B の流れもあります。
    • 上記を繰り返していると、ICE Candidateのどれかで相互通信が確認され、映像・音声のチャネルが接続され、動画通信が開始される。
  • 動画ストリームの追加
    • B: Aから新たな動画ストリームがきたら、 OnStreamAddedのようなコールバックが呼ばれるので、動画表示モジュール等にわたす。
    • A: Bから新たな動画ストリームがきたら、 OnStreamAddedのようなコールバックが呼ばれるので、動画表示モジュール等にわたす。

おおよそ以上です。上記に加え、オファーを送る側がBである逆パターンもあります。あと通信を閉じるところなどは除外していますが、なんとなく全体像がみえてきたら幸いです。

実装

Unity のクライアントコード

このOfferWithCamera()のあたり でOffer with Cameraボタン押下時の処理をしていますが、初期化して、AddStreamして、CreateOfferしています。

そうすると、OnLocalSdpReadyToSendが呼ばれるので、そこで得られたSDPを相手側に投げます。ここで、通常は、というかJavaScript APIの場合はSetLocalDescriptionを呼んでこのSDPをセットしますが、Unity Pluginの場合は、プラグイン内部でやっているようで、自分で呼ぶ必要はありません。

しばらくすると相手側からAnswerが送られてくるので、OnAnswerのあたりで受けて、もらったSDPをSetRemoteDescriptionでセットします。

あとはICEを送るだけです。 ちなみに、JS側のコードは、ICEはすべて初回のSDPに含めていることを前提にしているため、Unity C#側ではICE Candidateを受けるコードを書いていません。

このあたりはがねこまさしさんのVanilla ICEとTrickle ICEの説明などを参考にしてください。 html5experts.jp

※このあたりが適当なので、Unityアプリ対Unityアプリの通信は失敗する気がします。

うまくいけば、動画通信が開始され、OnI420RemoteFrameReadyがフレームごとに呼ばれます。

シグナリングサーバーコード

超適当実装です。

https://github.com/mhama/WebRtcUnityPluginSample/blob/master/server/index.js

ブラウザー用JSコード

html埋め込み状態です。Vanilla ICE式にしています。ICEが全部揃ってからSDPを送っています。

https://github.com/mhama/WebRtcUnityPluginSample/blob/master/server/index.html

ngrokで楽にhttps通信を実現する

ブラウザでWebRTCを動作させるには、 https 通信がほぼ必須ですが、ローカルマシンでhttpsサーバーをたてるには、ドメイン証明書の取得、ドメイン設定など面倒な部分があります。この点をクリアするため ngrok を利用しています。ngrokは、クラウドサービスですが、ローカルのポートで待ち受けるhttpサーバーをグローバルIPに公開することができ、さらに https 接続も可能になります。このためWebRTCのサンプルページを公開するのに非常に便利です。無料で24時間までは起動しておくことができます。

WebSocketを使わない理由について

Socket.IOには、ロングポーリング(http/https)での通信と、WebSocketでの通信のモードがあり、Socket.IO側で勝手に切り替えて通信を行ったりします。今回は、ngrokの設定が面倒にならないように、Socket.IOの初期化時にpollingを指定して、ロングポーリングに限定して通信をしています。

WebSocketにもhttpsのようなセキュア版(wss)と非セキュア版(ws)がありますが、WebRTCを使う場合は、セキュリティの関係でセキュア版を使う必要があります。これもngrokで公開できる可能性がありますが、もろもろ面倒なので今回は利用しませんでした。

ピクセルフォーマットの変換について

WebRTCでは、最終的にYUV420形式でフレーム情報が落ちてきます。今回のサンプルでは、YUV420形式のデータを、ピクセルごとにYUVそれぞれ8ビットをパックした形式に変換してUnityのテクスチャに格納し、最終的にshaderでRGBに変換して表示しています。 ただしこの方式は無駄が多いです。YUV420を、YUVそれぞれの3つのテクスチャに分けて扱うほうが処理が少なくて済みそうです。さらに、Unityのスレッドに渡さず、バックグラウンドスレッドでOpenGLテクスチャにセットするなどすればもっと高速になるでしょう。