Merge branch 'develop' of https://github.com/bigbluebutton/bigbluebutton into u23-recsa

This commit is contained in:
germanocaumo 2021-03-23 14:47:20 -03:00
commit 95fabfe8c2
233 changed files with 3007 additions and 2451 deletions

View File

@ -49,7 +49,6 @@ trait SystemConfiguration {
lazy val endMeetingWhenNoMoreAuthedUsers = Try(config.getBoolean("apps.endMeetingWhenNoMoreAuthedUsers")).getOrElse(false)
lazy val endMeetingWhenNoMoreAuthedUsersAfterMinutes = Try(config.getInt("apps.endMeetingWhenNoMoreAuthedUsersAfterMinutes")).getOrElse(2)
lazy val multiUserWhiteboardDefault = Try(config.getBoolean("whiteboard.multiUserDefault")).getOrElse(false)
// Redis server configuration
lazy val redisHost = Try(config.getString("redis.host")).getOrElse("127.0.0.1")

View File

@ -135,10 +135,6 @@ class BigBlueButtonActor(
RunningMeetings.add(meetings, m)
// Send new 2x message
val msgEvent = MsgBuilder.buildMeetingCreatedEvtMsg(m.props.meetingProp.intId, msg.body.props)
m.outMsgRouter.send(msgEvent)
case Some(m) =>
log.info("Meeting already created. meetingID={}", msg.body.props.meetingProp.intId)
// do nothing

View File

@ -10,6 +10,7 @@ object ScreenshareModel {
status.voiceConf = ""
status.screenshareConf = ""
status.timestamp = ""
status.hasAudio = false
}
def getScreenshareStarted(status: ScreenshareModel): Boolean = {
@ -79,6 +80,14 @@ object ScreenshareModel {
def getTimestamp(status: ScreenshareModel): String = {
status.timestamp
}
def setHasAudio(status: ScreenshareModel, hasAudio: Boolean): Unit = {
status.hasAudio = hasAudio
}
def getHasAudio(status: ScreenshareModel): Boolean = {
status.hasAudio
}
}
class ScreenshareModel {
@ -90,4 +99,5 @@ class ScreenshareModel {
private var voiceConf: String = ""
private var screenshareConf: String = ""
private var timestamp: String = ""
private var hasAudio = false
}

View File

@ -25,7 +25,14 @@ class WhiteboardModel extends SystemConfiguration {
}
private def createWhiteboard(wbId: String): Whiteboard = {
new Whiteboard(wbId, multiUserWhiteboardDefault, System.currentTimeMillis(), 0, new HashMap[String, List[AnnotationVO]]())
new Whiteboard(
wbId,
Array.empty[String],
Array.empty[String],
System.currentTimeMillis(),
0,
new HashMap[String, List[AnnotationVO]]()
)
}
private def getAnnotationsByUserId(wb: Whiteboard, id: String): List[AnnotationVO] = {
@ -184,7 +191,7 @@ class WhiteboardModel extends SystemConfiguration {
if (hasWhiteboard(wbId)) {
val wb = getWhiteboard(wbId)
if (wb.multiUser) {
if (wb.multiUser.contains(userId)) {
if (wb.annotationsMap.contains(userId)) {
val newWb = wb.copy(annotationsMap = wb.annotationsMap - userId)
saveWhiteboard(newWb)
@ -205,7 +212,7 @@ class WhiteboardModel extends SystemConfiguration {
var last: Option[AnnotationVO] = None
val wb = getWhiteboard(wbId)
if (wb.multiUser) {
if (wb.multiUser.contains(userId)) {
val usersAnnotations = getAnnotationsByUserId(wb, userId)
//not empty and head id equals annotation id
@ -234,13 +241,21 @@ class WhiteboardModel extends SystemConfiguration {
wb.copy(annotationsMap = newAnnotationsMap)
}
def modifyWhiteboardAccess(wbId: String, multiUser: Boolean) {
def modifyWhiteboardAccess(wbId: String, multiUser: Array[String]) {
val wb = getWhiteboard(wbId)
val newWb = wb.copy(multiUser = multiUser, changedModeOn = System.currentTimeMillis())
val newWb = wb.copy(multiUser = multiUser, oldMultiUser = wb.multiUser, changedModeOn = System.currentTimeMillis())
saveWhiteboard(newWb)
}
def getWhiteboardAccess(wbId: String): Boolean = getWhiteboard(wbId).multiUser
def getWhiteboardAccess(wbId: String): Array[String] = getWhiteboard(wbId).multiUser
def hasWhiteboardAccess(wbId: String, userId: String): Boolean = {
val wb = getWhiteboard(wbId)
wb.multiUser.contains(userId) || {
val lastChange = System.currentTimeMillis() - wb.changedModeOn
wb.oldMultiUser.contains(userId) && lastChange < 5000
}
}
def getChangedModeOn(wbId: String): Long = getWhiteboard(wbId).changedModeOn

View File

@ -25,9 +25,10 @@ trait GetScreenshareStatusReqMsgHdlr {
val vidWidth = ScreenshareModel.getScreenshareVideoWidth(liveMeeting.screenshareModel)
val vidHeight = ScreenshareModel.getScreenshareVideoHeight(liveMeeting.screenshareModel)
val timestamp = ScreenshareModel.getTimestamp(liveMeeting.screenshareModel)
val hasAudio = ScreenshareModel.getHasAudio(liveMeeting.screenshareModel)
val body = ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf, screenshareConf,
stream, vidWidth, vidHeight, timestamp)
stream, vidWidth, vidHeight, timestamp, hasAudio)
val event = ScreenshareRtmpBroadcastStartedEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}

View File

@ -10,7 +10,7 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr {
def handle(msg: ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
def broadcastEvent(voiceConf: String, screenshareConf: String, stream: String, vidWidth: Int, vidHeight: Int,
timestamp: String): BbbCommonEnvCoreMsg = {
timestamp: String, hasAudio: Boolean): BbbCommonEnvCoreMsg = {
val routing = Routing.addMsgToClientRouting(
MessageTypes.BROADCAST_TO_MEETING,
@ -23,7 +23,7 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr {
)
val body = ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf, screenshareConf,
stream, vidWidth, vidHeight, timestamp)
stream, vidWidth, vidHeight, timestamp, hasAudio)
val event = ScreenshareRtmpBroadcastStartedEvtMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
@ -41,12 +41,13 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr {
ScreenshareModel.setVoiceConf(liveMeeting.screenshareModel, msg.body.voiceConf)
ScreenshareModel.setScreenshareConf(liveMeeting.screenshareModel, msg.body.screenshareConf)
ScreenshareModel.setTimestamp(liveMeeting.screenshareModel, msg.body.timestamp)
ScreenshareModel.setHasAudio(liveMeeting.screenshareModel, msg.body.hasAudio)
log.info("START broadcast ALLOWED when isBroadcastingRTMP=false")
// Notify viewers in the meeting that there's an rtmp stream to view
val msgEvent = broadcastEvent(msg.body.voiceConf, msg.body.screenshareConf, msg.body.stream,
msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp)
msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp, msg.body.hasAudio)
bus.outGW.send(msgEvent)
} else {
log.info("START broadcast NOT ALLOWED when isBroadcastingRTMP=true")

View File

@ -21,7 +21,7 @@ trait ClearWhiteboardPubMsgHdlr extends RightsManagementTrait {
bus.outGW.send(msgEvent)
}
if (filterWhiteboardMessage(msg.body.whiteboardId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to clear the whiteboard."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)

View File

@ -9,7 +9,7 @@ trait GetWhiteboardAnnotationsReqMsgHdlr {
def handle(msg: GetWhiteboardAnnotationsReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
def broadcastEvent(msg: GetWhiteboardAnnotationsReqMsg, history: Array[AnnotationVO], multiUser: Boolean): Unit = {
def broadcastEvent(msg: GetWhiteboardAnnotationsReqMsg, history: Array[AnnotationVO], multiUser: Array[String]): Unit = {
val routing = Routing.addMsgToHtml5InstanceIdRouting(liveMeeting.props.meetingProp.intId, liveMeeting.props.systemProps.html5InstanceId.toString)
val envelope = BbbCoreEnvelope(GetWhiteboardAnnotationsRespMsg.NAME, routing)

View File

@ -21,7 +21,7 @@ trait ModifyWhiteboardAccessPubMsgHdlr extends RightsManagementTrait {
bus.outGW.send(msgEvent)
}
if (filterWhiteboardMessage(msg.body.whiteboardId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to modify access to the whiteboard."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)

View File

@ -21,7 +21,7 @@ trait SendCursorPositionPubMsgHdlr extends RightsManagementTrait {
bus.outGW.send(msgEvent)
}
if (filterWhiteboardMessage(msg.body.whiteboardId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to send your cursor position."
// Just drop messages as these might be delayed messages from multi-user whiteboard. Don't want to

View File

@ -71,7 +71,7 @@ trait SendWhiteboardAnnotationPubMsgHdlr extends RightsManagementTrait {
WhiteboardKeyUtil.DRAW_UPDATE_STATUS == annotation.status)
}
if (!excludedWbMsg(msg.body.annotation) && filterWhiteboardMessage(msg.body.annotation.wbId, liveMeeting) && permissionFailed(
if (!excludedWbMsg(msg.body.annotation) && filterWhiteboardMessage(msg.body.annotation.wbId, msg.header.userId, liveMeeting) && permissionFailed(
PermissionCheck.GUEST_LEVEL,
PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId
)) {

View File

@ -21,7 +21,7 @@ trait UndoWhiteboardPubMsgHdlr extends RightsManagementTrait {
bus.outGW.send(msgEvent)
}
if (filterWhiteboardMessage(msg.body.whiteboardId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to undo an annotation."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)

View File

@ -5,8 +5,16 @@ import akka.event.Logging
import org.bigbluebutton.core.running.LiveMeeting
import org.bigbluebutton.common2.msgs.AnnotationVO
import org.bigbluebutton.core.apps.WhiteboardKeyUtil
import scala.collection.immutable.{ Map, List }
case class Whiteboard(id: String, multiUser: Boolean, changedModeOn: Long, annotationCount: Int, annotationsMap: scala.collection.immutable.Map[String, scala.collection.immutable.List[AnnotationVO]])
case class Whiteboard(
id: String,
multiUser: Array[String],
oldMultiUser: Array[String],
changedModeOn: Long,
annotationCount: Int,
annotationsMap: Map[String, List[AnnotationVO]]
)
class WhiteboardApp2x(implicit val context: ActorContext)
extends SendCursorPositionPubMsgHdlr
@ -56,18 +64,18 @@ class WhiteboardApp2x(implicit val context: ActorContext)
liveMeeting.wbModel.undoWhiteboard(whiteboardId, requesterId)
}
def getWhiteboardAccess(whiteboardId: String, liveMeeting: LiveMeeting): Boolean = {
def getWhiteboardAccess(whiteboardId: String, liveMeeting: LiveMeeting): Array[String] = {
liveMeeting.wbModel.getWhiteboardAccess(whiteboardId)
}
def modifyWhiteboardAccess(whiteboardId: String, multiUser: Boolean, liveMeeting: LiveMeeting) {
def modifyWhiteboardAccess(whiteboardId: String, multiUser: Array[String], liveMeeting: LiveMeeting) {
liveMeeting.wbModel.modifyWhiteboardAccess(whiteboardId, multiUser)
}
def filterWhiteboardMessage(whiteboardId: String, liveMeeting: LiveMeeting): Boolean = {
def filterWhiteboardMessage(whiteboardId: String, userId: String, liveMeeting: LiveMeeting): Boolean = {
// Need to check if the wb mode change from multi-user to single-user. Give 5sec allowance to
// allow delayed messages to be handled as clients may have been sending messages while the wb
// mode was changed. (ralam nov 22, 2017)
if (!liveMeeting.wbModel.getWhiteboardAccess(whiteboardId) && liveMeeting.wbModel.getChangedModeOn(whiteboardId) > 5000) true else false
!liveMeeting.wbModel.hasWhiteboardAccess(whiteboardId, userId)
}
}

View File

@ -156,6 +156,10 @@ class MeetingActor(
var lastRttTestSentOn = System.currentTimeMillis()
// Send new 2x message
val msgEvent = MsgBuilder.buildMeetingCreatedEvtMsg(liveMeeting.props.meetingProp.intId, liveMeeting.props)
outGW.send(msgEvent)
// Create a default public group chat
state = groupChatApp.handleCreateDefaultPublicGroupChat(state, liveMeeting, msgBus)

View File

@ -95,8 +95,3 @@ recording {
# set zero to disable chapter break
chapterBreakLengthInMinutes = 0
}
whiteboard {
multiUserDefault = false
}

View File

@ -83,7 +83,7 @@ public class FreeswitchConferenceEventListener implements ConferenceEventListene
if (((ScreenshareRTMPBroadcastEvent) event).getBroadcast()) {
ScreenshareRTMPBroadcastEvent evt = (ScreenshareRTMPBroadcastEvent) event;
vcs.deskShareRTMPBroadcastStarted(evt.getRoom(), evt.getBroadcastingStreamUrl(),
evt.getVideoWidth(), evt.getVideoHeight(), evt.getTimestamp());
evt.getVideoWidth(), evt.getVideoHeight(), evt.getTimestamp(), evt.getHasAudio());
} else {
ScreenshareRTMPBroadcastEvent evt = (ScreenshareRTMPBroadcastEvent) event;
vcs.deskShareRTMPBroadcastStopped(evt.getRoom(), evt.getBroadcastingStreamUrl(),

View File

@ -55,7 +55,8 @@ public interface IVoiceConferenceService {
String streamname,
Integer videoWidth,
Integer videoHeight,
String timestamp);
String timestamp,
boolean hasAudio);
void deskShareRTMPBroadcastStopped(String room,
String streamname,

View File

@ -25,6 +25,7 @@ public class ScreenshareRTMPBroadcastEvent extends VoiceConferenceEvent {
private String streamUrl;
private Integer vw;
private Integer vh;
private boolean hasAudio;
private final String SCREENSHARE_SUFFIX = "-SCREENSHARE";
@ -46,6 +47,10 @@ public class ScreenshareRTMPBroadcastEvent extends VoiceConferenceEvent {
public void setVideoHeight(Integer vh) {this.vh = vh;}
public void setHasAudio(boolean hasAudio) {
this.hasAudio = hasAudio;
}
public Integer getVideoHeight() {return vh;}
public Integer getVideoWidth() {return vw;}
@ -65,4 +70,8 @@ public class ScreenshareRTMPBroadcastEvent extends VoiceConferenceEvent {
public boolean getBroadcast() {
return broadcast;
}
public boolean getHasAudio() {
return hasAudio;
}
}

View File

@ -237,13 +237,14 @@ class VoiceConferenceService(healthz: HealthzService,
streamname: String,
vw: java.lang.Integer,
vh: java.lang.Integer,
timestamp: String
timestamp: String,
hasAudio: Boolean
) {
val header = BbbCoreVoiceConfHeader(ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg.NAME, voiceConfId)
val body = ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgBody(voiceConf = voiceConfId, screenshareConf = voiceConfId,
stream = streamname, vidWidth = vw.intValue(), vidHeight = vh.intValue(),
timestamp)
timestamp, hasAudio)
val envelope = BbbCoreEnvelope(ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg.NAME, Map("voiceConf" -> voiceConfId))
val msg = new ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg(header, body)

View File

@ -24,7 +24,7 @@ case class ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg(
extends VoiceStandardMsg
case class ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgBody(voiceConf: String, screenshareConf: String,
stream: String, vidWidth: Int, vidHeight: Int,
timestamp: String)
timestamp: String, hasAudio: Boolean)
/**
* Sent to clients to notify them of an RTMP stream starting.
@ -37,7 +37,7 @@ case class ScreenshareRtmpBroadcastStartedEvtMsg(
extends BbbCoreMsg
case class ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf: String, screenshareConf: String,
stream: String, vidWidth: Int, vidHeight: Int,
timestamp: String)
timestamp: String, hasAudio: Boolean)
/**
* Send by FS that RTMP stream has stopped.

View File

@ -18,7 +18,7 @@ case class GetWhiteboardAnnotationsReqMsgBody(whiteboardId: String)
object ModifyWhiteboardAccessPubMsg { val NAME = "ModifyWhiteboardAccessPubMsg" }
case class ModifyWhiteboardAccessPubMsg(header: BbbClientMsgHeader, body: ModifyWhiteboardAccessPubMsgBody) extends StandardMsg
case class ModifyWhiteboardAccessPubMsgBody(whiteboardId: String, multiUser: Boolean)
case class ModifyWhiteboardAccessPubMsgBody(whiteboardId: String, multiUser: Array[String])
object SendCursorPositionPubMsg { val NAME = "SendCursorPositionPubMsg" }
case class SendCursorPositionPubMsg(header: BbbClientMsgHeader, body: SendCursorPositionPubMsgBody) extends StandardMsg
@ -48,11 +48,11 @@ case class ClearWhiteboardEvtMsgBody(whiteboardId: String, userId: String, fullC
object GetWhiteboardAnnotationsRespMsg { val NAME = "GetWhiteboardAnnotationsRespMsg" }
case class GetWhiteboardAnnotationsRespMsg(header: BbbClientMsgHeader, body: GetWhiteboardAnnotationsRespMsgBody) extends BbbCoreMsg
case class GetWhiteboardAnnotationsRespMsgBody(whiteboardId: String, annotations: Array[AnnotationVO], multiUser: Boolean)
case class GetWhiteboardAnnotationsRespMsgBody(whiteboardId: String, annotations: Array[AnnotationVO], multiUser: Array[String])
object ModifyWhiteboardAccessEvtMsg { val NAME = "ModifyWhiteboardAccessEvtMsg" }
case class ModifyWhiteboardAccessEvtMsg(header: BbbClientMsgHeader, body: ModifyWhiteboardAccessEvtMsgBody) extends BbbCoreMsg
case class ModifyWhiteboardAccessEvtMsgBody(whiteboardId: String, multiUser: Boolean)
case class ModifyWhiteboardAccessEvtMsgBody(whiteboardId: String, multiUser: Array[String])
object SendCursorPositionEvtMsg { val NAME = "SendCursorPositionEvtMsg" }
case class SendCursorPositionEvtMsg(header: BbbClientMsgHeader, body: SendCursorPositionEvtMsgBody) extends BbbCoreMsg

View File

@ -43,6 +43,7 @@ public class ApiParams {
public static final String MODERATOR_ONLY_MESSAGE = "moderatorOnlyMessage";
public static final String MODERATOR_PW = "moderatorPW";
public static final String MUTE_ON_START = "muteOnStart";
public static final String MEETING_KEEP_EVENTS = "meetingKeepEvents";
public static final String ALLOW_MODS_TO_UNMUTE_USERS = "allowModsToUnmuteUsers";
public static final String NAME = "name";
public static final String PARENT_MEETING_ID = "parentMeetingID";

View File

@ -120,7 +120,6 @@ public class MeetingService implements MessageListener {
private RedisStorageService storeService;
private CallbackUrlService callbackUrlService;
private HTML5LoadBalancingService html5LoadBalancingService;
private boolean keepEvents;
private long usersTimeout;
private long enteredUsersTimeout;
@ -356,7 +355,7 @@ public class MeetingService implements MessageListener {
}
private boolean storeEvents(Meeting m) {
return m.isRecord() || keepEvents;
return m.isRecord() || m.getMeetingKeepEvents();
}
private void handleCreateMeeting(Meeting m) {
@ -404,6 +403,8 @@ public class MeetingService implements MessageListener {
logData.put("logCode", "create_meeting");
logData.put("description", "Create meeting.");
logData.put("meetingKeepEvents", m.getMeetingKeepEvents());
Gson gson = new Gson();
String logStr = gson.toJson(logData);
@ -417,7 +418,7 @@ public class MeetingService implements MessageListener {
m.getDialNumber(), m.getMaxUsers(),
m.getMeetingExpireIfNoUserJoinedInMinutes(), m.getmeetingExpireWhenLastUserLeftInMinutes(),
m.getUserInactivityInspectTimerInMinutes(), m.getUserInactivityThresholdInMinutes(),
m.getUserActivitySignResponseDelayInMinutes(), m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), keepEvents,
m.getUserActivitySignResponseDelayInMinutes(), m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getMeetingKeepEvents(),
m.breakoutRoomsParams,
m.lockSettingsParams, m.getHtml5InstanceId());
}
@ -697,7 +698,7 @@ public class MeetingService implements MessageListener {
if (m != null) {
m.setForciblyEnded(true);
processRecording(m);
if (keepEvents) {
if (m.getMeetingKeepEvents()) {
// The creation of the ended tag must occur after the creation of the
// recorded tag to avoid concurrency issues at the recording scripts
recordingService.markAsEnded(m.getInternalId());
@ -1233,10 +1234,6 @@ public class MeetingService implements MessageListener {
stunTurnService = s;
}
public void setKeepEvents(boolean value) {
keepEvents = value;
}
public void setUsersTimeout(long value) {
usersTimeout = value;
}

View File

@ -87,6 +87,7 @@ public class ParamsProcessorUtil {
private boolean webcamsOnlyForModerator;
private boolean defaultMuteOnStart = false;
private boolean defaultAllowModsToUnmuteUsers = false;
private boolean defaultKeepEvents = false;
private boolean defaultBreakoutRoomsEnabled;
private boolean defaultBreakoutRoomsRecord;
@ -544,6 +545,12 @@ public class ParamsProcessorUtil {
meeting.setMuteOnStart(muteOnStart);
Boolean meetingKeepEvents = defaultKeepEvents;
if (!StringUtils.isEmpty(params.get(ApiParams.MEETING_KEEP_EVENTS))) {
meetingKeepEvents = Boolean.parseBoolean(params.get(ApiParams.MEETING_KEEP_EVENTS));
}
meeting.setMeetingKeepEvents(meetingKeepEvents);
Boolean allowModsToUnmuteUsers = defaultAllowModsToUnmuteUsers;
if (!StringUtils.isEmpty(params.get(ApiParams.ALLOW_MODS_TO_UNMUTE_USERS))) {
allowModsToUnmuteUsers = Boolean.parseBoolean(params.get(ApiParams.ALLOW_MODS_TO_UNMUTE_USERS));
@ -1026,6 +1033,10 @@ public class ParamsProcessorUtil {
return defaultMuteOnStart;
}
public void setDefaultKeepEvents(Boolean mke) {
defaultKeepEvents = mke;
}
public void setAllowModsToUnmuteUsers(Boolean value) {
defaultAllowModsToUnmuteUsers = value;
}

View File

@ -84,6 +84,7 @@ public class Meeting {
private String customCopyright = "";
private Boolean muteOnStart = false;
private Boolean allowModsToUnmuteUsers = false;
private Boolean meetingKeepEvents;
private Integer meetingExpireIfNoUserJoinedInMinutes = 5;
private Integer meetingExpireWhenLastUserLeftInMinutes = 1;
@ -503,6 +504,14 @@ public class Meeting {
return muteOnStart;
}
public void setMeetingKeepEvents(Boolean mke) {
meetingKeepEvents = mke;
}
public Boolean getMeetingKeepEvents() {
return meetingKeepEvents;
}
public void setAllowModsToUnmuteUsers(Boolean value) {
allowModsToUnmuteUsers = value;
}

View File

@ -1,2 +1,2 @@
BIGBLUEBUTTON_RELEASE=2.3.0-alpha8
BIGBLUEBUTTON_RELEASE=2.3.0-beta-1

View File

@ -23,15 +23,17 @@ else
SERVLET_DIR=/var/lib/tomcat7/webapps/bigbluebutton
fi
BBB_WEB_ETC_CONFIG=/etc/bigbluebutton/bbb-web.properties
PROTOCOL=http
if [ -f $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties ]; then
SERVER_URL=$(cat $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties | sed -n '/^bigbluebutton.web.serverURL/{s/.*\///;p}')
if cat $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties | grep bigbluebutton.web.serverURL | grep -q https; then
SERVER_URL=$(cat $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties $BBB_WEB_ETC_CONFIG | grep -v '#' | sed -n '/^bigbluebutton.web.serverURL/{s/.*\///;p}' | tail -n 1)
if cat $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties $BBB_WEB_ETC_CONFIG | grep -v '#' | grep ^bigbluebutton.web.serverURL | tail -n 1 | grep -q https; then
PROTOCOL=https
fi
fi
HOST=$(cat $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | sed -n '/^bigbluebutton.web.serverURL/{s/.*\///;p}')
HOST=$(cat $SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties $BBB_WEB_ETC_CONFIG | grep -v '#' | sed -n '/^bigbluebutton.web.serverURL/{s/.*\///;p}' | tail -n 1)
HTML5_CONFIG=/usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml
BBB_WEB_CONFIG=$SERVLET_DIR/WEB-INF/classes/bigbluebutton.properties

View File

@ -82,7 +82,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
</script>
<script src="compatibility/adapter.js?v=VERSION" language="javascript"></script>
<script src="compatibility/sip.js?v=VERSION" language="javascript"></script>
<script src="compatibility/kurento-extension.js?v=VERSION" language="javascript"></script>
<script src="compatibility/kurento-utils.js?v=VERSION" language="javascript"></script>
</head>
<body style="background-color: #06172A">

View File

@ -15,7 +15,7 @@ export default function handleWhiteboardAnnotations({ header, body }, meetingId)
check(annotations, Array);
check(whiteboardId, String);
check(multiUser, Boolean);
check(multiUser, Array);
clearAnnotations(meetingId, whiteboardId);

View File

@ -10,6 +10,8 @@ export default function clearWhiteboard(whiteboardId) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(whiteboardId, String);
const payload = {

View File

@ -1,8 +1,12 @@
import { check } from 'meteor/check';
import sendAnnotationHelper from './sendAnnotationHelper';
import { extractCredentials } from '/imports/api/common/server/helpers';
export default function sendAnnotation(annotation) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
sendAnnotationHelper(annotation, meetingId, requesterUserId);
}

View File

@ -1,8 +1,12 @@
import { extractCredentials } from '/imports/api/common/server/helpers';
import sendAnnotationHelper from './sendAnnotationHelper';
import { check } from 'meteor/check';
export default function sendBulkAnnotations(payload) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
payload.forEach(annotation => sendAnnotationHelper(annotation, meetingId, requesterUserId));
}

View File

@ -10,6 +10,8 @@ export default function undoAnnotation(whiteboardId) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(whiteboardId, String);
const payload = {

View File

@ -39,6 +39,7 @@ const WEBSOCKET_KEEP_ALIVE_DEBOUNCE = MEDIA.websocketKeepAliveDebounce || 10;
const TRACE_SIP = MEDIA.traceSip || false;
const AUDIO_MICROPHONE_CONSTRAINTS = Meteor.settings.public.app.defaultSettings
.application.microphoneConstraints;
const SDP_SEMANTICS = MEDIA.sdpSemantics;
const getAudioSessionNumber = () => {
let currItem = parseInt(sessionStorage.getItem(AUDIO_SESSION_NUM_KEY), 10);
@ -388,7 +389,7 @@ class SIPSession {
sessionDescriptionHandlerFactoryOptions: {
peerConnectionConfiguration: {
iceServers,
sdpSemantics: 'plan-b',
sdpSemantics: SDP_SEMANTICS,
},
},
displayName: callerIdName,

View File

@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor';
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function createBreakoutRoom(rooms, durationInMinutes, record = false) {
const REDIS_CONFIG = Meteor.settings.private.redis;
@ -12,6 +13,9 @@ export default function createBreakoutRoom(rooms, durationInMinutes, record = fa
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const eventName = 'CreateBreakoutRoomsCmdMsg';
if (rooms.length > MAX_BREAKOUT_ROOMS) {
Logger.info(`Attempt to create breakout rooms with invalid number of rooms in meeting id=${meetingId}`);

View File

@ -1,6 +1,7 @@
import { Meteor } from 'meteor/meteor';
import RedisPubSub from '/imports/startup/server/redis';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function requestJoinURL({ breakoutId, userId: userIdToInvite }) {
const REDIS_CONFIG = Meteor.settings.private.redis;
@ -8,6 +9,9 @@ export default function requestJoinURL({ breakoutId, userId: userIdToInvite }) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const userId = userIdToInvite || requesterUserId;
const eventName = 'RequestBreakoutJoinURLReqMsg';

View File

@ -11,12 +11,14 @@ export default function emitExternalVideoEvent(options) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const { status, playerStatus } = options;
const user = Users.findOne({ meetingId, userId: requesterUserId })
const user = Users.findOne({ meetingId, userId: requesterUserId });
if (user && user.presenter) {
check(status, String);
check(playerStatus, {
rate: Match.Maybe(Number),
@ -24,13 +26,14 @@ export default function emitExternalVideoEvent(options) {
state: Match.Maybe(Boolean),
});
let rate = playerStatus.rate || 0;
let time = playerStatus.time || 0;
let state = playerStatus.state || 0;
const payload = { status, rate, time, state };
const rate = playerStatus.rate || 0;
const time = playerStatus.time || 0;
const state = playerStatus.state || 0;
const payload = {
status, rate, time, state,
};
Logger.debug(`User id=${requesterUserId} sending ${EVENT_NAME} event:${state} for meeting ${meetingId}`);
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}
}

View File

@ -10,6 +10,10 @@ const PUBLIC_CHAT_TYPE = CHAT_CONFIG.type_public;
export default function chatMessageBeforeJoinCounter() {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const groupChats = GroupChat.find({
$or: [
{ meetingId, access: PUBLIC_CHAT_TYPE },

View File

@ -1,6 +1,7 @@
import { Meteor } from 'meteor/meteor';
import RedisPubSub from '/imports/startup/server/redis';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function clearPublicChatHistory() {
const REDIS_CONFIG = Meteor.settings.private.redis;
@ -11,6 +12,9 @@ export default function clearPublicChatHistory() {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const payload = {
chatId: PUBLIC_GROUP_CHAT_ID,
};

View File

@ -1,14 +1,18 @@
import { Meteor } from 'meteor/meteor';
import GroupChat from '/imports/api/group-chat';
import { GroupChatMsg } from '/imports/api/group-chat-msg';
import Users from '/imports/api/users';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
const CHAT_CONFIG = Meteor.settings.public.chat;
const ITENS_PER_PAGE = CHAT_CONFIG.itemsPerPage;
export default function fetchMessagePerPage(chatId, page) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const User = Users.findOne({ userId: requesterUserId, meetingId });
const messages = GroupChatMsg.find({ chatId, meetingId, timestamp: { $lt: User.authTokenValidatedTime } },

View File

@ -34,6 +34,8 @@ export default function sendGroupChatMsg(chatId, message) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(message, Object);
const parsedMessage = parseMessage(message.message);

View File

@ -11,6 +11,8 @@ export default function startUserTyping(chatId) {
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(chatId, String);
const payload = {

View File

@ -1,10 +1,14 @@
import { UsersTyping } from '/imports/api/group-chat-msg';
import stopTyping from '../modifiers/stopTyping';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function stopUserTyping() {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const userTyping = UsersTyping.findOne({
meetingId,
userId: requesterUserId,

View File

@ -11,6 +11,8 @@ export default function createGroupChat(receiver) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(receiver, Object);
const payload = {

View File

@ -1,12 +1,16 @@
import { Meteor } from 'meteor/meteor';
import RedisPubSub from '/imports/startup/server/redis';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function destroyGroupChat() {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const eventName = 'DestroyGroupChatReqMsg';
const payload = {

View File

@ -11,6 +11,8 @@ const EVENT_NAME = 'GuestsWaitingApprovedMsg';
export default function allowPendingUsers(guests, status) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(guests, Array);
const mappedGuests = guests.map(guest => ({ status, guest: guest.intId }));

View File

@ -11,6 +11,8 @@ const EVENT_NAME = 'SetGuestPolicyCmdMsg';
export default function changeGuestPolicy(policyRule) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(policyRule, String);
const payload = {

View File

@ -10,6 +10,8 @@ export default function userChangedLocalSettings(settings) {
if (!meetingId || !requesterUserId) return;
check(settings, Object);
check(meetingId, String);
check(requesterUserId, String);
const userLocalSettings = LocalSettings
.findOne({ meetingId, userId: requesterUserId },

View File

@ -4,6 +4,7 @@ import handleGetAllMeetings from './handlers/getAllMeetings';
import handleMeetingEnd from './handlers/meetingEnd';
import handleMeetingDestruction from './handlers/meetingDestruction';
import handleMeetingLocksChange from './handlers/meetingLockChange';
import handleGuestPolicyChanged from './handlers/guestPolicyChanged';
import handleGuestLobbyMessageChanged from './handlers/guestLobbyMessageChanged';
import handleUserLockChange from './handlers/userLockChange';
import handleRecordingStatusChange from './handlers/recordingStatusChange';
@ -22,6 +23,7 @@ RedisPubSub.on('RecordingStatusChangedEvtMsg', handleRecordingStatusChange);
RedisPubSub.on('UpdateRecordingTimerEvtMsg', handleRecordingTimerChange);
RedisPubSub.on('WebcamsOnlyForModeratorChangedEvtMsg', handleChangeWebcamOnlyModerator);
RedisPubSub.on('GetLockSettingsRespMsg', handleMeetingLocksChange);
RedisPubSub.on('GuestPolicyChangedEvtMsg', handleGuestPolicyChanged);
RedisPubSub.on('GuestLobbyMessageChangedEvtMsg', handleGuestLobbyMessageChanged);
RedisPubSub.on('MeetingTimeRemainingUpdateEvtMsg', handleTimeRemainingUpdate);
RedisPubSub.on('SelectRandomViewerRespMsg', handleSelectRandomViewer);

View File

@ -0,0 +1,12 @@
import setGuestPolicy from '../modifiers/setGuestPolicy';
import { check } from 'meteor/check';
export default function handleGuestPolicyChanged({ body }, meetingId) {
const { policy } = body;
check(meetingId, String);
check(policy, String);
return setGuestPolicy(meetingId, policy);
}

View File

@ -1,10 +1,14 @@
import Logger from '/imports/startup/server/logger';
import Meetings from '/imports/api/meetings';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function clearRandomlySelectedUser() {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const selector = {
meetingId,
};

View File

@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor';
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function endMeeting() {
const REDIS_CONFIG = Meteor.settings.private.redis;
@ -9,6 +10,9 @@ export default function endMeeting() {
const EVENT_NAME = 'LogoutAndEndMeetingCmdMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const payload = {
userId: requesterUserId,
};

View File

@ -9,6 +9,8 @@ export default function toggleLockSettings(lockSettingsProps) {
const EVENT_NAME = 'ChangeLockSettingsInMeetingCmdMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(lockSettingsProps, {
disableCam: Boolean,
disableMic: Boolean,

View File

@ -4,6 +4,7 @@ import RedisPubSub from '/imports/startup/server/redis';
import { RecordMeetings } from '/imports/api/meetings';
import Users from '/imports/api/users';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function toggleRecording() {
const REDIS_CONFIG = Meteor.settings.private.redis;
@ -11,6 +12,10 @@ export default function toggleRecording() {
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const EVENT_NAME = 'SetRecordingStatusCmdMsg';
let meetingRecorded;

View File

@ -9,6 +9,9 @@ export default function toggleWebcamsOnlyForModerator(webcamsOnlyForModerator) {
const EVENT_NAME = 'UpdateWebcamsOnlyForModeratorCmdMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(webcamsOnlyForModerator, Boolean);
const payload = {

View File

@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor';
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function transferUser(fromMeetingId, toMeetingId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
@ -11,6 +11,9 @@ export default function transferUser(fromMeetingId, toMeetingId) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const payload = {
fromMeetingId,
toMeetingId,

View File

@ -0,0 +1,28 @@
import Meetings from '/imports/api/meetings';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
export default function setGuestPolicy(meetingId, guestPolicy) {
check(meetingId, String);
check(guestPolicy, String);
const selector = {
meetingId,
};
const modifier = {
$set: {
'usersProp.guestPolicy': guestPolicy,
},
};
try {
const { numberAffected } = Meetings.upsert(selector, modifier);
if (numberAffected) {
Logger.verbose(`Set guest policy meetingId=${meetingId} guestPolicy=${guestPolicy}`);
}
} catch (err) {
Logger.error(`Setting guest policy: ${err}`);
}
}

View File

@ -5,6 +5,9 @@ import { extractCredentials } from '/imports/api/common/server/helpers';
export default function userInstabilityDetected(sender) {
const { meetingId, requesterUserId: receiver } = extractCredentials(this.userId);
check(meetingId, String);
check(receiver, String);
check(sender, String);
const payload = {

View File

@ -11,12 +11,28 @@ export default function sendPollChatMsg({ body }, meetingId) {
const { answers, numRespondents } = poll;
const caseInsensitiveReducer = (acc, item) => {
const index = acc.findIndex(ans => ans.key.toLowerCase() === item.key.toLowerCase());
if(index !== -1) {
if(acc[index].numVotes >= item.numVotes) acc[index].numVotes += item.numVotes;
else {
const tempVotes = acc[index].numVotes;
acc[index] = item;
acc[index].numVotes += tempVotes;
}
} else {
acc.push(item);
}
return acc;
};
let responded = 0;
let resultString = 'bbb-published-poll-\n';
answers.map((item) => {
responded += item.numVotes;
return item;
}).map((item) => {
}).reduce(caseInsensitiveReducer, []).map((item) => {
item.key = item.key.split('<br/>').join('<br#>');
const numResponded = responded === numRespondents ? numRespondents : responded;
const pct = Math.round(item.numVotes / numResponded * 100);
const pctFotmatted = `${Number.isNaN(pct) ? 0 : pct}%`;

View File

@ -2,6 +2,7 @@ import RedisPubSub from '/imports/startup/server/redis';
import Polls from '/imports/api/polls';
import Logger from '/imports/startup/server/logger';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function publishPoll() {
const REDIS_CONFIG = Meteor.settings.private.redis;
@ -9,6 +10,10 @@ export default function publishPoll() {
const EVENT_NAME = 'ShowPollResultReqMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const poll = Polls.findOne({ meetingId }); // TODO--send pollid from client
if (!poll) {
Logger.error(`Attempted to publish inexisting poll for meetingId: ${meetingId}`);

View File

@ -10,6 +10,8 @@ export default function publishTypedVote(id, pollAnswer) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(pollAnswer, String);
check(id, String);

View File

@ -10,6 +10,8 @@ export default function publishVote(pollId, pollAnswerId) {
const EVENT_NAME = 'RespondToPollReqMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(pollAnswerId, Number);
check(pollId, String);

View File

@ -10,6 +10,8 @@ export default function startPoll(pollType, pollId, question, answers) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(pollId, String);
check(pollType, String);

View File

@ -1,8 +1,13 @@
import RedisPubSub from '/imports/startup/server/redis';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function stopPoll() {
const { meetingId, requesterUserId: requesterId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterId, String);
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'StopPollReqMsg';

View File

@ -8,6 +8,9 @@ export default function requestPresentationUploadToken(podId, filename) {
const EVENT_NAME = 'PresentationUploadTokenReqMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(podId, String);
check(filename, String);

View File

@ -1,10 +1,14 @@
import PresentationUploadToken from '/imports/api/presentation-upload-token';
import Logger from '/imports/startup/server/logger';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function setUsedToken(authzToken) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const payload = {
$set: {
used: true,

View File

@ -9,6 +9,8 @@ export default function removePresentation(presentationId, podId) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(presentationId, String);
check(podId, String);

View File

@ -8,6 +8,8 @@ export default function setPresentation(presentationId, podId) {
const EVENT_NAME = 'SetCurrentPresentationPubMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(presentationId, String);
check(podId, String);

View File

@ -9,6 +9,8 @@ export default function setPresentationDownloadable(presentationId, downloadable
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(downloadable, Boolean);
check(presentationId, String);

View File

@ -0,0 +1,61 @@
import {
SFU_CLIENT_SIDE_ERRORS,
SFU_SERVER_SIDE_ERRORS
} from '/imports/ui/services/bbb-webrtc-sfu/broker-base-errors';
// Mapped getDisplayMedia errors. These are bridge agnostic
// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia
const GDM_ERRORS = {
// Fallback error: 1130
1130: 'GetDisplayMediaGenericError',
1131: 'AbortError',
1132: 'InvalidStateError',
1133: 'OverconstrainedError',
1134: 'TypeError',
1135: 'NotFoundError',
1136: 'NotAllowedError',
1137: 'NotSupportedError',
1138: 'NotReadableError',
};
// Import as many bridge specific errors you want in this utilitary and shove
// them into the error class slots down below.
const CLIENT_SIDE_ERRORS = {
1101: "SIGNALLING_TRANSPORT_DISCONNECTED",
1102: "SIGNALLING_TRANSPORT_CONNECTION_FAILED",
1104: "SCREENSHARE_PLAY_FAILED",
1105: "PEER_NEGOTIATION_FAILED",
1107: "ICE_STATE_FAILED",
1120: "MEDIA_TIMEOUT",
1121: "UNKNOWN_ERROR",
};
const SERVER_SIDE_ERRORS = {
...SFU_SERVER_SIDE_ERRORS,
}
const AGGREGATED_ERRORS = {
...CLIENT_SIDE_ERRORS,
...SERVER_SIDE_ERRORS,
...GDM_ERRORS,
}
const expandErrors = () => {
const expandedErrors = Object.keys(AGGREGATED_ERRORS).reduce((map, key) => {
map[AGGREGATED_ERRORS[key]] = { errorCode: key, errorMessage: AGGREGATED_ERRORS[key] };
return map;
}, {});
return { ...AGGREGATED_ERRORS, ...expandedErrors };
}
const SCREENSHARING_ERRORS = expandErrors();
export {
GDM_ERRORS,
BRIDGE_SERVER_SIDE_ERRORS,
BRIDGE_CLIENT_SIDE_ERRORS,
// All errors, [code]: [message]
// Expanded errors. It's AGGREGATED + message: { errorCode, errorMessage }
SCREENSHARING_ERRORS,
}

View File

@ -1,236 +1,297 @@
import Auth from '/imports/ui/services/auth';
import BridgeService from './service';
import { fetchWebRTCMappedStunTurnServers, getMappedFallbackStun } from '/imports/utils/fetchStunTurnServers';
import playAndRetry from '/imports/utils/mediaElementPlayRetry';
import logger from '/imports/startup/client/logger';
import BridgeService from './service';
import ScreenshareBroker from '/imports/ui/services/bbb-webrtc-sfu/screenshare-broker';
import { setSharingScreen, screenShareEndAlert } from '/imports/ui/components/screenshare/service';
import { SCREENSHARING_ERRORS } from './errors';
const SFU_CONFIG = Meteor.settings.public.kurento;
const SFU_URL = SFU_CONFIG.wsUrl;
const CHROME_DEFAULT_EXTENSION_KEY = SFU_CONFIG.chromeDefaultExtensionKey;
const CHROME_CUSTOM_EXTENSION_KEY = SFU_CONFIG.chromeExtensionKey;
const CHROME_SCREENSHARE_SOURCES = SFU_CONFIG.screenshare.chromeScreenshareSources;
const FIREFOX_SCREENSHARE_SOURCE = SFU_CONFIG.screenshare.firefoxScreenshareSource;
const BRIDGE_NAME = 'kurento'
const SCREENSHARE_VIDEO_TAG = 'screenshareVideo';
const SEND_ROLE = 'send';
const RECV_ROLE = 'recv';
const CHROME_EXTENSION_KEY = CHROME_CUSTOM_EXTENSION_KEY === 'KEY' ? CHROME_DEFAULT_EXTENSION_KEY : CHROME_CUSTOM_EXTENSION_KEY;
// the error-code mapping is bridge specific; that's why it's not in the errors util
const ERROR_MAP = {
1301: SCREENSHARING_ERRORS.SIGNALLING_TRANSPORT_DISCONNECTED,
1302: SCREENSHARING_ERRORS.SIGNALLING_TRANSPORT_CONNECTION_FAILED,
1305: SCREENSHARING_ERRORS.PEER_NEGOTIATION_FAILED,
1307: SCREENSHARING_ERRORS.ICE_STATE_FAILED,
}
const getUserId = () => Auth.userID;
const mapErrorCode = (error) => {
const { errorCode } = error;
const mappedError = ERROR_MAP[errorCode];
const getMeetingId = () => Auth.meetingID;
if (errorCode == null || mappedError == null) return error;
error.errorCode = mappedError.errorCode;
error.errorMessage = mappedError.errorMessage;
error.message = mappedError.errorMessage;
const getUsername = () => Auth.fullname;
const getSessionToken = () => Auth.sessionToken;
return error;
}
export default class KurentoScreenshareBridge {
static normalizeError(error = {}) {
const errorMessage = error.name || error.message || error.reason || 'Unknown error';
const errorCode = error.code || undefined;
const errorReason = error.reason || error.id || 'Undefined reason';
return { errorMessage, errorCode, errorReason };
constructor() {
this.role;
this.broker;
this._gdmStream;
this.hasAudio = false;
this.connectionAttempts = 0;
this.reconnecting = false;
this.reconnectionTimeout;
this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT;
}
static handlePresenterFailure(error, started = false) {
const normalizedError = KurentoScreenshareBridge.normalizeError(error);
if (!started) {
logger.error({
logCode: 'screenshare_presenter_error_failed_to_connect',
extraInfo: { ...normalizedError },
}, `Screenshare presenter failed when trying to start due to ${normalizedError.errorMessage}`);
} else {
logger.error({
logCode: 'screenshare_presenter_error_failed_after_success',
extraInfo: { ...normalizedError },
}, `Screenshare presenter failed during working session due to ${normalizedError.errorMessage}`);
get gdmStream() {
return this._gdmStream;
}
set gdmStream(stream) {
this._gdmStream = stream;
}
outboundStreamReconnect() {
const currentRestartIntervalMs = this.restartIntervalMs;
const stream = this.gdmStream;
logger.warn({
logCode: 'screenshare_presenter_reconnect',
extraInfo: {
reconnecting: this.reconnecting,
role: this.role,
bridge: BRIDGE_NAME
},
}, `Screenshare presenter session is reconnecting`);
this.stop();
this.restartIntervalMs = BridgeService.getNextReconnectionInterval(currentRestartIntervalMs);
this.share(stream, this.onerror).then(() => {
this.clearReconnectionTimeout();
}).catch(error => {
// Error handling is a no-op because it will be "handled" in handlePresenterFailure
logger.debug({
logCode: 'screenshare_reconnect_failed',
extraInfo: {
errorCode: error.errorCode,
errorMessage: error.errorMessage,
reconnecting: this.reconnecting,
role: this.role,
bridge: BRIDGE_NAME
},
}, 'Screensharing reconnect failed');
});
}
inboundStreamReconnect() {
const currentRestartIntervalMs = this.restartIntervalMs;
logger.warn({
logCode: 'screenshare_viewer_reconnect',
extraInfo: {
reconnecting: this.reconnecting,
role: this.role,
bridge: BRIDGE_NAME
},
}, `Screenshare viewer session is reconnecting`);
// Cleanly stop everything before triggering a reconnect
this.stop();
// Create new reconnect interval time
this.restartIntervalMs = BridgeService.getNextReconnectionInterval(currentRestartIntervalMs);
this.view(this.hasAudio).then(() => {
this.clearReconnectionTimeout();
}).catch(error => {
// Error handling is a no-op because it will be "handled" in handleViewerFailure
logger.debug({
logCode: 'screenshare_reconnect_failed',
extraInfo: {
errorCode: error.errorCode,
errorMessage: error.errorMessage,
reconnecting: this.reconnecting,
role: this.role,
bridge: BRIDGE_NAME
},
}, 'Screensharing reconnect failed');
});
}
handleConnectionTimeoutExpiry() {
this.reconnecting = true;
switch (this.role) {
case RECV_ROLE:
return this.inboundStreamReconnect();
case SEND_ROLE:
return this.outboundStreamReconnect();
default:
this.reconnecting = false;
logger.error({
logCode: 'screenshare_invalid_role'
}, 'Screen sharing with invalid role, wont reconnect');
break;
}
return normalizedError;
}
static handleViewerFailure(error, started = false) {
const normalizedError = KurentoScreenshareBridge.normalizeError(error);
if (!started) {
logger.error({
logCode: 'screenshare_viewer_error_failed_to_connect',
extraInfo: { ...normalizedError },
}, `Screenshare viewer failed when trying to start due to ${normalizedError.errorMessage}`);
} else {
logger.error({
logCode: 'screenshare_viewer_error_failed_after_success',
extraInfo: { ...normalizedError },
}, `Screenshare viewer failed during working session due to ${normalizedError.errorMessage}`);
maxConnectionAttemptsReached () {
return this.connectionAttempts > BridgeService.MAX_CONN_ATTEMPTS;
}
scheduleReconnect () {
if (this.reconnectionTimeout == null) {
this.reconnectionTimeout = setTimeout(
this.handleConnectionTimeoutExpiry.bind(this),
this.restartIntervalMs
);
}
return normalizedError;
}
static playElement(screenshareMediaElement) {
const mediaTagPlayed = () => {
logger.info({
logCode: 'screenshare_media_play_success',
}, 'Screenshare media played successfully');
clearReconnectionTimeout () {
this.reconnecting = false;
this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT;
if (this.reconnectionTimeout) {
clearTimeout(this.reconnectionTimeout);
this.reconnectionTimeout = null;
}
}
handleViewerStart() {
const mediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
if (mediaElement && this.broker && this.broker.webRtcPeer) {
const stream = this.broker.webRtcPeer.getRemoteStream();
BridgeService.screenshareLoadAndPlayMediaStream(stream, mediaElement, !this.broker.hasAudio);
}
this.clearReconnectionTimeout();
}
handleBrokerFailure(error) {
mapErrorCode(error);
const { errorMessage, errorCode } = error;
logger.error({
logCode: 'screenshare_broker_failure',
extraInfo: {
errorCode, errorMessage,
role: this.broker.role,
started: this.broker.started,
reconnecting: this.reconnecting,
bridge: BRIDGE_NAME
},
}, 'Screenshare broker failure');
// Screensharing was already successfully negotiated and error occurred during
// during call; schedule a reconnect
// If the session has not yet started, a reconnect should already be scheduled
if (this.broker.started) {
this.scheduleReconnect();
}
return error;
}
async view(hasAudio = false) {
this.hasAudio = hasAudio;
this.role = RECV_ROLE;
const iceServers = await BridgeService.getIceServers(Auth.sessionToken);
const options = {
iceServers,
userName: Auth.fullname,
hasAudio,
};
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();
}
this.broker = new ScreenshareBroker(
Auth.authenticateURL(SFU_URL),
BridgeService.getConferenceBridge(),
Auth.userID,
Auth.meetingID,
this.role,
options,
);
this.broker.onstart = this.handleViewerStart.bind(this);
this.broker.onerror = this.handleBrokerFailure.bind(this);
this.broker.onended = this.handleEnded.bind(this);
return this.broker.view().finally(this.scheduleReconnect.bind(this));
}
static screenshareElementLoadAndPlay(stream, element, muted) {
element.muted = muted;
element.pause();
element.srcObject = stream;
KurentoScreenshareBridge.playElement(element);
handlePresenterStart() {
logger.info({
logCode: 'screenshare_presenter_start_success',
}, 'Screenshare presenter started succesfully');
this.clearReconnectionTimeout();
this.reconnecting = false;
this.connectionAttempts = 0;
}
kurentoViewLocalPreview() {
const screenshareMediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
const { webRtcPeer } = window.kurentoManager.kurentoScreenshare;
if (webRtcPeer) {
const stream = webRtcPeer.getLocalStream();
KurentoScreenshareBridge.screenshareElementLoadAndPlay(stream, screenshareMediaElement, true);
}
handleEnded() {
screenShareEndAlert();
}
async kurentoViewScreen() {
const screenshareMediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
let iceServers = [];
let started = false;
share(stream, onFailure) {
return new Promise(async (resolve, reject) => {
this.onerror = onFailure;
this.connectionAttempts += 1;
this.role = SEND_ROLE;
this.hasAudio = BridgeService.streamHasAudioTrack(stream);
this.gdmStream = stream;
try {
iceServers = await fetchWebRTCMappedStunTurnServers(getSessionToken());
} catch (error) {
logger.error({
logCode: 'screenshare_viewer_fetchstunturninfo_error',
extraInfo: { error },
}, 'Screenshare bridge failed to fetch STUN/TURN info, using default');
iceServers = getMappedFallbackStun();
} finally {
const options = {
wsUrl: Auth.authenticateURL(SFU_URL),
iceServers,
logger,
userName: getUsername(),
};
const onerror = (error) => {
const normalizedError = this.handleBrokerFailure(error);
if (this.maxConnectionAttemptsReached()) {
this.clearReconnectionTimeout();
this.connectionAttempts = 0;
onFailure(SCREENSHARING_ERRORS.MEDIA_TIMEOUT);
const onFail = (error) => {
KurentoScreenshareBridge.handleViewerFailure(error, started);
};
// Callback for the kurento-extension.js script. It's called when the whole
// negotiation with SFU is successful. This will load the stream into the
// screenshare media element and play it manually.
const onSuccess = () => {
started = true;
const { webRtcPeer } = window.kurentoManager.kurentoVideo;
if (webRtcPeer) {
const stream = webRtcPeer.getRemoteStream();
KurentoScreenshareBridge.screenshareElementLoadAndPlay(
stream,
screenshareMediaElement,
true,
);
return reject(SCREENSHARING_ERRORS.MEDIA_TIMEOUT);
}
};
window.kurentoWatchVideo(
SCREENSHARE_VIDEO_TAG,
BridgeService.getConferenceBridge(),
getUserId(),
getMeetingId(),
onFail,
onSuccess,
options,
);
}
}
kurentoExitVideo() {
window.kurentoExitVideo();
}
async kurentoShareScreen(onFail, stream) {
let iceServers = [];
try {
iceServers = await fetchWebRTCMappedStunTurnServers(getSessionToken());
} catch (error) {
logger.error({ logCode: 'screenshare_presenter_fetchstunturninfo_error' },
'Screenshare bridge failed to fetch STUN/TURN info, using default');
iceServers = getMappedFallbackStun();
} finally {
const iceServers = await BridgeService.getIceServers(Auth.sessionToken);
const options = {
wsUrl: Auth.authenticateURL(SFU_URL),
chromeExtension: CHROME_EXTENSION_KEY,
chromeScreenshareSources: CHROME_SCREENSHARE_SOURCES,
firefoxScreenshareSource: FIREFOX_SCREENSHARE_SOURCE,
iceServers,
logger,
userName: getUsername(),
userName: Auth.fullname,
stream,
hasAudio: this.hasAudio,
};
let started = false;
const failureCallback = (error) => {
const normalizedError = KurentoScreenshareBridge.handlePresenterFailure(error, started);
onFail(normalizedError);
};
const successCallback = () => {
started = true;
logger.info({
logCode: 'screenshare_presenter_start_success',
}, 'Screenshare presenter started succesfully');
};
options.stream = stream || undefined;
window.kurentoShareScreen(
SCREENSHARE_VIDEO_TAG,
this.broker = new ScreenshareBroker(
Auth.authenticateURL(SFU_URL),
BridgeService.getConferenceBridge(),
getUserId(),
getMeetingId(),
failureCallback,
successCallback,
Auth.userID,
Auth.meetingID,
this.role,
options,
);
}
}
kurentoExitScreenShare() {
window.kurentoExitScreenShare();
this.broker.onerror = onerror.bind(this);
this.broker.onstreamended = this.stop.bind(this);
this.broker.onstart = this.handlePresenterStart.bind(this);
this.broker.onended = this.handleEnded.bind(this);
this.broker.share().then(() => {
this.scheduleReconnect();
return resolve();
}).catch(reject);
});
};
stop() {
if (this.broker) {
this.broker.stop();
// Checks if this session is a sharer and if it's not reconnecting
// If that's the case, clear the local sharing state in screen sharing UI
// component tracker to be extra sure we won't have any client-side state
// inconsistency - prlanzarin
if (this.broker.role === SEND_ROLE && !this.reconnecting) setSharingScreen(false);
this.broker = null;
}
this.gdmStream = null;
this.clearReconnectionTimeout();
}
}

View File

@ -1,37 +1,66 @@
import Meetings from '/imports/api/meetings';
import logger from '/imports/startup/client/logger';
import { fetchWebRTCMappedStunTurnServers, getMappedFallbackStun } from '/imports/utils/fetchStunTurnServers';
import loadAndPlayMediaStream from '/imports/ui/services/bbb-webrtc-sfu/load-play';
import { SCREENSHARING_ERRORS } from './errors';
const {
constraints: GDM_CONSTRAINTS,
mediaTimeouts: MEDIA_TIMEOUTS,
} = Meteor.settings.public.kurento.screenshare;
const {
baseTimeout: BASE_MEDIA_TIMEOUT,
maxTimeout: MAX_MEDIA_TIMEOUT,
maxConnectionAttempts: MAX_CONN_ATTEMPTS,
timeoutIncreaseFactor: TIMEOUT_INCREASE_FACTOR,
} = MEDIA_TIMEOUTS;
const hasDisplayMedia = (typeof navigator.getDisplayMedia === 'function'
const HAS_DISPLAY_MEDIA = (typeof navigator.getDisplayMedia === 'function'
|| (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function'));
const getConferenceBridge = () => Meetings.findOne().voiceProp.voiceConf;
const normalizeGetDisplayMediaError = (error) => {
return SCREENSHARING_ERRORS[error.name] || SCREENSHARING_ERRORS.GetDisplayMediaGenericError;
};
const getBoundGDM = () => {
if (typeof navigator.getDisplayMedia === 'function') {
return navigator.getDisplayMedia.bind(navigator);
} else if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') {
return navigator.mediaDevices.getDisplayMedia.bind(navigator.mediaDevices);
}
}
const getScreenStream = async () => {
const gDMCallback = (stream) => {
// Some older Chromium variants choke on gDM when audio: true by NOT generating
// a promise rejection AND not generating a valid input screen stream, need to
// work around that manually for now - prlanzarin
if (stream == null) {
return Promise.reject(SCREENSHARING_ERRORS.NotSupportedError);
}
if (typeof stream.getVideoTracks === 'function'
&& typeof constraints.video === 'object') {
stream.getVideoTracks().forEach((track) => {
if (typeof track.applyConstraints === 'function') {
track.applyConstraints(constraints.video).catch((error) => {
&& typeof GDM_CONSTRAINTS.video === 'object') {
stream.getVideoTracks().forEach(track => {
if (typeof track.applyConstraints === 'function') {
track.applyConstraints(GDM_CONSTRAINTS.video).catch(error => {
logger.warn({
logCode: 'screenshare_videoconstraint_failed',
extraInfo: { errorName: error.name, errorCode: error.code },
},
'Error applying screenshare video constraint');
'Error applying screenshare video constraint');
});
}
});
}
if (typeof stream.getAudioTracks === 'function'
&& typeof constraints.audio === 'object') {
stream.getAudioTracks().forEach((track) => {
if (typeof track.applyConstraints === 'function') {
track.applyConstraints(constraints.audio).catch((error) => {
&& typeof GDM_CONSTRAINTS.audio === 'object') {
stream.getAudioTracks().forEach(track => {
if (typeof track.applyConstraints === 'function') {
track.applyConstraints(GDM_CONSTRAINTS.audio).catch(error => {
logger.warn({
logCode: 'screenshare_audioconstraint_failed',
extraInfo: { errorName: error.name, errorCode: error.code },
@ -44,39 +73,81 @@ const getScreenStream = async () => {
return Promise.resolve(stream);
};
const constraints = hasDisplayMedia ? GDM_CONSTRAINTS : null;
const getDisplayMedia = getBoundGDM();
// getDisplayMedia isn't supported, generate no stream and let the legacy
// constraint fetcher work its way on kurento-extension.js
if (constraints == null) {
return Promise.resolve();
}
if (typeof navigator.getDisplayMedia === 'function') {
return navigator.getDisplayMedia(constraints)
if (typeof getDisplayMedia === 'function') {
return getDisplayMedia(GDM_CONSTRAINTS)
.then(gDMCallback)
.catch((error) => {
.catch(error => {
const normalizedError = normalizeGetDisplayMediaError(error);
logger.error({
logCode: 'screenshare_getdisplaymedia_failed',
extraInfo: { errorName: error.name, errorCode: error.code },
extraInfo: { errorCode: normalizedError.errorCode, errorMessage: normalizedError.errorMessage },
}, 'getDisplayMedia call failed');
return Promise.resolve();
});
} if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') {
return navigator.mediaDevices.getDisplayMedia(constraints)
.then(gDMCallback)
.catch((error) => {
logger.error({
logCode: 'screenshare_getdisplaymedia_failed',
extraInfo: { errorName: error.name, errorCode: error.code },
}, 'getDisplayMedia call failed');
return Promise.resolve();
return Promise.reject(normalizedError);
});
} else {
// getDisplayMedia isn't supported, error its way out
return Promise.reject(SCREENSHARING_ERRORS.NotSupportedError);
}
};
const getIceServers = (sessionToken) => {
return fetchWebRTCMappedStunTurnServers(sessionToken).catch(error => {
logger.error({
logCode: 'screenshare_fetchstunturninfo_error',
extraInfo: { error }
}, 'Screenshare bridge failed to fetch STUN/TURN info');
return getMappedFallbackStun();
});
}
const getNextReconnectionInterval = (oldInterval) => {
return Math.min(
TIMEOUT_INCREASE_FACTOR * oldInterval,
MAX_MEDIA_TIMEOUT,
);
}
const streamHasAudioTrack = (stream) => {
return stream
&& typeof stream.getAudioTracks === 'function'
&& stream.getAudioTracks().length >= 1;
}
const dispatchAutoplayHandlingEvent = (mediaElement) => {
const tagFailedEvent = new CustomEvent('screensharePlayFailed',
{ detail: { mediaElement } });
window.dispatchEvent(tagFailedEvent);
}
const screenshareLoadAndPlayMediaStream = (stream, mediaElement, muted) => {
return loadAndPlayMediaStream(stream, mediaElement, muted).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 },
}, 'Screen share media play failed: autoplay error');
dispatchAutoplayHandlingEvent(mediaElement);
} else {
throw {
errorCode: SCREENSHARING_ERRORS.SCREENSHARE_PLAY_FAILED.errorCode,
errorMessage: error.message || SCREENSHARING_ERRORS.SCREENSHARE_PLAY_FAILED.errorMessage,
};
}
});
}
export default {
hasDisplayMedia,
HAS_DISPLAY_MEDIA,
getConferenceBridge,
getScreenStream,
getIceServers,
getNextReconnectionInterval,
streamHasAudioTrack,
screenshareLoadAndPlayMediaStream,
BASE_MEDIA_TIMEOUT,
MAX_CONN_ATTEMPTS,
};

View File

@ -11,6 +11,9 @@ export default function switchSlide(slideNumber, podId) { // TODO-- send present
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'SetCurrentPagePubMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(slideNumber, Number);
const selector = {

View File

@ -3,6 +3,7 @@ import { Slides } from '/imports/api/slides';
import { Meteor } from 'meteor/meteor';
import RedisPubSub from '/imports/startup/server/redis';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function zoomSlide(slideNumber, podId, widthRatio, heightRatio, x, y) { // TODO-- send presentationId and SlideId
const REDIS_CONFIG = Meteor.settings.private.redis;
@ -11,6 +12,9 @@ export default function zoomSlide(slideNumber, podId, widthRatio, heightRatio, x
const EVENT_NAME = 'ResizeAndMovePagePubMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const selector = {
meetingId,
podId,

View File

@ -1,9 +1,14 @@
import UserInfos from '/imports/api/users-infos';
import Logger from '/imports/startup/server/logger';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function removeUserInformation() {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const selector = {
meetingId,
requesterUserId,

View File

@ -9,6 +9,9 @@ export default function getUserInformation(externalUserId) {
const EVENT_NAME = 'LookUpUserReqMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(externalUserId, String);
const payload = {

View File

@ -85,6 +85,9 @@ export default function addUserSettings(settings) {
const { meetingId, requesterUserId: userId } = extractCredentials(this.userId);
check(meetingId, String);
check(userId, String);
let parameters = {};
settings.forEach((el) => {

View File

@ -11,6 +11,9 @@ export default function assignPresenter(userId) { // TODO-- send username from c
const EVENT_NAME = 'AssignPresenterReqMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(userId, String);
const User = Users.findOne({

View File

@ -11,6 +11,8 @@ export default function changeRole(userId, role) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(userId, String);
check(role, String);

View File

@ -12,6 +12,8 @@ export default function removeUser(userId, banUser) {
const { meetingId, requesterUserId: ejectedBy } = extractCredentials(this.userId);
check(meetingId, String);
check(ejectedBy, String);
check(userId, String);
const payload = {

View File

@ -11,6 +11,8 @@ export default function setEmojiStatus(userId, status) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(userId, String);
const payload = {

View File

@ -1,6 +1,7 @@
import { Meteor } from 'meteor/meteor';
import RedisPubSub from '/imports/startup/server/redis';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function setRandomUser() {
const REDIS_CONFIG = Meteor.settings.private.redis;
@ -9,6 +10,9 @@ export default function setRandomUser() {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const payload = {
requestedBy: requesterUserId,
};

View File

@ -11,6 +11,8 @@ export default function setUserEffectiveConnectionType(effectiveConnectionType)
const EVENT_NAME = 'ChangeUserEffectiveConnectionMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(effectiveConnectionType, String);
const payload = {

View File

@ -11,6 +11,7 @@ export default function toggleUserLock(userId, lock) {
const { meetingId, requesterUserId: lockedBy } = extractCredentials(this.userId);
check(meetingId, String);
check(lockedBy, String);
check(userId, String);
check(lock, Boolean);

View File

@ -3,12 +3,17 @@ import Users from '/imports/api/users';
import RedisPubSub from '/imports/startup/server/redis';
import Logger from '/imports/startup/server/logger';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function userActivitySign() {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'UserActivitySignCmdMsg';
const { meetingId, requesterUserId: userId } = extractCredentials(this.userId);
check(meetingId, String);
check(userId, String);
const payload = {
userId,
};

View File

@ -2,11 +2,15 @@ import Logger from '/imports/startup/server/logger';
import Users from '/imports/api/users';
import { extractCredentials } from '/imports/api/common/server/helpers';
import ClientConnections from '/imports/startup/server/ClientConnections';
import { check } from 'meteor/check';
export default function userLeftMeeting() { // TODO-- spread the code to method/modifier/handler
// so we don't update the db in a method
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const selector = {
meetingId,
userId: requesterUserId,

View File

@ -3,6 +3,7 @@ import { Meteor } from 'meteor/meteor';
import Logger from '/imports/startup/server/logger';
import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
@ -18,7 +19,7 @@ function currentUser() {
const selector = {
meetingId,
userId: requesterUserId,
intId: { $exists: true }
intId: { $exists: true },
};
const options = {
@ -57,7 +58,7 @@ function users(role) {
$or: [
{ meetingId },
],
intId: { $exists: true }
intId: { $exists: true },
};
const User = Users.findOne({ userId, meetingId }, { fields: { role: 1 } });

View File

@ -10,9 +10,12 @@ export default function userShareWebcam(stream) {
const EVENT_NAME = 'UserBroadcastCamStartMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(stream, String);
Logger.info(`user sharing webcam: ${meetingId} ${requesterUserId}`);
check(stream, String);
// const actionName = 'joinVideo';
/* TODO throw an error if user has no permission to share webcam

View File

@ -10,10 +10,12 @@ export default function userUnshareWebcam(stream) {
const EVENT_NAME = 'UserBroadcastCamStopMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
Logger.info(`user unsharing webcam: ${meetingId} ${requesterUserId}`);
check(meetingId, String);
check(requesterUserId, String);
check(stream, String);
Logger.info(`user unsharing webcam: ${meetingId} ${requesterUserId}`);
// const actionName = 'joinVideo';
/* TODO throw an error if user has no permission to share webcam
if (!isAllowedTo(actionName, credentials)) {

View File

@ -9,6 +9,9 @@ export default function ejectUserFromVoice(userId) {
const EVENT_NAME = 'EjectUserFromVoiceCmdMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(userId, String);
const payload = {

View File

@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor';
import RedisPubSub from '/imports/startup/server/redis';
import Meetings from '/imports/api/meetings';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function muteAllExceptPresenterToggle() {
const REDIS_CONFIG = Meteor.settings.private.redis;
@ -10,6 +11,9 @@ export default function muteAllExceptPresenterToggle() {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const meeting = Meetings.findOne({ meetingId });
const toggleMeetingMuted = !meeting.voiceProp.muteOnStart;

View File

@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor';
import RedisPubSub from '/imports/startup/server/redis';
import Meetings from '/imports/api/meetings';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';
export default function muteAllToggle() {
const REDIS_CONFIG = Meteor.settings.private.redis;
@ -10,6 +11,9 @@ export default function muteAllToggle() {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const meeting = Meetings.findOne({ meetingId });
const toggleMeetingMuted = !meeting.voiceProp.muteOnStart;

View File

@ -5,6 +5,7 @@ import Users from '/imports/api/users';
import VoiceUsers from '/imports/api/voice-users';
import Meetings from '/imports/api/meetings';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
export default function muteToggle(uId, toggle) {
const REDIS_CONFIG = Meteor.settings.private.redis;
@ -12,6 +13,10 @@ export default function muteToggle(uId, toggle) {
const EVENT_NAME = 'MuteUserCmdMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const userToMute = uId || requesterUserId;
const requester = Users.findOne({

View File

@ -4,7 +4,7 @@ import modifyWhiteboardAccess from '../modifiers/modifyWhiteboardAccess';
export default function handleModifyWhiteboardAccess({ body }, meetingId) {
const { multiUser, whiteboardId } = body;
check(multiUser, Boolean);
check(multiUser, Array);
check(whiteboardId, String);
check(meetingId, String);

View File

@ -0,0 +1,31 @@
import Users from '/imports/api/users';
import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user/';
const getMultiUser = (meetingId, whiteboardId) => {
const data = WhiteboardMultiUser.findOne(
{
meetingId,
whiteboardId,
}, { fields: { multiUser: 1 } },
);
if (!data || !data.multiUser || !Array.isArray(data.multiUser)) return [];
return data.multiUser;
};
const getUsers = (meetingId) => {
const data = Users.find(
{ meetingId },
{ fields: { userId: 1 } },
).fetch();
if (!data) return [];
return data.map(user => user.userId);
};
export {
getMultiUser,
getUsers,
};

View File

@ -1,6 +1,12 @@
import { Meteor } from 'meteor/meteor';
import changeWhiteboardAccess from './methods/changeWhiteboardAccess';
import addGlobalAccess from './methods/addGlobalAccess';
import addIndividualAccess from './methods/addIndividualAccess';
import removeGlobalAccess from './methods/removeGlobalAccess';
import removeIndividualAccess from './methods/removeIndividualAccess';
Meteor.methods({
changeWhiteboardAccess,
addGlobalAccess,
addIndividualAccess,
removeGlobalAccess,
removeIndividualAccess,
});

View File

@ -0,0 +1,27 @@
import RedisPubSub from '/imports/startup/server/redis';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { getUsers } from '/imports/api/whiteboard-multi-user/server/helpers';
import { extractCredentials } from '/imports/api/common/server/helpers';
export default function addGlobalAccess(whiteboardId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'ModifyWhiteboardAccessPubMsg';
check(whiteboardId, String);
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const multiUser = getUsers(meetingId);
const payload = {
multiUser,
whiteboardId,
};
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}

View File

@ -0,0 +1,32 @@
import RedisPubSub from '/imports/startup/server/redis';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { getMultiUser } from '/imports/api/whiteboard-multi-user/server/helpers';
import { extractCredentials } from '/imports/api/common/server/helpers';
export default function addIndividualAccess(whiteboardId, userId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'ModifyWhiteboardAccessPubMsg';
check(whiteboardId, String);
check(userId, String);
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const multiUser = getMultiUser(meetingId, whiteboardId);
if (!multiUser.includes(userId)) {
multiUser.push(userId);
const payload = {
multiUser,
whiteboardId,
};
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}
}

View File

@ -3,18 +3,20 @@ import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { extractCredentials } from '/imports/api/common/server/helpers';
export default function changeWhiteboardAccess(multiUser, whiteboardId) {
export default function removeGlobalAccess(whiteboardId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'ModifyWhiteboardAccessPubMsg';
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(multiUser, Boolean);
check(whiteboardId, String);
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const payload = {
multiUser,
multiUser: [],
whiteboardId,
};

View File

@ -0,0 +1,30 @@
import RedisPubSub from '/imports/startup/server/redis';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { getMultiUser } from '/imports/api/whiteboard-multi-user/server/helpers';
import { extractCredentials } from '/imports/api/common/server/helpers';
export default function removeIndividualAccess(whiteboardId, userId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'ModifyWhiteboardAccessPubMsg';
check(whiteboardId, String);
check(userId, String);
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const multiUser = getMultiUser(meetingId, whiteboardId);
if (multiUser.includes(userId)) {
const payload = {
multiUser: multiUser.filter(id => id !== userId),
whiteboardId,
};
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}
}

Some files were not shown because too many files have changed in this diff Show More