From e8247a1ba3465cf942fd0607820bc2ddc79c6dd5 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 29 Jul 2020 19:03:11 +0200 Subject: [PATCH] Working datachannel, youtube support & server fix --- client/package-lock.json | 49 +++++++ client/package.json | 1 + client/src/components/Player.vue | 13 ++ client/src/main.js | 4 +- client/src/store/roomModule.js | 92 +++++++++++++- client/src/store/rtcModule.js | 20 ++- client/src/views/Room.vue | 120 +++++++++++++++++- .../gltronic/oozik/business/RoomManager.java | 2 +- .../gltronic/oozik/web/SocketHandler.java | 10 +- 9 files changed, 295 insertions(+), 16 deletions(-) create mode 100644 client/src/components/Player.vue diff --git a/client/package-lock.json b/client/package-lock.json index 78eba7d..01ac000 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -5727,6 +5727,11 @@ "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", "dev": true }, + "get-youtube-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-youtube-id/-/get-youtube-id-1.0.1.tgz", + "integrity": "sha512-5yidLzoLXbtw82a/Wb7LrajkGn29BM6JuLWeHyNfzOGp1weGyW4+7eMz6cP23+etqj27VlOFtq8fFFDMLq/FXQ==" + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -7058,6 +7063,11 @@ } } }, + "load-script": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", + "integrity": "sha1-BJGTngvuVkPuSUp+PaPSuscMbKQ=" + }, "loader-fs-cache": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.3.tgz", @@ -9923,6 +9933,11 @@ } } }, + "sister": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz", + "integrity": "sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA==" + }, "slash": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", @@ -11314,6 +11329,15 @@ "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", "dev": true }, + "vue-youtube": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/vue-youtube/-/vue-youtube-1.4.0.tgz", + "integrity": "sha512-PCyfGAouSt6rTX0GLUzpdX2XC52zYf7a9mUhdp53jeDlPoU40hpsvyV3Zg2+947pvbv27ORcmtzm2fqO08kh9Q==", + "requires": { + "get-youtube-id": "^1.0.0", + "youtube-player": "^5.4.0" + } + }, "vuex": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.5.1.tgz", @@ -12328,6 +12352,31 @@ "dev": true } } + }, + "youtube-player": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz", + "integrity": "sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==", + "requires": { + "debug": "^2.6.6", + "load-script": "^1.0.0", + "sister": "^3.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } } } } diff --git a/client/package.json b/client/package.json index f02698d..9f9157e 100644 --- a/client/package.json +++ b/client/package.json @@ -13,6 +13,7 @@ "register-service-worker": "^1.7.1", "vue": "^2.6.11", "vue-router": "^3.2.0", + "vue-youtube": "^1.4.0", "vuex": "^3.4.0" }, "devDependencies": { diff --git a/client/src/components/Player.vue b/client/src/components/Player.vue new file mode 100644 index 0000000..78715fe --- /dev/null +++ b/client/src/components/Player.vue @@ -0,0 +1,13 @@ + + + + + diff --git a/client/src/main.js b/client/src/main.js index 6d3fe40..6e1a96a 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -4,11 +4,13 @@ import './registerServiceWorker' import router from './router' import store from './store' import Buefy from 'buefy' +import VueYoutube from 'vue-youtube' import 'buefy/dist/buefy.css' - // import './assets/style.scss' + Vue.use(Buefy) +Vue.use(VueYoutube) Vue.config.productionTip = false new Vue({ diff --git a/client/src/store/roomModule.js b/client/src/store/roomModule.js index 7a0f27c..e2714f0 100644 --- a/client/src/store/roomModule.js +++ b/client/src/store/roomModule.js @@ -3,7 +3,16 @@ const state = { roomStatus: { roomName: '', roomCode: '', - current: '', + player: { + timeCode: 0, + playing: true + }, + current: { + link: '', + title: '', + votes: 0, + voters: [] + }, playlist: [] } } @@ -23,6 +32,35 @@ const actions = { }, setAdmin ({ commit }) { commit('SET_ADMIN') + }, + vote ({ commit, dispatch, state }, { link, isPositive, voterName }) { + console.log('vote on ' + link + ' (' + isPositive + ') by ' + voterName) + if (isPositive) { + commit('ADD_VOTE', { + link: link, + voterName: voterName + }) + } else { + commit('REMOVE_VOTE', { + link: link, + voterName: voterName + }) + } + dispatch('rtc/broadcast', { message: state.roomStatus, type: 'status' }, { root: true }) + }, + setCurrent ({ commit, dispatch }, { playerStatus, timeCode }) { + switch (playerStatus) { + case 0: + commit('CURRENT_END') + break + case 1: + commit('CURRENT_PLAY', timeCode) + break + case 2: + commit('CURRENT_PAUSE', timeCode) + break + } + dispatch('rtc/broadcast', { message: state.roomStatus, type: 'status' }, { root: true }) } } @@ -39,8 +77,58 @@ const mutations = { SET_ADMIN (state) { state.admin = true }, - BROADCAST_ROOMSTATUS (state) { + ADD_VOTE (state, { link, voterName }) { + var play = state.roomStatus.playlist.find(play => play.link === link) + if (play === undefined) { + play = { + link: link, + votes: 1, + voters: [ + voterName + ] + } + if (state.roomStatus.current.votes === 0) state.roomStatus.current = play + else state.roomStatus.playlist.push(play) + } else { + play.votes++ + play.voters.push(voterName) + } + }, + REMOVE_VOTE (state, { link, voterName }) { + var play = state.roomStatus.playlist.find(play => play.link === link) + play.votes-- + const index = play.voters.indexOf(voterName) + if (index > -1) { + play.voters.splice(index, 1) + } + if (play.vote === 0) { + const index = state.roomStatus.playlist.indexOf(play) + if (index > -1) { + state.roomStatus.playlist.splice(index, 1) + } + } + }, + CURRENT_END (state) { + if (state.roomStatus.playlist.length === 0) { + state.roomStatus.current.link = '' + state.roomStatus.current.title = '' + state.roomStatus.current.votes = 0 + state.roomStatus.current.voters = [] + } else { + state.roomStatus.playlist.sort((a, b) => { + return b.votes - a.votes + }) + state.roomStatus.current = state.roomStatus.playlist.shift() + } + }, + CURRENT_PAUSE (state, timeCode) { + state.roomStatus.player.playing = false + state.roomStatus.player.timeCode = timeCode + }, + CURRENT_PLAY (state, timeCode) { + state.roomStatus.player.playing = true + state.roomStatus.player.timeCode = timeCode } } diff --git a/client/src/store/rtcModule.js b/client/src/store/rtcModule.js index d08dadf..3e96b26 100644 --- a/client/src/store/rtcModule.js +++ b/client/src/store/rtcModule.js @@ -39,8 +39,8 @@ const actions = { leave ({ commit }) { commit('LEAVE') }, - broadcast ({ commit }, message) { - commit('BROADCAST', message) + broadcast ({ commit }, { message, type }) { + commit('BROADCAST', { message: message, type: type }) } } @@ -79,6 +79,7 @@ const mutations = { var peer = state.peers.find(peer => peer.name === target) peer.dataChannel = peer.connection.createDataChannel('dataChannel') + peer.dataChannel.onmessage = handleDataChannelMessage peer.dataChannel.onopen = handleDataChannelStateChangeEvent peer.dataChannel.onclose = handleDataChannelStateChangeEvent @@ -127,9 +128,14 @@ const mutations = { }) state.peers = {} }, - BROADCAST (state, message) { + BROADCAST (state, { message, type }) { + const data = JSON.stringify({ + type: type, + message: message + }) + console.log('[RTC] broadcast message ' + data) state.peers.forEach(peer => { - peer.dataChannel.send(message) + peer.dataChannel.send(data) }) } } @@ -167,17 +173,19 @@ function handleDataChannelCallback (event) { peer.dataChannel.onopen = handleDataChannelStateChangeEvent peer.dataChannel.onclose = handleDataChannelStateChangeEvent - store.dispatch('rtc/broadcast', store.state.room.roomStatus) + store.dispatch('rtc/broadcast', { message: store.state.room.roomStatus, type: 'status' }) } function handleDataChannelMessage (event) { console.log('[RTC] data channel message ' + event.data) - var data = event.data + var data = JSON.parse(event.data) + console.log('[RTC] data channel message type ' + data.type) switch (data.type) { case 'status': store.dispatch('room/setRoomStatus', data.message) break case 'vote': + store.dispatch('room/vote', { link: data.message.link, isPositive: data.message.isPositive, voterName: data.message.voterName }) break } } diff --git a/client/src/views/Room.vue b/client/src/views/Room.vue index 7b5a712..bc43fd6 100644 --- a/client/src/views/Room.vue +++ b/client/src/views/Room.vue @@ -4,7 +4,19 @@

{{roomStatus.current.title}}

+ +
+ +
+
+ + Add link - + + Force status update @@ -53,10 +88,13 @@ export default { name: 'Room', computed: { + player () { + return this.$refs.youtube.player + }, roomStatus () { return this.$store.state.room.roomStatus }, - showAdmin () { + isAdmin () { return this.$store.state.room.admin }, usersList () { @@ -65,14 +103,86 @@ export default { }, isLoggedIn () { return this.$store.state.app.loginSuccess + }, + isRoomLoading () { + return this.roomStatus.roomName === '' + } + }, + data () { + return { + settings: { + playLink: true, + showPlayer: true + } } }, mounted () { if (!this.isLoggedIn) this.$router.push({ name: 'Home' }) + this.player.addEventListener('onStateChange', this.playerStateChange) + this.player.playVideo() + }, + watch: { + roomStatus: function (status) { + this.player.seekTo(status.player.timeCode, true) + if (status.player.playing) this.player.playVideo() + else this.player.pauseVideo() + } + }, + methods: { + broadcastStatus () { + this.$store.dispatch('rtc/broadcast', { message: this.$store.state.room.roomStatus, type: 'status' }) + }, + addLinkPrompt () { + this.$buefy.dialog.prompt({ + message: 'Add a youtube link', + trapFocus: true, + inputAttrs: { + placeholder: 'https://www.youtube.com/watch?v=YItIK09bpKk', + minlength: 10 + }, + cancelText: 'Nah', + confirmText: 'Add', + onConfirm: (link) => this.addLink(link) + }) + }, + addLink (link) { + const linkID = this.$youtube.getIdFromUrl(link) + if (linkID === null) { + this.$buefy.toast.open('Invalid youtube link') + return + } + if (this.isAdmin) { + this.$store.dispatch('room/vote', { link: linkID, isPositive: true, voterName: this.$store.state.rtc.name }) + } else { + this.vote(linkID, true) + } + }, + vote (link, isPositive) { + const message = { + type: 'vote', + link: link, + isPositive: isPositive, + voterName: this.$store.state.rtc.name + } + this.$store.dispatch('rtc/broadcast', { message: message, type: 'vote' }) + }, + hasVoted (play) { + return play.voters.includes(this.$store.state.rtc.name) + }, + async playerStateChange (event) { + console.log('[PLAYER] Status change ' + event.data) + if (this.isAdmin) this.$store.dispatch('room/setCurrent', { playerStatus: event.data, timeCode: await this.$refs.youtube.player.getCurrentTime() }) + } } } diff --git a/server/src/main/java/gltronic/oozik/business/RoomManager.java b/server/src/main/java/gltronic/oozik/business/RoomManager.java index 4ff6bce..4ed0a44 100644 --- a/server/src/main/java/gltronic/oozik/business/RoomManager.java +++ b/server/src/main/java/gltronic/oozik/business/RoomManager.java @@ -98,7 +98,7 @@ public class RoomManager implements IRoomManager{ return; } - System.err.println("[ROOM] Foward RTC"); + System.err.println("[ROOM] Foward RTC message"); followMessage(targetSession, message); } diff --git a/server/src/main/java/gltronic/oozik/web/SocketHandler.java b/server/src/main/java/gltronic/oozik/web/SocketHandler.java index 306f95f..7fc5c6b 100644 --- a/server/src/main/java/gltronic/oozik/web/SocketHandler.java +++ b/server/src/main/java/gltronic/oozik/web/SocketHandler.java @@ -7,6 +7,7 @@ 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.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; @@ -22,7 +23,7 @@ public class SocketHandler extends TextWebSocketHandler { @Override public void handleTextMessage(WebSocketSession session, TextMessage message) throws InterruptedException, IOException { - System.err.println("SOCKET MESSAGE :" + message.getPayload()); + System.err.println("[WS] message :" + message.getPayload()); String payload = message.getPayload(); JSONObject jsonObject = new JSONObject(payload); @@ -59,4 +60,11 @@ public class SocketHandler extends TextWebSocketHandler { System.err.println("[WS] new connection"); sessions.add(session); } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus){ + System.err.println("[WS] connection closed"); + sessions.remove(session); + roomManager.leave(session); + } } \ No newline at end of file