167 lines
5.8 KiB
Ruby
Executable File
167 lines
5.8 KiB
Ruby
Executable File
#!/usr/bin/ruby
|
|
# frozen_string_literal: true
|
|
|
|
# Copyright © 2019 BigBlueButton Inc. and by respective authors.
|
|
#
|
|
# This file is part of the BigBlueButton open source conferencing system.
|
|
#
|
|
# 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 <https://www.gnu.org/licenses/>.
|
|
|
|
require 'rubygems'
|
|
require 'bundler/setup'
|
|
|
|
require File.expand_path('../lib/recordandplayback', __dir__)
|
|
|
|
require 'journald/logger'
|
|
require 'locale'
|
|
require 'rb-inotify'
|
|
require 'yaml'
|
|
|
|
# Read configuration and set up logger
|
|
|
|
props = File.open(File.expand_path('bigbluebutton.yml', __dir__)) do |bbb_yml|
|
|
YAML.safe_load(bbb_yml)
|
|
end
|
|
|
|
logger = Journald::Logger.new('bbb-rap-caption-inbox')
|
|
BigBlueButton.logger = logger
|
|
|
|
captions_dir = props['captions_dir']
|
|
unless captions_dir
|
|
logger.error('captions_dir was not defined in bigbluebutton.yml')
|
|
exit(1)
|
|
end
|
|
captions_inbox_dir = File.join(captions_dir, 'inbox')
|
|
|
|
# Internal error classes
|
|
|
|
# Base class for internal errors
|
|
class CaptionError < StandardError
|
|
end
|
|
|
|
# Indicates that uploaded caption files are invalid (unrecoverable)
|
|
class InvalidCaptionError < CaptionError
|
|
end
|
|
|
|
# Implementation
|
|
|
|
caption_file_notify = proc do |json_filename|
|
|
# There's a possible race condition where we can be notified twice for a new
|
|
# file. That's fine, just do nothing the second time.
|
|
return unless File.exist?(json_filename)
|
|
|
|
logger.info("Found new caption index file #{json_filename}")
|
|
|
|
# TODO: Rather than do anything directly in this script, it should create a
|
|
# queue job (resque?) that does the actual work.
|
|
|
|
captions_work_base = File.join(props['recording_dir'], 'caption', 'inbox')
|
|
new_caption_info = File.open(json_filename) { |file| JSON.parse(file.read) }
|
|
record_id = new_caption_info['record_id']
|
|
logger.tag(record_id: record_id) do
|
|
begin
|
|
# Read the existing captions index file
|
|
# TODO: This is racy if multiple tools are editing the captions.json file
|
|
index_filename = File.join(captions_dir, record_id, 'captions.json')
|
|
captions_info =
|
|
begin
|
|
File.open(index_filename) { |file| JSON.parse(file.read) }
|
|
rescue StandardError
|
|
# No captions file or cannot be read, assume none present
|
|
[]
|
|
end
|
|
|
|
temp_filename = new_caption_info['temp_filename']
|
|
raise InvalidCaptionError, 'Temp filename is blank' if temp_filename.nil? || temp_filename.empty?
|
|
|
|
src_filename = File.join(captions_inbox_dir, temp_filename)
|
|
|
|
langtag = Locale::Tag::Rfc.parse(new_caption_info['lang'])
|
|
raise InvalidCaptionError, 'Language tag is not well-formed' unless langtag
|
|
|
|
# Remove the info for an existing matching track, and add the new one
|
|
captions_info.delete_if do |caption_info|
|
|
caption_info['lang'] == new_caption_info['lang'] &&
|
|
caption_info['kind'] == new_caption_info['kind']
|
|
end
|
|
captions_info << {
|
|
'kind' => new_caption_info['kind'],
|
|
'label' => new_caption_info['label'],
|
|
'lang' => langtag.to_s,
|
|
'source' => 'upload',
|
|
}
|
|
|
|
captions_work = File.join(captions_work_base, record_id)
|
|
FileUtils.mkdir_p(captions_work)
|
|
dest_filename = "#{new_caption_info['kind']}_#{new_caption_info['lang']}.vtt"
|
|
tmp_dest = File.join(captions_work, dest_filename)
|
|
final_dest_dir = File.join(captions_dir, record_id)
|
|
final_dest = File.join(final_dest_dir, dest_filename)
|
|
|
|
# Convert the received caption file to WebVTT
|
|
ffmpeg_cmd = [
|
|
'ffmpeg', '-y', '-v', 'warning', '-nostats', '-nostdin',
|
|
'-i', src_filename, '-map', '0:s',
|
|
'-f', 'webvtt', tmp_dest,
|
|
]
|
|
ret = BigBlueButton.exec_ret(*ffmpeg_cmd)
|
|
raise InvalidCaptionError, 'FFmpeg could not read input' unless ret.zero?
|
|
|
|
FileUtils.mkdir_p(final_dest_dir)
|
|
FileUtils.mv(tmp_dest, final_dest)
|
|
|
|
# Finally, save the updated index file that references the new caption
|
|
File.open(index_filename, 'w') do |file|
|
|
file.write(JSON.pretty_generate(captions_info))
|
|
end
|
|
|
|
Dir.glob(File.expand_path('captions/*', __dir__)) do |caption_script|
|
|
next unless File.file?(caption_script) && File.executable?(caption_script)
|
|
|
|
logger.info("Running caption integration script #{caption_script}")
|
|
ret = BigBlueButton.exec_ret(caption_script, '--record-id', record_id)
|
|
logger.warn('Caption integration script failed') unless ret.zero?
|
|
end
|
|
|
|
logger.info('Removing files from inbox directory')
|
|
FileUtils.rm_f(src_filename) if src_filename
|
|
FileUtils.rm_f(json_filename)
|
|
rescue InvalidCaptionError => e
|
|
logger.exception(e)
|
|
|
|
logger.info('Deleting invalid files from inbox directory')
|
|
FileUtils.rm_f(src_filename) if src_filename
|
|
FileUtils.rm_f(json_filename)
|
|
ensure
|
|
FileUtils.rm_rf(File.join(captions_work_base, record_id))
|
|
end
|
|
end
|
|
end
|
|
|
|
logger.info("Setting up inotify watch on #{captions_inbox_dir}")
|
|
notifier = INotify::Notifier.new
|
|
notifier.watch(captions_inbox_dir, :moved_to, :create) do |event|
|
|
next unless event.name.end_with?('-track.json')
|
|
|
|
caption_file_notify.call(event.absolute_name)
|
|
end
|
|
|
|
logger.info('Checking for missed/skipped caption files')
|
|
Dir.glob(File.join(captions_inbox_dir, '*-track.json')).each do |filename|
|
|
caption_file_notify.call(filename)
|
|
end
|
|
|
|
logger.info('Waiting for new caption files...')
|
|
notifier.run
|