diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/adapter.tsx b/bigbluebutton-html5/imports/ui/components/video-provider/adapter.tsx similarity index 100% rename from bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/adapter.tsx rename to bigbluebutton-html5/imports/ui/components/video-provider/adapter.tsx diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx deleted file mode 100755 index df940d33d4..0000000000 --- a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx +++ /dev/null @@ -1,1313 +0,0 @@ -/* eslint react/sort-comp: 0 */ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import ReconnectingWebSocket from 'reconnecting-websocket'; -import { defineMessages, injectIntl } from 'react-intl'; -import { debounce } from '/imports/utils/debounce'; -import VideoService from './service'; -import VideoListContainer from './video-list/container'; -import { - fetchWebRTCMappedStunTurnServers, - getMappedFallbackStun, -} from '/imports/utils/fetchStunTurnServers'; -import logger from '/imports/startup/client/logger'; -import { notifyStreamStateChange } from '/imports/ui/services/bbb-webrtc-sfu/stream-state-service'; -import VideoPreviewService from '../video-preview/service'; -import MediaStreamUtils from '/imports/utils/media-stream-utils'; -import BBBVideoStream from '/imports/ui/services/webrtc-base/bbb-video-stream'; -import { - EFFECT_TYPES, - getSessionVirtualBackgroundInfo, -} from '/imports/ui/services/virtual-background/service'; -import { notify } from '/imports/ui/services/notification'; -import { shouldForceRelay } from '/imports/ui/services/bbb-webrtc-sfu/utils'; -import WebRtcPeer from '/imports/ui/services/webrtc-base/peer'; - -const intlClientErrors = defineMessages({ - permissionError: { - id: 'app.video.permissionError', - description: 'Webcam permission error', - }, - iceConnectionStateError: { - id: 'app.video.iceConnectionStateError', - description: 'Ice connection state failed', - }, - mediaFlowTimeout: { - id: 'app.video.mediaFlowTimeout1020', - description: 'Media flow timeout', - }, - mediaTimedOutError: { - id: 'app.video.mediaTimedOutError', - description: 'Media was ejected by the server due to lack of valid media', - }, - virtualBgGenericError: { - id: 'app.video.virtualBackground.genericError', - description: 'Failed to apply camera effect', - }, - inactiveError: { - id: 'app.video.inactiveError', - description: 'Camera stopped unexpectedly', - }, -}); - -const intlSFUErrors = defineMessages({ - 2000: { - id: 'app.sfu.mediaServerConnectionError2000', - description: 'SFU connection to the media server', - }, - 2001: { - id: 'app.sfu.mediaServerOffline2001', - description: 'SFU is offline', - }, - 2002: { - id: 'app.sfu.mediaServerNoResources2002', - description: 'Media server lacks disk, CPU or FDs', - }, - 2003: { - id: 'app.sfu.mediaServerRequestTimeout2003', - description: 'Media requests timeout due to lack of resources', - }, - 2021: { - id: 'app.sfu.serverIceGatheringFailed2021', - description: 'Server cannot enact ICE gathering', - }, - 2022: { - id: 'app.sfu.serverIceStateFailed2022', - description: 'Server endpoint transitioned to a FAILED ICE state', - }, - 2200: { - id: 'app.sfu.mediaGenericError2200', - description: 'SFU component generated a generic error', - }, - 2202: { - id: 'app.sfu.invalidSdp2202', - description: 'Client provided an invalid SDP', - }, - 2203: { - id: 'app.sfu.noAvailableCodec2203', - description: 'Server has no available codec for the client', - }, -}); - -const propTypes = { - streams: PropTypes.arrayOf(Array).isRequired, - intl: PropTypes.objectOf(Object).isRequired, - isUserLocked: PropTypes.bool.isRequired, - swapLayout: PropTypes.bool.isRequired, - currentVideoPageIndex: PropTypes.number.isRequired, - totalNumberOfStreams: PropTypes.number.isRequired, - isMeteorConnected: PropTypes.bool.isRequired, - playStart: PropTypes.func.isRequired, - sendUserUnshareWebcam: PropTypes.func.isRequired, -}; - -class VideoProvider extends Component { - onBeforeUnload() { - const { sendUserUnshareWebcam } = this.props; - VideoService.onBeforeUnload(sendUserUnshareWebcam); - } - - static shouldAttachVideoStream(peer, videoElement) { - // Conditions to safely attach a stream to a video element in all browsers: - // 1 - Peer exists, video element exists - // 2 - Target stream differs from videoElement's (diff) - // 3a - If the stream is a remote one, the safest (*ahem* Safari) moment to - // do so is waiting for the server to confirm that media has flown out of it - // towards te remote end (peer.started) - // 3b - If the stream is a local one (webcam sharer) and is started - // 4 - If the stream is local one, check if there area video tracks there are - // video tracks: attach it - if (peer == null || videoElement == null) return false; - const stream = peer.isPublisher ? peer.getLocalStream() : peer.getRemoteStream(); - const diff = stream && (stream.id !== videoElement.srcObject?.id || !videoElement.paused); - - if (peer.started && diff) return true; - - return peer.isPublisher - && peer.getLocalStream() - && peer.getLocalStream().getVideoTracks().length > 0 - && diff; - } - - constructor(props) { - super(props); - - // socketOpen state is there to force update when the signaling socket opens or closes - this.state = { - socketOpen: false, - }; - this._isMounted = false; - this.info = VideoService.getInfo(); - // Signaling message queue arrays indexed by stream (== cameraId) - this.wsQueues = {}; - this.restartTimeout = {}; - this.restartTimer = {}; - this.webRtcPeers = {}; - this.outboundIceQueues = {}; - this.videoTags = {}; - - this.createVideoTag = this.createVideoTag.bind(this); - this.destroyVideoTag = this.destroyVideoTag.bind(this); - this.onWsOpen = this.onWsOpen.bind(this); - this.onWsClose = this.onWsClose.bind(this); - this.onWsMessage = this.onWsMessage.bind(this); - this.updateStreams = this.updateStreams.bind(this); - this.connectStreams = this.connectStreams.bind(this); - this.debouncedConnectStreams = debounce( - this.connectStreams, - VideoService.getPageChangeDebounceTime(), - { leading: false, trailing: true }, - ); - this.startVirtualBackgroundByDrop = this.startVirtualBackgroundByDrop.bind(this); - this.onBeforeUnload = this.onBeforeUnload.bind(this); - } - - componentDidMount() { - this._isMounted = true; - VideoService.updatePeerDictionaryReference(this.webRtcPeers); - this.ws = this.openWs(); - window.addEventListener('beforeunload', this.onBeforeUnload); - } - - componentDidUpdate(prevProps) { - const { - isUserLocked, - streams, - currentVideoPageIndex, - isMeteorConnected, - sendUserUnshareWebcam, - } = this.props; - const { socketOpen } = this.state; - - // Only debounce when page changes to avoid unnecessary debouncing - const shouldDebounce = VideoService.isPaginationEnabled() - && prevProps.currentVideoPageIndex !== currentVideoPageIndex; - - if (isMeteorConnected && socketOpen) this.updateStreams(streams, shouldDebounce); - if (!prevProps.isUserLocked && isUserLocked) VideoService.lockUser(sendUserUnshareWebcam); - - // Signaling socket expired its retries and meteor is connected - create - // a new signaling socket instance from scratch - if (!socketOpen - && isMeteorConnected - && this.ws == null) { - this.ws = this.openWs(); - } - } - - componentWillUnmount() { - const { sendUserUnshareWebcam } = this.props; - this._isMounted = false; - VideoService.updatePeerDictionaryReference({}); - - if (this.ws) { - this.ws.onmessage = null; - this.ws.onopen = null; - this.ws.onclose = null; - } - - window.removeEventListener('beforeunload', this.onBeforeUnload); - VideoService.exitVideo(sendUserUnshareWebcam); - Object.keys(this.webRtcPeers).forEach((stream) => { - this.stopWebRTCPeer(stream, false); - }); - this.terminateWs(); - } - - openWs() { - // Default values and default empty object to be backwards compat with 2.2. - // FIXME Remove hardcoded defaults 2.3. - const { - connectionTimeout: WS_CONN_TIMEOUT = 4000, - maxRetries: WS_MAX_RETRIES = 5, - debug: WS_DEBUG, - } = window.meetingClientSettings.public.kurento.cameraWsOptions; - - const ws = new ReconnectingWebSocket( - VideoService.getAuthenticatedURL(), [], { - connectionTimeout: WS_CONN_TIMEOUT, - debug: WS_DEBUG, - maxRetries: WS_MAX_RETRIES, - maxEnqueuedMessages: 0, - }, - ); - ws.onopen = this.onWsOpen; - ws.onclose = this.onWsClose; - ws.onmessage = this.onWsMessage; - - return ws; - } - - terminateWs() { - if (this.ws) { - this.clearWSHeartbeat(); - this.ws.close(); - this.ws = null; - } - } - - _updateLastMsgTime() { - this.ws.isAlive = true; - this.ws.lastMsgTime = Date.now(); - } - - _getTimeSinceLastMsg() { - return Date.now() - this.ws.lastMsgTime; - } - - setupWSHeartbeat() { - // Default values and default empty object to be backwards compat with 2.2. - // FIXME Remove hardcoded defaults 2.3. - const { - heartbeat: WS_HEARTBEAT_OPTS = { - interval: 15000, - delay: 3000, - reconnectOnFailure: true, - }, - } = window.meetingClientSettings.public.kurento.cameraWsOptions; - - if (WS_HEARTBEAT_OPTS.interval === 0 || this.ws == null || this.ws.wsHeartbeat) return; - - this.ws.isAlive = true; - this.ws.wsHeartbeat = setInterval(() => { - if (this.ws.isAlive === false) { - logger.warn({ - logCode: 'video_provider_ws_heartbeat_failed', - }, 'Video provider WS heartbeat failed.'); - - if (WS_HEARTBEAT_OPTS.reconnectOnFailure) this.ws.reconnect(); - return; - } - - if (this._getTimeSinceLastMsg() < ( - WS_HEARTBEAT_OPTS.interval - WS_HEARTBEAT_OPTS.delay - )) { - return; - } - - this.ws.isAlive = false; - this.ping(); - }, WS_HEARTBEAT_OPTS.interval); - - this.ping(); - } - - clearWSHeartbeat() { - if (this.ws?.wsHeartbeat) { - clearInterval(this.ws.wsHeartbeat); - this.ws.wsHeartbeat = null; - } - } - - onWsMessage(message) { - this._updateLastMsgTime(); - const parsedMessage = JSON.parse(message.data); - - if (parsedMessage.id === 'pong') return; - - switch (parsedMessage.id) { - case 'startResponse': - this.startResponse(parsedMessage); - break; - - case 'playStart': - this.handlePlayStart(parsedMessage); - break; - - case 'playStop': - this.handlePlayStop(parsedMessage); - break; - - case 'iceCandidate': - this.handleIceCandidate(parsedMessage); - break; - - case 'pong': - break; - - case 'error': - default: - this.handleSFUError(parsedMessage); - break; - } - } - - onWsClose() { - const { sendUserUnshareWebcam } = this.props; - logger.info({ - logCode: 'video_provider_onwsclose', - }, 'Multiple video provider websocket connection closed.'); - - this.clearWSHeartbeat(); - VideoService.exitVideo(sendUserUnshareWebcam); - // Media is currently tied to signaling state - so if signaling shuts down, - // media will shut down server-side. This cleans up our local state faster - // and notify the state change as failed so the UI rolls back to the placeholder - // avatar UI in the camera container - Object.keys(this.webRtcPeers).forEach((stream) => { - if (this.stopWebRTCPeer(stream, false)) { - notifyStreamStateChange(stream, 'failed'); - } - }); - this.setState({ socketOpen: false }); - - const { - maxRetries: WS_MAX_RETRIES = 5, - } = window.meetingClientSettings.public.kurento.cameraWsOptions; - - if (this.ws && this.ws.retryCount >= WS_MAX_RETRIES) { - this.terminateWs(); - } - } - - onWsOpen() { - logger.info({ - logCode: 'video_provider_onwsopen', - }, 'Multiple video provider websocket connection opened.'); - - this._updateLastMsgTime(); - this.setupWSHeartbeat(); - this.setState({ socketOpen: true }); - // Resend queued messages that happened when socket was not connected - Object.entries(this.wsQueues).forEach(([stream, queue]) => { - if (this.webRtcPeers[stream]) { - // Peer - send enqueued - while (queue.length > 0) { - this.sendMessage(queue.pop()); - } - } else { - // No peer - delete queue - this.wsQueues[stream] = null; - } - }); - } - - findAllPrivilegedStreams () { - const { streams } = this.props; - // Privileged streams are: floor holders, pinned users - return streams.filter(stream => stream.floor || stream.pin); - } - - updateQualityThresholds(numberOfPublishers) { - const { threshold, profile } = VideoService.getThreshold(numberOfPublishers); - - const { - privilegedStreams: CAMERA_QUALITY_THR_PRIVILEGED = true, - } = window.meetingClientSettings.public.kurento.cameraQualityThresholds; - - if (profile) { - const privilegedStreams = this.findAllPrivilegedStreams(); - Object.values(this.webRtcPeers) - .filter(peer => peer.isPublisher) - .forEach((peer) => { - // Conditions which make camera revert their original profile - // 1) Threshold 0 means original profile/inactive constraint - // 2) Privileged streams - const exempt = threshold === 0 - || (CAMERA_QUALITY_THR_PRIVILEGED && privilegedStreams.some(vs => vs.stream === peer.stream)) - const profileToApply = exempt ? peer.originalProfileId : profile; - VideoService.applyCameraProfile(peer, profileToApply); - }); - } - } - - getStreamsToConnectAndDisconnect(streams) { - const streamsCameraIds = streams.filter(s => !s?.isGridItem).map(s => s.stream); - const streamsConnected = Object.keys(this.webRtcPeers); - - const streamsToConnect = streamsCameraIds.filter(stream => { - return !streamsConnected.includes(stream); - }); - - const streamsToDisconnect = streamsConnected.filter(stream => { - return !streamsCameraIds.includes(stream); - }); - - return [streamsToConnect, streamsToDisconnect]; - } - - connectStreams(streamsToConnect) { - streamsToConnect.forEach((stream) => { - const isLocal = VideoService.isLocalStream(stream); - this.createWebRTCPeer(stream, isLocal); - }); - } - - disconnectStreams(streamsToDisconnect) { - streamsToDisconnect.forEach(stream => this.stopWebRTCPeer(stream, false)); - } - - updateStreams(streams, shouldDebounce = false) { - const [streamsToConnect, streamsToDisconnect] = this.getStreamsToConnectAndDisconnect(streams); - - const { - enabled: CAMERA_QUALITY_THRESHOLDS_ENABLED = true, - } = window.meetingClientSettings.public.kurento.cameraQualityThresholds; - - if (shouldDebounce) { - this.debouncedConnectStreams(streamsToConnect); - } else { - this.connectStreams(streamsToConnect); - } - - this.disconnectStreams(streamsToDisconnect); - - if (CAMERA_QUALITY_THRESHOLDS_ENABLED) { - this.updateQualityThresholds(this.props.totalNumberOfStreams); - } - } - - ping() { - const message = { id: 'ping' }; - this.sendMessage(message); - } - - sendMessage(message) { - const { ws } = this; - - if (this.connectedToMediaServer()) { - const jsonMessage = JSON.stringify(message); - try { - ws.send(jsonMessage); - } catch (error) { - logger.error({ - logCode: 'video_provider_ws_send_error', - extraInfo: { - errorMessage: error.message || 'Unknown', - errorCode: error.code, - }, - }, 'Camera request failed to be sent to SFU'); - } - } else if (message.id !== 'stop') { - // No need to queue video stop messages - const { cameraId } = message; - if (cameraId) { - if (this.wsQueues[cameraId] == null) this.wsQueues[cameraId] = []; - this.wsQueues[cameraId].push(message); - } - } - } - - connectedToMediaServer() { - return this.ws && this.ws.readyState === ReconnectingWebSocket.OPEN; - } - - processOutboundIceQueue(peer, role, stream) { - const queue = this.outboundIceQueues[stream]; - while (queue && queue.length) { - const candidate = queue.shift(); - this.sendIceCandidateToSFU(peer, role, candidate, stream); - } - } - - sendLocalAnswer (peer, stream, answer) { - const message = { - id: 'subscriberAnswer', - type: 'video', - role: VideoService.getRole(peer.isPublisher), - cameraId: stream, - answer, - }; - - this.sendMessage(message); - } - - startResponse(message) { - const { cameraId: stream, role } = message; - const peer = this.webRtcPeers[stream]; - - logger.debug({ - logCode: 'video_provider_start_response_success', - extraInfo: { cameraId: stream, role }, - }, `Camera start request accepted by SFU. Role: ${role}`); - - if (peer) { - const processorFunc = peer.isPublisher - ? peer.processAnswer.bind(peer) - : peer.processOffer.bind(peer); - - processorFunc(message.sdpAnswer).then((answer) => { - if (answer) this.sendLocalAnswer(peer, stream, answer); - - peer.didSDPAnswered = true; - this.processOutboundIceQueue(peer, role, stream); - VideoService.processInboundIceQueue(peer, stream); - }).catch((error) => { - logger.error({ - logCode: 'video_provider_peerconnection_process_error', - extraInfo: { - cameraId: stream, - role, - errorMessage: error.message, - errorCode: error.code, - }, - }, 'Camera answer processing failed'); - }); - } else { - logger.warn({ - logCode: 'video_provider_startresponse_no_peer', - extraInfo: { cameraId: stream, role }, - }, 'No peer on SFU camera start response handler'); - } - } - - handleIceCandidate(message) { - const { cameraId: stream, candidate } = message; - const peer = this.webRtcPeers[stream]; - - if (peer) { - if (peer.didSDPAnswered) { - VideoService.addCandidateToPeer(peer, candidate, stream); - } else { - // ICE candidates are queued until a SDP answer has been processed. - // This was done due to a long term iOS/Safari quirk where it'd - // fail if candidates were added before the offer/answer cycle was completed. - // Dunno if that still happens, but it works even if it slows the ICE checks - // a bit - prlanzarin july 2019 - if (peer.inboundIceQueue == null) { - peer.inboundIceQueue = []; - } - peer.inboundIceQueue.push(candidate); - } - } else { - logger.warn({ - logCode: 'video_provider_addicecandidate_no_peer', - extraInfo: { cameraId: stream }, - }, 'Trailing camera ICE candidate, discarded'); - } - } - - clearRestartTimers(stream) { - if (this.restartTimeout[stream]) { - clearTimeout(this.restartTimeout[stream]); - delete this.restartTimeout[stream]; - } - - if (this.restartTimer[stream]) { - delete this.restartTimer[stream]; - } - } - - stopWebRTCPeer(stream, restarting = false) { - const isLocal = VideoService.isLocalStream(stream); - const { sendUserUnshareWebcam } = this.props; - - // in this case, 'closed' state is not caused by an error; - // we stop listening to prevent this from being treated as an error - const peer = this.webRtcPeers[stream]; - if (peer && peer.peerConnection) { - const conn = peer.peerConnection; - conn.oniceconnectionstatechange = null; - } - - if (isLocal) { - VideoService.stopVideo(stream, sendUserUnshareWebcam); - } - - const role = VideoService.getRole(isLocal); - - logger.info({ - logCode: 'video_provider_stopping_webcam_sfu', - extraInfo: { role, cameraId: stream, restarting }, - }, `Camera feed stop requested. Role ${role}, restarting ${restarting}`); - - this.sendMessage({ - id: 'stop', - type: 'video', - cameraId: stream, - role, - }); - - // Clear the shared camera media flow timeout and current reconnect period - // when destroying it if the peer won't restart - if (!restarting) { - this.clearRestartTimers(stream); - } - - return this.destroyWebRTCPeer(stream); - } - - destroyWebRTCPeer(stream) { - let stopped = false; - const peer = this.webRtcPeers[stream]; - const isLocal = VideoService.isLocalStream(stream); - const role = VideoService.getRole(isLocal); - - if (peer) { - if (peer && peer.bbbVideoStream) { - if (typeof peer.inactivationHandler === 'function') { - peer.bbbVideoStream.removeListener('inactive', peer.inactivationHandler); - } - peer.bbbVideoStream.stop(); - } - - if (typeof peer.dispose === 'function') { - peer.dispose(); - } - - delete this.webRtcPeers[stream]; - stopped = true; - } else { - logger.warn({ - logCode: 'video_provider_destroywebrtcpeer_no_peer', - extraInfo: { cameraId: stream, role }, - }, 'Trailing camera destroy request.'); - } - - delete this.outboundIceQueues[stream]; - delete this.wsQueues[stream]; - - return stopped; - } - - _createPublisher(stream, peerOptions) { - return new Promise((resolve, reject) => { - try { - const { id: profileId } = VideoService.getCameraProfile(); - let bbbVideoStream = VideoService.getPreloadedStream(); - - if (bbbVideoStream) { - peerOptions.videoStream = bbbVideoStream.mediaStream; - } - - const peer = new WebRtcPeer('sendonly', peerOptions); - peer.bbbVideoStream = bbbVideoStream; - this.webRtcPeers[stream] = peer; - peer.stream = stream; - peer.started = false; - peer.didSDPAnswered = false; - peer.inboundIceQueue = []; - peer.isPublisher = true; - peer.originalProfileId = profileId; - peer.currentProfileId = profileId; - peer.start(); - peer.generateOffer().then((offer) => { - // Store the media stream if necessary. The scenario here is one where - // there is no preloaded stream stored. - if (peer.bbbVideoStream == null) { - bbbVideoStream = new BBBVideoStream(peer.getLocalStream()); - VideoPreviewService.storeStream( - MediaStreamUtils.extractDeviceIdFromStream( - bbbVideoStream.mediaStream, - 'video', - ), - bbbVideoStream, - ); - } - - peer.bbbVideoStream = bbbVideoStream; - bbbVideoStream.on('streamSwapped', ({ newStream }) => { - if (newStream && newStream instanceof MediaStream) { - this.replacePCVideoTracks(stream, newStream); - } - }); - peer.inactivationHandler = () => this._handleLocalStreamInactive(stream); - bbbVideoStream.once('inactive', peer.inactivationHandler); - resolve(offer); - }).catch(reject); - } catch (error) { - reject(error); - } - }); - } - - _createSubscriber(stream, peerOptions) { - return new Promise((resolve, reject) => { - try { - const peer = new WebRtcPeer('recvonly', peerOptions); - this.webRtcPeers[stream] = peer; - peer.stream = stream; - peer.started = false; - peer.didSDPAnswered = false; - peer.inboundIceQueue = []; - peer.isPublisher = false; - peer.start(); - resolve(); - } catch (error) { - reject(error); - } - }); - } - - async createWebRTCPeer(stream, isLocal) { - const { - webcam: NETWORK_PRIORITY, - } = window.meetingClientSettings.public.media.networkPriorities || {}; - - const TRACE_LOGS = window.meetingClientSettings.public.kurento.traceLogs; - const GATHERING_TIMEOUT = window.meetingClientSettings.public.kurento.gatheringTimeout; - - let iceServers = []; - const role = VideoService.getRole(isLocal); - const peerBuilderFunc = isLocal - ? this._createPublisher.bind(this) - : this._createSubscriber.bind(this); - - // Check if the peer is already being processed - if (this.webRtcPeers[stream]) { - return; - } - - this.webRtcPeers[stream] = {}; - this.outboundIceQueues[stream] = []; - const { constraints, bitrate } = VideoService.getCameraProfile(); - const peerOptions = { - mediaConstraints: { - audio: false, - video: constraints, - }, - onicecandidate: this._getOnIceCandidateCallback(stream, isLocal), - configuration: { - }, - trace: TRACE_LOGS, - networkPriorities: NETWORK_PRIORITY ? { video: NETWORK_PRIORITY } : undefined, - gatheringTimeout: GATHERING_TIMEOUT, - }; - - try { - iceServers = await fetchWebRTCMappedStunTurnServers(this.info.sessionToken); - } catch (error) { - logger.error({ - logCode: 'video_provider_fetchstunturninfo_error', - extraInfo: { - cameraId: stream, - role, - errorCode: error.code, - errorMessage: error.message, - }, - }, 'video-provider failed to fetch STUN/TURN info, using default'); - // Use fallback STUN server - iceServers = getMappedFallbackStun(); - } finally { - // we need to set iceTransportPolicy after `fetchWebRTCMappedStunTurnServers` - // because `shouldForceRelay` uses the information from the stun API - peerOptions.configuration.iceTransportPolicy = shouldForceRelay() ? 'relay' : undefined; - if (iceServers.length > 0) { - peerOptions.configuration.iceServers = iceServers; - } - - peerBuilderFunc(stream, peerOptions).then((offer) => { - if (!this._isMounted) { - return this.stopWebRTCPeer(stream, false); - } - const peer = this.webRtcPeers[stream]; - - if (peer && peer.peerConnection) { - const conn = peer.peerConnection; - conn.onconnectionstatechange = () => { - this._handleIceConnectionStateChange(stream, isLocal); - }; - } - - const message = { - id: 'start', - type: 'video', - cameraId: stream, - role, - sdpOffer: offer, - bitrate, - record: VideoService.getRecord(), - mediaServer: VideoService.getMediaServerAdapter(), - }; - - logger.info({ - logCode: 'video_provider_sfu_request_start_camera', - extraInfo: { - cameraId: stream, - role, - }, - }, `Camera offer generated. Role: ${role}`); - - this.setReconnectionTimeout(stream, isLocal, false); - this.sendMessage(message); - - return; - }).catch(error => { - return this._onWebRTCError(error, stream, isLocal); - }); - } - } - - _getWebRTCStartTimeout(stream, isLocal) { - const { intl } = this.props; - - const { - maxTimeout: MAX_CAMERA_SHARE_FAILED_WAIT_TIME = 60000, - } = window.meetingClientSettings.public.kurento.cameraTimeouts || {}; - - return () => { - const role = VideoService.getRole(isLocal); - if (!isLocal) { - // Peer that timed out is a subscriber/viewer - // Subscribers try to reconnect according to their timers if media could - // not reach the server. That's why we pass the restarting flag as true - // to the stop procedure as to not destroy the timers - // Create new reconnect interval time - const oldReconnectTimer = this.restartTimer[stream]; - const newReconnectTimer = Math.min( - 2 * oldReconnectTimer, - MAX_CAMERA_SHARE_FAILED_WAIT_TIME, - ); - this.restartTimer[stream] = newReconnectTimer; - - // Clear the current reconnect interval so it can be re-set in createWebRTCPeer - if (this.restartTimeout[stream]) { - delete this.restartTimeout[stream]; - } - - logger.error({ - logCode: 'video_provider_camera_view_timeout', - extraInfo: { - cameraId: stream, - role, - oldReconnectTimer, - newReconnectTimer, - }, - }, 'Camera VIEWER failed. Reconnecting.'); - - this.reconnect(stream, isLocal); - } else { - // Peer that timed out is a sharer/publisher, clean it up, stop. - logger.error({ - logCode: 'video_provider_camera_share_timeout', - extraInfo: { - cameraId: stream, - role, - }, - }, 'Camera SHARER failed.'); - VideoService.notify(intl.formatMessage(intlClientErrors.mediaFlowTimeout)); - this.stopWebRTCPeer(stream, false); - } - }; - } - - _onWebRTCError(error, stream, isLocal) { - const { intl, streams } = this.props; - const { name: errorName, message: errorMessage } = error; - const errorLocale = intlClientErrors[errorName] - || intlClientErrors[errorMessage] - || intlSFUErrors[error]; - - logger.error({ - logCode: 'video_provider_webrtc_peer_error', - extraInfo: { - cameraId: stream, - role: VideoService.getRole(isLocal), - errorName: error.name, - errorMessage: error.message, - }, - }, 'Camera peer failed'); - - // Only display WebRTC negotiation error toasts to sharers. The viewer streams - // will try to autoreconnect silently, but the error will log nonetheless - if (isLocal) { - this.stopWebRTCPeer(stream, false); - if (errorLocale) VideoService.notify(intl.formatMessage(errorLocale)); - } else { - // If it's a viewer, set the reconnection timeout. There's a good chance - // no local candidate was generated and it wasn't set. - const peer = this.webRtcPeers[stream]; - const stillExists = streams.some(({ stream: streamId }) => streamId === stream); - - if (stillExists) { - const isEstablishedConnection = peer && peer.started; - this.setReconnectionTimeout(stream, isLocal, isEstablishedConnection); - } - - // second argument means it will only try to reconnect if - // it's a viewer instance (see stopWebRTCPeer restarting argument) - this.stopWebRTCPeer(stream, stillExists); - } - } - - reconnect(stream, isLocal) { - this.stopWebRTCPeer(stream, true); - this.createWebRTCPeer(stream, isLocal); - } - - setReconnectionTimeout(stream, isLocal, isEstablishedConnection) { - const peer = this.webRtcPeers[stream]; - const shouldSetReconnectionTimeout = !this.restartTimeout[stream] && !isEstablishedConnection; - - // This is an ongoing reconnection which succeeded in the first place but - // then failed mid call. Try to reconnect it right away. Clear the restart - // timers since we don't need them in this case. - if (isEstablishedConnection) { - this.clearRestartTimers(stream); - return this.reconnect(stream, isLocal); - } - - // This is a reconnection timer for a peer that hasn't succeeded in the first - // place. Set reconnection timeouts with random intervals between them to try - // and reconnect without flooding the server - const { - baseTimeout: CAMERA_SHARE_FAILED_WAIT_TIME = 15000, - } = window.meetingClientSettings.public.kurento.cameraTimeouts || {}; - - if (shouldSetReconnectionTimeout) { - const newReconnectTimer = this.restartTimer[stream] || CAMERA_SHARE_FAILED_WAIT_TIME; - this.restartTimer[stream] = newReconnectTimer; - - this.restartTimeout[stream] = setTimeout( - this._getWebRTCStartTimeout(stream, isLocal), - this.restartTimer[stream] - ); - } - } - - _getOnIceCandidateCallback(stream, isLocal) { - const SIGNAL_CANDIDATES = window.meetingClientSettings.public.kurento.signalCandidates; - - if (SIGNAL_CANDIDATES) { - return (candidate) => { - const peer = this.webRtcPeers[stream]; - const role = VideoService.getRole(isLocal); - - if (peer && !peer.didSDPAnswered) { - this.outboundIceQueues[stream].push(candidate); - return; - } - - this.sendIceCandidateToSFU(peer, role, candidate, stream); - }; - } - - return null; - } - - sendIceCandidateToSFU(peer, role, candidate, stream) { - const message = { - type: 'video', - role, - id: 'onIceCandidate', - candidate, - cameraId: stream, - }; - this.sendMessage(message); - } - - _handleLocalStreamInactive(stream) { - const peer = this.webRtcPeers[stream]; - const isLocal = VideoService.isLocalStream(stream); - const role = VideoService.getRole(isLocal); - - // Peer == null: this is a trailing event. - // !isLocal: someone is misusing this handler - local streams only. - if (peer == null || !isLocal) return; - - logger.error({ - logCode: 'video_provider_local_stream_inactive', - extraInfo: { - cameraId: stream, - role, - }, - }, 'Local camera stream stopped unexpectedly'); - - const error = new Error('inactiveError'); - this._onWebRTCError(error, stream, isLocal); - } - - _handleIceConnectionStateChange(stream, isLocal) { - const { intl } = this.props; - const peer = this.webRtcPeers[stream]; - const role = VideoService.getRole(isLocal); - - if (peer && peer.peerConnection) { - const pc = peer.peerConnection; - const connectionState = pc.connectionState; - notifyStreamStateChange(stream, connectionState); - - if (connectionState === 'failed' || connectionState === 'closed') { - const error = new Error('iceConnectionStateError'); - // prevent the same error from being detected multiple times - pc.onconnectionstatechange = null; - - logger.error({ - logCode: 'video_provider_ice_connection_failed_state', - extraInfo: { - cameraId: stream, - connectionState, - role, - }, - }, `Camera ICE connection state changed: ${connectionState}. Role: ${role}.`); - - this._onWebRTCError(error, stream, isLocal); - } - } else { - logger.error({ - logCode: 'video_provider_ice_connection_nopeer', - extraInfo: { cameraId: stream, role }, - }, `No peer at ICE connection state handler. Camera: ${stream}. Role: ${role}`); - } - } - - attach (peer, videoElement) { - if (peer && videoElement) { - const stream = peer.isPublisher ? peer.getLocalStream() : peer.getRemoteStream(); - videoElement.pause(); - videoElement.srcObject = stream; - videoElement.load(); - } - } - - getVideoElement(streamId) { - return this.videoTags[streamId]; - } - - attachVideoStream(stream) { - const videoElement = this.getVideoElement(stream); - const isLocal = VideoService.isLocalStream(stream); - const peer = this.webRtcPeers[stream]; - - if (VideoProvider.shouldAttachVideoStream(peer, videoElement)) { - const pc = peer.peerConnection; - // Notify current stream state again on attachment since the - // video-list-item component may not have been mounted before the stream - // reached the connected state. - // This is necessary to ensure that the video element is properly - // hidden/shown when the stream is attached. - notifyStreamStateChange(stream, pc.connectionState); - this.attach(peer, videoElement); - - if (isLocal) { - if (peer.bbbVideoStream == null) { - this.handleVirtualBgError(new TypeError('Undefined media stream')); - return; - } - - const deviceId = MediaStreamUtils.extractDeviceIdFromStream( - peer.bbbVideoStream.mediaStream, - 'video', - ); - const { type, name } = getSessionVirtualBackgroundInfo(deviceId); - - this.restoreVirtualBackground(peer.bbbVideoStream, type, name).catch((error) => { - this.handleVirtualBgError(error, type, name); - }); - } - } - } - - startVirtualBackgroundByDrop(stream, type, name, data) { - return new Promise((resolve, reject) => { - const peer = this.webRtcPeers[stream]; - const { bbbVideoStream } = peer; - const video = this.getVideoElement(stream); - - if (peer && video && video.srcObject) { - bbbVideoStream.startVirtualBackground(type, name, { file: data }) - .then(resolve) - .catch(reject); - } - }).catch((error) => { - this.handleVirtualBgErrorByDropping(error, type, name); - }); - } - - handleVirtualBgErrorByDropping(error, type, name) { - logger.error({ - logCode: `video_provider_virtualbg_error`, - extraInfo: { - errorName: error.name, - errorMessage: error.message, - virtualBgType: type, - virtualBgName: name, - }, - }, `Failed to start virtual background by dropping image: ${error.message}`); - } - - restoreVirtualBackground(stream, type, name) { - return new Promise((resolve, reject) => { - if (type !== EFFECT_TYPES.NONE_TYPE) { - stream.startVirtualBackground(type, name).then(() => { - resolve(); - }).catch((error) => { - reject(error); - }); - } - resolve(); - }); - } - - handleVirtualBgError(error, type, name) { - const { intl } = this.props; - logger.error({ - logCode: `video_provider_virtualbg_error`, - extraInfo: { - errorName: error.name, - errorMessage: error.message, - virtualBgType: type, - virtualBgName: name, - }, - }, `Failed to restore virtual background after reentering the room: ${error.message}`); - - notify(intl.formatMessage(intlClientErrors.virtualBgGenericError), 'error', 'video'); - } - - createVideoTag(stream, video) { - const peer = this.webRtcPeers[stream]; - this.videoTags[stream] = video; - - if (peer && peer.stream === stream) { - this.attachVideoStream(stream); - } - } - - destroyVideoTag(stream) { - const videoElement = this.videoTags[stream]; - - if (videoElement == null) return; - - if (typeof videoElement.pause === 'function') { - videoElement.pause(); - videoElement.srcObject = null; - } - - delete this.videoTags[stream]; - } - - handlePlayStop(message) { - const { intl } = this.props; - const { cameraId: stream, role } = message; - - logger.info({ - logCode: 'video_provider_handle_play_stop', - extraInfo: { - cameraId: stream, - role, - }, - }, `Received request from SFU to stop camera. Role: ${role}`); - - VideoService.notify(intl.formatMessage(intlClientErrors.mediaTimedOutError)); - this.stopWebRTCPeer(stream, false); - } - - handlePlayStart(message) { - const { cameraId: stream, role } = message; - const peer = this.webRtcPeers[stream]; - const { playStart } = this.props; - - if (peer) { - logger.info({ - logCode: 'video_provider_handle_play_start_flowing', - extraInfo: { - cameraId: stream, - role, - }, - }, `Camera media is flowing (server). Role: ${role}`); - - peer.started = true; - - // Clear camera shared timeout when camera successfully starts - this.clearRestartTimers(stream); - this.attachVideoStream(stream); - - playStart(stream); - } else { - logger.warn({ - logCode: 'video_provider_playstart_no_peer', - extraInfo: { cameraId: stream, role }, - }, 'Trailing camera playStart response.'); - } - } - - handleSFUError(message) { - const { intl, streams, sendUserUnshareWebcam } = this.props; - const { code, reason, streamId } = message; - const isLocal = VideoService.isLocalStream(streamId); - const role = VideoService.getRole(isLocal); - - logger.error({ - logCode: 'video_provider_handle_sfu_error', - extraInfo: { - errorCode: code, - errorReason: reason, - cameraId: streamId, - role, - }, - }, `SFU returned an error. Code: ${code}, reason: ${reason}`); - - if (isLocal) { - // The publisher instance received an error from the server. There's no reconnect, - // stop it. - VideoService.notify(intl.formatMessage(intlSFUErrors[code] || intlSFUErrors[2200])); - VideoService.stopVideo(streamId, sendUserUnshareWebcam); - } else { - const peer = this.webRtcPeers[streamId]; - const stillExists = streams.some(({ stream }) => streamId === stream); - - if (stillExists) { - const isEstablishedConnection = peer && peer.started; - this.setReconnectionTimeout(streamId, isLocal, isEstablishedConnection); - } - - this.stopWebRTCPeer(streamId, stillExists); - } - } - - replacePCVideoTracks(streamId, mediaStream) { - const peer = this.webRtcPeers[streamId]; - const videoElement = this.getVideoElement(streamId); - - if (peer == null || mediaStream == null || videoElement == null) return; - - const pc = peer.peerConnection; - const newTracks = mediaStream.getVideoTracks(); - - if (pc) { - const trackReplacers = pc.getSenders().map(async (sender, index) => { - if (sender.track == null || sender.track.kind !== 'video') return false; - const newTrack = newTracks[index]; - if (newTrack == null) return false; - try { - await sender.replaceTrack(newTrack); - return true; - } catch (error) { - logger.warn({ - logCode: 'video_provider_replacepc_error', - extraInfo: { errorMessage: error.message, cameraId: streamId }, - }, `Failed to replace peer connection tracks: ${error.message}`); - return false; - } - }); - Promise.all(trackReplacers).then(() => { - this.attach(peer, videoElement); - }); - } - } - - render() { - const { - swapLayout, - currentVideoPageIndex, - streams, - cameraDockBounds, - focusedId, - handleVideoFocus, - isGridEnabled, - users, - } = this.props; - - return ( - - ); - } -} - -VideoProvider.propTypes = propTypes; - -export default injectIntl(VideoProvider); diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/component.tsx b/bigbluebutton-html5/imports/ui/components/video-provider/component.tsx similarity index 100% rename from bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/component.tsx rename to bigbluebutton-html5/imports/ui/components/video-provider/component.tsx diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx deleted file mode 100755 index 86712d91aa..0000000000 --- a/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx +++ /dev/null @@ -1,130 +0,0 @@ -import React from 'react'; -import { withTracker } from 'meteor/react-meteor-data'; -import { useMutation } from '@apollo/client'; -import VideoProvider from './component'; -import VideoService from './service'; -import { sortVideoStreams } from '/imports/ui/components/video-provider/stream-sorting'; -import { CAMERA_BROADCAST_START, CAMERA_BROADCAST_STOP } from './mutations'; -import { getVideoData, getVideoDataGrid } from './queries'; -import useMeeting from '/imports/ui/core/hooks/useMeeting'; -import Auth from '/imports/ui/services/auth'; -import useCurrentUser from '../../core/hooks/useCurrentUser'; -import VideoProviderContainerGraphql from './video-provider-graphql/container'; -import useDeduplicatedSubscription from '../../core/hooks/useDeduplicatedSubscription'; - -const VideoProviderContainer = ({ children, ...props }) => { - const { streams, isGridEnabled } = props; - const [cameraBroadcastStart] = useMutation(CAMERA_BROADCAST_START); - const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP); - - const sendUserShareWebcam = (cameraId) => { - cameraBroadcastStart({ variables: { cameraId } }); - }; - - const sendUserUnshareWebcam = (cameraId) => { - cameraBroadcastStop({ variables: { cameraId } }); - }; - - const playStart = (cameraId) => { - if (VideoService.isLocalStream(cameraId)) { - sendUserShareWebcam(cameraId); - VideoService.joinedVideo(); - } - }; - - return ( - !streams.length && !isGridEnabled - ? null - : ( - - {children} - - ) - ); -}; - -withTracker(({ swapLayout, ...rest }) => { - const isGridLayout = Session.get('isGridEnabled'); - const graphqlQuery = isGridLayout ? getVideoDataGrid : getVideoData; - const currUserId = Auth.userID; - const { data: currentMeeting } = useMeeting((m) => ({ - usersPolicies: m.usersPolicies, - })); - - const { data: currentUser } = useCurrentUser((user) => ({ - locked: user.locked, - })); - - const fetchedStreams = VideoService.fetchVideoStreams(); - - const variables = isGridLayout - ? {} - : { - userIds: fetchedStreams.map((stream) => stream.userId) || [], - }; - - const { - data: videoUserSubscription, - } = useDeduplicatedSubscription(graphqlQuery, { variables }); - - const users = videoUserSubscription?.user || []; - - let streams = []; - let gridUsers = []; - let totalNumberOfStreams = 0; - - if (isGridLayout) { - streams = fetchedStreams; - gridUsers = VideoService.getGridUsers(videoUserSubscription?.user, fetchedStreams); - totalNumberOfStreams = fetchedStreams.length; - } else { - const { - streams: s, - totalNumberOfStreams: ts, - } = VideoService.getVideoStreams(); - streams = s; - - totalNumberOfStreams = ts; - } - - let usersVideo = streams; - - const { - defaultSorting: DEFAULT_SORTING, - } = window.meetingClientSettings.public.kurento.cameraSortingModes; - - if (gridUsers.length > 0) { - const items = usersVideo.concat(gridUsers); - usersVideo = sortVideoStreams(items, DEFAULT_SORTING); - } - - if (currentMeeting?.usersPolicies?.webcamsOnlyForModerator - && currentUser?.locked) { - if (users.length > 0) { - usersVideo = usersVideo.filter((uv) => { - if (uv.userId === currUserId) { - return true; - } - const user = users.find((u) => u.userId === uv.userId); - return user?.isModerator; - }); - } - } - - return { - swapLayout, - streams: usersVideo, - totalNumberOfStreams, - isUserLocked: VideoService.isUserLocked(), - currentVideoPageIndex: VideoService.getCurrentVideoPageIndex(), - isMeteorConnected: Meteor.status().connected, - users, - ...rest, - }; -})(VideoProviderContainer); - -export default VideoProviderContainerGraphql; diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/container.tsx b/bigbluebutton-html5/imports/ui/components/video-provider/container.tsx similarity index 100% rename from bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/container.tsx rename to bigbluebutton-html5/imports/ui/components/video-provider/container.tsx diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/hooks/index.ts b/bigbluebutton-html5/imports/ui/components/video-provider/hooks/index.ts similarity index 100% rename from bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/hooks/index.ts rename to bigbluebutton-html5/imports/ui/components/video-provider/hooks/index.ts diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/many-users-notify/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/many-users-notify/component.jsx deleted file mode 100644 index 251cc3283c..0000000000 --- a/bigbluebutton-html5/imports/ui/components/video-provider/many-users-notify/component.jsx +++ /dev/null @@ -1,101 +0,0 @@ -import React, { Component } from 'react'; -import { defineMessages, injectIntl } from 'react-intl'; -import { notify } from '/imports/ui/services/notification'; -import { toast } from 'react-toastify'; -import Styled from './styles'; - -const intlMessages = defineMessages({ - suggestLockTitle: { - id: 'app.video.suggestWebcamLock', - description: 'Label for notification title', - }, - suggestLockReason: { - id: 'app.video.suggestWebcamLockReason', - description: 'Reason for activate the webcams\'s lock', - }, - enable: { - id: 'app.video.enable', - description: 'Enable button label', - }, - cancel: { - id: 'app.video.cancel', - description: 'Cancel button label', - }, -}); - -const REPEAT_INTERVAL = 120000; - -class LockViewersNotifyComponent extends Component { - constructor(props) { - super(props); - this.interval = null; - this.intervalCallback = this.intervalCallback.bind(this); - } - - componentDidUpdate() { - const { - viewersInWebcam, - lockSettings, - limitOfViewersInWebcam, - webcamOnlyForModerator, - currentUserIsModerator, - limitOfViewersInWebcamIsEnable, - } = this.props; - const viwerersInWebcamGreaterThatLimit = (viewersInWebcam >= limitOfViewersInWebcam) - && limitOfViewersInWebcamIsEnable; - const webcamForViewersIsLocked = lockSettings.disableCam || webcamOnlyForModerator; - - if (viwerersInWebcamGreaterThatLimit - && !webcamForViewersIsLocked - && currentUserIsModerator - && !this.interval) { - this.interval = setInterval(this.intervalCallback, REPEAT_INTERVAL); - this.intervalCallback(); - } - if (webcamForViewersIsLocked || (!viwerersInWebcamGreaterThatLimit && this.interval)) { - clearInterval(this.interval); - this.interval = null; - } - } - - intervalCallback() { - const { - toggleWebcamsOnlyForModerator, - intl, - } = this.props; - const lockToastId = `suggestLock-${new Date().getTime()}`; - - notify( - ( - <> - {intl.formatMessage(intlMessages.suggestLockTitle)} - - - | - toast.dismiss(lockToastId)} - /> - - {intl.formatMessage(intlMessages.suggestLockReason)} - - ), - 'info', - 'rooms', - { - toastId: lockToastId, - }, - ); - } - - render() { - return null; - } -} - -export default injectIntl(LockViewersNotifyComponent); diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/many-users-notify/component.tsx b/bigbluebutton-html5/imports/ui/components/video-provider/many-users-notify/component.tsx similarity index 100% rename from bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/many-users-notify/component.tsx rename to bigbluebutton-html5/imports/ui/components/video-provider/many-users-notify/component.tsx diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/many-users-notify/container.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/many-users-notify/container.jsx deleted file mode 100644 index 0d286fd5d4..0000000000 --- a/bigbluebutton-html5/imports/ui/components/video-provider/many-users-notify/container.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import { withTracker } from 'meteor/react-meteor-data'; -import Meetings from '/imports/api/meetings'; -import Auth from '/imports/ui/services/auth'; -import Users from '/imports/api/users/'; -import VideoStreams from '/imports/api/video-streams'; -import { useMutation } from '@apollo/client'; -import ManyUsersComponent from './component'; -import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; -import { SET_WEBCAM_ONLY_FOR_MODERATOR } from '/imports/ui/components/lock-viewers/mutations'; - -const ManyUsersContainer = (props) => { - const { data: currentUserData } = useCurrentUser((user) => ({ - isModerator: user.isModerator, - })); - - const [setWebcamOnlyForModerator] = useMutation(SET_WEBCAM_ONLY_FOR_MODERATOR); - - const toggleWebcamsOnlyForModerator = () => { - setWebcamOnlyForModerator({ - variables: { - webcamsOnlyForModerator: true, - }, - }); - }; - - const currentUserIsModerator = currentUserData?.isModerator; - return ; -}; - -export default withTracker(() => { - const ROLE_VIEWER = window.meetingClientSettings.public.user.role_viewer; - - const meeting = Meetings.findOne({ - meetingId: Auth.meetingID, - }, { fields: { 'usersPolicies.webcamsOnlyForModerator': 1, lockSettings: 1 } }); - const videoStreams = VideoStreams.find({ meetingId: Auth.meetingID }, - { fields: { userId: 1 } }).fetch(); - const videoUsersIds = videoStreams.map(u => u.userId); - return { - viewersInWebcam: Users.find({ - meetingId: Auth.meetingID, - userId: { - $in: videoUsersIds, - }, - role: ROLE_VIEWER, - presenter: false, - }, { fields: {} }).count(), - lockSettings: meeting.lockSettings, - webcamOnlyForModerator: meeting.usersPolicies.webcamsOnlyForModerator, - limitOfViewersInWebcam: window.meetingClientSettings.public.app.viewersInWebcam, - limitOfViewersInWebcamIsEnable: window.meetingClientSettings - .public.app.enableLimitOfViewersInWebcam, - }; -})(ManyUsersContainer); diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/many-users-notify/container.tsx b/bigbluebutton-html5/imports/ui/components/video-provider/many-users-notify/container.tsx similarity index 100% rename from bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/many-users-notify/container.tsx rename to bigbluebutton-html5/imports/ui/components/video-provider/many-users-notify/container.tsx diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/many-users-notify/styles.js b/bigbluebutton-html5/imports/ui/components/video-provider/many-users-notify/styles.js deleted file mode 100644 index d41a18901d..0000000000 --- a/bigbluebutton-html5/imports/ui/components/video-provider/many-users-notify/styles.js +++ /dev/null @@ -1,44 +0,0 @@ -import styled from 'styled-components'; -import { colorPrimary } from '/imports/ui/stylesheets/styled-components/palette'; -import Button from '/imports/ui/components/common/button/component'; -import { headingsFontWeight } from '/imports/ui/stylesheets/styled-components/typography'; - -const Info = styled.p` - margin: 0; -`; - -const ButtonWrapper = styled.div` -display: flex; - flex-direction: row; - justify-content: space-between; - font-weight: ${headingsFontWeight}; - color: ${colorPrimary}; - - & > button { - padding: 0 0 0 .5rem; - } - background-color: inherit; - - &:focus,&:hover { - background-color: inherit; - } -`; - -const ManyUsersButton = styled(Button)` - flex: 0 1 48%; - color: ${colorPrimary}; - margin: 0; - font-weight: inherit; - - background-color: inherit; - - &:focus,&:hover { - background-color: inherit; - } -`; - -export default { - Info, - ButtonWrapper, - ManyUsersButton, -}; diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/many-users-notify/styles.ts b/bigbluebutton-html5/imports/ui/components/video-provider/many-users-notify/styles.ts similarity index 100% rename from bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/many-users-notify/styles.ts rename to bigbluebutton-html5/imports/ui/components/video-provider/many-users-notify/styles.ts diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/queries.ts b/bigbluebutton-html5/imports/ui/components/video-provider/queries.ts index 64009b2832..13793679bb 100644 --- a/bigbluebutton-html5/imports/ui/components/video-provider/queries.ts +++ b/bigbluebutton-html5/imports/ui/components/video-provider/queries.ts @@ -1,55 +1,119 @@ import { gql } from '@apollo/client'; -import { User } from '../../Types/user'; +import type { User } from './types'; -export interface getVideoDataResponse { - user: Array> +interface Voice { + floor: boolean; + lastFloorTime: string; } -export type queryUser = Pick -export const getVideoData = gql` -subscription getvideoData($userIds: [String]!) { - user(where: {userId: {_in: $userIds}}) { - loggedOut - away - disconnected - emoji - name - nameSortable - role - avatar - color - presenter - clientType - userId - raiseHand - isModerator - reactionEmoji +export interface VideoStreamsResponse { + user_camera: { + streamId: string; + user: User; + voice?: Voice; + }[]; +} + +export interface GridUsersResponse { + user: User[]; +} + +export interface OwnVideoStreamsResponse { + user_camera: { + streamId: string; + }[]; +} + +export const VIDEO_STREAMS_SUBSCRIPTION = gql` + subscription VideoStreams { + user_camera { + streamId + user { + name + userId + nameSortable + pinned + away + disconnected + emoji + role + avatar + color + presenter + clientType + raiseHand + isModerator + reactionEmoji + } + voice { + floor + lastFloorTime + } + } } -} `; -export const getVideoDataGrid = gql` -subscription getVideoDataGrid { - user { - loggedOut - away - disconnected - emoji - name - nameSortable - role - avatar - color - presenter - clientType - userId - raiseHand - reactionEmoji +export const OWN_VIDEO_STREAMS_QUERY = gql` + query OwnVideoStreams($userId: String!, $streamIdPrefix: String!) { + user_camera( + where: { + userId: { _eq: $userId }, + streamId: { _like: $streamIdPrefix } + }, + ) { + streamId + } + } +`; + +export const VIEWERS_IN_WEBCAM_COUNT_SUBSCRIPTION = gql` + subscription ViewerVideoStreams { + user_camera_aggregate(where: { + user: { role: { _eq: "VIEWER" }, presenter: { _eq: false } } + }) { + aggregate { + count + } + } + } +`; + +export const GRID_USERS_SUBSCRIPTION = gql` + subscription GridUsers($exceptUserIds: [String]!, $limit: Int!) { + user( + where: { + userId: { + _nin: $exceptUserIds, + }, + }, + limit: $limit, + order_by: { + nameSortable: asc, + userId: asc, + }, + ) { + name + userId + nameSortable + pinned + away + disconnected + emoji + role + avatar + color + presenter + clientType + raiseHand + isModerator + reactionEmoji + } } -} `; export default { - getVideoData, - getVideoDataGrid, + OWN_VIDEO_STREAMS_QUERY, + VIDEO_STREAMS_SUBSCRIPTION, + VIEWERS_IN_WEBCAM_COUNT_SUBSCRIPTION, + GRID_USERS_SUBSCRIPTION, }; diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/service.js b/bigbluebutton-html5/imports/ui/components/video-provider/service.js deleted file mode 100755 index 86cdae29d8..0000000000 --- a/bigbluebutton-html5/imports/ui/components/video-provider/service.js +++ /dev/null @@ -1,1105 +0,0 @@ -import { Tracker } from 'meteor/tracker'; -import Session from '/imports/ui/services/storage/in-memory'; -import { getSettingsSingletonInstance } from '/imports/ui/services/settings'; -import Auth from '/imports/ui/services/auth'; -import Meetings from '/imports/api/meetings'; -import Users from '/imports/api/users'; -import VideoStreams from '/imports/api/video-streams'; -import UserListService from '/imports/ui/components/user-list/service'; -import { meetingIsBreakout } from '/imports/ui/components/app/service'; -import { notify } from '/imports/ui/services/notification'; -import deviceInfo from '/imports/utils/deviceInfo'; -import browserInfo from '/imports/utils/browserInfo'; -import getFromUserSettings from '/imports/ui/services/users-settings'; -import VideoPreviewService from '../video-preview/service'; -import Storage from '/imports/ui/services/storage/session'; -import { getStorageSingletonInstance } from '/imports/ui/services/storage'; -import logger from '/imports/startup/client/logger'; -import { partition } from '/imports/utils/array-utils'; -import { - getSortingMethod, - sortVideoStreams, -} from '/imports/ui/components/video-provider/stream-sorting'; -import getFromMeetingSettings from '/imports/ui/services/meeting-settings'; - -const FILTER_VIDEO_STATS = [ - 'outbound-rtp', - 'inbound-rtp', -]; - -const TOKEN = '_'; - -class VideoService { - constructor() { - this.defineProperties({ - isConnecting: false, - isConnected: false, - currentVideoPageIndex: 0, - numberOfPages: 0, - pageSize: 0, - }); - this.userParameterProfile = null; - - this.isMobile = deviceInfo.isMobile; - this.isSafari = browserInfo.isSafari; - this.numberOfDevices = 0; - - this.record = null; - this.hackRecordViewer = null; - - // If the page isn't served over HTTPS there won't be mediaDevices - if (navigator.mediaDevices) { - this.updateNumberOfDevices = this.updateNumberOfDevices.bind(this); - // Safari doesn't support ondevicechange - if (!this.isSafari) { - navigator.mediaDevices.ondevicechange = event => this.updateNumberOfDevices(); - } - this.updateNumberOfDevices(); - } - - // FIXME this is abhorrent. Remove when peer lifecycle is properly decoupled - // from the React component's lifecycle. Any attempt at a half-baked - // decoupling will most probably generate problems - prlanzarin Dec 16 2021 - this.webRtcPeersRef = {}; - } - - defineProperties(obj) { - Object.keys(obj).forEach((key) => { - const privateKey = `_${key}`; - this[privateKey] = { - value: obj[key], - tracker: new Tracker.Dependency(), - }; - - Object.defineProperty(this, key, { - set: (value) => { - this[privateKey].value = value; - this[privateKey].tracker.changed(); - }, - get: () => { - this[privateKey].tracker.depend(); - return this[privateKey].value; - }, - }); - }); - } - - fetchNumberOfDevices(devices) { - const deviceIds = []; - devices.forEach(d => { - const validDeviceId = d.deviceId !== '' && !deviceIds.includes(d.deviceId) - if (d.kind === 'videoinput' && validDeviceId) { - deviceIds.push(d.deviceId); - } - }); - - return deviceIds.length; - } - - updateNumberOfDevices(devices = null) { - if (devices) { - this.numberOfDevices = this.fetchNumberOfDevices(devices); - } else { - navigator.mediaDevices.enumerateDevices().then(devices => { - this.numberOfDevices = this.fetchNumberOfDevices(devices); - }); - } - } - - joinVideo(deviceId) { - this.deviceId = deviceId; - this.isConnecting = true; - Storage.setItem('isFirstJoin', false); - } - - joinedVideo() { - this.isConnected = true; - } - - storeDeviceIds() { - const streams = VideoStreams.find( - { - meetingId: Auth.meetingID, - userId: Auth.userID, - }, { fields: { deviceId: 1 } }, - ).fetch(); - - let deviceIds = []; - streams.forEach(s => { - deviceIds.push(s.deviceId); - } - ); - Session.setItem('deviceIds', deviceIds.join()); - } - - exitVideo(sendUserUnshareWebcam) { - if (this.isConnected) { - logger.info({ - logCode: 'video_provider_unsharewebcam', - }, `Sending unshare all ${Auth.userID} webcams notification to meteor`); - const streams = VideoStreams.find( - { - meetingId: Auth.meetingID, - userId: Auth.userID, - }, { fields: { stream: 1 } }, - ).fetch(); - - streams.forEach(s => sendUserUnshareWebcam(s.stream)); - this.exitedVideo(); - } - } - - exitedVideo() { - this.isConnecting = false; - this.deviceId = null; - this.isConnected = false; - } - - stopVideo(cameraId, sendUserUnshareWebcam) { - const streams = VideoStreams.find( - { - meetingId: Auth.meetingID, - userId: Auth.userID, - }, { fields: { stream: 1 } }, - ).fetch(); - - const hasTargetStream = streams.some(s => s.stream === cameraId); - const hasOtherStream = streams.some(s => s.stream !== cameraId); - - // Check if the target (cameraId) stream exists in the remote collection. - // If it does, means it was successfully shared. So do the full stop procedure. - if (hasTargetStream) { - sendUserUnshareWebcam(cameraId); - } - - if (!hasOtherStream) { - // There's no other remote stream, meaning (OR) - // a) This was effectively the last webcam being unshared - // b) This was a connecting stream timing out (not effectively shared) - // For both cases, we clean everything up. - this.exitedVideo(); - } else { - // It was not the last webcam the user had successfully shared, - // nor was cameraId present in the server collection. - // Hence it's a connecting stream (not effectively shared) which timed out - this.stopConnectingStream(); - } - } - - getSharedDevices() { - const devices = VideoStreams.find( - { - meetingId: Auth.meetingID, - userId: Auth.userID, - }, { fields: { deviceId: 1 } }, - ).fetch().map(vs => vs.deviceId); - - return devices; - } - - getAuthenticatedURL() { - const SFU_URL = window.meetingClientSettings.public.kurento.wsUrl; - return Auth.authenticateURL(SFU_URL); - } - - getCameraProfiles() { - const CAMERA_PROFILES = window.meetingClientSettings.public.kurento.cameraProfiles; - return CAMERA_PROFILES; - }; - - getRoleModerator() { - const ROLE_MODERATOR = window.meetingClientSettings.public.user.role_moderator; - return ROLE_MODERATOR; - }; - - getRoleViewer() { - const ROLE_VIEWER = window.meetingClientSettings.public.user.role_viewer; - return ROLE_VIEWER; - }; - - getPageChangeDebounceTime() { - const { - pageChangeDebounceTime: PAGE_CHANGE_DEBOUNCE_TIME, - } = window.meetingClientSettings.public.kurento.pagination; - - return PAGE_CHANGE_DEBOUNCE_TIME; - } - - shouldRenderPaginationToggle() { - const { - paginationToggleEnabled: PAGINATION_TOGGLE_ENABLED, - } = window.meetingClientSettings.public.kurento.pagination; - // Only enable toggle if configured to do so and if we have a page size properly setup - return PAGINATION_TOGGLE_ENABLED && (this.getMyPageSize() > 0); - } - - isPaginationEnabled () { - const Settings = getSettingsSingletonInstance(); - return Settings.application.paginationEnabled && (this.getMyPageSize() > 0); - } - - setNumberOfPages (numberOfPublishers, numberOfSubscribers, pageSize) { - // Page size 0 means no pagination, return itself - if (pageSize === 0) return 0; - - // Page size refers only to the number of subscribers. Publishers are always - // shown, hence not accounted for - const nofPages = Math.ceil(numberOfSubscribers / pageSize); - - if (nofPages !== this.numberOfPages) { - this.numberOfPages = nofPages; - // Check if we have to page back on the current video page index due to a - // page ceasing to exist - if (nofPages === 0) { - this.currentVideoPageIndex = 0; - } else if ((this.currentVideoPageIndex + 1) > this.numberOfPages) { - this.getPreviousVideoPage(); - } - } - - return this.numberOfPages; - } - - getNumberOfPages () { - return this.numberOfPages; - } - - setCurrentVideoPageIndex (newVideoPageIndex) { - if (this.currentVideoPageIndex !== newVideoPageIndex) { - this.currentVideoPageIndex = newVideoPageIndex; - } - } - - getCurrentVideoPageIndex () { - return this.currentVideoPageIndex; - } - - calculateNextPage () { - if (this.numberOfPages === 0) { - return 0; - } - - return ((this.currentVideoPageIndex + 1) % this.numberOfPages + this.numberOfPages) % this.numberOfPages; - } - - calculatePreviousPage () { - if (this.numberOfPages === 0) { - return 0; - } - - return ((this.currentVideoPageIndex - 1) % this.numberOfPages + this.numberOfPages) % this.numberOfPages; - } - - getNextVideoPage() { - const nextPage = this.calculateNextPage(); - this.setCurrentVideoPageIndex(nextPage); - - return this.currentVideoPageIndex; - } - - getPreviousVideoPage() { - const previousPage = this.calculatePreviousPage(); - this.setCurrentVideoPageIndex(previousPage); - - return this.currentVideoPageIndex; - } - - getPageSizeDictionary () { - const { - desktopPageSizes: DESKTOP_PAGE_SIZES, - mobilePageSizes: MOBILE_PAGE_SIZES, - } = window.meetingClientSettings.public.kurento.pagination; - - const PAGINATION_THRESHOLDS_CONF = window.meetingClientSettings.public.kurento.paginationThresholds; - const PAGINATION_THRESHOLDS = PAGINATION_THRESHOLDS_CONF.thresholds.sort((t1, t2) => t1.users - t2.users); - const PAGINATION_THRESHOLDS_ENABLED = PAGINATION_THRESHOLDS_CONF.enabled; - - // Dynamic page sizes are disabled. Fetch the stock page sizes. - if (!PAGINATION_THRESHOLDS_ENABLED || PAGINATION_THRESHOLDS.length <= 0) { - return !this.isMobile ? DESKTOP_PAGE_SIZES : MOBILE_PAGE_SIZES; - } - - // Dynamic page sizes are enabled. Get the user count, isolate the - // matching threshold entry, return the val. - let targetThreshold; - const userCount = UserListService.getUserCount(); - const processThreshold = (threshold = { - desktopPageSizes: DESKTOP_PAGE_SIZES, - mobilePageSizes: MOBILE_PAGE_SIZES - }) => { - // We don't demand that all page sizes should be set in pagination profiles. - // That saves us some space because don't necessarily need to scale mobile - // endpoints. - // If eg mobile isn't set, then return the default value. - if (!this.isMobile) { - return threshold.desktopPageSizes || DESKTOP_PAGE_SIZES; - } else { - return threshold.mobilePageSizes || MOBILE_PAGE_SIZES; - } - }; - - // Short-circuit: no threshold yet, return stock values (processThreshold has a default arg) - if (userCount < PAGINATION_THRESHOLDS[0].users) return processThreshold(); - - // Reverse search for the threshold where our participant count is directly equal or great - // The PAGINATION_THRESHOLDS config is sorted when imported. - for (let mapIndex = PAGINATION_THRESHOLDS.length - 1; mapIndex >= 0; --mapIndex) { - targetThreshold = PAGINATION_THRESHOLDS[mapIndex]; - if (targetThreshold.users <= userCount) { - return processThreshold(targetThreshold); - } - } - } - - setPageSize (size) { - if (this.pageSize !== size) { - this.pageSize = size; - } - - return this.pageSize; - } - - getMyPageSize () { - let size; - const myRole = this.getMyRole(); - const pageSizes = this.getPageSizeDictionary(); - const ROLE_MODERATOR = this.getRoleModerator(); - const ROLE_VIEWER = this.getRoleViewer(); - switch (myRole) { - case ROLE_MODERATOR: - size = pageSizes.moderator; - break; - case ROLE_VIEWER: - default: - size = pageSizes.viewer - } - - return this.setPageSize(size); - } - - getGridSize () { - let size; - const myRole = this.getMyRole(); - const { - desktopGridSizes: DESKTOP_GRID_SIZES, - mobileGridSizes: MOBILE_GRID_SIZES, - } = window.meetingClientSettings.public.kurento.pagination; - - const gridSizes = !this.isMobile ? DESKTOP_GRID_SIZES : MOBILE_GRID_SIZES; - const ROLE_MODERATOR = this.getRoleModerator(); - const ROLE_VIEWER = this.getRoleViewer(); - switch (myRole) { - case ROLE_MODERATOR: - size = gridSizes.moderator; - break; - case ROLE_VIEWER: - default: - size = gridSizes.viewer - } - - return size; - } - - getVideoPage (streams, pageSize) { - // Publishers are taken into account for the page size calculations. They - // also appear on every page. Same for pinned user. - - const { - paginationSorting: PAGINATION_SORTING, - defaultSorting: DEFAULT_SORTING, - } = window.meetingClientSettings.public.kurento.cameraSortingModes; - - const [filtered, others] = partition(streams, (vs) => Auth.userID === vs.userId || vs.pin); - - // Separate pin from local cameras - const [pin, mine] = partition(filtered, (vs) => vs.pin); - - // Recalculate total number of pages - this.setNumberOfPages(filtered.length, others.length, pageSize); - const chunkIndex = this.currentVideoPageIndex * pageSize; - - // This is an extra check because pagination is globally in effect (hard - // limited page sizes, toggles on), but we might still only have one page. - // Use the default sorting method if that's the case. - const sortingMethod = (this.numberOfPages > 1) ? PAGINATION_SORTING : DEFAULT_SORTING; - const paginatedStreams = sortVideoStreams(others, sortingMethod) - .slice(chunkIndex, (chunkIndex + pageSize)) || []; - - if (getSortingMethod(sortingMethod).localFirst) { - return [...pin, ...mine, ...paginatedStreams]; - } - return [...pin, ...paginatedStreams, ...mine]; - } - - getUsersIdFromVideoStreams() { - const usersId = VideoStreams.find( - { meetingId: Auth.meetingID }, - { fields: { userId: 1 } }, - ).fetch().map(user => user.userId); - - return usersId; - } - - getVideoPinByUser(userId) { - const user = Users.findOne({ userId }, { fields: { pin: 1 } }); - - return user?.pin || false; - } - - isGridEnabled() { - return Session.get('isGridEnabled'); - } - - getVideoStreams() { - const pageSize = this.getMyPageSize(); - const isPaginationDisabled = !this.isPaginationEnabled() || pageSize === 0; - const { - paginationSorting: PAGINATION_SORTING, - defaultSorting: DEFAULT_SORTING, - } = window.meetingClientSettings.public.kurento.cameraSortingModes; - - const { neededDataTypes } = isPaginationDisabled - ? getSortingMethod(DEFAULT_SORTING) - : getSortingMethod(PAGINATION_SORTING); - const isGridEnabled = this.isGridEnabled(); - let gridUsers = []; - let users = []; - - if (isGridEnabled) { - users = Users.find( - { meetingId: Auth.meetingID }, - { fields: { loggedOut: 1, left: 1, ...neededDataTypes} }, - ).fetch(); - } - - let streams = VideoStreams.find( - { meetingId: Auth.meetingID }, - { fields: neededDataTypes }, - ).fetch(); - - // Data savings enabled will only show local streams - const Settings = getSettingsSingletonInstance(); - const { viewParticipantsWebcams } = Settings.dataSaving; - if (!viewParticipantsWebcams) streams = this.filterLocalOnly(streams); - - const connectingStream = this.getConnectingStream(streams); - if (connectingStream) streams.push(connectingStream); - - // Pagination is either explicitly disabled or pagination is set to 0 (which - // is equivalent to disabling it), so return the mapped streams as they are - // which produces the original non paginated behaviour - if (isPaginationDisabled) { - if (isGridEnabled) { - const streamUsers = streams.map((stream) => stream.userId); - - gridUsers = users.filter( - (user) => !user.loggedOut && !user.left && !streamUsers.includes(user.userId) - ).map((user) => ({ - isGridItem: true, - ...user, - })); - } - - return { - streams: sortVideoStreams(streams, DEFAULT_SORTING), - gridUsers, - totalNumberOfStreams: streams.length - }; - } - - const paginatedStreams = this.getVideoPage(streams, pageSize); - - if (isGridEnabled) { - const streamUsers = paginatedStreams.map((stream) => stream.userId); - - gridUsers = users.filter( - (user) => !user.loggedOut && !user.left && !streamUsers.includes(user.userId) - ).map((user) => ({ - isGridItem: true, - ...user, - })); - } - - return { streams: paginatedStreams, gridUsers, totalNumberOfStreams: streams.length }; - } - - fetchVideoStreams() { - const pageSize = this.getMyPageSize(); - const isPaginationDisabled = !this.isPaginationEnabled() || pageSize === 0; - - let streams = [...VideoStreams.find( - { meetingId: Auth.meetingID }, - ).fetch()]; - - const Settings = getSettingsSingletonInstance(); - const { viewParticipantsWebcams } = Settings.dataSaving; - if (!viewParticipantsWebcams) streams = this.filterLocalOnly(streams); - - const connectingStream = this.getConnectingStream(streams); - if (connectingStream) { - streams.push(connectingStream); - } - - if (!isPaginationDisabled) { - return this.getVideoPage(streams, pageSize); - } - - return streams; - } - - getGridUsers(users, streams) { - const isGridEnabled = this.isGridEnabled(); - const gridSize = this.getGridSize(); - - let gridUsers = []; - - if (isGridEnabled) { - const streamUsers = streams.map((stream) => stream.userId); - - gridUsers = users.filter( - (user) => !user.loggedOut && !user.left && !streamUsers.includes(user.userId), - ).map((user) => ({ - isGridItem: true, - ...user, - })).slice(0, gridSize - streams.length); - } - return gridUsers; - } - - stopConnectingStream() { - this.deviceId = null; - this.isConnecting = false; - } - - getConnectingStream(streams) { - let connectingStream; - - if (this.isConnecting) { - if (this.deviceId) { - const stream = this.buildStreamName(Auth.userID, this.deviceId); - if (!this.hasStream(streams, stream) && !this.isUserLocked()) { - connectingStream = { - stream, - userId: Auth.userID, - name: Auth.fullname, - }; - } else { - // Connecting stream is already stored at database - this.stopConnectingStream(); - } - } else { - logger.error({ - logCode: 'video_provider_missing_deviceid', - }, 'Could not retrieve a valid deviceId'); - } - } - - return connectingStream; - } - - buildStreamName(userId, deviceId) { - return `${userId}${TOKEN}${deviceId}`; - } - - hasVideoStream() { - const videoStreams = VideoStreams.findOne({ userId: Auth.userID }, - { fields: {} }); - return !!videoStreams; - } - - hasStream(streams, stream) { - return streams.find(s => s.stream === stream); - } - - getMediaServerAdapter() { - const DEFAULT_VIDEO_MEDIA_SERVER = window.meetingClientSettings.public.kurento.videoMediaServer; - return getFromMeetingSettings('media-server-video', DEFAULT_VIDEO_MEDIA_SERVER); - } - - getMyRole () { - return Users.findOne({ userId: Auth.userID }, - { fields: { role: 1 } })?.role; - } - - getRecord() { - if (this.record === null) { - this.record = getFromUserSettings('bbb_record_video', true); - } - - // TODO: Remove this - // This is a hack to handle a missing piece at the backend of a particular deploy. - // If, at the time the video is shared, the user has a viewer role and - // meta_hack-record-viewer-video is 'false' this user won't have this video - // stream recorded. - if (this.hackRecordViewer === null) { - const value = getFromMeetingSettings('hack-record-viewer-video', null); - this.hackRecordViewer = value ? value.toLowerCase() === 'true' : true; - } - - const hackRecord = this.getMyRole() === this.getRoleModerator() || this.hackRecordViewer; - - return this.record && hackRecord; - } - - filterModeratorOnly(streams) { - const ROLE_MODERATOR = this.getRoleModerator(); - const ROLE_VIEWER = this.getRoleViewer(); - const amIViewer = this.getMyRole() === ROLE_VIEWER; - - if (amIViewer) { - const moderators = Users.find( - { - role: ROLE_MODERATOR, - }, - { fields: { userId: 1 } }, - ).fetch().map(user => user.userId); - - return streams.reduce((result, stream) => { - const { userId } = stream; - - const isModerator = moderators.includes(userId); - const isMe = Auth.userID === userId; - - if (isModerator || isMe) result.push(stream); - - return result; - }, []); - } - return streams; - } - - filterLocalOnly(streams) { - return streams.filter(stream => stream.userId === Auth.userID); - } - - disableCam() { - const m = Meetings.findOne({ meetingId: Auth.meetingID }, - { fields: { 'lockSettings.disableCam': 1 } }); - return m.lockSettings ? m.lockSettings.disableCam : false; - } - - webcamsOnlyForModerator() { - const ROLE_MODERATOR = this.getRoleModerator(); - const meeting = Meetings.findOne({ meetingId: Auth.meetingID }, - { fields: { 'usersPolicies.webcamsOnlyForModerator': 1 } }); - const user = Users.findOne({ userId: Auth.userID }, { fields: { locked: 1, role: 1 } }); - - if (meeting?.usersPolicies && user?.role !== ROLE_MODERATOR && user?.locked) { - return meeting.usersPolicies.webcamsOnlyForModerator; - } - return false; - } - - hasCapReached() { - const meeting = Meetings.findOne( - { meetingId: Auth.meetingID }, - { - fields: { - meetingCameraCap: 1, - 'usersPolicies.userCameraCap': 1, - }, - }, - ); - - // If the meeting prop data is unreachable, force a safe return - if ( - meeting?.usersPolicies === undefined - || !meeting?.meetingCameraCap === undefined - ) return true; - const { meetingCameraCap } = meeting; - const { userCameraCap } = meeting.usersPolicies; - - const meetingCap = meetingCameraCap !== 0 && this.getVideoStreamsCount() >= meetingCameraCap; - const userCap = userCameraCap !== 0 && this.getLocalVideoStreamsCount() >= userCameraCap; - - return meetingCap || userCap; - } - - getVideoStreamsCount() { - const streams = VideoStreams.find({}).count(); - - return streams; - } - - getLocalVideoStreamsCount() { - const localStreams = VideoStreams.find( - { userId: Auth.userID } - ).count(); - - return localStreams; - } - - getInfo() { - const m = Meetings.findOne({ meetingId: Auth.meetingID }, - { fields: { 'voiceSettings.voiceConf': 1 } }); - const voiceBridge = m.voiceSettings ? m.voiceSettings.voiceConf : null; - return { - userId: Auth.userID, - userName: Auth.fullname, - meetingId: Auth.meetingID, - sessionToken: Auth.sessionToken, - voiceBridge, - }; - } - - mirrorOwnWebcam(userId = null) { - // only true if setting defined and video ids match - const MIRROR_WEBCAM = window.meetingClientSettings.public.app.mirrorOwnWebcam; - const isOwnWebcam = userId ? Auth.userID === userId : true; - const isEnabledMirroring = getFromUserSettings('bbb_mirror_own_webcam', MIRROR_WEBCAM); - return isOwnWebcam && isEnabledMirroring; - } - - isPinEnabled() { - const PIN_WEBCAM = window.meetingClientSettings.public.kurento.enableVideoPin; - return PIN_WEBCAM; - } - - // In user-list it is necessary to check if the user is sharing his webcam - isVideoPinEnabledForCurrentUser(isModerator) { - const isBreakout = meetingIsBreakout(); - const isPinEnabled = this.isPinEnabled(); - - return !!(isModerator - && isPinEnabled - && !isBreakout); - } - - getMyStreamId(deviceId) { - const videoStream = VideoStreams.findOne( - { - meetingId: Auth.meetingID, - userId: Auth.userID, - deviceId, - }, { fields: { stream: 1 } }, - ); - return videoStream ? videoStream.stream : null; - } - - isUserLocked() { - const ROLE_MODERATOR = this.getRoleModerator(); - return !!Users.findOne({ - userId: Auth.userID, - locked: true, - role: { $ne: ROLE_MODERATOR }, - }, { fields: {} }) && this.disableCam(); - } - - lockUser(sendUserUnshareWebcam) { - if (this.isConnected) { - this.exitVideo(sendUserUnshareWebcam); - } - } - - isLocalStream(cameraId) { - return cameraId?.startsWith(Auth.userID); - } - - playStart(cameraId) { - if (this.isLocalStream(cameraId)) { - this.sendUserShareWebcam(cameraId); - this.joinedVideo(); - } - } - - getCameraProfile() { - const BBBStorage = getStorageSingletonInstance(); - const CAMERA_PROFILES = this.getCameraProfiles(); - const profileId = BBBStorage.getItem('WebcamProfileId') || ''; - const cameraProfile = CAMERA_PROFILES.find(profile => profile.id === profileId) - || CAMERA_PROFILES.find(profile => profile.default) - || CAMERA_PROFILES[0]; - const deviceId = BBBStorage.getItem('WebcamDeviceId'); - if (deviceId) { - cameraProfile.constraints = cameraProfile.constraints || {}; - cameraProfile.constraints.deviceId = { exact: deviceId }; - } - - return cameraProfile; - } - - addCandidateToPeer(peer, candidate, cameraId) { - peer.addIceCandidate(candidate).catch((error) => { - if (error) { - // Just log the error. We can't be sure if a candidate failure on add is - // fatal or not, so that's why we have a timeout set up for negotiations - // and listeners for ICE state transitioning to failures, so we won't - // act on it here - logger.error({ - logCode: 'video_provider_addicecandidate_error', - extraInfo: { - cameraId, - error, - }, - }, `Adding ICE candidate failed for ${cameraId} due to ${error.message}`); - } - }); - } - - processInboundIceQueue(peer, cameraId) { - while (peer.inboundIceQueue.length) { - const candidate = peer.inboundIceQueue.shift(); - this.addCandidateToPeer(peer, candidate, cameraId); - } - } - - onBeforeUnload(sendUserUnshareWebcam) { - this.exitVideo(sendUserUnshareWebcam); - } - - getStatus() { - if (this.isConnecting) return 'videoConnecting'; - if (this.isConnected) return 'connected'; - return 'disconnected'; - } - - disableReason() { - const locks = { - videoLocked: this.isUserLocked(), - camCapReached: this.hasCapReached() && !this.hasVideoStream(), - meteorDisconnected: !Meteor.status().connected - }; - const locksKeys = Object.keys(locks); - const disableReason = locksKeys.filter( i => locks[i]).shift(); - return disableReason ? disableReason : false; - } - - getRole(isLocal) { - return isLocal ? 'share' : 'viewer'; - } - - getUserParameterProfile() { - const CAMERA_PROFILES = this.getCameraProfiles(); - if (this.userParameterProfile === null) { - this.userParameterProfile = getFromUserSettings( - 'bbb_preferred_camera_profile', - (CAMERA_PROFILES.find(i => i.default) || {}).id || null, - ); - } - - return this.userParameterProfile; - } - - isMultipleCamerasEnabled() { - // Multiple cameras shouldn't be enabled with video preview skipping - // Mobile shouldn't be able to share more than one camera at the same time - // Safari needs to implement devicechange event for safe device control - const MULTIPLE_CAMERAS = window.meetingClientSettings.public.app.enableMultipleCameras; - return MULTIPLE_CAMERAS - && !VideoPreviewService.getSkipVideoPreview() - && !this.isMobile - && !this.isSafari - && this.numberOfDevices > 1; - } - - isProfileBetter (newProfileId, originalProfileId) { - const CAMERA_PROFILES = this.getCameraProfiles(); - return CAMERA_PROFILES.findIndex(({ id }) => id === newProfileId) - > CAMERA_PROFILES.findIndex(({ id }) => id === originalProfileId); - } - - applyBitrate (peer, bitrate) { - const peerConnection = peer.peerConnection; - if ('RTCRtpSender' in window - && 'setParameters' in window.RTCRtpSender.prototype - && 'getParameters' in window.RTCRtpSender.prototype) { - peerConnection.getSenders().forEach(sender => { - const { track } = sender; - if (track && track.kind === 'video') { - const parameters = sender.getParameters(); - const normalizedBitrate = bitrate * 1000; - - // The encoder parameters might not be up yet; if that's the case, - // add a filler object so we can alter the parameters anyways - if (parameters.encodings == null || parameters.encodings.length === 0) { - parameters.encodings = [{}]; - } - - // Only reset bitrate if it changed in some way to avoid encoder fluctuations - if (parameters.encodings[0].maxBitrate !== normalizedBitrate) { - parameters.encodings[0].maxBitrate = normalizedBitrate; - sender.setParameters(parameters) - .then(() => { - logger.info({ - logCode: 'video_provider_bitratechange', - extraInfo: { bitrate }, - }, `Bitrate changed: ${bitrate}`); - }) - .catch(error => { - logger.warn({ - logCode: 'video_provider_bitratechange_failed', - extraInfo: { bitrate, errorMessage: error.message, errorCode: error.code }, - }, `Bitrate change failed.`); - }); - } - } - }) - } - } - - // Some browsers (mainly iOS Safari) garble the stream if a constraint is - // reconfigured without propagating previous height/width info - reapplyResolutionIfNeeded (track, constraints) { - if (typeof track.getSettings !== 'function') { - return constraints; - } - - const trackSettings = track.getSettings(); - - if (trackSettings.width && trackSettings.height) { - return { - ...constraints, - width: trackSettings.width, - height: trackSettings.height, - }; - } - - return constraints; - } - - getThreshold (numberOfPublishers) { - const { - thresholds: CAMERA_QUALITY_THRESHOLDS = [], - } = window.meetingClientSettings.public.kurento.cameraQualityThresholds; - - let targetThreshold = { threshold: 0, profile: 'original' }; - let finalThreshold = { threshold: 0, profile: 'original' }; - - for(let mapIndex = 0; mapIndex < CAMERA_QUALITY_THRESHOLDS.length; mapIndex++) { - targetThreshold = CAMERA_QUALITY_THRESHOLDS[mapIndex]; - if (targetThreshold.threshold <= numberOfPublishers) { - finalThreshold = targetThreshold; - } - } - - return finalThreshold; - } - - getPreloadedStream () { - if (this.deviceId == null) return; - return VideoPreviewService.getStream(this.deviceId); - } - - /** - * Get all active video peers. - * @returns An Object containing the reference for all active peers peers - */ - getActivePeers() { - const videoData = this.getVideoStreams(); - - if (!videoData) return null; - - const { streams: activeVideoStreams } = videoData; - - if (!activeVideoStreams) return null; - - const activePeers = {}; - - activeVideoStreams.forEach((stream) => { - if (this.webRtcPeersRef[stream.stream]) { - activePeers[stream.stream] = this.webRtcPeersRef[stream.stream].peerConnection; - } - }); - - return activePeers; - } - - /** - * Get stats about all active video peer. - * We filter the status based on FILTER_VIDEO_STATS constant. - * - * For more information see: - * https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/getStats - * and - * https://developer.mozilla.org/en-US/docs/Web/API/RTCStatsReport - * @returns An Object containing the information about each active peer. - * The returned object follows the format: - * { - * peerId: RTCStatsReport - * } - */ - async getStats() { - const peers = this.getActivePeers(); - - if (!peers) return null; - - const stats = {}; - - await Promise.all( - Object.keys(peers).map(async (peerId) => { - const peerStats = await peers[peerId].getStats(); - - const videoStats = {}; - - peerStats.forEach((stat) => { - if (FILTER_VIDEO_STATS.includes(stat.type)) { - videoStats[stat.type] = stat; - } - }); - stats[peerId] = videoStats; - }) - ); - - return stats; - } - - updatePeerDictionaryReference(newRef) { - this.webRtcPeersRef = newRef; - } -} - -const videoService = new VideoService(); - -export default { - storeDeviceIds: () => videoService.storeDeviceIds(), - exitVideo: (sendUserUnshareWebcam) => videoService.exitVideo(sendUserUnshareWebcam), - joinVideo: deviceId => videoService.joinVideo(deviceId), - stopVideo: (cameraId, sendUserUnshareWebcam) => videoService.stopVideo( - cameraId, - sendUserUnshareWebcam, - ), - getVideoStreams: () => videoService.getVideoStreams(), - getInfo: () => videoService.getInfo(), - getMyStreamId: deviceId => videoService.getMyStreamId(deviceId), - isUserLocked: () => videoService.isUserLocked(), - lockUser: (sendUserUnshareWebcam) => videoService.lockUser(sendUserUnshareWebcam), - getAuthenticatedURL: () => videoService.getAuthenticatedURL(), - isLocalStream: cameraId => videoService.isLocalStream(cameraId), - hasVideoStream: () => videoService.hasVideoStream(), - getStatus: () => videoService.getStatus(), - disableReason: () => videoService.disableReason(), - playStart: cameraId => videoService.playStart(cameraId), - getCameraProfile: () => videoService.getCameraProfile(), - addCandidateToPeer: (peer, candidate, cameraId) => videoService.addCandidateToPeer(peer, candidate, cameraId), - processInboundIceQueue: (peer, cameraId) => videoService.processInboundIceQueue(peer, cameraId), - getRole: isLocal => videoService.getRole(isLocal), - getMediaServerAdapter: () => videoService.getMediaServerAdapter(), - getRecord: () => videoService.getRecord(), - getSharedDevices: () => videoService.getSharedDevices(), - getUserParameterProfile: () => videoService.getUserParameterProfile(), - isMultipleCamerasEnabled: () => videoService.isMultipleCamerasEnabled(), - mirrorOwnWebcam: userId => videoService.mirrorOwnWebcam(userId), - hasCapReached: () => videoService.hasCapReached(), - onBeforeUnload: (sendUserUnshareWebcam) => videoService.onBeforeUnload(sendUserUnshareWebcam), - notify: message => notify(message, 'error', 'video'), - updateNumberOfDevices: devices => videoService.updateNumberOfDevices(devices), - getThreshold: (numberOfPublishers) => videoService.getThreshold(numberOfPublishers), - isPaginationEnabled: () => videoService.isPaginationEnabled(), - getNumberOfPages: () => videoService.getNumberOfPages(), - getCurrentVideoPageIndex: () => videoService.getCurrentVideoPageIndex(), - getPreviousVideoPage: () => videoService.getPreviousVideoPage(), - getNextVideoPage: () => videoService.getNextVideoPage(), - getPageChangeDebounceTime: () => videoService.getPageChangeDebounceTime(), - getUsersIdFromVideoStreams: () => videoService.getUsersIdFromVideoStreams(), - shouldRenderPaginationToggle: () => videoService.shouldRenderPaginationToggle(), - getVideoPinByUser: (userId) => videoService.getVideoPinByUser(userId), - isVideoPinEnabledForCurrentUser: (user) => videoService.isVideoPinEnabledForCurrentUser(user), - isPinEnabled: () => videoService.isPinEnabled(), - getPreloadedStream: () => videoService.getPreloadedStream(), - getStats: () => videoService.getStats(), - updatePeerDictionaryReference: (newRef) => videoService.updatePeerDictionaryReference(newRef), - joinedVideo: () => videoService.joinedVideo(), - fetchVideoStreams: () => videoService.fetchVideoStreams(), - getGridUsers: (users = [], streams = []) => videoService.getGridUsers(users, streams), - webcamsOnlyForModerators: () => videoService.webcamsOnlyForModerator(), -}; diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/service.ts b/bigbluebutton-html5/imports/ui/components/video-provider/service.ts similarity index 100% rename from bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/service.ts rename to bigbluebutton-html5/imports/ui/components/video-provider/service.ts diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/state.ts b/bigbluebutton-html5/imports/ui/components/video-provider/state.ts similarity index 100% rename from bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/state.ts rename to bigbluebutton-html5/imports/ui/components/video-provider/state.ts diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/stream-sorting.js b/bigbluebutton-html5/imports/ui/components/video-provider/stream-sorting.js deleted file mode 100644 index dcee88b9dc..0000000000 --- a/bigbluebutton-html5/imports/ui/components/video-provider/stream-sorting.js +++ /dev/null @@ -1,140 +0,0 @@ -import UserListService from '/imports/ui/components/user-list/service'; -import Auth from '/imports/ui/services/auth'; - -const DEFAULT_SORTING_MODE = 'LOCAL_ALPHABETICAL'; - -// pin first -export const sortPin = (s1, s2) => { - if (s1.pin) { - return -1; - } if (s2.pin) { - return 1; - } - return 0; -}; - -export const mandatorySorting = (s1, s2) => sortPin(s1, s2); - -// lastFloorTime, descending -export const sortVoiceActivity = (s1, s2) => { - if (s2.lastFloorTime < s1.lastFloorTime) { - return -1; - } else if (s2.lastFloorTime > s1.lastFloorTime) { - return 1; - } else return 0; -}; - -// pin -> lastFloorTime (descending) -> alphabetical -> local -export const sortVoiceActivityLocal = (s1, s2) => { - if (s1.userId === Auth.userID) { - return 1; - } if (s2.userId === Auth.userID) { - return -1; - } - - return mandatorySorting(s1, s2) - || sortVoiceActivity(s1, s2) - || UserListService.sortUsersByName(s1, s2); -}; - -// pin -> local -> lastFloorTime (descending) -> alphabetical -export const sortLocalVoiceActivity = (s1, s2) => mandatorySorting(s1, s2) - || UserListService.sortUsersByCurrent(s1, s2) - || sortVoiceActivity(s1, s2) - || UserListService.sortUsersByName(s1, s2); - -// pin -> local -> alphabetic -export const sortLocalAlphabetical = (s1, s2) => mandatorySorting(s1, s2) - || UserListService.sortUsersByCurrent(s1, s2) - || UserListService.sortUsersByName(s1, s2); - -export const sortPresenter = (s1, s2) => { - if (UserListService.isUserPresenter(s1.userId)) { - return -1; - } else if (UserListService.isUserPresenter(s2.userId)) { - return 1; - } else return 0; -}; - -// pin -> local -> presenter -> alphabetical -export const sortLocalPresenterAlphabetical = (s1, s2) => mandatorySorting(s1, s2) - || UserListService.sortUsersByCurrent(s1, s2) - || sortPresenter(s1, s2) - || UserListService.sortUsersByName(s1, s2); - -// SORTING_METHODS: registrar of configurable video stream sorting modes -// Keys are the method name (String) which are to be configured in settings.yml -// ${streamSortingMethod} flag. -// -// Values are a objects which describe the sorting mode: -// - sortingMethod (function): a sorting function defined in this module -// - neededData (Object): data members that will be fetched from the server's -// video-streams collection -// - filter (Boolean): whether the sorted stream list has to be post processed -// to remove uneeded attributes. The needed attributes are: userId, streams -// and name. Anything other than that is superfluous. -// - localFirst (Boolean): true pushes local streams to the beginning of the list, -// false to the end -// The reason why this flags exists is due to pagination: local streams are -// stripped out of the streams list prior to sorting+partiotioning. They're -// added (pushed) afterwards. To avoid re-sorting the page, this flag indicates -// where it should go. -// -// To add a new sorting flavor: -// 1 - implement a sorting function, add it here (like eg sortPresenterAlphabetical) -// 1.1.: the sorting function has the same behaviour as a regular .sort callback -// 2 - add an entry to SORTING_METHODS, the key being the name to be used -// in settings.yml and the value object like the aforementioned -const MANDATORY_DATA_TYPES = { - userId: 1, stream: 1, name: 1, sortName: 1, deviceId: 1, floor: 1, pin: 1, -}; -const SORTING_METHODS = Object.freeze({ - // Default - LOCAL_ALPHABETICAL: { - sortingMethod: sortLocalAlphabetical, - neededDataTypes: MANDATORY_DATA_TYPES, - localFirst: true, - }, - VOICE_ACTIVITY_LOCAL: { - sortingMethod: sortVoiceActivityLocal, - neededDataTypes: { - lastFloorTime: 1, floor: 1, ...MANDATORY_DATA_TYPES, - }, - filter: true, - localFirst: false, - }, - LOCAL_VOICE_ACTIVITY: { - sortingMethod: sortLocalVoiceActivity, - neededDataTypes: { - lastFloorTime: 1, floor: 1, ...MANDATORY_DATA_TYPES, - }, - filter: true, - localFirst: true, - }, - LOCAL_PRESENTER_ALPHABETICAL: { - sortingMethod: sortLocalPresenterAlphabetical, - neededDataTypes: MANDATORY_DATA_TYPES, - localFirst: true, - } -}); - -export const getSortingMethod = (identifier) => { - return SORTING_METHODS[identifier] || SORTING_METHODS[DEFAULT_SORTING_MODE]; -}; - -export const sortVideoStreams = (streams, mode) => { - const { sortingMethod, filter } = getSortingMethod(mode); - const sorted = streams.sort(sortingMethod); - - if (!filter) return sorted; - - return sorted.map(videoStream => ({ - stream: videoStream.stream, - isGridItem: videoStream?.isGridItem, - userId: videoStream.userId, - name: videoStream.name, - sortName: videoStream.sortName, - floor: videoStream.floor, - pin: videoStream.pin, - })); -}; diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/stream-sorting.ts b/bigbluebutton-html5/imports/ui/components/video-provider/stream-sorting.ts similarity index 100% rename from bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/stream-sorting.ts rename to bigbluebutton-html5/imports/ui/components/video-provider/stream-sorting.ts diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/types.ts b/bigbluebutton-html5/imports/ui/components/video-provider/types.ts similarity index 100% rename from bigbluebutton-html5/imports/ui/components/video-provider/video-provider-graphql/types.ts rename to bigbluebutton-html5/imports/ui/components/video-provider/types.ts diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx deleted file mode 100755 index 7fea770d51..0000000000 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx +++ /dev/null @@ -1,274 +0,0 @@ -import React, { memo, useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; -import ButtonEmoji from '/imports/ui/components/common/button/button-emoji/ButtonEmoji'; -import VideoService from '../service'; -import { defineMessages, injectIntl } from 'react-intl'; -import Styled from './styles'; -import deviceInfo from '/imports/utils/deviceInfo'; -import { debounce } from '/imports/utils/debounce'; -import BBBMenu from '/imports/ui/components/common/menu/component'; -import { isVirtualBackgroundsEnabled } from '/imports/ui/services/features'; -import Button from '/imports/ui/components/common/button/component'; -import VideoPreviewContainer from '/imports/ui/components/video-preview/container'; -import { CameraSettingsDropdownItemType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/extensible-areas/camera-settings-dropdown-item/enums'; -import { getSettingsSingletonInstance } from '/imports/ui/services/settings'; - -const intlMessages = defineMessages({ - videoSettings: { - id: 'app.video.videoSettings', - description: 'Open video settings', - }, - visualEffects: { - id: 'app.video.visualEffects', - description: 'Visual effects label', - }, - joinVideo: { - id: 'app.video.joinVideo', - description: 'Join video button label', - }, - leaveVideo: { - id: 'app.video.leaveVideo', - description: 'Leave video button label', - }, - advancedVideo: { - id: 'app.video.advancedVideo', - description: 'Open advanced video label', - }, - videoLocked: { - id: 'app.video.videoLocked', - description: 'video disabled label', - }, - videoConnecting: { - id: 'app.video.connecting', - description: 'video connecting label', - }, - camCapReached: { - id: 'app.video.meetingCamCapReached', - description: 'meeting camera cap label', - }, - meteorDisconnected: { - id: 'app.video.clientDisconnected', - description: 'Meteor disconnected label', - }, -}); - -const JOIN_VIDEO_DELAY_MILLISECONDS = 500; - -const propTypes = { - intl: PropTypes.object.isRequired, - hasVideoStream: PropTypes.bool.isRequired, - status: PropTypes.string.isRequired, - cameraSettingsDropdownItems: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string, - type: PropTypes.string, - })).isRequired, - sendUserUnshareWebcam: PropTypes.func.isRequired, - setLocalSettings: PropTypes.func.isRequired, -}; - -const JoinVideoButton = ({ - intl, - hasVideoStream, - status, - disableReason, - updateSettings, - cameraSettingsDropdownItems, - sendUserUnshareWebcam, - setLocalSettings, - away, -}) => { - const ENABLE_WEBCAM_SELECTOR_BUTTON = window.meetingClientSettings.public.app.enableWebcamSelectorButton; - const ENABLE_CAMERA_BRIGHTNESS = window.meetingClientSettings.public.app.enableCameraBrightness; - - const { isMobile } = deviceInfo; - const isMobileSharingCamera = hasVideoStream && isMobile; - const isDesktopSharingCamera = hasVideoStream && !isMobile; - const shouldEnableWebcamSelectorButton = ENABLE_WEBCAM_SELECTOR_BUTTON - && isDesktopSharingCamera; - const shouldEnableWebcamVisualEffectsButton = (isVirtualBackgroundsEnabled() - || ENABLE_CAMERA_BRIGHTNESS) - && hasVideoStream - && !isMobile; - const exitVideo = () => isDesktopSharingCamera && (!VideoService.isMultipleCamerasEnabled() - || shouldEnableWebcamSelectorButton); - - const [propsToPassModal, setPropsToPassModal] = useState({}); - const [forceOpen, setForceOpen] = useState(false); - const [isVideoPreviewModalOpen, setVideoPreviewModalIsOpen] = useState(false); - const [wasSelfViewDisabled, setWasSelfViewDisabled] = useState(false); - const Settings = getSettingsSingletonInstance(); - - useEffect(() => { - const isSelfViewDisabled = Settings.application.selfViewDisable; - - if (isVideoPreviewModalOpen && isSelfViewDisabled) { - setWasSelfViewDisabled(true); - const obj = { - application: - { ...Settings.application, selfViewDisable: false }, - }; - updateSettings(obj, null, setLocalSettings); - } - }, [isVideoPreviewModalOpen]); - - const handleOnClick = debounce(() => { - switch (status) { - case 'videoConnecting': - VideoService.stopVideo(undefined, sendUserUnshareWebcam); - break; - case 'connected': - default: - if (exitVideo()) { - VideoService.exitVideo(sendUserUnshareWebcam); - } else { - setForceOpen(isMobileSharingCamera); - setVideoPreviewModalIsOpen(true); - } - } - }, JOIN_VIDEO_DELAY_MILLISECONDS); - - const handleOpenAdvancedOptions = (callback) => { - if (callback) callback(); - setForceOpen(isDesktopSharingCamera); - setVideoPreviewModalIsOpen(true); - }; - - const getMessageFromStatus = () => { - let statusMessage = status; - if (status !== 'videoConnecting') { - statusMessage = exitVideo() ? 'leaveVideo' : 'joinVideo'; - } - return statusMessage; - }; - - const label = disableReason - ? intl.formatMessage(intlMessages[disableReason]) - : intl.formatMessage(intlMessages[getMessageFromStatus()]); - - const isSharing = hasVideoStream || status === 'videoConnecting'; - - const renderUserActions = () => { - const actions = []; - - if (shouldEnableWebcamSelectorButton) { - actions.push( - { - key: 'advancedVideo', - label: intl.formatMessage(intlMessages.advancedVideo), - onClick: () => handleOpenAdvancedOptions(), - dataTest: 'advancedVideoSettingsButton', - }, - ); - } - - if (shouldEnableWebcamVisualEffectsButton) { - actions.push( - { - key: 'virtualBgSelection', - label: intl.formatMessage(intlMessages.visualEffects), - onClick: () => handleOpenAdvancedOptions(( - ) => setPropsToPassModal({ isVisualEffects: true })), - }, - ); - } - - if (actions.length === 0 || away) return null; - const customStyles = { top: '-3.6rem' }; - - cameraSettingsDropdownItems.forEach((plugin) => { - switch (plugin.type) { - case CameraSettingsDropdownItemType.OPTION: - actions.push({ - key: plugin.id, - label: plugin.label, - onClick: plugin.onClick, - icon: plugin.icon, - }); - break; - case CameraSettingsDropdownItemType.SEPARATOR: - actions.push({ - key: plugin.id, - isSeparator: true, - }); - break; - default: - break; - } - }); - return ( - - )} - actions={actions} - opts={{ - id: 'video-dropdown-menu', - keepMounted: true, - transitionDuration: 0, - elevation: 3, - getcontentanchorel: null, - fullwidth: 'true', - anchorOrigin: { vertical: 'top', horizontal: 'center' }, - transformOrigin: { vertical: 'top', horizontal: 'center' }, - }} - /> - ); - }; - - return ( - <> - -