@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.
- package/LICENSE +202 -0
- package/README.md +26 -0
- package/dist/esm/JitsiConference.js +3692 -0
- package/dist/esm/JitsiConference.js.map +1 -0
- package/dist/esm/JitsiConferenceErrors.js +126 -0
- package/dist/esm/JitsiConferenceErrors.js.map +1 -0
- package/dist/esm/JitsiConferenceEventManager.js +424 -0
- package/dist/esm/JitsiConferenceEventManager.js.map +1 -0
- package/dist/esm/JitsiConferenceEvents.js +431 -0
- package/dist/esm/JitsiConferenceEvents.js.map +1 -0
- package/dist/esm/JitsiConnection.js +187 -0
- package/dist/esm/JitsiConnection.js.map +1 -0
- package/dist/esm/JitsiConnectionErrors.js +52 -0
- package/dist/esm/JitsiConnectionErrors.js.map +1 -0
- package/dist/esm/JitsiConnectionEvents.js +57 -0
- package/dist/esm/JitsiConnectionEvents.js.map +1 -0
- package/dist/esm/JitsiMediaDevices.js +221 -0
- package/dist/esm/JitsiMediaDevices.js.map +1 -0
- package/dist/esm/JitsiMediaDevicesEvents.js +29 -0
- package/dist/esm/JitsiMediaDevicesEvents.js.map +1 -0
- package/dist/esm/JitsiMeetJS.js +499 -0
- package/dist/esm/JitsiMeetJS.js.map +1 -0
- package/dist/esm/JitsiParticipant.js +323 -0
- package/dist/esm/JitsiParticipant.js.map +1 -0
- package/dist/esm/JitsiTrackError.js +122 -0
- package/dist/esm/JitsiTrackError.js.map +1 -0
- package/dist/esm/JitsiTrackErrors.js +91 -0
- package/dist/esm/JitsiTrackErrors.js.map +1 -0
- package/dist/esm/JitsiTrackEvents.js +60 -0
- package/dist/esm/JitsiTrackEvents.js.map +1 -0
- package/dist/esm/JitsiTranscriptionStatus.js +15 -0
- package/dist/esm/JitsiTranscriptionStatus.js.map +1 -0
- package/dist/esm/modules/RTC/BridgeChannel.js +398 -0
- package/dist/esm/modules/RTC/BridgeChannel.js.map +1 -0
- package/dist/esm/modules/RTC/JitsiLocalTrack.js +896 -0
- package/dist/esm/modules/RTC/JitsiLocalTrack.js.map +1 -0
- package/dist/esm/modules/RTC/JitsiRemoteTrack.js +427 -0
- package/dist/esm/modules/RTC/JitsiRemoteTrack.js.map +1 -0
- package/dist/esm/modules/RTC/JitsiTrack.js +453 -0
- package/dist/esm/modules/RTC/JitsiTrack.js.map +1 -0
- package/dist/esm/modules/RTC/MockClasses.js +388 -0
- package/dist/esm/modules/RTC/MockClasses.js.map +1 -0
- package/dist/esm/modules/RTC/RTC.js +658 -0
- package/dist/esm/modules/RTC/RTC.js.map +1 -0
- package/dist/esm/modules/RTC/RTCUtils.js +762 -0
- package/dist/esm/modules/RTC/RTCUtils.js.map +1 -0
- package/dist/esm/modules/RTC/ScreenObtainer.js +380 -0
- package/dist/esm/modules/RTC/ScreenObtainer.js.map +1 -0
- package/dist/esm/modules/RTC/TPCUtils.js +803 -0
- package/dist/esm/modules/RTC/TPCUtils.js.map +1 -0
- package/dist/esm/modules/RTC/TraceablePeerConnection.js +2223 -0
- package/dist/esm/modules/RTC/TraceablePeerConnection.js.map +1 -0
- package/dist/esm/modules/RTCStats/DefaulLogStorage.js +35 -0
- package/dist/esm/modules/RTCStats/DefaulLogStorage.js.map +1 -0
- package/dist/esm/modules/RTCStats/RTCStats.js +219 -0
- package/dist/esm/modules/RTCStats/RTCStats.js.map +1 -0
- package/dist/esm/modules/RTCStats/RTCStatsEvents.js +92 -0
- package/dist/esm/modules/RTCStats/RTCStatsEvents.js.map +1 -0
- package/dist/esm/modules/RTCStats/interfaces.js +2 -0
- package/dist/esm/modules/RTCStats/interfaces.js.map +1 -0
- package/dist/esm/modules/browser/BrowserCapabilities.js +345 -0
- package/dist/esm/modules/browser/BrowserCapabilities.js.map +1 -0
- package/dist/esm/modules/browser/index.js +3 -0
- package/dist/esm/modules/browser/index.js.map +1 -0
- package/dist/esm/modules/connectivity/ConnectionQuality.js +389 -0
- package/dist/esm/modules/connectivity/ConnectionQuality.js.map +1 -0
- package/dist/esm/modules/connectivity/IceFailedHandling.js +84 -0
- package/dist/esm/modules/connectivity/IceFailedHandling.js.map +1 -0
- package/dist/esm/modules/connectivity/NetworkInfo.js +49 -0
- package/dist/esm/modules/connectivity/NetworkInfo.js.map +1 -0
- package/dist/esm/modules/connectivity/TrackStreamingStatus.js +453 -0
- package/dist/esm/modules/connectivity/TrackStreamingStatus.js.map +1 -0
- package/dist/esm/modules/detection/ActiveDeviceDetector.js +79 -0
- package/dist/esm/modules/detection/ActiveDeviceDetector.js.map +1 -0
- package/dist/esm/modules/detection/DetectionEvents.js +58 -0
- package/dist/esm/modules/detection/DetectionEvents.js.map +1 -0
- package/dist/esm/modules/detection/NoAudioSignalDetection.js +127 -0
- package/dist/esm/modules/detection/NoAudioSignalDetection.js.map +1 -0
- package/dist/esm/modules/detection/P2PDominantSpeakerDetection.js +47 -0
- package/dist/esm/modules/detection/P2PDominantSpeakerDetection.js.map +1 -0
- package/dist/esm/modules/detection/TrackVADEmitter.js +190 -0
- package/dist/esm/modules/detection/TrackVADEmitter.js.map +1 -0
- package/dist/esm/modules/detection/VADAudioAnalyser.js +199 -0
- package/dist/esm/modules/detection/VADAudioAnalyser.js.map +1 -0
- package/dist/esm/modules/detection/VADNoiseDetection.js +168 -0
- package/dist/esm/modules/detection/VADNoiseDetection.js.map +1 -0
- package/dist/esm/modules/detection/VADReportingService.js +203 -0
- package/dist/esm/modules/detection/VADReportingService.js.map +1 -0
- package/dist/esm/modules/detection/VADTalkMutedDetection.js +131 -0
- package/dist/esm/modules/detection/VADTalkMutedDetection.js.map +1 -0
- package/dist/esm/modules/e2ee/Context.js +274 -0
- package/dist/esm/modules/e2ee/Context.js.map +1 -0
- package/dist/esm/modules/e2ee/E2EEContext.js +158 -0
- package/dist/esm/modules/e2ee/E2EEContext.js.map +1 -0
- package/dist/esm/modules/e2ee/E2EEErrors.js +10 -0
- package/dist/esm/modules/e2ee/E2EEErrors.js.map +1 -0
- package/dist/esm/modules/e2ee/E2EEncryption.js +87 -0
- package/dist/esm/modules/e2ee/E2EEncryption.js.map +1 -0
- package/dist/esm/modules/e2ee/ExternallyManagedKeyHandler.js +24 -0
- package/dist/esm/modules/e2ee/ExternallyManagedKeyHandler.js.map +1 -0
- package/dist/esm/modules/e2ee/KeyHandler.js +137 -0
- package/dist/esm/modules/e2ee/KeyHandler.js.map +1 -0
- package/dist/esm/modules/e2ee/ManagedKeyHandler.js +182 -0
- package/dist/esm/modules/e2ee/ManagedKeyHandler.js.map +1 -0
- package/dist/esm/modules/e2ee/OlmAdapter.js +860 -0
- package/dist/esm/modules/e2ee/OlmAdapter.js.map +1 -0
- package/dist/esm/modules/e2ee/SAS.js +128 -0
- package/dist/esm/modules/e2ee/SAS.js.map +1 -0
- package/dist/esm/modules/e2ee/Worker.js +102 -0
- package/dist/esm/modules/e2ee/Worker.js.map +1 -0
- package/dist/esm/modules/e2ee/crypto-utils.js +53 -0
- package/dist/esm/modules/e2ee/crypto-utils.js.map +1 -0
- package/dist/esm/modules/e2ee/utils.js +15 -0
- package/dist/esm/modules/e2ee/utils.js.map +1 -0
- package/dist/esm/modules/e2eping/e2eping.js +314 -0
- package/dist/esm/modules/e2eping/e2eping.js.map +1 -0
- package/dist/esm/modules/flags/FeatureFlags.js +36 -0
- package/dist/esm/modules/flags/FeatureFlags.js.map +1 -0
- package/dist/esm/modules/litemode/LiteModeContext.js +50 -0
- package/dist/esm/modules/litemode/LiteModeContext.js.map +1 -0
- package/dist/esm/modules/proxyconnection/CustomSignalingLayer.js +98 -0
- package/dist/esm/modules/proxyconnection/CustomSignalingLayer.js.map +1 -0
- package/dist/esm/modules/proxyconnection/ProxyConnectionPC.js +348 -0
- package/dist/esm/modules/proxyconnection/ProxyConnectionPC.js.map +1 -0
- package/dist/esm/modules/proxyconnection/ProxyConnectionService.js +279 -0
- package/dist/esm/modules/proxyconnection/ProxyConnectionService.js.map +1 -0
- package/dist/esm/modules/proxyconnection/constants.js +14 -0
- package/dist/esm/modules/proxyconnection/constants.js.map +1 -0
- package/dist/esm/modules/qualitycontrol/CodecSelection.js +222 -0
- package/dist/esm/modules/qualitycontrol/CodecSelection.js.map +1 -0
- package/dist/esm/modules/qualitycontrol/MockClasses.js +120 -0
- package/dist/esm/modules/qualitycontrol/MockClasses.js.map +1 -0
- package/dist/esm/modules/qualitycontrol/QualityController.js +366 -0
- package/dist/esm/modules/qualitycontrol/QualityController.js.map +1 -0
- package/dist/esm/modules/qualitycontrol/ReceiveAudioController.js +73 -0
- package/dist/esm/modules/qualitycontrol/ReceiveAudioController.js.map +1 -0
- package/dist/esm/modules/qualitycontrol/ReceiveVideoController.js +216 -0
- package/dist/esm/modules/qualitycontrol/ReceiveVideoController.js.map +1 -0
- package/dist/esm/modules/qualitycontrol/SendVideoController.js +133 -0
- package/dist/esm/modules/qualitycontrol/SendVideoController.js.map +1 -0
- package/dist/esm/modules/recording/JibriSession.js +279 -0
- package/dist/esm/modules/recording/JibriSession.js.map +1 -0
- package/dist/esm/modules/recording/RecordingManager.js +257 -0
- package/dist/esm/modules/recording/RecordingManager.js.map +1 -0
- package/dist/esm/modules/recording/recordingConstants.js +21 -0
- package/dist/esm/modules/recording/recordingConstants.js.map +1 -0
- package/dist/esm/modules/recording/recordingXMLUtils.js +69 -0
- package/dist/esm/modules/recording/recordingXMLUtils.js.map +1 -0
- package/dist/esm/modules/red/red.js +108 -0
- package/dist/esm/modules/red/red.js.map +1 -0
- package/dist/esm/modules/sdp/LocalSdpMunger.js +143 -0
- package/dist/esm/modules/sdp/LocalSdpMunger.js.map +1 -0
- package/dist/esm/modules/sdp/RtxModifier.js +179 -0
- package/dist/esm/modules/sdp/RtxModifier.js.map +1 -0
- package/dist/esm/modules/sdp/SDP.js +848 -0
- package/dist/esm/modules/sdp/SDP.js.map +1 -0
- package/dist/esm/modules/sdp/SDPDiffer.js +96 -0
- package/dist/esm/modules/sdp/SDPDiffer.js.map +1 -0
- package/dist/esm/modules/sdp/SDPUtil.js +798 -0
- package/dist/esm/modules/sdp/SDPUtil.js.map +1 -0
- package/dist/esm/modules/sdp/SampleSdpStrings.js +589 -0
- package/dist/esm/modules/sdp/SampleSdpStrings.js.map +1 -0
- package/dist/esm/modules/sdp/SdpSimulcast.js +196 -0
- package/dist/esm/modules/sdp/SdpSimulcast.js.map +1 -0
- package/dist/esm/modules/sdp/SdpTransformUtil.js +337 -0
- package/dist/esm/modules/sdp/SdpTransformUtil.js.map +1 -0
- package/dist/esm/modules/sdp/constants.js +2 -0
- package/dist/esm/modules/sdp/constants.js.map +1 -0
- package/dist/esm/modules/settings/Settings.js +95 -0
- package/dist/esm/modules/settings/Settings.js.map +1 -0
- package/dist/esm/modules/statistics/AnalyticsAdapter.js +277 -0
- package/dist/esm/modules/statistics/AnalyticsAdapter.js.map +1 -0
- package/dist/esm/modules/statistics/AvgRTPStatsReporter.js +817 -0
- package/dist/esm/modules/statistics/AvgRTPStatsReporter.js.map +1 -0
- package/dist/esm/modules/statistics/LocalStatsCollector.js +149 -0
- package/dist/esm/modules/statistics/LocalStatsCollector.js.map +1 -0
- package/dist/esm/modules/statistics/PreCallTest.js +15 -0
- package/dist/esm/modules/statistics/PreCallTest.js.map +1 -0
- package/dist/esm/modules/statistics/RTPStatsCollector.js +601 -0
- package/dist/esm/modules/statistics/RTPStatsCollector.js.map +1 -0
- package/dist/esm/modules/statistics/SpeakerStats.js +163 -0
- package/dist/esm/modules/statistics/SpeakerStats.js.map +1 -0
- package/dist/esm/modules/statistics/SpeakerStatsCollector.js +161 -0
- package/dist/esm/modules/statistics/SpeakerStatsCollector.js.map +1 -0
- package/dist/esm/modules/statistics/constants.js +7 -0
- package/dist/esm/modules/statistics/constants.js.map +1 -0
- package/dist/esm/modules/statistics/statistics.js +362 -0
- package/dist/esm/modules/statistics/statistics.js.map +1 -0
- package/dist/esm/modules/util/AsyncQueue.js +102 -0
- package/dist/esm/modules/util/AsyncQueue.js.map +1 -0
- package/dist/esm/modules/util/Deferred.js +41 -0
- package/dist/esm/modules/util/Deferred.js.map +1 -0
- package/dist/esm/modules/util/EventEmitter.js +17 -0
- package/dist/esm/modules/util/EventEmitter.js.map +1 -0
- package/dist/esm/modules/util/EventEmitterForwarder.js +52 -0
- package/dist/esm/modules/util/EventEmitterForwarder.js.map +1 -0
- package/dist/esm/modules/util/Listenable.js +106 -0
- package/dist/esm/modules/util/Listenable.js.map +1 -0
- package/dist/esm/modules/util/MathUtil.js +103 -0
- package/dist/esm/modules/util/MathUtil.js.map +1 -0
- package/dist/esm/modules/util/RandomUtil.js +66 -0
- package/dist/esm/modules/util/RandomUtil.js.map +1 -0
- package/dist/esm/modules/util/Retry.js +15 -0
- package/dist/esm/modules/util/Retry.js.map +1 -0
- package/dist/esm/modules/util/ScriptUtil.js +58 -0
- package/dist/esm/modules/util/ScriptUtil.js.map +1 -0
- package/dist/esm/modules/util/StringUtils.js +21 -0
- package/dist/esm/modules/util/StringUtils.js.map +1 -0
- package/dist/esm/modules/util/TestUtils.js +14 -0
- package/dist/esm/modules/util/TestUtils.js.map +1 -0
- package/dist/esm/modules/util/UsernameGenerator.js +436 -0
- package/dist/esm/modules/util/UsernameGenerator.js.map +1 -0
- package/dist/esm/modules/util/XMLUtils.js +135 -0
- package/dist/esm/modules/util/XMLUtils.js.map +1 -0
- package/dist/esm/modules/version/ComponentsVersions.js +52 -0
- package/dist/esm/modules/version/ComponentsVersions.js.map +1 -0
- package/dist/esm/modules/videosipgw/JitsiVideoSIPGWSession.js +137 -0
- package/dist/esm/modules/videosipgw/JitsiVideoSIPGWSession.js.map +1 -0
- package/dist/esm/modules/videosipgw/VideoSIPGW.js +102 -0
- package/dist/esm/modules/videosipgw/VideoSIPGW.js.map +1 -0
- package/dist/esm/modules/videosipgw/VideoSIPGWConstants.js +65 -0
- package/dist/esm/modules/videosipgw/VideoSIPGWConstants.js.map +1 -0
- package/dist/esm/modules/watchRTC/WatchRTC.js +69 -0
- package/dist/esm/modules/watchRTC/WatchRTC.js.map +1 -0
- package/dist/esm/modules/watchRTC/functions.js +31 -0
- package/dist/esm/modules/watchRTC/functions.js.map +1 -0
- package/dist/esm/modules/watchRTC/interfaces.js +2 -0
- package/dist/esm/modules/watchRTC/interfaces.js.map +1 -0
- package/dist/esm/modules/webaudio/AudioMixer.js +74 -0
- package/dist/esm/modules/webaudio/AudioMixer.js.map +1 -0
- package/dist/esm/modules/webaudio/WebAudioUtils.js +13 -0
- package/dist/esm/modules/webaudio/WebAudioUtils.js.map +1 -0
- package/dist/esm/modules/xmpp/AVModeration.js +156 -0
- package/dist/esm/modules/xmpp/AVModeration.js.map +1 -0
- package/dist/esm/modules/xmpp/BreakoutRooms.js +230 -0
- package/dist/esm/modules/xmpp/BreakoutRooms.js.map +1 -0
- package/dist/esm/modules/xmpp/Caps.js +223 -0
- package/dist/esm/modules/xmpp/Caps.js.map +1 -0
- package/dist/esm/modules/xmpp/ChatRoom.js +1877 -0
- package/dist/esm/modules/xmpp/ChatRoom.js.map +1 -0
- package/dist/esm/modules/xmpp/ConnectionPlugin.js +37 -0
- package/dist/esm/modules/xmpp/ConnectionPlugin.js.map +1 -0
- package/dist/esm/modules/xmpp/FileSharing.js +95 -0
- package/dist/esm/modules/xmpp/FileSharing.js.map +1 -0
- package/dist/esm/modules/xmpp/JingleHelperFunctions.js +168 -0
- package/dist/esm/modules/xmpp/JingleHelperFunctions.js.map +1 -0
- package/dist/esm/modules/xmpp/JingleSession.js +166 -0
- package/dist/esm/modules/xmpp/JingleSession.js.map +1 -0
- package/dist/esm/modules/xmpp/JingleSessionPC.js +1969 -0
- package/dist/esm/modules/xmpp/JingleSessionPC.js.map +1 -0
- package/dist/esm/modules/xmpp/JingleSessionState.js +23 -0
- package/dist/esm/modules/xmpp/JingleSessionState.js.map +1 -0
- package/dist/esm/modules/xmpp/Lobby.js +384 -0
- package/dist/esm/modules/xmpp/Lobby.js.map +1 -0
- package/dist/esm/modules/xmpp/MediaSessionEvents.js +12 -0
- package/dist/esm/modules/xmpp/MediaSessionEvents.js.map +1 -0
- package/dist/esm/modules/xmpp/MockClasses.js +77 -0
- package/dist/esm/modules/xmpp/MockClasses.js.map +1 -0
- package/dist/esm/modules/xmpp/Polls.js +87 -0
- package/dist/esm/modules/xmpp/Polls.js.map +1 -0
- package/dist/esm/modules/xmpp/ResumeTask.js +149 -0
- package/dist/esm/modules/xmpp/ResumeTask.js.map +1 -0
- package/dist/esm/modules/xmpp/RoomMetadata.js +96 -0
- package/dist/esm/modules/xmpp/RoomMetadata.js.map +1 -0
- package/dist/esm/modules/xmpp/SignalingLayerImpl.js +313 -0
- package/dist/esm/modules/xmpp/SignalingLayerImpl.js.map +1 -0
- package/dist/esm/modules/xmpp/StropheErrorHandler.js +53 -0
- package/dist/esm/modules/xmpp/StropheErrorHandler.js.map +1 -0
- package/dist/esm/modules/xmpp/StropheLastSuccess.js +52 -0
- package/dist/esm/modules/xmpp/StropheLastSuccess.js.map +1 -0
- package/dist/esm/modules/xmpp/XmppConnection.js +579 -0
- package/dist/esm/modules/xmpp/XmppConnection.js.map +1 -0
- package/dist/esm/modules/xmpp/moderator.js +524 -0
- package/dist/esm/modules/xmpp/moderator.js.map +1 -0
- package/dist/esm/modules/xmpp/sha1.js +165 -0
- package/dist/esm/modules/xmpp/sha1.js.map +1 -0
- package/dist/esm/modules/xmpp/strophe.disco.js +222 -0
- package/dist/esm/modules/xmpp/strophe.disco.js.map +1 -0
- package/dist/esm/modules/xmpp/strophe.emuc.js +206 -0
- package/dist/esm/modules/xmpp/strophe.emuc.js.map +1 -0
- package/dist/esm/modules/xmpp/strophe.jingle.js +404 -0
- package/dist/esm/modules/xmpp/strophe.jingle.js.map +1 -0
- package/dist/esm/modules/xmpp/strophe.logger.js +44 -0
- package/dist/esm/modules/xmpp/strophe.logger.js.map +1 -0
- package/dist/esm/modules/xmpp/strophe.ping.js +170 -0
- package/dist/esm/modules/xmpp/strophe.ping.js.map +1 -0
- package/dist/esm/modules/xmpp/strophe.rayo.js +117 -0
- package/dist/esm/modules/xmpp/strophe.rayo.js.map +1 -0
- package/dist/esm/modules/xmpp/strophe.stream-management.js +365 -0
- package/dist/esm/modules/xmpp/strophe.stream-management.js.map +1 -0
- package/dist/esm/modules/xmpp/strophe.util.js +116 -0
- package/dist/esm/modules/xmpp/strophe.util.js.map +1 -0
- package/dist/esm/modules/xmpp/xmpp.js +973 -0
- package/dist/esm/modules/xmpp/xmpp.js.map +1 -0
- package/dist/esm/service/RTC/BridgeVideoType.js +24 -0
- package/dist/esm/service/RTC/BridgeVideoType.js.map +1 -0
- package/dist/esm/service/RTC/CameraFacingMode.js +21 -0
- package/dist/esm/service/RTC/CameraFacingMode.js.map +1 -0
- package/dist/esm/service/RTC/CodecMimeType.js +36 -0
- package/dist/esm/service/RTC/CodecMimeType.js.map +1 -0
- package/dist/esm/service/RTC/MediaDirection.js +23 -0
- package/dist/esm/service/RTC/MediaDirection.js.map +1 -0
- package/dist/esm/service/RTC/MediaType.js +20 -0
- package/dist/esm/service/RTC/MediaType.js.map +1 -0
- package/dist/esm/service/RTC/RTCEvents.js +111 -0
- package/dist/esm/service/RTC/RTCEvents.js.map +1 -0
- package/dist/esm/service/RTC/ReceiverAudioSubscription.js +27 -0
- package/dist/esm/service/RTC/ReceiverAudioSubscription.js.map +1 -0
- package/dist/esm/service/RTC/Resolutions.js +56 -0
- package/dist/esm/service/RTC/Resolutions.js.map +1 -0
- package/dist/esm/service/RTC/SignalingEvents.js +42 -0
- package/dist/esm/service/RTC/SignalingEvents.js.map +1 -0
- package/dist/esm/service/RTC/SignalingLayer.js +153 -0
- package/dist/esm/service/RTC/SignalingLayer.js.map +1 -0
- package/dist/esm/service/RTC/StandardVideoQualitySettings.js +180 -0
- package/dist/esm/service/RTC/StandardVideoQualitySettings.js.map +1 -0
- package/dist/esm/service/RTC/VideoEncoderScalabilityMode.js +36 -0
- package/dist/esm/service/RTC/VideoEncoderScalabilityMode.js.map +1 -0
- package/dist/esm/service/RTC/VideoType.js +19 -0
- package/dist/esm/service/RTC/VideoType.js.map +1 -0
- package/dist/esm/service/authentication/AuthenticationEvents.js +13 -0
- package/dist/esm/service/authentication/AuthenticationEvents.js.map +1 -0
- package/dist/esm/service/connectivity/ConnectionQualityEvents.js +13 -0
- package/dist/esm/service/connectivity/ConnectionQualityEvents.js.map +1 -0
- package/dist/esm/service/connectivity/Constants.js +3 -0
- package/dist/esm/service/connectivity/Constants.js.map +1 -0
- package/dist/esm/service/e2eping/E2ePingEvents.js +8 -0
- package/dist/esm/service/e2eping/E2ePingEvents.js.map +1 -0
- package/dist/esm/service/statistics/AnalyticsEvents.js +521 -0
- package/dist/esm/service/statistics/AnalyticsEvents.js.map +1 -0
- package/dist/esm/service/statistics/Events.js +36 -0
- package/dist/esm/service/statistics/Events.js.map +1 -0
- package/dist/esm/service/statistics/constants.js +2 -0
- package/dist/esm/service/statistics/constants.js.map +1 -0
- package/dist/esm/service/xmpp/XMPPEvents.js +359 -0
- package/dist/esm/service/xmpp/XMPPEvents.js.map +1 -0
- package/dist/esm/service/xmpp/XMPPExtensioProtocols.js +64 -0
- package/dist/esm/service/xmpp/XMPPExtensioProtocols.js.map +1 -0
- package/dist/esm/test-setup-polyfill.js +34 -0
- package/dist/esm/test-setup-polyfill.js.map +1 -0
- package/dist/esm/tools/gen-version.js +15 -0
- package/dist/esm/tools/gen-version.js.map +1 -0
- package/dist/esm/version.js +3 -0
- package/dist/esm/version.js.map +1 -0
- package/dist/umd/lib-jitsi-meet.e2ee-worker.js +484 -0
- package/dist/umd/lib-jitsi-meet.min.js +3 -0
- package/dist/umd/lib-jitsi-meet.min.js.LICENSE.txt +18 -0
- package/dist/umd/lib-jitsi-meet.min.map +1 -0
- package/package.json +93 -0
- 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
|