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.

simulcast.js 31KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121
  1. /*jslint plusplus: true */
  2. /*jslint nomen: true*/
  3. /**
  4. *
  5. * @constructor
  6. */
  7. function SimulcastUtils() {
  8. this.logger = new SimulcastLogger("SimulcastUtils");
  9. }
  10. /**
  11. *
  12. * @type {{}}
  13. * @private
  14. */
  15. SimulcastUtils.prototype._emptyCompoundIndex = {};
  16. /**
  17. *
  18. * @param lines
  19. * @param videoSources
  20. * @private
  21. */
  22. SimulcastUtils.prototype._replaceVideoSources = function (lines, videoSources) {
  23. var i, inVideo = false, index = -1, howMany = 0;
  24. this.logger.info('Replacing video sources...');
  25. for (i = 0; i < lines.length; i++) {
  26. if (inVideo && lines[i].substring(0, 'm='.length) === 'm=') {
  27. // Out of video.
  28. break;
  29. }
  30. if (!inVideo && lines[i].substring(0, 'm=video '.length) === 'm=video ') {
  31. // In video.
  32. inVideo = true;
  33. }
  34. if (inVideo && (lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:'
  35. || lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:')) {
  36. if (index === -1) {
  37. index = i;
  38. }
  39. howMany++;
  40. }
  41. }
  42. // efficiency baby ;)
  43. lines.splice.apply(lines,
  44. [index, howMany].concat(videoSources));
  45. };
  46. SimulcastUtils.prototype._getVideoSources = function (lines) {
  47. var i, inVideo = false, sb = [];
  48. this.logger.info('Getting video sources...');
  49. for (i = 0; i < lines.length; i++) {
  50. if (inVideo && lines[i].substring(0, 'm='.length) === 'm=') {
  51. // Out of video.
  52. break;
  53. }
  54. if (!inVideo && lines[i].substring(0, 'm=video '.length) === 'm=video ') {
  55. // In video.
  56. inVideo = true;
  57. }
  58. if (inVideo && lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:') {
  59. // In SSRC.
  60. sb.push(lines[i]);
  61. }
  62. if (inVideo && lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:') {
  63. sb.push(lines[i]);
  64. }
  65. }
  66. return sb;
  67. };
  68. SimulcastUtils.prototype.parseMedia = function (lines, mediatypes) {
  69. var i, res = [], type, cur_media, idx, ssrcs, cur_ssrc, ssrc,
  70. ssrc_attribute, group, semantics, skip;
  71. this.logger.info('Parsing media sources...');
  72. for (i = 0; i < lines.length; i++) {
  73. if (lines[i].substring(0, 'm='.length) === 'm=') {
  74. type = lines[i]
  75. .substr('m='.length, lines[i].indexOf(' ') - 'm='.length);
  76. skip = mediatypes !== undefined && mediatypes.indexOf(type) === -1;
  77. if (!skip) {
  78. cur_media = {
  79. 'type': type,
  80. 'sources': {},
  81. 'groups': []
  82. };
  83. res.push(cur_media);
  84. }
  85. } else if (!skip && lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:') {
  86. idx = lines[i].indexOf(' ');
  87. ssrc = lines[i].substring('a=ssrc:'.length, idx);
  88. if (cur_media.sources[ssrc] === undefined) {
  89. cur_ssrc = {'ssrc': ssrc};
  90. cur_media.sources[ssrc] = cur_ssrc;
  91. }
  92. ssrc_attribute = lines[i].substr(idx + 1).split(':', 2)[0];
  93. cur_ssrc[ssrc_attribute] = lines[i].substr(idx + 1).split(':', 2)[1];
  94. if (cur_media.base === undefined) {
  95. cur_media.base = cur_ssrc;
  96. }
  97. } else if (!skip && lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:') {
  98. idx = lines[i].indexOf(' ');
  99. semantics = lines[i].substr(0, idx).substr('a=ssrc-group:'.length);
  100. ssrcs = lines[i].substr(idx).trim().split(' ');
  101. group = {
  102. 'semantics': semantics,
  103. 'ssrcs': ssrcs
  104. };
  105. cur_media.groups.push(group);
  106. } else if (!skip && (lines[i].substring(0, 'a=sendrecv'.length) === 'a=sendrecv' ||
  107. lines[i].substring(0, 'a=recvonly'.length) === 'a=recvonly' ||
  108. lines[i].substring(0, 'a=sendonly'.length) === 'a=sendonly' ||
  109. lines[i].substring(0, 'a=inactive'.length) === 'a=inactive')) {
  110. cur_media.direction = lines[i].substring('a='.length, 8);
  111. }
  112. }
  113. return res;
  114. };
  115. /**
  116. * The _indexOfArray() method returns the first a CompoundIndex at which a
  117. * given element can be found in the array, or _emptyCompoundIndex if it is
  118. * not present.
  119. *
  120. * Example:
  121. *
  122. * _indexOfArray('3', [ 'this is line 1', 'this is line 2', 'this is line 3' ])
  123. *
  124. * returns {row: 2, column: 14}
  125. *
  126. * @param needle
  127. * @param haystack
  128. * @param start
  129. * @returns {}
  130. * @private
  131. */
  132. SimulcastUtils.prototype._indexOfArray = function (needle, haystack, start) {
  133. var length = haystack.length, idx, i;
  134. if (!start) {
  135. start = 0;
  136. }
  137. for (i = start; i < length; i++) {
  138. idx = haystack[i].indexOf(needle);
  139. if (idx !== -1) {
  140. return {row: i, column: idx};
  141. }
  142. }
  143. return this._emptyCompoundIndex;
  144. };
  145. SimulcastUtils.prototype._removeSimulcastGroup = function (lines) {
  146. var i;
  147. for (i = lines.length - 1; i >= 0; i--) {
  148. if (lines[i].indexOf('a=ssrc-group:SIM') !== -1) {
  149. lines.splice(i, 1);
  150. }
  151. }
  152. };
  153. SimulcastUtils.prototype._compileVideoSources = function (videoSources) {
  154. var sb = [], ssrc, addedSSRCs = [];
  155. this.logger.info('Compiling video sources...');
  156. // Add the groups
  157. if (videoSources.groups && videoSources.groups.length !== 0) {
  158. videoSources.groups.forEach(function (group) {
  159. if (group.ssrcs && group.ssrcs.length !== 0) {
  160. sb.push([['a=ssrc-group:', group.semantics].join(''), group.ssrcs.join(' ')].join(' '));
  161. // if (group.semantics !== 'SIM') {
  162. group.ssrcs.forEach(function (ssrc) {
  163. addedSSRCs.push(ssrc);
  164. sb.splice.apply(sb, [sb.length, 0].concat([
  165. ["a=ssrc:", ssrc, " cname:", videoSources.sources[ssrc].cname].join(''),
  166. ["a=ssrc:", ssrc, " msid:", videoSources.sources[ssrc].msid].join('')]));
  167. });
  168. //}
  169. }
  170. });
  171. }
  172. // Then add any free sources.
  173. if (videoSources.sources) {
  174. for (ssrc in videoSources.sources) {
  175. if (addedSSRCs.indexOf(ssrc) === -1) {
  176. sb.splice.apply(sb, [sb.length, 0].concat([
  177. ["a=ssrc:", ssrc, " cname:", videoSources.sources[ssrc].cname].join(''),
  178. ["a=ssrc:", ssrc, " msid:", videoSources.sources[ssrc].msid].join('')]));
  179. }
  180. }
  181. }
  182. return sb;
  183. };
  184. function SimulcastReceiver() {
  185. this.simulcastUtils = new SimulcastUtils();
  186. this.logger = new SimulcastLogger('SimulcastReceiver');
  187. }
  188. SimulcastReceiver.prototype._remoteVideoSourceCache = '';
  189. SimulcastReceiver.prototype._remoteMaps = {
  190. msid2Quality: {},
  191. ssrc2Msid: {},
  192. receivingVideoStreams: {}
  193. };
  194. SimulcastReceiver.prototype._cacheRemoteVideoSources = function (lines) {
  195. this._remoteVideoSourceCache = this.simulcastUtils._getVideoSources(lines);
  196. };
  197. SimulcastReceiver.prototype._restoreRemoteVideoSources = function (lines) {
  198. this.simulcastUtils._replaceVideoSources(lines, this._remoteVideoSourceCache);
  199. };
  200. SimulcastReceiver.prototype._ensureGoogConference = function (lines) {
  201. var sb;
  202. this.logger.info('Ensuring x-google-conference flag...')
  203. if (this.simulcastUtils._indexOfArray('a=x-google-flag:conference', lines) === this.simulcastUtils._emptyCompoundIndex) {
  204. // Add the google conference flag
  205. sb = this.simulcastUtils._getVideoSources(lines);
  206. sb = ['a=x-google-flag:conference'].concat(sb);
  207. this.simulcastUtils._replaceVideoSources(lines, sb);
  208. }
  209. };
  210. SimulcastReceiver.prototype._restoreSimulcastGroups = function (sb) {
  211. this._restoreRemoteVideoSources(sb);
  212. };
  213. /**
  214. * Restores the simulcast groups of the remote description. In
  215. * transformRemoteDescription we remove those in order for the set remote
  216. * description to succeed. The focus needs the signal the groups to new
  217. * participants.
  218. *
  219. * @param desc
  220. * @returns {*}
  221. */
  222. SimulcastReceiver.prototype.reverseTransformRemoteDescription = function (desc) {
  223. var sb;
  224. if (!desc || desc == null) {
  225. return desc;
  226. }
  227. if (config.enableSimulcast) {
  228. sb = desc.sdp.split('\r\n');
  229. this._restoreSimulcastGroups(sb);
  230. desc = new RTCSessionDescription({
  231. type: desc.type,
  232. sdp: sb.join('\r\n')
  233. });
  234. }
  235. return desc;
  236. };
  237. SimulcastUtils.prototype._ensureOrder = function (lines) {
  238. var videoSources, sb;
  239. videoSources = this.parseMedia(lines, ['video'])[0];
  240. sb = this._compileVideoSources(videoSources);
  241. this._replaceVideoSources(lines, sb);
  242. };
  243. SimulcastReceiver.prototype._updateRemoteMaps = function (lines) {
  244. var remoteVideoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0],
  245. videoSource, quality;
  246. // (re) initialize the remote maps.
  247. this._remoteMaps.msid2Quality = {};
  248. this._remoteMaps.ssrc2Msid = {};
  249. var self = this;
  250. if (remoteVideoSources.groups && remoteVideoSources.groups.length !== 0) {
  251. remoteVideoSources.groups.forEach(function (group) {
  252. if (group.semantics === 'SIM' && group.ssrcs && group.ssrcs.length !== 0) {
  253. quality = 0;
  254. group.ssrcs.forEach(function (ssrc) {
  255. videoSource = remoteVideoSources.sources[ssrc];
  256. self._remoteMaps.msid2Quality[videoSource.msid] = quality++;
  257. self._remoteMaps.ssrc2Msid[videoSource.ssrc] = videoSource.msid;
  258. });
  259. }
  260. });
  261. }
  262. };
  263. SimulcastReceiver.prototype._setReceivingVideoStream = function (endpoint, ssrc) {
  264. this._remoteMaps.receivingVideoStreams[endpoint] = ssrc;
  265. };
  266. /**
  267. * Returns a stream with single video track, the one currently being
  268. * received by this endpoint.
  269. *
  270. * @param stream the remote simulcast stream.
  271. * @returns {webkitMediaStream}
  272. */
  273. SimulcastReceiver.prototype.getReceivingVideoStream = function (stream) {
  274. var tracks, i, electedTrack, msid, quality = 0, receivingTrackId;
  275. var self = this;
  276. if (config.enableSimulcast) {
  277. stream.getVideoTracks().some(function (track) {
  278. return Object.keys(self._remoteMaps.receivingVideoStreams).some(function (endpoint) {
  279. var ssrc = self._remoteMaps.receivingVideoStreams[endpoint];
  280. var msid = self._remoteMaps.ssrc2Msid[ssrc];
  281. if (msid == [stream.id, track.id].join(' ')) {
  282. electedTrack = track;
  283. return true;
  284. }
  285. });
  286. });
  287. if (!electedTrack) {
  288. // we don't have an elected track, choose by initial quality.
  289. tracks = stream.getVideoTracks();
  290. for (i = 0; i < tracks.length; i++) {
  291. msid = [stream.id, tracks[i].id].join(' ');
  292. if (this._remoteMaps.msid2Quality[msid] === quality) {
  293. electedTrack = tracks[i];
  294. break;
  295. }
  296. }
  297. // TODO(gp) if the initialQuality could not be satisfied, lower
  298. // the requirement and try again.
  299. }
  300. }
  301. return (electedTrack)
  302. ? new webkitMediaStream([electedTrack])
  303. : stream;
  304. };
  305. /**
  306. * Gets the fully qualified msid (stream.id + track.id) associated to the
  307. * SSRC.
  308. *
  309. * @param ssrc
  310. * @returns {*}
  311. */
  312. SimulcastReceiver.prototype.getRemoteVideoStreamIdBySSRC = function (ssrc) {
  313. return this._remoteMaps.ssrc2Msid[ssrc];
  314. };
  315. function SimulcastSender() {
  316. this.simulcastUtils = new SimulcastUtils();
  317. this.logger = new SimulcastLogger('SimulcastSender');
  318. }
  319. SimulcastSender.prototype._localVideoSourceCache = '';
  320. SimulcastSender.prototype.localStream = null;
  321. SimulcastSender.prototype.displayedLocalVideoStream = null;
  322. SimulcastSender.prototype._generateGuid = (function () {
  323. function s4() {
  324. return Math.floor((1 + Math.random()) * 0x10000)
  325. .toString(16)
  326. .substring(1);
  327. }
  328. return function () {
  329. return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
  330. s4() + '-' + s4() + s4() + s4();
  331. };
  332. }());
  333. SimulcastSender.prototype._cacheLocalVideoSources = function (lines) {
  334. this._localVideoSourceCache = this.simulcastUtils._getVideoSources(lines);
  335. };
  336. SimulcastSender.prototype._restoreLocalVideoSources = function (lines) {
  337. this.simulcastUtils._replaceVideoSources(lines, this._localVideoSourceCache);
  338. };
  339. // Returns a random integer between min (included) and max (excluded)
  340. // Using Math.round() gives a non-uniform distribution!
  341. SimulcastSender.prototype._generateRandomSSRC = function () {
  342. var min = 0, max = 0xffffffff;
  343. return Math.floor(Math.random() * (max - min)) + min;
  344. };
  345. SimulcastSender.prototype._appendSimulcastGroup = function (lines) {
  346. var videoSources, ssrcGroup, simSSRC, numOfSubs = 2, i, sb, msid;
  347. this.logger.info('Appending simulcast group...');
  348. // Get the primary SSRC information.
  349. videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0];
  350. // Start building the SIM SSRC group.
  351. ssrcGroup = ['a=ssrc-group:SIM'];
  352. // The video source buffer.
  353. sb = [];
  354. // Create the simulcast sub-streams.
  355. for (i = 0; i < numOfSubs; i++) {
  356. // TODO(gp) prevent SSRC collision.
  357. simSSRC = this._generateRandomSSRC();
  358. ssrcGroup.push(simSSRC);
  359. sb.splice.apply(sb, [sb.length, 0].concat(
  360. [["a=ssrc:", simSSRC, " cname:", videoSources.base.cname].join(''),
  361. ["a=ssrc:", simSSRC, " msid:", videoSources.base.msid].join('')]
  362. ));
  363. this.logger.info(['Generated substream ', i, ' with SSRC ', simSSRC, '.'].join(''));
  364. }
  365. // Add the group sim layers.
  366. sb.splice(0, 0, ssrcGroup.join(' '))
  367. this.simulcastUtils._replaceVideoSources(lines, sb);
  368. };
  369. // Does the actual patching.
  370. SimulcastSender.prototype._ensureSimulcastGroup = function (lines) {
  371. this.logger.info('Ensuring simulcast group...');
  372. if (this.simulcastUtils._indexOfArray('a=ssrc-group:SIM', lines) === this.simulcastUtils._emptyCompoundIndex) {
  373. this._appendSimulcastGroup(lines);
  374. this._cacheLocalVideoSources(lines);
  375. } else {
  376. // verify that the ssrcs participating in the SIM group are present
  377. // in the SDP (needed for presence).
  378. this._restoreLocalVideoSources(lines);
  379. }
  380. };
  381. SimulcastSender.prototype.getLocalVideoStream = function () {
  382. return (this.displayedLocalVideoStream != null)
  383. ? this.displayedLocalVideoStream
  384. // in case we have no simulcast at all, i.e. we didn't perform the GUM
  385. : connection.jingle.localVideo;
  386. };
  387. function NativeSimulcastSender() {
  388. SimulcastSender.call(this); // call the super constructor.
  389. }
  390. NativeSimulcastSender.prototype = Object.create(SimulcastSender.prototype);
  391. NativeSimulcastSender.prototype._localExplosionMap = {};
  392. /**
  393. * Produces a single stream with multiple tracks for local video sources.
  394. *
  395. * @param lines
  396. * @private
  397. */
  398. NativeSimulcastSender.prototype._explodeSimulcastSenderSources = function (lines) {
  399. var sb, msid, sid, tid, videoSources, self;
  400. this.logger.info('Exploding local video sources...');
  401. videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0];
  402. self = this;
  403. if (videoSources.groups && videoSources.groups.length !== 0) {
  404. videoSources.groups.forEach(function (group) {
  405. if (group.semantics === 'SIM') {
  406. group.ssrcs.forEach(function (ssrc) {
  407. // Get the msid for this ssrc..
  408. if (self._localExplosionMap[ssrc]) {
  409. // .. either from the explosion map..
  410. msid = self._localExplosionMap[ssrc];
  411. } else {
  412. // .. or generate a new one (msid).
  413. sid = videoSources.sources[ssrc].msid
  414. .substring(0, videoSources.sources[ssrc].msid.indexOf(' '));
  415. tid = self._generateGuid();
  416. msid = [sid, tid].join(' ');
  417. self._localExplosionMap[ssrc] = msid;
  418. }
  419. // Assign it to the source object.
  420. videoSources.sources[ssrc].msid = msid;
  421. // TODO(gp) Change the msid of associated sources.
  422. });
  423. }
  424. });
  425. }
  426. sb = this.simulcastUtils._compileVideoSources(videoSources);
  427. this.simulcastUtils._replaceVideoSources(lines, sb);
  428. };
  429. /**
  430. * GUM for simulcast.
  431. *
  432. * @param constraints
  433. * @param success
  434. * @param err
  435. */
  436. NativeSimulcastSender.prototype.getUserMedia = function (constraints, success, err) {
  437. // There's nothing special to do for native simulcast, so just do a normal GUM.
  438. var self = this;
  439. navigator.webkitGetUserMedia(constraints, function (hqStream) {
  440. self.localStream = hqStream;
  441. success(hqStream);
  442. }, err);
  443. };
  444. /**
  445. * Prepares the local description for public usage (i.e. to be signaled
  446. * through Jingle to the focus).
  447. *
  448. * @param desc
  449. * @returns {RTCSessionDescription}
  450. */
  451. NativeSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) {
  452. var sb;
  453. if (!desc || desc == null) {
  454. return desc;
  455. }
  456. sb = desc.sdp.split('\r\n');
  457. this._explodeSimulcastSenderSources(sb);
  458. desc = new RTCSessionDescription({
  459. type: desc.type,
  460. sdp: sb.join('\r\n')
  461. });
  462. this.logger.fine(['Exploded local video sources', desc.sdp].join(' '));
  463. return desc;
  464. };
  465. /**
  466. * Ensures that the simulcast group is present in the answer, _if_ native
  467. * simulcast is enabled,
  468. *
  469. * @param desc
  470. * @returns {*}
  471. */
  472. NativeSimulcastSender.prototype.transformAnswer = function (desc) {
  473. var sb = desc.sdp.split('\r\n');
  474. // Even if we have enabled native simulcasting previously
  475. // (with a call to SLD with an appropriate SDP, for example),
  476. // createAnswer seems to consistently generate incomplete SDP
  477. // with missing SSRCS.
  478. //
  479. // So, subsequent calls to SLD will have missing SSRCS and presence
  480. // won't have the complete list of SRCs.
  481. this._ensureSimulcastGroup(sb);
  482. desc = new RTCSessionDescription({
  483. type: desc.type,
  484. sdp: sb.join('\r\n')
  485. });
  486. this.logger.fine(['Transformed answer', desc.sdp].join(' '));
  487. return desc;
  488. };
  489. /**
  490. *
  491. *
  492. * @param desc
  493. * @returns {*}
  494. */
  495. NativeSimulcastSender.prototype.transformLocalDescription = function (desc) {
  496. return desc;
  497. };
  498. /**
  499. * Removes the ssrc-group:SIM from the remote description bacause Chrome
  500. * either gets confused and thinks this is an FID group or, if an FID group
  501. * is already present, it fails to set the remote description.
  502. *
  503. * @param desc
  504. * @returns {*}
  505. */
  506. SimulcastReceiver.prototype.transformRemoteDescription = function (desc) {
  507. var sb = desc.sdp.split('\r\n');
  508. this._updateRemoteMaps(sb);
  509. this._cacheRemoteVideoSources(sb);
  510. // NOTE(gp) this needs to be called after updateRemoteMaps because we need the simulcast group in the _updateRemoteMaps() method.
  511. this.simulcastUtils._removeSimulcastGroup(sb);
  512. // We don't need the goog conference flag if we're not doing native
  513. // simulcast, but at the receiver, we have no idea if the sender is
  514. // doing native or not-native simulcast.
  515. this._ensureGoogConference(sb);
  516. desc = new RTCSessionDescription({
  517. type: desc.type,
  518. sdp: sb.join('\r\n')
  519. });
  520. this.logger.fine(['Transformed remote description', desc.sdp].join(' '));
  521. return desc;
  522. };
  523. NativeSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) {
  524. // Nothing to do here, native simulcast does that auto-magically.
  525. };
  526. NativeSimulcastSender.prototype.constructor = NativeSimulcastSender;
  527. function SimpleSimulcastSender() {
  528. SimulcastSender.call(this);
  529. }
  530. SimpleSimulcastSender.prototype = Object.create(SimulcastSender.prototype);
  531. SimpleSimulcastSender.prototype._localMaps = {
  532. msids: [],
  533. msid2ssrc: {}
  534. };
  535. /**
  536. * Groups local video sources together in the ssrc-group:SIM group.
  537. *
  538. * @param lines
  539. * @private
  540. */
  541. SimpleSimulcastSender.prototype._groupLocalVideoSources = function (lines) {
  542. var sb, videoSources, ssrcs = [], ssrc;
  543. this.logger.info('Grouping local video sources...');
  544. videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0];
  545. for (ssrc in videoSources.sources) {
  546. // jitsi-meet destroys/creates streams at various places causing
  547. // the original local stream ids to change. The only thing that
  548. // remains unchanged is the trackid.
  549. this._localMaps.msid2ssrc[videoSources.sources[ssrc].msid.split(' ')[1]] = ssrc;
  550. }
  551. var self = this;
  552. // TODO(gp) add only "free" sources.
  553. this._localMaps.msids.forEach(function (msid) {
  554. ssrcs.push(self._localMaps.msid2ssrc[msid]);
  555. });
  556. if (!videoSources.groups) {
  557. videoSources.groups = [];
  558. }
  559. videoSources.groups.push({
  560. 'semantics': 'SIM',
  561. 'ssrcs': ssrcs
  562. });
  563. sb = this.simulcastUtils._compileVideoSources(videoSources);
  564. this.simulcastUtils._replaceVideoSources(lines, sb);
  565. };
  566. /**
  567. * GUM for simulcast.
  568. *
  569. * @param constraints
  570. * @param success
  571. * @param err
  572. */
  573. SimpleSimulcastSender.prototype.getUserMedia = function (constraints, success, err) {
  574. // TODO(gp) what if we request a resolution not supported by the hardware?
  575. // TODO(gp) make the lq stream configurable; although this wouldn't work with native simulcast
  576. var lqConstraints = {
  577. audio: false,
  578. video: {
  579. mandatory: {
  580. maxWidth: 320,
  581. maxHeight: 180,
  582. maxFrameRate: 15
  583. }
  584. }
  585. };
  586. this.logger.info('HQ constraints: ', constraints);
  587. this.logger.info('LQ constraints: ', lqConstraints);
  588. // NOTE(gp) if we request the lq stream first webkitGetUserMedia
  589. // fails randomly. Tested with Chrome 37. As fippo suggested, the
  590. // reason appears to be that Chrome only acquires the cam once and
  591. // then downscales the picture (https://code.google.com/p/chromium/issues/detail?id=346616#c11)
  592. var self = this;
  593. navigator.webkitGetUserMedia(constraints, function (hqStream) {
  594. self.localStream = hqStream;
  595. // reset local maps.
  596. self._localMaps.msids = [];
  597. self._localMaps.msid2ssrc = {};
  598. // add hq trackid to local map
  599. self._localMaps.msids.push(hqStream.getVideoTracks()[0].id);
  600. navigator.webkitGetUserMedia(lqConstraints, function (lqStream) {
  601. self.displayedLocalVideoStream = lqStream;
  602. // NOTE(gp) The specification says Array.forEach() will visit
  603. // the array elements in numeric order, and that it doesn't
  604. // visit elements that don't exist.
  605. // add lq trackid to local map
  606. self._localMaps.msids.splice(0, 0, lqStream.getVideoTracks()[0].id);
  607. self.localStream.addTrack(lqStream.getVideoTracks()[0]);
  608. success(self.localStream);
  609. }, err);
  610. }, err);
  611. };
  612. /**
  613. * Prepares the local description for public usage (i.e. to be signaled
  614. * through Jingle to the focus).
  615. *
  616. * @param desc
  617. * @returns {RTCSessionDescription}
  618. */
  619. SimpleSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) {
  620. var sb;
  621. if (!desc || desc == null) {
  622. return desc;
  623. }
  624. sb = desc.sdp.split('\r\n');
  625. this._groupLocalVideoSources(sb);
  626. desc = new RTCSessionDescription({
  627. type: desc.type,
  628. sdp: sb.join('\r\n')
  629. });
  630. this.logger.fine('Grouped local video sources');
  631. this.logger.fine(desc.sdp);
  632. return desc;
  633. };
  634. /**
  635. * Ensures that the simulcast group is present in the answer, _if_ native
  636. * simulcast is enabled,
  637. *
  638. * @param desc
  639. * @returns {*}
  640. */
  641. SimpleSimulcastSender.prototype.transformAnswer = function (desc) {
  642. return desc;
  643. };
  644. /**
  645. *
  646. *
  647. * @param desc
  648. * @returns {*}
  649. */
  650. SimpleSimulcastSender.prototype.transformLocalDescription = function (desc) {
  651. var sb = desc.sdp.split('\r\n');
  652. this.simulcastUtils._removeSimulcastGroup(sb);
  653. desc = new RTCSessionDescription({
  654. type: desc.type,
  655. sdp: sb.join('\r\n')
  656. });
  657. this.logger.fine('Transformed local description');
  658. this.logger.fine(desc.sdp);
  659. return desc;
  660. };
  661. SimpleSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) {
  662. var trackid;
  663. var self = this;
  664. this.logger.log(['Requested to', enabled ? 'enable' : 'disable', ssrc].join(' '));
  665. if (Object.keys(this._localMaps.msid2ssrc).some(function (tid) {
  666. // Search for the track id that corresponds to the ssrc
  667. if (self._localMaps.msid2ssrc[tid] == ssrc) {
  668. trackid = tid;
  669. return true;
  670. }
  671. }) && self.localStream.getVideoTracks().some(function (track) {
  672. // Start/stop the track that corresponds to the track id
  673. if (track.id === trackid) {
  674. track.enabled = enabled;
  675. return true;
  676. }
  677. })) {
  678. this.logger.log([trackid, enabled ? 'enabled' : 'disabled'].join(' '));
  679. $(document).trigger(enabled
  680. ? 'simulcastlayerstarted'
  681. : 'simulcastlayerstopped');
  682. } else {
  683. this.logger.error("I don't have a local stream with SSRC " + ssrc);
  684. }
  685. };
  686. SimpleSimulcastSender.prototype.constructor = SimpleSimulcastSender;
  687. function NoSimulcastSender() {
  688. SimulcastSender.call(this);
  689. }
  690. NoSimulcastSender.prototype = Object.create(SimulcastSender.prototype);
  691. /**
  692. * GUM for simulcast.
  693. *
  694. * @param constraints
  695. * @param success
  696. * @param err
  697. */
  698. NoSimulcastSender.prototype.getUserMedia = function (constraints, success, err) {
  699. var self = this;
  700. navigator.webkitGetUserMedia(constraints, function (hqStream) {
  701. self.localStream = hqStream;
  702. success(self.localStream);
  703. }, err);
  704. };
  705. /**
  706. * Prepares the local description for public usage (i.e. to be signaled
  707. * through Jingle to the focus).
  708. *
  709. * @param desc
  710. * @returns {RTCSessionDescription}
  711. */
  712. NoSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) {
  713. return desc;
  714. };
  715. /**
  716. * Ensures that the simulcast group is present in the answer, _if_ native
  717. * simulcast is enabled,
  718. *
  719. * @param desc
  720. * @returns {*}
  721. */
  722. NoSimulcastSender.prototype.transformAnswer = function (desc) {
  723. return desc;
  724. };
  725. /**
  726. *
  727. *
  728. * @param desc
  729. * @returns {*}
  730. */
  731. NoSimulcastSender.prototype.transformLocalDescription = function (desc) {
  732. return desc;
  733. };
  734. NoSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) {
  735. };
  736. NoSimulcastSender.prototype.constructor = NoSimulcastSender;
  737. /**
  738. *
  739. * @constructor
  740. */
  741. function SimulcastManager() {
  742. // Create the simulcast utilities.
  743. this.simulcastUtils = new SimulcastUtils();
  744. // Create remote simulcast.
  745. this.simulcastReceiver = new SimulcastReceiver();
  746. // Initialize local simulcast.
  747. // TODO(gp) move into SimulcastManager.prototype.getUserMedia and take into
  748. // account constraints.
  749. if (!config.enableSimulcast) {
  750. this.simulcastSender = new NoSimulcastSender();
  751. } else {
  752. var isChromium = window.chrome,
  753. vendorName = window.navigator.vendor;
  754. if(isChromium !== null && isChromium !== undefined && vendorName === "Google Inc.") {
  755. var ver = parseInt(window.navigator.appVersion.match(/Chrome\/(\d+)\./)[1], 10);
  756. if (ver > 37) {
  757. this.simulcastSender = new NativeSimulcastSender();
  758. } else {
  759. this.simulcastSender = new NoSimulcastSender();
  760. }
  761. } else {
  762. this.simulcastSender = new NoSimulcastSender();
  763. }
  764. }
  765. }
  766. /**
  767. * Restores the simulcast groups of the remote description. In
  768. * transformRemoteDescription we remove those in order for the set remote
  769. * description to succeed. The focus needs the signal the groups to new
  770. * participants.
  771. *
  772. * @param desc
  773. * @returns {*}
  774. */
  775. SimulcastManager.prototype.reverseTransformRemoteDescription = function (desc) {
  776. return this.simulcastReceiver.reverseTransformRemoteDescription(desc);
  777. };
  778. /**
  779. * Removes the ssrc-group:SIM from the remote description bacause Chrome
  780. * either gets confused and thinks this is an FID group or, if an FID group
  781. * is already present, it fails to set the remote description.
  782. *
  783. * @param desc
  784. * @returns {*}
  785. */
  786. SimulcastManager.prototype.transformRemoteDescription = function (desc) {
  787. return this.simulcastReceiver.transformRemoteDescription(desc);
  788. };
  789. /**
  790. * Gets the fully qualified msid (stream.id + track.id) associated to the
  791. * SSRC.
  792. *
  793. * @param ssrc
  794. * @returns {*}
  795. */
  796. SimulcastManager.prototype.getRemoteVideoStreamIdBySSRC = function (ssrc) {
  797. return this.simulcastReceiver.getRemoteVideoStreamIdBySSRC(ssrc);
  798. };
  799. /**
  800. * Returns a stream with single video track, the one currently being
  801. * received by this endpoint.
  802. *
  803. * @param stream the remote simulcast stream.
  804. * @returns {webkitMediaStream}
  805. */
  806. SimulcastManager.prototype.getReceivingVideoStream = function (stream) {
  807. return this.simulcastReceiver.getReceivingVideoStream(stream);
  808. };
  809. /**
  810. *
  811. *
  812. * @param desc
  813. * @returns {*}
  814. */
  815. SimulcastManager.prototype.transformLocalDescription = function (desc) {
  816. return this.simulcastSender.transformLocalDescription(desc);
  817. };
  818. /**
  819. *
  820. * @returns {*}
  821. */
  822. SimulcastManager.prototype.getLocalVideoStream = function() {
  823. return this.simulcastSender.getLocalVideoStream();
  824. };
  825. /**
  826. * GUM for simulcast.
  827. *
  828. * @param constraints
  829. * @param success
  830. * @param err
  831. */
  832. SimulcastManager.prototype.getUserMedia = function (constraints, success, err) {
  833. this.simulcastSender.getUserMedia(constraints, success, err);
  834. };
  835. /**
  836. * Prepares the local description for public usage (i.e. to be signaled
  837. * through Jingle to the focus).
  838. *
  839. * @param desc
  840. * @returns {RTCSessionDescription}
  841. */
  842. SimulcastManager.prototype.reverseTransformLocalDescription = function (desc) {
  843. return this.simulcastSender.reverseTransformLocalDescription(desc);
  844. };
  845. /**
  846. * Ensures that the simulcast group is present in the answer, _if_ native
  847. * simulcast is enabled,
  848. *
  849. * @param desc
  850. * @returns {*}
  851. */
  852. SimulcastManager.prototype.transformAnswer = function (desc) {
  853. return this.simulcastSender.transformAnswer(desc);
  854. };
  855. /**
  856. *
  857. * @param lines
  858. * @param mediatypes
  859. * @returns {*}
  860. */
  861. SimulcastManager.prototype.parseMedia = function(lines, mediatypes) {
  862. var sb = lines.sdp.split('\r\n');
  863. return this.simulcastUtils.parseMedia(sb, mediatypes);
  864. };
  865. SimulcastManager.prototype._setReceivingVideoStream = function(endpoint, ssrc) {
  866. this.simulcastReceiver._setReceivingVideoStream(endpoint, ssrc);
  867. };
  868. SimulcastManager.prototype._setLocalVideoStreamEnabled = function(ssrc, enabled) {
  869. this.simulcastSender._setLocalVideoStreamEnabled(ssrc, enabled);
  870. };
  871. /**
  872. *
  873. * @constructor
  874. */
  875. function SimulcastLogger(name) {
  876. this.name = name;
  877. }
  878. SimulcastLogger.prototype.log = function (text) {
  879. console.log(text);
  880. };
  881. SimulcastLogger.prototype.info = function (text) {
  882. console.info(text);
  883. };
  884. SimulcastLogger.prototype.fine = function (text) {
  885. console.log(text);
  886. };
  887. SimulcastLogger.prototype.error = function (text) {
  888. console.error(text);
  889. };
  890. var simulcast = new SimulcastManager();
  891. $(document).bind('simulcastlayerschanged', function (event, endpointSimulcastLayers) {
  892. endpointSimulcastLayers.forEach(function (esl) {
  893. var ssrc = esl.simulcastLayer.primarySSRC;
  894. simulcast._setReceivingVideoStream(esl.endpoint, ssrc);
  895. });
  896. });
  897. $(document).bind('startsimulcastlayer', function (event, simulcastLayer) {
  898. var ssrc = simulcastLayer.primarySSRC;
  899. simulcast._setLocalVideoStreamEnabled(ssrc, true);
  900. });
  901. $(document).bind('stopsimulcastlayer', function (event, simulcastLayer) {
  902. var ssrc = simulcastLayer.primarySSRC;
  903. simulcast._setLocalVideoStreamEnabled(ssrc, false);
  904. });