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 fromAkkaAppsJsonChannel = Try(config.getString("eventBus.fromAkkaAppsChannel")).getOrElse("from-akka-apps-json-channel")
|
||||||
|
|
||||||
lazy val applyPermissionCheck = Try(config.getBoolean("apps.checkPermissions")).getOrElse(false)
|
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 voiceConfRecordPath = Try(config.getString("voiceConf.recordPath")).getOrElse("/var/freeswitch/meetings")
|
||||||
lazy val voiceConfRecordCodec = Try(config.getString("voiceConf.recordCodec")).getOrElse("wav")
|
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 MOD_LEVEL = 100
|
||||||
val AUTHED_LEVEL = 50
|
val AUTHED_LEVEL = 50
|
||||||
@ -83,12 +83,17 @@ object PermissionCheck {
|
|||||||
|
|
||||||
def ejectUserForFailedPermission(meetingId: String, userId: String, reason: String,
|
def ejectUserForFailedPermission(meetingId: String, userId: String, reason: String,
|
||||||
outGW: OutMsgRouter, liveMeeting: LiveMeeting): Unit = {
|
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
|
// send a system message to force disconnection
|
||||||
Sender.sendDisconnectClientSysMsg(meetingId, userId, ejectedBy, reason, outGW)
|
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 = {
|
def addOldPresenter(users: Users2x, userId: String): OldPresenter = {
|
||||||
|
@ -56,7 +56,7 @@ trait RegisterUserReqMsgHdlr {
|
|||||||
val g = GuestApprovedVO(regUser.id, GuestStatus.ALLOW)
|
val g = GuestApprovedVO(regUser.id, GuestStatus.ALLOW)
|
||||||
UsersApp.approveOrRejectGuest(liveMeeting, outGW, g, SystemUser.ID)
|
UsersApp.approveOrRejectGuest(liveMeeting, outGW, g, SystemUser.ID)
|
||||||
case GuestStatus.WAIT =>
|
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)
|
addGuestToWaitingForApproval(guest, liveMeeting.guestsWaiting)
|
||||||
notifyModeratorsOfGuestWaiting(Vector(guest), liveMeeting.users2x, liveMeeting.props.meetingProp.intId)
|
notifyModeratorsOfGuestWaiting(Vector(guest), liveMeeting.users2x, liveMeeting.props.meetingProp.intId)
|
||||||
case GuestStatus.DENY =>
|
case GuestStatus.DENY =>
|
||||||
|
@ -51,7 +51,7 @@ class GuestsWaiting {
|
|||||||
def setGuestPolicy(policy: GuestPolicy) = guestPolicy = policy
|
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)
|
case class GuestPolicy(policy: String, setBy: String)
|
||||||
|
|
||||||
object GuestPolicyType {
|
object GuestPolicyType {
|
||||||
|
@ -27,7 +27,7 @@ import java.util.*;
|
|||||||
* Source:<br>
|
* Source:<br>
|
||||||
* Phoenix: An Interactive Curve Design System Based on the Automatic Fitting
|
* Phoenix: An Interactive Curve Design System Based on the Automatic Fitting
|
||||||
* of Hand-Sketched Curves.<br>
|
* 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
|
* A thesis submitted in partial fulfillment of the requirements for the degree
|
||||||
* of Master of Science, University of Washington.
|
* of Master of Science, University of Washington.
|
||||||
* <p>
|
* <p>
|
||||||
@ -238,7 +238,7 @@ public class Bezier {
|
|||||||
* @param digitizedPoints Digitized points
|
* @param digitizedPoints Digitized points
|
||||||
* @param maxAngle maximal angle in radians between the current point and its
|
* @param maxAngle maximal angle in radians between the current point and its
|
||||||
* predecessor and successor up to which the point does not break the
|
* 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
|
* @return Segments of digitized points, each segment having less than maximal
|
||||||
* angle between points.
|
* angle between points.
|
||||||
*/
|
*/
|
||||||
|
@ -44,7 +44,7 @@ object MsgBuilder {
|
|||||||
val envelope = BbbCoreEnvelope(GetGuestsWaitingApprovalRespMsg.NAME, routing)
|
val envelope = BbbCoreEnvelope(GetGuestsWaitingApprovalRespMsg.NAME, routing)
|
||||||
val header = BbbClientMsgHeader(GetGuestsWaitingApprovalRespMsg.NAME, meetingId, userId)
|
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 body = GetGuestsWaitingApprovalRespMsgBody(guestsWaiting)
|
||||||
val event = GetGuestsWaitingApprovalRespMsg(header, body)
|
val event = GetGuestsWaitingApprovalRespMsg(header, body)
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ object MsgBuilder {
|
|||||||
val envelope = BbbCoreEnvelope(GuestsWaitingForApprovalEvtMsg.NAME, routing)
|
val envelope = BbbCoreEnvelope(GuestsWaitingForApprovalEvtMsg.NAME, routing)
|
||||||
val header = BbbClientMsgHeader(GuestsWaitingForApprovalEvtMsg.NAME, meetingId, userId)
|
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 body = GuestsWaitingForApprovalEvtMsgBody(guestsWaiting)
|
||||||
val event = GuestsWaitingForApprovalEvtMsg(header, body)
|
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,
|
val guest1 = createUserVoiceAndCam(liveMeeting, Roles.VIEWER_ROLE, guest = true, authed = true, CallingWith.WEBRTC, muted = false,
|
||||||
talking = false, listenOnly = false)
|
talking = false, listenOnly = false)
|
||||||
Users2x.add(liveMeeting.users2x, guest1)
|
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)
|
GuestsWaiting.add(liveMeeting.guestsWaiting, guestWait1)
|
||||||
|
|
||||||
val guest2 = createUserVoiceAndCam(liveMeeting, Roles.VIEWER_ROLE, guest = true, authed = true, CallingWith.FLASH, muted = false,
|
val guest2 = createUserVoiceAndCam(liveMeeting, Roles.VIEWER_ROLE, guest = true, authed = true, CallingWith.FLASH, muted = false,
|
||||||
talking = false, listenOnly = false)
|
talking = false, listenOnly = false)
|
||||||
Users2x.add(liveMeeting.users2x, guest2)
|
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)
|
GuestsWaiting.add(liveMeeting.guestsWaiting, guestWait2)
|
||||||
|
|
||||||
val vu1 = FakeUserGenerator.createFakeVoiceOnlyUser(CallingWith.PHONE, muted = false, talking = false, listenOnly = false)
|
val vu1 = FakeUserGenerator.createFakeVoiceOnlyUser(CallingWith.PHONE, muted = false, talking = false, listenOnly = false)
|
||||||
|
@ -71,6 +71,7 @@ services {
|
|||||||
|
|
||||||
apps {
|
apps {
|
||||||
checkPermissions = true
|
checkPermissions = true
|
||||||
|
ejectOnViolation = false
|
||||||
endMeetingWhenNoMoreAuthedUsers = false
|
endMeetingWhenNoMoreAuthedUsers = false
|
||||||
endMeetingWhenNoMoreAuthedUsersAfterMinutes = 2
|
endMeetingWhenNoMoreAuthedUsersAfterMinutes = 2
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ case class GetGuestsWaitingApprovalRespMsg(
|
|||||||
body: GetGuestsWaitingApprovalRespMsgBody
|
body: GetGuestsWaitingApprovalRespMsgBody
|
||||||
) extends BbbCoreMsg
|
) extends BbbCoreMsg
|
||||||
case class GetGuestsWaitingApprovalRespMsgBody(guests: Vector[GuestWaitingVO])
|
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
|
* 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 = "lockSettingsLockOnJoin";
|
||||||
public static final String LOCK_SETTINGS_LOCK_ON_JOIN_CONFIGURABLE = "lockSettingsLockOnJoinConfigurable";
|
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() {
|
private ApiParams() {
|
||||||
throw new IllegalStateException("ApiParams is a utility class. Instanciation is forbidden.");
|
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.Executors;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.http.client.utils.URIBuilder;
|
import org.apache.http.client.utils.URIBuilder;
|
||||||
import org.bigbluebutton.api.domain.GuestPolicy;
|
import org.bigbluebutton.api.domain.GuestPolicy;
|
||||||
import org.bigbluebutton.api.domain.Meeting;
|
import org.bigbluebutton.api.domain.Meeting;
|
||||||
@ -81,8 +82,9 @@ import org.bigbluebutton.api2.IBbbWebApiGWApp;
|
|||||||
import org.bigbluebutton.api2.domain.UploadedTrack;
|
import org.bigbluebutton.api2.domain.UploadedTrack;
|
||||||
import org.bigbluebutton.common2.redis.RedisStorageService;
|
import org.bigbluebutton.common2.redis.RedisStorageService;
|
||||||
import org.bigbluebutton.presentation.PresentationUrlDownloadService;
|
import org.bigbluebutton.presentation.PresentationUrlDownloadService;
|
||||||
import org.bigbluebutton.web.services.RegisteredUserCleanupTimerTask;
|
|
||||||
import org.bigbluebutton.web.services.WaitingGuestCleanupTimerTask;
|
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.CallbackUrlService;
|
||||||
import org.bigbluebutton.web.services.callback.MeetingEndedEvent;
|
import org.bigbluebutton.web.services.callback.MeetingEndedEvent;
|
||||||
import org.bigbluebutton.web.services.turn.StunTurnService;
|
import org.bigbluebutton.web.services.turn.StunTurnService;
|
||||||
@ -107,13 +109,17 @@ public class MeetingService implements MessageListener {
|
|||||||
private final ConcurrentMap<String, UserSession> sessions;
|
private final ConcurrentMap<String, UserSession> sessions;
|
||||||
|
|
||||||
private RecordingService recordingService;
|
private RecordingService recordingService;
|
||||||
private RegisteredUserCleanupTimerTask registeredUserCleaner;
|
|
||||||
private WaitingGuestCleanupTimerTask waitingGuestCleaner;
|
private WaitingGuestCleanupTimerTask waitingGuestCleaner;
|
||||||
|
private UserCleanupTimerTask userCleaner;
|
||||||
|
private EnteredUserCleanupTimerTask enteredUserCleaner;
|
||||||
private StunTurnService stunTurnService;
|
private StunTurnService stunTurnService;
|
||||||
private RedisStorageService storeService;
|
private RedisStorageService storeService;
|
||||||
private CallbackUrlService callbackUrlService;
|
private CallbackUrlService callbackUrlService;
|
||||||
private boolean keepEvents;
|
private boolean keepEvents;
|
||||||
|
|
||||||
|
private long usersTimeout;
|
||||||
|
private long enteredUsersTimeout;
|
||||||
|
|
||||||
private ParamsProcessorUtil paramsProcessorUtil;
|
private ParamsProcessorUtil paramsProcessorUtil;
|
||||||
private PresentationUrlDownloadService presDownloadService;
|
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()) {
|
for (AbstractMap.Entry<String, Meeting> entry : this.meetings.entrySet()) {
|
||||||
Long now = System.currentTimeMillis();
|
Long now = System.currentTimeMillis();
|
||||||
Meeting meeting = entry.getValue();
|
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()) {
|
if (!user.hasLeft()) continue;
|
||||||
String registeredUserID = registeredUser.getKey();
|
|
||||||
RegisteredUser registeredUserDate = registeredUser.getValue();
|
|
||||||
|
|
||||||
long elapsedTime = now - registeredUserDate.registeredOn;
|
long elapsedTime = now - user.getLeftOn();
|
||||||
if (elapsedTime >= 60000 && !users.containsKey(registeredUserID)) {
|
if (elapsedTime >= usersTimeout) {
|
||||||
meeting.userUnregistered(registeredUserID);
|
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();
|
String endCallbackUrl = "endCallbackUrl".toLowerCase();
|
||||||
Map<String, String> metadata = m.getMetadata();
|
Map<String, String> metadata = m.getMetadata();
|
||||||
if (!m.isBreakout() && metadata.containsKey(endCallbackUrl)) {
|
if (!m.isBreakout()) {
|
||||||
String callbackUrl = metadata.get(endCallbackUrl);
|
if (metadata.containsKey(endCallbackUrl)) {
|
||||||
try {
|
String callbackUrl = metadata.get(endCallbackUrl);
|
||||||
|
try {
|
||||||
callbackUrl = new URIBuilder(new URI(callbackUrl))
|
callbackUrl = new URIBuilder(new URI(callbackUrl))
|
||||||
.addParameter("recordingmarks", m.haveRecordingMarks() ? "true" : "false")
|
.addParameter("recordingmarks", m.haveRecordingMarks() ? "true" : "false")
|
||||||
.addParameter("meetingID", m.getExternalId()).build().toURL().toString();
|
.addParameter("meetingID", m.getExternalId()).build().toURL().toString();
|
||||||
callbackUrlService.handleMessage(new MeetingEndedEvent(m.getInternalId(), m.getExternalId(), m.getName(), callbackUrl));
|
MeetingEndedEvent event = new MeetingEndedEvent(m.getInternalId(), m.getExternalId(), m.getName(), callbackUrl);
|
||||||
} catch (MalformedURLException e) {
|
processMeetingEndedCallback(event);
|
||||||
log.error("Malformed URL in callback url=[{}]", callbackUrl, e);
|
} catch (Exception e) {
|
||||||
} catch (URISyntaxException e) {
|
log.error("Error in callback url=[{}]", callbackUrl, e);
|
||||||
log.error("URI Syntax error in callback url=[{}]", callbackUrl, e);
|
}
|
||||||
} 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);
|
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) {
|
private void userJoined(UserJoined message) {
|
||||||
Meeting m = getMeeting(message.meetingId);
|
Meeting m = getMeeting(message.meetingId);
|
||||||
if (m != null) {
|
if (m != null) {
|
||||||
@ -867,13 +925,6 @@ public class MeetingService implements MessageListener {
|
|||||||
// the meeting ended.
|
// the meeting ended.
|
||||||
m.setEndTime(System.currentTimeMillis());
|
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 {
|
} else {
|
||||||
if (message.userId.startsWith("v_")) {
|
if (message.userId.startsWith("v_")) {
|
||||||
// A dial-in user joined the meeting. Dial-in users by convention has userId that starts with "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");
|
true, GuestPolicy.ALLOW, "DIAL-IN");
|
||||||
vuser.setVoiceJoined(true);
|
vuser.setVoiceJoined(true);
|
||||||
m.userJoined(vuser);
|
m.userJoined(vuser);
|
||||||
@ -1086,8 +1137,9 @@ public class MeetingService implements MessageListener {
|
|||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
processMessage = false;
|
processMessage = false;
|
||||||
registeredUserCleaner.stop();
|
|
||||||
waitingGuestCleaner.stop();
|
waitingGuestCleaner.stop();
|
||||||
|
userCleaner.stop();
|
||||||
|
enteredUserCleaner.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRecordingService(RecordingService s) {
|
public void setRecordingService(RecordingService s) {
|
||||||
@ -1112,11 +1164,16 @@ public class MeetingService implements MessageListener {
|
|||||||
waitingGuestCleaner.start();
|
waitingGuestCleaner.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRegisteredUserCleanupTimerTask(
|
public void setEnteredUserCleanupTimerTask(EnteredUserCleanupTimerTask c) {
|
||||||
RegisteredUserCleanupTimerTask c) {
|
enteredUserCleaner = c;
|
||||||
registeredUserCleaner = c;
|
enteredUserCleaner.setMeetingService(this);
|
||||||
registeredUserCleaner.setMeetingService(this);
|
enteredUserCleaner.start();
|
||||||
registeredUserCleaner.start();
|
}
|
||||||
|
|
||||||
|
public void setUserCleanupTimerTask(UserCleanupTimerTask c) {
|
||||||
|
userCleaner = c;
|
||||||
|
userCleaner.setMeetingService(this);
|
||||||
|
userCleaner.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStunTurnService(StunTurnService s) {
|
public void setStunTurnService(StunTurnService s) {
|
||||||
@ -1126,4 +1183,12 @@ public class MeetingService implements MessageListener {
|
|||||||
public void setKeepEvents(boolean value) {
|
public void setKeepEvents(boolean value) {
|
||||||
keepEvents = 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 moderatorsJoinViaHTML5Client;
|
||||||
private Boolean attendeesJoinViaHTML5Client;
|
private Boolean attendeesJoinViaHTML5Client;
|
||||||
private Boolean allowRequestsWithoutSession;
|
private Boolean allowRequestsWithoutSession;
|
||||||
|
private Boolean useDefaultAvatar = false;
|
||||||
private String defaultAvatarURL;
|
private String defaultAvatarURL;
|
||||||
private String defaultConfigURL;
|
private String defaultConfigURL;
|
||||||
private String defaultGuestPolicy;
|
private String defaultGuestPolicy;
|
||||||
@ -114,6 +115,7 @@ public class ParamsProcessorUtil {
|
|||||||
private Integer userInactivityThresholdInMinutes = 30;
|
private Integer userInactivityThresholdInMinutes = 30;
|
||||||
private Integer userActivitySignResponseDelayInMinutes = 5;
|
private Integer userActivitySignResponseDelayInMinutes = 5;
|
||||||
private Boolean defaultAllowDuplicateExtUserid = true;
|
private Boolean defaultAllowDuplicateExtUserid = true;
|
||||||
|
private Boolean defaultEndWhenNoModerator = false;
|
||||||
|
|
||||||
private String formatConfNum(String s) {
|
private String formatConfNum(String s) {
|
||||||
if (s.length() > 5) {
|
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;
|
String guestPolicy = defaultGuestPolicy;
|
||||||
if (!StringUtils.isEmpty(params.get(ApiParams.GUEST_POLICY))) {
|
if (!StringUtils.isEmpty(params.get(ApiParams.GUEST_POLICY))) {
|
||||||
guestPolicy = params.get(ApiParams.GUEST_POLICY);
|
guestPolicy = params.get(ApiParams.GUEST_POLICY);
|
||||||
@ -454,6 +465,8 @@ public class ParamsProcessorUtil {
|
|||||||
externalMeetingId = externalHash + "-" + timeStamp;
|
externalMeetingId = externalHash + "-" + timeStamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String avatarURL = useDefaultAvatar ? defaultAvatarURL : "";
|
||||||
|
|
||||||
// Create the meeting with all passed in parameters.
|
// Create the meeting with all passed in parameters.
|
||||||
Meeting meeting = new Meeting.Builder(externalMeetingId,
|
Meeting meeting = new Meeting.Builder(externalMeetingId,
|
||||||
internalMeetingId, createTime).withName(meetingName)
|
internalMeetingId, createTime).withName(meetingName)
|
||||||
@ -464,7 +477,7 @@ public class ParamsProcessorUtil {
|
|||||||
.withBannerText(bannerText).withBannerColor(bannerColor)
|
.withBannerText(bannerText).withBannerColor(bannerColor)
|
||||||
.withTelVoice(telVoice).withWebVoice(webVoice)
|
.withTelVoice(telVoice).withWebVoice(webVoice)
|
||||||
.withDialNumber(dialNumber)
|
.withDialNumber(dialNumber)
|
||||||
.withDefaultAvatarURL(defaultAvatarURL)
|
.withDefaultAvatarURL(avatarURL)
|
||||||
.withAutoStartRecording(autoStartRec)
|
.withAutoStartRecording(autoStartRec)
|
||||||
.withAllowStartStopRecording(allowStartStoptRec)
|
.withAllowStartStopRecording(allowStartStoptRec)
|
||||||
.withWebcamsOnlyForModerator(webcamsOnlyForMod)
|
.withWebcamsOnlyForModerator(webcamsOnlyForMod)
|
||||||
@ -487,6 +500,11 @@ public class ParamsProcessorUtil {
|
|||||||
meeting.setModeratorOnlyMessage(moderatorOnlyMessage);
|
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.setMeetingExpireIfNoUserJoinedInMinutes(meetingExpireIfNoUserJoinedInMinutes);
|
||||||
meeting.setMeetingExpireWhenLastUserLeftInMinutes(meetingExpireWhenLastUserLeftInMinutes);
|
meeting.setMeetingExpireWhenLastUserLeftInMinutes(meetingExpireWhenLastUserLeftInMinutes);
|
||||||
meeting.setUserInactivityInspectTimerInMinutes(userInactivityInspectTimerInMinutes);
|
meeting.setUserInactivityInspectTimerInMinutes(userInactivityInspectTimerInMinutes);
|
||||||
@ -937,6 +955,10 @@ public class ParamsProcessorUtil {
|
|||||||
this.webcamsOnlyForModerator = webcamsOnlyForModerator;
|
this.webcamsOnlyForModerator = webcamsOnlyForModerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setUseDefaultAvatar(Boolean value) {
|
||||||
|
this.useDefaultAvatar = value;
|
||||||
|
}
|
||||||
|
|
||||||
public void setdefaultAvatarURL(String url) {
|
public void setdefaultAvatarURL(String url) {
|
||||||
this.defaultAvatarURL = url;
|
this.defaultAvatarURL = url;
|
||||||
}
|
}
|
||||||
@ -1115,4 +1137,10 @@ public class ParamsProcessorUtil {
|
|||||||
public void setAllowDuplicateExtUserid(Boolean allow) {
|
public void setAllowDuplicateExtUserid(Boolean allow) {
|
||||||
this.defaultAllowDuplicateExtUserid = 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 Map<String, Object> userCustomData;
|
||||||
private final ConcurrentMap<String, User> users;
|
private final ConcurrentMap<String, User> users;
|
||||||
private final ConcurrentMap<String, RegisteredUser> registeredUsers;
|
private final ConcurrentMap<String, RegisteredUser> registeredUsers;
|
||||||
|
private final ConcurrentMap<String, Long> enteredUsers;
|
||||||
private final ConcurrentMap<String, Config> configs;
|
private final ConcurrentMap<String, Config> configs;
|
||||||
private final Boolean isBreakout;
|
private final Boolean isBreakout;
|
||||||
private final List<String> breakoutRooms = new ArrayList<>();
|
private final List<String> breakoutRooms = new ArrayList<>();
|
||||||
@ -92,6 +93,11 @@ public class Meeting {
|
|||||||
|
|
||||||
public final Boolean allowDuplicateExtUserid;
|
public final Boolean allowDuplicateExtUserid;
|
||||||
|
|
||||||
|
private String meetingEndedCallbackURL = "";
|
||||||
|
|
||||||
|
public final Boolean endWhenNoModerator;
|
||||||
|
|
||||||
|
|
||||||
public Meeting(Meeting.Builder builder) {
|
public Meeting(Meeting.Builder builder) {
|
||||||
name = builder.name;
|
name = builder.name;
|
||||||
extMeetingId = builder.externalId;
|
extMeetingId = builder.externalId;
|
||||||
@ -120,12 +126,14 @@ public class Meeting {
|
|||||||
guestPolicy = builder.guestPolicy;
|
guestPolicy = builder.guestPolicy;
|
||||||
breakoutRoomsParams = builder.breakoutRoomsParams;
|
breakoutRoomsParams = builder.breakoutRoomsParams;
|
||||||
lockSettingsParams = builder.lockSettingsParams;
|
lockSettingsParams = builder.lockSettingsParams;
|
||||||
allowDuplicateExtUserid = builder.allowDuplicateExtUserid;
|
allowDuplicateExtUserid = builder.allowDuplicateExtUserid;
|
||||||
|
endWhenNoModerator = builder.endWhenNoModerator;
|
||||||
|
|
||||||
userCustomData = new HashMap<>();
|
userCustomData = new HashMap<>();
|
||||||
|
|
||||||
users = new ConcurrentHashMap<>();
|
users = new ConcurrentHashMap<>();
|
||||||
registeredUsers = new ConcurrentHashMap<>();
|
registeredUsers = new ConcurrentHashMap<>();
|
||||||
|
enteredUsers = new ConcurrentHashMap<>();;
|
||||||
|
|
||||||
configs = new ConcurrentHashMap<>();
|
configs = new ConcurrentHashMap<>();
|
||||||
}
|
}
|
||||||
@ -448,12 +456,28 @@ public class Meeting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void userJoined(User user) {
|
public void userJoined(User user) {
|
||||||
userHasJoined = true;
|
User u = getUserById(user.getInternalUserId());
|
||||||
this.users.put(user.getInternalUserId(), user);
|
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){
|
public User userLeft(String userId) {
|
||||||
return users.remove(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){
|
public User getUserById(String id){
|
||||||
@ -563,6 +587,14 @@ public class Meeting {
|
|||||||
this.userActivitySignResponseDelayInMinutes = userActivitySignResponseDelayInMinutes;
|
this.userActivitySignResponseDelayInMinutes = userActivitySignResponseDelayInMinutes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getMeetingEndedCallbackURL() {
|
||||||
|
return meetingEndedCallbackURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMeetingEndedCallbackURL(String meetingEndedCallbackURL) {
|
||||||
|
this.meetingEndedCallbackURL = meetingEndedCallbackURL;
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, Object> getUserCustomData(String userID){
|
public Map<String, Object> getUserCustomData(String userID){
|
||||||
return (Map<String, Object>) userCustomData.get(userID);
|
return (Map<String, Object>) userCustomData.get(userID);
|
||||||
}
|
}
|
||||||
@ -579,6 +611,29 @@ public class Meeting {
|
|||||||
return registeredUsers;
|
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
|
* Meeting Builder
|
||||||
*
|
*
|
||||||
@ -612,6 +667,7 @@ public class Meeting {
|
|||||||
private BreakoutRoomsParams breakoutRoomsParams;
|
private BreakoutRoomsParams breakoutRoomsParams;
|
||||||
private LockSettingsParams lockSettingsParams;
|
private LockSettingsParams lockSettingsParams;
|
||||||
private Boolean allowDuplicateExtUserid;
|
private Boolean allowDuplicateExtUserid;
|
||||||
|
private Boolean endWhenNoModerator;
|
||||||
|
|
||||||
public Builder(String externalId, String internalId, long createTime) {
|
public Builder(String externalId, String internalId, long createTime) {
|
||||||
this.externalId = externalId;
|
this.externalId = externalId;
|
||||||
@ -743,6 +799,11 @@ public class Meeting {
|
|||||||
this.allowDuplicateExtUserid = allowDuplicateExtUserid;
|
this.allowDuplicateExtUserid = allowDuplicateExtUserid;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder withEndWhenNoModerator(Boolean endWhenNoModerator) {
|
||||||
|
this.endWhenNoModerator = endWhenNoModerator;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Meeting build() {
|
public Meeting build() {
|
||||||
return new Meeting(this);
|
return new Meeting(this);
|
||||||
|
@ -38,6 +38,7 @@ public class User {
|
|||||||
private Boolean voiceJoined = false;
|
private Boolean voiceJoined = false;
|
||||||
private String clientType;
|
private String clientType;
|
||||||
private List<String> streams;
|
private List<String> streams;
|
||||||
|
private Long leftOn = null;
|
||||||
|
|
||||||
public User(String internalUserId,
|
public User(String internalUserId,
|
||||||
String externalUserId,
|
String externalUserId,
|
||||||
@ -90,6 +91,22 @@ public class User {
|
|||||||
return this.guestStatus;
|
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() {
|
public String getFullname() {
|
||||||
return fullname;
|
return fullname;
|
||||||
}
|
}
|
||||||
|
@ -1,231 +1,224 @@
|
|||||||
/**
|
/**
|
||||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||||
*
|
*
|
||||||
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
|
* 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
|
* 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
|
* 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
|
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||||
* version.
|
* version.
|
||||||
*
|
*
|
||||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
* 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
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
* 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
|
* You should have received a copy of the GNU Lesser General Public License along
|
||||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
package org.bigbluebutton.presentation.imp;
|
||||||
package org.bigbluebutton.presentation.imp;
|
import java.io.File;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.io.File;
|
import java.util.ArrayList;
|
||||||
import java.lang.reflect.Method;
|
import java.util.HashMap;
|
||||||
import java.util.ArrayList;
|
import java.util.Map;
|
||||||
import java.util.HashMap;
|
import org.bigbluebutton.presentation.ConversionMessageConstants;
|
||||||
import java.util.Map;
|
import org.bigbluebutton.presentation.SupportedFileTypes;
|
||||||
|
import org.bigbluebutton.presentation.UploadedPresentation;
|
||||||
import org.bigbluebutton.presentation.ConversionMessageConstants;
|
import org.jodconverter.core.office.OfficeException;
|
||||||
import org.bigbluebutton.presentation.SupportedFileTypes;
|
import org.jodconverter.core.office.OfficeUtils;
|
||||||
import org.bigbluebutton.presentation.UploadedPresentation;
|
import org.jodconverter.local.LocalConverter;
|
||||||
import org.jodconverter.core.office.OfficeException;
|
import org.jodconverter.local.office.ExternalOfficeManager;
|
||||||
import org.jodconverter.core.office.OfficeUtils;
|
import org.slf4j.Logger;
|
||||||
import org.jodconverter.local.LocalConverter;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.jodconverter.local.office.ExternalOfficeManager;
|
import com.sun.star.document.UpdateDocMode;
|
||||||
import org.slf4j.Logger;
|
import com.google.gson.Gson;
|
||||||
import org.slf4j.LoggerFactory;
|
public class OfficeToPdfConversionService {
|
||||||
|
private static Logger log = LoggerFactory.getLogger(OfficeToPdfConversionService.class);
|
||||||
import com.google.gson.Gson;
|
private OfficeDocumentValidator2 officeDocumentValidator;
|
||||||
|
private final ArrayList<ExternalOfficeManager> officeManagers;
|
||||||
public class OfficeToPdfConversionService {
|
private ExternalOfficeManager currentManager = null;
|
||||||
private static Logger log = LoggerFactory.getLogger(OfficeToPdfConversionService.class);
|
private boolean skipOfficePrecheck = false;
|
||||||
|
private int sofficeBasePort = 0;
|
||||||
private OfficeDocumentValidator2 officeDocumentValidator;
|
private int sofficeManagers = 0;
|
||||||
private final ArrayList<ExternalOfficeManager> officeManagers;
|
private String sofficeWorkingDirBase = null;
|
||||||
private ExternalOfficeManager currentManager = null;
|
public OfficeToPdfConversionService() throws OfficeException {
|
||||||
private boolean skipOfficePrecheck = false;
|
officeManagers = new ArrayList<>();
|
||||||
private int sofficeBasePort = 0;
|
}
|
||||||
private int sofficeManagers = 0;
|
/*
|
||||||
private String sofficeWorkingDirBase = null;
|
* Convert the Office document to PDF. If successful, update
|
||||||
|
* UploadPresentation.uploadedFile with the new PDF out and
|
||||||
public OfficeToPdfConversionService() throws OfficeException {
|
* UploadPresentation.lastStepSuccessful to TRUE.
|
||||||
officeManagers = new ArrayList<>();
|
*/
|
||||||
}
|
public UploadedPresentation convertOfficeToPdf(UploadedPresentation pres) {
|
||||||
|
initialize(pres);
|
||||||
/*
|
if (SupportedFileTypes.isOfficeFile(pres.getFileType())) {
|
||||||
* Convert the Office document to PDF. If successful, update
|
// Check if we need to precheck office document
|
||||||
* UploadPresentation.uploadedFile with the new PDF out and
|
if (!skipOfficePrecheck && officeDocumentValidator.isValid(pres)) {
|
||||||
* UploadPresentation.lastStepSuccessful to TRUE.
|
Map<String, Object> logData = new HashMap<>();
|
||||||
*/
|
logData.put("meetingId", pres.getMeetingId());
|
||||||
public UploadedPresentation convertOfficeToPdf(UploadedPresentation pres) {
|
logData.put("presId", pres.getId());
|
||||||
initialize(pres);
|
logData.put("filename", pres.getName());
|
||||||
if (SupportedFileTypes.isOfficeFile(pres.getFileType())) {
|
logData.put("logCode", "problems_office_to_pdf_validation");
|
||||||
// Check if we need to precheck office document
|
logData.put("message", "Problems detected prior to converting the file to PDF.");
|
||||||
if (!skipOfficePrecheck && officeDocumentValidator.isValid(pres)) {
|
Gson gson = new Gson();
|
||||||
Map<String, Object> logData = new HashMap<>();
|
String logStr = gson.toJson(logData);
|
||||||
logData.put("meetingId", pres.getMeetingId());
|
log.warn(" --analytics-- data={}", logStr);
|
||||||
logData.put("presId", pres.getId());
|
pres.setConversionStatus(ConversionMessageConstants.OFFICE_DOC_CONVERSION_INVALID_KEY);
|
||||||
logData.put("filename", pres.getName());
|
return pres;
|
||||||
logData.put("logCode", "problems_office_to_pdf_validation");
|
}
|
||||||
logData.put("message", "Problems detected prior to converting the file to PDF.");
|
File pdfOutput = setupOutputPdfFile(pres);
|
||||||
Gson gson = new Gson();
|
if (convertOfficeDocToPdf(pres, pdfOutput)) {
|
||||||
String logStr = gson.toJson(logData);
|
Map<String, Object> logData = new HashMap<>();
|
||||||
log.warn(" --analytics-- data={}", logStr);
|
logData.put("meetingId", pres.getMeetingId());
|
||||||
|
logData.put("presId", pres.getId());
|
||||||
pres.setConversionStatus(ConversionMessageConstants.OFFICE_DOC_CONVERSION_INVALID_KEY);
|
logData.put("filename", pres.getName());
|
||||||
return pres;
|
logData.put("logCode", "office_to_pdf_success");
|
||||||
}
|
logData.put("message", "Successfully converted office file to pdf.");
|
||||||
File pdfOutput = setupOutputPdfFile(pres);
|
Gson gson = new Gson();
|
||||||
if (convertOfficeDocToPdf(pres, pdfOutput)) {
|
String logStr = gson.toJson(logData);
|
||||||
Map<String, Object> logData = new HashMap<>();
|
log.info(" --analytics-- data={}", logStr);
|
||||||
logData.put("meetingId", pres.getMeetingId());
|
makePdfTheUploadedFileAndSetStepAsSuccess(pres, pdfOutput);
|
||||||
logData.put("presId", pres.getId());
|
} else {
|
||||||
logData.put("filename", pres.getName());
|
Map<String, Object> logData = new HashMap<>();
|
||||||
logData.put("logCode", "office_to_pdf_success");
|
logData.put("meetingId", pres.getMeetingId());
|
||||||
logData.put("message", "Successfully converted office file to pdf.");
|
logData.put("presId", pres.getId());
|
||||||
Gson gson = new Gson();
|
logData.put("filename", pres.getName());
|
||||||
String logStr = gson.toJson(logData);
|
logData.put("logCode", "office_to_pdf_failed");
|
||||||
log.info(" --analytics-- data={}", logStr);
|
logData.put("message", "Failed to convert " + pres.getUploadedFile().getAbsolutePath() + " to Pdf.");
|
||||||
|
Gson gson = new Gson();
|
||||||
makePdfTheUploadedFileAndSetStepAsSuccess(pres, pdfOutput);
|
String logStr = gson.toJson(logData);
|
||||||
} else {
|
log.warn(" --analytics-- data={}", logStr);
|
||||||
Map<String, Object> logData = new HashMap<>();
|
pres.setConversionStatus(ConversionMessageConstants.OFFICE_DOC_CONVERSION_FAILED_KEY);
|
||||||
logData.put("meetingId", pres.getMeetingId());
|
return pres;
|
||||||
logData.put("presId", pres.getId());
|
}
|
||||||
logData.put("filename", pres.getName());
|
}
|
||||||
logData.put("logCode", "office_to_pdf_failed");
|
return pres;
|
||||||
logData.put("message", "Failed to convert " + pres.getUploadedFile().getAbsolutePath() + " to Pdf.");
|
}
|
||||||
Gson gson = new Gson();
|
public void initialize(UploadedPresentation pres) {
|
||||||
String logStr = gson.toJson(logData);
|
pres.setConversionStatus(ConversionMessageConstants.OFFICE_DOC_CONVERSION_FAILED_KEY);
|
||||||
log.warn(" --analytics-- data={}", logStr);
|
}
|
||||||
pres.setConversionStatus(ConversionMessageConstants.OFFICE_DOC_CONVERSION_FAILED_KEY);
|
private File setupOutputPdfFile(UploadedPresentation pres) {
|
||||||
return pres;
|
File presentationFile = pres.getUploadedFile();
|
||||||
}
|
String filenameWithoutExt = presentationFile.getAbsolutePath().substring(0,
|
||||||
}
|
presentationFile.getAbsolutePath().lastIndexOf('.'));
|
||||||
return pres;
|
return new File(filenameWithoutExt + ".pdf");
|
||||||
}
|
}
|
||||||
|
private boolean convertOfficeDocToPdf(UploadedPresentation pres,
|
||||||
public void initialize(UploadedPresentation pres) {
|
File pdfOutput) {
|
||||||
pres.setConversionStatus(ConversionMessageConstants.OFFICE_DOC_CONVERSION_FAILED_KEY);
|
boolean success = false;
|
||||||
}
|
int attempts = 0;
|
||||||
|
while(!success) {
|
||||||
private File setupOutputPdfFile(UploadedPresentation pres) {
|
final Map<String, Object> loadProperties = new HashMap<>();
|
||||||
File presentationFile = pres.getUploadedFile();
|
loadProperties.put("Hidden", true);
|
||||||
String filenameWithoutExt = presentationFile.getAbsolutePath().substring(0,
|
loadProperties.put("ReadOnly", true);
|
||||||
presentationFile.getAbsolutePath().lastIndexOf('.'));
|
loadProperties.put("UpdateDocMode", UpdateDocMode.NO_UPDATE);
|
||||||
return new File(filenameWithoutExt + ".pdf");
|
LocalConverter documentConverter = LocalConverter
|
||||||
}
|
.builder()
|
||||||
|
.officeManager(currentManager)
|
||||||
private boolean convertOfficeDocToPdf(UploadedPresentation pres,
|
.loadProperties(loadProperties)
|
||||||
File pdfOutput) {
|
.filterChain(new OfficeDocumentConversionFilter())
|
||||||
boolean success = false;
|
.build();
|
||||||
int attempts = 0;
|
|
||||||
|
success = Office2PdfPageConverter.convert(pres.getUploadedFile(), pdfOutput, 0, pres, documentConverter);
|
||||||
while(!success) {
|
|
||||||
LocalConverter documentConverter = LocalConverter
|
if(!success) {
|
||||||
.builder()
|
// In case of failure, try with other open Office Manager
|
||||||
.officeManager(currentManager)
|
|
||||||
.filterChain(new OfficeDocumentConversionFilter())
|
if(++attempts != officeManagers.size()) {
|
||||||
.build();
|
// Go to next Office Manager ( if the last retry with the first one )
|
||||||
|
int currentManagerIndex = officeManagers.indexOf(currentManager);
|
||||||
success = Office2PdfPageConverter.convert(pres.getUploadedFile(), pdfOutput, 0, pres, documentConverter);
|
|
||||||
|
boolean isLastManager = ( currentManagerIndex == officeManagers.size()-1 );
|
||||||
if(!success) {
|
if(isLastManager) {
|
||||||
// In case of failure, try with other open Office Manager
|
currentManager = officeManagers.get(0);
|
||||||
|
} else {
|
||||||
if(++attempts != officeManagers.size()) {
|
currentManager = officeManagers.get(currentManagerIndex+1);
|
||||||
// Go to next Office Manager ( if the last retry with the first one )
|
}
|
||||||
int currentManagerIndex = officeManagers.indexOf(currentManager);
|
} else {
|
||||||
|
// We tried to use all our office managers and it's still failing
|
||||||
boolean isLastManager = ( currentManagerIndex == officeManagers.size()-1 );
|
break;
|
||||||
if(isLastManager) {
|
}
|
||||||
currentManager = officeManagers.get(0);
|
}
|
||||||
} else {
|
}
|
||||||
currentManager = officeManagers.get(currentManagerIndex+1);
|
|
||||||
}
|
return success;
|
||||||
} else {
|
}
|
||||||
// We tried to use all our office managers and it's still failing
|
|
||||||
break;
|
private void makePdfTheUploadedFileAndSetStepAsSuccess(UploadedPresentation pres, File pdf) {
|
||||||
}
|
pres.setUploadedFile(pdf);
|
||||||
}
|
pres.setConversionStatus(ConversionMessageConstants.OFFICE_DOC_CONVERSION_SUCCESS_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
public void setOfficeDocumentValidator(OfficeDocumentValidator2 v) {
|
||||||
}
|
officeDocumentValidator = v;
|
||||||
|
}
|
||||||
private void makePdfTheUploadedFileAndSetStepAsSuccess(UploadedPresentation pres, File pdf) {
|
|
||||||
pres.setUploadedFile(pdf);
|
public void setSkipOfficePrecheck(boolean skipOfficePrecheck) {
|
||||||
pres.setConversionStatus(ConversionMessageConstants.OFFICE_DOC_CONVERSION_SUCCESS_KEY);
|
this.skipOfficePrecheck = skipOfficePrecheck;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOfficeDocumentValidator(OfficeDocumentValidator2 v) {
|
public void setSofficeBasePort(int sofficeBasePort) {
|
||||||
officeDocumentValidator = v;
|
this.sofficeBasePort = sofficeBasePort;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSkipOfficePrecheck(boolean skipOfficePrecheck) {
|
public void setSofficeManagers(int sofficeServiceManagers) {
|
||||||
this.skipOfficePrecheck = skipOfficePrecheck;
|
this.sofficeManagers = sofficeServiceManagers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSofficeBasePort(int sofficeBasePort) {
|
public void setSofficeWorkingDirBase(String sofficeWorkingDirBase) {
|
||||||
this.sofficeBasePort = sofficeBasePort;
|
this.sofficeWorkingDirBase = sofficeWorkingDirBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSofficeManagers(int sofficeServiceManagers) {
|
public void start() {
|
||||||
this.sofficeManagers = sofficeServiceManagers;
|
log.info("Starting LibreOffice pool with " + sofficeManagers + " managers, starting from port " + sofficeBasePort);
|
||||||
}
|
|
||||||
|
for(int managerIndex = 0; managerIndex < sofficeManagers; managerIndex ++) {
|
||||||
public void setSofficeWorkingDirBase(String sofficeWorkingDirBase) {
|
Integer instanceNumber = managerIndex + 1; // starts at 1
|
||||||
this.sofficeWorkingDirBase = sofficeWorkingDirBase;
|
|
||||||
}
|
try {
|
||||||
|
final File workingDir = new File(sofficeWorkingDirBase + String.format("%02d", instanceNumber));
|
||||||
public void start() {
|
|
||||||
log.info("Starting LibreOffice pool with " + sofficeManagers + " managers, starting from port " + sofficeBasePort);
|
if(!workingDir.exists()) {
|
||||||
|
workingDir.mkdir();
|
||||||
for(int managerIndex = 0; managerIndex < sofficeManagers; managerIndex ++) {
|
}
|
||||||
Integer instanceNumber = managerIndex + 1; // starts at 1
|
|
||||||
|
ExternalOfficeManager officeManager = ExternalOfficeManager
|
||||||
try {
|
.builder()
|
||||||
final File workingDir = new File(sofficeWorkingDirBase + String.format("%02d", instanceNumber));
|
.connectTimeout(2000L)
|
||||||
|
.retryInterval(500L)
|
||||||
if(!workingDir.exists()) {
|
.portNumber(sofficeBasePort + managerIndex)
|
||||||
workingDir.mkdir();
|
.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();
|
||||||
ExternalOfficeManager officeManager = ExternalOfficeManager
|
|
||||||
.builder()
|
// Workaround for jodconverter not calling makeTempDir when connectOnStart=false (issue 211)
|
||||||
.connectTimeout(2000L)
|
Method method = officeManager.getClass().getSuperclass().getDeclaredMethod("makeTempDir");
|
||||||
.retryInterval(500L)
|
method.setAccessible(true);
|
||||||
.portNumber(sofficeBasePort + managerIndex)
|
method.invoke(officeManager);
|
||||||
.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 )
|
// End of workaround for jodconverter not calling makeTempDir
|
||||||
.workingDir(workingDir)
|
|
||||||
.build();
|
officeManager.start();
|
||||||
|
officeManagers.add(officeManager);
|
||||||
// Workaround for jodconverter not calling makeTempDir when connectOnStart=false (issue 211)
|
} catch (Exception e) {
|
||||||
Method method = officeManager.getClass().getSuperclass().getDeclaredMethod("makeTempDir");
|
log.error("Could not start Office Manager " + instanceNumber + ". Details: " + e.getMessage());
|
||||||
method.setAccessible(true);
|
}
|
||||||
method.invoke(officeManager);
|
}
|
||||||
// End of workaround for jodconverter not calling makeTempDir
|
|
||||||
|
if (officeManagers.size() == 0) {
|
||||||
officeManager.start();
|
log.error("No office managers could be started");
|
||||||
officeManagers.add(officeManager);
|
return;
|
||||||
} catch (Exception e) {
|
}
|
||||||
log.error("Could not start Office Manager " + instanceNumber + ". Details: " + e.getMessage());
|
|
||||||
}
|
currentManager = officeManagers.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (officeManagers.size() == 0) {
|
public void stop() {
|
||||||
log.error("No office managers could be started");
|
try {
|
||||||
return;
|
officeManagers.forEach(officeManager -> officeManager.stop() );
|
||||||
}
|
} catch (Exception e) {
|
||||||
|
log.error("Could not stop Office Manager", e);
|
||||||
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/
|
* 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
|
* 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
|
* 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;
|
import org.bigbluebutton.api.MeetingService;
|
||||||
|
|
||||||
public class RegisteredUserCleanupTimerTask {
|
public class EnteredUserCleanupTimerTask {
|
||||||
|
|
||||||
private MeetingService service;
|
private MeetingService service;
|
||||||
private ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);
|
private ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);
|
||||||
private long runEvery = 60000;
|
private long runEvery = 30000;
|
||||||
|
|
||||||
public void setMeetingService(MeetingService svc) {
|
public void setMeetingService(MeetingService svc) {
|
||||||
this.service = svc;
|
this.service = svc;
|
||||||
@ -50,7 +50,7 @@ public class RegisteredUserCleanupTimerTask {
|
|||||||
private class CleanupTask implements Runnable {
|
private class CleanupTask implements Runnable {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
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.
|
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
|
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
|
# which (if exists) will be run by `bbb-conf --setip` and `bbb-conf --restart` before restarting
|
||||||
# BigBlueButton.
|
# BigBlueButton.
|
||||||
#
|
#
|
||||||
# The purpose of apply-config.sh is to make it easy for you apply defaults to BigBlueButton server that get applied after
|
# The purpose of apply-config.sh is to make it easy to apply your configuration changes to a BigBlueButton server
|
||||||
# each package update (since the last step in doing an upate is to run `bbb-conf --setip`.
|
# 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() {
|
enableUFWRules() {
|
||||||
echo " - Enable Firewall and opening 22/tcp, 80/tcp, 443/tcp and 16384:32768/udp"
|
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() {
|
notCalled() {
|
||||||
#
|
#
|
||||||
# This function is not called.
|
# This function is not called.
|
||||||
@ -112,6 +241,9 @@ source /etc/bigbluebutton/bbb-conf/apply-lib.sh
|
|||||||
#enableHTML5ClientLog
|
#enableHTML5ClientLog
|
||||||
#enableUFWRules
|
#enableUFWRules
|
||||||
|
|
||||||
|
#enableHTML5CameraQualityThresholds
|
||||||
|
#enableHTML5WebcamPagination
|
||||||
|
|
||||||
HERE
|
HERE
|
||||||
chmod +x /etc/bigbluebutton/bbb-conf/apply-config.sh
|
chmod +x /etc/bigbluebutton/bbb-conf/apply-config.sh
|
||||||
## Stop Copying HERE
|
## Stop Copying HERE
|
||||||
|
@ -1996,16 +1996,17 @@ if [ -n "$HOST" ]; then
|
|||||||
#fi
|
#fi
|
||||||
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
|
if [ "$ESL_PASSWORD" == "ClueCon" ]; then
|
||||||
ESL_PASSWORD=$(openssl rand -hex 8)
|
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
|
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 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 ..."
|
echo "Restarting the BigBlueButton $BIGBLUEBUTTON_RELEASE ..."
|
||||||
|
@ -81,7 +81,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<script src="compatibility/adapter.js?v=VERSION" language="javascript"></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/sip.js?v=VERSION" language="javascript"></script>
|
||||||
<script src="compatibility/kurento-extension.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>
|
<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">
|
<body style="background-color: #06172A">
|
||||||
<div id="app" role="document"></div>
|
<div id="app" role="document"></div>
|
||||||
<span id="destination"></span>
|
<span id="destination"></span>
|
||||||
<audio id="remote-media" autoPlay="autoplay">
|
<audio id="remote-media" autoplay>
|
||||||
<track kind="captions" /> {/* These captions are brought to you by eslint */}
|
|
||||||
</audio>
|
</audio>
|
||||||
</body>
|
</body>
|
||||||
|
@ -2,8 +2,6 @@ import { check } from 'meteor/check';
|
|||||||
|
|
||||||
const ANNOTATION_TYPE_TEXT = 'text';
|
const ANNOTATION_TYPE_TEXT = 'text';
|
||||||
const ANNOTATION_TYPE_PENCIL = 'pencil';
|
const ANNOTATION_TYPE_PENCIL = 'pencil';
|
||||||
const DEFAULT_TEXT_WIDTH = 30;
|
|
||||||
const DEFAULT_TEXT_HEIGHT = 20;
|
|
||||||
|
|
||||||
// line, triangle, ellipse, rectangle
|
// line, triangle, ellipse, rectangle
|
||||||
function handleCommonAnnotation(meetingId, whiteboardId, userId, annotation) {
|
function handleCommonAnnotation(meetingId, whiteboardId, userId, annotation) {
|
||||||
@ -41,23 +39,6 @@ function handleTextUpdate(meetingId, whiteboardId, userId, annotation) {
|
|||||||
id, status, annotationType, annotationInfo, wbId, position,
|
id, status, annotationType, annotationInfo, wbId, position,
|
||||||
} = annotation;
|
} = 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 = {
|
const selector = {
|
||||||
meetingId,
|
meetingId,
|
||||||
id,
|
id,
|
||||||
|
@ -60,14 +60,22 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
|
|||||||
let iceServers = [];
|
let iceServers = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
logger.info({
|
||||||
|
logCode: 'sfuaudiobridge_stunturn_fetch_start',
|
||||||
|
extraInfo: { iceServers },
|
||||||
|
}, 'SFU audio bridge starting STUN/TURN fetch');
|
||||||
|
|
||||||
iceServers = await fetchWebRTCMappedStunTurnServers(this.user.sessionToken);
|
iceServers = await fetchWebRTCMappedStunTurnServers(this.user.sessionToken);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ logCode: 'sfuaudiobridge_stunturn_fetch_failed' },
|
logger.error({ logCode: 'sfuaudiobridge_stunturn_fetch_failed' },
|
||||||
'SFU audio bridge failed to fetch STUN/TURN info, using default servers');
|
'SFU audio bridge failed to fetch STUN/TURN info, using default servers');
|
||||||
iceServers = getMappedFallbackStun();
|
iceServers = getMappedFallbackStun();
|
||||||
} finally {
|
} finally {
|
||||||
logger.debug({ logCode: 'sfuaudiobridge_stunturn_fetch_sucess', extraInfo: { iceServers } },
|
logger.info({
|
||||||
'SFU audio bridge got STUN/TURN servers');
|
logCode: 'sfuaudiobridge_stunturn_fetch_sucess',
|
||||||
|
extraInfo: { iceServers },
|
||||||
|
}, 'SFU audio bridge got STUN/TURN servers');
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
wsUrl: Auth.authenticateURL(SFU_URL),
|
wsUrl: Auth.authenticateURL(SFU_URL),
|
||||||
userName: this.user.name,
|
userName: this.user.name,
|
||||||
@ -131,12 +139,25 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
|
|||||||
|
|
||||||
this.hasSuccessfullyStarted = true;
|
this.hasSuccessfullyStarted = true;
|
||||||
if (webRtcPeer) {
|
if (webRtcPeer) {
|
||||||
|
logger.info({
|
||||||
|
logCode: 'sfuaudiobridge_audio_negotiation_success',
|
||||||
|
}, 'SFU audio bridge negotiated audio with success');
|
||||||
|
|
||||||
const stream = webRtcPeer.getRemoteStream();
|
const stream = webRtcPeer.getRemoteStream();
|
||||||
|
|
||||||
audioTag.pause();
|
audioTag.pause();
|
||||||
audioTag.srcObject = stream;
|
audioTag.srcObject = stream;
|
||||||
audioTag.muted = false;
|
audioTag.muted = false;
|
||||||
|
logger.info({
|
||||||
|
logCode: 'sfuaudiobridge_audio_ready_to_play',
|
||||||
|
}, 'SFU audio bridge is ready to play');
|
||||||
|
|
||||||
playElement();
|
playElement();
|
||||||
} else {
|
} else {
|
||||||
|
logger.info({
|
||||||
|
logCode: 'sfuaudiobridge_audio_negotiation_failed',
|
||||||
|
}, 'SFU audio bridge failed to negotiate audio');
|
||||||
|
|
||||||
this.callback({
|
this.callback({
|
||||||
status: this.baseCallStates.failed,
|
status: this.baseCallStates.failed,
|
||||||
error: this.baseErrorCodes.CONNECTION_ERROR,
|
error: this.baseErrorCodes.CONNECTION_ERROR,
|
||||||
@ -218,6 +239,9 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
|
|||||||
return reject(new Error('Invalid bridge option'));
|
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(
|
window.kurentoJoinAudio(
|
||||||
MEDIA_TAG,
|
MEDIA_TAG,
|
||||||
this.voiceBridge,
|
this.voiceBridge,
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import browser from 'browser-detect';
|
import browser from 'browser-detect';
|
||||||
import BaseAudioBridge from './base';
|
import BaseAudioBridge from './base';
|
||||||
import logger from '/imports/startup/client/logger';
|
import logger from '/imports/startup/client/logger';
|
||||||
import { fetchStunTurnServers, getFallbackStun } from '/imports/utils/fetchStunTurnServers';
|
import {
|
||||||
|
fetchWebRTCMappedStunTurnServers,
|
||||||
|
getMappedFallbackStun,
|
||||||
|
} from '/imports/utils/fetchStunTurnServers';
|
||||||
import {
|
import {
|
||||||
isUnifiedPlan,
|
isUnifiedPlan,
|
||||||
toUnifiedPlan,
|
toUnifiedPlan,
|
||||||
@ -20,13 +23,13 @@ const MEDIA_TAG = MEDIA.mediaTag;
|
|||||||
const CALL_TRANSFER_TIMEOUT = MEDIA.callTransferTimeout;
|
const CALL_TRANSFER_TIMEOUT = MEDIA.callTransferTimeout;
|
||||||
const CALL_HANGUP_TIMEOUT = MEDIA.callHangupTimeout;
|
const CALL_HANGUP_TIMEOUT = MEDIA.callHangupTimeout;
|
||||||
const CALL_HANGUP_MAX_RETRIES = MEDIA.callHangupMaximumRetries;
|
const CALL_HANGUP_MAX_RETRIES = MEDIA.callHangupMaximumRetries;
|
||||||
const RELAY_ONLY_ON_RECONNECT = MEDIA.relayOnlyOnReconnect;
|
|
||||||
const IPV4_FALLBACK_DOMAIN = Meteor.settings.public.app.ipv4FallbackDomain;
|
const IPV4_FALLBACK_DOMAIN = Meteor.settings.public.app.ipv4FallbackDomain;
|
||||||
const ICE_NEGOTIATION_FAILED = ['iceConnectionFailed'];
|
|
||||||
const CALL_CONNECT_TIMEOUT = 20000;
|
const CALL_CONNECT_TIMEOUT = 20000;
|
||||||
const ICE_NEGOTIATION_TIMEOUT = 20000;
|
const ICE_NEGOTIATION_TIMEOUT = 20000;
|
||||||
const AUDIO_SESSION_NUM_KEY = 'AudioSessionNumber';
|
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 = () => {
|
const getAudioSessionNumber = () => {
|
||||||
let currItem = parseInt(sessionStorage.getItem(AUDIO_SESSION_NUM_KEY), 10);
|
let currItem = parseInt(sessionStorage.getItem(AUDIO_SESSION_NUM_KEY), 10);
|
||||||
@ -39,6 +42,24 @@ const getAudioSessionNumber = () => {
|
|||||||
return currItem;
|
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 {
|
class SIPSession {
|
||||||
constructor(user, userData, protocol, hostname,
|
constructor(user, userData, protocol, hostname,
|
||||||
baseCallStates, baseErrorCodes, reconnectAttempt) {
|
baseCallStates, baseErrorCodes, reconnectAttempt) {
|
||||||
@ -49,9 +70,15 @@ class SIPSession {
|
|||||||
this.baseCallStates = baseCallStates;
|
this.baseCallStates = baseCallStates;
|
||||||
this.baseErrorCodes = baseErrorCodes;
|
this.baseErrorCodes = baseErrorCodes;
|
||||||
this.reconnectAttempt = reconnectAttempt;
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const callExtension = extension ? `${extension}${this.userData.voiceBridge}` : this.userData.voiceBridge;
|
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
|
// If there's an extension passed it means that we're joining the echo test first
|
||||||
this.inEchoTest = !!extension;
|
this.inEchoTest = !!extension;
|
||||||
|
|
||||||
return this.doCall({ callExtension, isListenOnly, inputStream })
|
return this.doCall({ callExtension, isListenOnly, inputDeviceId })
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
reject(reason);
|
reject(reason);
|
||||||
});
|
});
|
||||||
@ -87,7 +114,7 @@ class SIPSession {
|
|||||||
|
|
||||||
async getIceServers(sessionToken) {
|
async getIceServers(sessionToken) {
|
||||||
try {
|
try {
|
||||||
const iceServers = await fetchStunTurnServers(sessionToken);
|
const iceServers = await fetchWebRTCMappedStunTurnServers(sessionToken);
|
||||||
return iceServers;
|
return iceServers;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({
|
logger.error({
|
||||||
@ -98,15 +125,18 @@ class SIPSession {
|
|||||||
callerIdName: this.user.callerIdName,
|
callerIdName: this.user.callerIdName,
|
||||||
},
|
},
|
||||||
}, 'Full audio bridge failed to fetch STUN/TURN info');
|
}, 'Full audio bridge failed to fetch STUN/TURN info');
|
||||||
return getFallbackStun();
|
return getMappedFallbackStun();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
doCall(options) {
|
doCall(options) {
|
||||||
const {
|
const {
|
||||||
isListenOnly,
|
isListenOnly,
|
||||||
|
inputDeviceId,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
this.inputDeviceId = inputDeviceId;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
userId,
|
userId,
|
||||||
name,
|
name,
|
||||||
@ -124,8 +154,7 @@ class SIPSession {
|
|||||||
|
|
||||||
return this.getIceServers(sessionToken)
|
return this.getIceServers(sessionToken)
|
||||||
.then(this.createUserAgent.bind(this))
|
.then(this.createUserAgent.bind(this))
|
||||||
.then(this.inviteUserAgent.bind(this))
|
.then(this.inviteUserAgent.bind(this));
|
||||||
.then(this.setupEventHandlers.bind(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transferCall(onTransferSuccess) {
|
transferCall(onTransferSuccess) {
|
||||||
@ -149,7 +178,18 @@ class SIPSession {
|
|||||||
}, CALL_TRANSFER_TIMEOUT);
|
}, CALL_TRANSFER_TIMEOUT);
|
||||||
|
|
||||||
// This is is the call transfer code ask @chadpilkey
|
// 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) => {
|
Tracker.autorun((c) => {
|
||||||
trackerControl = 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() {
|
exitAudio() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let hangupRetries = 0;
|
let hangupRetries = 0;
|
||||||
let hangup = false;
|
this._hangupFlag = false;
|
||||||
|
|
||||||
this.userRequestedHangup = true;
|
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 = () => {
|
const tryHangup = () => {
|
||||||
if ((this.currentSession && this.currentSession.endTime)
|
if (this._hangupFlag) {
|
||||||
|| (this.userAgent && this.userAgent.status === SIP.UA.C.STATUS_USER_CLOSED)) {
|
resolve();
|
||||||
hangup = true;
|
}
|
||||||
|
|
||||||
|
if ((this.currentSession
|
||||||
|
&& (this.currentSession.state === SIP.SessionState.Terminated))
|
||||||
|
|| (this.userAgent && (!this.userAgent.isConnected()))) {
|
||||||
|
this._hangupFlag = true;
|
||||||
return resolve();
|
return resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.currentSession) this.currentSession.bye();
|
if (this.currentSession
|
||||||
if (this.userAgent) this.userAgent.stop();
|
&& ((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;
|
hangupRetries += 1;
|
||||||
|
|
||||||
@ -206,23 +300,24 @@ class SIPSession {
|
|||||||
return reject(this.baseErrorCodes.REQUEST_TIMEOUT);
|
return reject(this.baseErrorCodes.REQUEST_TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hangup) return tryHangup();
|
if (!this._hangupFlag) return tryHangup();
|
||||||
return resolve();
|
return resolve();
|
||||||
}, CALL_HANGUP_TIMEOUT);
|
}, CALL_HANGUP_TIMEOUT);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.currentSession) {
|
|
||||||
this.currentSession.on('bye', () => {
|
|
||||||
hangup = true;
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return tryHangup();
|
return tryHangup();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createUserAgent({ stun, turn }) {
|
onBeforeUnload() {
|
||||||
|
if (this.userAgent) {
|
||||||
|
return this.userAgent.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
createUserAgent(iceServers) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (this.userRequestedHangup === true) reject();
|
if (this.userRequestedHangup === true) reject();
|
||||||
|
|
||||||
@ -236,17 +331,6 @@ class SIPSession {
|
|||||||
sessionToken,
|
sessionToken,
|
||||||
} = this.user;
|
} = 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');
|
logger.debug({ logCode: 'sip_js_creating_user_agent', extraInfo: { callerIdName } }, 'Creating the user agent');
|
||||||
|
|
||||||
if (this.userAgent && this.userAgent.isConnected()) {
|
if (this.userAgent && this.userAgent.isConnected()) {
|
||||||
@ -275,112 +359,261 @@ class SIPSession {
|
|||||||
let userAgentConnected = false;
|
let userAgentConnected = false;
|
||||||
const token = `sessionToken=${sessionToken}`;
|
const token = `sessionToken=${sessionToken}`;
|
||||||
|
|
||||||
this.userAgent = new window.SIP.UA({
|
this.userAgent = new SIP.UserAgent({
|
||||||
uri: `sip:${encodeURIComponent(callerIdName)}@${hostname}`,
|
uri: SIP.UserAgent.makeURI(`sip:${encodeURIComponent(callerIdName)}@${hostname}`),
|
||||||
wsServers: `${(protocol === 'https:' ? 'wss://' : 'ws://')}${hostname}/ws?${token}`,
|
transportOptions: {
|
||||||
|
server: `${(protocol === 'https:' ? 'wss://' : 'ws://')}${hostname}/ws?${token}`,
|
||||||
|
connectionTimeout: USER_AGENT_CONNECTION_TIMEOUT_MS,
|
||||||
|
},
|
||||||
|
sessionDescriptionHandlerFactoryOptions: {
|
||||||
|
peerConnectionConfiguration: {
|
||||||
|
iceServers,
|
||||||
|
},
|
||||||
|
},
|
||||||
displayName: callerIdName,
|
displayName: callerIdName,
|
||||||
register: false,
|
register: false,
|
||||||
traceSip: true,
|
|
||||||
autostart: false,
|
|
||||||
userAgentString: 'BigBlueButton',
|
userAgentString: 'BigBlueButton',
|
||||||
stunServers: stun,
|
|
||||||
turnServers: turn,
|
|
||||||
hackPlanBUnifiedPlanTranslation: isSafari,
|
|
||||||
hackAddAudioTransceiver: isSafariWebview,
|
|
||||||
relayOnlyOnReconnect: this.reconnectAttempt && RELAY_ONLY_ON_RECONNECT,
|
|
||||||
localSdpCallback,
|
|
||||||
remoteSdpCallback,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleUserAgentConnection = () => {
|
const handleUserAgentConnection = () => {
|
||||||
userAgentConnected = true;
|
if (!userAgentConnected) {
|
||||||
resolve(this.userAgent);
|
userAgentConnected = true;
|
||||||
|
resolve(this.userAgent);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUserAgentDisconnection = () => {
|
const handleUserAgentDisconnection = () => {
|
||||||
if (this.userAgent) {
|
if (this.userAgent) {
|
||||||
this.userAgent.removeAllListeners();
|
if (this.userRequestedHangup) {
|
||||||
this.userAgent.stop();
|
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.transport.onConnect = handleUserAgentConnection;
|
||||||
this.userAgent.on('disconnected', handleUserAgentDisconnection);
|
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) {
|
inviteUserAgent(userAgent) {
|
||||||
if (this.userRequestedHangup === true) Promise.reject();
|
return new Promise((resolve, reject) => {
|
||||||
|
if (this.userRequestedHangup === true) reject();
|
||||||
|
const {
|
||||||
|
hostname,
|
||||||
|
} = this;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hostname,
|
callExtension,
|
||||||
} = this;
|
isListenOnly,
|
||||||
|
} = this.callOptions;
|
||||||
|
|
||||||
const {
|
|
||||||
inputStream,
|
|
||||||
callExtension,
|
|
||||||
} = this.callOptions;
|
|
||||||
|
|
||||||
const options = {
|
const target = SIP.UserAgent.makeURI(`sip:${callExtension}@${hostname}`);
|
||||||
media: {
|
|
||||||
stream: inputStream,
|
const audioDeviceConstraint = this.inputDeviceId
|
||||||
constraints: {
|
? { deviceId: { exact: this.inputDeviceId } }
|
||||||
audio: true,
|
: true;
|
||||||
video: false,
|
|
||||||
|
const inviterOptions = {
|
||||||
|
sessionDescriptionHandlerOptions: {
|
||||||
|
constraints: {
|
||||||
|
audio: isListenOnly
|
||||||
|
? false
|
||||||
|
: audioDeviceConstraint,
|
||||||
|
video: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
render: {
|
sessionDescriptionHandlerModifiersPostICEGathering:
|
||||||
remote: document.querySelector(MEDIA_TAG),
|
[stripMDnsCandidates],
|
||||||
},
|
};
|
||||||
},
|
|
||||||
RTCConstraints: {
|
|
||||||
offerToReceiveAudio: true,
|
|
||||||
offerToReceiveVideo: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
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) {
|
setupEventHandlers(currentSession) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (this.userRequestedHangup === true) reject();
|
if (this.userRequestedHangup === true) reject();
|
||||||
|
|
||||||
const { mediaHandler } = currentSession;
|
|
||||||
|
|
||||||
let iceCompleted = false;
|
let iceCompleted = false;
|
||||||
let fsReady = false;
|
let fsReady = false;
|
||||||
|
|
||||||
this.currentSession = currentSession;
|
const setupRemoteMedia = () => {
|
||||||
|
const mediaElement = document.querySelector(MEDIA_TAG);
|
||||||
|
|
||||||
let connectionCompletedEvents = ['iceConnectionCompleted', 'iceConnectionConnected'];
|
this.remoteStream = new MediaStream();
|
||||||
// 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
|
this.currentSession.sessionDescriptionHandler
|
||||||
// way to ignore one status is to not listen for it.
|
.peerConnection.getReceivers().forEach((receiver) => {
|
||||||
if (browser().name === 'edge') {
|
if (receiver.track) {
|
||||||
connectionCompletedEvents = ['iceConnectionCompleted'];
|
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 = () => {
|
const checkIfCallReady = () => {
|
||||||
if (this.userRequestedHangup === true) {
|
if (this.userRequestedHangup === true) {
|
||||||
@ -388,8 +621,28 @@ class SIPSession {
|
|||||||
resolve();
|
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) {
|
if (iceCompleted && fsReady) {
|
||||||
this.webrtcConnected = true;
|
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 });
|
this.callback({ status: this.baseCallStates.started });
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
@ -412,7 +665,6 @@ class SIPSession {
|
|||||||
const handleSessionAccepted = () => {
|
const handleSessionAccepted = () => {
|
||||||
logger.info({ logCode: 'sip_js_session_accepted', extraInfo: { callerIdName: this.user.callerIdName } }, 'Audio call session accepted');
|
logger.info({ logCode: 'sip_js_session_accepted', extraInfo: { callerIdName: this.user.callerIdName } }, 'Audio call session accepted');
|
||||||
clearTimeout(callTimeout);
|
clearTimeout(callTimeout);
|
||||||
currentSession.off('accepted', handleSessionAccepted);
|
|
||||||
|
|
||||||
// If ICE isn't connected yet then start timeout waiting for ICE to finish
|
// If ICE isn't connected yet then start timeout waiting for ICE to finish
|
||||||
if (!iceCompleted) {
|
if (!iceCompleted) {
|
||||||
@ -420,45 +672,114 @@ class SIPSession {
|
|||||||
this.callback({
|
this.callback({
|
||||||
status: this.baseCallStates.failed,
|
status: this.baseCallStates.failed,
|
||||||
error: 1010,
|
error: 1010,
|
||||||
bridgeError: `ICE negotiation timeout after ${ICE_NEGOTIATION_TIMEOUT / 1000}s`,
|
bridgeError: 'ICE negotiation timeout after '
|
||||||
|
+ `${ICE_NEGOTIATION_TIMEOUT / 1000}s`,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.exitAudio();
|
this.exitAudio();
|
||||||
|
|
||||||
|
reject({
|
||||||
|
type: this.baseErrorCodes.CONNECTION_ERROR,
|
||||||
|
});
|
||||||
}, ICE_NEGOTIATION_TIMEOUT);
|
}, 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();
|
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) => {
|
const handleSessionTerminated = (message, cause) => {
|
||||||
clearTimeout(callTimeout);
|
clearTimeout(callTimeout);
|
||||||
clearTimeout(iceNegotiationTimeout);
|
clearTimeout(iceNegotiationTimeout);
|
||||||
currentSession.off('terminated', handleSessionTerminated);
|
|
||||||
|
|
||||||
if (!message && !cause && !!this.userRequestedHangup) {
|
if (!message && !cause && !!this.userRequestedHangup) {
|
||||||
return this.callback({
|
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({
|
logger.error({
|
||||||
logCode: 'sip_js_call_terminated',
|
logCode: 'sip_js_call_terminated',
|
||||||
extraInfo: { cause, callerIdName: this.user.callerIdName },
|
extraInfo: { cause, callerIdName: this.user.callerIdName },
|
||||||
@ -484,39 +809,33 @@ class SIPSession {
|
|||||||
bridgeError: cause,
|
bridgeError: cause,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
currentSession.on('terminated', handleSessionTerminated);
|
|
||||||
|
|
||||||
const handleIceNegotiationFailed = (peer) => {
|
currentSession.stateChange.addListener((state) => {
|
||||||
if (iceCompleted) {
|
switch (state) {
|
||||||
logger.error({ logCode: 'sipjs_ice_failed_after', extraInfo: { callerIdName: this.user.callerIdName } }, 'ICE connection failed after success');
|
case SIP.SessionState.Initial:
|
||||||
} else {
|
break;
|
||||||
logger.error({ logCode: 'sipjs_ice_failed_before', extraInfo: { callerIdName: this.user.callerIdName } }, 'ICE connection failed before success');
|
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);
|
this._currentSessionState = state;
|
||||||
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));
|
|
||||||
|
|
||||||
Tracker.autorun((c) => {
|
Tracker.autorun((c) => {
|
||||||
const selector = { meetingId: Auth.meetingID, userId: Auth.userID };
|
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;
|
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 !== '';
|
const hasFallbackDomain = typeof IPV4_FALLBACK_DOMAIN === 'string' && IPV4_FALLBACK_DOMAIN !== '';
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -603,7 +928,12 @@ export default class SIPBridge extends BaseAudioBridge {
|
|||||||
const fallbackExtension = this.activeSession.inEchoTest ? extension : undefined;
|
const fallbackExtension = this.activeSession.inEchoTest ? extension : undefined;
|
||||||
this.activeSession = new SIPSession(this.user, this.userData, this.protocol,
|
this.activeSession = new SIPSession(this.user, this.userData, this.protocol,
|
||||||
hostname, this.baseCallStates, this.baseErrorCodes, true);
|
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) => {
|
.then((value) => {
|
||||||
resolve(value);
|
resolve(value);
|
||||||
}).catch((reason) => {
|
}).catch((reason) => {
|
||||||
@ -615,7 +945,12 @@ export default class SIPBridge extends BaseAudioBridge {
|
|||||||
return managerCallback(message);
|
return managerCallback(message);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.activeSession.joinAudio({ isListenOnly, extension, inputStream }, callback)
|
const { inputDeviceId } = this.media.inputDevice;
|
||||||
|
this.activeSession.joinAudio({
|
||||||
|
isListenOnly,
|
||||||
|
extension,
|
||||||
|
inputDeviceId,
|
||||||
|
}, callback)
|
||||||
.then((value) => {
|
.then((value) => {
|
||||||
resolve(value);
|
resolve(value);
|
||||||
}).catch((reason) => {
|
}).catch((reason) => {
|
||||||
@ -630,8 +965,8 @@ export default class SIPBridge extends BaseAudioBridge {
|
|||||||
|
|
||||||
getPeerConnection() {
|
getPeerConnection() {
|
||||||
const { currentSession } = this.activeSession;
|
const { currentSession } = this.activeSession;
|
||||||
if (currentSession && currentSession.mediaHandler) {
|
if (currentSession && currentSession.sessionDescriptionHandler) {
|
||||||
return currentSession.mediaHandler.peerConnection;
|
return currentSession.sessionDescriptionHandler.peerConnection;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -641,62 +976,16 @@ export default class SIPBridge extends BaseAudioBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setDefaultInputDevice() {
|
setDefaultInputDevice() {
|
||||||
const handleMediaSuccess = (mediaStream) => {
|
this.media.inputDevice.inputDeviceId = DEFAULT_INPUT_DEVICE_ID;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changeInputDevice(deviceId, deviceLabel) {
|
async changeInputDeviceId(inputDeviceId) {
|
||||||
const {
|
if (!inputDeviceId) {
|
||||||
media,
|
throw new Error();
|
||||||
} = 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('AudioContext' in window) {
|
this.media.inputDevice.inputDeviceId = inputDeviceId;
|
||||||
media.inputDevice.audioContext = new window.AudioContext();
|
return inputDeviceId;
|
||||||
} 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeOutputDevice(value) {
|
async changeOutputDevice(value) {
|
||||||
|
@ -6,11 +6,14 @@ import { extractCredentials } from '/imports/api/common/server/helpers';
|
|||||||
export default function createBreakoutRoom(rooms, durationInMinutes, record = false) {
|
export default function createBreakoutRoom(rooms, durationInMinutes, record = false) {
|
||||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
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 { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||||
|
|
||||||
const eventName = 'CreateBreakoutRoomsCmdMsg';
|
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 = {
|
const payload = {
|
||||||
record,
|
record,
|
||||||
durationInMinutes,
|
durationInMinutes,
|
||||||
|
@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor';
|
|||||||
import { check } from 'meteor/check';
|
import { check } from 'meteor/check';
|
||||||
import Logger from '/imports/startup/server/logger';
|
import Logger from '/imports/startup/server/logger';
|
||||||
import Meetings from '/imports/api/meetings';
|
import Meetings from '/imports/api/meetings';
|
||||||
|
import Users from '/imports/api/users';
|
||||||
import RedisPubSub from '/imports/startup/server/redis';
|
import RedisPubSub from '/imports/startup/server/redis';
|
||||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||||
|
|
||||||
@ -10,16 +11,29 @@ export default function startWatchingExternalVideo(options) {
|
|||||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||||
const EVENT_NAME = 'StartExternalVideoMsg';
|
const EVENT_NAME = 'StartExternalVideoMsg';
|
||||||
|
|
||||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
const { meetingId, requesterUserId: userId } = extractCredentials(this.userId);
|
||||||
const { externalVideoUrl } = options;
|
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 { Meteor } from 'meteor/meteor';
|
||||||
import Logger from '/imports/startup/server/logger';
|
import Logger from '/imports/startup/server/logger';
|
||||||
import Meetings from '/imports/api/meetings';
|
import Meetings from '/imports/api/meetings';
|
||||||
|
import Users from '/imports/api/users';
|
||||||
import RedisPubSub from '/imports/startup/server/redis';
|
import RedisPubSub from '/imports/startup/server/redis';
|
||||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||||
|
|
||||||
@ -9,19 +10,33 @@ export default function stopWatchingExternalVideo(options) {
|
|||||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||||
const EVENT_NAME = 'StopExternalVideoMsg';
|
const EVENT_NAME = 'StopExternalVideoMsg';
|
||||||
|
|
||||||
if (this.userId) {
|
const { meetingId, requesterUserId } = this.userId ? extractCredentials(this.userId) : options;
|
||||||
options = extractCredentials(this.userId);
|
|
||||||
|
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 = {
|
const payload = {
|
||||||
userId: requesterUserId,
|
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);
|
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 clearRecordMeeting from './clearRecordMeeting';
|
||||||
import clearVoiceCallStates from '/imports/api/voice-call-states/server/modifiers/clearVoiceCallStates';
|
import clearVoiceCallStates from '/imports/api/voice-call-states/server/modifiers/clearVoiceCallStates';
|
||||||
import clearVideoStreams from '/imports/api/video-streams/server/modifiers/clearVideoStreams';
|
import clearVideoStreams from '/imports/api/video-streams/server/modifiers/clearVideoStreams';
|
||||||
|
import BannedUsers from '/imports/api/users/server/store/bannedUsers';
|
||||||
|
|
||||||
export default function meetingHasEnded(meetingId) {
|
export default function meetingHasEnded(meetingId) {
|
||||||
removeAnnotationsStreamer(meetingId);
|
removeAnnotationsStreamer(meetingId);
|
||||||
@ -46,6 +47,7 @@ export default function meetingHasEnded(meetingId) {
|
|||||||
clearRecordMeeting(meetingId);
|
clearRecordMeeting(meetingId);
|
||||||
clearVoiceCallStates(meetingId);
|
clearVoiceCallStates(meetingId);
|
||||||
clearVideoStreams(meetingId);
|
clearVideoStreams(meetingId);
|
||||||
|
BannedUsers.delete(meetingId);
|
||||||
|
|
||||||
return Logger.info(`Cleared Meetings with id ${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 Logger from '/imports/startup/server/logger';
|
||||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
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 REDIS_CONFIG = Meteor.settings.private.redis;
|
||||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||||
const EVENT_NAME = 'RespondToPollReqMsg';
|
const EVENT_NAME = 'RespondToPollReqMsg';
|
||||||
|
|
||||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
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(pollAnswerId, Number);
|
||||||
check(currentPoll, Object);
|
check(pollId, String);
|
||||||
check(currentPoll.meetingId, String);
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
requesterId: requesterUserId,
|
|
||||||
pollId: currentPoll.id,
|
|
||||||
questionId: 0,
|
|
||||||
answerId: pollAnswerId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const selector = {
|
const selector = {
|
||||||
users: requesterUserId,
|
users: requesterUserId,
|
||||||
@ -39,6 +20,18 @@ export default function publishVote(id, pollAnswerId) { // TODO discuss location
|
|||||||
'answers.id': pollAnswerId,
|
'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 = {
|
const modifier = {
|
||||||
$pull: {
|
$pull: {
|
||||||
users: requesterUserId,
|
users: requesterUserId,
|
||||||
@ -47,11 +40,11 @@ export default function publishVote(id, pollAnswerId) { // TODO discuss location
|
|||||||
|
|
||||||
const cb = (err) => {
|
const cb = (err) => {
|
||||||
if (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}, `
|
return Logger.info(`Removed responded user=${requesterUserId} from poll (meetingId: ${meetingId}, `
|
||||||
+ `pollId: ${currentPoll.id}!)`);
|
+ `pollId: ${pollId}!)`);
|
||||||
};
|
};
|
||||||
|
|
||||||
Polls.update(selector, modifier, cb);
|
Polls.update(selector, modifier, cb);
|
||||||
|
@ -63,15 +63,80 @@ export default class KurentoScreenshareBridge {
|
|||||||
return normalizedError;
|
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 iceServers = [];
|
||||||
let started = false;
|
let started = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
iceServers = await fetchWebRTCMappedStunTurnServers(getSessionToken());
|
iceServers = await fetchWebRTCMappedStunTurnServers(getSessionToken());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ logCode: 'screenshare_viwer_fetchstunturninfo_error', extraInfo: { error } },
|
logger.error({
|
||||||
'Screenshare bridge failed to fetch STUN/TURN info, using default');
|
logCode: 'screenshare_viewer_fetchstunturninfo_error',
|
||||||
|
extraInfo: { error },
|
||||||
|
}, 'Screenshare bridge failed to fetch STUN/TURN info, using default');
|
||||||
iceServers = getMappedFallbackStun();
|
iceServers = getMappedFallbackStun();
|
||||||
} finally {
|
} finally {
|
||||||
const options = {
|
const options = {
|
||||||
@ -81,52 +146,6 @@ export default class KurentoScreenshareBridge {
|
|||||||
userName: getUsername(),
|
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) => {
|
const onFail = (error) => {
|
||||||
KurentoScreenshareBridge.handleViewerFailure(error, started);
|
KurentoScreenshareBridge.handleViewerFailure(error, started);
|
||||||
};
|
};
|
||||||
@ -139,10 +158,11 @@ export default class KurentoScreenshareBridge {
|
|||||||
const { webRtcPeer } = window.kurentoManager.kurentoVideo;
|
const { webRtcPeer } = window.kurentoManager.kurentoVideo;
|
||||||
if (webRtcPeer) {
|
if (webRtcPeer) {
|
||||||
const stream = webRtcPeer.getRemoteStream();
|
const stream = webRtcPeer.getRemoteStream();
|
||||||
screenshareTag.muted = true;
|
KurentoScreenshareBridge.screenshareElementLoadAndPlay(
|
||||||
screenshareTag.pause();
|
stream,
|
||||||
screenshareTag.srcObject = stream;
|
screenshareMediaElement,
|
||||||
playElement();
|
true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ const oldParametersKeys = Object.keys(oldParameters);
|
|||||||
const currentParameters = [
|
const currentParameters = [
|
||||||
// APP
|
// APP
|
||||||
'bbb_ask_for_feedback_on_logout',
|
'bbb_ask_for_feedback_on_logout',
|
||||||
|
'bbb_override_default_locale',
|
||||||
'bbb_auto_join_audio',
|
'bbb_auto_join_audio',
|
||||||
'bbb_client_title',
|
'bbb_client_title',
|
||||||
'bbb_force_listen_only',
|
'bbb_force_listen_only',
|
||||||
@ -69,7 +70,7 @@ const currentParameters = [
|
|||||||
|
|
||||||
function valueParser(val) {
|
function valueParser(val) {
|
||||||
try {
|
try {
|
||||||
const parsedValue = JSON.parse(val.toLowerCase());
|
const parsedValue = JSON.parse(val.toLowerCase().trim());
|
||||||
return parsedValue;
|
return parsedValue;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`addUserSettings:Parameter ${val} could not be parsed (was not json)`);
|
logger.warn(`addUserSettings:Parameter ${val} could not be parsed (was not json)`);
|
||||||
@ -86,21 +87,22 @@ export default function addUserSettings(settings) {
|
|||||||
|
|
||||||
settings.forEach((el) => {
|
settings.forEach((el) => {
|
||||||
const settingKey = Object.keys(el).shift();
|
const settingKey = Object.keys(el).shift();
|
||||||
|
const normalizedKey = settingKey.trim();
|
||||||
|
|
||||||
if (currentParameters.includes(settingKey)) {
|
if (currentParameters.includes(normalizedKey)) {
|
||||||
if (!Object.keys(parameters).includes(settingKey)) {
|
if (!Object.keys(parameters).includes(normalizedKey)) {
|
||||||
parameters = {
|
parameters = {
|
||||||
[settingKey]: valueParser(el[settingKey]),
|
[normalizedKey]: valueParser(el[settingKey]),
|
||||||
...parameters,
|
...parameters,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
parameters[settingKey] = el[settingKey];
|
parameters[normalizedKey] = el[settingKey];
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldParametersKeys.includes(settingKey)) {
|
if (oldParametersKeys.includes(normalizedKey)) {
|
||||||
const matchingNewKey = oldParameters[settingKey];
|
const matchingNewKey = oldParameters[normalizedKey];
|
||||||
if (!Object.keys(parameters).includes(matchingNewKey)) {
|
if (!Object.keys(parameters).includes(matchingNewKey)) {
|
||||||
parameters = {
|
parameters = {
|
||||||
[matchingNewKey]: valueParser(el[settingKey]),
|
[matchingNewKey]: valueParser(el[settingKey]),
|
||||||
@ -110,7 +112,7 @@ export default function addUserSettings(settings) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.warn(`Parameter ${settingKey} not handled`);
|
logger.warn(`Parameter ${normalizedKey} not handled`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const settingsAdded = [];
|
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 */
|
/* Logic migrated from validateAuthToken method ( postponed to only run in case of success response ) - Begin */
|
||||||
const sessionId = `${meetingId}--${userId}`;
|
const sessionId = `${meetingId}--${userId}`;
|
||||||
|
|
||||||
methodInvocationObject.setUserId(sessionId);
|
methodInvocationObject.setUserId(sessionId);
|
||||||
|
|
||||||
const User = Users.findOne({
|
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 the current user connection is not the same that triggered the leave we skip
|
||||||
if (User.connectionId !== connectionId) {
|
if (User.connectionId !== connectionId) {
|
||||||
|
Logger.info(`Skipping userLeaving. User connectionId=${User.connectionId} is different from requester connectionId=${connectionId}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import RedisPubSub from '/imports/startup/server/redis';
|
|||||||
import Logger from '/imports/startup/server/logger';
|
import Logger from '/imports/startup/server/logger';
|
||||||
import pendingAuthenticationsStore from '../store/pendingAuthentications';
|
import pendingAuthenticationsStore from '../store/pendingAuthentications';
|
||||||
import BannedUsers from '../store/bannedUsers';
|
import BannedUsers from '../store/bannedUsers';
|
||||||
|
import Users from '/imports/api/users';
|
||||||
|
|
||||||
export default function validateAuthToken(meetingId, requesterUserId, requesterToken, externalId) {
|
export default function validateAuthToken(meetingId, requesterUserId, requesterToken, externalId) {
|
||||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||||
@ -13,10 +14,27 @@ export default function validateAuthToken(meetingId, requesterUserId, requesterT
|
|||||||
if (externalId) {
|
if (externalId) {
|
||||||
if (BannedUsers.has(meetingId, externalId)) {
|
if (BannedUsers.has(meetingId, externalId)) {
|
||||||
Logger.warn(`A banned user with extId ${externalId} tried to enter in meeting ${meetingId}`);
|
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 )
|
// Store reference of methodInvocationObject ( to postpone the connection userId definition )
|
||||||
pendingAuthenticationsStore.add(meetingId, requesterUserId, requesterToken, this);
|
pendingAuthenticationsStore.add(meetingId, requesterUserId, requesterToken, this);
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ class BannedUsers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(meetingId) {
|
init(meetingId) {
|
||||||
Logger.debug('BannedUsers :: init', meetingId);
|
Logger.debug('BannedUsers :: init', { meetingId });
|
||||||
|
|
||||||
if (!this.store[meetingId]) this.store[meetingId] = new Set();
|
if (!this.store[meetingId]) this.store[meetingId] = new Set();
|
||||||
}
|
}
|
||||||
@ -20,7 +20,7 @@ class BannedUsers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delete(meetingId) {
|
delete(meetingId) {
|
||||||
Logger.debug('BannedUsers :: delete', meetingId);
|
Logger.debug('BannedUsers :: delete', { meetingId });
|
||||||
delete this.store[meetingId];
|
delete this.store[meetingId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -359,8 +359,9 @@ const BaseContainer = withTracker(() => {
|
|||||||
changed: (newDocument) => {
|
changed: (newDocument) => {
|
||||||
if (newDocument.validated && newDocument.name && newDocument.userId !== localUserId) {
|
if (newDocument.validated && newDocument.name && newDocument.userId !== localUserId) {
|
||||||
if (userJoinAudioAlerts) {
|
if (userJoinAudioAlerts) {
|
||||||
const audio = new Audio(`${Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename}/resources/sounds/userJoin.mp3`);
|
AudioService.playAlertSound(`${Meteor.settings.public.app.cdn
|
||||||
audio.play();
|
+ Meteor.settings.public.app.basename}`
|
||||||
|
+ '/resources/sounds/userJoin.mp3');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userJoinPushAlerts) {
|
if (userJoinPushAlerts) {
|
||||||
|
@ -1,109 +1,10 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { withTracker } from 'meteor/react-meteor-data';
|
import { withTracker } from 'meteor/react-meteor-data';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import Settings from '/imports/ui/services/settings';
|
import Settings from '/imports/ui/services/settings';
|
||||||
import LoadingScreen from '/imports/ui/components/loading-screen/component';
|
import LoadingScreen from '/imports/ui/components/loading-screen/component';
|
||||||
|
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||||
// 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,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
locale: PropTypes.string,
|
locale: PropTypes.string,
|
||||||
@ -120,7 +21,12 @@ const defaultProps = {
|
|||||||
|
|
||||||
class IntlStartup extends Component {
|
class IntlStartup extends Component {
|
||||||
static saveLocale(localeName) {
|
static saveLocale(localeName) {
|
||||||
|
if (Settings.application.locale !== localeName) {
|
||||||
|
Settings.application.changedLocale = localeName;
|
||||||
|
}
|
||||||
|
|
||||||
Settings.application.locale = localeName;
|
Settings.application.locale = localeName;
|
||||||
|
|
||||||
if (RTL_LANGUAGES.includes(localeName.substring(0, 2))) {
|
if (RTL_LANGUAGES.includes(localeName.substring(0, 2))) {
|
||||||
document.body.parentNode.setAttribute('dir', 'rtl');
|
document.body.parentNode.setAttribute('dir', 'rtl');
|
||||||
Settings.application.isRTL = true;
|
Settings.application.isRTL = true;
|
||||||
@ -155,16 +61,32 @@ class IntlStartup extends Component {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { fetching, normalizedLocale, localeChanged } = this.state;
|
const { fetching, normalizedLocale, localeChanged } = this.state;
|
||||||
const { locale } = this.props;
|
const { locale, overrideLocale, changedLocale } = this.props;
|
||||||
|
|
||||||
if (prevProps.locale !== locale) {
|
if (prevProps.locale !== locale) {
|
||||||
this.setState({
|
this.setState({
|
||||||
localeChanged: true,
|
localeChanged: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (overrideLocale) {
|
||||||
|
if (!fetching
|
||||||
|
&& (overrideLocale !== normalizedLocale.toLowerCase())
|
||||||
|
&& !localeChanged
|
||||||
|
&& !changedLocale) {
|
||||||
|
this.fetchLocalizedMessages(overrideLocale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localeChanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!fetching
|
if (!fetching
|
||||||
&& normalizedLocale
|
&& normalizedLocale
|
||||||
&& ((locale.toLowerCase() !== normalizedLocale.toLowerCase()))) {
|
&& ((locale.toLowerCase() !== normalizedLocale.toLowerCase()))) {
|
||||||
if (((DEFAULT_LANGUAGE === normalizedLocale.toLowerCase()) && !localeChanged)) return;
|
if (((DEFAULT_LANGUAGE === normalizedLocale.toLowerCase()) && !localeChanged)) return;
|
||||||
|
|
||||||
this.fetchLocalizedMessages(locale);
|
this.fetchLocalizedMessages(locale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -209,10 +131,12 @@ class IntlStartup extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const IntlStartupContainer = withTracker(() => {
|
const IntlStartupContainer = withTracker(() => {
|
||||||
const { locale } = Settings.application;
|
const { locale, changedLocale } = Settings.application;
|
||||||
|
const overrideLocale = getFromUserSettings('bbb_override_default_locale', null);
|
||||||
return {
|
return {
|
||||||
locale,
|
locale,
|
||||||
|
overrideLocale,
|
||||||
|
changedLocale,
|
||||||
};
|
};
|
||||||
})(IntlStartup);
|
})(IntlStartup);
|
||||||
|
|
||||||
|
@ -71,7 +71,8 @@ class MeteorStream {
|
|||||||
'logClient',
|
'logClient',
|
||||||
nameFromLevel[this.rec.level],
|
nameFromLevel[this.rec.level],
|
||||||
this.rec.msg,
|
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 { check } from 'meteor/check';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import Redis from './redis';
|
import Redis from './redis';
|
||||||
|
|
||||||
import setMinBrowserVersions from './minBrowserVersion';
|
import setMinBrowserVersions from './minBrowserVersion';
|
||||||
import userLeaving from '/imports/api/users/server/methods/userLeaving';
|
import userLeaving from '/imports/api/users/server/methods/userLeaving';
|
||||||
|
|
||||||
|
let guestWaitHtml = '';
|
||||||
const AVAILABLE_LOCALES = fs.readdirSync('assets/app/locales');
|
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(() => {
|
Meteor.startup(() => {
|
||||||
const APP_CONFIG = Meteor.settings.public.app;
|
const APP_CONFIG = Meteor.settings.public.app;
|
||||||
@ -151,23 +178,13 @@ WebApp.connectHandlers.use('/locale', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
WebApp.connectHandlers.use('/locales', (req, res) => {
|
WebApp.connectHandlers.use('/locales', (req, res) => {
|
||||||
if (!avaibleLocalesNames.length) {
|
if (!avaibleLocalesNamesJSON) {
|
||||||
try {
|
avaibleLocalesNamesJSON = JSON.stringify(generateLocaleOptions());
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Content-Type', 'application/json');
|
||||||
res.writeHead(200);
|
res.writeHead(200);
|
||||||
res.end(JSON.stringify(avaibleLocalesNames));
|
res.end(avaibleLocalesNamesJSON);
|
||||||
});
|
});
|
||||||
|
|
||||||
WebApp.connectHandlers.use('/feedback', (req, res) => {
|
WebApp.connectHandlers.use('/feedback', (req, res) => {
|
||||||
@ -227,6 +244,21 @@ WebApp.connectHandlers.use('/useragent', (req, res) => {
|
|||||||
res.end(response);
|
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 eventEmitter = Redis.emitter;
|
||||||
|
|
||||||
export const redisPubSub = Redis;
|
export const redisPubSub = Redis;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 Button from '/imports/ui/components/button/component';
|
||||||
import Dropdown from '/imports/ui/components/dropdown/component';
|
import Dropdown from '/imports/ui/components/dropdown/component';
|
||||||
import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component';
|
import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component';
|
||||||
@ -17,7 +17,7 @@ import { styles } from '../styles';
|
|||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
amIPresenter: PropTypes.bool.isRequired,
|
amIPresenter: PropTypes.bool.isRequired,
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
mountModal: PropTypes.func.isRequired,
|
mountModal: PropTypes.func.isRequired,
|
||||||
amIModerator: PropTypes.bool.isRequired,
|
amIModerator: PropTypes.bool.isRequired,
|
||||||
shortcuts: PropTypes.string,
|
shortcuts: PropTypes.string,
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cx from 'classnames';
|
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 { styles } from '/imports/ui/components/actions-bar/styles';
|
||||||
import Button from '/imports/ui/components/button/component';
|
import Button from '/imports/ui/components/button/component';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
isActive: PropTypes.bool.isRequired,
|
isActive: PropTypes.bool.isRequired,
|
||||||
handleOnClick: PropTypes.func.isRequired,
|
handleOnClick: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -118,7 +118,7 @@ class ActionsBar extends PureComponent {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className={cx(styles.button, autoArrangeLayout || styles.btn)}
|
className={cx(styles.btn, autoArrangeLayout || styles.btn)}
|
||||||
icon={autoArrangeLayout ? 'lock' : 'unlock'}
|
icon={autoArrangeLayout ? 'lock' : 'unlock'}
|
||||||
color={autoArrangeLayout ? 'primary' : 'default'}
|
color={autoArrangeLayout ? 'primary' : 'default'}
|
||||||
ghost={!autoArrangeLayout}
|
ghost={!autoArrangeLayout}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import browser from 'browser-detect';
|
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 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 = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
isInvitation: PropTypes.bool.isRequired,
|
isInvitation: PropTypes.bool.isRequired,
|
||||||
meetingName: PropTypes.string.isRequired,
|
meetingName: PropTypes.string.isRequired,
|
||||||
users: PropTypes.arrayOf(PropTypes.object).isRequired,
|
users: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
@ -117,11 +117,12 @@ input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-i
|
|||||||
}
|
}
|
||||||
|
|
||||||
.boxContainer {
|
.boxContainer {
|
||||||
height: 50vh;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: repeat(3, minmax(4rem, 16rem));
|
||||||
grid-template-rows: 33% 33% 33%;
|
grid-template-rows: repeat(auto-fill, minmax(4rem, 8rem));
|
||||||
grid-gap: 1.5rem 1rem;
|
grid-gap: 1.5rem 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.changeToWarn {
|
.changeToWarn {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import browser from 'browser-detect';
|
import browser from 'browser-detect';
|
||||||
import Button from '/imports/ui/components/button/component';
|
import Button from '/imports/ui/components/button/component';
|
||||||
import logger from '/imports/startup/client/logger';
|
import logger from '/imports/startup/client/logger';
|
||||||
@ -12,7 +12,7 @@ import { styles } from '../styles';
|
|||||||
import ScreenshareBridgeService from '/imports/api/screenshare/client/bridge/service';
|
import ScreenshareBridgeService from '/imports/api/screenshare/client/bridge/service';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
amIPresenter: PropTypes.bool.isRequired,
|
amIPresenter: PropTypes.bool.isRequired,
|
||||||
handleShareScreen: PropTypes.func.isRequired,
|
handleShareScreen: PropTypes.func.isRequired,
|
||||||
handleUnshareScreen: PropTypes.func.isRequired,
|
handleUnshareScreen: PropTypes.func.isRequired,
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 Button from '/imports/ui/components/button/component';
|
||||||
import MediaService from '/imports/ui/components/media/service';
|
import MediaService from '/imports/ui/components/media/service';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
toggleSwapLayout: PropTypes.func.isRequired,
|
toggleSwapLayout: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { defineMessages, intlShape } from 'react-intl';
|
import { defineMessages } from 'react-intl';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { makeCall } from '/imports/ui/services/api';
|
import { makeCall } from '/imports/ui/services/api';
|
||||||
import Button from '/imports/ui/components/button/component';
|
import Button from '/imports/ui/components/button/component';
|
||||||
@ -35,7 +35,7 @@ const intlMessages = defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
parseCurrentSlideContent: PropTypes.func.isRequired,
|
parseCurrentSlideContent: PropTypes.func.isRequired,
|
||||||
amIPresenter: PropTypes.bool.isRequired,
|
amIPresenter: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 Button from '/imports/ui/components/button/component';
|
||||||
import Modal from '/imports/ui/components/modal/simple/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';
|
import { styles } from './styles';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
responseDelay: PropTypes.number.isRequired,
|
responseDelay: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import Modal from 'react-modal';
|
import Modal from 'react-modal';
|
||||||
import browser from 'browser-detect';
|
import browser from 'browser-detect';
|
||||||
import PanelManager from '/imports/ui/components/panel-manager/component';
|
import PanelManager from '/imports/ui/components/panel-manager/component';
|
||||||
@ -83,7 +83,7 @@ const propTypes = {
|
|||||||
actionsbar: PropTypes.element,
|
actionsbar: PropTypes.element,
|
||||||
captions: PropTypes.element,
|
captions: PropTypes.element,
|
||||||
locale: PropTypes.string,
|
locale: PropTypes.string,
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
|
@ -12,6 +12,7 @@ import getFromUserSettings from '/imports/ui/services/users-settings';
|
|||||||
import deviceInfo from '/imports/utils/deviceInfo';
|
import deviceInfo from '/imports/utils/deviceInfo';
|
||||||
import UserInfos from '/imports/api/users-infos';
|
import UserInfos from '/imports/api/users-infos';
|
||||||
import { startBandwidthMonitoring, updateNavigatorConnection } from '/imports/ui/services/network-information/index';
|
import { startBandwidthMonitoring, updateNavigatorConnection } from '/imports/ui/services/network-information/index';
|
||||||
|
import logger from '/imports/startup/client/logger';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getFontSize,
|
getFontSize,
|
||||||
@ -72,9 +73,9 @@ const currentUserEmoji = currentUser => (currentUser ? {
|
|||||||
status: currentUser.emoji,
|
status: currentUser.emoji,
|
||||||
changedAt: currentUser.emojiTime,
|
changedAt: currentUser.emojiTime,
|
||||||
} : {
|
} : {
|
||||||
status: 'none',
|
status: 'none',
|
||||||
changedAt: null,
|
changedAt: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default injectIntl(withModalMounter(withTracker(({ intl, baseControls }) => {
|
export default injectIntl(withModalMounter(withTracker(({ intl, baseControls }) => {
|
||||||
const currentUser = Users.findOne({ userId: Auth.userID }, { fields: { approved: 1, emoji: 1 } });
|
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) {
|
changed(id, fields) {
|
||||||
const hasNewConnection = 'connectionId' in fields && (fields.connectionId !== Meteor.connection._lastSessionId);
|
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');
|
endMeeting('403');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cx from 'classnames';
|
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 Button from '/imports/ui/components/button/component';
|
||||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||||
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
|
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
|
||||||
@ -37,7 +37,7 @@ const propTypes = {
|
|||||||
showMute: PropTypes.bool.isRequired,
|
showMute: PropTypes.bool.isRequired,
|
||||||
inAudio: PropTypes.bool.isRequired,
|
inAudio: PropTypes.bool.isRequired,
|
||||||
listenOnly: PropTypes.bool.isRequired,
|
listenOnly: PropTypes.bool.isRequired,
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
talking: PropTypes.bool.isRequired,
|
talking: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { injectIntl, defineMessages, intlShape } from 'react-intl';
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
import { styles } from './styles';
|
import { styles } from './styles';
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
const intlMessages = defineMessages({
|
||||||
@ -23,7 +23,7 @@ const intlMessages = defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
formattedDialNum: PropTypes.string.isRequired,
|
formattedDialNum: PropTypes.string.isRequired,
|
||||||
telVoice: 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 Button from '/imports/ui/components/button/component';
|
||||||
import { Session } from 'meteor/session';
|
import { Session } from 'meteor/session';
|
||||||
import {
|
import {
|
||||||
defineMessages, injectIntl, intlShape, FormattedMessage,
|
defineMessages, injectIntl, FormattedMessage,
|
||||||
} from 'react-intl';
|
} from 'react-intl';
|
||||||
import { styles } from './styles';
|
import { styles } from './styles';
|
||||||
import PermissionsOverlay from '../permissions-overlay/component';
|
import PermissionsOverlay from '../permissions-overlay/component';
|
||||||
@ -16,7 +16,7 @@ import AudioDial from '../audio-dial/component';
|
|||||||
import AudioAutoplayPrompt from '../autoplay/component';
|
import AudioAutoplayPrompt from '../autoplay/component';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
closeModal: PropTypes.func.isRequired,
|
closeModal: PropTypes.func.isRequired,
|
||||||
joinMicrophone: PropTypes.func.isRequired,
|
joinMicrophone: PropTypes.func.isRequired,
|
||||||
joinListenOnly: PropTypes.func.isRequired,
|
joinListenOnly: PropTypes.func.isRequired,
|
||||||
@ -245,8 +245,6 @@ class AudioModal extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
inputDeviceId,
|
|
||||||
outputDeviceId,
|
|
||||||
joinEchoTest,
|
joinEchoTest,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@ -268,12 +266,27 @@ class AudioModal extends Component {
|
|||||||
disableActions: false,
|
disableActions: false,
|
||||||
});
|
});
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
if (err.type === 'MEDIA_ERROR') {
|
const { type } = err;
|
||||||
this.setState({
|
switch (type) {
|
||||||
content: 'help',
|
case 'MEDIA_ERROR':
|
||||||
errCode: err.code,
|
this.setState({
|
||||||
disableActions: false,
|
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 React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 Button from '/imports/ui/components/button/component';
|
||||||
import { withModalMounter } from '/imports/ui/components/modal/service';
|
import { withModalMounter } from '/imports/ui/components/modal/service';
|
||||||
import DeviceSelector from '/imports/ui/components/audio/device-selector/component';
|
import DeviceSelector from '/imports/ui/components/audio/device-selector/component';
|
||||||
@ -9,7 +9,7 @@ import cx from 'classnames';
|
|||||||
import { styles } from './styles';
|
import { styles } from './styles';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
changeInputDevice: PropTypes.func.isRequired,
|
changeInputDevice: PropTypes.func.isRequired,
|
||||||
changeOutputDevice: PropTypes.func.isRequired,
|
changeOutputDevice: PropTypes.func.isRequired,
|
||||||
handleBack: PropTypes.func.isRequired,
|
handleBack: PropTypes.func.isRequired,
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Button from '/imports/ui/components/button/component';
|
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';
|
import { styles } from './styles.scss';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
handlePlayAudioSample: PropTypes.func.isRequired,
|
handlePlayAudioSample: PropTypes.func.isRequired,
|
||||||
outputDeviceId: PropTypes.string,
|
outputDeviceId: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Button from '/imports/ui/components/button/component';
|
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';
|
import { styles } from './styles';
|
||||||
|
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ const intlMessages = defineMessages({
|
|||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
handleAllowAutoplay: PropTypes.func.isRequired,
|
handleAllowAutoplay: PropTypes.func.isRequired,
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
class AudioAutoplayPrompt extends PureComponent {
|
class AudioAutoplayPrompt extends PureComponent {
|
||||||
|
@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Session } from 'meteor/session';
|
import { Session } from 'meteor/session';
|
||||||
import Button from '/imports/ui/components/button/component';
|
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';
|
import { styles } from './styles';
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
const intlMessages = defineMessages({
|
||||||
@ -27,7 +27,7 @@ const intlMessages = defineMessages({
|
|||||||
const propTypes = {
|
const propTypes = {
|
||||||
handleYes: PropTypes.func.isRequired,
|
handleYes: PropTypes.func.isRequired,
|
||||||
handleNo: PropTypes.func.isRequired,
|
handleNo: PropTypes.func.isRequired,
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
class EchoTest extends Component {
|
class EchoTest extends Component {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
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 Modal from '/imports/ui/components/modal/simple/component';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { styles } from './styles';
|
import { styles } from './styles';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
closeModal: PropTypes.func.isRequired,
|
closeModal: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -88,4 +88,5 @@ export default {
|
|||||||
isVoiceUser,
|
isVoiceUser,
|
||||||
autoplayBlocked: () => AudioManager.autoplayBlocked,
|
autoplayBlocked: () => AudioManager.autoplayBlocked,
|
||||||
handleAllowAutoplay: () => AudioManager.handleAllowAutoplay(),
|
handleAllowAutoplay: () => AudioManager.handleAllowAutoplay(),
|
||||||
|
playAlertSound: url => AudioManager.playAlertSound(url),
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
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 { withModalMounter } from '/imports/ui/components/modal/service';
|
||||||
import Modal from '/imports/ui/components/modal/fullscreen/component';
|
import Modal from '/imports/ui/components/modal/fullscreen/component';
|
||||||
import logger from '/imports/startup/client/logger';
|
import logger from '/imports/startup/client/logger';
|
||||||
@ -40,7 +40,7 @@ const intlMessages = defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
breakout: PropTypes.objectOf(Object).isRequired,
|
breakout: PropTypes.objectOf(Object).isRequired,
|
||||||
getURL: PropTypes.func.isRequired,
|
getURL: PropTypes.func.isRequired,
|
||||||
mountModal: PropTypes.func.isRequired,
|
mountModal: PropTypes.func.isRequired,
|
||||||
|
@ -224,13 +224,12 @@ class BreakoutRoom extends PureComponent {
|
|||||||
(
|
(
|
||||||
<Button
|
<Button
|
||||||
label={
|
label={
|
||||||
moderatorJoinedAudio
|
stateBreakoutId === breakoutId && joinedAudioOnly
|
||||||
&& stateBreakoutId === breakoutId
|
|
||||||
&& joinedAudioOnly
|
|
||||||
? intl.formatMessage(intlMessages.breakoutReturnAudio)
|
? intl.formatMessage(intlMessages.breakoutReturnAudio)
|
||||||
: intl.formatMessage(intlMessages.breakoutJoinAudio)
|
: intl.formatMessage(intlMessages.breakoutJoinAudio)
|
||||||
}
|
}
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
|
disabled={stateBreakoutId !== breakoutId && joinedAudioOnly}
|
||||||
key={`join-audio-${breakoutId}`}
|
key={`join-audio-${breakoutId}`}
|
||||||
onClick={audioAction}
|
onClick={audioAction}
|
||||||
/>
|
/>
|
||||||
@ -261,7 +260,7 @@ class BreakoutRoom extends PureComponent {
|
|||||||
>
|
>
|
||||||
<div className={styles.content} key={`breakoutRoomList-${breakout.breakoutId}`}>
|
<div className={styles.content} key={`breakoutRoomList-${breakout.breakoutId}`}>
|
||||||
<span aria-hidden>
|
<span aria-hidden>
|
||||||
{intl.formatMessage(intlMessages.breakoutRoom, breakout.sequence.toString())}
|
{intl.formatMessage(intlMessages.breakoutRoom, { 0: breakout.sequence })}
|
||||||
<span className={styles.usersAssignedNumberLabel}>
|
<span className={styles.usersAssignedNumberLabel}>
|
||||||
(
|
(
|
||||||
{breakout.joinedUsers.length}
|
{breakout.joinedUsers.length}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import AudioService from '/imports/ui/components/audio/service';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
play: PropTypes.bool.isRequired,
|
play: PropTypes.bool.isRequired,
|
||||||
@ -8,17 +9,16 @@ const propTypes = {
|
|||||||
class ChatAudioAlert extends Component {
|
class ChatAudioAlert extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(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.handleAudioLoaded = this.handleAudioLoaded.bind(this);
|
||||||
this.playAudio = this.playAudio.bind(this);
|
this.playAudio = this.playAudio.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.audio.addEventListener('loadedmetadata', this.handleAudioLoaded);
|
this.handleAudioLoaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.audio.removeEventListener('loadedmetadata', this.handleAudioLoaded);
|
this.handleAudioLoaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAudioLoaded() {
|
handleAudioLoaded() {
|
||||||
@ -28,7 +28,9 @@ class ChatAudioAlert extends Component {
|
|||||||
playAudio() {
|
playAudio() {
|
||||||
const { play } = this.props;
|
const { play } = this.props;
|
||||||
if (!play) return;
|
if (!play) return;
|
||||||
this.audio.play();
|
AudioService.playAlertSound(`${Meteor.settings.public.app.cdn
|
||||||
|
+ Meteor.settings.public.app.basename}`
|
||||||
|
+ '/resources/sounds/notify.mp3');
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -116,7 +116,7 @@ export default injectIntl(withTracker(({ intl }) => {
|
|||||||
|
|
||||||
const messagesFormated = messagesBeforeWelcomeMsg
|
const messagesFormated = messagesBeforeWelcomeMsg
|
||||||
.concat(welcomeMsg)
|
.concat(welcomeMsg)
|
||||||
.concat((amIModerator && modOnlyMessage) || [])
|
.concat((amIModerator && modOnlyMessage) ? moderatorMsg : [])
|
||||||
.concat(messagesAfterWelcomeMsg);
|
.concat(messagesAfterWelcomeMsg);
|
||||||
|
|
||||||
messages = messagesFormated.sort((a, b) => (a.time - b.time));
|
messages = messagesFormated.sort((a, b) => (a.time - b.time));
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import TextareaAutosize from 'react-autosize-textarea';
|
import TextareaAutosize from 'react-autosize-textarea';
|
||||||
import browser from 'browser-detect';
|
import browser from 'browser-detect';
|
||||||
@ -10,7 +10,7 @@ import { styles } from './styles.scss';
|
|||||||
import Button from '../../button/component';
|
import Button from '../../button/component';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
chatId: PropTypes.string.isRequired,
|
chatId: PropTypes.string.isRequired,
|
||||||
disabled: PropTypes.bool.isRequired,
|
disabled: PropTypes.bool.isRequired,
|
||||||
minMessageLength: PropTypes.number.isRequired,
|
minMessageLength: PropTypes.number.isRequired,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import {
|
import {
|
||||||
defineMessages, injectIntl, intlShape, FormattedMessage,
|
defineMessages, injectIntl, FormattedMessage,
|
||||||
} from 'react-intl';
|
} from 'react-intl';
|
||||||
import browser from 'browser-detect';
|
import browser from 'browser-detect';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@ -8,7 +8,7 @@ import cx from 'classnames';
|
|||||||
import { styles } from '../styles.scss';
|
import { styles } from '../styles.scss';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
typingUsers: PropTypes.arrayOf(Object).isRequired,
|
typingUsers: PropTypes.arrayOf(Object).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ class MessageList extends Component {
|
|||||||
handleScrollUpdate,
|
handleScrollUpdate,
|
||||||
} = this.props;
|
} = 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
|
// 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
|
// the user has sent a message and the message list should scroll to bottom
|
||||||
handleScrollUpdate(1);
|
handleScrollUpdate(1);
|
||||||
@ -159,10 +159,10 @@ class MessageList extends Component {
|
|||||||
|
|
||||||
handleScrollChange(e) {
|
handleScrollChange(e) {
|
||||||
const { scrollArea } = this.state;
|
const { scrollArea } = this.state;
|
||||||
const scrollCursorPosition = e.scrollTop + scrollArea.offsetHeight;
|
const scrollCursorPosition = e.scrollTop + scrollArea?.offsetHeight;
|
||||||
const shouldScrollBottom = e.scrollTop === null
|
const shouldScrollBottom = e.scrollTop === null
|
||||||
|| scrollCursorPosition === scrollArea.scrollHeight
|
|| scrollCursorPosition === scrollArea?.scrollHeight
|
||||||
|| (scrollArea.scrollHeight - scrollCursorPosition < 1);
|
|| (scrollArea?.scrollHeight - scrollCursorPosition < 1);
|
||||||
|
|
||||||
if ((e.scrollTop < this.lastKnowScrollPosition) && !shouldScrollBottom) {
|
if ((e.scrollTop < this.lastKnowScrollPosition) && !shouldScrollBottom) {
|
||||||
this.setState({ shouldScrollToBottom: false });
|
this.setState({ shouldScrollToBottom: false });
|
||||||
|
@ -126,6 +126,7 @@ class MessageListItem extends Component {
|
|||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
color={user.color}
|
color={user.color}
|
||||||
moderator={user.isModerator}
|
moderator={user.isModerator}
|
||||||
|
avatar={user.avatar}
|
||||||
>
|
>
|
||||||
{user.name.toLowerCase().slice(0, 2)}
|
{user.name.toLowerCase().slice(0, 2)}
|
||||||
</UserAvatar>
|
</UserAvatar>
|
||||||
|
@ -50,13 +50,18 @@ const mapGroupMessage = (message) => {
|
|||||||
const sender = Users.findOne({ userId: message.sender },
|
const sender = Users.findOne({ userId: message.sender },
|
||||||
{
|
{
|
||||||
fields: {
|
fields: {
|
||||||
color: 1, role: 1, name: 1, connectionStatus: 1,
|
color: 1,
|
||||||
|
role: 1,
|
||||||
|
name: 1,
|
||||||
|
avatar: 1,
|
||||||
|
connectionStatus: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
color,
|
color,
|
||||||
role,
|
role,
|
||||||
name,
|
name,
|
||||||
|
avatar,
|
||||||
connectionStatus,
|
connectionStatus,
|
||||||
} = sender;
|
} = sender;
|
||||||
|
|
||||||
@ -64,6 +69,7 @@ const mapGroupMessage = (message) => {
|
|||||||
color,
|
color,
|
||||||
isModerator: role === ROLE_MODERATOR,
|
isModerator: role === ROLE_MODERATOR,
|
||||||
name,
|
name,
|
||||||
|
avatar,
|
||||||
isOnline: connectionStatus === CONNECTION_STATUS_ONLINE,
|
isOnline: connectionStatus === CONNECTION_STATUS_ONLINE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -84,8 +84,9 @@ class ConnectionStatusComponent extends PureComponent {
|
|||||||
<div className={styles.left}>
|
<div className={styles.left}>
|
||||||
<div className={styles.avatar}>
|
<div className={styles.avatar}>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
className={styles.icon}
|
className={cx({ [styles.initials]: conn.avatar.length === 0 })}
|
||||||
you={conn.you}
|
you={conn.you}
|
||||||
|
avatar={conn.avatar}
|
||||||
moderator={conn.moderator}
|
moderator={conn.moderator}
|
||||||
color={conn.color}
|
color={conn.color}
|
||||||
>
|
>
|
||||||
|
@ -82,7 +82,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.icon {
|
.initials {
|
||||||
min-width: 2.25rem;
|
min-width: 2.25rem;
|
||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
}
|
}
|
||||||
|
@ -87,6 +87,7 @@ const getConnectionStatus = () => {
|
|||||||
userId: 1,
|
userId: 1,
|
||||||
name: 1,
|
name: 1,
|
||||||
role: 1,
|
role: 1,
|
||||||
|
avatar: 1,
|
||||||
color: 1,
|
color: 1,
|
||||||
connectionStatus: 1,
|
connectionStatus: 1,
|
||||||
},
|
},
|
||||||
@ -96,6 +97,7 @@ const getConnectionStatus = () => {
|
|||||||
userId,
|
userId,
|
||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
|
avatar,
|
||||||
color,
|
color,
|
||||||
connectionStatus: userStatus,
|
connectionStatus: userStatus,
|
||||||
} = user;
|
} = user;
|
||||||
@ -105,6 +107,7 @@ const getConnectionStatus = () => {
|
|||||||
if (status) {
|
if (status) {
|
||||||
result.push({
|
result.push({
|
||||||
name,
|
name,
|
||||||
|
avatar,
|
||||||
offline: userStatus === 'offline',
|
offline: userStatus === 'offline',
|
||||||
you: Auth.userID === userId,
|
you: Auth.userID === userId,
|
||||||
moderator: role === ROLE_MODERATOR,
|
moderator: role === ROLE_MODERATOR,
|
||||||
|
@ -35,10 +35,7 @@ export function publishCursorUpdate(payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initCursorStreamListener() {
|
export function initCursorStreamListener() {
|
||||||
logger.info({
|
logger.info({ logCode: 'init_cursor_stream_listener' }, 'initCursorStreamListener called');
|
||||||
logCode: 'init_cursor_stream_listener',
|
|
||||||
extraInfo: { meetingId: Auth.meetingID, userId: Auth.userID },
|
|
||||||
}, 'initCursorStreamListener called');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We create a promise to add the handlers after a ddp subscription stop.
|
* We create a promise to add the handlers after a ddp subscription stop.
|
||||||
@ -60,9 +57,7 @@ export function initCursorStreamListener() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
startStreamHandlersPromise.then(() => {
|
startStreamHandlersPromise.then(() => {
|
||||||
logger.debug({
|
logger.debug({ logCode: 'cursor_stream_handler_attach' }, 'Attaching handlers for cursor stream');
|
||||||
logCode: 'init_cursor_stream_listener',
|
|
||||||
}, 'initCursorStreamListener called');
|
|
||||||
|
|
||||||
cursorStreamListener.on('message', ({ cursors }) => {
|
cursorStreamListener.on('message', ({ cursors }) => {
|
||||||
Object.keys(cursors).forEach((userId) => {
|
Object.keys(cursors).forEach((userId) => {
|
||||||
|
@ -4,7 +4,7 @@ import { findDOMNode } from 'react-dom';
|
|||||||
import { isMobile } from 'react-device-detect';
|
import { isMobile } from 'react-device-detect';
|
||||||
import TetherComponent from 'react-tether';
|
import TetherComponent from 'react-tether';
|
||||||
import cx from 'classnames';
|
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 Button from '/imports/ui/components/button/component';
|
||||||
import screenreaderTrap from 'makeup-screenreader-trap';
|
import screenreaderTrap from 'makeup-screenreader-trap';
|
||||||
import { styles } from './styles';
|
import { styles } from './styles';
|
||||||
@ -52,7 +52,7 @@ const propTypes = {
|
|||||||
onHide: PropTypes.func,
|
onHide: PropTypes.func,
|
||||||
onShow: PropTypes.func,
|
onShow: PropTypes.func,
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
tethered: PropTypes.bool,
|
tethered: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -300,4 +300,4 @@ class Dropdown extends Component {
|
|||||||
|
|
||||||
Dropdown.propTypes = propTypes;
|
Dropdown.propTypes = propTypes;
|
||||||
Dropdown.defaultProps = defaultProps;
|
Dropdown.defaultProps = defaultProps;
|
||||||
export default injectIntl(Dropdown);
|
export default injectIntl(Dropdown, { forwardRef: true });
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import Icon from '/imports/ui/components/icon/component';
|
import Icon from '/imports/ui/components/icon/component';
|
||||||
@ -18,7 +19,13 @@ const defaultProps = {
|
|||||||
tabIndex: 0,
|
tabIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class DropdownListItem extends Component {
|
const messages = defineMessages({
|
||||||
|
activeAriaLabel: {
|
||||||
|
id: 'app.dropdown.list.item.activeLabel',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
class DropdownListItem extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.labelID = _.uniqueId('dropdown-item-label-');
|
this.labelID = _.uniqueId('dropdown-item-label-');
|
||||||
@ -38,9 +45,12 @@ export default class DropdownListItem extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
id, label, description, children, injectRef, tabIndex, onClick, onKeyDown,
|
id, label, description, children, injectRef, tabIndex, onClick, onKeyDown,
|
||||||
className, style,
|
className, style, intl,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const isSelected = className && className.includes('emojiSelected');
|
||||||
|
const _label = isSelected ? `${label} (${intl.formatMessage(messages.activeAriaLabel)})` : label;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
id={id}
|
id={id}
|
||||||
@ -59,8 +69,8 @@ export default class DropdownListItem extends Component {
|
|||||||
children || this.renderDefault()
|
children || this.renderDefault()
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
label ?
|
label
|
||||||
(<span id={this.labelID} key="labelledby" hidden>{label}</span>)
|
? (<span id={this.labelID} key="labelledby" hidden>{_label}</span>)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
<span id={this.descID} key="describedby" hidden>{description}</span>
|
<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.propTypes = propTypes;
|
||||||
DropdownListItem.defaultProps = defaultProps;
|
DropdownListItem.defaultProps = defaultProps;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 Button from '/imports/ui/components/button/component';
|
||||||
import Modal from '/imports/ui/components/modal/simple/component';
|
import Modal from '/imports/ui/components/modal/simple/component';
|
||||||
import { styles } from './styles';
|
import { styles } from './styles';
|
||||||
@ -26,7 +26,7 @@ const intlMessages = defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
closeModal: PropTypes.func.isRequired,
|
closeModal: PropTypes.func.isRequired,
|
||||||
endMeeting: 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 { withModalMounter } from '/imports/ui/components/modal/service';
|
||||||
import { makeCall } from '/imports/ui/services/api';
|
import { makeCall } from '/imports/ui/services/api';
|
||||||
import EndMeetingComponent from './component';
|
import EndMeetingComponent from './component';
|
||||||
|
import logger from '/imports/startup/client/logger';
|
||||||
|
|
||||||
const EndMeetingContainer = props => <EndMeetingComponent {...props} />;
|
const EndMeetingContainer = props => <EndMeetingComponent {...props} />;
|
||||||
|
|
||||||
@ -12,6 +13,10 @@ export default withModalMounter(withTracker(({ mountModal }) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
endMeeting: () => {
|
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');
|
makeCall('endMeeting');
|
||||||
mountModal(null);
|
mountModal(null);
|
||||||
},
|
},
|
||||||
|
@ -15,6 +15,9 @@ const intlMessages = defineMessages({
|
|||||||
410: {
|
410: {
|
||||||
id: 'app.error.410',
|
id: 'app.error.410',
|
||||||
},
|
},
|
||||||
|
408: {
|
||||||
|
id: 'app.error.408',
|
||||||
|
},
|
||||||
404: {
|
404: {
|
||||||
id: 'app.error.404',
|
id: 'app.error.404',
|
||||||
defaultMessage: 'Not found',
|
defaultMessage: 'Not found',
|
||||||
|
@ -104,6 +104,7 @@ class VideoPlayer extends Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
window.addEventListener('resize', this.resizeListener);
|
window.addEventListener('resize', this.resizeListener);
|
||||||
|
window.addEventListener('layoutSizesSets', this.resizeListener);
|
||||||
window.addEventListener('beforeunload', this.onBeforeUnload);
|
window.addEventListener('beforeunload', this.onBeforeUnload);
|
||||||
|
|
||||||
clearInterval(this.syncInterval);
|
clearInterval(this.syncInterval);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import loadScript from 'load-script';
|
import loadScript from 'load-script';
|
||||||
import React, { Component } from 'react'
|
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';
|
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 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 Button from '/imports/ui/components/button/component';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@ -10,10 +10,14 @@ const intlMessages = defineMessages({
|
|||||||
id: 'app.fullscreenButton.label',
|
id: 'app.fullscreenButton.label',
|
||||||
description: 'Fullscreen label',
|
description: 'Fullscreen label',
|
||||||
},
|
},
|
||||||
|
fullscreenUndoButton: {
|
||||||
|
id: 'app.fullscreenUndoButton.label',
|
||||||
|
description: 'Undo fullscreen label',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
fullscreenRef: PropTypes.instanceOf(Element),
|
fullscreenRef: PropTypes.instanceOf(Element),
|
||||||
dark: PropTypes.bool,
|
dark: PropTypes.bool,
|
||||||
bottom: PropTypes.bool,
|
bottom: PropTypes.bool,
|
||||||
@ -47,10 +51,17 @@ const FullscreenButtonComponent = ({
|
|||||||
}) => {
|
}) => {
|
||||||
if (isIphone) return null;
|
if (isIphone) return null;
|
||||||
|
|
||||||
const formattedLabel = intl.formatMessage(
|
const formattedLabel = (isFullscreen) => {
|
||||||
intlMessages.fullscreenButton,
|
return(isFullscreen ?
|
||||||
({ 0: elementName || '' }),
|
intl.formatMessage(
|
||||||
);
|
intlMessages.fullscreenUndoButton,
|
||||||
|
({ 0: elementName || '' }),
|
||||||
|
) :
|
||||||
|
intl.formatMessage(
|
||||||
|
intlMessages.fullscreenButton,
|
||||||
|
({ 0: elementName || '' }),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
const wrapperClassName = cx({
|
const wrapperClassName = cx({
|
||||||
[styles.wrapper]: true,
|
[styles.wrapper]: true,
|
||||||
@ -67,7 +78,7 @@ const FullscreenButtonComponent = ({
|
|||||||
icon={!isFullscreen ? 'fullscreen' : 'exit_fullscreen'}
|
icon={!isFullscreen ? 'fullscreen' : 'exit_fullscreen'}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleToggleFullScreen(fullscreenRef)}
|
onClick={() => handleToggleFullScreen(fullscreenRef)}
|
||||||
label={formattedLabel}
|
label={formattedLabel(isFullscreen)}
|
||||||
hideLabel
|
hideLabel
|
||||||
className={cx(styles.button, styles.fullScreenButton, className)}
|
className={cx(styles.button, styles.fullScreenButton, className)}
|
||||||
data-test="presentationFullscreenButton"
|
data-test="presentationFullscreenButton"
|
||||||
|
@ -178,6 +178,7 @@ class JoinHandler extends Component {
|
|||||||
const { response } = parseToJson;
|
const { response } = parseToJson;
|
||||||
|
|
||||||
setLogoutURL(response);
|
setLogoutURL(response);
|
||||||
|
logUserInfo();
|
||||||
|
|
||||||
if (response.returncode !== 'FAILED') {
|
if (response.returncode !== 'FAILED') {
|
||||||
await setAuth(response);
|
await setAuth(response);
|
||||||
@ -185,7 +186,6 @@ class JoinHandler extends Component {
|
|||||||
setBannerProps(response);
|
setBannerProps(response);
|
||||||
setLogoURL(response);
|
setLogoURL(response);
|
||||||
setModOnlyMessage(response);
|
setModOnlyMessage(response);
|
||||||
logUserInfo();
|
|
||||||
|
|
||||||
Tracker.autorun(async (cd) => {
|
Tracker.autorun(async (cd) => {
|
||||||
const user = Users.findOne({ userId: Auth.userID, approved: true }, { fields: { _id: 1 } });
|
const user = Users.findOne({ userId: Auth.userID, approved: true }, { fields: { _id: 1 } });
|
||||||
|
@ -312,25 +312,43 @@ class LayoutManager extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (openPanel === 'userlist') {
|
switch (openPanel) {
|
||||||
newChatSize = {
|
case 'userlist': {
|
||||||
width: 0,
|
newChatSize = {
|
||||||
};
|
width: 0,
|
||||||
newBreakoutRoomSize = {
|
};
|
||||||
width: 0,
|
newBreakoutRoomSize = {
|
||||||
};
|
width: 0,
|
||||||
}
|
};
|
||||||
|
break;
|
||||||
if (openPanel === '') {
|
}
|
||||||
newUserListSize = {
|
case 'chat': {
|
||||||
width: 0,
|
newBreakoutRoomSize = {
|
||||||
};
|
width: 0,
|
||||||
newChatSize = {
|
};
|
||||||
width: 0,
|
break;
|
||||||
};
|
}
|
||||||
newBreakoutRoomSize = {
|
case 'breakoutroom': {
|
||||||
width: 0,
|
newChatSize = {
|
||||||
};
|
width: 0,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case '': {
|
||||||
|
newUserListSize = {
|
||||||
|
width: 0,
|
||||||
|
};
|
||||||
|
newChatSize = {
|
||||||
|
width: 0,
|
||||||
|
};
|
||||||
|
newBreakoutRoomSize = {
|
||||||
|
width: 0,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error('Unexpected openPanel value');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -5,70 +5,70 @@ import './styles.css';
|
|||||||
|
|
||||||
|
|
||||||
// currently supported locales.
|
// currently supported locales.
|
||||||
import ar from 'react-intl/locale-data/ar';
|
// import ar from 'react-intl/locale-data/ar';
|
||||||
import bg from 'react-intl/locale-data/bg';
|
// import bg from 'react-intl/locale-data/bg';
|
||||||
import cs from 'react-intl/locale-data/cs';
|
// import cs from 'react-intl/locale-data/cs';
|
||||||
import de from 'react-intl/locale-data/de';
|
// import de from 'react-intl/locale-data/de';
|
||||||
import el from 'react-intl/locale-data/el';
|
// import el from 'react-intl/locale-data/el';
|
||||||
import en from 'react-intl/locale-data/en';
|
// import en from 'react-intl/locale-data/en';
|
||||||
import es from 'react-intl/locale-data/es';
|
// import es from 'react-intl/locale-data/es';
|
||||||
import eu from 'react-intl/locale-data/eu';
|
// import eu from 'react-intl/locale-data/eu';
|
||||||
import fa from 'react-intl/locale-data/fa';
|
// import fa from 'react-intl/locale-data/fa';
|
||||||
import fi from 'react-intl/locale-data/fi';
|
// import fi from 'react-intl/locale-data/fi';
|
||||||
import fr from 'react-intl/locale-data/fr';
|
// import fr from 'react-intl/locale-data/fr';
|
||||||
import he from 'react-intl/locale-data/he';
|
// import he from 'react-intl/locale-data/he';
|
||||||
import hi from 'react-intl/locale-data/hi';
|
// import hi from 'react-intl/locale-data/hi';
|
||||||
import hu from 'react-intl/locale-data/hu';
|
// import hu from 'react-intl/locale-data/hu';
|
||||||
import id from 'react-intl/locale-data/id';
|
// import id from 'react-intl/locale-data/id';
|
||||||
import it from 'react-intl/locale-data/it';
|
// import it from 'react-intl/locale-data/it';
|
||||||
import ja from 'react-intl/locale-data/ja';
|
// import ja from 'react-intl/locale-data/ja';
|
||||||
import km from 'react-intl/locale-data/km';
|
// import km from 'react-intl/locale-data/km';
|
||||||
import pl from 'react-intl/locale-data/pl';
|
// import pl from 'react-intl/locale-data/pl';
|
||||||
import pt from 'react-intl/locale-data/pt';
|
// import pt from 'react-intl/locale-data/pt';
|
||||||
import ru from 'react-intl/locale-data/ru';
|
// import ru from 'react-intl/locale-data/ru';
|
||||||
import sv from 'react-intl/locale-data/sv';
|
// import sv from 'react-intl/locale-data/sv';
|
||||||
import tr from 'react-intl/locale-data/tr';
|
// import tr from 'react-intl/locale-data/tr';
|
||||||
import uk from 'react-intl/locale-data/uk';
|
// import uk from 'react-intl/locale-data/uk';
|
||||||
import vi from 'react-intl/locale-data/vi';
|
// import vi from 'react-intl/locale-data/vi';
|
||||||
import zh from 'react-intl/locale-data/zh';
|
// import zh from 'react-intl/locale-data/zh';
|
||||||
|
|
||||||
// This class is the only component loaded on legacy (unsupported) browsers.
|
// // This class is the only component loaded on legacy (unsupported) browsers.
|
||||||
// What is included here needs to be minimal and carefully considered because some
|
// // What is included here needs to be minimal and carefully considered because some
|
||||||
// things can't be polyfilled.
|
// // things can't be polyfilled.
|
||||||
|
|
||||||
addLocaleData([
|
// addLocaleData([
|
||||||
...ar,
|
// ...ar,
|
||||||
...bg,
|
// ...bg,
|
||||||
...cs,
|
// ...cs,
|
||||||
...de,
|
// ...de,
|
||||||
...el,
|
// ...el,
|
||||||
...en,
|
// ...en,
|
||||||
...es,
|
// ...es,
|
||||||
...eu,
|
// ...eu,
|
||||||
...fa,
|
// ...fa,
|
||||||
...fi,
|
// ...fi,
|
||||||
...fr,
|
// ...fr,
|
||||||
...he,
|
// ...he,
|
||||||
...hi,
|
// ...hi,
|
||||||
...hu,
|
// ...hu,
|
||||||
...id,
|
// ...id,
|
||||||
...it,
|
// ...it,
|
||||||
...ja,
|
// ...ja,
|
||||||
...km,
|
// ...km,
|
||||||
...pl,
|
// ...pl,
|
||||||
...pt,
|
// ...pt,
|
||||||
...ru,
|
// ...ru,
|
||||||
...sv,
|
// ...sv,
|
||||||
...tr,
|
// ...tr,
|
||||||
...uk,
|
// ...uk,
|
||||||
...vi,
|
// ...vi,
|
||||||
...zh,
|
// ...zh,
|
||||||
]);
|
// ]);
|
||||||
|
|
||||||
const FETCHING = 'fetching';
|
const FETCHING = 'fetching';
|
||||||
const FALLBACK = 'fallback';
|
const FALLBACK = 'fallback';
|
||||||
const READY = 'ready';
|
const READY = 'ready';
|
||||||
const supportedBrowsers = ['chrome', 'firefox', 'safari', 'opera', 'edge'];
|
const supportedBrowsers = ['chrome', 'firefox', 'safari', 'opera', 'edge', 'yandex'];
|
||||||
|
|
||||||
export default class Legacy extends Component {
|
export default class Legacy extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
|
import Settings from '/imports/ui/services/settings';
|
||||||
import { isMobile, isIPad13 } from 'react-device-detect';
|
import { isMobile, isIPad13 } from 'react-device-detect';
|
||||||
import WebcamDraggable from './webcam-draggable-overlay/component';
|
import WebcamDraggable from './webcam-draggable-overlay/component';
|
||||||
import { styles } from './styles';
|
import { styles } from './styles';
|
||||||
@ -65,6 +66,9 @@ export default class Media extends Component {
|
|||||||
[styles.containerV]: webcamsPlacement === 'top' || webcamsPlacement === 'bottom' || webcamsPlacement === 'floating',
|
[styles.containerV]: webcamsPlacement === 'top' || webcamsPlacement === 'bottom' || webcamsPlacement === 'floating',
|
||||||
[styles.containerH]: webcamsPlacement === 'left' || webcamsPlacement === 'right',
|
[styles.containerH]: webcamsPlacement === 'left' || webcamsPlacement === 'right',
|
||||||
});
|
});
|
||||||
|
const { viewParticipantsWebcams } = Settings.dataSaving;
|
||||||
|
const showVideo = usersVideo.length > 0 && viewParticipantsWebcams;
|
||||||
|
const fullHeight = !showVideo || (webcamsPlacement === 'floating');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -103,22 +107,18 @@ export default class Media extends Component {
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{
|
{showVideo ? (
|
||||||
usersVideo.length > 0
|
<WebcamDraggable
|
||||||
? (
|
refMediaContainer={this.refContainer}
|
||||||
<WebcamDraggable
|
swapLayout={swapLayout}
|
||||||
refMediaContainer={this.refContainer}
|
singleWebcam={singleWebcam}
|
||||||
swapLayout={swapLayout}
|
usersVideoLenght={usersVideo.length}
|
||||||
singleWebcam={singleWebcam}
|
hideOverlay={hideOverlay}
|
||||||
usersVideoLenght={usersVideo.length}
|
disableVideo={disableVideo}
|
||||||
hideOverlay={hideOverlay}
|
audioModalIsOpen={audioModalIsOpen}
|
||||||
disableVideo={disableVideo}
|
usersVideo={usersVideo}
|
||||||
audioModalIsOpen={audioModalIsOpen}
|
/>
|
||||||
usersVideo={usersVideo}
|
) : null}
|
||||||
/>
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { withTracker } from 'meteor/react-meteor-data';
|
import { withTracker } from 'meteor/react-meteor-data';
|
||||||
import Settings from '/imports/ui/services/settings';
|
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 PropTypes from 'prop-types';
|
||||||
import { Session } from 'meteor/session';
|
import { Session } from 'meteor/session';
|
||||||
import { notify } from '/imports/ui/services/notification';
|
import { notify } from '/imports/ui/services/notification';
|
||||||
@ -22,7 +22,7 @@ const KURENTO_CONFIG = Meteor.settings.public.kurento;
|
|||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
isScreensharing: PropTypes.bool.isRequired,
|
isScreensharing: PropTypes.bool.isRequired,
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
const intlMessages = defineMessages({
|
||||||
@ -49,19 +49,22 @@ const intlMessages = defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
class MediaContainer extends Component {
|
class MediaContainer extends Component {
|
||||||
componentWillMount() {
|
componentDidMount() {
|
||||||
document.addEventListener('installChromeExtension', this.installChromeExtension.bind(this));
|
document.addEventListener('installChromeExtension', this.installChromeExtension.bind(this));
|
||||||
document.addEventListener('screenshareNotSupported', this.screenshareNotSupported.bind(this));
|
document.addEventListener('screenshareNotSupported', this.screenshareNotSupported.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const {
|
const {
|
||||||
isScreensharing,
|
isScreensharing,
|
||||||
intl,
|
intl,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const {
|
||||||
|
isScreensharing: wasScreenSharing,
|
||||||
|
} = prevProps;
|
||||||
|
|
||||||
if (isScreensharing !== nextProps.isScreensharing) {
|
if (isScreensharing !== wasScreenSharing) {
|
||||||
if (nextProps.isScreensharing) {
|
if (wasScreenSharing) {
|
||||||
notify(intl.formatMessage(intlMessages.screenshareStarted), 'info', 'desktop');
|
notify(intl.formatMessage(intlMessages.screenshareStarted), 'info', 'desktop');
|
||||||
} else {
|
} else {
|
||||||
notify(intl.formatMessage(intlMessages.screenshareEnded), 'info', 'desktop');
|
notify(intl.formatMessage(intlMessages.screenshareEnded), 'info', 'desktop');
|
||||||
@ -107,7 +110,7 @@ export default withLayoutConsumer(withModalMounter(withTracker(() => {
|
|||||||
const { dataSaving } = Settings;
|
const { dataSaving } = Settings;
|
||||||
const { viewParticipantsWebcams, viewScreenshare } = dataSaving;
|
const { viewParticipantsWebcams, viewScreenshare } = dataSaving;
|
||||||
const hidePresentation = getFromUserSettings('bbb_hide_presentation', LAYOUT_CONFIG.hidePresentation);
|
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 { current_presentation: hasPresentation } = MediaService.getPresentationInfo();
|
||||||
const data = {
|
const data = {
|
||||||
children: <DefaultContent {...{ autoSwapLayout, hidePresentation }} />,
|
children: <DefaultContent {...{ autoSwapLayout, hidePresentation }} />,
|
||||||
|
@ -4,6 +4,7 @@ import { defineMessages, injectIntl } from 'react-intl';
|
|||||||
import { Meteor } from 'meteor/meteor';
|
import { Meteor } from 'meteor/meteor';
|
||||||
import Auth from '/imports/ui/services/auth';
|
import Auth from '/imports/ui/services/auth';
|
||||||
import Button from '/imports/ui/components/button/component';
|
import Button from '/imports/ui/components/button/component';
|
||||||
|
import allowRedirectToLogoutURL from './service';
|
||||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||||
import logoutRouteHandler from '/imports/utils/logoutRouteHandler';
|
import logoutRouteHandler from '/imports/utils/logoutRouteHandler';
|
||||||
import Rating from './rating/component';
|
import Rating from './rating/component';
|
||||||
@ -11,6 +12,7 @@ import { styles } from './styles';
|
|||||||
import logger from '/imports/startup/client/logger';
|
import logger from '/imports/startup/client/logger';
|
||||||
import Users from '/imports/api/users';
|
import Users from '/imports/api/users';
|
||||||
import AudioManager from '/imports/ui/services/audio-manager';
|
import AudioManager from '/imports/ui/services/audio-manager';
|
||||||
|
import { meetingIsBreakout } from '/imports/ui/components/app/service';
|
||||||
|
|
||||||
const intlMessage = defineMessages({
|
const intlMessage = defineMessages({
|
||||||
410: {
|
410: {
|
||||||
@ -102,6 +104,7 @@ class MeetingEnded extends PureComponent {
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
selected: 0,
|
selected: 0,
|
||||||
|
dispatched: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const user = Users.findOne({ userId: Auth.userID });
|
const user = Users.findOne({ userId: Auth.userID });
|
||||||
@ -110,8 +113,9 @@ class MeetingEnded extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setSelectedStar = this.setSelectedStar.bind(this);
|
this.setSelectedStar = this.setSelectedStar.bind(this);
|
||||||
|
this.confirmRedirect = this.confirmRedirect.bind(this);
|
||||||
this.sendFeedback = this.sendFeedback.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();
|
AudioManager.exitAudio();
|
||||||
Meteor.disconnect();
|
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 {
|
const {
|
||||||
selected,
|
selected,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
if (selected <= 0) {
|
if (selected <= 0) {
|
||||||
logoutRouteHandler();
|
if (meetingIsBreakout()) window.close();
|
||||||
return;
|
if (allowRedirectToLogoutURL()) logoutRouteHandler();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendFeedback() {
|
||||||
|
const {
|
||||||
|
selected,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
const { fullname } = Auth.credentials;
|
const { fullname } = Auth.credentials;
|
||||||
|
|
||||||
@ -156,25 +171,70 @@ class MeetingEnded extends PureComponent {
|
|||||||
// client logger
|
// client logger
|
||||||
logger.info({ logCode: 'feedback_functionality', extraInfo: { feedback: message } }, 'Feedback component');
|
logger.info({ logCode: 'feedback_functionality', extraInfo: { feedback: message } }, 'Feedback component');
|
||||||
|
|
||||||
const FEEDBACK_WAIT_TIME = 500;
|
this.setState({
|
||||||
setTimeout(() => {
|
dispatched: true,
|
||||||
fetch(url, options)
|
});
|
||||||
.then(() => {
|
|
||||||
logoutRouteHandler();
|
if (allowRedirectToLogoutURL()) {
|
||||||
})
|
const FEEDBACK_WAIT_TIME = 500;
|
||||||
.catch(() => {
|
setTimeout(() => {
|
||||||
logoutRouteHandler();
|
fetch(url, options)
|
||||||
});
|
.then(() => {
|
||||||
}, FEEDBACK_WAIT_TIME);
|
logoutRouteHandler();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
logoutRouteHandler();
|
||||||
|
});
|
||||||
|
}, FEEDBACK_WAIT_TIME);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
renderNoFeedback() {
|
||||||
const { code, intl, reason } = this.props;
|
const { intl, code, reason } = this.props;
|
||||||
const { selected } = this.state;
|
|
||||||
|
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;
|
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 (
|
return (
|
||||||
<div className={styles.parent}>
|
<div className={styles.parent}>
|
||||||
@ -186,11 +246,12 @@ class MeetingEnded extends PureComponent {
|
|||||||
}
|
}
|
||||||
</h1>
|
</h1>
|
||||||
<div className={styles.text}>
|
<div className={styles.text}>
|
||||||
{this.shouldShowFeedback
|
{this.shouldShowFeedback()
|
||||||
? intl.formatMessage(intlMessage.subtitle)
|
? intl.formatMessage(intlMessage.subtitle)
|
||||||
: intl.formatMessage(intlMessage.messageEnded)}
|
: intl.formatMessage(intlMessage.messageEnded)}
|
||||||
</div>
|
</div>
|
||||||
{this.shouldShowFeedback ? (
|
|
||||||
|
{this.shouldShowFeedback() ? (
|
||||||
<div data-test="rating">
|
<div data-test="rating">
|
||||||
<Rating
|
<Rating
|
||||||
total="5"
|
total="5"
|
||||||
@ -207,22 +268,35 @@ class MeetingEnded extends PureComponent {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null }
|
) : null }
|
||||||
<Button
|
{noRating && allowRedirectToLogoutURL() ? (
|
||||||
color="primary"
|
<Button
|
||||||
onClick={this.sendFeedback}
|
color="primary"
|
||||||
className={styles.button}
|
onClick={this.confirmRedirect}
|
||||||
label={noRating
|
className={styles.button}
|
||||||
? intl.formatMessage(intlMessage.buttonOkay)
|
label={intl.formatMessage(intlMessage.buttonOkay)}
|
||||||
: intl.formatMessage(intlMessage.sendLabel)}
|
description={intl.formatMessage(intlMessage.confirmDesc)}
|
||||||
description={noRating
|
/>
|
||||||
? intl.formatMessage(intlMessage.confirmDesc)
|
) : null}
|
||||||
: intl.formatMessage(intlMessage.sendDesc)}
|
|
||||||
/>
|
{!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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.shouldShowFeedback()) return this.renderFeedback();
|
||||||
|
return this.renderNoFeedback();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MeetingEnded.propTypes = propTypes;
|
MeetingEnded.propTypes = propTypes;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import { styles } from './styles';
|
import { styles } from './styles';
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
const intlMessages = defineMessages({
|
||||||
@ -16,7 +16,7 @@ const intlMessages = defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
onRate: PropTypes.func.isRequired,
|
onRate: PropTypes.func.isRequired,
|
||||||
total: PropTypes.string.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 humanizeSeconds from '/imports/utils/humanizeSeconds';
|
||||||
import Tooltip from '/imports/ui/components/tooltip/component';
|
import Tooltip from '/imports/ui/components/tooltip/component';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import { styles } from './styles';
|
import { styles } from './styles';
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
const intlMessages = defineMessages({
|
||||||
@ -46,7 +46,7 @@ const intlMessages = defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
amIModerator: PropTypes.bool,
|
amIModerator: PropTypes.bool,
|
||||||
record: PropTypes.bool,
|
record: PropTypes.bool,
|
||||||
recording: PropTypes.bool,
|
recording: PropTypes.bool,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withModalMounter } from '/imports/ui/components/modal/service';
|
import { withModalMounter } from '/imports/ui/components/modal/service';
|
||||||
@ -92,7 +92,7 @@ const intlMessages = defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
handleToggleFullscreen: PropTypes.func.isRequired,
|
handleToggleFullscreen: PropTypes.func.isRequired,
|
||||||
mountModal: PropTypes.func.isRequired,
|
mountModal: PropTypes.func.isRequired,
|
||||||
noIOSFullscreen: PropTypes.bool,
|
noIOSFullscreen: PropTypes.bool,
|
||||||
|
@ -98,27 +98,6 @@ class PanelManager extends Component {
|
|||||||
this.setUserListWidth = this.setUserListWidth.bind(this);
|
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) {
|
componentDidUpdate(prevProps) {
|
||||||
const {
|
const {
|
||||||
userlistWidth,
|
userlistWidth,
|
||||||
|
@ -5,6 +5,7 @@ import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrap
|
|||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { styles } from './styles.scss';
|
import { styles } from './styles.scss';
|
||||||
|
import AudioService from '/imports/ui/components/audio/service';
|
||||||
|
|
||||||
const MAX_INPUT_CHARS = 45;
|
const MAX_INPUT_CHARS = 45;
|
||||||
|
|
||||||
@ -55,8 +56,9 @@ class Polling extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
play() {
|
play() {
|
||||||
this.alert = new Audio(`${Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename}/resources/sounds/Poll.mp3`);
|
AudioService.playAlertSound(`${Meteor.settings.public.app.cdn
|
||||||
this.alert.play();
|
+ Meteor.settings.public.app.basename}`
|
||||||
|
+ '/resources/sounds/Poll.mp3');
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdateResponseInput(e) {
|
handleUpdateResponseInput(e) {
|
||||||
@ -78,7 +80,10 @@ class Polling extends Component {
|
|||||||
typedAns,
|
typedAns,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
if (!poll) return null;
|
||||||
|
|
||||||
const { stackOptions, answers, question } = poll;
|
const { stackOptions, answers, question } = poll;
|
||||||
|
|
||||||
const pollAnswerStyles = {
|
const pollAnswerStyles = {
|
||||||
[styles.pollingAnswers]: true,
|
[styles.pollingAnswers]: true,
|
||||||
[styles.removeColumns]: answers.length === 1,
|
[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 WhiteboardOverlayContainer from '/imports/ui/components/whiteboard/whiteboard-overlay/container';
|
||||||
import WhiteboardToolbarContainer from '/imports/ui/components/whiteboard/whiteboard-toolbar/container';
|
import WhiteboardToolbarContainer from '/imports/ui/components/whiteboard/whiteboard-toolbar/container';
|
||||||
import { HUNDRED_PERCENT, MAX_PERCENT } from '/imports/utils/slideCalcUtils';
|
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 { toast } from 'react-toastify';
|
||||||
import PresentationToolbarContainer from './presentation-toolbar/container';
|
import PresentationToolbarContainer from './presentation-toolbar/container';
|
||||||
import CursorWrapperContainer from './cursor/cursor-wrapper-container/container';
|
import CursorWrapperContainer from './cursor/cursor-wrapper-container/container';
|
||||||
@ -854,7 +854,7 @@ class PresentationArea extends PureComponent {
|
|||||||
export default injectIntl(withDraggableConsumer(withLayoutConsumer(PresentationArea)));
|
export default injectIntl(withDraggableConsumer(withLayoutConsumer(PresentationArea)));
|
||||||
|
|
||||||
PresentationArea.propTypes = {
|
PresentationArea.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
podId: PropTypes.string.isRequired,
|
podId: PropTypes.string.isRequired,
|
||||||
// Defines a boolean value to detect whether a current user is a presenter
|
// Defines a boolean value to detect whether a current user is a presenter
|
||||||
userIsPresenter: PropTypes.bool.isRequired,
|
userIsPresenter: PropTypes.bool.isRequired,
|
||||||
|
@ -38,7 +38,7 @@ export default class Cursor extends Component {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getScaledSizes(props) {
|
static getScaledSizes(props, state) {
|
||||||
// TODO: This might need to change for the use case of fit-to-width portrait
|
// 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.
|
// slides in non-presenter view. Some elements are still shrinking.
|
||||||
const scaleFactor = props.widthRatio / props.physicalWidthRatio;
|
const scaleFactor = props.widthRatio / props.physicalWidthRatio;
|
||||||
@ -58,14 +58,25 @@ export default class Cursor extends Component {
|
|||||||
yOffset: props.cursorLabelBox.yOffset * scaleFactor,
|
yOffset: props.cursorLabelBox.yOffset * scaleFactor,
|
||||||
// making width and height a little bit larger than the size of the text
|
// 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
|
// received from BBox, so that the text didn't touch the border
|
||||||
width: (props.labelBoxWidth + 3) * scaleFactor,
|
width: (state.labelBoxWidth + 3) * scaleFactor,
|
||||||
height: (props.labelBoxHeight + 3) * scaleFactor,
|
height: (state.labelBoxHeight + 3) * scaleFactor,
|
||||||
strokeWidth: props.cursorLabelBox.labelBoxStrokeWidth * 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 {
|
const {
|
||||||
cursorX,
|
cursorX,
|
||||||
cursorY,
|
cursorY,
|
||||||
@ -75,8 +86,9 @@ export default class Cursor extends Component {
|
|||||||
isMultiUser,
|
isMultiUser,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// setting the initial cursor info
|
this.setState({
|
||||||
this.scaledSizes = Cursor.getScaledSizes(this.props);
|
scaledSizes: Cursor.getScaledSizes(this.props, this.state),
|
||||||
|
});
|
||||||
this.cursorCoordinate = Cursor.getCursorCoordinates(
|
this.cursorCoordinate = Cursor.getCursorCoordinates(
|
||||||
cursorX,
|
cursorX,
|
||||||
cursorY,
|
cursorY,
|
||||||
@ -87,66 +99,83 @@ export default class Cursor extends Component {
|
|||||||
const { fill, displayLabel } = Cursor.getFillAndLabel(presenter, isMultiUser);
|
const { fill, displayLabel } = Cursor.getFillAndLabel(presenter, isMultiUser);
|
||||||
this.fill = fill;
|
this.fill = fill;
|
||||||
this.displayLabel = displayLabel;
|
this.displayLabel = displayLabel;
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
// we need to find the BBox of the text, so that we could set a proper border box arount it
|
// 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 {
|
const {
|
||||||
presenter,
|
presenter,
|
||||||
isMultiUser,
|
isMultiUser,
|
||||||
widthRatio,
|
widthRatio,
|
||||||
physicalWidthRatio,
|
physicalWidthRatio,
|
||||||
labelBoxWidth,
|
|
||||||
labelBoxHeight,
|
|
||||||
cursorX,
|
cursorX,
|
||||||
cursorY,
|
cursorY,
|
||||||
|
slideWidth,
|
||||||
|
slideHeight,
|
||||||
} = this.props;
|
} = 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(
|
const { fill, displayLabel } = Cursor.getFillAndLabel(
|
||||||
nextProps.presenter,
|
presenter,
|
||||||
nextProps.isMultiUser,
|
isMultiUser,
|
||||||
);
|
);
|
||||||
this.displayLabel = displayLabel;
|
this.displayLabel = displayLabel;
|
||||||
this.fill = fill;
|
this.fill = fill;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((widthRatio !== nextProps.widthRatio
|
if ((widthRatio !== prevProps.widthRatio
|
||||||
|| physicalWidthRatio !== nextProps.physicalWidthRatio)
|
|| physicalWidthRatio !== prevProps.physicalWidthRatio)
|
||||||
|| (labelBoxWidth !== nextProps.labelBoxWidth
|
|| (labelBoxWidth !== prevLabelBoxWidth
|
||||||
|| labelBoxHeight !== nextProps.labelBoxHeight)) {
|
|| labelBoxHeight !== prevLabelBoxHeight)) {
|
||||||
this.scaledSizes = Cursor.getScaledSizes(nextProps);
|
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(
|
const cursorCoordinate = Cursor.getCursorCoordinates(
|
||||||
nextProps.cursorX,
|
cursorX,
|
||||||
nextProps.cursorY,
|
cursorY,
|
||||||
nextProps.slideWidth,
|
slideWidth,
|
||||||
nextProps.slideHeight,
|
slideHeight,
|
||||||
);
|
);
|
||||||
this.cursorCoordinate = cursorCoordinate;
|
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
|
// this function retrieves the text node, measures its BBox and sets the size for the outer box
|
||||||
calculateCursorLabelBoxDimensions() {
|
calculateCursorLabelBoxDimensions() {
|
||||||
const {
|
|
||||||
setLabelBoxDimensions,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
let labelBoxWidth = 0;
|
let labelBoxWidth = 0;
|
||||||
let labelBoxHeight = 0;
|
let labelBoxHeight = 0;
|
||||||
|
|
||||||
if (this.cursorLabelRef) {
|
if (this.cursorLabelRef) {
|
||||||
const { width, height } = this.cursorLabelRef.getBBox();
|
const { width, height } = this.cursorLabelRef.getBBox();
|
||||||
const { widthRatio, physicalWidthRatio, cursorLabelBox } = this.props;
|
const { widthRatio, physicalWidthRatio, cursorLabelBox } = this.props;
|
||||||
labelBoxWidth = Cursor.invertScale(width, widthRatio, physicalWidthRatio);
|
labelBoxWidth = Cursor.invertScale(width, widthRatio, physicalWidthRatio);
|
||||||
labelBoxHeight = Cursor.invertScale(height, 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 the width of the text node is bigger than the maxSize - set the width to maxWidth
|
||||||
if (labelBoxWidth > cursorLabelBox.maxWidth) {
|
if (labelBoxWidth > cursorLabelBox.maxWidth) {
|
||||||
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
|
// updating labelBoxWidth and labelBoxHeight in the container, which then passes it down here
|
||||||
setLabelBoxDimensions(labelBoxWidth, labelBoxHeight);
|
this.setLabelBoxDimensions(labelBoxWidth, labelBoxHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {
|
||||||
|
scaledSizes,
|
||||||
|
} = this.state;
|
||||||
const {
|
const {
|
||||||
cursorId,
|
cursorId,
|
||||||
userName,
|
userName,
|
||||||
isRTL,
|
isRTL,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
cursorCoordinate,
|
cursorCoordinate,
|
||||||
fill,
|
fill,
|
||||||
} = this;
|
} = this;
|
||||||
|
|
||||||
|
if (!scaledSizes) return null;
|
||||||
const {
|
const {
|
||||||
cursorLabelBox,
|
cursorLabelBox,
|
||||||
cursorLabelText,
|
cursorLabelText,
|
||||||
finalRadius,
|
finalRadius,
|
||||||
} = this.scaledSizes;
|
} = scaledSizes;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
x,
|
x,
|
||||||
@ -191,7 +224,7 @@ export default class Cursor extends Component {
|
|||||||
<circle
|
<circle
|
||||||
cx={x}
|
cx={x}
|
||||||
cy={y}
|
cy={y}
|
||||||
r={finalRadius === Infinity ? 0 : finalRadius}
|
r={finalRadius}
|
||||||
fill={fill}
|
fill={fill}
|
||||||
fillOpacity="0.6"
|
fillOpacity="0.6"
|
||||||
/>
|
/>
|
||||||
@ -292,17 +325,6 @@ Cursor.propTypes = {
|
|||||||
fontSize: PropTypes.number.isRequired,
|
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
|
// Defines the direction the client text should be displayed
|
||||||
isRTL: PropTypes.bool.isRequired,
|
isRTL: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -6,33 +6,15 @@ import Cursor from './component';
|
|||||||
|
|
||||||
|
|
||||||
class CursorContainer extends 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() {
|
render() {
|
||||||
const { cursorX, cursorY } = this.props;
|
const { cursorX, cursorY } = this.props;
|
||||||
const { labelBoxWidth, labelBoxHeight } = this.state;
|
|
||||||
|
|
||||||
if (cursorX > 0 && cursorY > 0) {
|
if (cursorX > 0 && cursorY > 0) {
|
||||||
return (
|
return (
|
||||||
<Cursor
|
<Cursor
|
||||||
cursorX={cursorX}
|
cursorX={cursorX}
|
||||||
cursorY={cursorY}
|
cursorY={cursorY}
|
||||||
labelBoxWidth={labelBoxWidth}
|
|
||||||
labelBoxHeight={labelBoxHeight}
|
|
||||||
setLabelBoxDimensions={this.setLabelBoxDimensions}
|
setLabelBoxDimensions={this.setLabelBoxDimensions}
|
||||||
{...this.props}
|
{...this.props}
|
||||||
/>
|
/>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
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 Button from '/imports/ui/components/button/component';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@ -13,7 +13,7 @@ const intlMessages = defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
handleDownloadPresentation: PropTypes.func.isRequired,
|
handleDownloadPresentation: PropTypes.func.isRequired,
|
||||||
dark: PropTypes.bool,
|
dark: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,7 @@ import ZoomTool from './zoom-tool/component';
|
|||||||
import FullscreenButtonContainer from '../../fullscreen-button/container';
|
import FullscreenButtonContainer from '../../fullscreen-button/container';
|
||||||
import TooltipContainer from '/imports/ui/components/tooltip/container';
|
import TooltipContainer from '/imports/ui/components/tooltip/container';
|
||||||
import QuickPollDropdownContainer from '/imports/ui/components/actions-bar/quick-poll-dropdown/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';
|
import KEY_CODES from '/imports/utils/keyCodes';
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
const intlMessages = defineMessages({
|
||||||
@ -101,6 +102,7 @@ class PresentationToolbar extends PureComponent {
|
|||||||
switchSlide(event) {
|
switchSlide(event) {
|
||||||
const { target, which } = event;
|
const { target, which } = event;
|
||||||
const isBody = target.nodeName === 'BODY';
|
const isBody = target.nodeName === 'BODY';
|
||||||
|
const { fullscreenRef } = this.props;
|
||||||
|
|
||||||
if (isBody) {
|
if (isBody) {
|
||||||
switch (which) {
|
switch (which) {
|
||||||
@ -112,6 +114,9 @@ class PresentationToolbar extends PureComponent {
|
|||||||
case KEY_CODES.PAGE_DOWN:
|
case KEY_CODES.PAGE_DOWN:
|
||||||
this.nextSlideHandler();
|
this.nextSlideHandler();
|
||||||
break;
|
break;
|
||||||
|
case KEY_CODES.ENTER:
|
||||||
|
FullscreenService.toggleFullScreen(fullscreenRef);
|
||||||
|
break;
|
||||||
default:
|
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