proctoring 2.0.2

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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +245 -0
  4. data/Rakefile +32 -0
  5. data/app/assets/config/100ms_manifest.js +1 -0
  6. data/app/assets/config/knights_watch_manifest.js +4 -0
  7. data/app/assets/config/kurento_manifest.js +1 -0
  8. data/app/assets/config/proctoring_manifest.js +3 -0
  9. data/app/assets/config/videojs_manifest.js +2 -0
  10. data/app/assets/images/proctoring/poster.png +0 -0
  11. data/app/assets/javascripts/100ms/examine.js +27 -0
  12. data/app/assets/javascripts/100ms/hundred_ms.js +143 -0
  13. data/app/assets/javascripts/100ms/join_proctor_room.js +17 -0
  14. data/app/assets/javascripts/100ms/proctor.js +152 -0
  15. data/app/assets/javascripts/kurento/LiveVideoUsingSignalingServer.js +344 -0
  16. data/app/assets/javascripts/kurento/VideoPlayer.js +63 -0
  17. data/app/assets/javascripts/kurento/VideoRecording.js +286 -0
  18. data/app/assets/javascripts/kurento/VideoRecordingUsingSignalingServer.js +224 -0
  19. data/app/assets/javascripts/kurento/co.js +299 -0
  20. data/app/assets/javascripts/kurento/kurento-utils.js +4418 -0
  21. data/app/assets/javascripts/proctoring/stream_channel.js +66 -0
  22. data/app/assets/javascripts/proctoring/stream_room.js +131 -0
  23. data/app/assets/javascripts/proctoring/video_recorder.js +172 -0
  24. data/app/assets/javascripts/videojs/videojs-playlist-ui.js +516 -0
  25. data/app/assets/javascripts/videojs/videojs-playlist.js +909 -0
  26. data/app/assets/stylesheets/proctoring/application.css +15 -0
  27. data/app/assets/stylesheets/proctoring/video_player_100ms.css +34 -0
  28. data/app/assets/stylesheets/proctoring/video_streamings.css +49 -0
  29. data/app/assets/stylesheets/scaffold.css +80 -0
  30. data/app/assets/stylesheets/videojs/videojs-playlist-ui.css +1 -0
  31. data/app/controllers/proctoring/api/v1/authentication_controller.rb +31 -0
  32. data/app/controllers/proctoring/api/v1/hundred_ms/services_controller.rb +24 -0
  33. data/app/controllers/proctoring/application_controller.rb +23 -0
  34. data/app/controllers/proctoring/video_streamings_controller.rb +108 -0
  35. data/app/helpers/proctoring/application_helper.rb +4 -0
  36. data/app/helpers/proctoring/hundred_ms_service_helper.rb +90 -0
  37. data/app/helpers/proctoring/tokens_helper.rb +55 -0
  38. data/app/helpers/proctoring/video_streamings_helper.rb +4 -0
  39. data/app/jobs/proctoring/application_job.rb +4 -0
  40. data/app/mailers/proctoring/application_mailer.rb +6 -0
  41. data/app/models/proctoring/application_record.rb +5 -0
  42. data/app/models/proctoring/video_streaming.rb +54 -0
  43. data/app/models/proctoring/video_streaming_room.rb +29 -0
  44. data/app/views/layouts/proctoring/application.html.erb +15 -0
  45. data/app/views/proctoring/video_player/live_video_proctoring.html.erb +84 -0
  46. data/app/views/proctoring/video_player/live_video_proctoring_100ms.html.erb +48 -0
  47. data/app/views/proctoring/video_player/video_player.html.erb +40 -0
  48. data/app/views/proctoring/video_streamings/_form.html.erb +27 -0
  49. data/app/views/proctoring/video_streamings/_list.html.erb +27 -0
  50. data/app/views/proctoring/video_streamings/_record_video_from_client.html.erb +8 -0
  51. data/app/views/proctoring/video_streamings/_socket_rtc_scripts.html.erb +5 -0
  52. data/app/views/proctoring/video_streamings/_stream_video.html.erb +8 -0
  53. data/app/views/proctoring/video_streamings/_video_recording.html.erb +3 -0
  54. data/app/views/proctoring/video_streamings/_video_recording100ms.html.erb +4 -0
  55. data/app/views/proctoring/video_streamings/distribute_channel_to_rooms.html.erb +39 -0
  56. data/app/views/proctoring/video_streamings/edit.html.erb +6 -0
  57. data/app/views/proctoring/video_streamings/event.html.erb +9 -0
  58. data/app/views/proctoring/video_streamings/index.html.erb +1 -0
  59. data/app/views/proctoring/video_streamings/new.html.erb +5 -0
  60. data/app/views/proctoring/video_streamings/show.html.erb +19 -0
  61. data/app/views/proctoring/video_streamings/stream_channel.html.erb +1 -0
  62. data/app/views/proctoring/video_streamings/stream_room.html.erb +1 -0
  63. data/config/routes.rb +24 -0
  64. data/db/migrate/20200526061313_create_proctoring_video_streamings.rb +15 -0
  65. data/db/migrate/20200527045158_create_proctoring_video_streaming_rooms.rb +13 -0
  66. data/lib/proctoring/engine.rb +41 -0
  67. data/lib/proctoring/version.rb +3 -0
  68. data/lib/proctoring.rb +5 -0
  69. data/lib/tasks/proctoring_tasks.rake +4 -0
  70. metadata +158 -0
@@ -0,0 +1,4418 @@
1
+ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.kurentoUtils = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
2
+ var freeice = require('freeice');
3
+ var inherits = require('inherits');
4
+ var UAParser = require('ua-parser-js');
5
+ var uuidv4 = require('uuid/v4');
6
+ var hark = require('hark');
7
+ var EventEmitter = require('events').EventEmitter;
8
+ var recursive = require('merge').recursive.bind(undefined, true);
9
+ var sdpTranslator = require('sdp-translator');
10
+ var logger = window.Logger || console;
11
+ try {
12
+ require('kurento-browser-extensions');
13
+ } catch (error) {
14
+ if (typeof getScreenConstraints === 'undefined') {
15
+ logger.warn('screen sharing is not available');
16
+ getScreenConstraints = function getScreenConstraints(sendSource, callback) {
17
+ callback(new Error('This library is not enabled for screen sharing'));
18
+ };
19
+ }
20
+ }
21
+ var MEDIA_CONSTRAINTS = {
22
+ audio: true,
23
+ video: {
24
+ width: 640,
25
+ framerate: 15
26
+ }
27
+ };
28
+ var ua = window && window.navigator ? window.navigator.userAgent : '';
29
+ var parser = new UAParser(ua);
30
+ var browser = parser.getBrowser();
31
+ function insertScriptSrcInHtmlDom(scriptSrc) {
32
+ var script = document.createElement('script');
33
+ script.src = scriptSrc;
34
+ var ref = document.querySelector('script');
35
+ ref.parentNode.insertBefore(script, ref);
36
+ }
37
+ function importScriptsDependsOnBrowser() {
38
+ if (browser.name === 'IE') {
39
+ insertScriptSrcInHtmlDom('https://cdn.temasys.io/adapterjs/0.15.x/adapter.debug.js');
40
+ }
41
+ }
42
+ importScriptsDependsOnBrowser();
43
+ var usePlanB = false;
44
+ if (browser.name === 'Chrome' || browser.name === 'Chromium') {
45
+ logger.debug(browser.name + ': using SDP PlanB');
46
+ usePlanB = true;
47
+ }
48
+ function noop(error) {
49
+ if (error)
50
+ logger.error(error);
51
+ }
52
+ function trackStop(track) {
53
+ track.stop && track.stop();
54
+ }
55
+ function streamStop(stream) {
56
+ stream.getTracks().forEach(trackStop);
57
+ }
58
+ var dumpSDP = function (description) {
59
+ if (typeof description === 'undefined' || description === null) {
60
+ return '';
61
+ }
62
+ return 'type: ' + description.type + '\r\n' + description.sdp;
63
+ };
64
+ function bufferizeCandidates(pc, onerror) {
65
+ var candidatesQueue = [];
66
+ function setSignalingstatechangeAccordingWwebBrowser(functionToExecute, pc) {
67
+ if (typeof AdapterJS !== 'undefined' && AdapterJS.webrtcDetectedBrowser === 'IE' && AdapterJS.webrtcDetectedVersion >= 9) {
68
+ pc.onsignalingstatechange = functionToExecute;
69
+ } else {
70
+ pc.addEventListener('signalingstatechange', functionToExecute);
71
+ }
72
+ }
73
+ var signalingstatechangeFunction = function () {
74
+ if (pc.signalingState === 'stable') {
75
+ while (candidatesQueue.length) {
76
+ var entry = candidatesQueue.shift();
77
+ pc.addIceCandidate(entry.candidate, entry.callback, entry.callback);
78
+ }
79
+ }
80
+ };
81
+ setSignalingstatechangeAccordingWwebBrowser(signalingstatechangeFunction, pc);
82
+ return function (candidate, callback) {
83
+ callback = callback || onerror;
84
+ switch (pc.signalingState) {
85
+ case 'closed':
86
+ callback(new Error('PeerConnection object is closed'));
87
+ break;
88
+ case 'stable':
89
+ if (pc.remoteDescription) {
90
+ pc.addIceCandidate(candidate, callback, callback);
91
+ break;
92
+ }
93
+ default:
94
+ candidatesQueue.push({
95
+ candidate: candidate,
96
+ callback: callback
97
+ });
98
+ }
99
+ };
100
+ }
101
+ function removeFIDFromOffer(sdp) {
102
+ var n = sdp.indexOf('a=ssrc-group:FID');
103
+ if (n > 0) {
104
+ return sdp.slice(0, n);
105
+ } else {
106
+ return sdp;
107
+ }
108
+ }
109
+ function getSimulcastInfo(videoStream) {
110
+ var videoTracks = videoStream.getVideoTracks();
111
+ if (!videoTracks.length) {
112
+ logger.warn('No video tracks available in the video stream');
113
+ return '';
114
+ }
115
+ var lines = [
116
+ 'a=x-google-flag:conference',
117
+ 'a=ssrc-group:SIM 1 2 3',
118
+ 'a=ssrc:1 cname:localVideo',
119
+ 'a=ssrc:1 msid:' + videoStream.id + ' ' + videoTracks[0].id,
120
+ 'a=ssrc:1 mslabel:' + videoStream.id,
121
+ 'a=ssrc:1 label:' + videoTracks[0].id,
122
+ 'a=ssrc:2 cname:localVideo',
123
+ 'a=ssrc:2 msid:' + videoStream.id + ' ' + videoTracks[0].id,
124
+ 'a=ssrc:2 mslabel:' + videoStream.id,
125
+ 'a=ssrc:2 label:' + videoTracks[0].id,
126
+ 'a=ssrc:3 cname:localVideo',
127
+ 'a=ssrc:3 msid:' + videoStream.id + ' ' + videoTracks[0].id,
128
+ 'a=ssrc:3 mslabel:' + videoStream.id,
129
+ 'a=ssrc:3 label:' + videoTracks[0].id
130
+ ];
131
+ lines.push('');
132
+ return lines.join('\n');
133
+ }
134
+ function sleep(milliseconds) {
135
+ var start = new Date().getTime();
136
+ for (var i = 0; i < 10000000; i++) {
137
+ if (new Date().getTime() - start > milliseconds) {
138
+ break;
139
+ }
140
+ }
141
+ }
142
+ function setIceCandidateAccordingWebBrowser(functionToExecute, pc) {
143
+ if (typeof AdapterJS !== 'undefined' && AdapterJS.webrtcDetectedBrowser === 'IE' && AdapterJS.webrtcDetectedVersion >= 9) {
144
+ pc.onicecandidate = functionToExecute;
145
+ } else {
146
+ pc.addEventListener('icecandidate', functionToExecute);
147
+ }
148
+ }
149
+ function WebRtcPeer(mode, options, callback) {
150
+ if (!(this instanceof WebRtcPeer)) {
151
+ return new WebRtcPeer(mode, options, callback);
152
+ }
153
+ WebRtcPeer.super_.call(this);
154
+ if (options instanceof Function) {
155
+ callback = options;
156
+ options = undefined;
157
+ }
158
+ options = options || {};
159
+ callback = (callback || noop).bind(this);
160
+ var self = this;
161
+ var localVideo = options.localVideo;
162
+ var remoteVideo = options.remoteVideo;
163
+ var videoStream = options.videoStream;
164
+ var audioStream = options.audioStream;
165
+ var mediaConstraints = options.mediaConstraints;
166
+ var connectionConstraints = options.connectionConstraints;
167
+ var pc = options.peerConnection;
168
+ var sendSource = options.sendSource || 'webcam';
169
+ var dataChannelConfig = options.dataChannelConfig;
170
+ var useDataChannels = options.dataChannels || false;
171
+ var dataChannel;
172
+ var guid = uuidv4();
173
+ var configuration = recursive({ iceServers: freeice() }, options.configuration);
174
+ var onicecandidate = options.onicecandidate;
175
+ if (onicecandidate)
176
+ this.on('icecandidate', onicecandidate);
177
+ var oncandidategatheringdone = options.oncandidategatheringdone;
178
+ if (oncandidategatheringdone) {
179
+ this.on('candidategatheringdone', oncandidategatheringdone);
180
+ }
181
+ var simulcast = options.simulcast;
182
+ var multistream = options.multistream;
183
+ var interop = new sdpTranslator.Interop();
184
+ var candidatesQueueOut = [];
185
+ var candidategatheringdone = false;
186
+ Object.defineProperties(this, {
187
+ 'peerConnection': {
188
+ get: function () {
189
+ return pc;
190
+ }
191
+ },
192
+ 'id': {
193
+ value: options.id || guid,
194
+ writable: false
195
+ },
196
+ 'remoteVideo': {
197
+ get: function () {
198
+ return remoteVideo;
199
+ }
200
+ },
201
+ 'localVideo': {
202
+ get: function () {
203
+ return localVideo;
204
+ }
205
+ },
206
+ 'dataChannel': {
207
+ get: function () {
208
+ return dataChannel;
209
+ }
210
+ },
211
+ 'currentFrame': {
212
+ get: function () {
213
+ if (!remoteVideo)
214
+ return;
215
+ if (remoteVideo.readyState < remoteVideo.HAVE_CURRENT_DATA)
216
+ throw new Error('No video stream data available');
217
+ var canvas = document.createElement('canvas');
218
+ canvas.width = remoteVideo.videoWidth;
219
+ canvas.height = remoteVideo.videoHeight;
220
+ canvas.getContext('2d').drawImage(remoteVideo, 0, 0);
221
+ return canvas;
222
+ }
223
+ }
224
+ });
225
+ if (!pc) {
226
+ pc = new RTCPeerConnection(configuration);
227
+ if (useDataChannels && !dataChannel) {
228
+ var dcId = 'WebRtcPeer-' + self.id;
229
+ var dcOptions = undefined;
230
+ if (dataChannelConfig) {
231
+ dcId = dataChannelConfig.id || dcId;
232
+ dcOptions = dataChannelConfig.options;
233
+ }
234
+ dataChannel = pc.createDataChannel(dcId, dcOptions);
235
+ if (dataChannelConfig) {
236
+ dataChannel.onopen = dataChannelConfig.onopen;
237
+ dataChannel.onclose = dataChannelConfig.onclose;
238
+ dataChannel.onmessage = dataChannelConfig.onmessage;
239
+ dataChannel.onbufferedamountlow = dataChannelConfig.onbufferedamountlow;
240
+ dataChannel.onerror = dataChannelConfig.onerror || noop;
241
+ }
242
+ }
243
+ }
244
+ if (!pc.getLocalStreams && pc.getSenders) {
245
+ pc.getLocalStreams = function () {
246
+ var stream = new MediaStream();
247
+ pc.getSenders().forEach(function (sender) {
248
+ stream.addTrack(sender.track);
249
+ });
250
+ return [stream];
251
+ };
252
+ }
253
+ if (!pc.getRemoteStreams && pc.getReceivers) {
254
+ pc.getRemoteStreams = function () {
255
+ var stream = new MediaStream();
256
+ pc.getReceivers().forEach(function (sender) {
257
+ stream.addTrack(sender.track);
258
+ });
259
+ return [stream];
260
+ };
261
+ }
262
+ var iceCandidateFunction = function (event) {
263
+ var candidate = event.candidate;
264
+ if (EventEmitter.listenerCount(self, 'icecandidate') || EventEmitter.listenerCount(self, 'candidategatheringdone')) {
265
+ if (candidate) {
266
+ var cand;
267
+ if (multistream && usePlanB) {
268
+ cand = interop.candidateToUnifiedPlan(candidate);
269
+ } else {
270
+ cand = candidate;
271
+ }
272
+ if (typeof AdapterJS === 'undefined') {
273
+ self.emit('icecandidate', cand);
274
+ }
275
+ candidategatheringdone = false;
276
+ } else if (!candidategatheringdone) {
277
+ if (typeof AdapterJS !== 'undefined' && AdapterJS.webrtcDetectedBrowser === 'IE' && AdapterJS.webrtcDetectedVersion >= 9) {
278
+ EventEmitter.prototype.emit('candidategatheringdone', cand);
279
+ } else {
280
+ self.emit('candidategatheringdone');
281
+ }
282
+ candidategatheringdone = true;
283
+ }
284
+ } else if (!candidategatheringdone) {
285
+ candidatesQueueOut.push(candidate);
286
+ if (!candidate)
287
+ candidategatheringdone = true;
288
+ }
289
+ };
290
+ setIceCandidateAccordingWebBrowser(iceCandidateFunction, pc);
291
+ pc.onaddstream = options.onaddstream;
292
+ pc.onnegotiationneeded = options.onnegotiationneeded;
293
+ this.on('newListener', function (event, listener) {
294
+ if (event === 'icecandidate' || event === 'candidategatheringdone') {
295
+ while (candidatesQueueOut.length) {
296
+ var candidate = candidatesQueueOut.shift();
297
+ if (!candidate === (event === 'candidategatheringdone')) {
298
+ listener(candidate);
299
+ }
300
+ }
301
+ }
302
+ });
303
+ var addIceCandidate = bufferizeCandidates(pc);
304
+ this.addIceCandidate = function (iceCandidate, callback) {
305
+ var candidate;
306
+ if (multistream && usePlanB) {
307
+ candidate = interop.candidateToPlanB(iceCandidate);
308
+ } else {
309
+ candidate = new RTCIceCandidate(iceCandidate);
310
+ }
311
+ logger.debug('Remote ICE candidate received', iceCandidate);
312
+ callback = (callback || noop).bind(this);
313
+ addIceCandidate(candidate, callback);
314
+ };
315
+ this.generateOffer = function (callback) {
316
+ callback = callback.bind(this);
317
+ if (mode === 'recvonly') {
318
+ var useAudio = mediaConstraints && typeof mediaConstraints.audio === 'boolean' ? mediaConstraints.audio : true;
319
+ var useVideo = mediaConstraints && typeof mediaConstraints.video === 'boolean' ? mediaConstraints.video : true;
320
+ if (useAudio) {
321
+ pc.addTransceiver('audio', { direction: 'recvonly' });
322
+ }
323
+ if (useVideo) {
324
+ pc.addTransceiver('video', { direction: 'recvonly' });
325
+ }
326
+ }
327
+ if (typeof AdapterJS !== 'undefined' && AdapterJS.webrtcDetectedBrowser === 'IE' && AdapterJS.webrtcDetectedVersion >= 9) {
328
+ var setLocalDescriptionOnSuccess = function () {
329
+ sleep(1000);
330
+ var localDescription = pc.localDescription;
331
+ logger.debug('Local description set\n', localDescription.sdp);
332
+ if (multistream && usePlanB) {
333
+ localDescription = interop.toUnifiedPlan(localDescription);
334
+ logger.debug('offer::origPlanB->UnifiedPlan', dumpSDP(localDescription));
335
+ }
336
+ callback(null, localDescription.sdp, self.processAnswer.bind(self));
337
+ };
338
+ var createOfferOnSuccess = function (offer) {
339
+ logger.debug('Created SDP offer');
340
+ logger.debug('Local description set\n', pc.localDescription);
341
+ pc.setLocalDescription(offer, setLocalDescriptionOnSuccess, callback);
342
+ };
343
+ pc.createOffer(createOfferOnSuccess, callback);
344
+ } else {
345
+ pc.createOffer().then(function (offer) {
346
+ logger.debug('Created SDP offer');
347
+ offer = mangleSdpToAddSimulcast(offer);
348
+ return pc.setLocalDescription(offer);
349
+ }).then(function () {
350
+ var localDescription = pc.localDescription;
351
+ logger.debug('Local description set\n', localDescription.sdp);
352
+ if (multistream && usePlanB) {
353
+ localDescription = interop.toUnifiedPlan(localDescription);
354
+ logger.debug('offer::origPlanB->UnifiedPlan', dumpSDP(localDescription));
355
+ }
356
+ callback(null, localDescription.sdp, self.processAnswer.bind(self));
357
+ }).catch(callback);
358
+ }
359
+ };
360
+ this.getLocalSessionDescriptor = function () {
361
+ return pc.localDescription;
362
+ };
363
+ this.getRemoteSessionDescriptor = function () {
364
+ return pc.remoteDescription;
365
+ };
366
+ function setRemoteVideo() {
367
+ if (remoteVideo) {
368
+ remoteVideo.pause();
369
+ var stream = pc.getRemoteStreams()[0];
370
+ remoteVideo.srcObject = stream;
371
+ logger.debug('Remote stream:', stream);
372
+ if (typeof AdapterJS !== 'undefined' && AdapterJS.webrtcDetectedBrowser === 'IE' && AdapterJS.webrtcDetectedVersion >= 9) {
373
+ remoteVideo = attachMediaStream(remoteVideo, stream);
374
+ } else {
375
+ remoteVideo.load();
376
+ }
377
+ }
378
+ }
379
+ this.showLocalVideo = function () {
380
+ localVideo.srcObject = videoStream;
381
+ localVideo.muted = true;
382
+ if (typeof AdapterJS !== 'undefined' && AdapterJS.webrtcDetectedBrowser === 'IE' && AdapterJS.webrtcDetectedVersion >= 9) {
383
+ localVideo = attachMediaStream(localVideo, videoStream);
384
+ }
385
+ };
386
+ this.send = function (data) {
387
+ if (dataChannel && dataChannel.readyState === 'open') {
388
+ dataChannel.send(data);
389
+ } else {
390
+ logger.warn('Trying to send data over a non-existing or closed data channel');
391
+ }
392
+ };
393
+ this.processAnswer = function (sdpAnswer, callback) {
394
+ callback = (callback || noop).bind(this);
395
+ var answer = new RTCSessionDescription({
396
+ type: 'answer',
397
+ sdp: sdpAnswer
398
+ });
399
+ if (multistream && usePlanB) {
400
+ var planBAnswer = interop.toPlanB(answer);
401
+ logger.debug('asnwer::planB', dumpSDP(planBAnswer));
402
+ answer = planBAnswer;
403
+ }
404
+ logger.debug('SDP answer received, setting remote description');
405
+ if (pc.signalingState === 'closed') {
406
+ return callback('PeerConnection is closed');
407
+ }
408
+ pc.setRemoteDescription(answer, function () {
409
+ setRemoteVideo();
410
+ callback();
411
+ }, callback);
412
+ };
413
+ this.processOffer = function (sdpOffer, callback) {
414
+ callback = callback.bind(this);
415
+ var offer = new RTCSessionDescription({
416
+ type: 'offer',
417
+ sdp: sdpOffer
418
+ });
419
+ if (multistream && usePlanB) {
420
+ var planBOffer = interop.toPlanB(offer);
421
+ logger.debug('offer::planB', dumpSDP(planBOffer));
422
+ offer = planBOffer;
423
+ }
424
+ logger.debug('SDP offer received, setting remote description');
425
+ if (pc.signalingState === 'closed') {
426
+ return callback('PeerConnection is closed');
427
+ }
428
+ pc.setRemoteDescription(offer).then(function () {
429
+ return setRemoteVideo();
430
+ }).then(function () {
431
+ return pc.createAnswer();
432
+ }).then(function (answer) {
433
+ answer = mangleSdpToAddSimulcast(answer);
434
+ logger.debug('Created SDP answer');
435
+ return pc.setLocalDescription(answer);
436
+ }).then(function () {
437
+ var localDescription = pc.localDescription;
438
+ if (multistream && usePlanB) {
439
+ localDescription = interop.toUnifiedPlan(localDescription);
440
+ logger.debug('answer::origPlanB->UnifiedPlan', dumpSDP(localDescription));
441
+ }
442
+ logger.debug('Local description set\n', localDescription.sdp);
443
+ callback(null, localDescription.sdp);
444
+ }).catch(callback);
445
+ };
446
+ function mangleSdpToAddSimulcast(answer) {
447
+ if (simulcast) {
448
+ if (browser.name === 'Chrome' || browser.name === 'Chromium') {
449
+ logger.debug('Adding multicast info');
450
+ answer = new RTCSessionDescription({
451
+ 'type': answer.type,
452
+ 'sdp': removeFIDFromOffer(answer.sdp) + getSimulcastInfo(videoStream)
453
+ });
454
+ } else {
455
+ logger.warn('Simulcast is only available in Chrome browser.');
456
+ }
457
+ }
458
+ return answer;
459
+ }
460
+ function start() {
461
+ if (pc.signalingState === 'closed') {
462
+ callback('The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue');
463
+ }
464
+ if (videoStream && localVideo) {
465
+ self.showLocalVideo();
466
+ }
467
+ if (videoStream) {
468
+ videoStream.getTracks().forEach(function (track) {
469
+ pc.addTrack(track, videoStream);
470
+ });
471
+ }
472
+ if (audioStream) {
473
+ audioStream.getTracks().forEach(function (track) {
474
+ pc.addTrack(track, audioStream);
475
+ });
476
+ }
477
+ var browser = parser.getBrowser();
478
+ if (mode === 'sendonly' && (browser.name === 'Chrome' || browser.name === 'Chromium') && browser.major === 39) {
479
+ mode = 'sendrecv';
480
+ }
481
+ callback();
482
+ }
483
+ if (mode !== 'recvonly' && !videoStream && !audioStream) {
484
+ function getMedia(constraints) {
485
+ if (constraints === undefined) {
486
+ constraints = MEDIA_CONSTRAINTS;
487
+ }
488
+ if (typeof AdapterJS !== 'undefined' && AdapterJS.webrtcDetectedBrowser === 'IE' && AdapterJS.webrtcDetectedVersion >= 9) {
489
+ navigator.getUserMedia(constraints, function (stream) {
490
+ videoStream = stream;
491
+ start();
492
+ }, callback);
493
+ } else {
494
+ navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {
495
+ videoStream = stream;
496
+ start();
497
+ }).catch(callback);
498
+ }
499
+ }
500
+ if (sendSource === 'webcam') {
501
+ getMedia(mediaConstraints);
502
+ } else {
503
+ getScreenConstraints(sendSource, function (error, constraints_) {
504
+ if (error)
505
+ return callback(error);
506
+ constraints = [mediaConstraints];
507
+ constraints.unshift(constraints_);
508
+ getMedia(recursive.apply(undefined, constraints));
509
+ }, guid);
510
+ }
511
+ } else {
512
+ setTimeout(start, 0);
513
+ }
514
+ this.on('_dispose', function () {
515
+ if (localVideo) {
516
+ localVideo.pause();
517
+ localVideo.srcObject = null;
518
+ if (typeof AdapterJS === 'undefined') {
519
+ localVideo.load();
520
+ }
521
+ localVideo.muted = false;
522
+ }
523
+ if (remoteVideo) {
524
+ remoteVideo.pause();
525
+ remoteVideo.srcObject = null;
526
+ if (typeof AdapterJS === 'undefined') {
527
+ remoteVideo.load();
528
+ }
529
+ }
530
+ self.removeAllListeners();
531
+ if (window.cancelChooseDesktopMedia !== undefined) {
532
+ window.cancelChooseDesktopMedia(guid);
533
+ }
534
+ });
535
+ }
536
+ inherits(WebRtcPeer, EventEmitter);
537
+ function createEnableDescriptor(type) {
538
+ var method = 'get' + type + 'Tracks';
539
+ return {
540
+ enumerable: true,
541
+ get: function () {
542
+ if (!this.peerConnection)
543
+ return;
544
+ var streams = this.peerConnection.getLocalStreams();
545
+ if (!streams.length)
546
+ return;
547
+ for (var i = 0, stream; stream = streams[i]; i++) {
548
+ var tracks = stream[method]();
549
+ for (var j = 0, track; track = tracks[j]; j++)
550
+ if (!track.enabled)
551
+ return false;
552
+ }
553
+ return true;
554
+ },
555
+ set: function (value) {
556
+ function trackSetEnable(track) {
557
+ track.enabled = value;
558
+ }
559
+ this.peerConnection.getLocalStreams().forEach(function (stream) {
560
+ stream[method]().forEach(trackSetEnable);
561
+ });
562
+ }
563
+ };
564
+ }
565
+ Object.defineProperties(WebRtcPeer.prototype, {
566
+ 'enabled': {
567
+ enumerable: true,
568
+ get: function () {
569
+ return this.audioEnabled && this.videoEnabled;
570
+ },
571
+ set: function (value) {
572
+ this.audioEnabled = this.videoEnabled = value;
573
+ }
574
+ },
575
+ 'audioEnabled': createEnableDescriptor('Audio'),
576
+ 'videoEnabled': createEnableDescriptor('Video')
577
+ });
578
+ WebRtcPeer.prototype.getLocalStream = function (index) {
579
+ if (this.peerConnection) {
580
+ return this.peerConnection.getLocalStreams()[index || 0];
581
+ }
582
+ };
583
+ WebRtcPeer.prototype.getRemoteStream = function (index) {
584
+ if (this.peerConnection) {
585
+ return this.peerConnection.getRemoteStreams()[index || 0];
586
+ }
587
+ };
588
+ WebRtcPeer.prototype.dispose = function () {
589
+ logger.debug('Disposing WebRtcPeer');
590
+ var pc = this.peerConnection;
591
+ var dc = this.dataChannel;
592
+ try {
593
+ if (dc) {
594
+ if (dc.signalingState === 'closed')
595
+ return;
596
+ dc.close();
597
+ }
598
+ if (pc) {
599
+ if (pc.signalingState === 'closed')
600
+ return;
601
+ pc.getLocalStreams().forEach(streamStop);
602
+ pc.close();
603
+ }
604
+ } catch (err) {
605
+ logger.warn('Exception disposing webrtc peer ' + err);
606
+ }
607
+ if (typeof AdapterJS === 'undefined') {
608
+ this.emit('_dispose');
609
+ }
610
+ };
611
+ function WebRtcPeerRecvonly(options, callback) {
612
+ if (!(this instanceof WebRtcPeerRecvonly)) {
613
+ return new WebRtcPeerRecvonly(options, callback);
614
+ }
615
+ WebRtcPeerRecvonly.super_.call(this, 'recvonly', options, callback);
616
+ }
617
+ inherits(WebRtcPeerRecvonly, WebRtcPeer);
618
+ function WebRtcPeerSendonly(options, callback) {
619
+ if (!(this instanceof WebRtcPeerSendonly)) {
620
+ return new WebRtcPeerSendonly(options, callback);
621
+ }
622
+ WebRtcPeerSendonly.super_.call(this, 'sendonly', options, callback);
623
+ }
624
+ inherits(WebRtcPeerSendonly, WebRtcPeer);
625
+ function WebRtcPeerSendrecv(options, callback) {
626
+ if (!(this instanceof WebRtcPeerSendrecv)) {
627
+ return new WebRtcPeerSendrecv(options, callback);
628
+ }
629
+ WebRtcPeerSendrecv.super_.call(this, 'sendrecv', options, callback);
630
+ }
631
+ inherits(WebRtcPeerSendrecv, WebRtcPeer);
632
+ function harkUtils(stream, options) {
633
+ return hark(stream, options);
634
+ }
635
+ exports.bufferizeCandidates = bufferizeCandidates;
636
+ exports.WebRtcPeerRecvonly = WebRtcPeerRecvonly;
637
+ exports.WebRtcPeerSendonly = WebRtcPeerSendonly;
638
+ exports.WebRtcPeerSendrecv = WebRtcPeerSendrecv;
639
+ exports.hark = harkUtils;
640
+ },{"events":4,"freeice":5,"hark":8,"inherits":9,"kurento-browser-extensions":10,"merge":11,"sdp-translator":18,"ua-parser-js":21,"uuid/v4":24}],2:[function(require,module,exports){
641
+ if (window.addEventListener)
642
+ module.exports = require('./index');
643
+ },{"./index":3}],3:[function(require,module,exports){
644
+ var WebRtcPeer = require('./WebRtcPeer');
645
+ exports.WebRtcPeer = WebRtcPeer;
646
+ },{"./WebRtcPeer":1}],4:[function(require,module,exports){
647
+ // Copyright Joyent, Inc. and other Node contributors.
648
+ //
649
+ // Permission is hereby granted, free of charge, to any person obtaining a
650
+ // copy of this software and associated documentation files (the
651
+ // "Software"), to deal in the Software without restriction, including
652
+ // without limitation the rights to use, copy, modify, merge, publish,
653
+ // distribute, sublicense, and/or sell copies of the Software, and to permit
654
+ // persons to whom the Software is furnished to do so, subject to the
655
+ // following conditions:
656
+ //
657
+ // The above copyright notice and this permission notice shall be included
658
+ // in all copies or substantial portions of the Software.
659
+ //
660
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
661
+ // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
662
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
663
+ // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
664
+ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
665
+ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
666
+ // USE OR OTHER DEALINGS IN THE SOFTWARE.
667
+
668
+ var objectCreate = Object.create || objectCreatePolyfill
669
+ var objectKeys = Object.keys || objectKeysPolyfill
670
+ var bind = Function.prototype.bind || functionBindPolyfill
671
+
672
+ function EventEmitter() {
673
+ if (!this._events || !Object.prototype.hasOwnProperty.call(this, '_events')) {
674
+ this._events = objectCreate(null);
675
+ this._eventsCount = 0;
676
+ }
677
+
678
+ this._maxListeners = this._maxListeners || undefined;
679
+ }
680
+ module.exports = EventEmitter;
681
+
682
+ // Backwards-compat with node 0.10.x
683
+ EventEmitter.EventEmitter = EventEmitter;
684
+
685
+ EventEmitter.prototype._events = undefined;
686
+ EventEmitter.prototype._maxListeners = undefined;
687
+
688
+ // By default EventEmitters will print a warning if more than 10 listeners are
689
+ // added to it. This is a useful default which helps finding memory leaks.
690
+ var defaultMaxListeners = 10;
691
+
692
+ var hasDefineProperty;
693
+ try {
694
+ var o = {};
695
+ if (Object.defineProperty) Object.defineProperty(o, 'x', { value: 0 });
696
+ hasDefineProperty = o.x === 0;
697
+ } catch (err) { hasDefineProperty = false }
698
+ if (hasDefineProperty) {
699
+ Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
700
+ enumerable: true,
701
+ get: function() {
702
+ return defaultMaxListeners;
703
+ },
704
+ set: function(arg) {
705
+ // check whether the input is a positive number (whose value is zero or
706
+ // greater and not a NaN).
707
+ if (typeof arg !== 'number' || arg < 0 || arg !== arg)
708
+ throw new TypeError('"defaultMaxListeners" must be a positive number');
709
+ defaultMaxListeners = arg;
710
+ }
711
+ });
712
+ } else {
713
+ EventEmitter.defaultMaxListeners = defaultMaxListeners;
714
+ }
715
+
716
+ // Obviously not all Emitters should be limited to 10. This function allows
717
+ // that to be increased. Set to zero for unlimited.
718
+ EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
719
+ if (typeof n !== 'number' || n < 0 || isNaN(n))
720
+ throw new TypeError('"n" argument must be a positive number');
721
+ this._maxListeners = n;
722
+ return this;
723
+ };
724
+
725
+ function $getMaxListeners(that) {
726
+ if (that._maxListeners === undefined)
727
+ return EventEmitter.defaultMaxListeners;
728
+ return that._maxListeners;
729
+ }
730
+
731
+ EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
732
+ return $getMaxListeners(this);
733
+ };
734
+
735
+ // These standalone emit* functions are used to optimize calling of event
736
+ // handlers for fast cases because emit() itself often has a variable number of
737
+ // arguments and can be deoptimized because of that. These functions always have
738
+ // the same number of arguments and thus do not get deoptimized, so the code
739
+ // inside them can execute faster.
740
+ function emitNone(handler, isFn, self) {
741
+ if (isFn)
742
+ handler.call(self);
743
+ else {
744
+ var len = handler.length;
745
+ var listeners = arrayClone(handler, len);
746
+ for (var i = 0; i < len; ++i)
747
+ listeners[i].call(self);
748
+ }
749
+ }
750
+ function emitOne(handler, isFn, self, arg1) {
751
+ if (isFn)
752
+ handler.call(self, arg1);
753
+ else {
754
+ var len = handler.length;
755
+ var listeners = arrayClone(handler, len);
756
+ for (var i = 0; i < len; ++i)
757
+ listeners[i].call(self, arg1);
758
+ }
759
+ }
760
+ function emitTwo(handler, isFn, self, arg1, arg2) {
761
+ if (isFn)
762
+ handler.call(self, arg1, arg2);
763
+ else {
764
+ var len = handler.length;
765
+ var listeners = arrayClone(handler, len);
766
+ for (var i = 0; i < len; ++i)
767
+ listeners[i].call(self, arg1, arg2);
768
+ }
769
+ }
770
+ function emitThree(handler, isFn, self, arg1, arg2, arg3) {
771
+ if (isFn)
772
+ handler.call(self, arg1, arg2, arg3);
773
+ else {
774
+ var len = handler.length;
775
+ var listeners = arrayClone(handler, len);
776
+ for (var i = 0; i < len; ++i)
777
+ listeners[i].call(self, arg1, arg2, arg3);
778
+ }
779
+ }
780
+
781
+ function emitMany(handler, isFn, self, args) {
782
+ if (isFn)
783
+ handler.apply(self, args);
784
+ else {
785
+ var len = handler.length;
786
+ var listeners = arrayClone(handler, len);
787
+ for (var i = 0; i < len; ++i)
788
+ listeners[i].apply(self, args);
789
+ }
790
+ }
791
+
792
+ EventEmitter.prototype.emit = function emit(type) {
793
+ var er, handler, len, args, i, events;
794
+ var doError = (type === 'error');
795
+
796
+ events = this._events;
797
+ if (events)
798
+ doError = (doError && events.error == null);
799
+ else if (!doError)
800
+ return false;
801
+
802
+ // If there is no 'error' event listener then throw.
803
+ if (doError) {
804
+ if (arguments.length > 1)
805
+ er = arguments[1];
806
+ if (er instanceof Error) {
807
+ throw er; // Unhandled 'error' event
808
+ } else {
809
+ // At least give some kind of context to the user
810
+ var err = new Error('Unhandled "error" event. (' + er + ')');
811
+ err.context = er;
812
+ throw err;
813
+ }
814
+ return false;
815
+ }
816
+
817
+ handler = events[type];
818
+
819
+ if (!handler)
820
+ return false;
821
+
822
+ var isFn = typeof handler === 'function';
823
+ len = arguments.length;
824
+ switch (len) {
825
+ // fast cases
826
+ case 1:
827
+ emitNone(handler, isFn, this);
828
+ break;
829
+ case 2:
830
+ emitOne(handler, isFn, this, arguments[1]);
831
+ break;
832
+ case 3:
833
+ emitTwo(handler, isFn, this, arguments[1], arguments[2]);
834
+ break;
835
+ case 4:
836
+ emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]);
837
+ break;
838
+ // slower
839
+ default:
840
+ args = new Array(len - 1);
841
+ for (i = 1; i < len; i++)
842
+ args[i - 1] = arguments[i];
843
+ emitMany(handler, isFn, this, args);
844
+ }
845
+
846
+ return true;
847
+ };
848
+
849
+ function _addListener(target, type, listener, prepend) {
850
+ var m;
851
+ var events;
852
+ var existing;
853
+
854
+ if (typeof listener !== 'function')
855
+ throw new TypeError('"listener" argument must be a function');
856
+
857
+ events = target._events;
858
+ if (!events) {
859
+ events = target._events = objectCreate(null);
860
+ target._eventsCount = 0;
861
+ } else {
862
+ // To avoid recursion in the case that type === "newListener"! Before
863
+ // adding it to the listeners, first emit "newListener".
864
+ if (events.newListener) {
865
+ target.emit('newListener', type,
866
+ listener.listener ? listener.listener : listener);
867
+
868
+ // Re-assign `events` because a newListener handler could have caused the
869
+ // this._events to be assigned to a new object
870
+ events = target._events;
871
+ }
872
+ existing = events[type];
873
+ }
874
+
875
+ if (!existing) {
876
+ // Optimize the case of one listener. Don't need the extra array object.
877
+ existing = events[type] = listener;
878
+ ++target._eventsCount;
879
+ } else {
880
+ if (typeof existing === 'function') {
881
+ // Adding the second element, need to change to array.
882
+ existing = events[type] =
883
+ prepend ? [listener, existing] : [existing, listener];
884
+ } else {
885
+ // If we've already got an array, just append.
886
+ if (prepend) {
887
+ existing.unshift(listener);
888
+ } else {
889
+ existing.push(listener);
890
+ }
891
+ }
892
+
893
+ // Check for listener leak
894
+ if (!existing.warned) {
895
+ m = $getMaxListeners(target);
896
+ if (m && m > 0 && existing.length > m) {
897
+ existing.warned = true;
898
+ var w = new Error('Possible EventEmitter memory leak detected. ' +
899
+ existing.length + ' "' + String(type) + '" listeners ' +
900
+ 'added. Use emitter.setMaxListeners() to ' +
901
+ 'increase limit.');
902
+ w.name = 'MaxListenersExceededWarning';
903
+ w.emitter = target;
904
+ w.type = type;
905
+ w.count = existing.length;
906
+ if (typeof console === 'object' && console.warn) {
907
+ console.warn('%s: %s', w.name, w.message);
908
+ }
909
+ }
910
+ }
911
+ }
912
+
913
+ return target;
914
+ }
915
+
916
+ EventEmitter.prototype.addListener = function addListener(type, listener) {
917
+ return _addListener(this, type, listener, false);
918
+ };
919
+
920
+ EventEmitter.prototype.on = EventEmitter.prototype.addListener;
921
+
922
+ EventEmitter.prototype.prependListener =
923
+ function prependListener(type, listener) {
924
+ return _addListener(this, type, listener, true);
925
+ };
926
+
927
+ function onceWrapper() {
928
+ if (!this.fired) {
929
+ this.target.removeListener(this.type, this.wrapFn);
930
+ this.fired = true;
931
+ switch (arguments.length) {
932
+ case 0:
933
+ return this.listener.call(this.target);
934
+ case 1:
935
+ return this.listener.call(this.target, arguments[0]);
936
+ case 2:
937
+ return this.listener.call(this.target, arguments[0], arguments[1]);
938
+ case 3:
939
+ return this.listener.call(this.target, arguments[0], arguments[1],
940
+ arguments[2]);
941
+ default:
942
+ var args = new Array(arguments.length);
943
+ for (var i = 0; i < args.length; ++i)
944
+ args[i] = arguments[i];
945
+ this.listener.apply(this.target, args);
946
+ }
947
+ }
948
+ }
949
+
950
+ function _onceWrap(target, type, listener) {
951
+ var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener };
952
+ var wrapped = bind.call(onceWrapper, state);
953
+ wrapped.listener = listener;
954
+ state.wrapFn = wrapped;
955
+ return wrapped;
956
+ }
957
+
958
+ EventEmitter.prototype.once = function once(type, listener) {
959
+ if (typeof listener !== 'function')
960
+ throw new TypeError('"listener" argument must be a function');
961
+ this.on(type, _onceWrap(this, type, listener));
962
+ return this;
963
+ };
964
+
965
+ EventEmitter.prototype.prependOnceListener =
966
+ function prependOnceListener(type, listener) {
967
+ if (typeof listener !== 'function')
968
+ throw new TypeError('"listener" argument must be a function');
969
+ this.prependListener(type, _onceWrap(this, type, listener));
970
+ return this;
971
+ };
972
+
973
+ // Emits a 'removeListener' event if and only if the listener was removed.
974
+ EventEmitter.prototype.removeListener =
975
+ function removeListener(type, listener) {
976
+ var list, events, position, i, originalListener;
977
+
978
+ if (typeof listener !== 'function')
979
+ throw new TypeError('"listener" argument must be a function');
980
+
981
+ events = this._events;
982
+ if (!events)
983
+ return this;
984
+
985
+ list = events[type];
986
+ if (!list)
987
+ return this;
988
+
989
+ if (list === listener || list.listener === listener) {
990
+ if (--this._eventsCount === 0)
991
+ this._events = objectCreate(null);
992
+ else {
993
+ delete events[type];
994
+ if (events.removeListener)
995
+ this.emit('removeListener', type, list.listener || listener);
996
+ }
997
+ } else if (typeof list !== 'function') {
998
+ position = -1;
999
+
1000
+ for (i = list.length - 1; i >= 0; i--) {
1001
+ if (list[i] === listener || list[i].listener === listener) {
1002
+ originalListener = list[i].listener;
1003
+ position = i;
1004
+ break;
1005
+ }
1006
+ }
1007
+
1008
+ if (position < 0)
1009
+ return this;
1010
+
1011
+ if (position === 0)
1012
+ list.shift();
1013
+ else
1014
+ spliceOne(list, position);
1015
+
1016
+ if (list.length === 1)
1017
+ events[type] = list[0];
1018
+
1019
+ if (events.removeListener)
1020
+ this.emit('removeListener', type, originalListener || listener);
1021
+ }
1022
+
1023
+ return this;
1024
+ };
1025
+
1026
+ EventEmitter.prototype.removeAllListeners =
1027
+ function removeAllListeners(type) {
1028
+ var listeners, events, i;
1029
+
1030
+ events = this._events;
1031
+ if (!events)
1032
+ return this;
1033
+
1034
+ // not listening for removeListener, no need to emit
1035
+ if (!events.removeListener) {
1036
+ if (arguments.length === 0) {
1037
+ this._events = objectCreate(null);
1038
+ this._eventsCount = 0;
1039
+ } else if (events[type]) {
1040
+ if (--this._eventsCount === 0)
1041
+ this._events = objectCreate(null);
1042
+ else
1043
+ delete events[type];
1044
+ }
1045
+ return this;
1046
+ }
1047
+
1048
+ // emit removeListener for all listeners on all events
1049
+ if (arguments.length === 0) {
1050
+ var keys = objectKeys(events);
1051
+ var key;
1052
+ for (i = 0; i < keys.length; ++i) {
1053
+ key = keys[i];
1054
+ if (key === 'removeListener') continue;
1055
+ this.removeAllListeners(key);
1056
+ }
1057
+ this.removeAllListeners('removeListener');
1058
+ this._events = objectCreate(null);
1059
+ this._eventsCount = 0;
1060
+ return this;
1061
+ }
1062
+
1063
+ listeners = events[type];
1064
+
1065
+ if (typeof listeners === 'function') {
1066
+ this.removeListener(type, listeners);
1067
+ } else if (listeners) {
1068
+ // LIFO order
1069
+ for (i = listeners.length - 1; i >= 0; i--) {
1070
+ this.removeListener(type, listeners[i]);
1071
+ }
1072
+ }
1073
+
1074
+ return this;
1075
+ };
1076
+
1077
+ function _listeners(target, type, unwrap) {
1078
+ var events = target._events;
1079
+
1080
+ if (!events)
1081
+ return [];
1082
+
1083
+ var evlistener = events[type];
1084
+ if (!evlistener)
1085
+ return [];
1086
+
1087
+ if (typeof evlistener === 'function')
1088
+ return unwrap ? [evlistener.listener || evlistener] : [evlistener];
1089
+
1090
+ return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);
1091
+ }
1092
+
1093
+ EventEmitter.prototype.listeners = function listeners(type) {
1094
+ return _listeners(this, type, true);
1095
+ };
1096
+
1097
+ EventEmitter.prototype.rawListeners = function rawListeners(type) {
1098
+ return _listeners(this, type, false);
1099
+ };
1100
+
1101
+ EventEmitter.listenerCount = function(emitter, type) {
1102
+ if (typeof emitter.listenerCount === 'function') {
1103
+ return emitter.listenerCount(type);
1104
+ } else {
1105
+ return listenerCount.call(emitter, type);
1106
+ }
1107
+ };
1108
+
1109
+ EventEmitter.prototype.listenerCount = listenerCount;
1110
+ function listenerCount(type) {
1111
+ var events = this._events;
1112
+
1113
+ if (events) {
1114
+ var evlistener = events[type];
1115
+
1116
+ if (typeof evlistener === 'function') {
1117
+ return 1;
1118
+ } else if (evlistener) {
1119
+ return evlistener.length;
1120
+ }
1121
+ }
1122
+
1123
+ return 0;
1124
+ }
1125
+
1126
+ EventEmitter.prototype.eventNames = function eventNames() {
1127
+ return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : [];
1128
+ };
1129
+
1130
+ // About 1.5x faster than the two-arg version of Array#splice().
1131
+ function spliceOne(list, index) {
1132
+ for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1)
1133
+ list[i] = list[k];
1134
+ list.pop();
1135
+ }
1136
+
1137
+ function arrayClone(arr, n) {
1138
+ var copy = new Array(n);
1139
+ for (var i = 0; i < n; ++i)
1140
+ copy[i] = arr[i];
1141
+ return copy;
1142
+ }
1143
+
1144
+ function unwrapListeners(arr) {
1145
+ var ret = new Array(arr.length);
1146
+ for (var i = 0; i < ret.length; ++i) {
1147
+ ret[i] = arr[i].listener || arr[i];
1148
+ }
1149
+ return ret;
1150
+ }
1151
+
1152
+ function objectCreatePolyfill(proto) {
1153
+ var F = function() {};
1154
+ F.prototype = proto;
1155
+ return new F;
1156
+ }
1157
+ function objectKeysPolyfill(obj) {
1158
+ var keys = [];
1159
+ for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) {
1160
+ keys.push(k);
1161
+ }
1162
+ return k;
1163
+ }
1164
+ function functionBindPolyfill(context) {
1165
+ var fn = this;
1166
+ return function () {
1167
+ return fn.apply(context, arguments);
1168
+ };
1169
+ }
1170
+
1171
+ },{}],5:[function(require,module,exports){
1172
+ /* jshint node: true */
1173
+ 'use strict';
1174
+
1175
+ var normalice = require('normalice');
1176
+
1177
+ /**
1178
+ # freeice
1179
+
1180
+ The `freeice` module is a simple way of getting random STUN or TURN server
1181
+ for your WebRTC application. The list of servers (just STUN at this stage)
1182
+ were sourced from this [gist](https://gist.github.com/zziuni/3741933).
1183
+
1184
+ ## Example Use
1185
+
1186
+ The following demonstrates how you can use `freeice` with
1187
+ [rtc-quickconnect](https://github.com/rtc-io/rtc-quickconnect):
1188
+
1189
+ <<< examples/quickconnect.js
1190
+
1191
+ As the `freeice` module generates ice servers in a list compliant with the
1192
+ WebRTC spec you will be able to use it with raw `RTCPeerConnection`
1193
+ constructors and other WebRTC libraries.
1194
+
1195
+ ## Hey, don't use my STUN/TURN server!
1196
+
1197
+ If for some reason your free STUN or TURN server ends up in the
1198
+ list of servers ([stun](https://github.com/DamonOehlman/freeice/blob/master/stun.json) or
1199
+ [turn](https://github.com/DamonOehlman/freeice/blob/master/turn.json))
1200
+ that is used in this module, you can feel
1201
+ free to open an issue on this repository and those servers will be removed
1202
+ within 24 hours (or sooner). This is the quickest and probably the most
1203
+ polite way to have something removed (and provides us some visibility
1204
+ if someone opens a pull request requesting that a server is added).
1205
+
1206
+ ## Please add my server!
1207
+
1208
+ If you have a server that you wish to add to the list, that's awesome! I'm
1209
+ sure I speak on behalf of a whole pile of WebRTC developers who say thanks.
1210
+ To get it into the list, feel free to either open a pull request or if you
1211
+ find that process a bit daunting then just create an issue requesting
1212
+ the addition of the server (make sure you provide all the details, and if
1213
+ you have a Terms of Service then including that in the PR/issue would be
1214
+ awesome).
1215
+
1216
+ ## I know of a free server, can I add it?
1217
+
1218
+ Sure, if you do your homework and make sure it is ok to use (I'm currently
1219
+ in the process of reviewing the terms of those STUN servers included from
1220
+ the original list). If it's ok to go, then please see the previous entry
1221
+ for how to add it.
1222
+
1223
+ ## Current List of Servers
1224
+
1225
+ * current as at the time of last `README.md` file generation
1226
+
1227
+ ### STUN
1228
+
1229
+ <<< stun.json
1230
+
1231
+ ### TURN
1232
+
1233
+ <<< turn.json
1234
+
1235
+ **/
1236
+
1237
+ var freeice = function(opts) {
1238
+ // if a list of servers has been provided, then use it instead of defaults
1239
+ var servers = {
1240
+ stun: (opts || {}).stun || require('./stun.json'),
1241
+ turn: (opts || {}).turn || require('./turn.json')
1242
+ };
1243
+
1244
+ var stunCount = (opts || {}).stunCount || 2;
1245
+ var turnCount = (opts || {}).turnCount || 0;
1246
+ var selected;
1247
+
1248
+ function getServers(type, count) {
1249
+ var out = [];
1250
+ var input = [].concat(servers[type]);
1251
+ var idx;
1252
+
1253
+ while (input.length && out.length < count) {
1254
+ idx = (Math.random() * input.length) | 0;
1255
+ out = out.concat(input.splice(idx, 1));
1256
+ }
1257
+
1258
+ return out.map(function(url) {
1259
+ //If it's a not a string, don't try to "normalice" it otherwise using type:url will screw it up
1260
+ if ((typeof url !== 'string') && (! (url instanceof String))) {
1261
+ return url;
1262
+ } else {
1263
+ return normalice(type + ':' + url);
1264
+ }
1265
+ });
1266
+ }
1267
+
1268
+ // add stun servers
1269
+ selected = [].concat(getServers('stun', stunCount));
1270
+
1271
+ if (turnCount) {
1272
+ selected = selected.concat(getServers('turn', turnCount));
1273
+ }
1274
+
1275
+ return selected;
1276
+ };
1277
+
1278
+ module.exports = freeice;
1279
+ },{"./stun.json":6,"./turn.json":7,"normalice":12}],6:[function(require,module,exports){
1280
+ module.exports=[
1281
+ "stun.l.google.com:19302",
1282
+ "stun1.l.google.com:19302",
1283
+ "stun2.l.google.com:19302",
1284
+ "stun3.l.google.com:19302",
1285
+ "stun4.l.google.com:19302",
1286
+ "stun.ekiga.net",
1287
+ "stun.ideasip.com",
1288
+ "stun.schlund.de",
1289
+ "stun.stunprotocol.org:3478",
1290
+ "stun.voiparound.com",
1291
+ "stun.voipbuster.com",
1292
+ "stun.voipstunt.com",
1293
+ "stun.voxgratia.org"
1294
+ ]
1295
+
1296
+ },{}],7:[function(require,module,exports){
1297
+ module.exports=[]
1298
+
1299
+ },{}],8:[function(require,module,exports){
1300
+ var WildEmitter = require('wildemitter');
1301
+
1302
+ function getMaxVolume (analyser, fftBins) {
1303
+ var maxVolume = -Infinity;
1304
+ analyser.getFloatFrequencyData(fftBins);
1305
+
1306
+ for(var i=4, ii=fftBins.length; i < ii; i++) {
1307
+ if (fftBins[i] > maxVolume && fftBins[i] < 0) {
1308
+ maxVolume = fftBins[i];
1309
+ }
1310
+ };
1311
+
1312
+ return maxVolume;
1313
+ }
1314
+
1315
+
1316
+ var audioContextType;
1317
+ if (typeof window !== 'undefined') {
1318
+ audioContextType = window.AudioContext || window.webkitAudioContext;
1319
+ }
1320
+ // use a single audio context due to hardware limits
1321
+ var audioContext = null;
1322
+ module.exports = function(stream, options) {
1323
+ var harker = new WildEmitter();
1324
+
1325
+ // make it not break in non-supported browsers
1326
+ if (!audioContextType) return harker;
1327
+
1328
+ //Config
1329
+ var options = options || {},
1330
+ smoothing = (options.smoothing || 0.1),
1331
+ interval = (options.interval || 50),
1332
+ threshold = options.threshold,
1333
+ play = options.play,
1334
+ history = options.history || 10,
1335
+ running = true;
1336
+
1337
+ // Ensure that just a single AudioContext is internally created
1338
+ audioContext = options.audioContext || audioContext || new audioContextType();
1339
+
1340
+ var sourceNode, fftBins, analyser;
1341
+
1342
+ analyser = audioContext.createAnalyser();
1343
+ analyser.fftSize = 512;
1344
+ analyser.smoothingTimeConstant = smoothing;
1345
+ fftBins = new Float32Array(analyser.frequencyBinCount);
1346
+
1347
+ if (stream.jquery) stream = stream[0];
1348
+ if (stream instanceof HTMLAudioElement || stream instanceof HTMLVideoElement) {
1349
+ //Audio Tag
1350
+ sourceNode = audioContext.createMediaElementSource(stream);
1351
+ if (typeof play === 'undefined') play = true;
1352
+ threshold = threshold || -50;
1353
+ } else {
1354
+ //WebRTC Stream
1355
+ sourceNode = audioContext.createMediaStreamSource(stream);
1356
+ threshold = threshold || -50;
1357
+ }
1358
+
1359
+ sourceNode.connect(analyser);
1360
+ if (play) analyser.connect(audioContext.destination);
1361
+
1362
+ harker.speaking = false;
1363
+
1364
+ harker.suspend = function() {
1365
+ return audioContext.suspend();
1366
+ }
1367
+ harker.resume = function() {
1368
+ return audioContext.resume();
1369
+ }
1370
+ Object.defineProperty(harker, 'state', { get: function() {
1371
+ return audioContext.state;
1372
+ }});
1373
+ audioContext.onstatechange = function() {
1374
+ harker.emit('state_change', audioContext.state);
1375
+ }
1376
+
1377
+ harker.setThreshold = function(t) {
1378
+ threshold = t;
1379
+ };
1380
+
1381
+ harker.setInterval = function(i) {
1382
+ interval = i;
1383
+ };
1384
+
1385
+ harker.stop = function() {
1386
+ running = false;
1387
+ harker.emit('volume_change', -100, threshold);
1388
+ if (harker.speaking) {
1389
+ harker.speaking = false;
1390
+ harker.emit('stopped_speaking');
1391
+ }
1392
+ analyser.disconnect();
1393
+ sourceNode.disconnect();
1394
+ };
1395
+ harker.speakingHistory = [];
1396
+ for (var i = 0; i < history; i++) {
1397
+ harker.speakingHistory.push(0);
1398
+ }
1399
+
1400
+ // Poll the analyser node to determine if speaking
1401
+ // and emit events if changed
1402
+ var looper = function() {
1403
+ setTimeout(function() {
1404
+
1405
+ //check if stop has been called
1406
+ if(!running) {
1407
+ return;
1408
+ }
1409
+
1410
+ var currentVolume = getMaxVolume(analyser, fftBins);
1411
+
1412
+ harker.emit('volume_change', currentVolume, threshold);
1413
+
1414
+ var history = 0;
1415
+ if (currentVolume > threshold && !harker.speaking) {
1416
+ // trigger quickly, short history
1417
+ for (var i = harker.speakingHistory.length - 3; i < harker.speakingHistory.length; i++) {
1418
+ history += harker.speakingHistory[i];
1419
+ }
1420
+ if (history >= 2) {
1421
+ harker.speaking = true;
1422
+ harker.emit('speaking');
1423
+ }
1424
+ } else if (currentVolume < threshold && harker.speaking) {
1425
+ for (var i = 0; i < harker.speakingHistory.length; i++) {
1426
+ history += harker.speakingHistory[i];
1427
+ }
1428
+ if (history == 0) {
1429
+ harker.speaking = false;
1430
+ harker.emit('stopped_speaking');
1431
+ }
1432
+ }
1433
+ harker.speakingHistory.shift();
1434
+ harker.speakingHistory.push(0 + (currentVolume > threshold));
1435
+
1436
+ looper();
1437
+ }, interval);
1438
+ };
1439
+ looper();
1440
+
1441
+ return harker;
1442
+ }
1443
+
1444
+ },{"wildemitter":25}],9:[function(require,module,exports){
1445
+ if (typeof Object.create === 'function') {
1446
+ // implementation from standard node.js 'util' module
1447
+ module.exports = function inherits(ctor, superCtor) {
1448
+ if (superCtor) {
1449
+ ctor.super_ = superCtor
1450
+ ctor.prototype = Object.create(superCtor.prototype, {
1451
+ constructor: {
1452
+ value: ctor,
1453
+ enumerable: false,
1454
+ writable: true,
1455
+ configurable: true
1456
+ }
1457
+ })
1458
+ }
1459
+ };
1460
+ } else {
1461
+ // old school shim for old browsers
1462
+ module.exports = function inherits(ctor, superCtor) {
1463
+ if (superCtor) {
1464
+ ctor.super_ = superCtor
1465
+ var TempCtor = function () {}
1466
+ TempCtor.prototype = superCtor.prototype
1467
+ ctor.prototype = new TempCtor()
1468
+ ctor.prototype.constructor = ctor
1469
+ }
1470
+ }
1471
+ }
1472
+
1473
+ },{}],10:[function(require,module,exports){
1474
+ // Does nothing at all.
1475
+
1476
+ },{}],11:[function(require,module,exports){
1477
+ /*!
1478
+ * @name JavaScript/NodeJS Merge v1.2.1
1479
+ * @author yeikos
1480
+ * @repository https://github.com/yeikos/js.merge
1481
+
1482
+ * Copyright 2014 yeikos - MIT license
1483
+ * https://raw.github.com/yeikos/js.merge/master/LICENSE
1484
+ */
1485
+
1486
+ ;(function(isNode) {
1487
+
1488
+ /**
1489
+ * Merge one or more objects
1490
+ * @param bool? clone
1491
+ * @param mixed,... arguments
1492
+ * @return object
1493
+ */
1494
+
1495
+ var Public = function(clone) {
1496
+
1497
+ return merge(clone === true, false, arguments);
1498
+
1499
+ }, publicName = 'merge';
1500
+
1501
+ /**
1502
+ * Merge two or more objects recursively
1503
+ * @param bool? clone
1504
+ * @param mixed,... arguments
1505
+ * @return object
1506
+ */
1507
+
1508
+ Public.recursive = function(clone) {
1509
+
1510
+ return merge(clone === true, true, arguments);
1511
+
1512
+ };
1513
+
1514
+ /**
1515
+ * Clone the input removing any reference
1516
+ * @param mixed input
1517
+ * @return mixed
1518
+ */
1519
+
1520
+ Public.clone = function(input) {
1521
+
1522
+ var output = input,
1523
+ type = typeOf(input),
1524
+ index, size;
1525
+
1526
+ if (type === 'array') {
1527
+
1528
+ output = [];
1529
+ size = input.length;
1530
+
1531
+ for (index=0;index<size;++index)
1532
+
1533
+ output[index] = Public.clone(input[index]);
1534
+
1535
+ } else if (type === 'object') {
1536
+
1537
+ output = {};
1538
+
1539
+ for (index in input)
1540
+
1541
+ output[index] = Public.clone(input[index]);
1542
+
1543
+ }
1544
+
1545
+ return output;
1546
+
1547
+ };
1548
+
1549
+ /**
1550
+ * Merge two objects recursively
1551
+ * @param mixed input
1552
+ * @param mixed extend
1553
+ * @return mixed
1554
+ */
1555
+
1556
+ function merge_recursive(base, extend) {
1557
+
1558
+ if (typeOf(base) !== 'object')
1559
+
1560
+ return extend;
1561
+
1562
+ for (var key in extend) {
1563
+
1564
+ if (typeOf(base[key]) === 'object' && typeOf(extend[key]) === 'object') {
1565
+
1566
+ base[key] = merge_recursive(base[key], extend[key]);
1567
+
1568
+ } else {
1569
+
1570
+ base[key] = extend[key];
1571
+
1572
+ }
1573
+
1574
+ }
1575
+
1576
+ return base;
1577
+
1578
+ }
1579
+
1580
+ /**
1581
+ * Merge two or more objects
1582
+ * @param bool clone
1583
+ * @param bool recursive
1584
+ * @param array argv
1585
+ * @return object
1586
+ */
1587
+
1588
+ function merge(clone, recursive, argv) {
1589
+
1590
+ var result = argv[0],
1591
+ size = argv.length;
1592
+
1593
+ if (clone || typeOf(result) !== 'object')
1594
+
1595
+ result = {};
1596
+
1597
+ for (var index=0;index<size;++index) {
1598
+
1599
+ var item = argv[index],
1600
+
1601
+ type = typeOf(item);
1602
+
1603
+ if (type !== 'object') continue;
1604
+
1605
+ for (var key in item) {
1606
+
1607
+ if (key === '__proto__') continue;
1608
+
1609
+ var sitem = clone ? Public.clone(item[key]) : item[key];
1610
+
1611
+ if (recursive) {
1612
+
1613
+ result[key] = merge_recursive(result[key], sitem);
1614
+
1615
+ } else {
1616
+
1617
+ result[key] = sitem;
1618
+
1619
+ }
1620
+
1621
+ }
1622
+
1623
+ }
1624
+
1625
+ return result;
1626
+
1627
+ }
1628
+
1629
+ /**
1630
+ * Get type of variable
1631
+ * @param mixed input
1632
+ * @return string
1633
+ *
1634
+ * @see http://jsperf.com/typeofvar
1635
+ */
1636
+
1637
+ function typeOf(input) {
1638
+
1639
+ return ({}).toString.call(input).slice(8, -1).toLowerCase();
1640
+
1641
+ }
1642
+
1643
+ if (isNode) {
1644
+
1645
+ module.exports = Public;
1646
+
1647
+ } else {
1648
+
1649
+ window[publicName] = Public;
1650
+
1651
+ }
1652
+
1653
+ })(typeof module === 'object' && module && typeof module.exports === 'object' && module.exports);
1654
+ },{}],12:[function(require,module,exports){
1655
+ /**
1656
+ # normalice
1657
+
1658
+ Normalize an ice server configuration object (or plain old string) into a format
1659
+ that is usable in all browsers supporting WebRTC. Primarily this module is designed
1660
+ to help with the transition of the `url` attribute of the configuration object to
1661
+ the `urls` attribute.
1662
+
1663
+ ## Example Usage
1664
+
1665
+ <<< examples/simple.js
1666
+
1667
+ **/
1668
+
1669
+ var protocols = [
1670
+ 'stun:',
1671
+ 'turn:'
1672
+ ];
1673
+
1674
+ module.exports = function(input) {
1675
+ var url = (input || {}).url || input;
1676
+ var protocol;
1677
+ var parts;
1678
+ var output = {};
1679
+
1680
+ // if we don't have a string url, then allow the input to passthrough
1681
+ if (typeof url != 'string' && (! (url instanceof String))) {
1682
+ return input;
1683
+ }
1684
+
1685
+ // trim the url string, and convert to an array
1686
+ url = url.trim();
1687
+
1688
+ // if the protocol is not known, then passthrough
1689
+ protocol = protocols[protocols.indexOf(url.slice(0, 5))];
1690
+ if (! protocol) {
1691
+ return input;
1692
+ }
1693
+
1694
+ // now let's attack the remaining url parts
1695
+ url = url.slice(5);
1696
+ parts = url.split('@');
1697
+
1698
+ output.username = input.username;
1699
+ output.credential = input.credential;
1700
+ // if we have an authentication part, then set the credentials
1701
+ if (parts.length > 1) {
1702
+ url = parts[1];
1703
+ parts = parts[0].split(':');
1704
+
1705
+ // add the output credential and username
1706
+ output.username = parts[0];
1707
+ output.credential = (input || {}).credential || parts[1] || '';
1708
+ }
1709
+
1710
+ output.url = protocol + url;
1711
+ output.urls = [ output.url ];
1712
+
1713
+ return output;
1714
+ };
1715
+
1716
+ },{}],13:[function(require,module,exports){
1717
+ var grammar = module.exports = {
1718
+ v: [{
1719
+ name: 'version',
1720
+ reg: /^(\d*)$/
1721
+ }],
1722
+ o: [{ //o=- 20518 0 IN IP4 203.0.113.1
1723
+ // NB: sessionId will be a String in most cases because it is huge
1724
+ name: 'origin',
1725
+ reg: /^(\S*) (\d*) (\d*) (\S*) IP(\d) (\S*)/,
1726
+ names: ['username', 'sessionId', 'sessionVersion', 'netType', 'ipVer', 'address'],
1727
+ format: "%s %s %d %s IP%d %s"
1728
+ }],
1729
+ // default parsing of these only (though some of these feel outdated)
1730
+ s: [{ name: 'name' }],
1731
+ i: [{ name: 'description' }],
1732
+ u: [{ name: 'uri' }],
1733
+ e: [{ name: 'email' }],
1734
+ p: [{ name: 'phone' }],
1735
+ z: [{ name: 'timezones' }], // TODO: this one can actually be parsed properly..
1736
+ r: [{ name: 'repeats' }], // TODO: this one can also be parsed properly
1737
+ //k: [{}], // outdated thing ignored
1738
+ t: [{ //t=0 0
1739
+ name: 'timing',
1740
+ reg: /^(\d*) (\d*)/,
1741
+ names: ['start', 'stop'],
1742
+ format: "%d %d"
1743
+ }],
1744
+ c: [{ //c=IN IP4 10.47.197.26
1745
+ name: 'connection',
1746
+ reg: /^IN IP(\d) (\S*)/,
1747
+ names: ['version', 'ip'],
1748
+ format: "IN IP%d %s"
1749
+ }],
1750
+ b: [{ //b=AS:4000
1751
+ push: 'bandwidth',
1752
+ reg: /^(TIAS|AS|CT|RR|RS):(\d*)/,
1753
+ names: ['type', 'limit'],
1754
+ format: "%s:%s"
1755
+ }],
1756
+ m: [{ //m=video 51744 RTP/AVP 126 97 98 34 31
1757
+ // NB: special - pushes to session
1758
+ // TODO: rtp/fmtp should be filtered by the payloads found here?
1759
+ reg: /^(\w*) (\d*) ([\w\/]*)(?: (.*))?/,
1760
+ names: ['type', 'port', 'protocol', 'payloads'],
1761
+ format: "%s %d %s %s"
1762
+ }],
1763
+ a: [
1764
+ { //a=rtpmap:110 opus/48000/2
1765
+ push: 'rtp',
1766
+ reg: /^rtpmap:(\d*) ([\w\-]*)(?:\s*\/(\d*)(?:\s*\/(\S*))?)?/,
1767
+ names: ['payload', 'codec', 'rate', 'encoding'],
1768
+ format: function (o) {
1769
+ return (o.encoding) ?
1770
+ "rtpmap:%d %s/%s/%s":
1771
+ o.rate ?
1772
+ "rtpmap:%d %s/%s":
1773
+ "rtpmap:%d %s";
1774
+ }
1775
+ },
1776
+ {
1777
+ //a=fmtp:108 profile-level-id=24;object=23;bitrate=64000
1778
+ //a=fmtp:111 minptime=10; useinbandfec=1
1779
+ push: 'fmtp',
1780
+ reg: /^fmtp:(\d*) ([\S| ]*)/,
1781
+ names: ['payload', 'config'],
1782
+ format: "fmtp:%d %s"
1783
+ },
1784
+ { //a=control:streamid=0
1785
+ name: 'control',
1786
+ reg: /^control:(.*)/,
1787
+ format: "control:%s"
1788
+ },
1789
+ { //a=rtcp:65179 IN IP4 193.84.77.194
1790
+ name: 'rtcp',
1791
+ reg: /^rtcp:(\d*)(?: (\S*) IP(\d) (\S*))?/,
1792
+ names: ['port', 'netType', 'ipVer', 'address'],
1793
+ format: function (o) {
1794
+ return (o.address != null) ?
1795
+ "rtcp:%d %s IP%d %s":
1796
+ "rtcp:%d";
1797
+ }
1798
+ },
1799
+ { //a=rtcp-fb:98 trr-int 100
1800
+ push: 'rtcpFbTrrInt',
1801
+ reg: /^rtcp-fb:(\*|\d*) trr-int (\d*)/,
1802
+ names: ['payload', 'value'],
1803
+ format: "rtcp-fb:%d trr-int %d"
1804
+ },
1805
+ { //a=rtcp-fb:98 nack rpsi
1806
+ push: 'rtcpFb',
1807
+ reg: /^rtcp-fb:(\*|\d*) ([\w-_]*)(?: ([\w-_]*))?/,
1808
+ names: ['payload', 'type', 'subtype'],
1809
+ format: function (o) {
1810
+ return (o.subtype != null) ?
1811
+ "rtcp-fb:%s %s %s":
1812
+ "rtcp-fb:%s %s";
1813
+ }
1814
+ },
1815
+ { //a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
1816
+ //a=extmap:1/recvonly URI-gps-string
1817
+ push: 'ext',
1818
+ reg: /^extmap:([\w_\/]*) (\S*)(?: (\S*))?/,
1819
+ names: ['value', 'uri', 'config'], // value may include "/direction" suffix
1820
+ format: function (o) {
1821
+ return (o.config != null) ?
1822
+ "extmap:%s %s %s":
1823
+ "extmap:%s %s";
1824
+ }
1825
+ },
1826
+ {
1827
+ //a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32
1828
+ push: 'crypto',
1829
+ reg: /^crypto:(\d*) ([\w_]*) (\S*)(?: (\S*))?/,
1830
+ names: ['id', 'suite', 'config', 'sessionConfig'],
1831
+ format: function (o) {
1832
+ return (o.sessionConfig != null) ?
1833
+ "crypto:%d %s %s %s":
1834
+ "crypto:%d %s %s";
1835
+ }
1836
+ },
1837
+ { //a=setup:actpass
1838
+ name: 'setup',
1839
+ reg: /^setup:(\w*)/,
1840
+ format: "setup:%s"
1841
+ },
1842
+ { //a=mid:1
1843
+ name: 'mid',
1844
+ reg: /^mid:([^\s]*)/,
1845
+ format: "mid:%s"
1846
+ },
1847
+ { //a=msid:0c8b064d-d807-43b4-b434-f92a889d8587 98178685-d409-46e0-8e16-7ef0db0db64a
1848
+ name: 'msid',
1849
+ reg: /^msid:(.*)/,
1850
+ format: "msid:%s"
1851
+ },
1852
+ { //a=ptime:20
1853
+ name: 'ptime',
1854
+ reg: /^ptime:(\d*)/,
1855
+ format: "ptime:%d"
1856
+ },
1857
+ { //a=maxptime:60
1858
+ name: 'maxptime',
1859
+ reg: /^maxptime:(\d*)/,
1860
+ format: "maxptime:%d"
1861
+ },
1862
+ { //a=sendrecv
1863
+ name: 'direction',
1864
+ reg: /^(sendrecv|recvonly|sendonly|inactive)/
1865
+ },
1866
+ { //a=ice-lite
1867
+ name: 'icelite',
1868
+ reg: /^(ice-lite)/
1869
+ },
1870
+ { //a=ice-ufrag:F7gI
1871
+ name: 'iceUfrag',
1872
+ reg: /^ice-ufrag:(\S*)/,
1873
+ format: "ice-ufrag:%s"
1874
+ },
1875
+ { //a=ice-pwd:x9cml/YzichV2+XlhiMu8g
1876
+ name: 'icePwd',
1877
+ reg: /^ice-pwd:(\S*)/,
1878
+ format: "ice-pwd:%s"
1879
+ },
1880
+ { //a=fingerprint:SHA-1 00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33
1881
+ name: 'fingerprint',
1882
+ reg: /^fingerprint:(\S*) (\S*)/,
1883
+ names: ['type', 'hash'],
1884
+ format: "fingerprint:%s %s"
1885
+ },
1886
+ {
1887
+ //a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host
1888
+ //a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0
1889
+ //a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0
1890
+ //a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0
1891
+ //a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0
1892
+ push:'candidates',
1893
+ reg: /^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?/,
1894
+ names: ['foundation', 'component', 'transport', 'priority', 'ip', 'port', 'type', 'raddr', 'rport', 'tcptype', 'generation'],
1895
+ format: function (o) {
1896
+ var str = "candidate:%s %d %s %d %s %d typ %s";
1897
+
1898
+ str += (o.raddr != null) ? " raddr %s rport %d" : "%v%v";
1899
+
1900
+ // NB: candidate has three optional chunks, so %void middles one if it's missing
1901
+ str += (o.tcptype != null) ? " tcptype %s" : "%v";
1902
+
1903
+ if (o.generation != null) {
1904
+ str += " generation %d";
1905
+ }
1906
+ return str;
1907
+ }
1908
+ },
1909
+ { //a=end-of-candidates (keep after the candidates line for readability)
1910
+ name: 'endOfCandidates',
1911
+ reg: /^(end-of-candidates)/
1912
+ },
1913
+ { //a=remote-candidates:1 203.0.113.1 54400 2 203.0.113.1 54401 ...
1914
+ name: 'remoteCandidates',
1915
+ reg: /^remote-candidates:(.*)/,
1916
+ format: "remote-candidates:%s"
1917
+ },
1918
+ { //a=ice-options:google-ice
1919
+ name: 'iceOptions',
1920
+ reg: /^ice-options:(\S*)/,
1921
+ format: "ice-options:%s"
1922
+ },
1923
+ { //a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1
1924
+ push: "ssrcs",
1925
+ reg: /^ssrc:(\d*) ([\w_]*):(.*)/,
1926
+ names: ['id', 'attribute', 'value'],
1927
+ format: "ssrc:%d %s:%s"
1928
+ },
1929
+ { //a=ssrc-group:FEC 1 2
1930
+ push: "ssrcGroups",
1931
+ reg: /^ssrc-group:(\w*) (.*)/,
1932
+ names: ['semantics', 'ssrcs'],
1933
+ format: "ssrc-group:%s %s"
1934
+ },
1935
+ { //a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV
1936
+ name: "msidSemantic",
1937
+ reg: /^msid-semantic:\s?(\w*) (\S*)/,
1938
+ names: ['semantic', 'token'],
1939
+ format: "msid-semantic: %s %s" // space after ":" is not accidental
1940
+ },
1941
+ { //a=group:BUNDLE audio video
1942
+ push: 'groups',
1943
+ reg: /^group:(\w*) (.*)/,
1944
+ names: ['type', 'mids'],
1945
+ format: "group:%s %s"
1946
+ },
1947
+ { //a=rtcp-mux
1948
+ name: 'rtcpMux',
1949
+ reg: /^(rtcp-mux)/
1950
+ },
1951
+ { //a=rtcp-rsize
1952
+ name: 'rtcpRsize',
1953
+ reg: /^(rtcp-rsize)/
1954
+ },
1955
+ { // any a= that we don't understand is kepts verbatim on media.invalid
1956
+ push: 'invalid',
1957
+ names: ["value"]
1958
+ }
1959
+ ]
1960
+ };
1961
+
1962
+ // set sensible defaults to avoid polluting the grammar with boring details
1963
+ Object.keys(grammar).forEach(function (key) {
1964
+ var objs = grammar[key];
1965
+ objs.forEach(function (obj) {
1966
+ if (!obj.reg) {
1967
+ obj.reg = /(.*)/;
1968
+ }
1969
+ if (!obj.format) {
1970
+ obj.format = "%s";
1971
+ }
1972
+ });
1973
+ });
1974
+
1975
+ },{}],14:[function(require,module,exports){
1976
+ var parser = require('./parser');
1977
+ var writer = require('./writer');
1978
+
1979
+ exports.write = writer;
1980
+ exports.parse = parser.parse;
1981
+ exports.parseFmtpConfig = parser.parseFmtpConfig;
1982
+ exports.parsePayloads = parser.parsePayloads;
1983
+ exports.parseRemoteCandidates = parser.parseRemoteCandidates;
1984
+
1985
+ },{"./parser":15,"./writer":16}],15:[function(require,module,exports){
1986
+ var toIntIfInt = function (v) {
1987
+ return String(Number(v)) === v ? Number(v) : v;
1988
+ };
1989
+
1990
+ var attachProperties = function (match, location, names, rawName) {
1991
+ if (rawName && !names) {
1992
+ location[rawName] = toIntIfInt(match[1]);
1993
+ }
1994
+ else {
1995
+ for (var i = 0; i < names.length; i += 1) {
1996
+ if (match[i+1] != null) {
1997
+ location[names[i]] = toIntIfInt(match[i+1]);
1998
+ }
1999
+ }
2000
+ }
2001
+ };
2002
+
2003
+ var parseReg = function (obj, location, content) {
2004
+ var needsBlank = obj.name && obj.names;
2005
+ if (obj.push && !location[obj.push]) {
2006
+ location[obj.push] = [];
2007
+ }
2008
+ else if (needsBlank && !location[obj.name]) {
2009
+ location[obj.name] = {};
2010
+ }
2011
+ var keyLocation = obj.push ?
2012
+ {} : // blank object that will be pushed
2013
+ needsBlank ? location[obj.name] : location; // otherwise, named location or root
2014
+
2015
+ attachProperties(content.match(obj.reg), keyLocation, obj.names, obj.name);
2016
+
2017
+ if (obj.push) {
2018
+ location[obj.push].push(keyLocation);
2019
+ }
2020
+ };
2021
+
2022
+ var grammar = require('./grammar');
2023
+ var validLine = RegExp.prototype.test.bind(/^([a-z])=(.*)/);
2024
+
2025
+ exports.parse = function (sdp) {
2026
+ var session = {}
2027
+ , media = []
2028
+ , location = session; // points at where properties go under (one of the above)
2029
+
2030
+ // parse lines we understand
2031
+ sdp.split(/(\r\n|\r|\n)/).filter(validLine).forEach(function (l) {
2032
+ var type = l[0];
2033
+ var content = l.slice(2);
2034
+ if (type === 'm') {
2035
+ media.push({rtp: [], fmtp: []});
2036
+ location = media[media.length-1]; // point at latest media line
2037
+ }
2038
+
2039
+ for (var j = 0; j < (grammar[type] || []).length; j += 1) {
2040
+ var obj = grammar[type][j];
2041
+ if (obj.reg.test(content)) {
2042
+ return parseReg(obj, location, content);
2043
+ }
2044
+ }
2045
+ });
2046
+
2047
+ session.media = media; // link it up
2048
+ return session;
2049
+ };
2050
+
2051
+ var fmtpReducer = function (acc, expr) {
2052
+ var s = expr.split('=');
2053
+ if (s.length === 2) {
2054
+ acc[s[0]] = toIntIfInt(s[1]);
2055
+ }
2056
+ return acc;
2057
+ };
2058
+
2059
+ exports.parseFmtpConfig = function (str) {
2060
+ return str.split(/\;\s?/).reduce(fmtpReducer, {});
2061
+ };
2062
+
2063
+ exports.parsePayloads = function (str) {
2064
+ return str.split(' ').map(Number);
2065
+ };
2066
+
2067
+ exports.parseRemoteCandidates = function (str) {
2068
+ var candidates = [];
2069
+ var parts = str.split(' ').map(toIntIfInt);
2070
+ for (var i = 0; i < parts.length; i += 3) {
2071
+ candidates.push({
2072
+ component: parts[i],
2073
+ ip: parts[i + 1],
2074
+ port: parts[i + 2]
2075
+ });
2076
+ }
2077
+ return candidates;
2078
+ };
2079
+
2080
+ },{"./grammar":13}],16:[function(require,module,exports){
2081
+ var grammar = require('./grammar');
2082
+
2083
+ // customized util.format - discards excess arguments and can void middle ones
2084
+ var formatRegExp = /%[sdv%]/g;
2085
+ var format = function (formatStr) {
2086
+ var i = 1;
2087
+ var args = arguments;
2088
+ var len = args.length;
2089
+ return formatStr.replace(formatRegExp, function (x) {
2090
+ if (i >= len) {
2091
+ return x; // missing argument
2092
+ }
2093
+ var arg = args[i];
2094
+ i += 1;
2095
+ switch (x) {
2096
+ case '%%':
2097
+ return '%';
2098
+ case '%s':
2099
+ return String(arg);
2100
+ case '%d':
2101
+ return Number(arg);
2102
+ case '%v':
2103
+ return '';
2104
+ }
2105
+ });
2106
+ // NB: we discard excess arguments - they are typically undefined from makeLine
2107
+ };
2108
+
2109
+ var makeLine = function (type, obj, location) {
2110
+ var str = obj.format instanceof Function ?
2111
+ (obj.format(obj.push ? location : location[obj.name])) :
2112
+ obj.format;
2113
+
2114
+ var args = [type + '=' + str];
2115
+ if (obj.names) {
2116
+ for (var i = 0; i < obj.names.length; i += 1) {
2117
+ var n = obj.names[i];
2118
+ if (obj.name) {
2119
+ args.push(location[obj.name][n]);
2120
+ }
2121
+ else { // for mLine and push attributes
2122
+ args.push(location[obj.names[i]]);
2123
+ }
2124
+ }
2125
+ }
2126
+ else {
2127
+ args.push(location[obj.name]);
2128
+ }
2129
+ return format.apply(null, args);
2130
+ };
2131
+
2132
+ // RFC specified order
2133
+ // TODO: extend this with all the rest
2134
+ var defaultOuterOrder = [
2135
+ 'v', 'o', 's', 'i',
2136
+ 'u', 'e', 'p', 'c',
2137
+ 'b', 't', 'r', 'z', 'a'
2138
+ ];
2139
+ var defaultInnerOrder = ['i', 'c', 'b', 'a'];
2140
+
2141
+
2142
+ module.exports = function (session, opts) {
2143
+ opts = opts || {};
2144
+ // ensure certain properties exist
2145
+ if (session.version == null) {
2146
+ session.version = 0; // "v=0" must be there (only defined version atm)
2147
+ }
2148
+ if (session.name == null) {
2149
+ session.name = " "; // "s= " must be there if no meaningful name set
2150
+ }
2151
+ session.media.forEach(function (mLine) {
2152
+ if (mLine.payloads == null) {
2153
+ mLine.payloads = "";
2154
+ }
2155
+ });
2156
+
2157
+ var outerOrder = opts.outerOrder || defaultOuterOrder;
2158
+ var innerOrder = opts.innerOrder || defaultInnerOrder;
2159
+ var sdp = [];
2160
+
2161
+ // loop through outerOrder for matching properties on session
2162
+ outerOrder.forEach(function (type) {
2163
+ grammar[type].forEach(function (obj) {
2164
+ if (obj.name in session && session[obj.name] != null) {
2165
+ sdp.push(makeLine(type, obj, session));
2166
+ }
2167
+ else if (obj.push in session && session[obj.push] != null) {
2168
+ session[obj.push].forEach(function (el) {
2169
+ sdp.push(makeLine(type, obj, el));
2170
+ });
2171
+ }
2172
+ });
2173
+ });
2174
+
2175
+ // then for each media line, follow the innerOrder
2176
+ session.media.forEach(function (mLine) {
2177
+ sdp.push(makeLine('m', grammar.m[0], mLine));
2178
+
2179
+ innerOrder.forEach(function (type) {
2180
+ grammar[type].forEach(function (obj) {
2181
+ if (obj.name in mLine && mLine[obj.name] != null) {
2182
+ sdp.push(makeLine(type, obj, mLine));
2183
+ }
2184
+ else if (obj.push in mLine && mLine[obj.push] != null) {
2185
+ mLine[obj.push].forEach(function (el) {
2186
+ sdp.push(makeLine(type, obj, el));
2187
+ });
2188
+ }
2189
+ });
2190
+ });
2191
+ });
2192
+
2193
+ return sdp.join('\r\n') + '\r\n';
2194
+ };
2195
+
2196
+ },{"./grammar":13}],17:[function(require,module,exports){
2197
+ /* Copyright @ 2015 Atlassian Pty Ltd
2198
+ *
2199
+ * Licensed under the Apache License, Version 2.0 (the "License");
2200
+ * you may not use this file except in compliance with the License.
2201
+ * You may obtain a copy of the License at
2202
+ *
2203
+ * http://www.apache.org/licenses/LICENSE-2.0
2204
+ *
2205
+ * Unless required by applicable law or agreed to in writing, software
2206
+ * distributed under the License is distributed on an "AS IS" BASIS,
2207
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2208
+ * See the License for the specific language governing permissions and
2209
+ * limitations under the License.
2210
+ */
2211
+
2212
+ module.exports = function arrayEquals(array) {
2213
+ // if the other array is a falsy value, return
2214
+ if (!array)
2215
+ return false;
2216
+
2217
+ // compare lengths - can save a lot of time
2218
+ if (this.length != array.length)
2219
+ return false;
2220
+
2221
+ for (var i = 0, l = this.length; i < l; i++) {
2222
+ // Check if we have nested arrays
2223
+ if (this[i] instanceof Array && array[i] instanceof Array) {
2224
+ // recurse into the nested arrays
2225
+ if (!arrayEquals.apply(this[i], [array[i]]))
2226
+ return false;
2227
+ } else if (this[i] != array[i]) {
2228
+ // Warning - two different object instances will never be equal:
2229
+ // {x:20} != {x:20}
2230
+ return false;
2231
+ }
2232
+ }
2233
+ return true;
2234
+ };
2235
+
2236
+
2237
+ },{}],18:[function(require,module,exports){
2238
+ /* Copyright @ 2015 Atlassian Pty Ltd
2239
+ *
2240
+ * Licensed under the Apache License, Version 2.0 (the "License");
2241
+ * you may not use this file except in compliance with the License.
2242
+ * You may obtain a copy of the License at
2243
+ *
2244
+ * http://www.apache.org/licenses/LICENSE-2.0
2245
+ *
2246
+ * Unless required by applicable law or agreed to in writing, software
2247
+ * distributed under the License is distributed on an "AS IS" BASIS,
2248
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2249
+ * See the License for the specific language governing permissions and
2250
+ * limitations under the License.
2251
+ */
2252
+
2253
+ exports.Interop = require('./interop');
2254
+
2255
+ },{"./interop":19}],19:[function(require,module,exports){
2256
+ /* Copyright @ 2015 Atlassian Pty Ltd
2257
+ *
2258
+ * Licensed under the Apache License, Version 2.0 (the "License");
2259
+ * you may not use this file except in compliance with the License.
2260
+ * You may obtain a copy of the License at
2261
+ *
2262
+ * http://www.apache.org/licenses/LICENSE-2.0
2263
+ *
2264
+ * Unless required by applicable law or agreed to in writing, software
2265
+ * distributed under the License is distributed on an "AS IS" BASIS,
2266
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2267
+ * See the License for the specific language governing permissions and
2268
+ * limitations under the License.
2269
+ */
2270
+
2271
+ /* global RTCSessionDescription */
2272
+ /* global RTCIceCandidate */
2273
+ /* jshint -W097 */
2274
+ "use strict";
2275
+
2276
+ var transform = require('./transform');
2277
+ var arrayEquals = require('./array-equals');
2278
+
2279
+ function Interop() {
2280
+
2281
+ /**
2282
+ * This map holds the most recent Unified Plan offer/answer SDP that was
2283
+ * converted to Plan B, with the SDP type ('offer' or 'answer') as keys and
2284
+ * the SDP string as values.
2285
+ *
2286
+ * @type {{}}
2287
+ */
2288
+ this.cache = {
2289
+ mlB2UMap : {},
2290
+ mlU2BMap : {}
2291
+ };
2292
+ }
2293
+
2294
+ module.exports = Interop;
2295
+
2296
+ /**
2297
+ * Changes the candidate args to match with the related Unified Plan
2298
+ */
2299
+ Interop.prototype.candidateToUnifiedPlan = function(candidate) {
2300
+ var cand = new RTCIceCandidate(candidate);
2301
+
2302
+ cand.sdpMLineIndex = this.cache.mlB2UMap[cand.sdpMLineIndex];
2303
+ /* TODO: change sdpMid to (audio|video)-SSRC */
2304
+
2305
+ return cand;
2306
+ };
2307
+
2308
+ /**
2309
+ * Changes the candidate args to match with the related Plan B
2310
+ */
2311
+ Interop.prototype.candidateToPlanB = function(candidate) {
2312
+ var cand = new RTCIceCandidate(candidate);
2313
+
2314
+ if (cand.sdpMid.indexOf('audio') === 0) {
2315
+ cand.sdpMid = 'audio';
2316
+ } else if (cand.sdpMid.indexOf('video') === 0) {
2317
+ cand.sdpMid = 'video';
2318
+ } else {
2319
+ throw new Error('candidate with ' + cand.sdpMid + ' not allowed');
2320
+ }
2321
+
2322
+ cand.sdpMLineIndex = this.cache.mlU2BMap[cand.sdpMLineIndex];
2323
+
2324
+ return cand;
2325
+ };
2326
+
2327
+ /**
2328
+ * Returns the index of the first m-line with the given media type and with a
2329
+ * direction which allows sending, in the last Unified Plan description with
2330
+ * type "answer" converted to Plan B. Returns {null} if there is no saved
2331
+ * answer, or if none of its m-lines with the given type allow sending.
2332
+ * @param type the media type ("audio" or "video").
2333
+ * @returns {*}
2334
+ */
2335
+ Interop.prototype.getFirstSendingIndexFromAnswer = function(type) {
2336
+ if (!this.cache.answer) {
2337
+ return null;
2338
+ }
2339
+
2340
+ var session = transform.parse(this.cache.answer);
2341
+ if (session && session.media && Array.isArray(session.media)){
2342
+ for (var i = 0; i < session.media.length; i++) {
2343
+ if (session.media[i].type == type &&
2344
+ (!session.media[i].direction /* default to sendrecv */ ||
2345
+ session.media[i].direction === 'sendrecv' ||
2346
+ session.media[i].direction === 'sendonly')){
2347
+ return i;
2348
+ }
2349
+ }
2350
+ }
2351
+
2352
+ return null;
2353
+ };
2354
+
2355
+ /**
2356
+ * This method transforms a Unified Plan SDP to an equivalent Plan B SDP. A
2357
+ * PeerConnection wrapper transforms the SDP to Plan B before passing it to the
2358
+ * application.
2359
+ *
2360
+ * @param desc
2361
+ * @returns {*}
2362
+ */
2363
+ Interop.prototype.toPlanB = function(desc) {
2364
+ var self = this;
2365
+ //#region Preliminary input validation.
2366
+
2367
+ if (typeof desc !== 'object' || desc === null ||
2368
+ typeof desc.sdp !== 'string') {
2369
+ console.warn('An empty description was passed as an argument.');
2370
+ return desc;
2371
+ }
2372
+
2373
+ // Objectify the SDP for easier manipulation.
2374
+ var session = transform.parse(desc.sdp);
2375
+
2376
+ // If the SDP contains no media, there's nothing to transform.
2377
+ if (typeof session.media === 'undefined' ||
2378
+ !Array.isArray(session.media) || session.media.length === 0) {
2379
+ console.warn('The description has no media.');
2380
+ return desc;
2381
+ }
2382
+
2383
+ // Try some heuristics to "make sure" this is a Unified Plan SDP. Plan B
2384
+ // SDP has a video, an audio and a data "channel" at most.
2385
+ if (session.media.length <= 3 && session.media.every(function(m) {
2386
+ return ['video', 'audio', 'data'].indexOf(m.mid) !== -1;
2387
+ })) {
2388
+ console.warn('This description does not look like Unified Plan.');
2389
+ return desc;
2390
+ }
2391
+
2392
+ //#endregion
2393
+
2394
+ // HACK https://bugzilla.mozilla.org/show_bug.cgi?id=1113443
2395
+ var sdp = desc.sdp;
2396
+ var rewrite = false;
2397
+ for (var i = 0; i < session.media.length; i++) {
2398
+ var uLine = session.media[i];
2399
+ uLine.rtp.forEach(function(rtp) {
2400
+ if (rtp.codec === 'NULL')
2401
+ {
2402
+ rewrite = true;
2403
+ var offer = transform.parse(self.cache.offer);
2404
+ rtp.codec = offer.media[i].rtp[0].codec;
2405
+ }
2406
+ });
2407
+ }
2408
+ if (rewrite) {
2409
+ sdp = transform.write(session);
2410
+ }
2411
+
2412
+ // Unified Plan SDP is our "precious". Cache it for later use in the Plan B
2413
+ // -> Unified Plan transformation.
2414
+ this.cache[desc.type] = sdp;
2415
+
2416
+ //#region Convert from Unified Plan to Plan B.
2417
+
2418
+ // We rebuild the session.media array.
2419
+ var media = session.media;
2420
+ session.media = [];
2421
+
2422
+ // Associative array that maps channel types to channel objects for fast
2423
+ // access to channel objects by their type, e.g. type2bl['audio']->channel
2424
+ // obj.
2425
+ var type2bl = {};
2426
+
2427
+ // Used to build the group:BUNDLE value after the channels construction
2428
+ // loop.
2429
+ var types = [];
2430
+
2431
+ media.forEach(function(uLine) {
2432
+ // rtcp-mux is required in the Plan B SDP.
2433
+ if ((typeof uLine.rtcpMux !== 'string' ||
2434
+ uLine.rtcpMux !== 'rtcp-mux') &&
2435
+ uLine.direction !== 'inactive') {
2436
+ throw new Error('Cannot convert to Plan B because m-lines ' +
2437
+ 'without the rtcp-mux attribute were found.');
2438
+ }
2439
+
2440
+ // If we don't have a channel for this uLine.type OR the selected is
2441
+ // inactive, then select this uLine as the channel basis.
2442
+ if (typeof type2bl[uLine.type] === 'undefined' ||
2443
+ type2bl[uLine.type].direction === 'inactive') {
2444
+ type2bl[uLine.type] = uLine;
2445
+ }
2446
+
2447
+ if (uLine.protocol != type2bl[uLine.type].protocol) {
2448
+ throw new Error('Cannot convert to Plan B because m-lines ' +
2449
+ 'have different protocols and this library does not have ' +
2450
+ 'support for that');
2451
+ }
2452
+
2453
+ if (uLine.payloads != type2bl[uLine.type].payloads) {
2454
+ throw new Error('Cannot convert to Plan B because m-lines ' +
2455
+ 'have different payloads and this library does not have ' +
2456
+ 'support for that');
2457
+ }
2458
+
2459
+ });
2460
+
2461
+ // Implode the Unified Plan m-lines/tracks into Plan B channels.
2462
+ media.forEach(function(uLine) {
2463
+ if (uLine.type === 'application') {
2464
+ session.media.push(uLine);
2465
+ types.push(uLine.mid);
2466
+ return;
2467
+ }
2468
+
2469
+ // Add sources to the channel and handle a=msid.
2470
+ if (typeof uLine.sources === 'object') {
2471
+ Object.keys(uLine.sources).forEach(function(ssrc) {
2472
+ if (typeof type2bl[uLine.type].sources !== 'object')
2473
+ type2bl[uLine.type].sources = {};
2474
+
2475
+ // Assign the sources to the channel.
2476
+ type2bl[uLine.type].sources[ssrc] =
2477
+ uLine.sources[ssrc];
2478
+
2479
+ if (typeof uLine.msid !== 'undefined') {
2480
+ // In Plan B the msid is an SSRC attribute. Also, we don't
2481
+ // care about the obsolete label and mslabel attributes.
2482
+ //
2483
+ // Note that it is not guaranteed that the uLine will
2484
+ // have an msid. recvonly channels in particular don't have
2485
+ // one.
2486
+ type2bl[uLine.type].sources[ssrc].msid =
2487
+ uLine.msid;
2488
+ }
2489
+ // NOTE ssrcs in ssrc groups will share msids, as
2490
+ // draft-uberti-rtcweb-plan-00 mandates.
2491
+ });
2492
+ }
2493
+
2494
+ // Add ssrc groups to the channel.
2495
+ if (typeof uLine.ssrcGroups !== 'undefined' &&
2496
+ Array.isArray(uLine.ssrcGroups)) {
2497
+
2498
+ // Create the ssrcGroups array, if it's not defined.
2499
+ if (typeof type2bl[uLine.type].ssrcGroups === 'undefined' ||
2500
+ !Array.isArray(type2bl[uLine.type].ssrcGroups)) {
2501
+ type2bl[uLine.type].ssrcGroups = [];
2502
+ }
2503
+
2504
+ type2bl[uLine.type].ssrcGroups =
2505
+ type2bl[uLine.type].ssrcGroups.concat(
2506
+ uLine.ssrcGroups);
2507
+ }
2508
+
2509
+ if (type2bl[uLine.type] === uLine) {
2510
+ // Plan B mids are in ['audio', 'video', 'data']
2511
+ uLine.mid = uLine.type;
2512
+
2513
+ // Plan B doesn't support/need the bundle-only attribute.
2514
+ delete uLine.bundleOnly;
2515
+
2516
+ // In Plan B the msid is an SSRC attribute.
2517
+ delete uLine.msid;
2518
+
2519
+ if (uLine.type == media[0].type) {
2520
+ types.unshift(uLine.type);
2521
+ // Add the channel to the new media array.
2522
+ session.media.unshift(uLine);
2523
+ } else {
2524
+ types.push(uLine.type);
2525
+ // Add the channel to the new media array.
2526
+ session.media.push(uLine);
2527
+ }
2528
+ }
2529
+ });
2530
+
2531
+ if (typeof session.groups !== 'undefined') {
2532
+ // We regenerate the BUNDLE group with the new mids.
2533
+ session.groups.some(function(group) {
2534
+ if (group.type === 'BUNDLE') {
2535
+ group.mids = types.join(' ');
2536
+ return true;
2537
+ }
2538
+ });
2539
+ }
2540
+
2541
+ // msid semantic
2542
+ session.msidSemantic = {
2543
+ semantic: 'WMS',
2544
+ token: '*'
2545
+ };
2546
+
2547
+ var resStr = transform.write(session);
2548
+
2549
+ return new RTCSessionDescription({
2550
+ type: desc.type,
2551
+ sdp: resStr
2552
+ });
2553
+
2554
+ //#endregion
2555
+ };
2556
+
2557
+ /* follow rules defined in RFC4145 */
2558
+ function addSetupAttr(uLine) {
2559
+ if (typeof uLine.setup === 'undefined') {
2560
+ return;
2561
+ }
2562
+
2563
+ if (uLine.setup === "active") {
2564
+ uLine.setup = "passive";
2565
+ } else if (uLine.setup === "passive") {
2566
+ uLine.setup = "active";
2567
+ }
2568
+ }
2569
+
2570
+ /**
2571
+ * This method transforms a Plan B SDP to an equivalent Unified Plan SDP. A
2572
+ * PeerConnection wrapper transforms the SDP to Unified Plan before passing it
2573
+ * to FF.
2574
+ *
2575
+ * @param desc
2576
+ * @returns {*}
2577
+ */
2578
+ Interop.prototype.toUnifiedPlan = function(desc) {
2579
+ var self = this;
2580
+ //#region Preliminary input validation.
2581
+
2582
+ if (typeof desc !== 'object' || desc === null ||
2583
+ typeof desc.sdp !== 'string') {
2584
+ console.warn('An empty description was passed as an argument.');
2585
+ return desc;
2586
+ }
2587
+
2588
+ var session = transform.parse(desc.sdp);
2589
+
2590
+ // If the SDP contains no media, there's nothing to transform.
2591
+ if (typeof session.media === 'undefined' ||
2592
+ !Array.isArray(session.media) || session.media.length === 0) {
2593
+ console.warn('The description has no media.');
2594
+ return desc;
2595
+ }
2596
+
2597
+ // Try some heuristics to "make sure" this is a Plan B SDP. Plan B SDP has
2598
+ // a video, an audio and a data "channel" at most.
2599
+ if (session.media.length > 3 || !session.media.every(function(m) {
2600
+ return ['video', 'audio', 'data'].indexOf(m.mid) !== -1;
2601
+ })) {
2602
+ console.warn('This description does not look like Plan B.');
2603
+ return desc;
2604
+ }
2605
+
2606
+ // Make sure this Plan B SDP can be converted to a Unified Plan SDP.
2607
+ var mids = [];
2608
+ session.media.forEach(function(m) {
2609
+ mids.push(m.mid);
2610
+ });
2611
+
2612
+ var hasBundle = false;
2613
+ if (typeof session.groups !== 'undefined' &&
2614
+ Array.isArray(session.groups)) {
2615
+ hasBundle = session.groups.every(function(g) {
2616
+ return g.type !== 'BUNDLE' ||
2617
+ arrayEquals.apply(g.mids.sort(), [mids.sort()]);
2618
+ });
2619
+ }
2620
+
2621
+ if (!hasBundle) {
2622
+ var mustBeBundle = false;
2623
+
2624
+ session.media.forEach(function(m) {
2625
+ if (m.direction !== 'inactive') {
2626
+ mustBeBundle = true;
2627
+ }
2628
+ });
2629
+
2630
+ if (mustBeBundle) {
2631
+ throw new Error("Cannot convert to Unified Plan because m-lines that" +
2632
+ " are not bundled were found.");
2633
+ }
2634
+ }
2635
+
2636
+ //#endregion
2637
+
2638
+
2639
+ //#region Convert from Plan B to Unified Plan.
2640
+
2641
+ // Unfortunately, a Plan B offer/answer doesn't have enough information to
2642
+ // rebuild an equivalent Unified Plan offer/answer.
2643
+ //
2644
+ // For example, if this is a local answer (in Unified Plan style) that we
2645
+ // convert to Plan B prior to handing it over to the application (the
2646
+ // PeerConnection wrapper called us, for instance, after a successful
2647
+ // createAnswer), we want to remember the m-line at which we've seen the
2648
+ // (local) SSRC. That's because when the application wants to do call the
2649
+ // SLD method, forcing us to do the inverse transformation (from Plan B to
2650
+ // Unified Plan), we need to know to which m-line to assign the (local)
2651
+ // SSRC. We also need to know all the other m-lines that the original
2652
+ // answer had and include them in the transformed answer as well.
2653
+ //
2654
+ // Another example is if this is a remote offer that we convert to Plan B
2655
+ // prior to giving it to the application, we want to remember the mid at
2656
+ // which we've seen the (remote) SSRC.
2657
+ //
2658
+ // In the iteration that follows, we use the cached Unified Plan (if it
2659
+ // exists) to assign mids to ssrcs.
2660
+
2661
+ var type;
2662
+ if (desc.type === 'answer') {
2663
+ type = 'offer';
2664
+ } else if (desc.type === 'offer') {
2665
+ type = 'answer';
2666
+ } else {
2667
+ throw new Error("Type '" + desc.type + "' not supported.");
2668
+ }
2669
+
2670
+ var cached;
2671
+ if (typeof this.cache[type] !== 'undefined') {
2672
+ cached = transform.parse(this.cache[type]);
2673
+ }
2674
+
2675
+ var recvonlySsrcs = {
2676
+ audio: {},
2677
+ video: {}
2678
+ };
2679
+
2680
+ // A helper map that sends mids to m-line objects. We use it later to
2681
+ // rebuild the Unified Plan style session.media array.
2682
+ var mid2ul = {};
2683
+ var bIdx = 0;
2684
+ var uIdx = 0;
2685
+
2686
+ var sources2ul = {};
2687
+
2688
+ var candidates;
2689
+ var iceUfrag;
2690
+ var icePwd;
2691
+ var fingerprint;
2692
+ var payloads = {};
2693
+ var rtcpFb = {};
2694
+ var rtp = {};
2695
+
2696
+ session.media.forEach(function(bLine) {
2697
+ if ((typeof bLine.rtcpMux !== 'string' ||
2698
+ bLine.rtcpMux !== 'rtcp-mux') &&
2699
+ bLine.direction !== 'inactive') {
2700
+ throw new Error("Cannot convert to Unified Plan because m-lines " +
2701
+ "without the rtcp-mux attribute were found.");
2702
+ }
2703
+
2704
+ if (bLine.type === 'application') {
2705
+ mid2ul[bLine.mid] = bLine;
2706
+ return;
2707
+ }
2708
+
2709
+ // With rtcp-mux and bundle all the channels should have the same ICE
2710
+ // stuff.
2711
+ var sources = bLine.sources;
2712
+ var ssrcGroups = bLine.ssrcGroups;
2713
+ var port = bLine.port;
2714
+
2715
+ /* Chrome adds different candidates even using bundle, so we concat the candidates list */
2716
+ if (typeof bLine.candidates != 'undefined') {
2717
+ if (typeof candidates != 'undefined') {
2718
+ candidates = candidates.concat(bLine.candidates);
2719
+ } else {
2720
+ candidates = bLine.candidates;
2721
+ }
2722
+ }
2723
+
2724
+ if ((typeof iceUfrag != 'undefined') && (typeof bLine.iceUfrag != 'undefined') && (iceUfrag != bLine.iceUfrag)) {
2725
+ throw new Error("Only BUNDLE supported, iceUfrag must be the same for all m-lines.\n" +
2726
+ "\tLast iceUfrag: " + iceUfrag + "\n" +
2727
+ "\tNew iceUfrag: " + bLine.iceUfrag
2728
+ );
2729
+ }
2730
+
2731
+ if (typeof bLine.iceUfrag != 'undefined') {
2732
+ iceUfrag = bLine.iceUfrag;
2733
+ }
2734
+
2735
+ if ((typeof icePwd != 'undefined') && (typeof bLine.icePwd != 'undefined') && (icePwd != bLine.icePwd)) {
2736
+ throw new Error("Only BUNDLE supported, icePwd must be the same for all m-lines.\n" +
2737
+ "\tLast icePwd: " + icePwd + "\n" +
2738
+ "\tNew icePwd: " + bLine.icePwd
2739
+ );
2740
+ }
2741
+
2742
+ if (typeof bLine.icePwd != 'undefined') {
2743
+ icePwd = bLine.icePwd;
2744
+ }
2745
+
2746
+ if ((typeof fingerprint != 'undefined') && (typeof bLine.fingerprint != 'undefined') &&
2747
+ (fingerprint.type != bLine.fingerprint.type || fingerprint.hash != bLine.fingerprint.hash)) {
2748
+ throw new Error("Only BUNDLE supported, fingerprint must be the same for all m-lines.\n" +
2749
+ "\tLast fingerprint: " + JSON.stringify(fingerprint) + "\n" +
2750
+ "\tNew fingerprint: " + JSON.stringify(bLine.fingerprint)
2751
+ );
2752
+ }
2753
+
2754
+ if (typeof bLine.fingerprint != 'undefined') {
2755
+ fingerprint = bLine.fingerprint;
2756
+ }
2757
+
2758
+ payloads[bLine.type] = bLine.payloads;
2759
+ rtcpFb[bLine.type] = bLine.rtcpFb;
2760
+ rtp[bLine.type] = bLine.rtp;
2761
+
2762
+ // inverted ssrc group map
2763
+ var ssrc2group = {};
2764
+ if (typeof ssrcGroups !== 'undefined' && Array.isArray(ssrcGroups)) {
2765
+ ssrcGroups.forEach(function (ssrcGroup) {
2766
+ // XXX This might brake if an SSRC is in more than one group
2767
+ // for some reason.
2768
+ if (typeof ssrcGroup.ssrcs !== 'undefined' &&
2769
+ Array.isArray(ssrcGroup.ssrcs)) {
2770
+ ssrcGroup.ssrcs.forEach(function (ssrc) {
2771
+ if (typeof ssrc2group[ssrc] === 'undefined') {
2772
+ ssrc2group[ssrc] = [];
2773
+ }
2774
+
2775
+ ssrc2group[ssrc].push(ssrcGroup);
2776
+ });
2777
+ }
2778
+ });
2779
+ }
2780
+
2781
+ // ssrc to m-line index.
2782
+ var ssrc2ml = {};
2783
+
2784
+ if (typeof sources === 'object') {
2785
+
2786
+ // We'll use the "bLine" object as a prototype for each new "mLine"
2787
+ // that we create, but first we need to clean it up a bit.
2788
+ delete bLine.sources;
2789
+ delete bLine.ssrcGroups;
2790
+ delete bLine.candidates;
2791
+ delete bLine.iceUfrag;
2792
+ delete bLine.icePwd;
2793
+ delete bLine.fingerprint;
2794
+ delete bLine.port;
2795
+ delete bLine.mid;
2796
+
2797
+ // Explode the Plan B channel sources with one m-line per source.
2798
+ Object.keys(sources).forEach(function(ssrc) {
2799
+
2800
+ // The (unified) m-line for this SSRC. We either create it from
2801
+ // scratch or, if it's a grouped SSRC, we re-use a related
2802
+ // mline. In other words, if the source is grouped with another
2803
+ // source, put the two together in the same m-line.
2804
+ var uLine;
2805
+
2806
+ // We assume here that we are the answerer in the O/A, so any
2807
+ // offers which we translate come from the remote side, while
2808
+ // answers are local. So the check below is to make that we
2809
+ // handle receive-only SSRCs in a special way only if they come
2810
+ // from the remote side.
2811
+ if (desc.type==='offer') {
2812
+ // We want to detect SSRCs which are used by a remote peer
2813
+ // in an m-line with direction=recvonly (i.e. they are
2814
+ // being used for RTCP only).
2815
+ // This information would have gotten lost if the remote
2816
+ // peer used Unified Plan and their local description was
2817
+ // translated to Plan B. So we use the lack of an MSID
2818
+ // attribute to deduce a "receive only" SSRC.
2819
+ if (!sources[ssrc].msid) {
2820
+ recvonlySsrcs[bLine.type][ssrc] = sources[ssrc];
2821
+ // Receive-only SSRCs must not create new m-lines. We
2822
+ // will assign them to an existing m-line later.
2823
+ return;
2824
+ }
2825
+ }
2826
+
2827
+ if (typeof ssrc2group[ssrc] !== 'undefined' &&
2828
+ Array.isArray(ssrc2group[ssrc])) {
2829
+ ssrc2group[ssrc].some(function (ssrcGroup) {
2830
+ // ssrcGroup.ssrcs *is* an Array, no need to check
2831
+ // again here.
2832
+ return ssrcGroup.ssrcs.some(function (related) {
2833
+ if (typeof ssrc2ml[related] === 'object') {
2834
+ uLine = ssrc2ml[related];
2835
+ return true;
2836
+ }
2837
+ });
2838
+ });
2839
+ }
2840
+
2841
+ if (typeof uLine === 'object') {
2842
+ // the m-line already exists. Just add the source.
2843
+ uLine.sources[ssrc] = sources[ssrc];
2844
+ delete sources[ssrc].msid;
2845
+ } else {
2846
+ // Use the "bLine" as a prototype for the "uLine".
2847
+ uLine = Object.create(bLine);
2848
+ ssrc2ml[ssrc] = uLine;
2849
+
2850
+ if (typeof sources[ssrc].msid !== 'undefined') {
2851
+ // Assign the msid of the source to the m-line. Note
2852
+ // that it is not guaranteed that the source will have
2853
+ // msid. In particular "recvonly" sources don't have an
2854
+ // msid. Note that "recvonly" is a term only defined
2855
+ // for m-lines.
2856
+ uLine.msid = sources[ssrc].msid;
2857
+ delete sources[ssrc].msid;
2858
+ }
2859
+
2860
+ // We assign one SSRC per media line.
2861
+ uLine.sources = {};
2862
+ uLine.sources[ssrc] = sources[ssrc];
2863
+ uLine.ssrcGroups = ssrc2group[ssrc];
2864
+
2865
+ // Use the cached Unified Plan SDP (if it exists) to assign
2866
+ // SSRCs to mids.
2867
+ if (typeof cached !== 'undefined' &&
2868
+ typeof cached.media !== 'undefined' &&
2869
+ Array.isArray(cached.media)) {
2870
+
2871
+ cached.media.forEach(function (m) {
2872
+ if (typeof m.sources === 'object') {
2873
+ Object.keys(m.sources).forEach(function (s) {
2874
+ if (s === ssrc) {
2875
+ uLine.mid = m.mid;
2876
+ }
2877
+ });
2878
+ }
2879
+ });
2880
+ }
2881
+
2882
+ if (typeof uLine.mid === 'undefined') {
2883
+
2884
+ // If this is an SSRC that we see for the first time
2885
+ // assign it a new mid. This is typically the case when
2886
+ // this method is called to transform a remote
2887
+ // description for the first time or when there is a
2888
+ // new SSRC in the remote description because a new
2889
+ // peer has joined the conference. Local SSRCs should
2890
+ // have already been added to the map in the toPlanB
2891
+ // method.
2892
+ //
2893
+ // Because FF generates answers in Unified Plan style,
2894
+ // we MUST already have a cached answer with all the
2895
+ // local SSRCs mapped to some m-line/mid.
2896
+
2897
+ uLine.mid = [bLine.type, '-', ssrc].join('');
2898
+ }
2899
+
2900
+ // Include the candidates in the 1st media line.
2901
+ uLine.candidates = candidates;
2902
+ uLine.iceUfrag = iceUfrag;
2903
+ uLine.icePwd = icePwd;
2904
+ uLine.fingerprint = fingerprint;
2905
+ uLine.port = port;
2906
+
2907
+ mid2ul[uLine.mid] = uLine;
2908
+ sources2ul[uIdx] = uLine.sources;
2909
+
2910
+ self.cache.mlU2BMap[uIdx] = bIdx;
2911
+ if (typeof self.cache.mlB2UMap[bIdx] === 'undefined') {
2912
+ self.cache.mlB2UMap[bIdx] = uIdx;
2913
+ }
2914
+ uIdx++;
2915
+ }
2916
+ });
2917
+ } else {
2918
+ var uLine = bLine;
2919
+
2920
+ uLine.candidates = candidates;
2921
+ uLine.iceUfrag = iceUfrag;
2922
+ uLine.icePwd = icePwd;
2923
+ uLine.fingerprint = fingerprint;
2924
+ uLine.port = port;
2925
+
2926
+ mid2ul[uLine.mid] = uLine;
2927
+
2928
+ self.cache.mlU2BMap[uIdx] = bIdx;
2929
+ if (typeof self.cache.mlB2UMap[bIdx] === 'undefined') {
2930
+ self.cache.mlB2UMap[bIdx] = uIdx;
2931
+ }
2932
+ }
2933
+
2934
+ bIdx++;
2935
+ });
2936
+
2937
+ // Rebuild the media array in the right order and add the missing mLines
2938
+ // (missing from the Plan B SDP).
2939
+ session.media = [];
2940
+ mids = []; // reuse
2941
+
2942
+ if (desc.type === 'answer') {
2943
+
2944
+ // The media lines in the answer must match the media lines in the
2945
+ // offer. The order is important too. Here we assume that Firefox is
2946
+ // the answerer, so we merely have to use the reconstructed (unified)
2947
+ // answer to update the cached (unified) answer accordingly.
2948
+ //
2949
+ // In the general case, one would have to use the cached (unified)
2950
+ // offer to find the m-lines that are missing from the reconstructed
2951
+ // answer, potentially grabbing them from the cached (unified) answer.
2952
+ // One has to be careful with this approach because inactive m-lines do
2953
+ // not always have an mid, making it tricky (impossible?) to find where
2954
+ // exactly and which m-lines are missing from the reconstructed answer.
2955
+
2956
+ for (var i = 0; i < cached.media.length; i++) {
2957
+ var uLine = cached.media[i];
2958
+
2959
+ delete uLine.msid;
2960
+ delete uLine.sources;
2961
+ delete uLine.ssrcGroups;
2962
+
2963
+ if (typeof sources2ul[i] === 'undefined') {
2964
+ if (!uLine.direction
2965
+ || uLine.direction === 'sendrecv')
2966
+ uLine.direction = 'recvonly';
2967
+ else if (uLine.direction === 'sendonly')
2968
+ uLine.direction = 'inactive';
2969
+ } else {
2970
+ if (!uLine.direction
2971
+ || uLine.direction === 'sendrecv')
2972
+ uLine.direction = 'sendrecv';
2973
+ else if (uLine.direction === 'recvonly')
2974
+ uLine.direction = 'sendonly';
2975
+ }
2976
+
2977
+ uLine.sources = sources2ul[i];
2978
+ uLine.candidates = candidates;
2979
+ uLine.iceUfrag = iceUfrag;
2980
+ uLine.icePwd = icePwd;
2981
+ uLine.fingerprint = fingerprint;
2982
+
2983
+ uLine.rtp = rtp[uLine.type];
2984
+ uLine.payloads = payloads[uLine.type];
2985
+ uLine.rtcpFb = rtcpFb[uLine.type];
2986
+
2987
+ session.media.push(uLine);
2988
+
2989
+ if (typeof uLine.mid === 'string') {
2990
+ // inactive lines don't/may not have an mid.
2991
+ mids.push(uLine.mid);
2992
+ }
2993
+ }
2994
+ } else {
2995
+
2996
+ // SDP offer/answer (and the JSEP spec) forbids removing an m-section
2997
+ // under any circumstances. If we are no longer interested in sending a
2998
+ // track, we just remove the msid and ssrc attributes and set it to
2999
+ // either a=recvonly (as the reofferer, we must use recvonly if the
3000
+ // other side was previously sending on the m-section, but we can also
3001
+ // leave the possibility open if it wasn't previously in use), or
3002
+ // a=inactive.
3003
+
3004
+ if (typeof cached !== 'undefined' &&
3005
+ typeof cached.media !== 'undefined' &&
3006
+ Array.isArray(cached.media)) {
3007
+ cached.media.forEach(function(uLine) {
3008
+ mids.push(uLine.mid);
3009
+ if (typeof mid2ul[uLine.mid] !== 'undefined') {
3010
+ session.media.push(mid2ul[uLine.mid]);
3011
+ } else {
3012
+ delete uLine.msid;
3013
+ delete uLine.sources;
3014
+ delete uLine.ssrcGroups;
3015
+
3016
+ if (!uLine.direction
3017
+ || uLine.direction === 'sendrecv') {
3018
+ uLine.direction = 'sendonly';
3019
+ }
3020
+ if (!uLine.direction
3021
+ || uLine.direction === 'recvonly') {
3022
+ uLine.direction = 'inactive';
3023
+ }
3024
+
3025
+ addSetupAttr (uLine);
3026
+ session.media.push(uLine);
3027
+ }
3028
+ });
3029
+ }
3030
+
3031
+ // Add all the remaining (new) m-lines of the transformed SDP.
3032
+ Object.keys(mid2ul).forEach(function(mid) {
3033
+ if (mids.indexOf(mid) === -1) {
3034
+ mids.push(mid);
3035
+ if (mid2ul[mid].direction === 'recvonly') {
3036
+ // This is a remote recvonly channel. Add its SSRC to the
3037
+ // appropriate sendrecv or sendonly channel.
3038
+ // TODO(gp) what if we don't have sendrecv/sendonly
3039
+ // channel?
3040
+
3041
+ var done = false;
3042
+
3043
+ session.media.some(function (uLine) {
3044
+ if ((uLine.direction === 'sendrecv' ||
3045
+ uLine.direction === 'sendonly') &&
3046
+ uLine.type === mid2ul[mid].type) {
3047
+ // mid2ul[mid] shouldn't have any ssrc-groups
3048
+ Object.keys(mid2ul[mid].sources).forEach(
3049
+ function (ssrc) {
3050
+ uLine.sources[ssrc] =
3051
+ mid2ul[mid].sources[ssrc];
3052
+ });
3053
+
3054
+ done = true;
3055
+ return true;
3056
+ }
3057
+ });
3058
+
3059
+ if (!done) {
3060
+ session.media.push(mid2ul[mid]);
3061
+ }
3062
+ } else {
3063
+ session.media.push(mid2ul[mid]);
3064
+ }
3065
+ }
3066
+ });
3067
+ }
3068
+
3069
+ // After we have constructed the Plan Unified m-lines we can figure out
3070
+ // where (in which m-line) to place the 'recvonly SSRCs'.
3071
+ // Note: we assume here that we are the answerer in the O/A, so any offers
3072
+ // which we translate come from the remote side, while answers are local
3073
+ // (and so our last local description is cached as an 'answer').
3074
+ ["audio", "video"].forEach(function (type) {
3075
+ if (!session || !session.media || !Array.isArray(session.media))
3076
+ return;
3077
+
3078
+ var idx = null;
3079
+ if (Object.keys(recvonlySsrcs[type]).length > 0) {
3080
+ idx = self.getFirstSendingIndexFromAnswer(type);
3081
+ if (idx === null){
3082
+ // If this is the first offer we receive, we don't have a
3083
+ // cached answer. Assume that we will be sending media using
3084
+ // the first m-line for each media type.
3085
+
3086
+ for (var i = 0; i < session.media.length; i++) {
3087
+ if (session.media[i].type === type) {
3088
+ idx = i;
3089
+ break;
3090
+ }
3091
+ }
3092
+ }
3093
+ }
3094
+
3095
+ if (idx && session.media.length > idx) {
3096
+ var mLine = session.media[idx];
3097
+ Object.keys(recvonlySsrcs[type]).forEach(function(ssrc) {
3098
+ if (mLine.sources && mLine.sources[ssrc]) {
3099
+ console.warn("Replacing an existing SSRC.");
3100
+ }
3101
+ if (!mLine.sources) {
3102
+ mLine.sources = {};
3103
+ }
3104
+
3105
+ mLine.sources[ssrc] = recvonlySsrcs[type][ssrc];
3106
+ });
3107
+ }
3108
+ });
3109
+
3110
+ if (typeof session.groups !== 'undefined') {
3111
+ // We regenerate the BUNDLE group (since we regenerated the mids)
3112
+ session.groups.some(function(group) {
3113
+ if (group.type === 'BUNDLE') {
3114
+ group.mids = mids.join(' ');
3115
+ return true;
3116
+ }
3117
+ });
3118
+ }
3119
+
3120
+ // msid semantic
3121
+ session.msidSemantic = {
3122
+ semantic: 'WMS',
3123
+ token: '*'
3124
+ };
3125
+
3126
+ var resStr = transform.write(session);
3127
+
3128
+ // Cache the transformed SDP (Unified Plan) for later re-use in this
3129
+ // function.
3130
+ this.cache[desc.type] = resStr;
3131
+
3132
+ return new RTCSessionDescription({
3133
+ type: desc.type,
3134
+ sdp: resStr
3135
+ });
3136
+
3137
+ //#endregion
3138
+ };
3139
+
3140
+ },{"./array-equals":17,"./transform":20}],20:[function(require,module,exports){
3141
+ /* Copyright @ 2015 Atlassian Pty Ltd
3142
+ *
3143
+ * Licensed under the Apache License, Version 2.0 (the "License");
3144
+ * you may not use this file except in compliance with the License.
3145
+ * You may obtain a copy of the License at
3146
+ *
3147
+ * http://www.apache.org/licenses/LICENSE-2.0
3148
+ *
3149
+ * Unless required by applicable law or agreed to in writing, software
3150
+ * distributed under the License is distributed on an "AS IS" BASIS,
3151
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3152
+ * See the License for the specific language governing permissions and
3153
+ * limitations under the License.
3154
+ */
3155
+
3156
+ var transform = require('sdp-transform');
3157
+
3158
+ exports.write = function(session, opts) {
3159
+
3160
+ if (typeof session !== 'undefined' &&
3161
+ typeof session.media !== 'undefined' &&
3162
+ Array.isArray(session.media)) {
3163
+
3164
+ session.media.forEach(function (mLine) {
3165
+ // expand sources to ssrcs
3166
+ if (typeof mLine.sources !== 'undefined' &&
3167
+ Object.keys(mLine.sources).length !== 0) {
3168
+ mLine.ssrcs = [];
3169
+ Object.keys(mLine.sources).forEach(function (ssrc) {
3170
+ var source = mLine.sources[ssrc];
3171
+ Object.keys(source).forEach(function (attribute) {
3172
+ mLine.ssrcs.push({
3173
+ id: ssrc,
3174
+ attribute: attribute,
3175
+ value: source[attribute]
3176
+ });
3177
+ });
3178
+ });
3179
+ delete mLine.sources;
3180
+ }
3181
+
3182
+ // join ssrcs in ssrc groups
3183
+ if (typeof mLine.ssrcGroups !== 'undefined' &&
3184
+ Array.isArray(mLine.ssrcGroups)) {
3185
+ mLine.ssrcGroups.forEach(function (ssrcGroup) {
3186
+ if (typeof ssrcGroup.ssrcs !== 'undefined' &&
3187
+ Array.isArray(ssrcGroup.ssrcs)) {
3188
+ ssrcGroup.ssrcs = ssrcGroup.ssrcs.join(' ');
3189
+ }
3190
+ });
3191
+ }
3192
+ });
3193
+ }
3194
+
3195
+ // join group mids
3196
+ if (typeof session !== 'undefined' &&
3197
+ typeof session.groups !== 'undefined' && Array.isArray(session.groups)) {
3198
+
3199
+ session.groups.forEach(function (g) {
3200
+ if (typeof g.mids !== 'undefined' && Array.isArray(g.mids)) {
3201
+ g.mids = g.mids.join(' ');
3202
+ }
3203
+ });
3204
+ }
3205
+
3206
+ return transform.write(session, opts);
3207
+ };
3208
+
3209
+ exports.parse = function(sdp) {
3210
+ var session = transform.parse(sdp);
3211
+
3212
+ if (typeof session !== 'undefined' && typeof session.media !== 'undefined' &&
3213
+ Array.isArray(session.media)) {
3214
+
3215
+ session.media.forEach(function (mLine) {
3216
+ // group sources attributes by ssrc
3217
+ if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) {
3218
+ mLine.sources = {};
3219
+ mLine.ssrcs.forEach(function (ssrc) {
3220
+ if (!mLine.sources[ssrc.id])
3221
+ mLine.sources[ssrc.id] = {};
3222
+ mLine.sources[ssrc.id][ssrc.attribute] = ssrc.value;
3223
+ });
3224
+
3225
+ delete mLine.ssrcs;
3226
+ }
3227
+
3228
+ // split ssrcs in ssrc groups
3229
+ if (typeof mLine.ssrcGroups !== 'undefined' &&
3230
+ Array.isArray(mLine.ssrcGroups)) {
3231
+ mLine.ssrcGroups.forEach(function (ssrcGroup) {
3232
+ if (typeof ssrcGroup.ssrcs === 'string') {
3233
+ ssrcGroup.ssrcs = ssrcGroup.ssrcs.split(' ');
3234
+ }
3235
+ });
3236
+ }
3237
+ });
3238
+ }
3239
+ // split group mids
3240
+ if (typeof session !== 'undefined' &&
3241
+ typeof session.groups !== 'undefined' && Array.isArray(session.groups)) {
3242
+
3243
+ session.groups.forEach(function (g) {
3244
+ if (typeof g.mids === 'string') {
3245
+ g.mids = g.mids.split(' ');
3246
+ }
3247
+ });
3248
+ }
3249
+
3250
+ return session;
3251
+ };
3252
+
3253
+
3254
+ },{"sdp-transform":14}],21:[function(require,module,exports){
3255
+ /*!
3256
+ * UAParser.js v0.7.21
3257
+ * Lightweight JavaScript-based User-Agent string parser
3258
+ * https://github.com/faisalman/ua-parser-js
3259
+ *
3260
+ * Copyright © 2012-2019 Faisal Salman <f@faisalman.com>
3261
+ * Licensed under MIT License
3262
+ */
3263
+
3264
+ (function (window, undefined) {
3265
+
3266
+ 'use strict';
3267
+
3268
+ //////////////
3269
+ // Constants
3270
+ /////////////
3271
+
3272
+
3273
+ var LIBVERSION = '0.7.21',
3274
+ EMPTY = '',
3275
+ UNKNOWN = '?',
3276
+ FUNC_TYPE = 'function',
3277
+ UNDEF_TYPE = 'undefined',
3278
+ OBJ_TYPE = 'object',
3279
+ STR_TYPE = 'string',
3280
+ MAJOR = 'major', // deprecated
3281
+ MODEL = 'model',
3282
+ NAME = 'name',
3283
+ TYPE = 'type',
3284
+ VENDOR = 'vendor',
3285
+ VERSION = 'version',
3286
+ ARCHITECTURE= 'architecture',
3287
+ CONSOLE = 'console',
3288
+ MOBILE = 'mobile',
3289
+ TABLET = 'tablet',
3290
+ SMARTTV = 'smarttv',
3291
+ WEARABLE = 'wearable',
3292
+ EMBEDDED = 'embedded';
3293
+
3294
+
3295
+ ///////////
3296
+ // Helper
3297
+ //////////
3298
+
3299
+
3300
+ var util = {
3301
+ extend : function (regexes, extensions) {
3302
+ var mergedRegexes = {};
3303
+ for (var i in regexes) {
3304
+ if (extensions[i] && extensions[i].length % 2 === 0) {
3305
+ mergedRegexes[i] = extensions[i].concat(regexes[i]);
3306
+ } else {
3307
+ mergedRegexes[i] = regexes[i];
3308
+ }
3309
+ }
3310
+ return mergedRegexes;
3311
+ },
3312
+ has : function (str1, str2) {
3313
+ if (typeof str1 === "string") {
3314
+ return str2.toLowerCase().indexOf(str1.toLowerCase()) !== -1;
3315
+ } else {
3316
+ return false;
3317
+ }
3318
+ },
3319
+ lowerize : function (str) {
3320
+ return str.toLowerCase();
3321
+ },
3322
+ major : function (version) {
3323
+ return typeof(version) === STR_TYPE ? version.replace(/[^\d\.]/g,'').split(".")[0] : undefined;
3324
+ },
3325
+ trim : function (str) {
3326
+ return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
3327
+ }
3328
+ };
3329
+
3330
+
3331
+ ///////////////
3332
+ // Map helper
3333
+ //////////////
3334
+
3335
+
3336
+ var mapper = {
3337
+
3338
+ rgx : function (ua, arrays) {
3339
+
3340
+ var i = 0, j, k, p, q, matches, match;
3341
+
3342
+ // loop through all regexes maps
3343
+ while (i < arrays.length && !matches) {
3344
+
3345
+ var regex = arrays[i], // even sequence (0,2,4,..)
3346
+ props = arrays[i + 1]; // odd sequence (1,3,5,..)
3347
+ j = k = 0;
3348
+
3349
+ // try matching uastring with regexes
3350
+ while (j < regex.length && !matches) {
3351
+
3352
+ matches = regex[j++].exec(ua);
3353
+
3354
+ if (!!matches) {
3355
+ for (p = 0; p < props.length; p++) {
3356
+ match = matches[++k];
3357
+ q = props[p];
3358
+ // check if given property is actually array
3359
+ if (typeof q === OBJ_TYPE && q.length > 0) {
3360
+ if (q.length == 2) {
3361
+ if (typeof q[1] == FUNC_TYPE) {
3362
+ // assign modified match
3363
+ this[q[0]] = q[1].call(this, match);
3364
+ } else {
3365
+ // assign given value, ignore regex match
3366
+ this[q[0]] = q[1];
3367
+ }
3368
+ } else if (q.length == 3) {
3369
+ // check whether function or regex
3370
+ if (typeof q[1] === FUNC_TYPE && !(q[1].exec && q[1].test)) {
3371
+ // call function (usually string mapper)
3372
+ this[q[0]] = match ? q[1].call(this, match, q[2]) : undefined;
3373
+ } else {
3374
+ // sanitize match using given regex
3375
+ this[q[0]] = match ? match.replace(q[1], q[2]) : undefined;
3376
+ }
3377
+ } else if (q.length == 4) {
3378
+ this[q[0]] = match ? q[3].call(this, match.replace(q[1], q[2])) : undefined;
3379
+ }
3380
+ } else {
3381
+ this[q] = match ? match : undefined;
3382
+ }
3383
+ }
3384
+ }
3385
+ }
3386
+ i += 2;
3387
+ }
3388
+ },
3389
+
3390
+ str : function (str, map) {
3391
+
3392
+ for (var i in map) {
3393
+ // check if array
3394
+ if (typeof map[i] === OBJ_TYPE && map[i].length > 0) {
3395
+ for (var j = 0; j < map[i].length; j++) {
3396
+ if (util.has(map[i][j], str)) {
3397
+ return (i === UNKNOWN) ? undefined : i;
3398
+ }
3399
+ }
3400
+ } else if (util.has(map[i], str)) {
3401
+ return (i === UNKNOWN) ? undefined : i;
3402
+ }
3403
+ }
3404
+ return str;
3405
+ }
3406
+ };
3407
+
3408
+
3409
+ ///////////////
3410
+ // String map
3411
+ //////////////
3412
+
3413
+
3414
+ var maps = {
3415
+
3416
+ browser : {
3417
+ oldsafari : {
3418
+ version : {
3419
+ '1.0' : '/8',
3420
+ '1.2' : '/1',
3421
+ '1.3' : '/3',
3422
+ '2.0' : '/412',
3423
+ '2.0.2' : '/416',
3424
+ '2.0.3' : '/417',
3425
+ '2.0.4' : '/419',
3426
+ '?' : '/'
3427
+ }
3428
+ }
3429
+ },
3430
+
3431
+ device : {
3432
+ amazon : {
3433
+ model : {
3434
+ 'Fire Phone' : ['SD', 'KF']
3435
+ }
3436
+ },
3437
+ sprint : {
3438
+ model : {
3439
+ 'Evo Shift 4G' : '7373KT'
3440
+ },
3441
+ vendor : {
3442
+ 'HTC' : 'APA',
3443
+ 'Sprint' : 'Sprint'
3444
+ }
3445
+ }
3446
+ },
3447
+
3448
+ os : {
3449
+ windows : {
3450
+ version : {
3451
+ 'ME' : '4.90',
3452
+ 'NT 3.11' : 'NT3.51',
3453
+ 'NT 4.0' : 'NT4.0',
3454
+ '2000' : 'NT 5.0',
3455
+ 'XP' : ['NT 5.1', 'NT 5.2'],
3456
+ 'Vista' : 'NT 6.0',
3457
+ '7' : 'NT 6.1',
3458
+ '8' : 'NT 6.2',
3459
+ '8.1' : 'NT 6.3',
3460
+ '10' : ['NT 6.4', 'NT 10.0'],
3461
+ 'RT' : 'ARM'
3462
+ }
3463
+ }
3464
+ }
3465
+ };
3466
+
3467
+
3468
+ //////////////
3469
+ // Regex map
3470
+ /////////////
3471
+
3472
+
3473
+ var regexes = {
3474
+
3475
+ browser : [[
3476
+
3477
+ // Presto based
3478
+ /(opera\smini)\/([\w\.-]+)/i, // Opera Mini
3479
+ /(opera\s[mobiletab]+).+version\/([\w\.-]+)/i, // Opera Mobi/Tablet
3480
+ /(opera).+version\/([\w\.]+)/i, // Opera > 9.80
3481
+ /(opera)[\/\s]+([\w\.]+)/i // Opera < 9.80
3482
+ ], [NAME, VERSION], [
3483
+
3484
+ /(opios)[\/\s]+([\w\.]+)/i // Opera mini on iphone >= 8.0
3485
+ ], [[NAME, 'Opera Mini'], VERSION], [
3486
+
3487
+ /\s(opr)\/([\w\.]+)/i // Opera Webkit
3488
+ ], [[NAME, 'Opera'], VERSION], [
3489
+
3490
+ // Mixed
3491
+ /(kindle)\/([\w\.]+)/i, // Kindle
3492
+ /(lunascape|maxthon|netfront|jasmine|blazer)[\/\s]?([\w\.]*)/i,
3493
+ // Lunascape/Maxthon/Netfront/Jasmine/Blazer
3494
+ // Trident based
3495
+ /(avant\s|iemobile|slim)(?:browser)?[\/\s]?([\w\.]*)/i,
3496
+ // Avant/IEMobile/SlimBrowser
3497
+ /(bidubrowser|baidubrowser)[\/\s]?([\w\.]+)/i, // Baidu Browser
3498
+ /(?:ms|\()(ie)\s([\w\.]+)/i, // Internet Explorer
3499
+
3500
+ // Webkit/KHTML based
3501
+ /(rekonq)\/([\w\.]*)/i, // Rekonq
3502
+ /(chromium|flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs|bowser|quark|qupzilla|falkon)\/([\w\.-]+)/i
3503
+ // Chromium/Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS/Bowser/QupZilla/Falkon
3504
+ ], [NAME, VERSION], [
3505
+
3506
+ /(konqueror)\/([\w\.]+)/i // Konqueror
3507
+ ], [[NAME, 'Konqueror'], VERSION], [
3508
+
3509
+ /(trident).+rv[:\s]([\w\.]+).+like\sgecko/i // IE11
3510
+ ], [[NAME, 'IE'], VERSION], [
3511
+
3512
+ /(edge|edgios|edga|edg)\/((\d+)?[\w\.]+)/i // Microsoft Edge
3513
+ ], [[NAME, 'Edge'], VERSION], [
3514
+
3515
+ /(yabrowser)\/([\w\.]+)/i // Yandex
3516
+ ], [[NAME, 'Yandex'], VERSION], [
3517
+
3518
+ /(Avast)\/([\w\.]+)/i // Avast Secure Browser
3519
+ ], [[NAME, 'Avast Secure Browser'], VERSION], [
3520
+
3521
+ /(AVG)\/([\w\.]+)/i // AVG Secure Browser
3522
+ ], [[NAME, 'AVG Secure Browser'], VERSION], [
3523
+
3524
+ /(puffin)\/([\w\.]+)/i // Puffin
3525
+ ], [[NAME, 'Puffin'], VERSION], [
3526
+
3527
+ /(focus)\/([\w\.]+)/i // Firefox Focus
3528
+ ], [[NAME, 'Firefox Focus'], VERSION], [
3529
+
3530
+ /(opt)\/([\w\.]+)/i // Opera Touch
3531
+ ], [[NAME, 'Opera Touch'], VERSION], [
3532
+
3533
+ /((?:[\s\/])uc?\s?browser|(?:juc.+)ucweb)[\/\s]?([\w\.]+)/i // UCBrowser
3534
+ ], [[NAME, 'UCBrowser'], VERSION], [
3535
+
3536
+ /(comodo_dragon)\/([\w\.]+)/i // Comodo Dragon
3537
+ ], [[NAME, /_/g, ' '], VERSION], [
3538
+
3539
+ /(windowswechat qbcore)\/([\w\.]+)/i // WeChat Desktop for Windows Built-in Browser
3540
+ ], [[NAME, 'WeChat(Win) Desktop'], VERSION], [
3541
+
3542
+ /(micromessenger)\/([\w\.]+)/i // WeChat
3543
+ ], [[NAME, 'WeChat'], VERSION], [
3544
+
3545
+ /(brave)\/([\w\.]+)/i // Brave browser
3546
+ ], [[NAME, 'Brave'], VERSION], [
3547
+
3548
+ /(qqbrowserlite)\/([\w\.]+)/i // QQBrowserLite
3549
+ ], [NAME, VERSION], [
3550
+
3551
+ /(QQ)\/([\d\.]+)/i // QQ, aka ShouQ
3552
+ ], [NAME, VERSION], [
3553
+
3554
+ /m?(qqbrowser)[\/\s]?([\w\.]+)/i // QQBrowser
3555
+ ], [NAME, VERSION], [
3556
+
3557
+ /(baiduboxapp)[\/\s]?([\w\.]+)/i // Baidu App
3558
+ ], [NAME, VERSION], [
3559
+
3560
+ /(2345Explorer)[\/\s]?([\w\.]+)/i // 2345 Browser
3561
+ ], [NAME, VERSION], [
3562
+
3563
+ /(MetaSr)[\/\s]?([\w\.]+)/i // SouGouBrowser
3564
+ ], [NAME], [
3565
+
3566
+ /(LBBROWSER)/i // LieBao Browser
3567
+ ], [NAME], [
3568
+
3569
+ /xiaomi\/miuibrowser\/([\w\.]+)/i // MIUI Browser
3570
+ ], [VERSION, [NAME, 'MIUI Browser']], [
3571
+
3572
+ /;fbav\/([\w\.]+);/i // Facebook App for iOS & Android
3573
+ ], [VERSION, [NAME, 'Facebook']], [
3574
+
3575
+ /safari\s(line)\/([\w\.]+)/i, // Line App for iOS
3576
+ /android.+(line)\/([\w\.]+)\/iab/i // Line App for Android
3577
+ ], [NAME, VERSION], [
3578
+
3579
+ /headlesschrome(?:\/([\w\.]+)|\s)/i // Chrome Headless
3580
+ ], [VERSION, [NAME, 'Chrome Headless']], [
3581
+
3582
+ /\swv\).+(chrome)\/([\w\.]+)/i // Chrome WebView
3583
+ ], [[NAME, /(.+)/, '$1 WebView'], VERSION], [
3584
+
3585
+ /((?:oculus|samsung)browser)\/([\w\.]+)/i
3586
+ ], [[NAME, /(.+(?:g|us))(.+)/, '$1 $2'], VERSION], [ // Oculus / Samsung Browser
3587
+
3588
+ /android.+version\/([\w\.]+)\s+(?:mobile\s?safari|safari)*/i // Android Browser
3589
+ ], [VERSION, [NAME, 'Android Browser']], [
3590
+
3591
+ /(sailfishbrowser)\/([\w\.]+)/i // Sailfish Browser
3592
+ ], [[NAME, 'Sailfish Browser'], VERSION], [
3593
+
3594
+ /(chrome|omniweb|arora|[tizenoka]{5}\s?browser)\/v?([\w\.]+)/i
3595
+ // Chrome/OmniWeb/Arora/Tizen/Nokia
3596
+ ], [NAME, VERSION], [
3597
+
3598
+ /(dolfin)\/([\w\.]+)/i // Dolphin
3599
+ ], [[NAME, 'Dolphin'], VERSION], [
3600
+
3601
+ /(qihu|qhbrowser|qihoobrowser|360browser)/i // 360
3602
+ ], [[NAME, '360 Browser']], [
3603
+
3604
+ /((?:android.+)crmo|crios)\/([\w\.]+)/i // Chrome for Android/iOS
3605
+ ], [[NAME, 'Chrome'], VERSION], [
3606
+
3607
+ /(coast)\/([\w\.]+)/i // Opera Coast
3608
+ ], [[NAME, 'Opera Coast'], VERSION], [
3609
+
3610
+ /fxios\/([\w\.-]+)/i // Firefox for iOS
3611
+ ], [VERSION, [NAME, 'Firefox']], [
3612
+
3613
+ /version\/([\w\.]+).+?mobile\/\w+\s(safari)/i // Mobile Safari
3614
+ ], [VERSION, [NAME, 'Mobile Safari']], [
3615
+
3616
+ /version\/([\w\.]+).+?(mobile\s?safari|safari)/i // Safari & Safari Mobile
3617
+ ], [VERSION, NAME], [
3618
+
3619
+ /webkit.+?(gsa)\/([\w\.]+).+?(mobile\s?safari|safari)(\/[\w\.]+)/i // Google Search Appliance on iOS
3620
+ ], [[NAME, 'GSA'], VERSION], [
3621
+
3622
+ /webkit.+?(mobile\s?safari|safari)(\/[\w\.]+)/i // Safari < 3.0
3623
+ ], [NAME, [VERSION, mapper.str, maps.browser.oldsafari.version]], [
3624
+
3625
+ /(webkit|khtml)\/([\w\.]+)/i
3626
+ ], [NAME, VERSION], [
3627
+
3628
+ // Gecko based
3629
+ /(navigator|netscape)\/([\w\.-]+)/i // Netscape
3630
+ ], [[NAME, 'Netscape'], VERSION], [
3631
+ /(swiftfox)/i, // Swiftfox
3632
+ /(icedragon|iceweasel|camino|chimera|fennec|maemo\sbrowser|minimo|conkeror)[\/\s]?([\w\.\+]+)/i,
3633
+ // IceDragon/Iceweasel/Camino/Chimera/Fennec/Maemo/Minimo/Conkeror
3634
+ /(firefox|seamonkey|k-meleon|icecat|iceape|firebird|phoenix|palemoon|basilisk|waterfox)\/([\w\.-]+)$/i,
3635
+
3636
+ // Firefox/SeaMonkey/K-Meleon/IceCat/IceApe/Firebird/Phoenix
3637
+ /(mozilla)\/([\w\.]+).+rv\:.+gecko\/\d+/i, // Mozilla
3638
+
3639
+ // Other
3640
+ /(polaris|lynx|dillo|icab|doris|amaya|w3m|netsurf|sleipnir)[\/\s]?([\w\.]+)/i,
3641
+ // Polaris/Lynx/Dillo/iCab/Doris/Amaya/w3m/NetSurf/Sleipnir
3642
+ /(links)\s\(([\w\.]+)/i, // Links
3643
+ /(gobrowser)\/?([\w\.]*)/i, // GoBrowser
3644
+ /(ice\s?browser)\/v?([\w\._]+)/i, // ICE Browser
3645
+ /(mosaic)[\/\s]([\w\.]+)/i // Mosaic
3646
+ ], [NAME, VERSION]
3647
+ ],
3648
+
3649
+ cpu : [[
3650
+
3651
+ /(?:(amd|x(?:(?:86|64)[_-])?|wow|win)64)[;\)]/i // AMD64
3652
+ ], [[ARCHITECTURE, 'amd64']], [
3653
+
3654
+ /(ia32(?=;))/i // IA32 (quicktime)
3655
+ ], [[ARCHITECTURE, util.lowerize]], [
3656
+
3657
+ /((?:i[346]|x)86)[;\)]/i // IA32
3658
+ ], [[ARCHITECTURE, 'ia32']], [
3659
+
3660
+ // PocketPC mistakenly identified as PowerPC
3661
+ /windows\s(ce|mobile);\sppc;/i
3662
+ ], [[ARCHITECTURE, 'arm']], [
3663
+
3664
+ /((?:ppc|powerpc)(?:64)?)(?:\smac|;|\))/i // PowerPC
3665
+ ], [[ARCHITECTURE, /ower/, '', util.lowerize]], [
3666
+
3667
+ /(sun4\w)[;\)]/i // SPARC
3668
+ ], [[ARCHITECTURE, 'sparc']], [
3669
+
3670
+ /((?:avr32|ia64(?=;))|68k(?=\))|arm(?:64|(?=v\d+[;l]))|(?=atmel\s)avr|(?:irix|mips|sparc)(?:64)?(?=;)|pa-risc)/i
3671
+ // IA64, 68K, ARM/64, AVR/32, IRIX/64, MIPS/64, SPARC/64, PA-RISC
3672
+ ], [[ARCHITECTURE, util.lowerize]]
3673
+ ],
3674
+
3675
+ device : [[
3676
+
3677
+ /\((ipad|playbook);[\w\s\),;-]+(rim|apple)/i // iPad/PlayBook
3678
+ ], [MODEL, VENDOR, [TYPE, TABLET]], [
3679
+
3680
+ /applecoremedia\/[\w\.]+ \((ipad)/ // iPad
3681
+ ], [MODEL, [VENDOR, 'Apple'], [TYPE, TABLET]], [
3682
+
3683
+ /(apple\s{0,1}tv)/i // Apple TV
3684
+ ], [[MODEL, 'Apple TV'], [VENDOR, 'Apple'], [TYPE, SMARTTV]], [
3685
+
3686
+ /(archos)\s(gamepad2?)/i, // Archos
3687
+ /(hp).+(touchpad)/i, // HP TouchPad
3688
+ /(hp).+(tablet)/i, // HP Tablet
3689
+ /(kindle)\/([\w\.]+)/i, // Kindle
3690
+ /\s(nook)[\w\s]+build\/(\w+)/i, // Nook
3691
+ /(dell)\s(strea[kpr\s\d]*[\dko])/i // Dell Streak
3692
+ ], [VENDOR, MODEL, [TYPE, TABLET]], [
3693
+
3694
+ /(kf[A-z]+)\sbuild\/.+silk\//i // Kindle Fire HD
3695
+ ], [MODEL, [VENDOR, 'Amazon'], [TYPE, TABLET]], [
3696
+ /(sd|kf)[0349hijorstuw]+\sbuild\/.+silk\//i // Fire Phone
3697
+ ], [[MODEL, mapper.str, maps.device.amazon.model], [VENDOR, 'Amazon'], [TYPE, MOBILE]], [
3698
+ /android.+aft([bms])\sbuild/i // Fire TV
3699
+ ], [MODEL, [VENDOR, 'Amazon'], [TYPE, SMARTTV]], [
3700
+
3701
+ /\((ip[honed|\s\w*]+);.+(apple)/i // iPod/iPhone
3702
+ ], [MODEL, VENDOR, [TYPE, MOBILE]], [
3703
+ /\((ip[honed|\s\w*]+);/i // iPod/iPhone
3704
+ ], [MODEL, [VENDOR, 'Apple'], [TYPE, MOBILE]], [
3705
+
3706
+ /(blackberry)[\s-]?(\w+)/i, // BlackBerry
3707
+ /(blackberry|benq|palm(?=\-)|sonyericsson|acer|asus|dell|meizu|motorola|polytron)[\s_-]?([\w-]*)/i,
3708
+ // BenQ/Palm/Sony-Ericsson/Acer/Asus/Dell/Meizu/Motorola/Polytron
3709
+ /(hp)\s([\w\s]+\w)/i, // HP iPAQ
3710
+ /(asus)-?(\w+)/i // Asus
3711
+ ], [VENDOR, MODEL, [TYPE, MOBILE]], [
3712
+ /\(bb10;\s(\w+)/i // BlackBerry 10
3713
+ ], [MODEL, [VENDOR, 'BlackBerry'], [TYPE, MOBILE]], [
3714
+ // Asus Tablets
3715
+ /android.+(transfo[prime\s]{4,10}\s\w+|eeepc|slider\s\w+|nexus 7|padfone|p00c)/i
3716
+ ], [MODEL, [VENDOR, 'Asus'], [TYPE, TABLET]], [
3717
+
3718
+ /(sony)\s(tablet\s[ps])\sbuild\//i, // Sony
3719
+ /(sony)?(?:sgp.+)\sbuild\//i
3720
+ ], [[VENDOR, 'Sony'], [MODEL, 'Xperia Tablet'], [TYPE, TABLET]], [
3721
+ /android.+\s([c-g]\d{4}|so[-l]\w+)(?=\sbuild\/|\).+chrome\/(?![1-6]{0,1}\d\.))/i
3722
+ ], [MODEL, [VENDOR, 'Sony'], [TYPE, MOBILE]], [
3723
+
3724
+ /\s(ouya)\s/i, // Ouya
3725
+ /(nintendo)\s([wids3u]+)/i // Nintendo
3726
+ ], [VENDOR, MODEL, [TYPE, CONSOLE]], [
3727
+
3728
+ /android.+;\s(shield)\sbuild/i // Nvidia
3729
+ ], [MODEL, [VENDOR, 'Nvidia'], [TYPE, CONSOLE]], [
3730
+
3731
+ /(playstation\s[34portablevi]+)/i // Playstation
3732
+ ], [MODEL, [VENDOR, 'Sony'], [TYPE, CONSOLE]], [
3733
+
3734
+ /(sprint\s(\w+))/i // Sprint Phones
3735
+ ], [[VENDOR, mapper.str, maps.device.sprint.vendor], [MODEL, mapper.str, maps.device.sprint.model], [TYPE, MOBILE]], [
3736
+
3737
+ /(htc)[;_\s-]+([\w\s]+(?=\)|\sbuild)|\w+)/i, // HTC
3738
+ /(zte)-(\w*)/i, // ZTE
3739
+ /(alcatel|geeksphone|nexian|panasonic|(?=;\s)sony)[_\s-]?([\w-]*)/i
3740
+ // Alcatel/GeeksPhone/Nexian/Panasonic/Sony
3741
+ ], [VENDOR, [MODEL, /_/g, ' '], [TYPE, MOBILE]], [
3742
+
3743
+ /(nexus\s9)/i // HTC Nexus 9
3744
+ ], [MODEL, [VENDOR, 'HTC'], [TYPE, TABLET]], [
3745
+
3746
+ /d\/huawei([\w\s-]+)[;\)]/i,
3747
+ /(nexus\s6p|vog-l29|ane-lx1|eml-l29)/i // Huawei
3748
+ ], [MODEL, [VENDOR, 'Huawei'], [TYPE, MOBILE]], [
3749
+
3750
+ /android.+(bah2?-a?[lw]\d{2})/i // Huawei MediaPad
3751
+ ], [MODEL, [VENDOR, 'Huawei'], [TYPE, TABLET]], [
3752
+
3753
+ /(microsoft);\s(lumia[\s\w]+)/i // Microsoft Lumia
3754
+ ], [VENDOR, MODEL, [TYPE, MOBILE]], [
3755
+
3756
+ /[\s\(;](xbox(?:\sone)?)[\s\);]/i // Microsoft Xbox
3757
+ ], [MODEL, [VENDOR, 'Microsoft'], [TYPE, CONSOLE]], [
3758
+ /(kin\.[onetw]{3})/i // Microsoft Kin
3759
+ ], [[MODEL, /\./g, ' '], [VENDOR, 'Microsoft'], [TYPE, MOBILE]], [
3760
+
3761
+ // Motorola
3762
+ /\s(milestone|droid(?:[2-4x]|\s(?:bionic|x2|pro|razr))?:?(\s4g)?)[\w\s]+build\//i,
3763
+ /mot[\s-]?(\w*)/i,
3764
+ /(XT\d{3,4}) build\//i,
3765
+ /(nexus\s6)/i
3766
+ ], [MODEL, [VENDOR, 'Motorola'], [TYPE, MOBILE]], [
3767
+ /android.+\s(mz60\d|xoom[\s2]{0,2})\sbuild\//i
3768
+ ], [MODEL, [VENDOR, 'Motorola'], [TYPE, TABLET]], [
3769
+
3770
+ /hbbtv\/\d+\.\d+\.\d+\s+\([\w\s]*;\s*(\w[^;]*);([^;]*)/i // HbbTV devices
3771
+ ], [[VENDOR, util.trim], [MODEL, util.trim], [TYPE, SMARTTV]], [
3772
+
3773
+ /hbbtv.+maple;(\d+)/i
3774
+ ], [[MODEL, /^/, 'SmartTV'], [VENDOR, 'Samsung'], [TYPE, SMARTTV]], [
3775
+
3776
+ /\(dtv[\);].+(aquos)/i // Sharp
3777
+ ], [MODEL, [VENDOR, 'Sharp'], [TYPE, SMARTTV]], [
3778
+
3779
+ /android.+((sch-i[89]0\d|shw-m380s|gt-p\d{4}|gt-n\d+|sgh-t8[56]9|nexus 10))/i,
3780
+ /((SM-T\w+))/i
3781
+ ], [[VENDOR, 'Samsung'], MODEL, [TYPE, TABLET]], [ // Samsung
3782
+ /smart-tv.+(samsung)/i
3783
+ ], [VENDOR, [TYPE, SMARTTV], MODEL], [
3784
+ /((s[cgp]h-\w+|gt-\w+|galaxy\snexus|sm-\w[\w\d]+))/i,
3785
+ /(sam[sung]*)[\s-]*(\w+-?[\w-]*)/i,
3786
+ /sec-((sgh\w+))/i
3787
+ ], [[VENDOR, 'Samsung'], MODEL, [TYPE, MOBILE]], [
3788
+
3789
+ /sie-(\w*)/i // Siemens
3790
+ ], [MODEL, [VENDOR, 'Siemens'], [TYPE, MOBILE]], [
3791
+
3792
+ /(maemo|nokia).*(n900|lumia\s\d+)/i, // Nokia
3793
+ /(nokia)[\s_-]?([\w-]*)/i
3794
+ ], [[VENDOR, 'Nokia'], MODEL, [TYPE, MOBILE]], [
3795
+
3796
+ /android[x\d\.\s;]+\s([ab][1-7]\-?[0178a]\d\d?)/i // Acer
3797
+ ], [MODEL, [VENDOR, 'Acer'], [TYPE, TABLET]], [
3798
+
3799
+ /android.+([vl]k\-?\d{3})\s+build/i // LG Tablet
3800
+ ], [MODEL, [VENDOR, 'LG'], [TYPE, TABLET]], [
3801
+ /android\s3\.[\s\w;-]{10}(lg?)-([06cv9]{3,4})/i // LG Tablet
3802
+ ], [[VENDOR, 'LG'], MODEL, [TYPE, TABLET]], [
3803
+ /(lg) netcast\.tv/i // LG SmartTV
3804
+ ], [VENDOR, MODEL, [TYPE, SMARTTV]], [
3805
+ /(nexus\s[45])/i, // LG
3806
+ /lg[e;\s\/-]+(\w*)/i,
3807
+ /android.+lg(\-?[\d\w]+)\s+build/i
3808
+ ], [MODEL, [VENDOR, 'LG'], [TYPE, MOBILE]], [
3809
+
3810
+ /(lenovo)\s?(s(?:5000|6000)(?:[\w-]+)|tab(?:[\s\w]+))/i // Lenovo tablets
3811
+ ], [VENDOR, MODEL, [TYPE, TABLET]], [
3812
+ /android.+(ideatab[a-z0-9\-\s]+)/i // Lenovo
3813
+ ], [MODEL, [VENDOR, 'Lenovo'], [TYPE, TABLET]], [
3814
+ /(lenovo)[_\s-]?([\w-]+)/i
3815
+ ], [VENDOR, MODEL, [TYPE, MOBILE]], [
3816
+
3817
+ /linux;.+((jolla));/i // Jolla
3818
+ ], [VENDOR, MODEL, [TYPE, MOBILE]], [
3819
+
3820
+ /((pebble))app\/[\d\.]+\s/i // Pebble
3821
+ ], [VENDOR, MODEL, [TYPE, WEARABLE]], [
3822
+
3823
+ /android.+;\s(oppo)\s?([\w\s]+)\sbuild/i // OPPO
3824
+ ], [VENDOR, MODEL, [TYPE, MOBILE]], [
3825
+
3826
+ /crkey/i // Google Chromecast
3827
+ ], [[MODEL, 'Chromecast'], [VENDOR, 'Google'], [TYPE, SMARTTV]], [
3828
+
3829
+ /android.+;\s(glass)\s\d/i // Google Glass
3830
+ ], [MODEL, [VENDOR, 'Google'], [TYPE, WEARABLE]], [
3831
+
3832
+ /android.+;\s(pixel c)[\s)]/i // Google Pixel C
3833
+ ], [MODEL, [VENDOR, 'Google'], [TYPE, TABLET]], [
3834
+
3835
+ /android.+;\s(pixel( [23])?( xl)?)[\s)]/i // Google Pixel
3836
+ ], [MODEL, [VENDOR, 'Google'], [TYPE, MOBILE]], [
3837
+
3838
+ /android.+;\s(\w+)\s+build\/hm\1/i, // Xiaomi Hongmi 'numeric' models
3839
+ /android.+(hm[\s\-_]*note?[\s_]*(?:\d\w)?)\s+build/i, // Xiaomi Hongmi
3840
+ /android.+(mi[\s\-_]*(?:a\d|one|one[\s_]plus|note lte)?[\s_]*(?:\d?\w?)[\s_]*(?:plus)?)\s+build/i,
3841
+ // Xiaomi Mi
3842
+ /android.+(redmi[\s\-_]*(?:note)?(?:[\s_]*[\w\s]+))\s+build/i // Redmi Phones
3843
+ ], [[MODEL, /_/g, ' '], [VENDOR, 'Xiaomi'], [TYPE, MOBILE]], [
3844
+ /android.+(mi[\s\-_]*(?:pad)(?:[\s_]*[\w\s]+))\s+build/i // Mi Pad tablets
3845
+ ],[[MODEL, /_/g, ' '], [VENDOR, 'Xiaomi'], [TYPE, TABLET]], [
3846
+ /android.+;\s(m[1-5]\snote)\sbuild/i // Meizu
3847
+ ], [MODEL, [VENDOR, 'Meizu'], [TYPE, MOBILE]], [
3848
+ /(mz)-([\w-]{2,})/i
3849
+ ], [[VENDOR, 'Meizu'], MODEL, [TYPE, MOBILE]], [
3850
+
3851
+ /android.+a000(1)\s+build/i, // OnePlus
3852
+ /android.+oneplus\s(a\d{4})[\s)]/i
3853
+ ], [MODEL, [VENDOR, 'OnePlus'], [TYPE, MOBILE]], [
3854
+
3855
+ /android.+[;\/]\s*(RCT[\d\w]+)\s+build/i // RCA Tablets
3856
+ ], [MODEL, [VENDOR, 'RCA'], [TYPE, TABLET]], [
3857
+
3858
+ /android.+[;\/\s]+(Venue[\d\s]{2,7})\s+build/i // Dell Venue Tablets
3859
+ ], [MODEL, [VENDOR, 'Dell'], [TYPE, TABLET]], [
3860
+
3861
+ /android.+[;\/]\s*(Q[T|M][\d\w]+)\s+build/i // Verizon Tablet
3862
+ ], [MODEL, [VENDOR, 'Verizon'], [TYPE, TABLET]], [
3863
+
3864
+ /android.+[;\/]\s+(Barnes[&\s]+Noble\s+|BN[RT])(V?.*)\s+build/i // Barnes & Noble Tablet
3865
+ ], [[VENDOR, 'Barnes & Noble'], MODEL, [TYPE, TABLET]], [
3866
+
3867
+ /android.+[;\/]\s+(TM\d{3}.*\b)\s+build/i // Barnes & Noble Tablet
3868
+ ], [MODEL, [VENDOR, 'NuVision'], [TYPE, TABLET]], [
3869
+
3870
+ /android.+;\s(k88)\sbuild/i // ZTE K Series Tablet
3871
+ ], [MODEL, [VENDOR, 'ZTE'], [TYPE, TABLET]], [
3872
+
3873
+ /android.+[;\/]\s*(gen\d{3})\s+build.*49h/i // Swiss GEN Mobile
3874
+ ], [MODEL, [VENDOR, 'Swiss'], [TYPE, MOBILE]], [
3875
+
3876
+ /android.+[;\/]\s*(zur\d{3})\s+build/i // Swiss ZUR Tablet
3877
+ ], [MODEL, [VENDOR, 'Swiss'], [TYPE, TABLET]], [
3878
+
3879
+ /android.+[;\/]\s*((Zeki)?TB.*\b)\s+build/i // Zeki Tablets
3880
+ ], [MODEL, [VENDOR, 'Zeki'], [TYPE, TABLET]], [
3881
+
3882
+ /(android).+[;\/]\s+([YR]\d{2})\s+build/i,
3883
+ /android.+[;\/]\s+(Dragon[\-\s]+Touch\s+|DT)(\w{5})\sbuild/i // Dragon Touch Tablet
3884
+ ], [[VENDOR, 'Dragon Touch'], MODEL, [TYPE, TABLET]], [
3885
+
3886
+ /android.+[;\/]\s*(NS-?\w{0,9})\sbuild/i // Insignia Tablets
3887
+ ], [MODEL, [VENDOR, 'Insignia'], [TYPE, TABLET]], [
3888
+
3889
+ /android.+[;\/]\s*((NX|Next)-?\w{0,9})\s+build/i // NextBook Tablets
3890
+ ], [MODEL, [VENDOR, 'NextBook'], [TYPE, TABLET]], [
3891
+
3892
+ /android.+[;\/]\s*(Xtreme\_)?(V(1[045]|2[015]|30|40|60|7[05]|90))\s+build/i
3893
+ ], [[VENDOR, 'Voice'], MODEL, [TYPE, MOBILE]], [ // Voice Xtreme Phones
3894
+
3895
+ /android.+[;\/]\s*(LVTEL\-)?(V1[12])\s+build/i // LvTel Phones
3896
+ ], [[VENDOR, 'LvTel'], MODEL, [TYPE, MOBILE]], [
3897
+
3898
+ /android.+;\s(PH-1)\s/i
3899
+ ], [MODEL, [VENDOR, 'Essential'], [TYPE, MOBILE]], [ // Essential PH-1
3900
+
3901
+ /android.+[;\/]\s*(V(100MD|700NA|7011|917G).*\b)\s+build/i // Envizen Tablets
3902
+ ], [MODEL, [VENDOR, 'Envizen'], [TYPE, TABLET]], [
3903
+
3904
+ /android.+[;\/]\s*(Le[\s\-]+Pan)[\s\-]+(\w{1,9})\s+build/i // Le Pan Tablets
3905
+ ], [VENDOR, MODEL, [TYPE, TABLET]], [
3906
+
3907
+ /android.+[;\/]\s*(Trio[\s\-]*.*)\s+build/i // MachSpeed Tablets
3908
+ ], [MODEL, [VENDOR, 'MachSpeed'], [TYPE, TABLET]], [
3909
+
3910
+ /android.+[;\/]\s*(Trinity)[\-\s]*(T\d{3})\s+build/i // Trinity Tablets
3911
+ ], [VENDOR, MODEL, [TYPE, TABLET]], [
3912
+
3913
+ /android.+[;\/]\s*TU_(1491)\s+build/i // Rotor Tablets
3914
+ ], [MODEL, [VENDOR, 'Rotor'], [TYPE, TABLET]], [
3915
+
3916
+ /android.+(KS(.+))\s+build/i // Amazon Kindle Tablets
3917
+ ], [MODEL, [VENDOR, 'Amazon'], [TYPE, TABLET]], [
3918
+
3919
+ /android.+(Gigaset)[\s\-]+(Q\w{1,9})\s+build/i // Gigaset Tablets
3920
+ ], [VENDOR, MODEL, [TYPE, TABLET]], [
3921
+
3922
+ /\s(tablet|tab)[;\/]/i, // Unidentifiable Tablet
3923
+ /\s(mobile)(?:[;\/]|\ssafari)/i // Unidentifiable Mobile
3924
+ ], [[TYPE, util.lowerize], VENDOR, MODEL], [
3925
+
3926
+ /[\s\/\(](smart-?tv)[;\)]/i // SmartTV
3927
+ ], [[TYPE, SMARTTV]], [
3928
+
3929
+ /(android[\w\.\s\-]{0,9});.+build/i // Generic Android Device
3930
+ ], [MODEL, [VENDOR, 'Generic']]
3931
+ ],
3932
+
3933
+ engine : [[
3934
+
3935
+ /windows.+\sedge\/([\w\.]+)/i // EdgeHTML
3936
+ ], [VERSION, [NAME, 'EdgeHTML']], [
3937
+
3938
+ /webkit\/537\.36.+chrome\/(?!27)([\w\.]+)/i // Blink
3939
+ ], [VERSION, [NAME, 'Blink']], [
3940
+
3941
+ /(presto)\/([\w\.]+)/i, // Presto
3942
+ /(webkit|trident|netfront|netsurf|amaya|lynx|w3m|goanna)\/([\w\.]+)/i,
3943
+ // WebKit/Trident/NetFront/NetSurf/Amaya/Lynx/w3m/Goanna
3944
+ /(khtml|tasman|links)[\/\s]\(?([\w\.]+)/i, // KHTML/Tasman/Links
3945
+ /(icab)[\/\s]([23]\.[\d\.]+)/i // iCab
3946
+ ], [NAME, VERSION], [
3947
+
3948
+ /rv\:([\w\.]{1,9}).+(gecko)/i // Gecko
3949
+ ], [VERSION, NAME]
3950
+ ],
3951
+
3952
+ os : [[
3953
+
3954
+ // Windows based
3955
+ /microsoft\s(windows)\s(vista|xp)/i // Windows (iTunes)
3956
+ ], [NAME, VERSION], [
3957
+ /(windows)\snt\s6\.2;\s(arm)/i, // Windows RT
3958
+ /(windows\sphone(?:\sos)*)[\s\/]?([\d\.\s\w]*)/i, // Windows Phone
3959
+ /(windows\smobile|windows)[\s\/]?([ntce\d\.\s]+\w)/i
3960
+ ], [NAME, [VERSION, mapper.str, maps.os.windows.version]], [
3961
+ /(win(?=3|9|n)|win\s9x\s)([nt\d\.]+)/i
3962
+ ], [[NAME, 'Windows'], [VERSION, mapper.str, maps.os.windows.version]], [
3963
+
3964
+ // Mobile/Embedded OS
3965
+ /\((bb)(10);/i // BlackBerry 10
3966
+ ], [[NAME, 'BlackBerry'], VERSION], [
3967
+ /(blackberry)\w*\/?([\w\.]*)/i, // Blackberry
3968
+ /(tizen|kaios)[\/\s]([\w\.]+)/i, // Tizen/KaiOS
3969
+ /(android|webos|palm\sos|qnx|bada|rim\stablet\sos|meego|sailfish|contiki)[\/\s-]?([\w\.]*)/i
3970
+ // Android/WebOS/Palm/QNX/Bada/RIM/MeeGo/Contiki/Sailfish OS
3971
+ ], [NAME, VERSION], [
3972
+ /(symbian\s?os|symbos|s60(?=;))[\/\s-]?([\w\.]*)/i // Symbian
3973
+ ], [[NAME, 'Symbian'], VERSION], [
3974
+ /\((series40);/i // Series 40
3975
+ ], [NAME], [
3976
+ /mozilla.+\(mobile;.+gecko.+firefox/i // Firefox OS
3977
+ ], [[NAME, 'Firefox OS'], VERSION], [
3978
+
3979
+ // Console
3980
+ /(nintendo|playstation)\s([wids34portablevu]+)/i, // Nintendo/Playstation
3981
+
3982
+ // GNU/Linux based
3983
+ /(mint)[\/\s\(]?(\w*)/i, // Mint
3984
+ /(mageia|vectorlinux)[;\s]/i, // Mageia/VectorLinux
3985
+ /(joli|[kxln]?ubuntu|debian|suse|opensuse|gentoo|(?=\s)arch|slackware|fedora|mandriva|centos|pclinuxos|redhat|zenwalk|linpus)[\/\s-]?(?!chrom)([\w\.-]*)/i,
3986
+ // Joli/Ubuntu/Debian/SUSE/Gentoo/Arch/Slackware
3987
+ // Fedora/Mandriva/CentOS/PCLinuxOS/RedHat/Zenwalk/Linpus
3988
+ /(hurd|linux)\s?([\w\.]*)/i, // Hurd/Linux
3989
+ /(gnu)\s?([\w\.]*)/i // GNU
3990
+ ], [NAME, VERSION], [
3991
+
3992
+ /(cros)\s[\w]+\s([\w\.]+\w)/i // Chromium OS
3993
+ ], [[NAME, 'Chromium OS'], VERSION],[
3994
+
3995
+ // Solaris
3996
+ /(sunos)\s?([\w\.\d]*)/i // Solaris
3997
+ ], [[NAME, 'Solaris'], VERSION], [
3998
+
3999
+ // BSD based
4000
+ /\s([frentopc-]{0,4}bsd|dragonfly)\s?([\w\.]*)/i // FreeBSD/NetBSD/OpenBSD/PC-BSD/DragonFly
4001
+ ], [NAME, VERSION],[
4002
+
4003
+ /(haiku)\s(\w+)/i // Haiku
4004
+ ], [NAME, VERSION],[
4005
+
4006
+ /cfnetwork\/.+darwin/i,
4007
+ /ip[honead]{2,4}(?:.*os\s([\w]+)\slike\smac|;\sopera)/i // iOS
4008
+ ], [[VERSION, /_/g, '.'], [NAME, 'iOS']], [
4009
+
4010
+ /(mac\sos\sx)\s?([\w\s\.]*)/i,
4011
+ /(macintosh|mac(?=_powerpc)\s)/i // Mac OS
4012
+ ], [[NAME, 'Mac OS'], [VERSION, /_/g, '.']], [
4013
+
4014
+ // Other
4015
+ /((?:open)?solaris)[\/\s-]?([\w\.]*)/i, // Solaris
4016
+ /(aix)\s((\d)(?=\.|\)|\s)[\w\.])*/i, // AIX
4017
+ /(plan\s9|minix|beos|os\/2|amigaos|morphos|risc\sos|openvms|fuchsia)/i,
4018
+ // Plan9/Minix/BeOS/OS2/AmigaOS/MorphOS/RISCOS/OpenVMS/Fuchsia
4019
+ /(unix)\s?([\w\.]*)/i // UNIX
4020
+ ], [NAME, VERSION]
4021
+ ]
4022
+ };
4023
+
4024
+
4025
+ /////////////////
4026
+ // Constructor
4027
+ ////////////////
4028
+ var UAParser = function (uastring, extensions) {
4029
+
4030
+ if (typeof uastring === 'object') {
4031
+ extensions = uastring;
4032
+ uastring = undefined;
4033
+ }
4034
+
4035
+ if (!(this instanceof UAParser)) {
4036
+ return new UAParser(uastring, extensions).getResult();
4037
+ }
4038
+
4039
+ var ua = uastring || ((window && window.navigator && window.navigator.userAgent) ? window.navigator.userAgent : EMPTY);
4040
+ var rgxmap = extensions ? util.extend(regexes, extensions) : regexes;
4041
+
4042
+ this.getBrowser = function () {
4043
+ var browser = { name: undefined, version: undefined };
4044
+ mapper.rgx.call(browser, ua, rgxmap.browser);
4045
+ browser.major = util.major(browser.version); // deprecated
4046
+ return browser;
4047
+ };
4048
+ this.getCPU = function () {
4049
+ var cpu = { architecture: undefined };
4050
+ mapper.rgx.call(cpu, ua, rgxmap.cpu);
4051
+ return cpu;
4052
+ };
4053
+ this.getDevice = function () {
4054
+ var device = { vendor: undefined, model: undefined, type: undefined };
4055
+ mapper.rgx.call(device, ua, rgxmap.device);
4056
+ return device;
4057
+ };
4058
+ this.getEngine = function () {
4059
+ var engine = { name: undefined, version: undefined };
4060
+ mapper.rgx.call(engine, ua, rgxmap.engine);
4061
+ return engine;
4062
+ };
4063
+ this.getOS = function () {
4064
+ var os = { name: undefined, version: undefined };
4065
+ mapper.rgx.call(os, ua, rgxmap.os);
4066
+ return os;
4067
+ };
4068
+ this.getResult = function () {
4069
+ return {
4070
+ ua : this.getUA(),
4071
+ browser : this.getBrowser(),
4072
+ engine : this.getEngine(),
4073
+ os : this.getOS(),
4074
+ device : this.getDevice(),
4075
+ cpu : this.getCPU()
4076
+ };
4077
+ };
4078
+ this.getUA = function () {
4079
+ return ua;
4080
+ };
4081
+ this.setUA = function (uastring) {
4082
+ ua = uastring;
4083
+ return this;
4084
+ };
4085
+ return this;
4086
+ };
4087
+
4088
+ UAParser.VERSION = LIBVERSION;
4089
+ UAParser.BROWSER = {
4090
+ NAME : NAME,
4091
+ MAJOR : MAJOR, // deprecated
4092
+ VERSION : VERSION
4093
+ };
4094
+ UAParser.CPU = {
4095
+ ARCHITECTURE : ARCHITECTURE
4096
+ };
4097
+ UAParser.DEVICE = {
4098
+ MODEL : MODEL,
4099
+ VENDOR : VENDOR,
4100
+ TYPE : TYPE,
4101
+ CONSOLE : CONSOLE,
4102
+ MOBILE : MOBILE,
4103
+ SMARTTV : SMARTTV,
4104
+ TABLET : TABLET,
4105
+ WEARABLE: WEARABLE,
4106
+ EMBEDDED: EMBEDDED
4107
+ };
4108
+ UAParser.ENGINE = {
4109
+ NAME : NAME,
4110
+ VERSION : VERSION
4111
+ };
4112
+ UAParser.OS = {
4113
+ NAME : NAME,
4114
+ VERSION : VERSION
4115
+ };
4116
+
4117
+ ///////////
4118
+ // Export
4119
+ //////////
4120
+
4121
+
4122
+ // check js environment
4123
+ if (typeof(exports) !== UNDEF_TYPE) {
4124
+ // nodejs env
4125
+ if (typeof module !== UNDEF_TYPE && module.exports) {
4126
+ exports = module.exports = UAParser;
4127
+ }
4128
+ exports.UAParser = UAParser;
4129
+ } else {
4130
+ // requirejs env (optional)
4131
+ if (typeof(define) === 'function' && define.amd) {
4132
+ define(function () {
4133
+ return UAParser;
4134
+ });
4135
+ } else if (window) {
4136
+ // browser env
4137
+ window.UAParser = UAParser;
4138
+ }
4139
+ }
4140
+
4141
+ // jQuery/Zepto specific (optional)
4142
+ // Note:
4143
+ // In AMD env the global scope should be kept clean, but jQuery is an exception.
4144
+ // jQuery always exports to global scope, unless jQuery.noConflict(true) is used,
4145
+ // and we should catch that.
4146
+ var $ = window && (window.jQuery || window.Zepto);
4147
+ if ($ && !$.ua) {
4148
+ var parser = new UAParser();
4149
+ $.ua = parser.getResult();
4150
+ $.ua.get = function () {
4151
+ return parser.getUA();
4152
+ };
4153
+ $.ua.set = function (uastring) {
4154
+ parser.setUA(uastring);
4155
+ var result = parser.getResult();
4156
+ for (var prop in result) {
4157
+ $.ua[prop] = result[prop];
4158
+ }
4159
+ };
4160
+ }
4161
+
4162
+ })(typeof window === 'object' ? window : this);
4163
+
4164
+ },{}],22:[function(require,module,exports){
4165
+ /**
4166
+ * Convert array of 16 byte values to UUID string format of the form:
4167
+ * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
4168
+ */
4169
+ var byteToHex = [];
4170
+ for (var i = 0; i < 256; ++i) {
4171
+ byteToHex[i] = (i + 0x100).toString(16).substr(1);
4172
+ }
4173
+
4174
+ function bytesToUuid(buf, offset) {
4175
+ var i = offset || 0;
4176
+ var bth = byteToHex;
4177
+ // join used to fix memory issue caused by concatenation: https://bugs.chromium.org/p/v8/issues/detail?id=3175#c4
4178
+ return ([
4179
+ bth[buf[i++]], bth[buf[i++]],
4180
+ bth[buf[i++]], bth[buf[i++]], '-',
4181
+ bth[buf[i++]], bth[buf[i++]], '-',
4182
+ bth[buf[i++]], bth[buf[i++]], '-',
4183
+ bth[buf[i++]], bth[buf[i++]], '-',
4184
+ bth[buf[i++]], bth[buf[i++]],
4185
+ bth[buf[i++]], bth[buf[i++]],
4186
+ bth[buf[i++]], bth[buf[i++]]
4187
+ ]).join('');
4188
+ }
4189
+
4190
+ module.exports = bytesToUuid;
4191
+
4192
+ },{}],23:[function(require,module,exports){
4193
+ // Unique ID creation requires a high quality random # generator. In the
4194
+ // browser this is a little complicated due to unknown quality of Math.random()
4195
+ // and inconsistent support for the `crypto` API. We do the best we can via
4196
+ // feature-detection
4197
+
4198
+ // getRandomValues needs to be invoked in a context where "this" is a Crypto
4199
+ // implementation. Also, find the complete implementation of crypto on IE11.
4200
+ var getRandomValues = (typeof(crypto) != 'undefined' && crypto.getRandomValues && crypto.getRandomValues.bind(crypto)) ||
4201
+ (typeof(msCrypto) != 'undefined' && typeof window.msCrypto.getRandomValues == 'function' && msCrypto.getRandomValues.bind(msCrypto));
4202
+
4203
+ if (getRandomValues) {
4204
+ // WHATWG crypto RNG - http://wiki.whatwg.org/wiki/Crypto
4205
+ var rnds8 = new Uint8Array(16); // eslint-disable-line no-undef
4206
+
4207
+ module.exports = function whatwgRNG() {
4208
+ getRandomValues(rnds8);
4209
+ return rnds8;
4210
+ };
4211
+ } else {
4212
+ // Math.random()-based (RNG)
4213
+ //
4214
+ // If all else fails, use Math.random(). It's fast, but is of unspecified
4215
+ // quality.
4216
+ var rnds = new Array(16);
4217
+
4218
+ module.exports = function mathRNG() {
4219
+ for (var i = 0, r; i < 16; i++) {
4220
+ if ((i & 0x03) === 0) r = Math.random() * 0x100000000;
4221
+ rnds[i] = r >>> ((i & 0x03) << 3) & 0xff;
4222
+ }
4223
+
4224
+ return rnds;
4225
+ };
4226
+ }
4227
+
4228
+ },{}],24:[function(require,module,exports){
4229
+ var rng = require('./lib/rng');
4230
+ var bytesToUuid = require('./lib/bytesToUuid');
4231
+
4232
+ function v4(options, buf, offset) {
4233
+ var i = buf && offset || 0;
4234
+
4235
+ if (typeof(options) == 'string') {
4236
+ buf = options === 'binary' ? new Array(16) : null;
4237
+ options = null;
4238
+ }
4239
+ options = options || {};
4240
+
4241
+ var rnds = options.random || (options.rng || rng)();
4242
+
4243
+ // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
4244
+ rnds[6] = (rnds[6] & 0x0f) | 0x40;
4245
+ rnds[8] = (rnds[8] & 0x3f) | 0x80;
4246
+
4247
+ // Copy bytes to buffer, if provided
4248
+ if (buf) {
4249
+ for (var ii = 0; ii < 16; ++ii) {
4250
+ buf[i + ii] = rnds[ii];
4251
+ }
4252
+ }
4253
+
4254
+ return buf || bytesToUuid(rnds);
4255
+ }
4256
+
4257
+ module.exports = v4;
4258
+
4259
+ },{"./lib/bytesToUuid":22,"./lib/rng":23}],25:[function(require,module,exports){
4260
+ /*
4261
+ WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based
4262
+ on @visionmedia's Emitter from UI Kit.
4263
+
4264
+ Why? I wanted it standalone.
4265
+
4266
+ I also wanted support for wildcard emitters like this:
4267
+
4268
+ emitter.on('*', function (eventName, other, event, payloads) {
4269
+
4270
+ });
4271
+
4272
+ emitter.on('somenamespace*', function (eventName, payloads) {
4273
+
4274
+ });
4275
+
4276
+ Please note that callbacks triggered by wildcard registered events also get
4277
+ the event name as the first argument.
4278
+ */
4279
+
4280
+ module.exports = WildEmitter;
4281
+
4282
+ function WildEmitter() { }
4283
+
4284
+ WildEmitter.mixin = function (constructor) {
4285
+ var prototype = constructor.prototype || constructor;
4286
+
4287
+ prototype.isWildEmitter= true;
4288
+
4289
+ // Listen on the given `event` with `fn`. Store a group name if present.
4290
+ prototype.on = function (event, groupName, fn) {
4291
+ this.callbacks = this.callbacks || {};
4292
+ var hasGroup = (arguments.length === 3),
4293
+ group = hasGroup ? arguments[1] : undefined,
4294
+ func = hasGroup ? arguments[2] : arguments[1];
4295
+ func._groupName = group;
4296
+ (this.callbacks[event] = this.callbacks[event] || []).push(func);
4297
+ return this;
4298
+ };
4299
+
4300
+ // Adds an `event` listener that will be invoked a single
4301
+ // time then automatically removed.
4302
+ prototype.once = function (event, groupName, fn) {
4303
+ var self = this,
4304
+ hasGroup = (arguments.length === 3),
4305
+ group = hasGroup ? arguments[1] : undefined,
4306
+ func = hasGroup ? arguments[2] : arguments[1];
4307
+ function on() {
4308
+ self.off(event, on);
4309
+ func.apply(this, arguments);
4310
+ }
4311
+ this.on(event, group, on);
4312
+ return this;
4313
+ };
4314
+
4315
+ // Unbinds an entire group
4316
+ prototype.releaseGroup = function (groupName) {
4317
+ this.callbacks = this.callbacks || {};
4318
+ var item, i, len, handlers;
4319
+ for (item in this.callbacks) {
4320
+ handlers = this.callbacks[item];
4321
+ for (i = 0, len = handlers.length; i < len; i++) {
4322
+ if (handlers[i]._groupName === groupName) {
4323
+ //console.log('removing');
4324
+ // remove it and shorten the array we're looping through
4325
+ handlers.splice(i, 1);
4326
+ i--;
4327
+ len--;
4328
+ }
4329
+ }
4330
+ }
4331
+ return this;
4332
+ };
4333
+
4334
+ // Remove the given callback for `event` or all
4335
+ // registered callbacks.
4336
+ prototype.off = function (event, fn) {
4337
+ this.callbacks = this.callbacks || {};
4338
+ var callbacks = this.callbacks[event],
4339
+ i;
4340
+
4341
+ if (!callbacks) return this;
4342
+
4343
+ // remove all handlers
4344
+ if (arguments.length === 1) {
4345
+ delete this.callbacks[event];
4346
+ return this;
4347
+ }
4348
+
4349
+ // remove specific handler
4350
+ i = callbacks.indexOf(fn);
4351
+ if (i !== -1) {
4352
+ callbacks.splice(i, 1);
4353
+ if (callbacks.length === 0) {
4354
+ delete this.callbacks[event];
4355
+ }
4356
+ }
4357
+ return this;
4358
+ };
4359
+
4360
+ /// Emit `event` with the given args.
4361
+ // also calls any `*` handlers
4362
+ prototype.emit = function (event) {
4363
+ this.callbacks = this.callbacks || {};
4364
+ var args = [].slice.call(arguments, 1),
4365
+ callbacks = this.callbacks[event],
4366
+ specialCallbacks = this.getWildcardCallbacks(event),
4367
+ i,
4368
+ len,
4369
+ item,
4370
+ listeners;
4371
+
4372
+ if (callbacks) {
4373
+ listeners = callbacks.slice();
4374
+ for (i = 0, len = listeners.length; i < len; ++i) {
4375
+ if (!listeners[i]) {
4376
+ break;
4377
+ }
4378
+ listeners[i].apply(this, args);
4379
+ }
4380
+ }
4381
+
4382
+ if (specialCallbacks) {
4383
+ len = specialCallbacks.length;
4384
+ listeners = specialCallbacks.slice();
4385
+ for (i = 0, len = listeners.length; i < len; ++i) {
4386
+ if (!listeners[i]) {
4387
+ break;
4388
+ }
4389
+ listeners[i].apply(this, [event].concat(args));
4390
+ }
4391
+ }
4392
+
4393
+ return this;
4394
+ };
4395
+
4396
+ // Helper for for finding special wildcard event handlers that match the event
4397
+ prototype.getWildcardCallbacks = function (eventName) {
4398
+ this.callbacks = this.callbacks || {};
4399
+ var item,
4400
+ split,
4401
+ result = [];
4402
+
4403
+ for (item in this.callbacks) {
4404
+ split = item.split('*');
4405
+ if (item === '*' || (split.length === 2 && eventName.slice(0, split[0].length) === split[0])) {
4406
+ result = result.concat(this.callbacks[item]);
4407
+ }
4408
+ }
4409
+ return result;
4410
+ };
4411
+
4412
+ };
4413
+
4414
+ WildEmitter.mixin(WildEmitter);
4415
+
4416
+ },{}]},{},[2])(2)
4417
+ });
4418
+