Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

simulcast.js 31KB

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