forked from External/greenlight
Add endpoint to detect new recordings and update the interface
The endpoint receives events from BigBlueButton via webhooks or scripts in the record and playback workflow. For now it only treats the event for when a recording is ready. When it happens, it uses action cable to update the interface dynamically with the new recording.
This commit is contained in:
parent
b518458622
commit
e5efb05a01
|
@ -26,23 +26,32 @@
|
||||||
|
|
||||||
var recordings = Recordings.getInstance();
|
var recordings = Recordings.getInstance();
|
||||||
var table = recordings.table.api();
|
var table = recordings.table.api();
|
||||||
var row = table.row("#"+data.record_id);
|
var row = table.row("#"+data.id);
|
||||||
|
|
||||||
if (data.action === 'update') {
|
if (data.action === 'update') {
|
||||||
var rowData = row.data();
|
var rowData = row.data();
|
||||||
|
|
||||||
rowData.published = data.published;
|
rowData.published = data.published;
|
||||||
rowData.listed = data.listed;
|
rowData.listed = data.listed;
|
||||||
table.row("#"+data.record_id).data(rowData);
|
table.row("#"+data.id).data(rowData);
|
||||||
recordings.draw();
|
recordings.draw();
|
||||||
|
|
||||||
var status = data.published ? (data.listed ? 'published' : 'unlisted') : 'unpublished';
|
var status = data.published ? (data.listed ? 'published' : 'unlisted') : 'unpublished';
|
||||||
showAlert(I18n['recording_'+status], 4000);
|
showAlert(I18n['recording_'+status], 4000);
|
||||||
|
|
||||||
} else if (data.action === 'delete') {
|
} else if (data.action === 'delete') {
|
||||||
row.remove();
|
row.remove();
|
||||||
recordings.draw();
|
recordings.draw();
|
||||||
|
|
||||||
showAlert(I18n.recording_deleted, 4000);
|
showAlert(I18n.recording_deleted, 4000);
|
||||||
|
|
||||||
|
} else if (data.action === 'create') {
|
||||||
|
if (row.length == 0) {
|
||||||
|
data.duration = data.length;
|
||||||
|
table.rows.add([data]);
|
||||||
|
recordings.draw();
|
||||||
|
showAlert(I18n.recording_created, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -166,13 +166,13 @@ class @Recordings
|
||||||
listed = btn.data('visibility') == "published"
|
listed = btn.data('visibility') == "published"
|
||||||
|
|
||||||
btn.prop('disabled', true)
|
btn.prop('disabled', true)
|
||||||
|
|
||||||
|
data = { published: published.toString() }
|
||||||
|
data["meta_" + GreenLight.META_LISTED] = listed.toString();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
url: url+'/recordings/'+id,
|
url: url+'/recordings/'+id,
|
||||||
data: {
|
data: data
|
||||||
published: published.toString(),
|
|
||||||
"meta_greenlight-listed": listed.toString()
|
|
||||||
}
|
|
||||||
}).done((data) ->
|
}).done((data) ->
|
||||||
|
|
||||||
).fail((data) ->
|
).fail((data) ->
|
||||||
|
|
|
@ -34,6 +34,7 @@ var loopJoin = function() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var alertTimeout = null;
|
||||||
var showAlert = function(html, timeout_delay) {
|
var showAlert = function(html, timeout_delay) {
|
||||||
if (!html) {
|
if (!html) {
|
||||||
return;
|
return;
|
||||||
|
@ -43,11 +44,12 @@ var showAlert = function(html, timeout_delay) {
|
||||||
$('#alerts').html($('.alert-template').html());
|
$('#alerts').html($('.alert-template').html());
|
||||||
|
|
||||||
if (timeout_delay) {
|
if (timeout_delay) {
|
||||||
setTimeout(function() {
|
clearTimeout(alertTimeout);
|
||||||
|
alertTimeout = setTimeout(function() {
|
||||||
$('#alerts > .alert').alert('close');
|
$('#alerts > .alert').alert('close');
|
||||||
}, timeout_delay);
|
}, timeout_delay);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
var displayRoomURL = function() {
|
var displayRoomURL = function() {
|
||||||
$('.meeting-url').val(Meeting.getInstance().getURL());
|
$('.meeting-url').val(Meeting.getInstance().getURL());
|
||||||
|
|
|
@ -20,6 +20,9 @@ class BbbController < ApplicationController
|
||||||
before_action :authorize_recording_owner!, only: [:update_recordings, :delete_recordings]
|
before_action :authorize_recording_owner!, only: [:update_recordings, :delete_recordings]
|
||||||
before_action :load_and_authorize_room_owner!, only: [:end]
|
before_action :load_and_authorize_room_owner!, only: [:end]
|
||||||
|
|
||||||
|
skip_before_action :verify_authenticity_token, only: :callback
|
||||||
|
before_action :validate_checksum, only: :callback
|
||||||
|
|
||||||
# GET /:resource/:id/join
|
# GET /:resource/:id/join
|
||||||
def join
|
def join
|
||||||
if params[:name].blank?
|
if params[:name].blank?
|
||||||
|
@ -39,7 +42,9 @@ class BbbController < ApplicationController
|
||||||
user_is_moderator: true
|
user_is_moderator: true
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
options[:meeting_logout_url] = "#{request.base_url}/#{params[:resource]}/#{params[:id]}"
|
base_url = "#{request.base_url}/#{params[:resource]}/#{params[:id]}"
|
||||||
|
options[:meeting_logout_url] = base_url
|
||||||
|
options[:hook_url] = "#{base_url}/callback"
|
||||||
|
|
||||||
bbb_res = bbb_join_url(
|
bbb_res = bbb_join_url(
|
||||||
params[:id],
|
params[:id],
|
||||||
|
@ -55,6 +60,20 @@ class BbbController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# POST /:resource/:id/callback
|
||||||
|
# Endpoint for webhook calls from BigBlueButton
|
||||||
|
def callback
|
||||||
|
begin
|
||||||
|
data = JSON.parse(read_body(request))
|
||||||
|
treat_callback_event(data["event"])
|
||||||
|
rescue Exception => e
|
||||||
|
logger.error "Error parsing webhook data. Data: #{data}, exception: #{e.inspect}"
|
||||||
|
|
||||||
|
# respond with 200 anyway so BigBlueButton knows the hook call was ok
|
||||||
|
render head(:ok)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# DELETE /rooms/:id/end
|
# DELETE /rooms/:id/end
|
||||||
def end
|
def end
|
||||||
load_and_authorize_room_owner!
|
load_and_authorize_room_owner!
|
||||||
|
@ -130,4 +149,61 @@ class BbbController < ApplicationController
|
||||||
@response = response
|
@response = response
|
||||||
render status: @status
|
render status: @status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def read_body(request)
|
||||||
|
request.body.read.force_encoding("UTF-8")
|
||||||
|
end
|
||||||
|
|
||||||
|
def treat_callback_event(event)
|
||||||
|
|
||||||
|
# a recording is ready
|
||||||
|
if event['header']['name'] == "publish_ended"
|
||||||
|
token = event['payload']['metadata'][META_TOKEN]
|
||||||
|
record_id = event['payload']['meeting_id']
|
||||||
|
|
||||||
|
# the webhook event doesn't have all the data we need, so we need
|
||||||
|
# to send a getRecordings anyway
|
||||||
|
# TODO: if the webhooks included all data we wouldn't need this
|
||||||
|
rec_info = bbb_get_recordings(token, record_id)
|
||||||
|
rec_info = rec_info[:recordings].first
|
||||||
|
RecordingCreatedJob.perform_later(token, parse_recording_for_view(rec_info))
|
||||||
|
end
|
||||||
|
|
||||||
|
render head(:ok) && return
|
||||||
|
end
|
||||||
|
|
||||||
|
# Validates the checksum received in a callback call.
|
||||||
|
# If the checksum doesn't match, renders an ok and aborts execution.
|
||||||
|
def validate_checksum
|
||||||
|
secret = ENV['BIGBLUEBUTTON_SECRET']
|
||||||
|
checksum = params["checksum"]
|
||||||
|
data = read_body(request)
|
||||||
|
callback_url = uri_remove_param(request.url, "checksum")
|
||||||
|
|
||||||
|
checksum_str = "#{callback_url}#{data}#{secret}"
|
||||||
|
calculated_checksum = Digest::SHA1.hexdigest(checksum_str)
|
||||||
|
|
||||||
|
if calculated_checksum != checksum
|
||||||
|
logger.error "Checksum did not match. Calculated: #{calculated_checksum}, received: #{checksum}"
|
||||||
|
|
||||||
|
# respond with 200 anyway so BigBlueButton knows the hook call was ok
|
||||||
|
# but abort execution
|
||||||
|
render head(:ok) && return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Removes parameters from an URI
|
||||||
|
def uri_remove_param(uri, params = nil)
|
||||||
|
return uri unless params
|
||||||
|
params = Array(params)
|
||||||
|
uri_parsed = URI.parse(uri)
|
||||||
|
return uri unless uri_parsed.query
|
||||||
|
new_params = uri_parsed.query.gsub(/&/, '&').split('&').reject { |q| params.include?(q.split('=').first) }
|
||||||
|
uri = uri.split('?').first
|
||||||
|
if new_params.count > 0
|
||||||
|
"#{uri}?#{new_params.join('&')}"
|
||||||
|
else
|
||||||
|
uri
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2016 BigBlueButton Inc. and by respective authors (see below).
|
||||||
|
#
|
||||||
|
# This program 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.0 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/>.
|
||||||
|
|
||||||
|
class RecordingCreatedJob < ApplicationJob
|
||||||
|
include BbbApi
|
||||||
|
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(room, recording)
|
||||||
|
ActionCable.server.broadcast "#{room}_recording_updates_channel",
|
||||||
|
{ action: 'create' }.merge(recording)
|
||||||
|
end
|
||||||
|
end
|
|
@ -28,7 +28,7 @@ class RecordingDeletesJob < ApplicationJob
|
||||||
if !bbb_res[:recordings] || bbb_res[:messageKey] == 'noRecordings'
|
if !bbb_res[:recordings] || bbb_res[:messageKey] == 'noRecordings'
|
||||||
ActionCable.server.broadcast "#{room}_recording_updates_channel",
|
ActionCable.server.broadcast "#{room}_recording_updates_channel",
|
||||||
action: 'delete',
|
action: 'delete',
|
||||||
record_id: record_id
|
id: record_id
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
sleep sleep_time
|
sleep sleep_time
|
||||||
|
|
|
@ -24,7 +24,7 @@ class RecordingUpdatesJob < ApplicationJob
|
||||||
recording = bbb_res[:recordings].first
|
recording = bbb_res[:recordings].first
|
||||||
ActionCable.server.broadcast "#{room}_recording_updates_channel",
|
ActionCable.server.broadcast "#{room}_recording_updates_channel",
|
||||||
action: 'update',
|
action: 'update',
|
||||||
record_id: record_id,
|
id: record_id,
|
||||||
published: recording[:published],
|
published: recording[:published],
|
||||||
listed: bbb_is_recording_listed(recording)
|
listed: bbb_is_recording_listed(recording)
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,7 +15,9 @@
|
||||||
# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
module BbbApi
|
module BbbApi
|
||||||
META_LISTED = "greenlight-listed"
|
META_LISTED = "gl-listed"
|
||||||
|
META_TOKEN = "gl-token"
|
||||||
|
META_HOOK_URL = "gl-webhooks-callback-url"
|
||||||
|
|
||||||
def bbb_endpoint
|
def bbb_endpoint
|
||||||
Rails.configuration.bigbluebutton_endpoint || ''
|
Rails.configuration.bigbluebutton_endpoint || ''
|
||||||
|
@ -53,7 +55,7 @@ module BbbApi
|
||||||
|
|
||||||
# See if the meeting is running
|
# See if the meeting is running
|
||||||
begin
|
begin
|
||||||
bbb_meeting_info = bbb.get_meeting_info( meeting_id, nil )
|
bbb_meeting_info = bbb.get_meeting_info(meeting_id, nil)
|
||||||
rescue BigBlueButton::BigBlueButtonException => exc
|
rescue BigBlueButton::BigBlueButtonException => exc
|
||||||
# This means that is not created
|
# This means that is not created
|
||||||
|
|
||||||
|
@ -72,9 +74,14 @@ module BbbApi
|
||||||
logoutURL: logout_url,
|
logoutURL: logout_url,
|
||||||
moderatorPW: moderator_password,
|
moderatorPW: moderator_password,
|
||||||
attendeePW: viewer_password,
|
attendeePW: viewer_password,
|
||||||
"meta_#{BbbApi::META_LISTED}": false
|
"meta_#{BbbApi::META_LISTED}": false,
|
||||||
|
"meta_#{BbbApi::META_TOKEN}": meeting_token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
meeting_options.merge!(
|
||||||
|
{ "meta_#{BbbApi::META_HOOK_URL}": options[:hook_url] }
|
||||||
|
) if options[:hook_url]
|
||||||
|
|
||||||
# Create the meeting
|
# Create the meeting
|
||||||
bbb.create_meeting(options[:meeting_name], meeting_id, meeting_options)
|
bbb.create_meeting(options[:meeting_name], meeting_id, meeting_options)
|
||||||
|
|
||||||
|
@ -110,7 +117,7 @@ module BbbApi
|
||||||
options[:recordID] = record_id
|
options[:recordID] = record_id
|
||||||
end
|
end
|
||||||
if meeting_id
|
if meeting_id
|
||||||
options[:meetingID] = (Digest::SHA1.hexdigest(Rails.application.secrets[:secret_key_base]+meeting_id)).to_s
|
options[:meetingID] = bbb_meeting_id(meeting_id)
|
||||||
end
|
end
|
||||||
res = bbb_safe_execute :get_recordings, options
|
res = bbb_safe_execute :get_recordings, options
|
||||||
|
|
||||||
|
@ -210,6 +217,42 @@ module BbbApi
|
||||||
recording[:metadata][BbbApi::META_LISTED.to_sym] == "true"
|
recording[:metadata][BbbApi::META_LISTED.to_sym] == "true"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Parses a recording as returned by getRecordings and returns it
|
||||||
|
# as an object as expected by the views.
|
||||||
|
# TODO: this is almost the same done by jbuilder templates (bbb/recordings),
|
||||||
|
# how to reuse them?
|
||||||
|
def parse_recording_for_view(recording)
|
||||||
|
recording[:previews] ||= []
|
||||||
|
previews = recording[:previews].map do |preview|
|
||||||
|
{
|
||||||
|
url: preview[:content],
|
||||||
|
width: preview[:width],
|
||||||
|
height: preview[:height],
|
||||||
|
alt: preview[:alt]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
recording[:playbacks] ||= []
|
||||||
|
playbacks = recording[:playbacks].map do |playback|
|
||||||
|
{
|
||||||
|
type: playback[:type],
|
||||||
|
type_i18n: t(playback[:type]),
|
||||||
|
url: playback[:url],
|
||||||
|
previews: previews
|
||||||
|
}
|
||||||
|
end
|
||||||
|
{
|
||||||
|
id: recording[:recordID],
|
||||||
|
name: recording[:name],
|
||||||
|
published: recording[:published],
|
||||||
|
end_time: recording[:endTime].to_s,
|
||||||
|
start_time: recording[:startTime].to_s,
|
||||||
|
length: recording[:length],
|
||||||
|
listed: recording[:listed],
|
||||||
|
playbacks: playbacks,
|
||||||
|
previews: previews
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def success_join_res(join_url)
|
def success_join_res(join_url)
|
||||||
{
|
{
|
||||||
returncode: true,
|
returncode: true,
|
||||||
|
|
|
@ -79,4 +79,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.I18n = <%= client_translations.to_json.html_safe %>
|
window.I18n = <%= client_translations.to_json.html_safe %>
|
||||||
|
window.GreenLight = {};
|
||||||
|
window.GreenLight.META_LISTED = "<%= BbbApi::META_LISTED %>";
|
||||||
|
window.GreenLight.META_TOKEN = "<%= BbbApi::META_TOKEN %>";
|
||||||
|
window.GreenLight.META_HOOK_URL = "<%= BbbApi::META_HOOK_URL %>";
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -48,6 +48,7 @@ en-US:
|
||||||
no_recordings: No Recordings
|
no_recordings: No Recordings
|
||||||
no_recordings_yet: No Recordings (Yet!)
|
no_recordings_yet: No Recordings (Yet!)
|
||||||
publish_recording: Publish recording
|
publish_recording: Publish recording
|
||||||
|
recording_created: A recording was created
|
||||||
recording_deleted: Recording was deleted
|
recording_deleted: Recording was deleted
|
||||||
recording_published: Recording was published
|
recording_published: Recording was published
|
||||||
recording_unlisted: Recording was unlisted
|
recording_unlisted: Recording was unlisted
|
||||||
|
|
|
@ -31,6 +31,7 @@ Rails.application.routes.draw do
|
||||||
get '/:resource/:id/join', to: 'bbb#join', as: :bbb_join, defaults: {format: 'json'}
|
get '/:resource/:id/join', to: 'bbb#join', as: :bbb_join, defaults: {format: 'json'}
|
||||||
get '/:resource/:id/wait', to: 'landing#wait_for_moderator'
|
get '/:resource/:id/wait', to: 'landing#wait_for_moderator'
|
||||||
get '/:resource/:id/session_status_refresh', to: 'landing#session_status_refresh'
|
get '/:resource/:id/session_status_refresh', to: 'landing#session_status_refresh'
|
||||||
|
post '/:resource/:id/callback', to: 'bbb#callback' #, defaults: {format: 'json'}
|
||||||
delete '/rooms/:id/end', to: 'bbb#end', defaults: {format: 'json'}
|
delete '/rooms/:id/end', to: 'bbb#end', defaults: {format: 'json'}
|
||||||
get '/rooms/:id/recordings', to: 'bbb#recordings', defaults: {format: 'json'}
|
get '/rooms/:id/recordings', to: 'bbb#recordings', defaults: {format: 'json'}
|
||||||
patch '/rooms/:id/recordings/:record_id', to: 'bbb#update_recordings', defaults: {format: 'json'}
|
patch '/rooms/:id/recordings/:record_id', to: 'bbb#update_recordings', defaults: {format: 'json'}
|
||||||
|
|
Loading…
Reference in New Issue