diff --git a/bigbluebutton-config/bin/bbb-conf b/bigbluebutton-config/bin/bbb-conf
index 2f060c37c17f1f4147bcb8febedaa6161ecbf5d7..9e3948ed16b950ff9d5bd0fda58ea71ff3fc864b 100755
--- a/bigbluebutton-config/bin/bbb-conf
+++ b/bigbluebutton-config/bin/bbb-conf
@@ -969,7 +969,7 @@ check_state() {
 	#
 	FFMPEG_VERSION=$(ffmpeg -version 2>/dev/null | grep ffmpeg | cut -d ' ' -f3)
 	case "$FFMPEG_VERSION" in
-	0.11.*)
+	2.0|2.0.*)
 		# This is the current supported version; OK.
 		;;
 	'')
diff --git a/record-and-playback/core/lib/recordandplayback.rb b/record-and-playback/core/lib/recordandplayback.rb
index 0ab40a1999f6797e81d8cc3b9102fc281edd176c..a1289cfc195b6440ef1ba911ab4ff38122995698 100755
--- a/record-and-playback/core/lib/recordandplayback.rb
+++ b/record-and-playback/core/lib/recordandplayback.rb
@@ -111,4 +111,30 @@ module BigBlueButton
     end
     status
   end
+
+  def self.exec_ret(*command)
+    BigBlueButton.logger.info "Executing: #{command.join(' ')}"
+    IO.popen([*command, :err => [:child, :out]]) do |io|
+      io.lines.each do |line|
+        BigBlueButton.logger.info line.chomp
+      end
+    end
+    BigBlueButton.logger.info "Exit status: #{$?.exitstatus}"
+    return $?.exitstatus
+  end
+
+  def self.exec_redirect_ret(outio, *command)
+    BigBlueButton.logger.info "Executing: #{command.join(' ')}"
+    BigBlueButton.logger.info "Sending output to #{outio}"
+    IO.pipe do |r, w|
+      pid = spawn(*command, :out => outio, :err => w)
+      w.close
+      r.lines.each do |line|
+        BigBlueButton.logger.info line.chomp
+      end
+      Process.waitpid(pid)
+      BigBlueButton.logger.info "Exit status: #{$?.exitstatus}"
+      return $?.exitstatus
+    end
+  end
 end
diff --git a/record-and-playback/core/lib/recordandplayback/edl.rb b/record-and-playback/core/lib/recordandplayback/edl.rb
new file mode 100644
index 0000000000000000000000000000000000000000..edd0054389f94b65f543deee01b4de42c10a83eb
--- /dev/null
+++ b/record-and-playback/core/lib/recordandplayback/edl.rb
@@ -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
diff --git a/record-and-playback/core/lib/recordandplayback/edl/audio.rb b/record-and-playback/core/lib/recordandplayback/edl/audio.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b972bbb04b48c71a0dfaac58105b2b761f35ebf1
--- /dev/null
+++ b/record-and-playback/core/lib/recordandplayback/edl/audio.rb
@@ -0,0 +1,143 @@
+# encoding: UTF-8
+
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
+#
+# Copyright (c) 2013 BigBlueButton Inc. and by respective authors.
+#
+# BigBlueButton is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with BigBlueButton.  If not, see <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
diff --git a/record-and-playback/core/lib/recordandplayback/edl/video.rb b/record-and-playback/core/lib/recordandplayback/edl/video.rb
new file mode 100644
index 0000000000000000000000000000000000000000..be1ef98f32f843a5da3f68a35f3eb5b54d38e021
--- /dev/null
+++ b/record-and-playback/core/lib/recordandplayback/edl/video.rb
@@ -0,0 +1,394 @@
+# encoding: UTF-8
+
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
+#
+# Copyright (c) 2013 BigBlueButton Inc. and by respective authors.
+#
+# BigBlueButton is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with BigBlueButton.  If not, see <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
diff --git a/record-and-playback/core/lib/recordandplayback/generators/audio.rb b/record-and-playback/core/lib/recordandplayback/generators/audio.rb
index adc750d966edc0e734713f84fc2292da1bd1aefb..f11385e0dc216e56a914f21495e957eabd2b6023 100755
--- a/record-and-playback/core/lib/recordandplayback/generators/audio.rb
+++ b/record-and-playback/core/lib/recordandplayback/generators/audio.rb
@@ -178,6 +178,56 @@ module BigBlueButton
         return [create_gap_audio_event(last_event - first_event + 1, last_event, first_event)]
       end
     end
+
+    def self.create_audio_edl(archive_dir)
+      audio_edl = []
+      audio_dir = "#{archive_dir}/audio"
+      events = Nokogiri::XML(File.open("#{archive_dir}/events.xml"))
+
+      event = events.at_xpath('/recording/event[position()=1]')
+      initial_timestamp = event['timestamp'].to_i
+      event = events.at_xpath('/recording/event[position()=last()]')
+      final_timestamp = event['timestamp'].to_i
+
+      # Initially start with silence
+      audio_edl << {
+        :timestamp => 0,
+        :audio => nil
+      }
+
+      # Add events for recording start/stop
+      events.xpath('/recording/event[@module="VOICE"]').each do |event|
+        timestamp = event['timestamp'].to_i - initial_timestamp
+        case event['eventname']
+        when 'StartRecordingEvent'
+          filename = event.at_xpath('filename').text
+          filename = "#{audio_dir}/#{File.basename(filename)}"
+          audio_edl << {
+            :timestamp => timestamp,
+            :audio => { :filename => filename, :timestamp => 0 }
+          }
+        when 'StopRecordingEvent'
+          filename = event.at_xpath('filename').text
+          filename = "#{audio_dir}/#{File.basename(filename)}"
+          if audio_edl.last[:audio] && audio_edl.last[:audio][:filename] == filename
+            audio_edl << {
+              :timestamp => timestamp,
+              :audio => nil
+            }
+          end
+        end
+      end
+
+      audio_edl << {
+        :timestamp => final_timestamp - initial_timestamp,
+        :audio => nil
+      }
+
+      return audio_edl
+    end
+
+
+
         
     TIMESTAMP = 'timestamp'
     BRIDGE = 'bridge'
@@ -289,29 +339,6 @@ module BigBlueButton
         File.rename(temp_wav_file, file)
       end
     end
-
-    # Stretch/squish the length of the audio file to match the requested length
-    # The length parameter should be in milliseconds.
-    # Returns the filename of the new file (in the same directory as the original)
-    def self.stretch_audio_file(file, length, sample_rate)
-      BigBlueButton.logger.info("Task: Stretching/Squishing Audio")
-      orig_length = determine_length_of_audio_from_file(file)
-      new_file = "#{file}.stretch.wav"
-
-      if (orig_length == 0)
-        BigBlueButton.logger.error("Stretch received 0-length file as input!")
-        # Generate silence to fill the length
-        generate_silence(length, new_file, sample_rate)
-        return new_file
-      end
-
-      speed = orig_length.to_f / length.to_f
-      BigBlueButton.logger.info("Adjusting #{file} speed to #{speed}")
-      sox_cmd = "sox #{file} #{new_file} speed #{speed} rate -h #{sample_rate} trim 0 #{length.to_f/1000}"
-
-      BigBlueButton.execute(sox_cmd)
-      return new_file
-    end
     
     # Determine the audio padding we need to generate.
     def self.generate_audio_paddings(events, events_xml)
@@ -341,7 +368,7 @@ module BigBlueButton
         if (not ar_prev.eql?(ar_next))
           length_of_gap = ar_next.start_event_timestamp.to_i - ar_prev.stop_event_timestamp.to_i
 
-          # Check if the silence is greater that 10 minutes long. If it is, assume something went wrong with the
+          # Check if the silence is greater than 1 hour long. If it is, assume something went wrong with the
           # recording. This prevents us from generating a veeeerrryyy looonnngggg silence maxing disk space.
           if (length_of_gap < 3600000)
             if (length_of_gap < 0)
@@ -358,9 +385,9 @@ module BigBlueButton
       end
 
       length_of_gap =  BigBlueButton::Events.last_event_timestamp(events_xml).to_i - events[-1].stop_event_timestamp.to_i
-      # Check if the silence is greater that 10 minutes long. If it is, assume something went wrong with the
+      # Check if the silence is greater than 2 hours long. If it is, assume something went wrong with the
       # recording. This prevents us from generating a veeeerrryyy looonnngggg silence maxing disk space.
-      if (length_of_gap < 3600000)
+      if (length_of_gap < 7200000)
         if (length_of_gap < 0)
             trim_audio_file(events[-1].file, length_of_gap.abs)
         else
diff --git a/record-and-playback/core/lib/recordandplayback/generators/audio_processor.rb b/record-and-playback/core/lib/recordandplayback/generators/audio_processor.rb
index 9c1ba04852d6c4f6b0572a5a5b44331d05545051..21c1eca3d8be432843b7f003b9cd5bd53e68e5fc 100755
--- a/record-and-playback/core/lib/recordandplayback/generators/audio_processor.rb
+++ b/record-and-playback/core/lib/recordandplayback/generators/audio_processor.rb
@@ -22,37 +22,33 @@
 
 require 'fileutils'
 
+require File.expand_path('../../edl', __FILE__)
+
 module BigBlueButton
   class AudioProcessor
     # Process the raw recorded audio to ogg file.
     #   archive_dir - directory location of the raw archives. Assumes there is audio file and events.xml present.
-    #   ogg_file - the file name of the ogg audio output
+    #   file_basename - the file name of the audio output. '.webm' will be added
     #
-    def self.process(archive_dir, ogg_file)
+    def self.process(archive_dir, file_basename)
+      audio_edl = BigBlueButton::AudioEvents.create_audio_edl(archive_dir)
+
       audio_dir = "#{archive_dir}/audio"
       events_xml = "#{archive_dir}/events.xml"
-      audio_events = BigBlueButton::AudioEvents.process_events(audio_dir, events_xml)
-      audio_files = []
-      first_no_silence = audio_events.select { |e| !e.padding }.first
-      sampling_rate = first_no_silence.nil? ? 16000 :  FFMPEG::Movie.new(first_no_silence.file).audio_sample_rate
-      audio_events.each do |ae|
-        if ae.padding 
-          ae.file = "#{audio_dir}/#{ae.length_of_gap}.wav"
-          BigBlueButton::AudioEvents.generate_silence(ae.length_of_gap, ae.file, sampling_rate)
-        else
-          # Substitute the original file location with the archive location
-          orig_file = ae.file.sub(/.+\//, "#{audio_dir}/")
-          length = ae.stop_event_timestamp.to_i - ae.start_event_timestamp.to_i
-
-          ae.file = BigBlueButton::AudioEvents.stretch_audio_file(orig_file, length, sampling_rate)
-        end
-        
-        audio_files << ae.file
-      end
-      
-      wav_file = "#{audio_dir}/recording.wav"
-      BigBlueButton::AudioEvents.concatenate_audio_files(audio_files, wav_file)    
-      BigBlueButton::AudioEvents.wav_to_ogg(wav_file, ogg_file)
+
+      wav_file = BigBlueButton::EDL::Audio.render(audio_edl, "#{audio_dir}/recording")
+
+      ogg_format = {
+        :extension => 'ogg',
+        :parameters => [ [ '-c:a', 'libvorbis', '-b:a', '32K', '-f', 'ogg' ] ]
+      }
+      BigBlueButton::EDL.encode(wav_file, nil, ogg_format, file_basename)
+
+      webm_format = {
+        :extension => 'webm',
+        :parameters => [ [ '-c:a', 'libvorbis', '-b:a', '32K', '-f', 'webm' ] ]
+      }
+      BigBlueButton::EDL.encode(wav_file, nil, webm_format, file_basename)
     end
   end
 end
diff --git a/record-and-playback/core/lib/recordandplayback/generators/events.rb b/record-and-playback/core/lib/recordandplayback/generators/events.rb
index 28207d1a89a3c65a1e8723e671e41a72e369222b..4d26b9ae1dc729e7a1f6e59fab08470122c880b4 100755
--- a/record-and-playback/core/lib/recordandplayback/generators/events.rb
+++ b/record-and-playback/core/lib/recordandplayback/generators/events.rb
@@ -101,6 +101,78 @@ module BigBlueButton
       end
       stop_events
     end
+
+    # Build a webcam EDL
+    def self.create_webcam_edl(archive_dir)
+      events = Nokogiri::XML(File.open("#{archive_dir}/events.xml"))
+
+      recording = events.at_xpath('/recording')
+      meeting_id = recording['meeting_id']
+      event = events.at_xpath('/recording/event[position()=1]')
+      initial_timestamp = event['timestamp'].to_i
+      event = events.at_xpath('/recording/event[position()=last()]')
+      final_timestamp = event['timestamp'].to_i
+
+      video_dir = "#{archive_dir}/video/#{meeting_id}"
+
+      videos = {}
+      active_videos = []
+      video_edl = []
+      
+      video_edl << {
+        :timestamp => 0,
+        :areas => { :webcam => [] } 
+      }
+
+      events.xpath('/recording/event[@module="WEBCAM"]').each do |event|
+        timestamp = event['timestamp'].to_i - initial_timestamp
+        case event['eventname']
+        when 'StartWebcamShareEvent'
+          stream = event.at_xpath('stream').text
+          filename = "#{video_dir}/#{stream}.flv"
+
+          videos[filename] = { :timestamp => timestamp }
+          active_videos << filename
+
+          edl_entry = {
+            :timestamp => timestamp,
+            :areas => { :webcam => [] }
+          }
+          active_videos.each do |filename|
+            edl_entry[:areas][:webcam] << {
+              :filename => filename,
+              :timestamp => timestamp - videos[filename][:timestamp]
+            }
+          end
+          video_edl << edl_entry
+        when 'StopWebcamShareEvent'
+          stream = event.at_xpath('stream').text
+          filename = "#{video_dir}/#{stream}.flv"
+
+          active_videos.delete(filename)
+
+          edl_entry = {
+            :timestamp => timestamp,
+            :areas => { :webcam => [] }
+          }
+          active_videos.each do |filename|
+            edl_entry[:areas][:webcam] << {
+              :filename => filename,
+              :timestamp => timestamp - videos[filename][:timestamp]
+            }
+          end
+          video_edl << edl_entry
+        end
+      end
+
+      video_edl << {
+        :timestamp => final_timestamp - initial_timestamp,
+        :areas => { :webcam => [] }
+      }
+
+      return video_edl
+    end
+
         
     # Determine if the start and stop event matched.
     def self.deskshare_event_matched?(stop_events, start)
@@ -152,6 +224,51 @@ module BigBlueButton
       stop_events.sort {|a, b| a[:stop_timestamp] <=> b[:stop_timestamp]}
     end
 
+    def self.create_deskshare_edl(archive_dir)
+      events = Nokogiri::XML(File.open("#{archive_dir}/events.xml"))
+
+      event = events.at_xpath('/recording/event[position()=1]')
+      initial_timestamp = event['timestamp'].to_i
+      event = events.at_xpath('/recording/event[position()=last()]')
+      final_timestamp = event['timestamp'].to_i
+
+      deskshare_edl = []
+
+      deskshare_edl << {
+        :timestamp => 0,
+        :areas => { :deskshare => [] }
+      }
+
+      events.xpath('/recording/event[@module="Deskshare"]').each do |event|
+        timestamp = event['timestamp'].to_i - initial_timestamp
+        case event['eventname']
+        when 'DeskshareStartedEvent'
+          filename = event.at_xpath('file').text
+          filename = "#{archive_dir}/deskshare/#{File.basename(filename)}"
+          deskshare_edl << {
+            :timestamp => timestamp,
+            :areas => {
+              :deskshare => [
+                { :filename => filename, :timestamp => 0 }
+              ]
+            }
+          }
+        when 'DeskshareStoppedEvent'
+          deskshare_edl << {
+            :timestamp => timestamp,
+            :areas => { :deskshare => [] }
+          }
+        end
+      end
+
+      deskshare_edl << {
+        :timestamp => final_timestamp - initial_timestamp,
+        :areas => {}
+      }
+
+      return deskshare_edl
+    end
+
 	def self.linkify( text )
 	  generic_URL_regexp = Regexp.new( '(^|[\n ])([\w]+?://[\w]+[^ \"\n\r\t<]*)', Regexp::MULTILINE | Regexp::IGNORECASE )
 	  starts_with_www_regexp = Regexp.new( '(^|[\n ])((www)\.[^ \"\t\n\r<]*)', Regexp::MULTILINE | Regexp::IGNORECASE )
diff --git a/record-and-playback/core/lib/recordandplayback/generators/video.rb b/record-and-playback/core/lib/recordandplayback/generators/video.rb
index d4ba877ba116458046e32166e40ba93008190fb9..920064130fabf755f5621c90e25a6636f12d1ce8 100755
--- a/record-and-playback/core/lib/recordandplayback/generators/video.rb
+++ b/record-and-playback/core/lib/recordandplayback/generators/video.rb
@@ -22,11 +22,12 @@
 
 require 'rubygems'
 require 'streamio-ffmpeg'
-require 'pp'
+
+require File.expand_path('../../edl', __FILE__)
 
 module BigBlueButton
-  # use "info" or "debug" for development
-  FFMPEG_LOG_LEVEL = "fatal"
+
+  FFMPEG_CMD_BASE="ffmpeg -loglevel warning -nostats"
 
   # Strips the audio stream from the video file
   #   video_in - the FLV file that needs to be stripped of audio
@@ -35,7 +36,7 @@ module BigBlueButton
   #    strip_audio_from_video(orig-video.flv, video2.flv)
   def self.strip_audio_from_video(video_in, video_out)
     BigBlueButton.logger.info("Task: Stripping audio from video")      
-    command = "ffmpeg -y -i #{video_in} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -an -vcodec copy -sameq #{video_out}"
+    command = "#{FFMPEG_CMD_BASE} -i #{video_in} -an -vcodec copy #{video_out}"
     BigBlueButton.execute(command)
     # TODO: check for result, raise an exception when there is an error
   end
@@ -47,14 +48,7 @@ module BigBlueButton
   #   video_out - the resulting new video
   def self.trim_video(start, duration, video_in, video_out)
     BigBlueButton.logger.info("Task: Trimming video")
-    command = ""
-    if duration != (BigBlueButton.get_video_duration(video_in) * 1000).to_i
-      # -ss coming before AND after the -i option improves the precision ==> http://ffmpeg.org/pipermail/ffmpeg-user/2012-August/008767.html
-      command = "ffmpeg -y -ss #{BigBlueButton.ms_to_strtime(start)} -i #{video_in} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -vcodec copy -acodec copy -ss 0 -t #{BigBlueButton.ms_to_strtime(duration)} -sameq #{video_out}"
-    else
-      BigBlueButton.logger.info("The video has exactly the same duration as requested, so it will be just copied")
-      command = "ffmpeg -y -i #{video_in} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -vcodec copy -acodec copy -sameq #{video_out}"
-    end
+    command = "#{FFMPEG_CMD_BASE} -i #{video_in} -vcodec copy -acodec copy -ss #{BigBlueButton.ms_to_strtime(start)} -t #{BigBlueButton.ms_to_strtime(duration)} #{video_out}"
     BigBlueButton.execute(command)  
     # TODO: check for result, raise an exception when there is an error
   end
@@ -68,7 +62,7 @@ module BigBlueButton
   #   create_blank_video(15, 1000, canvas.jpg, blank-video.flv)
   def self.create_blank_deskshare_video(length, rate, blank_canvas, video_out)
     BigBlueButton.logger.info("Task: Creating blank deskshare video")      
-    command = "ffmpeg -y -loop 1 -t #{length} -i #{blank_canvas} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -r #{rate} -vcodec flashsv #{video_out}"
+    command = "#{FFMPEG_CMD_BASE} -loop 1 -i #{blank_canvas} -t #{length} -r #{rate} -vcodec flashsv #{video_out}"
     BigBlueButton.execute(command)
     # TODO: check for result, raise exception when there is an error
   end
@@ -82,7 +76,7 @@ module BigBlueButton
   #   create_blank_video(15, 1000, canvas.jpg, blank-video.flv)
   def self.create_blank_video(length, rate, blank_canvas, video_out)
     BigBlueButton.logger.info("Task: Creating blank video")      
-    command = "ffmpeg -y -loop 1 -t #{length} -i #{blank_canvas} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -r #{rate} #{video_out}"
+    command = "#{FFMPEG_CMD_BASE} -y -loop 1 -i #{blank_canvas} -t #{length} -r #{rate} #{video_out}"
     BigBlueButton.execute(command)
     # TODO: check for result, raise exception when there is an error
   end
@@ -144,7 +138,7 @@ module BigBlueButton
 
   #Converts flv to mpg
   def self.convert_flv_to_mpg(flv_video, mpg_video_out)
-        command = "ffmpeg -y -i #{flv_video} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -sameq -f mpegts -r 29.97 #{mpg_video_out}"
+        command = "#{FFMPEG_CMD_BASE} -i #{flv_video} -same_quant -f mpegts -r 29.97 #{mpg_video_out}"
         BigBlueButton.logger.info("Task: Converting .flv to .mpg")    
         BigBlueButton.execute(command)
   end
@@ -158,7 +152,7 @@ module BigBlueButton
 
   #Converts .mpg to .flv
   def self.convert_mpg_to_flv(mpg_video,flv_video_out)
-        command = "ffmpeg -y -i  #{mpg_video} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -sameq  #{flv_video_out}"
+        command = "#{FFMPEG_CMD_BASE} -i  #{mpg_video} -same_quant  #{flv_video_out}"
         BigBlueButton.logger.info("Task: Converting .mpg to .flv")
         BigBlueButton.execute(command);
   end
@@ -169,7 +163,7 @@ module BigBlueButton
   #  video - the video file. Must not contain an audio stream. 
   def self.multiplex_audio_and_video(audio, video, video_out)
     BigBlueButton.logger.info("Task: Multiplexing audio and video")      
-    command = "ffmpeg -y -i #{audio} -i #{video} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -map 1:0 -map 0:0 -ar 22050 #{video_out}"
+    command = "#{FFMPEG_CMD_BASE} -i #{audio} -i #{video} -map 1:0 -map 0:0 #{video_out}"
     BigBlueButton.execute(command)
     # TODO: check result, raise an exception when there is an error
   end
@@ -409,7 +403,7 @@ module BigBlueButton
  
    			# Use for newer version of FFMPEG
     		padding = "-vf pad=#{MAX_VID_WIDTH}:#{MAX_VID_HEIGHT}:#{side_padding}:#{top_bottom_padding}:FFFFFF"       
-		    command = "ffmpeg -y -i #{stripped_webcam} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -aspect 4:3 -r 1000 -sameq #{frame_size} #{padding} #{scaled_flv}" 
+		    command = "#{FFMPEG_CMD_BASE} -i #{stripped_webcam} -aspect 4:3 -r 1000 -same_quant #{frame_size} #{padding} #{scaled_flv}" 
 		    #BigBlueButton.logger.info(command)
 		    #IO.popen(command)
 		    #Process.wait                
@@ -458,7 +452,7 @@ module BigBlueButton
  
    			# Use for newer version of FFMPEG
     		padding = "-vf pad=#{MAX_VID_WIDTH}:#{MAX_VID_HEIGHT}:#{side_padding}:#{top_bottom_padding}:FFFFFF"       
-		    command = "ffmpeg -y -i #{flv_in} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -aspect 4:3 -r 1000 -sameq #{frame_size} #{padding} -vcodec flashsv #{scaled_flv}" 
+		    command = "#{FFMPEG_CMD_BASE} -i #{flv_in} -aspect 4:3 -r 1000 -same_quant #{frame_size} #{padding} -vcodec flashsv #{scaled_flv}" 
 		    BigBlueButton.execute(command)
 		    #BigBlueButton.logger.info(command)
 		    #IO.popen(command)
@@ -479,342 +473,54 @@ module BigBlueButton
     t = Time.at(ms / 1000, (ms % 1000) * 1000)
     return t.getutc.strftime("%H:%M:%S.%L")
   end
-
-  def self.hash_to_str(hash)
-    return PP.pp(hash, "")
-  end
-
-  def self.calculate_videos_grid(videos_list, details, output_video)
-    # try to find the number of rows and columns to maximize the internal videos
-    each_row = 0
-    num_rows = 0
-    slot_width = 0
-    slot_height = 0
-    total_area = 0
-    num_cams = videos_list.length
-    for tmp_num_rows in 1..(num_cams)
-      tmp_each_row = (num_cams / tmp_num_rows.to_f).ceil
-      max_width = (output_video[:width] / tmp_each_row).floor
-      max_height = (output_video[:height] / tmp_num_rows).floor
-      if max_width <= 0 or max_height <= 0 then
-        next
-      end
-      
-      tmp_total_area = 0
-      videos_list.each do |stream|
-        measurements = BigBlueButton.fit_to(details[stream][:width], details[stream][:height], max_width, max_height)
-        tmp_total_area += measurements[:width] * measurements[:height]
-      end
-      
-      if tmp_total_area > total_area
-        slot_width = max_width
-        slot_height = max_height
-        num_rows = tmp_num_rows
-        each_row = tmp_each_row
-        total_area = tmp_total_area
-      end
-    end
-
-    return {
-      :rows => num_rows,
-      :columns => each_row,
-      :cell_width => slot_width,
-      :cell_height => slot_height
-    }
-  end
-
-  def self.calculate_video_position_and_size(videos_list, details, output_video, grid)
-    transformation = Hash.new
-    videos_list.each_with_index do |stream, index|
-      video_dimensions = BigBlueButton.fit_to(details[stream][:width], details[stream][:height], grid[:cell_width], grid[:cell_height])
-      slot_x = (index%grid[:columns])       * grid[:cell_width]  + (output_video[:width]  - grid[:cell_width]  * grid[:columns]) / 2
-      slot_y = (index/grid[:columns]).floor * grid[:cell_height] + (output_video[:height] - grid[:cell_height] * grid[:rows]) / 2
-      x = slot_x + (grid[:cell_width]  - video_dimensions[:width])  / 2
-      y = slot_y + (grid[:cell_height] - video_dimensions[:height]) / 2
-
-      transformation[stream] = {
-        :x => x,
-        :y => y,
-        :width => video_dimensions[:width],
-        :height => video_dimensions[:height]
-      }
-    end
-    return transformation
-  end
-
-  def self.get_video_dir(temp_dir, meeting_id, type="video")
-    dir = nil
-    if type == "video"
-      dir = "#{temp_dir}/#{meeting_id}/video/#{meeting_id}"
-    elsif type == "deskshare"
-      dir = "#{temp_dir}/#{meeting_id}/deskshare"
-    end
-    return dir
-  end
-
+  
   def BigBlueButton.process_multiple_videos(target_dir, temp_dir, meeting_id, output_width, output_height, audio_offset, include_deskshare=false)
     BigBlueButton.logger.info("Processing webcam videos")
 
     # Process audio
-    BigBlueButton::AudioProcessor.process("#{temp_dir}/#{meeting_id}", "#{target_dir}/audio.ogg")
+    audio_edl = BigBlueButton::AudioEvents.create_audio_edl(
+      "#{temp_dir}/#{meeting_id}")
+    BigBlueButton::EDL::Audio.dump(audio_edl)
 
-    # 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)
+    audio_file = BigBlueButton::EDL::Audio.render(
+      audio_edl, "#{target_dir}/webcams")
 
-    start_evt.each {|evt| evt[:stream_type] = "video"}
-    stop_evt.each {|evt| evt[:stream_type] = "video"}
+    # Process video
+    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)
+
+    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
-    end
-
-    (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]
+      layout[:areas] += [ { :name => :deskshare, :x => 0, :y => 0,
+        :width => output_width, :height => output_height, :pad => true } ]
     end
-
-    # fix the stop events list so the matched events will be consistent
-    start_evt.each do |evt|
-      video_dir = get_video_dir(temp_dir, meeting_id, evt[:stream_type])
-      if stop_evt.select{ |s| s[:stream] == evt[:stream] }.empty?
-        new_event = { 
-          :stream => evt[:stream],
-          :stop_timestamp => evt[:start_timestamp] + (BigBlueButton.get_video_duration("#{video_dir}/#{evt[:stream_file_name]}") * 1000).to_i
-        }
-        BigBlueButton.logger.debug("Adding stop event: #{new_event}")
-        stop_evt << new_event
-      end
-    end
-    
-    # fix the start events list so the matched events will be consistent
-    stop_evt.each do |evt|
-      if start_evt.select{ |s| s[:stream] == evt[:stream] }.empty?
-        new_event = {
-          :stream => evt[:stream],
-          :start_timestamp => evt[:stop_timestamp] - (BigBlueButton.get_video_duration("#{video_dir}/#{evt[:stream]}.flv") * 1000).to_i
-        }
-        BigBlueButton.logger.debug("Adding stop event: #{new_event}")
-        start_evt << new_event
-      end
-    end
-
-    matched_evts = BigBlueButton::Events.match_start_and_stop_video_events(start_evt, stop_evt)
-
-    BigBlueButton.logger.debug("First timestamp: #{first_timestamp}")
-    BigBlueButton.logger.debug("Last timestamp: #{last_timestamp}")
-    BigBlueButton.logger.debug("Matched events:")
-    BigBlueButton.logger.debug(hash_to_str(matched_evts))
-
-    video_streams = Hash.new
-
-    matched_evts.each do |evt|
-      video_dir = BigBlueButton.get_video_dir(temp_dir, meeting_id, evt[:stream_type])
-      # removes audio stream
-      stripped_webcam = "#{temp_dir}/#{meeting_id}/stripped-#{evt[:stream]}.flv"
-      BigBlueButton.strip_audio_from_video("#{video_dir}/#{evt[:stream_file_name]}", stripped_webcam)
-
-      # the encoder for Flash Screen Codec v2 doesn't work on the current version of FFmpeg, so trim doesn't work at all
-      # in this step the desktop sharing file is re-encoded with Flash Screen Codec v1 so everything works fine
-      if evt[:stream_type] == "deskshare"
-        processed_deskshare = "#{temp_dir}/#{meeting_id}/stripped-#{evt[:stream]}_processed.flv"
-        command = "ffmpeg -y -i #{stripped_webcam} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -vcodec flashsv -sameq #{processed_deskshare}"
-        BigBlueButton.execute(command)
-        stripped_webcam = processed_deskshare
-      end
-
-      video_stream = {
-        :name => evt[:stream],
-        :file => stripped_webcam,
-        :duration => (BigBlueButton.get_video_duration(stripped_webcam) * 1000).to_i,
-        :width => BigBlueButton.get_video_width(stripped_webcam),
-        :height => BigBlueButton.get_video_height(stripped_webcam),
-        :start => evt[:start_timestamp] - first_timestamp,
-        :stop => evt[:stop_timestamp] - first_timestamp,
-        :stream_type => evt[:stream_type]
+    video_file = BigBlueButton::EDL::Video.render(
+      video_edl, layout, "#{target_dir}/webcams")
+
+    formats = [
+      {
+        :extension => 'webm',
+        :parameters => [
+          [ '-c:v', 'libvpx', '-crf', '34', '-b:v', '60M',
+            '-threads', '2', '-deadline', 'good', '-cpu-used', '3',
+            '-c:a', 'libvorbis', '-b:a', '32K',
+            '-f', 'webm' ]
+        ]
       }
-      video_stream[:start_error] = video_stream[:stop] - (video_stream[:start] + video_stream[:duration])
-      # adjust the start_timestamp based on the video duration
-      video_stream[:start] = video_stream[:start] + video_stream[:start_error]
-      video_streams[video_stream[:name]] = video_stream
-    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
+    ]
+    formats.each do |format|
+      filename = BigBlueButton::EDL::encode(
+        audio_file, video_file, format, "#{target_dir}/webcams", audio_offset)
     end
-    timeline << { :timestamp => last_timestamp - first_timestamp, :streams => [] }
-
-    BigBlueButton.logger.debug("Current timeline:")
-    BigBlueButton.logger.debug(hash_to_str(timeline))
-
-    # isolate the desktop sharing stream so while the presenter is sharing his screen, it will be the only video in the playback
-    timeline.each do |evt|
-      deskshare_streams = evt[:streams].select{ |stream_name| video_streams[stream_name][:stream_type] == "deskshare" }
-      if not deskshare_streams.empty?
-        evt[:streams] = deskshare_streams
-      end
-    end
-
-    BigBlueButton.logger.debug("Timeline keeping desktop sharing isolated:")
-    BigBlueButton.logger.debug(hash_to_str(timeline))
-
-    # remove the consecutive events with the same streams list
-    timeline_no_duplicates = []
-    timeline.each do |evt|
-      if timeline_no_duplicates.empty? or timeline_no_duplicates.last()[:streams].uniq.sort != evt[:streams].uniq.sort
-        timeline_no_duplicates << evt
-      end
-    end
-    timeline = timeline_no_duplicates
-
-    BigBlueButton.logger.debug("Timeline with no duplicates:")
-    BigBlueButton.logger.debug(hash_to_str(timeline))
-
-    for i in 1..(timeline.length-1)
-      current_event = timeline[i-1]
-      next_event = timeline[i]
-
-      current_event[:duration] = next_event[:timestamp] - current_event[:timestamp]
-      current_event[:streams_detailed] = Hash.new
-      current_event[:streams].each do |stream|
-        current_event[:streams_detailed][stream] = {
-          :begin => current_event[:timestamp] - video_streams[stream][:start],
-          :end => current_event[:timestamp] - video_streams[stream][:start] + current_event[:duration]
-        }
-      end
-      current_event[:grid] = calculate_videos_grid(current_event[:streams], video_streams, { :width => output_width, :height => output_height })
-      current_event[:transformation] = calculate_video_position_and_size(current_event[:streams], video_streams, { :width => output_width, :height => output_height }, current_event[:grid])
-    end
-    # last_timestamp - first_timestamp is the actual duration of the entire meeting
-    timeline.last()[:duration] = (last_timestamp - first_timestamp) - timeline.last()[:timestamp]
-    timeline.pop() if timeline.last()[:duration] == 0
-
-    BigBlueButton.logger.debug("Current timeline with details on streams:")
-    BigBlueButton.logger.debug(hash_to_str(timeline))
-
-    blank_duration_error = 0
-    concat = []
-
-    timeline.each do |event|
-      blank_video = "#{temp_dir}/#{meeting_id}/blank_video_#{event[:timestamp]}.flv"
-
-      # Here I evaluated the MSE between the blank video requested duration and the retrieved duration ranging the FPS and found 15 as the best value
-      # Mean Squared Error over blank videos duration using k = 10: 32.62620320850205
-      # Mean Squared Error over blank videos duration using k = 15: 4.742765771202899 <-- best value
-      # Mean Squared Error over blank videos duration using k = 25: 7.874007874011811
-      # Mean Squared Error over blank videos duration using k = 30: 7.874007874011811
-      # Mean Squared Error over blank videos duration using k = 45: 16.33824567096903
-      # Mean Squared Error over blank videos duration using k = 60: 16.33824567096903
-      # Mean Squared Error over blank videos duration using k = 100: 29.06399919802359
-      # Mean Squared Error over blank videos duration using k = 1000: 29.06399919802359
-      BigBlueButton.create_blank_video_ms(event[:duration], 15, blank_canvas, blank_video)
-      blank_duration_error = event[:duration] - (BigBlueButton.get_video_duration(blank_video) * 1000).to_i
-      BigBlueButton.logger.info("Blank video duration needed: #{event[:duration]}; duration retrieved: #{(BigBlueButton.get_video_duration(blank_video) * 1000).to_i}; error: #{blank_duration_error}")
-
-      filter = ""
-      next_main = "0:0"
-
-      event[:streams].each_with_index do |stream_name, index|
-        trimmed_video = "#{temp_dir}/#{meeting_id}/#{stream_name}_#{event[:timestamp]}.flv"
-        BigBlueButton.trim_video(event[:streams_detailed][stream_name][:begin], event[:duration], video_streams[stream_name][:file], trimmed_video)
-        trimmed_duration_error = event[:duration] - (BigBlueButton.get_video_duration(trimmed_video) * 1000).to_i
-        BigBlueButton.logger.info("Trimmed video duration needed: #{event[:duration]}; duration retrieved: #{(BigBlueButton.get_video_duration(trimmed_video) * 1000).to_i}; error: #{trimmed_duration_error}")
-
-        current_main = next_main
-        next_main = "out-#{Time.now.to_i}"
-
-        filter += "; " if not filter.empty?
-        filter += "movie=#{trimmed_video}, scale=#{event[:transformation][stream_name][:width]}:#{event[:transformation][stream_name][:height]}, setpts=PTS%+f/TB [#{stream_name}]; [#{current_main}][#{stream_name}] overlay=#{event[:transformation][stream_name][:x]}:#{event[:transformation][stream_name][:y]}" % (trimmed_duration_error/1000.to_f)
-        filter += " [#{next_main}]" unless index == event[:streams].length - 1
-      end
-      filter = "-filter_complex \"#{filter}\"" unless filter.empty?
-
-      patch_video = "#{temp_dir}/#{meeting_id}/patch_video_#{event[:timestamp]}.flv"
-      command = "ffmpeg -y -i #{blank_video} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 #{filter} -sameq #{patch_video}"
-      BigBlueButton.execute(command)
-
-      concat << patch_video
-    end
-    
-    output = "#{temp_dir}/#{meeting_id}/output.flv"
-=begin
-    # This is probably the best solution to concatenate the videos, but the filter "concat" isn't available on this version of FFmpeg. Anyway, it could be used in the future.
-    # There's also a concat demuxex on FFmpeg 1.1 and above http://ffmpeg.org/trac/ffmpeg/wiki/How%20to%20concatenate%20(join%2C%20merge)%20media%20files
-#    filter_input = []
-    input = []
-    concat.each_with_index do |file,index|
-      input << "-i #{file}"
-#      filter_input << "[#{index}:0]"
-    end
-#    command = "ffmpeg #{input.join(' ')} -vcodec copy -sameq -filter_complex \"#{filter_input.join()} concat=n=#{input.length}:v=#{input.length}:a=0\" #{output}"
-    command = "ffmpeg -y -f concat #{input.join(' ')} -vcodec copy -sameq #{output}"
-    BigBlueButton.execute(command)
-=end
-    BigBlueButton.concatenate_videos(concat, output)
-
-    # create webm video and mux audio
-    command = "ffmpeg -itsoffset #{BigBlueButton.ms_to_strtime(audio_offset)} -i #{target_dir}/audio.ogg -i #{output} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -vcodec libvpx -b 1000k -threads 0 -map 1:0 -map 0:0 -ar 22050 #{target_dir}/webcams.webm"
-    BigBlueButton.execute(command)
 
-    recording_duration = last_timestamp - first_timestamp
-    audio_duration = (BigBlueButton.get_video_duration("#{target_dir}/audio.ogg") * 1000).to_i
-    video_duration = (BigBlueButton.get_video_duration(output) * 1000).to_i
-    output_duration = (BigBlueButton.get_video_duration("#{target_dir}/webcams.webm") * 1000).to_i
-    BigBlueButton.logger.debug("Recording duration: #{recording_duration}")
-    BigBlueButton.logger.debug("Audio file duration: #{audio_duration}")
-    BigBlueButton.logger.debug("Video file duration: #{video_duration}")
-    BigBlueButton.logger.debug("Output file duration: #{output_duration}")
   end
 
 
@@ -823,7 +529,7 @@ module BigBlueButton
  # deskshare_file : Video of shared desktop of the recording
 
  def self.mux_audio_deskshare(target_dir, audio_file, deskshare_file)
-  command = "ffmpeg -y -i #{audio_file} -i #{deskshare_file} -loglevel #{BigBlueButton::FFMPEG_LOG_LEVEL} -v -10 -vcodec flv -b 1000k -threads 0  -map 1:0 -map 0:0 -ar 22050 #{target_dir}/muxed_audio_deskshare.flv"
+  command = "#{FFMPEG_CMD_BASE} -i #{audio_file} -i #{deskshare_file} -vcodec flv -b 1000k -threads 0  -map 1:0 -map 0:0 -ar 22050 #{target_dir}/muxed_audio_deskshare.flv"
   BigBlueButton.execute(command)
   FileUtils.mv("#{target_dir}/muxed_audio_deskshare.flv","#{target_dir}/deskshare.flv")
  end
diff --git a/record-and-playback/presentation/playback/presentation/css/bbb.playback.css b/record-and-playback/presentation/playback/presentation/css/bbb.playback.css
index 31ef542096881255b66d56f7688d820e752109b8..0ae7bce48d118c1577867fda421ee90d748c270a 100755
--- a/record-and-playback/presentation/playback/presentation/css/bbb.playback.css
+++ b/record-and-playback/presentation/playback/presentation/css/bbb.playback.css
@@ -93,6 +93,7 @@ br{
 
 #video{
   width: 402px;
+  background: white;
 }
 
 /* To remove the white space on top of the audio tag in Firefox
diff --git a/record-and-playback/presentation/playback/presentation/playback.js b/record-and-playback/presentation/playback/presentation/playback.js
index 1c4e1ea07d3091ebb83941acd3b9cd25018b6d68..3d9f29224f488a4ba2668ae7ab01a8120d7d8207 100755
--- a/record-and-playback/presentation/playback/presentation/playback.js
+++ b/record-and-playback/presentation/playback/presentation/playback.js
@@ -309,10 +309,14 @@ load_video = function(){
    console.log("Loading video")
    //document.getElementById("video").style.visibility = "hidden"  
    var video = document.createElement("video")   
-   video.setAttribute('src', RECORDINGS + '/video/webcams.webm');
-   video.setAttribute('type','video/webm');
-   video.setAttribute('class','webcam');  
    video.setAttribute('id','video');  
+   video.setAttribute('class','webcam');  
+
+   var webmsource = document.createElement("source");
+   webmsource.setAttribute('src', RECORDINGS + '/video/webcams.webm');
+   webmsource.setAttribute('type','video/webm; codecs="vp8.0, vorbis"');
+   video.appendChild(webmsource);
+
    /*var time_manager = Popcorn("#video");
    var pc_webcam = Popcorn("#webcam");
    time_manager.on( "timeupdate", function() {
@@ -330,14 +334,21 @@ load_video = function(){
 load_audio = function() {
    console.log("Loading audio")
    var audio = document.createElement("audio") ;
-   if (navigator.appName === "Microsoft Internet Explorer"){
-     audio.setAttribute('src', RECORDINGS + '/audio/audio.webm'); //hack for IE
-     audio.setAttribute('type','audio/ogg');
-   }else{
-     audio.setAttribute('src', RECORDINGS + '/audio/audio.ogg');
-     audio.setAttribute('type','audio/ogg');
-   }
    audio.setAttribute('id', 'video');
+
+   // The webm file will work in IE with WebM components installed,
+   // and should load faster in Chrome too
+   var webmsource = document.createElement("source");
+   webmsource.setAttribute('src', RECORDINGS + '/audio/audio.webm');
+   webmsource.setAttribute('type', 'audio/webm; codecs="vorbis"');
+   audio.appendChild(webmsource);
+
+   // Need to keep the ogg source around for compat with old recordings
+   var oggsource = document.createElement("source");
+   oggsource.setAttribute('src', RECORDINGS + '/audio/audio.ogg');
+   oggsource.setAttribute('type', 'audio/ogg; codecs="vorbis"');
+   audio.appendChild(oggsource);
+
    audio.setAttribute('data-timeline-sources', SLIDES_XML);
    //audio.setAttribute('controls','');
    //leave auto play turned off for accessiblity support
diff --git a/record-and-playback/presentation/scripts/presentation.yml b/record-and-playback/presentation/scripts/presentation.yml
index d84000c00a9a67321c726d85bcf4ddcc1fe35585..ac3bafd5bb70ffeb72b6c57de202a4975f6c11aa 100644
--- a/record-and-playback/presentation/scripts/presentation.yml
+++ b/record-and-playback/presentation/scripts/presentation.yml
@@ -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
diff --git a/record-and-playback/presentation/scripts/process/presentation.rb b/record-and-playback/presentation/scripts/process/presentation.rb
index ef77ebf3bf66c9cb54ff11cce73f93fb2283a6ff..c745ad9c7f184a72d347bc3857562e1e2c503c31 100755
--- a/record-and-playback/presentation/scripts/process/presentation.rb
+++ b/record-and-playback/presentation/scripts/process/presentation.rb
@@ -1,3 +1,5 @@
+#!/usr/bin/ruby1.9.1
+
 # Set encoding to utf-8
 # encoding: UTF-8
 
@@ -19,7 +21,7 @@
 # with BigBlueButton; if not, see <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
     
diff --git a/record-and-playback/presentation/scripts/publish/presentation.rb b/record-and-playback/presentation/scripts/publish/presentation.rb
index a0cb92506e43c7fb6ec7dfec49bf479b7159e275..e0b4206e1dce87d8bdcf3eb0bf6d472ffe374926 100644
--- a/record-and-playback/presentation/scripts/publish/presentation.rb
+++ b/record-and-playback/presentation/scripts/publish/presentation.rb
@@ -1,3 +1,5 @@
+#!/usr/bin/ruby1.9.1
+
 # Set encoding to utf-8
 # encoding: UTF-8
 
@@ -47,27 +49,29 @@ def processPanAndZooms
 			y_prev = nil
 			timestamp_orig_prev = nil
 			timestamp_prev = nil
-			last_time = nil
-			if $panzoom_events.empty?
-				if !$slides_events.empty?
-					BigBlueButton.logger.info("Slides found, but no panzoom events; synthesizing one")
-					timestamp_orig = $slides_events.first[:timestamp].to_f + 1000
-					timestamp = ((timestamp_orig - $join_time) / 1000).round(1)
-					$xml.event(:timestamp => timestamp, :orig => timestamp_orig) do
-						$xml.viewBox "0 0 #{$vbox_width} #{$vbox_height}"
-					end
-					timestamp_orig_prev = timestamp_orig
-					timestamp_prev = timestamp
-					h_ratio_prev = 100
-					w_ratio_prev = 100
-					x_prev = 0
-					y_prev = 0
-				else
-					BigBlueButton.logger.info("Couldn't find any slides! panzooms will be empty.")
-				end
-			else
-				last_time = $panzoom_events.last[:timestamp].to_f
-			end
+                        if $panzoom_events.empty?
+                          BigBlueButton.logger.info("No panzoom events; old recording?")
+                          BigBlueButton.logger.info("Synthesizing a panzoom event")
+                          if !$slides_events.empty?
+                            timestamp_orig = $slides_events.first[:timestamp].to_f
+                            # make sure this is scheduled *after* the slide is shown. Dunno if needed.
+                            timestamp_orig += 1000
+                            timestamp = ((timestamp_orig - $join_time) / 1000).round(1)
+                            $xml.event(:timestamp => timestamp, :orig => timestamp_orig) do
+                              $xml.viewBox "0 0 #{$vbox_width} #{$vbox_height}"
+                            end
+                            timestamp_orig_prev = timestamp_orig
+                            timestamp_prev = timestamp
+                            h_ratio_prev = 100
+                            w_ratio_prev = 100
+                            x_prev = 0
+                            y_prev = 0
+                          else
+                            BigBlueButton.logger.info("Couldn't find any slides! panzooms will be empty.")
+                          end
+                        else
+                          last_time = $panzoom_events.last[:timestamp].to_f
+                        end
 			$panzoom_events.each do |panZoomEvent|
 				# Get variables
 				timestamp_orig = panZoomEvent[:timestamp].to_f
@@ -524,7 +528,7 @@ def processShapesAndClears
 						
 						if(in_this_image)
                                                         # Get variables
-                                                        BigBlueButton.logger.info shape.to_xml(:indent => 2)
+                                                        BigBlueButton.logger.info shape
                                                         $shapeType = shape.xpath(".//type")[0].text()
                                                         $pageNumber = shape.xpath(".//pageNumber")[0].text()
                                                         $shapeDataPoints = shape.xpath(".//dataPoints")[0].text().split(",")
@@ -729,24 +733,21 @@ if ($playback == "presentation")
 		  FileUtils.mkdir_p video_dir
 		  BigBlueButton.logger.info("Made video dir - copying: #{$process_dir}/webcams.webm to -> #{video_dir}")
 		  FileUtils.cp("#{$process_dir}/webcams.webm", video_dir)
-		  BigBlueButton.logger.info("Copied .webm file")
+                  BigBlueButton.logger.info("Copied .webm file")
 		else
 		  audio_dir = "#{package_dir}/audio"
    		  BigBlueButton.logger.info("Making audio dir")
 		  FileUtils.mkdir_p audio_dir
-		  BigBlueButton.logger.info("Made audio dir - copying: #{$process_dir}/audio.ogg to -> #{audio_dir}")
-		  FileUtils.cp("#{$process_dir}/audio.ogg", audio_dir)
-		  BigBlueButton.logger.info("Copied .ogg file - copying: #{$process_dir}/audio.webm to -> #{audio_dir}")
-		  FileUtils.cp("#{$process_dir}/audio.webm", audio_dir)
-		  BigBlueButton.logger.info("Copied audio.webm file")	
+		  BigBlueButton.logger.info("Made audio dir - copying: #{$process_dir}/audio.webm to -> #{audio_dir}")
+                  FileUtils.cp("#{$process_dir}/audio.webm", audio_dir)
+		  BigBlueButton.logger.info("Copied audio.webm file - copying: #{$process_dir}/audio.ogg to -> #{audio_dir}")
+                  FileUtils.cp("#{$process_dir}/audio.ogg", audio_dir)
+                  BigBlueButton.logger.info("Copied audio.ogg file")
 		end
 
 		BigBlueButton.logger.info("Copying files to package dir")
 		FileUtils.cp_r("#{$process_dir}/presentation", package_dir)
 		BigBlueButton.logger.info("Copied files to package dir")
-
-		processing_time = File.read("#{$process_dir}/processing_time")
-
 		BigBlueButton.logger.info("Creating metadata.xml")
 		# Create metadata.xml
 		b = Builder::XmlMarkup.new(:indent => 2)
@@ -761,11 +762,9 @@ if ($playback == "presentation")
 			b.playback {
 				b.format("presentation")
 				b.link("http://#{playback_host}/playback/presentation/playback.html?meetingId=#{$meeting_id}")
-				b.processing_time("#{processing_time}")
 			}
 			b.meta {
 				BigBlueButton::Events.get_meeting_metadata("#{$process_dir}/events.xml").each { |k,v| b.method_missing(k,v) }
-
 			}
 		}
 		metadata_xml = File.new("#{package_dir}/metadata.xml","w")
@@ -832,7 +831,11 @@ if ($playback == "presentation")
 		FileUtils.rm_r(Dir.glob("#{target_dir}/*"))
                 rescue  Exception => e
                         BigBlueButton.logger.error(e.message)
-                end
+			e.backtrace.each do |traceline|
+				BigBlueButton.logger.error(traceline)
+			end
+			exit 1
+		end
 	else
 		BigBlueButton.logger.info("#{target_dir} is already there")
 	end