Merge branch 'develop' of https://github.com/bigbluebutton/bigbluebutton into 2.3-updated-polling
This commit is contained in:
commit
73f890c9d8
@ -37,6 +37,7 @@ trait SystemConfiguration {
|
||||
lazy val fromAkkaAppsJsonChannel = Try(config.getString("eventBus.fromAkkaAppsChannel")).getOrElse("from-akka-apps-json-channel")
|
||||
|
||||
lazy val applyPermissionCheck = Try(config.getBoolean("apps.checkPermissions")).getOrElse(false)
|
||||
lazy val ejectOnViolation = Try(config.getBoolean("apps.ejectOnViolation")).getOrElse(false)
|
||||
|
||||
lazy val voiceConfRecordPath = Try(config.getString("voiceConf.recordPath")).getOrElse("/var/freeswitch/meetings")
|
||||
lazy val voiceConfRecordCodec = Try(config.getString("voiceConf.recordCodec")).getOrElse("wav")
|
||||
|
@ -37,7 +37,7 @@ trait RightsManagementTrait extends SystemConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
object PermissionCheck {
|
||||
object PermissionCheck extends SystemConfiguration {
|
||||
|
||||
val MOD_LEVEL = 100
|
||||
val AUTHED_LEVEL = 50
|
||||
@ -83,12 +83,17 @@ object PermissionCheck {
|
||||
|
||||
def ejectUserForFailedPermission(meetingId: String, userId: String, reason: String,
|
||||
outGW: OutMsgRouter, liveMeeting: LiveMeeting): Unit = {
|
||||
val ejectedBy = SystemUser.ID
|
||||
if (ejectOnViolation) {
|
||||
val ejectedBy = SystemUser.ID
|
||||
|
||||
UsersApp.ejectUserFromMeeting(outGW, liveMeeting, userId, ejectedBy, reason, EjectReasonCode.PERMISSION_FAILED, ban = false)
|
||||
UsersApp.ejectUserFromMeeting(outGW, liveMeeting, userId, ejectedBy, reason, EjectReasonCode.PERMISSION_FAILED, ban = false)
|
||||
|
||||
// send a system message to force disconnection
|
||||
Sender.sendDisconnectClientSysMsg(meetingId, userId, ejectedBy, reason, outGW)
|
||||
// send a system message to force disconnection
|
||||
Sender.sendDisconnectClientSysMsg(meetingId, userId, ejectedBy, reason, outGW)
|
||||
} else {
|
||||
// TODO: get this object a context so it can use the akka logging system
|
||||
println(s"Skipping violation ejection of ${userId} trying to ${reason} in ${meetingId}")
|
||||
}
|
||||
}
|
||||
|
||||
def addOldPresenter(users: Users2x, userId: String): OldPresenter = {
|
||||
|
@ -56,7 +56,7 @@ trait RegisterUserReqMsgHdlr {
|
||||
val g = GuestApprovedVO(regUser.id, GuestStatus.ALLOW)
|
||||
UsersApp.approveOrRejectGuest(liveMeeting, outGW, g, SystemUser.ID)
|
||||
case GuestStatus.WAIT =>
|
||||
val guest = GuestWaiting(regUser.id, regUser.name, regUser.role, regUser.guest, regUser.authed)
|
||||
val guest = GuestWaiting(regUser.id, regUser.name, regUser.role, regUser.guest, regUser.avatarURL, regUser.authed)
|
||||
addGuestToWaitingForApproval(guest, liveMeeting.guestsWaiting)
|
||||
notifyModeratorsOfGuestWaiting(Vector(guest), liveMeeting.users2x, liveMeeting.props.meetingProp.intId)
|
||||
case GuestStatus.DENY =>
|
||||
|
@ -51,7 +51,7 @@ class GuestsWaiting {
|
||||
def setGuestPolicy(policy: GuestPolicy) = guestPolicy = policy
|
||||
}
|
||||
|
||||
case class GuestWaiting(intId: String, name: String, role: String, guest: Boolean, authenticated: Boolean)
|
||||
case class GuestWaiting(intId: String, name: String, role: String, guest: Boolean, avatar: String, authenticated: Boolean)
|
||||
case class GuestPolicy(policy: String, setBy: String)
|
||||
|
||||
object GuestPolicyType {
|
||||
|
@ -27,7 +27,7 @@ import java.util.*;
|
||||
* Source:<br>
|
||||
* Phoenix: An Interactive Curve Design System Based on the Automatic Fitting
|
||||
* of Hand-Sketched Curves.<br>
|
||||
* © Copyright by Philip J. Schneider 1988.<br>
|
||||
* Copyright (c) by Philip J. Schneider 1988.<br>
|
||||
* A thesis submitted in partial fulfillment of the requirements for the degree
|
||||
* of Master of Science, University of Washington.
|
||||
* <p>
|
||||
@ -238,7 +238,7 @@ public class Bezier {
|
||||
* @param digitizedPoints Digitized points
|
||||
* @param maxAngle maximal angle in radians between the current point and its
|
||||
* predecessor and successor up to which the point does not break the
|
||||
* digitized list into segments. Recommended value 44° = 44 * 180d / Math.PI
|
||||
* digitized list into segments. Recommended value 44 deg = 44 * 180d / Math.PI
|
||||
* @return Segments of digitized points, each segment having less than maximal
|
||||
* angle between points.
|
||||
*/
|
||||
|
@ -44,7 +44,7 @@ object MsgBuilder {
|
||||
val envelope = BbbCoreEnvelope(GetGuestsWaitingApprovalRespMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(GetGuestsWaitingApprovalRespMsg.NAME, meetingId, userId)
|
||||
|
||||
val guestsWaiting = guests.map(g => GuestWaitingVO(g.intId, g.name, g.role, g.guest, g.authenticated))
|
||||
val guestsWaiting = guests.map(g => GuestWaitingVO(g.intId, g.name, g.role, g.guest, g.avatar, g.authenticated))
|
||||
val body = GetGuestsWaitingApprovalRespMsgBody(guestsWaiting)
|
||||
val event = GetGuestsWaitingApprovalRespMsg(header, body)
|
||||
|
||||
@ -56,7 +56,7 @@ object MsgBuilder {
|
||||
val envelope = BbbCoreEnvelope(GuestsWaitingForApprovalEvtMsg.NAME, routing)
|
||||
val header = BbbClientMsgHeader(GuestsWaitingForApprovalEvtMsg.NAME, meetingId, userId)
|
||||
|
||||
val guestsWaiting = guests.map(g => GuestWaitingVO(g.intId, g.name, g.role, g.guest, g.authenticated))
|
||||
val guestsWaiting = guests.map(g => GuestWaitingVO(g.intId, g.name, g.role, g.guest, g.avatar, g.authenticated))
|
||||
val body = GuestsWaitingForApprovalEvtMsgBody(guestsWaiting)
|
||||
val event = GuestsWaitingForApprovalEvtMsg(header, body)
|
||||
|
||||
|
@ -20,13 +20,13 @@ trait FakeTestData {
|
||||
val guest1 = createUserVoiceAndCam(liveMeeting, Roles.VIEWER_ROLE, guest = true, authed = true, CallingWith.WEBRTC, muted = false,
|
||||
talking = false, listenOnly = false)
|
||||
Users2x.add(liveMeeting.users2x, guest1)
|
||||
val guestWait1 = GuestWaiting(guest1.intId, guest1.name, guest1.role, guest1.guest, guest1.authed)
|
||||
val guestWait1 = GuestWaiting(guest1.intId, guest1.name, guest1.role, guest1.guest, "", guest1.authed)
|
||||
GuestsWaiting.add(liveMeeting.guestsWaiting, guestWait1)
|
||||
|
||||
val guest2 = createUserVoiceAndCam(liveMeeting, Roles.VIEWER_ROLE, guest = true, authed = true, CallingWith.FLASH, muted = false,
|
||||
talking = false, listenOnly = false)
|
||||
Users2x.add(liveMeeting.users2x, guest2)
|
||||
val guestWait2 = GuestWaiting(guest2.intId, guest2.name, guest2.role, guest2.guest, guest2.authed)
|
||||
val guestWait2 = GuestWaiting(guest2.intId, guest2.name, guest2.role, guest2.guest, "", guest2.authed)
|
||||
GuestsWaiting.add(liveMeeting.guestsWaiting, guestWait2)
|
||||
|
||||
val vu1 = FakeUserGenerator.createFakeVoiceOnlyUser(CallingWith.PHONE, muted = false, talking = false, listenOnly = false)
|
||||
|
@ -71,6 +71,7 @@ services {
|
||||
|
||||
apps {
|
||||
checkPermissions = true
|
||||
ejectOnViolation = false
|
||||
endMeetingWhenNoMoreAuthedUsers = false
|
||||
endMeetingWhenNoMoreAuthedUsersAfterMinutes = 2
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ case class GetGuestsWaitingApprovalRespMsg(
|
||||
body: GetGuestsWaitingApprovalRespMsgBody
|
||||
) extends BbbCoreMsg
|
||||
case class GetGuestsWaitingApprovalRespMsgBody(guests: Vector[GuestWaitingVO])
|
||||
case class GuestWaitingVO(intId: String, name: String, role: String, guest: Boolean, authenticated: Boolean)
|
||||
case class GuestWaitingVO(intId: String, name: String, role: String, guest: Boolean, avatar: String, authenticated: Boolean)
|
||||
|
||||
/**
|
||||
* Message sent to client for list of guest waiting for approval. This is sent when
|
||||
|
@ -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.");
|
||||
}
|
||||
|
@ -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;
|
||||
@ -81,8 +82,9 @@ import org.bigbluebutton.api2.IBbbWebApiGWApp;
|
||||
import org.bigbluebutton.api2.domain.UploadedTrack;
|
||||
import org.bigbluebutton.common2.redis.RedisStorageService;
|
||||
import org.bigbluebutton.presentation.PresentationUrlDownloadService;
|
||||
import org.bigbluebutton.web.services.RegisteredUserCleanupTimerTask;
|
||||
import org.bigbluebutton.web.services.WaitingGuestCleanupTimerTask;
|
||||
import org.bigbluebutton.web.services.UserCleanupTimerTask;
|
||||
import org.bigbluebutton.web.services.EnteredUserCleanupTimerTask;
|
||||
import org.bigbluebutton.web.services.callback.CallbackUrlService;
|
||||
import org.bigbluebutton.web.services.callback.MeetingEndedEvent;
|
||||
import org.bigbluebutton.web.services.turn.StunTurnService;
|
||||
@ -107,13 +109,17 @@ public class MeetingService implements MessageListener {
|
||||
private final ConcurrentMap<String, UserSession> sessions;
|
||||
|
||||
private RecordingService recordingService;
|
||||
private RegisteredUserCleanupTimerTask registeredUserCleaner;
|
||||
private WaitingGuestCleanupTimerTask waitingGuestCleaner;
|
||||
private UserCleanupTimerTask userCleaner;
|
||||
private EnteredUserCleanupTimerTask enteredUserCleaner;
|
||||
private StunTurnService stunTurnService;
|
||||
private RedisStorageService storeService;
|
||||
private CallbackUrlService callbackUrlService;
|
||||
private boolean keepEvents;
|
||||
|
||||
private long usersTimeout;
|
||||
private long enteredUsersTimeout;
|
||||
|
||||
private ParamsProcessorUtil paramsProcessorUtil;
|
||||
private PresentationUrlDownloadService presDownloadService;
|
||||
|
||||
@ -180,22 +186,63 @@ public class MeetingService implements MessageListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove registered users who did not successfully joined the meeting.
|
||||
* Remove users who did not successfully reconnected to the meeting.
|
||||
*/
|
||||
public void purgeRegisteredUsers() {
|
||||
public void purgeUsers() {
|
||||
for (AbstractMap.Entry<String, Meeting> entry : this.meetings.entrySet()) {
|
||||
Long now = System.currentTimeMillis();
|
||||
Meeting meeting = entry.getValue();
|
||||
|
||||
ConcurrentMap<String, User> users = meeting.getUsersMap();
|
||||
for (AbstractMap.Entry<String, User> userEntry : meeting.getUsersMap().entrySet()) {
|
||||
String userId = userEntry.getKey();
|
||||
User user = userEntry.getValue();
|
||||
|
||||
for (AbstractMap.Entry<String, RegisteredUser> registeredUser : meeting.getRegisteredUsers().entrySet()) {
|
||||
String registeredUserID = registeredUser.getKey();
|
||||
RegisteredUser registeredUserDate = registeredUser.getValue();
|
||||
if (!user.hasLeft()) continue;
|
||||
|
||||
long elapsedTime = now - registeredUserDate.registeredOn;
|
||||
if (elapsedTime >= 60000 && !users.containsKey(registeredUserID)) {
|
||||
meeting.userUnregistered(registeredUserID);
|
||||
long elapsedTime = now - user.getLeftOn();
|
||||
if (elapsedTime >= usersTimeout) {
|
||||
meeting.removeUser(userId);
|
||||
|
||||
Map<String, Object> logData = new HashMap<>();
|
||||
logData.put("meetingId", meeting.getInternalId());
|
||||
logData.put("userId", userId);
|
||||
logData.put("logCode", "removed_user");
|
||||
logData.put("description", "User left and was removed from the meeting.");
|
||||
|
||||
Gson gson = new Gson();
|
||||
String logStr = gson.toJson(logData);
|
||||
|
||||
log.info(" --analytics-- data={}", logStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove entered users who did not join.
|
||||
*/
|
||||
public void purgeEnteredUsers() {
|
||||
for (AbstractMap.Entry<String, Meeting> entry : this.meetings.entrySet()) {
|
||||
Long now = System.currentTimeMillis();
|
||||
Meeting meeting = entry.getValue();
|
||||
|
||||
for (AbstractMap.Entry<String, Long> enteredUser : meeting.getEnteredUsers().entrySet()) {
|
||||
String userId = enteredUser.getKey();
|
||||
|
||||
long elapsedTime = now - enteredUser.getValue();
|
||||
if (elapsedTime >= enteredUsersTimeout) {
|
||||
meeting.removeEnteredUser(userId);
|
||||
|
||||
Map<String, Object> logData = new HashMap<>();
|
||||
logData.put("meetingId", meeting.getInternalId());
|
||||
logData.put("userId", userId);
|
||||
logData.put("logCode", "purged_entered_user");
|
||||
logData.put("description", "Purged user that called ENTER from the API but never joined");
|
||||
|
||||
Gson gson = new Gson();
|
||||
String logStr = gson.toJson(logData);
|
||||
|
||||
log.info(" --analytics-- data={}", logStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -778,27 +825,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) {
|
||||
@ -867,13 +925,6 @@ public class MeetingService implements MessageListener {
|
||||
// the meeting ended.
|
||||
m.setEndTime(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
RegisteredUser userRegistered = m.userUnregistered(message.userId);
|
||||
if (userRegistered != null) {
|
||||
log.info("User unregistered from meeting");
|
||||
} else {
|
||||
log.info("User was not unregistered from meeting because it was not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -911,7 +962,7 @@ public class MeetingService implements MessageListener {
|
||||
} else {
|
||||
if (message.userId.startsWith("v_")) {
|
||||
// A dial-in user joined the meeting. Dial-in users by convention has userId that starts with "v_".
|
||||
User vuser = new User(message.userId, message.userId, message.name, "DIAL-IN-USER", "no-avatar-url",
|
||||
User vuser = new User(message.userId, message.userId, message.name, "DIAL-IN-USER", "",
|
||||
true, GuestPolicy.ALLOW, "DIAL-IN");
|
||||
vuser.setVoiceJoined(true);
|
||||
m.userJoined(vuser);
|
||||
@ -1086,8 +1137,9 @@ public class MeetingService implements MessageListener {
|
||||
|
||||
public void stop() {
|
||||
processMessage = false;
|
||||
registeredUserCleaner.stop();
|
||||
waitingGuestCleaner.stop();
|
||||
userCleaner.stop();
|
||||
enteredUserCleaner.stop();
|
||||
}
|
||||
|
||||
public void setRecordingService(RecordingService s) {
|
||||
@ -1112,11 +1164,16 @@ public class MeetingService implements MessageListener {
|
||||
waitingGuestCleaner.start();
|
||||
}
|
||||
|
||||
public void setRegisteredUserCleanupTimerTask(
|
||||
RegisteredUserCleanupTimerTask c) {
|
||||
registeredUserCleaner = c;
|
||||
registeredUserCleaner.setMeetingService(this);
|
||||
registeredUserCleaner.start();
|
||||
public void setEnteredUserCleanupTimerTask(EnteredUserCleanupTimerTask c) {
|
||||
enteredUserCleaner = c;
|
||||
enteredUserCleaner.setMeetingService(this);
|
||||
enteredUserCleaner.start();
|
||||
}
|
||||
|
||||
public void setUserCleanupTimerTask(UserCleanupTimerTask c) {
|
||||
userCleaner = c;
|
||||
userCleaner.setMeetingService(this);
|
||||
userCleaner.start();
|
||||
}
|
||||
|
||||
public void setStunTurnService(StunTurnService s) {
|
||||
@ -1126,4 +1183,12 @@ public class MeetingService implements MessageListener {
|
||||
public void setKeepEvents(boolean value) {
|
||||
keepEvents = value;
|
||||
}
|
||||
|
||||
public void setUsersTimeout(long value) {
|
||||
usersTimeout = value;
|
||||
}
|
||||
|
||||
public void setEnteredUsersTimeout(long value) {
|
||||
enteredUsersTimeout = value;
|
||||
}
|
||||
}
|
||||
|
@ -78,6 +78,7 @@ public class ParamsProcessorUtil {
|
||||
private Boolean moderatorsJoinViaHTML5Client;
|
||||
private Boolean attendeesJoinViaHTML5Client;
|
||||
private Boolean allowRequestsWithoutSession;
|
||||
private Boolean useDefaultAvatar = false;
|
||||
private String defaultAvatarURL;
|
||||
private String defaultConfigURL;
|
||||
private String defaultGuestPolicy;
|
||||
@ -114,6 +115,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 +422,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);
|
||||
@ -454,6 +465,8 @@ public class ParamsProcessorUtil {
|
||||
externalMeetingId = externalHash + "-" + timeStamp;
|
||||
}
|
||||
|
||||
String avatarURL = useDefaultAvatar ? defaultAvatarURL : "";
|
||||
|
||||
// Create the meeting with all passed in parameters.
|
||||
Meeting meeting = new Meeting.Builder(externalMeetingId,
|
||||
internalMeetingId, createTime).withName(meetingName)
|
||||
@ -464,7 +477,7 @@ public class ParamsProcessorUtil {
|
||||
.withBannerText(bannerText).withBannerColor(bannerColor)
|
||||
.withTelVoice(telVoice).withWebVoice(webVoice)
|
||||
.withDialNumber(dialNumber)
|
||||
.withDefaultAvatarURL(defaultAvatarURL)
|
||||
.withDefaultAvatarURL(avatarURL)
|
||||
.withAutoStartRecording(autoStartRec)
|
||||
.withAllowStartStopRecording(allowStartStoptRec)
|
||||
.withWebcamsOnlyForModerator(webcamsOnlyForMod)
|
||||
@ -487,6 +500,11 @@ 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.setMeetingExpireIfNoUserJoinedInMinutes(meetingExpireIfNoUserJoinedInMinutes);
|
||||
meeting.setMeetingExpireWhenLastUserLeftInMinutes(meetingExpireWhenLastUserLeftInMinutes);
|
||||
meeting.setUserInactivityInspectTimerInMinutes(userInactivityInspectTimerInMinutes);
|
||||
@ -937,6 +955,10 @@ public class ParamsProcessorUtil {
|
||||
this.webcamsOnlyForModerator = webcamsOnlyForModerator;
|
||||
}
|
||||
|
||||
public void setUseDefaultAvatar(Boolean value) {
|
||||
this.useDefaultAvatar = value;
|
||||
}
|
||||
|
||||
public void setdefaultAvatarURL(String url) {
|
||||
this.defaultAvatarURL = url;
|
||||
}
|
||||
@ -1115,4 +1137,10 @@ public class ParamsProcessorUtil {
|
||||
public void setAllowDuplicateExtUserid(Boolean allow) {
|
||||
this.defaultAllowDuplicateExtUserid = allow;
|
||||
}
|
||||
|
||||
public void setEndWhenNoModerator(Boolean val) {
|
||||
this.defaultEndWhenNoModerator = val;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -73,6 +73,7 @@ public class Meeting {
|
||||
private Map<String, Object> userCustomData;
|
||||
private final ConcurrentMap<String, User> users;
|
||||
private final ConcurrentMap<String, RegisteredUser> registeredUsers;
|
||||
private final ConcurrentMap<String, Long> enteredUsers;
|
||||
private final ConcurrentMap<String, Config> configs;
|
||||
private final Boolean isBreakout;
|
||||
private final List<String> breakoutRooms = new ArrayList<>();
|
||||
@ -92,6 +93,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,12 +126,14 @@ public class Meeting {
|
||||
guestPolicy = builder.guestPolicy;
|
||||
breakoutRoomsParams = builder.breakoutRoomsParams;
|
||||
lockSettingsParams = builder.lockSettingsParams;
|
||||
allowDuplicateExtUserid = builder.allowDuplicateExtUserid;
|
||||
allowDuplicateExtUserid = builder.allowDuplicateExtUserid;
|
||||
endWhenNoModerator = builder.endWhenNoModerator;
|
||||
|
||||
userCustomData = new HashMap<>();
|
||||
|
||||
users = new ConcurrentHashMap<>();
|
||||
registeredUsers = new ConcurrentHashMap<>();
|
||||
enteredUsers = new ConcurrentHashMap<>();;
|
||||
|
||||
configs = new ConcurrentHashMap<>();
|
||||
}
|
||||
@ -448,12 +456,28 @@ public class Meeting {
|
||||
}
|
||||
|
||||
public void userJoined(User user) {
|
||||
userHasJoined = true;
|
||||
this.users.put(user.getInternalUserId(), user);
|
||||
User u = getUserById(user.getInternalUserId());
|
||||
if (u != null) {
|
||||
u.joined();
|
||||
} else {
|
||||
if (!userHasJoined) userHasJoined = true;
|
||||
this.users.put(user.getInternalUserId(), user);
|
||||
// Clean this user up from the entered user's list
|
||||
removeEnteredUser(user.getInternalUserId());
|
||||
}
|
||||
}
|
||||
|
||||
public User userLeft(String userid){
|
||||
return users.remove(userid);
|
||||
public User userLeft(String userId) {
|
||||
User user = getUserById(userId);
|
||||
if (user != null) {
|
||||
user.left();
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public User removeUser(String userId) {
|
||||
return this.users.remove(userId);
|
||||
}
|
||||
|
||||
public User getUserById(String id){
|
||||
@ -563,6 +587,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);
|
||||
}
|
||||
@ -579,6 +611,29 @@ public class Meeting {
|
||||
return registeredUsers;
|
||||
}
|
||||
|
||||
public ConcurrentMap<String, Long> getEnteredUsers() {
|
||||
return this.enteredUsers;
|
||||
}
|
||||
|
||||
public void userEntered(String userId) {
|
||||
// Skip if user already joined
|
||||
User u = getUserById(userId);
|
||||
if (u != null) return;
|
||||
|
||||
if (!enteredUsers.containsKey(userId)) {
|
||||
Long time = System.currentTimeMillis();
|
||||
this.enteredUsers.put(userId, time);
|
||||
}
|
||||
}
|
||||
|
||||
public Long removeEnteredUser(String userId) {
|
||||
return this.enteredUsers.remove(userId);
|
||||
}
|
||||
|
||||
public Long getEnteredUserById(String userId) {
|
||||
return this.enteredUsers.get(userId);
|
||||
}
|
||||
|
||||
/***
|
||||
* Meeting Builder
|
||||
*
|
||||
@ -612,6 +667,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 +799,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);
|
||||
|
@ -38,6 +38,7 @@ public class User {
|
||||
private Boolean voiceJoined = false;
|
||||
private String clientType;
|
||||
private List<String> streams;
|
||||
private Long leftOn = null;
|
||||
|
||||
public User(String internalUserId,
|
||||
String externalUserId,
|
||||
@ -90,6 +91,22 @@ public class User {
|
||||
return this.guestStatus;
|
||||
}
|
||||
|
||||
public Boolean hasLeft() {
|
||||
return leftOn != null;
|
||||
}
|
||||
|
||||
public void joined() {
|
||||
this.leftOn = null;
|
||||
}
|
||||
|
||||
public void left() {
|
||||
this.leftOn = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public Long getLeftOn() {
|
||||
return this.leftOn;
|
||||
}
|
||||
|
||||
public String getFullname() {
|
||||
return fullname;
|
||||
}
|
||||
|
@ -1,231 +1,224 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.presentation.imp;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bigbluebutton.presentation.ConversionMessageConstants;
|
||||
import org.bigbluebutton.presentation.SupportedFileTypes;
|
||||
import org.bigbluebutton.presentation.UploadedPresentation;
|
||||
import org.jodconverter.core.office.OfficeException;
|
||||
import org.jodconverter.core.office.OfficeUtils;
|
||||
import org.jodconverter.local.LocalConverter;
|
||||
import org.jodconverter.local.office.ExternalOfficeManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
public class OfficeToPdfConversionService {
|
||||
private static Logger log = LoggerFactory.getLogger(OfficeToPdfConversionService.class);
|
||||
|
||||
private OfficeDocumentValidator2 officeDocumentValidator;
|
||||
private final ArrayList<ExternalOfficeManager> officeManagers;
|
||||
private ExternalOfficeManager currentManager = null;
|
||||
private boolean skipOfficePrecheck = false;
|
||||
private int sofficeBasePort = 0;
|
||||
private int sofficeManagers = 0;
|
||||
private String sofficeWorkingDirBase = null;
|
||||
|
||||
public OfficeToPdfConversionService() throws OfficeException {
|
||||
officeManagers = new ArrayList<>();
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert the Office document to PDF. If successful, update
|
||||
* UploadPresentation.uploadedFile with the new PDF out and
|
||||
* UploadPresentation.lastStepSuccessful to TRUE.
|
||||
*/
|
||||
public UploadedPresentation convertOfficeToPdf(UploadedPresentation pres) {
|
||||
initialize(pres);
|
||||
if (SupportedFileTypes.isOfficeFile(pres.getFileType())) {
|
||||
// Check if we need to precheck office document
|
||||
if (!skipOfficePrecheck && officeDocumentValidator.isValid(pres)) {
|
||||
Map<String, Object> logData = new HashMap<>();
|
||||
logData.put("meetingId", pres.getMeetingId());
|
||||
logData.put("presId", pres.getId());
|
||||
logData.put("filename", pres.getName());
|
||||
logData.put("logCode", "problems_office_to_pdf_validation");
|
||||
logData.put("message", "Problems detected prior to converting the file to PDF.");
|
||||
Gson gson = new Gson();
|
||||
String logStr = gson.toJson(logData);
|
||||
log.warn(" --analytics-- data={}", logStr);
|
||||
|
||||
pres.setConversionStatus(ConversionMessageConstants.OFFICE_DOC_CONVERSION_INVALID_KEY);
|
||||
return pres;
|
||||
}
|
||||
File pdfOutput = setupOutputPdfFile(pres);
|
||||
if (convertOfficeDocToPdf(pres, pdfOutput)) {
|
||||
Map<String, Object> logData = new HashMap<>();
|
||||
logData.put("meetingId", pres.getMeetingId());
|
||||
logData.put("presId", pres.getId());
|
||||
logData.put("filename", pres.getName());
|
||||
logData.put("logCode", "office_to_pdf_success");
|
||||
logData.put("message", "Successfully converted office file to pdf.");
|
||||
Gson gson = new Gson();
|
||||
String logStr = gson.toJson(logData);
|
||||
log.info(" --analytics-- data={}", logStr);
|
||||
|
||||
makePdfTheUploadedFileAndSetStepAsSuccess(pres, pdfOutput);
|
||||
} else {
|
||||
Map<String, Object> logData = new HashMap<>();
|
||||
logData.put("meetingId", pres.getMeetingId());
|
||||
logData.put("presId", pres.getId());
|
||||
logData.put("filename", pres.getName());
|
||||
logData.put("logCode", "office_to_pdf_failed");
|
||||
logData.put("message", "Failed to convert " + pres.getUploadedFile().getAbsolutePath() + " to Pdf.");
|
||||
Gson gson = new Gson();
|
||||
String logStr = gson.toJson(logData);
|
||||
log.warn(" --analytics-- data={}", logStr);
|
||||
pres.setConversionStatus(ConversionMessageConstants.OFFICE_DOC_CONVERSION_FAILED_KEY);
|
||||
return pres;
|
||||
}
|
||||
}
|
||||
return pres;
|
||||
}
|
||||
|
||||
public void initialize(UploadedPresentation pres) {
|
||||
pres.setConversionStatus(ConversionMessageConstants.OFFICE_DOC_CONVERSION_FAILED_KEY);
|
||||
}
|
||||
|
||||
private File setupOutputPdfFile(UploadedPresentation pres) {
|
||||
File presentationFile = pres.getUploadedFile();
|
||||
String filenameWithoutExt = presentationFile.getAbsolutePath().substring(0,
|
||||
presentationFile.getAbsolutePath().lastIndexOf('.'));
|
||||
return new File(filenameWithoutExt + ".pdf");
|
||||
}
|
||||
|
||||
private boolean convertOfficeDocToPdf(UploadedPresentation pres,
|
||||
File pdfOutput) {
|
||||
boolean success = false;
|
||||
int attempts = 0;
|
||||
|
||||
while(!success) {
|
||||
LocalConverter documentConverter = LocalConverter
|
||||
.builder()
|
||||
.officeManager(currentManager)
|
||||
.filterChain(new OfficeDocumentConversionFilter())
|
||||
.build();
|
||||
|
||||
success = Office2PdfPageConverter.convert(pres.getUploadedFile(), pdfOutput, 0, pres, documentConverter);
|
||||
|
||||
if(!success) {
|
||||
// In case of failure, try with other open Office Manager
|
||||
|
||||
if(++attempts != officeManagers.size()) {
|
||||
// Go to next Office Manager ( if the last retry with the first one )
|
||||
int currentManagerIndex = officeManagers.indexOf(currentManager);
|
||||
|
||||
boolean isLastManager = ( currentManagerIndex == officeManagers.size()-1 );
|
||||
if(isLastManager) {
|
||||
currentManager = officeManagers.get(0);
|
||||
} else {
|
||||
currentManager = officeManagers.get(currentManagerIndex+1);
|
||||
}
|
||||
} else {
|
||||
// We tried to use all our office managers and it's still failing
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private void makePdfTheUploadedFileAndSetStepAsSuccess(UploadedPresentation pres, File pdf) {
|
||||
pres.setUploadedFile(pdf);
|
||||
pres.setConversionStatus(ConversionMessageConstants.OFFICE_DOC_CONVERSION_SUCCESS_KEY);
|
||||
}
|
||||
|
||||
public void setOfficeDocumentValidator(OfficeDocumentValidator2 v) {
|
||||
officeDocumentValidator = v;
|
||||
}
|
||||
|
||||
public void setSkipOfficePrecheck(boolean skipOfficePrecheck) {
|
||||
this.skipOfficePrecheck = skipOfficePrecheck;
|
||||
}
|
||||
|
||||
public void setSofficeBasePort(int sofficeBasePort) {
|
||||
this.sofficeBasePort = sofficeBasePort;
|
||||
}
|
||||
|
||||
public void setSofficeManagers(int sofficeServiceManagers) {
|
||||
this.sofficeManagers = sofficeServiceManagers;
|
||||
}
|
||||
|
||||
public void setSofficeWorkingDirBase(String sofficeWorkingDirBase) {
|
||||
this.sofficeWorkingDirBase = sofficeWorkingDirBase;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
log.info("Starting LibreOffice pool with " + sofficeManagers + " managers, starting from port " + sofficeBasePort);
|
||||
|
||||
for(int managerIndex = 0; managerIndex < sofficeManagers; managerIndex ++) {
|
||||
Integer instanceNumber = managerIndex + 1; // starts at 1
|
||||
|
||||
try {
|
||||
final File workingDir = new File(sofficeWorkingDirBase + String.format("%02d", instanceNumber));
|
||||
|
||||
if(!workingDir.exists()) {
|
||||
workingDir.mkdir();
|
||||
}
|
||||
|
||||
ExternalOfficeManager officeManager = ExternalOfficeManager
|
||||
.builder()
|
||||
.connectTimeout(2000L)
|
||||
.retryInterval(500L)
|
||||
.portNumber(sofficeBasePort + managerIndex)
|
||||
.connectOnStart(false) // If it's true and soffice is not available, exception is thrown here ( we don't want exception here - we want the manager alive trying to reconnect )
|
||||
.workingDir(workingDir)
|
||||
.build();
|
||||
|
||||
// Workaround for jodconverter not calling makeTempDir when connectOnStart=false (issue 211)
|
||||
Method method = officeManager.getClass().getSuperclass().getDeclaredMethod("makeTempDir");
|
||||
method.setAccessible(true);
|
||||
method.invoke(officeManager);
|
||||
// End of workaround for jodconverter not calling makeTempDir
|
||||
|
||||
officeManager.start();
|
||||
officeManagers.add(officeManager);
|
||||
} catch (Exception e) {
|
||||
log.error("Could not start Office Manager " + instanceNumber + ". Details: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (officeManagers.size() == 0) {
|
||||
log.error("No office managers could be started");
|
||||
return;
|
||||
}
|
||||
|
||||
currentManager = officeManagers.get(0);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
try {
|
||||
officeManagers.forEach(officeManager -> officeManager.stop() );
|
||||
} catch (Exception e) {
|
||||
log.error("Could not stop Office Manager", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package org.bigbluebutton.presentation.imp;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.bigbluebutton.presentation.ConversionMessageConstants;
|
||||
import org.bigbluebutton.presentation.SupportedFileTypes;
|
||||
import org.bigbluebutton.presentation.UploadedPresentation;
|
||||
import org.jodconverter.core.office.OfficeException;
|
||||
import org.jodconverter.core.office.OfficeUtils;
|
||||
import org.jodconverter.local.LocalConverter;
|
||||
import org.jodconverter.local.office.ExternalOfficeManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import com.sun.star.document.UpdateDocMode;
|
||||
import com.google.gson.Gson;
|
||||
public class OfficeToPdfConversionService {
|
||||
private static Logger log = LoggerFactory.getLogger(OfficeToPdfConversionService.class);
|
||||
private OfficeDocumentValidator2 officeDocumentValidator;
|
||||
private final ArrayList<ExternalOfficeManager> officeManagers;
|
||||
private ExternalOfficeManager currentManager = null;
|
||||
private boolean skipOfficePrecheck = false;
|
||||
private int sofficeBasePort = 0;
|
||||
private int sofficeManagers = 0;
|
||||
private String sofficeWorkingDirBase = null;
|
||||
public OfficeToPdfConversionService() throws OfficeException {
|
||||
officeManagers = new ArrayList<>();
|
||||
}
|
||||
/*
|
||||
* Convert the Office document to PDF. If successful, update
|
||||
* UploadPresentation.uploadedFile with the new PDF out and
|
||||
* UploadPresentation.lastStepSuccessful to TRUE.
|
||||
*/
|
||||
public UploadedPresentation convertOfficeToPdf(UploadedPresentation pres) {
|
||||
initialize(pres);
|
||||
if (SupportedFileTypes.isOfficeFile(pres.getFileType())) {
|
||||
// Check if we need to precheck office document
|
||||
if (!skipOfficePrecheck && officeDocumentValidator.isValid(pres)) {
|
||||
Map<String, Object> logData = new HashMap<>();
|
||||
logData.put("meetingId", pres.getMeetingId());
|
||||
logData.put("presId", pres.getId());
|
||||
logData.put("filename", pres.getName());
|
||||
logData.put("logCode", "problems_office_to_pdf_validation");
|
||||
logData.put("message", "Problems detected prior to converting the file to PDF.");
|
||||
Gson gson = new Gson();
|
||||
String logStr = gson.toJson(logData);
|
||||
log.warn(" --analytics-- data={}", logStr);
|
||||
pres.setConversionStatus(ConversionMessageConstants.OFFICE_DOC_CONVERSION_INVALID_KEY);
|
||||
return pres;
|
||||
}
|
||||
File pdfOutput = setupOutputPdfFile(pres);
|
||||
if (convertOfficeDocToPdf(pres, pdfOutput)) {
|
||||
Map<String, Object> logData = new HashMap<>();
|
||||
logData.put("meetingId", pres.getMeetingId());
|
||||
logData.put("presId", pres.getId());
|
||||
logData.put("filename", pres.getName());
|
||||
logData.put("logCode", "office_to_pdf_success");
|
||||
logData.put("message", "Successfully converted office file to pdf.");
|
||||
Gson gson = new Gson();
|
||||
String logStr = gson.toJson(logData);
|
||||
log.info(" --analytics-- data={}", logStr);
|
||||
makePdfTheUploadedFileAndSetStepAsSuccess(pres, pdfOutput);
|
||||
} else {
|
||||
Map<String, Object> logData = new HashMap<>();
|
||||
logData.put("meetingId", pres.getMeetingId());
|
||||
logData.put("presId", pres.getId());
|
||||
logData.put("filename", pres.getName());
|
||||
logData.put("logCode", "office_to_pdf_failed");
|
||||
logData.put("message", "Failed to convert " + pres.getUploadedFile().getAbsolutePath() + " to Pdf.");
|
||||
Gson gson = new Gson();
|
||||
String logStr = gson.toJson(logData);
|
||||
log.warn(" --analytics-- data={}", logStr);
|
||||
pres.setConversionStatus(ConversionMessageConstants.OFFICE_DOC_CONVERSION_FAILED_KEY);
|
||||
return pres;
|
||||
}
|
||||
}
|
||||
return pres;
|
||||
}
|
||||
public void initialize(UploadedPresentation pres) {
|
||||
pres.setConversionStatus(ConversionMessageConstants.OFFICE_DOC_CONVERSION_FAILED_KEY);
|
||||
}
|
||||
private File setupOutputPdfFile(UploadedPresentation pres) {
|
||||
File presentationFile = pres.getUploadedFile();
|
||||
String filenameWithoutExt = presentationFile.getAbsolutePath().substring(0,
|
||||
presentationFile.getAbsolutePath().lastIndexOf('.'));
|
||||
return new File(filenameWithoutExt + ".pdf");
|
||||
}
|
||||
private boolean convertOfficeDocToPdf(UploadedPresentation pres,
|
||||
File pdfOutput) {
|
||||
boolean success = false;
|
||||
int attempts = 0;
|
||||
while(!success) {
|
||||
final Map<String, Object> loadProperties = new HashMap<>();
|
||||
loadProperties.put("Hidden", true);
|
||||
loadProperties.put("ReadOnly", true);
|
||||
loadProperties.put("UpdateDocMode", UpdateDocMode.NO_UPDATE);
|
||||
LocalConverter documentConverter = LocalConverter
|
||||
.builder()
|
||||
.officeManager(currentManager)
|
||||
.loadProperties(loadProperties)
|
||||
.filterChain(new OfficeDocumentConversionFilter())
|
||||
.build();
|
||||
|
||||
success = Office2PdfPageConverter.convert(pres.getUploadedFile(), pdfOutput, 0, pres, documentConverter);
|
||||
|
||||
if(!success) {
|
||||
// In case of failure, try with other open Office Manager
|
||||
|
||||
if(++attempts != officeManagers.size()) {
|
||||
// Go to next Office Manager ( if the last retry with the first one )
|
||||
int currentManagerIndex = officeManagers.indexOf(currentManager);
|
||||
|
||||
boolean isLastManager = ( currentManagerIndex == officeManagers.size()-1 );
|
||||
if(isLastManager) {
|
||||
currentManager = officeManagers.get(0);
|
||||
} else {
|
||||
currentManager = officeManagers.get(currentManagerIndex+1);
|
||||
}
|
||||
} else {
|
||||
// We tried to use all our office managers and it's still failing
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private void makePdfTheUploadedFileAndSetStepAsSuccess(UploadedPresentation pres, File pdf) {
|
||||
pres.setUploadedFile(pdf);
|
||||
pres.setConversionStatus(ConversionMessageConstants.OFFICE_DOC_CONVERSION_SUCCESS_KEY);
|
||||
}
|
||||
|
||||
public void setOfficeDocumentValidator(OfficeDocumentValidator2 v) {
|
||||
officeDocumentValidator = v;
|
||||
}
|
||||
|
||||
public void setSkipOfficePrecheck(boolean skipOfficePrecheck) {
|
||||
this.skipOfficePrecheck = skipOfficePrecheck;
|
||||
}
|
||||
|
||||
public void setSofficeBasePort(int sofficeBasePort) {
|
||||
this.sofficeBasePort = sofficeBasePort;
|
||||
}
|
||||
|
||||
public void setSofficeManagers(int sofficeServiceManagers) {
|
||||
this.sofficeManagers = sofficeServiceManagers;
|
||||
}
|
||||
|
||||
public void setSofficeWorkingDirBase(String sofficeWorkingDirBase) {
|
||||
this.sofficeWorkingDirBase = sofficeWorkingDirBase;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
log.info("Starting LibreOffice pool with " + sofficeManagers + " managers, starting from port " + sofficeBasePort);
|
||||
|
||||
for(int managerIndex = 0; managerIndex < sofficeManagers; managerIndex ++) {
|
||||
Integer instanceNumber = managerIndex + 1; // starts at 1
|
||||
|
||||
try {
|
||||
final File workingDir = new File(sofficeWorkingDirBase + String.format("%02d", instanceNumber));
|
||||
|
||||
if(!workingDir.exists()) {
|
||||
workingDir.mkdir();
|
||||
}
|
||||
|
||||
ExternalOfficeManager officeManager = ExternalOfficeManager
|
||||
.builder()
|
||||
.connectTimeout(2000L)
|
||||
.retryInterval(500L)
|
||||
.portNumber(sofficeBasePort + managerIndex)
|
||||
.connectOnStart(false) // If it's true and soffice is not available, exception is thrown here ( we don't want exception here - we want the manager alive trying to reconnect )
|
||||
.workingDir(workingDir)
|
||||
.build();
|
||||
|
||||
// Workaround for jodconverter not calling makeTempDir when connectOnStart=false (issue 211)
|
||||
Method method = officeManager.getClass().getSuperclass().getDeclaredMethod("makeTempDir");
|
||||
method.setAccessible(true);
|
||||
method.invoke(officeManager);
|
||||
// End of workaround for jodconverter not calling makeTempDir
|
||||
|
||||
officeManager.start();
|
||||
officeManagers.add(officeManager);
|
||||
} catch (Exception e) {
|
||||
log.error("Could not start Office Manager " + instanceNumber + ". Details: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (officeManagers.size() == 0) {
|
||||
log.error("No office managers could be started");
|
||||
return;
|
||||
}
|
||||
|
||||
currentManager = officeManagers.get(0);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
try {
|
||||
officeManagers.forEach(officeManager -> officeManager.stop() );
|
||||
} catch (Exception e) {
|
||||
log.error("Could not stop Office Manager", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
|
||||
* Copyright (c) 2020 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
@ -25,11 +25,11 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.bigbluebutton.api.MeetingService;
|
||||
|
||||
public class RegisteredUserCleanupTimerTask {
|
||||
public class EnteredUserCleanupTimerTask {
|
||||
|
||||
private MeetingService service;
|
||||
private ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);
|
||||
private long runEvery = 60000;
|
||||
private long runEvery = 30000;
|
||||
|
||||
public void setMeetingService(MeetingService svc) {
|
||||
this.service = svc;
|
||||
@ -50,7 +50,7 @@ public class RegisteredUserCleanupTimerTask {
|
||||
private class CleanupTask implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
service.purgeRegisteredUsers();
|
||||
service.purgeEnteredUsers();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2020 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.web.services;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.bigbluebutton.api.MeetingService;
|
||||
|
||||
public class UserCleanupTimerTask {
|
||||
|
||||
private MeetingService service;
|
||||
private ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);
|
||||
private long runEvery = 15000;
|
||||
|
||||
public void setMeetingService(MeetingService svc) {
|
||||
this.service = svc;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
scheduledThreadPool.scheduleWithFixedDelay(new CleanupTask(), 60000, runEvery, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
scheduledThreadPool.shutdownNow();
|
||||
}
|
||||
|
||||
public void setRunEvery(long v) {
|
||||
runEvery = v;
|
||||
}
|
||||
|
||||
private class CleanupTask implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
service.purgeUsers();
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ bbb-webhooks
|
||||
|
||||
This is a node.js application that listens for all events on BigBlueButton and sends POST requests with details about these events to hooks registered via an API. A hook is any external URL that can receive HTTP POST requests.
|
||||
|
||||
You can read the full documentation at: http://docs.bigbluebutton.org/labs/webhooks.html
|
||||
You can read the full documentation at: https://docs.bigbluebutton.org/dev/webhooks.html
|
||||
|
||||
|
||||
Development
|
||||
|
@ -1,83 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
|
||||
|
||||
<head>
|
||||
<title>Guest Lobby</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
||||
<style></style>
|
||||
|
||||
<script src="lib/jquery-2.1.1.min.js" type="text/javascript"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
function updateMessage(message) {
|
||||
$('#content > p').html(message);
|
||||
}
|
||||
|
||||
function findSessionToken() {
|
||||
return location.search
|
||||
.substr(1)
|
||||
.split('&')
|
||||
.find(function(item) {
|
||||
return item.split('=')[0] === 'sessionToken'
|
||||
});
|
||||
};
|
||||
|
||||
function fetchGuestWait(sessionToken) {
|
||||
const GUEST_WAIT_ENDPOINT = '/bigbluebutton/api/guestWait';
|
||||
|
||||
return $.get(GUEST_WAIT_ENDPOINT, sessionToken.concat('&redirect=false'));
|
||||
};
|
||||
|
||||
function pollGuestStatus(token, attempt, limit, everyMs) {
|
||||
setTimeout(function() {
|
||||
var REDIRECT_STATUSES = ['ALLOW', 'DENY'];
|
||||
|
||||
|
||||
if (attempt >= limit) {
|
||||
updateMessage('TIMEOUT_MESSAGE_HERE');
|
||||
return;
|
||||
}
|
||||
|
||||
fetchGuestWait(token).always(function(data) {
|
||||
console.log("data=" + JSON.stringify(data));
|
||||
var status = data.response.guestStatus;
|
||||
|
||||
if (REDIRECT_STATUSES.includes(status)) {
|
||||
window.location = data.response.url;
|
||||
return;
|
||||
}
|
||||
|
||||
return pollGuestStatus(token, attempt + 1, limit, everyMs);
|
||||
})
|
||||
}, everyMs);
|
||||
};
|
||||
|
||||
window.onload = function() {
|
||||
try {
|
||||
var ATTEMPT_EVERY_MS = 5000;
|
||||
var ATTEMPT_LIMIT = 100;
|
||||
|
||||
var sessionToken = findSessionToken();
|
||||
|
||||
if(!sessionToken) {
|
||||
updateMessage('NO_SESSION_TOKEN_MESSAGE');
|
||||
return;
|
||||
}
|
||||
|
||||
pollGuestStatus(sessionToken, 0, ATTEMPT_LIMIT, ATTEMPT_EVERY_MS);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
updateMessage('GENERIC_ERROR_MESSAGE');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="banner"></div>
|
||||
<div id="content">
|
||||
<p>Please wait for a moderator to approve you joining the meeting.</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
File diff suppressed because one or more lines are too long
@ -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
|
||||
|
@ -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 ..."
|
||||
|
@ -81,7 +81,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
});
|
||||
</script>
|
||||
<script src="compatibility/adapter.js?v=VERSION" language="javascript"></script>
|
||||
<script src="compatibility/bowser.js?v=VERSION" language="javascript"></script>
|
||||
<script src="compatibility/sip.js?v=VERSION" language="javascript"></script>
|
||||
<script src="compatibility/kurento-extension.js?v=VERSION" language="javascript"></script>
|
||||
<script src="compatibility/kurento-utils.js?v=VERSION" language="javascript"></script>
|
||||
@ -89,7 +88,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
<body style="background-color: #06172A">
|
||||
<div id="app" role="document"></div>
|
||||
<span id="destination"></span>
|
||||
<audio id="remote-media" autoPlay="autoplay">
|
||||
<track kind="captions" /> {/* These captions are brought to you by eslint */}
|
||||
<audio id="remote-media" autoplay>
|
||||
</audio>
|
||||
</body>
|
||||
|
@ -2,8 +2,6 @@ import { check } from 'meteor/check';
|
||||
|
||||
const ANNOTATION_TYPE_TEXT = 'text';
|
||||
const ANNOTATION_TYPE_PENCIL = 'pencil';
|
||||
const DEFAULT_TEXT_WIDTH = 30;
|
||||
const DEFAULT_TEXT_HEIGHT = 20;
|
||||
|
||||
// line, triangle, ellipse, rectangle
|
||||
function handleCommonAnnotation(meetingId, whiteboardId, userId, annotation) {
|
||||
@ -41,23 +39,6 @@ function handleTextUpdate(meetingId, whiteboardId, userId, annotation) {
|
||||
id, status, annotationType, annotationInfo, wbId, position,
|
||||
} = annotation;
|
||||
|
||||
const { textBoxWidth, textBoxHeight, calcedFontSize } = annotationInfo;
|
||||
const useDefaultSize = (textBoxWidth === 0 && textBoxHeight === 0)
|
||||
|| textBoxWidth < calcedFontSize
|
||||
|| textBoxHeight < calcedFontSize;
|
||||
|
||||
if (useDefaultSize) {
|
||||
annotationInfo.textBoxWidth = DEFAULT_TEXT_WIDTH;
|
||||
annotationInfo.textBoxHeight = DEFAULT_TEXT_HEIGHT;
|
||||
|
||||
if (100 - annotationInfo.x < DEFAULT_TEXT_WIDTH) {
|
||||
annotationInfo.textBoxWidth = 100 - annotationInfo.x;
|
||||
}
|
||||
if (100 - annotationInfo.y < DEFAULT_TEXT_HEIGHT) {
|
||||
annotationInfo.textBoxHeight = 100 - annotationInfo.y;
|
||||
}
|
||||
}
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
id,
|
||||
|
@ -60,14 +60,22 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
|
||||
let iceServers = [];
|
||||
|
||||
try {
|
||||
logger.info({
|
||||
logCode: 'sfuaudiobridge_stunturn_fetch_start',
|
||||
extraInfo: { iceServers },
|
||||
}, 'SFU audio bridge starting STUN/TURN fetch');
|
||||
|
||||
iceServers = await fetchWebRTCMappedStunTurnServers(this.user.sessionToken);
|
||||
} catch (error) {
|
||||
logger.error({ logCode: 'sfuaudiobridge_stunturn_fetch_failed' },
|
||||
'SFU audio bridge failed to fetch STUN/TURN info, using default servers');
|
||||
iceServers = getMappedFallbackStun();
|
||||
} finally {
|
||||
logger.debug({ logCode: 'sfuaudiobridge_stunturn_fetch_sucess', extraInfo: { iceServers } },
|
||||
'SFU audio bridge got STUN/TURN servers');
|
||||
logger.info({
|
||||
logCode: 'sfuaudiobridge_stunturn_fetch_sucess',
|
||||
extraInfo: { iceServers },
|
||||
}, 'SFU audio bridge got STUN/TURN servers');
|
||||
|
||||
const options = {
|
||||
wsUrl: Auth.authenticateURL(SFU_URL),
|
||||
userName: this.user.name,
|
||||
@ -131,12 +139,25 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
|
||||
|
||||
this.hasSuccessfullyStarted = true;
|
||||
if (webRtcPeer) {
|
||||
logger.info({
|
||||
logCode: 'sfuaudiobridge_audio_negotiation_success',
|
||||
}, 'SFU audio bridge negotiated audio with success');
|
||||
|
||||
const stream = webRtcPeer.getRemoteStream();
|
||||
|
||||
audioTag.pause();
|
||||
audioTag.srcObject = stream;
|
||||
audioTag.muted = false;
|
||||
logger.info({
|
||||
logCode: 'sfuaudiobridge_audio_ready_to_play',
|
||||
}, 'SFU audio bridge is ready to play');
|
||||
|
||||
playElement();
|
||||
} else {
|
||||
logger.info({
|
||||
logCode: 'sfuaudiobridge_audio_negotiation_failed',
|
||||
}, 'SFU audio bridge failed to negotiate audio');
|
||||
|
||||
this.callback({
|
||||
status: this.baseCallStates.failed,
|
||||
error: this.baseErrorCodes.CONNECTION_ERROR,
|
||||
@ -218,6 +239,9 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
|
||||
return reject(new Error('Invalid bridge option'));
|
||||
}
|
||||
|
||||
logger.info({
|
||||
logCode: 'sfuaudiobridge_ready_to_join_audio',
|
||||
}, 'SFU audio bridge is ready to join audio');
|
||||
window.kurentoJoinAudio(
|
||||
MEDIA_TAG,
|
||||
this.voiceBridge,
|
||||
|
@ -1,7 +1,10 @@
|
||||
import browser from 'browser-detect';
|
||||
import BaseAudioBridge from './base';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import { fetchStunTurnServers, getFallbackStun } from '/imports/utils/fetchStunTurnServers';
|
||||
import {
|
||||
fetchWebRTCMappedStunTurnServers,
|
||||
getMappedFallbackStun,
|
||||
} from '/imports/utils/fetchStunTurnServers';
|
||||
import {
|
||||
isUnifiedPlan,
|
||||
toUnifiedPlan,
|
||||
@ -20,13 +23,13 @@ const MEDIA_TAG = MEDIA.mediaTag;
|
||||
const CALL_TRANSFER_TIMEOUT = MEDIA.callTransferTimeout;
|
||||
const CALL_HANGUP_TIMEOUT = MEDIA.callHangupTimeout;
|
||||
const CALL_HANGUP_MAX_RETRIES = MEDIA.callHangupMaximumRetries;
|
||||
const RELAY_ONLY_ON_RECONNECT = MEDIA.relayOnlyOnReconnect;
|
||||
const IPV4_FALLBACK_DOMAIN = Meteor.settings.public.app.ipv4FallbackDomain;
|
||||
const ICE_NEGOTIATION_FAILED = ['iceConnectionFailed'];
|
||||
const CALL_CONNECT_TIMEOUT = 20000;
|
||||
const ICE_NEGOTIATION_TIMEOUT = 20000;
|
||||
const AUDIO_SESSION_NUM_KEY = 'AudioSessionNumber';
|
||||
|
||||
const USER_AGENT_RECONNECTION_ATTEMPTS = 3;
|
||||
const USER_AGENT_RECONNECTION_DELAY_MS = 5000;
|
||||
const USER_AGENT_CONNECTION_TIMEOUT_MS = 5000;
|
||||
|
||||
const getAudioSessionNumber = () => {
|
||||
let currItem = parseInt(sessionStorage.getItem(AUDIO_SESSION_NUM_KEY), 10);
|
||||
@ -39,6 +42,24 @@ const getAudioSessionNumber = () => {
|
||||
return currItem;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get error code from SIP.js websocket messages.
|
||||
*/
|
||||
const getErrorCode = (error) => {
|
||||
try {
|
||||
if (!error) return error;
|
||||
|
||||
const match = error.message.match(/code: \d+/g);
|
||||
|
||||
const _codeArray = match[0].split(':');
|
||||
|
||||
return parseInt(_codeArray[1].trim(), 10);
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
class SIPSession {
|
||||
constructor(user, userData, protocol, hostname,
|
||||
baseCallStates, baseErrorCodes, reconnectAttempt) {
|
||||
@ -49,9 +70,15 @@ class SIPSession {
|
||||
this.baseCallStates = baseCallStates;
|
||||
this.baseErrorCodes = baseErrorCodes;
|
||||
this.reconnectAttempt = reconnectAttempt;
|
||||
this.currentSession = null;
|
||||
this.remoteStream = null;
|
||||
this.inputDeviceId = null;
|
||||
this._hangupFlag = false;
|
||||
this._reconnecting = false;
|
||||
this._currentSessionState = null;
|
||||
}
|
||||
|
||||
joinAudio({ isListenOnly, extension, inputStream }, managerCallback) {
|
||||
joinAudio({ isListenOnly, extension, inputDeviceId }, managerCallback) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const callExtension = extension ? `${extension}${this.userData.voiceBridge}` : this.userData.voiceBridge;
|
||||
|
||||
@ -78,7 +105,7 @@ class SIPSession {
|
||||
// If there's an extension passed it means that we're joining the echo test first
|
||||
this.inEchoTest = !!extension;
|
||||
|
||||
return this.doCall({ callExtension, isListenOnly, inputStream })
|
||||
return this.doCall({ callExtension, isListenOnly, inputDeviceId })
|
||||
.catch((reason) => {
|
||||
reject(reason);
|
||||
});
|
||||
@ -87,7 +114,7 @@ class SIPSession {
|
||||
|
||||
async getIceServers(sessionToken) {
|
||||
try {
|
||||
const iceServers = await fetchStunTurnServers(sessionToken);
|
||||
const iceServers = await fetchWebRTCMappedStunTurnServers(sessionToken);
|
||||
return iceServers;
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
@ -98,15 +125,18 @@ class SIPSession {
|
||||
callerIdName: this.user.callerIdName,
|
||||
},
|
||||
}, 'Full audio bridge failed to fetch STUN/TURN info');
|
||||
return getFallbackStun();
|
||||
return getMappedFallbackStun();
|
||||
}
|
||||
}
|
||||
|
||||
doCall(options) {
|
||||
const {
|
||||
isListenOnly,
|
||||
inputDeviceId,
|
||||
} = options;
|
||||
|
||||
this.inputDeviceId = inputDeviceId;
|
||||
|
||||
const {
|
||||
userId,
|
||||
name,
|
||||
@ -124,8 +154,7 @@ class SIPSession {
|
||||
|
||||
return this.getIceServers(sessionToken)
|
||||
.then(this.createUserAgent.bind(this))
|
||||
.then(this.inviteUserAgent.bind(this))
|
||||
.then(this.setupEventHandlers.bind(this));
|
||||
.then(this.inviteUserAgent.bind(this));
|
||||
}
|
||||
|
||||
transferCall(onTransferSuccess) {
|
||||
@ -149,7 +178,18 @@ class SIPSession {
|
||||
}, CALL_TRANSFER_TIMEOUT);
|
||||
|
||||
// This is is the call transfer code ask @chadpilkey
|
||||
this.currentSession.dtmf(1);
|
||||
if (this.sessionSupportRTPPayloadDtmf(this.currentSession)) {
|
||||
this.currentSession.sessionDescriptionHandler.sendDtmf(1);
|
||||
} else {
|
||||
// RFC4733 not supported , sending DTMF through INFO
|
||||
logger.debug({
|
||||
logCode: 'sip_js_rtp_payload_dtmf_not_supported',
|
||||
extraInfo: {
|
||||
callerIdName: this.user.callerIdName,
|
||||
},
|
||||
}, 'Browser do not support payload dtmf, using INFO instead');
|
||||
this.sendDtmf(1);
|
||||
}
|
||||
|
||||
Tracker.autorun((c) => {
|
||||
trackerControl = c;
|
||||
@ -171,28 +211,82 @@ class SIPSession {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* sessionSupportRTPPayloadDtmf
|
||||
* tells if browser support RFC4733 DTMF.
|
||||
* Safari 13 doens't support it yet
|
||||
*/
|
||||
sessionSupportRTPPayloadDtmf(session) {
|
||||
try {
|
||||
const sessionDescriptionHandler = session
|
||||
? session.sessionDescriptionHandler
|
||||
: this.currentSession.sessionDescriptionHandler;
|
||||
|
||||
const senders = sessionDescriptionHandler.peerConnection.getSenders();
|
||||
return !!(senders[0].dtmf);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* sendDtmf - send DTMF Tones using INFO message
|
||||
*
|
||||
* same as SimpleUser's dtmf
|
||||
*/
|
||||
sendDtmf(tone) {
|
||||
const dtmf = tone;
|
||||
const duration = 2000;
|
||||
const body = {
|
||||
contentDisposition: 'render',
|
||||
contentType: 'application/dtmf-relay',
|
||||
content: `Signal=${dtmf}\r\nDuration=${duration}`,
|
||||
};
|
||||
const requestOptions = { body };
|
||||
return this.currentSession.info({ requestOptions });
|
||||
}
|
||||
|
||||
exitAudio() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let hangupRetries = 0;
|
||||
let hangup = false;
|
||||
this._hangupFlag = false;
|
||||
|
||||
this.userRequestedHangup = true;
|
||||
|
||||
if (this.currentSession) {
|
||||
const { mediaHandler } = this.currentSession;
|
||||
|
||||
// Removing termination events to avoid triggering an error
|
||||
ICE_NEGOTIATION_FAILED.forEach(e => mediaHandler.off(e));
|
||||
}
|
||||
const tryHangup = () => {
|
||||
if ((this.currentSession && this.currentSession.endTime)
|
||||
|| (this.userAgent && this.userAgent.status === SIP.UA.C.STATUS_USER_CLOSED)) {
|
||||
hangup = true;
|
||||
if (this._hangupFlag) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
if ((this.currentSession
|
||||
&& (this.currentSession.state === SIP.SessionState.Terminated))
|
||||
|| (this.userAgent && (!this.userAgent.isConnected()))) {
|
||||
this._hangupFlag = true;
|
||||
return resolve();
|
||||
}
|
||||
|
||||
if (this.currentSession) this.currentSession.bye();
|
||||
if (this.userAgent) this.userAgent.stop();
|
||||
if (this.currentSession
|
||||
&& ((this.currentSession.state === SIP.SessionState.Establishing))) {
|
||||
this.currentSession.cancel().then(() => {
|
||||
this._hangupFlag = true;
|
||||
return resolve();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.currentSession
|
||||
&& ((this.currentSession.state === SIP.SessionState.Established))) {
|
||||
this.currentSession.bye().then(() => {
|
||||
this._hangupFlag = true;
|
||||
return resolve();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.userAgent && this.userAgent.isConnected()) {
|
||||
this.userAgent.stop();
|
||||
window.removeEventListener('beforeunload', this.onBeforeUnload);
|
||||
}
|
||||
|
||||
|
||||
hangupRetries += 1;
|
||||
|
||||
@ -206,23 +300,24 @@ class SIPSession {
|
||||
return reject(this.baseErrorCodes.REQUEST_TIMEOUT);
|
||||
}
|
||||
|
||||
if (!hangup) return tryHangup();
|
||||
if (!this._hangupFlag) return tryHangup();
|
||||
return resolve();
|
||||
}, CALL_HANGUP_TIMEOUT);
|
||||
};
|
||||
|
||||
if (this.currentSession) {
|
||||
this.currentSession.on('bye', () => {
|
||||
hangup = true;
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
return tryHangup();
|
||||
});
|
||||
}
|
||||
|
||||
createUserAgent({ stun, turn }) {
|
||||
onBeforeUnload() {
|
||||
if (this.userAgent) {
|
||||
return this.userAgent.stop();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
createUserAgent(iceServers) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.userRequestedHangup === true) reject();
|
||||
|
||||
@ -236,17 +331,6 @@ class SIPSession {
|
||||
sessionToken,
|
||||
} = this.user;
|
||||
|
||||
// WebView safari needs a transceiver to be added. Made it a SIP.js hack.
|
||||
// Don't like the UA picking though, we should straighten everything to user
|
||||
// transceivers - prlanzarin 2019/05/21
|
||||
const browserUA = window.navigator.userAgent.toLocaleLowerCase();
|
||||
const isSafariWebview = ((browserUA.indexOf('iphone') > -1
|
||||
|| browserUA.indexOf('ipad') > -1) && browserUA.indexOf('safari') === -1);
|
||||
|
||||
// Second UA check to get all Safari browsers to enable Unified Plan <-> PlanB
|
||||
// translation
|
||||
const isSafari = browser().name === 'safari';
|
||||
|
||||
logger.debug({ logCode: 'sip_js_creating_user_agent', extraInfo: { callerIdName } }, 'Creating the user agent');
|
||||
|
||||
if (this.userAgent && this.userAgent.isConnected()) {
|
||||
@ -275,112 +359,261 @@ class SIPSession {
|
||||
let userAgentConnected = false;
|
||||
const token = `sessionToken=${sessionToken}`;
|
||||
|
||||
this.userAgent = new window.SIP.UA({
|
||||
uri: `sip:${encodeURIComponent(callerIdName)}@${hostname}`,
|
||||
wsServers: `${(protocol === 'https:' ? 'wss://' : 'ws://')}${hostname}/ws?${token}`,
|
||||
this.userAgent = new SIP.UserAgent({
|
||||
uri: SIP.UserAgent.makeURI(`sip:${encodeURIComponent(callerIdName)}@${hostname}`),
|
||||
transportOptions: {
|
||||
server: `${(protocol === 'https:' ? 'wss://' : 'ws://')}${hostname}/ws?${token}`,
|
||||
connectionTimeout: USER_AGENT_CONNECTION_TIMEOUT_MS,
|
||||
},
|
||||
sessionDescriptionHandlerFactoryOptions: {
|
||||
peerConnectionConfiguration: {
|
||||
iceServers,
|
||||
},
|
||||
},
|
||||
displayName: callerIdName,
|
||||
register: false,
|
||||
traceSip: true,
|
||||
autostart: false,
|
||||
userAgentString: 'BigBlueButton',
|
||||
stunServers: stun,
|
||||
turnServers: turn,
|
||||
hackPlanBUnifiedPlanTranslation: isSafari,
|
||||
hackAddAudioTransceiver: isSafariWebview,
|
||||
relayOnlyOnReconnect: this.reconnectAttempt && RELAY_ONLY_ON_RECONNECT,
|
||||
localSdpCallback,
|
||||
remoteSdpCallback,
|
||||
});
|
||||
|
||||
const handleUserAgentConnection = () => {
|
||||
userAgentConnected = true;
|
||||
resolve(this.userAgent);
|
||||
if (!userAgentConnected) {
|
||||
userAgentConnected = true;
|
||||
resolve(this.userAgent);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUserAgentDisconnection = () => {
|
||||
if (this.userAgent) {
|
||||
this.userAgent.removeAllListeners();
|
||||
this.userAgent.stop();
|
||||
if (this.userRequestedHangup) {
|
||||
userAgentConnected = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let error;
|
||||
let bridgeError;
|
||||
|
||||
if (!this._reconnecting) {
|
||||
|
||||
logger.info({
|
||||
logCode: 'sip_js_session_ua_disconnected',
|
||||
extraInfo: {
|
||||
callerIdName: this.user.callerIdName,
|
||||
},
|
||||
}, 'User agent disconnected: trying to reconnect...'
|
||||
+ ` (userHangup = ${!!this.userRequestedHangup})`);
|
||||
|
||||
logger.info({
|
||||
logCode: 'sip_js_session_ua_reconnecting',
|
||||
extraInfo: {
|
||||
callerIdName: this.user.callerIdName,
|
||||
},
|
||||
}, 'User agent disconnected, reconnecting');
|
||||
|
||||
this.reconnect().then(() => {
|
||||
logger.info({
|
||||
logCode: 'sip_js_session_ua_reconnected',
|
||||
extraInfo: {
|
||||
callerIdName: this.user.callerIdName,
|
||||
},
|
||||
}, 'User agent succesfully reconnected');
|
||||
}).catch(() => {
|
||||
if (userAgentConnected) {
|
||||
error = 1001;
|
||||
bridgeError = 'Websocket disconnected';
|
||||
} else {
|
||||
error = 1002;
|
||||
bridgeError = 'Websocket failed to connect';
|
||||
}
|
||||
this.callback({
|
||||
status: this.baseCallStates.failed,
|
||||
error,
|
||||
bridgeError,
|
||||
});
|
||||
reject(this.baseErrorCodes.CONNECTION_ERROR);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let error;
|
||||
let bridgeError;
|
||||
|
||||
if (this.userRequestedHangup) return;
|
||||
|
||||
if (userAgentConnected) {
|
||||
error = 1001;
|
||||
bridgeError = 'Websocket disconnected';
|
||||
} else {
|
||||
error = 1002;
|
||||
bridgeError = 'Websocket failed to connect';
|
||||
}
|
||||
|
||||
this.callback({
|
||||
status: this.baseCallStates.failed,
|
||||
error,
|
||||
bridgeError,
|
||||
});
|
||||
reject(this.baseErrorCodes.CONNECTION_ERROR);
|
||||
};
|
||||
|
||||
this.userAgent.on('connected', handleUserAgentConnection);
|
||||
this.userAgent.on('disconnected', handleUserAgentDisconnection);
|
||||
this.userAgent.transport.onConnect = handleUserAgentConnection;
|
||||
this.userAgent.transport.onDisconnect = handleUserAgentDisconnection;
|
||||
|
||||
this.userAgent.start();
|
||||
const preturn = this.userAgent.start().then(() => {
|
||||
logger.info({
|
||||
logCode: 'sip_js_session_ua_connected',
|
||||
extraInfo: {
|
||||
callerIdName: this.user.callerIdName,
|
||||
},
|
||||
}, 'User agent succesfully connected');
|
||||
|
||||
window.addEventListener('beforeunload', this.onBeforeUnload.bind(this));
|
||||
|
||||
resolve();
|
||||
}).catch((error) => {
|
||||
logger.info({
|
||||
logCode: 'sip_js_session_ua_reconnecting',
|
||||
extraInfo: {
|
||||
callerIdName: this.user.callerIdName,
|
||||
},
|
||||
}, 'User agent failed to connect, reconnecting');
|
||||
|
||||
const code = getErrorCode(error);
|
||||
|
||||
|
||||
if (code === 1006) {
|
||||
this.callback({
|
||||
status: this.baseCallStates.failed,
|
||||
error: 1006,
|
||||
bridgeError: 'Websocket failed to connect',
|
||||
});
|
||||
return reject({
|
||||
type: this.baseErrorCodes.CONNECTION_ERROR,
|
||||
});
|
||||
}
|
||||
|
||||
this.reconnect().then(() => {
|
||||
logger.info({
|
||||
logCode: 'sip_js_session_ua_reconnected',
|
||||
extraInfo: {
|
||||
callerIdName: this.user.callerIdName,
|
||||
},
|
||||
}, 'User agent succesfully reconnected');
|
||||
|
||||
resolve();
|
||||
}).catch(() => {
|
||||
logger.info({
|
||||
logCode: 'sip_js_session_ua_disconnected',
|
||||
extraInfo: {
|
||||
callerIdName: this.user.callerIdName,
|
||||
},
|
||||
}, 'User agent failed to reconnect after'
|
||||
+ ` ${USER_AGENT_RECONNECTION_ATTEMPTS} attemps`);
|
||||
|
||||
this.callback({
|
||||
status: this.baseCallStates.failed,
|
||||
error: 1002,
|
||||
bridgeError: 'Websocket failed to connect',
|
||||
});
|
||||
|
||||
reject({
|
||||
type: this.baseErrorCodes.CONNECTION_ERROR,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return preturn;
|
||||
});
|
||||
}
|
||||
|
||||
reconnect(attempts = 1) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this._reconnecting) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
if (attempts > USER_AGENT_RECONNECTION_ATTEMPTS) {
|
||||
return reject({
|
||||
type: this.baseErrorCodes.CONNECTION_ERROR,
|
||||
});
|
||||
}
|
||||
|
||||
this._reconnecting = true;
|
||||
|
||||
setTimeout(() => {
|
||||
this.userAgent.reconnect().then(() => {
|
||||
this._reconnecting = false;
|
||||
resolve();
|
||||
}).catch(() => {
|
||||
this._reconnecting = false;
|
||||
this.reconnect(++attempts).then(() => {
|
||||
resolve();
|
||||
}).catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}, USER_AGENT_RECONNECTION_DELAY_MS);
|
||||
});
|
||||
}
|
||||
|
||||
inviteUserAgent(userAgent) {
|
||||
if (this.userRequestedHangup === true) Promise.reject();
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.userRequestedHangup === true) reject();
|
||||
const {
|
||||
hostname,
|
||||
} = this;
|
||||
|
||||
const {
|
||||
hostname,
|
||||
} = this;
|
||||
const {
|
||||
callExtension,
|
||||
isListenOnly,
|
||||
} = this.callOptions;
|
||||
|
||||
const {
|
||||
inputStream,
|
||||
callExtension,
|
||||
} = this.callOptions;
|
||||
|
||||
const options = {
|
||||
media: {
|
||||
stream: inputStream,
|
||||
constraints: {
|
||||
audio: true,
|
||||
video: false,
|
||||
const target = SIP.UserAgent.makeURI(`sip:${callExtension}@${hostname}`);
|
||||
|
||||
const audioDeviceConstraint = this.inputDeviceId
|
||||
? { deviceId: { exact: this.inputDeviceId } }
|
||||
: true;
|
||||
|
||||
const inviterOptions = {
|
||||
sessionDescriptionHandlerOptions: {
|
||||
constraints: {
|
||||
audio: isListenOnly
|
||||
? false
|
||||
: audioDeviceConstraint,
|
||||
video: false,
|
||||
},
|
||||
},
|
||||
render: {
|
||||
remote: document.querySelector(MEDIA_TAG),
|
||||
},
|
||||
},
|
||||
RTCConstraints: {
|
||||
offerToReceiveAudio: true,
|
||||
offerToReceiveVideo: false,
|
||||
},
|
||||
};
|
||||
sessionDescriptionHandlerModifiersPostICEGathering:
|
||||
[stripMDnsCandidates],
|
||||
};
|
||||
|
||||
return userAgent.invite(`sip:${callExtension}@${hostname}`, options);
|
||||
|
||||
if (isListenOnly) {
|
||||
inviterOptions.sessionDescriptionHandlerOptions.offerOptions = {
|
||||
offerToReceiveAudio: true,
|
||||
};
|
||||
}
|
||||
|
||||
const inviter = new SIP.Inviter(userAgent, target, inviterOptions);
|
||||
this.currentSession = inviter;
|
||||
|
||||
this.setupEventHandlers(inviter).then(() => {
|
||||
inviter.invite().then(() => {
|
||||
resolve();
|
||||
}).catch(e => reject(e));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupEventHandlers(currentSession) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.userRequestedHangup === true) reject();
|
||||
|
||||
const { mediaHandler } = currentSession;
|
||||
|
||||
let iceCompleted = false;
|
||||
let fsReady = false;
|
||||
|
||||
this.currentSession = currentSession;
|
||||
const setupRemoteMedia = () => {
|
||||
const mediaElement = document.querySelector(MEDIA_TAG);
|
||||
|
||||
let connectionCompletedEvents = ['iceConnectionCompleted', 'iceConnectionConnected'];
|
||||
// Edge sends a connected first and then a completed, but the call isn't ready until
|
||||
// the completed comes in. Due to the way that we have the listeners set up, the only
|
||||
// way to ignore one status is to not listen for it.
|
||||
if (browser().name === 'edge') {
|
||||
connectionCompletedEvents = ['iceConnectionCompleted'];
|
||||
}
|
||||
this.remoteStream = new MediaStream();
|
||||
|
||||
this.currentSession.sessionDescriptionHandler
|
||||
.peerConnection.getReceivers().forEach((receiver) => {
|
||||
if (receiver.track) {
|
||||
this.remoteStream.addTrack(receiver.track);
|
||||
}
|
||||
});
|
||||
|
||||
logger.info({
|
||||
logCode: 'sip_js_session_playing_remote_media',
|
||||
extraInfo: {
|
||||
callerIdName: this.user.callerIdName,
|
||||
},
|
||||
}, 'Audio call - playing remote media');
|
||||
|
||||
mediaElement.srcObject = this.remoteStream;
|
||||
mediaElement.play();
|
||||
};
|
||||
|
||||
const checkIfCallReady = () => {
|
||||
if (this.userRequestedHangup === true) {
|
||||
@ -388,8 +621,28 @@ class SIPSession {
|
||||
resolve();
|
||||
}
|
||||
|
||||
logger.info({
|
||||
logCode: 'sip_js_session_check_if_call_ready',
|
||||
extraInfo: {
|
||||
iceCompleted,
|
||||
fsReady,
|
||||
},
|
||||
}, 'Audio call - check if ICE is finished and FreeSWITCH is ready');
|
||||
if (iceCompleted && fsReady) {
|
||||
this.webrtcConnected = true;
|
||||
setupRemoteMedia();
|
||||
|
||||
const { sdp } = this.currentSession.sessionDescriptionHandler
|
||||
.peerConnection.remoteDescription;
|
||||
|
||||
logger.info({
|
||||
logCode: 'sip_js_session_setup_remote_media',
|
||||
extraInfo: {
|
||||
callerIdName: this.user.callerIdName,
|
||||
sdp,
|
||||
},
|
||||
}, 'Audio call - setup remote media');
|
||||
|
||||
this.callback({ status: this.baseCallStates.started });
|
||||
resolve();
|
||||
}
|
||||
@ -412,7 +665,6 @@ class SIPSession {
|
||||
const handleSessionAccepted = () => {
|
||||
logger.info({ logCode: 'sip_js_session_accepted', extraInfo: { callerIdName: this.user.callerIdName } }, 'Audio call session accepted');
|
||||
clearTimeout(callTimeout);
|
||||
currentSession.off('accepted', handleSessionAccepted);
|
||||
|
||||
// If ICE isn't connected yet then start timeout waiting for ICE to finish
|
||||
if (!iceCompleted) {
|
||||
@ -420,45 +672,114 @@ class SIPSession {
|
||||
this.callback({
|
||||
status: this.baseCallStates.failed,
|
||||
error: 1010,
|
||||
bridgeError: `ICE negotiation timeout after ${ICE_NEGOTIATION_TIMEOUT / 1000}s`,
|
||||
bridgeError: 'ICE negotiation timeout after '
|
||||
+ `${ICE_NEGOTIATION_TIMEOUT / 1000}s`,
|
||||
});
|
||||
|
||||
this.exitAudio();
|
||||
|
||||
reject({
|
||||
type: this.baseErrorCodes.CONNECTION_ERROR,
|
||||
});
|
||||
}, ICE_NEGOTIATION_TIMEOUT);
|
||||
}
|
||||
};
|
||||
currentSession.on('accepted', handleSessionAccepted);
|
||||
|
||||
const handleSessionProgress = (update) => {
|
||||
logger.info({ logCode: 'sip_js_session_progress', extraInfo: { callerIdName: this.user.callerIdName } }, 'Audio call session progress update');
|
||||
clearTimeout(callTimeout);
|
||||
currentSession.off('progress', handleSessionProgress);
|
||||
};
|
||||
currentSession.on('progress', handleSessionProgress);
|
||||
|
||||
const handleConnectionCompleted = (peer) => {
|
||||
logger.info({
|
||||
logCode: 'sip_js_ice_connection_success',
|
||||
extraInfo: {
|
||||
currentState: peer.iceConnectionState,
|
||||
callerIdName: this.user.callerIdName,
|
||||
},
|
||||
}, `ICE connection success. Current state - ${peer.iceConnectionState}`);
|
||||
clearTimeout(callTimeout);
|
||||
clearTimeout(iceNegotiationTimeout);
|
||||
connectionCompletedEvents.forEach(e => mediaHandler.off(e, handleConnectionCompleted));
|
||||
iceCompleted = true;
|
||||
|
||||
logSelectedCandidate(peer, this.protocolIsIpv6);
|
||||
|
||||
checkIfCallReady();
|
||||
};
|
||||
connectionCompletedEvents.forEach(e => mediaHandler.on(e, handleConnectionCompleted));
|
||||
|
||||
const handleIceNegotiationFailed = (peer) => {
|
||||
if (iceCompleted) {
|
||||
logger.error({
|
||||
logCode: 'sipjs_ice_failed_after',
|
||||
extraInfo: {
|
||||
callerIdName: this.user.callerIdName,
|
||||
},
|
||||
}, 'ICE connection failed after success');
|
||||
} else {
|
||||
logger.error({
|
||||
logCode: 'sipjs_ice_failed_before',
|
||||
extraInfo: {
|
||||
callerIdName: this.user.callerIdName,
|
||||
},
|
||||
}, 'ICE connection failed before success');
|
||||
}
|
||||
clearTimeout(callTimeout);
|
||||
clearTimeout(iceNegotiationTimeout);
|
||||
this.callback({
|
||||
status: this.baseCallStates.failed,
|
||||
error: 1007,
|
||||
bridgeError: 'ICE negotiation failed. Current state '
|
||||
+ `- ${peer.iceConnectionState}`,
|
||||
});
|
||||
};
|
||||
|
||||
const handleIceConnectionTerminated = (peer) => {
|
||||
if (!this.userRequestedHangup) {
|
||||
logger.error({
|
||||
logCode: 'sipjs_ice_closed',
|
||||
extraInfo: {
|
||||
callerIdName: this.user.callerIdName,
|
||||
},
|
||||
}, 'ICE connection closed');
|
||||
}
|
||||
|
||||
this.callback({
|
||||
status: this.baseCallStates.failed,
|
||||
error: 1012,
|
||||
bridgeError: 'ICE connection closed. Current state -'
|
||||
+ `${peer.iceConnectionState}`,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSessionProgress = (update) => {
|
||||
logger.info({
|
||||
logCode: 'sip_js_session_progress',
|
||||
extraInfo: {
|
||||
callerIdName: this.user.callerIdName,
|
||||
update,
|
||||
},
|
||||
}, 'Audio call session progress update');
|
||||
|
||||
this.currentSession.sessionDescriptionHandler.peerConnectionDelegate = {
|
||||
onconnectionstatechange: (event) => {
|
||||
const peer = event.target;
|
||||
|
||||
switch (peer.connectionState) {
|
||||
case 'connected':
|
||||
logger.info({
|
||||
logCode: 'sip_js_ice_connection_success',
|
||||
extraInfo: {
|
||||
currentState: peer.connectionState,
|
||||
callerIdName: this.user.callerIdName,
|
||||
},
|
||||
}, 'ICE connection success. Current state - '
|
||||
+ `${peer.iceConnectionState}`);
|
||||
|
||||
clearTimeout(callTimeout);
|
||||
clearTimeout(iceNegotiationTimeout);
|
||||
|
||||
iceCompleted = true;
|
||||
|
||||
logSelectedCandidate(peer, this.protocolIsIpv6);
|
||||
|
||||
checkIfCallReady();
|
||||
break;
|
||||
case 'failed':
|
||||
handleIceNegotiationFailed(peer);
|
||||
break;
|
||||
|
||||
case 'closed':
|
||||
handleIceConnectionTerminated(peer);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const handleSessionTerminated = (message, cause) => {
|
||||
clearTimeout(callTimeout);
|
||||
clearTimeout(iceNegotiationTimeout);
|
||||
currentSession.off('terminated', handleSessionTerminated);
|
||||
|
||||
if (!message && !cause && !!this.userRequestedHangup) {
|
||||
return this.callback({
|
||||
@ -466,6 +787,10 @@ class SIPSession {
|
||||
});
|
||||
}
|
||||
|
||||
// if session hasn't even started, we let audio-modal to handle
|
||||
// any possile errors
|
||||
if (!this._currentSessionState) return false;
|
||||
|
||||
logger.error({
|
||||
logCode: 'sip_js_call_terminated',
|
||||
extraInfo: { cause, callerIdName: this.user.callerIdName },
|
||||
@ -484,39 +809,33 @@ class SIPSession {
|
||||
bridgeError: cause,
|
||||
});
|
||||
};
|
||||
currentSession.on('terminated', handleSessionTerminated);
|
||||
|
||||
const handleIceNegotiationFailed = (peer) => {
|
||||
if (iceCompleted) {
|
||||
logger.error({ logCode: 'sipjs_ice_failed_after', extraInfo: { callerIdName: this.user.callerIdName } }, 'ICE connection failed after success');
|
||||
} else {
|
||||
logger.error({ logCode: 'sipjs_ice_failed_before', extraInfo: { callerIdName: this.user.callerIdName } }, 'ICE connection failed before success');
|
||||
currentSession.stateChange.addListener((state) => {
|
||||
switch (state) {
|
||||
case SIP.SessionState.Initial:
|
||||
break;
|
||||
case SIP.SessionState.Establishing:
|
||||
handleSessionProgress();
|
||||
break;
|
||||
case SIP.SessionState.Established:
|
||||
handleSessionAccepted();
|
||||
break;
|
||||
case SIP.SessionState.Terminating:
|
||||
break;
|
||||
case SIP.SessionState.Terminated:
|
||||
handleSessionTerminated();
|
||||
break;
|
||||
default:
|
||||
logger.error({
|
||||
logCode: 'sipjs_ice_session_unknown_state',
|
||||
extraInfo: {
|
||||
callerIdName: this.user.callerIdName,
|
||||
},
|
||||
}, 'SIP.js unknown session state');
|
||||
break;
|
||||
}
|
||||
clearTimeout(callTimeout);
|
||||
clearTimeout(iceNegotiationTimeout);
|
||||
ICE_NEGOTIATION_FAILED.forEach(e => mediaHandler.off(e, handleIceNegotiationFailed));
|
||||
this.callback({
|
||||
status: this.baseCallStates.failed,
|
||||
error: 1007,
|
||||
bridgeError: `ICE negotiation failed. Current state - ${peer.iceConnectionState}`,
|
||||
});
|
||||
};
|
||||
ICE_NEGOTIATION_FAILED.forEach(e => mediaHandler.on(e, handleIceNegotiationFailed));
|
||||
|
||||
const handleIceConnectionTerminated = (peer) => {
|
||||
['iceConnectionClosed'].forEach(e => mediaHandler.off(e, handleIceConnectionTerminated));
|
||||
if (!this.userRequestedHangup) {
|
||||
logger.error({ logCode: 'sipjs_ice_closed', extraInfo: { callerIdName: this.user.callerIdName } }, 'ICE connection closed');
|
||||
}
|
||||
/*
|
||||
this.callback({
|
||||
status: this.baseCallStates.failed,
|
||||
error: 1012,
|
||||
bridgeError: "ICE connection closed. Current state - " + peer.iceConnectionState,
|
||||
});
|
||||
*/
|
||||
};
|
||||
['iceConnectionClosed'].forEach(e => mediaHandler.on(e, handleIceConnectionTerminated));
|
||||
this._currentSessionState = state;
|
||||
});
|
||||
|
||||
Tracker.autorun((c) => {
|
||||
const selector = { meetingId: Auth.meetingID, userId: Auth.userID };
|
||||
@ -534,6 +853,8 @@ class SIPSession {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -571,7 +892,11 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
window.clientLogger = logger;
|
||||
}
|
||||
|
||||
joinAudio({ isListenOnly, extension, inputStream }, managerCallback) {
|
||||
get inputDeviceId () {
|
||||
return this.media.inputDevice ? this.media.inputDevice.inputDeviceId : null;
|
||||
}
|
||||
|
||||
joinAudio({ isListenOnly, extension }, managerCallback) {
|
||||
const hasFallbackDomain = typeof IPV4_FALLBACK_DOMAIN === 'string' && IPV4_FALLBACK_DOMAIN !== '';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -603,7 +928,12 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
const fallbackExtension = this.activeSession.inEchoTest ? extension : undefined;
|
||||
this.activeSession = new SIPSession(this.user, this.userData, this.protocol,
|
||||
hostname, this.baseCallStates, this.baseErrorCodes, true);
|
||||
this.activeSession.joinAudio({ isListenOnly, extension: fallbackExtension, inputStream }, callback)
|
||||
const { inputDeviceId } = this.media.inputDevice;
|
||||
this.activeSession.joinAudio({
|
||||
isListenOnly,
|
||||
extension: fallbackExtension,
|
||||
inputDeviceId,
|
||||
}, callback)
|
||||
.then((value) => {
|
||||
resolve(value);
|
||||
}).catch((reason) => {
|
||||
@ -615,7 +945,12 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
return managerCallback(message);
|
||||
};
|
||||
|
||||
this.activeSession.joinAudio({ isListenOnly, extension, inputStream }, callback)
|
||||
const { inputDeviceId } = this.media.inputDevice;
|
||||
this.activeSession.joinAudio({
|
||||
isListenOnly,
|
||||
extension,
|
||||
inputDeviceId,
|
||||
}, callback)
|
||||
.then((value) => {
|
||||
resolve(value);
|
||||
}).catch((reason) => {
|
||||
@ -630,8 +965,8 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
|
||||
getPeerConnection() {
|
||||
const { currentSession } = this.activeSession;
|
||||
if (currentSession && currentSession.mediaHandler) {
|
||||
return currentSession.mediaHandler.peerConnection;
|
||||
if (currentSession && currentSession.sessionDescriptionHandler) {
|
||||
return currentSession.sessionDescriptionHandler.peerConnection;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -641,62 +976,16 @@ export default class SIPBridge extends BaseAudioBridge {
|
||||
}
|
||||
|
||||
setDefaultInputDevice() {
|
||||
const handleMediaSuccess = (mediaStream) => {
|
||||
const deviceLabel = mediaStream.getAudioTracks()[0].label;
|
||||
window.defaultInputStream = mediaStream.getTracks();
|
||||
return navigator.mediaDevices.enumerateDevices().then((mediaDevices) => {
|
||||
const device = mediaDevices.find(d => d.label === deviceLabel);
|
||||
return this.changeInputDevice(device.deviceId, deviceLabel);
|
||||
});
|
||||
};
|
||||
|
||||
return navigator.mediaDevices.getUserMedia({ audio: true }).then(handleMediaSuccess);
|
||||
this.media.inputDevice.inputDeviceId = DEFAULT_INPUT_DEVICE_ID;
|
||||
}
|
||||
|
||||
changeInputDevice(deviceId, deviceLabel) {
|
||||
const {
|
||||
media,
|
||||
} = this;
|
||||
if (media.inputDevice.audioContext) {
|
||||
const handleAudioContextCloseSuccess = () => {
|
||||
media.inputDevice.audioContext = null;
|
||||
media.inputDevice.scriptProcessor = null;
|
||||
media.inputDevice.source = null;
|
||||
return this.changeInputDevice(deviceId);
|
||||
};
|
||||
|
||||
return media.inputDevice.audioContext.close().then(handleAudioContextCloseSuccess);
|
||||
async changeInputDeviceId(inputDeviceId) {
|
||||
if (!inputDeviceId) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if ('AudioContext' in window) {
|
||||
media.inputDevice.audioContext = new window.AudioContext();
|
||||
} else {
|
||||
media.inputDevice.audioContext = new window.webkitAudioContext();
|
||||
}
|
||||
|
||||
media.inputDevice.id = deviceId;
|
||||
media.inputDevice.label = deviceLabel;
|
||||
media.inputDevice.scriptProcessor = media.inputDevice.audioContext
|
||||
.createScriptProcessor(2048, 1, 1);
|
||||
media.inputDevice.source = null;
|
||||
|
||||
const constraints = {
|
||||
audio: {
|
||||
deviceId,
|
||||
},
|
||||
};
|
||||
|
||||
const handleMediaSuccess = (mediaStream) => {
|
||||
media.inputDevice.stream = mediaStream;
|
||||
media.inputDevice.source = media.inputDevice.audioContext
|
||||
.createMediaStreamSource(mediaStream);
|
||||
media.inputDevice.source.connect(media.inputDevice.scriptProcessor);
|
||||
media.inputDevice.scriptProcessor.connect(media.inputDevice.audioContext.destination);
|
||||
|
||||
return this.media.inputDevice;
|
||||
};
|
||||
|
||||
return navigator.mediaDevices.getUserMedia(constraints).then(handleMediaSuccess);
|
||||
this.media.inputDevice.inputDeviceId = inputDeviceId;
|
||||
return inputDeviceId;
|
||||
}
|
||||
|
||||
async changeOutputDevice(value) {
|
||||
|
@ -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,
|
||||
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ export default function endMeeting() {
|
||||
const payload = {
|
||||
userId: requesterUserId,
|
||||
};
|
||||
Logger.verbose(`Meeting '${meetingId}' is destroyed by '${requesterUserId}'`);
|
||||
Logger.warn(`Meeting '${meetingId}' is destroyed by '${requesterUserId}'`);
|
||||
|
||||
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
}
|
||||
|
@ -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}`);
|
||||
});
|
||||
|
@ -4,34 +4,15 @@ import Polls from '/imports/api/polls';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
|
||||
export default function publishVote(id, pollAnswerId) { // TODO discuss location
|
||||
export default function publishVote(pollId, pollAnswerId) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'RespondToPollReqMsg';
|
||||
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
/*
|
||||
We keep an array of people who were in the meeting at the time the poll
|
||||
was started. The poll is published to them only.
|
||||
Once they vote - their ID is removed and they cannot see the poll anymore
|
||||
*/
|
||||
const currentPoll = Polls.findOne({
|
||||
users: requesterUserId,
|
||||
meetingId,
|
||||
'answers.id': pollAnswerId,
|
||||
id,
|
||||
});
|
||||
|
||||
check(pollAnswerId, Number);
|
||||
check(currentPoll, Object);
|
||||
check(currentPoll.meetingId, String);
|
||||
|
||||
const payload = {
|
||||
requesterId: requesterUserId,
|
||||
pollId: currentPoll.id,
|
||||
questionId: 0,
|
||||
answerId: pollAnswerId,
|
||||
};
|
||||
check(pollId, String);
|
||||
|
||||
const selector = {
|
||||
users: requesterUserId,
|
||||
@ -39,6 +20,18 @@ export default function publishVote(id, pollAnswerId) { // TODO discuss location
|
||||
'answers.id': pollAnswerId,
|
||||
};
|
||||
|
||||
const payload = {
|
||||
requesterId: requesterUserId,
|
||||
pollId,
|
||||
questionId: 0,
|
||||
answerId: pollAnswerId,
|
||||
};
|
||||
|
||||
/*
|
||||
We keep an array of people who were in the meeting at the time the poll
|
||||
was started. The poll is published to them only.
|
||||
Once they vote - their ID is removed and they cannot see the poll anymore
|
||||
*/
|
||||
const modifier = {
|
||||
$pull: {
|
||||
users: requesterUserId,
|
||||
@ -47,11 +40,11 @@ export default function publishVote(id, pollAnswerId) { // TODO discuss location
|
||||
|
||||
const cb = (err) => {
|
||||
if (err) {
|
||||
return Logger.error(`Updating Polls collection: ${err}`);
|
||||
return Logger.error(`Removing responded user from Polls collection: ${err}`);
|
||||
}
|
||||
|
||||
return Logger.info(`Updating Polls collection (meetingId: ${meetingId}, `
|
||||
+ `pollId: ${currentPoll.id}!)`);
|
||||
return Logger.info(`Removed responded user=${requesterUserId} from poll (meetingId: ${meetingId}, `
|
||||
+ `pollId: ${pollId}!)`);
|
||||
};
|
||||
|
||||
Polls.update(selector, modifier, cb);
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -31,6 +31,7 @@ const oldParametersKeys = Object.keys(oldParameters);
|
||||
const currentParameters = [
|
||||
// APP
|
||||
'bbb_ask_for_feedback_on_logout',
|
||||
'bbb_override_default_locale',
|
||||
'bbb_auto_join_audio',
|
||||
'bbb_client_title',
|
||||
'bbb_force_listen_only',
|
||||
@ -69,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)`);
|
||||
@ -86,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]),
|
||||
@ -110,7 +112,7 @@ export default function addUserSettings(settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn(`Parameter ${settingKey} not handled`);
|
||||
logger.warn(`Parameter ${normalizedKey} not handled`);
|
||||
});
|
||||
|
||||
const settingsAdded = [];
|
||||
|
@ -59,6 +59,7 @@ export default function handleValidateAuthToken({ body }, meetingId) {
|
||||
|
||||
/* Logic migrated from validateAuthToken method ( postponed to only run in case of success response ) - Begin */
|
||||
const sessionId = `${meetingId}--${userId}`;
|
||||
|
||||
methodInvocationObject.setUserId(sessionId);
|
||||
|
||||
const User = Users.findOne({
|
||||
|
@ -24,6 +24,7 @@ export default function userLeaving(meetingId, userId, connectionId) {
|
||||
|
||||
// If the current user connection is not the same that triggered the leave we skip
|
||||
if (User.connectionId !== connectionId) {
|
||||
Logger.info(`Skipping userLeaving. User connectionId=${User.connectionId} is different from requester connectionId=${connectionId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
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;
|
||||
@ -13,10 +14,27 @@ export default function validateAuthToken(meetingId, requesterUserId, requesterT
|
||||
if (externalId) {
|
||||
if (BannedUsers.has(meetingId, externalId)) {
|
||||
Logger.warn(`A banned user with extId ${externalId} tried to enter in meeting ${meetingId}`);
|
||||
return;
|
||||
return { invalid: true, reason: 'User has been banned', error_type: 'user_banned' };
|
||||
}
|
||||
}
|
||||
|
||||
// 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'}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Store reference of methodInvocationObject ( to postpone the connection userId definition )
|
||||
pendingAuthenticationsStore.add(meetingId, requesterUserId, requesterToken, this);
|
||||
|
||||
|
@ -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];
|
||||
}
|
||||
|
||||
|
@ -359,8 +359,9 @@ const BaseContainer = withTracker(() => {
|
||||
changed: (newDocument) => {
|
||||
if (newDocument.validated && newDocument.name && newDocument.userId !== localUserId) {
|
||||
if (userJoinAudioAlerts) {
|
||||
const audio = new Audio(`${Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename}/resources/sounds/userJoin.mp3`);
|
||||
audio.play();
|
||||
AudioService.playAlertSound(`${Meteor.settings.public.app.cdn
|
||||
+ Meteor.settings.public.app.basename}`
|
||||
+ '/resources/sounds/userJoin.mp3');
|
||||
}
|
||||
|
||||
if (userJoinPushAlerts) {
|
||||
|
@ -1,109 +1,10 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import LoadingScreen from '/imports/ui/components/loading-screen/component';
|
||||
|
||||
// currently supported locales.
|
||||
import ar from 'react-intl/locale-data/ar';
|
||||
import az from 'react-intl/locale-data/az';
|
||||
import bg from 'react-intl/locale-data/bg';
|
||||
import ca from 'react-intl/locale-data/ca';
|
||||
import cs from 'react-intl/locale-data/cs';
|
||||
import da from 'react-intl/locale-data/da';
|
||||
import de from 'react-intl/locale-data/de';
|
||||
import el from 'react-intl/locale-data/el';
|
||||
import en from 'react-intl/locale-data/en';
|
||||
import eo from 'react-intl/locale-data/eo';
|
||||
import es from 'react-intl/locale-data/es';
|
||||
import et from 'react-intl/locale-data/et';
|
||||
import eu from 'react-intl/locale-data/eu';
|
||||
import fa from 'react-intl/locale-data/fa';
|
||||
import fi from 'react-intl/locale-data/fi';
|
||||
import fr from 'react-intl/locale-data/fr';
|
||||
import gl from 'react-intl/locale-data/gl';
|
||||
import he from 'react-intl/locale-data/he';
|
||||
import hi from 'react-intl/locale-data/hi';
|
||||
import hr from 'react-intl/locale-data/hr';
|
||||
import hu from 'react-intl/locale-data/hu';
|
||||
import hy from 'react-intl/locale-data/hy';
|
||||
import id from 'react-intl/locale-data/id';
|
||||
import it from 'react-intl/locale-data/it';
|
||||
import ja from 'react-intl/locale-data/ja';
|
||||
import ka from 'react-intl/locale-data/ka';
|
||||
import km from 'react-intl/locale-data/km';
|
||||
import kn from 'react-intl/locale-data/kn';
|
||||
import ko from 'react-intl/locale-data/ko';
|
||||
import lt from 'react-intl/locale-data/lt';
|
||||
import lv from 'react-intl/locale-data/lv';
|
||||
import nb from 'react-intl/locale-data/nb';
|
||||
import nl from 'react-intl/locale-data/nl';
|
||||
import pl from 'react-intl/locale-data/pl';
|
||||
import pt from 'react-intl/locale-data/pt';
|
||||
import ro from 'react-intl/locale-data/ro';
|
||||
import ru from 'react-intl/locale-data/ru';
|
||||
import sk from 'react-intl/locale-data/sk';
|
||||
import sl from 'react-intl/locale-data/sl';
|
||||
import sr from 'react-intl/locale-data/sr';
|
||||
import sv from 'react-intl/locale-data/sv';
|
||||
import te from 'react-intl/locale-data/te';
|
||||
import th from 'react-intl/locale-data/th';
|
||||
import tr from 'react-intl/locale-data/tr';
|
||||
import uk from 'react-intl/locale-data/uk';
|
||||
import vi from 'react-intl/locale-data/vi';
|
||||
import zh from 'react-intl/locale-data/zh';
|
||||
|
||||
|
||||
addLocaleData([
|
||||
...ar,
|
||||
...az,
|
||||
...bg,
|
||||
...ca,
|
||||
...cs,
|
||||
...da,
|
||||
...de,
|
||||
...el,
|
||||
...et,
|
||||
...en,
|
||||
...eo,
|
||||
...es,
|
||||
...eu,
|
||||
...fa,
|
||||
...fi,
|
||||
...fr,
|
||||
...gl,
|
||||
...he,
|
||||
...hi,
|
||||
...hr,
|
||||
...hu,
|
||||
...hy,
|
||||
...id,
|
||||
...it,
|
||||
...ja,
|
||||
...ka,
|
||||
...km,
|
||||
...kn,
|
||||
...ko,
|
||||
...lt,
|
||||
...lv,
|
||||
...nb,
|
||||
...nl,
|
||||
...pl,
|
||||
...pt,
|
||||
...ro,
|
||||
...ru,
|
||||
...sk,
|
||||
...sl,
|
||||
...sr,
|
||||
...sv,
|
||||
...te,
|
||||
...th,
|
||||
...tr,
|
||||
...uk,
|
||||
...vi,
|
||||
...zh,
|
||||
]);
|
||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
|
||||
const propTypes = {
|
||||
locale: PropTypes.string,
|
||||
@ -120,7 +21,12 @@ const defaultProps = {
|
||||
|
||||
class IntlStartup extends Component {
|
||||
static saveLocale(localeName) {
|
||||
if (Settings.application.locale !== localeName) {
|
||||
Settings.application.changedLocale = localeName;
|
||||
}
|
||||
|
||||
Settings.application.locale = localeName;
|
||||
|
||||
if (RTL_LANGUAGES.includes(localeName.substring(0, 2))) {
|
||||
document.body.parentNode.setAttribute('dir', 'rtl');
|
||||
Settings.application.isRTL = true;
|
||||
@ -155,16 +61,32 @@ class IntlStartup extends Component {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { fetching, normalizedLocale, localeChanged } = this.state;
|
||||
const { locale } = this.props;
|
||||
const { locale, overrideLocale, changedLocale } = this.props;
|
||||
|
||||
if (prevProps.locale !== locale) {
|
||||
this.setState({
|
||||
localeChanged: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (overrideLocale) {
|
||||
if (!fetching
|
||||
&& (overrideLocale !== normalizedLocale.toLowerCase())
|
||||
&& !localeChanged
|
||||
&& !changedLocale) {
|
||||
this.fetchLocalizedMessages(overrideLocale);
|
||||
}
|
||||
|
||||
if (!localeChanged) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fetching
|
||||
&& normalizedLocale
|
||||
&& ((locale.toLowerCase() !== normalizedLocale.toLowerCase()))) {
|
||||
if (((DEFAULT_LANGUAGE === normalizedLocale.toLowerCase()) && !localeChanged)) return;
|
||||
|
||||
this.fetchLocalizedMessages(locale);
|
||||
}
|
||||
}
|
||||
@ -209,10 +131,12 @@ class IntlStartup extends Component {
|
||||
}
|
||||
|
||||
const IntlStartupContainer = withTracker(() => {
|
||||
const { locale } = Settings.application;
|
||||
|
||||
const { locale, changedLocale } = Settings.application;
|
||||
const overrideLocale = getFromUserSettings('bbb_override_default_locale', null);
|
||||
return {
|
||||
locale,
|
||||
overrideLocale,
|
||||
changedLocale,
|
||||
};
|
||||
})(IntlStartup);
|
||||
|
||||
|
@ -71,7 +71,8 @@ class MeteorStream {
|
||||
'logClient',
|
||||
nameFromLevel[this.rec.level],
|
||||
this.rec.msg,
|
||||
{ clientURL },
|
||||
this.rec.logCode,
|
||||
{ ...rec.extraInfo, clientURL },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,11 +8,38 @@ import { lookup as lookupUserAgent } from 'useragent';
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from './logger';
|
||||
import Redis from './redis';
|
||||
|
||||
import setMinBrowserVersions from './minBrowserVersion';
|
||||
import userLeaving from '/imports/api/users/server/methods/userLeaving';
|
||||
|
||||
let guestWaitHtml = '';
|
||||
const AVAILABLE_LOCALES = fs.readdirSync('assets/app/locales');
|
||||
let avaibleLocalesNames = [];
|
||||
const FALLBACK_LOCALES = JSON.parse(Assets.getText('config/fallbackLocales.json'));
|
||||
|
||||
const generateLocaleOptions = () => {
|
||||
try {
|
||||
Logger.warn('Calculating aggregateLocales (heavy)');
|
||||
const tempAggregateLocales = AVAILABLE_LOCALES
|
||||
.map(file => file.replace('.json', ''))
|
||||
.map(file => file.replace('_', '-'))
|
||||
.map((locale) => {
|
||||
const localeName = (Langmap[locale] || {}).nativeName
|
||||
|| (FALLBACK_LOCALES[locale] || {}).nativeName
|
||||
|| locale;
|
||||
return {
|
||||
locale,
|
||||
name: localeName,
|
||||
};
|
||||
});
|
||||
Logger.warn(`Total locales: ${tempAggregateLocales.length}`, tempAggregateLocales);
|
||||
return tempAggregateLocales;
|
||||
} catch (e) {
|
||||
Logger.error(`'Could not process locales error: ${e}`);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
let avaibleLocalesNamesJSON = JSON.stringify(generateLocaleOptions());
|
||||
|
||||
Meteor.startup(() => {
|
||||
const APP_CONFIG = Meteor.settings.public.app;
|
||||
@ -151,23 +178,13 @@ WebApp.connectHandlers.use('/locale', (req, res) => {
|
||||
});
|
||||
|
||||
WebApp.connectHandlers.use('/locales', (req, res) => {
|
||||
if (!avaibleLocalesNames.length) {
|
||||
try {
|
||||
avaibleLocalesNames = AVAILABLE_LOCALES
|
||||
.map(file => file.replace('.json', ''))
|
||||
.map(file => file.replace('_', '-'))
|
||||
.map(locale => ({
|
||||
locale,
|
||||
name: Langmap[locale].nativeName,
|
||||
}));
|
||||
} catch (e) {
|
||||
Logger.warn(`'Could not process locales error: ${e}`);
|
||||
}
|
||||
if (!avaibleLocalesNamesJSON) {
|
||||
avaibleLocalesNamesJSON = JSON.stringify(generateLocaleOptions());
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify(avaibleLocalesNames));
|
||||
res.end(avaibleLocalesNamesJSON);
|
||||
});
|
||||
|
||||
WebApp.connectHandlers.use('/feedback', (req, res) => {
|
||||
@ -227,6 +244,21 @@ WebApp.connectHandlers.use('/useragent', (req, res) => {
|
||||
res.end(response);
|
||||
});
|
||||
|
||||
WebApp.connectHandlers.use('/guestWait', (req, res) => {
|
||||
if (!guestWaitHtml) {
|
||||
try {
|
||||
guestWaitHtml = Assets.getText('static/guest-wait/guest-wait.html');
|
||||
} catch (e) {
|
||||
Logger.warn(`Could not process guest wait html file: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.writeHead(200);
|
||||
res.end(guestWaitHtml);
|
||||
});
|
||||
|
||||
|
||||
export const eventEmitter = Redis.emitter;
|
||||
|
||||
export const redisPubSub = Redis;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, intlShape } from 'react-intl';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import Dropdown from '/imports/ui/components/dropdown/component';
|
||||
import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component';
|
||||
@ -17,7 +17,7 @@ import { styles } from '../styles';
|
||||
|
||||
const propTypes = {
|
||||
amIPresenter: PropTypes.bool.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
mountModal: PropTypes.func.isRequired,
|
||||
amIModerator: PropTypes.bool.isRequired,
|
||||
shortcuts: PropTypes.string,
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { styles } from '/imports/ui/components/actions-bar/styles';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
handleOnClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
@ -118,7 +118,7 @@ class ActionsBar extends PureComponent {
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className={cx(styles.button, autoArrangeLayout || styles.btn)}
|
||||
className={cx(styles.btn, autoArrangeLayout || styles.btn)}
|
||||
icon={autoArrangeLayout ? 'lock' : 'unlock'}
|
||||
color={autoArrangeLayout ? 'primary' : 'default'}
|
||||
ghost={!autoArrangeLayout}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import _ from 'lodash';
|
||||
import cx from 'classnames';
|
||||
import browser from 'browser-detect';
|
||||
@ -109,11 +109,12 @@ 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: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
isInvitation: PropTypes.bool.isRequired,
|
||||
meetingName: PropTypes.string.isRequired,
|
||||
users: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
@ -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 {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { memo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import browser from 'browser-detect';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
@ -12,7 +12,7 @@ import { styles } from '../styles';
|
||||
import ScreenshareBridgeService from '/imports/api/screenshare/client/bridge/service';
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
amIPresenter: PropTypes.bool.isRequired,
|
||||
handleShareScreen: PropTypes.func.isRequired,
|
||||
handleUnshareScreen: PropTypes.func.isRequired,
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import MediaService from '/imports/ui/components/media/service';
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
toggleSwapLayout: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, intlShape } from 'react-intl';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import _ from 'lodash';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
@ -35,7 +35,7 @@ const intlMessages = defineMessages({
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
parseCurrentSlideContent: PropTypes.func.isRequired,
|
||||
amIPresenter: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, intlShape } from 'react-intl';
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import Modal from '/imports/ui/components/modal/simple/component';
|
||||
@ -9,7 +9,7 @@ import { makeCall } from '/imports/ui/services/api';
|
||||
import { styles } from './styles';
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
responseDelay: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { throttle } from 'lodash';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Modal from 'react-modal';
|
||||
import browser from 'browser-detect';
|
||||
import PanelManager from '/imports/ui/components/panel-manager/component';
|
||||
@ -83,7 +83,7 @@ const propTypes = {
|
||||
actionsbar: PropTypes.element,
|
||||
captions: PropTypes.element,
|
||||
locale: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
|
@ -12,6 +12,7 @@ import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import UserInfos from '/imports/api/users-infos';
|
||||
import { startBandwidthMonitoring, updateNavigatorConnection } from '/imports/ui/services/network-information/index';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
|
||||
import {
|
||||
getFontSize,
|
||||
@ -72,9 +73,9 @@ const currentUserEmoji = currentUser => (currentUser ? {
|
||||
status: currentUser.emoji,
|
||||
changedAt: currentUser.emojiTime,
|
||||
} : {
|
||||
status: 'none',
|
||||
changedAt: null,
|
||||
});
|
||||
status: 'none',
|
||||
changedAt: null,
|
||||
});
|
||||
|
||||
export default injectIntl(withModalMounter(withTracker(({ intl, baseControls }) => {
|
||||
const currentUser = Users.findOne({ userId: Auth.userID }, { fields: { approved: 1, emoji: 1 } });
|
||||
@ -91,7 +92,18 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
|
||||
changed(id, fields) {
|
||||
const hasNewConnection = 'connectionId' in fields && (fields.connectionId !== Meteor.connection._lastSessionId);
|
||||
|
||||
if (fields.ejected || hasNewConnection) {
|
||||
if (hasNewConnection) {
|
||||
logger.info({
|
||||
logCode: 'user_connection_id_changed',
|
||||
extraInfo: {
|
||||
currentConnectionId: fields.connectionId,
|
||||
previousConnectionId: Meteor.connection._lastSessionId,
|
||||
},
|
||||
}, 'User connectionId changed ');
|
||||
endMeeting('401');
|
||||
}
|
||||
|
||||
if (fields.ejected) {
|
||||
endMeeting('403');
|
||||
}
|
||||
},
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import { defineMessages, intlShape, injectIntl } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
|
||||
@ -37,7 +37,7 @@ const propTypes = {
|
||||
showMute: PropTypes.bool.isRequired,
|
||||
inAudio: PropTypes.bool.isRequired,
|
||||
listenOnly: PropTypes.bool.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
talking: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, defineMessages, intlShape } from 'react-intl';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import { styles } from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -23,7 +23,7 @@ const intlMessages = defineMessages({
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
formattedDialNum: PropTypes.string.isRequired,
|
||||
telVoice: PropTypes.string.isRequired,
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ import Modal from '/imports/ui/components/modal/simple/component';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import { Session } from 'meteor/session';
|
||||
import {
|
||||
defineMessages, injectIntl, intlShape, FormattedMessage,
|
||||
defineMessages, injectIntl, FormattedMessage,
|
||||
} from 'react-intl';
|
||||
import { styles } from './styles';
|
||||
import PermissionsOverlay from '../permissions-overlay/component';
|
||||
@ -16,7 +16,7 @@ import AudioDial from '../audio-dial/component';
|
||||
import AudioAutoplayPrompt from '../autoplay/component';
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
joinMicrophone: PropTypes.func.isRequired,
|
||||
joinListenOnly: PropTypes.func.isRequired,
|
||||
@ -245,8 +245,6 @@ class AudioModal extends Component {
|
||||
}
|
||||
|
||||
const {
|
||||
inputDeviceId,
|
||||
outputDeviceId,
|
||||
joinEchoTest,
|
||||
} = this.props;
|
||||
|
||||
@ -268,12 +266,27 @@ class AudioModal extends Component {
|
||||
disableActions: false,
|
||||
});
|
||||
}).catch((err) => {
|
||||
if (err.type === 'MEDIA_ERROR') {
|
||||
this.setState({
|
||||
content: 'help',
|
||||
errCode: err.code,
|
||||
disableActions: false,
|
||||
});
|
||||
const { type } = err;
|
||||
switch (type) {
|
||||
case 'MEDIA_ERROR':
|
||||
this.setState({
|
||||
content: 'help',
|
||||
errCode: 0,
|
||||
disableActions: false,
|
||||
});
|
||||
break;
|
||||
case 'CONNECTION_ERROR':
|
||||
this.setState({
|
||||
errCode: 0,
|
||||
disableActions: false,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
this.setState({
|
||||
errCode: 0,
|
||||
disableActions: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import { withModalMounter } from '/imports/ui/components/modal/service';
|
||||
import DeviceSelector from '/imports/ui/components/audio/device-selector/component';
|
||||
@ -9,7 +9,7 @@ import cx from 'classnames';
|
||||
import { styles } from './styles';
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
changeInputDevice: PropTypes.func.isRequired,
|
||||
changeOutputDevice: PropTypes.func.isRequired,
|
||||
handleBack: PropTypes.func.isRequired,
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import { defineMessages, intlShape, injectIntl } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { styles } from './styles.scss';
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
handlePlayAudioSample: PropTypes.func.isRequired,
|
||||
outputDeviceId: PropTypes.string,
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import { defineMessages, intlShape, injectIntl } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { styles } from './styles';
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ const intlMessages = defineMessages({
|
||||
|
||||
const propTypes = {
|
||||
handleAllowAutoplay: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
class AudioAutoplayPrompt extends PureComponent {
|
||||
|
@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Session } from 'meteor/session';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import { defineMessages, intlShape, injectIntl } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { styles } from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -27,7 +27,7 @@ const intlMessages = defineMessages({
|
||||
const propTypes = {
|
||||
handleYes: PropTypes.func.isRequired,
|
||||
handleNo: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
class EchoTest extends Component {
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { injectIntl, intlShape, defineMessages } from 'react-intl';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import Modal from '/imports/ui/components/modal/simple/component';
|
||||
import PropTypes from 'prop-types';
|
||||
import { styles } from './styles';
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
@ -88,4 +88,5 @@ export default {
|
||||
isVoiceUser,
|
||||
autoplayBlocked: () => AudioManager.autoplayBlocked,
|
||||
handleAllowAutoplay: () => AudioManager.handleAllowAutoplay(),
|
||||
playAlertSound: url => AudioManager.playAlertSound(url),
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { withModalMounter } from '/imports/ui/components/modal/service';
|
||||
import Modal from '/imports/ui/components/modal/fullscreen/component';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
@ -40,7 +40,7 @@ const intlMessages = defineMessages({
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
breakout: PropTypes.objectOf(Object).isRequired,
|
||||
getURL: PropTypes.func.isRequired,
|
||||
mountModal: PropTypes.func.isRequired,
|
||||
|
@ -224,13 +224,12 @@ class BreakoutRoom extends PureComponent {
|
||||
(
|
||||
<Button
|
||||
label={
|
||||
moderatorJoinedAudio
|
||||
&& stateBreakoutId === breakoutId
|
||||
&& joinedAudioOnly
|
||||
stateBreakoutId === breakoutId && joinedAudioOnly
|
||||
? intl.formatMessage(intlMessages.breakoutReturnAudio)
|
||||
: intl.formatMessage(intlMessages.breakoutJoinAudio)
|
||||
}
|
||||
className={styles.button}
|
||||
disabled={stateBreakoutId !== breakoutId && joinedAudioOnly}
|
||||
key={`join-audio-${breakoutId}`}
|
||||
onClick={audioAction}
|
||||
/>
|
||||
@ -261,7 +260,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}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AudioService from '/imports/ui/components/audio/service';
|
||||
|
||||
const propTypes = {
|
||||
play: PropTypes.bool.isRequired,
|
||||
@ -8,17 +9,16 @@ const propTypes = {
|
||||
class ChatAudioAlert extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.audio = new Audio(`${Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename}/resources/sounds/notify.mp3`);
|
||||
this.handleAudioLoaded = this.handleAudioLoaded.bind(this);
|
||||
this.playAudio = this.playAudio.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.audio.addEventListener('loadedmetadata', this.handleAudioLoaded);
|
||||
this.handleAudioLoaded();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.audio.removeEventListener('loadedmetadata', this.handleAudioLoaded);
|
||||
this.handleAudioLoaded();
|
||||
}
|
||||
|
||||
handleAudioLoaded() {
|
||||
@ -28,7 +28,9 @@ class ChatAudioAlert extends Component {
|
||||
playAudio() {
|
||||
const { play } = this.props;
|
||||
if (!play) return;
|
||||
this.audio.play();
|
||||
AudioService.playAlertSound(`${Meteor.settings.public.app.cdn
|
||||
+ Meteor.settings.public.app.basename}`
|
||||
+ '/resources/sounds/notify.mp3');
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -116,7 +116,7 @@ export default injectIntl(withTracker(({ intl }) => {
|
||||
|
||||
const messagesFormated = messagesBeforeWelcomeMsg
|
||||
.concat(welcomeMsg)
|
||||
.concat((amIModerator && modOnlyMessage) || [])
|
||||
.concat((amIModerator && modOnlyMessage) ? moderatorMsg : [])
|
||||
.concat(messagesAfterWelcomeMsg);
|
||||
|
||||
messages = messagesFormated.sort((a, b) => (a.time - b.time));
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import cx from 'classnames';
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
import browser from 'browser-detect';
|
||||
@ -10,7 +10,7 @@ import { styles } from './styles.scss';
|
||||
import Button from '../../button/component';
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
chatId: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool.isRequired,
|
||||
minMessageLength: PropTypes.number.isRequired,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import {
|
||||
defineMessages, injectIntl, intlShape, FormattedMessage,
|
||||
defineMessages, injectIntl, FormattedMessage,
|
||||
} from 'react-intl';
|
||||
import browser from 'browser-detect';
|
||||
import PropTypes from 'prop-types';
|
||||
@ -8,7 +8,7 @@ import cx from 'classnames';
|
||||
import { styles } from '../styles.scss';
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
typingUsers: PropTypes.arrayOf(Object).isRequired,
|
||||
};
|
||||
|
||||
|
@ -147,7 +147,7 @@ class MessageList extends Component {
|
||||
handleScrollUpdate,
|
||||
} = this.props;
|
||||
|
||||
if (position !== null && position + target.offsetHeight === target.scrollHeight) {
|
||||
if (position !== null && position + target?.offsetHeight === target?.scrollHeight) {
|
||||
// I used one because the null value is used to notify that
|
||||
// the user has sent a message and the message list should scroll to bottom
|
||||
handleScrollUpdate(1);
|
||||
@ -159,10 +159,10 @@ class MessageList extends Component {
|
||||
|
||||
handleScrollChange(e) {
|
||||
const { scrollArea } = this.state;
|
||||
const scrollCursorPosition = e.scrollTop + scrollArea.offsetHeight;
|
||||
const scrollCursorPosition = e.scrollTop + scrollArea?.offsetHeight;
|
||||
const shouldScrollBottom = e.scrollTop === null
|
||||
|| scrollCursorPosition === scrollArea.scrollHeight
|
||||
|| (scrollArea.scrollHeight - scrollCursorPosition < 1);
|
||||
|| scrollCursorPosition === scrollArea?.scrollHeight
|
||||
|| (scrollArea?.scrollHeight - scrollCursorPosition < 1);
|
||||
|
||||
if ((e.scrollTop < this.lastKnowScrollPosition) && !shouldScrollBottom) {
|
||||
this.setState({ shouldScrollToBottom: false });
|
||||
|
@ -126,6 +126,7 @@ class MessageListItem extends Component {
|
||||
className={styles.avatar}
|
||||
color={user.color}
|
||||
moderator={user.isModerator}
|
||||
avatar={user.avatar}
|
||||
>
|
||||
{user.name.toLowerCase().slice(0, 2)}
|
||||
</UserAvatar>
|
||||
|
@ -50,13 +50,18 @@ const mapGroupMessage = (message) => {
|
||||
const sender = Users.findOne({ userId: message.sender },
|
||||
{
|
||||
fields: {
|
||||
color: 1, role: 1, name: 1, connectionStatus: 1,
|
||||
color: 1,
|
||||
role: 1,
|
||||
name: 1,
|
||||
avatar: 1,
|
||||
connectionStatus: 1,
|
||||
},
|
||||
});
|
||||
const {
|
||||
color,
|
||||
role,
|
||||
name,
|
||||
avatar,
|
||||
connectionStatus,
|
||||
} = sender;
|
||||
|
||||
@ -64,6 +69,7 @@ const mapGroupMessage = (message) => {
|
||||
color,
|
||||
isModerator: role === ROLE_MODERATOR,
|
||||
name,
|
||||
avatar,
|
||||
isOnline: connectionStatus === CONNECTION_STATUS_ONLINE,
|
||||
};
|
||||
|
||||
|
@ -84,8 +84,9 @@ class ConnectionStatusComponent extends PureComponent {
|
||||
<div className={styles.left}>
|
||||
<div className={styles.avatar}>
|
||||
<UserAvatar
|
||||
className={styles.icon}
|
||||
className={cx({ [styles.initials]: conn.avatar.length === 0 })}
|
||||
you={conn.you}
|
||||
avatar={conn.avatar}
|
||||
moderator={conn.moderator}
|
||||
color={conn.color}
|
||||
>
|
||||
|
@ -82,7 +82,7 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
.initials {
|
||||
min-width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
@ -87,6 +87,7 @@ const getConnectionStatus = () => {
|
||||
userId: 1,
|
||||
name: 1,
|
||||
role: 1,
|
||||
avatar: 1,
|
||||
color: 1,
|
||||
connectionStatus: 1,
|
||||
},
|
||||
@ -96,6 +97,7 @@ const getConnectionStatus = () => {
|
||||
userId,
|
||||
name,
|
||||
role,
|
||||
avatar,
|
||||
color,
|
||||
connectionStatus: userStatus,
|
||||
} = user;
|
||||
@ -105,6 +107,7 @@ const getConnectionStatus = () => {
|
||||
if (status) {
|
||||
result.push({
|
||||
name,
|
||||
avatar,
|
||||
offline: userStatus === 'offline',
|
||||
you: Auth.userID === userId,
|
||||
moderator: role === ROLE_MODERATOR,
|
||||
|
@ -35,10 +35,7 @@ export function publishCursorUpdate(payload) {
|
||||
}
|
||||
|
||||
export function initCursorStreamListener() {
|
||||
logger.info({
|
||||
logCode: 'init_cursor_stream_listener',
|
||||
extraInfo: { meetingId: Auth.meetingID, userId: Auth.userID },
|
||||
}, 'initCursorStreamListener called');
|
||||
logger.info({ logCode: 'init_cursor_stream_listener' }, 'initCursorStreamListener called');
|
||||
|
||||
/**
|
||||
* We create a promise to add the handlers after a ddp subscription stop.
|
||||
@ -60,9 +57,7 @@ export function initCursorStreamListener() {
|
||||
});
|
||||
|
||||
startStreamHandlersPromise.then(() => {
|
||||
logger.debug({
|
||||
logCode: 'init_cursor_stream_listener',
|
||||
}, 'initCursorStreamListener called');
|
||||
logger.debug({ logCode: 'cursor_stream_handler_attach' }, 'Attaching handlers for cursor stream');
|
||||
|
||||
cursorStreamListener.on('message', ({ cursors }) => {
|
||||
Object.keys(cursors).forEach((userId) => {
|
||||
|
@ -4,7 +4,7 @@ import { findDOMNode } from 'react-dom';
|
||||
import { isMobile } from 'react-device-detect';
|
||||
import TetherComponent from 'react-tether';
|
||||
import cx from 'classnames';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import screenreaderTrap from 'makeup-screenreader-trap';
|
||||
import { styles } from './styles';
|
||||
@ -52,7 +52,7 @@ const propTypes = {
|
||||
onHide: PropTypes.func,
|
||||
onShow: PropTypes.func,
|
||||
autoFocus: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
tethered: PropTypes.bool,
|
||||
};
|
||||
|
||||
@ -300,4 +300,4 @@ class Dropdown extends Component {
|
||||
|
||||
Dropdown.propTypes = propTypes;
|
||||
Dropdown.defaultProps = defaultProps;
|
||||
export default injectIntl(Dropdown);
|
||||
export default injectIntl(Dropdown, { forwardRef: true });
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import _ from 'lodash';
|
||||
import cx from 'classnames';
|
||||
import Icon from '/imports/ui/components/icon/component';
|
||||
@ -18,7 +19,13 @@ const defaultProps = {
|
||||
tabIndex: 0,
|
||||
};
|
||||
|
||||
export default class DropdownListItem extends Component {
|
||||
const messages = defineMessages({
|
||||
activeAriaLabel: {
|
||||
id: 'app.dropdown.list.item.activeLabel',
|
||||
},
|
||||
});
|
||||
|
||||
class DropdownListItem extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.labelID = _.uniqueId('dropdown-item-label-');
|
||||
@ -38,9 +45,12 @@ export default class DropdownListItem extends Component {
|
||||
render() {
|
||||
const {
|
||||
id, label, description, children, injectRef, tabIndex, onClick, onKeyDown,
|
||||
className, style,
|
||||
className, style, intl,
|
||||
} = this.props;
|
||||
|
||||
const isSelected = className && className.includes('emojiSelected');
|
||||
const _label = isSelected ? `${label} (${intl.formatMessage(messages.activeAriaLabel)})` : label;
|
||||
|
||||
return (
|
||||
<li
|
||||
id={id}
|
||||
@ -59,8 +69,8 @@ export default class DropdownListItem extends Component {
|
||||
children || this.renderDefault()
|
||||
}
|
||||
{
|
||||
label ?
|
||||
(<span id={this.labelID} key="labelledby" hidden>{label}</span>)
|
||||
label
|
||||
? (<span id={this.labelID} key="labelledby" hidden>{_label}</span>)
|
||||
: null
|
||||
}
|
||||
<span id={this.descID} key="describedby" hidden>{description}</span>
|
||||
@ -69,5 +79,7 @@ export default class DropdownListItem extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(DropdownListItem);
|
||||
|
||||
DropdownListItem.propTypes = propTypes;
|
||||
DropdownListItem.defaultProps = defaultProps;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import Modal from '/imports/ui/components/modal/simple/component';
|
||||
import { styles } from './styles';
|
||||
@ -26,7 +26,7 @@ const intlMessages = defineMessages({
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
endMeeting: PropTypes.func.isRequired,
|
||||
};
|
||||
|
@ -3,6 +3,7 @@ import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { withModalMounter } from '/imports/ui/components/modal/service';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import EndMeetingComponent from './component';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
|
||||
const EndMeetingContainer = props => <EndMeetingComponent {...props} />;
|
||||
|
||||
@ -12,6 +13,10 @@ export default withModalMounter(withTracker(({ mountModal }) => ({
|
||||
},
|
||||
|
||||
endMeeting: () => {
|
||||
logger.warn({
|
||||
logCode: 'moderator_forcing_end_meeting',
|
||||
extraInfo: { logType: 'user_action' },
|
||||
}, 'this user clicked on EndMeeting and confirmed, removing everybody from the meeting');
|
||||
makeCall('endMeeting');
|
||||
mountModal(null);
|
||||
},
|
||||
|
@ -15,6 +15,9 @@ const intlMessages = defineMessages({
|
||||
410: {
|
||||
id: 'app.error.410',
|
||||
},
|
||||
408: {
|
||||
id: 'app.error.408',
|
||||
},
|
||||
404: {
|
||||
id: 'app.error.404',
|
||||
defaultMessage: 'Not found',
|
||||
|
@ -104,6 +104,7 @@ class VideoPlayer extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('resize', this.resizeListener);
|
||||
window.addEventListener('layoutSizesSets', this.resizeListener);
|
||||
window.addEventListener('beforeunload', this.onBeforeUnload);
|
||||
|
||||
clearInterval(this.syncInterval);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import loadScript from 'load-script';
|
||||
import React, { Component } from 'react'
|
||||
|
||||
const MATCH_URL = new RegExp("https?:\/\/(\\w+)[.](instructuremedia.com)(\/embed)?\/([-abcdef0-9]+)");
|
||||
const MATCH_URL = new RegExp("https?:\/\/(.*)(instructuremedia.com)(\/embed)?\/([-abcdef0-9]+)");
|
||||
|
||||
const SDK_URL = 'https://files.instructuremedia.com/instructure-media-script/instructure-media-1.1.0.js';
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import cx from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
@ -10,10 +10,14 @@ const intlMessages = defineMessages({
|
||||
id: 'app.fullscreenButton.label',
|
||||
description: 'Fullscreen label',
|
||||
},
|
||||
fullscreenUndoButton: {
|
||||
id: 'app.fullscreenUndoButton.label',
|
||||
description: 'Undo fullscreen label',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
fullscreenRef: PropTypes.instanceOf(Element),
|
||||
dark: PropTypes.bool,
|
||||
bottom: PropTypes.bool,
|
||||
@ -47,10 +51,17 @@ const FullscreenButtonComponent = ({
|
||||
}) => {
|
||||
if (isIphone) return null;
|
||||
|
||||
const formattedLabel = intl.formatMessage(
|
||||
intlMessages.fullscreenButton,
|
||||
({ 0: elementName || '' }),
|
||||
);
|
||||
const formattedLabel = (isFullscreen) => {
|
||||
return(isFullscreen ?
|
||||
intl.formatMessage(
|
||||
intlMessages.fullscreenUndoButton,
|
||||
({ 0: elementName || '' }),
|
||||
) :
|
||||
intl.formatMessage(
|
||||
intlMessages.fullscreenButton,
|
||||
({ 0: elementName || '' }),
|
||||
));
|
||||
};
|
||||
|
||||
const wrapperClassName = cx({
|
||||
[styles.wrapper]: true,
|
||||
@ -67,7 +78,7 @@ const FullscreenButtonComponent = ({
|
||||
icon={!isFullscreen ? 'fullscreen' : 'exit_fullscreen'}
|
||||
size="sm"
|
||||
onClick={() => handleToggleFullScreen(fullscreenRef)}
|
||||
label={formattedLabel}
|
||||
label={formattedLabel(isFullscreen)}
|
||||
hideLabel
|
||||
className={cx(styles.button, styles.fullScreenButton, className)}
|
||||
data-test="presentationFullscreenButton"
|
||||
|
@ -178,6 +178,7 @@ class JoinHandler extends Component {
|
||||
const { response } = parseToJson;
|
||||
|
||||
setLogoutURL(response);
|
||||
logUserInfo();
|
||||
|
||||
if (response.returncode !== 'FAILED') {
|
||||
await setAuth(response);
|
||||
@ -185,7 +186,6 @@ class JoinHandler extends Component {
|
||||
setBannerProps(response);
|
||||
setLogoURL(response);
|
||||
setModOnlyMessage(response);
|
||||
logUserInfo();
|
||||
|
||||
Tracker.autorun(async (cd) => {
|
||||
const user = Users.findOne({ userId: Auth.userID, approved: true }, { fields: { _id: 1 } });
|
||||
|
@ -312,25 +312,43 @@ class LayoutManager extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
if (openPanel === 'userlist') {
|
||||
newChatSize = {
|
||||
width: 0,
|
||||
};
|
||||
newBreakoutRoomSize = {
|
||||
width: 0,
|
||||
};
|
||||
}
|
||||
|
||||
if (openPanel === '') {
|
||||
newUserListSize = {
|
||||
width: 0,
|
||||
};
|
||||
newChatSize = {
|
||||
width: 0,
|
||||
};
|
||||
newBreakoutRoomSize = {
|
||||
width: 0,
|
||||
};
|
||||
switch (openPanel) {
|
||||
case 'userlist': {
|
||||
newChatSize = {
|
||||
width: 0,
|
||||
};
|
||||
newBreakoutRoomSize = {
|
||||
width: 0,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'chat': {
|
||||
newBreakoutRoomSize = {
|
||||
width: 0,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'breakoutroom': {
|
||||
newChatSize = {
|
||||
width: 0,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case '': {
|
||||
newUserListSize = {
|
||||
width: 0,
|
||||
};
|
||||
newChatSize = {
|
||||
width: 0,
|
||||
};
|
||||
newBreakoutRoomSize = {
|
||||
width: 0,
|
||||
};
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error('Unexpected openPanel value');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -5,70 +5,70 @@ import './styles.css';
|
||||
|
||||
|
||||
// currently supported locales.
|
||||
import ar from 'react-intl/locale-data/ar';
|
||||
import bg from 'react-intl/locale-data/bg';
|
||||
import cs from 'react-intl/locale-data/cs';
|
||||
import de from 'react-intl/locale-data/de';
|
||||
import el from 'react-intl/locale-data/el';
|
||||
import en from 'react-intl/locale-data/en';
|
||||
import es from 'react-intl/locale-data/es';
|
||||
import eu from 'react-intl/locale-data/eu';
|
||||
import fa from 'react-intl/locale-data/fa';
|
||||
import fi from 'react-intl/locale-data/fi';
|
||||
import fr from 'react-intl/locale-data/fr';
|
||||
import he from 'react-intl/locale-data/he';
|
||||
import hi from 'react-intl/locale-data/hi';
|
||||
import hu from 'react-intl/locale-data/hu';
|
||||
import id from 'react-intl/locale-data/id';
|
||||
import it from 'react-intl/locale-data/it';
|
||||
import ja from 'react-intl/locale-data/ja';
|
||||
import km from 'react-intl/locale-data/km';
|
||||
import pl from 'react-intl/locale-data/pl';
|
||||
import pt from 'react-intl/locale-data/pt';
|
||||
import ru from 'react-intl/locale-data/ru';
|
||||
import sv from 'react-intl/locale-data/sv';
|
||||
import tr from 'react-intl/locale-data/tr';
|
||||
import uk from 'react-intl/locale-data/uk';
|
||||
import vi from 'react-intl/locale-data/vi';
|
||||
import zh from 'react-intl/locale-data/zh';
|
||||
// import ar from 'react-intl/locale-data/ar';
|
||||
// import bg from 'react-intl/locale-data/bg';
|
||||
// import cs from 'react-intl/locale-data/cs';
|
||||
// import de from 'react-intl/locale-data/de';
|
||||
// import el from 'react-intl/locale-data/el';
|
||||
// import en from 'react-intl/locale-data/en';
|
||||
// import es from 'react-intl/locale-data/es';
|
||||
// import eu from 'react-intl/locale-data/eu';
|
||||
// import fa from 'react-intl/locale-data/fa';
|
||||
// import fi from 'react-intl/locale-data/fi';
|
||||
// import fr from 'react-intl/locale-data/fr';
|
||||
// import he from 'react-intl/locale-data/he';
|
||||
// import hi from 'react-intl/locale-data/hi';
|
||||
// import hu from 'react-intl/locale-data/hu';
|
||||
// import id from 'react-intl/locale-data/id';
|
||||
// import it from 'react-intl/locale-data/it';
|
||||
// import ja from 'react-intl/locale-data/ja';
|
||||
// import km from 'react-intl/locale-data/km';
|
||||
// import pl from 'react-intl/locale-data/pl';
|
||||
// import pt from 'react-intl/locale-data/pt';
|
||||
// import ru from 'react-intl/locale-data/ru';
|
||||
// import sv from 'react-intl/locale-data/sv';
|
||||
// import tr from 'react-intl/locale-data/tr';
|
||||
// import uk from 'react-intl/locale-data/uk';
|
||||
// import vi from 'react-intl/locale-data/vi';
|
||||
// import zh from 'react-intl/locale-data/zh';
|
||||
|
||||
// This class is the only component loaded on legacy (unsupported) browsers.
|
||||
// What is included here needs to be minimal and carefully considered because some
|
||||
// things can't be polyfilled.
|
||||
// // This class is the only component loaded on legacy (unsupported) browsers.
|
||||
// // What is included here needs to be minimal and carefully considered because some
|
||||
// // things can't be polyfilled.
|
||||
|
||||
addLocaleData([
|
||||
...ar,
|
||||
...bg,
|
||||
...cs,
|
||||
...de,
|
||||
...el,
|
||||
...en,
|
||||
...es,
|
||||
...eu,
|
||||
...fa,
|
||||
...fi,
|
||||
...fr,
|
||||
...he,
|
||||
...hi,
|
||||
...hu,
|
||||
...id,
|
||||
...it,
|
||||
...ja,
|
||||
...km,
|
||||
...pl,
|
||||
...pt,
|
||||
...ru,
|
||||
...sv,
|
||||
...tr,
|
||||
...uk,
|
||||
...vi,
|
||||
...zh,
|
||||
]);
|
||||
// addLocaleData([
|
||||
// ...ar,
|
||||
// ...bg,
|
||||
// ...cs,
|
||||
// ...de,
|
||||
// ...el,
|
||||
// ...en,
|
||||
// ...es,
|
||||
// ...eu,
|
||||
// ...fa,
|
||||
// ...fi,
|
||||
// ...fr,
|
||||
// ...he,
|
||||
// ...hi,
|
||||
// ...hu,
|
||||
// ...id,
|
||||
// ...it,
|
||||
// ...ja,
|
||||
// ...km,
|
||||
// ...pl,
|
||||
// ...pt,
|
||||
// ...ru,
|
||||
// ...sv,
|
||||
// ...tr,
|
||||
// ...uk,
|
||||
// ...vi,
|
||||
// ...zh,
|
||||
// ]);
|
||||
|
||||
const FETCHING = 'fetching';
|
||||
const FALLBACK = 'fallback';
|
||||
const READY = 'ready';
|
||||
const supportedBrowsers = ['chrome', 'firefox', 'safari', 'opera', 'edge'];
|
||||
const supportedBrowsers = ['chrome', 'firefox', 'safari', 'opera', 'edge', 'yandex'];
|
||||
|
||||
export default class Legacy extends Component {
|
||||
constructor(props) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import { isMobile, isIPad13 } from 'react-device-detect';
|
||||
import WebcamDraggable from './webcam-draggable-overlay/component';
|
||||
import { styles } from './styles';
|
||||
@ -65,6 +66,9 @@ export default class Media extends Component {
|
||||
[styles.containerV]: webcamsPlacement === 'top' || webcamsPlacement === 'bottom' || webcamsPlacement === 'floating',
|
||||
[styles.containerH]: webcamsPlacement === 'left' || webcamsPlacement === 'right',
|
||||
});
|
||||
const { viewParticipantsWebcams } = Settings.dataSaving;
|
||||
const showVideo = usersVideo.length > 0 && viewParticipantsWebcams;
|
||||
const fullHeight = !showVideo || (webcamsPlacement === 'floating');
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -103,22 +107,18 @@ export default class Media extends Component {
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{
|
||||
usersVideo.length > 0
|
||||
? (
|
||||
<WebcamDraggable
|
||||
refMediaContainer={this.refContainer}
|
||||
swapLayout={swapLayout}
|
||||
singleWebcam={singleWebcam}
|
||||
usersVideoLenght={usersVideo.length}
|
||||
hideOverlay={hideOverlay}
|
||||
disableVideo={disableVideo}
|
||||
audioModalIsOpen={audioModalIsOpen}
|
||||
usersVideo={usersVideo}
|
||||
/>
|
||||
)
|
||||
: null
|
||||
}
|
||||
{showVideo ? (
|
||||
<WebcamDraggable
|
||||
refMediaContainer={this.refContainer}
|
||||
swapLayout={swapLayout}
|
||||
singleWebcam={singleWebcam}
|
||||
usersVideoLenght={usersVideo.length}
|
||||
hideOverlay={hideOverlay}
|
||||
disableVideo={disableVideo}
|
||||
audioModalIsOpen={audioModalIsOpen}
|
||||
usersVideo={usersVideo}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Settings from '/imports/ui/services/settings';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Session } from 'meteor/session';
|
||||
import { notify } from '/imports/ui/services/notification';
|
||||
@ -22,7 +22,7 @@ const KURENTO_CONFIG = Meteor.settings.public.kurento;
|
||||
|
||||
const propTypes = {
|
||||
isScreensharing: PropTypes.bool.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -49,19 +49,22 @@ const intlMessages = defineMessages({
|
||||
});
|
||||
|
||||
class MediaContainer extends Component {
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
document.addEventListener('installChromeExtension', this.installChromeExtension.bind(this));
|
||||
document.addEventListener('screenshareNotSupported', this.screenshareNotSupported.bind(this));
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isScreensharing,
|
||||
intl,
|
||||
} = this.props;
|
||||
const {
|
||||
isScreensharing: wasScreenSharing,
|
||||
} = prevProps;
|
||||
|
||||
if (isScreensharing !== nextProps.isScreensharing) {
|
||||
if (nextProps.isScreensharing) {
|
||||
if (isScreensharing !== wasScreenSharing) {
|
||||
if (wasScreenSharing) {
|
||||
notify(intl.formatMessage(intlMessages.screenshareStarted), 'info', 'desktop');
|
||||
} else {
|
||||
notify(intl.formatMessage(intlMessages.screenshareEnded), 'info', 'desktop');
|
||||
@ -107,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 }} />,
|
||||
|
@ -4,6 +4,7 @@ import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import allowRedirectToLogoutURL from './service';
|
||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import logoutRouteHandler from '/imports/utils/logoutRouteHandler';
|
||||
import Rating from './rating/component';
|
||||
@ -11,6 +12,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: {
|
||||
@ -102,6 +104,7 @@ class MeetingEnded extends PureComponent {
|
||||
super(props);
|
||||
this.state = {
|
||||
selected: 0,
|
||||
dispatched: false,
|
||||
};
|
||||
|
||||
const user = Users.findOne({ userId: Auth.userID });
|
||||
@ -110,8 +113,9 @@ class MeetingEnded extends PureComponent {
|
||||
}
|
||||
|
||||
this.setSelectedStar = this.setSelectedStar.bind(this);
|
||||
this.confirmRedirect = this.confirmRedirect.bind(this);
|
||||
this.sendFeedback = this.sendFeedback.bind(this);
|
||||
this.shouldShowFeedback = getFromUserSettings('bbb_ask_for_feedback_on_logout', Meteor.settings.public.app.askForFeedbackOnLogout);
|
||||
this.shouldShowFeedback = this.shouldShowFeedback.bind(this);
|
||||
|
||||
AudioManager.exitAudio();
|
||||
Meteor.disconnect();
|
||||
@ -123,15 +127,26 @@ class MeetingEnded extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
sendFeedback() {
|
||||
shouldShowFeedback() {
|
||||
const { dispatched } = this.state;
|
||||
return getFromUserSettings('bbb_ask_for_feedback_on_logout', Meteor.settings.public.app.askForFeedbackOnLogout) && !dispatched;
|
||||
}
|
||||
|
||||
confirmRedirect() {
|
||||
const {
|
||||
selected,
|
||||
} = this.state;
|
||||
|
||||
if (selected <= 0) {
|
||||
logoutRouteHandler();
|
||||
return;
|
||||
if (meetingIsBreakout()) window.close();
|
||||
if (allowRedirectToLogoutURL()) logoutRouteHandler();
|
||||
}
|
||||
}
|
||||
|
||||
sendFeedback() {
|
||||
const {
|
||||
selected,
|
||||
} = this.state;
|
||||
|
||||
const { fullname } = Auth.credentials;
|
||||
|
||||
@ -156,25 +171,70 @@ class MeetingEnded extends PureComponent {
|
||||
// client logger
|
||||
logger.info({ logCode: 'feedback_functionality', extraInfo: { feedback: message } }, 'Feedback component');
|
||||
|
||||
const FEEDBACK_WAIT_TIME = 500;
|
||||
setTimeout(() => {
|
||||
fetch(url, options)
|
||||
.then(() => {
|
||||
logoutRouteHandler();
|
||||
})
|
||||
.catch(() => {
|
||||
logoutRouteHandler();
|
||||
});
|
||||
}, FEEDBACK_WAIT_TIME);
|
||||
this.setState({
|
||||
dispatched: true,
|
||||
});
|
||||
|
||||
if (allowRedirectToLogoutURL()) {
|
||||
const FEEDBACK_WAIT_TIME = 500;
|
||||
setTimeout(() => {
|
||||
fetch(url, options)
|
||||
.then(() => {
|
||||
logoutRouteHandler();
|
||||
})
|
||||
.catch(() => {
|
||||
logoutRouteHandler();
|
||||
});
|
||||
}, FEEDBACK_WAIT_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { code, intl, reason } = this.props;
|
||||
const { selected } = this.state;
|
||||
renderNoFeedback() {
|
||||
const { intl, code, reason } = this.props;
|
||||
|
||||
logger.info({ logCode: 'meeting_ended_code', extraInfo: { endedCode: code, reason } }, 'Meeting ended component, no feedback configured');
|
||||
|
||||
return (
|
||||
<div className={styles.parent}>
|
||||
<div className={styles.modal}>
|
||||
<div className={styles.content}>
|
||||
<h1 className={styles.title} data-test="meetingEndedModalTitle">
|
||||
{
|
||||
intl.formatMessage(intlMessage[code] || intlMessage[430])
|
||||
}
|
||||
</h1>
|
||||
{!allowRedirectToLogoutURL() ? null : (
|
||||
<div>
|
||||
<div className={styles.text}>
|
||||
{intl.formatMessage(intlMessage.messageEnded)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={this.confirmRedirect}
|
||||
className={styles.button}
|
||||
label={intl.formatMessage(intlMessage.buttonOkay)}
|
||||
description={intl.formatMessage(intlMessage.confirmDesc)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderFeedback() {
|
||||
const { intl, code, reason } = this.props;
|
||||
const {
|
||||
selected,
|
||||
dispatched,
|
||||
} = this.state;
|
||||
|
||||
const noRating = selected <= 0;
|
||||
|
||||
logger.info({ logCode: 'meeting_ended_code', extraInfo: { endedCode: code, reason } }, 'Meeting ended component');
|
||||
logger.info({ logCode: 'meeting_ended_code', extraInfo: { endedCode: code, reason } }, 'Meeting ended component, feedback allowed');
|
||||
|
||||
return (
|
||||
<div className={styles.parent}>
|
||||
@ -186,11 +246,12 @@ class MeetingEnded extends PureComponent {
|
||||
}
|
||||
</h1>
|
||||
<div className={styles.text}>
|
||||
{this.shouldShowFeedback
|
||||
{this.shouldShowFeedback()
|
||||
? intl.formatMessage(intlMessage.subtitle)
|
||||
: intl.formatMessage(intlMessage.messageEnded)}
|
||||
</div>
|
||||
{this.shouldShowFeedback ? (
|
||||
|
||||
{this.shouldShowFeedback() ? (
|
||||
<div data-test="rating">
|
||||
<Rating
|
||||
total="5"
|
||||
@ -207,22 +268,35 @@ class MeetingEnded extends PureComponent {
|
||||
) : null}
|
||||
</div>
|
||||
) : null }
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={this.sendFeedback}
|
||||
className={styles.button}
|
||||
label={noRating
|
||||
? intl.formatMessage(intlMessage.buttonOkay)
|
||||
: intl.formatMessage(intlMessage.sendLabel)}
|
||||
description={noRating
|
||||
? intl.formatMessage(intlMessage.confirmDesc)
|
||||
: intl.formatMessage(intlMessage.sendDesc)}
|
||||
/>
|
||||
{noRating && allowRedirectToLogoutURL() ? (
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={this.confirmRedirect}
|
||||
className={styles.button}
|
||||
label={intl.formatMessage(intlMessage.buttonOkay)}
|
||||
description={intl.formatMessage(intlMessage.confirmDesc)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!noRating && !dispatched ? (
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={this.sendFeedback}
|
||||
className={styles.button}
|
||||
label={intl.formatMessage(intlMessage.sendLabel)}
|
||||
description={intl.formatMessage(intlMessage.sendDesc)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shouldShowFeedback()) return this.renderFeedback();
|
||||
return this.renderNoFeedback();
|
||||
}
|
||||
}
|
||||
|
||||
MeetingEnded.propTypes = propTypes;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import _ from 'lodash';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { styles } from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -16,7 +16,7 @@ const intlMessages = defineMessages({
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onRate: PropTypes.func.isRequired,
|
||||
total: PropTypes.string.isRequired,
|
||||
};
|
||||
|
@ -0,0 +1,22 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
|
||||
|
||||
export default function allowRedirectToLogoutURL() {
|
||||
const ALLOW_DEFAULT_LOGOUT_URL = Meteor.settings.public.app.allowDefaultLogoutUrl;
|
||||
const protocolPattern = /^((http|https):\/\/)/;
|
||||
if (Auth.logoutURL) {
|
||||
// default logoutURL
|
||||
// compare only the host to ignore protocols
|
||||
const urlWithoutProtocolForAuthLogout = Auth.logoutURL.replace(protocolPattern, '');
|
||||
const urlWithoutProtocolForLocationOrigin = window.location.origin.replace(protocolPattern, '');
|
||||
if (urlWithoutProtocolForAuthLogout === urlWithoutProtocolForLocationOrigin) {
|
||||
return ALLOW_DEFAULT_LOGOUT_URL;
|
||||
}
|
||||
|
||||
// custom logoutURL
|
||||
return true;
|
||||
}
|
||||
// no logout url
|
||||
return false;
|
||||
}
|
@ -3,7 +3,7 @@ import RecordingContainer from '/imports/ui/components/recording/container';
|
||||
import humanizeSeconds from '/imports/utils/humanizeSeconds';
|
||||
import Tooltip from '/imports/ui/components/tooltip/component';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { styles } from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -46,7 +46,7 @@ const intlMessages = defineMessages({
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
amIModerator: PropTypes.bool,
|
||||
record: PropTypes.bool,
|
||||
recording: PropTypes.bool,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withModalMounter } from '/imports/ui/components/modal/service';
|
||||
@ -92,7 +92,7 @@ const intlMessages = defineMessages({
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
handleToggleFullscreen: PropTypes.func.isRequired,
|
||||
mountModal: PropTypes.func.isRequired,
|
||||
noIOSFullscreen: PropTypes.bool,
|
||||
|
@ -98,27 +98,6 @@ class PanelManager extends Component {
|
||||
this.setUserListWidth = this.setUserListWidth.bind(this);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(prevProps) {
|
||||
const { layoutContextState } = this.props;
|
||||
const { layoutContextState: prevLayoutContextState } = prevProps;
|
||||
const {
|
||||
userListSize,
|
||||
chatSize,
|
||||
breakoutRoomSize,
|
||||
} = layoutContextState;
|
||||
const {
|
||||
userListSize: prevUserListSize,
|
||||
chatSize: prevChatSize,
|
||||
breakoutRoomSize: prevBreakoutRoomSize,
|
||||
} = prevLayoutContextState;
|
||||
|
||||
if ((layoutContextState !== prevLayoutContextState)
|
||||
&& (userListSize.width === prevUserListSize.width
|
||||
&& chatSize.width === prevChatSize.width
|
||||
&& breakoutRoomSize.width === prevBreakoutRoomSize.width)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
userlistWidth,
|
||||
|
@ -5,6 +5,7 @@ import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrap
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import cx from 'classnames';
|
||||
import { styles } from './styles.scss';
|
||||
import AudioService from '/imports/ui/components/audio/service';
|
||||
|
||||
const MAX_INPUT_CHARS = 45;
|
||||
|
||||
@ -55,8 +56,9 @@ class Polling extends Component {
|
||||
}
|
||||
|
||||
play() {
|
||||
this.alert = new Audio(`${Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename}/resources/sounds/Poll.mp3`);
|
||||
this.alert.play();
|
||||
AudioService.playAlertSound(`${Meteor.settings.public.app.cdn
|
||||
+ Meteor.settings.public.app.basename}`
|
||||
+ '/resources/sounds/Poll.mp3');
|
||||
}
|
||||
|
||||
handleUpdateResponseInput(e) {
|
||||
@ -78,7 +80,10 @@ class Polling extends Component {
|
||||
typedAns,
|
||||
} = this.state;
|
||||
|
||||
if (!poll) return null;
|
||||
|
||||
const { stackOptions, answers, question } = poll;
|
||||
|
||||
const pollAnswerStyles = {
|
||||
[styles.pollingAnswers]: true,
|
||||
[styles.removeColumns]: answers.length === 1,
|
||||
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import WhiteboardOverlayContainer from '/imports/ui/components/whiteboard/whiteboard-overlay/container';
|
||||
import WhiteboardToolbarContainer from '/imports/ui/components/whiteboard/whiteboard-toolbar/container';
|
||||
import { HUNDRED_PERCENT, MAX_PERCENT } from '/imports/utils/slideCalcUtils';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { toast } from 'react-toastify';
|
||||
import PresentationToolbarContainer from './presentation-toolbar/container';
|
||||
import CursorWrapperContainer from './cursor/cursor-wrapper-container/container';
|
||||
@ -854,7 +854,7 @@ class PresentationArea extends PureComponent {
|
||||
export default injectIntl(withDraggableConsumer(withLayoutConsumer(PresentationArea)));
|
||||
|
||||
PresentationArea.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
podId: PropTypes.string.isRequired,
|
||||
// Defines a boolean value to detect whether a current user is a presenter
|
||||
userIsPresenter: PropTypes.bool.isRequired,
|
||||
|
@ -38,7 +38,7 @@ export default class Cursor extends Component {
|
||||
return obj;
|
||||
}
|
||||
|
||||
static getScaledSizes(props) {
|
||||
static getScaledSizes(props, state) {
|
||||
// TODO: This might need to change for the use case of fit-to-width portrait
|
||||
// slides in non-presenter view. Some elements are still shrinking.
|
||||
const scaleFactor = props.widthRatio / props.physicalWidthRatio;
|
||||
@ -58,14 +58,25 @@ export default class Cursor extends Component {
|
||||
yOffset: props.cursorLabelBox.yOffset * scaleFactor,
|
||||
// making width and height a little bit larger than the size of the text
|
||||
// received from BBox, so that the text didn't touch the border
|
||||
width: (props.labelBoxWidth + 3) * scaleFactor,
|
||||
height: (props.labelBoxHeight + 3) * scaleFactor,
|
||||
width: (state.labelBoxWidth + 3) * scaleFactor,
|
||||
height: (state.labelBoxHeight + 3) * scaleFactor,
|
||||
strokeWidth: props.cursorLabelBox.labelBoxStrokeWidth * scaleFactor,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
scaledSizes: null,
|
||||
labelBoxWidth: 0,
|
||||
labelBoxHeight: 0,
|
||||
};
|
||||
|
||||
this.setLabelBoxDimensions = this.setLabelBoxDimensions.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
cursorX,
|
||||
cursorY,
|
||||
@ -75,8 +86,9 @@ export default class Cursor extends Component {
|
||||
isMultiUser,
|
||||
} = this.props;
|
||||
|
||||
// setting the initial cursor info
|
||||
this.scaledSizes = Cursor.getScaledSizes(this.props);
|
||||
this.setState({
|
||||
scaledSizes: Cursor.getScaledSizes(this.props, this.state),
|
||||
});
|
||||
this.cursorCoordinate = Cursor.getCursorCoordinates(
|
||||
cursorX,
|
||||
cursorY,
|
||||
@ -87,66 +99,83 @@ export default class Cursor extends Component {
|
||||
const { fill, displayLabel } = Cursor.getFillAndLabel(presenter, isMultiUser);
|
||||
this.fill = fill;
|
||||
this.displayLabel = displayLabel;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// we need to find the BBox of the text, so that we could set a proper border box arount it
|
||||
this.calculateCursorLabelBoxDimensions();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
scaledSizes,
|
||||
} = this.state;
|
||||
if (!prevState.scaledSizes && scaledSizes) {
|
||||
this.calculateCursorLabelBoxDimensions();
|
||||
}
|
||||
|
||||
const {
|
||||
presenter,
|
||||
isMultiUser,
|
||||
widthRatio,
|
||||
physicalWidthRatio,
|
||||
labelBoxWidth,
|
||||
labelBoxHeight,
|
||||
cursorX,
|
||||
cursorY,
|
||||
slideWidth,
|
||||
slideHeight,
|
||||
} = this.props;
|
||||
const {
|
||||
labelBoxWidth,
|
||||
labelBoxHeight,
|
||||
} = this.state;
|
||||
|
||||
if (presenter !== nextProps.presenter || isMultiUser !== nextProps.isMultiUser) {
|
||||
const {
|
||||
labelBoxWidth: prevLabelBoxWidth,
|
||||
labelBoxHeight: prevLabelBoxHeight,
|
||||
} = prevState;
|
||||
|
||||
if (presenter !== prevProps.presenter || isMultiUser !== prevProps.isMultiUser) {
|
||||
const { fill, displayLabel } = Cursor.getFillAndLabel(
|
||||
nextProps.presenter,
|
||||
nextProps.isMultiUser,
|
||||
presenter,
|
||||
isMultiUser,
|
||||
);
|
||||
this.displayLabel = displayLabel;
|
||||
this.fill = fill;
|
||||
}
|
||||
|
||||
if ((widthRatio !== nextProps.widthRatio
|
||||
|| physicalWidthRatio !== nextProps.physicalWidthRatio)
|
||||
|| (labelBoxWidth !== nextProps.labelBoxWidth
|
||||
|| labelBoxHeight !== nextProps.labelBoxHeight)) {
|
||||
this.scaledSizes = Cursor.getScaledSizes(nextProps);
|
||||
if ((widthRatio !== prevProps.widthRatio
|
||||
|| physicalWidthRatio !== prevProps.physicalWidthRatio)
|
||||
|| (labelBoxWidth !== prevLabelBoxWidth
|
||||
|| labelBoxHeight !== prevLabelBoxHeight)) {
|
||||
this.setState({
|
||||
scaledSizes: Cursor.getScaledSizes(this.props, this.state),
|
||||
});
|
||||
}
|
||||
|
||||
if (cursorX !== nextProps.cursorX || cursorY !== nextProps.cursorY) {
|
||||
if (cursorX !== prevProps.cursorX || cursorY !== prevProps.cursorY) {
|
||||
const cursorCoordinate = Cursor.getCursorCoordinates(
|
||||
nextProps.cursorX,
|
||||
nextProps.cursorY,
|
||||
nextProps.slideWidth,
|
||||
nextProps.slideHeight,
|
||||
cursorX,
|
||||
cursorY,
|
||||
slideWidth,
|
||||
slideHeight,
|
||||
);
|
||||
this.cursorCoordinate = cursorCoordinate;
|
||||
}
|
||||
}
|
||||
|
||||
setLabelBoxDimensions(labelBoxWidth, labelBoxHeight) {
|
||||
this.setState({
|
||||
labelBoxWidth,
|
||||
labelBoxHeight,
|
||||
});
|
||||
}
|
||||
|
||||
// this function retrieves the text node, measures its BBox and sets the size for the outer box
|
||||
calculateCursorLabelBoxDimensions() {
|
||||
const {
|
||||
setLabelBoxDimensions,
|
||||
} = this.props;
|
||||
|
||||
let labelBoxWidth = 0;
|
||||
let labelBoxHeight = 0;
|
||||
|
||||
if (this.cursorLabelRef) {
|
||||
const { width, height } = this.cursorLabelRef.getBBox();
|
||||
const { widthRatio, physicalWidthRatio, cursorLabelBox } = this.props;
|
||||
labelBoxWidth = Cursor.invertScale(width, widthRatio, physicalWidthRatio);
|
||||
labelBoxHeight = Cursor.invertScale(height, widthRatio, physicalWidthRatio);
|
||||
|
||||
// if the width of the text node is bigger than the maxSize - set the width to maxWidth
|
||||
if (labelBoxWidth > cursorLabelBox.maxWidth) {
|
||||
labelBoxWidth = cursorLabelBox.maxWidth;
|
||||
@ -154,26 +183,30 @@ export default class Cursor extends Component {
|
||||
}
|
||||
|
||||
// updating labelBoxWidth and labelBoxHeight in the container, which then passes it down here
|
||||
setLabelBoxDimensions(labelBoxWidth, labelBoxHeight);
|
||||
this.setLabelBoxDimensions(labelBoxWidth, labelBoxHeight);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
scaledSizes,
|
||||
} = this.state;
|
||||
const {
|
||||
cursorId,
|
||||
userName,
|
||||
isRTL,
|
||||
} = this.props;
|
||||
|
||||
|
||||
const {
|
||||
cursorCoordinate,
|
||||
fill,
|
||||
} = this;
|
||||
|
||||
|
||||
if (!scaledSizes) return null;
|
||||
const {
|
||||
cursorLabelBox,
|
||||
cursorLabelText,
|
||||
finalRadius,
|
||||
} = this.scaledSizes;
|
||||
} = scaledSizes;
|
||||
|
||||
const {
|
||||
x,
|
||||
@ -191,7 +224,7 @@ export default class Cursor extends Component {
|
||||
<circle
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={finalRadius === Infinity ? 0 : finalRadius}
|
||||
r={finalRadius}
|
||||
fill={fill}
|
||||
fillOpacity="0.6"
|
||||
/>
|
||||
@ -292,17 +325,6 @@ Cursor.propTypes = {
|
||||
fontSize: PropTypes.number.isRequired,
|
||||
}),
|
||||
|
||||
// Defines the width of the label box
|
||||
labelBoxWidth: PropTypes.number.isRequired,
|
||||
// Defines the height of the label box
|
||||
labelBoxHeight: PropTypes.number.isRequired,
|
||||
|
||||
// Defines the function, which sets the state for the label box and passes it back down
|
||||
// we need it, since we need to render the text first -> measure its dimensions ->
|
||||
// set proper width and height of the border box -> pass it down ->
|
||||
// catch in the 'componentWillReceiveProps' -> apply new values
|
||||
setLabelBoxDimensions: PropTypes.func.isRequired,
|
||||
|
||||
// Defines the direction the client text should be displayed
|
||||
isRTL: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
@ -6,33 +6,15 @@ import Cursor from './component';
|
||||
|
||||
|
||||
class CursorContainer extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
labelBoxWidth: 0,
|
||||
labelBoxHeight: 0,
|
||||
};
|
||||
this.setLabelBoxDimensions = this.setLabelBoxDimensions.bind(this);
|
||||
}
|
||||
|
||||
setLabelBoxDimensions(labelBoxWidth, labelBoxHeight) {
|
||||
this.setState({
|
||||
labelBoxWidth,
|
||||
labelBoxHeight,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { cursorX, cursorY } = this.props;
|
||||
const { labelBoxWidth, labelBoxHeight } = this.state;
|
||||
|
||||
if (cursorX > 0 && cursorY > 0) {
|
||||
return (
|
||||
<Cursor
|
||||
cursorX={cursorX}
|
||||
cursorY={cursorY}
|
||||
labelBoxWidth={labelBoxWidth}
|
||||
labelBoxHeight={labelBoxHeight}
|
||||
setLabelBoxDimensions={this.setLabelBoxDimensions}
|
||||
{...this.props}
|
||||
/>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import cx from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
@ -13,7 +13,7 @@ const intlMessages = defineMessages({
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
handleDownloadPresentation: PropTypes.func.isRequired,
|
||||
dark: PropTypes.bool,
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ import ZoomTool from './zoom-tool/component';
|
||||
import FullscreenButtonContainer from '../../fullscreen-button/container';
|
||||
import TooltipContainer from '/imports/ui/components/tooltip/container';
|
||||
import QuickPollDropdownContainer from '/imports/ui/components/actions-bar/quick-poll-dropdown/container';
|
||||
import FullscreenService from '/imports/ui/components/fullscreen-button/service';
|
||||
import KEY_CODES from '/imports/utils/keyCodes';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
@ -101,6 +102,7 @@ class PresentationToolbar extends PureComponent {
|
||||
switchSlide(event) {
|
||||
const { target, which } = event;
|
||||
const isBody = target.nodeName === 'BODY';
|
||||
const { fullscreenRef } = this.props;
|
||||
|
||||
if (isBody) {
|
||||
switch (which) {
|
||||
@ -112,6 +114,9 @@ class PresentationToolbar extends PureComponent {
|
||||
case KEY_CODES.PAGE_DOWN:
|
||||
this.nextSlideHandler();
|
||||
break;
|
||||
case KEY_CODES.ENTER:
|
||||
FullscreenService.toggleFullScreen(fullscreenRef);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user