Implements connection quality indicator.

This commit is contained in:
hristoterezov 2014-10-16 18:11:26 +03:00
parent 6a12d817f8
commit dc60cfc52a
16 changed files with 1224 additions and 186 deletions

5
app.js
View File

@ -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();

121
connectionquality.js Normal file
View File

@ -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;
})();

View File

@ -103,6 +103,12 @@
.icon-reload:before {
content: "\e618";
}
.icon-filmstrip:before {
content: "\e619";
}
.icon-connection:before {
line-height: normal;
content: "\e61a";
}

103
css/jitsi_popover.css Normal file
View File

@ -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;
}

View File

@ -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;

Binary file not shown.

View File

@ -33,4 +33,5 @@
<glyph unicode="&#xe617;" d="M32.887 258.374c5.026 4.679 12.994 10.886 21.642 16.349 25.668 16.31 54.057 25.449 83.415 32.066 24.381 5.475 49.123 8.444 74.033 10.101 27.877 1.877 55.779 1.89 83.696 0.399 19.972-1.092 39.843-3.251 59.56-6.606 21.978-3.753 43.519-8.997 64.392-16.875 12.209-4.587 24.086-10.053 35.267-16.786 14.858-8.946 28.276-19.612 38.61-33.674 10.409-14.151 15.861-30.204 16.914-47.696 0.873-13.701 0.358-27.349-2.828-40.794-1.438-6.041-4.113-11.567-8.277-16.193-5.709-6.324-13.212-8.51-21.386-8.818-10.231-0.334-20.205 2.057-30.18 4.113-19.456 3.985-38.918 8.123-58.349 12.364-7.069 1.517-14.344 2.546-20.825 6.298-11.154 6.478-17.223 15.887-17.017 28.892 0.129 8.435 1.108 16.891 1.235 25.348 0.156 12.505-4.962 22.581-15.449 29.521-7.197 4.769-15.347 7.456-23.726 9.333-20.206 4.523-40.693 5.089-61.281 5.025-14.411-0.063-28.791-0.834-43.047-3.071-9.974-1.581-19.781-3.906-28.866-8.507-12.159-6.182-19.677-15.732-20.036-29.676-0.22-8.175 0.487-16.401 0.964-24.575 0.321-5.911-0.040-11.723-2.648-17.144-4.63-9.692-12.468-15.836-22.685-18.482-11.323-2.933-22.802-5.27-34.252-7.611-19.051-3.882-38.108-7.684-57.208-11.259-7.263-1.387-14.627-0.976-21.567 1.801-9.371 3.728-14.462 11.387-17.069 20.668-3.548 12.699-3.921 25.757-3.483 38.865 0.45 13.52 2.942 26.618 9.202 38.803 4.897 9.532 11.246 17.977 21.246 27.821z" horiz-adv-x="513" />
<glyph unicode="&#xe618;" d="M398.543 56.151c-0.029 0.082-0.060 0.164-0.080 0.243-35.7-22.819-75.891-34.966-117.012-34.966-0.007 0-0.010 0-0.014 0-61.26 0-118.75 26.386-157.734 72.37-49.889 58.849-67.126 164.977-36.511 213.894 2.002-0.831 3.938-1.616 5.84-2.387 6.793-2.756 13.207-5.358 21.153-9.548 3.031-1.601 6.169-2.406 9.337-2.406 5.857 0 11.3 2.824 14.924 7.743 3.907 5.309 5.156 12.389 3.269 18.476l-1.762 5.705c-5.344 17.295-10.862 35.177-17.106 52.539-4.992 13.882-11.2 31.163-29.613 31.163-6.028 0-13.019-1.828-23.365-6.102-22.147-9.159-35.529-14.981-57.267-24.905-7.551-3.444-12.617-11.349-12.601-19.672 0.014-7.921 4.496-14.668 11.988-18.058 9.104-4.128 15.268-6.858 21.734-9.723l5.343-2.377c-50.969-129.551 12.401-263.229 105.657-319.606 41.749-25.237 89.25-38.57 137.385-38.57h0.021c51.36 0 102.781 15.55 142.25 42.599-15.865 14.401-22.783 34.584-25.836 43.586zM549.101 105.045c-9.057 4.288-15.178 7.129-21.611 10.122l-5.248 2.446c53.224 128.634-7.784 263.401-100.034 321.394-42.68 26.832-91.562 41.016-141.358 41.016-52.424 0-103.205-15.297-142.983-43.083l-2.692-1.882c15.798-13.782 22.93-33.394 26.459-43.205 36.463 23.97 77.838 36.704 119.947 36.704 62.704 0 121.071-27.392 160.147-75.158 48.841-59.724 64.219-166.128 32.749-214.508-1.995 0.868-3.908 1.692-5.812 2.499-6.736 2.88-13.102 5.59-20.977 9.911-3.101 1.712-6.322 2.577-9.606 2.577-5.793 0-11.2-2.779-14.845-7.634-3.906-5.217-5.239-12.216-3.483-18.257l1.639-5.651c5.048-17.423 10.265-35.428 16.206-52.921 4.794-14.119 10.757-31.691 29.589-31.691 5.921 0 12.788 1.712 22.94 5.7 22.175 8.719 35.66 14.3 57.704 23.889 7.595 3.312 12.801 11.126 12.929 19.447 0.14 7.911-4.222 14.75-11.663 18.284z" horiz-adv-x="561" />
<glyph unicode="&#xe619;" d="M23.497 480.85c230.617 0 276.897 0 507.512 0 17.96 0 26.678-12.98 26.678-28.98-0.29-151.63-0.163-303.244-0.225-454.904 0-21.992-6.601-28.529-28.851-28.529-221.536-0.063-278.226-0.063-499.776 0-22.267 0-28.899 6.505-28.899 28.529-0.049 151.629 0.242 304.036-0.046 455.664-0.017 13.105 5.651 26.88 23.608 28.219zM155.702 225.149c0-59.522-0.036-86.084 0.029-145.625 0.018-25.022 5.604-30.525 31.060-30.525 116.676 0 68.537 0 185.261 0 23.538 0 29.625 5.991 29.625 29.048 0.063 119.555 0.063 173.169 0 292.695 0 24.069-5.344 29.495-28.884 29.495-117.661 0.050-70.422 0.050-188.078 0-23.522 0-28.965-5.522-28.983-29.445-0.065-59.554-0.029-86.105-0.029-145.643zM76.972 419.283c-37.465-0.031-33.343 2.979-33.422-33.343-0.1-31.975-3.527-31.767 31.264-31.686 36.499 0.097 33.6-1.882 33.651 33.777 0 33.861 2.043 31.298-31.493 31.251zM481.822 419.283c-35.579-0.017-32.78 3.092-32.875-32.682-0.065-33.651-2.254-32.346 32.264-32.346 36.544 0 32.649-1.015 32.649 33.119-0.001 34.323 3.478 31.955-32.038 31.909zM108.414 61.204c0.18 36.547 2.32 33.457-33.679 33.585-34.052 0.096-31.285 1.382-31.203-31.655 0.065-36.738-3.477-33.26 33.537-33.325 33.021-0.033 31.571-3.028 31.346 31.394zM513.859 62.2c0.067 34.167 3.221 32.652-31.649 32.589-35.066-0.066-33.328 2.897-33.264-32.652 0.065-35.322-2.192-32.361 31.878-32.329 35.998 0.066 33.101-3.349 33.034 32.392zM513.859 171.038c0 35.275 3.61 33.421-33.743 33.261-0.449 0-0.937 0-1.419 0-29.688 0-29.688 0-29.688-29.012 0-38.961-3.221-34.968 34.647-35.098 33.038-0.193 30.269-1.546 30.202 30.849zM75.653 244.936c34.147-0.082 32.907-2.784 32.812 31.491-0.097 35.564 2.448 32.459-33.007 32.505-34.953 0.050-31.907 2.352-31.942-31.989-0.031-33.715-2.85-32.231 32.138-32.007zM480.632 244.936c36.256-0.129 33.295-2.302 33.228 32.247 0 34.279 3.092 31.769-32.134 31.749-35.098-0.014-32.843 3.026-32.749-32.747 0.066-31.25 0.034-31.25 31.655-31.25zM75.2 140.19c35.502 0 33.329-3.284 33.233 32.264-0.082 31.847-0.018 31.75-32.507 31.878-35.403 0.129-32.411 1.337-32.411-31.878 0.018-34.584-2.959-32.394 31.684-32.264z" horiz-adv-x="558" />
<glyph unicode="&#xe61a;" d="M1.94 73.418h110.13v-105.418h-110.13v105.418zM154.409 175.072h110.135v-207.072h-110.135v207.072zM306.882 276.706h110.134v-308.706h-110.134v308.706zM459.342 378.358h110.132v-410.358h-110.132v410.358zM611.814 480h110.131v-512h-110.131v512z" horiz-adv-x="722" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Binary file not shown.

View File

@ -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,

View File

@ -48,6 +48,7 @@
<script src="rtp_sts.js?v=1"></script><!-- RTP stats processing -->
<script src="local_sts.js?v=1"></script><!-- Local stats processing -->
<script src="videolayout.js?v=15"></script><!-- video ui -->
<script src="connectionquality.js?v=1"></script>
<script src="toolbar.js?v=6"></script><!-- toolbar ui -->
<script src="toolbar_toggler.js?v=2"></script>
<script src="canvas_util.js?v=1"></script><!-- canvas drawing utils -->
@ -57,6 +58,7 @@
<script src="roomname_generator.js?v=1"></script><!-- generator for random room names -->
<script src="keyboard_shortcut.js?v=2"></script>
<script src="tracking.js?v=1"></script><!-- tracking -->
<script src="jitsipopover.js?v=1"></script>
<script src="message_handler.js?v=1"></script>
<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
<link rel="stylesheet" href="css/font.css?v=4"/>
@ -66,6 +68,7 @@
<link rel="stylesheet" href="css/modaldialog.css?v=3">
<link rel="stylesheet" href="css/popup_menu.css?v=4">
<link rel="stylesheet" href="css/popover.css?v=2">
<link rel="stylesheet" href="css/jitsi_popover.css?v=2">
<link rel="stylesheet" href="css/contact_list.css?v=3">
<link rel="stylesheet" href="css/welcome_page.css?v=2">
<!--
@ -256,6 +259,11 @@
</span>
<audio id="localAudio" autoplay oncontextmenu="return false;" muted></audio>
<span class="focusindicator"></span>
<!--<div class="connectionindicator">
<span class="connection connection_empty"><i class="icon-connection"></i></span>
<span class="connection connection_full"><i class="icon-connection"></i></span>
</div>-->
</span>
<audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
<audio id="userLeft" src="sounds/left.wav" preload="auto"></audio>

115
jitsipopover.js Normal file
View File

@ -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 = ' <div class="jitsipopover ' + this.options.skin +
'"><div class="arrow"></div><div class="jitsipopover-content"></div>' +
'<div class="jitsiPopupmenuPadding"></div></div>';
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;
})();

View File

@ -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

23
muc.js
View File

@ -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) {

View File

@ -1,34 +1,12 @@
/* global ssrc2jid */
/**
* Function object which once created can be used to calculate moving average of
* given period. Example for SMA3:</br>
* 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 <tt>period</tt>.
* @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 <tt>ssrc</tt> 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 <tt>ssrc</tt> 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 <tt>ssrc</tt> 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 = [];
/**
* <tt>StatsCollector</tt> registers for stats updates of given
* <tt>peerconnection</tt> in given <tt>interval</tt>. On each update particular
* stats are extracted and put in {@link PeerStats} objects. Once the processing
* is done <tt>updateCallback</tt> is called with <tt>this</tt> instance as
* is done <tt>audioLevelsUpdateCallback</tt> is called with <tt>this</tt> 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);
}
};

View File

@ -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 = "<span class='jitsipopover_green'>&darr;</span>" +
(this.packetLoss.download != null? this.packetLoss.download : "N/A") +
"% <span class='jitsipopover_orange'>&uarr;</span>" +
(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 = "<span class='jitsipopover_blue'>Bitrate:</span> <span class='jitsipopover_green'>&darr;</span>" +
downloadBitrate + " <span class='jitsipopover_orange'>&uarr;</span>" +
uploadBitrate + "<br />" +
"<span class='jitsipopover_blue'>Packet loss: </span>" + packetLoss + "<br />" +
"<span class='jitsipopover_blue'>Resolution:</span> " + resolution + "<br />";
if(this.videoContainer.id == "localVideoContainer")
result += "<div class=\"jitsipopover_showmore\" onclick = \"VideoLayout.connectionIndicators['" +
this.videoContainer.id + "'].showMore()\">" + (this.showMoreValue? "Show less" : "Show More") + "</div><br />";
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 = "<span class='jitsipopover_blue'>Address:</span> N/A";
}
else
{
transport = "<span class='jitsipopover_blue'>Address:</span> " + this.transport[0].ip.substring(0,this.transport[0].ip.indexOf(":")) + "<br />";
if(this.transport.length > 1)
{
transport += "<span class='jitsipopover_blue'>Ports:</span> ";
}
else
{
transport += "<span class='jitsipopover_blue'>Port:</span> ";
}
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 += "<br /><span class='jitsipopover_blue'>Transport:</span> " + this.transport[0].type + "<br />";
}
result += "<span class='jitsipopover_blue'>Estimated bandwidth:</span> " +
"<span class='jitsipopover_green'>&darr;</span>" + downloadBandwidth +
" <span class='jitsipopover_orange'>&uarr;</span>" +
uploadBandwidth + "<br />";
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: "<div class=\"connection_info\">Come back here for " +
"connection information once the conference starts</div>", 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("<div class=\"connection_info\">" + this.generateText() + "</div>");
};
/**
* 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 || {}));