diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/User.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/User.java index 59521552c3041b30ef9e527d1635b3d614919fd8..da93ef6b915b503a3b6bf9954e1b2c1c6194dd4e 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/User.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/User.java @@ -151,7 +151,9 @@ public class User { } public void addStream(String stream) { - streams.add(stream); + if (!streams.contains(stream)) { + streams.add(stream); + } } public void removeStream(String stream) { diff --git a/bigbluebutton-html5/imports/api/annotations/server/handlers/whiteboardSend.js b/bigbluebutton-html5/imports/api/annotations/server/handlers/whiteboardSend.js index ae16937c170634002d6785ac8564d538dbb34433..a8f37165c68dcbe99d0bd92c6aaca3ba6c686585 100755 --- a/bigbluebutton-html5/imports/api/annotations/server/handlers/whiteboardSend.js +++ b/bigbluebutton-html5/imports/api/annotations/server/handlers/whiteboardSend.js @@ -5,12 +5,14 @@ import Metrics from '/imports/startup/server/metrics'; const { queueMetrics } = Meteor.settings.private.redis.metrics; -const ANNOTATION_PROCCESS_INTERVAL = 60; +const { + annotationsQueueProcessInterval: ANNOTATION_PROCESS_INTERVAL, +} = Meteor.settings.public.whiteboard; let annotationsQueue = {}; let annotationsRecieverIsRunning = false; -const proccess = () => { +const process = () => { if (!Object.keys(annotationsQueue).length) { annotationsRecieverIsRunning = false; return; @@ -24,7 +26,7 @@ const proccess = () => { }); annotationsQueue = {}; - Meteor.setTimeout(proccess, ANNOTATION_PROCCESS_INTERVAL); + Meteor.setTimeout(process, ANNOTATION_PROCESS_INTERVAL); }; export default function handleWhiteboardSend({ header, body }, meetingId) { @@ -45,7 +47,7 @@ export default function handleWhiteboardSend({ header, body }, meetingId) { if (queueMetrics) { Metrics.setAnnotationQueueLength(meetingId, annotationsQueue[meetingId].length); } - if (!annotationsRecieverIsRunning) proccess(); + if (!annotationsRecieverIsRunning) process(); return addAnnotation(meetingId, whiteboardId, userId, annotation); } diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js index 7b81fd76066848806a326788e3ea96646e902a3b..3b223e395aff4c47bb95bd0a73eb32d1ce130e14 100755 --- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js +++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js @@ -17,6 +17,7 @@ import { Tracker } from 'meteor/tracker'; import VoiceCallStates from '/imports/api/voice-call-states'; import CallStateOptions from '/imports/api/voice-call-states/utils/callStates'; import Auth from '/imports/ui/services/auth'; +import Settings from '/imports/ui/services/settings'; const MEDIA = Meteor.settings.public.media; const MEDIA_TAG = MEDIA.mediaTag; @@ -36,6 +37,8 @@ const BRIDGE_NAME = 'sip'; const WEBSOCKET_KEEP_ALIVE_INTERVAL = MEDIA.websocketKeepAliveInterval || 0; const WEBSOCKET_KEEP_ALIVE_DEBOUNCE = MEDIA.websocketKeepAliveDebounce || 10; const TRACE_SIP = MEDIA.traceSip || false; +const AUDIO_MICROPHONE_CONSTRAINTS = Meteor.settings.public.app.defaultSettings + .application.microphoneConstraints; const getAudioSessionNumber = () => { let currItem = parseInt(sessionStorage.getItem(AUDIO_SESSION_NUM_KEY), 10); @@ -578,16 +581,24 @@ class SIPSession { const target = SIP.UserAgent.makeURI(`sip:${callExtension}@${hostname}`); - const audioDeviceConstraint = this.inputDeviceId - ? { deviceId: { exact: this.inputDeviceId } } - : true; + const userSettingsConstraints = Settings.application.microphoneConstraints; + const audioDeviceConstraints = userSettingsConstraints + || AUDIO_MICROPHONE_CONSTRAINTS || {}; + + const matchConstraints = this.filterSupportedConstraints( + audioDeviceConstraints, + ); + + if (this.inputDeviceId) { + matchConstraints.deviceId = { exact: this.inputDeviceId }; + } const inviterOptions = { sessionDescriptionHandlerOptions: { constraints: { audio: isListenOnly ? false - : audioDeviceConstraint, + : matchConstraints, video: false, }, iceGatheringTimeout: ICE_GATHERING_TIMEOUT, @@ -932,6 +943,88 @@ class SIPSession { resolve(); }); } + + /** + * Filter constraints set in audioDeviceConstraints, based on + * constants supported by browser. This avoids setting a constraint + * unsupported by browser. In currently safari version (13+), for example, + * setting an unsupported constraint crashes the audio. + * @param {Object} audioDeviceConstraints Constraints to be set + * see: https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints + * @return {Object} A new Object of the same type as + * input, containing only the supported constraints. + */ + filterSupportedConstraints(audioDeviceConstraints) { + try { + const matchConstraints = {}; + const supportedConstraints = navigator + .mediaDevices.getSupportedConstraints() || {}; + Object.entries(audioDeviceConstraints).forEach( + ([constraintName, constraintValue]) => { + if (supportedConstraints[constraintName]) { + matchConstraints[constraintName] = constraintValue; + } + } + ); + + return matchConstraints; + } catch (error) { + logger.error({ + logCode: 'sipjs_unsupported_audio_constraint_error', + extraInfo: { + callerIdName: this.user.callerIdName, + }, + }, 'SIP.js unsupported constraint error'); + return {}; + } + } + + /** + * Update audio constraints for current local MediaStream (microphone) + * @param {Object} constraints MediaTrackConstraints object. See: + * https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints + * @return {Promise} A Promise for this process + */ + async updateAudioConstraints(constraints) { + try { + if (typeof constraints !== 'object') return; + + logger.info({ + logCode: 'sipjs_update_audio_constraint', + extraInfo: { + callerIdName: this.user.callerIdName, + }, + }, 'SIP.js updating audio constraint'); + + const matchConstraints = this.filterSupportedConstraints(constraints); + + //Chromium bug - see: https://bugs.chromium.org/p/chromium/issues/detail?id=796964&q=applyConstraints&can=2 + if (browser().name === 'chrome') { + matchConstraints.deviceId = this.inputDeviceId; + + const stream = await navigator.mediaDevices.getUserMedia( + { audio: matchConstraints }, + ); + + this.currentSession.sessionDescriptionHandler + .setLocalMediaStream(stream); + } else { + const { localMediaStream } = this.currentSession + .sessionDescriptionHandler; + + localMediaStream.getAudioTracks().forEach( + track => track.applyConstraints(matchConstraints), + ); + } + } catch (error) { + logger.error({ + logCode: 'sipjs_audio_constraint_error', + extraInfo: { + callerIdName: this.user.callerIdName, + }, + }, 'SIP.js failed to update audio constraint'); + } + } } export default class SIPBridge extends BaseAudioBridge { @@ -1082,4 +1175,8 @@ export default class SIPBridge extends BaseAudioBridge { return this.media.outputDeviceId || value; } + + async updateAudioConstraints(constraints) { + return this.activeSession.updateAudioConstraints(constraints); + } } diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/modifiers/startTyping.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/modifiers/startTyping.js index 494fb95677c42a168b47e09bc532ee0266e2adcf..86dd9d60681499ea52ad77e07f3f8cf44605798c 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/modifiers/startTyping.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/modifiers/startTyping.js @@ -15,7 +15,7 @@ export default function startTyping(meetingId, userId, chatId) { userId, }; - const user = Users.findOne(selector); + const user = Users.findOne(selector, { fields: { name: 1, role: 1 } }); const modifier = { meetingId, diff --git a/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js b/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js index 42dd7e2e7fed6fc970ed7a8526ceadad47ff251b..c9637bdd74b5f26601900803a3ce80c470e0e3e4 100644 --- a/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js +++ b/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js @@ -4,6 +4,7 @@ import Users from '/imports/api/users'; import userJoin from './userJoin'; import pendingAuthenticationsStore from '../store/pendingAuthentications'; import createDummyUser from '../modifiers/createDummyUser'; +import ClientConnections from '/imports/startup/server/ClientConnections'; import upsertValidationState from '/imports/api/auth-token-validation/server/modifiers/upsertValidationState'; import { ValidationStates } from '/imports/api/auth-token-validation'; @@ -75,6 +76,7 @@ export default function handleValidateAuthToken({ body }, meetingId) { createDummyUser(meetingId, userId, authToken); } + ClientConnections.add(sessionId, methodInvocationObject.connection); upsertValidationState(meetingId, userId, ValidationStates.VALIDATED, methodInvocationObject.connection.id); /* End of logic migrated from validateAuthToken */ diff --git a/bigbluebutton-html5/imports/api/users/server/methods/userLeaving.js b/bigbluebutton-html5/imports/api/users/server/methods/userLeaving.js index a075d36662f02270aad7bb58b6538929d0c8ac27..36bcd600aba0d1acd160888b0248163c7696af06 100755 --- a/bigbluebutton-html5/imports/api/users/server/methods/userLeaving.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/userLeaving.js @@ -4,6 +4,7 @@ import RedisPubSub from '/imports/startup/server/redis'; import Logger from '/imports/startup/server/logger'; import AuthTokenValidation from '/imports/api/auth-token-validation'; import Users from '/imports/api/users'; +import ClientConnections from '/imports/startup/server/ClientConnections'; export default function userLeaving(meetingId, userId, connectionId) { const REDIS_CONFIG = Meteor.settings.private.redis; @@ -40,6 +41,8 @@ export default function userLeaving(meetingId, userId, connectionId) { sessionId: meetingId, }; + ClientConnections.removeClientConnection(`${meetingId}--${userId}`, connectionId); + Logger.info(`User '${userId}' is leaving meeting '${meetingId}'`); return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, userId, payload); } diff --git a/bigbluebutton-html5/imports/api/users/server/methods/userLeftMeeting.js b/bigbluebutton-html5/imports/api/users/server/methods/userLeftMeeting.js index 38e31316ac795c70f46fd8f980c59e1f15b90504..c2ed519acceeee65e83f8ada074071f47f7669a8 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/userLeftMeeting.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/userLeftMeeting.js @@ -17,18 +17,8 @@ export default function userLeftMeeting() { // TODO-- spread the code to method/ if (numberAffected) { Logger.info(`user left id=${requesterUserId} meeting=${meetingId}`); + ClientConnections.removeClientConnection(this.userId, this.connection.id); } - ClientConnections.removeClientConnection(this.userId, this.connection.id); - - Users.update( - selector, - { - $set: { - loggedOut: true, - }, - }, - cb, - ); } catch (err) { Logger.error(`leaving dummy user to collection: ${err}`); } diff --git a/bigbluebutton-html5/imports/api/users/server/methods/validateAuthToken.js b/bigbluebutton-html5/imports/api/users/server/methods/validateAuthToken.js index a53690907ab9900ff4d74413e55e16b9487ccc1a..e3ca329a3a6f3ee10c9a5d705f8603758fc09b7f 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/validateAuthToken.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/validateAuthToken.js @@ -1,18 +1,18 @@ import { Meteor } from 'meteor/meteor'; import RedisPubSub from '/imports/startup/server/redis'; import Logger from '/imports/startup/server/logger'; -import ClientConnections from '/imports/startup/server/ClientConnections'; import upsertValidationState from '/imports/api/auth-token-validation/server/modifiers/upsertValidationState'; import { ValidationStates } from '/imports/api/auth-token-validation'; import pendingAuthenticationsStore from '../store/pendingAuthentications'; import BannedUsers from '../store/bannedUsers'; -import Users from '/imports/api/users'; export default function validateAuthToken(meetingId, requesterUserId, requesterToken, externalId) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'ValidateAuthTokenReqMsg'; + Logger.debug('ValidateAuthToken method called', { meetingId, requesterUserId, requesterToken, externalId }); + // Check if externalId is banned from the meeting if (externalId) { if (BannedUsers.has(meetingId, externalId)) { @@ -21,24 +21,7 @@ export default function validateAuthToken(meetingId, requesterUserId, requesterT } } - // Prevent users who have left or been ejected to use the same sessionToken again. - const isUserInvalid = Users.findOne({ - meetingId, - userId: requesterUserId, - authToken: requesterToken, - $or: [{ ejected: true }, { loggedOut: true }], - }); - - if (isUserInvalid) { - Logger.warn(`An invalid sessionToken tried to validateAuthToken meetingId=${meetingId} authToken=${requesterToken}`); - return { - invalid: true, - reason: `User has an invalid sessionToken due to ${isUserInvalid.ejected ? 'ejection' : 'log out'}`, - error_type: `invalid_session_token_due_to_${isUserInvalid.ejected ? 'eject' : 'log_out'}`, - }; - } - - ClientConnections.add(`${meetingId}--${requesterUserId}`, this.connection); + if (!meetingId) return false; // Store reference of methodInvocationObject ( to postpone the connection userId definition ) pendingAuthenticationsStore.add(meetingId, requesterUserId, requesterToken, this); diff --git a/bigbluebutton-html5/imports/api/users/server/publishers.js b/bigbluebutton-html5/imports/api/users/server/publishers.js index 55e4acbb89e4780c4d085d6e6e425be279019a26..3cdba628ad98a2acb5b51e1a211f3e2e946e851b 100644 --- a/bigbluebutton-html5/imports/api/users/server/publishers.js +++ b/bigbluebutton-html5/imports/api/users/server/publishers.js @@ -18,6 +18,7 @@ function currentUser() { const selector = { meetingId, userId: requesterUserId, + intId: { $exists: true } }; const options = { @@ -56,6 +57,7 @@ function users(role) { $or: [ { meetingId }, ], + intId: { $exists: true } }; const User = Users.findOne({ userId, meetingId }, { fields: { role: 1 } }); diff --git a/bigbluebutton-html5/imports/startup/server/ClientConnections.js b/bigbluebutton-html5/imports/startup/server/ClientConnections.js index 5e4ca6bdfabc837f2796b1eb00bbc260ac3dbd7a..9bb837f4ae1900d86e027bd407397cbcfbd57311 100644 --- a/bigbluebutton-html5/imports/startup/server/ClientConnections.js +++ b/bigbluebutton-html5/imports/startup/server/ClientConnections.js @@ -1,6 +1,10 @@ import Logger from './logger'; import userLeaving from '/imports/api/users/server/methods/userLeaving'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import AuthTokenValidation from '/imports/api/auth-token-validation'; +import Users from '/imports/api/users'; + +const { enabled, syncInterval } = Meteor.settings.public.syncUsersWithConnectionManager; class ClientConnections { constructor() { @@ -11,10 +15,15 @@ class ClientConnections { this.print(); }, 30000); - // setTimeout(() => { - // this.syncConnectionsWithServer(); - // }, 10000); + if (enabled) { + const syncConnections = Meteor.bindEnvironment(() => { + this.syncConnectionsWithServer(); + }); + setInterval(() => { + syncConnections(); + }, syncInterval); + } } add(sessionId, connection) { @@ -29,6 +38,13 @@ class ClientConnections { const { meetingId, requesterUserId: userId } = extractCredentials(sessionId); + if (!meetingId) { + Logger.error('Error on add new client connection. sessionId=${sessionId} connection=${connection.id}', + { logCode: 'client_connections_add_error_meeting_id_null', extraInfo: { meetingId, userId } } + ); + return false; + } + if (!this.exists(meetingId)) { Logger.info(`Meeting not found in connections: meetingId=${meetingId}`); this.createMeetingConnections(meetingId); @@ -92,7 +108,7 @@ class ClientConnections { Logger.info(`Removing connectionId for user. sessionId=${sessionId} connectionId=${connectionId}`); const { meetingId, requesterUserId: userId } = extractCredentials(sessionId); - const meetingConnections = this.connections.get(meetingId) + const meetingConnections = this.connections.get(meetingId); if (meetingConnections?.has(userId)) { const filteredConnections = meetingConnections.get(userId).filter(c => c !== connectionId); @@ -109,7 +125,38 @@ class ClientConnections { } syncConnectionsWithServer() { - console.error('syncConnectionsWithServer', Array.from(Meteor.server.sessions.keys()), Meteor.server); + Logger.info('Syncing ClientConnections with server'); + const activeConnections = Array.from(Meteor.server.sessions.keys()); + + Logger.debug(`Found ${activeConnections.length} active connections in server`); + + const onlineUsers = AuthTokenValidation + .find( + { connectionId: { $in: activeConnections } }, + { fields: { meetingId: 1, userId: 1 } } + ) + .fetch(); + + const onlineUsersId = onlineUsers.map(({ userId }) => userId); + + const usersQuery = { userId: { $nin: onlineUsersId } }; + + const userWithoutConnectionIds = Users.find(usersQuery, { fields: { meetingId: 1, userId: 1 } }).fetch(); + + const removedUsersWithoutConnection = Users.remove(usersQuery); + + if (removedUsersWithoutConnection) { + Logger.info(`Removed ${removedUsersWithoutConnection} users that are not connected`); + Logger.info(`Clearing connections`); + try { + userWithoutConnectionIds + .forEach(({ meetingId, userId }) => { + this.removeClientConnection(`${meetingId}--${userId}`); + }); + } catch (err) { + Logger.error('Error on sync ClientConnections', err); + } + } } } diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx index 6b1aba372275309aa7e4ee43b40067e67b6bf5dc..4680459f112995d253b1e3a2242b53b697981515 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx @@ -107,7 +107,7 @@ class QuickPollDropdown extends Component { const parsedSlide = parseCurrentSlideContent( intl.formatMessage(intlMessages.yesOptionLabel), intl.formatMessage(intlMessages.noOptionLabel), - intl.formatMessage(intlMessages.abstentionOptionLabel) + intl.formatMessage(intlMessages.abstentionOptionLabel), intl.formatMessage(intlMessages.trueOptionLabel), intl.formatMessage(intlMessages.falseOptionLabel), ); diff --git a/bigbluebutton-html5/imports/ui/components/audio/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/container.jsx index 864af9d23d3f866faf44a4d77e3cd973d4d94459..26e295ae635171a5854f81c8b675324877f4cc81 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/container.jsx @@ -11,6 +11,7 @@ import VideoPreviewContainer from '/imports/ui/components/video-preview/containe import lockContextContainer from '/imports/ui/components/lock-viewers/context/container'; import Service from './service'; import AudioModalContainer from './audio-modal/container'; +import Settings from '/imports/ui/services/settings'; const APP_CONFIG = Meteor.settings.public.app; const KURENTO_CONFIG = Meteor.settings.public.kurento; @@ -105,6 +106,7 @@ const messages = { }; export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ mountModal, intl, userLocks }) => { + const { microphoneConstraints } = Settings.application; const autoJoin = getFromUserSettings('bbb_auto_join_audio', APP_CONFIG.autoJoin); const enableVideo = getFromUserSettings('bbb_enable_video', KURENTO_CONFIG.enableVideo); const autoShareWebcam = getFromUserSettings('bbb_auto_share_webcam', KURENTO_CONFIG.autoShareWebcam); @@ -117,12 +119,14 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m if (userWebcam) return resolve(); mountModal(<VideoPreviewContainer resolve={resolve} />); }); - if (userMic - && Service.isConnected() - && !Service.isListenOnly() - && !Service.isMuted()) { - Service.toggleMuteMicrophone(); - notify(intl.formatMessage(intlMessages.reconectingAsListener), 'info', 'audio_on'); + + if (Service.isConnected() && !Service.isListenOnly()) { + Service.updateAudioConstraints(microphoneConstraints); + + if (userMic && !Service.isMuted()) { + Service.toggleMuteMicrophone(); + notify(intl.formatMessage(intlMessages.reconectingAsListener), 'info', 'audio_on'); + } } Breakouts.find().observeChanges({ diff --git a/bigbluebutton-html5/imports/ui/components/audio/service.js b/bigbluebutton-html5/imports/ui/components/audio/service.js index b0ae6e1002c5179032034ea4aa697777766d62c3..3ae8226bd2dc441eff8ca0b3f2f60890803eabad 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/service.js +++ b/bigbluebutton-html5/imports/ui/components/audio/service.js @@ -1,6 +1,6 @@ import Users from '/imports/api/users'; import Auth from '/imports/ui/services/auth'; -import { debounce } from 'lodash'; +import { debounce, throttle } from 'lodash'; import AudioManager from '/imports/ui/services/audio-manager'; import Meetings from '/imports/api/meetings'; import { makeCall } from '/imports/ui/services/api'; @@ -8,6 +8,7 @@ import VoiceUsers from '/imports/api/voice-users'; import logger from '/imports/startup/client/logger'; const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; +const TOGGLE_MUTE_THROTTLE_TIME = Meteor.settings.public.media.toggleMuteThrottleTime; const init = (messages, intl) => { AudioManager.setAudioMessages(messages, intl); @@ -40,11 +41,11 @@ const isVoiceUser = () => { { fields: { joined: 1 } }); return voiceUser ? voiceUser.joined : false; }; -const toggleMuteMicrophone = () => { + +const toggleMuteMicrophone = throttle(() => { const user = VoiceUsers.findOne({ meetingId: Auth.meetingID, intId: Auth.userID, }, { fields: { muted: 1 } }); - if (user.muted) { logger.info({ logCode: 'audiomanager_unmute_audio', @@ -58,7 +59,7 @@ const toggleMuteMicrophone = () => { }, 'microphone muted by user'); makeCall('toggleVoice'); } -}; +}, TOGGLE_MUTE_THROTTLE_TIME); export default { @@ -89,4 +90,6 @@ export default { autoplayBlocked: () => AudioManager.autoplayBlocked, handleAllowAutoplay: () => AudioManager.handleAllowAutoplay(), playAlertSound: url => AudioManager.playAlertSound(url), + updateAudioConstraints: + constraints => AudioManager.updateAudioConstraints(constraints), }; diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-form/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-form/container.jsx index 7628a11661a4fdc409f57a7f5057a009d9a18d39..8ab1028c8a5993f9f6508b7ccea97c745f094420 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/message-form/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/message-form/container.jsx @@ -1,10 +1,12 @@ import React, { PureComponent } from 'react'; import { withTracker } from 'meteor/react-meteor-data'; +import _ from 'lodash'; import { makeCall } from '/imports/ui/services/api'; import ChatForm from './component'; import ChatService from '../service'; const CHAT_CONFIG = Meteor.settings.public.chat; +const START_TYPING_THROTTLE_INTERVAL = 2000; class ChatContainer extends PureComponent { render() { @@ -25,7 +27,7 @@ export default withTracker(() => { const stopUserTyping = () => makeCall('stopUserTyping'); return { - startUserTyping, + startUserTyping: _.throttle(startUserTyping, START_TYPING_THROTTLE_INTERVAL), stopUserTyping, UnsentMessagesCollection: ChatService.UnsentMessagesCollection, minMessageLength: CHAT_CONFIG.min_message_length, diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.scss b/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.scss index d9cc1f2ab72c88985e12cb2e9e82bafc0a485d5e..3c7360106a2b698d50e322b7654d3a1f6afed21a 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.scss @@ -139,7 +139,7 @@ > span { display: block; - width: 100%; + margin-right: 0.05rem; line-height: var(--font-size-md); } diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/talking-indicator/container.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/talking-indicator/container.jsx index ec0309527bd78a1699321d88ec16964b53af810c..36fcd5d036735402e447d82793b06ca821b3b8b9 100644 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/talking-indicator/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/talking-indicator/container.jsx @@ -10,6 +10,7 @@ import Service from './service'; const APP_CONFIG = Meteor.settings.public.app; const { enableTalkingIndicator } = APP_CONFIG; +const TALKING_INDICATOR_MUTE_INTERVAL = 500; const TalkingIndicatorContainer = (props) => { if (!enableTalkingIndicator) return null; @@ -47,7 +48,7 @@ export default withTracker(() => { } } - const muteUser = (id) => { + const muteUser = debounce((id) => { const user = VoiceUsers.findOne({ meetingId, voiceUserId: id }, { fields: { muted: 1, @@ -55,11 +56,11 @@ export default withTracker(() => { }); if (user.muted) return; makeCall('toggleVoice', id); - }; + }, TALKING_INDICATOR_MUTE_INTERVAL, { leading: true, trailing: false }); return { talkers, - muteUser: id => debounce(muteUser(id), 500, { leading: true, trailing: false }), + muteUser, openPanel: Session.get('openPanel'), isBreakoutRoom: meetingIsBreakout(), }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/service.js b/bigbluebutton-html5/imports/ui/components/presentation/service.js index 08a173fff33e78518af7cd3224dc7299c37672cc..3f53a8a61731f575efee5a71812f3304853503be 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/service.js @@ -16,7 +16,7 @@ const downloadPresentationUri = (podId) => { return null; } - const presentationFileName = currentPresentation.id + '.' + currentPresentation.name.split('.').pop(); + const presentationFileName = `${currentPresentation.id}.${currentPresentation.name.split('.').pop()}`; const uri = `https://${window.document.location.hostname}/bigbluebutton/presentation/download/` + `${currentPresentation.meetingId}/${currentPresentation.id}` diff --git a/bigbluebutton-html5/imports/ui/components/settings/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/component.jsx index f92a87a31c2772b77c855ba10098e15a47abcd20..f043f7c8b4cbc0403fd206ff9eeae64ceb1463f3 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/settings/component.jsx @@ -74,6 +74,7 @@ const propTypes = { fallbackLocale: PropTypes.string, fontSize: PropTypes.string, locale: PropTypes.string, + microphoneConstraints: PropTypes.objectOf(Object), }).isRequired, updateSettings: PropTypes.func.isRequired, availableLocales: PropTypes.objectOf(PropTypes.array).isRequired, diff --git a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx index d91cacee4324f7b2d7d36c9a9cb2de8c7bf284e2..911a02c0fcb7e8762e5f3b95d479fafcd7e890fc 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx @@ -17,6 +17,10 @@ const intlMessages = defineMessages({ id: 'app.submenu.application.animationsLabel', description: 'animations label', }, + audioFilterLabel: { + id: 'app.submenu.application.audioFilterLabel', + description: 'audio filters label', + }, fontSizeControlLabel: { id: 'app.submenu.application.fontSizeControlLabel', description: 'label for font size ontrol', @@ -76,6 +80,8 @@ class ApplicationMenu extends BaseMenu { '18px', '20px', ], + audioFilterEnabled: ApplicationMenu.isAudioFilterEnabled(props + .settings.microphoneConstraints), }; } @@ -118,6 +124,49 @@ class ApplicationMenu extends BaseMenu { }); } + static isAudioFilterEnabled(_constraints) { + if (typeof _constraints === 'undefined') return true; + + const _isConstraintEnabled = (constraintValue) => { + switch (typeof constraintValue) { + case 'boolean': + return constraintValue; + case 'string': + return constraintValue === 'true'; + case 'object': + return !!(constraintValue.exact || constraintValue.ideal); + default: + return false; + } + }; + + let isAnyFilterEnabled = true; + + const constraints = _constraints && (typeof _constraints.advanced === 'object') + ? _constraints.advanced + : _constraints || {}; + + isAnyFilterEnabled = Object.values(constraints).find( + constraintValue => _isConstraintEnabled(constraintValue), + ); + + return isAnyFilterEnabled; + } + + handleAudioFilterChange() { + const _audioFilterEnabled = !ApplicationMenu.isAudioFilterEnabled(this + .state.settings.microphoneConstraints); + const _newConstraints = { + autoGainControl: _audioFilterEnabled, + echoCancellation: _audioFilterEnabled, + noiseSuppression: _audioFilterEnabled, + }; + + const obj = this.state; + obj.settings.microphoneConstraints = _newConstraints; + this.handleUpdateSettings(this.state.settings, obj.settings); + } + handleUpdateFontSize(size) { const obj = this.state; obj.settings.fontSize = size; @@ -208,6 +257,25 @@ class ApplicationMenu extends BaseMenu { </div> </div> + <div className={styles.row}> + <div className={styles.col} aria-hidden="true"> + <div className={styles.formElement}> + <label className={styles.label}> + {intl.formatMessage(intlMessages.audioFilterLabel)} + </label> + </div> + </div> + <div className={styles.col}> + <div className={cx(styles.formElement, styles.pullContentRight)}> + <Toggle + icons={false} + defaultChecked={this.state.audioFilterEnabled} + onChange={() => this.handleAudioFilterChange()} + ariaLabel={intl.formatMessage(intlMessages.audioFilterLabel)} + /> + </div> + </div> + </div> <div className={styles.row}> <div className={styles.col} aria-hidden="true"> <div className={styles.formElement}> @@ -249,6 +317,27 @@ class ApplicationMenu extends BaseMenu { </span> </div> </div> + + <div className={styles.row}> + <div className={styles.col} aria-hidden="true"> + <div className={styles.formElement}> + <label className={styles.label}> + {intl.formatMessage(intlMessages.audioFilterLabel)} + </label> + </div> + </div> + <div className={styles.col}> + <div className={cx(styles.formElement, styles.pullContentRight)}> + <Toggle + icons={false} + defaultChecked={this.state.audioFilterEnabled} + onChange={() => this.handleAudioFilterChange()} + ariaLabel={intl.formatMessage(intlMessages.audioFilterLabel)} + /> + </div> + </div> + </div> + <hr className={styles.separator} /> <div className={styles.row}> <div className={styles.col}> @@ -303,3 +392,4 @@ class ApplicationMenu extends BaseMenu { } export default injectIntl(ApplicationMenu); + diff --git a/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx b/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx index 18cfe74df2e996ee53a513298109540b3ea9cee4..67ea6939228b9339b9facfc99d5adf72c9ee9290 100755 --- a/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx @@ -7,12 +7,14 @@ import Button from '/imports/ui/components/button/component'; // import { notify } from '/imports/ui/services/notification'; import logger from '/imports/startup/client/logger'; import Modal from '/imports/ui/components/modal/simple/component'; +import Service from './service'; import browser from 'browser-detect'; import VideoService from '../video-provider/service'; import cx from 'classnames'; import { styles } from './styles'; const CAMERA_PROFILES = Meteor.settings.public.kurento.cameraProfiles; +const GUM_TIMEOUT = Meteor.settings.public.kurento.gUMTimeout; const VIEW_STATES = { finding: 'finding', @@ -118,6 +120,22 @@ const intlMessages = defineMessages({ id: 'app.video.permissionError', description: 'Error message for webcam permission', }, + AbortError: { + id: 'app.video.abortError', + description: 'Some problem occurred which prevented the device from being used', + }, + OverconstrainedError: { + id: 'app.video.overconstrainedError', + description: 'No candidate devices which met the criteria requested', + }, + SecurityError: { + id: 'app.video.securityError', + description: 'Media support is disabled on the Document', + }, + TypeError: { + id: 'app.video.typeError', + description: 'List of constraints specified is empty, or has all constraints set to false', + }, NotFoundError: { id: 'app.video.notFoundError', description: 'error message when can not get webcam video', @@ -134,6 +152,10 @@ const intlMessages = defineMessages({ id: 'app.video.notReadableError', description: 'error message When the webcam is being used by other software', }, + TimeoutError: { + id: 'app.video.timeoutError', + description: 'error message when promise did not return', + }, iOSError: { id: 'app.audioModal.iOSBrowser', description: 'Audio/Video Not supported warning', @@ -146,54 +168,13 @@ const intlMessages = defineMessages({ id: 'app.audioModal.iOSErrorRecommendation', description: 'Audio/Video recommended action', }, + genericError: { + id: 'app.video.genericError', + description: 'error message for when the webcam sharing fails with unknown error', + }, }); class VideoPreview extends Component { - static handleGUMError(error) { - // logger.error(error); - // logger.error(error.id); - // logger.error(error.name); - // console.log(error); - // console.log(error.name) - // console.log(error.message) - - // let convertedError; - - /* switch (error.name) { - case 'SourceUnavailableError': - case 'NotReadableError': - // hardware failure with the device - // NotReadableError: Could not start video source - break; - case 'NotAllowedError': - // media was disallowed - // NotAllowedError: Permission denied - convertedError = intlMessages.NotAllowedError; - break; - case 'AbortError': - // generic error occured - // AbortError: Starting video failed (FF when there's a hardware failure) - break; - case 'NotFoundError': - // no webcam found - // NotFoundError: The object can not be found here. - // NotFoundError: Requested device not found - convertedError = intlMessages.NotFoundError; - break; - case 'SecurityError': - // user media support is disabled on the document - break; - case 'TypeError': - // issue with constraints or maybe Chrome with HTTP - break; - default: - // default error message handling - break; - } */ - - return `${error.name}: ${error.message}`; - } - constructor(props) { super(props); @@ -240,12 +221,20 @@ class VideoPreview extends Component { // skipped then we get devices with no labels if (hasMediaDevices) { try { - navigator.mediaDevices.getUserMedia({ audio: false, video: { facingMode: 'user' } }) + let firstAllowedDeviceId; + + const constraints = { + audio: false, + video: { + facingMode: 'user', + }, + }; + + Service.promiseTimeout(GUM_TIMEOUT, navigator.mediaDevices.getUserMedia(constraints)) .then((stream) => { if (!this._isMounted) return; this.deviceStream = stream; // try and get the deviceId for the initial stream - let firstAllowedDeviceId; if (stream.getVideoTracks) { const videoTracks = stream.getVideoTracks(); if (videoTracks.length > 0 && videoTracks[0].getSettings) { @@ -253,7 +242,9 @@ class VideoPreview extends Component { firstAllowedDeviceId = trackSettings.deviceId; } } - + }).catch((error) => { + this.handleDeviceError('initial_device', error, 'getting initial device'); + }).finally(() => { navigator.mediaDevices.enumerateDevices().then((devices) => { const webcams = []; let initialDeviceId; @@ -297,43 +288,11 @@ class VideoPreview extends Component { }); } }).catch((error) => { - logger.warn({ - logCode: 'video_preview_enumerate_error', - extraInfo: { - errorName: error.name, - errorMessage: error.message, - }, - }, 'Error enumerating devices'); - this.setState({ - viewState: VIEW_STATES.error, - deviceError: VideoPreview.handleGUMError(error), - }); - }); - }).catch((error) => { - logger.warn({ - logCode: 'video_preview_initial_device_error', - extraInfo: { - errorName: error.name, - errorMessage: error.message, - }, - }, 'Error getting initial device'); - this.setState({ - viewState: VIEW_STATES.error, - deviceError: VideoPreview.handleGUMError(error), + this.handleDeviceError('enumerate', error, 'enumerating devices'); }); }); } catch (error) { - logger.warn({ - logCode: 'video_preview_grabbing_error', - extraInfo: { - errorName: error.name, - errorMessage: error.message, - }, - }, 'Error grabbing initial video stream'); - this.setState({ - viewState: VIEW_STATES.error, - deviceError: VideoPreview.handleGUMError(error), - }); + this.handleDeviceError('grabbing', error, 'grabbing initial video stream'); } } else { // TODO: Add an error message when media is globablly disabled @@ -409,6 +368,51 @@ class VideoPreview extends Component { if (resolve) resolve(); } + handlePreviewError(logCode, error, description) { + logger.warn({ + logCode: `video_preview_${logCode}_error`, + extraInfo: { + errorName: error.name, + errorMessage: error.message, + }, + }, `Error ${description}`); + this.setState({ + previewError: this.handleGUMError(error), + }); + } + + handleDeviceError(logCode, error, description) { + logger.warn({ + logCode: `video_preview_${logCode}_error`, + extraInfo: { + errorName: error.name, + errorMessage: error.message, + }, + }, `Error ${description}`); + this.setState({ + viewState: VIEW_STATES.error, + deviceError: this.handleGUMError(error), + }); + } + + handleGUMError(error) { + const { intl } = this.props; + + logger.error({ + logCode: 'video_preview_gum_failure', + extraInfo: { + errorName: error.name, errorMessage: error.message, + }, + }, 'getUserMedia failed in video-preview'); + + if (intlMessages[error.name]) { + return intl.formatMessage(intlMessages[error.name]); + } + + return intl.formatMessage(intlMessages.genericError, + { 0: `${error.name}: ${error.message}` }); + } + displayInitialPreview(deviceId) { const { changeWebcam } = this.props; const availableProfiles = CAMERA_PROFILES.filter(p => !p.hidden); @@ -441,7 +445,7 @@ class VideoPreview extends Component { } this.deviceStream = null; - return navigator.mediaDevices.getUserMedia(constraints); + return Service.promiseTimeout(GUM_TIMEOUT, navigator.mediaDevices.getUserMedia(constraints)); } displayPreview(deviceId, profile) { @@ -467,14 +471,7 @@ class VideoPreview extends Component { this.video.srcObject = stream; this.deviceStream = stream; }).catch((error) => { - logger.warn({ - logCode: 'video_preview_do_gum_preview_error', - extraInfo: { - errorName: error.name, - errorMessage: error.message, - }, - }, 'Error displaying final selection.'); - this.setState({ previewError: VideoPreview.handleGUMError(error) }); + this.handlePreviewError('do_gum_preview', error, 'displaying final selection'); }); } @@ -547,7 +544,7 @@ class VideoPreview extends Component { <label className={styles.label} htmlFor="setQuality"> {intl.formatMessage(intlMessages.qualityLabel)} </label> - { availableProfiles && availableProfiles.length > 0 + {availableProfiles && availableProfiles.length > 0 ? ( <select id="setQuality" @@ -565,7 +562,8 @@ class VideoPreview extends Component { <option key={profile.id} value={profile.id}> {`${label}`} </option> - )})} + ) + })} </select> ) : ( diff --git a/bigbluebutton-html5/imports/ui/components/video-preview/service.js b/bigbluebutton-html5/imports/ui/components/video-preview/service.js index 54125aa91f0093e20624545586f19370a723dc09..a5d92d6cb05918e8a56ef77bdb0d1fc6701d754e 100755 --- a/bigbluebutton-html5/imports/ui/components/video-preview/service.js +++ b/bigbluebutton-html5/imports/ui/components/video-preview/service.js @@ -1,4 +1,25 @@ +const promiseTimeout = (ms, promise) => { + const timeout = new Promise((resolve, reject) => { + const id = setTimeout(() => { + clearTimeout(id); + + const error = { + name: 'TimeoutError', + message: 'Promise did not return', + }; + + reject(error); + }, ms); + }); + + return Promise.race([ + promise, + timeout, + ]); +}; + export default { + promiseTimeout, changeWebcam: (deviceId) => { Session.set('WebcamDeviceId', deviceId); }, diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/service.js b/bigbluebutton-html5/imports/ui/components/video-provider/service.js index f71407d1e07daeb435fb03731c92ecec3bba4c94..e1d61fa0ff3da42ff2bab0dea124c85d98fbe3a8 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/service.js +++ b/bigbluebutton-html5/imports/ui/components/video-provider/service.js @@ -68,8 +68,6 @@ class VideoService { const BROWSER_RESULTS = browser(); this.isMobile = BROWSER_RESULTS.mobile || BROWSER_RESULTS.os.includes('Android'); this.isSafari = BROWSER_RESULTS.name === 'safari'; - this.pageChangeLocked = false; - this.numberOfDevices = 0; this.record = null; @@ -167,11 +165,28 @@ class VideoService { meetingId: Auth.meetingID, userId: Auth.userID, }, { fields: { stream: 1 } }, - ).fetch().length; - this.sendUserUnshareWebcam(cameraId); - if (streams < 2) { - // If the user had less than 2 streams, set as a full disconnection + ).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) { + this.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(); } } @@ -333,6 +348,11 @@ class VideoService { return { streams: paginatedStreams, totalNumberOfStreams: mappedStreams.length }; } + stopConnectingStream () { + this.deviceId = null; + this.isConnecting = false; + } + getConnectingStream(streams) { let connectingStream; @@ -347,8 +367,7 @@ class VideoService { }; } else { // Connecting stream is already stored at database - this.deviceId = null; - this.isConnecting = false; + this.stopConnectingStream(); } } else { logger.error({ 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 index 4043f183ad226d473ac4675d4a6a90cd975fe4e1..5b44dcb42a6daa78766f9b5de4b7dd252666093e 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx @@ -74,7 +74,7 @@ const JoinVideoButton = ({ <Button label={label} data-test="joinVideo" - className={cx(styles.btn, hasVideoStream || styles.btn)} + className={cx(hasVideoStream || styles.btn)} onClick={handleOnClick} hideLabel color={hasVideoStream ? 'primary' : 'default'} diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/component.jsx index ac1340fb3ecdf3b09e336eb24b8aaf696a1c139e..c4cef8f638b4eb9905b328678f97c828e8a52954 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/component.jsx @@ -77,6 +77,8 @@ const findOptimalGrid = (canvasWidth, canvasHeight, gutter, aspectRatio, numItem }; const ASPECT_RATIO = 4 / 3; +const ACTION_NAME_FOCUS = 'focus'; +const ACTION_NAME_MIRROR = 'mirror'; class VideoList extends Component { constructor(props) { @@ -308,6 +310,7 @@ class VideoList extends Component { const isFocusedIntlKey = !isFocused ? 'focus' : 'unfocus'; const isMirrored = this.cameraIsMirrored(cameraId); let actions = [{ + actionName: ACTION_NAME_MIRROR, label: intl.formatMessage(intlMessages['mirrorLabel']), description: intl.formatMessage(intlMessages['mirrorDesc']), onClick: () => this.mirrorCamera(cameraId), @@ -315,6 +318,7 @@ class VideoList extends Component { if (numOfStreams > 2) { actions.push({ + actionName: ACTION_NAME_FOCUS, label: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Label`]), description: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Desc`]), onClick: () => this.handleVideoFocus(cameraId), diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx index 798d99f43fb7a379f4d70c093008bc34d05b6da5..c83a8da8810303334ce57352dc43e241a27233d7 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx @@ -111,7 +111,7 @@ class VideoListItem extends Component { return _.compact([ <DropdownListTitle className={styles.hiddenDesktop} key="name">{name}</DropdownListTitle>, <DropdownListSeparator className={styles.hiddenDesktop} key="sep" />, - ...actions.map(action => (<DropdownListItem key={cameraId} {...action} />)), + ...actions.map(action => (<DropdownListItem key={`${cameraId}-${action.actionName}`} {...action} />)), ]); } diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/cursor-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/cursor-listener/component.jsx index efb4d7a281ac069eb49a278e2df81045fb276c2b..8f04eeba79c9e7511fc97518c78d80799c29a807 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/cursor-listener/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/cursor-listener/component.jsx @@ -2,9 +2,10 @@ import React, { Component } from 'react'; import { throttle } from 'lodash'; import PropTypes from 'prop-types'; +const { cursorInterval: CURSOR_INTERVAL } = Meteor.settings.public.whiteboard; + // maximum value of z-index to prevent other things from overlapping const MAX_Z_INDEX = (2 ** 31) - 1; -const CURSOR_INTERVAL = 40; export default class CursorListener extends Component { static touchCenterPoint(touches) { diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx index f791cb6a98dd7c9f5f5bd8fd8c3fe33689b79a7a..8a96c794062799b1f986497d71e09814d2edee15 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import {Meteor} from "meteor/meteor"; +import { Meteor } from 'meteor/meteor'; const ANNOTATION_CONFIG = Meteor.settings.public.whiteboard.annotations; const DRAW_START = ANNOTATION_CONFIG.status.start; @@ -51,6 +51,7 @@ export default class TextDrawListener extends Component { // Check it to figure if you can add onTouchStart in render(), or should use raw DOM api this.hasBeenTouchedRecently = false; + this.handleClick = this.handleClick.bind(this); this.handleMouseDown = this.handleMouseDown.bind(this); this.handleMouseMove = this.handleMouseMove.bind(this); this.handleMouseUp = this.handleMouseUp.bind(this); @@ -65,9 +66,9 @@ export default class TextDrawListener extends Component { componentDidMount() { window.addEventListener('beforeunload', this.sendLastMessage); + window.addEventListener('click', this.handleClick); } - // If the activeId suddenly became empty - this means the shape was deleted // While the user was drawing it. So we are resetting the state. componentWillReceiveProps(nextProps) { @@ -113,11 +114,17 @@ export default class TextDrawListener extends Component { componentWillUnmount() { window.removeEventListener('beforeunload', this.sendLastMessage); + window.removeEventListener('click', this.handleClick); // sending the last message on componentDidUnmount // for example in case when you switched a tool while drawing text shape this.sendLastMessage(); } + handleClick() { + const { isWritingText } = this.state; + if (isWritingText) this.sendLastMessage(); + } + // checks if the input textarea is focused or not, and if not - moves focus there // returns false if text area wasn't focused // returns true if textarea was focused @@ -510,8 +517,9 @@ export default class TextDrawListener extends Component { } = this.state; const { contextMenuHandler } = actions; - - const baseName = Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename + Meteor.settings.public.app.instanceId; + const { settings } = Meteor; + const { public: _public } = settings; + const baseName = _public.app.cdn + _public.app.basename + _public.app.instanceId; const textDrawStyle = { width: '100%', height: '100%', diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js index 26801f82b824983a479a7b221ca16b55206a0be0..d7c1a2f425367c64f1ec797a40566743eae3803e 100755 --- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js +++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js @@ -635,6 +635,10 @@ class AudioManager { return audioAlert.play(); } + + async updateAudioConstraints(constraints) { + await this.bridge.updateAudioConstraints(constraints); + } } const audioManager = new AudioManager(); diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 12d27d5696f1db96960f4e2161209af6a9944244..c9a21301f3ba09a215ed4d9b08d1b26e103e0d5d 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -56,6 +56,25 @@ public: raiseHandPushAlerts: true fallbackLocale: en overrideLocale: null + #Audio constraints for microphone. Use this to control browser's + #filters, such as AGC (Auto Gain Control) , Echo Cancellation, + #Noise Supression, etc. + #For more deails, see: + # https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints + #Currently, google chrome sets {ideal: true} for autoGainControl, + #echoCancellation and noiseSuppression, if not set. + #The accepted value for each constraint is an object of type + #https://developer.mozilla.org/en-US/docs/Web/API/ConstrainBoolean + #These values are used as initial constraints for every new participant, + #and can be changed by user in: Settings > Application > Microphone + #Audio Filters. + # microphoneConstraints: + # autoGainControl: + # ideal: true + # echoCancellation: + # ideal: true + # noiseSuppression: + # ideal: true audio: inputDeviceId: undefined outputDeviceId: undefined @@ -111,10 +130,12 @@ public: # Valid for video-provider. Time (ms) before its WS connection times out # and tries to reconnect. wsConnectionTimeout: 4000 + # Time in milis to wait for the browser to return a gUM call (used in video-preview) + gUMTimeout: 20000 cameraTimeouts: # Base camera timeout: used as the camera *sharing* timeout and # as the minimum camera subscribe reconnection timeout - baseTimeout: 15000 + baseTimeout: 30000 # Max timeout: used as the max camera subscribe reconnection timeout. Each # subscribe reattempt increases the reconnection timer up to this maxTimeout: 60000 @@ -253,9 +274,9 @@ public: mobilePageSizes: moderator: 2 viewer: 2 - pingPong: - clearUsersInSeconds: 180 - pongTimeInSeconds: 15 + syncUsersWithConnectionManager: + enabled: false + syncInterval: 60000 allowOutsideCommands: toggleRecording: false toggleSelfVoice: false @@ -324,6 +345,8 @@ public: #user activates microphone. iceGatheringTimeout: 5000 sipjsHackViaWs: false + # Mute/umute toggle throttle time + toggleMuteThrottleTime: 300 #Websocket keepAlive interval (seconds). You may set this to prevent #websocket disconnection in some environments. When set, BBB will send #'\r\n\r\n' string through SIP.js's websocket. If not set, default value @@ -401,6 +424,8 @@ public: role_moderator: MODERATOR role_viewer: VIEWER whiteboard: + annotationsQueueProcessInterval: 60 + cursorInterval: 40 annotations: status: start: DRAW_START @@ -539,6 +564,7 @@ private: leak: enabled: false heapdump: + thresholdMb: 1024 enabled: false heapdumpFolderPath: HEAPDUMP_FOLDER heapdumpIntervalMs: 3600000 diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index f5314096e525dbaef585fd526a625a29385af3f0..b28f48fd22f5ee59c56d7463d139ac688a9f3687 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -309,6 +309,7 @@ "app.screenshare.screenShareLabel" : "Screen share", "app.submenu.application.applicationSectionTitle": "Application", "app.submenu.application.animationsLabel": "Animations", + "app.submenu.application.audioFilterLabel": "Audio Filters for Microphone", "app.submenu.application.fontSizeControlLabel": "Font size", "app.submenu.application.increaseFontBtnLabel": "Increase application font size", "app.submenu.application.decreaseFontBtnLabel": "Decrease application font size", @@ -609,10 +610,16 @@ "app.video.iceConnectionStateError": "Connection failure (ICE error 1107)", "app.video.permissionError": "Error on sharing webcam. Please check permissions", "app.video.sharingError": "Error on sharing webcam", + "app.video.abortError": "Some problem occurred which prevented the device from being used", + "app.video.overconstrainedError": "No candidate devices which met the criteria requested", + "app.video.securityError": "Media support is disabled on the Document", + "app.video.typeError": "List of constraints specified is empty, or has all constraints set to false", "app.video.notFoundError": "Could not find webcam. Please make sure it's connected", "app.video.notAllowed": "Missing permission for share webcam, please make sure your browser permissions", "app.video.notSupportedError": "Can share webcam video only with safe sources, make sure your SSL certificate is valid", "app.video.notReadableError": "Could not get webcam video. Please make sure another program is not using the webcam ", + "app.video.timeoutError": "Browser did not respond in time.", + "app.video.genericError": "An unknown error has occurred with the device (Error {0})", "app.video.mediaFlowTimeout1020": "Media could not reach the server (error 1020)", "app.video.suggestWebcamLock": "Enforce lock setting to viewers webcams?", "app.video.suggestWebcamLockReason": "(this will improve the stability of the meeting)", @@ -739,4 +746,4 @@ "app.debugWindow.form.button.copy": "Copy", "app.debugWindow.form.enableAutoarrangeLayoutLabel": "Enable Auto Arrange Layout", "app.debugWindow.form.enableAutoarrangeLayoutDescription": "(it will be disabled if you drag or resize the webcams area)" -} \ No newline at end of file +} diff --git a/bigbluebutton-html5/private/locales/pt_BR.json b/bigbluebutton-html5/private/locales/pt_BR.json index a50ffdf759f12f9698e981183ac03a8c408b553d..2017603dad8e60c53886e5dad5ff3e5c6e750f4d 100644 --- a/bigbluebutton-html5/private/locales/pt_BR.json +++ b/bigbluebutton-html5/private/locales/pt_BR.json @@ -283,6 +283,7 @@ "app.screenshare.screenShareLabel" : "Compartilhamento de tela", "app.submenu.application.applicationSectionTitle": "Aplicação", "app.submenu.application.animationsLabel": "Animações", + "app.submenu.application.audioFilterLabel": "Filtros de áudio para o microfone", "app.submenu.application.fontSizeControlLabel": "Tamanho da fonte", "app.submenu.application.increaseFontBtnLabel": "Aumentar o tamanho da fonte da aplicação", "app.submenu.application.decreaseFontBtnLabel": "Diminuir o tamanho da fonte da aplicação",