Hero img
Unity WebRTCを動かす方法と解説

UnityのWebRTCのサンプルが動作せず、自前でサーバーを構築しWebRTCで映像通信をしてみた。

UnityのWebRTCを使いブラウザーとunityの1:1映像双方向通信を実現しました。


目次

  • 目標
  • 環境
  • 事前準備
  • Unityscript
  • とりあえずコード
  • 解説
  • 1.必要ライブラリーをインポート
  • 初期設定
  • wbsocketデータ受信と送信ファンクション
  • 起動時
  • Answer処理
  • まとめ

目標

Unity.WebRTCを使ってUnityとブラウザーで映像の送受信をする。

unity-webrtc-result

環境

  • windows 10 64 bit
  • node v18.17.1
  • [email protected]
  • Unity 2021.3.16f1e
  • Unity WebRTC 3.0.0-pre.7
  • visual studio 2022

事前準備

signalingサーバーとブラウザーのWebRTCが使用できる環境を用意します。 前回のを使います。

Unityでwebsocketが使用できるようにする必要があります。

すごく分かりやすくまとめてくださいっているのでそのまま 参考にします。

Unityscript

UnityのC#コードを書いていきます。 今回のスクリプトは起動時Websocketでlocalhost:3001に接続し、websocketでofferデータを受け取ったら Peer交換を行い、映像の送受信ができるように実装しました。

このコードをそのまま使うのであればcubeを次のようにコンポーネントを追加します。

unity-webrtc-cube

とりあえずコード

全コード
using UnityEngine;
using System.Collections;
using WebSocketSharp;
using Unity.WebRTC;
using System.Threading;
using wssjson;
using System.Linq;

public class wbsocket : MonoBehaviour
{
    // Start is called before the first frame update
    private WebSocket ws;
    private RTCPeerConnection localPeer;
    private Material material;
    private VideoStreamTrack videoTrack;
    MediaStreamTrack vtrack;
    private string servername = "ws://localhost:3001/";

    /// <summary>
    /// wss Event list
    /// </summary>
    public void wsOnmsg(MessageEventArgs e)
    {
        var message = e.Data;
        RTCSessionDescription sDP = JsonUtility.FromJson<RTCSessionDescription>(message);
        if(sDP.type == RTCSdpType.Offer)
        {
            StartCoroutine(Answer(sDP));
        }
        
    }
    private void WssmsgSend(object data)
    {
        string message = JsonUtility.ToJson(data);
        ws.Send(message);
    }

    //LoginEvent


    void Start()
    {
        StartCoroutine(WebRTC.Update());
        material = GetComponent<Renderer>().material;
        var _context = SynchronizationContext.Current;
        ws = new WebSocket(servername);
        ws.OnOpen += (sender, e) =>
        {
            Debug.Log("WebSocket Open");
        };
        ws.OnError += (sender, e) =>
        {
            Debug.Log("WebSocket Error Message: " + e.Message);
        };
        ws.OnClose += (sender, e) =>
        {
            Debug.Log("MainWebSocket Close");
        };
        /////////////////////////////////////CaptureStreamTrack
        ws.OnMessage += (sender, e) =>
        {
            _context.Post((_) =>
            {
                wsOnmsg(e);
            }, null);
        };
        ws.Connect();
        PeerSetting();
    }


    private IEnumerator Answer(RTCSessionDescription offerobj)
    {
        // リモートオファーのSDPを設定
        RTCSessionDescription rTC = new RTCSessionDescription
        {
            sdp = offerobj.sdp,
            type = RTCSdpType.Offer
        };

        // SetRemoteDescriptionの完了を待機
        var setRemoteSdpOperation = localPeer.SetRemoteDescription(ref offerobj);
        yield return new WaitUntil(() => setRemoteSdpOperation.IsDone);

        if (setRemoteSdpOperation.IsError)
        {
            Debug.LogError("FAILED REMOTE");
            yield break;
        }

        RTCOfferAnswerOptions options = default;

        // アンサーの作成を待機
        var createAnswerOperation = localPeer.CreateAnswer(ref options);
        yield return new WaitUntil(() => createAnswerOperation.IsDone);

        if (createAnswerOperation.IsError)
        {
            Debug.LogError("FAILED ANSWER");
            yield break;
        }

        // アンサーSDPをローカルディスクリプションとして設定
        var sdpAnswer = createAnswerOperation.Desc;
        var setLocalDspOperation = localPeer.SetLocalDescription(ref sdpAnswer);
        yield return new WaitUntil(() => setLocalDspOperation.IsDone);

        if (setLocalDspOperation.IsError)
        {
            Debug.LogError("FAILED LOCAL DESCRIPTION");
            yield break;
        }
    }



    public void PC_AddTrack(RTCTrackEvent e)
    {
        try { 
        if (e.Track is VideoStreamTrack track)
        {
            videoTrack = track;
            videoTrack.OnVideoReceived += OnVideoFrameReceived;
            Debug.Log("VideoStreamTrack successfully attached.");
        }
        else
        {
            Debug.LogError("Received track is not a VideoStreamTrack." + "TRACK is:::" + e.Track);
        }
        }
        catch { }
    }

    private void OnVideoFrameReceived(Texture render)
    {
        material.color = Color.white;
        Debug.Log($"Shader: {material.shader.name}, Texture: {material.mainTexture}");
        Debug.Log($"VIDEO RECEIVED {render.width}x{render.height}");
        material.mainTexture = render;

    }

    public void PeerSetting()
    {
        var config = new RTCconfig().RTCConfiguration;
        
        RTCIceServer[] iceServers = new RTCIceServer[] {
        new RTCIceServer(){urls = new string[] { "stun:stun.l.google.com:19302" }}
        };

        config.iceServers = iceServers;
        localPeer = new RTCPeerConnection(ref config);
        localPeer.OnTrack += PC_AddTrack;

        var camera = GetComponent<Camera>();
        var stream = camera.CaptureStream(1280, 720);
        vtrack = stream.GetTracks().First();
        localPeer.AddTrack(vtrack, stream);

        localPeer.OnIceCandidate += (RTCIceCandidate e) => {
            Debug.Log("OnIceCandidate" + e);
            if (!string.IsNullOrEmpty(e.Candidate))
            {
                WssmsgSend(localPeer.LocalDescription);
            }
        };

    }

    void DEMOSQQ()
    {
    }
    public float rotationSpeed = 15f;

    // Update is called once per frame
    void Update()
    {
        transform.Rotate(Vector3.up * rotationSpeed * Time.deltaTime);
        if (Input.GetKeyDown(KeyCode.A))
        {
            DEMOSQQ();
        }
    }
    void OnDestroy()
    {
        ws.Close();
        ws = null;
    }

    private void Awake()
    {
    }

}

解説

上記コードを分解して解説していきます。

1.必要ライブラリーをインポート

外部ライブラリで必要となるものは、WebSocketSharpとUnity.WebRTCの2つです。
その他に必要なライブラリもインポートします。

using UnityEngine;
using System.Collections;
using WebSocketSharp;
using Unity.WebRTC;
using System.Threading;
using System.Linq;

初期設定

変数を定義していきます。
websocket接続系、webrtc(peer)コネクト系、マテリアル・テクスチャー系を定義します。


public class wbsocket : MonoBehaviour
{
    private WebSocket ws;//WebSocket
    private RTCPeerConnection localPeer;//ローカルのピア―
    private Material material;//このオブジェクトのマテリアルを格納
    private VideoStreamTrack videoTrack;//映像受け取り用
    private MediaStreamTrack vtrack;//映像送信用
    private string servername = "ws://localhost:3001/";//websocketの接続先
}

wbsocketデータ受信と送信ファンクション

wbsocketの接続先ws://localhost:3001/で受け取ったイベントはここで処理します。
offerのデータであればPeer Answerを作成する処理です。

    public void wsOnmsg(MessageEventArgs e)
    {
        var message = e.Data;
        RTCSessionDescription sDP = JsonUtility.FromJson<RTCSessionDescription>(message);
        if(sDP.type == RTCSdpType.Offer)
        {
            StartCoroutine(Answer(sDP));
        }
        
    }

こちらはwebsocket送信用ファンクションになります。

    private void WssmsgSend(object data)
    {
        string message = JsonUtility.ToJson(data);
        ws.Send(message);
    }

起動時

プロジェクト起動時、初期設定を行います。主にwebsocketの初期化、マテリアルの指定、Peer設定です。
私が良く忘れてしまうのは、ws.Connect()の処理を忘れて、接続がされないとこですね

    void Start()
    {
        StartCoroutine(WebRTC.Update());//重要
        material = GetComponent<Renderer>().material;//マテリアル用
        var _context = SynchronizationContext.Current;//メインスレッド起動用
        ws = new WebSocket(servername);
        ws.OnOpen += (sender, e) =>
        {
            Debug.Log("WebSocket Open");
        };
        ws.OnError += (sender, e) =>
        {
            Debug.Log("WebSocket Error Message: " + e.Message);
        };
        ws.OnClose += (sender, e) =>
        {
            Debug.Log("MainWebSocket Close");
        };
        ws.OnMessage += (sender, e) =>
        {
            _context.Post((_) =>
            {
                wsOnmsg(e);
            }, null);
        };
        ws.Connect();
        PeerSetting();
    }

WebRTC.Updateが重要

これが無いと受信できない、と悩む羽目になる。

メインスレッド

Unityの非同期処理はサブプロセスで実行されるため、Unity上の操作や、描写の更新がでできない。Debug.Logは正常にに動作する。 そのため、このような手法が必要になります。 Websocketは非同期サブプロセス起動なので色々と機能が制限される。そのためSynchronizationContextを使いメインプロセスとして処理してもらいます。

        var _context = SynchronizationContext.Current;

        vws.OnMessage += (sender, e) =>
        {
            _context.Post((_) =>
            {
                Vws_OnMsg(e);   
            }, null);
        };

Peer設定

WebRTCのPeer設定をします。映像の送信もするため、AddTrackでUnityのカメラを追加します。

    public void PeerSetting()
    {
        var config = new RTCconfig().RTCConfiguration;
        
        RTCIceServer[] iceServers = new RTCIceServer[] {
        new RTCIceServer(){urls = new string[] { "stun:stun.l.google.com:19302" }}
        };

        config.iceServers = iceServers;
        localPeer = new RTCPeerConnection(ref config);
        localPeer.OnTrack += PC_AddTrack;

        var camera = GetComponent<Camera>();
        var stream = camera.CaptureStream(1280, 720);
        vtrack = stream.GetTracks().First();
        localPeer.AddTrack(vtrack, stream);

        localPeer.OnIceCandidate += (RTCIceCandidate e) => {
            Debug.Log("OnIceCandidate" + e);
            if (!string.IsNullOrEmpty(e.Candidate))
            {
                WssmsgSend(localPeer.LocalDescription);
            }
        };

    }

他のサンプル等確認すると、以下のようにAddTrack(引数1)と指定し、映像を送信しているようだが、うまく動作しなかった。かなり詰まってしまったポイントである。
具体的な挙動としては、映像受信側のブラウザー側で映像が表示されない現象である。wiresharkで通信を確認して見ると、何かデータを送信はしている様だが...ブラウザー側では映像データとして認識?されていないようでした。つまり、カメラ->テクスチャー->画像データ(videotrack)->WebRTC->ブラウザー。の流れの画像videotrackの部分が正常でない気がする。

映像送れなかった例
   var camera = Camera.main;
   var videoStreamTrack = new VideoStreamTrack(camera);
   peerConnection.AddTrack(videoStreamTrack);

OnIceCandidate

jsとかも同様に、このようにすることでVanilla ICEで接続します。SetLocalDescriptionが終わった時にこのイベントが発火します。

OnTrack

WebRTCで映像受け取った際その映像をmainTextureに指定しすることで、映像を表示させます。

    public void PC_AddTrack(RTCTrackEvent e)
    {
        try { 
        if (e.Track is VideoStreamTrack track)
        {
            videoTrack = track;
            videoTrack.OnVideoReceived += OnVideoFrameReceived;
            Debug.Log("VideoStreamTrack successfully attached.");
        }
        else
        {
            Debug.LogError("Received track is not a VideoStreamTrack." + "TRACK is:::" + e.Track);
        }
        }
        catch { }
    }


    private void OnVideoFrameReceived(Texture render)
    {
        material.color = Color.white;
        Debug.Log($"Shader: {material.shader.name}, Texture: {material.mainTexture}");
        Debug.Log($"VIDEO RECEIVED {render.width}x{render.height}");
        material.mainTexture = render;
    }

Answer処理

websocketでSDP情報を受け取った際の処理になります。
非同期処理で実行されてしまいますが、順番に(同期処理)実行してほしいため、IEnumeratorを使います。

   private IEnumerator Answer(RTCSessionDescription offerobj)
   {
       // リモートオファーのSDPを設定
       RTCSessionDescription rTC = new RTCSessionDescription
       {
           sdp = offerobj.sdp,
           type = RTCSdpType.Offer
       };

       // SetRemoteDescriptionの完了を待機
       var setRemoteSdpOperation = localPeer.SetRemoteDescription(ref offerobj);
       yield return new WaitUntil(() => setRemoteSdpOperation.IsDone);

       if (setRemoteSdpOperation.IsError)
       {
           Debug.LogError("FAILED REMOTE");
           yield break;
       }

       RTCOfferAnswerOptions options = default;

       // アンサーの作成を待機
       var createAnswerOperation = localPeer.CreateAnswer(ref options);
       yield return new WaitUntil(() => createAnswerOperation.IsDone);

       if (createAnswerOperation.IsError)
       {
           Debug.LogError("FAILED ANSWER");
           yield break;
       }

       // アンサーSDPをローカルディスクリプションとして設定
       var sdpAnswer = createAnswerOperation.Desc;
       var setLocalDspOperation = localPeer.SetLocalDescription(ref sdpAnswer);
       yield return new WaitUntil(() => setLocalDspOperation.IsDone);

       if (setLocalDspOperation.IsError)
       {
           Debug.LogError("FAILED LOCAL DESCRIPTION");
           yield break;
       }
   }

websocketデータが来たらここでAnswerを作成します。その後SetLocalDescriptionが完了するとOnIceCandidateイベントが実行されます。

まとめ

簡単に実装できると思っていたが、意外と時間がかかってしまった。理由としては

  1. 1.メインプロセスで処理する必要がある。
  2. 2.WebRTC.Update()が必要である。
  3. 3.localPeer.AddTrack(vtrack, stream);と指定する必要がある。
  4. 4.Gamewindowを表示しておかないと映像が更新されない。

この3点でつまづいてしまった。これさえ気を付ければ大丈夫だと思う。Unityの公式サンプルも見てみたけど動かすことができず、とても困ってしまった。sora SDKを使ったサンプル等は出ているがSDKを使わない、すぐに動くサンプルが無かったのが大変だった。

関連記事

コメント

コメントを書く

質問や、間違いがありましたら、お気軽にどうぞ

※お名前とコメントの内容を入力してください。
※全てこのブログ上に公表されます。
※許可なく管理者によって修正・削除する場合がございます。 詳しくはプライバシーポリシーを参照ください