diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ExternalProcessExecutor.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ExternalProcessExecutor.java index c92943eb4dca41c02f3c1d3f0c18eef612792453..d583f1eaad97ca8d8eb9445b9d3a7950e0f6f029 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ExternalProcessExecutor.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ExternalProcessExecutor.java @@ -19,66 +19,76 @@ package org.bigbluebutton.presentation.imp; -import java.util.Timer; -import java.util.TimerTask; +import java.io.File; +import java.io.IOException; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * A wrapper class the executes an external command. - * - * See http://kylecartmell.com/?p=9 + * A wrapper class the executes an external command. * * @author Richard Alam - * + * @author Marcel Hellkamp */ public class ExternalProcessExecutor { private static Logger log = LoggerFactory.getLogger(ExternalProcessExecutor.class); - - public boolean exec(String COMMAND, long timeoutMillis) { - Timer timer = new Timer(false); - Process p = null; - try { - InterruptTimerTask interrupter = new InterruptTimerTask(Thread.currentThread()); - timer.schedule(interrupter, timeoutMillis); - p = Runtime.getRuntime().exec(COMMAND); - int result = p.waitFor(); - if (result == 0) { - return true; - } else { - return false; - } + // Replace with ProcessBuilder.Redirect.DISCARD in java 9+ + private static File DISCARD = new File( + System.getProperty("os.name").startsWith("Windows") ? "NUL" : "/dev/null"); - } catch(Exception e) { - log.info("TIMEDOUT excuting : {}", COMMAND); - if (p != null) { - p.destroy(); - } - } finally { - timer.cancel(); // If the process returns within the timeout period, we have to stop the interrupter - // so that it does not unexpectedly interrupt some other code later. - - Thread.interrupted(); // We need to clear the interrupt flag on the current thread just in case - // interrupter executed after waitFor had already returned but before timer.cancel - // took effect. - // - // Oh, and there's also Sun bug 6420270 to worry about here. - } - return false; + /** + * Run COMMAND for at most timeoutMillis while ignoring any output. + * + * @deprecated The COMMAND string is split on whitespace to create an argument + * list. This won't work for arguments that contain whitespace. Use + * {@link #exec(List, Duration)} instead. + * + * @param COMMAND A single command or whitespace separated list of + * arguments. + * @param timeoutMillis Timeout in milliseconds. + * @return true if the command terminated in time with an exit value of 0. + */ + @Deprecated + public boolean exec(String COMMAND, long timeoutMillis) { + return exec(Arrays.asList(COMMAND.split("[ \\t\\n\\r\\f]+")), Duration.ofMillis(timeoutMillis)); } - - class InterruptTimerTask extends TimerTask { - private Thread thread; + /** + * Run a command for a limited amount of time while ignoring any output. + * + * @param cmd List containing the program and its arguments. + * @param timeout Maximum execution time. + * @return true if the command terminated in time with an exit value of 0. + */ + public boolean exec(List<String> cmd, Duration timeout) { - public InterruptTimerTask(Thread t) { - this.thread = t; - } + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.redirectError(DISCARD); + pb.redirectOutput(DISCARD); - public void run() { - thread.interrupt(); - } + Process proc; + try { + proc = pb.start(); + } catch (IOException e) { + log.error("Failed to execute: {}", String.join(" ", cmd), e); + return false; + } + try { + if (!proc.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS)) { + log.warn("TIMEDOUT excuting: {}", String.join(" ", cmd)); + proc.destroy(); + } + return !proc.isAlive() && proc.exitValue() == 0; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + proc.destroy(); + return false; + } } } diff --git a/bigbluebutton-config/bigbluebutton-release b/bigbluebutton-config/bigbluebutton-release index e248e56c569cb0a2e307ba8c771a88a371d49782..b8ca37e4f9914d60e24c3fbc86753be9348ec3ee 100644 --- a/bigbluebutton-config/bigbluebutton-release +++ b/bigbluebutton-config/bigbluebutton-release @@ -1 +1 @@ -BIGBLUEBUTTON_RELEASE=2.3.6 +BIGBLUEBUTTON_RELEASE=2.3.9 diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js index 4141f2a6633f03f7ab90c994e1f8d78833aae11d..587a2ae08cc20db302a65ab6cf49076fa20b4894 100755 --- a/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js +++ b/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js @@ -277,8 +277,16 @@ export default class KurentoAudioBridge extends BaseAudioBridge { } exitAudio() { + const mediaElement = document.getElementById(MEDIA_TAG); + this.broker.stop(); this.clearReconnectionTimeout(); + + if (mediaElement && typeof mediaElement.pause === 'function') { + mediaElement.pause(); + mediaElement.srcObject = null; + } + return Promise.resolve(); } } diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js index 67ef270c1fc7d759105af5d41e230854f9b55cc8..f3a5f329f56cabbec3b6d9839b1fff491e0b08cf 100755 --- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js +++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js @@ -730,17 +730,23 @@ class SIPSession { } onIceGatheringStateChange(event) { - const secondsToGatherIce = (new Date() - this._sessionStartTime) / 1000; - const iceGatheringState = event.target ? event.target.iceGatheringState : null; + if ((iceGatheringState === 'gathering') && (!this._iceGatheringStartTime)) { + this._iceGatheringStartTime = new Date(); + } + if (iceGatheringState === 'complete') { + const secondsToGatherIce = (new Date() + - (this._iceGatheringStartTime || this._sessionStartTime)) / 1000; + logger.info({ logCode: 'sip_js_ice_gathering_time', extraInfo: { callerIdName: this.user.callerIdName, + secondsToGatherIce, }, }, `ICE gathering candidates took (s): ${secondsToGatherIce}`); } diff --git a/bigbluebutton-html5/imports/api/breakouts/server/publishers.js b/bigbluebutton-html5/imports/api/breakouts/server/publishers.js index 1e9b6f0e0dc0270caa47d4bd273ff21f6966e797..f3c39edd06dc0f344412c35b3152cdb355ecd012 100755 --- a/bigbluebutton-html5/imports/api/breakouts/server/publishers.js +++ b/bigbluebutton-html5/imports/api/breakouts/server/publishers.js @@ -45,7 +45,28 @@ function breakouts(role) { ], }; - return Breakouts.find(selector); + const fields = { + fields: { + users: { + $elemMatch: { + // do not allow users to obtain 'redirectToHtml5JoinURL' for others + userId, + }, + }, + breakoutId: 1, + externalId: 1, + freeJoin: 1, + isDefaultName: 1, + joinedUsers: 1, + name: 1, + parentMeetingId: 1, + sequence: 1, + shortName: 1, + timeRemaining: 1, + }, + }; + + return Breakouts.find(selector, fields); } function publish(...args) { diff --git a/bigbluebutton-html5/imports/api/connection-status/server/methods/addConnectionStatus.js b/bigbluebutton-html5/imports/api/connection-status/server/methods/addConnectionStatus.js index 0c1c923014bdd985a900a6c346ad878b867a257d..f451f0d6af0272d18b047d91cbc4bf72c1f2fa49 100644 --- a/bigbluebutton-html5/imports/api/connection-status/server/methods/addConnectionStatus.js +++ b/bigbluebutton-html5/imports/api/connection-status/server/methods/addConnectionStatus.js @@ -11,8 +11,6 @@ const logConnectionStatus = (meetingId, userId, status, type, value) => { Logger.info(`Connection status updated: meetingId=${meetingId} userId=${userId} status=${status} type=${type}`); break; case 'warning': - // Skip - break; case 'danger': case 'critical': switch (type) { diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js index 82317c35ee78610f743a9fdd8720669fdce89f8d..9b4c17639a8bdd2262241559376e63beb4adba92 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js @@ -25,7 +25,7 @@ function groupChatMsg(chatsIds) { timestamp: { $gte: User.authTokenValidatedTime }, $or: [ { meetingId, chatId: { $eq: PUBLIC_GROUP_CHAT_ID } }, - { chatId: { $in: chatsIds } }, + { meetingId, chatId: { $in: chatsIds } }, ], }; return GroupChatMsg.find(selector); diff --git a/bigbluebutton-html5/imports/api/guest-users/server/publishers.js b/bigbluebutton-html5/imports/api/guest-users/server/publishers.js index bc0b063a221c4f914dde39d4894044ba78105222..136559b95faea131d1bde3c592b36f0d447afb38 100644 --- a/bigbluebutton-html5/imports/api/guest-users/server/publishers.js +++ b/bigbluebutton-html5/imports/api/guest-users/server/publishers.js @@ -1,18 +1,30 @@ import GuestUsers from '/imports/api/guest-users/'; +import Users from '/imports/api/users'; import { Meteor } from 'meteor/meteor'; import Logger from '/imports/startup/server/logger'; import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation'; +const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; + function guestUsers() { const tokenValidation = AuthTokenValidation.findOne({ connectionId: this.connection.id }); if (!tokenValidation || tokenValidation.validationStatus !== ValidationStates.VALIDATED) { - Logger.warn(`Publishing GuestUsers was requested by unauth connection ${this.connection.id}`); + Logger.warn(`Publishing GuestUser was requested by unauth connection ${this.connection.id}`); return GuestUsers.find({ meetingId: '' }); } const { meetingId, userId } = tokenValidation; + const User = Users.findOne({ userId, meetingId }, { fields: { role: 1 } }); + if (!User || User.role !== ROLE_MODERATOR) { + Logger.warn( + 'Publishing current-poll was requested by non-moderator connection', + { meetingId, userId, connectionId: this.connection.id }, + ); + return GuestUsers.find({ meetingId: '' }); + } + Logger.debug(`Publishing GuestUsers for ${meetingId} ${userId}`); return GuestUsers.find({ meetingId }); diff --git a/bigbluebutton-html5/imports/api/meetings/server/handlers/meetingEnd.js b/bigbluebutton-html5/imports/api/meetings/server/handlers/meetingEnd.js index 48e29bef3607186454f0afcc22cc73c7f927af49..9463afa4990d2a4f93e1d38447ad0cff3d32deb1 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/handlers/meetingEnd.js +++ b/bigbluebutton-html5/imports/api/meetings/server/handlers/meetingEnd.js @@ -6,7 +6,7 @@ import Logger from '/imports/startup/server/logger'; export default function handleMeetingEnd({ header, body }) { check(body, Object); - const { meetingId } = body; + const { meetingId, reason } = body; check(meetingId, String); check(header, Object); @@ -24,7 +24,7 @@ export default function handleMeetingEnd({ header, body }) { }; Meetings.update({ meetingId }, - { $set: { meetingEnded: true, meetingEndedBy: userId } }, + { $set: { meetingEnded: true, meetingEndedBy: userId, meetingEndedReason: reason } }, (err, num) => { cb(err, num, 'Meeting'); }); Breakouts.update({ parentMeetingId: meetingId }, diff --git a/bigbluebutton-html5/imports/api/polls/server/publishers.js b/bigbluebutton-html5/imports/api/polls/server/publishers.js index 85bbd08b0612f7a57df8768241dfad3ad51939d5..620a73b22dceb5847ae7647536ecb8b2bd8ee7cc 100644 --- a/bigbluebutton-html5/imports/api/polls/server/publishers.js +++ b/bigbluebutton-html5/imports/api/polls/server/publishers.js @@ -1,8 +1,10 @@ import { Meteor } from 'meteor/meteor'; import Logger from '/imports/startup/server/logger'; +import Users from '/imports/api/users'; import Polls from '/imports/api/polls'; import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation'; +const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; function currentPoll() { const tokenValidation = AuthTokenValidation.findOne({ connectionId: this.connection.id }); @@ -13,6 +15,15 @@ function currentPoll() { const { meetingId, userId } = tokenValidation; + const User = Users.findOne({ userId, meetingId }, { fields: { role: 1 } }); + if (!User || User.role !== ROLE_MODERATOR) { + Logger.warn( + 'Publishing current-poll was requested by non-moderator connection', + { meetingId, userId, connectionId: this.connection.id }, + ); + return Polls.find({ meetingId: '' }); + } + Logger.debug('Publishing Polls', { meetingId, userId }); const selector = { diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js index d63005606733887c0c3558f80d594ebdcdcb0520..42ee92cbd5f7d4c77993133f4b9f9549a02b4860 100755 --- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js +++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js @@ -283,6 +283,8 @@ export default class KurentoScreenshareBridge { }; stop() { + const mediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG); + if (this.broker) { this.broker.stop(); // Checks if this session is a sharer and if it's not reconnecting @@ -292,6 +294,12 @@ export default class KurentoScreenshareBridge { if (this.broker.role === SEND_ROLE && !this.reconnecting) setSharingScreen(false); this.broker = null; } + + if (mediaElement && typeof mediaElement.pause === 'function') { + mediaElement.pause(); + mediaElement.srcObject = null; + } + this.gdmStream = null; this.clearReconnectionTimeout(); } diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx index 636d680f9057115ed925659551eb2ae77486997d..da45c6d635f8c20500408872daaea103b6ee078a 100755 --- a/bigbluebutton-html5/imports/startup/client/base.jsx +++ b/bigbluebutton-html5/imports/startup/client/base.jsx @@ -226,6 +226,7 @@ class Base extends Component { ejectedReason, meetingExist, meetingHasEnded, + meetingEndedReason, meetingIsBreakout, subscriptionsReady, User, @@ -236,7 +237,7 @@ class Base extends Component { } if (ejected) { - return (<MeetingEnded code="403" reason={ejectedReason} />); + return (<MeetingEnded code="403" ejectedReason={ejectedReason} />); } if ((meetingHasEnded || User?.loggedOut) && meetingIsBreakout) { @@ -245,7 +246,7 @@ class Base extends Component { } if (((meetingHasEnded && !meetingIsBreakout)) || (codeError && User?.loggedOut)) { - return (<MeetingEnded code={codeError} />); + return (<MeetingEnded code={codeError} endedReason={meetingEndedReason} ejectedReason={ejectedReason} />); } if (codeError && !meetingHasEnded) { @@ -317,6 +318,7 @@ const BaseContainer = withTracker(() => { const meeting = Meetings.findOne({ meetingId }, { fields: { meetingEnded: 1, + meetingEndedReason: 1, meetingProp: 1, }, }); @@ -328,6 +330,7 @@ const BaseContainer = withTracker(() => { const approved = User?.approved && User?.guest; const ejected = User?.ejected; const ejectedReason = User?.ejectedReason; + const meetingEndedReason = meeting?.meetingEndedReason; let userSubscriptionHandler; @@ -425,6 +428,7 @@ const BaseContainer = withTracker(() => { isMeteorConnected: Meteor.status().connected, meetingExist: !!meeting, meetingHasEnded: !!meeting && meeting.meetingEnded, + meetingEndedReason, meetingIsBreakout: AppService.meetingIsBreakout(), subscriptionsReady: Session.get('subscriptionsReady'), loggedIn, diff --git a/bigbluebutton-html5/imports/ui/components/captions/pad/component.jsx b/bigbluebutton-html5/imports/ui/components/captions/pad/component.jsx index 8f2c57e9f853859b14b0749c63342e977812ce44..683092791ca0e60054423420d2d1e255fbfba174 100644 --- a/bigbluebutton-html5/imports/ui/components/captions/pad/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/captions/pad/component.jsx @@ -7,6 +7,7 @@ import Button from '/imports/ui/components/button/component'; import logger from '/imports/startup/client/logger'; import PadService from './service'; import CaptionsService from '/imports/ui/components/captions/service'; +import { notify } from '/imports/ui/services/notification'; import { styles } from './styles'; const intlMessages = defineMessages({ @@ -46,6 +47,10 @@ const intlMessages = defineMessages({ id: 'app.captions.pad.dictationOffDesc', description: 'Aria description for button that turns off speech recognition', }, + speechRecognitionStop: { + id: 'app.captions.pad.speechRecognitionStop', + description: 'Notification for stopped speech recognition', + }, }); const propTypes = { @@ -76,11 +81,21 @@ class Pad extends PureComponent { listening: false, }; - const { locale } = props; + const { locale, intl } = props; this.recognition = CaptionsService.initSpeechRecognition(locale); this.toggleListen = this.toggleListen.bind(this); this.handleListen = this.handleListen.bind(this); + + if (this.recognition) { + this.recognition.addEventListener('end', () => { + const { listening } = this.state; + if (listening) { + notify(intl.formatMessage(intlMessages.speechRecognitionStop), 'info', 'warning'); + this.stopListen(); + } + }); + } } componentDidUpdate() { @@ -91,8 +106,13 @@ class Pad extends PureComponent { } = this.props; if (this.recognition) { + if (ownerId !== currentUserId) { + this.recognition.stop(); + } else if (this.state.listening && this.recognition.lang !== locale) { + this.recognition.stop(); + this.stopListen(); + } this.recognition.lang = locale; - if (ownerId !== currentUserId) this.recognition.stop(); } } @@ -168,6 +188,10 @@ class Pad extends PureComponent { listening: !listening, }, this.handleListen); } + + stopListen() { + this.setState({ listening: false }); + } render() { const { diff --git a/bigbluebutton-html5/imports/ui/components/media/component.jsx b/bigbluebutton-html5/imports/ui/components/media/component.jsx index 4e578bb18d110f16d3525281f1e75450a4718c53..31e2fa01e2ef238311a44ec32aca7d5130b59d94 100644 --- a/bigbluebutton-html5/imports/ui/components/media/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/media/component.jsx @@ -18,6 +18,7 @@ const propTypes = { disableVideo: PropTypes.bool, audioModalIsOpen: PropTypes.bool, layoutContextState: PropTypes.instanceOf(Object).isRequired, + isRTL: PropTypes.bool.isRequired, }; const defaultProps = { @@ -46,6 +47,7 @@ export default class Media extends Component { usersVideo, layoutContextState, isMeteorConnected, + isRTL, } = this.props; const { webcamsPlacement: placement } = layoutContextState; @@ -117,6 +119,7 @@ export default class Media extends Component { disableVideo={disableVideo} audioModalIsOpen={audioModalIsOpen} usersVideo={usersVideo} + isRTL={isRTL} /> ) : null} </div> diff --git a/bigbluebutton-html5/imports/ui/components/media/container.jsx b/bigbluebutton-html5/imports/ui/components/media/container.jsx index 0c6cda22f901a1cf429c8d377a4f389c00636fed..0712c41549210dd6284dbdda31e7cec972850120 100755 --- a/bigbluebutton-html5/imports/ui/components/media/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/media/container.jsx @@ -137,6 +137,7 @@ export default withLayoutConsumer(withModalMounter(withTracker(() => { } data.webcamsPlacement = Storage.getItem('webcamsPlacement'); + data.isRTL = document.documentElement.getAttribute('dir') === 'rtl'; MediaContainer.propTypes = propTypes; return data; diff --git a/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx b/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx index bf32aa466b8feee5b09f2f25947d635ec8c052df..cc9fdeba4d866d4b4ef87fc3acff45622e3794cb 100644 --- a/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx @@ -21,6 +21,7 @@ const propTypes = { refMediaContainer: PropTypes.shape({ current: PropTypes.instanceOf(Element) }), layoutContextState: PropTypes.objectOf(Object).isRequired, layoutContextDispatch: PropTypes.func.isRequired, + isRTL: PropTypes.bool.isRequired, }; const defaultProps = { @@ -198,12 +199,12 @@ class WebcamDraggable extends PureComponent { } calculatePosition() { - const { layoutContextState } = this.props; + const { layoutContextState, isRTL } = this.props; const { mediaBounds } = layoutContextState; const { top: mediaTop, left: mediaLeft } = mediaBounds; const { top: webcamsListTop, left: webcamsListLeft } = this.getWebcamsListBounds(); - const x = webcamsListLeft - mediaLeft; + const x = !isRTL ? (webcamsListLeft - mediaLeft) : webcamsListLeft; const y = webcamsListTop - mediaTop; return { x, @@ -282,6 +283,7 @@ class WebcamDraggable extends PureComponent { hideOverlay, disableVideo, audioModalIsOpen, + isRTL, } = this.props; const { isMobile } = deviceInfo; @@ -429,7 +431,7 @@ class WebcamDraggable extends PureComponent { /> </div> <div - className={dropZoneLeftClassName} + className={!isRTL ? dropZoneLeftClassName : dropZoneRightClassName} style={{ width: '15vh', height: `calc(${mediaHeight}px - (15vh * 2))`, @@ -514,7 +516,7 @@ class WebcamDraggable extends PureComponent { /> </div> <div - className={dropZoneRightClassName} + className={!isRTL ? dropZoneRightClassName : dropZoneLeftClassName} style={{ width: '15vh', height: `calc(${mediaHeight}px - (15vh * 2))`, diff --git a/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx b/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx index 4ff69b97cc5a5c481977833107b28e0953b76b69..05a2e4ece4ca83785f2c14518f54a8c0c49e64a0 100755 --- a/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx @@ -40,6 +40,14 @@ const intlMessage = defineMessages({ id: 'app.meeting.endedByUserMessage', description: 'message informing who ended the meeting', }, + messageEndedByNoModeratorSingular: { + id: 'app.meeting.endedByNoModeratorMessageSingular', + description: 'message informing that the meeting was ended due to no moderator present (singular)', + }, + messageEndedByNoModeratorPlural: { + id: 'app.meeting.endedByNoModeratorMessagePlural', + description: 'message informing that the meeting was ended due to no moderator present (plural)', + }, buttonOkay: { id: 'app.meeting.endNotification.ok.label', description: 'label okay for button', @@ -95,11 +103,13 @@ const propTypes = { formatMessage: PropTypes.func.isRequired, }).isRequired, code: PropTypes.string.isRequired, - reason: PropTypes.string, + ejectedReason: PropTypes.string, + endedReason: PropTypes.string, }; const defaultProps = { - reason: null, + ejectedReason: null, + endedReason: null, }; class MeetingEnded extends PureComponent { @@ -123,6 +133,8 @@ class MeetingEnded extends PureComponent { const meeting = Meetings.findOne({ id: user.meetingID }); if (meeting) { + this.endWhenNoModeratorMinutes = meeting.durationProps.endWhenNoModeratorDelayInMinutes; + const endedBy = Users.findOne({ userId: meeting.meetingEndedBy, }, { fields: { name: 1 } }); @@ -136,6 +148,7 @@ class MeetingEnded extends PureComponent { this.confirmRedirect = this.confirmRedirect.bind(this); this.sendFeedback = this.sendFeedback.bind(this); this.shouldShowFeedback = this.shouldShowFeedback.bind(this); + this.getEndingMessage = this.getEndingMessage.bind(this); AudioManager.exitAudio(); Meteor.disconnect(); @@ -163,6 +176,26 @@ class MeetingEnded extends PureComponent { } } + getEndingMessage() { + const { intl, code, endedReason } = this.props; + + if (endedReason && endedReason === 'ENDED_DUE_TO_NO_MODERATOR') { + return this.endWhenNoModeratorMinutes === 1 + ? intl.formatMessage(intlMessage.messageEndedByNoModeratorSingular) + : intl.formatMessage(intlMessage.messageEndedByNoModeratorPlural, { 0: this.endWhenNoModeratorMinutes }); + } + + if (this.meetingEndedBy) { + return intl.formatMessage(intlMessage.messageEndedByUser, { 0: this.meetingEndedBy }); + } + + if (intlMessage[code]) { + return intl.formatMessage(intlMessage[code]); + } + + return intl.formatMessage(intlMessage[430]); + } + sendFeedback() { const { selected, @@ -210,19 +243,17 @@ class MeetingEnded extends PureComponent { } renderNoFeedback() { - const { intl, code, reason } = this.props; + const { intl, code, ejectedReason } = this.props; - const logMessage = reason === 'user_requested_eject_reason' ? 'User removed from the meeting' : 'Meeting ended component, no feedback configured'; - logger.info({ logCode: 'meeting_ended_code', extraInfo: { endedCode: code, reason } }, logMessage); + const logMessage = ejectedReason === 'user_requested_eject_reason' ? 'User removed from the meeting' : 'Meeting ended component, no feedback configured'; + logger.info({ logCode: 'meeting_ended_code', extraInfo: { endedCode: code, reason: ejectedReason } }, logMessage); return ( <div className={styles.parent}> <div className={styles.modal}> <div className={styles.content}> <h1 className={styles.title} data-test="meetingEndedModalTitle"> - {this.meetingEndedBy - ? intl.formatMessage(intlMessage.messageEndedByUser, { 0: this.meetingEndedBy }) - : intl.formatMessage(intlMessage[code] || intlMessage[430])} + {this.getEndingMessage()} </h1> {!allowRedirectToLogoutURL() ? null : ( <div> @@ -248,7 +279,7 @@ class MeetingEnded extends PureComponent { } renderFeedback() { - const { intl, code, reason } = this.props; + const { intl, code, ejectedReason } = this.props; const { selected, dispatched, @@ -256,17 +287,15 @@ class MeetingEnded extends PureComponent { const noRating = selected <= 0; - const logMessage = reason === 'user_requested_eject_reason' ? 'User removed from the meeting' : 'Meeting ended component, feedback allowed'; - logger.info({ logCode: 'meeting_ended_code', extraInfo: { endedCode: code, reason } }, logMessage); + const logMessage = ejectedReason === 'user_requested_eject_reason' ? 'User removed from the meeting' : 'Meeting ended component, feedback allowed'; + logger.info({ logCode: 'meeting_ended_code', extraInfo: { endedCode: code, reason: ejectedReason } }, logMessage); return ( <div className={styles.parent}> <div className={styles.modal} data-test="meetingEndedModal"> <div className={styles.content}> <h1 className={styles.title}> - { - intl.formatMessage(intlMessage[reason] || intlMessage[430]) - } + {this.getEndingMessage()} </h1> <div className={styles.text}> {this.shouldShowFeedback() diff --git a/bigbluebutton-html5/imports/ui/components/note/service.js b/bigbluebutton-html5/imports/ui/components/note/service.js index 369fbba7fd435ecfa09b78268acb468bcc4f5b0d..d25cf4a46f62e54294bc4157555ac3bb5719ee7b 100644 --- a/bigbluebutton-html5/imports/ui/components/note/service.js +++ b/bigbluebutton-html5/imports/ui/components/note/service.js @@ -42,7 +42,7 @@ const isLocked = () => { const meeting = Meetings.findOne({ meetingId: Auth.meetingID }, { fields: { 'lockSettingsProps.disableNote': 1 } }); const user = Users.findOne({ userId: Auth.userID }, { fields: { locked: 1, role: 1 } }); - if (meeting.lockSettingsProps && user.role !== ROLE_MODERATOR) { + if (meeting.lockSettingsProps && user.role !== ROLE_MODERATOR && user.locked) { return meeting.lockSettingsProps.disableNote; } return false; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js index 83bd7e632ea84781dfcf00713f7047b38486f4b2..e8d6ed69c2da55cb55cd2c09ae4a9326baa6720e 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/service.js +++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js @@ -284,16 +284,19 @@ const getActiveChats = ({ groupChatsMessages, groupChats, users }) => { const isVoiceOnlyUser = userId => userId.toString().startsWith('v_'); const isMeetingLocked = (id) => { - const meeting = Meetings.findOne({ meetingId: id }, { fields: { lockSettingsProps: 1 } }); + const meeting = Meetings.findOne({ meetingId: id }, + { fields: { lockSettingsProps: 1, usersProp: 1 } }); let isLocked = false; if (meeting.lockSettingsProps !== undefined) { - const lockSettings = meeting.lockSettingsProps; + const {lockSettingsProps:lockSettings, usersProp} = meeting; if (lockSettings.disableCam || lockSettings.disableMic || lockSettings.disablePrivateChat - || lockSettings.disablePublicChat) { + || lockSettings.disablePublicChat + || lockSettings.disableNote + || usersProp.webcamsOnlyForModerator) { isLocked = true; } } diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx index 7bd1a5a9e797e715b835b7e3587e7189699f19f2..87867c96b9b744fd3945db6a421e07cf814069b7 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx @@ -17,6 +17,7 @@ import CaptionsWriterMenu from '/imports/ui/components/captions/writer-menu/cont import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component'; import { styles } from './styles'; import { getUserNamesLink } from '/imports/ui/components/user-list/service'; +import Settings from '/imports/ui/services/settings'; const propTypes = { intl: PropTypes.shape({ @@ -156,16 +157,17 @@ class UserOptions extends PureComponent { onSaveUserNames() { const { intl, meetingName } = this.props; + const lang = Settings.application.locale; const date = new Date(); + + const dateString = lang ? date.toLocaleDateString(lang) : date.toLocaleDateString(); + const timeString = lang ? date.toLocaleTimeString(lang) : date.toLocaleTimeString(); + getUserNamesLink( intl.formatMessage(intlMessages.savedNamesListTitle, { 0: meetingName, - 1: `${date.toLocaleDateString( - document.documentElement.lang, - )}:${date.toLocaleTimeString( - document.documentElement.lang, - )}`, + 1: `${dateString}:${timeString}`, }), intl.formatMessage(intlMessages.sortedFirstNameHeading), intl.formatMessage(intlMessages.sortedLastNameHeading), diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx index 410b992826add6d5905b97c2106081380c1ee359..ba8df05b32cd3d1484dc291e50d10845b3f32ebb 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx @@ -787,7 +787,16 @@ class VideoProvider extends Component { } destroyVideoTag(stream) { - delete this.videoTags[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) { diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/service.js b/bigbluebutton-html5/imports/ui/components/video-provider/service.js index 27bdfe70c9d870e8c9c4dad5876a38cf15b1c49a..98f403b2b85b62f7c704871b9fba7fbb21d67eaa 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/service.js +++ b/bigbluebutton-html5/imports/ui/components/video-provider/service.js @@ -519,9 +519,14 @@ class VideoService { } webcamsOnlyForModerator() { - const m = Meetings.findOne({ meetingId: Auth.meetingID }, + const meeting = Meetings.findOne({ meetingId: Auth.meetingID }, { fields: { 'usersProp.webcamsOnlyForModerator': 1 } }); - return m?.usersProp ? m.usersProp.webcamsOnlyForModerator : false; + const user = Users.findOne({ userId: Auth.userID }, { fields: { locked: 1, role: 1 } }); + + if (meeting?.usersProp && user?.role !== ROLE_MODERATOR && user?.locked) { + return meeting.usersProp.webcamsOnlyForModerator; + } + return false; } getInfo() { diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index ea3770d7f80943229dce805fd4f59d712beb498f..e03abfab98caccb56fe8355bacd2b0e986f8a8ad 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -49,6 +49,7 @@ "app.captions.pad.dictationStop": "Stop dictation", "app.captions.pad.dictationOnDesc": "Turns speech recognition on", "app.captions.pad.dictationOffDesc": "Turns speech recognition off", + "app.captions.pad.speechRecognitionStop": "Speech recognition stopped due to the browser incompatibility or some time of silence", "app.textInput.sendLabel": "Send", "app.note.title": "Shared Notes", "app.note.label": "Note", @@ -140,6 +141,8 @@ "app.meeting.meetingTimeRemaining": "Meeting time remaining: {0}", "app.meeting.meetingTimeHasEnded": "Time ended. Meeting will close soon", "app.meeting.endedByUserMessage": "This session was ended by {0}", + "app.meeting.endedByNoModeratorMessageSingular": "The meeting has ended due to no moderator being present after one minute", + "app.meeting.endedByNoModeratorMessagePlural": "The meeting has ended due to no moderator being present after {0} minutes", "app.meeting.endedMessage": "You will be forwarded back to the home screen", "app.meeting.alertMeetingEndsUnderMinutesSingular": "Meeting is closing in one minute.", "app.meeting.alertMeetingEndsUnderMinutesPlural": "Meeting is closing in {0} minutes.", diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties index 422db063a1d5dfdc50c3313d693554757fd967e0..c3f4e7c2680b59611164a7dc620da2b801a456e0 100755 --- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties +++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties @@ -191,8 +191,8 @@ authenticatedGuest=true # # native2ascii -encoding UTF8 bigbluebutton.properties bigbluebutton.properties # -defaultWelcomeMessage=Welcome to <b>%%CONFNAME%%</b>!<br><br>For help on using BigBlueButton see these (short) <a href="https://www.bigbluebutton.org/html5"><u>tutorial videos</u></a>.<br><br>To join the audio bridge click the phone button. Use a headset to avoid causing background noise for others. -defaultWelcomeMessageFooter=This server is running <a href="https://docs.bigbluebutton.org/" target="_blank"><u>BigBlueButton</u></a>. +defaultWelcomeMessage=Welcome to <b>%%CONFNAME%%</b>!<br><br>For help on using BigBlueButton see these (short) <a href="https://bigbluebutton.org/html5"><u>tutorial videos</u></a>.<br><br>To join the audio bridge click the phone button. Use a headset to avoid causing background noise for others. +defaultWelcomeMessageFooter=This server is running <a href="https://bigbluebutton.org/" target="_blank"><u>BigBlueButton</u></a>. # Default maximum number of users a meeting can have. # Current default is 0 (meeting doesn't have a user limit).