Merge branch 'master' of https://github.com/bigbluebutton/bigbluebutton
This commit is contained in:
commit
d1021d07b7
@ -47,7 +47,7 @@ Panel {
|
||||
color: #e1e2e5;
|
||||
}
|
||||
|
||||
Button, .logoutButtonStyle, .chatSendButtonStyle, .helpLinkButtonStyle, .cameraDisplaySettingsWindowChangeResolutionCombo {
|
||||
Button, .logoutButtonStyle, .chatSendButtonStyle, .helpLinkButtonStyle, .cameraDisplaySettingsWindowChangeResolutionCombo, .languageSelectorStyle {
|
||||
textIndent: 0;
|
||||
paddingLeft: 10;
|
||||
paddingRight: 10;
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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" />
|
||||
-->
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
84
record-and-playback/core/lib/recordandplayback/edl.rb
Normal file
84
record-and-playback/core/lib/recordandplayback/edl.rb
Normal 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
|
143
record-and-playback/core/lib/recordandplayback/edl/audio.rb
Normal file
143
record-and-playback/core/lib/recordandplayback/edl/audio.rb
Normal 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
|
394
record-and-playback/core/lib/recordandplayback/edl/video.rb
Normal file
394
record-and-playback/core/lib/recordandplayback/edl/video.rb
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 )
|
||||
|
@ -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
|
||||
|
@ -93,6 +93,7 @@ br{
|
||||
|
||||
#video{
|
||||
width: 402px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* To remove the white space on top of the audio tag in Firefox
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user