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.

SimulcastSender.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. var SimulcastLogger = require("./SimulcastLogger");
  2. var SimulcastUtils = require("./SimulcastUtils");
  3. function SimulcastSender() {
  4. this.simulcastUtils = new SimulcastUtils();
  5. this.logger = new SimulcastLogger('SimulcastSender', 1);
  6. }
  7. SimulcastSender.prototype.displayedLocalVideoStream = null;
  8. SimulcastSender.prototype._generateGuid = (function () {
  9. function s4() {
  10. return Math.floor((1 + Math.random()) * 0x10000)
  11. .toString(16)
  12. .substring(1);
  13. }
  14. return function () {
  15. return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
  16. s4() + '-' + s4() + s4() + s4();
  17. };
  18. }());
  19. // Returns a random integer between min (included) and max (excluded)
  20. // Using Math.round() gives a non-uniform distribution!
  21. SimulcastSender.prototype._generateRandomSSRC = function () {
  22. var min = 0, max = 0xffffffff;
  23. return Math.floor(Math.random() * (max - min)) + min;
  24. };
  25. SimulcastSender.prototype.getLocalVideoStream = function () {
  26. return (this.displayedLocalVideoStream != null)
  27. ? this.displayedLocalVideoStream
  28. // in case we have no simulcast at all, i.e. we didn't perform the GUM
  29. : APP.RTC.localVideo.getOriginalStream();
  30. };
  31. function NativeSimulcastSender() {
  32. SimulcastSender.call(this); // call the super constructor.
  33. }
  34. NativeSimulcastSender.prototype = Object.create(SimulcastSender.prototype);
  35. NativeSimulcastSender.prototype._localExplosionMap = {};
  36. NativeSimulcastSender.prototype._isUsingScreenStream = false;
  37. NativeSimulcastSender.prototype._localVideoSourceCache = '';
  38. NativeSimulcastSender.prototype.reset = function () {
  39. this._localExplosionMap = {};
  40. this._isUsingScreenStream = APP.desktopsharing.isUsingScreenStream();
  41. };
  42. NativeSimulcastSender.prototype._cacheLocalVideoSources = function (lines) {
  43. this._localVideoSourceCache = this.simulcastUtils._getVideoSources(lines);
  44. };
  45. NativeSimulcastSender.prototype._restoreLocalVideoSources = function (lines) {
  46. this.simulcastUtils._replaceVideoSources(lines, this._localVideoSourceCache);
  47. };
  48. NativeSimulcastSender.prototype._appendSimulcastGroup = function (lines) {
  49. var videoSources, ssrcGroup, simSSRC, numOfSubs = 2, i, sb, msid;
  50. this.logger.info('Appending simulcast group...');
  51. // Get the primary SSRC information.
  52. videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0];
  53. // Start building the SIM SSRC group.
  54. ssrcGroup = ['a=ssrc-group:SIM'];
  55. // The video source buffer.
  56. sb = [];
  57. // Create the simulcast sub-streams.
  58. for (i = 0; i < numOfSubs; i++) {
  59. // TODO(gp) prevent SSRC collision.
  60. simSSRC = this._generateRandomSSRC();
  61. ssrcGroup.push(simSSRC);
  62. if (videoSources.base) {
  63. sb.splice.apply(sb, [sb.length, 0].concat(
  64. [["a=ssrc:", simSSRC, " cname:", videoSources.base.cname].join(''),
  65. ["a=ssrc:", simSSRC, " msid:", videoSources.base.msid].join('')]
  66. ));
  67. }
  68. this.logger.info(['Generated substream ', i, ' with SSRC ', simSSRC, '.'].join(''));
  69. }
  70. // Add the group sim layers.
  71. sb.splice(0, 0, ssrcGroup.join(' '))
  72. this.simulcastUtils._replaceVideoSources(lines, sb);
  73. };
  74. // Does the actual patching.
  75. NativeSimulcastSender.prototype._ensureSimulcastGroup = function (lines) {
  76. this.logger.info('Ensuring simulcast group...');
  77. if (this.simulcastUtils._indexOfArray('a=ssrc-group:SIM', lines) === this.simulcastUtils._emptyCompoundIndex) {
  78. this._appendSimulcastGroup(lines);
  79. this._cacheLocalVideoSources(lines);
  80. } else {
  81. // verify that the ssrcs participating in the SIM group are present
  82. // in the SDP (needed for presence).
  83. this._restoreLocalVideoSources(lines);
  84. }
  85. };
  86. /**
  87. * Produces a single stream with multiple tracks for local video sources.
  88. *
  89. * @param lines
  90. * @private
  91. */
  92. NativeSimulcastSender.prototype._explodeSimulcastSenderSources = function (lines) {
  93. var sb, msid, sid, tid, videoSources, self;
  94. this.logger.info('Exploding local video sources...');
  95. videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0];
  96. self = this;
  97. if (videoSources.groups && videoSources.groups.length !== 0) {
  98. videoSources.groups.forEach(function (group) {
  99. if (group.semantics === 'SIM') {
  100. group.ssrcs.forEach(function (ssrc) {
  101. // Get the msid for this ssrc..
  102. if (self._localExplosionMap[ssrc]) {
  103. // .. either from the explosion map..
  104. msid = self._localExplosionMap[ssrc];
  105. } else {
  106. // .. or generate a new one (msid).
  107. sid = videoSources.sources[ssrc].msid
  108. .substring(0, videoSources.sources[ssrc].msid.indexOf(' '));
  109. tid = self._generateGuid();
  110. msid = [sid, tid].join(' ');
  111. self._localExplosionMap[ssrc] = msid;
  112. }
  113. // Assign it to the source object.
  114. videoSources.sources[ssrc].msid = msid;
  115. // TODO(gp) Change the msid of associated sources.
  116. });
  117. }
  118. });
  119. }
  120. sb = this.simulcastUtils._compileVideoSources(videoSources);
  121. this.simulcastUtils._replaceVideoSources(lines, sb);
  122. };
  123. /**
  124. * GUM for simulcast.
  125. *
  126. * @param constraints
  127. * @param success
  128. * @param err
  129. */
  130. NativeSimulcastSender.prototype.getUserMedia = function (constraints, success, err) {
  131. // There's nothing special to do for native simulcast, so just do a normal GUM.
  132. navigator.webkitGetUserMedia(constraints, function (hqStream) {
  133. success(hqStream);
  134. }, err);
  135. };
  136. /**
  137. * Prepares the local description for public usage (i.e. to be signaled
  138. * through Jingle to the focus).
  139. *
  140. * @param desc
  141. * @returns {RTCSessionDescription}
  142. */
  143. NativeSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) {
  144. var sb;
  145. if (!this.simulcastUtils.isValidDescription(desc) || this._isUsingScreenStream) {
  146. return desc;
  147. }
  148. sb = desc.sdp.split('\r\n');
  149. this._explodeSimulcastSenderSources(sb);
  150. desc = new RTCSessionDescription({
  151. type: desc.type,
  152. sdp: sb.join('\r\n')
  153. });
  154. this.logger.fine(['Exploded local video sources', desc.sdp].join(' '));
  155. return desc;
  156. };
  157. /**
  158. * Ensures that the simulcast group is present in the answer, _if_ native
  159. * simulcast is enabled,
  160. *
  161. * @param desc
  162. * @returns {*}
  163. */
  164. NativeSimulcastSender.prototype.transformAnswer = function (desc) {
  165. if (!this.simulcastUtils.isValidDescription(desc) || this._isUsingScreenStream) {
  166. return desc;
  167. }
  168. var sb = desc.sdp.split('\r\n');
  169. // Even if we have enabled native simulcasting previously
  170. // (with a call to SLD with an appropriate SDP, for example),
  171. // createAnswer seems to consistently generate incomplete SDP
  172. // with missing SSRCS.
  173. //
  174. // So, subsequent calls to SLD will have missing SSRCS and presence
  175. // won't have the complete list of SRCs.
  176. this._ensureSimulcastGroup(sb);
  177. desc = new RTCSessionDescription({
  178. type: desc.type,
  179. sdp: sb.join('\r\n')
  180. });
  181. this.logger.fine(['Transformed answer', desc.sdp].join(' '));
  182. return desc;
  183. };
  184. /**
  185. *
  186. *
  187. * @param desc
  188. * @returns {*}
  189. */
  190. NativeSimulcastSender.prototype.transformLocalDescription = function (desc) {
  191. return desc;
  192. };
  193. NativeSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) {
  194. // Nothing to do here, native simulcast does that auto-magically.
  195. };
  196. NativeSimulcastSender.prototype.constructor = NativeSimulcastSender;
  197. function SimpleSimulcastSender() {
  198. SimulcastSender.call(this);
  199. }
  200. SimpleSimulcastSender.prototype = Object.create(SimulcastSender.prototype);
  201. SimpleSimulcastSender.prototype.localStream = null;
  202. SimpleSimulcastSender.prototype._localMaps = {
  203. msids: [],
  204. msid2ssrc: {}
  205. };
  206. /**
  207. * Groups local video sources together in the ssrc-group:SIM group.
  208. *
  209. * @param lines
  210. * @private
  211. */
  212. SimpleSimulcastSender.prototype._groupLocalVideoSources = function (lines) {
  213. var sb, videoSources, ssrcs = [], ssrc;
  214. this.logger.info('Grouping local video sources...');
  215. videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0];
  216. for (ssrc in videoSources.sources) {
  217. // jitsi-meet destroys/creates streams at various places causing
  218. // the original local stream ids to change. The only thing that
  219. // remains unchanged is the trackid.
  220. this._localMaps.msid2ssrc[videoSources.sources[ssrc].msid.split(' ')[1]] = ssrc;
  221. }
  222. var self = this;
  223. // TODO(gp) add only "free" sources.
  224. this._localMaps.msids.forEach(function (msid) {
  225. ssrcs.push(self._localMaps.msid2ssrc[msid]);
  226. });
  227. if (!videoSources.groups) {
  228. videoSources.groups = [];
  229. }
  230. videoSources.groups.push({
  231. 'semantics': 'SIM',
  232. 'ssrcs': ssrcs
  233. });
  234. sb = this.simulcastUtils._compileVideoSources(videoSources);
  235. this.simulcastUtils._replaceVideoSources(lines, sb);
  236. };
  237. /**
  238. * GUM for simulcast.
  239. *
  240. * @param constraints
  241. * @param success
  242. * @param err
  243. */
  244. SimpleSimulcastSender.prototype.getUserMedia = function (constraints, success, err) {
  245. // TODO(gp) what if we request a resolution not supported by the hardware?
  246. // TODO(gp) make the lq stream configurable; although this wouldn't work with native simulcast
  247. var lqConstraints = {
  248. audio: false,
  249. video: {
  250. mandatory: {
  251. maxWidth: 320,
  252. maxHeight: 180,
  253. maxFrameRate: 15
  254. }
  255. }
  256. };
  257. this.logger.info('HQ constraints: ', constraints);
  258. this.logger.info('LQ constraints: ', lqConstraints);
  259. // NOTE(gp) if we request the lq stream first webkitGetUserMedia
  260. // fails randomly. Tested with Chrome 37. As fippo suggested, the
  261. // reason appears to be that Chrome only acquires the cam once and
  262. // then downscales the picture (https://code.google.com/p/chromium/issues/detail?id=346616#c11)
  263. var self = this;
  264. navigator.webkitGetUserMedia(constraints, function (hqStream) {
  265. self.localStream = hqStream;
  266. // reset local maps.
  267. self._localMaps.msids = [];
  268. self._localMaps.msid2ssrc = {};
  269. // add hq trackid to local map
  270. self._localMaps.msids.push(hqStream.getVideoTracks()[0].id);
  271. navigator.webkitGetUserMedia(lqConstraints, function (lqStream) {
  272. self.displayedLocalVideoStream = lqStream;
  273. // NOTE(gp) The specification says Array.forEach() will visit
  274. // the array elements in numeric order, and that it doesn't
  275. // visit elements that don't exist.
  276. // add lq trackid to local map
  277. self._localMaps.msids.splice(0, 0, lqStream.getVideoTracks()[0].id);
  278. self.localStream.addTrack(lqStream.getVideoTracks()[0]);
  279. success(self.localStream);
  280. }, err);
  281. }, err);
  282. };
  283. /**
  284. * Prepares the local description for public usage (i.e. to be signaled
  285. * through Jingle to the focus).
  286. *
  287. * @param desc
  288. * @returns {RTCSessionDescription}
  289. */
  290. SimpleSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) {
  291. var sb;
  292. if (!this.simulcastUtils.isValidDescription(desc)) {
  293. return desc;
  294. }
  295. sb = desc.sdp.split('\r\n');
  296. this._groupLocalVideoSources(sb);
  297. desc = new RTCSessionDescription({
  298. type: desc.type,
  299. sdp: sb.join('\r\n')
  300. });
  301. this.logger.fine('Grouped local video sources');
  302. this.logger.fine(desc.sdp);
  303. return desc;
  304. };
  305. /**
  306. * Ensures that the simulcast group is present in the answer, _if_ native
  307. * simulcast is enabled,
  308. *
  309. * @param desc
  310. * @returns {*}
  311. */
  312. SimpleSimulcastSender.prototype.transformAnswer = function (desc) {
  313. return desc;
  314. };
  315. /**
  316. *
  317. *
  318. * @param desc
  319. * @returns {*}
  320. */
  321. SimpleSimulcastSender.prototype.transformLocalDescription = function (desc) {
  322. var sb = desc.sdp.split('\r\n');
  323. this.simulcastUtils._removeSimulcastGroup(sb);
  324. desc = new RTCSessionDescription({
  325. type: desc.type,
  326. sdp: sb.join('\r\n')
  327. });
  328. this.logger.fine('Transformed local description');
  329. this.logger.fine(desc.sdp);
  330. return desc;
  331. };
  332. SimpleSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) {
  333. var trackid;
  334. var self = this;
  335. this.logger.log(['Requested to', enabled ? 'enable' : 'disable', ssrc].join(' '));
  336. if (Object.keys(this._localMaps.msid2ssrc).some(function (tid) {
  337. // Search for the track id that corresponds to the ssrc
  338. if (self._localMaps.msid2ssrc[tid] == ssrc) {
  339. trackid = tid;
  340. return true;
  341. }
  342. }) && self.localStream.getVideoTracks().some(function (track) {
  343. // Start/stop the track that corresponds to the track id
  344. if (track.id === trackid) {
  345. track.enabled = enabled;
  346. return true;
  347. }
  348. })) {
  349. this.logger.log([trackid, enabled ? 'enabled' : 'disabled'].join(' '));
  350. $(document).trigger(enabled
  351. ? 'simulcastlayerstarted'
  352. : 'simulcastlayerstopped');
  353. } else {
  354. this.logger.error("I don't have a local stream with SSRC " + ssrc);
  355. }
  356. };
  357. SimpleSimulcastSender.prototype.constructor = SimpleSimulcastSender;
  358. function NoSimulcastSender() {
  359. SimulcastSender.call(this);
  360. }
  361. NoSimulcastSender.prototype = Object.create(SimulcastSender.prototype);
  362. /**
  363. * GUM for simulcast.
  364. *
  365. * @param constraints
  366. * @param success
  367. * @param err
  368. */
  369. NoSimulcastSender.prototype.getUserMedia = function (constraints, success, err) {
  370. navigator.webkitGetUserMedia(constraints, function (hqStream) {
  371. success(hqStream);
  372. }, err);
  373. };
  374. /**
  375. * Prepares the local description for public usage (i.e. to be signaled
  376. * through Jingle to the focus).
  377. *
  378. * @param desc
  379. * @returns {RTCSessionDescription}
  380. */
  381. NoSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) {
  382. return desc;
  383. };
  384. /**
  385. * Ensures that the simulcast group is present in the answer, _if_ native
  386. * simulcast is enabled,
  387. *
  388. * @param desc
  389. * @returns {*}
  390. */
  391. NoSimulcastSender.prototype.transformAnswer = function (desc) {
  392. return desc;
  393. };
  394. /**
  395. *
  396. *
  397. * @param desc
  398. * @returns {*}
  399. */
  400. NoSimulcastSender.prototype.transformLocalDescription = function (desc) {
  401. return desc;
  402. };
  403. NoSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) {
  404. };
  405. NoSimulcastSender.prototype.constructor = NoSimulcastSender;
  406. module.exports = {
  407. "native": NativeSimulcastSender,
  408. "no": NoSimulcastSender
  409. }