Merge branch 'v2.2.x-release' of github.com:bigbluebutton/bigbluebutton into 09-16-merge

This commit is contained in:
Anton Georgiev 2020-09-17 14:37:28 +00:00
commit 377dc27a8d
31 changed files with 588 additions and 181 deletions

View File

@ -70,6 +70,16 @@ public class ApiParams {
public static final String LOCK_SETTINGS_LOCK_ON_JOIN = "lockSettingsLockOnJoin";
public static final String LOCK_SETTINGS_LOCK_ON_JOIN_CONFIGURABLE = "lockSettingsLockOnJoinConfigurable";
// New param passed on create call to callback when meeting ends.
// This is a duplicate of the endCallbackUrl meta param as we want this
// param to stay on the server and not propagated to client and recordings.
public static final String MEETING_ENDED_CALLBACK_URL = "meetingEndedURL";
// Param to end the meeting when there are no moderators after a certain period of time.
// Needed for classes where teacher gets disconnected and can't get back in. Prevents
// students from running amok.
public static final String END_WHEN_NO_MODERATOR = "endWhenNoModerator";
private ApiParams() {
throw new IllegalStateException("ApiParams is a utility class. Instanciation is forbidden.");
}

View File

@ -40,6 +40,7 @@ import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.URIBuilder;
import org.bigbluebutton.api.domain.GuestPolicy;
import org.bigbluebutton.api.domain.Meeting;
@ -778,27 +779,38 @@ public class MeetingService implements MessageListener {
String endCallbackUrl = "endCallbackUrl".toLowerCase();
Map<String, String> metadata = m.getMetadata();
if (!m.isBreakout() && metadata.containsKey(endCallbackUrl)) {
String callbackUrl = metadata.get(endCallbackUrl);
try {
if (!m.isBreakout()) {
if (metadata.containsKey(endCallbackUrl)) {
String callbackUrl = metadata.get(endCallbackUrl);
try {
callbackUrl = new URIBuilder(new URI(callbackUrl))
.addParameter("recordingmarks", m.haveRecordingMarks() ? "true" : "false")
.addParameter("meetingID", m.getExternalId()).build().toURL().toString();
callbackUrlService.handleMessage(new MeetingEndedEvent(m.getInternalId(), m.getExternalId(), m.getName(), callbackUrl));
} catch (MalformedURLException e) {
log.error("Malformed URL in callback url=[{}]", callbackUrl, e);
} catch (URISyntaxException e) {
log.error("URI Syntax error in callback url=[{}]", callbackUrl, e);
} catch (Exception e) {
log.error("Error in callback url=[{}]", callbackUrl, e);
.addParameter("recordingmarks", m.haveRecordingMarks() ? "true" : "false")
.addParameter("meetingID", m.getExternalId()).build().toURL().toString();
MeetingEndedEvent event = new MeetingEndedEvent(m.getInternalId(), m.getExternalId(), m.getName(), callbackUrl);
processMeetingEndedCallback(event);
} catch (Exception e) {
log.error("Error in callback url=[{}]", callbackUrl, e);
}
}
if (! StringUtils.isEmpty(m.getMeetingEndedCallbackURL())) {
String meetingEndedCallbackURL = m.getMeetingEndedCallbackURL();
callbackUrlService.handleMessage(new MeetingEndedEvent(m.getInternalId(), m.getExternalId(), m.getName(), meetingEndedCallbackURL));
}
}
processRemoveEndedMeeting(message);
}
}
private void processMeetingEndedCallback(MeetingEndedEvent event) {
try {
callbackUrlService.handleMessage(event);
} catch (Exception e) {
log.error("Error in callback url=[{}]", event.getCallbackUrl(), e);
}
}
private void userJoined(UserJoined message) {
Meeting m = getMeeting(message.meetingId);
if (m != null) {

View File

@ -114,6 +114,7 @@ public class ParamsProcessorUtil {
private Integer userInactivityThresholdInMinutes = 30;
private Integer userActivitySignResponseDelayInMinutes = 5;
private Boolean defaultAllowDuplicateExtUserid = true;
private Boolean defaultEndWhenNoModerator = false;
private String formatConfNum(String s) {
if (s.length() > 5) {
@ -420,6 +421,15 @@ public class ParamsProcessorUtil {
}
}
boolean endWhenNoModerator = defaultEndWhenNoModerator;
if (!StringUtils.isEmpty(params.get(ApiParams.END_WHEN_NO_MODERATOR))) {
try {
endWhenNoModerator = Boolean.parseBoolean(params.get(ApiParams.END_WHEN_NO_MODERATOR));
} catch (Exception ex) {
log.warn("Invalid param [endWhenNoModerator] for meeting=[{}]", internalMeetingId);
}
}
String guestPolicy = defaultGuestPolicy;
if (!StringUtils.isEmpty(params.get(ApiParams.GUEST_POLICY))) {
guestPolicy = params.get(ApiParams.GUEST_POLICY);
@ -487,6 +497,13 @@ public class ParamsProcessorUtil {
meeting.setModeratorOnlyMessage(moderatorOnlyMessage);
}
if (!StringUtils.isEmpty(params.get(ApiParams.MEETING_ENDED_CALLBACK_URL))) {
String meetingEndedCallbackURL = params.get(ApiParams.MEETING_ENDED_CALLBACK_URL);
meeting.setMeetingEndedCallbackURL(meetingEndedCallbackURL);
}
meeting.setMaxInactivityTimeoutMinutes(maxInactivityTimeoutMinutes);
meeting.setWarnMinutesBeforeMax(warnMinutesBeforeMax);
meeting.setMeetingExpireIfNoUserJoinedInMinutes(meetingExpireIfNoUserJoinedInMinutes);
meeting.setMeetingExpireWhenLastUserLeftInMinutes(meetingExpireWhenLastUserLeftInMinutes);
meeting.setUserInactivityInspectTimerInMinutes(userInactivityInspectTimerInMinutes);
@ -1115,4 +1132,10 @@ public class ParamsProcessorUtil {
public void setAllowDuplicateExtUserid(Boolean allow) {
this.defaultAllowDuplicateExtUserid = allow;
}
public void setEndWhenNoModerator(Boolean val) {
this.defaultEndWhenNoModerator = val;
}
}

View File

@ -92,6 +92,11 @@ public class Meeting {
public final Boolean allowDuplicateExtUserid;
private String meetingEndedCallbackURL = "";
public final Boolean endWhenNoModerator;
public Meeting(Meeting.Builder builder) {
name = builder.name;
extMeetingId = builder.externalId;
@ -120,7 +125,8 @@ public class Meeting {
guestPolicy = builder.guestPolicy;
breakoutRoomsParams = builder.breakoutRoomsParams;
lockSettingsParams = builder.lockSettingsParams;
allowDuplicateExtUserid = builder.allowDuplicateExtUserid;
allowDuplicateExtUserid = builder.allowDuplicateExtUserid;
endWhenNoModerator = builder.endWhenNoModerator;
userCustomData = new HashMap<>();
@ -563,6 +569,14 @@ public class Meeting {
this.userActivitySignResponseDelayInMinutes = userActivitySignResponseDelayInMinutes;
}
public String getMeetingEndedCallbackURL() {
return meetingEndedCallbackURL;
}
public void setMeetingEndedCallbackURL(String meetingEndedCallbackURL) {
this.meetingEndedCallbackURL = meetingEndedCallbackURL;
}
public Map<String, Object> getUserCustomData(String userID){
return (Map<String, Object>) userCustomData.get(userID);
}
@ -612,6 +626,7 @@ public class Meeting {
private BreakoutRoomsParams breakoutRoomsParams;
private LockSettingsParams lockSettingsParams;
private Boolean allowDuplicateExtUserid;
private Boolean endWhenNoModerator;
public Builder(String externalId, String internalId, long createTime) {
this.externalId = externalId;
@ -743,6 +758,11 @@ public class Meeting {
this.allowDuplicateExtUserid = allowDuplicateExtUserid;
return this;
}
public Builder withEndWhenNoModerator(Boolean endWhenNoModerator) {
this.endWhenNoModerator = endWhenNoModerator;
return this;
}
public Meeting build() {
return new Meeting(this);

View File

@ -5,8 +5,8 @@
# which (if exists) will be run by `bbb-conf --setip` and `bbb-conf --restart` before restarting
# BigBlueButton.
#
# The purpose of apply-config.sh is to make it easy for you apply defaults to BigBlueButton server that get applied after
# each package update (since the last step in doing an upate is to run `bbb-conf --setip`.
# The purpose of apply-config.sh is to make it easy to apply your configuration changes to a BigBlueButton server
# before BigBlueButton starts
#
@ -74,7 +74,19 @@ HERE
}
# Enable firewall rules to lock down access to server
enableHTML5CameraQualityThresholds() {
echo " - Enable HTML5 cameraQualityThresholds"
yq w -i $HTML5_CONFIG public.kurento.cameraQualityThresholds.enabled true
}
enableHTML5WebcamPagination() {
echo " - Enable HTML5 webcam pagination"
yq w -i $HTML5_CONFIG public.kurento.pagination.enabled true
}
#
# Enable firewall rules to open only
#
enableUFWRules() {
echo " - Enable Firewall and opening 22/tcp, 80/tcp, 443/tcp and 16384:32768/udp"
@ -90,6 +102,123 @@ enableUFWRules() {
}
enableMultipleKurentos() {
echo " - Configuring three Kurento Media Servers: one for listen only, webcam, and screeshare"
# Step 1. Setup shared certificate between FreeSWITCH and Kurento
HOSTNAME=$(cat /etc/nginx/sites-available/bigbluebutton | grep -v '#' | sed -n '/server_name/{s/.*server_name[ ]*//;s/;//;p}' | cut -d' ' -f1 | head -n 1)
openssl req -x509 -new -nodes -newkey rsa:2048 -sha256 -days 3650 -subj "/C=BR/ST=Ottawa/O=BigBlueButton Inc./OU=Live/CN=$HOSTNAME" -keyout /tmp/dtls-srtp-key.pem -out /tmp/dtls-srtp-cert.pem
cat /tmp/dtls-srtp-key.pem /tmp/dtls-srtp-cert.pem > /etc/kurento/dtls-srtp.pem
cat /tmp/dtls-srtp-key.pem /tmp/dtls-srtp-cert.pem > /opt/freeswitch/etc/freeswitch/tls/dtls-srtp.pem
sed -i 's/;pemCertificateRSA=.*/pemCertificateRSA=\/etc\/kurento\/dtls-srtp.pem/g' /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini
# Step 2. Setup systemd unit files to launch three separate instances of Kurento
for i in `seq 8888 8890`; do
cat > /usr/lib/systemd/system/kurento-media-server-${i}.service << HERE
# /usr/lib/systemd/system/kurento-media-server-#{i}.service
[Unit]
Description=Kurento Media Server daemon (${i})
After=network.target
PartOf=kurento-media-server.service
After=kurento-media-server.service
[Service]
UMask=0002
Environment=KURENTO_LOGS_PATH=/var/log/kurento-media-server
Environment=KURENTO_CONF_FILE=/etc/kurento/kurento-${i}.conf.json
User=kurento
Group=kurento
LimitNOFILE=1000000
ExecStartPre=-/bin/rm -f /var/kurento/.cache/gstreamer-1.5/registry.x86_64.bin
ExecStart=/usr/bin/kurento-media-server --gst-debug-level=3 --gst-debug="3,Kurento*:4,kms*:4,KurentoWebSocketTransport:5"
Type=simple
PIDFile=/var/run/kurento-media-server-${i}.pid
Restart=always
[Install]
WantedBy=kurento-media-server.service
HERE
# Make a new configuration file each instance of Kurento that binds to a different port
cp /etc/kurento/kurento.conf.json /etc/kurento/kurento-${i}.conf.json
sed -i "s/8888/${i}/g" /etc/kurento/kurento-${i}.conf.json
done
# Step 3. Override the main kurento-media-server unit to start/stop the three Kurento instances
cat > /etc/systemd/system/kurento-media-server.service << HERE
[Unit]
Description=Kurento Media Server
[Service]
Type=oneshot
ExecStart=/bin/true
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
HERE
systemctl daemon-reload
for i in `seq 8888 8890`; do
systemctl enable kurento-media-server-${i}.service
done
# Step 4. Modify bbb-webrtc-sfu config to use the three Kurento servers
KURENTO_CONFIG=/usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml
MEDIA_TYPE=(main audio content)
IP=$(yq r /usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml kurento[0].ip)
for i in `seq 0 2`; do
yq w -i $KURENTO_CONFIG "kurento[$i].ip" $IP
yq w -i $KURENTO_CONFIG "kurento[$i].url" "ws://127.0.0.1:$(($i + 8888))/kurento"
yq w -i $KURENTO_CONFIG "kurento[$i].mediaType" "${MEDIA_TYPE[$i]}"
yq w -i $KURENTO_CONFIG "kurento[$i].ipClassMappings.local" ""
yq w -i $KURENTO_CONFIG "kurento[$i].ipClassMappings.private" ""
yq w -i $KURENTO_CONFIG "kurento[$i].ipClassMappings.public" ""
yq w -i $KURENTO_CONFIG "kurento[$i].options.failAfter" 5
yq w -i $KURENTO_CONFIG "kurento[$i].options.request_timeout" 30000
yq w -i $KURENTO_CONFIG "kurento[$i].options.response_timeout" 30000
done
yq w -i $KURENTO_CONFIG balancing-strategy MEDIA_TYPE
}
disableMultipleKurentos() {
echo " - Configuring a single Kurento Media Server for listen only, webcam, and screeshare"
systemctl stop kurento-media-server.service
for i in `seq 8888 8890`; do
systemctl disable kurento-media-server-${i}.service
done
# Remove the overrride (restoring the original kurento-media-server.service unit file)
rm -f /etc/systemd/system/kurento-media-server.service
systemctl daemon-reload
# Restore bbb-webrtc-sfu configuration to use a single instance of Kurento
KURENTO_CONFIG=/usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml
yq d -i $KURENTO_CONFIG kurento[1]
yq d -i $KURENTO_CONFIG kurento[1]
yq w -i $KURENTO_CONFIG "kurento[0].url" "ws://127.0.0.1:8888/kurento"
yq w -i $KURENTO_CONFIG "kurento[0].mediaType" ""
yq w -i $KURENTO_CONFIG balancing-strategy ROUND_ROBIN
}
notCalled() {
#
# This function is not called.
@ -112,6 +241,9 @@ source /etc/bigbluebutton/bbb-conf/apply-lib.sh
#enableHTML5ClientLog
#enableUFWRules
#enableHTML5CameraQualityThresholds
#enableHTML5WebcamPagination
HERE
chmod +x /etc/bigbluebutton/bbb-conf/apply-config.sh
## Stop Copying HERE

View File

@ -1996,16 +1996,17 @@ if [ -n "$HOST" ]; then
#fi
fi
ESL_PASSWORD=$(xmlstarlet sel -t -m 'configuration/settings/param[@name="password"]' -v @value /opt/freeswitch/etc/freeswitch/autoload_configs/event_socket.conf.xml)
#
# Update ESL passwords in three configuration files
#
ESL_PASSWORD=$(cat /usr/share/bbb-fsesl-akka/conf/application.conf | grep password | head -n 1 | sed 's/.*="//g' | sed 's/"//g')
if [ "$ESL_PASSWORD" == "ClueCon" ]; then
ESL_PASSWORD=$(openssl rand -hex 8)
echo "Changing default password for FreeSWITCH Event Socket Layer (see /opt/freeswitch/etc/freeswitch/autoload_configs/event_socket.conf.xml)"
sudo sed -i "s/ClueCon/$ESL_PASSWORD/g" /usr/share/bbb-fsesl-akka/conf/application.conf
fi
# Update all references to ESL password
sudo sed -i "s/ClueCon/$ESL_PASSWORD/g" /opt/freeswitch/etc/freeswitch/autoload_configs/event_socket.conf.xml
sudo sed -i "s/ClueCon/$ESL_PASSWORD/g" /usr/share/bbb-fsesl-akka/conf/application.conf
sudo yq w -i /usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml freeswitch.esl_password "$ESL_PASSWORD"
sudo xmlstarlet edit --inplace --update 'configuration/settings//param[@name="password"]/@value' --value $ESL_PASSWORD /opt/freeswitch/etc/freeswitch/autoload_configs/event_socket.conf.xml
echo "Restarting the BigBlueButton $BIGBLUEBUTTON_RELEASE ..."

View File

@ -6,11 +6,14 @@ import { extractCredentials } from '/imports/api/common/server/helpers';
export default function createBreakoutRoom(rooms, durationInMinutes, record = false) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const BREAKOUT_LIM = Meteor.settings.public.app.breakoutRoomLimit;
const MIN_BREAKOUT_ROOMS = 2;
const MAX_BREAKOUT_ROOMS = BREAKOUT_LIM > MIN_BREAKOUT_ROOMS ? BREAKOUT_LIM : MIN_BREAKOUT_ROOMS;
const { meetingId, requesterUserId } = extractCredentials(this.userId);
const eventName = 'CreateBreakoutRoomsCmdMsg';
if (rooms.length > 8) return Logger.info(`Attempt to create breakout rooms with invalid number of rooms in meeting id=${meetingId}`);
if (rooms.length > MAX_BREAKOUT_ROOMS) return Logger.info(`Attempt to create breakout rooms with invalid number of rooms in meeting id=${meetingId}`);
const payload = {
record,
durationInMinutes,

View File

@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Meetings from '/imports/api/meetings';
import Users from '/imports/api/users';
import RedisPubSub from '/imports/startup/server/redis';
import { extractCredentials } from '/imports/api/common/server/helpers';
@ -10,16 +11,29 @@ export default function startWatchingExternalVideo(options) {
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'StartExternalVideoMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
const { meetingId, requesterUserId: userId } = extractCredentials(this.userId);
const { externalVideoUrl } = options;
check(externalVideoUrl, String);
try {
check(meetingId, String);
check(userId, String);
check(externalVideoUrl, String);
Meetings.update({ meetingId }, { $set: { externalVideoUrl } });
const user = Users.findOne({ meetingId, userId, presenter: true }, { presenter: 1 });
const payload = { externalVideoUrl };
if (!user) {
Logger.error(`Only presenters are allowed to start external video for a meeting. meeting=${meetingId} userId=${userId}`);
return;
}
Logger.info(`User id=${requesterUserId} sharing an external video: ${externalVideoUrl} for meeting ${meetingId}`);
Meetings.update({ meetingId }, { $set: { externalVideoUrl } });
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
const payload = { externalVideoUrl };
Logger.info(`User id=${userId} sharing an external video: ${externalVideoUrl} for meeting ${meetingId}`);
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, userId, payload);
} catch (error) {
Logger.error(`Error on sharing an external video: ${externalVideoUrl} ${error}`);
}
}

View File

@ -1,6 +1,7 @@
import { Meteor } from 'meteor/meteor';
import Logger from '/imports/startup/server/logger';
import Meetings from '/imports/api/meetings';
import Users from '/imports/api/users';
import RedisPubSub from '/imports/startup/server/redis';
import { extractCredentials } from '/imports/api/common/server/helpers';
@ -9,19 +10,33 @@ export default function stopWatchingExternalVideo(options) {
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'StopExternalVideoMsg';
if (this.userId) {
options = extractCredentials(this.userId);
const { meetingId, requesterUserId } = this.userId ? extractCredentials(this.userId) : options;
try {
check(meetingId, String);
check(requesterUserId, String);
const user = Users.findOne({
meetingId,
userId: requesterUserId,
presenter: true,
}, { presenter: 1 });
if (this.userId && !user) {
Logger.error(`Only presenters are allowed to stop external video for a meeting. meeting=${meetingId} userId=${requesterUserId}`);
return;
}
const meeting = Meetings.findOne({ meetingId });
if (!meeting || meeting.externalVideoUrl === null) return;
Meetings.update({ meetingId }, { $set: { externalVideoUrl: null } });
const payload = {};
Logger.info(`User id=${requesterUserId} stopped sharing an external video for meeting=${meetingId}`);
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
} catch (error) {
Logger.error(`Error on stop sharing an external video for meeting=${meetingId} ${error}`);
}
const { meetingId, requesterUserId } = options;
const meeting = Meetings.findOne({ meetingId });
if (!meeting || meeting.externalVideoUrl === null) return;
Meetings.update({ meetingId }, { $set: { externalVideoUrl: null } });
const payload = {};
Logger.info(`User id=${requesterUserId} stopped sharing an external video for meeting=${meetingId}`);
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}

View File

@ -22,6 +22,7 @@ import clearLocalSettings from '/imports/api/local-settings/server/modifiers/cle
import clearRecordMeeting from './clearRecordMeeting';
import clearVoiceCallStates from '/imports/api/voice-call-states/server/modifiers/clearVoiceCallStates';
import clearVideoStreams from '/imports/api/video-streams/server/modifiers/clearVideoStreams';
import BannedUsers from '/imports/api/users/server/store/bannedUsers';
export default function meetingHasEnded(meetingId) {
removeAnnotationsStreamer(meetingId);
@ -46,6 +47,7 @@ export default function meetingHasEnded(meetingId) {
clearRecordMeeting(meetingId);
clearVoiceCallStates(meetingId);
clearVideoStreams(meetingId);
BannedUsers.delete(meetingId);
return Logger.info(`Cleared Meetings with id ${meetingId}`);
});

View File

@ -63,15 +63,80 @@ export default class KurentoScreenshareBridge {
return normalizedError;
}
async kurentoWatchVideo() {
static playElement(screenshareMediaElement) {
const mediaTagPlayed = () => {
logger.info({
logCode: 'screenshare_media_play_success',
}, 'Screenshare media played successfully');
};
if (screenshareMediaElement.paused) {
// Tag isn't playing yet. Play it.
screenshareMediaElement.play()
.then(mediaTagPlayed)
.catch((error) => {
// NotAllowedError equals autoplay issues, fire autoplay handling event.
// This will be handled in the screenshare react component.
if (error.name === 'NotAllowedError') {
logger.error({
logCode: 'screenshare_error_autoplay',
extraInfo: { errorName: error.name },
}, 'Screenshare play failed due to autoplay error');
const tagFailedEvent = new CustomEvent('screensharePlayFailed',
{ detail: { mediaElement: screenshareMediaElement } });
window.dispatchEvent(tagFailedEvent);
} else {
// Tag failed for reasons other than autoplay. Log the error and
// try playing again a few times until it works or fails for good
const played = playAndRetry(screenshareMediaElement);
if (!played) {
logger.error({
logCode: 'screenshare_error_media_play_failed',
extraInfo: { errorName: error.name },
}, `Screenshare media play failed due to ${error.name}`);
} else {
mediaTagPlayed();
}
}
});
} else {
// Media tag is already playing, so log a success. This is really a
// logging fallback for a case that shouldn't happen. But if it does
// (ie someone re-enables the autoPlay prop in the element), then it
// means the stream is playing properly and it'll be logged.
mediaTagPlayed();
}
}
static screenshareElementLoadAndPlay(stream, element, muted) {
element.muted = muted;
element.pause();
element.srcObject = stream;
KurentoScreenshareBridge.playElement(element);
}
kurentoViewLocalPreview() {
const screenshareMediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
const { webRtcPeer } = window.kurentoManager.kurentoScreenshare;
if (webRtcPeer) {
const stream = webRtcPeer.getLocalStream();
KurentoScreenshareBridge.screenshareElementLoadAndPlay(stream, screenshareMediaElement, true);
}
}
async kurentoViewScreen() {
const screenshareMediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
let iceServers = [];
let started = false;
try {
iceServers = await fetchWebRTCMappedStunTurnServers(getSessionToken());
} catch (error) {
logger.error({ logCode: 'screenshare_viwer_fetchstunturninfo_error', extraInfo: { error } },
'Screenshare bridge failed to fetch STUN/TURN info, using default');
logger.error({
logCode: 'screenshare_viewer_fetchstunturninfo_error',
extraInfo: { error },
}, 'Screenshare bridge failed to fetch STUN/TURN info, using default');
iceServers = getMappedFallbackStun();
} finally {
const options = {
@ -81,52 +146,6 @@ export default class KurentoScreenshareBridge {
userName: getUsername(),
};
const screenshareTag = document.getElementById(SCREENSHARE_VIDEO_TAG);
const playElement = () => {
const mediaTagPlayed = () => {
logger.info({
logCode: 'screenshare_viewer_media_play_success',
}, 'Screenshare viewer media played successfully');
};
if (screenshareTag.paused) {
// Tag isn't playing yet. Play it.
screenshareTag.play()
.then(mediaTagPlayed)
.catch((error) => {
// NotAllowedError equals autoplay issues, fire autoplay handling event.
// This will be handled in the screenshare react component.
if (error.name === 'NotAllowedError') {
logger.error({
logCode: 'screenshare_viewer_error_autoplay',
extraInfo: { errorName: error.name },
}, 'Screenshare viewer play failed due to autoplay error');
const tagFailedEvent = new CustomEvent('screensharePlayFailed',
{ detail: { mediaElement: screenshareTag } });
window.dispatchEvent(tagFailedEvent);
} else {
// Tag failed for reasons other than autoplay. Log the error and
// try playing again a few times until it works or fails for good
const played = playAndRetry(screenshareTag);
if (!played) {
logger.error({
logCode: 'screenshare_viewer_error_media_play_failed',
extraInfo: { errorName: error.name },
}, `Screenshare viewer media play failed due to ${error.name}`);
} else {
mediaTagPlayed();
}
}
});
} else {
// Media tag is already playing, so log a success. This is really a
// logging fallback for a case that shouldn't happen. But if it does
// (ie someone re-enables the autoPlay prop in the element), then it
// means the stream is playing properly and it'll be logged.
mediaTagPlayed();
}
};
const onFail = (error) => {
KurentoScreenshareBridge.handleViewerFailure(error, started);
};
@ -139,10 +158,11 @@ export default class KurentoScreenshareBridge {
const { webRtcPeer } = window.kurentoManager.kurentoVideo;
if (webRtcPeer) {
const stream = webRtcPeer.getRemoteStream();
screenshareTag.muted = true;
screenshareTag.pause();
screenshareTag.srcObject = stream;
playElement();
KurentoScreenshareBridge.screenshareElementLoadAndPlay(
stream,
screenshareMediaElement,
true,
);
}
};

View File

@ -70,7 +70,7 @@ const currentParameters = [
function valueParser(val) {
try {
const parsedValue = JSON.parse(val.toLowerCase());
const parsedValue = JSON.parse(val.toLowerCase().trim());
return parsedValue;
} catch (error) {
logger.warn(`addUserSettings:Parameter ${val} could not be parsed (was not json)`);
@ -87,21 +87,22 @@ export default function addUserSettings(settings) {
settings.forEach((el) => {
const settingKey = Object.keys(el).shift();
const normalizedKey = settingKey.trim();
if (currentParameters.includes(settingKey)) {
if (!Object.keys(parameters).includes(settingKey)) {
if (currentParameters.includes(normalizedKey)) {
if (!Object.keys(parameters).includes(normalizedKey)) {
parameters = {
[settingKey]: valueParser(el[settingKey]),
[normalizedKey]: valueParser(el[settingKey]),
...parameters,
};
} else {
parameters[settingKey] = el[settingKey];
parameters[normalizedKey] = el[settingKey];
}
return;
}
if (oldParametersKeys.includes(settingKey)) {
const matchingNewKey = oldParameters[settingKey];
if (oldParametersKeys.includes(normalizedKey)) {
const matchingNewKey = oldParameters[normalizedKey];
if (!Object.keys(parameters).includes(matchingNewKey)) {
parameters = {
[matchingNewKey]: valueParser(el[settingKey]),
@ -111,7 +112,7 @@ export default function addUserSettings(settings) {
return;
}
logger.warn(`Parameter ${settingKey} not handled`);
logger.warn(`Parameter ${normalizedKey} not handled`);
});
const settingsAdded = [];

View File

@ -7,7 +7,7 @@ class BannedUsers {
}
init(meetingId) {
Logger.debug('BannedUsers :: init', meetingId);
Logger.debug('BannedUsers :: init', { meetingId });
if (!this.store[meetingId]) this.store[meetingId] = new Set();
}
@ -20,7 +20,7 @@ class BannedUsers {
}
delete(meetingId) {
Logger.debug('BannedUsers :: delete', meetingId);
Logger.debug('BannedUsers :: delete', { meetingId });
delete this.store[meetingId];
}

View File

@ -109,8 +109,9 @@ const intlMessages = defineMessages({
},
});
const BREAKOUT_LIM = Meteor.settings.public.app.breakoutRoomLimit;
const MIN_BREAKOUT_ROOMS = 2;
const MAX_BREAKOUT_ROOMS = 8;
const MAX_BREAKOUT_ROOMS = BREAKOUT_LIM > MIN_BREAKOUT_ROOMS ? BREAKOUT_LIM : MIN_BREAKOUT_ROOMS;
const propTypes = {
intl: PropTypes.object.isRequired,

View File

@ -117,11 +117,12 @@ input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-i
}
.boxContainer {
height: 50vh;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 33% 33% 33%;
grid-template-columns: repeat(3, minmax(4rem, 16rem));
grid-template-rows: repeat(auto-fill, minmax(4rem, 8rem));
grid-gap: 1.5rem 1rem;
box-sizing: border-box;
padding-bottom: 1rem;
}
.changeToWarn {

View File

@ -261,7 +261,7 @@ class BreakoutRoom extends PureComponent {
>
<div className={styles.content} key={`breakoutRoomList-${breakout.breakoutId}`}>
<span aria-hidden>
{intl.formatMessage(intlMessages.breakoutRoom, breakout.sequence.toString())}
{intl.formatMessage(intlMessages.breakoutRoom, { 0: breakout.sequence })}
<span className={styles.usersAssignedNumberLabel}>
(
{breakout.joinedUsers.length}

View File

@ -110,7 +110,7 @@ export default withLayoutConsumer(withModalMounter(withTracker(() => {
const { dataSaving } = Settings;
const { viewParticipantsWebcams, viewScreenshare } = dataSaving;
const hidePresentation = getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation);
const autoSwapLayout = getFromUserSettings('userdata-bbb_auto_swap_layout', LAYOUT_CONFIG.autoSwapLayout);
const autoSwapLayout = getFromUserSettings('bbb_auto_swap_layout', LAYOUT_CONFIG.autoSwapLayout);
const { current_presentation: hasPresentation } = MediaService.getPresentationInfo();
const data = {
children: <DefaultContent {...{ autoSwapLayout, hidePresentation }} />,

View File

@ -11,6 +11,7 @@ import { styles } from './styles';
import logger from '/imports/startup/client/logger';
import Users from '/imports/api/users';
import AudioManager from '/imports/ui/services/audio-manager';
import { meetingIsBreakout } from '/imports/ui/components/app/service';
const intlMessage = defineMessages({
410: {
@ -129,6 +130,7 @@ class MeetingEnded extends PureComponent {
} = this.state;
if (selected <= 0) {
if (meetingIsBreakout()) window.close();
logoutRouteHandler();
return;
}

View File

@ -7,18 +7,20 @@ import { tryGenerateIceCandidates } from '/imports/utils/safari-webrtc';
import { stopWatching } from '/imports/ui/components/external-video-player/service';
import Meetings from '/imports/api/meetings';
import Auth from '/imports/ui/services/auth';
import UserListService from '/imports/ui/components/user-list/service';
// when the meeting information has been updated check to see if it was
// screensharing. If it has changed either trigger a call to receive video
// and display it, or end the call and hide the video
const isVideoBroadcasting = () => {
const ds = Screenshare.findOne({});
const screenshareEntry = Screenshare.findOne({ meetingId: Auth.meetingID },
{ fields: { 'screenshare.stream': 1 } });
if (!ds) {
if (!screenshareEntry) {
return false;
}
return !!ds.screenshare.stream;
return !!screenshareEntry.screenshare.stream;
};
// if remote screenshare has been ended disconnect and hide the video stream
@ -28,15 +30,21 @@ const presenterScreenshareHasEnded = () => {
KurentoBridge.kurentoExitVideo();
};
const viewScreenshare = () => {
const amIPresenter = UserListService.isUserPresenter(Auth.userID);
if (!amIPresenter) {
KurentoBridge.kurentoViewScreen();
} else {
KurentoBridge.kurentoViewLocalPreview();
}
};
// if remote screenshare has been started connect and display the video stream
const presenterScreenshareHasStarted = () => {
// KurentoBridge.kurentoWatchVideo: references a function in the global
// namespace inside kurento-extension.js that we load dynamically
// WebRTC restrictions may need a capture device permission to release
// useful ICE candidates on recvonly/no-gUM peers
tryGenerateIceCandidates().then(() => {
KurentoBridge.kurentoWatchVideo();
viewScreenshare();
}).catch((error) => {
logger.error({
logCode: 'screenshare_no_valid_candidate_gum_failure',
@ -46,7 +54,7 @@ const presenterScreenshareHasStarted = () => {
},
}, `Forced gUM to release additional ICE candidates failed due to ${error.name}.`);
// The fallback gUM failed. Try it anyways and hope for the best.
KurentoBridge.kurentoWatchVideo();
viewScreenshare();
});
};

View File

@ -524,12 +524,58 @@ const requestUserInformation = (userId) => {
makeCall('requestUserInformation', userId);
};
export const getUserNamesLink = () => {
const sortUsersByFirstName = (a, b) => {
const aName = a.firstName.toLowerCase();
const bName = b.firstName.toLowerCase();
if (aName < bName) return -1;
if (aName > bName) return 1;
return 0;
};
const sortUsersByLastName = (a, b) => {
if (a.lastName && !b.lastName) return -1;
if (!a.lastName && b.lastName) return 1;
const aName = a.lastName.toLowerCase();
const bName = b.lastName.toLowerCase();
if (aName < bName) return -1;
if (aName > bName) return 1;
return 0;
};
const isUserPresenter = (userId) => {
const user = Users.findOne({ userId },
{ fields: { presenter: 1 } });
return user ? user.presenter : false;
};
export const getUserNamesLink = (docTitle, fnSortedLabel, lnSortedLabel) => {
const mimeType = 'text/plain';
const userNamesObj = getUsers();
const userNameListString = userNamesObj
.map(u => u.name)
.join('\r\n');
const userNamesObj = getUsers()
.map((u) => {
const name = u.sortName.split(' ');
return ({
firstName: name[0],
middleNames: name.length > 2 ? name.slice(1, name.length - 1) : null,
lastName: name.length > 1 ? name[name.length - 1] : null,
});
});
const getUsernameString = (user) => {
const { firstName, middleNames, lastName } = user;
return `${firstName || ''} ${middleNames && middleNames.length > 0 ? middleNames.join(' ') : ''} ${lastName || ''}`;
};
const namesByFirstName = userNamesObj.sort(sortUsersByFirstName)
.map(u => getUsernameString(u)).join('\r\n');
const namesByLastName = userNamesObj.sort(sortUsersByLastName)
.map(u => getUsernameString(u)).join('\r\n');
const namesListsString = `${docTitle}\r\n\r\n${fnSortedLabel}\r\n${namesByFirstName}
\r\n\r\n${lnSortedLabel}\r\n${namesByLastName}`.replace(/ {2}/g, ' ');
const link = document.createElement('a');
const meeting = Meetings.findOne({ meetingId: Auth.meetingID },
{ fields: { 'meetingProp.name': 1 } });
@ -539,7 +585,7 @@ export const getUserNamesLink = () => {
link.setAttribute('download', `bbb-${meeting.meetingProp.name}[users-list]_${dateString}.txt`);
link.setAttribute(
'href',
`data: ${mimeType} ;charset=utf-16,${encodeURIComponent(userNameListString)}`,
`data: ${mimeType} ;charset=utf-16,${encodeURIComponent(namesListsString)}`,
);
return link;
};
@ -571,4 +617,5 @@ export default {
toggleUserLock,
requestUserInformation,
focusFirstDropDownItem,
isUserPresenter,
};

View File

@ -112,6 +112,18 @@ const intlMessages = defineMessages({
id: 'app.actionsBar.actionsDropdown.captionsDesc',
description: 'Captions menu toggle description',
},
savedNamesListTitle: {
id: 'app.userList.userOptions.savedNames.title',
description: '',
},
sortedFirstNameHeading: {
id: 'app.userList.userOptions.sortedFirstName.heading',
description: '',
},
sortedLastNameHeading: {
id: 'app.userList.userOptions.sortedLastName.heading',
description: '',
},
});
class UserOptions extends PureComponent {
@ -142,7 +154,21 @@ class UserOptions extends PureComponent {
}
onSaveUserNames() {
getUserNamesLink().dispatchEvent(new MouseEvent('click',
const { intl, meetingName } = this.props;
const date = new Date();
getUserNamesLink(
intl.formatMessage(intlMessages.savedNamesListTitle,
{
0: meetingName,
1: `${date.toLocaleDateString(
document.documentElement.lang,
)}:${date.toLocaleTimeString(
document.documentElement.lang,
)}`,
}),
intl.formatMessage(intlMessages.sortedFirstNameHeading),
intl.formatMessage(intlMessages.sortedLastNameHeading),
).dispatchEvent(new MouseEvent('click',
{ bubbles: true, cancelable: true, view: window }));
}

View File

@ -48,6 +48,13 @@ const UserOptionsContainer = withTracker((props) => {
return muteOnStart;
};
const getMeetingName = () => {
const { meetingProp } = Meetings.findOne({ meetingId: Auth.meetingID },
{ fields: { 'meetingProp.name': 1 } });
const { name } = meetingProp;
return name;
};
return {
toggleMuteAllUsers: () => {
UserListService.muteAllUsers(Auth.userID);
@ -78,6 +85,7 @@ const UserOptionsContainer = withTracker((props) => {
isBreakoutRecordable: ActionsBarService.isBreakoutRecordable(),
users: ActionsBarService.users(),
isMeteorConnected: Meteor.status().connected,
meetingName: getMeetingName(),
};
})(UserOptions);

View File

@ -1,16 +1,16 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReconnectingWebSocket from 'reconnecting-websocket';
import { defineMessages, injectIntl } from 'react-intl';
import _ from 'lodash';
import VideoService from './service';
import VideoListContainer from './video-list/container';
import { defineMessages, injectIntl } from 'react-intl';
import {
fetchWebRTCMappedStunTurnServers,
getMappedFallbackStun,
} from '/imports/utils/fetchStunTurnServers';
import { tryGenerateIceCandidates } from '/imports/utils/safari-webrtc';
import logger from '/imports/startup/client/logger';
import _ from 'lodash';
// Default values and default empty object to be backwards compat with 2.2.
// FIXME Remove hardcoded defaults 2.3.
@ -83,6 +83,7 @@ const propTypes = {
isUserLocked: PropTypes.bool.isRequired,
swapLayout: PropTypes.bool.isRequired,
currentVideoPageIndex: PropTypes.number.isRequired,
totalNumberOfStreams: PropTypes.number.isRequired,
};
class VideoProvider extends Component {
@ -122,7 +123,7 @@ class VideoProvider extends Component {
this.debouncedConnectStreams = _.debounce(
this.connectStreams,
VideoService.getPageChangeDebounceTime(),
{ leading: false, trailing: true, }
{ leading: false, trailing: true },
);
}
@ -229,15 +230,15 @@ class VideoProvider extends Component {
this.setState({ socketOpen: true });
}
updateThreshold (numberOfPublishers) {
updateThreshold(numberOfPublishers) {
const { threshold, profile } = VideoService.getThreshold(numberOfPublishers);
if (profile) {
const publishers = Object.values(this.webRtcPeers)
.filter(peer => peer.isPublisher)
.forEach(peer => {
.forEach((peer) => {
// 0 means no threshold in place. Reapply original one if needed
let profileToApply = (threshold === 0) ? peer.originalProfileId : profile;
VideoService.applyCameraProfile(peer, profileToApply)
const profileToApply = (threshold === 0) ? peer.originalProfileId : profile;
VideoService.applyCameraProfile(peer, profileToApply);
});
}
}
@ -271,7 +272,7 @@ class VideoProvider extends Component {
updateStreams(streams, shouldDebounce = false) {
const [streamsToConnect, streamsToDisconnect] = this.getStreamsToConnectAndDisconnect(streams);
if(shouldDebounce) {
if (shouldDebounce) {
this.debouncedConnectStreams(streamsToConnect);
} else {
this.connectStreams(streamsToConnect);
@ -679,7 +680,7 @@ class VideoProvider extends Component {
this.restartTimeout[cameraId] = setTimeout(
this._getWebRTCStartTimeout(cameraId, isLocal),
this.restartTimer[cameraId]
this.restartTimer[cameraId],
);
}
}
@ -879,13 +880,8 @@ class VideoProvider extends Component {
}
render() {
const { swapLayout, currentVideoPageIndex } = this.props;
const { socketOpen } = this.state;
if (!socketOpen) return null;
const { swapLayout, currentVideoPageIndex, streams } = this.props;
const {
streams,
} = this.props;
return (
<VideoListContainer
streams={streams}

View File

@ -34,17 +34,11 @@ const {
const TOKEN = '_';
class VideoService {
static isUserPresenter(userId) {
const user = Users.findOne({ userId },
{ fields: { presenter: 1 } });
return user ? user.presenter : false;
}
// Paginated streams: sort with following priority: local -> presenter -> alphabetic
static sortPaginatedStreams(s1, s2) {
if (VideoService.isUserPresenter(s1.userId) && !VideoService.isUserPresenter(s2.userId)) {
if (UserListService.isUserPresenter(s1.userId) && !UserListService.isUserPresenter(s2.userId)) {
return -1;
} else if (VideoService.isUserPresenter(s2.userId) && !VideoService.isUserPresenter(s1.userId)) {
} else if (UserListService.isUserPresenter(s2.userId) && !UserListService.isUserPresenter(s1.userId)) {
return 1;
} else {
return UserListService.sortUsersByName(s1, s2);
@ -53,8 +47,10 @@ class VideoService {
// Full mesh: sort with the following priority: local -> alphabetic
static sortMeshStreams(s1, s2) {
if (s1.userId === Auth.userID) {
if (s1.userId === Auth.userID && s2.userId !== Auth.userID) {
return -1;
} else if (s2.userId === Auth.userID && s1.userId !== Auth.userID) {
return 1;
} else {
return UserListService.sortUsersByName(s1, s2);
}
@ -546,10 +542,17 @@ class VideoService {
this.exitVideo();
}
isDisabled() {
disableReason() {
const { viewParticipantsWebcams } = Settings.dataSaving;
return this.isUserLocked() || this.isConnecting || !viewParticipantsWebcams;
const locks = {
videoLocked: this.isUserLocked(),
videoConnecting: this.isConnecting,
dataSaving: !viewParticipantsWebcams,
meteorDisconnected: !Meteor.status().connected
};
const locksKeys = Object.keys(locks);
const disableReason = locksKeys.filter( i => locks[i]).shift();
return disableReason ? disableReason : false;
}
getRole(isLocal) {
@ -739,7 +742,7 @@ export default {
getAuthenticatedURL: () => videoService.getAuthenticatedURL(),
isLocalStream: cameraId => videoService.isLocalStream(cameraId),
hasVideoStream: () => videoService.hasVideoStream(),
isDisabled: () => videoService.isDisabled(),
disableReason: () => videoService.disableReason(),
playStart: cameraId => videoService.playStart(cameraId),
getCameraProfile: () => videoService.getCameraProfile(),
addCandidateToPeer: (peer, candidate, cameraId) => videoService.addCandidateToPeer(peer, candidate, cameraId),

View File

@ -24,6 +24,18 @@ const intlMessages = defineMessages({
id: 'app.video.videoLocked',
description: 'video disabled label',
},
videoConnecting: {
id: 'app.video.connecting',
description: 'video connecting label',
},
dataSaving: {
id: 'app.video.dataSaving',
description: 'video data saving label',
},
meteorDisconnected: {
id: 'app.video.clientDisconnected',
description: 'Meteor disconnected label',
},
iOSWarning: {
id: 'app.iOSWarning.label',
description: 'message indicating to upgrade ios version',
@ -33,14 +45,13 @@ const intlMessages = defineMessages({
const propTypes = {
intl: PropTypes.object.isRequired,
hasVideoStream: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired,
mountVideoPreview: PropTypes.func.isRequired,
};
const JoinVideoButton = ({
intl,
hasVideoStream,
isDisabled,
disableReason,
mountVideoPreview,
}) => {
const exitVideo = () => hasVideoStream && !VideoService.isMultipleCamerasEnabled();
@ -57,14 +68,14 @@ const JoinVideoButton = ({
}
};
const label = exitVideo() ?
intl.formatMessage(intlMessages.leaveVideo) :
intl.formatMessage(intlMessages.joinVideo);
const label = exitVideo()
? intl.formatMessage(intlMessages.leaveVideo)
: intl.formatMessage(intlMessages.joinVideo);
return (
<Button
data-test="joinVideo"
label={isDisabled ? intl.formatMessage(intlMessages.videoLocked) : label}
label={disableReason ? intl.formatMessage(intlMessages[disableReason]) : label}
className={cx(styles.button, hasVideoStream || styles.btn)}
onClick={handleOnClick}
hideLabel
@ -74,7 +85,7 @@ const JoinVideoButton = ({
ghost={!hasVideoStream}
size="lg"
circle
disabled={isDisabled}
disabled={!!disableReason}
/>
);
};

View File

@ -9,7 +9,7 @@ import VideoService from '../service';
const JoinVideoOptionsContainer = (props) => {
const {
hasVideoStream,
isDisabled,
disableReason,
intl,
mountModal,
...restProps
@ -19,7 +19,7 @@ const JoinVideoOptionsContainer = (props) => {
return (
<JoinVideoButton {...{
mountVideoPreview, hasVideoStream, isDisabled, ...restProps,
mountVideoPreview, hasVideoStream, disableReason, ...restProps,
}}
/>
);
@ -27,5 +27,5 @@ const JoinVideoOptionsContainer = (props) => {
export default withModalMounter(injectIntl(withTracker(() => ({
hasVideoStream: VideoService.hasVideoStream(),
isDisabled: VideoService.isDisabled() || !Meteor.status().connected,
disableReason: VideoService.disableReason(),
}))(JoinVideoOptionsContainer)));

View File

@ -280,6 +280,31 @@ class AudioManager {
return this.bridge.transferCall(this.onAudioJoin.bind(this));
}
onVoiceUserChanges(fields) {
if (fields.muted !== undefined && fields.muted !== this.isMuted) {
let muteState;
this.isMuted = fields.muted;
if (this.isMuted) {
muteState = 'selfMuted';
this.mute();
} else {
muteState = 'selfUnmuted';
this.unmute();
}
window.parent.postMessage({ response: muteState }, '*');
}
if (fields.talking !== undefined && fields.talking !== this.isTalking) {
this.isTalking = fields.talking;
}
if (this.isMuted) {
this.isTalking = false;
}
}
onAudioJoin() {
this.isConnecting = false;
this.isConnected = true;
@ -288,21 +313,8 @@ class AudioManager {
if (!this.muteHandle) {
const query = VoiceUsers.find({ intId: Auth.userID }, { fields: { muted: 1, talking: 1 } });
this.muteHandle = query.observeChanges({
changed: (id, fields) => {
if (fields.muted !== undefined && fields.muted !== this.isMuted) {
this.isMuted = fields.muted;
const muteState = this.isMuted ? 'selfMuted' : 'selfUnmuted';
window.parent.postMessage({ response: muteState }, '*');
}
if (fields.talking !== undefined && fields.talking !== this.isTalking) {
this.isTalking = fields.talking;
}
if (this.isMuted) {
this.isTalking = false;
}
},
added: (id, fields) => this.onVoiceUserChanges(fields),
changed: (id, fields) => this.onVoiceUserChanges(fields),
});
}
@ -562,6 +574,29 @@ class AudioManager {
this.autoplayBlocked = true;
}
}
setSenderTrackEnabled(shouldEnable) {
// If the bridge is set to listen only mode, nothing to do here. This method
// is solely for muting outbound tracks.
if (this.isListenOnly) return;
// Bridge -> SIP.js bridge, the only full audio capable one right now
const peer = this.bridge.getPeerConnection();
peer.getSenders().forEach((sender) => {
const { track } = sender;
if (track && track.kind === 'audio') {
track.enabled = shouldEnable;
}
});
}
mute() {
this.setSenderTrackEnabled(false);
}
unmute() {
this.setSenderTrackEnabled(true);
}
}
const audioManager = new AudioManager();

View File

@ -35,6 +35,10 @@ public:
duration: 4000
remainingTimeThreshold: 30
remainingTimeAlertThreshold: 1
# Warning: increasing the limit of breakout rooms per meeting
# can generate excessive overhead to the server. We recommend
# this value to be kept under 12.
breakoutRoomLimit: 8
defaultSettings:
application:
animations: true

View File

@ -113,6 +113,9 @@
"app.userList.userOptions.enableNote": "Shared notes are now enabled",
"app.userList.userOptions.showUserList": "User list is now shown to viewers",
"app.userList.userOptions.enableOnlyModeratorWebcam": "You can enable your webcam now, everyone will see you",
"app.userList.userOptions.savedNames.title": "List of users in meeting {0} at {1}",
"app.userList.userOptions.sortedFirstName.heading": "Sorted by first name:",
"app.userList.userOptions.sortedLastName.heading": "Sorted by last name:",
"app.media.label": "Media",
"app.media.autoplayAlertDesc": "Allow Access",
"app.media.screenshare.start": "Screenshare has started",
@ -587,6 +590,8 @@
"app.videoPreview.webcamNotFoundLabel": "Webcam not found",
"app.videoPreview.profileNotFoundLabel": "No supported camera profile",
"app.video.joinVideo": "Share webcam",
"app.video.connecting": "Webcam sharing is starting ...",
"app.video.dataSaving": "Webcam sharing is disabled in Data Saving",
"app.video.leaveVideo": "Stop sharing webcam",
"app.video.iceCandidateError": "Error on adding ICE candidate",
"app.video.iceConnectionStateError": "Connection failure (ICE error 1107)",
@ -612,6 +617,7 @@
"app.video.chromeExtensionErrorLink": "this Chrome extension",
"app.video.pagination.prevPage": "See previous videos",
"app.video.pagination.nextPage": "See next videos",
"app.video.clientDisconnected": "Webcam cannot be shared due to connection issues",
"app.fullscreenButton.label": "Make {0} fullscreen",
"app.deskshare.iceConnectionStateError": "Connection failed when sharing screen (ICE error 1108)",
"app.sfu.mediaServerConnectionError2000": "Unable to connect to media server (error 2000)",

View File

@ -241,10 +241,10 @@ defaultClientUrl=${bigbluebutton.web.serverURL}/client/BigBlueButton.html
allowRequestsWithoutSession=false
# Force all attendees to join the meeting using the HTML5 client
attendeesJoinViaHTML5Client=false
attendeesJoinViaHTML5Client=true
# Force all moderators to join the meeting using the HTML5 client
moderatorsJoinViaHTML5Client=false
moderatorsJoinViaHTML5Client=true
# The url of the BigBlueButton HTML5 client. Users will be redirected here when
# successfully joining the meeting.
@ -354,3 +354,8 @@ lockSettingsLockOnJoinConfigurable=false
allowDuplicateExtUserid=true
defaultTextTrackUrl=${bigbluebutton.web.serverURL}/bigbluebutton
# Param to end the meeting when there are no moderators after a certain period of time.
# Needed for classes where teacher gets disconnected and can't get back in. Prevents
# students from running amok.
endWhenNoModerator=false

View File

@ -158,6 +158,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<property name="lockSettingsLockOnJoin" value="${lockSettingsLockOnJoin}"/>
<property name="lockSettingsLockOnJoinConfigurable" value="${lockSettingsLockOnJoinConfigurable}"/>
<property name="allowDuplicateExtUserid" value="${allowDuplicateExtUserid}"/>
<property name="endWhenNoModerator" value="${endWhenNoModerator}"/>
</bean>
<import resource="doc-conversion.xml"/>