/record-and-playback/presentation/scripts/process/presentation.rb

http://github.com/bigbluebutton/bigbluebutton · Ruby · 275 lines · 184 code · 41 blank · 50 comment · 25 complexity · 3208d4284326d785082e071c5dda9777 MD5 · raw file

  1. # Set encoding to utf-8
  2. # encoding: UTF-8
  3. #
  4. # BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
  5. #
  6. # Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
  7. #
  8. # This program is free software; you can redistribute it and/or modify it under the
  9. # terms of the GNU Lesser General Public License as published by the Free Software
  10. # Foundation; either version 3.0 of the License, or (at your option) any later
  11. # version.
  12. #
  13. # BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
  14. # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
  15. # PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Lesser General Public License along
  18. # with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
  19. #
  20. # For DEVELOPMENT
  21. # Allows us to run the script manually
  22. # require File.expand_path('../../../../core/lib/recordandplayback', __FILE__)
  23. # For PRODUCTION
  24. require File.expand_path('../../../lib/recordandplayback', __FILE__)
  25. require 'rubygems'
  26. require 'trollop'
  27. require 'yaml'
  28. require 'json'
  29. opts = Trollop::options do
  30. opt :meeting_id, "Meeting id to archive", :default => '58f4a6b3-cd07-444d-8564-59116cb53974', :type => String
  31. end
  32. meeting_id = opts[:meeting_id]
  33. # This script lives in scripts/archive/steps while properties.yaml lives in scripts/
  34. props = BigBlueButton.read_props
  35. presentation_props = YAML::load(File.open('presentation.yml'))
  36. presentation_props['audio_offset'] = 0 if presentation_props['audio_offset'].nil?
  37. presentation_props['include_deskshare'] = false if presentation_props['include_deskshare'].nil?
  38. recording_dir = props['recording_dir']
  39. raw_archive_dir = "#{recording_dir}/raw/#{meeting_id}"
  40. log_dir = props['log_dir']
  41. target_dir = "#{recording_dir}/process/presentation/#{meeting_id}"
  42. if not FileTest.directory?(target_dir)
  43. FileUtils.mkdir_p "#{log_dir}/presentation"
  44. logger = Logger.new("#{log_dir}/presentation/process-#{meeting_id}.log", 'daily' )
  45. BigBlueButton.logger = logger
  46. BigBlueButton.logger.info("Processing script presentation.rb")
  47. FileUtils.mkdir_p target_dir
  48. begin
  49. # Create a copy of the raw archives
  50. temp_dir = "#{target_dir}/temp"
  51. FileUtils.mkdir_p temp_dir
  52. FileUtils.cp_r(raw_archive_dir, temp_dir)
  53. # Create initial metadata.xml
  54. b = Builder::XmlMarkup.new(:indent => 2)
  55. metaxml = b.recording {
  56. b.id(meeting_id)
  57. b.state("processing")
  58. b.published(false)
  59. b.start_time
  60. b.end_time
  61. b.participants
  62. b.playback
  63. b.meta
  64. }
  65. metadata_xml = File.new("#{target_dir}/metadata.xml","w")
  66. metadata_xml.write(metaxml)
  67. metadata_xml.close
  68. BigBlueButton.logger.info("Created inital metadata.xml")
  69. BigBlueButton::AudioProcessor.process("#{temp_dir}/#{meeting_id}", "#{target_dir}/audio")
  70. events_xml = "#{temp_dir}/#{meeting_id}/events.xml"
  71. FileUtils.cp(events_xml, target_dir)
  72. presentation_dir = "#{temp_dir}/#{meeting_id}/presentation"
  73. presentations = BigBlueButton::Presentation.get_presentations(events_xml)
  74. processed_pres_dir = "#{target_dir}/presentation"
  75. FileUtils.mkdir_p processed_pres_dir
  76. # Get the real-time start and end timestamp
  77. @doc = Nokogiri::XML(File.read("#{target_dir}/events.xml"))
  78. meeting_start = @doc.xpath("//event")[0][:timestamp]
  79. meeting_end = @doc.xpath("//event").last()[:timestamp]
  80. match = /.*-(\d+)$/.match(meeting_id)
  81. real_start_time = match[1]
  82. real_end_time = (real_start_time.to_i + (meeting_end.to_i - meeting_start.to_i)).to_s
  83. # Add start_time, end_time and meta to metadata.xml
  84. ## Load metadata.xml
  85. metadata = Nokogiri::XML(File.read("#{target_dir}/metadata.xml"))
  86. ## Add start_time and end_time
  87. recording = metadata.root
  88. ### Date Format for recordings: Thu Mar 04 14:05:56 UTC 2010
  89. start_time = recording.at_xpath("start_time")
  90. start_time.content = real_start_time
  91. end_time = recording.at_xpath("end_time")
  92. end_time.content = real_end_time
  93. ## Copy the breakout and breakout rooms node from
  94. ## events.xml if present.
  95. breakout_xpath = @doc.xpath("//breakout")
  96. breakout_rooms_xpath = @doc.xpath("//breakoutRooms")
  97. meeting_xpath = @doc.xpath("//meeting")
  98. if (meeting_xpath != nil)
  99. recording << meeting_xpath
  100. end
  101. if (breakout_xpath != nil)
  102. recording << breakout_xpath
  103. end
  104. if (breakout_rooms_xpath != nil)
  105. recording << breakout_rooms_xpath
  106. end
  107. participants = recording.at_xpath("participants")
  108. participants.content = BigBlueButton::Events.get_num_participants(@doc)
  109. ## Remove empty meta
  110. metadata.search('//recording/meta').each do |meta|
  111. meta.remove
  112. end
  113. ## Add the actual meta
  114. metadata_with_playback = Nokogiri::XML::Builder.with(metadata.at('recording')) do |xml|
  115. xml.meta {
  116. BigBlueButton::Events.get_meeting_metadata("#{target_dir}/events.xml").each { |k,v| xml.method_missing(k,v) }
  117. }
  118. end
  119. ## Write the new metadata.xml
  120. metadata_file = File.new("#{target_dir}/metadata.xml","w")
  121. metadata = Nokogiri::XML(metadata.to_xml) { |x| x.noblanks }
  122. metadata_file.write(metadata.root)
  123. metadata_file.close
  124. BigBlueButton.logger.info("Created an updated metadata.xml with start_time and end_time")
  125. # Start processing raw files
  126. presentation_text = {}
  127. presentations.each do |pres|
  128. pres_dir = "#{presentation_dir}/#{pres}"
  129. num_pages = BigBlueButton::Presentation.get_number_of_pages_for(pres_dir)
  130. target_pres_dir = "#{processed_pres_dir}/#{pres}"
  131. FileUtils.mkdir_p target_pres_dir
  132. FileUtils.mkdir_p "#{target_pres_dir}/textfiles"
  133. images=Dir.glob("#{pres_dir}/#{pres}.{jpg,jpeg,png,gif,JPG,JPEG,PNG,GIF}")
  134. if images.empty?
  135. pres_name = "#{pres_dir}/#{pres}"
  136. if File.exists?("#{pres_name}.pdf")
  137. pres_pdf = "#{pres_name}.pdf"
  138. BigBlueButton.logger.info("Found pdf file for presentation #{pres_pdf}")
  139. elsif File.exists?("#{pres_name}.PDF")
  140. pres_pdf = "#{pres_name}.PDF"
  141. BigBlueButton.logger.info("Found PDF file for presentation #{pres_pdf}")
  142. elsif File.exists?("#{pres_name}")
  143. pres_pdf = pres_name
  144. BigBlueButton.logger.info("Falling back to old presentation filename #{pres_pdf}")
  145. else
  146. pres_pdf = ""
  147. BigBlueButton.logger.warn("Could not find pdf file for presentation #{pres}")
  148. end
  149. if !pres_pdf.empty?
  150. text = {}
  151. 1.upto(num_pages) do |page|
  152. BigBlueButton::Presentation.extract_png_page_from_pdf(
  153. page, pres_pdf, "#{target_pres_dir}/slide-#{page}.png", '1600x1600')
  154. if File.exist?("#{pres_dir}/textfiles/slide-#{page}.txt") then
  155. t = File.read("#{pres_dir}/textfiles/slide-#{page}.txt", encoding: 'UTF-8')
  156. text["slide-#{page}"] = t.encode('UTF-8', invalid: :replace)
  157. FileUtils.cp("#{pres_dir}/textfiles/slide-#{page}.txt", "#{target_pres_dir}/textfiles")
  158. end
  159. end
  160. presentation_text[pres] = text
  161. end
  162. else
  163. ext = File.extname("#{images[0]}")
  164. BigBlueButton::Presentation.convert_image_to_png(
  165. images[0], "#{target_pres_dir}/slide-1.png", '1600x1600')
  166. end
  167. # Copy thumbnails from raw files
  168. FileUtils.cp_r("#{pres_dir}/thumbnails", "#{target_pres_dir}/thumbnails") if File.exist?("#{pres_dir}/thumbnails")
  169. end
  170. BigBlueButton.logger.info("Generating closed captions")
  171. ret = BigBlueButton.exec_ret('utils/gen_webvtt', '-i', raw_archive_dir, '-o', target_dir)
  172. if ret != 0
  173. raise "Generating closed caption files failed"
  174. end
  175. captions = JSON.load(File.new("#{target_dir}/captions.json", 'r'))
  176. if not presentation_text.empty?
  177. # Write presentation_text.json to file
  178. File.open("#{target_dir}/presentation_text.json","w") { |f| f.puts presentation_text.to_json }
  179. end
  180. # We have to decide whether to actually generate the webcams video file
  181. # We do so if any of the following conditions are true:
  182. # - There is webcam video present, or
  183. # - There's broadcast video present, or
  184. # - There are closed captions present (they need a video stream to be rendered on top of)
  185. if !Dir["#{raw_archive_dir}/video/*"].empty? or
  186. !Dir["#{raw_archive_dir}/video-broadcast/*"].empty? or
  187. captions.length > 0
  188. webcam_width = presentation_props['video_output_width']
  189. webcam_height = presentation_props['video_output_height']
  190. webcam_framerate = presentation_props['video_output_framerate']
  191. # Use a higher resolution video canvas if there's broadcast video streams
  192. if !Dir["#{raw_archive_dir}/video-broadcast/*"].empty?
  193. webcam_width = presentation_props['deskshare_output_width']
  194. webcam_height = presentation_props['deskshare_output_height']
  195. webcam_framerate = presentation_props['deskshare_output_framerate']
  196. end
  197. webcam_framerate = 15 if webcam_framerate.nil?
  198. processed_audio_file = BigBlueButton::AudioProcessor.get_processed_audio_file("#{temp_dir}/#{meeting_id}", "#{target_dir}/audio")
  199. BigBlueButton.process_webcam_videos(target_dir, temp_dir, meeting_id, webcam_width, webcam_height, webcam_framerate, presentation_props['audio_offset'], processed_audio_file, presentation_props['video_formats'])
  200. end
  201. if !Dir["#{raw_archive_dir}/deskshare/*"].empty? and presentation_props['include_deskshare']
  202. deskshare_width = presentation_props['deskshare_output_width']
  203. deskshare_height = presentation_props['deskshare_output_height']
  204. deskshare_framerate = presentation_props['deskshare_output_framerate']
  205. deskshare_framerate = 5 if deskshare_framerate.nil?
  206. BigBlueButton.process_deskshare_videos(target_dir, temp_dir, meeting_id, deskshare_width, deskshare_height, deskshare_framerate, presentation_props['video_formats'])
  207. end
  208. # Copy shared notes from raw files
  209. if !Dir["#{raw_archive_dir}/notes/*"].empty?
  210. FileUtils.cp_r("#{raw_archive_dir}/notes", target_dir)
  211. end
  212. process_done = File.new("#{recording_dir}/status/processed/#{meeting_id}-presentation.done", "w")
  213. process_done.write("Processed #{meeting_id}")
  214. process_done.close
  215. # Update state in metadata.xml
  216. ## Load metadata.xml
  217. metadata = Nokogiri::XML(File.read("#{target_dir}/metadata.xml"))
  218. ## Update status
  219. recording = metadata.root
  220. state = recording.at_xpath("state")
  221. state.content = "processed"
  222. ## Write the new metadata.xml
  223. metadata_file = File.new("#{target_dir}/metadata.xml","w")
  224. metadata_file.write(metadata.root)
  225. metadata_file.close
  226. BigBlueButton.logger.info("Created an updated metadata.xml with state=processed")
  227. rescue Exception => e
  228. BigBlueButton.logger.error(e.message)
  229. e.backtrace.each do |traceline|
  230. BigBlueButton.logger.error(traceline)
  231. end
  232. exit 1
  233. end
  234. end