From 7c16d5509455c1c0ab819f936367ebc510fb1461 Mon Sep 17 00:00:00 2001 From: Lyubomir Marinov Date: Fri, 18 Jul 2014 15:17:55 +0300 Subject: [PATCH 01/18] Makes video aspect ratio an optional constraint in order to fix failures in which available resolutions meet the constraints on the width and height of a requested resolution but none of the available resolutions satisfy the constraint with respect to aspect ratio. --- libs/strophe/strophe.jingle.adapter.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/libs/strophe/strophe.jingle.adapter.js b/libs/strophe/strophe.jingle.adapter.js index 1db9c6fdd..05c69bcd6 100644 --- a/libs/strophe/strophe.jingle.adapter.js +++ b/libs/strophe/strophe.jingle.adapter.js @@ -510,7 +510,7 @@ function getUserMediaWithConstraints(um, success_callback, failure_callback, res var constraints = {audio: false, video: false}; if (um.indexOf('video') >= 0) { - constraints.video = {mandatory: {}};// same behaviour as true + constraints.video = { mandatory: {}, optional: [] };// same behaviour as true } if (um.indexOf('audio') >= 0) { constraints.audio = {};// same behaviour as true @@ -523,7 +523,8 @@ function getUserMediaWithConstraints(um, success_callback, failure_callback, res maxWidth: window.screen.width, maxHeight: window.screen.height, maxFrameRate: 3 - } + }, + optional: [] }; } if (um.indexOf('desktop') >= 0) { @@ -535,7 +536,8 @@ function getUserMediaWithConstraints(um, success_callback, failure_callback, res maxWidth: window.screen.width, maxHeight: window.screen.height, maxFrameRate: 3 - } + }, + optional: [] } } @@ -543,7 +545,7 @@ function getUserMediaWithConstraints(um, success_callback, failure_callback, res var isAndroid = navigator.userAgent.indexOf('Android') != -1; if (resolution && !constraints.video || isAndroid) { - constraints.video = {mandatory: {}};// same behaviour as true + constraints.video = { mandatory: {}, optional: [] };// same behaviour as true } // see https://code.google.com/p/chromium/issues/detail?id=143631#c9 for list of supported resolutions switch (resolution) { @@ -552,23 +554,23 @@ function getUserMediaWithConstraints(um, success_callback, failure_callback, res case 'fullhd': constraints.video.mandatory.minWidth = 1920; constraints.video.mandatory.minHeight = 1080; - constraints.video.mandatory.minAspectRatio = 1.77; + constraints.video.optional.push({ minAspectRatio: 1.77 }); break; case '720': case 'hd': constraints.video.mandatory.minWidth = 1280; constraints.video.mandatory.minHeight = 720; - constraints.video.mandatory.minAspectRatio = 1.77; + constraints.video.optional.push({ minAspectRatio: 1.77 }); break; case '360': constraints.video.mandatory.minWidth = 640; constraints.video.mandatory.minHeight = 360; - constraints.video.mandatory.minAspectRatio = 1.77; + constraints.video.optional.push({ minAspectRatio: 1.77 }); break; case '180': constraints.video.mandatory.minWidth = 320; constraints.video.mandatory.minHeight = 180; - constraints.video.mandatory.minAspectRatio = 1.77; + constraints.video.optional.push({ minAspectRatio: 1.77 }); break; // 4:3 case '960': From 8146655a70d56659b0fd79c904fad194e2aa5185 Mon Sep 17 00:00:00 2001 From: Boris Grozev Date: Fri, 18 Jul 2014 17:36:03 +0200 Subject: [PATCH 02/18] Fix bug with starting recording multiple times. --- libs/colibri/colibri.focus.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/libs/colibri/colibri.focus.js b/libs/colibri/colibri.focus.js index 56708074b..ed62d818e 100644 --- a/libs/colibri/colibri.focus.js +++ b/libs/colibri/colibri.focus.js @@ -172,7 +172,8 @@ ColibriFocus.prototype.makeConference = function (peers) { }; // Sends a COLIBRI message which enables or disables (according to 'state') the -// recording on the bridge. +// recording on the bridge. Waits for the result IQ and calls 'callback' with +// the new recording state, according to the IQ. ColibriFocus.prototype.setRecording = function(state, token, callback) { var self = this; var elem = $iq({to: this.bridgejid, type: 'get'}); @@ -187,10 +188,7 @@ ColibriFocus.prototype.setRecording = function(state, token, callback) { function (result) { console.log('Set recording "', state, '". Result:', result); var recordingElem = $(result).find('>conference>recording'); - var newState = recordingElem.attr('state'); - if (newState == null){ - newState = false; - } + var newState = ('true' === recordingElem.attr('state')); self.recordingEnabled = newState; callback(newState); From b8d27e8ab02b92eac8e63fe6fcda1caa9a199a99 Mon Sep 17 00:00:00 2001 From: Boris Grozev Date: Fri, 18 Jul 2014 17:37:52 +0200 Subject: [PATCH 03/18] s/secrect/secret/ --- toolbar.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/toolbar.js b/toolbar.js index cdaaef239..f486aa6bb 100644 --- a/toolbar.js +++ b/toolbar.js @@ -29,7 +29,7 @@ var Toolbar = (function (my) { if (sharedKey) { $.prompt("Are you sure you would like to remove your secret key?", { - title: "Remove secrect key", + title: "Remove secret key", persistent: false, buttons: { "Remove": true, "Cancel": false}, defaultButton: 1, @@ -42,7 +42,7 @@ var Toolbar = (function (my) { } ); } else { - $.prompt('

Set a secrect key to lock your room

' + + $.prompt('

Set a secret key to lock your room

' + '', { persistent: false, @@ -142,7 +142,7 @@ var Toolbar = (function (my) { $.prompt('

Configure your conference

' + ' Participants join muted
' + ' Require nicknames

' + - 'Set a secrect key to lock your room: ', + 'Set a secret key to lock your room: ', { persistent: false, buttons: { "Save": true, "Cancel": false}, @@ -285,4 +285,4 @@ var Toolbar = (function (my) { }; return my; -}(Toolbar || {})); \ No newline at end of file +}(Toolbar || {})); From 6964e3197abe16b5dd11f7db6ff22821902ecdc4 Mon Sep 17 00:00:00 2001 From: Boris Grozev Date: Fri, 18 Jul 2014 18:06:06 +0200 Subject: [PATCH 04/18] Uses glow to indicate that recording is active. --- app.js | 4 ++-- css/main.css | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index 29d00e983..4ad7dc55d 100644 --- a/app.js +++ b/app.js @@ -923,14 +923,14 @@ function toggleRecording() { } var oldState = focus.recordingEnabled; - buttonClick("#recordButton", "icon-recEnable icon-recDisable"); + $('#recordButton').toggleClass('active'); focus.setRecording(!oldState, recordingToken, function (state) { console.log("New recording state: ", state); if (state == oldState) //failed to change, reset the token because it might have been wrong { - buttonClick("#recordButton", "icon-recEnable icon-recDisable"); + $('#recordButton').toggleClass('active'); setRecordingToken(null); } } diff --git a/css/main.css b/css/main.css index e0a1726e9..620faf17e 100644 --- a/css/main.css +++ b/css/main.css @@ -151,6 +151,27 @@ html, body{ 0 -1px 10px #00ccff; } +#recordButton { + -webkit-transition: all .5s ease-in-out; + -moz-transition: all .5s ease-in-out; + transition: all .5s ease-in-out; +} +/*#ffde00*/ +#recordButton.active { + -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 { top: 0; cursor: pointer; From 777475c9ce4c1e222797b7f5bc8c4746d1666c1d Mon Sep 17 00:00:00 2001 From: Lyubomir Marinov Date: Sun, 20 Jul 2014 09:01:23 +0300 Subject: [PATCH 05/18] Fixes an issue which could cause last-n settings to not be respected by new channel allocations. --- libs/colibri/colibri.focus.js | 68 ++++++++++++++++------------------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/libs/colibri/colibri.focus.js b/libs/colibri/colibri.focus.js index ed62d818e..a078cfc11 100644 --- a/libs/colibri/colibri.focus.js +++ b/libs/colibri/colibri.focus.js @@ -201,8 +201,8 @@ ColibriFocus.prototype.setRecording = function(state, token, callback) { ColibriFocus.prototype._makeConference = function () { var self = this; - var elem = $iq({to: this.bridgejid, type: 'get'}); - elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'}); + var elem = $iq({ to: this.bridgejid, type: 'get' }); + elem.c('conference', { xmlns: 'http://jitsi.org/protocol/colibri' }); this.media.forEach(function (name) { var elemName; @@ -216,11 +216,11 @@ ColibriFocus.prototype._makeConference = function () { else { elemName = 'channel'; - if (('video' === name) && (this.channelLastN >= 0)) - elemAttrs['last-n'] = this.channelLastN; + if (('video' === name) && (self.channelLastN >= 0)) + elemAttrs['last-n'] = self.channelLastN; } - elem.c('content', {name: name}); + elem.c('content', { name: name }); elem.c(elemName, elemAttrs); elem.attrs({ endpoint: self.myMucResource }); @@ -655,9 +655,7 @@ ColibriFocus.prototype.addNewParticipant = function (peer) { { console.error('local description not ready yet, postponing', peer); } - window.setTimeout(function () { - self.addNewParticipant(peer); - }, 250); + window.setTimeout(function () { self.addNewParticipant(peer); }, 250); return; } var index = this.channels.length; @@ -665,7 +663,9 @@ ColibriFocus.prototype.addNewParticipant = function (peer) { this.peers.push(peer); var elem = $iq({to: this.bridgejid, type: 'get'}); - elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid}); + elem.c( + 'conference', + { xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid }); 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:')); @@ -685,11 +685,11 @@ ColibriFocus.prototype.addNewParticipant = function (peer) { else { elemName = 'channel'; - if (('video' === name) && (this.channelLastN >= 0)) - elemAttrs['last-n'] = this.channelLastN; + if (('video' === name) && (self.channelLastN >= 0)) + elemAttrs['last-n'] = self.channelLastN; } - elem.c('content', {name: name}); + elem.c('content', { name: name }); elem.c(elemName, elemAttrs); elem.up(); // end of channel/sctpconnection elem.up(); // end of content @@ -817,12 +817,7 @@ ColibriFocus.prototype.addSource = function (elem, fromJid) { if (!this.peerconnection.localDescription) { console.warn("addSource - localDescription not ready yet") - setTimeout(function() - { - self.addSource(elem, fromJid); - }, - 200 - ); + setTimeout(function() { self.addSource(elem, fromJid); }, 200); return; } @@ -863,12 +858,7 @@ ColibriFocus.prototype.removeSource = function (elem, fromJid) { if (!self.peerconnection.localDescription) { console.warn("removeSource - localDescription not ready yet"); - setTimeout(function() - { - self.removeSource(elem, fromJid); - }, - 200 - ); + setTimeout(function() { self.removeSource(elem, fromJid); }, 200); return; } @@ -1009,11 +999,13 @@ ColibriFocus.prototype.sendIceCandidate = function (candidate) { } if (this.drip_container.length === 0) { // start 20ms callout - window.setTimeout(function () { - if (self.drip_container.length === 0) return; - self.sendIceCandidates(self.drip_container); - self.drip_container = []; - }, 20); + window.setTimeout( + function () { + if (self.drip_container.length === 0) return; + self.sendIceCandidates(self.drip_container); + self.drip_container = []; + }, + 20); } this.drip_container.push(candidate); }; @@ -1210,17 +1202,17 @@ ColibriFocus.prototype.setChannelLastN = function (channelLastN) { this.channelLastN = channelLastN; // Update/patch the existing channels. - var patch = $iq({ to:this.bridgejid, type:'set' }); + 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' }); + { 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 + 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++) @@ -1228,18 +1220,18 @@ ColibriFocus.prototype.setChannelLastN = function (channelLastN) { patch.c( 'channel', { - id:$(this.channels[p][1 /* video */]).attr('id'), - 'last-n':this.channelLastN + 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); + console.info('Set channel last-n succeeded:', res); }, function (err) { - console.error('Set channel last-n failed: ', err); + console.error('Set channel last-n failed:', err); }); } }; From 304cdf5b406ac4398bba198d1970abc3cbd00704 Mon Sep 17 00:00:00 2001 From: Boris Grozev Date: Mon, 21 Jul 2014 09:49:50 +0200 Subject: [PATCH 06/18] Fix a potential problem with removing old SSRCs. --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index 4ad7dc55d..1251849a4 100644 --- a/app.js +++ b/app.js @@ -734,7 +734,7 @@ $(document).bind('presence.muc', function (event, jid, info, pres) { if (ssrc2jid[ssrc] == jid) { delete ssrc2jid[ssrc]; } - if (ssrc2videoType == jid) { + if (ssrc2videoType[ssrc] == jid) { delete ssrc2videoType[ssrc]; } }); From 50f1521d5cb863576b53d0df183f837b0fdc4f47 Mon Sep 17 00:00:00 2001 From: yanas Date: Mon, 21 Jul 2014 12:36:11 +0200 Subject: [PATCH 07/18] Audio level indication. Improvements in rollover and active speaker UI. --- audio_levels.js | 193 ++++++++++++++++++++++++++++++++++++++++++++++++ canvas_util.js | 101 +++++++++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 audio_levels.js create mode 100644 canvas_util.js diff --git a/audio_levels.js b/audio_levels.js new file mode 100644 index 000000000..3f9734378 --- /dev/null +++ b/audio_levels.js @@ -0,0 +1,193 @@ +/** + * The audio Levels plugin. + */ +var AudioLevels = (function(my) { + var CANVAS_EXTRA = 104; + var CANVAS_RADIUS = 7; + var SHADOW_COLOR = '#00ccff'; + var audioLevelCanvasCache = {}; + + /** + * Updates the audio level canvas for the given peerJid. If the canvas + * didn't exist we create it. + */ + my.updateAudioLevelCanvas = function (peerJid) { + var resourceJid = null; + var videoSpanId = null; + if (!peerJid) + videoSpanId = 'localVideoContainer'; + else { + resourceJid = Strophe.getResourceFromJid(peerJid); + + videoSpanId = 'participant_' + resourceJid; + } + + videoSpan = document.getElementById(videoSpanId); + + if (!videoSpan) { + if (resourceJid) + console.error("No video element for jid", resourceJid); + else + console.error("No video element for local video."); + + return; + } + + var audioLevelCanvas = $('#' + videoSpanId + '>canvas'); + + var videoSpaceWidth = $('#remoteVideos').width(); + var thumbnailSize + = VideoLayout.calculateThumbnailSize(videoSpaceWidth); + var thumbnailWidth = thumbnailSize[0]; + var thumbnailHeight = thumbnailSize[1]; + + if (!audioLevelCanvas || audioLevelCanvas.length === 0) { + + audioLevelCanvas = document.createElement('canvas'); + audioLevelCanvas.className = "audiolevel"; + audioLevelCanvas.style.bottom = "-" + CANVAS_EXTRA/2 + "px"; + audioLevelCanvas.style.left = "-" + CANVAS_EXTRA/2 + "px"; + resizeAudioLevelCanvas( audioLevelCanvas, + thumbnailWidth, + thumbnailHeight); + + videoSpan.appendChild(audioLevelCanvas); + } else { + audioLevelCanvas = audioLevelCanvas.get(0); + + resizeAudioLevelCanvas( audioLevelCanvas, + thumbnailWidth, + thumbnailHeight); + } + }; + + /** + * Updates the audio level UI for the given resourceJid. + * + * @param resourceJid the resource jid indicating the video element for + * which we draw the audio level + * @param audioLevel the newAudio level to render + */ + my.updateAudioLevel = function (resourceJid, audioLevel) { + drawAudioLevelCanvas(resourceJid, audioLevel); + + var videoSpanId = null; + if (resourceJid + === Strophe.getResourceFromJid(connection.emuc.myroomjid)) + videoSpanId = 'localVideoContainer'; + else + videoSpanId = 'participant_' + resourceJid; + + var audioLevelCanvas = $('#' + videoSpanId + '>canvas').get(0); + + if (!audioLevelCanvas) + return ; + + var drawContext = audioLevelCanvas.getContext('2d'); + + var canvasCache = audioLevelCanvasCache[resourceJid]; + + drawContext.clearRect (0, 0, + audioLevelCanvas.width, audioLevelCanvas.height); + drawContext.drawImage(canvasCache, 0, 0); + }; + + function resizeAudioLevelCanvas(audioLevelCanvas, + thumbnailWidth, + thumbnailHeight) { + audioLevelCanvas.width = thumbnailWidth + CANVAS_EXTRA; + audioLevelCanvas.height = thumbnailHeight + CANVAS_EXTRA; + }; + + /** + * Draws the audio level canvas into the cached canvas object. + * + * @param resourceJid the resource jid indicating the video element for + * which we draw the audio level + * @param audioLevel the newAudio level to render + */ + function drawAudioLevelCanvas(resourceJid, audioLevel) { + if (!audioLevelCanvasCache[resourceJid]) { + var videoSpanId = null; + if (resourceJid + === Strophe.getResourceFromJid(connection.emuc.myroomjid)) + videoSpanId = 'localVideoContainer'; + else + videoSpanId = 'participant_' + resourceJid; + + var audioLevelCanvasOrig = $('#' + videoSpanId + '>canvas').get(0); + + audioLevelCanvasCache[resourceJid] + = CanvasUtil.cloneCanvas(audioLevelCanvasOrig); + } + + var canvas = audioLevelCanvasCache[resourceJid]; + + var drawContext = canvas.getContext('2d'); + + drawContext.clearRect (0, 0, canvas.width, canvas.height); + + var shadowLevel = getShadowLevel(audioLevel); + + if (shadowLevel > 0) + // drawContext, x, y, w, h, r, shadowColor, shadowLevel + CanvasUtil.drawRoundRectGlow( drawContext, + CANVAS_EXTRA/2, CANVAS_EXTRA/2, + canvas.width - CANVAS_EXTRA, + canvas.height - CANVAS_EXTRA, + CANVAS_RADIUS, + SHADOW_COLOR, + shadowLevel); + }; + + /** + * Returns the shadow/glow level for the given audio level. + * + * @param audioLevel the audio level from which we determine the shadow + * level + */ + function getShadowLevel (audioLevel) { + var shadowLevel = 0; + + if (audioLevel <= 0.3) { + shadowLevel = Math.round(CANVAS_EXTRA/2*(audioLevel/0.3)); + } + else if (audioLevel <= 0.6) { + shadowLevel = Math.round(CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3)); + } + else { + shadowLevel = Math.round(CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4)); + } + return shadowLevel; + }; + + /** + * Indicates that the remote video has been resized. + */ + $(document).bind('remotevideo.resized', function (event, width, height) { + var resized = false; + $('#remoteVideos>span>canvas').each(function() { + var canvas = $(this).get(0); + if (canvas.width !== width + CANVAS_EXTRA) { + canvas.width = width + CANVAS_EXTRA; + resized = true; + } + + if (canvas.heigh !== height + CANVAS_EXTRA) { + canvas.height = height + CANVAS_EXTRA; + resized = true; + } + }); + + if (resized) + Object.keys(audioLevelCanvasCache).forEach(function (resourceJid) { + audioLevelCanvasCache[resourceJid].width + = width + CANVAS_EXTRA; + audioLevelCanvasCache[resourceJid].height + = height + CANVAS_EXTRA; + }); + }); + + return my; + +})(AudioLevels || {}); \ No newline at end of file diff --git a/canvas_util.js b/canvas_util.js new file mode 100644 index 000000000..bc96f030a --- /dev/null +++ b/canvas_util.js @@ -0,0 +1,101 @@ +/** + * Utility class for drawing canvas shapes. + */ +var CanvasUtil = (function(my) { + + /** + * Draws a round rectangle with a glow. The glowWidth indicates the depth + * of the glow. + * + * @param drawContext the context of the canvas to draw to + * @param x the x coordinate of the round rectangle + * @param y the y coordinate of the round rectangle + * @param w the width of the round rectangle + * @param h the height of the round rectangle + * @param glowColor the color of the glow + * @param glowWidth the width of the glow + */ + my.drawRoundRectGlow + = function(drawContext, x, y, w, h, r, glowColor, glowWidth) { + + // Save the previous state of the context. + drawContext.save(); + + if (w < 2 * r) r = w / 2; + if (h < 2 * r) r = h / 2; + + // Draw a round rectangle. + drawContext.beginPath(); + drawContext.moveTo(x+r, y); + drawContext.arcTo(x+w, y, x+w, y+h, r); + drawContext.arcTo(x+w, y+h, x, y+h, r); + drawContext.arcTo(x, y+h, x, y, r); + drawContext.arcTo(x, y, x+w, y, r); + drawContext.closePath(); + + // Add a shadow around the rectangle + drawContext.shadowColor = glowColor; + drawContext.shadowBlur = glowWidth; + drawContext.shadowOffsetX = 0; + drawContext.shadowOffsetY = 0; + + // Fill the shape. + drawContext.fill(); + + drawContext.save(); + + drawContext.restore(); + +// 1) Uncomment this line to use Composite Operation, which is doing the +// same as the clip function below and is also antialiasing the round +// border, but is said to be less fast performance wise. + +// drawContext.globalCompositeOperation='destination-out'; + + drawContext.beginPath(); + drawContext.moveTo(x+r, y); + drawContext.arcTo(x+w, y, x+w, y+h, r); + drawContext.arcTo(x+w, y+h, x, y+h, r); + drawContext.arcTo(x, y+h, x, y, r); + drawContext.arcTo(x, y, x+w, y, r); + drawContext.closePath(); + +// 2) Uncomment this line to use Composite Operation, which is doing the +// same as the clip function below and is also antialiasing the round +// border, but is said to be less fast performance wise. + +// drawContext.fill(); + + // Comment these two lines if choosing to do the same with composite + // operation above 1 and 2. + drawContext.clip(); + drawContext.clearRect(0, 0, 277, 200); + + // Restore the previous context state. + drawContext.restore(); + }; + + /** + * Clones the given canvas. + * + * @return the new cloned canvas. + */ + my.cloneCanvas = function (oldCanvas) { + + //create a new canvas + var newCanvas = document.createElement('canvas'); + var context = newCanvas.getContext('2d'); + + //set dimensions + newCanvas.width = oldCanvas.width; + newCanvas.height = oldCanvas.height; + + //apply the old canvas to the new one + context.drawImage(oldCanvas, 0, 0); + + //return the new canvas + return newCanvas; + }; + + return my; +})(CanvasUtil || {}); \ No newline at end of file From 1d3df3c41c877e390001247f18a1ba6b4523ef1b Mon Sep 17 00:00:00 2001 From: yanas Date: Tue, 22 Jul 2014 15:12:48 +0200 Subject: [PATCH 08/18] Audio level indication. Improvements in rollover and active speaker UI. Part 2. --- app.js | 10 +++++++--- chat.js | 15 +++++++++++++-- config.js | 2 +- css/videolayout_default.css | 19 +++++++++++++++++-- index.html | 12 +++++++----- local_stats.js | 9 +++------ videolayout.js | 8 ++++++++ 7 files changed, 56 insertions(+), 19 deletions(-) diff --git a/app.js b/app.js index 1251849a4..c15fd864b 100644 --- a/app.js +++ b/app.js @@ -448,8 +448,9 @@ function statsUpdated(statsCollector) var peerStats = statsCollector.jid2stats[jid]; Object.keys(peerStats.ssrc2AudioLevel).forEach(function (ssrc) { -// console.info(jid + " audio level: " + -// peerStats.ssrc2AudioLevel[ssrc] + " of ssrc: " + ssrc); + if (jid !== connection.emuc.myRoomJid) + AudioLevels.updateAudioLevel( Strophe.getResourceFromJid(jid), + peerStats.ssrc2AudioLevel[ssrc]); }); }); } @@ -461,7 +462,10 @@ function statsUpdated(statsCollector) */ function localStatsUpdated(statsCollector) { -// console.info("Local audio level: " + statsCollector.audioLevel); + if (connection.emuc.myRoomJid) + AudioLevels.updateAudioLevel( + Strophe.getResourceFromJid(connection.emuc.myRoomJid), + statsCollector.audioLevel); } /** diff --git a/chat.js b/chat.js index 051829dac..afb06c771 100644 --- a/chat.js +++ b/chat.js @@ -175,7 +175,13 @@ var Chat = (function (my) { $('#remoteVideos>span').animate({height: thumbnailsHeight, width: thumbnailsWidth}, {queue: false, - duration: 500}); + duration: 500, + complete: function() { + $(document).trigger( + "remotevideo.resized", + [thumbnailsWidth, + thumbnailsHeight]); + }}); $('#largeVideoContainer').animate({ width: videospaceWidth, height: videospaceHeight}, @@ -219,7 +225,12 @@ var Chat = (function (my) { $('#remoteVideos>span').animate({height: thumbnailsHeight, width: thumbnailsWidth}, {queue: false, - duration: 500}); + duration: 500, + complete: function() { + $(document).trigger( + "remotevideo.resized", + [thumbnailsWidth, thumbnailsHeight]); + }}); $('#largeVideoContainer').animate({ width: videospaceWidth, height: videospaceHeight}, diff --git a/config.js b/config.js index b68914179..db9c3b195 100644 --- a/config.js +++ b/config.js @@ -12,7 +12,7 @@ var config = { desktopSharing: 'ext', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable. chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension minChromeExtVersion: '0.1', // Required version of Chrome extension - enableRtpStats: false, // Enables RTP stats processing + enableRtpStats: true, // Enables RTP stats processing 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/videolayout_default.css b/css/videolayout_default.css index f140071b8..b87782db4 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -49,8 +49,16 @@ -webkit-animation-name: greyPulse; -webkit-animation-duration: 2s; -webkit-animation-iteration-count: 1; - -webkit-box-shadow: 0 0 18px #388396; - border: 2px solid #388396; +} + +#remoteVideos .videocontainer:hover { + -webkit-box-shadow: inset 0 0 10px #FFFFFF, 0 0 10px #FFFFFF; + border: 2px solid #FFFFFF; +} + +#remoteVideos .videocontainer.videoContainerFocused { + -webkit-box-shadow: inset 0 0 28px #006d91; + border: 2px solid #006d91; } #localVideoWrapper { @@ -291,3 +299,10 @@ background-image:url(../images/rightwatermark.png); background-position: center right; } + +.audiolevel { + display: inline-block; + position: absolute; + z-index: 0; + border-radius:10px; +} diff --git a/index.html b/index.html index c197e091d..ef310d7a7 100644 --- a/index.html +++ b/index.html @@ -22,14 +22,14 @@ - + - + - + @@ -39,12 +39,14 @@ - + + + - + diff --git a/local_stats.js b/local_stats.js index 2802fe455..6b2c44277 100644 --- a/local_stats.js +++ b/local_stats.js @@ -32,7 +32,6 @@ var LocalStatsCollector = (function() { this.audioLevel = 0; } - /** * Starts the collecting the statistics. */ @@ -61,8 +60,7 @@ var LocalStatsCollector = (function() { }, this.intervalMilis ); - - } + }; /** * Stops collecting the statistics. @@ -72,8 +70,7 @@ var LocalStatsCollector = (function() { clearInterval(this.intervalId); this.intervalId = null; } - } - + }; /** * Converts frequency data array to audio level. @@ -91,7 +88,7 @@ var LocalStatsCollector = (function() { } return maxVolume / 255; - } + }; return LocalStatsCollectorProto; })(); \ No newline at end of file diff --git a/videolayout.js b/videolayout.js index 11c2dbb1c..0c94799f3 100644 --- a/videolayout.js +++ b/videolayout.js @@ -26,6 +26,8 @@ var VideoLayout = (function (my) { var localVideoContainer = document.getElementById('localVideoWrapper'); localVideoContainer.appendChild(localVideo); + AudioLevels.updateAudioLevelCanvas(); + var localVideoSelector = $('#' + localVideo.id); // Add click handler to both video and video wrapper elements in case // there's no video. @@ -313,6 +315,8 @@ var VideoLayout = (function (my) { addRemoteVideoMenu(peerJid, container); remotes.appendChild(container); + AudioLevels.updateAudioLevelCanvas(peerJid); + return container; }; @@ -579,6 +583,8 @@ var VideoLayout = (function (my) { $('#remoteVideos').height(height); $('#remoteVideos>span').width(width); $('#remoteVideos>span').height(height); + + $(document).trigger("remotevideo.resized", [width, height]); }; /** @@ -958,3 +964,5 @@ var VideoLayout = (function (my) { return my; }(VideoLayout || {})); + + From 256694b966392b7aa3c01f25a46c073e3ad82e86 Mon Sep 17 00:00:00 2001 From: Boris Grozev Date: Wed, 23 Jul 2014 09:12:36 +0200 Subject: [PATCH 09/18] Sends endpoint information in COLIBRI messages (in 'endpoint' children of 'conference'). --- app.js | 16 +++++++ libs/colibri/colibri.focus.js | 85 ++++++++++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index c15fd864b..0eeb5fdc4 100644 --- a/app.js +++ b/app.js @@ -632,6 +632,10 @@ $(document).bind('joined.muc', function (event, jid, info) { if (Object.keys(connection.emuc.members).length < 1) { focus = new ColibriFocus(connection, config.hosts.bridge); + if (nickname !== null) { + focus.setEndpointDisplayName(connection.emuc.myroomjid, + nickname); + } showRecordingButton(false); } @@ -710,6 +714,10 @@ $(document).bind('left.muc', function (event, jid) { && !sessionTerminated) { console.log('welcome to our new focus... myself'); focus = new ColibriFocus(connection, config.hosts.bridge); + if (nickname !== null) { + focus.setEndpointDisplayName(connection.emuc.myroomjid, + nickname); + } if (Object.keys(connection.emuc.members).length > 0) { focus.makeConference(Object.keys(connection.emuc.members)); @@ -723,6 +731,10 @@ $(document).bind('left.muc', function (event, jid) { // problems with reinit disposeConference(); focus = new ColibriFocus(connection, config.hosts.bridge); + if (nickname !== null) { + focus.setEndpointDisplayName(connection.emuc.myroomjid, + nickname); + } showRecordingButton(false); } if (connection.emuc.getPrezi(jid)) { @@ -776,6 +788,10 @@ $(document).bind('presence.muc', function (event, jid, info, pres) { 'participant_' + Strophe.getResourceFromJid(jid), info.displayName); } + + if (focus !== null && info.displayName !== null) { + focus.setEndpointDisplayName(jid, info.displayName); + } }); $(document).bind('passwordrequired.muc', function (event, jid) { diff --git a/libs/colibri/colibri.focus.js b/libs/colibri/colibri.focus.js index a078cfc11..20a55a53e 100644 --- a/libs/colibri/colibri.focus.js +++ b/libs/colibri/colibri.focus.js @@ -84,6 +84,10 @@ function ColibriFocus(connection, bridgejid) { this.wait = true; this.recordingEnabled = false; + + // stores information about the endpoints (i.e. display names) to + // be sent to the videobridge. + this.endpointsInfo = null; } // creates a conferences with an initial set of peers @@ -176,7 +180,7 @@ ColibriFocus.prototype.makeConference = function (peers) { // the new recording state, according to the IQ. ColibriFocus.prototype.setRecording = function(state, token, callback) { var self = this; - var elem = $iq({to: this.bridgejid, type: 'get'}); + var elem = $iq({to: this.bridgejid, type: 'set'}); elem.c('conference', { xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid @@ -199,6 +203,74 @@ ColibriFocus.prototype.setRecording = function(state, token, callback) { ); }; +/* + * Updates the display name for an endpoint with a specific jid. + * jid: the jid associated with the endpoint. + * displayName: the new display name for the endpoint. + */ +ColibriFocus.prototype.setEndpointDisplayName = function(jid, displayName) { + var endpointId = jid.substr(1 + jid.lastIndexOf('/')); + var update = false; + + if (this.endpointsInfo === null) { + this.endpointsInfo = {}; + } + + var endpointInfo = this.endpointsInfo[endpointId]; + if ('undefined' === typeof endpointInfo) { + endpointInfo = this.endpointsInfo[endpointId] = {}; + } + + if (endpointInfo['displayname'] !== displayName) { + endpointInfo['displayname'] = displayName; + update = true; + } + + if (update) { + this.updateEndpoints(); + } +}; + +/* + * Sends a colibri message to the bridge that contains the + * current endpoints and their display names. + */ +ColibriFocus.prototype.updateEndpoints = function() { + if (this.confid === null + || this.endpointsInfo === null) { + return; + } + + if (this.confid === 0) { + // the colibri conference is currently initiating + var self = this; + window.setTimeout(function() { self.updateEndpoints()}, 1000); + return; + } + + var elem = $iq({to: this.bridgejid, type: 'set'}); + elem.c('conference', { + xmlns: 'http://jitsi.org/protocol/colibri', + id: this.confid + }); + + for (var id in this.endpointsInfo) { + elem.c('endpoint'); + elem.attrs({ id: id, + displayname: this.endpointsInfo[id]['displayname'] + }); + elem.up(); + } + + //elem.up(); //conference + + this.connection.sendIQ( + elem, + function (result) {}, + function (error) { console.warn(error); } + ); +}; + ColibriFocus.prototype._makeConference = function () { var self = this; var elem = $iq({ to: this.bridgejid, type: 'get' }); @@ -235,6 +307,17 @@ ColibriFocus.prototype._makeConference = function () { } elem.up(); // end of content }); + + if (this.endpointsInfo !== null) { + for (var id in this.endpointsInfo) { + elem.c('endpoint'); + elem.attrs({ id: id, + displayname: this.endpointsInfo[id]['displayname'] + }); + elem.up(); + } + } + /* var localSDP = new SDP(this.peerconnection.localDescription.sdp); localSDP.media.forEach(function (media, channel) { From 05975e30a3be7e5a7863ed8a15ee7ac2f4bdfafd Mon Sep 17 00:00:00 2001 From: Boris Grozev Date: Wed, 23 Jul 2014 10:47:00 +0300 Subject: [PATCH 10/18] Moves recording button related code to toolbar.js. --- app.js | 24 ++++++------------------ toolbar.js | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/app.js b/app.js index 0eeb5fdc4..447a76753 100644 --- a/app.js +++ b/app.js @@ -636,7 +636,7 @@ $(document).bind('joined.muc', function (event, jid, info) { focus.setEndpointDisplayName(connection.emuc.myroomjid, nickname); } - showRecordingButton(false); + Toolbar.showRecordingButton(false); } if (focus && config.etherpad_base) { @@ -668,7 +668,7 @@ $(document).bind('entered.muc', function (event, jid, info, pres) { if (focus.confid === null) { console.log('make new conference with', jid); focus.makeConference(Object.keys(connection.emuc.members)); - showRecordingButton(true); + Toolbar.showRecordingButton(true); } else { console.log('invite', jid, 'into conference'); focus.addNewParticipant(jid); @@ -721,7 +721,7 @@ $(document).bind('left.muc', function (event, jid) { if (Object.keys(connection.emuc.members).length > 0) { focus.makeConference(Object.keys(connection.emuc.members)); - showRecordingButton(true); + Toolbar.showRecordingButton(true); } $(document).trigger('focusechanged.muc', [focus]); } @@ -735,7 +735,7 @@ $(document).bind('left.muc', function (event, jid) { focus.setEndpointDisplayName(connection.emuc.myroomjid, nickname); } - showRecordingButton(false); + Toolbar.showRecordingButton(false); } if (connection.emuc.getPrezi(jid)) { $(document).trigger('presentationremoved.muc', @@ -943,14 +943,14 @@ function toggleRecording() { } var oldState = focus.recordingEnabled; - $('#recordButton').toggleClass('active'); + Toolbar.toggleRecordingButtonState(); focus.setRecording(!oldState, recordingToken, function (state) { console.log("New recording state: ", state); if (state == oldState) //failed to change, reset the token because it might have been wrong { - $('#recordButton').toggleClass('active'); + Toolbar.toggleRecordingButtonState(); setRecordingToken(null); } } @@ -1263,19 +1263,7 @@ function setView(viewName) { // } } -function showRecordingButton(show) { - if (!config.enableRecording) { - return; - } - if (show) { - $('#recording').css({display: "inline"}); - } - else { - $('#recording').css({display: "none"}); - } - -} $(document).bind('fatalError.jingle', function (event, session, error) diff --git a/toolbar.js b/toolbar.js index f486aa6bb..870dfa47b 100644 --- a/toolbar.js +++ b/toolbar.js @@ -284,5 +284,24 @@ var Toolbar = (function (my) { } }; + // Shows or hides the 'recording' button. + my.showRecordingButton = function (show) { + if (!config.enableRecording) { + return; + } + + if (show) { + $('#recording').css({display: "inline"}); + } + else { + $('#recording').css({display: "none"}); + } + }; + + // Toggle the state of the recording button + my.toggleRecordingButtonState = function() { + $('#recordButton').toggleClass('active'); + }; + return my; }(Toolbar || {})); From e9a3b453165f1b6ef20a6f99d881593194ea0953 Mon Sep 17 00:00:00 2001 From: Lyubomir Marinov Date: Wed, 23 Jul 2014 19:05:56 +0300 Subject: [PATCH 11/18] Fixes an error caused by trying to get a property of undefined related to audio levels. --- audio_levels.js | 20 ++++++++++++++++---- canvas_util.js | 10 +++++++++- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/audio_levels.js b/audio_levels.js index 3f9734378..a25da0116 100644 --- a/audio_levels.js +++ b/audio_levels.js @@ -117,15 +117,27 @@ var AudioLevels = (function(my) { var audioLevelCanvasOrig = $('#' + videoSpanId + '>canvas').get(0); - audioLevelCanvasCache[resourceJid] - = CanvasUtil.cloneCanvas(audioLevelCanvasOrig); + /* + * FIXME Testing has shown that audioLevelCanvasOrig may not exist. + * In such a case, the method CanvasUtil.cloneCanvas may throw an + * error. Since audio levels are frequently updated, the errors have + * been observed to pile into the console, strain the CPU. + */ + if (audioLevelCanvasOrig) + { + audioLevelCanvasCache[resourceJid] + = CanvasUtil.cloneCanvas(audioLevelCanvasOrig); + } } var canvas = audioLevelCanvasCache[resourceJid]; + if (!canvas) + return; + var drawContext = canvas.getContext('2d'); - drawContext.clearRect (0, 0, canvas.width, canvas.height); + drawContext.clearRect(0, 0, canvas.width, canvas.height); var shadowLevel = getShadowLevel(audioLevel); @@ -190,4 +202,4 @@ var AudioLevels = (function(my) { return my; -})(AudioLevels || {}); \ No newline at end of file +})(AudioLevels || {}); diff --git a/canvas_util.js b/canvas_util.js index bc96f030a..b8a1b0e9e 100644 --- a/canvas_util.js +++ b/canvas_util.js @@ -81,6 +81,14 @@ var CanvasUtil = (function(my) { * @return the new cloned canvas. */ my.cloneCanvas = function (oldCanvas) { + /* + * FIXME Testing has shown that oldCanvas may not exist. In such a case, + * the method CanvasUtil.cloneCanvas may throw an error. Since audio + * levels are frequently updated, the errors have been observed to pile + * into the console, strain the CPU. + */ + if (!oldCanvas) + return oldCanvas; //create a new canvas var newCanvas = document.createElement('canvas'); @@ -98,4 +106,4 @@ var CanvasUtil = (function(my) { }; return my; -})(CanvasUtil || {}); \ No newline at end of file +})(CanvasUtil || {}); From 5f34f67fc5eb404f60d918660c65dcea4e771076 Mon Sep 17 00:00:00 2001 From: Lyubomir Marinov Date: Wed, 23 Jul 2014 20:08:53 +0300 Subject: [PATCH 12/18] Fixes an error when clicking on a thumbnail without video. --- app.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index 447a76753..9711e8209 100644 --- a/app.js +++ b/app.js @@ -312,8 +312,15 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { // Add click handler. container.onclick = function (event) { - VideoLayout.handleVideoThumbClicked( - $('#' + container.id + '>video').get(0).src); + /* + * FIXME It turns out that videoThumb may not exist (if there is no + * actual video). + */ + var videoThumb = $('#' + container.id + '>video').get(0); + + if (videoThumb) + VideoLayout.handleVideoThumbClicked(videoThumb.src); + event.preventDefault(); return false; }; From 9e29378d7f379ac2b544462e9cfe6f6dc1e86c8e Mon Sep 17 00:00:00 2001 From: yanas Date: Thu, 24 Jul 2014 13:22:51 +0200 Subject: [PATCH 13/18] Fixes appearing scroll after adding the audio levels. --- css/videolayout_default.css | 1 + 1 file changed, 1 insertion(+) diff --git a/css/videolayout_default.css b/css/videolayout_default.css index b87782db4..145fcf616 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -4,6 +4,7 @@ top: 0px; left: 0px; right: 0px; + overflow: hidden; } #remoteVideos { From 1dcbf7fce65ea169316fe14401b84f938dcd2509 Mon Sep 17 00:00:00 2001 From: yanas Date: Thu, 24 Jul 2014 13:39:46 +0200 Subject: [PATCH 14/18] Fixes a minor bug in toolbar hide/show behavior when etherpad or prezi is opened. --- chat.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/chat.js b/chat.js index afb06c771..8db249b7e 100644 --- a/chat.js +++ b/chat.js @@ -204,8 +204,10 @@ var Chat = (function (my) { duration: 500}); } else { - // Undock the toolbar when the chat is shown. - Toolbar.dockToolbar(false); + // Undock the toolbar when the chat is shown and if we're in a + // video mode. + if (VideoLayout.isLargeVideoVisible()) + Toolbar.dockToolbar(false); videospace.animate({right: chatSize[0], width: videospaceWidth, From 913cdb9c7a90642fbc122ba4d360206579f3d9d5 Mon Sep 17 00:00:00 2001 From: yanas Date: Thu, 24 Jul 2014 16:14:37 +0200 Subject: [PATCH 15/18] Fixes audio level interface in the single user in conference case. --- app.js | 15 +++++++-------- audio_levels.js | 38 +++++++++++++++++++++++++------------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/app.js b/app.js index 9711e8209..c34a0605c 100644 --- a/app.js +++ b/app.js @@ -293,7 +293,8 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { data.stream.onended = function () { console.log('stream ended', this.id); - // Mark video as removed to cancel waiting loop(if video is removed before has started) + // Mark video as removed to cancel waiting loop(if video is removed + // before has started) sel.removed = true; sel.remove(); @@ -455,9 +456,8 @@ function statsUpdated(statsCollector) var peerStats = statsCollector.jid2stats[jid]; Object.keys(peerStats.ssrc2AudioLevel).forEach(function (ssrc) { - if (jid !== connection.emuc.myRoomJid) - AudioLevels.updateAudioLevel( Strophe.getResourceFromJid(jid), - peerStats.ssrc2AudioLevel[ssrc]); + AudioLevels.updateAudioLevel( Strophe.getResourceFromJid(jid), + peerStats.ssrc2AudioLevel[ssrc]); }); }); } @@ -469,10 +469,9 @@ function statsUpdated(statsCollector) */ function localStatsUpdated(statsCollector) { - if (connection.emuc.myRoomJid) - AudioLevels.updateAudioLevel( - Strophe.getResourceFromJid(connection.emuc.myRoomJid), - statsCollector.audioLevel); + AudioLevels.updateAudioLevel( + AudioLevels.LOCAL_LEVEL, + statsCollector.audioLevel); } /** diff --git a/audio_levels.js b/audio_levels.js index a25da0116..3a94fff22 100644 --- a/audio_levels.js +++ b/audio_levels.js @@ -7,6 +7,8 @@ var AudioLevels = (function(my) { var SHADOW_COLOR = '#00ccff'; var audioLevelCanvasCache = {}; + my.LOCAL_LEVEL = 'local'; + /** * Updates the audio level canvas for the given peerJid. If the canvas * didn't exist we create it. @@ -71,17 +73,12 @@ var AudioLevels = (function(my) { my.updateAudioLevel = function (resourceJid, audioLevel) { drawAudioLevelCanvas(resourceJid, audioLevel); - var videoSpanId = null; - if (resourceJid - === Strophe.getResourceFromJid(connection.emuc.myroomjid)) - videoSpanId = 'localVideoContainer'; - else - videoSpanId = 'participant_' + resourceJid; + var videoSpanId = getVideoSpanId(resourceJid); var audioLevelCanvas = $('#' + videoSpanId + '>canvas').get(0); if (!audioLevelCanvas) - return ; + return; var drawContext = audioLevelCanvas.getContext('2d'); @@ -92,6 +89,9 @@ var AudioLevels = (function(my) { drawContext.drawImage(canvasCache, 0, 0); }; + /** + * Resizes the given audio level canvas to match the given thumbnail size. + */ function resizeAudioLevelCanvas(audioLevelCanvas, thumbnailWidth, thumbnailHeight) { @@ -108,12 +108,8 @@ var AudioLevels = (function(my) { */ function drawAudioLevelCanvas(resourceJid, audioLevel) { if (!audioLevelCanvasCache[resourceJid]) { - var videoSpanId = null; - if (resourceJid - === Strophe.getResourceFromJid(connection.emuc.myroomjid)) - videoSpanId = 'localVideoContainer'; - else - videoSpanId = 'participant_' + resourceJid; + + var videoSpanId = getVideoSpanId(resourceJid); var audioLevelCanvasOrig = $('#' + videoSpanId + '>canvas').get(0); @@ -173,6 +169,22 @@ var AudioLevels = (function(my) { return shadowLevel; }; + /** + * Returns the video span id corresponding to the given resourceJid or local + * user. + */ + function getVideoSpanId(resourceJid) { + var videoSpanId = null; + if (resourceJid === AudioLevels.LOCAL_LEVEL + || (connection.emuc.myroomjid && resourceJid + === Strophe.getResourceFromJid(connection.emuc.myroomjid))) + videoSpanId = 'localVideoContainer'; + else + videoSpanId = 'participant_' + resourceJid; + + return videoSpanId; + }; + /** * Indicates that the remote video has been resized. */ From 83b4ee96d3e7e3e01cceae257947fc07f1456653 Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Sun, 27 Jul 2014 15:24:27 +0300 Subject: [PATCH 16/18] Enables LocalStatsCollector for the local audio levels instead of rtp stats. Fixes issue with local audio level calculations. --- app.js | 49 +++++++++++++--------------------------- local_stats.js | 61 ++++++++++++++++++++++++++++++++++++++++---------- rtp_stats.js | 4 ++-- 3 files changed, 67 insertions(+), 47 deletions(-) diff --git a/app.js b/app.js index c34a0605c..9cacb7b37 100644 --- a/app.js +++ b/app.js @@ -445,33 +445,23 @@ function muteVideo(pc, unmute) { } /** - * Callback called by {@link StatsCollector} in intervals supplied to it's - * constructor. - * @param statsCollector {@link StatsCollector} source of the event. + * Callback for audio levels changed. + * @param jid JID of the user + * @param audioLevel the audio level value */ -function statsUpdated(statsCollector) +function audioLevelUpdated(jid, audioLevel) { - Object.keys(statsCollector.jid2stats).forEach(function (jid) + var resourceJid; + if(jid === LocalStatsCollector.LOCAL_JID) { - var peerStats = statsCollector.jid2stats[jid]; - Object.keys(peerStats.ssrc2AudioLevel).forEach(function (ssrc) - { - AudioLevels.updateAudioLevel( Strophe.getResourceFromJid(jid), - peerStats.ssrc2AudioLevel[ssrc]); - }); - }); -} + resourceJid = AudioLevels.LOCAL_LEVEL; + } + else + { + resourceJid = Strophe.getResourceFromJid(jid); + } -/** - * Callback called by {@link LocalStatsCollector} in intervals supplied to it's - * constructor. - * @param statsCollector {@link LocalStatsCollector} source of the event. - */ -function localStatsUpdated(statsCollector) -{ - AudioLevels.updateAudioLevel( - AudioLevels.LOCAL_LEVEL, - statsCollector.audioLevel); + AudioLevels.updateAudioLevel(resourceJid, audioLevel); } /** @@ -483,10 +473,7 @@ function startRtpStatsCollector() if (config.enableRtpStats) { statsCollector = new StatsCollector( - getConferenceHandler().peerconnection, 200, statsUpdated); - - stopLocalRtpStatsCollector(); - + getConferenceHandler().peerconnection, 200, audioLevelUpdated); statsCollector.start(); } } @@ -511,7 +498,7 @@ function startLocalRtpStatsCollector(stream) { if(config.enableRtpStats) { - localStatsCollector = new LocalStatsCollector(stream, 200, localStatsUpdated); + localStatsCollector = new LocalStatsCollector(stream, 100, audioLevelUpdated); localStatsCollector.start(); } } @@ -1123,11 +1110,7 @@ function disposeConference(onUnload) { handler.peerconnection.close(); } stopRTPStatsCollector(); - if(!onUnload) { - startLocalRtpStatsCollector(connection.jingle.localAudio); - } - else - { + if(onUnload) { stopLocalRtpStatsCollector(); } focus = null; diff --git a/local_stats.js b/local_stats.js index 6b2c44277..fbefa98c8 100644 --- a/local_stats.js +++ b/local_stats.js @@ -6,13 +6,13 @@ var LocalStatsCollector = (function() { * Size of the webaudio analizer buffer. * @type {number} */ - var WEBAUDIO_ANALIZER_FFT_SIZE = 512; + var WEBAUDIO_ANALIZER_FFT_SIZE = 2048; /** * Value of the webaudio analizer smoothing time parameter. * @type {number} */ - var WEBAUDIO_ANALIZER_SMOOTING_TIME = 0.1; + var WEBAUDIO_ANALIZER_SMOOTING_TIME = 0.8; /** * LocalStatsCollector calculates statistics for the local stream. @@ -54,12 +54,16 @@ var LocalStatsCollector = (function() { this.intervalId = setInterval( function () { var array = new Uint8Array(analyser.frequencyBinCount); - analyser.getByteFrequencyData(array); - self.audioLevel = FrequencyDataToAudioLevel(array); - self.updateCallback(self); + analyser.getByteTimeDomainData(array); + var audioLevel = TimeDomainDataToAudioLevel(array); + if(audioLevel != self.audioLevel) { + self.audioLevel = animateLevel(audioLevel, self.audioLevel); + self.updateCallback(LocalStatsCollectorProto.LOCAL_JID, self.audioLevel); + } }, this.intervalMilis ); + }; /** @@ -73,22 +77,55 @@ var LocalStatsCollector = (function() { }; /** - * Converts frequency data array to audio level. - * @param array the frequency data array. + * Converts time domain data array to audio level. + * @param array the time domain data array. * @returns {number} the audio level */ - var FrequencyDataToAudioLevel = function (array) { + var TimeDomainDataToAudioLevel = function (samples) { + var maxVolume = 0; - var length = array.length; + var length = samples.length; for (var i = 0; i < length; i++) { - if (maxVolume < array[i]) - maxVolume = array[i]; + if (maxVolume < samples[i]) + maxVolume = samples[i]; } - return maxVolume / 255; + return parseFloat(((maxVolume - 127) / 128).toFixed(3)); }; + /** + * Animates audio level change + * @param newLevel the new audio level + * @param lastLevel the last audio level + * @returns {Number} the audio level to be set + */ + function animateLevel(newLevel, lastLevel) + { + var value = 0; + var diff = lastLevel - newLevel; + if(diff > 0.2) + { + value = lastLevel - 0.2; + } + else if(diff < -0.4) + { + value = lastLevel + 0.4; + } + else + { + value = newLevel; + } + + return parseFloat(value.toFixed(3)); + } + + /** + * Indicates that this audio level is for local jid. + * @type {string} + */ + LocalStatsCollectorProto.LOCAL_JID = 'local'; + return LocalStatsCollectorProto; })(); \ No newline at end of file diff --git a/rtp_stats.js b/rtp_stats.js index 8bfdcbc38..dafc04832 100644 --- a/rtp_stats.js +++ b/rtp_stats.js @@ -213,6 +213,8 @@ StatsCollector.prototype.processReport = function () // but it seems to vary between 0 and around 32k. audioLevel = audioLevel / 32767; jidStats.setSsrcAudioLevel(ssrc, audioLevel); + if(jid != connection.emuc.myroomjid) + this.updateCallback(jid, audioLevel); } var key = 'packetsReceived'; @@ -281,7 +283,5 @@ StatsCollector.prototype.processReport = function () // bar indicator //console.info("Loss SMA3: " + outputAvg + " Q: " + quality); } - - self.updateCallback(self); }; From 0e7c1ed9a9f86a6516dabd281915a5f142d83da0 Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Mon, 28 Jul 2014 14:31:32 +0300 Subject: [PATCH 17/18] Fixes the local audio levels when the user is muted. --- app.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app.js b/app.js index 9cacb7b37..f7ac1bd53 100644 --- a/app.js +++ b/app.js @@ -455,6 +455,8 @@ function audioLevelUpdated(jid, audioLevel) if(jid === LocalStatsCollector.LOCAL_JID) { resourceJid = AudioLevels.LOCAL_LEVEL; + if(isAudioMuted()) + return; } else { @@ -901,6 +903,20 @@ function toggleAudio() { buttonClick("#mute", "icon-microphone icon-mic-disabled"); } +/** + * Checks whether the audio is muted or not. + * @returns {boolean} true if audio is muted and false if not. + */ +function isAudioMuted() +{ + var localAudio = connection.jingle.localAudio; + for (var idx = 0; idx < localAudio.getAudioTracks().length; idx++) { + if(localAudio.getAudioTracks()[idx].enabled === true) + return false; + } + return true; +} + // Starts or stops the recording for the conference. function toggleRecording() { if (focus === null || focus.confid === null) { From 215ee92eb51f635c2a1cd5c1a58f92382e99bfc6 Mon Sep 17 00:00:00 2001 From: yanas Date: Mon, 28 Jul 2014 15:39:22 +0200 Subject: [PATCH 18/18] Adds rollover effect for focused videos. --- css/videolayout_default.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/css/videolayout_default.css b/css/videolayout_default.css index 145fcf616..3a33e0269 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -62,6 +62,11 @@ border: 2px solid #006d91; } +#remoteVideos .videocontainer.videoContainerFocused:hover { + -webkit-box-shadow: inset 0 0 5px #FFFFFF, 0 0 10px #FFFFFF, inset 0 0 60px #006d91; + border: 2px solid #FFFFFF; +} + #localVideoWrapper { display:inline-block; -webkit-mask-box-image: url(../images/videomask.svg);