You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ConnectionQuality.js 9.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. import * as ConnectionQualityEvents
  2. from "../../service/connectivity/ConnectionQualityEvents";
  3. import * as ConferenceEvents from "../../JitsiConferenceEvents";
  4. import {getLogger} from "jitsi-meet-logger";
  5. var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
  6. var MediaType = require('../../service/RTC/MediaType');
  7. const logger = getLogger(__filename);
  8. /**
  9. * The value to use for the "type" field for messages sent by ConnectionQuality
  10. * over the data channel.
  11. */
  12. const STATS_MESSAGE_TYPE = "stats";
  13. // webrtc table describing simulcast resolutions and used bandwidth
  14. // https://chromium.googlesource.com/external/webrtc/+/master/webrtc/media/engine/simulcast.cc#42
  15. const _bandwidthMap = [
  16. { width: 1920, height: 1080, layers:3, max: 5000, min: 800 },
  17. { width: 1280, height: 720, layers:3, max: 2500, min: 600 },
  18. { width: 960, height: 540, layers:3, max: 900, min: 450 },
  19. { width: 640, height: 360, layers:2, max: 700, min: 150 },
  20. { width: 480, height: 270, layers:2, max: 450, min: 150 },
  21. { width: 320, height: 180, layers:1, max: 200, min: 30 }
  22. ];
  23. /**
  24. * Calculates the quality percent based on passed new and old value.
  25. * @param newVal the new value
  26. * @param oldVal the old value
  27. */
  28. function calculateQuality(newVal, oldVal) {
  29. return (newVal <= oldVal) ? newVal : (9*oldVal + newVal) / 10;
  30. }
  31. /**
  32. * Calculates the quality percentage based on the input resolution height and
  33. * the upload reported by the client. The value is based on the interval from
  34. * _bandwidthMap.
  35. * @param inputHeight the resolution used to open the camera.
  36. * @param upload the upload rate reported by client.
  37. * @returns {int} the percent of upload based on _bandwidthMap and maximum value
  38. * of 100, as values of the map are approximate and clients can stream above
  39. * those values. Returns undefined if no result is found.
  40. */
  41. function calculateQualityUsingUpload(inputHeight, upload) {
  42. // found resolution from _bandwidthMap which height is equal or less than
  43. // the inputHeight
  44. let foundResolution = _bandwidthMap.find((r) => (r.height <= inputHeight));
  45. if (!foundResolution)
  46. return undefined;
  47. if (upload <= foundResolution.min)
  48. return 0;
  49. return Math.min(
  50. ((upload - foundResolution.min)*100)
  51. / (foundResolution.max - foundResolution.min),
  52. 100);
  53. }
  54. export default class ConnectionQuality {
  55. constructor(conference, eventEmitter, options) {
  56. this.eventEmitter = eventEmitter;
  57. /**
  58. * The owning JitsiConference.
  59. */
  60. this.conference = conference;
  61. this.disableQualityBasedOnBandwidth =
  62. options.forceQualityBasedOnBandwidth
  63. ? false : !!options.disableSimulcast;
  64. /**
  65. * local stats
  66. * @type {{}}
  67. */
  68. this.localStats = {};
  69. /**
  70. * remote stats
  71. * @type {{}}
  72. */
  73. this.remoteStats = {};
  74. conference.on(ConferenceEvents.CONNECTION_INTERRUPTED,
  75. () => { this._updateLocalConnectionQuality(0); });
  76. conference.on(
  77. ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
  78. (participant, payload) => {
  79. if (payload.type === STATS_MESSAGE_TYPE) {
  80. let remoteVideo = participant.getTracks()
  81. .find(tr => tr.isVideoTrack());
  82. this.updateRemoteStats(
  83. participant.getId(),
  84. payload.values,
  85. remoteVideo ? remoteVideo.videoType : undefined,
  86. remoteVideo ? remoteVideo.isMuted() : undefined);
  87. }
  88. });
  89. conference.on(
  90. ConferenceEvents.CONNECTION_STATS,
  91. (stats) => {
  92. let localVideoTracks =
  93. conference.getLocalTracks(MediaType.VIDEO);
  94. let localVideoTrack
  95. = localVideoTracks.length > 0 ? localVideoTracks[0] : null;
  96. // if we say video muted we will use old method of calculating
  97. // quality and will not depend on localVideo if it is missing
  98. this.updateLocalStats(
  99. stats,
  100. conference.isConnectionInterrupted(),
  101. localVideoTrack ? localVideoTrack.videoType : undefined,
  102. localVideoTrack ? localVideoTrack.isMuted() : true,
  103. localVideoTrack ? localVideoTrack.resolution : null);
  104. });
  105. }
  106. /**
  107. * Returns the new quality value based on the input parameters.
  108. * Used to calculate remote and local values.
  109. * @param data the data
  110. * @param lastQualityValue the last value we calculated
  111. * @param videoType need to check whether we are screen sharing
  112. * @param isMuted is video muted
  113. * @param resolution the input resolution used by the camera
  114. * @returns {*} the newly calculated value or undefined if no result
  115. * @private
  116. */
  117. _getNewQualityValue(
  118. data, lastQualityValue, videoType, isMuted, resolution) {
  119. if (this.disableQualityBasedOnBandwidth
  120. || isMuted
  121. || videoType === 'desktop'
  122. || !resolution) {
  123. return calculateQuality(
  124. 100 - data.packetLoss.total,
  125. lastQualityValue || 100);
  126. } else {
  127. return calculateQualityUsingUpload(
  128. resolution,
  129. data.bitrate.upload);
  130. }
  131. }
  132. /**
  133. * Updates only the localConnectionQuality value
  134. * @param values {int} the new value. should be from 0 - 100.
  135. */
  136. _updateLocalConnectionQuality(value) {
  137. this.localStats.connectionQuality = value;
  138. this.eventEmitter.emit(
  139. ConnectionQualityEvents.LOCAL_STATS_UPDATED,
  140. this.localStats);
  141. this._broadcastLocalStats();
  142. }
  143. /**
  144. * Broadcasts the local statistics to all other participants in the
  145. * conference.
  146. */
  147. _broadcastLocalStats() {
  148. // Send only the data that remote participants care about.
  149. let data = {
  150. bitrate: this.localStats.bitrate,
  151. packetLoss: this.localStats.packetLoss,
  152. connectionQuality: this.localStats.connectionQuality
  153. };
  154. let localVideoTracks = this.conference.getLocalTracks(MediaType.VIDEO);
  155. let localVideoTrack
  156. = localVideoTracks.length > 0 ? localVideoTracks[0] : null;
  157. if (localVideoTrack && localVideoTrack.resolution) {
  158. data.resolution = localVideoTrack.resolution;
  159. }
  160. try {
  161. this.conference.broadcastEndpointMessage({
  162. type: STATS_MESSAGE_TYPE,
  163. values: data });
  164. } catch (e) {
  165. let errorMsg = "Failed to broadcast local stats";
  166. logger.error(errorMsg, e);
  167. GlobalOnErrorHandler.callErrorHandler(
  168. new Error(errorMsg + ": " + e));
  169. }
  170. }
  171. /**
  172. * Updates the local statistics
  173. * @param data new statistics
  174. * @param updateLocalConnectionQuality {boolean} weather to recalculate
  175. * localConnectionQuality or not.
  176. * @param videoType the local video type
  177. * @param isMuted current state of local video, whether it is muted
  178. * @param resolution the current resolution used by local video
  179. */
  180. updateLocalStats(data, updateLocalConnectionQuality,
  181. videoType, isMuted, resolution) {
  182. this.localStats = data;
  183. if(updateLocalConnectionQuality) {
  184. let val = this._getNewQualityValue(
  185. this.localStats,
  186. this.localStats.connectionQuality,
  187. videoType,
  188. isMuted,
  189. resolution);
  190. if (val !== undefined) {
  191. this.localStats.connectionQuality = val;
  192. }
  193. }
  194. this.eventEmitter.emit(
  195. ConnectionQualityEvents.LOCAL_STATS_UPDATED,
  196. this.localStats);
  197. this._broadcastLocalStats();
  198. }
  199. /**
  200. * Updates remote statistics
  201. * @param id the id associated with the statistics
  202. * @param data the statistics received
  203. * @param remoteVideoType the video type of the remote video
  204. * @param isRemoteVideoMuted whether remote video is muted
  205. */
  206. updateRemoteStats(id, data, remoteVideoType, isRemoteVideoMuted) {
  207. if (!data ||
  208. !("packetLoss" in data) ||
  209. !("total" in data.packetLoss)) {
  210. this.eventEmitter.emit(
  211. ConnectionQualityEvents.REMOTE_STATS_UPDATED,
  212. id,
  213. null);
  214. return;
  215. }
  216. // Use only the fields we need
  217. this.remoteStats[id] = {
  218. bitrate: data.bitrate,
  219. packetLoss: data.packetLoss,
  220. connectionQuality: data.connectionQuality
  221. };
  222. this.eventEmitter.emit(
  223. ConnectionQualityEvents.REMOTE_STATS_UPDATED,
  224. id,
  225. this.remoteStats[id]);
  226. }
  227. /**
  228. * Returns the local statistics.
  229. */
  230. getStats() {
  231. return this.localStats;
  232. }
  233. }