zhyz-cloudrender-v5 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/adapter.js +3364 -0
- package/cloudRender.es.js +24523 -0
- package/cloudRender.js +24529 -0
- package/cloudRender.umd.js +24531 -0
- package/p2p.js +1426 -0
- package/package.json +11 -0
- package/webRtcPlayer.js +575 -0
package/package.json
ADDED
package/webRtcPlayer.js
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
2
|
+
// universal module definition - read https://www.davidbcalhoun.com/2014/what-is-amd-commonjs-and-umd/
|
|
3
|
+
|
|
4
|
+
(function (root, factory) {
|
|
5
|
+
if (typeof define === 'function' && define.amd) {
|
|
6
|
+
// AMD. Register as an anonymous module.
|
|
7
|
+
define(["./adapter"], factory);
|
|
8
|
+
} else if (typeof exports === 'object') {
|
|
9
|
+
// Node. Does not work with strict CommonJS, but
|
|
10
|
+
// only CommonJS-like environments that support module.exports,
|
|
11
|
+
// like Node.
|
|
12
|
+
module.exports = factory(require("./adapter"));
|
|
13
|
+
} else {
|
|
14
|
+
// Browser globals (root is window)
|
|
15
|
+
root.webRtcPlayer = factory(root.adapter);
|
|
16
|
+
}
|
|
17
|
+
}(this, function (adapter) {
|
|
18
|
+
|
|
19
|
+
function webRtcPlayer(parOptions) {
|
|
20
|
+
parOptions = typeof parOptions !== 'undefined' ? parOptions : {};
|
|
21
|
+
|
|
22
|
+
var self = this;
|
|
23
|
+
|
|
24
|
+
//**********************
|
|
25
|
+
//Config setup
|
|
26
|
+
//**********************
|
|
27
|
+
this.cfg = {};// typeof parOptions.peerConnectionOptions !== 'undefined' ? parOptions.peerConnectionOptions : {};
|
|
28
|
+
this.cfg.sdpSemantics = 'unified-plan';
|
|
29
|
+
// this.cfg.rtcAudioJitterBufferMaxPackets = 10;
|
|
30
|
+
// this.cfg.rtcAudioJitterBufferFastAccelerate = true;
|
|
31
|
+
// this.cfg.rtcAudioJitterBufferMinDelayMs = 0;
|
|
32
|
+
|
|
33
|
+
// If this is true in Chrome 89+ SDP is sent that is incompatible with UE Pixel Streaming 4.26 and below.
|
|
34
|
+
// However 4.27 Pixel Streaming does not need this set to false as it supports `offerExtmapAllowMixed`.
|
|
35
|
+
// tdlr; uncomment this line for older versions of Pixel Streaming that need Chrome 89+.
|
|
36
|
+
this.cfg.offerExtmapAllowMixed = false;
|
|
37
|
+
|
|
38
|
+
//**********************
|
|
39
|
+
//Variables
|
|
40
|
+
//**********************
|
|
41
|
+
this.pcClient = null;
|
|
42
|
+
this.dcClient = null;
|
|
43
|
+
this.tnClient = null;
|
|
44
|
+
|
|
45
|
+
this.sdpConstraints = {
|
|
46
|
+
offerToReceiveAudio: 1, //Note: if you don't need audio you can get improved latency by turning this off.
|
|
47
|
+
offerToReceiveVideo: 1,
|
|
48
|
+
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// See https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit for values (this is needed for Firefox to be consistent with Chrome.)
|
|
52
|
+
this.dataChannelOptions = {ordered: true};
|
|
53
|
+
|
|
54
|
+
// This is useful if the video/audio needs to autoplay (without user input) as browsers do not allow autoplay non-muted of sound sources without user interaction.
|
|
55
|
+
this.startVideoMuted = true; //typeof parOptions.startVideoMuted !== 'undefined' ? parOptions.startVideoMuted : false;
|
|
56
|
+
this.autoPlayAudio = true; //typeof parOptions.autoPlayAudio !== 'undefined' ? parOptions.autoPlayAudio : true;
|
|
57
|
+
|
|
58
|
+
// To enable mic in browser use SSL/localhost and have ?useMic in the query string.
|
|
59
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
60
|
+
this.useMic = urlParams.has('useMic');
|
|
61
|
+
if(!this.useMic)
|
|
62
|
+
{
|
|
63
|
+
// console.log("Microphone access is not enabled. Pass ?useMic in the url to enable it.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// When ?useMic check for SSL or localhost
|
|
67
|
+
let isLocalhostConnection = location.hostname === "localhost" || location.hostname === "127.0.0.1";
|
|
68
|
+
let isHttpsConnection = location.protocol === 'https:';
|
|
69
|
+
if(this.useMic && !isLocalhostConnection && !isHttpsConnection)
|
|
70
|
+
{
|
|
71
|
+
this.useMic = false;
|
|
72
|
+
console.error("Microphone access in the browser will not work if you are not on HTTPS or localhost. Disabling mic access.");
|
|
73
|
+
console.error("For testing you can enable HTTP microphone access Chrome by visiting chrome://flags/ and enabling 'unsafely-treat-insecure-origin-as-secure'");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Latency tester
|
|
77
|
+
this.latencyTestTimings =
|
|
78
|
+
{
|
|
79
|
+
TestStartTimeMs: null,
|
|
80
|
+
UEReceiptTimeMs: null,
|
|
81
|
+
UEPreCaptureTimeMs: null,
|
|
82
|
+
UEPostCaptureTimeMs: null,
|
|
83
|
+
UEPreEncodeTimeMs: null,
|
|
84
|
+
UEPostEncodeTimeMs: null,
|
|
85
|
+
UETransmissionTimeMs: null,
|
|
86
|
+
BrowserReceiptTimeMs: null,
|
|
87
|
+
FrameDisplayDeltaTimeMs: null,
|
|
88
|
+
Reset: function()
|
|
89
|
+
{
|
|
90
|
+
this.TestStartTimeMs = null;
|
|
91
|
+
this.UEReceiptTimeMs = null;
|
|
92
|
+
this.UEPreCaptureTimeMs = null;
|
|
93
|
+
this.UEPostCaptureTimeMs = null;
|
|
94
|
+
this.UEPreEncodeTimeMs = null;
|
|
95
|
+
this.UEPostEncodeTimeMs = null;
|
|
96
|
+
this.UETransmissionTimeMs = null;
|
|
97
|
+
this.BrowserReceiptTimeMs = null;
|
|
98
|
+
this.FrameDisplayDeltaTimeMs = null;
|
|
99
|
+
},
|
|
100
|
+
SetUETimings: function(UETimings)
|
|
101
|
+
{
|
|
102
|
+
this.UEReceiptTimeMs = UETimings.ReceiptTimeMs;
|
|
103
|
+
this.UEPreCaptureTimeMs = UETimings.PreCaptureTimeMs;
|
|
104
|
+
this.UEPostCaptureTimeMs = UETimings.PostCaptureTimeMs;
|
|
105
|
+
this.UEPreEncodeTimeMs = UETimings.PreEncodeTimeMs;
|
|
106
|
+
this.UEPostEncodeTimeMs = UETimings.PostEncodeTimeMs;
|
|
107
|
+
this.UETransmissionTimeMs = UETimings.TransmissionTimeMs;
|
|
108
|
+
this.BrowserReceiptTimeMs = Date.now();
|
|
109
|
+
this.OnAllLatencyTimingsReady(this);
|
|
110
|
+
},
|
|
111
|
+
SetFrameDisplayDeltaTime: function(DeltaTimeMs)
|
|
112
|
+
{
|
|
113
|
+
if(this.FrameDisplayDeltaTimeMs == null)
|
|
114
|
+
{
|
|
115
|
+
this.FrameDisplayDeltaTimeMs = Math.round(DeltaTimeMs);
|
|
116
|
+
this.OnAllLatencyTimingsReady(this);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
OnAllLatencyTimingsReady: function(Timings){}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
//**********************
|
|
123
|
+
//Functions
|
|
124
|
+
//**********************
|
|
125
|
+
|
|
126
|
+
//Create Video element and expose that as a parameter
|
|
127
|
+
this.createWebRtcVideo = function() {
|
|
128
|
+
if(document.getElementById('streamingVideo')){
|
|
129
|
+
return document.getElementById('streamingVideo');
|
|
130
|
+
}
|
|
131
|
+
var video = document.createElement('video');
|
|
132
|
+
console.log('----> create video !!!');
|
|
133
|
+
video.id = "streamingVideo";
|
|
134
|
+
video.playsInline = true;
|
|
135
|
+
video.disablepictureinpicture = true;
|
|
136
|
+
video.muted = self.startVideoMuted;
|
|
137
|
+
// video.hidden = true;
|
|
138
|
+
video.style.objectFit = 'fill';
|
|
139
|
+
video.addEventListener('loadedmetadata', function(e){
|
|
140
|
+
if(self.onVideoInitialised){
|
|
141
|
+
self.onVideoInitialised();
|
|
142
|
+
}
|
|
143
|
+
}, true);
|
|
144
|
+
video.addEventListener('contextmenu', function( e ) {
|
|
145
|
+
e.preventDefault()
|
|
146
|
+
})
|
|
147
|
+
// Check if request video frame callback is supported
|
|
148
|
+
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
|
|
149
|
+
// The API is supported!
|
|
150
|
+
|
|
151
|
+
const onVideoFrameReady = (now, metadata) => {
|
|
152
|
+
|
|
153
|
+
if(metadata.receiveTime && metadata.expectedDisplayTime)
|
|
154
|
+
{
|
|
155
|
+
const receiveToCompositeMs = metadata.presentationTime - metadata.receiveTime;
|
|
156
|
+
self.aggregatedStats.receiveToCompositeMs = receiveToCompositeMs;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
// Re-register the callback to be notified about the next frame.
|
|
161
|
+
video.requestVideoFrameCallback(onVideoFrameReady);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Initially register the callback to be notified about the first frame.
|
|
165
|
+
video.requestVideoFrameCallback(onVideoFrameReady);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return video;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.offsecreenvideo = this.createWebRtcVideo();
|
|
172
|
+
|
|
173
|
+
let onsignalingstatechange = function(state)
|
|
174
|
+
{
|
|
175
|
+
// console.log('iceconnectionstatechange ---> [' + self.pcClient.iceConnectionState + '] ^_^ !!!');
|
|
176
|
+
// console.info('signaling state change:', state)
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
let oniceconnectionstatechange = function(state) {
|
|
180
|
+
// console.log('iceconnectionstatechange ---> [' + self.pcClient.iceConnectionState + '] ^_^ !!!');
|
|
181
|
+
// console.info('ice connection state change:', state)
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
let onicegatheringstatechange = function(state) {
|
|
185
|
+
// console.log('iceconnectionstatechange ---> [' + self.pcClient.iceConnectionState + '] ^_^ !!!');
|
|
186
|
+
// console.info('ice gathering state change:', state)
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
let handleOnTrack = function(e) {
|
|
190
|
+
// console.log('handleOnTrack', e.streams);
|
|
191
|
+
|
|
192
|
+
if (e.track)
|
|
193
|
+
{
|
|
194
|
+
// console.log('Got track - ' + e.track.kind + ' id=' + e.track.id + ' readyState=' + e.track.readyState);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if(e.track.kind == "audio")
|
|
198
|
+
{
|
|
199
|
+
handleOnAudioTrack(e.streams[0]);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
else(e.track.kind == "video" && self.offsecreenvideo.srcObject !== e.streams[0])
|
|
203
|
+
{
|
|
204
|
+
self.offsecreenvideo.srcObject = e.streams[0];
|
|
205
|
+
// console.log('Set video source from video track ontrack.');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
let handleOnAudioTrack = function(audioMediaStream)
|
|
212
|
+
{
|
|
213
|
+
// do nothing the video has the same media stream as the audio track we have here (they are linked)
|
|
214
|
+
if(self.offsecreenvideo.srcObject == audioMediaStream)
|
|
215
|
+
{
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// video element has some other media stream that is not associated with this audio track
|
|
219
|
+
else if(self.offsecreenvideo.srcObject && self.offsecreenvideo.srcObject !== audioMediaStream)
|
|
220
|
+
{
|
|
221
|
+
// create a new audio element
|
|
222
|
+
let audioElem = document.createElement("Audio");
|
|
223
|
+
audioElem.srcObject = audioMediaStream;
|
|
224
|
+
|
|
225
|
+
// there is no way to autoplay audio (even muted), so we defer audio until first click
|
|
226
|
+
if(!self.autoPlayAudio) {
|
|
227
|
+
|
|
228
|
+
let clickToPlayAudio = function() {
|
|
229
|
+
audioElem && audioElem.play();
|
|
230
|
+
self.offsecreenvideo.removeEventListener("click", clickToPlayAudio);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
self.offsecreenvideo.addEventListener("click", clickToPlayAudio);
|
|
234
|
+
}
|
|
235
|
+
// we assume the user has clicked somewhere on the page and autoplaying audio will work
|
|
236
|
+
else {
|
|
237
|
+
audioElem && audioElem.play();
|
|
238
|
+
}
|
|
239
|
+
// console.log('Created new audio element to play seperate audio stream.');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let setupDataChannel = function(pc, label, options) {
|
|
245
|
+
try {
|
|
246
|
+
let datachannel = pc.createDataChannel(label, options);
|
|
247
|
+
// console.log(`Created datachannel (${label})`)
|
|
248
|
+
|
|
249
|
+
// Inform browser we would like binary data as an ArrayBuffer (FF chooses Blob by default!)
|
|
250
|
+
datachannel.binaryType = "arraybuffer";
|
|
251
|
+
|
|
252
|
+
datachannel.onopen = function (e) {
|
|
253
|
+
// console.log(`data channel (${label}) connect`)
|
|
254
|
+
if(self.onDataChannelConnected){
|
|
255
|
+
self.onDataChannelConnected();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
datachannel.onclose = function (e) {
|
|
260
|
+
// console.log(`data channel (${label}) closed`)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
datachannel.onmessage = function (e) {
|
|
264
|
+
// console.log(`Got message (${label})`, e.data)
|
|
265
|
+
if (self.onDataChannelMessage)
|
|
266
|
+
self.onDataChannelMessage(e.data);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return datachannel;
|
|
270
|
+
} catch (e) {
|
|
271
|
+
console.warn('No data channel', e);
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let onicecandidate = function (e) {
|
|
277
|
+
// console.log('ICE candidate', e)
|
|
278
|
+
if (e.candidate && e.candidate.candidate) {
|
|
279
|
+
self.onWebRtcCandidate(0, e.candidate);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
let handleCreateOffer = function (pc) {
|
|
284
|
+
pc.createOffer( ).then(function (offer) {
|
|
285
|
+
|
|
286
|
+
// Munging is where we modifying the sdp string to set parameters that are not exposed to the browser's WebRTC API
|
|
287
|
+
// mungeSDPOffer(offer);
|
|
288
|
+
// console.log('offer =', offer);
|
|
289
|
+
// Set our munged SDP on the local peer connection so it is "set" and will be send across
|
|
290
|
+
pc.setLocalDescription(offer);
|
|
291
|
+
if (self.onWebRtcOffer) {
|
|
292
|
+
// console.log('rtctype == 0-');
|
|
293
|
+
self.onWebRtcOffer(0, offer);
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
function () { console.warn("Couldn't create offer") });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let mungeSDPOffer = function (offer) {
|
|
300
|
+
|
|
301
|
+
// turn off video-timing sdp sent from browser
|
|
302
|
+
//offer.sdp = offer.sdp.replace("http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", "");
|
|
303
|
+
|
|
304
|
+
// this indicate we support stereo (Chrome needs this)
|
|
305
|
+
offer.sdp = offer.sdp.replace('useinbandfec=1', 'useinbandfec=1;stereo=1;sprop-maxcapturerate=48000');
|
|
306
|
+
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let setupPeerConnection = function (pc) {
|
|
310
|
+
if (pc.SetBitrate)
|
|
311
|
+
// console.log("Hurray! there's RTCPeerConnection.SetBitrate function");
|
|
312
|
+
|
|
313
|
+
//Setup peerConnection events
|
|
314
|
+
pc.onsignalingstatechange = onsignalingstatechange;
|
|
315
|
+
pc.oniceconnectionstatechange = oniceconnectionstatechange;
|
|
316
|
+
pc.onicegatheringstatechange = onicegatheringstatechange;
|
|
317
|
+
|
|
318
|
+
pc.ontrack = handleOnTrack;
|
|
319
|
+
pc.onicecandidate = onicecandidate;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
let generateAggregatedStatsFunction = function(){
|
|
323
|
+
if(!self.aggregatedStats)
|
|
324
|
+
self.aggregatedStats = {};
|
|
325
|
+
|
|
326
|
+
return function(stats){
|
|
327
|
+
//console.log('Printing Stats');
|
|
328
|
+
|
|
329
|
+
let newStat = {};
|
|
330
|
+
|
|
331
|
+
stats.forEach(stat => {
|
|
332
|
+
// console.log(JSON.stringify(stat, undefined, 4));
|
|
333
|
+
if (stat.type == 'inbound-rtp'
|
|
334
|
+
&& !stat.isRemote
|
|
335
|
+
&& (stat.mediaType == 'video' || stat.id.toLowerCase().includes('video'))) {
|
|
336
|
+
|
|
337
|
+
newStat.timestamp = stat.timestamp;
|
|
338
|
+
newStat.bytesReceived = stat.bytesReceived;
|
|
339
|
+
newStat.framesDecoded = stat.framesDecoded;
|
|
340
|
+
newStat.packetsLost = stat.packetsLost;
|
|
341
|
+
newStat.bytesReceivedStart = self.aggregatedStats && self.aggregatedStats.bytesReceivedStart ? self.aggregatedStats.bytesReceivedStart : stat.bytesReceived;
|
|
342
|
+
newStat.framesDecodedStart = self.aggregatedStats && self.aggregatedStats.framesDecodedStart ? self.aggregatedStats.framesDecodedStart : stat.framesDecoded;
|
|
343
|
+
newStat.timestampStart = self.aggregatedStats && self.aggregatedStats.timestampStart ? self.aggregatedStats.timestampStart : stat.timestamp;
|
|
344
|
+
|
|
345
|
+
if(self.aggregatedStats && self.aggregatedStats.timestamp){
|
|
346
|
+
if(self.aggregatedStats.bytesReceived){
|
|
347
|
+
// bitrate = bits received since last time / number of ms since last time
|
|
348
|
+
//This is automatically in kbits (where k=1000) since time is in ms and stat we want is in seconds (so a '* 1000' then a '/ 1000' would negate each other)
|
|
349
|
+
newStat.bitrate = 8 * (newStat.bytesReceived - self.aggregatedStats.bytesReceived) / (newStat.timestamp - self.aggregatedStats.timestamp);
|
|
350
|
+
newStat.bitrate = Math.floor(newStat.bitrate);
|
|
351
|
+
newStat.lowBitrate = self.aggregatedStats.lowBitrate && self.aggregatedStats.lowBitrate < newStat.bitrate ? self.aggregatedStats.lowBitrate : newStat.bitrate
|
|
352
|
+
newStat.highBitrate = self.aggregatedStats.highBitrate && self.aggregatedStats.highBitrate > newStat.bitrate ? self.aggregatedStats.highBitrate : newStat.bitrate
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if(self.aggregatedStats.bytesReceivedStart){
|
|
356
|
+
newStat.avgBitrate = 8 * (newStat.bytesReceived - self.aggregatedStats.bytesReceivedStart) / (newStat.timestamp - self.aggregatedStats.timestampStart);
|
|
357
|
+
newStat.avgBitrate = Math.floor(newStat.avgBitrate);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if(self.aggregatedStats.framesDecoded){
|
|
361
|
+
// framerate = frames decoded since last time / number of seconds since last time
|
|
362
|
+
newStat.framerate = (newStat.framesDecoded - self.aggregatedStats.framesDecoded) / ((newStat.timestamp - self.aggregatedStats.timestamp) / 1000);
|
|
363
|
+
newStat.framerate = Math.floor(newStat.framerate);
|
|
364
|
+
newStat.lowFramerate = self.aggregatedStats.lowFramerate && self.aggregatedStats.lowFramerate < newStat.framerate ? self.aggregatedStats.lowFramerate : newStat.framerate
|
|
365
|
+
newStat.highFramerate = self.aggregatedStats.highFramerate && self.aggregatedStats.highFramerate > newStat.framerate ? self.aggregatedStats.highFramerate : newStat.framerate
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if(self.aggregatedStats.framesDecodedStart){
|
|
369
|
+
newStat.avgframerate = (newStat.framesDecoded - self.aggregatedStats.framesDecodedStart) / ((newStat.timestamp - self.aggregatedStats.timestampStart) / 1000);
|
|
370
|
+
newStat.avgframerate = Math.floor(newStat.avgframerate);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
//Read video track stats
|
|
376
|
+
if(stat.type == 'track' && (stat.trackIdentifier == 'video_label' || stat.kind == 'video')) {
|
|
377
|
+
newStat.framesDropped = stat.framesDropped;
|
|
378
|
+
newStat.framesReceived = stat.framesReceived;
|
|
379
|
+
newStat.framesDroppedPercentage = stat.framesDropped / stat.framesReceived * 100;
|
|
380
|
+
newStat.frameHeight = stat.frameHeight;
|
|
381
|
+
newStat.frameWidth = stat.frameWidth;
|
|
382
|
+
newStat.frameHeightStart = self.aggregatedStats && self.aggregatedStats.frameHeightStart ? self.aggregatedStats.frameHeightStart : stat.frameHeight;
|
|
383
|
+
newStat.frameWidthStart = self.aggregatedStats && self.aggregatedStats.frameWidthStart ? self.aggregatedStats.frameWidthStart : stat.frameWidth;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if(stat.type =='candidate-pair' && stat.hasOwnProperty('currentRoundTripTime') && stat.currentRoundTripTime != 0){
|
|
387
|
+
newStat.currentRoundTripTime = stat.currentRoundTripTime;
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
if(self.aggregatedStats.receiveToCompositeMs)
|
|
393
|
+
{
|
|
394
|
+
newStat.receiveToCompositeMs = self.aggregatedStats.receiveToCompositeMs;
|
|
395
|
+
self.latencyTestTimings.SetFrameDisplayDeltaTime(self.aggregatedStats.receiveToCompositeMs);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
self.aggregatedStats = newStat;
|
|
399
|
+
|
|
400
|
+
if(self.onAggregatedStats)
|
|
401
|
+
self.onAggregatedStats(newStat)
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
let setupTracksToSendAsync = async function(pc){
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
// Setup a transceiver for sending mic audio to UE and receiving audio from UE
|
|
410
|
+
if(!self.useMic)
|
|
411
|
+
{
|
|
412
|
+
pc.addTransceiver("audio", { direction: "recvonly" });
|
|
413
|
+
}
|
|
414
|
+
else
|
|
415
|
+
{
|
|
416
|
+
let audioSendOptions = self.useMic ?
|
|
417
|
+
{
|
|
418
|
+
autoGainControl: false,
|
|
419
|
+
channelCount: 1,
|
|
420
|
+
echoCancellation: false,
|
|
421
|
+
latency: 0,
|
|
422
|
+
noiseSuppression: false,
|
|
423
|
+
sampleRate: 16000,
|
|
424
|
+
volume: 1.0
|
|
425
|
+
} : false;
|
|
426
|
+
|
|
427
|
+
// Note using mic on android chrome requires SSL or chrome://flags/ "unsafely-treat-insecure-origin-as-secure"
|
|
428
|
+
const stream = await navigator.mediaDevices.getUserMedia({video: false, audio: audioSendOptions});
|
|
429
|
+
if(stream)
|
|
430
|
+
{
|
|
431
|
+
for (const track of stream.getTracks()) {
|
|
432
|
+
if(track.kind && track.kind == "audio")
|
|
433
|
+
{
|
|
434
|
+
pc.addTransceiver(track, { direction: "sendrecv" });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
else
|
|
439
|
+
{
|
|
440
|
+
pc.addTransceiver("audio", { direction: "recvonly" });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// Setup a transceiver for getting UE video
|
|
444
|
+
pc.addTransceiver("video", { direction: "recvonly" });
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
//**********************
|
|
449
|
+
//Public functions
|
|
450
|
+
//**********************
|
|
451
|
+
|
|
452
|
+
this.setVideoEnabled = function(enabled) {
|
|
453
|
+
self.offsecreenvideo.srcObject.getTracks().forEach(track => track.enabled = enabled);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
this.startLatencyTest = function(onTestStarted) {
|
|
457
|
+
// Can't start latency test without a video element
|
|
458
|
+
if(!self.offsecreenvideo)
|
|
459
|
+
{
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
self.latencyTestTimings.Reset();
|
|
464
|
+
self.latencyTestTimings.TestStartTimeMs = Date.now();
|
|
465
|
+
onTestStarted(self.latencyTestTimings.TestStartTimeMs);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
//This is called when revceiving new ice candidates individually instead of part of the offer
|
|
469
|
+
//This is currently not used but would be called externally from this class
|
|
470
|
+
this.handleCandidateFromServer = function(iceCandidate) {
|
|
471
|
+
// console.log("ICE candidate: ", iceCandidate);
|
|
472
|
+
|
|
473
|
+
//?????Candidate??
|
|
474
|
+
// var candidate = new RTCIceCandidate({
|
|
475
|
+
// sdpMLineIndex :data.label,
|
|
476
|
+
// candidate:data.candidate
|
|
477
|
+
// });
|
|
478
|
+
//
|
|
479
|
+
let candidate = new RTCIceCandidate(iceCandidate);
|
|
480
|
+
self.pcClient.addIceCandidate(candidate).then(_=>{
|
|
481
|
+
// console.log('ICE candidate successfully added');
|
|
482
|
+
});
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
//Called externaly to create an offer for the server
|
|
486
|
+
this.createOffer = function() {
|
|
487
|
+
if(self.pcClient){
|
|
488
|
+
// console.log("Closing existing PeerConnection")
|
|
489
|
+
self.pcClient.close();
|
|
490
|
+
self.pcClient = null;
|
|
491
|
+
}
|
|
492
|
+
var pcConfig = {
|
|
493
|
+
'iceServer' : [{
|
|
494
|
+
//TURN?????
|
|
495
|
+
'urls': 'stun:stun.l.google.com:19302'
|
|
496
|
+
// TURN??????
|
|
497
|
+
//'username': 'xxx',
|
|
498
|
+
//TURN?????
|
|
499
|
+
//'credential': "xxx"
|
|
500
|
+
}],
|
|
501
|
+
// ????relay?????? turn ?? ?? ^_^
|
|
502
|
+
"iceTransportPolicy": "all",
|
|
503
|
+
//"iceTransportPolicy": "relay",
|
|
504
|
+
// ?????????
|
|
505
|
+
"bundlePolicy": "max-bundle",
|
|
506
|
+
// ???rtcp?rtp? ????Candidate ???????? ^_^
|
|
507
|
+
"rtcpMuxPolicy": "require",
|
|
508
|
+
"iceCandiatePoolSize": "0"
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
self.pcClient = new RTCPeerConnection( /*self.cfg*/);
|
|
512
|
+
|
|
513
|
+
setupTracksToSendAsync(self.pcClient).finally(function()
|
|
514
|
+
{
|
|
515
|
+
setupPeerConnection(self.pcClient);
|
|
516
|
+
self.dcClient = setupDataChannel(self.pcClient, 'rtc', self.dataChannelOptions);
|
|
517
|
+
handleCreateOffer(self.pcClient);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
//Called externaly when an answer is received from the server
|
|
523
|
+
this.receiveAnswer = function(answer) {
|
|
524
|
+
// console.log('Received answer:');
|
|
525
|
+
// console.log(answer);
|
|
526
|
+
// var answerDesc = new RTCSessionDescription({type: 'answer', sdp: answer});
|
|
527
|
+
self.pcClient.setRemoteDescription(new RTCSessionDescription(answer));
|
|
528
|
+
// console.log('setremote sdp ok !!!');
|
|
529
|
+
|
|
530
|
+
let receivers = self.pcClient.getReceivers();
|
|
531
|
+
// console.log('setremote sdp getReceivers');
|
|
532
|
+
for(let receiver of receivers)
|
|
533
|
+
{
|
|
534
|
+
receiver.playoutDelayHint = 0;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
this.close = function(){
|
|
540
|
+
if(self.pcClient){
|
|
541
|
+
// console.log("Closing existing peerClient")
|
|
542
|
+
self.pcClient.close();
|
|
543
|
+
self.pcClient = null;
|
|
544
|
+
}
|
|
545
|
+
if(self.aggregateStatsIntervalId)
|
|
546
|
+
clearInterval(self.aggregateStatsIntervalId);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
//Sends data across the datachannel
|
|
550
|
+
this.send = function(data)
|
|
551
|
+
{
|
|
552
|
+
// console.log('---------------> self.dcClient.readyState = ', self.dcClient.readyState);
|
|
553
|
+
if(self.dcClient && self.dcClient.readyState == 'open'){
|
|
554
|
+
self.dcClient.send(data);
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
this.getStats = function(onStats){
|
|
559
|
+
if(self.pcClient && onStats){
|
|
560
|
+
self.pcClient.getStats(null).then((stats) => {
|
|
561
|
+
onStats(stats);
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
this.aggregateStats = function(checkInterval){
|
|
567
|
+
let calcAggregatedStats = generateAggregatedStatsFunction();
|
|
568
|
+
let printAggregatedStats = () => { self.getStats(calcAggregatedStats); }
|
|
569
|
+
self.aggregateStatsIntervalId = setInterval(printAggregatedStats, checkInterval);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return webRtcPlayer;
|
|
574
|
+
|
|
575
|
+
}));
|