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"
|
2020-11-09 07:38:14 +13:00
|
|
|
v-show="state.connection.websocket == 'connected' && state.connection.webrtc == 'connected'"
|
2020-11-08 03:08:39 +13:00
|
|
|
>
|
2020-11-07 08:49:05 +13:00
|
|
|
<video ref="video" />
|
|
|
|
<neko-overlay
|
|
|
|
:webrtc="webrtc"
|
2020-11-09 07:38:14 +13:00
|
|
|
:screenWidth="state.screen.size.width"
|
|
|
|
:screenHeight="state.screen.size.height"
|
|
|
|
:isControling="state.member.is_controlling"
|
|
|
|
:scrollSensitivity="state.control.scroll.sensitivity"
|
|
|
|
:scrollInvert="state.control.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 state = {
|
2020-11-09 07:38:14 +13:00
|
|
|
connection: {
|
|
|
|
websocket: 'disconnected',
|
|
|
|
webrtc: 'disconnected',
|
|
|
|
type: 'none',
|
|
|
|
can_watch: false,
|
|
|
|
can_control: false,
|
|
|
|
clipboard_access: false,
|
2020-11-07 08:49:05 +13:00
|
|
|
},
|
2020-11-09 07:38:14 +13:00
|
|
|
video: {
|
|
|
|
playable: false,
|
|
|
|
playing: false,
|
|
|
|
volume: 0,
|
2020-11-08 06:47:02 +13:00
|
|
|
},
|
2020-11-09 07:38:14 +13:00
|
|
|
control: {
|
|
|
|
scroll: {
|
|
|
|
inverse: true,
|
|
|
|
sensitivity: 10,
|
|
|
|
},
|
|
|
|
host: null,
|
|
|
|
},
|
|
|
|
screen: {
|
|
|
|
size: {
|
|
|
|
width: 1280,
|
|
|
|
height: 720,
|
|
|
|
rate: 30,
|
|
|
|
},
|
|
|
|
configurations: [],
|
|
|
|
is_fullscreen: false,
|
|
|
|
},
|
|
|
|
member: {
|
|
|
|
id: null,
|
|
|
|
name: null,
|
|
|
|
is_admin: false,
|
|
|
|
is_watching: false,
|
|
|
|
is_controlling: false,
|
|
|
|
can_watch: false,
|
|
|
|
can_control: false,
|
|
|
|
clipboard_access: false,
|
|
|
|
},
|
|
|
|
members: [],
|
2020-11-07 10:19:41 +13:00
|
|
|
} as NekoState
|
2020-11-07 08:49:05 +13:00
|
|
|
|
2020-11-09 07:38:14 +13:00
|
|
|
public events = new NekoMessages(this.websocket, this.state)
|
|
|
|
|
2020-11-08 08:12:13 +13:00
|
|
|
public get connected() {
|
2020-11-09 07:38:14 +13:00
|
|
|
return this.state.connection.websocket == 'connected' && this.state.connection.webrtc == 'connected'
|
2020-11-08 08:12:13 +13:00
|
|
|
}
|
|
|
|
|
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) => {
|
2020-11-09 07:38:14 +13:00
|
|
|
Vue.set(this.state.control.scroll, 'sensitivity', sensitivity)
|
2020-11-08 08:12:13 +13:00
|
|
|
},
|
|
|
|
inverse: (inverse: boolean) => {
|
2020-11-09 07:38:14 +13:00
|
|
|
Vue.set(this.state.control.scroll, 'inverse', inverse)
|
2020-11-08 08:12:13 +13:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
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-09 07:38:14 +13:00
|
|
|
Vue.set(this.state.member, '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) => {
|
2020-11-09 07:38:14 +13:00
|
|
|
Vue.set(this.state.member, 'is_controlling', id != null && id === this.state.member.id)
|
2020-11-08 06:47:02 +13:00
|
|
|
})
|
|
|
|
|
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-09 07:38:14 +13:00
|
|
|
Vue.set(this.state.member, 'id', payload.id)
|
2020-11-07 10:19:41 +13:00
|
|
|
|
2020-11-07 08:49:05 +13:00
|
|
|
try {
|
|
|
|
let sdp = await this.webrtc.connect(payload.sdp, payload.lite, payload.ice)
|
2020-11-09 07:38:14 +13:00
|
|
|
this.websocket.send('signal/answer', { sdp, displayname: this.state.member.name })
|
2020-11-07 08:49:05 +13:00
|
|
|
} catch (e) {}
|
|
|
|
break
|
|
|
|
case 'screen/resolution':
|
2020-11-09 07:38:14 +13:00
|
|
|
Vue.set(this.state.screen, 'size', payload)
|
2020-11-07 08:49:05 +13:00
|
|
|
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
|
|
|
|
})
|
|
|
|
|
2020-11-09 07:38:14 +13:00
|
|
|
Vue.set(this.state.screen, 'configurations', conf)
|
2020-11-08 08:12:13 +13:00
|
|
|
this.onResize()
|
|
|
|
break
|
2020-11-07 08:49:05 +13:00
|
|
|
}
|
|
|
|
})
|
|
|
|
this.websocket.on('connecting', () => {
|
2020-11-09 07:38:14 +13:00
|
|
|
Vue.set(this.state.connection, '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-09 07:38:14 +13:00
|
|
|
Vue.set(this.state.connection, '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-09 07:38:14 +13:00
|
|
|
Vue.set(this.state.connection, '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-09 07:38:14 +13:00
|
|
|
Vue.set(this.state.connection, '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-09 07:38:14 +13:00
|
|
|
Vue.set(this.state.connection, '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-09 07:38:14 +13:00
|
|
|
Vue.set(this.state.connection, '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.')
|
|
|
|
|
2020-11-09 07:38:14 +13:00
|
|
|
const { width, height } = this.state.screen.size
|
2020-11-07 08:49:05 +13:00
|
|
|
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>
|