feat: implement negotiation-needed handler on UnityRenderStreaming (#334)

* wip receive video sample

* wip add sample receive video on unity

* fix websocket signaling

* implemet onanswer and onnegatiationneeded

* use addtranceiver

* fix offer setting

* revet signaling data handle

* fix review

* avoid null on uicontroler

* implement receive video viewer component

* fix custom editor error

* async mainthread about signaling invoke event

* change signaling test to unitytest

* fix get answer request time on httpsignaling

* fix mediastream handle on receivevideoviewer
This commit is contained in:
Takashi Kannan 2020-10-02 16:49:02 +09:00 коммит произвёл GitHub
Родитель 6f885746e4
Коммит e3a610d7bc
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
16 изменённых файлов: 1634 добавлений и 140 удалений

Просмотреть файл

@ -12,3 +12,36 @@
<div id="player"></div>
</body>
</html>
<!-- <!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Unity Video Sender</title>
</head>
<body>
Send to Unity<br />
<button type="button" id="startVideoButton">Start Video</button>
<button type="button" id="setUpButton">Set Up</button>
<button type="button" id="addTrackButton">AddTrack</button>
<button type="button" id="hangUpButton">Hang Up</button>
<div>
<video id="local_video" autoplay muted=true style="width: 160px; height: 120px; border: 1px solid black;"></video>
<video id="remote_video" autoplay style="width: 160px; height: 120px; border: 1px solid black;"></video>
</div>
<p>SDP to send:<br />
<textarea id="text_for_send_sdp" rows="5" cols="60" readonly="readonly">SDP to send</textarea>
</p>
<p>SDP to receive:<br />
<textarea id="text_for_receive_sdp" rows="5" cols="60"></textarea>
</p>
<script type="text/javascript" src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="https://unpkg.com/event-target@latest/min.js"></script>
<script src="https://unpkg.com/resize-observer-polyfill@1.5.0/dist/ResizeObserver.global.js"></script>
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=IntersectionObserver"></script>
<script type="module" src="app2.js"></script>
</body>
</html> -->

Просмотреть файл

@ -0,0 +1,38 @@
import { SendVideo } from "./sendvideo.js";
const localVideo = document.getElementById('local_video');
const remoteVideo = document.getElementById('remote_video');
//const textForSendSdp = document.getElementById('text_for_send_sdp');
//const textToReceiveSdp = document.getElementById('text_for_receive_sdp');
let sendVideo = new SendVideo();
let startButton = document.getElementById('startVideoButton');
startButton.addEventListener('click', startVideo);
let setupButton = document.getElementById('setUpButton');
setupButton.addEventListener('click', setUp);
let addTrackButton = document.getElementById('addTrackButton');
addTrackButton.addEventListener('click', addTrack);
let hangUpButton = document.getElementById('hangUpButton');
hangUpButton.addEventListener('click', hangUp);
async function startVideo() {
await sendVideo.startVideo(localVideo);
}
async function setUp() {
await sendVideo.setupConnection(remoteVideo);
}
async function addTrack() {
await sendVideo.addTrack();
}
function hangUp() {
sendVideo.hangUp();
remoteVideo.pause();
remoteVideo.srcObject = null;
// textForSendSdp.value = '';
// textToReceiveSdp.value = '';
}

Просмотреть файл

@ -0,0 +1,153 @@
import Signaling, { WebSocketSignaling } from "./signaling.js"
export class SendVideo {
constructor() {
const _this = this;
this.config = SendVideo.getConfiguration();
this.pc = null;
this.localStream = null;
this.remoteStram = new MediaStream();
this.negotiationneededCounter = 0;
this.isOffer = false;
}
static getConfiguration() {
let config = {};
config.sdpSemantics = 'unified-plan';
config.iceServers = [{ urls: ['stun:stun.l.google.com:19302'] }];
return config;
}
async startVideo(localVideo) {
try {
this.localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
localVideo.srcObject = this.localStream;
await localVideo.play();
} catch (err) {
console.error('mediaDevice.getUserMedia() error:', err);
}
}
async setupConnection(remoteVideo) {
const _this = this;
this.remoteVideo = remoteVideo;
this.remoteVideo.srcObject = this.remoteStram;
this.remoteStram.onaddtrack = async (e) => await _this.remoteVideo.play();
const protocolEndPoint = location.protocol + '//' + location.host + location.pathname + 'protocol';
const createResponse = await fetch(protocolEndPoint);
const res = await createResponse.json();
if (res.useWebSocket) {
this.signaling = new WebSocketSignaling();
} else {
this.signaling = new Signaling();
}
this.signaling.addEventListener('offer', async (e) => {
console.error(e);
if (_this.pc) {
console.error('peerConnection alreay exist');
}
_this.prepareNewPeerConnection(false);
const offer = e.detail;
const desc = new RTCSessionDescription({ sdp: offer.sdp, type: "offer" });
await _this.pc.setRemoteDescription(desc);
let answer = await _this.pc.createAnswer();
await _this.pc.setLocalDescription(answer);
_this.signaling.sendAnswer(answer.sdp);
});
this.signaling.addEventListener('answer', async (e) => {
if (!_this.pc) {
console.error('peerConnection NOT exist!');
return;
}
const answer = e.detail;
const desc = new RTCSessionDescription({ sdp: answer.sdp, type: "answer" });
await _this.pc.setRemoteDescription(desc);
});
this.signaling.addEventListener('candidate', async (e) => {
const candidate = e.detail;
const iceCandidate = new RTCIceCandidate({ candidate: candidate.candidate, sdpMid: candidate.sdpMid, sdpMLineIndex: candidate.sdpMLineIndex });
_this.pc.addIceCandidate(iceCandidate);
});
await this.signaling.start();
this.prepareNewPeerConnection(true);
}
async addTrack() {
const _this = this;
this.localStream.getTracks().forEach(track => _this.pc.addTrack(track, _this.localStream));
}
prepareNewPeerConnection(isOffer) {
const _this = this;
this.isOffer = isOffer;
// close current RTCPeerConnection
if (this.pc) {
console.log('Close current PeerConnection');
this.pc.close();
this.pc = null;
}
// Create peerConnection with proxy server and set up handlers
this.pc = new RTCPeerConnection(this.config);
this.pc.onsignalingstatechange = e => {
console.log('signalingState changed:', e);
};
this.pc.oniceconnectionstatechange = e => {
console.log('iceConnectionState changed:', e);
console.log('pc.iceConnectionState:' + _this.pc.iceConnectionState);
if (_this.pc.iceConnectionState === 'disconnected') {
_this.hangUp();
}
};
this.pc.onicegatheringstatechange = e => {
console.log('iceGatheringState changed:', e);
};
this.pc.ontrack = async (e) => {
_this.remoteStram.addTrack(e.track);
};
this.pc.onicecandidate = e => {
if (e.candidate != null) {
_this.signaling.sendCandidate(e.candidate.candidate, e.candidate.sdpMid, e.candidate.sdpMLineIndex);
}
};
this.pc.onnegotiationneeded = async () => {
if (_this.isOffer) {
if (_this.negotiationneededCounter === 0) {
let offer = await _this.pc.createOffer();
console.log('createOffer() succsess in promise');
await _this.pc.setLocalDescription(offer);
console.log('setLocalDescription() succsess in promise');
_this.signaling.sendOffer(offer.sdp);
_this.negotiationneededCounter++;
}
}
};
}
hangUp() {
if (this.pc) {
if (this.pc.iceConnectionState !== 'closed') {
this.pc.close();
this.pc = null;
negotiationneededCounter = 0;
console.log('sending close message');
this.signaling.stop();
return;
}
}
console.log('peerConnection is closed.');
}
}

Просмотреть файл

@ -28,10 +28,29 @@ export default class Signaling extends EventTarget {
const connection = await res.json();
this.connectionId = connection.connectionId;
this.loopGetOffer();
this.loopGetAnswer();
this.loopGetCandidate();
}
async loopGetOffer() {
let lastTimeRequest = Date.now() - 30000;
while (true) {
const res = await this.getOffer(lastTimeRequest);
lastTimeRequest = Date.parse(res.headers.get('Date'));
const data = await res.json();
const offers = data.offers;
offers.forEach(offer => {
this.dispatchEvent(new CustomEvent('offer', { detail: offer }));
});
await this.sleep(this.interval);
}
}
async loopGetAnswer() {
// receive answer message from 30secs ago
let lastTimeRequest = Date.now() - 30000;

Просмотреть файл

@ -103,10 +103,15 @@ export default class WSSignaling {
const newOffer = new Offer(message.sdp, Date.now());
offers.set(connectionId, newOffer);
connectionPair.set(connectionId, [ws, null]);
clients.forEach((_v, k) => k.send(JSON.stringify({from:connectionId, to:"", type:"offer", data:newOffer})));
clients.forEach((_v, k) => {
if (k == ws) {
return;
}
k.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer }));
});
}
private onAnswer(ws: WebSocket, message: any){
private onAnswer(ws: WebSocket, message: any) {
const connectionId = message.connectionId as string;
const connectionIds = getOrCreateConnectionIds(ws);
connectionIds.add(connectionId);
@ -114,17 +119,22 @@ export default class WSSignaling {
answers.set(connectionId, newAnswer);
const pair = connectionPair.get(connectionId);
const otherSessionId = pair[0];
connectionPair.set(connectionId, [otherSessionId, ws]);
const otherSessionWs = pair[0];
connectionPair.set(connectionId, [otherSessionWs, ws]);
const mapCandidates = candidates.get(otherSessionId);
const mapCandidates = candidates.get(otherSessionWs);
if (mapCandidates) {
const arrayCandidates = mapCandidates.get(connectionId);
for (const candidate of arrayCandidates) {
candidate.datetime = Date.now();
}
}
clients.forEach((_v, k) => k.send(JSON.stringify({from:connectionId, to:"", type:"answer", data:newAnswer})));
clients.forEach((_v, k) => {
if (k == ws) {
return;
}
k.send(JSON.stringify({ from: connectionId, to: "", type: "answer", data: newAnswer }))
});
}
private onCandidate(ws: WebSocket, message: any){
@ -142,7 +152,7 @@ export default class WSSignaling {
arr.push(candidate);
clients.forEach((_v, k) => {
if(k === ws){
if (k === ws) {
return;
}
k.send(JSON.stringify({from:connectionId, to:"", type:"candidate", data:candidate}));

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e62a4b241050ded4ab2cd9f240c462c6
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Просмотреть файл

@ -0,0 +1,43 @@
using System;
using Unity.WebRTC;
using UnityEngine;
using UnityEngine.UI;
namespace Unity.RenderStreaming
{
public class ReceiveVideoViewer : MonoBehaviour
{
[SerializeField] private Button sendOfferButton;
[SerializeField] private RawImage receiveImage;
private MediaStream m_receiveStream;
void Start()
{
sendOfferButton.onClick.AddListener(() => RenderStreaming.Instance?.AddTransceiver());
}
void OnEnable()
{
m_receiveStream = new MediaStream();
RenderStreaming.Instance?.AddVideoReceiveStream(m_receiveStream);
// ToDo: need update webrtc package to 2.2
// m_receiveStream.OnAddTrack = e =>
// {
// if (receiveImage != null && e.Track.Kind == TrackKind.Video)
// {
// var videoTrack = (VideoStreamTrack)e.Track;
// receiveImage.texture = videoTrack.InitializeReceiver();
// }
// };
}
void OnDisable()
{
RenderStreaming.Instance?.RemoveVideoReceiveStream(m_receiveStream);
m_receiveStream.OnAddTrack = null;
m_receiveStream.Dispose();
m_receiveStream = null;
}
}
}

Просмотреть файл

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f1e06fd32ea04af9b843af232d7e9db2
timeCreated: 1600837296

Просмотреть файл

@ -2,9 +2,9 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using UnityEngine;
using Unity.WebRTC;
using System.Text.RegularExpressions;
using Unity.RenderStreaming.Signaling;
using UnityEngine.InputSystem.EnhancedTouch;
@ -49,8 +49,10 @@ namespace Unity.RenderStreaming
[SerializeField, Tooltip("Array to set your own click event")]
private ButtonClickElement[] arrayButtonClickEvent;
#pragma warning restore 0649
private SynchronizationContext m_mainThreadContext;
private ISignaling m_signaling;
private readonly Dictionary<string, RTCPeerConnection> m_mapConnectionIdAndPeer = new Dictionary<string, RTCPeerConnection>();
private readonly Dictionary<RTCPeerConnection, DataChannelDictionary> m_mapPeerAndChannelDictionary = new Dictionary<RTCPeerConnection, DataChannelDictionary>();
@ -59,9 +61,11 @@ namespace Unity.RenderStreaming
private readonly List<SimpleCameraController> m_listController = new List<SimpleCameraController>();
private readonly List<VideoStreamTrack> m_listVideoStreamTrack = new List<VideoStreamTrack>();
private readonly Dictionary<MediaStreamTrack, List<RTCRtpSender>> m_mapTrackAndSenderList = new Dictionary<MediaStreamTrack, List<RTCRtpSender>>();
private readonly List<MediaStream> m_listVideoReceiveStream = new List<MediaStream>();
private MediaStream m_audioStream;
private DefaultInput m_defaultInput;
private RTCConfiguration m_conf;
private string m_connectionId;
public static RenderStreaming Instance { get; private set; }
@ -78,6 +82,7 @@ namespace Unity.RenderStreaming
WebRTC.WebRTC.Initialize(encoderType);
m_defaultInput = new DefaultInput();
EnhancedTouchSupport.Enable();
m_mainThreadContext = SynchronizationContext.Current;
}
public void OnDestroy()
@ -87,11 +92,11 @@ namespace Unity.RenderStreaming
WebRTC.WebRTC.Dispose();
RemoteInputReceiver.Dispose();
Unity.WebRTC.Audio.Stop();
m_mainThreadContext = null;
}
public void Start()
{
m_audioStream = Unity.WebRTC.Audio.CaptureStream();
m_conf = default;
m_conf.iceServers = iceServers;
StartCoroutine(WebRTC.WebRTC.Update());
@ -101,11 +106,17 @@ namespace Unity.RenderStreaming
{
if (this.m_signaling == null)
{
Type t = Type.GetType(signalingType);
object[] args = { urlSignaling, interval };
object[] args = { urlSignaling, interval, m_mainThreadContext };
this.m_signaling = (ISignaling)Activator.CreateInstance(t, args);
this.m_signaling.OnOffer += OnOffer;
this.m_signaling.OnStart += signaling => signaling.CreateConnection();
this.m_signaling.OnCreateConnection += (signaling, id) =>
{
m_connectionId = id;
CreatePeerConnection(signaling, m_connectionId, true);
};
this.m_signaling.OnOffer += (signaling, data) => StartCoroutine(OnOffer(signaling, data));
this.m_signaling.OnAnswer += (signaling, data) => StartCoroutine(OnAnswer(signaling, data));
this.m_signaling.OnIceCandidate += OnIceCandidate;
}
this.m_signaling.Start();
@ -132,6 +143,28 @@ namespace Unity.RenderStreaming
m_listVideoStreamTrack.Remove(track);
}
public void AddVideoReceiveStream(MediaStream stream)
{
m_listVideoReceiveStream.Add(stream);
}
public void RemoveVideoReceiveStream(MediaStream stream)
{
m_listVideoReceiveStream.Remove(stream);
}
public void AddTransceiver()
{
if (string.IsNullOrEmpty(m_connectionId) ||
!m_mapConnectionIdAndPeer.TryGetValue(m_connectionId, out var pc))
{
return;
}
// ToDo: need update webrtc package to 2.2
// pc.AddTransceiver(TrackKind.Video);
}
public void ChangeVideoParameters(VideoStreamTrack track, ulong? bitrate, uint? framerate)
{
foreach (var sender in m_mapTrackAndSenderList[track])
@ -155,35 +188,30 @@ namespace Unity.RenderStreaming
}
}
void OnOffer(ISignaling signaling, DescData e)
IEnumerator OnOffer(ISignaling signaling, DescData e)
{
RTCSessionDescription _desc;
_desc.type = RTCSdpType.Offer;
_desc.sdp = e.sdp;
var connectionId = e.connectionId;
if (m_mapConnectionIdAndPeer.ContainsKey(connectionId))
{
return;
Debug.LogError($"connection:{connectionId} peerConnection already exist");
yield break;
}
var pc = new RTCPeerConnection();
m_mapConnectionIdAndPeer.Add(e.connectionId, pc);
pc.OnDataChannel = new DelegateOnDataChannel(channel => { OnDataChannel(pc, channel); });
pc.SetConfiguration(ref m_conf);
pc.OnIceCandidate = new DelegateOnIceCandidate(candidate =>
{
signaling.SendCandidate(e.connectionId, candidate);
});
pc.OnIceConnectionChange = new DelegateOnIceConnectionChange(state =>
{
if(state == RTCIceConnectionState.Disconnected)
{
pc.Close();
m_mapConnectionIdAndPeer.Remove(e.connectionId);
}
});
var pc = CreatePeerConnection(signaling, connectionId, false);
RTCSessionDescription _desc;
_desc.type = RTCSdpType.Offer;
_desc.sdp = e.sdp;
var opRemoteDesc = pc.SetRemoteDescription(ref _desc);
yield return opRemoteDesc;
if (opRemoteDesc.IsError)
{
Debug.LogError($"Network Error: {opRemoteDesc.Error.message}");
yield break;
}
pc.SetRemoteDescription(ref _desc);
foreach (var track in m_listVideoStreamTrack.Concat(m_audioStream.GetTracks()))
{
RTCRtpSender sender = pc.AddTrack(track);
@ -197,29 +225,131 @@ namespace Unity.RenderStreaming
RTCAnswerOptions options = default;
var op = pc.CreateAnswer(ref options);
while (op.MoveNext())
{
}
yield return op;
if (op.IsError)
{
Debug.LogError($"Network Error: {op.Error}");
return;
Debug.LogError($"Network Error: {op.Error.message}");
yield break;
}
var desc = op.Desc;
var opLocalDesc = pc.SetLocalDescription(ref desc);
while (opLocalDesc.MoveNext())
{
}
yield return opLocalDesc;
if (opLocalDesc.IsError)
{
Debug.LogError($"Network Error: {opLocalDesc.Error}");
return;
Debug.LogError($"Network Error: {opLocalDesc.Error.message}");
yield break;
}
signaling.SendAnswer(connectionId, desc);
}
RTCPeerConnection CreatePeerConnection(ISignaling signaling, string connectionId, bool isOffer)
{
if (m_mapConnectionIdAndPeer.TryGetValue(connectionId, out var peer))
{
peer.Close();
}
var pc = new RTCPeerConnection();
m_mapConnectionIdAndPeer[connectionId] = pc;
pc.OnDataChannel = new DelegateOnDataChannel(channel => { OnDataChannel(pc, channel); });
pc.SetConfiguration(ref m_conf);
pc.OnIceCandidate = new DelegateOnIceCandidate(candidate =>
{
signaling.SendCandidate(connectionId, candidate);
});
pc.OnIceConnectionChange = new DelegateOnIceConnectionChange(state =>
{
if(state == RTCIceConnectionState.Disconnected)
{
pc.Close();
m_mapConnectionIdAndPeer.Remove(connectionId);
}
});
pc.OnTrack = trackEvent =>
{
foreach (var receiveStream in m_listVideoReceiveStream)
{
receiveStream.AddTrack(trackEvent.Track);
}
};
pc.OnNegotiationNeeded = () => StartCoroutine(OnNegotiationNeeded(signaling, connectionId, isOffer));
return pc;
}
IEnumerator OnNegotiationNeeded(ISignaling signaling, string connectionId, bool isOffer)
{
if (!isOffer)
{
yield break;
}
if (!m_mapConnectionIdAndPeer.TryGetValue(connectionId, out var pc))
{
Debug.LogError($"connectionId: {connectionId}, did not created peerConnection");
yield break;
}
RTCOfferOptions option = new RTCOfferOptions
{
offerToReceiveAudio = true,
offerToReceiveVideo = true
};
var offerOp = pc.CreateOffer(ref option);
yield return offerOp;
if (offerOp.IsError)
{
Debug.LogError($"Network Error: {offerOp.Error.message}");
yield break;
}
if (pc.SignalingState != RTCSignalingState.Stable)
{
Debug.LogError($"peerConnection's signaling state is not stable.");
yield break;
}
var desc = offerOp.Desc;
var setLocalSdp = pc.SetLocalDescription(ref desc);
yield return setLocalSdp;
if (setLocalSdp.IsError)
{
Debug.LogError($"Network Error: {setLocalSdp.Error.message}");
yield break;
}
signaling.SendOffer(connectionId, desc);
}
IEnumerator OnAnswer(ISignaling signaling, DescData e)
{
if (!m_mapConnectionIdAndPeer.TryGetValue(e.connectionId, out var pc))
{
Debug.Log($"connectiondId:{e.connectionId}, peerConnection not exist");
yield break;
}
var desc = new RTCSessionDescription();
desc.type = RTCSdpType.Answer;
desc.sdp = e.sdp;
var opRemoteSdp = pc.SetRemoteDescription(ref desc);
yield return opRemoteSdp;
if (opRemoteSdp.IsError)
{
Debug.LogError($"Network Error: {opRemoteSdp.Error.message}");
yield break;
}
}
void OnIceCandidate(ISignaling signaling, CandidateData e)
{
if (!m_mapConnectionIdAndPeer.TryGetValue(e.connectionId, out var pc))
@ -227,7 +357,7 @@ namespace Unity.RenderStreaming
return;
}
RTCIceCandidate _candidate = default;
RTCIceCandidate _candidate = default;
_candidate.candidate = e.candidate;
_candidate.sdpMLineIndex = e.sdpMLineIndex;
_candidate.sdpMid = e.sdpMid;

Просмотреть файл

@ -28,15 +28,17 @@ namespace Unity.RenderStreaming.Signaling
{
private float m_timeout;
private bool m_running;
private SynchronizationContext m_mainThreadContext;
private Thread m_signalingThread;
private AutoResetEvent m_wsCloseEvent;
private WebSocket m_webSocket;
public delegate void OnSignedInHandler(ISignaling sender);
public FurioosSignaling(string url, float timeout)
public FurioosSignaling(string url, float timeout, SynchronizationContext mainThreadContext)
{
m_timeout = timeout;
m_mainThreadContext = mainThreadContext;
m_wsCloseEvent = new AutoResetEvent(false);
}
@ -196,7 +198,7 @@ namespace Unity.RenderStreaming.Signaling
offer.connectionId = routedMessage.from;
offer.sdp = msg.sdp;
OnOffer?.Invoke(this, offer);
m_mainThreadContext.Post(d => OnOffer?.Invoke(this, offer), null);
}
else
{
@ -214,7 +216,7 @@ namespace Unity.RenderStreaming.Signaling
candidate.sdpMLineIndex = msg.sdpMLineIndex;
candidate.sdpMid = msg.sdpMid;
OnIceCandidate?.Invoke(this, candidate);
m_mainThreadContext.Post(d => OnIceCandidate?.Invoke(this, candidate), null);
}
else
{

Просмотреть файл

@ -11,18 +11,21 @@ namespace Unity.RenderStreaming.Signaling
{
private string m_url;
private float m_timeout;
private SynchronizationContext m_mainThreadContext;
private bool m_running;
private Thread m_signalingThread;
private string m_sessionId;
private long m_lastTimeGetOfferRequest;
private long m_lastTimeGetAnswerRequest;
private long m_lastTimeGetCandidateRequest;
public HttpSignaling(string url, float timeout)
public HttpSignaling(string url, float timeout, SynchronizationContext mainThreadContext)
{
m_url = url;
m_timeout = timeout;
m_mainThreadContext = mainThreadContext;
if (m_url.StartsWith("https"))
{
@ -93,6 +96,7 @@ namespace Unity.RenderStreaming.Signaling
{
// ignore messages arrived before 30 secs ago
m_lastTimeGetOfferRequest = DateTime.UtcNow.Millisecond - 30000;
m_lastTimeGetAnswerRequest = DateTime.UtcNow.Millisecond - 30000;
m_lastTimeGetCandidateRequest = DateTime.UtcNow.Millisecond - 30000;
@ -201,7 +205,7 @@ namespace Unity.RenderStreaming.Signaling
m_sessionId = resp.sessionId;
Debug.Log("Signaling: HTTP connected, sessionId : " + m_sessionId);
OnStart?.Invoke(this);
m_mainThreadContext.Post(d => OnStart?.Invoke(this), null);
return true;
}
else
@ -261,8 +265,7 @@ namespace Unity.RenderStreaming.Signaling
m_lastTimeGetOfferRequest = DateTimeExtension.ParseHttpDate(response.Headers[HttpResponseHeader.Date])
.ToJsMilliseconds();
OnCreateConnection?.Invoke(this, data.connectionId);
m_mainThreadContext.Post(d => OnCreateConnection?.Invoke(this, data.connectionId), null);
return true;
}
@ -285,7 +288,7 @@ namespace Unity.RenderStreaming.Signaling
foreach (var offer in list.offers)
{
OnOffer?.Invoke(this, offer);
m_mainThreadContext.Post(d => OnOffer?.Invoke(this, offer), null);
}
return true;
@ -294,7 +297,7 @@ namespace Unity.RenderStreaming.Signaling
private bool HTTPGetAnswers()
{
HttpWebRequest request =
(HttpWebRequest)WebRequest.Create($"{m_url}/signaling/answer?fromtime={m_lastTimeGetOfferRequest}");
(HttpWebRequest)WebRequest.Create($"{m_url}/signaling/answer?fromtime={m_lastTimeGetAnswerRequest}");
request.Method = "GET";
request.ContentType = "application/json";
request.Headers.Add("Session-Id", m_sessionId);
@ -305,12 +308,12 @@ namespace Unity.RenderStreaming.Signaling
if (list == null) return false;
m_lastTimeGetOfferRequest = DateTimeExtension.ParseHttpDate(response.Headers[HttpResponseHeader.Date])
m_lastTimeGetAnswerRequest = DateTimeExtension.ParseHttpDate(response.Headers[HttpResponseHeader.Date])
.ToJsMilliseconds();
foreach (var answer in list.answers)
{
OnAnswer?.Invoke(this, answer);
m_mainThreadContext.Post(d => OnAnswer?.Invoke(this, answer), null);
}
return true;
@ -337,7 +340,7 @@ namespace Unity.RenderStreaming.Signaling
foreach (var candidate in candidateContainer.candidates)
{
candidate.connectionId = candidateContainer.connectionId;
OnIceCandidate?.Invoke(this, candidate);
m_mainThreadContext.Post(d => OnIceCandidate?.Invoke(this, candidate), null);
}
}

Просмотреть файл

@ -22,6 +22,6 @@ namespace Unity.RenderStreaming.Signaling
void CreateConnection();
void SendOffer(string connectionId, RTCSessionDescription answer);
void SendAnswer(string connectionId, RTCSessionDescription answer);
void SendCandidate(string connectionId, RTCIceCandidate candidate);
void SendCandidate(string connectionId, RTCIceCandidate candidate);
}
}

Просмотреть файл

@ -12,15 +12,17 @@ namespace Unity.RenderStreaming.Signaling
{
private string m_url;
private float m_timeout;
private SynchronizationContext m_mainThreadContext;
private bool m_running;
private Thread m_signalingThread;
private AutoResetEvent m_wsCloseEvent;
private WebSocket m_webSocket;
public WebSocketSignaling(string url, float timeout)
public WebSocketSignaling(string url, float timeout, SynchronizationContext mainThreadContext)
{
m_url = url;
m_timeout = timeout;
m_mainThreadContext = mainThreadContext;
m_wsCloseEvent = new AutoResetEvent(false);
}
@ -74,7 +76,7 @@ namespace Unity.RenderStreaming.Signaling
data.type = "answer";
RoutedMessage<DescData> routedMessage = new RoutedMessage<DescData>();
routedMessage.to = connectionId;
routedMessage.from = connectionId;
routedMessage.data = data;
routedMessage.type = "answer";
@ -90,7 +92,7 @@ namespace Unity.RenderStreaming.Signaling
data.sdpMid = candidate.sdpMid;
RoutedMessage<CandidateData> routedMessage = new RoutedMessage<CandidateData>();
routedMessage.to = connectionId;
routedMessage.from = connectionId;
routedMessage.data = data;
routedMessage.type = "candidate";
@ -160,56 +162,34 @@ namespace Unity.RenderStreaming.Signaling
if (routedMessage.type == "connect")
{
string connectionId = JsonUtility.FromJson<SignalingMessage>(content).connectionId;
OnCreateConnection?.Invoke(this, connectionId);
m_mainThreadContext.Post(d => OnCreateConnection?.Invoke(this, connectionId), null);
}
else if (routedMessage.type == "offer")
{
if (!string.IsNullOrEmpty(routedMessage.from))
{
DescData offer = new DescData();
offer.connectionId = routedMessage.from;
offer.sdp = msg.sdp;
OnOffer?.Invoke(this, offer);
}
else
{
Debug.LogError("Signaling: Received message from unknown peer");
}
DescData offer = new DescData();
offer.connectionId = routedMessage.from;
offer.sdp = msg.sdp;
m_mainThreadContext.Post(d => OnOffer?.Invoke(this, offer), null);
}
else if (routedMessage.type == "answer")
{
if (!string.IsNullOrEmpty(routedMessage.from))
DescData answer = new DescData
{
DescData answer = new DescData
{
connectionId = routedMessage.from,
sdp = msg.sdp
};
OnAnswer?.Invoke(this, answer);
}
else
{
Debug.LogError("Signaling: Received message from unknown peer");
}
connectionId = routedMessage.from,
sdp = msg.sdp
};
m_mainThreadContext.Post(d => OnAnswer?.Invoke(this, answer), null);
}
else if (routedMessage.type == "candidate")
{
if (!string.IsNullOrEmpty(routedMessage.from))
CandidateData candidate = new CandidateData
{
CandidateData candidate = new CandidateData
{
connectionId = routedMessage.@from,
candidate = msg.candidate,
sdpMLineIndex = msg.sdpMLineIndex,
sdpMid = msg.sdpMid
};
OnIceCandidate?.Invoke(this, candidate);
}
else
{
Debug.LogError("Signaling: Received message from unknown peer");
}
connectionId = routedMessage.@from,
candidate = msg.candidate,
sdpMLineIndex = msg.sdpMLineIndex,
sdpMid = msg.sdpMid
};
m_mainThreadContext.Post(d => OnIceCandidate?.Invoke(this, candidate), null);
}
}
}
@ -222,7 +202,7 @@ namespace Unity.RenderStreaming.Signaling
private void WSConnected(object sender, EventArgs e)
{
Debug.Log("Signaling: WS connected.");
OnStart?.Invoke(this);
m_mainThreadContext.Post(d => OnStart?.Invoke(this), null);
}

Просмотреть файл

@ -37,8 +37,9 @@ namespace Unity.RenderStreaming
private Process m_ServerProcess;
private RTCSessionDescription m_DescOffer;
private RTCSessionDescription m_DescAnswer;
private RTCIceCandidate m_candidate;
private RTCIceCandidate m_candidate;
private SynchronizationContext m_Context;
private ISignaling signaling1;
private ISignaling signaling2;
@ -104,15 +105,15 @@ namespace Unity.RenderStreaming
m_ServerProcess.Kill();
}
ISignaling CreateSignaling(Type type)
ISignaling CreateSignaling(Type type, SynchronizationContext mainThread)
{
if (type == typeof(WebSocketSignaling))
{
return new WebSocketSignaling("ws://localhost", 0.1f);
return new WebSocketSignaling("ws://localhost", 0.1f, mainThread);
}
if (type == typeof(HttpSignaling))
{
return new HttpSignaling("http://localhost", 0.1f);
return new HttpSignaling("http://localhost", 0.1f, mainThread);
}
throw new ArgumentException();
}
@ -123,7 +124,7 @@ namespace Unity.RenderStreaming
WebRTC.WebRTC.Initialize();
RTCConfiguration config = default;
RTCIceCandidate? candidate_ = null;
RTCIceCandidate? candidate_ = null;
config.iceServers = new[] { new RTCIceServer { urls = new[] { "stun:stun.l.google.com:19302" } } };
var peer1 = new RTCPeerConnection(ref config);
@ -158,8 +159,9 @@ namespace Unity.RenderStreaming
peer1.Close();
peer2.Close();
signaling1 = CreateSignaling(m_SignalingType);
signaling2 = CreateSignaling(m_SignalingType);
m_Context = SynchronizationContext.Current;
signaling1 = CreateSignaling(m_SignalingType, m_Context);
signaling2 = CreateSignaling(m_SignalingType, m_Context);
}
[TearDown]
@ -169,50 +171,51 @@ namespace Unity.RenderStreaming
signaling1.Stop();
signaling2.Stop();
m_Context = null;
}
[Test]
public void OnConnect()
[UnityTest]
public IEnumerator OnConnect()
{
bool startRaised1 = false;
string connectionId1 = null;
signaling1.Start();
signaling1.OnStart += s => { startRaised1 = true; };
Assert.True(Wait(() => startRaised1));
signaling1.Start();
yield return new WaitUntil(() => startRaised1);
signaling1.OnCreateConnection += (s, connectionId) => { connectionId1 = connectionId; };
signaling1.CreateConnection();
Assert.True(Wait(() => !string.IsNullOrEmpty(connectionId1)));
yield return new WaitUntil(() => !string.IsNullOrEmpty(connectionId1));
Assert.IsNotEmpty(connectionId1);
}
[Test]
public void OnOffer()
[UnityTest]
public IEnumerator OnOffer()
{
bool startRaised1 = false;
bool startRaised2 = false;
bool offerRaised = false;
string connectionId1 = null;
signaling1.Start();
signaling2.Start();
signaling1.OnStart += s => { startRaised1 = true; };
signaling2.OnStart += s => { startRaised2 = true; };
Assert.True(Wait(() => startRaised1 && startRaised2));
signaling1.Start();
signaling2.Start();
yield return new WaitUntil(() => startRaised1 && startRaised2);
signaling1.OnCreateConnection += (s, connectionId) => { connectionId1 = connectionId; };
signaling1.CreateConnection();
Assert.True(Wait(() => !string.IsNullOrEmpty(connectionId1)));
yield return new WaitUntil(() => !string.IsNullOrEmpty(connectionId1));
signaling2.OnOffer += (s, e) => { offerRaised = true; };
signaling1.SendOffer(connectionId1, m_DescOffer);
Assert.True(Wait(() => offerRaised));
yield return new WaitUntil(() => offerRaised);
}
[Test]
public void OnAnswer()
[UnityTest]
public IEnumerator OnAnswer()
{
bool startRaised1 = false;
bool startRaised2 = false;
@ -220,27 +223,27 @@ namespace Unity.RenderStreaming
bool answerRaised = false;
string connectionId1 = null;
signaling1.Start();
signaling2.Start();
signaling1.OnStart += s => { startRaised1 = true; };
signaling2.OnStart += s => { startRaised2 = true; };
Assert.True(Wait(() => startRaised1 && startRaised2));
signaling1.Start();
signaling2.Start();
yield return new WaitUntil(() => startRaised1 && startRaised2);
signaling1.OnCreateConnection += (s, connectionId) => { connectionId1 = connectionId; };
signaling1.CreateConnection();
Assert.True(Wait(() => !string.IsNullOrEmpty(connectionId1)));
yield return new WaitUntil(() => !string.IsNullOrEmpty(connectionId1));
signaling2.OnOffer += (s, e) => { offerRaised = true; };
signaling1.SendOffer(connectionId1, m_DescOffer);
Assert.True(Wait(() => offerRaised));
yield return new WaitUntil(() => offerRaised);
signaling1.OnAnswer += (s, e) => { answerRaised = true; };
signaling2.SendAnswer(connectionId1, m_DescAnswer);
Assert.True(Wait(() => answerRaised));
yield return new WaitUntil(() => answerRaised);
}
[Test]
public void OnCandidate()
[UnityTest]
public IEnumerator OnCandidate()
{
bool startRaised1 = false;
bool startRaised2 = false;
@ -250,31 +253,31 @@ namespace Unity.RenderStreaming
bool candidateRaised2 = false;
string connectionId1 = null;
signaling1.Start();
signaling2.Start();
signaling1.OnStart += s => { startRaised1 = true; };
signaling2.OnStart += s => { startRaised2 = true; };
Assert.True(Wait(() => startRaised1 && startRaised2));
signaling1.Start();
signaling2.Start();
yield return new WaitUntil(() => startRaised1 && startRaised2);
signaling1.OnCreateConnection += (s, connectionId) => { connectionId1 = connectionId; };
signaling1.CreateConnection();
Assert.True(Wait(() => !string.IsNullOrEmpty(connectionId1)));
yield return new WaitUntil(() => !string.IsNullOrEmpty(connectionId1));
signaling2.OnOffer += (s, e) => { offerRaised = true; };
signaling1.SendOffer(connectionId1, m_DescOffer);
Assert.True(Wait(() => offerRaised));
yield return new WaitUntil(() => offerRaised);
signaling1.OnAnswer += (s, e) => { answerRaised = true; };
signaling2.SendAnswer(connectionId1, m_DescAnswer);
Assert.True(Wait(() => answerRaised));
yield return new WaitUntil(() => answerRaised);
signaling2.OnIceCandidate += (s, e) => { candidateRaised1 = true; };
signaling1.SendCandidate(connectionId1, m_candidate);
Assert.True(Wait(() => candidateRaised1));
yield return new WaitUntil(() => candidateRaised1);
signaling1.OnIceCandidate += (s, e) => { candidateRaised2 = true; };
signaling2.SendCandidate(connectionId1, m_candidate);
Assert.True(Wait(() => candidateRaised2));
yield return new WaitUntil(() => candidateRaised2);
}
}
}

Просмотреть файл

@ -1,2 +1,2 @@
m_EditorVersion: 2019.4.7f1
m_EditorVersionWithRevision: 2019.4.7f1 (e992b1a16e65)
m_EditorVersion: 2019.4.8f1
m_EditorVersionWithRevision: 2019.4.8f1 (60781d942082)