preload big blue button conference recording data

test plan:
* have big blue button plugin enabled
 (may need to test in a production-like environment)
* the index should display recording data just like before
 (but also hopefully load much faster
 when there are multiple conferences)

closes #LA-716

Change-Id: I51e814996c02edebf7c38520d66891967eda0dd1
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/228972
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Mysti Lilla <mysti@instructure.com>
QA-Review: Anju Reddy <areddy@instructure.com>
Product-Review: James Williams <jamesw@instructure.com>
This commit is contained in:
James Williams 2020-03-05 08:49:28 -07:00
parent 32c99a4044
commit 82acf24eaa
5 changed files with 212 additions and 11 deletions

View File

@ -174,11 +174,14 @@ class ConferencesController < ApplicationController
def api_index(conferences)
route = polymorphic_url([:api_v1, @context, :conferences])
web_conferences = Api.paginate(conferences, self, route)
preload_recordings(web_conferences)
render json: api_conferences_json(web_conferences, @current_user, session)
end
protected :api_index
def web_index(conferences)
conferences = conferences.to_a
preload_recordings(conferences)
@new_conferences, @concluded_conferences = conferences.partition { |conference|
conference.ended_at.nil?
}
@ -408,4 +411,12 @@ class ConferencesController < ApplicationController
params.require(:web_conference).
permit(:title, :duration, :description, :conference_type, :user_settings => strong_anything)
end
def preload_recordings(conferences)
conferences.group_by(&:class).each do |klass, klass_conferences|
if klass.respond_to?(:preload_recordings) # should only be BigBlueButton for now
klass.preload_recordings(klass_conferences)
end
end
end
end

View File

@ -134,6 +134,29 @@ class BigBlueButtonConference < WebConference
super
end
attr_writer :loaded_recordings
# we can use the same API method with multiple meeting ids to load all the recordings up in one go
# instead of making a bunch of individual calls
def self.preload_recordings(conferences)
filtered_conferences = conferences.select{|c| c.conference_key && c.settings[:record]}
return unless filtered_conferences.any?
# have a limit so we don't send a ridiculously long URL over
limit = Setting.get("big_blue_button_preloaded_recordings_limit", "50").to_i
filtered_conferences.each_slice(limit) do |sliced_conferences|
meeting_ids = sliced_conferences.map(&:conference_key).join(",")
response = send_request(:getRecordings, {
:meetingID => meeting_ids,
})
result = response[:recordings] if response
result = [] if result.is_a?(String)
grouped_result = Array(result).group_by{|r| r[:meetingID]}
sliced_conferences.each do |c|
c.loaded_recordings = grouped_result[c.conference_key] || []
end
end
end
private
def retouch?
@ -167,22 +190,35 @@ class BigBlueButtonConference < WebConference
end
def fetch_recordings
return [] unless conference_key && settings[:record]
response = send_request(:getRecordings, {
:meetingID => conference_key,
})
result = response[:recordings] if response
result = [] if result.is_a?(String)
Array(result)
@loaded_recordings ||= begin
if conference_key && settings[:record]
response = send_request(:getRecordings, {
:meetingID => conference_key,
})
result = response[:recordings] if response
result = [] if result.is_a?(String)
Array(result)
else
[]
end
end
end
def generate_request(action, options)
def generate_request(*args)
self.class.generate_request(*args)
end
def self.generate_request(action, options)
query_string = options.to_query
query_string << ("&checksum=" + Digest::SHA1.hexdigest(action.to_s + query_string + config[:secret_dec]))
"https://#{config[:domain]}/bigbluebutton/api/#{action}?#{query_string}"
end
def send_request(action, options)
def send_request(*args)
self.class.send_request(*args)
end
def self.send_request(action, options)
url_str = generate_request(action, options)
http_response = nil
Canvas.timeout_protection("big_blue_button") do
@ -207,13 +243,13 @@ class BigBlueButtonConference < WebConference
nil
end
def xml_to_hash(xml_string)
def self.xml_to_hash(xml_string)
doc = Nokogiri::XML(xml_string)
# assumes the top level value will be a hash
xml_to_value(doc.root)
end
def xml_to_value(node)
def self.xml_to_value(node)
child_elements = node.element_children
# if there are no children at all, then this is an empty node

View File

@ -97,6 +97,23 @@ describe ConferencesController do
get 'index', params: {:course_id => @course.id}
expect(assigns[:new_conferences]).to be_empty
end
it "should preload recordings for BBB conferences" do
PluginSetting.create!(name: 'big_blue_button',
:settings => {
:domain => "bbb.totallyanexampleplzdontcallthis.com",
:secret_dec => "secret",
})
allow(BigBlueButtonConference).to receive(:send_request).and_return('')
user_session(@teacher)
@bbb = BigBlueButtonConference.create!(:title => "my conference", :user => @teacher, :context => @course)
@other = @course.web_conferences.create!(:conference_type => 'Wimba', :duration => 60, :user => @teacher)
expect(BigBlueButtonConference).to receive(:preload_recordings).with([@bbb])
get 'index', params: {:course_id => @course.id}
expect(response).to be_success
end
end
describe "POST 'create'" do

View File

@ -0,0 +1,120 @@
{
"returncode": "SUCCESS",
"recordings":
[
{
"recordID": "somerecordingidformeeting1a",
"meetingID": "instructure_web_conference_somemeetingkey1",
"name": "Conference Development 101",
"published": "true",
"protected": "false",
"startTime": "1513031612000",
"endTime": "1513031628000",
"metadata": {
"isBreakout": "false"
},
"playback":
[
{
"type": "statistics",
"url": "https://bbb.blah.com/instructure/ae094b1eb62b634c377fc255149de61a04ee8787-1521832370987/statistics/",
"length": null
},
{
"type": "presentation",
"url": "https://bbb.blah.com/instructure/18974fe54920ac60ba913e34f49e4a9dabfeea2c-1513031612456/presentation/",
"length": "2",
"preview": {
"images":
[
"https://bbb.blah.com/instructure/18974fe54920ac60ba913e34f49e4a9dabfeea2c-1513031612456/presentation/thumbnails/thumb-1.png",
"https://bbb.blah.com/instructure/18974fe54920ac60ba913e34f49e4a9dabfeea2c-1513031612456/presentation/thumbnails/thumb-2.png",
"https://bbb.blah.com/instructure/18974fe54920ac60ba913e34f49e4a9dabfeea2c-1513031612456/presentation/thumbnails/thumb-3.png"
]
}
},
{
"type": "video",
"url": "https://bbb.blah.com/instructure/18974fe54920ac60ba913e34f49e4a9dabfeea2c-1513031612456/capture/",
"length": "2"
}
]
},
{
"recordID": "somerecordingidformeeting1b",
"meetingID": "instructure_web_conference_somemeetingkey1",
"name": "Conference Development 101",
"published": "true",
"protected": "false",
"startTime": "1513031142000",
"endTime": "1513031164000",
"metadata": {
"isBreakout": "false"
},
"playback":
[
{
"type": "statistics",
"url": "https://bbb.blah.com/instructure/18974fe54920ac60ba913e34f49e4a9dabfeea2c-1513031142256/statistics/",
"length": null
},
{
"type": "presentation",
"url": "https://bbb.blah.com/instructure/18974fe54920ac60ba913e34f49e4a9dabfeea2c-1513031142256/presentation/",
"length": "2",
"preview": {
"images":
[
"https://bbb.blah.com/instructure/18974fe54920ac60ba913e34f49e4a9dabfeea2c-1513031142256/presentation/thumbnails/thumb-1.png",
"https://bbb.blah.com/instructure/18974fe54920ac60ba913e34f49e4a9dabfeea2c-1513031142256/presentation/thumbnails/thumb-2.png",
"https://bbb.blah.com/instructure/18974fe54920ac60ba913e34f49e4a9dabfeea2c-1513031142256/presentation/thumbnails/thumb-3.png"
]
}
},
{
"type": "video",
"url": "https://bbb.blah.com/instructure/18974fe54920ac60ba913e34f49e4a9dabfeea2c-1513031142256/capture/",
"length": "2"
}
]
},
{
"recordID": "somerecordingidformeeting2",
"meetingID": "instructure_web_conference_somemeetingkey2",
"name": "Conference Development 101",
"published": "true",
"protected": "false",
"startTime": "1513031142000",
"endTime": "1513031164000",
"metadata": {
"isBreakout": "false"
},
"playback":
[
{
"type": "statistics",
"url": "https://bbb.blah.com/instructure/18974fe54920ac60ba913e34f49e4a9dabfeea2c-1513031142256/statistics/",
"length": null
},
{
"type": "presentation",
"url": "https://bbb.blah.com/instructure/18974fe54920ac60ba913e34f49e4a9dabfeea2c-1513031142256/presentation/",
"length": "2",
"preview": {
"images":
[
"https://bbb.blah.com/instructure/18974fe54920ac60ba913e34f49e4a9dabfeea2c-1513031142256/presentation/thumbnails/thumb-1.png",
"https://bbb.blah.com/instructure/18974fe54920ac60ba913e34f49e4a9dabfeea2c-1513031142256/presentation/thumbnails/thumb-2.png",
"https://bbb.blah.com/instructure/18974fe54920ac60ba913e34f49e4a9dabfeea2c-1513031142256/presentation/thumbnails/thumb-3.png"
]
}
},
{
"type": "video",
"url": "https://bbb.blah.com/instructure/18974fe54920ac60ba913e34f49e4a9dabfeea2c-1513031142256/capture/",
"length": "2"
}
]
}
]
}

View File

@ -107,6 +107,7 @@ describe BigBlueButtonConference do
describe 'plugin setting recording_enabled is enabled' do
let(:get_recordings_fixture){File.read(Rails.root.join('spec', 'fixtures', 'files', 'conferences', 'big_blue_button_get_recordings_two.json'))}
let(:get_recordings_bulk_fixture){File.read(Rails.root.join('spec', 'fixtures', 'files', 'conferences', 'big_blue_button_get_recordings_bulk.json'))}
before do
allow(WebConference).to receive(:plugins).and_return([
@ -218,6 +219,22 @@ describe BigBlueButtonConference do
expect(response[:deleted]).to eq true
end
end
describe "recording preloading" do
it "should load up all recordings in a single api call" do
@bbb2 = BigBlueButtonConference.create!(:context => @bbb.context, :user => @bbb.user, :user_settings => @bbb.user_settings)
allow(@bbb).to receive(:conference_key).and_return('instructure_web_conference_somemeetingkey1')
allow(@bbb2).to receive(:conference_key).and_return('instructure_web_conference_somemeetingkey2')
response = JSON.parse(get_recordings_bulk_fixture, {symbolize_names: true})
allow(BigBlueButtonConference).to receive(:send_request).and_return(response)
BigBlueButtonConference.preload_recordings([@bbb, @bbb2])
[@bbb, @bbb2].each{|c| expect(c).to_not receive(:send_request)} # shouldn't need to send individual requests anymore
expect(@bbb.recordings.map{|r| r[:recording_id]}).to match_array(["somerecordingidformeeting1a", "somerecordingidformeeting1b"])
expect(@bbb2.recordings.map{|r| r[:recording_id]}).to match_array(["somerecordingidformeeting2"])
end
end
end
describe 'plugin setting recording disabled' do