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:
germanocaumo 2022-06-23 17:04:09 +00:00
parent 75efa60f7d
commit 853d0dfd9b
9 changed files with 353 additions and 60 deletions

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
} }

View File

@ -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"
}

View File

@ -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))

View File

@ -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);

View File

@ -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

View File

@ -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')

View File

@ -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