diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Users2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Users2x.scala index 9fe5d4e652..0cba9f2142 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Users2x.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Users2x.scala @@ -252,11 +252,10 @@ case class UserState( locked: Boolean, presenter: Boolean, avatar: String, - roleChangedOn: Long = System.currentTimeMillis(), - lastActivityTime: Long = TimeUtil.timeNowInMs(), + roleChangedOn: Long = System.currentTimeMillis(), + lastActivityTime: Long = TimeUtil.timeNowInMs(), clientType: String, - userLeftFlag: UserLeftFlag) - + userLeftFlag: UserLeftFlag) case class UserIdAndName(id: String, name: String) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index 61c607cd42..280978f8da 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -53,34 +53,34 @@ class MeetingActor( val eventBus: InternalEventBus, val outGW: OutMsgRouter, val liveMeeting: LiveMeeting) - extends BaseMeetingActor - with SystemConfiguration - with GuestsApp - with LayoutApp2x - with VoiceApp2x - with BreakoutApp2x - with UsersApp2x + extends BaseMeetingActor + with SystemConfiguration + with GuestsApp + with LayoutApp2x + with VoiceApp2x + with BreakoutApp2x + with UsersApp2x - with UserBroadcastCamStartMsgHdlr - with UserJoinMeetingReqMsgHdlr - with UserJoinMeetingAfterReconnectReqMsgHdlr - with UserBroadcastCamStopMsgHdlr - with UserConnectedToGlobalAudioMsgHdlr - with UserDisconnectedFromGlobalAudioMsgHdlr - with MuteAllExceptPresentersCmdMsgHdlr - with MuteMeetingCmdMsgHdlr - with IsMeetingMutedReqMsgHdlr + with UserBroadcastCamStartMsgHdlr + with UserJoinMeetingReqMsgHdlr + with UserJoinMeetingAfterReconnectReqMsgHdlr + with UserBroadcastCamStopMsgHdlr + with UserConnectedToGlobalAudioMsgHdlr + with UserDisconnectedFromGlobalAudioMsgHdlr + with MuteAllExceptPresentersCmdMsgHdlr + with MuteMeetingCmdMsgHdlr + with IsMeetingMutedReqMsgHdlr - with EjectUserFromVoiceCmdMsgHdlr - with EndMeetingSysCmdMsgHdlr - with DestroyMeetingSysCmdMsgHdlr - with SendTimeRemainingUpdateHdlr - with SendBreakoutTimeRemainingMsgHdlr - with ChangeLockSettingsInMeetingCmdMsgHdlr - with SyncGetMeetingInfoRespMsgHdlr - with ClientToServerLatencyTracerMsgHdlr - with ValidateConnAuthTokenSysMsgHdlr - with UserActivitySignCmdMsgHdlr { + with EjectUserFromVoiceCmdMsgHdlr + with EndMeetingSysCmdMsgHdlr + with DestroyMeetingSysCmdMsgHdlr + with SendTimeRemainingUpdateHdlr + with SendBreakoutTimeRemainingMsgHdlr + with ChangeLockSettingsInMeetingCmdMsgHdlr + with SyncGetMeetingInfoRespMsgHdlr + with ClientToServerLatencyTracerMsgHdlr + with ValidateConnAuthTokenSysMsgHdlr + with UserActivitySignCmdMsgHdlr { override val supervisorStrategy = OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) { case e: Exception => { @@ -179,6 +179,7 @@ class MeetingActor( def receive = { //============================= + // 2x messages case msg: BbbCommonEnvCoreMsg => handleBbbCommonEnvCoreMsg(msg) @@ -240,6 +241,19 @@ class MeetingActor( } private def handleBbbCommonEnvCoreMsg(msg: BbbCommonEnvCoreMsg): Unit = { + msg.core match { + case m: ClientToServerLatencyTracerMsg => handleMessageThatDoesNotAffectsInactivity(msg) + case _ => handleMessageThatAffectsInactivity(msg) + } + } + + private def handleMessageThatDoesNotAffectsInactivity(msg: BbbCommonEnvCoreMsg): Unit = { + msg.core match { + case m: ClientToServerLatencyTracerMsg => handleClientToServerLatencyTracerMsg(m) + } + } + + private def handleMessageThatAffectsInactivity(msg: BbbCommonEnvCoreMsg): Unit = { msg.core match { case m: EndMeetingSysCmdMsg => handleEndMeeting(m, state) @@ -287,8 +301,6 @@ class MeetingActor( case m: SendWhiteboardAnnotationPubMsg => wbApp.handle(m, liveMeeting, msgBus) case m: GetWhiteboardAnnotationsReqMsg => wbApp.handle(m, liveMeeting, msgBus) - case m: ClientToServerLatencyTracerMsg => handleClientToServerLatencyTracerMsg(m) - // Poll case m: StartPollReqMsg => pollApp.handle(m, state, liveMeeting, msgBus) // passing state but not modifying it @@ -592,7 +604,10 @@ class MeetingActor( updateParentMeetingWithUsers() } - if (Users2x.numUsers(liveMeeting.users2x) == 0) { + if (state.expiryTracker.userHasJoined && + Users2x.numUsers(liveMeeting.users2x) == 0 + && !state.expiryTracker.lastUserLeftOnInMs.isDefined) { + log.info("Setting meeting no more users. meetingId=" + props.meetingProp.intId) val tracker = state.expiryTracker.setLastUserLeftOn(TimeUtil.timeNowInMs()) state.update(tracker) } else { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/testdata/FakeTestData.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/testdata/FakeTestData.scala index b9af74ca01..e0966b6bb9 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/testdata/FakeTestData.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/testdata/FakeTestData.scala @@ -69,8 +69,7 @@ trait FakeTestData { UserState(intId = regUser.id, extId = regUser.externId, name = regUser.name, role = regUser.role, guest = regUser.guest, authed = regUser.authed, guestStatus = regUser.guestStatus, emoji = "none", locked = false, presenter = false, avatar = regUser.avatarURL, clientType = "unknown", - userLeftFlag = UserLeftFlag(false, 0) - ) + userLeftFlag = UserLeftFlag(false, 0)) } - + } diff --git a/bbb-webhooks/.dockerignore b/bbb-webhooks/.dockerignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/bbb-webhooks/.dockerignore @@ -0,0 +1 @@ +node_modules/ diff --git a/bbb-webhooks/config.js b/bbb-webhooks/config.js index cfc7ccda07..4c06a78fbb 100644 --- a/bbb-webhooks/config.js +++ b/bbb-webhooks/config.js @@ -21,7 +21,10 @@ if (!config.hooks.channels) { config.hooks.channels = { mainChannel: 'from-akka-apps-redis-channel', rapChannel: 'bigbluebutton:from-rap', - chatChannel: 'from-akka-apps-chat-redis-channel' + chatChannel: 'from-akka-apps-chat-redis-channel', + compMeetingChannel: 'bigbluebutton:from-bbb-apps:meeting', + compUserChannel: 'bigbluebutton:from-bbb-apps:users', + compChatChannel: 'bigbluebutton:from-bbb-apps:chat' } } // IP where permanent hook will post data (more than 1 URL means more than 1 permanent hook) diff --git a/bbb-webhooks/messageMapping.js b/bbb-webhooks/messageMapping.js index 7ea2062c8a..3501d19fca 100644 --- a/bbb-webhooks/messageMapping.js +++ b/bbb-webhooks/messageMapping.js @@ -11,6 +11,9 @@ module.exports = class MessageMapping { this.userEvents = ["UserJoinedMeetingEvtMsg","UserLeftMeetingEvtMsg","UserJoinedVoiceConfToClientEvtMsg","UserLeftVoiceConfToClientEvtMsg","PresenterAssignedEvtMsg", "PresenterUnassignedEvtMsg", "UserBroadcastCamStartedEvtMsg", "UserBroadcastCamStoppedEvtMsg", "UserEmojiChangedEvtMsg"]; this.chatEvents = ["SendPublicMessageEvtMsg","SendPrivateMessageEvtMsg"]; this.rapEvents = ["archive_started","archive_ended","sanity_started","sanity_ended","post_archive_started","post_archive_ended","process_started","process_ended","post_process_started","post_process_ended","publish_started","publish_ended","post_publish_started","post_publish_ended"]; + + this.compMeetingEvents = ["meeting_created_message","meeting_destroyed_event"]; + this.compUserEvents = ["user_joined_message","user_left_message","user_listening_only","user_joined_voice_message","user_left_voice_message","user_shared_webcam_message","user_unshared_webcam_message","user_status_changed_message"]; } // Map internal message based on it's type @@ -23,6 +26,10 @@ module.exports = class MessageMapping { this.chatTemplate(messageObj); } else if (this.mappedEvent(messageObj,this.rapEvents)) { this.rapTemplate(messageObj); + } else if (this.mappedEvent(messageObj,this.compMeetingEvents)) { + this.compMeetingTemplate(messageObj); + } else if (this.mappedEvent(messageObj,this.compUserEvents)) { + this.compUserTemplate(messageObj); } } @@ -79,6 +86,46 @@ module.exports = class MessageMapping { Logger.info("[MessageMapping] Mapped message:", this.mappedMessage); } + compMeetingTemplate(messageObj) { + const props = messageObj.payload; + const meetingId = props.meeting_id; + this.mappedObject.data = { + "type": "event", + "id": this.mapInternalMessage(messageObj), + "attributes":{ + "meeting":{ + "internal-meeting-id": meetingId, + "external-meeting-id": IDMapping.getExternalMeetingID(meetingId) + } + }, + "event":{ + "ts": Date.now() + } + }; + if (messageObj.header.name === "meeting_created_message") { + this.mappedObject.data.attributes = { + "meeting":{ + "internal-meeting-id": meetingId, + "external-meeting-id": props.external_meeting_id, + "name": props.name, + "is-breakout": props.is_breakout, + "duration": props.duration, + "create-time": props.create_time, + "create-date": props.create_date, + "moderator-pass": props.moderator_pass, + "viewer-pass": props.viewer_pass, + "record": props.recorded, + "voice-conf": props.voice_conf, + "dial-number": props.dial_number, + "max-users": props.max_users, + "metadata": props.metadata + } + }; + } + this.mappedMessage = JSON.stringify(this.mappedObject); + Logger.info("[MessageMapping] Mapped message:", this.mappedMessage); + } + // Map internal to external message for user information userTemplate(messageObj) { const msgBody = messageObj.core.body; @@ -111,6 +158,68 @@ module.exports = class MessageMapping { Logger.info("[MessageMapping] Mapped message:", this.mappedMessage); } + // Map internal to external message for user information + compUserTemplate(messageObj) { + const msgBody = messageObj.payload; + const msgHeader = messageObj.header; + + let user; + if (msgHeader.name === "user_joined_message") { + user = { + "internal-user-id": msgBody.user.userid, + "external-user-id": msgBody.user.extern_userid, + "sharing-mic": msgBody.user.voiceUser.joined, + "name": msgBody.user.name, + "role": msgBody.user.role, + "presenter": msgBody.user.presenter, + "stream": msgBody.user.webcam_stream, + "listening-only": msgBody.user.listenOnly + } + } + else { + user = UserMapping.getUser(msgBody.userid) || { "internal-user-id": msgBody.userid || msgBody.user.userid }; + if (msgHeader.name === "user_status_changed_message") { + if (msgBody.status === "presenter") { + user["presenter"] = msgBody.value; + } + } + else if (msgHeader.name === "user_listening_only") { + user["listening-only"] = msgBody.listen_only; + } + else if (msgHeader.name === "user_joined_voice_message" || msgHeader.name === "user_left_voice_message") { + user["sharing-mic"] = msgBody.user.voiceUser.joined; + } + else if (msgHeader.name === "user_shared_webcam_message") { + user["stream"].push(msgBody.stream); + } + else if (msgHeader.name === "user_unshared_webcam_message") { + let streams = user["stream"]; + let index = streams.indexOf(msgBody.stream); + if (index != -1) { + streams.splice(index,1); + } + user["stream"] = streams; + } + } + + this.mappedObject.data = { + "type": "event", + "id": this.mapInternalMessage(messageObj), + "attributes":{ + "meeting":{ + "internal-meeting-id": msgBody.meeting_id, + "external-meeting-id": IDMapping.getExternalMeetingID(msgBody.meeting_id) + }, + "user": user + }, + "event":{ + "ts": Date.now() + } + }; + this.mappedMessage = JSON.stringify(this.mappedObject); + Logger.info("[MessageMapping] Mapped message:", this.mappedMessage); + } + // Map internal to external message for chat information chatTemplate(messageObj) { const message = messageObj.core.body.message; @@ -150,12 +259,13 @@ module.exports = class MessageMapping { const data = messageObj.payload; this.mappedObject.data = { "type": "event", - "id": this.mapInternalMessage(messageObj.header.name), + "id": this.mapInternalMessage(messageObj), "attributes": { "meeting": { "internal-meeting-id": data.meeting_id, - "external-meeting-id": data.external_meeting_id + "external-meeting-id": data.external_meeting_id || IDMapping.getExternalMeetingID(data.meeting_id) }, + "record-id": data.record_id, "success": data.success, "step-time": data.step_time }, @@ -164,14 +274,18 @@ module.exports = class MessageMapping { } }; - if (this.mappedObject.data["id"] == "rap-publish-ended") { - this.mappedObject.data["attributes"]["recording"] = { + if (data.workflow) { + this.mappedObject.data.attributes.workflow = data.workflow; + } + + if (this.mappedObject.data.id === "rap-publish-ended") { + this.mappedObject.data.attributes.recording = { "name": data.metadata.meetingName, - "isBreakout": data.metadata.isBreakout, - "startTime": data.startTime, - "endTime": data.endTime, + "is-breakout": data.metadata.isBreakout, + "start-time": data.startTime, + "end-time": data.endTime, "size": data.playback.size, - "rawSize": data.rawSize, + "raw-size": data.rawSize, "metadata": data.metadata, "playback": data.playback, "download": data.download @@ -183,13 +297,14 @@ module.exports = class MessageMapping { mapInternalMessage(message) { + let name; if (message.envelope) { - message = message.envelope.name + name = message.envelope.name } else if (message.header) { - message = message.header.name + name = message.header.name } - const mappedMsg = (() => { switch (message) { + const mappedMsg = (() => { switch (name) { case "MeetingCreatedEvtMsg": return "meeting-created"; case "MeetingDestroyedEvtMsg": return "meeting-ended"; case "RecordingStatusChangedEvtMsg": return "meeting-recording-changed"; @@ -221,6 +336,19 @@ module.exports = class MessageMapping { case "publish_ended": return "rap-publish-ended"; case "post_publish_started": return "rap-post-publish-started"; case "post_publish_ended": return "rap-post-publish-ended"; + case "meeting_created_message": return "meeting-created"; + case "meeting_destroyed_event": return "meeting-ended"; + case "user_joined_message": return "user-joined"; + case "user_left_message": return "user-left"; + case "user_listening_only": return (message.payload.listen_only ? "user-audio-listen-only-enabled" : "user-audio-listen-only-disabled"); + case "user_joined_voice_message": return "user-audio-voice-enabled"; + case "user_left_voice_message": return "user-audio-voice-disabled"; + case "user_shared_webcam_message": return "user-cam-broadcast-start"; + case "video_stream_unpublished": return "user-cam-broadcast-end"; + case "user_status_changed_message": + if (message.payload.status === "presenter") { + return (message.payload.value === "true" ? "user-presenter-assigned" : "user-presenter-unassigned" ); + } } })(); return mappedMsg; } diff --git a/bbb-webhooks/userMapping.js b/bbb-webhooks/userMapping.js index 185e2fc9a4..bf11b4a031 100644 --- a/bbb-webhooks/userMapping.js +++ b/bbb-webhooks/userMapping.js @@ -32,16 +32,18 @@ module.exports = class UserMapping { this.externalUserID = null; this.internalUserID = null; this.meetingId = null; + this.user = null; this.redisClient = config.redis.client; } save(callback) { + db[this.internalUserID] = this; + this.redisClient.hmset(config.redis.keys.userMap(this.id), this.toRedis(), (error, reply) => { if (error != null) { Logger.error("[UserMapping] error saving mapping to redis:", error, reply); } this.redisClient.sadd(config.redis.keys.userMaps, this.id, (error, reply) => { if (error != null) { Logger.error("[UserMapping] error saving mapping ID to the list of mappings:", error, reply); } - db[this.internalUserID] = this; (typeof callback === 'function' ? callback(error, db[this.internalUserID]) : undefined); }); }); @@ -68,7 +70,8 @@ module.exports = class UserMapping { "id": this.id, "internalUserID": this.internalUserID, "externalUserID": this.externalUserID, - "meetingId": this.meetingId + "meetingId": this.meetingId, + "user": this.user }; return r; } @@ -78,18 +81,20 @@ module.exports = class UserMapping { this.externalUserID = redisData.externalUserID; this.internalUserID = redisData.internalUserID; this.meetingId = redisData.meetingId; + this.user = redisData.user; } print() { return JSON.stringify(this.toRedis()); } - static addMapping(internalUserID, externalUserID, meetingId, callback) { + static addOrUpdateMapping(internalUserID, externalUserID, meetingId, user, callback) { let mapping = new UserMapping(); mapping.id = nextID++; mapping.internalUserID = internalUserID; mapping.externalUserID = externalUserID; mapping.meetingId = meetingId; + mapping.user = user; mapping.save(function(error, result) { Logger.info(`[UserMapping] added user mapping to the list ${internalUserID}:`, mapping.print()); (typeof callback === 'function' ? callback(error, result) : undefined); @@ -131,6 +136,12 @@ module.exports = class UserMapping { })(); } + static getUser(internalUserID) { + if (db[internalUserID]){ + return db[internalUserID].user; + } + } + static getExternalUserID(internalUserID) { if (db[internalUserID]){ return db[internalUserID].externalUserID; diff --git a/bbb-webhooks/web_hooks.js b/bbb-webhooks/web_hooks.js index c00a2cf0b1..787027787a 100644 --- a/bbb-webhooks/web_hooks.js +++ b/bbb-webhooks/web_hooks.js @@ -54,7 +54,7 @@ module.exports = class WebHooks { }); break; case "user-joined": - UserMapping.addMapping(message.data.attributes.user["internal-user-id"],message.data.attributes.user["external-user-id"], intId, () => { + UserMapping.addOrUpdateMapping(message.data.attributes.user["internal-user-id"],message.data.attributes.user["external-user-id"], intId, message.data.attributes.user, () => { processMessage(); }); break; diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js index ba2635714d..2961cfd2cb 100755 --- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js +++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js @@ -12,6 +12,7 @@ const CALL_TRANSFER_TIMEOUT = MEDIA.callTransferTimeout; const CALL_HANGUP_TIMEOUT = MEDIA.callHangupTimeout; const CALL_HANGUP_MAX_RETRIES = MEDIA.callHangupMaximumRetries; const CONNECTION_TERMINATED_EVENTS = ['iceConnectionFailed', 'iceConnectionClosed']; +const CALL_CONNECT_NOTIFICATION_TIMEOUT = 500; export default class SIPBridge extends BaseAudioBridge { constructor(userData) { @@ -275,9 +276,15 @@ export default class SIPBridge extends BaseAudioBridge { const connectionCompletedEvents = ['iceConnectionCompleted', 'iceConnectionConnected']; const handleConnectionCompleted = () => { connectionCompletedEvents.forEach(e => mediaHandler.off(e, handleConnectionCompleted)); - this.callback({ status: this.baseCallStates.started }); - this.connectionCompleted = true; - resolve(); + // We have to delay notifying that the call is connected because it is sometimes not + // actually ready and if the user says "Yes they can hear themselves" too quickly the + // B-leg transfer will fail + const that = this; + setTimeout(() => { + that.callback({ status: that.baseCallStates.started }); + that.connectionCompleted = true; + resolve(); + }, CALL_CONNECT_NOTIFICATION_TIMEOUT); }; connectionCompletedEvents.forEach(e => mediaHandler.on(e, handleConnectionCompleted)); diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss index 06a6d28322..3e42276c40 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss @@ -70,7 +70,7 @@ position: relative; height: 100%; width: 100%; - object-fit: cover; + object-fit: contain; border-radius: 5px; }