Signaling server, WS, login, room creation

This commit is contained in:
Thomas
2020-07-27 21:44:36 +02:00
parent 0ef9ba675b
commit c1787cacba
25 changed files with 1114 additions and 123 deletions

View File

@@ -1,9 +1,5 @@
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div>
</template>
@@ -19,14 +15,5 @@
#nav {
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
}
</style>

View File

@@ -1,61 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa" target="_blank" rel="noopener">pwa</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@@ -3,7 +3,12 @@ import App from './App.vue'
import './registerServiceWorker'
import router from './router'
import store from './store'
import Buefy from 'buefy'
import 'buefy/dist/buefy.css'
// import './assets/style.scss'
Vue.use(Buefy)
Vue.config.productionTip = false
new Vue({

View File

@@ -11,12 +11,9 @@ const routes = [
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
path: '/room',
name: 'Room',
component: () => import('../views/Room.vue')
}
]

205
client/src/rtc.js Normal file
View File

@@ -0,0 +1,205 @@
var name
var connections = new Map()
const configuration = {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
}
const offerOptions = {
offerToReceiveAudio: 1,
offerToReceiveVideo: 1
}
function handleLogin (success) {
if (success === false) {
alert('try a different username')
} else {
loginDiv.style.display = 'none'
connectDiv.style.display = 'block'
}
}
async function createPeerConnection (target) {
console.log('CREATED PEER CONNECTION')
var connection = new RTCPeerConnection(configuration)
connections[target] = connection
connection.onicecandidate = function (event) {
if (event.candidate) {
send({
type: 'candidate',
name: name,
target: target,
candidate: event.candidate
})
}
}
connection.onnegotiationneeded = function () { handleNegotiationNeededEvent(target) }
connection.ontrack = function (event) { handleTrackEvent(event) }
connection.onsignalingstatechange = function () { handleSignalingStateChangeEvent(connection) }
connection.oniceconnectionstatechange = function () { handleICEConnectionStateChangeEvent(connection) }
connection.onicegatheringstatechange = function () { handleICEGatheringStateChangeEvent(connection) }
// window.setInterval(getConnectionStats, 1000)
}
function handleICEConnectionStateChangeEvent (connection) {
console.log('ICE CONNECTION CHANGE ' + connection.iceConnectionState)
}
function handleICEGatheringStateChangeEvent (connection) {
console.log('ICE GATHERING CHANGE ' + connection.iceGatheringState)
}
async function makeOffer (target) {
createPeerConnection(target)
var connection = connections[target]
var offer = await connection.createOffer()
send({
type: 'offer',
name: name,
target: target,
offer: offer
})
await connection.setLocalDescription(offer)
}
async function handleOffer (offer, target) {
console.log('GOT OFFER FROM ' + target)
await createPeerConnection(target)
var connection = connections[target]
await connection.setRemoteDescription(new RTCSessionDescription(offer))
if (stream) {
console.log('STREAM DETECTED')
stream.getTracks().forEach((track) => {
console.log('ADDED TRACK')
connection.addTrack(track, stream)
})
}
var answer = await connection.createAnswer()
await connection.setLocalDescription(answer)
send({
type: 'answer',
name: name,
target: target,
answer: answer
})
for (let user of connections.keys()) {
console.log(user)
}
console.log('CONNECTION SIZE ' + connections.size)
videoTitle.innerHTML = name + ' | ' + connections.size + ' users connected'
}
async function handleAnswer (answer, target) {
console.log('GOT ANSWER FROM ' + target)
var connection = connections[target]
await connection.setRemoteDescription(new RTCSessionDescription(answer))
}
async function handleCandidate (candidate, target) {
console.log('GOT CANDIDATE FROM ' + target)
var connection = connections[target]
await connection.addIceCandidate(new RTCIceCandidate(candidate))
}
async function handleNegotiationNeededEvent (target) {
console.log('NEGOTIATION NEEDED FROM ' + target)
var connection = connections[target]
var offer = await connection.createOffer(offerOptions)
await connection.setLocalDescription(offer)
send({
type: 'video-offer',
name: name,
target: target,
sdp: connection.localDescription
})
}
function handleLeave () {
connections.forEach((connection) => {
connection.close()
connection = null
})
connections = new Map()
remoteVideo.pause()
remoteVideo.src = null
}
async function handleVideoOffer (sdp, target) {
console.log('GOT VIDEO OFFER FROM ' + target)
await createPeerConnection(target)
var connection = connections[target]
await connection.setRemoteDescription(new RTCSessionDescription(sdp))
if (stream) {
console.log('STREAM DETECTED')
stream.getTracks().forEach((track) => {
console.log('ADDED TRACK')
connection.addTrack(track, stream)
})
}
var answer = await connection.createAnswer()
await connection.setLocalDescription(answer)
send({
type: 'video-answer',
name: name,
target: target,
sdp: answer
})
// var keys = connections.keys().next().value
videoTitle.innerHTML = name + ' | connected to ' + target
}
async function handleVideoAnswer(sdp, target) {
console.log('GOT VIDEO ANSWER FROM ' + target)
var connection = connections[target]
await connection.setRemoteDescription(new RTCSessionDescription(sdp))
}
async function handleSignalingStateChangeEvent (connection) {
console.log('STATE CHANGED TO : ' + connection.signalingState)
switch(connection.signalingState) {
case 'closed':
await connection.close()
break
}
}
function handleTrackEvent (event) {
console.log('GOT TRACK')
remoteVideo.srcObject = event.streams[0]
videoDiv.style.display = 'block'
loadDiv.style.display = 'none'
}
function getConnectionStats () {
connections.forEach((connection, target) => {
console.log('[' + target + '] ' + connection.connectionState)
})
}
export const rtc = {
send
}

92
client/src/script.js Normal file
View File

@@ -0,0 +1,92 @@
/*
const loginDiv = document.querySelector('#loginDiv')
const loginInput = document.querySelector('#loginInput')
const loginBt = document.querySelector('#loginBt')
const connectDiv = document.querySelector('#connectDiv')
const callInput = document.querySelector('#callInput')
const callDatalist = document.querySelector('#callDatalist')
const callBt = document.querySelector('#callBt')
const videoInput = document.querySelector('#videoInput')
const videoDiv = document.querySelector('#videoDiv')
const videoTitle = document.querySelector('#videoTitle')
const remoteVideo = document.querySelector('#video')
const disconnectBt = document.querySelector('#disconnectBt')
const loadDiv = document.querySelector('#loadDiv')
const titleDiv = document.querySelector('#titleDiv')
var stream
var users
loginDiv.style.display = 'block'
loginBt.addEventListener('click', function (event) {
name = loginInput.value
if (name.length > 0) {
send({
type: 'login',
name: name
})
}
})
callBt.addEventListener('click', function () {
var callToUsername = callInput.value;
if (callToUsername.length > 0) {
makeOffer(callToUsername)
loadDiv.style.display = 'block'
titleDiv.style.display = 'none'
connectDiv.style.display = 'none'
}
})
disconnectBt.addEventListener('click', function () {
send({
type: 'leave',
name: name
})
handleLeave()
videoDiv.style.display = 'none'
loginDiv.style.display = 'block'
titleDiv.style.display = 'block'
})
videoInput.addEventListener('change', function (event) {
remoteVideo.src = URL.createObjectURL(this.files[0])
videoDiv.style.display = 'block'
connectDiv.style.display = 'none'
titleDiv.style.display = 'none'
videoTitle.innerHTML = name + ' | ' + connections.size + ' users connected'
})
remoteVideo.onplay = function () {
console.log('ADD STREAM')
if (remoteVideo.mozCaptureStream()) stream = remoteVideo.mozCaptureStream()
else stream = remoteVideo.captureStream()
}
function handleUserlist(list) {
console.log('GOT USER LIST '+list)
users = Array.from(list)
console.log(' users '+users)
console.log(typeof users)
console.log(' users '+users)
callDatalist.innerHTML = ''
users.forEach(user => {
if(user != name) callDatalist.innerHTML += '<option value="'+user+'"/>'
})
}
function handleError(message) {
console.log('Error '+message)
alert(
'AWWW FUCK \n\n'
+message
+'\n\nYou should probably reload the page')
}
*/

66
client/src/signal.js Normal file
View File

@@ -0,0 +1,66 @@
// const conn = new WebSocket('wss://oozik.gltronic.ovh/')
const conn = new WebSocket('wss://localhost:8080')
conn.onopen = function () {
console.log('Connected to the signaling server')
}
conn.onmessage = function (msg) {
console.log('Got message', msg.data)
var data = JSON.parse(msg.data)
switch (data.type) {
case 'login':
handleLogin(data.success)
break
case 'offer':
handleOffer(data.offer, data.name)
break
case 'answer':
handleAnswer(data.answer, data.name)
break
case 'candidate':
handleCandidate(data.candidate, data.name)
break
case 'userlist':
handleUserlist(data.users)
break
case 'leave':
handleLeave()
break
case 'video-offer':
handleVideoOffer(data.sdp, data.name)
break
case 'video-answer':
handleVideoAnswer(data.sdp, data.name)
break
case 'error':
handleError(data.message)
break
default:
break
}
}
conn.onerror = function (err) {
console.log('Got error', err)
}
function send (message) {
console.log('Sended message', message)
conn.send(JSON.stringify(message))
}
export const signal = {
send
}

View File

@@ -0,0 +1,75 @@
import { DialogProgrammatic as Dialog } from 'buefy'
import router from '@/router/index'
const state = {
signalServerConnected: false,
loginSuccess: false,
error: null,
serverStatus: {}
}
const getters = {
displayError: state => state.error,
displayUserList: state => state.userList,
displayLoginStatus: state => state.loginSuccess,
displayServerStatus: state => state.signalServerConnected
}
const actions = {
signalConnected ({ commit }) {
commit('SIGNAL_SUCCESS')
},
signalError ({ commit }, error) {
commit('SIGNAL_ERROR', error)
},
login ({ commit }, success) {
if (success) commit('LOGIN_SUCCESS')
else commit('LOGIN_ERROR')
},
serverStatus ({ commit }, serverStatus) {
commit('SET_SERVERSTATUS', serverStatus)
},
createRoom ({ commit, dispatch }, code) {
commit('CREATE_ROOM')
dispatch('room/setRoomCode', code, { root: true })
},
error ({ commit }, error) {
commit('ERROR', error)
}
}
const mutations = {
SIGNAL_SUCCESS (state) {
state.signalServerConnected = true
},
SIGNAL_ERROR (state, error) {
state.signalServerConnected = false
state.error = error
},
LOGIN_SUCCESS (state) {
state.loginSuccess = true
},
LOGIN_ERROR (state) {
state.loginSuccess = false
},
SET_SERVERSTATUS (state, serverStatus) {
state.serverStatus = serverStatus
},
CREATE_ROOM (state) {
state.room = true
state.admin = true
router.push({ name: 'Room' })
},
ERROR (state, error) {
state.error = error
Dialog.alert(error)
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

View File

@@ -1,15 +1,19 @@
import Vue from 'vue'
import Vuex from 'vuex'
import signal from './signalPlugin'
import rtc from './rtcModule'
import room from './roomModule'
import app from './appModule'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
rtc,
app,
room
},
plugins: [
signal()
]
})

View File

@@ -0,0 +1,46 @@
const state = {
admin: false,
roomStatus: {
roomName: '',
roomCode: '',
current: '',
playlist: []
}
}
const getters = {
displayRoomCode: state => state.roomCode,
displayRoomName: state => state.roomName
}
const actions = {
setRoomCode ({ commit }, roomCode) {
commit('SET_ROOMCODE', roomCode)
},
setRoomName ({ commit }, roomName) {
commit('SET_ROOMNAME', roomName)
},
setRoomStatus ({ commit }, roomStatus) {
commit('SET_ROOMSTATUS', roomStatus)
}
}
const mutations = {
SET_ROOMCODE (state, code) {
state.roomStatus.roomCode = code
},
SET_ROOMNAME (state, name) {
state.roomStatus.roomName = name
},
SET_ROOMSTATUS (state, roomStatus) {
state.roomStatus = roomStatus
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

View File

@@ -0,0 +1,128 @@
import { send } from './signalPlugin'
const configuration = {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
}
const state = {
name: null,
connections: new Map()
}
const getters = {
displayName: state => state.name
}
const actions = {
setName ({ commit }, name) {
commit('SET_NAME', name)
},
async offer ({ commit }, offer, name) {
commit('CREATE_PEER_CONNECTION', name)
commit('ANSWER', name, offer)
},
answer ({ commit }, answer, name) {
commit('OFFER', name, answer)
},
candidate ({ commit }, candidate, name) {
commit('ANSWER', name, candidate)
},
leave ({ commit }) {
commit('LEAVE')
},
broadcast ({ commit }, message) {
commit('BROADCAST', message)
}
}
const mutations = {
SET_NAME (state, name) {
state.name = name
},
CREATE_PEER_CONNECTION (state, target) {
console.log('CREATED PEER CONNECTION')
var connection = new RTCPeerConnection(configuration)
state.connections[target] = connection
connection.onicecandidate = function (event) {
if (event.candidate) {
send({
type: 'candidate',
name: name,
target: target,
candidate: event.candidate
})
}
}
connection.onnegotiationneeded = function () { handleNegotiationNeededEvent(target) }
connection.onsignalingstatechange = function () { handleSignalingStateChangeEvent(connection) }
connection.oniceconnectionstatechange = function () { handleICEConnectionStateChangeEvent(connection) }
connection.onicegatheringstatechange = function () { handleICEGatheringStateChangeEvent(connection) }
},
CREATE_DATA_CHANNEL (state) {
},
ANSWER (state, target, offer) {
var connection = state.connections[target]
connection.setRemoteDescription(new RTCSessionDescription(offer))
var answer = connection.createAnswer()
connection.setLocalDescription(answer)
send({
type: 'answer',
name: name,
target: target,
answer: answer
})
},
OFFER (state, target, answer) {
var connection = state.connections[target]
connection.setRemoteDescription(new RTCSessionDescription(answer))
},
CANDIDATE (state, target, candidate) {
var connection = state.connections[target]
connection.addIceCandidate(new RTCIceCandidate(candidate))
},
LEAVE (state) {
state.connections.forEach((connection) => {
connection.close()
connection = null
})
state.connections = new Map()
},
BROADCAST (state, message) {
}
}
function handleICEConnectionStateChangeEvent (connection) {
console.log('ICE CONNECTION CHANGE ' + connection.iceConnectionState)
}
function handleICEGatheringStateChangeEvent (connection) {
console.log('ICE GATHERING CHANGE ' + connection.iceGatheringState)
}
async function handleNegotiationNeededEvent (target) {
console.log('NEGOTIATION NEEDED FROM ' + target)
}
async function handleSignalingStateChangeEvent (connection) {
console.log('STATE CHANGED TO : ' + connection.signalingState)
switch (connection.signalingState) {
case 'closed':
await connection.close()
break
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

View File

@@ -0,0 +1,64 @@
const connection = new WebSocket('ws://localhost:8181/socket')
// const connection = new WebSocket('wss://echo.websocket.org')
export default function createSignalPlugin () {
return store => {
connection.onopen = function () {
console.log('WS connected')
store.dispatch('app/signalConnected')
}
connection.onerror = function (error) {
console.log('WS error ' + error)
store.dispatch('app/signalError', error)
}
connection.onmessage = function (message) {
console.log('WS message', message.data)
var data = JSON.parse(message.data)
switch (data.type) {
case 'offer':
store.dispatch('rtc/offer', data.offer, data.name)
break
case 'answer':
store.dispatch('rtc/answer', data.answer, data.name)
break
case 'candidate':
store.dispatch('rtc/candidate', data.candidate, data.name)
break
case 'leave':
store.dispatch('rtc/leave')
break
case 'login':
store.dispatch('app/login', data.message)
break
case 'serverInfos':
store.dispatch('app/serverStatus', data)
break
case 'createRoom':
store.dispatch('app/createRoom', data.message)
break
case 'error':
store.dispatch('app/error', data.message)
break
default:
break
}
}
}
}
export function send (message) {
console.log('WS send', message)
connection.send(JSON.stringify(message))
}

View File

@@ -1,5 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

View File

@@ -1,18 +1,103 @@
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
<div v-if="serverConnected">
<h1 class="title has-text-success">Server online</h1>
<h1 class="title">{{serverStatus.userCount}} users | {{serverStatus.roomCount}} rooms</h1>
</div>
<h1 v-else class="title has-text-danger">Server offline</h1>
<hr>
<b-button type="is-primary" @click="connectToRoomPrompt">Join a room</b-button>
<hr>
<b-button type="is-primary" @click="makeRoomPrompt">Make a room</b-button>
</div>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
import { send } from '../store/signalPlugin'
export default {
name: 'Home',
components: {
HelloWorld
computed: {
serverStatus () {
return this.$store.state.app.serverStatus
},
serverConnected () {
return this.$store.state.app.signalServerConnected
}
},
mounted () {
this.loginPrompt()
},
methods: {
loginPrompt () {
this.$buefy.dialog.prompt({
message: 'Choose a name',
trapFocus: true,
inputAttrs: {
placeholder: 'pedro',
minlength: 3,
maxlength: 30
},
confirmText: 'KK',
onConfirm: (name) => this.login(name)
})
},
login (name) {
send({
type: 'login',
name: name
})
this.$store.dispatch('rtc/setName', name)
},
makeRoomPrompt () {
this.$buefy.dialog.prompt({
message: 'Choose a room name',
trapFocus: true,
inputAttrs: {
placeholder: 'mah roomy',
minlength: 3,
maxlength: 30
},
cancelText: 'Nah',
confirmText: 'Go',
onConfirm: (name) => this.makeRoom(name)
})
},
makeRoom (name) {
send({
type: 'createRoom',
name: name
})
this.$store.dispatch('room/setRoomName', name)
},
connectToRoomPrompt () {
this.$buefy.dialog.prompt({
message: 'Enter room code',
trapFocus: true,
inputAttrs: {
placeholder: 'mah roomy',
minlength: 3,
maxlength: 30
},
cancelText: 'Nah',
confirmText: 'Connect',
onConfirm: (code) => this.connectToRoom(code)
})
},
connectToRoom (code) {
send({
type: 'connectRoom',
name: code
})
}
}
}
</script>
<style>
.home {
text-align: center;
color: #2c3e50;
margin: 20px 15px 0px 15px;
}
</style>

43
client/src/views/Room.vue Normal file
View File

@@ -0,0 +1,43 @@
<template>
<div class="room container">
<h1 class="title is-1">{{roomStatus.roomName}}</h1>
<h1 class="subtitle is-4">{{roomStatus.roomCode}}</h1>
<h2 class="subtitle">{{roomStatus.current.title}}</h2>
<b-table :data="roomStatus.playlist" striped hoverable default-sort="vote">
<template slot-scope="props">
<b-table-column field="title">
{{props.row.title}}
</b-table-column>
<b-table-column field="link">
{{props.row.link}}
</b-table-column>
<b-table-column field="vote">
{{props.row.vote}}
</b-table-column>
<b-table-column>
<b-button icon-left="arrow-up-bold-outline" type="is-dark"/>
<b-button icon-left="arrow-down-bold-outline" type="is-dark"/>
</b-table-column>
</template>
</b-table>
</div>
</template>
<script>
export default {
name: 'Room',
computed: {
roomStatus () {
return this.$store.state.room.roomStatus
}
}
}
</script>
<style>
</style>