diff --git a/app.js b/app.js index 93ec61035..29d00e983 100644 --- a/app.js +++ b/app.js @@ -562,7 +562,7 @@ $(document).bind('callactive.jingle', function (event, videoelem, sid) { // Update the large video to the last added video only if there's no // current active or focused speaker. - if (!focusedVideoSrc && !VideoLayout.getActiveSpeakerResourceJid()) + if (!focusedVideoSrc && !VideoLayout.getDominantSpeakerResourceJid()) VideoLayout.updateLargeVideo(videoelem.attr('src'), 1); VideoLayout.showFocusIndicator(); diff --git a/chat.js b/chat.js index b7255a346..051829dac 100644 --- a/chat.js +++ b/chat.js @@ -115,8 +115,7 @@ var Chat = (function (my) { + ''); $('#chatconversation').animate( { scrollTop: $('#chatconversation')[0].scrollHeight}, 1000); - - } + }; /** * Sets the subject to the UI @@ -135,8 +134,7 @@ var Chat = (function (my) { { $("#subject").css({display: "block"}); } - } - + }; /** * Opens / closes the chat area. @@ -159,6 +157,10 @@ var Chat = (function (my) { var horizontalIndent = videoPosition[0]; var verticalIndent = videoPosition[1]; + var thumbnailSize = VideoLayout.calculateThumbnailSize(videospaceWidth); + var thumbnailsWidth = thumbnailSize[0]; + var thumbnailsHeight = thumbnailSize[1]; + if (chatspace.is(":visible")) { videospace.animate({right: chatSize[0], width: videospaceWidth, @@ -166,6 +168,15 @@ var Chat = (function (my) { {queue: false, duration: 500}); + $('#remoteVideos').animate({height: thumbnailsHeight}, + {queue: false, + duration: 500}); + + $('#remoteVideos>span').animate({height: thumbnailsHeight, + width: thumbnailsWidth}, + {queue: false, + duration: 500}); + $('#largeVideoContainer').animate({ width: videospaceWidth, height: videospaceHeight}, {queue: false, @@ -187,6 +198,9 @@ var Chat = (function (my) { duration: 500}); } else { + // Undock the toolbar when the chat is shown. + Toolbar.dockToolbar(false); + videospace.animate({right: chatSize[0], width: videospaceWidth, height: videospaceHeight}, @@ -198,6 +212,15 @@ var Chat = (function (my) { } }); + $('#remoteVideos').animate({height: thumbnailsHeight}, + {queue: false, + duration: 500}); + + $('#remoteVideos>span').animate({height: thumbnailsHeight, + width: thumbnailsWidth}, + {queue: false, + duration: 500}); + $('#largeVideoContainer').animate({ width: videospaceWidth, height: videospaceHeight}, {queue: false, @@ -290,7 +313,7 @@ var Chat = (function (my) { if (unreadMessages) { unreadMsgElement.innerHTML = unreadMessages.toString(); - Toolbar.showToolbar(); + Toolbar.dockToolbar(true); var chatButtonElement = document.getElementById('chatButton').parentNode; diff --git a/config.js b/config.js index 90bf9cf85..b68914179 100644 --- a/config.js +++ b/config.js @@ -13,6 +13,7 @@ var config = { chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension minChromeExtVersion: '0.1', // Required version of Chrome extension enableRtpStats: false, // Enables RTP stats processing - openSctp: true, //Toggle to enable/disable SCTP channels + openSctp: true, // Toggle to enable/disable SCTP channels +// channelLastN: -1, // The default value of the channel attribute last-n. enableRecording: false }; diff --git a/css/main.css b/css/main.css index eb9cc650d..e0a1726e9 100644 --- a/css/main.css +++ b/css/main.css @@ -131,15 +131,24 @@ html, body{ } #chatButton { - -webkit-transition: all .5s ease-in-out;; - -moz-transition: all .5s ease-in-out;; - transition: all .5s ease-in-out;; + -webkit-transition: all .5s ease-in-out; + -moz-transition: all .5s ease-in-out; + transition: all .5s ease-in-out; } - +/*#ffde00*/ #chatButton.active { - -webkit-text-shadow: 0 0 10px #ffffff; - -moz-text-shadow: 0 0 10px #ffffff; - text-shadow: 0 0 10px #ffffff; + -webkit-text-shadow: -1px 0 10px #00ccff, + 0 1px 10px #00ccff, + 1px 0 10px #00ccff, + 0 -1px 10px #00ccff; + -moz-text-shadow: 1px 0 10px #00ccff, + 0 1px 10px #00ccff, + 1px 0 10px #00ccff, + 0 -1px 10px #00ccff; + text-shadow: -1px 0 10px #00ccff, + 0 1px 10px #00ccff, + 1px 0 10px #00ccff, + 0 -1px 10px #00ccff; } a.button:hover { diff --git a/css/videolayout_default.css b/css/videolayout_default.css index e37cf6c62..f140071b8 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -94,7 +94,7 @@ height: 100%; } -.activespeaker { +.dominantspeaker { background: #000 !important; } diff --git a/data_channels.js b/data_channels.js index 7a1ae59e3..ac6ce4dd1 100644 --- a/data_channels.js +++ b/data_channels.js @@ -10,7 +10,7 @@ function onDataChannel(event) dataChannel.onopen = function () { - console.info("Data channel opened by the bridge !!!", dataChannel); + console.info("Data channel opened by the Videobridge!", dataChannel); // Code sample for sending string and/or binary data // Sends String message to the bridge @@ -26,18 +26,56 @@ function onDataChannel(event) dataChannel.onmessage = function (event) { - var msgData = event.data; - console.info("Got Data Channel Message:", msgData, dataChannel); + var data = event.data; + // JSON + var obj; - // Active speaker event - if (msgData.indexOf('activeSpeaker') === 0) + try { - // Endpoint ID from the bridge - var resourceJid = msgData.split(":")[1]; + obj = JSON.parse(data); + } + catch (e) + { + console.error( + "Failed to parse data channel message as JSON: ", + data, + dataChannel); + } + if (('undefined' !== typeof(obj)) && (null !== obj)) + { + var colibriClass = obj.colibriClass; - console.info( - "Data channel new active speaker event: " + resourceJid); - $(document).trigger('activespeakerchanged', [resourceJid]); + if ("DominantSpeakerEndpointChangeEvent" === colibriClass) + { + // Endpoint ID from the Videobridge. + var dominantSpeakerEndpoint = obj.dominantSpeakerEndpoint; + + console.info( + "Data channel new dominant speaker event: ", + dominantSpeakerEndpoint); + $(document).trigger( + 'dominantspeakerchanged', + [dominantSpeakerEndpoint]); + } + else if ("LastNEndpointsChangeEvent" === colibriClass) + { + // The new/latest list of last-n endpoint IDs. + var lastNEndpoints = obj.lastNEndpoints; + /* + * The list of endpoint IDs which are entering the list of + * last-n at this time i.e. were not in the old list of last-n + * endpoint IDs. + */ + var endpointsEnteringLastN = obj.endpointsEnteringLastN; + + console.debug( + "Data channel new last-n event: ", + lastNEndpoints); + } + else + { + console.debug("Data channel JSON-formatted message: ", obj); + } } }; @@ -77,4 +115,5 @@ function bindDataChannelListener(peerConnection) var msgData = event.data; console.info("Got My Data Channel Message:", msgData, dataChannel); };*/ -} \ No newline at end of file +} + diff --git a/index.html b/index.html index cb519c250..c197e091d 100644 --- a/index.html +++ b/index.html @@ -27,9 +27,9 @@ - + - + @@ -39,11 +39,11 @@ - - + + - + @@ -123,7 +123,7 @@ - + diff --git a/libs/colibri/colibri.focus.js b/libs/colibri/colibri.focus.js index e0e6cf2c5..56708074b 100644 --- a/libs/colibri/colibri.focus.js +++ b/libs/colibri/colibri.focus.js @@ -54,17 +54,22 @@ function ColibriFocus(connection, bridgejid) { * Default channel expire value in seconds. * @type {number} */ - this.channelExpire = 60; + this.channelExpire + = ('number' === typeof(config.channelExpire)) + ? config.channelExpire + : 15; + /** + * Default channel last-n value. + * @type {number} + */ + this.channelLastN + = ('number' === typeof(config.channelLastN)) ? config.channelLastN : -1; // media types of the conference if (config.openSctp) - { this.media = ['audio', 'video', 'data']; - } else - { this.media = ['audio', 'video']; - } this.connection.jingle.sessions[this.sid] = this; this.mychannel = []; @@ -202,29 +207,33 @@ ColibriFocus.prototype._makeConference = function () { elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'}); this.media.forEach(function (name) { - var isData = name === 'data'; - var channel = isData ? 'sctpconnection' : 'channel'; + var elemName; + var elemAttrs = { initiator: 'true', expire: self.channelExpire }; + + if ('data' === name) + { + elemName = 'sctpconnection'; + elemAttrs['port'] = 5000; + } + else + { + elemName = 'channel'; + if (('video' === name) && (this.channelLastN >= 0)) + elemAttrs['last-n'] = this.channelLastN; + } elem.c('content', {name: name}); - elem.c(channel, { - initiator: 'true', - expire: '15', - endpoint: self.myMucResource - }); - if (isData) - elem.attrs({port: 5000}); - elem.up();// end of channel + elem.c(elemName, elemAttrs); + elem.attrs({ endpoint: self.myMucResource }); + elem.up();// end of channel/sctpconnection for (var j = 0; j < self.peers.length; j++) { - elem.c(channel, { - initiator: 'true', - expire: '15', - endpoint: self.peers[j].substr(1 + self.peers[j].lastIndexOf('/')) - }); - if (isData) - elem.attrs({port: 5000}); - elem.up(); // end of channel + var peer = self.peers[j]; + + elem.c(elemName, elemAttrs); + elem.attrs({ endpoint: peer.substr(1 + peer.lastIndexOf('/')) }); + elem.up(); // end of channel/sctpconnection } elem.up(); // end of content }); @@ -233,7 +242,7 @@ ColibriFocus.prototype._makeConference = function () { localSDP.media.forEach(function (media, channel) { var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media; elem.c('content', {name: name}); - elem.c('channel', {initiator: 'false', expire: '15'}); + elem.c('channel', {initiator: 'false', expire: self.channelExpire}); // FIXME: should reuse code from .toJingle var mline = SDPUtil.parse_mline(media.split('\r\n')[0]); @@ -247,7 +256,7 @@ ColibriFocus.prototype._makeConference = function () { elem.up(); // end of channel for (j = 0; j < self.peers.length; j++) { - elem.c('channel', {initiator: 'true', expire:'15' }).up(); + elem.c('channel', {initiator: 'true', expire: self.channelExpire }).up(); } elem.up(); // end of content }); @@ -540,7 +549,8 @@ ColibriFocus.prototype.initiate = function (peer, isInitiator) { if (1) { //i > 0) { // not for audio FIXME: does not work as intended // re-add all remote a=ssrcs for (var jid in this.remotessrc) { - if (jid == peer) continue; + if (jid == peer || !this.remotessrc[jid][i]) + continue; sdp.media[i] += this.remotessrc[jid][i]; } // and local a=ssrc lines @@ -661,24 +671,28 @@ ColibriFocus.prototype.addNewParticipant = function (peer) { var localSDP = new SDP(this.peerconnection.localDescription.sdp); localSDP.media.forEach(function (media, channel) { var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:')); - elem.c('content', {name: name}); - if (name !== 'data') - { - elem.c('channel', { + var elemName; + var elemAttrs + = { initiator: 'true', expire: self.channelExpire, endpoint: peer.substr(1 + peer.lastIndexOf('/')) - }); + }; + + if ('data' == name) + { + elemName = 'sctpconnection'; + elemAttrs['port'] = 5000; } else { - elem.c('sctpconnection', { - endpoint: peer.substr(1 + peer.lastIndexOf('/')), - initiator: 'true', - expire: self.channelExpire, - port: 5000 - }); + elemName = 'channel'; + if (('video' === name) && (this.channelLastN >= 0)) + elemAttrs['last-n'] = this.channelLastN; } + + elem.c('content', {name: name}); + elem.c(elemName, elemAttrs); elem.up(); // end of channel/sctpconnection elem.up(); // end of content }); @@ -714,6 +728,9 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) { change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid}); for (channel = 0; channel < this.channels[participant].length; channel++) { + if (!remoteSDP.media[channel]) + continue; + var name = SDPUtil.parse_mid(SDPUtil.find_line(remoteSDP.media[channel], 'a=mid:')); change.c('content', {name: name}); if (name !== 'data') @@ -894,6 +911,9 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) this.remotessrc[session.peerjid] = []; for (channel = 0; channel < this.channels[participant].length; channel++) { //if (channel == 0) continue; FIXME: does not work as intended + if (!remoteSDP.media[channel]) + continue; + if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length) { this.remotessrc[session.peerjid][channel] = @@ -905,6 +925,9 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) // ACT 4: add new a=ssrc lines to local remotedescription for (channel = 0; channel < this.channels[participant].length; channel++) { //if (channel == 0) continue; FIXME: does not work as intended + if (!remoteSDP.media[channel]) + continue; + if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length) { this.peerconnection.enqueueAddSsrc( channel, @@ -1153,3 +1176,73 @@ ColibriFocus.prototype.sendTerminate = function (session, reason, text) { this.statsinterval = null; } }; + +ColibriFocus.prototype.setRTCPTerminationStrategy = function (strategyFQN) { + var self = this; + var strategyIQ = $iq({to: this.bridgejid, type: 'set'}); + strategyIQ.c('conference', { + xmlns: 'http://jitsi.org/protocol/colibri', + id: this.confid, + }); + + strategyIQ.c('rtcp-termination-strategy', {name: strategyFQN }); + + strategyIQ.c('content', {name: "video"}); + strategyIQ.up(); // end of content + + console.log('setting RTCP termination strategy', strategyFQN); + this.connection.sendIQ(strategyIQ, + function (res) { + console.log('got result'); + }, + function (err) { + console.error('got error', err); + } + ); +}; + +/** + * Sets the default value of the channel last-n attribute in this conference and + * updates/patches the existing channels. + */ +ColibriFocus.prototype.setChannelLastN = function (channelLastN) { + if (('number' === typeof(channelLastN)) + && (this.channelLastN !== channelLastN)) + { + this.channelLastN = channelLastN; + + // Update/patch the existing channels. + var patch = $iq({ to:this.bridgejid, type:'set' }); + + patch.c( + 'conference', + { xmlns:'http://jitsi.org/protocol/colibri', id:this.confid }); + patch.c('content', { name:'video' }); + patch.c( + 'channel', + { + id:$(this.mychannel[1 /* video */]).attr('id'), + 'last-n':this.channelLastN + }); + patch.up(); // end of channel + for (var p = 0; p < this.channels.length; p++) + { + patch.c( + 'channel', + { + id:$(this.channels[p][1 /* video */]).attr('id'), + 'last-n':this.channelLastN + }); + patch.up(); // end of channel + } + this.connection.sendIQ( + patch, + function (res) { + console.info('Set channel last-n succeeded: ', res); + }, + function (err) { + console.error('Set channel last-n failed: ', err); + }); + } +}; + diff --git a/libs/strophe/strophe.jingle.adapter.js b/libs/strophe/strophe.jingle.adapter.js index 2e8e1e209..1db9c6fdd 100644 --- a/libs/strophe/strophe.jingle.adapter.js +++ b/libs/strophe/strophe.jingle.adapter.js @@ -519,6 +519,7 @@ function getUserMediaWithConstraints(um, success_callback, failure_callback, res constraints.video = { mandatory: { chromeMediaSource: "screen", + googLeakyBucket: true, maxWidth: window.screen.width, maxHeight: window.screen.height, maxFrameRate: 3 @@ -530,6 +531,7 @@ function getUserMediaWithConstraints(um, success_callback, failure_callback, res mandatory: { chromeMediaSource: "desktop", chromeMediaSourceId: desktopStream, + googLeakyBucket: true, maxWidth: window.screen.width, maxHeight: window.screen.height, maxFrameRate: 3 diff --git a/toolbar.js b/toolbar.js index 5d078c260..cdaaef239 100644 --- a/toolbar.js +++ b/toolbar.js @@ -5,7 +5,7 @@ var Toolbar = (function (my) { /** * Opens the lock room dialog. */ - my.openLockDialog = function() { + my.openLockDialog = function () { // Only the focus is able to set a shared key. if (focus === null) { if (sharedKey) @@ -54,7 +54,7 @@ var Toolbar = (function (my) { submit: function (e, v, m, f) { if (v) { var lockKey = document.getElementById('lockKey'); - + if (lockKey.value) { setSharedKey(Util.escapeHtml(lockKey.value)); lockRoom(true); @@ -70,24 +70,75 @@ var Toolbar = (function (my) { /** * Opens the invite link dialog. */ - my.openLinkDialog = function() { + my.openLinkDialog = function () { + var inviteLink; + if (roomUrl == null) + inviteLink = "Your conference is currently being created..."; + else + inviteLink = encodeURI(roomUrl); + $.prompt('', - { - title: "Share this link with everyone you want to invite", - persistent: false, - buttons: { "Cancel": false}, - loaded: function (event) { - document.getElementById('inviteLinkRef').select(); + inviteLink + '" onclick="this.select();" readonly>', + { + title: "Share this link with everyone you want to invite", + persistent: false, + buttons: { "Invite": true, "Cancel": false}, + defaultButton: 1, + loaded: function (event) { + if (roomUrl) + document.getElementById('inviteLinkRef').select(); + else + document.getElementById('jqi_state0_buttonInvite') + .disabled = true; + }, + submit: function (e, v, m, f) { + if (v) { + if (roomUrl) { + inviteParticipants(); + } + } + } } - } - ); + ); }; + /** + * Invite participants to conference. + */ + function inviteParticipants() { + if (roomUrl == null) + return; + + var sharedKeyText = ""; + if (sharedKey && sharedKey.length > 0) + sharedKeyText + = "This conference is password protected. Please use the " + + "following pin when joining:%0D%0A%0D%0A" + + sharedKey + "%0D%0A%0D%0A"; + + var conferenceName = roomUrl.substring(roomUrl.lastIndexOf('/') + 1); + var subject = "Invitation to a Jitsi Meet (" + conferenceName + ")"; + var body = "Hey there, I%27d like to invite you to a Jitsi Meet" + + " conference I%27ve just set up.%0D%0A%0D%0A" + + "Please click on the following link in order" + + " to join the conference.%0D%0A%0D%0A" + + roomUrl + "%0D%0A%0D%0A" + + sharedKeyText + + "Note that Jitsi Meet is currently only supported by Chromim," + + " Google Chrome and Opera, so you need" + + " to be using one of these browsers.%0D%0A%0D%0A" + + "Talk to you in a sec!"; + + if (window.localStorage.displayname) + body += "%0D%0A%0D%0A" + window.localStorage.displayname; + + window.open("mailto:?subject=" + subject + "&body=" + body, '_blank'); + } + /** * Opens the settings dialog. */ - my.openSettingsDialog = function() { + my.openSettingsDialog = function () { $.prompt('