From 853d0dfd9bf2812d7cbd45e0805c707939b8f8d3 Mon Sep 17 00:00:00 2001 From: germanocaumo Date: Thu, 23 Jun 2022 17:04:09 +0000 Subject: [PATCH] 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. --- .../AddTldrawShapeWhiteboardRecordEvent.scala | 65 ++++++ .../events/DeleteTldrawShapeRecordEvent.scala | 39 ++++ .../ResizeAndMoveSlideRecordEvent.scala | 23 +- .../TldrawCameraChangedRecordEvent.scala | 54 +++++ .../endpoint/redis/RedisRecorderActor.scala | 6 +- .../whiteboard/cursors/component.jsx | 7 +- .../recordandplayback/generators/events.rb | 10 +- .../scripts/process/presentation.rb | 4 + .../scripts/publish/presentation.rb | 205 ++++++++++++++---- 9 files changed, 353 insertions(+), 60 deletions(-) create mode 100755 akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AddTldrawShapeWhiteboardRecordEvent.scala create mode 100755 akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/DeleteTldrawShapeRecordEvent.scala create mode 100755 akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/TldrawCameraChangedRecordEvent.scala diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AddTldrawShapeWhiteboardRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AddTldrawShapeWhiteboardRecordEvent.scala new file mode 100755 index 0000000000..cdba60459d --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AddTldrawShapeWhiteboardRecordEvent.scala @@ -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 . + * + */ + +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" +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/DeleteTldrawShapeRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/DeleteTldrawShapeRecordEvent.scala new file mode 100755 index 0000000000..4322e03895 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/DeleteTldrawShapeRecordEvent.scala @@ -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 . + * + */ + +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" +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/ResizeAndMoveSlideRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/ResizeAndMoveSlideRecordEvent.scala index 1797a89ada..0905fd5d8a 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/ResizeAndMoveSlideRecordEvent.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/ResizeAndMoveSlideRecordEvent.scala @@ -32,23 +32,28 @@ class ResizeAndMoveSlideRecordEvent extends AbstractPresentationRecordEvent { eventMap.put(ID, id) } - def setXCamera(xCamera: Double) { - eventMap.put(X_CAMERA, xCamera.toString) + def setXOffset(offset: Double) { + eventMap.put(X_OFFSET, offset.toString) } - def setYCamera(yCamera: Double) { - eventMap.put(Y_CAMERA, yCamera.toString) + def setYOffset(offset: Double) { + eventMap.put(Y_OFFSET, offset.toString) } - def setZoom(zoom: Double) { - eventMap.put(ZOOM, zoom.toString) + def setWidthRatio(ratio: Double) { + eventMap.put(WIDTH_RATIO, ratio.toString) + } + + def setHeightRatio(ratio: Double) { + eventMap.put(HEIGHT_RATIO, ratio.toString) } } object ResizeAndMoveSlideRecordEvent { 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" + protected final val X_OFFSET = "xOffset" + protected final val Y_OFFSET = "yOffset" + protected final val WIDTH_RATIO = "widthRatio" + protected final val HEIGHT_RATIO = "heightRatio" } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/TldrawCameraChangedRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/TldrawCameraChangedRecordEvent.scala new file mode 100755 index 0000000000..f5ea3deba8 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/TldrawCameraChangedRecordEvent.scala @@ -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 . + * + */ + +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" +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala index 0954bed385..04097fc8ab 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala @@ -184,7 +184,7 @@ class RedisRecorderActor( } private def handleResizeAndMovePageEvtMsg(msg: ResizeAndMovePageEvtMsg) { - val ev = new ResizeAndMoveSlideRecordEvent() + val ev = new TldrawCameraChangedRecordEvent() ev.setMeetingId(msg.header.meetingId) ev.setPodId(msg.body.podId) ev.setPresentationName(msg.body.presentationId) @@ -280,7 +280,7 @@ class RedisRecorderActor( private def handleSendWhiteboardAnnotationsEvtMsg(msg: SendWhiteboardAnnotationsEvtMsg) { msg.body.annotations.foreach(annotation => { - val ev = new AddShapeWhiteboardRecordEvent() + val ev = new AddTldrawShapeWhiteboardRecordEvent() ev.setMeetingId(msg.header.meetingId) ev.setPresentation(getPresentationId(annotation.wbId)) ev.setPageNumber(getPageNum(annotation.wbId)) @@ -320,7 +320,7 @@ class RedisRecorderActor( private def handleDeleteWhiteboardAnnotationsEvtMsg(msg: DeleteWhiteboardAnnotationsEvtMsg) { msg.body.annotationsIds.foreach(annotationId => { - val ev = new UndoAnnotationRecordEvent() + val ev = new DeleteTldrawShapeRecordEvent() ev.setMeetingId(msg.header.meetingId) ev.setPresentation(getPresentationId(msg.body.whiteboardId)) ev.setPageNumber(getPageNum(msg.body.whiteboardId)) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/cursors/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/cursors/component.jsx index 387685915e..8aaf70dd0a 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/cursors/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/cursors/component.jsx @@ -1,6 +1,5 @@ import * as React from "react"; import ReactCursorPosition from "react-cursor-position"; -import Vec from "@tldraw/vec"; import { _ } from "lodash"; function usePrevious(value) { @@ -79,7 +78,7 @@ const PositionLabel = (props) => { whiteboardId, } = props; - const { name, color, userId, presenter } = currentUser; + const { name, color } = currentUser; const prevCurrentPoint = usePrevious(currentPoint); React.useEffect(() => { @@ -128,8 +127,8 @@ export default function Cursors(props) { !cursorWrapper.hasOwnProperty("mouseleave") && cursorWrapper?.addEventListener("mouseleave", (event) => { publishCursorUpdate({ - xPercent: null, - yPercent: null, + xPercent: -1.0, + yPercent: -1.0, whiteboardId: whiteboardId, }); setActive(false); diff --git a/record-and-playback/core/lib/recordandplayback/generators/events.rb b/record-and-playback/core/lib/recordandplayback/generators/events.rb index 97beb67cdf..e145cf3972 100755 --- a/record-and-playback/core/lib/recordandplayback/generators/events.rb +++ b/record-and-playback/core/lib/recordandplayback/generators/events.rb @@ -960,7 +960,7 @@ module BigBlueButton # The following events are considered to indicate that the presentation # area was actively used during the session. when 'AddShapeEvent', 'ModifyTextEvent', 'UndoShapeEvent', - 'ClearPageEvent' + 'ClearPageEvent', 'AddTldrawShapeEvent', 'DeleteTldrawShapeEvent' BigBlueButton.logger.debug("Seen a #{event['eventname']} event, presentation area used.") return true # We ignore the first SharePresentationEvent, since it's the default @@ -1093,5 +1093,11 @@ module BigBlueButton return false 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 \ No newline at end of file +end diff --git a/record-and-playback/presentation/scripts/process/presentation.rb b/record-and-playback/presentation/scripts/process/presentation.rb index aa64702af2..d0b0630b1b 100755 --- a/record-and-playback/presentation/scripts/process/presentation.rb +++ b/record-and-playback/presentation/scripts/process/presentation.rb @@ -192,6 +192,10 @@ unless FileTest.directory?(target_dir) # Copy thumbnails from raw files 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 BigBlueButton.logger.info('Generating closed captions') diff --git a/record-and-playback/presentation/scripts/publish/presentation.rb b/record-and-playback/presentation/scripts/publish/presentation.rb index 38ad1049e5..503f64e6a0 100755 --- a/record-and-playback/presentation/scripts/publish/presentation.rb +++ b/record-and-playback/presentation/scripts/publish/presentation.rb @@ -382,6 +382,30 @@ def svg_render_shape_poll(g, slide, shape) width: width, height: height, x: x, y: y) 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) shape_in = shape[:in] shape_out = shape[:out] @@ -426,7 +450,7 @@ def svg_render_shape(canvas, slide, shape, image_id) end @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] presentation = slide[:presentation] slide_in = slide[:in] @@ -458,15 +482,25 @@ def svg_render_image(svg, slide, shapes) shapes = shapes[presentation][slide_number] - canvas = doc.create_element('g', - class: 'canvas', id: "canvas#{image_id}", - image: "image#{image_id}", display: 'none') + if !tldraw + canvas = doc.create_element('g', + class: 'canvas', id: "canvas#{image_id}", + image: "image#{image_id}", display: 'none') - shapes.each do |shape| - svg_render_shape(canvas, slide, shape, image_id) + shapes.each do |shape| + 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 - - svg << canvas unless canvas.element_children.empty? end def panzoom_viewbox(panzoom) @@ -483,13 +517,19 @@ def panzoom_viewbox(panzoom) [x, y, w, h] end -def panzooms_emit_event(rec, panzoom) +def panzooms_emit_event(rec, panzoom, tldraw) panzoom_in = panzoom[:in] return if panzoom_in == panzoom[:out] - rec.event(timestamp: panzoom_in) do - x, y, w, h = panzoom_viewbox(panzoom) - rec.viewBox("#{x} #{y} #{w} #{h}") + if !tldraw + rec.event(timestamp: panzoom_in) do + 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 @@ -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) end -def cursors_emit_event(rec, cursor) +def cursors_emit_event(rec, cursor, tldraw) cursor_in = cursor[:in] return if cursor_in == cursor[:out] rec.event(timestamp: cursor_in) do panzoom = cursor[:panzoom] 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 # Use the panzoom information to convert it to be relative to viewbox x = convert_cursor_coordinate(cursor[:x], panzoom[:x_offset], panzoom[:width_ratio]) @@ -535,6 +575,53 @@ def determine_slide_number(slide, current_slide) slide 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) # Figure out what presentation+slide this shape is for, with fallbacks # 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 -def events_get_image_info(slide) +def events_get_image_info(slide, tldraw) slide_deskshare = slide[:deskshare] slide_presentation = slide[:presentation] @@ -744,7 +831,8 @@ def events_get_image_info(slide) slide[:src] = 'presentation/logo.png' else 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" end image_path = "#{@process_dir}/#{slide[:src]}" @@ -792,6 +880,7 @@ def process_presentation(package_dir) # Current pan/zoom state current_x_offset = current_y_offset = 0.0 current_width_ratio = current_height_ratio = 100.0 + current_x_camera = current_y_camera = current_zoom = 0.0 # Current cursor status cursor_x = cursor_y = -1.0 cursor_visible = false @@ -802,6 +891,8 @@ def process_presentation(package_dir) panzooms = [] cursors = [] shapes = {} + tldraw = BigBlueButton::Events.check_for_tldraw_events(@doc) + tldraw_shapes = {} # Iterate through the events.xml and store the events, building the # xml files as we go @@ -836,6 +927,12 @@ def process_presentation(package_dir) current_height_ratio = event.at_xpath('heightRatio').text.to_f 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' deskshare = slide_changed = true if @presentation_props['include_deskshare'] @@ -848,7 +945,10 @@ def process_presentation(package_dir) when 'AddShapeEvent', 'ModifyTextEvent' 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) when 'ClearPageEvent', 'ClearWhiteboardEvent' @@ -887,7 +987,7 @@ def process_presentation(package_dir) else if slide slide[:out] = timestamp - svg_render_image(svg, slide, shapes) + svg_render_image(svg, slide, shapes, tldraw, tldraw_shapes) end BigBlueButton.logger.info("Presentation #{current_presentation} Slide #{current_slide} Deskshare #{deskshare}") @@ -897,7 +997,7 @@ def process_presentation(package_dir) in: timestamp, deskshare: deskshare, } - events_get_image_info(slide) + events_get_image_info(slide, tldraw) slides << slide end end @@ -909,40 +1009,59 @@ def process_presentation(package_dir) slide_width = slide[:width] slide_height = slide[:height] if panzoom && + (panzoom[:deskshare] == deskshare) && + ((!tldraw && (panzoom[:x_offset] == current_x_offset) && (panzoom[:y_offset] == current_y_offset) && (panzoom[:width_ratio] == current_width_ratio) && (panzoom[:height_ratio] == current_height_ratio) && (panzoom[:width] == slide_width) && - (panzoom[:height] == slide_height) && - (panzoom[:deskshare] == deskshare) + (panzoom[:height] == slide_height)) || + (tldraw && + (panzoom[:x_camera] == current_x_camera) && + (panzoom[:y_camera] == current_y_camera) && + (panzoom[:zoom] == current_zoom)) + ) BigBlueButton.logger.info('Panzoom: skipping, no changes') panzoom_changed = false else if panzoom 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 - 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 end end # Perform cursor finalization if cursor_changed || panzoom_changed - unless cursor_x >= 0 && cursor_x <= 100 && - cursor_y >= 0 && cursor_y <= 100 - cursor_visible = false + if !tldraw + unless cursor_x >= 0 && cursor_x <= 100 && + cursor_y >= 0 && cursor_y <= 100 + cursor_visible = false + end end panzoom = panzooms.last @@ -955,7 +1074,7 @@ def process_presentation(package_dir) else if cursor cursor[:out] = timestamp - cursors_emit_event(cursors_rec, cursor) + cursors_emit_event(cursors_rec, cursor, tldraw) end cursor = { visible: cursor_visible, @@ -972,26 +1091,27 @@ def process_presentation(package_dir) # Add the last slide, panzoom, and cursor slide = slides.last slide[:out] = last_timestamp - svg_render_image(svg, slide, shapes) + svg_render_image(svg, slide, shapes, tldraw, tldraw_shapes) panzoom = panzooms.last panzoom[:out] = last_timestamp - panzooms_emit_event(panzooms_rec, panzoom) + panzooms_emit_event(panzooms_rec, panzoom, tldraw) cursor = cursors.last 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.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.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 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}/#{@cursor_xml_filename}", cursors_doc.target!) + generate_json_file(package_dir, @tldraw_shapes_filename, tldraw_shapes) if tldraw end def process_chat_messages(events, bbb_props) @@ -1170,6 +1290,7 @@ end @panzooms_xml_filename = 'panzooms.xml' @cursor_xml_filename = 'cursor.xml' @deskshare_xml_filename = 'deskshare.xml' +@tldraw_shapes_filename = 'tldraw.json' @svg_shape_id = 1 @svg_shape_unique_id = 1