This commit is contained in:
jent46 2013-08-30 16:44:17 -07:00
commit d1021d07b7
19 changed files with 1026 additions and 467 deletions

View File

@ -47,7 +47,7 @@ Panel {
color: #e1e2e5;
}
Button, .logoutButtonStyle, .chatSendButtonStyle, .helpLinkButtonStyle, .cameraDisplaySettingsWindowChangeResolutionCombo {
Button, .logoutButtonStyle, .chatSendButtonStyle, .helpLinkButtonStyle, .cameraDisplaySettingsWindowChangeResolutionCombo, .languageSelectorStyle {
textIndent: 0;
paddingLeft: 10;
paddingRight: 10;

View File

@ -20,13 +20,48 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
-->
<mx:ComboBox xmlns:mx="http://www.adobe.com/2006/mxml" dataProvider="{ResourceUtil.getInstance().localeNames}"
<mx:ComboBox xmlns:mx="http://www.adobe.com/2006/mxml"
dataProvider="{ResourceUtil.getInstance().localeNames}"
selectedIndex="{ResourceUtil.getInstance().localeIndex}"
change="changeLanguage()" rowCount="15" width="120" height="22">
<mx:itemRenderer >
<mx:Component >
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml"
mouseOver="highlightItem()"
mouseOut="removeHighlightItem()"
enabled="true"
width="100%"
paddingLeft="0" paddingRight="0" paddingTop="0" paddingBottom="0">
<mx:Script>
<![CDATA[
import org.bigbluebutton.util.i18n.ResourceUtil;
private function highlightItem():void {
boxCont.setStyle("backgroundColor", "0xB2E1FF");
}
private function removeHighlightItem():void {
if(data == ResourceUtil.getInstance().getPreferredLocaleName())
boxCont.setStyle("backgroundColor", "0x7FCEFF");
else
boxCont.setStyle("backgroundColor", "0xFFFFFF");
}
]]>
</mx:Script>
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml"
paddingLeft="0" paddingRight="0" paddingTop="0" paddingBottom="0" id="boxCont" width="100%" backgroundColor="{data == ResourceUtil.getInstance().getPreferredLocaleName() ? 0x7FCEFF : 0xFFFFFF}">
<mx:Label paddingLeft="0" paddingRight="0" paddingTop="0" paddingBottom="0" text="{data}" width="90"/>
</mx:HBox>
</mx:HBox>
</mx:Component>
</mx:itemRenderer>
<mx:Script>
<![CDATA[
import org.bigbluebutton.util.i18n.ResourceUtil;
import org.bigbluebutton.common.LogUtil;
private function changeLanguage():void {
ResourceUtil.getInstance().setPreferredLocale(ResourceUtil.getInstance().getLocaleCodeForIndex(selectedIndex));
}

View File

@ -336,7 +336,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<views:LanguageSelector id="langSelector"
visible="false"
tabIndex="{baseIndex+numButtons+11}"
accessibilityName="{ResourceUtil.getInstance().getString('bbb.mainToolbar.langSelector')}"/>
accessibilityName="{ResourceUtil.getInstance().getString('bbb.mainToolbar.langSelector')}"
styleName="languageSelectorStyle" />
<!--
<mx:Button label="DISCONNECT!" click="BBB.initConnectionManager().forceClose()" height="22" toolTip="Click to simulate disconnection" />
-->

View File

@ -25,7 +25,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
import org.bigbluebutton.util.i18n.ResourceUtil;
]]>
</mx:Script>
<mx:Label id="nameLabel" textAlign="left" text="{data.name} {data.me ? '(you)' : ''}"
<mx:Label id="nameLabel" textAlign="left" text="{data.name} {data.me ? '(' + ResourceUtil.getInstance().getString('bbb.users.usersGrid.nameItemRenderer.youIdentifier') + ')' : ''}"
fontWeight="{data.me ? 'bold' : 'normal'}"
color="{data.me ? 0x003399 : 0x000000}"/>
</mx:HBox>

View File

@ -134,10 +134,14 @@ 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);
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);

View File

@ -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.
;;
'')
@ -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

View File

@ -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

View File

@ -0,0 +1,84 @@
# 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 <http://www.gnu.org/licenses/>.
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

View File

@ -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 <http://www.gnu.org/licenses/>.
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 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.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]
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

View File

@ -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 <http://www.gnu.org/licenses/>.
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

View File

@ -179,6 +179,56 @@ module BigBlueButton
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'
FILE = 'filename'
@ -290,29 +340,6 @@ module BigBlueButton
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)
BigBlueButton.logger.info("Task: Generating audio paddings")
@ -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

View File

@ -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
wav_file = BigBlueButton::EDL::Audio.render(audio_edl, "#{audio_dir}/recording")
audio_files << ae.file
end
ogg_format = {
:extension => 'ogg',
:parameters => [ [ '-c:a', 'libvorbis', '-b:a', '32K', '-f', 'ogg' ] ]
}
BigBlueButton::EDL.encode(wav_file, nil, ogg_format, file_basename)
wav_file = "#{audio_dir}/recording.wav"
BigBlueButton::AudioEvents.concatenate_audio_files(audio_files, wav_file)
BigBlueButton::AudioEvents.wav_to_ogg(wav_file, ogg_file)
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

View File

@ -102,6 +102,78 @@ module BigBlueButton
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)
BigBlueButton.logger.info("Task: Determining if the start and stop DESKSHARE events matched")
@ -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 )

View File

@ -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)
@ -480,341 +474,53 @@ module BigBlueButton
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
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' ]
]
}
BigBlueButton.logger.debug("Adding stop event: #{new_event}")
stop_evt << new_event
end
]
formats.each do |format|
filename = BigBlueButton::EDL::encode(
audio_file, video_file, format, "#{target_dir}/webcams", audio_offset)
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]
}
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
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

View File

@ -93,6 +93,7 @@ br{
#video{
width: 402px;
background: white;
}
/* To remove the white space on top of the audio tag in Firefox

View File

@ -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

View File

@ -2,9 +2,11 @@
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
# recommended value for h.264: 1200
audio_offset: 1200
audio_offset: 0
include_deskshare: true

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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)
@ -105,11 +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'])
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)
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")
@ -119,6 +122,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

View File

@ -1,3 +1,5 @@
#!/usr/bin/ruby1.9.1
# Set encoding to utf-8
# encoding: UTF-8
@ -47,11 +49,13 @@ def processPanAndZooms
y_prev = nil
timestamp_orig_prev = nil
timestamp_prev = nil
last_time = nil
if $panzoom_events.empty?
BigBlueButton.logger.info("No panzoom events; old recording?")
BigBlueButton.logger.info("Synthesizing a panzoom event")
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_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}"
@ -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(",")
@ -734,19 +738,16 @@ if ($playback == "presentation")
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}")
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")
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,6 +831,10 @@ if ($playback == "presentation")
FileUtils.rm_r(Dir.glob("#{target_dir}/*"))
rescue Exception => e
BigBlueButton.logger.error(e.message)
e.backtrace.each do |traceline|
BigBlueButton.logger.error(traceline)
end
exit 1
end
else
BigBlueButton.logger.info("#{target_dir} is already there")