diff --git a/README.md b/README.md index 7f6795c..571e1d6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # oozik 2 -Webapp edition \ No newline at end of file +Webapp edition + +socket messages types: serverInfos, login, leave, userList, createRoom, connectRoom, offer, answer, candidate diff --git a/client/src/App.vue b/client/src/App.vue index 3943904..c077d53 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,9 +1,5 @@ - - Home | - About - @@ -19,14 +15,5 @@ #nav { padding: 30px; - - a { - font-weight: bold; - color: #2c3e50; - - &.router-link-exact-active { - color: #42b983; - } - } } diff --git a/client/src/components/HelloWorld.vue b/client/src/components/HelloWorld.vue deleted file mode 100644 index 5673c54..0000000 --- a/client/src/components/HelloWorld.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - {{ msg }} - - For a guide and recipes on how to configure / customize this project, - check out the - vue-cli documentation. - - Installed CLI Plugins - - babel - pwa - router - vuex - eslint - - Essential Links - - Core Docs - Forum - Community Chat - Twitter - News - - Ecosystem - - vue-router - vuex - vue-devtools - vue-loader - awesome-vue - - - - - - - - diff --git a/client/src/main.js b/client/src/main.js index d12df39..6d3fe40 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -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({ diff --git a/client/src/router/index.js b/client/src/router/index.js index e44cdcd..325d91b 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -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') } ] diff --git a/client/src/rtc.js b/client/src/rtc.js new file mode 100644 index 0000000..93894fd --- /dev/null +++ b/client/src/rtc.js @@ -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 +} diff --git a/client/src/script.js b/client/src/script.js new file mode 100644 index 0000000..9b9bd4e --- /dev/null +++ b/client/src/script.js @@ -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 += '' + }) +} + +function handleError(message) { + console.log('Error '+message) + alert( + 'AWWW FUCK \n\n' + +message + +'\n\nYou should probably reload the page') +} +*/ diff --git a/client/src/signal.js b/client/src/signal.js new file mode 100644 index 0000000..35a440f --- /dev/null +++ b/client/src/signal.js @@ -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 +} diff --git a/client/src/store/appModule.js b/client/src/store/appModule.js new file mode 100644 index 0000000..7478e05 --- /dev/null +++ b/client/src/store/appModule.js @@ -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 +} diff --git a/client/src/store/index.js b/client/src/store/index.js index 332b916..c191c93 100644 --- a/client/src/store/index.js +++ b/client/src/store/index.js @@ -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() + ] }) diff --git a/client/src/store/roomModule.js b/client/src/store/roomModule.js new file mode 100644 index 0000000..83c9709 --- /dev/null +++ b/client/src/store/roomModule.js @@ -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 +} diff --git a/client/src/store/rtcModule.js b/client/src/store/rtcModule.js new file mode 100644 index 0000000..4a522a4 --- /dev/null +++ b/client/src/store/rtcModule.js @@ -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 +} diff --git a/client/src/store/signalPlugin.js b/client/src/store/signalPlugin.js new file mode 100644 index 0000000..f827bb6 --- /dev/null +++ b/client/src/store/signalPlugin.js @@ -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)) +} diff --git a/client/src/views/About.vue b/client/src/views/About.vue deleted file mode 100644 index 3fa2807..0000000 --- a/client/src/views/About.vue +++ /dev/null @@ -1,5 +0,0 @@ - - - This is an about page - - diff --git a/client/src/views/Home.vue b/client/src/views/Home.vue index 8bd6c57..de5471b 100644 --- a/client/src/views/Home.vue +++ b/client/src/views/Home.vue @@ -1,18 +1,103 @@ - - + + Server online + {{serverStatus.userCount}} users | {{serverStatus.roomCount}} rooms + + Server offline + + Join a room + + Make a room + + diff --git a/client/src/views/Room.vue b/client/src/views/Room.vue new file mode 100644 index 0000000..242cac7 --- /dev/null +++ b/client/src/views/Room.vue @@ -0,0 +1,43 @@ + + + {{roomStatus.roomName}} + {{roomStatus.roomCode}} + {{roomStatus.current.title}} + + + + + {{props.row.title}} + + + + {{props.row.link}} + + + + {{props.row.vote}} + + + + + + + + + + + + + + diff --git a/server/pom.xml b/server/pom.xml index 04ca6b1..33f18a6 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -11,26 +11,22 @@ gltronic oozik 0.0.1-SNAPSHOT - demo - Demo project for Spring Boot + oozik + oozik signaling server 11 - - org.springframework.boot - spring-boot-starter-data-jdbc - - - org.springframework.boot - spring-boot-starter-data-jpa - org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-websocket + org.projectlombok @@ -48,6 +44,12 @@ + + + org.json + json + 20200518 + diff --git a/server/src/main/java/gltronic/oozik/DemoApplication.java b/server/src/main/java/gltronic/oozik/DemoApplication.java deleted file mode 100644 index 1dd1dd7..0000000 --- a/server/src/main/java/gltronic/oozik/DemoApplication.java +++ /dev/null @@ -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); - } - -} diff --git a/server/src/main/java/gltronic/oozik/OozikApplication.java b/server/src/main/java/gltronic/oozik/OozikApplication.java new file mode 100644 index 0000000..6339a02 --- /dev/null +++ b/server/src/main/java/gltronic/oozik/OozikApplication.java @@ -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); + } + +} diff --git a/server/src/main/java/gltronic/oozik/business/IRoomManager.java b/server/src/main/java/gltronic/oozik/business/IRoomManager.java new file mode 100644 index 0000000..c0a1027 --- /dev/null +++ b/server/src/main/java/gltronic/oozik/business/IRoomManager.java @@ -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; +} \ No newline at end of file diff --git a/server/src/main/java/gltronic/oozik/business/RoomManager.java b/server/src/main/java/gltronic/oozik/business/RoomManager.java new file mode 100644 index 0000000..d6000c3 --- /dev/null +++ b/server/src/main/java/gltronic/oozik/business/RoomManager.java @@ -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 users = new BiMap(); + BiMap rooms = new BiMap(); + + 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()+"\"}")); + } + +} \ No newline at end of file diff --git a/server/src/main/java/gltronic/oozik/model/BiMap.java b/server/src/main/java/gltronic/oozik/model/BiMap.java new file mode 100644 index 0000000..f45206b --- /dev/null +++ b/server/src/main/java/gltronic/oozik/model/BiMap.java @@ -0,0 +1,46 @@ +package gltronic.oozik.model; + +import java.util.HashMap; + +public class BiMap { + HashMap map = new HashMap(); + HashMap inversedMap = new HashMap(); + + 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); + } + +} \ No newline at end of file diff --git a/server/src/main/java/gltronic/oozik/web/SocketHandler.java b/server/src/main/java/gltronic/oozik/web/SocketHandler.java new file mode 100644 index 0000000..a481581 --- /dev/null +++ b/server/src/main/java/gltronic/oozik/web/SocketHandler.java @@ -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 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); + } +} \ No newline at end of file diff --git a/server/src/main/java/gltronic/oozik/web/WebSocketConfiguration.java b/server/src/main/java/gltronic/oozik/web/WebSocketConfiguration.java new file mode 100644 index 0000000..ed40ea6 --- /dev/null +++ b/server/src/main/java/gltronic/oozik/web/WebSocketConfiguration.java @@ -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("*"); + } +} \ No newline at end of file diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 8b13789..d636638 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -1 +1 @@ - +server.port=8181
- For a guide and recipes on how to configure / customize this project, - check out the - vue-cli documentation. -