diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/MeetingConfigurationEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/MeetingConfigurationEvent.scala new file mode 100755 index 0000000000..917a32a632 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/MeetingConfigurationEvent.scala @@ -0,0 +1,34 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * + * Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below). + * + * This program is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation; either version 3.0 of the License, or (at your option) any later + * version. + * + * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with BigBlueButton; if not, see . + * + */ + +package org.bigbluebutton.core.record.events + +class MeetingConfigurationEvent extends StarterConfigurationEvent { + import MeetingConfigurationEvent._ + + setEvent("MeetingConfigurationEvent") + + def setWebcamsOnlyForModerator(webcamsOnlyForModerator: Boolean) { + eventMap.put(WEBCAMS_ONLY_FOR_MODERATOR, webcamsOnlyForModerator.toString) + } +} + +object MeetingConfigurationEvent { + val WEBCAMS_ONLY_FOR_MODERATOR = "webcamsOnlyForModerator" +} \ No newline at end of file diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StarterConfigurationEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StarterConfigurationEvent.scala new file mode 100755 index 0000000000..b452bb8163 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/StarterConfigurationEvent.scala @@ -0,0 +1,24 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * + * Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below). + * + * This program is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation; either version 3.0 of the License, or (at your option) any later + * version. + * + * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with BigBlueButton; if not, see . + * + */ + +package org.bigbluebutton.core.record.events + +trait StarterConfigurationEvent extends RecordEvent { + setModule("CONFIG") +} \ No newline at end of file diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala index a106261155..d0bbd0a395 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala @@ -116,6 +116,7 @@ class RedisRecorderActor( case m: RecordStatusResetSysMsg => handleRecordStatusResetSysMsg(m) case m: WebcamsOnlyForModeratorChangedEvtMsg => handleWebcamsOnlyForModeratorChangedEvtMsg(m) case m: MeetingEndingEvtMsg => handleEndAndKickAllSysMsg(m) + case m: MeetingCreatedEvtMsg => handleStarterConfigurations(m) // Recording case m: RecordingChapterBreakSysMsg => handleRecordingChapterBreakSysMsg(m) @@ -622,4 +623,10 @@ class RedisRecorderActor( log.error("recording database is not available.") } + private def handleStarterConfigurations(msg: MeetingCreatedEvtMsg): Unit = { + val ev = new MeetingConfigurationEvent() + ev.setWebcamsOnlyForModerator(msg.body.props.usersProp.webcamsOnlyForModerator) + record(msg.body.props.meetingProp.intId, ev.toMap().asJava) + } + } 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 9ebf1cd9dc..8aa872c7f8 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 @@ -60,6 +60,7 @@ import org.bigbluebutton.api.messaging.converters.messages.DeletedRecordingMessa import org.bigbluebutton.api.messaging.messages.*; import org.bigbluebutton.api2.IBbbWebApiGWApp; import org.bigbluebutton.api2.domain.UploadedTrack; +import org.bigbluebutton.common2.msgs.MeetingCreatedEvtMsg; import org.bigbluebutton.common2.redis.RedisStorageService; import org.bigbluebutton.presentation.PresentationUrlDownloadService; import org.bigbluebutton.presentation.imp.SwfSlidesGenerationProgressNotifier; diff --git a/record-and-playback/core/lib/recordandplayback/generators/events.rb b/record-and-playback/core/lib/recordandplayback/generators/events.rb index f25c190e25..97beb67cdf 100755 --- a/record-and-playback/core/lib/recordandplayback/generators/events.rb +++ b/record-and-playback/core/lib/recordandplayback/generators/events.rb @@ -112,8 +112,60 @@ module BigBlueButton start_events end + def self.to_boolean(obj) + return obj.to_s.downcase == "true" + end + + def self.is_user_moderator(user_id, list_user_info) + user_role = list_user_info[user_id] + if !user_role.nil? + if user_role == "MODERATOR" + return true + else + return false + end + end + return false + end + + def self.get_id_from_filename(filename) + return filename.split("/")[-1].split("-")[1] + end + + def self.extract_filename_from_userId(userId, filenames_list) + filename_return = "" + filenames_list.each do |filename| + if !filename.match(userId).nil? + filename_return = filename + end + end + return filename_return + end + + def self.process_webcamsOnlyForModerator(list_user_info, active_videos, inactive_videos, webcamsOnlyForModerator) + + if webcamsOnlyForModerator + list_user_info.each do |user_id, user_role| + # If the user is a viewer: + if !BigBlueButton::Events.is_user_moderator(user_id, list_user_info) + filename = BigBlueButton::Events.extract_filename_from_userId(user_id, active_videos) + if filename != "" + active_videos.delete(filename) + inactive_videos << filename + end + end + end + else + # If the WebcamsOnlyForModerator is false, all previously inactive videos will become active + inactive_videos.each do |filename| + active_videos << filename + end + inactive_videos.clear + end + end + # Build a webcam EDL - def self.create_webcam_edl(events, archive_dir) + def self.create_webcam_edl(events, archive_dir, show_moderator_viewpoint) recording = events.at_xpath('/recording') meeting_id = recording['meeting_id'] event = events.at_xpath('/recording/event[position()=1]') @@ -125,57 +177,203 @@ module BigBlueButton videos = {} active_videos = [] + inactive_videos = [] video_edl = [] video_edl << { :timestamp => 0, :areas => { :webcam => [] } } - - events.xpath('/recording/event[@module="WEBCAM" or (@module="bbb-webrtc-sfu" and (@eventname="StartWebRTCShareEvent" or @eventname="StopWebRTCShareEvent"))]').each do |event| - timestamp = event['timestamp'].to_i - initial_timestamp - # Determine the video filename - case event['eventname'] - when 'StartWebcamShareEvent', 'StopWebcamShareEvent' - stream = event.at_xpath('stream').text - filename = "#{video_dir}/#{stream}.flv" - when 'StartWebRTCShareEvent', 'StopWebRTCShareEvent' - uri = event.at_xpath('filename').text - filename = "#{video_dir}/#{File.basename(uri)}" + list_user_info = {} + webcamsOnlyForModerator = false + if show_moderator_viewpoint + events.xpath('/recording/event[@module="WEBCAM" or (@module="bbb-webrtc-sfu" and (@eventname="StartWebRTCShareEvent" or @eventname="StopWebRTCShareEvent"))]').each do |event| + timestamp = event['timestamp'].to_i - initial_timestamp + # Determine the video filename + case event['eventname'] + when 'StartWebcamShareEvent', 'StopWebcamShareEvent' + stream = event.at_xpath('stream').text + filename = "#{video_dir}/#{stream}.flv" + when 'StartWebRTCShareEvent', 'StopWebRTCShareEvent' + uri = event.at_xpath('filename').text + filename = "#{video_dir}/#{File.basename(uri)}" + end + raise "Couldn't determine webcam filename" if filename.nil? + + # Add the video to the EDL + case event['eventname'] + when 'StartWebcamShareEvent', 'StartWebRTCShareEvent' + videos[filename] = { :timestamp => timestamp } + active_videos << filename + + edl_entry = { + :timestamp => timestamp, + :areas => { :webcam => [] } + } + active_videos.each do |filename| + edl_entry[:areas][:webcam] << { + :filename => filename, + :timestamp => timestamp - videos[filename][:timestamp] + } + end + video_edl << edl_entry + when 'StopWebcamShareEvent', 'StopWebRTCShareEvent' + active_videos.delete(filename) + + edl_entry = { + :timestamp => timestamp, + :areas => { :webcam => [] } + } + active_videos.each do |filename| + edl_entry[:areas][:webcam] << { + :filename => filename, + :timestamp => timestamp - videos[filename][:timestamp] + } + end + video_edl << edl_entry + end end - raise "Couldn't determine webcam filename" if filename.nil? + else + events.xpath('/recording/event[@module="WEBCAM" or (@module="bbb-webrtc-sfu" and (@eventname="StartWebRTCShareEvent" or @eventname="StopWebRTCShareEvent")) or (@module="PARTICIPANT" and (@eventname="ParticipantStatusChangeEvent" or @eventname="ParticipantJoinEvent")) or @eventname="WebcamsOnlyForModeratorEvent" or @eventname="MeetingConfigurationEvent"]').each do |event| + timestamp = event['timestamp'].to_i - initial_timestamp - # Add the video to the EDL - case event['eventname'] - when 'StartWebcamShareEvent', 'StartWebRTCShareEvent' - videos[filename] = { :timestamp => timestamp } - active_videos << filename - - edl_entry = { - :timestamp => timestamp, - :areas => { :webcam => [] } - } - active_videos.each do |filename| - edl_entry[:areas][:webcam] << { - :filename => filename, - :timestamp => timestamp - videos[filename][:timestamp] - } + # Determine the video filename if event is as the following + case event['eventname'] + when 'StartWebcamShareEvent', 'StopWebcamShareEvent' + stream = event.at_xpath('stream').text + filename = "#{video_dir}/#{stream}.flv" + when 'StartWebRTCShareEvent', 'StopWebRTCShareEvent' + uri = event.at_xpath('filename').text + filename = "#{video_dir}/#{File.basename(uri)}" end - video_edl << edl_entry - when 'StopWebcamShareEvent', 'StopWebRTCShareEvent' - active_videos.delete(filename) - edl_entry = { - :timestamp => timestamp, - :areas => { :webcam => [] } - } - active_videos.each do |filename| - edl_entry[:areas][:webcam] << { - :filename => filename, - :timestamp => timestamp - videos[filename][:timestamp] + # Add the video to the EDL + case event['eventname'] + when 'StartWebcamShareEvent', 'StartWebRTCShareEvent' + userId = BigBlueButton::Events.get_id_from_filename(filename) + is_in_forbidden_period = webcamsOnlyForModerator + + if (!is_in_forbidden_period) || (is_in_forbidden_period && BigBlueButton::Events.is_user_moderator(userId, list_user_info)) + + videos[filename] = { :timestamp => timestamp } + active_videos << filename + + + edl_entry = { + :timestamp => timestamp, + :areas => { :webcam => [] } + } + active_videos.each do |filename| + edl_entry[:areas][:webcam] << { + :filename => filename, + :timestamp => timestamp - videos[filename][:timestamp], + :user_id => BigBlueButton::Events.get_id_from_filename(filename) + } + end + video_edl << edl_entry + elsif is_in_forbidden_period && !BigBlueButton::Events.is_user_moderator(userId, list_user_info) + inactive_videos << filename + videos[filename] = { :timestamp => timestamp } + end + when 'StopWebcamShareEvent', 'StopWebRTCShareEvent' + userId = BigBlueButton::Events.get_id_from_filename(filename) + is_in_forbidden_period = webcamsOnlyForModerator + + if (!is_in_forbidden_period) || (is_in_forbidden_period && BigBlueButton::Events.is_user_moderator(userId, list_user_info)) + active_videos.delete(filename) + + edl_entry = { + :timestamp => timestamp, + :areas => { :webcam => [] } + } + active_videos.each do |filename| + edl_entry[:areas][:webcam] << { + :filename => filename, + :timestamp => timestamp - videos[filename][:timestamp], + :user_id => BigBlueButton::Events.get_id_from_filename(filename) + } + end + video_edl << edl_entry + elsif is_in_forbidden_period && !BigBlueButton::Events.is_user_moderator(userId, list_user_info) + inactive_videos.delete(filename) + end + when "ParticipantJoinEvent" + user_id = event.at_xpath('userId').text + list_user_info[user_id] = event.at_xpath('role').text + + when "ParticipantStatusChangeEvent" + is_in_forbidden_period = webcamsOnlyForModerator + userId = "" + filename_to_add = "" + + if event.at_xpath('status').text == "role" + userId = event.at_xpath('userId').text + + if is_in_forbidden_period && event.at_xpath('value').text == "MODERATOR" + filename_to_add = BigBlueButton::Events.extract_filename_from_userId(userId, inactive_videos) + if filename_to_add != "" + inactive_videos.delete(filename_to_add) + active_videos << filename_to_add + + edl_entry = { + :timestamp => timestamp, + :areas => { :webcam => [] } + } + active_videos.each do |filename| + edl_entry[:areas][:webcam] << { + :filename => filename, + :timestamp => timestamp - videos[filename][:timestamp], + :user_id => userId + } + end + video_edl << edl_entry + end + elsif is_in_forbidden_period && event.at_xpath('value').text == "VIEWER" + filename_to_add = BigBlueButton::Events.extract_filename_from_userId(userId, active_videos) + if filename != "" + active_videos.delete(filename_to_add) + inactive_videos << filename_to_add + + edl_entry = { + :timestamp => timestamp, + :areas => { :webcam => [] } + } + active_videos.each do |filename| + edl_entry[:areas][:webcam] << { + :filename => filename, + :timestamp => timestamp - videos[filename][:timestamp], + :user_id => userId + } + end + video_edl << edl_entry + end + end + user_id = event.at_xpath('userId').text + list_user_info[user_id] = event.at_xpath('value').text + end + + when "MeetingConfigurationEvent" + webcamsOnlyForModerator = BigBlueButton::Events.to_boolean(event.at_xpath('webcamsOnlyForModerator').text) + + when "WebcamsOnlyForModeratorEvent" + # Change active and inactive videos. + BigBlueButton::Events.process_webcamsOnlyForModerator(list_user_info, active_videos, inactive_videos, BigBlueButton::Events.to_boolean(event.at_xpath("webcamsOnlyForModerator").text)) + + edl_entry = { + :timestamp => timestamp, + :areas => { :webcam => [] } } + active_videos.each do |filename| + edl_entry[:areas][:webcam] << { + :filename => filename, + :timestamp => timestamp - videos[filename][:timestamp], + :user_id => userId + } + end + video_edl << edl_entry + + webcamsOnlyForModerator = BigBlueButton::Events.to_boolean(event.at_xpath('webcamsOnlyForModerator').text) end - video_edl << edl_entry end end diff --git a/record-and-playback/core/lib/recordandplayback/generators/video.rb b/record-and-playback/core/lib/recordandplayback/generators/video.rb index 76e8ecee2d..c7a0ac5a18 100755 --- a/record-and-playback/core/lib/recordandplayback/generators/video.rb +++ b/record-and-playback/core/lib/recordandplayback/generators/video.rb @@ -28,7 +28,7 @@ require File.expand_path('../../edl', __FILE__) module BigBlueButton - def BigBlueButton.process_webcam_videos(target_dir, raw_archive_dir, output_width, output_height, output_framerate, audio_offset, processed_audio_file, video_formats=['webm']) + def BigBlueButton.process_webcam_videos(target_dir, raw_archive_dir, output_width, output_height, output_framerate, audio_offset, processed_audio_file, video_formats=['webm'], show_moderator_viewpoint=false) BigBlueButton.logger.info("Processing webcam videos") # raw_archive_dir already contains meeting_id @@ -38,7 +38,7 @@ module BigBlueButton start_time = BigBlueButton::Events.first_event_timestamp(events) end_time = BigBlueButton::Events.last_event_timestamp(events) webcam_edl = BigBlueButton::Events.create_webcam_edl( - events, raw_archive_dir) + events, raw_archive_dir, show_moderator_viewpoint) user_video_edl = BigBlueButton::Events.edl_match_recording_marks_video( webcam_edl, events, start_time, end_time) BigBlueButton::EDL::Video.dump(user_video_edl) diff --git a/record-and-playback/core/scripts/bigbluebutton.yml b/record-and-playback/core/scripts/bigbluebutton.yml index 74a75217e6..3568e5eb67 100755 --- a/record-and-playback/core/scripts/bigbluebutton.yml +++ b/record-and-playback/core/scripts/bigbluebutton.yml @@ -40,6 +40,11 @@ anonymize_chat: false # meta param: meta_bbb-anonymize-chat-moderators (true/false) anonymize_chat_moderators: false +# By default, recordings assume the Viewer viewpoint (with all locks) +# so when webcamsOnlyForModerator=true, only moderators webcams are included in recordings. +# Use this option to ignore locks and assume Moderator viewpoint instead. +show_moderator_viewpoint: false + # Sequence of recording steps. Keys are the current step, values # are the next step(s). Examples: # current_step: next_step diff --git a/record-and-playback/presentation/scripts/process/presentation.rb b/record-and-playback/presentation/scripts/process/presentation.rb index 06878e5e26..aa64702af2 100755 --- a/record-and-playback/presentation/scripts/process/presentation.rb +++ b/record-and-playback/presentation/scripts/process/presentation.rb @@ -225,7 +225,7 @@ unless FileTest.directory?(target_dir) webcam_framerate = 15 if webcam_framerate.nil? processed_audio_file = BigBlueButton::AudioProcessor.get_processed_audio_file(raw_archive_dir, "#{target_dir}/audio") - BigBlueButton.process_webcam_videos(target_dir, raw_archive_dir, webcam_width, webcam_height, webcam_framerate, presentation_props['audio_offset'], processed_audio_file, presentation_props['video_formats']) + BigBlueButton.process_webcam_videos(target_dir, raw_archive_dir, webcam_width, webcam_height, webcam_framerate, presentation_props['audio_offset'], processed_audio_file, presentation_props['video_formats'], props['show_moderator_viewpoint']) end if !Dir["#{raw_archive_dir}/deskshare/*"].empty? && presentation_props['include_deskshare'] diff --git a/record-and-playback/screenshare/scripts/process/screenshare.rb b/record-and-playback/screenshare/scripts/process/screenshare.rb index 5716374dc8..a093b7765e 100644 --- a/record-and-playback/screenshare/scripts/process/screenshare.rb +++ b/record-and-playback/screenshare/scripts/process/screenshare.rb @@ -84,7 +84,7 @@ begin logger.info "Generating video events list" # Webcams - webcam_edl = BigBlueButton::Events.create_webcam_edl(events, raw_archive_dir) + webcam_edl = BigBlueButton::Events.create_webcam_edl(events, raw_archive_dir, props['show_moderator_viewpoint']) logger.debug "Webcam EDL:" BigBlueButton::EDL::Video.dump(webcam_edl)