@abrar71/lib-jitsi-meet 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +26 -0
  3. package/dist/esm/JitsiConference.js +3692 -0
  4. package/dist/esm/JitsiConference.js.map +1 -0
  5. package/dist/esm/JitsiConferenceErrors.js +126 -0
  6. package/dist/esm/JitsiConferenceErrors.js.map +1 -0
  7. package/dist/esm/JitsiConferenceEventManager.js +424 -0
  8. package/dist/esm/JitsiConferenceEventManager.js.map +1 -0
  9. package/dist/esm/JitsiConferenceEvents.js +431 -0
  10. package/dist/esm/JitsiConferenceEvents.js.map +1 -0
  11. package/dist/esm/JitsiConnection.js +187 -0
  12. package/dist/esm/JitsiConnection.js.map +1 -0
  13. package/dist/esm/JitsiConnectionErrors.js +52 -0
  14. package/dist/esm/JitsiConnectionErrors.js.map +1 -0
  15. package/dist/esm/JitsiConnectionEvents.js +57 -0
  16. package/dist/esm/JitsiConnectionEvents.js.map +1 -0
  17. package/dist/esm/JitsiMediaDevices.js +221 -0
  18. package/dist/esm/JitsiMediaDevices.js.map +1 -0
  19. package/dist/esm/JitsiMediaDevicesEvents.js +29 -0
  20. package/dist/esm/JitsiMediaDevicesEvents.js.map +1 -0
  21. package/dist/esm/JitsiMeetJS.js +499 -0
  22. package/dist/esm/JitsiMeetJS.js.map +1 -0
  23. package/dist/esm/JitsiParticipant.js +323 -0
  24. package/dist/esm/JitsiParticipant.js.map +1 -0
  25. package/dist/esm/JitsiTrackError.js +122 -0
  26. package/dist/esm/JitsiTrackError.js.map +1 -0
  27. package/dist/esm/JitsiTrackErrors.js +91 -0
  28. package/dist/esm/JitsiTrackErrors.js.map +1 -0
  29. package/dist/esm/JitsiTrackEvents.js +60 -0
  30. package/dist/esm/JitsiTrackEvents.js.map +1 -0
  31. package/dist/esm/JitsiTranscriptionStatus.js +15 -0
  32. package/dist/esm/JitsiTranscriptionStatus.js.map +1 -0
  33. package/dist/esm/modules/RTC/BridgeChannel.js +398 -0
  34. package/dist/esm/modules/RTC/BridgeChannel.js.map +1 -0
  35. package/dist/esm/modules/RTC/JitsiLocalTrack.js +896 -0
  36. package/dist/esm/modules/RTC/JitsiLocalTrack.js.map +1 -0
  37. package/dist/esm/modules/RTC/JitsiRemoteTrack.js +427 -0
  38. package/dist/esm/modules/RTC/JitsiRemoteTrack.js.map +1 -0
  39. package/dist/esm/modules/RTC/JitsiTrack.js +453 -0
  40. package/dist/esm/modules/RTC/JitsiTrack.js.map +1 -0
  41. package/dist/esm/modules/RTC/MockClasses.js +388 -0
  42. package/dist/esm/modules/RTC/MockClasses.js.map +1 -0
  43. package/dist/esm/modules/RTC/RTC.js +658 -0
  44. package/dist/esm/modules/RTC/RTC.js.map +1 -0
  45. package/dist/esm/modules/RTC/RTCUtils.js +762 -0
  46. package/dist/esm/modules/RTC/RTCUtils.js.map +1 -0
  47. package/dist/esm/modules/RTC/ScreenObtainer.js +380 -0
  48. package/dist/esm/modules/RTC/ScreenObtainer.js.map +1 -0
  49. package/dist/esm/modules/RTC/TPCUtils.js +803 -0
  50. package/dist/esm/modules/RTC/TPCUtils.js.map +1 -0
  51. package/dist/esm/modules/RTC/TraceablePeerConnection.js +2223 -0
  52. package/dist/esm/modules/RTC/TraceablePeerConnection.js.map +1 -0
  53. package/dist/esm/modules/RTCStats/DefaulLogStorage.js +35 -0
  54. package/dist/esm/modules/RTCStats/DefaulLogStorage.js.map +1 -0
  55. package/dist/esm/modules/RTCStats/RTCStats.js +219 -0
  56. package/dist/esm/modules/RTCStats/RTCStats.js.map +1 -0
  57. package/dist/esm/modules/RTCStats/RTCStatsEvents.js +92 -0
  58. package/dist/esm/modules/RTCStats/RTCStatsEvents.js.map +1 -0
  59. package/dist/esm/modules/RTCStats/interfaces.js +2 -0
  60. package/dist/esm/modules/RTCStats/interfaces.js.map +1 -0
  61. package/dist/esm/modules/browser/BrowserCapabilities.js +345 -0
  62. package/dist/esm/modules/browser/BrowserCapabilities.js.map +1 -0
  63. package/dist/esm/modules/browser/index.js +3 -0
  64. package/dist/esm/modules/browser/index.js.map +1 -0
  65. package/dist/esm/modules/connectivity/ConnectionQuality.js +389 -0
  66. package/dist/esm/modules/connectivity/ConnectionQuality.js.map +1 -0
  67. package/dist/esm/modules/connectivity/IceFailedHandling.js +84 -0
  68. package/dist/esm/modules/connectivity/IceFailedHandling.js.map +1 -0
  69. package/dist/esm/modules/connectivity/NetworkInfo.js +49 -0
  70. package/dist/esm/modules/connectivity/NetworkInfo.js.map +1 -0
  71. package/dist/esm/modules/connectivity/TrackStreamingStatus.js +453 -0
  72. package/dist/esm/modules/connectivity/TrackStreamingStatus.js.map +1 -0
  73. package/dist/esm/modules/detection/ActiveDeviceDetector.js +79 -0
  74. package/dist/esm/modules/detection/ActiveDeviceDetector.js.map +1 -0
  75. package/dist/esm/modules/detection/DetectionEvents.js +58 -0
  76. package/dist/esm/modules/detection/DetectionEvents.js.map +1 -0
  77. package/dist/esm/modules/detection/NoAudioSignalDetection.js +127 -0
  78. package/dist/esm/modules/detection/NoAudioSignalDetection.js.map +1 -0
  79. package/dist/esm/modules/detection/P2PDominantSpeakerDetection.js +47 -0
  80. package/dist/esm/modules/detection/P2PDominantSpeakerDetection.js.map +1 -0
  81. package/dist/esm/modules/detection/TrackVADEmitter.js +190 -0
  82. package/dist/esm/modules/detection/TrackVADEmitter.js.map +1 -0
  83. package/dist/esm/modules/detection/VADAudioAnalyser.js +199 -0
  84. package/dist/esm/modules/detection/VADAudioAnalyser.js.map +1 -0
  85. package/dist/esm/modules/detection/VADNoiseDetection.js +168 -0
  86. package/dist/esm/modules/detection/VADNoiseDetection.js.map +1 -0
  87. package/dist/esm/modules/detection/VADReportingService.js +203 -0
  88. package/dist/esm/modules/detection/VADReportingService.js.map +1 -0
  89. package/dist/esm/modules/detection/VADTalkMutedDetection.js +131 -0
  90. package/dist/esm/modules/detection/VADTalkMutedDetection.js.map +1 -0
  91. package/dist/esm/modules/e2ee/Context.js +274 -0
  92. package/dist/esm/modules/e2ee/Context.js.map +1 -0
  93. package/dist/esm/modules/e2ee/E2EEContext.js +158 -0
  94. package/dist/esm/modules/e2ee/E2EEContext.js.map +1 -0
  95. package/dist/esm/modules/e2ee/E2EEErrors.js +10 -0
  96. package/dist/esm/modules/e2ee/E2EEErrors.js.map +1 -0
  97. package/dist/esm/modules/e2ee/E2EEncryption.js +87 -0
  98. package/dist/esm/modules/e2ee/E2EEncryption.js.map +1 -0
  99. package/dist/esm/modules/e2ee/ExternallyManagedKeyHandler.js +24 -0
  100. package/dist/esm/modules/e2ee/ExternallyManagedKeyHandler.js.map +1 -0
  101. package/dist/esm/modules/e2ee/KeyHandler.js +137 -0
  102. package/dist/esm/modules/e2ee/KeyHandler.js.map +1 -0
  103. package/dist/esm/modules/e2ee/ManagedKeyHandler.js +182 -0
  104. package/dist/esm/modules/e2ee/ManagedKeyHandler.js.map +1 -0
  105. package/dist/esm/modules/e2ee/OlmAdapter.js +860 -0
  106. package/dist/esm/modules/e2ee/OlmAdapter.js.map +1 -0
  107. package/dist/esm/modules/e2ee/SAS.js +128 -0
  108. package/dist/esm/modules/e2ee/SAS.js.map +1 -0
  109. package/dist/esm/modules/e2ee/Worker.js +102 -0
  110. package/dist/esm/modules/e2ee/Worker.js.map +1 -0
  111. package/dist/esm/modules/e2ee/crypto-utils.js +53 -0
  112. package/dist/esm/modules/e2ee/crypto-utils.js.map +1 -0
  113. package/dist/esm/modules/e2ee/utils.js +15 -0
  114. package/dist/esm/modules/e2ee/utils.js.map +1 -0
  115. package/dist/esm/modules/e2eping/e2eping.js +314 -0
  116. package/dist/esm/modules/e2eping/e2eping.js.map +1 -0
  117. package/dist/esm/modules/flags/FeatureFlags.js +36 -0
  118. package/dist/esm/modules/flags/FeatureFlags.js.map +1 -0
  119. package/dist/esm/modules/litemode/LiteModeContext.js +50 -0
  120. package/dist/esm/modules/litemode/LiteModeContext.js.map +1 -0
  121. package/dist/esm/modules/proxyconnection/CustomSignalingLayer.js +98 -0
  122. package/dist/esm/modules/proxyconnection/CustomSignalingLayer.js.map +1 -0
  123. package/dist/esm/modules/proxyconnection/ProxyConnectionPC.js +348 -0
  124. package/dist/esm/modules/proxyconnection/ProxyConnectionPC.js.map +1 -0
  125. package/dist/esm/modules/proxyconnection/ProxyConnectionService.js +279 -0
  126. package/dist/esm/modules/proxyconnection/ProxyConnectionService.js.map +1 -0
  127. package/dist/esm/modules/proxyconnection/constants.js +14 -0
  128. package/dist/esm/modules/proxyconnection/constants.js.map +1 -0
  129. package/dist/esm/modules/qualitycontrol/CodecSelection.js +222 -0
  130. package/dist/esm/modules/qualitycontrol/CodecSelection.js.map +1 -0
  131. package/dist/esm/modules/qualitycontrol/MockClasses.js +120 -0
  132. package/dist/esm/modules/qualitycontrol/MockClasses.js.map +1 -0
  133. package/dist/esm/modules/qualitycontrol/QualityController.js +366 -0
  134. package/dist/esm/modules/qualitycontrol/QualityController.js.map +1 -0
  135. package/dist/esm/modules/qualitycontrol/ReceiveAudioController.js +73 -0
  136. package/dist/esm/modules/qualitycontrol/ReceiveAudioController.js.map +1 -0
  137. package/dist/esm/modules/qualitycontrol/ReceiveVideoController.js +216 -0
  138. package/dist/esm/modules/qualitycontrol/ReceiveVideoController.js.map +1 -0
  139. package/dist/esm/modules/qualitycontrol/SendVideoController.js +133 -0
  140. package/dist/esm/modules/qualitycontrol/SendVideoController.js.map +1 -0
  141. package/dist/esm/modules/recording/JibriSession.js +279 -0
  142. package/dist/esm/modules/recording/JibriSession.js.map +1 -0
  143. package/dist/esm/modules/recording/RecordingManager.js +257 -0
  144. package/dist/esm/modules/recording/RecordingManager.js.map +1 -0
  145. package/dist/esm/modules/recording/recordingConstants.js +21 -0
  146. package/dist/esm/modules/recording/recordingConstants.js.map +1 -0
  147. package/dist/esm/modules/recording/recordingXMLUtils.js +69 -0
  148. package/dist/esm/modules/recording/recordingXMLUtils.js.map +1 -0
  149. package/dist/esm/modules/red/red.js +108 -0
  150. package/dist/esm/modules/red/red.js.map +1 -0
  151. package/dist/esm/modules/sdp/LocalSdpMunger.js +143 -0
  152. package/dist/esm/modules/sdp/LocalSdpMunger.js.map +1 -0
  153. package/dist/esm/modules/sdp/RtxModifier.js +179 -0
  154. package/dist/esm/modules/sdp/RtxModifier.js.map +1 -0
  155. package/dist/esm/modules/sdp/SDP.js +848 -0
  156. package/dist/esm/modules/sdp/SDP.js.map +1 -0
  157. package/dist/esm/modules/sdp/SDPDiffer.js +96 -0
  158. package/dist/esm/modules/sdp/SDPDiffer.js.map +1 -0
  159. package/dist/esm/modules/sdp/SDPUtil.js +798 -0
  160. package/dist/esm/modules/sdp/SDPUtil.js.map +1 -0
  161. package/dist/esm/modules/sdp/SampleSdpStrings.js +589 -0
  162. package/dist/esm/modules/sdp/SampleSdpStrings.js.map +1 -0
  163. package/dist/esm/modules/sdp/SdpSimulcast.js +196 -0
  164. package/dist/esm/modules/sdp/SdpSimulcast.js.map +1 -0
  165. package/dist/esm/modules/sdp/SdpTransformUtil.js +337 -0
  166. package/dist/esm/modules/sdp/SdpTransformUtil.js.map +1 -0
  167. package/dist/esm/modules/sdp/constants.js +2 -0
  168. package/dist/esm/modules/sdp/constants.js.map +1 -0
  169. package/dist/esm/modules/settings/Settings.js +95 -0
  170. package/dist/esm/modules/settings/Settings.js.map +1 -0
  171. package/dist/esm/modules/statistics/AnalyticsAdapter.js +277 -0
  172. package/dist/esm/modules/statistics/AnalyticsAdapter.js.map +1 -0
  173. package/dist/esm/modules/statistics/AvgRTPStatsReporter.js +817 -0
  174. package/dist/esm/modules/statistics/AvgRTPStatsReporter.js.map +1 -0
  175. package/dist/esm/modules/statistics/LocalStatsCollector.js +149 -0
  176. package/dist/esm/modules/statistics/LocalStatsCollector.js.map +1 -0
  177. package/dist/esm/modules/statistics/PreCallTest.js +15 -0
  178. package/dist/esm/modules/statistics/PreCallTest.js.map +1 -0
  179. package/dist/esm/modules/statistics/RTPStatsCollector.js +601 -0
  180. package/dist/esm/modules/statistics/RTPStatsCollector.js.map +1 -0
  181. package/dist/esm/modules/statistics/SpeakerStats.js +163 -0
  182. package/dist/esm/modules/statistics/SpeakerStats.js.map +1 -0
  183. package/dist/esm/modules/statistics/SpeakerStatsCollector.js +161 -0
  184. package/dist/esm/modules/statistics/SpeakerStatsCollector.js.map +1 -0
  185. package/dist/esm/modules/statistics/constants.js +7 -0
  186. package/dist/esm/modules/statistics/constants.js.map +1 -0
  187. package/dist/esm/modules/statistics/statistics.js +362 -0
  188. package/dist/esm/modules/statistics/statistics.js.map +1 -0
  189. package/dist/esm/modules/util/AsyncQueue.js +102 -0
  190. package/dist/esm/modules/util/AsyncQueue.js.map +1 -0
  191. package/dist/esm/modules/util/Deferred.js +41 -0
  192. package/dist/esm/modules/util/Deferred.js.map +1 -0
  193. package/dist/esm/modules/util/EventEmitter.js +17 -0
  194. package/dist/esm/modules/util/EventEmitter.js.map +1 -0
  195. package/dist/esm/modules/util/EventEmitterForwarder.js +52 -0
  196. package/dist/esm/modules/util/EventEmitterForwarder.js.map +1 -0
  197. package/dist/esm/modules/util/Listenable.js +106 -0
  198. package/dist/esm/modules/util/Listenable.js.map +1 -0
  199. package/dist/esm/modules/util/MathUtil.js +103 -0
  200. package/dist/esm/modules/util/MathUtil.js.map +1 -0
  201. package/dist/esm/modules/util/RandomUtil.js +66 -0
  202. package/dist/esm/modules/util/RandomUtil.js.map +1 -0
  203. package/dist/esm/modules/util/Retry.js +15 -0
  204. package/dist/esm/modules/util/Retry.js.map +1 -0
  205. package/dist/esm/modules/util/ScriptUtil.js +58 -0
  206. package/dist/esm/modules/util/ScriptUtil.js.map +1 -0
  207. package/dist/esm/modules/util/StringUtils.js +21 -0
  208. package/dist/esm/modules/util/StringUtils.js.map +1 -0
  209. package/dist/esm/modules/util/TestUtils.js +14 -0
  210. package/dist/esm/modules/util/TestUtils.js.map +1 -0
  211. package/dist/esm/modules/util/UsernameGenerator.js +436 -0
  212. package/dist/esm/modules/util/UsernameGenerator.js.map +1 -0
  213. package/dist/esm/modules/util/XMLUtils.js +135 -0
  214. package/dist/esm/modules/util/XMLUtils.js.map +1 -0
  215. package/dist/esm/modules/version/ComponentsVersions.js +52 -0
  216. package/dist/esm/modules/version/ComponentsVersions.js.map +1 -0
  217. package/dist/esm/modules/videosipgw/JitsiVideoSIPGWSession.js +137 -0
  218. package/dist/esm/modules/videosipgw/JitsiVideoSIPGWSession.js.map +1 -0
  219. package/dist/esm/modules/videosipgw/VideoSIPGW.js +102 -0
  220. package/dist/esm/modules/videosipgw/VideoSIPGW.js.map +1 -0
  221. package/dist/esm/modules/videosipgw/VideoSIPGWConstants.js +65 -0
  222. package/dist/esm/modules/videosipgw/VideoSIPGWConstants.js.map +1 -0
  223. package/dist/esm/modules/watchRTC/WatchRTC.js +69 -0
  224. package/dist/esm/modules/watchRTC/WatchRTC.js.map +1 -0
  225. package/dist/esm/modules/watchRTC/functions.js +31 -0
  226. package/dist/esm/modules/watchRTC/functions.js.map +1 -0
  227. package/dist/esm/modules/watchRTC/interfaces.js +2 -0
  228. package/dist/esm/modules/watchRTC/interfaces.js.map +1 -0
  229. package/dist/esm/modules/webaudio/AudioMixer.js +74 -0
  230. package/dist/esm/modules/webaudio/AudioMixer.js.map +1 -0
  231. package/dist/esm/modules/webaudio/WebAudioUtils.js +13 -0
  232. package/dist/esm/modules/webaudio/WebAudioUtils.js.map +1 -0
  233. package/dist/esm/modules/xmpp/AVModeration.js +156 -0
  234. package/dist/esm/modules/xmpp/AVModeration.js.map +1 -0
  235. package/dist/esm/modules/xmpp/BreakoutRooms.js +230 -0
  236. package/dist/esm/modules/xmpp/BreakoutRooms.js.map +1 -0
  237. package/dist/esm/modules/xmpp/Caps.js +223 -0
  238. package/dist/esm/modules/xmpp/Caps.js.map +1 -0
  239. package/dist/esm/modules/xmpp/ChatRoom.js +1877 -0
  240. package/dist/esm/modules/xmpp/ChatRoom.js.map +1 -0
  241. package/dist/esm/modules/xmpp/ConnectionPlugin.js +37 -0
  242. package/dist/esm/modules/xmpp/ConnectionPlugin.js.map +1 -0
  243. package/dist/esm/modules/xmpp/FileSharing.js +95 -0
  244. package/dist/esm/modules/xmpp/FileSharing.js.map +1 -0
  245. package/dist/esm/modules/xmpp/JingleHelperFunctions.js +168 -0
  246. package/dist/esm/modules/xmpp/JingleHelperFunctions.js.map +1 -0
  247. package/dist/esm/modules/xmpp/JingleSession.js +166 -0
  248. package/dist/esm/modules/xmpp/JingleSession.js.map +1 -0
  249. package/dist/esm/modules/xmpp/JingleSessionPC.js +1969 -0
  250. package/dist/esm/modules/xmpp/JingleSessionPC.js.map +1 -0
  251. package/dist/esm/modules/xmpp/JingleSessionState.js +23 -0
  252. package/dist/esm/modules/xmpp/JingleSessionState.js.map +1 -0
  253. package/dist/esm/modules/xmpp/Lobby.js +384 -0
  254. package/dist/esm/modules/xmpp/Lobby.js.map +1 -0
  255. package/dist/esm/modules/xmpp/MediaSessionEvents.js +12 -0
  256. package/dist/esm/modules/xmpp/MediaSessionEvents.js.map +1 -0
  257. package/dist/esm/modules/xmpp/MockClasses.js +77 -0
  258. package/dist/esm/modules/xmpp/MockClasses.js.map +1 -0
  259. package/dist/esm/modules/xmpp/Polls.js +87 -0
  260. package/dist/esm/modules/xmpp/Polls.js.map +1 -0
  261. package/dist/esm/modules/xmpp/ResumeTask.js +149 -0
  262. package/dist/esm/modules/xmpp/ResumeTask.js.map +1 -0
  263. package/dist/esm/modules/xmpp/RoomMetadata.js +96 -0
  264. package/dist/esm/modules/xmpp/RoomMetadata.js.map +1 -0
  265. package/dist/esm/modules/xmpp/SignalingLayerImpl.js +313 -0
  266. package/dist/esm/modules/xmpp/SignalingLayerImpl.js.map +1 -0
  267. package/dist/esm/modules/xmpp/StropheErrorHandler.js +53 -0
  268. package/dist/esm/modules/xmpp/StropheErrorHandler.js.map +1 -0
  269. package/dist/esm/modules/xmpp/StropheLastSuccess.js +52 -0
  270. package/dist/esm/modules/xmpp/StropheLastSuccess.js.map +1 -0
  271. package/dist/esm/modules/xmpp/XmppConnection.js +579 -0
  272. package/dist/esm/modules/xmpp/XmppConnection.js.map +1 -0
  273. package/dist/esm/modules/xmpp/moderator.js +524 -0
  274. package/dist/esm/modules/xmpp/moderator.js.map +1 -0
  275. package/dist/esm/modules/xmpp/sha1.js +165 -0
  276. package/dist/esm/modules/xmpp/sha1.js.map +1 -0
  277. package/dist/esm/modules/xmpp/strophe.disco.js +222 -0
  278. package/dist/esm/modules/xmpp/strophe.disco.js.map +1 -0
  279. package/dist/esm/modules/xmpp/strophe.emuc.js +206 -0
  280. package/dist/esm/modules/xmpp/strophe.emuc.js.map +1 -0
  281. package/dist/esm/modules/xmpp/strophe.jingle.js +404 -0
  282. package/dist/esm/modules/xmpp/strophe.jingle.js.map +1 -0
  283. package/dist/esm/modules/xmpp/strophe.logger.js +44 -0
  284. package/dist/esm/modules/xmpp/strophe.logger.js.map +1 -0
  285. package/dist/esm/modules/xmpp/strophe.ping.js +170 -0
  286. package/dist/esm/modules/xmpp/strophe.ping.js.map +1 -0
  287. package/dist/esm/modules/xmpp/strophe.rayo.js +117 -0
  288. package/dist/esm/modules/xmpp/strophe.rayo.js.map +1 -0
  289. package/dist/esm/modules/xmpp/strophe.stream-management.js +365 -0
  290. package/dist/esm/modules/xmpp/strophe.stream-management.js.map +1 -0
  291. package/dist/esm/modules/xmpp/strophe.util.js +116 -0
  292. package/dist/esm/modules/xmpp/strophe.util.js.map +1 -0
  293. package/dist/esm/modules/xmpp/xmpp.js +973 -0
  294. package/dist/esm/modules/xmpp/xmpp.js.map +1 -0
  295. package/dist/esm/service/RTC/BridgeVideoType.js +24 -0
  296. package/dist/esm/service/RTC/BridgeVideoType.js.map +1 -0
  297. package/dist/esm/service/RTC/CameraFacingMode.js +21 -0
  298. package/dist/esm/service/RTC/CameraFacingMode.js.map +1 -0
  299. package/dist/esm/service/RTC/CodecMimeType.js +36 -0
  300. package/dist/esm/service/RTC/CodecMimeType.js.map +1 -0
  301. package/dist/esm/service/RTC/MediaDirection.js +23 -0
  302. package/dist/esm/service/RTC/MediaDirection.js.map +1 -0
  303. package/dist/esm/service/RTC/MediaType.js +20 -0
  304. package/dist/esm/service/RTC/MediaType.js.map +1 -0
  305. package/dist/esm/service/RTC/RTCEvents.js +111 -0
  306. package/dist/esm/service/RTC/RTCEvents.js.map +1 -0
  307. package/dist/esm/service/RTC/ReceiverAudioSubscription.js +27 -0
  308. package/dist/esm/service/RTC/ReceiverAudioSubscription.js.map +1 -0
  309. package/dist/esm/service/RTC/Resolutions.js +56 -0
  310. package/dist/esm/service/RTC/Resolutions.js.map +1 -0
  311. package/dist/esm/service/RTC/SignalingEvents.js +42 -0
  312. package/dist/esm/service/RTC/SignalingEvents.js.map +1 -0
  313. package/dist/esm/service/RTC/SignalingLayer.js +153 -0
  314. package/dist/esm/service/RTC/SignalingLayer.js.map +1 -0
  315. package/dist/esm/service/RTC/StandardVideoQualitySettings.js +180 -0
  316. package/dist/esm/service/RTC/StandardVideoQualitySettings.js.map +1 -0
  317. package/dist/esm/service/RTC/VideoEncoderScalabilityMode.js +36 -0
  318. package/dist/esm/service/RTC/VideoEncoderScalabilityMode.js.map +1 -0
  319. package/dist/esm/service/RTC/VideoType.js +19 -0
  320. package/dist/esm/service/RTC/VideoType.js.map +1 -0
  321. package/dist/esm/service/authentication/AuthenticationEvents.js +13 -0
  322. package/dist/esm/service/authentication/AuthenticationEvents.js.map +1 -0
  323. package/dist/esm/service/connectivity/ConnectionQualityEvents.js +13 -0
  324. package/dist/esm/service/connectivity/ConnectionQualityEvents.js.map +1 -0
  325. package/dist/esm/service/connectivity/Constants.js +3 -0
  326. package/dist/esm/service/connectivity/Constants.js.map +1 -0
  327. package/dist/esm/service/e2eping/E2ePingEvents.js +8 -0
  328. package/dist/esm/service/e2eping/E2ePingEvents.js.map +1 -0
  329. package/dist/esm/service/statistics/AnalyticsEvents.js +521 -0
  330. package/dist/esm/service/statistics/AnalyticsEvents.js.map +1 -0
  331. package/dist/esm/service/statistics/Events.js +36 -0
  332. package/dist/esm/service/statistics/Events.js.map +1 -0
  333. package/dist/esm/service/statistics/constants.js +2 -0
  334. package/dist/esm/service/statistics/constants.js.map +1 -0
  335. package/dist/esm/service/xmpp/XMPPEvents.js +359 -0
  336. package/dist/esm/service/xmpp/XMPPEvents.js.map +1 -0
  337. package/dist/esm/service/xmpp/XMPPExtensioProtocols.js +64 -0
  338. package/dist/esm/service/xmpp/XMPPExtensioProtocols.js.map +1 -0
  339. package/dist/esm/test-setup-polyfill.js +34 -0
  340. package/dist/esm/test-setup-polyfill.js.map +1 -0
  341. package/dist/esm/tools/gen-version.js +15 -0
  342. package/dist/esm/tools/gen-version.js.map +1 -0
  343. package/dist/esm/version.js +3 -0
  344. package/dist/esm/version.js.map +1 -0
  345. package/dist/umd/lib-jitsi-meet.e2ee-worker.js +484 -0
  346. package/dist/umd/lib-jitsi-meet.min.js +3 -0
  347. package/dist/umd/lib-jitsi-meet.min.js.LICENSE.txt +18 -0
  348. package/dist/umd/lib-jitsi-meet.min.map +1 -0
  349. package/package.json +93 -0
  350. package/types/index.d.ts +16180 -0
@@ -0,0 +1,3692 @@
1
+ import { getLogger } from '@jitsi/logger';
2
+ import { isEqual } from 'lodash-es';
3
+ import { Strophe } from 'strophe.js';
4
+ import * as JitsiConferenceErrors from './JitsiConferenceErrors';
5
+ import JitsiConferenceEventManager from './JitsiConferenceEventManager';
6
+ import { JitsiConferenceEvents } from './JitsiConferenceEvents';
7
+ import { JitsiConnectionEvents } from './JitsiConnectionEvents';
8
+ import JitsiParticipant from './JitsiParticipant';
9
+ import JitsiTrackError from './JitsiTrackError';
10
+ import * as JitsiTrackErrors from './JitsiTrackErrors';
11
+ import { JitsiTrackEvents } from './JitsiTrackEvents';
12
+ import RTC from './modules/RTC/RTC';
13
+ import { SS_DEFAULT_FRAME_RATE } from './modules/RTC/ScreenObtainer';
14
+ import browser from './modules/browser';
15
+ import ConnectionQuality from './modules/connectivity/ConnectionQuality';
16
+ import IceFailedHandling from './modules/connectivity/IceFailedHandling';
17
+ import { DetectionEvents } from './modules/detection/DetectionEvents';
18
+ import NoAudioSignalDetection from './modules/detection/NoAudioSignalDetection';
19
+ import P2PDominantSpeakerDetection from './modules/detection/P2PDominantSpeakerDetection';
20
+ import VADAudioAnalyser from './modules/detection/VADAudioAnalyser';
21
+ import VADNoiseDetection from './modules/detection/VADNoiseDetection';
22
+ import VADTalkMutedDetection from './modules/detection/VADTalkMutedDetection';
23
+ import { E2EEncryption } from './modules/e2ee/E2EEncryption';
24
+ import E2ePing from './modules/e2eping/e2eping';
25
+ import FeatureFlags from './modules/flags/FeatureFlags';
26
+ import { LiteModeContext } from './modules/litemode/LiteModeContext';
27
+ import { QualityController } from './modules/qualitycontrol/QualityController';
28
+ import RecordingManager from './modules/recording/RecordingManager';
29
+ import Settings from './modules/settings/Settings';
30
+ import AvgRTPStatsReporter from './modules/statistics/AvgRTPStatsReporter';
31
+ import LocalStatsCollector from './modules/statistics/LocalStatsCollector';
32
+ import SpeakerStatsCollector from './modules/statistics/SpeakerStatsCollector';
33
+ import Statistics from './modules/statistics/statistics';
34
+ import Listenable from './modules/util/Listenable';
35
+ import { isValidNumber, safeSubtract } from './modules/util/MathUtil';
36
+ import RandomUtil from './modules/util/RandomUtil';
37
+ import { getJitterDelay } from './modules/util/Retry';
38
+ import { findAll, findFirst, getAttribute } from './modules/util/XMLUtils';
39
+ import ComponentsVersions from './modules/version/ComponentsVersions';
40
+ import VideoSIPGW from './modules/videosipgw/VideoSIPGW';
41
+ import * as VideoSIPGWConstants from './modules/videosipgw/VideoSIPGWConstants';
42
+ import { MediaSessionEvents } from './modules/xmpp/MediaSessionEvents';
43
+ import SignalingLayerImpl from './modules/xmpp/SignalingLayerImpl';
44
+ import XMPP, { FEATURE_E2EE, FEATURE_JIGASI, JITSI_MEET_MUC_TYPE } from './modules/xmpp/xmpp';
45
+ import { BridgeVideoType } from './service/RTC/BridgeVideoType';
46
+ import { CodecMimeType } from './service/RTC/CodecMimeType';
47
+ import { MediaType } from './service/RTC/MediaType';
48
+ import { RTCEvents } from './service/RTC/RTCEvents';
49
+ import { SignalingEvents } from './service/RTC/SignalingEvents';
50
+ import { getMediaTypeFromSourceName, getSourceNameForJitsiTrack } from './service/RTC/SignalingLayer';
51
+ import { VideoType } from './service/RTC/VideoType';
52
+ import { MAX_CONNECTION_RETRIES } from './service/connectivity/Constants';
53
+ import { AnalyticsEvents, createConferenceEvent, createJingleEvent, createJvbIceFailedEvent, createP2PEvent } from './service/statistics/AnalyticsEvents';
54
+ import { XMPPEvents } from './service/xmpp/XMPPEvents';
55
+ const logger = getLogger('core:JitsiConference');
56
+ /**
57
+ * How long since Jicofo is supposed to send a session-initiate, before
58
+ * {@link ACTION_JINGLE_SI_TIMEOUT} analytics event is sent (in ms).
59
+ * @type {number}
60
+ */
61
+ const JINGLE_SI_TIMEOUT = 5000;
62
+ /**
63
+ * Default source language for transcribing the local participant.
64
+ */
65
+ const DEFAULT_TRANSCRIPTION_LANGUAGE = 'en-US';
66
+ /**
67
+ * Checks if a given string is a valid video codec mime type.
68
+ *
69
+ * @param {string} codec the codec string that needs to be validated.
70
+ * @returns {CodecMimeType|null} mime type if valid, null otherwise.
71
+ * @private
72
+ */
73
+ function _getCodecMimeType(codec) {
74
+ if (typeof codec === 'string') {
75
+ return Object.values(CodecMimeType).find(value => value === codec.toLowerCase()) || null;
76
+ }
77
+ return null;
78
+ }
79
+ /**
80
+ * Creates a JitsiConference object with the given name and properties.
81
+ * Note: this constructor is not a part of the public API (objects should be
82
+ * created using JitsiConnection.createConference).
83
+ * @param options.config properties / settings related to the conference that
84
+ * will be created.
85
+ * @param options.name the name of the conference
86
+ * @param options.connection the JitsiConnection object for this
87
+ * JitsiConference.
88
+ * @param {number} [options.config.avgRtpStatsN=15] how many samples are to be
89
+ * collected by `AvgRTPStatsReporter`, before arithmetic mean is
90
+ * calculated and submitted to the analytics module.
91
+ * @param {boolean} [options.config.p2p.enabled] when set to <tt>true</tt>
92
+ * the peer to peer mode will be enabled. It means that when there are only 2
93
+ * participants in the conference an attempt to make direct connection will be
94
+ * made. If the connection succeeds the conference will stop sending data
95
+ * through the JVB connection and will use the direct one instead.
96
+ * @param {number} [options.config.p2p.backToP2PDelay=5] a delay given in
97
+ * seconds, before the conference switches back to P2P, after the 3rd
98
+ * participant has left the room.
99
+ * @param {number} [options.config.channelLastN=-1] The requested amount of
100
+ * videos are going to be delivered after the value is in effect. Set to -1 for
101
+ * unlimited or all available videos.
102
+ *
103
+ * @noInheritDoc
104
+ */
105
+ export default class JitsiConference extends Listenable {
106
+ /**
107
+ * @param {IConferenceOptions} options
108
+ */
109
+ constructor(options) {
110
+ super();
111
+ if (!options.name || options.name.toLowerCase() !== options.name.toString()) {
112
+ const errmsg = 'Invalid conference name (no conference name passed or it '
113
+ + 'contains invalid characters like capital letters)!';
114
+ const additionalLogMsg = options.name
115
+ ? `roomName=${options.name}; condition - ${options.name.toLowerCase()}!==${options.name.toString()}`
116
+ : 'No room name passed!';
117
+ logger.error(`${errmsg} ${additionalLogMsg}`);
118
+ throw new Error(errmsg);
119
+ }
120
+ this.connection = options.connection;
121
+ this._xmpp = this.connection?.xmpp;
122
+ if (this._xmpp.isRoomCreated(options.name, options.customDomain)) {
123
+ const errmsg = 'A conference with the same name has already been created!';
124
+ delete this.connection;
125
+ delete this._xmpp;
126
+ logger.error(errmsg);
127
+ throw new Error(errmsg);
128
+ }
129
+ this.options = options;
130
+ this.eventManager = new JitsiConferenceEventManager(this);
131
+ /**
132
+ * List of all the participants in the conference.
133
+ * @type {Map<string, JitsiParticipant>}
134
+ */
135
+ this.participants = new Map();
136
+ /**
137
+ * The signaling layer instance.
138
+ * @type {SignalingLayerImpl}
139
+ * @private
140
+ */
141
+ this._signalingLayer = new SignalingLayerImpl();
142
+ this._init(options);
143
+ this.componentsVersions = new ComponentsVersions(this);
144
+ /**
145
+ * Jingle session instance for the JVB connection.
146
+ * @type {JingleSessionPC}
147
+ */
148
+ this.jvbJingleSession = null;
149
+ this.lastDominantSpeaker = null;
150
+ this.dtmfManager = null;
151
+ this.somebodySupportsDTMF = false;
152
+ this.authEnabled = false;
153
+ this.startMutedPolicy = {
154
+ audio: false,
155
+ video: false
156
+ };
157
+ // AV Moderation.
158
+ this.isMutedByFocus = false;
159
+ this.isVideoMutedByFocus = false;
160
+ this.isDesktopMutedByFocus = false;
161
+ this.mutedByFocusActor = null;
162
+ this.mutedVideoByFocusActor = null;
163
+ this.mutedDesktopByFocusActor = null;
164
+ // Flag indicates if the 'onCallEnded' method was ever called on this
165
+ // instance. Used to log extra analytics event for debugging purpose.
166
+ // We need to know if the potential issue happened before or after
167
+ // the restart.
168
+ this.wasStopped = false;
169
+ // Conference properties, maintained by jicofo.
170
+ this.properties = {};
171
+ /**
172
+ * The object which monitors local and remote connection statistics (e.g.
173
+ * sending bitrate) and calculates a number which represents the connection
174
+ * quality.
175
+ */
176
+ this.connectionQuality = new ConnectionQuality(this, this.eventEmitter, options);
177
+ /**
178
+ * Reports average RTP statistics to the analytics module.
179
+ * @type {AvgRTPStatsReporter}
180
+ */
181
+ this.avgRtpStatsReporter = new AvgRTPStatsReporter(this, options.config.avgRtpStatsN || 15);
182
+ /**
183
+ * Indicates whether the connection is interrupted or not.
184
+ */
185
+ this.isJvbConnectionInterrupted = false;
186
+ /**
187
+ * The object which tracks active speaker times
188
+ */
189
+ this.speakerStatsCollector = new SpeakerStatsCollector(this);
190
+ /* P2P related fields below: */
191
+ /**
192
+ * Stores reference to deferred start P2P task. It's created when 3rd
193
+ * participant leaves the room in order to avoid ping pong effect (it
194
+ * could be just a page reload).
195
+ * @type {number|null}
196
+ */
197
+ this.deferredStartP2PTask = null;
198
+ const delay = Number.parseInt(String(options.config.p2p?.backToP2PDelay || 5), 10);
199
+ /**
200
+ * A delay given in seconds, before the conference switches back to P2P
201
+ * after the 3rd participant has left.
202
+ * @type {number}
203
+ */
204
+ this.backToP2PDelay = isValidNumber(delay) ? delay : 5;
205
+ logger.info(`backToP2PDelay: ${this.backToP2PDelay}`);
206
+ /**
207
+ * If set to <tt>true</tt> it means the P2P ICE is no longer connected.
208
+ * When <tt>false</tt> it means that P2P ICE (media) connection is up
209
+ * and running.
210
+ * @type {boolean}
211
+ */
212
+ this.isP2PConnectionInterrupted = false;
213
+ /**
214
+ * Flag set to <tt>true</tt> when P2P session has been established
215
+ * (ICE has been connected) and this conference is currently in the peer to
216
+ * peer mode (P2P connection is the active one).
217
+ * @type {boolean}
218
+ */
219
+ this.p2p = false;
220
+ /**
221
+ * A JingleSession for the direct peer to peer connection.
222
+ * @type {JingleSessionPC}
223
+ */
224
+ this.p2pJingleSession = null;
225
+ this.videoSIPGWHandler = new VideoSIPGW(this.room);
226
+ this.recordingManager = new RecordingManager(this.room);
227
+ /**
228
+ * If the conference.joined event has been sent this will store the timestamp when it happened.
229
+ *
230
+ * @type {undefined|number}
231
+ * @private
232
+ */
233
+ this._conferenceJoinAnalyticsEventSent = undefined;
234
+ /**
235
+ * End-to-End Encryption. Make it available if supported.
236
+ */
237
+ if (this.isE2EESupported()) {
238
+ logger.info('End-to-End Encryption is supported');
239
+ this._e2eEncryption = new E2EEncryption(this);
240
+ }
241
+ if (FeatureFlags.isRunInLiteModeEnabled()) {
242
+ logger.info('Lite mode enabled');
243
+ this._liteModeContext = new LiteModeContext(this);
244
+ }
245
+ /**
246
+ * Flag set to <tt>true</tt> when Jicofo sends a presence message indicating that the max audio sender limit has
247
+ * been reached for the call. Once this is set, unmuting audio will be disabled
248
+ * from the client until it gets reset
249
+ * again by Jicofo.
250
+ */
251
+ this._audioSenderLimitReached = undefined;
252
+ /**
253
+ * Flag set to <tt>true</tt> when Jicofo sends a presence message indicating that the max video sender limit has
254
+ * been reached for the call. Once this is set, unmuting video will be disabled
255
+ * from the client until it gets reset
256
+ * again by Jicofo.
257
+ */
258
+ this._videoSenderLimitReached = undefined;
259
+ this._firefoxP2pEnabled = browser.isVersionGreaterThan(109)
260
+ && (this.options.config.testing?.enableFirefoxP2p ?? true);
261
+ /**
262
+ * Number of times ICE restarts that have been attempted after ICE connectivity with the JVB was lost.
263
+ */
264
+ this._iceRestarts = 0;
265
+ this._unsubscribers = [];
266
+ }
267
+ /**
268
+ * Create a resource for the a jid. We use the room nickname (the resource part
269
+ * of the occupant JID, see XEP-0045) as the endpoint ID in colibri. We require
270
+ * endpoint IDs to be 8 hex digits because in some cases they get serialized
271
+ * into a 32bit field.
272
+ *
273
+ * @param {string} jid - The id set onto the XMPP connection.
274
+ * @param {boolean} isAuthenticatedUser - Whether or not the user has connected
275
+ * to the XMPP service with a password.
276
+ * @returns {string}
277
+ */
278
+ static resourceCreator(jid, isAuthenticatedUser) {
279
+ let mucNickname;
280
+ if (isAuthenticatedUser) {
281
+ // For authenticated users generate a random ID.
282
+ mucNickname = RandomUtil.randomHexString(8).toLowerCase();
283
+ }
284
+ else {
285
+ // Use first part of node for anonymous users if it matches format
286
+ mucNickname = Strophe.getNodeFromJid(jid)?.substr(0, 8)
287
+ .toLowerCase();
288
+ // But if this doesn't have the required format we just generate a new
289
+ // random nickname.
290
+ const re = /[0-9a-f]{8}/g;
291
+ if (!mucNickname || !re.test(mucNickname)) {
292
+ mucNickname = RandomUtil.randomHexString(8).toLowerCase();
293
+ }
294
+ }
295
+ return mucNickname;
296
+ }
297
+ /**
298
+ * Initializes the conference object properties
299
+ * @param options {object}
300
+ * @param options.connection {JitsiConnection} overrides this.connection
301
+ */
302
+ _init(options) {
303
+ this.eventManager.setupXMPPListeners();
304
+ const { config } = this.options;
305
+ this._statsCurrentId = config.statisticsId ?? Settings.callStatsUserName;
306
+ this.room = this._xmpp.createRoom(this.options.name, {
307
+ ...config,
308
+ statsId: this._statsCurrentId
309
+ }, JitsiConference.resourceCreator);
310
+ this._signalingLayer.setChatRoom(this.room);
311
+ this._signalingLayer.on(SignalingEvents.SOURCE_UPDATED, (sourceName, endpointId, muted, videoType) => {
312
+ const participant = this.participants.get(endpointId);
313
+ const mediaType = getMediaTypeFromSourceName(sourceName);
314
+ if (participant) {
315
+ participant._setSources(mediaType, muted, sourceName, videoType);
316
+ this.eventEmitter.emit(JitsiConferenceEvents.PARTICIPANT_SOURCE_UPDATED, participant);
317
+ }
318
+ });
319
+ // ICE Connection interrupted/restored listeners.
320
+ this._onIceConnectionEstablished = this._onIceConnectionEstablished.bind(this);
321
+ this.room.addListener(XMPPEvents.CONNECTION_ESTABLISHED, this._onIceConnectionEstablished);
322
+ this._onIceConnectionFailed = this._onIceConnectionFailed.bind(this);
323
+ this.room.addListener(XMPPEvents.CONNECTION_ICE_FAILED, this._onIceConnectionFailed);
324
+ this._onIceConnectionInterrupted = this._onIceConnectionInterrupted.bind(this);
325
+ this.room.addListener(XMPPEvents.CONNECTION_INTERRUPTED, this._onIceConnectionInterrupted);
326
+ this._onIceConnectionRestored = this._onIceConnectionRestored.bind(this);
327
+ this.room.addListener(XMPPEvents.CONNECTION_RESTORED, this._onIceConnectionRestored);
328
+ this._updateProperties = this._updateProperties.bind(this);
329
+ this.room.addListener(XMPPEvents.CONFERENCE_PROPERTIES_CHANGED, this._updateProperties);
330
+ this._sendConferenceJoinAnalyticsEvent = this._sendConferenceJoinAnalyticsEvent.bind(this);
331
+ this.room.addListener(XMPPEvents.MEETING_ID_SET, this._sendConferenceJoinAnalyticsEvent);
332
+ this._removeLocalSourceOnReject = this._removeLocalSourceOnReject.bind(this);
333
+ this._updateRoomPresence = this._updateRoomPresence.bind(this);
334
+ this.room.addListener(XMPPEvents.SESSION_ACCEPT, this._updateRoomPresence);
335
+ this.room.addListener(XMPPEvents.SOURCE_ADD, this._updateRoomPresence);
336
+ this.room.addListener(XMPPEvents.SOURCE_ADD_ERROR, this._removeLocalSourceOnReject);
337
+ this.room.addListener(XMPPEvents.SOURCE_REMOVE, this._updateRoomPresence);
338
+ if (config.e2eping?.enabled) {
339
+ this.e2eping = new E2ePing(this, config, (message, to) => {
340
+ try {
341
+ this.sendMessage(message, to, true /* sendThroughVideobridge */);
342
+ }
343
+ catch (error) {
344
+ logger.warn('Failed to send E2E ping request or response.', error?.msg);
345
+ }
346
+ });
347
+ }
348
+ if (!this.rtc) {
349
+ this.rtc = new RTC(this, options);
350
+ this.eventManager.setupRTCListeners();
351
+ this._registerRtcListeners(this.rtc);
352
+ }
353
+ // Get the codec preference settings from config.js.
354
+ const qualityOptions = {
355
+ enableAdaptiveMode: config.videoQuality?.enableAdaptiveMode,
356
+ jvb: {
357
+ disabledCodec: _getCodecMimeType(config.videoQuality?.disabledCodec),
358
+ enableAV1ForFF: config.testing?.enableAV1ForFF,
359
+ preferenceOrder: browser.isMobileDevice()
360
+ ? config.videoQuality?.mobileCodecPreferenceOrder
361
+ : config.videoQuality?.codecPreferenceOrder,
362
+ preferredCodec: _getCodecMimeType(config.videoQuality?.preferredCodec),
363
+ screenshareCodec: browser.isMobileDevice()
364
+ ? _getCodecMimeType(config.videoQuality?.mobileScreenshareCodec)
365
+ : _getCodecMimeType(config.videoQuality?.screenshareCodec)
366
+ },
367
+ lastNRampupTime: config.testing?.lastNRampupTime ?? 60000,
368
+ p2p: {
369
+ disabledCodec: _getCodecMimeType(config.p2p?.disabledCodec),
370
+ enableAV1ForFF: true, // For P2P no simulcast is needed, therefore AV1 can be used.
371
+ preferenceOrder: browser.isMobileDevice()
372
+ ? config.p2p?.mobileCodecPreferenceOrder
373
+ : config.p2p?.codecPreferenceOrder,
374
+ preferredCodec: _getCodecMimeType(config.p2p?.preferredCodec),
375
+ screenshareCodec: browser.isMobileDevice()
376
+ ? _getCodecMimeType(config.p2p?.mobileScreenshareCodec)
377
+ : _getCodecMimeType(config.p2p?.screenshareCodec)
378
+ }
379
+ };
380
+ this.qualityController = new QualityController(this, qualityOptions);
381
+ if (!this.statistics) {
382
+ this.statistics = new Statistics(this, {
383
+ // @ts-ignore
384
+ aliasName: this._statsCurrentId,
385
+ applicationName: config.applicationName,
386
+ confID: config.confID ?? `${this.connection.options.hosts.domain}/${this.options.name}`,
387
+ roomName: this.options.name,
388
+ userName: config.statisticsDisplayName ?? this.myUserId()
389
+ });
390
+ Statistics.analytics.addPermanentProperties({
391
+ 'callstats_name': this._statsCurrentId
392
+ });
393
+ }
394
+ this.eventManager.setupChatRoomListeners();
395
+ // Always add listeners because on reload we are executing leave and the
396
+ // listeners are removed from statistics module.
397
+ this.eventManager.setupStatisticsListeners();
398
+ // Disable VAD processing on Safari since it causes audio input to
399
+ // fail on some of the mobile devices.
400
+ if (config.enableTalkWhileMuted && browser.supportsVADDetection()) {
401
+ // If VAD processor factory method is provided uses VAD based detection, otherwise fallback to audio level
402
+ // based detection.
403
+ if (config.createVADProcessor) {
404
+ logger.info('Using VAD detection for generating talk while muted events');
405
+ if (!this._audioAnalyser) {
406
+ this._audioAnalyser = new VADAudioAnalyser(this, config.createVADProcessor);
407
+ }
408
+ const vadTalkMutedDetection = new VADTalkMutedDetection();
409
+ vadTalkMutedDetection.on(DetectionEvents.VAD_TALK_WHILE_MUTED, () => this.eventEmitter.emit(JitsiConferenceEvents.TALK_WHILE_MUTED));
410
+ this._audioAnalyser.addVADDetectionService(vadTalkMutedDetection);
411
+ }
412
+ else {
413
+ logger.warn('No VAD Processor was provided. Talk while muted detection service was not initialized!');
414
+ }
415
+ }
416
+ // Disable noisy mic detection on safari since it causes the audio input to
417
+ // fail on Safari on iPadOS.
418
+ if (config.enableNoisyMicDetection && browser.supportsVADDetection()) {
419
+ if (config.createVADProcessor) {
420
+ if (!this._audioAnalyser) {
421
+ this._audioAnalyser = new VADAudioAnalyser(this, config.createVADProcessor);
422
+ }
423
+ const vadNoiseDetection = new VADNoiseDetection();
424
+ vadNoiseDetection.on(DetectionEvents.VAD_NOISY_DEVICE, () => this.eventEmitter.emit(JitsiConferenceEvents.NOISY_MIC));
425
+ this._audioAnalyser.addVADDetectionService(vadNoiseDetection);
426
+ }
427
+ else {
428
+ logger.warn('No VAD Processor was provided. Noisy microphone detection service was not initialized!');
429
+ }
430
+ }
431
+ // Generates events based on no audio input detector.
432
+ if (config.enableNoAudioDetection && !config.disableAudioLevels
433
+ && LocalStatsCollector.isLocalStatsSupported()) {
434
+ this._noAudioSignalDetection = new NoAudioSignalDetection(this);
435
+ this._noAudioSignalDetection.on(DetectionEvents.NO_AUDIO_INPUT, () => this.eventEmitter.emit(JitsiConferenceEvents.NO_AUDIO_INPUT));
436
+ this._noAudioSignalDetection.on(DetectionEvents.AUDIO_INPUT_STATE_CHANGE, hasAudioSignal => this.eventEmitter.emit(JitsiConferenceEvents.AUDIO_INPUT_STATE_CHANGE, hasAudioSignal));
437
+ }
438
+ if ('channelLastN' in config) {
439
+ this.setLastN(config.channelLastN);
440
+ }
441
+ // creates dominant speaker detection that works only in p2p mode
442
+ this.p2pDominantSpeakerDetection = new P2PDominantSpeakerDetection(this);
443
+ // TODO: Drop this after the change to use the region from the http requests
444
+ // to prosody is propagated to majority of deployments
445
+ if (config?.deploymentInfo?.userRegion) {
446
+ this.setLocalParticipantProperty('region', config.deploymentInfo.userRegion);
447
+ }
448
+ // Publish the codec preference to presence.
449
+ this.setLocalParticipantProperty('codecList', this.qualityController.codecController.getCodecPreferenceList('jvb'));
450
+ // Set transcription language presence extension.
451
+ // In case the language config is undefined or has the default value that the transcriber uses
452
+ // (in our case Jigasi uses 'en-US'), don't set the participant property in order to avoid
453
+ // needlessly polluting the presence stanza.
454
+ const transcriptionLanguage = config?.transcriptionLanguage ?? DEFAULT_TRANSCRIPTION_LANGUAGE;
455
+ if (transcriptionLanguage !== DEFAULT_TRANSCRIPTION_LANGUAGE) {
456
+ this.setTranscriptionLanguage(transcriptionLanguage);
457
+ }
458
+ }
459
+ /**
460
+ * Registers event listeners on the RTC instance.
461
+ * @param {RTC} rtc - the RTC module instance used by this conference.
462
+ * @private
463
+ * @returns {void}
464
+ */
465
+ _registerRtcListeners(rtc) {
466
+ rtc.addListener(RTCEvents.DATA_CHANNEL_OPEN, () => {
467
+ for (const localTrack of this.rtc.localTracks) {
468
+ localTrack.isVideoTrack() && this._sendBridgeVideoTypeMessage(localTrack);
469
+ }
470
+ });
471
+ }
472
+ /**
473
+ * Sends a conference.join analytics event.
474
+ *
475
+ * @returns {void}
476
+ */
477
+ _sendConferenceJoinAnalyticsEvent() {
478
+ const meetingId = this.getMeetingUniqueId();
479
+ if (this._conferenceJoinAnalyticsEventSent || !meetingId || this.getActivePeerConnection() === null) {
480
+ return;
481
+ }
482
+ const conferenceConnectionTimes = this.getConnectionTimes();
483
+ const xmppConnectionTimes = this.connection.getConnectionTimes();
484
+ const gumStart = window.connectionTimes['firstObtainPermissions.start'];
485
+ const gumEnd = window.connectionTimes['firstObtainPermissions.end'];
486
+ const globalNSConnectionTimes = window.JitsiMeetJS?.app?.connectionTimes ?? {};
487
+ const connectionTimes = {
488
+ ...conferenceConnectionTimes,
489
+ ...xmppConnectionTimes,
490
+ ...globalNSConnectionTimes,
491
+ connectedToMUCJoinedTime: safeSubtract(conferenceConnectionTimes['muc.joined'], xmppConnectionTimes.connected),
492
+ connectingToMUCJoinedTime: safeSubtract(conferenceConnectionTimes['muc.joined'], xmppConnectionTimes.connecting),
493
+ gumDuration: safeSubtract(gumEnd, gumStart),
494
+ numberOfParticipantsOnJoin: this._numberOfParticipantsOnJoin,
495
+ xmppConnectingTime: safeSubtract(xmppConnectionTimes.connected, xmppConnectionTimes.connecting)
496
+ };
497
+ Statistics.sendAnalytics(createConferenceEvent('joined', {
498
+ ...connectionTimes,
499
+ meetingId,
500
+ participantId: `${meetingId}.${this._statsCurrentId}`
501
+ }));
502
+ this._conferenceJoinAnalyticsEventSent = Date.now();
503
+ }
504
+ /**
505
+ * Sends conference.left analytics event.
506
+ * @private
507
+ */
508
+ _sendConferenceLeftAnalyticsEvent() {
509
+ const meetingId = this.getMeetingUniqueId();
510
+ if (!meetingId || !this._conferenceJoinAnalyticsEventSent) {
511
+ return;
512
+ }
513
+ Statistics.sendAnalytics(createConferenceEvent('left', {
514
+ meetingId,
515
+ participantId: `${meetingId}.${this._statsCurrentId}`,
516
+ stats: {
517
+ duration: Math.floor((Date.now() - this._conferenceJoinAnalyticsEventSent) / 1000)
518
+ }
519
+ }));
520
+ }
521
+ /**
522
+ * Restarts all active media sessions.
523
+ *
524
+ * @returns {void}
525
+ */
526
+ _restartMediaSessions() {
527
+ if (this.p2pJingleSession) {
528
+ this._stopP2PSession({
529
+ reasonDescription: 'restart',
530
+ requestRestart: true
531
+ });
532
+ }
533
+ if (this.jvbJingleSession) {
534
+ this._stopJvbSession({
535
+ reason: 'success',
536
+ reasonDescription: 'restart required',
537
+ requestRestart: true,
538
+ sendSessionTerminate: true
539
+ });
540
+ }
541
+ this._maybeStartOrStopP2P(false);
542
+ }
543
+ /**
544
+ * Fires TRACK_AUDIO_LEVEL_CHANGED change conference event (for local tracks).
545
+ * @param {number} audioLevel - The audio level.
546
+ * @param {TraceablePeerConnection} [tpc] - The peer connection.
547
+ * @private
548
+ */
549
+ _fireAudioLevelChangeEvent(audioLevel, tpc) {
550
+ const activeTpc = this.getActivePeerConnection();
551
+ // There will be no TraceablePeerConnection if audio levels do not come from
552
+ // a peerconnection. LocalStatsCollector.js measures audio levels using Web
553
+ // Audio Analyser API and emits local audio levels events through
554
+ // JitsiTrack.setAudioLevel, but does not provide TPC instance which is
555
+ // optional.
556
+ if (!tpc || activeTpc === tpc) {
557
+ this.eventEmitter.emit(JitsiConferenceEvents.TRACK_AUDIO_LEVEL_CHANGED, this.myUserId(), audioLevel);
558
+ }
559
+ }
560
+ /**
561
+ * Fires TRACK_MUTE_CHANGED change conference event.
562
+ * @param {JitsiLocalTrack} track - The JitsiTrack object related to the event.
563
+ */
564
+ _fireMuteChangeEvent(track) {
565
+ // check if track was muted by focus and now is unmuted by user
566
+ if (this.isMutedByFocus && track.isAudioTrack() && !track.isMuted()) {
567
+ this.isMutedByFocus = false;
568
+ // unmute local user on server
569
+ this.room.muteParticipant(this.room.myroomjid, false, MediaType.AUDIO);
570
+ }
571
+ else if (this.isVideoMutedByFocus && track.isVideoTrack()
572
+ && track.getVideoType() !== VideoType.DESKTOP && !track.isMuted()) {
573
+ this.isVideoMutedByFocus = false;
574
+ // unmute local user on server
575
+ this.room.muteParticipant(this.room.myroomjid, false, MediaType.VIDEO);
576
+ }
577
+ else if (this.isDesktopMutedByFocus && track.isVideoTrack()
578
+ && track.getVideoType() === VideoType.DESKTOP && !track.isMuted()) {
579
+ this.isDesktopMutedByFocus = false;
580
+ // unmute local user on server
581
+ this.room.muteParticipant(this.room.myroomjid, false, MediaType.DESKTOP);
582
+ }
583
+ let actorParticipant;
584
+ if (this.mutedByFocusActor && track.isAudioTrack()) {
585
+ const actorId = Strophe.getResourceFromJid(this.mutedByFocusActor);
586
+ actorParticipant = this.participants.get(actorId);
587
+ }
588
+ else if (this.mutedVideoByFocusActor && track.isVideoTrack()
589
+ && track.getVideoType() !== VideoType.DESKTOP) {
590
+ const actorId = Strophe.getResourceFromJid(this.mutedVideoByFocusActor);
591
+ actorParticipant = this.participants.get(actorId);
592
+ }
593
+ else if (this.mutedDesktopByFocusActor && track.isVideoTrack()
594
+ && track.getVideoType() === VideoType.DESKTOP) {
595
+ const actorId = Strophe.getResourceFromJid(this.mutedDesktopByFocusActor);
596
+ actorParticipant = this.participants.get(actorId);
597
+ }
598
+ // Send the video type message to the bridge if the track is not removed/added to the pc as part of
599
+ // the mute/unmute operation.
600
+ // In React Native we mute the camera by setting track.enabled but that doesn't
601
+ // work for screen-share tracks, so do the remove-as-mute for those.
602
+ const doesVideoMuteByStreamRemove = browser.isReactNative() ? track.videoType === VideoType.DESKTOP : browser.doesVideoMuteByStreamRemove();
603
+ if (track.isVideoTrack() && !doesVideoMuteByStreamRemove) {
604
+ this._sendBridgeVideoTypeMessage(track);
605
+ }
606
+ this.eventEmitter.emit(JitsiConferenceEvents.TRACK_MUTE_CHANGED, track, actorParticipant);
607
+ }
608
+ /**
609
+ * Replaces the tracks at the lower level by going through the Jingle session
610
+ * and WebRTC peer connection. The method will resolve immediately if there is
611
+ * currently no JingleSession started.
612
+ * @param {JitsiLocalTrack|null} oldTrack - The track to be removed during
613
+ * the process or <tt>null</t> if the method should act as "add track".
614
+ * @param {JitsiLocalTrack|null} newTrack - The new track to be added or
615
+ * <tt>null</tt> if the method should act as "remove track".
616
+ * @return {Promise} Resolved when the process is done or rejected with a string
617
+ * which describes the error.
618
+ * @private
619
+ */
620
+ async _doReplaceTrack(oldTrack, newTrack) {
621
+ const replaceTrackPromises = [];
622
+ if (this.jvbJingleSession) {
623
+ replaceTrackPromises.push(this.jvbJingleSession.replaceTrack(oldTrack, newTrack));
624
+ }
625
+ else {
626
+ logger.info('_doReplaceTrack - no JVB JingleSession');
627
+ }
628
+ if (this.p2pJingleSession) {
629
+ replaceTrackPromises.push(this.p2pJingleSession.replaceTrack(oldTrack, newTrack));
630
+ }
631
+ else {
632
+ logger.info('_doReplaceTrack - no P2P JingleSession');
633
+ }
634
+ await Promise.all(replaceTrackPromises);
635
+ }
636
+ /**
637
+ * Handler for when a source-add for a local source is rejected by Jicofo.
638
+ * @param {JingleSessionPC} jingleSession - The media session.
639
+ * @param {Error} error - The error message.
640
+ * @param {MediaType} mediaType - The media type of the track associated with the source that was rejected.
641
+ * @returns {void}
642
+ */
643
+ _removeLocalSourceOnReject(jingleSession, error, mediaType) {
644
+ if (!jingleSession) {
645
+ return;
646
+ }
647
+ const errorReason = error?.reason;
648
+ logger.warn(`Source-add rejected on ${jingleSession}, reason="${errorReason}", message="${error?.message}"`);
649
+ const track = this.getLocalTracks(mediaType)[0];
650
+ this.eventEmitter.emit(JitsiConferenceEvents.TRACK_UNMUTE_REJECTED, track);
651
+ }
652
+ /**
653
+ * Operations related to creating a new track.
654
+ * @param {JitsiLocalTrack} newTrack - The new track being created.
655
+ */
656
+ _setupNewTrack(newTrack) {
657
+ const mediaType = newTrack.getType();
658
+ if (!newTrack.getSourceName()) {
659
+ const sourceName = getSourceNameForJitsiTrack(this.myUserId(), mediaType, this.getLocalTracks(mediaType)?.length);
660
+ newTrack.setSourceName(sourceName);
661
+ }
662
+ this.rtc.addLocalTrack(newTrack);
663
+ newTrack.setConference(this);
664
+ // Add event handlers.
665
+ this._unsubscribers.push(newTrack.addCancellableListener(JitsiTrackEvents.TRACK_MUTE_CHANGED, this._fireMuteChangeEvent.bind(this, newTrack)));
666
+ if (newTrack.isAudioTrack()) {
667
+ this._unsubscribers.push(newTrack.addCancellableListener(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._fireAudioLevelChangeEvent.bind(this)));
668
+ }
669
+ this.eventEmitter.emit(JitsiConferenceEvents.TRACK_ADDED, newTrack);
670
+ }
671
+ /**
672
+ * Sets the video type.
673
+ * @param {JitsiLocalTrack} track - The track.
674
+ * @return {boolean} <tt>true</tt> if video type was changed in presence.
675
+ * @private
676
+ */
677
+ _setNewVideoType(track) {
678
+ let videoTypeChanged = false;
679
+ if (track) {
680
+ videoTypeChanged = this._signalingLayer.setTrackVideoType(track.getSourceName(), track.videoType) || false;
681
+ }
682
+ return videoTypeChanged;
683
+ }
684
+ /**
685
+ * Maybe clears the timeout which emits {@link ACTION_JINGLE_SI_TIMEOUT}
686
+ * analytics event.
687
+ * @private
688
+ */
689
+ _maybeClearSITimeout() {
690
+ if (this._sessionInitiateTimeout
691
+ && (this.jvbJingleSession || this.getParticipantCount() < 2)) {
692
+ window.clearTimeout(this._sessionInitiateTimeout);
693
+ this._sessionInitiateTimeout = null;
694
+ }
695
+ }
696
+ /**
697
+ * Sets a timeout which will emit {@link ACTION_JINGLE_SI_TIMEOUT} analytics
698
+ * event.
699
+ * @private
700
+ */
701
+ _maybeSetSITimeout() {
702
+ // Jicofo is supposed to invite if there are at least 2 participants
703
+ if (!this.jvbJingleSession
704
+ && this.getParticipantCount() >= 2
705
+ && !this._sessionInitiateTimeout) {
706
+ this._sessionInitiateTimeout = window.setTimeout(() => {
707
+ this._sessionInitiateTimeout = null;
708
+ Statistics.sendAnalytics(createJingleEvent(AnalyticsEvents.ACTION_JINGLE_SI_TIMEOUT, {
709
+ p2p: false,
710
+ value: JINGLE_SI_TIMEOUT
711
+ }));
712
+ }, JINGLE_SI_TIMEOUT);
713
+ }
714
+ }
715
+ /**
716
+ * Clears the deferred start P2P task if it has been scheduled.
717
+ * @private
718
+ */
719
+ _maybeClearDeferredStartP2P() {
720
+ if (this.deferredStartP2PTask) {
721
+ logger.info('Cleared deferred start P2P task');
722
+ clearTimeout(this.deferredStartP2PTask);
723
+ this.deferredStartP2PTask = null;
724
+ }
725
+ }
726
+ /**
727
+ * Removes from the conference remote tracks associated with the JVB
728
+ * connection.
729
+ * @private
730
+ */
731
+ _removeRemoteJVBTracks() {
732
+ this._removeRemoteTracks('JVB', this.jvbJingleSession.peerconnection.getRemoteTracks());
733
+ }
734
+ /**
735
+ * Removes from the conference remote tracks associated with the P2P
736
+ * connection.
737
+ * @private
738
+ */
739
+ _removeRemoteP2PTracks() {
740
+ this._removeRemoteTracks('P2P', this.p2pJingleSession.peerconnection.getRemoteTracks());
741
+ }
742
+ /**
743
+ * Generates fake "remote track removed" events for given Jingle session.
744
+ * @param {string} sessionNickname the session's nickname which will appear in
745
+ * log messages.
746
+ * @param {Array<JitsiRemoteTrack>} remoteTracks the tracks that will be removed
747
+ * @private
748
+ */
749
+ _removeRemoteTracks(sessionNickname, remoteTracks) {
750
+ for (const track of remoteTracks) {
751
+ logger.info(`Removing remote ${sessionNickname} track: ${track}`);
752
+ this.onRemoteTrackRemoved(track);
753
+ }
754
+ }
755
+ /**
756
+ * Resumes media transfer over the JVB connection.
757
+ * @private
758
+ */
759
+ _resumeMediaTransferForJvbConnection() {
760
+ logger.info('Resuming media transfer over the JVB connection...');
761
+ this.jvbJingleSession.setMediaTransferActive(true)
762
+ .then(() => {
763
+ logger.info('Resumed media transfer over the JVB connection!');
764
+ })
765
+ .catch(error => {
766
+ logger.error('Failed to resume media transfer over the JVB connection:', error);
767
+ });
768
+ }
769
+ /**
770
+ * Sets new P2P status and updates some events/states hijacked from
771
+ * the <tt>JitsiConference</tt>.
772
+ * @param {boolean} newStatus the new P2P status value, <tt>true</tt> means that
773
+ * P2P is now in use, <tt>false</tt> means that the JVB connection is now in use
774
+ * @private
775
+ */
776
+ _setP2PStatus(newStatus) {
777
+ if (this.p2p === newStatus) {
778
+ logger.debug(`Called _setP2PStatus with the same status: ${newStatus}`);
779
+ return;
780
+ }
781
+ this.p2p = newStatus;
782
+ if (newStatus) {
783
+ logger.info('Peer to peer connection established!');
784
+ // When we end up in a valid P2P session need to reset the properties
785
+ // in case they have persisted, after session with another peer.
786
+ Statistics.analytics.addPermanentProperties({
787
+ p2pFailed: false
788
+ });
789
+ // Sync up video transfer active in case p2pJingleSession not existed
790
+ // when the lastN value was being adjusted.
791
+ const isVideoActive = this.getLastN() !== 0;
792
+ this.p2pJingleSession.setP2pVideoTransferActive(isVideoActive)
793
+ .catch(error => {
794
+ logger.error(`Failed to sync up P2P video transfer status (${isVideoActive}), ${error}`);
795
+ });
796
+ }
797
+ else {
798
+ logger.info('Peer to peer connection closed!');
799
+ }
800
+ // Clear dtmfManager, so that it can be recreated with new connection
801
+ this.dtmfManager = null;
802
+ // Update P2P status
803
+ this.eventEmitter.emit(JitsiConferenceEvents.P2P_STATUS, this, this.p2p);
804
+ this.eventEmitter.emit(JitsiConferenceEvents._MEDIA_SESSION_ACTIVE_CHANGED, this.getActiveMediaSession());
805
+ // Refresh connection interrupted/restored
806
+ this.eventEmitter.emit(this.isConnectionInterrupted()
807
+ ? JitsiConferenceEvents.CONNECTION_INTERRUPTED
808
+ : JitsiConferenceEvents.CONNECTION_RESTORED);
809
+ }
810
+ /**
811
+ * Starts new P2P session.
812
+ * @param {string} remoteJid the JID of the remote participant
813
+ * @private
814
+ */
815
+ _startP2PSession(remoteJid) {
816
+ this._maybeClearDeferredStartP2P();
817
+ if (this.p2pJingleSession) {
818
+ logger.error('P2P session already started!');
819
+ return;
820
+ }
821
+ this.isP2PConnectionInterrupted = false;
822
+ this.p2pJingleSession
823
+ = this.xmpp.connection.jingle.newP2PJingleSession(this.room.myroomjid, remoteJid);
824
+ logger.info('Created new P2P JingleSession', this.room.myroomjid, remoteJid);
825
+ this._sendConferenceJoinAnalyticsEvent();
826
+ this.p2pJingleSession.initialize(this.room, this.rtc, this._signalingLayer, {
827
+ ...this.options.config,
828
+ codecSettings: {
829
+ codecList: this.qualityController.codecController.getCodecPreferenceList('p2p'),
830
+ mediaType: MediaType.VIDEO,
831
+ screenshareCodec: this.qualityController.codecController.getScreenshareCodec('p2p')
832
+ },
833
+ enableInsertableStreams: this.isE2EEEnabled() || FeatureFlags.isRunInLiteModeEnabled()
834
+ });
835
+ const localTracks = this.getLocalTracks();
836
+ this.p2pJingleSession.invite(localTracks)
837
+ .then(() => {
838
+ this.p2pJingleSession.addEventListener(MediaSessionEvents.VIDEO_CODEC_CHANGED, () => {
839
+ this.eventEmitter.emit(JitsiConferenceEvents.VIDEO_CODEC_CHANGED);
840
+ });
841
+ })
842
+ .catch(error => {
843
+ logger.error('Failed to start P2P Jingle session', error);
844
+ if (this.p2pJingleSession) {
845
+ this.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_FAILED, JitsiConferenceErrors.OFFER_ANSWER_FAILED, error);
846
+ }
847
+ });
848
+ }
849
+ /**
850
+ * Suspends media transfer over the JVB connection.
851
+ * @private
852
+ */
853
+ _suspendMediaTransferForJvbConnection() {
854
+ logger.info('Suspending media transfer over the JVB connection...');
855
+ this.jvbJingleSession.setMediaTransferActive(false)
856
+ .then(() => {
857
+ logger.info('Suspended media transfer over the JVB connection !');
858
+ })
859
+ .catch(error => {
860
+ logger.error('Failed to suspend media transfer over the JVB connection:', error);
861
+ });
862
+ }
863
+ /**
864
+ * Method when called will decide whether it's the time to start or stop
865
+ * the P2P session.
866
+ * @param {boolean} userLeftEvent if <tt>true</tt> it means that the call
867
+ * originates from the user left event.
868
+ * @private
869
+ */
870
+ _maybeStartOrStopP2P(userLeftEvent = false) {
871
+ if (!this.isP2PEnabled()
872
+ || this.isP2PTestModeEnabled()
873
+ || (browser.isFirefox() && !this._firefoxP2pEnabled)
874
+ || this.isE2EEEnabled()) {
875
+ logger.info('Auto P2P disabled');
876
+ return;
877
+ }
878
+ const peers = this.getParticipants();
879
+ const peerCount = peers.length;
880
+ // FIXME 1 peer and it must *support* P2P switching
881
+ const shouldBeInP2P = this._shouldBeInP2PMode();
882
+ // Clear deferred "start P2P" task
883
+ if (!shouldBeInP2P && this.deferredStartP2PTask) {
884
+ this._maybeClearDeferredStartP2P();
885
+ }
886
+ // Start peer to peer session
887
+ if (!this.p2pJingleSession && shouldBeInP2P) {
888
+ const peer = peerCount && peers[0];
889
+ const myId = this.myUserId();
890
+ const peersId = peer.getId();
891
+ const jid = peer.getJid();
892
+ // Force initiator or responder mode for testing if option is passed to config.
893
+ if (this.options.config.testing?.forceInitiator) {
894
+ logger.debug(`Forcing P2P initiator, will start P2P with: ${jid}`);
895
+ this._startP2PSession(jid);
896
+ }
897
+ else if (this.options.config.testing?.forceResponder) {
898
+ logger.debug(`Forcing P2P responder, will wait for the other peer ${jid} to start P2P`);
899
+ }
900
+ else {
901
+ if (myId > peersId) {
902
+ logger.debug('I\'m the bigger peersId - the other peer should start P2P', myId, peersId);
903
+ return;
904
+ }
905
+ else if (myId === peersId) {
906
+ logger.error('The same IDs ? ', myId, peersId);
907
+ return;
908
+ }
909
+ if (userLeftEvent) {
910
+ if (this.deferredStartP2PTask) {
911
+ logger.error('Deferred start P2P task\'s been set already!');
912
+ return;
913
+ }
914
+ logger.info(`Will start P2P with: ${jid} after ${this.backToP2PDelay} seconds...`);
915
+ this.deferredStartP2PTask = Number(setTimeout(this._startP2PSession.bind(this, jid), this.backToP2PDelay * 1000));
916
+ }
917
+ else {
918
+ logger.info(`Will start P2P with: ${jid}`);
919
+ this._startP2PSession(jid);
920
+ }
921
+ }
922
+ }
923
+ else if (this.p2pJingleSession && !shouldBeInP2P) {
924
+ logger.info(`Will stop P2P with: ${this.p2pJingleSession.remoteJid}`);
925
+ // Log that there will be a switch back to the JVB connection
926
+ if (this.p2pJingleSession.isInitiator && peerCount > 1) {
927
+ Statistics.sendAnalyticsAndLog(createP2PEvent(AnalyticsEvents.ACTION_P2P_SWITCH_TO_JVB));
928
+ }
929
+ this._stopP2PSession();
930
+ }
931
+ }
932
+ /**
933
+ * Tells whether or not this conference should be currently in the P2P mode.
934
+ *
935
+ * @private
936
+ * @returns {boolean}
937
+ */
938
+ _shouldBeInP2PMode() {
939
+ const peers = this.getParticipants();
940
+ const peerCount = peers.length;
941
+ const hasBotPeer = peers.find(p => p.getBotType() === 'poltergeist'
942
+ || p.hasFeature(FEATURE_JIGASI)) !== undefined;
943
+ const shouldBeInP2P = peerCount === 1 && !hasBotPeer && !this._hasVisitors
944
+ && !this._hasVisitors && !this._transcribingEnabled;
945
+ logger.debug(`P2P? peerCount: ${peerCount}, hasBotPeer: ${hasBotPeer} => ${shouldBeInP2P}`);
946
+ return shouldBeInP2P;
947
+ }
948
+ /**
949
+ * Stops the current P2P session.
950
+ * @param {Object} options - Options for stopping P2P.
951
+ * @param {string} options.reason - One of the Jingle "reason" element
952
+ * names as defined by https://xmpp.org/extensions/xep-0166.html#def-reason
953
+ * @param {string} options.reasonDescription - Text description that will be
954
+ * included in the session terminate message.
955
+ * @param {boolean} options.requestRestart - Whether this is due to a session restart, in which case
956
+ * media will not be resumed on the JVB.
957
+ * @private
958
+ */
959
+ _stopP2PSession(options = {}) {
960
+ const { reason = 'success', reasonDescription = 'Turning off P2P session', requestRestart = false } = options;
961
+ if (!this.p2pJingleSession) {
962
+ logger.error('No P2P session to be stopped!');
963
+ return;
964
+ }
965
+ const wasP2PEstablished = this.isP2PActive();
966
+ // Swap remote tracks, but only if the P2P has been fully established
967
+ if (wasP2PEstablished) {
968
+ if (this.jvbJingleSession && !requestRestart) {
969
+ this._resumeMediaTransferForJvbConnection();
970
+ }
971
+ // Remove remote P2P tracks
972
+ this._removeRemoteP2PTracks();
973
+ }
974
+ // Stop P2P stats
975
+ logger.info('Stopping remote stats for P2P connection');
976
+ this.statistics.stopRemoteStats(this.p2pJingleSession.peerconnection);
977
+ this.p2pJingleSession.terminate(() => {
978
+ logger.info('P2P session terminate RESULT');
979
+ this.p2pJingleSession = null;
980
+ }, error => {
981
+ // Because both initiator and responder are simultaneously
982
+ // terminating their JingleSessions in case of the 'to JVB switch'
983
+ // when 3rd participant joins, both will dispose their sessions and
984
+ // reply with 'item-not-found' (see strophe.jingle.js). We don't
985
+ // want to log this as an error since it's expected behaviour.
986
+ //
987
+ // We want them both to terminate, because in case of initiator's
988
+ // crash the responder would stay in P2P mode until ICE fails which
989
+ // could take up to 20 seconds.
990
+ //
991
+ // NOTE: whilst this is an error callback, 'success' as a reason is
992
+ // considered as graceful session terminate
993
+ // where both initiator and responder terminate their sessions
994
+ // simultaneously.
995
+ if (reason !== 'success') {
996
+ logger.error('An error occurred while trying to terminate P2P Jingle session', error);
997
+ }
998
+ }, {
999
+ reason,
1000
+ reasonDescription,
1001
+ sendSessionTerminate: Boolean(this.room
1002
+ && this.getParticipantById(Strophe.getResourceFromJid(this.p2pJingleSession.remoteJid)))
1003
+ });
1004
+ this.p2pJingleSession = null;
1005
+ // Update P2P status and other affected events/states
1006
+ this._setP2PStatus(false);
1007
+ if (wasP2PEstablished) {
1008
+ // Add back remote JVB tracks
1009
+ if (this.jvbJingleSession && !requestRestart) {
1010
+ this._addRemoteJVBTracks();
1011
+ }
1012
+ else {
1013
+ logger.info('Not adding remote JVB tracks - no session yet');
1014
+ }
1015
+ }
1016
+ }
1017
+ /**
1018
+ * Updates room presence if needed and send the packet in case of a modification.
1019
+ * @param {JingleSessionPC} jingleSession the session firing the event, contains the peer connection which
1020
+ * tracks we will check.
1021
+ * @param {Object|null} ctx a context object we can distinguish multiple calls of the same pass of updating tracks.
1022
+ */
1023
+ _updateRoomPresence(jingleSession, ctx = {}) {
1024
+ if (!jingleSession) {
1025
+ return;
1026
+ }
1027
+ // skips sending presence twice for the same pass of updating ssrcs
1028
+ if (ctx) {
1029
+ if (ctx.skip) {
1030
+ return;
1031
+ }
1032
+ ctx.skip = true;
1033
+ }
1034
+ let presenceChanged = false;
1035
+ let muteStatusChanged;
1036
+ let videoTypeChanged;
1037
+ const localTracks = jingleSession.peerconnection.getLocalTracks();
1038
+ // Set presence for all the available local tracks.
1039
+ for (const track of localTracks) {
1040
+ const muted = track.isMuted();
1041
+ muteStatusChanged = this._setTrackMuteStatus(track, muted);
1042
+ muteStatusChanged && logger.debug(`Updating mute state of ${track} in presence to muted=${muted}`);
1043
+ if (track.getType() === MediaType.VIDEO) {
1044
+ videoTypeChanged = this._setNewVideoType(track);
1045
+ videoTypeChanged && logger.debug(`Updating videoType in presence to ${track.getVideoType()}`);
1046
+ }
1047
+ presenceChanged = presenceChanged || muteStatusChanged || videoTypeChanged;
1048
+ }
1049
+ presenceChanged && this.room.sendPresence();
1050
+ }
1051
+ /**
1052
+ * Updates features for a participant.
1053
+ * @param {JitsiParticipant} participant - The participant to query for features.
1054
+ * @returns {void}
1055
+ * @private
1056
+ */
1057
+ _updateFeatures(participant) {
1058
+ participant.getFeatures()
1059
+ .then(features => {
1060
+ participant._supportsDTMF = features.has('urn:xmpp:jingle:dtmf:0');
1061
+ this.updateDTMFSupport();
1062
+ if (features.has(FEATURE_JIGASI)) {
1063
+ participant.setProperty('features_jigasi', true);
1064
+ }
1065
+ if (features.has(FEATURE_E2EE)) {
1066
+ participant.setProperty('features_e2ee', true);
1067
+ }
1068
+ })
1069
+ .catch(() => false);
1070
+ }
1071
+ /**
1072
+ * Accepts an incoming call event for the JVB Jingle session.
1073
+ * @param {JingleSessionPC} jingleSession - The Jingle session for the incoming call.
1074
+ * @param {Element} jingleOffer - An element pointing to 'jingle' IQ element containing the offer.
1075
+ * @param {number} now - The timestamp when the call was received.
1076
+ * @private
1077
+ */
1078
+ _acceptJvbIncomingCall(jingleSession, jingleOffer, now) {
1079
+ // Accept incoming call
1080
+ this.jvbJingleSession = jingleSession;
1081
+ this.room.connectionTimes['session.initiate'] = now;
1082
+ this._sendConferenceJoinAnalyticsEvent();
1083
+ if (this.wasStopped) {
1084
+ Statistics.sendAnalyticsAndLog(createJingleEvent(AnalyticsEvents.ACTION_JINGLE_RESTART, { p2p: false }));
1085
+ }
1086
+ // Use *|xmlns to match xmlns attributes across any namespace (CSS Selectors Level 3)
1087
+ const serverRegion = getAttribute(findFirst(jingleOffer, ':scope>bridge-session[*|xmlns="http://jitsi.org/protocol/focus"]'), 'region');
1088
+ this.eventEmitter.emit(JitsiConferenceEvents.SERVER_REGION_CHANGED, serverRegion);
1089
+ this._maybeClearSITimeout();
1090
+ Statistics.sendAnalytics(createJingleEvent(AnalyticsEvents.ACTION_JINGLE_SI_RECEIVED, {
1091
+ p2p: false,
1092
+ value: now
1093
+ }));
1094
+ try {
1095
+ jingleSession.initialize(this.room, this.rtc, this._signalingLayer, {
1096
+ ...this.options.config,
1097
+ codecSettings: {
1098
+ codecList: this.qualityController.codecController.getCodecPreferenceList('jvb'),
1099
+ mediaType: MediaType.VIDEO,
1100
+ screenshareCodec: this.qualityController.codecController.getScreenshareCodec('jvb')
1101
+ },
1102
+ enableInsertableStreams: this.isE2EEEnabled() || FeatureFlags.isRunInLiteModeEnabled()
1103
+ });
1104
+ }
1105
+ catch (error) {
1106
+ logger.error(error);
1107
+ return;
1108
+ }
1109
+ // Open a channel with the videobridge.
1110
+ this._setBridgeChannel(jingleOffer, jingleSession.peerconnection);
1111
+ const localTracks = this.getLocalTracks();
1112
+ try {
1113
+ jingleSession.acceptOffer(jingleOffer, () => {
1114
+ // If for any reason invite for the JVB session arrived after
1115
+ // the P2P has been established already the media transfer needs
1116
+ // to be turned off here.
1117
+ if (this.isP2PActive() && this.jvbJingleSession) {
1118
+ this._suspendMediaTransferForJvbConnection();
1119
+ }
1120
+ this.eventEmitter.emit(JitsiConferenceEvents._MEDIA_SESSION_STARTED, jingleSession);
1121
+ if (!this.isP2PActive()) {
1122
+ this.eventEmitter.emit(JitsiConferenceEvents._MEDIA_SESSION_ACTIVE_CHANGED, jingleSession);
1123
+ }
1124
+ jingleSession.addEventListener(MediaSessionEvents.VIDEO_CODEC_CHANGED, () => {
1125
+ this.eventEmitter.emit(JitsiConferenceEvents.VIDEO_CODEC_CHANGED);
1126
+ });
1127
+ }, error => {
1128
+ logger.error('Failed to accept incoming JVB Jingle session', error);
1129
+ this.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_FAILED, JitsiConferenceErrors.OFFER_ANSWER_FAILED, error);
1130
+ }, localTracks);
1131
+ // Set the capture fps for screenshare if it is set through the UI.
1132
+ this._desktopSharingFrameRate
1133
+ && jingleSession.peerconnection.setDesktopSharingFrameRate(this._desktopSharingFrameRate);
1134
+ this.statistics.startRemoteStats(this.jvbJingleSession.peerconnection);
1135
+ }
1136
+ catch (e) {
1137
+ logger.error(e);
1138
+ }
1139
+ }
1140
+ /**
1141
+ * Sets the BridgeChannel.
1142
+ *
1143
+ * @param {Object} offerIq - An element pointing to the jingle element of
1144
+ * the offer IQ which may carry the WebSocket URL for the 'websocket'
1145
+ * BridgeChannel mode.
1146
+ * @param {TraceablePeerConnection} pc - The peer connection which will be used
1147
+ * to listen for new WebRTC Data Channels (in the 'datachannel' mode).
1148
+ * @private
1149
+ */
1150
+ _setBridgeChannel(offerIq, pc) {
1151
+ const ignoreDomain = this.connection?.options?.bridgeChannel?.ignoreDomain;
1152
+ const preferSctp = this.connection?.options?.bridgeChannel?.preferSctp ?? true;
1153
+ const sctpOffered = findAll(offerIq, ':scope>content[name="data"]').length === 1;
1154
+ let wsUrl = null;
1155
+ logger.info(`SCTP: offered=${sctpOffered}, prefered=${preferSctp}`);
1156
+ if (!(sctpOffered && preferSctp)) {
1157
+ findAll(offerIq, ':scope>content>transport>web-socket')
1158
+ .map(e => e.getAttribute('url'))
1159
+ .forEach(url => {
1160
+ if (!wsUrl && (!ignoreDomain || ignoreDomain !== new URL(url).hostname)) {
1161
+ wsUrl = url;
1162
+ logger.info(`Using colibri-ws url ${url}`);
1163
+ }
1164
+ else if (!wsUrl) {
1165
+ logger.info(`Ignoring colibri-ws url with domain ${ignoreDomain}`);
1166
+ }
1167
+ });
1168
+ if (!wsUrl) {
1169
+ const firstWsUrl = findFirst(offerIq, ':scope>content>transport>web-socket');
1170
+ if (firstWsUrl) {
1171
+ wsUrl = firstWsUrl.getAttribute('url');
1172
+ logger.info(`Falling back to ${wsUrl}`);
1173
+ }
1174
+ }
1175
+ }
1176
+ if (wsUrl && !(sctpOffered && preferSctp)) {
1177
+ // If the offer contains a websocket and we don't prefer SCTP use it.
1178
+ this.rtc.initializeBridgeChannel(null, wsUrl);
1179
+ }
1180
+ else if (sctpOffered) {
1181
+ // Otherwise, fall back to an attempt to use SCTP.
1182
+ this.rtc.initializeBridgeChannel(pc.peerconnection, null);
1183
+ }
1184
+ else {
1185
+ logger.warn('Neither SCTP nor a websocket is available. Will not initialize bridge channel.');
1186
+ }
1187
+ }
1188
+ /**
1189
+ * Rejects incoming Jingle call.
1190
+ * @param {JingleSessionPC} jingleSession - The session instance to be rejected.
1191
+ * @param {object} [options] - Optional parameters for rejection.
1192
+ * @param {string} options.reason - The name of the reason element as defined by Jingle.
1193
+ * @param {string} options.reasonDescription - The reason description which will be
1194
+ * included in Jingle 'session-terminate' message.
1195
+ * @param {string} options.errorMsg - An error message to be logged on global error handler.
1196
+ * @private
1197
+ */
1198
+ _rejectIncomingCall(jingleSession, options) {
1199
+ if (options?.errorMsg) {
1200
+ logger.warn(options.errorMsg);
1201
+ }
1202
+ // Terminate the jingle session with a reason
1203
+ jingleSession.terminate(null /* success callback => we don't care */, error => {
1204
+ logger.warn('An error occurred while trying to terminate'
1205
+ + ' invalid Jingle session', error);
1206
+ }, {
1207
+ reason: options?.reason,
1208
+ reasonDescription: options?.reasonDescription,
1209
+ sendSessionTerminate: true
1210
+ });
1211
+ }
1212
+ /**
1213
+ * Handles an incoming call event for the P2P Jingle session.
1214
+ * @param {JingleSessionPC} jingleSession - The Jingle session for the incoming call.
1215
+ * @param {Element} jingleOffer - An element pointing to 'jingle' IQ element containing the offer.
1216
+ * @private
1217
+ */
1218
+ _onIncomingCallP2P(jingleSession, jingleOffer) {
1219
+ let rejectReason;
1220
+ const contentName = getAttribute(findFirst(jingleOffer, ':scope>content'), 'name');
1221
+ const peerUsesUnifiedPlan = contentName === '0' || contentName === '1';
1222
+ // Reject P2P between endpoints that are not running in the same mode w.r.t to SDPs (plan-b and unified plan).
1223
+ if (!peerUsesUnifiedPlan) {
1224
+ rejectReason = {
1225
+ errorMsg: 'P2P across two endpoints in different SDP modes is disabled',
1226
+ reason: 'decline',
1227
+ reasonDescription: 'P2P disabled'
1228
+ };
1229
+ }
1230
+ else if ((!this.isP2PEnabled() && !this.isP2PTestModeEnabled())
1231
+ || (browser.isFirefox() && !this._firefoxP2pEnabled)) {
1232
+ rejectReason = {
1233
+ errorMsg: 'P2P mode disabled in the configuration or browser unsupported',
1234
+ reason: 'decline',
1235
+ reasonDescription: 'P2P disabled'
1236
+ };
1237
+ }
1238
+ else if (this.p2pJingleSession) {
1239
+ // Reject incoming P2P call (already in progress)
1240
+ rejectReason = {
1241
+ errorMsg: 'Duplicated P2P "session-initiate"',
1242
+ reason: 'busy',
1243
+ reasonDescription: 'P2P already in progress'
1244
+ };
1245
+ }
1246
+ else if (!this._shouldBeInP2PMode()) {
1247
+ rejectReason = {
1248
+ errorMsg: 'Received P2P "session-initiate" when should not be in P2P mode',
1249
+ reason: 'decline',
1250
+ reasonDescription: 'P2P requirements not met'
1251
+ };
1252
+ Statistics.sendAnalytics(createJingleEvent(AnalyticsEvents.ACTION_P2P_DECLINED));
1253
+ }
1254
+ if (rejectReason) {
1255
+ this._rejectIncomingCall(jingleSession, rejectReason);
1256
+ }
1257
+ else {
1258
+ this._acceptP2PIncomingCall(jingleSession, jingleOffer);
1259
+ }
1260
+ }
1261
+ /**
1262
+ * Handles CONNECTION_INTERRUPTED event.
1263
+ * @param {JingleSessionPC} session - The Jingle session.
1264
+ * @private
1265
+ */
1266
+ _onIceConnectionInterrupted(session) {
1267
+ if (session.isP2P) {
1268
+ this.isP2PConnectionInterrupted = true;
1269
+ }
1270
+ else {
1271
+ this.isJvbConnectionInterrupted = true;
1272
+ }
1273
+ if (session.isP2P === this.isP2PActive()) {
1274
+ this.eventEmitter.emit(JitsiConferenceEvents.CONNECTION_INTERRUPTED);
1275
+ }
1276
+ }
1277
+ /**
1278
+ * Handles CONNECTION_ICE_FAILED event.
1279
+ * @param {JingleSessionPC} session - The Jingle session.
1280
+ * @private
1281
+ */
1282
+ _onIceConnectionFailed(session) {
1283
+ if (session.isP2P) {
1284
+ // Add p2pFailed property to analytics to distinguish, between "good"
1285
+ // and "bad" connection
1286
+ Statistics.analytics.addPermanentProperties({ p2pFailed: true });
1287
+ if (this.p2pJingleSession) {
1288
+ Statistics.sendAnalyticsAndLog(createP2PEvent(AnalyticsEvents.ACTION_P2P_FAILED, {
1289
+ initiator: this.p2pJingleSession.isInitiator
1290
+ }));
1291
+ }
1292
+ this._stopP2PSession({
1293
+ reason: 'connectivity-error',
1294
+ reasonDescription: 'ICE FAILED'
1295
+ });
1296
+ }
1297
+ else if (session && this.jvbJingleSession === session && this._iceRestarts < MAX_CONNECTION_RETRIES) {
1298
+ // Use an exponential backoff timer for ICE restarts.
1299
+ const jitterDelay = getJitterDelay(this._iceRestarts, 1000 /* min. delay */);
1300
+ this._delayedIceFailed = new IceFailedHandling(this);
1301
+ setTimeout(() => {
1302
+ logger.error(`triggering ice restart after ${jitterDelay} `);
1303
+ this._delayedIceFailed.start();
1304
+ this._iceRestarts++;
1305
+ }, jitterDelay);
1306
+ }
1307
+ else if (this.jvbJingleSession === session) {
1308
+ logger.warn('ICE failed, force reloading the conference after failed attempts to re-establish ICE');
1309
+ Statistics.sendAnalyticsAndLog(createJvbIceFailedEvent(AnalyticsEvents.ACTION_JVB_ICE_FAILED, {
1310
+ participantId: this.myUserId(),
1311
+ userRegion: this.options.config.deploymentInfo?.userRegion
1312
+ }));
1313
+ this.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_FAILED, JitsiConferenceErrors.ICE_FAILED);
1314
+ }
1315
+ }
1316
+ /**
1317
+ * Handles CONNECTION_RESTORED event.
1318
+ * @param {JingleSessionPC} session - The Jingle session.
1319
+ * @private
1320
+ */
1321
+ _onIceConnectionRestored(session) {
1322
+ if (session.isP2P) {
1323
+ this.isP2PConnectionInterrupted = false;
1324
+ }
1325
+ else {
1326
+ this.isJvbConnectionInterrupted = false;
1327
+ this._delayedIceFailed && this._delayedIceFailed.cancel();
1328
+ }
1329
+ if (session.isP2P === this.isP2PActive()) {
1330
+ this.eventEmitter.emit(JitsiConferenceEvents.CONNECTION_RESTORED);
1331
+ }
1332
+ }
1333
+ /**
1334
+ * Accepts an incoming P2P Jingle call.
1335
+ * @param {JingleSessionPC} jingleSession - The Jingle session instance.
1336
+ * @param {Element} jingleOffer - An element pointing to 'jingle' IQ element containing the offer.
1337
+ * @private
1338
+ */
1339
+ _acceptP2PIncomingCall(jingleSession, jingleOffer) {
1340
+ this.isP2PConnectionInterrupted = false;
1341
+ // Accept the offer
1342
+ this.p2pJingleSession = jingleSession;
1343
+ this._sendConferenceJoinAnalyticsEvent();
1344
+ this.p2pJingleSession.initialize(this.room, this.rtc, this._signalingLayer, {
1345
+ ...this.options.config,
1346
+ codecSettings: {
1347
+ codecList: this.qualityController.codecController.getCodecPreferenceList('p2p'),
1348
+ mediaType: MediaType.VIDEO,
1349
+ screenshareCodec: this.qualityController.codecController.getScreenshareCodec('p2p')
1350
+ },
1351
+ enableInsertableStreams: this.isE2EEEnabled() || FeatureFlags.isRunInLiteModeEnabled()
1352
+ });
1353
+ const localTracks = this.getLocalTracks();
1354
+ this.p2pJingleSession.acceptOffer(jingleOffer, () => {
1355
+ logger.debug('Got RESULT for P2P "session-accept"');
1356
+ this.eventEmitter.emit(JitsiConferenceEvents._MEDIA_SESSION_STARTED, jingleSession);
1357
+ jingleSession.addEventListener(MediaSessionEvents.VIDEO_CODEC_CHANGED, () => {
1358
+ this.eventEmitter.emit(JitsiConferenceEvents.VIDEO_CODEC_CHANGED);
1359
+ });
1360
+ }, error => {
1361
+ logger.error('Failed to accept incoming P2P Jingle session', error);
1362
+ if (this.p2pJingleSession) {
1363
+ this.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_FAILED, JitsiConferenceErrors.OFFER_ANSWER_FAILED, error);
1364
+ }
1365
+ }, localTracks);
1366
+ }
1367
+ /**
1368
+ * Adds remote tracks to the conference associated with the JVB session.
1369
+ * @private
1370
+ * @returns {void}
1371
+ */
1372
+ _addRemoteJVBTracks() {
1373
+ this._addRemoteTracks('JVB', this.jvbJingleSession.peerconnection.getRemoteTracks());
1374
+ }
1375
+ /**
1376
+ * Adds remote tracks to the conference associated with the P2P session.
1377
+ * @private
1378
+ * @returns {void}
1379
+ */
1380
+ _addRemoteP2PTracks() {
1381
+ this._addRemoteTracks('P2P', this.p2pJingleSession.peerconnection.getRemoteTracks());
1382
+ }
1383
+ /**
1384
+ * Generates fake "remote track added" events for given Jingle session.
1385
+ * @param {string} logName - The session's nickname which will appear in log messages.
1386
+ * @param {Array<JitsiRemoteTrack>} remoteTracks - The tracks that will be added.
1387
+ * @private
1388
+ */
1389
+ _addRemoteTracks(logName, remoteTracks) {
1390
+ for (const track of remoteTracks) {
1391
+ if (this.participants.has(track.ownerEndpointId)) {
1392
+ logger.info(`Adding remote ${logName} track: ${track}`);
1393
+ this.onRemoteTrackAdded(track);
1394
+ }
1395
+ }
1396
+ }
1397
+ /**
1398
+ * Handles the ICE connection establishment event for a Jingle session.
1399
+ * @private
1400
+ * @param {JingleSessionPC} jingleSession - The Jingle session for which ICE connection was established.
1401
+ */
1402
+ _onIceConnectionEstablished(jingleSession) {
1403
+ if (this.p2pJingleSession !== null) {
1404
+ // store the establishment time of the p2p session as a field of the
1405
+ // JitsiConference because the p2pJingleSession might get disposed (thus
1406
+ // the value is lost).
1407
+ this.p2pEstablishmentDuration
1408
+ = this.p2pJingleSession.establishmentDuration;
1409
+ }
1410
+ if (this.jvbJingleSession !== null) {
1411
+ this.jvbEstablishmentDuration
1412
+ = this.jvbJingleSession.establishmentDuration;
1413
+ }
1414
+ let done = false;
1415
+ // We don't care about the JVB case, there's nothing to be done
1416
+ if (!jingleSession.isP2P) {
1417
+ done = true;
1418
+ }
1419
+ else if (this.p2pJingleSession !== jingleSession) {
1420
+ logger.error('CONNECTION_ESTABLISHED - wrong P2P session instance ?!');
1421
+ done = true;
1422
+ }
1423
+ if (isValidNumber(this.p2pEstablishmentDuration)
1424
+ && isValidNumber(this.jvbEstablishmentDuration)) {
1425
+ const establishmentDurationDiff = this.p2pEstablishmentDuration - this.jvbEstablishmentDuration;
1426
+ Statistics.sendAnalytics(AnalyticsEvents.ICE_ESTABLISHMENT_DURATION_DIFF, { value: establishmentDurationDiff });
1427
+ }
1428
+ if (jingleSession.isP2P === this.isP2PActive()) {
1429
+ this.eventEmitter.emit(JitsiConferenceEvents.CONNECTION_ESTABLISHED);
1430
+ }
1431
+ if (done) {
1432
+ return;
1433
+ }
1434
+ // Update P2P status and emit events
1435
+ this._setP2PStatus(true);
1436
+ // Remove remote tracks
1437
+ if (this.jvbJingleSession) {
1438
+ this._removeRemoteJVBTracks();
1439
+ }
1440
+ else {
1441
+ logger.info('Not removing remote JVB tracks - no session yet');
1442
+ }
1443
+ this._addRemoteP2PTracks();
1444
+ // Stop media transfer over the JVB connection
1445
+ if (this.jvbJingleSession) {
1446
+ this._suspendMediaTransferForJvbConnection();
1447
+ }
1448
+ logger.info('Starting remote stats with p2p connection');
1449
+ this.statistics.startRemoteStats(this.p2pJingleSession.peerconnection);
1450
+ Statistics.sendAnalyticsAndLog(createP2PEvent(AnalyticsEvents.ACTION_P2P_ESTABLISHED, {
1451
+ initiator: this.p2pJingleSession.isInitiator
1452
+ }));
1453
+ }
1454
+ /**
1455
+ * Called when the chat room reads a new list of properties from jicofo's
1456
+ * presence. The properties may have changed, but they don't have to.
1457
+ *
1458
+ * @param {Object} properties - The properties keyed by the property name
1459
+ * ('key').
1460
+ * @private
1461
+ */
1462
+ _updateProperties(properties = {}) {
1463
+ const changed = !isEqual(properties, this.properties);
1464
+ this.properties = properties;
1465
+ if (changed) {
1466
+ this.eventEmitter.emit(JitsiConferenceEvents.PROPERTIES_CHANGED, this.properties);
1467
+ const audioLimitReached = this.properties['audio-limit-reached'] === 'true';
1468
+ const videoLimitReached = this.properties['video-limit-reached'] === 'true';
1469
+ if (this._audioSenderLimitReached !== audioLimitReached) {
1470
+ this._audioSenderLimitReached = audioLimitReached;
1471
+ this.eventEmitter.emit(JitsiConferenceEvents.AUDIO_UNMUTE_PERMISSIONS_CHANGED, audioLimitReached);
1472
+ logger.info(`Audio unmute permissions set by Jicofo to ${audioLimitReached}`);
1473
+ }
1474
+ if (this._videoSenderLimitReached !== videoLimitReached) {
1475
+ this._videoSenderLimitReached = videoLimitReached;
1476
+ this.eventEmitter.emit(JitsiConferenceEvents.VIDEO_UNMUTE_PERMISSIONS_CHANGED, videoLimitReached);
1477
+ logger.info(`Video unmute permissions set by Jicofo to ${videoLimitReached}`);
1478
+ }
1479
+ // Some of the properties need to be added to analytics events.
1480
+ const analyticsKeys = [
1481
+ // The number of jitsi-videobridge instances currently used for the
1482
+ // conference.
1483
+ 'bridge-count'
1484
+ ];
1485
+ analyticsKeys.forEach(key => {
1486
+ if (properties[key] !== undefined) {
1487
+ Statistics.analytics.addPermanentProperties({
1488
+ [key.replace('-', '_')]: properties[key]
1489
+ });
1490
+ }
1491
+ });
1492
+ // Handle changes to aggregate list of visitor codecs.
1493
+ let publishedCodecs = this.properties['visitor-codecs']?.split(',');
1494
+ if (publishedCodecs?.length) {
1495
+ publishedCodecs = publishedCodecs.filter(codec => typeof codec === 'string'
1496
+ && codec.trim().length
1497
+ && Object.values(CodecMimeType).find(val => val === codec));
1498
+ }
1499
+ if (this._visitorCodecs !== publishedCodecs) {
1500
+ this._visitorCodecs = publishedCodecs;
1501
+ this.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_VISITOR_CODECS_CHANGED, this._visitorCodecs);
1502
+ }
1503
+ const oldValue = this._hasVisitors;
1504
+ this._hasVisitors = this.properties['visitor-count'] > 0;
1505
+ oldValue !== this._hasVisitors && this._maybeStartOrStopP2P(true);
1506
+ }
1507
+ }
1508
+ /**
1509
+ * Fires CONFERENCE_FAILED event with INCOMPATIBLE_SERVER_VERSIONS parameter.
1510
+ * @returns {void}
1511
+ * @private
1512
+ */
1513
+ _fireIncompatibleVersionsEvent() {
1514
+ this.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_FAILED, JitsiConferenceErrors.INCOMPATIBLE_SERVER_VERSIONS);
1515
+ }
1516
+ /**
1517
+ * Sends the 'VideoTypeMessage' to the bridge on the bridge channel so that the bridge can make bitrate allocation
1518
+ * decisions based on the video type of the local source.
1519
+ *
1520
+ * @param {JitsiLocalTrack} localtrack - The track associated with the local source signaled to the bridge.
1521
+ * @returns {void}
1522
+ * @internal
1523
+ */
1524
+ _sendBridgeVideoTypeMessage(localtrack) {
1525
+ let videoType = !localtrack || localtrack.isMuted() ? BridgeVideoType.NONE : localtrack.getVideoType();
1526
+ if (videoType === VideoType.DESKTOP && this._desktopSharingFrameRate > SS_DEFAULT_FRAME_RATE) {
1527
+ videoType = BridgeVideoType.DESKTOP_HIGH_FPS;
1528
+ }
1529
+ localtrack && this.rtc.sendSourceVideoType(localtrack.getSourceName(), videoType);
1530
+ }
1531
+ /**
1532
+ * Stops the current JVB jingle session.
1533
+ *
1534
+ * @param {Object} options - options for stopping JVB session.
1535
+ * @param {string} options.reason - One of the Jingle "reason" element
1536
+ * names as defined by https://xmpp.org/extensions/xep-0166.html#def-reason
1537
+ * @param {string} options.reasonDescription - Text description that will be included
1538
+ * in the session terminate message.
1539
+ * @param {boolean} options.requestRestart - Whether this is due to
1540
+ * a session restart, in which case, session will be
1541
+ * set to null.
1542
+ * @param {boolean} options.sendSessionTerminate - Whether session-terminate needs to be sent to Jicofo.
1543
+ * @internal
1544
+ */
1545
+ _stopJvbSession(options) {
1546
+ const { requestRestart = false, sendSessionTerminate = false } = options;
1547
+ if (!this.jvbJingleSession) {
1548
+ logger.error('No JVB session to be stopped');
1549
+ return;
1550
+ }
1551
+ // Remove remote JVB tracks.
1552
+ !this.isP2PActive() && this._removeRemoteJVBTracks();
1553
+ logger.info('Stopping stats for jvb connection');
1554
+ this.statistics.stopRemoteStats(this.jvbJingleSession.peerconnection);
1555
+ this.jvbJingleSession.terminate(() => {
1556
+ if (requestRestart && sendSessionTerminate) {
1557
+ logger.info('session-terminate for ice restart - done');
1558
+ }
1559
+ this.jvbJingleSession = null;
1560
+ }, error => {
1561
+ if (requestRestart && sendSessionTerminate) {
1562
+ logger.error('session-terminate for ice restart failed: reloading the client');
1563
+ // Initiate a client reload if Jicofo responds to the session-terminate with an error.
1564
+ this.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_FAILED, JitsiConferenceErrors.ICE_FAILED);
1565
+ }
1566
+ logger.error(`An error occurred while trying to terminate the JVB session', reason=${error.reason},`
1567
+ + `msg=${error.msg}`);
1568
+ }, options);
1569
+ }
1570
+ /**
1571
+ * Method called by the {@link JitsiLocalTrack} in order to remove the underlying MediaStream from the
1572
+ * RTCPeerConnection.
1573
+ * @param {JitsiLocalTrack} track - The local track that will be removed.
1574
+ * @return {Promise} Resolved when the process is done or rejected with a string which describes the error.
1575
+ * @internal
1576
+ */
1577
+ _removeLocalTrackFromPc(track) {
1578
+ const removePromises = [];
1579
+ if (track.conference === this) {
1580
+ if (this.jvbJingleSession) {
1581
+ removePromises.push(this.jvbJingleSession.removeTrackFromPc(track));
1582
+ }
1583
+ else {
1584
+ logger.debug('Remove local MediaStream - no JVB JingleSession started yet');
1585
+ }
1586
+ if (this.p2pJingleSession) {
1587
+ removePromises.push(this.p2pJingleSession.removeTrackFromPc(track));
1588
+ }
1589
+ else {
1590
+ logger.debug('Remove local MediaStream - no P2P JingleSession started yet');
1591
+ }
1592
+ }
1593
+ return Promise.allSettled(removePromises);
1594
+ }
1595
+ /**
1596
+ * Method called by the {@link JitsiLocalTrack} in order to add the underlying MediaStream to the RTCPeerConnection.
1597
+ * @param {JitsiLocalTrack} track - The local track that will be added to the pc.
1598
+ * @return {Promise} Resolved when the process is done or rejected with a string which describes the error.
1599
+ * @internal
1600
+ */
1601
+ async _addLocalTrackToPc(track) {
1602
+ const addPromises = [];
1603
+ if (track.conference === this) {
1604
+ if (this.jvbJingleSession) {
1605
+ addPromises.push(this.jvbJingleSession.addTrackToPc(track));
1606
+ }
1607
+ else {
1608
+ logger.debug('Add local MediaStream - no JVB Jingle session started yet');
1609
+ }
1610
+ if (this.p2pJingleSession) {
1611
+ addPromises.push(this.p2pJingleSession.addTrackToPc(track));
1612
+ }
1613
+ else {
1614
+ logger.debug('Add local MediaStream - no P2P Jingle session started yet');
1615
+ }
1616
+ }
1617
+ else {
1618
+ // If the track hasn't been added to the conference yet because of start muted by focus, add it to the
1619
+ // conference instead of adding it only to the media sessions.
1620
+ addPromises.push(this.addTrack(track));
1621
+ }
1622
+ await Promise.allSettled(addPromises);
1623
+ }
1624
+ /**
1625
+ * Sets mute status.
1626
+ * @param {JitsiLocalTrack} localTrack - The local track.
1627
+ * @param {boolean} isMuted - Whether the track is muted.
1628
+ * @return {boolean} <tt>true</tt> when presence was changed, <tt>false</tt> otherwise.
1629
+ * @internal
1630
+ */
1631
+ _setTrackMuteStatus(localTrack, isMuted) {
1632
+ let presenceChanged = false;
1633
+ if (localTrack) {
1634
+ presenceChanged = this._signalingLayer.setTrackMuteStatus(localTrack.getSourceName(), isMuted) || false;
1635
+ presenceChanged && logger.debug(`Mute state of ${localTrack} changed to muted=${isMuted}`);
1636
+ }
1637
+ return presenceChanged;
1638
+ }
1639
+ /**
1640
+ * Updates conference startMuted policy if needed and fires an event.
1641
+ * @param {boolean} audio - Whether audio should be muted.
1642
+ * @param {boolean} video - Whether video should be muted.
1643
+ * @returns {void}
1644
+ * @internal
1645
+ */
1646
+ _updateStartMutedPolicy(audio, video) {
1647
+ // Update the start muted policy for the conference only if the meta data is received before conference join.
1648
+ if (this.isJoined()) {
1649
+ return;
1650
+ }
1651
+ let updated = false;
1652
+ if (audio !== this.startMutedPolicy.audio) {
1653
+ this.startMutedPolicy.audio = audio;
1654
+ updated = true;
1655
+ }
1656
+ if (video !== this.startMutedPolicy.video) {
1657
+ this.startMutedPolicy.video = video;
1658
+ updated = true;
1659
+ }
1660
+ if (updated) {
1661
+ this.eventEmitter.emit(JitsiConferenceEvents.START_MUTED_POLICY_CHANGED, this.startMutedPolicy);
1662
+ }
1663
+ }
1664
+ /**
1665
+ * Set the transcribingEnabled flag. When transcribing is enabled, p2p is disabled.
1666
+ * @param {boolean} enabled - Whether transcribing should be enabled.
1667
+ * @internal
1668
+ */
1669
+ _setTranscribingEnabled(enabled) {
1670
+ if (this._transcribingEnabled !== enabled) {
1671
+ this._transcribingEnabled = enabled;
1672
+ this._maybeStartOrStopP2P(true);
1673
+ }
1674
+ }
1675
+ /**
1676
+ * Get notified when we joined the room.
1677
+ *
1678
+ * @internal
1679
+ */
1680
+ _onMucJoined() {
1681
+ this._numberOfParticipantsOnJoin = this.getParticipantCount();
1682
+ this._maybeStartOrStopP2P();
1683
+ }
1684
+ /**
1685
+ * Get notified when member bot type had changed.
1686
+ * @param jid the member jid
1687
+ * @param botType the new botType value
1688
+ * @internal
1689
+ */
1690
+ _onMemberBotTypeChanged(jid, botType) {
1691
+ // find the participant and mark it as non bot, as the real one will join
1692
+ // in a moment
1693
+ const peers = this.getParticipants();
1694
+ const botParticipant = peers.find(p => p.getJid() === jid);
1695
+ if (botParticipant) {
1696
+ botParticipant.setBotType(botType);
1697
+ const id = Strophe.getResourceFromJid(jid);
1698
+ this.eventEmitter.emit(JitsiConferenceEvents.BOT_TYPE_CHANGED, id, botType);
1699
+ }
1700
+ // if botType changed to undefined, botType was removed, in case of
1701
+ // poltergeist mode this is the moment when the poltergeist had exited and
1702
+ // the real participant had already replaced it.
1703
+ // In this case we can check and try p2p
1704
+ if (!botParticipant.getBotType()) {
1705
+ this._maybeStartOrStopP2P();
1706
+ }
1707
+ }
1708
+ /**
1709
+ * Handles the suspend detected event. Leaves the room and fires suspended.
1710
+ * @param {JingleSessionPC} jingleSession - The Jingle session.
1711
+ * @internal
1712
+ */
1713
+ onSuspendDetected(jingleSession) {
1714
+ if (!jingleSession.isP2P) {
1715
+ this.leave();
1716
+ this.eventEmitter.emit(JitsiConferenceEvents.SUSPEND_DETECTED);
1717
+ }
1718
+ }
1719
+ /**
1720
+ * Joins the conference.
1721
+ * @param password {string} the password
1722
+ * @param replaceParticipant {boolean} whether the current join replaces
1723
+ * an existing participant with same jwt from the meeting.
1724
+ */
1725
+ join(password = '', replaceParticipant = false) {
1726
+ if (this.room) {
1727
+ this.room.join(password, replaceParticipant).then(() => this._maybeSetSITimeout());
1728
+ }
1729
+ }
1730
+ /**
1731
+ * Connects to the XMPP server using the specified credentials and contacts
1732
+ * Jicofo in order to obtain a session ID (which is then stored in the local
1733
+ * storage). The user's role of the parent conference will be upgraded to
1734
+ * moderator (by Jicofo). It's also used to join the conference when starting
1735
+ * from anonymous domain and only authenticated users are allowed to create new
1736
+ * rooms.
1737
+ *
1738
+ * @param options - Options for authentication and upgrade.
1739
+ * @returns A thenable which settles when the process finishes and has a cancel method.
1740
+ * @internal
1741
+ */
1742
+ authenticateAndUpgradeRole({ id, password, onCreateResource,
1743
+ // 2. Let the API client/consumer know as soon as the XMPP user has been
1744
+ // successfully logged in.
1745
+ onLoginSuccessful }) {
1746
+ let canceled = false;
1747
+ let rejectPromise;
1748
+ let xmpp = new XMPP(this.connection.options, undefined);
1749
+ const process = new Promise((resolve, reject) => {
1750
+ // The process is represented by a Thenable with a cancel method. The
1751
+ // Thenable is implemented using Promise and the cancel using the
1752
+ // Promise's reject function.
1753
+ rejectPromise = reject;
1754
+ xmpp.addListener(JitsiConnectionEvents.CONNECTION_DISCONNECTED, () => {
1755
+ xmpp = undefined;
1756
+ });
1757
+ xmpp.addListener(JitsiConnectionEvents.CONNECTION_ESTABLISHED, () => {
1758
+ if (canceled) {
1759
+ return;
1760
+ }
1761
+ // Let the caller know that the XMPP login was successful.
1762
+ onLoginSuccessful?.();
1763
+ const { config } = this.options;
1764
+ // Now authenticate with Jicofo and get a new session ID.
1765
+ const room = xmpp.createRoom(this.options.name, {
1766
+ ...config,
1767
+ statsId: this._statsCurrentId
1768
+ }, onCreateResource);
1769
+ room.xmpp.moderator.authenticate(room.roomjid)
1770
+ .then(() => {
1771
+ xmpp?.disconnect();
1772
+ if (canceled) {
1773
+ return;
1774
+ }
1775
+ // we execute this logic in JitsiConference where we bind the current conference as `this`
1776
+ // At this point we should have the new session ID
1777
+ // stored in the settings. Send a new conference IQ.
1778
+ this.room.xmpp.moderator.sendConferenceRequest(this.room.roomjid)
1779
+ .catch(e => logger.trace('sendConferenceRequest rejected', e))
1780
+ .finally(() => {
1781
+ // we need to reset it because of breakout rooms which will
1782
+ // reuse connection but will invite jicofo
1783
+ this.room.xmpp.moderator.conferenceRequestSent = false;
1784
+ resolve(undefined);
1785
+ });
1786
+ })
1787
+ .catch(({ error, message }) => {
1788
+ xmpp.disconnect();
1789
+ reject({
1790
+ authenticationError: error,
1791
+ message
1792
+ });
1793
+ });
1794
+ });
1795
+ xmpp.addListener(JitsiConnectionEvents.CONNECTION_FAILED, (connectionError, message, credentials) => {
1796
+ reject({
1797
+ connectionError,
1798
+ credentials,
1799
+ message
1800
+ });
1801
+ xmpp = undefined;
1802
+ });
1803
+ canceled || xmpp.connect(id, password);
1804
+ });
1805
+ /**
1806
+ * Cancels the process, if it's in progress, of authenticating and upgrading
1807
+ * the role of the local participant/user.
1808
+ *
1809
+ * @public
1810
+ * @returns {void}
1811
+ */
1812
+ process.cancel = () => {
1813
+ canceled = true;
1814
+ rejectPromise({});
1815
+ xmpp?.disconnect();
1816
+ };
1817
+ return process;
1818
+ }
1819
+ /**
1820
+ * Check if joined to the conference.
1821
+ * @returns {boolean} True if joined, false otherwise.
1822
+ */
1823
+ isJoined() {
1824
+ return this.room?.joined;
1825
+ }
1826
+ /**
1827
+ * Tells whether or not the P2P mode is enabled in the configuration.
1828
+ * @returns {boolean} True if P2P is enabled, false otherwise.
1829
+ */
1830
+ isP2PEnabled() {
1831
+ return (Boolean(this.options.config.p2p?.enabled)
1832
+ // FIXME: remove once we have a default config template. -saghul
1833
+ || typeof this.options.config.p2p === 'undefined');
1834
+ }
1835
+ /**
1836
+ * When in P2P test mode, the conference will not automatically switch to P2P
1837
+ * when there are 2 participants.
1838
+ * @returns {boolean} True if P2P test mode is enabled, false otherwise.
1839
+ */
1840
+ isP2PTestModeEnabled() {
1841
+ return Boolean(this.options.config.testing?.p2pTestMode);
1842
+ }
1843
+ /**
1844
+ * Leaves the conference.
1845
+ * @param {string|undefined} reason - The reason for leaving the conference.
1846
+ * @returns {Promise}
1847
+ */
1848
+ async leave(reason) {
1849
+ if (this.avgRtpStatsReporter) {
1850
+ this.avgRtpStatsReporter.dispose();
1851
+ this.avgRtpStatsReporter = null;
1852
+ }
1853
+ if (this.e2eping) {
1854
+ this.e2eping.stop();
1855
+ this.e2eping = null;
1856
+ }
1857
+ this.getLocalTracks().forEach(track => this.onLocalTrackRemoved(track));
1858
+ this.rtc.closeBridgeChannel();
1859
+ this._sendConferenceLeftAnalyticsEvent();
1860
+ if (this.statistics) {
1861
+ this.statistics.dispose();
1862
+ }
1863
+ this._delayedIceFailed && this._delayedIceFailed.cancel();
1864
+ this._maybeClearSITimeout();
1865
+ // Close both JVb and P2P JingleSessions
1866
+ if (this.jvbJingleSession) {
1867
+ this.jvbJingleSession.close();
1868
+ this.jvbJingleSession = null;
1869
+ }
1870
+ if (this.p2pJingleSession) {
1871
+ this.p2pJingleSession.close();
1872
+ this.p2pJingleSession = null;
1873
+ }
1874
+ // Leave the conference. If this.room == null we are calling second time leave().
1875
+ if (!this.room) {
1876
+ return;
1877
+ }
1878
+ // let's check is this breakout
1879
+ if (reason === 'switch_room' && this.getBreakoutRooms()?.isBreakoutRoom()) {
1880
+ const mJid = this.getBreakoutRooms().getMainRoomJid();
1881
+ this.xmpp.connection._breakoutMovingToMain = mJid;
1882
+ }
1883
+ const room = this.room;
1884
+ // Unregister connection state listeners
1885
+ room.removeListener(XMPPEvents.CONNECTION_INTERRUPTED, this._onIceConnectionInterrupted);
1886
+ room.removeListener(XMPPEvents.CONNECTION_RESTORED, this._onIceConnectionRestored);
1887
+ room.removeListener(XMPPEvents.CONNECTION_ESTABLISHED, this._onIceConnectionEstablished);
1888
+ room.removeListener(XMPPEvents.CONFERENCE_PROPERTIES_CHANGED, this._updateProperties);
1889
+ room.removeListener(XMPPEvents.MEETING_ID_SET, this._sendConferenceJoinAnalyticsEvent);
1890
+ room.removeListener(XMPPEvents.SESSION_ACCEPT, this._updateRoomPresence);
1891
+ room.removeListener(XMPPEvents.SOURCE_ADD, this._updateRoomPresence);
1892
+ room.removeListener(XMPPEvents.SOURCE_ADD_ERROR, this._removeLocalSourceOnReject);
1893
+ room.removeListener(XMPPEvents.SOURCE_REMOVE, this._updateRoomPresence);
1894
+ this.eventManager.removeXMPPListeners();
1895
+ this._signalingLayer.setChatRoom(null);
1896
+ this.room = null;
1897
+ let leaveError;
1898
+ try {
1899
+ await room.leave(reason);
1900
+ }
1901
+ catch (err) {
1902
+ leaveError = err;
1903
+ // Remove all participants because currently the conference
1904
+ // won't be usable anyway. This is done on success automatically
1905
+ // by the ChatRoom instance.
1906
+ this.getParticipants().forEach(participant => this.onMemberLeft(participant.getJid()));
1907
+ }
1908
+ if (this.rtc) {
1909
+ this.rtc.destroy();
1910
+ }
1911
+ if (leaveError) {
1912
+ throw leaveError;
1913
+ }
1914
+ }
1915
+ /**
1916
+ * Disposes of conference resources. This operation is a short-hand for leaving
1917
+ * the conference and disconnecting the connection.
1918
+ * @returns {Promise}
1919
+ */
1920
+ async dispose() {
1921
+ await this.leave();
1922
+ await this.connection?.disconnect();
1923
+ }
1924
+ /**
1925
+ * Returns true if end conference support is enabled in the backend.
1926
+ * @returns {boolean} whether end conference is supported in the backend.
1927
+ */
1928
+ isEndConferenceSupported() {
1929
+ return Boolean(this.room?.xmpp.endConferenceComponentAddress);
1930
+ }
1931
+ /**
1932
+ * Ends the conference.
1933
+ */
1934
+ end() {
1935
+ if (!this.isEndConferenceSupported()) {
1936
+ logger.warn('Cannot end conference: is not supported.');
1937
+ return;
1938
+ }
1939
+ if (!this.room) {
1940
+ throw new Error('You have already left the conference');
1941
+ }
1942
+ this.room.end();
1943
+ }
1944
+ /**
1945
+ * Returns the currently active media session if any.
1946
+ * @returns {Optional<JingleSessionPC>}
1947
+ */
1948
+ getActiveMediaSession() {
1949
+ return this.isP2PActive() ? this.p2pJingleSession : this.jvbJingleSession;
1950
+ }
1951
+ /**
1952
+ * Returns an array containing all media sessions existing in this conference.
1953
+ * @returns {Array<JingleSessionPC>}
1954
+ */
1955
+ getMediaSessions() {
1956
+ const sessions = [];
1957
+ this.jvbJingleSession && sessions.push(this.jvbJingleSession);
1958
+ this.p2pJingleSession && sessions.push(this.p2pJingleSession);
1959
+ return sessions;
1960
+ }
1961
+ /**
1962
+ * Returns name of this conference.
1963
+ * @returns {string}
1964
+ */
1965
+ getName() {
1966
+ return this.options.name.toString();
1967
+ }
1968
+ /**
1969
+ * Returns the {@link JitsiConnection} used by this conference.
1970
+ * @returns {JitsiConnection}
1971
+ */
1972
+ getConnection() {
1973
+ return this.connection;
1974
+ }
1975
+ /**
1976
+ * Check if authentication is enabled for this conference.
1977
+ * @returns {boolean}
1978
+ */
1979
+ isAuthEnabled() {
1980
+ return this.authEnabled;
1981
+ }
1982
+ /**
1983
+ * Check if user is logged in.
1984
+ * @returns {boolean}
1985
+ */
1986
+ isLoggedIn() {
1987
+ return Boolean(this.authIdentity);
1988
+ }
1989
+ /**
1990
+ * Get authorized login.
1991
+ * @returns {string|null}
1992
+ */
1993
+ getAuthLogin() {
1994
+ return this.authIdentity;
1995
+ }
1996
+ /**
1997
+ * Returns the local tracks of the given media type, or all local tracks if no
1998
+ * specific type is given.
1999
+ * @param {MediaType} [mediaType] Optional media type (audio or video).
2000
+ * @returns {Array<JitsiLocalTrack>}
2001
+ */
2002
+ getLocalTracks(mediaType) {
2003
+ let tracks = [];
2004
+ if (this.rtc) {
2005
+ tracks = this.rtc.getLocalTracks(mediaType);
2006
+ }
2007
+ return tracks;
2008
+ }
2009
+ /**
2010
+ * Obtains local audio track.
2011
+ * @returns {JitsiLocalTrack|null}
2012
+ */
2013
+ getLocalAudioTrack() {
2014
+ return this.rtc ? this.rtc.getLocalAudioTrack() : null;
2015
+ }
2016
+ /**
2017
+ * Obtains local video track.
2018
+ * @returns {JitsiLocalTrack|null}
2019
+ */
2020
+ getLocalVideoTrack() {
2021
+ return this.rtc ? this.rtc.getLocalVideoTrack() : null;
2022
+ }
2023
+ /**
2024
+ * Returns all the local video tracks.
2025
+ * @returns {Array<JitsiLocalTrack>|null}
2026
+ */
2027
+ getLocalVideoTracks() {
2028
+ return this.rtc ? this.rtc.getLocalVideoTracks() : null;
2029
+ }
2030
+ /**
2031
+ * Receives notifications from other participants about commands / custom events
2032
+ * (sent by sendCommand or sendCommandOnce methods).
2033
+ * @param {string} command - The name of the command.
2034
+ * @param {Function} handler - Handler for the command.
2035
+ */
2036
+ addCommandListener(command, handler) {
2037
+ if (this.room) {
2038
+ this.room.addPresenceListener(command, handler);
2039
+ }
2040
+ }
2041
+ /**
2042
+ * Removes command listener.
2043
+ * @param {string} command - The name of the command.
2044
+ * @param {Function} handler - Handler to remove for the command.
2045
+ */
2046
+ removeCommandListener(command, handler) {
2047
+ if (this.room) {
2048
+ this.room.removePresenceListener(command, handler);
2049
+ }
2050
+ }
2051
+ /**
2052
+ /**
2053
+ * Sends text message to the other participants in the conference.
2054
+ * @param {string} message - The text message.
2055
+ * @param {string} [elementName='body'] - The element name to encapsulate the message.
2056
+ * @param {string} [replyToId] - The ID of the message being replied to.
2057
+ * @deprecated Use 'sendMessage' instead. TODO: this should be private.
2058
+ */
2059
+ sendTextMessage(message, elementName = 'body', replyToId) {
2060
+ if (this.room) {
2061
+ this.room.sendMessage(message, elementName, replyToId);
2062
+ }
2063
+ }
2064
+ /**
2065
+ * Sends a reaction to the other participants in the conference.
2066
+ * @param {string} reaction - The reaction.
2067
+ * @param {string} messageId - The ID of the message to attach the reaction to.
2068
+ * @param {string} receiverId - The intended recipient, if the message is private.
2069
+ */
2070
+ sendReaction(reaction, messageId, receiverId) {
2071
+ if (this.room) {
2072
+ this.room.sendReaction(reaction, messageId, receiverId);
2073
+ }
2074
+ }
2075
+ /**
2076
+ * Sends private text message to another participant of the conference.
2077
+ * @param {string} id - The ID of the participant to send a private message.
2078
+ * @param {string} message - The text message.
2079
+ * @param {string} [elementName='body'] - The element name to encapsulate the message.
2080
+ * @param {boolean} [useFullJid=false] - Whether to use the full JID.
2081
+ * @param {string} [replyToId] - The ID of the message being replied to.
2082
+ * @deprecated Use 'sendMessage' instead. TODO: this should be private.
2083
+ */
2084
+ sendPrivateTextMessage(id, message, elementName = 'body', useFullJid = false, replyToId) {
2085
+ if (this.room) {
2086
+ this.room.sendPrivateMessage(id, message, elementName, useFullJid, replyToId);
2087
+ }
2088
+ }
2089
+ /**
2090
+ * Send presence command.
2091
+ * @param {string} name - The name of the command.
2092
+ * @param {Record<string, unknown>} values - With keys and values that will be sent.
2093
+ */
2094
+ sendCommand(name, values) {
2095
+ if (this.room) {
2096
+ this.room.addOrReplaceInPresence(name, values) && this.room.sendPresence();
2097
+ }
2098
+ else {
2099
+ logger.warn('Not sending a command, room not initialized.');
2100
+ }
2101
+ }
2102
+ /**
2103
+ * Send presence command one time.
2104
+ * @param {string} name - The name of the command.
2105
+ * @param {Record<string, unknown>} values - With keys and values that will be sent.
2106
+ */
2107
+ sendCommandOnce(name, values) {
2108
+ this.sendCommand(name, values);
2109
+ this.removeCommand(name);
2110
+ }
2111
+ /**
2112
+ * Removes presence command.
2113
+ * @param {string} name - The name of the command.
2114
+ */
2115
+ removeCommand(name) {
2116
+ if (this.room) {
2117
+ this.room.removeFromPresence(name);
2118
+ }
2119
+ }
2120
+ /**
2121
+ * Sets the display name for this conference.
2122
+ * @param {string} name - The display name to set.
2123
+ */
2124
+ setDisplayName(name) {
2125
+ if (this.room) {
2126
+ const nickKey = 'nick';
2127
+ if (name) {
2128
+ this.room.addOrReplaceInPresence(nickKey, {
2129
+ attributes: { xmlns: 'http://jabber.org/protocol/nick' },
2130
+ value: name
2131
+ }) && this.room.sendPresence(false);
2132
+ }
2133
+ else if (this.room.getFromPresence(nickKey)) {
2134
+ this.room.removeFromPresence(nickKey);
2135
+ this.room.sendPresence(false);
2136
+ }
2137
+ }
2138
+ }
2139
+ /**
2140
+ * Set join without audio.
2141
+ * @param {boolean} silent - Whether user joined without audio.
2142
+ */
2143
+ setIsSilent(silent) {
2144
+ if (this.room) {
2145
+ this.room.addOrReplaceInPresence('silent', {
2146
+ attributes: { xmlns: 'http://jitsi.org/protocol/silent' },
2147
+ value: silent
2148
+ }) && this.room.sendPresence(false);
2149
+ }
2150
+ }
2151
+ /**
2152
+ * Set new subject for this conference. (Available only for moderator)
2153
+ * @param {string} subject - New subject.
2154
+ */
2155
+ setSubject(subject) {
2156
+ if (this.room && this.isModerator()) {
2157
+ this.room.setSubject(subject);
2158
+ }
2159
+ else {
2160
+ logger.warn(`Failed to set subject, ${this.room ? '' : 'not in a room, '}${this.isModerator() ? '' : 'participant is not a moderator'}`);
2161
+ }
2162
+ }
2163
+ /**
2164
+ * Returns the transcription status.
2165
+ * @returns {string} "on" or "off".
2166
+ */
2167
+ getTranscriptionStatus() {
2168
+ return this.room.transcriptionStatus;
2169
+ }
2170
+ /**
2171
+ * Adds JitsiLocalTrack object to the conference.
2172
+ * @param {JitsiLocalTrack} track - The JitsiLocalTrack object.
2173
+ * @returns {Promise<void>}
2174
+ * @throws {Error} If the specified track is a video track and there is already
2175
+ * another video track in the conference.
2176
+ */
2177
+ addTrack(track) {
2178
+ if (!track) {
2179
+ throw new Error('addTrack - a track is required');
2180
+ }
2181
+ const mediaType = track.getType();
2182
+ const localTracks = this.rtc.getLocalTracks(mediaType);
2183
+ // Ensure there's exactly 1 local track of each media type in the conference.
2184
+ if (localTracks.length > 0) {
2185
+ // Don't be excessively harsh and severe if the API
2186
+ // client happens to attempt to add the same local track twice.
2187
+ if (track === localTracks[0]) {
2188
+ return Promise.resolve();
2189
+ }
2190
+ // Currently, only adding multiple video streams of different video types is supported.
2191
+ // TODO - remove this limitation once issues with jitsi-meet trying to add multiple camera streams is fixed.
2192
+ if (this.options.config.testing?.allowMultipleTracks
2193
+ || (mediaType === MediaType.VIDEO && !localTracks.find(t => t.getVideoType() === track.getVideoType()))) {
2194
+ const sourceName = getSourceNameForJitsiTrack(this.myUserId(), mediaType, this.getLocalTracks(mediaType)?.length);
2195
+ track.setSourceName(sourceName);
2196
+ const addTrackPromises = [];
2197
+ this.p2pJingleSession && addTrackPromises.push(this.p2pJingleSession.addTracks([track]));
2198
+ this.jvbJingleSession && addTrackPromises.push(this.jvbJingleSession.addTracks([track]));
2199
+ return Promise.all(addTrackPromises)
2200
+ .then(() => {
2201
+ this._setupNewTrack(track);
2202
+ mediaType === MediaType.VIDEO && this._sendBridgeVideoTypeMessage(track);
2203
+ this._updateRoomPresence(this.getActiveMediaSession());
2204
+ if (this.isMutedByFocus || this.isVideoMutedByFocus || this.isDesktopMutedByFocus) {
2205
+ this._fireMuteChangeEvent(track);
2206
+ }
2207
+ });
2208
+ }
2209
+ return Promise.reject(new Error(`Cannot add second ${mediaType} track to the conference`));
2210
+ }
2211
+ return this.replaceTrack(null, track)
2212
+ .then(() => {
2213
+ // Presence needs to be sent here for desktop track since we need the presence to reach the remote peer
2214
+ // before signaling so that a fake participant tile is created for screenshare. Otherwise, presence will
2215
+ // only be sent after a session-accept or source-add is ack'ed.
2216
+ if (track.getVideoType() === VideoType.DESKTOP) {
2217
+ this._updateRoomPresence(this.getActiveMediaSession());
2218
+ }
2219
+ });
2220
+ }
2221
+ /**
2222
+ * Clear JitsiLocalTrack properties and listeners.
2223
+ * @param {JitsiLocalTrack} track - The JitsiLocalTrack object.
2224
+ * @internal
2225
+ */
2226
+ onLocalTrackRemoved(track) {
2227
+ track.setConference(null);
2228
+ this.rtc.removeLocalTrack(track);
2229
+ this._unsubscribers.forEach(remove => remove());
2230
+ this._unsubscribers = [];
2231
+ this.eventEmitter.emit(JitsiConferenceEvents.TRACK_REMOVED, track);
2232
+ }
2233
+ /**
2234
+ * Removes JitsiLocalTrack from the conference and performs
2235
+ * a new offer/answer cycle.
2236
+ * @param {JitsiLocalTrack} track - The track to remove.
2237
+ * @returns {Promise}
2238
+ */
2239
+ removeTrack(track) {
2240
+ return this.replaceTrack(track, null);
2241
+ }
2242
+ /**
2243
+ * Replaces oldTrack with newTrack and performs a single offer/answer
2244
+ * cycle after both operations are done. Either oldTrack or newTrack
2245
+ * can be null; replacing a valid 'oldTrack' with a null 'newTrack'
2246
+ * effectively just removes 'oldTrack'
2247
+ * @param {JitsiLocalTrack} oldTrack - The current stream in use to be replaced.
2248
+ * @param {JitsiLocalTrack} newTrack - The new stream to use.
2249
+ * @returns {Promise} Resolves when the replacement is finished.
2250
+ */
2251
+ replaceTrack(oldTrack, newTrack) {
2252
+ const oldVideoType = oldTrack?.getVideoType();
2253
+ const mediaType = oldTrack?.getType() || newTrack?.getType();
2254
+ const newVideoType = newTrack?.getVideoType();
2255
+ if (oldTrack && newTrack && oldVideoType !== newVideoType) {
2256
+ throw new Error(`Replacing a track of videoType=${oldVideoType} with a track of videoType=${newVideoType} is`
2257
+ + ' not supported in this mode.');
2258
+ }
2259
+ if (newTrack) {
2260
+ const sourceName = oldTrack
2261
+ ? oldTrack.getSourceName()
2262
+ : getSourceNameForJitsiTrack(this.myUserId(), mediaType, this.getLocalTracks(mediaType)?.length);
2263
+ newTrack.setSourceName(sourceName);
2264
+ }
2265
+ const oldTrackBelongsToConference = this === oldTrack?.conference;
2266
+ if (oldTrackBelongsToConference && oldTrack.disposed) {
2267
+ return Promise.reject(new JitsiTrackError(JitsiTrackErrors.TRACK_IS_DISPOSED));
2268
+ }
2269
+ if (newTrack?.disposed) {
2270
+ return Promise.reject(new JitsiTrackError(JitsiTrackErrors.TRACK_IS_DISPOSED));
2271
+ }
2272
+ if (oldTrack && !oldTrackBelongsToConference) {
2273
+ logger.warn(`JitsiConference.replaceTrack oldTrack (${oldTrack} does not belong to this conference`);
2274
+ }
2275
+ // Now replace the stream at the lower levels
2276
+ return this._doReplaceTrack(oldTrackBelongsToConference ? oldTrack : null, newTrack)
2277
+ .then(() => {
2278
+ if (oldTrackBelongsToConference && !oldTrack.isMuted() && !newTrack) {
2279
+ oldTrack._sendMuteStatus(true);
2280
+ }
2281
+ oldTrackBelongsToConference && this.onLocalTrackRemoved(oldTrack);
2282
+ newTrack && this._setupNewTrack(newTrack);
2283
+ // Send 'VideoTypeMessage' on the bridge channel when a video track is added/removed.
2284
+ if ((oldTrackBelongsToConference && oldTrack?.isVideoTrack()) || newTrack?.isVideoTrack()) {
2285
+ this._sendBridgeVideoTypeMessage(newTrack);
2286
+ }
2287
+ this._updateRoomPresence(this.getActiveMediaSession());
2288
+ if (newTrack !== null && (this.isMutedByFocus || this.isVideoMutedByFocus
2289
+ || this.isDesktopMutedByFocus)) {
2290
+ this._fireMuteChangeEvent(newTrack);
2291
+ }
2292
+ return Promise.resolve();
2293
+ })
2294
+ .catch(error => {
2295
+ logger.error(`replaceTrack failed: ${error?.stack}`);
2296
+ return Promise.reject(error);
2297
+ });
2298
+ }
2299
+ /**
2300
+ * Get role of the local user.
2301
+ * @returns {string} User role: 'moderator' or 'none'.
2302
+ */
2303
+ getRole() {
2304
+ return this.room.role;
2305
+ }
2306
+ /**
2307
+ * Returns whether or not the current conference has been joined as a hidden user.
2308
+ * @returns {boolean} True if hidden, false otherwise. Will return false if no connection is active.
2309
+ */
2310
+ isHidden() {
2311
+ if (!this.connection) {
2312
+ return false;
2313
+ }
2314
+ return Strophe.getDomainFromJid(this.connection.getJid())
2315
+ === this.options.config.hiddenDomain;
2316
+ }
2317
+ /**
2318
+ * Check if local user is moderator.
2319
+ * @returns {boolean} true if local user is moderator, false otherwise. If
2320
+ * we're no longer in the conference room then <tt>false</tt> is returned.
2321
+ */
2322
+ isModerator() {
2323
+ return this.room ? this.room.isModerator() : false;
2324
+ }
2325
+ /**
2326
+ * Set password for the room.
2327
+ * @param {string} password new password for the room.
2328
+ * @returns {Promise}
2329
+ */
2330
+ lock(password) {
2331
+ if (!this.isModerator()) {
2332
+ return Promise.reject(new Error('You are not moderator.'));
2333
+ }
2334
+ return new Promise((resolve, reject) => {
2335
+ this.room.lockRoom(password || '', () => resolve(), (err) => reject(err), () => reject(JitsiConferenceErrors.PASSWORD_NOT_SUPPORTED));
2336
+ });
2337
+ }
2338
+ /**
2339
+ * Remove password from the room.
2340
+ * @returns {Promise}
2341
+ */
2342
+ unlock() {
2343
+ return this.lock('');
2344
+ }
2345
+ /**
2346
+ * Obtains the current value for "lastN". See {@link setLastN} for more info.
2347
+ * @returns {number}
2348
+ */
2349
+ getLastN() {
2350
+ return this.qualityController.receiveVideoController.getLastN();
2351
+ }
2352
+ /**
2353
+ * Obtains the forwarded sources list in this conference.
2354
+ * @return {Array<string>}
2355
+ * @internal
2356
+ */
2357
+ getForwardedSources() {
2358
+ return this.rtc.getForwardedSources();
2359
+ }
2360
+ /**
2361
+ * Sets the audio subscription mode for the local user.
2362
+ *
2363
+ * @param {IReceiverAudioSubscriptionMessage} message - The audio subscription mode to set.
2364
+ * @returns {void}
2365
+ */
2366
+ setAudioSubscriptionMode(message) {
2367
+ this.qualityController.audioController.setAudioSubscriptionMode(message);
2368
+ }
2369
+ /**
2370
+ * Selects a new value for "lastN". The requested amount of videos are going
2371
+ * to be delivered after the value is in effect. Set to -1 for unlimited or
2372
+ * all available videos.
2373
+ * @param lastN the new number of videos the user would like to receive.
2374
+ * @throws Error or RangeError if the given value is not a number or is smaller
2375
+ * than -1.
2376
+ */
2377
+ setLastN(lastN) {
2378
+ if (!Number.isInteger(lastN)) {
2379
+ throw new Error(`Invalid value for lastN: ${lastN}`);
2380
+ }
2381
+ const n = Number(lastN);
2382
+ if (n < -1) {
2383
+ throw new RangeError('lastN cannot be smaller than -1');
2384
+ }
2385
+ this.qualityController.receiveVideoController.setLastN(n);
2386
+ // If the P2P session is not fully established yet, we wait until it gets established.
2387
+ if (this.p2pJingleSession) {
2388
+ const isVideoActive = n !== 0;
2389
+ this.p2pJingleSession
2390
+ .setP2pVideoTransferActive(isVideoActive)
2391
+ .catch(error => {
2392
+ logger.error(`Failed to adjust video transfer status (${isVideoActive})`, error);
2393
+ });
2394
+ }
2395
+ }
2396
+ /**
2397
+ * @return Array<JitsiParticipant> an array of all participants in this conference.
2398
+ */
2399
+ getParticipants() {
2400
+ return Array.from(this.participants.values());
2401
+ }
2402
+ /**
2403
+ * Returns the number of participants in the conference, including the local
2404
+ * participant.
2405
+ * @param countHidden {boolean} Whether or not to include hidden participants
2406
+ * in the count. Default: false.
2407
+ **/
2408
+ getParticipantCount(countHidden = false) {
2409
+ let participants = this.getParticipants();
2410
+ if (!countHidden) {
2411
+ participants = participants.filter(p => !p.isHidden());
2412
+ }
2413
+ // Add one for the local participant.
2414
+ return participants.length + 1;
2415
+ }
2416
+ /**
2417
+ * @returns {JitsiParticipant} the participant in this conference with the
2418
+ * specified id (or undefined if there isn't one).
2419
+ * @param id the id of the participant.
2420
+ */
2421
+ getParticipantById(id) {
2422
+ return this.participants.get(id);
2423
+ }
2424
+ /**
2425
+ * Grant owner rights to the participant.
2426
+ * @param {string} id id of the participant to grant owner rights to.
2427
+ */
2428
+ grantOwner(id) {
2429
+ const participant = this.getParticipantById(id);
2430
+ if (!participant) {
2431
+ return;
2432
+ }
2433
+ this.room.setAffiliation(participant.getConnectionJid(), 'owner');
2434
+ }
2435
+ /**
2436
+ * Revoke owner rights to the participant or local Participant as
2437
+ * the user might want to refuse to be a moderator.
2438
+ * @param {string} id id of the participant to revoke owner rights to.
2439
+ */
2440
+ revokeOwner(id) {
2441
+ const participant = this.getParticipantById(id);
2442
+ const isMyself = this.myUserId() === id;
2443
+ const role = this.isMembersOnly() ? 'member' : 'none';
2444
+ if (isMyself) {
2445
+ this.room.setAffiliation(this.connection.getJid(), role);
2446
+ }
2447
+ else if (participant) {
2448
+ this.room.setAffiliation(participant.getConnectionJid(), role);
2449
+ }
2450
+ }
2451
+ /**
2452
+ * Kick participant from this conference.
2453
+ * @param {string} id id of the participant to kick
2454
+ * @param {string} reason reason of the participant to kick
2455
+ */
2456
+ kickParticipant(id, reason) {
2457
+ const participant = this.getParticipantById(id);
2458
+ if (!participant) {
2459
+ return;
2460
+ }
2461
+ this.room.kick(participant.getJid(), reason);
2462
+ }
2463
+ /**
2464
+ * Mutes or unmutes the remote audio streams based on the provided parameter.
2465
+ *
2466
+ * @param {boolean} muted - Whether the user should stop receiving remote audio.
2467
+ * @returns {void}
2468
+ */
2469
+ muteRemoteAudio(muted) {
2470
+ this.qualityController.audioController.muteRemoteAudio(muted);
2471
+ }
2472
+ /**
2473
+ * Mutes a participant.
2474
+ * @param {string} id The id of the participant to mute.
2475
+ */
2476
+ muteParticipant(id, mediaType = MediaType.AUDIO) {
2477
+ if (!mediaType) {
2478
+ logger.error(`Unsupported media type: ${mediaType}`);
2479
+ return;
2480
+ }
2481
+ const participant = this.getParticipantById(id);
2482
+ if (!participant) {
2483
+ return;
2484
+ }
2485
+ this.room.muteParticipant(participant.getJid(), true, mediaType);
2486
+ }
2487
+ /* eslint-disable max-params */
2488
+ /**
2489
+ * Notifies this JitsiConference that a new member has joined its chat room.
2490
+ *
2491
+ * FIXME This should NOT be exposed!
2492
+ *
2493
+ * @param jid the jid of the participant in the MUC
2494
+ * @param nick the display name of the participant
2495
+ * @param role the role of the participant in the MUC
2496
+ * @param isHidden indicates if this is a hidden participant (system
2497
+ * participant for example a recorder).
2498
+ * @param statsID the participant statsID (optional)
2499
+ * @param status the initial status if any
2500
+ * @param identity the member identity, if any
2501
+ * @param botType the member botType, if any
2502
+ * @param fullJid the member full jid, if any
2503
+ * @param features the member botType, if any
2504
+ * @param isReplaceParticipant whether this join replaces a participant with
2505
+ * the same jwt.
2506
+ * @internal
2507
+ */
2508
+ onMemberJoined(jid, nick, role, isHidden, statsID, status, identity, botType, fullJid, features, isReplaceParticipant) {
2509
+ const id = Strophe.getResourceFromJid(jid);
2510
+ if (id === 'focus' || this.myUserId() === id) {
2511
+ return;
2512
+ }
2513
+ const participant = new JitsiParticipant(jid, this, nick, isHidden, statsID, status, identity);
2514
+ participant.setConnectionJid(fullJid);
2515
+ participant.setRole(role);
2516
+ participant.setBotType(botType);
2517
+ participant.setFeatures(features ? new Set([features]) : undefined);
2518
+ participant.setIsReplacing(isReplaceParticipant);
2519
+ // Set remote tracks on the participant if source signaling was received before presence.
2520
+ const remoteTracks = this.isP2PActive()
2521
+ ? this.p2pJingleSession?.peerconnection.getRemoteTracks(id) ?? []
2522
+ : this.jvbJingleSession?.peerconnection.getRemoteTracks(id) ?? [];
2523
+ for (const track of remoteTracks) {
2524
+ participant._tracks.push(track);
2525
+ }
2526
+ this.participants.set(id, participant);
2527
+ this.eventEmitter.emit(JitsiConferenceEvents.USER_JOINED, id, participant);
2528
+ this._updateFeatures(participant);
2529
+ // maybeStart only if we had finished joining as then we will have information for the number of participants
2530
+ if (this.isJoined()) {
2531
+ this._maybeStartOrStopP2P();
2532
+ }
2533
+ this._maybeSetSITimeout();
2534
+ const { startAudioMuted, startVideoMuted } = this.options.config;
2535
+ // Ignore startAudio/startVideoMuted settings if the media session has already been established.
2536
+ // Apply the policy if the number of participants exceeds the startMuted thresholds.
2537
+ if ((this.jvbJingleSession && this.getActiveMediaSession() === this.jvbJingleSession)
2538
+ || ((typeof startAudioMuted === 'undefined' || startAudioMuted === -1)
2539
+ && (typeof startVideoMuted === 'undefined' || startVideoMuted === -1))) {
2540
+ return;
2541
+ }
2542
+ let audioMuted = false;
2543
+ let videoMuted = false;
2544
+ const numberOfParticipants = this.getParticipantCount();
2545
+ if (numberOfParticipants > this.options.config.startAudioMuted) {
2546
+ audioMuted = true;
2547
+ }
2548
+ if (numberOfParticipants > this.options.config.startVideoMuted) {
2549
+ videoMuted = true;
2550
+ }
2551
+ if ((audioMuted && !this.startMutedPolicy.audio) || (videoMuted && !this.startMutedPolicy.video)) {
2552
+ this._updateStartMutedPolicy(audioMuted, videoMuted);
2553
+ }
2554
+ }
2555
+ /**
2556
+ * Handles the logic when a remote participant leaves the conference.
2557
+ * @param {string} jid - The Jabber ID (JID) of the participant who left.
2558
+ * @param {string} [reason] - Optional reason provided for the participant leaving.
2559
+ * @internal
2560
+ */
2561
+ onMemberLeft(jid, reason) {
2562
+ const id = Strophe.getResourceFromJid(jid);
2563
+ if (id === 'focus' || this.myUserId() === id) {
2564
+ return;
2565
+ }
2566
+ const mediaSessions = this.getMediaSessions();
2567
+ let tracksToBeRemoved = [];
2568
+ for (const session of mediaSessions) {
2569
+ const remoteTracks = session.peerconnection.getRemoteTracks(id);
2570
+ remoteTracks && (tracksToBeRemoved = [...tracksToBeRemoved, ...remoteTracks]);
2571
+ // Update the SSRC owners list.
2572
+ session._signalingLayer.updateSsrcOwnersOnLeave(id);
2573
+ if (!FeatureFlags.isSsrcRewritingSupported()) {
2574
+ // Remove the ssrcs from the remote description and renegotiate.
2575
+ session.removeRemoteStreamsOnLeave(id);
2576
+ }
2577
+ }
2578
+ tracksToBeRemoved.forEach(track => {
2579
+ // Fire the event before renegotiation is done so that the thumbnails can be removed immediately.
2580
+ this.eventEmitter.emit(JitsiConferenceEvents.TRACK_REMOVED, track);
2581
+ if (FeatureFlags.isSsrcRewritingSupported()) {
2582
+ track.setSourceName(null);
2583
+ track.setOwner(null);
2584
+ }
2585
+ });
2586
+ const participant = this.participants.get(id);
2587
+ if (participant) {
2588
+ this.participants.delete(id);
2589
+ this.eventEmitter.emit(JitsiConferenceEvents.USER_LEFT, id, participant, reason);
2590
+ }
2591
+ if (this.room !== null) { // Skip if we have left the room already.
2592
+ this._maybeStartOrStopP2P(true /* triggered by user left event */);
2593
+ this._maybeClearSITimeout();
2594
+ }
2595
+ }
2596
+ /* eslint-disable max-params */
2597
+ /**
2598
+ * Designates an event indicating that we were kicked from the XMPP MUC.
2599
+ * @param {boolean} isSelfPresence - whether it is for local participant
2600
+ * or another participant.
2601
+ * @param {string} actorId - the id of the participant who was initiator
2602
+ * of the kick.
2603
+ * @param {string?} kickedParticipantId - when it is not a kick for local participant,
2604
+ * this is the id of the participant which was kicked.
2605
+ * @param {string} reason - reason of the participant to kick
2606
+ * @param {boolean?} isReplaceParticipant - whether this is a server initiated kick in order
2607
+ * to replace it with a participant with same jwt.
2608
+ * @internal
2609
+ */
2610
+ onMemberKicked(isSelfPresence, actorId, kickedParticipantId, reason, isReplaceParticipant) {
2611
+ let actorParticipant;
2612
+ if (actorId === this.myUserId()) {
2613
+ // When we kick someone we also want to send the PARTICIPANT_KICKED event, but there is no
2614
+ // JitsiParticipant object for ourselves so create a minimum fake one.
2615
+ actorParticipant = {
2616
+ getId: () => actorId
2617
+ };
2618
+ }
2619
+ else {
2620
+ actorParticipant = this.participants.get(actorId);
2621
+ }
2622
+ if (isSelfPresence) {
2623
+ this.leave().finally(() => this._xmpp.disconnect());
2624
+ this.eventEmitter.emit(JitsiConferenceEvents.KICKED, actorParticipant, reason, isReplaceParticipant);
2625
+ return;
2626
+ }
2627
+ const kickedParticipant = this.participants.get(kickedParticipantId);
2628
+ kickedParticipant.setIsReplaced(isReplaceParticipant);
2629
+ this.eventEmitter.emit(JitsiConferenceEvents.PARTICIPANT_KICKED, actorParticipant, kickedParticipant, reason);
2630
+ }
2631
+ /**
2632
+ * Method called on local MUC role change.
2633
+ * @param {string} role the name of new user's role as defined by XMPP MUC.
2634
+ * @internal
2635
+ */
2636
+ onLocalRoleChanged(role) {
2637
+ // Emit role changed for local JID
2638
+ this.eventEmitter.emit(JitsiConferenceEvents.USER_ROLE_CHANGED, this.myUserId(), role);
2639
+ }
2640
+ /**
2641
+ * Handles changes to a user's role within the conference.
2642
+ * @param {string} jid - The Jabber ID (JID) of the user whose role has changed.
2643
+ * @param {string} role - The new role assigned to the user (e.g., 'moderator', 'participant').
2644
+ * @internal
2645
+ */
2646
+ onUserRoleChanged(jid, role) {
2647
+ const id = Strophe.getResourceFromJid(jid);
2648
+ const participant = this.getParticipantById(id);
2649
+ if (!participant) {
2650
+ return;
2651
+ }
2652
+ participant.setRole(role);
2653
+ this.eventEmitter.emit(JitsiConferenceEvents.USER_ROLE_CHANGED, id, role);
2654
+ }
2655
+ /**
2656
+ * Handles updates to a participant's display name.
2657
+ * @param {string} jid - The Jabber ID (JID) of the participant whose display name changed.
2658
+ * @param {string} displayName - The new display name for the participant.
2659
+ * @internal
2660
+ */
2661
+ onDisplayNameChanged(jid, displayName) {
2662
+ const id = Strophe.getResourceFromJid(jid);
2663
+ const participant = this.getParticipantById(id);
2664
+ if (!participant) {
2665
+ return;
2666
+ }
2667
+ if (participant._displayName === displayName) {
2668
+ return;
2669
+ }
2670
+ participant._displayName = displayName;
2671
+ this.eventEmitter.emit(JitsiConferenceEvents.DISPLAY_NAME_CHANGED, id, displayName);
2672
+ }
2673
+ /**
2674
+ * Handles changes to a participant's silent status.
2675
+ * @param {string} jid - The Jabber ID (JID) of the participant whose silent status has changed.
2676
+ * @param {boolean} isSilent - The new silent status of the participant (true if silent, false otherwise).
2677
+ * @internal
2678
+ */
2679
+ onSilentStatusChanged(jid, isSilent) {
2680
+ const id = Strophe.getResourceFromJid(jid);
2681
+ const participant = this.getParticipantById(id);
2682
+ if (!participant) {
2683
+ return;
2684
+ }
2685
+ participant.setIsSilent(isSilent);
2686
+ this.eventEmitter.emit(JitsiConferenceEvents.SILENT_STATUS_CHANGED, id, isSilent);
2687
+ }
2688
+ /**
2689
+ * Notifies this JitsiConference that a JitsiRemoteTrack was added to the conference.
2690
+ *
2691
+ * @param {JitsiRemoteTrack} track the JitsiRemoteTrack which was added to this JitsiConference.
2692
+ * @internal
2693
+ */
2694
+ onRemoteTrackAdded(track) {
2695
+ if (track.isP2P && !this.isP2PActive()) {
2696
+ logger.info('Trying to add remote P2P track, when not in P2P - IGNORED');
2697
+ return;
2698
+ }
2699
+ else if (!track.isP2P && this.isP2PActive()) {
2700
+ logger.info('Trying to add remote JVB track, when in P2P - IGNORED');
2701
+ return;
2702
+ }
2703
+ const id = track.getParticipantId();
2704
+ const participant = this.getParticipantById(id);
2705
+ // Add track to JitsiParticipant.
2706
+ if (participant) {
2707
+ participant._tracks.push(track);
2708
+ }
2709
+ else {
2710
+ logger.info(`Source signaling received before presence for ${id}`);
2711
+ }
2712
+ const emitter = this.eventEmitter;
2713
+ track.addEventListener(JitsiTrackEvents.TRACK_MUTE_CHANGED, () => emitter.emit(JitsiConferenceEvents.TRACK_MUTE_CHANGED, track));
2714
+ track.isAudioTrack() && track.addEventListener(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, (audioLevel, tpc) => {
2715
+ const activeTPC = this.getActivePeerConnection();
2716
+ if (activeTPC === tpc) {
2717
+ emitter.emit(JitsiConferenceEvents.TRACK_AUDIO_LEVEL_CHANGED, id, audioLevel);
2718
+ }
2719
+ });
2720
+ emitter.emit(JitsiConferenceEvents.TRACK_ADDED, track);
2721
+ }
2722
+ // eslint-disable-next-line no-unused-vars
2723
+ /**
2724
+ * Callback called by the Jingle plugin when 'session-answer' is received.
2725
+ * @param {JingleSessionPC} session - The Jingle session for which an answer was received.
2726
+ * @param {Element} answer - An element pointing to 'jingle' IQ element.
2727
+ * @internal
2728
+ */
2729
+ onCallAccepted(session, answer) {
2730
+ if (this.p2pJingleSession === session) {
2731
+ logger.info('P2P setAnswer');
2732
+ this.p2pJingleSession.setAnswer(answer)
2733
+ .then(() => {
2734
+ this.eventEmitter.emit(JitsiConferenceEvents._MEDIA_SESSION_STARTED, this.p2pJingleSession);
2735
+ })
2736
+ .catch(error => {
2737
+ logger.error('Error setting P2P answer', error);
2738
+ if (this.p2pJingleSession) {
2739
+ this.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_FAILED, JitsiConferenceErrors.OFFER_ANSWER_FAILED, error);
2740
+ }
2741
+ });
2742
+ }
2743
+ }
2744
+ // eslint-disable-next-line no-unused-vars
2745
+ /**
2746
+ * Callback called by the Jingle plugin when 'transport-info' is received.
2747
+ * @param {JingleSessionPC} session - The Jingle session for which the IQ was received.
2748
+ * @param {Element} transportInfo - An element pointing to 'jingle' IQ element.
2749
+ * @internal
2750
+ */
2751
+ onTransportInfo(session, transportInfo) {
2752
+ if (this.p2pJingleSession === session) {
2753
+ logger.info('P2P addIceCandidates');
2754
+ this.p2pJingleSession.addIceCandidates(transportInfo);
2755
+ }
2756
+ }
2757
+ /**
2758
+ * Notifies this JitsiConference that a JitsiRemoteTrack was removed from the conference.
2759
+ *
2760
+ * @param {JitsiRemoteTrack} removedTrack - The track that was removed.
2761
+ * @internal
2762
+ */
2763
+ onRemoteTrackRemoved(removedTrack) {
2764
+ this.getParticipants().forEach(participant => {
2765
+ const tracks = participant.getTracks();
2766
+ for (let i = 0; i < tracks.length; i++) {
2767
+ if (tracks[i] === removedTrack) {
2768
+ // Since the tracks have been compared and are
2769
+ // considered equal the result of splice can be ignored.
2770
+ participant._tracks.splice(i, 1);
2771
+ this.eventEmitter.emit(JitsiConferenceEvents.TRACK_REMOVED, removedTrack);
2772
+ break;
2773
+ }
2774
+ }
2775
+ }, this);
2776
+ }
2777
+ /**
2778
+ * Handles an incoming call event.
2779
+ * @param {JingleSessionPC} jingleSession - The Jingle session for the incoming call.
2780
+ * @param {Element} jingleOffer - An element pointing to 'jingle' IQ element containing the offer.
2781
+ * @param {number} now - The timestamp when the call was received.
2782
+ * @internal
2783
+ */
2784
+ onIncomingCall(jingleSession, jingleOffer, now) {
2785
+ // Handle incoming P2P call
2786
+ if (jingleSession.isP2P) {
2787
+ this._onIncomingCallP2P(jingleSession, jingleOffer);
2788
+ }
2789
+ else {
2790
+ if (!this.isFocus(jingleSession.remoteJid)) {
2791
+ const description = 'Rejecting session-initiate from non-focus.';
2792
+ this._rejectIncomingCall(jingleSession, {
2793
+ errorMsg: description,
2794
+ reason: 'security-error',
2795
+ reasonDescription: description
2796
+ });
2797
+ return;
2798
+ }
2799
+ this._acceptJvbIncomingCall(jingleSession, jingleOffer, now);
2800
+ }
2801
+ }
2802
+ /**
2803
+ * Handles the call ended event.
2804
+ * XXX is this due to the remote side terminating the Jingle session?
2805
+ *
2806
+ * @param {JingleSessionPC} jingleSession - The Jingle session which has been terminated.
2807
+ * @param {String} reasonCondition - The Jingle reason condition.
2808
+ * @param {String|null} reasonText - Human readable reason text which may provide
2809
+ * more details about why the call has been terminated.
2810
+ * @internal
2811
+ */
2812
+ onCallEnded(jingleSession, reasonCondition, reasonText) {
2813
+ logger.info(`Call ended: ${reasonCondition} - ${reasonText} P2P ?${jingleSession.isP2P}`);
2814
+ if (jingleSession === this.jvbJingleSession) {
2815
+ this.wasStopped = true;
2816
+ Statistics.sendAnalytics(createJingleEvent(AnalyticsEvents.ACTION_JINGLE_TERMINATE, { p2p: false }));
2817
+ // Stop the stats
2818
+ if (this.statistics) {
2819
+ this.statistics.stopRemoteStats(this.jvbJingleSession.peerconnection);
2820
+ }
2821
+ // Current JVB JingleSession is no longer valid, so set it to null
2822
+ this.jvbJingleSession = null;
2823
+ // Let the RTC service do any cleanups
2824
+ this.rtc.onCallEnded();
2825
+ }
2826
+ else if (jingleSession === this.p2pJingleSession) {
2827
+ const stopOptions = {};
2828
+ if (reasonCondition === 'connectivity-error' && reasonText === 'ICE FAILED') {
2829
+ // It can happen that the other peer detects ICE failed and
2830
+ // terminates the session, before we get the event
2831
+ // on our side. But we are able to parse the reason and mark it here.
2832
+ Statistics.analytics.addPermanentProperties({ p2pFailed: true });
2833
+ }
2834
+ else if (reasonCondition === 'success' && reasonText === 'restart') {
2835
+ // When we are restarting media sessions we don't want to switch the tracks to the JVB just yet.
2836
+ stopOptions.requestRestart = true;
2837
+ }
2838
+ this._stopP2PSession(stopOptions);
2839
+ }
2840
+ else {
2841
+ logger.error('Received onCallEnded for invalid session', jingleSession.sid, jingleSession.remoteJid, reasonCondition, reasonText);
2842
+ }
2843
+ }
2844
+ /**
2845
+ * Updates DTMF support based on participants' capabilities.
2846
+ * @returns {void}
2847
+ */
2848
+ updateDTMFSupport() {
2849
+ let somebodySupportsDTMF = false;
2850
+ const participants = this.getParticipants();
2851
+ // check if at least 1 participant supports DTMF
2852
+ for (let i = 0; i < participants.length; i += 1) {
2853
+ if (participants[i].supportsDTMF()) {
2854
+ somebodySupportsDTMF = true;
2855
+ break;
2856
+ }
2857
+ }
2858
+ if (somebodySupportsDTMF !== this.somebodySupportsDTMF) {
2859
+ this.somebodySupportsDTMF = somebodySupportsDTMF;
2860
+ this.eventEmitter.emit(JitsiConferenceEvents.DTMF_SUPPORT_CHANGED, somebodySupportsDTMF);
2861
+ }
2862
+ }
2863
+ /**
2864
+ * Allows to check if there is at least one user in the conference that supports DTMF.
2865
+ * @returns {boolean} True if somebody supports DTMF, false otherwise.
2866
+ */
2867
+ isDTMFSupported() {
2868
+ return this.somebodySupportsDTMF;
2869
+ }
2870
+ /**
2871
+ * Returns the local user's ID.
2872
+ * @returns {string|null} Local user's ID or null if not available.
2873
+ */
2874
+ myUserId() {
2875
+ return (this.room?.myroomjid
2876
+ ? Strophe.getResourceFromJid(this.room.myroomjid)
2877
+ : null);
2878
+ }
2879
+ /**
2880
+ * Sends DTMF tones to the active peer connection.
2881
+ * @param {string} tones - The DTMF tones to send.
2882
+ * @param {number} duration - The duration of each tone in milliseconds.
2883
+ * @param {number} pause - The pause duration between tones in milliseconds.
2884
+ * @returns {void}
2885
+ */
2886
+ sendTones(tones, duration, pause) {
2887
+ const peerConnection = this.getActivePeerConnection();
2888
+ if (peerConnection) {
2889
+ peerConnection.sendTones(tones, duration, pause);
2890
+ }
2891
+ else {
2892
+ logger.warn('cannot sendTones: no peer connection');
2893
+ }
2894
+ }
2895
+ /**
2896
+ * Starts recording the current conference.
2897
+ *
2898
+ * @param {IRecordingOptions} options - Configuration for the recording.
2899
+ * @returns {Promise} Resolves when recording starts successfully, rejects otherwise.
2900
+ */
2901
+ startRecording(options) {
2902
+ if (this.room) {
2903
+ return this.recordingManager.startRecording(options);
2904
+ }
2905
+ return Promise.reject(new Error('The conference is not created yet!'));
2906
+ }
2907
+ /**
2908
+ * Stops a recording session.
2909
+ *
2910
+ * @param {string} sessionID - The ID of the recording session to stop.
2911
+ * @returns {Promise} Resolves when recording stops successfully, rejects otherwise.
2912
+ */
2913
+ stopRecording(sessionID) {
2914
+ if (this.room) {
2915
+ return this.recordingManager.stopRecording(sessionID);
2916
+ }
2917
+ return Promise.reject(new Error('The conference is not created yet!'));
2918
+ }
2919
+ /**
2920
+ * Returns true if SIP calls are supported, false otherwise.
2921
+ * @returns {boolean} True if SIP calling is supported, false otherwise.
2922
+ */
2923
+ isSIPCallingSupported() {
2924
+ return this.room?.xmpp?.moderator?.isSipGatewayEnabled() ?? false;
2925
+ }
2926
+ /**
2927
+ * Dials a phone number to join the conference.
2928
+ * @param {string} number - The phone number to dial.
2929
+ * @returns {Promise} Resolves when the dial is successful, rejects otherwise.
2930
+ */
2931
+ dial(number) {
2932
+ if (this.room) {
2933
+ return this.room.dial(number);
2934
+ }
2935
+ return Promise.reject(new Error('The conference is not created yet!'));
2936
+ }
2937
+ /**
2938
+ * Hangs up an existing call.
2939
+ * @returns {Promise} Resolves when the hangup is successful.
2940
+ */
2941
+ hangup() {
2942
+ if (this.room) {
2943
+ return this.room.hangup();
2944
+ }
2945
+ return Promise.resolve();
2946
+ }
2947
+ /**
2948
+ * Returns the phone number for joining the conference.
2949
+ * @returns {string|null} The phone number or null if not available.
2950
+ */
2951
+ getPhoneNumber() {
2952
+ if (this.room) {
2953
+ return this.room.getPhoneNumber();
2954
+ }
2955
+ return null;
2956
+ }
2957
+ /**
2958
+ * Returns the PIN for joining the conference via phone.
2959
+ * @returns {string|null} The phone PIN or null if not available.
2960
+ */
2961
+ getPhonePin() {
2962
+ if (this.room) {
2963
+ return this.room.getPhonePin();
2964
+ }
2965
+ return null;
2966
+ }
2967
+ /**
2968
+ * Returns the meeting unique ID if any.
2969
+ * @returns {string|undefined} The meeting ID or undefined if not available.
2970
+ */
2971
+ getMeetingUniqueId() {
2972
+ if (this.room) {
2973
+ return this.room.getMeetingId();
2974
+ }
2975
+ }
2976
+ /**
2977
+ * Returns the active peer connection (P2P or JVB).
2978
+ * @returns {TraceablePeerConnection|null} The active peer connection or null if none is available.
2979
+ * @public
2980
+ */
2981
+ getActivePeerConnection() {
2982
+ const session = this.isP2PActive() ? this.p2pJingleSession : this.jvbJingleSession;
2983
+ return session ? session.peerconnection : null;
2984
+ }
2985
+ /**
2986
+ * Returns the connection state for the current room.
2987
+ * NOTE that "completed" ICE state which can appear on the P2P connection will
2988
+ * be converted to "connected".
2989
+ * @returns {string|null} The ICE connection state or null if no active peer connection exists.
2990
+ */
2991
+ getConnectionState() {
2992
+ const peerConnection = this.getActivePeerConnection();
2993
+ return peerConnection ? peerConnection.getConnectionState() : null;
2994
+ }
2995
+ /**
2996
+ * Sets the start muted policy for new participants.
2997
+ * @param {Object} policy - Object with boolean properties for audio and video muting.
2998
+ * @param {boolean} policy.audio - Whether audio should be muted for new participants.
2999
+ * @param {boolean} policy.video - Whether video should be muted for new participants.
3000
+ * @returns {void}
3001
+ */
3002
+ setStartMutedPolicy(policy) {
3003
+ if (!this.isModerator()) {
3004
+ logger.warn(`Failed to set start muted policy, ${this.room ? '' : 'not in a room, '}${this.isModerator() ? '' : 'participant is not a moderator'}`);
3005
+ return;
3006
+ }
3007
+ logger.info(`Setting start muted policy: ${JSON.stringify(policy)} in presence and in conference metadata`);
3008
+ // TODO: to remove using presence for startmuted policy after old clients update to using metadata always.
3009
+ this.room.addOrReplaceInPresence('startmuted', {
3010
+ attributes: {
3011
+ audio: policy.audio,
3012
+ video: policy.video,
3013
+ xmlns: 'http://jitsi.org/jitmeet/start-muted'
3014
+ }
3015
+ }) && this.room.sendPresence();
3016
+ this.getMetadataHandler().setMetadata('startMuted', {
3017
+ audio: policy.audio,
3018
+ video: policy.video
3019
+ });
3020
+ }
3021
+ /**
3022
+ * Returns the current start muted policy.
3023
+ * @returns {Object} Object with audio and video properties indicating the start muted policy.
3024
+ * @internal
3025
+ */
3026
+ getStartMutedPolicy() {
3027
+ return this.startMutedPolicy;
3028
+ }
3029
+ /**
3030
+ * Returns measured connection times.
3031
+ * @returns {Object} The connection times for the room.
3032
+ */
3033
+ getConnectionTimes() {
3034
+ return this.room.connectionTimes;
3035
+ }
3036
+ /**
3037
+ * Sets a property for the local participant.
3038
+ * @param {string} name - The name of the property.
3039
+ * @param {string} value - The value of the property.
3040
+ * @returns {void}
3041
+ */
3042
+ setLocalParticipantProperty(name, value) {
3043
+ this.sendCommand(`jitsi_participant_${name}`, { value });
3044
+ }
3045
+ /**
3046
+ * Removes a property for the local participant and sends the updated presence.
3047
+ * @param {string} name - The name of the property to remove.
3048
+ * @returns {void}
3049
+ */
3050
+ removeLocalParticipantProperty(name) {
3051
+ this.removeCommand(`jitsi_participant_${name}`);
3052
+ if (this.room) {
3053
+ this.room.sendPresence();
3054
+ }
3055
+ }
3056
+ /**
3057
+ * Sets the transcription language.
3058
+ * NB: Unlike _init_ here we don't check for the default value since we want to allow
3059
+ * the value to be reset.
3060
+ * @param {string} lang - The new transcription language to be used.
3061
+ * @returns {void}
3062
+ */
3063
+ setTranscriptionLanguage(lang) {
3064
+ this.setLocalParticipantProperty('transcription_language', lang);
3065
+ }
3066
+ /**
3067
+ * Gets a local participant property.
3068
+ * @param {string} name - The name of the property to retrieve.
3069
+ * @returns {string|undefined} The value of the property if it exists, otherwise undefined.
3070
+ */
3071
+ getLocalParticipantProperty(name) {
3072
+ const property = this.room.presMap.nodes.find(prop => prop.tagName === `jitsi_participant_${name}`);
3073
+ return property ? property.value : undefined;
3074
+ }
3075
+ /**
3076
+ * Sends feedback if enabled.
3077
+ * @param {number} overallFeedback - An integer between 1 and 5 indicating user feedback.
3078
+ * @param {string} detailedFeedback - Detailed feedback from the user (not yet used).
3079
+ * @returns {Promise} Resolves if feedback is submitted successfully.
3080
+ */
3081
+ sendFeedback(overallFeedback, detailedFeedback) {
3082
+ return this.statistics.sendFeedback(overallFeedback, detailedFeedback);
3083
+ }
3084
+ /**
3085
+ * Finds the SSRC of a given track.
3086
+ * @param {JitsiTrack} track - The track to find the SSRC for.
3087
+ * @returns {Optional<number>} The SSRC of the specified track, or undefined if not found.
3088
+ */
3089
+ getSsrcByTrack(track) {
3090
+ return track.isLocal() ? this.getActivePeerConnection()?.getLocalSSRC(track) : track.getSsrc();
3091
+ }
3092
+ /**
3093
+ * Sends an application log (no-op since callstats is no longer supported).
3094
+ * @returns {void}
3095
+ */
3096
+ sendApplicationLog() {
3097
+ // eslint-disable-next-line no-empty-function
3098
+ }
3099
+ /**
3100
+ * Checks if the user identified by given MUC JID is the conference focus.
3101
+ * @param {string} mucJid - The full MUC address of the user to check.
3102
+ * @returns {boolean|null} True if the user is the conference focus,
3103
+ * false if not, null if not in MUC or invalid JID.
3104
+ * @internal
3105
+ */
3106
+ isFocus(mucJid) {
3107
+ return this.room ? this.room.isFocus(mucJid) : null;
3108
+ }
3109
+ /**
3110
+ * Sends a message via the data channel.
3111
+ * @param {string} to - The ID of the endpoint to receive the message, or empty string to broadcast.
3112
+ * @param {object} payload - The payload of the message.
3113
+ * @throws {NetworkError|InvalidStateError|Error} If the operation fails.
3114
+ * @deprecated Use 'sendMessage' instead. TODO: this should be private.
3115
+ */
3116
+ sendEndpointMessage(to, payload) {
3117
+ this.rtc.sendChannelMessage(to, payload);
3118
+ }
3119
+ /**
3120
+ * Sends local stats via the bridge channel to other endpoints selectively.
3121
+ * @param {Object} payload - The payload of the message.
3122
+ * @throws {NetworkError|InvalidStateError|Error} If the operation fails or no data channel exists.
3123
+ * @internal
3124
+ */
3125
+ sendEndpointStatsMessage(payload) {
3126
+ this.rtc.sendEndpointStatsMessage(payload);
3127
+ }
3128
+ /**
3129
+ * Sends a broadcast message via the data channel.
3130
+ * @param {object} payload - The payload of the message.
3131
+ * @throws {NetworkError|InvalidStateError|Error} If the operation fails.
3132
+ * @deprecated Use 'sendMessage' instead. TODO: this should be private.
3133
+ */
3134
+ broadcastEndpointMessage(payload) {
3135
+ this.sendEndpointMessage('', payload);
3136
+ }
3137
+ /**
3138
+ * Sends a message to a given endpoint or broadcasts it to all endpoints.
3139
+ * @param {string|object} message - The message to send (string for chat, object for JSON).
3140
+ * @param {string} [to=''] - The ID of the recipient endpoint, or empty string to broadcast.
3141
+ * @param {boolean} [sendThroughVideobridge=false] - Whether to send through jitsi-videobridge.
3142
+ * @param {string} [replyToId] - The ID of the message being replied to.
3143
+ */
3144
+ sendMessage(message, to = '', sendThroughVideobridge = false, replyToId) {
3145
+ const messageType = typeof message;
3146
+ // Through videobridge we support only objects. Through XMPP we support
3147
+ // objects (encapsulated in a specific JSON format) and strings (i.e.
3148
+ // regular chat messages).
3149
+ if (messageType !== 'object'
3150
+ && (sendThroughVideobridge || messageType !== 'string')) {
3151
+ logger.error(`Can not send a message of type ${messageType}`);
3152
+ return;
3153
+ }
3154
+ if (sendThroughVideobridge) {
3155
+ this.sendEndpointMessage(to, message);
3156
+ }
3157
+ else {
3158
+ let messageToSend = message;
3159
+ // Name of packet extension of message stanza to send the required
3160
+ // message in.
3161
+ let elementName = 'body';
3162
+ if (messageType === 'object') {
3163
+ elementName = 'json-message';
3164
+ // Mark as valid JSON message if not already
3165
+ if (!messageToSend.hasOwnProperty(JITSI_MEET_MUC_TYPE)) {
3166
+ messageToSend[JITSI_MEET_MUC_TYPE] = '';
3167
+ }
3168
+ try {
3169
+ messageToSend = JSON.stringify(messageToSend);
3170
+ }
3171
+ catch (e) {
3172
+ logger.error('Can not send a message, stringify failed: ', e);
3173
+ return;
3174
+ }
3175
+ }
3176
+ if (to) {
3177
+ this.sendPrivateTextMessage(to, messageToSend, elementName, false, replyToId);
3178
+ }
3179
+ else {
3180
+ // Broadcast
3181
+ this.sendTextMessage(messageToSend, elementName, replyToId);
3182
+ }
3183
+ }
3184
+ }
3185
+ /**
3186
+ * Checks if the connection is interrupted.
3187
+ * @returns {boolean} True if the connection is interrupted, false otherwise.
3188
+ */
3189
+ isConnectionInterrupted() {
3190
+ return this.isP2PActive()
3191
+ ? this.isP2PConnectionInterrupted : this.isJvbConnectionInterrupted;
3192
+ }
3193
+ /**
3194
+ * Gets a conference property with a given key.
3195
+ *
3196
+ * @param {string} key - The key.
3197
+ * @returns {*} The value
3198
+ */
3199
+ getProperty(key) {
3200
+ return this.properties[key];
3201
+ }
3202
+ /**
3203
+ * Checks whether or not the conference is currently in the peer to peer mode.
3204
+ * Being in peer to peer mode means that the direct connection has been
3205
+ * established and the P2P connection is being used for media transmission.
3206
+ * @return {boolean} <tt>true</tt> if in P2P mode or <tt>false</tt> otherwise.
3207
+ */
3208
+ isP2PActive() {
3209
+ return this.p2p;
3210
+ }
3211
+ /**
3212
+ * Returns the current ICE state of the P2P connection.
3213
+ * NOTE: method is used by the jitsi-meet-torture tests.
3214
+ * @return {string|null} an ICE state or <tt>null</tt> if there's currently
3215
+ * no P2P connection.
3216
+ */
3217
+ getP2PConnectionState() {
3218
+ if (this.isP2PActive()) {
3219
+ return this.p2pJingleSession.peerconnection.getConnectionState();
3220
+ }
3221
+ return null;
3222
+ }
3223
+ /**
3224
+ * Configures the peerconnection so that a given framre rate can be achieved for desktop share.
3225
+ *
3226
+ * @param {number} maxFps The capture framerate to be used for desktop tracks.
3227
+ * @returns {boolean} true if the operation is successful, false otherwise.
3228
+ */
3229
+ setDesktopSharingFrameRate(maxFps) {
3230
+ if (!isValidNumber(maxFps)) {
3231
+ logger.error(`Invalid value ${maxFps} specified for desktop capture frame rate`);
3232
+ return false;
3233
+ }
3234
+ const fps = Number(maxFps);
3235
+ this._desktopSharingFrameRate = fps;
3236
+ // Set capture fps for screenshare.
3237
+ this.jvbJingleSession?.peerconnection.setDesktopSharingFrameRate(fps);
3238
+ // Set the capture rate for desktop sharing.
3239
+ this.rtc.setDesktopSharingFrameRate(fps);
3240
+ return true;
3241
+ }
3242
+ /**
3243
+ * Manually starts new P2P session (should be used only in the tests).
3244
+ * @returns {void}
3245
+ * @internal
3246
+ */
3247
+ startP2PSession() {
3248
+ const peers = this.getParticipants();
3249
+ // Start peer to peer session
3250
+ if (peers.length === 1) {
3251
+ const peerJid = peers[0].getJid();
3252
+ this._startP2PSession(peerJid);
3253
+ }
3254
+ else {
3255
+ throw new Error('There must be exactly 1 participant to start the P2P session !');
3256
+ }
3257
+ }
3258
+ /**
3259
+ * Manually stops the current P2P session (should be used only in the tests).
3260
+ * @param {Object} options - Options for stopping P2P.
3261
+ * @returns {void}
3262
+ * @internal
3263
+ */
3264
+ stopP2PSession(options) {
3265
+ this._stopP2PSession(options);
3266
+ }
3267
+ /**
3268
+ * Get a summary of how long current participants have been the dominant speaker
3269
+ * @returns {{[userId: string]: SpeakerStats}} The speaker statistics.
3270
+ */
3271
+ getSpeakerStats() {
3272
+ return this.speakerStatsCollector.getStats();
3273
+ }
3274
+ /**
3275
+ * Sends a face landmarks object to the xmpp server.
3276
+ * @param {IFaceLandmarksPayload} payload - The face landmarks data to send.
3277
+ * @returns {void}
3278
+ */
3279
+ sendFaceLandmarks(payload) {
3280
+ if (payload.faceExpression) {
3281
+ this._xmpp.sendFaceLandmarksEvent(this.room.roomjid, payload);
3282
+ }
3283
+ }
3284
+ /**
3285
+ * Sets the constraints for the video that is requested from the bridge.
3286
+ *
3287
+ * @param {IReceiverVideoConstraints} videoConstraints The constraints which are specified in the following format. The message updates
3288
+ * the fields that are present and leaves the rest unchanged on the bridge.
3289
+ * Therefore, any field that is not applicable
3290
+ * anymore should be cleared by passing an empty object or list (whatever is applicable).
3291
+ * {
3292
+ * 'lastN': 20,
3293
+ * 'selectedSources': ['A', 'B', 'C'],
3294
+ * 'onStageSources': ['A'],
3295
+ * 'defaultConstraints': { 'maxHeight': 180 },
3296
+ * 'constraints': {
3297
+ * 'A': { 'maxHeight': 720 }
3298
+ * }
3299
+ * }
3300
+ * Where A, B and C are source-names of the remote tracks that are being requested from the bridge.
3301
+ * @returns {void}
3302
+ */
3303
+ setReceiverConstraints(videoConstraints) {
3304
+ this.qualityController.receiveVideoController.setReceiverConstraints(videoConstraints);
3305
+ }
3306
+ /**
3307
+ * Sets the assumed bandwidth bps for the video that is requested from the bridge.
3308
+ *
3309
+ * @param {Number} assumedBandwidthBps - The bandwidth value expressed in bits per second.
3310
+ * @returns {void}
3311
+ */
3312
+ setAssumedBandwidthBps(assumedBandwidthBps) {
3313
+ this.qualityController.receiveVideoController.setAssumedBandwidthBps(assumedBandwidthBps);
3314
+ }
3315
+ /**
3316
+ * Sets the maximum video size the local participant should receive from remote
3317
+ * participants.
3318
+ *
3319
+ * @param {number} maxFrameHeight - the maximum frame height, in pixels,
3320
+ * this receiver is willing to receive.
3321
+ * @returns {void}
3322
+ */
3323
+ setReceiverVideoConstraint(maxFrameHeight) {
3324
+ this.qualityController.receiveVideoController.setPreferredReceiveMaxFrameHeight(maxFrameHeight);
3325
+ }
3326
+ /**
3327
+ * Sets the maximum video size the local participant should send to remote
3328
+ * participants.
3329
+ * @param {number} maxFrameHeight - The user preferred max frame height.
3330
+ * @returns {Promise} promise that will be resolved when the operation is
3331
+ * successful and rejected otherwise.
3332
+ */
3333
+ setSenderVideoConstraint(maxFrameHeight) {
3334
+ return this.qualityController.sendVideoController.setPreferredSendMaxFrameHeight(maxFrameHeight);
3335
+ }
3336
+ /**
3337
+ * Creates a video SIP GW session and returns it if service is enabled. Before
3338
+ * creating a session one need to check whether video SIP GW service is
3339
+ * available in the system. Even
3340
+ * if there are available nodes to serve this request, after creating the
3341
+ * session those nodes can be taken and the request about using the
3342
+ * created session can fail.
3343
+ *
3344
+ * @param {string} sipAddress - The sip address to be used.
3345
+ * @param {string} displayName - The display name to be used for this session.
3346
+ * @returns {JitsiVideoSIPGWSession|Error} Returns null if conference is not
3347
+ * initialised and there is no room.
3348
+ */
3349
+ createVideoSIPGWSession(sipAddress, displayName) {
3350
+ if (!this.room) {
3351
+ return new Error(VideoSIPGWConstants.ERROR_NO_CONNECTION);
3352
+ }
3353
+ return this.videoSIPGWHandler
3354
+ .createVideoSIPGWSession(sipAddress, displayName);
3355
+ }
3356
+ /**
3357
+ * Returns whether End-To-End encryption is enabled.
3358
+ *
3359
+ * @returns {boolean}
3360
+ */
3361
+ isE2EEEnabled() {
3362
+ return Boolean(this._e2eEncryption?.isEnabled());
3363
+ }
3364
+ /**
3365
+ * Returns whether End-To-End encryption is supported. Note that not all participants
3366
+ * in the conference may support it.
3367
+ *
3368
+ * @returns {boolean}
3369
+ */
3370
+ isE2EESupported() {
3371
+ return E2EEncryption.isSupported(this.options.config);
3372
+ }
3373
+ /**
3374
+ * Enables / disables End-to-End encryption.
3375
+ *
3376
+ * @param {boolean} enabled whether to enable E2EE or not.
3377
+ * @returns {void}
3378
+ */
3379
+ toggleE2EE(enabled) {
3380
+ if (!this.isE2EESupported()) {
3381
+ logger.warn('Cannot enable / disable E2EE: platform is not supported.');
3382
+ return;
3383
+ }
3384
+ this._e2eEncryption.setEnabled(enabled);
3385
+ }
3386
+ /**
3387
+ * Sets the key and index for End-to-End encryption.
3388
+ *
3389
+ * @param {CryptoKey} [keyInfo.encryptionKey] - encryption key.
3390
+ * @param {Number} [keyInfo.index] - the index of the encryption key.
3391
+ * @returns {void}
3392
+ */
3393
+ setMediaEncryptionKey(keyInfo) {
3394
+ this._e2eEncryption.setEncryptionKey(keyInfo);
3395
+ }
3396
+ /**
3397
+ * Starts the participant verification process.
3398
+ *
3399
+ * @param {string} participantId The participant which will be marked as verified.
3400
+ * @returns {void}
3401
+ */
3402
+ startVerification(participantId) {
3403
+ const participant = this.getParticipantById(participantId);
3404
+ if (!participant) {
3405
+ return;
3406
+ }
3407
+ this._e2eEncryption.startVerification(participant);
3408
+ }
3409
+ /**
3410
+ * Marks the given participant as verified. After this is done, MAC verification will
3411
+ * be performed and an event will be emitted with the result.
3412
+ *
3413
+ * @param {string} participantId The participant which will be marked as verified.
3414
+ * @param {boolean} isVerified - whether the verification was succesfull.
3415
+ * @returns {void}
3416
+ */
3417
+ markParticipantVerified(participantId, isVerified) {
3418
+ const participant = this.getParticipantById(participantId);
3419
+ if (!participant) {
3420
+ return;
3421
+ }
3422
+ this._e2eEncryption.markParticipantVerified(participant, isVerified);
3423
+ }
3424
+ /**
3425
+ * Returns <tt>true</tt> if lobby support is enabled in the backend.
3426
+ *
3427
+ * @returns {boolean} whether lobby is supported in the backend.
3428
+ */
3429
+ isLobbySupported() {
3430
+ return Boolean(this.room?.getLobby().isSupported());
3431
+ }
3432
+ /**
3433
+ * Returns <tt>true</tt> if the room has members only enabled.
3434
+ *
3435
+ * @returns {boolean} whether conference room is members only.
3436
+ */
3437
+ isMembersOnly() {
3438
+ return Boolean(this.room?.membersOnlyEnabled);
3439
+ }
3440
+ /**
3441
+ * Returns <tt>true</tt> if the room supports visitors feature.
3442
+ *
3443
+ * @returns {boolean} whether conference room has visitors support.
3444
+ */
3445
+ isVisitorsSupported() {
3446
+ return Boolean(this.room?.visitorsSupported);
3447
+ }
3448
+ /**
3449
+ * Enables lobby by moderators
3450
+ *
3451
+ * @returns {Promise} resolves when lobby room is joined or rejects with the error.
3452
+ */
3453
+ enableLobby() {
3454
+ if (this.room && this.isModerator()) {
3455
+ return this.room.getLobby().enable();
3456
+ }
3457
+ return Promise.reject(new Error('The conference not started or user is not moderator'));
3458
+ }
3459
+ /**
3460
+ * Disabled lobby by moderators
3461
+ *
3462
+ * @returns {void}
3463
+ */
3464
+ disableLobby() {
3465
+ if (this.room && this.isModerator()) {
3466
+ this.room.getLobby().disable();
3467
+ }
3468
+ else {
3469
+ logger.warn(`Failed to disable lobby, ${this.room ? '' : 'not in a room, '}${this.isModerator() ? '' : 'participant is not a moderator'}`);
3470
+ }
3471
+ }
3472
+ /**
3473
+ * Joins the lobby room with display name and optional email or with a shared password to skip waiting.
3474
+ *
3475
+ * @param {string} displayName Display name should be set to show it to moderators.
3476
+ * @param {string} email Optional email is used to present avatar to the moderator.
3477
+ * @returns {Promise<never>}
3478
+ */
3479
+ joinLobby(displayName, email) {
3480
+ if (this.room) {
3481
+ return this.room.getLobby().join(displayName, email);
3482
+ }
3483
+ return Promise.reject(new Error('The conference not started'));
3484
+ }
3485
+ /**
3486
+ * Gets the local id for a participant in a lobby room.
3487
+ * Returns undefined when current participant is not in the lobby room.
3488
+ * This is used for lobby room private chat messages.
3489
+ *
3490
+ * @returns {string}
3491
+ */
3492
+ myLobbyUserId() {
3493
+ if (this.room) {
3494
+ return this.room.getLobby().getLocalId();
3495
+ }
3496
+ }
3497
+ /**
3498
+ * Sends a message to a lobby room.
3499
+ * When id is specified it sends a private message.
3500
+ * Otherwise it sends the message to all moderators.
3501
+ * @param {object} message The message to send
3502
+ * @param {string} id The participant id.
3503
+ *
3504
+ * @returns {void}
3505
+ */
3506
+ sendLobbyMessage(message, id) {
3507
+ if (this.room) {
3508
+ if (id) {
3509
+ return this.room.getLobby().sendPrivateMessage(id, message);
3510
+ }
3511
+ return this.room.getLobby().sendMessage(message);
3512
+ }
3513
+ }
3514
+ /**
3515
+ * Adds a message listener to the lobby room
3516
+ * @param {Function} listener The listener function,
3517
+ * called when a new message is received in the lobby room.
3518
+ *
3519
+ * @returns {Function} Handler returned to be able to remove it later.
3520
+ */
3521
+ addLobbyMessageListener(listener) {
3522
+ if (this.room) {
3523
+ return this.room.getLobby().addMessageListener(listener);
3524
+ }
3525
+ }
3526
+ /**
3527
+ * Removes a message handler from the lobby room
3528
+ * @param {Function} handler The handler function to remove.
3529
+ *
3530
+ * @returns {void}
3531
+ */
3532
+ removeLobbyMessageHandler(handler) {
3533
+ if (this.room) {
3534
+ return this.room.getLobby().removeMessageHandler(handler);
3535
+ }
3536
+ }
3537
+ /**
3538
+ * Denies an occupant in the lobby room access to the conference.
3539
+ * @param {string} id The participant id.
3540
+ * @returns {void}
3541
+ */
3542
+ lobbyDenyAccess(id) {
3543
+ if (this.room) {
3544
+ this.room.getLobby().denyAccess(id);
3545
+ }
3546
+ }
3547
+ /**
3548
+ * Approves the request to join the conference to a participant waiting in the lobby.
3549
+ *
3550
+ * @param {string|Array<string>} param The participant id or an array of ids.
3551
+ * @returns {void}
3552
+ */
3553
+ lobbyApproveAccess(param) {
3554
+ if (this.room) {
3555
+ this.room.getLobby().approveAccess(param);
3556
+ }
3557
+ }
3558
+ /**
3559
+ * Returns <tt>true</tt> if AV Moderation support is enabled in the backend.
3560
+ *
3561
+ * @returns {boolean} whether AV Moderation is supported in the backend.
3562
+ */
3563
+ isAVModerationSupported() {
3564
+ return Boolean(this.room?.getAVModeration().isSupported());
3565
+ }
3566
+ /**
3567
+ * Enables AV Moderation.
3568
+ * @param {MediaType} mediaType "audio", "desktop" or "video"
3569
+ * @returns {void}
3570
+ */
3571
+ enableAVModeration(mediaType) {
3572
+ if (this.room && this.isModerator()
3573
+ && (mediaType === MediaType.AUDIO || mediaType === MediaType.DESKTOP || mediaType === MediaType.VIDEO)) {
3574
+ this.room.getAVModeration().enable(true, mediaType);
3575
+ }
3576
+ else {
3577
+ logger.warn(`Failed to enable AV moderation, ${this.room ? '' : 'not in a room, '}${this.isModerator() ? '' : 'participant is not a moderator, '}${this.room && this.isModerator() ? 'wrong media type passed' : ''}`);
3578
+ }
3579
+ }
3580
+ /**
3581
+ * Disables AV Moderation.
3582
+ * @param {MediaType} mediaType "audio", "desktop" or "video"
3583
+ * @returns {void}
3584
+ */
3585
+ disableAVModeration(mediaType) {
3586
+ if (this.room && this.isModerator()
3587
+ && (mediaType === MediaType.AUDIO || mediaType === MediaType.DESKTOP || mediaType === MediaType.VIDEO)) {
3588
+ this.room.getAVModeration().enable(false, mediaType);
3589
+ }
3590
+ else {
3591
+ logger.warn(`Failed to disable AV moderation, ${this.room ? '' : 'not in a room, '}${this.isModerator() ? '' : 'participant is not a moderator, '}${this.room && this.isModerator() ? 'wrong media type passed' : ''}`);
3592
+ }
3593
+ }
3594
+ /**
3595
+ * Approve participant access to certain media, allows unmuting audio or video.
3596
+ *
3597
+ * @param {MediaType} mediaType "audio", "desktop" or "video"
3598
+ * @param id the id of the participant.
3599
+ * @returns {void}
3600
+ */
3601
+ avModerationApprove(mediaType, id) {
3602
+ if (this.room && this.isModerator()
3603
+ && (mediaType === MediaType.AUDIO || mediaType === MediaType.DESKTOP || mediaType === MediaType.VIDEO)) {
3604
+ const participant = this.getParticipantById(id);
3605
+ if (!participant) {
3606
+ return;
3607
+ }
3608
+ this.room.getAVModeration().approve(mediaType, participant.getJid());
3609
+ }
3610
+ else {
3611
+ logger.warn(`AV moderation approve skipped , ${this.room ? '' : 'not in a room, '}${this.isModerator() ? '' : 'participant is not a moderator, '}${this.room && this.isModerator() ? 'wrong media type passed' : ''}`);
3612
+ }
3613
+ }
3614
+ /**
3615
+ * Reject participant access to certain media, blocks unmuting audio or video.
3616
+ *
3617
+ * @param {MediaType} mediaType "audio", "desktop" or "video"
3618
+ * @param id the id of the participant.
3619
+ * @returns {void}
3620
+ */
3621
+ avModerationReject(mediaType, id) {
3622
+ if (this.room && this.isModerator()
3623
+ && (mediaType === MediaType.AUDIO || mediaType === MediaType.DESKTOP || mediaType === MediaType.VIDEO)) {
3624
+ const participant = this.getParticipantById(id);
3625
+ if (!participant) {
3626
+ return;
3627
+ }
3628
+ this.room.getAVModeration().reject(mediaType, participant.getJid());
3629
+ }
3630
+ else {
3631
+ logger.warn(`AV moderation reject skipped , ${this.room ? '' : 'not in a room, '}${this.isModerator() ? '' : 'participant is not a moderator, '}${this.room && this.isModerator() ? 'wrong media type passed' : ''}`);
3632
+ }
3633
+ }
3634
+ /**
3635
+ * Returns the breakout rooms manager object.
3636
+ *
3637
+ * @returns {Optional<BreakoutRooms>} the breakout rooms manager.
3638
+ */
3639
+ getBreakoutRooms() {
3640
+ return this.room?.getBreakoutRooms();
3641
+ }
3642
+ /**
3643
+ * Returns the file sharing manager object.
3644
+ *
3645
+ * @returns {Optional<FileSharing>} the file sharing manager.
3646
+ */
3647
+ getFileSharing() {
3648
+ return this.room?.getFileSharing();
3649
+ }
3650
+ /**
3651
+ * Returns the metadata handler object.
3652
+ *
3653
+ * @returns {Optional<RoomMetadata>} the room metadata handler.
3654
+ */
3655
+ getMetadataHandler() {
3656
+ return this.room?.getMetadataHandler();
3657
+ }
3658
+ /**
3659
+ * Returns the polls object.
3660
+ *
3661
+ * @returns {Optional<Polls>} the polls.
3662
+ */
3663
+ getPolls() {
3664
+ return this.room?.getPolls();
3665
+ }
3666
+ /**
3667
+ * Requests short-term credentials from the backend if available.
3668
+ * @param {string} service - The service for which to request the credentials.
3669
+ * @returns {Promise} A promise that resolves with the credentials or rejects with an error.
3670
+ */
3671
+ getShortTermCredentials(service) {
3672
+ if (this.room) {
3673
+ return this.room.getShortTermCredentials(service);
3674
+ }
3675
+ return Promise.reject(new Error('The conference is not created yet!'));
3676
+ }
3677
+ /**
3678
+ * @internal
3679
+ * @returns {Optional<VADAudioAnalyser>} the audio analyser.
3680
+ */
3681
+ getAudioAnalyser() {
3682
+ return this?._audioAnalyser;
3683
+ }
3684
+ /**
3685
+ * @internal
3686
+ * @returns {XMPP} the XMPP connection object.
3687
+ */
3688
+ get xmpp() {
3689
+ return this._xmpp;
3690
+ }
3691
+ }
3692
+ //# sourceMappingURL=JitsiConference.js.map