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

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