refactor state & public methods.

This commit is contained in:
Miroslav Šedivý 2020-11-10 19:57:52 +01:00
parent 6177ffc242
commit 619ac3350b
3 changed files with 209 additions and 109 deletions

View File

@ -32,20 +32,67 @@
<td>{{ neko.state.video.playable }}</td> <td>{{ neko.state.video.playable }}</td>
</tr> </tr>
<tr class="ok"> <tr class="ok">
<th>video.playing</th> <th rowspan="2">video.playing</th>
<td><input type="checkbox" v-model="neko.state.video.playing" /></td> <td>{{ neko.state.video.playing }}</td>
</tr> </tr>
<tr class="ok"> <tr class="ok">
<th>video.volume</th> <td>
<td><input type="range" min="0" max="1" v-model="neko.state.video.volume" step="0.01" /></td> <button v-if="!neko.state.video.playing" @click="neko.play()">play</button>
<button v-else @click="neko.pause()">pause</button>
</td>
</tr> </tr>
<tr class="ok"> <tr class="ok">
<th>control.scroll.inverse</th> <th rowspan="2">video.volume</th>
<td><input type="checkbox" v-model="neko.state.control.scroll.inverse" /></td> <td>{{ neko.state.video.volume }}</td>
</tr> </tr>
<tr class="ok"> <tr class="ok">
<th>control.scroll.sensitivity</th> <td>
<td><input type="number" v-model="neko.state.control.scroll.sensitivity" /></td> <input
type="range"
min="0"
max="1"
:value="neko.state.video.volume"
@input="neko.setVolume(parseInt($event))"
step="0.01"
/>
</td>
</tr>
<tr class="ok">
<th rowspan="2">video.fullscreen</th>
<td>{{ neko.state.video.fullscreen }}</td>
</tr>
<tr class="ok">
<td>
<button v-if="!neko.state.video.fullscreen" @click="neko.requestFullscreen()">request</button>
<button v-else @click="neko.exitFullscreen()">exit</button>
</td>
</tr>
<tr class="ok">
<th rowspan="2">control.scroll.inverse</th>
<td>{{ neko.state.control.scroll.inverse }}</td>
</tr>
<tr class="ok">
<td>
<button v-if="!neko.state.control.scroll.inverse" @click="neko.setScrollInverse(true)">switch to inverse</button>
<button v-else @click="neko.setScrollInverse(false)">switch to normal</button>
</td>
</tr>
<tr class="ok">
<th rowspan="2">control.scroll.sensitivity</th>
<td>{{ neko.state.control.scroll.sensitivity }}</td>
</tr>
<tr class="ok">
<td>
<input
type="number"
:value="neko.state.control.scroll.sensitivity"
@input="neko.setScrollSensitivity(parseInt($event.target.value))"
/>
</td>
</tr>
<tr>
<th>control.clipboard.data</th>
<td>{{ neko.state.control.clipboard.data }}</td>
</tr> </tr>
<tr> <tr>
<th>control.host</th> <th>control.host</th>
@ -64,13 +111,16 @@
<td>{{ neko.state.screen.size.rate }}</td> <td>{{ neko.state.screen.size.rate }}</td>
</tr> </tr>
<tr class="ok"> <tr class="ok">
<th>screen.configurations</th> <th rowspan="2">screen.configurations</th>
<td>Total {{ neko.state.screen.configurations.length }} configurations.</td>
</tr>
<tr class="ok">
<td> <td>
<select <select
:value="Object.values(neko.state.screen.size).join()" :value="Object.values(neko.state.screen.size).join()"
@input=" @input="
a = String($event.target.value).split(',') a = String($event.target.value).split(',')
neko.screen.size(parseInt(a[0]), parseInt(a[1]), parseInt(a[2])) neko.setScreenSize(parseInt(a[0]), parseInt(a[1]), parseInt(a[2]))
" "
> >
<option <option
@ -83,10 +133,6 @@
</select> </select>
</td> </td>
</tr> </tr>
<tr class="ok">
<th>screen.fullscreen</th>
<td><input type="checkbox" v-model="neko.state.screen.fullscreen" /></td>
</tr>
<tr class="ok"> <tr class="ok">
<th>member.id</th> <th>member.id</th>
<td>{{ neko.state.member.id }}</td> <td>{{ neko.state.member.id }}</td>
@ -104,9 +150,15 @@
<td>{{ neko.state.member.is_watching }}</td> <td>{{ neko.state.member.is_watching }}</td>
</tr> </tr>
<tr class="ok"> <tr class="ok">
<th>member.is_controlling</th> <th rowspan="2">member.is_controlling</th>
<td>{{ neko.state.member.is_controlling }}</td> <td>{{ neko.state.member.is_controlling }}</td>
</tr> </tr>
<tr class="ok">
<td>
<button v-if="!neko.state.member.is_controlling" @click="neko.controlRequest()">request control</button>
<button v-else @click="neko.controlRelease()">release control</button>
</td>
</tr>
<tr> <tr>
<th>member.can_watch</th> <th>member.can_watch</th>
<td>{{ neko.state.member.can_watch }}</td> <td>{{ neko.state.member.can_watch }}</td>
@ -126,15 +178,17 @@
</table> </table>
</div> </div>
<div> <div>
<div v-if="loaded && !neko.connected">
<input type="text" placeholder="URL" v-model="url" />
<input type="text" placeholder="Password" v-model="pass" />
<input type="text" placeholder="Display Name" v-model="name" />
<button @click="connect()">Connect</button> <button @click="connect()">Connect</button>
<button @click="disconnect()">Disonnect</button> </div>
<button v-if="loaded && neko.connected" @click="disconnect()">Disonnect</button>
<template v-if="loaded && neko.connected"> <template v-if="loaded && neko.connected">
<button v-if="!is_controlling" @click="neko.control.request()">request control</button> <button v-if="!is_controlling" @click="neko.controlRequest()">request control</button>
<button v-else @click="neko.control.release()">release control</button> <button v-else @click="neko.controlRelease()">release control</button>
<button v-if="neko.state.video.playing" @click="neko.state.video.playing = false">pause stream</button>
<button v-else @click="neko.state.video.playing = true">play stream</button><br />
</template> </template>
<div ref="container" style="width: 1280px; height: 720px; border: 2px solid red"> <div ref="container" style="width: 1280px; height: 720px; border: 2px solid red">
@ -182,8 +236,12 @@
return this.neko.state.member.is_controlling return this.neko.state.member.is_controlling
} }
url: string = 'ws://192.168.1.20:3000/'
pass: string = 'admin'
name: string = 'test'
connect() { connect() {
this.neko.connect('ws://192.168.1.20:3000/', 'admin', 'test') this.neko.connect(this.url, this.pass, this.name)
} }
disconnect() { disconnect() {

View File

@ -52,12 +52,6 @@
import NekoState from '~/types/state' import NekoState from '~/types/state'
import Overlay from './overlay.vue' import Overlay from './overlay.vue'
export interface NekoEvents {
connecting: () => void
connected: () => void
disconnected: (error?: Error) => void
}
@Component({ @Component({
name: 'neko-canvas', name: 'neko-canvas',
components: { components: {
@ -67,12 +61,15 @@
export default class extends Vue { export default class extends Vue {
@Ref('component') readonly _component!: HTMLElement @Ref('component') readonly _component!: HTMLElement
@Ref('container') readonly _container!: HTMLElement @Ref('container') readonly _container!: HTMLElement
@Ref('video') readonly video!: HTMLVideoElement @Ref('video') readonly _video!: HTMLVideoElement
private websocket = new NekoWebSocket() websocket = new NekoWebSocket()
private webrtc = new NekoWebRTC() webrtc = new NekoWebRTC()
private observer = new ResizeObserver(this.onResize.bind(this)) observer = new ResizeObserver(this.onResize.bind(this))
/////////////////////////////
// Public state
/////////////////////////////
public state = { public state = {
connection: { connection: {
websocket: 'disconnected', websocket: 'disconnected',
@ -86,12 +83,16 @@
playable: false, playable: false,
playing: false, playing: false,
volume: 0, volume: 0,
fullscreen: false,
}, },
control: { control: {
scroll: { scroll: {
inverse: true, inverse: true,
sensitivity: 1, sensitivity: 1,
}, },
clipboard: {
data: null,
},
host: null, host: null,
}, },
screen: { screen: {
@ -101,7 +102,6 @@
rate: 30, rate: 30,
}, },
configurations: [], configurations: [],
fullscreen: false,
}, },
member: { member: {
id: null, id: null,
@ -116,64 +116,18 @@
members: [], members: [],
} as NekoState } as NekoState
public events = new NekoMessages(this.websocket, this.state)
public get connected() { public get connected() {
return this.state.connection.websocket == 'connected' && this.state.connection.webrtc == 'connected' return this.state.connection.websocket == 'connected' && this.state.connection.webrtc == 'connected'
} }
@Watch('state.video.playing') /////////////////////////////
onVideoPlayingChanged(play: boolean) { // Public events
if (this.video.paused && play) { /////////////////////////////
this.video.play() public events = new NekoMessages(this.websocket, this.state)
}
if (!this.video.paused && !play) {
this.video.pause()
}
// TODO: check if user has tab focused and send via websocket
Vue.set(this.state.member, 'is_watching', play)
}
@Watch('state.video.volume')
onVideoVolumeChanged(value: number) {
if (value < 0 || value > 1) {
throw new Error('Out of range. Value must be between 0 and 1.')
}
this.video.volume = value
}
@Watch('state.screen.size')
onScreenSizeChanged() {
this.onResize()
}
@Watch('state.screen.fullscreen')
onScreenFullscreenChanged() {
if (document.fullscreenElement !== null) {
document.exitFullscreen()
} else {
this._component.requestFullscreen()
}
}
public control = {
request: () => {
this.websocket.send('control/request')
},
release: () => {
this.websocket.send('control/release')
},
}
public screen = {
size: (width: number, height: number, rate: number) => {
this.websocket.send('screen/set', { width, height, rate })
},
}
/////////////////////////////
// Public methods
/////////////////////////////
public connect(url: string, password: string, name: string) { public connect(url: string, password: string, name: string) {
if (this.connected) { if (this.connected) {
throw new Error('client already connected') throw new Error('client already connected')
@ -189,32 +143,91 @@
} }
this.websocket.disconnect() this.websocket.disconnect()
// TODO: reset state
Vue.set(this.state.member, 'is_controlling', false)
} }
private mounted() { public play() {
// update canvas on resize this._video.play()
}
public pause() {
this._video.pause()
}
public setVolume(value: number) {
if (value < 0 || value > 1) {
throw new Error('Out of range. Value must be between 0 and 1.')
}
this._video.volume = value
}
public requestFullscreen() {
this._component.requestFullscreen()
}
public exitFullscreen() {
document.exitFullscreen()
}
public setScrollInverse(value: boolean = true) {
Vue.set(this.state.control.scroll, 'inverse', value)
}
public setScrollSensitivity(value: number) {
Vue.set(this.state.control.scroll, 'sensitivity', value)
}
public setClipboardData(value: number) {
// TODO: Via REST API.
}
public controlRequest() {
// TODO: Via REST API.
this.websocket.send('control/request')
}
public controlRelease() {
// TODO: Via REST API.
this.websocket.send('control/release')
}
public controlTake() {
// TODO: Via REST API.
}
public controlGive(id: string) {
// TODO: Via REST API.
}
public controlReset() {
// TODO: Via REST API.
}
public setScreenSize(width: number, height: number, rate: number) {
// TODO: Via REST API.
this.websocket.send('screen/set', { width, height, rate })
}
/////////////////////////////
// Component lifecycle
/////////////////////////////
mounted() {
// component size change
this.observer.observe(this._component) this.observer.observe(this._component)
// change host // host change
this.events.on('control.host', (id: string | null) => { this.events.on('control.host', (id: string | null) => {
Vue.set(this.state.member, 'is_controlling', id != null && id === this.state.member.id) Vue.set(this.state.member, 'is_controlling', id != null && id === this.state.member.id)
}) })
// hardcoded webrtc for now // fullscreen change
Vue.set(this.state.connection, 'type', 'webrtc')
Vue.set(this.state.connection, 'can_watch', this.webrtc.supported)
Vue.set(this.state.connection, 'can_control', this.webrtc.supported)
this._component.addEventListener('fullscreenchange', () => { this._component.addEventListener('fullscreenchange', () => {
Vue.set(this.state.screen, 'fullscreen', document.fullscreenElement !== null) Vue.set(this.state.video, 'fullscreen', document.fullscreenElement !== null)
this.onResize() this.onResize()
}) })
// video // video events
VideoRegister(this.video, this.state.video) VideoRegister(this._video, this.state.video)
// websocket // websocket
this.websocket.on('message', async (event: string, payload: any) => { this.websocket.on('message', async (event: string, payload: any) => {
@ -241,6 +254,18 @@
Vue.set(this.state.connection, 'websocket', 'disconnected') Vue.set(this.state.connection, 'websocket', 'disconnected')
this.events.emit('system.websocket', 'disconnected') this.events.emit('system.websocket', 'disconnected')
this.webrtc.disconnect() this.webrtc.disconnect()
// TODO: reset state
Vue.set(this.state, 'member', {
id: null,
name: null,
is_admin: false,
is_watching: false,
is_controlling: false,
can_watch: false,
can_control: false,
clipboard_access: false,
})
}) })
// webrtc // webrtc
@ -249,14 +274,14 @@
if (track.kind === 'audio') return if (track.kind === 'audio') return
// create stream // create stream
if ('srcObject' in this.video) { if ('srcObject' in this._video) {
this.video.srcObject = streams[0] this._video.srcObject = streams[0]
} else { } else {
// @ts-ignore // @ts-ignore
this.video.src = window.URL.createObjectURL(streams[0]) // for older browsers this._video.src = window.URL.createObjectURL(streams[0]) // for older browsers
} }
this.video.play() this._video.play()
}) })
this.webrtc.on('connecting', () => { this.webrtc.on('connecting', () => {
Vue.set(this.state.connection, 'webrtc', 'connecting') Vue.set(this.state.connection, 'webrtc', 'connecting')
@ -270,17 +295,29 @@
Vue.set(this.state.connection, 'webrtc', 'disconnected') Vue.set(this.state.connection, 'webrtc', 'disconnected')
this.events.emit('system.webrtc', 'disconnected') this.events.emit('system.webrtc', 'disconnected')
// @ts-ignore // @ts-ignore
if (this.video) this.video.src = null this._video.src = null
}) })
// hardcoded webrtc for now
Vue.set(this.state.connection, 'type', 'webrtc')
Vue.set(this.state.connection, 'can_watch', this.webrtc.supported)
Vue.set(this.state.connection, 'can_control', this.webrtc.supported)
} }
private beforeDestroy() { beforeDestroy() {
this.observer.disconnect() this.observer.disconnect()
this.webrtc.disconnect() this.webrtc.disconnect()
this.websocket.disconnect() this.websocket.disconnect()
} }
private onResize() { @Watch('state.video.playing')
onVideoPlayingChanged(play: boolean) {
// TODO: check if user has tab focused and send via websocket
Vue.set(this.state.member, 'is_watching', play)
}
@Watch('state.screen.size')
onResize() {
const { width, height } = this.state.screen.size const { width, height } = this.state.screen.size
const screen_ratio = width / height const screen_ratio = width / height

View File

@ -26,6 +26,7 @@ export interface Video {
playable: boolean playable: boolean
playing: boolean playing: boolean
volume: number volume: number
fullscreen: boolean
} }
///////////////////////////// /////////////////////////////
@ -33,6 +34,7 @@ export interface Video {
///////////////////////////// /////////////////////////////
export interface Control { export interface Control {
scroll: Scroll scroll: Scroll
clipboard: Clipboard
host: Member | null host: Member | null
} }
@ -41,13 +43,16 @@ export interface Scroll {
sensitivity: number sensitivity: number
} }
export interface Clipboard {
data: string | null
}
///////////////////////////// /////////////////////////////
// Screen // Screen
///////////////////////////// /////////////////////////////
export interface Screen { export interface Screen {
size: ScreenSize size: ScreenSize
configurations: ScreenSize[] configurations: ScreenSize[]
fullscreen: boolean
} }
export interface ScreenSize { export interface ScreenSize {