作成日:2024-09-02 ・更新日:2024-09-06
UnityのWebRTCを使いブラウザーとunityの1:1映像双方向通信を実現しました。
Unity.WebRTCを使ってUnityとブラウザーで映像の送受信をする。
signalingサーバーとブラウザーのWebRTCが使用できる環境を用意します。 前回のを使います。
Unityでwebsocketが使用できるようにする必要があります。
すごく分かりやすくまとめてくださいっているのでそのまま 参考にします。
UnityのC#コードを書いていきます。 今回のスクリプトは起動時Websocketでlocalhost:3001に接続し、websocketでofferデータを受け取ったら Peer交換を行い、映像の送受信ができるように実装しました。
このコードをそのまま使うのであれば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()
{
}
}
上記コードを分解して解説していきます。
外部ライブラリで必要となるものは、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の接続先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();
}
これが無いと受信できない、と悩む羽目になる。
Unityの非同期処理はサブプロセスで実行されるため、Unity上の操作や、描写の更新がでできない。Debug.Logは正常にに動作する。 そのため、このような手法が必要になります。 Websocketは非同期サブプロセス起動なので色々と機能が制限される。そのためSynchronizationContextを使いメインプロセスとして処理してもらいます。
var _context = SynchronizationContext.Current;
vws.OnMessage += (sender, e) =>
{
_context.Post((_) =>
{
Vws_OnMsg(e);
}, null);
};
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);
jsとかも同様に、このようにすることでVanilla ICEで接続します。SetLocalDescriptionが終わった時にこのイベントが発火します。
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;
}
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イベントが実行されます。
簡単に実装できると思っていたが、意外と時間がかかってしまった。理由としては
この3点でつまづいてしまった。これさえ気を付ければ大丈夫だと思う。Unityの公式サンプルも見てみたけど動かすことができず、とても困ってしまった。sora SDKを使ったサンプル等は出ているがSDKを使わない、すぐに動くサンプルが無かったのが大変だった。
質問や、間違いがありましたら、お気軽にどうぞ
※お名前とコメントの内容を入力してください。
※全てこのブログ上に公表されます。
※許可なく管理者によって修正・削除する場合がございます。 詳しくはプライバシーポリシーを参照ください