Working datachannel, youtube support & server fix

This commit is contained in:
Thomas
2020-07-29 19:03:11 +02:00
parent 927fa2d1a2
commit e8247a1ba3
9 changed files with 295 additions and 16 deletions

View File

@@ -5727,6 +5727,11 @@
"integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
"dev": true "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": { "getpass": {
"version": "0.1.7", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "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": { "loader-fs-cache": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.3.tgz", "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": { "slash": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
@@ -11314,6 +11329,15 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true "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": { "vuex": {
"version": "3.5.1", "version": "3.5.1",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.5.1.tgz", "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.5.1.tgz",
@@ -12328,6 +12352,31 @@
"dev": true "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="
}
}
} }
} }
} }

View File

@@ -13,6 +13,7 @@
"register-service-worker": "^1.7.1", "register-service-worker": "^1.7.1",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-router": "^3.2.0", "vue-router": "^3.2.0",
"vue-youtube": "^1.4.0",
"vuex": "^3.4.0" "vuex": "^3.4.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -0,0 +1,13 @@
<template>
<div id="player"></div>
</template>
<script>
export default {
}
</script>
<style>
</style>

View File

@@ -4,11 +4,13 @@ import './registerServiceWorker'
import router from './router' import router from './router'
import store from './store' import store from './store'
import Buefy from 'buefy' import Buefy from 'buefy'
import VueYoutube from 'vue-youtube'
import 'buefy/dist/buefy.css' import 'buefy/dist/buefy.css'
// import './assets/style.scss' // import './assets/style.scss'
Vue.use(Buefy) Vue.use(Buefy)
Vue.use(VueYoutube)
Vue.config.productionTip = false Vue.config.productionTip = false
new Vue({ new Vue({

View File

@@ -3,7 +3,16 @@ const state = {
roomStatus: { roomStatus: {
roomName: '', roomName: '',
roomCode: '', roomCode: '',
current: '', player: {
timeCode: 0,
playing: true
},
current: {
link: '',
title: '',
votes: 0,
voters: []
},
playlist: [] playlist: []
} }
} }
@@ -23,6 +32,35 @@ const actions = {
}, },
setAdmin ({ commit }) { setAdmin ({ commit }) {
commit('SET_ADMIN') 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) { SET_ADMIN (state) {
state.admin = true 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
} }
} }

View File

@@ -39,8 +39,8 @@ const actions = {
leave ({ commit }) { leave ({ commit }) {
commit('LEAVE') commit('LEAVE')
}, },
broadcast ({ commit }, message) { broadcast ({ commit }, { message, type }) {
commit('BROADCAST', message) commit('BROADCAST', { message: message, type: type })
} }
} }
@@ -79,6 +79,7 @@ const mutations = {
var peer = state.peers.find(peer => peer.name === target) var peer = state.peers.find(peer => peer.name === target)
peer.dataChannel = peer.connection.createDataChannel('dataChannel') peer.dataChannel = peer.connection.createDataChannel('dataChannel')
peer.dataChannel.onmessage = handleDataChannelMessage
peer.dataChannel.onopen = handleDataChannelStateChangeEvent peer.dataChannel.onopen = handleDataChannelStateChangeEvent
peer.dataChannel.onclose = handleDataChannelStateChangeEvent peer.dataChannel.onclose = handleDataChannelStateChangeEvent
@@ -127,9 +128,14 @@ const mutations = {
}) })
state.peers = {} 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 => { 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.onopen = handleDataChannelStateChangeEvent
peer.dataChannel.onclose = 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) { function handleDataChannelMessage (event) {
console.log('[RTC] data channel message ' + event.data) 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) { switch (data.type) {
case 'status': case 'status':
store.dispatch('room/setRoomStatus', data.message) store.dispatch('room/setRoomStatus', data.message)
break break
case 'vote': case 'vote':
store.dispatch('room/vote', { link: data.message.link, isPositive: data.message.isPositive, voterName: data.message.voterName })
break break
} }
} }

View File

@@ -4,7 +4,19 @@
<h2 class="subtitle">{{roomStatus.current.title}}</h2> <h2 class="subtitle">{{roomStatus.current.title}}</h2>
<b-tabs> <b-tabs>
<b-tab-item label="Player" :visible="settings.showPlayer">
<div class="playerDiv">
<youtube
:video-id="roomStatus.current.link"
nocookie="true"
ref="youtube"
@playing="roomStatus.player.playing"
></youtube>
</div>
</b-tab-item>
<b-tab-item label="Playlist"> <b-tab-item label="Playlist">
<b-button icon-left="plus" @click="addLinkPrompt">Add link</b-button>
<b-table :data="roomStatus.playlist" striped hoverable default-sort="vote"> <b-table :data="roomStatus.playlist" striped hoverable default-sort="vote">
<template slot-scope="props"> <template slot-scope="props">
<b-table-column field="title" label="Title"> <b-table-column field="title" label="Title">
@@ -16,24 +28,37 @@
</b-table-column> </b-table-column>
<b-table-column field="vote" label="Votes"> <b-table-column field="vote" label="Votes">
{{props.row.vote}} {{props.row.votes}}
</b-table-column>
<b-table-column field="voters" label="Voters" :visible="isAdmin">
{{props.row.voters}}
</b-table-column> </b-table-column>
<b-table-column> <b-table-column>
<b-button icon-left="arrow-up-bold-outline" type="is-dark"/> <b-button v-if="hasVoted(props.row)" icon-left="arrow-down-bold-outline" type="is-dark" @click="vote (props.row.link, false)"/>
<b-button icon-left="arrow-down-bold-outline" type="is-dark"/> <b-button v-else icon-left="arrow-up-bold-outline" type="is-dark" @click="vote (props.row.link, true)"/>
</b-table-column> </b-table-column>
</template> </template>
</b-table> </b-table>
</b-tab-item> </b-tab-item>
<b-tab-item label="Admin" :visible="showAdmin"> <b-tab-item label="Admin" :visible="isAdmin">
<b-button @click="broadcastStatus">Force status update</b-button>
<b-table :data="usersList" striped hoverable> <b-table :data="usersList" striped hoverable>
<template slot-scope="props"> <template slot-scope="props">
<b-table-column field="name" label="Name"> <b-table-column field="name" label="Name">
{{props.row.name}} {{props.row.name}}
</b-table-column> </b-table-column>
<b-table-column field="connection" label="Connection">
{{props.row.connection.signalingState}}
</b-table-column>
<b-table-column field="data" label="DataChannel">
{{props.row.dataChannel.readyState}}
</b-table-column>
<b-table-column> <b-table-column>
<b-button icon-left="karate" type="is-dark"/> <b-button icon-left="karate" type="is-dark"/>
</b-table-column> </b-table-column>
@@ -45,7 +70,17 @@
<h1 class="subtitle">{{roomStatus.roomCode}}</h1> <h1 class="subtitle">{{roomStatus.roomCode}}</h1>
</b-tab-item> </b-tab-item>
<b-tab-item label="Settings">
<div class="field">
<b-switch v-model="settings.playLink">Play link</b-switch>
</div>
<div class="field">
<b-switch v-model="settings.showPlayer" :disabled="!settings.playLink">Show player</b-switch>
</div>
</b-tab-item>
</b-tabs> </b-tabs>
<b-loading is-full-page :active.sync="isRoomLoading"/>
</div> </div>
</template> </template>
@@ -53,10 +88,13 @@
export default { export default {
name: 'Room', name: 'Room',
computed: { computed: {
player () {
return this.$refs.youtube.player
},
roomStatus () { roomStatus () {
return this.$store.state.room.roomStatus return this.$store.state.room.roomStatus
}, },
showAdmin () { isAdmin () {
return this.$store.state.room.admin return this.$store.state.room.admin
}, },
usersList () { usersList () {
@@ -65,14 +103,86 @@ export default {
}, },
isLoggedIn () { isLoggedIn () {
return this.$store.state.app.loginSuccess return this.$store.state.app.loginSuccess
},
isRoomLoading () {
return this.roomStatus.roomName === ''
}
},
data () {
return {
settings: {
playLink: true,
showPlayer: true
}
} }
}, },
mounted () { mounted () {
if (!this.isLoggedIn) this.$router.push({ name: 'Home' }) 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() })
}
} }
} }
</script> </script>
<style> <style>
.room {
margin-top: 20px;
}
.playerDiv {
height: 500px;
}
</style> </style>

View File

@@ -98,7 +98,7 @@ public class RoomManager implements IRoomManager{
return; return;
} }
System.err.println("[ROOM] Foward RTC"); System.err.println("[ROOM] Foward RTC message");
followMessage(targetSession, message); followMessage(targetSession, message);
} }

View File

@@ -7,6 +7,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import org.json.JSONObject; import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler; import org.springframework.web.socket.handler.TextWebSocketHandler;
@@ -22,7 +23,7 @@ public class SocketHandler extends TextWebSocketHandler {
@Override @Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws InterruptedException, IOException { 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(); String payload = message.getPayload();
JSONObject jsonObject = new JSONObject(payload); JSONObject jsonObject = new JSONObject(payload);
@@ -59,4 +60,11 @@ public class SocketHandler extends TextWebSocketHandler {
System.err.println("[WS] new connection"); System.err.println("[WS] new connection");
sessions.add(session); sessions.add(session);
} }
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus){
System.err.println("[WS] connection closed");
sessions.remove(session);
roomManager.leave(session);
}
} }