Add video chat
This commit is contained in:
parent
a35a62b7bb
commit
d64c3b8d8d
1
.gitignore
vendored
1
.gitignore
vendored
@ -83,3 +83,4 @@ pickle-email-*.html
|
||||
# Environment files that may contain sensitive data
|
||||
.env
|
||||
.powenv
|
||||
node_modules/
|
||||
|
||||
@ -15,6 +15,8 @@ Development Environment Setup
|
||||
TODO
|
||||
----
|
||||
|
||||
- change from google ice server ./frontend/components/video_util.js:6
|
||||
|
||||
- Add video chat https://medium.com/@nicolas.e.schneider/videochat-in-under-30-a-rails-react-tutorial-534930c6cd96
|
||||
|
||||
- Refactor seat.jsx buttons in to a <form>.
|
||||
|
||||
190
app/assets/javascripts/bundle.js
Normal file
190
app/assets/javascripts/bundle.js
Normal file
File diff suppressed because one or more lines are too long
13
app/assets/javascripts/cable.js
Normal file
13
app/assets/javascripts/cable.js
Normal file
@ -0,0 +1,13 @@
|
||||
// Action Cable provides the framework to deal with WebSockets in Rails.
|
||||
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
|
||||
//
|
||||
//= require action_cable
|
||||
//= require_self
|
||||
//= require_tree ./channels
|
||||
|
||||
(function() {
|
||||
this.App || (this.App = {});
|
||||
|
||||
App.cable = ActionCable.createConsumer();
|
||||
|
||||
}).call(this);
|
||||
3
app/assets/javascripts/calls.coffee
Normal file
3
app/assets/javascripts/calls.coffee
Normal file
@ -0,0 +1,3 @@
|
||||
# Place all the behaviors and hooks related to the matching controller here.
|
||||
# All this logic will automatically be available in application.js.
|
||||
# You can use CoffeeScript in this file: http://coffeescript.org/
|
||||
9
app/assets/javascripts/channels/call.coffee
Normal file
9
app/assets/javascripts/channels/call.coffee
Normal file
@ -0,0 +1,9 @@
|
||||
#App.call = App.cable.subscriptions.create "CallChannel",
|
||||
# connected: ->
|
||||
# # Called when the subscription is ready for use on the server
|
||||
#
|
||||
# disconnected: ->
|
||||
# # Called when the subscription has been terminated by the server
|
||||
#
|
||||
# received: (data) ->
|
||||
# # Called when there's incoming data on the websocket for this channel
|
||||
3
app/assets/stylesheets/calls.scss
Normal file
3
app/assets/stylesheets/calls.scss
Normal file
@ -0,0 +1,3 @@
|
||||
// Place all the styles related to the Calls controller here.
|
||||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: https://sass-lang.com/
|
||||
4
app/channels/application_cable/channel.rb
Normal file
4
app/channels/application_cable/channel.rb
Normal file
@ -0,0 +1,4 @@
|
||||
module ApplicationCable
|
||||
class Channel < ActionCable::Channel::Base
|
||||
end
|
||||
end
|
||||
4
app/channels/application_cable/connection.rb
Normal file
4
app/channels/application_cable/connection.rb
Normal file
@ -0,0 +1,4 @@
|
||||
module ApplicationCable
|
||||
class Connection < ActionCable::Connection::Base
|
||||
end
|
||||
end
|
||||
9
app/channels/call_channel.rb
Normal file
9
app/channels/call_channel.rb
Normal file
@ -0,0 +1,9 @@
|
||||
class CallChannel < ApplicationCable::Channel
|
||||
def subscribed
|
||||
stream_from "call_channel"
|
||||
end
|
||||
|
||||
def unsubscribed
|
||||
# Any cleanup needed when channel is unsubscribed
|
||||
end
|
||||
end
|
||||
@ -1,7 +1,8 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
# Prevent CSRF attacks by raising an exception.
|
||||
# For APIs, you may want to use :null_session instead.
|
||||
protect_from_forgery with: :exception
|
||||
# protect_from_forgery with: :exception
|
||||
protect_from_forgery unless: -> { request.format.json? }
|
||||
after_action :flash_to_http_header
|
||||
|
||||
if (Rails.env.development? || Rails.env.test?)
|
||||
|
||||
12
app/controllers/calls_controller.rb
Normal file
12
app/controllers/calls_controller.rb
Normal file
@ -0,0 +1,12 @@
|
||||
class CallsController < ApplicationController
|
||||
before_action :skip_authorization
|
||||
def create
|
||||
head :no_content
|
||||
ActionCable.server.broadcast("call_channel", call_params)
|
||||
end
|
||||
private
|
||||
|
||||
def call_params
|
||||
params.permit(:call, :type, :from, :to, :sdp)
|
||||
end
|
||||
end
|
||||
2
app/helpers/calls_helper.rb
Normal file
2
app/helpers/calls_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module CallsHelper
|
||||
end
|
||||
3
app/views/calls/root.html.haml
Normal file
3
app/views/calls/root.html.haml
Normal file
@ -0,0 +1,3 @@
|
||||
%main#root
|
||||
%h1 If you're seeing this, something broke
|
||||
-# , props = {id: @game.id, current_user: current_user}, html_options = {id: "game-component-container"}
|
||||
@ -51,4 +51,7 @@ Rails.application.routes.draw do
|
||||
resources :users
|
||||
get '/loaderio-456644f4db1fcd1e8578be882d2fc476.txt', to: redirect('/assets/loaderio-456644f4db1fcd1e8578be882d2fc476.txt')
|
||||
get '/loaderio-eb4afd672bf5b9af467fbadcba243f26.txt', to: redirect('/assets/loaderio-eb4afd672bf5b9af467fbadcba243f26.txt')
|
||||
get '/calls', to: 'calls#root'
|
||||
resources :calls, only: :create
|
||||
mount ActionCable.server, at: '/cable'
|
||||
end
|
||||
|
||||
7
frontend/App.jsx
Normal file
7
frontend/App.jsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import VideoCall from './components/VideoCall';
|
||||
import ReactDOM from 'react-dom';
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const root = document.getElementById("root");
|
||||
ReactDOM.render(<VideoCall />, root)
|
||||
});
|
||||
151
frontend/components/VideoCall.jsx
Normal file
151
frontend/components/VideoCall.jsx
Normal file
@ -0,0 +1,151 @@
|
||||
import React from 'react';
|
||||
import { broadcastData, JOIN_CALL, LEAVE_CALL, EXCHANGE, ice } from './video_util.js';
|
||||
|
||||
class VideoCall extends React.Component{
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.pcPeers = {};
|
||||
this.userId = Math.floor(Math.random() * 10000);
|
||||
this.joinCall = this.joinCall.bind(this);
|
||||
this.leaveCall = this.leaveCall.bind(this);
|
||||
}
|
||||
componentDidMount(){
|
||||
this.remoteVideoContainer =
|
||||
document.getElementById("remote-video-container")
|
||||
navigator.mediaDevices.getUserMedia({audio: false, video: true})
|
||||
.then(stream => {
|
||||
this.localStream = stream;
|
||||
document.getElementById("local-video").srcObject = stream;
|
||||
}).catch(error => { console.log(error)});
|
||||
}
|
||||
join(data){ this.createPC(data.from, true) }
|
||||
joinCall(e){
|
||||
App.cable.subscriptions.create(
|
||||
{ channel: "CallChannel" },
|
||||
{
|
||||
connected: () => {
|
||||
broadcastData({ type: JOIN_CALL, from: this.userId});
|
||||
},
|
||||
received: data => {
|
||||
console.log("RECEIVED: ", data);
|
||||
if (data.from === this.userId) return;
|
||||
switch(data.type){
|
||||
case JOIN_CALL:
|
||||
return this.join(data);
|
||||
case EXCHANGE:
|
||||
if (data.to !== this.userId) return;
|
||||
return this.exchange(data);
|
||||
case LEAVE_CALL:
|
||||
return this.removeUser(data);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
createPC(userId, offerBool){
|
||||
const pc = new RTCPeerConnection(ice);
|
||||
this.pcPeers[userId] = pc;
|
||||
this.localStream.getTracks()
|
||||
.forEach(track => pc.addTrack(track, this.localStream));
|
||||
if (offerBool) {
|
||||
pc.createOffer().then(offer => {
|
||||
pc.setLocalDescription(offer).then(() => {
|
||||
setTimeout( () => {
|
||||
broadcastData({
|
||||
type: EXCHANGE,
|
||||
from: this.userId,
|
||||
to: userId,
|
||||
sdp: JSON.stringify(pc.localDescription),
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
pc.onicecandidate = (e) => {
|
||||
broadcastData({
|
||||
type: EXCHANGE,
|
||||
from: this.userId,
|
||||
to: userId,
|
||||
sdp: JSON.stringify(e.candidate)
|
||||
})
|
||||
}
|
||||
pc.ontrack = (e) => {
|
||||
const remoteVid = document.createElement("video");
|
||||
remoteVid.id = `remoteVideoContainer+${userId}`;
|
||||
remoteVid.autoplay = "autoplay";
|
||||
remoteVid.srcObject = e.streams[0];
|
||||
this.remoteVideoContainer.appendChild(remoteVid);
|
||||
}
|
||||
pc.oniceconnectionstatechange = (e) => {
|
||||
if (pc.iceConnectionState === 'disconnected'){
|
||||
broadcastData({ type: LEAVE_CALL, from: userId });
|
||||
}
|
||||
}
|
||||
return pc;
|
||||
}
|
||||
|
||||
exchange(data){
|
||||
let pc;
|
||||
if(this.pcPeers[data.from]){
|
||||
pc = this.pcPeers[data.from];
|
||||
} else{
|
||||
pc = this.createPC(data.from, false);
|
||||
}
|
||||
if (data.candidate){
|
||||
let candidate = JSON.parse(data.candidate)
|
||||
pc.addIceCandidate(new RTCIceCandidate(candidate))
|
||||
}
|
||||
if(data.sdp){
|
||||
const sdp = JSON.parse(data.sdp);
|
||||
if(sdp && !sdp.candidate){
|
||||
pc.setRemoteDescription(sdp).then( () =>{
|
||||
if (sdp.type === 'offer'){
|
||||
pc.createAnswer().then(answer => {
|
||||
pc.setLocalDescription(answer)
|
||||
.then( () => {
|
||||
broadcastData({
|
||||
type: EXCHANGE,
|
||||
from: this.userId,
|
||||
to: data.from,
|
||||
sdp: JSON.stringify(pc.localDescription)
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
leaveCall(e){
|
||||
const pcKeys = Object.keys(this.pcPeers);
|
||||
for (let i = 0; i < pcKeys.length; i++) {
|
||||
this.pcPeers[pcKeys[i]].close();
|
||||
}
|
||||
this.pcPeers = {};
|
||||
this.localVideo.srcObject.getTracks()
|
||||
.forEach(function (track) { track.stop(); })
|
||||
|
||||
this.localVideo.srcObject = null;
|
||||
App.cable.subscriptions.subscriptions = [];
|
||||
this.remoteVideoContainer.innerHTML = "";
|
||||
broadcastData({ type: LEAVE_CALL, from: this.userId });
|
||||
}
|
||||
removeUser(data){
|
||||
let video = document.getElementById(`remoteVideoContainer+${data.from}`);
|
||||
video && video.remove();
|
||||
let peers = this.pcPeers
|
||||
delete peers[data.from]
|
||||
}
|
||||
render(){
|
||||
return(
|
||||
<div className="VideoCall">
|
||||
<div id="remote-video-container"></div>
|
||||
<video id="local-video" autoPlay></video>
|
||||
<button onClick={this.joinCall}>Join Call</button>
|
||||
<button onClick={this.leaveCall}>Leave Call</button>
|
||||
</div>)
|
||||
}
|
||||
}
|
||||
export default VideoCall;
|
||||
16
frontend/components/video_util.js
Normal file
16
frontend/components/video_util.js
Normal file
@ -0,0 +1,16 @@
|
||||
export const JOIN_CALL = "JOIN_CALL";
|
||||
export const EXCHANGE = "EXCHANGE";
|
||||
export const LEAVE_CALL = "LEAVE_CALL";
|
||||
export const ice = { iceServers: [
|
||||
{
|
||||
urls: "stun:stun2.l.google.com:19302"
|
||||
}
|
||||
]};
|
||||
export const broadcastData = data => {
|
||||
fetch("calls", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: {"content-type": "application/json"}
|
||||
}
|
||||
);
|
||||
};
|
||||
3475
package-lock.json
generated
Normal file
3475
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
package.json
Normal file
26
package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "thirteen-tien-len",
|
||||
"version": "1.0.0",
|
||||
"description": "Thirteen Tien Len ================",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"webpack": "webpack --mode=development --watch"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
"@babel/preset-env": "^7.22.10",
|
||||
"@babel/preset-react": "^7.22.5",
|
||||
"babel-loader": "^9.1.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-cli": "^5.1.4"
|
||||
}
|
||||
}
|
||||
20
webpack.config.js
Normal file
20
webpack.config.js
Normal file
@ -0,0 +1,20 @@
|
||||
var path = require('path');
|
||||
module.exports = {
|
||||
entry: "./frontend/App.jsx",
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'app', 'assets', 'javascripts'),
|
||||
filename: "bundle.js"
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: [/\.jsx?$/],
|
||||
exclude: /(node_modules)/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: { presets: ['@babel/env', '@babel/react'] }
|
||||
},
|
||||
}]
|
||||
},
|
||||
devtool: 'eval-source-map',
|
||||
resolve: { extensions: ['.js', '.jsx', '*']}
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user