Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

ConnectionQuality.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. import * as ConnectionQualityEvents
  2. from "../../service/connectivity/ConnectionQualityEvents";
  3. import * as ConferenceEvents from "../../JitsiConferenceEvents";
  4. import {getLogger} from "jitsi-meet-logger";
  5. import RTCBrowserType from "../RTC/RTCBrowserType";
  6. var XMPPEvents = require('../../service/xmpp/XMPPEvents');
  7. var GlobalOnErrorHandler = require("../util/GlobalOnErrorHandler");
  8. var MediaType = require('../../service/RTC/MediaType');
  9. var VideoType = require('../../service/RTC/VideoType');
  10. var Resolutions = require("../../service/RTC/Resolutions");
  11. const logger = getLogger(__filename);
  12. /**
  13. * The value to use for the "type" field for messages sent by ConnectionQuality
  14. * over the data channel.
  15. */
  16. const STATS_MESSAGE_TYPE = "stats";
  17. /**
  18. * See media/engine/simulcast.ss from webrtc.org
  19. */
  20. const kSimulcastFormats = [
  21. { width: 1920, height: 1080, layers:3, max: 5000, target: 4000, min: 800 },
  22. { width: 1280, height: 720, layers:3, max: 2500, target: 2500, min: 600 },
  23. { width: 960, height: 540, layers:3, max: 900, target: 900, min: 450 },
  24. { width: 640, height: 360, layers:2, max: 700, target: 500, min: 150 },
  25. { width: 480, height: 270, layers:2, max: 450, target: 350, min: 150 },
  26. { width: 320, height: 180, layers:1, max: 200, target: 150, min: 30 }
  27. ];
  28. /**
  29. * The initial bitrate for video in kbps.
  30. */
  31. var startBitrate = 800;
  32. /**
  33. * Gets the expected bitrate (in kbps) in perfect network conditions.
  34. * @param simulcast {boolean} whether simulcast is enabled or not.
  35. * @param resolution {Resolution} the resolution.
  36. * @param millisSinceStart {number} the number of milliseconds since sending
  37. * video started.
  38. */
  39. function getTarget(simulcast, resolution, millisSinceStart) {
  40. let target = 0;
  41. let height = Math.min(resolution.height, resolution.width);
  42. if (simulcast) {
  43. // Find the first format with height no bigger than ours.
  44. let simulcastFormat = kSimulcastFormats.find(f => f.height <= height);
  45. if (simulcastFormat) {
  46. // Sum the target fields from all simulcast layers for the given
  47. // resolution (e.g. 720p + 360p + 180p).
  48. for (height = simulcastFormat.height; height >= 180; height /=2) {
  49. simulcastFormat
  50. = kSimulcastFormats.find(f => f.height == height);
  51. if (simulcastFormat) {
  52. target += simulcastFormat.target;
  53. } else {
  54. break;
  55. }
  56. }
  57. }
  58. } else {
  59. // See GetMaxDefaultVideoBitrateKbps in
  60. // media/engine/webrtcvideoengine2.cc from webrtc.org
  61. let pixels = resolution.width * resolution.height;
  62. if (pixels <= 320 * 240) {
  63. target = 600;
  64. } else if (pixels <= 640 * 480) {
  65. target = 1700;
  66. } else if (pixels <= 960 * 540) {
  67. target = 2000;
  68. } else {
  69. target = 2500;
  70. }
  71. }
  72. // Allow for an additional 3 seconds for ramp up -- delay any initial drop
  73. // of connection quality by 3 seconds.
  74. return Math.min(target, rampUp(Math.max(0, millisSinceStart - 3000)));
  75. }
  76. /**
  77. * Gets the bitrate to which GCC would have ramped up in perfect network
  78. * conditions after millisSinceStart milliseconds.
  79. * @param millisSinceStart {number} the number of milliseconds since sending
  80. * video was enabled.
  81. */
  82. function rampUp(millisSinceStart) {
  83. // According to GCC the send side bandwidth estimation grows with at most
  84. // 8% per second.
  85. // https://tools.ietf.org/html/draft-ietf-rmcat-gcc-02#section-5.5
  86. return startBitrate * Math.pow(1.08, millisSinceStart / 1000);
  87. }
  88. /**
  89. * A class which monitors the local statistics coming from the RTC modules, and
  90. * calculates a "connection quality" value, in percent, for the media
  91. * connection. A value of 100% indicates a very good network connection, and a
  92. * value of 0% indicates a poor connection.
  93. */
  94. export default class ConnectionQuality {
  95. constructor(conference, eventEmitter, options) {
  96. this.eventEmitter = eventEmitter;
  97. /**
  98. * The owning JitsiConference.
  99. */
  100. this._conference = conference;
  101. /**
  102. * Whether simulcast is supported. Note that even if supported, it is
  103. * currently not used for screensharing, which is why we have an
  104. * additional check.
  105. */
  106. this._simulcast
  107. = !options.disableSimulcast && RTCBrowserType.supportsSimulcast();
  108. /**
  109. * Holds statistics about the local connection quality.
  110. */
  111. this._localStats = {connectionQuality: 100};
  112. /**
  113. * The time this._localStats.connectionQuality was last updated.
  114. */
  115. this._lastConnectionQualityUpdate = -1;
  116. /**
  117. * Maps a participant ID to an object holding connection quality
  118. * statistics received from this participant.
  119. */
  120. this._remoteStats = {};
  121. /**
  122. * The time that the ICE state last changed to CONNECTED. We use this
  123. * to calculate how much time we as a sender have had to ramp-up.
  124. */
  125. this._timeIceConnected = -1;
  126. /**
  127. * The time that local video was unmuted. We use this to calculate how
  128. * much time we as a sender have had to ramp-up.
  129. */
  130. this._timeVideoUnmuted = -1;
  131. // We assume a global startBitrate value for the sake of simplicity.
  132. if (options.startBitrate && options.startBitrate > 0) {
  133. startBitrate = options.startBitrate;
  134. }
  135. // TODO: consider ignoring these events and letting the user of
  136. // lib-jitsi-meet handle these separately.
  137. conference.on(
  138. ConferenceEvents.CONNECTION_INTERRUPTED,
  139. () => {
  140. this._updateLocalConnectionQuality(0);
  141. this.eventEmitter.emit(
  142. ConnectionQualityEvents.LOCAL_STATS_UPDATED,
  143. this._localStats);
  144. this._broadcastLocalStats();
  145. });
  146. conference.room.addListener(
  147. XMPPEvents.ICE_CONNECTION_STATE_CHANGED,
  148. (newState) => {
  149. if (newState === 'connected') {
  150. this._timeIceConnected = window.performance.now();
  151. }
  152. });
  153. // Listen to DataChannel message from other participants in the
  154. // conference, and update the _remoteStats field accordingly.
  155. conference.on(
  156. ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
  157. (participant, payload) => {
  158. if (payload.type === STATS_MESSAGE_TYPE) {
  159. this._updateRemoteStats(
  160. participant.getId(), payload.values);
  161. }
  162. });
  163. // Listen to local statistics events originating from the RTC module
  164. // and update the _localStats field.
  165. // Oh, and by the way, the resolutions of all remote participants are
  166. // also piggy-backed in these "local" statistics. It's obvious, really,
  167. // if one carefully reads the *code* (but not the docs) in
  168. // UI/VideoLayout/VideoLayout.js#updateLocalConnectionStats in
  169. // jitsi-meet
  170. // TODO: We should keep track of the remote resolution in _remoteStats,
  171. // and notify about changes via separate events.
  172. conference.on(
  173. ConferenceEvents.CONNECTION_STATS,
  174. this._updateLocalStats.bind(this));
  175. // Save the last time we were unmuted.
  176. conference.on(
  177. ConferenceEvents.TRACK_MUTE_CHANGED,
  178. (track) => {
  179. if (track.isVideoTrack()) {
  180. if (track.isMuted()) {
  181. this._timeVideoUnmuted = -1;
  182. } else {
  183. this._maybeUpdateUnmuteTime();
  184. }
  185. }
  186. });
  187. conference.on(
  188. ConferenceEvents.TRACK_ADDED,
  189. (track) => {
  190. if (track.isVideoTrack() && !track.isMuted())
  191. {
  192. this._maybeUpdateUnmuteTime();
  193. }
  194. });
  195. }
  196. /**
  197. * Sets _timeVideoUnmuted if it was previously unset. If it was already set,
  198. * doesn't change it.
  199. */
  200. _maybeUpdateUnmuteTime() {
  201. if (this._timeVideoUnmuted < 0) {
  202. this._timeVideoUnmuted = window.performance.now();
  203. }
  204. }
  205. /**
  206. * Calculates a new "connection quality" value.
  207. * @param videoType {VideoType} the type of the video source (camera or
  208. * a screen capture).
  209. * @param isMuted {boolean} whether the local video is muted.
  210. * @param resolutionName {Resolution} the input resolution used by the
  211. * camera.
  212. * @returns {*} the newly calculated connection quality.
  213. */
  214. _calculateConnectionQuality(videoType, isMuted, resolutionName) {
  215. // resolutionName is an index into Resolutions (where "720" is
  216. // "1280x720" and "960" is "960x720" ...).
  217. let resolution = Resolutions[resolutionName];
  218. let quality = 100;
  219. if (isMuted || !resolution
  220. || this._timeIceConnected < 0
  221. || this._timeVideoUnmuted < 0) {
  222. // Calculate a value based on packet loss only.
  223. if (!this._localStats.packetLoss
  224. || this._localStats.packetLoss.total === undefined) {
  225. logger.error("Cannot calculate connection quality, unknown "
  226. + "packet loss.");
  227. quality = 100;
  228. } else {
  229. let loss = this._localStats.packetLoss.total;
  230. if (loss <= 2) {
  231. quality = 100;
  232. } else if (loss <= 4) {
  233. quality = 70; // 4 bars
  234. } else if (loss <= 6) {
  235. quality = 50; // 3 bars
  236. } else if (loss <= 8) {
  237. quality = 30; // 2 bars
  238. } else if (loss <= 12) {
  239. quality = 10; // 1 bars
  240. } else {
  241. quality = 0; // Still 1 bar, but slower climb-up.
  242. }
  243. }
  244. } else {
  245. // Calculate a value based on the sending bitrate.
  246. // simulcast is not used for screensharing.
  247. let simulcast = (this._simulcast && videoType === VideoType.CAMERA);
  248. // time since sending of video was enabled.
  249. let millisSinceStart = window.performance.now()
  250. - Math.max(this._timeVideoUnmuted, this._timeIceConnected);
  251. // expected sending bitrate in perfect conditions
  252. let target = getTarget(simulcast, resolution, millisSinceStart);
  253. target = 0.9 * target;
  254. quality = 100 * this._localStats.bitrate.upload / target;
  255. // Whatever the bitrate, drop early if there is significant loss
  256. if (this._localStats.packetLoss
  257. && this._localStats.packetLoss.total >= 10) {
  258. quality = Math.min(quality, 30);
  259. }
  260. }
  261. // Make sure that the quality doesn't climb quickly
  262. if (this._lastConnectionQualityUpdate > 0)
  263. {
  264. let maxIncreasePerSecond = 2;
  265. let prevConnectionQuality = this._localStats.connectionQuality;
  266. let diffSeconds
  267. = (window.performance.now()
  268. - this._lastConnectionQualityUpdate) / 1000;
  269. quality = Math.min(
  270. quality,
  271. prevConnectionQuality + diffSeconds * maxIncreasePerSecond);
  272. }
  273. return Math.min(100, quality);
  274. }
  275. /**
  276. * Updates the localConnectionQuality value
  277. * @param values {number} the new value. Should be in [0, 100].
  278. */
  279. _updateLocalConnectionQuality(value) {
  280. this._localStats.connectionQuality = value;
  281. this._lastConnectionQualityUpdate = window.performance.now();
  282. }
  283. /**
  284. * Broadcasts the local statistics to all other participants in the
  285. * conference.
  286. */
  287. _broadcastLocalStats() {
  288. // Send only the data that remote participants care about.
  289. let data = {
  290. bitrate: this._localStats.bitrate,
  291. packetLoss: this._localStats.packetLoss,
  292. connectionQuality: this._localStats.connectionQuality
  293. };
  294. // TODO: It looks like the remote participants don't really "care"
  295. // about the resolution, and they look at their local rendered
  296. // resolution instead. Consider removing this.
  297. let localVideoTrack
  298. = this._conference.getLocalTracks(MediaType.VIDEO)
  299. .find(track => track.isVideoTrack());
  300. if (localVideoTrack && localVideoTrack.resolution) {
  301. data.resolution = localVideoTrack.resolution;
  302. }
  303. try {
  304. this._conference.broadcastEndpointMessage({
  305. type: STATS_MESSAGE_TYPE,
  306. values: data });
  307. } catch (e) {
  308. let errorMsg = "Failed to broadcast local stats";
  309. logger.error(errorMsg, e);
  310. GlobalOnErrorHandler.callErrorHandler(
  311. new Error(errorMsg + ": " + e));
  312. }
  313. }
  314. /**
  315. * Updates the local statistics
  316. * @param data new statistics
  317. */
  318. _updateLocalStats(data) {
  319. let key;
  320. let updateLocalConnectionQuality
  321. = !this._conference.isConnectionInterrupted();
  322. let localVideoTrack =
  323. this._conference.getLocalTracks(MediaType.VIDEO)
  324. .find(track => track.isVideoTrack());
  325. let videoType = localVideoTrack ? localVideoTrack.videoType : undefined;
  326. let isMuted = localVideoTrack ? localVideoTrack.isMuted() : true;
  327. let resolution = localVideoTrack ? localVideoTrack.resolution : null;
  328. if (!isMuted) {
  329. this._maybeUpdateUnmuteTime();
  330. }
  331. // Copy the fields already in 'data'.
  332. for (key in data) {
  333. if (data.hasOwnProperty(key)) {
  334. this._localStats[key] = data[key];
  335. }
  336. }
  337. // And re-calculate the connectionQuality field.
  338. if (updateLocalConnectionQuality) {
  339. this._updateLocalConnectionQuality(
  340. this._calculateConnectionQuality(
  341. videoType,
  342. isMuted,
  343. resolution));
  344. }
  345. this.eventEmitter.emit(
  346. ConnectionQualityEvents.LOCAL_STATS_UPDATED,
  347. this._localStats);
  348. this._broadcastLocalStats();
  349. }
  350. /**
  351. * Updates remote statistics
  352. * @param id the id of the remote participant
  353. * @param data the statistics received
  354. */
  355. _updateRemoteStats(id, data) {
  356. // Use only the fields we need
  357. this._remoteStats[id] = {
  358. bitrate: data.bitrate,
  359. packetLoss: data.packetLoss,
  360. connectionQuality: data.connectionQuality
  361. };
  362. this.eventEmitter.emit(
  363. ConnectionQualityEvents.REMOTE_STATS_UPDATED,
  364. id,
  365. this._remoteStats[id]);
  366. }
  367. /**
  368. * Returns the local statistics.
  369. * Exported only for use in jitsi-meet-torture.
  370. */
  371. getStats() {
  372. return this._localStats;
  373. }
  374. }