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 || {}));