2012-07-18 03:39:44 +08:00
# 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)
2017-08-18 01:27:09 +08:00
bbb_props = YAML :: load ( File . open ( '../../core/scripts/bigbluebutton.yml' ) )
presentation_props = YAML :: load ( File . open ( 'presentation.yml' ) )
2017-02-24 02:21:10 +08:00
2017-08-18 01:27:09 +08:00
# 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
2017-02-24 02:21:10 +08:00
2016-12-16 01:19:10 +08:00
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
2017-02-24 04:17:16 +08:00
def getDeskshareVideoDimension ( deskshare_stream_name )
2017-02-24 02:21:10 +08:00
video_width = 1280
video_height = 720
2017-02-24 04:17:16 +08:00
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 )
2016-12-13 02:29:06 +08:00
else
2017-02-24 04:17:16 +08:00
BigBlueButton . logger . error ( " Could not find deskshare video: #{ deskshare_video_filename } " )
2016-12-13 02:29:06 +08:00
end
2017-02-24 04:17:16 +08:00
2017-02-24 02:21:10 +08:00
return video_width , video_height
2016-11-30 01:08:28 +08:00
end
2017-08-18 01:27:09 +08:00
#
# 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 ]
2016-01-20 03:52:28 +08:00
end
2012-07-18 03:39:44 +08:00
end
2017-08-18 01:27:09 +08:00
#
# 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
2012-07-18 03:39:44 +08:00
end
2017-08-18 01:27:09 +08:00
#
# 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 ]
2016-01-20 03:52:28 +08:00
end
end
2017-08-18 01:27:09 +08:00
# 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
2016-01-20 03:52:28 +08:00
2017-08-18 01:27:09 +08:00
def color_to_hex ( color )
color = color . to_i . to_s ( 16 )
return '0' * ( 6 - color . length ) + color
2012-07-18 03:39:44 +08:00
end
2017-08-18 01:27:09 +08:00
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 ]
2016-01-20 03:52:28 +08:00
end
2012-07-18 03:39:44 +08:00
end
2017-08-18 01:27:09 +08:00
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
2017-08-04 00:17:48 +08:00
else
path = [ ]
2017-08-18 01:27:09 +08:00
data_points = shape [ :data_points ] . each
2017-08-04 00:17:48 +08:00
2017-08-18 01:27:09 +08:00
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 |
2017-08-04 00:17:48 +08:00
case command
2017-08-18 01:27:09 +08:00
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 } " )
2017-08-04 00:17:48 +08:00
else
raise " Unknown pencil command: #{ command } "
end
end
else
2017-08-18 01:27:09 +08:00
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
2017-08-04 00:17:48 +08:00
end
end
2017-08-18 01:27:09 +08:00
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
2016-01-20 03:52:28 +08:00
end
2012-07-18 03:39:44 +08:00
end
2017-08-18 01:27:09 +08:00
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
2016-01-20 03:52:28 +08:00
2017-08-18 01:27:09 +08:00
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 "
2016-01-20 03:52:28 +08:00
2017-08-18 01:27:09 +08:00
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 ] )
2016-01-20 03:52:28 +08:00
2017-08-18 01:27:09 +08:00
width = ( x2 - x1 ) . abs
height = ( y2 - y1 ) . abs
2012-09-07 03:21:55 +08:00
2017-08-18 01:27:09 +08:00
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
2016-01-20 03:52:28 +08:00
end
end
2012-09-04 09:14:29 +08:00
2017-08-18 01:27:09 +08:00
path = doc . create_element ( 'path' ,
d : " M #{ x1 } #{ y1 } L #{ x2 } #{ y1 } L #{ x2 } #{ y2 } L #{ x1 } #{ y2 } Z " )
g << path
end
2012-09-04 09:14:29 +08:00
2017-08-18 01:27:09 +08:00
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 "
2012-09-04 09:14:29 +08:00
2017-08-18 01:27:09 +08:00
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 ] )
2012-09-04 09:14:29 +08:00
2017-08-18 01:27:09 +08:00
px = ( ( x1 + x2 ) / 2 . 0 ) . round ( 5 )
2012-09-04 09:14:29 +08:00
2017-08-18 01:27:09 +08:00
path = doc . create_element ( 'path' ,
d : " M #{ px } #{ y1 } L #{ x2 } #{ y2 } L #{ x1 } #{ y2 } Z " )
g << path
end
2012-09-04 09:14:29 +08:00
2017-08-18 01:27:09 +08:00
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 "
2012-09-04 09:14:29 +08:00
2017-08-18 01:27:09 +08:00
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 ] )
2012-09-04 09:14:29 +08:00
2017-08-18 01:27:09 +08:00
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 )
2016-08-12 23:53:01 +08:00
2017-08-18 01:27:09 +08:00
if shape [ :circle ]
2016-08-12 23:53:01 +08:00
# Convert to a circle, keeping aligned with the start point
2017-08-18 01:27:09 +08:00
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
2016-01-20 03:52:28 +08:00
end
2017-08-18 01:27:09 +08:00
end
2016-08-12 23:53:01 +08:00
2017-08-18 01:27:09 +08:00
# 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
2013-09-26 21:32:55 +08:00
end
2017-08-18 01:27:09 +08:00
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' )
2016-01-20 03:52:28 +08:00
end
2017-08-18 01:27:09 +08:00
p << doc . create_text_node ( line . chomp )
end
fo << p
switch << fo
g << switch
2012-07-18 03:39:44 +08:00
end
2017-08-18 01:27:09 +08:00
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 ]
2017-04-07 02:33:38 +08:00
max_num_votes = result . map { | r | r [ 'num_votes' ] } . max
2015-08-12 02:55:59 +08:00
2017-08-18 01:27:09 +08:00
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 "
2015-08-12 02:55:59 +08:00
# 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' )
2016-03-08 00:26:17 +08:00
g . puts ( " set term pdfcairo size #{ height / 72 } , #{ width / 72 } font \" Arial,48 \" noenhanced " )
2017-04-07 06:04:10 +08:00
g . puts ( 'set lmargin 0.5' )
g . puts ( 'set rmargin 0.5' )
2015-08-12 02:55:59 +08:00
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' )
2016-03-08 00:26:17 +08:00
xtics = result . map { | r | " #{ r [ 'key' ] . gsub ( '%' , '%%' ) . inspect } #{ r [ 'id' ] } " } . join ( ', ' )
2015-08-12 02:55:59 +08:00
g . puts ( " set xtics rotate by 90 scale 0 right ( #{ xtics } ) " )
2015-08-25 02:42:31 +08:00
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
2015-08-12 02:55:59 +08:00
g . puts ( 'set linetype 1 linewidth 1 linecolor rgb "black"' )
result . each do | r |
2017-04-07 02:33:38 +08:00
if r [ 'num_votes' ] == 0 or r [ 'num_votes' ] . to_f / max_num_votes < = 0 . 5
2015-08-12 02:55:59 +08:00
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
2017-08-18 01:27:09 +08:00
# gnuplot svg rendering has issues, so we render to pdf...
2015-08-12 02:55:59 +08:00
ret = BigBlueButton . exec_ret ( 'gnuplot' , '-d' , gpl_file )
raise " Failed to generate plot pdf " if ret != 0
2017-08-18 01:27:09 +08:00
# then use pdftocairo to turn it into svg
2015-08-12 02:55:59 +08:00
ret = BigBlueButton . exec_ret ( 'pdftocairo' , '-svg' , pdf_file , svg_file )
raise " Failed to convert poll to svg " if ret != 0
2017-08-18 01:27:09 +08:00
# 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
2015-08-12 02:55:59 +08:00
2017-08-18 01:27:09 +08:00
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
2015-08-12 02:55:59 +08:00
2017-08-18 01:27:09 +08:00
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
2015-08-12 02:55:59 +08:00
2017-08-18 01:27:09 +08:00
if g . element_children . length > 0
canvas << g
2015-08-12 02:55:59 +08:00
end
end
2017-08-18 01:27:09 +08:00
$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
2016-01-20 03:52:28 +08:00
2017-08-18 01:27:09 +08:00
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
2016-01-20 03:52:28 +08:00
end
2017-08-18 01:27:09 +08:00
shapes = shapes [ slide [ :presentation ] ] [ slide [ :slide ] ]
2013-09-19 04:54:51 +08:00
2017-08-18 01:27:09 +08:00
canvas = doc . create_element ( 'g' ,
class : 'canvas' , id : " canvas #{ image_id } " ,
image : " image #{ image_id } " , display : 'none' )
2013-09-19 04:54:51 +08:00
2017-08-18 01:27:09 +08:00
shapes . each do | shape |
svg_render_shape ( canvas , slide , shape )
2016-01-20 03:52:28 +08:00
end
2017-08-18 01:27:09 +08:00
if canvas . element_children . length > 0
svg << canvas
2016-01-20 03:52:28 +08:00
end
2013-09-26 21:32:55 +08:00
end
2013-09-19 04:54:51 +08:00
2017-08-18 01:27:09 +08:00
def panzooms_emit_event ( rec , panzoom )
if panzoom [ :in ] == panzoom [ :out ]
BigBlueButton . logger . info ( " Panzoom: not emitting, duration rounds to 0 " )
return
2016-01-20 03:52:28 +08:00
end
2013-09-19 04:54:51 +08:00
2017-08-18 01:27:09 +08:00
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
2017-02-24 02:21:10 +08:00
end
2017-08-18 01:27:09 +08:00
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
2017-02-24 02:21:10 +08:00
end
2017-08-18 01:27:09 +08:00
def cursors_emit_event ( rec , cursor )
if cursor [ :in ] == cursor [ :out ]
BigBlueButton . logger . info ( " Cursor: not emitting, duration rounds to 0 " )
return
end
2016-12-03 03:40:50 +08:00
2017-08-18 01:27:09 +08:00
doc = rec . document
event = doc . create_element ( 'event' , timestamp : cursor [ :in ] )
2017-08-03 04:50:24 +08:00
2017-08-18 01:27:09 +08:00
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
2017-08-03 04:50:24 +08:00
2017-08-18 01:27:09 +08:00
cursor_e = doc . create_element ( 'cursor' )
cursor_e . content = " #{ x } #{ y } "
2016-11-30 01:08:28 +08:00
2017-08-18 01:27:09 +08:00
BigBlueButton . logger . info ( " Cursor #{ cursor_e . content } at #{ cursor [ :in ] } " )
2017-02-24 02:21:10 +08:00
2017-08-18 01:27:09 +08:00
event << cursor_e
rec << event
end
2017-08-03 04:50:24 +08:00
2017-08-18 01:27:09 +08:00
$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
# TODO double-check this version number
slide -= 1 unless $version_atleast_0_9_0
end
2017-08-03 04:50:24 +08:00
2017-08-18 01:27:09 +08:00
# 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
2017-08-03 04:50:24 +08:00
else
2017-08-18 01:27:09 +08:00
shape [ :thickness ] = thickness . to_i
2017-08-03 04:50:24 +08:00
end
2017-08-18 01:27:09 +08:00
end
if shape [ :type ] == 'rectangle'
square = event . at_xpath ( 'square' )
if ! square . nil?
shape [ 'square' ] = ( square . text == 'true' )
2017-08-03 04:50:24 +08:00
end
2017-08-18 01:27:09 +08:00
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
2017-08-03 04:50:24 +08:00
else
2017-08-18 01:27:09 +08:00
shape [ :text ] = ''
2017-08-03 04:50:24 +08:00
end
2017-08-18 01:27:09 +08:00
end
2017-08-03 04:50:24 +08:00
2017-08-18 01:27:09 +08:00
# 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
2017-08-03 04:50:24 +08:00
end
2017-08-18 01:27:09 +08:00
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
2016-01-20 03:52:28 +08:00
2017-08-18 01:27:09 +08:00
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
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
2017-08-03 22:40:17 +08:00
else
2017-08-18 01:27:09 +08:00
BigBlueButton . logger . info ( " Undo: no applicable shapes found " )
2016-01-20 03:52:28 +08:00
end
2017-08-03 22:50:14 +08:00
end
2012-07-18 03:39:44 +08:00
end
2017-08-18 01:27:09 +08:00
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
end
2016-01-20 03:52:28 +08:00
2017-08-18 01:27:09 +08:00
# 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
2016-01-20 03:52:28 +08:00
2017-08-18 01:27:09 +08:00
# Set up the shapes data structures if needed
shapes [ presentation ] = { } if shapes [ presentation ] . nil?
shapes [ presentation ] [ slide ] = [ ] if shapes [ presentation ] [ slide ] . nil?
2016-01-20 03:52:28 +08:00
2017-08-18 01:27:09 +08:00
# We only need to deal with shapes for this slide
shapes = shapes [ presentation ] [ slide ]
2016-01-20 03:52:28 +08:00
2017-08-18 01:27:09 +08:00
if full_clear
BigBlueButton . logger . info ( " Clear: removing all shapes " )
else
BigBlueButton . logger . info ( " Clear: removing shapes for User #{ user_id } " )
end
2016-01-20 03:52:28 +08:00
2017-08-18 01:27:09 +08:00
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
2016-01-20 03:52:28 +08:00
2017-08-18 01:27:09 +08:00
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
2016-01-20 03:52:28 +08:00
2017-08-18 01:27:09 +08:00
# 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
2016-01-20 03:52:28 +08:00
2017-08-18 01:27:09 +08:00
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
2017-08-03 23:21:51 +08:00
2017-08-18 01:27:09 +08:00
# 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 )
2016-01-20 03:52:28 +08:00
end
2017-08-18 01:27:09 +08:00
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
2016-01-20 03:52:28 +08:00
end
end
end
2017-08-18 01:27:09 +08:00
# 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 )
2012-07-18 03:39:44 +08:00
end
def processChatMessages
2016-01-20 03:52:28 +08:00
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
2016-06-22 05:32:59 +08:00
# 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
2016-01-20 03:52:28 +08:00
end
end
current_time += re [ :stop_timestamp ] - re [ :start_timestamp ]
end
}
end
2012-07-18 03:39:44 +08:00
end
2016-09-10 05:31:27 +08:00
def processDeskshareEvents
2017-02-24 02:21:10 +08:00
BigBlueButton . logger . info ( " Processing deskshare events " )
deskshare_matched_events = BigBlueButton :: Events . get_matched_start_and_stop_deskshare_events ( " #{ $process_dir } /events.xml " )
2016-09-10 05:31:27 +08:00
$deskshare_xml = Nokogiri :: XML :: Builder . new do | xml |
$xml = xml
$xml . recording ( 'id' = > 'deskshare_events' ) do
2017-02-24 02:21:10 +08:00
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 )
2017-05-24 04:02:26 +08:00
if ! BigBlueButton . is_video_valid? ( " #{ $deskshare_dir } / #{ event [ :stream ] } " )
2017-05-08 06:12:34 +08:00
BigBlueButton . logger . warn ( " #{ event [ :stream ] } is not a valid video file, skipping... " )
next
end
2017-02-24 04:17:16 +08:00
video_width , video_height = getDeskshareVideoDimension ( event [ :stream ] )
2017-02-24 02:21:10 +08:00
$xml . event ( :start_timestamp = > start_timestamp ,
:stop_timestamp = > stop_timestamp ,
:video_width = > video_width ,
:video_height = > video_height )
2016-09-10 05:31:27 +08:00
end
end
end
end
end
2012-07-18 03:39:44 +08:00
$shapes_svg_filename = 'shapes.svg'
$panzooms_xml_filename = 'panzooms.xml'
$cursor_xml_filename = 'cursor.xml'
2016-09-10 05:31:27 +08:00
$deskshare_xml_filename = 'deskshare.xml'
2012-07-18 03:39:44 +08:00
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
2014-04-15 06:14:14 +08:00
begin
2016-01-27 03:25:02 +08:00
if ( $playback == " presentation " )
2014-04-04 04:23:49 +08:00
2016-01-27 03:25:02 +08:00
# This script lives in scripts/archive/steps while properties.yaml lives in scripts/
2014-04-04 04:23:49 +08:00
log_dir = bbb_props [ 'log_dir' ]
2016-01-27 03:25:02 +08:00
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 " )
2017-08-18 01:27:09 +08:00
publish_dir = presentation_props [ 'publish_dir' ]
2016-01-27 03:25:02 +08:00
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 } "
2016-12-01 02:31:19 +08:00
$deskshare_dir = " #{ recording_dir } /raw/ #{ $meeting_id } /deskshare "
2016-01-27 03:25:02 +08:00
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
2016-01-20 03:52:28 +08:00
2016-02-02 01:23:39 +08:00
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
2016-10-21 03:39:35 +08:00
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
2016-11-29 22:00:19 +08:00
if File . exist? ( " #{ $process_dir } /presentation_text.json " )
FileUtils . cp ( " #{ $process_dir } /presentation_text.json " , package_dir )
end
2016-01-20 03:52:28 +08:00
2016-01-27 03:25:02 +08:00
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 " ) )
2016-08-12 23:53:01 +08:00
recording_time = BigBlueButton :: Events . get_recording_length ( " #{ $process_dir } /events.xml " )
2016-01-27 03:25:02 +08:00
# presentation_url = "/slides/" + $meeting_id + "/presentation"
@doc = Nokogiri :: XML ( File . open ( " #{ $process_dir } /events.xml " ) )
2016-03-04 00:27:28 +08:00
$meeting_start = @doc . xpath ( " //event " ) [ 0 ] [ :timestamp ]
$meeting_end = @doc . xpath ( " //event " ) . last ( ) [ :timestamp ]
2016-01-27 03:25:02 +08:00
$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 )
2017-08-03 23:21:51 +08:00
$version_atleast_2_0 = BigBlueButton :: Events . bbb_version_compare ( " #{ $process_dir } /events.xml " , 2 , 0 , 0 )
2016-01-27 03:25:02 +08:00
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
2016-01-28 23:13:45 +08:00
#### INSTEAD OF CREATING THE WHOLE metadata.xml FILE AGAIN, ONLY ADD <playback>
2016-01-27 06:16:28 +08:00
# Copy metadata.xml from process_dir
FileUtils . cp ( " #{ $process_dir } /metadata.xml " , package_dir )
BigBlueButton . logger . info ( " Copied metadata.xml file " )
2016-02-13 01:08:33 +08:00
# Update state and add playback to metadata.xml
2016-01-27 06:16:28 +08:00
## Load metadata.xml
metadata = Nokogiri :: XML ( File . open ( " #{ package_dir } /metadata.xml " ) )
2016-02-13 01:08:33 +08:00
## Update state
2016-01-27 06:16:28 +08:00
recording = metadata . root
state = recording . at_xpath ( " state " )
state . content = " published "
2016-02-10 06:26:29 +08:00
published = recording . at_xpath ( " published " )
published . content = " true "
2016-01-28 23:13:45 +08:00
## Remove empty playback
2016-01-27 06:16:28 +08:00
metadata . search ( '//recording/playback' ) . each do | playback |
playback . remove
end
2016-02-10 06:26:29 +08:00
## Add the actual playback
2016-09-21 04:46:45 +08:00
presentation = BigBlueButton :: Presentation . get_presentation_for_preview ( " #{ $process_dir } " )
2016-01-27 06:16:28 +08:00
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/0.9.0/playback.html?meetingId= #{ $meeting_id } " )
xml . processing_time ( " #{ processing_time } " )
xml . duration ( " #{ recording_time } " )
2016-09-21 02:10:28 +08:00
unless presentation . empty?
2016-09-20 06:09:31 +08:00
xml . extensions {
xml . preview {
xml . images {
2016-09-28 03:19:53 +08:00
presentation [ :slides ] . each do | key , val |
2016-09-28 04:02:20 +08:00
attributes = { :width = > " 176 " , :height = > " 136 " , :alt = > ( val [ :alt ] != nil ) ? " #{ val [ :alt ] } " : " " }
2016-09-28 03:19:53 +08:00
xml . image ( attributes ) { xml . text ( " #{ playback_protocol } :// #{ playback_host } /presentation/ #{ $meeting_id } /presentation/ #{ presentation [ :id ] } /thumbnails/thumb- #{ key } .png " ) }
2016-09-21 02:10:28 +08:00
end
2016-09-20 06:09:31 +08:00
}
}
}
end
2016-01-27 06:16:28 +08:00
}
end
## Write the new metadata.xml
2016-02-13 01:08:33 +08:00
metadata_file = File . new ( " #{ package_dir } /metadata.xml " , " w " )
2016-01-28 23:13:45 +08:00
metadata = Nokogiri :: XML ( metadata . to_xml ) { | x | x . noblanks }
2016-02-13 01:08:33 +08:00
metadata_file . write ( metadata . root )
metadata_file . close
2016-01-27 06:16:28 +08:00
BigBlueButton . logger . info ( " Added playback to metadata.xml " )
2016-01-27 03:25:02 +08:00
#Create slides.xml
2016-02-13 01:08:33 +08:00
BigBlueButton . logger . info ( " Generating xml for slides and chat " )
2016-01-27 03:25:02 +08:00
# Gathering all the events from the events.xml
$chat_events = @doc . xpath ( " //event[@eventname='PublicChatEvent'] " )
2016-06-22 05:32:59 +08:00
# Create a list of timestamps when the moderator cleared the public chat
2017-02-24 21:46:32 +08:00
$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!
2016-03-05 01:40:48 +08:00
2016-01-27 03:25:02 +08:00
calculateRecordEventsOffset ( )
processChatMessages ( )
2016-01-20 03:52:28 +08:00
2017-08-18 01:27:09 +08:00
processPresentation ( )
2016-01-20 03:52:28 +08:00
2016-09-10 05:31:27 +08:00
processDeskshareEvents ( )
2016-01-27 03:25:02 +08:00
# Write slides.xml to file
File . open ( " #{ package_dir } /slides_new.xml " , 'w' ) { | f | f . puts $slides_doc . to_xml }
2016-01-20 03:52:28 +08:00
2016-09-10 05:31:27 +08:00
# Write deskshare.xml to file
File . open ( " #{ package_dir } / #{ $deskshare_xml_filename } " , 'w' ) { | f | f . puts $deskshare_xml . to_xml }
2016-01-27 03:25:02 +08:00
BigBlueButton . logger . info ( " Copying files to package dir " )
FileUtils . cp_r ( " #{ $process_dir } /presentation " , package_dir )
BigBlueButton . logger . info ( " Copied files to package dir " )
2016-01-20 03:52:28 +08:00
2016-01-27 03:25:02 +08:00
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
2016-03-02 12:03:15 +08:00
# 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 )
2016-01-27 03:25:02 +08:00
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
2016-01-20 03:52:28 +08:00
end
2016-01-27 03:25:02 +08:00
publish_done = File . new ( " #{ recording_dir } /status/published/ #{ $meeting_id } -presentation.done " , " w " )
publish_done . write ( " Published #{ $meeting_id } " )
publish_done . close
2014-04-15 06:14:14 +08:00
2016-01-27 03:25:02 +08:00
else
BigBlueButton . logger . info ( " #{ target_dir } is already there " )
end
2016-01-20 03:52:28 +08:00
end
2012-07-18 03:39:44 +08:00
2016-08-12 23:53:01 +08:00
rescue Exception = > e
BigBlueButton . logger . error ( e . message )
e . backtrace . each do | traceline |
BigBlueButton . logger . error ( traceline )
2016-01-27 03:25:02 +08:00
end
publish_done = File . new ( " #{ recording_dir } /status/published/ #{ $meeting_id } -presentation.fail " , " w " )
publish_done . write ( " Failed Publishing #{ $meeting_id } " )
publish_done . close
2012-09-07 03:21:55 +08:00
2016-01-27 03:25:02 +08:00
exit 1
2014-04-15 06:14:14 +08:00
end
2012-11-28 06:41:14 +08:00