Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

ParticipantConnectionStatus.js 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. /* global __filename, module, require */
  2. var logger = require('jitsi-meet-logger').getLogger(__filename);
  3. var MediaType = require('../../service/RTC/MediaType');
  4. var RTCBrowserType = require('../RTC/RTCBrowserType');
  5. var RTCEvents = require('../../service/RTC/RTCEvents');
  6. import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
  7. import * as JitsiTrackEvents from '../../JitsiTrackEvents';
  8. import Statistics from '../statistics/statistics';
  9. /**
  10. * Default value of 2000 milliseconds for
  11. * {@link ParticipantConnectionStatus.rtcMuteTimeout}.
  12. *
  13. * @type {number}
  14. */
  15. const DEFAULT_RTC_MUTE_TIMEOUT = 2000;
  16. /**
  17. * Class is responsible for emitting
  18. * JitsiConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED events.
  19. */
  20. export default class ParticipantConnectionStatus {
  21. /**
  22. * Creates new instance of <tt>ParticipantConnectionStatus</tt>.
  23. *
  24. * @constructor
  25. * @param {RTC} rtc the RTC service instance
  26. * @param {JitsiConference} conference parent conference instance
  27. * @param {number} rtcMuteTimeout (optional) custom value for
  28. * {@link ParticipantConnectionStatus.rtcMuteTimeout}.
  29. */
  30. constructor(rtc, conference, rtcMuteTimeout) {
  31. this.rtc = rtc;
  32. this.conference = conference;
  33. /**
  34. * A map of the "endpoint ID"(which corresponds to the resource part
  35. * of MUC JID(nickname)) to the timeout callback IDs scheduled using
  36. * window.setTimeout.
  37. * @type {Object.<string, number>}
  38. */
  39. this.trackTimers = {};
  40. /**
  41. * This map holds the endpoint connection status received from the JVB
  42. * (as it might be different than the one stored in JitsiParticipant).
  43. * Required for getting back in sync when remote video track is removed.
  44. * @type {Object.<string, boolean>}
  45. */
  46. this.rtcConnStatusCache = { };
  47. /**
  48. * How long we're going to wait after the RTC video track muted event
  49. * for the corresponding signalling mute event, before the connection
  50. * interrupted is fired. The default value is
  51. * {@link DEFAULT_RTC_MUTE_TIMEOUT}.
  52. *
  53. * @type {number} amount of time in milliseconds
  54. */
  55. this.rtcMuteTimeout
  56. = typeof rtcMuteTimeout === 'number'
  57. ? rtcMuteTimeout : DEFAULT_RTC_MUTE_TIMEOUT;
  58. logger.info("RtcMuteTimeout set to: " + this.rtcMuteTimeout);
  59. }
  60. /**
  61. * Initializes <tt>ParticipantConnectionStatus</tt> and bind required event
  62. * listeners.
  63. */
  64. init() {
  65. this._onEndpointConnStatusChanged
  66. = this.onEndpointConnStatusChanged.bind(this);
  67. this.rtc.addListener(
  68. RTCEvents.ENDPOINT_CONN_STATUS_CHANGED,
  69. this._onEndpointConnStatusChanged);
  70. // On some browsers MediaStreamTrack trigger "onmute"/"onunmute"
  71. // events for video type tracks when they stop receiving data which is
  72. // often a sign that remote user is having connectivity issues
  73. if (RTCBrowserType.isVideoMuteOnConnInterruptedSupported()) {
  74. this._onTrackRtcMuted = this.onTrackRtcMuted.bind(this);
  75. this.rtc.addListener(
  76. RTCEvents.REMOTE_TRACK_MUTE, this._onTrackRtcMuted);
  77. this._onTrackRtcUnmuted = this.onTrackRtcUnmuted.bind(this);
  78. this.rtc.addListener(
  79. RTCEvents.REMOTE_TRACK_UNMUTE, this._onTrackRtcUnmuted);
  80. // Track added/removed listeners are used to bind "mute"/"unmute"
  81. // event handlers
  82. this._onRemoteTrackAdded = this.onRemoteTrackAdded.bind(this);
  83. this.conference.on(
  84. JitsiConferenceEvents.TRACK_ADDED,
  85. this._onRemoteTrackAdded);
  86. this._onRemoteTrackRemoved = this.onRemoteTrackRemoved.bind(this);
  87. this.conference.on(
  88. JitsiConferenceEvents.TRACK_REMOVED,
  89. this._onRemoteTrackRemoved);
  90. // Listened which will be bound to JitsiRemoteTrack to listen for
  91. // signalling mute/unmute events.
  92. this._onSignallingMuteChanged
  93. = this.onSignallingMuteChanged.bind(this);
  94. }
  95. }
  96. /**
  97. * Removes all event listeners and disposes of all resources held by this
  98. * instance.
  99. */
  100. dispose() {
  101. this.rtc.removeListener(
  102. RTCEvents.ENDPOINT_CONN_STATUS_CHANGED,
  103. this._onEndpointConnStatusChanged);
  104. if (RTCBrowserType.isVideoMuteOnConnInterruptedSupported()) {
  105. this.rtc.removeListener(
  106. RTCEvents.REMOTE_TRACK_MUTE,
  107. this._onTrackRtcMuted);
  108. this.rtc.removeListener(
  109. RTCEvents.REMOTE_TRACK_UNMUTE,
  110. this._onTrackRtcUnmuted);
  111. this.conference.off(
  112. JitsiConferenceEvents.TRACK_ADDED,
  113. this._onRemoteTrackAdded);
  114. this.conference.off(
  115. JitsiConferenceEvents.TRACK_REMOVED,
  116. this._onRemoteTrackRemoved);
  117. }
  118. Object.keys(this.trackTimers).forEach(function (participantId) {
  119. this.clearTimeout(participantId);
  120. }.bind(this));
  121. // Clear RTC connection status cache
  122. this.rtcConnStatusCache = {};
  123. }
  124. /**
  125. * Handles RTCEvents.ENDPOINT_CONN_STATUS_CHANGED triggered when we receive
  126. * notification over the data channel from the bridge about endpoint's
  127. * connection status update.
  128. * @param endpointId {string} the endpoint ID(MUC nickname/resource JID)
  129. * @param isActive {boolean} true if the connection is OK or false otherwise
  130. */
  131. onEndpointConnStatusChanged(endpointId, isActive) {
  132. logger.debug(
  133. 'Detector RTCEvents.ENDPOINT_CONN_STATUS_CHANGED('
  134. + Date.now() +'): ' + endpointId + ': ' + isActive);
  135. // Filter out events for the local JID for now
  136. if (endpointId !== this.conference.myUserId()) {
  137. // Cache the status received received over the data channels, as
  138. // it will be needed to verify for out of sync when the remote video
  139. // track is being removed.
  140. this.rtcConnStatusCache[endpointId] = isActive;
  141. var participant = this.conference.getParticipantById(endpointId);
  142. // Delay the 'active' event until the video track gets
  143. // the RTC unmuted event
  144. if (isActive
  145. && RTCBrowserType.isVideoMuteOnConnInterruptedSupported()
  146. && participant
  147. && participant.hasAnyVideoTrackWebRTCMuted()
  148. && !participant.isVideoMuted()) {
  149. logger.debug(
  150. 'Ignoring RTCEvents.ENDPOINT_CONN_STATUS_CHANGED -'
  151. + ' will wait for unmute event');
  152. } else {
  153. this._changeConnectionStatus(endpointId, isActive);
  154. }
  155. }
  156. }
  157. _changeConnectionStatus(endpointId, newStatus) {
  158. var participant = this.conference.getParticipantById(endpointId);
  159. if (!participant) {
  160. // This will happen when participant exits the conference with
  161. // broken ICE connection and we join after that. The bridge keeps
  162. // sending that notification until the conference does not expire.
  163. logger.warn(
  164. 'Missed participant connection status update - ' +
  165. 'no participant for endpoint: ' + endpointId);
  166. return;
  167. }
  168. if (participant.isConnectionActive() !== newStatus) {
  169. participant._setIsConnectionActive(newStatus);
  170. logger.debug(
  171. 'Emit endpoint conn status(' + Date.now() + ') '
  172. + endpointId + ": " + newStatus);
  173. // Log the event on CallStats
  174. Statistics.sendLog(
  175. JSON.stringify({
  176. id: 'peer.conn.status',
  177. participant: endpointId,
  178. status: newStatus
  179. }));
  180. // and analytics
  181. Statistics.analytics.sendEvent('peer.conn.status',
  182. {label: newStatus});
  183. this.conference.eventEmitter.emit(
  184. JitsiConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
  185. endpointId, newStatus);
  186. }
  187. }
  188. /**
  189. * Reset the postponed "connection interrupted" event which was previously
  190. * scheduled as a timeout on RTC 'onmute' event.
  191. *
  192. * @param participantId the participant for which the "connection
  193. * interrupted" timeout was scheduled
  194. */
  195. clearTimeout(participantId) {
  196. if (this.trackTimers[participantId]) {
  197. window.clearTimeout(this.trackTimers[participantId]);
  198. this.trackTimers[participantId] = null;
  199. }
  200. }
  201. /**
  202. * Bind signalling mute event listeners for video {JitsiRemoteTrack} when
  203. * a new one is added to the conference.
  204. *
  205. * @param {JitsiTrack} remoteTrack the {JitsiTrack} which is being added to
  206. * the conference.
  207. */
  208. onRemoteTrackAdded(remoteTrack) {
  209. if (!remoteTrack.isLocal()
  210. && remoteTrack.getType() === MediaType.VIDEO) {
  211. logger.debug(
  212. 'Detector on remote track added for: '
  213. + remoteTrack.getParticipantId());
  214. remoteTrack.on(
  215. JitsiTrackEvents.TRACK_MUTE_CHANGED,
  216. this._onSignallingMuteChanged);
  217. }
  218. }
  219. /**
  220. * Removes all event listeners bound to the remote video track and clears
  221. * any related timeouts.
  222. *
  223. * @param {JitsiRemoteTrack} remoteTrack the remote track which is being
  224. * removed from the conference.
  225. */
  226. onRemoteTrackRemoved(remoteTrack) {
  227. if (!remoteTrack.isLocal()
  228. && remoteTrack.getType() === MediaType.VIDEO) {
  229. const endpointId = remoteTrack.getParticipantId();
  230. logger.debug(
  231. 'Detector on remote track removed: ' + endpointId);
  232. remoteTrack.off(
  233. JitsiTrackEvents.TRACK_MUTE_CHANGED,
  234. this._onSignallingMuteChanged);
  235. this.clearTimeout(endpointId);
  236. // Only if we're using video muted events - check if the JVB status
  237. // should be restored from cache.
  238. if (RTCBrowserType.isVideoMuteOnConnInterruptedSupported())
  239. {
  240. this.maybeRestoreCachedStatus(endpointId);
  241. }
  242. }
  243. }
  244. /**
  245. * When RTC video track muted events are taken into account,
  246. * at the point when the track is being removed we have to update
  247. * to the current connectivity status according to the JVB. That's
  248. * because if the current track is muted then the new one which
  249. * replaces it is always added as unmuted and there may be no
  250. * 'muted'/'unmuted' event sequence if the connection restores in
  251. * the meantime.
  252. *
  253. * XXX See onEndpointConnStatusChanged method where the update is
  254. * postponed and which is the cause for this workaround. If we
  255. * decide to not wait for video unmuted event and accept the JVB
  256. * status immediately then it's fine to remove the code below.
  257. */
  258. maybeRestoreCachedStatus(endpointId) {
  259. var participant = this.conference.getParticipantById(endpointId);
  260. if (!participant) {
  261. // Probably the participant is no longer in the conference
  262. // (at the time of writing this code, participant is
  263. // detached from the conference and TRACK_REMOVED events are
  264. // fired),
  265. // so we don't care, but let's print the warning for
  266. // debugging purpose
  267. logger.warn(
  268. 'maybeRestoreCachedStatus - ' +
  269. 'no participant for endpoint: ' + endpointId);
  270. return;
  271. }
  272. const isConnectionActive = participant.isConnectionActive();
  273. const hasAnyVideoRTCMuted = participant.hasAnyVideoTrackWebRTCMuted();
  274. let isConnActiveByJvb = this.rtcConnStatusCache[endpointId];
  275. // If no status was received from the JVB it means that it's active
  276. // (the bridge does not send notification unless there is a problem).
  277. if (typeof isConnActiveByJvb !== 'boolean') {
  278. logger.debug("Assuming connection active by JVB - no notification");
  279. isConnActiveByJvb = true;
  280. }
  281. logger.debug(
  282. "Remote track removed, is active: " + isConnectionActive
  283. + " is active(jvb):" + isConnActiveByJvb
  284. + " video RTC muted:" + hasAnyVideoRTCMuted);
  285. if (!isConnectionActive && isConnActiveByJvb && !hasAnyVideoRTCMuted) {
  286. // FIXME adjust the log level or remove the message completely once
  287. // the feature gets mature enough.
  288. logger.info(
  289. "Remote track removed for disconnected" +
  290. " participant, when the status according to" +
  291. " the JVB is connected. Adjusting to the JVB value for: "
  292. + endpointId);
  293. this._changeConnectionStatus(endpointId, isConnActiveByJvb);
  294. }
  295. }
  296. /**
  297. * Handles RTC 'onmute' event for the video track.
  298. *
  299. * @param {JitsiRemoteTrack} track the video track for which 'onmute' event
  300. * will be processed.
  301. */
  302. onTrackRtcMuted(track) {
  303. var participantId = track.getParticipantId();
  304. var participant = this.conference.getParticipantById(participantId);
  305. logger.debug('Detector track RTC muted: ' + participantId);
  306. if (!participant) {
  307. logger.error('No participant for id: ' + participantId);
  308. return;
  309. }
  310. if (!participant.isVideoMuted()) {
  311. // If the user is not muted according to the signalling we'll give
  312. // it some time, before the connection interrupted event is
  313. // triggered.
  314. this.trackTimers[participantId] = window.setTimeout(function () {
  315. if (!track.isMuted() && participant.isConnectionActive()) {
  316. logger.info(
  317. 'Connection interrupted through the RTC mute: '
  318. + participantId, Date.now());
  319. this._changeConnectionStatus(participantId, false);
  320. }
  321. this.clearTimeout(participantId);
  322. }.bind(this), this.rtcMuteTimeout);
  323. }
  324. }
  325. /**
  326. * Handles RTC 'onunmute' event for the video track.
  327. *
  328. * @param {JitsiRemoteTrack} track the video track for which 'onunmute'
  329. * event will be processed.
  330. */
  331. onTrackRtcUnmuted(track) {
  332. var participantId = track.getParticipantId();
  333. logger.debug('Detector track RTC unmuted: ' + participantId);
  334. if (!track.isMuted() &&
  335. !this.conference.getParticipantById(participantId)
  336. .isConnectionActive()) {
  337. logger.info(
  338. 'Detector connection restored through the RTC unmute: '
  339. + participantId, Date.now());
  340. this._changeConnectionStatus(participantId, true);
  341. }
  342. this.clearTimeout(participantId);
  343. }
  344. /**
  345. * Here the signalling "mute"/"unmute" events are processed.
  346. *
  347. * @param {JitsiRemoteTrack} track the remote video track for which
  348. * the signalling mute/unmute event will be processed.
  349. */
  350. onSignallingMuteChanged (track) {
  351. var isMuted = track.isMuted();
  352. var participantId = track.getParticipantId();
  353. logger.debug(
  354. 'Detector on track signalling mute changed: ',
  355. participantId, track.isMuted());
  356. var participant = this.conference.getParticipantById(participantId);
  357. if (!participant) {
  358. logger.error('No participant for id: ' + participantId);
  359. return;
  360. }
  361. var isConnectionActive = participant.isConnectionActive();
  362. if (isMuted && isConnectionActive && this.trackTimers[participantId]) {
  363. logger.debug(
  364. 'Signalling got in sync - cancelling task for: '
  365. + participantId);
  366. this.clearTimeout(participantId);
  367. }
  368. }
  369. }