I’m struggling to get the streaming from OBS v30 (beta) to connect with Unity’s webRTC. OBS uses
WebRTC-HTTP ingestion protocol (WHIP). Its my understanding that I can bypass the need for a TURN/ICE service when the peer-to-peer connection is done via LAN to a known IP. I can’t find any examples of this with the Samples in the Unity WebRTC package or in the Unity Render Streaming package.
(OBS)
I’m sorry but I havent tested the communication between OBS and Unity Render Streaming.
I want to share a third party project. It would help you.
https://github.com/ossrs/srs-unity
gtk2k
October 13, 2023, 9:01am
3
You can implement WHIP signaling yourself using the lower level WebRTC for Unity used within Unity Render Streaming.
Simple implementation example
using Cysharp.Threading.Tasks;
using System;
using System.IO;
using System.Net;
using Unity.WebRTC;
using UnityEngine;
public class WHIPWebRTCPeer : MonoBehaviour
{
readonly HttpListener _httpListener = new HttpListener();
public int port = 4649;
public string path = "/";
public bool startOnAwake = true;
private RTCPeerConnection peer;
public GameObject Display;
void Start()
{
StartCoroutine(WebRTC.Update());
_httpListener.Prefixes.Add("http://*:" + port + path);
if (startOnAwake)
{
StartServer();
}
}
void OnDestroy()
{
StopServer();
}
async void StartServer()
{
_httpListener.Start();
while (true)
{
var context = await _httpListener.GetContextAsync();
var response = context.Response;
Debug.Log($"{context.Request.HttpMethod} Request path: {context.Request.RawUrl}");
if (context.Request.HttpMethod == "POST")
{
foreach (var k in context.Request.Headers.Keys)
{
Debug.Log($"Header[{k}]: {context.Request.Headers[k.ToString()]}");
}
var dataText = await new StreamReader(context.Request.InputStream,
context.Request.ContentEncoding).ReadToEndAsync();
var offer = new RTCSessionDescription
{
type = RTCSdpType.Offer,
sdp = dataText
};
var utcs = new UniTaskCompletionSource<RTCSessionDescription?>();
await SetupPeer(utcs, offer);
var answer = await utcs.Task;
var data = "error";
if (answer.HasValue)
{
response.StatusCode = 201;
response.ContentType = "application/sdp";
response.Headers.Add("Location", "http://localhost:4649");
data = answer.Value.sdp;
}
else
{
response.StatusCode = 500;
response.ContentType = "text/plane";
}
var buffer = System.Text.Encoding.UTF8.GetBytes(data);
response.ContentType = "application/sdp";
response.ContentLength64 = buffer.Length;
var output = response.OutputStream;
await output.WriteAsync(buffer, 0, buffer.Length);
output.Close();
context.Response.Close();
}
else if (context.Request.HttpMethod == "DELETE")
{
peer?.Close();
peer = null;
}
}
}
void StopServer()
{
_httpListener.Stop();
}
private async UniTask SetupPeer(UniTaskCompletionSource<RTCSessionDescription?> utcs, RTCSessionDescription offer)
{
Debug.Log($"CreatePeer");
peer = new RTCPeerConnection();
var flg = false;
peer.OnIceCandidate = async cand =>
{
if (cand.Type == RTCIceCandidateType.Host)
{
if (!flg)
{
flg = true;
Debug.Log($"Answer:{peer.LocalDescription.sdp}");
utcs.TrySetResult(peer.LocalDescription);
}
}
};
peer.OnTrack = evt =>
{
if (evt.Track is VideoStreamTrack videoTrack)
{
videoTrack.OnVideoReceived += VideoTrack_OnVideoReceived;
}
};
var opSetRemoteDesc = peer.SetRemoteDescription(ref offer);
await opSetRemoteDesc;
if (opSetRemoteDesc.IsError)
{
Debug.LogError(opSetRemoteDesc.Error.message);
utcs.TrySetException(new Exception(opSetRemoteDesc.Error.message));
return;
}
Debug.Log($"Set Offer");
var opCreateAnswer = peer.CreateAnswer();
await opCreateAnswer;
if (opCreateAnswer.IsError)
{
Debug.LogError(opSetRemoteDesc.Error.message);
utcs.TrySetException(new Exception(opCreateAnswer.Error.message));
return;
}
Debug.Log($"Create Answer");
var answer = opCreateAnswer.Desc;
var opSetAnswer = peer.SetLocalDescription(ref answer);
if (opSetAnswer.IsError)
{
Debug.LogError(opSetAnswer.Error.message);
}
Debug.Log($"Set Answer");
return;
}
private void VideoTrack_OnVideoReceived(Texture renderer)
{
Display.GetComponent<Renderer>().material.mainTexture = renderer;
}
}
3 Likes
gtk2k:
You can implement WHIP signaling yourself using the lower level WebRTC for Unity used within Unity Render Streaming.
Simple implementation example
using Cysharp.Threading.Tasks;
using System;
using System.IO;
using System.Net;
using Unity.WebRTC;
using UnityEngine;
public class WHIPWebRTCPeer : MonoBehaviour
{
readonly HttpListener _httpListener = new HttpListener();
public int port = 4649;
public string path = "/";
public bool startOnAwake = true;
private RTCPeerConnection peer;
public GameObject Display;
void Start()
{
StartCoroutine(WebRTC.Update());
_httpListener.Prefixes.Add("http://*:" + port + path);
if (startOnAwake)
{
StartServer();
}
}
void OnDestroy()
{
StopServer();
}
async void StartServer()
{
_httpListener.Start();
while (true)
{
var context = await _httpListener.GetContextAsync();
var response = context.Response;
Debug.Log($"{context.Request.HttpMethod} Request path: {context.Request.RawUrl}");
if (context.Request.HttpMethod == "POST")
{
foreach (var k in context.Request.Headers.Keys)
{
Debug.Log($"Header[{k}]: {context.Request.Headers[k.ToString()]}");
}
var dataText = await new StreamReader(context.Request.InputStream,
context.Request.ContentEncoding).ReadToEndAsync();
var offer = new RTCSessionDescription
{
type = RTCSdpType.Offer,
sdp = dataText
};
var utcs = new UniTaskCompletionSource<RTCSessionDescription?>();
await SetupPeer(utcs, offer);
var answer = await utcs.Task;
var data = "error";
if (answer.HasValue)
{
response.StatusCode = 201;
response.ContentType = "application/sdp";
response.Headers.Add("Location", "http://localhost:4649");
data = answer.Value.sdp;
}
else
{
response.StatusCode = 500;
response.ContentType = "text/plane";
}
var buffer = System.Text.Encoding.UTF8.GetBytes(data);
response.ContentType = "application/sdp";
response.ContentLength64 = buffer.Length;
var output = response.OutputStream;
await output.WriteAsync(buffer, 0, buffer.Length);
output.Close();
context.Response.Close();
}
else if (context.Request.HttpMethod == "DELETE")
{
peer?.Close();
peer = null;
}
}
}
void StopServer()
{
_httpListener.Stop();
}
private async UniTask SetupPeer(UniTaskCompletionSource<RTCSessionDescription?> utcs, RTCSessionDescription offer)
{
Debug.Log($"CreatePeer");
peer = new RTCPeerConnection();
var flg = false;
peer.OnIceCandidate = async cand =>
{
if (cand.Type == RTCIceCandidateType.Host)
{
if (!flg)
{
flg = true;
Debug.Log($"Answer:{peer.LocalDescription.sdp}");
utcs.TrySetResult(peer.LocalDescription);
}
}
};
peer.OnTrack = evt =>
{
if (evt.Track is VideoStreamTrack videoTrack)
{
videoTrack.OnVideoReceived += VideoTrack_OnVideoReceived;
}
};
var opSetRemoteDesc = peer.SetRemoteDescription(ref offer);
await opSetRemoteDesc;
if (opSetRemoteDesc.IsError)
{
Debug.LogError(opSetRemoteDesc.Error.message);
utcs.TrySetException(new Exception(opSetRemoteDesc.Error.message));
return;
}
Debug.Log($"Set Offer");
var opCreateAnswer = peer.CreateAnswer();
await opCreateAnswer;
if (opCreateAnswer.IsError)
{
Debug.LogError(opSetRemoteDesc.Error.message);
utcs.TrySetException(new Exception(opCreateAnswer.Error.message));
return;
}
Debug.Log($"Create Answer");
var answer = opCreateAnswer.Desc;
var opSetAnswer = peer.SetLocalDescription(ref answer);
if (opSetAnswer.IsError)
{
Debug.LogError(opSetAnswer.Error.message);
}
Debug.Log($"Set Answer");
return;
}
private void VideoTrack_OnVideoReceived(Texture renderer)
{
Display.GetComponent<Renderer>().material.mainTexture = renderer;
}
}
This worked beautifully. Our team is very happy this works. Thank you very much.
Now we have to figure out how to get WebRTC to not crash when building with Vulcan on Oculus Quest. I posted about that issue here: https://discussions.unity.com/t/931186