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,3 +1,5 @@
# oozik 2
Webapp edition
socket messages types: serverInfos, login, leave, userList, createRoom, connectRoom, offer, answer, candidate

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>

View File

@@ -11,26 +11,22 @@
<groupId>gltronic</groupId>
<artifactId>oozik</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<name>oozik</name>
<description>oozik signaling server</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
@@ -48,6 +44,12 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20200518</version>
</dependency>
</dependencies>
<build>

View File

@@ -1,13 +0,0 @@
package gltronic.oozik;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

View File

@@ -0,0 +1,17 @@
package gltronic.oozik;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.PropertySource;
@SpringBootApplication
@ComponentScan(basePackages = "gltronic.oozik")
@PropertySource("classpath:application.properties")
public class OozikApplication {
public static void main(String[] args) {
SpringApplication.run(OozikApplication.class, args);
}
}

View File

@@ -0,0 +1,16 @@
package gltronic.oozik.business;
import java.io.IOException;
import org.json.JSONObject;
import org.springframework.web.socket.WebSocketSession;
public interface IRoomManager {
public void login(WebSocketSession session, String name) throws InterruptedException, IOException;
public void leave(WebSocketSession session);
public void createRoom(WebSocketSession session) throws InterruptedException, IOException;
public void connectRoom(WebSocketSession session, String roomName) throws InterruptedException, IOException;
public void followRTC(WebSocketSession session, JSONObject jsonObject) throws InterruptedException, IOException;
public void sendMessage(WebSocketSession session, String type, String message) throws InterruptedException, IOException;
public void sendServerInfos(WebSocketSession session) throws InterruptedException, IOException;
}

View File

@@ -0,0 +1,110 @@
package gltronic.oozik.business;
import java.io.IOException;
import java.util.Random;
import org.json.JSONObject;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import gltronic.oozik.model.BiMap;
@Service
public class RoomManager implements IRoomManager{
BiMap<String, WebSocketSession> users = new BiMap<String, WebSocketSession>();
BiMap<String, String> rooms = new BiMap<String, String>();
public void login(WebSocketSession session, String name) throws InterruptedException, IOException {
if (name == null) {
sendMessage(session, "error", "bad command command");
return;
}
if (users.containsKey(name)) {
sendMessage(session, "login", "false");
return;
}
users.put(name, session);
sendMessage(session, "login", "true");
sendServerInfos(session);
System.err.println("[ROOM] Logged "+name);
}
public void leave(WebSocketSession session) {
String name = users.getKey(session);
users.removeValue(session);
if(rooms.containsValue(name)) rooms.removeValue(name);
}
public void createRoom(WebSocketSession session) throws InterruptedException, IOException {
if (!users.containsValue(session)) {
sendMessage(session, "error", "need login");
return;
}
String userName = users.getKey(session);
if (rooms.containsKey(userName)) {
sendMessage(session, "error", "no multiple room");
return;
}
Random random = new Random();
String roomName = Integer.toString(random.nextInt(9999));
while (roomName.length() < 4) roomName += 0 + roomName;
rooms.put(roomName, userName);
sendMessage(session, "createRoom", roomName);
System.err.println("[ROOM] Created room "+roomName+" by "+userName);
}
public void connectRoom(WebSocketSession session, String roomName) throws InterruptedException, IOException {
if (roomName == null) {
sendMessage(session, "error", "bad command command");
return;
}
if (!users.containsValue(session)) {
sendMessage(session, "error", "need login");
return;
}
if (!rooms.containsKey(roomName)) {
sendMessage(session, "error", "no room");
return;
}
String roomAdmin = rooms.getKey(roomName);
sendMessage(session, "coonectRoom", roomAdmin);
}
public void followRTC(WebSocketSession session, JSONObject jsonObject) throws InterruptedException, IOException {
String target = (String) jsonObject.get("target");
String type = (String) jsonObject.get("type");
String data = (String) jsonObject.get("data");
if (target == null) {
sendMessage(session, "error", "no target");
return;
}
WebSocketSession targetSession = users.get(target);
if (targetSession == null) {
sendMessage(session, "error", "unknow target");
return;
}
System.err.println("[ROOM] Foward RTC");
sendMessage(targetSession, type, data);
}
public void sendMessage(WebSocketSession session, String type, String message) throws InterruptedException, IOException {
session.sendMessage(new TextMessage("{\"type\":\""+type+"\",\"message\":\""+message+"\"}"));
}
public void sendServerInfos(WebSocketSession session) throws InterruptedException, IOException {
session.sendMessage(new TextMessage("{\"type\":\"serverInfos\",\"userCount\":\""+users.size()+"\",\"roomCount\":\""+rooms.size()+"\"}"));
}
}

View File

@@ -0,0 +1,46 @@
package gltronic.oozik.model;
import java.util.HashMap;
public class BiMap<K, V> {
HashMap<K,V> map = new HashMap<K, V>();
HashMap<V,K> inversedMap = new HashMap<V, K>();
public void put(K k, V v) {
map.put(k, v);
inversedMap.put(v, k);
}
public V get(K k) {
return map.get(k);
}
public K getKey(V v) {
return inversedMap.get(v);
}
public boolean containsKey(K k) {
return map.containsKey(k);
}
public boolean containsValue(V v) {
return map.containsValue(v);
}
public int size() {
return map.size();
}
public void removeKey(K k) {
V v = map.get(k);
map.remove(k);
inversedMap.remove(v);
}
public void removeValue(V v) {
K k = inversedMap.get(v);
inversedMap.remove(v);
map.remove(k);
}
}

View File

@@ -0,0 +1,60 @@
package gltronic.oozik.web;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import gltronic.oozik.business.IRoomManager;
@Component
public class SocketHandler extends TextWebSocketHandler {
List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();
@Autowired
IRoomManager roomManager;
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws InterruptedException, IOException {
System.err.println("SOCKET MESSAGE :" + message.getPayload());
String payload = message.getPayload();
JSONObject jsonObject = new JSONObject(payload);
String type = (String) jsonObject.get("type");
switch (type) {
case "serverInfos":
roomManager.sendServerInfos(session);
break;
case "login":
roomManager.login(session, (String) jsonObject.get("name"));
break;
case "createRoom":
roomManager.createRoom(session);
break;
case "connectRoom":
roomManager.connectRoom(session, (String) jsonObject.get("name"));
break;
case "leave":
roomManager.leave(session);
case "offer":
case "answer":
case "candidate":
roomManager.followRTC(session, jsonObject);
default:
roomManager.sendMessage(session, "error", "unknow command");
}
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.err.println("[WS] new connection");
sessions.add(session);
}
}

View File

@@ -0,0 +1,20 @@
package gltronic.oozik.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {
@Autowired
private SocketHandler socketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(socketHandler, "/socket").setAllowedOrigins("*");
}
}

View File

@@ -1 +1 @@
server.port=8181