diff --git a/modules/xmpp/JingleSession.js b/modules/xmpp/JingleSession.js index 4179dc13e..5928676ba 100644 --- a/modules/xmpp/JingleSession.js +++ b/modules/xmpp/JingleSession.js @@ -7,7 +7,7 @@ var async = require("async"); var transform = require("sdp-transform"); var XMPPEvents = require("../../service/xmpp/XMPPEvents"); var RTCBrowserType = require("../RTC/RTCBrowserType"); -var VideoSSRCHack = require("./VideoSSRCHack"); +var SSRCReplacement = require("./LocalSSRCReplacement"); // Jingle stuff function JingleSession(me, sid, connection, service, eventEmitter) { @@ -254,7 +254,7 @@ JingleSession.prototype.accept = function () { //console.log('setLocalDescription success'); self.setLocalDescription(); - VideoSSRCHack.processSessionInit(accept); + SSRCReplacement.processSessionInit(accept); self.connection.sendIQ(accept, function () { @@ -348,7 +348,7 @@ JingleSession.prototype.sendIceCandidate = function (candidate) { self.initiator == self.me ? 'initiator' : 'responder', ssrc); - VideoSSRCHack.processSessionInit(init); + SSRCReplacement.processSessionInit(init); self.connection.sendIQ(init, function () { @@ -467,7 +467,7 @@ JingleSession.prototype.createdOffer = function (sdp) { this.initiator == this.me ? 'initiator' : 'responder', this.localStreamsSSRC); - VideoSSRCHack.processSessionInit(init); + SSRCReplacement.processSessionInit(init); self.connection.sendIQ(init, function () { @@ -733,7 +733,7 @@ JingleSession.prototype.createdAnswer = function (sdp, provisional) { self.initiator == self.me ? 'initiator' : 'responder', ssrcs); - VideoSSRCHack.processSessionInit(accept); + SSRCReplacement.processSessionInit(accept); self.connection.sendIQ(accept, function () { @@ -1135,7 +1135,7 @@ JingleSession.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) { // Let 'source-remove' IQ through the hack and see if we're allowed to send // it in the current form if (removed) - remove = VideoSSRCHack.processSourceRemove(remove); + remove = SSRCReplacement.processSourceRemove(remove); if (removed && remove) { console.info("Sending source-remove", remove); @@ -1166,7 +1166,7 @@ JingleSession.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) { // Let 'source-add' IQ through the hack and see if we're allowed to send // it in the current form if (added) - add = VideoSSRCHack.processSourceAdd(add); + add = SSRCReplacement.processSourceAdd(add); if (added && add) { console.info("Sending source-add", add); diff --git a/modules/xmpp/VideoSSRCHack.js b/modules/xmpp/LocalSSRCReplacement.js similarity index 69% rename from modules/xmpp/VideoSSRCHack.js rename to modules/xmpp/LocalSSRCReplacement.js index 09ec10a06..0ee0459b2 100644 --- a/modules/xmpp/VideoSSRCHack.js +++ b/modules/xmpp/LocalSSRCReplacement.js @@ -1,8 +1,21 @@ /* global $ */ /* - The purpose of this hack is to re-use SSRC of first video stream ever created - for any video streams created later on. In order to do that this hack: + Here we do modifications of local video SSRCs. There are 2 situations we have + to handle: + + 1. We generate SSRC for local recvonly video stream. This is the case when we + have no local camera and it is not generated automatically, but SSRC=1 is + used implicitly. If that happens RTCP packets will be dropped by the JVB + and we won't be able to request video key frames correctly. + + 2. A hack to re-use SSRC of the first video stream for any new stream created + in future. It turned out that Chrome may keep on using the SSRC of removed + video stream in RTCP even though a new one has been created. So we just + want to avoid that by re-using it. Jingle 'source-remove'/'source-add' + notifications are blocked once first video SSRC is advertised to the focus. + + What this hack does: 1. Stores the SSRC of the first video stream created by a) scanning Jingle session-accept/session-invite for existing video SSRC @@ -33,6 +46,18 @@ var isEnabled = !RTCBrowserType.isFirefox(); */ var localVideoSSRC; +/** + * SSRC used for recvonly video stream when we have no local camera. + * This is in order to tell Chrome what SSRC should be used in RTCP requests + * instead of 1. + */ +var localRecvOnlySSRC; + +/** + * cname for localRecvOnlySSRC + */ +var localRecvOnlyCName; + /** * Method removes element which describes localVideoSSRC * from given Jingle IQ. @@ -79,21 +104,41 @@ var storeLocalVideoSSRC = function (jingleIq) { $(jingleIq.tree()) .find('>jingle>content[name="video"]>description>source'); - console.info('Video desc: ', videoSSRCs); - if (!videoSSRCs.length) - return; - - var ssrc = videoSSRCs.attr('ssrc'); - if (ssrc) { - localVideoSSRC = ssrc; - console.info( - 'Stored local video SSRC for future re-use: ' + localVideoSSRC); - } else { - console.error('No "ssrc" attribute present in element'); - } + videoSSRCs.each(function (idx, ssrcElem) { + if (localVideoSSRC) + return; + // We consider SSRC real only if it has msid attribute + // recvonly streams in FF do not have it as well as local SSRCs + // we generate for recvonly streams in Chrome + var ssrSel = $(ssrcElem); + var msid = ssrSel.find('>parameter[name="msid"]'); + if (msid.length) { + var ssrcVal = ssrSel.attr('ssrc'); + if (ssrcVal) { + localVideoSSRC = ssrcVal; + console.info('Stored local video SSRC' + + ' for future re-use: ' + localVideoSSRC); + } + } + }); }; -var LocalVideoSSRCHack = { +/** + * Generates new SSRC for local video recvonly stream. + * FIXME what about eventual SSRC collision ? + */ +function generateRecvonlySSRC() { + // + localRecvOnlySSRC = + Math.random().toString(10).substring(2, 11); + localRecvOnlyCName = + Math.random().toString(36).substring(2); + console.info( + "Generated local recvonly SSRC: " + localRecvOnlySSRC + + ", cname: " + localRecvOnlyCName); +} + +var LocalSSRCReplacement = { /** * Method must be called before 'session-initiate' or 'session-invite' is * sent. Scans the IQ for local video SSRC and stores it if detected. @@ -144,6 +189,25 @@ var LocalVideoSSRCHack = { new RegExp('a=ssrc:' + newSSRC, 'g'), 'a=ssrc:' + localVideoSSRC); } + } else { + // Make sure we have any SSRC for recvonly video stream + var sdp = new SDP(localDescription.sdp); + + if (sdp.media[1] && sdp.media[1].indexOf('a=ssrc:') === -1 && + sdp.media[1].indexOf('a=recvonly') !== -1) { + + if (!localRecvOnlySSRC) { + generateRecvonlySSRC(); + } + + console.info('No SSRC in video recvonly stream' + + ' - adding SSRC: ' + localRecvOnlySSRC); + + sdp.media[1] += 'a=ssrc:' + localRecvOnlySSRC + + ' cname:' + localRecvOnlyCName + '\r\n'; + + localDescription.sdp = sdp.session + sdp.media.join(''); + } } return localDescription; }, @@ -196,4 +260,4 @@ var LocalVideoSSRCHack = { } }; -module.exports = LocalVideoSSRCHack; +module.exports = LocalSSRCReplacement; diff --git a/modules/xmpp/TraceablePeerConnection.js b/modules/xmpp/TraceablePeerConnection.js index 1bc68f122..ee748fd39 100644 --- a/modules/xmpp/TraceablePeerConnection.js +++ b/modules/xmpp/TraceablePeerConnection.js @@ -1,7 +1,7 @@ var RTC = require('../RTC/RTC'); var RTCBrowserType = require("../RTC/RTCBrowserType.js"); var XMPPEvents = require("../../service/xmpp/XMPPEvents"); -var VideoSSRCHack = require("./VideoSSRCHack"); +var SSRCReplacement = require("./LocalSSRCReplacement"); function TraceablePeerConnection(ice_config, constraints, session) { var self = this; @@ -213,7 +213,7 @@ if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) { function() { var desc = this.peerconnection.localDescription; - desc = VideoSSRCHack.mungeLocalVideoSSRC(desc); + desc = SSRCReplacement.mungeLocalVideoSSRC(desc); this.trace('getLocalDescription::preTransform', dumpSDP(desc)); @@ -372,7 +372,7 @@ TraceablePeerConnection.prototype.createOffer self.trace('createOfferOnSuccess::postTransform (Plan B)', dumpSDP(offer)); } - offer = VideoSSRCHack.mungeLocalVideoSSRC(offer); + offer = SSRCReplacement.mungeLocalVideoSSRC(offer); if (config.enableSimulcast && self.simulcast.isSupported()) { offer = self.simulcast.mungeLocalDescription(offer); @@ -402,7 +402,7 @@ TraceablePeerConnection.prototype.createAnswer } // munge local video SSRC - answer = VideoSSRCHack.mungeLocalVideoSSRC(answer); + answer = SSRCReplacement.mungeLocalVideoSSRC(answer); if (config.enableSimulcast && self.simulcast.isSupported()) { answer = self.simulcast.mungeLocalDescription(answer);