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.

ParticipantConnectionStatus.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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. /**
  9. * Default value of 2000 milliseconds for
  10. * {@link ParticipantConnectionStatus.rtcMuteTimeout}.
  11. *
  12. * @type {number}
  13. */
  14. const DEFAULT_RTC_MUTE_TIMEOUT = 2000;
  15. /**
  16. * Class is responsible for emitting
  17. * JitsiConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED events.
  18. *
  19. * @constructor
  20. * @param {RTC} rtc the RTC service instance
  21. * @param {JitsiConference} conference parent conference instance
  22. * @param {number} rtcMuteTimeout (optional) custom value for
  23. * {@link ParticipantConnectionStatus.rtcMuteTimeout}.
  24. */
  25. function ParticipantConnectionStatus(rtc, conference, rtcMuteTimeout) {
  26. this.rtc = rtc;
  27. this.conference = conference;
  28. /**
  29. * A map of the "endpoint ID"(which corresponds to the resource part of MUC
  30. * JID(nickname)) to the timeout callback IDs scheduled using
  31. * window.setTimeout.
  32. * @type {Object.<string, number>}
  33. */
  34. this.trackTimers = {};
  35. /**
  36. * How long we're going to wait after the RTC video track muted event for
  37. * the corresponding signalling mute event, before the connection
  38. * interrupted is fired. The default value is
  39. * {@link DEFAULT_RTC_MUTE_TIMEOUT}.
  40. *
  41. * @type {number} amount of time in milliseconds
  42. */
  43. this.rtcMuteTimeout
  44. = typeof rtcMuteTimeout === 'number'
  45. ? rtcMuteTimeout : DEFAULT_RTC_MUTE_TIMEOUT;
  46. logger.info("RtcMuteTimeout set to: " + this.rtcMuteTimeout);
  47. }
  48. /**
  49. * Initializes <tt>ParticipantConnectionStatus</tt> and bind required event
  50. * listeners.
  51. */
  52. ParticipantConnectionStatus.prototype.init = function() {
  53. this._onEndpointConnStatusChanged
  54. = this.onEndpointConnStatusChanged.bind(this);
  55. this.rtc.addListener(
  56. RTCEvents.ENDPOINT_CONN_STATUS_CHANGED,
  57. this._onEndpointConnStatusChanged);
  58. // On some browsers MediaStreamTrack trigger "onmute"/"onunmute"
  59. // events for video type tracks when they stop receiving data which is
  60. // often a sign that remote user is having connectivity issues
  61. if (RTCBrowserType.isVideoMuteOnConnInterruptedSupported()) {
  62. this._onTrackRtcMuted = this.onTrackRtcMuted.bind(this);
  63. this.rtc.addListener(
  64. RTCEvents.REMOTE_TRACK_MUTE, this._onTrackRtcMuted);
  65. this._onTrackRtcUnmuted = this.onTrackRtcUnmuted.bind(this);
  66. this.rtc.addListener(
  67. RTCEvents.REMOTE_TRACK_UNMUTE, this._onTrackRtcUnmuted);
  68. // Track added/removed listeners are used to bind "mute"/"unmute"
  69. // event handlers
  70. this._onRemoteTrackAdded = this.onRemoteTrackAdded.bind(this);
  71. this.conference.on(
  72. JitsiConferenceEvents.TRACK_ADDED, this._onRemoteTrackAdded);
  73. this._onRemoteTrackRemoved = this.onRemoteTrackRemoved.bind(this);
  74. this.conference.on(
  75. JitsiConferenceEvents.TRACK_REMOVED, this._onRemoteTrackRemoved);
  76. // Listened which will be bound to JitsiRemoteTrack to listen for
  77. // signalling mute/unmute events.
  78. this._onSignallingMuteChanged = this.onSignallingMuteChanged.bind(this);
  79. }
  80. };
  81. /**
  82. * Removes all event listeners and disposes of all resources held by this
  83. * instance.
  84. */
  85. ParticipantConnectionStatus.prototype.dispose = function () {
  86. this.rtc.removeListener(
  87. RTCEvents.ENDPOINT_CONN_STATUS_CHANGED,
  88. this._onEndpointConnStatusChanged);
  89. if (RTCBrowserType.isVideoMuteOnConnInterruptedSupported()) {
  90. this.rtc.removeListener(
  91. RTCEvents.REMOTE_TRACK_MUTE, this._onTrackRtcMuted);
  92. this.rtc.removeListener(
  93. RTCEvents.REMOTE_TRACK_UNMUTE, this._onTrackRtcUnmuted);
  94. this.conference.off(
  95. JitsiConferenceEvents.TRACK_ADDED, this._onRemoteTrackAdded);
  96. this.conference.off(
  97. JitsiConferenceEvents.TRACK_REMOVED, this._onRemoteTrackRemoved);
  98. }
  99. Object.keys(this.trackTimers).forEach(function (participantId) {
  100. this.clearTimeout(participantId);
  101. }.bind(this));
  102. };
  103. /**
  104. * Checks whether given <tt>JitsiParticipant</tt> has any muted video
  105. * <tt>MediaStreamTrack</tt>s.
  106. *
  107. * @param {JitsiParticipant} participant to be checked for muted video tracks
  108. *
  109. * @return {boolean} <tt>true</tt> if given <tt>participant</tt> contains any
  110. * video <tt>MediaStreamTrack</tt>s muted according to their 'muted' field.
  111. */
  112. var hasRtcMutedVideoTrack = function (participant) {
  113. return participant.getTracks().some(function(jitsiTrack) {
  114. var rtcTrack = jitsiTrack.getTrack();
  115. return jitsiTrack.getType() === MediaType.VIDEO
  116. && rtcTrack && rtcTrack.muted === true;
  117. });
  118. };
  119. /**
  120. * Handles RTCEvents.ENDPOINT_CONN_STATUS_CHANGED triggered when we receive
  121. * notification over the data channel from the bridge about endpoint's
  122. * connection status update.
  123. * @param endpointId {string} the endpoint ID(MUC nickname/resource JID)
  124. * @param isActive {boolean} true if the connection is OK or false otherwise
  125. */
  126. ParticipantConnectionStatus.prototype.onEndpointConnStatusChanged
  127. = function(endpointId, isActive) {
  128. logger.debug(
  129. 'Detector RTCEvents.ENDPOINT_CONN_STATUS_CHANGED('
  130. + Date.now() +'): ' + endpointId + ': ' + isActive);
  131. // Filter out events for the local JID for now
  132. if (endpointId !== this.conference.myUserId()) {
  133. var participant = this.conference.getParticipantById(endpointId);
  134. // Delay the 'active' event until the video track gets RTC unmuted event
  135. if (isActive
  136. && RTCBrowserType.isVideoMuteOnConnInterruptedSupported()
  137. && participant
  138. && hasRtcMutedVideoTrack(participant)
  139. && !participant.isVideoMuted()) {
  140. logger.debug(
  141. 'Ignoring RTCEvents.ENDPOINT_CONN_STATUS_CHANGED -'
  142. + ' will wait for unmute event');
  143. } else {
  144. this._changeConnectionStatus(endpointId, isActive);
  145. }
  146. }
  147. };
  148. ParticipantConnectionStatus.prototype._changeConnectionStatus
  149. = function (endpointId, newStatus) {
  150. var participant = this.conference.getParticipantById(endpointId);
  151. if (!participant) {
  152. // This will happen when participant exits the conference with broken
  153. // ICE connection and we join after that. The bridge keeps sending
  154. // that notification until the conference does not expire.
  155. logger.warn(
  156. 'Missed participant connection status update - ' +
  157. 'no participant for endpoint: ' + endpointId);
  158. return;
  159. }
  160. if (participant.isConnectionActive() !== newStatus) {
  161. participant._setIsConnectionActive(newStatus);
  162. logger.debug(
  163. 'Emit endpoint conn status(' + Date.now() + '): ',
  164. endpointId, newStatus);
  165. this.conference.eventEmitter.emit(
  166. JitsiConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
  167. endpointId, newStatus);
  168. }
  169. };
  170. /**
  171. * Reset the postponed "connection interrupted" event which was previously
  172. * scheduled as a timeout on RTC 'onmute' event.
  173. *
  174. * @param participantId the participant for which the "connection interrupted"
  175. * timeout was scheduled
  176. */
  177. ParticipantConnectionStatus.prototype.clearTimeout = function (participantId) {
  178. if (this.trackTimers[participantId]) {
  179. window.clearTimeout(this.trackTimers[participantId]);
  180. this.trackTimers[participantId] = null;
  181. }
  182. };
  183. /**
  184. * Bind signalling mute event listeners for video {JitsiRemoteTrack} when
  185. * a new one is added to the conference.
  186. *
  187. * @param {JitsiTrack} remoteTrack the {JitsiTrack} which is being added to
  188. * the conference.
  189. */
  190. ParticipantConnectionStatus.prototype.onRemoteTrackAdded
  191. = function(remoteTrack) {
  192. if (!remoteTrack.isLocal() && remoteTrack.getType() === MediaType.VIDEO) {
  193. logger.debug(
  194. 'Detector on remote track added: ', remoteTrack.getParticipantId());
  195. remoteTrack.on(
  196. JitsiTrackEvents.TRACK_MUTE_CHANGED,
  197. this._onSignallingMuteChanged);
  198. }
  199. };
  200. /**
  201. * Removes all event listeners bound to the remote video track and clears any
  202. * related timeouts.
  203. *
  204. * @param {JitsiRemoteTrack} remoteTrack the remote track which is being removed
  205. * from the conference.
  206. */
  207. ParticipantConnectionStatus.prototype.onRemoteTrackRemoved
  208. = function(remoteTrack) {
  209. if (!remoteTrack.isLocal() && remoteTrack.getType() === MediaType.VIDEO) {
  210. logger.debug(
  211. 'Detector on remote track removed: ',
  212. remoteTrack.getParticipantId());
  213. remoteTrack.off(
  214. JitsiTrackEvents.TRACK_MUTE_CHANGED,
  215. this._onSignallingMuteChanged);
  216. this.clearTimeout(remoteTrack.getParticipantId());
  217. }
  218. };
  219. /**
  220. * Handles RTC 'onmute' event for the video track.
  221. *
  222. * @param {JitsiRemoteTrack} track the video track for which 'onmute' event will
  223. * be processed.
  224. */
  225. ParticipantConnectionStatus.prototype.onTrackRtcMuted = function(track) {
  226. var participantId = track.getParticipantId();
  227. var participant = this.conference.getParticipantById(participantId);
  228. logger.debug('Detector track RTC muted: ', participantId);
  229. if (!participant) {
  230. logger.error('No participant for id: ' + participantId);
  231. return;
  232. }
  233. if (!participant.isVideoMuted()) {
  234. // If the user is not muted according to the signalling we'll give it
  235. // some time, before the connection interrupted event is triggered.
  236. this.trackTimers[participantId] = window.setTimeout(function () {
  237. if (!track.isMuted() && participant.isConnectionActive()) {
  238. logger.info(
  239. 'Connection interrupted through the RTC mute: '
  240. + participantId, Date.now());
  241. this._changeConnectionStatus(participantId, false);
  242. }
  243. this.clearTimeout(participantId);
  244. }.bind(this), this.rtcMuteTimeout);
  245. }
  246. };
  247. /**
  248. * Handles RTC 'onunmute' event for the video track.
  249. *
  250. * @param {JitsiRemoteTrack} track the video track for which 'onunmute' event
  251. * will be processed.
  252. */
  253. ParticipantConnectionStatus.prototype.onTrackRtcUnmuted = function(track) {
  254. logger.debug('Detector track RTC unmuted: ', track);
  255. var participantId = track.getParticipantId();
  256. if (!track.isMuted() &&
  257. !this.conference.getParticipantById(participantId)
  258. .isConnectionActive()) {
  259. logger.info(
  260. 'Detector connection restored through the RTC unmute: '
  261. + participantId, Date.now());
  262. this._changeConnectionStatus(participantId, true);
  263. }
  264. this.clearTimeout(participantId);
  265. };
  266. /**
  267. * Here the signalling "mute"/"unmute" events are processed.
  268. *
  269. * @param {JitsiRemoteTrack} track the remote video track for which
  270. * the signalling mute/unmute event will be processed.
  271. */
  272. ParticipantConnectionStatus.prototype.onSignallingMuteChanged
  273. = function (track) {
  274. logger.debug(
  275. 'Detector on track signalling mute changed: ', track, track.isMuted());
  276. var isMuted = track.isMuted();
  277. var participantId = track.getParticipantId();
  278. var participant = this.conference.getParticipantById(participantId);
  279. if (!participant) {
  280. logger.error('No participant for id: ' + participantId);
  281. return;
  282. }
  283. var isConnectionActive = participant.isConnectionActive();
  284. if (isMuted && isConnectionActive && this.trackTimers[participantId]) {
  285. logger.debug(
  286. 'Signalling got in sync - cancelling task for: ' + participantId);
  287. this.clearTimeout(participantId);
  288. }
  289. };
  290. module.exports = ParticipantConnectionStatus;