diff --git a/akka-bbb-apps/project/Dependencies.scala b/akka-bbb-apps/project/Dependencies.scala index 9bd9dcf8214d3ba2062385868dc071dc7a0c1338..576e5fb2a527d65ae07ce923da4a2b0706eb6227 100644 --- a/akka-bbb-apps/project/Dependencies.scala +++ b/akka-bbb-apps/project/Dependencies.scala @@ -21,8 +21,8 @@ object Dependencies { val spray = "1.3.4" // Apache Commons - val lang = "3.8.1" - val codec = "1.11" + val lang = "3.9" + val codec = "1.14" // BigBlueButton val bbbCommons = "0.0.20-SNAPSHOT" diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/CreateBreakoutRoomsCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/CreateBreakoutRoomsCmdMsgHdlr.scala index bd87ec10a0fa079706bcdfc3628a229544c25605..950f77b0ba5a30060a8cb9fd4c54e3165e256c14 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/CreateBreakoutRoomsCmdMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/breakout/CreateBreakoutRoomsCmdMsgHdlr.scala @@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.breakout import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.core.apps.{ BreakoutModel, PermissionCheck, RightsManagementTrait } import org.bigbluebutton.core.domain.{ BreakoutRoom2x, MeetingState2x } +import org.bigbluebutton.core.models.PresentationInPod import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter } import org.bigbluebutton.core.running.MeetingActor @@ -103,7 +104,7 @@ trait CreateBreakoutRoomsCmdMsgHdlr extends RightsManagementTrait { for { defaultPod <- state.presentationPodManager.getDefaultPod() curPres <- defaultPod.getCurrentPresentation() - curPage <- curPres.getCurrentPage(curPres) + curPage <- PresentationInPod.getCurrentPage(curPres) } yield { currentSlide = curPage.num } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PdfConversionInvalidErrorSysPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PdfConversionInvalidErrorSysPubMsgHdlr.scala index c625dbde7747b49928fe689c0863177cdc315c06..2577dc3bd0f5821d510e00a87798d64f63d704de 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PdfConversionInvalidErrorSysPubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PdfConversionInvalidErrorSysPubMsgHdlr.scala @@ -6,7 +6,7 @@ import org.bigbluebutton.core.domain.MeetingState2x import org.bigbluebutton.core.running.LiveMeeting trait PdfConversionInvalidErrorSysPubMsgHdlr { - this: PresentationPodHdlrs => + this: PresentationPodHdlrs => def handle( msg: PdfConversionInvalidErrorSysPubMsg, state: MeetingState2x, diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionCompletedSysPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionCompletedSysPubMsgHdlr.scala index 2cdac5898bd254039ecdc95d17cc2dbb069c75d5..b39ba0d5a2d88bf94610cd8d9ab06819b5300342 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionCompletedSysPubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionCompletedSysPubMsgHdlr.scala @@ -4,8 +4,6 @@ import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.core.bus.MessageBus import org.bigbluebutton.core.domain.MeetingState2x import org.bigbluebutton.core.running.LiveMeeting -import org.bigbluebutton.common2.domain.{ PageVO } -import org.bigbluebutton.core.models.PresentationInPod trait PresentationConversionCompletedSysPubMsgHdlr { @@ -18,32 +16,33 @@ trait PresentationConversionCompletedSysPubMsgHdlr { val meetingId = liveMeeting.props.meetingProp.intId - val pages = new collection.mutable.HashMap[String, PageVO] - - msg.body.presentation.pages.foreach { p => - val page = PageVO(p.id, p.num, p.thumbUri, p.swfUri, p.txtUri, p.svgUri, p.current, p.xOffset, p.yOffset, - p.widthRatio, p.heightRatio) - pages += page.id -> page - } - - val downloadable = msg.body.presentation.downloadable - val presentationId = msg.body.presentation.id - val pres = new PresentationInPod(presentationId, msg.body.presentation.name, msg.body.presentation.current, - pages.toMap, downloadable) - val presVO = PresentationPodsApp.translatePresentationToPresentationVO(pres) - val podId = msg.body.podId - val newState = for { - pod <- PresentationPodsApp.getPresentationPod(state, podId) + pod <- PresentationPodsApp.getPresentationPod(state, msg.body.podId) + pres <- pod.getPresentation(msg.body.presentation.id) } yield { - PresentationSender.broadcastPresentationConversionCompletedEvtMsg(bus, meetingId, - pod.id, msg.header.userId, msg.body.messageKey, msg.body.code, presVO) - PresentationSender.broadcastSetPresentationDownloadableEvtMsg(bus, meetingId, pod.id, - msg.header.userId, presentationId, downloadable, pres.name) + val presVO = PresentationPodsApp.translatePresentationToPresentationVO(pres) + + PresentationSender.broadcastPresentationConversionCompletedEvtMsg( + bus, + meetingId, + pod.id, + msg.header.userId, + msg.body.messageKey, + msg.body.code, + presVO + ) + PresentationSender.broadcastSetPresentationDownloadableEvtMsg( + bus, + meetingId, + pod.id, + msg.header.userId, + pres.id, + pres.downloadable, + pres.name + ) var pods = state.presentationPodManager.addPod(pod) pods = pods.addPresentationToPod(pod.id, pres) - pods = pods.setCurrentPresentation(pod.id, pres.id) state.update(pods) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionEndedSysMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionEndedSysMsgHdlr.scala new file mode 100755 index 0000000000000000000000000000000000000000..ffedc00c38cfdd21474c45ca72367e77e0a4ccfb --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionEndedSysMsgHdlr.scala @@ -0,0 +1,40 @@ +package org.bigbluebutton.core.apps.presentationpod + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.domain.MeetingState2x +import org.bigbluebutton.core.running.LiveMeeting + +trait PresentationConversionEndedSysMsgHdlr { + this: PresentationPodHdlrs => + + def handle(msg: PresentationConversionEndedSysMsg, state: MeetingState2x, + liveMeeting: LiveMeeting, bus: MessageBus): MeetingState2x = { + + def broadcastEvent(msg: PresentationConversionEndedSysMsg): Unit = { + val routing = Routing.addMsgToClientRouting( + MessageTypes.BROADCAST_TO_MEETING, + liveMeeting.props.meetingProp.intId, msg.header.userId + ) + val envelope = BbbCoreEnvelope(PresentationConversionEndedEventMsg.NAME, routing) + val header = BbbClientMsgHeader( + PresentationConversionEndedEventMsg.NAME, + liveMeeting.props.meetingProp.intId, msg.header.userId + ) + + val body = PresentationConversionEndedEventMsgBody( + podId = msg.body.podId, + presentationId = msg.body.presentationId, + presName = msg.body.presName + ) + val event = PresentationConversionEndedEventMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + bus.outGW.send(msgEvent) + } + + broadcastEvent(msg) + + state + } + +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionUpdatePubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionUpdatePubMsgHdlr.scala index a50462b3263cbbd6efd217ac4e066149efc0f431..89bca3a68db1ffdb6bea01a38ec478249a29f581 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionUpdatePubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationConversionUpdatePubMsgHdlr.scala @@ -22,8 +22,13 @@ trait PresentationConversionUpdatePubMsgHdlr { liveMeeting.props.meetingProp.intId, msg.header.userId ) - val body = PresentationConversionUpdateEvtMsgBody(msg.body.podId, msg.body.messageKey, - msg.body.code, msg.body.presentationId, msg.body.presName) + val body = PresentationConversionUpdateEvtMsgBody( + msg.body.podId, + msg.body.messageKey, + msg.body.code, + msg.body.presentationId, + msg.body.presName + ) val event = PresentationConversionUpdateEvtMsg(header, body) val msgEvent = BbbCommonEnvCoreMsg(envelope, event) bus.outGW.send(msgEvent) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPageConversionStartedSysMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPageConversionStartedSysMsgHdlr.scala new file mode 100755 index 0000000000000000000000000000000000000000..1294a035111fdba81f1708e0603d35039d6925ec --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPageConversionStartedSysMsgHdlr.scala @@ -0,0 +1,66 @@ +package org.bigbluebutton.core.apps.presentationpod + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.domain.MeetingState2x +import org.bigbluebutton.core.models.PresentationInPod +import org.bigbluebutton.core.running.LiveMeeting + +trait PresentationPageConversionStartedSysMsgHdlr { + this: PresentationPodHdlrs => + + def handle(msg: PresentationPageConversionStartedSysMsg, state: MeetingState2x, + liveMeeting: LiveMeeting, bus: MessageBus): MeetingState2x = { + + def broadcastEvent(msg: PresentationPageConversionStartedSysMsg): Unit = { + val routing = Routing.addMsgToClientRouting( + MessageTypes.BROADCAST_TO_MEETING, + liveMeeting.props.meetingProp.intId, msg.header.userId + ) + val envelope = BbbCoreEnvelope(PresentationPageConversionStartedSysMsg.NAME, routing) + val header = BbbClientMsgHeader( + PresentationPageConversionStartedSysMsg.NAME, + liveMeeting.props.meetingProp.intId, msg.header.userId + ) + + val body = PresentationPageConversionStartedSysMsgBody( + podId = msg.body.podId, + presentationId = msg.body.presentationId, + current = msg.body.current, + presName = msg.body.presName, + downloadable = msg.body.downloadable, + authzToken = msg.body.authzToken, + numPages = msg.body.numPages + ) + val event = PresentationPageConversionStartedSysMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + bus.outGW.send(msgEvent) + } + + val downloadable = msg.body.downloadable + val presentationId = msg.body.presentationId + val podId = msg.body.podId + + val pres = new PresentationInPod(presentationId, msg.body.presName, msg.body.current, Map.empty, downloadable) + + val newState = for { + pod <- PresentationPodsApp.getPresentationPod(state, podId) + } yield { + var pods = state.presentationPodManager.addPod(pod) + pods = pods.addPresentationToPod(pod.id, pres) + if (msg.body.current) { + pods = pods.setCurrentPresentation(pod.id, pres.id) + } + + state.update(pods) + } + + broadcastEvent(msg) + + newState match { + case Some(ns) => ns + case None => state + } + + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPageConvertedSysMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPageConvertedSysMsgHdlr.scala new file mode 100755 index 0000000000000000000000000000000000000000..ccffb183ee4ae259a7caa9b8b3b41ef49069a4bf --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPageConvertedSysMsgHdlr.scala @@ -0,0 +1,78 @@ +package org.bigbluebutton.core.apps.presentationpod + +import org.bigbluebutton.common2.domain.PresentationPageVO +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.domain.MeetingState2x +import org.bigbluebutton.core.models.{ PresentationInPod, PresentationPage } +import org.bigbluebutton.core.running.LiveMeeting + +trait PresentationPageConvertedSysMsgHdlr { + this: PresentationPodHdlrs => + + def handle( + msg: PresentationPageConvertedSysMsg, + state: MeetingState2x, + liveMeeting: LiveMeeting, + bus: MessageBus + ): MeetingState2x = { + + def broadcastEvent(msg: PresentationPageConvertedSysMsg): Unit = { + val routing = Routing.addMsgToClientRouting( + MessageTypes.BROADCAST_TO_MEETING, + liveMeeting.props.meetingProp.intId, msg.header.userId + ) + val envelope = BbbCoreEnvelope(PresentationPageConvertedEventMsg.NAME, routing) + val header = BbbClientMsgHeader( + PresentationPageConvertedEventMsg.NAME, + liveMeeting.props.meetingProp.intId, msg.header.userId + ) + + val page = PresentationPageVO( + id = msg.body.page.id, + num = msg.body.page.num, + urls = msg.body.page.urls, + current = msg.body.page.current + ) + + val body = PresentationPageConvertedEventMsgBody( + msg.body.podId, + msg.body.messageKey, + msg.body.code, + msg.body.presentationId, + msg.body.numberOfPages, + msg.body.pagesCompleted, + msg.body.presName, + page + ) + val event = PresentationPageConvertedEventMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + bus.outGW.send(msgEvent) + } + + val page = PresentationPage( + msg.body.page.id, + msg.body.page.num, + msg.body.page.urls, + msg.body.page.current + ) + + val newState = for { + pod <- PresentationPodsApp.getPresentationPod(state, msg.body.podId) + pres <- pod.getPresentation(msg.body.presentationId) + } yield { + val newPres = PresentationInPod.addPage(pres, page) + var pods = state.presentationPodManager.addPod(pod) + pods = pods.addPresentationToPod(pod.id, newPres) + + state.update(pods) + } + + broadcastEvent(msg) + + newState match { + case Some(ns) => ns + case None => state + } + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala index bcb6bb2895c6d2acfc096398a3067f0e5d8885e2..965d43e9ea1a6398a57edc70820342d01b72acf0 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodHdlrs.scala @@ -20,7 +20,10 @@ class PresentationPodHdlrs(implicit val context: ActorContext) with PresentationUploadTokenReqMsgHdlr with ResizeAndMovePagePubMsgHdlr with SyncGetPresentationPodsMsgHdlr - with RemovePresentationPodPubMsgHdlr { + with RemovePresentationPodPubMsgHdlr + with PresentationPageConvertedSysMsgHdlr + with PresentationPageConversionStartedSysMsgHdlr + with PresentationConversionEndedSysMsgHdlr { val log = Logging(context.system, getClass) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodsApp.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodsApp.scala index 2440540dbf2f9dc9699e942d51e641083dc236ef..e7210ab82a4f4f7fab701c69a23033d99921a583 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodsApp.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/PresentationPodsApp.scala @@ -41,10 +41,28 @@ object PresentationPodsApp { def translatePresentationPodToVO(pod: PresentationPod): PresentationPodVO = { val presentationObjects = pod.presentations - val presentationVOs = presentationObjects.values.map(p => PresentationVO(p.id, p.name, p.current, - p.pages.values.toVector, p.downloadable)).toVector + val presentationVOs = presentationObjects.values.map { p => + val pages = p.pages.values.map { page => + PageVO( + id = page.id, + num = page.num, + thumbUri = page.urls.getOrElse("thumb", ""), + swfUri = page.urls.getOrElse("swf", ""), + txtUri = page.urls.getOrElse("text", ""), + svgUri = page.urls.getOrElse("svg", ""), + current = page.current, + xOffset = page.xOffset, + yOffset = page.yOffset, + widthRatio = page.widthRatio, + heightRatio = page.heightRatio + ) + } + + PresentationVO(p.id, p.name, p.current, + pages.toVector, p.downloadable) + } - PresentationPodVO(pod.id, pod.currentPresenter, presentationVOs) + PresentationPodVO(pod.id, pod.currentPresenter, presentationVOs.toVector) } def findPodsWhereUserIsPresenter(mgr: PresentationPodManager, userId: String): Vector[PresentationPod] = { @@ -57,7 +75,22 @@ object PresentationPodsApp { } def translatePresentationToPresentationVO(pres: PresentationInPod): PresentationVO = { - PresentationVO(pres.id, pres.name, pres.current, pres.pages.values.toVector, pres.downloadable) + val pages = pres.pages.values.map { page => + PageVO( + id = page.id, + num = page.num, + thumbUri = page.urls.getOrElse("thumb", ""), + swfUri = page.urls.getOrElse("swf", ""), + txtUri = page.urls.getOrElse("text", ""), + svgUri = page.urls.getOrElse("svg", ""), + current = page.current, + xOffset = page.xOffset, + yOffset = page.yOffset, + widthRatio = page.widthRatio, + heightRatio = page.heightRatio + ) + } + PresentationVO(pres.id, pres.name, pres.current, pages.toVector, pres.downloadable) } def setCurrentPresentationInPod(state: MeetingState2x, podId: String, nextCurrentPresId: String): Option[PresentationPod] = { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/ResizeAndMovePagePubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/ResizeAndMovePagePubMsgHdlr.scala index eb77c7a1b86bdc50296acdcb155c2224feabb6b8..138e513e003927691e244cecabb7289a600df841 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/ResizeAndMovePagePubMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/presentationpod/ResizeAndMovePagePubMsgHdlr.scala @@ -1,11 +1,11 @@ package org.bigbluebutton.core.apps.presentationpod -import org.bigbluebutton.common2.domain.PageVO import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.core.bus.MessageBus import org.bigbluebutton.core.domain.MeetingState2x import org.bigbluebutton.core.running.LiveMeeting import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait } +import org.bigbluebutton.core.models.PresentationPage trait ResizeAndMovePagePubMsgHdlr extends RightsManagementTrait { this: PresentationPodHdlrs => @@ -13,7 +13,7 @@ trait ResizeAndMovePagePubMsgHdlr extends RightsManagementTrait { def handle(msg: ResizeAndMovePagePubMsg, state: MeetingState2x, liveMeeting: LiveMeeting, bus: MessageBus): MeetingState2x = { - def broadcastEvent(msg: ResizeAndMovePagePubMsg, podId: String, page: PageVO): Unit = { + def broadcastEvent(msg: ResizeAndMovePagePubMsg, podId: String, page: PresentationPage): Unit = { val routing = Routing.addMsgToClientRouting( MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, msg.header.userId diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Polls.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Polls.scala index a231ab6c7f91ad1e04b86e3e8ff4748aa65c6227..4aab4ac88c3d591b9c074795250ca5491bbfc242 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Polls.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/Polls.scala @@ -25,7 +25,7 @@ object Polls { for { pod <- state.presentationPodManager.getDefaultPod() pres <- pod.getCurrentPresentation() - page <- pres.getCurrentPage(pres) + page <- PresentationInPod.getCurrentPage(pres) pageId: String = if (pollId.contains("deskshare")) "deskshare" else page.id stampedPollId: String = pageId + "/" + System.currentTimeMillis() numRespondents: Int = Users2x.numUsers(lm.users2x) - 1 // subtract the presenter @@ -42,7 +42,7 @@ object Polls { for { pod <- state.presentationPodManager.getDefaultPod() pres <- pod.getCurrentPresentation() - page <- pres.getCurrentPage(pres) + page <- PresentationInPod.getCurrentPage(pres) curPoll <- getRunningPollThatStartsWith(page.id, lm.polls) } yield { stopPoll(curPoll.id, lm.polls) @@ -73,7 +73,7 @@ object Polls { for { pod <- state.presentationPodManager.getDefaultPod() pres <- pod.getCurrentPresentation() - page <- pres.getCurrentPage(pres) + page <- PresentationInPod.getCurrentPage(pres) } yield { val pageId = if (poll.id.contains("deskshare")) "deskshare" else page.id val updatedShape = shape + ("whiteboardId" -> pageId) @@ -98,7 +98,7 @@ object Polls { val poll = for { pod <- state.presentationPodManager.getDefaultPod() pres <- pod.getCurrentPresentation() - page <- pres.getCurrentPage(pres) + page <- PresentationInPod.getCurrentPage(pres) curPoll <- getRunningPollThatStartsWith(page.id, lm.polls) } yield curPoll @@ -143,7 +143,7 @@ object Polls { for { pod <- state.presentationPodManager.getDefaultPod() pres <- pod.getCurrentPresentation() - page <- pres.getCurrentPage(pres) + page <- PresentationInPod.getCurrentPage(pres) pageId: String = if (pollId.contains("deskshare")) "deskshare" else page.id stampedPollId: String = pageId + "/" + System.currentTimeMillis() numRespondents: Int = Users2x.numUsers(lm.users2x) - 1 // subtract the presenter diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala index 40ba0b8e779b5ea026956777c79dea1cccfbdf5f..9cf85357998d5b97a56d0ea09a2eba75834fac07 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/PresentationPods.scala @@ -1,6 +1,7 @@ package org.bigbluebutton.core.models import org.bigbluebutton.common2.domain.PageVO +import org.bigbluebutton.core.models.PresentationInPod import org.bigbluebutton.core.util.RandomStringGenerator object PresentationPodFactory { @@ -18,8 +19,22 @@ object PresentationPodFactory { } } -case class PresentationInPod(id: String, name: String, current: Boolean = false, - pages: scala.collection.immutable.Map[String, PageVO], downloadable: Boolean) { +case class PresentationPage( + id: String, + num: Int, + urls: Map[String, String], + current: Boolean = false, + xOffset: Double = 0, + yOffset: Double = 0, + widthRatio: Double = 100D, + heightRatio: Double = 100D +) + +object PresentationInPod { + def addPage(pres: PresentationInPod, page: PresentationPage): PresentationInPod = { + val newPages = pres.pages + (page.id -> page) + pres.copy(pages = newPages) + } def makePageCurrent(pres: PresentationInPod, pageId: String): Option[PresentationInPod] = { pres.pages.get(pageId) match { @@ -33,12 +48,20 @@ case class PresentationInPod(id: String, name: String, current: Boolean = false, } } - def getCurrentPage(pres: PresentationInPod): Option[PageVO] = { + def getCurrentPage(pres: PresentationInPod): Option[PresentationPage] = { pres.pages.values find (p => p.current) } } +case class PresentationInPod( + id: String, + name: String, + current: Boolean = false, + pages: scala.collection.immutable.Map[String, PresentationPage], + downloadable: Boolean +) + object PresentationPod { val DEFAULT_PRESENTATION_POD = "DEFAULT_PRESENTATION_POD" } @@ -99,7 +122,7 @@ case class PresentationPod(id: String, currentPresenter: String, def setCurrentPage(presentationId: String, pageId: String): Option[PresentationPod] = { for { pres <- presentations.get(presentationId) - newPres <- pres.makePageCurrent(pres, pageId) + newPres <- PresentationInPod.makePageCurrent(pres, pageId) } yield { addPresentation(deactivateCurrentPage(newPres, pageId)) } @@ -129,7 +152,7 @@ case class PresentationPod(id: String, currentPresenter: String, def resizePage(presentationId: String, pageId: String, xOffset: Double, yOffset: Double, widthRatio: Double, - heightRatio: Double): Option[(PresentationPod, PageVO)] = { + heightRatio: Double): Option[(PresentationPod, PresentationPage)] = { // Force coordinate that are out-of-bounds inside valid values // 0.25D is 400% zoom // 100D-checkedWidth is the maximum the page can be moved over diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala index 756d5bd92cf1969da3f8e11167b16f799720ef3b..1842b9a24ed18138c39b045629f78ced7484775f 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala @@ -224,6 +224,14 @@ class ReceivedJsonMsgHandlerActor( routeGenericMsg[PresentationPageCountErrorSysPubMsg](envelope, jsonNode) case PresentationPageGeneratedSysPubMsg.NAME => routeGenericMsg[PresentationPageGeneratedSysPubMsg](envelope, jsonNode) + case PresentationPageConvertedSysMsg.NAME => + routeGenericMsg[PresentationPageConvertedSysMsg](envelope, jsonNode) + case PresentationPageConversionStartedSysMsg.NAME => + routeGenericMsg[PresentationPageConversionStartedSysMsg](envelope, jsonNode) + case PresentationConversionEndedSysMsg.NAME => + routeGenericMsg[PresentationConversionEndedSysMsg](envelope, jsonNode) + case PresentationConversionRequestReceivedSysMsg.NAME => + routeGenericMsg[PresentationConversionRequestReceivedSysMsg](envelope, jsonNode) case PresentationConversionCompletedSysPubMsg.NAME => routeGenericMsg[PresentationConversionCompletedSysPubMsg](envelope, jsonNode) case PdfConversionInvalidErrorSysPubMsg.NAME => 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 42ffe9c0e2ab8bc91186103b68201f00321ac8be..48ab65be1e3652b13846a9479e7a78f26d318afc 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 @@ -439,6 +439,9 @@ class MeetingActor( case m: PresentationPageCountErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) case m: PresentationUploadTokenReqMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) case m: ResizeAndMovePagePubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) + case m: PresentationPageConvertedSysMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) + case m: PresentationPageConversionStartedSysMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) + case m: PresentationConversionEndedSysMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) // Caption case m: EditCaptionHistoryPubMsg => captionApp2x.handle(m, liveMeeting, msgBus) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala index 019dec7d2f25acfdb8639931db8eb4369201409e..92aa396bd3cb9b5434fa8f64546595bbc8f21343 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala @@ -102,12 +102,26 @@ class AnalyticsActor extends Actor with ActorLogging { case m: BreakoutRoomEndedEvtMsg => logMessage(msg) // Presentation - case m: PresentationConversionCompletedSysPubMsg => logMessage(msg) + //case m: PresentationConversionCompletedSysPubMsg => logMessage(msg) case m: PdfConversionInvalidErrorSysPubMsg => logMessage(msg) case m: SetCurrentPresentationPubMsg => logMessage(msg) case m: SetCurrentPresentationEvtMsg => logMessage(msg) case m: SetPresentationDownloadablePubMsg => logMessage(msg) case m: SetPresentationDownloadableEvtMsg => logMessage(msg) + //case m: PresentationPageConvertedSysMsg => logMessage(msg) + //case m: PresentationPageConvertedEventMsg => logMessage(msg) + case m: PresentationPageConversionStartedSysMsg => logMessage(msg) + case m: PresentationConversionEndedSysMsg => logMessage(msg) + case m: PresentationConversionRequestReceivedSysMsg => logMessage(msg) + //case m: PresentationConversionCompletedEvtMsg => logMessage(msg) + case m: GetAllPresentationPodsReqMsg => logMessage(msg) + //case m: PresentationPageGeneratedSysPubMsg => logMessage(msg) + //case m: PresentationPageGeneratedEvtMsg => logMessage(msg) + //case m: ResizeAndMovePagePubMsg => logMessage(msg) + case m: PresentationConversionUpdateSysPubMsg => logMessage(msg) + case m: PresentationConversionUpdateEvtMsgBody => logMessage(msg) + case m: PresentationPageCountErrorSysPubMsg => logMessage(msg) + case m: PresentationPageCountErrorEvtMsg => logMessage(msg) // Group Chats case m: SendGroupChatMessageMsg => logMessage(msg) diff --git a/akka-bbb-fsesl/project/Dependencies.scala b/akka-bbb-fsesl/project/Dependencies.scala index 51597f6a5c5f98b2b2e5ee029e04d5773298fdd9..b6e51d790cc9af5999d77976f60e506559d05cbd 100755 --- a/akka-bbb-fsesl/project/Dependencies.scala +++ b/akka-bbb-fsesl/project/Dependencies.scala @@ -17,8 +17,8 @@ object Dependencies { val logback = "1.2.3" // Apache Commons - val lang = "3.8.1" - val codec = "1.11" + val lang = "3.9" + val codec = "1.14" // BigBlueButton val bbbCommons = "0.0.20-SNAPSHOT" @@ -39,7 +39,6 @@ object Dependencies { val akkaStream = "com.typesafe.akka" %% "akka-stream" % Versions.akkaVersion val akkaHttp = "com.typesafe.akka" %% "akka-http" % Versions.akkaHttpVersion - // https://mvnrepository.com/artifact/com.typesafe.akka/akka-http-spray-json val akkaHttpSprayJson = "com.typesafe.akka" %% "akka-http-spray-json" % Versions.akkaHttpVersion val logback = "ch.qos.logback" % "logback-classic" % Versions.logback diff --git a/bbb-common-message/project/Dependencies.scala b/bbb-common-message/project/Dependencies.scala index 05566bb4fac9860961118497c1343936ffb0f567..8a3894c7c8042e828f79156fbbed1b758d5c4de1 100644 --- a/bbb-common-message/project/Dependencies.scala +++ b/bbb-common-message/project/Dependencies.scala @@ -18,7 +18,7 @@ object Dependencies { val jackson = "2.9.7" val sl4j = "1.7.25" val red5 = "1.0.10-M9" - val pool = "2.6.0" + val pool = "2.8.0" // Redis val lettuce = "5.1.3.RELEASE" diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Presentation.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Presentation.scala index 925c5b2c1ad5cb4c454bbb89793f7d10a419d1a9..a7d55b635928a4ba29129042f333edfad6599676 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Presentation.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Presentation.scala @@ -9,3 +9,21 @@ case class PageVO(id: String, num: Int, thumbUri: String = "", swfUri: String, case class PresentationPodVO(id: String, currentPresenter: String, presentations: Vector[PresentationVO]) + +case class PresentationPageConvertedVO( + id: String, + num: Int, + urls: Map[String, String], + current: Boolean = false +) + +case class PresentationPageVO( + id: String, + num: Int, + urls: Map[String, String], + current: Boolean = false, + xOffset: Double = 0, + yOffset: Double = 0, + widthRatio: Double = 100D, + heightRatio: Double = 100D +) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationPodsMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationPodsMsgs.scala index 2d36a93c848ad68ed3366cbb725c46c0c6696200..75623c81b32e7c7dbd0550a57031170602d82925 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationPodsMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PresentationPodsMsgs.scala @@ -1,6 +1,6 @@ package org.bigbluebutton.common2.msgs -import org.bigbluebutton.common2.domain.{ PresentationPodVO, PresentationVO } +import org.bigbluebutton.common2.domain.{ PageVO, PresentationPageConvertedVO, PresentationPageVO, PresentationPodVO, PresentationVO } // ------------ client to akka-apps ------------ object CreateNewPresentationPodPubMsg { val NAME = "CreateNewPresentationPodPubMsg" } @@ -73,8 +73,8 @@ case class PdfConversionInvalidErrorSysPubMsg( body: PdfConversionInvalidErrorSysPubMsgBody ) extends StandardMsg case class PdfConversionInvalidErrorSysPubMsgBody(podId: String, messageKey: String, code: String, presentationId: String, - bigPageNumber: Int, bigPageSize: Int, presName: String) - + bigPageNumber: Int, bigPageSize: Int, presName: String) + object PresentationPageGeneratedSysPubMsg { val NAME = "PresentationPageGeneratedSysPubMsg" } case class PresentationPageGeneratedSysPubMsg( header: BbbClientMsgHeader, @@ -90,6 +90,63 @@ case class PresentationConversionCompletedSysPubMsg( ) extends StandardMsg case class PresentationConversionCompletedSysPubMsgBody(podId: String, messageKey: String, code: String, presentation: PresentationVO) + +object PresentationPageConvertedSysMsg { val NAME = "PresentationPageConvertedSysMsg" } +case class PresentationPageConvertedSysMsg( + header: BbbClientMsgHeader, + body: PresentationPageConvertedSysMsgBody +) extends StandardMsg +case class PresentationPageConvertedSysMsgBody( + podId: String, + messageKey: String, + code: String, + presentationId: String, + numberOfPages: Int, + pagesCompleted: Int, + presName: String, + page: PresentationPageConvertedVO +) + +object PresentationConversionRequestReceivedSysMsg { val NAME = "PresentationConversionRequestReceivedSysMsg" } +case class PresentationConversionRequestReceivedSysMsg( + header: BbbClientMsgHeader, + body: PresentationConversionRequestReceivedSysMsgBody +) extends StandardMsg +case class PresentationConversionRequestReceivedSysMsgBody( + podId: String, + presentationId: String, + current: Boolean, + presName: String, + downloadable: Boolean, + authzToken: String +) + +object PresentationPageConversionStartedSysMsg { val NAME = "PresentationPageConversionStartedSysMsg" } +case class PresentationPageConversionStartedSysMsg( + header: BbbClientMsgHeader, + body: PresentationPageConversionStartedSysMsgBody +) extends StandardMsg +case class PresentationPageConversionStartedSysMsgBody( + podId: String, + presentationId: String, + current: Boolean, + presName: String, + downloadable: Boolean, + authzToken: String, + numPages: Int +) + +object PresentationConversionEndedSysMsg { val NAME = "PresentationConversionEndedSysMsg" } +case class PresentationConversionEndedSysMsg( + header: BbbClientMsgHeader, + body: PresentationConversionEndedSysMsgBody +) extends StandardMsg +case class PresentationConversionEndedSysMsgBody( + podId: String, + presentationId: String, + presName: String +) + // ------------ bbb-common-web to akka-apps ------------ // ------------ akka-apps to client ------------ @@ -125,6 +182,62 @@ object PresentationPageGeneratedEvtMsg { val NAME = "PresentationPageGeneratedEv case class PresentationPageGeneratedEvtMsg(header: BbbClientMsgHeader, body: PresentationPageGeneratedEvtMsgBody) extends BbbCoreMsg case class PresentationPageGeneratedEvtMsgBody(podId: String, messageKey: String, code: String, presentationId: String, numberOfPages: Int, pagesCompleted: Int, presName: String) +object PresentationPageConvertedEventMsg { val NAME = "PresentationPageConvertedEventMsg" } +case class PresentationPageConvertedEventMsg( + header: BbbClientMsgHeader, + body: PresentationPageConvertedEventMsgBody +) extends BbbCoreMsg +case class PresentationPageConvertedEventMsgBody( + podId: String, + messageKey: String, + code: String, + presentationId: String, + numberOfPages: Int, + pagesCompleted: Int, + presName: String, + page: PresentationPageVO +) + +object PresentationConversionRequestReceivedEventMsg { val NAME = "PresentationConversionRequestReceivedEventMsg" } +case class PresentationConversionRequestReceivedEventMsg( + header: BbbClientMsgHeader, + body: PresentationConversionRequestReceivedEventMsgBody +) extends StandardMsg +case class PresentationConversionRequestReceivedEventMsgBody( + podId: String, + presentationId: String, + current: Boolean, + presName: String, + downloadable: Boolean, + authzToken: String +) + +object PresentationPageConversionStartedEventMsg { val NAME = "PresentationPageConversionStartedEventMsg" } +case class PresentationPageConversionStartedEventMsg( + header: BbbClientMsgHeader, + body: PresentationPageConversionStartedEventMsgBody +) extends StandardMsg +case class PresentationPageConversionStartedEventMsgBody( + podId: String, + presentationId: String, + current: Boolean, + presName: String, + downloadable: Boolean, + numPages: Int, + authzToken: String +) + +object PresentationConversionEndedEventMsg { val NAME = "PresentationConversionEndedEventMsg" } +case class PresentationConversionEndedEventMsg( + header: BbbClientMsgHeader, + body: PresentationConversionEndedEventMsgBody +) extends StandardMsg +case class PresentationConversionEndedEventMsgBody( + podId: String, + presentationId: String, + presName: String +) + object PresentationConversionCompletedEvtMsg { val NAME = "PresentationConversionCompletedEvtMsg" } case class PresentationConversionCompletedEvtMsg(header: BbbClientMsgHeader, body: PresentationConversionCompletedEvtMsgBody) extends BbbCoreMsg case class PresentationConversionCompletedEvtMsgBody(podId: String, messageKey: String, code: String, presentation: PresentationVO) diff --git a/bbb-common-web/project/Dependencies.scala b/bbb-common-web/project/Dependencies.scala index 5c7efa7551bceb08e0015f88054991f73216cc26..c9e9447b525a60c5ba55669370570f9cf1a05c2b 100644 --- a/bbb-common-web/project/Dependencies.scala +++ b/bbb-common-web/project/Dependencies.scala @@ -16,13 +16,13 @@ object Dependencies { val akkaVersion = "2.5.19" val gson = "2.8.5" val jackson = "2.9.7" - val freemaker = "2.3.28" + val freemarker = "2.3.28" val apacheHttp = "4.5.6" val apacheHttpAsync = "4.1.4" // Office and document conversion val jodConverter = "4.2.1" - val apachePoi = "3.17" + val apachePoi = "4.1.2" val nuProcess = "1.2.4" val libreOffice = "5.4.2" @@ -30,9 +30,9 @@ object Dependencies { val servlet = "3.1.0" // Apache Commons - val lang = "3.8.1" + val lang = "3.9" val io = "2.6" - val pool = "2.6.0" + val pool = "2.8.0" // BigBlueButton val bbbCommons = "0.0.20-SNAPSHOT" @@ -51,7 +51,7 @@ object Dependencies { val googleGson = "com.google.code.gson" % "gson" % Versions.gson val jacksonModule = "com.fasterxml.jackson.module" %% "jackson-module-scala" % Versions.jackson val jacksonXml = "com.fasterxml.jackson.dataformat" % "jackson-dataformat-xml" % Versions.jackson - val freeMaker = "org.freemarker" % "freemarker" % Versions.freemaker + val freemarker = "org.freemarker" % "freemarker" % Versions.freemarker val apacheHttp = "org.apache.httpcomponents" % "httpclient" % Versions.apacheHttp val apacheHttpAsync = "org.apache.httpcomponents" % "httpasyncclient" % Versions.apacheHttpAsync @@ -95,7 +95,7 @@ object Dependencies { Compile.googleGson, Compile.jacksonModule, Compile.jacksonXml, - Compile.freeMaker, + Compile.freemarker, Compile.apacheHttp, Compile.apacheHttpAsync, Compile.poiXml, diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/ConversionMessageConstants.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/ConversionMessageConstants.java index e585a4000ad5fbc11490c76d4d4e7d237f370221..5c4618a982f42404332c4d6ea6c299c2969d0a3c 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/ConversionMessageConstants.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/ConversionMessageConstants.java @@ -35,6 +35,7 @@ public class ConversionMessageConstants { public static final String GENERATED_TEXTFILES_KEY = "GENERATED_TEXTFILES"; public static final String GENERATING_SVGIMAGES_KEY = "GENERATING_SVGIMAGES"; public static final String GENERATED_SVGIMAGES_KEY = "GENERATED_SVGIMAGES"; + public static final String CONVERSION_STARTED_KEY = "CONVERSION_STARTED_KEY"; public static final String CONVERSION_COMPLETED_KEY = "CONVERSION_COMPLETED"; private ConversionMessageConstants() { diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/DocumentConversionServiceImp.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/DocumentConversionServiceImp.java index 3f979ef8b7250ab93df382278a189f7bec7af7b8..3a1c4f70fbef855b7028e28bcfe4c23e64ddd337 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/DocumentConversionServiceImp.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/DocumentConversionServiceImp.java @@ -23,38 +23,30 @@ import java.util.HashMap; import java.util.Map; import org.bigbluebutton.api2.IBbbWebApiGWApp; -import org.bigbluebutton.presentation.imp.ImageToSwfSlidesGenerationService; -import org.bigbluebutton.presentation.imp.OfficeToPdfConversionService; -import org.bigbluebutton.presentation.imp.PdfToSwfSlidesGenerationService; +import org.bigbluebutton.presentation.imp.*; +import org.bigbluebutton.presentation.messages.DocPageConversionStarted; +import org.bigbluebutton.presentation.messages.DocConversionRequestReceived; +import org.bigbluebutton.presentation.messages.DocPageCountExceeded; +import org.bigbluebutton.presentation.messages.DocPageCountFailed; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; public class DocumentConversionServiceImp implements DocumentConversionService { - private static Logger log = LoggerFactory - .getLogger(DocumentConversionServiceImp.class); + private static Logger log = LoggerFactory.getLogger(DocumentConversionServiceImp.class); private IBbbWebApiGWApp gw; private OfficeToPdfConversionService officeToPdfConversionService; - private PdfToSwfSlidesGenerationService pdfToSwfSlidesGenerationService; - private ImageToSwfSlidesGenerationService imageToSwfSlidesGenerationService; + private SwfSlidesGenerationProgressNotifier notifier; + + private PresentationFileProcessor presentationFileProcessor; public void processDocument(UploadedPresentation pres) { - SupportedDocumentFilter sdf = new SupportedDocumentFilter(gw); - Map<String, Object> logData = new HashMap<String, Object>(); - logData.put("podId", pres.getPodId()); - logData.put("meetingId", pres.getMeetingId()); - logData.put("presId", pres.getId()); - logData.put("filename", pres.getName()); - logData.put("current", pres.isCurrent()); - logData.put("logCode", "presentation_conversion_start"); - logData.put("message", "Start presentation conversion."); + SupportedDocumentFilter sdf = new SupportedDocumentFilter(gw); - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - log.info(" --analytics-- data={}", logStr); + sendDocConversionRequestReceived(pres); if (sdf.isSupported(pres)) { String fileType = pres.getFileType(); @@ -75,10 +67,11 @@ public class DocumentConversionServiceImp implements DocumentConversionService { ocsf.sendProgress(pres); } } else if (SupportedFileTypes.isPdfFile(fileType)) { - pdfToSwfSlidesGenerationService.generateSlides(pres); + presentationFileProcessor.process(pres); } else if (SupportedFileTypes.isImageFile(fileType)) { - imageToSwfSlidesGenerationService.generateSlides(pres); + presentationFileProcessor.process(pres); } else { + Map<String, Object> logData = new HashMap<String, Object>(); logData = new HashMap<String, Object>(); logData.put("podId", pres.getPodId()); logData.put("meetingId", pres.getMeetingId()); @@ -87,12 +80,14 @@ public class DocumentConversionServiceImp implements DocumentConversionService { logData.put("current", pres.isCurrent()); logData.put("logCode", "supported_file_not_handled"); logData.put("message", "Supported file not handled."); - gson = new Gson(); - logStr = gson.toJson(logData); + + Gson gson = new Gson(); + String logStr = gson.toJson(logData); log.warn(" --analytics-- data={}", logStr); } } else { + Map<String, Object> logData = new HashMap<String, Object>(); logData = new HashMap<String, Object>(); logData.put("podId", pres.getPodId()); logData.put("meetingId", pres.getMeetingId()); @@ -101,22 +96,58 @@ public class DocumentConversionServiceImp implements DocumentConversionService { logData.put("current", pres.isCurrent()); logData.put("logCode", "unsupported_file_format"); logData.put("message", "Unsupported file format"); - gson = new Gson(); - logStr = gson.toJson(logData); + + Gson gson = new Gson(); + String logStr = gson.toJson(logData); log.error(" --analytics-- data={}", logStr); + + logData.clear(); + + logData.put("podId", pres.getPodId()); + logData.put("meetingId", pres.getMeetingId()); + logData.put("presId", pres.getId()); + logData.put("filename", pres.getName()); + logData.put("current", pres.isCurrent()); + logData.put("logCode", "presentation_conversion_end"); + logData.put("message", "End presentation conversion."); + + logStr = gson.toJson(logData); + log.info(" --analytics-- data={}", logStr); + + notifier.sendConversionCompletedMessage(pres); } - logData = new HashMap<String, Object>(); - logData.put("podId", pres.getPodId()); - logData.put("meetingId", pres.getMeetingId()); - logData.put("presId", pres.getId()); - logData.put("filename", pres.getName()); - logData.put("current", pres.isCurrent()); - logData.put("logCode", "presentation_conversion_end"); - logData.put("message", "End presentation conversion."); - gson = new Gson(); - logStr = gson.toJson(logData); - log.info(" --analytics-- data={}", logStr); + } + + private void sendDocConversionRequestReceived(UploadedPresentation pres) { + if (! pres.isConversionStarted()) { + Map<String, Object> logData = new HashMap<String, Object>(); + + logData.put("podId", pres.getPodId()); + logData.put("meetingId", pres.getMeetingId()); + logData.put("presId", pres.getId()); + logData.put("filename", pres.getName()); + logData.put("current", pres.isCurrent()); + logData.put("authzToken", pres.getAuthzToken()); + logData.put("logCode", "presentation_conversion_start"); + logData.put("message", "Start presentation conversion."); + + Gson gson = new Gson(); + String logStr = gson.toJson(logData); + log.info(" --analytics-- data={}", logStr); + + pres.startConversion(); + + DocConversionRequestReceived progress = new DocConversionRequestReceived( + pres.getPodId(), + pres.getMeetingId(), + pres.getId(), + pres.getName(), + pres.getAuthzToken(), + pres.isDownloadable(), + pres.isCurrent()); + notifier.sendDocConversionProgress(progress); + } } public void setBbbWebApiGWApp(IBbbWebApiGWApp m) { @@ -127,13 +158,11 @@ public class DocumentConversionServiceImp implements DocumentConversionService { officeToPdfConversionService = s; } - public void setPdfToSwfSlidesGenerationService( - PdfToSwfSlidesGenerationService s) { - pdfToSwfSlidesGenerationService = s; + public void setSwfSlidesGenerationProgressNotifier(SwfSlidesGenerationProgressNotifier notifier) { + this.notifier = notifier; } - public void setImageToSwfSlidesGenerationService( - ImageToSwfSlidesGenerationService s) { - imageToSwfSlidesGenerationService = s; + public void setPresentationFileProcessor(PresentationFileProcessor presentationFileProcessor) { + this.presentationFileProcessor = presentationFileProcessor; } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PdfToSwfSlide.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PdfToSwfSlide.java index 76f5da9a6ca4dd929673042eea9c36f11766fe2a..f39d50077e9f7cafe802411b1891f9355e682e4b 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PdfToSwfSlide.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PdfToSwfSlide.java @@ -41,22 +41,21 @@ public class PdfToSwfSlide { private volatile boolean done = false; private File slide; + private File pageFile; - public PdfToSwfSlide(UploadedPresentation pres, int page) { + public PdfToSwfSlide(UploadedPresentation pres, int page, File pageFile) { this.pres = pres; this.page = page; + this.pageFile = pageFile; } public PdfToSwfSlide createSlide() { - File presentationFile = pres.getUploadedFile(); - slide = new File(presentationFile.getParent() + File.separatorChar - + "slide-" + page + ".swf"); - pdfToSwfConverter.convert(presentationFile, slide, page, pres); + slide = new File(pageFile.getParent() + File.separatorChar + "slide-" + page + ".swf"); + pdfToSwfConverter.convert(pageFile, slide, page, pres); // If all fails, generate a blank slide. if (!slide.exists()) { - log.warn("Failed to create slide. Creating blank slide for " - + slide.getAbsolutePath()); + log.warn("Failed to create slide. Creating blank slide for " + slide.getAbsolutePath()); generateBlankSlide(); } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PngCreator.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PngCreator.java index 1ba895d140d5de280203ae4ab73fadf8874cb925..5bf36b5297664fc9c52fd5c412e50da242fcf17a 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PngCreator.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PngCreator.java @@ -18,6 +18,8 @@ package org.bigbluebutton.presentation; +import java.io.File; + public interface PngCreator { - public boolean createPng(UploadedPresentation pres); + public boolean createPng(UploadedPresentation pres, int page, File pageFile); } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PresentationUrlDownloadService.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PresentationUrlDownloadService.java index 0c0b1a360bc60e176fdb047133ecb56340b72163..6e13cf2428c39091c4d7bed2459844238af3790d 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PresentationUrlDownloadService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PresentationUrlDownloadService.java @@ -59,10 +59,10 @@ public class PresentationUrlDownloadService { } public void processUploadedFile(String podId, String meetingId, String presId, - String filename, File presFile, Boolean current) { + String filename, File presFile, Boolean current, String authzToken) { // TODO add podId UploadedPresentation uploadedPres = new UploadedPresentation(podId, meetingId, - presId, filename, presentationBaseURL, current); + presId, filename, presentationBaseURL, current, authzToken); uploadedPres.setUploadedFile(presFile); processUploadedPresentation(uploadedPres); } @@ -145,7 +145,7 @@ public class PresentationUrlDownloadService { // Hardcode pre-uploaded presentation for breakout room to the default presentation window processUploadedFile("DEFAULT_PRESENTATION_POD", destinationMeetingId, presId, "default-" + presentationSlide.toString() + "." + filenameExt, - newPresentation, true); + newPresentation, true, "breakout-authz-token"); } public String generatePresentationId(String name) { diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/SvgImageCreator.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/SvgImageCreator.java index d0af7ad08d7ce646630052056a3082d664e43fc8..f43e48ff1b021708bf96ff5f20c457fcb2f07b07 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/SvgImageCreator.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/SvgImageCreator.java @@ -23,5 +23,5 @@ package org.bigbluebutton.presentation; public interface SvgImageCreator { - public boolean createSvgImages(UploadedPresentation pres); + public boolean createSvgImage(UploadedPresentation pres, int page); } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/TextFileCreator.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/TextFileCreator.java index a412755eb32f10eee4ae3002c9b08c9521138afd..145bb8424a43747bcf665da05e348896ba317df1 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/TextFileCreator.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/TextFileCreator.java @@ -20,5 +20,5 @@ package org.bigbluebutton.presentation; public interface TextFileCreator { - public boolean createTextFiles(UploadedPresentation pres); + public boolean createTextFile(UploadedPresentation pres, int page); } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/ThumbnailCreator.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/ThumbnailCreator.java index 711c5b763d9b2164ad3f75d11f499603a5e0e1f1..5c82a20cb7ddd91756ca685f8d14118166848d29 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/ThumbnailCreator.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/ThumbnailCreator.java @@ -19,6 +19,8 @@ package org.bigbluebutton.presentation; +import java.io.File; + public interface ThumbnailCreator { - public boolean createThumbnails(UploadedPresentation pres); + public boolean createThumbnail(UploadedPresentation pres, int page, File pageFile); } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/UploadedPresentation.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/UploadedPresentation.java index 867982abe822aaf5015041579aa64501f0edc8cc..0bbb49eec26f8957751a6567178c176087d2d64b 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/UploadedPresentation.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/UploadedPresentation.java @@ -33,8 +33,16 @@ public final class UploadedPresentation { private final String baseUrl; private boolean isDownloadable = false; private boolean current = false; - - public UploadedPresentation(String podId, String meetingId, String id, String name, String baseUrl, Boolean current) { + private String authzToken; + private boolean conversionStarted = false; + + public UploadedPresentation(String podId, + String meetingId, + String id, + String name, + String baseUrl, + Boolean current, + String authzToken) { this.podId = podId; this.meetingId = meetingId; this.id = id; @@ -42,6 +50,7 @@ public final class UploadedPresentation { this.baseUrl = baseUrl; this.isDownloadable = false; this.current = current; + this.authzToken = authzToken; } public File getUploadedFile() { @@ -111,4 +120,16 @@ public final class UploadedPresentation { public void setCurrent(Boolean value) { this.current = value; } + + public String getAuthzToken() { + return authzToken; + } + + public void startConversion() { + conversionStarted = true; + } + + public boolean isConversionStarted() { + return conversionStarted; + } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ImageToSwfSlidesGenerationService.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ImageToSwfSlidesGenerationService.java index 85c80db1f2895492287bc0536fe0236e5e021437..febc1fa9640a75f2941b73db889f2bfb0b98a4f3 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ImageToSwfSlidesGenerationService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ImageToSwfSlidesGenerationService.java @@ -67,29 +67,35 @@ public class ImageToSwfSlidesGenerationService { executor = Executors.newFixedThreadPool(numThreads); completionService = new ExecutorCompletionService<ImageToSwfSlide>(executor); } - + public void generateSlides(UploadedPresentation pres) { - pres.setNumberOfPages(1); // There should be only one image to convert. - if (swfSlidesRequired) { - if (pres.getNumberOfPages() > 0) { - PageConverter pageConverter = determinePageConverter(pres); - convertImageToSwf(pres, pageConverter); - } - } - - /* adding accessibility */ - createTextFiles(pres); - createThumbnails(pres); - - if (svgImagesRequired) { - createSvgImages(pres); + + for (int page = 1; page <= pres.getNumberOfPages(); page++) { + if (swfSlidesRequired) { + if (pres.getNumberOfPages() > 0) { + PageConverter pageConverter = determinePageConverter(pres); + convertImageToSwf(pres, pageConverter); + } + } + + /* adding accessibility */ + createTextFiles(pres, page); + createThumbnails(pres, page); + + if (svgImagesRequired) { + createSvgImages(pres, page); + } + + if (generatePngs) { + createPngImages(pres, page); + } + + notifier.sendConversionUpdateMessage(page, pres, page); } - - if (generatePngs) { - createPngImages(pres); - } - + + System.out.println("****** Conversion complete for " + pres.getName()); notifier.sendConversionCompletedMessage(pres); + } private PageConverter determinePageConverter(UploadedPresentation pres) { @@ -101,26 +107,26 @@ public class ImageToSwfSlidesGenerationService { return pngToSwfConverter; } - private void createTextFiles(UploadedPresentation pres) { + private void createTextFiles(UploadedPresentation pres, int page) { log.debug("Creating textfiles for accessibility."); notifier.sendCreatingTextFilesUpdateMessage(pres); - textFileCreator.createTextFiles(pres); + textFileCreator.createTextFile(pres, page); } - private void createThumbnails(UploadedPresentation pres) { + private void createThumbnails(UploadedPresentation pres, int page) { log.debug("Creating thumbnails."); notifier.sendCreatingThumbnailsUpdateMessage(pres); - thumbnailCreator.createThumbnails(pres); + thumbnailCreator.createThumbnail(pres, page, pres.getUploadedFile()); } - private void createSvgImages(UploadedPresentation pres) { + private void createSvgImages(UploadedPresentation pres, int page) { log.debug("Creating SVG images."); notifier.sendCreatingSvgImagesUpdateMessage(pres); - svgImageCreator.createSvgImages(pres); + svgImageCreator.createSvgImage(pres, page); } - private void createPngImages(UploadedPresentation pres) { - pngCreator.createPng(pres); + private void createPngImages(UploadedPresentation pres, int page) { + pngCreator.createPng(pres, page, pres.getUploadedFile()); } private void convertImageToSwf(UploadedPresentation pres, PageConverter pageConverter) { @@ -144,8 +150,7 @@ public class ImageToSwfSlidesGenerationService { private void handleSlideGenerationResult(UploadedPresentation pres, ImageToSwfSlide[] slides) { long endTime = System.currentTimeMillis() + MAX_CONVERSION_TIME; - int slideGenerated = 0; - + for (int t = 0; t < slides.length; t++) { Future<ImageToSwfSlide> future = null; ImageToSwfSlide slide = null; @@ -166,8 +171,6 @@ public class ImageToSwfSlidesGenerationService { slide.generateBlankSlide(); } } - slideGenerated++; - notifier.sendConversionUpdateMessage(slideGenerated, pres); } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/Jpeg2SwfPageConverter.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/Jpeg2SwfPageConverter.java index d01e52c25b3fa469e1f073d5e79018edc5b0361a..49f15275ac0f5f06387a7631f875023a75c9132c 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/Jpeg2SwfPageConverter.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/Jpeg2SwfPageConverter.java @@ -39,7 +39,7 @@ public class Jpeg2SwfPageConverter implements PageConverter { String COMMAND = SWFTOOLS_DIR + File.separatorChar + "jpeg2swf -o " + output.getAbsolutePath() + " " + presentationFile.getAbsolutePath(); - boolean done = new ExternalProcessExecutor().exec(COMMAND, 60000); + boolean done = new ExternalProcessExecutor().exec(COMMAND, 10000); if (done && output.exists()) { return true; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/OfficeDocumentValidator2.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/OfficeDocumentValidator2.java index bb32ba6baf3a19e9695caacfa4cfb5cfd3516857..0f274ec67523ed6ef7f4996ce85558699fc9bc5c 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/OfficeDocumentValidator2.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/OfficeDocumentValidator2.java @@ -24,7 +24,7 @@ public class OfficeDocumentValidator2 { log.info("Running pres check " + COMMAND); - boolean done = new ExternalProcessExecutor().exec(COMMAND, 60000); + boolean done = new ExternalProcessExecutor().exec(COMMAND, 25000); if (done) { return true; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/OfficeToPdfConversionService.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/OfficeToPdfConversionService.java index d182dcb2ff3347d8f303b2fa70fe6e71f410011b..e60ec5794d86c31bfd4c74bdfad7d1c53e887057 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/OfficeToPdfConversionService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/OfficeToPdfConversionService.java @@ -41,6 +41,7 @@ public class OfficeToPdfConversionService { private OfficeDocumentValidator2 officeDocumentValidator; private final OfficeManager officeManager; private final OfficeDocumentConverter documentConverter; + private boolean skipOfficePrecheck = false; public OfficeToPdfConversionService() { final DefaultOfficeManagerBuilder configuration = new DefaultOfficeManagerBuilder(); @@ -57,8 +58,8 @@ public class OfficeToPdfConversionService { public UploadedPresentation convertOfficeToPdf(UploadedPresentation pres) { initialize(pres); if (SupportedFileTypes.isOfficeFile(pres.getFileType())) { - boolean valid = officeDocumentValidator.isValid(pres); - if (!valid) { + // Check if we need to precheck office document + if (!skipOfficePrecheck && officeDocumentValidator.isValid(pres)) { Map<String, Object> logData = new HashMap<>(); logData.put("meetingId", pres.getMeetingId()); logData.put("presId", pres.getId()); @@ -128,6 +129,10 @@ public class OfficeToPdfConversionService { officeDocumentValidator = v; } + public void setSkipOfficePrecheck(boolean skipOfficePrecheck) { + this.skipOfficePrecheck = skipOfficePrecheck; + } + public void start() { try { officeManager.start(); diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PageExtractorImp.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PageExtractorImp.java index 42a6bb9cdba36469a7696ee76aef7033a4bfe524..334d072e65e516aa7cd73d0ac3e00afea1fcfbd6 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PageExtractorImp.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PageExtractorImp.java @@ -29,10 +29,11 @@ public class PageExtractorImp implements PageExtractor { private static Logger log = LoggerFactory.getLogger(PageExtractorImp.class); private static final String SPACE = " "; + private static final long extractTimeout = 10000; // 10sec public boolean extractPage(File presentationFile, File output, int page) { String COMMAND = "pdfseparate -f " + page + " -l " + page + SPACE + presentationFile.getAbsolutePath() + SPACE + output.getAbsolutePath(); - return new ExternalProcessExecutor().exec(COMMAND, 60000); + return new ExternalProcessExecutor().exec(COMMAND, extractTimeout); } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PageToConvert.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PageToConvert.java new file mode 100755 index 0000000000000000000000000000000000000000..dd6eb1ef2ab7deac2bfb4060484f1b811f75a65a --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PageToConvert.java @@ -0,0 +1,135 @@ +package org.bigbluebutton.presentation.imp; + + +import org.bigbluebutton.presentation.*; +import java.io.File; + +public class PageToConvert { + + private UploadedPresentation pres; + private int page; + + private boolean swfSlidesRequired; + private boolean svgImagesRequired; + private boolean generatePngs; + private PageExtractor pageExtractor; + + private String BLANK_SLIDE; + private int MAX_SWF_FILE_SIZE; + + private TextFileCreator textFileCreator; + private SvgImageCreator svgImageCreator; + private ThumbnailCreator thumbnailCreator; + private PngCreator pngCreator; + private PageConverter pdfToSwfConverter; + private SwfSlidesGenerationProgressNotifier notifier; + private File pageFile; + + public PageToConvert(UploadedPresentation pres, + int page, + File pageFile, + boolean swfSlidesRequired, + boolean svgImagesRequired, + boolean generatePngs, + TextFileCreator textFileCreator, + SvgImageCreator svgImageCreator, + ThumbnailCreator thumbnailCreator, + PngCreator pngCreator, + PageConverter pdfToSwfConverter, + SwfSlidesGenerationProgressNotifier notifier, + String blankSlide, + int maxSwfFileSize) { + this.pres = pres; + this.page = page; + this.pageFile = pageFile; + this.swfSlidesRequired = swfSlidesRequired; + this.svgImagesRequired = svgImagesRequired; + this.generatePngs = generatePngs; + this.textFileCreator = textFileCreator; + this.svgImageCreator = svgImageCreator; + this.thumbnailCreator = thumbnailCreator; + this.pngCreator = pngCreator; + this.pdfToSwfConverter = pdfToSwfConverter; + this.notifier = notifier; + this.BLANK_SLIDE = blankSlide; + this.MAX_SWF_FILE_SIZE = maxSwfFileSize; + } + + public File getPageFile() { + return pageFile; + } + + public int getPageNumber() { + return page; + } + + public String getPresId() { + return pres.getId(); + } + + public PageToConvert convert() { + + // Only create SWF files if the configuration requires it + if (swfSlidesRequired) { + convertPdfToSwf(pres, page, pageFile); + } + + /* adding accessibility */ + createThumbnails(pres, page, pageFile); + + createTextFiles(pres, page); + + // only create SVG images if the configuration requires it + if (svgImagesRequired) { + createSvgImages(pres, page); + } + + // only create PNG images if the configuration requires it + if (generatePngs) { + createPngImages(pres, page, pageFile); + } + + return this; + } + + private void createThumbnails(UploadedPresentation pres, int page, File pageFile) { + //notifier.sendCreatingThumbnailsUpdateMessage(pres); + thumbnailCreator.createThumbnail(pres, page, pageFile); + } + + private void createTextFiles(UploadedPresentation pres, int page) { + //notifier.sendCreatingTextFilesUpdateMessage(pres); + textFileCreator.createTextFile(pres, page); + } + + private void createSvgImages(UploadedPresentation pres, int page) { + //notifier.sendCreatingSvgImagesUpdateMessage(pres); + svgImageCreator.createSvgImage(pres, page); + } + + private void createPngImages(UploadedPresentation pres, int page, File pageFile) { + pngCreator.createPng(pres, page, pageFile); + } + + private void convertPdfToSwf(UploadedPresentation pres, int page, File pageFile) { + PdfToSwfSlide slide = setupSlide(pres, page, pageFile); + generateSlides(pres, slide); + } + + + private void generateSlides(UploadedPresentation pres, PdfToSwfSlide slide) { + slide.createSlide(); + if (!slide.isDone()) { + slide.generateBlankSlide(); + } + } + + private PdfToSwfSlide setupSlide(UploadedPresentation pres, int page, File pageFile) { + PdfToSwfSlide slide = new PdfToSwfSlide(pres, page, pageFile); + slide.setBlankSlide(BLANK_SLIDE); + slide.setMaxSwfFileSize(MAX_SWF_FILE_SIZE); + slide.setPageConverter(pdfToSwfConverter); + + return slide; + } +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/Pdf2SwfPageConverter.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/Pdf2SwfPageConverter.java index dea17d7b7add89888b6376269066ef31134a7412..cdde5ef21d4394a5004fef707925521a012099ec 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/Pdf2SwfPageConverter.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/Pdf2SwfPageConverter.java @@ -51,8 +51,7 @@ public class Pdf2SwfPageConverter implements PageConverter { private String convTimeout = "7s"; private int WAIT_FOR_SEC = 7; - public boolean convert(File presentation, File output, int page, - UploadedPresentation pres) { + public boolean convert(File presentation, File output, int page, UploadedPresentation pres) { long convertStart = System.currentTimeMillis(); String source = presentation.getAbsolutePath(); @@ -64,7 +63,7 @@ public class Pdf2SwfPageConverter implements PageConverter { NuProcessBuilder pb = new NuProcessBuilder(Arrays.asList("timeout", convTimeout, "/bin/sh", "-c", SWFTOOLS_DIR + File.separatorChar + "pdf2swf" + " -vv " + AVM2SWF + " -F " - + fontsDir + " -p " + Integer.toString(page) + " " + source + " -o " + + fontsDir + " " + source + " -o " + dest + " | egrep 'shape id|Updating font|Drawing' | sed 's/ / /g' | cut -d' ' -f 1-3 | sort | uniq -cw 15")); @@ -81,8 +80,7 @@ public class Pdf2SwfPageConverter implements PageConverter { } long pdf2SwfEnd = System.currentTimeMillis(); - log.debug("Pdf2Swf conversion duration: {} sec", - (pdf2SwfEnd - pdf2SwfStart) / 1000); + log.debug("Pdf2Swf conversion duration: {} sec", (pdf2SwfEnd - pdf2SwfStart) / 1000); boolean timedOut = pdf2SwfEnd - pdf2SwfStart >= Integer.parseInt(convTimeout.replaceFirst("s", "")) @@ -93,6 +91,7 @@ public class Pdf2SwfPageConverter implements PageConverter { + defineTextThreshold + imageTagThreshold) * 2; File destFile = new File(dest); + if (pHandler.isCommandSuccessful() && destFile.exists() && pHandler.numberOfPlacements() < placementsThreshold && pHandler.numberOfTextTags() < defineTextThreshold @@ -144,7 +143,6 @@ public class Pdf2SwfPageConverter implements PageConverter { NuProcessBuilder pbPng = new NuProcessBuilder( Arrays.asList("timeout", convTimeout, "pdftocairo", "-png", "-singlefile", "-r", timedOut || twiceTotalObjects ? "72" : "150", - "-f", String.valueOf(page), "-l", String.valueOf(page), presentation.getAbsolutePath(), tempPng.getAbsolutePath() .substring(0, tempPng.getAbsolutePath().lastIndexOf('.')))); @@ -177,6 +175,7 @@ public class Pdf2SwfPageConverter implements PageConverter { log.error("InterruptedException while creating SWF {}", pres.getName(), e); } + //long png2swfEnd = System.currentTimeMillis(); //log.debug("SwfTools conversion duration: {} sec", (png2swfEnd - png2swfStart) / 1000); diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PdfPageDownscaler.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PdfPageDownscaler.java new file mode 100755 index 0000000000000000000000000000000000000000..fa5c95b12e2910709e5234618ad66db53f5b7735 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PdfPageDownscaler.java @@ -0,0 +1,18 @@ +package org.bigbluebutton.presentation.imp; + +import java.io.File; + +public class PdfPageDownscaler { + private static final String SPACE = " "; + + public boolean downscale(File source,File dest) { + String COMMAND = "gs -sDEVICE=pdfwrite -dNOPAUSE -dQUIET -dBATCH -dFirstPage=1 -dLastPage=1 -sOutputFile=" + + dest.getAbsolutePath() + SPACE + + "/etc/bigbluebutton/nopdfmark.ps" + SPACE + + source.getAbsolutePath(); + + //System.out.println("DOWNSCALING " + COMMAND); + + return new ExternalProcessExecutor().exec(COMMAND, 10000); + } +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PdfToSwfSlidesGenerationService.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PdfToSwfSlidesGenerationService.java index a54c27b14cc61524b45ce32067964eb4c707d219..4453d0d433d39f3088121b1afad50e5df1127812 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PdfToSwfSlidesGenerationService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PdfToSwfSlidesGenerationService.java @@ -19,445 +19,43 @@ package org.bigbluebutton.presentation.imp; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.Callable; -import java.util.concurrent.CompletionService; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorCompletionService; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import org.bigbluebutton.presentation.*; -import org.bigbluebutton.presentation.ConversionUpdateMessage.MessageBuilder; -import org.bigbluebutton.presentation.messages.DocPageCountExceeded; -import org.bigbluebutton.presentation.messages.DocPageCountFailed; -import org.bigbluebutton.presentation.messages.PdfConversionInvalid; +import java.util.ArrayList; +import java.util.concurrent.*; +import org.bigbluebutton.presentation.messages.PageConvertProgressMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.Gson; - public class PdfToSwfSlidesGenerationService { private static Logger log = LoggerFactory.getLogger(PdfToSwfSlidesGenerationService.class); - private SwfSlidesGenerationProgressNotifier notifier; - private PageCounterService counterService; - private PageConverter pdfToSwfConverter; private ExecutorService executor; - private ThumbnailCreator thumbnailCreator; - private PngCreator pngCreator; - private TextFileCreator textFileCreator; - private SvgImageCreator svgImageCreator; - private long bigPdfSize; - private long maxBigPdfPageSize; - private PageExtractor pageExtractor; - private long MAX_CONVERSION_TIME = 5 * 60 * 1000L * 1000L * 1000L; - private String BLANK_SLIDE; - private int MAX_SWF_FILE_SIZE; - private boolean swfSlidesRequired; - private boolean svgImagesRequired; - private boolean generatePngs; + private BlockingQueue<PageToConvert> messages = new LinkedBlockingQueue<PageToConvert>(); + + private PresentationConversionCompletionService presentationConversionCompletionService; public PdfToSwfSlidesGenerationService(int numConversionThreads) { executor = Executors.newFixedThreadPool(numConversionThreads); } - public void generateSlides(UploadedPresentation pres) { - determineNumberOfPages(pres); - if (pres.getNumberOfPages() > 0) { - if (pres.getUploadedFile().length() > bigPdfSize) { - try { - hasBigPage(pres); - } catch (BigPdfException e) { - sendFailedToConvertBigPdfMessage(e, pres); - return; - } - } - - // Only create SWF files if the configuration requires it - if (swfSlidesRequired) { - convertPdfToSwf(pres); - } - - /* adding accessibility */ - createThumbnails(pres); - createTextFiles(pres); - - // only create SVG images if the configuration requires it - if (svgImagesRequired) { - createSvgImages(pres); - } - - // only create PNG images if the configuration requires it - if (generatePngs) { - createPngImages(pres); - } - - notifier.sendConversionCompletedMessage(pres); - } - } - - private boolean determineNumberOfPages(UploadedPresentation pres) { - try { - counterService.determineNumberOfPages(pres); - return true; - } catch (CountingPageException e) { - sendFailedToCountPageMessage(e, pres); - } - return false; - } - - private boolean hasBigPage(UploadedPresentation pres) throws BigPdfException { - long lastPageSize = 0; - int currentPage = 1; - String basePresentationame = UUID.randomUUID().toString(); - if (pres.getNumberOfPages() > 1) { - while(currentPage < pres.getNumberOfPages()) { - File tempPage; - try { - tempPage = File.createTempFile(basePresentationame + "-" + currentPage, ".pdf"); - pageExtractor.extractPage(pres.getUploadedFile(), tempPage, currentPage); - lastPageSize = tempPage.length(); - // Delete the temporary file - tempPage.delete(); - } catch (IOException e) { - e.printStackTrace(); - } - - if (lastPageSize > maxBigPdfPageSize) { - throw new BigPdfException(BigPdfException.ExceptionType.PDF_HAS_BIG_PAGE, currentPage, lastPageSize); - } - - lastPageSize = 0; - currentPage++; - } - } else { - if ((int)pres.getUploadedFile().length() > bigPdfSize) { - throw new BigPdfException(BigPdfException.ExceptionType.PDF_HAS_BIG_PAGE, 1, pres.getUploadedFile().length()); + public void process(PageToConvert pageToConvert) { + Runnable task = new Runnable() { + public void run() { + pageToConvert.convert(); + PageConvertProgressMessage msg = new PageConvertProgressMessage( + pageToConvert.getPageNumber(), + pageToConvert.getPresId(), + new ArrayList<>()); + presentationConversionCompletionService.handle(msg); + pageToConvert.getPageFile().delete(); } - } + }; - - return false; + executor.execute(task); } - private void sendFailedToCountPageMessage(CountingPageException e, UploadedPresentation pres) { - MessageBuilder builder = new ConversionUpdateMessage.MessageBuilder(pres); - - if (e.getExceptionType() == CountingPageException.ExceptionType.PAGE_COUNT_EXCEPTION) { - builder.messageKey(ConversionMessageConstants.PAGE_COUNT_FAILED_KEY); - - Map<String, Object> logData = new HashMap<>(); - logData.put("podId", pres.getPodId()); - logData.put("meetingId", pres.getMeetingId()); - logData.put("presId", pres.getId()); - logData.put("filename", pres.getName()); - logData.put("logCode", "determine_num_pages_failed"); - logData.put("message", "Failed to determine number of pages."); - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - log.error(" --analytics-- data={}", logStr, e); - - DocPageCountFailed progress = new DocPageCountFailed(pres.getPodId(), pres.getMeetingId(), - pres.getId(), pres.getId(), - pres.getName(), "notUsedYet", "notUsedYet", - pres.isDownloadable(), ConversionMessageConstants.PAGE_COUNT_FAILED_KEY); - - notifier.sendDocConversionProgress(progress); - - } else if (e.getExceptionType() == CountingPageException.ExceptionType.PAGE_EXCEEDED_EXCEPTION) { - builder.numberOfPages(e.getPageCount()); - builder.maxNumberPages(e.getMaxNumberOfPages()); - builder.messageKey(ConversionMessageConstants.PAGE_COUNT_EXCEEDED_KEY); - - Map<String, Object> logData = new HashMap<String, Object>(); - logData.put("podId", pres.getPodId()); - logData.put("meetingId", pres.getMeetingId()); - logData.put("presId", pres.getId()); - logData.put("filename", pres.getName()); - logData.put("pageCount", e.getPageCount()); - logData.put("maxNumPages", e.getMaxNumberOfPages()); - logData.put("logCode", "num_pages_exceeded"); - logData.put("message", "Number of pages exceeded."); - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - log.warn(" --analytics-- data={}", logStr); - - DocPageCountExceeded progress = new DocPageCountExceeded(pres.getPodId(), pres.getMeetingId(), - pres.getId(), pres.getId(), - pres.getName(), "notUsedYet", "notUsedYet", - pres.isDownloadable(), ConversionMessageConstants.PAGE_COUNT_EXCEEDED_KEY, - e.getPageCount(), e.getMaxNumberOfPages()); - - notifier.sendDocConversionProgress(progress); - } - + public void setPresentationConversionCompletionService(PresentationConversionCompletionService s) { + this.presentationConversionCompletionService = s; } - - private void sendFailedToConvertBigPdfMessage(BigPdfException e, UploadedPresentation pres) { - MessageBuilder builder = new ConversionUpdateMessage.MessageBuilder(pres); - - builder.messageKey(ConversionMessageConstants.PDF_HAS_BIG_PAGE); - - Map<String, Object> logData = new HashMap<>(); - logData.put("podId", pres.getPodId()); - logData.put("meetingId", pres.getMeetingId()); - logData.put("presId", pres.getId()); - logData.put("filename", pres.getName()); - logData.put("pdfSize", pres.getUploadedFile().length()); - logData.put("bigPageNumber", e.getBigPageNumber()); - logData.put("bigPageSize", e.getBigPageSize()); - logData.put("logCode", "big_pdf_has_a_big_page"); - logData.put("message", "The PDF contains a big page."); - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - log.error(" --analytics-- data={}", logStr, e); - - PdfConversionInvalid progress = new PdfConversionInvalid(pres.getPodId(), pres.getMeetingId(), - pres.getId(), pres.getId(), - pres.getName(), "notUsedYet", "notUsedYet", - pres.isDownloadable(), e.getBigPageNumber(), (int)e.getBigPageSize(), - ConversionMessageConstants.PDF_HAS_BIG_PAGE); - - notifier.sendDocConversionProgress(progress); - } - - private void createThumbnails(UploadedPresentation pres) { - notifier.sendCreatingThumbnailsUpdateMessage(pres); - thumbnailCreator.createThumbnails(pres); - } - - private void createTextFiles(UploadedPresentation pres) { - notifier.sendCreatingTextFilesUpdateMessage(pres); - textFileCreator.createTextFiles(pres); - } - - private void createSvgImages(UploadedPresentation pres) { - notifier.sendCreatingSvgImagesUpdateMessage(pres); - svgImageCreator.createSvgImages(pres); - } - - private void createPngImages(UploadedPresentation pres) { - pngCreator.createPng(pres); - } - - private void convertPdfToSwf(UploadedPresentation pres) { - int numPages = pres.getNumberOfPages(); - List<PdfToSwfSlide> slides = setupSlides(pres, numPages); - - CompletionService<PdfToSwfSlide> completionService = new ExecutorCompletionService<PdfToSwfSlide>( - executor); - - generateSlides(pres, slides, completionService); - } - - private void generateSlides(UploadedPresentation pres, - List<PdfToSwfSlide> slides, - CompletionService<PdfToSwfSlide> completionService) { - int slidesCompleted = 0; - - long presConvStart = System.currentTimeMillis(); - - for (final PdfToSwfSlide slide : slides) { - long pageConvStart = System.currentTimeMillis(); - - Callable<PdfToSwfSlide> c = new Callable<PdfToSwfSlide>() { - public PdfToSwfSlide call() { - return slide.createSlide(); - } - }; - - Future<PdfToSwfSlide> f = executor.submit(c); - long endNanos = System.nanoTime() + MAX_CONVERSION_TIME; - try { - // Only wait for the remaining time budget - long timeLeft = endNanos - System.nanoTime(); - PdfToSwfSlide s = f.get(timeLeft, TimeUnit.NANOSECONDS); - slidesCompleted++; - notifier.sendConversionUpdateMessage(slidesCompleted, pres); - } catch (ExecutionException e) { - Map<String, Object> logData = new HashMap<>(); - logData.put("podId", pres.getPodId()); - logData.put("meetingId", pres.getMeetingId()); - logData.put("presId", pres.getId()); - logData.put("filename", pres.getName()); - logData.put("page", slide.getPageNumber()); - logData.put("logCode", "page_conversion_failed"); - logData.put("message", "ExecutionException while converting page."); - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - log.error(" --analytics-- data={}", logStr, e); - } catch (InterruptedException e) { - Map<String, Object> logData = new HashMap<>(); - logData.put("podId", pres.getPodId()); - logData.put("meetingId", pres.getMeetingId()); - logData.put("presId", pres.getId()); - logData.put("filename", pres.getName()); - logData.put("page", slide.getPageNumber()); - logData.put("logCode", "page_conversion_failed"); - logData.put("message", "InterruptedException while converting page"); - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - log.error(" --analytics-- data={}", logStr, e); - - Thread.currentThread().interrupt(); - } catch (TimeoutException e) { - Map<String, Object> logData = new HashMap<>(); - logData.put("podId", pres.getPodId()); - logData.put("meetingId", pres.getMeetingId()); - logData.put("presId", pres.getId()); - logData.put("filename", pres.getName()); - logData.put("page", slide.getPageNumber()); - logData.put("logCode", "page_conversion_failed"); - logData.put("message", "TimeoutException while converting page"); - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - log.error(" --analytics-- data={}", logStr, e); - - f.cancel(true); - } - - long pageConvEnd = System.currentTimeMillis(); - Map<String, Object> logData = new HashMap<>(); - logData.put("podId", pres.getPodId()); - logData.put("meetingId", pres.getMeetingId()); - logData.put("presId", pres.getId()); - logData.put("filename", pres.getName()); - logData.put("page", slide.getPageNumber()); - logData.put("conversionTime(sec)", (pageConvEnd - pageConvStart) / 1000); - logData.put("logCode", "page_conversion_duration"); - logData.put("message", "Page conversion duration(sec)"); - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - log.info(" --analytics-- data={}", logStr); - - } - - for (final PdfToSwfSlide slide : slides) { - if (!slide.isDone()) { - - slide.generateBlankSlide(); - - Map<String, Object> logData = new HashMap<>(); - logData.put("podId", pres.getPodId()); - logData.put("meetingId", pres.getMeetingId()); - logData.put("presId", pres.getId()); - logData.put("filename", pres.getName()); - logData.put("page", slide.getPageNumber()); - logData.put("logCode", "create_blank_slide"); - logData.put("message", "Creating blank slide"); - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - log.warn(" --analytics-- data={}", logStr); - - notifier.sendConversionUpdateMessage(slidesCompleted++, pres); - } - } - - long presConvEnd = System.currentTimeMillis(); - Map<String, Object> logData = new HashMap<>(); - logData.put("podId", pres.getPodId()); - logData.put("meetingId", pres.getMeetingId()); - logData.put("presId", pres.getId()); - logData.put("filename", pres.getName()); - logData.put("conversionTime(sec)", (presConvEnd - presConvStart) / 1000); - logData.put("logCode", "presentation_conversion_duration"); - logData.put("message", "Presentation conversion duration (sec)"); - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - log.info(" --analytics-- data={}", logStr); - - } - - private List<PdfToSwfSlide> setupSlides(UploadedPresentation pres, - int numPages) { - List<PdfToSwfSlide> slides = new ArrayList<>(numPages); - - for (int page = 1; page <= numPages; page++) { - PdfToSwfSlide slide = new PdfToSwfSlide(pres, page); - slide.setBlankSlide(BLANK_SLIDE); - slide.setMaxSwfFileSize(MAX_SWF_FILE_SIZE); - slide.setPageConverter(pdfToSwfConverter); - - slides.add(slide); - } - - return slides; - } - - public void setCounterService(PageCounterService counterService) { - this.counterService = counterService; - } - - public void setPageConverter(PageConverter converter) { - this.pdfToSwfConverter = converter; - } - - public void setBlankSlide(String blankSlide) { - this.BLANK_SLIDE = blankSlide; - } - - public void setMaxSwfFileSize(int size) { - this.MAX_SWF_FILE_SIZE = size; - } - - public void setGeneratePngs(boolean generatePngs) { - this.generatePngs = generatePngs; - } - - public void setSwfSlidesRequired(boolean swfSlidesRequired) { - this.swfSlidesRequired = swfSlidesRequired; - } - - public void setBigPdfSize(long bigPdfSize) { - this.bigPdfSize = bigPdfSize; - } - - public void setMaxBigPdfPageSize(long maxBigPdfPageSize) { - this.maxBigPdfPageSize = maxBigPdfPageSize; - } - - public void setPageExtractor(PageExtractor extractor) { - this.pageExtractor = extractor; - } - - public void setSvgImagesRequired(boolean svgImagesRequired) { - this.svgImagesRequired = svgImagesRequired; - } - - public void setThumbnailCreator(ThumbnailCreator thumbnailCreator) { - this.thumbnailCreator = thumbnailCreator; - } - - public void setPngCreator(PngCreator pngCreator) { - this.pngCreator = pngCreator; - } - - public void setTextFileCreator(TextFileCreator textFileCreator) { - this.textFileCreator = textFileCreator; - } - - public void setSvgImageCreator(SvgImageCreator svgImageCreator) { - this.svgImageCreator = svgImageCreator; - } - - public void setMaxConversionTime(int minutes) { - MAX_CONVERSION_TIME = minutes * 60 * 1000L * 1000L * 1000L; - } - - public void setSwfSlidesGenerationProgressNotifier( - SwfSlidesGenerationProgressNotifier notifier) { - this.notifier = notifier; - } - } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/Png2SwfPageConverter.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/Png2SwfPageConverter.java index 2a0fa04760b40d05ecfc42dbbf8981157b141e1d..d7324368befee5a149c68806b26b16e8cf3414f7 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/Png2SwfPageConverter.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/Png2SwfPageConverter.java @@ -38,7 +38,7 @@ public class Png2SwfPageConverter implements PageConverter { public boolean convert(File presentationFile, File output, int page, UploadedPresentation pres){ String COMMAND = SWFTOOLS_DIR + File.separatorChar + "png2swf -o " + output.getAbsolutePath() + " " + presentationFile.getAbsolutePath(); - boolean done = new ExternalProcessExecutor().exec(COMMAND, 60000); + boolean done = new ExternalProcessExecutor().exec(COMMAND, 10000); if (done && output.exists()) { return true; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PngCreatorImp.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PngCreatorImp.java index a0bcd56f169d7ddd5f3a2f32f152aa307f1bd388..cb2c54f76cd038cd07a4414f74836559738c6196 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PngCreatorImp.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PngCreatorImp.java @@ -19,62 +19,104 @@ package org.bigbluebutton.presentation.imp; import com.google.gson.Gson; +import com.zaxxer.nuprocess.NuProcess; +import com.zaxxer.nuprocess.NuProcessBuilder; import org.apache.commons.io.FileUtils; import org.bigbluebutton.presentation.PngCreator; +import org.bigbluebutton.presentation.SupportedFileTypes; import org.bigbluebutton.presentation.UploadedPresentation; +import org.bigbluebutton.presentation.handlers.Png2SvgConversionHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; public class PngCreatorImp implements PngCreator { private static Logger log = LoggerFactory.getLogger(PngCreatorImp.class); - private static final Pattern PAGE_NUMBER_PATTERN = Pattern.compile("(.+-png)-([0-9]+)(.png)"); + private static final Pattern PAGE_NUMBER_PATTERN = Pattern.compile("(.+-png)-([0-9]+)-([0-9]+)(.png)"); private String BLANK_PNG; private int slideWidth = 800; + private String convTimeout = "7s"; + private int WAIT_FOR_SEC = 7; private static final String TEMP_PNG_NAME = "temp-png"; - public boolean createPng(UploadedPresentation pres) { + public boolean createPng(UploadedPresentation pres, int page, File pageFile) { boolean success = false; File pngDir = determinePngDirectory(pres.getUploadedFile()); if (!pngDir.exists()) pngDir.mkdir(); - cleanDirectory(pngDir); - try { - success = generatePngs(pngDir, pres); + long start = System.currentTimeMillis(); + success = generatePng(pngDir, pres, page, pageFile); + long end = System.currentTimeMillis(); + //System.out.println("*** GENERATE PNG " + (end - start)); } catch (InterruptedException e) { log.warn("Interrupted Exception while generating png."); success = false; } + long start = System.currentTimeMillis(); + renamePng(pngDir, page); // Create blank thumbnails for pages that failed to generate a thumbnail. - createBlankPngs(pngDir, pres.getNumberOfPages()); + createBlankPng(pngDir, page); + long end = System.currentTimeMillis(); + //System.out.println("*** GENERATE BLANK PNG " + (end - start)); - renamePng(pngDir); + //start = System.currentTimeMillis(); + //renamePng(pngDir); + //end = System.currentTimeMillis(); + //System.out.println("*** RENAME PNG " + (end - start)); return success; } - private boolean generatePngs(File pngsDir, UploadedPresentation pres) + private boolean generatePng(File pngsDir, UploadedPresentation pres, int page, File pageFile) throws InterruptedException { - String source = pres.getUploadedFile().getAbsolutePath(); + String source = pageFile.getAbsolutePath(); String dest; + + if (SupportedFileTypes.isImageFile(pres.getFileType())) { + // Need to create a PDF as intermediate step. + // Convert single image file + dest = pngsDir.getAbsolutePath() + File.separator + "slide-1.pdf"; + + NuProcessBuilder convertImgToSvg = new NuProcessBuilder( + Arrays.asList("timeout", convTimeout, "convert", source, "-auto-orient", dest)); + + Png2SvgConversionHandler pHandler = new Png2SvgConversionHandler(); + convertImgToSvg.setProcessListener(pHandler); + + NuProcess process = convertImgToSvg.start(); + try { + process.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.error("InterruptedException while converting to PDF {}", dest, e); + return false; + } + + // Use the intermediate PDF file as source + source = dest; + } + String COMMAND = ""; - dest = pngsDir.getAbsolutePath() + File.separator + TEMP_PNG_NAME; + dest = pngsDir.getAbsolutePath() + File.separator + TEMP_PNG_NAME + "-" + page; // the "-x.png" is appended automagically COMMAND = "pdftocairo -png -scale-to " + slideWidth + " " + source + " " + dest; - boolean done = new ExternalProcessExecutor().exec(COMMAND, 60000); + //System.out.println("********* CREATING PNGs " + COMMAND); + + boolean done = new ExternalProcessExecutor().exec(COMMAND, 10000); if (done) { return true; @@ -98,7 +140,7 @@ public class PngCreatorImp implements PngCreator { return new File(presentationFile.getParent() + File.separatorChar + "pngs"); } - private void renamePng(File dir) { + private void renamePng(File dir, int page) { /* * If more than 1 file, filename like 'temp-png-X.png' else filename is * 'temp-png.png' @@ -107,6 +149,9 @@ public class PngCreatorImp implements PngCreator { File[] files = dir.listFiles(); Matcher matcher; for (int i = 0; i < files.length; i++) { + + //System.out.println("*** PPNG file " + files[i].getAbsolutePath()); + matcher = PAGE_NUMBER_PATTERN.matcher(files[i].getAbsolutePath()); if (matcher.matches()) { // Path should be something like @@ -118,15 +163,19 @@ public class PngCreatorImp implements PngCreator { // 3. .png // We are interested in the second match. int pageNum = Integer.parseInt(matcher.group(2).trim()); - String newFilename = "slide-" + (pageNum) + ".png"; - File renamedFile = new File( - dir.getAbsolutePath() + File.separator + newFilename); - files[i].renameTo(renamedFile); + if (pageNum == page) { + String newFilename = "slide-" + (page) + ".png"; + File renamedFile = new File( + dir.getAbsolutePath() + File.separator + newFilename); + files[i].renameTo(renamedFile); + } + } } } else if (dir.list().length == 1) { File oldFilename = new File( dir.getAbsolutePath() + File.separator + dir.list()[0]); + //System.out.println("*** PPNG file " + oldFilename.getAbsolutePath()); String newFilename = "slide-1.png"; File renamedFile = new File( oldFilename.getParent() + File.separator + newFilename); @@ -134,17 +183,11 @@ public class PngCreatorImp implements PngCreator { } } - private void createBlankPngs(File pngsDir, int pageCount) { - File[] pngs = pngsDir.listFiles(); - - if (pngs.length != pageCount) { - for (int i = 0; i < pageCount; i++) { - File png = new File(pngsDir.getAbsolutePath() + File.separator + TEMP_PNG_NAME + "-" + i + ".png"); - if (!png.exists()) { - log.info("Copying blank png for slide {}", i); - copyBlankPng(png); - } - } + private void createBlankPng(File pngsDir, int page) { + File png = new File(pngsDir.getAbsolutePath() + File.separator + "slide-" + page + ".png"); + if (!png.exists()) { + log.info("Copying blank png for slide {}", page); + copyBlankPng(png); } } @@ -152,7 +195,7 @@ public class PngCreatorImp implements PngCreator { try { FileUtils.copyFile(new File(BLANK_PNG), png); } catch (IOException e) { - log.error("IOException while copying blank thumbnail."); + log.error("IOException while copying blank PNG."); } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PresentationConversionCompletionService.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PresentationConversionCompletionService.java new file mode 100755 index 0000000000000000000000000000000000000000..ce6cd45eae5494f75a03c17ba4c40b1d8745c6be --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PresentationConversionCompletionService.java @@ -0,0 +1,104 @@ +package org.bigbluebutton.presentation.imp; + +import com.google.gson.Gson; +import org.bigbluebutton.presentation.messages.IPresentationCompletionMessage; +import org.bigbluebutton.presentation.messages.PageConvertProgressMessage; +import org.bigbluebutton.presentation.messages.PresentationConvertMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.*; + +public class PresentationConversionCompletionService { + private static Logger log = LoggerFactory.getLogger(PresentationConversionCompletionService.class); + + private SwfSlidesGenerationProgressNotifier notifier; + + private ExecutorService executor; + private volatile boolean processProgress = false; + + private final ConcurrentMap<String, PresentationToConvert> presentationsToConvert + = new ConcurrentHashMap<String, PresentationToConvert>(); + + private BlockingQueue<IPresentationCompletionMessage> messages = new LinkedBlockingQueue<IPresentationCompletionMessage>(); + + public PresentationConversionCompletionService() { + executor = Executors.newSingleThreadExecutor(); + } + + public void handle(IPresentationCompletionMessage msg) { + messages.offer(msg); + } + + private void processMessage(IPresentationCompletionMessage msg) { + if (msg instanceof PresentationConvertMessage) { + PresentationConvertMessage m = (PresentationConvertMessage) msg; + PresentationToConvert p = new PresentationToConvert(m.pres); + presentationsToConvert.put(p.getKey(), p); + } else if (msg instanceof PageConvertProgressMessage) { + + PageConvertProgressMessage m = (PageConvertProgressMessage) msg; + PresentationToConvert p = presentationsToConvert.get(m.presId); + if (p != null) { + p.incrementPagesCompleted(); + notifier.sendConversionUpdateMessage(p.getPagesCompleted(), p.pres, m.page); + if (p.getPagesCompleted() == p.pres.getNumberOfPages()) { + handleEndProcessing(p); + } + } + } + } + + private void handleEndProcessing(PresentationToConvert p) { + presentationsToConvert.remove(p.getKey()); + + Map<String, Object> logData = new HashMap<String, Object>(); + logData = new HashMap<String, Object>(); + logData.put("podId", p.pres.getPodId()); + logData.put("meetingId", p.pres.getMeetingId()); + logData.put("presId", p.pres.getId()); + logData.put("filename", p.pres.getName()); + logData.put("current", p.pres.isCurrent()); + logData.put("logCode", "presentation_conversion_end"); + logData.put("message", "End presentation conversion."); + + Gson gson = new Gson(); + String logStr = gson.toJson(logData); + log.info(" --analytics-- data={}", logStr); + + notifier.sendConversionCompletedMessage(p.pres); + } + public void start() { + log.info("Ready to process presentation files!"); + + try { + processProgress = true; + + Runnable messageProcessor = new Runnable() { + public void run() { + while (processProgress) { + try { + IPresentationCompletionMessage msg = messages.take(); + processMessage(msg); + } catch (InterruptedException e) { + log.warn("Error while taking presentation file from queue."); + } + } + } + }; + executor.submit(messageProcessor); + } catch (Exception e) { + log.error("Error processing presentation file: {}", e); + } + } + + public void stop() { + processProgress = false; + } + + public void setSwfSlidesGenerationProgressNotifier(SwfSlidesGenerationProgressNotifier notifier) { + this.notifier = notifier; + } +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PresentationFileProcessor.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PresentationFileProcessor.java new file mode 100755 index 0000000000000000000000000000000000000000..6a509c2bb54a7b9413d5afaf169ccaff0824de15 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PresentationFileProcessor.java @@ -0,0 +1,328 @@ +package org.bigbluebutton.presentation.imp; + +import com.google.gson.Gson; +import org.bigbluebutton.presentation.*; +import org.bigbluebutton.presentation.messages.DocPageConversionStarted; +import org.bigbluebutton.presentation.messages.DocPageCountExceeded; +import org.bigbluebutton.presentation.messages.DocPageCountFailed; +import org.bigbluebutton.presentation.messages.PresentationConvertMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; + +public class PresentationFileProcessor { + private static Logger log = LoggerFactory.getLogger(PresentationFileProcessor.class); + + private boolean swfSlidesRequired; + private boolean svgImagesRequired; + private boolean generatePngs; + private PageExtractor pageExtractor; + + private String BLANK_SLIDE; + private int MAX_SWF_FILE_SIZE; + private long bigPdfSize; + private long maxBigPdfPageSize; + + private long MAX_CONVERSION_TIME = 5 * 60 * 1000L; + + private TextFileCreator textFileCreator; + private SvgImageCreator svgImageCreator; + private ThumbnailCreator thumbnailCreator; + private PngCreator pngCreator; + private PageConverter pdfToSwfConverter; + private SwfSlidesGenerationProgressNotifier notifier; + private PageCounterService counterService; + private PresentationConversionCompletionService presentationConversionCompletionService; + private ImageToSwfSlidesGenerationService imageToSwfSlidesGenerationService; + private PdfToSwfSlidesGenerationService pdfToSwfSlidesGenerationService; + + private ExecutorService executor; + private volatile boolean processPresentation = false; + + private BlockingQueue<UploadedPresentation> presentations = new LinkedBlockingQueue<UploadedPresentation>(); + + public PresentationFileProcessor(int numConversionThreads) { + executor = Executors.newFixedThreadPool(numConversionThreads); + } + + public synchronized void process(UploadedPresentation pres) { + Runnable messageProcessor = new Runnable() { + public void run() { + processUploadedPresentation(pres); + } + }; + executor.submit(messageProcessor); + } + + private void processUploadedPresentation(UploadedPresentation pres) { + if (SupportedFileTypes.isPdfFile(pres.getFileType())) { + determineNumberOfPages(pres); + sendDocPageConversionStartedProgress(pres); + PresentationConvertMessage msg = new PresentationConvertMessage(pres); + presentationConversionCompletionService.handle(msg); + extractIntoPages(pres); + } else if (SupportedFileTypes.isImageFile(pres.getFileType())) { + pres.setNumberOfPages(1); // There should be only one image to convert. + sendDocPageConversionStartedProgress(pres); + imageToSwfSlidesGenerationService.generateSlides(pres); + } + } + + private void extractIntoPages(UploadedPresentation pres) { + for (int page = 1; page <= pres.getNumberOfPages(); page++) { + String presDir = pres.getUploadedFile().getParent(); + File pageFile = new File(presDir + "/page" + "-" + page + ".pdf"); + + File extractedPageFile = extractPage(pres, page); + + if (extractedPageFile.length() > maxBigPdfPageSize) { + File downscaledPageFile = downscalePage(pres, extractedPageFile, page); + downscaledPageFile.renameTo(pageFile); + extractedPageFile.delete(); + } else { + extractedPageFile.renameTo(pageFile); + } + + PageToConvert pageToConvert = new PageToConvert( + pres, + page, + pageFile, + swfSlidesRequired, + svgImagesRequired, + generatePngs, + textFileCreator, + svgImageCreator, + thumbnailCreator, + pngCreator, + pdfToSwfConverter, + notifier, + BLANK_SLIDE, + MAX_SWF_FILE_SIZE + ); + + pdfToSwfSlidesGenerationService.process(pageToConvert); + } + } + + private File downscalePage(UploadedPresentation pres, File filePage, int pageNum) { + String presDir = pres.getUploadedFile().getParent(); + File tempPage = new File(presDir + "/downscaled" + "-" + pageNum + ".pdf"); + PdfPageDownscaler downscaler = new PdfPageDownscaler(); + downscaler.downscale(filePage, tempPage); + if (tempPage.exists()) { + return tempPage; + } + + return filePage; + } + + private File extractPage(UploadedPresentation pres, int page) { + String presDir = pres.getUploadedFile().getParent(); + + File tempPage = new File(presDir + "/extracted" + "-" + page + ".pdf"); + pageExtractor.extractPage(pres.getUploadedFile(), tempPage, page); + + return tempPage; + } + + private boolean determineNumberOfPages(UploadedPresentation pres) { + try { + counterService.determineNumberOfPages(pres); + return true; + } catch (CountingPageException e) { + sendFailedToCountPageMessage(e, pres); + } + return false; + } + + private void sendDocPageConversionStartedProgress(UploadedPresentation pres) { + Map<String, Object> logData = new HashMap<String, Object>(); + + logData.put("podId", pres.getPodId()); + logData.put("meetingId", pres.getMeetingId()); + logData.put("presId", pres.getId()); + logData.put("filename", pres.getName()); + logData.put("num_pages", pres.getNumberOfPages()); + logData.put("authzToken", pres.getAuthzToken()); + logData.put("logCode", "presentation_conversion_num_pages"); + logData.put("message", "Presentation conversion number of pages."); + + Gson gson = new Gson(); + String logStr = gson.toJson(logData); + log.info(" --analytics-- data={}", logStr); + + DocPageConversionStarted progress = new DocPageConversionStarted( + pres.getPodId(), + pres.getMeetingId(), + pres.getId(), + pres.getName(), + pres.getAuthzToken(), + pres.isDownloadable(), + pres.isCurrent(), + pres.getNumberOfPages()); + notifier.sendDocConversionProgress(progress); + } + + private void sendFailedToCountPageMessage(CountingPageException e, UploadedPresentation pres) { + ConversionUpdateMessage.MessageBuilder builder = new ConversionUpdateMessage.MessageBuilder(pres); + + if (e.getExceptionType() == CountingPageException.ExceptionType.PAGE_COUNT_EXCEPTION) { + builder.messageKey(ConversionMessageConstants.PAGE_COUNT_FAILED_KEY); + + Map<String, Object> logData = new HashMap<>(); + logData.put("podId", pres.getPodId()); + logData.put("meetingId", pres.getMeetingId()); + logData.put("presId", pres.getId()); + logData.put("filename", pres.getName()); + logData.put("logCode", "determine_num_pages_failed"); + logData.put("message", "Failed to determine number of pages."); + Gson gson = new Gson(); + String logStr = gson.toJson(logData); + log.error(" --analytics-- data={}", logStr, e); + + DocPageCountFailed progress = new DocPageCountFailed(pres.getPodId(), pres.getMeetingId(), + pres.getId(), pres.getId(), + pres.getName(), "notUsedYet", "notUsedYet", + pres.isDownloadable(), ConversionMessageConstants.PAGE_COUNT_FAILED_KEY); + + notifier.sendDocConversionProgress(progress); + + } else if (e.getExceptionType() == CountingPageException.ExceptionType.PAGE_EXCEEDED_EXCEPTION) { + builder.numberOfPages(e.getPageCount()); + builder.maxNumberPages(e.getMaxNumberOfPages()); + builder.messageKey(ConversionMessageConstants.PAGE_COUNT_EXCEEDED_KEY); + + Map<String, Object> logData = new HashMap<String, Object>(); + logData.put("podId", pres.getPodId()); + logData.put("meetingId", pres.getMeetingId()); + logData.put("presId", pres.getId()); + logData.put("filename", pres.getName()); + logData.put("pageCount", e.getPageCount()); + logData.put("maxNumPages", e.getMaxNumberOfPages()); + logData.put("logCode", "num_pages_exceeded"); + logData.put("message", "Number of pages exceeded."); + Gson gson = new Gson(); + String logStr = gson.toJson(logData); + log.warn(" --analytics-- data={}", logStr); + + DocPageCountExceeded progress = new DocPageCountExceeded(pres.getPodId(), pres.getMeetingId(), + pres.getId(), pres.getId(), + pres.getName(), "notUsedYet", "notUsedYet", + pres.isDownloadable(), ConversionMessageConstants.PAGE_COUNT_EXCEEDED_KEY, + e.getPageCount(), e.getMaxNumberOfPages()); + + notifier.sendDocConversionProgress(progress); + } + } + + public void start() { + log.info("Ready to process presentation files!"); + + try { + processPresentation = true; + + Runnable messageProcessor = new Runnable() { + public void run() { + while (processPresentation) { + try { + UploadedPresentation pres = presentations.take(); + processUploadedPresentation(pres); + } catch (InterruptedException e) { + log.warn("Error while taking presentation file from queue."); + } + } + } + }; + executor.submit(messageProcessor); + } catch (Exception e) { + log.error("Error processing presentation file: {}", e); + } + } + + public void stop() { + processPresentation = false; + } + + public void setSwfSlidesGenerationProgressNotifier(SwfSlidesGenerationProgressNotifier notifier) { + this.notifier = notifier; + } + + public void setCounterService(PageCounterService counterService) { + this.counterService = counterService; + } + + public void setPageExtractor(PageExtractor extractor) { + this.pageExtractor = extractor; + } + + public void setPageConverter(PageConverter converter) { + this.pdfToSwfConverter = converter; + } + + public void setBlankSlide(String blankSlide) { + this.BLANK_SLIDE = blankSlide; + } + + public void setMaxSwfFileSize(int size) { + this.MAX_SWF_FILE_SIZE = size; + } + + public void setGeneratePngs(boolean generatePngs) { + this.generatePngs = generatePngs; + } + + public void setSwfSlidesRequired(boolean swfSlidesRequired) { + this.swfSlidesRequired = swfSlidesRequired; + } + + public void setBigPdfSize(long bigPdfSize) { + this.bigPdfSize = bigPdfSize; + } + + public void setMaxBigPdfPageSize(long maxBigPdfPageSize) { + this.maxBigPdfPageSize = maxBigPdfPageSize; + } + + public void setSvgImagesRequired(boolean svgImagesRequired) { + this.svgImagesRequired = svgImagesRequired; + } + + public void setThumbnailCreator(ThumbnailCreator thumbnailCreator) { + this.thumbnailCreator = thumbnailCreator; + } + + public void setPngCreator(PngCreator pngCreator) { + this.pngCreator = pngCreator; + } + + public void setTextFileCreator(TextFileCreator textFileCreator) { + this.textFileCreator = textFileCreator; + } + + public void setSvgImageCreator(SvgImageCreator svgImageCreator) { + this.svgImageCreator = svgImageCreator; + } + + public void setMaxConversionTime(int minutes) { + MAX_CONVERSION_TIME = minutes * 60 * 1000L * 1000L * 1000L; + } + + public void setImageToSwfSlidesGenerationService(ImageToSwfSlidesGenerationService s) { + imageToSwfSlidesGenerationService = s; + } + + public void setPresentationConversionCompletionService(PresentationConversionCompletionService s) { + this.presentationConversionCompletionService = s; + } + + public void setPdfToSwfSlidesGenerationService(PdfToSwfSlidesGenerationService s) { + this.pdfToSwfSlidesGenerationService = s; + } +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PresentationToConvert.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PresentationToConvert.java new file mode 100755 index 0000000000000000000000000000000000000000..d4a8bcef7fdf14226b3207d68a96a94a8381b8e7 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/PresentationToConvert.java @@ -0,0 +1,24 @@ +package org.bigbluebutton.presentation.imp; + +import org.bigbluebutton.presentation.UploadedPresentation; + +public class PresentationToConvert { + public final UploadedPresentation pres; + private int pagesCompleted = 0; + + public PresentationToConvert(UploadedPresentation pres) { + this.pres = pres; + } + + public String getKey() { + return pres.getId(); + } + + public int getPagesCompleted() { + return pagesCompleted; + } + + public void incrementPagesCompleted() { + pagesCompleted++; + } +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SvgImageCreatorImp.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SvgImageCreatorImp.java index cafeb3835d1177c0c56559c1c1a789182a2070a8..b52a7c00f4de61803417af424ba9cd7f3fe4097d 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SvgImageCreatorImp.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SvgImageCreatorImp.java @@ -31,17 +31,17 @@ public class SvgImageCreatorImp implements SvgImageCreator { private long pathsThreshold; private String convTimeout = "7s"; private int WAIT_FOR_SEC = 7; + private String BLANK_SVG; @Override - public boolean createSvgImages(UploadedPresentation pres) { + public boolean createSvgImage(UploadedPresentation pres, int page) { boolean success = false; File svgImagesPresentationDir = determineSvgImagesDirectory(pres.getUploadedFile()); if (!svgImagesPresentationDir.exists()) svgImagesPresentationDir.mkdir(); try { - FileUtils.cleanDirectory(svgImagesPresentationDir); - success = generateSvgImages(svgImagesPresentationDir, pres); + success = generateSvgImage(svgImagesPresentationDir, pres, page); } catch (Exception e) { log.error("Interrupted Exception while generating images {}", pres.getName(), e); success = false; @@ -50,18 +50,18 @@ public class SvgImageCreatorImp implements SvgImageCreator { return success; } - private boolean generateSvgImages(File imagePresentationDir, UploadedPresentation pres) + private boolean generateSvgImage(File imagePresentationDir, UploadedPresentation pres, int page) throws InterruptedException { String source = pres.getUploadedFile().getAbsolutePath(); String dest; - int numSlides; + + int numSlides = 1; boolean done = false; - int slidesCompleted = 0; - + // Convert single image file if (SupportedFileTypes.isImageFile(pres.getFileType())) { - numSlides = 1; - dest = imagePresentationDir.getAbsolutePath() + File.separator + "slide1.pdf"; + + dest = imagePresentationDir.getAbsolutePath() + File.separator + "slide-1.pdf"; NuProcessBuilder convertImgToSvg = new NuProcessBuilder( Arrays.asList("timeout", convTimeout, "convert", source, "-auto-orient", dest)); @@ -78,134 +78,138 @@ public class SvgImageCreatorImp implements SvgImageCreator { log.error("InterruptedException while converting to SVG {}", dest, e); } - source = imagePresentationDir.getAbsolutePath() + File.separator + "slide1.pdf"; - } else { - numSlides = pres.getNumberOfPages(); + // Use the intermediate PDF file as source + source = dest; } + //System.out.println("******** CREATING SVG page "); + // Continue image processing - for (int i = 1; i <= numSlides; i++) { - File destsvg = new File(imagePresentationDir.getAbsolutePath() + File.separatorChar + "slide" + i + ".svg"); + long startConv = System.currentTimeMillis(); + + File destsvg = new File(imagePresentationDir.getAbsolutePath() + File.separatorChar + "slide" + page + ".svg"); - NuProcessBuilder convertPdfToSvg = createConversionProcess("-svg", i, source, destsvg.getAbsolutePath(), + NuProcessBuilder convertPdfToSvg = createConversionProcess("-svg", page, source, destsvg.getAbsolutePath(), true); - SvgConversionHandler pHandler = new SvgConversionHandler(); - convertPdfToSvg.setProcessListener(pHandler); + SvgConversionHandler pHandler = new SvgConversionHandler(); + convertPdfToSvg.setProcessListener(pHandler); - NuProcess process = convertPdfToSvg.start(); - try { - process.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS); - done = true; - } catch (InterruptedException e) { - log.error("Interrupted Exception while generating SVG slides {}", pres.getName(), e); - } - if (!done) { - break; - } + NuProcess process = convertPdfToSvg.start(); + try { + process.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS); + done = true; + } catch (InterruptedException e) { + log.error("Interrupted Exception while generating SVG slides {}", pres.getName(), e); + } - if (destsvg.length() == 0 || pHandler.numberOfImageTags() > imageTagThreshold - || pHandler.numberOfPaths() > pathsThreshold) { - // We need t delete the destination file as we are starting a - // new conversion process - if (destsvg.exists()) { - destsvg.delete(); - } + if (!done) { + return done; + } - done = false; + if (destsvg.length() == 0 || pHandler.numberOfImageTags() > imageTagThreshold + || pHandler.numberOfPaths() > pathsThreshold) { + // We need t delete the destination file as we are starting a + // new conversion process + if (destsvg.exists()) { + destsvg.delete(); + } - Map<String, Object> logData = new HashMap<String, Object>(); + done = false; + + Map<String, Object> logData = new HashMap<String, Object>(); + logData.put("meetingId", pres.getMeetingId()); + logData.put("presId", pres.getId()); + logData.put("filename", pres.getName()); + logData.put("page", page); + logData.put("convertSuccess", pHandler.isCommandSuccessful()); + logData.put("fileExists", destsvg.exists()); + logData.put("numberOfImages", pHandler.numberOfImageTags()); + logData.put("numberOfPaths", pHandler.numberOfPaths()); + logData.put("logCode", "potential_problem_with_svg"); + logData.put("message", "Potential problem with generated SVG"); + Gson gson = new Gson(); + String logStr = gson.toJson(logData); + + log.warn(" --analytics-- data={}", logStr); + + File tempPng = null; + String basePresentationame = UUID.randomUUID().toString(); + try { + tempPng = File.createTempFile(basePresentationame + "-" + page, ".png"); + } catch (IOException ioException) { + // We should never fall into this if the server is correctly + // configured + logData = new HashMap<String, Object>(); logData.put("meetingId", pres.getMeetingId()); logData.put("presId", pres.getId()); logData.put("filename", pres.getName()); - logData.put("page", i); - logData.put("convertSuccess", pHandler.isCommandSuccessful()); - logData.put("fileExists", destsvg.exists()); - logData.put("numberOfImages", pHandler.numberOfImageTags()); - logData.put("numberOfPaths", pHandler.numberOfPaths()); - logData.put("logCode", "potential_problem_with_svg"); - logData.put("message", "Potential problem with generated SVG"); - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - - log.warn(" --analytics-- data={}", logStr); - - File tempPng = null; - String basePresentationame = UUID.randomUUID().toString(); - try { - tempPng = File.createTempFile(basePresentationame + "-" + i, ".png"); - } catch (IOException ioException) { - // We should never fall into this if the server is correctly - // configured - logData = new HashMap<String, Object>(); - logData.put("meetingId", pres.getMeetingId()); - logData.put("presId", pres.getId()); - logData.put("filename", pres.getName()); - logData.put("logCode", "problem_with_creating_svg"); - logData.put("message", "Unable to create temporary files"); - gson = new Gson(); - logStr = gson.toJson(logData); - log.error(" --analytics-- data={}", logStr, ioException); - } - - // Step 1: Convert a PDF page to PNG using a raw pdftocairo - NuProcessBuilder convertPdfToPng = createConversionProcess("-png", i, source, + logData.put("logCode", "problem_with_creating_svg"); + logData.put("message", "Unable to create temporary files"); + gson = new Gson(); + logStr = gson.toJson(logData); + log.error(" --analytics-- data={}", logStr, ioException); + } + + // Step 1: Convert a PDF page to PNG using a raw pdftocairo + NuProcessBuilder convertPdfToPng = createConversionProcess("-png", page, source, tempPng.getAbsolutePath().substring(0, tempPng.getAbsolutePath().lastIndexOf('.')), false); - Pdf2PngPageConverterHandler pngHandler = new Pdf2PngPageConverterHandler(); - convertPdfToPng.setProcessListener(pngHandler); - NuProcess pngProcess = convertPdfToPng.start(); - try { - pngProcess.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS); - } catch (InterruptedException e) { - log.error("Interrupted Exception while generating PNG image {}", pres.getName(), e); - } - - // Step 2: Convert a PNG image to SVG - NuProcessBuilder convertPngToSvg = new NuProcessBuilder(Arrays.asList("timeout", convTimeout, "convert", + Pdf2PngPageConverterHandler pngHandler = new Pdf2PngPageConverterHandler(); + convertPdfToPng.setProcessListener(pngHandler); + NuProcess pngProcess = convertPdfToPng.start(); + try { + pngProcess.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.error("Interrupted Exception while generating PNG image {}", pres.getName(), e); + } + + // Step 2: Convert a PNG image to SVG + NuProcessBuilder convertPngToSvg = new NuProcessBuilder(Arrays.asList("timeout", convTimeout, "convert", tempPng.getAbsolutePath(), destsvg.getAbsolutePath())); - Png2SvgConversionHandler svgHandler = new Png2SvgConversionHandler(); - convertPngToSvg.setProcessListener(svgHandler); - NuProcess svgProcess = convertPngToSvg.start(); - try { - svgProcess.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS); - } catch (InterruptedException e) { - log.error("Interrupted Exception while generating SVG image {}", pres.getName(), e); - } + Png2SvgConversionHandler svgHandler = new Png2SvgConversionHandler(); + convertPngToSvg.setProcessListener(svgHandler); + NuProcess svgProcess = convertPngToSvg.start(); + try { + svgProcess.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.error("Interrupted Exception while generating SVG image {}", pres.getName(), e); + } - done = svgHandler.isCommandSuccessful(); + done = svgHandler.isCommandSuccessful(); - // Delete the temporary PNG after finishing the image conversion - tempPng.delete(); + // Delete the temporary PNG after finishing the image conversion + tempPng.delete(); - // Step 3: Add SVG namespace to the destionation file - // Check : https://phabricator.wikimedia.org/T43174 - NuProcessBuilder addNameSpaceToSVG = new NuProcessBuilder(Arrays.asList("timeout", convTimeout, + // Step 3: Add SVG namespace to the destionation file + // Check : https://phabricator.wikimedia.org/T43174 + NuProcessBuilder addNameSpaceToSVG = new NuProcessBuilder(Arrays.asList("timeout", convTimeout, "/bin/sh", "-c", "sed -i " + "'4s|>| xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.2\">|' " + destsvg.getAbsolutePath())); - AddNamespaceToSvgHandler namespaceHandler = new AddNamespaceToSvgHandler(); - addNameSpaceToSVG.setProcessListener(namespaceHandler); - NuProcess namespaceProcess = addNameSpaceToSVG.start(); - try { - namespaceProcess.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS); - } catch (InterruptedException e) { - log.error("Interrupted Exception while adding SVG namespace {}", pres.getName(), e); - } + AddNamespaceToSvgHandler namespaceHandler = new AddNamespaceToSvgHandler(); + addNameSpaceToSVG.setProcessListener(namespaceHandler); + NuProcess namespaceProcess = addNameSpaceToSVG.start(); + try { + namespaceProcess.waitFor(WAIT_FOR_SEC, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.error("Interrupted Exception while adding SVG namespace {}", pres.getName(), e); } - - slidesCompleted++; - notifier.sendConversionUpdateMessage(slidesCompleted, pres); - } + long endConv = System.currentTimeMillis(); + + //System.out.println("******** CREATING SVG page " + page + " " + (endConv - startConv)); + if (done) { return true; } + copyBlankSvgs(imagePresentationDir, pres.getNumberOfPages()); + Map<String, Object> logData = new HashMap<String, Object>(); logData.put("meetingId", pres.getMeetingId()); logData.put("presId", pres.getId()); @@ -235,6 +239,33 @@ public class SvgImageCreatorImp implements SvgImageCreator { return new File(presentationFile.getParent() + File.separatorChar + "svgs"); } + private void copyBlankSvgs(File svgssDir, int pageCount) { + File[] svgs = svgssDir.listFiles(); + + if (svgs.length != pageCount) { + for (int i = 1; i <= pageCount; i++) { + File svg = new File(svgssDir.getAbsolutePath() + File.separator + "slide" + i + ".svg"); + if (!svg.exists()) { + log.info("Copying blank svg for slide {}", i); + copyBlankSvg(svg); + } + } + } + } + + private void copyBlankSvg(File svg) { + try { + FileUtils.copyFile(new File(BLANK_SVG), svg); + } catch (IOException e) { + log.error("IOException while copying blank SVG."); + } + } + + + public void setBlankSvg(String blankSvg) { + BLANK_SVG = blankSvg; + } + public void setImageTagThreshold(long threshold) { imageTagThreshold = threshold; } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SwfSlidesGenerationProgressNotifier.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SwfSlidesGenerationProgressNotifier.java index 3039d37e0a6aa80ec2de18aed8aa021188829e9b..00206efb1286dbf7de620050efd59fd7dd888847 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SwfSlidesGenerationProgressNotifier.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/SwfSlidesGenerationProgressNotifier.java @@ -42,12 +42,21 @@ public class SwfSlidesGenerationProgressNotifier { } - public void sendConversionUpdateMessage(int slidesCompleted, UploadedPresentation pres) { - DocPageGeneratedProgress progress = new DocPageGeneratedProgress(pres.getPodId(), pres.getMeetingId(), - pres.getId(), pres.getId(), - pres.getName(), "notUsedYet", "notUsedYet", - pres.isDownloadable(), ConversionMessageConstants.GENERATED_SLIDE_KEY, - pres.getNumberOfPages(), slidesCompleted); + public void sendConversionUpdateMessage(int slidesCompleted, UploadedPresentation pres, int pageGenerated) { + DocPageGeneratedProgress progress = new DocPageGeneratedProgress(pres.getPodId(), + pres.getMeetingId(), + pres.getId(), + pres.getId(), + pres.getName(), + "notUsedYet", + "notUsedYet", + pres.isDownloadable(), + ConversionMessageConstants.GENERATED_SLIDE_KEY, + pres.getNumberOfPages(), + slidesCompleted, + generateBasePresUrl(pres), + pageGenerated, + (pageGenerated == 1)); messagingService.sendDocConversionMsg(progress); } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/TextFileCreatorImp.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/TextFileCreatorImp.java index 4763fb3b8aa156b8c8d51808757bc55c86467f96..7fabc20697f7ed63e1a18c06d14338c85d1eef3b 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/TextFileCreatorImp.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/TextFileCreatorImp.java @@ -39,16 +39,15 @@ public class TextFileCreatorImp implements TextFileCreator { private static Logger log = LoggerFactory.getLogger(TextFileCreatorImp.class); @Override - public boolean createTextFiles(UploadedPresentation pres) { + public boolean createTextFile(UploadedPresentation pres, int page) { boolean success = false; File textfilesDir = determineTextfilesDirectory(pres.getUploadedFile()); if (!textfilesDir.exists()) textfilesDir.mkdir(); - cleanDirectory(textfilesDir); try { - success = generateTextFiles(textfilesDir, pres); + success = generateTextFile(textfilesDir, pres, page); } catch (InterruptedException e) { log.error("Interrupted Exception while generating thumbnails {}", pres.getName(), e); success = false; @@ -61,8 +60,8 @@ public class TextFileCreatorImp implements TextFileCreator { return success; } - private boolean generateTextFiles(File textfilesDir, - UploadedPresentation pres) throws InterruptedException { + private boolean generateTextFile(File textfilesDir, + UploadedPresentation pres, int page) throws InterruptedException { boolean success = true; String source = pres.getUploadedFile().getAbsolutePath(); String dest; @@ -90,11 +89,14 @@ public class TextFileCreatorImp implements TextFileCreator { } } else { - dest = textfilesDir.getAbsolutePath() + File.separatorChar + "slide-"; + dest = textfilesDir.getAbsolutePath() + File.separatorChar + "slide-" + page + ".txt"; // sudo apt-get install xpdf-utils - for (int i = 1; i <= pres.getNumberOfPages(); i++) { - COMMAND = "pdftotext -raw -nopgbrk -enc UTF-8 -f " + i + " -l " + i - + " " + source + " " + dest + i + ".txt"; + + COMMAND = "pdftotext -raw -nopgbrk -enc UTF-8 -f " + page + " -l " + page + + " " + source + " " + dest; + + //System.out.println(COMMAND); + boolean done = new ExternalProcessExecutor().exec(COMMAND, 60000); if (!done) { success = false; @@ -110,10 +112,7 @@ public class TextFileCreatorImp implements TextFileCreator { String logStr = gson.toJson(logData); log.warn(" --analytics-- data={}", logStr); - break; } - } - } return success; diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ThumbnailCreatorImp.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ThumbnailCreatorImp.java index f4c05c8c9256b5ad6512e45aa54e837cd56a3733..59f04bb89441a41249d09af6bed19c07672384e9 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ThumbnailCreatorImp.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ThumbnailCreatorImp.java @@ -36,11 +36,9 @@ import org.slf4j.LoggerFactory; import com.google.gson.Gson; public class ThumbnailCreatorImp implements ThumbnailCreator { - private static Logger log = LoggerFactory - .getLogger(ThumbnailCreatorImp.class); + private static Logger log = LoggerFactory.getLogger(ThumbnailCreatorImp.class); - private static final Pattern PAGE_NUMBER_PATTERN = Pattern - .compile("(.+-thumb)-([0-9]+)(.png)"); + private static final Pattern PAGE_NUMBER_PATTERN = Pattern.compile("(.+-thumb)-([0-9]+)-([0-9]+)(.png)"); private static String TEMP_THUMB_NAME = "temp-thumb"; @@ -49,44 +47,46 @@ public class ThumbnailCreatorImp implements ThumbnailCreator { private String BLANK_THUMBNAIL; @Override - public boolean createThumbnails(UploadedPresentation pres) { + public boolean createThumbnail(UploadedPresentation pres, int page, File pageFile) { boolean success = false; File thumbsDir = determineThumbnailDirectory(pres.getUploadedFile()); if (!thumbsDir.exists()) thumbsDir.mkdir(); - cleanDirectory(thumbsDir); - try { - success = generateThumbnails(thumbsDir, pres); + success = generateThumbnail(thumbsDir, pres, page, pageFile); } catch (InterruptedException e) { log.error("Interrupted Exception while generating thumbnails {}", pres.getName(), e); success = false; } + renameThumbnails(thumbsDir, page); + // Create blank thumbnails for pages that failed to generate a thumbnail. - createBlankThumbnails(thumbsDir, pres.getNumberOfPages()); + createBlankThumbnail(thumbsDir, page); - renameThumbnails(thumbsDir); return success; } - private boolean generateThumbnails(File thumbsDir, UploadedPresentation pres) + private boolean generateThumbnail(File thumbsDir, UploadedPresentation pres, int page, File pageFile) throws InterruptedException { - String source = pres.getUploadedFile().getAbsolutePath(); + String source = pageFile.getAbsolutePath(); String dest; String COMMAND = ""; - dest = thumbsDir.getAbsolutePath() + File.separatorChar + TEMP_THUMB_NAME; + if (SupportedFileTypes.isImageFile(pres.getFileType())) { - COMMAND = IMAGEMAGICK_DIR + File.separatorChar + "convert -thumbnail 150x150 " - + source + " " + dest + ".png"; + dest = thumbsDir.getAbsolutePath() + File.separatorChar + "thumb-" + page + ".png"; + COMMAND = IMAGEMAGICK_DIR + File.separatorChar + "convert -thumbnail 150x150 " + source + " " + dest; } else { + dest = thumbsDir.getAbsolutePath() + File.separatorChar + TEMP_THUMB_NAME + "-" + page; // the "-x.png" is appended automagically COMMAND = "pdftocairo -png -scale-to 150 " + source + " " + dest; } - boolean done = new ExternalProcessExecutor().exec(COMMAND, 60000); + //System.out.println(COMMAND); + + boolean done = new ExternalProcessExecutor().exec(COMMAND, 10000); if (done) { return true; @@ -95,6 +95,7 @@ public class ThumbnailCreatorImp implements ThumbnailCreator { logData.put("meetingId", pres.getMeetingId()); logData.put("presId", pres.getId()); logData.put("filename", pres.getName()); + logData.put("page", page); logData.put("logCode", "create_thumbnails_failed"); logData.put("message", "Failed to create thumbnails."); @@ -111,7 +112,7 @@ public class ThumbnailCreatorImp implements ThumbnailCreator { presentationFile.getParent() + File.separatorChar + "thumbnails"); } - private void renameThumbnails(File dir) { + private void renameThumbnails(File dir, int page) { /* * If more than 1 file, filename like 'temp-thumb-X.png' else filename is * 'temp-thumb.png' @@ -131,10 +132,12 @@ public class ThumbnailCreatorImp implements ThumbnailCreator { // 3. .png // We are interested in the second match. int pageNum = Integer.valueOf(matcher.group(2).trim()).intValue(); - String newFilename = "thumb-" + (pageNum) + ".png"; - File renamedFile = new File( - dir.getAbsolutePath() + File.separatorChar + newFilename); - file.renameTo(renamedFile); + if (pageNum == page) { + String newFilename = "thumb-" + (page) + ".png"; + File renamedFile = new File( + dir.getAbsolutePath() + File.separatorChar + newFilename); + file.renameTo(renamedFile); + } } } } else if (dir.list().length == 1) { @@ -147,18 +150,13 @@ public class ThumbnailCreatorImp implements ThumbnailCreator { } } - private void createBlankThumbnails(File thumbsDir, int pageCount) { + private void createBlankThumbnail(File thumbsDir, int page) { File[] thumbs = thumbsDir.listFiles(); - if (thumbs.length != pageCount) { - for (int i = 0; i < pageCount; i++) { - File thumb = new File(thumbsDir.getAbsolutePath() + File.separatorChar - + TEMP_THUMB_NAME + "-" + i + ".png"); - if (!thumb.exists()) { - log.info("Copying blank thumbnail for slide {}", i); - copyBlankThumbnail(thumb); - } - } + File thumb = new File(thumbsDir.getAbsolutePath() + File.separatorChar + "thumb-" + page + ".png"); + if (!thumb.exists()) { + log.info("Copying blank thumbnail for slide {}", page); + copyBlankThumbnail(thumb); } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/DocConversionRequestReceived.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/DocConversionRequestReceived.java new file mode 100755 index 0000000000000000000000000000000000000000..ed3ced6ce13453eb36f63c869d856b9855a569e8 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/DocConversionRequestReceived.java @@ -0,0 +1,27 @@ +package org.bigbluebutton.presentation.messages; + +public class DocConversionRequestReceived implements IDocConversionMsg { + public final String podId; + public final String meetingId; + public final String presId; + public final String filename; + public final String authzToken; + public final Boolean downloadable; + public final Boolean current; + + public DocConversionRequestReceived(String podId, + String meetingId, + String presId, + String filename, + String authzToken, + Boolean downloadable, + Boolean current) { + this.podId = podId; + this.meetingId = meetingId; + this.presId = presId; + this.filename = filename; + this.authzToken = authzToken; + this.downloadable = downloadable; + this.current = current; + } +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/DocPageConversionStarted.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/DocPageConversionStarted.java new file mode 100755 index 0000000000000000000000000000000000000000..45db2929d60ccb16f607d3ac56ac57de9465e0e4 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/DocPageConversionStarted.java @@ -0,0 +1,30 @@ +package org.bigbluebutton.presentation.messages; + +public class DocPageConversionStarted implements IDocConversionMsg { + public final String podId; + public final String meetingId; + public final String presId; + public final String filename; + public final String authzToken; + public final Boolean downloadable; + public final Boolean current; + public final Integer numPages; + + public DocPageConversionStarted(String podId, + String meetingId, + String presId, + String filename, + String authzToken, + Boolean downloadable, + Boolean current, + Integer numPages) { + this.podId = podId; + this.meetingId = meetingId; + this.presId = presId; + this.filename = filename; + this.authzToken = authzToken; + this.downloadable = downloadable; + this.current = current; + this.numPages = numPages; + } +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/DocPageGeneratedProgress.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/DocPageGeneratedProgress.java index a67249da85c819cd9ac08e537d66269da04c794a..8a8fbe4119f3370df9565101de86ebc4ea85d227 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/DocPageGeneratedProgress.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/DocPageGeneratedProgress.java @@ -12,11 +12,24 @@ public class DocPageGeneratedProgress implements IDocConversionMsg { public final String key; public final Integer numPages; public final Integer pagesCompleted; + public final String presBaseUrl; + public final Boolean current; + public final Integer page; - public DocPageGeneratedProgress(String podId, String meetingId, String presId, String presInstance, - String filename, String uploaderId, String authzToken, - Boolean downloadable, String key, - Integer numPages, Integer pagesCompleted) { + public DocPageGeneratedProgress(String podId, + String meetingId, + String presId, + String presInstance, + String filename, + String uploaderId, + String authzToken, + Boolean downloadable, + String key, + Integer numPages, + Integer pagesCompleted, + String presBaseUrl, + Integer page, + Boolean current) { this.podId = podId; this.meetingId = meetingId; this.presId = presId; @@ -28,5 +41,8 @@ public class DocPageGeneratedProgress implements IDocConversionMsg { this.key = key; this.numPages = numPages; this.pagesCompleted = pagesCompleted; + this.presBaseUrl = presBaseUrl; + this.page = page; + this.current = current; } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/IPresentationCompletionMessage.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/IPresentationCompletionMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..329d1f3e7d630f7cbaaf0bd711e0df1d7a01a272 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/IPresentationCompletionMessage.java @@ -0,0 +1,4 @@ +package org.bigbluebutton.presentation.messages; + +public interface IPresentationCompletionMessage { +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/PageConvertProgressMessage.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/PageConvertProgressMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..0b235e0447bec5b66da51d9c6aeb4563e45d06c1 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/PageConvertProgressMessage.java @@ -0,0 +1,16 @@ +package org.bigbluebutton.presentation.messages; + +import java.util.List; + +public class PageConvertProgressMessage implements IPresentationCompletionMessage { + + public final String presId; + public final int page; + public final List<String> errors; + + public PageConvertProgressMessage(int page, String presId, List<String> errors) { + this.presId = presId; + this.page = page; + this.errors = errors; + } +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/PresentationConvertMessage.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/PresentationConvertMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..73b7cdf7f85ecd3fa945911487252687d90457dd --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/messages/PresentationConvertMessage.java @@ -0,0 +1,11 @@ +package org.bigbluebutton.presentation.messages; + +import org.bigbluebutton.presentation.UploadedPresentation; + +public class PresentationConvertMessage implements IPresentationCompletionMessage { + public final UploadedPresentation pres; + + public PresentationConvertMessage(UploadedPresentation pres) { + this.pres = pres; + } +} 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 7942a7ab7ecce6e3d9ae94df315d28a1deca4f1b..ac6ec822bd7a5cf32e89ed40f92657bf05dd23be 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 @@ -279,12 +279,21 @@ class BbbWebApiGWApp( if (msg.isInstanceOf[DocPageGeneratedProgress]) { val event = MsgBuilder.buildPresentationPageGeneratedPubMsg(msg.asInstanceOf[DocPageGeneratedProgress]) msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event)) + + // Send new event with page urls + val newEvent = MsgBuilder.buildPresentationPageConvertedSysMsg(msg.asInstanceOf[DocPageGeneratedProgress]) + msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, newEvent)) } else if (msg.isInstanceOf[OfficeDocConversionProgress]) { val event = MsgBuilder.buildPresentationConversionUpdateSysPubMsg(msg.asInstanceOf[OfficeDocConversionProgress]) msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event)) } else if (msg.isInstanceOf[DocPageCompletedProgress]) { val event = MsgBuilder.buildPresentationConversionCompletedSysPubMsg(msg.asInstanceOf[DocPageCompletedProgress]) msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event)) + + // Send new event with page urls + val newEvent = MsgBuilder.buildPresentationConversionEndedSysMsg(msg.asInstanceOf[DocPageCompletedProgress]) + msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, newEvent)) + } else if (msg.isInstanceOf[DocPageCountFailed]) { val event = MsgBuilder.buildPresentationPageCountFailedSysPubMsg(msg.asInstanceOf[DocPageCountFailed]) msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event)) @@ -294,6 +303,12 @@ class BbbWebApiGWApp( } else if (msg.isInstanceOf[PdfConversionInvalid]) { val event = MsgBuilder.buildPdfConversionInvalidErrorSysPubMsg(msg.asInstanceOf[PdfConversionInvalid]) msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event)) + } else if (msg.isInstanceOf[DocConversionRequestReceived]) { + val event = MsgBuilder.buildPresentationConversionRequestReceivedSysMsg(msg.asInstanceOf[DocConversionRequestReceived]) + msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event)) + } else if (msg.isInstanceOf[DocPageConversionStarted]) { + val event = MsgBuilder.buildPresentationPageConversionStartedSysMsg(msg.asInstanceOf[DocPageConversionStarted]) + msgToAkkaAppsEventBus.publish(MsgToAkkaApps(toAkkaAppsChannel, event)) } } diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/MsgBuilder.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/MsgBuilder.scala index 7134e2ebb867962ab0628172abf0902710ff6ad8..0429f9f11233361cf68dff2d8ca46b56918f1f03 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/MsgBuilder.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/MsgBuilder.scala @@ -2,7 +2,7 @@ package org.bigbluebutton.api2 import org.bigbluebutton.api.messaging.converters.messages._ import org.bigbluebutton.api2.meeting.RegisterUser -import org.bigbluebutton.common2.domain.{ DefaultProps, PageVO, PresentationVO } +import org.bigbluebutton.common2.domain.{ DefaultProps, PageVO, PresentationPageConvertedVO, PresentationVO } import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.presentation.messages._ @@ -73,10 +73,52 @@ object MsgBuilder { BbbCommonEnvCoreMsg(envelope, req) } + def generatePresentationPage(presId: String, numPages: Int, presBaseUrl: String, page: Int): PresentationPageConvertedVO = { + val id = presId + "/" + page + val current = if (page == 1) true else false + val thumbUrl = presBaseUrl + "/thumbnail/" + page + val swfUrl = presBaseUrl + "/slide/" + page + + val txtUrl = presBaseUrl + "/textfiles/" + page + val svgUrl = presBaseUrl + "/svg/" + page + val pngUrl = presBaseUrl + "/png/" + page + + val urls = Map("swf" -> swfUrl, "thumb" -> thumbUrl, "text" -> txtUrl, "svg" -> svgUrl, "png" -> pngUrl) + + PresentationPageConvertedVO( + id = id, + num = page, + urls = urls, + current = current + ) + } + + def buildPresentationPageConvertedSysMsg(msg: DocPageGeneratedProgress): BbbCommonEnvCoreMsg = { + val routing = collection.immutable.HashMap("sender" -> "bbb-web") + val envelope = BbbCoreEnvelope(PresentationPageConvertedSysMsg.NAME, routing) + val header = BbbClientMsgHeader(PresentationPageConvertedSysMsg.NAME, msg.meetingId, msg.authzToken) + + val page = generatePresentationPage(msg.presId, msg.numPages.intValue(), msg.presBaseUrl, msg.page.intValue()) + + val body = PresentationPageConvertedSysMsgBody( + podId = msg.podId, + messageKey = msg.key, + code = msg.key, + presentationId = msg.presId, + numberOfPages = msg.numPages.intValue(), + pagesCompleted = msg.pagesCompleted.intValue(), + presName = msg.filename, + page + ) + val req = PresentationPageConvertedSysMsg(header, body) + BbbCommonEnvCoreMsg(envelope, req) + } + def buildPresentationPageGeneratedPubMsg(msg: DocPageGeneratedProgress): BbbCommonEnvCoreMsg = { val routing = collection.immutable.HashMap("sender" -> "bbb-web") val envelope = BbbCoreEnvelope(PresentationPageGeneratedSysPubMsg.NAME, routing) val header = BbbClientMsgHeader(PresentationPageGeneratedSysPubMsg.NAME, msg.meetingId, msg.authzToken) + val body = PresentationPageGeneratedSysPubMsgBody(podId = msg.podId, messageKey = msg.key, code = msg.key, presentationId = msg.presId, numberOfPages = msg.numPages.intValue(), pagesCompleted = msg.pagesCompleted.intValue(), presName = msg.filename) @@ -94,6 +136,20 @@ object MsgBuilder { BbbCommonEnvCoreMsg(envelope, req) } + def buildPresentationConversionEndedSysMsg(msg: DocPageCompletedProgress): BbbCommonEnvCoreMsg = { + val routing = collection.immutable.HashMap("sender" -> "bbb-web") + val envelope = BbbCoreEnvelope(PresentationConversionEndedSysMsg.NAME, routing) + val header = BbbClientMsgHeader(PresentationConversionEndedSysMsg.NAME, msg.meetingId, msg.authzToken) + + val body = PresentationConversionEndedSysMsgBody( + podId = msg.podId, + presentationId = msg.presId, + presName = msg.filename + ) + val req = PresentationConversionEndedSysMsg(header, body) + BbbCommonEnvCoreMsg(envelope, req) + } + def buildPresentationConversionCompletedSysPubMsg(msg: DocPageCompletedProgress): BbbCommonEnvCoreMsg = { val routing = collection.immutable.HashMap("sender" -> "bbb-web") val envelope = BbbCoreEnvelope(PresentationConversionCompletedSysPubMsg.NAME, routing) @@ -152,7 +208,7 @@ object MsgBuilder { BbbCommonEnvCoreMsg(envelope, req) } - def buildPdfConversionInvalidErrorSysPubMsg(msg: PdfConversionInvalid): BbbCommonEnvCoreMsg ={ + def buildPdfConversionInvalidErrorSysPubMsg(msg: PdfConversionInvalid): BbbCommonEnvCoreMsg = { val routing = collection.immutable.HashMap("sender" -> "bbb-web") val envelope = BbbCoreEnvelope(PdfConversionInvalidErrorSysPubMsg.NAME, routing) val header = BbbClientMsgHeader(PdfConversionInvalidErrorSysPubMsg.NAME, msg.meetingId, msg.authzToken) @@ -162,7 +218,42 @@ object MsgBuilder { val req = PdfConversionInvalidErrorSysPubMsg(header, body) BbbCommonEnvCoreMsg(envelope, req) } - + + def buildPresentationConversionRequestReceivedSysMsg(msg: DocConversionRequestReceived): BbbCommonEnvCoreMsg = { + val routing = collection.immutable.HashMap("sender" -> "bbb-web") + val envelope = BbbCoreEnvelope(PresentationConversionRequestReceivedSysMsg.NAME, routing) + val header = BbbClientMsgHeader(PresentationConversionRequestReceivedSysMsg.NAME, msg.meetingId, msg.authzToken) + + val body = PresentationConversionRequestReceivedSysMsgBody( + podId = msg.podId, + presentationId = msg.presId, + current = msg.current, + presName = msg.filename, + downloadable = msg.downloadable, + authzToken = msg.authzToken + ) + val req = PresentationConversionRequestReceivedSysMsg(header, body) + BbbCommonEnvCoreMsg(envelope, req) + } + + def buildPresentationPageConversionStartedSysMsg(msg: DocPageConversionStarted): BbbCommonEnvCoreMsg = { + val routing = collection.immutable.HashMap("sender" -> "bbb-web") + val envelope = BbbCoreEnvelope(PresentationPageConversionStartedSysMsg.NAME, routing) + val header = BbbClientMsgHeader(PresentationPageConversionStartedSysMsg.NAME, msg.meetingId, msg.authzToken) + + val body = PresentationPageConversionStartedSysMsgBody( + podId = msg.podId, + presentationId = msg.presId, + current = msg.current, + presName = msg.filename, + downloadable = msg.downloadable, + numPages = msg.numPages, + authzToken = msg.authzToken + ) + val req = PresentationPageConversionStartedSysMsg(header, body) + BbbCommonEnvCoreMsg(envelope, req) + } + def buildPublishedRecordingSysMsg(msg: PublishedRecordingMessage): BbbCommonEnvCoreMsg = { val routing = collection.immutable.HashMap("sender" -> "bbb-web") val envelope = BbbCoreEnvelope(PublishedRecordingSysMsg.NAME, routing) diff --git a/bigbluebutton-config/slides/blank-png.png b/bigbluebutton-config/slides/blank-png.png new file mode 100644 index 0000000000000000000000000000000000000000..39a230dccb0192c283f7fc64819a740feaae639f Binary files /dev/null and b/bigbluebutton-config/slides/blank-png.png differ diff --git a/bigbluebutton-config/slides/blank-presentation.pdf b/bigbluebutton-config/slides/blank-presentation.pdf index ebf6257403b88d9f0eeeb1dfcb096f0c9fc52a9d..02e8b9cc06ca82dac413c149503c14ca45c3f125 100644 Binary files a/bigbluebutton-config/slides/blank-presentation.pdf and b/bigbluebutton-config/slides/blank-presentation.pdf differ diff --git a/bigbluebutton-config/slides/blank-svg.svg b/bigbluebutton-config/slides/blank-svg.svg new file mode 100644 index 0000000000000000000000000000000000000000..9c1a952984808e11fecb8271801447740b1d7d0b --- /dev/null +++ b/bigbluebutton-config/slides/blank-svg.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 841.89 595.28" style="enable-background:new 0 0 841.89 595.28;" xml:space="preserve"> +</svg> diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js index fd426ee21765228ff3fd1b2d98181aba3d3ea8d8..3c72abbfcbe045a12abb64b1446c11629dbbae82 100755 --- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js +++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js @@ -502,7 +502,8 @@ class SIPSession { query.observeChanges({ changed: (id, fields) => { - if (fields.callState === CallStateOptions.IN_ECHO_TEST) { + if ((this.inEchoTest && fields.callState === CallStateOptions.IN_ECHO_TEST) + || (!this.inEchoTest && fields.callState === CallStateOptions.IN_CONFERENCE)) { fsReady = true; checkIfCallReady(); @@ -543,6 +544,9 @@ export default class SIPBridge extends BaseAudioBridge { window.toUnifiedPlan = toUnifiedPlan; window.toPlanB = toPlanB; window.stripMDnsCandidates = stripMDnsCandidates; + + // No easy way to expose the client logger to sip.js code so we need to attach it globally + window.clientLogger = logger; } joinAudio({ isListenOnly, extension, inputStream }, managerCallback) { diff --git a/bigbluebutton-html5/imports/api/breakouts/server/publishers.js b/bigbluebutton-html5/imports/api/breakouts/server/publishers.js index 5f682d423a3e0c41ca7ec0c56e48a0db1d1991ad..d0040bd87ecc475f1a1475dc82ef96ba822ff568 100755 --- a/bigbluebutton-html5/imports/api/breakouts/server/publishers.js +++ b/bigbluebutton-html5/imports/api/breakouts/server/publishers.js @@ -15,7 +15,7 @@ function breakouts(moderator = false) { Logger.debug(`Publishing Breakouts for ${meetingId} ${requesterUserId}`); if (moderator) { - const User = Users.findOne({ userId: requesterUserId }); + const User = Users.findOne({ userId: requesterUserId, meetingId }); if (!!User && User.role === ROLE_MODERATOR) { const presenterSelector = { $or: [ diff --git a/bigbluebutton-html5/imports/api/cursor/server/handlers/cursorUpdate.js b/bigbluebutton-html5/imports/api/cursor/server/handlers/cursorUpdate.js index 2e094fb1cf52696469f52ad44b5c2162f58d1ad1..bad37677da3171edd8228a4987e202c589c1b309 100755 --- a/bigbluebutton-html5/imports/api/cursor/server/handlers/cursorUpdate.js +++ b/bigbluebutton-html5/imports/api/cursor/server/handlers/cursorUpdate.js @@ -1,34 +1,33 @@ import { check } from 'meteor/check'; import CursorStreamer from '/imports/api/cursor/server/streamer'; import Logger from '/imports/startup/server/logger'; - +import _ from 'lodash'; const { streamerLog } = Meteor.settings.private.serverLog; const CURSOR_PROCCESS_INTERVAL = 30; -let cursorQueue = {}; -let cursorReceiverIsRunning = false; - -const proccess = () => { - if (!Object.keys(cursorQueue).length) { - cursorReceiverIsRunning = false; - return; - } - cursorReceiverIsRunning = true; +const cursorQueue = {}; +const proccess = _.throttle(() => { try { Object.keys(cursorQueue).forEach((meetingId) => { - CursorStreamer(meetingId).emit('message', { meetingId, cursors: cursorQueue[meetingId] }); + try { + const cursors = cursorQueue[meetingId]; + delete cursorQueue[meetingId]; + CursorStreamer(meetingId).emit('message', { meetingId, cursors }); + + if (streamerLog) { + Logger.debug(`CursorUpdate process for meeting ${meetingId} has finished`); + } + } catch (error) { + Logger.error(`Error while trying to send cursor streamer data for meeting ${meetingId}. ${error}`); + } }); - cursorQueue = {}; - - Meteor.setTimeout(proccess, CURSOR_PROCCESS_INTERVAL); } catch (error) { - Logger.error(`Error while trying to send cursor streamer data. ${error}`); - cursorReceiverIsRunning = false; + Logger.error(`Error while processing cursor queue. ${error}`); } -}; +}, CURSOR_PROCCESS_INTERVAL); export default function handleCursorUpdate({ header, body }, meetingId) { const { userId } = header; @@ -37,15 +36,12 @@ export default function handleCursorUpdate({ header, body }, meetingId) { check(meetingId, String); check(userId, String); - if (!cursorQueue.hasOwnProperty(meetingId)) { + if (!cursorQueue[meetingId]) { cursorQueue[meetingId] = {}; } - if (streamerLog) { - Logger.debug(`CursorUpdate process for meeting ${meetingId} is running: ${cursorReceiverIsRunning}`); - } - // overwrite since we dont care about the other positions cursorQueue[meetingId][userId] = body; - if (!cursorReceiverIsRunning) proccess(); + + proccess(); } diff --git a/bigbluebutton-html5/imports/api/external-videos/server/methods.js b/bigbluebutton-html5/imports/api/external-videos/server/methods.js index 79c3e4fd2e66538898b36a6dd7934d555ff52fd5..7df620cb7a18089285bfccefa51d2aaffbde4e50 100644 --- a/bigbluebutton-html5/imports/api/external-videos/server/methods.js +++ b/bigbluebutton-html5/imports/api/external-videos/server/methods.js @@ -2,9 +2,11 @@ import { Meteor } from 'meteor/meteor'; import startWatchingExternalVideo from './methods/startWatchingExternalVideo'; import stopWatchingExternalVideo from './methods/stopWatchingExternalVideo'; import initializeExternalVideo from './methods/initializeExternalVideo'; +import emitExternalVideoEvent from './methods/emitExternalVideoEvent'; Meteor.methods({ initializeExternalVideo, startWatchingExternalVideo, stopWatchingExternalVideo, + emitExternalVideoEvent, }); diff --git a/bigbluebutton-html5/imports/api/external-videos/server/methods/emitExternalVideoEvent.js b/bigbluebutton-html5/imports/api/external-videos/server/methods/emitExternalVideoEvent.js new file mode 100644 index 0000000000000000000000000000000000000000..8c5d1535b8a926cf274e8d783769e4d069df5a58 --- /dev/null +++ b/bigbluebutton-html5/imports/api/external-videos/server/methods/emitExternalVideoEvent.js @@ -0,0 +1,20 @@ +import Users from '/imports/api/users'; +import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +export default function emitExternalVideoEvent(messageName, ...rest) { + const { meetingId, requesterUserId: userId } = extractCredentials(this.userId); + + const user = Users.findOne({ userId, meetingId }); + + if (user && user.presenter) { + const streamerName = `external-videos-${meetingId}`; + const streamer = Meteor.StreamerCentral.instances[streamerName]; + + if (streamer) { + streamer.emit(messageName, ...rest); + } else { + Logger.error(`External Video Streamer not found for meetingId: ${meetingId} userId: ${userId}`); + } + } +} diff --git a/bigbluebutton-html5/imports/api/external-videos/server/methods/initializeExternalVideo.js b/bigbluebutton-html5/imports/api/external-videos/server/methods/initializeExternalVideo.js index 84d08a8c2cffabbe0e670ee22dd39ba195882e0c..2c76ad38fe920919f9555b9f0944e94a900f5737 100644 --- a/bigbluebutton-html5/imports/api/external-videos/server/methods/initializeExternalVideo.js +++ b/bigbluebutton-html5/imports/api/external-videos/server/methods/initializeExternalVideo.js @@ -1,22 +1,26 @@ -import { Meteor } from 'meteor/meteor'; import { extractCredentials } from '/imports/api/common/server/helpers'; -import Users from '/imports/api/users'; import Logger from '/imports/startup/server/logger'; -const allowFromPresenter = (eventName, message) => { +const allowRecentMessages = (eventName, message) => { + const LATE_MESSAGE_THRESHOLD = 3000; + const { userId, + meetingId, time, + timestamp, rate, state, } = message; - const user = Users.findOne({ userId }); - const ret = user && user.presenter; + if (timestamp > Date.now() - LATE_MESSAGE_THRESHOLD) { + Logger.debug(`ExternalVideo Streamer auth allowed userId: ${userId}, meetingId: ${meetingId}, event: ${eventName}, time: ${time}, timestamp: ${timestamp/1000} rate: ${rate}, state: ${state}`); + return true; + } - Logger.info(`ExternalVideo Streamer auth userid: ${userId}, meetingId: ${user.meetingId}, event: ${eventName}, suc: ${ret}, time: ${time}, rate: ${rate}, state: ${state}`); + Logger.debug(`ExternalVideo Streamer auth rejected userId: ${userId}, meetingId: ${meetingId}, event: ${eventName}, time: ${time}, timestamp: ${timestamp/1000} rate: ${rate}, state: ${state}`); - return ret; + return false; }; export default function initializeExternalVideo() { @@ -26,8 +30,9 @@ export default function initializeExternalVideo() { if (!Meteor.StreamerCentral.instances[streamName]) { const streamer = new Meteor.Streamer(streamName); streamer.allowRead('all'); - streamer.allowWrite('all'); - streamer.allowEmit(allowFromPresenter); + streamer.allowWrite('none'); + streamer.allowEmit(allowRecentMessages); + Logger.info(`Created External Video streamer for ${streamName}`); } else { Logger.debug(`External Video streamer is already created for ${streamName}`); } diff --git a/bigbluebutton-html5/imports/api/external-videos/server/methods/stopWatchingExternalVideo.js b/bigbluebutton-html5/imports/api/external-videos/server/methods/stopWatchingExternalVideo.js index 5165e73e085672b881844a9d5412ddc976067a9e..60384b82f772230d68d5c5399008f7de9e780610 100644 --- a/bigbluebutton-html5/imports/api/external-videos/server/methods/stopWatchingExternalVideo.js +++ b/bigbluebutton-html5/imports/api/external-videos/server/methods/stopWatchingExternalVideo.js @@ -2,12 +2,19 @@ import { Meteor } from 'meteor/meteor'; import Logger from '/imports/startup/server/logger'; import Meetings from '/imports/api/meetings'; import RedisPubSub from '/imports/startup/server/redis'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function stopWatchingExternalVideo(meetingId, requesterUserId) { +export default function stopWatchingExternalVideo(options) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'StopExternalVideoMsg'; + if (this.userId) { + options = extractCredentials(this.userId); + } + + const { meetingId, requesterUserId } = options; + const meeting = Meetings.findOne({ meetingId }); if (!meeting || meeting.externalVideoUrl === null) return; diff --git a/bigbluebutton-html5/imports/api/meetings/server/publishers.js b/bigbluebutton-html5/imports/api/meetings/server/publishers.js index 01a1e5452fe3a12556a799e7da4a6c25794f1eb2..997c79b66b4b158e2c50b38c7a09401c42934fa1 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/publishers.js +++ b/bigbluebutton-html5/imports/api/meetings/server/publishers.js @@ -21,7 +21,7 @@ function meetings(isModerator = false) { }; if (isModerator) { - const User = Users.findOne({ userId: requesterUserId }); + const User = Users.findOne({ userId: requesterUserId, meetingId }); if (!!User && User.role === ROLE_MODERATOR) { selector.$or.push({ 'meetingProp.isBreakout': true, diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/removeUser.js b/bigbluebutton-html5/imports/api/users/server/modifiers/removeUser.js index 13755349dbf3bf12bfa4298615b2e029d5c7cdd5..8535499e2987ecb4a492d95dfad6a950fbe177e3 100755 --- a/bigbluebutton-html5/imports/api/users/server/modifiers/removeUser.js +++ b/bigbluebutton-html5/imports/api/users/server/modifiers/removeUser.js @@ -16,7 +16,7 @@ export default function removeUser(meetingId, userId) { check(meetingId, String); check(userId, String); - const userToRemove = Users.findOne({ userId }); + const userToRemove = Users.findOne({ userId, meetingId }); if (userToRemove) { const { presenter } = userToRemove; diff --git a/bigbluebutton-html5/imports/api/users/server/publishers.js b/bigbluebutton-html5/imports/api/users/server/publishers.js index 2e342ec03f452511df0991cc7de77ff472c9985c..861fb357e16daebcc17417ef6f1efc2eb8d69c5b 100644 --- a/bigbluebutton-html5/imports/api/users/server/publishers.js +++ b/bigbluebutton-html5/imports/api/users/server/publishers.js @@ -64,7 +64,7 @@ function users(isModerator = false) { }; if (isModerator) { - const User = Users.findOne({ userId: requesterUserId }); + const User = Users.findOne({ userId: requesterUserId, meetingId }); if (!!User && User.role === ROLE_MODERATOR) { selector.$or.push({ 'breakoutProps.isBreakoutUser': true, diff --git a/bigbluebutton-html5/imports/api/voice-call-states/server/handlers/voiceCallStateEvent.js b/bigbluebutton-html5/imports/api/voice-call-states/server/handlers/voiceCallStateEvent.js index 16ed32e39e0575a00e13193f049427f131393a4d..ca66c2473d5eaaf65522ea512bd4390136271adf 100644 --- a/bigbluebutton-html5/imports/api/voice-call-states/server/handlers/voiceCallStateEvent.js +++ b/bigbluebutton-html5/imports/api/voice-call-states/server/handlers/voiceCallStateEvent.js @@ -41,7 +41,7 @@ export default function handleVoiceCallStateEvent({ body }, meetingId) { return Logger.error(`Update voice call state=${userId}: ${err}`); } - return Logger.debug(`Update voice call state=${userId} meeting=${meetingId} clientSession=${clientSession}`); + return Logger.debug(`Update voice call state=${userId} meeting=${meetingId} clientSession=${clientSession} callState=${callState}`); }; return VoiceCallState.upsert(selector, modifier, cb); diff --git a/bigbluebutton-html5/imports/api/voice-users/server/methods.js b/bigbluebutton-html5/imports/api/voice-users/server/methods.js index e78ab55dc740b968d478da1705c4429481a19256..6e40c34917afd32fa9bb6756f528b67b2277447c 100755 --- a/bigbluebutton-html5/imports/api/voice-users/server/methods.js +++ b/bigbluebutton-html5/imports/api/voice-users/server/methods.js @@ -1,12 +1,10 @@ import { Meteor } from 'meteor/meteor'; -import listenOnlyToggle from './methods/listenOnlyToggle'; import muteToggle from './methods/muteToggle'; import muteAllToggle from './methods/muteAllToggle'; import muteAllExceptPresenterToggle from './methods/muteAllExceptPresenterToggle'; import ejectUserFromVoice from './methods/ejectUserFromVoice'; Meteor.methods({ - listenOnlyToggle, toggleVoice: muteToggle, muteAllUsers: muteAllToggle, muteAllExceptPresenter: muteAllExceptPresenterToggle, diff --git a/bigbluebutton-html5/imports/api/voice-users/server/methods/listenOnlyToggle.js b/bigbluebutton-html5/imports/api/voice-users/server/methods/listenOnlyToggle.js deleted file mode 100644 index c5ce88176a6d24b2ff757793e08033b3bdc31da1..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/api/voice-users/server/methods/listenOnlyToggle.js +++ /dev/null @@ -1,46 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import RedisPubSub from '/imports/startup/server/redis'; -import Logger from '/imports/startup/server/logger'; -import Meetings from '/imports/api/meetings'; -import VoiceUsers from '/imports/api/voice-users'; -import { extractCredentials } from '/imports/api/common/server/helpers'; - -export default function listenOnlyToggle(isJoining = true) { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(isJoining, Boolean); - - let EVENT_NAME; - - if (isJoining) { - EVENT_NAME = 'UserConnectedToGlobalAudioMsg'; - } else { - EVENT_NAME = 'UserDisconnectedFromGlobalAudioMsg'; - } - - const VoiceUser = VoiceUsers.findOne({ - intId: requesterUserId, - }); - - const Meeting = Meetings.findOne({ meetingId }); - - if (!VoiceUser) { - throw new Meteor.Error('user-not-found', 'You need a valid user to be able to toggle audio'); - } - - // check(User.user.name, String); - - const payload = { - userId: requesterUserId, - name: VoiceUser.callerName, - }; - - Logger.info(`VoiceUser '${requesterUserId}' ${isJoining - ? 'joined' : 'left'} global audio from meeting '${meetingId}'`); - - return RedisPubSub.publishVoiceMessage(CHANNEL, EVENT_NAME, Meeting.voiceProp.voiceConf, payload); -} diff --git a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js index 81d4ab92b91b483c7c20426ef50486ce516438ea..fdc3c23f4d4a6df62b6e16e7ce6327bf3a23ddba 100644 --- a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js +++ b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js @@ -19,6 +19,7 @@ export default function muteToggle(uId) { const voiceUser = VoiceUsers.findOne({ intId: userToMute, + meetingId, }); if (!requester || !voiceUser) return; diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/component.jsx b/bigbluebutton-html5/imports/ui/components/external-video-player/component.jsx index 3655917ed340c756a6247f35f972e188330c6aa0..845076181ce37854b24c79b55919d9fd86a9f29d 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/component.jsx @@ -17,6 +17,7 @@ const intlMessages = defineMessages({ }); const SYNC_INTERVAL_SECONDS = 5; +const THROTTLE_INTERVAL_SECONDS = 0.5; const AUTO_PLAY_BLOCK_DETECTION_TIMEOUT_SECONDS = 5; ReactPlayer.addCustomPlayer(ArcPlayer); @@ -34,6 +35,9 @@ class VideoPlayer extends Component { this.playerIsReady = false; this.lastMessage = null; + this.lastMessageTimestamp = Date.now(); + + this.throttleTimeout = null; this.state = { mutedByEchoTest: false, @@ -147,11 +151,31 @@ class VideoPlayer extends Component { } sendSyncMessage(msg, params) { + const timestamp = Date.now(); + + // If message is just a quick pause/un-pause just send nothing + const sinceLastMessage = (timestamp - this.lastMessageTimestamp)/1000; + if ((msg === 'play' && this.lastMessage === 'stop' || + msg === 'stop' && this.lastMessage === 'play') && + sinceLastMessage < THROTTLE_INTERVAL_SECONDS) { + + return clearTimeout(this.throttleTimeout); + } + + // Ignore repeat presenter ready messages if (this.lastMessage === msg && msg === 'presenterReady') { logger.debug("Ignoring a repeated presenterReady message"); } else { - sendMessage(msg, params); + // Play/pause messages are sent with a delay, to permit cancelling it in case of + // quick sucessive play/pauses + const messageDelay = (msg === 'play' || msg === 'stop') ? THROTTLE_INTERVAL_SECONDS : 0; + + this.throttleTimeout = setTimeout(() => { + sendMessage(msg, { ...params, timestamp }); + }, messageDelay*1000); + this.lastMessage = msg; + this.lastMessageTimestamp = timestamp; } } @@ -243,26 +267,25 @@ class VideoPlayer extends Component { }, SYNC_INTERVAL_SECONDS * 1000); } else { - onMessage('play', ({ time }) => { + onMessage('play', ({ time, timestamp }) => { const { hasPlayedBefore, player } = this; if (!player || !hasPlayedBefore) { return; } - - player.seekTo(time); + this.seekTo(time, timestamp); this.setState({ playing: true }); logger.debug({ logCode: 'external_video_client_play' }, 'Play external video'); }); - onMessage('stop', ({ time }) => { + onMessage('stop', ({ time, timestamp }) => { const { hasPlayedBefore, player } = this; if (!player || !hasPlayedBefore) { return; } - player.seekTo(time); + this.seekTo(time, timestamp); this.setState({ playing: false }); logger.debug({ logCode: 'external_video_client_stop' }, 'Stop external video'); @@ -281,38 +304,61 @@ class VideoPlayer extends Component { onMessage('playerUpdate', (data) => { const { hasPlayedBefore, player } = this; const { playing } = this.state; + const { time, timestamp, rate, state } = data; if (!player || !hasPlayedBefore) { return; } - if (data.rate !== this.getCurrentPlaybackRate()) { - this.setPlaybackRate(data.rate); + if (rate !== this.getCurrentPlaybackRate()) { + this.setPlaybackRate(rate); logger.debug({ logCode: 'external_video_client_update_rate', extraInfo: { - newRate: data.rate, + newRate: rate, }, }, 'Change external video playback rate.'); } - if (Math.abs(this.getCurrentTime() - data.time) > SYNC_INTERVAL_SECONDS) { - player.seekTo(data.time, true); - logger.debug({ - logCode: 'external_video_client_update_seek', - extraInfo: { - time: data.time, - }, - }, 'Seek external video to:'); - } + this.seekTo(time, timestamp); - if (playing !== data.state) { - this.setState({ playing: data.state }); + if (playing !== state) { + this.setState({ playing: state }); } }); } } + seekTo(time, timestamp) { + const { player } = this; + + if (!player) { + return logger.error("No player on seek"); + } + + const curTimestamp = Date.now(); + const timestampDiff = (curTimestamp - timestamp)/1000; + const realTime = time + timestampDiff; + + // Ignore seek commands that arrived too late + if (timestampDiff > SYNC_INTERVAL_SECONDS) { + logger.debug({ + logCode: 'external_video_client_message_too_late', + extraInfo: { time, timestamp, }, + }, 'Not seeking because message came too late'); + return; + } + + // Seek if viewer has drifted too far away from presenter + if (Math.abs(this.getCurrentTime() - realTime) > SYNC_INTERVAL_SECONDS*0.75) { + player.seekTo(realTime, true); + logger.debug({ + logCode: 'external_video_client_update_seek', + extraInfo: { time, timestamp, }, + }, `Seek external video to: ${time}`); + } + } + handleOnReady() { const { hasPlayedBefore, playerIsReady } = this; diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/service.js b/bigbluebutton-html5/imports/ui/components/external-video-player/service.js index 038101ac1f08264e7258adb87bafa028f81b6245..927a31a53d92d70bff24fb7baa8ff5ac0b271403 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/service.js +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/service.js @@ -1,5 +1,8 @@ import Meetings from '/imports/api/meetings'; +import Users from '/imports/api/users'; import Auth from '/imports/ui/services/auth'; +import Logger from '/imports/startup/client/logger'; + import { getStreamer } from '/imports/api/external-videos'; import { makeCall } from '/imports/ui/services/api'; @@ -17,13 +20,10 @@ const stopWatching = () => { }; const sendMessage = (event, data) => { - const streamer = getStreamer(Auth.meetingID); + const meetingId = Auth.meetingID; + const userId = Auth.userID; - streamer.emit(event, { - ...data, - meetingId: Auth.meetingID, - userId: Auth.userID, - }); + makeCall('emitExternalVideoEvent', event, { ...data, meetingId, userId }); }; const onMessage = (message, func) => { diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx index dc24e69e9859fa061b20a7bedd09c106df1f51c3..b02cbaf0a7f60996eb2e753c32b715fca01036f0 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx @@ -89,10 +89,6 @@ const intlMessages = defineMessages({ id: 'app.presentationUploder.fileToUpload', description: 'message used in the file selected for upload', }, - genericError: { - id: 'app.presentationUploder.genericError', - description: 'generic error while uploading/converting', - }, rejectedError: { id: 'app.presentationUploder.rejectedError', description: 'some files rejected, please check the file mime types', @@ -132,10 +128,22 @@ const intlMessages = defineMessages({ id: 'app.presentationUploder.conversion.pageCountExceeded', description: 'warns the user that the conversion failed because of the page count', }, + PAGE_COUNT_FAILED: { + id: 'app.presentationUploder.conversion.pageCountFailed', + description: '', + }, PDF_HAS_BIG_PAGE: { id: 'app.presentationUploder.conversion.pdfHasBigPage', description: 'warns the user that the conversion failed because of the pdf page siz that exceeds the allowed limit', }, + OFFICE_DOC_CONVERSION_INVALID: { + id: 'app.presentationUploder.conversion.officeDocConversionInvalid', + description: '', + }, + OFFICE_DOC_CONVERSION_FAILED: { + id: 'app.presentationUploder.conversion.officeDocConversionFailed', + description: '', + }, isDownloadable: { id: 'app.presentationUploder.isDownloadableLabel', description: 'presentation is available for downloading by all viewers', @@ -249,7 +257,7 @@ class PresentationUploader extends Component { } handleConfirm() { - const { mountModal, intl, handleSave } = this.props; + const { mountModal, handleSave } = this.props; const { disableActions, presentations, oldCurrentId } = this.state; const presentationsToSave = presentations .filter(p => !p.upload.error && !p.conversion.error); @@ -287,7 +295,6 @@ class PresentationUploader extends Component { }); }) .catch((error) => { - notify(intl.formatMessage(intlMessages.genericError), 'error'); logger.error({ logCode: 'presentationuploader_component_save_error', extraInfo: { error }, @@ -483,6 +490,7 @@ class PresentationUploader extends Component { renderPresentationItemStatus(item) { const { intl } = this.props; + if (!item.upload.done && item.upload.progress === 0) { return intl.formatMessage(intlMessages.fileToUpload); } @@ -494,13 +502,11 @@ class PresentationUploader extends Component { } if (item.upload.done && item.upload.error) { - const errorMessage = intlMessages[item.upload.status] || intlMessages.genericError; - return intl.formatMessage(errorMessage); + return intl.formatMessage(intlMessages[item.upload.status]); } if (!item.conversion.done && item.conversion.error) { - const errorMessage = intlMessages[item.conversion.status] || intlMessages.genericError; - return intl.formatMessage(errorMessage); + return intl.formatMessage(intlMessages[item.conversion.status]); } if (!item.conversion.done && !item.conversion.error) { diff --git a/bigbluebutton-html5/package-lock.json b/bigbluebutton-html5/package-lock.json index c92690df8dd87bc1dae6b45ddba6ebdd7d15113b..574e2e493a045ab77d12d2f809081f45e34ee01c 100644 --- a/bigbluebutton-html5/package-lock.json +++ b/bigbluebutton-html5/package-lock.json @@ -3842,18 +3842,6 @@ } } }, - "history": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz", - "integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==", - "requires": { - "invariant": "^2.2.1", - "loose-envify": "^1.2.0", - "resolve-pathname": "^2.2.0", - "value-equal": "^0.4.0", - "warning": "^3.0.0" - } - }, "hoist-non-react-statics": { "version": "2.5.5", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", @@ -9237,11 +9225,6 @@ "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", "dev": true }, - "resolve-pathname": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", - "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==" - }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -10547,11 +10530,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "value-equal": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz", - "integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw==" - }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", diff --git a/bigbluebutton-html5/package.json b/bigbluebutton-html5/package.json index b58055dd2e02d29478f577725a5bb114a4e9bbc6..85867b5c7124fe7f3ab44d72062b61fc4fcd91d5 100755 --- a/bigbluebutton-html5/package.json +++ b/bigbluebutton-html5/package.json @@ -43,7 +43,6 @@ "fastdom": "^1.0.9", "fibers": "^4.0.2", "flat": "~4.1.0", - "history": "~4.7.2", "immutability-helper": "~2.8.1", "langmap": "0.0.16", "lodash": "^4.17.15", diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 5ac23b4386e3f924bdbea7424b61bac80402b97c..2c72438b9e8dfd5f24bd409f50c8a65e7b0e27ed 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -169,7 +169,6 @@ "app.presentationUploder.browseImagesLabel": "or browse/capture for images", "app.presentationUploder.fileToUpload": "To be uploaded ...", "app.presentationUploder.currentBadge": "Current", - "app.presentationUploder.genericError": "Ops, something went wrong", "app.presentationUploder.rejectedError": "The selected file(s) have been rejected. Please check the file type(s).", "app.presentationUploder.upload.progress": "Uploading ({0}%)", "app.presentationUploder.upload.413": "File is too large. Please split into multiple files.", @@ -178,9 +177,12 @@ "app.presentationUploder.conversion.generatingThumbnail": "Generating thumbnails ...", "app.presentationUploder.conversion.generatedSlides": "Slides generated ...", "app.presentationUploder.conversion.generatingSvg": "Generating SVG images ...", - "app.presentationUploder.conversion.pageCountExceeded": "Ops, the page count exceeded the limit of 200 pages", + "app.presentationUploder.conversion.pageCountExceeded": "Number of pages exceeded. Please break file into multiple files.", + "app.presentationUploder.conversion.officeDocConversionInvalid": "Failed to process office document. Please upload a PDF instead.", + "app.presentationUploder.conversion.officeDocConversionFailed": "Failed to process office document. Please upload a PDF instead.", "app.presentationUploder.conversion.pdfHasBigPage": "We could not convert the PDF file, please try optimizing it", "app.presentationUploder.conversion.timeout": "Ops, the conversion took too long", + "app.presentationUploder.conversion.pageCountFailed": "Failed to determine the number of pages.", "app.presentationUploder.isDownloadableLabel": "Do not allow presentation to be downloaded", "app.presentationUploder.isNotDownloadableLabel": "Allow presentation to be downloaded", "app.presentationUploder.removePresentationLabel": "Remove presentation", diff --git a/bigbluebutton-html5/public/compatibility/sip.js b/bigbluebutton-html5/public/compatibility/sip.js index 235eae04a2c244c135d65b05158d8f92f86fb067..20be1c483812a3b3f5d74847072b7e1accb0a64a 100755 --- a/bigbluebutton-html5/public/compatibility/sip.js +++ b/bigbluebutton-html5/public/compatibility/sip.js @@ -11773,19 +11773,115 @@ MediaStreamManager.render = function render (streams, elements) { throw new TypeError('elements must not be empty'); } + function sendLog(logType, logCode, message, extraInfo) { + if (window.clientLogger) { + let logObject = {}; + if (logCode) logObject.logCode = logCode; + if (extraInfo) logObject.extraInfo = extraInfo; + + window.clientLogger[logType](logObject, message); + } + } + + function tryUsingAudioContext(element, stream) { + try { + mediaElement.pause(); + } catch (e) { + // Pausing is just an extra precaution, but we dont want failures to bubble up + } + + element.srcObject = null; + + var AudioContext = window.AudioContext || window.webkitAudioContext; + var audioContext = new AudioContext(); + + audioContext.createMediaStreamSource(stream).connect(audioContext.destination); + + audioContext.onstatechange = function() { + sendLog("info", "sipjs_audiocontext_state_change", `Audio context state change, new state: ${audioContext.state}`, {state: audioContext.state}); + }; + sendLog("info", "sipjs_audiocontext_initial_state", `The audio context is: ${audioContext.state}`, {state: audioContext.state}); + + audioContext.resume().then(() => { + sendLog("info", "sipjs_audiocontext_play_succcess", `The audio context resumed`); + }).catch(error => { + sendLog("info", "sipjs_audiocontext_play_error", `The audio context encountered an error on resume: ${error.name}`, {errorCode: error.name}); + + var savedStyle = document.getElementById("app").style; + document.getElementById("app").style = "display: none"; + document.body.style = "display: flex; align-items: center; width: 100%; justify-content: center"; + var promptDiv = document.createElement("DIV"); + promptDiv.style = "font-size: 1.5rem; display: flex; align-items: center; flex-direction: column; margin: 0.25rem"; + var promptLabel = document.createElement("DIV"); + promptLabel.innerHTML = "We need your approval to play the audio"; + promptLabel.style = "color: var(--color-off-white); margin: 0.25rem"; + promptDiv.appendChild(promptLabel); + var playButton = document.createElement("BUTTON"); + playButton.innerHTML = "Play"; + playButton.style = "background-color: var(--color-primary); color: var(--color-off-white); border-radius: 4px; border: none; padding: 4px 8px; margin: 0.25rem;"; + playButton.onclick = () => { + audioContext.resume(); + document.body.style = undefined; + document.getElementById("app").style = savedStyle; + document.body.removeChild(promptDiv); + window.dispatchEvent(new Event('resize')); + }; + promptDiv.appendChild(playButton); + document.body.appendChild(promptDiv) + }); + } + function attachMediaStream(element, stream) { element.srcObject = stream; } - function ensureMediaPlaying (mediaElement) { + function ensureMediaPlaying (mediaElement, stream) { let startPlayPromise = mediaElement.play(); if (startPlayPromise !== undefined) { + // There are cases (mainly Safari) where the Promise will never resolve with success or + // failure. A timeout is required to handle this case and fallback to trying to play the + // stream with an AudioContext instead (Web Audio API) + let fallenBack = false; + + let playTimeout = setTimeout(() => { + // If it fails log it and try to play with the audio context + // TODO: put log about falling back to AudioContext + sendLog("info", "sipjs_audioelement_play_timeout", `The audio element timed out on play`); + fallenBack = true; + tryUsingAudioContext(mediaElement, stream); + }, 500); + + function canPlayHandler(){ + mediaElement.removeEventListener('canplay', canPlayHandler); + + if (playTimeout) { + clearTimeout(playTimeout); + playTimeout = null; + } + } + mediaElement.addEventListener('canplay', canPlayHandler); + startPlayPromise.then(() => { - // Start whatever you need to do only after playback - // has begun. + // Start whatever you need to do only after playback has begun. + if (fallenBack) return; + + sendLog("info", "sipjs_audioelement_play_success", `The audio element played successfully`); + + if (playTimeout) { + clearTimeout(playTimeout); + playTimeout = null; + } }).catch(error => { + if (playTimeout) { + clearTimeout(playTimeout); + playTimeout = null; + } + if (error.name === "NotAllowedError") { + if (fallenBack) return; + sendLog("info", "sipjs_audioelement_play_error", `The audio element encountered an error on play: ${error.name}`, {errorCode: error.name}); + var savedStyle = document.getElementById("app").style; document.getElementById("app").style = "display: none"; document.body.style = "display: flex; align-items: center; width: 100%; justify-content: center"; @@ -11831,7 +11927,7 @@ MediaStreamManager.render = function render (streams, elements) { element = element(); } (environment.attachMediaStream || attachMediaStream)(element, stream); - ensureMediaPlaying(element); + ensureMediaPlaying(element, stream); } // [].concat "casts" `elements` into an array diff --git a/bigbluebutton-web/build.gradle b/bigbluebutton-web/build.gradle index 5dfb4f6434b5c3c4def4d98ee58b70f040281f97..51b1082fbfb2b4aa9f59b29b9ef2c681b8df01bc 100755 --- a/bigbluebutton-web/build.gradle +++ b/bigbluebutton-web/build.gradle @@ -76,7 +76,6 @@ dependencies { compile "org.freemarker:freemarker:2.3.28" compile "com.google.code.gson:gson:2.8.5" compile "org.json:json:20180813" - compile "org.apache.poi:poi-ooxml:3.17" compile "org.jodconverter:jodconverter-local:4.2.1" compile "com.zaxxer:nuprocess:1.2.4" compile "net.java.dev.jna:jna:4.5.1" diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties index 6bd5cacf22ab219903d62d8b71519b844b9e91ab..c613665c4275314ff66d51115752b8ce142b0514 100755 --- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties +++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties @@ -43,14 +43,19 @@ fontsDir=/usr/share/fonts # Executable for presentation checker presCheckExec=/usr/share/prescheck/prescheck.sh +#---------------------------------------------------- +# Skip Office doc conversion pre-check. Attempt to convert +# Office doc to PDF right away. +skipOfficePrecheck=true #---------------------------------------------------- # These will be copied in cases where the conversion process # fails to generate a slide from the uploaded presentation -BLANK_SLIDE=/var/bigbluebutton/blank/blank-slide.swf -BLANK_PRESENTATION=/var/bigbluebutton/blank/blank-presentation.pdf -BLANK_THUMBNAIL=/var/bigbluebutton/blank/blank-thumb.png -BLANK_PNG=/var/bigbluebutton/blank/blank-png.png +BLANK_SLIDE=/usr/share/bigbluebutton/blank/blank-slide.swf +BLANK_PRESENTATION=/usr/share/bigbluebutton/blank/blank-presentation.pdf +BLANK_THUMBNAIL=/usr/share/bigbluebutton/blank/blank-thumb.png +BLANK_PNG=/usr/share/bigbluebutton/blank/blank-png.png +BLANK_SVG=/usr/share/bigbluebutton/blank/blank-svg.svg #---------------------------------------------------- # Number of minutes the conversion should take. If it takes @@ -82,7 +87,12 @@ defineTextThreshold=2000 #------------------------------------ # Number of threads in the pool to do the presentation conversion. #------------------------------------ -numConversionThreads=2 +numConversionThreads=5 + +#------------------------------------ +# Number of threads to process file uploads +#------------------------------------ +numFileProcessorThreads=2 #---------------------------------------------------- # Conversion of the presentation slides to SWF to be @@ -111,8 +121,8 @@ maxImageSize=2000000 # Configuration for large PDF, 14 MB by default, if bigger it will be analysed during the conversion process bigPdfSize=14000000 -# The maximum allowed page size for PDF files exceeding the 'pdfCheckSize' value, 12 MB by default -maxBigPdfPageSize=12000000 +# The maximum allowed page size for PDF files exceeding the 'pdfCheckSize' value, 2 MB by default +maxBigPdfPageSize=2000000 #---------------------------------------------------- # Default dial access number @@ -193,7 +203,7 @@ autoStartRecording=false allowStartStopRecording=true # Allow webcams streaming reception only to and from moderators -webcamsOnlyForModerator=false +webcamsOnlyForModerator=false # Mute the meeting on start muteOnStart=false diff --git a/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml b/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml index 96b7bd1d77be51044373378654deea1ca846495c..6658ebc5ef3987a60141af5273dbbf3514b746ac 100755 --- a/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml +++ b/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml @@ -26,8 +26,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <bean id="documentConversionService" class="org.bigbluebutton.presentation.DocumentConversionServiceImp"> <property name="bbbWebApiGWApp" ref="bbbWebApiGWApp"/> <property name="officeToPdfConversionService" ref="officeToPdfConversionService"/> - <property name="pdfToSwfSlidesGenerationService" ref="pdfToSwfSlidesGenerationService"/> - <property name="imageToSwfSlidesGenerationService" ref="imageToSwfSlidesGenerationService"/> + <property name="presentationFileProcessor" ref="presentationFileProcessor"/> + <property name="swfSlidesGenerationProgressNotifier" ref="swfSlidesGenerationProgressNotifier"/> </bean> <bean id="officeDocumentValidator" class="org.bigbluebutton.presentation.imp.OfficeDocumentValidator2"> @@ -38,6 +38,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <bean id="officeToPdfConversionService" class="org.bigbluebutton.presentation.imp.OfficeToPdfConversionService" init-method="start" destroy-method="stop"> <property name="officeDocumentValidator" ref="officeDocumentValidator"/> + <property name="skipOfficePrecheck" value="${skipOfficePrecheck}"/> </bean> <bean id="pageExtractor" class="org.bigbluebutton.presentation.imp.PageExtractorImp"/> @@ -83,6 +84,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <property name="swfSlidesGenerationProgressNotifier" ref="swfSlidesGenerationProgressNotifier"/> <property name="imageTagThreshold" value="${imageTagThreshold}"/> <property name="pathsThreshold" value="${placementsThreshold}"/> + <property name="blankSvg" value="${BLANK_SVG}"/> </bean> <bean id="generatedSlidesInfoHelper" class="org.bigbluebutton.presentation.GeneratedSlidesInfoHelperImp"/> @@ -90,7 +92,12 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <bean id="pdfToSwfSlidesGenerationService" class="org.bigbluebutton.presentation.imp.PdfToSwfSlidesGenerationService"> <constructor-arg index="0" value="${numConversionThreads}"/> - <property name="counterService" ref="pageCounterService"/> + <property name="presentationConversionCompletionService" ref="presentationConversionCompletionService"/> + </bean> + + <bean id="presentationFileProcessor" + class="org.bigbluebutton.presentation.imp.PresentationFileProcessor"> + <constructor-arg index="0" value="${numFileProcessorThreads}"/> <property name="pageConverter" ref="pdf2SwfPageConverter"/> <property name="thumbnailCreator" ref="thumbCreator"/> <property name="pngCreator" ref="pngCreator"/> @@ -106,6 +113,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <property name="swfSlidesRequired" value="${swfSlidesRequired}"/> <property name="svgImagesRequired" value="${svgImagesRequired}"/> <property name="generatePngs" value="${generatePngs}"/> + <property name="presentationConversionCompletionService" ref="presentationConversionCompletionService"/> + <property name="imageToSwfSlidesGenerationService" ref="imageToSwfSlidesGenerationService"/> + <property name="counterService" ref="pageCounterService"/> + <property name="pdfToSwfSlidesGenerationService" ref="pdfToSwfSlidesGenerationService"/> </bean> <bean id="imageToSwfSlidesGenerationService" @@ -131,4 +142,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <property name="messagingService" ref="bbbWebApiGWApp"/> <property name="generatedSlidesInfoHelper" ref="generatedSlidesInfoHelper"/> </bean> + + <bean id="presentationConversionCompletionService" + class="org.bigbluebutton.presentation.imp.PresentationConversionCompletionService" + init-method="start" destroy-method="stop"> + <property name="swfSlidesGenerationProgressNotifier" ref="swfSlidesGenerationProgressNotifier"/> + </bean> + </beans> diff --git a/bigbluebutton-web/grails-app/conf/spring/resources.xml b/bigbluebutton-web/grails-app/conf/spring/resources.xml index c47424b390d525721b35299f6fd3b80668ed5667..ff695f99cd1ebeb348df2b1418102d7ade3c9b73 100755 --- a/bigbluebutton-web/grails-app/conf/spring/resources.xml +++ b/bigbluebutton-web/grails-app/conf/spring/resources.xml @@ -27,6 +27,12 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. http://www.springframework.org/schema/util/spring-util-2.0.xsd"> + <bean id="characterEncodingFilter" class="org.springframework.web.filter.CharacterEncodingFilter"> + <property name="encoding"> + <value>utf-8</value> + </property> + </bean> + <bean id="registeredUserCleanupTimerTask" class="org.bigbluebutton.web.services.RegisteredUserCleanupTimerTask"/> <bean id="waitingGuestCleanupTimerTask" class="org.bigbluebutton.web.services.WaitingGuestCleanupTimerTask"/> 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 7edf6eb130288b19b1ae60df835c213b88e03f0a..bb7f40b54585a5a4f4afbe86030efb209d20e794 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 @@ -2032,7 +2032,13 @@ class ApiController { fos.close() // Hardcode pre-uploaded presentation to the default presentation window - processUploadedFile("DEFAULT_PRESENTATION_POD", meetingId, presId, presFilename, pres, current); + processUploadedFile("DEFAULT_PRESENTATION_POD", + meetingId, + presId, + presFilename, + pres, + current, + "preupload-raw-authz-token"); } } @@ -2058,7 +2064,13 @@ class ApiController { if (presDownloadService.savePresentation(meetingId, newFilePath, address)) { def pres = new File(newFilePath) // Hardcode pre-uploaded presentation to the default presentation window - processUploadedFile("DEFAULT_PRESENTATION_POD", meetingId, presId, presFilename, pres, current); + processUploadedFile("DEFAULT_PRESENTATION_POD", + meetingId, + presId, + presFilename, + pres, + current, + "preupload-download-authz-token"); } else { log.error("Failed to download presentation=[${address}], meeting=[${meetingId}], fileName=[${fileName}]") } @@ -2066,10 +2078,16 @@ class ApiController { } - def processUploadedFile(podId, meetingId, presId, filename, presFile, current) { + def processUploadedFile(podId, meetingId, presId, filename, presFile, current, authzToken) { def presentationBaseUrl = presentationService.presentationBaseUrl // TODO add podId - UploadedPresentation uploadedPres = new UploadedPresentation(podId, meetingId, presId, filename, presentationBaseUrl, current); + UploadedPresentation uploadedPres = new UploadedPresentation(podId, + meetingId, + presId, + filename, + presentationBaseUrl, + current, + authzToken); uploadedPres.setUploadedFile(presFile); presentationService.processUploadedPresentation(uploadedPres); } diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy index 6c7f79b5d37b8c5cb28b065850b40285546772d0..edce746e1419a0dd82328109326718aac9ff93bb 100755 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy @@ -119,7 +119,8 @@ class PresentationController { log.debug("processing file upload " + presFilename) def presentationBaseUrl = presentationService.presentationBaseUrl UploadedPresentation uploadedPres = new UploadedPresentation(podId, meetingId, presId, - presFilename, presentationBaseUrl, false /* default presentation */); + presFilename, presentationBaseUrl, false /* default presentation */, + params.authzToken); if (isDownloadable) { log.debug "@Setting file to be downloadable..." diff --git a/bigbluebutton-web/pres-checker/build.gradle b/bigbluebutton-web/pres-checker/build.gradle index 8f1a36da33c10df66cf3dba14773f5e676d784f1..883a53906cdb91b59ed1ab808fb2eb736b67b38a 100755 --- a/bigbluebutton-web/pres-checker/build.gradle +++ b/bigbluebutton-web/pres-checker/build.gradle @@ -20,14 +20,14 @@ repositories { } dependencies { - compile 'org.apache.poi:poi:4.0.0' - compile 'org.apache.poi:poi-ooxml:4.0.0' - compile 'org.apache.poi:poi-ooxml-schemas:4.0.0' + compile 'org.apache.poi:poi:4.1.2' + compile 'org.apache.poi:poi-ooxml:4.1.2' + compile 'org.apache.poi:poi-ooxml-schemas:4.1.2' compile 'commons-io:commons-io:2.6' - compile 'org.apache.commons:commons-lang3:3.8.1' - compile 'org.apache.commons:commons-collections4:4.2' + compile 'org.apache.commons:commons-lang3:3.9' + compile 'org.apache.commons:commons-collections4:4.4' compile 'org.apache.xmlbeans:xmlbeans:3.0.2' - compile 'org.apache.commons:commons-compress:1.18' + compile 'org.apache.commons:commons-compress:1.20' } jar { diff --git a/record-and-playback/core/Gemfile.lock b/record-and-playback/core/Gemfile.lock index f23de03b5270d972874a732921a7552968bef5be..7d3453290f1f531df8d1f5776856496d9ede1119 100644 --- a/record-and-playback/core/Gemfile.lock +++ b/record-and-playback/core/Gemfile.lock @@ -23,7 +23,7 @@ GEM multi_json (1.14.1) mustermann (1.1.1) ruby2_keywords (~> 0.0.1) - nokogiri (1.10.7) + nokogiri (1.10.8) mini_portile2 (~> 2.4.0) open4 (1.3.4) parallel (1.19.1)