bigbluebutton-Github/record-and-playback/presentation/scripts/publish/presentation.rb
Calvin Walton 321119a79e Create a new 2.0 recording playback directory, revert 0.9.0 to old shapes code.
The new shapes code, required for handling smooth shape updates & multi-user
whiteboard in the 2.0 BigBlueButton, hits a bug in old recordings where
the pencil tool incorrectly used "line" in its shape names, meaning that
there could be both a pencil mark and a line with the same shape name.

The old recording code didn't rely on the shape name to match shapes, since
there was no chance of concurrent shapes. As this is an incompatible playback
change, we need to make a new playback directory for the updated files.
2017-08-18 11:34:15 -04:00

1350 lines
49 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 '../../core/lib/recordandplayback'
require 'rubygems'
require 'trollop'
require 'yaml'
require 'builder'
require 'fastimage' # require fastimage to get the image size of the slides (gem install fastimage)
bbb_props = YAML::load(File.open('../../core/scripts/bigbluebutton.yml'))
presentation_props = YAML::load(File.open('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 = 720.to_f
deskshare_video_width = 1280.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 = 1280
video_height = 720
deskshare_video_filename = "#{$deskshare_dir}/#{deskshare_stream_name}"
if File.exist?(deskshare_video_filename)
video_width = BigBlueButton.get_video_width(deskshare_video_filename)
video_height = BigBlueButton.get_video_height(deskshare_video_filename)
video_width, video_height = scaleToDeskshareVideo(video_width, video_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.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-linecap:round;stroke-linejoin:round;stroke-width:#{shape_thickness(slide,shape)};visibility:hidden;fill:none"
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-linecap:round;stroke-linejoin:round;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 = (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-linecap:round;stroke-linejoin:round;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])
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-linecap:round;stroke-linejoin:round;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 = JSON.load(shape[:result])
num_responders = shape[:num_responders]
presentation = shape[:presentation]
max_num_votes = result.map{ |r| r['num_votes'] }.max
dat_file = "#{$process_dir}/poll_result#{poll_id}.dat"
gpl_file = "#{$process_dir}/poll_result#{poll_id}.gpl"
pdf_file = "#{$process_dir}/poll_result#{poll_id}.pdf"
svg_file = "#{$process_dir}/presentation/#{presentation}/poll_result#{poll_id}.svg"
# Use gnuplot to generate an SVG image for the graph
File.open(dat_file, 'w') do |d|
result.each do |r|
d.puts("#{r['id']} #{r['num_votes']}")
end
end
File.open(dat_file, 'r') do |d|
BigBlueButton.logger.debug("gnuplot data:")
BigBlueButton.logger.debug(d.readlines(nil)[0])
end
File.open(gpl_file, 'w') do |g|
g.puts('reset')
g.puts("set term pdfcairo size #{height / 72}, #{width / 72} font \"Arial,48\" noenhanced")
g.puts('set lmargin 0.5')
g.puts('set rmargin 0.5')
g.puts('unset key')
g.puts('set style data boxes')
g.puts('set style fill solid border -1')
g.puts('set boxwidth 0.9 relative')
g.puts('set yrange [0:*]')
g.puts('unset border')
g.puts('unset ytics')
xtics = result.map{ |r| "#{r['key'].gsub('%', '%%').inspect} #{r['id']}" }.join(', ')
g.puts("set xtics rotate by 90 scale 0 right (#{xtics})")
if num_responders > 0
x2tics = result.map{ |r| "\"#{(r['num_votes'].to_f / num_responders * 100).to_i}%%\" #{r['id']}" }.join(', ')
g.puts("set x2tics rotate by 90 scale 0 left (#{x2tics})")
end
g.puts('set linetype 1 linewidth 1 linecolor rgb "black"')
result.each do |r|
if r['num_votes'] == 0 or r['num_votes'].to_f / max_num_votes <= 0.5
g.puts("set label \"#{r['num_votes']}\" at #{r['id']},#{r['num_votes']} left rotate by 90 offset 0,character 0.5 front")
else
g.puts("set label \"#{r['num_votes']}\" at #{r['id']},#{r['num_votes']} right rotate by 90 offset 0,character -0.5 textcolor rgb \"white\" front")
end
end
g.puts("set output \"#{pdf_file}\"")
g.puts("plot \"#{dat_file}\"")
end
File.open(gpl_file, 'r') do |d|
BigBlueButton.logger.debug("gnuplot script:")
BigBlueButton.logger.debug(d.readlines(nil)[0])
end
# gnuplot svg rendering has issues, so we render to pdf...
ret = BigBlueButton.exec_ret('gnuplot', '-d', gpl_file)
raise "Failed to generate plot pdf" if ret != 0
# then use pdftocairo to turn it into svg
ret = BigBlueButton.exec_ret('pdftocairo', '-svg', pdf_file, svg_file)
raise "Failed to convert poll to svg" if ret != 0
# Outer box to act as a poll result backdrop
g << doc.create_element('rect',
x: x + 2, y: y + 2, width: width - 4, height: height - 4,
fill: 'white', stroke: 'black', 'stroke-width' => 4)
# Poll image (note that the image is sideways and has to be rotated)
g << doc.create_element('image',
'xlink:href' => "presentation/#{presentation}/poll_result#{poll_id}.svg",
height: width, width: height, x: slide[:width], y: y,
transform: "rotate(90, #{slide[:width]}, #{y})")
end
def svg_render_shape(canvas, slide, shape)
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: "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'
svg_render_shape_poll(g, slide, shape)
else
BigBlueButton.logger.warn("Ignoring unhandled shape type #{shape[:type]}")
end
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)
end
if canvas.element_children.length > 0
svg << canvas
end
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])
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])
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])
if cursor[:visible]
x = (cursor[:x] / 100.0 * cursor[:width] / $magic_mystery_number).round(5)
y = (cursor[:y] / 100.0 * cursor[:height] / $magic_mystery_number).round(5)
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').text
if $version_atleast_2_0_0
shape[:thickness_percent] = thickness.to_f
else
shape[:thickness] = thickness.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
shape[:num_respondents] = event.at_xpath('num_respondents').text
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
shape[:calced_font_size] = event.at_xpath('calcedFontSize').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] = 'logo.png'
end
if !File.exist?(slide[:src])
BigBlueButton.logger.warn("Missing image file #{slide[:src]}!")
# Emergency last-ditch blank image creation
FileUtils.mkdir_p(File.dirname(slide[:src]))
if slide[:deskshare]
command = "convert -size #{presentation_props['deskshare_output_width']}x#{presentation_props['deskshare_output_height']} xc:transparent -background transparent #{slide[:src]}"
else
command = "convert -size 1600x1200 xc:white -quality 90 +dither -depth 8 -colors 256 #{slide[:src]}"
end
BigBlueButton.execute(command)
end
slide[:width], slide[:height] = FastImage.size(slide[:src])
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
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.open("#{$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 eventname == 'DeskshareStartedEvent' and presentation_props['include_deskshare']
deskshare = true
slide_changed = true
elsif eventname == 'DeskshareStoppedEvent' and presentation_props['include_deskshare']
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 * 100.0
cursor_y = event.at_xpath('yOffset').text.to_f * 100.0
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')
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 = {
src: "presentation/#{current_presentation}/slide-#{current_slide + 1}.png",
text: "presentation/#{current_presentation}/textfiles/slide-#{current_slide + 1}.txt",
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')
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
unless cursor_x >= 0 and cursor_x <= 100 and
cursor_y >= 0 and cursor_y <= 100
cursor_visible = false
end
slide = slides.last
cursor = cursors.last
if !cursor.nil? and
((!cursor[:visible] and !cursor_visible) or
(cursor[:x] == cursor_x and cursor[:y] == cursor_y))
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} (#{slide[:width]}x#{slide[:height]})")
cursor = {
visible: cursor_visible,
x: cursor_x,
y: cursor_y,
width: slide[:width],
height: slide[:height],
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
BigBlueButton.logger.info("Processing chat events")
# Create slides.xml and chat.
$slides_doc = Nokogiri::XML::Builder.new do |xml|
$xml = xml
$xml.popcorn {
# Process chat events.
current_time = 0
$rec_events.each do |re|
$chat_events.each do |node|
if (node[:timestamp].to_i >= re[:start_timestamp] and node[:timestamp].to_i <= re[:stop_timestamp])
chat_timestamp = node[:timestamp]
chat_sender = node.xpath(".//sender")[0].text()
chat_message = BigBlueButton::Events.linkify(node.xpath(".//message")[0].text())
chat_start = ( translateTimestamp(chat_timestamp) / 1000).to_i
# Creates a list of the clear timestamps that matter for this message
next_clear_timestamps = $clear_chat_timestamps.select{ |e| e >= node[:timestamp] }
# If there is none we skip it, or else we add the out time that will remove a message
if next_clear_timestamps.empty?
$xml.chattimeline(:in => chat_start, :direction => :down, :name => chat_sender, :message => chat_message, :target => :chat )
else
chat_end = ( translateTimestamp( next_clear_timestamps.first ) / 1000).to_i
$xml.chattimeline(:in => chat_start, :out => chat_end, :direction => :down, :name => chat_sender, :message => chat_message, :target => :chat )
end
end
end
current_time += re[:stop_timestamp] - re[:start_timestamp]
end
}
end
end
def processDeskshareEvents
BigBlueButton.logger.info("Processing deskshare events")
deskshare_matched_events = BigBlueButton::Events.get_matched_start_and_stop_deskshare_events("#{$process_dir}/events.xml")
$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)
if !BigBlueButton.is_video_valid?("#{$deskshare_dir}/#{event[:stream]}")
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
$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")
# This script lives in scripts/archive/steps while properties.yaml lives in scripts/
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
if File.exist?("#{$process_dir}/webcams.webm")
BigBlueButton.logger.info("Making video dir")
video_dir = "#{package_dir}/video"
FileUtils.mkdir_p video_dir
BigBlueButton.logger.info("Made video dir - copying: #{$process_dir}/webcams.webm to -> #{video_dir}")
FileUtils.cp("#{$process_dir}/webcams.webm", video_dir)
BigBlueButton.logger.info("Copied .webm file")
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
if File.exist?("#{$process_dir}/deskshare.webm")
BigBlueButton.logger.info("Making deskshare dir")
deskshare_dir = "#{package_dir}/deskshare"
FileUtils.mkdir_p deskshare_dir
BigBlueButton.logger.info("Made deskshare dir - copying: #{$process_dir}/deskshare.webm to -> #{deskshare_dir}")
FileUtils.cp("#{$process_dir}/deskshare.webm", deskshare_dir)
BigBlueButton.logger.info("Copied deskshare.webm file")
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
processing_time = File.read("#{$process_dir}/processing_time")
# 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("#{$process_dir}/events.xml"))
recording_time = BigBlueButton::Events.get_recording_length("#{$process_dir}/events.xml")
# presentation_url = "/slides/" + $meeting_id + "/presentation"
@doc = Nokogiri::XML(File.open("#{$process_dir}/events.xml"))
$meeting_start = @doc.xpath("//event")[0][:timestamp]
$meeting_end = @doc.xpath("//event").last()[:timestamp]
$version = BigBlueButton::Events.bbb_version("#{$process_dir}/events.xml")
$version_atleast_0_9_0 = BigBlueButton::Events.bbb_version_compare("#{$process_dir}/events.xml", 0, 9, 0)
$version_atleast_2_0 = BigBlueButton::Events.bbb_version_compare("#{$process_dir}/events.xml", 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.open("#{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.0/playback.html?meetingId=#{$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")
# Gathering all the events from the events.xml
$chat_events = @doc.xpath("//event[@eventname='PublicChatEvent']")
# Create a list of timestamps when the moderator cleared the public chat
$clear_chat_timestamps = [ ]
clear_chat_events = @doc.xpath("//event[@eventname='ClearPublicChatEvent']")
clear_chat_events.each { |clear| $clear_chat_timestamps << clear[:timestamp] }
$clear_chat_timestamps.sort!
calculateRecordEventsOffset()
processChatMessages()
processPresentation()
processDeskshareEvents()
# Write slides.xml to file
File.open("#{package_dir}/slides_new.xml", 'w') { |f| f.puts $slides_doc.to_xml }
# 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