You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

TraceablePeerConnection.js 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. /* global mozRTCPeerConnection, webkitRTCPeerConnection */
  2. import { getLogger } from "jitsi-meet-logger";
  3. const logger = getLogger(__filename);
  4. import SdpConsistency from "./SdpConsistency.js";
  5. var RTCBrowserType = require("../RTC/RTCBrowserType.js");
  6. var XMPPEvents = require("../../service/xmpp/XMPPEvents");
  7. var transform = require('sdp-transform');
  8. var RandomUtil = require('../util/RandomUtil');
  9. var SDP = require("./SDP");
  10. var SDPUtil = require("./SDPUtil");
  11. var SIMULCAST_LAYERS = 3;
  12. function TraceablePeerConnection(ice_config, constraints, session) {
  13. var self = this;
  14. this.session = session;
  15. var RTCPeerConnectionType = null;
  16. if (RTCBrowserType.isFirefox()) {
  17. RTCPeerConnectionType = mozRTCPeerConnection;
  18. } else if (RTCBrowserType.isTemasysPluginUsed()) {
  19. RTCPeerConnectionType = RTCPeerConnection;
  20. } else {
  21. RTCPeerConnectionType = webkitRTCPeerConnection;
  22. }
  23. this.peerconnection = new RTCPeerConnectionType(ice_config, constraints);
  24. this.updateLog = [];
  25. this.stats = {};
  26. this.statsinterval = null;
  27. this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
  28. var Interop = require('sdp-interop').Interop;
  29. this.interop = new Interop();
  30. var Simulcast = require('sdp-simulcast');
  31. this.simulcast = new Simulcast({numOfLayers: SIMULCAST_LAYERS,
  32. explodeRemoteSimulcast: false});
  33. this.sdpConsistency = new SdpConsistency();
  34. this.eventEmitter = this.session.room.eventEmitter;
  35. // override as desired
  36. this.trace = function (what, info) {
  37. /*logger.warn('WTRACE', what, info);
  38. if (info && RTCBrowserType.isIExplorer()) {
  39. if (info.length > 1024) {
  40. logger.warn('WTRACE', what, info.substr(1024));
  41. }
  42. if (info.length > 2048) {
  43. logger.warn('WTRACE', what, info.substr(2048));
  44. }
  45. }*/
  46. self.updateLog.push({
  47. time: new Date(),
  48. type: what,
  49. value: info || ""
  50. });
  51. };
  52. this.onicecandidate = null;
  53. this.peerconnection.onicecandidate = function (event) {
  54. // FIXME: this causes stack overflow with Temasys Plugin
  55. if (!RTCBrowserType.isTemasysPluginUsed())
  56. self.trace('onicecandidate', JSON.stringify(event.candidate, null, ' '));
  57. if (self.onicecandidate !== null) {
  58. self.onicecandidate(event);
  59. }
  60. };
  61. this.onaddstream = null;
  62. this.peerconnection.onaddstream = function (event) {
  63. self.trace('onaddstream', event.stream.id);
  64. if (self.onaddstream !== null) {
  65. self.onaddstream(event);
  66. }
  67. };
  68. this.onremovestream = null;
  69. this.peerconnection.onremovestream = function (event) {
  70. self.trace('onremovestream', event.stream.id);
  71. if (self.onremovestream !== null) {
  72. self.onremovestream(event);
  73. }
  74. };
  75. this.onsignalingstatechange = null;
  76. this.peerconnection.onsignalingstatechange = function (event) {
  77. self.trace('onsignalingstatechange', self.signalingState);
  78. if (self.onsignalingstatechange !== null) {
  79. self.onsignalingstatechange(event);
  80. }
  81. };
  82. this.oniceconnectionstatechange = null;
  83. this.peerconnection.oniceconnectionstatechange = function (event) {
  84. self.trace('oniceconnectionstatechange', self.iceConnectionState);
  85. if (self.oniceconnectionstatechange !== null) {
  86. self.oniceconnectionstatechange(event);
  87. }
  88. };
  89. this.onnegotiationneeded = null;
  90. this.peerconnection.onnegotiationneeded = function (event) {
  91. self.trace('onnegotiationneeded');
  92. if (self.onnegotiationneeded !== null) {
  93. self.onnegotiationneeded(event);
  94. }
  95. };
  96. self.ondatachannel = null;
  97. this.peerconnection.ondatachannel = function (event) {
  98. self.trace('ondatachannel', event);
  99. if (self.ondatachannel !== null) {
  100. self.ondatachannel(event);
  101. }
  102. };
  103. // XXX: do all non-firefox browsers which we support also support this?
  104. if (!RTCBrowserType.isFirefox() && this.maxstats) {
  105. this.statsinterval = window.setInterval(function() {
  106. self.peerconnection.getStats(function(stats) {
  107. var results = stats.result();
  108. var now = new Date();
  109. for (var i = 0; i < results.length; ++i) {
  110. results[i].names().forEach(function (name) {
  111. var id = results[i].id + '-' + name;
  112. if (!self.stats[id]) {
  113. self.stats[id] = {
  114. startTime: now,
  115. endTime: now,
  116. values: [],
  117. times: []
  118. };
  119. }
  120. self.stats[id].values.push(results[i].stat(name));
  121. self.stats[id].times.push(now.getTime());
  122. if (self.stats[id].values.length > self.maxstats) {
  123. self.stats[id].values.shift();
  124. self.stats[id].times.shift();
  125. }
  126. self.stats[id].endTime = now;
  127. });
  128. }
  129. });
  130. }, 1000);
  131. }
  132. }
  133. /**
  134. * Returns a string representation of a SessionDescription object.
  135. */
  136. var dumpSDP = function(description) {
  137. if (typeof description === 'undefined' || description == null) {
  138. return '';
  139. }
  140. return 'type: ' + description.type + '\r\n' + description.sdp;
  141. };
  142. /**
  143. * Returns map with keys msid and values ssrc.
  144. * @param desc the SDP that will be modified.
  145. */
  146. function extractSSRCMap(desc) {
  147. if (typeof desc !== 'object' || desc === null ||
  148. typeof desc.sdp !== 'string') {
  149. logger.warn('An empty description was passed as an argument.');
  150. return desc;
  151. }
  152. var ssrcList = {};
  153. var ssrcGroups = {};
  154. var session = transform.parse(desc.sdp);
  155. if (!Array.isArray(session.media))
  156. {
  157. return;
  158. }
  159. session.media.forEach(function (bLine) {
  160. if (!Array.isArray(bLine.ssrcs))
  161. {
  162. return;
  163. }
  164. if (typeof bLine.ssrcGroups !== 'undefined' &&
  165. Array.isArray(bLine.ssrcGroups)) {
  166. bLine.ssrcGroups.forEach(function (group) {
  167. if (typeof group.semantics !== 'undefined' &&
  168. typeof group.ssrcs !== 'undefined') {
  169. var primarySSRC = Number(group.ssrcs.split(' ')[0]);
  170. ssrcGroups[primarySSRC] = ssrcGroups[primarySSRC] || [];
  171. ssrcGroups[primarySSRC].push(group);
  172. }
  173. });
  174. }
  175. bLine.ssrcs.forEach(function (ssrc) {
  176. if(ssrc.attribute !== 'msid')
  177. return;
  178. ssrcList[ssrc.value] = ssrcList[ssrc.value] ||
  179. {groups: [], ssrcs: []};
  180. ssrcList[ssrc.value].ssrcs.push(ssrc.id);
  181. if(ssrcGroups[ssrc.id]){
  182. ssrcGroups[ssrc.id].forEach(function (group) {
  183. ssrcList[ssrc.value].groups.push(
  184. {primarySSRC: ssrc.id, group: group});
  185. });
  186. }
  187. });
  188. });
  189. return ssrcList;
  190. }
  191. /**
  192. * Takes a SessionDescription object and returns a "normalized" version.
  193. * Currently it only takes care of ordering the a=ssrc lines.
  194. */
  195. var normalizePlanB = function(desc) {
  196. if (typeof desc !== 'object' || desc === null ||
  197. typeof desc.sdp !== 'string') {
  198. logger.warn('An empty description was passed as an argument.');
  199. return desc;
  200. }
  201. var transform = require('sdp-transform');
  202. var session = transform.parse(desc.sdp);
  203. if (typeof session !== 'undefined' &&
  204. typeof session.media !== 'undefined' && Array.isArray(session.media)) {
  205. session.media.forEach(function (mLine) {
  206. // Chrome appears to be picky about the order in which a=ssrc lines
  207. // are listed in an m-line when rtx is enabled (and thus there are
  208. // a=ssrc-group lines with FID semantics). Specifically if we have
  209. // "a=ssrc-group:FID S1 S2" and the "a=ssrc:S2" lines appear before
  210. // the "a=ssrc:S1" lines, SRD fails.
  211. // So, put SSRC which appear as the first SSRC in an FID ssrc-group
  212. // first.
  213. var firstSsrcs = [];
  214. var newSsrcLines = [];
  215. if (typeof mLine.ssrcGroups !== 'undefined' &&
  216. Array.isArray(mLine.ssrcGroups)) {
  217. mLine.ssrcGroups.forEach(function (group) {
  218. if (typeof group.semantics !== 'undefined' &&
  219. group.semantics === 'FID') {
  220. if (typeof group.ssrcs !== 'undefined') {
  221. firstSsrcs.push(Number(group.ssrcs.split(' ')[0]));
  222. }
  223. }
  224. });
  225. }
  226. if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) {
  227. var i;
  228. for (i = 0; i<mLine.ssrcs.length; i++){
  229. if (typeof mLine.ssrcs[i] === 'object'
  230. && typeof mLine.ssrcs[i].id !== 'undefined'
  231. && firstSsrcs.indexOf(mLine.ssrcs[i].id) >= 0) {
  232. newSsrcLines.push(mLine.ssrcs[i]);
  233. delete mLine.ssrcs[i];
  234. }
  235. }
  236. for (i = 0; i<mLine.ssrcs.length; i++){
  237. if (typeof mLine.ssrcs[i] !== 'undefined') {
  238. newSsrcLines.push(mLine.ssrcs[i]);
  239. }
  240. }
  241. mLine.ssrcs = newSsrcLines;
  242. }
  243. });
  244. }
  245. var resStr = transform.write(session);
  246. return new RTCSessionDescription({
  247. type: desc.type,
  248. sdp: resStr
  249. });
  250. };
  251. var getters = {
  252. signalingState: function () {
  253. return this.peerconnection.signalingState;
  254. },
  255. iceConnectionState: function () {
  256. return this.peerconnection.iceConnectionState;
  257. },
  258. localDescription: function() {
  259. var desc = this.peerconnection.localDescription;
  260. this.trace('getLocalDescription::preTransform', dumpSDP(desc));
  261. // if we're running on FF, transform to Plan B first.
  262. if (RTCBrowserType.usesUnifiedPlan()) {
  263. desc = this.interop.toPlanB(desc);
  264. this.trace('getLocalDescription::postTransform (Plan B)',
  265. dumpSDP(desc));
  266. }
  267. return desc;
  268. },
  269. remoteDescription: function() {
  270. var desc = this.peerconnection.remoteDescription;
  271. this.trace('getRemoteDescription::preTransform', dumpSDP(desc));
  272. // if we're running on FF, transform to Plan B first.
  273. if (RTCBrowserType.usesUnifiedPlan()) {
  274. desc = this.interop.toPlanB(desc);
  275. this.trace('getRemoteDescription::postTransform (Plan B)', dumpSDP(desc));
  276. }
  277. return desc;
  278. }
  279. };
  280. Object.keys(getters).forEach(function (prop) {
  281. Object.defineProperty(
  282. TraceablePeerConnection.prototype,
  283. prop, {
  284. get: getters[prop]
  285. }
  286. );
  287. });
  288. TraceablePeerConnection.prototype.addStream = function (stream, ssrcInfo) {
  289. this.trace('addStream', stream ? stream.id : "null");
  290. if (stream)
  291. this.peerconnection.addStream(stream);
  292. if (ssrcInfo && ssrcInfo.type === "addMuted") {
  293. this.sdpConsistency.setPrimarySsrc(ssrcInfo.ssrc.ssrcs[0]);
  294. this.simulcast.setSsrcCache(ssrcInfo.ssrc.ssrcs);
  295. }
  296. };
  297. TraceablePeerConnection.prototype.removeStream = function (stream) {
  298. this.trace('removeStream', stream.id);
  299. // FF doesn't support this yet.
  300. if (this.peerconnection.removeStream) {
  301. this.peerconnection.removeStream(stream);
  302. }
  303. };
  304. TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
  305. this.trace('createDataChannel', label, opts);
  306. return this.peerconnection.createDataChannel(label, opts);
  307. };
  308. TraceablePeerConnection.prototype.setLocalDescription
  309. = function (description, successCallback, failureCallback) {
  310. this.trace('setLocalDescription::preTransform', dumpSDP(description));
  311. // if we're running on FF, transform to Plan A first.
  312. if (RTCBrowserType.usesUnifiedPlan()) {
  313. description = this.interop.toUnifiedPlan(description);
  314. this.trace('setLocalDescription::postTransform (Plan A)',
  315. dumpSDP(description));
  316. }
  317. var self = this;
  318. this.peerconnection.setLocalDescription(description,
  319. function () {
  320. self.trace('setLocalDescriptionOnSuccess');
  321. successCallback();
  322. },
  323. function (err) {
  324. self.trace('setLocalDescriptionOnFailure', err);
  325. self.eventEmitter.emit(XMPPEvents.SET_LOCAL_DESCRIPTION_FAILED,
  326. err, self.peerconnection);
  327. failureCallback(err);
  328. }
  329. );
  330. };
  331. TraceablePeerConnection.prototype.setRemoteDescription
  332. = function (description, successCallback, failureCallback) {
  333. this.trace('setRemoteDescription::preTransform', dumpSDP(description));
  334. // TODO the focus should squeze or explode the remote simulcast
  335. description = this.simulcast.mungeRemoteDescription(description);
  336. this.trace('setRemoteDescription::postTransform (simulcast)', dumpSDP(description));
  337. // if we're running on FF, transform to Plan A first.
  338. if (RTCBrowserType.usesUnifiedPlan()) {
  339. description = this.interop.toUnifiedPlan(description);
  340. this.trace('setRemoteDescription::postTransform (Plan A)', dumpSDP(description));
  341. }
  342. if (RTCBrowserType.usesPlanB()) {
  343. description = normalizePlanB(description);
  344. }
  345. var self = this;
  346. this.peerconnection.setRemoteDescription(description,
  347. function () {
  348. self.trace('setRemoteDescriptionOnSuccess');
  349. successCallback();
  350. },
  351. function (err) {
  352. self.trace('setRemoteDescriptionOnFailure', err);
  353. self.eventEmitter.emit(XMPPEvents.SET_REMOTE_DESCRIPTION_FAILED,
  354. err, self.peerconnection);
  355. failureCallback(err);
  356. }
  357. );
  358. /*
  359. if (this.statsinterval === null && this.maxstats > 0) {
  360. // start gathering stats
  361. }
  362. */
  363. };
  364. /**
  365. * Makes the underlying TraceablePeerConnection generate new SSRC for
  366. * the recvonly video stream.
  367. * @deprecated
  368. */
  369. TraceablePeerConnection.prototype.generateRecvonlySsrc = function() {
  370. // FIXME replace with SDPUtil.generateSsrc (when it's added)
  371. const newSSRC = this.generateNewStreamSSRCInfo().ssrcs[0];
  372. logger.info("Generated new recvonly SSRC: " + newSSRC);
  373. this.sdpConsistency.setPrimarySsrc(newSSRC);
  374. };
  375. TraceablePeerConnection.prototype.close = function () {
  376. this.trace('stop');
  377. if (this.statsinterval !== null) {
  378. window.clearInterval(this.statsinterval);
  379. this.statsinterval = null;
  380. }
  381. this.peerconnection.close();
  382. };
  383. /**
  384. * Modifies the values of the setup attributes (defined by
  385. * {@link http://tools.ietf.org/html/rfc4145#section-4}) of a specific SDP
  386. * answer in order to overcome a delay of 1 second in the connection
  387. * establishment between Chrome and Videobridge.
  388. *
  389. * @param {SDP} offer - the SDP offer to which the specified SDP answer is
  390. * being prepared to respond
  391. * @param {SDP} answer - the SDP to modify
  392. * @private
  393. */
  394. var _fixAnswerRFC4145Setup = function (offer, answer) {
  395. if (!RTCBrowserType.isChrome()) {
  396. // It looks like Firefox doesn't agree with the fix (at least in its
  397. // current implementation) because it effectively remains active even
  398. // after we tell it to become passive. Apart from Firefox which I tested
  399. // after the fix was deployed, I tested Chrome only. In order to prevent
  400. // issues with other browsers, limit the fix to Chrome for the time
  401. // being.
  402. return;
  403. }
  404. // XXX Videobridge is the (SDP) offerer and WebRTC (e.g. Chrome) is the
  405. // answerer (as orchestrated by Jicofo). In accord with
  406. // http://tools.ietf.org/html/rfc5245#section-5.2 and because both peers
  407. // are ICE FULL agents, Videobridge will take on the controlling role and
  408. // WebRTC will take on the controlled role. In accord with
  409. // https://tools.ietf.org/html/rfc5763#section-5, Videobridge will use the
  410. // setup attribute value of setup:actpass and WebRTC will be allowed to
  411. // choose either the setup attribute value of setup:active or
  412. // setup:passive. Chrome will by default choose setup:active because it is
  413. // RECOMMENDED by the respective RFC since setup:passive adds additional
  414. // latency. The case of setup:active allows WebRTC to send a DTLS
  415. // ClientHello as soon as an ICE connectivity check of its succeeds.
  416. // Unfortunately, Videobridge will be unable to respond immediately because
  417. // may not have WebRTC's answer or may have not completed the ICE
  418. // connectivity establishment. Even more unfortunate is that in the
  419. // described scenario Chrome's DTLS implementation will insist on
  420. // retransmitting its ClientHello after a second (the time is in accord
  421. // with the respective RFC) and will thus cause the whole connection
  422. // establishment to exceed at least 1 second. To work around Chrome's
  423. // idiosyncracy, don't allow it to send a ClientHello i.e. change its
  424. // default choice of setup:active to setup:passive.
  425. if (offer && answer
  426. && offer.media && answer.media
  427. && offer.media.length == answer.media.length) {
  428. answer.media.forEach(function (a, i) {
  429. if (SDPUtil.find_line(
  430. offer.media[i],
  431. 'a=setup:actpass',
  432. offer.session)) {
  433. answer.media[i]
  434. = a.replace(/a=setup:active/g, 'a=setup:passive');
  435. }
  436. });
  437. answer.raw = answer.session + answer.media.join('');
  438. }
  439. };
  440. TraceablePeerConnection.prototype.createAnswer
  441. = function (successCallback, failureCallback, constraints) {
  442. this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
  443. this.peerconnection.createAnswer(
  444. (answer) => {
  445. try {
  446. this.trace(
  447. 'createAnswerOnSuccess::preTransform', dumpSDP(answer));
  448. // if we're running on FF, transform to Plan A first.
  449. if (RTCBrowserType.usesUnifiedPlan()) {
  450. answer = this.interop.toPlanB(answer);
  451. this.trace('createAnswerOnSuccess::postTransform (Plan B)',
  452. dumpSDP(answer));
  453. }
  454. /**
  455. * We don't keep ssrcs consitent for Firefox because rewriting
  456. * the ssrcs between createAnswer and setLocalDescription
  457. * breaks the caching in sdp-interop (sdp-interop must
  458. * know about all ssrcs, and it updates its cache in
  459. * toPlanB so if we rewrite them after that, when we
  460. * try and go back to unified plan it will complain
  461. * about unmapped ssrcs)
  462. */
  463. if (!RTCBrowserType.isFirefox()) {
  464. answer.sdp = this.sdpConsistency.makeVideoPrimarySsrcsConsistent(answer.sdp);
  465. this.trace('createAnswerOnSuccess::postTransform (make primary video ssrcs consistent)',
  466. dumpSDP(answer));
  467. }
  468. // Add simulcast streams if simulcast is enabled
  469. if (!this.session.room.options.disableSimulcast
  470. && this.simulcast.isSupported()) {
  471. answer = this.simulcast.mungeLocalDescription(answer);
  472. this.trace(
  473. 'createAnswerOnSuccess::postTransform (simulcast)',
  474. dumpSDP(answer));
  475. }
  476. // Fix the setup attribute (see _fixAnswerRFC4145Setup for
  477. // details)
  478. let remoteDescription = new SDP(this.remoteDescription.sdp);
  479. let localDescription = new SDP(answer.sdp);
  480. _fixAnswerRFC4145Setup(remoteDescription, localDescription);
  481. answer.sdp = localDescription.raw;
  482. this.eventEmitter.emit(XMPPEvents.SENDRECV_STREAMS_CHANGED,
  483. extractSSRCMap(answer));
  484. successCallback(answer);
  485. } catch (e) {
  486. this.trace('createAnswerOnError', e);
  487. this.trace('createAnswerOnError', dumpSDP(answer));
  488. logger.error('createAnswerOnError', e, dumpSDP(answer));
  489. failureCallback(e);
  490. }
  491. },
  492. (err) => {
  493. this.trace('createAnswerOnFailure', err);
  494. this.eventEmitter.emit(XMPPEvents.CREATE_ANSWER_FAILED, err,
  495. this.peerconnection);
  496. failureCallback(err);
  497. },
  498. constraints
  499. );
  500. };
  501. TraceablePeerConnection.prototype.addIceCandidate
  502. // eslint-disable-next-line no-unused-vars
  503. = function (candidate, successCallback, failureCallback) {
  504. //var self = this;
  505. this.trace('addIceCandidate', JSON.stringify(candidate, null, ' '));
  506. this.peerconnection.addIceCandidate(candidate);
  507. /* maybe later
  508. this.peerconnection.addIceCandidate(candidate,
  509. function () {
  510. self.trace('addIceCandidateOnSuccess');
  511. successCallback();
  512. },
  513. function (err) {
  514. self.trace('addIceCandidateOnFailure', err);
  515. failureCallback(err);
  516. }
  517. );
  518. */
  519. };
  520. TraceablePeerConnection.prototype.getStats = function(callback, errback) {
  521. // TODO: Is this the correct way to handle Opera, Temasys?
  522. if (RTCBrowserType.isFirefox()
  523. || RTCBrowserType.isTemasysPluginUsed()
  524. || RTCBrowserType.isReactNative()) {
  525. // ignore for now...
  526. if(!errback)
  527. errback = function () {};
  528. this.peerconnection.getStats(null, callback, errback);
  529. } else {
  530. this.peerconnection.getStats(callback);
  531. }
  532. };
  533. /**
  534. * Generate ssrc info object for a stream with the following properties:
  535. * - ssrcs - Array of the ssrcs associated with the stream.
  536. * - groups - Array of the groups associated with the stream.
  537. */
  538. TraceablePeerConnection.prototype.generateNewStreamSSRCInfo = function () {
  539. if (!this.session.room.options.disableSimulcast
  540. && this.simulcast.isSupported()) {
  541. var ssrcInfo = {ssrcs: [], groups: []};
  542. for(var i = 0; i < SIMULCAST_LAYERS; i++)
  543. ssrcInfo.ssrcs.push(RandomUtil.randomInt(1, 0xffffffff));
  544. ssrcInfo.groups.push({
  545. primarySSRC: ssrcInfo.ssrcs[0],
  546. group: {ssrcs: ssrcInfo.ssrcs.join(" "), semantics: "SIM"}});
  547. return ssrcInfo;
  548. } else {
  549. return {ssrcs: [RandomUtil.randomInt(1, 0xffffffff)], groups: []};
  550. }
  551. };
  552. module.exports = TraceablePeerConnection;