diff --git a/conference.js b/conference.js index f578f4794..4aa5fff23 100644 --- a/conference.js +++ b/conference.js @@ -1372,7 +1372,8 @@ export default { } room.on(ConferenceEvents.CONNECTION_STATS, function (stats) { - ConnectionQuality.updateLocalStats(stats, connectionIsInterrupted); + ConnectionQuality.updateLocalStats( + stats, connectionIsInterrupted, localVideo); }); ConnectionQuality.addListener(CQEvents.LOCALSTATS_UPDATED, @@ -1382,6 +1383,12 @@ export default { let data = { bitrate: stats.bitrate, packetLoss: stats.packetLoss}; + if (localVideo && localVideo.resolution) { + data.resolution = { + inputHeight: localVideo.resolution + }; + } + try { room.broadcastEndpointMessage({ type: this.commands.defaults.CONNECTION_QUALITY, @@ -1394,10 +1401,15 @@ export default { room.on(ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, (participant, payload) => { switch(payload.type) { - case this.commands.defaults.CONNECTION_QUALITY: - ConnectionQuality.updateRemoteStats(participant.getId(), - payload.values); + case this.commands.defaults.CONNECTION_QUALITY: { + let id = participant.getId(); + ConnectionQuality.updateRemoteStats( + id, + payload.values, + APP.UI.getRemoteVideoType(id), + APP.UI.isRemoteVideoMuted(id)); break; + } default: console.warn("Unknown datachannel message", payload); } diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 70678b97a..e05c95486 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -779,6 +779,15 @@ UI.getRemoteVideoType = function (jid) { return VideoLayout.getRemoteVideoType(jid); }; +/** + * Return the mute state of the remote video. + * @param jid the jid for the remote video + * @returns the video mute state. + */ +UI.isRemoteVideoMuted = function (jid) { + return VideoLayout.isRemoteVideoMuted(jid); +}; + UI.connectionIndicatorShowMore = function(id) { VideoLayout.showMore(id); }; diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 749dd23ec..44cce4704 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -327,6 +327,16 @@ var VideoLayout = { return smallVideo ? smallVideo.getVideoType() : null; }, + /** + * Return the mute state of the remote video. + * @param id the id for the remote video + * @returns {boolean} the video mute state. + */ + isRemoteVideoMuted (id) { + let smallVideo = VideoLayout.getSmallVideo(id); + return smallVideo ? smallVideo.isVideoMuted : null; + }, + isPinned (id) { return (pinnedId) ? (id === pinnedId) : false; }, diff --git a/modules/connectionquality/connectionquality.js b/modules/connectionquality/connectionquality.js index 1b1c17bd5..124d58463 100644 --- a/modules/connectionquality/connectionquality.js +++ b/modules/connectionquality/connectionquality.js @@ -1,3 +1,4 @@ +/* global config */ import EventEmitter from "events"; import CQEvents from "../../service/connectionquality/CQEvents"; @@ -35,6 +36,58 @@ function calculateQuality(newVal, oldVal) { return (newVal <= oldVal) ? newVal : (9*oldVal + newVal) / 10; } +// webrtc table describing simulcast resolutions and used bandwidth +// https://chromium.googlesource.com/external/webrtc/+/master/webrtc/media/engine/simulcast.cc#42 +var _bandwidthMap = [ + { width: 1920, height: 1080, layers:3, max: 5000, min: 800 }, + { width: 1280, height: 720, layers:3, max: 2500, min: 600 }, + { width: 960, height: 540, layers:3, max: 900, min: 450 }, + { width: 640, height: 360, layers:2, max: 700, min: 150 }, + { width: 480, height: 270, layers:2, max: 450, min: 150 }, + { width: 320, height: 180, layers:1, max: 200, min: 30 } +]; + +/** + * We disable quality calculations based on bandwidth if simulcast is disabled, + * or enable it in case of no simulcast and we force it. + * @type {boolean} + */ +var disableQualityBasedOnBandwidth = + config.forceQualityBasedOnBandwidth ? false : config.disableSimulcast; + +/** + * Calculates the quality percentage based on the input resolution height and + * the upload reported by the client. The value is based on the interval from + * _bandwidthMap. + * @param inputHeight the resolution used to open the camera. + * @param upload the upload rate reported by client. + * @returns {*} the percent of upload based on _bandwidthMap and maximum value + * of 100, as values of the map are approximate and clients can stream above + * those values. + */ +function calculateQualityUsingUpload(inputHeight, upload) { + let foundResolution = null; + + for (let i in _bandwidthMap) { + let r = _bandwidthMap[i]; + if (r.height <= inputHeight) { + foundResolution = r; + break; + } + } + + if (!foundResolution) + return false; + + if (upload <= foundResolution.min) + return 0; + + return Math.min( + ((upload - foundResolution.min)*100) + / (foundResolution.max - foundResolution.min), + 100); +} + export default { /** * Updates the local statistics @@ -42,15 +95,28 @@ export default { * @param dontUpdateLocalConnectionQuality {boolean} if true - * localConnectionQuality wont be recalculated. */ - updateLocalStats: function (data, dontUpdateLocalConnectionQuality) { - stats = data; - if(!dontUpdateLocalConnectionQuality) { - var newVal = 100 - stats.packetLoss.total; - localConnectionQuality = - calculateQuality(newVal, localConnectionQuality); - } - eventEmitter.emit(CQEvents.LOCALSTATS_UPDATED, localConnectionQuality, - stats); + updateLocalStats: + function (data, dontUpdateLocalConnectionQuality, localVideo) { + stats = data; + if(!dontUpdateLocalConnectionQuality) { + if (!disableQualityBasedOnBandwidth + && !localVideo.isMuted() + && localVideo.videoType !== 'desktop' + && localVideo.resolution) { + let val = calculateQualityUsingUpload( + localVideo.resolution, + data.bitrate.upload); + if (val) { + localConnectionQuality = val; + } + } else { + var newVal = 100 - stats.packetLoss.total; + localConnectionQuality = + calculateQuality(newVal, localConnectionQuality); + } + } + eventEmitter.emit( + CQEvents.LOCALSTATS_UPDATED, localConnectionQuality, stats); }, /** @@ -68,23 +134,41 @@ export default { * @param id the id associated with the statistics * @param data the statistics */ - updateRemoteStats: function (id, data) { - if (!data || !("packetLoss" in data) || !("total" in data.packetLoss)) { - eventEmitter.emit(CQEvents.REMOTESTATS_UPDATED, id, null, null); - return; - } - // Use only the fields we need - data = {bitrate: data.bitrate, packetLoss: data.packetLoss}; + updateRemoteStats: + function (id, data, remoteVideoType, isRemoteVideoMuted) { + if (!data || + !("packetLoss" in data) || + !("total" in data.packetLoss)) { + eventEmitter.emit(CQEvents.REMOTESTATS_UPDATED, id, null, null); + return; + } - remoteStats[id] = data; + let inputResolution = data.resolution; + // Use only the fields we need + data = {bitrate: data.bitrate, packetLoss: data.packetLoss}; - var newVal = 100 - data.packetLoss.total; - var oldVal = remoteConnectionQuality[id]; - remoteConnectionQuality[id] = calculateQuality(newVal, oldVal || 100); + remoteStats[id] = data; - eventEmitter.emit( - CQEvents.REMOTESTATS_UPDATED, id, remoteConnectionQuality[id], - remoteStats[id]); + if (disableQualityBasedOnBandwidth + || isRemoteVideoMuted + || remoteVideoType === 'desktop' + || !inputResolution) { + var newVal = 100 - data.packetLoss.total; + var oldVal = remoteConnectionQuality[id]; + remoteConnectionQuality[id] + = calculateQuality(newVal, oldVal || 100); + } else { + let val = calculateQualityUsingUpload( + inputResolution.inputHeight, + data.bitrate.upload); + if (val) { + remoteConnectionQuality[id] = val; + } + } + + eventEmitter.emit( + CQEvents.REMOTESTATS_UPDATED, id, + remoteConnectionQuality[id], remoteStats[id]); }, /**