diff --git a/app.js b/app.js index 0f7f53aca..897b9d66e 100644 --- a/app.js +++ b/app.js @@ -497,7 +497,8 @@ function startRtpStatsCollector() if (config.enableRtpStats) { statsCollector = new StatsCollector( - getConferenceHandler().peerconnection, 200, audioLevelUpdated); + getConferenceHandler().peerconnection, 200, audioLevelUpdated, 2000, + ConnectionQuality.updateLocalStats); statsCollector.start(); } } @@ -511,6 +512,7 @@ function stopRTPStatsCollector() { statsCollector.stop(); statsCollector = null; + ConnectionQuality.stopSendingStats(); } } @@ -728,6 +730,7 @@ $(document).bind('left.muc', function (event, jid) { var container = document.getElementById( 'participant_' + Strophe.getResourceFromJid(jid)); if (container) { + VideoLayout.removeConnectionIndicator(jid); // hide here, wait for video to close before removing $(container).hide(); VideoLayout.resizeThumbnails(); diff --git a/connectionquality.js b/connectionquality.js new file mode 100644 index 000000000..7e7322609 --- /dev/null +++ b/connectionquality.js @@ -0,0 +1,121 @@ +var ConnectionQuality = (function () { + + /** + * Constructs new ConnectionQuality object + * @constructor + */ + function ConnectionQuality() { + + } + + /** + * local stats + * @type {{}} + */ + var stats = {}; + + /** + * remote stats + * @type {{}} + */ + var remoteStats = {}; + + /** + * Interval for sending statistics to other participants + * @type {null} + */ + var sendIntervalId = null; + + /** + * Updates the local statistics + * @param data new statistics + */ + ConnectionQuality.updateLocalStats = function (data) { + stats = data; + VideoLayout.updateLocalConnectionStats(100 - stats.packetLoss.total,stats); + if(sendIntervalId == null) + { + startSendingStats(); + } + }; + + /** + * Start statistics sending. + */ + function startSendingStats() { + sendStats(); + sendIntervalId = setInterval(sendStats, 10000); + } + + /** + * Sends statistics to other participants + */ + function sendStats() { + connection.emuc.addConnectionInfoToPresence(convertToMUCStats(stats)); + connection.emuc.sendPresence(); + } + + /** + * Converts statistics to format for sending through XMPP + * @param stats the statistics + * @returns {{bitrate_donwload: *, bitrate_uplpoad: *, packetLoss_total: *, packetLoss_download: *, packetLoss_upload: *}} + */ + function convertToMUCStats(stats) { + return { + "bitrate_donwload": stats.bitrate.download, + "bitrate_uplpoad": stats.bitrate.upload, + "packetLoss_total": stats.packetLoss.total, + "packetLoss_download": stats.packetLoss.download, + "packetLoss_upload": stats.packetLoss.upload + }; + } + + /** + * Converts statitistics to format used by VideoLayout + * @param stats + * @returns {{bitrate: {download: *, upload: *}, packetLoss: {total: *, download: *, upload: *}}} + */ + function parseMUCStats(stats) { + return { + bitrate: { + download: stats.bitrate_donwload, + upload: stats.bitrate_uplpoad + }, + packetLoss: { + total: stats.packetLoss_total, + download: stats.packetLoss_download, + upload: stats.packetLoss_upload + } + }; + } + + /** + * Updates remote statistics + * @param jid the jid associated with the statistics + * @param data the statistics + */ + ConnectionQuality.updateRemoteStats = function (jid, data) { + if(data == null || data.packetLoss_total == null) + { + VideoLayout.updateConnectionStats(jid, null, null); + return; + } + remoteStats[jid] = parseMUCStats(data); + console.log(remoteStats[jid]); + + VideoLayout.updateConnectionStats(jid, 100 - data.packetLoss_total,remoteStats[jid]); + + }; + + /** + * Stops statistics sending. + */ + ConnectionQuality.stopSendingStats = function () { + clearInterval(sendIntervalId); + sendIntervalId = null; + //notify UI about stopping statistics gathering + VideoLayout.onStatsStop(); + }; + + return ConnectionQuality; +})(); \ No newline at end of file diff --git a/css/font.css b/css/font.css index 315e40efa..a04848cee 100644 --- a/css/font.css +++ b/css/font.css @@ -103,6 +103,12 @@ .icon-reload:before { content: "\e618"; } + .icon-filmstrip:before { content: "\e619"; +} + +.icon-connection:before { + line-height: normal; + content: "\e61a"; } \ No newline at end of file diff --git a/css/jitsi_popover.css b/css/jitsi_popover.css new file mode 100644 index 000000000..0489dd217 --- /dev/null +++ b/css/jitsi_popover.css @@ -0,0 +1,103 @@ +.jitsipopover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + max-width: 300px; + min-width: 100px; + padding: 1px; + text-align: left; + color: #333333; + background-color: #ffffff; + background-clip: padding-box; + border: 1px solid #cccccc; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + /*-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);*/ + /*box-shadow: 0 5px 10px rgba(0, 0, 0, 0.4);*/ + white-space: normal; + margin-top: -10px; + margin-bottom: 35px; +} + +.jitsipopover.black +{ + background-color: rgba(0,0,0,0.8); + color: #ffffff; +} + +.jitsipopover-content { + padding: 9px 14px; + font-size: 10pt; + white-space:pre-wrap; + text-align: center; +} + +.jitsipopover > .arrow, +.jitsipopover > .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.jitsipopover > .arrow { + border-width: 11px; + left: 50%; + margin-left: -11px; + border-bottom-width: 0; + border-top-color: #999999; + border-top-color: rgba(0, 0, 0, 0.25); + bottom: -11px; +} +.jitsipopover > .arrow:after { + border-width: 10px; + content: " "; + bottom: 1px; + margin-left: -10px; + border-bottom-width: 0; + border-top-color: #ffffff; +} + +.jitsipopover.black > .arrow:after +{ + border-top-color: rgba(0, 0, 0, 0.8); +} + +.jitsiPopupmenuPadding { + height: 35px; + width: 100px; + position: absolute; + bottom: -35; +} + +.jitsipopover_green +{ + color: #4abd04; +} + +.jitsipopover_orange +{ + color: #ffa800; +} + +.jitsipopover_blue +{ + color: #06a5df; +} + +.jitsipopover_showmore +{ + background-color: #06a5df; + color: #ffffff; + cursor: pointer; + border-radius: 3px; + text-align: center; + width: 90px; + height: 16px; + padding-top: 4px; + margin: 15px auto 0px auto; +} diff --git a/css/videolayout_default.css b/css/videolayout_default.css index db2867ed7..124181d3b 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -197,6 +197,50 @@ border-radius:20px; } +.connectionindicator +{ + display: inline-block; + position: absolute; + top: 7px; + right: 0; + padding: 0px 5px; + z-index: 3; + width: 18px; + height: 13px; +} + +.connection.connection_empty +{ + color: #8B8B8B;/*#FFFFFF*/ + overflow: hidden; +} + +.connection.connection_full +{ + color: #FFFFFF;/*#15A1ED*/ + overflow: hidden; +} + +.connection +{ + position: absolute; + left: 0px; + top: 0px; + font-size: 8pt; + text-shadow: 1px 1px 1px rgba(0,0,0,1), -1px -1px -1px rgba(0,0,0,1); + border: 0px; + width: 18px; + height: 13px; +} + +.connection_info +{ + text-align: left; + font-size: 11px; + white-space:nowrap; + /*width: 260px;*/ +} + #localVideoContainer>span.status:hover, #localVideoContainer>span.displayname:hover { cursor: text; @@ -233,7 +277,7 @@ position: absolute; color: #FFFFFF; top: 0; - right: 0; + right: 23px; padding: 8px 5px; width: 25px; font-size: 8pt; diff --git a/fonts/jitsi.eot b/fonts/jitsi.eot index 9df08a5bf..d7e56378b 100755 Binary files a/fonts/jitsi.eot and b/fonts/jitsi.eot differ diff --git a/fonts/jitsi.svg b/fonts/jitsi.svg index 5eb8c8541..d0fb3e7b4 100755 --- a/fonts/jitsi.svg +++ b/fonts/jitsi.svg @@ -33,4 +33,5 @@ + \ No newline at end of file diff --git a/fonts/jitsi.ttf b/fonts/jitsi.ttf index ac19cf33c..89d81f008 100755 Binary files a/fonts/jitsi.ttf and b/fonts/jitsi.ttf differ diff --git a/fonts/jitsi.woff b/fonts/jitsi.woff index f47286076..1a8bf8c86 100755 Binary files a/fonts/jitsi.woff and b/fonts/jitsi.woff differ diff --git a/fonts/selection.json b/fonts/selection.json index de562a472..12a9c95cf 100755 --- a/fonts/selection.json +++ b/fonts/selection.json @@ -1,6 +1,76 @@ { "IcoMoonType": "selection", "icons": [ + { + "icon": { + "paths": [ + "M3.881 813.165h220.26v210.835h-220.26v-210.835z", + "M308.817 609.857h220.27v414.143h-220.27v-414.143z", + "M613.764 406.588h220.268v617.412h-220.268v-617.412z", + "M918.685 203.285h220.265v820.715h-220.265v-820.715z", + "M1223.629 0h220.263v1024h-220.263v-1024z" + ], + "attrs": [ + { + "opacity": 1, + "visibility": false + }, + { + "opacity": 1, + "visibility": false + }, + { + "opacity": 1, + "visibility": false + }, + { + "opacity": 1, + "visibility": false + }, + { + "opacity": 1, + "visibility": false + } + ], + "width": 1444, + "grid": 0, + "tags": [ + "connection-2" + ] + }, + "attrs": [ + { + "opacity": 1, + "visibility": false + }, + { + "opacity": 1, + "visibility": false + }, + { + "opacity": 1, + "visibility": false + }, + { + "opacity": 1, + "visibility": false + }, + { + "opacity": 1, + "visibility": false + } + ], + "properties": { + "order": 27, + "id": 31, + "prevSize": 32, + "code": 58906, + "name": "connection", + "ligatures": "" + }, + "setIdx": 0, + "iconIdx": 0 + }, { "icon": { "paths": [ @@ -13,7 +83,7 @@ "grid": 0 }, "properties": { - "order": 26, + "order": 25, "id": 29, "prevSize": 32, "code": 58905, @@ -21,7 +91,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 0 + "iconIdx": 1 }, { "icon": { @@ -37,7 +107,7 @@ "grid": 0 }, "properties": { - "order": 25, + "order": 24, "id": 28, "prevSize": 32, "code": 58904, @@ -45,7 +115,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 1 + "iconIdx": 2 }, { "icon": { @@ -59,7 +129,7 @@ "grid": 0 }, "properties": { - "order": 24, + "order": 23, "id": 27, "prevSize": 32, "code": 58903, @@ -67,7 +137,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 2 + "iconIdx": 3 }, { "icon": { @@ -81,7 +151,7 @@ "grid": 0 }, "properties": { - "order": 22, + "order": 21, "id": 26, "prevSize": 32, "code": 58901, @@ -89,7 +159,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 3 + "iconIdx": 4 }, { "icon": { @@ -104,7 +174,7 @@ "grid": 0 }, "properties": { - "order": 23, + "order": 22, "id": 25, "prevSize": 32, "code": 58902, @@ -112,7 +182,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 4 + "iconIdx": 5 }, { "icon": { @@ -127,7 +197,7 @@ "grid": 0 }, "properties": { - "order": 18, + "order": 17, "id": 24, "prevSize": 32, "code": 58897, @@ -135,7 +205,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 5 + "iconIdx": 6 }, { "icon": { @@ -150,7 +220,7 @@ "grid": 0 }, "properties": { - "order": 19, + "order": 18, "id": 23, "prevSize": 32, "code": 58898, @@ -158,7 +228,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 6 + "iconIdx": 7 }, { "icon": { @@ -174,7 +244,7 @@ "grid": 0 }, "properties": { - "order": 20, + "order": 19, "id": 22, "prevSize": 32, "code": 58899, @@ -182,7 +252,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 7 + "iconIdx": 8 }, { "icon": { @@ -199,7 +269,7 @@ "grid": 0 }, "properties": { - "order": 21, + "order": 20, "id": 21, "prevSize": 32, "code": 58900, @@ -207,7 +277,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 8 + "iconIdx": 9 }, { "icon": { @@ -222,7 +292,7 @@ "grid": 0 }, "properties": { - "order": 17, + "order": 16, "id": 20, "prevSize": 32, "code": 58895, @@ -230,7 +300,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 9 + "iconIdx": 10 }, { "icon": { @@ -246,7 +316,7 @@ "grid": 0 }, "properties": { - "order": 16, + "order": 15, "id": 19, "prevSize": 32, "code": 58896, @@ -254,7 +324,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 10 + "iconIdx": 11 }, { "icon": { @@ -270,7 +340,7 @@ "grid": 0 }, "properties": { - "order": 15, + "order": 14, "id": 18, "prevSize": 32, "code": 58882, @@ -278,7 +348,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 11 + "iconIdx": 12 }, { "icon": { @@ -292,7 +362,7 @@ "grid": 0 }, "properties": { - "order": 14, + "order": 13, "id": 17, "prevSize": 32, "code": 58886, @@ -300,7 +370,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 12 + "iconIdx": 13 }, { "icon": { @@ -316,7 +386,7 @@ "grid": 0 }, "properties": { - "order": 13, + "order": 12, "id": 16, "prevSize": 32, "code": 58893, @@ -324,7 +394,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 13 + "iconIdx": 14 }, { "icon": { @@ -340,7 +410,7 @@ "grid": 0 }, "properties": { - "order": 12, + "order": 11, "id": 15, "prevSize": 32, "code": 58894, @@ -348,7 +418,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 14 + "iconIdx": 15 }, { "icon": { @@ -367,7 +437,7 @@ "grid": 0 }, "properties": { - "order": 11, + "order": 10, "id": 14, "prevSize": 32, "code": 58892, @@ -375,7 +445,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 15 + "iconIdx": 16 }, { "icon": { @@ -410,7 +480,7 @@ } ], "properties": { - "order": 27, + "order": 26, "id": 30, "prevSize": 32, "code": 58880, @@ -418,7 +488,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 16 + "iconIdx": 17 }, { "icon": { @@ -442,7 +512,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 17 + "iconIdx": 18 }, { "icon": { @@ -467,7 +537,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 18 + "iconIdx": 19 }, { "icon": { @@ -489,7 +559,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 19 + "iconIdx": 20 }, { "icon": { @@ -512,7 +582,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 20 + "iconIdx": 21 }, { "icon": { @@ -526,7 +596,7 @@ "grid": 0 }, "properties": { - "order": 6, + "order": 5, "id": 5, "prevSize": 32, "code": 58887, @@ -534,7 +604,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 21 + "iconIdx": 22 }, { "icon": { @@ -549,7 +619,7 @@ "grid": 0 }, "properties": { - "order": 7, + "order": 6, "id": 4, "prevSize": 32, "code": 58888, @@ -557,7 +627,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 22 + "iconIdx": 23 }, { "icon": { @@ -572,7 +642,7 @@ "grid": 0 }, "properties": { - "order": 8, + "order": 7, "id": 3, "prevSize": 32, "code": 58889, @@ -580,7 +650,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 23 + "iconIdx": 24 }, { "icon": { @@ -596,7 +666,7 @@ "grid": 0 }, "properties": { - "order": 9, + "order": 8, "id": 2, "prevSize": 32, "code": 58890, @@ -604,7 +674,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 24 + "iconIdx": 25 }, { "icon": { @@ -619,7 +689,7 @@ "grid": 0 }, "properties": { - "order": 10, + "order": 9, "id": 1, "prevSize": 32, "code": 58891, @@ -627,7 +697,7 @@ "ligatures": "" }, "setIdx": 0, - "iconIdx": 25 + "iconIdx": 26 } ], "height": 1024, diff --git a/index.html b/index.html index ee3d608b3..f18f66c17 100644 --- a/index.html +++ b/index.html @@ -48,6 +48,7 @@ + @@ -57,6 +58,7 @@ + @@ -66,6 +68,7 @@ + + diff --git a/jitsipopover.js b/jitsipopover.js new file mode 100644 index 000000000..d0619519c --- /dev/null +++ b/jitsipopover.js @@ -0,0 +1,115 @@ +var JitsiPopover = (function () { + /** + * Constructs new JitsiPopover and attaches it to the element + * @param element jquery selector + * @param options the options for the popover. + * @constructor + */ + function JitsiPopover(element, options) + { + this.options = { + skin: "white", + content: "" + }; + if(options) + { + if(options.skin) + this.options.skin = options.skin; + + if(options.content) + this.options.content = options.content; + } + + this.elementIsHovered = false; + this.popoverIsHovered = false; + this.popoverShown = false; + + element.data("jitsi_popover", this); + this.element = element; + this.template = '
' + + '
'; + var self = this; + this.element.on("mouseenter", function () { + self.elementIsHovered = true; + self.show(); + }).on("mouseleave", function () { + self.elementIsHovered = false; + setTimeout(function () { + self.hide(); + }, 10); + }); + } + + /** + * Shows the popover + */ + JitsiPopover.prototype.show = function () { + this.createPopover(); + this.popoverShown = true; + + }; + + /** + * Hides the popover + */ + JitsiPopover.prototype.hide = function () { + if(!this.elementIsHovered && !this.popoverIsHovered && this.popoverShown) + { + $(".jitsipopover").remove(); + this.popoverShown = false; + + } + }; + + /** + * Creates the popover html + */ + JitsiPopover.prototype.createPopover = function () { + $("body").append(this.template); + $(".jitsipopover > .jitsipopover-content").html(this.options.content); + var self = this; + $(".jitsipopover").on("mouseenter", function () { + self.popoverIsHovered = true; + }).on("mouseleave", function () { + self.popoverIsHovered = false; + self.hide(); + }); + + this.refreshPosition(); + }; + + /** + * Refreshes the position of the popover + */ + JitsiPopover.prototype.refreshPosition = function () { + $(".jitsipopover").position({ + my: "bottom", + at: "top", + collision: "fit", + of: this.element, + using: function (position, elements) { + var calcLeft = elements.target.left - elements.element.left + elements.target.width/2; + $(".jitsipopover").css({top: position.top, left: position.left, display: "block"}); + $(".jitsipopover > .arrow").css({left: calcLeft}); + $(".jitsipopover > .jitsiPopupmenuPadding").css({left: calcLeft - 50}); + } + }); + }; + + /** + * Updates the content of popover. + * @param content new content + */ + JitsiPopover.prototype.updateContent = function (content) { + this.options.content = content; + if(!this.popoverShown) + return; + $(".jitsipopover").remove(); + this.createPopover(); + }; + + return JitsiPopover; + + +})(); \ No newline at end of file diff --git a/local_sts.js b/local_sts.js index fbefa98c8..424b47d98 100644 --- a/local_sts.js +++ b/local_sts.js @@ -28,7 +28,7 @@ var LocalStatsCollector = (function() { this.stream = stream; this.intervalId = null; this.intervalMilis = interval; - this.updateCallback = updateCallback; + this.audioLevelsUpdateCallback = updateCallback; this.audioLevel = 0; } @@ -58,7 +58,7 @@ var LocalStatsCollector = (function() { var audioLevel = TimeDomainDataToAudioLevel(array); if(audioLevel != self.audioLevel) { self.audioLevel = animateLevel(audioLevel, self.audioLevel); - self.updateCallback(LocalStatsCollectorProto.LOCAL_JID, self.audioLevel); + self.audioLevelsUpdateCallback(LocalStatsCollectorProto.LOCAL_JID, self.audioLevel); } }, this.intervalMilis diff --git a/muc.js b/muc.js index e4d3ac7a3..3580716e4 100644 --- a/muc.js +++ b/muc.js @@ -94,6 +94,16 @@ Strophe.addConnectionPlugin('emuc', { $(document).trigger('videomuted.muc', [from, videoMuted.text()]); } + var stats = $(pres).find('>stats'); + if(stats.length) + { + var statsObj = {}; + Strophe.forEachChild(stats[0], "stat", function (el) { + statsObj[el.getAttribute("name")] = el.getAttribute("value"); + }); + ConnectionQuality.updateRemoteStats(from, statsObj); + } + // Parse status. if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="201"]').length) { // http://xmpp.org/extensions/xep-0045.html#createroom-instant @@ -319,6 +329,15 @@ Strophe.addConnectionPlugin('emuc', { .t(this.presMap['videomuted']).up(); } + if(this.presMap['statsns']) + { + var stats = pres.c('stats', {xmlns: this.presMap['statsns']}); + for(var stat in this.presMap["stats"]) + if(this.presMap["stats"][stat] != null) + stats.c("stat",{name: stat, value: this.presMap["stats"][stat]}).up(); + pres.up(); + } + if (this.presMap['prezins']) { pres.c('prezi', {xmlns: this.presMap['prezins'], @@ -401,6 +420,10 @@ Strophe.addConnectionPlugin('emuc', { this.presMap['videons'] = 'http://jitsi.org/jitmeet/video'; this.presMap['videomuted'] = isMuted.toString(); }, + addConnectionInfoToPresence: function(stats) { + this.presMap['statsns'] = 'http://jitsi.org/jitmeet/stats'; + this.presMap['stats'] = stats; + }, findJidFromResource: function(resourceJid) { var peerJid = null; Object.keys(this.members).some(function (jid) { diff --git a/rtp_sts.js b/rtp_sts.js index dafc04832..246d99025 100644 --- a/rtp_sts.js +++ b/rtp_sts.js @@ -1,34 +1,12 @@ /* global ssrc2jid */ - /** - * Function object which once created can be used to calculate moving average of - * given period. Example for SMA3:
- * var sma3 = new SimpleMovingAverager(3); - * while(true) // some update loop - * { - * var currentSma3Value = sma3(nextInputValue); - * } - * - * @param period moving average period that will be used by created instance. - * @returns {Function} SMA calculator function of given period. - * @constructor + * Calculates packet lost percent using the number of lost packets and the number of all packet. + * @param lostPackets the number of lost packets + * @param totalPackets the number of all packets. + * @returns {number} packet loss percent */ -function SimpleMovingAverager(period) -{ - var nums = []; - return function (num) - { - nums.push(num); - if (nums.length > period) - nums.splice(0, 1); - var sum = 0; - for (var i in nums) - sum += nums[i]; - var n = period; - if (nums.length < period) - n = nums.length; - return (sum / n); - }; +function calculatePacketLoss(lostPackets, totalPackets) { + return Math.round((lostPackets/totalPackets)*100); } /** @@ -39,8 +17,30 @@ function PeerStats() { this.ssrc2Loss = {}; this.ssrc2AudioLevel = {}; + this.ssrc2bitrate = {}; + this.resolution = null; } +/** + * The bandwidth + * @type {{}} + */ +PeerStats.bandwidth = {}; + +/** + * The bit rate + * @type {{}} + */ +PeerStats.bitrate = {}; + + + +/** + * The packet loss rate + * @type {{}} + */ +PeerStats.packetLoss = null; + /** * Sets packets loss rate for given ssrc that blong to the peer * represented by this instance. @@ -52,6 +52,17 @@ PeerStats.prototype.setSsrcLoss = function (ssrc, lossRate) this.ssrc2Loss[ssrc] = lossRate; }; +/** + * Sets the bit rate for given ssrc that blong to the peer + * represented by this instance. + * @param ssrc audio or video RTP stream SSRC. + * @param bitrate new bitrate value to be set. + */ +PeerStats.prototype.setSsrcBitrate = function (ssrc, bitrate) +{ + this.ssrc2bitrate[ssrc] = bitrate; +}; + /** * Sets new audio level(input or output) for given ssrc that identifies * the stream which belongs to the peer represented by this instance. @@ -67,52 +78,42 @@ PeerStats.prototype.setSsrcAudioLevel = function (ssrc, audioLevel) }; /** - * Calculates average packet loss for all streams that belong to the peer - * represented by this instance. - * @returns {number} average packet loss for all streams that belong to the peer - * represented by this instance. + * Array with the transport information. + * @type {Array} */ -PeerStats.prototype.getAvgLoss = function () -{ - var self = this; - var avg = 0; - var count = Object.keys(this.ssrc2Loss).length; - Object.keys(this.ssrc2Loss).forEach( - function (ssrc) - { - avg += self.ssrc2Loss[ssrc]; - } - ); - return count > 0 ? avg / count : 0; -}; +PeerStats.transport = []; /** * StatsCollector registers for stats updates of given * peerconnection in given interval. On each update particular * stats are extracted and put in {@link PeerStats} objects. Once the processing - * is done updateCallback is called with this instance as + * is done audioLevelsUpdateCallback is called with this instance as * an event source. * * @param peerconnection webRTC peer connection object. * @param interval stats refresh interval given in ms. - * @param {function(StatsCollector)} updateCallback the callback called on stats + * @param {function(StatsCollector)} audioLevelsUpdateCallback the callback called on stats * update. * @constructor */ -function StatsCollector(peerconnection, interval, updateCallback) +function StatsCollector(peerconnection, audioLevelsInterval, audioLevelsUpdateCallback, statsInterval, statsUpdateCallback) { this.peerconnection = peerconnection; - this.baselineReport = null; - this.currentReport = null; - this.intervalId = null; + this.baselineAudioLevelsReport = null; + this.currentAudioLevelsReport = null; + this.currentStatsReport = null; + this.baselineStatsReport = null; + this.audioLevelsIntervalId = null; // Updates stats interval - this.intervalMilis = interval; - // Use SMA 3 to average packet loss changes over time - this.sma3 = new SimpleMovingAverager(3); + this.audioLevelsIntervalMilis = audioLevelsInterval; + + this.statsIntervalId = null; + this.statsIntervalMilis = statsInterval; // Map of jids to PeerStats this.jid2stats = {}; - this.updateCallback = updateCallback; + this.audioLevelsUpdateCallback = audioLevelsUpdateCallback; + this.statsUpdateCallback = statsUpdateCallback; } /** @@ -120,10 +121,12 @@ function StatsCollector(peerconnection, interval, updateCallback) */ StatsCollector.prototype.stop = function () { - if (this.intervalId) + if (this.audioLevelsIntervalId) { - clearInterval(this.intervalId); - this.intervalId = null; + clearInterval(this.audioLevelsIntervalId); + this.audioLevelsIntervalId = null; + clearInterval(this.statsIntervalId); + this.statsIntervalId = null; } }; @@ -143,7 +146,7 @@ StatsCollector.prototype.errorCallback = function (error) StatsCollector.prototype.start = function () { var self = this; - this.intervalId = setInterval( + this.audioLevelsIntervalId = setInterval( function () { // Interval updates @@ -152,36 +155,252 @@ StatsCollector.prototype.start = function () { var results = report.result(); //console.error("Got interval report", results); - self.currentReport = results; - self.processReport(); - self.baselineReport = self.currentReport; + self.currentAudioLevelsReport = results; + self.processAudioLevelReport(); + self.baselineAudioLevelsReport = self.currentAudioLevelsReport; }, self.errorCallback ); }, - self.intervalMilis + self.audioLevelsIntervalMilis + ); + + this.statsIntervalId = setInterval( + function () { + // Interval updates + self.peerconnection.getStats( + function (report) + { + var results = report.result(); + //console.error("Got interval report", results); + self.currentStatsReport = results; + self.processStatsReport(); + self.baselineStatsReport = self.currentStatsReport; + }, + self.errorCallback + ); + }, + self.statsIntervalMilis ); }; + /** * Stats processing logic. */ -StatsCollector.prototype.processReport = function () +StatsCollector.prototype.processStatsReport = function () { + if (!this.baselineStatsReport) { + return; + } + + for (var idx in this.currentStatsReport) { + var now = this.currentStatsReport[idx]; + if (now.stat('googAvailableReceiveBandwidth') || now.stat('googAvailableSendBandwidth')) { + PeerStats.bandwidth = { + "download": Math.round((now.stat('googAvailableReceiveBandwidth') * 8) / 1000), + "upload": Math.round((now.stat('googAvailableSendBandwidth') * 8) / 1000) + }; + } + + if(now.type == 'googCandidatePair') + { + var ip = now.stat('googRemoteAddress'); + var type = now.stat("googTransportType"); + if(!ip || !type) + continue; + var addressSaved = false; + for(var i = 0; i < PeerStats.transport.length; i++) + { + if(PeerStats.transport[i].ip == ip && PeerStats.transport[i].type == type) + { + addressSaved = true; + } + } + if(addressSaved) + continue; + PeerStats.transport.push({ip: ip, type: type}); + continue; + } + +// console.log("bandwidth: " + now.stat('googAvailableReceiveBandwidth') + " - " + now.stat('googAvailableSendBandwidth')); + if (now.type != 'ssrc') { + continue; + } + + var before = this.baselineStatsReport[idx]; + if (!before) { + console.warn(now.stat('ssrc') + ' not enough data'); + continue; + } + + var ssrc = now.stat('ssrc'); + var jid = ssrc2jid[ssrc]; + if (!jid) { + console.warn("No jid for ssrc: " + ssrc); + continue; + } + + var jidStats = this.jid2stats[jid]; + if (!jidStats) { + jidStats = new PeerStats(); + this.jid2stats[jid] = jidStats; + } + + + var isDownloadStream = true; + var key = 'packetsReceived'; + if (!now.stat(key)) + { + isDownloadStream = false; + key = 'packetsSent'; + if (!now.stat(key)) + { + console.error("No packetsReceived nor packetSent stat found"); + this.stop(); + return; + } + } + var packetsNow = now.stat(key); + var packetsBefore = before.stat(key); + var packetRate = packetsNow - packetsBefore; + + var currentLoss = now.stat('packetsLost'); + var previousLoss = before.stat('packetsLost'); + var lossRate = currentLoss - previousLoss; + + var packetsTotal = (packetRate + lossRate); + + jidStats.setSsrcLoss(ssrc, {"packetsTotal": packetsTotal, "packetsLost": lossRate, + "isDownloadStream": isDownloadStream}); + + var bytesReceived = 0, bytesSent = 0; + if(now.stat("bytesReceived")) + { + bytesReceived = now.stat("bytesReceived") - before.stat("bytesReceived"); + } + + if(now.stat("bytesSent")) + { + bytesSent = now.stat("bytesSent") - before.stat("bytesSent"); + } + + if(bytesReceived < 0) + bytesReceived = 0; + if(bytesSent < 0) + bytesSent = 0; + + var time = Math.round((now.timestamp - before.timestamp) / 1000); + jidStats.setSsrcBitrate(ssrc, { + "download": Math.round(((bytesReceived * 8) / time) / 1000), + "upload": Math.round(((bytesSent * 8) / time) / 1000)}); + var resolution = {height: null, width: null}; + if(now.stat("googFrameHeightReceived") && now.stat("googFrameWidthReceived")) + { + resolution.height = now.stat("googFrameHeightReceived"); + resolution.width = now.stat("googFrameWidthReceived"); + } + else if(now.stat("googFrameHeightSent") && now.stat("googFrameWidthSent")) + { + resolution.height = now.stat("googFrameHeightSent"); + resolution.width = now.stat("googFrameWidthSent"); + } + + if(!jidStats.resolution) + jidStats.resolution = null; + + console.log(jid + " - resolution: " + resolution.height + "x" + resolution.width); + if(resolution.height && resolution.width) + { + if(!jidStats.resolution) + jidStats.resolution = { hq: resolution, lq: resolution}; + else if(jidStats.resolution.hq.width > resolution.width && + jidStats.resolution.hq.height > resolution.height) + { + jidStats.resolution.lq = resolution; + } + else + { + jidStats.resolution.hq = resolution; + } + } + + + } + + var self = this; + // Jid stats + var totalPackets = {download: 0, upload: 0}; + var lostPackets = {download: 0, upload: 0}; + var bitrateDownload = 0; + var bitrateUpload = 0; + var resolution = {}; + Object.keys(this.jid2stats).forEach( + function (jid) + { + Object.keys(self.jid2stats[jid].ssrc2Loss).forEach( + function (ssrc) + { + var type = "upload"; + if(self.jid2stats[jid].ssrc2Loss[ssrc].isDownloadStream) + type = "download"; + totalPackets[type] += self.jid2stats[jid].ssrc2Loss[ssrc].packetsTotal; + lostPackets[type] += self.jid2stats[jid].ssrc2Loss[ssrc].packetsLost; + } + ); + Object.keys(self.jid2stats[jid].ssrc2bitrate).forEach( + function (ssrc) { + bitrateDownload += self.jid2stats[jid].ssrc2bitrate[ssrc].download; + bitrateUpload += self.jid2stats[jid].ssrc2bitrate[ssrc].upload; + } + ); + resolution[jid] = self.jid2stats[jid].resolution; + delete self.jid2stats[jid].resolution; + } + ); + + PeerStats.bitrate = {"upload": bitrateUpload, "download": bitrateDownload}; + + PeerStats.packetLoss = { + total: + calculatePacketLoss(lostPackets.download + lostPackets.upload, + totalPackets.download + totalPackets.upload), + download: + calculatePacketLoss(lostPackets.download, totalPackets.download), + upload: + calculatePacketLoss(lostPackets.upload, totalPackets.upload) + }; + this.statsUpdateCallback( + { + "bitrate": PeerStats.bitrate, + "packetLoss": PeerStats.packetLoss, + "bandwidth": PeerStats.bandwidth, + "resolution": resolution, + "transport": PeerStats.transport + }); + PeerStats.transport = []; + +} + +/** + * Stats processing logic. + */ +StatsCollector.prototype.processAudioLevelReport = function () { - if (!this.baselineReport) + if (!this.baselineAudioLevelsReport) { return; } - for (var idx in this.currentReport) + for (var idx in this.currentAudioLevelsReport) { - var now = this.currentReport[idx]; + var now = this.currentAudioLevelsReport[idx]; + if (now.type != 'ssrc') { continue; } - var before = this.baselineReport[idx]; + var before = this.baselineAudioLevelsReport[idx]; if (!before) { console.warn(now.stat('ssrc') + ' not enough data'); @@ -214,74 +433,10 @@ StatsCollector.prototype.processReport = function () audioLevel = audioLevel / 32767; jidStats.setSsrcAudioLevel(ssrc, audioLevel); if(jid != connection.emuc.myroomjid) - this.updateCallback(jid, audioLevel); + this.audioLevelsUpdateCallback(jid, audioLevel); } - var key = 'packetsReceived'; - if (!now.stat(key)) - { - key = 'packetsSent'; - if (!now.stat(key)) - { - console.error("No packetsReceived nor packetSent stat found"); - this.stop(); - return; - } - } - var packetsNow = now.stat(key); - var packetsBefore = before.stat(key); - var packetRate = packetsNow - packetsBefore; - - var currentLoss = now.stat('packetsLost'); - var previousLoss = before.stat('packetsLost'); - var lossRate = currentLoss - previousLoss; - - var packetsTotal = (packetRate + lossRate); - var lossPercent; - - if (packetsTotal > 0) - lossPercent = lossRate / packetsTotal; - else - lossPercent = 0; - - //console.info(jid + " ssrc: " + ssrc + " " + key + ": " + packetsNow); - - jidStats.setSsrcLoss(ssrc, lossPercent); } - var self = this; - // Jid stats - var allPeersAvg = 0; - var jids = Object.keys(this.jid2stats); - jids.forEach( - function (jid) - { - var peerAvg = self.jid2stats[jid].getAvgLoss( - function (avg) - { - //console.info(jid + " stats: " + (avg * 100) + " %"); - allPeersAvg += avg; - } - ); - } - ); - if (jids.length > 1) - { - // Our streams loss is reported as 0 always, so -1 to length - allPeersAvg = allPeersAvg / (jids.length - 1); - - /** - * Calculates number of connection quality bars from 4(hi) to 0(lo). - */ - var outputAvg = self.sma3(allPeersAvg); - // Linear from 4(0%) to 0(25%). - var quality = Math.round(4 - outputAvg * 16); - quality = Math.max(quality, 0); // lower limit 0 - quality = Math.min(quality, 4); // upper limit 4 - // TODO: quality can be used to indicate connection quality using 4 step - // bar indicator - //console.info("Loss SMA3: " + outputAvg + " Q: " + quality); - } }; - diff --git a/videolayout.js b/videolayout.js index 448f0e7d2..36acb1e48 100644 --- a/videolayout.js +++ b/videolayout.js @@ -6,6 +6,7 @@ var VideoLayout = (function (my) { updateInProgress: false, newSrc: '' }; + my.connectionIndicators = {}; my.changeLocalAudio = function(stream) { connection.jingle.localAudio = stream; @@ -30,6 +31,11 @@ var VideoLayout = (function (my) { // Set default display name. setDisplayName('localVideoContainer'); + if(!VideoLayout.connectionIndicators["localVideoContainer"]) { + VideoLayout.connectionIndicators["localVideoContainer"] + = new ConnectionIndicator($("#localVideoContainer")[0]); + } + AudioLevels.updateAudioLevelCanvas(); var localVideoSelector = $('#' + localVideo.id); @@ -175,15 +181,42 @@ var VideoLayout = (function (my) { if (largeVideoState.oldJid) { var oldResourceJid = Strophe.getResourceFromJid(largeVideoState.oldJid); VideoLayout.enableDominantSpeaker(oldResourceJid, false); + if(VideoLayout.connectionIndicators) { + var videoContainerId = null; + if (oldResourceJid == Strophe.getResourceFromJid(connection.emuc.myroomjid)) { + videoContainerId = 'localVideoContainer'; + } + else { + videoContainerId = 'participant_' + oldResourceJid; + } + if(VideoLayout.connectionIndicators[videoContainerId]) + VideoLayout.connectionIndicators[videoContainerId].setShowHQ(false); + } + } // Enable new dominant speaker in the remote videos section. if (largeVideoState.userJid) { var resourceJid = Strophe.getResourceFromJid(largeVideoState.userJid); VideoLayout.enableDominantSpeaker(resourceJid, true); + if(VideoLayout.connectionIndicators) + { + var videoContainerId = null; + if (resourceJid + === Strophe.getResourceFromJid(connection.emuc.myroomjid)) { + videoContainerId = 'localVideoContainer'; + } + else { + videoContainerId = 'participant_' + resourceJid; + } + if(VideoLayout.connectionIndicators[videoContainerId]) + VideoLayout.connectionIndicators[videoContainerId].setShowHQ(true); + } + } largeVideoState.updateInProgress = false; + if (fade) { // using "this" should be ok because we're called // from within the fadeOut event. @@ -344,6 +377,8 @@ var VideoLayout = (function (my) { // Set default display name. setDisplayName(videoSpanId); + VideoLayout.connectionIndicators[videoSpanId] = new ConnectionIndicator(container); + var nickfield = document.createElement('span'); nickfield.className = "nick"; nickfield.appendChild(document.createTextNode(resourceJid)); @@ -504,8 +539,11 @@ var VideoLayout = (function (my) { if (!audioCount && !videoCount) { console.log("Remove whole user", container.id); + if(VideoLayout.connectionIndicators[container.id]) + VideoLayout.connectionIndicators[container.id].remove(); // Remove whole container container.remove(); + Util.playSoundNotification('userLeft'); VideoLayout.resizeThumbnails(); } @@ -526,7 +564,11 @@ var VideoLayout = (function (my) { if (!peerContainer.is(':visible') && isShow) peerContainer.show(); else if (peerContainer.is(':visible') && !isShow) + { peerContainer.hide(); + if(VideoLayout.connectionIndicators['participant_' + resourceJid]) + VideoLayout.connectionIndicators['participant_' + resourceJid].hide(); + } VideoLayout.resizeThumbnails(); @@ -758,7 +800,7 @@ var VideoLayout = (function (my) { } var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted'); videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted'); - videoMutedSpan.css({right: ((audioMutedSpan.length > 0)?'30px':'0px')}); + videoMutedSpan.css({right: ((audioMutedSpan.length > 0)?'50px':'30px')}); } }; @@ -1431,5 +1473,352 @@ var VideoLayout = (function (my) { }); }); + /** + * Constructs new connection indicator. + * @param videoContainer the video container associated with the indicator. + * @constructor + */ + function ConnectionIndicator(videoContainer) + { + this.videoContainer = videoContainer; + this.bandwidth = null; + this.packetLoss = null; + this.bitrate = null; + this.showMoreValue = false; + this.resolution = null; + this.transport = []; + this.popover = null; + this.showHQ = false; + this.create(); + } + + /** + * Values for the connection quality + * @type {{98: string, 81: string, 64: string, 47: string, 30: string, 0: string}} + */ + ConnectionIndicator.connectionQualityValues = { + 98: "18px", //full + 81: "15px",//4 bars + 64: "11px",//3 bars + 47: "7px",//2 bars + 30: "3px",//1 bar + 0: "0px"//empty + }; + + /** + * Sets the value of the property that indicates whether the displayed resolution si the + * resolution of High Quality stream or Low Quality + * @param value boolean. + */ + ConnectionIndicator.prototype.setShowHQ = function (value) { + this.showHQ = value; + this.updatePopoverData(); + }; + + /** + * Generates the html content. + * @returns {string} the html content. + */ + ConnectionIndicator.prototype.generateText = function () { + var downloadBitrate, uploadBitrate, packetLoss, resolution; + + if(this.bitrate === null) + { + downloadBitrate = "N/A"; + uploadBitrate = "N/A"; + } + else + { + downloadBitrate = this.bitrate.download? this.bitrate.download + " Kbps" : "N/A"; + uploadBitrate = this.bitrate.upload? this.bitrate.upload + " Kbps" : "N/A"; + } + + if(this.packetLoss === null) + { + packetLoss = "N/A"; + } + else + { + + packetLoss = "" + + (this.packetLoss.download != null? this.packetLoss.download : "N/A") + + "% " + + (this.packetLoss.upload != null? this.packetLoss.upload : "N/A") + "%"; + } + + var resolutionValue = null; + if(this.resolution) + { + if(this.showHQ && this.resolution.hq) + { + resolutionValue = this.resolution.hq; + } + else if(!this.showHQ && this.resolution.lq) + { + resolutionValue = this.resolution.lq; + } + } + + if(!resolutionValue || + !resolutionValue.height || + !resolutionValue.width) + { + resolution = "N/A"; + } + else + { + resolution = resolutionValue.width + "x" + resolutionValue.height; + } + + var result = "Bitrate: " + + downloadBitrate + " " + + uploadBitrate + "
" + + "Packet loss: " + packetLoss + "
" + + "Resolution: " + resolution + "
"; + + if(this.videoContainer.id == "localVideoContainer") + result += "
" + (this.showMoreValue? "Show less" : "Show More") + "

"; + + if(this.showMoreValue) + { + var downloadBandwidth, uploadBandwidth, transport; + if(this.bandwidth === null) + { + downloadBandwidth = "N/A"; + uploadBandwidth = "N/A"; + } + else + { + downloadBandwidth = this.bandwidth.download? this.bandwidth.download + " Kbps" : "N/A"; + uploadBandwidth = this.bandwidth.upload? this.bandwidth.upload + " Kbps" : "N/A"; + } + + if(!this.transport || this.transport.length === 0) + { + transport = "Address: N/A"; + } + else + { + transport = "Address: " + this.transport[0].ip.substring(0,this.transport[0].ip.indexOf(":")) + "
"; + if(this.transport.length > 1) + { + transport += "Ports: "; + } + else + { + transport += "Port: "; + } + for(var i = 0; i < this.transport.length; i++) + { + transport += ((i !== 0)? ", " : "") + + this.transport[i].ip.substring(this.transport[i].ip.indexOf(":")+1, + this.transport[i].ip.length); + } + transport += "
Transport: " + this.transport[0].type + "
"; + } + + result += "Estimated bandwidth: " + + "" + downloadBandwidth + + " " + + uploadBandwidth + "
"; + + result += transport; + + } + + return result; + }; + + /** + * Shows or hide the additional information. + */ + ConnectionIndicator.prototype.showMore = function () { + this.showMoreValue = !this.showMoreValue; + this.updatePopoverData(); + }; + + /** + * Creates the indicator + */ + ConnectionIndicator.prototype.create = function () { + this.connectionIndicatorContainer = document.createElement("div"); + this.connectionIndicatorContainer.className = "connectionindicator"; + this.connectionIndicatorContainer.style.display = "none"; + this.videoContainer.appendChild(this.connectionIndicatorContainer); + this.popover = new JitsiPopover($("#" + this.videoContainer.id + " > .connectionindicator"), + {content: "
Come back here for " + + "connection information once the conference starts
", skin: "black"}); + + function createIcon(classes) + { + var icon = document.createElement("span"); + for(var i in classes) + { + icon.classList.add(classes[i]); + } + icon.appendChild(document.createElement("i")).classList.add("icon-connection"); + return icon; + } + this.emptyIcon = this.connectionIndicatorContainer.appendChild( + createIcon(["connection", "connection_empty"])); + this.fullIcon = this.connectionIndicatorContainer.appendChild( + createIcon(["connection", "connection_full"])); + + }; + + /** + * Removes the indicator + */ + ConnectionIndicator.prototype.remove = function() + { + this.popover.hide(); + this.connectionIndicatorContainer.remove(); + + }; + + /** + * Updates the data of the indicator + * @param percent the percent of connection quality + * @param object the statistics data. + */ + ConnectionIndicator.prototype.updateConnectionQuality = function (percent, object) { + + if(percent === null) + { + this.connectionIndicatorContainer.style.display = "none"; + return; + } + else + { + this.connectionIndicatorContainer.style.display = "block"; + } + this.bandwidth = object.bandwidth; + this.bitrate = object.bitrate; + this.packetLoss = object.packetLoss; + this.transport = object.transport; + if(object.resolution) + { + this.resolution = object.resolution; + } + for(var quality in ConnectionIndicator.connectionQualityValues) + { + if(percent >= quality) + { + this.fullIcon.style.width = ConnectionIndicator.connectionQualityValues[quality]; + } + } + this.updatePopoverData(); + }; + + /** + * Updates the resolution + * @param resolution the new resolution + */ + ConnectionIndicator.prototype.updateResolution = function (resolution) { + this.resolution = resolution; + this.updatePopoverData(); + }; + + /** + * Updates the content of the popover + */ + ConnectionIndicator.prototype.updatePopoverData = function () { + this.popover.updateContent("
" + this.generateText() + "
"); + }; + + /** + * Hides the popover + */ + ConnectionIndicator.prototype.hide = function () { + this.popover.hide(); + }; + + /** + * Hides the indicator + */ + ConnectionIndicator.prototype.hideIndicator = function () { + this.connectionIndicatorContainer.style.display = "none"; + }; + + /** + * Updates the data for the indicator + * @param id the id of the indicator + * @param percent the percent for connection quality + * @param object the data + */ + function updateStatsIndicator(id, percent, object) { + if(VideoLayout.connectionIndicators[id]) + VideoLayout.connectionIndicators[id].updateConnectionQuality(percent, object); + } + + /** + * Updates local stats + * @param percent + * @param object + */ + my.updateLocalConnectionStats = function (percent, object) { + var resolution = null; + if(object.resolution !== null) + { + resolution = object.resolution; + object.resolution = resolution[connection.emuc.myroomjid]; + delete resolution[connection.emuc.myroomjid]; + } + updateStatsIndicator("localVideoContainer", percent, object); + for(var jid in resolution) + { + if(resolution[jid] === null) + continue; + var id = 'participant_' + Strophe.getResourceFromJid(jid); + if(VideoLayout.connectionIndicators[id]) + { + VideoLayout.connectionIndicators[id].updateResolution(resolution[jid]); + } + } + + }; + + /** + * Updates remote stats. + * @param jid the jid associated with the stats + * @param percent the connection quality percent + * @param object the stats data + */ + my.updateConnectionStats = function (jid, percent, object) { + var resourceJid = Strophe.getResourceFromJid(jid); + + var videoSpanId = 'participant_' + resourceJid; + updateStatsIndicator(videoSpanId, percent, object); + }; + + /** + * Removes the connection + * @param jid + */ + my.removeConnectionIndicator = function (jid) { + if(VideoLayout.connectionIndicators['participant_' + Strophe.getResourceFromJid(jid)]) + VideoLayout.connectionIndicators['participant_' + Strophe.getResourceFromJid(jid)].remove(); + }; + + /** + * Hides the connection indicator + * @param jid + */ + my.hideConnectionIndicator = function (jid) { + if(VideoLayout.connectionIndicators['participant_' + Strophe.getResourceFromJid(jid)]) + VideoLayout.connectionIndicators['participant_' + Strophe.getResourceFromJid(jid)].hide(); + }; + + /** + * Hides all the indicators + */ + my.onStatsStop = function () { + for(var indicator in VideoLayout.connectionIndicators) + { + VideoLayout.connectionIndicators[indicator].hideIndicator(); + } + }; + return my; }(VideoLayout || {}));