From 4d1aa87a88c72fcc694f8fe81be5823c04fd2965 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 30 May 2023 14:56:50 -0300 Subject: [PATCH] feat: only record media while meeting is being actively recorded Only record media (microphone, webcams and screens) while meeting is being actively recorded (ie an user has enabled recording in the conference). If the conference's recording is paused, media capture will stop as well (with appropriate recording events). A bigbluebutton.properties/API#create parameter called `recordFullDurationMedia` is added to control this behavior. The default is false (only capture while recording is active). Setting it to `true` enables the current (legacy) behavior: always capture media if the meeting's `recorded` prop is true. --- .../users/GetRecordingStatusReqMsgHdlr.scala | 20 ++++++++--- .../users/SetRecordingStatusCmdMsgHdlr.scala | 22 ++++++++++++ .../core/apps/voice/VoiceApp.scala | 35 ++++++++++++------- .../voice/VoiceConfRunningEvtMsgHdlr.scala | 14 +++----- .../core/running/MeetingActor.scala | 29 ++++++--------- .../common2/domain/Meeting2x.scala | 2 +- .../common2/msgs/UsersMsgs.scala | 7 +++- .../bigbluebutton/common2/TestFixtures.scala | 6 +++- .../java/org/bigbluebutton/api/ApiParams.java | 2 ++ .../org/bigbluebutton/api/MeetingService.java | 3 +- .../api/ParamsProcessorUtil.java | 20 ++++++++++- .../org/bigbluebutton/api/domain/Meeting.java | 14 +++++++- .../messages/CreateMeetingMessage.java | 11 +++--- .../api/pub/IPublisherService.java | 7 ++-- .../bigbluebutton/api2/IBbbWebApiGWApp.java | 4 ++- .../api2/domain/RecordProp2.java | 6 +++- .../bigbluebutton/api2/BbbWebApiGWApp.scala | 8 +++-- bbb-webrtc-sfu.placeholder.sh | 2 +- .../grails-app/conf/bigbluebutton.properties | 5 +++ .../grails-app/conf/spring/resources.xml | 1 + .../web/controllers/ApiController.groovy | 1 + docs/docs/administration/customize.md | 1 + docs/docs/data/create.tsx | 7 ++++ docs/docs/development/api.md | 2 +- 24 files changed, 167 insertions(+), 62 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/GetRecordingStatusReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/GetRecordingStatusReqMsgHdlr.scala index c7cf9fc74f..f2f76250a2 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/GetRecordingStatusReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/GetRecordingStatusReqMsgHdlr.scala @@ -12,18 +12,30 @@ trait GetRecordingStatusReqMsgHdlr { def handleGetRecordingStatusReqMsg(msg: GetRecordingStatusReqMsg) { - def buildGetRecordingStatusRespMsg(meetingId: String, userId: String, recorded: Boolean, recording: Boolean): BbbCommonEnvCoreMsg = { + def buildGetRecordingStatusRespMsg( + meetingId: String, + userId: String, + recorded: Boolean, + recording: Boolean, + recordFullDurationMedia: Boolean + ): BbbCommonEnvCoreMsg = { val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, userId) val envelope = BbbCoreEnvelope(GetRecordingStatusRespMsg.NAME, routing) - val body = GetRecordingStatusRespMsgBody(recorded, recording, userId) + val body = GetRecordingStatusRespMsgBody(recorded, recording, recordFullDurationMedia, userId) val header = BbbClientMsgHeader(GetRecordingStatusRespMsg.NAME, meetingId, userId) val event = GetRecordingStatusRespMsg(header, body) BbbCommonEnvCoreMsg(envelope, event) } - val event = buildGetRecordingStatusRespMsg(liveMeeting.props.meetingProp.intId, msg.body.requestedBy, - liveMeeting.props.recordProp.record, MeetingStatus2x.isRecording(liveMeeting.status)) + val event = buildGetRecordingStatusRespMsg( + liveMeeting.props.meetingProp.intId, + msg.body.requestedBy, + liveMeeting.props.recordProp.record, + MeetingStatus2x.isRecording(liveMeeting.status), + liveMeeting.props.recordProp.recordFullDurationMedia + ) + outGW.send(event) } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SetRecordingStatusCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SetRecordingStatusCmdMsgHdlr.scala index 9274e2d2e3..01a076b6f2 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SetRecordingStatusCmdMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SetRecordingStatusCmdMsgHdlr.scala @@ -9,6 +9,7 @@ import org.bigbluebutton.core.bus.BigBlueButtonEvent import org.bigbluebutton.core.api.SendRecordingTimerInternalMsg import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait } import org.bigbluebutton.core2.message.senders.{ MsgBuilder } +import org.bigbluebutton.core.apps.voice.VoiceApp trait SetRecordingStatusCmdMsgHdlr extends RightsManagementTrait { this: UsersApp => @@ -49,6 +50,19 @@ trait SetRecordingStatusCmdMsgHdlr extends RightsManagementTrait { outGW.send(notifyEvent) MeetingStatus2x.recordingStarted(liveMeeting.status) + + // If meeting is not set to record full duration media, then we need to + // start recording media here. Audio/FS recording is triggered here; + // SFU intercepts this event and toggles rec for video and screen sharing. + if (!liveMeeting.props.recordProp.recordFullDurationMedia) { + log.info("Send START RECORDING voice conf. meetingId=" + + liveMeeting.props.meetingProp.intId + + " voice conf=" + liveMeeting.props.voiceProp.voiceConf) + VoiceApp.startRecordingVoiceConference( + liveMeeting, + outGW + ) + } } else { val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg( liveMeeting.props.meetingProp.intId, @@ -61,6 +75,14 @@ trait SetRecordingStatusCmdMsgHdlr extends RightsManagementTrait { outGW.send(notifyEvent) MeetingStatus2x.recordingStopped(liveMeeting.status) + + // If meeting is not set to record full duration media, then we need to stop recording + if (!liveMeeting.props.recordProp.recordFullDurationMedia) { + VoiceApp.stopRecordingVoiceConference( + liveMeeting, + outGW + ) + } } val event = buildRecordingStatusChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.setBy, msg.body.recording) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp.scala index 0a80fe578d..96fe80b810 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp.scala @@ -11,9 +11,10 @@ import org.bigbluebutton.core.running.{LiveMeeting, MeetingActor, OutMsgRouter} import org.bigbluebutton.core.models._ import org.bigbluebutton.core.apps.users.UsersApp import org.bigbluebutton.core.util.ColorPicker +import org.bigbluebutton.core.util.TimeUtil + object VoiceApp extends SystemConfiguration { - def genRecordPath( recordDir: String, meetingId: String, @@ -35,18 +36,7 @@ object VoiceApp extends SystemConfiguration { } } - def startRecordingVoiceConference(liveMeeting: LiveMeeting, outGW: OutMsgRouter, stream: String): Unit = { - MeetingStatus2x.voiceRecordingStart(liveMeeting.status, stream) - val event = MsgBuilder.buildStartRecordingVoiceConfSysMsg( - liveMeeting.props.meetingProp.intId, - liveMeeting.props.voiceProp.voiceConf, - stream - ) - outGW.send(event) - } - def stopRecordingVoiceConference(liveMeeting: LiveMeeting, outGW: OutMsgRouter): Unit = { - val recStreams = MeetingStatus2x.getVoiceRecordingStreams(liveMeeting.status) recStreams foreach { rs => @@ -58,6 +48,27 @@ object VoiceApp extends SystemConfiguration { } } + def startRecordingVoiceConference( + liveMeeting: LiveMeeting, + outGW: OutMsgRouter + ): Unit = { + val meetingId = liveMeeting.props.meetingProp.intId + val now = TimeUtil.timeNowInMs() + val recordFile = genRecordPath( + voiceConfRecordPath, + meetingId, + now, + voiceConfRecordCodec + ) + MeetingStatus2x.voiceRecordingStart(liveMeeting.status, recordFile) + val event = MsgBuilder.buildStartRecordingVoiceConfSysMsg( + liveMeeting.props.meetingProp.intId, + liveMeeting.props.voiceProp.voiceConf, + recordFile + ) + outGW.send(event) + } + def broadcastUserMutedVoiceEvtMsg( meetingId: String, vu: VoiceUserState, diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceConfRunningEvtMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceConfRunningEvtMsgHdlr.scala index 5fae886469..4ecf2e73a0 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceConfRunningEvtMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceConfRunningEvtMsgHdlr.scala @@ -3,7 +3,8 @@ package org.bigbluebutton.core.apps.voice import org.bigbluebutton.SystemConfiguration import org.bigbluebutton.common2.msgs.VoiceConfRunningEvtMsg import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting, OutMsgRouter } -import org.bigbluebutton.core.util.TimeUtil +import org.bigbluebutton.core2.MeetingStatus2x +import org.bigbluebutton.core.apps.voice.VoiceApp trait VoiceConfRunningEvtMsgHdlr extends SystemConfiguration { this: BaseMeetingActor => @@ -15,17 +16,12 @@ trait VoiceConfRunningEvtMsgHdlr extends SystemConfiguration { log.info("Received VoiceConfRunningEvtMsg " + msg.body.running) if (liveMeeting.props.recordProp.record) { - if (msg.body.running) { + if (msg.body.running && + (MeetingStatus2x.isRecording(liveMeeting.status) || liveMeeting.props.recordProp.recordFullDurationMedia)) { val meetingId = liveMeeting.props.meetingProp.intId - val recordFile = VoiceApp.genRecordPath( - voiceConfRecordPath, - meetingId, - TimeUtil.timeNowInMs(), - voiceConfRecordCodec - ) log.info("Send START RECORDING voice conf. meetingId=" + meetingId + " voice conf=" + liveMeeting.props.voiceProp.voiceConf) - VoiceApp.startRecordingVoiceConference(liveMeeting, outGW, recordFile) + VoiceApp.startRecordingVoiceConference(liveMeeting, outGW) } else { VoiceApp.stopRecordingVoiceConference(liveMeeting, outGW) } 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 94b326ee44..3a01f2d145 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 @@ -779,6 +779,7 @@ class MeetingActor( val elapsedInMin = TimeUtil.millisToMinutes(elapsedInMs) if (props.recordProp.record && + (MeetingStatus2x.isRecording(liveMeeting.status) || props.recordProp.recordFullDurationMedia) && recordingChapterBreakLengthInMinutes > 0 && elapsedInMin > recordingChapterBreakLengthInMinutes) { lastRecBreakSentOn = now @@ -786,15 +787,7 @@ class MeetingActor( outGW.send(event) VoiceApp.stopRecordingVoiceConference(liveMeeting, outGW) - - val meetingId = liveMeeting.props.meetingProp.intId - val recordFile = VoiceApp.genRecordPath( - voiceConfRecordPath, - meetingId, - now, - voiceConfRecordCodec - ) - VoiceApp.startRecordingVoiceConference(liveMeeting, outGW, recordFile) + VoiceApp.startRecordingVoiceConference(liveMeeting, outGW) } } @@ -993,17 +986,15 @@ class MeetingActor( // Remove recording streams that have stopped so we should only have // one active recording stream. - // Let us start recording. - val meetingId = liveMeeting.props.meetingProp.intId - val recordFile = VoiceApp.genRecordPath( - voiceConfRecordPath, - meetingId, - TimeUtil.timeNowInMs(), - voiceConfRecordCodec - ) - log.info("Forcing START RECORDING voice conf. meetingId=" + meetingId + " voice conf=" + liveMeeting.props.voiceProp.voiceConf) + // If the meeting is being actively recorded or recordFullDurationMedia is true + // then we should start recording. + if (MeetingStatus2x.isRecording(liveMeeting.status) || + liveMeeting.props.recordProp.recordFullDurationMedia) { + val meetingId = liveMeeting.props.meetingProp.intId + log.info("Forcing START RECORDING voice conf. meetingId=" + meetingId + " voice conf=" + liveMeeting.props.voiceProp.voiceConf) - VoiceApp.startRecordingVoiceConference(liveMeeting, outGW, recordFile) + VoiceApp.startRecordingVoiceConference(liveMeeting, outGW) + } } } diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala index 30ec1370ec..d3fd4db930 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala @@ -34,7 +34,7 @@ case class BreakoutProps( case class PasswordProp(moderatorPass: String, viewerPass: String, learningDashboardAccessToken: String) -case class RecordProp(record: Boolean, autoStartRecording: Boolean, allowStartStopRecording: Boolean, keepEvents: Boolean) +case class RecordProp(record: Boolean, autoStartRecording: Boolean, allowStartStopRecording: Boolean, recordFullDurationMedia: Boolean, keepEvents: Boolean) case class WelcomeProp(welcomeMsgTemplate: String, welcomeMsg: String, modOnlyMessage: String) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala index 9e7d62f8f2..3dd1bbb87c 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/UsersMsgs.scala @@ -124,7 +124,12 @@ case class GetRecordingStatusReqMsgBody(requestedBy: String) */ object GetRecordingStatusRespMsg { val NAME = "GetRecordingStatusRespMsg" } case class GetRecordingStatusRespMsg(header: BbbClientMsgHeader, body: GetRecordingStatusRespMsgBody) extends BbbCoreMsg -case class GetRecordingStatusRespMsgBody(recorded: Boolean, recording: Boolean, requestedBy: String) +case class GetRecordingStatusRespMsgBody( + recorded: Boolean, + recording: Boolean, + recordFullDurationMedia: Boolean, + requestedBy: String +) /** * Sent by user to start recording mark. diff --git a/bbb-common-message/src/test/scala/org/bigbluebutton/common2/TestFixtures.scala b/bbb-common-message/src/test/scala/org/bigbluebutton/common2/TestFixtures.scala index 3bcdb6d5bf..88cc48c925 100755 --- a/bbb-common-message/src/test/scala/org/bigbluebutton/common2/TestFixtures.scala +++ b/bbb-common-message/src/test/scala/org/bigbluebutton/common2/TestFixtures.scala @@ -22,6 +22,7 @@ trait TestFixtures { val autoStartRecording = false val allowStartStopRecording = false + val recordFullDurationMedia = false val webcamsOnlyForModerator = false val meetingCameraCap = 0 val userCameraCap = 0 @@ -61,7 +62,10 @@ trait TestFixtures { userInactivityInspectTimerInMinutes = userInactivityInspectTimerInMinutes, userInactivityThresholdInMinutes = userInactivityInspectTimerInMinutes, userActivitySignResponseDelayInMinutes = userActivitySignResponseDelayInMinutes) val password = PasswordProp(moderatorPass = moderatorPassword, viewerPass = viewerPassword, learningDashboardAccessToken = learningDashboardAccessToken) val recordProp = RecordProp(record = record, autoStartRecording = autoStartRecording, - allowStartStopRecording = allowStartStopRecording, keepEvents = keepEvents) + allowStartStopRecording = allowStartStopRecording, + recordFullDurationMedia = recordFullDurationMedia, + keepEvents = keepEvents + ) val welcomeProp = WelcomeProp(welcomeMsgTemplate = welcomeMsgTemplate, welcomeMsg = welcomeMsg, modOnlyMessage = modOnlyMessage) val voiceProp = VoiceProp(telVoice = voiceConfId, voiceConf = voiceConfId, dialNumber = dialNumber, muteOnStart = muteOnStart) diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java index c47dea087e..af0e43f4ee 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ApiParams.java @@ -107,6 +107,8 @@ public class ApiParams { public static final String END_WHEN_NO_MODERATOR = "endWhenNoModerator"; public static final String END_WHEN_NO_MODERATOR_DELAY_IN_MINUTES = "endWhenNoModeratorDelayInMinutes"; + public static final String RECORD_FULL_DURATION_MEDIA = "recordFullDurationMedia"; + private ApiParams() { throw new IllegalStateException("ApiParams is a utility class. Instanciation is forbidden."); } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java index 7c7e3c977a..bf918aa949 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java @@ -400,6 +400,7 @@ public class MeetingService implements MessageListener { gw.createMeeting(m.getInternalId(), m.getExternalId(), m.getParentMeetingId(), m.getName(), m.isRecord(), m.getTelVoice(), m.getDuration(), m.getAutoStartRecording(), m.getAllowStartStopRecording(), + m.getRecordFullDurationMedia(), m.getWebcamsOnlyForModerator(), m.getMeetingCameraCap(), m.getUserCameraCap(), m.getMaxPinnedCameras(), m.getModeratorPassword(), m.getViewerPassword(), m.getLearningDashboardAccessToken(), m.getCreateTime(), formatPrettyDate(m.getCreateTime()), m.isBreakout(), m.getSequence(), m.isFreeJoin(), m.getMetadata(), @@ -1167,7 +1168,7 @@ public class MeetingService implements MessageListener { } else if (message instanceof GuestLobbyMessageChanged) { processGuestLobbyMessageChanged((GuestLobbyMessageChanged) message); } else if (message instanceof PrivateGuestLobbyMessageChanged) { - processPrivateGuestLobbyMessageChanged((PrivateGuestLobbyMessageChanged) message); + processPrivateGuestLobbyMessageChanged((PrivateGuestLobbyMessageChanged) message); } else if (message instanceof RecordChapterBreak) { processRecordingChapterBreak((RecordChapterBreak) message); } else if (message instanceof MakePresentationDownloadableMsg) { diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java index dc69bbb764..b768e4a1f4 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/ParamsProcessorUtil.java @@ -91,6 +91,7 @@ public class ParamsProcessorUtil { private boolean disableRecordingDefault; private boolean autoStartRecording; private boolean allowStartStopRecording; + private boolean recordFullDurationMedia; private boolean learningDashboardEnabled = true; private int learningDashboardCleanupDelayInMinutes; private boolean webcamsOnlyForModerator; @@ -503,6 +504,18 @@ public class ParamsProcessorUtil { } } + boolean _recordFullDurationMedia = recordFullDurationMedia; + if (!StringUtils.isEmpty(params.get(ApiParams.ALLOW_START_STOP_RECORDING))) { + try { + _recordFullDurationMedia = Boolean.parseBoolean(params + .get(ApiParams.RECORD_FULL_DURATION_MEDIA)); + } catch (Exception ex) { + log.warn( + "Invalid param [recordFullDurationMedia] for meeting=[{}]", + internalMeetingId); + } + } + // Check Disabled Features ArrayList listOfDisabledFeatures=new ArrayList(Arrays.asList(defaultDisabledFeatures.split(","))); if (!StringUtils.isEmpty(params.get(ApiParams.DISABLED_FEATURES))) { @@ -555,7 +568,7 @@ public class ParamsProcessorUtil { int learningDashboardCleanupMins = 0; if(listOfDisabledFeatures.contains("learningDashboard") == false) { learningDashboardAccessToken = RandomStringUtils.randomAlphanumeric(12).toLowerCase(); - + learningDashboardCleanupMins = learningDashboardCleanupDelayInMinutes; if (!StringUtils.isEmpty(params.get(ApiParams.LEARNING_DASHBOARD_CLEANUP_DELAY_IN_MINUTES))) { try { @@ -735,6 +748,7 @@ public class ParamsProcessorUtil { .withDefaultAvatarURL(avatarURL) .withAutoStartRecording(autoStartRec) .withAllowStartStopRecording(allowStartStoptRec) + .withRecordFullDurationMedia(_recordFullDurationMedia) .withWebcamsOnlyForModerator(webcamsOnlyForMod) .withMeetingCameraCap(meetingCameraCap) .withUserCameraCap(userCameraCap) @@ -1226,6 +1240,10 @@ public class ParamsProcessorUtil { this.allowStartStopRecording = allowStartStopRecording; } + public void setRecordFullDurationMedia(boolean recordFullDurationMedia) { + this.recordFullDurationMedia = recordFullDurationMedia; + } + public void setLearningDashboardEnabled(boolean learningDashboardEnabled) { this.learningDashboardEnabled = learningDashboardEnabled; } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java index 2401faeceb..033841f0db 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java @@ -70,6 +70,7 @@ public class Meeting { private boolean record; private boolean autoStartRecording = false; private boolean allowStartStopRecording = false; + private boolean recordFullDurationMedia = false; private boolean haveRecordingMarks = false; private boolean webcamsOnlyForModerator = false; private Integer meetingCameraCap = 0; @@ -148,6 +149,7 @@ public class Meeting { record = builder.record; autoStartRecording = builder.autoStartRecording; allowStartStopRecording = builder.allowStartStopRecording; + recordFullDurationMedia = builder.recordFullDurationMedia; webcamsOnlyForModerator = builder.webcamsOnlyForModerator; meetingCameraCap = builder.meetingCameraCap; userCameraCap = builder.userCameraCap; @@ -321,7 +323,7 @@ public class Meeting { public void setCaptureSlides(Boolean capture) { this.captureSlides = captureSlides; } - + public Boolean isCaptureNotes() { return captureNotes; } @@ -582,6 +584,10 @@ public class Meeting { return allowStartStopRecording; } + public boolean getRecordFullDurationMedia() { + return recordFullDurationMedia; + } + public boolean getWebcamsOnlyForModerator() { return webcamsOnlyForModerator; } @@ -862,6 +868,7 @@ public class Meeting { private int maxUsers; private boolean record; private boolean autoStartRecording; + private boolean recordFullDurationMedia; private boolean allowStartStopRecording; private boolean webcamsOnlyForModerator; private Integer meetingCameraCap; @@ -938,6 +945,11 @@ public class Meeting { return this; } + public Builder withRecordFullDurationMedia(boolean recordFullDurationMedia) { + this.recordFullDurationMedia = recordFullDurationMedia; + return this; + } + public Builder withWebcamsOnlyForModerator(boolean only) { this.webcamsOnlyForModerator = only; return this; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/converters/messages/CreateMeetingMessage.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/converters/messages/CreateMeetingMessage.java index 6a21fcf8f9..96a155687a 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/converters/messages/CreateMeetingMessage.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/converters/messages/CreateMeetingMessage.java @@ -7,7 +7,7 @@ import java.util.Map; public class CreateMeetingMessage { public static final String CREATE_MEETING_REQUEST_EVENT = "create_meeting_request"; public static final String VERSION = "0.0.1"; - + public final String id; public final String externalId; public final String name; @@ -16,6 +16,7 @@ public class CreateMeetingMessage { public final Long duration; public boolean autoStartRecording; public boolean allowStartStopRecording; + public boolean recordFullDurationMedia; public boolean webcamsOnlyForModerator; public final Integer meetingCameraCap; public final Integer userCameraCap; @@ -30,10 +31,11 @@ public class CreateMeetingMessage { public final Long createTime; public final String createDate; public final Map metadata; - - public CreateMeetingMessage(String id, String externalId, String name, Boolean record, - String voiceBridge, Long duration, + + public CreateMeetingMessage(String id, String externalId, String name, Boolean record, + String voiceBridge, Long duration, Boolean autoStartRecording, Boolean allowStartStopRecording, + Boolean recordFullDurationMedia, Boolean webcamsOnlyForModerator, Integer meetingCameraCap, Integer userCameraCap, Integer maxPinnedCameras, String moderatorPass, String viewerPass, String learningDashboardAccessToken, ArrayList disabledFeatures, @@ -49,6 +51,7 @@ public class CreateMeetingMessage { this.duration = duration; this.autoStartRecording = autoStartRecording; this.allowStartStopRecording = allowStartStopRecording; + this.recordFullDurationMedia = recordFullDurationMedia; this.webcamsOnlyForModerator = webcamsOnlyForModerator; this.meetingCameraCap = meetingCameraCap; this.userCameraCap = userCameraCap; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/pub/IPublisherService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/pub/IPublisherService.java index f9c350181a..8792829324 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/pub/IPublisherService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/pub/IPublisherService.java @@ -11,11 +11,14 @@ public interface IPublisherService { void createMeeting(String meetingID, String externalMeetingID, String parentMeetingID, String meetingName, Boolean recorded, String voiceBridge, Integer duration, Boolean autoStartRecording, - Boolean allowStartStopRecording, Boolean webcamsOnlyForModerator, + Boolean allowStartStopRecording, + Boolean recordFullDurationMedia, + Boolean webcamsOnlyForModerator, Integer userCameraCap, String moderatorPass, String viewerPass, Long createTime, String createDate, Boolean isBreakout, Integer sequence, - Boolean freeJoin, Map metadata, String guestPolicy); + Boolean freeJoin, Map metadata, String guestPolicy + ); void endMeeting(String meetingId); void send(String channel, String message); void registerUser(String meetingID, String internalUserId, String fullname, String role, String externUserID, diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java b/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java index d3bf85e813..cfb28e33d8 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java @@ -18,7 +18,9 @@ public interface IBbbWebApiGWApp { void createMeeting(String meetingID, String externalMeetingID, String parentMeetingID, String meetingName, Boolean recorded, String voiceBridge, Integer duration, Boolean autoStartRecording, - Boolean allowStartStopRecording, Boolean webcamsOnlyForModerator, + Boolean allowStartStopRecording, + Boolean recordFullDurationMedia, + Boolean webcamsOnlyForModerator, Integer meetingCameraCap, Integer userCameraCap, Integer maxPinnedCameras, diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api2/domain/RecordProp2.java b/bbb-common-web/src/main/java/org/bigbluebutton/api2/domain/RecordProp2.java index b945b99110..0f4de0cc41 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api2/domain/RecordProp2.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api2/domain/RecordProp2.java @@ -5,12 +5,16 @@ public class RecordProp2 { public final boolean record; public final boolean autoStartRecording; public final boolean allowStartStopRecording; + public final boolean recordFullDurationMedia; public RecordProp2(boolean record, boolean autoStartRecording, - boolean allowStartStopRecording) { + boolean allowStartStopRecording, + boolean recordFullDurationMedia + ) { this.record = record; this.autoStartRecording = autoStartRecording; this.allowStartStopRecording = allowStartStopRecording; + this.recordFullDurationMedia = recordFullDurationMedia; } } diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala index 42860858bd..22ec7e5245 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala @@ -120,7 +120,9 @@ class BbbWebApiGWApp( def createMeeting(meetingId: String, extMeetingId: String, parentMeetingId: String, meetingName: String, recorded: java.lang.Boolean, voiceBridge: String, duration: java.lang.Integer, autoStartRecording: java.lang.Boolean, - allowStartStopRecording: java.lang.Boolean, webcamsOnlyForModerator: java.lang.Boolean, + allowStartStopRecording: java.lang.Boolean, + recordFullDurationMedia: java.lang.Boolean, + webcamsOnlyForModerator: java.lang.Boolean, meetingCameraCap: java.lang.Integer, userCameraCap: java.lang.Integer, maxPinnedCameras: java.lang.Integer, @@ -182,7 +184,9 @@ class BbbWebApiGWApp( val password = PasswordProp(moderatorPass = moderatorPass, viewerPass = viewerPass, learningDashboardAccessToken = learningDashboardAccessToken) val recordProp = RecordProp(record = recorded.booleanValue(), autoStartRecording = autoStartRecording.booleanValue(), - allowStartStopRecording = allowStartStopRecording.booleanValue(), keepEvents = keepEvents.booleanValue()) + allowStartStopRecording = allowStartStopRecording.booleanValue(), + recordFullDurationMedia = recordFullDurationMedia.booleanValue(), + keepEvents = keepEvents.booleanValue()) val breakoutProps = BreakoutProps( parentId = parentMeetingId, diff --git a/bbb-webrtc-sfu.placeholder.sh b/bbb-webrtc-sfu.placeholder.sh index 0c753df801..bd08a3fb49 100755 --- a/bbb-webrtc-sfu.placeholder.sh +++ b/bbb-webrtc-sfu.placeholder.sh @@ -1 +1 @@ -git clone --branch v2.9.12 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu +git clone --branch v2.9.13 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties index 7e6aa2ffab..395b80b3f5 100644 --- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties +++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties @@ -231,6 +231,11 @@ autoStartRecording=false # Allow the user to start/stop recording. allowStartStopRecording=true +# Whether media (audio, cameras and screen sharing) should be captured on their +# full duration if the meeting is recorded (recorded=true). Effectively ignores +# the meeting's current recording state (paused/running). +recordFullDurationMedia=false + # Number of minutes that Learning Dashboard will be available after the end of the meeting # if 0, the Learning Dashboard will keep available permanently # this is the default value, can be customized using the create API diff --git a/bigbluebutton-web/grails-app/conf/spring/resources.xml b/bigbluebutton-web/grails-app/conf/spring/resources.xml index 924c57903d..e8a7adb584 100755 --- a/bigbluebutton-web/grails-app/conf/spring/resources.xml +++ b/bigbluebutton-web/grails-app/conf/spring/resources.xml @@ -158,6 +158,7 @@ with BigBlueButton; if not, see . + diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy index 61f0a7a3f3..54707d6fa7 100755 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy @@ -915,6 +915,7 @@ class ApiController { isBreakout meeting.isBreakout() logoutTimer meeting.getLogoutTimer() allowStartStopRecording meeting.getAllowStartStopRecording() + recordFullDurationMedia meeting.getRecordFullDurationMedia() welcome us.welcome if (!StringUtils.isEmpty(meeting.moderatorOnlyMessage) && us.role.equals(ROLE_MODERATOR)) { modOnlyMessage meeting.moderatorOnlyMessage diff --git a/docs/docs/administration/customize.md b/docs/docs/administration/customize.md index ae1cbeefa6..d406f1d179 100644 --- a/docs/docs/administration/customize.md +++ b/docs/docs/administration/customize.md @@ -1324,6 +1324,7 @@ These configs can be set in `/etc/bigbluebutton/bbb-web.properties` | `allowRequestsWithoutSession` | Allow requests without JSESSIONID to be handled | true/false | false | | `supportedChecksumAlgorithms` | List of supported hash algorithms for validating checksums | sha1, sha256, sha384, sha512 | sha1, sha256, sha384, sha512 | | `allowRevealOfBBBVersion` | Allow endpoint with current BigBlueButton version | true/false | false | +| `recordFullDurationMedia` | Controls whether media should be captured on their full duration if the meeting's recorded property is true | true/false | false | - _`overwritable`_: Config will be overwritten if the param is present in the API `/create` request diff --git a/docs/docs/data/create.tsx b/docs/docs/data/create.tsx index 92024fe5b6..6ce4c07ffe 100644 --- a/docs/docs/data/create.tsx +++ b/docs/docs/data/create.tsx @@ -377,6 +377,13 @@ const createEndpointTableData = [ "required": false, "type": "String", "description": (<>Message to be displayed in presentation uploader modal describing how to use an external application to upload presentation files. Only works if presentationUploadExternalUrl is also set. (added 2.6)) + }, + { + "name": "recordFullDurationMedia", + "required": false, + "type": "Boolean", + "default": "false", + "description": (<>Controls whether media (audio, cameras and screen sharing) should be captured on their full duration if the meeting's recorded property is true (recorded=true). Default is false: only captures media while recording is running in the meeting. (added 2.6.9)) } ] diff --git a/docs/docs/development/api.md b/docs/docs/development/api.md index 4e533178cf..bcc4712c71 100644 --- a/docs/docs/development/api.md +++ b/docs/docs/development/api.md @@ -96,7 +96,7 @@ Updated in 2.5: Updated in 2.6: -- **create** - **Added:** `notifyRecordingIsOn`, `presentationUploadExternalUrl`, `presentationUploadExternalDescription`; Added `liveTranscription` and `presentation` as options for `disabledFeatures=`. +- **create** - **Added:** `notifyRecordingIsOn`, `presentationUploadExternalUrl`, `presentationUploadExternalDescription`, `recordFullDurationMedia` (v2.6.9); Added `liveTranscription` and `presentation` as options for `disabledFeatures=`. - **getRecordings** - **Added:** Added support for pagination using `offset`, `limit`