123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 |
- var SimulcastLogger = require("./SimulcastLogger");
- var SimulcastUtils = require("./SimulcastUtils");
-
- function SimulcastSender() {
- this.simulcastUtils = new SimulcastUtils();
- this.logger = new SimulcastLogger('SimulcastSender', 1);
- }
-
- SimulcastSender.prototype.displayedLocalVideoStream = null;
-
- SimulcastSender.prototype._generateGuid = (function () {
- function s4() {
- return Math.floor((1 + Math.random()) * 0x10000)
- .toString(16)
- .substring(1);
- }
-
- return function () {
- return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
- s4() + '-' + s4() + s4() + s4();
- };
- }());
-
- // Returns a random integer between min (included) and max (excluded)
- // Using Math.round() gives a non-uniform distribution!
- SimulcastSender.prototype._generateRandomSSRC = function () {
- var min = 0, max = 0xffffffff;
- return Math.floor(Math.random() * (max - min)) + min;
- };
-
- SimulcastSender.prototype.getLocalVideoStream = function () {
- return (this.displayedLocalVideoStream != null)
- ? this.displayedLocalVideoStream
- // in case we have no simulcast at all, i.e. we didn't perform the GUM
- : RTC.localVideo.getOriginalStream();
- };
-
- function NativeSimulcastSender() {
- SimulcastSender.call(this); // call the super constructor.
- }
-
- NativeSimulcastSender.prototype = Object.create(SimulcastSender.prototype);
-
- NativeSimulcastSender.prototype._localExplosionMap = {};
- NativeSimulcastSender.prototype._isUsingScreenStream = false;
- NativeSimulcastSender.prototype._localVideoSourceCache = '';
-
- NativeSimulcastSender.prototype.reset = function () {
- this._localExplosionMap = {};
- this._isUsingScreenStream = desktopsharing.isUsingScreenStream();
- };
-
- NativeSimulcastSender.prototype._cacheLocalVideoSources = function (lines) {
- this._localVideoSourceCache = this.simulcastUtils._getVideoSources(lines);
- };
-
- NativeSimulcastSender.prototype._restoreLocalVideoSources = function (lines) {
- this.simulcastUtils._replaceVideoSources(lines, this._localVideoSourceCache);
- };
-
- NativeSimulcastSender.prototype._appendSimulcastGroup = function (lines) {
- var videoSources, ssrcGroup, simSSRC, numOfSubs = 2, i, sb, msid;
-
- this.logger.info('Appending simulcast group...');
-
- // Get the primary SSRC information.
- videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0];
-
- // Start building the SIM SSRC group.
- ssrcGroup = ['a=ssrc-group:SIM'];
-
- // The video source buffer.
- sb = [];
-
- // Create the simulcast sub-streams.
- for (i = 0; i < numOfSubs; i++) {
- // TODO(gp) prevent SSRC collision.
- simSSRC = this._generateRandomSSRC();
- ssrcGroup.push(simSSRC);
-
- sb.splice.apply(sb, [sb.length, 0].concat(
- [["a=ssrc:", simSSRC, " cname:", videoSources.base.cname].join(''),
- ["a=ssrc:", simSSRC, " msid:", videoSources.base.msid].join('')]
- ));
-
- this.logger.info(['Generated substream ', i, ' with SSRC ', simSSRC, '.'].join(''));
-
- }
-
- // Add the group sim layers.
- sb.splice(0, 0, ssrcGroup.join(' '))
-
- this.simulcastUtils._replaceVideoSources(lines, sb);
- };
-
- // Does the actual patching.
- NativeSimulcastSender.prototype._ensureSimulcastGroup = function (lines) {
-
- this.logger.info('Ensuring simulcast group...');
-
- if (this.simulcastUtils._indexOfArray('a=ssrc-group:SIM', lines) === this.simulcastUtils._emptyCompoundIndex) {
- this._appendSimulcastGroup(lines);
- this._cacheLocalVideoSources(lines);
- } else {
- // verify that the ssrcs participating in the SIM group are present
- // in the SDP (needed for presence).
- this._restoreLocalVideoSources(lines);
- }
- };
-
- /**
- * Produces a single stream with multiple tracks for local video sources.
- *
- * @param lines
- * @private
- */
- NativeSimulcastSender.prototype._explodeSimulcastSenderSources = function (lines) {
- var sb, msid, sid, tid, videoSources, self;
-
- this.logger.info('Exploding local video sources...');
-
- videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0];
-
- self = this;
- if (videoSources.groups && videoSources.groups.length !== 0) {
- videoSources.groups.forEach(function (group) {
- if (group.semantics === 'SIM') {
- group.ssrcs.forEach(function (ssrc) {
-
- // Get the msid for this ssrc..
- if (self._localExplosionMap[ssrc]) {
- // .. either from the explosion map..
- msid = self._localExplosionMap[ssrc];
- } else {
- // .. or generate a new one (msid).
- sid = videoSources.sources[ssrc].msid
- .substring(0, videoSources.sources[ssrc].msid.indexOf(' '));
-
- tid = self._generateGuid();
- msid = [sid, tid].join(' ');
- self._localExplosionMap[ssrc] = msid;
- }
-
- // Assign it to the source object.
- videoSources.sources[ssrc].msid = msid;
-
- // TODO(gp) Change the msid of associated sources.
- });
- }
- });
- }
-
- sb = this.simulcastUtils._compileVideoSources(videoSources);
-
- this.simulcastUtils._replaceVideoSources(lines, sb);
- };
-
- /**
- * GUM for simulcast.
- *
- * @param constraints
- * @param success
- * @param err
- */
- NativeSimulcastSender.prototype.getUserMedia = function (constraints, success, err) {
-
- // There's nothing special to do for native simulcast, so just do a normal GUM.
- navigator.webkitGetUserMedia(constraints, function (hqStream) {
- success(hqStream);
- }, err);
- };
-
- /**
- * Prepares the local description for public usage (i.e. to be signaled
- * through Jingle to the focus).
- *
- * @param desc
- * @returns {RTCSessionDescription}
- */
- NativeSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) {
- var sb;
-
- if (!this.simulcastUtils.isValidDescription(desc) || this._isUsingScreenStream) {
- return desc;
- }
-
-
- sb = desc.sdp.split('\r\n');
-
- this._explodeSimulcastSenderSources(sb);
-
- desc = new RTCSessionDescription({
- type: desc.type,
- sdp: sb.join('\r\n')
- });
-
- this.logger.fine(['Exploded local video sources', desc.sdp].join(' '));
-
- return desc;
- };
-
- /**
- * Ensures that the simulcast group is present in the answer, _if_ native
- * simulcast is enabled,
- *
- * @param desc
- * @returns {*}
- */
- NativeSimulcastSender.prototype.transformAnswer = function (desc) {
-
- if (!this.simulcastUtils.isValidDescription(desc) || this._isUsingScreenStream) {
- return desc;
- }
-
- var sb = desc.sdp.split('\r\n');
-
- // Even if we have enabled native simulcasting previously
- // (with a call to SLD with an appropriate SDP, for example),
- // createAnswer seems to consistently generate incomplete SDP
- // with missing SSRCS.
- //
- // So, subsequent calls to SLD will have missing SSRCS and presence
- // won't have the complete list of SRCs.
- this._ensureSimulcastGroup(sb);
-
- desc = new RTCSessionDescription({
- type: desc.type,
- sdp: sb.join('\r\n')
- });
-
- this.logger.fine(['Transformed answer', desc.sdp].join(' '));
-
- return desc;
- };
-
-
- /**
- *
- *
- * @param desc
- * @returns {*}
- */
- NativeSimulcastSender.prototype.transformLocalDescription = function (desc) {
- return desc;
- };
-
- NativeSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) {
- // Nothing to do here, native simulcast does that auto-magically.
- };
-
- NativeSimulcastSender.prototype.constructor = NativeSimulcastSender;
-
- function SimpleSimulcastSender() {
- SimulcastSender.call(this);
- }
-
- SimpleSimulcastSender.prototype = Object.create(SimulcastSender.prototype);
-
- SimpleSimulcastSender.prototype.localStream = null;
- SimpleSimulcastSender.prototype._localMaps = {
- msids: [],
- msid2ssrc: {}
- };
-
- /**
- * Groups local video sources together in the ssrc-group:SIM group.
- *
- * @param lines
- * @private
- */
- SimpleSimulcastSender.prototype._groupLocalVideoSources = function (lines) {
- var sb, videoSources, ssrcs = [], ssrc;
-
- this.logger.info('Grouping local video sources...');
-
- videoSources = this.simulcastUtils.parseMedia(lines, ['video'])[0];
-
- for (ssrc in videoSources.sources) {
- // jitsi-meet destroys/creates streams at various places causing
- // the original local stream ids to change. The only thing that
- // remains unchanged is the trackid.
- this._localMaps.msid2ssrc[videoSources.sources[ssrc].msid.split(' ')[1]] = ssrc;
- }
-
- var self = this;
- // TODO(gp) add only "free" sources.
- this._localMaps.msids.forEach(function (msid) {
- ssrcs.push(self._localMaps.msid2ssrc[msid]);
- });
-
- if (!videoSources.groups) {
- videoSources.groups = [];
- }
-
- videoSources.groups.push({
- 'semantics': 'SIM',
- 'ssrcs': ssrcs
- });
-
- sb = this.simulcastUtils._compileVideoSources(videoSources);
-
- this.simulcastUtils._replaceVideoSources(lines, sb);
- };
-
- /**
- * GUM for simulcast.
- *
- * @param constraints
- * @param success
- * @param err
- */
- SimpleSimulcastSender.prototype.getUserMedia = function (constraints, success, err) {
-
- // TODO(gp) what if we request a resolution not supported by the hardware?
- // TODO(gp) make the lq stream configurable; although this wouldn't work with native simulcast
- var lqConstraints = {
- audio: false,
- video: {
- mandatory: {
- maxWidth: 320,
- maxHeight: 180,
- maxFrameRate: 15
- }
- }
- };
-
- this.logger.info('HQ constraints: ', constraints);
- this.logger.info('LQ constraints: ', lqConstraints);
-
-
- // NOTE(gp) if we request the lq stream first webkitGetUserMedia
- // fails randomly. Tested with Chrome 37. As fippo suggested, the
- // reason appears to be that Chrome only acquires the cam once and
- // then downscales the picture (https://code.google.com/p/chromium/issues/detail?id=346616#c11)
-
- var self = this;
- navigator.webkitGetUserMedia(constraints, function (hqStream) {
-
- self.localStream = hqStream;
-
- // reset local maps.
- self._localMaps.msids = [];
- self._localMaps.msid2ssrc = {};
-
- // add hq trackid to local map
- self._localMaps.msids.push(hqStream.getVideoTracks()[0].id);
-
- navigator.webkitGetUserMedia(lqConstraints, function (lqStream) {
-
- self.displayedLocalVideoStream = lqStream;
-
- // NOTE(gp) The specification says Array.forEach() will visit
- // the array elements in numeric order, and that it doesn't
- // visit elements that don't exist.
-
- // add lq trackid to local map
- self._localMaps.msids.splice(0, 0, lqStream.getVideoTracks()[0].id);
-
- self.localStream.addTrack(lqStream.getVideoTracks()[0]);
- success(self.localStream);
- }, err);
- }, err);
- };
-
- /**
- * Prepares the local description for public usage (i.e. to be signaled
- * through Jingle to the focus).
- *
- * @param desc
- * @returns {RTCSessionDescription}
- */
- SimpleSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) {
- var sb;
-
- if (!this.simulcastUtils.isValidDescription(desc)) {
- return desc;
- }
-
- sb = desc.sdp.split('\r\n');
-
- this._groupLocalVideoSources(sb);
-
- desc = new RTCSessionDescription({
- type: desc.type,
- sdp: sb.join('\r\n')
- });
-
- this.logger.fine('Grouped local video sources');
- this.logger.fine(desc.sdp);
-
- return desc;
- };
-
- /**
- * Ensures that the simulcast group is present in the answer, _if_ native
- * simulcast is enabled,
- *
- * @param desc
- * @returns {*}
- */
- SimpleSimulcastSender.prototype.transformAnswer = function (desc) {
- return desc;
- };
-
-
- /**
- *
- *
- * @param desc
- * @returns {*}
- */
- SimpleSimulcastSender.prototype.transformLocalDescription = function (desc) {
-
- var sb = desc.sdp.split('\r\n');
-
- this.simulcastUtils._removeSimulcastGroup(sb);
-
- desc = new RTCSessionDescription({
- type: desc.type,
- sdp: sb.join('\r\n')
- });
-
- this.logger.fine('Transformed local description');
- this.logger.fine(desc.sdp);
-
- return desc;
- };
-
- SimpleSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) {
- var trackid;
-
- var self = this;
- this.logger.log(['Requested to', enabled ? 'enable' : 'disable', ssrc].join(' '));
- if (Object.keys(this._localMaps.msid2ssrc).some(function (tid) {
- // Search for the track id that corresponds to the ssrc
- if (self._localMaps.msid2ssrc[tid] == ssrc) {
- trackid = tid;
- return true;
- }
- }) && self.localStream.getVideoTracks().some(function (track) {
- // Start/stop the track that corresponds to the track id
- if (track.id === trackid) {
- track.enabled = enabled;
- return true;
- }
- })) {
- this.logger.log([trackid, enabled ? 'enabled' : 'disabled'].join(' '));
- $(document).trigger(enabled
- ? 'simulcastlayerstarted'
- : 'simulcastlayerstopped');
- } else {
- this.logger.error("I don't have a local stream with SSRC " + ssrc);
- }
- };
-
- SimpleSimulcastSender.prototype.constructor = SimpleSimulcastSender;
-
- function NoSimulcastSender() {
- SimulcastSender.call(this);
- }
-
- NoSimulcastSender.prototype = Object.create(SimulcastSender.prototype);
-
- /**
- * GUM for simulcast.
- *
- * @param constraints
- * @param success
- * @param err
- */
- NoSimulcastSender.prototype.getUserMedia = function (constraints, success, err) {
- navigator.webkitGetUserMedia(constraints, function (hqStream) {
- success(hqStream);
- }, err);
- };
-
- /**
- * Prepares the local description for public usage (i.e. to be signaled
- * through Jingle to the focus).
- *
- * @param desc
- * @returns {RTCSessionDescription}
- */
- NoSimulcastSender.prototype.reverseTransformLocalDescription = function (desc) {
- return desc;
- };
-
- /**
- * Ensures that the simulcast group is present in the answer, _if_ native
- * simulcast is enabled,
- *
- * @param desc
- * @returns {*}
- */
- NoSimulcastSender.prototype.transformAnswer = function (desc) {
- return desc;
- };
-
-
- /**
- *
- *
- * @param desc
- * @returns {*}
- */
- NoSimulcastSender.prototype.transformLocalDescription = function (desc) {
- return desc;
- };
-
- NoSimulcastSender.prototype._setLocalVideoStreamEnabled = function (ssrc, enabled) {
-
- };
-
- NoSimulcastSender.prototype.constructor = NoSimulcastSender;
-
- module.exports = {
- "native": NativeSimulcastSender,
- "no": NoSimulcastSender
- }
|