bigbluebutton-Github/record-and-playback/presentation/scripts/publish/presentation.rb
Calvin Walton 1654f26084 recording: Anonymize names in chat
Move the handling of chat events into the shared library so it can be
used by multiple recording formats.

The anonymization of names is based on the external user id, if
available, so users have a consistent name through the meeting. Note
that no effort is made to edit chat messages - if someone is mentioned
by name in a chat message, that will still be visible.

Default settings for anonymization can be controlled in
bigbluebutton.yml, and per-meeting overrides can be done using meta
parameters on the create call.
2021-09-02 15:02:27 -04:00

1474 lines
51 KiB
Ruby
Executable File

# Set encoding to utf-8
# encoding: UTF-8
#
# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
#
# Copyright (c) 2012 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/>.
#
performance_start = Time.now
require File.expand_path('../../../lib/recordandplayback', __FILE__)
require 'rubygems'
require 'trollop'
require 'yaml'
require 'builder'
require 'fastimage' # require fastimage to get the image size of the slides (gem install fastimage)
require 'json'
# This script lives in scripts/archive/steps while properties.yaml lives in scripts/
bbb_props = BigBlueButton.read_props
$presentation_props = YAML::load(File.read('presentation.yml'))
# There's a couple of places where stuff is mysteriously divided or multiplied
# by 2. This is just here to call out how spooky that is.
$magic_mystery_number = 2
def scaleToDeskshareVideo(width, height)
deskshare_video_height = $presentation_props['deskshare_output_height'].to_f
deskshare_video_width = $presentation_props['deskshare_output_height'].to_f
scale = [deskshare_video_width/width, deskshare_video_height/height]
video_width = width * scale.min
video_height = height * scale.min
return video_width.floor, video_height.floor
end
def getDeskshareVideoDimension(deskshare_stream_name)
video_width = $presentation_props['deskshare_output_height'].to_f
video_height = $presentation_props['deskshare_output_height'].to_f
deskshare_video_filename = "#{$deskshare_dir}/#{deskshare_stream_name}"
if File.exist?(deskshare_video_filename)
video_info = BigBlueButton::EDL::Video.video_info(deskshare_video_filename)
video_width, video_height = scaleToDeskshareVideo(video_info[:width], video_info[:height])
else
BigBlueButton.logger.error("Could not find deskshare video: #{deskshare_video_filename}")
end
return video_width, video_height
end
#
# Calculate the offsets based on the start and stop recording events, so it's easier
# to translate the timestamps later based on these offsets
#
def calculateRecordEventsOffset
accumulated_duration = 0
previous_stop_recording = $meeting_start.to_f
$rec_events.each do |event|
event[:offset] = event[:start_timestamp] - accumulated_duration
event[:duration] = event[:stop_timestamp] - event[:start_timestamp]
event[:accumulated_duration] = accumulated_duration
previous_stop_recording = event[:stop_timestamp]
accumulated_duration += event[:duration]
end
end
#
# Translated an arbitrary Unix timestamp to the recording timestamp. This is the
# function that others will call
#
def translateTimestamp(timestamp)
new_timestamp = translateTimestamp_helper(timestamp.to_f).to_f
# BigBlueButton.logger.info("Translating #{timestamp}, old value=#{timestamp.to_f-$meeting_start.to_f}, new value=#{new_timestamp}")
new_timestamp
end
#
# Translated an arbitrary Unix timestamp to the recording timestamp
#
def translateTimestamp_helper(timestamp)
$rec_events.each do |event|
# if the timestamp comes before the start recording event, then the timestamp is translated to the moment it starts recording
if timestamp <= event[:start_timestamp]
return event[:start_timestamp] - event[:offset]
# if the timestamp is during the recording period, it is just translated to the new one using the offset
elsif timestamp > event[:start_timestamp] and timestamp <= event[:stop_timestamp]
return timestamp - event[:offset]
end
end
# if the timestamp comes after the last stop recording event, then the timestamp is translated to the last stop recording event timestamp
return timestamp - $rec_events.last()[:offset] + $rec_events.last()[:duration]
end
def color_to_hex(color)
color = color.to_i.to_s(16)
return '0'*(6-color.length) + color
end
def shape_scale_width(slide, x)
return (x / 100.0 * slide[:width]).round(5)
end
def shape_scale_height(slide, y)
return (y / 100.0 * slide[:height]).round(5)
end
def shape_thickness(slide, shape)
if !shape[:thickness_percent].nil?
return shape_scale_width(slide, shape[:thickness_percent])
else
return shape[:thickness]
end
end
def svg_render_shape_pencil(g, slide, shape)
g['shape'] = "pencil#{shape[:shape_unique_id]}"
doc = g.document
if shape[:data_points].length < 2
BigBlueButton.logger.warn("Pencil #{shape[:shape_unique_id]} doesn't have enough points")
return
end
if shape[:data_points].length == 2
BigBlueButton.logger.info("Pencil #{shape[:shape_unique_id]}: Drawing single point")
g['style'] = "stroke:none;fill:##{shape[:color]};visibility:hidden"
circle = doc.create_element('circle',
cx: shape_scale_width(slide, shape[:data_points][0]),
cy: shape_scale_height(slide, shape[:data_points][1]),
r: (shape_thickness(slide, shape) / 2.0).round(5))
g << circle
else
path = []
data_points = shape[:data_points].each
if !shape[:commands].nil?
BigBlueButton.logger.info("Pencil #{shape[:shape_unique_id]}: Drawing from command string (#{shape[:commands].length} commands)")
shape[:commands].each do |command|
case command
when 1 # MOVE_TO
x = shape_scale_width(slide, data_points.next)
y = shape_scale_height(slide, data_points.next)
path.push("M#{x} #{y}")
when 2 # LINE_TO
x = shape_scale_width(slide, data_points.next)
y = shape_scale_height(slide, data_points.next)
path.push("L#{x} #{y}")
when 3 # Q_CURVE_TO
cx1 = shape_scale_width(slide, data_points.next)
cy1 = shape_scale_height(slide, data_points.next)
x = shape_scale_width(slide, data_points.next)
y = shape_scale_height(slide, data_points.next)
path.push("Q#{cx1} #{cy2},#{x} #{y}")
when 4 # C_CURVE_TO
cx1 = shape_scale_width(slide, data_points.next)
cy1 = shape_scale_height(slide, data_points.next)
cx2 = shape_scale_width(slide, data_points.next)
cy2 = shape_scale_height(slide, data_points.next)
x = shape_scale_width(slide, data_points.next)
y = shape_scale_height(slide, data_points.next)
path.push("C#{cx1} #{cy1},#{cx2} #{cy2},#{x} #{y}")
else
raise "Unknown pencil command: #{command}"
end
end
else
BigBlueButton.logger.info("Pencil #{shape[:shape_unique_id]}: Drawing simple line (#{shape[:data_points].length / 2} points)")
x = shape_scale_width(slide, data_points.next)
y = shape_scale_height(slide, data_points.next)
path << "M#{x} #{y}"
begin
while true
x = shape_scale_width(slide, data_points.next)
y = shape_scale_height(slide, data_points.next)
path << "L#{x} #{y}"
end
rescue StopIteration
end
end
path = path.join('')
g['style'] = "stroke:##{shape[:color]};stroke-linecap:round;stroke-linejoin:round;stroke-width:#{shape_thickness(slide,shape)};visibility:hidden;fill:none"
svg_path = doc.create_element('path', d: path)
g << svg_path
end
end
def svg_render_shape_line(g, slide, shape)
g['shape'] = "line#{shape[:shape_unique_id]}"
g['style'] = "stroke:##{shape[:color]};stroke-width:#{shape_thickness(slide,shape)};visibility:hidden;fill:none"
if $version_atleast_2_0_0
g['style'] += ";stroke-linecap:butt"
else
g['style'] += ";stroke-linecap:round"
end
doc = g.document
data_points = shape[:data_points]
line = doc.create_element('line',
x1: shape_scale_width(slide, data_points[0]),
y1: shape_scale_height(slide, data_points[1]),
x2: shape_scale_width(slide, data_points[2]),
y2: shape_scale_height(slide, data_points[3]))
g << line
end
def svg_render_shape_rect(g, slide, shape)
g['shape'] = "rect#{shape[:shape_unique_id]}"
g['style'] = "stroke:##{shape[:color]};stroke-width:#{shape_thickness(slide,shape)};visibility:hidden;fill:none"
if $version_atleast_2_0_0
g['style'] += ";stroke-linejoin:miter"
else
g['style'] += ";stroke-linejoin:round"
end
doc = g.document
data_points = shape[:data_points]
x1 = shape_scale_width(slide, data_points[0])
y1 = shape_scale_height(slide, data_points[1])
x2 = shape_scale_width(slide, data_points[2])
y2 = shape_scale_height(slide, data_points[3])
width = (x2 - x1).abs
height = (y2 - y1).abs
if shape[:square]
# Convert to a square, keeping aligned with the start point.
if x2 > x1
y2 = y1 + width
else
# This replicates a bug in the BigBlueButton flash client
BigBlueButton.logger.info("Rect #{shape[:shape_unique_id]} reversed square bug")
y2 = y1 - width
end
end
path = doc.create_element('path',
d: "M#{x1} #{y1}L#{x2} #{y1}L#{x2} #{y2}L#{x1} #{y2}Z")
g << path
end
def svg_render_shape_triangle(g, slide, shape)
g['shape'] = "triangle#{shape[:shape_unique_id]}"
g['style'] = "stroke:##{shape[:color]};stroke-width:#{shape_thickness(slide,shape)};visibility:hidden;fill:none"
if $version_atleast_2_0_0
g['style'] += ";stroke-linejoin:miter;stroke-miterlimit:8"
else
g['style'] += ";stroke-linejoin:round"
end
doc = g.document
data_points = shape[:data_points]
x1 = shape_scale_width(slide, data_points[0])
y1 = shape_scale_height(slide, data_points[1])
x2 = shape_scale_width(slide, data_points[2])
y2 = shape_scale_height(slide, data_points[3])
px = ((x1 + x2) / 2.0).round(5)
path = doc.create_element('path',
d: "M#{px} #{y1}L#{x2} #{y2}L#{x1} #{y2}Z")
g << path
end
def svg_render_shape_ellipse(g, slide, shape)
g['shape'] = "ellipse#{shape[:shape_unique_id]}"
g['style'] = "stroke:##{shape[:color]};stroke-width:#{shape_thickness(slide,shape)};visibility:hidden;fill:none"
doc = g.document
data_points = shape[:data_points]
x1 = shape_scale_width(slide, data_points[0])
y1 = shape_scale_height(slide, data_points[1])
x2 = shape_scale_width(slide, data_points[2])
y2 = shape_scale_height(slide, data_points[3])
width_r = ((x2 - x1).abs / 2.0).round(5)
height_r = ((y2 - y1).abs / 2.0).round(5)
hx = ((x1 + x2) / 2.0).round(5)
hy = ((y1 + y2) / 2.0).round(5)
if shape[:circle]
# Convert to a circle, keeping aligned with the start point
height_r = width_r
if x2 > x1
y2 = y1 + height_r + height_r
else
# This replicates a bug in the BigBlueButton flash client
BigBlueButton.logger.info("Ellipse #{shape[:shape_unique_id]} reversed circle bug")
y2 = y1 - height_r - height_r
end
end
# Normalize the x,y coordinates
x1, x2 = x2, x1 if x1 > x2
y1, y2 = y2, y1 if y1 > y2
# SVG's ellipse element doesn't render if r_x or r_y is 0, but
# we want to display a line segment in that case. But the SVG
# path element's elliptical arc code renders r_x or r_y
# degenerate cases as line segments, so we can use that.
path = "M#{x1} #{hy}"
path += "A#{width_r} #{height_r} 0 0 1 #{hx} #{y1}"
path += "A#{width_r} #{height_r} 0 0 1 #{x2} #{hy}"
path += "A#{width_r} #{height_r} 0 0 1 #{hx} #{y2}"
path += "A#{width_r} #{height_r} 0 0 1 #{x1} #{hy}"
path += "Z"
svg_path = doc.create_element('path', d: path)
g << svg_path
end
def svg_render_shape_text(g, slide, shape)
g['shape'] = "text#{shape[:shape_unique_id]}"
doc = g.document
data_points = shape[:data_points]
x = shape_scale_width(slide, data_points[0])
y = shape_scale_height(slide, data_points[1])
width = shape_scale_width(slide, shape[:text_box_width])
height = shape_scale_height(slide, shape[:text_box_height])
font_size = shape_scale_height(slide, shape[:calced_font_size])
BigBlueButton.logger.info("Text #{shape[:shape_unique_id]} width #{width} height #{height} font size #{font_size}")
g['style'] = "color:##{shape[:font_color]};word-wrap:break-word;visibility:hidden;font-family:Arial;font-size:#{font_size}px"
switch = doc.create_element('switch')
fo = doc.create_element('foreignObject',
width: width, height: height, x: x, y: y)
p = doc.create_element('p',
xmlns: 'http://www.w3.org/1999/xhtml', style: 'margin:0;padding:0')
shape[:text].each_line.with_index do |line, index|
if index > 0
p << doc.create_element('br')
end
p << doc.create_text_node(line.chomp)
end
fo << p
switch << fo
g << switch
end
def svg_render_shape_poll(g, slide, shape)
poll_id = shape[:shape_unique_id]
g['shape'] = "poll#{poll_id}"
g['style'] = 'visibility:hidden'
doc = g.document
data_points = shape[:data_points]
x = shape_scale_width(slide, data_points[0])
y = shape_scale_height(slide, data_points[1])
width = shape_scale_width(slide, data_points[2])
height = shape_scale_height(slide, data_points[3])
result = shape[:result]
num_responders = shape[:num_responders]
presentation = slide[:presentation]
json_file = "#{$process_dir}/poll_result#{poll_id}.json"
svg_file = "#{$process_dir}/presentation/#{presentation}/poll_result#{poll_id}.svg"
# Save the poll json to a temp file
IO.write(json_file, result)
# Render the poll svg
ret = BigBlueButton.exec_ret('utils/gen_poll_svg', '-i', json_file, '-w', "#{width.round}", '-h', "#{height.round}", '-n', "#{num_responders}", '-o', svg_file)
raise "Failed to generate poll svg" if ret != 0
# Poll image
g << doc.create_element('image',
'xlink:href' => "presentation/#{presentation}/poll_result#{poll_id}.svg",
width: width, height: height, x: x, y: y)
end
def svg_render_shape(canvas, slide, shape, image_id)
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] or
(!shape[:out].nil? and 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
BigBlueButton.logger.info("Draw #{shape[:shape_id]} Shape #{shape[:shape_unique_id]} Type #{shape[:type]} from #{shape[:in]} to #{shape[:out]} undo #{shape[:undo]}")
doc = canvas.document
g = doc.create_element('g',
id: "image#{image_id}-draw#{shape[:shape_id]}", class: 'shape',
timestamp: shape[:in], undo: (shape[:undo].nil? ? -1 : shape[:undo]))
case shape[:type]
when 'pencil'
svg_render_shape_pencil(g, slide, shape)
when 'line'
svg_render_shape_line(g, slide, shape)
when 'rectangle'
svg_render_shape_rect(g, slide, shape)
when 'triangle'
svg_render_shape_triangle(g, slide, shape)
when 'ellipse'
svg_render_shape_ellipse(g, slide, shape)
when 'text'
svg_render_shape_text(g, slide, shape)
when 'poll_result'
svg_render_shape_poll(g, slide, shape)
else
BigBlueButton.logger.warn("Ignoring unhandled shape type #{shape[:type]}")
end
g[:shape] = "image#{image_id}-#{g[:shape]}"
if g.element_children.length > 0
canvas << g
end
end
$svg_image_id = 1
def svg_render_image(svg, slide, shapes)
if slide[:in] == slide[:out]
BigBlueButton.logger.info("Presentation #{slide[:presentation]} Slide #{slide[:slide]} is never shown (duration rounds to 0)")
return
end
image_id = $svg_image_id
$svg_image_id += 1
BigBlueButton.logger.info("Image #{image_id}: Presentation #{slide[:presentation]} Slide #{slide[:slide]} Deskshare #{slide[:deskshare]} from #{slide[:in]} to #{slide[:out]}")
doc = svg.document
image = doc.create_element('image',
id: "image#{image_id}", class: 'slide',
in: slide[:in], out: slide[:out],
'xlink:href' => slide[:src],
width: slide[:width], height: slide[:height], x: 0, y: 0,
style: 'visibility:hidden')
image['text'] = slide[:text] if !slide[:text].nil?
svg << image
if slide[:deskshare] or
shapes[slide[:presentation]].nil? or
shapes[slide[:presentation]][slide[:slide]].nil?
return
end
shapes = shapes[slide[:presentation]][slide[:slide]]
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)
end
if canvas.element_children.length > 0
svg << canvas
end
end
def panzoom_viewbox(panzoom)
if panzoom[:deskshare]
panzoom[:x_offset] = 0.0
panzoom[:y_offset] = 0.0
panzoom[:width_ratio] = 100.0
panzoom[:height_ratio] = 100.0
end
x = (-panzoom[:x_offset] * $magic_mystery_number / 100.0 * panzoom[:width]).round(5)
y = (-panzoom[:y_offset] * $magic_mystery_number / 100.0 * panzoom[:height]).round(5)
w = shape_scale_width(panzoom, panzoom[:width_ratio])
h = shape_scale_height(panzoom, panzoom[:height_ratio])
return [x, y, w, h]
end
def panzooms_emit_event(rec, panzoom)
if panzoom[:in] == panzoom[:out]
BigBlueButton.logger.info("Panzoom: not emitting, duration rounds to 0")
return
end
doc = rec.document
event = doc.create_element('event', timestamp: panzoom[:in])
x, y, w, h = panzoom_viewbox(panzoom)
viewbox = doc.create_element('viewBox')
viewbox.content = "#{x} #{y} #{w} #{h}"
BigBlueButton.logger.info("Panzoom viewbox #{viewbox.content} at #{panzoom[:in]}")
event << viewbox
rec << event
end
def cursors_emit_event(rec, cursor)
if cursor[:in] == cursor[:out]
BigBlueButton.logger.info("Cursor: not emitting, duration rounds to 0")
return
end
doc = rec.document
event = doc.create_element('event', timestamp: cursor[:in])
panzoom = cursor[:panzoom]
if cursor[:visible]
if $version_atleast_2_0_0
# 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 = (((cursor[:x] / 100.0) + (panzoom[:x_offset] * $magic_mystery_number / 100.0)) /
(panzoom[:width_ratio] / 100.0)).round(5)
y = (((cursor[:y] / 100.0) + (panzoom[:y_offset] * $magic_mystery_number / 100.0)) /
(panzoom[:height_ratio] / 100.0)).round(5)
if x < 0 or x > 1 or y < 0 or y > 1
x = -1.0
y = -1.0
end
else
# Cursor position is relative to the visible area
x = cursor[:x].round(5)
y = cursor[:y].round(5)
end
else
x = -1.0
y = -1.0
end
cursor_e = doc.create_element('cursor')
cursor_e.content = "#{x} #{y}"
BigBlueButton.logger.info("Cursor #{cursor_e.content} at #{cursor[:in]}")
event << cursor_e
rec << event
end
$svg_shape_id = 1
$svg_shape_unique_id = 1
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
presentation = event.at_xpath('presentation')
slide = event.at_xpath('pageNumber')
if presentation.nil?
presentation = current_presentation
else
presentation = presentation.text
end
if slide.nil?
slide = current_slide
else
slide = slide.text.to_i
slide -=1 unless $version_atleast_0_9_0
end
# Set up the shapes data structures if needed
shapes[presentation] = {} if shapes[presentation].nil?
shapes[presentation][slide] = [] if shapes[presentation][slide].nil?
# 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[:type] = event.at_xpath('type').text
shape[:data_points] = event.at_xpath('dataPoints').text.split(',').map { |p| p.to_f }
# These can be missing in old BBB versions, there are fallbacks
user_id = event.at_xpath('userId')
shape[:user_id] = user_id.text if !user_id.nil?
shape_id = event.at_xpath('id')
shape[:id] = shape_id.text if !shape_id.nil?
status = event.at_xpath('status')
shape[:status] = status.text if !status.nil?
shape[:shape_id] = $svg_shape_id
$svg_shape_id += 1
# Some shape-specific properties
if shape[:type] == 'pencil' or shape[:type] == 'rectangle' or
shape[:type] == 'ellipse' or shape[:type] == 'triangle' or
shape[:type] == 'line'
shape[:color] = color_to_hex(event.at_xpath('color').text)
thickness = event.at_xpath('thickness')
unless thickness
BigBlueButton.logger.warn("Draw #{shape[:shape_id]} Shape #{shape[:shape_unique_id]} ID #{shape[:id]} is missing thickness")
return
end
if $version_atleast_2_0_0
shape[:thickness_percent] = thickness.text.to_f
else
shape[:thickness] = thickness.text.to_i
end
end
if shape[:type] == 'rectangle'
square = event.at_xpath('square')
if !square.nil?
shape[:square] = (square.text == 'true')
end
end
if shape[:type] == 'ellipse'
circle = event.at_xpath('circle')
if !circle.nil?
shape[:circle] = (circle.text == 'true')
end
end
if shape[:type] == 'pencil'
commands = event.at_xpath('commands')
if !commands.nil?
shape[:commands] = commands.text.split(',').map { |c| c.to_i }
end
end
if shape[:type] == 'poll_result'
shape[:num_responders] = event.at_xpath('num_responders').text.to_i
shape[:num_respondents] = event.at_xpath('num_respondents').text.to_i
shape[:result] = event.at_xpath('result').text
end
if shape[:type] == 'text'
shape[:text_box_width] = event.at_xpath('textBoxWidth').text.to_f
shape[:text_box_height] = event.at_xpath('textBoxHeight').text.to_f
calced_font_size = event.at_xpath('calcedFontSize')
unless calced_font_size
BigBlueButton.logger.warn("Draw #{shape[:shape_id]} Shape #{shape[:shape_unique_id]} ID #{shape[:id]} is missing calcedFontSize")
return
end
shape[:calced_font_size] = calced_font_size.text.to_f
shape[:font_color] = color_to_hex(event.at_xpath('fontColor').text)
text = event.at_xpath('text')
if !text.nil?
shape[:text] = text.text
else
shape[:text] = ''
end
end
# Find the previous shape, for updates
prev_shape = nil
if !shape[:id].nil?
# If we have a shape ID, look up the previous shape by ID
prev_shape = shapes.find_all {|s| s[:id] == shape[:id] }.last
else
# No shape ID, so do heuristic matching. If the previous shape had the
# same type and same first two data points, update it.
last_shape = shapes.last
if last_shape[:type] == shape[:type] and
last_shape[:data_points][0] == shape[:data_points][0] and
last_shape[:data_points][1] == shape[:data_points][1]
prev_shape = last_shape
end
end
if !prev_shape.nil?
prev_shape[:out] = timestamp
shape[:shape_unique_id] = prev_shape[:shape_unique_id]
if shape[:type] == 'pencil' and shape[:status] == 'DRAW_UPDATE'
# BigBlueButton 2.0 quirk - 'DRAW_UPDATE' events on pencil tool only
# include newly added points, rather than the full list.
shape[:data_points] = prev_shape[:data_points] + shape[:data_points]
end
else
shape[:shape_unique_id] = $svg_shape_unique_id
$svg_shape_unique_id += 1
end
BigBlueButton.logger.info("Draw #{shape[:shape_id]} Shape #{shape[:shape_unique_id]} ID #{shape[:id]} Type #{shape[:type]}")
shapes << shape
end
def events_parse_undo(shapes, event, current_presentation, current_slide, timestamp)
# Figure out what presentation+slide this undo is for, with fallbacks
# for old BBB where this info isn't in the undo messages
presentation = event.at_xpath('presentation')
slide = event.at_xpath('pageNumber')
if presentation.nil?
presentation = current_presentation
else
presentation = presentation.text
end
if slide.nil?
slide = current_slide
else
slide = slide.text.to_i
slide -=1 unless $version_atleast_0_9_0
end
# Newer undo messages have the shape id, making this a lot easier
shape_id = event.at_xpath('shapeId')
if !shape_id.nil?
shape_id = shape_id.text
end
# Set up the shapes data structures if needed
shapes[presentation] = {} if shapes[presentation].nil?
shapes[presentation][slide] = [] if shapes[presentation][slide].nil?
# We only need to deal with shapes for this slide
shapes = shapes[presentation][slide]
if !shape_id.nil?
# If we have the shape id, we simply have to update the undo time on
# all the shapes with that id.
BigBlueButton.logger.info("Undo: removing shape with ID #{shape_id} at #{timestamp}")
shapes.each do |shape|
if shape[:id] == shape_id
if shape[:undo].nil? or shape[:undo] > timestamp
shape[:undo] = timestamp
end
end
end
else
# The undo command removes the most recently added shape that has not
# already been removed by another undo or clear. Find that shape.
undo_shape = shapes.select { |s| s[:undo].nil? }.last
if !undo_shape.nil?
BigBlueButton.logger.info("Undo: removing Shape #{undo_shape[:shape_unique_id]} at #{timestamp}")
# We have an id number assigned to associate all the updated versions
# of the same shape. Use that to determine which shapes to apply undo
# times to.
shapes.each do |shape|
if shape[:shape_unique_id] == undo_shape[:shape_unique_id]
if shape[:undo].nil? or shape[:undo] > timestamp
shape[:undo] = timestamp
end
end
end
else
BigBlueButton.logger.info("Undo: no applicable shapes found")
end
end
end
def events_parse_clear(shapes, event, current_presentation, current_slide, timestamp)
# Figure out what presentation+slide this clear is for, with fallbacks
# for old BBB where this info isn't in the clear messages
presentation = event.at_xpath('presentation')
slide = event.at_xpath('pageNumber')
if presentation.nil?
presentation = current_presentation
else
presentation = presentation.text
end
if slide.nil?
slide = current_slide
else
slide = slide.text.to_i
slide -=1 unless $version_atleast_0_9_0
end
# BigBlueButton 2.0 per-user clear features
full_clear = event.at_xpath('fullClear')
if !full_clear.nil?
full_clear = (full_clear.text == 'true')
else
# Default to full clear on older versions
full_clear = true
end
user_id = event.at_xpath('userId')
if !user_id.nil?
user_id = user_id.text
end
# Set up the shapes data structures if needed
shapes[presentation] = {} if shapes[presentation].nil?
shapes[presentation][slide] = [] if shapes[presentation][slide].nil?
# We only need to deal with shapes for this slide
shapes = shapes[presentation][slide]
if full_clear
BigBlueButton.logger.info("Clear: removing all shapes")
else
BigBlueButton.logger.info("Clear: removing shapes for User #{user_id}")
end
shapes.each do |shape|
if full_clear or user_id == shape[:user_id]
if shape[:undo].nil? or shape[:undo] > timestamp
shape[:undo] = timestamp
end
end
end
end
def events_get_image_info(slide)
if slide[:deskshare]
slide[:src] = 'presentation/deskshare.png'
elsif slide[:presentation] == ''
slide[:src] = 'presentation/logo.png'
else
slide[:src] = "presentation/#{slide[:presentation]}/slide-#{slide[:slide] + 1}.png"
slide[:text] = "presentation/#{slide[:presentation]}/textfiles/slide-#{slide[:slide] + 1}.txt"
end
image_path = "#{$process_dir}/#{slide[:src]}"
unless File.exist?(image_path)
BigBlueButton.logger.warn("Missing image file #{image_path}!")
# Emergency last-ditch blank image creation
FileUtils.mkdir_p(File.dirname(image_path))
command = \
if slide[:deskshare]
['convert', '-size', "#{$presentation_props['deskshare_output_width']}x#{$presentation_props['deskshare_output_height']}", 'xc:transparent', '-background', 'transparent', image_path]
else
['convert', '-size', '1600x1200', 'xc:transparent', '-background', 'transparent', '-quality', '90', '+dither', '-depth', '8', '-colors', '256', image_path]
end
BigBlueButton.exec_ret(*command) || raise("Unable to generate blank image for #{image_path}")
end
slide[:width], slide[:height] = FastImage.size(image_path)
BigBlueButton.logger.info("Image size is #{slide[:width]}x#{slide[:height]}")
end
# Create the shapes.svg, cursors.xml, and panzooms.xml files used for
# rendering the presentation area
def processPresentation(package_dir)
shapes_doc = Nokogiri::XML::Document.new()
shapes_doc.create_internal_subset('svg', '-//W3C//DTD SVG 1.1//EN',
'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd')
svg = shapes_doc.root = shapes_doc.create_element('svg',
id: 'svgfile',
style: 'position:absolute;height:600px;width:800px',
xmlns: 'http://www.w3.org/2000/svg',
'xmlns:xlink' => 'http://www.w3.org/1999/xlink',
version: '1.1',
viewBox: '0 0 800 600')
panzooms_doc = Nokogiri::XML::Document.new()
panzooms_rec = panzooms_doc.root = panzooms_doc.create_element('recording',
id: 'panzoom_events')
cursors_doc = Nokogiri::XML::Document.new()
cursors_rec = cursors_doc.root = cursors_doc.create_element('recording',
id: 'cursor_events')
# Current presentation/slide state
current_presentation_slide = {}
current_presentation = ''
current_slide = 0
# Current pan/zoom state
current_x_offset = 0.0
current_y_offset = 0.0
current_width_ratio = 100.0
current_height_ratio = 100.0
# Current cursor status
cursor_x = -1.0
cursor_y = -1.0
cursor_visible = false
presenter = nil
# Current deskshare state (affects presentation and pan/zoom)
deskshare = false
slides = []
panzooms = []
cursors = []
shapes = {}
# Iterate through the events.xml and store the events, building the
# xml files as we go
last_timestamp = 0.0
events_xml = Nokogiri::XML(File.read("#{$process_dir}/events.xml"))
events_xml.xpath('/recording/event').each do |event|
eventname = event['eventname']
last_timestamp = timestamp =
(translateTimestamp(event['timestamp']) / 1000.0).round(1)
# Make sure to add initial entries to the slide & panzoom lists
slide_changed = slides.empty?
panzoom_changed = panzooms.empty?
cursor_changed = cursors.empty?
# Do event specific processing
if eventname == 'SharePresentationEvent'
current_presentation = event.at_xpath('presentationName').text
current_slide = current_presentation_slide[current_presentation].to_i
slide_changed = true
panzoom_changed = true
elsif eventname == 'GotoSlideEvent'
current_slide = event.at_xpath('slide').text.to_i
current_presentation_slide[current_presentation] = current_slide
slide_changed = true
panzoom_changed = true
elsif eventname == 'ResizeAndMoveSlideEvent'
current_x_offset = event.at_xpath('xOffset').text.to_f
current_y_offset = event.at_xpath('yOffset').text.to_f
current_width_ratio = event.at_xpath('widthRatio').text.to_f
current_height_ratio = event.at_xpath('heightRatio').text.to_f
panzoom_changed = true
elsif $presentation_props['include_deskshare'] and (eventname == 'DeskshareStartedEvent' or eventname == 'StartWebRTCDesktopShareEvent')
deskshare = true
slide_changed = true
elsif $presentation_props['include_deskshare'] and (eventname == 'DeskshareStoppedEvent' or eventname == 'StopWebRTCDesktopShareEvent')
deskshare = false
slide_changed = true
elsif eventname == 'AddShapeEvent' or eventname == 'ModifyTextEvent'
events_parse_shape(shapes, event, current_presentation, current_slide, timestamp)
elsif eventname == 'UndoShapeEvent' or eventname == 'UndoAnnotationEvent'
events_parse_undo(shapes, event, current_presentation, current_slide, timestamp)
elsif eventname == 'ClearPageEvent' or eventname == 'ClearWhiteboardEvent'
events_parse_clear(shapes, event, current_presentation, current_slide, timestamp)
elsif eventname == 'AssignPresenterEvent'
# Move cursor offscreen on presenter switch, it'll reappear if the new
# presenter moves it
presenter = event.at_xpath('userid').text
cursor_visible = false
cursor_changed = true
elsif eventname == 'CursorMoveEvent'
cursor_x = event.at_xpath('xOffset').text.to_f
cursor_y = event.at_xpath('yOffset').text.to_f
cursor_visible = true
cursor_changed = true
elsif eventname == 'WhiteboardCursorMoveEvent'
user_id = event.at_xpath('userId')
# Only draw cursor for current presentor. TODO multi-cursor support
if user_id.nil? or user_id.text == presenter
cursor_x = event.at_xpath('xOffset').text.to_f
cursor_y = event.at_xpath('yOffset').text.to_f
cursor_visible = true
cursor_changed = true
end
end
# Perform slide finalization
if slide_changed
slide = slides.last
if !slide.nil? and
slide[:presentation] == current_presentation and
slide[:slide] == current_slide and
slide[:deskshare] == deskshare
BigBlueButton.logger.info('Presentation/Slide: skipping, no changes')
slide_changed = false
else
if !slide.nil?
slide[:out] = timestamp
svg_render_image(svg, slide, shapes)
end
BigBlueButton.logger.info("Presentation #{current_presentation} Slide #{current_slide} Deskshare #{deskshare}")
slide = {
presentation: current_presentation,
slide: current_slide,
in: timestamp,
deskshare: deskshare
}
events_get_image_info(slide)
slides << slide
end
end
# Perform panzoom finalization
if panzoom_changed
slide = slides.last
panzoom = panzooms.last
if !panzoom.nil? and
panzoom[:x_offset] == current_x_offset and
panzoom[:y_offset] == current_y_offset and
panzoom[:width_ratio] == current_width_ratio and
panzoom[:height_ratio] == current_height_ratio and
panzoom[:width] == slide[:width] and
panzoom[:height] == slide[:height] and
panzoom[:deskshare] == deskshare
BigBlueButton.logger.info('Panzoom: skipping, no changes')
panzoom_changed = false
else
if !panzoom.nil?
panzoom[:out] = timestamp
panzooms_emit_event(panzooms_rec, panzoom)
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 or panzoom_changed
unless cursor_x >= 0 and cursor_x <= 100 and
cursor_y >= 0 and cursor_y <= 100
cursor_visible = false
end
panzoom = panzooms.last
cursor = cursors.last
if !cursor.nil? and
((!cursor[:visible] and !cursor_visible) or
(cursor[:x] == cursor_x and cursor[:y] == cursor_y)) and
!panzoom_changed
BigBlueButton.logger.info('Cursor: skipping, no changes')
else
if !cursor.nil?
cursor[:out] = timestamp
cursors_emit_event(cursors_rec, cursor)
end
BigBlueButton.logger.info("Cursor: visible #{cursor_visible}, #{cursor_x} #{cursor_y} (#{panzoom[:width]}x#{panzoom[:height]})")
cursor = {
visible: cursor_visible,
x: cursor_x,
y: cursor_y,
panzoom: panzoom,
in: timestamp,
}
cursors << cursor
end
end
end
# Add the last slide, panzoom, and cursor
slide = slides.last
slide[:out] = last_timestamp
svg_render_image(svg, slide, shapes)
panzoom = panzooms.last
panzoom[:out] = last_timestamp
panzooms_emit_event(panzooms_rec, panzoom)
cursor = cursors.last
cursor[:out] = last_timestamp
cursors_emit_event(cursors_rec, cursor)
# And save the result
File.write("#{package_dir}/#{$shapes_svg_filename}", shapes_doc.to_xml)
File.write("#{package_dir}/#{$panzooms_xml_filename}", panzooms_doc.to_xml)
File.write("#{package_dir}/#{$cursor_xml_filename}", cursors_doc.to_xml)
end
def processChatMessages(events, bbb_props)
BigBlueButton.logger.info("Processing chat events")
# Create slides.xml and chat.
Nokogiri::XML::Builder.new do |xml|
xml.popcorn {
BigBlueButton::Events.get_chat_events(events, $meeting_start.to_i, $meeting_end.to_i, bbb_props).each do |chat|
chattimeline = {
in: (chat[:in] / 1000.0).round(1),
direction: 'down',
name: chat[:sender],
message: chat[:message],
target: 'chat'
}
chattimeline[:out] = (chat[:out] / 1000.0).round(1) unless chat[:out].nil?
xml.chattimeline(**chattimeline)
end
}
end
end
def processDeskshareEvents(events)
BigBlueButton.logger.info("Processing deskshare events")
deskshare_matched_events = BigBlueButton::Events.get_matched_start_and_stop_deskshare_events(events)
$deskshare_xml = Nokogiri::XML::Builder.new do |xml|
$xml = xml
$xml.recording('id' => 'deskshare_events') do
deskshare_matched_events.each do |event|
start_timestamp = (translateTimestamp(event[:start_timestamp].to_f) / 1000).round(1)
stop_timestamp = (translateTimestamp(event[:stop_timestamp].to_f) / 1000).round(1)
if (start_timestamp != stop_timestamp)
video_info = BigBlueButton::EDL::Video.video_info("#{$deskshare_dir}/#{event[:stream]}")
if !video_info[:video]
BigBlueButton.logger.warn("#{event[:stream]} is not a valid video file, skipping...")
next
end
video_width, video_height = getDeskshareVideoDimension(event[:stream])
$xml.event(:start_timestamp => start_timestamp,
:stop_timestamp => stop_timestamp,
:video_width => video_width,
:video_height => video_height)
end
end
end
end
end
def getPollQuestion(event)
question = ""
if not event.at_xpath("question").nil?
question = event.at_xpath("question").text
end
question
end
def getPollAnswers(event)
answers = []
if not event.at_xpath("answers").nil?
answers = JSON.load(event.at_xpath("answers").content)
end
answers
end
def getPollRespondents(event)
respondents = 0
if not event.at_xpath("numRespondents").nil?
respondents = event.at_xpath("numRespondents").text.to_i
end
respondents
end
def getPollResponders(event)
responders = 0
if not event.at_xpath("numResponders").nil?
responders = event.at_xpath("numResponders").text.to_i
end
responders
end
def getPollId(event)
id = ""
if not event.at_xpath("pollId").nil?
id = event.at_xpath("pollId").text
end
id
end
def getPollType(events, published_poll_event)
published_poll_id = getPollId(published_poll_event)
type = ""
events.xpath("//event[@eventname='PollStartedRecordEvent']").each do |event|
poll_id = getPollId(event)
if poll_id.eql?(published_poll_id)
type = event.at_xpath("type").text
break
end
end
type
end
def processPollEvents(events, package_dir)
BigBlueButton.logger.info("Processing poll events")
published_polls = []
$rec_events.each do |re|
events.xpath("//event[@eventname='PollPublishedRecordEvent']").each do |event|
if (event[:timestamp].to_i >= re[:start_timestamp] and event[:timestamp].to_i <= re[:stop_timestamp])
published_polls << {
:timestamp => (translateTimestamp(event[:timestamp]) / 1000).to_i,
:type => getPollType(events, event),
:question => getPollQuestion(event),
:answers => getPollAnswers(event),
:respondents => getPollRespondents(event),
:responders => getPollResponders(event)
}
end
end
end
if not published_polls.empty?
File.open("#{package_dir}/polls.json", "w") do |f|
f.puts(published_polls.to_json)
end
end
end
def processExternalVideoEvents(events, package_dir)
BigBlueButton.logger.info("Processing external video events")
# Retrieve external video events
external_video_events = BigBlueButton::Events.match_start_and_stop_external_video_events(
BigBlueButton::Events.get_start_and_stop_external_video_events(@doc))
external_videos = []
$rec_events.each do |re|
external_video_events.each do |event|
#BigBlueButton.logger.info("Processing rec event #{re} and external video event #{event}")
timestamp = (translateTimestamp(event[:start_timestamp]) / 1000).to_i
# do not add same external_video twice
if (external_videos.find {|ev| ev[:timestamp] == timestamp}.nil?)
if ((event[:start_timestamp] >= re[:start_timestamp] and event[:start_timestamp] <= re[:stop_timestamp]) ||
(event[:start_timestamp] < re[:start_timestamp] and event[:stop_timestamp] >= re[:start_timestamp]))
external_videos << {
:timestamp => timestamp,
:external_video_url => event[:external_video_url]
}
end
end
end
end
if not external_videos.empty?
File.open("#{package_dir}/external_videos.json", "w") do |f|
f.puts(external_videos.to_json)
end
end
end
$shapes_svg_filename = 'shapes.svg'
$panzooms_xml_filename = 'panzooms.xml'
$cursor_xml_filename = 'cursor.xml'
$deskshare_xml_filename = 'deskshare.xml'
opts = Trollop::options do
opt :meeting_id, "Meeting id to archive", :default => '58f4a6b3-cd07-444d-8564-59116cb53974', :type => String
end
$meeting_id = opts[:meeting_id]
puts $meeting_id
match = /(.*)-(.*)/.match $meeting_id
$meeting_id = match[1]
$playback = match[2]
puts $meeting_id
puts $playback
begin
if ($playback == "presentation")
log_dir = bbb_props['log_dir']
logger = Logger.new("#{log_dir}/presentation/publish-#{$meeting_id}.log", 'daily' )
BigBlueButton.logger = logger
BigBlueButton.logger.info("Setting recording dir")
recording_dir = bbb_props['recording_dir']
BigBlueButton.logger.info("Setting process dir")
$process_dir = "#{recording_dir}/process/presentation/#{$meeting_id}"
BigBlueButton.logger.info("setting publish dir")
publish_dir = $presentation_props['publish_dir']
BigBlueButton.logger.info("setting playback url info")
playback_protocol = bbb_props['playback_protocol']
playback_host = bbb_props['playback_host']
BigBlueButton.logger.info("setting target dir")
target_dir = "#{recording_dir}/publish/presentation/#{$meeting_id}"
$deskshare_dir = "#{recording_dir}/raw/#{$meeting_id}/deskshare"
if not FileTest.directory?(target_dir)
BigBlueButton.logger.info("Making dir target_dir")
FileUtils.mkdir_p target_dir
package_dir = "#{target_dir}/#{$meeting_id}"
BigBlueButton.logger.info("Making dir package_dir")
FileUtils.mkdir_p package_dir
begin
video_formats = $presentation_props['video_formats']
video_files = Dir.glob("#{$process_dir}/webcams.{#{video_formats.join(',')}}")
if ! video_files.empty?
BigBlueButton.logger.info("Making video dir")
video_dir = "#{package_dir}/video"
FileUtils.mkdir_p video_dir
video_files.each do |video_file|
BigBlueButton.logger.info("Made video dir - copying: #{video_file} to -> #{video_dir}")
FileUtils.cp(video_file, video_dir)
BigBlueButton.logger.info("Copied #{File.extname(video_file)} file")
end
else
audio_dir = "#{package_dir}/audio"
BigBlueButton.logger.info("Making audio dir")
FileUtils.mkdir_p audio_dir
BigBlueButton.logger.info("Made audio dir - copying: #{$process_dir}/audio.webm to -> #{audio_dir}")
FileUtils.cp("#{$process_dir}/audio.webm", audio_dir)
BigBlueButton.logger.info("Copied audio.webm file - copying: #{$process_dir}/audio.ogg to -> #{audio_dir}")
FileUtils.cp("#{$process_dir}/audio.ogg", audio_dir)
BigBlueButton.logger.info("Copied audio.ogg file")
end
if File.exist?("#{$process_dir}/captions.json")
BigBlueButton.logger.info("Copying caption files")
FileUtils.cp("#{$process_dir}/captions.json", package_dir)
Dir.glob("#{$process_dir}/caption_*.vtt").each do |caption|
BigBlueButton.logger.debug(caption)
FileUtils.cp(caption, package_dir)
end
end
video_files = Dir.glob("#{$process_dir}/deskshare.{#{video_formats.join(',')}}")
if ! video_files.empty?
BigBlueButton.logger.info("Making deskshare dir")
deskshare_dir = "#{package_dir}/deskshare"
FileUtils.mkdir_p deskshare_dir
video_files.each do |video_file|
BigBlueButton.logger.info("Made deskshare dir - copying: #{video_file} to -> #{deskshare_dir}")
FileUtils.cp(video_file, deskshare_dir)
BigBlueButton.logger.info("Copied #{File.extname(video_file)} file")
end
else
BigBlueButton.logger.info("Could not copy deskshares.webm: file doesn't exist")
end
if File.exist?("#{$process_dir}/presentation_text.json")
FileUtils.cp("#{$process_dir}/presentation_text.json", package_dir)
end
if File.exist?("#{$process_dir}/notes/notes.html")
FileUtils.cp("#{$process_dir}/notes/notes.html", package_dir)
end
processing_time = File.read("#{$process_dir}/processing_time")
@doc = Nokogiri::XML(File.read("#{$process_dir}/events.xml"))
# Retrieve record events and calculate total recording duration.
$rec_events = BigBlueButton::Events.match_start_and_stop_rec_events(
BigBlueButton::Events.get_start_and_stop_rec_events(@doc))
recording_time = BigBlueButton::Events.get_recording_length(@doc)
# presentation_url = "/slides/" + $meeting_id + "/presentation"
$meeting_start = @doc.xpath("//event")[0][:timestamp]
$meeting_end = @doc.xpath("//event").last()[:timestamp]
$version_atleast_0_9_0 = BigBlueButton::Events.bbb_version_compare(
@doc, 0, 9, 0)
$version_atleast_2_0_0 = BigBlueButton::Events.bbb_version_compare(
@doc, 2, 0, 0)
BigBlueButton.logger.info("Creating metadata.xml")
# Get the real-time start and end timestamp
match = /.*-(\d+)$/.match($meeting_id)
real_start_time = match[1]
real_end_time = (real_start_time.to_i + ($meeting_end.to_i - $meeting_start.to_i)).to_s
#### INSTEAD OF CREATING THE WHOLE metadata.xml FILE AGAIN, ONLY ADD <playback>
# Copy metadata.xml from process_dir
FileUtils.cp("#{$process_dir}/metadata.xml", package_dir)
BigBlueButton.logger.info("Copied metadata.xml file")
# Update state and add playback to metadata.xml
## Load metadata.xml
metadata = Nokogiri::XML(File.read("#{package_dir}/metadata.xml"))
## Update state
recording = metadata.root
state = recording.at_xpath("state")
state.content = "published"
published = recording.at_xpath("published")
published.content = "true"
## Remove empty playback
metadata.search('//recording/playback').each do |playback|
playback.remove
end
## Add the actual playback
presentation = BigBlueButton::Presentation.get_presentation_for_preview("#{$process_dir}")
metadata_with_playback = Nokogiri::XML::Builder.with(metadata.at('recording')) do |xml|
xml.playback {
xml.format("presentation")
xml.link("#{playback_protocol}://#{playback_host}/playback/presentation/2.3/#{$meeting_id}")
xml.processing_time("#{processing_time}")
xml.duration("#{recording_time}")
unless presentation.empty?
xml.extensions {
xml.preview {
xml.images {
presentation[:slides].each do |key,val|
attributes = {:width => "176", :height => "136", :alt => (val[:alt] != nil)? "#{val[:alt]}": ""}
xml.image(attributes){ xml.text("#{playback_protocol}://#{playback_host}/presentation/#{$meeting_id}/presentation/#{presentation[:id]}/thumbnails/thumb-#{key}.png") }
end
}
}
}
end
}
end
## Write the new metadata.xml
metadata_file = File.new("#{package_dir}/metadata.xml","w")
metadata = Nokogiri::XML(metadata.to_xml) { |x| x.noblanks }
metadata_file.write(metadata.root)
metadata_file.close
BigBlueButton.logger.info("Added playback to metadata.xml")
#Create slides.xml
BigBlueButton.logger.info("Generating xml for slides and chat")
calculateRecordEventsOffset()
# Write slides.xml to file
slides_doc = processChatMessages(@doc, bbb_props)
File.open("#{package_dir}/slides_new.xml", 'w') { |f| f.puts slides_doc.to_xml }
processPresentation(package_dir)
processDeskshareEvents(@doc)
processPollEvents(@doc, package_dir)
processExternalVideoEvents(@doc, package_dir)
# Write deskshare.xml to file
File.open("#{package_dir}/#{$deskshare_xml_filename}", 'w') { |f| f.puts $deskshare_xml.to_xml }
BigBlueButton.logger.info("Copying files to package dir")
FileUtils.cp_r("#{$process_dir}/presentation", package_dir)
BigBlueButton.logger.info("Copied files to package dir")
BigBlueButton.logger.info("Publishing slides")
# Now publish this recording files by copying them into the publish folder.
if not FileTest.directory?(publish_dir)
FileUtils.mkdir_p publish_dir
end
# Get raw size of presentation files
raw_dir = "#{recording_dir}/raw/#{$meeting_id}"
# After all the processing we'll add the published format and raw sizes to the metadata file
BigBlueButton.add_raw_size_to_metadata(package_dir, raw_dir)
BigBlueButton.add_playback_size_to_metadata(package_dir)
FileUtils.cp_r(package_dir, publish_dir) # Copy all the files.
BigBlueButton.logger.info("Finished publishing script presentation.rb successfully.")
BigBlueButton.logger.info("Removing processed files.")
FileUtils.rm_r(Dir.glob("#{$process_dir}/*"))
BigBlueButton.logger.info("Removing published files.")
FileUtils.rm_r(Dir.glob("#{target_dir}/*"))
rescue Exception => e
BigBlueButton.logger.error(e.message)
e.backtrace.each do |traceline|
BigBlueButton.logger.error(traceline)
end
exit 1
end
publish_done = File.new("#{recording_dir}/status/published/#{$meeting_id}-presentation.done", "w")
publish_done.write("Published #{$meeting_id}")
publish_done.close
else
BigBlueButton.logger.info("#{target_dir} is already there")
end
end
rescue Exception => e
BigBlueButton.logger.error(e.message)
e.backtrace.each do |traceline|
BigBlueButton.logger.error(traceline)
end
publish_done = File.new("#{recording_dir}/status/published/#{$meeting_id}-presentation.fail", "w")
publish_done.write("Failed Publishing #{$meeting_id}")
publish_done.close
exit 1
end