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 32KB

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