neko/src/components/canvas.vue

282 lines
8.2 KiB
Vue
Raw Normal View History

2020-11-07 08:49:05 +13:00
<template>
<div ref="component" class="component">
2020-11-08 03:08:39 +13:00
<div
ref="container"
class="player-container"
v-show="state.websocket == 'connected' && state.webrtc == 'connected'"
>
2020-11-07 08:49:05 +13:00
<video ref="video" />
<neko-overlay
:webrtc="webrtc"
:screenWidth="state.screen_size.width"
:screenHeight="state.screen_size.height"
2020-11-07 10:19:41 +13:00
:isControling="state.is_controlling"
2020-11-08 06:47:02 +13:00
:scrollSensitivity="state.scroll.sensitivity"
:scrollInvert="state.scroll.invert"
2020-11-07 08:49:05 +13:00
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.component {
width: 100%;
height: 100%;
}
.player-container {
position: relative;
video {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
display: flex;
background: #000;
&::-webkit-media-controls {
display: none !important;
}
}
}
</style>
<script lang="ts">
import { Vue, Component, Ref, Watch, Prop } from 'vue-property-decorator'
import ResizeObserver from 'resize-observer-polyfill'
2020-11-08 01:26:07 +13:00
import EventEmitter from 'eventemitter3'
2020-11-07 08:49:05 +13:00
import { NekoWebSocket } from '~/internal/websocket'
import { NekoWebRTC } from '~/internal/webrtc'
2020-11-07 11:27:07 +13:00
import { NekoMessages } from '~/internal/messages'
2020-11-07 08:49:05 +13:00
2020-11-07 10:19:41 +13:00
import NekoState from '~/types/state'
2020-11-07 08:49:05 +13:00
import Overlay from './overlay.vue'
2020-11-08 01:26:07 +13:00
export interface NekoEvents {
connecting: () => void
connected: () => void
disconnected: (error?: Error) => void
}
2020-11-07 08:49:05 +13:00
@Component({
name: 'neko-canvas',
components: {
'neko-overlay': Overlay,
},
})
export default class extends Vue {
@Ref('component') readonly _component!: HTMLElement
@Ref('container') readonly _container!: HTMLElement
@Ref('video') public readonly video!: HTMLVideoElement
private websocket = new NekoWebSocket()
private webrtc = new NekoWebRTC()
2020-11-08 06:47:02 +13:00
private observer = new ResizeObserver(this.onResize.bind(this))
2020-11-07 08:49:05 +13:00
2020-11-08 08:12:13 +13:00
public events = new NekoMessages(this.websocket)
public state = {
2020-11-07 10:19:41 +13:00
id: null,
display_name: null,
2020-11-07 08:49:05 +13:00
screen_size: {
2020-11-07 10:19:41 +13:00
width: 1280,
height: 720,
rate: 30,
2020-11-07 08:49:05 +13:00
},
2020-11-08 08:12:13 +13:00
available_screen_sizes: [],
2020-11-08 06:47:02 +13:00
scroll: {
sensitivity: 10,
invert: true,
},
2020-11-07 10:19:41 +13:00
is_controlling: false,
2020-11-08 01:26:07 +13:00
websocket: 'disconnected',
webrtc: 'disconnected',
2020-11-07 10:19:41 +13:00
} as NekoState
2020-11-07 08:49:05 +13:00
2020-11-08 08:12:13 +13:00
public get connected() {
return this.state.websocket == 'connected' && this.state.webrtc == 'connected'
}
2020-11-07 08:49:05 +13:00
public control = {
request: () => {
this.websocket.send('control/request')
},
release: () => {
this.websocket.send('control/release')
},
}
2020-11-08 06:47:02 +13:00
public screen = {
size: (width: number, height: number, rate: number) => {
this.websocket.send('screen/set', { width, height, rate })
},
}
2020-11-08 08:12:13 +13:00
public scroll = {
sensitivity: (sensitivity: number) => {
Vue.set(this.state.scroll, 'sensitivity', sensitivity)
},
inverse: (inverse: boolean) => {
Vue.set(this.state.scroll, 'inverse', inverse)
},
}
public connect(url: string, password: string, name: string) {
2020-11-07 08:49:05 +13:00
if (this.websocket.connected) {
throw new Error('client already connected')
}
2020-11-08 08:12:13 +13:00
Vue.set(this.state, 'display_name', name)
2020-11-07 08:49:05 +13:00
this.websocket.connect(url, password)
}
public disconnect() {
if (!this.websocket.connected) {
throw new Error('client not connected')
}
this.websocket.disconnect()
}
private mounted() {
// Update canvas on resize
this.observer.observe(this._component)
2020-11-08 06:47:02 +13:00
this.events.on('control.host', (id: string | null) => {
Vue.set(this.state, 'is_controlling', id != null && id === this.state.id)
})
2020-11-07 08:49:05 +13:00
// WebSocket
this.websocket.on('message', async (event: string, payload: any) => {
switch (event) {
case 'signal/provide':
2020-11-07 10:19:41 +13:00
Vue.set(this.state, 'id', payload.id)
2020-11-07 08:49:05 +13:00
try {
let sdp = await this.webrtc.connect(payload.sdp, payload.lite, payload.ice)
2020-11-07 10:19:41 +13:00
this.websocket.send('signal/answer', { sdp, displayname: this.state.display_name })
2020-11-07 08:49:05 +13:00
} catch (e) {}
break
case 'screen/resolution':
Vue.set(this.state, 'screen_size', payload)
this.onResize()
break
2020-11-08 08:12:13 +13:00
case 'screen/configurations':
let data = []
for (const i of Object.keys(payload.configurations)) {
const { width, height, rates } = payload.configurations[i]
if (width >= 600 && height >= 300) {
for (const j of Object.keys(rates)) {
const rate = rates[j]
if (rate === 30 || rate === 60) {
data.push({
width,
height,
rate,
})
}
}
}
}
let conf = data.sort((a, b) => {
if (b.width === a.width && b.height == a.height) {
return b.rate - a.rate
} else if (b.width === a.width) {
return b.height - a.height
}
return b.width - a.width
})
Vue.set(this.state, 'available_screen_sizes', conf)
this.onResize()
break
2020-11-07 08:49:05 +13:00
}
})
this.websocket.on('connecting', () => {
2020-11-08 01:26:07 +13:00
Vue.set(this.state, 'websocket', 'connecting')
2020-11-08 06:47:02 +13:00
this.events.emit('system.websocket', 'connecting')
2020-11-07 08:49:05 +13:00
})
this.websocket.on('connected', () => {
2020-11-08 01:26:07 +13:00
Vue.set(this.state, 'websocket', 'connected')
2020-11-08 06:47:02 +13:00
this.events.emit('system.websocket', 'connected')
2020-11-07 08:49:05 +13:00
})
this.websocket.on('disconnected', () => {
2020-11-08 01:26:07 +13:00
Vue.set(this.state, 'websocket', 'disconnected')
2020-11-08 06:47:02 +13:00
this.events.emit('system.websocket', 'disconnected')
2020-11-07 08:49:05 +13:00
this.webrtc.disconnect()
})
// WebRTC
this.webrtc.on('track', (event: RTCTrackEvent) => {
const { track, streams } = event
2020-11-08 06:47:02 +13:00
if (track.kind === 'audio') return
2020-11-07 08:49:05 +13:00
// Create stream
if ('srcObject' in this.video) {
this.video.srcObject = streams[0]
} else {
// @ts-ignore
this.video.src = window.URL.createObjectURL(streams[0]) // for older browsers
}
this.video.play()
})
this.webrtc.on('connecting', () => {
2020-11-08 01:26:07 +13:00
Vue.set(this.state, 'webrtc', 'connecting')
2020-11-08 06:47:02 +13:00
this.events.emit('system.webrtc', 'connecting')
2020-11-07 08:49:05 +13:00
})
this.webrtc.on('connected', () => {
2020-11-08 01:26:07 +13:00
Vue.set(this.state, 'webrtc', 'connected')
2020-11-08 06:47:02 +13:00
this.events.emit('system.webrtc', 'connected')
2020-11-07 08:49:05 +13:00
})
this.webrtc.on('disconnected', () => {
2020-11-08 01:26:07 +13:00
Vue.set(this.state, 'webrtc', 'disconnected')
2020-11-08 06:47:02 +13:00
this.events.emit('system.webrtc', 'disconnected')
2020-11-07 08:49:05 +13:00
})
}
private beforeDestroy() {
2020-11-08 08:12:13 +13:00
this.observer.disconnect()
2020-11-07 08:49:05 +13:00
this.webrtc.disconnect()
this.websocket.disconnect()
}
private onResize() {
console.log('Resize event triggered.')
const { width, height } = this.state.screen_size
const screen_ratio = width / height
const { offsetWidth, offsetHeight } = this._component
const canvas_ratio = offsetWidth / offsetHeight
// Vertical centering
2020-11-07 10:19:41 +13:00
if (screen_ratio > canvas_ratio) {
2020-11-07 08:49:05 +13:00
const vertical = offsetWidth / screen_ratio
this._container.style.width = `${offsetWidth}px`
this._container.style.height = `${vertical}px`
this._container.style.marginTop = `${(offsetHeight - vertical) / 2}px`
this._container.style.marginLeft = `0px`
}
// Horizontal centering
2020-11-07 10:19:41 +13:00
else if (screen_ratio < canvas_ratio) {
2020-11-07 08:49:05 +13:00
const horizontal = screen_ratio * offsetHeight
this._container.style.width = `${horizontal}px`
this._container.style.height = `${offsetHeight}px`
this._container.style.marginTop = `0px`
this._container.style.marginLeft = `${(offsetWidth - horizontal) / 2}px`
}
// No centering
else {
this._container.style.width = `${offsetWidth}px`
this._container.style.height = `${offsetHeight}px`
this._container.style.marginTop = `0px`
this._container.style.marginLeft = `0px`
}
}
}
</script>