-
Notifications
You must be signed in to change notification settings - Fork 355
/
RenderStreaming.cs
270 lines (244 loc) · 9.86 KB
/
RenderStreaming.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.WebRTC;
using System.Text.RegularExpressions;
namespace Unity.RenderStreaming
{
[Serializable]
public class ButtonClickEvent : UnityEngine.Events.UnityEvent<int> { }
[Serializable]
public class ButtonClickElement
{
[Tooltip("Specifies the ID on the HTML")]
public int elementId;
public ButtonClickEvent click;
}
public class RenderStreaming : MonoBehaviour
{
#pragma warning disable 0649
[SerializeField, Tooltip("Address for signaling server")]
private string urlSignaling = "http://localhost";
[SerializeField, Tooltip("Array to set your own STUN/TURN servers")]
private RTCIceServer[] iceServers = new RTCIceServer[]
{
new RTCIceServer()
{
urls = new string[] { "stun:stun.l.google.com:19302" }
}
};
[SerializeField, Tooltip("Streaming size should match display aspect ratio")]
private Vector2Int streamingSize = new Vector2Int(1280, 720);
[SerializeField, Tooltip("Time interval for polling from signaling server")]
private float interval = 5.0f;
[SerializeField, Tooltip("Camera to capture video stream")]
private Camera captureCamera;
[SerializeField, Tooltip("Array to set your own click event")]
private ButtonClickElement[] arrayButtonClickEvent;
#pragma warning restore 0649
private Signaling signaling;
private Dictionary<string, RTCPeerConnection> pcs = new Dictionary<string, RTCPeerConnection>();
private Dictionary<RTCPeerConnection, Dictionary<int, RTCDataChannel>> mapChannels = new Dictionary<RTCPeerConnection, Dictionary<int, RTCDataChannel>>();
private RTCConfiguration conf;
private string sessionId;
private MediaStream videoStream;
private MediaStream audioStream;
public void Awake()
{
WebRTC.WebRTC.Initialize();
RemoteInput.Initialize();
RemoteInput.ActionButtonClick = OnButtonClick;
}
public void OnDestroy()
{
WebRTC.WebRTC.Finalize();
RemoteInput.Destroy();
Unity.WebRTC.Audio.Stop();
}
public IEnumerator Start()
{
if (!WebRTC.WebRTC.HWEncoderSupport)
{
yield break;
}
videoStream = captureCamera.CaptureStream(streamingSize.x, streamingSize.y);
audioStream = Unity.WebRTC.Audio.CaptureStream();
signaling = new Signaling(urlSignaling);
var opCreate = signaling.Create();
yield return opCreate;
if (opCreate.webRequest.isNetworkError)
{
Debug.LogError($"Network Error: {opCreate.webRequest.error}");
yield break;
}
var newResData = opCreate.webRequest.DownloadHandlerJson<NewResData>().GetObject();
sessionId = newResData.sessionId;
conf = default;
conf.iceServers = iceServers;
StartCoroutine(WebRTC.WebRTC.Update());
StartCoroutine(LoopPolling());
}
long lastTimeGetOfferRequest = 0;
long lastTimeGetCandidateRequest = 0;
IEnumerator LoopPolling()
{
// ignore messages arrived before 30 secs ago
lastTimeGetOfferRequest = DateTime.UtcNow.ToJsMilliseconds() - 30000;
lastTimeGetCandidateRequest = DateTime.UtcNow.ToJsMilliseconds() - 30000;
while (true)
{
yield return StartCoroutine(GetOffer());
yield return StartCoroutine(GetCandidate());
yield return new WaitForSeconds(interval);
}
}
IEnumerator GetOffer()
{
var op = signaling.GetOffer(sessionId, lastTimeGetOfferRequest);
yield return op;
if (op.webRequest.isNetworkError)
{
Debug.LogError($"Network Error: {op.webRequest.error}");
yield break;
}
var date = DateTimeExtension.ParseHttpDate(op.webRequest.GetResponseHeader("Date"));
lastTimeGetOfferRequest = date.ToJsMilliseconds();
var obj = op.webRequest.DownloadHandlerJson<OfferResDataList>().GetObject();
if (obj == null)
{
yield break;
}
foreach (var offer in obj.offers)
{
RTCSessionDescription _desc = default;
_desc.type = RTCSdpType.Offer;
_desc.sdp = offer.sdp;
var connectionId = offer.connectionId;
if (pcs.ContainsKey(connectionId))
{
continue;
}
var pc = new RTCPeerConnection();
pcs.Add(offer.connectionId, pc);
pc.OnDataChannel = new DelegateOnDataChannel(channel => { OnDataChannel(pc, channel); });
pc.SetConfiguration(ref conf);
pc.OnIceCandidate = new DelegateOnIceCandidate(candidate => { StartCoroutine(OnIceCandidate(offer.connectionId, candidate)); });
pc.OnIceConnectionChange = new DelegateOnIceConnectionChange(state =>
{
if(state == RTCIceConnectionState.Disconnected)
{
pc.Close();
}
});
//make video bit rate starts at 16000kbits, and 160000kbits at max.
string pattern = @"(a=fmtp:\d+ .*level-asymmetry-allowed=.*)\r\n";
_desc.sdp = Regex.Replace(_desc.sdp, pattern, "$1;x-google-start-bitrate=16000;x-google-max-bitrate=160000\r\n");
pc.SetRemoteDescription(ref _desc);
foreach (var track in videoStream.GetTracks())
{
pc.AddTrack(track);
}
foreach(var track in audioStream.GetTracks())
{
pc.AddTrack(track);
}
StartCoroutine(Answer(connectionId));
}
}
IEnumerator Answer(string connectionId)
{
RTCAnswerOptions options = default;
var pc = pcs[connectionId];
var op = pc.CreateAnswer(ref options);
yield return op;
if (op.isError)
{
Debug.LogError($"Network Error: {op.error}");
yield break;
}
var opLocalDesc = pc.SetLocalDescription(ref op.desc);
yield return opLocalDesc;
if (opLocalDesc.isError)
{
Debug.LogError($"Network Error: {opLocalDesc.error}");
yield break;
}
var op3 = signaling.PostAnswer(this.sessionId, connectionId, op.desc.sdp);
yield return op3;
if (op3.webRequest.isNetworkError)
{
Debug.LogError($"Network Error: {op3.webRequest.error}");
yield break;
}
}
IEnumerator GetCandidate()
{
var op = signaling.GetCandidate(sessionId, lastTimeGetCandidateRequest);
yield return op;
if (op.webRequest.isNetworkError)
{
Debug.LogError($"Network Error: {op.webRequest.error}");
yield break;
}
var date = DateTimeExtension.ParseHttpDate(op.webRequest.GetResponseHeader("Date"));
lastTimeGetCandidateRequest = date.ToJsMilliseconds();
var obj = op.webRequest.DownloadHandlerJson<CandidateContainerResDataList>().GetObject();
if (obj == null)
{
yield break;
}
foreach (var candidateContainer in obj.candidates)
{
RTCPeerConnection pc;
if (!pcs.TryGetValue(candidateContainer.connectionId, out pc))
{
continue;
}
foreach (var candidate in candidateContainer.candidates)
{
RTCIceCandidate _candidate = default;
_candidate.candidate = candidate.candidate;
_candidate.sdpMLineIndex = candidate.sdpMLineIndex;
_candidate.sdpMid = candidate.sdpMid;
pcs[candidateContainer.connectionId].AddIceCandidate(ref _candidate);
}
}
}
IEnumerator OnIceCandidate(string connectionId, RTCIceCandidate candidate)
{
var opCandidate = signaling.PostCandidate(sessionId, connectionId, candidate.candidate, candidate.sdpMid, candidate.sdpMLineIndex);
yield return opCandidate;
if (opCandidate.webRequest.isNetworkError)
{
Debug.LogError($"Network Error: {opCandidate.webRequest.error}");
yield break;
}
}
void OnDataChannel(RTCPeerConnection pc, RTCDataChannel channel)
{
Dictionary<int, RTCDataChannel> channels;
if (!mapChannels.TryGetValue(pc, out channels))
{
channels = new Dictionary<int, RTCDataChannel>();
mapChannels.Add(pc, channels);
}
channels.Add(channel.Id, channel);
if(channel.Label == "data")
{
channel.OnMessage = new DelegateOnMessage(bytes => { RemoteInput.ProcessInput(bytes); });
channel.OnClose = new DelegateOnClose(() => { RemoteInput.Reset(); });
}
}
void OnButtonClick(int elementId)
{
foreach (var element in arrayButtonClickEvent)
{
if (element.elementId == elementId)
{
element.click.Invoke(elementId);
}
}
}
}
}