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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856
  1. /*jslint plusplus: true */
  2. /*jslint nomen: true*/
  3. /**
  4. * Created by gp on 11/08/14.
  5. */
  6. function Simulcast() {
  7. "use strict";
  8. // TODO(gp) split the Simulcast class in two classes : NativeSimulcast and ClassicSimulcast.
  9. // Once we properly support native simulcast, enable it automatically in the
  10. // supported browsers (Chrome).
  11. this.useNativeSimulcast = false;
  12. // TODO(gp) we need a logging framework for javascript à la log4j or the
  13. // java logging framework that allows for selective log display
  14. this.debugLvl = 0;
  15. }
  16. (function () {
  17. "use strict";
  18. // global state for all transformers.
  19. var localExplosionMap = {}, localVideoSourceCache, emptyCompoundIndex,
  20. remoteVideoSourceCache, remoteMaps = {
  21. msid2Quality: {},
  22. ssrc2Msid: {},
  23. receivingVideoStreams: {}
  24. }, localMaps = {
  25. msids: [],
  26. msid2ssrc: {}
  27. };
  28. Simulcast.prototype._generateGuid = (function () {
  29. function s4() {
  30. return Math.floor((1 + Math.random()) * 0x10000)
  31. .toString(16)
  32. .substring(1);
  33. }
  34. return function () {
  35. return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
  36. s4() + '-' + s4() + s4() + s4();
  37. };
  38. }());
  39. Simulcast.prototype._cacheLocalVideoSources = function (lines) {
  40. localVideoSourceCache = this._getVideoSources(lines);
  41. };
  42. Simulcast.prototype._restoreLocalVideoSources = function (lines) {
  43. this._replaceVideoSources(lines, localVideoSourceCache);
  44. };
  45. Simulcast.prototype._cacheRemoteVideoSources = function (lines) {
  46. remoteVideoSourceCache = this._getVideoSources(lines);
  47. };
  48. Simulcast.prototype._restoreRemoteVideoSources = function (lines) {
  49. this._replaceVideoSources(lines, remoteVideoSourceCache);
  50. };
  51. Simulcast.prototype._replaceVideoSources = function (lines, videoSources) {
  52. var i, inVideo = false, index = -1, howMany = 0;
  53. if (this.debugLvl) {
  54. console.info('Replacing video sources...');
  55. }
  56. for (i = 0; i < lines.length; i++) {
  57. if (inVideo && lines[i].substring(0, 'm='.length) === 'm=') {
  58. // Out of video.
  59. break;
  60. }
  61. if (!inVideo && lines[i].substring(0, 'm=video '.length) === 'm=video ') {
  62. // In video.
  63. inVideo = true;
  64. }
  65. if (inVideo && (lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:'
  66. || lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:')) {
  67. if (index === -1) {
  68. index = i;
  69. }
  70. howMany++;
  71. }
  72. }
  73. // efficiency baby ;)
  74. lines.splice.apply(lines,
  75. [index, howMany].concat(videoSources));
  76. };
  77. Simulcast.prototype._getVideoSources = function (lines) {
  78. var i, inVideo = false, sb = [];
  79. if (this.debugLvl) {
  80. console.info('Getting video sources...');
  81. }
  82. for (i = 0; i < lines.length; i++) {
  83. if (inVideo && lines[i].substring(0, 'm='.length) === 'm=') {
  84. // Out of video.
  85. break;
  86. }
  87. if (!inVideo && lines[i].substring(0, 'm=video '.length) === 'm=video ') {
  88. // In video.
  89. inVideo = true;
  90. }
  91. if (inVideo && lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:') {
  92. // In SSRC.
  93. sb.push(lines[i]);
  94. }
  95. if (inVideo && lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:') {
  96. sb.push(lines[i]);
  97. }
  98. }
  99. return sb;
  100. };
  101. Simulcast.prototype._parseMedia = function (lines, mediatypes) {
  102. var i, res = [], type, cur_media, idx, ssrcs, cur_ssrc, ssrc,
  103. ssrc_attribute, group, semantics, skip;
  104. if (this.debugLvl) {
  105. console.info('Parsing media sources...');
  106. }
  107. for (i = 0; i < lines.length; i++) {
  108. if (lines[i].substring(0, 'm='.length) === 'm=') {
  109. type = lines[i]
  110. .substr('m='.length, lines[i].indexOf(' ') - 'm='.length);
  111. skip = mediatypes !== undefined && mediatypes.indexOf(type) === -1;
  112. if (!skip) {
  113. cur_media = {
  114. 'type': type,
  115. 'sources': {},
  116. 'groups': []
  117. };
  118. res.push(cur_media);
  119. }
  120. } else if (!skip && lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:') {
  121. idx = lines[i].indexOf(' ');
  122. ssrc = lines[i].substring('a=ssrc:'.length, idx);
  123. if (cur_media.sources[ssrc] === undefined) {
  124. cur_ssrc = {'ssrc': ssrc};
  125. cur_media.sources[ssrc] = cur_ssrc;
  126. }
  127. ssrc_attribute = lines[i].substr(idx + 1).split(':', 2)[0];
  128. cur_ssrc[ssrc_attribute] = lines[i].substr(idx + 1).split(':', 2)[1];
  129. if (cur_media.base === undefined) {
  130. cur_media.base = cur_ssrc;
  131. }
  132. } else if (!skip && lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:') {
  133. idx = lines[i].indexOf(' ');
  134. semantics = lines[i].substr(0, idx).substr('a=ssrc-group:'.length);
  135. ssrcs = lines[i].substr(idx).trim().split(' ');
  136. group = {
  137. 'semantics': semantics,
  138. 'ssrcs': ssrcs
  139. };
  140. cur_media.groups.push(group);
  141. } else if (!skip && (lines[i].substring(0, 'a=sendrecv'.length) === 'a=sendrecv' ||
  142. lines[i].substring(0, 'a=recvonly'.length) === 'a=recvonly' ||
  143. lines[i].substring(0, 'a=sendonly'.length) === 'a=sendonly' ||
  144. lines[i].substring(0, 'a=inactive'.length) === 'a=inactive')) {
  145. cur_media.direction = lines[i].substring('a='.length, 8);
  146. }
  147. }
  148. return res;
  149. };
  150. // Returns a random integer between min (included) and max (excluded)
  151. // Using Math.round() gives a non-uniform distribution!
  152. Simulcast.prototype._generateRandomSSRC = function () {
  153. var min = 0, max = 0xffffffff;
  154. return Math.floor(Math.random() * (max - min)) + min;
  155. };
  156. function CompoundIndex(obj) {
  157. if (obj !== undefined) {
  158. this.row = obj.row;
  159. this.column = obj.column;
  160. }
  161. }
  162. emptyCompoundIndex = new CompoundIndex();
  163. /**
  164. * The _indexOfArray() method returns the first a CompoundIndex at which a
  165. * given element can be found in the array, or emptyCompoundIndex if it is
  166. * not present.
  167. *
  168. * Example:
  169. *
  170. * _indexOfArray('3', [ 'this is line 1', 'this is line 2', 'this is line 3' ])
  171. *
  172. * returns {row: 2, column: 14}
  173. *
  174. * @param needle
  175. * @param haystack
  176. * @param start
  177. * @returns {CompoundIndex}
  178. * @private
  179. */
  180. Simulcast.prototype._indexOfArray = function (needle, haystack, start) {
  181. var length = haystack.length, idx, i;
  182. if (!start) {
  183. start = 0;
  184. }
  185. for (i = start; i < length; i++) {
  186. idx = haystack[i].indexOf(needle);
  187. if (idx !== -1) {
  188. return new CompoundIndex({row: i, column: idx});
  189. }
  190. }
  191. return emptyCompoundIndex;
  192. };
  193. Simulcast.prototype._removeSimulcastGroup = function (lines) {
  194. var i;
  195. for (i = lines.length - 1; i >= 0; i--) {
  196. if (lines[i].indexOf('a=ssrc-group:SIM') !== -1) {
  197. lines.splice(i, 1);
  198. }
  199. }
  200. };
  201. /**
  202. * Produces a single stream with multiple tracks for local video sources.
  203. *
  204. * @param lines
  205. * @private
  206. */
  207. Simulcast.prototype._explodeLocalSimulcastSources = function (lines) {
  208. var sb, msid, sid, tid, videoSources, self;
  209. if (this.debugLvl) {
  210. console.info('Exploding local video sources...');
  211. }
  212. videoSources = this._parseMedia(lines, ['video'])[0];
  213. self = this;
  214. if (videoSources.groups && videoSources.groups.length !== 0) {
  215. videoSources.groups.forEach(function (group) {
  216. if (group.semantics === 'SIM') {
  217. group.ssrcs.forEach(function (ssrc) {
  218. // Get the msid for this ssrc..
  219. if (localExplosionMap[ssrc]) {
  220. // .. either from the explosion map..
  221. msid = localExplosionMap[ssrc];
  222. } else {
  223. // .. or generate a new one (msid).
  224. sid = videoSources.sources[ssrc].msid
  225. .substring(0, videoSources.sources[ssrc].msid.indexOf(' '));
  226. tid = self._generateGuid();
  227. msid = [sid, tid].join(' ');
  228. localExplosionMap[ssrc] = msid;
  229. }
  230. // Assign it to the source object.
  231. videoSources.sources[ssrc].msid = msid;
  232. // TODO(gp) Change the msid of associated sources.
  233. });
  234. }
  235. });
  236. }
  237. sb = this._compileVideoSources(videoSources);
  238. this._replaceVideoSources(lines, sb);
  239. };
  240. /**
  241. * Groups local video sources together in the ssrc-group:SIM group.
  242. *
  243. * @param lines
  244. * @private
  245. */
  246. Simulcast.prototype._groupLocalVideoSources = function (lines) {
  247. var sb, videoSources, ssrcs = [], ssrc;
  248. if (this.debugLvl) {
  249. console.info('Grouping local video sources...');
  250. }
  251. videoSources = this._parseMedia(lines, ['video'])[0];
  252. for (ssrc in videoSources.sources) {
  253. // jitsi-meet destroys/creates streams at various places causing
  254. // the original local stream ids to change. The only thing that
  255. // remains unchanged is the trackid.
  256. localMaps.msid2ssrc[videoSources.sources[ssrc].msid.split(' ')[1]] = ssrc;
  257. }
  258. // TODO(gp) add only "free" sources.
  259. localMaps.msids.forEach(function (msid) {
  260. ssrcs.push(localMaps.msid2ssrc[msid]);
  261. });
  262. if (!videoSources.groups) {
  263. videoSources.groups = [];
  264. }
  265. videoSources.groups.push({
  266. 'semantics': 'SIM',
  267. 'ssrcs': ssrcs
  268. });
  269. sb = this._compileVideoSources(videoSources);
  270. this._replaceVideoSources(lines, sb);
  271. };
  272. Simulcast.prototype._appendSimulcastGroup = function (lines) {
  273. var videoSources, ssrcGroup, simSSRC, numOfSubs = 3, i, sb, msid;
  274. if (this.debugLvl) {
  275. console.info('Appending simulcast group...');
  276. }
  277. // Get the primary SSRC information.
  278. videoSources = this._parseMedia(lines, ['video'])[0];
  279. // Start building the SIM SSRC group.
  280. ssrcGroup = ['a=ssrc-group:SIM'];
  281. // The video source buffer.
  282. sb = [];
  283. // Create the simulcast sub-streams.
  284. for (i = 0; i < numOfSubs; i++) {
  285. // TODO(gp) prevent SSRC collision.
  286. simSSRC = this._generateRandomSSRC();
  287. ssrcGroup.push(simSSRC);
  288. sb.splice.apply(sb, [sb.length, 0].concat(
  289. [["a=ssrc:", simSSRC, " cname:", videoSources.base.cname].join(''),
  290. ["a=ssrc:", simSSRC, " msid:", videoSources.base.msid].join('')]
  291. ));
  292. if (this.debugLvl) {
  293. console.info(['Generated substream ', i, ' with SSRC ', simSSRC, '.'].join(''));
  294. }
  295. }
  296. // Add the group sim layers.
  297. sb.splice(0, 0, ssrcGroup.join(' '))
  298. this._replaceVideoSources(lines, sb);
  299. };
  300. // Does the actual patching.
  301. Simulcast.prototype._ensureSimulcastGroup = function (lines) {
  302. if (this.debugLvl) {
  303. console.info('Ensuring simulcast group...');
  304. }
  305. if (this._indexOfArray('a=ssrc-group:SIM', lines) === emptyCompoundIndex) {
  306. this._appendSimulcastGroup(lines);
  307. this._cacheLocalVideoSources(lines);
  308. } else {
  309. // verify that the ssrcs participating in the SIM group are present
  310. // in the SDP (needed for presence).
  311. this._restoreLocalVideoSources(lines);
  312. }
  313. };
  314. Simulcast.prototype._ensureGoogConference = function (lines) {
  315. var sb;
  316. if (this.debugLvl) {
  317. console.info('Ensuring x-google-conference flag...')
  318. }
  319. if (this._indexOfArray('a=x-google-flag:conference', lines) === emptyCompoundIndex) {
  320. // Add the google conference flag
  321. sb = this._getVideoSources(lines);
  322. sb = ['a=x-google-flag:conference'].concat(sb);
  323. this._replaceVideoSources(lines, sb);
  324. }
  325. };
  326. Simulcast.prototype._compileVideoSources = function (videoSources) {
  327. var sb = [], ssrc, addedSSRCs = [];
  328. if (this.debugLvl) {
  329. console.info('Compiling video sources...');
  330. }
  331. // Add the groups
  332. if (videoSources.groups && videoSources.groups.length !== 0) {
  333. videoSources.groups.forEach(function (group) {
  334. if (group.ssrcs && group.ssrcs.length !== 0) {
  335. sb.push([['a=ssrc-group:', group.semantics].join(''), group.ssrcs.join(' ')].join(' '));
  336. // if (group.semantics !== 'SIM') {
  337. group.ssrcs.forEach(function (ssrc) {
  338. addedSSRCs.push(ssrc);
  339. sb.splice.apply(sb, [sb.length, 0].concat([
  340. ["a=ssrc:", ssrc, " cname:", videoSources.sources[ssrc].cname].join(''),
  341. ["a=ssrc:", ssrc, " msid:", videoSources.sources[ssrc].msid].join('')]));
  342. });
  343. //}
  344. }
  345. });
  346. }
  347. // Then add any free sources.
  348. if (videoSources.sources) {
  349. for (ssrc in videoSources.sources) {
  350. if (addedSSRCs.indexOf(ssrc) === -1) {
  351. sb.splice.apply(sb, [sb.length, 0].concat([
  352. ["a=ssrc:", ssrc, " cname:", videoSources.sources[ssrc].cname].join(''),
  353. ["a=ssrc:", ssrc, " msid:", videoSources.sources[ssrc].msid].join('')]));
  354. }
  355. }
  356. }
  357. return sb;
  358. };
  359. /**
  360. * Ensures that the simulcast group is present in the answer, _if_ native
  361. * simulcast is enabled,
  362. *
  363. * @param desc
  364. * @returns {*}
  365. */
  366. Simulcast.prototype.transformAnswer = function (desc) {
  367. if (config.enableSimulcast && this.useNativeSimulcast) {
  368. var sb = desc.sdp.split('\r\n');
  369. // Even if we have enabled native simulcasting previously
  370. // (with a call to SLD with an appropriate SDP, for example),
  371. // createAnswer seems to consistently generate incomplete SDP
  372. // with missing SSRCS.
  373. //
  374. // So, subsequent calls to SLD will have missing SSRCS and presence
  375. // won't have the complete list of SRCs.
  376. this._ensureSimulcastGroup(sb);
  377. desc = new RTCSessionDescription({
  378. type: desc.type,
  379. sdp: sb.join('\r\n')
  380. });
  381. if (this.debugLvl && this.debugLvl > 1) {
  382. console.info('Transformed answer');
  383. console.info(desc.sdp);
  384. }
  385. }
  386. return desc;
  387. };
  388. Simulcast.prototype._restoreSimulcastGroups = function (sb) {
  389. this._restoreRemoteVideoSources(sb);
  390. };
  391. /**
  392. * Restores the simulcast groups of the remote description. In
  393. * transformRemoteDescription we remove those in order for the set remote
  394. * description to succeed. The focus needs the signal the groups to new
  395. * participants.
  396. *
  397. * @param desc
  398. * @returns {*}
  399. */
  400. Simulcast.prototype.reverseTransformRemoteDescription = function (desc) {
  401. var sb;
  402. if (!desc || desc == null) {
  403. return desc;
  404. }
  405. if (config.enableSimulcast) {
  406. sb = desc.sdp.split('\r\n');
  407. this._restoreSimulcastGroups(sb);
  408. desc = new RTCSessionDescription({
  409. type: desc.type,
  410. sdp: sb.join('\r\n')
  411. });
  412. }
  413. return desc;
  414. };
  415. /**
  416. * Prepares the local description for public usage (i.e. to be signaled
  417. * through Jingle to the focus).
  418. *
  419. * @param desc
  420. * @returns {RTCSessionDescription}
  421. */
  422. Simulcast.prototype.reverseTransformLocalDescription = function (desc) {
  423. var sb;
  424. if (!desc || desc == null) {
  425. return desc;
  426. }
  427. if (config.enableSimulcast) {
  428. if (this.useNativeSimulcast) {
  429. sb = desc.sdp.split('\r\n');
  430. this._explodeLocalSimulcastSources(sb);
  431. desc = new RTCSessionDescription({
  432. type: desc.type,
  433. sdp: sb.join('\r\n')
  434. });
  435. if (this.debugLvl && this.debugLvl > 1) {
  436. console.info('Exploded local video sources');
  437. console.info(desc.sdp);
  438. }
  439. } else {
  440. sb = desc.sdp.split('\r\n');
  441. this._groupLocalVideoSources(sb);
  442. desc = new RTCSessionDescription({
  443. type: desc.type,
  444. sdp: sb.join('\r\n')
  445. });
  446. if (this.debugLvl && this.debugLvl > 1) {
  447. console.info('Grouped local video sources');
  448. console.info(desc.sdp);
  449. }
  450. }
  451. }
  452. return desc;
  453. };
  454. Simulcast.prototype._ensureOrder = function (lines) {
  455. var videoSources, sb;
  456. videoSources = this._parseMedia(lines, ['video'])[0];
  457. sb = this._compileVideoSources(videoSources);
  458. this._replaceVideoSources(lines, sb);
  459. };
  460. Simulcast.prototype._updateRemoteMaps = function (lines) {
  461. var remoteVideoSources = this._parseMedia(lines, ['video'])[0],
  462. videoSource, quality;
  463. // (re) initialize the remote maps.
  464. remoteMaps.msid2Quality = {};
  465. remoteMaps.ssrc2Msid = {};
  466. if (remoteVideoSources.groups && remoteVideoSources.groups.length !== 0) {
  467. remoteVideoSources.groups.forEach(function (group) {
  468. if (group.semantics === 'SIM' && group.ssrcs && group.ssrcs.length !== 0) {
  469. quality = 0;
  470. group.ssrcs.forEach(function (ssrc) {
  471. videoSource = remoteVideoSources.sources[ssrc];
  472. remoteMaps.msid2Quality[videoSource.msid] = quality++;
  473. remoteMaps.ssrc2Msid[videoSource.ssrc] = videoSource.msid;
  474. });
  475. }
  476. });
  477. }
  478. };
  479. /**
  480. *
  481. *
  482. * @param desc
  483. * @returns {*}
  484. */
  485. Simulcast.prototype.transformLocalDescription = function (desc) {
  486. if (config.enableSimulcast && !this.useNativeSimulcast) {
  487. var sb = desc.sdp.split('\r\n');
  488. this._removeSimulcastGroup(sb);
  489. desc = new RTCSessionDescription({
  490. type: desc.type,
  491. sdp: sb.join('\r\n')
  492. });
  493. if (this.debugLvl && this.debugLvl > 1) {
  494. console.info('Transformed local description');
  495. console.info(desc.sdp);
  496. }
  497. }
  498. return desc;
  499. };
  500. /**
  501. * Removes the ssrc-group:SIM from the remote description bacause Chrome
  502. * either gets confused and thinks this is an FID group or, if an FID group
  503. * is already present, it fails to set the remote description.
  504. *
  505. * @param desc
  506. * @returns {*}
  507. */
  508. Simulcast.prototype.transformRemoteDescription = function (desc) {
  509. if (config.enableSimulcast) {
  510. var sb = desc.sdp.split('\r\n');
  511. this._updateRemoteMaps(sb);
  512. this._cacheRemoteVideoSources(sb);
  513. this._removeSimulcastGroup(sb); // NOTE(gp) this needs to be called after updateRemoteMaps because we need the simulcast group in the _updateRemoteMaps() method.
  514. if (this.useNativeSimulcast) {
  515. // We don't need the goog conference flag if we're not doing
  516. // native simulcast.
  517. this._ensureGoogConference(sb);
  518. }
  519. desc = new RTCSessionDescription({
  520. type: desc.type,
  521. sdp: sb.join('\r\n')
  522. });
  523. if (this.debugLvl && this.debugLvl > 1) {
  524. console.info('Transformed remote description');
  525. console.info(desc.sdp);
  526. }
  527. }
  528. return desc;
  529. };
  530. Simulcast.prototype._setReceivingVideoStream = function (endpoint, ssrc) {
  531. remoteMaps.receivingVideoStreams[endpoint] = ssrc;
  532. };
  533. /**
  534. * Returns a stream with single video track, the one currently being
  535. * received by this endpoint.
  536. *
  537. * @param stream the remote simulcast stream.
  538. * @returns {webkitMediaStream}
  539. */
  540. Simulcast.prototype.getReceivingVideoStream = function (stream) {
  541. var tracks, i, electedTrack, msid, quality = 0, receivingTrackId;
  542. if (config.enableSimulcast) {
  543. stream.getVideoTracks().some(function(track) {
  544. return Object.keys(remoteMaps.receivingVideoStreams).some(function(endpoint) {
  545. var ssrc = remoteMaps.receivingVideoStreams[endpoint];
  546. var msid = remoteMaps.ssrc2Msid[ssrc];
  547. if (msid == [stream.id, track.id].join(' ')) {
  548. electedTrack = track;
  549. return true;
  550. }
  551. });
  552. });
  553. if (!electedTrack) {
  554. // we don't have an elected track, choose by initial quality.
  555. tracks = stream.getVideoTracks();
  556. for (i = 0; i < tracks.length; i++) {
  557. msid = [stream.id, tracks[i].id].join(' ');
  558. if (remoteMaps.msid2Quality[msid] === quality) {
  559. electedTrack = tracks[i];
  560. break;
  561. }
  562. }
  563. // TODO(gp) if the initialQuality could not be satisfied, lower
  564. // the requirement and try again.
  565. }
  566. }
  567. return (electedTrack)
  568. ? new webkitMediaStream([electedTrack])
  569. : stream;
  570. };
  571. var localStream, displayedLocalVideoStream;
  572. /**
  573. * GUM for simulcast.
  574. *
  575. * @param constraints
  576. * @param success
  577. * @param err
  578. */
  579. Simulcast.prototype.getUserMedia = function (constraints, success, err) {
  580. // TODO(gp) what if we request a resolution not supported by the hardware?
  581. // TODO(gp) make the lq stream configurable; although this wouldn't work with native simulcast
  582. var lqConstraints = {
  583. audio: false,
  584. video: {
  585. mandatory: {
  586. maxWidth: 320,
  587. maxHeight: 180,
  588. maxFrameRate: 15
  589. }
  590. }
  591. };
  592. console.log('HQ constraints: ', constraints);
  593. console.log('LQ constraints: ', lqConstraints);
  594. if (config.enableSimulcast && !this.useNativeSimulcast) {
  595. // NOTE(gp) if we request the lq stream first webkitGetUserMedia
  596. // fails randomly. Tested with Chrome 37. As fippo suggested, the
  597. // reason appears to be that Chrome only acquires the cam once and
  598. // then downscales the picture (https://code.google.com/p/chromium/issues/detail?id=346616#c11)
  599. navigator.webkitGetUserMedia(constraints, function (hqStream) {
  600. localStream = hqStream;
  601. // reset local maps.
  602. localMaps.msids = [];
  603. localMaps.msid2ssrc = {};
  604. // add hq trackid to local map
  605. localMaps.msids.push(hqStream.getVideoTracks()[0].id);
  606. navigator.webkitGetUserMedia(lqConstraints, function (lqStream) {
  607. displayedLocalVideoStream = lqStream;
  608. // NOTE(gp) The specification says Array.forEach() will visit
  609. // the array elements in numeric order, and that it doesn't
  610. // visit elements that don't exist.
  611. // add lq trackid to local map
  612. localMaps.msids.splice(0, 0, lqStream.getVideoTracks()[0].id);
  613. localStream.addTrack(lqStream.getVideoTracks()[0]);
  614. success(localStream);
  615. }, err);
  616. }, err);
  617. } else {
  618. // There's nothing special to do for native simulcast, so just do a normal GUM.
  619. navigator.webkitGetUserMedia(constraints, function (hqStream) {
  620. // reset local maps.
  621. localMaps.msids = [];
  622. localMaps.msid2ssrc = {};
  623. // add hq stream to local map
  624. localMaps.msids.push(hqStream.getVideoTracks()[0].id);
  625. displayedLocalVideoStream = localStream = hqStream;
  626. success(localStream);
  627. }, err);
  628. }
  629. };
  630. /**
  631. * Gets the fully qualified msid (stream.id + track.id) associated to the
  632. * SSRC.
  633. *
  634. * @param ssrc
  635. * @returns {*}
  636. */
  637. Simulcast.prototype.getRemoteVideoStreamIdBySSRC = function (ssrc) {
  638. return remoteMaps.ssrc2Msid[ssrc];
  639. };
  640. Simulcast.prototype.parseMedia = function (desc, mediatypes) {
  641. var lines = desc.sdp.split('\r\n');
  642. return this._parseMedia(lines, mediatypes);
  643. };
  644. Simulcast.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) {
  645. var trackid;
  646. console.log(['Requested to', enabled ? 'enable' : 'disable', ssrc].join(' '));
  647. if (Object.keys(localMaps.msid2ssrc).some(function (tid) {
  648. // Search for the track id that corresponds to the ssrc
  649. if (localMaps.msid2ssrc[tid] == ssrc) {
  650. trackid = tid;
  651. return true;
  652. }
  653. }) && localStream.getVideoTracks().some(function(track) {
  654. // Start/stop the track that corresponds to the track id
  655. if (track.id === trackid) {
  656. track.enabled = enabled;
  657. return true;
  658. }
  659. })) {
  660. console.log([trackid, enabled ? 'enabled' : 'disabled'].join(' '));
  661. $(document).trigger(enabled
  662. ? 'simulcastlayerstarted'
  663. : 'simulcastlayerstopped');
  664. } else {
  665. console.error("I don't have a local stream with SSRC " + ssrc);
  666. }
  667. };
  668. Simulcast.prototype.getLocalVideoStream = function() {
  669. return (displayedLocalVideoStream)
  670. ? displayedLocalVideoStream
  671. // in case we have no simulcast at all, i.e. we didn't perform the GUM
  672. : connection.jingle.localVideo;
  673. };
  674. $(document).bind('simulcastlayerschanged', function (event, endpointSimulcastLayers) {
  675. endpointSimulcastLayers.forEach(function (esl) {
  676. var ssrc = esl.simulcastLayer.primarySSRC;
  677. var simulcast = new Simulcast();
  678. simulcast._setReceivingVideoStream(esl.endpoint, ssrc);
  679. });
  680. });
  681. $(document).bind('startsimulcastlayer', function(event, simulcastLayer) {
  682. var ssrc = simulcastLayer.primarySSRC;
  683. var simulcast = new Simulcast();
  684. simulcast._setLocalVideoStreamEnabled(ssrc, true);
  685. });
  686. $(document).bind('stopsimulcastlayer', function(event, simulcastLayer) {
  687. var ssrc = simulcastLayer.primarySSRC;
  688. var simulcast = new Simulcast();
  689. simulcast._setLocalVideoStreamEnabled(ssrc, false);
  690. });
  691. }());