diff --git a/app/controllers/video_chats_controller.rb b/app/controllers/video_chats_controller.rb new file mode 100644 index 000000000..09f86ac63 --- /dev/null +++ b/app/controllers/video_chats_controller.rb @@ -0,0 +1,23 @@ +class VideoChatsController < ApplicationController + before_action :authenticate_user! + + def show + account_sid = ApplicationConfig["TWILIO_ACCOUNT_SID"] + api_key = ApplicationConfig["TWILIO_VIDEO_API_KEY"] + api_secret = ApplicationConfig["TWILIO_VIDEO_API_SECRET"] + + token = Twilio::JWT::AccessToken.new( + account_sid, + api_key, + api_secret, + [], + identity: current_user.id, + ) + + grant = Twilio::JWT::AccessToken::VideoGrant.new + grant.room = params[:id] + token.add_grant(grant) + + @token = token.to_jwt + end +end diff --git a/app/javascript/packs/videoChat.jsx b/app/javascript/packs/videoChat.jsx new file mode 100644 index 000000000..c7a47fd89 --- /dev/null +++ b/app/javascript/packs/videoChat.jsx @@ -0,0 +1,119 @@ +import { h, Component, render } from 'preact'; +import PropTypes from 'prop-types'; + +const root = document.getElementById('chat') + + +export default class VideoChat extends Component { + + componentDidMount() { + this.setupCallChannel(root.dataset.token) + } + + setupCallChannel = token => { + const component = this; + const activeChannelId = root.dataset.channel + console.log() + import('twilio-video').then(({ connect, createLocalVideoTrack }) => { + connect( + token, + { + name: `private-video-channel-${activeChannelId}`, + audio: true, + type: 'peer-to-peer', + video: { width: 640 }, + }, + ).then( + function onConnectSuccess(room) { + console.log(room) + component.setState({ token: token, room }); + createLocalVideoTrack().then(track => { + const localMediaContainer = document.getElementById( + 'videolocalscreen', + ); + localMediaContainer.appendChild(track.attach()); + }); + const roomParticipants = []; + room.participants.forEach(participant => { + component.triggerRemoteJoin(participant); + roomParticipants.push(participant); + }); + component.setState({ participants: roomParticipants }); + room.on('participantConnected', function onParticipantConnected( + participant, + ) { + console.log('participant joined') + component.triggerRemoteJoin(participant); + room.participants.forEach(p => { + roomParticipants.push(p); + }); + component.setState({ participants: roomParticipants }); + room.on( + 'participantDisconnected', + function onParticipantDisconnected() { + console.log('disconnected') + }, + ); + participant.on('dominantSpeakerChanged', dominantSpeaker => { + // eslint-disable-next-line no-console + console.log( + 'The new dominant speaker in the Room is:', + dominantSpeaker, + ); + }); + }); + }, + function onConnectFailure(error) { + document.getElementById('videoremotescreen').innerHTML = ''; + // eslint-disable-next-line no-console + console.error(`Unable to connect to Room: ${error.message}`); + }, + ); + }); + }; + + triggerRemoteJoin = participant => { + participant.on('trackSubscribed', track => { + console.log(track) + if (!document.getElementById(`${track.kind}-${track.id}`)) { + const trackDiv = document.createElement('div'); + trackDiv.className = `chat__videocalltrackdiv--${track.kind}`; + trackDiv.id = participant.sid; + trackDiv.appendChild(track.attach()); + document.getElementById('videoremotescreen').appendChild(trackDiv); + document.getElementById( + 'videoremotescreen', + ).lastChild.id = `${track.kind}-${track.sid}`; + } + }); + participant.on('trackRemoved', track => { + if (document.getElementById(track.id)) { + document.getElementById(track.id).outerHTML = ''; + } + }); + // participant.on('trackDisabled', track => { + // console.log('disabled') + // console.log('TODO: Show track status on video') + // console.log(track.mediaStreamTrack.id) + // }); + // participant.on('trackEnabled', track => { + // console.log('enabled') + // console.log('TODO: Show track status on video') + // console.log(track.mediaStreamTrack.id) + // }); + }; + + + render() { + + return
+
+
+
+ } + +} + + +render(, root, root.firstElementChild); + diff --git a/app/models/message.rb b/app/models/message.rb index 5f1dcf597..e8bfd4c6e 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -118,6 +118,14 @@ class Message < ApplicationRecord #{user.name} ".html_safe + elsif anchor["href"].include?("/video_chats/") + html += " +

+ 👾 Experimental Video Chat Started +

+
".html_safe end end html diff --git a/app/views/video_chats/show.html.erb b/app/views/video_chats/show.html.erb new file mode 100644 index 000000000..a385459ea --- /dev/null +++ b/app/views/video_chats/show.html.erb @@ -0,0 +1,13 @@ +
+ +
+

This is an experiment

+

It's only a partially implemented view.

+

But this could be a more straightforward way to do video chat within /connect.

+<%# TODO Make paramsp[:id] map to a chat channel and ensure the users have permission %> +<%# That part is pretty straightforward %> +<%# Then close the loop on the JavaScript part of it. %> +<%# Also should be *fairly* straightforward in terms of following a basic tutorial. %> +<%# Then we drop a link in chat rather than trying to make video work with complicated state in chat. %> +<%# The complicated part has always been chat state/channels etc. Doing in sidecar means more containerization of logic. %> +<%= javascript_packs_with_chunks_tag "videoChat", defer: true %> diff --git a/config/routes.rb b/config/routes.rb index bb7470dac..0b44d3dc6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -214,6 +214,7 @@ Rails.application.routes.draw do resources :user_blocks, param: :blocked_id, only: %i[show create destroy] resources :podcasts, only: %i[new create] resources :article_approvals, only: %i[create] + resources :video_chats, only: %i[show] resolve("ProMembership") { [:pro_membership] } # see https://guides.rubyonrails.org/routing.html#using-resolve namespace :followings, defaults: { format: :json } do get :users diff --git a/spec/requests/video_chats_spec.rb b/spec/requests/video_chats_spec.rb new file mode 100644 index 000000000..cc887d6c6 --- /dev/null +++ b/spec/requests/video_chats_spec.rb @@ -0,0 +1,25 @@ +require "rails_helper" + +RSpec.describe "VideoChats", type: :request do + let(:user) { create(:user) } + + describe "GET /video_chats/:id" do + context "with user signed in" do + before do + sign_in user + end + + it "displays basic html for working" do + get "/video_chats/1" + expect(response.body).to include("