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

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