From afc002b2b5b94d44973313d36a488792c2b3bd9d Mon Sep 17 00:00:00 2001 From: Hugo Lazzari Date: Tue, 29 Jan 2013 10:08:47 -0200 Subject: [PATCH 01/13] Created custom item renderer to highlight a label. --- .../main/views/LanguageSelector.mxml | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/LanguageSelector.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/LanguageSelector.mxml index 348676283b..05b8c65d3c 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/LanguageSelector.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/LanguageSelector.mxml @@ -23,13 +23,42 @@ + + + + + + + + + + + + + + + - - + import org.bigbluebutton.common.LogUtil; + + private function changeLanguage():void { + ResourceUtil.getInstance().setPreferredLocale(ResourceUtil.getInstance().getLocaleCodeForIndex(selectedIndex)); + } + ]]> + + From 15974a22073b1365082e941bcc3ee6f057d0d022 Mon Sep 17 00:00:00 2001 From: Hugo Lazzari Date: Wed, 30 Jan 2013 09:43:48 -0200 Subject: [PATCH 02/13] Add selection color on languageSelector and method getPreferredLocaleName in ResourceUtil --- .../bigbluebutton/main/views/LanguageSelector.mxml | 11 ++++++++--- .../src/org/bigbluebutton/util/i18n/ResourceUtil.as | 10 +++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/LanguageSelector.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/LanguageSelector.mxml index 05b8c65d3c..a9d12d8a85 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/LanguageSelector.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/LanguageSelector.mxml @@ -31,19 +31,24 @@ mouseOut="removeHighlightItem()" enabled="true" width="100%" - paddingLeft="0" paddingRight="0" paddingTop="0" paddingBottom="0" > + paddingLeft="0" paddingRight="0" paddingTop="0" paddingBottom="0"> + paddingLeft="0" paddingRight="0" paddingTop="0" paddingBottom="0" id="boxCont" width="100%" backgroundColor="{data == ResourceUtil.getInstance().getPreferredLocaleName() ? 0x7FCEFF : 0xFFFFFF}"> diff --git a/bigbluebutton-client/src/org/bigbluebutton/util/i18n/ResourceUtil.as b/bigbluebutton-client/src/org/bigbluebutton/util/i18n/ResourceUtil.as index ddd4806759..5e11fee458 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/util/i18n/ResourceUtil.as +++ b/bigbluebutton-client/src/org/bigbluebutton/util/i18n/ResourceUtil.as @@ -119,6 +119,10 @@ package org.bigbluebutton.util.i18n } return -1; } + + public function getPreferredLocaleName():String { + return localeNames[localeIndex]; + } public function setPreferredLocale(locale:String):void { LogUtil.debug("Setting up preferred locale " + locale); @@ -142,10 +146,10 @@ package org.bigbluebutton.util.i18n */ loadResource(locale); } - + private function loadResource(language:String):IEventDispatcher { // Add a random string on the query so that we don't get a cached version. - + var date:Date = new Date(); var localeURI:String = 'locale/' + language + '_resources.swf?a=' + date.time; LogUtil.debug("Loading locale at [ " + localeURI + " ]"); @@ -234,4 +238,4 @@ package org.bigbluebutton.util.i18n } } -class SingletonEnforcer{} +class SingletonEnforcer{} From 60eda3852d485ee057e32413e3c9a3b7cac3e7dd Mon Sep 17 00:00:00 2001 From: Hugo Lazzari Date: Fri, 26 Jul 2013 11:15:47 -0300 Subject: [PATCH 03/13] Fixed error. --- .../src/org/bigbluebutton/util/i18n/ResourceUtil.as | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-client/src/org/bigbluebutton/util/i18n/ResourceUtil.as b/bigbluebutton-client/src/org/bigbluebutton/util/i18n/ResourceUtil.as index 99ed821a03..7eae74ad2c 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/util/i18n/ResourceUtil.as +++ b/bigbluebutton-client/src/org/bigbluebutton/util/i18n/ResourceUtil.as @@ -140,8 +140,8 @@ package org.bigbluebutton.util.i18n public function setPreferredLocale(locale:String):void { LogUtil.debug("Setting up preferred locale " + locale); - if (isPreferredLocaleAvailable(preferredLocale)) { - LogUtil.debug("The locale " + preferredLocale + " is available"); + if (isPreferredLocaleAvailable(locale)) { + LogUtil.debug("The locale " + locale + " is available"); preferredLocale = locale; }else{ LogUtil.debug("The locale " + preferredLocale + " isn't available. Default will be: " + MASTER_LOCALE); From 42e195bbe859efcaeb7e2b3658c74368c148f857 Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Mon, 26 Aug 2013 11:20:33 -0400 Subject: [PATCH 04/13] Rewrite the audio/video encoding components for BigBlueButton. I've been working on this for a while, and it's adapted from code that has been fairly well-tested on a wide variety of recordings. I've found it to do a more accurate job of combining multiple webcam files, and it should be more accurate in the audio as well. Another key feature is that it does fewer re-encoding steps during video processing, which should both speed it up and hopefully improve quality. The settings on the VP8 encoder have been tuned somewhat as well. --- .../core/lib/recordandplayback.rb | 26 ++ .../core/lib/recordandplayback/edl.rb | 67 +++ .../core/lib/recordandplayback/edl/audio.rb | 143 +++++++ .../core/lib/recordandplayback/edl/video.rb | 394 ++++++++++++++++++ .../lib/recordandplayback/generators/audio.rb | 79 ++-- .../generators/audio_processor.rb | 42 +- .../recordandplayback/generators/events.rb | 117 ++++++ .../lib/recordandplayback/generators/video.rb | 392 +++-------------- .../playback/presentation/playback.js | 31 +- .../scripts/process/presentation.rb | 15 +- .../scripts/publish/presentation.rb | 71 ++-- 11 files changed, 934 insertions(+), 443 deletions(-) create mode 100644 record-and-playback/core/lib/recordandplayback/edl.rb create mode 100644 record-and-playback/core/lib/recordandplayback/edl/audio.rb create mode 100644 record-and-playback/core/lib/recordandplayback/edl/video.rb diff --git a/record-and-playback/core/lib/recordandplayback.rb b/record-and-playback/core/lib/recordandplayback.rb index 0ab40a1999..a1289cfc19 100755 --- a/record-and-playback/core/lib/recordandplayback.rb +++ b/record-and-playback/core/lib/recordandplayback.rb @@ -111,4 +111,30 @@ module BigBlueButton end status end + + def self.exec_ret(*command) + BigBlueButton.logger.info "Executing: #{command.join(' ')}" + IO.popen([*command, :err => [:child, :out]]) do |io| + io.lines.each do |line| + BigBlueButton.logger.info line.chomp + end + end + BigBlueButton.logger.info "Exit status: #{$?.exitstatus}" + return $?.exitstatus + end + + def self.exec_redirect_ret(outio, *command) + BigBlueButton.logger.info "Executing: #{command.join(' ')}" + BigBlueButton.logger.info "Sending output to #{outio}" + IO.pipe do |r, w| + pid = spawn(*command, :out => outio, :err => w) + w.close + r.lines.each do |line| + BigBlueButton.logger.info line.chomp + end + Process.waitpid(pid) + BigBlueButton.logger.info "Exit status: #{$?.exitstatus}" + return $?.exitstatus + end + end end diff --git a/record-and-playback/core/lib/recordandplayback/edl.rb b/record-and-playback/core/lib/recordandplayback/edl.rb new file mode 100644 index 0000000000..2802207f44 --- /dev/null +++ b/record-and-playback/core/lib/recordandplayback/edl.rb @@ -0,0 +1,67 @@ +# encoding: UTF-8 + +require File.expand_path('../edl/video', __FILE__) +require File.expand_path('../edl/audio', __FILE__) + +module BigBlueButton + module EDL + FFMPEG = ['ffmpeg', '-y', '-v', 'warning', '-nostats'] + FFPROBE = ['ffprobe', '-v', 'warning', '-print_format', 'json', '-show_format', '-show_streams'] + + def self.encode(audio, video, format, output_basename, audio_offset = 0) + output = "#{output_basename}.#{format[:extension]}" + lastoutput = nil + format[:parameters].each_with_index do |pass, i| + BigBlueButton.logger.info "Performing video encode pass #{i}" + lastoutput = "#{output_basename}.encode.#{format[:extension]}" + ffmpeg_cmd = FFMPEG + ffmpeg_cmd += ['-i', video] if video + if audio + if audio_offset != 0 + ffmpeg_cmd += ['-itsoffset', ms_to_s(audio_offset)] + end + ffmpeg_cmd += ['-i', audio] + end + ffmpeg_cmd += [*pass, lastoutput] + Dir.chdir(File.dirname(output)) do + exitstatus = BigBlueButton.exec_ret(*ffmpeg_cmd) + raise "ffmpeg failed, exit code #{exitstatus}" if exitstatus != 0 + end + end + + # Some formats have post-processing to prepare for streaming + if format[:postprocess] + format[:postprocess].each_with_index do |pp, i| + BigBlueButton.logger.info "Performing post-processing step #{i}" + ppoutput = "#{output_basename}.pp#{i}.#{format[:extension]}" + cmd = pp.map do |arg| + case arg + when ':input' + lastoutput + when ':output' + ppoutput + else + arg + end + end + Dir.chdir(File.dirname(output)) do + exitstatus = BigBlueButton.exec_ret(*cmd) + raise "postprocess failed, exit code #{exitstatus}" if exitstatus != 0 + end + lastoutput = ppoutput + end + end + + FileUtils.mv(lastoutput, output) + + return output + end + + def self.ms_to_s(timestamp) + s = timestamp / 1000 + ms = timestamp % 1000 + "%d.%03d" % [s, ms] + end + + end +end diff --git a/record-and-playback/core/lib/recordandplayback/edl/audio.rb b/record-and-playback/core/lib/recordandplayback/edl/audio.rb new file mode 100644 index 0000000000..e4a421f576 --- /dev/null +++ b/record-and-playback/core/lib/recordandplayback/edl/audio.rb @@ -0,0 +1,143 @@ +# encoding: UTF-8 + +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +# +# Copyright (c) 2013 BigBlueButton Inc. and by respective authors. +# +# BigBlueButton 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 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 . + +module BigBlueButton + module EDL + module Audio + SOX = ['sox', '-q'] + SOX_WF_AUDIO_ARGS = ['-b', '16', '-c', '1', '-e', 'signed', '-r', '16000', '-L'] + SOX_WF_ARGS = [*SOX_WF_AUDIO_ARGS, '-t', 'wav'] + WF_EXT = 'wav' + + def self.dump(edl) + BigBlueButton.logger.debug "EDL Dump:" + edl.each do |entry| + BigBlueButton.logger.debug "---" + BigBlueButton.logger.debug " Timestamp: #{entry[:timestamp]}" + BigBlueButton.logger.debug " Audio:" + audio = entry[:audio] + if audio + BigBlueButton.logger.debug " #{audio[:filename]} at #{audio[:timestamp]}" + else + BigBlueButton.logger.debug " silence" + end + end + end + + def self.render(edl, output_basename) + sections = [] + audioinfo = {} + + BigBlueButton.logger.info "Pre-processing EDL" + for i in 0...(edl.length - 1) + # The render scripts use this to calculate cut lengths + edl[i][:next_timestamp] = edl[i+1][:timestamp] + # Build a list of audio files to read information from + if edl[i][:audio] + audioinfo[edl[i][:audio][:filename]] = {} + end + end + + BigBlueButton.logger.info "Reading source audio information" + audioinfo.keys.each do |audiofile| + BigBlueButton.logger.debug " #{audiofile}" + info = audio_info(audiofile) + BigBlueButton.logger.debug " sample rate: #{info[:sample_rate]}, duration: #{info[:duration]}" + + audioinfo[audiofile] = info + end + + BigBlueButton.logger.info "Generating sections" + for i in 0...(edl.length - 1) + + sox_cmd = SOX + entry = edl[i] + audio = entry[:audio] + duration = edl[i][:next_timestamp] - edl[i][:timestamp] + filename = "#{output_basename}.temp-%03d.#{WF_EXT}" % i + + if audio + BigBlueButton.logger.info " Using input #{audio[:filename]}" + sox_cmd += ['-m', *SOX_WF_AUDIO_ARGS, '-n', audio[:filename]] + else + BigBlueButton.logger.info " Generating silence" + sox_cmd += [*SOX_WF_AUDIO_ARGS, '-n'] + end + + BigBlueButton.logger.info " Outputting to #{filename}" + sox_cmd += [*SOX_WF_ARGS, filename] + sections << filename + + if audio + # If the audio file length is within 2% of where it should be, + # adjust the speed to match up timing. + # TODO: This should be part of the import logic somehow, since + # render can be run after cutting. + if ((duration - audioinfo[audio[:filename]][:duration]).to_f / duration).abs < 0.02 + speed = audioinfo[audio[:filename]][:duration].to_f / duration + BigBlueButton.logger.warn " Audio file length mismatch, adjusting speed to #{speed}" + sox_cmd += ['speed', speed.to_s, 'rate', '-h', audioinfo[audio[:filename]][:sample_rate].to_s] + end + + BigBlueButton.logger.info " Trimming from #{audio[:timestamp]} to #{audio[:timestamp] + duration}" + sox_cmd += ['trim', "#{ms_to_s(audio[:timestamp])}", "#{ms_to_s(audio[:timestamp] + duration)}"] + else + BigBlueButton.logger.info " Trimming to #{duration}" + sox_cmd += ['trim', '0.000', "#{ms_to_s(duration)}"] + end + + exitstatus = BigBlueButton.exec_ret(*sox_cmd) + raise "sox failed, exit code #{exitstatus}" if exitstatus != 0 + end + + output = "#{output_basename}.#{WF_EXT}" + BigBlueButton.logger.info "Concatenating sections to #{output}" + sox_cmd = [*SOX, *sections, *SOX_WF_ARGS, output] + exitstatus = BigBlueButton.exec_ret(*sox_cmd) + raise "sox failed, exit code #{exitstatus}" if exitstatus != 0 + + output + end + + # The methods below should be considered private + + def self.audio_info(filename) + IO.popen([*FFPROBE, filename]) do |probe| + info = JSON.parse(probe.read, :symbolize_names => true) + info[:audio] = info[:streams].find { |stream| stream[:codec_type] == 'audio' } + + if info[:audio] + info[:sample_rate] = info[:audio][:sample_rate].to_i + end + + info[:duration] = (info[:format][:duration].to_r * 1000).to_i + + return info + end + {} + end + + def self.ms_to_s(timestamp) + s = timestamp / 1000 + ms = timestamp % 1000 + "%d.%03d" % [s, ms] + end + end + end +end diff --git a/record-and-playback/core/lib/recordandplayback/edl/video.rb b/record-and-playback/core/lib/recordandplayback/edl/video.rb new file mode 100644 index 0000000000..be1ef98f32 --- /dev/null +++ b/record-and-playback/core/lib/recordandplayback/edl/video.rb @@ -0,0 +1,394 @@ +# encoding: UTF-8 + +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +# +# Copyright (c) 2013 BigBlueButton Inc. and by respective authors. +# +# BigBlueButton 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 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 . + +require 'json' + +module BigBlueButton + module EDL + module Video + FFMPEG_WF_CODEC = 'mpeg2video' + FFMPEG_WF_ARGS = ['-an', '-codec', FFMPEG_WF_CODEC, '-q:v', '2', '-pix_fmt', 'yuv420p', '-r', '24', '-f', 'mpegts'] + WF_EXT = 'ts' + + def self.dump(edl) + BigBlueButton.logger.debug "EDL Dump:" + edl.each do |entry| + BigBlueButton.logger.debug "---" + BigBlueButton.logger.debug " Timestamp: #{entry[:timestamp]}" + BigBlueButton.logger.debug " Video Areas:" + entry[:areas].each do |name, videos| + BigBlueButton.logger.debug " #{name}" + videos.each do |video| + BigBlueButton.logger.debug " #{video[:filename]} at #{video[:timestamp]}" + end + end + end + end + + def self.merge(*edls) + entries_i = Array.new(edls.length, 0) + done = Array.new(edls.length, false) + merged_edl = [ { :timestamp => 0, :areas => {} } ] + + while !done.all? + # Figure out what the next entry in each edl is + entries = [] + entries_i.each_with_index do |entry, edl| + entries << edls[edl][entry] + end + + # Find the next entry - the one with the lowest timestamp + next_edl = nil + next_entry = nil + entries.each_with_index do |entry, edl| + if entry + if !next_entry or entry[:timestamp] < next_entry[:timestamp] + next_edl = edl + next_entry = entry + end + end + end + + # To calculate differences, need the previous entry from the same edl + prev_entry = nil + if entries_i[next_edl] > 0 + prev_entry = edls[next_edl][entries_i[next_edl] - 1] + end + + # Find new videos that were added + add_areas = {} + if prev_entry + next_entry[:areas].each do |area, videos| + add_areas[area] = [] + if !prev_entry[:areas][area] + add_areas[area] = videos + else + videos.each do |video| + if !prev_entry[:areas][area].find { |v| v[:filename] == video[:filename] } + add_areas[area] << video + end + end + end + end + else + add_areas = next_entry[:areas] + end + + # Find videos that were removed + del_areas = {} + if prev_entry + prev_entry[:areas].each do |area, videos| + del_areas[area] = [] + if !next_entry[:areas][area] + del_areas[area] = videos + else + videos.each do |video| + if !next_entry[:areas][area].find { |v| v[:filename] == video[:filename] } + del_areas[area] << video + end + end + end + end + end + + # Determine whether to create a new entry or edit the previous one + merged_entry = { :timestamp => next_entry[:timestamp], :areas => {} } + last_entry = merged_edl.last + if last_entry[:timestamp] == next_entry[:timestamp] + # Edit the existing entry + merged_entry = last_entry + else + # Create a new entry + merged_entry = { :timestamp => next_entry[:timestamp], :areas => {} } + merged_edl << merged_entry + # Have to copy videos from the last entry into the new entry, updating timestamps + last_entry[:areas].each do |area, videos| + merged_entry[:areas][area] = videos.map do |video| + { + :filename => video[:filename], + :timestamp => video[:timestamp] + merged_entry[:timestamp] - last_entry[:timestamp] + } + end + end + end + + # Remove deleted videos + del_areas.each do |area, videos| + merged_entry[:areas][area] = merged_entry[:areas][area].reject do |video| + videos.find { |v| v[:filename] == video[:filename] } + end + end + # Add new videos + add_areas.each do |area, videos| + if !merged_entry[:areas][area] + merged_entry[:areas][area] = videos + else + merged_entry[:areas][area] += videos + end + end + + entries_i[next_edl] += 1 + if entries_i[next_edl] >= edls[next_edl].length + done[next_edl] = true + end + end + + merged_edl + end + + def self.render(edl, layout, output_basename) + videoinfo = {} + + BigBlueButton.logger.info "Pre-processing EDL" + for i in 0...(edl.length - 1) + # The render scripts need this to calculate cut lengths + edl[i][:next_timestamp] = edl[i+1][:timestamp] + # Have to fetch information about all the input video files, + # so collect them. + edl[i][:areas].each do |name, videos| + videos.each do |video| + videoinfo[video[:filename]] = {} + end + end + end + + BigBlueButton.logger.info "Reading source video information" + videoinfo.keys.each do |videofile| + BigBlueButton.logger.debug " #{videofile}" + info = video_info(videofile) + BigBlueButton.logger.debug " width: #{info[:width]}, height: #{info[:height]}, duration: #{info[:duration]}" + + videoinfo[videofile] = info + end + + BigBlueButton.logger.info "Generating missing video end events" + videoinfo.each do |filename, info| + + edl.each_with_index do |event, index| + + new_entry = { :areas => {} } + add_new_entry = false + event[:areas].each do |area, videos| + videos.each do |video| + if video[:filename] == filename + if video[:timestamp] > info[:duration] + videos.delete(video) + # Note that I'm using a 5-second fuzz factor here. + # If there's a stop event within 5 seconds of the video ending, don't bother to generate + # an extra event. + elsif video[:timestamp] + (event[:next_timestamp] - event[:timestamp]) > info[:duration] + 5000 + BigBlueButton.logger.warn "Over-long video #{video[:filename]}, synthesizing stop event" + new_entry[:timestamp] = event[:timestamp] + info[:duration] - video[:timestamp] + new_entry[:next_timestamp] = event[:next_timestamp] + event[:next_timestamp] = new_entry[:timestamp] + add_new_entry = true + end + end + end + end + + if add_new_entry + event[:areas].each do |area, videos| + new_entry[:areas][area] = videos.select do |video| + video[:filename] != filename + end.map do |video| + { + :filename => video[:filename], + :timestamp => video[:timestamp] + new_entry[:timestamp] - event[:timestamp] + } + end + end + edl.insert(index + 1, new_entry) + end + + end + + end + dump(edl) + + BigBlueButton.logger.info "Compositing cuts" + render = "#{output_basename}.#{WF_EXT}" + for i in 0...(edl.length - 1) + if edl[i][:timestamp] == edl[i][:next_timestamp] + warn 'Skipping 0-length edl entry' + next + end + composite_cut(render, edl[i], layout, videoinfo) + end + + return render + end + + # The methods below are for private use + + def self.video_info(filename) + IO.popen([*FFPROBE, filename]) do |probe| + info = JSON.parse(probe.read, :symbolize_names => true) + info[:video] = info[:streams].find { |stream| stream[:codec_type] == 'video' } + info[:audio] = info[:streams].find { |stream| stream[:codec_type] == 'audio' } + + if info[:video] + info[:width] = info[:video][:width].to_i + info[:height] = info[:video][:height].to_i + + info[:aspect_ratio] = info[:video][:display_aspect_ratio].to_r + if info[:aspect_ratio] == 0 + info[:aspect_ratio] = Rational(info[:width], info[:height]) + end + end + + # Convert the duration to milliseconds + info[:duration] = (info[:format][:duration].to_r * 1000).to_i + + return info + end + {} + end + + def self.ms_to_s(timestamp) + s = timestamp / 1000 + ms = timestamp % 1000 + "%d.%03d" % [s, ms] + end + + def self.aspect_scale(old_width, old_height, new_width, new_height) + if old_width.to_f / old_height > new_width.to_f / new_height + [new_width, old_height * new_width / old_width] + else + [old_width * new_height / old_height, new_height] + end + end + + def self.pad_offset(video_width, video_height, area_width, area_height) + [(area_width - video_width) / 2, (area_height - video_height) / 2] + end + + def self.composite_cut(output, cut, layout, videoinfo) + stop_ts = cut[:next_timestamp] - cut[:timestamp] + BigBlueButton.logger.info " Cut start time #{cut[:timestamp]}, duration #{stop_ts}" + + ffmpeg_inputs = [] + ffmpeg_filter = "color=c=white:s=#{layout[:width]}x#{layout[:height]}:r=24" + + index = 0 + + layout[:areas].each do |layout_area| + area = cut[:areas][layout_area[:name]] + video_count = area.length + BigBlueButton.logger.debug " Laying out #{video_count} videos in #{layout_area[:name]}" + + tile_offset_x = layout_area[:x] + tile_offset_y = layout_area[:y] + + tiles_h = 0 + tiles_v = 0 + tile_width = 0 + tile_height = 0 + total_area = 0 + + # Do an exhaustive search to maximize video areas + for tmp_tiles_v in 1..video_count + tmp_tiles_h = (video_count / tmp_tiles_v.to_f).ceil + tmp_tile_width = layout_area[:width] / tmp_tiles_h + tmp_tile_height = layout_area[:height] / tmp_tiles_v + next if tmp_tile_width <= 0 or tmp_tile_height <= 0 + + tmp_total_area = 0 + area.each do |video| + video_width = videoinfo[video[:filename]][:width] + video_height = videoinfo[video[:filename]][:height] + scale_width, scale_height = aspect_scale(video_width, video_height, tmp_tile_width, tmp_tile_height) + tmp_total_area += scale_width * scale_height + end + + if tmp_total_area > total_area + tiles_h = tmp_tiles_h + tiles_v = tmp_tiles_v + tile_width = tmp_tile_width + tile_height = tmp_tile_height + total_area = tmp_total_area + end + end + + tile_x = 0 + tile_y = 0 + + BigBlueButton.logger.debug " Tiling in a #{tiles_h}x#{tiles_v} grid" + + area.each do |video| + BigBlueButton.logger.debug " clip ##{index}" + BigBlueButton.logger.debug " tile location (#{tile_x}, #{tile_y})" + video_width = videoinfo[video[:filename]][:width] + video_height = videoinfo[video[:filename]][:height] + BigBlueButton.logger.debug " original size: #{video_width}x#{video_height}" + + scale_width, scale_height = aspect_scale(video_width, video_height, tile_width, tile_height) + BigBlueButton.logger.debug " scaled size: #{scale_width}x#{scale_height}" + + offset_x, offset_y = pad_offset(scale_width, scale_height, tile_width, tile_height) + offset_x += tile_offset_x + (tile_x * tile_width) + offset_y += tile_offset_y + (tile_y * tile_height) + BigBlueButton.logger.debug " offset: left: #{offset_x}, top: #{offset_y}" + + BigBlueButton.logger.debug " start timestamp: #{video[:timestamp]}" + + # Seeking can be pretty inaccurate. + # Seek to before the video start; we'll do accurate trimming in + # a filter. + seek = video[:timestamp] - 30000 + seek = 0 if seek < 0 + + ffmpeg_inputs << { + :filename => video[:filename], + :seek => seek + } + ffmpeg_filter << "[in#{index}]; [#{index}]fps=24,select=gte(t\\,#{ms_to_s(video[:timestamp])}),setpts=PTS-STARTPTS,scale=#{scale_width}:#{scale_height}" + if layout_area[:pad] + ffmpeg_filter << ",pad=w=#{tile_width}:h=#{tile_height}:x=#{offset_x}:y=#{offset_y}:color=white" + offset_x = 0 + offset_y = 0 + end + ffmpeg_filter << "[mv#{index}]; [in#{index}][mv#{index}] overlay=#{offset_x}:#{offset_y}" + + tile_x += 1 + if tile_x >= tiles_h + tile_x = 0 + tile_y += 1 + end + index += 1 + end + end + + ffmpeg_cmd = [*FFMPEG] + ffmpeg_inputs.each do |input| + ffmpeg_cmd += ['-ss', ms_to_s(input[:seek]), '-itsoffset', ms_to_s(input[:seek]), '-i', input[:filename]] + end + ffmpeg_cmd += ['-to', ms_to_s(stop_ts), '-filter_complex', ffmpeg_filter, *FFMPEG_WF_ARGS, '-'] + + File.open(output, 'a') do |outio| + exitstatus = BigBlueButton.exec_redirect_ret(outio, *ffmpeg_cmd) + raise "ffmpeg failed, exit code #{exitstatus}" if exitstatus != 0 + end + + return output + end + + end + end +end diff --git a/record-and-playback/core/lib/recordandplayback/generators/audio.rb b/record-and-playback/core/lib/recordandplayback/generators/audio.rb index adc750d966..f11385e0dc 100755 --- a/record-and-playback/core/lib/recordandplayback/generators/audio.rb +++ b/record-and-playback/core/lib/recordandplayback/generators/audio.rb @@ -178,6 +178,56 @@ module BigBlueButton return [create_gap_audio_event(last_event - first_event + 1, last_event, first_event)] end end + + def self.create_audio_edl(archive_dir) + audio_edl = [] + audio_dir = "#{archive_dir}/audio" + events = Nokogiri::XML(File.open("#{archive_dir}/events.xml")) + + event = events.at_xpath('/recording/event[position()=1]') + initial_timestamp = event['timestamp'].to_i + event = events.at_xpath('/recording/event[position()=last()]') + final_timestamp = event['timestamp'].to_i + + # Initially start with silence + audio_edl << { + :timestamp => 0, + :audio => nil + } + + # Add events for recording start/stop + events.xpath('/recording/event[@module="VOICE"]').each do |event| + timestamp = event['timestamp'].to_i - initial_timestamp + case event['eventname'] + when 'StartRecordingEvent' + filename = event.at_xpath('filename').text + filename = "#{audio_dir}/#{File.basename(filename)}" + audio_edl << { + :timestamp => timestamp, + :audio => { :filename => filename, :timestamp => 0 } + } + when 'StopRecordingEvent' + filename = event.at_xpath('filename').text + filename = "#{audio_dir}/#{File.basename(filename)}" + if audio_edl.last[:audio] && audio_edl.last[:audio][:filename] == filename + audio_edl << { + :timestamp => timestamp, + :audio => nil + } + end + end + end + + audio_edl << { + :timestamp => final_timestamp - initial_timestamp, + :audio => nil + } + + return audio_edl + end + + + TIMESTAMP = 'timestamp' BRIDGE = 'bridge' @@ -289,29 +339,6 @@ module BigBlueButton File.rename(temp_wav_file, file) end end - - # Stretch/squish the length of the audio file to match the requested length - # The length parameter should be in milliseconds. - # Returns the filename of the new file (in the same directory as the original) - def self.stretch_audio_file(file, length, sample_rate) - BigBlueButton.logger.info("Task: Stretching/Squishing Audio") - orig_length = determine_length_of_audio_from_file(file) - new_file = "#{file}.stretch.wav" - - if (orig_length == 0) - BigBlueButton.logger.error("Stretch received 0-length file as input!") - # Generate silence to fill the length - generate_silence(length, new_file, sample_rate) - return new_file - end - - speed = orig_length.to_f / length.to_f - BigBlueButton.logger.info("Adjusting #{file} speed to #{speed}") - sox_cmd = "sox #{file} #{new_file} speed #{speed} rate -h #{sample_rate} trim 0 #{length.to_f/1000}" - - BigBlueButton.execute(sox_cmd) - return new_file - end # Determine the audio padding we need to generate. def self.generate_audio_paddings(events, events_xml) @@ -341,7 +368,7 @@ module BigBlueButton if (not ar_prev.eql?(ar_next)) length_of_gap = ar_next.start_event_timestamp.to_i - ar_prev.stop_event_timestamp.to_i - # Check if the silence is greater that 10 minutes long. If it is, assume something went wrong with the + # Check if the silence is greater than 1 hour long. If it is, assume something went wrong with the # recording. This prevents us from generating a veeeerrryyy looonnngggg silence maxing disk space. if (length_of_gap < 3600000) if (length_of_gap < 0) @@ -358,9 +385,9 @@ module BigBlueButton end length_of_gap = BigBlueButton::Events.last_event_timestamp(events_xml).to_i - events[-1].stop_event_timestamp.to_i - # Check if the silence is greater that 10 minutes long. If it is, assume something went wrong with the + # Check if the silence is greater than 2 hours long. If it is, assume something went wrong with the # recording. This prevents us from generating a veeeerrryyy looonnngggg silence maxing disk space. - if (length_of_gap < 3600000) + if (length_of_gap < 7200000) if (length_of_gap < 0) trim_audio_file(events[-1].file, length_of_gap.abs) else diff --git a/record-and-playback/core/lib/recordandplayback/generators/audio_processor.rb b/record-and-playback/core/lib/recordandplayback/generators/audio_processor.rb index 9c1ba04852..21c1eca3d8 100755 --- a/record-and-playback/core/lib/recordandplayback/generators/audio_processor.rb +++ b/record-and-playback/core/lib/recordandplayback/generators/audio_processor.rb @@ -22,37 +22,33 @@ require 'fileutils' +require File.expand_path('../../edl', __FILE__) + module BigBlueButton class AudioProcessor # Process the raw recorded audio to ogg file. # archive_dir - directory location of the raw archives. Assumes there is audio file and events.xml present. - # ogg_file - the file name of the ogg audio output + # file_basename - the file name of the audio output. '.webm' will be added # - def self.process(archive_dir, ogg_file) + def self.process(archive_dir, file_basename) + audio_edl = BigBlueButton::AudioEvents.create_audio_edl(archive_dir) + audio_dir = "#{archive_dir}/audio" events_xml = "#{archive_dir}/events.xml" - audio_events = BigBlueButton::AudioEvents.process_events(audio_dir, events_xml) - audio_files = [] - first_no_silence = audio_events.select { |e| !e.padding }.first - sampling_rate = first_no_silence.nil? ? 16000 : FFMPEG::Movie.new(first_no_silence.file).audio_sample_rate - audio_events.each do |ae| - if ae.padding - ae.file = "#{audio_dir}/#{ae.length_of_gap}.wav" - BigBlueButton::AudioEvents.generate_silence(ae.length_of_gap, ae.file, sampling_rate) - else - # Substitute the original file location with the archive location - orig_file = ae.file.sub(/.+\//, "#{audio_dir}/") - length = ae.stop_event_timestamp.to_i - ae.start_event_timestamp.to_i - ae.file = BigBlueButton::AudioEvents.stretch_audio_file(orig_file, length, sampling_rate) - end - - audio_files << ae.file - end - - wav_file = "#{audio_dir}/recording.wav" - BigBlueButton::AudioEvents.concatenate_audio_files(audio_files, wav_file) - BigBlueButton::AudioEvents.wav_to_ogg(wav_file, ogg_file) + wav_file = BigBlueButton::EDL::Audio.render(audio_edl, "#{audio_dir}/recording") + + ogg_format = { + :extension => 'ogg', + :parameters => [ [ '-c:a', 'libvorbis', '-b:a', '32K', '-f', 'ogg' ] ] + } + BigBlueButton::EDL.encode(wav_file, nil, ogg_format, file_basename) + + webm_format = { + :extension => 'webm', + :parameters => [ [ '-c:a', 'libvorbis', '-b:a', '32K', '-f', 'webm' ] ] + } + BigBlueButton::EDL.encode(wav_file, nil, webm_format, file_basename) end end end diff --git a/record-and-playback/core/lib/recordandplayback/generators/events.rb b/record-and-playback/core/lib/recordandplayback/generators/events.rb index 28207d1a89..4d26b9ae1d 100755 --- a/record-and-playback/core/lib/recordandplayback/generators/events.rb +++ b/record-and-playback/core/lib/recordandplayback/generators/events.rb @@ -101,6 +101,78 @@ module BigBlueButton end stop_events end + + # Build a webcam EDL + def self.create_webcam_edl(archive_dir) + events = Nokogiri::XML(File.open("#{archive_dir}/events.xml")) + + recording = events.at_xpath('/recording') + meeting_id = recording['meeting_id'] + event = events.at_xpath('/recording/event[position()=1]') + initial_timestamp = event['timestamp'].to_i + event = events.at_xpath('/recording/event[position()=last()]') + final_timestamp = event['timestamp'].to_i + + video_dir = "#{archive_dir}/video/#{meeting_id}" + + videos = {} + active_videos = [] + video_edl = [] + + video_edl << { + :timestamp => 0, + :areas => { :webcam => [] } + } + + events.xpath('/recording/event[@module="WEBCAM"]').each do |event| + timestamp = event['timestamp'].to_i - initial_timestamp + case event['eventname'] + when 'StartWebcamShareEvent' + stream = event.at_xpath('stream').text + filename = "#{video_dir}/#{stream}.flv" + + videos[filename] = { :timestamp => timestamp } + active_videos << filename + + edl_entry = { + :timestamp => timestamp, + :areas => { :webcam => [] } + } + active_videos.each do |filename| + edl_entry[:areas][:webcam] << { + :filename => filename, + :timestamp => timestamp - videos[filename][:timestamp] + } + end + video_edl << edl_entry + when 'StopWebcamShareEvent' + stream = event.at_xpath('stream').text + filename = "#{video_dir}/#{stream}.flv" + + active_videos.delete(filename) + + edl_entry = { + :timestamp => timestamp, + :areas => { :webcam => [] } + } + active_videos.each do |filename| + edl_entry[:areas][:webcam] << { + :filename => filename, + :timestamp => timestamp - videos[filename][:timestamp] + } + end + video_edl << edl_entry + end + end + + video_edl << { + :timestamp => final_timestamp - initial_timestamp, + :areas => { :webcam => [] } + } + + return video_edl + end + # Determine if the start and stop event matched. def self.deskshare_event_matched?(stop_events, start) @@ -152,6 +224,51 @@ module BigBlueButton stop_events.sort {|a, b| a[:stop_timestamp] <=> b[:stop_timestamp]} end + def self.create_deskshare_edl(archive_dir) + events = Nokogiri::XML(File.open("#{archive_dir}/events.xml")) + + event = events.at_xpath('/recording/event[position()=1]') + initial_timestamp = event['timestamp'].to_i + event = events.at_xpath('/recording/event[position()=last()]') + final_timestamp = event['timestamp'].to_i + + deskshare_edl = [] + + deskshare_edl << { + :timestamp => 0, + :areas => { :deskshare => [] } + } + + events.xpath('/recording/event[@module="Deskshare"]').each do |event| + timestamp = event['timestamp'].to_i - initial_timestamp + case event['eventname'] + when 'DeskshareStartedEvent' + filename = event.at_xpath('file').text + filename = "#{archive_dir}/deskshare/#{File.basename(filename)}" + deskshare_edl << { + :timestamp => timestamp, + :areas => { + :deskshare => [ + { :filename => filename, :timestamp => 0 } + ] + } + } + when 'DeskshareStoppedEvent' + deskshare_edl << { + :timestamp => timestamp, + :areas => { :deskshare => [] } + } + end + end + + deskshare_edl << { + :timestamp => final_timestamp - initial_timestamp, + :areas => {} + } + + return deskshare_edl + end + def self.linkify( text ) generic_URL_regexp = Regexp.new( '(^|[\n ])([\w]+?://[\w]+[^ \"\n\r\t<]*)', Regexp::MULTILINE | Regexp::IGNORECASE ) starts_with_www_regexp = Regexp.new( '(^|[\n ])((www)\.[^ \"\t\n\r<]*)', Regexp::MULTILINE | Regexp::IGNORECASE ) diff --git a/record-and-playback/core/lib/recordandplayback/generators/video.rb b/record-and-playback/core/lib/recordandplayback/generators/video.rb index d4ba877ba1..920064130f 100755 --- a/record-and-playback/core/lib/recordandplayback/generators/video.rb +++ b/record-and-playback/core/lib/recordandplayback/generators/video.rb @@ -22,11 +22,12 @@ require 'rubygems' require 'streamio-ffmpeg' -require 'pp' + +require File.expand_path('../../edl', __FILE__) module BigBlueButton - # use "info" or "debug" for development - FFMPEG_LOG_LEVEL = "fatal" + + FFMPEG_CMD_BASE="ffmpeg -loglevel warning -nostats" # Strips the audio stream from the video file # video_in - the FLV file that needs to be stripped of audio @@ -35,7 +36,7 @@ module BigBlueButton # strip_audio_from_video(orig-video.flv, video2.flv) def self.strip_audio_from_video(video_in, video_out) BigBlueButton.logger.info("Task: Stripping audio from video") - command = "ffmpeg -y -i #{video_in} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -an -vcodec copy -sameq #{video_out}" + command = "#{FFMPEG_CMD_BASE} -i #{video_in} -an -vcodec copy #{video_out}" BigBlueButton.execute(command) # TODO: check for result, raise an exception when there is an error end @@ -47,14 +48,7 @@ module BigBlueButton # video_out - the resulting new video def self.trim_video(start, duration, video_in, video_out) BigBlueButton.logger.info("Task: Trimming video") - command = "" - if duration != (BigBlueButton.get_video_duration(video_in) * 1000).to_i - # -ss coming before AND after the -i option improves the precision ==> http://ffmpeg.org/pipermail/ffmpeg-user/2012-August/008767.html - command = "ffmpeg -y -ss #{BigBlueButton.ms_to_strtime(start)} -i #{video_in} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -vcodec copy -acodec copy -ss 0 -t #{BigBlueButton.ms_to_strtime(duration)} -sameq #{video_out}" - else - BigBlueButton.logger.info("The video has exactly the same duration as requested, so it will be just copied") - command = "ffmpeg -y -i #{video_in} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -vcodec copy -acodec copy -sameq #{video_out}" - end + command = "#{FFMPEG_CMD_BASE} -i #{video_in} -vcodec copy -acodec copy -ss #{BigBlueButton.ms_to_strtime(start)} -t #{BigBlueButton.ms_to_strtime(duration)} #{video_out}" BigBlueButton.execute(command) # TODO: check for result, raise an exception when there is an error end @@ -68,7 +62,7 @@ module BigBlueButton # create_blank_video(15, 1000, canvas.jpg, blank-video.flv) def self.create_blank_deskshare_video(length, rate, blank_canvas, video_out) BigBlueButton.logger.info("Task: Creating blank deskshare video") - command = "ffmpeg -y -loop 1 -t #{length} -i #{blank_canvas} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -r #{rate} -vcodec flashsv #{video_out}" + command = "#{FFMPEG_CMD_BASE} -loop 1 -i #{blank_canvas} -t #{length} -r #{rate} -vcodec flashsv #{video_out}" BigBlueButton.execute(command) # TODO: check for result, raise exception when there is an error end @@ -82,7 +76,7 @@ module BigBlueButton # create_blank_video(15, 1000, canvas.jpg, blank-video.flv) def self.create_blank_video(length, rate, blank_canvas, video_out) BigBlueButton.logger.info("Task: Creating blank video") - command = "ffmpeg -y -loop 1 -t #{length} -i #{blank_canvas} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -r #{rate} #{video_out}" + command = "#{FFMPEG_CMD_BASE} -y -loop 1 -i #{blank_canvas} -t #{length} -r #{rate} #{video_out}" BigBlueButton.execute(command) # TODO: check for result, raise exception when there is an error end @@ -144,7 +138,7 @@ module BigBlueButton #Converts flv to mpg def self.convert_flv_to_mpg(flv_video, mpg_video_out) - command = "ffmpeg -y -i #{flv_video} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -sameq -f mpegts -r 29.97 #{mpg_video_out}" + command = "#{FFMPEG_CMD_BASE} -i #{flv_video} -same_quant -f mpegts -r 29.97 #{mpg_video_out}" BigBlueButton.logger.info("Task: Converting .flv to .mpg") BigBlueButton.execute(command) end @@ -158,7 +152,7 @@ module BigBlueButton #Converts .mpg to .flv def self.convert_mpg_to_flv(mpg_video,flv_video_out) - command = "ffmpeg -y -i #{mpg_video} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -sameq #{flv_video_out}" + command = "#{FFMPEG_CMD_BASE} -i #{mpg_video} -same_quant #{flv_video_out}" BigBlueButton.logger.info("Task: Converting .mpg to .flv") BigBlueButton.execute(command); end @@ -169,7 +163,7 @@ module BigBlueButton # video - the video file. Must not contain an audio stream. def self.multiplex_audio_and_video(audio, video, video_out) BigBlueButton.logger.info("Task: Multiplexing audio and video") - command = "ffmpeg -y -i #{audio} -i #{video} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -map 1:0 -map 0:0 -ar 22050 #{video_out}" + command = "#{FFMPEG_CMD_BASE} -i #{audio} -i #{video} -map 1:0 -map 0:0 #{video_out}" BigBlueButton.execute(command) # TODO: check result, raise an exception when there is an error end @@ -409,7 +403,7 @@ module BigBlueButton # Use for newer version of FFMPEG padding = "-vf pad=#{MAX_VID_WIDTH}:#{MAX_VID_HEIGHT}:#{side_padding}:#{top_bottom_padding}:FFFFFF" - command = "ffmpeg -y -i #{stripped_webcam} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -aspect 4:3 -r 1000 -sameq #{frame_size} #{padding} #{scaled_flv}" + command = "#{FFMPEG_CMD_BASE} -i #{stripped_webcam} -aspect 4:3 -r 1000 -same_quant #{frame_size} #{padding} #{scaled_flv}" #BigBlueButton.logger.info(command) #IO.popen(command) #Process.wait @@ -458,7 +452,7 @@ module BigBlueButton # Use for newer version of FFMPEG padding = "-vf pad=#{MAX_VID_WIDTH}:#{MAX_VID_HEIGHT}:#{side_padding}:#{top_bottom_padding}:FFFFFF" - command = "ffmpeg -y -i #{flv_in} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -aspect 4:3 -r 1000 -sameq #{frame_size} #{padding} -vcodec flashsv #{scaled_flv}" + command = "#{FFMPEG_CMD_BASE} -i #{flv_in} -aspect 4:3 -r 1000 -same_quant #{frame_size} #{padding} -vcodec flashsv #{scaled_flv}" BigBlueButton.execute(command) #BigBlueButton.logger.info(command) #IO.popen(command) @@ -479,342 +473,54 @@ module BigBlueButton t = Time.at(ms / 1000, (ms % 1000) * 1000) return t.getutc.strftime("%H:%M:%S.%L") end - - def self.hash_to_str(hash) - return PP.pp(hash, "") - end - - def self.calculate_videos_grid(videos_list, details, output_video) - # try to find the number of rows and columns to maximize the internal videos - each_row = 0 - num_rows = 0 - slot_width = 0 - slot_height = 0 - total_area = 0 - num_cams = videos_list.length - for tmp_num_rows in 1..(num_cams) - tmp_each_row = (num_cams / tmp_num_rows.to_f).ceil - max_width = (output_video[:width] / tmp_each_row).floor - max_height = (output_video[:height] / tmp_num_rows).floor - if max_width <= 0 or max_height <= 0 then - next - end - - tmp_total_area = 0 - videos_list.each do |stream| - measurements = BigBlueButton.fit_to(details[stream][:width], details[stream][:height], max_width, max_height) - tmp_total_area += measurements[:width] * measurements[:height] - end - - if tmp_total_area > total_area - slot_width = max_width - slot_height = max_height - num_rows = tmp_num_rows - each_row = tmp_each_row - total_area = tmp_total_area - end - end - - return { - :rows => num_rows, - :columns => each_row, - :cell_width => slot_width, - :cell_height => slot_height - } - end - - def self.calculate_video_position_and_size(videos_list, details, output_video, grid) - transformation = Hash.new - videos_list.each_with_index do |stream, index| - video_dimensions = BigBlueButton.fit_to(details[stream][:width], details[stream][:height], grid[:cell_width], grid[:cell_height]) - slot_x = (index%grid[:columns]) * grid[:cell_width] + (output_video[:width] - grid[:cell_width] * grid[:columns]) / 2 - slot_y = (index/grid[:columns]).floor * grid[:cell_height] + (output_video[:height] - grid[:cell_height] * grid[:rows]) / 2 - x = slot_x + (grid[:cell_width] - video_dimensions[:width]) / 2 - y = slot_y + (grid[:cell_height] - video_dimensions[:height]) / 2 - - transformation[stream] = { - :x => x, - :y => y, - :width => video_dimensions[:width], - :height => video_dimensions[:height] - } - end - return transformation - end - - def self.get_video_dir(temp_dir, meeting_id, type="video") - dir = nil - if type == "video" - dir = "#{temp_dir}/#{meeting_id}/video/#{meeting_id}" - elsif type == "deskshare" - dir = "#{temp_dir}/#{meeting_id}/deskshare" - end - return dir - end - + def BigBlueButton.process_multiple_videos(target_dir, temp_dir, meeting_id, output_width, output_height, audio_offset, include_deskshare=false) BigBlueButton.logger.info("Processing webcam videos") # Process audio - BigBlueButton::AudioProcessor.process("#{temp_dir}/#{meeting_id}", "#{target_dir}/audio.ogg") + audio_edl = BigBlueButton::AudioEvents.create_audio_edl( + "#{temp_dir}/#{meeting_id}") + BigBlueButton::EDL::Audio.dump(audio_edl) + + audio_file = BigBlueButton::EDL::Audio.render( + audio_edl, "#{target_dir}/webcams") # Process video - events_xml = "#{temp_dir}/#{meeting_id}/events.xml" - first_timestamp = BigBlueButton::Events.first_event_timestamp(events_xml) - last_timestamp = BigBlueButton::Events.last_event_timestamp(events_xml) - start_evt = BigBlueButton::Events.get_start_video_events(events_xml) - stop_evt = BigBlueButton::Events.get_stop_video_events(events_xml) + webcam_edl = BigBlueButton::Events.create_webcam_edl( + "#{temp_dir}/#{meeting_id}") + deskshare_edl = BigBlueButton::Events.create_deskshare_edl( + "#{temp_dir}/#{meeting_id}") + video_edl = BigBlueButton::EDL::Video.merge(webcam_edl, deskshare_edl) + BigBlueButton::EDL::Video.dump(video_edl) - start_evt.each {|evt| evt[:stream_type] = "video"} - stop_evt.each {|evt| evt[:stream_type] = "video"} + layout = { + :width => output_width, :height => output_height, + :areas => [ { :name => :webcam, :x => 0, :y => 0, + :width => output_width, :height => output_height } ] + } if include_deskshare - start_deskshare_evt = BigBlueButton::Events.get_start_deskshare_events(events_xml) - stop_deskshare_evt = BigBlueButton::Events.get_stop_deskshare_events(events_xml) - start_deskshare_evt.each {|evt| evt[:stream_type] = "deskshare"} - stop_deskshare_evt.each {|evt| evt[:stream_type] = "deskshare"} - start_evt = start_evt + start_deskshare_evt - stop_evt = stop_evt + stop_deskshare_evt + layout[:areas] += [ { :name => :deskshare, :x => 0, :y => 0, + :width => output_width, :height => output_height, :pad => true } ] end + video_file = BigBlueButton::EDL::Video.render( + video_edl, layout, "#{target_dir}/webcams") - (start_evt + stop_evt).each do |evt| - ext = File.extname(evt[:stream]) - ext = ".flv" if ext.empty? - evt[:file_extension] = ext - # removes the file extension from :stream - evt[:stream].chomp!(File.extname(evt[:stream])) - evt[:stream_file_name] = evt[:stream] + evt[:file_extension] - end - - # fix the stop events list so the matched events will be consistent - start_evt.each do |evt| - video_dir = get_video_dir(temp_dir, meeting_id, evt[:stream_type]) - if stop_evt.select{ |s| s[:stream] == evt[:stream] }.empty? - new_event = { - :stream => evt[:stream], - :stop_timestamp => evt[:start_timestamp] + (BigBlueButton.get_video_duration("#{video_dir}/#{evt[:stream_file_name]}") * 1000).to_i - } - BigBlueButton.logger.debug("Adding stop event: #{new_event}") - stop_evt << new_event - end - end - - # fix the start events list so the matched events will be consistent - stop_evt.each do |evt| - if start_evt.select{ |s| s[:stream] == evt[:stream] }.empty? - new_event = { - :stream => evt[:stream], - :start_timestamp => evt[:stop_timestamp] - (BigBlueButton.get_video_duration("#{video_dir}/#{evt[:stream]}.flv") * 1000).to_i - } - BigBlueButton.logger.debug("Adding stop event: #{new_event}") - start_evt << new_event - end - end - - matched_evts = BigBlueButton::Events.match_start_and_stop_video_events(start_evt, stop_evt) - - BigBlueButton.logger.debug("First timestamp: #{first_timestamp}") - BigBlueButton.logger.debug("Last timestamp: #{last_timestamp}") - BigBlueButton.logger.debug("Matched events:") - BigBlueButton.logger.debug(hash_to_str(matched_evts)) - - video_streams = Hash.new - - matched_evts.each do |evt| - video_dir = BigBlueButton.get_video_dir(temp_dir, meeting_id, evt[:stream_type]) - # removes audio stream - stripped_webcam = "#{temp_dir}/#{meeting_id}/stripped-#{evt[:stream]}.flv" - BigBlueButton.strip_audio_from_video("#{video_dir}/#{evt[:stream_file_name]}", stripped_webcam) - - # the encoder for Flash Screen Codec v2 doesn't work on the current version of FFmpeg, so trim doesn't work at all - # in this step the desktop sharing file is re-encoded with Flash Screen Codec v1 so everything works fine - if evt[:stream_type] == "deskshare" - processed_deskshare = "#{temp_dir}/#{meeting_id}/stripped-#{evt[:stream]}_processed.flv" - command = "ffmpeg -y -i #{stripped_webcam} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -vcodec flashsv -sameq #{processed_deskshare}" - BigBlueButton.execute(command) - stripped_webcam = processed_deskshare - end - - video_stream = { - :name => evt[:stream], - :file => stripped_webcam, - :duration => (BigBlueButton.get_video_duration(stripped_webcam) * 1000).to_i, - :width => BigBlueButton.get_video_width(stripped_webcam), - :height => BigBlueButton.get_video_height(stripped_webcam), - :start => evt[:start_timestamp] - first_timestamp, - :stop => evt[:stop_timestamp] - first_timestamp, - :stream_type => evt[:stream_type] + formats = [ + { + :extension => 'webm', + :parameters => [ + [ '-c:v', 'libvpx', '-crf', '34', '-b:v', '60M', + '-threads', '2', '-deadline', 'good', '-cpu-used', '3', + '-c:a', 'libvorbis', '-b:a', '32K', + '-f', 'webm' ] + ] } - video_stream[:start_error] = video_stream[:stop] - (video_stream[:start] + video_stream[:duration]) - # adjust the start_timestamp based on the video duration - video_stream[:start] = video_stream[:start] + video_stream[:start_error] - video_streams[video_stream[:name]] = video_stream + ] + formats.each do |format| + filename = BigBlueButton::EDL::encode( + audio_file, video_file, format, "#{target_dir}/webcams", audio_offset) end - BigBlueButton.logger.debug("Video streams:") - BigBlueButton.logger.debug(hash_to_str(video_streams)) - - # if we include desktop sharing streams to the video, we will use the highest resolution as the output video resolution in order to preserve the quality of the desktop sharing - deskshare_streams = video_streams.select{ |stream_name, stream_details| stream_details[:stream_type] == "deskshare" } - if not deskshare_streams.empty? - max = deskshare_streams.values().max_by {|stream| stream[:width] * stream[:height]} - output_width = max[:width] - output_height = max[:height] - BigBlueButton.logger.debug("Modifying the output video resolution to #{output_width}x#{output_height}") - end - - blank_color = "000000" - blank_canvas = "#{temp_dir}/canvas.jpg" - BigBlueButton.create_blank_canvas(output_width, output_height, "##{blank_color}", blank_canvas) - - # put all events in a single list in ascendent timestamp order - all_events = [] - video_streams.each do |name, video_stream| - { "start" => video_stream[:start], "stop" => video_stream[:stop] }.each do |type, timestamp| - event = Hash.new - event[:type] = type - event[:timestamp] = timestamp - event[:stream] = name - all_events << event - end - end - all_events.sort!{|a,b| a[:timestamp] <=> b[:timestamp]} - - BigBlueButton.logger.debug("All events:") - BigBlueButton.logger.debug(hash_to_str(all_events)) - - # create a single timeline of events, keeping for each interval which video streams are enabled - timeline = [ { :timestamp => 0, :streams => [] } ] - all_events.each do |event| - new_event = { :timestamp => event[:timestamp], :streams => timeline.last[:streams].clone } - if event[:type] == "start" - new_event[:streams] << event[:stream] - elsif event[:type] == "stop" - new_event[:streams].delete(event[:stream]) - end - timeline << new_event - end - timeline << { :timestamp => last_timestamp - first_timestamp, :streams => [] } - - BigBlueButton.logger.debug("Current timeline:") - BigBlueButton.logger.debug(hash_to_str(timeline)) - - # isolate the desktop sharing stream so while the presenter is sharing his screen, it will be the only video in the playback - timeline.each do |evt| - deskshare_streams = evt[:streams].select{ |stream_name| video_streams[stream_name][:stream_type] == "deskshare" } - if not deskshare_streams.empty? - evt[:streams] = deskshare_streams - end - end - - BigBlueButton.logger.debug("Timeline keeping desktop sharing isolated:") - BigBlueButton.logger.debug(hash_to_str(timeline)) - - # remove the consecutive events with the same streams list - timeline_no_duplicates = [] - timeline.each do |evt| - if timeline_no_duplicates.empty? or timeline_no_duplicates.last()[:streams].uniq.sort != evt[:streams].uniq.sort - timeline_no_duplicates << evt - end - end - timeline = timeline_no_duplicates - - BigBlueButton.logger.debug("Timeline with no duplicates:") - BigBlueButton.logger.debug(hash_to_str(timeline)) - - for i in 1..(timeline.length-1) - current_event = timeline[i-1] - next_event = timeline[i] - - current_event[:duration] = next_event[:timestamp] - current_event[:timestamp] - current_event[:streams_detailed] = Hash.new - current_event[:streams].each do |stream| - current_event[:streams_detailed][stream] = { - :begin => current_event[:timestamp] - video_streams[stream][:start], - :end => current_event[:timestamp] - video_streams[stream][:start] + current_event[:duration] - } - end - current_event[:grid] = calculate_videos_grid(current_event[:streams], video_streams, { :width => output_width, :height => output_height }) - current_event[:transformation] = calculate_video_position_and_size(current_event[:streams], video_streams, { :width => output_width, :height => output_height }, current_event[:grid]) - end - # last_timestamp - first_timestamp is the actual duration of the entire meeting - timeline.last()[:duration] = (last_timestamp - first_timestamp) - timeline.last()[:timestamp] - timeline.pop() if timeline.last()[:duration] == 0 - - BigBlueButton.logger.debug("Current timeline with details on streams:") - BigBlueButton.logger.debug(hash_to_str(timeline)) - - blank_duration_error = 0 - concat = [] - - timeline.each do |event| - blank_video = "#{temp_dir}/#{meeting_id}/blank_video_#{event[:timestamp]}.flv" - - # Here I evaluated the MSE between the blank video requested duration and the retrieved duration ranging the FPS and found 15 as the best value - # Mean Squared Error over blank videos duration using k = 10: 32.62620320850205 - # Mean Squared Error over blank videos duration using k = 15: 4.742765771202899 <-- best value - # Mean Squared Error over blank videos duration using k = 25: 7.874007874011811 - # Mean Squared Error over blank videos duration using k = 30: 7.874007874011811 - # Mean Squared Error over blank videos duration using k = 45: 16.33824567096903 - # Mean Squared Error over blank videos duration using k = 60: 16.33824567096903 - # Mean Squared Error over blank videos duration using k = 100: 29.06399919802359 - # Mean Squared Error over blank videos duration using k = 1000: 29.06399919802359 - BigBlueButton.create_blank_video_ms(event[:duration], 15, blank_canvas, blank_video) - blank_duration_error = event[:duration] - (BigBlueButton.get_video_duration(blank_video) * 1000).to_i - BigBlueButton.logger.info("Blank video duration needed: #{event[:duration]}; duration retrieved: #{(BigBlueButton.get_video_duration(blank_video) * 1000).to_i}; error: #{blank_duration_error}") - - filter = "" - next_main = "0:0" - - event[:streams].each_with_index do |stream_name, index| - trimmed_video = "#{temp_dir}/#{meeting_id}/#{stream_name}_#{event[:timestamp]}.flv" - BigBlueButton.trim_video(event[:streams_detailed][stream_name][:begin], event[:duration], video_streams[stream_name][:file], trimmed_video) - trimmed_duration_error = event[:duration] - (BigBlueButton.get_video_duration(trimmed_video) * 1000).to_i - BigBlueButton.logger.info("Trimmed video duration needed: #{event[:duration]}; duration retrieved: #{(BigBlueButton.get_video_duration(trimmed_video) * 1000).to_i}; error: #{trimmed_duration_error}") - - current_main = next_main - next_main = "out-#{Time.now.to_i}" - - filter += "; " if not filter.empty? - filter += "movie=#{trimmed_video}, scale=#{event[:transformation][stream_name][:width]}:#{event[:transformation][stream_name][:height]}, setpts=PTS%+f/TB [#{stream_name}]; [#{current_main}][#{stream_name}] overlay=#{event[:transformation][stream_name][:x]}:#{event[:transformation][stream_name][:y]}" % (trimmed_duration_error/1000.to_f) - filter += " [#{next_main}]" unless index == event[:streams].length - 1 - end - filter = "-filter_complex \"#{filter}\"" unless filter.empty? - - patch_video = "#{temp_dir}/#{meeting_id}/patch_video_#{event[:timestamp]}.flv" - command = "ffmpeg -y -i #{blank_video} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 #{filter} -sameq #{patch_video}" - BigBlueButton.execute(command) - - concat << patch_video - end - - output = "#{temp_dir}/#{meeting_id}/output.flv" -=begin - # This is probably the best solution to concatenate the videos, but the filter "concat" isn't available on this version of FFmpeg. Anyway, it could be used in the future. - # There's also a concat demuxex on FFmpeg 1.1 and above http://ffmpeg.org/trac/ffmpeg/wiki/How%20to%20concatenate%20(join%2C%20merge)%20media%20files -# filter_input = [] - input = [] - concat.each_with_index do |file,index| - input << "-i #{file}" -# filter_input << "[#{index}:0]" - end -# command = "ffmpeg #{input.join(' ')} -vcodec copy -sameq -filter_complex \"#{filter_input.join()} concat=n=#{input.length}:v=#{input.length}:a=0\" #{output}" - command = "ffmpeg -y -f concat #{input.join(' ')} -vcodec copy -sameq #{output}" - BigBlueButton.execute(command) -=end - BigBlueButton.concatenate_videos(concat, output) - - # create webm video and mux audio - command = "ffmpeg -itsoffset #{BigBlueButton.ms_to_strtime(audio_offset)} -i #{target_dir}/audio.ogg -i #{output} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -vcodec libvpx -b 1000k -threads 0 -map 1:0 -map 0:0 -ar 22050 #{target_dir}/webcams.webm" - BigBlueButton.execute(command) - - recording_duration = last_timestamp - first_timestamp - audio_duration = (BigBlueButton.get_video_duration("#{target_dir}/audio.ogg") * 1000).to_i - video_duration = (BigBlueButton.get_video_duration(output) * 1000).to_i - output_duration = (BigBlueButton.get_video_duration("#{target_dir}/webcams.webm") * 1000).to_i - BigBlueButton.logger.debug("Recording duration: #{recording_duration}") - BigBlueButton.logger.debug("Audio file duration: #{audio_duration}") - BigBlueButton.logger.debug("Video file duration: #{video_duration}") - BigBlueButton.logger.debug("Output file duration: #{output_duration}") end @@ -823,7 +529,7 @@ module BigBlueButton # deskshare_file : Video of shared desktop of the recording def self.mux_audio_deskshare(target_dir, audio_file, deskshare_file) - command = "ffmpeg -y -i #{audio_file} -i #{deskshare_file} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -vcodec flv -b 1000k -threads 0 -map 1:0 -map 0:0 -ar 22050 #{target_dir}/muxed_audio_deskshare.flv" + command = "#{FFMPEG_CMD_BASE} -i #{audio_file} -i #{deskshare_file} -vcodec flv -b 1000k -threads 0 -map 1:0 -map 0:0 -ar 22050 #{target_dir}/muxed_audio_deskshare.flv" BigBlueButton.execute(command) FileUtils.mv("#{target_dir}/muxed_audio_deskshare.flv","#{target_dir}/deskshare.flv") end diff --git a/record-and-playback/presentation/playback/presentation/playback.js b/record-and-playback/presentation/playback/presentation/playback.js index 1c4e1ea07d..3d9f29224f 100755 --- a/record-and-playback/presentation/playback/presentation/playback.js +++ b/record-and-playback/presentation/playback/presentation/playback.js @@ -309,10 +309,14 @@ load_video = function(){ console.log("Loading video") //document.getElementById("video").style.visibility = "hidden" var video = document.createElement("video") - video.setAttribute('src', RECORDINGS + '/video/webcams.webm'); - video.setAttribute('type','video/webm'); - video.setAttribute('class','webcam'); video.setAttribute('id','video'); + video.setAttribute('class','webcam'); + + var webmsource = document.createElement("source"); + webmsource.setAttribute('src', RECORDINGS + '/video/webcams.webm'); + webmsource.setAttribute('type','video/webm; codecs="vp8.0, vorbis"'); + video.appendChild(webmsource); + /*var time_manager = Popcorn("#video"); var pc_webcam = Popcorn("#webcam"); time_manager.on( "timeupdate", function() { @@ -330,14 +334,21 @@ load_video = function(){ load_audio = function() { console.log("Loading audio") var audio = document.createElement("audio") ; - if (navigator.appName === "Microsoft Internet Explorer"){ - audio.setAttribute('src', RECORDINGS + '/audio/audio.webm'); //hack for IE - audio.setAttribute('type','audio/ogg'); - }else{ - audio.setAttribute('src', RECORDINGS + '/audio/audio.ogg'); - audio.setAttribute('type','audio/ogg'); - } audio.setAttribute('id', 'video'); + + // The webm file will work in IE with WebM components installed, + // and should load faster in Chrome too + var webmsource = document.createElement("source"); + webmsource.setAttribute('src', RECORDINGS + '/audio/audio.webm'); + webmsource.setAttribute('type', 'audio/webm; codecs="vorbis"'); + audio.appendChild(webmsource); + + // Need to keep the ogg source around for compat with old recordings + var oggsource = document.createElement("source"); + oggsource.setAttribute('src', RECORDINGS + '/audio/audio.ogg'); + oggsource.setAttribute('type', 'audio/ogg; codecs="vorbis"'); + audio.appendChild(oggsource); + audio.setAttribute('data-timeline-sources', SLIDES_XML); //audio.setAttribute('controls',''); //leave auto play turned off for accessiblity support diff --git a/record-and-playback/presentation/scripts/process/presentation.rb b/record-and-playback/presentation/scripts/process/presentation.rb index ef77ebf3bf..2392e30b3b 100755 --- a/record-and-playback/presentation/scripts/process/presentation.rb +++ b/record-and-playback/presentation/scripts/process/presentation.rb @@ -1,3 +1,5 @@ +#!/usr/bin/ruby1.9.1 + # Set encoding to utf-8 # encoding: UTF-8 @@ -19,7 +21,7 @@ # with BigBlueButton; if not, see . # -require '../../core/lib/recordandplayback' +require File.expand_path('../../../lib/recordandplayback', __FILE__) require 'rubygems' require 'trollop' require 'yaml' @@ -56,8 +58,7 @@ if not FileTest.directory?(target_dir) FileUtils.mkdir_p temp_dir FileUtils.cp_r(raw_archive_dir, temp_dir) - BigBlueButton::AudioProcessor.process("#{temp_dir}/#{meeting_id}", "#{target_dir}/audio.ogg") - + BigBlueButton::AudioProcessor.process("#{temp_dir}/#{meeting_id}", "#{target_dir}/audio") events_xml = "#{temp_dir}/#{meeting_id}/events.xml" FileUtils.cp(events_xml, target_dir) @@ -106,10 +107,6 @@ if not FileTest.directory?(target_dir) if !Dir["#{raw_archive_dir}/video/*"].empty? or (presentation_props['include_deskshare'] and !Dir["#{raw_archive_dir}/deskshare/*"].empty?) BigBlueButton.process_multiple_videos(target_dir, temp_dir, meeting_id, presentation_props['video_output_width'], presentation_props['video_output_height'], presentation_props['audio_offset'], presentation_props['include_deskshare']) - else - #Convert the audio file to webm to play it in IE - command = "ffmpeg -i #{target_dir}/audio.ogg #{target_dir}/audio.webm" - BigBlueButton.execute(command) end process_done = File.new("#{recording_dir}/status/processed/#{meeting_id}-presentation.done", "w") @@ -119,6 +116,10 @@ if not FileTest.directory?(target_dir) # BigBlueButton.logger.debug("Skipping #{meeting_id} as it has already been processed.") rescue Exception => e BigBlueButton.logger.error(e.message) + e.backtrace.each do |traceline| + BigBlueButton.logger.error(traceline) + end + exit 1 end end diff --git a/record-and-playback/presentation/scripts/publish/presentation.rb b/record-and-playback/presentation/scripts/publish/presentation.rb index a0cb92506e..e0b4206e1d 100644 --- a/record-and-playback/presentation/scripts/publish/presentation.rb +++ b/record-and-playback/presentation/scripts/publish/presentation.rb @@ -1,3 +1,5 @@ +#!/usr/bin/ruby1.9.1 + # Set encoding to utf-8 # encoding: UTF-8 @@ -47,27 +49,29 @@ def processPanAndZooms y_prev = nil timestamp_orig_prev = nil timestamp_prev = nil - last_time = nil - if $panzoom_events.empty? - if !$slides_events.empty? - BigBlueButton.logger.info("Slides found, but no panzoom events; synthesizing one") - timestamp_orig = $slides_events.first[:timestamp].to_f + 1000 - timestamp = ((timestamp_orig - $join_time) / 1000).round(1) - $xml.event(:timestamp => timestamp, :orig => timestamp_orig) do - $xml.viewBox "0 0 #{$vbox_width} #{$vbox_height}" - end - timestamp_orig_prev = timestamp_orig - timestamp_prev = timestamp - h_ratio_prev = 100 - w_ratio_prev = 100 - x_prev = 0 - y_prev = 0 - else - BigBlueButton.logger.info("Couldn't find any slides! panzooms will be empty.") - end - else - last_time = $panzoom_events.last[:timestamp].to_f - end + if $panzoom_events.empty? + BigBlueButton.logger.info("No panzoom events; old recording?") + BigBlueButton.logger.info("Synthesizing a panzoom event") + if !$slides_events.empty? + timestamp_orig = $slides_events.first[:timestamp].to_f + # make sure this is scheduled *after* the slide is shown. Dunno if needed. + timestamp_orig += 1000 + timestamp = ((timestamp_orig - $join_time) / 1000).round(1) + $xml.event(:timestamp => timestamp, :orig => timestamp_orig) do + $xml.viewBox "0 0 #{$vbox_width} #{$vbox_height}" + end + timestamp_orig_prev = timestamp_orig + timestamp_prev = timestamp + h_ratio_prev = 100 + w_ratio_prev = 100 + x_prev = 0 + y_prev = 0 + else + BigBlueButton.logger.info("Couldn't find any slides! panzooms will be empty.") + end + else + last_time = $panzoom_events.last[:timestamp].to_f + end $panzoom_events.each do |panZoomEvent| # Get variables timestamp_orig = panZoomEvent[:timestamp].to_f @@ -524,7 +528,7 @@ def processShapesAndClears if(in_this_image) # Get variables - BigBlueButton.logger.info shape.to_xml(:indent => 2) + BigBlueButton.logger.info shape $shapeType = shape.xpath(".//type")[0].text() $pageNumber = shape.xpath(".//pageNumber")[0].text() $shapeDataPoints = shape.xpath(".//dataPoints")[0].text().split(",") @@ -729,24 +733,21 @@ if ($playback == "presentation") 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") + 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.ogg to -> #{audio_dir}") - FileUtils.cp("#{$process_dir}/audio.ogg", audio_dir) - BigBlueButton.logger.info("Copied .ogg file - copying: #{$process_dir}/audio.webm to -> #{audio_dir}") - FileUtils.cp("#{$process_dir}/audio.webm", audio_dir) - BigBlueButton.logger.info("Copied audio.webm file") + 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 BigBlueButton.logger.info("Copying files to package dir") FileUtils.cp_r("#{$process_dir}/presentation", package_dir) BigBlueButton.logger.info("Copied files to package dir") - - processing_time = File.read("#{$process_dir}/processing_time") - BigBlueButton.logger.info("Creating metadata.xml") # Create metadata.xml b = Builder::XmlMarkup.new(:indent => 2) @@ -761,11 +762,9 @@ if ($playback == "presentation") b.playback { b.format("presentation") b.link("http://#{playback_host}/playback/presentation/playback.html?meetingId=#{$meeting_id}") - b.processing_time("#{processing_time}") } b.meta { BigBlueButton::Events.get_meeting_metadata("#{$process_dir}/events.xml").each { |k,v| b.method_missing(k,v) } - } } metadata_xml = File.new("#{package_dir}/metadata.xml","w") @@ -832,7 +831,11 @@ if ($playback == "presentation") FileUtils.rm_r(Dir.glob("#{target_dir}/*")) rescue Exception => e BigBlueButton.logger.error(e.message) - end + e.backtrace.each do |traceline| + BigBlueButton.logger.error(traceline) + end + exit 1 + end else BigBlueButton.logger.info("#{target_dir} is already there") end From 7c7540a0de90dbb38f5e9e4b5513246189f2dc60 Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Mon, 26 Aug 2013 11:27:53 -0400 Subject: [PATCH 05/13] Add a video quality bump when deskshare videos are present. --- record-and-playback/presentation/scripts/presentation.yml | 4 ++++ .../presentation/scripts/process/presentation.rb | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/record-and-playback/presentation/scripts/presentation.yml b/record-and-playback/presentation/scripts/presentation.yml index d84000c00a..8f1bbe6931 100644 --- a/record-and-playback/presentation/scripts/presentation.yml +++ b/record-and-playback/presentation/scripts/presentation.yml @@ -2,6 +2,10 @@ publish_dir: /var/bigbluebutton/published/presentation video_output_width: 640 video_output_height: 480 +# Alternate output size to use when deskshare videos are present +# Set higher so that deskshare output is higher quality, but uses more space. +deskshare_output_width: 1280 +deskshare_output_height: 720 # offset applied to audio in the output video file # audio_offset = 1200 means that the audio will be delayed by 1200ms # recommended value for Sorenson: 800 diff --git a/record-and-playback/presentation/scripts/process/presentation.rb b/record-and-playback/presentation/scripts/process/presentation.rb index 2392e30b3b..c745ad9c7f 100755 --- a/record-and-playback/presentation/scripts/process/presentation.rb +++ b/record-and-playback/presentation/scripts/process/presentation.rb @@ -106,7 +106,13 @@ if not FileTest.directory?(target_dir) end if !Dir["#{raw_archive_dir}/video/*"].empty? or (presentation_props['include_deskshare'] and !Dir["#{raw_archive_dir}/deskshare/*"].empty?) - BigBlueButton.process_multiple_videos(target_dir, temp_dir, meeting_id, presentation_props['video_output_width'], presentation_props['video_output_height'], presentation_props['audio_offset'], presentation_props['include_deskshare']) + width = presentation_props['video_output_width'] + height = presentation_props['video_output_height'] + if !Dir["#{raw_archive_dir}/deskshare/*"].empty? + width = presentation_props['deskshare_output_width'] + height = presentation_props['deskshare_output_height'] + end + BigBlueButton.process_multiple_videos(target_dir, temp_dir, meeting_id, width, height, presentation_props['audio_offset'], presentation_props['include_deskshare']) end process_done = File.new("#{recording_dir}/status/processed/#{meeting_id}-presentation.done", "w") From c2884bdb9d075725efb492d0dcdd9035ce780b77 Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Mon, 26 Aug 2013 11:44:22 -0400 Subject: [PATCH 06/13] Add missing license header to edl.rb --- .../core/lib/recordandplayback/edl.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/record-and-playback/core/lib/recordandplayback/edl.rb b/record-and-playback/core/lib/recordandplayback/edl.rb index 2802207f44..edd0054389 100644 --- a/record-and-playback/core/lib/recordandplayback/edl.rb +++ b/record-and-playback/core/lib/recordandplayback/edl.rb @@ -1,5 +1,22 @@ # encoding: UTF-8 +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +# +# Copyright (c) 2013 BigBlueButton Inc. and by respective authors. +# +# BigBlueButton 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 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 . + require File.expand_path('../edl/video', __FILE__) require File.expand_path('../edl/audio', __FILE__) From 047368213780c0240557adc3214f4893987c555a Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Mon, 26 Aug 2013 13:23:49 -0400 Subject: [PATCH 07/13] Reset the offset to 0; the new scripts seem to have better sync? --- record-and-playback/presentation/scripts/presentation.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/record-and-playback/presentation/scripts/presentation.yml b/record-and-playback/presentation/scripts/presentation.yml index 8f1bbe6931..ac3bafd5bb 100644 --- a/record-and-playback/presentation/scripts/presentation.yml +++ b/record-and-playback/presentation/scripts/presentation.yml @@ -8,7 +8,5 @@ deskshare_output_width: 1280 deskshare_output_height: 720 # offset applied to audio in the output video file # audio_offset = 1200 means that the audio will be delayed by 1200ms -# recommended value for Sorenson: 800 -# recommended value for h.264: 1200 -audio_offset: 1200 +audio_offset: 0 include_deskshare: true From ca9a4c2b928dcf53126c8b96c8fddd80f365fe28 Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Mon, 26 Aug 2013 13:33:23 -0400 Subject: [PATCH 08/13] Bump the max difference for audio stretch up to 5% --- record-and-playback/core/lib/recordandplayback/edl/audio.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/record-and-playback/core/lib/recordandplayback/edl/audio.rb b/record-and-playback/core/lib/recordandplayback/edl/audio.rb index e4a421f576..b972bbb04b 100644 --- a/record-and-playback/core/lib/recordandplayback/edl/audio.rb +++ b/record-and-playback/core/lib/recordandplayback/edl/audio.rb @@ -85,11 +85,11 @@ module BigBlueButton sections << filename if audio - # If the audio file length is within 2% of where it should be, + # If the audio file length is within 5% of where it should be, # adjust the speed to match up timing. # TODO: This should be part of the import logic somehow, since # render can be run after cutting. - if ((duration - audioinfo[audio[:filename]][:duration]).to_f / duration).abs < 0.02 + if ((duration - audioinfo[audio[:filename]][:duration]).to_f / duration).abs < 0.05 speed = audioinfo[audio[:filename]][:duration].to_f / duration BigBlueButton.logger.warn " Audio file length mismatch, adjusting speed to #{speed}" sox_cmd += ['speed', speed.to_s, 'rate', '-h', audioinfo[audio[:filename]][:sample_rate].to_s] From 5267a7d2e182701d71ed1394776dbc02c881805b Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Tue, 27 Aug 2013 11:04:02 -0400 Subject: [PATCH 09/13] Update the ffmpeg version check in bbb-conf --- bigbluebutton-config/bin/bbb-conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-config/bin/bbb-conf b/bigbluebutton-config/bin/bbb-conf index 2f060c37c1..9e3948ed16 100755 --- a/bigbluebutton-config/bin/bbb-conf +++ b/bigbluebutton-config/bin/bbb-conf @@ -969,7 +969,7 @@ check_state() { # FFMPEG_VERSION=$(ffmpeg -version 2>/dev/null | grep ffmpeg | cut -d ' ' -f3) case "$FFMPEG_VERSION" in - 0.11.*) + 2.0|2.0.*) # This is the current supported version; OK. ;; '') From 2b5aab3224c2200facf2efa140dc70620d4077e8 Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Tue, 27 Aug 2013 11:27:13 -0400 Subject: [PATCH 10/13] Change the background color of the video area to white to match The new scripts render the videos over white, so you get occasional black bits sneaking in without this. --- .../presentation/playback/presentation/css/bbb.playback.css | 1 + 1 file changed, 1 insertion(+) diff --git a/record-and-playback/presentation/playback/presentation/css/bbb.playback.css b/record-and-playback/presentation/playback/presentation/css/bbb.playback.css index 31ef542096..0ae7bce48d 100755 --- a/record-and-playback/presentation/playback/presentation/css/bbb.playback.css +++ b/record-and-playback/presentation/playback/presentation/css/bbb.playback.css @@ -93,6 +93,7 @@ br{ #video{ width: 402px; + background: white; } /* To remove the white space on top of the audio tag in Firefox From ccb29c30ebd39987c808ba1108b0e08c59604da1 Mon Sep 17 00:00:00 2001 From: Felipe Cecagno Date: Thu, 29 Aug 2013 14:51:31 -0300 Subject: [PATCH 11/13] setting up the language selector style; fixed identation --- .../branding/default/style/css/BBBDefault.css | 2 +- .../main/views/LanguageSelector.mxml | 49 ++++++++++--------- .../bigbluebutton/main/views/MainToolbar.mxml | 3 +- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/bigbluebutton-client/branding/default/style/css/BBBDefault.css b/bigbluebutton-client/branding/default/style/css/BBBDefault.css index 42c271ea28..7f83f31f55 100755 --- a/bigbluebutton-client/branding/default/style/css/BBBDefault.css +++ b/bigbluebutton-client/branding/default/style/css/BBBDefault.css @@ -47,7 +47,7 @@ Panel { color: #e1e2e5; } -Button, .logoutButtonStyle, .chatSendButtonStyle, .helpLinkButtonStyle, .cameraDisplaySettingsWindowChangeResolutionCombo { +Button, .logoutButtonStyle, .chatSendButtonStyle, .helpLinkButtonStyle, .cameraDisplaySettingsWindowChangeResolutionCombo, .languageSelectorStyle { textIndent: 0; paddingLeft: 10; paddingRight: 10; diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/LanguageSelector.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/LanguageSelector.mxml index 1f680175c3..d1d173b348 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/LanguageSelector.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/LanguageSelector.mxml @@ -20,19 +20,20 @@ with BigBlueButton; if not, see . --> - + - - - + + + . boxCont.setStyle("backgroundColor", "0xFFFFFF"); } ]]> - - - + + + - - - + + + @@ -61,9 +62,9 @@ with BigBlueButton; if not, see . import org.bigbluebutton.util.i18n.ResourceUtil; import org.bigbluebutton.common.LogUtil; - private function changeLanguage():void { - ResourceUtil.getInstance().setPreferredLocale(ResourceUtil.getInstance().getLocaleCodeForIndex(selectedIndex)); - } - ]]> - - + private function changeLanguage():void { + ResourceUtil.getInstance().setPreferredLocale(ResourceUtil.getInstance().getLocaleCodeForIndex(selectedIndex)); + } + ]]> + + diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml index a81801dbff..6dd8f8863b 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml @@ -336,7 +336,8 @@ with BigBlueButton; if not, see . + accessibilityName="{ResourceUtil.getInstance().getString('bbb.mainToolbar.langSelector')}" + styleName="languageSelectorStyle" /> From 5c8d19ca8fda44e6915a8767a0ec42fe75faee04 Mon Sep 17 00:00:00 2001 From: Felipe Cecagno Date: Thu, 29 Aug 2013 15:56:51 -0300 Subject: [PATCH 12/13] the user private chat selector was still using the hardcoded "you" instead of using the localized string of the users module, I just applied the same string here --- .../src/org/bigbluebutton/modules/chat/views/UserRenderer.mxml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/UserRenderer.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/UserRenderer.mxml index bd778e5b07..8daa026ecf 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/UserRenderer.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/UserRenderer.mxml @@ -25,7 +25,7 @@ with BigBlueButton; if not, see . import org.bigbluebutton.util.i18n.ResourceUtil; ]]> - From 25dff3021307320b52e36eb945533446ef376262 Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Fri, 30 Aug 2013 17:03:54 -0400 Subject: [PATCH 13/13] Add a big fat warning about the implications of leaving demos installed. --- bigbluebutton-config/bin/bbb-conf | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-config/bin/bbb-conf b/bigbluebutton-config/bin/bbb-conf index 9e3948ed16..27726a3455 100755 --- a/bigbluebutton-config/bin/bbb-conf +++ b/bigbluebutton-config/bin/bbb-conf @@ -1158,7 +1158,9 @@ check_state() { echo "#" echo "# http://$BBB_WEB/" echo "#" - echo "# Use the API demos test your BigBlueButton setup. To remove" + echo "# These API demos allow anyone to access your server without authentication" + echo "# to create/manage meetings and recordings. They are for testing purposes only." + echo "# If you are running a production system, remove them by running:" echo "#" echo "# sudo apt-get purge bbb-demo" echo