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 2f41cb3bca4538eadd8d55b4ad2d23bbc9efe36b..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(); @@ -63,16 +55,23 @@ public class DocumentConversionServiceImp implements DocumentConversionService { pres = officeToPdfConversionService.convertOfficeToPdf(pres); OfficeToPdfConversionSuccessFilter ocsf = new OfficeToPdfConversionSuccessFilter(gw); if (ocsf.didConversionSucceed(pres)) { + ocsf.sendProgress(pres); // Successfully converted to pdf. Call the process again, this time it // should be handled by // the PDF conversion service. processDocument(pres); + } else { + // Send notification that office to pdf conversion failed. + // The cause should have been set by the previous step. + // (ralam feb 15, 2020) + 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()); @@ -81,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()); @@ -95,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) { @@ -121,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/OfficeToPdfConversionSuccessFilter.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/OfficeToPdfConversionSuccessFilter.java index dab81a405b8a8f4b8548d4bdcbb4c0cdb108c530..5c4d48ca07961fd02f158418fa798a6038077a6d 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/OfficeToPdfConversionSuccessFilter.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/OfficeToPdfConversionSuccessFilter.java @@ -48,32 +48,19 @@ public class OfficeToPdfConversionSuccessFilter { } public boolean didConversionSucceed(UploadedPresentation pres) { - notifyProgressListener(pres); return ConversionMessageConstants.OFFICE_DOC_CONVERSION_SUCCESS_KEY.equals(pres.getConversionStatus()); } - private void notifyProgressListener(UploadedPresentation pres) { - Map<String, Object> msg = new HashMap<String, Object>(); - msg.put("conference", pres.getMeetingId()); - msg.put("room", pres.getMeetingId()); - msg.put("returnCode", "CONVERT"); - msg.put("presentationId", pres.getId()); - msg.put("podId", pres.getPodId()); - msg.put("presentationName", pres.getId()); - msg.put("filename", pres.getName()); - msg.put("message", conversionMessagesMap.get(pres.getConversionStatus())); - msg.put("messageKey", pres.getConversionStatus()); - - log.info("Notifying of {} for {}", pres.getConversionStatus(), pres.getUploadedFile().getAbsolutePath()); - sendProgress(pres); - } - - public void sendProgress(UploadedPresentation pres) { OfficeDocConversionProgress progress = new OfficeDocConversionProgress(pres.getPodId(), - pres.getMeetingId(),pres.getId(), pres.getId(), - pres.getName(), "notUsedYet", "notUsedYet", - pres.isDownloadable(), pres.getConversionStatus()); + pres.getMeetingId(), + pres.getId(), + pres.getId(), + pres.getName(), + "notUsedYet", + "notUsedYet", + pres.isDownloadable(), + pres.getConversionStatus()); gw.sendDocConversionMsg(progress); } 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 841582ec3541755836f5e1c22976ad921c339c57..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()); @@ -95,6 +96,8 @@ public class OfficeToPdfConversionService { Gson gson = new Gson(); String logStr = gson.toJson(logData); log.warn(" --analytics-- data={}", logStr); + pres.setConversionStatus(ConversionMessageConstants.OFFICE_DOC_CONVERSION_FAILED_KEY); + return pres; } } return pres; @@ -126,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-client/src/org/bigbluebutton/modules/phone/managers/WebRTCCallManager.as b/bigbluebutton-client/src/org/bigbluebutton/modules/phone/managers/WebRTCCallManager.as index 854b87f9d91c901f077fc6e76e2de7a36fc5b130..bcdc3b8ad0c9e9a5b37e58e70a02c69e58b89bb6 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/phone/managers/WebRTCCallManager.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/phone/managers/WebRTCCallManager.as @@ -26,6 +26,7 @@ package org.bigbluebutton.modules.phone.managers import org.bigbluebutton.modules.phone.models.WebRTCAudioStatus; import org.bigbluebutton.modules.phone.models.WebRTCModel; import org.bigbluebutton.util.i18n.ResourceUtil; + import org.bigbluebutton.util.browser.BrowserCheck; public class WebRTCCallManager { @@ -47,7 +48,9 @@ package org.bigbluebutton.modules.phone.managers options = Options.getOptions(PhoneOptions) as PhoneOptions; // only show the warning if the admin has enabled WebRTC - if (options.useWebRTCIfAvailable && !isWebRTCSupported()) { + // and don't show it in Puffin Browser, because it is used + // when no other browsers are available + if (options.useWebRTCIfAvailable && !isWebRTCSupported() && !BrowserCheck.isPuffin()) { dispatcher.dispatchEvent(new ClientStatusEvent(ClientStatusEvent.WARNING_MESSAGE_EVENT, ResourceUtil.getInstance().getString("bbb.clientstatus.webrtc.title"), ResourceUtil.getInstance().getString("bbb.clientstatus.webrtc.message"), diff --git a/bigbluebutton-client/src/org/bigbluebutton/util/browser/BrowserCheck.as b/bigbluebutton-client/src/org/bigbluebutton/util/browser/BrowserCheck.as index 743c13acfd5a1c32a3d87b21ff916c1974bc7a3c..e23f096b612dcaf33c687935a83756e77936fb6c 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/util/browser/BrowserCheck.as +++ b/bigbluebutton-client/src/org/bigbluebutton/util/browser/BrowserCheck.as @@ -76,6 +76,10 @@ package org.bigbluebutton.util.browser { return _browserName.toLowerCase() == "edge"; } + public static function isPuffin():Boolean { + return _browserName.toLowerCase() == "puffin"; + } + public static function isPuffinBelow46():Boolean { return _browserName.toLowerCase() == "puffin" && String(_fullVersion).substr(0, 3) < "4.6"; } diff --git a/bigbluebutton-config/bin/bbb-conf b/bigbluebutton-config/bin/bbb-conf index 821bca8497360b8d0cc40de46a2409203c09e23e..147c0d1cefc11ba602370141ddf690ad7b2a23d4 100755 --- a/bigbluebutton-config/bin/bbb-conf +++ b/bigbluebutton-config/bin/bbb-conf @@ -917,7 +917,26 @@ check_configuration() { echo fi fi +} +update_gstreamer() { + # due to a change in the kurento packages naming convention, gstreamer packages don't naturally upgrade + # this snippet checks if the installed gstreamer packages are the same as the one available in the repository + # if they are not, it will update (possibly downgrade) + # TODO remove it on 2.3 or above + if [ "$DISTRIB_CODENAME" == "xenial" ]; then + DOWNGRADE_LIST="" + for PACKAGE in $(dpkg -l | grep gst | tr -s ' ' | cut -d' ' -f2 | cut -d':' -f1); do + RIGHT_VERSION=$(apt-cache policy $PACKAGE | grep -B1 ubuntu.bigbluebutton.org | head -n1 | tr -s ' ' | cut -d' ' -f2) + if [[ $RIGHT_VERSION != "***" ]] && [[ $RIGHT_VERSION != "" ]]; then + echo "Force $PACKAGE to version $RIGHT_VERSION" + DOWNGRADE_LIST="$PACKAGE=$RIGHT_VERSION $DOWNGRADE_LIST" + fi + done + if [[ $DOWNGRADE_LIST != "" ]]; then + apt-get -y --allow-downgrades install $DOWNGRADE_LIST > /dev/null + fi + fi } @@ -1435,7 +1454,7 @@ check_state() { if systemctl status freeswitch | grep -q SETSCHEDULER; then echo "# Error: FreeSWITCH failed to start with SETSCHEDULER error, see" echo "#" - echo "# https://docs.bigbluebutton.org/install/install.html#freeswitch-fails-to-start-with-a-setscheduler-error" + echo "# http://docs.bigbluebutton.org/2.2/troubleshooting.html#freeswitch-fails-to-start-with-a-setscheduler-error" echo "#" fi @@ -1652,6 +1671,15 @@ if [ $CHECK ]; then echo " enableListenOnly: $(yq r $HTML5_CONFIG public.kurento.enableListenOnly)" fi + if ! java -version 2>&1 | grep -q "1.8.0"; then + echo + echo "# Warning: Did not detect Java 8 as default version" + echo + echo " sudo apt-get install openjdk-8-jdk" + echo " update-alternatives --config java" + echo " bbb-conf --restart" + fi + check_state echo @@ -1960,6 +1988,7 @@ if [ -n "$HOST" ]; then echo "Restarting the BigBlueButton $BIGBLUEBUTTON_RELEASE ..." stop_bigbluebutton + update_gstreamer echo start_bigbluebutton @@ -1974,6 +2003,7 @@ if [ $RESTART ]; then echo "Restarting BigBlueButton $BIGBLUEBUTTON_RELEASE ..." stop_bigbluebutton + update_gstreamer start_bigbluebutton check_state fi @@ -1985,6 +2015,7 @@ if [ $CLEAN ]; then echo "Restarting BigBlueButton $BIGBLUEBUTTON_RELEASE (and cleaning out all log files) ..." stop_bigbluebutton + update_gstreamer # # Clean log files diff --git a/bigbluebutton-config/cron.daily/bigbluebutton b/bigbluebutton-config/cron.daily/bigbluebutton index a754e68ac5445c71935dbc23472129372262c832..5db7732fefa93efe97cb6ff61fc358339c5dfcf5 100755 --- a/bigbluebutton-config/cron.daily/bigbluebutton +++ b/bigbluebutton-config/cron.daily/bigbluebutton @@ -59,9 +59,10 @@ for app in recordings screenshare; do done # -# Delete FreeSWITCH wav recordings older than N days +# Delete FreeSWITCH wav/opus recordings older than N days # -find /var/freeswitch/meetings/ -name "*.wav" -mtime +$history -delete +find /var/freeswitch/meetings/ -name "*.wav" -mtime +$history -delete +find /var/freeswitch/meetings/ -name "*.opus" -mtime +$history -delete # # Delete old/rotated log files 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-config/web/css/style.css b/bigbluebutton-config/web/css/style.css old mode 100755 new mode 100644 index 0eb9749d162440f313d4dfcdc2f98b9a8c5fc470..f8533a6cb6844169a97ec483e50f63a0fb1f44f7 --- a/bigbluebutton-config/web/css/style.css +++ b/bigbluebutton-config/web/css/style.css @@ -108,7 +108,7 @@ input { height: auto; text-align: center; z-index: 100; - color: #fff; + color: #edf4fb; font-size: 40px; } diff --git a/bigbluebutton-config/web/images/bbb-logo.png b/bigbluebutton-config/web/images/bbb-logo.png index 3737f7c5773ebbca34a14ccd00560893c3c3166a..6b8412e87966b8dce560c5984195f1bf72266942 100644 Binary files a/bigbluebutton-config/web/images/bbb-logo.png and b/bigbluebutton-config/web/images/bbb-logo.png differ diff --git a/bigbluebutton-config/web/images/blankoverview.png b/bigbluebutton-config/web/images/blankoverview.png new file mode 100644 index 0000000000000000000000000000000000000000..4104c66e9614f781e131f4d23b6d23a94a8fab95 Binary files /dev/null and b/bigbluebutton-config/web/images/blankoverview.png differ diff --git a/bigbluebutton-config/web/index.html b/bigbluebutton-config/web/index.html index 3e8f53eed8ced5d50813c2edd16b0e40d4917229..0e40e8c7f28b1034874871bb32b688bfcf76f944 100644 --- a/bigbluebutton-config/web/index.html +++ b/bigbluebutton-config/web/index.html @@ -1,3 +1,4 @@ + <!DOCTYPE html> <html> <head> @@ -34,7 +35,7 @@ <div class='navbar'> <div class='container'> <div class="logo"> - <img src="images/bbb-logo.png" alt="BigBlueButton Demo"/> + <img src="images/bbb-logo.png" width="240" alt="BigBlueButton Demo"/> </div> </div> </div> @@ -51,7 +52,8 @@ <div class='span six html5clientOnly input-center'> <div class='join-meeting '> <h4>Try BigBlueButton</h4> - <p>(no app required)</p> + <p>(requires <a href="http://docs.bigbluebutton.org/2.2/install.html#5-install-api-demos-optional">API demos</a> to be installed)</p> + <form name="form1" method="GET" onsubmit="return checkform(this);" action="/demo/demoHTML5.jsp"> <input type="text" id="username" required="" name="username" placeholder="Enter Your Name" size="29" class="field input-default" autofocus> @@ -60,7 +62,8 @@ <input type="hidden" name="action" value="create"> </form> - <a class="watch" href="https://www.youtube.com/watch?v=d5v6Uar79Yc" class="pull-right">Watch overview of BigBlueButton (YouTube)</a> + <a class="watch" href="https://www.youtube.com/watch?v=uYYnryIM0Uw;feature=youtu.be" class="pull-right">Viewer Overview</a> - + <a class="watch" href="https://www.youtube.com/watch?v=Q2tG2SS4gXA&feature=youtu.be" class="pull-right">Moderator Overview</a> </div> </div> @@ -71,13 +74,12 @@ <div class='row'> <div > <h2>Built for online learning</h2> - <p> BigBlueButton is an open source web conferencing system built for on-line learning.</p> - <p>BigBlueButton provides all the core capabilities you expect in a web conferencing system: real-time sharing of audio, video, presentation, and screen. BigBlueButton gives you many ways to engage users with interactive chat (public and private), multi-user whiteboard, shared notes, emojis, polling, and breakout rooms. You can record any session for later playback.</p> - <p> BigBlueButton client is pure <a href="https://www.youtube.com/watch?v=d5v6Uar79Yc">HTML5 client</a>. There is no app to install. The HTML5 client runs on all devices: laptop, desktop, chromebook and mobile (Android 6.0+ and iOS 12.2+). The HTML5 client uses the browser's built-in support for <a href="https://webrtc.org/">web real-time communications (WebRTC)</a> to share audio, video, and screen with other users. We recommend Chrome and FireFox as they have the best support for WebRTC.</p> + <p> BigBlueButton is an open source web conferencing system for on-line learning.</p> + <p> BigBlueButton gives you all the core features you would expect from a commercial web conferencing system (but under an open source license), including real-time sharing of audio, video, presentation, and screen – along with collaboration tools such as multi-user whiteboard, shared notes, polling, public/private chat, emojis, and breakout rooms. BigBlueButton can record your sessions for later playback. </p> + <p> BigBlueButton runs on desktop and laptop computers witin latest version of Chrome, FireFox, or Edge browser. There is no binary to download, no application to install. BigBlueButton runs within Safari Mobile (default browser) on iOS version 12.2+, and the Chrome browser on Android version 6.0+. BigBlueButton also runs on Chromebooks using the Chrome browser.</p> + + <p> For more information visit our <a href="https://bigbluebutton.org/">website</a>, <a href="https://docs.bigbluebutton.org/">documentation</a>, and <a href="https://demo.bigbluebutton.org/">demo server</a>.</p> - <h4>For Developers</h4> - <p> Under the hood, the HTML5 client uses the React for a responsive experience. The BigBlueButton project is <a href="http://bigbluebutton.org/support">supported</a> by a community of developers that care about good design and a streamlined user experience. See <a href="http://docs.bigbluebutton.org" target="_blank">our documentation site</a> for more information on how you can integrate BigBlueButton with your project.</p> - </div> <div class="span one"></div> </div> @@ -107,7 +109,7 @@ <i class="fa fa-pencil-square-o"></i> <div class="bbb-features-content"> - <h3>Whiteboard</h3> + <h3><a href="https://youtu.be/Q2tG2SS4gXA?t=190">Whiteboard</a></h3> <p>The whiteboard controls let you annotate key parts of your presentation.</p> </div> </div> @@ -116,8 +118,8 @@ <i class="fa fa-desktop"></i> <div class="bbb-features-content"> - <h3><a href="https://youtu.be/xTFuEvmEqB0">Desktop Sharing</a></h3> - <p>You can broadcast your desktop for all users to see (requires lastest version of Java for presenter only).</p> + <h3><a href="https://youtu.be/Q2tG2SS4gXA?t=331">Screen Sharing</a></h3> + <p>You can broadcast your desktop for all users to see.</p> </div> </div> </div> @@ -128,7 +130,7 @@ <i class="fa fa-microphone"></i> <div class="bbb-features-content"> - <h3><a href="https://youtu.be/4Y__UsUrRx0">WebRTC Audio</a></h3> + <h3><a href="https://youtu.be/uYYnryIM0Uw?t=4">Audio</a></h3> <p>Users of Chrome and FireFox browsers will benefit from high-quality, low-latency WebRTC audio.</p> </div> </div> @@ -146,7 +148,7 @@ <i class="fa fa-video-camera"></i> <div class="bbb-features-content"> - <h3>Web Cam</h3> + <h3><a href="https://youtu.be/uYYnryIM0Uw?t=117">Webcam</a></h3> <p>Multiple users can share their webcam at the same time. There is no built-in limit on the number of simultaneously active webcams.</p> </div> </div> @@ -157,7 +159,7 @@ <i class="fa fa-smile-o"></i> <div class="bbb-features-content"> - <h3>Emoji</h3> + <h3><a href="https://youtu.be/uYYnryIM0Uw?t=107">Emoji</a></h3> <p>Students can raise hand and use emoji icons for feedback.</p> </div> </div> @@ -166,7 +168,7 @@ <i class="fa fa-check-square-o"></i> <div class="bbb-features-content"> - <h3>Polling</h3> + <h3><a href="https://youtu.be/Q2tG2SS4gXA">Polling</a></h3> <p>You can poll students anytime to increase engagement.</p> </div> </div> @@ -175,7 +177,7 @@ <i class="fa fa-comments-o"></i> <div class="bbb-features-content"> - <h3>Chat</h3> + <h3><a href="https://youtu.be/uYYnryIM0Uw?t=50">Chat</a></h3> <p>You can interact with students through public and private chat.</p> </div> </div> @@ -186,7 +188,7 @@ <i class="fa fa-cc"></i> <div class="bbb-features-content"> - <h3><a href="https://youtu.be/vDpurrMgal0">Live Captioning</a></h3> + <h3><a href="https://youtu.be/feC_zm1y3N4">Live Captioning</a></h3> <p>You can enter live captions for students. These captions will later appear as subtitles in recordings.</p> </div> </div> @@ -195,7 +197,7 @@ <i class="fa fa-users"></i> <div class="bbb-features-content"> - <h3><a href="https://youtu.be/q5N-lcocJss">Breakout Rooms</a></h3> + <h3><a href="https://youtu.be/Q2tG2SS4gXA?t=126">Breakout Rooms</a></h3> <p>You can group and place students into breakout rooms (full BigBlueButton sessions) for give number of minutes for increased collaboration.</p> </div> </div> @@ -221,27 +223,27 @@ </div> <div class='span four first video-item'> - <a href="https://www.youtube.com/watch?v=4Y__UsUrRx0&feature=youtu.be" target="_blank"> + <a href="https://www.youtube.com/watch?v=uYYnryIM0Uw" target="_blank"> <div class="video-btn"><i class="fa fa-play-circle-o"></i></div> - <img src="images/bbb-setup-audio.jpg" alt="Setting Up Audio"/> + <img src="images/bbb-viewer-overview.png" alt="BigBlueButton Viewer Overview Video"/> </a> - <h3><a href="https://www.youtube.com/watch?v=4Y__UsUrRx0&feature=youtu.be" title="Setup Audio" target="_blank">Setting Up Audio</a></h3> + <h3><a href="https://www.youtube.com/watch?v=uYYnryIM0Uw;feature=youtu.be" title="Viewer Overview" target="_blank">Viewer Overview</a></h3> </div> <div class='span four video-item'> - <a href="https://www.youtube.com/watch?v=uYYnryIM0Uw" target="_blank"> + <a href="https://www.youtube.com/watch?v=Q2tG2SS4gXA&feature=youtu.be" target="_blank"> <div class="video-btn"><i class="fa fa-play-circle-o"></i></div> - <img src="images/bbb-viewer-overview.png" alt="BigBlueButton Viewer Overview Video"/> + <img src="images/bbb-presenter-overview.png" alt="Moderator/Presenter Overview Video"/> </a> - <h3><a href="https://www.youtube.com/watch?v=uYYnryIM0Uw;feature=youtu.be" title="Student Overview" target="_blank">Viewer Overview</a></h3> + <h3><a href="https://www.youtube.com/watch?v=Q2tG2SS4gXA&feature=youtu.be" title="Moderator/Presenter Overview" target="_blank">Moderator/Presenter Overview</a></h3> </div> <div class='span four last video-item'> - <a href="https://www.youtube.com/watch?v=Q2tG2SS4gXA&feature=youtu.be" target="_blank"> + <a href="https://bigbluebutton.org/videos" target="_blank"> <div class="video-btn"><i class="fa fa-play-circle-o"></i></div> - <img src="images/bbb-presenter-overview.png" alt="Moderator/Presenter Overview Video"/> + <img src="images/blankoverview.png" alt="More videos"/> </a> - <h3><a href="https://www.youtube.com/watch?v=Q2tG2SS4gXA&feature=youtu.be" title="Moderator/Presenter Overview" target="_blank">Moderator/Presenter Overview</a></h3> + <h3><a href="https://bigbluebutton.org/videos" title="More Videos" target="_blank">More Videos</a></h3> </div> </div> </div> @@ -272,8 +274,8 @@ <div class="row"> <div class="span twelve center"> - <p>Copyright © 2019 BigBlueButton Inc.<br> - <small>Version <a href="http://docs.bigbluebutton.org/">2.2-RC</a></small> + <p>Copyright © 2020 BigBlueButton Inc.<br> + <small>Version <a href="http://docs.bigbluebutton.org/">2.2</a></small> </p> </div> </div> @@ -281,4 +283,3 @@ </footer> </body> </html> - diff --git a/bigbluebutton-html5/.meteor/packages b/bigbluebutton-html5/.meteor/packages index acae08d1c9c2d318a7fa9b0d9057b05207fee0ce..92645a8900c9b7ea3d8a77039561d9fb15f81852 100644 --- a/bigbluebutton-html5/.meteor/packages +++ b/bigbluebutton-html5/.meteor/packages @@ -20,11 +20,9 @@ http@1.4.2 session@1.2.0 tracker@1.2.0 check@1.3.1 -jquery nathantreid:css-modules@4.1.0 rocketchat:streamer cfs:power-queue cfs:micro-queue cfs:reactive-list -stevezhu:lodash diff --git a/bigbluebutton-html5/.meteor/versions b/bigbluebutton-html5/.meteor/versions index f069c4fed58c4806c6974659b2f249222da5713b..1990d472ff56f4cf641809868a518884a13e707e 100644 --- a/bigbluebutton-html5/.meteor/versions +++ b/bigbluebutton-html5/.meteor/versions @@ -35,7 +35,6 @@ htmljs@1.0.11 http@1.4.2 id-map@1.1.0 inter-process-messaging@0.1.0 -jquery@1.11.11 launch-screen@1.1.1 livedata@1.0.18 logging@1.1.20 @@ -72,7 +71,6 @@ spacebars-compiler@1.1.3 standard-minifier-css@1.6.0 standard-minifier-js@2.6.0 static-html@1.2.2 -stevezhu:lodash@4.17.2 templating-tools@1.1.2 tmeasday:check-npm-versions@0.3.2 tracker@1.2.0 diff --git a/bigbluebutton-html5/client/main.html b/bigbluebutton-html5/client/main.html index 245455cbde6cf7335f07899df34d0199b964889b..fe5209cf13a23c11cddf8928c3f726afd6fbe1eb 100755 --- a/bigbluebutton-html5/client/main.html +++ b/bigbluebutton-html5/client/main.html @@ -1,3 +1,21 @@ +<!-- +BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + +Copyright (c) 2019 BigBlueButton Inc. and by respective authors (see below). + +This program is free software; you can redistribute it and/or modify it under the +terms of the GNU Lesser General Public License as published by the Free Software +Foundation; either version 3.0 of the License, or (at your option) any later +version. + +BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. +--> + <head> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <style> diff --git a/bigbluebutton-html5/client/main.jsx b/bigbluebutton-html5/client/main.jsx index 893beb2ef487f68a6cab61a63b747cae56a47ce8..ad0759f96748fc341389164a42c291fbcf35de19 100755 --- a/bigbluebutton-html5/client/main.jsx +++ b/bigbluebutton-html5/client/main.jsx @@ -1,3 +1,20 @@ +/* + BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + + Copyright (c) 2019 BigBlueButton Inc. and by respective authors (see below). + + This program is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3.0 of the License, or (at your option) any later + version. + + BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along + with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. +*/ /* eslint no-unused-vars: 0 */ import React from 'react'; import { Meteor } from 'meteor/meteor'; diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/clearWhiteboard.js b/bigbluebutton-html5/imports/api/annotations/server/methods/clearWhiteboard.js index 25de55fe320e2f982333b3d0fe876a1a0681cb76..5c146c7c8e7a0584916a0bdd33892ea0cacff3ca 100644 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/clearWhiteboard.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods/clearWhiteboard.js @@ -1,28 +1,17 @@ -import { getMultiUserStatus } from '/imports/api/common/server/helpers'; import RedisPubSub from '/imports/startup/server/redis'; import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -import isPodPresenter from '/imports/api/presentation-pods/server/utils/isPodPresenter'; - -export default function clearWhiteboard(credentials, whiteboardId) { +export default function clearWhiteboard(whiteboardId) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'ClearWhiteboardPubMsg'; - const { meetingId, requesterUserId, requesterToken } = credentials; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); check(whiteboardId, String); - const allowed = isPodPresenter(meetingId, whiteboardId, requesterUserId) - || getMultiUserStatus(meetingId, whiteboardId); - if (!allowed) { - throw new Meteor.Error('not-allowed', `User ${requesterUserId} is not allowed to clear the whiteboard`); - } - const payload = { whiteboardId, }; diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotation.js b/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotation.js index 9700c6415f4b876ba8afcd5fae72f8d8d0e0213d..daa01d8f2410f4f8d5d6d2d94f3f122d86e99257 100755 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotation.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotation.js @@ -1,105 +1,8 @@ -import { getMultiUserStatus } from '/imports/api/common/server/helpers'; -import RedisPubSub from '/imports/startup/server/redis'; -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import Annotations from '/imports/api/annotations'; +import sendAnnotationHelper from './sendAnnotationHelper'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -import isPodPresenter from '/imports/api/presentation-pods/server/utils/isPodPresenter'; +export default function sendAnnotation(annotation) { + const { meetingId, requesterUserId } = extractCredentials(this.userId); -function isLastMessage(meetingId, annotation, userId) { - const DRAW_END = Meteor.settings.public.whiteboard.annotations.status.end; - - if (annotation.status === DRAW_END) { - const selector = { - meetingId, - id: annotation.id, - userId, - }; - - const _annotation = Annotations.findOne(selector); - return _annotation !== null; - } - - return false; -} - -export default function sendAnnotation(credentials, annotation) { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const EVENT_NAME = 'SendWhiteboardAnnotationPubMsg'; - - const { meetingId, requesterUserId, requesterToken } = credentials; - const whiteboardId = annotation.wbId; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); - check(annotation, Object); - check(whiteboardId, String); - - // We allow messages to pass through in 3 cases: - // 1. When it's a standard message in presenter mode (Acl check) - // 2. When it's a standard message in multi-user mode (getMultUserStatus check) - // 3. When it's the last message, happens when the user is currently drawing - // and then slide/presentation changes, the user lost presenter rights, - // or multi-user whiteboard gets turned off - // So we allow the last "DRAW_END" message to pass through, to finish the shape. - const allowed = isPodPresenter(meetingId, whiteboardId, requesterUserId) - || getMultiUserStatus(meetingId, whiteboardId) - || isLastMessage(meetingId, annotation, requesterUserId); - - if (!allowed) { - throw new Meteor.Error('not-allowed', `User ${requesterUserId} is not allowed to send an annotation`); - } - - if (annotation.annotationType === 'text') { - check(annotation, { - id: String, - status: String, - annotationType: String, - annotationInfo: { - x: Number, - y: Number, - fontColor: Number, - calcedFontSize: Number, - textBoxWidth: Number, - text: String, - textBoxHeight: Number, - id: String, - whiteboardId: String, - status: String, - fontSize: Number, - dataPoints: String, - type: String, - }, - wbId: String, - userId: String, - position: Number, - }); - } else { - check(annotation, { - id: String, - status: String, - annotationType: String, - annotationInfo: { - color: Number, - thickness: Number, - points: Array, - id: String, - whiteboardId: String, - status: String, - type: String, - dimensions: Match.Maybe([Number]), - }, - wbId: String, - userId: String, - position: Number, - }); - } - - const payload = { - annotation, - }; - - return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + sendAnnotationHelper(annotation, meetingId, requesterUserId); } diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotationHelper.js b/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotationHelper.js new file mode 100755 index 0000000000000000000000000000000000000000..88ef045cfa74a8b0542ab8392b009fb574405a55 --- /dev/null +++ b/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotationHelper.js @@ -0,0 +1,65 @@ +import RedisPubSub from '/imports/startup/server/redis'; +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; + +export default function sendAnnotationHelper(annotation, meetingId, requesterUserId) { + const REDIS_CONFIG = Meteor.settings.private.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'SendWhiteboardAnnotationPubMsg'; + + const whiteboardId = annotation.wbId; + + check(annotation, Object); + check(whiteboardId, String); + + if (annotation.annotationType === 'text') { + check(annotation, { + id: String, + status: String, + annotationType: String, + annotationInfo: { + x: Number, + y: Number, + fontColor: Number, + calcedFontSize: Number, + textBoxWidth: Number, + text: String, + textBoxHeight: Number, + id: String, + whiteboardId: String, + status: String, + fontSize: Number, + dataPoints: String, + type: String, + }, + wbId: String, + userId: String, + position: Number, + }); + } else { + check(annotation, { + id: String, + status: String, + annotationType: String, + annotationInfo: { + color: Number, + thickness: Number, + points: Array, + id: String, + whiteboardId: String, + status: String, + type: String, + dimensions: Match.Maybe([Number]), + }, + wbId: String, + userId: String, + position: Number, + }); + } + + const payload = { + annotation, + }; + + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); +} diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/sendBulkAnnotations.js b/bigbluebutton-html5/imports/api/annotations/server/methods/sendBulkAnnotations.js index eb17b4cee795ebe741959538b0a25a462428c2db..fd4530e744775d2f3430b70912cb6460d1270bd1 100644 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/sendBulkAnnotations.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods/sendBulkAnnotations.js @@ -1,9 +1,8 @@ -import { check } from 'meteor/check'; -import sendAnnotation from './sendAnnotation'; +import { extractCredentials } from '/imports/api/common/server/helpers'; +import sendAnnotationHelper from './sendAnnotationHelper'; -export default function sendBulkAnnotations(credentials, payload) { - check(credentials, Object); - check(payload, [Object]); +export default function sendBulkAnnotations(payload) { + const { meetingId, requesterUserId } = extractCredentials(this.userId); - payload.forEach(annotation => sendAnnotation(credentials, annotation)); + payload.forEach(annotation => sendAnnotationHelper(annotation, meetingId, requesterUserId)); } diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/undoAnnotation.js b/bigbluebutton-html5/imports/api/annotations/server/methods/undoAnnotation.js index a55397de53a443879b77bb1732237ef8d2867159..bcab1c2b3509fd2b100d57c62d5e06e6cf2a92f6 100644 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/undoAnnotation.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods/undoAnnotation.js @@ -1,29 +1,17 @@ -import { getMultiUserStatus } from '/imports/api/common/server/helpers'; import RedisPubSub from '/imports/startup/server/redis'; import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -import isPodPresenter from '/imports/api/presentation-pods/server/utils/isPodPresenter'; - -export default function undoAnnotation(credentials, whiteboardId) { +export default function undoAnnotation(whiteboardId) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'UndoWhiteboardPubMsg'; - const { meetingId, requesterUserId, requesterToken } = credentials; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); check(whiteboardId, String); - const allowed = isPodPresenter(meetingId, whiteboardId, requesterUserId) - || getMultiUserStatus(meetingId, whiteboardId); - - if (!allowed) { - throw new Meteor.Error('not-allowed', `User ${requesterUserId} is not allowed to undo the annotation`); - } - const payload = { whiteboardId, }; diff --git a/bigbluebutton-html5/imports/api/annotations/server/publishers.js b/bigbluebutton-html5/imports/api/annotations/server/publishers.js index da757739ed5040603ecb480a6dc67659b565a18f..d79333f70415f73362dcbd0116e0c86f0439de63 100644 --- a/bigbluebutton-html5/imports/api/annotations/server/publishers.js +++ b/bigbluebutton-html5/imports/api/annotations/server/publishers.js @@ -1,16 +1,16 @@ import Annotations from '/imports/api/annotations'; import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -function annotations(credentials) { - const { meetingId, requesterUserId, requesterToken } = credentials; +function annotations() { + if (!this.userId) { + return Annotations.find({ meetingId: '' }); + } - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); - Logger.debug(`Publishing Annotations for ${meetingId} ${requesterUserId} ${requesterToken}`); + Logger.debug(`Publishing Annotations for ${meetingId} ${requesterUserId}`); return Annotations.find({ meetingId }); } diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js index 8b2b07528f9cd4affe0b763aa4a79bd3cd04174c..3c72abbfcbe045a12abb64b1446c11629dbbae82 100755 --- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js +++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js @@ -10,6 +10,10 @@ import { analyzeSdp, logSelectedCandidate, } from '/imports/utils/sdpUtils'; +import { Tracker } from 'meteor/tracker'; +import VoiceCallStates from '/imports/api/voice-call-states'; +import CallStateOptions from '/imports/api/voice-call-states/utils/callStates'; +import Auth from '/imports/ui/services/auth'; const MEDIA = Meteor.settings.public.media; const MEDIA_TAG = MEDIA.mediaTag; @@ -47,14 +51,6 @@ class SIPSession { this.reconnectAttempt = reconnectAttempt; } - static parseDTMF(message) { - const parse = message.match(/Signal=(.)/); - if (parse && parse.length === 2) { - return parse[1]; - } - return ''; - } - joinAudio({ isListenOnly, extension, inputStream }, managerCallback) { return new Promise((resolve, reject) => { const callExtension = extension ? `${extension}${this.userData.voiceBridge}` : this.userData.voiceBridge; @@ -119,8 +115,10 @@ class SIPSession { return new Promise((resolve, reject) => { this.inEchoTest = false; - const timeout = setInterval(() => { - clearInterval(timeout); + let trackerControl = null; + + const timeout = setTimeout(() => { + trackerControl.stop(); logger.error({ logCode: 'sip_js_transfer_timed_out' }, 'Timeout on transferring from echo test to conference'); this.callback({ status: this.baseCallStates.failed, @@ -136,15 +134,22 @@ class SIPSession { // This is is the call transfer code ask @chadpilkey this.currentSession.dtmf(1); - this.currentSession.on('dtmf', (event) => { - if (event.body && (typeof event.body === 'string')) { - const key = SIPSession.parseDTMF(event.body); - if (key === '7') { - clearInterval(timeout); - onTransferSuccess(); - resolve(); - } - } + Tracker.autorun((c) => { + trackerControl = c; + const selector = { meetingId: Auth.meetingID, userId: Auth.userID }; + const query = VoiceCallStates.find(selector); + + query.observeChanges({ + changed: (id, fields) => { + if (fields.callState === CallStateOptions.IN_CONFERENCE) { + clearTimeout(timeout); + onTransferSuccess(); + + c.stop(); + resolve(); + } + }, + }); }); }); } @@ -491,17 +496,22 @@ class SIPSession { }; ['iceConnectionClosed'].forEach(e => mediaHandler.on(e, handleIceConnectionTerminated)); - const inEchoDTMF = (event) => { - if (event.body && typeof event.body === 'string') { - const dtmf = SIPSession.parseDTMF(event.body); - if (dtmf === '0') { - fsReady = true; - checkIfCallReady(); - } - } - currentSession.off('dtmf', inEchoDTMF); - }; - currentSession.on('dtmf', inEchoDTMF); + Tracker.autorun((c) => { + const selector = { meetingId: Auth.meetingID, userId: Auth.userID }; + const query = VoiceCallStates.find(selector); + + query.observeChanges({ + changed: (id, fields) => { + if ((this.inEchoTest && fields.callState === CallStateOptions.IN_ECHO_TEST) + || (!this.inEchoTest && fields.callState === CallStateOptions.IN_CONFERENCE)) { + fsReady = true; + checkIfCallReady(); + + c.stop(); + } + }, + }); + }); }); } } @@ -534,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/methods/createBreakout.js b/bigbluebutton-html5/imports/api/breakouts/server/methods/createBreakout.js index 8fbc22740f6cdfa73aa5e4d4f080a0cf81afd2cb..2b069675ef3fdfc0b0a3e3340998071f89152f45 100644 --- a/bigbluebutton-html5/imports/api/breakouts/server/methods/createBreakout.js +++ b/bigbluebutton-html5/imports/api/breakouts/server/methods/createBreakout.js @@ -1,21 +1,13 @@ 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 { extractCredentials } from '/imports/api/common/server/helpers'; -export default function createBreakoutRoom(credentials, rooms, durationInMinutes, record = false) { +export default function createBreakoutRoom(rooms, durationInMinutes, record = false) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const { - meetingId, - requesterUserId, - requesterToken, - } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); const eventName = 'CreateBreakoutRoomsCmdMsg'; if (rooms.length > 8) return Logger.info(`Attempt to create breakout rooms with invalid number of rooms in meeting id=${meetingId}`); diff --git a/bigbluebutton-html5/imports/api/breakouts/server/methods/endAllBreakouts.js b/bigbluebutton-html5/imports/api/breakouts/server/methods/endAllBreakouts.js index 4753fc6685e1ad2fa72f7b52600b4a10e0f8516e..ff309659130c7ced4dcecb05b507bcbb9e24f4a1 100644 --- a/bigbluebutton-html5/imports/api/breakouts/server/methods/endAllBreakouts.js +++ b/bigbluebutton-html5/imports/api/breakouts/server/methods/endAllBreakouts.js @@ -1,20 +1,16 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function endAllBreakouts(credentials) { +export default function endAllBreakouts() { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const { - meetingId, - requesterUserId, - requesterToken, - } = credentials; + const { meetingId, requesterUserId } = extractCredentials(this.userId); check(meetingId, String); check(requesterUserId, String); - check(requesterToken, String); const eventName = 'EndAllBreakoutRoomsMsg'; return RedisPubSub.publishUserMessage(CHANNEL, eventName, meetingId, requesterUserId, null); diff --git a/bigbluebutton-html5/imports/api/breakouts/server/methods/requestJoinURL.js b/bigbluebutton-html5/imports/api/breakouts/server/methods/requestJoinURL.js index f2da4d7d3208eeec9217d34d6477e0b569484530..ace15db4b87d59c80e7e9616b6b26cec95d49054 100755 --- a/bigbluebutton-html5/imports/api/breakouts/server/methods/requestJoinURL.js +++ b/bigbluebutton-html5/imports/api/breakouts/server/methods/requestJoinURL.js @@ -1,16 +1,13 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function requestJoinURL(credentials, { breakoutId, userId: userIdToInvite }) { +export default function requestJoinURL({ breakoutId, userId: userIdToInvite }) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const { meetingId, requesterUserId, requesterToken } = credentials; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); const userId = userIdToInvite || requesterUserId; const eventName = 'RequestBreakoutJoinURLReqMsg'; diff --git a/bigbluebutton-html5/imports/api/breakouts/server/publishers.js b/bigbluebutton-html5/imports/api/breakouts/server/publishers.js index dae21d5126e60a03ec0491f0ebe82ca81831bc5b..d0040bd87ecc475f1a1475dc82ef96ba822ff568 100755 --- a/bigbluebutton-html5/imports/api/breakouts/server/publishers.js +++ b/bigbluebutton-html5/imports/api/breakouts/server/publishers.js @@ -2,18 +2,20 @@ import { Meteor } from 'meteor/meteor'; import Breakouts from '/imports/api/breakouts'; import Users from '/imports/api/users'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; -function breakouts(credentials, moderator = false) { - const { - meetingId, - requesterUserId, - } = credentials; +function breakouts(moderator = false) { + if (!this.userId) { + return Breakouts.find({ meetingId: '' }); + } + + const { meetingId, requesterUserId } = extractCredentials(this.userId); 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/captions/server/methods/takeOwnership.js b/bigbluebutton-html5/imports/api/captions/server/methods/takeOwnership.js index e541239a83a2c2bf7e2acbf7e363594e1150c0f4..51b176a2a61ebeb5c2a9136281535e169653b009 100644 --- a/bigbluebutton-html5/imports/api/captions/server/methods/takeOwnership.js +++ b/bigbluebutton-html5/imports/api/captions/server/methods/takeOwnership.js @@ -1,12 +1,11 @@ import { check } from 'meteor/check'; import Captions from '/imports/api/captions'; import updateOwnerId from '/imports/api/captions/server/modifiers/updateOwnerId'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function takeOwnership(credentials, locale) { - const { meetingId, requesterUserId } = credentials; +export default function takeOwnership(locale) { + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); check(locale, String); const pad = Captions.findOne({ meetingId, padId: { $regex: `_captions_${locale}$` } }); diff --git a/bigbluebutton-html5/imports/api/captions/server/methods/updateOwner.js b/bigbluebutton-html5/imports/api/captions/server/methods/updateOwner.js index 2262e818af7e563f808510a048a77a9fce2f484f..3608ec308511463fd09d72813e2b33f3cd969175 100644 --- a/bigbluebutton-html5/imports/api/captions/server/methods/updateOwner.js +++ b/bigbluebutton-html5/imports/api/captions/server/methods/updateOwner.js @@ -4,7 +4,7 @@ import Logger from '/imports/startup/server/logger'; import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -export default function editCaptions(meetingId, userId, padId) { +export default function editCaptions(meetingId, userId, padId) { // TODO const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'UpdateCaptionOwnerPubMsg'; diff --git a/bigbluebutton-html5/imports/api/captions/server/publishers.js b/bigbluebutton-html5/imports/api/captions/server/publishers.js index 245954527e1aa4f6839650ebfc89c8f9341398a8..fc4ce428297ba74496f08a4501f7aa7e0514c142 100644 --- a/bigbluebutton-html5/imports/api/captions/server/publishers.js +++ b/bigbluebutton-html5/imports/api/captions/server/publishers.js @@ -1,13 +1,13 @@ import Captions from '/imports/api/captions'; import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -function captions(credentials) { - const { meetingId } = credentials; - - check(meetingId, String); - +function captions() { + if (!this.userId) { + return Captions.find({ meetingId: '' }); + } + const { meetingId } = extractCredentials(this.userId); Logger.debug(`Publishing Captions for ${meetingId}`); return Captions.find({ meetingId }); diff --git a/bigbluebutton-html5/imports/api/common/server/helpers.js b/bigbluebutton-html5/imports/api/common/server/helpers.js index d4208347344d6ad4fba2bd339ea4bd517285500d..99720cbdb8594cda151eee7b2cda1893e847e65f 100755 --- a/bigbluebutton-html5/imports/api/common/server/helpers.js +++ b/bigbluebutton-html5/imports/api/common/server/helpers.js @@ -1,4 +1,3 @@ -import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user/'; import Users from '/imports/api/users'; const MSG_DIRECT_TYPE = 'DIRECT'; @@ -39,17 +38,6 @@ export const processForHTML5ServerOnly = fn => (message, ...args) => { return fn(message, ...args); }; - -export const getMultiUserStatus = (meetingId, whiteboardId) => { - const data = WhiteboardMultiUser.findOne({ meetingId, whiteboardId }); - - if (data) { - return data.multiUser; - } - - return false; -}; - /** * Calculate a 32 bit FNV-1a hash * Found here: https://gist.github.com/vaiorabbit/5657561 @@ -75,3 +63,11 @@ export const hashFNV32a = (str, asString, seed) => { return hval >>> 0; }; /* eslint-enable */ + +export const extractCredentials = (credentials) => { + if (!credentials) return {}; + const credentialsArray = credentials.split('--'); + const meetingId = credentialsArray[0]; + const requesterUserId = credentialsArray[1]; + return { meetingId, requesterUserId }; +}; 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/cursor/server/methods/publishCursorUpdate.js b/bigbluebutton-html5/imports/api/cursor/server/methods/publishCursorUpdate.js index c5690c00dee77492bcf34882f9baad419e55ccc3..0ee0dbc411ddcc3697c39a9ca2682634dce087e0 100755 --- a/bigbluebutton-html5/imports/api/cursor/server/methods/publishCursorUpdate.js +++ b/bigbluebutton-html5/imports/api/cursor/server/methods/publishCursorUpdate.js @@ -1,38 +1,10 @@ -import { getMultiUserStatus } from '/imports/api/common/server/helpers'; import RedisPubSub from '/imports/startup/server/redis'; import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import isPodPresenter from '/imports/api/presentation-pods/server/utils/isPodPresenter'; - -export default function publishCursorUpdate(credentials, payload) { +export default function publishCursorUpdate(meetingId, requesterUserId, payload) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'SendCursorPositionPubMsg'; - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); - check(payload, { - xPercent: Number, - yPercent: Number, - whiteboardId: String, - }); - - const { - whiteboardId, - xPercent, - yPercent, - } = payload; - - const allowed = isPodPresenter(meetingId, whiteboardId, requesterUserId) - || getMultiUserStatus(meetingId, whiteboardId) - || (xPercent < 0 && yPercent < 0); - if (!allowed) { - throw new Meteor.Error('not-allowed', `User ${requesterUserId} is not allowed to move the cursor`); - } - return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); } diff --git a/bigbluebutton-html5/imports/api/cursor/server/streamer.js b/bigbluebutton-html5/imports/api/cursor/server/streamer.js index 4c3218960b71813ef6b108f3fb04f9d5c9a7d95d..d8a869c48ccf1d4ef1f7f510db4a918275f4b709 100644 --- a/bigbluebutton-html5/imports/api/cursor/server/streamer.js +++ b/bigbluebutton-html5/imports/api/cursor/server/streamer.js @@ -26,7 +26,7 @@ export function addCursorStreamer(meetingId) { }); streamer.on('publish', (message) => { - publishCursorUpdate(message.credentials, message.payload); + publishCursorUpdate(meetingId, message.userId, message.payload); }); } 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/destroyExternalVideo.js b/bigbluebutton-html5/imports/api/external-videos/server/methods/destroyExternalVideo.js index cd2b1772e37a6e3f2d1ae4bd8e60c9808dfdcf64..0be97c31985c1188cec4c5a9c228ed4eaf9a0352 100644 --- a/bigbluebutton-html5/imports/api/external-videos/server/methods/destroyExternalVideo.js +++ b/bigbluebutton-html5/imports/api/external-videos/server/methods/destroyExternalVideo.js @@ -8,4 +8,4 @@ export default function destroyExternalVideo(meetingId) { Logger.info(`Destroying External Video streamer object for ${streamName}`); delete Meteor.StreamerCentral.instances[streamName]; } -}; +} 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 459d99bd9be2f19fb9fd6592fe71c869924d9f41..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,35 +1,38 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import Users from '/imports/api/users'; +import { extractCredentials } from '/imports/api/common/server/helpers'; 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(credentials) { - const { meetingId } = credentials; - - check(meetingId, String); +export default function initializeExternalVideo() { + const { meetingId } = extractCredentials(this.userId); const streamName = `external-videos-${meetingId}`; 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/startWatchingExternalVideo.js b/bigbluebutton-html5/imports/api/external-videos/server/methods/startWatchingExternalVideo.js index 407d8a2b0903f037979c9528eca0f70f24ed42a0..e157b7a0b3e0ede194b9d16fa76152a59633bc1b 100644 --- a/bigbluebutton-html5/imports/api/external-videos/server/methods/startWatchingExternalVideo.js +++ b/bigbluebutton-html5/imports/api/external-videos/server/methods/startWatchingExternalVideo.js @@ -3,17 +3,16 @@ import { check } from 'meteor/check'; 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 startWatchingExternalVideo(credentials, options) { +export default function startWatchingExternalVideo(options) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'StartExternalVideoMsg'; - const { meetingId, requesterUserId } = credentials; + const { meetingId, requesterUserId } = extractCredentials(this.userId); const { externalVideoUrl } = options; - check(meetingId, String); - check(requesterUserId, String); check(externalVideoUrl, String); Meetings.update({ meetingId }, { $set: { externalVideoUrl } }); 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 67f6eef54a5c6b4a588720d8612aa08037f94826..60384b82f772230d68d5c5399008f7de9e780610 100644 --- a/bigbluebutton-html5/imports/api/external-videos/server/methods/stopWatchingExternalVideo.js +++ b/bigbluebutton-html5/imports/api/external-videos/server/methods/stopWatchingExternalVideo.js @@ -1,18 +1,19 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; 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(credentials) { +export default function stopWatchingExternalVideo(options) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'StopExternalVideoMsg'; - const { meetingId, requesterUserId } = credentials; + if (this.userId) { + options = extractCredentials(this.userId); + } - check(meetingId, String); - check(requesterUserId, String); + const { meetingId, requesterUserId } = options; const meeting = Meetings.findOne({ meetingId }); if (!meeting || meeting.externalVideoUrl === null) return; diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/handlers/userTyping.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/handlers/userTyping.js index 24456ec5476f88cf4525128a1bcd502bd2fae506..e90006a4d2d0a97ac793698e0751c5d16db03f34 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/handlers/userTyping.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/handlers/userTyping.js @@ -1,5 +1,4 @@ import { check } from 'meteor/check'; -import { UsersTyping } from '/imports/api/group-chat-msg'; import startTyping from '../modifiers/startTyping'; export default function handleUserTyping({ body }, meetingId) { diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/clearPublicChatHistory.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/clearPublicChatHistory.js index 1044404b39f7508f1d6f3cd5596d56626b983b94..df88282d41d3fe5d86172de81dabf8413eddc708 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/clearPublicChatHistory.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/clearPublicChatHistory.js @@ -1,19 +1,15 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function clearPublicChatHistory(credentials) { +export default function clearPublicChatHistory() { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'ClearPublicChatHistoryPubMsg'; const CHAT_CONFIG = Meteor.settings.public.chat; const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); const payload = { chatId: PUBLIC_GROUP_CHAT_ID, diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/sendGroupChatMsg.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/sendGroupChatMsg.js index 5d4aa6882e926786bdbd298a2257c431126e0096..f486cdfe6c56ee17e8867fcd804b49119dceb2f8 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/sendGroupChatMsg.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/sendGroupChatMsg.js @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; import RegexWebUrl from '/imports/utils/regex-weburl'; +import { extractCredentials } from '/imports/api/common/server/helpers'; const HTML_SAFE_MAP = { '<': '<', @@ -26,16 +27,13 @@ const parseMessage = (message) => { return parsedMessage; }; -export default function sendGroupChatMsg(credentials, chatId, message) { +export default function sendGroupChatMsg(chatId, message) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'SendGroupChatMessageMsg'; - const { meetingId, requesterUserId, requesterToken } = credentials; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); check(message, Object); const parsedMessage = parseMessage(message.message); diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/startUserTyping.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/startUserTyping.js index 4b88e07055e5f1ed5f219ac370dd90260f8e696d..b46288d80e6d27f07a02eff1862cf328ff5c4a93 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/startUserTyping.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/startUserTyping.js @@ -1,17 +1,16 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function startUserTyping(credentials, chatId) { +export default function startUserTyping(chatId) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'UserTypingPubMsg'; const CHAT_CONFIG = Meteor.settings.public.chat; const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; - const { meetingId, requesterUserId } = credentials; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); check(chatId, String); const payload = { diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/stopUserTyping.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/stopUserTyping.js index 582f1d95de3f294e8d7babc4d68990c3098a6786..12cdaefa07a4b6f28cf004a798aeb71fab4619a1 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/stopUserTyping.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/stopUserTyping.js @@ -1,12 +1,9 @@ -import { check } from 'meteor/check'; import { UsersTyping } from '/imports/api/group-chat-msg'; import stopTyping from '../modifiers/stopTyping'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function stopUserTyping(credentials) { - const { meetingId, requesterUserId } = credentials; - - check(meetingId, String); - check(requesterUserId, String); +export default function stopUserTyping() { + const { meetingId, requesterUserId } = extractCredentials(this.userId); const userTyping = UsersTyping.findOne({ meetingId, diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js index cc9282cdfe1e5d3da6192560e5bb14ecb7f060e7..0f29669348112ed3a1622bea42bac1c609f8e5f8 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js @@ -1,20 +1,19 @@ import { GroupChatMsg, UsersTyping } from '/imports/api/group-chat-msg'; import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -function groupChatMsg(credentials, chatsIds) { - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); +function groupChatMsg(chatsIds) { + if (!this.userId) { + return GroupChatMsg.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); const CHAT_CONFIG = Meteor.settings.public.chat; const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; - Logger.debug(`Publishing group-chat-msg for ${meetingId} ${requesterUserId} ${requesterToken}`); + Logger.debug(`Publishing group-chat-msg for ${meetingId} ${requesterUserId}`); return GroupChatMsg.find({ $or: [ @@ -31,12 +30,14 @@ function publish(...args) { Meteor.publish('group-chat-msg', publish); -function usersTyping(credentials) { - const { meetingId, requesterUserId, requesterToken } = credentials; +function usersTyping() { + if (!this.userId) { + return UsersTyping.find({ meetingId: '' }); + } + + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); + Logger.debug(`Publishing users-typing for ${meetingId} ${requesterUserId}`); return UsersTyping.find({ meetingId }); } diff --git a/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js b/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js index df2bc90d48d3d8fca5c6ab1f7703e414399f12e3..2244289cc84a9300b87a3dd5a548b9da73f51d4a 100644 --- a/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js +++ b/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js @@ -2,18 +2,15 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; import { CHAT_ACCESS_PRIVATE } from '/imports/api/group-chat'; +import { extractCredentials } from '/imports/api/common/server/helpers'; - -export default function createGroupChat(credentials, receiver) { +export default function createGroupChat(receiver) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'CreateGroupChatReqMsg'; - const { meetingId, requesterUserId, requesterToken } = credentials; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); check(receiver, Object); const payload = { diff --git a/bigbluebutton-html5/imports/api/group-chat/server/methods/destroyGroupChat.js b/bigbluebutton-html5/imports/api/group-chat/server/methods/destroyGroupChat.js index 99a0fa481a15a52243505dbe64fb5935aac11f12..fc221e22298862947b674abf9a7fcad246013022 100644 --- a/bigbluebutton-html5/imports/api/group-chat/server/methods/destroyGroupChat.js +++ b/bigbluebutton-html5/imports/api/group-chat/server/methods/destroyGroupChat.js @@ -1,15 +1,11 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function createGroupChat(credentials) { +export default function createGroupChat() { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); const eventName = 'DestroyGroupChatReqMsg'; diff --git a/bigbluebutton-html5/imports/api/group-chat/server/publishers.js b/bigbluebutton-html5/imports/api/group-chat/server/publishers.js index d967f1310309a624e3d2388cbcef1024840cc7a3..f03ad0586ae0eb28675eefe2fb663083190b5acb 100644 --- a/bigbluebutton-html5/imports/api/group-chat/server/publishers.js +++ b/bigbluebutton-html5/imports/api/group-chat/server/publishers.js @@ -1,20 +1,19 @@ import GroupChat from '/imports/api/group-chat'; import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -function groupChat(credentials) { - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); +function groupChat() { + if (!this.userId) { + return GroupChat.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); const CHAT_CONFIG = Meteor.settings.public.chat; const PUBLIC_CHAT_TYPE = CHAT_CONFIG.type_public; - Logger.debug(`Publishing group-chat for ${meetingId} ${requesterUserId} ${requesterToken}`); + Logger.debug(`Publishing group-chat for ${meetingId} ${requesterUserId}`); return GroupChat.find({ $or: [ diff --git a/bigbluebutton-html5/imports/api/guest-users/server/methods/allowPendingUsers.js b/bigbluebutton-html5/imports/api/guest-users/server/methods/allowPendingUsers.js index 090ad1662127b0d7f5302bacd8e96081074f33f3..55837d56c238e3365086de887f057e202a31d66d 100644 --- a/bigbluebutton-html5/imports/api/guest-users/server/methods/allowPendingUsers.js +++ b/bigbluebutton-html5/imports/api/guest-users/server/methods/allowPendingUsers.js @@ -2,21 +2,15 @@ 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 { extractCredentials } from '/imports/api/common/server/helpers'; const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'GuestsWaitingApprovedMsg'; -export default function allowPendingUsers(credentials, guests, status) { - const { - meetingId, - requesterUserId, - requesterToken, - } = credentials; +export default function allowPendingUsers(guests, status) { + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); check(guests, Array); const mappedGuests = guests.map(guest => ({ status, guest: guest.intId })); @@ -24,7 +18,6 @@ export default function allowPendingUsers(credentials, guests, status) { approvedBy: requesterUserId, guests: mappedGuests, }; - Logger.info(`User=${requesterUserId} ${status} guests ${JSON.stringify(mappedGuests)}`); diff --git a/bigbluebutton-html5/imports/api/guest-users/server/methods/changeGuestPolicy.js b/bigbluebutton-html5/imports/api/guest-users/server/methods/changeGuestPolicy.js index 638993955884b635d7e5bc09971341e0e385949b..d0f7e140e56f5cfbe9230da8364d7bab2faa04db 100644 --- a/bigbluebutton-html5/imports/api/guest-users/server/methods/changeGuestPolicy.js +++ b/bigbluebutton-html5/imports/api/guest-users/server/methods/changeGuestPolicy.js @@ -2,21 +2,15 @@ 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 { extractCredentials } from '/imports/api/common/server/helpers'; const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'SetGuestPolicyCmdMsg'; -export default function changeGuestPolicy(credentials, policyRule) { - const { - meetingId, - requesterUserId, - requesterToken, - } = credentials; +export default function changeGuestPolicy(policyRule) { + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); check(policyRule, String); const payload = { diff --git a/bigbluebutton-html5/imports/api/guest-users/server/publishers.js b/bigbluebutton-html5/imports/api/guest-users/server/publishers.js index de3f8fb14b785b7d390b25c6530774ab58a74589..23e41d51bcf9c48cf08af105b767415447bd1d1f 100644 --- a/bigbluebutton-html5/imports/api/guest-users/server/publishers.js +++ b/bigbluebutton-html5/imports/api/guest-users/server/publishers.js @@ -1,16 +1,16 @@ import GuestUsers from '/imports/api/guest-users/'; import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -function guestUsers(credentials) { - const { meetingId, requesterUserId, requesterToken } = credentials; +function guestUsers() { + if (!this.userId) { + return GuestUsers.find({ meetingId: '' }); + } - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); - Logger.info(`Publishing Slides for ${meetingId} ${requesterUserId} ${requesterToken}`); + Logger.info(`Publishing Slides for ${meetingId} ${requesterUserId}`); return GuestUsers.find({ meetingId }); } diff --git a/bigbluebutton-html5/imports/api/local-settings/server/methods/userChangedLocalSettings.js b/bigbluebutton-html5/imports/api/local-settings/server/methods/userChangedLocalSettings.js index 032263d7bf191678cf135f2b480d37740dfb1409..4cb6a5d5615a4d4b73cd8847c4998fce2e8a15ef 100644 --- a/bigbluebutton-html5/imports/api/local-settings/server/methods/userChangedLocalSettings.js +++ b/bigbluebutton-html5/imports/api/local-settings/server/methods/userChangedLocalSettings.js @@ -2,14 +2,13 @@ import _ from 'lodash'; import { check } from 'meteor/check'; import LocalSettings from '/imports/api/local-settings'; import setChangedLocalSettings from '../modifiers/setChangedLocalSettings'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function userChangedLocalSettings(credentials, settings) { - const { meetingId, requesterUserId } = credentials; +export default function userChangedLocalSettings(settings) { + const { meetingId, requesterUserId } = extractCredentials(this.userId); if (!meetingId || !requesterUserId) return; - check(meetingId, String); - check(requesterUserId, String); check(settings, Object); const userLocalSettings = LocalSettings diff --git a/bigbluebutton-html5/imports/api/local-settings/server/publishers.js b/bigbluebutton-html5/imports/api/local-settings/server/publishers.js index 74a581589530b6886b21a3e7e44693a538e90be3..b492c801f3011dc3fe4d4a6a6dab3445d24c3a02 100644 --- a/bigbluebutton-html5/imports/api/local-settings/server/publishers.js +++ b/bigbluebutton-html5/imports/api/local-settings/server/publishers.js @@ -1,13 +1,13 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import LocalSettings from '/imports/api/local-settings'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -function localSettings(credentials) { - const { meetingId, requesterUserId } = credentials; - - check(meetingId, String); - check(requesterUserId, String); +function localSettings() { + if (!this.userId) { + return LocalSettings.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); Logger.debug(`Publishing local settings for user=${requesterUserId}`); diff --git a/bigbluebutton-html5/imports/api/log-client/server/methods/logClient.js b/bigbluebutton-html5/imports/api/log-client/server/methods/logClient.js index 837c8042bb3cc4be9f79586e5e46216203de6cea..d709aa1d7069c09e9e151196aa3c09f2fb0ef193 100755 --- a/bigbluebutton-html5/imports/api/log-client/server/methods/logClient.js +++ b/bigbluebutton-html5/imports/api/log-client/server/methods/logClient.js @@ -11,7 +11,7 @@ const logClient = function (type, logDescription, logCode = 'was_not_provided', userInfo, }; - if (User) { + if (User) { // TODO-- if ((userInfo.credentials && User.meetingId === userInfo.credentials.meetingId) || ((userInfo.meetingId && User.meetingId === userInfo.meetingId))) { logContents.extraInfo.validUser = 'valid'; diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/endMeeting.js b/bigbluebutton-html5/imports/api/meetings/server/methods/endMeeting.js index d482dee1f02abcdac9d2f907d30628b6894ca039..2d8811e8fb6b358f68e1b32225d9a3c70acd2560 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/methods/endMeeting.js +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/endMeeting.js @@ -1,34 +1,18 @@ 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 Users from '/imports/api/users'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function endMeeting(credentials) { +export default function endMeeting() { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'LogoutAndEndMeetingCmdMsg'; - const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); - - const selector = { - meetingId, + const payload = { userId: requesterUserId, }; - const user = Users.findOne(selector); - - if (!!user && user.role === ROLE_MODERATOR) { - const payload = { - userId: requesterUserId, - }; - - Logger.verbose(`Meeting '${meetingId}' is destroyed by '${requesterUserId}'`); + Logger.verbose(`Meeting '${meetingId}' is destroyed by '${requesterUserId}'`); - return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - } + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); } diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/toggleLockSettings.js b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleLockSettings.js index 823840320736a9f12500ee0c6e5e7a1f216d45f4..152d6039475279dc27b176159542a641e54bb043 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/methods/toggleLockSettings.js +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleLockSettings.js @@ -1,16 +1,14 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function toggleLockSettings(credentials, lockSettingsProps) { +export default function toggleLockSettings(lockSettingsProps) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'ChangeLockSettingsInMeetingCmdMsg'; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - const { meetingId, requesterUserId } = credentials; - - check(meetingId, String); - check(requesterUserId, String); check(lockSettingsProps, { disableCam: Boolean, disableMic: Boolean, diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/toggleRecording.js b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleRecording.js index 4d708c0564453178e0a8b9ae71c6ca7ca7b5cc15..0bf20e5211018b2a2e48b5bcbcedb8f1d2fe9e60 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/methods/toggleRecording.js +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleRecording.js @@ -1,20 +1,16 @@ -import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; import { Meteor } from 'meteor/meteor'; import RedisPubSub from '/imports/startup/server/redis'; import { RecordMeetings } from '/imports/api/meetings'; import Users from '/imports/api/users'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function toggleRecording(credentials) { +export default function toggleRecording() { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); const EVENT_NAME = 'SetRecordingStatusCmdMsg'; let meetingRecorded; @@ -30,7 +26,7 @@ export default function toggleRecording(credentials) { } = recordObject; meetingRecorded = recording; - allowedToRecord = record && allowStartStopRecording; + allowedToRecord = record && allowStartStopRecording; // TODO-- remove some day } const payload = { diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/toggleWebcamsOnlyForModerator.js b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleWebcamsOnlyForModerator.js index 94935c51a76c87decedb9482d1b7b0f9c83f9bc2..93553bb128a0c5c15f49bb8400e0029a8361390e 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/methods/toggleWebcamsOnlyForModerator.js +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/toggleWebcamsOnlyForModerator.js @@ -1,16 +1,14 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; +import { extractCredentials } from '../../../common/server/helpers'; -export default function toggleWebcamsOnlyForModerator(credentials, webcamsOnlyForModerator) { +export default function toggleWebcamsOnlyForModerator(webcamsOnlyForModerator) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'UpdateWebcamsOnlyForModeratorCmdMsg'; - const { meetingId, requesterUserId } = credentials; - - check(meetingId, String); - check(requesterUserId, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); check(webcamsOnlyForModerator, Boolean); const payload = { diff --git a/bigbluebutton-html5/imports/api/meetings/server/methods/transferUser.js b/bigbluebutton-html5/imports/api/meetings/server/methods/transferUser.js index 6841e1147138b1d9bd1dabefa61a9965247640a4..628267300ec84c7fcc0909ea617c4ac9f76e22a2 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/methods/transferUser.js +++ b/bigbluebutton-html5/imports/api/meetings/server/methods/transferUser.js @@ -1,18 +1,15 @@ 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 { extractCredentials } from '/imports/api/common/server/helpers'; -export default function transferUser(credentials, fromMeetingId, toMeetingId) { + +export default function transferUser(fromMeetingId, toMeetingId) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'TransferUserToMeetingRequestMsg'; - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); const payload = { fromMeetingId, diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js index f51f6aa75b111678920cc5a1f084cc7c1dc069b6..6b245da80711ee14f28dc679f2f2c86e1b27a00e 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js @@ -19,6 +19,7 @@ import clearNote from '/imports/api/note/server/modifiers/clearNote'; import clearNetworkInformation from '/imports/api/network-information/server/modifiers/clearNetworkInformation'; import clearLocalSettings from '/imports/api/local-settings/server/modifiers/clearLocalSettings'; import clearRecordMeeting from './clearRecordMeeting'; +import clearVoiceCallStates from '/imports/api/voice-call-states/server/modifiers/clearVoiceCallStates'; export default function meetingHasEnded(meetingId) { removeAnnotationsStreamer(meetingId); @@ -40,6 +41,7 @@ export default function meetingHasEnded(meetingId) { clearNetworkInformation(meetingId); clearLocalSettings(meetingId); clearRecordMeeting(meetingId); + clearVoiceCallStates(meetingId); return Logger.info(`Cleared Meetings with id ${meetingId}`); }); diff --git a/bigbluebutton-html5/imports/api/meetings/server/publishers.js b/bigbluebutton-html5/imports/api/meetings/server/publishers.js index af0012b942b25575e8efa72c777732ce5c9f7b3a..997c79b66b4b158e2c50b38c7a09401c42934fa1 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/publishers.js +++ b/bigbluebutton-html5/imports/api/meetings/server/publishers.js @@ -1,19 +1,18 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import Meetings, { RecordMeetings, MeetingTimeRemaining } from '/imports/api/meetings'; import Users from '/imports/api/users'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; -function meetings(credentials, isModerator = false) { - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); +function meetings(isModerator = false) { + if (!this.userId) { + return Meetings.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); - Logger.debug(`Publishing meeting =${meetingId} ${requesterUserId} ${requesterToken}`); + Logger.debug(`Publishing meeting =${meetingId} ${requesterUserId}`); const selector = { $or: [ @@ -22,7 +21,7 @@ function meetings(credentials, 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, @@ -47,11 +46,11 @@ function publish(...args) { Meteor.publish('meetings', publish); -function recordMeetings(credentials) { - const { meetingId, requesterUserId, requesterToken } = credentials; - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); +function recordMeetings() { + if (!this.userId) { + return RecordMeetings.find({ meetingId: '' }); + } + const { meetingId } = extractCredentials(this.userId); return RecordMeetings.find({ meetingId }); } @@ -62,11 +61,11 @@ function recordPublish(...args) { Meteor.publish('record-meetings', recordPublish); -function meetingTimeRemaining(credentials) { - const { meetingId, requesterUserId, requesterToken } = credentials; - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); +function meetingTimeRemaining() { + if (!this.userId) { + return MeetingTimeRemaining.find({ meetingId: '' }); + } + const { meetingId } = extractCredentials(this.userId); return MeetingTimeRemaining.find({ meetingId }); } diff --git a/bigbluebutton-html5/imports/api/network-information/server/methods/userInstabilityDetected.js b/bigbluebutton-html5/imports/api/network-information/server/methods/userInstabilityDetected.js index 790058e8e08ebdc9b4ea37c11ac6be2c138cdfd8..0d7955e4e72c5b250ed078d866ac738aaf7a6f0f 100644 --- a/bigbluebutton-html5/imports/api/network-information/server/methods/userInstabilityDetected.js +++ b/bigbluebutton-html5/imports/api/network-information/server/methods/userInstabilityDetected.js @@ -1,12 +1,10 @@ import { check } from 'meteor/check'; import NetworkInformation from '/imports/api/network-information'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function userInstabilityDetected(credentials, sender) { - const { meetingId, requesterUserId: receiver } = credentials; - - check(meetingId, String); - check(receiver, String); +export default function userInstabilityDetected(sender) { + const { meetingId, requesterUserId: receiver } = extractCredentials(this.userId); check(sender, String); const payload = { diff --git a/bigbluebutton-html5/imports/api/network-information/server/publisher.js b/bigbluebutton-html5/imports/api/network-information/server/publisher.js index 0e83b5cb9e987e80eb03f395f64fbf57a6aa4062..cef87a33ea2fc2ddd925884c80dbae699d893674 100644 --- a/bigbluebutton-html5/imports/api/network-information/server/publisher.js +++ b/bigbluebutton-html5/imports/api/network-information/server/publisher.js @@ -1,11 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import NetworkInformation from '/imports/api/network-information'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -function networkInformation(credentials) { - const { meetingId } = credentials; - - check(meetingId, String); +function networkInformation() { + if (!this.userId) { + return NetworkInformation.find({ meetingId: '' }); + } + const { meetingId } = extractCredentials(this.userId); return NetworkInformation.find({ meetingId, diff --git a/bigbluebutton-html5/imports/api/note/server/publishers.js b/bigbluebutton-html5/imports/api/note/server/publishers.js index 2728b80f28722f925bdf797ef48d9bb3de7257e2..41092f671fe40b8f976bbee32afc028f2340c5d1 100644 --- a/bigbluebutton-html5/imports/api/note/server/publishers.js +++ b/bigbluebutton-html5/imports/api/note/server/publishers.js @@ -1,16 +1,15 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; import Note from '/imports/api/note'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -function note(credentials) { - const { meetingId, requesterUserId, requesterToken } = credentials; +function note() { + if (!this.userId) { + return Note.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); - - Logger.info(`Publishing note for ${meetingId} ${requesterUserId} ${requesterToken}`); + Logger.info(`Publishing note for ${meetingId} ${requesterUserId}`); return Note.find({ meetingId }); } diff --git a/bigbluebutton-html5/imports/api/ping-pong/server/methods/ping.js b/bigbluebutton-html5/imports/api/ping-pong/server/methods/ping.js index 4b4e3e880cb03ca20478176d00aab6a6d19c9be9..7fcd87b81c8e401d9b155045cdfce8dd5bcf9ceb 100644 --- a/bigbluebutton-html5/imports/api/ping-pong/server/methods/ping.js +++ b/bigbluebutton-html5/imports/api/ping-pong/server/methods/ping.js @@ -1,13 +1,9 @@ -import { check } from 'meteor/check'; import Users from '/imports/api/users'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function ping(credentials) { - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); +export default function ping() { + const { meetingId, requesterUserId } = extractCredentials(this.userId); const selector = { meetingId, diff --git a/bigbluebutton-html5/imports/api/ping-pong/server/publishers.js b/bigbluebutton-html5/imports/api/ping-pong/server/publishers.js index 972393535e504ce3757c38326dd3a2f8544c96b0..bacb6ed8f38033d964e5b61def6e7876c1f23e74 100644 --- a/bigbluebutton-html5/imports/api/ping-pong/server/publishers.js +++ b/bigbluebutton-html5/imports/api/ping-pong/server/publishers.js @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; import Logger from '/imports/startup/server/logger'; import _ from 'lodash'; +import { extractCredentials } from '/imports/api/common/server/helpers'; const COLLECTION_NAME = 'ping-pong'; const INTERVAL_IN_SETTINGS = (Meteor.settings.public.pingPong.clearUsersInSeconds) * 1000; @@ -9,8 +10,11 @@ const PONG_INTERVAL_IN_SETTINGS = (Meteor.settings.public.pingPong.pongTimeInSec const PONG_INTERVAL = PONG_INTERVAL_IN_SETTINGS >= (INTERVAL_TIME / 2) ? (INTERVAL_TIME / 2) : PONG_INTERVAL_IN_SETTINGS; -function pingPong(credentials) { - const { meetingId, requesterUserId } = credentials; +function pingPong() { + if (!this.userId) { + return; // TODO-- is there a more appropriate set to return? + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); const id = _.uniqueId('pong-'); Logger.info(`Starting ping-pong publish for userId: ${requesterUserId}`); const pongSender = (interval) => { diff --git a/bigbluebutton-html5/imports/api/polls/server/methods/publishPoll.js b/bigbluebutton-html5/imports/api/polls/server/methods/publishPoll.js index b2f40f9fae17b4b8753b4af1f29f539c1916858a..701bea3b5c46f585c95bd0ac947bb23c903ae5a4 100644 --- a/bigbluebutton-html5/imports/api/polls/server/methods/publishPoll.js +++ b/bigbluebutton-html5/imports/api/polls/server/methods/publishPoll.js @@ -1,18 +1,15 @@ import RedisPubSub from '/imports/startup/server/redis'; -import { check } from 'meteor/check'; import Polls from '/imports/api/polls'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function publishPoll(credentials) { - const { meetingId, requesterUserId } = credentials; +export default function publishPoll() { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'ShowPollResultReqMsg'; - check(meetingId, String); - check(requesterUserId, String); - - const poll = Polls.findOne({ meetingId }); + const { meetingId, requesterUserId } = extractCredentials(this.userId); + const poll = Polls.findOne({ meetingId }); // TODO--send pollid from client if (!poll) { Logger.error(`Attempted to publish inexisting poll for meetingId: ${meetingId}`); return false; diff --git a/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js b/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js index 63985bd8e62e244be654085b7d0705463b72dbbc..66b6dd938b5870463812b7982bec77cc30332e91 100644 --- a/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js +++ b/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js @@ -2,14 +2,14 @@ import RedisPubSub from '/imports/startup/server/redis'; import { check } from 'meteor/check'; import Polls from '/imports/api/polls'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function publishVote(credentials, id, pollAnswerId) { // TODO discuss location +export default function publishVote(id, pollAnswerId) { // TODO discuss location const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'RespondToPollReqMsg'; - const { meetingId, requesterUserId } = credentials; - + const { meetingId, requesterUserId } = extractCredentials(this.userId); /* We keep an array of people who were in the meeting at the time the poll was started. The poll is published to them only. @@ -22,8 +22,6 @@ export default function publishVote(credentials, id, pollAnswerId) { // TODO dis id, }); - check(meetingId, String); - check(requesterUserId, String); check(pollAnswerId, Number); check(currentPoll, Object); check(currentPoll.meetingId, String); diff --git a/bigbluebutton-html5/imports/api/polls/server/methods/startPoll.js b/bigbluebutton-html5/imports/api/polls/server/methods/startPoll.js index 7f4722837a881088b52a508d751f0ec4fd1a287d..f6a08e3dd9045736783f28c8ef4ada7375048bbc 100644 --- a/bigbluebutton-html5/imports/api/polls/server/methods/startPoll.js +++ b/bigbluebutton-html5/imports/api/polls/server/methods/startPoll.js @@ -1,16 +1,15 @@ import RedisPubSub from '/imports/startup/server/redis'; -import Polls from '/imports/api/polls'; import { check } from 'meteor/check'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function startPoll(credentials, pollType, pollId, answers) { - const { meetingId, requesterUserId } = credentials; +export default function startPoll(pollType, pollId, answers) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; let EVENT_NAME = 'StartPollReqMsg'; - check(meetingId, String); - check(requesterUserId, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); + check(pollId, String); check(pollType, String); diff --git a/bigbluebutton-html5/imports/api/polls/server/methods/stopPoll.js b/bigbluebutton-html5/imports/api/polls/server/methods/stopPoll.js index 0d20a850b39a268e470e2ca58b245b8bbb47ca1b..af0c50dd007bf60700260495483e8a696c465869 100644 --- a/bigbluebutton-html5/imports/api/polls/server/methods/stopPoll.js +++ b/bigbluebutton-html5/imports/api/polls/server/methods/stopPoll.js @@ -1,20 +1,17 @@ import RedisPubSub from '/imports/startup/server/redis'; -import { check } from 'meteor/check'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function stopPoll(credentials) { - const { meetingId, requesterUserId } = credentials; +export default function stopPoll() { + const { meetingId, requesterUserId: requesterId } = extractCredentials(this.userId); const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'StopPollReqMsg'; - check(meetingId, String); - check(requesterUserId, String); - return RedisPubSub.publishUserMessage( CHANNEL, EVENT_NAME, meetingId, - requesterUserId, - ({ requesterId: requesterUserId }), + requesterId, + ({ requesterId }), ); } diff --git a/bigbluebutton-html5/imports/api/polls/server/publishers.js b/bigbluebutton-html5/imports/api/polls/server/publishers.js index c2e018fe6f176a7e930b8b758fe513e30dd14ce6..70804193fc6eef13403017c75a2d1b373e16cb9b 100644 --- a/bigbluebutton-html5/imports/api/polls/server/publishers.js +++ b/bigbluebutton-html5/imports/api/polls/server/publishers.js @@ -1,10 +1,13 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; import Polls from '/imports/api/polls'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -Meteor.publish('current-poll', (meetingId) => { - check(meetingId, String); +function currentPoll() { + if (!this.userId) { + return Polls.find({ meetingId: '' }); + } + const { meetingId } = extractCredentials(this.userId); const selector = { meetingId, @@ -13,17 +16,24 @@ Meteor.publish('current-poll', (meetingId) => { Logger.debug(`Publishing poll for meeting=${meetingId}`); return Polls.find(selector); -}); +} + +function publishCurrentPoll(...args) { + const boundPolls = currentPoll.bind(this); + return boundPolls(...args); +} + +Meteor.publish('current-poll', publishCurrentPoll); -function polls(credentials) { - const { meetingId, requesterUserId, requesterToken } = credentials; +function polls() { + if (!this.userId) { + return Polls.find({ meetingId: '' }); + } - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); - Logger.debug(`Publishing polls =${meetingId} ${requesterUserId} ${requesterToken}`); + Logger.debug(`Publishing polls =${meetingId} ${requesterUserId}`); const selector = { meetingId, diff --git a/bigbluebutton-html5/imports/api/presentation-pods/server/methods/setPresenterInPodReqMsg.js b/bigbluebutton-html5/imports/api/presentation-pods/server/methods/setPresenterInPodReqMsg.js index 8679aa62cf75357d4aa4ad916a512edcbbecac81..6408dd1311dfa2b4504751e05552c66304ebb691 100644 --- a/bigbluebutton-html5/imports/api/presentation-pods/server/methods/setPresenterInPodReqMsg.js +++ b/bigbluebutton-html5/imports/api/presentation-pods/server/methods/setPresenterInPodReqMsg.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; -export default function setPresenterInPodReqMsg(credentials) { +export default function setPresenterInPodReqMsg(credentials) { // TODO-- switch to meetingId, etc const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'SetPresenterInPodReqMsg'; diff --git a/bigbluebutton-html5/imports/api/presentation-pods/server/publishers.js b/bigbluebutton-html5/imports/api/presentation-pods/server/publishers.js index 908c10bccdfbd361c8f929dc4e57de5fff7a0c1f..439b47e22f961d5368536be1d332e2e6d8ff3664 100644 --- a/bigbluebutton-html5/imports/api/presentation-pods/server/publishers.js +++ b/bigbluebutton-html5/imports/api/presentation-pods/server/publishers.js @@ -1,16 +1,14 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import PresentationPods from '/imports/api/presentation-pods'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -function presentationPods(credentials) { - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); - - Logger.debug(`Publishing presentation-pods for ${meetingId} ${requesterUserId} ${requesterToken}`); +function presentationPods() { + if (!this.userId) { + return PresentationPods.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); + Logger.debug(`Publishing presentation-pods for ${meetingId} ${requesterUserId}`); return PresentationPods.find({ meetingId }); } diff --git a/bigbluebutton-html5/imports/api/presentation-pods/server/utils/isPodPresenter.js b/bigbluebutton-html5/imports/api/presentation-pods/server/utils/isPodPresenter.js deleted file mode 100755 index 48cfdc3e53a2e67a8735c842c94b6db8d6f2fcd8..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/api/presentation-pods/server/utils/isPodPresenter.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Slides } from '/imports/api/slides'; -import PresentationPods from '/imports/api/presentation-pods'; - -export default function isPodPresenter(meetingId, whiteboardId, userId) { - const slide = Slides.findOne({ meetingId, id: whiteboardId }); - const pod = PresentationPods.findOne({ meetingId, podId: slide.podId }); - - return pod.currentPresenterId === userId; -} diff --git a/bigbluebutton-html5/imports/api/presentation-upload-token/server/methods/requestPresentationUploadToken.js b/bigbluebutton-html5/imports/api/presentation-upload-token/server/methods/requestPresentationUploadToken.js index ae0efc36329b6feac8d36179f07e7104c2f38ba3..b01937dea06bd8185c0ec961375ef9dd1f7e965d 100644 --- a/bigbluebutton-html5/imports/api/presentation-upload-token/server/methods/requestPresentationUploadToken.js +++ b/bigbluebutton-html5/imports/api/presentation-upload-token/server/methods/requestPresentationUploadToken.js @@ -1,15 +1,13 @@ import RedisPubSub from '/imports/startup/server/redis'; import { check } from 'meteor/check'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function requestPresentationUploadToken(credentials, podId, filename) { +export default function requestPresentationUploadToken(podId, filename) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'PresentationUploadTokenReqMsg'; - const { meetingId, requesterUserId } = credentials; - - check(meetingId, String); - check(requesterUserId, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); check(podId, String); check(filename, String); diff --git a/bigbluebutton-html5/imports/api/presentation-upload-token/server/methods/setUsedToken.js b/bigbluebutton-html5/imports/api/presentation-upload-token/server/methods/setUsedToken.js index a09c9d7c1f658c0198c7042a0d68f6e98450666c..8bb0f725c97c121d0c3a6a1a64df61782196c53a 100644 --- a/bigbluebutton-html5/imports/api/presentation-upload-token/server/methods/setUsedToken.js +++ b/bigbluebutton-html5/imports/api/presentation-upload-token/server/methods/setUsedToken.js @@ -1,12 +1,9 @@ import PresentationUploadToken from '/imports/api/presentation-upload-token'; import Logger from '/imports/startup/server/logger'; -import { check } from 'meteor/check'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function setUsedToken(credentials, authzToken) { - const { meetingId, requesterUserId, requesterToken } = credentials; - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); +export default function setUsedToken(authzToken) { + const { meetingId, requesterUserId } = extractCredentials(this.userId); const payload = { $set: { diff --git a/bigbluebutton-html5/imports/api/presentation-upload-token/server/publishers.js b/bigbluebutton-html5/imports/api/presentation-upload-token/server/publishers.js index 50edcb12cf1143629cefde6f3889079356727879..ffd1d5f49626db48b753ee6e4a0cf093c83cd845 100644 --- a/bigbluebutton-html5/imports/api/presentation-upload-token/server/publishers.js +++ b/bigbluebutton-html5/imports/api/presentation-upload-token/server/publishers.js @@ -2,13 +2,13 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import PresentationUploadToken from '/imports/api/presentation-upload-token'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -Meteor.publish('presentation-upload-token', (credentials, podId, filename) => { - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); +function presentationUploadToken(podId, filename) { + if (!this.userId) { + return PresentationUploadToken.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); check(podId, String); check(filename, String); @@ -19,7 +19,14 @@ Meteor.publish('presentation-upload-token', (credentials, podId, filename) => { filename, }; - Logger.debug(`Publishing PresentationUploadToken for ${meetingId} ${requesterUserId} ${requesterToken}`); + Logger.debug(`Publishing PresentationUploadToken for ${meetingId} ${requesterUserId}`); return PresentationUploadToken.find(selector); -}); +} + +function publish(...args) { + const boundPresentationUploadToken = presentationUploadToken.bind(this); + return boundPresentationUploadToken(...args); +} + +Meteor.publish('presentation-upload-token', publish); diff --git a/bigbluebutton-html5/imports/api/presentations/server/methods/removePresentation.js b/bigbluebutton-html5/imports/api/presentations/server/methods/removePresentation.js index 259fa4d5c6b78fdaa85120d303a6025989f11599..a4169aa0f3b064e0ca074c567830a62db862edf6 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/methods/removePresentation.js +++ b/bigbluebutton-html5/imports/api/presentations/server/methods/removePresentation.js @@ -1,30 +1,17 @@ import RedisPubSub from '/imports/startup/server/redis'; import { check } from 'meteor/check'; -import Presentations from '/imports/api/presentations'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function removePresentation(credentials, presentationId, podId) { - const PRESENTATION_CONFIG = Meteor.settings.public.presentation; +export default function removePresentation(presentationId, podId) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'RemovePresentationPubMsg'; - const { meetingId, requesterUserId } = credentials; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); check(presentationId, String); check(podId, String); - const presentationToDelete = Presentations.findOne({ - meetingId, - id: presentationId, - podId, - }); - - if (presentationToDelete.name === PRESENTATION_CONFIG.defaultPresentationFile) { - throw new Meteor.Error('not-allowed', 'You are not allowed to remove the default slide'); - } - const payload = { presentationId, podId, diff --git a/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentation.js b/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentation.js index 164370e313c4760d713d246a1395049f8b548456..ebe15cbe0daeac955a6c3ad6805c968dacea8af2 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentation.js +++ b/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentation.js @@ -1,39 +1,20 @@ import RedisPubSub from '/imports/startup/server/redis'; import { check } from 'meteor/check'; -import Presentations from '/imports/api/presentations'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function setPresentation(credentials, presentationId, podId) { +export default function setPresentation(presentationId, podId) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'SetCurrentPresentationPubMsg'; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - const { meetingId, requesterUserId } = credentials; - - check(meetingId, String); - check(requesterUserId, String); check(presentationId, String); check(podId, String); - const currentPresentation = Presentations.findOne({ - meetingId, - id: presentationId, + const payload = { + presentationId, podId, - }); - - if (currentPresentation) { - if (currentPresentation.current) { - return Promise.resolve(); - } - - const payload = { - presentationId, - podId, - }; - - return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - } + }; - // did not find presentation with such id. abandon - // return Promise.resolve(); // will close the uploading modal - throw new Meteor.Error('presentation-not-found', `Did not find a presentation with id ${presentationId} in method setPresentation`); + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); } diff --git a/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentationDownloadable.js b/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentationDownloadable.js index edcbcd6c9e7b9d70774ec377f5b889c9d921f2c6..5d51af14f071e5f8023333c76181551d899f8f10 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentationDownloadable.js +++ b/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentationDownloadable.js @@ -1,14 +1,14 @@ import RedisPubSub from '/imports/startup/server/redis'; import { check } from 'meteor/check'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function setPresentationDownloadable(credentials, presentationId, downloadable) { +export default function setPresentationDownloadable(presentationId, downloadable) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'SetPresentationDownloadablePubMsg'; - const { meetingId, requesterUserId } = credentials; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); check(downloadable, Boolean); check(presentationId, String); diff --git a/bigbluebutton-html5/imports/api/presentations/server/publishers.js b/bigbluebutton-html5/imports/api/presentations/server/publishers.js index 3839c58c984456ad63073a85089baa1c355085e4..3fb5e0e5bd9d0164c5961f007fa921743311cf48 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/publishers.js +++ b/bigbluebutton-html5/imports/api/presentations/server/publishers.js @@ -1,16 +1,15 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import Presentations from '/imports/api/presentations'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -function presentations(credentials) { - const { meetingId, requesterUserId, requesterToken } = credentials; +function presentations() { + if (!this.userId) { + return Presentations.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); - - Logger.debug(`Publishing Presentations for ${meetingId} ${requesterUserId} ${requesterToken}`); + Logger.debug(`Publishing Presentations for ${meetingId} ${requesterUserId}`); return Presentations.find({ meetingId }); } diff --git a/bigbluebutton-html5/imports/api/screenshare/server/publishers.js b/bigbluebutton-html5/imports/api/screenshare/server/publishers.js index 5ffc1b185a6cd29c088b41e2915beed0c721a327..8042a790c641170e16bb35025c747e160ff2d794 100644 --- a/bigbluebutton-html5/imports/api/screenshare/server/publishers.js +++ b/bigbluebutton-html5/imports/api/screenshare/server/publishers.js @@ -1,13 +1,13 @@ import Screenshare from '/imports/api/screenshare'; import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -function screenshare(credentials) { - const { meetingId, requesterUserId } = credentials; - - check(meetingId, String); - check(requesterUserId, String); +function screenshare() { + if (!this.userId) { + return Screenshare.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); Logger.debug(`Publishing Screenshare for ${meetingId} ${requesterUserId}`); diff --git a/bigbluebutton-html5/imports/api/slides/server/methods/switchSlide.js b/bigbluebutton-html5/imports/api/slides/server/methods/switchSlide.js index b9e32fb6857a4074f7a0c345d17ac48339e72c04..2251095ced32366f724e021d6ebe674c7b97bbe5 100755 --- a/bigbluebutton-html5/imports/api/slides/server/methods/switchSlide.js +++ b/bigbluebutton-html5/imports/api/slides/server/methods/switchSlide.js @@ -3,18 +3,14 @@ import { Slides } from '/imports/api/slides'; import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function switchSlide(credentials, slideNumber, podId) { +export default function switchSlide(slideNumber, podId) { // TODO-- send presentationId and SlideId const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'SetCurrentPagePubMsg'; - - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); check(slideNumber, Number); const selector = { diff --git a/bigbluebutton-html5/imports/api/slides/server/methods/zoomSlide.js b/bigbluebutton-html5/imports/api/slides/server/methods/zoomSlide.js index b4255e808380b98c7238d6a7a80c930a2e0ba1ab..3d247742f3a22009c5b64ad5130c6a94e04c04cc 100755 --- a/bigbluebutton-html5/imports/api/slides/server/methods/zoomSlide.js +++ b/bigbluebutton-html5/imports/api/slides/server/methods/zoomSlide.js @@ -1,20 +1,15 @@ import Presentations from '/imports/api/presentations'; import { Slides } from '/imports/api/slides'; import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function zoomSlide(credentials, slideNumber, podId, widthRatio, heightRatio, x, y) { +export default function zoomSlide(slideNumber, podId, widthRatio, heightRatio, x, y) { // TODO-- send presentationId and SlideId const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'ResizeAndMovePagePubMsg'; - - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); const selector = { meetingId, diff --git a/bigbluebutton-html5/imports/api/slides/server/publishers.js b/bigbluebutton-html5/imports/api/slides/server/publishers.js index 273c4b60cf9953fad24ab0c54dd30efa9cb41881..99895cfbb02feedc40855e4696e31dc3129f3439 100755 --- a/bigbluebutton-html5/imports/api/slides/server/publishers.js +++ b/bigbluebutton-html5/imports/api/slides/server/publishers.js @@ -1,16 +1,14 @@ import { Slides, SlidePositions } from '/imports/api/slides'; import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -function slides(credentials) { - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); - - Logger.debug(`Publishing Slides for ${meetingId} ${requesterUserId} ${requesterToken}`); +function slides() { + if (!this.userId) { + return Slides.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); + Logger.debug(`Publishing Slides for ${meetingId} ${requesterUserId}`); return Slides.find({ meetingId }); } @@ -22,14 +20,13 @@ function publish(...args) { Meteor.publish('slides', publish); -function slidePositions(credentials) { - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); +function slidePositions() { + if (!this.userId) { + return SlidePositions.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); - Logger.debug(`Publishing SlidePositions for ${meetingId} ${requesterUserId} ${requesterToken}`); + Logger.debug(`Publishing SlidePositions for ${meetingId} ${requesterUserId}`); return SlidePositions.find({ meetingId }); } diff --git a/bigbluebutton-html5/imports/api/users-infos/server/methods/removeUserInformation.js b/bigbluebutton-html5/imports/api/users-infos/server/methods/removeUserInformation.js index 3328b07c5b0a27990e19736b3c85b0124c534f86..5201a8c0a0b58938a0686600241cdbfb4282d735 100644 --- a/bigbluebutton-html5/imports/api/users-infos/server/methods/removeUserInformation.js +++ b/bigbluebutton-html5/imports/api/users-infos/server/methods/removeUserInformation.js @@ -1,7 +1,7 @@ import UserInfos from '/imports/api/users-infos'; import Logger from '/imports/startup/server/logger'; -export default function removeUserInformation(credentials, meetingId, requesterUserId) { +export default function removeUserInformation(meetingId, requesterUserId) { const selector = { meetingId, requesterUserId, diff --git a/bigbluebutton-html5/imports/api/users-infos/server/methods/requestUserInformation.js b/bigbluebutton-html5/imports/api/users-infos/server/methods/requestUserInformation.js index 4d93c99c2615a9f0fb6e48248be1e14880fc2535..ed89eedd8e6b120438149891aeaa8180d717a502 100644 --- a/bigbluebutton-html5/imports/api/users-infos/server/methods/requestUserInformation.js +++ b/bigbluebutton-html5/imports/api/users-infos/server/methods/requestUserInformation.js @@ -1,16 +1,14 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function getUserInformation(credentials, externalUserId) { +export default function getUserInformation(externalUserId) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toThirdParty; const EVENT_NAME = 'LookUpUserReqMsg'; - const { meetingId, requesterUserId } = credentials; - - check(meetingId, String); - check(requesterUserId, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); check(externalUserId, String); const payload = { diff --git a/bigbluebutton-html5/imports/api/users-infos/server/publishers.js b/bigbluebutton-html5/imports/api/users-infos/server/publishers.js index 6eed7b35c38ede6bd6a17eaa711a61fd9cd5db17..1205c00e6fcc987a3fac6289a535cb4d6824104b 100644 --- a/bigbluebutton-html5/imports/api/users-infos/server/publishers.js +++ b/bigbluebutton-html5/imports/api/users-infos/server/publishers.js @@ -1,13 +1,13 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import UserInfos from '/imports/api/users-infos'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -function userInfos(credentials) { - const { meetingId, requesterUserId } = credentials; - - check(meetingId, String); - check(requesterUserId, String); +function userInfos() { + if (!this.userId) { + return UserInfos.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); Logger.debug(`Publishing user infos requested by user=${requesterUserId}`); diff --git a/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js b/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js index 1635159c92ca8d3502fb26b374165c1b49400d15..ac4b49c9e042ace7bbcd05917372004acb1ad507 100644 --- a/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js +++ b/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js @@ -71,9 +71,7 @@ function valueParser(val) { } } -export default function addUserSettings(credentials, meetingId, userId, settings) { - check(meetingId, String); - check(userId, String); +export default function addUserSettings(meetingId, userId, settings) { check(settings, [Object]); let parameters = {}; diff --git a/bigbluebutton-html5/imports/api/users-settings/server/publishers.js b/bigbluebutton-html5/imports/api/users-settings/server/publishers.js index 408b10f1c04065f133b8402f4c7bfc7624bee923..0b772099b42c8eb1007a2387a14a0d1d747f4194 100644 --- a/bigbluebutton-html5/imports/api/users-settings/server/publishers.js +++ b/bigbluebutton-html5/imports/api/users-settings/server/publishers.js @@ -1,13 +1,13 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import UserSettings from '/imports/api/users-settings'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -function userSettings(credentials) { - const { meetingId, requesterUserId } = credentials; - - check(meetingId, String); - check(requesterUserId, String); +function userSettings() { + if (!this.userId) { + return UserSettings.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); Logger.debug(`Publishing user settings for user=${requesterUserId}`); diff --git a/bigbluebutton-html5/imports/api/users/server/methods/userJoin.js b/bigbluebutton-html5/imports/api/users/server/handlers/userJoin.js similarity index 78% rename from bigbluebutton-html5/imports/api/users/server/methods/userJoin.js rename to bigbluebutton-html5/imports/api/users/server/handlers/userJoin.js index dc68d83f55336236207e45b49bd5235b43bc704c..93ecd8bdcf0cf0f14d65a8903b6503dc01e374a6 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/userJoin.js +++ b/bigbluebutton-html5/imports/api/users/server/handlers/userJoin.js @@ -8,10 +8,6 @@ export default function userJoin(meetingId, userId, authToken) { const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'UserJoinMeetingReqMsg'; - Logger.info(`User='${userId}' is joining meeting='${meetingId}' authToken='${authToken}' pt1`); - - check(meetingId, String); - check(userId, String); check(authToken, String); const payload = { @@ -20,7 +16,7 @@ export default function userJoin(meetingId, userId, authToken) { clientType: 'HTML5', }; - Logger.info(`User='${userId}' is joining meeting='${meetingId}' authToken='${authToken}' pt2`); + Logger.info(`User='${userId}' is joining meeting='${meetingId}' authToken='${authToken}'`); return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, userId, payload); } diff --git a/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js b/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js index e60d924dd0adb6ab4cd159c640533f33b600b130..f8a04154b10d575dc24f1334b0085acaabfaea4b 100644 --- a/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js +++ b/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js @@ -1,7 +1,7 @@ import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; import Users from '/imports/api/users'; -import userJoin from '../methods/userJoin'; +import userJoin from './userJoin'; const clearOtherSessions = (sessionUserId, current = false) => { const serverSessions = Meteor.server.sessions; diff --git a/bigbluebutton-html5/imports/api/users/server/methods/assignPresenter.js b/bigbluebutton-html5/imports/api/users/server/methods/assignPresenter.js index add2afe1d06beac5bdc405a88f64344d849f3084..d8b5da899ae0a05f8358217b5c6b072900118ee1 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/assignPresenter.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/assignPresenter.js @@ -3,16 +3,14 @@ import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; import Logger from '/imports/startup/server/logger'; import Users from '/imports/api/users'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function assignPresenter(credentials, userId) { +export default function assignPresenter(userId) { // TODO-- send username from client side const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'AssignPresenterReqMsg'; - const { meetingId, requesterUserId } = credentials; - - check(meetingId, String); - check(requesterUserId, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); check(userId, String); const User = Users.findOne({ diff --git a/bigbluebutton-html5/imports/api/users/server/methods/changeRole.js b/bigbluebutton-html5/imports/api/users/server/methods/changeRole.js index beff64791c4a9654e1c0213050b1ad43b41d121c..f45abca3887de2dd6f7ea5d777f73313c46bca23 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/changeRole.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/changeRole.js @@ -2,36 +2,25 @@ 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 Users from '/imports/api/users'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function changeRole(credentials, userId, role) { +export default function changeRole(userId, role) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'ChangeUserRoleCmdMsg'; - const { meetingId, requesterUserId } = credentials; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); check(userId, String); check(role, String); - const User = Users.findOne({ - meetingId, - userId, - }); - - if (!User) { - throw new Meteor.Error('user-not-found', `You need a valid user to be able to set '${role}'`); - } - const payload = { userId, role, changedBy: requesterUserId, }; - Logger.verbose(`User '${userId}' setted as '${role} role by '${requesterUserId}' from meeting '${meetingId}'`); + Logger.verbose(`User '${userId}' set as '${role} role by '${requesterUserId}' from meeting '${meetingId}'`); return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); } diff --git a/bigbluebutton-html5/imports/api/users/server/methods/removeUser.js b/bigbluebutton-html5/imports/api/users/server/methods/removeUser.js index 80d90ec221d9a27e1e9ba862d8c07cf592ea9a47..afe41d6f9f83c3e34a23dd0932a67080b9bb77cc 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/removeUser.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/removeUser.js @@ -1,22 +1,21 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function removeUser(credentials, userId) { +export default function removeUser(userId) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'EjectUserFromMeetingCmdMsg'; - const { requesterUserId, meetingId } = credentials; + const { meetingId, requesterUserId: ejectedBy } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); check(userId, String); const payload = { userId, - ejectedBy: requesterUserId, + ejectedBy, }; - return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, ejectedBy, payload); } diff --git a/bigbluebutton-html5/imports/api/users/server/methods/setEmojiStatus.js b/bigbluebutton-html5/imports/api/users/server/methods/setEmojiStatus.js index c63e65ed058183e52592ab45f7ca6adfc6bc3c3a..5cea8279e734848adde14f98a9edd5ea001e87d7 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/setEmojiStatus.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/setEmojiStatus.js @@ -2,16 +2,15 @@ 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 { extractCredentials } from '/imports/api/common/server/helpers'; -export default function setEmojiStatus(credentials, userId, status) { +export default function setEmojiStatus(userId, status) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'ChangeUserEmojiCmdMsg'; - const { meetingId, requesterUserId } = credentials; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); check(userId, String); const payload = { diff --git a/bigbluebutton-html5/imports/api/users/server/methods/setUserEffectiveConnectionType.js b/bigbluebutton-html5/imports/api/users/server/methods/setUserEffectiveConnectionType.js index fd6a632d90cfb6425b9d6d661558a94c1f92a79f..6c1242ee4537b9b78d0793a2184c01a8ef340a5f 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/setUserEffectiveConnectionType.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/setUserEffectiveConnectionType.js @@ -2,17 +2,15 @@ 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 { extractCredentials } from '/imports/api/common/server/helpers'; import setEffectiveConnectionType from '../modifiers/setUserEffectiveConnectionType'; -export default function setUserEffectiveConnectionType(credentials, effectiveConnectionType) { +export default function setUserEffectiveConnectionType(effectiveConnectionType) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'ChangeUserEffectiveConnectionMsg'; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - const { meetingId, requesterUserId } = credentials; - - check(meetingId, String); - check(requesterUserId, String); check(effectiveConnectionType, String); const payload = { diff --git a/bigbluebutton-html5/imports/api/users/server/methods/toggleUserLock.js b/bigbluebutton-html5/imports/api/users/server/methods/toggleUserLock.js index 0919194b9261f371210f5981ea3fdca69be95476..f13aa323f7d8502a404c696d62a07291fe21a593 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/toggleUserLock.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/toggleUserLock.js @@ -2,15 +2,15 @@ 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 { extractCredentials } from '/imports/api/common/server/helpers'; -export default function toggleUserLock(credentials, userId, lock) { +export default function toggleUserLock(userId, lock) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'LockUserInMeetingCmdMsg'; - const { meetingId, requesterUserId: lockedBy } = credentials; + const { meetingId, requesterUserId: lockedBy } = extractCredentials(this.userId); - check(meetingId, String); check(lockedBy, String); check(userId, String); check(lock, Boolean); diff --git a/bigbluebutton-html5/imports/api/users/server/methods/userActivitySign.js b/bigbluebutton-html5/imports/api/users/server/methods/userActivitySign.js index d4ac0f1b8847f9abaccfeaa3f605470bbcee1229..16642f732bbcdfcc9b147b298f526b926c35493a 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/userActivitySign.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/userActivitySign.js @@ -1,19 +1,14 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import Users from '/imports/api/users'; import RedisPubSub from '/imports/startup/server/redis'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function userActivitySign(credentials) { +export default function userActivitySign() { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'UserActivitySignCmdMsg'; - - const { meetingId, requesterUserId: userId } = credentials; - - check(meetingId, String); - check(userId, String); - + const { meetingId, requesterUserId: userId } = extractCredentials(this.userId); const payload = { userId, }; @@ -28,7 +23,7 @@ export default function userActivitySign(credentials) { }, }; - Users.update(selector, modifier); + Users.update(selector, modifier); // TODO-- we should move this to a modifier Logger.info(`User ${userId} sent a activity sign for meeting ${meetingId}`); diff --git a/bigbluebutton-html5/imports/api/users/server/methods/userLeaving.js b/bigbluebutton-html5/imports/api/users/server/methods/userLeaving.js index 321689b3fd6200352170816864b6ecf67479ad6e..9c21e9be7912a8f253cbdd1b73fceeacbce6391b 100755 --- a/bigbluebutton-html5/imports/api/users/server/methods/userLeaving.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/userLeaving.js @@ -4,15 +4,11 @@ import RedisPubSub from '/imports/startup/server/redis'; import Logger from '/imports/startup/server/logger'; import Users from '/imports/api/users'; -export default function userLeaving(credentials, userId, connectionId) { +export default function userLeaving(meetingId, userId, connectionId) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'UserLeaveReqMsg'; - const { meetingId, requesterUserId } = credentials; - - check(meetingId, String); - check(requesterUserId, String); check(userId, String); const selector = { @@ -37,5 +33,5 @@ export default function userLeaving(credentials, userId, connectionId) { }; Logger.info(`User '${userId}' is leaving meeting '${meetingId}'`); - return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, userId, payload); } diff --git a/bigbluebutton-html5/imports/api/users/server/methods/userLeftMeeting.js b/bigbluebutton-html5/imports/api/users/server/methods/userLeftMeeting.js index 4723ba94e51538edaaa9ada00af9a694d1fcc8a9..eb198b5f88a688a5860bee76858b6b112615888f 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/userLeftMeeting.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/userLeftMeeting.js @@ -1,16 +1,10 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; import Users from '/imports/api/users'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function userLeftMeeting(credentials) { - const { - meetingId, - requesterUserId, - } = credentials; - - check(meetingId, String); - check(requesterUserId, String); +export default function userLeftMeeting() { // TODO-- spread the code to method/modifier/handler + // so we don't update the db in a method + const { meetingId, requesterUserId } = extractCredentials(this.userId); const selector = { meetingId, diff --git a/bigbluebutton-html5/imports/api/users/server/methods/validateAuthToken.js b/bigbluebutton-html5/imports/api/users/server/methods/validateAuthToken.js index 136c00db1a8d5917d3b9547cc2a1f4579a8cdedc..e01b6ad348fde23e60eea7cf571b3938c4739286 100644 --- a/bigbluebutton-html5/imports/api/users/server/methods/validateAuthToken.js +++ b/bigbluebutton-html5/imports/api/users/server/methods/validateAuthToken.js @@ -1,23 +1,16 @@ 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 Users from '/imports/api/users'; import createDummyUser from '../modifiers/createDummyUser'; import setConnectionIdAndAuthToken from '../modifiers/setConnectionIdAndAuthToken'; -export default function validateAuthToken(credentials) { +export default function validateAuthToken(meetingId, requesterUserId, requesterToken) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'ValidateAuthTokenReqMsg'; - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); - - const sessionId = `${meetingId}-${requesterUserId}`; + const sessionId = `${meetingId}--${requesterUserId}`; this.setUserId(sessionId); const User = Users.findOne({ 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 1751a2517718cdb07a2611a39a9a538c67908275..861fb357e16daebcc17417ef6f1efc2eb8d69c5b 100644 --- a/bigbluebutton-html5/imports/api/users/server/publishers.js +++ b/bigbluebutton-html5/imports/api/users/server/publishers.js @@ -5,16 +5,23 @@ import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; import userLeaving from './methods/userLeaving'; +import { extractCredentials } from '/imports/api/common/server/helpers'; const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; -Meteor.publish('current-user', function currentUserPub(credentials) { - const { meetingId, requesterUserId, requesterToken } = credentials; +function currentUser() { + if (!this.userId) { + return Users.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); const connectionId = this.connection.id; const onCloseConnection = Meteor.bindEnvironment(() => { try { - userLeaving(credentials, requesterUserId, connectionId); + userLeaving(meetingId, requesterUserId, connectionId); } catch (e) { Logger.error(`Exception while executing userLeaving: ${e}`); } @@ -22,35 +29,33 @@ Meteor.publish('current-user', function currentUserPub(credentials) { this._session.socket.on('close', _.debounce(onCloseConnection, 100)); - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); - const selector = { meetingId, userId: requesterUserId, - authToken: requesterToken, }; const options = { fields: { user: false, + authToken: false, // Not asking for authToken from client side but also not exposing it }, }; return Users.find(selector, options); -}); +} -function users(credentials, isModerator = false) { - const { - meetingId, - requesterUserId, - requesterToken, - } = credentials; +function publishCurrentUser(...args) { + const boundUsers = currentUser.bind(this); + return boundUsers(...args); +} - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); +Meteor.publish('current-user', publishCurrentUser); + +function users(isModerator = false) { + if (!this.userId) { + return Users.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); const selector = { $or: [ @@ -59,7 +64,7 @@ function users(credentials, 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, @@ -76,7 +81,7 @@ function users(credentials, isModerator = false) { }, }; - Logger.debug(`Publishing Users for ${meetingId} ${requesterUserId} ${requesterToken}`); + Logger.debug(`Publishing Users for ${meetingId} ${requesterUserId}`); return Users.find(selector, options); } diff --git a/bigbluebutton-html5/imports/api/video-streams/server/methods/userShareWebcam.js b/bigbluebutton-html5/imports/api/video-streams/server/methods/userShareWebcam.js index 2d7e51cb06c38e1896ccdbcace3488bc46629938..4b679293712923df7ae3a58630c03962792d2fb6 100644 --- a/bigbluebutton-html5/imports/api/video-streams/server/methods/userShareWebcam.js +++ b/bigbluebutton-html5/imports/api/video-streams/server/methods/userShareWebcam.js @@ -2,19 +2,16 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; import RedisPubSub from '/imports/startup/server/redis'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function userShareWebcam(credentials, stream) { +export default function userShareWebcam(stream) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'UserBroadcastCamStartMsg'; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - const { meetingId, requesterUserId, requesterToken } = credentials; + Logger.info(`user sharing webcam: ${meetingId} ${requesterUserId}`); - Logger.info(' user sharing webcam: ', credentials); - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); check(stream, String); // const actionName = 'joinVideo'; diff --git a/bigbluebutton-html5/imports/api/video-streams/server/methods/userUnshareWebcam.js b/bigbluebutton-html5/imports/api/video-streams/server/methods/userUnshareWebcam.js index 13f6609ee85bc09f9d9d111156cf4446fe5fed11..c04d863efdcdba50f0cebc19518c10673de41d3d 100644 --- a/bigbluebutton-html5/imports/api/video-streams/server/methods/userUnshareWebcam.js +++ b/bigbluebutton-html5/imports/api/video-streams/server/methods/userUnshareWebcam.js @@ -2,19 +2,16 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; import RedisPubSub from '/imports/startup/server/redis'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function userUnshareWebcam(credentials, stream) { +export default function userUnshareWebcam(stream) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'UserBroadcastCamStopMsg'; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - const { meetingId, requesterUserId, requesterToken } = credentials; + Logger.info(`user unsharing webcam: ${meetingId} ${requesterUserId}`); - Logger.info(' user unsharing webcam: ', credentials); - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); check(stream, String); // const actionName = 'joinVideo'; diff --git a/bigbluebutton-html5/imports/api/video-streams/server/publisher.js b/bigbluebutton-html5/imports/api/video-streams/server/publisher.js index b471bbbe396321eae1f52547e467cbfcdf475fe4..046a054ff2406fdbb78b9a8379c5d06050209f2e 100644 --- a/bigbluebutton-html5/imports/api/video-streams/server/publisher.js +++ b/bigbluebutton-html5/imports/api/video-streams/server/publisher.js @@ -1,14 +1,13 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; import VideoStreams from '/imports/api/video-streams'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -function videoStreams(credentials) { - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); +function videoStreams() { + if (!this.userId) { + return VideoStreams.find({ meetingId: '' }); + } + const { meetingId } = extractCredentials(this.userId); Logger.debug(`video users of meeting id=${meetingId}`); diff --git a/bigbluebutton-html5/imports/api/voice-call-states/index.js b/bigbluebutton-html5/imports/api/voice-call-states/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7d2dfc3e3927f6b086f6c425e233ca8462e01ec8 --- /dev/null +++ b/bigbluebutton-html5/imports/api/voice-call-states/index.js @@ -0,0 +1,13 @@ +import { Meteor } from 'meteor/meteor'; + +const VoiceCallStates = new Mongo.Collection('voiceCallStates'); + +if (Meteor.isServer) { + // types of queries for the voice users: + // 1. intId + // 2. meetingId, intId + + VoiceCallStates._ensureIndex({ meetingId: 1, userId: 1 }); +} + +export default VoiceCallStates; diff --git a/bigbluebutton-html5/imports/api/voice-call-states/server/eventHandlers.js b/bigbluebutton-html5/imports/api/voice-call-states/server/eventHandlers.js new file mode 100644 index 0000000000000000000000000000000000000000..4d7a7397a389f15122f677786bea810040de69a8 --- /dev/null +++ b/bigbluebutton-html5/imports/api/voice-call-states/server/eventHandlers.js @@ -0,0 +1,4 @@ +import RedisPubSub from '/imports/startup/server/redis'; +import handleVoiceCallStateEvent from './handlers/voiceCallStateEvent'; + +RedisPubSub.on('VoiceCallStateEvtMsg', handleVoiceCallStateEvent); 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 new file mode 100644 index 0000000000000000000000000000000000000000..ca66c2473d5eaaf65522ea512bd4390136271adf --- /dev/null +++ b/bigbluebutton-html5/imports/api/voice-call-states/server/handlers/voiceCallStateEvent.js @@ -0,0 +1,48 @@ +import { check } from 'meteor/check'; +import VoiceCallState from '/imports/api/voice-call-states'; +import Logger from '/imports/startup/server/logger'; + +// "CALL_STARTED", "IN_ECHO_TEST", "IN_CONFERENCE", "CALL_ENDED" + +export default function handleVoiceCallStateEvent({ body }, meetingId) { + const { + voiceConf, + clientSession, + userId, + callerName, + callState, + } = body; + + check(meetingId, String); + check(voiceConf, String); + check(clientSession, String); + check(userId, String); + check(callerName, String); + check(callState, String); + + const selector = { + meetingId, + userId, + clientSession, + }; + + const modifier = { + $set: { + meetingId, + userId, + voiceConf, + clientSession, + callState, + }, + }; + + const cb = (err) => { + if (err) { + return Logger.error(`Update voice call state=${userId}: ${err}`); + } + + 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-call-states/server/index.js b/bigbluebutton-html5/imports/api/voice-call-states/server/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f993f38e5b8c16360e3831259db8865128b1574e --- /dev/null +++ b/bigbluebutton-html5/imports/api/voice-call-states/server/index.js @@ -0,0 +1,2 @@ +import './eventHandlers'; +import './publishers'; diff --git a/bigbluebutton-html5/imports/api/voice-call-states/server/modifiers/clearVoiceCallStates.js b/bigbluebutton-html5/imports/api/voice-call-states/server/modifiers/clearVoiceCallStates.js new file mode 100644 index 0000000000000000000000000000000000000000..6eed55eec658887b0cd11fdb11e8b287072b9dfb --- /dev/null +++ b/bigbluebutton-html5/imports/api/voice-call-states/server/modifiers/clearVoiceCallStates.js @@ -0,0 +1,14 @@ +import Logger from '/imports/startup/server/logger'; +import VoiceCallStates from '/imports/api/voice-users'; + +export default function clearVoiceCallStates(meetingId) { + if (meetingId) { + return VoiceCallStates.remove({ meetingId }, () => { + Logger.info(`Cleared VoiceCallStates in (${meetingId})`); + }); + } + + return VoiceCallStates.remove({}, () => { + Logger.info('Cleared VoiceCallStates in all meetings'); + }); +} diff --git a/bigbluebutton-html5/imports/api/voice-call-states/server/publishers.js b/bigbluebutton-html5/imports/api/voice-call-states/server/publishers.js new file mode 100644 index 0000000000000000000000000000000000000000..aa43e1786140985fe21df69051aebe188be19f2b --- /dev/null +++ b/bigbluebutton-html5/imports/api/voice-call-states/server/publishers.js @@ -0,0 +1,22 @@ +import VoiceCallStates from '/imports/api/voice-call-states'; +import { Meteor } from 'meteor/meteor'; +import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; + +function voiceCallStates() { + if (!this.userId) { + return VoiceCallStates.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + Logger.debug(`Publishing Voice Call States for ${meetingId} ${requesterUserId}`); + + return VoiceCallStates.find({ meetingId, userId: requesterUserId }); +} + +function publish(...args) { + const boundVoiceCallStates = voiceCallStates.bind(this); + return boundVoiceCallStates(...args); +} + +Meteor.publish('voice-call-states', publish); diff --git a/bigbluebutton-html5/imports/api/voice-call-states/utils/callStates.js b/bigbluebutton-html5/imports/api/voice-call-states/utils/callStates.js new file mode 100644 index 0000000000000000000000000000000000000000..332ad303704cd69f12bb13d09828ccba4126536c --- /dev/null +++ b/bigbluebutton-html5/imports/api/voice-call-states/utils/callStates.js @@ -0,0 +1,8 @@ +const CallStateOptions = { + CALL_STARTED: 'CALL_STARTED', + IN_ECHO_TEST: 'IN_ECHO_TEST', + IN_CONFERENCE: 'IN_CONFERENCE', + CALL_ENDED: 'CALL_ENDED', +}; + +export default CallStateOptions; diff --git a/bigbluebutton-html5/imports/api/voice-users/server/methods.js b/bigbluebutton-html5/imports/api/voice-users/server/methods.js index 975770e43fdadd32ae7ffd64dc5a8c3143162ebe..6e40c34917afd32fa9bb6756f528b67b2277447c 100755 --- a/bigbluebutton-html5/imports/api/voice-users/server/methods.js +++ b/bigbluebutton-html5/imports/api/voice-users/server/methods.js @@ -1,13 +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, - toggleSelfVoice: (credentials) => { muteToggle(credentials, credentials.requesterUserId); }, toggleVoice: muteToggle, muteAllUsers: muteAllToggle, muteAllExceptPresenter: muteAllExceptPresenterToggle, diff --git a/bigbluebutton-html5/imports/api/voice-users/server/methods/ejectUserFromVoice.js b/bigbluebutton-html5/imports/api/voice-users/server/methods/ejectUserFromVoice.js index bc8fe6f20054cbb59ed1247923aa65583b1e5b89..1039a83d9234fa2e47cad612d963c64e8dc81ab7 100644 --- a/bigbluebutton-html5/imports/api/voice-users/server/methods/ejectUserFromVoice.js +++ b/bigbluebutton-html5/imports/api/voice-users/server/methods/ejectUserFromVoice.js @@ -1,16 +1,14 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function ejectUserFromVoice(credentials, userId) { +export default function ejectUserFromVoice(userId) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'EjectUserFromVoiceCmdMsg'; - const { requesterUserId, meetingId } = credentials; - - check(meetingId, String); - check(requesterUserId, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); check(userId, String); const payload = { 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 365423ab698101ba45339d8eba7bb5f8a28deb29..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/api/voice-users/server/methods/listenOnlyToggle.js +++ /dev/null @@ -1,47 +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'; - -export default function listenOnlyToggle(credentials, isJoining = true) { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - - const { meetingId, requesterUserId } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - 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/muteAllExceptPresenterToggle.js b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteAllExceptPresenterToggle.js index db73c7df61fd4c4dc6631c2fdd22a0bbcd7cdb78..c82e280d509f6d11b16d1514ce5a91101ed98f7f 100755 --- a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteAllExceptPresenterToggle.js +++ b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteAllExceptPresenterToggle.js @@ -1,17 +1,15 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; import Meetings from '/imports/api/meetings'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function muteAllExceptPresenterToggle(credentials) { +export default function muteAllExceptPresenterToggle() { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'MuteAllExceptPresentersCmdMsg'; - const { meetingId, requesterUserId } = credentials; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); const meeting = Meetings.findOne({ meetingId }); const toggleMeetingMuted = !meeting.voiceProp.muteOnStart; diff --git a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteAllToggle.js b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteAllToggle.js index 2d142dabef3b20539ceb683c762abc28fbe3867d..ecfd3433f91e48e54343f0967d45ada212b007a1 100755 --- a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteAllToggle.js +++ b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteAllToggle.js @@ -1,17 +1,15 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; import Meetings from '/imports/api/meetings'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function muteAllToggle(credentials) { +export default function muteAllToggle() { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'MuteMeetingCmdMsg'; - const { meetingId, requesterUserId } = credentials; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); const meeting = Meetings.findOne({ meetingId }); const toggleMeetingMuted = !meeting.voiceProp.muteOnStart; 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 be28a30199fa02f69b55a5534a9ca607f1a542f8..fdc3c23f4d4a6df62b6e16e7ce6327bf3a23ddba 100644 --- a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js +++ b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js @@ -1,18 +1,16 @@ import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; +import { extractCredentials } from '/imports/api/common/server/helpers'; import RedisPubSub from '/imports/startup/server/redis'; import Users from '/imports/api/users'; import VoiceUsers from '/imports/api/voice-users'; -export default function muteToggle(credentials, userId) { +export default function muteToggle(uId) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'MuteUserCmdMsg'; - const { meetingId, requesterUserId } = credentials; - - check(meetingId, String); - check(requesterUserId, String); + const { meetingId, requesterUserId } = extractCredentials(this.userId); + const userToMute = uId || requesterUserId; const requester = Users.findOne({ meetingId, @@ -20,7 +18,8 @@ export default function muteToggle(credentials, userId) { }); const voiceUser = VoiceUsers.findOne({ - intId: userId, + intId: userToMute, + meetingId, }); if (!requester || !voiceUser) return; @@ -29,7 +28,7 @@ export default function muteToggle(credentials, userId) { if (listenOnly) return; const payload = { - userId, + userId: userToMute, mutedBy: requesterUserId, mute: !muted, }; diff --git a/bigbluebutton-html5/imports/api/voice-users/server/publishers.js b/bigbluebutton-html5/imports/api/voice-users/server/publishers.js index 22515c0708e3732cc40973aab49a9972ab9d41f8..cca405660072e15fbf437842103ed36b1534b3e0 100644 --- a/bigbluebutton-html5/imports/api/voice-users/server/publishers.js +++ b/bigbluebutton-html5/imports/api/voice-users/server/publishers.js @@ -1,13 +1,13 @@ import VoiceUsers from '/imports/api/voice-users'; import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -function voiceUser(credentials) { - const { meetingId, requesterUserId } = credentials; - - check(meetingId, String); - check(requesterUserId, String); +function voiceUser() { + if (!this.userId) { + return VoiceUsers.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); Logger.debug(`Publishing Voice User for ${meetingId} ${requesterUserId}`); diff --git a/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/changeWhiteboardAccess.js b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/changeWhiteboardAccess.js index 5ceb031e29676cb89eb488a79f7c2b88eaf6b1b4..ffd3b1662695d00fec3c57a6a1d0474b538a06ee 100644 --- a/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/changeWhiteboardAccess.js +++ b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/methods/changeWhiteboardAccess.js @@ -1,17 +1,15 @@ import RedisPubSub from '/imports/startup/server/redis'; import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -export default function changeWhiteboardAccess(credentials, multiUser, whiteboardId) { +export default function changeWhiteboardAccess(multiUser, whiteboardId) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const EVENT_NAME = 'ModifyWhiteboardAccessPubMsg'; - const { meetingId, requesterUserId, requesterToken } = credentials; + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); check(multiUser, Boolean); check(whiteboardId, String); diff --git a/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/publishers.js b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/publishers.js index 6047ac46d10d6daa6b250b38bff64aaea46ce40c..e8e8a8e4b14828f1c2f29e9ac191689cfac6f5f9 100644 --- a/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/publishers.js +++ b/bigbluebutton-html5/imports/api/whiteboard-multi-user/server/publishers.js @@ -1,18 +1,20 @@ import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user/'; import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; +import { extractCredentials } from '/imports/api/common/server/helpers'; -function whiteboardMultiUser(credentials) { - const { meetingId, requesterUserId, requesterToken } = credentials; +function whiteboardMultiUser() { + if (!this.userId) { + return WhiteboardMultiUser.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); - check(meetingId, String); - - Logger.debug(`Publishing whiteboard-multi-user for ${meetingId} ${requesterUserId} ${requesterToken}`); + Logger.debug(`Publishing whiteboard-multi-user for ${meetingId} ${requesterUserId}`); return WhiteboardMultiUser.find({ meetingId }); } + function publish(...args) { const boundMultiUser = whiteboardMultiUser.bind(this); return boundMultiUser(...args); diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx index edc3b6a05fe4c310d257e7604de7e5d930e5742a..48d08d190f02ef6f75be31e6eb5b6293cc3e11b2 100755 --- a/bigbluebutton-html5/imports/startup/client/base.jsx +++ b/bigbluebutton-html5/imports/startup/client/base.jsx @@ -21,7 +21,9 @@ import { notify } from '/imports/ui/services/notification'; import deviceInfo from '/imports/utils/deviceInfo'; import getFromUserSettings from '/imports/ui/services/users-settings'; -const CHAT_ENABLED = Meteor.settings.public.chat.enabled; +const CHAT_CONFIG = Meteor.settings.public.chat; +const CHAT_ENABLED = CHAT_CONFIG.enabled; +const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id; const BREAKOUT_END_NOTIFY_DELAY = 50; @@ -356,7 +358,7 @@ const BaseContainer = withTracker(() => { Session.set('openPanel', 'userlist'); if (CHAT_ENABLED) { Session.set('openPanel', 'chat'); - Session.set('idChatOpen', ''); + Session.set('idChatOpen', PUBLIC_CHAT_ID); } } else { Session.set('openPanel', ''); diff --git a/bigbluebutton-html5/imports/startup/server/index.js b/bigbluebutton-html5/imports/startup/server/index.js index ce679f2ee353cdbd65a3301e2cddc8544be1f826..14c898836e9e44e64afeb852dbfe5bb60a2df611 100755 --- a/bigbluebutton-html5/imports/startup/server/index.js +++ b/bigbluebutton-html5/imports/startup/server/index.js @@ -90,10 +90,9 @@ Meteor.startup(() => { Logger.info('Removing inactive users'); users.forEach((user) => { Logger.info(`Detected inactive user, userId:${user.userId}, meetingId:${user.meetingId}`); - user.requesterUserId = user.userId; - return userLeaving(user, user.userId, user.connectionId); + return userLeaving(user.meetingId, user.userId, user.connectionId); }); - return Logger.info('All inactive user have been removed'); + return Logger.info('All inactive users have been removed'); }, INTERVAL_TIME); Logger.warn(`SERVER STARTED.\nENV=${env},\nnodejs version=${process.version}\nCDN=${CDN_URL}\n`, APP_CONFIG); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx index 6e43bc0c2a33a119d50df0ea7c936dcb28e25824..ec7b1acb777ebbf2dc3db36de4fb40ea4412ce62 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx @@ -186,11 +186,11 @@ class ActionsDropdown extends PureComponent { podIds, } = this.props; - let podId = null; - if (podIds) { - const defaultPodId = podIds[0].podId; - podId = podIds.length > 0 ? defaultPodId : null; - } + if (!podIds || podIds.length < 1) return []; + + // We still have code for other pods from the Flash client. This intentionally only cares + // about the first one because it's the default. + const { podId } = podIds[0]; const presentationItemElements = presentations.map((p) => { const itemStyles = {}; @@ -251,7 +251,6 @@ class ActionsDropdown extends PureComponent { <Button hideLabel aria-label={intl.formatMessage(intlMessages.actionsLabel)} - className={styles.button} label={intl.formatMessage(intlMessages.actionsLabel)} icon="plus" color="primary" diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/captions/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/captions/component.jsx index 3a09d38749f79035b474548fb3d046ca0b6d813d..2ff8328728a4573ae8f4a4c9a5068696ff071798 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/captions/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/captions/component.jsx @@ -24,7 +24,7 @@ const intlMessages = defineMessages({ const CaptionsButton = ({ intl, isActive, handleOnClick }) => ( <Button - className={cx(styles.button, isActive || styles.btn)} + className={cx(isActive || styles.btn)} icon="closed_caption" label={intl.formatMessage(isActive ? intlMessages.stop : intlMessages.start)} color={isActive ? 'primary' : 'default'} diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index fcfcdb55bdd4283e9f9fc8fc471ac5c62bb5a066..cbb2f601bf9e0956fce298540ad1e1a447e0f429 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -3,7 +3,6 @@ import cx from 'classnames'; import { styles } from './styles.scss'; import DesktopShare from './desktop-share/component'; import ActionsDropdown from './actions-dropdown/component'; -import QuickPollDropdown from './quick-poll-dropdown/component'; import AudioControlsContainer from '../audio/audio-controls/container'; import JoinVideoOptionsContainer from '../video-provider/video-button/container'; import CaptionsButtonContainer from '/imports/ui/components/actions-bar/captions/container'; @@ -63,18 +62,6 @@ class ActionsBar extends PureComponent { podIds, }} /> - {isPollingEnabled - ? ( - <QuickPollDropdown - {...{ - currentSlidHasContent, - intl, - amIPresenter, - parseCurrentSlideContent, - }} - /> - ) : null - } {isCaptionsAvailable ? ( <CaptionsButtonContainer {...{ intl }} /> diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx index 3b8ca5bec454f7a7cacb96d71485db5740443e3a..b42908138321762cb3d6cbfc69d092a6b0d8180e 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx @@ -161,7 +161,7 @@ const DesktopShare = ({ return (shouldAllowScreensharing ? ( <Button - className={cx(styles.button, isVideoBroadcasting || styles.btn)} + className={cx(isVideoBroadcasting || styles.btn)} disabled={(!isMeteorConnected && !isVideoBroadcasting) || !screenshareDataSavingSetting} icon={isVideoBroadcasting ? 'desktop' : 'desktop_off'} label={intl.formatMessage(vLabel)} diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx index 3f442c6a74f257a6649be0fef820537ac08ebb95..909115eb6c91e15429b25d05605737a25cb78e8a 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import { defineMessages, injectIntl, intlShape } from 'react-intl'; import Button from '/imports/ui/components/button/component'; import MediaService from '/imports/ui/components/media/service'; -import { styles } from '../styles'; const propTypes = { intl: intlShape.isRequired, @@ -27,7 +26,6 @@ const PresentationOptionsContainer = ({ intl, toggleSwapLayout, isThereCurrentPr if (shouldUnswapLayout()) toggleSwapLayout(); return ( <Button - className={styles.button} icon="presentation" label={intl.formatMessage(intlMessages.restorePresentationLabel)} description={intl.formatMessage(intlMessages.restorePresentationDesc)} diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx index 311968660056490b1f3e8e10c03bf90ecd02955c..5907f095095296cf689879353ec9416eb788ca38 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { defineMessages, intlShape } from 'react-intl'; import _ from 'lodash'; @@ -44,6 +44,7 @@ const handleClickQuickPoll = (slideId, poll) => { const { type } = poll; Session.set('openPanel', 'poll'); Session.set('forcePollOpen', true); + Session.set('pollInitiated', true); makeCall('startPoll', type, slideId); }; @@ -87,40 +88,92 @@ const getAvailableQuickPolls = (slideId, parsedSlides) => { }); }; -const QuickPollDropdown = (props) => { - const { amIPresenter, intl, parseCurrentSlideContent } = props; - const parsedSlide = parseCurrentSlideContent( - intl.formatMessage(intlMessages.yesOptionLabel), - intl.formatMessage(intlMessages.noOptionLabel), - intl.formatMessage(intlMessages.trueOptionLabel), - intl.formatMessage(intlMessages.falseOptionLabel), - ); +class QuickPollDropdown extends Component { + render() { + const { + amIPresenter, + intl, + parseCurrentSlideContent, + startPoll, + currentSlide, + activePoll, + className, + } = this.props; - const { slideId, quickPollOptions } = parsedSlide; + const parsedSlide = parseCurrentSlideContent( + intl.formatMessage(intlMessages.yesOptionLabel), + intl.formatMessage(intlMessages.noOptionLabel), + intl.formatMessage(intlMessages.trueOptionLabel), + intl.formatMessage(intlMessages.falseOptionLabel), + ); + + const { slideId, quickPollOptions } = parsedSlide; + const quickPolls = getAvailableQuickPolls(slideId, quickPollOptions); + + if (quickPollOptions.length === 0) return null; + + let quickPollLabel = ''; + if (quickPolls.length > 0) { + const { props: pollProps } = quickPolls[0]; + quickPollLabel = pollProps.label; + } - return amIPresenter && quickPollOptions && quickPollOptions.length ? ( - <Dropdown> - <DropdownTrigger tabIndex={0}> + let singlePollType = null; + if (quickPolls.length === 1 && quickPollOptions.length) { + const { type } = quickPollOptions[0]; + singlePollType = type; + } + + let btn = ( + <Button + aria-label={intl.formatMessage(intlMessages.quickPollLabel)} + className={styles.quickPollBtn} + label={quickPollLabel} + tooltipLabel={intl.formatMessage(intlMessages.quickPollLabel)} + onClick={() => startPoll(singlePollType, currentSlide.id)} + size="lg" + disabled={!!activePoll} + /> + ); + + const usePollDropdown = quickPollOptions && quickPollOptions.length && quickPolls.length > 1; + let dropdown = null; + + if (usePollDropdown) { + btn = ( <Button aria-label={intl.formatMessage(intlMessages.quickPollLabel)} - circle - className={styles.button} - color="primary" - hideLabel - icon="polling" - label={intl.formatMessage(intlMessages.quickPollLabel)} + className={styles.quickPollBtn} + label={quickPollLabel} + tooltipLabel={intl.formatMessage(intlMessages.quickPollLabel)} onClick={() => null} size="lg" + disabled={!!activePoll} /> - </DropdownTrigger> - <DropdownContent placement="top left"> - <DropdownList> - {getAvailableQuickPolls(slideId, quickPollOptions)} - </DropdownList> - </DropdownContent> - </Dropdown> - ) : null; -}; + ); + + dropdown = ( + <Dropdown className={className}> + <DropdownTrigger tabIndex={0}> + {btn} + </DropdownTrigger> + <DropdownContent placement="top left"> + <DropdownList> + {quickPolls} + </DropdownList> + </DropdownContent> + </Dropdown> + ); + } + + return amIPresenter && usePollDropdown ? ( + dropdown + ) : ( + btn + ); + } +} + QuickPollDropdown.propTypes = propTypes; diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/container.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d6f7eeadceea212b2530e37aaecaa7f29dad23c9 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/container.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Meteor } from 'meteor/meteor'; +import { withTracker } from 'meteor/react-meteor-data'; +import { injectIntl } from 'react-intl'; +import QuickPollDropdown from './component'; + +const QuickPollDropdownContainer = props => <QuickPollDropdown {...props} />; + +export default withTracker(() => ({ + activePoll: Session.get('pollInitiated') || false, +}))(injectIntl(QuickPollDropdownContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss index 2dc98929db8276addc68ff732f312786660e56f4..d30cd81c5631994f6ea689dff4b4cfd51c559c00 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss @@ -73,9 +73,24 @@ } } -.button { +.quickPollBtn { + padding: var(--whiteboard-toolbar-padding); + background-color: var(--color-off-white) !important; + box-shadow: none !important; + span:first-child { - box-shadow: 0 2px 5px 0 rgb(0, 0, 0); + border: 1px solid var(--toolbar-button-color); + border-radius: var(--border-size-large); + color: var(--toolbar-button-color); + font-size: small; + font-weight: var(--headings-font-weight); + opacity: 1; + padding-right: var(--border-size-large); + padding-left: var(--border-size-large); + } + + span:first-child:hover { + opacity: 1 !important; } } diff --git a/bigbluebutton-html5/imports/ui/components/app/styles.scss b/bigbluebutton-html5/imports/ui/components/app/styles.scss index d719ba56c8f1184ac5853210d07001603b8fbe55..0d03f0eba183c776db7a25b3910ff4078da5d792 100755 --- a/bigbluebutton-html5/imports/ui/components/app/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/app/styles.scss @@ -108,7 +108,7 @@ @extend %text-elipsis; @include highContrastOutline(); outline-style: solid; - z-index: 2; + z-index: 3; overflow: visible; order: 1; @@ -148,7 +148,7 @@ order: 2; height: 100%; @include mq($small-only) { - z-index: 3; + z-index: 4; height: auto; top: var(--navbar-height); overflow: visible; @@ -198,6 +198,7 @@ position: relative; margin: 0 0 0 var(--panel-margin-right); overflow: hidden; + z-index: 0; [dir="rtl"] & { margin: 0 var(--panel-margin-right) 0 0; diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx index caa79c1c0296931289e6f240721afa2ec4c566ee..04161e8635cfaaa187dee9d1e6651fd7f21c69c1 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx @@ -14,7 +14,7 @@ const AudioControlsContainer = props => <AudioControls {...props} />; const processToggleMuteFromOutside = (e) => { switch (e.data) { case 'c_mute': { - makeCall('toggleSelfVoice'); + makeCall('toggleVoice'); break; } case 'get_audio_joined_status': { diff --git a/bigbluebutton-html5/imports/ui/components/audio/service.js b/bigbluebutton-html5/imports/ui/components/audio/service.js index 0e7ffaf27c29391c97fb420a61722c70a7fc7c81..59efc2b487b2edb8fb20d7bda4b3d2522722add6 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/service.js +++ b/bigbluebutton-html5/imports/ui/components/audio/service.js @@ -50,13 +50,13 @@ const toggleMuteMicrophone = () => { logCode: 'audiomanager_unmute_audio', extraInfo: { logType: 'user_action' }, }, 'microphone unmuted by user'); - makeCall('toggleSelfVoice'); + makeCall('toggleVoice'); } else { logger.info({ logCode: 'audiomanager_mute_audio', extraInfo: { logType: 'user_action' }, }, 'microphone muted by user'); - makeCall('toggleSelfVoice'); + makeCall('toggleVoice'); } }; diff --git a/bigbluebutton-html5/imports/ui/components/authenticated-handler/component.jsx b/bigbluebutton-html5/imports/ui/components/authenticated-handler/component.jsx index 209052abd1b6053409f12ead0125bd7c08a8c339..d61e337e7c7c3f2b6aff526ce5cef995cc487741 100644 --- a/bigbluebutton-html5/imports/ui/components/authenticated-handler/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/authenticated-handler/component.jsx @@ -5,8 +5,6 @@ import Auth from '/imports/ui/services/auth'; import LoadingScreen from '/imports/ui/components/loading-screen/component'; const STATUS_CONNECTING = 'connecting'; -const CHAT_CONFIG = Meteor.settings.public.chat; -const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id; class AuthenticatedHandler extends Component { static setError(codeError) { @@ -68,7 +66,9 @@ class AuthenticatedHandler extends Component { } componentDidMount() { - if (Session.get('codeError')) return this.changeState(true); + if (Session.get('codeError')) { + this.setState({ authenticated: true }); + } AuthenticatedHandler.authenticatedRouteHandler((value, error) => { if (error) AuthenticatedHandler.setError(error); this.setState({ authenticated: true }); @@ -83,10 +83,9 @@ class AuthenticatedHandler extends Component { authenticated, } = this.state; - Session.set('isChatOpen', false); - Session.set('idChatOpen', PUBLIC_CHAT_ID); Session.set('isMeetingEnded', false); Session.set('isPollOpen', false); + // TODO: breakoutRoomIsOpen doesn't seem used Session.set('breakoutRoomIsOpen', false); return authenticated diff --git a/bigbluebutton-html5/imports/ui/components/chat/alert/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/alert/container.jsx index 3c15dcd777c164e1d819ef0b76e25f419f2db40e..6091d870b6fd8d288493c00c7052709599d842f0 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/alert/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/alert/container.jsx @@ -14,12 +14,23 @@ export default withTracker(() => { const AppSettings = Settings.application; const activeChats = UserListService.getActiveChats(); const { loginTime } = Users.findOne({ userId: Auth.userID }, { fields: { loginTime: 1 } }); + + const openPanel = Session.get('openPanel'); + let idChatOpen = Session.get('idChatOpen'); + + // Currently the panel can switch from the chat panel to something else and the idChatOpen won't + // always reset. A better solution would be to make the openPanel Session variable an + // Object { panelType: <String>, panelOptions: <Object> } and then get rid of idChatOpen + if (openPanel !== 'chat') { + idChatOpen = ''; + } + return { audioAlertDisabled: !AppSettings.chatAudioAlerts, pushAlertDisabled: !AppSettings.chatPushAlerts, activeChats, publicUserId: Meteor.settings.public.chat.public_group_id, joinTimestamp: loginTime, - idChatOpen: Session.get('idChatOpen'), + idChatOpen, }; })(memo(ChatAlertContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/chat/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/container.jsx index 7dfe11426d45a0714ee3daa6b7f7e9e095320f66..4efc1f5d4f096407ba7db33ee335f7e78156a26a 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/container.jsx @@ -39,7 +39,15 @@ class ChatContainer extends PureComponent { } render() { - const { children } = this.props; + const { + children, + unmounting, + } = this.props; + + if (unmounting === true) { + return null; + } + return ( <Chat {...this.props}> {children} @@ -49,7 +57,7 @@ class ChatContainer extends PureComponent { } export default injectIntl(withTracker(({ intl }) => { - const chatID = Session.get('idChatOpen') || PUBLIC_CHAT_KEY; + const chatID = Session.get('idChatOpen'); let messages = []; let isChatLocked = ChatService.isChatLocked(chatID); let title = intl.formatMessage(intlMessages.titlePublic); @@ -109,7 +117,7 @@ export default injectIntl(withTracker(({ intl }) => { .concat(messagesAfterWelcomeMsg); messages = messagesFormated.sort((a, b) => (a.time - b.time)); - } else { + } else if (chatID) { messages = ChatService.getPrivateGroupMessages(); const receiverUser = ChatService.getUser(chatID); @@ -135,6 +143,11 @@ export default injectIntl(withTracker(({ intl }) => { messages.push(messagePartnerLoggedOut); isChatLocked = true; } + } else { + // No chatID is set so the panel is closed, about to close, or wasn't opened correctly + return { + unmounting: true, + }; } messages = messages.map((message) => { diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-form/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-form/component.jsx index e05191fa77a927c01b3229fe4be0ae3f80345723..d7507ee471a7e01a6984c7b0ba978b7fb75ed3de 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/message-form/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/message-form/component.jsx @@ -45,9 +45,6 @@ const messages = defineMessages({ id: 'app.chat.inputPlaceholder', description: 'Chat message input placeholder', }, - errorMinMessageLength: { - id: 'app.chat.errorMinMessageLength', - }, errorMaxMessageLength: { id: 'app.chat.errorMaxMessageLength', }, @@ -230,7 +227,6 @@ class MessageForm extends PureComponent { e.preventDefault(); const { - intl, disabled, minMessageLength, maxMessageLength, @@ -240,18 +236,9 @@ class MessageForm extends PureComponent { const { message } = this.state; let msg = message.trim(); - if (message.length < minMessageLength) { - this.setState({ - hasErrors: true, - error: intl.formatMessage( - messages.errorMinMessageLength, - { 0: minMessageLength - message.length }, - ), - }); - } + if (msg.length < minMessageLength) return; if (disabled - || msg.length === 0 || msg.length > maxMessageLength) { this.setState({ hasErrors: true }); return false; diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx index f53367646debfb97a7d1c9be5be0f0afbce82097..9ea5eab539d232d3ef1c5192db91415f72d82d65 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx @@ -66,6 +66,7 @@ class MessageList extends Component { }; this.listRef = null; + this.virualRef = null; this.lastWidth = 0; } @@ -75,9 +76,22 @@ class MessageList extends Component { scrollPosition, } = this.props; this.scrollTo(scrollPosition); + + const { childNodes } = this.messageListWrapper; + this.virualRef = childNodes ? childNodes[0].firstChild : null; + + if (this.virualRef) { + this.virualRef.style.direction = document.documentElement.dir; + } } componentDidUpdate(prevProps) { + if (this.virualRef) { + if (this.virualRef.style.direction !== document.documentElement.dir) { + this.virualRef.style.direction = document.documentElement.dir; + } + } + const { scrollPosition, chatId, @@ -275,7 +289,7 @@ class MessageList extends Component { } = this.state; return ( - [<div className={styles.messageListWrapper} key="chat-list" data-test="chatMessages"> + [<div className={styles.messageListWrapper} key="chat-list" data-test="chatMessages" ref={node => this.messageListWrapper = node}> <AutoSizer> {({ height, width }) => { if (width !== this.lastWidth) { diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss index 1aae167fec600b062601dc58e179748073179f73..5b8817b1c6822a1662591941eb2c25846fa98abb 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss @@ -69,7 +69,7 @@ display: flex; flex: 1; flex-flow: row; - line-height: 1; + line-height: 1.35; & + .message { margin-top: 0; diff --git a/bigbluebutton-html5/imports/ui/components/chat/service.js b/bigbluebutton-html5/imports/ui/components/chat/service.js index e4f3f5c0a1f7bc5c5f97cf3c5328c4c36e4d11a5..5cf7bf787b88e7b7986568af000f4192351e0ea5 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/service.js +++ b/bigbluebutton-html5/imports/ui/components/chat/service.js @@ -172,7 +172,7 @@ const lastReadMessageTime = (receiverID) => { }; const sendGroupMessage = (message) => { - const chatID = Session.get('idChatOpen') || PUBLIC_CHAT_ID; + const chatID = Session.get('idChatOpen'); const isPublicChat = chatID === PUBLIC_CHAT_ID; let destinationChatId = PUBLIC_GROUP_CHAT_ID; @@ -223,7 +223,7 @@ const updateScrollPosition = position => ScrollCollection.upsert( ); const updateUnreadMessage = (timestamp) => { - const chatID = Session.get('idChatOpen') || PUBLIC_CHAT_ID; + const chatID = Session.get('idChatOpen'); const isPublic = chatID === PUBLIC_CHAT_ID; const chatType = isPublic ? PUBLIC_GROUP_CHAT_ID : chatID; return UnreadMessages.update(chatType, timestamp); diff --git a/bigbluebutton-html5/imports/ui/components/cursor/service.js b/bigbluebutton-html5/imports/ui/components/cursor/service.js index 3a2efb7992b926fd8933f76d802343bcce4a8bc1..7c7893fb4d49e3850c11523215fcb34425998aea 100755 --- a/bigbluebutton-html5/imports/ui/components/cursor/service.js +++ b/bigbluebutton-html5/imports/ui/components/cursor/service.js @@ -26,7 +26,7 @@ export function publishCursorUpdate(payload) { const throttledEmit = throttle(cursorStreamListener.emit.bind(cursorStreamListener), 30, { trailing: true }); throttledEmit('publish', { - credentials: Auth.credentials, + userId: Auth.userID, payload, }); } diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss b/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss index c4caf291afa428e0958a6b7230f6d4a3c107ad2c..d26b835073da3f7539296574a636ce265fac34f7 100755 --- a/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss @@ -25,6 +25,7 @@ .verticalList { @extend %list; flex-direction: column; + width: 100%; } .horizontalList { diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/title/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/list/title/component.jsx index 241b8fc7b3e4b9d4725077a2e08b990aab928d37..d2acd5ff72abce24c694d055b835c5e95ca09d04 100644 --- a/bigbluebutton-html5/imports/ui/components/dropdown/list/title/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/title/component.jsx @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; +import _ from 'lodash'; import { styles } from '../styles'; const propTypes = { @@ -14,7 +15,7 @@ export default class DropdownListTitle extends Component { } render() { - const { className, description } = this.props; + const { className } = this.props; return ( <li className={cx(styles.title, className)} aria-hidden> diff --git a/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx b/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx index bfcf865a4eceb677c70f2948cabeace51ab48e2b..bf8ddcee4bf63c5febcfb0732e011dd94c97ce73 100644 --- a/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx @@ -10,7 +10,7 @@ import { styles } from './styles'; const intlMessages = defineMessages({ 500: { id: 'app.error.500', - defaultMessage: 'Ops, something went wrong', + defaultMessage: 'Oops, something went wrong', }, 410: { id: 'app.error.410', 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/media/webcam-draggable-overlay/component.jsx b/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx index 501e43def113aee9c7728ebb389b8763627c99bb..a899153b56555ce296778711745782f7a7dd7be8 100644 --- a/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx @@ -159,11 +159,11 @@ class WebcamDraggable extends PureComponent { const { webcamDraggableState } = this.props; const { optimalGrid, placement } = webcamDraggableState; if (placement === 'top' || placement === 'bottom') { - const mediaHeight = $('section[class^=media]').height(); + const mediaHeight = document.querySelector('section[class^=media]').offsetHeight; this.setState({ placementPercent: (optimalGrid.height * 100) / mediaHeight }); } if (placement === 'left' || placement === 'right') { - const mediaWidth = $('section[class^=media]').width(); + const mediaWidth = document.querySelector('section[class^=media]').offsetWidth; this.setState({ placementPercent: (optimalGrid.width * 100) / mediaWidth }); } } @@ -441,8 +441,8 @@ class WebcamDraggable extends PureComponent { [styles.dropZoneBgRight]: true, }); - const mHeight = $('section[class^=media]').height(); - const mWidth = $('section[class^=media]').width(); + const mHeight = document.querySelector('section[class^=media]').offsetHeight; + const mWidth = document.querySelector('section[class^=media]').offsetWidth; let resizeWidth; let resizeHeight; diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx index 8ead98f12a5291fc1c53078c7a0905a13775b003..703e71b90ed91a30a25144c4cba36a9ff74a9e2d 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx @@ -97,6 +97,7 @@ class NavBar extends PureComponent { ghost circle hideLabel + data-test={hasUnreadMessages ? 'hasUnreadMessages' : null} label={intl.formatMessage(intlMessages.toggleUserListLabel)} aria-label={ariaLabel} icon="user" diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx index 299b273f6d2f9c325607c77e018817fceb022f0c..1da1181f9ef240d79e04d5f98e57444d1d79b6c2 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx @@ -228,6 +228,7 @@ class SettingsDropdown extends PureComponent { (<DropdownListItem key="list-item-settings" icon="settings" + data-test="settings" label={intl.formatMessage(intlMessages.settingsLabel)} description={intl.formatMessage(intlMessages.settingsDesc)} onClick={() => mountModal(<SettingsMenuContainer />)} diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/talking-indicator/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/talking-indicator/component.jsx index 429e096d8f6278fcc34728af889c2da60c03365f..6cc2d58adc2e5b645981bf5fee42ecf4dff0e74b 100644 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/talking-indicator/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/talking-indicator/component.jsx @@ -72,6 +72,7 @@ class TalkingIndicator extends PureComponent { ? `${intl.formatMessage(intlMessages.muteLabel)} ${callerName}` : null } + data-test={talking ? 'isTalking' : 'wasTalking'} aria-label={ariaLabel} aria-describedby={talking ? 'description' : null} color="primary" diff --git a/bigbluebutton-html5/imports/ui/components/note/service.js b/bigbluebutton-html5/imports/ui/components/note/service.js index 3ae8dde37694344f9d9e93ba579d940ae40e2f70..0bdfa4d02a6accfccf72fa28ded9c90015d3e4a0 100644 --- a/bigbluebutton-html5/imports/ui/components/note/service.js +++ b/bigbluebutton-html5/imports/ui/components/note/service.js @@ -97,11 +97,19 @@ const isEnabled = () => { return NOTE_CONFIG.enabled && note; }; +const toggleNotePanel = () => { + Session.set( + 'openPanel', + isPanelOpened() ? 'userlist' : 'note' + ); +}; + const isPanelOpened = () => Session.get('openPanel') === 'note'; export default { getNoteURL, getReadOnlyURL, + toggleNotePanel, isLocked, isEnabled, isPanelOpened, diff --git a/bigbluebutton-html5/imports/ui/components/poll/component.jsx b/bigbluebutton-html5/imports/ui/components/poll/component.jsx index d2d0a6594861eb54bddf6793e48824d22ff9308c..ff61eb4f92109e2cf3459becd7354b9cc19d058b 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/poll/component.jsx @@ -357,6 +357,7 @@ class Poll extends Component { } Session.set('openPanel', 'userlist'); Session.set('forcePollOpen', false); + Session.set('pollInitiated', false); }} className={styles.closeBtn} icon="close" diff --git a/bigbluebutton-html5/imports/ui/components/poll/container.jsx b/bigbluebutton-html5/imports/ui/components/poll/container.jsx index 2665e2cb68d7d371d957e793a3e18f7d5aa02451..a0a2c52af8a3908ffa84e706e4a088ccbbf17490 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/poll/container.jsx @@ -10,7 +10,7 @@ import Service from './service'; const PollContainer = ({ ...props }) => <Poll {...props} />; export default withTracker(() => { - Meteor.subscribe('current-poll', Auth.meetingID); + Meteor.subscribe('current-poll'); const currentPresentation = Presentations.findOne({ current: true, diff --git a/bigbluebutton-html5/imports/ui/components/poll/live-result/component.jsx b/bigbluebutton-html5/imports/ui/components/poll/live-result/component.jsx index e267b74db9d32531ef9c832c30cf11c1813c6842..5556b4873dd3a05f95c4c3811132750815e5e263 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/live-result/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/poll/live-result/component.jsx @@ -190,6 +190,7 @@ class LiveResult extends PureComponent { <Button disabled={!isMeteorConnected} onClick={() => { + Session.set('pollInitiated', false); Service.publishPoll(); const { answers, numRespondents } = currentPoll; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx index c56b776c4391349d9ee2c3d8acdfa7490a189728..af7c7d4d5d44ae2599599a22eb2467767c693e71 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx @@ -522,6 +522,7 @@ class PresentationArea extends PureComponent { fitToWidth, zoom, podId, + currentSlide, }} isFullscreen={isFullscreen} fullscreenRef={this.refPresentationContainer} diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx index 3f87f88623c052544c5bb07ed0c003edfc546845..78ab1defa314dec05a77aba9c188b75767920a03 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx @@ -10,6 +10,7 @@ import { styles } from './styles.scss'; import ZoomTool from './zoom-tool/component'; import FullscreenButtonContainer from '../../fullscreen-button/container'; import Tooltip from '/imports/ui/components/tooltip/component'; +import QuickPollDropdownContainer from '/imports/ui/components/actions-bar/quick-poll-dropdown/container'; import KEY_CODES from '/imports/utils/keyCodes'; const intlMessages = defineMessages({ @@ -211,6 +212,12 @@ class PresentationToolbar extends PureComponent { isFullscreen, fullscreenRef, isMeteorConnected, + isPollingEnabled, + amIPresenter, + currentSlidHasContent, + parseCurrentSlideContent, + startPoll, + currentSlide, } = this.props; const BROWSER_RESULTS = browser(); @@ -231,7 +238,25 @@ class PresentationToolbar extends PureComponent { return ( <div id="presentationToolbarWrapper" className={styles.presentationToolbarWrapper}> {this.renderAriaDescs()} - {<div />} + { + <div> + {isPollingEnabled + ? ( + <QuickPollDropdownContainer + {...{ + currentSlidHasContent, + intl, + amIPresenter, + parseCurrentSlideContent, + startPoll, + currentSlide, + }} + className={styles.presentationBtn} + /> + ) : null + } + </div> + } { <div className={styles.presentationSlideControls}> <Button diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx index 8216b01653adb5d4b9c047f19199851404a239a4..fe15bf91366eef11ce174f796d17f25a4e256f7f 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx @@ -3,9 +3,13 @@ import PropTypes from 'prop-types'; import { withTracker } from 'meteor/react-meteor-data'; import PresentationService from '/imports/ui/components/presentation/service'; import MediaService from '/imports/ui/components/media/service'; +import Service from '/imports/ui/components/actions-bar/service'; +import { makeCall } from '/imports/ui/services/api'; import PresentationToolbar from './component'; import PresentationToolbarService from './service'; +const POLLING_ENABLED = Meteor.settings.public.poll.enabled; + const PresentationToolbarContainer = (props) => { const { userIsPresenter, @@ -30,7 +34,15 @@ export default withTracker((params) => { presentationId, } = params; + const startPoll = (type, id) => { + Session.set('openPanel', 'poll'); + Session.set('forcePollOpen', true); + + makeCall('startPoll', type, id); + }; + return { + amIPresenter: Service.amIPresenter(), layoutSwapped: MediaService.getSwapLayout() && MediaService.shouldEnableSwapLayout(), userIsPresenter: PresentationService.isPresenter(podId), numberOfSlides: PresentationToolbarService.getNumberOfSlides(podId, presentationId), @@ -38,6 +50,10 @@ export default withTracker((params) => { previousSlide: PresentationToolbarService.previousSlide, skipToSlide: PresentationToolbarService.skipToSlide, isMeteorConnected: Meteor.status().connected, + isPollingEnabled: POLLING_ENABLED, + currentSlidHasContent: PresentationService.currentSlidHasContent(), + parseCurrentSlideContent: PresentationService.parseCurrentSlideContent, + startPoll, }; })(PresentationToolbarContainer); diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss index a250c30cc09f28a5e295ed3222d256e4cb6fa6da..8dec10b174f04291a2dcecf9b44fc51e518a4576 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss @@ -26,6 +26,16 @@ .presentationBtn { position: relative; + color: var(--toolbar-button-color); + background-color: var(--color-off-white); + border-radius: 0; + box-shadow: none !important; + border: 0; + + &:focus { + background-color: var(--color-off-white); + border: 0; + } } .presentationZoomControls { @@ -86,21 +96,6 @@ } } - button, - select, - >div { - color: var(--toolbar-button-color); - background-color: var(--color-off-white); - border-radius: 0; - box-shadow: none !important; - border: 0; - - &:focus { - background-color: var(--color-off-white); - border: 0; - } - } - i { color: var(--toolbar-button-color); display: flex; 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/imports/ui/components/presentation/presentation-uploader/service.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js index d103c99a3d898e1dd811ee1fc7cb59eb9c33461a..a01c24d1a8cf633d40ab5983d1cb5b17b0f0cca8 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js @@ -113,7 +113,7 @@ const requestPresentationUploadToken = ( Tracker.autorun((c) => { computation = c; - const sub = Meteor.subscribe('presentation-upload-token', Auth.credentials, podId, filename); + const sub = Meteor.subscribe('presentation-upload-token', podId, filename); if (!sub.ready()) return; const PresentationToken = PresentationUploadToken.findOne({ diff --git a/bigbluebutton-html5/imports/ui/components/presentation/styles.scss b/bigbluebutton-html5/imports/ui/components/presentation/styles.scss index 58ec9110c80ba10b81d99bae2e53ca9dcd3306e2..28a8236a677663d13f46208fcda55f9f42471461 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/presentation/styles.scss @@ -54,7 +54,7 @@ .presentationToolbar{ display: flex; - overflow-x: auto; + overflow-x: visible; order: 2; position: absolute; bottom: 0; diff --git a/bigbluebutton-html5/imports/ui/components/shortcut-help/service.jsx b/bigbluebutton-html5/imports/ui/components/shortcut-help/service.jsx index 692fc6c998722d6fec95a5f654b029d1804359bd..01d1a5e50024978e14712c711787ebb3b1b4b993 100644 --- a/bigbluebutton-html5/imports/ui/components/shortcut-help/service.jsx +++ b/bigbluebutton-html5/imports/ui/components/shortcut-help/service.jsx @@ -10,9 +10,9 @@ const withShortcutHelper = (WrappedComponent, param) => (props) => { if (ENABLED_SHORTCUTS) { shortcuts = Object.values(BASE_SHORTCUTS).map((el) => { const obj = { ...el }; - obj.descIdLowerCase = obj.descId.toLowerCase(); + obj.descId = obj.descId.toLowerCase(); return obj; - }).filter(el => ENABLED_SHORTCUTS.includes(el.descIdLowerCase)); + }).filter(el => ENABLED_SHORTCUTS.includes(el.descId.toLowerCase())); } if (param !== undefined) { diff --git a/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx b/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx index 80f4efc3037caa911f599f7b205102a99cbf1773..62fd2f003acd7edd452908d623280cdd640a9be7 100755 --- a/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx @@ -20,6 +20,7 @@ const SUBSCRIPTIONS = [ 'voiceUsers', 'whiteboard-multi-user', 'screenshare', 'group-chat', 'presentation-pods', 'users-settings', 'guestUser', 'users-infos', 'note', 'meeting-time-remaining', 'network-information', 'ping-pong', 'local-settings', 'users-typing', 'record-meetings', 'video-streams', + 'voice-call-states', ]; class Subscriptions extends Component { @@ -83,7 +84,7 @@ export default withTracker(() => { const chatIds = chats.map(chat => chat.chatId); - groupChatMessageHandler = Meteor.subscribe('group-chat-msg', credentials, chatIds, subscriptionErrorHandler); + groupChatMessageHandler = Meteor.subscribe('group-chat-msg', chatIds, subscriptionErrorHandler); subscriptionsHandlers.push(groupChatMessageHandler); } @@ -91,12 +92,12 @@ export default withTracker(() => { if (User) { const userIsModerator = User.role === ROLE_MODERATOR; - Meteor.subscribe('users', credentials, userIsModerator, subscriptionErrorHandler); - Meteor.subscribe('breakouts', credentials, userIsModerator, subscriptionErrorHandler); - Meteor.subscribe('meetings', credentials, userIsModerator, subscriptionErrorHandler); + Meteor.subscribe('users', userIsModerator, subscriptionErrorHandler); + Meteor.subscribe('breakouts', userIsModerator, subscriptionErrorHandler); + Meteor.subscribe('meetings', userIsModerator, subscriptionErrorHandler); } - const annotationsHandler = Meteor.subscribe('annotations', credentials, { + const annotationsHandler = Meteor.subscribe('annotations', { onReady: () => { const activeTextShapeId = AnnotationsTextService.activeTextShapeId(); AnnotationsLocal.remove({ id: { $ne: `${activeTextShapeId}-fake` } }); diff --git a/bigbluebutton-html5/imports/ui/components/toast/component.jsx b/bigbluebutton-html5/imports/ui/components/toast/component.jsx index fb6022f218ac10693f6291a21275af70b2a5bd01..14008fc70046fd889e24ca47c6d9883a84ab2c43 100755 --- a/bigbluebutton-html5/imports/ui/components/toast/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/toast/component.jsx @@ -37,7 +37,7 @@ const Toast = ({ <div className={cx(styles.icon, small ? styles.smallIcon : null)}> <Icon iconName={icon || defaultIcons[type]} /> </div> - <div className={cx(styles.message, small ? styles.smallMessage : null)}> + <div data-test="toastSmallMsg" className={cx(styles.message, small ? styles.smallMessage : null)}> <span>{message}</span> </div> </div> diff --git a/bigbluebutton-html5/imports/ui/components/user-list/captions-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/captions-list-item/component.jsx index eab8c234128d3e386906c4f61d3be7f1e52aabc5..cb52c2b9fd4d1a1ee273c1dee02d0dd522551fd0 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/captions-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/captions-list-item/component.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import Icon from '/imports/ui/components/icon/component'; import { Session } from 'meteor/session'; import { defineMessages, injectIntl, intlShape } from 'react-intl'; -import { styles } from './styles'; +import { styles } from '/imports/ui/components/user-list/user-list-content/styles'; const propTypes = { intl: intlShape.isRequired, @@ -49,7 +49,7 @@ const CaptionsListItem = (props) => { role="button" tabIndex={tabIndex} id={locale.locale} - className={styles.captionsListItem} + className={styles.listItem} onClick={() => handleClickToggleCaptions(locale.locale)} aria-label={`${locale.name} ${intl.formatMessage(intlMessages.captionLabel)}`} > diff --git a/bigbluebutton-html5/imports/ui/components/user-list/captions-list-item/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/captions-list-item/styles.scss deleted file mode 100644 index fec0cd4a97b9f7033deb5111871a5c8589381648..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/ui/components/user-list/captions-list-item/styles.scss +++ /dev/null @@ -1,53 +0,0 @@ -@import "/imports/ui/stylesheets/variables/_all"; -@import "/imports/ui/components/user-list/styles.scss"; - -.captionsListItem { - @extend %list-item; - align-items: center; - cursor: pointer; - display: flex; - flex-flow: row; - flex-grow: 0; - flex-shrink: 0; - padding-top: var(--lg-padding-y); - padding-bottom: var(--lg-padding-y); - padding-left: var(--lg-padding-y); - text-decoration: none; - width: 100%; - color: var(--color-gray-dark); - background-color: var(--color-off-white); - - [dir="rtl"] & { - padding-right: var(--lg-padding-y); - padding-left: none; - } - - > i { - display: flex; - font-size: 175%; - color: var(--color-gray-light); - flex: 0 0 2.2rem; - } - - > span { - font-weight: 400; - font-size: var(--font-size-small); - color: var(--color-gray-dark); - position: relative; - flex-grow: 1; - line-height: 2; - text-align: left; - padding-left: var(--lg-padding-y); - text-overflow: ellipsis; - - [dir="rtl"] & { - text-align: right; - padding-right: var(--md-padding-x); - } - } - - &:active { - background-color: var(--list-item-bg-hover); - box-shadow: inset 0 0 0 var(--border-size) var(--item-focus-border), inset 1px 0 0 1px var(--item-focus-border); - } -} diff --git a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/chat-unread-messages/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/chat-unread-messages/component.jsx index a6ac2cb9ff972422e2165fc0aae2d786e6b899f1..c42057bc15fc17820445dcd4adcb80c38da04e81 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/chat-unread-messages/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/chat-unread-messages/component.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; -import { styles } from './styles'; +import { styles } from '/imports/ui/components/user-list/user-list-content/styles'; const intlMessages = defineMessages({ unreadPlural: { diff --git a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/chat-unread-messages/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/chat-unread-messages/styles.scss deleted file mode 100644 index 5f4c6341db98ce55d9409ad3018efb7c3fcce36d..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/chat-unread-messages/styles.scss +++ /dev/null @@ -1,19 +0,0 @@ -@import "/imports/ui/components/user-list/styles.scss"; - -.unreadMessages { - @extend %flex-column; - justify-content: center; -} - -.unreadMessagesText { - @extend %flex-column; - @extend %no-margin; - justify-content: center; - color: var(--color-white); - line-height: calc(1rem + 1px); - padding: 0 0.5rem; - text-align: center; - border-radius: 0.5rem/50%; - font-size: 0.8rem; - background-color: var(--unread-messages-bg); -} diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js index 189a52085f1c1f95e4f7d3da935fad72cedb893d..e3b0e38a39dc04739809cdb782718b19356dfe4e 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/service.js +++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js @@ -159,6 +159,15 @@ const sortChatsByIcon = (a, b) => { return 0; }; +const sortByRecentActivity = (a, b) => { + const _a = a.lastActivity; + const _b = b.lastActivity; + if (a.userId === 'public') return -1; + if (!_b || _a > _b) return -1; + if (!_a || _a < _b) return 1; + return 0; +}; + const isPublicChat = chat => ( chat.userId === 'public' ); @@ -170,7 +179,7 @@ const sortChats = (a, b) => { sort = sortChatsByName(a, b); } - return sort; + return sort = sortByRecentActivity(a, b); }; const userFindSorting = { @@ -223,8 +232,15 @@ const getActiveChats = (chatID) => { let activeChats = GroupChatMsg .find(filter) - .fetch() - .map(mapActiveChats); + .fetch(); + + const idsWithTimeStamp = {}; + + activeChats.map((chat) => { + idsWithTimeStamp[`${chat.sender}`] = chat.timestamp; + }); + + activeChats = activeChats.map(mapActiveChats); if (chatID) { activeChats.push(chatID); @@ -239,6 +255,7 @@ const getActiveChats = (chatID) => { activeChat.unreadCounter = UnreadMessages.count(op.userId); activeChat.name = op.name; activeChat.isModerator = op.role === ROLE_MODERATOR; + activeChat.lastActivity = idsWithTimeStamp[`${op.userId}`]; return activeChat; }); diff --git a/bigbluebutton-html5/imports/ui/components/user-list/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/styles.scss index c5d0799169c64df86059d074f0958df5603874b9..62212b02ce16ded93da2e349e56541c6e66eeaee 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/user-list/styles.scss @@ -86,7 +86,6 @@ background-color: var(--user-list-bg); color: var(--user-list-text); height: 100%; - padding-top: var(--md-padding-x); } .lists { diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/breakout-room/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/breakout-room/component.jsx index 1ea313e20fe31cc50b0da937ffadd3d4380d49aa..2371966dcebd2ea19ec83c19668894aafb260a61 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/breakout-room/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/breakout-room/component.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; import { Session } from 'meteor/session'; import Icon from '/imports/ui/components/icon/component'; -import { styles } from './styles'; +import { styles } from '/imports/ui/components/user-list/user-list-content/styles'; const intlMessages = defineMessages({ breakoutTitle: { @@ -26,25 +26,25 @@ const BreakoutRoomItem = ({ }) => { if (hasBreakoutRoom) { return ( - <div> - <h2 className={styles.smallTitle}> - {intl.formatMessage(intlMessages.breakoutTitle).toUpperCase()} - </h2> - <div - role="button" - tabIndex={0} - onClick={toggleBreakoutPanel} - data-test="breakoutRoomsItem" - className={styles.BreakoutRoomsItem} - aria-label={intl.formatMessage(intlMessages.breakoutTitle)} - > - <div className={styles.BreakoutRoomsContents} aria-hidden> - <div className={styles.BreakoutRoomsIcon}> + <div className={styles.messages}> + <div className={styles.container}> + <h2 className={styles.smallTitle}> + {intl.formatMessage(intlMessages.breakoutTitle)} + </h2> + </div> + <div className={styles.scrollableList}> + <div className={styles.list}> + <div + role="button" + tabIndex={0} + onClick={toggleBreakoutPanel} + data-test="breakoutRoomsItem" + className={styles.listItem} + aria-label={intl.formatMessage(intlMessages.breakoutTitle)} + > <Icon iconName="rooms" /> + <span aria-hidden>{intl.formatMessage(intlMessages.breakoutTitle)}</span> </div> - <span className={styles.BreakoutRoomsText}> - {intl.formatMessage(intlMessages.breakoutTitle)} - </span> </div> </div> </div> diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/breakout-room/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/breakout-room/styles.scss deleted file mode 100644 index 6f36c918df222a76cb5253ac5ada74056f865297..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/breakout-room/styles.scss +++ /dev/null @@ -1,70 +0,0 @@ -@import "/imports/ui/components/user-list/styles.scss"; -@import "/imports/ui/stylesheets/mixins/_scrollable"; -@import "/imports/ui/stylesheets/mixins/focus"; -@import "/imports/ui/stylesheets/variables/_all"; -@import "/imports/ui/stylesheets/variables/typography"; - -.smallTitle { - @extend .smallTitle; - margin-bottom: var(--lg-padding-y); - margin-top: var(--sm-padding-x); -} - -.BreakoutRoomsContents { - flex-grow: 0; - display: flex; - flex-flow: row; - align-items: center; -} - -.BreakoutRoomsIcon { - display: flex; - flex-flow: row; - justify-content: space-between; - margin-left: var(--sm-padding-x) / 2; - text-align: right; - font-size: 175%; - flex-shrink: 1; - color: var(--user-icons-color); - - [dir="rtl"] & { - text-align: left; - margin-right: var(--sm-padding-x) / 2; - } -} - -.BreakoutRoomsText { - @extend %flex-column; - @extend %no-margin; - @extend %text-elipsis; - font-weight: 400; - font-size: var(--font-size-small); - color: var(--color-gray-dark); - flex-grow: 1; - line-height: 2; - text-align: left; - padding-left: var(--lg-padding-y); - text-overflow: ellipsis; - - [dir="rtl"] & { - text-align: right; - padding-right: var(--lg-padding-y); - } -} - -.BreakoutRoomsItem { - @extend %list-item; - margin-left: var(--md-padding-y); - padding-top: var(--lg-padding-y); - padding-bottom: var(--lg-padding-y); - padding-left: var(--lg-padding-y); - - [dir="rtl"] & { - margin-right: var(--md-padding-y); - padding-right: var(--lg-padding-y); - } -} - -.link { - text-decoration: none; -} \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/styles.scss index a160eab66bd39539c1b42529cb11d32b97c5acf4..fac980e0fd802aac66729997f8fc5c4c305afd6c 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/styles.scss @@ -44,6 +44,77 @@ } } +.listItem { + @extend %list-item; + align-items: center; + cursor: pointer; + display: flex; + flex-flow: row; + flex-grow: 0; + flex-shrink: 0; + padding-top: var(--lg-padding-y); + padding-bottom: var(--lg-padding-y); + padding-left: var(--lg-padding-y); + text-decoration: none; + width: 100%; + color: var(--color-gray-dark); + background-color: var(--color-off-white); + + [dir="rtl"] & { + padding-right: var(--lg-padding-y); + padding-left: none; + } + + > i { + display: flex; + font-size: 175%; + color: var(--color-gray-light); + flex: 0 0 2.2rem; + } + + > span { + font-weight: 400; + font-size: var(--font-size-small); + color: var(--color-gray-dark); + position: relative; + flex-grow: 1; + line-height: 2; + text-align: left; + padding-left: var(--lg-padding-y); + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + + [dir="rtl"] & { + text-align: right; + padding-right: var(--md-padding-x); + } + } + + &:active { + background-color: var(--list-item-bg-hover); + box-shadow: inset 0 0 0 var(--border-size) var(--item-focus-border), inset 1px 0 0 1px var(--item-focus-border); + } +} + +.unreadMessages { + @extend %flex-column; + justify-content: center; +} + +.unreadMessagesText { + @extend %flex-column; + @extend %no-margin; + justify-content: center; + color: var(--color-white); + line-height: calc(1rem + 1px); + padding: 0 0.5rem; + text-align: center; + border-radius: 0.5rem/50%; + font-size: 0.8rem; + background-color: var(--unread-messages-bg); +} + .smallTitle { @extend .smallTitle; flex: 1; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-captions/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-captions/component.jsx index 0d3ea738f77c2f60f13140663a70487d566a8c93..f728ce3a094948ea83a119f6470f08dde8fa146f 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-captions/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-captions/component.jsx @@ -75,9 +75,11 @@ class UserCaptions extends Component { return ( <div className={styles.messages}> - <h2 className={styles.smallTitle}> - {intl.formatMessage(intlMessages.title)} - </h2> + <div className={styles.container}> + <h2 className={styles.smallTitle}> + {intl.formatMessage(intlMessages.title)} + </h2> + </div> <div role="tabpanel" tabIndex={0} diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/component.jsx index 00dccc8cfb986425ced34616406c0bf13139a67f..36c1d41088a199363782713c7dd6660dca7d9fa3 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/component.jsx @@ -121,15 +121,17 @@ class UserMessages extends PureComponent { return ( <div className={styles.messages}> - { - !compact ? ( - <h2 className={styles.smallTitle}> - {intl.formatMessage(intlMessages.messagesTitle)} - </h2> - ) : ( - <hr className={styles.separator} /> - ) - } + <div className={styles.container}> + { + !compact ? ( + <h2 className={styles.smallTitle}> + {intl.formatMessage(intlMessages.messagesTitle)} + </h2> + ) : ( + <hr className={styles.separator} /> + ) + } + </div> <div role="tabpanel" tabIndex={0} diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-notes/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-notes/component.jsx index b54f868289da8cd6eb3a89cd1059479e25a2276c..7dab4f051b7d51ba46b719155684d00aa3cbce06 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-notes/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-notes/component.jsx @@ -1,10 +1,9 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import cx from 'classnames'; import { defineMessages } from 'react-intl'; import Icon from '/imports/ui/components/icon/component'; import NoteService from '/imports/ui/components/note/service'; -import { styles } from './styles'; +import { styles } from '/imports/ui/components/user-list/user-list-content/styles'; const propTypes = { intl: PropTypes.shape({ @@ -15,11 +14,11 @@ const propTypes = { }; const intlMessages = defineMessages({ - notesTitle: { + title: { id: 'app.userList.notesTitle', description: 'Title for the notes list', }, - title: { + sharedNotes: { id: 'app.note.title', description: 'Title for the shared notes', }, @@ -59,56 +58,53 @@ class UserNotes extends Component { } } - render() { - const { intl, isPanelOpened } = this.props; + renderNotes() { + const { intl } = this.props; const { unread } = this.state; - if (!NoteService.isEnabled()) return null; - - const toggleNotePanel = () => { - Session.set( - 'openPanel', - isPanelOpened - ? 'userlist' - : 'note', - ); - }; - - const linkClasses = {}; - linkClasses[styles.active] = isPanelOpened; - - let notification = null; if (unread) { notification = ( <div - className={styles.unreadContent} + className={styles.unreadMessages} aria-label={intl.formatMessage(intlMessages.unreadContent)} > - <div className={styles.unreadContentText} aria-hidden="true"> + <div className={styles.unreadMessagesText} aria-hidden="true"> ··· </div> </div> ); } + return ( + <div + role="button" + tabIndex={0} + className={styles.listItem} + onClick={NoteService.toggleNotePanel} + > + <Icon iconName="copy" /> + <span aria-hidden>{intl.formatMessage(intlMessages.sharedNotes)}</span> + {notification} + </div> + ); + } + + render() { + const { intl } = this.props; + + if (!NoteService.isEnabled()) return null; + return ( <div className={styles.messages}> - { + <div className={styles.container}> <h2 className={styles.smallTitle}> - {intl.formatMessage(intlMessages.notesTitle)} + {intl.formatMessage(intlMessages.title)} </h2> - } + </div> <div className={styles.scrollableList}> - <div - role="button" - tabIndex={0} - className={cx(styles.noteLink, linkClasses)} - onClick={toggleNotePanel} - > - <Icon iconName="copy" /> - <span>{intl.formatMessage(intlMessages.title)}</span> - {notification} + <div className={styles.list}> + {this.renderNotes()} </div> </div> </div> diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-notes/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-notes/styles.scss deleted file mode 100644 index b164690ff4a506d59df6bca664468a82d65f4d7f..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-notes/styles.scss +++ /dev/null @@ -1,96 +0,0 @@ -@import "../../styles.scss"; -@import "/imports/ui/stylesheets/variables/_all"; -@import "/imports/ui/stylesheets/variables/typography"; - -.smallTitle { - font-size: var(--font-size-small); - font-weight: 600; - text-transform: uppercase; - padding: 0 var(--sm-padding-x); - margin-top: var(--sm-padding-x); -} - -.scrollableList { - margin: 0 0 0 var(--md-padding-y); - - [dir="rtl"] & { - margin: 0 var(--md-padding-y) 0 0; - } - - padding-top: var(--border-size); - margin-bottom: var(--sm-padding-y); -} - -.noteLink { - @extend %list-item; - align-items: center; - cursor: pointer; - display: flex; - flex-flow: row; - flex-grow: 0; - flex-shrink: 0; - padding-top: var(--lg-padding-y); - padding-bottom: var(--lg-padding-y); - padding-left: var(--lg-padding-y); - padding-right: 0; - text-decoration: none; - width: 100%; - color: var(--color-gray-dark); - background-color: var(--color-off-white); - - [dir="rtl"] & { - padding-left: 0; - padding-right: var(--lg-padding-y); - } - - > i { - display: flex; - font-size: 175%; - color: var(--color-gray-light); - flex: 0 0 2.2rem; - } - - > span { - font-weight: 400; - font-size: var(--font-size-small); - color: var(--color-gray-dark); - position: relative; - flex-grow: 1; - line-height: 2; - text-align: left; - padding-left: var(--lg-padding-y); - padding-right: var(--sm-padding-y); - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - - [dir="rtl"] & { - text-align: right; - padding-left: 0; - padding-right: var(--lg-padding-y); - } - } -} - -.active { - background-color: var(--list-item-bg-hover); - box-shadow: inset 0 0 0 var(--border-size) var(--item-focus-border), inset 1px 0 0 1px var(--item-focus-border); -} - -.unreadContent { - @extend %flex-column; - justify-content: center; -} - -.unreadContentText { - @extend %flex-column; - @extend %no-margin; - justify-content: center; - color: var(--color-white); - line-height: calc(1rem + 1px); - padding: 0 0.5rem; - text-align: center; - border-radius: 0.5rem/50%; - font-size: 0.8rem; - background-color: var(--unread-messages-bg); -} diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx index a00ff33f836f8009ae3ae6b344836aba789feac3..6b29e66e1c3982009bed2daadb67e0da60d3a688 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx @@ -347,15 +347,6 @@ class UserDropdown extends PureComponent { )); } - if (allowedToRemove && isMeteorConnected) { - actions.push(this.makeDropdownItem( - 'remove', - intl.formatMessage(messages.RemoveUserLabel, { 0: user.name }), - () => this.onActionsHide(removeUser(user.userId)), - 'circle_close', - )); - } - if (allowedToPromote && isMeteorConnected) { actions.push(this.makeDropdownItem( 'promote', @@ -394,6 +385,15 @@ class UserDropdown extends PureComponent { )); } + if (allowedToRemove && isMeteorConnected) { + actions.push(this.makeDropdownItem( + 'remove', + intl.formatMessage(messages.RemoveUserLabel, { 0: user.name }), + () => this.onActionsHide(removeUser(user.userId)), + 'circle_close', + )); + } + return actions; } diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-polls/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-polls/component.jsx index 2f707d083b26b4670e5dc5fff60d7c1a9b983f84..6e61fe0bb6e6142a7bc8008154aa445469730777 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-polls/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-polls/component.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; import Icon from '/imports/ui/components/icon/component'; import { Session } from 'meteor/session'; -import { styles } from './styles'; +import { styles } from '/imports/ui/components/user-list/user-list-content/styles'; const intlMessages = defineMessages({ pollLabel: { @@ -35,20 +35,22 @@ class UserPolls extends PureComponent { return ( <div className={styles.messages}> - { + <div className={styles.container}> <h2 className={styles.smallTitle}> {intl.formatMessage(intlMessages.pollLabel)} </h2> - } - <div className={styles.scrollableList}> - <div - role="button" - tabIndex={0} - className={styles.pollLink} - onClick={handleClickTogglePoll} - > - <Icon iconName="polling" /> - <span>{intl.formatMessage(intlMessages.pollLabel)}</span> + </div> + <div className={styles.list}> + <div className={styles.scrollableList}> + <div + role="button" + tabIndex={0} + className={styles.listItem} + onClick={handleClickTogglePoll} + > + <Icon iconName="polling" /> + <span>{intl.formatMessage(intlMessages.pollLabel)}</span> + </div> </div> </div> </div> diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-polls/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-polls/styles.scss deleted file mode 100644 index cf4b74743f3e82b83efb8e3d25ed47ce10245b0e..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-polls/styles.scss +++ /dev/null @@ -1,74 +0,0 @@ -@import "../../styles.scss"; -@import "/imports/ui/stylesheets/variables/_all"; - -$list-item-bg-hover: darken(#F3F6F9, 7%); -$item-focus-border: #92BCEA; - -.smallTitle { - font-size: var(--font-size-small); - font-weight: 600; - text-transform: uppercase; - padding: 0 var(--sm-padding-x); - color: var(--color-gray-light); - margin-bottom: var(--sm-padding-x); - margin-top: var(--sm-padding-x); -} - -.scrollableList { - margin: 0 0 1px 0.45rem; - - [dir="rtl"] & { - margin: 0 0.45rem 1px 0; - } -} - -.pollLink { - @extend %list-item; - cursor: pointer; - display: flex; - flex-flow: row; - flex-grow: 0; - flex-shrink: 0; - padding-top: var(--lg-padding-y); - padding-bottom: var(--lg-padding-y); - padding-left: var(--lg-padding-y); - padding-right: 0; - text-decoration: none; - width: 100%; - color: var(--color-gray-dark); - background-color: var(--color-off-white); - - [dir="rtl"] & { - padding-left: 0; - padding-right: var(--lg-padding-y); - } - - > i { - display: flex; - font-size: 175%; - color: #8B9AA8; - flex: 0 0 2.2rem; - } - - > span { - font-size: 0.9rem; - font-weight: 400; - color: black; - position: relative; - flex-grow: 1; - line-height: 2; - text-align: left; - padding: 0 0 0 var(--lg-padding-y); - text-overflow: ellipsis; - - [dir="rtl"] & { - text-align: right; - padding: 0 var(--lg-padding-y) 0 0; - } - } - - &:active { - background-color: $list-item-bg-hover; - box-shadow: inset 0 0 0 var(--border-size) $item-focus-border, inset 1px 0 0 1px $item-focus-border; - } -} diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/waiting-users/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/waiting-users/component.jsx index de5261ff5cd8f34eb4d46d1f54e5926fdde06eb1..a4c55265589c333ffa5a3bb3cbdb4d120a91f5b0 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/waiting-users/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/waiting-users/component.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { defineMessages } from 'react-intl'; import Icon from '/imports/ui/components/icon/component'; import { Session } from 'meteor/session'; -import { styles } from './styles'; +import { styles } from '/imports/ui/components/user-list/user-list-content/styles'; const propTypes = { intl: PropTypes.shape({ @@ -39,26 +39,27 @@ class WaitingUsers extends PureComponent { pendingUsers, } = this.props; - return ( <div className={styles.messages}> - { + <div className={styles.container}> <h2 className={styles.smallTitle}> {intl.formatMessage(intlMessages.waitingUsersTitle)} </h2> - } + </div> <div className={styles.scrollableList}> - <div - role='button' - tabIndex={0} - className={styles.noteLink} - onClick={WaitingUsers.toggleWaitingPanel} - > - <Icon iconName="user" className={styles.icon} /> - <span className={styles.label}>{intl.formatMessage(intlMessages.title)}</span> - <div className={styles.waitingUsersWarn}> - <div className={styles.waitingUsersWarnText}> - {pendingUsers.length} + <div className={styles.list}> + <div + role='button' + tabIndex={0} + className={styles.listItem} + onClick={WaitingUsers.toggleWaitingPanel} + > + <Icon iconName="user" /> + <span>{intl.formatMessage(intlMessages.title)}</span> + <div className={styles.unreadMessages}> + <div className={styles.unreadMessagesText}> + {pendingUsers.length} + </div> </div> </div> </div> diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/waiting-users/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/waiting-users/styles.scss deleted file mode 100644 index 703a77bb77c3c9781b21722efef20a56b7dbbc99..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/waiting-users/styles.scss +++ /dev/null @@ -1,83 +0,0 @@ -@import "../../styles.scss"; -@import "/imports/ui/stylesheets/variables/_all"; -@import "/imports/ui/components/user-list/chat-list-item/chat-unread-messages/styles.scss"; - -.waitingUsersWarn { - @extend .unreadMessages; -} - -.waitingUsersWarnText { - @extend .unreadMessagesText; -} - -.smallTitle { - font-size: var(--font-size-small); - font-weight: 600; - text-transform: uppercase; - padding: 0 var(--sm-padding-x); - color: var(--color-gray-light); - margin-bottom: var(--sm-padding-x); - margin-top: var(--sm-padding-x); -} - -.scrollableList { - margin: 0 0 1px 0.45rem; - outline: none; - - [dir="rtl"] & { - margin: 0 0.45rem 1px 0; - } -} - -.noteLink { - @extend %list-item; - - cursor: pointer; - display: flex; - flex-flow: row; - flex-grow: 0; - flex-shrink: 0; - padding-top: var(--lg-padding-y); - padding-bottom: var(--lg-padding-y); - padding-left: var(--lg-padding-y); - padding-right: 0; - text-decoration: none; - width: 100%; - color: var(--color-gray-dark); - background-color: var(--color-off-white); - - [dir="rtl"] & { - padding-left: 0; - padding-right: var(--lg-padding-y); - } - - > i { - display: flex; - font-size: 175%; - color: var(--color-gray-light); - flex: 0 0 2.2rem; - } - - > span { - font-size: 0.9rem; - font-weight: 400; - color: black; - position: relative; - flex-grow: 1; - line-height: 2; - text-align: left; - padding: 0 0 0 var(--lg-padding-y); - text-overflow: ellipsis; - - [dir="rtl"] & { - text-align: right; - padding: 0 var(--lg-padding-y) 0 0; - } - } - - &:active { - background-color: var(--list-item-bg-hover); - box-shadow: inset 0 0 0 var(--border-size) var(--item-focus-border), inset 1px 0 0 1px var(--item-focus-border); - outline: none; - } -} diff --git a/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx b/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx index 6703a2897498669d4a2b3cac15a831460adb6e4c..50ba16939c69ee33fcab78145fb138f0fa89a19c 100755 --- a/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx @@ -586,6 +586,7 @@ class VideoPreview extends Component { : ( <video id="preview" + data-test="videoPreview" className={styles.preview} ref={(ref) => { this.video = ref; }} autoPlay @@ -646,6 +647,7 @@ class VideoPreview extends Component { disabled={shouldDisableButtons} /> <Button + data-test="startSharingWebcam" color={shared ? "danger" : "primary"} label={intl.formatMessage(shared ? intlMessages.stopSharingLabel : intlMessages.startSharingLabel)} onClick={shared ? this.handleStopSharing : this.handleStartSharing} diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/service.js b/bigbluebutton-html5/imports/ui/components/video-provider/service.js index 0e4eb45438283484ceb1ed6593f5562ff5c99d9c..9fa5c1e9c9b8500a72f0f743f6a2bb0bff68cd93 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/service.js +++ b/bigbluebutton-html5/imports/ui/components/video-provider/service.js @@ -32,7 +32,7 @@ class VideoService { this.skipVideoPreview = getFromUserSettings('bbb_skip_video_preview', false) || SKIP_VIDEO_PREVIEW; this.userParameterProfile = getFromUserSettings( 'bbb_preferred_camera_profile', - (CAMERA_PROFILES.filter(i => i.default) || {}).id + (CAMERA_PROFILES.filter(i => i.default) || {}).id, ); const BROWSER_RESULTS = browser(); this.isMobile = BROWSER_RESULTS.mobile || BROWSER_RESULTS.os.includes('Android'); @@ -40,12 +40,15 @@ class VideoService { this.numberOfDevices = 0; - this.updateNumberOfDevices = this.updateNumberOfDevices.bind(this); - // Safari doesn't support ondevicechange - if (!this.isSafari) { - navigator.mediaDevices.ondevicechange = (event) => this.updateNumberOfDevices(); + // If the page isn't served over HTTPS there won't be mediaDevices + if (navigator.mediaDevices) { + this.updateNumberOfDevices = this.updateNumberOfDevices.bind(this); + // Safari doesn't support ondevicechange + if (!this.isSafari) { + navigator.mediaDevices.ondevicechange = event => this.updateNumberOfDevices(); + } + this.updateNumberOfDevices(); } - this.updateNumberOfDevices(); } defineProperties(obj) { @@ -70,9 +73,9 @@ class VideoService { } updateNumberOfDevices() { - navigator.mediaDevices.enumerateDevices().then(devices => { + navigator.mediaDevices.enumerateDevices().then((devices) => { const deviceIds = []; - devices.forEach(d => { + devices.forEach((d) => { if (d.kind === 'videoinput' && !deviceIds.includes(d.deviceId)) { deviceIds.push(d.deviceId); } @@ -242,8 +245,8 @@ class VideoService { { meetingId: Auth.meetingID, userId: Auth.userID, - deviceId: deviceId - }, { fields: { stream: 1 } } + deviceId, + }, { fields: { stream: 1 } }, ); return videoStream ? videoStream.stream : null; } @@ -338,11 +341,11 @@ class VideoService { // Multiple cameras shouldn't be enabled with video preview skipping // Mobile shouldn't be able to share more than one camera at the same time // Safari needs to implement devicechange event for safe device control - return MULTIPLE_CAMERAS && - !this.skipVideoPreview && - !this.isMobile && - !this.isSafari && - this.numberOfDevices > 1; + return MULTIPLE_CAMERAS + && !this.skipVideoPreview + && !this.isMobile + && !this.isSafari + && this.numberOfDevices > 1; } monitor(conn) { diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx index 488328eed7c687559fcac5d82dcd11d9f40873f8..b7d567cb26bf5d7d29bca21161fae77b5827d156 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx @@ -63,6 +63,7 @@ const JoinVideoButton = ({ return ( <Button + data-test="joinVideo" label={isDisabled ? intl.formatMessage(intlMessages.videoLocked) : label} className={cx(styles.button, hasVideoStream || styles.btn)} onClick={handleOnClick} diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx index d35d6763d1d000d3a7c5d40de8e0fb2675625c6c..1bf1ffe01d536408daaabb37bc2016ebea92f90d 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import browser from 'browser-detect'; import { Meteor } from 'meteor/meteor'; import PropTypes from 'prop-types'; +import _ from 'lodash'; import cx from 'classnames'; import Dropdown from '/imports/ui/components/dropdown/component'; import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component'; @@ -123,6 +124,7 @@ class VideoListItem extends Component { return ( <FullscreenButtonContainer + data-test="presentationFullscreenButton" fullscreenRef={this.videoContainer} elementName={name} isFullscreen={isFullscreen} @@ -165,6 +167,7 @@ class VideoListItem extends Component { > <video muted + data-test="videoContainer" className={cx({ [styles.media]: true, [styles.cursorGrab]: !webcamDraggableState.dragging diff --git a/bigbluebutton-html5/imports/ui/services/api/index.js b/bigbluebutton-html5/imports/ui/services/api/index.js index 5f7385fc7f5aec24bbcaf3e85b75e73d1a4cf3d2..59fd844e43a27473a6d3b426f3c56e41c2e74587 100755 --- a/bigbluebutton-html5/imports/ui/services/api/index.js +++ b/bigbluebutton-html5/imports/ui/services/api/index.js @@ -13,11 +13,11 @@ import logger from '/imports/startup/client/logger'; export function makeCall(name, ...args) { check(name, String); - const { credentials } = Auth; + // const { credentials } = Auth; return new Promise((resolve, reject) => { if (Meteor.status().connected) { - Meteor.call(name, credentials, ...args, (error, result) => { + Meteor.call(name, ...args, (error, result) => { if (error) { reject(error); } @@ -26,7 +26,7 @@ export function makeCall(name, ...args) { }); } else { const failureString = `Call to ${name} failed because Meteor is not connected`; - // We don't want to send a log message if the call that failed wasa log message. + // We don't want to send a log message if the call that failed was a log message. // Without this you can get into an endless loop of failed logging. if (name !== 'logClient') { logger.warn({ diff --git a/bigbluebutton-html5/imports/ui/services/auth/index.js b/bigbluebutton-html5/imports/ui/services/auth/index.js index 69b0ecad531fd1929ae09ea4a9371df0a7ac10f6..e6e80b1d66f3059a72b37e89e899076d853e9e19 100755 --- a/bigbluebutton-html5/imports/ui/services/auth/index.js +++ b/bigbluebutton-html5/imports/ui/services/auth/index.js @@ -180,13 +180,16 @@ class Auth { return Promise.resolve(); } + return new Promise((resolve) => { resolve(this._logoutURL); }); } authenticate(force) { - if (this.loggedIn && !force) return Promise.resolve(); + if (this.loggedIn && !force) { + return Promise.resolve(); + } if (!(this.meetingID && this.userID && this.token)) { return Promise.reject({ @@ -205,7 +208,6 @@ class Auth { validateAuthToken() { return new Promise((resolve, reject) => { - Meteor.connection.setUserId(`${this.meetingID}-${this.userID}`); let computation = null; const validationTimeout = setTimeout(() => { @@ -218,7 +220,8 @@ class Auth { Tracker.autorun((c) => { computation = c; - Meteor.subscribe('current-user', this.credentials); + makeCall('validateAuthToken', this.meetingID, this.userID, this.token); + Meteor.subscribe('current-user'); const selector = { meetingId: this.meetingID, userId: this.userID }; const fields = { @@ -228,7 +231,8 @@ class Auth { // Skip in case the user is not in the collection yet or is a dummy user if (!User || !('intId' in User)) { logger.info({ logCode: 'auth_service_resend_validateauthtoken' }, 're-send validateAuthToken for delayed authentication'); - makeCall('validateAuthToken'); + makeCall('validateAuthToken', this.meetingID, this.userID, this.token); + return; } @@ -250,7 +254,6 @@ class Auth { setTimeout(() => resolve(true), 100); } }); - makeCall('validateAuthToken'); }); } diff --git a/bigbluebutton-html5/imports/utils/fetchStunTurnServers.js b/bigbluebutton-html5/imports/utils/fetchStunTurnServers.js index bee6931a71b2091859c221e85916e803c15f76bd..ee90b03d21796a26e44d15e2a8f3f7a399802563 100644 --- a/bigbluebutton-html5/imports/utils/fetchStunTurnServers.js +++ b/bigbluebutton-html5/imports/utils/fetchStunTurnServers.js @@ -1,3 +1,5 @@ +import _ from 'lodash'; + const MEDIA = Meteor.settings.public.media; const STUN_TURN_FETCH_URL = MEDIA.stunTurnServersFetchAddress; @@ -33,7 +35,7 @@ const fetchStunTurnServers = function (sessionToken) { } return response; }); -} +}; const fetchWebRTCMappedStunTurnServers = function (sessionToken) { return new Promise(async (resolve, reject) => { 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..352652a5e07214b045cf0a6f4ddbfc5b7d6cf124 100755 --- a/bigbluebutton-html5/package.json +++ b/bigbluebutton-html5/package.json @@ -1,6 +1,7 @@ { "name": "bbb-html5-client", "description": "BigBlueButton HTML5 Client", + "license": "LGPL-3.0", "scripts": { "start": "if test \"$NODE_ENV\" = \"production\" ; then npm run start:prod; else npm run start:dev; fi", "test-visual-regression": "export BROWSER_NAME=firefox; wdio ./tests/webdriverio/wdio.vreg.conf.js; export BROWSER_NAME=chrome; wdio ./tests/webdriverio/wdio.vreg.conf.js; export BROWSER_NAME=chrome_mobile; DEVICE_NAME='iPhone 6 Plus'; export DEVICE_NAME; wdio ./tests/webdriverio/wdio.vreg.conf.js; DEVICE_NAME='Nexus 5X'; export DEVICE_NAME; wdio ./tests/webdriverio/wdio.vreg.conf.js", @@ -43,7 +44,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/ar.json b/bigbluebutton-html5/private/locales/ar.json index 455eb6e78de42080d5bec23b113f64c81c6008b4..a0474990b7edef0ec94b9405b3d251d1fc793ed0 100644 --- a/bigbluebutton-html5/private/locales/ar.json +++ b/bigbluebutton-html5/private/locales/ar.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "أو تصÙØ/التقاط الصور", "app.presentationUploder.fileToUpload": "وسيتم رÙعها ...", "app.presentationUploder.currentBadge": "Øالي", - "app.presentationUploder.genericError": "عÙوا، Øدث خطأ ما", "app.presentationUploder.rejectedError": "تم رÙض المل٠(الملÙات) المØدد(Ø©). يرجى التØقق من نوع (أنواع) المل٠(الملÙات).", "app.presentationUploder.upload.progress": "رÙع ({0}Ùª)", "app.presentationUploder.upload.413": " المل٠كبير جدا. يرجى تقسيمه إلى ملÙات متعددة.", @@ -177,7 +176,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "إنشاء صور مصغرة ...", "app.presentationUploder.conversion.generatedSlides": "Ø§Ù„Ø´Ø±Ø§Ø¦Ø Ø§Ù„Ù…Ù†Ø´Ø£Ø©...", "app.presentationUploder.conversion.generatingSvg": "إنشاء صور SVG ...", - "app.presentationUploder.conversion.pageCountExceeded": "عÙوا, عدد الصÙØات تجاوز الØد الأقصى", "app.presentationUploder.conversion.pdfHasBigPage": " لم نتمكن من تØويل مل٠PDF ØŒ يرجى Ù…Øاولة تØسينه", "app.presentationUploder.conversion.timeout": "مكتب خدمات المشاريع, تØويل يستغرق وقتا اطول", "app.presentationUploder.isDownloadableLabel": "عدم Ø§Ù„Ø³Ù…Ø§Ø Ø¨ØªØميل العرض", diff --git a/bigbluebutton-html5/private/locales/bg_BG.json b/bigbluebutton-html5/private/locales/bg_BG.json index 9d38f180817e7f9aa8baad0373ca471dee513b8e..fdd2fb80e40130910bcb7fd4b38845f52a0192f8 100644 --- a/bigbluebutton-html5/private/locales/bg_BG.json +++ b/bigbluebutton-html5/private/locales/bg_BG.json @@ -39,7 +39,6 @@ "app.presentation.presentationToolbar.zoomDesc": "Change the zoom level of the presentation", "app.presentation.presentationToolbar.fitToWidth": "Fit to width", "app.presentationUploder.dismissLabel": "Cancel", - "app.presentationUploder.genericError": "Ops, something went wrong", "app.presentationUploder.tableHeading.options": "Options", "app.poll.closeLabel": "Close", "app.poll.liveResult.usersTitle": "Потребители", diff --git a/bigbluebutton-html5/private/locales/cs_CZ.json b/bigbluebutton-html5/private/locales/cs_CZ.json index 58b7ba0697907477fa1ecabe2802fd92c896a995..48e267e13185a830814519a9b7e5533ac83c2121 100644 --- a/bigbluebutton-html5/private/locales/cs_CZ.json +++ b/bigbluebutton-html5/private/locales/cs_CZ.json @@ -99,7 +99,6 @@ "app.presentationUploder.browseImagesLabel": "nebo procházejte soubory s obrázky", "app.presentationUploder.fileToUpload": "K nahrávánÃ...", "app.presentationUploder.currentBadge": "AktuálnÃ", - "app.presentationUploder.genericError": "O ou, nÄ›co se nepovedlo.", "app.presentationUploder.upload.progress": "Nahrávánà ({0}%)", "app.presentationUploder.conversion.conversionProcessingSlides": "Zpracovávám stránku {0} z {1}", "app.presentationUploder.conversion.genericConversionStatus": "Zpracovánà souboru...", diff --git a/bigbluebutton-html5/private/locales/da.json b/bigbluebutton-html5/private/locales/da.json index 84d995f2d1ab33f714a4d7ceb1a1737ada593cf8..104c831c9839fae011305fc8eaf85b07af5db1eb 100644 --- a/bigbluebutton-html5/private/locales/da.json +++ b/bigbluebutton-html5/private/locales/da.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "eller gennemse/tag billeder", "app.presentationUploder.fileToUpload": "Venter pÃ¥ at blive uploadet ...", "app.presentationUploder.currentBadge": "Nuværende", - "app.presentationUploder.genericError": "Ups, noget gik galt", "app.presentationUploder.rejectedError": "De valgte file(r) er blevet afvist. Kontroller filtypen.", "app.presentationUploder.upload.progress": "Uploading ({0}%)", "app.presentationUploder.upload.413": "Filen er for stor. Opdel venligst i flere filer.", @@ -177,7 +176,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "Fremstiller thumbnails ...", "app.presentationUploder.conversion.generatedSlides": "Slides genereret ....", "app.presentationUploder.conversion.generatingSvg": "Generere SVG billede ...", - "app.presentationUploder.conversion.pageCountExceeded": "Ups, sideantallet overskrider begrænsningen pÃ¥ 200 sider", "app.presentationUploder.conversion.pdfHasBigPage": "Vi kunne ikke konvertere PDF filen, prøv venligst at optimere den. ", "app.presentationUploder.conversion.timeout": "Ups, konverteringen tog for lang tid", "app.presentationUploder.isDownloadableLabel": "Tillad ikke, at præsentationen bliver downloadet", diff --git a/bigbluebutton-html5/private/locales/de.json b/bigbluebutton-html5/private/locales/de.json index d6d723eb7f7f974d2b9ff8e5e5e1af268a17640c..4354d8f5b3fb162c06a10c5d50a172209ee8dcb2 100644 --- a/bigbluebutton-html5/private/locales/de.json +++ b/bigbluebutton-html5/private/locales/de.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "oder auf der Festplatte nach Bildern suchen", "app.presentationUploder.fileToUpload": "Wird hochgeladen...", "app.presentationUploder.currentBadge": "Aktuell", - "app.presentationUploder.genericError": "Ups, irgendwas ist schief gelaufen", "app.presentationUploder.rejectedError": "Die ausgewählten Dateien wurden zurückgewiesen. Bitte prüfen Sie die zulässigen Dateitypen.", "app.presentationUploder.upload.progress": "Hochladen ({0}%)", "app.presentationUploder.upload.413": "Die Datei ist zu groß. Bitte teilen Sie sie in mehrere kleinere Dateien auf.", @@ -177,9 +176,12 @@ "app.presentationUploder.conversion.generatingThumbnail": "Erstelle Miniaturbilder...", "app.presentationUploder.conversion.generatedSlides": "Folien wurden generiert...", "app.presentationUploder.conversion.generatingSvg": "SVG-Bilder werden generiert...", - "app.presentationUploder.conversion.pageCountExceeded": "Ups, die Seitenanzahl überschreitet das Limit von 200 Seiten", + "app.presentationUploder.conversion.pageCountExceeded": "Die maximale Seitenanzahl wurde überschritten. Bitte teilen Sie die Datei in mehrere Dateien auf.", + "app.presentationUploder.conversion.officeDocConversionInvalid": "Die Verarbeitung des Office-Dokuments ist fehlgeschlagen, bitte laden Sie eine PDF-Datei hoch.", + "app.presentationUploder.conversion.officeDocConversionFailed": "Die Verarbeitung des Office-Dokuments ist fehlgeschlagen, bitte laden Sie eine PDF-Datei hoch.", "app.presentationUploder.conversion.pdfHasBigPage": "Die PDF Datei konnte nicht konvertiert werden, bitte versuchen Sie die Datei zu optimieren.", "app.presentationUploder.conversion.timeout": "Ups, die Konvertierung hat zu lange gedauert", + "app.presentationUploder.conversion.pageCountFailed": "Die Seitenanzahl konnte nicht ermittelt werden.", "app.presentationUploder.isDownloadableLabel": "Präsentation darf nicht heruntergeladen werden", "app.presentationUploder.isNotDownloadableLabel": "Präsentation darf heruntergeladen werden", "app.presentationUploder.removePresentationLabel": "Präsentation entfernen", diff --git a/bigbluebutton-html5/private/locales/el_GR.json b/bigbluebutton-html5/private/locales/el_GR.json index 65a7ea2f8de073591eef3ba4165fce2d683bc5fa..ae02aa2c2a410fd9c6e294d5a45c82a13b6e3073 100644 --- a/bigbluebutton-html5/private/locales/el_GR.json +++ b/bigbluebutton-html5/private/locales/el_GR.json @@ -152,7 +152,6 @@ "app.presentationUploder.browseImagesLabel": "ή πεÏιήγηση/σÏλληψη για εικόνες", "app.presentationUploder.fileToUpload": "Î Ïος μεταφόÏτωση...", "app.presentationUploder.currentBadge": "ΤÏÎχον", - "app.presentationUploder.genericError": "Ωχ, κάτι πήγε στÏαβά", "app.presentationUploder.upload.progress": "ΜεταφόÏτωση ({0}%)", "app.presentationUploder.upload.413": "Î Î¿Î»Ï Î¼ÎµÎ³Î¬Î»Î¿ αÏχείο. Î Ïοσπαθήστε να το μοιÏάσετε σε πολλαπλά ξεχωÏιστά αÏχεία.", "app.presentationUploder.conversion.conversionProcessingSlides": "ΕπεξεÏγασία σελίδας {0} από {1}", @@ -160,7 +159,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "ΔημιουÏγία μικÏογÏαφιών...", "app.presentationUploder.conversion.generatedSlides": "Διαφάνειες που δημιουÏγήθηκαν...", "app.presentationUploder.conversion.generatingSvg": "ΔημιουÏγία SVG εικόνων...", - "app.presentationUploder.conversion.pageCountExceeded": "Ωχ, το πλήθος των σελίδων ξεπεÏνάει το ÏŒÏιο των 200 σελίδων", "app.presentationUploder.conversion.pdfHasBigPage": "Δεν είναι δυνατή η μετατÏοπή του αÏχείου PDF, παÏακαλοÏμε δοκιμάστε να το βελτιστοποιήσετε", "app.presentationUploder.conversion.timeout": "Ουπς, η μετατÏοπή διÎÏκησε πάÏα πολÏ", "app.presentationUploder.isDownloadableLabel": "Îα μην επιτÏÎπεται η λήψη της παÏουσίασης", diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 0ff2af430f5d40944bcdc3a075d82c0ecf8675ad..cad7c5d73827d12cd7808858a6a48d4fef66cf3b 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -1,7 +1,6 @@ { "app.home.greeting": "Your presentation will begin shortly ...", "app.chat.submitLabel": "Send message", - "app.chat.errorMinMessageLength": "The message is {0} characters(s) too short", "app.chat.errorMaxMessageLength": "The message is {0} characters(s) too long", "app.chat.disconnected": "You are disconnected, messages can't be sent", "app.chat.locked": "Chat is locked, messages can't be sent", @@ -169,7 +168,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 +176,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", @@ -365,18 +366,18 @@ "app.actionsBar.currentStatusDesc": "current status {0}", "app.actionsBar.captions.start": "Start viewing closed captions", "app.actionsBar.captions.stop": "Stop viewing closed captions", - "app.audioNotification.audioFailedError1001": "Error 1001: WebSocket disconnected", - "app.audioNotification.audioFailedError1002": "Error 1002: Could not make a WebSocket connection", - "app.audioNotification.audioFailedError1003": "Error 1003: Browser version not supported", - "app.audioNotification.audioFailedError1004": "Error 1004: Failure on call (reason={0})", - "app.audioNotification.audioFailedError1005": "Error 1005: Call ended unexpectedly", - "app.audioNotification.audioFailedError1006": "Error 1006: Call timed out", - "app.audioNotification.audioFailedError1007": "Error 1007: ICE negotiation failed", - "app.audioNotification.audioFailedError1008": "Error 1008: Transfer failed", - "app.audioNotification.audioFailedError1009": "Error 1009: Could not fetch STUN/TURN server information", - "app.audioNotification.audioFailedError1010": "Error 1010: ICE negotiation timeout", - "app.audioNotification.audioFailedError1011": "Error 1011: ICE gathering timeout", - "app.audioNotification.audioFailedError1012": "Error 1012: ICE connection closed", + "app.audioNotification.audioFailedError1001": "WebSocket disconnected (error 1001)", + "app.audioNotification.audioFailedError1002": "Could not make a WebSocket connection (error 1002)", + "app.audioNotification.audioFailedError1003": "Browser version not supported (error 1003)", + "app.audioNotification.audioFailedError1004": "Failure on call (reason={0}) (error 1004)", + "app.audioNotification.audioFailedError1005": "Call ended unexpectedly (error 1005)", + "app.audioNotification.audioFailedError1006": "Call timed out (error 1006)", + "app.audioNotification.audioFailedError1007": "Connection failure (ICE error 1007)", + "app.audioNotification.audioFailedError1008": "Transfer failed (error 1008)", + "app.audioNotification.audioFailedError1009": "Could not fetch STUN/TURN server information (error 1009)", + "app.audioNotification.audioFailedError1010": "Connection negotiation timeout (ICE error 1010)", + "app.audioNotification.audioFailedError1011": "Connection timeout (ICE error 1011)", + "app.audioNotification.audioFailedError1012": "Connection closed (ICE error 1012)", "app.audioNotification.audioFailedMessage": "Your audio connection failed to connect", "app.audioNotification.mediaFailedMessage": "getUserMicMedia failed as only secure origins are allowed", "app.audioNotification.closeLabel": "Close", @@ -552,14 +553,14 @@ "app.video.joinVideo": "Share webcam", "app.video.leaveVideo": "Stop sharing webcam", "app.video.iceCandidateError": "Error on adding ICE candidate", - "app.video.iceConnectionStateError": "Error 1107: ICE negotiation failed", + "app.video.iceConnectionStateError": "Connection failure (ICE error 1107)", "app.video.permissionError": "Error on sharing webcam. Please check permissions", "app.video.sharingError": "Error on sharing webcam", "app.video.notFoundError": "Could not find webcam. Please make sure it's connected", "app.video.notAllowed": "Missing permission for share webcam, please make sure your browser permissions", "app.video.notSupportedError": "Can share webcam video only with safe sources, make sure your SSL certificate is valid", "app.video.notReadableError": "Could not get webcam video. Please make sure another program is not using the webcam ", - "app.video.mediaFlowTimeout1020": "Error 1020: media could not reach the server", + "app.video.mediaFlowTimeout1020": "Media could not reach the server (error 1020)", "app.video.suggestWebcamLock": "Enforce lock setting to viewers webcams?", "app.video.suggestWebcamLockReason": "(this will improve the stability of the meeting)", "app.video.enable": "Enable", @@ -587,16 +588,16 @@ "app.video.stats.encodeUsagePercent": "Encode usage", "app.video.stats.currentDelay": "Current delay", "app.fullscreenButton.label": "Make {0} fullscreen", - "app.deskshare.iceConnectionStateError": "Error 1108: ICE connection failed when sharing screen", - "app.sfu.mediaServerConnectionError2000": "Error 2000: Unable to connect to media server", - "app.sfu.mediaServerOffline2001": "Error 2001: Media server is offline. Please try again later.", - "app.sfu.mediaServerNoResources2002": "Error 2002: Media server has no available resources", - "app.sfu.mediaServerRequestTimeout2003": "Error 2003: Media server requests are timing out", - "app.sfu.serverIceGatheringFailed2021": "Error 2021: Media server cannot gather ICE candidates", - "app.sfu.serverIceGatheringFailed2022": "Error 2022: Media server ICE connection failed", - "app.sfu.mediaGenericError2200": "Error 2200: Media server failed to process request", - "app.sfu.invalidSdp2202":"Error 2202: Client generated an invalid SDP", - "app.sfu.noAvailableCodec2203": "Error 2203: Server could not find an appropriate codec", + "app.deskshare.iceConnectionStateError": "Connection failed when sharing screen (ICE error 1108)", + "app.sfu.mediaServerConnectionError2000": "Unable to connect to media server (error 2000)", + "app.sfu.mediaServerOffline2001": "Media server is offline. Please try again later (error 2001)", + "app.sfu.mediaServerNoResources2002": "Media server has no available resources (error 2002)", + "app.sfu.mediaServerRequestTimeout2003": "Media server requests are timing out (error 2003)", + "app.sfu.serverIceGatheringFailed2021": "Media server cannot gather connection candidates (ICE error 2021)", + "app.sfu.serverIceGatheringFailed2022": "Media server connection failed (ICE error 2022)", + "app.sfu.mediaGenericError2200": "Media server failed to process request (error 2200)", + "app.sfu.invalidSdp2202":"Client generated an invalid media request (SDP error 2202)", + "app.sfu.noAvailableCodec2203": "Server could not find an appropriate codec (error 2203)", "app.meeting.endNotification.ok.label": "OK", "app.whiteboard.annotations.poll": "Poll results were published to Public Chat and Whiteboard", "app.whiteboard.toolbar.tools": "Tools", diff --git a/bigbluebutton-html5/private/locales/es.json b/bigbluebutton-html5/private/locales/es.json index 220f32851b9a980b8a95a8e602d0a8bba4b91941..38db9775c25a5a478b7b9ebd7631567778e938fb 100644 --- a/bigbluebutton-html5/private/locales/es.json +++ b/bigbluebutton-html5/private/locales/es.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "o buscar imagenes", "app.presentationUploder.fileToUpload": "En proceso de ser cargado ...", "app.presentationUploder.currentBadge": "Acual", - "app.presentationUploder.genericError": "Ups, algo salio mal", "app.presentationUploder.rejectedError": "El(los) archivo(s) seleccionado(s) ha(n) sido rechazado(s). Por favor, revise el(los) tipo(s) de archivo.", "app.presentationUploder.upload.progress": "Cargando ({0}%)", "app.presentationUploder.upload.413": "Archivo muy grande, por favor divÃdelo en varios ficheros", @@ -177,7 +176,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "Generando miniaturas ...", "app.presentationUploder.conversion.generatedSlides": "Diapositivas han sido generadas ...", "app.presentationUploder.conversion.generatingSvg": "Generando imágenes SVG ...", - "app.presentationUploder.conversion.pageCountExceeded": "Ops, el contador ha excedido el lÃmite de 200 páginas", "app.presentationUploder.conversion.pdfHasBigPage": "No se puede convertir el fichero PDF, por favor prueba a optimizarlo", "app.presentationUploder.conversion.timeout": "La conversión tomó demasiado tiempo ...", "app.presentationUploder.isDownloadableLabel": "La descarga de la presentación no está permitida", diff --git a/bigbluebutton-html5/private/locales/es_ES.json b/bigbluebutton-html5/private/locales/es_ES.json index 47786c087d723665a7f5041fbbe83570d4a2af72..d6d67659824da8aa90f17c23550adb3eef488c4a 100644 --- a/bigbluebutton-html5/private/locales/es_ES.json +++ b/bigbluebutton-html5/private/locales/es_ES.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "o buscar/capturar imágenes", "app.presentationUploder.fileToUpload": "Para ser cargados...", "app.presentationUploder.currentBadge": "Actual", - "app.presentationUploder.genericError": "Ups, algo salio mal", "app.presentationUploder.rejectedError": "El(los) archivo(s) seleccionado(s) ha(n) sido rechazado(s). Por favor, revise el(los) tipo(s) de archivo(s).", "app.presentationUploder.upload.progress": "Cargando ({0}%)", "app.presentationUploder.upload.413": "El archivo es demasiado grande. Por favor, divÃdalo en múltiples archivos.", @@ -177,7 +176,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "Generando miniaturas...", "app.presentationUploder.conversion.generatedSlides": "Diapositivas generadas...", "app.presentationUploder.conversion.generatingSvg": "Generando imágenes SVG...", - "app.presentationUploder.conversion.pageCountExceeded": "Oh! el recuento de páginas excedió el lÃmite de 200 páginas", "app.presentationUploder.conversion.pdfHasBigPage": "No es posible convertir el archivo PDF. Por favor, intente optimizarlo", "app.presentationUploder.conversion.timeout": "Oh, la conversión tardó demasiado", "app.presentationUploder.isDownloadableLabel": "No está permitida la descarga de la presentación", @@ -576,14 +574,18 @@ "app.video.stats.codec": "Codec", "app.video.stats.decodeDelay": "Retardo de decodificación", "app.video.stats.rtt": "RTT", + "app.video.stats.encodeUsagePercent": "Codificar uso", "app.video.stats.currentDelay": "Retardo actual", "app.fullscreenButton.label": "Hacer {0} pantalla completa", "app.deskshare.iceConnectionStateError": "Error 1108: La conexión ICE falló al compartir pantalla", "app.sfu.mediaServerConnectionError2000": "Error 2000: Incapaz de conectarse al servidor multimedia", "app.sfu.mediaServerOffline2001": "Error 2001: El servidor multimedia está fuera de lÃnea. Por favor, inténtelo de nuevo más tarde", "app.sfu.mediaServerNoResources2002": "Error 2002: El servidor multimedia no tiene recursos disponibles", + "app.sfu.mediaServerRequestTimeout2003": "Error 2003: Solicitudes del servidor de medios están agotándose", + "app.sfu.serverIceGatheringFailed2021": "Error 2021: Servidor de medios no puede reunir candidatos ICE", "app.sfu.serverIceGatheringFailed2022": "Error 2022: Error en la conexión al servidor multimedia ICE", "app.sfu.mediaGenericError2200": "Error 2200: El servidor multimedia falló al procesar la solicitud", + "app.sfu.invalidSdp2202":"Error 2202: El cliente generó un SDP inválido", "app.sfu.noAvailableCodec2203": "Error 2203: El servidor no pudo encontrar el codec adecuado", "app.meeting.endNotification.ok.label": "OK", "app.whiteboard.annotations.poll": "Los resultados de la encuesta fueron publicados", @@ -621,6 +623,10 @@ "app.feedback.textarea": "¿Cómo podemos hacer BigBlueButton mejor?", "app.feedback.sendFeedback": "Enviar retroalimentación", "app.feedback.sendFeedbackDesc": "Enviar retroalimentación y abandonar la reunión", + "app.videoDock.webcamFocusLabel": "Enfoque", + "app.videoDock.webcamFocusDesc": "Enfoque la cámara seleccionada", + "app.videoDock.webcamUnfocusLabel": "Desenfoque", + "app.videoDock.webcamUnfocusDesc": "Desenfoque la cámara seleccionada", "app.videoDock.autoplayBlockedDesc": "Necesitamos su permiso para mostrarle las webcams de otros usuarios", "app.videoDock.autoplayAllowLabel": "Ver webcams", "app.invitation.title": "Invitación a sala externa", @@ -635,6 +641,7 @@ "app.createBreakoutRoom.notAssigned": "No asignado ({0})", "app.createBreakoutRoom.join": "Unirse a la sala", "app.createBreakoutRoom.joinAudio": "Unirse al audio", + "app.createBreakoutRoom.returnAudio": "Regrese el audio", "app.createBreakoutRoom.alreadyConnected": "Ya en la sala", "app.createBreakoutRoom.confirm": "Crear", "app.createBreakoutRoom.record": "Grabación", @@ -645,9 +652,12 @@ "app.createBreakoutRoom.roomName": "{0} (Sala - {1})", "app.createBreakoutRoom.doneLabel": "Hecho", "app.createBreakoutRoom.nextLabel": "Siguiente", + "app.createBreakoutRoom.minusRoomTime": "Disminuya el tiempo de la sala externa a ", + "app.createBreakoutRoom.addRoomTime": "Incremente tiempo de la sala externa a ", "app.createBreakoutRoom.addParticipantLabel": "+ Añadir participante", "app.createBreakoutRoom.freeJoin": "Permitir a los usuarios escoger una sala externa a la que unirse", "app.createBreakoutRoom.leastOneWarnBreakout": "Debe colocar al menos un usuario en la sala externa", + "app.createBreakoutRoom.modalDesc": "Sugerencia: puede arrastrar y soltar el nombre de un usuario para asignarlo a una sala externa especÃfica.", "app.createBreakoutRoom.roomTime": "{0} minutos", "app.createBreakoutRoom.numberOfRoomsError": "El número de salas es inválido", "app.externalVideo.start": "Compartir un nuevo video", @@ -656,11 +666,16 @@ "app.externalVideo.urlInput": "Introducir URL de video", "app.externalVideo.urlError": "La URL de este video no está soportada", "app.externalVideo.close": "Cerrar", + "app.externalVideo.autoPlayWarning": "Reproduzca el video para habilitar la sincronización de medios", + "app.network.connection.effective.slow": "Estamos notando problemas de conectividad.", "app.network.connection.effective.slow.help": "Más información", + "app.externalVideo.noteLabel": "Nota: los videos externos compartidos no aparecerán en la grabación. Se admiten las URL de YouTube, Vimeo, Instructure Media, Twitch y Daily Motion.", "app.actionsBar.actionsDropdown.shareExternalVideo": "Compartir un video externo", "app.actionsBar.actionsDropdown.stopShareExternalVideo": "Detener compartir video externo", "app.iOSWarning.label": "Por favor, actualice a iOS 12.2 o mayor", - "app.legacy.upgradeBrowser": "Parece que está usando una versión antigua no soportada del navegador. Por favor, actualice su navegador para soporte total." + "app.legacy.unsupportedBrowser": "Parece que estás usando un navegador que no es compatible. Utilice {0} o {1} para obtener soporte completo.", + "app.legacy.upgradeBrowser": "Parece que está usando una versión antigua no soportada del navegador. Por favor, actualice su navegador para soporte total.", + "app.legacy.criosBrowser": "En iOS, use Safari para obtener soporte completo." } diff --git a/bigbluebutton-html5/private/locales/es_MX.json b/bigbluebutton-html5/private/locales/es_MX.json index 621549454daa4f80a730e908f4cb09c472670691..80d9b65c15f16a231e40a8400159d56e501cdb40 100644 --- a/bigbluebutton-html5/private/locales/es_MX.json +++ b/bigbluebutton-html5/private/locales/es_MX.json @@ -117,14 +117,12 @@ "app.presentationUploder.browseImagesLabel": "o buscar imagenes", "app.presentationUploder.fileToUpload": "En proceso de ser cargado ...", "app.presentationUploder.currentBadge": "Acual", - "app.presentationUploder.genericError": "Ups, algo salio mal", "app.presentationUploder.upload.progress": "Cargando ({0}%)", "app.presentationUploder.conversion.conversionProcessingSlides": "Procesando página {0} de {1}", "app.presentationUploder.conversion.genericConversionStatus": "Convirtiendo archivos ...", "app.presentationUploder.conversion.generatingThumbnail": "Generando miniaturas ...", "app.presentationUploder.conversion.generatedSlides": "Diapositivas han sido generadas ...", "app.presentationUploder.conversion.generatingSvg": "Generando imágenes SVG ...", - "app.presentationUploder.conversion.pageCountExceeded": "Se ha excedido el número de páginas permitidas que es de 200.", "app.presentationUploder.conversion.timeout": "La conversión tomó demasiado tiempo ...", "app.presentationUploder.isDownloadableLabel": "No permitir la descarga de diapositivas", "app.presentationUploder.isNotDownloadableLabel": "Permitir descarga de diapositivas", diff --git a/bigbluebutton-html5/private/locales/eu.json b/bigbluebutton-html5/private/locales/eu.json index 28844358e761c0ca66a53f34f576ad1277dea38d..3f15c495497e8689ce0893fc805bd8f05cb25df4 100644 --- a/bigbluebutton-html5/private/locales/eu.json +++ b/bigbluebutton-html5/private/locales/eu.json @@ -154,7 +154,6 @@ "app.presentationUploder.browseImagesLabel": "edo arakatu/kapturatu irudiak", "app.presentationUploder.fileToUpload": "Kargatzeko ...", "app.presentationUploder.currentBadge": "Unekoa", - "app.presentationUploder.genericError": "Ops, zerbait oker joan da", "app.presentationUploder.rejectedError": "Hautatutako fitxategia(k) baztertu egin d(ir)a. Egiaztatu fitxategi mota(k).", "app.presentationUploder.upload.progress": "Kargatzen (%{0})", "app.presentationUploder.conversion.conversionProcessingSlides": "{1} orritatik {0} prozesatzen", @@ -162,7 +161,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "Miniaturak sortzen...", "app.presentationUploder.conversion.generatedSlides": "Diapositiba sortuak...", "app.presentationUploder.conversion.generatingSvg": "SVG irudiak sortzen...", - "app.presentationUploder.conversion.pageCountExceeded": "Ops, 200 orrien muga gainditu da", "app.presentationUploder.conversion.timeout": "Ops, bihurketa gehiegi luzatu da", "app.presentationUploder.isDownloadableLabel": "Ez eman baimenik aurkezpena deskargatzeko", "app.presentationUploder.isNotDownloadableLabel": "Baimena eman aurkezpena deskargatzeko", diff --git a/bigbluebutton-html5/private/locales/fa_IR.json b/bigbluebutton-html5/private/locales/fa_IR.json index 84a5d9fecfd9b3a4377513248322e8c2ea4d91a8..641fc71b5ce6bb7b3060481d58d678e255f3db28 100644 --- a/bigbluebutton-html5/private/locales/fa_IR.json +++ b/bigbluebutton-html5/private/locales/fa_IR.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "یا عکس ها را بروز/بگیر", "app.presentationUploder.fileToUpload": "آماده بارگزاری ...", "app.presentationUploder.currentBadge": "کنونی", - "app.presentationUploder.genericError": "اوپس، خطایی رخ داد", "app.presentationUploder.rejectedError": "Ùایل(های) انتخاب شده رد شدند. لطÙا نوع Ùایل(ها) را بررسی کنید", "app.presentationUploder.upload.progress": "در Øال بارگزاری ({0}%)", "app.presentationUploder.upload.413": "Øجم Ùایل زیاد است.لطÙا آن را به چند Ùایل کوچکتر تبدیل کنید", @@ -177,7 +176,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "در Øال تولید تصاویر Ú©ÙˆÚ†Ú© ...", "app.presentationUploder.conversion.generatedSlides": "اسلاید ها تولید شدند ...", "app.presentationUploder.conversion.generatingSvg": "در Øال تولید تصاویر SVG ...", - "app.presentationUploder.conversion.pageCountExceeded": "اوپس، تعداد Øداکثر صÙØات از 200 صÙØÙ‡ بیشتر شده است", "app.presentationUploder.conversion.pdfHasBigPage": "تبدیل Ùایل pdf موÙÙ‚ نبود. لطÙا Ùایل خود را ویرایش کنید.", "app.presentationUploder.conversion.timeout": "اوپس، عملیات تبدیل خیلی طول کشید", "app.presentationUploder.isDownloadableLabel": "مجوز دانلود ارائه را نده", diff --git a/bigbluebutton-html5/private/locales/fi.json b/bigbluebutton-html5/private/locales/fi.json index c495c136104a46a91e41bd65b3d585f66973997e..7923411ba14e786300d9bf2dd1f9112541b0fee4 100644 --- a/bigbluebutton-html5/private/locales/fi.json +++ b/bigbluebutton-html5/private/locales/fi.json @@ -146,7 +146,6 @@ "app.presentationUploder.browseImagesLabel": "tai selaa/ota kuva", "app.presentationUploder.fileToUpload": "Valmiina ladattavaksi", "app.presentationUploder.currentBadge": "Nykyinen", - "app.presentationUploder.genericError": "Jotain meni pieleen.", "app.presentationUploder.rejectedError": "Valittu tiedosto(t) hylättiin. Ole hyvä tarkitsa tiedostotyyppi(t).", "app.presentationUploder.upload.progress": "Lähettää ({0}%)", "app.presentationUploder.conversion.conversionProcessingSlides": "Prosessoi sivua {0} / {1}", @@ -154,7 +153,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "Luo esikatselukuvia ...", "app.presentationUploder.conversion.generatedSlides": "Dioja luodaan...", "app.presentationUploder.conversion.generatingSvg": "Luodaan SVG-kuvia ...", - "app.presentationUploder.conversion.pageCountExceeded": "Virhe, sivumäärä ylittää sallitun 200 sivun maksimirajan.", "app.presentationUploder.conversion.timeout": "Virhe, konversio kesti liian kauan joten se keskeytyi", "app.presentationUploder.isDownloadableLabel": "Estä esitystiedostojen lataaminen", "app.presentationUploder.isNotDownloadableLabel": "Salli esitystiedostojen lataaminen", diff --git a/bigbluebutton-html5/private/locales/fr.json b/bigbluebutton-html5/private/locales/fr.json index de8836863e076b69ab53bda32840609d83492c0a..f600f60f724bf01b4cf4320ac68f898131bcb55c 100644 --- a/bigbluebutton-html5/private/locales/fr.json +++ b/bigbluebutton-html5/private/locales/fr.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "ou parcourir/capturer des images", "app.presentationUploder.fileToUpload": "Prêt à être chargé...", "app.presentationUploder.currentBadge": "En cours", - "app.presentationUploder.genericError": "Oups, quelque chose s'est mal passé", "app.presentationUploder.rejectedError": "Le(s) fichier(s) sélectionné(s) a été rejeté(s). Veuillez vérifier le format de ce(s) fichier(s).", "app.presentationUploder.upload.progress": "Chargement ({0}%)", "app.presentationUploder.upload.413": "Le fichier est trop volumineux. Veuillez le diviser en plusieurs fichiers s'il vous plaît.", @@ -177,7 +176,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "Génération des vignettes...", "app.presentationUploder.conversion.generatedSlides": "Diapositives générées...", "app.presentationUploder.conversion.generatingSvg": "Génération des images SVG...", - "app.presentationUploder.conversion.pageCountExceeded": "Oops, le nombre de pages a dépassé la limite de 200 pages", "app.presentationUploder.conversion.pdfHasBigPage": "Nous n'avons pas pu convertir le fichier PDF, veuillez essayer de l'optimiser.", "app.presentationUploder.conversion.timeout": "Oops, la conversion a pris trop de temps", "app.presentationUploder.isDownloadableLabel": "Ne pas autoriser le téléchargement de la présentation", diff --git a/bigbluebutton-html5/private/locales/gl.json b/bigbluebutton-html5/private/locales/gl.json index e6fc1a8857c83f8208c5bec9f639e2c69aba203d..cc39caa75fcfe7c1d8892df1defc8d6ced445abe 100644 --- a/bigbluebutton-html5/private/locales/gl.json +++ b/bigbluebutton-html5/private/locales/gl.json @@ -163,7 +163,6 @@ "app.presentationUploder.browseImagesLabel": "ou buscar imagenes", "app.presentationUploder.fileToUpload": "En proceso de ser cargado ...", "app.presentationUploder.currentBadge": "Acual", - "app.presentationUploder.genericError": "Ups, algo salio mal", "app.presentationUploder.rejectedError": "O(os) arquivo(s) seleccionado(s) ha(n) sido rexeitado(s). Por favor, revise o(os) tipo(s) de arquivo.", "app.presentationUploder.upload.progress": "Cargando ({0}%)", "app.presentationUploder.upload.413": "Arquivo moi grande, por favor divÃdeo en varios ficheiros", @@ -172,7 +171,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "Xerando miniaturas ...", "app.presentationUploder.conversion.generatedSlides": "Diapositivas foron xeradas ...", "app.presentationUploder.conversion.generatingSvg": "Xerando imaxes SVG ...", - "app.presentationUploder.conversion.pageCountExceeded": "Ops, o contador excedeu o lÃmite de 200 páxinas", "app.presentationUploder.conversion.pdfHasBigPage": "Non se pode converter o ficheiro PDF/PDF, por favor proba a optimizalo", "app.presentationUploder.conversion.timeout": "A conversión tomou demasiado tempo ...", "app.presentationUploder.isDownloadableLabel": "A descarga da presentación non está permitida", diff --git a/bigbluebutton-html5/private/locales/hi_IN.json b/bigbluebutton-html5/private/locales/hi_IN.json index ed1cdf75b183452362f2d5c84879ee8a5a8cf54b..2d912fbf161a7a187dc130cab6f96ef2ec87678e 100644 --- a/bigbluebutton-html5/private/locales/hi_IN.json +++ b/bigbluebutton-html5/private/locales/hi_IN.json @@ -117,14 +117,12 @@ "app.presentationUploder.browseImagesLabel": "या छवियों के लिठबà¥à¤°à¤¾à¤‰à¤œà¤¼ / कैपà¥à¤šà¤° करें", "app.presentationUploder.fileToUpload": "अपलोड किया जाना है ...", "app.presentationUploder.currentBadge": "वरà¥à¤¤à¤®à¤¾à¤¨", - "app.presentationUploder.genericError": "ओह! कà¥à¤› गलत हो गया है", "app.presentationUploder.upload.progress": "अपलोड करना ({0}%)", "app.presentationUploder.conversion.conversionProcessingSlides": "पà¥à¤°à¥‹à¤¸à¥‡à¤¸à¤¿à¤‚ग पेज {0} का {1}", "app.presentationUploder.conversion.genericConversionStatus": "फ़ाइल परिवरà¥à¤¤à¤¿à¤¤ कर रहा है ...", "app.presentationUploder.conversion.generatingThumbnail": "थंबनेल बनाठजा रहे हैं ...", "app.presentationUploder.conversion.generatedSlides": "सà¥à¤²à¤¾à¤‡à¤¡à¥à¤¸ बनाई गईं ...", "app.presentationUploder.conversion.generatingSvg": "SVG चितà¥à¤° बनाना ...", - "app.presentationUploder.conversion.pageCountExceeded": "ऑपà¥à¤¸, पृषà¥à¤ संखà¥à¤¯à¤¾ 200 पृषà¥à¤ ों की सीमा को पार कर गई है", "app.presentationUploder.conversion.timeout": "ऑपà¥à¤¸, रूपांतरण में बहà¥à¤¤ लंबा समय लगा", "app.presentationUploder.isDownloadableLabel": "पà¥à¤°à¤¸à¥à¤¤à¥à¤¤à¤¿ को डाउनलोड न होने दें", "app.presentationUploder.isNotDownloadableLabel": "पà¥à¤°à¤¸à¥à¤¤à¥à¤¤à¤¿ को डाउनलोड करने दें", diff --git a/bigbluebutton-html5/private/locales/hu_HU.json b/bigbluebutton-html5/private/locales/hu_HU.json index 6e2b45184aca85d2a06c4b99c90cae78497a715e..da3750bf83fcdde89ab82750c3b54e8d16188d99 100644 --- a/bigbluebutton-html5/private/locales/hu_HU.json +++ b/bigbluebutton-html5/private/locales/hu_HU.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "vagy tallózz/metsz képeket", "app.presentationUploder.fileToUpload": "FeltöltendÅ‘ ...", "app.presentationUploder.currentBadge": "Jelenlegi", - "app.presentationUploder.genericError": "Sajnáljuk, hiba történt", "app.presentationUploder.rejectedError": "A kiválasztott fájl(oka)t visszautasÃtottuk. Kérjük, ellenÅ‘rizd a fájl(ok) tÃpusát.", "app.presentationUploder.upload.progress": "({0}%) feltöltve", "app.presentationUploder.upload.413": "A fájl túl nagy. Kérjük, szedd szét több darabra.", @@ -177,9 +176,12 @@ "app.presentationUploder.conversion.generatingThumbnail": "Miniatűrök létrehozása ...", "app.presentationUploder.conversion.generatedSlides": "Diák létrehozása ...", "app.presentationUploder.conversion.generatingSvg": "SVG képek létrehozása ...", - "app.presentationUploder.conversion.pageCountExceeded": "Sajnáljuk, a oldalszámláló elérte a maximális 200 oldalt", + "app.presentationUploder.conversion.pageCountExceeded": "Túl sok oldal, kérjük, szedd szét több darabra.", + "app.presentationUploder.conversion.officeDocConversionInvalid": "Az office dokumentumot nem sikerült előállÃtani. Kérjük, PDF-fájlt tölts fel.", + "app.presentationUploder.conversion.officeDocConversionFailed": "Az office dokumentumot nem sikerült előállÃtani. Kérjük, PDF-fájlt tölts fel.", "app.presentationUploder.conversion.pdfHasBigPage": "A PDF fájl átalakÃtása sikertelen. Kérjük, próbáld optimalizálni.", "app.presentationUploder.conversion.timeout": "Sajnáljuk, az átalakÃtás túl sokáig tartott", + "app.presentationUploder.conversion.pageCountFailed": "Az oldalak számát nem sikerült meghatározni.", "app.presentationUploder.isDownloadableLabel": "A prezentáció letöltésének tiltása", "app.presentationUploder.isNotDownloadableLabel": "A prezentáció letöltésének engedélyezése", "app.presentationUploder.removePresentationLabel": "Prezentáció törlése", diff --git a/bigbluebutton-html5/private/locales/id.json b/bigbluebutton-html5/private/locales/id.json index 49205968eed393aba66657657273570b88212e70..867cc5d2e7b0934842c53df69a103d1e8ec95a74 100644 --- a/bigbluebutton-html5/private/locales/id.json +++ b/bigbluebutton-html5/private/locales/id.json @@ -56,7 +56,6 @@ "app.presentationUploder.dismissDesc": "Tutup jendela dan batalkan perubahan.", "app.presentationUploder.dropzoneLabel": "Drag file untuk unggah", "app.presentationUploder.browseFilesLabel": "atau telusuri file", - "app.presentationUploder.genericError": "Ops, terjadi kesalahan", "app.presentationUploder.tableHeading.options": "Pilihan", "app.poll.closeLabel": "Tutup", "app.poll.liveResult.usersTitle": "Pengguna", diff --git a/bigbluebutton-html5/private/locales/it_IT.json b/bigbluebutton-html5/private/locales/it_IT.json index 5c1c3dbdac32632a82ae87177f5e6cbe7dfcda36..82038ae71241120a69243bd0be1ed6d0c32b5aaf 100644 --- a/bigbluebutton-html5/private/locales/it_IT.json +++ b/bigbluebutton-html5/private/locales/it_IT.json @@ -167,7 +167,6 @@ "app.presentationUploder.browseImagesLabel": "o sfoglia/acquisisci immagini", "app.presentationUploder.fileToUpload": "Da caricare...", "app.presentationUploder.currentBadge": "Attuale", - "app.presentationUploder.genericError": "Dannazione, qualcosa è andato storto", "app.presentationUploder.rejectedError": "Il file selezionato è stato rifiutato. controllare il tipo di file.", "app.presentationUploder.upload.progress": "Caricamento ({0}%)", "app.presentationUploder.upload.413": "Il file è troppo grande. Per favore dividilo in più file.", @@ -176,7 +175,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "Elaborazione anteprima...", "app.presentationUploder.conversion.generatedSlides": "Slide elaborate...", "app.presentationUploder.conversion.generatingSvg": "Elaborazione immagine SVG...", - "app.presentationUploder.conversion.pageCountExceeded": "Accidenti, il numero di pagine supera il limite di 200.", "app.presentationUploder.conversion.pdfHasBigPage": "Non è stato possibile convertire il file PDF, prova ad ottimizzarlo", "app.presentationUploder.conversion.timeout": "Maledizione, la conversione del file ci sta impiegando troppo", "app.presentationUploder.isDownloadableLabel": "Non permettere il download della presentazione", diff --git a/bigbluebutton-html5/private/locales/ja.json b/bigbluebutton-html5/private/locales/ja.json index d1addec15da1e00caa769fe9cc77d1dd43e9f818..d8c8d61c253007bd5f7df558566f07f5871e24e2 100644 --- a/bigbluebutton-html5/private/locales/ja.json +++ b/bigbluebutton-html5/private/locales/ja.json @@ -167,7 +167,6 @@ "app.presentationUploder.browseImagesLabel": "ã¾ãŸã¯ç”»åƒã‚’探ã™/ã‚ャプãƒãƒ£ã™ã‚‹", "app.presentationUploder.fileToUpload": "アップãƒãƒ¼ãƒ‰ã•ã‚Œã¾ã™â€¦", "app.presentationUploder.currentBadge": "ç¾åœ¨", - "app.presentationUploder.genericError": "エラーãŒç™ºç”Ÿã—ã¾ã—ãŸ", "app.presentationUploder.rejectedError": "é¸æŠžãƒ•ã‚¡ã‚¤ãƒ«ãŒæ‹’å¦ã•ã‚Œã¾ã—ãŸã€‚ファイル形å¼ã‚’確èªã—ã¦ãã ã•ã„。", "app.presentationUploder.upload.progress": "アップãƒãƒ¼ãƒ‰ä¸({0}%)", "app.presentationUploder.conversion.conversionProcessingSlides": "{1}ページä¸{0}ページ目を処ç†ä¸", @@ -175,7 +174,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "サムãƒã‚¤ãƒ«ä½œæˆä¸â€¦", "app.presentationUploder.conversion.generatedSlides": "スライド作æˆä¸â€¦", "app.presentationUploder.conversion.generatingSvg": "SVGç”»åƒä½œæˆä¸â€¦", - "app.presentationUploder.conversion.pageCountExceeded": "ページ数ãŒ200ページを超ãˆã¾ã—ãŸ", "app.presentationUploder.conversion.timeout": "エラー:変æ›ã«æ™‚é–“ãŒã‹ã‹ã‚Šã™ãŽã¾ã—ãŸ", "app.presentationUploder.isDownloadableLabel": "プレゼンテーションã®ãƒ€ã‚¦ãƒ³ãƒãƒ¼ãƒ‰ã‚’許å¯ã—ãªã„", "app.presentationUploder.isNotDownloadableLabel": "プレゼンテーションã®ãƒ€ã‚¦ãƒ³ãƒãƒ¼ãƒ‰ã‚’許å¯ã™ã‚‹", diff --git a/bigbluebutton-html5/private/locales/ja_JP.json b/bigbluebutton-html5/private/locales/ja_JP.json index fb5377e87d4d2d9e488bfbb88e9bab9679eda6dd..004fdd77817961a0ce8637cf8d49adcff2c82c1d 100644 --- a/bigbluebutton-html5/private/locales/ja_JP.json +++ b/bigbluebutton-html5/private/locales/ja_JP.json @@ -137,7 +137,6 @@ "app.presentationUploder.browseImagesLabel": "ã¾ãŸã¯ç”»åƒã‚’探ã™/ã‚ャプãƒãƒ£ã™ã‚‹", "app.presentationUploder.fileToUpload": "アップãƒãƒ¼ãƒ‰ã™ã‚‹...", "app.presentationUploder.currentBadge": "ç¾åœ¨", - "app.presentationUploder.genericError": "エラーãŒç™ºç”Ÿã—ã¾ã—ãŸ", "app.presentationUploder.rejectedError": "é¸æŠžãƒ•ã‚¡ã‚¤ãƒ«ãŒæ‹’å¦ã•ã‚Œã¾ã—ãŸã€‚ファイル形å¼ã‚’確èªã—ã¦ãã ã•ã„。", "app.presentationUploder.upload.progress": "アップãƒãƒ¼ãƒ‰ä¸ ({0}%)", "app.presentationUploder.conversion.conversionProcessingSlides": "{1}ページä¸{0}ページ目を処ç†ä¸", @@ -145,7 +144,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "サムãƒã‚¤ãƒ«ä½œæˆä¸â€¦", "app.presentationUploder.conversion.generatedSlides": "スライド作æˆä¸â€¦", "app.presentationUploder.conversion.generatingSvg": "SVGç”»åƒã‚’作æˆä¸â€¦", - "app.presentationUploder.conversion.pageCountExceeded": "ページ数ãŒ200ページを超ãˆã¾ã—ãŸ", "app.presentationUploder.conversion.timeout": "エラー:変æ›ã«æ™‚é–“ãŒã‹ã‹ã‚Šã™ãŽã¾ã—ãŸ", "app.presentationUploder.isDownloadableLabel": "プレゼンテーションã®ãƒ€ã‚¦ãƒ³ãƒãƒ¼ãƒ‰ã‚’許å¯ã—ãªã„", "app.presentationUploder.isNotDownloadableLabel": "プレゼンテーションã®ãƒ€ã‚¦ãƒ³ãƒãƒ¼ãƒ‰ã‚’許å¯ã™ã‚‹", @@ -573,7 +571,7 @@ "app.createBreakoutRoom.confirm": "作æˆ", "app.createBreakoutRoom.record": "録画", "app.createBreakoutRoom.numberOfRooms": "ルーム数", - "app.createBreakoutRoom.durationInMinutes": "利用時間(秒)", + "app.createBreakoutRoom.durationInMinutes": "利用時間(分)", "app.createBreakoutRoom.randomlyAssign": "ランダムã«å‰²ã‚Šå½“ã¦ã‚‹", "app.createBreakoutRoom.endAllBreakouts": "å…¨ã¦ã®ãƒ–レイクアウトルームを終了ã™ã‚‹", "app.createBreakoutRoom.roomName": "{0}(ルーム-{1})", @@ -588,7 +586,8 @@ "app.network.connection.effective.slow": "接続ã®å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸ", "app.iOSWarning.label": "iOS 12.2ã¾ãŸã¯ãれ以é™ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã«ã‚¢ãƒƒãƒ—グレードã—ã¦ãã ã•ã„", "app.legacy.unsupportedBrowser": "サãƒãƒ¼ãƒˆå¯¾è±¡å¤–ã®ãƒ–ラウザを使用ã—ã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚サãƒãƒ¼ãƒˆå¯¾è±¡ã®{0}ã¾ãŸã¯{1}ã‚’ãŠä½¿ã„ãã ã•ã„。", - "app.legacy.upgradeBrowser": "ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã®ãƒ–ラウザãŒå¤ã„å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚サãƒãƒ¼ãƒˆå¯¾è±¡ã®ãƒ–ラウザã¸ã‚¢ãƒƒãƒ—グレードã—ã¦ãã ã•ã„。" + "app.legacy.upgradeBrowser": "ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã®ãƒ–ラウザãŒå¤ã„å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚サãƒãƒ¼ãƒˆå¯¾è±¡ã®ãƒ–ラウザã¸ã‚¢ãƒƒãƒ—グレードã—ã¦ãã ã•ã„。", + "app.legacy.criosBrowser": "iOSã§ã¯ãƒ•ãƒ«ã‚µãƒãƒ¼ãƒˆã®ãŸã‚ã«Safariã‚’ãŠä½¿ã„ãã ã•ã„。" } diff --git a/bigbluebutton-html5/private/locales/km.json b/bigbluebutton-html5/private/locales/km.json index e2dc78f531621d9bf2a90cec198de78fe0d065a7..afe42aa7faeb2805e8b8c4a0108c5be9d6bd5bf0 100644 --- a/bigbluebutton-html5/private/locales/km.json +++ b/bigbluebutton-html5/private/locales/km.json @@ -160,7 +160,6 @@ "app.presentationUploder.browseImagesLabel": "ឫកáŸážŸáŸ’វែងរក/ážážážšáž¼áž”", "app.presentationUploder.fileToUpload": "នឹងážáŸ’រូវផ្ទុកឡើង...", "app.presentationUploder.currentBadge": "បច្ចុប្បន្ន", - "app.presentationUploder.genericError": "អុញ! មាន​បញ្ហា​អ្វី​មួយ​ហើយ", "app.presentationUploder.rejectedError": "ឯកសារដែលបានជ្រើសបានážáŸ’រូវបានបដិសáŸáž’។ សូមពិនិážáŸ’យមើលប្រភáŸáž‘របស់ឯកសារនáŸáŸ‡áŸ”", "app.presentationUploder.upload.progress": "កំពុងផ្ទុកឡើង ({0}%)", "app.presentationUploder.conversion.conversionProcessingSlides": "កំពុងពិនិážáŸ’យទំពáŸážš {0} ក្នុងចំណោម {1}", @@ -168,7 +167,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "កំពុងបង្កើážážšáž¼áž”ភាពážáž¼áž… ...", "app.presentationUploder.conversion.generatedSlides": "ស្លាយážáŸ’រូវបានបង្កើហ...", "app.presentationUploder.conversion.generatingSvg": "កំពុងបង្កើážážšáž¼áž”ភាព SVG ...", - "app.presentationUploder.conversion.pageCountExceeded": "អុញ ចំនួនទំពáŸážšáž›áž¾ážŸáž–ីចំនួនកំណážáŸ‹ ២០០ទំពáŸážšáž ើយ", "app.presentationUploder.conversion.timeout": "អុញ ការបម្លែងចំណាយពáŸáž›áž…្រើនហួស", "app.presentationUploder.isDownloadableLabel": "មិនឲ្យទាញយកបទបង្ហាញ", "app.presentationUploder.isNotDownloadableLabel": "ឲ្យទាញយកបទបង្ហាញ", diff --git a/bigbluebutton-html5/private/locales/ko_KR.json b/bigbluebutton-html5/private/locales/ko_KR.json index dbff728d6a2085154c8dbd3701e3305e7528f1b4..4d6cd640587505058ef7e02fb64b4175d041ff08 100644 --- a/bigbluebutton-html5/private/locales/ko_KR.json +++ b/bigbluebutton-html5/private/locales/ko_KR.json @@ -167,7 +167,6 @@ "app.presentationUploder.browseImagesLabel": "í˜¹ì€ ì´ë¯¸ì§€ 파ì¼ì„ 열거나 ìº¡ì³ ", "app.presentationUploder.fileToUpload": "업로드 ë ì˜ˆì •", "app.presentationUploder.currentBadge": "지금", - "app.presentationUploder.genericError": "어머나 ! ë”ê°€ 잘못 ë˜ì—ˆì–´ìš” ", "app.presentationUploder.rejectedError": "ì„ íƒí•œ 파ì¼(들)ì´ ê±°ì ˆ ë˜ì—ˆìŠµë‹ˆë‹¤. íŒŒì¼ ì¢…ë¥˜(들)를 살펴 보세요 ", "app.presentationUploder.upload.progress": "업로드중 ({0}%)", "app.presentationUploder.upload.413": "파ì¼ì´ 너무 í½ë‹ˆë‹¤. 여러파ì¼ë¡œ 나누세요 ", @@ -176,7 +175,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "ì¸ë„¤ì¼ ìƒì„±ì¤‘", "app.presentationUploder.conversion.generatedSlides": "슬ë¼ì´ë“œ ìƒì„±ì¤‘", "app.presentationUploder.conversion.generatingSvg": "SVG ì´ë¯¸ì§€ ìƒì„±ì¤‘", - "app.presentationUploder.conversion.pageCountExceeded": "어머나! 페ì´ì§€ê°€ 200 페ì´ì§€ ì œí•œì„ ë„˜ì—ˆìŠµë‹ˆë‹¤ ", "app.presentationUploder.conversion.pdfHasBigPage": "PDF 파ì¼ì„ ë³€í™˜í• ìˆ˜ 없습니다. 최ì 화를 ì‹œë„ í•´ 보세요 ", "app.presentationUploder.conversion.timeout": "어머나! ë³€í™˜ì´ ë„ˆë¬´ 오래 걸리네요 ", "app.presentationUploder.isDownloadableLabel": "프리ì í…Œì´ì…˜ 다운로드는 금지ë©ë‹ˆë‹¤ ", diff --git a/bigbluebutton-html5/private/locales/nl.json b/bigbluebutton-html5/private/locales/nl.json index 3b503ac9e7635940e7fd76a0885bdebd7cd8d049..14350eda94cec7757df8e237dab16b73bfe5b312 100644 --- a/bigbluebutton-html5/private/locales/nl.json +++ b/bigbluebutton-html5/private/locales/nl.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "of bladeren / vastleggen voor afbeeldingen", "app.presentationUploder.fileToUpload": "Wordt geüpload ...", "app.presentationUploder.currentBadge": "Current", - "app.presentationUploder.genericError": "Ops, er is iets misgegaan", "app.presentationUploder.rejectedError": "De geselecteerde bestanden zijn geweigerd. Controleer de bestandstype (s).", "app.presentationUploder.upload.progress": "Uploaden ({0}%)", "app.presentationUploder.upload.413": "Bestand is te groot. Splitsen in meerdere bestanden.", @@ -177,7 +176,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "Miniaturen genereren ...", "app.presentationUploder.conversion.generatedSlides": "Dia's gegenereerd ...", "app.presentationUploder.conversion.generatingSvg": "SVG-afbeeldingen genereren ...", - "app.presentationUploder.conversion.pageCountExceeded": "Oops, het aantal pagina's overschreed de limiet van 200 pagina's", "app.presentationUploder.conversion.pdfHasBigPage": "We konden het PDF-bestand niet converteren, probeer het te optimaliseren", "app.presentationUploder.conversion.timeout": "Ops, de conversie heeft te lang geduurd", "app.presentationUploder.isDownloadableLabel": "Sta niet toe dat de presentatie wordt gedownload", diff --git a/bigbluebutton-html5/private/locales/pl_PL.json b/bigbluebutton-html5/private/locales/pl_PL.json index dbfdd6e2ceb5b41780645477acb5aa5de0606105..10371d2d531fec51d2f8ba996bd95b2369bf644c 100644 --- a/bigbluebutton-html5/private/locales/pl_PL.json +++ b/bigbluebutton-html5/private/locales/pl_PL.json @@ -157,7 +157,6 @@ "app.presentationUploder.browseImagesLabel": "lub przeglÄ…daj obrazy lokalne", "app.presentationUploder.fileToUpload": "Do przesÅ‚ania...", "app.presentationUploder.currentBadge": "Bieżąca", - "app.presentationUploder.genericError": "Ups, coÅ› poszÅ‚o nie tak", "app.presentationUploder.rejectedError": "Plik(i) odrzucono. Sprawdź typ pliku(ów)", "app.presentationUploder.upload.progress": "PrzesyÅ‚anie ({0}%)", "app.presentationUploder.upload.413": "Plik jest za duży. Podziel go na kilka plików.", @@ -166,7 +165,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "Generowanie miniatur...", "app.presentationUploder.conversion.generatedSlides": "Utworzono slajdy...", "app.presentationUploder.conversion.generatingSvg": "Generowanie obrazów SVG...", - "app.presentationUploder.conversion.pageCountExceeded": "Ups, liczba stron przekroczyÅ‚a limit (200)", "app.presentationUploder.conversion.pdfHasBigPage": "Nie można skonwertować pliku PDF, spróbuj go zoptymalizować", "app.presentationUploder.conversion.timeout": "Ups, konwersja trwaÅ‚a zbyt dÅ‚ugo", "app.presentationUploder.isDownloadableLabel": "Nie pozwalaj pobierać prezentacji", diff --git a/bigbluebutton-html5/private/locales/pt.json b/bigbluebutton-html5/private/locales/pt.json index 811065e28f249bd93f2375667ebf38c25cfd86fc..e3211c279bf18c1acca51c05ec5d1251135acc1f 100644 --- a/bigbluebutton-html5/private/locales/pt.json +++ b/bigbluebutton-html5/private/locales/pt.json @@ -129,7 +129,6 @@ "app.presentationUploder.browseImagesLabel": "ou procure/capture imagens", "app.presentationUploder.fileToUpload": "Para carregar ...", "app.presentationUploder.currentBadge": "Atual", - "app.presentationUploder.genericError": "Ops, ocorreu um erro", "app.presentationUploder.rejectedError": "O(s) ficheiro(s) selecionados foram rejeitados.\nVerifique por favor os tipos de ficheiro.", "app.presentationUploder.upload.progress": "A carregar ({0}%)", "app.presentationUploder.conversion.conversionProcessingSlides": "A processar a página {0} de {1}", @@ -137,7 +136,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "A gerar miniaturas ...", "app.presentationUploder.conversion.generatedSlides": "Slides gerados ...", "app.presentationUploder.conversion.generatingSvg": "A gerar imagens SVG ...", - "app.presentationUploder.conversion.pageCountExceeded": "Ops, o número de páginas não pode exceder o limite de 200 páginas", "app.presentationUploder.conversion.timeout": "Ops, a conversão demorou muito", "app.presentationUploder.isDownloadableLabel": "Não permitir que a apresentação seja descarregada", "app.presentationUploder.isNotDownloadableLabel": "Permitir que a apresentação seja descarregada", diff --git a/bigbluebutton-html5/private/locales/pt_BR.json b/bigbluebutton-html5/private/locales/pt_BR.json index 948ca54b6eeeaecf2199cfc959a547683f95a343..0d6f657e7c2625fb28d11d63b4ce63c33cc441a3 100644 --- a/bigbluebutton-html5/private/locales/pt_BR.json +++ b/bigbluebutton-html5/private/locales/pt_BR.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "ou procure/capture imagens", "app.presentationUploder.fileToUpload": "Carregar arquivo...", "app.presentationUploder.currentBadge": "Atual", - "app.presentationUploder.genericError": "Ops, algo deu errado", "app.presentationUploder.rejectedError": "Os arquivos selecionados foram rejeitados. Por favor, verifique os tipos de arquivos permitidos.", "app.presentationUploder.upload.progress": "Carregando ({0}%)", "app.presentationUploder.upload.413": "O arquivo é muito grande. Por favor, divida o conteúdo em múltiplos arquivos.", @@ -177,9 +176,12 @@ "app.presentationUploder.conversion.generatingThumbnail": "Gerando miniaturas...", "app.presentationUploder.conversion.generatedSlides": "Slides gerados...", "app.presentationUploder.conversion.generatingSvg": "Gerando imagens SVG...", - "app.presentationUploder.conversion.pageCountExceeded": "Ops, o número de páginas excedeu o limite de 200 páginas", + "app.presentationUploder.conversion.pageCountExceeded": "Número de páginas excedido. Por favor, divida o arquivo em vários arquivos.", + "app.presentationUploder.conversion.officeDocConversionInvalid": "Falha ao processar o documento do office. Faça o upload de um arquivo em PDF.", + "app.presentationUploder.conversion.officeDocConversionFailed": "Falha ao processar o documento do office. Faça o upload de um arquivo em PDF.", "app.presentationUploder.conversion.pdfHasBigPage": "Não foi possÃvel converter o arquivo PDF", "app.presentationUploder.conversion.timeout": "Ops, a conversão levou tempo demais", + "app.presentationUploder.conversion.pageCountFailed": "Falha ao determinar o número de páginas.", "app.presentationUploder.isDownloadableLabel": "Não permitir download da apresentação", "app.presentationUploder.isNotDownloadableLabel": "Permitir download da apresentação", "app.presentationUploder.removePresentationLabel": "Remover apresentação", diff --git a/bigbluebutton-html5/private/locales/ro_RO.json b/bigbluebutton-html5/private/locales/ro_RO.json index 9b7c9a44319ce5d19e512b0640055e5d9e8f30aa..ab9ece2924e7c1a1420c77944ecea6a1e9bc631f 100644 --- a/bigbluebutton-html5/private/locales/ro_RO.json +++ b/bigbluebutton-html5/private/locales/ro_RO.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "sau cautati/capturati imagini", "app.presentationUploder.fileToUpload": "Pentru incarcare", "app.presentationUploder.currentBadge": "Curent", - "app.presentationUploder.genericError": "Ops, ceva a fost gresit", "app.presentationUploder.rejectedError": "Fisierele selectate au fost respinse. Va rugam sa verificati tipul (tipurile) de fisier.", "app.presentationUploder.upload.progress": "Incarcare ({0}%)", "app.presentationUploder.upload.413": "Fisierul este foarte mare.Va rog sa il impartiti in mai multe fisiere.", @@ -177,7 +176,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "Generare de miniaturi ...", "app.presentationUploder.conversion.generatedSlides": "Slide-urile generate ...", "app.presentationUploder.conversion.generatingSvg": "Generam imaginile SVG ...", - "app.presentationUploder.conversion.pageCountExceeded": "Ops, ati depasit limita de 200 de pagini", "app.presentationUploder.conversion.pdfHasBigPage": "Nu putem converti fisierul PDF, va rog sa incercati optimizarea lui", "app.presentationUploder.conversion.timeout": "Ops, conversia tine prea mult", "app.presentationUploder.isDownloadableLabel": "Nu permit download-ul prezentarii", diff --git a/bigbluebutton-html5/private/locales/ru_RU.json b/bigbluebutton-html5/private/locales/ru_RU.json index 8e7737fc2d834a7d218ae562f04371b3385ea919..d51ec5688b847738763cb0b234af960bc712def2 100644 --- a/bigbluebutton-html5/private/locales/ru_RU.json +++ b/bigbluebutton-html5/private/locales/ru_RU.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "или выберите/захватите картинку", "app.presentationUploder.fileToUpload": "чтобы ее загрузить", "app.presentationUploder.currentBadge": "Текущий", - "app.presentationUploder.genericError": "Ой, что-то пошло не так", "app.presentationUploder.rejectedError": "Загрузка выбранных файлов была отклонена. ПожалуйÑта проверьте тип файла (файлов)", "app.presentationUploder.upload.progress": "Загрузка ({0}%)", "app.presentationUploder.upload.413": "Файл Ñлишком большой. ПожалуйÑта, разделите его на неÑколько файлов меньшего размера.", @@ -177,7 +176,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "СоздаютÑÑ Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð¿Ñ€ÐµÐ´Ð²Ð°Ñ€Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ð³Ð¾ проÑмотра...", "app.presentationUploder.conversion.generatedSlides": "СоздаютÑÑ Ñлайды...", "app.presentationUploder.conversion.generatingSvg": "СоздаютÑÑ Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ SVG...", - "app.presentationUploder.conversion.pageCountExceeded": "Ой, доÑтигнут лимит количеÑтва Ñтраниц (200)", "app.presentationUploder.conversion.pdfHasBigPage": "Мы не Ñмогли Ñконвертировать PDF файл. ПожалуйÑта, попробуйте оптимизировать его.", "app.presentationUploder.conversion.timeout": "Ой, ÐºÐ¾Ð½Ð²ÐµÑ€Ñ‚Ð°Ñ†Ð¸Ñ Ð·Ð°Ð½Ð¸Ð¼Ð°ÐµÑ‚ Ñлишком много времени...", "app.presentationUploder.isDownloadableLabel": "Ðе разрешать Ñкачивание презентации", diff --git a/bigbluebutton-html5/private/locales/sk_SK.json b/bigbluebutton-html5/private/locales/sk_SK.json index 3d2b046df4cdc311f4ba9126ad94ffcf2e706fc7..7657092e7fe4112ec68d93199ff961ea39bfa19d 100644 --- a/bigbluebutton-html5/private/locales/sk_SK.json +++ b/bigbluebutton-html5/private/locales/sk_SK.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "alebo si vyhľadajte obrazové súbory", "app.presentationUploder.fileToUpload": "ktoré chcete nahraÅ¥ na server ...", "app.presentationUploder.currentBadge": "Aktuálne", - "app.presentationUploder.genericError": "Ops, nieÄo sa nepodarilo", "app.presentationUploder.rejectedError": "Vybratý súbor bol odmietnutý. prosÃm skontrolujte typ súboru.", "app.presentationUploder.upload.progress": "Nahrávanie ({0}%)", "app.presentationUploder.upload.413": "Súbor je prÃliÅ¡ veľký. ProsÃm rozdeľte ho na viacero menÅ¡Ãch súborov.", @@ -177,7 +176,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "Generujem náhľady ...", "app.presentationUploder.conversion.generatedSlides": "SnÃmky boli vygenerované ...", "app.presentationUploder.conversion.generatingSvg": "Generujem obrázky SVG ...", - "app.presentationUploder.conversion.pageCountExceeded": "Ops, poÄet strán presiahol limit 200 strán", "app.presentationUploder.conversion.pdfHasBigPage": "Nebolo možné skonvertovaÅ¥ .PDF súbor, prosÃm skúste ho optimalizovaÅ¥", "app.presentationUploder.conversion.timeout": "Ops, konverzia trvala prÃliÅ¡ dlho", "app.presentationUploder.isDownloadableLabel": "ZakázaÅ¥ sÅ¥ahovanie prezentácie", diff --git a/bigbluebutton-html5/private/locales/sr.json b/bigbluebutton-html5/private/locales/sr.json index 825822e9b8ea130786f149668135dd39a521c416..45fb10640e36b6f22c70bfc333706ff1a1a57371 100644 --- a/bigbluebutton-html5/private/locales/sr.json +++ b/bigbluebutton-html5/private/locales/sr.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "ili pretražite/napravite slike", "app.presentationUploder.fileToUpload": "ÄŒeka na uvoz ...", "app.presentationUploder.currentBadge": "Trenutni", - "app.presentationUploder.genericError": "Ups, doÅ¡lo je do greÅ¡ke", "app.presentationUploder.rejectedError": "Odabrani fajl/fajlovi su odbijeni. Proverite vrstu fajla/fajlova.", "app.presentationUploder.upload.progress": "Uvoz ({0}%)", "app.presentationUploder.upload.413": "Fajl je prevelik. Podelite u viÅ¡e fajlova.", @@ -177,7 +176,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "Kreiranje ikona ...", "app.presentationUploder.conversion.generatedSlides": "Kreiranje slajdova ...", "app.presentationUploder.conversion.generatingSvg": "Kreiranje SVG slika ...", - "app.presentationUploder.conversion.pageCountExceeded": "Ups, broj stranica je preaÅ¡o dozvoljen limit od 200", "app.presentationUploder.conversion.pdfHasBigPage": "Nismo uspeli da konvertujemo PDF fajl, probajte da ga optimizujete", "app.presentationUploder.conversion.timeout": "Ups, konverzija traje predugo", "app.presentationUploder.isDownloadableLabel": "Onemogućite preuzimanje prezentacije", diff --git a/bigbluebutton-html5/private/locales/sv_SE.json b/bigbluebutton-html5/private/locales/sv_SE.json index 590a95689aa21b83161741a36a994332674ff20d..0d2ed907a553005b82e86101ca65eb2fc5b7f873 100644 --- a/bigbluebutton-html5/private/locales/sv_SE.json +++ b/bigbluebutton-html5/private/locales/sv_SE.json @@ -164,7 +164,6 @@ "app.presentationUploder.browseImagesLabel": "eller bläddra/fÃ¥nga bilder", "app.presentationUploder.fileToUpload": "Att ladda upp ...", "app.presentationUploder.currentBadge": "Nuvarande", - "app.presentationUploder.genericError": "Hoppsan, nÃ¥nting gick snett", "app.presentationUploder.rejectedError": "Den valda filen(filerna) har blivit avvisad(e). Vänligen kontrollera filtypen(-typerna).", "app.presentationUploder.upload.progress": "Uppladdning ({0}%)", "app.presentationUploder.conversion.conversionProcessingSlides": "Bearbetningssida {0} av {1}", @@ -172,7 +171,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "Genererar miniatyrbilder ...", "app.presentationUploder.conversion.generatedSlides": "Bildspel genererade ...", "app.presentationUploder.conversion.generatingSvg": "Genererar SVG bilder ...", - "app.presentationUploder.conversion.pageCountExceeded": "Hoppsan, sidantalet överstig gränsen pÃ¥ 200 sidor", "app.presentationUploder.conversion.timeout": "Hoppsan, konverteringen tog för lÃ¥ng tid", "app.presentationUploder.isDownloadableLabel": "TillÃ¥t inte presentationen att kunna laddas ner", "app.presentationUploder.isNotDownloadableLabel": "TillÃ¥t presentationen att kunna laddas ner", diff --git a/bigbluebutton-html5/private/locales/th.json b/bigbluebutton-html5/private/locales/th.json index 390760ec140843cf68ec823c659f6e5ff10754a1..81910917c9400c4aa3468193eaf6a5544b96ae8c 100644 --- a/bigbluebutton-html5/private/locales/th.json +++ b/bigbluebutton-html5/private/locales/th.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "หรืà¸à¹€à¸£à¸µà¸¢à¸à¸”ู / จับภาพ", "app.presentationUploder.fileToUpload": "ที่จะà¸à¸±à¸žà¹‚หลด ...", "app.presentationUploder.currentBadge": "ปัจจุบัน", - "app.presentationUploder.genericError": "Ops มีบางà¸à¸¢à¹ˆà¸²à¸‡à¸œà¸´à¸”ปà¸à¸•à¸´", "app.presentationUploder.rejectedError": "ไฟล์(s)ที่เลืà¸à¸à¸–ูà¸à¸›à¸à¸´à¹€à¸ªà¸˜ โปรดตรวจสà¸à¸šà¸›à¸£à¸°à¹€à¸ ทขà¸à¸‡à¹„ฟล์(s)", "app.presentationUploder.upload.progress": "à¸à¸±à¸žà¹‚หลด ({0}%)", "app.presentationUploder.upload.413": "ไฟล์ใหà¸à¹ˆà¹€à¸à¸´à¸™à¹„ป โปรดà¹à¸šà¹ˆà¸‡à¸à¸à¸à¹€à¸›à¹‡à¸™à¸«à¸¥à¸²à¸¢à¹„ฟล์", @@ -177,7 +176,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "à¸à¸³à¸¥à¸±à¸‡à¸ªà¸£à¹‰à¸²à¸‡à¸ าพขนาดย่ภ...", "app.presentationUploder.conversion.generatedSlides": "สร้างสไลด์à¹à¸¥à¹‰à¸§ ...", "app.presentationUploder.conversion.generatingSvg": "à¸à¸³à¸¥à¸±à¸‡à¸ªà¸£à¹‰à¸²à¸‡à¸ าพ SVG ...", - "app.presentationUploder.conversion.pageCountExceeded": "Ops จำนวนหน้าเà¸à¸´à¸™à¸‚ีด จำà¸à¸±à¸” 200 หน้า", "app.presentationUploder.conversion.pdfHasBigPage": "เราไม่สามารถà¹à¸›à¸¥à¸‡à¹„ฟล์ PDF ได้โปรดลà¸à¸‡à¹€à¸žà¸´à¹ˆà¸¡à¸›à¸£à¸°à¸ªà¸´à¸—ธิภาพ", "app.presentationUploder.conversion.timeout": "Ops à¸à¸²à¸£à¹à¸›à¸¥à¸‡à¹ƒà¸Šà¹‰à¹€à¸§à¸¥à¸²à¸™à¸²à¸™à¹€à¸à¸´à¸™à¹„ป", "app.presentationUploder.isDownloadableLabel": "ไม่à¸à¸™à¸¸à¸à¸²à¸•à¹ƒà¸«à¹‰à¸”าวน์โหลดงานนำเสนà¸", diff --git a/bigbluebutton-html5/private/locales/th_TH.json b/bigbluebutton-html5/private/locales/th_TH.json index 390760ec140843cf68ec823c659f6e5ff10754a1..81910917c9400c4aa3468193eaf6a5544b96ae8c 100644 --- a/bigbluebutton-html5/private/locales/th_TH.json +++ b/bigbluebutton-html5/private/locales/th_TH.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "หรืà¸à¹€à¸£à¸µà¸¢à¸à¸”ู / จับภาพ", "app.presentationUploder.fileToUpload": "ที่จะà¸à¸±à¸žà¹‚หลด ...", "app.presentationUploder.currentBadge": "ปัจจุบัน", - "app.presentationUploder.genericError": "Ops มีบางà¸à¸¢à¹ˆà¸²à¸‡à¸œà¸´à¸”ปà¸à¸•à¸´", "app.presentationUploder.rejectedError": "ไฟล์(s)ที่เลืà¸à¸à¸–ูà¸à¸›à¸à¸´à¹€à¸ªà¸˜ โปรดตรวจสà¸à¸šà¸›à¸£à¸°à¹€à¸ ทขà¸à¸‡à¹„ฟล์(s)", "app.presentationUploder.upload.progress": "à¸à¸±à¸žà¹‚หลด ({0}%)", "app.presentationUploder.upload.413": "ไฟล์ใหà¸à¹ˆà¹€à¸à¸´à¸™à¹„ป โปรดà¹à¸šà¹ˆà¸‡à¸à¸à¸à¹€à¸›à¹‡à¸™à¸«à¸¥à¸²à¸¢à¹„ฟล์", @@ -177,7 +176,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "à¸à¸³à¸¥à¸±à¸‡à¸ªà¸£à¹‰à¸²à¸‡à¸ าพขนาดย่ภ...", "app.presentationUploder.conversion.generatedSlides": "สร้างสไลด์à¹à¸¥à¹‰à¸§ ...", "app.presentationUploder.conversion.generatingSvg": "à¸à¸³à¸¥à¸±à¸‡à¸ªà¸£à¹‰à¸²à¸‡à¸ าพ SVG ...", - "app.presentationUploder.conversion.pageCountExceeded": "Ops จำนวนหน้าเà¸à¸´à¸™à¸‚ีด จำà¸à¸±à¸” 200 หน้า", "app.presentationUploder.conversion.pdfHasBigPage": "เราไม่สามารถà¹à¸›à¸¥à¸‡à¹„ฟล์ PDF ได้โปรดลà¸à¸‡à¹€à¸žà¸´à¹ˆà¸¡à¸›à¸£à¸°à¸ªà¸´à¸—ธิภาพ", "app.presentationUploder.conversion.timeout": "Ops à¸à¸²à¸£à¹à¸›à¸¥à¸‡à¹ƒà¸Šà¹‰à¹€à¸§à¸¥à¸²à¸™à¸²à¸™à¹€à¸à¸´à¸™à¹„ป", "app.presentationUploder.isDownloadableLabel": "ไม่à¸à¸™à¸¸à¸à¸²à¸•à¹ƒà¸«à¹‰à¸”าวน์โหลดงานนำเสนà¸", diff --git a/bigbluebutton-html5/private/locales/tr.json b/bigbluebutton-html5/private/locales/tr.json index 7bac7adfbd0588802a6ee3566f474da0ebb34f3f..db4e496d2dd32985a91639ab5192568c97ee301a 100644 --- a/bigbluebutton-html5/private/locales/tr.json +++ b/bigbluebutton-html5/private/locales/tr.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "veya dosyalara göz atın", "app.presentationUploder.fileToUpload": "Yüklenecek ...", "app.presentationUploder.currentBadge": "Åžimdiki", - "app.presentationUploder.genericError": "Hops, birÅŸeyler ters gitti", "app.presentationUploder.rejectedError": "Seçilen dosya(lar) reddedildi. Lütfen dosya tür(ler)ini kontrol edin.", "app.presentationUploder.upload.progress": "Yükleniyor ({0}%)", "app.presentationUploder.upload.413": "Dosya çok büyük. Lütfen daha küçük dosyalara bölün.", @@ -177,7 +176,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "Küçük resimler oluÅŸturuluyor ...", "app.presentationUploder.conversion.generatedSlides": "Slaytlar oluÅŸturuluyor ...", "app.presentationUploder.conversion.generatingSvg": "SVG görselleri üretiliyor ...", - "app.presentationUploder.conversion.pageCountExceeded": "Hata, sayfa sayısı 200 sayfa sınırını aÅŸtı", "app.presentationUploder.conversion.pdfHasBigPage": "PDF dosyasını dönüştüremedik, lütfen optimize etmeyi deneyin", "app.presentationUploder.conversion.timeout": "Hata, dönüşüm çok uzun sürdü", "app.presentationUploder.isDownloadableLabel": "Sunumun indirilmesine izin verme", diff --git a/bigbluebutton-html5/private/locales/tr_TR.json b/bigbluebutton-html5/private/locales/tr_TR.json index 7bac7adfbd0588802a6ee3566f474da0ebb34f3f..db4e496d2dd32985a91639ab5192568c97ee301a 100644 --- a/bigbluebutton-html5/private/locales/tr_TR.json +++ b/bigbluebutton-html5/private/locales/tr_TR.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "veya dosyalara göz atın", "app.presentationUploder.fileToUpload": "Yüklenecek ...", "app.presentationUploder.currentBadge": "Åžimdiki", - "app.presentationUploder.genericError": "Hops, birÅŸeyler ters gitti", "app.presentationUploder.rejectedError": "Seçilen dosya(lar) reddedildi. Lütfen dosya tür(ler)ini kontrol edin.", "app.presentationUploder.upload.progress": "Yükleniyor ({0}%)", "app.presentationUploder.upload.413": "Dosya çok büyük. Lütfen daha küçük dosyalara bölün.", @@ -177,7 +176,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "Küçük resimler oluÅŸturuluyor ...", "app.presentationUploder.conversion.generatedSlides": "Slaytlar oluÅŸturuluyor ...", "app.presentationUploder.conversion.generatingSvg": "SVG görselleri üretiliyor ...", - "app.presentationUploder.conversion.pageCountExceeded": "Hata, sayfa sayısı 200 sayfa sınırını aÅŸtı", "app.presentationUploder.conversion.pdfHasBigPage": "PDF dosyasını dönüştüremedik, lütfen optimize etmeyi deneyin", "app.presentationUploder.conversion.timeout": "Hata, dönüşüm çok uzun sürdü", "app.presentationUploder.isDownloadableLabel": "Sunumun indirilmesine izin verme", diff --git a/bigbluebutton-html5/private/locales/uk_UA.json b/bigbluebutton-html5/private/locales/uk_UA.json index c9f30fc3f9f9d9167c1eec55f1667da1a6cccb3c..25591cd47cf4c465a21e01e0886ae9f6aa7c40d9 100644 --- a/bigbluebutton-html5/private/locales/uk_UA.json +++ b/bigbluebutton-html5/private/locales/uk_UA.json @@ -3,7 +3,10 @@ "app.chat.submitLabel": "ÐадіÑлати повідомленнÑ", "app.chat.errorMinMessageLength": "ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ñ–Ð· {0} Ñимвола(-ів) занадто коротке", "app.chat.errorMaxMessageLength": "ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ñ–Ð· {0} Ñимвола(-ів) занадто довге", + "app.chat.disconnected": "Ви від'єдналиÑÑŒ, Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð½Ðµ можуть бути надіÑлані", + "app.chat.locked": "Чат заблокований, Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð½ÐµÐ¼Ð¾Ð¶Ð»Ð¸Ð²Ð¾ надіÑлати", "app.chat.inputLabel": "Ð’Ð²ÐµÐ´ÐµÐ½Ð½Ñ Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ñ‡Ð°Ñ‚Ñƒ {0}", + "app.chat.inputPlaceholder": "ÐадіÑлати Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ {0}", "app.chat.titlePublic": "Загальний чат", "app.chat.titlePrivate": "Приватний чат з {0}", "app.chat.partnerDisconnected": "{0} покинув конференцію", @@ -17,10 +20,18 @@ "app.chat.label": "Чат", "app.chat.offline": "Ðе в мережі", "app.chat.emptyLogLabel": "Журнал чату порожній", - "app.chat.clearPublicChatMessage": "ІÑÑ‚Ð¾Ñ€Ñ–Ñ Ð¿ÑƒÐ±Ð»Ñ–Ñ‡Ð½Ð¾Ð³Ð¾ чату була очищена модератором", + "app.chat.clearPublicChatMessage": "ІÑÑ‚Ð¾Ñ€Ñ–Ñ Ð·Ð°Ð³Ð°Ð»ÑŒÐ½Ð¾Ð³Ð¾ чату була очищена модератором", + "app.chat.multi.typing": "Декілька кориÑтувачів набирають", + "app.chat.one.typing": "{0} набирає", + "app.chat.two.typing": "{0} Ñ– {1} набирають", + "app.captions.label": "Субтитри", "app.captions.menu.close": "Закрити", "app.captions.menu.start": "Почати", + "app.captions.menu.ariaStart": "Почати пиÑати Ñубтитри", + "app.captions.menu.ariaStartDesc": "Відкриває редактор Ñубтитрів та закриває цей діалог", "app.captions.menu.select": "Виберіть доÑтупну мову", + "app.captions.menu.ariaSelect": "Мова Ñубтитрів", + "app.captions.menu.subtitle": "Будь лаÑка, виберіть мову Ñ– Ñтилі прихованих Ñубтитрів Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ ÑеанÑу", "app.captions.menu.title": "Субтитри", "app.captions.menu.fontSize": "Розмір", "app.captions.menu.fontColor": "Колір текÑту", @@ -30,6 +41,13 @@ "app.captions.menu.cancelLabel": "Відмінити", "app.captions.pad.hide": "Приховати Ñубтитри", "app.captions.pad.tip": "ÐатиÑніть Esc, щоб ÑфокуÑувати панель інÑтрументів редактора", + "app.captions.pad.ownership": "Стати ведучим", + "app.captions.pad.ownershipTooltip": "Ви будете призначені Ñк влаÑник {0} Ñубтитрів", + "app.captions.pad.interimResult": "Проміжні результати", + "app.captions.pad.dictationStart": "Почати диктуваннÑ", + "app.captions.pad.dictationStop": "Зупинити диктуваннÑ", + "app.captions.pad.dictationOnDesc": "Включити Ñ€Ð¾Ð·Ð¿Ñ–Ð·Ð½Ð°Ð²Ð°Ð½Ð½Ñ Ð¼Ð¾Ð²Ð¸", + "app.captions.pad.dictationOffDesc": "Виключити Ñ€Ð¾Ð·Ð¿Ñ–Ð·Ð½Ð°Ð²Ð°Ð½Ð½Ñ Ð¼Ð¾Ð²Ð¸", "app.note.title": "Спільні примітки", "app.note.label": "Примітки", "app.note.hideNoteLabel": "Сховати примітки", @@ -41,6 +59,8 @@ "app.userList.participantsTitle": "УчаÑники", "app.userList.messagesTitle": "ПовідомленнÑ", "app.userList.notesTitle": "Примітки", + "app.userList.notesListItem.unreadContent": "Ð’ розділі \"Спільні примітки\" поÑвилаÑÑŒ нова інформаціÑ", + "app.userList.captionsTitle": "Субтитри", "app.userList.presenter": "Ведучий", "app.userList.you": "Ви", "app.userList.locked": "ОбмеженнÑ", @@ -50,6 +70,7 @@ "app.userList.menuTitleContext": "ДоÑтупні опції", "app.userList.chatListItem.unreadSingular": "{0} нове повідомленнÑ", "app.userList.chatListItem.unreadPlural": "{0} нових повідомлень", + "app.userList.menu.chat.label": "Почати приватний чат", "app.userList.menu.clearStatus.label": "ОчиÑтити ÑтатуÑ", "app.userList.menu.removeUser.label": "Виключити кориÑтувача", "app.userList.menu.muteUserAudio.label": "Вимкнути мікрофон кориÑтувача", @@ -59,6 +80,7 @@ "app.userList.menu.demoteUser.label": "Понизити до глÑдача", "app.userList.menu.unlockUser.label": "ЗнÑти Ð¾Ð±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ Ð´Ð»Ñ {0}", "app.userList.menu.lockUser.label": "Обмежити можливоÑÑ‚Ñ– Ð´Ð»Ñ {0}", + "app.userList.menu.directoryLookup.label": "Пошук в каталозі", "app.userList.menu.makePresenter.label": "Зробити ведучим", "app.userList.userOptions.manageUsersLabel": "Керувати кориÑтувачами", "app.userList.userOptions.muteAllLabel": "Вимкнути мікрофон вÑім", @@ -74,19 +96,29 @@ "app.userList.userOptions.disableCam": "Веб-камери глÑдачів відключені", "app.userList.userOptions.disableMic": "Мікрофони глÑдачів відключені", "app.userList.userOptions.disablePrivChat": "Приватний чат вимкнено", - "app.userList.userOptions.disablePubChat": "ЗагальнодоÑтупний чат вимкнено", + "app.userList.userOptions.disablePubChat": "Загальний чат вимкнено", "app.userList.userOptions.disableNote": "Спільні нотатки тепер заблоковані", + "app.userList.userOptions.hideUserList": "СпиÑок кориÑтувачів тепер прихований від учаÑників", "app.userList.userOptions.webcamsOnlyForModerator": "Веб-камери глÑдачів можуть бачити лише модератори (через Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ)", + "app.userList.content.participants.options.clearedStatus": "Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ð¾Ñ‡Ð¸Ñ‰ÐµÐ½Ð¾", "app.userList.userOptions.enableCam": "Веб-камери глÑдачів увімкнено", "app.userList.userOptions.enableMic": "Мікрофони глÑдачів увімкнено", "app.userList.userOptions.enablePrivChat": "Приватний чат увімкнено", - "app.userList.userOptions.enablePubChat": "ЗагальнодоÑтупний чат увімкнено", + "app.userList.userOptions.enablePubChat": "Загальний чат увімкнено", "app.userList.userOptions.enableNote": "Спільні нотатки тепер увімкнено", + "app.userList.userOptions.showUserList": "СпиÑок кориÑтувачів тепер видимий Ð´Ð»Ñ ÑƒÑ‡Ð°Ñників", "app.userList.userOptions.enableOnlyModeratorWebcam": "Тепер можна активувати веб-камеру, вÑÑ– бачитимуть ваÑ", "app.media.label": "Медіа", + "app.media.autoplayAlertDesc": "Дозволити доÑтуп", "app.media.screenshare.start": "ДемонÑÑ‚Ñ€Ð°Ñ†Ñ–Ñ ÐµÐºÑ€Ð°Ð½Ñƒ розпочалаÑÑ", "app.media.screenshare.end": "ДемонÑтрацію екрану закінчено", "app.media.screenshare.safariNotSupported": "ДемонÑÑ‚Ñ€Ð°Ñ†Ñ–Ñ ÐµÐºÑ€Ð°Ð½Ñƒ наразі не підтримуєтьÑÑ Safari. Будь лаÑка, викориÑтовуйте Firefox або Google Chrome.", + "app.media.screenshare.autoplayBlockedDesc": "Ðам потрібен Ваш дозвіл, щоб показати Вам екран ведучого.", + "app.media.screenshare.autoplayAllowLabel": "Показати екран, Ñкий демонÑтруєтьÑÑ", + "app.screenshare.notAllowed": "Помилка: Дозвіл на доÑтуп до екрану не було надано.", + "app.screenshare.notSupportedError": "Помилка: ДемонÑÑ‚Ñ€Ð°Ñ†Ñ–Ñ ÐµÐºÑ€Ð°Ð½Ñƒ можлива тільки на безпечних (SSL) доменах", + "app.screenshare.notReadableError": "Помилка: При Ñпробі захопити екран ÑталаÑÑŒ помилка", + "app.screenshare.genericError": "Помилка: ВідбулаÑÑŒ помилка при демонÑтрації екрану. Будь лаÑка, Ñпробуйте пізніше", "app.meeting.ended": "Ð¦Ñ ÑеÑÑ–Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐ¸Ð»Ð°ÑÑ", "app.meeting.meetingTimeRemaining": "ЗалишилоÑÑŒ чаÑу зуÑтрічі: {0}", "app.meeting.meetingTimeHasEnded": "Ð§Ð°Ñ Ð·Ð°ÐºÑ–Ð½Ñ‡Ð¸Ð²ÑÑ. ЗуÑтріч буде закрито незабаром", @@ -99,6 +131,8 @@ "app.presentation.startSlideContent": "Початок вміÑту Ñлайду", "app.presentation.endSlideContent": "Кінець вміÑту Ñлайду", "app.presentation.emptySlideContent": "Даний Ñлайд порожній", + "app.presentation.presentationToolbar.noNextSlideDesc": "Кінець презентації", + "app.presentation.presentationToolbar.noPrevSlideDesc": "Початок презентації", "app.presentation.presentationToolbar.selectLabel": "Вибрати Ñлайд", "app.presentation.presentationToolbar.prevSlideLabel": "Попередній Ñлайд", "app.presentation.presentationToolbar.prevSlideDesc": "Перемкнути презентацію на попередній Ñлайд", @@ -119,8 +153,10 @@ "app.presentation.presentationToolbar.zoomReset": "Скинути маÑштаб", "app.presentation.presentationToolbar.zoomIndicator": "Поточне Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð¼Ð°Ñштабу", "app.presentation.presentationToolbar.fitToWidth": "Підігнати по ширині", + "app.presentation.presentationToolbar.fitToPage": "Підігнати під розмір Ñторінки", "app.presentation.presentationToolbar.goToSlide": "Слайд {0}", "app.presentationUploder.title": "ПрезентаціÑ", + "app.presentationUploder.message": "Як ведучий ви маєте можливіÑÑ‚ÑŒ завантажувати будь-Ñкий офіÑний документ або PDF-файл. Ð”Ð»Ñ Ð½Ð°Ð¹ÐºÑ€Ð°Ñ‰Ð¸Ñ… результатів ми рекомендуємо PDF-файл. ПереконайтеÑÑ, що вибрано презентацію за допомогою Ð¿Ñ€Ð°Ð¿Ð¾Ñ€Ñ†Ñ Ð¿Ñ€Ð°Ð²Ð¾Ñ€ÑƒÑ‡.", "app.presentationUploder.uploadLabel": "Завантажити", "app.presentationUploder.confirmLabel": "Підтвердити", "app.presentationUploder.confirmDesc": "Зберегти зміни та розпочати презентацію", @@ -132,15 +168,15 @@ "app.presentationUploder.browseImagesLabel": "або виберіть/захопіть зображеннÑ", "app.presentationUploder.fileToUpload": "Буде завантажено ...", "app.presentationUploder.currentBadge": "Поточний", - "app.presentationUploder.genericError": "Ой, щоÑÑŒ пішло не так", "app.presentationUploder.rejectedError": "Вибрані файл(и) відхилено. Перевірте тип файлу(iв).", "app.presentationUploder.upload.progress": "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ ({0}%)", + "app.presentationUploder.upload.413": "Файл занадто великий. Будь лаÑка, розділіть його на декілька файлів меншого розміру.", "app.presentationUploder.conversion.conversionProcessingSlides": "Обробка Ñторінки {0} з {1}", "app.presentationUploder.conversion.genericConversionStatus": "Файл конвертуєтьÑÑ...", "app.presentationUploder.conversion.generatingThumbnail": "Ð“ÐµÐ½ÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð¼Ñ–Ð½Ñ–Ð°Ñ‚ÑŽÑ€...", "app.presentationUploder.conversion.generatedSlides": "Слайди генеруютьÑÑ...", - "app.presentationUploder.conversion.generatingSvg": "Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ñ–Ñ Ñлайдів SVG...", - "app.presentationUploder.conversion.pageCountExceeded": "Ой, кількіÑÑ‚ÑŒ Ñторінок перевищила ліміт у 200 Ñторінок", + "app.presentationUploder.conversion.generatingSvg": "Ð“ÐµÐ½ÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ñлайдів SVG...", + "app.presentationUploder.conversion.pdfHasBigPage": "Ми не змогли конвертувати PDF файл, будь лаÑка, Ñпробуйте оптимізувати його", "app.presentationUploder.conversion.timeout": "Ой, Ð¿ÐµÑ€ÐµÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð·Ð°Ð¹Ð¼Ð°Ñ” надто багато чаÑу", "app.presentationUploder.isDownloadableLabel": "Ðе дозволÑти Ñкачувати презентацію", "app.presentationUploder.isNotDownloadableLabel": "Дозволити Ñкачувати презентацію", @@ -156,6 +192,7 @@ "app.poll.quickPollInstruction": "Оберіть опцію нижче, щоб почати опитуваннÑ.", "app.poll.customPollLabel": "Своє опитуваннÑ", "app.poll.startCustomLabel": "Розпочати Ñвоє опитуваннÑ", + "app.poll.activePollInstruction": "Залиште цю панель відкритою, щоб бачити відповіді на Ð¾Ð¿Ð¸Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð² реальному чаÑÑ–. Коли будете готові, оберіть \"Опублікувати результати голоÑуваннÑ\", щоб опублікувати результати Ñ– завершити опитуваннÑ.", "app.poll.publishLabel": "Опублікувати результати опитуваннÑ", "app.poll.backLabel": "Ðазад до параметрів опитуваннÑ", "app.poll.closeLabel": "Закрити", @@ -192,6 +229,7 @@ "app.downloadPresentationButton.label": "Скачати оригінал презентації", "app.connectingMessage": "З'єднаннÑ...", "app.waitingMessage": "Втрачено з'еднаннÑ. Спроба повторного Ð¿Ñ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ñ‡ÐµÑ€ÐµÐ· {0} Ñекунд...", + "app.retryNow": "Повторити", "app.navBar.settingsDropdown.optionsLabel": "Опції", "app.navBar.settingsDropdown.fullscreenLabel": "Розгорнути на веÑÑŒ екран", "app.navBar.settingsDropdown.settingsLabel": "Відкрити параметри", @@ -203,7 +241,7 @@ "app.navBar.settingsDropdown.aboutDesc": "Показати інформацію про клієнта", "app.navBar.settingsDropdown.leaveSessionDesc": "Залишити конференцію", "app.navBar.settingsDropdown.exitFullscreenDesc": "Вийти з повноекранного режиму", - "app.navBar.settingsDropdown.hotkeysLabel": "ГарÑчі клавіши", + "app.navBar.settingsDropdown.hotkeysLabel": "ГарÑчі клавіші", "app.navBar.settingsDropdown.hotkeysDesc": "ÐŸÐµÑ€ÐµÐ¿Ð¸Ñ Ð´Ð¾Ñтупних гарÑчих клавiш", "app.navBar.settingsDropdown.helpLabel": "Допомога", "app.navBar.settingsDropdown.helpDesc": "ПеренаправлÑÑ” кориÑтувача з відеоуроками (відкриваєтьÑÑ Ð½Ð¾Ð²Ð° вкладка)", @@ -215,6 +253,7 @@ "app.navBar.recording": "Ð¦Ñ ÑеÑÑ–Ñ Ð·Ð°Ð¿Ð¸ÑуєтьÑÑ", "app.navBar.recording.on": "ЗапиÑуєтьÑÑ", "app.navBar.recording.off": "Ðе запиÑуєтьÑÑ", + "app.navBar.emptyAudioBrdige": "Ðемає активного мікрофону. Ðктивуйте Ваш мікрофон, щоб додати звук в даний запиÑ.", "app.leaveConfirmation.confirmLabel": "Вийти", "app.leaveConfirmation.confirmDesc": "Вийти з конференції", "app.endMeeting.title": "Завершити зуÑтріч", @@ -241,6 +280,8 @@ "app.submenu.application.animationsLabel": "Ðнімації", "app.submenu.application.audioAlertLabel": "Ðудіо ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ñ‡Ð°Ñ‚Ñƒ", "app.submenu.application.pushAlertLabel": "Спливаючі ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ñ‡Ð°Ñ‚Ñƒ", + "app.submenu.application.userJoinAudioAlertLabel": "Ðудіо ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¸Ñ”Ð´Ð½Ð°Ð½Ð½Ñ ÐºÐ¾Ñ€Ð¸Ñтувача", + "app.submenu.application.userJoinPushAlertLabel": "Спливаючі ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¸Ñ”Ð´Ð½Ð°Ð½Ð½Ñ ÐºÐ¾Ñ€Ð¸Ñтувача", "app.submenu.application.fontSizeControlLabel": "Розмір шрифту", "app.submenu.application.increaseFontBtnLabel": "Збільшити шрифт заÑтоÑунку", "app.submenu.application.decreaseFontBtnLabel": "Зменшити шрифт заÑтоÑунку", @@ -273,10 +314,14 @@ "app.settings.save-notification.label": "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð·Ð±ÐµÑ€ÐµÐ¶ÐµÐ½Ð¾", "app.switch.onLabel": "УВІМК.", "app.switch.offLabel": "ВИМК.", + "app.talkingIndicator.ariaMuteDesc" : "ÐатиÑніть, щоб вимкнути мікрофон кориÑтувача", + "app.talkingIndicator.isTalking" : "{0} говорить", + "app.talkingIndicator.wasTalking" : "{0} закінчив говорити", "app.actionsBar.actionsDropdown.actionsLabel": "Дії", "app.actionsBar.actionsDropdown.presentationLabel": "Завантажити презентацію", "app.actionsBar.actionsDropdown.initPollLabel": "Розпочати опитуваннÑ", "app.actionsBar.actionsDropdown.desktopShareLabel": "ДемонÑтрувати ваш екран", + "app.actionsBar.actionsDropdown.lockedDesktopShareLabel": "ДемонÑÑ‚Ñ€Ð°Ñ†Ñ–Ñ ÐµÐºÑ€Ð°Ð½Ñƒ заблокована", "app.actionsBar.actionsDropdown.stopDesktopShareLabel": "Зупинити демонÑтрацію екрану", "app.actionsBar.actionsDropdown.presentationDesc": "Завантажити вашу презентацію", "app.actionsBar.actionsDropdown.initPollDesc": "Розпочати опитуваннÑ", @@ -284,8 +329,11 @@ "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Зупинити демонÑтрацію екрану", "app.actionsBar.actionsDropdown.pollBtnLabel": "Розпочати опитуваннÑ", "app.actionsBar.actionsDropdown.pollBtnDesc": "Вкл/Викл панель опитуваннÑ", + "app.actionsBar.actionsDropdown.saveUserNames": "Зберегти імена кориÑтувачів", "app.actionsBar.actionsDropdown.createBreakoutRoom": "Створити кімнати Ð´Ð»Ñ ÑƒÑ‡Ð°Ñників", "app.actionsBar.actionsDropdown.createBreakoutRoomDesc": "Ñтворити кімнати Ñ– розділити учаÑників між ними ", + "app.actionsBar.actionsDropdown.captionsLabel": "ÐапиÑати приховані Ñубтитри", + "app.actionsBar.actionsDropdown.captionsDesc": "Включає панель Ñубтитрів", "app.actionsBar.actionsDropdown.takePresenter": "Стати презентатором", "app.actionsBar.actionsDropdown.takePresenterDesc": "Ð’Ñтановити Ñебе ведучим/презентером", "app.actionsBar.emojiMenu.statusTriggerLabel": "Задати ÑтатуÑ", @@ -337,6 +385,7 @@ "app.breakoutTimeRemainingMessage": "Ð§Ð°Ñ Ð´Ð¾ Ð·Ð°ÐºÑ–Ð½Ñ‡ÐµÐ½Ð½Ñ ÐºÐ¾Ð½Ñ„ÐµÑ€ÐµÐ½Ñ†Ñ–Ñ—: {0}", "app.breakoutWillCloseMessage": "Ð§Ð°Ñ Ð²Ð¸Ñ‡ÐµÑ€Ð¿Ð°Ð½Ð¾. Конференцію невдовзі буде закрито", "app.calculatingBreakoutTimeRemaining": "Підрахунок чаÑу що залишивÑÑ...", + "app.audioModal.ariaTitle": "Вікно Ð¿Ñ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ð´Ð¾ аудіо-конференції", "app.audioModal.microphoneLabel": "Мікрофон", "app.audioModal.listenOnlyLabel": "Тільки Ñлухати", "app.audioModal.audioChoiceLabel": "Як ви хочете увійти в аудіо-конференцію?", @@ -353,14 +402,23 @@ "app.audioModal.echoTestTitle": "Це приватний ехо-теÑÑ‚. Промовте кілька Ñлів. Чи чуєте ви Ñебе в динаміках?", "app.audioModal.settingsTitle": "Змінити Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð°ÑƒÐ´Ñ–Ð¾", "app.audioModal.helpTitle": "З'ÑвилиÑÑ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð¸ з вашими аудіоприладами", - "app.audioModal.helpText": "Чи надали ви BigBlueButton дозвіл на доÑтуп до мікрофона? Зверніть увагу що коли ви намагаєтеÑÑ Ð¿Ñ€Ð¸Ñ”Ð´Ð½Ð°Ñ‚Ð¸ÑÑ Ð´Ð¾ аудіоконференції має з'ÑвитиÑÑ Ð´Ñ–Ð°Ð»Ð¾Ð³Ð¾Ð²Ðµ вікно запитуючи дозволи на Ð¿Ñ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ð¼ÐµÐ´Ñ–Ð°-приÑтрою, будь лаÑка, прийміть це, щоб приєднатиÑÑ Ð´Ð¾ аудіоконференції. Якщо цього не відбулоÑÑ Ñпробуйте змінити дозволи мікрофона у налаштуваннÑÑ… вашого веб-переглÑдача.", + "app.audioModal.helpText": "Чи надали ви BigBlueButton дозвіл на доÑтуп до мікрофона? Зверніть увагу, що коли ви намагаєтеÑÑ Ð¿Ñ€Ð¸Ñ”Ð´Ð½Ð°Ñ‚Ð¸ÑÑ Ð´Ð¾ аудіо-конференції, має з'ÑвитиÑÑ Ð´Ñ–Ð°Ð»Ð¾Ð³Ð¾Ð²Ðµ вікно, в Ñкому Ð’Ð°Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ð°ÑŽÑ‚ÑŒ дозвіл на Ð¿Ñ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ð¼ÐµÐ´Ñ–Ð°-приÑтрою, будь лаÑка, надайте його, щоб приєднатиÑÑ Ð´Ð¾ аудіо-конференції. Якщо цього не відбулоÑÑ, Ñпробуйте змінити дозволи мікрофона у налаштуваннÑÑ… вашого веб-переглÑдача.", + "app.audioModal.help.noSSL": "Сторінка незахищена. Щоб дозволити доÑтуп до мікрофона, Ñторінка повинна обÑлуговуватиÑÑ Ñ‡ÐµÑ€ÐµÐ· HTTPS. Будь лаÑка, зв'ÑжітьÑÑ Ð· адмініÑтратором Ñервера.", + "app.audioModal.help.macNotAllowed": "Схоже, ÑиÑтемні наÑтройки Mac блокують доÑтуп до Вашого мікрофону. Відкрийте System Preferences> Security & Privacy> Privacy> Microphone, Ñ– переконайтеÑÑ, що викориÑтовуваний Вами браузер відзначений.", "app.audioModal.audioDialTitle": "ПриєднатиÑÑ Ð·Ð° допомогою телефону", + "app.audioDial.audioDialDescription": "Ðаберіть номер", "app.audioDial.audioDialConfrenceText": "Ñ– введіть PIN-код конференції:", + "app.audioModal.autoplayBlockedDesc": "Ðам необхідний Ваш дозвіл на Ð²Ñ–Ð´Ñ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð°ÑƒÐ´Ñ–Ð¾", + "app.audioModal.playAudio": "Відтворювати звук", + "app.audioModal.playAudio.arialabel" : "Відтворювати звук", + "app.audioDial.tipIndicator": "Підказка", + "app.audioDial.tipMessage": "ÐатиÑніть кнопку '0' на телефоні, щоб відключити / включити Ñвій мікрофон", "app.audioModal.connecting": "ПідключеннÑ", "app.audioModal.connectingEchoTest": "ÐŸÑ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ð´Ð¾ ехо теÑту", "app.audioManager.joinedAudio": "Ви приєдналиÑÑ Ð´Ð¾ аудіоконференції", "app.audioManager.joinedEcho": "Ви приєдналиÑÑ Ð´Ð¾ ехо теÑту", "app.audioManager.leftAudio": "Ви покинули аудіо конференцію", + "app.audioManager.reconnectingAudio": "Спроба повторно підключити аудіо", "app.audioManager.genericError": "Помилка: ЩоÑÑŒ пішло не так, будь лаÑка, Ñпробуйте ще раз", "app.audioManager.connectionError": "Помилка: ÐŸÑ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ð½Ðµ вдалоÑÑ", "app.audioManager.requestTimeout": "Помилка: Ð§Ð°Ñ Ð¾Ñ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ñ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ð²Ð¸Ñ‡ÐµÑ€Ð¿Ð°Ð½Ð¾", @@ -389,9 +447,12 @@ "app.meeting.logout.validateTokenFailedEjectReason": "Ðе вдалоÑÑ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€Ð¸Ñ‚Ð¸ токен авторизації", "app.meeting.logout.userInactivityEjectReason": "КориÑтувач неактивний занадто довго", "app.meeting-ended.rating.legendLabel": "Рейтинг відгуків", + "app.meeting-ended.rating.starLabel": "Зірка", "app.modal.close": "Закрити", + "app.modal.close.description": "ВідхилÑÑ” зміни та закриває вікно", "app.modal.confirm": "Готово", "app.modal.newTab": "(відкриває нову вкладку)", + "app.modal.confirm.description": "Зберігає зміни та закриває вікно", "app.dropdown.close": "Закрити", "app.error.400": "Поганий запит", "app.error.401": "Ðеавторизований", @@ -407,42 +468,77 @@ "app.userList.guest.waitingUsers": "ÐžÑ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ñ€Ð¸Ñтувачів", "app.userList.guest.waitingUsersTitle": "ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ñ€Ð¸Ñтувачами", "app.userList.guest.optionTitle": "ПереглÑньте очікуваних кориÑтувачів", + "app.userList.guest.allowAllAuthenticated": "Дозволити вÑім аутентифікованим", + "app.userList.guest.allowAllGuests": "Дозволити вÑім гоÑÑ‚Ñм", + "app.userList.guest.allowEveryone": "Дозволити вÑім", + "app.userList.guest.denyEveryone": "Заборонити вÑім", + "app.userList.guest.pendingUsers": "{0} КориÑтувачів в очікуванні", + "app.userList.guest.pendingGuestUsers": "{0} ГоÑтей в очікуванні", + "app.userList.guest.pendingGuestAlert": "ПриєднавÑÑ Ð´Ð¾ ÑеÑÑ–Ñ— та очікує Вашого дозволу", + "app.userList.guest.rememberChoice": "Запам'Ñтати вибір", + "app.user-info.title": "Пошук в каталозі", "app.toast.breakoutRoomEnded": "ÐšÐ¾Ð½Ñ„ÐµÑ€ÐµÐ½Ñ†Ñ–Ñ Ð·Ð°ÐºÑ–Ð½Ñ‡Ð¸Ð»Ð°ÑÑ. Будь лаÑка, приєднайтеÑÑŒ знову до аудіо конференції.", "app.toast.chat.public": "Ðове Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ñƒ публічному чаті", "app.toast.chat.private": "Ðове Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ñƒ приватному чаті", "app.toast.chat.system": "СиÑтема", + "app.toast.clearedEmoji.label": "Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÐµÐ¼Ð¾Ð´Ð·Ñ– очищено", + "app.toast.setEmoji.label": "Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÐµÐ¼Ð¾Ð´Ð·Ñ– вÑтановлено: {0}", + "app.toast.meetingMuteOn.label": "Ð’Ñім кориÑтувачам виключено мікрофони", + "app.toast.meetingMuteOff.label": "Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ñ„Ð¾Ð½Ñƒ виключено", "app.notification.recordingStart": "Цей ÑÐµÐ°Ð½Ñ Ð½Ð°Ñ€Ð°Ð·Ñ– запиÑуєтьÑÑ", + "app.notification.recordingStop": "Цей ÑÐµÐ°Ð½Ñ Ð½Ðµ запиÑуєтьÑÑ", "app.notification.recordingPaused": "Цей ÑÐµÐ°Ð½Ñ Ð±Ñ–Ð»ÑŒÑˆÐµ не запиÑуєтьÑÑ", "app.notification.recordingAriaLabel": "ЗапиÑано чаÑу ", - "app.shortcut-help.title": "ГарÑчі клавіши", + "app.notification.userJoinPushAlert": "{0} приєднавÑÑ Ð´Ð¾ ÑеанÑу", + "app.shortcut-help.title": "ГарÑчі клавіші", "app.shortcut-help.accessKeyNotAvailable": "Клавіші швидкого доÑтупу недоÑтупні.", "app.shortcut-help.comboLabel": "Комбо", "app.shortcut-help.functionLabel": "ФункціÑ", "app.shortcut-help.closeLabel": "Закрити", + "app.shortcut-help.closeDesc": "Закриває вікно клавіш швидкого доÑтупу", "app.shortcut-help.openOptions": "Відкриває параметри", "app.shortcut-help.toggleUserList": "Вмикає ÑпиÑок кориÑтувачів", "app.shortcut-help.toggleMute": "Вмикає / Вимикає мікрофон", - "app.shortcut-help.togglePublicChat": "Вмикає публічний чат (СпиÑок кориÑтувачів має бути відкритим)", + "app.shortcut-help.togglePublicChat": "Вмикає загальний чат (СпиÑок кориÑтувачів має бути відкритим)", "app.shortcut-help.hidePrivateChat": "Приховує приватний чат", "app.shortcut-help.closePrivateChat": "Закриває приватний чат", "app.shortcut-help.openActions": "Відкриває меню дій", "app.shortcut-help.openStatus": "Відкриває меню ÑтатуÑу", + "app.shortcut-help.togglePan": "Ðктивувати інÑтрумент Ð¿Ð°Ð½Ð¾Ñ€Ð°Ð¼ÑƒÐ²Ð°Ð½Ð½Ñ (Ведучий)", + "app.shortcut-help.nextSlideDesc": "ÐаÑтупний Ñлайд (Ведучий)", + "app.shortcut-help.previousSlideDesc": "Попередній Ñлайд (Ведучий)", "app.lock-viewers.title": "ÐžÐ±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ Ð¼Ð¾Ð¶Ð»Ð¸Ð²Ð¾Ñтей кориÑтувачів", + "app.lock-viewers.description": "Ці Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð´Ð¾Ð·Ð²Ð¾Ð»ÑÑŽÑ‚ÑŒ заборонити учаÑникам викориÑтовувати певні функції", "app.lock-viewers.featuresLable": "ФункціÑ", "app.lock-viewers.lockStatusLabel": "СтатуÑ", "app.lock-viewers.webcamLabel": "ТранÑлювати веб-камеру", "app.lock-viewers.otherViewersWebcamLabel": "Бачити веб-камери інших глÑдачів", - "app.lock-viewers.button.cancel": "Відмінити", + "app.lock-viewers.microphoneLable": "Увімкнути мікрофон", + "app.lock-viewers.PublicChatLabel": "ÐадіÑлати Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ñƒ загальному чаті", + "app.lock-viewers.PrivateChatLable": "ÐадіÑлати Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ñƒ приватному чаті", + "app.lock-viewers.notesLabel": "Редагувати Ñпільні примітки", + "app.lock-viewers.userListLabel": "ПодивитиÑÑ Ñ–Ð½ÑˆÐ¸Ñ… учаÑників в ÑпиÑку кориÑтувачів", + "app.lock-viewers.ariaTitle": "Вікно Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ñ€Ð¸Ñтувачів", + "app.lock-viewers.button.apply": "ЗаÑтоÑувати", + "app.lock-viewers.button.cancel": "Відхилити", "app.lock-viewers.locked": "ОбмеженнÑ", + "app.lock-viewers.unlocked": "Розблокований", "app.recording.startTitle": "Почати запиÑ", + "app.recording.stopTitle": "ПоÑтавити Ð·Ð°Ð¿Ð¸Ñ Ð½Ð° паузу", + "app.recording.resumeTitle": "Відновити запиÑ", + "app.recording.startDescription": "Ви зможете натиÑнути пізніше ще раз кнопку запиÑу, щоб призупинити запиÑ.", + "app.recording.stopDescription": "Ви впевнені, що хочете призупинити запиÑ? Ви зможете відновити запиÑ, повторно натиÑнувши кнопку запиÑу.", "app.videoPreview.cameraLabel": "Камера", + "app.videoPreview.profileLabel": "ЯкіÑÑ‚ÑŒ", "app.videoPreview.cancelLabel": "Відмінити", "app.videoPreview.closeLabel": "Закрити", + "app.videoPreview.findingWebcamsLabel": "Пошук веб-камер", "app.videoPreview.startSharingLabel": "Почати транÑлÑцію", "app.videoPreview.webcamOptionLabel": "Виберіть веб-камеру", "app.videoPreview.webcamPreviewLabel": "Попередній переглÑд веб-камери", "app.videoPreview.webcamSettingsTitle": "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð²ÐµÐ±-камери", "app.videoPreview.webcamNotFoundLabel": "Веб-камеру не знайдено", + "app.videoPreview.profileNotFoundLabel": "Ðе знайдено підтримуваних веб-камер", "app.video.joinVideo": "ТранÑлювати веб-камеру", "app.video.leaveVideo": "Припинити транÑлювати веб-камеру", "app.video.iceCandidateError": "Помилка Ð´Ð¾Ð´Ð°Ð²Ð°Ð½Ð½Ñ ICE кандидату", @@ -454,9 +550,13 @@ "app.video.notSupportedError": "Можна транÑлювати веб-камеру лише з безпечних джерел, переконайтеÑÑ, що Ñертифікат SSL дійÑний", "app.video.notReadableError": "Ðе вдалоÑÑ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ñ‚Ð¸ відео з веб-камери. Будь лаÑка, переконайтеÑÑ, що інша програма не викориÑтовує веб-камеру", "app.video.mediaFlowTimeout1020": "Помилка 1020: медіа не можуть бути доÑтавлені на Ñервер", + "app.video.suggestWebcamLock": "ПримуÑово заблокувати веб-камери учаÑникам?", + "app.video.suggestWebcamLockReason": "(це підвищить ÑтабільніÑÑ‚ÑŒ конференції)", + "app.video.enable": "Включити", "app.video.cancel": "Відмінити", "app.video.swapCam": "Змінити", "app.video.swapCamDesc": "помінÑти напрÑмок веб-камер", + "app.video.videoLocked": "ТранÑлювати веб-камеру заблоковано", "app.video.videoButtonDesc": "ТранÑлювати веб-камеру", "app.video.videoMenu": "Меню відео", "app.video.videoMenuDisabled": "Меню відео веб-камера відключено в налаштуваннÑÑ…", @@ -476,7 +576,7 @@ "app.video.stats.rtt": "Ð§Ð°Ñ RTT", "app.video.stats.encodeUsagePercent": "ВикориÑÑ‚Ð°Ð½Ð½Ñ ÐºÐ¾Ð´ÑƒÐ²Ð°Ð½Ð½Ñм", "app.video.stats.currentDelay": "Поточна затримка", - "app.fullscreenButton.label": "Зробити {0} на веÑÑŒ екран", + "app.fullscreenButton.label": "{0} на веÑÑŒ екран", "app.deskshare.iceConnectionStateError": "Помилка 1108: при з'єднанні екрана не вдалоÑÑ Ð¿Ñ–Ð´ÐºÐ»ÑŽÑ‡Ð¸Ñ‚Ð¸ ICE", "app.sfu.mediaServerConnectionError2000": "Помилка 2000: неможливо підключитиÑÑ Ð´Ð¾ медіаÑервера", "app.sfu.mediaServerOffline2001": "Помилка 2001: медіаÑервер недоÑтупний. Будь-лаÑка Ñпробуйте пізніше.", @@ -488,6 +588,7 @@ "app.sfu.invalidSdp2202":"Помилка 2202: Клієнт Ñтворив невірний SDP", "app.sfu.noAvailableCodec2203": "Помилка 2203: Ñервер не зміг знайти відповідного кодека", "app.meeting.endNotification.ok.label": "OK", + "app.whiteboard.annotations.poll": "Результати Ð¾Ð¿Ð¸Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð±ÑƒÐ»Ð¸ опубліковані", "app.whiteboard.toolbar.tools": "ІнÑтрументи", "app.whiteboard.toolbar.tools.hand": "ПереміщеннÑ", "app.whiteboard.toolbar.tools.pencil": "Олівець", @@ -526,6 +627,8 @@ "app.videoDock.webcamFocusDesc": "СфокуÑувати вибрану веб-камеру", "app.videoDock.webcamUnfocusLabel": "РозфокуÑувати", "app.videoDock.webcamUnfocusDesc": "РозфокуÑувати вибрану веб-камеру", + "app.videoDock.autoplayBlockedDesc": "Ðам потрібен Ваш дозвіл, щоб показати Вам веб-камери інших.", + "app.videoDock.autoplayAllowLabel": "ПодивитиÑÑ Ð²ÐµÐ±-камери", "app.invitation.title": "Ð—Ð°Ð¿Ñ€Ð¾ÑˆÐµÐ½Ð½Ñ Ð² кімнату Ð´Ð»Ñ ÑƒÑ‡Ð°Ñників", "app.invitation.confirm": "ЗапроÑити", "app.createBreakoutRoom.title": "Кімнати Ð´Ð»Ñ ÑƒÑ‡Ð°Ñників", @@ -539,7 +642,9 @@ "app.createBreakoutRoom.join": "ПриєднатиÑÑŒ до кімнати", "app.createBreakoutRoom.joinAudio": "ПриєднатиÑÑ Ð´Ð¾ аудіоконференції", "app.createBreakoutRoom.returnAudio": "Повернути аудіо", + "app.createBreakoutRoom.alreadyConnected": "Вже в кімнаті", "app.createBreakoutRoom.confirm": "Створити", + "app.createBreakoutRoom.record": "ЗапиÑати", "app.createBreakoutRoom.numberOfRooms": "КількіÑÑ‚ÑŒ кімнат", "app.createBreakoutRoom.durationInMinutes": "ТриваліÑÑ‚ÑŒ (хвилини)", "app.createBreakoutRoom.randomlyAssign": "Випадково приÑвоїти", @@ -547,11 +652,30 @@ "app.createBreakoutRoom.roomName": "{0} (Кімната - {1})", "app.createBreakoutRoom.doneLabel": "Готово", "app.createBreakoutRoom.nextLabel": "Далі", + "app.createBreakoutRoom.minusRoomTime": "Зменшити триваліÑÑ‚ÑŒ до", + "app.createBreakoutRoom.addRoomTime": "Збільшити триваліÑÑ‚ÑŒ до", "app.createBreakoutRoom.addParticipantLabel": "+ Додати учаÑника", "app.createBreakoutRoom.freeJoin": "Дозволити кориÑтувачам обирати кімнату ÑамоÑтійно", "app.createBreakoutRoom.leastOneWarnBreakout": "Щонайменш один кориÑтувач повинен бути в кімнаті.", - "app.externalVideo.start": "ПоділитиÑÑŒ новим відео", - "app.externalVideo.close": "Закрити" + "app.createBreakoutRoom.modalDesc": "Замітка: Ви можете перетÑгувати імена кориÑтувачів, щоб призначити Ñ—Ñ… у певні групові кімнати.", + "app.createBreakoutRoom.roomTime": "{0} хвилин", + "app.createBreakoutRoom.numberOfRoomsError": "КількіÑÑ‚ÑŒ кімнат неправильна.", + "app.externalVideo.start": "ПоділитиÑÑ Ð½Ð¾Ð²Ð¸Ð¼ відео", + "app.externalVideo.title": "ПоділитиÑÑ Ð²Ñ–Ð´ÐµÐ¾ із зовнішніх реÑурÑів", + "app.externalVideo.input": "Ð—Ð¾Ð²Ð½Ñ–ÑˆÐ½Ñ URL-адреÑа відео", + "app.externalVideo.urlInput": "Додати URL-адреÑу відео", + "app.externalVideo.urlError": "Ð¦Ñ URL-адреÑа відео не підтримуєтьÑÑ", + "app.externalVideo.close": "Закрити", + "app.externalVideo.autoPlayWarning": "ЗапуÑÑ‚Ñ–Ñ‚ÑŒ відео, щоб активувати Ñинхронізацію медіа", + "app.network.connection.effective.slow": "СпоÑтерігаютьÑÑ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð¸ зі з'єднаннÑм", + "app.network.connection.effective.slow.help": "Детальна інформаціÑ", + "app.externalVideo.noteLabel": "Замітка: відео із зовнішніх реÑурÑів не буде відображатиÑÑ Ð² запиÑÑ–. ПідтримуютьÑÑ Ð¿Ð¾ÑÐ¸Ð»Ð°Ð½Ð½Ñ YouTube, Vimeo, Instructure Media, Twitch Ñ– Daily Motion.", + "app.actionsBar.actionsDropdown.shareExternalVideo": "ПоділитиÑÑ Ð²Ñ–Ð´ÐµÐ¾ із зовнішніх реÑурÑів", + "app.actionsBar.actionsDropdown.stopShareExternalVideo": "Припинити показ відео із зовнішніх реÑурÑів", + "app.iOSWarning.label": "Будь лаÑка, оновітьÑÑ Ð´Ð¾ iOS 12.2 або більш нової верÑÑ–Ñ—", + "app.legacy.unsupportedBrowser": "Схоже, ви викориÑтовуєте браузер, Ñкий в повному обÑÑзі не підтримуєтьÑÑ. Будь лаÑка, викориÑтовуйте {0} або {1} Ð´Ð»Ñ Ð¿Ð¾Ð²Ð½Ð¾Ñ— підтримки.", + "app.legacy.upgradeBrowser": "Схоже, ви викориÑтовуєте Ñтаршу верÑÑ–ÑŽ підтримуваного браузера. Будь лаÑка, вÑтановіть нову верÑÑ–ÑŽ Ð´Ð»Ñ Ð¿Ð¾Ð²Ð½Ð¾Ñ— підтримки.", + "app.legacy.criosBrowser": "Ðа iOS, будь лаÑка, викориÑтовуйте браузер Safari Ð´Ð»Ñ Ð¿Ð¾Ð²Ð½Ð¾Ñ— підтримки." } diff --git a/bigbluebutton-html5/private/locales/vi_VN.json b/bigbluebutton-html5/private/locales/vi_VN.json index e2c0ebc592049b11019615cc7112dd8ec52aac50..a7dd4526fea488f89b2f5dff9ae72f08e6e857c1 100644 --- a/bigbluebutton-html5/private/locales/vi_VN.json +++ b/bigbluebutton-html5/private/locales/vi_VN.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "Hoặc chá»n Ä‘Æ°á»ng dẫn/chụp ảnh cho các hình ảnh.", "app.presentationUploder.fileToUpload": "Chuẩn bị dÆ°a lên...", "app.presentationUploder.currentBadge": "Hiện tại", - "app.presentationUploder.genericError": "Ops, đã xảy ra lá»—i", "app.presentationUploder.rejectedError": "(Các) Tà i liệu không hợp lệ, xin hãy kiểm tra lại định dạng (các) tà i liệu", "app.presentationUploder.upload.progress": "Äang Ä‘Æ°a lên ({0}%)", "app.presentationUploder.upload.413": "File quá dung lượng. Vui lòng cắt nhá» thà nh nhiá»u file để giảm dung lượng.", @@ -177,7 +176,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "Tạo thumbnails", "app.presentationUploder.conversion.generatedSlides": "Các slide được tạo...", "app.presentationUploder.conversion.generatingSvg": "Tạo hình ảnh SVG ...", - "app.presentationUploder.conversion.pageCountExceeded": "Ops, Số trang hiện tại vượt quá 200 trang.", "app.presentationUploder.conversion.pdfHasBigPage": "Chúng tôi không thể chuyển thà nh PDF, vui lòng tối Æ°u hóa nó", "app.presentationUploder.conversion.timeout": "Ops, sá»± thay đổi mất má»™t khoảng thá»i gian", "app.presentationUploder.isDownloadableLabel": "Không cho phép tải xuống phần trình bà y", @@ -282,6 +280,8 @@ "app.submenu.application.animationsLabel": " Các hình ảnh Ä‘á»™ng", "app.submenu.application.audioAlertLabel": "Cảnh báo âm thanh cho cuá»™c trò chuyện", "app.submenu.application.pushAlertLabel": "Cảnh báo quảng cáo cho cuá»™c trò chuyện", + "app.submenu.application.userJoinAudioAlertLabel": "Âm thanh thông báo cho ngÆ°á»i tham gia", + "app.submenu.application.userJoinPushAlertLabel": "Popup thông báo cho ngÆ°á»i tham gia", "app.submenu.application.fontSizeControlLabel": "Cỡ chữ", "app.submenu.application.increaseFontBtnLabel": "Tăng cỡ chữ của ứng dụng", "app.submenu.application.decreaseFontBtnLabel": "Giảm cỡ chữ của ứng dụng", @@ -314,6 +314,9 @@ "app.settings.save-notification.label": "Cà i đặt đã được lÆ°u lại", "app.switch.onLabel": "Mở", "app.switch.offLabel": "Tắt", + "app.talkingIndicator.ariaMuteDesc" : "Chá»n để tắt tiếng ngÆ°á»i dùng", + "app.talkingIndicator.isTalking" : "{0} Ä‘ang nói", + "app.talkingIndicator.wasTalking" : "{0} dừng nói", "app.actionsBar.actionsDropdown.actionsLabel": "Các hà nh Ä‘á»™ng", "app.actionsBar.actionsDropdown.presentationLabel": "Tải lên phần trình bà y", "app.actionsBar.actionsDropdown.initPollLabel": "Khởi tạo cuá»™c thăm dò ý kiến", @@ -486,6 +489,7 @@ "app.notification.recordingStop": "Phiên hoạt Ä‘á»™ng nà y Ä‘ang không được ghi lại", "app.notification.recordingPaused": "Buổi há»c nà y không được ghi hình lại", "app.notification.recordingAriaLabel": "Thá»i gian ghi hình", + "app.notification.userJoinPushAlert": "{0} đã tham gia và o", "app.shortcut-help.title": "Các phÃm tắt bà n phÃm ", "app.shortcut-help.accessKeyNotAvailable": "Khóa truy cáºp không có sẵn", "app.shortcut-help.comboLabel": "Kết hợp", @@ -615,8 +619,8 @@ "app.whiteboard.toolbar.multiUserOff": "Tắt bảng trắng cho nhiá»u ngÆ°á»i dùng", "app.whiteboard.toolbar.fontSize": "Danh sách kÃch thÆ°á»›c phông chữ", "app.feedback.title": "Bạn vừa đăng xuất khá»i cuá»™c há»™i nghị", - "app.feedback.subtitle": "Chúng tôi muốn nghe vá» trải nghiệm của bạn vá» hệ thống nà y (tùy chá»n)", - "app.feedback.textarea": "Là m thế nà o để chúng tối tạo BigBlueButton tốt hÆ¡n ?", + "app.feedback.subtitle": "Chúng tôi muốn nghe trải nghiệm của bạn vá» hệ thống nà y (tùy chá»n)", + "app.feedback.textarea": "Là m thế nà o để hệ thống tốt hÆ¡n?", "app.feedback.sendFeedback": "Gá»i phản hồi", "app.feedback.sendFeedbackDesc": "Gá»i phản hồi và rá»i cuá»™c há»p", "app.videoDock.webcamFocusLabel": "Táºp trung", diff --git a/bigbluebutton-html5/private/locales/zh_CN.json b/bigbluebutton-html5/private/locales/zh_CN.json index 988aae6a2eca7105d90734ec2c719a3d4d8994bf..b6f2b73967fa06824790bd1805d8236c604408df 100644 --- a/bigbluebutton-html5/private/locales/zh_CN.json +++ b/bigbluebutton-html5/private/locales/zh_CN.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "或æµè§ˆ/æ•èŽ·å›¾ç‰‡", "app.presentationUploder.fileToUpload": "ç‰å¾…ä¸Šä¼ ", "app.presentationUploder.currentBadge": "当å‰", - "app.presentationUploder.genericError": "哎哟,出错了", "app.presentationUploder.rejectedError": "选定的文件(s)已被拒ç»ã€‚请检查文件类型(s)。", "app.presentationUploder.upload.progress": "ä¸Šä¼ ä¸({0}%)", "app.presentationUploder.upload.413": "文件太大,请分æˆå¤šä¸ªæ–‡ä»¶ã€‚", @@ -177,9 +176,12 @@ "app.presentationUploder.conversion.generatingThumbnail": "æ£åœ¨ç”Ÿæˆç¼©ç•¥å›¾...", "app.presentationUploder.conversion.generatedSlides": "å¹»ç¯ç‰‡å·²ç”Ÿæˆ...", "app.presentationUploder.conversion.generatingSvg": "æ£åœ¨ç”ŸæˆSVG图片...", - "app.presentationUploder.conversion.pageCountExceeded": "哎呀,页数超过了200页的é™åˆ¶", + "app.presentationUploder.conversion.pageCountExceeded": "超过页数。请拆分文件为多个文件。", + "app.presentationUploder.conversion.officeDocConversionInvalid": "æ— æ³•å¤„ç†office文档。请改为上载PDF。", + "app.presentationUploder.conversion.officeDocConversionFailed": "æ— æ³•å¤„ç†office文档。请改为上载PDF。", "app.presentationUploder.conversion.pdfHasBigPage": "æ— æ³•è½¬æ¢PDF文件,请å°è¯•ä¼˜åŒ–它", "app.presentationUploder.conversion.timeout": "ç³Ÿç³•ï¼Œæ ¼å¼è½¬æ¢è€—费了太长时间", + "app.presentationUploder.conversion.pageCountFailed": "æ— æ³•ç¡®å®šé¡µæ•°ã€‚", "app.presentationUploder.isDownloadableLabel": "ä¸å…许演示文件被下载", "app.presentationUploder.isNotDownloadableLabel": "å…许演示文件被下载", "app.presentationUploder.removePresentationLabel": "åˆ é™¤æ¼”ç¤ºæ–‡ä»¶", diff --git a/bigbluebutton-html5/private/locales/zh_TW.json b/bigbluebutton-html5/private/locales/zh_TW.json index 4a621bb96f61a2d7613bcb844e17fcdd419ce0fb..c14c16ee7a1265d1bddcd78f720868692a356b07 100644 --- a/bigbluebutton-html5/private/locales/zh_TW.json +++ b/bigbluebutton-html5/private/locales/zh_TW.json @@ -168,7 +168,6 @@ "app.presentationUploder.browseImagesLabel": "或ç€è¦½æˆ–æ•æ‰åœ–片", "app.presentationUploder.fileToUpload": "ç‰å¾…上傳 ...", "app.presentationUploder.currentBadge": "ç›®å‰", - "app.presentationUploder.genericError": "哎呀,有å•é¡Œ", "app.presentationUploder.rejectedError": "所é¸æª”案(複數檔)已被退回,請檢查(其它)æª”æ¡ˆæ ¼å¼ã€‚", "app.presentationUploder.upload.progress": "ä¸Šå‚³ä¸ ({0}%)", "app.presentationUploder.upload.413": "檔案太大了,請分æˆå¤šå€‹æª”案。", @@ -177,7 +176,6 @@ "app.presentationUploder.conversion.generatingThumbnail": "æ£åœ¨ç”¢ç”Ÿç¸®åœ– ...", "app.presentationUploder.conversion.generatedSlides": "投影片已產生 ...", "app.presentationUploder.conversion.generatingSvg": "æ£åœ¨ç”¢ç”ŸSVG圖片 ...", - "app.presentationUploder.conversion.pageCountExceeded": "哎呀,é é¢å·²é”200é é™åˆ¶ã€‚", "app.presentationUploder.conversion.pdfHasBigPage": "我們沒法轉æ›é€™å€‹PDF檔,請åšè©¦å„ªåŒ–他。", "app.presentationUploder.conversion.timeout": "哎呀,轉移時間花太久了。", "app.presentationUploder.isDownloadableLabel": "ä¸å…è¨±ç°¡å ±è¢«ä¸‹è¼‰", diff --git a/bigbluebutton-html5/public/compatibility/kurento-extension.js b/bigbluebutton-html5/public/compatibility/kurento-extension.js index 1b4efc5c58cdd391483b47e6416bdf34513888d9..49c0154887279ea4f04c1cdbd9f701aa6903f5b5 100755 --- a/bigbluebutton-html5/public/compatibility/kurento-extension.js +++ b/bigbluebutton-html5/public/compatibility/kurento-extension.js @@ -158,6 +158,10 @@ KurentoManager.prototype.exitAudio = function () { 'Exiting listen only'); } + if (this.kurentoAudio.webRtcPeer) { + this.kurentoAudio.webRtcPeer.peerConnection.oniceconnectionstatechange = null; + } + if (this.kurentoAudio.ws !== null) { this.kurentoAudio.ws.onclose = function () {}; this.kurentoAudio.ws.close(); @@ -607,7 +611,6 @@ Kurento.prototype.setAudio = function (tag) { }; Kurento.prototype.listenOnly = function () { - const self = this; if (!this.webRtcPeer) { const options = { onicecandidate : this.onListenOnlyIceCandidate.bind(this), @@ -619,12 +622,31 @@ Kurento.prototype.listenOnly = function () { this.addIceServers(this.iceServers, options); - self.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, function (error) { + this.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, (error) => { if (error) { - return self.onFail(error); + return this.onFail(error); } - self.webRtcPeer.iceQueue = []; - this.generateOffer(self.onOfferListenOnly.bind(self)); + + this.webRtcPeer.iceQueue = []; + this.webRtcPeer.peerConnection.oniceconnectionstatechange = () => { + if (this.webRtcPeer) { + const iceConnectionState = this.webRtcPeer.peerConnection.iceConnectionState; + + if (iceConnectionState === 'failed' || iceConnectionState === 'closed') { + this.webRtcPeer.peerConnection.oniceconnectionstatechange = null; + this.logger.error({ + logCode: 'kurentoextension_listenonly_ice_failed', + extraInfo: { iceConnectionState } + }, `WebRTC peer for listen only failed due to ICE transitioning to ${iceConnectionState}`); + this.onFail({ + errorCode: 1007, + errorMessage: `ICE negotiation failed. Current state - ${iceConnectionState}`, + }); + } + } + } + + this.webRtcPeer.generateOffer(this.onOfferListenOnly.bind(this)); }); } }; 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-html5/server/main.js b/bigbluebutton-html5/server/main.js index fbf7cf1edf5e1aee8d58c7e06c9e56d031ca192e..17a83e32ce0dfbfa66382827f64b81cb64ad03cc 100755 --- a/bigbluebutton-html5/server/main.js +++ b/bigbluebutton-html5/server/main.js @@ -26,9 +26,20 @@ import '/imports/api/external-videos/server'; import '/imports/api/guest-users/server'; import '/imports/api/ping-pong/server'; import '/imports/api/local-settings/server'; - +import '/imports/api/voice-call-states/server'; // Commons import '/imports/api/log-client/server'; import '/imports/api/common/server/helpers'; import '/imports/startup/server/logger'; + +// Needed for Atmosphere package RocketChat/meteor-streamer +// It is out of date and was written when Meteor contained lodash +// package. However, we now import lodash as an npm package +// in order to control versions, update flexibly, etc.. +// Setting the global._ to utilize the npm lodash package is an interim fix +// and its introduction was inspired by +// https://github.com/RocketChat/meteor-streamer/issues/40#issuecomment-497627893 +import _ from 'lodash'; + +global._ = _; diff --git a/bigbluebutton-html5/tests/puppeteer/.gitignore b/bigbluebutton-html5/tests/puppeteer/.gitignore index ff306fbfbb436fce37c8a62d0fc92d8c38c72f04..a7b53d8b6233012c7691f78b5b8bbd39355deef1 100644 --- a/bigbluebutton-html5/tests/puppeteer/.gitignore +++ b/bigbluebutton-html5/tests/puppeteer/.gitignore @@ -5,3 +5,4 @@ downloads/* !downloads/downloads.txt .directory .env +media/* \ No newline at end of file diff --git a/bigbluebutton-html5/tests/puppeteer/audio.test.js b/bigbluebutton-html5/tests/puppeteer/audio.test.js new file mode 100644 index 0000000000000000000000000000000000000000..1ccdd1b3a08bc051f2ade55f69ca4201b37e8e45 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/audio.test.js @@ -0,0 +1,31 @@ +const Audio = require('./audio/audio'); + +describe('Audio', () => { + test('Join audio', async () => { + const test = new Audio(); + let response; + try { + await test.init(); + response = await test.test(); + } catch (e) { + console.log(e); + } finally { + await test.close(); + } + expect(response).toBe(true); + }); + + test('Mute the other User', async () => { + const test = new Audio(); + let response; + try { + await test.init(); + response = await test.mute(); + } catch (e) { + console.log(e); + } finally { + await test.close(); + } + expect(response).toBe(true); + }); +}); diff --git a/bigbluebutton-html5/tests/puppeteer/audio/audio.js b/bigbluebutton-html5/tests/puppeteer/audio/audio.js new file mode 100644 index 0000000000000000000000000000000000000000..4aa33590c3395ef628c6b775848215d5bd00a5e8 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/audio/audio.js @@ -0,0 +1,52 @@ +const util = require('./util'); +const Page = require('../core/page'); +const params = require('../params'); +const pe = require('../core/elements'); + +class Audio { + constructor() { + this.page1 = new Page(); + this.page2 = new Page(); + } + + // Join BigBlueButton meeting + async init(meetingId) { + await this.page1.init(Page.getArgsWithAudio(), meetingId, { ...params, fullName: 'BroadCaster1' }); + await this.page2.init(Page.getArgsWithAudio(), this.page1.meetingId, { ...params, fullName: 'BroadCaster2' }); + await this.page1.joinMicrophone(); + await this.page2.joinMicrophone(); + } + + async test() { + // User1 is checking if User2 is talking + const isTalkingIndicatorUser1 = await util.checkUserIsTalkingIndicator(this.page1); + + // User2 is checking if User1 is talking + const isTalkingIndicatorUser2 = await util.checkUserIsTalkingIndicator(this.page2); + const doneCheckingIsTalkingIndicator = isTalkingIndicatorUser1 && isTalkingIndicatorUser2; + const response = doneCheckingIsTalkingIndicator == true; + return response; + } + + async mute() { + // User1 mutes User2 & User2 mutes User1 + await util.mute(this.page1, this.page2); + + // User1 checks if he still can see User2 highlighting + const wasTalkingIndicatorUser1 = await util.checkUserWasTalkingIndicator(this.page1); + + // User2 checks if he still can see User1 highlighting + const wasTalkingIndicatorUser2 = await util.checkUserWasTalkingIndicator(this.page2); + + const doneCheckingIsTalkingIndicator = wasTalkingIndicatorUser1 && wasTalkingIndicatorUser2; + const response = doneCheckingIsTalkingIndicator == true; + return response; + } + + async close() { + this.page1.close(); + this.page2.close(); + } +} + +module.exports = exports = Audio; diff --git a/bigbluebutton-html5/tests/puppeteer/audio/util.js b/bigbluebutton-html5/tests/puppeteer/audio/util.js new file mode 100644 index 0000000000000000000000000000000000000000..92dacac364291d2b42c027edfc1723b3ce087e47 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/audio/util.js @@ -0,0 +1,41 @@ +const pe = require('../core/elements'); +const ule = require('../user/elements'); + +async function checkUserAvatarIfHighlighting(test) { + await test.waitForSelector(ule.statusIcon); + await test.waitForSelector('[class^="talking--"]'); + const response = await test.page.evaluate(async () => await document.querySelectorAll('[data-test="userAvatar"]')[1].querySelectorAll('[class^="talking--"]') !== null); + return response; +} + +async function checkUserIsTalkingIndicator(test) { + const response = await test.page.evaluate(getTestElement, pe.isTalking) !== null; + return response; +} + +async function checkUserWasTalkingIndicator(test) { + const response = await test.page.evaluate(getTestElement, pe.wasTalking) !== null; + return response; +} + +async function getTestElement(element) { + await document.querySelectorAll(element)[1]; +} + +async function clickTestElement(element) { + await document.querySelectorAll(element)[0].click(); +} + +async function mute(test) { + await test.page.evaluate(async () => { + await document.querySelectorAll('[data-test="userListItem"]')[0].click(); + await document.querySelectorAll('[data-test="mute"]')[0].click(); + }); +} + +exports.mute = mute; +exports.clickTestElement = clickTestElement; +exports.getTestElement = getTestElement; +exports.checkUserAvatarIfHighlighting = checkUserAvatarIfHighlighting; +exports.checkUserIsTalkingIndicator = checkUserIsTalkingIndicator; +exports.checkUserWasTalkingIndicator = checkUserWasTalkingIndicator; diff --git a/bigbluebutton-html5/tests/puppeteer/chat/copy.js b/bigbluebutton-html5/tests/puppeteer/chat/copy.js index 467c53ec6a34df5dba98d0673c934db9d187169d..849b6030c79d1d3ac47e0b94c51fc7bf06a69ff2 100644 --- a/bigbluebutton-html5/tests/puppeteer/chat/copy.js +++ b/bigbluebutton-html5/tests/puppeteer/chat/copy.js @@ -22,9 +22,8 @@ class Copy extends Page { await this.click(e.chatCopy, true); const copiedChat = clipboardy.readSync(); - expect(copiedChat).toEqual(expect.stringContaining(`User1 : ${e.message}`)); - return true; + return copiedChat.includes(`User1 : ${e.message}`); } } diff --git a/bigbluebutton-html5/tests/puppeteer/chat/save.js b/bigbluebutton-html5/tests/puppeteer/chat/save.js index e268b40838ebad2f3cf786c646ff4855d1ea0ddf..b57aca9333f759d22ce23881c4bf64014d5c3e85 100644 --- a/bigbluebutton-html5/tests/puppeteer/chat/save.js +++ b/bigbluebutton-html5/tests/puppeteer/chat/save.js @@ -14,15 +14,9 @@ class Save extends Page { await this.click(e.chatOptions); await this.click(e.chatSave, true); - - // TODO: Replace this with a download event listener - await this.screenshot(true); - await this.screenshot(true); - await this.screenshot(true); - - // TODO: Check test - return true; + const clicked = await this.page.addListener('click', () => document.addEventListener('click')); + return clicked; } } -module.exports = exports = Save; +module.exports = exports = Save; \ No newline at end of file diff --git a/bigbluebutton-html5/tests/puppeteer/core/elements.js b/bigbluebutton-html5/tests/puppeteer/core/elements.js index 167a5e543b386d0cde4d538797698171bef536b8..b040acec230ebe741f9564aeb3a7b5a7c3d7d66b 100644 --- a/bigbluebutton-html5/tests/puppeteer/core/elements.js +++ b/bigbluebutton-html5/tests/puppeteer/core/elements.js @@ -1,4 +1,4 @@ -exports.audioDialog = '.ReactModal__Content[aria-label="Join audio modal"]'; +exports.audioDialog = '[aria-label="Join audio modal"]'; exports.closeAudio = 'button[aria-label="Close Join audio modal"]'; exports.microphoneButton = 'button[aria-label="Microphone"]'; exports.listenButton = 'button[aria-label="Listen Only"]'; @@ -6,6 +6,12 @@ exports.echoYes = 'button[aria-label="Echo is audible"]'; exports.title = '._imports_ui_components_nav_bar__styles__presentationTitle'; exports.alerts = '.toastify-content'; + +exports.isTalking = '[data-test="isTalking"]'; +exports.wasTalking = '[data-test="wasTalking"]'; +exports.joinAudio = 'button[aria-label="Join Audio"]'; +exports.leaveAudio = 'button[aria-label="Leave Audio"]'; + exports.actions = 'button[aria-label="Actions"]'; exports.options = 'button[aria-label="Options"]'; exports.userList = 'button[aria-label="Users and Messages Toggle"]'; @@ -13,3 +19,4 @@ exports.joinAudio = 'button[aria-label="Join Audio"]'; exports.leaveAudio = 'button[aria-label="Leave Audio"]'; exports.videoMenu = 'button[aria-label="Open video menu dropdown"]'; exports.screenShare = 'button[aria-label="Share your screen"]'; +exports.screenShareVideo = '[id="screenshareVideo"]'; diff --git a/bigbluebutton-html5/tests/puppeteer/core/page.js b/bigbluebutton-html5/tests/puppeteer/core/page.js index 01829faa37844bf31eb7c88bed4ec06f7dda7e15..a55aafb584db1f0173f572e8b8e2943a5dff083a 100644 --- a/bigbluebutton-html5/tests/puppeteer/core/page.js +++ b/bigbluebutton-html5/tests/puppeteer/core/page.js @@ -32,16 +32,38 @@ class Page { const joinURL = helper.getJoinURL(this.meetingId, this.effectiveParams, isModerator); await this.page.goto(joinURL); - await this.waitForSelector(e.audioDialog); - await this.click(e.closeAudio, true); const checkForGetMetrics = async () => { if (process.env.BBB_COLLECT_METRICS === 'true') { await this.getMetrics(); } }; + if (process.env.IS_AUDIO_TEST !== 'true') { + await this.closeAudioModal(); + } await checkForGetMetrics(); } + // Joining audio with microphone + async joinMicrophone() { + await this.waitForSelector(e.audioDialog); + await this.waitForSelector(e.microphoneButton); + await this.click(e.microphoneButton, true); + await this.waitForSelector(e.echoYes); + await this.click(e.echoYes, true); + } + + // Joining audio with Listen Only mode + async listenOnly() { + await this.waitForSelector(e.audioDialog); + await this.waitForSelector(e.listenButton); + await this.click(e.listenButton); + } + + async closeAudioModal() { + await this.waitForSelector(e.audioDialog); + await this.click(e.closeAudio, true); + } + async setDownloadBehavior(downloadPath) { const downloadBehavior = { behavior: 'allow', downloadPath }; await this.page._client.send('Page.setDownloadBehavior', downloadBehavior); @@ -65,6 +87,32 @@ class Page { return { headless: false, args: ['--no-sandbox', '--use-fake-ui-for-media-stream'] }; } + static getArgsWithAudio() { + return { + headless: false, + args: [ + '--no-sandbox', + '--use-fake-ui-for-media-stream', + '--use-fake-device-for-media-stream', + `--use-file-for-fake-audio-capture=${process.env.AUDIO_FILE}`, + '--allow-file-access', + ], + }; + } + + static getArgsWithVideo() { + return { + headless: false, + args: [ + '--no-sandbox', + '--use-fake-ui-for-media-stream', + '--use-fake-device-for-media-stream', + `--use-file-for-fake-video-capture=${process.env.VIDEO_FILE}`, + '--allow-file-access', + ], + }; + } + // Returns a Promise that resolves when an element does not exist/is removed from the DOM elementRemoved(element) { return this.page.waitFor(element => !document.querySelector(element), {}, element); diff --git a/bigbluebutton-html5/tests/puppeteer/notifications.test.js b/bigbluebutton-html5/tests/puppeteer/notifications.test.js new file mode 100644 index 0000000000000000000000000000000000000000..2f2300710f79e013a2060cf0d2217d8f9f1a0369 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/notifications.test.js @@ -0,0 +1,47 @@ +const Page = require('./core/page'); +const MultiUsers = require('./user/multiusers'); +const Notifications = require('./notifications/notifications'); + +describe('Notifications', () => { + test('Save settings notification', async () => { + const test = new Notifications(); + let response; + try { + await test.init(); + response = await test.saveSttingsNotification(); + } catch (e) { + console.log(e); + } finally { + await test.close(); + } + expect(response).toBe(true); + }); + + test('Public Chat notification', async () => { + const test = new Notifications(); + let response; + try { + await test.init(); + response = await test.publicChatNotification(); + } catch (e) { + console.log(e); + } finally { + await test.close(); + } + expect(response).toBe(true); + }); + + test('Private Chat notification', async () => { + const test = new Notifications(); + let response; + try { + await test.init(); + response = await test.privateChatNotification(); + } catch (e) { + console.log(e); + } finally { + await test.close(); + } + expect(response).toBe(true); + }); +}); diff --git a/bigbluebutton-html5/tests/puppeteer/notifications/elements.js b/bigbluebutton-html5/tests/puppeteer/notifications/elements.js new file mode 100644 index 0000000000000000000000000000000000000000..de9c263b19ac2b29780bc530a9e69885a7184ac2 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/notifications/elements.js @@ -0,0 +1,11 @@ +exports.settings = 'li[data-test="settings"]'; +exports.settingsModal = 'div[aria-label="Settings"]'; +exports.chatPushAlerts = '[data-test="chatPushAlerts"]'; +exports.smallToastMsg = 'div[data-test="toastSmallMsg"]'; +exports.saveSettings = '[data-test="modalConfirmButton"]'; + +exports.savedSettingsToast = 'Settings have been saved'; +exports.publicChatToast = 'New Public Chat message'; +exports.privateChatToast = 'New Private Chat message'; +exports.userListNotifiedIcon = '[class^=btnWithNotificationDot]'; +exports.hasUnreadMessages = 'button[data-test="hasUnreadMessages"]'; diff --git a/bigbluebutton-html5/tests/puppeteer/notifications/notifications.js b/bigbluebutton-html5/tests/puppeteer/notifications/notifications.js new file mode 100644 index 0000000000000000000000000000000000000000..7eec15371ed8f3fcd7149127b99794c4906458ad --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/notifications/notifications.js @@ -0,0 +1,52 @@ +const Page = require('../core/page'); +const util = require('./util'); +const utilMultiUsers = require('../chat/util'); +const MultiUsers = require('../user/multiusers'); +const params = require('../params'); +const ne = require('./elements'); +const e = require('../chat/elements'); + +class Notifications extends MultiUsers { + constructor() { + super('notifications'); + this.page1 = new Page(); + this.page2 = new Page(); + } + + async init(meetingId) { + await this.page1.init(Page.getArgs(), meetingId, { ...params }); + await this.page2.init(Page.getArgs(), this.page1.meetingId, { ...params, fullName: 'User2' }); + } + + async saveSttingsNotification() { + await util.popupMenu(this.page1); + await util.saveSettings(this.page1); + const resp = await util.getLastToastValue(this.page1) === ne.savedSettingsToast; + return resp; + } + + async publicChatNotification() { + await util.popupMenu(this.page1); + await util.enableChatPopup(this.page1); + await util.saveSettings(this.page1); + const expectedToastValue = await util.publicChatMessageToast(this.page1, this.page2); + await this.page1.waitForSelector(ne.smallToastMsg); + await this.page1.waitForSelector(ne.hasUnreadMessages); + const lastToast = await util.getOtherToastValue(this.page1); + return expectedToastValue === lastToast; + } + + + async privateChatNotification() { + await util.popupMenu(this.page1); + await util.enableChatPopup(this.page1); + await util.saveSettings(this.page1); + const expectedToastValue = await util.privateChatMessageToast(this.page2); + await this.page1.waitForSelector(ne.smallToastMsg); + await this.page1.waitForSelector(ne.hasUnreadMessages); + const lastToast = await util.getOtherToastValue(this.page1); + return expectedToastValue === lastToast; + } +} + +module.exports = exports = Notifications; diff --git a/bigbluebutton-html5/tests/puppeteer/notifications/util.js b/bigbluebutton-html5/tests/puppeteer/notifications/util.js new file mode 100644 index 0000000000000000000000000000000000000000..eaf7b430c8765a617fd097188a93020f7a84467b --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/notifications/util.js @@ -0,0 +1,88 @@ +const e = require('../core/elements'); +const ne = require('../notifications/elements'); +const ce = require('../chat/elements'); +const ule = require('../user/elements'); + +async function popupMenu(page1) { + await page1.waitForSelector(e.options); + await page1.click(e.options, true); + await page1.waitForSelector(ne.settings); + await page1.click(ne.settings, true); +} + +async function enableChatPopup(test) { + await test.waitForSelector(ne.chatPushAlerts); + await test.page.evaluate(() => document.querySelector('[data-test="chatPushAlerts"]').children[0].click()); +} + +async function saveSettings(page1) { + await page1.waitForSelector(ne.saveSettings); + await page1.click(ne.saveSettings, true); +} + +async function waitForToast(test) { + await test.waitForSelector(ne.smallToastMsg); + const resp = await test.page.evaluate(getTestElement, ne.smallToastMsg) !== null; + return resp; +} + +async function getLastToastValue(test) { + await test.waitForSelector(ne.smallToastMsg); + const toast = test.page.evaluate(async () => { + const lastToast = await document.querySelectorAll('[data-test="toastSmallMsg"]')[0].innerText; + return lastToast; + }); + return toast; +} + +async function getOtherToastValue(test) { + await test.waitForSelector(ne.smallToastMsg); + const toast = test.page.evaluate(async () => { + const lastToast = await document.querySelectorAll('[data-test="toastSmallMsg"]')[1]; + return lastToast.innerText; + }); + return toast; +} + +async function getTestElement(element) { + await document.querySelectorAll(element)[1]; +} + + +async function clickOnTheOtherUser(element) { + await document.querySelectorAll(element)[0].click(); +} + +async function clickThePrivateChatButton(element) { + await document.querySelectorAll(element)[0].click(); +} + +async function publicChatMessageToast(page1, page2) { + // Open private Chat with the other User + await page1.page.evaluate(clickOnTheOtherUser, ule.userListItem); + await page1.page.evaluate(clickThePrivateChatButton, ce.activeChat); + // send a public message + await page2.page.type(ce.publicChat, ce.publicMessage1); + await page2.page.click(ce.sendButton, true); + return ne.publicChatToast; +} + +async function privateChatMessageToast(page2) { + // Open private Chat with the other User + await page2.page.evaluate(clickOnTheOtherUser, ule.userListItem); + await page2.page.evaluate(clickThePrivateChatButton, ce.activeChat); + // send a private message + await page2.page.type(ce.privateChat, ce.message1); + await page2.page.click(ce.sendButton, true); + return ne.privateChatToast; +} + +exports.privateChatMessageToast = privateChatMessageToast; +exports.publicChatMessageToast = publicChatMessageToast; +exports.getOtherToastValue = getOtherToastValue; +exports.getLastToastValue = getLastToastValue; +exports.enableChatPopup = enableChatPopup; +exports.getTestElement = getTestElement; +exports.saveSettings = saveSettings; +exports.waitForToast = waitForToast; +exports.popupMenu = popupMenu; diff --git a/bigbluebutton-html5/tests/puppeteer/presentation/slide.js b/bigbluebutton-html5/tests/puppeteer/presentation/slide.js index 1d6ebd9824d25d139a9fe583e489f1e3a9c68d07..3480ec09ee23840a2d5e4d7f34a8bf9c9cbf33b2 100644 --- a/bigbluebutton-html5/tests/puppeteer/presentation/slide.js +++ b/bigbluebutton-html5/tests/puppeteer/presentation/slide.js @@ -31,8 +31,7 @@ class Slide extends Page { console.log('\nAfter previous slide:'); console.log(svg2); - // TODO: Check test - return true; + return svg0 !== svg1 && svg1 !== svg2; } async getTestElements() { diff --git a/bigbluebutton-html5/tests/puppeteer/presentation/upload.js b/bigbluebutton-html5/tests/puppeteer/presentation/upload.js index 93cfc7629dea0d8c7377dc2830ff908770487f38..b77237e6a8d0181932a1b296bd7ee966629c060c 100644 --- a/bigbluebutton-html5/tests/puppeteer/presentation/upload.js +++ b/bigbluebutton-html5/tests/puppeteer/presentation/upload.js @@ -37,8 +37,7 @@ class Upload extends Page { console.log(slides1.slideList); console.log(slides1.svg); - // TODO: Check test - return true; + return slides0.svg !== slides1.svg; } async getTestElements() { diff --git a/bigbluebutton-html5/tests/puppeteer/screenshare.test.js b/bigbluebutton-html5/tests/puppeteer/screenshare.test.js new file mode 100644 index 0000000000000000000000000000000000000000..6f27a13eab1a287f02d8f2c27b80491349e6c631 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/screenshare.test.js @@ -0,0 +1,18 @@ +const ShareScreen = require('./screenshare/screenshare'); +const Page = require('./core/page'); + +describe('Screen Share', () => { + test('Share screen', async () => { + const test = new ShareScreen(); + let response; + try { + await test.init(Page.getArgsWithVideo()); + response = await test.test(); + } catch (e) { + console.log(e); + } finally { + await test.close(); + } + expect(response).toBe(true); + }); +}); diff --git a/bigbluebutton-html5/tests/puppeteer/screenshare/screenshare.js b/bigbluebutton-html5/tests/puppeteer/screenshare/screenshare.js new file mode 100644 index 0000000000000000000000000000000000000000..80a09df9d61921e106a45d8bf4fbf51304b6e9ce --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/screenshare/screenshare.js @@ -0,0 +1,21 @@ +const Page = require('../core/page'); +const util = require('./util'); +const e = require('../core/elements'); + +class ShareScreen extends Page { + constructor() { + super('share-screen'); + } + + async test() { + await util.startScreenshare(this.page); + this.page.on('dialog', async (dialog) => { + await dialog.accept(); + }); + await this.page.waitForSelector(e.screenShareVideo); + const response = await util.getScreenShareContainer(this.page); + return response; + } +} + +module.exports = exports = ShareScreen; diff --git a/bigbluebutton-html5/tests/puppeteer/screenshare/util.js b/bigbluebutton-html5/tests/puppeteer/screenshare/util.js new file mode 100644 index 0000000000000000000000000000000000000000..e9a1935b561a2be09eee8d54633db56e0bdbf972 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/screenshare/util.js @@ -0,0 +1,21 @@ +const e = require('../core/elements'); + +async function startScreenshare(test) { + await test.waitForSelector(e.screenShare); + await test.click(e.screenShare, true); +} + +async function getTestElement(element) { + (await document.querySelectorAll(element)[0]) !== null; +} + +async function getScreenShareContainer(test) { + await test.waitForSelector(e.screenShareVideo); + const screenShareContainer = await test.evaluate(getTestElement, e.screenshareVideo); + const response = screenShareContainer !== null; + return response; +} + +exports.getScreenShareContainer = getScreenShareContainer; +exports.getTestElement = getTestElement; +exports.startScreenshare = startScreenshare; diff --git a/bigbluebutton-html5/tests/puppeteer/user/status.js b/bigbluebutton-html5/tests/puppeteer/user/status.js index e73891fa90121f7f0136f127a5f062d401b1521f..fa1ee6358ab7f4f7c3baa8e2b0148a7431dd4fe1 100644 --- a/bigbluebutton-html5/tests/puppeteer/user/status.js +++ b/bigbluebutton-html5/tests/puppeteer/user/status.js @@ -30,8 +30,8 @@ class Status extends Page { await this.screenshot(true); const status3 = await util.getTestElements(this); - // TODO: Check test - return true; + // status0 and status3 are equal as initial and last status + return status0 !== status1 && status1 !== status2 && status2 !== status3 && status2 !== status0 && status3 !== status1; } } diff --git a/bigbluebutton-html5/tests/puppeteer/webcam.test.js b/bigbluebutton-html5/tests/puppeteer/webcam.test.js new file mode 100644 index 0000000000000000000000000000000000000000..4aaa37f242d7e828a444eeed6dcb7b34e985a3f3 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/webcam.test.js @@ -0,0 +1,47 @@ +const Share = require('./webcam/share'); +const Check = require('./webcam/check'); +const LoadingTime = require('./webcam/loadtime'); + +describe('Webcam', () => { + test('Shares webcam', async () => { + const test = new Share(); + let response; + try { + await test.init(); + response = await test.test(); + } catch (e) { + console.log(e); + } finally { + await test.close(); + } + expect(response).toBe(true); + }); + + test('Check Webcam loading time', async () => { + const test = new LoadingTime(); + let response; + try { + await test.init(); + response = await test.test(); + } catch (e) { + console.log(e); + } finally { + await test.close(); + } + expect(response).toBeLessThan(parseInt(process.env.CAMERA_SHARE_FAILED_WAIT_TIME)); + }); + + test('Checks content of webcam', async () => { + const test = new Check(); + let response; + try { + await test.init(); + response = await test.test(); + } catch (e) { + console.log(e); + } finally { + await test.close(); + } + expect(response).toBe(true); + }); +}); diff --git a/bigbluebutton-html5/tests/puppeteer/webcam/check.js b/bigbluebutton-html5/tests/puppeteer/webcam/check.js new file mode 100644 index 0000000000000000000000000000000000000000..61846e1cc7f0e7e3a5e954c61674ae61db9800a5 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/webcam/check.js @@ -0,0 +1,17 @@ +const Share = require('./share'); +const util = require('./util'); + +class Check extends Share { + constructor() { + super('check-webcam-content'); + } + + async test() { + await util.startAndCheckForWebcams(this.page1); + await util.startAndCheckForWebcams(this.page2); + const responseUser1 = await util.webcamContentCheck(this.page1); + const responseUser2 = await util.webcamContentCheck(this.page2); + return responseUser1 && responseUser2; + } +} +module.exports = exports = Check; diff --git a/bigbluebutton-html5/tests/puppeteer/webcam/elements.js b/bigbluebutton-html5/tests/puppeteer/webcam/elements.js new file mode 100644 index 0000000000000000000000000000000000000000..2f5ecf643175fc17914143b6ba256f8c896a8a99 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/webcam/elements.js @@ -0,0 +1,6 @@ +exports.joinVideo = 'button[data-test="joinVideo"]'; +exports.videoPreview = 'video[data-test="videoPreview"]'; +exports.startSharingWebcam = 'button[data-test="startSharingWebcam"]'; +exports.videoContainer = 'video[data-test="videoContainer"]'; +exports.webcamConnectingStatus = '[data-test="webcamConnectingStatus"]'; +exports.presentationFullscreenButton = '[data-test="presentationFullscreenButton"]'; diff --git a/bigbluebutton-html5/tests/puppeteer/webcam/loadtime.js b/bigbluebutton-html5/tests/puppeteer/webcam/loadtime.js new file mode 100644 index 0000000000000000000000000000000000000000..1bb96551245a15e06f2cdfed442112765f0f2999 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/webcam/loadtime.js @@ -0,0 +1,19 @@ +const Share = require('./share'); +const util = require('./util'); + +class LoadingTime extends Share { + constructor() { + super('check-webcam-loading-time'); + } + + async test() { + await util.enableWebcam(this.page1); + await util.enableWebcam(this.page2); + const now = new Date().getMilliseconds(); + await util.waitForWebcamsLoading(this.page1); + await util.waitForWebcamsLoading(this.page2); + const end = new Date().getMilliseconds(); + return end - now; + } +} +module.exports = exports = LoadingTime; diff --git a/bigbluebutton-html5/tests/puppeteer/webcam/share.js b/bigbluebutton-html5/tests/puppeteer/webcam/share.js new file mode 100644 index 0000000000000000000000000000000000000000..c0d02cac7d6da04617b8988ebd579fc173b69410 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/webcam/share.js @@ -0,0 +1,30 @@ +const Page = require('../core/page'); +const util = require('./util'); +const params = require('../params'); + +class Share { + constructor() { + this.page1 = new Page(); + this.page2 = new Page(); + } + + async init(meetingId) { + await this.page1.init(Page.getArgsWithVideo(), meetingId, { ...params, fullName: 'Streamer1' }); + await this.page2.init(Page.getArgsWithVideo(), this.page1.meetingId, { ...params, fullName: 'Streamer2' }); + } + + async test() { + await util.enableWebcam(this.page1); + await util.enableWebcam(this.page2); + const responseUser1 = await util.evaluateCheck(this.page1); + const responseUser2 = await util.evaluateCheck(this.page2); + return responseUser1 && responseUser2; + } + + async close() { + await this.page1.close(); + await this.page2.close(); + } +} + +module.exports = exports = Share; diff --git a/bigbluebutton-html5/tests/puppeteer/webcam/util.js b/bigbluebutton-html5/tests/puppeteer/webcam/util.js new file mode 100644 index 0000000000000000000000000000000000000000..c48e72a6010bc6e725d152a53dfe595bad71f9bd --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/webcam/util.js @@ -0,0 +1,65 @@ +const we = require('./elements'); + +async function enableWebcam(test) { + // Enabling webcam + await test.waitForSelector(we.joinVideo); + await test.click(we.joinVideo, true); + await test.waitForSelector(we.videoPreview); + await test.waitForSelector(we.startSharingWebcam); + await test.click(we.startSharingWebcam, true); +} + +async function getTestElement(element) { + (await document.querySelectorAll(element)[0]) !== null; +} + +async function evaluateCheck(test) { + await test.waitForSelector(we.videoContainer); + await test.waitForSelector(we.webcamConnectingStatus); + const videoContainer = await test.page.evaluate(getTestElement, we.presentationFullscreenButton); + const response = videoContainer !== null; + return response; +} + +async function startAndCheckForWebcams(test) { + await enableWebcam(test); + const response = await evaluateCheck(test); + return response; +} + +async function checkCameras() { + const videos = document.querySelectorAll('video'); + const lastVideoColor = document.lastVideoColor || {}; + document.lastVideoColor = lastVideoColor; + + for (let v = 0; v < videos.length; v++) { + const video = videos[v]; + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight); + const pixel = context.getImageData(50, 50, 1, 1).data; + const pixelString = new Array(pixel).join(' ').toString(); + + if (lastVideoColor[v]) { + if (lastVideoColor[v] == pixelString) { + return false; + } + } + return lastVideoColor[v] !== pixelString === true; + } +} + +async function webcamContentCheck(test) { + const repeats = 5; + let check; + for (let i = repeats; i >= 0; i--) { + await test.page.waitFor(parseInt(process.env.LOOP_INTERVAL)); + return check = await test.page.evaluate(checkCameras); + } +} + +exports.startAndCheckForWebcams = startAndCheckForWebcams; +exports.webcamContentCheck = webcamContentCheck; +exports.evaluateCheck = evaluateCheck; +exports.getTestElement = getTestElement; +exports.enableWebcam = enableWebcam; diff --git a/bigbluebutton-html5/tests/puppeteer/whiteboard/draw.js b/bigbluebutton-html5/tests/puppeteer/whiteboard/draw.js index 53609625bc222f4b2d8417cea70b40009e40a144..60c5ee7e24586fd49263a011a73bf8c57739cf0c 100644 --- a/bigbluebutton-html5/tests/puppeteer/whiteboard/draw.js +++ b/bigbluebutton-html5/tests/puppeteer/whiteboard/draw.js @@ -28,8 +28,7 @@ class Draw extends Page { console.log('\nShapes after drawing box:'); console.log(shapes1); - // TODO: Check test - return true; + return shapes0 !== shapes1; } async getTestElements() { 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 8036762c3418cef8aeec60d5934ab882c7203ae9..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 @@ -97,7 +107,7 @@ svgImagesRequired=true #---------------------------------------------------- # Additional conversion of the presentation slides to PNG # to be used in the IOS mobile client -generatePngs=true +generatePngs=false pngSlideWidth=1200 # Default number of digits for voice conference users joining through the PSTN. @@ -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)