Merge pull request #3397 from riadvice/breakout-rooms-use-current-presentation

Breakout rooms use current presentation slide
This commit is contained in:
Fred Dixon 2016-09-30 17:16:33 -04:00 committed by GitHub
commit 3552fca5c5
20 changed files with 406 additions and 424 deletions

View File

@ -43,7 +43,6 @@ http {
services {
bbbWebAPI = "http://192.168.23.33/bigbluebutton/api"
sharedSecret = "changeme"
defaultPresentationURL = "http://localhost/default.pdf"
}
red5 {

View File

@ -18,7 +18,6 @@ trait SystemConfiguration {
lazy val bbbWebSharedSecret = Try(config.getString("services.sharedSecret")).getOrElse("changeme")
lazy val bbbWebModeratorPassword = Try(config.getString("services.moderatorPassword")).getOrElse("changeme")
lazy val bbbWebViewerPassword = Try(config.getString("services.viewerPassword")).getOrElse("changeme")
lazy val bbbWebDefaultPresentationURL = Try(config.getString("services.defaultPresentationURL")).getOrElse("changeme")
lazy val keysExpiresInSec = Try(config.getInt("redis.keyExpiry")).getOrElse(14 * 86400) // 14 days
lazy val red5DeskShareIP = Try(config.getString("red5.deskshareip")).getOrElse("127.0.0.1")
lazy val red5DeskShareApp = Try(config.getString("red5.deskshareapp")).getOrElse("")

View File

@ -94,7 +94,7 @@ class JsonMessageSenderActor(val service: MessageSender)
private def handleCreateBreakoutRoom(msg: CreateBreakoutRoom) {
val payload = new CreateBreakoutRoomRequestPayload(msg.room.breakoutId, msg.room.parentId, msg.room.name,
msg.room.voiceConfId, msg.room.viewerPassword, msg.room.moderatorPassword,
msg.room.durationInMinutes, msg.room.defaultPresentationURL, msg.room.record)
msg.room.durationInMinutes, msg.room.sourcePresentationId, msg.room.sourcePresentationSlide, msg.room.record)
val request = new CreateBreakoutRoomRequest(payload)
service.send(MessagingConstants.FROM_MEETING_CHANNEL, request.toJson())
}

View File

@ -117,7 +117,7 @@ class LiveMeeting(val mProps: MeetingProperties,
meetingModel.meetingHasEnded
/**
* See if this meeting has breakout rooms. If so, we also need to end them.
* Check if this meeting has breakout rooms. If so, we also need to end them.
*/
handleEndAllBreakoutRooms(new EndAllBreakoutRooms(msg.meetingId))

View File

@ -33,7 +33,7 @@ object UserMessagesProtocol extends DefaultJsonProtocol {
implicit val inMsgHeaderFormat = jsonFormat1(InMessageHeader)
implicit val outMsgHeaderFormat = jsonFormat1(OutMsgHeader)
implicit val outMsgEnvelopeHeaderFormat = jsonFormat2(OutMsgEnvelopeHeader)
implicit val createBreakoutRoomOutMsgPayloadFormat = jsonFormat8(CreateBreakoutRoomOutMsgPayload)
implicit val createBreakoutRoomOutMsgPayloadFormat = jsonFormat10(CreateBreakoutRoomOutMsgPayload)
implicit val createBreakoutRoomOutMsgEnvelopePayloadFormat = jsonFormat2(CreateBreakoutRoomOutMsgEnvelopePayload)
implicit val createBreakoutRoomOutMsgEnvelopeFormat = jsonFormat2(CreateBreakoutRoomOutMsgEnvelope)

View File

@ -5,12 +5,10 @@ case class OutMsgEnvelopeHeader(`type`: MessageType.MessageType, address: String
trait OutMessage
case class CreateBreakoutRoomOutMsgEnvelope(header: OutMsgEnvelopeHeader,
payload: CreateBreakoutRoomOutMsgEnvelopePayload)
case class CreateBreakoutRoomOutMsgEnvelopePayload(header: OutMsgHeader,
payload: CreateBreakoutRoomOutMsgPayload)
case class CreateBreakoutRoomOutMsgEnvelope(header: OutMsgEnvelopeHeader, payload: CreateBreakoutRoomOutMsgEnvelopePayload)
case class CreateBreakoutRoomOutMsgEnvelopePayload(header: OutMsgHeader, payload: CreateBreakoutRoomOutMsgPayload)
case class CreateBreakoutRoomOutMsgPayload(breakoutId: String, name: String, parentId: String,
voiceConfId: String, durationInMinutes: Int,
moderatorPassword: String, viewerPassword: String,
defaultPresentationUrl: String)
sourcePresentationId: String, sourcePresentationSlide: Int, record: Boolean)

View File

@ -33,7 +33,7 @@ case class CreateBreakoutRoom(meetingId: String, room: BreakoutRoomOutPayload) e
case class EndBreakoutRoom(breakoutId: String) extends IOutMessage
case class BreakoutRoomOutPayload(breakoutId: String, name: String, parentId: String,
voiceConfId: String, durationInMinutes: Int, moderatorPassword: String, viewerPassword: String,
defaultPresentationURL: String, record: Boolean)
sourcePresentationId: String, sourcePresentationSlide: Int, record: Boolean)
case class BreakoutRoomJoinURLOutMessage(meetingId: String, recorded: Boolean, breakoutId: String, userId: String, joinURL: String) extends IOutMessage
case class BreakoutRoomStartedOutMessage(meetingId: String, recorded: Boolean, breakout: BreakoutRoomBody) extends IOutMessage
case class BreakoutRoomBody(name: String, breakoutId: String)

View File

@ -20,15 +20,6 @@ trait BreakoutRoomApp extends SystemConfiguration {
val outGW: OutMessageGateway
val eventBus: IncomingEventBus
def getDefaultPresentationURL(): String = {
var presURL = bbbWebDefaultPresentationURL
val page = presModel.getCurrentPage()
page foreach { p =>
presURL = BreakoutRoomsUtil.fromSWFtoPDF(p.swfUri)
}
presURL
}
def handleBreakoutRoomsList(msg: BreakoutRoomsListMessage) {
val breakoutRooms = breakoutModel.getRooms().toVector map { r => new BreakoutRoomBody(r.name, r.id) }
outGW.send(new BreakoutRoomsListOutMessage(mProps.meetingID, breakoutRooms, breakoutModel.pendingRoomsNumber == 0 && breakoutRooms.length > 0));
@ -40,17 +31,20 @@ trait BreakoutRoomApp extends SystemConfiguration {
log.warning("CreateBreakoutRooms event received while {} are pending to be created for meeting {}", breakoutModel.pendingRoomsNumber, mProps.meetingID)
return
}
var i = 0
val sourcePresentationId = presModel.getCurrentPresentation().get.id
val sourcePresentationSlide = presModel.getCurrentPage().get.num
breakoutModel.pendingRoomsNumber = msg.rooms.length;
for (room <- msg.rooms) {
i += 1
val presURL = bbbWebDefaultPresentationURL
val breakoutMeetingId = BreakoutRoomsUtil.createMeetingId(mProps.meetingID, i)
val voiceConfId = BreakoutRoomsUtil.createVoiceConfId(mProps.voiceBridge, i)
val r = breakoutModel.createBreakoutRoom(breakoutMeetingId, room.name, voiceConfId, room.users, presURL)
val r = breakoutModel.createBreakoutRoom(breakoutMeetingId, room.name, voiceConfId, room.users)
val p = new BreakoutRoomOutPayload(r.id, r.name, mProps.meetingID,
r.voiceConfId, msg.durationInMinutes, mProps.moderatorPass, mProps.viewerPass,
r.defaultPresentationURL, msg.record)
sourcePresentationId, sourcePresentationSlide, msg.record)
outGW.send(new CreateBreakoutRoom(mProps.meetingID, p))
}
meetingModel.breakoutRoomsdurationInMinutes = msg.durationInMinutes;
@ -85,7 +79,7 @@ trait BreakoutRoomApp extends SystemConfiguration {
breakoutModel.getRooms().foreach { room =>
breakoutModel.getAssignedUsers(room.id) foreach { users =>
users.foreach { u =>
log.debug("## Sending Join URL for users: {}", u);
log.debug("Sending Join URL for users: {}", u);
sendJoinURL(u, room.id)
}
}
@ -181,7 +175,7 @@ object BreakoutRoomsUtil {
}
def joinParams(username: String, userId: String, isBreakout: Boolean, breakoutId: String,
password: String, redirect: Boolean): mutable.Map[String, String] = {
password: String, redirect: Boolean): mutable.Map[String, String] = {
val params = new collection.mutable.HashMap[String, String]
params += "fullName" -> urlEncode(username)
params += "userID" -> urlEncode(userId + "-" + breakoutId.substring(breakoutId.lastIndexOf("-") + 1));

View File

@ -5,7 +5,7 @@ import scala.collection.immutable.HashMap
case class BreakoutUser(id: String, name: String)
case class BreakoutRoom(id: String, name: String, voiceConfId: String,
assignedUsers: Vector[String], users: Vector[BreakoutUser], defaultPresentationURL: String)
assignedUsers: Vector[String], users: Vector[BreakoutUser])
class BreakoutRoomModel {
private var rooms = new collection.immutable.HashMap[String, BreakoutRoom]
@ -22,8 +22,8 @@ class BreakoutRoomModel {
}
def createBreakoutRoom(id: String, name: String, voiceConfId: String,
assignedUsers: Vector[String], defaultPresentationURL: String): BreakoutRoom = {
val room = new BreakoutRoom(id, name, voiceConfId, assignedUsers, Vector(), defaultPresentationURL)
assignedUsers: Vector[String]): BreakoutRoom = {
val room = new BreakoutRoom(id, name, voiceConfId, assignedUsers, Vector())
add(room)
}

View File

@ -94,16 +94,10 @@ trait PresentationApp {
}
def handleGotoSlide(msg: GotoSlide) {
// println("Received GotoSlide for meeting=[" + msg.meetingID + "] page=[" + msg.page + "]")
// println("*** Before change page ****")
// printPresentations
presModel.changePage(msg.page) foreach { page =>
// println("Switching page for meeting=[" + msg.meetingID + "] page=[" + page.id + "]")
log.debug("Switching page for meeting=[{}] page=[{}]", msg.meetingID, page.num);
outGW.send(new GotoSlideOutMsg(mProps.meetingID, mProps.recorded, page))
}
// println("*** After change page ****")
// printPresentations
usersModel.getCurrentPresenter() foreach { pres =>
handleStopPollRequest(StopPollRequest(mProps.meetingID, pres.userID))

View File

@ -1,27 +1,31 @@
package org.bigbluebutton.messages.payload;
public class CreateBreakoutRoomRequestPayload {
public final String breakoutId;
public final String parentId; // The main meeting internal id
public final String name; // The name of the breakout room
public final String voiceConfId; // The voice conference id
public final String viewerPassword;
public final String moderatorPassword;
public final Integer durationInMinutes; // The duration of the breakout room
public final String defaultPresentationURL;
public final Boolean record;
public CreateBreakoutRoomRequestPayload(String breakoutId, String parentId, String name,
String voiceConfId, String viewerPassword, String moderatorPassword,
Integer duration, String defaultPresentationURL, Boolean record) {
this.breakoutId = breakoutId;
this.parentId = parentId;
this.name = name;
this.voiceConfId = voiceConfId;
this.viewerPassword = viewerPassword;
this.moderatorPassword = moderatorPassword;
this.durationInMinutes = duration;
this.defaultPresentationURL = defaultPresentationURL;
this.record = record;
}
public final String breakoutId;
public final String parentId; // The main meeting internal id
public final String name; // The name of the breakout room
public final String voiceConfId; // The voice conference id
public final String viewerPassword;
public final String moderatorPassword;
public final Integer durationInMinutes; // The duration of the breakout room
public final String sourcePresentationId;
public final Integer sourcePresentationSlide;
public final Boolean record;
public CreateBreakoutRoomRequestPayload(String breakoutId, String parentId,
String name, String voiceConfId, String viewerPassword,
String moderatorPassword, Integer duration,
String sourcePresentationId, Integer sourcePresentationSlide,
Boolean record) {
this.breakoutId = breakoutId;
this.parentId = parentId;
this.name = name;
this.voiceConfId = voiceConfId;
this.viewerPassword = viewerPassword;
this.moderatorPassword = moderatorPassword;
this.durationInMinutes = duration;
this.sourcePresentationId = sourcePresentationId;
this.sourcePresentationSlide = sourcePresentationSlide;
this.record = record;
}
}

View File

@ -9,19 +9,20 @@ import com.google.gson.Gson;
public class CreateBreakoutRoomRequestTest {
@Test
public void testCreateBreakoutRoomRequest() {
String breakoutId = "abc123";
String breakoutId = "183f0bf3a0982a127bdb8161e0c44eb696b3e75c-1474984695664";
String parentId = "abc-123";
Integer durationInMinutes = 20;
String name = "Breakout room 1";
String voiceConfId = "851153";
String viewerPassword = "vp";
String moderatorPassword = "mp";
String defaultPresentationURL = "http://localhost/foo.pdf";
String sourcePresentationId = "d2d9a672040fbde2a47a10bf6c37b6a4b5ae187f-1474984695907";
Integer sourePresentationSlide = 5;
Boolean record = false;
CreateBreakoutRoomRequestPayload payload =
new CreateBreakoutRoomRequestPayload(breakoutId, parentId, name, voiceConfId,
viewerPassword, moderatorPassword, durationInMinutes, defaultPresentationURL, record);
viewerPassword, moderatorPassword, durationInMinutes, sourcePresentationId, sourePresentationSlide, record);
CreateBreakoutRoomRequest msg = new CreateBreakoutRoomRequest(payload);
Gson gson = new Gson();
String json = gson.toJson(msg);
@ -36,7 +37,8 @@ public class CreateBreakoutRoomRequestTest {
Assert.assertEquals(rxMsg.payload.viewerPassword, viewerPassword);
Assert.assertEquals(rxMsg.payload.moderatorPassword, moderatorPassword);
Assert.assertEquals(rxMsg.payload.durationInMinutes, durationInMinutes);
Assert.assertEquals(rxMsg.payload.defaultPresentationURL, defaultPresentationURL);
Assert.assertEquals(rxMsg.payload.sourcePresentationId, sourcePresentationId);
Assert.assertEquals(rxMsg.payload.sourcePresentationSlide, sourePresentationSlide);
Assert.assertEquals(rxMsg.payload.record, record);
}
}

View File

@ -21,7 +21,7 @@ dependencies {
compile 'commons-io:commons-io:2.4'
compile 'com.google.code.gson:gson:1.7.1'
compile 'commons-httpclient:commons-httpclient:3.1'
compile 'com.zaxxer:nuprocess:1.0.4'
compile 'com.zaxxer:nuprocess:1.1.0'
compile 'org.bigbluebutton:bbb-common-message:0.0.18-SNAPSHOT'

View File

@ -58,8 +58,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<bean id="presDownloadService" class="org.bigbluebutton.presentation.PresentationUrlDownloadService">
<property name="presentationDir" value="${presentationDir}"/>
<property name="presentationBaseURL" value="${presentationBaseURL}"/>
<property name="documentConversionService" ref="documentConversionService"/>
<property name="presentationBaseURL" value="${presentationBaseURL}"/>
<property name="pageExtractor" ref="pageExtractor"/>
<property name="documentConversionService" ref="documentConversionService"/>
</bean>
<bean id="recordingService" class="org.bigbluebutton.api.RecordingService" >

View File

@ -113,7 +113,8 @@ public class MeetingService implements MessageListener {
}
public void registerUser(String meetingID, String internalUserId,
String fullname, String role, String externUserID, String authToken, String avatarURL) {
String fullname, String role, String externUserID,
String authToken, String avatarURL) {
handle(new RegisterUser(meetingID, internalUserId, fullname, role,
externUserID, authToken, avatarURL));
}
@ -142,19 +143,22 @@ public class MeetingService implements MessageListener {
* Remove registered users who did not successfully joined the meeting.
*/
public void purgeRegisteredUsers() {
for (AbstractMap.Entry<String, Meeting> entry : this.meetings.entrySet()) {
for (AbstractMap.Entry<String, Meeting> entry : this.meetings
.entrySet()) {
Long now = System.nanoTime();
Meeting meeting = entry.getValue();
ConcurrentMap<String, User> users = meeting.getUsersMap();
for (AbstractMap.Entry<String, Long> registeredUser : meeting.getRegisteredUsers().entrySet()) {
for (AbstractMap.Entry<String, Long> registeredUser : meeting
.getRegisteredUsers().entrySet()) {
String registeredUserID = registeredUser.getKey();
Long registeredUserDate = registeredUser.getValue();
long registrationTime = registeredUserDate.longValue();
long elapsedTime = now - registrationTime;
if ( elapsedTime >= 60000 && !users.containsKey(registeredUserID)) {
if (elapsedTime >= 60000
&& !users.containsKey(registeredUserID)) {
meeting.userUnregistered(registeredUserID);
}
}
@ -319,7 +323,8 @@ public class MeetingService implements MessageListener {
m.getName(), m.isRecord(), m.getTelVoice(), m.getDuration(),
m.getAutoStartRecording(), m.getAllowStartStopRecording(),
m.getModeratorPassword(), m.getViewerPassword(),
m.getCreateTime(), formatPrettyDate(m.getCreateTime()), m.isBreakout());
m.getCreateTime(), formatPrettyDate(m.getCreateTime()),
m.isBreakout());
}
private String formatPrettyDate(Long timestamp) {
@ -357,8 +362,10 @@ public class MeetingService implements MessageListener {
int dashes = meetingId.split("-", -1).length - 1;
for (String key : meetings.keySet()) {
int keyDashes = key.split("-", -1).length - 1;
if (dashes == 2 && key.equals(meetingId)
|| (dashes < 2 && keyDashes < 2 && key.startsWith(meetingId))) {
if (dashes == 2
&& key.equals(meetingId)
|| (dashes < 2 && keyDashes < 2 && key
.startsWith(meetingId))) {
return (Meeting) meetings.get(key);
}
}
@ -470,21 +477,22 @@ public class MeetingService implements MessageListener {
public void setPublishRecording(List<String> idList, boolean publish) {
for (String id : idList) {
if (publish) {
recordingService.changeState(id, Recording.STATE_PUBLISHED);
} else {
recordingService.changeState(id, Recording.STATE_UNPUBLISHED);
}
if (publish) {
recordingService.changeState(id, Recording.STATE_PUBLISHED);
} else {
recordingService.changeState(id, Recording.STATE_UNPUBLISHED);
}
}
}
public void deleteRecordings(List<String> idList) {
for (String id : idList) {
recordingService.changeState(id, Recording.STATE_DELETED);
recordingService.changeState(id, Recording.STATE_DELETED);
}
}
public void updateRecordings(List<String> idList, Map<String, String> metaParams) {
public void updateRecordings(List<String> idList,
Map<String, String> metaParams) {
recordingService.updateMetaParams(idList, metaParams);
}
@ -527,12 +535,14 @@ public class MeetingService implements MessageListener {
params.put("voiceBridge", message.voiceConfId);
params.put("duration", message.durationInMinutes.toString());
params.put("record", message.record.toString());
params.put("welcome", getMeeting(message.parentId).getWelcomeMessageTemplate());
params.put("welcome", getMeeting(message.parentId)
.getWelcomeMessageTemplate());
Map<String, String> parentMeetingMetadata = parentMeeting.getMetadata();
Map<String, String> parentMeetingMetadata = parentMeeting
.getMetadata();
String metaPrefix = "meta_";
for (String key: parentMeetingMetadata.keySet()) {
for (String key : parentMeetingMetadata.keySet()) {
String metaName = metaPrefix + key;
// Inject metadata from parent meeting into the breakout room.
params.put(metaName, parentMeetingMetadata.get(key));
@ -542,10 +552,12 @@ public class MeetingService implements MessageListener {
handleCreateMeeting(breakout);
presDownloadService.downloadAndProcessDocument(
message.defaultPresentationURL, breakout.getInternalId());
presDownloadService.extractPage(message.parentId,
message.sourcePresentationId,
message.sourcePresentationSlide, breakout.getInternalId());
} else {
log.error("Failed to create breakout room " + message.breakoutId + ". Parent meeting not found.");
log.error("Failed to create breakout room " + message.breakoutId
+ ".Reason: Parent meeting not found.");
}
}
@ -595,6 +607,8 @@ public class MeetingService implements MessageListener {
Gson gson = new Gson();
String logStr = gson.toJson(logData);
log.info("Meeting restarted: data={}", logStr);
} else {
Map<String, Object> logData = new HashMap<String, Object>();
logData.put("meetingId", m.getInternalId());
@ -606,14 +620,14 @@ public class MeetingService implements MessageListener {
logData.put("event", "meeting_restarted");
logData.put("description", "Meeting has restarted.");
Gson gson = new Gson();
String logStr = gson.toJson(logData);
Gson gson = new Gson();
String logStr = gson.toJson(logData);
log.info("Meeting restarted: data={}", logStr);
}
return;
}
}
log.info("Meeting restarted: data={}", logStr);
}
return;
}
}
private void meetingDestroyed(MeetingDestroyed message) {
Meeting m = getMeeting(message.meetingId);
@ -655,7 +669,7 @@ public class MeetingService implements MessageListener {
logData.put("description", "Meeting has been destroyed.");
Gson gson = new Gson();
String logStr = gson.toJson(logData);
String logStr = gson.toJson(logData);
log.info("Meeting destroyed: data={}", logStr);
@ -688,7 +702,7 @@ public class MeetingService implements MessageListener {
logData.put("description", "User had joined the meeting.");
Gson gson = new Gson();
String logStr = gson.toJson(logData);
String logStr = gson.toJson(logData);
log.info("User joined meeting: data={}", logStr);
return;
@ -738,47 +752,49 @@ public class MeetingService implements MessageListener {
}
}
private void updatedStatus(UserStatusChanged message) {
Meeting m = getMeeting(message.meetingId);
if (m != null) {
User user = m.getUserById(message.userId);
if(user != null){
user.setStatus(message.status, message.value);
return;
}
return;
}
}
@Override
public void handle(IMessage message) {
receivedMessages.add(message);
}
public void setParamsProcessorUtil(ParamsProcessorUtil util) {
this.paramsProcessorUtil = util;
}
public void setPresDownloadService(PresentationUrlDownloadService presDownloadService) {
this.presDownloadService = presDownloadService;
}
private void processStunTurnInfoRequested (StunTurnInfoRequested message) {
Set<StunServer> stuns = stunTurnService.getStunServers();
log.info("\nhere are the stuns:");
for(StunServer s : stuns) {
log.info("a stun: " + s.url);
private void updatedStatus(UserStatusChanged message) {
Meeting m = getMeeting(message.meetingId);
if (m != null) {
User user = m.getUserById(message.userId);
if (user != null) {
user.setStatus(message.status, message.value);
return;
}
return;
}
Set<TurnEntry> turns = stunTurnService.getStunAndTurnServersFor(message.internalUserId);
log.info("\nhere are the (" + turns.size() +") turns for internalUserId:" + message.internalUserId);
for(TurnEntry t : turns) {
log.info("a turn: " + t.url + "username/pass=" + t.username + '/' + t.password);
}
messagingService.sendStunTurnInfo(message.meetingId, message.internalUserId, stuns, turns);
}
@Override
public void handle(IMessage message) {
receivedMessages.add(message);
}
public void setParamsProcessorUtil(ParamsProcessorUtil util) {
this.paramsProcessorUtil = util;
}
public void setPresDownloadService(
PresentationUrlDownloadService presDownloadService) {
this.presDownloadService = presDownloadService;
}
private void processStunTurnInfoRequested(StunTurnInfoRequested message) {
Set<StunServer> stuns = stunTurnService.getStunServers();
log.info("\nhere are the stuns:");
for (StunServer s : stuns) {
log.info("a stun: " + s.url);
}
Set<TurnEntry> turns = stunTurnService
.getStunAndTurnServersFor(message.internalUserId);
log.info("\nhere are the (" + turns.size()
+ ") turns for internalUserId:" + message.internalUserId);
for (TurnEntry t : turns) {
log.info("a turn: " + t.url + "username/pass=" + t.username + '/'
+ t.password);
}
messagingService.sendStunTurnInfo(message.meetingId,
message.internalUserId, stuns, turns);
}
public void userJoinedVoice(UserJoinedVoice message) {
Meeting m = getMeeting(message.meetingId);
@ -886,7 +902,6 @@ public class MeetingService implements MessageListener {
runExec.execute(task);
}
public void start() {
log.info("Starting Meeting Service.");
try {
@ -936,7 +951,8 @@ public class MeetingService implements MessageListener {
messagingService = mess;
}
public void setExpiredMeetingCleanupTimerTask(ExpiredMeetingCleanupTimerTask c) {
public void setExpiredMeetingCleanupTimerTask(
ExpiredMeetingCleanupTimerTask c) {
cleaner = c;
cleaner.setMeetingService(this);
cleaner.start();
@ -946,11 +962,14 @@ public class MeetingService implements MessageListener {
removeMeetingWhenEnded = s;
}
public void setRegisteredUserCleanupTimerTask(RegisteredUserCleanupTimerTask c) {
public void setRegisteredUserCleanupTimerTask(
RegisteredUserCleanupTimerTask c) {
registeredUserCleaner = c;
registeredUserCleaner.setMeetingService(this);
registeredUserCleaner.start();
}
public void setStunTurnService(StunTurnService s) { stunTurnService = s; }
public void setStunTurnService(StunTurnService s) {
stunTurnService = s;
}
}

View File

@ -79,7 +79,8 @@ public class MeetingMessageHandler implements MessageHandler {
msg.payload.viewerPassword,
msg.payload.moderatorPassword,
msg.payload.durationInMinutes,
msg.payload.defaultPresentationURL,
msg.payload.sourcePresentationId,
msg.payload.sourcePresentationSlide,
msg.payload.record
)
);

View File

@ -9,13 +9,15 @@ public class CreateBreakoutRoom implements IMessage {
public final String viewerPassword;
public final String moderatorPassword;
public final Integer durationInMinutes; // The duration of the breakout room
public final String defaultPresentationURL;
public final String sourcePresentationId;
public final Integer sourcePresentationSlide;
public final Boolean record;
public CreateBreakoutRoom(String breakoutId, String parentId, String name,
String voiceConfId, String viewerPassword,
String moderatorPassword, Integer duration,
String defaultPresentationURL, Boolean record) {
String sourcePresentationId, Integer sourcePresentationSlide,
Boolean record) {
this.breakoutId = breakoutId;
this.parentId = parentId;
this.name = name;
@ -23,7 +25,8 @@ public class CreateBreakoutRoom implements IMessage {
this.viewerPassword = viewerPassword;
this.moderatorPassword = moderatorPassword;
this.durationInMinutes = duration;
this.defaultPresentationURL = defaultPresentationURL;
this.sourcePresentationId = sourcePresentationId;
this.sourcePresentationSlide = sourcePresentationSlide;
this.record = record;
}
}

View File

@ -1,169 +1,113 @@
package org.bigbluebutton.presentation;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Timer;
import java.io.FilenameFilter;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PresentationUrlDownloadService {
private static Logger log = LoggerFactory.getLogger(PresentationUrlDownloadService.class);
private final int maxRedirects = 5;
private DocumentConversionService documentConversionService;
private String presentationBaseURL;
private String presentationDir;
public void processUploadedPresentation(UploadedPresentation uploadedPres) {
documentConversionService.processDocument(uploadedPres);
}
public void processUploadedFile(String meetingId, String presId, String filename, File presFile) {
UploadedPresentation uploadedPres = new UploadedPresentation(meetingId, presId, filename, presentationBaseURL);
uploadedPres.setUploadedFile(presFile);
processUploadedPresentation(uploadedPres);
}
public void downloadAndProcessDocument(String address, String meetingId) {
log.debug("downloadAndProcessDocument [pres=" + address + ", meeting=" + meetingId + "]");
String presFilename = address.substring(address.lastIndexOf('/') + 1);
log.debug("downloadAndProcessDocument [filename=" + presFilename + "]");
String filenameExt = getFilenameExt(presFilename);
private static Logger log = LoggerFactory
.getLogger(PresentationUrlDownloadService.class);
String presId = generatePresentationId(presFilename);
File uploadDir = createPresentationDirectory(meetingId, presentationDir, presId);
if (uploadDir != null) {
String newFilename = createNewFilename(presId, filenameExt);
String newFilePath = uploadDir.getAbsolutePath() + File.separatorChar + newFilename;
if (savePresentation(meetingId, newFilePath, address)) {
File pres = new File(newFilePath);
processUploadedFile(meetingId, presId, presFilename, pres);
} else {
log.error("Failed to download presentation=[" + address + "], meeting=[" + meetingId + "]");
}
}
}
public String generatePresentationId(String name) {
long timestamp = System.currentTimeMillis();
return DigestUtils.shaHex(name) + "-" + timestamp;
}
public String getFilenameExt(String filename) {
return filename.substring(filename.lastIndexOf("."));
}
public String createNewFilename(String presId, String fileExt) {
return presId + fileExt;
}
public File createPresentationDirectory(String meetingId, String presentationDir, String presentationId) {
String meetingPath = presentationDir + File.separatorChar + meetingId + File.separatorChar + meetingId;
String presPath = meetingPath + File.separatorChar + presentationId;
File dir = new File(presPath);
log.debug("Creating dir [{}]", presPath);
if (dir.mkdirs()) {
return dir;
}
return null;
}
private String followRedirect(String meetingId, String redirectUrl,
int redirectCount, String origUrl) {
private PageExtractor pageExtractor;
private DocumentConversionService documentConversionService;
private String presentationBaseURL;
private String presentationDir;
if (redirectCount > maxRedirects) {
log.error("Max redirect reached for meeting=[{}] with url=[{}]", meetingId, origUrl);
return null;
}
URL presUrl;
try {
presUrl = new URL(redirectUrl);
} catch (MalformedURLException e) {
log.error("Malformed url=[{}] for meeting=[{}]", redirectUrl, meetingId);
return null;
}
HttpURLConnection conn;
try {
conn = (HttpURLConnection) presUrl.openConnection();
conn.setReadTimeout(5000);
conn.addRequestProperty("Accept-Language", "en-US,en;q=0.8");
conn.addRequestProperty("User-Agent", "Mozilla");
// normally, 3xx is redirect
int status = conn.getResponseCode();
if (status != HttpURLConnection.HTTP_OK) {
if (status == HttpURLConnection.HTTP_MOVED_TEMP
|| status == HttpURLConnection.HTTP_MOVED_PERM
|| status == HttpURLConnection.HTTP_SEE_OTHER) {
String newUrl = conn.getHeaderField("Location");
return followRedirect(meetingId, newUrl, redirectCount + 1, origUrl);
} else {
log.error("Invalid HTTP response=[{}] for url=[{}] with meeting[{}]", status, redirectUrl, meetingId);
return null;
}
} else {
return redirectUrl;
}
} catch (IOException e) {
log.error("IOException for url=[{}] with meeting[{}]", redirectUrl, meetingId);
return null;
public void processUploadedPresentation(UploadedPresentation uploadedPres) {
documentConversionService.processDocument(uploadedPres);
}
}
public boolean savePresentation(final String meetingId, final String filename,
final String urlString) {
String finalUrl = followRedirect(meetingId, urlString, 0, urlString);
if (finalUrl == null) return false;
boolean success = false;
GetMethod method = new GetMethod(finalUrl);
HttpClient client = new HttpClient();
try {
int statusCode = client.executeMethod(method);
if (statusCode == HttpStatus.SC_OK) {
FileUtils.copyInputStreamToFile(
method.getResponseBodyAsStream(), new File(filename));
log.info("Downloaded presentation at [{}]", finalUrl);
success = true;
}
} catch (HttpException e) {
log.error("HttpException while downloading presentation at [{}]", finalUrl);
} catch (IOException e) {
log.error("IOException while downloading presentation at [{}]", finalUrl);
} finally {
method.releaseConnection();
}
return success;
}
public void setPresentationDir(String presDir) {
presentationDir = presDir;
}
public void setPresentationBaseURL(String presentationBaseUrl) {
presentationBaseURL = presentationBaseUrl;
}
public void setDocumentConversionService(DocumentConversionService documentConversionService) {
this.documentConversionService = documentConversionService;
}
public void processUploadedFile(String meetingId, String presId,
String filename, File presFile) {
UploadedPresentation uploadedPres = new UploadedPresentation(meetingId,
presId, filename, presentationBaseURL);
uploadedPres.setUploadedFile(presFile);
processUploadedPresentation(uploadedPres);
}
public void extractPage(String sourceMeetingId, String presentationId,
Integer presentationSlide, String destinationMeetingId) {
// Construct the source meeting path
File sourceMeetingPath = new File(presentationDir + File.separator
+ sourceMeetingId + File.separator + sourceMeetingId
+ File.separator + presentationId);
final String presentationFilter = presentationId;
FilenameFilter filter = new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.startsWith(presentationFilter);
}
};
File[] children = sourceMeetingPath.listFiles(filter);
if (children.length != 1) {
log.error("Not matching file with prefix {} found at {}",
sourceMeetingId, sourceMeetingPath);
return;
} else {
File sourcePresentationFile = children[0];
String filenameExt = FilenameUtils
.getExtension(sourcePresentationFile.getName());
String presId = generatePresentationId(presentationId);
String newFilename = createNewFilename(presId, filenameExt);
File uploadDir = createPresentationDirectory(destinationMeetingId,
presentationDir, presId);
String newFilePath = uploadDir.getAbsolutePath()
+ File.separatorChar + newFilename;
pageExtractor.extractPage(sourcePresentationFile, new File(
newFilePath), presentationSlide);
File pres = new File(newFilePath);
processUploadedFile(destinationMeetingId, presId, "default-"
+ presentationSlide.toString() + "." + filenameExt, pres);
}
}
public String generatePresentationId(String name) {
long timestamp = System.currentTimeMillis();
return DigestUtils.shaHex(name) + "-" + timestamp;
}
public String createNewFilename(String presId, String fileExt) {
return presId + "." + fileExt;
}
public File createPresentationDirectory(String meetingId,
String presentationDir, String presentationId) {
String meetingPath = presentationDir + File.separatorChar + meetingId
+ File.separatorChar + meetingId;
String presPath = meetingPath + File.separatorChar + presentationId;
File dir = new File(presPath);
log.debug("Creating dir [{}]", presPath);
if (dir.mkdirs()) {
return dir;
}
return null;
}
public void setPageExtractor(PageExtractor extractor) {
this.pageExtractor = extractor;
}
public void setPresentationDir(String presDir) {
presentationDir = presDir;
}
public void setPresentationBaseURL(String presentationBaseUrl) {
presentationBaseURL = presentationBaseUrl;
}
public void setDocumentConversionService(
DocumentConversionService documentConversionService) {
this.documentConversionService = documentConversionService;
}
}

View File

@ -1,56 +1,60 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 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 <http://www.gnu.org/licenses/>.
*
*/
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 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 <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.presentation.imp;
import java.io.File;
import org.bigbluebutton.presentation.PageExtractor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class GhostscriptPageExtractor implements PageExtractor {
private static Logger log = LoggerFactory.getLogger(GhostscriptPageExtractor.class);
private String GHOSTSCRIPT_EXEC;
private String noPdfMarkWorkaround;
private String SPACE = " ";
public boolean extractPage(File presentationFile, File output, int page){
String OPTIONS = "-sDEVICE=pdfwrite -dNOPAUSE -dQUIET -dBATCH";
String FIRST_PAGE = "-dFirstPage=" + page;
String LAST_PAGE = "-dLastPage=" + page;
String DESTINATION = output.getAbsolutePath();
String OUTPUT_FILE = "-sOutputFile=" + DESTINATION;
//extract that specific page and create a temp-pdf(only one page) with GhostScript
String COMMAND = GHOSTSCRIPT_EXEC + SPACE + OPTIONS + SPACE + FIRST_PAGE + SPACE + LAST_PAGE + SPACE
+ OUTPUT_FILE + SPACE + noPdfMarkWorkaround + SPACE + presentationFile.getAbsolutePath();
log.debug(COMMAND);
private static Logger log = LoggerFactory
.getLogger(GhostscriptPageExtractor.class);
private String GHOSTSCRIPT_EXEC;
private String noPdfMarkWorkaround;
private String SPACE = " ";
public boolean extractPage(File presentationFile, File output, int page) {
String OPTIONS = "-sDEVICE=pdfwrite -dNOPAUSE -dQUIET -dBATCH";
String FIRST_PAGE = "-dFirstPage=" + page;
String LAST_PAGE = "-dLastPage=" + page;
String DESTINATION = output.getAbsolutePath();
String OUTPUT_FILE = "-sOutputFile=" + DESTINATION;
// extract that specific page and create a temp-pdf(only one page) with
// GhostScript
String COMMAND = GHOSTSCRIPT_EXEC + SPACE + OPTIONS + SPACE
+ FIRST_PAGE + SPACE + LAST_PAGE + SPACE + OUTPUT_FILE + SPACE
+ noPdfMarkWorkaround + SPACE
+ presentationFile.getAbsolutePath();
return new ExternalProcessExecutor().exec(COMMAND, 60000);
}
public void setGhostscriptExec(String exec) {
GHOSTSCRIPT_EXEC = exec;
}
public void setNoPdfMarkWorkaround(String noPdfMarkWorkaround) {
this.noPdfMarkWorkaround = noPdfMarkWorkaround;
}
}
public void setGhostscriptExec(String exec) {
GHOSTSCRIPT_EXEC = exec;
}
public void setNoPdfMarkWorkaround(String noPdfMarkWorkaround) {
this.noPdfMarkWorkaround = noPdfMarkWorkaround;
}
}

View File

@ -13,93 +13,113 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SvgImageCreatorImp implements SvgImageCreator {
private static Logger log = LoggerFactory.getLogger(SvgImageCreatorImp.class);
private String IMAGEMAGICK_DIR;
@Override
public boolean createSvgImages(UploadedPresentation pres) {
boolean success = false;
File imagePresentationDir = determineSvgImagesDirectory(pres.getUploadedFile());
if (! imagePresentationDir.exists())
imagePresentationDir.mkdir();
cleanDirectory(imagePresentationDir);
try {
extractPdfPages(pres);
success = generateSvgImages(imagePresentationDir,pres);
} catch (InterruptedException e) {
log.warn("Interrupted Exception while generating images.");
success = false;
}
return success;
}
private void extractPdfPages(UploadedPresentation pres){
File tmpDir = new File(pres.getUploadedFile().getParent() + File.separatorChar + "svgs" + File.separatorChar + "tmp");
if (! tmpDir.exists())
tmpDir.mkdir();
if(SupportedFileTypes.isPdfFile(pres.getFileType())){
for(int i=1; i<=pres.getNumberOfPages(); i++){
File tmp = new File(tmpDir.getAbsolutePath() + File.separatorChar + "slide" + i + ".pdf");
String COMMAND = IMAGEMAGICK_DIR + "/gs -sDEVICE=pdfwrite -dNOPAUSE -dQUIET -dBATCH -dFirstPage=" + i + " -dLastPage=" + i + " -sOutputFile=" + tmp.getAbsolutePath() + " /etc/bigbluebutton/nopdfmark.ps " + pres.getUploadedFile().getAbsolutePath();
new ExternalProcessExecutor().exec(COMMAND, 60000);
}
}
}
private static Logger log = LoggerFactory
.getLogger(SvgImageCreatorImp.class);
private boolean generateSvgImages(File imagePresentationDir, UploadedPresentation pres) throws InterruptedException {
String source = pres.getUploadedFile().getAbsolutePath();
String dest;
String COMMAND = "";
boolean done = true;
if(SupportedFileTypes.isImageFile(pres.getFileType())){
dest = imagePresentationDir.getAbsolutePath() + File.separator + "slide1.pdf";
COMMAND = IMAGEMAGICK_DIR + "/convert " + source + " " + dest;
done = new ExternalProcessExecutor().exec(COMMAND, 60000);
private String IMAGEMAGICK_DIR;
source = imagePresentationDir.getAbsolutePath() + File.separator + "slide1.pdf";
dest = imagePresentationDir.getAbsolutePath() + File.separator + "slide1.svg";
COMMAND = "pdftocairo -rx 300 -ry 300 -svg -q -f 1 -l 1 " + source + " " + dest;
done = new ExternalProcessExecutor().exec(COMMAND, 60000);
}else{
for(int i=1; i<=pres.getNumberOfPages(); i++){
File tmp = new File(imagePresentationDir.getAbsolutePath() + File.separatorChar + "tmp" + File.separatorChar + "slide" + i + ".pdf");
File destsvg = new File(imagePresentationDir.getAbsolutePath() + File.separatorChar + "slide" + i + ".svg");
COMMAND = "pdftocairo -rx 300 -ry 300 -svg -q -f 1 -l 1 " + File.separatorChar + tmp.getAbsolutePath() + " " + destsvg.getAbsolutePath();
@Override
public boolean createSvgImages(UploadedPresentation pres) {
boolean success = false;
File svgImagesPresentationDir = determineSvgImagesDirectory(pres
.getUploadedFile());
if (!svgImagesPresentationDir.exists())
svgImagesPresentationDir.mkdir();
done = new ExternalProcessExecutor().exec(COMMAND, 60000);
if(!done){
break;
}
}
}
if (done) {
return true;
}
log.warn("Failed to create svg images: " + COMMAND);
return false;
}
private File determineSvgImagesDirectory(File presentationFile) {
return new File(presentationFile.getParent() + File.separatorChar + "svgs");
}
private void cleanDirectory(File directory) {
File[] files = directory.listFiles();
for (int i = 0; i < files.length; i++) {
files[i].delete();
}
}
cleanDirectory(svgImagesPresentationDir);
public void setImageMagickDir(String imageMagickDir) {
IMAGEMAGICK_DIR = imageMagickDir;
}
try {
extractPdfPages(pres);
success = generateSvgImages(svgImagesPresentationDir, pres);
} catch (InterruptedException e) {
log.warn("Interrupted Exception while generating images.");
success = false;
}
return success;
}
private void extractPdfPages(UploadedPresentation pres) {
File pdfDir = new File(pres.getUploadedFile().getParent()
+ File.separatorChar + "pdfs");
if (!pdfDir.exists())
pdfDir.mkdir();
if (SupportedFileTypes.isPdfFile(pres.getFileType())) {
for (int i = 1; i <= pres.getNumberOfPages(); i++) {
File pdfFile = new File(pdfDir.getAbsolutePath()
+ File.separatorChar + "slide" + i + ".pdf");
String COMMAND = IMAGEMAGICK_DIR
+ "/gs -sDEVICE=pdfwrite -dNOPAUSE -dQUIET -dBATCH -dFirstPage="
+ i + " -dLastPage=" + i + " -sOutputFile="
+ pdfFile.getAbsolutePath()
+ " /etc/bigbluebutton/nopdfmark.ps "
+ pres.getUploadedFile().getAbsolutePath();
new ExternalProcessExecutor().exec(COMMAND, 60000);
}
}
}
private boolean generateSvgImages(File imagePresentationDir,
UploadedPresentation pres) throws InterruptedException {
String source = pres.getUploadedFile().getAbsolutePath();
String dest;
String COMMAND = "";
boolean done = true;
if (SupportedFileTypes.isImageFile(pres.getFileType())) {
dest = imagePresentationDir.getAbsolutePath() + File.separator
+ "slide1.pdf";
COMMAND = IMAGEMAGICK_DIR + "/convert " + source + " " + dest;
done = new ExternalProcessExecutor().exec(COMMAND, 60000);
source = imagePresentationDir.getAbsolutePath() + File.separator
+ "slide1.pdf";
dest = imagePresentationDir.getAbsolutePath() + File.separator
+ "slide1.svg";
COMMAND = "pdftocairo -rx 300 -ry 300 -svg -q -f 1 -l 1 " + source
+ " " + dest;
done = new ExternalProcessExecutor().exec(COMMAND, 60000);
} else {
for (int i = 1; i <= pres.getNumberOfPages(); i++) {
File pdfFile = new File(imagePresentationDir.getParent()
+ File.separatorChar + "pdfs" + File.separatorChar
+ "slide" + i + ".pdf");
File destsvg = new File(imagePresentationDir.getAbsolutePath()
+ File.separatorChar + "slide" + i + ".svg");
COMMAND = "pdftocairo -rx 300 -ry 300 -svg -q -f 1 -l 1 "
+ File.separatorChar + pdfFile.getAbsolutePath() + " "
+ destsvg.getAbsolutePath();
done = new ExternalProcessExecutor().exec(COMMAND, 60000);
if (!done) {
break;
}
}
}
if (done) {
return true;
}
log.warn("Failed to create svg images: " + COMMAND);
return false;
}
private File determineSvgImagesDirectory(File presentationFile) {
return new File(presentationFile.getParent() + File.separatorChar
+ "svgs");
}
private void cleanDirectory(File directory) {
File[] files = directory.listFiles();
for (int i = 0; i < files.length; i++) {
files[i].delete();
}
}
public void setImageMagickDir(String imageMagickDir) {
IMAGEMAGICK_DIR = imageMagickDir;
}
}