diff --git a/record-and-playback/core/lib/recordandplayback/generators/presentation.rb b/record-and-playback/core/lib/recordandplayback/generators/presentation.rb index 66494b825160..cfccaa80a3da 100755 --- a/record-and-playback/core/lib/recordandplayback/generators/presentation.rb +++ b/record-and-playback/core/lib/recordandplayback/generators/presentation.rb @@ -23,6 +23,16 @@ require 'rubygems' require 'nokogiri' +class Range + def intersection(other) + raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range) + new_min = self.cover?(other.min) ? other.min : other.cover?(min) ? min : nil + new_max = self.cover?(other.max) ? other.max : other.cover?(max) ? max : nil + new_min && new_max ? new_min..new_max : nil + end + alias_method :&, :intersection +end + module BigBlueButton class Presentation # Get the presentations. @@ -110,35 +120,81 @@ def self.get_text_from_slide(textfiles_dir, slide_num) end # Get from events the presentation that will be used for preview. - def self.get_presentation_for_preview(process_dir) + def self.get_presentation_for_preview(process_dir, heuristic_thumbnails, number_thumbnails) events_xml = "#{process_dir}/events.xml" BigBlueButton.logger.info("Task: Getting from events the presentation to be used for preview") - presentation = {} + presentation = [] doc = Nokogiri::XML(File.open(events_xml)) presentation_filenames = {} doc.xpath("//event[@eventname='ConversionCompletedEvent']").each do |conversion_event| presentation_filenames[conversion_event.xpath("presentationName").text] = conversion_event.xpath("originalFilename").text end - doc.xpath("//event[@eventname='SharePresentationEvent']").each do |presentation_event| - # Extract presentation data from events - presentation_id = presentation_event.xpath("presentationName").text - presentation_filename = presentation_filenames[presentation_id] - # Set textfile directory - textfiles_dir = "#{process_dir}/presentation/#{presentation_id}/textfiles" - # Set presentation hashmap to be returned - unless presentation_filename == "default.pdf" - presentation[:id] = presentation_id - presentation[:filename] = presentation_filename - presentation[:slides] = {} - for i in 1..3 - if File.file?("#{textfiles_dir}/slide-#{i}.txt") - text_from_slide = self.get_text_from_slide(textfiles_dir, i) - presentation[:slides][i] = { :alt => text_from_slide == nil ? '' : text_from_slide } + sesseion_end = doc.xpath('//event[last()]')[0].attributes['timestamp'].value.to_i + slide_events = doc.xpath("//event[@eventname='GotoSlideEvent']") + slide_time = {} + current_slide = nil + current_ts = nil + slide_events.each_with_index do |e, i| + if i > 0 + time_shown = current_ts..e.attributes["timestamp"].value.to_i + if slide_time[current_slide] + slide_time[current_slide].push(time_shown) + else + slide_time[current_slide] = [time_shown] + end + end + current_slide = e.at('id').text + current_ts = e.attributes["timestamp"].value.to_i + end + if current_ts + time_shown = current_ts..sesseion_end + if slide_time[current_slide] + slide_time[current_slide].push(time_shown) + else + slide_time[current_slide] = [time_shown] + end + end + #BigBlueButton.logger.info("slide_time: #{slide_time}") + + record_time = [] + record_events = doc.xpath("//event[@eventname='RecordStatusEvent']") + record_events.each_with_index do |e, i| + if i.odd? + record_time.push(record_events[i-1].attributes["timestamp"].value.to_i..e.attributes["timestamp"].value.to_i) + end + end + if record_events.size.odd? + record_time.push(record_events[-1].attributes["timestamp"].value.to_i..sesseion_end) + end + #BigBlueButton.logger.info("record_time: #{record_time}") + + # Intersect with the recorded periods + slide_time_recorded = {} + slide_time.each do |id, periods| + periods.each do |period| + record_time.each do |record| + intersected_period = period.intersection(record) + if slide_time_recorded[id] + slide_time_recorded[id].push(intersected_period) + else + slide_time_recorded[id] = [intersected_period] end end - # Break because something else than default.pdf was found - break end + slide_time_recorded[id].compact! if slide_time_recorded[id] + end + #BigBlueButton.logger.info("slide_time_recorded: #{slide_time_recorded}") + + slide_time_sort = slide_time_recorded.map{|k, v| [k, v.inject(0){|s, n| s += n.size}] }.sort_by{|_, v| -v} if heuristic_thumbnails + BigBlueButton.logger.info("Thumbnail candidates: #{slide_time_sort}") + slide_time_sort.each do |st| + break if presentation.size >= number_thumbnails + p, s = st[0].split('/') + presentation_filename = presentation_filenames[p] + next if presentation_filename == "default.pdf" + textfiles_dir = "#{process_dir}/presentation/#{p}/textfiles" + text_from_slide = self.get_text_from_slide(textfiles_dir, s) if File.file?("#{textfiles_dir}/slide-#{s}.txt") + presentation.push({ :id => p, :filename => presentation_filename, :i => s, :alt => text_from_slide == nil ? '' : text_from_slide, :duration => st[1] }) end presentation end diff --git a/record-and-playback/presentation/scripts/presentation.yml b/record-and-playback/presentation/scripts/presentation.yml index 8101ba5bca4a..f7aa684ec8ca 100755 --- a/record-and-playback/presentation/scripts/presentation.yml +++ b/record-and-playback/presentation/scripts/presentation.yml @@ -11,6 +11,10 @@ deskshare_output_framerate: 5 audio_offset: 0 include_deskshare: true +# Generate thumbnails from most viewed slides +heuristic_thumbnails: false +number_thumbnails: 3 + # For PRODUCTION publish_dir: /var/bigbluebutton/published/presentation video_formats: diff --git a/record-and-playback/presentation/scripts/publish/presentation.rb b/record-and-playback/presentation/scripts/publish/presentation.rb index 1767ddaadc41..6349485dea91 100755 --- a/record-and-playback/presentation/scripts/publish/presentation.rb +++ b/record-and-playback/presentation/scripts/publish/presentation.rb @@ -1388,7 +1388,7 @@ def copy_media_files_helper(media, media_files, package_dir) ## Remove empty playback metadata.search('recording/playback').each(&:remove) ## Add the actual playback - presentation = BigBlueButton::Presentation.get_presentation_for_preview(@process_dir.to_s) + presentation = BigBlueButton::Presentation.get_presentation_for_preview(@process_dir.to_s, @presentation_props['heuristic_thumbnails'], @presentation_props['number_thumbnails']) Nokogiri::XML::Builder.with(metadata.at('recording')) do |xml| xml.playback do xml.format('presentation') @@ -1399,10 +1399,10 @@ def copy_media_files_helper(media, media_files, package_dir) xml.extensions do xml.preview do xml.images do - presentation[:slides].each do |key, val| - attributes = { width: '176', height: '136', alt: val[:alt]&.to_s || '' } + presentation.each do |p| + attributes = { width: '176', height: '136', alt: p[:alt]&.to_s || '', duration: p[:duration]/1000 } xml.image(attributes) do - xml.text("#{playback_protocol}://#{playback_host}/presentation/#{@meeting_id}/presentation/#{presentation[:id]}/thumbnails/thumb-#{key}.png") + xml.text("#{playback_protocol}://#{playback_host}/presentation/#{@meeting_id}/presentation/#{p[:id]}/thumbnails/thumb-#{p[:i]}.png") end end end