Skip to content

Commit

Permalink
feat: only record media while meeting is being actively recorded
Browse files Browse the repository at this point in the history
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 is the
meeting's `recorded` prop is true.
  • Loading branch information
prlanzarin committed May 31, 2023
1 parent c6f40a8 commit 225075f
Show file tree
Hide file tree
Showing 21 changed files with 159 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

object VoiceApp extends SystemConfiguration {
def genRecordPath(
recordDir: String,
meetingId: String,
Expand All @@ -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 =>
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -779,22 +779,15 @@ class MeetingActor(
val elapsedInMin = TimeUtil.millisToMinutes(elapsedInMs)

if (props.recordProp.record &&
(MeetingStatus2x.isRecording(liveMeeting.status) || props.recordProp.recordFullDurationMedia) &&
recordingChapterBreakLengthInMinutes > 0 &&
elapsedInMin > recordingChapterBreakLengthInMinutes) {
lastRecBreakSentOn = now
val event = MsgBuilder.buildRecordingChapterBreakSysMsg(props.meetingProp.intId, TimeUtil.timeNowInMs())
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)
}
}

Expand Down Expand Up @@ -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)

VoiceApp.startRecordingVoiceConference(liveMeeting, outGW, recordFile)
// 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)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ trait TestFixtures {

val autoStartRecording = false
val allowStartStopRecording = false
val recordFullDurationMedia = false
val webcamsOnlyForModerator = false
val meetingCameraCap = 0
val userCameraCap = 0
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ private void handleCreateMeeting(Meeting m) {

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(),
Expand Down Expand Up @@ -1167,7 +1168,7 @@ public void run() {
} 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -503,6 +504,18 @@ boolean record = processRecordMeeting(params.get(ApiParams.RECORD));
}
}

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<String> listOfDisabledFeatures=new ArrayList(Arrays.asList(defaultDisabledFeatures.split(",")));
if (!StringUtils.isEmpty(params.get(ApiParams.DISABLED_FEATURES))) {
Expand Down Expand Up @@ -555,7 +568,7 @@ boolean record = processRecordMeeting(params.get(ApiParams.RECORD));
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 {
Expand Down Expand Up @@ -735,6 +748,7 @@ boolean record = processRecordMeeting(params.get(ApiParams.RECORD));
.withDefaultAvatarURL(avatarURL)
.withAutoStartRecording(autoStartRec)
.withAllowStartStopRecording(allowStartStoptRec)
.withRecordFullDurationMedia(_recordFullDurationMedia)
.withWebcamsOnlyForModerator(webcamsOnlyForMod)
.withMeetingCameraCap(meetingCameraCap)
.withUserCameraCap(userCameraCap)
Expand Down Expand Up @@ -1226,6 +1240,10 @@ public void setAllowStartStopRecording(boolean allowStartStopRecording) {
this.allowStartStopRecording = allowStartStopRecording;
}

public void setRecordFullDurationMedia(boolean recordFullDurationMedia) {
this.recordFullDurationMedia = recordFullDurationMedia;
}

public void setLearningDashboardEnabled(boolean learningDashboardEnabled) {
this.learningDashboardEnabled = learningDashboardEnabled;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -148,6 +149,7 @@ public Meeting(Meeting.Builder builder) {
record = builder.record;
autoStartRecording = builder.autoStartRecording;
allowStartStopRecording = builder.allowStartStopRecording;
recordFullDurationMedia = builder.recordFullDurationMedia;
webcamsOnlyForModerator = builder.webcamsOnlyForModerator;
meetingCameraCap = builder.meetingCameraCap;
userCameraCap = builder.userCameraCap;
Expand Down Expand Up @@ -321,7 +323,7 @@ public Boolean isCaptureSlides() {
public void setCaptureSlides(Boolean capture) {
this.captureSlides = captureSlides;
}

public Boolean isCaptureNotes() {
return captureNotes;
}
Expand Down Expand Up @@ -582,6 +584,10 @@ public boolean getAllowStartStopRecording() {
return allowStartStopRecording;
}

public boolean getRecordFullDurationMedia() {
return recordFullDurationMedia;
}

public boolean getWebcamsOnlyForModerator() {
return webcamsOnlyForModerator;
}
Expand Down Expand Up @@ -862,6 +868,7 @@ public static class Builder {
private int maxUsers;
private boolean record;
private boolean autoStartRecording;
private boolean recordFullDurationMedia;
private boolean allowStartStopRecording;
private boolean webcamsOnlyForModerator;
private Integer meetingCameraCap;
Expand Down Expand Up @@ -938,6 +945,11 @@ public Builder withAllowStartStopRecording(boolean allow) {
return this;
}

public Builder withRecordFullDurationMedia(boolean recordFullDurationMedia) {
this.recordFullDurationMedia = recordFullDurationMedia;
return this;
}

public Builder withWebcamsOnlyForModerator(boolean only) {
this.webcamsOnlyForModerator = only;
return this;
Expand Down

0 comments on commit 225075f

Please sign in to comment.