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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  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. this.debugLvl = 1;
  10. }
  11. (function () {
  12. "use strict";
  13. // global state for all transformers.
  14. var localExplosionMap = {}, localVideoSourceCache, emptyCompoundIndex,
  15. remoteMaps = {
  16. msid2Quality: {},
  17. ssrc2Msid: {},
  18. receivingVideoStreams: {}
  19. }, localMaps = {
  20. msids: [],
  21. msid2ssrc: {}
  22. };
  23. Simulcast.prototype._generateGuid = (function () {
  24. function s4() {
  25. return Math.floor((1 + Math.random()) * 0x10000)
  26. .toString(16)
  27. .substring(1);
  28. }
  29. return function () {
  30. return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
  31. s4() + '-' + s4() + s4() + s4();
  32. };
  33. }());
  34. Simulcast.prototype._cacheVideoSources = function (lines) {
  35. localVideoSourceCache = this._getVideoSources(lines);
  36. };
  37. Simulcast.prototype._restoreVideoSources = function (lines) {
  38. this._replaceVideoSources(lines, localVideoSourceCache);
  39. };
  40. Simulcast.prototype._replaceVideoSources = function (lines, videoSources) {
  41. var i, inVideo = false, index = -1, howMany = 0;
  42. if (this.debugLvl) {
  43. console.info('Replacing video sources...');
  44. }
  45. for (i = 0; i < lines.length; i++) {
  46. if (inVideo && lines[i].substring(0, 'm='.length) === 'm=') {
  47. // Out of video.
  48. break;
  49. }
  50. if (!inVideo && lines[i].substring(0, 'm=video '.length) === 'm=video ') {
  51. // In video.
  52. inVideo = true;
  53. }
  54. if (inVideo && (lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:'
  55. || lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:')) {
  56. if (index === -1) {
  57. index = i;
  58. }
  59. howMany++;
  60. }
  61. }
  62. // efficiency baby ;)
  63. lines.splice.apply(lines,
  64. [index, howMany].concat(videoSources));
  65. };
  66. Simulcast.prototype._getVideoSources = function (lines) {
  67. var i, inVideo = false, sb = [];
  68. if (this.debugLvl) {
  69. console.info('Getting video sources...');
  70. }
  71. for (i = 0; i < lines.length; i++) {
  72. if (inVideo && lines[i].substring(0, 'm='.length) === 'm=') {
  73. // Out of video.
  74. break;
  75. }
  76. if (!inVideo && lines[i].substring(0, 'm=video '.length) === 'm=video ') {
  77. // In video.
  78. inVideo = true;
  79. }
  80. if (inVideo && lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:') {
  81. // In SSRC.
  82. sb.push(lines[i]);
  83. }
  84. if (inVideo && lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:') {
  85. sb.push(lines[i]);
  86. }
  87. }
  88. return sb;
  89. };
  90. Simulcast.prototype._parseMedia = function (lines, mediatypes) {
  91. var i, res = [], type, cur_media, idx, ssrcs, cur_ssrc, ssrc,
  92. ssrc_attribute, group, semantics, skip;
  93. if (this.debugLvl) {
  94. console.info('Parsing media sources...');
  95. }
  96. for (i = 0; i < lines.length; i++) {
  97. if (lines[i].substring(0, 'm='.length) === 'm=') {
  98. type = lines[i]
  99. .substr('m='.length, lines[i].indexOf(' ') - 'm='.length);
  100. skip = mediatypes !== undefined && mediatypes.indexOf(type) === -1;
  101. if (!skip) {
  102. cur_media = {
  103. 'type': type,
  104. 'sources': {},
  105. 'groups': []
  106. };
  107. res.push(cur_media);
  108. }
  109. } else if (!skip && lines[i].substring(0, 'a=ssrc:'.length) === 'a=ssrc:') {
  110. idx = lines[i].indexOf(' ');
  111. ssrc = lines[i].substring('a=ssrc:'.length, idx);
  112. if (cur_media.sources[ssrc] === undefined) {
  113. cur_ssrc = {'ssrc': ssrc};
  114. cur_media.sources[ssrc] = cur_ssrc;
  115. }
  116. ssrc_attribute = lines[i].substr(idx + 1).split(':', 2)[0];
  117. cur_ssrc[ssrc_attribute] = lines[i].substr(idx + 1).split(':', 2)[1];
  118. if (cur_media.base === undefined) {
  119. cur_media.base = cur_ssrc;
  120. }
  121. } else if (!skip && lines[i].substring(0, 'a=ssrc-group:'.length) === 'a=ssrc-group:') {
  122. idx = lines[i].indexOf(' ');
  123. semantics = lines[i].substr(0, idx).substr('a=ssrc-group:'.length);
  124. ssrcs = lines[i].substr(idx).trim().split(' ');
  125. group = {
  126. 'semantics': semantics,
  127. 'ssrcs': ssrcs
  128. };
  129. cur_media.groups.push(group);
  130. } else if (!skip && (lines[i].substring(0, 'a=sendrecv'.length) === 'a=sendrecv' ||
  131. lines[i].substring(0, 'a=recvonly'.length) === 'a=recvonly' ||
  132. lines[i].substring(0, 'a=sendonly'.length) === 'a=sendonly' ||
  133. lines[i].substring(0, 'a=inactive'.length) === 'a=inactive')) {
  134. cur_media.direction = lines[i].substring('a='.length, 8);
  135. }
  136. }
  137. return res;
  138. };
  139. // Returns a random integer between min (included) and max (excluded)
  140. // Using Math.round() will give you a non-uniform distribution!
  141. Simulcast.prototype._generateRandomSSRC = function () {
  142. var min = 0, max = 0xffffffff;
  143. return Math.floor(Math.random() * (max - min)) + min;
  144. };
  145. function CompoundIndex(obj) {
  146. if (obj !== undefined) {
  147. this.row = obj.row;
  148. this.column = obj.column;
  149. }
  150. }
  151. emptyCompoundIndex = new CompoundIndex();
  152. Simulcast.prototype._indexOfArray = function (needle, haystack, start) {
  153. var length = haystack.length, idx, i;
  154. if (!start) {
  155. start = 0;
  156. }
  157. for (i = start; i < length; i++) {
  158. idx = haystack[i].indexOf(needle);
  159. if (idx !== -1) {
  160. return new CompoundIndex({row: i, column: idx});
  161. }
  162. }
  163. return emptyCompoundIndex;
  164. };
  165. Simulcast.prototype._removeSimulcastGroup = function (lines) {
  166. var i;
  167. for (i = lines.length - 1; i >= 0; i--) {
  168. if (lines[i].indexOf('a=ssrc-group:SIM') !== -1) {
  169. lines.splice(i, 1);
  170. }
  171. }
  172. };
  173. Simulcast.prototype._explodeLocalSimulcastSources = function (lines) {
  174. var sb, msid, sid, tid, videoSources, self;
  175. if (this.debugLvl) {
  176. console.info('Exploding local video sources...');
  177. }
  178. videoSources = this._parseMedia(lines, ['video'])[0];
  179. self = this;
  180. if (videoSources.groups && videoSources.groups.length !== 0) {
  181. videoSources.groups.forEach(function (group) {
  182. if (group.semantics === 'SIM') {
  183. group.ssrcs.forEach(function (ssrc) {
  184. // Get the msid for this ssrc..
  185. if (localExplosionMap[ssrc]) {
  186. // .. either from the explosion map..
  187. msid = localExplosionMap[ssrc];
  188. } else {
  189. // .. or generate a new one (msid).
  190. sid = videoSources.sources[ssrc].msid
  191. .substring(0, videoSources.sources[ssrc].msid.indexOf(' '));
  192. tid = self._generateGuid();
  193. msid = [sid, tid].join(' ');
  194. localExplosionMap[ssrc] = msid;
  195. }
  196. // Assign it to the source object.
  197. videoSources.sources[ssrc].msid = msid;
  198. // TODO(gp) Change the msid of associated sources.
  199. });
  200. }
  201. });
  202. }
  203. sb = this._compileVideoSources(videoSources);
  204. this._replaceVideoSources(lines, sb);
  205. };
  206. Simulcast.prototype._groupLocalVideoSources = function (lines) {
  207. var sb, videoSources, ssrcs = [], ssrc;
  208. if (this.debugLvl) {
  209. console.info('Grouping local video sources...');
  210. }
  211. videoSources = this._parseMedia(lines, ['video'])[0];
  212. for (ssrc in videoSources.sources) {
  213. // jitsi-meet destroys/creates streams at various places causing
  214. // the original local stream ids to change. The only thing that
  215. // remains unchanged is the trackid.
  216. localMaps.msid2ssrc[videoSources.sources[ssrc].msid.split(' ')[1]] = ssrc;
  217. }
  218. // TODO(gp) add only "free" sources.
  219. localMaps.msids.forEach(function (msid) {
  220. ssrcs.push(localMaps.msid2ssrc[msid]);
  221. });
  222. if (!videoSources.groups) {
  223. videoSources.groups = [];
  224. }
  225. videoSources.groups.push({
  226. 'semantics': 'SIM',
  227. 'ssrcs': ssrcs
  228. });
  229. sb = this._compileVideoSources(videoSources);
  230. this._replaceVideoSources(lines, sb);
  231. };
  232. Simulcast.prototype._appendSimulcastGroup = function (lines) {
  233. var videoSources, ssrcGroup, simSSRC, numOfSubs = 3, i, sb, msid;
  234. if (this.debugLvl) {
  235. console.info('Appending simulcast group...');
  236. }
  237. // Get the primary SSRC information.
  238. videoSources = this._parseMedia(lines, ['video'])[0];
  239. // Start building the SIM SSRC group.
  240. ssrcGroup = ['a=ssrc-group:SIM'];
  241. // The video source buffer.
  242. sb = [];
  243. // Create the simulcast sub-streams.
  244. for (i = 0; i < numOfSubs; i++) {
  245. // TODO(gp) prevent SSRC collision.
  246. simSSRC = this._generateRandomSSRC();
  247. ssrcGroup.push(simSSRC);
  248. sb.splice.apply(sb, [sb.length, 0].concat(
  249. [["a=ssrc:", simSSRC, " cname:", videoSources.base.cname].join(''),
  250. ["a=ssrc:", simSSRC, " msid:", videoSources.base.msid].join('')]
  251. ));
  252. if (this.debugLvl) {
  253. console.info(['Generated substream ', i, ' with SSRC ', simSSRC, '.'].join(''));
  254. }
  255. }
  256. // Add the group sim layers.
  257. sb.splice(0, 0, ssrcGroup.join(' '))
  258. this._replaceVideoSources(lines, sb);
  259. };
  260. // Does the actual patching.
  261. Simulcast.prototype._ensureSimulcastGroup = function (lines) {
  262. if (this.debugLvl) {
  263. console.info('Ensuring simulcast group...');
  264. }
  265. if (this._indexOfArray('a=ssrc-group:SIM', lines) === emptyCompoundIndex) {
  266. this._appendSimulcastGroup(lines);
  267. this._cacheVideoSources(lines);
  268. } else {
  269. // verify that the ssrcs participating in the SIM group are present
  270. // in the SDP (needed for presence).
  271. this._restoreVideoSources(lines);
  272. }
  273. };
  274. Simulcast.prototype._ensureGoogConference = function (lines) {
  275. var sb;
  276. if (this.debugLvl) {
  277. console.info('Ensuring x-google-conference flag...')
  278. }
  279. if (this._indexOfArray('a=x-google-flag:conference', lines) === emptyCompoundIndex) {
  280. // Add the google conference flag
  281. sb = this._getVideoSources(lines);
  282. sb = ['a=x-google-flag:conference'].concat(sb);
  283. this._replaceVideoSources(lines, sb);
  284. }
  285. };
  286. Simulcast.prototype._compileVideoSources = function (videoSources) {
  287. var sb = [], ssrc, addedSSRCs = [];
  288. if (this.debugLvl) {
  289. console.info('Compiling video sources...');
  290. }
  291. // Add the groups
  292. if (videoSources.groups && videoSources.groups.length !== 0) {
  293. videoSources.groups.forEach(function (group) {
  294. if (group.ssrcs && group.ssrcs.length !== 0) {
  295. sb.push([['a=ssrc-group:', group.semantics].join(''), group.ssrcs.join(' ')].join(' '));
  296. // if (group.semantics !== 'SIM') {
  297. group.ssrcs.forEach(function (ssrc) {
  298. addedSSRCs.push(ssrc);
  299. sb.splice.apply(sb, [sb.length, 0].concat([
  300. ["a=ssrc:", ssrc, " cname:", videoSources.sources[ssrc].cname].join(''),
  301. ["a=ssrc:", ssrc, " msid:", videoSources.sources[ssrc].msid].join('')]));
  302. });
  303. //}
  304. }
  305. });
  306. }
  307. // Then add any free sources.
  308. if (videoSources.sources) {
  309. for (ssrc in videoSources.sources) {
  310. if (addedSSRCs.indexOf(ssrc) === -1) {
  311. sb.splice.apply(sb, [sb.length, 0].concat([
  312. ["a=ssrc:", ssrc, " cname:", videoSources.sources[ssrc].cname].join(''),
  313. ["a=ssrc:", ssrc, " msid:", videoSources.sources[ssrc].msid].join('')]));
  314. }
  315. }
  316. }
  317. return sb;
  318. };
  319. Simulcast.prototype.transformAnswer = function (desc) {
  320. if (config.enableSimulcast && config.useNativeSimulcast) {
  321. var sb = desc.sdp.split('\r\n');
  322. // Even if we have enabled native simulcasting previously
  323. // (with a call to SLD with an appropriate SDP, for example),
  324. // createAnswer seems to consistently generate incomplete SDP
  325. // with missing SSRCS.
  326. //
  327. // So, subsequent calls to SLD will have missing SSRCS and presence
  328. // won't have the complete list of SRCs.
  329. this._ensureSimulcastGroup(sb);
  330. desc = new RTCSessionDescription({
  331. type: desc.type,
  332. sdp: sb.join('\r\n')
  333. });
  334. if (this.debugLvl && this.debugLvl > 1) {
  335. console.info('Transformed answer');
  336. console.info(desc.sdp);
  337. }
  338. }
  339. return desc;
  340. };
  341. Simulcast.prototype.makeLocalDescriptionPublic = function (desc) {
  342. var sb;
  343. if (!desc || desc == null)
  344. return desc;
  345. if (config.enableSimulcast) {
  346. if (config.useNativeSimulcast) {
  347. sb = desc.sdp.split('\r\n');
  348. this._explodeLocalSimulcastSources(sb);
  349. desc = new RTCSessionDescription({
  350. type: desc.type,
  351. sdp: sb.join('\r\n')
  352. });
  353. if (this.debugLvl && this.debugLvl > 1) {
  354. console.info('Exploded local video sources');
  355. console.info(desc.sdp);
  356. }
  357. } else {
  358. sb = desc.sdp.split('\r\n');
  359. this._groupLocalVideoSources(sb);
  360. desc = new RTCSessionDescription({
  361. type: desc.type,
  362. sdp: sb.join('\r\n')
  363. });
  364. if (this.debugLvl && this.debugLvl > 1) {
  365. console.info('Grouped local video sources');
  366. console.info(desc.sdp);
  367. }
  368. }
  369. }
  370. return desc;
  371. };
  372. Simulcast.prototype._ensureOrder = function (lines) {
  373. var videoSources, sb;
  374. videoSources = this._parseMedia(lines, ['video'])[0];
  375. sb = this._compileVideoSources(videoSources);
  376. this._replaceVideoSources(lines, sb);
  377. };
  378. Simulcast.prototype.transformBridgeDescription = function (desc) {
  379. if (config.enableSimulcast && config.useNativeSimulcast) {
  380. var sb = desc.sdp.split('\r\n');
  381. this._ensureGoogConference(sb);
  382. desc = new RTCSessionDescription({
  383. type: desc.type,
  384. sdp: sb.join('\r\n')
  385. });
  386. if (this.debugLvl && this.debugLvl > 1) {
  387. console.info('Transformed bridge description');
  388. console.info(desc.sdp);
  389. }
  390. }
  391. return desc;
  392. };
  393. Simulcast.prototype._updateRemoteMaps = function (lines) {
  394. var remoteVideoSources = this._parseMedia(lines, ['video'])[0], videoSource, quality;
  395. if (remoteVideoSources.groups && remoteVideoSources.groups.length !== 0) {
  396. remoteVideoSources.groups.forEach(function (group) {
  397. if (group.semantics === 'SIM' && group.ssrcs && group.ssrcs.length !== 0) {
  398. quality = 0;
  399. group.ssrcs.forEach(function (ssrc) {
  400. videoSource = remoteVideoSources.sources[ssrc];
  401. remoteMaps.msid2Quality[videoSource.msid] = quality++;
  402. remoteMaps.ssrc2Msid[videoSource.ssrc] = videoSource.msid;
  403. });
  404. }
  405. });
  406. }
  407. };
  408. Simulcast.prototype.transformLocalDescription = function (desc) {
  409. if (config.enableSimulcast && !config.useNativeSimulcast) {
  410. var sb = desc.sdp.split('\r\n');
  411. this._removeSimulcastGroup(sb);
  412. desc = new RTCSessionDescription({
  413. type: desc.type,
  414. sdp: sb.join('\r\n')
  415. });
  416. if (this.debugLvl && this.debugLvl > 1) {
  417. console.info('Transformed local description');
  418. console.info(desc.sdp);
  419. }
  420. }
  421. return desc;
  422. };
  423. Simulcast.prototype.transformRemoteDescription = function (desc) {
  424. if (config.enableSimulcast) {
  425. var sb = desc.sdp.split('\r\n');
  426. this._updateRemoteMaps(sb);
  427. this._removeSimulcastGroup(sb); // NOTE(gp) this needs to be called after updateRemoteMaps!
  428. this._ensureGoogConference(sb);
  429. desc = new RTCSessionDescription({
  430. type: desc.type,
  431. sdp: sb.join('\r\n')
  432. });
  433. if (this.debugLvl && this.debugLvl > 1) {
  434. console.info('Transformed remote description');
  435. console.info(desc.sdp);
  436. }
  437. }
  438. return desc;
  439. };
  440. Simulcast.prototype.setReceivingVideoStream = function (ssrc) {
  441. var receivingTrack = remoteMaps.ssrc2Msid[ssrc],
  442. msidParts = receivingTrack.split(' ');
  443. remoteMaps.receivingVideoStreams[msidParts[0]] = msidParts[1];
  444. };
  445. Simulcast.prototype.getReceivingVideoStream = function (stream) {
  446. var tracks, track, i, electedTrack, msid, quality = 1, receivingTrackId;
  447. if (config.enableSimulcast) {
  448. if (remoteMaps.receivingVideoStreams[stream.id])
  449. {
  450. receivingTrackId = remoteMaps.receivingVideoStreams[stream.id];
  451. tracks = stream.getVideoTracks();
  452. for (i = 0; i < tracks.length; i++) {
  453. if (receivingTrackId === tracks[i].id) {
  454. electedTrack = tracks[i];
  455. break;
  456. }
  457. }
  458. }
  459. if (!electedTrack) {
  460. tracks = stream.getVideoTracks();
  461. for (i = 0; i < tracks.length; i++) {
  462. track = tracks[i];
  463. msid = [stream.id, track.id].join(' ');
  464. if (remoteMaps.msid2Quality[msid] === quality) {
  465. electedTrack = track;
  466. break;
  467. }
  468. }
  469. }
  470. }
  471. return (electedTrack)
  472. ? new webkitMediaStream([electedTrack])
  473. : stream;
  474. };
  475. Simulcast.prototype.getUserMedia = function (constraints, success, err) {
  476. // TODO(gp) what if we request a resolution not supported by the hardware?
  477. // TODO(gp) make the lq stream configurable; although this wouldn't work with native simulcast
  478. var lqConstraints = {
  479. audio: false,
  480. video: {
  481. mandatory: {
  482. maxWidth: 320,
  483. maxHeight: 180
  484. }
  485. }
  486. };
  487. if (config.enableSimulcast && !config.useNativeSimulcast) {
  488. // NOTE(gp) if we request the lq stream first webkitGetUserMedia fails randomly. Tested with Chrome 37.
  489. navigator.webkitGetUserMedia(constraints, function (hqStream) {
  490. // reset local maps.
  491. localMaps.msids = [];
  492. localMaps.msid2ssrc = {};
  493. // add hq trackid to local map
  494. localMaps.msids.push(hqStream.getVideoTracks()[0].id);
  495. navigator.webkitGetUserMedia(lqConstraints, function (lqStream) {
  496. // add lq trackid to local map
  497. localMaps.msids.push(lqStream.getVideoTracks()[0].id);
  498. hqStream.addTrack(lqStream.getVideoTracks()[0]);
  499. success(hqStream);
  500. }, err);
  501. }, err);
  502. } else {
  503. // There's nothing special to do for native simulcast, so just do a normal GUM.
  504. navigator.webkitGetUserMedia(constraints, function (hqStream) {
  505. // reset local maps.
  506. localMaps.msids = [];
  507. localMaps.msid2ssrc = {};
  508. // add hq stream to local map
  509. localMaps.msids.push(hqStream.getVideoTracks()[0].id);
  510. success(hqStream);
  511. }, err);
  512. }
  513. };
  514. Simulcast.prototype.getRemoteVideoStreamIdBySSRC = function (primarySSRC) {
  515. return remoteMaps.ssrc2Msid[primarySSRC];
  516. };
  517. Simulcast.prototype.parseMedia = function (desc, mediatypes) {
  518. var lines = desc.sdp.split('\r\n');
  519. return this._parseMedia(lines, mediatypes);
  520. };
  521. }());