fix(whiteboard): tldraw recording processing/publishing
Changed the names of tldraw record events to differentiate from before. Publish tldraw.json file with all shape information during the meeting to be used in playback. Adapted cursor.xml and panzoom.xml to store tldraw data. Publish slides svgs to be used by playback's tldraw component (otherwise we have different image sizes in pngs and thus messing the coordinates). Retro-compatible with old recordings.
This commit is contained in:
parent
75efa60f7d
commit
853d0dfd9b
@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||||
|
*
|
||||||
|
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the
|
||||||
|
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||||
|
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||||
|
* version.
|
||||||
|
*
|
||||||
|
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||||
|
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License along
|
||||||
|
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.bigbluebutton.core.record.events
|
||||||
|
|
||||||
|
import org.bigbluebutton.common2.domain.SimpleVoteOutVO
|
||||||
|
import scala.collection.immutable.List
|
||||||
|
import scala.collection.Map
|
||||||
|
import scala.collection.mutable.ArrayBuffer
|
||||||
|
import spray.json._
|
||||||
|
import DefaultJsonProtocol._
|
||||||
|
|
||||||
|
class AddTldrawShapeWhiteboardRecordEvent extends AbstractWhiteboardRecordEvent {
|
||||||
|
import AddTldrawShapeWhiteboardRecordEvent._
|
||||||
|
|
||||||
|
implicit object AnyJsonFormat extends JsonFormat[Any] {
|
||||||
|
def write(x: Any) = x match {
|
||||||
|
case n: Int => JsNumber(n)
|
||||||
|
case s: String => JsString(s)
|
||||||
|
case d: Double => JsNumber(d)
|
||||||
|
case m: scala.collection.immutable.Map[String, _] => mapFormat[String, Any].write(m)
|
||||||
|
case l: List[_] => listFormat[Any].write(l)
|
||||||
|
case b: Boolean if b == true => JsTrue
|
||||||
|
case b: Boolean if b == false => JsFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
def read(value: JsValue) = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
setEvent("AddTldrawShapeEvent")
|
||||||
|
|
||||||
|
def setUserId(id: String) {
|
||||||
|
eventMap.put(USER_ID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setAnnotationId(id: String) {
|
||||||
|
eventMap.put(SHAPE_ID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
def addAnnotation(annotation: scala.collection.immutable.Map[String, Any]) {
|
||||||
|
eventMap.put(SHAPE_DATA, annotation.toJson.compactPrint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object AddTldrawShapeWhiteboardRecordEvent {
|
||||||
|
protected final val USER_ID = "userId"
|
||||||
|
protected final val SHAPE_ID = "shapeId"
|
||||||
|
protected final val SHAPE_DATA = "shapeData"
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||||
|
*
|
||||||
|
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the
|
||||||
|
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||||
|
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||||
|
* version.
|
||||||
|
*
|
||||||
|
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||||
|
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License along
|
||||||
|
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.bigbluebutton.core.record.events
|
||||||
|
|
||||||
|
class DeleteTldrawShapeRecordEvent extends AbstractWhiteboardRecordEvent {
|
||||||
|
import DeleteTldrawShapeRecordEvent._
|
||||||
|
|
||||||
|
setEvent("DeleteTldrawShapeEvent")
|
||||||
|
|
||||||
|
def setUserId(userId: String) {
|
||||||
|
eventMap.put(USER_ID, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setShapeId(shapeId: String) {
|
||||||
|
eventMap.put(SHAPE_ID, shapeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object DeleteTldrawShapeRecordEvent {
|
||||||
|
protected final val USER_ID = "userId"
|
||||||
|
protected final val SHAPE_ID = "shapeId"
|
||||||
|
}
|
@ -32,23 +32,28 @@ class ResizeAndMoveSlideRecordEvent extends AbstractPresentationRecordEvent {
|
|||||||
eventMap.put(ID, id)
|
eventMap.put(ID, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
def setXCamera(xCamera: Double) {
|
def setXOffset(offset: Double) {
|
||||||
eventMap.put(X_CAMERA, xCamera.toString)
|
eventMap.put(X_OFFSET, offset.toString)
|
||||||
}
|
}
|
||||||
|
|
||||||
def setYCamera(yCamera: Double) {
|
def setYOffset(offset: Double) {
|
||||||
eventMap.put(Y_CAMERA, yCamera.toString)
|
eventMap.put(Y_OFFSET, offset.toString)
|
||||||
}
|
}
|
||||||
|
|
||||||
def setZoom(zoom: Double) {
|
def setWidthRatio(ratio: Double) {
|
||||||
eventMap.put(ZOOM, zoom.toString)
|
eventMap.put(WIDTH_RATIO, ratio.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setHeightRatio(ratio: Double) {
|
||||||
|
eventMap.put(HEIGHT_RATIO, ratio.toString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object ResizeAndMoveSlideRecordEvent {
|
object ResizeAndMoveSlideRecordEvent {
|
||||||
protected final val PRES_NAME = "presentationName"
|
protected final val PRES_NAME = "presentationName"
|
||||||
protected final val ID = "id"
|
protected final val ID = "id"
|
||||||
protected final val X_CAMERA = "xCamera"
|
protected final val X_OFFSET = "xOffset"
|
||||||
protected final val Y_CAMERA = "yCamera"
|
protected final val Y_OFFSET = "yOffset"
|
||||||
protected final val ZOOM = "zoom"
|
protected final val WIDTH_RATIO = "widthRatio"
|
||||||
|
protected final val HEIGHT_RATIO = "heightRatio"
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||||
|
*
|
||||||
|
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the
|
||||||
|
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||||
|
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||||
|
* version.
|
||||||
|
*
|
||||||
|
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||||
|
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License along
|
||||||
|
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.bigbluebutton.core.record.events
|
||||||
|
|
||||||
|
class TldrawCameraChangedRecordEvent extends AbstractPresentationRecordEvent {
|
||||||
|
import TldrawCameraChangedRecordEvent._
|
||||||
|
|
||||||
|
setEvent("TldrawCameraChangedEvent")
|
||||||
|
|
||||||
|
def setPresentationName(name: String) {
|
||||||
|
eventMap.put(PRES_NAME, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setId(id: String) {
|
||||||
|
eventMap.put(ID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setXCamera(xCamera: Double) {
|
||||||
|
eventMap.put(X_CAMERA, xCamera.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setYCamera(yCamera: Double) {
|
||||||
|
eventMap.put(Y_CAMERA, yCamera.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setZoom(zoom: Double) {
|
||||||
|
eventMap.put(ZOOM, zoom.toString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object TldrawCameraChangedRecordEvent {
|
||||||
|
protected final val PRES_NAME = "presentationName"
|
||||||
|
protected final val ID = "id"
|
||||||
|
protected final val X_CAMERA = "xCamera"
|
||||||
|
protected final val Y_CAMERA = "yCamera"
|
||||||
|
protected final val ZOOM = "zoom"
|
||||||
|
}
|
@ -184,7 +184,7 @@ class RedisRecorderActor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private def handleResizeAndMovePageEvtMsg(msg: ResizeAndMovePageEvtMsg) {
|
private def handleResizeAndMovePageEvtMsg(msg: ResizeAndMovePageEvtMsg) {
|
||||||
val ev = new ResizeAndMoveSlideRecordEvent()
|
val ev = new TldrawCameraChangedRecordEvent()
|
||||||
ev.setMeetingId(msg.header.meetingId)
|
ev.setMeetingId(msg.header.meetingId)
|
||||||
ev.setPodId(msg.body.podId)
|
ev.setPodId(msg.body.podId)
|
||||||
ev.setPresentationName(msg.body.presentationId)
|
ev.setPresentationName(msg.body.presentationId)
|
||||||
@ -280,7 +280,7 @@ class RedisRecorderActor(
|
|||||||
|
|
||||||
private def handleSendWhiteboardAnnotationsEvtMsg(msg: SendWhiteboardAnnotationsEvtMsg) {
|
private def handleSendWhiteboardAnnotationsEvtMsg(msg: SendWhiteboardAnnotationsEvtMsg) {
|
||||||
msg.body.annotations.foreach(annotation => {
|
msg.body.annotations.foreach(annotation => {
|
||||||
val ev = new AddShapeWhiteboardRecordEvent()
|
val ev = new AddTldrawShapeWhiteboardRecordEvent()
|
||||||
ev.setMeetingId(msg.header.meetingId)
|
ev.setMeetingId(msg.header.meetingId)
|
||||||
ev.setPresentation(getPresentationId(annotation.wbId))
|
ev.setPresentation(getPresentationId(annotation.wbId))
|
||||||
ev.setPageNumber(getPageNum(annotation.wbId))
|
ev.setPageNumber(getPageNum(annotation.wbId))
|
||||||
@ -320,7 +320,7 @@ class RedisRecorderActor(
|
|||||||
|
|
||||||
private def handleDeleteWhiteboardAnnotationsEvtMsg(msg: DeleteWhiteboardAnnotationsEvtMsg) {
|
private def handleDeleteWhiteboardAnnotationsEvtMsg(msg: DeleteWhiteboardAnnotationsEvtMsg) {
|
||||||
msg.body.annotationsIds.foreach(annotationId => {
|
msg.body.annotationsIds.foreach(annotationId => {
|
||||||
val ev = new UndoAnnotationRecordEvent()
|
val ev = new DeleteTldrawShapeRecordEvent()
|
||||||
ev.setMeetingId(msg.header.meetingId)
|
ev.setMeetingId(msg.header.meetingId)
|
||||||
ev.setPresentation(getPresentationId(msg.body.whiteboardId))
|
ev.setPresentation(getPresentationId(msg.body.whiteboardId))
|
||||||
ev.setPageNumber(getPageNum(msg.body.whiteboardId))
|
ev.setPageNumber(getPageNum(msg.body.whiteboardId))
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import ReactCursorPosition from "react-cursor-position";
|
import ReactCursorPosition from "react-cursor-position";
|
||||||
import Vec from "@tldraw/vec";
|
|
||||||
import { _ } from "lodash";
|
import { _ } from "lodash";
|
||||||
|
|
||||||
function usePrevious(value) {
|
function usePrevious(value) {
|
||||||
@ -79,7 +78,7 @@ const PositionLabel = (props) => {
|
|||||||
whiteboardId,
|
whiteboardId,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { name, color, userId, presenter } = currentUser;
|
const { name, color } = currentUser;
|
||||||
const prevCurrentPoint = usePrevious(currentPoint);
|
const prevCurrentPoint = usePrevious(currentPoint);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -128,8 +127,8 @@ export default function Cursors(props) {
|
|||||||
!cursorWrapper.hasOwnProperty("mouseleave") &&
|
!cursorWrapper.hasOwnProperty("mouseleave") &&
|
||||||
cursorWrapper?.addEventListener("mouseleave", (event) => {
|
cursorWrapper?.addEventListener("mouseleave", (event) => {
|
||||||
publishCursorUpdate({
|
publishCursorUpdate({
|
||||||
xPercent: null,
|
xPercent: -1.0,
|
||||||
yPercent: null,
|
yPercent: -1.0,
|
||||||
whiteboardId: whiteboardId,
|
whiteboardId: whiteboardId,
|
||||||
});
|
});
|
||||||
setActive(false);
|
setActive(false);
|
||||||
|
@ -960,7 +960,7 @@ module BigBlueButton
|
|||||||
# The following events are considered to indicate that the presentation
|
# The following events are considered to indicate that the presentation
|
||||||
# area was actively used during the session.
|
# area was actively used during the session.
|
||||||
when 'AddShapeEvent', 'ModifyTextEvent', 'UndoShapeEvent',
|
when 'AddShapeEvent', 'ModifyTextEvent', 'UndoShapeEvent',
|
||||||
'ClearPageEvent'
|
'ClearPageEvent', 'AddTldrawShapeEvent', 'DeleteTldrawShapeEvent'
|
||||||
BigBlueButton.logger.debug("Seen a #{event['eventname']} event, presentation area used.")
|
BigBlueButton.logger.debug("Seen a #{event['eventname']} event, presentation area used.")
|
||||||
return true
|
return true
|
||||||
# We ignore the first SharePresentationEvent, since it's the default
|
# We ignore the first SharePresentationEvent, since it's the default
|
||||||
@ -1093,5 +1093,11 @@ module BigBlueButton
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Check if doc has tldraw events
|
||||||
|
def self.check_for_tldraw_events(events)
|
||||||
|
return !(events.xpath("recording/event[@eventname='TldrawCameraChangedEvent']").empty? &&
|
||||||
|
events.xpath("recording/event[@eventname='AddTldrawShapeEvent']").empty?)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -192,6 +192,10 @@ unless FileTest.directory?(target_dir)
|
|||||||
|
|
||||||
# Copy thumbnails from raw files
|
# Copy thumbnails from raw files
|
||||||
FileUtils.cp_r("#{pres_dir}/thumbnails", "#{target_pres_dir}/thumbnails") if File.exist?("#{pres_dir}/thumbnails")
|
FileUtils.cp_r("#{pres_dir}/thumbnails", "#{target_pres_dir}/thumbnails") if File.exist?("#{pres_dir}/thumbnails")
|
||||||
|
tldraw = BigBlueButton::Events.check_for_tldraw_events(@doc);
|
||||||
|
if (tldraw)
|
||||||
|
FileUtils.cp_r("#{pres_dir}/svgs", "#{target_pres_dir}/svgs") if File.exist?("#{pres_dir}/svgs")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
BigBlueButton.logger.info('Generating closed captions')
|
BigBlueButton.logger.info('Generating closed captions')
|
||||||
|
@ -382,6 +382,30 @@ def svg_render_shape_poll(g, slide, shape)
|
|||||||
width: width, height: height, x: x, y: y)
|
width: width, height: height, x: x, y: y)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_tldraw_shape(image_shapes, slide, shape)
|
||||||
|
shape_in = shape[:in]
|
||||||
|
shape_out = shape[:out]
|
||||||
|
|
||||||
|
if shape_in == shape_out
|
||||||
|
BigBlueButton.logger.info("Draw #{shape[:shape_id]} Shape #{shape[:shape_unique_id]} is never shown (duration rounds to 0)")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if (shape_in >= slide[:out]) || (!shape[:out].nil? && shape[:out] <= slide[:in])
|
||||||
|
BigBlueButton.logger.info("Draw #{shape[:shape_id]} Shape #{shape[:shape_unique_id]} is not visible during image time span")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
tldraw_shape = {
|
||||||
|
id: shape[:shape_id],
|
||||||
|
timestamp: shape_in,
|
||||||
|
undo: (shape[:undo].nil? ? -1 : shape[:undo]),
|
||||||
|
shape_data: shape[:shape_data]
|
||||||
|
}
|
||||||
|
|
||||||
|
image_shapes.push(tldraw_shape)
|
||||||
|
end
|
||||||
|
|
||||||
def svg_render_shape(canvas, slide, shape, image_id)
|
def svg_render_shape(canvas, slide, shape, image_id)
|
||||||
shape_in = shape[:in]
|
shape_in = shape[:in]
|
||||||
shape_out = shape[:out]
|
shape_out = shape[:out]
|
||||||
@ -426,7 +450,7 @@ def svg_render_shape(canvas, slide, shape, image_id)
|
|||||||
end
|
end
|
||||||
|
|
||||||
@svg_image_id = 1
|
@svg_image_id = 1
|
||||||
def svg_render_image(svg, slide, shapes)
|
def svg_render_image(svg, slide, shapes, tldraw, tldraw_shapes)
|
||||||
slide_number = slide[:slide]
|
slide_number = slide[:slide]
|
||||||
presentation = slide[:presentation]
|
presentation = slide[:presentation]
|
||||||
slide_in = slide[:in]
|
slide_in = slide[:in]
|
||||||
@ -458,15 +482,25 @@ def svg_render_image(svg, slide, shapes)
|
|||||||
|
|
||||||
shapes = shapes[presentation][slide_number]
|
shapes = shapes[presentation][slide_number]
|
||||||
|
|
||||||
canvas = doc.create_element('g',
|
if !tldraw
|
||||||
class: 'canvas', id: "canvas#{image_id}",
|
canvas = doc.create_element('g',
|
||||||
image: "image#{image_id}", display: 'none')
|
class: 'canvas', id: "canvas#{image_id}",
|
||||||
|
image: "image#{image_id}", display: 'none')
|
||||||
|
|
||||||
shapes.each do |shape|
|
shapes.each do |shape|
|
||||||
svg_render_shape(canvas, slide, shape, image_id)
|
svg_render_shape(canvas, slide, shape, image_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
svg << canvas unless canvas.element_children.empty?
|
||||||
|
else
|
||||||
|
image_shapes = []
|
||||||
|
|
||||||
|
shapes.each do |shape|
|
||||||
|
build_tldraw_shape(image_shapes, slide, shape)
|
||||||
|
end
|
||||||
|
|
||||||
|
tldraw_shapes[image_id] = { :shapes=>image_shapes, :timestamp=> slide_in}
|
||||||
end
|
end
|
||||||
|
|
||||||
svg << canvas unless canvas.element_children.empty?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def panzoom_viewbox(panzoom)
|
def panzoom_viewbox(panzoom)
|
||||||
@ -483,13 +517,19 @@ def panzoom_viewbox(panzoom)
|
|||||||
[x, y, w, h]
|
[x, y, w, h]
|
||||||
end
|
end
|
||||||
|
|
||||||
def panzooms_emit_event(rec, panzoom)
|
def panzooms_emit_event(rec, panzoom, tldraw)
|
||||||
panzoom_in = panzoom[:in]
|
panzoom_in = panzoom[:in]
|
||||||
return if panzoom_in == panzoom[:out]
|
return if panzoom_in == panzoom[:out]
|
||||||
|
|
||||||
rec.event(timestamp: panzoom_in) do
|
if !tldraw
|
||||||
x, y, w, h = panzoom_viewbox(panzoom)
|
rec.event(timestamp: panzoom_in) do
|
||||||
rec.viewBox("#{x} #{y} #{w} #{h}")
|
x, y, w, h = panzoom_viewbox(panzoom)
|
||||||
|
rec.viewBox("#{x} #{y} #{w} #{h}")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
rec.event(timestamp: panzoom_in) do
|
||||||
|
rec.cameraAndZoom("#{panzoom[:x_camera]} #{panzoom[:y_camera]} #{panzoom[:zoom]}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -497,14 +537,14 @@ def convert_cursor_coordinate(cursor_coord, panzoom_offset, panzoom_ratio)
|
|||||||
(((cursor_coord / 100.0) + (panzoom_offset * MAGIC_MYSTERY_NUMBER / 100.0)) / (panzoom_ratio / 100.0)).round(5)
|
(((cursor_coord / 100.0) + (panzoom_offset * MAGIC_MYSTERY_NUMBER / 100.0)) / (panzoom_ratio / 100.0)).round(5)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cursors_emit_event(rec, cursor)
|
def cursors_emit_event(rec, cursor, tldraw)
|
||||||
cursor_in = cursor[:in]
|
cursor_in = cursor[:in]
|
||||||
return if cursor_in == cursor[:out]
|
return if cursor_in == cursor[:out]
|
||||||
|
|
||||||
rec.event(timestamp: cursor_in) do
|
rec.event(timestamp: cursor_in) do
|
||||||
panzoom = cursor[:panzoom]
|
panzoom = cursor[:panzoom]
|
||||||
if cursor[:visible]
|
if cursor[:visible]
|
||||||
if @version_atleast_2_0_0
|
if @version_atleast_2_0_0 && !tldraw
|
||||||
# In BBB 2.0, the cursor now uses the same coordinate system as annotations
|
# In BBB 2.0, the cursor now uses the same coordinate system as annotations
|
||||||
# Use the panzoom information to convert it to be relative to viewbox
|
# Use the panzoom information to convert it to be relative to viewbox
|
||||||
x = convert_cursor_coordinate(cursor[:x], panzoom[:x_offset], panzoom[:width_ratio])
|
x = convert_cursor_coordinate(cursor[:x], panzoom[:x_offset], panzoom[:width_ratio])
|
||||||
@ -535,6 +575,53 @@ def determine_slide_number(slide, current_slide)
|
|||||||
slide
|
slide
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def events_parse_tldraw_shape(shapes, event, current_presentation, current_slide, timestamp)
|
||||||
|
presentation = event.at_xpath('presentation')
|
||||||
|
slide = event.at_xpath('pageNumber')
|
||||||
|
|
||||||
|
presentation = determine_presentation(presentation, current_presentation)
|
||||||
|
slide = determine_slide_number(slide, current_slide)
|
||||||
|
|
||||||
|
# Set up the shapes data structures if needed
|
||||||
|
shapes[presentation] ||= {}
|
||||||
|
shapes[presentation][slide] ||= []
|
||||||
|
|
||||||
|
# We only need to deal with shapes for this slide
|
||||||
|
shapes = shapes[presentation][slide]
|
||||||
|
|
||||||
|
# Set up the structure for this shape
|
||||||
|
shape = {}
|
||||||
|
# Common properties
|
||||||
|
shape[:in] = timestamp
|
||||||
|
shape_data = shape[:shape_data] = JSON.parse(event.at_xpath('shapeData'))
|
||||||
|
|
||||||
|
user_id = event.at_xpath('userId')&.text
|
||||||
|
shape[:user_id] = user_id if user_id
|
||||||
|
|
||||||
|
shape_id = event.at_xpath('shapeId')&.text
|
||||||
|
shape[:id] = shape_id if shape_id
|
||||||
|
|
||||||
|
draw_id = shape[:shape_id] = @svg_shape_id
|
||||||
|
@svg_shape_id += 1
|
||||||
|
|
||||||
|
# Find the previous shape, for updates
|
||||||
|
prev_shape = nil
|
||||||
|
if shape_id
|
||||||
|
# If we have a shape ID, look up the previous shape by ID
|
||||||
|
prev_shape_pos = shapes.rindex { |s| s[:shade_id] == shape_id }
|
||||||
|
prev_shape = prev_shape_pos ? shapes[prev_shape_pos] : nil
|
||||||
|
end
|
||||||
|
if prev_shape
|
||||||
|
prev_shape[:out] = timestamp
|
||||||
|
shape[:shape_unique_id] = prev_shape[:shape_unique_id]
|
||||||
|
else
|
||||||
|
shape[:shape_unique_id] = @svg_shape_unique_id
|
||||||
|
@svg_shape_unique_id += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
shapes << shape
|
||||||
|
end
|
||||||
|
|
||||||
def events_parse_shape(shapes, event, current_presentation, current_slide, timestamp)
|
def events_parse_shape(shapes, event, current_presentation, current_slide, timestamp)
|
||||||
# Figure out what presentation+slide this shape is for, with fallbacks
|
# Figure out what presentation+slide this shape is for, with fallbacks
|
||||||
# for old BBB where this info isn't in the shape messages
|
# for old BBB where this info isn't in the shape messages
|
||||||
@ -734,7 +821,7 @@ def events_parse_clear(shapes, event, current_presentation, current_slide, times
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def events_get_image_info(slide)
|
def events_get_image_info(slide, tldraw)
|
||||||
slide_deskshare = slide[:deskshare]
|
slide_deskshare = slide[:deskshare]
|
||||||
slide_presentation = slide[:presentation]
|
slide_presentation = slide[:presentation]
|
||||||
|
|
||||||
@ -744,7 +831,8 @@ def events_get_image_info(slide)
|
|||||||
slide[:src] = 'presentation/logo.png'
|
slide[:src] = 'presentation/logo.png'
|
||||||
else
|
else
|
||||||
slide_nr = slide[:slide] + 1
|
slide_nr = slide[:slide] + 1
|
||||||
slide[:src] = "presentation/#{slide_presentation}/slide-#{slide_nr}.png"
|
tldraw ? slide[:src] = "presentation/#{slide_presentation}/svgs/slide#{slide_nr}.svg"
|
||||||
|
: slide[:src] = "presentation/#{slide_presentation}/slide-#{slide_nr}.png"
|
||||||
slide[:text] = "presentation/#{slide_presentation}/textfiles/slide-#{slide_nr}.txt"
|
slide[:text] = "presentation/#{slide_presentation}/textfiles/slide-#{slide_nr}.txt"
|
||||||
end
|
end
|
||||||
image_path = "#{@process_dir}/#{slide[:src]}"
|
image_path = "#{@process_dir}/#{slide[:src]}"
|
||||||
@ -792,6 +880,7 @@ def process_presentation(package_dir)
|
|||||||
# Current pan/zoom state
|
# Current pan/zoom state
|
||||||
current_x_offset = current_y_offset = 0.0
|
current_x_offset = current_y_offset = 0.0
|
||||||
current_width_ratio = current_height_ratio = 100.0
|
current_width_ratio = current_height_ratio = 100.0
|
||||||
|
current_x_camera = current_y_camera = current_zoom = 0.0
|
||||||
# Current cursor status
|
# Current cursor status
|
||||||
cursor_x = cursor_y = -1.0
|
cursor_x = cursor_y = -1.0
|
||||||
cursor_visible = false
|
cursor_visible = false
|
||||||
@ -802,6 +891,8 @@ def process_presentation(package_dir)
|
|||||||
panzooms = []
|
panzooms = []
|
||||||
cursors = []
|
cursors = []
|
||||||
shapes = {}
|
shapes = {}
|
||||||
|
tldraw = BigBlueButton::Events.check_for_tldraw_events(@doc)
|
||||||
|
tldraw_shapes = {}
|
||||||
|
|
||||||
# Iterate through the events.xml and store the events, building the
|
# Iterate through the events.xml and store the events, building the
|
||||||
# xml files as we go
|
# xml files as we go
|
||||||
@ -836,6 +927,12 @@ def process_presentation(package_dir)
|
|||||||
current_height_ratio = event.at_xpath('heightRatio').text.to_f
|
current_height_ratio = event.at_xpath('heightRatio').text.to_f
|
||||||
panzoom_changed = true
|
panzoom_changed = true
|
||||||
|
|
||||||
|
when 'TldrawCameraChangedEvent'
|
||||||
|
current_x_camera = event.at_xpath('xCamera').text.to_f
|
||||||
|
current_y_camera = event.at_xpath('yCamera').text.to_f
|
||||||
|
current_zoom = event.at_xpath('zoom').text.to_f
|
||||||
|
panzoom_changed = true
|
||||||
|
|
||||||
when 'DeskshareStartedEvent', 'StartWebRTCDesktopShareEvent'
|
when 'DeskshareStartedEvent', 'StartWebRTCDesktopShareEvent'
|
||||||
deskshare = slide_changed = true if @presentation_props['include_deskshare']
|
deskshare = slide_changed = true if @presentation_props['include_deskshare']
|
||||||
|
|
||||||
@ -848,7 +945,10 @@ def process_presentation(package_dir)
|
|||||||
when 'AddShapeEvent', 'ModifyTextEvent'
|
when 'AddShapeEvent', 'ModifyTextEvent'
|
||||||
events_parse_shape(shapes, event, current_presentation, current_slide, timestamp)
|
events_parse_shape(shapes, event, current_presentation, current_slide, timestamp)
|
||||||
|
|
||||||
when 'UndoShapeEvent', 'UndoAnnotationEvent'
|
when 'AddTldrawShapeEvent'
|
||||||
|
events_parse_tldraw_shape(shapes, event, current_presentation, current_slide, timestamp)
|
||||||
|
|
||||||
|
when 'UndoShapeEvent', 'UndoAnnotationEvent', 'DeleteTldrawShapeEvent'
|
||||||
events_parse_undo(shapes, event, current_presentation, current_slide, timestamp)
|
events_parse_undo(shapes, event, current_presentation, current_slide, timestamp)
|
||||||
|
|
||||||
when 'ClearPageEvent', 'ClearWhiteboardEvent'
|
when 'ClearPageEvent', 'ClearWhiteboardEvent'
|
||||||
@ -887,7 +987,7 @@ def process_presentation(package_dir)
|
|||||||
else
|
else
|
||||||
if slide
|
if slide
|
||||||
slide[:out] = timestamp
|
slide[:out] = timestamp
|
||||||
svg_render_image(svg, slide, shapes)
|
svg_render_image(svg, slide, shapes, tldraw, tldraw_shapes)
|
||||||
end
|
end
|
||||||
|
|
||||||
BigBlueButton.logger.info("Presentation #{current_presentation} Slide #{current_slide} Deskshare #{deskshare}")
|
BigBlueButton.logger.info("Presentation #{current_presentation} Slide #{current_slide} Deskshare #{deskshare}")
|
||||||
@ -897,7 +997,7 @@ def process_presentation(package_dir)
|
|||||||
in: timestamp,
|
in: timestamp,
|
||||||
deskshare: deskshare,
|
deskshare: deskshare,
|
||||||
}
|
}
|
||||||
events_get_image_info(slide)
|
events_get_image_info(slide, tldraw)
|
||||||
slides << slide
|
slides << slide
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -909,40 +1009,59 @@ def process_presentation(package_dir)
|
|||||||
slide_width = slide[:width]
|
slide_width = slide[:width]
|
||||||
slide_height = slide[:height]
|
slide_height = slide[:height]
|
||||||
if panzoom &&
|
if panzoom &&
|
||||||
|
(panzoom[:deskshare] == deskshare) &&
|
||||||
|
((!tldraw &&
|
||||||
(panzoom[:x_offset] == current_x_offset) &&
|
(panzoom[:x_offset] == current_x_offset) &&
|
||||||
(panzoom[:y_offset] == current_y_offset) &&
|
(panzoom[:y_offset] == current_y_offset) &&
|
||||||
(panzoom[:width_ratio] == current_width_ratio) &&
|
(panzoom[:width_ratio] == current_width_ratio) &&
|
||||||
(panzoom[:height_ratio] == current_height_ratio) &&
|
(panzoom[:height_ratio] == current_height_ratio) &&
|
||||||
(panzoom[:width] == slide_width) &&
|
(panzoom[:width] == slide_width) &&
|
||||||
(panzoom[:height] == slide_height) &&
|
(panzoom[:height] == slide_height)) ||
|
||||||
(panzoom[:deskshare] == deskshare)
|
(tldraw &&
|
||||||
|
(panzoom[:x_camera] == current_x_camera) &&
|
||||||
|
(panzoom[:y_camera] == current_y_camera) &&
|
||||||
|
(panzoom[:zoom] == current_zoom))
|
||||||
|
)
|
||||||
BigBlueButton.logger.info('Panzoom: skipping, no changes')
|
BigBlueButton.logger.info('Panzoom: skipping, no changes')
|
||||||
panzoom_changed = false
|
panzoom_changed = false
|
||||||
else
|
else
|
||||||
if panzoom
|
if panzoom
|
||||||
panzoom[:out] = timestamp
|
panzoom[:out] = timestamp
|
||||||
panzooms_emit_event(panzooms_rec, panzoom)
|
panzooms_emit_event(panzooms_rec, panzoom, tldraw)
|
||||||
|
end
|
||||||
|
if !tldraw
|
||||||
|
BigBlueButton.logger.info("Panzoom: #{current_x_offset} #{current_y_offset} #{current_width_ratio} #{current_height_ratio} (#{slide_width}x#{slide_height})")
|
||||||
|
panzoom = {
|
||||||
|
x_offset: current_x_offset,
|
||||||
|
y_offset: current_y_offset,
|
||||||
|
width_ratio: current_width_ratio,
|
||||||
|
height_ratio: current_height_ratio,
|
||||||
|
width: slide[:width],
|
||||||
|
height: slide[:height],
|
||||||
|
in: timestamp,
|
||||||
|
deskshare: deskshare,
|
||||||
|
}
|
||||||
|
else
|
||||||
|
BigBlueButton.logger.info("Panzoom: #{current_x_camera} #{current_y_camera} #{current_zoom} (#{slide_width}x#{slide_height})")
|
||||||
|
panzoom = {
|
||||||
|
x_camera: current_x_camera,
|
||||||
|
y_camera: current_y_camera,
|
||||||
|
zoom: current_zoom,
|
||||||
|
in: timestamp,
|
||||||
|
deskshare: deskshare,
|
||||||
|
}
|
||||||
end
|
end
|
||||||
BigBlueButton.logger.info("Panzoom: #{current_x_offset} #{current_y_offset} #{current_width_ratio} #{current_height_ratio} (#{slide_width}x#{slide_height})")
|
|
||||||
panzoom = {
|
|
||||||
x_offset: current_x_offset,
|
|
||||||
y_offset: current_y_offset,
|
|
||||||
width_ratio: current_width_ratio,
|
|
||||||
height_ratio: current_height_ratio,
|
|
||||||
width: slide[:width],
|
|
||||||
height: slide[:height],
|
|
||||||
in: timestamp,
|
|
||||||
deskshare: deskshare,
|
|
||||||
}
|
|
||||||
panzooms << panzoom
|
panzooms << panzoom
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Perform cursor finalization
|
# Perform cursor finalization
|
||||||
if cursor_changed || panzoom_changed
|
if cursor_changed || panzoom_changed
|
||||||
unless cursor_x >= 0 && cursor_x <= 100 &&
|
if !tldraw
|
||||||
cursor_y >= 0 && cursor_y <= 100
|
unless cursor_x >= 0 && cursor_x <= 100 &&
|
||||||
cursor_visible = false
|
cursor_y >= 0 && cursor_y <= 100
|
||||||
|
cursor_visible = false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
panzoom = panzooms.last
|
panzoom = panzooms.last
|
||||||
@ -955,7 +1074,7 @@ def process_presentation(package_dir)
|
|||||||
else
|
else
|
||||||
if cursor
|
if cursor
|
||||||
cursor[:out] = timestamp
|
cursor[:out] = timestamp
|
||||||
cursors_emit_event(cursors_rec, cursor)
|
cursors_emit_event(cursors_rec, cursor, tldraw)
|
||||||
end
|
end
|
||||||
cursor = {
|
cursor = {
|
||||||
visible: cursor_visible,
|
visible: cursor_visible,
|
||||||
@ -972,26 +1091,27 @@ def process_presentation(package_dir)
|
|||||||
# Add the last slide, panzoom, and cursor
|
# Add the last slide, panzoom, and cursor
|
||||||
slide = slides.last
|
slide = slides.last
|
||||||
slide[:out] = last_timestamp
|
slide[:out] = last_timestamp
|
||||||
svg_render_image(svg, slide, shapes)
|
svg_render_image(svg, slide, shapes, tldraw, tldraw_shapes)
|
||||||
panzoom = panzooms.last
|
panzoom = panzooms.last
|
||||||
panzoom[:out] = last_timestamp
|
panzoom[:out] = last_timestamp
|
||||||
panzooms_emit_event(panzooms_rec, panzoom)
|
panzooms_emit_event(panzooms_rec, panzoom, tldraw)
|
||||||
cursor = cursors.last
|
cursor = cursors.last
|
||||||
cursor[:out] = last_timestamp
|
cursor[:out] = last_timestamp
|
||||||
cursors_emit_event(cursors_rec, cursor)
|
cursors_emit_event(cursors_rec, cursor, tldraw)
|
||||||
|
|
||||||
cursors_doc = Builder::XmlMarkup.new(indent: 2)
|
cursors_doc = Builder::XmlMarkup.new(indent: 2)
|
||||||
cursors_doc.instruct!
|
cursors_doc.instruct!
|
||||||
cursors_doc.recording(id: 'cursor_events') { |xml| xml << cursors_rec.target! }
|
cursors_doc.recording(id: 'cursor_events', tldraw: tldraw) { |xml| xml << cursors_rec.target! }
|
||||||
|
|
||||||
panzooms_doc = Builder::XmlMarkup.new(indent: 2)
|
panzooms_doc = Builder::XmlMarkup.new(indent: 2)
|
||||||
panzooms_doc.instruct!
|
panzooms_doc.instruct!
|
||||||
panzooms_doc.recording(id: 'panzoom_events') { |xml| xml << panzooms_rec.target! }
|
panzooms_doc.recording(id: 'panzoom_events', tldraw: tldraw) { |xml| xml << panzooms_rec.target! }
|
||||||
|
|
||||||
# And save the result
|
# And save the result
|
||||||
File.write("#{package_dir}/#{@shapes_svg_filename}", shapes_doc.to_xml)
|
File.write("#{package_dir}/#{@shapes_svg_filename}", shapes_doc.to_xml)
|
||||||
File.write("#{package_dir}/#{@panzooms_xml_filename}", panzooms_doc.target!)
|
File.write("#{package_dir}/#{@panzooms_xml_filename}", panzooms_doc.target!)
|
||||||
File.write("#{package_dir}/#{@cursor_xml_filename}", cursors_doc.target!)
|
File.write("#{package_dir}/#{@cursor_xml_filename}", cursors_doc.target!)
|
||||||
|
generate_json_file(package_dir, @tldraw_shapes_filename, tldraw_shapes) if tldraw
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_chat_messages(events, bbb_props)
|
def process_chat_messages(events, bbb_props)
|
||||||
@ -1170,6 +1290,7 @@ end
|
|||||||
@panzooms_xml_filename = 'panzooms.xml'
|
@panzooms_xml_filename = 'panzooms.xml'
|
||||||
@cursor_xml_filename = 'cursor.xml'
|
@cursor_xml_filename = 'cursor.xml'
|
||||||
@deskshare_xml_filename = 'deskshare.xml'
|
@deskshare_xml_filename = 'deskshare.xml'
|
||||||
|
@tldraw_shapes_filename = 'tldraw.json'
|
||||||
@svg_shape_id = 1
|
@svg_shape_id = 1
|
||||||
@svg_shape_unique_id = 1
|
@svg_shape_unique_id = 1
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user