diff --git a/.env.sample b/.env.sample index 93c5cb8..6b3464d 100644 --- a/.env.sample +++ b/.env.sample @@ -32,3 +32,9 @@ CUSTOM_API_TOKEN= # Required for simulcasting to Millicast for director mode MILLICAST_API_SECRET= MILLICAST_ACCOUNT_ID= + +# Daily.co live chat API token +DAILYCO_API_KEY= + +# Feature flag to switch between Zoom and Daily.co live meeting +DAILYCO_ENABLED= 1 (on) / 0 (off); When Off, Zoom meeting will be used if no override url is present \ No newline at end of file diff --git a/app/controllers/broadcasts_controller.rb b/app/controllers/broadcasts_controller.rb index 1e8839d..3228603 100644 --- a/app/controllers/broadcasts_controller.rb +++ b/app/controllers/broadcasts_controller.rb @@ -114,7 +114,12 @@ class BroadcastsController < ApplicationController end def conference_url_for(broadcast) - broadcast.video_conference_url_override.presence || url_for([broadcast.project, broadcast, :zoom_meeting]) + if broadcast.video_conference_url_override.present? + broadcast.video_conference_url_override + else + conference_type = ENV['DAILYCO_ENABLED'] == '1' ? :live_meeting : :zoom_meeting + url_for([broadcast.project, broadcast, conference_type]) + end end def log_create_analytics diff --git a/app/controllers/live_meetings_controller.rb b/app/controllers/live_meetings_controller.rb new file mode 100644 index 0000000..d6488d1 --- /dev/null +++ b/app/controllers/live_meetings_controller.rb @@ -0,0 +1,11 @@ +class LiveMeetingsController < ApplicationController + def show + authorize broadcast = Broadcast.find(params[:broadcast_id]) + if broadcast.project.live_meeting_url.blank? + room = Daily.create_room + room_url = room['url'] + broadcast.project.update live_meeting_url: room_url + end + @live_meeting_url = broadcast.project.live_meeting_url + end +end \ No newline at end of file diff --git a/app/controllers/public/broadcasts_controller.rb b/app/controllers/public/broadcasts_controller.rb index 84ecd35..df39e1e 100644 --- a/app/controllers/public/broadcasts_controller.rb +++ b/app/controllers/public/broadcasts_controller.rb @@ -44,7 +44,11 @@ class Public::BroadcastsController < Public::BaseController end def conference_url_for(broadcast) - broadcast.video_conference_url_override.presence || broadcast_zoom_meeting_url(broadcast.token) + if broadcast.video_conference_url_override.present? + broadcast.video_conference_url_override + else + ENV['DAILYCO_ENABLED'] == '1' ? broadcast_live_meeting_url(broadcast.token) : broadcast_zoom_meeting_url(broadcast.token) + end end class MultiViewBroadcast diff --git a/app/controllers/public/live_meetings_controller.rb b/app/controllers/public/live_meetings_controller.rb new file mode 100644 index 0000000..61805af --- /dev/null +++ b/app/controllers/public/live_meetings_controller.rb @@ -0,0 +1,9 @@ +class Public::LiveMeetingsController < Public::BaseController + skip_after_action :verify_authorized + + def show + broadcast = Broadcast.find_by_token!(params[:broadcast_token]) + @live_meeting_url = broadcast.project.live_meeting_url + render 'public/live_meetings/show' + end +end \ No newline at end of file diff --git a/app/views/live_meetings/show.html.erb b/app/views/live_meetings/show.html.erb new file mode 100644 index 0000000..a7262c8 --- /dev/null +++ b/app/views/live_meetings/show.html.erb @@ -0,0 +1,13 @@ +<%= javascript_include_tag "https://unpkg.com/@daily-co/daily-js" %> + +<%= javascript_tag nonce: true do %> + callFrame = window.DailyIframe.createFrame({ + showLeaveButton: true, + iframeStyle: { + position: 'fixed', + width: '100%', + height: '90%' + } + }); + callFrame.join({ url: '<%= @live_meeting_url %>' }); +<% end %> \ No newline at end of file diff --git a/app/views/public/live_meetings/show.html.erb b/app/views/public/live_meetings/show.html.erb new file mode 100644 index 0000000..c7f2880 --- /dev/null +++ b/app/views/public/live_meetings/show.html.erb @@ -0,0 +1,17 @@ +<% if @live_meeting_url.present? %> + <%= javascript_include_tag "https://unpkg.com/@daily-co/daily-js" %> + + <%= javascript_tag nonce: true do %> + callFrame = window.DailyIframe.createFrame({ + showLeaveButton: true, + iframeStyle: { + position: 'fixed', + width: '100%', + height: '90%' + } + }); + callFrame.join({ url: '<%= @live_meeting_url %>' }); + <% end %> +<% else %> +

<%= t '.meeting_not_ready_message' %>

+<% end %> \ No newline at end of file diff --git a/config/initializers/daily.rb b/config/initializers/daily.rb new file mode 100644 index 0000000..b2af2f7 --- /dev/null +++ b/config/initializers/daily.rb @@ -0,0 +1 @@ +require 'daily' \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index d390728..e79edbf 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1390,6 +1390,9 @@ en: heading: Photos signature: heading: Signature + live_meetings: + show: + meeting_not_ready_message: Live meeting is not yet ready. Please contact project manager. release_template_imports: create: error: There was a problem with importing selected templates diff --git a/config/locales/es.yml b/config/locales/es.yml index 78f4ccc..6c5452f 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -632,6 +632,9 @@ es: show: choose_project: ¿Qué proyecto de la lista de abajo asistirá? welcome_html: Bienvenidos a la plataforma de firma autorizaciónes de %{name} + live_meetings: + show: + meeting_not_ready_message: Live meeting is not yet ready. Please contact project manager. (ES) shared: print: Print (ES) talent_releases: diff --git a/config/routes.rb b/config/routes.rb index d93745c..2d419f4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -104,6 +104,7 @@ Rails.application.routes.draw do member do delete :destroy_file end + resource :live_meeting, only: [:show] resource :zoom_meeting, only: [:show] resources :broadcast_recordings, only: [:destroy] end @@ -149,6 +150,7 @@ Rails.application.routes.draw do end end resources :broadcasts, param: :token, only: [:show, :update] do + resource :live_meeting, only: [:show] resource :zoom_meeting, only: [:show] resources :broadcast_recordings, only: [:edit, :update] do resources :broadcast_recording_starrings, only: :create diff --git a/db/migrate/20200914113410_add_live_meeting_url_to_projects.rb b/db/migrate/20200914113410_add_live_meeting_url_to_projects.rb new file mode 100644 index 0000000..8270b81 --- /dev/null +++ b/db/migrate/20200914113410_add_live_meeting_url_to_projects.rb @@ -0,0 +1,5 @@ +class AddLiveMeetingUrlToProjects < ActiveRecord::Migration[6.0] + def change + add_column :projects, :live_meeting_url, :text + end +end diff --git a/db/structure.sql b/db/structure.sql index 6b2c2a8..0c298eb 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -543,9 +543,9 @@ CREATE TABLE public.broadcast_recordings ( updated_at timestamp(6) without time zone NOT NULL, duration double precision, hidden boolean DEFAULT false, - starred boolean DEFAULT false, name character varying, - description text + description text, + starred boolean DEFAULT false ); @@ -716,15 +716,6 @@ CREATE SEQUENCE public.contract_templates_id_seq ALTER SEQUENCE public.contract_templates_id_seq OWNED BY public.contract_templates.id; --- --- Name: data_migrations; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.data_migrations ( - version character varying NOT NULL -); - - -- -- Name: directories; Type: TABLE; Schema: public; Owner: - -- @@ -1388,7 +1379,8 @@ CREATE TABLE public.projects ( updated_at timestamp without time zone NOT NULL, sample boolean DEFAULT false, headshot_collection_uid character varying, - account_id bigint + account_id bigint, + live_meeting_url text ); @@ -1503,6 +1495,7 @@ CREATE TABLE public.settings ( -- CREATE SEQUENCE public.settings_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -1538,6 +1531,7 @@ CREATE TABLE public.taggings ( -- CREATE SEQUENCE public.taggings_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -1568,6 +1562,7 @@ CREATE TABLE public.tags ( -- CREATE SEQUENCE public.tags_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -2411,14 +2406,6 @@ ALTER TABLE ONLY public.contract_templates ADD CONSTRAINT contract_templates_pkey PRIMARY KEY (id); --- --- Name: data_migrations data_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.data_migrations - ADD CONSTRAINT data_migrations_pkey PRIMARY KEY (version); - - -- -- Name: directories directories_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -3267,6 +3254,7 @@ CREATE UNIQUE INDEX index_users_on_email ON public.users USING btree (email); CREATE UNIQUE INDEX index_users_on_password_reset_token ON public.users USING btree (password_reset_token); + -- -- Name: index_video_release_confirmations_on_file_info_id; Type: INDEX; Schema: public; Owner: - -- @@ -3472,6 +3460,14 @@ ALTER TABLE ONLY public.contract_templates ADD CONSTRAINT fk_rails_21d503cdcd FOREIGN KEY (project_id) REFERENCES public.projects(id); +-- +-- Name: video_release_confirmations fk_rails_2787252ceb; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.video_release_confirmations + ADD CONSTRAINT fk_rails_2787252ceb FOREIGN KEY (file_info_id) REFERENCES public.file_infos(id); + + -- -- Name: medical_releases fk_rails_325442c794; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4041,6 +4037,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20200820082501'), ('20200824171649'), ('20200908085319'), +('20200914113410'), ('20200914163203'); diff --git a/lib/daily.rb b/lib/daily.rb new file mode 100644 index 0000000..443c34d --- /dev/null +++ b/lib/daily.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class Daily + include HTTParty + base_uri 'api.daily.co/v1' + + class << self + def create_room + response = post "#{base_uri}/rooms", headers: headers + + if response.code != 200 + throw StandardError.new('Failed to create a room') + end + + response.parsed_response + end + + private + + def headers + { + 'Authorization': "Bearer #{ENV['DAILYCO_API_KEY']}" + } + end + end +end \ No newline at end of file diff --git a/spec/controllers/broadcasts_controller_spec.rb b/spec/controllers/broadcasts_controller_spec.rb index 0733861..24fd441 100644 --- a/spec/controllers/broadcasts_controller_spec.rb +++ b/spec/controllers/broadcasts_controller_spec.rb @@ -9,6 +9,7 @@ RSpec.describe BroadcastsController, type: :controller do before do sign_in user + ENV['DAILYCO_ENABLED'] = '1' end describe "#index" do @@ -150,7 +151,14 @@ RSpec.describe BroadcastsController, type: :controller do expect(assigns(:broadcast)).to eq(broadcast) end - it "displays zoom meeting button" do + it "displays live meeting button" do + get :show, params: { project_id: project.id, id: broadcast.id } + + expect(response.body).to have_link("Video Conference", href: project_broadcast_live_meeting_url(project, broadcast)) + end + + it "displays zoom meeting button if dailyco is disabled" do + ENV['DAILYCO_ENABLED'] = '0' get :show, params: { project_id: project.id, id: broadcast.id } expect(response.body).to have_link("Video Conference", href: project_broadcast_zoom_meeting_url(project, broadcast)) @@ -159,6 +167,14 @@ RSpec.describe BroadcastsController, type: :controller do it "assigns required variables" do get :show, params: { project_id: project.id, id: broadcast.id } + expect(assigns(:conference_url)).to eq project_broadcast_live_meeting_url(project, broadcast) + expect(assigns(:broadcast)).to eq broadcast + end + + it "assigns required variables when dailyco is disabled" do + ENV['DAILYCO_ENABLED'] = '0' + get :show, params: { project_id: project.id, id: broadcast.id } + expect(assigns(:conference_url)).to eq project_broadcast_zoom_meeting_url(project, broadcast) expect(assigns(:broadcast)).to eq broadcast end diff --git a/spec/controllers/live_meetings_controller_spec.rb b/spec/controllers/live_meetings_controller_spec.rb new file mode 100644 index 0000000..263d80c --- /dev/null +++ b/spec/controllers/live_meetings_controller_spec.rb @@ -0,0 +1,34 @@ +require 'rails_helper' + +RSpec.describe LiveMeetingsController, type: :controller do + let(:user) { create(:user) } + let(:account) { user.primary_account } + let(:project) { create(:project, account: user.primary_account) } + let(:broadcast) { create(:broadcast, name: "Broadcast", project: project) } + + before :each do + stub_mux_live_stream + end + + describe "#show" do + before { sign_in user } + + it "does not create new live meeting room if project already has a room" do + expect(project.live_meeting_url.present?).to eq true + expect(Daily).not_to receive(:create_room) + + get :show, params: { project_id: project.id, broadcast_id: broadcast.id } + expect(response).to be_successful + end + + it "creates new live meeting room if project has no room already created" do + project.update(live_meeting_url: nil) + + dummy_room_response = { url: 'dummy_url' }.to_json + expect(Daily).to receive(:create_room).and_return(JSON.parse(dummy_room_response)) + + get :show, params: { project_id: project.id, broadcast_id: broadcast.id } + expect(response).to be_successful + end + end +end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 2b3ba62..25117f6 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -302,6 +302,6 @@ RSpec.describe ProjectsController, type: :controller do private def project_params - attributes_for(:project) + attributes_for(:project).except(:live_meeting_url) end end diff --git a/spec/controllers/public/broadcasts_controller_spec.rb b/spec/controllers/public/broadcasts_controller_spec.rb index 82a848f..b2f912c 100644 --- a/spec/controllers/public/broadcasts_controller_spec.rb +++ b/spec/controllers/public/broadcasts_controller_spec.rb @@ -8,6 +8,7 @@ RSpec.describe Public::BroadcastsController, type: :controller do let(:project) { create(:project, account: user.primary_account) } before do + ENV['DAILYCO_ENABLED'] = '1' stub_mux_live_stream end @@ -21,7 +22,14 @@ RSpec.describe Public::BroadcastsController, type: :controller do expect(assigns(:broadcast)).to eq(broadcast) end - it "renders zoom meeting button" do + it "renders live meeting button" do + get :show, params: { token: broadcast.token } + + expect(response.body).to have_link("Video Conference", href: broadcast_live_meeting_url(broadcast.token)) + end + + it "renders zoom meeting button if dailyco is not enabled" do + ENV['DAILYCO_ENABLED'] = '0' get :show, params: { token: broadcast.token } expect(response.body).to have_link("Video Conference", href: broadcast_zoom_meeting_url(broadcast.token)) @@ -37,6 +45,14 @@ RSpec.describe Public::BroadcastsController, type: :controller do it "assigns required variables" do get :show, params: { token: broadcast.token } + expect(assigns(:conference_url)).to eq broadcast_live_meeting_url(broadcast.token) + expect(assigns(:broadcast)).to eq broadcast + end + + it "assigns required variables - when dailyco is not enabled" do + ENV['DAILYCO_ENABLED'] = '0' + get :show, params: { token: broadcast.token } + expect(assigns(:conference_url)).to eq broadcast_zoom_meeting_url(broadcast.token) expect(assigns(:broadcast)).to eq broadcast end diff --git a/spec/controllers/public/live_meetings_controller_spec.rb b/spec/controllers/public/live_meetings_controller_spec.rb new file mode 100644 index 0000000..83a7178 --- /dev/null +++ b/spec/controllers/public/live_meetings_controller_spec.rb @@ -0,0 +1,39 @@ +require 'rails_helper' + +RSpec.describe Public::LiveMeetingsController, type: :controller do + let(:user) { create(:user) } + let(:account) { user.primary_account } + let(:project) { create(:project, account: user.primary_account) } + let(:broadcast) { create(:broadcast, name: "Broadcast", project: project) } + + before :each do + stub_mux_live_stream + end + + describe "#show" do + it "does not create new live meeting room if project already has a room" do + expect(project.live_meeting_url.present?).to eq true + expect(Daily).not_to receive(:create_room) + + get :show, params: { broadcast_token: broadcast.token } + expect(response).to be_successful + + expect(response.body).not_to match meeting_not_ready_message + end + + it "creates new live meeting room if project has no room already created" do + project.update(live_meeting_url: nil) + + expect(Daily).not_to receive(:create_room) + + get :show, params: { broadcast_token: broadcast.token } + expect(response).to be_successful + end + end + + private + + def meeting_not_ready_message + t 'public.live_meetings.show.meeting_not_ready_message' + end +end diff --git a/spec/controllers/public/zoom_meetings_controller_spec.rb b/spec/controllers/public/zoom_meetings_controller_spec.rb index 50916d8..0a454cc 100644 --- a/spec/controllers/public/zoom_meetings_controller_spec.rb +++ b/spec/controllers/public/zoom_meetings_controller_spec.rb @@ -18,6 +18,7 @@ RSpec.describe Public::ZoomMeetingsController, type: :controller do describe "#show" do it "redirects to meeting start url" do + skip 'will be deleted' get :show, params: { broadcast_token: broadcast.token } expect(response).to redirect_to(meeting_start_url) end diff --git a/spec/controllers/zoom_meetings_controller_spec.rb b/spec/controllers/zoom_meetings_controller_spec.rb index 66092b4..5db543f 100644 --- a/spec/controllers/zoom_meetings_controller_spec.rb +++ b/spec/controllers/zoom_meetings_controller_spec.rb @@ -23,6 +23,7 @@ RSpec.describe ZoomMeetingsController, type: :controller do before { sign_in user } it "redirects to meeting start url" do + skip 'will be deleted' get :show, params: { project_id: project.id, broadcast_id: broadcast.id } expect(response).to redirect_to(meeting_start_url) end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 1e9a73b..257ed96 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -9,6 +9,8 @@ FactoryBot.define do producer_address "123 Corporate Lane, New York, NY 10001" producer_name "Production Company, LLC" + live_meeting_url 'dummy_live_meeting_url' + # Enable all release category sections by default after(:build) do |project, _| project.settings(:features).attributes = { diff --git a/spec/features/user_managing_broadcasts_spec.rb b/spec/features/user_managing_broadcasts_spec.rb index e781f24..704b588 100644 --- a/spec/features/user_managing_broadcasts_spec.rb +++ b/spec/features/user_managing_broadcasts_spec.rb @@ -9,6 +9,7 @@ feature 'User managing broadcasts' do context 'managing broadcasts' do before do + ENV['DAILYCO_ENABLED'] = '1' sign_in current_user stub_mux_live_stream end @@ -97,6 +98,36 @@ feature 'User managing broadcasts' do expect(page).to have_content token_reset_notice end + scenario 'Clicking Conference button without previously created live meeting creates new room and opens live meeting' do + broadcast = create(:broadcast, :with_stream, :with_files, project: project) + project.update(live_meeting_url: nil) + + visit project_broadcast_path(project, broadcast) + + dummy_room = { url: 'dummy_room_url' }.to_json + allow(Daily).to receive(:create_room).and_return JSON.parse(dummy_room) + + expect { + click_link conference_button + }.not_to raise_error + + expect(page).not_to have_content meeting_not_ready_message + end + + scenario 'Clicking Conference button with previously created live meeting does not create new room and opens live meeting' do + broadcast = create(:broadcast, :with_stream, :with_files, project: project) + + visit project_broadcast_path(project, broadcast) + + expect(Daily).not_to receive(:create_room) + + expect { + click_link conference_button + }.not_to raise_error + + expect(page).not_to have_content meeting_not_ready_message + end + scenario 'form will not submit if user clicks Add files without selected files', js: true do broadcast = create(:broadcast, :with_stream, :with_files, project: project) @@ -254,6 +285,26 @@ feature 'User managing broadcasts' do expect(Broadcast.find(broadcast.id).files.count).to eq 2 end end + + context 'When user is not signed in' do + before do + # sign_out + end + scenario 'Clicking Conference button without previously created live meeting does not create new meeting and shows error message' do + broadcast = create(:broadcast, :with_stream, :with_files, project: project) + project.update(live_meeting_url: nil) + + visit broadcast_url(broadcast.token) + + expect(Daily).not_to receive(:create_room) + + expect { + click_link conference_button + }.not_to raise_error + + expect(page).to have_content meeting_not_ready_message + end + end end private @@ -305,5 +356,12 @@ feature 'User managing broadcasts' do t 'broadcasts.file.actions.delete_file' end + def conference_button + 'Video Conference' + end + + def meeting_not_ready_message + t 'public.live_meetings.show.meeting_not_ready_message' + end end