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