neko/src/component/main.vue

392 lines
11 KiB
Vue
Raw Normal View History

2020-11-07 08:49:05 +13:00
<template>
<div ref="component" class="component">
2020-11-09 10:44:37 +13:00
<div ref="container" class="player-container">
2020-11-07 08:49:05 +13:00
<video ref="video" />
<neko-overlay
:webrtc="webrtc"
2021-01-10 11:28:56 +13:00
:control="state.control"
2020-11-09 07:38:14 +13:00
:screenWidth="state.screen.size.width"
:screenHeight="state.screen.size.height"
2020-12-08 06:46:29 +13:00
:isControling="controlling && watching"
2020-12-07 08:08:25 +13:00
:implicitControl="state.control.implicit_hosting && state.members[state.member_id].profile.can_host"
2020-12-02 22:45:23 +13:00
@implicit-control-request="websocket.send('control/request')"
@implicit-control-release="websocket.send('control/release')"
2021-01-09 08:51:38 +13:00
@drop-files="uploadDrop($event)"
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
2020-12-27 11:21:49 +13:00
import { NekoApi, MembersApi, RoomApi } from './internal/api'
2020-11-29 09:47:16 +13:00
import { NekoWebSocket } from './internal/websocket'
import { NekoWebRTC } from './internal/webrtc'
import { NekoMessages } from './internal/messages'
import { register as VideoRegister } from './internal/video'
2020-11-07 08:49:05 +13:00
2020-11-29 09:47:16 +13:00
import NekoState from './types/state'
2020-11-07 08:49:05 +13:00
import Overlay from './overlay.vue'
@Component({
name: 'neko-canvas',
components: {
'neko-overlay': Overlay,
},
})
export default class extends Vue {
@Ref('component') readonly _component!: HTMLElement
@Ref('container') readonly _container!: HTMLElement
2020-11-11 07:57:52 +13:00
@Ref('video') readonly _video!: HTMLVideoElement
2020-11-07 08:49:05 +13:00
2020-11-30 03:34:52 +13:00
api = new NekoApi()
2020-11-11 07:57:52 +13:00
websocket = new NekoWebSocket()
webrtc = new NekoWebRTC()
observer = new ResizeObserver(this.onResize.bind(this))
2020-11-07 08:49:05 +13:00
2020-11-11 07:57:52 +13:00
/////////////////////////////
// Public state
/////////////////////////////
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-30 00:06:01 +13:00
muted: false,
2020-11-11 07:57:52 +13:00
fullscreen: false,
2020-11-08 06:47:02 +13:00
},
2020-11-09 07:38:14 +13:00
control: {
scroll: {
inverse: true,
2020-11-09 09:19:46 +13:00
sensitivity: 1,
2020-11-09 07:38:14 +13:00
},
2021-01-10 11:28:56 +13:00
cursor: null,
clipboard: null,
host_id: null,
2020-12-02 23:24:27 +13:00
implicit_hosting: false,
2020-11-09 07:38:14 +13:00
},
screen: {
size: {
width: 1280,
height: 720,
rate: 30,
},
configurations: [],
},
member_id: null,
2020-12-02 10:58:05 +13:00
members: {},
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() {
2020-12-08 06:46:29 +13:00
return this.state.connection.websocket == 'connected'
}
public get watching() {
return this.state.connection.webrtc == 'connected'
2020-11-08 08:12:13 +13:00
}
2020-11-11 09:46:22 +13:00
public get controlling() {
return this.state.control.host_id !== null && this.state.member_id === this.state.control.host_id
2020-11-11 09:46:22 +13:00
}
2020-12-07 08:30:34 +13:00
public get is_admin() {
return this.state.member_id != null ? this.state.members[this.state.member_id].profile.is_admin : false
}
2020-11-11 07:57:52 +13:00
/////////////////////////////
// Public events
/////////////////////////////
public events = new NekoMessages(this.websocket, this.state)
/////////////////////////////
// Public methods
/////////////////////////////
2020-11-29 03:28:11 +13:00
public connect(url: string, id: string, secret: string) {
2020-11-11 07:57:52 +13:00
if (this.connected) {
throw new Error('client already connected')
2020-11-09 10:02:00 +13:00
}
2020-11-30 03:34:52 +13:00
const wsURL = url.replace(/^http/, 'ws').replace(/\/$|\/ws\/?$/, '')
this.websocket.connect(wsURL, id, secret)
const httpURL = url.replace(/^ws/, 'http').replace(/\/$|\/ws\/?$/, '')
this.api.connect(httpURL, id, secret)
2020-11-11 07:57:52 +13:00
}
public disconnect() {
if (!this.connected) {
throw new Error('client not connected')
2020-11-09 10:02:00 +13:00
}
2020-11-09 10:48:04 +13:00
2020-11-11 07:57:52 +13:00
this.websocket.disconnect()
2020-11-30 03:34:52 +13:00
this.api.disconnect()
2020-11-11 07:57:52 +13:00
}
2020-12-08 06:46:29 +13:00
public webrtcConnect() {
if (!this.connected) {
throw new Error('client not connected to websocket')
}
if (this.watching) {
throw new Error('client already connected to webrtc')
}
this.websocket.send('signal/request')
}
public webrtcDisconnect() {
if (!this.connected) {
throw new Error('client not connected to websocket')
}
if (!this.watching) {
throw new Error('client not connected to webrtc')
}
this.webrtc.disconnect()
}
2020-11-11 07:57:52 +13:00
public play() {
this._video.play()
2020-11-09 10:02:00 +13:00
}
2020-11-11 07:57:52 +13:00
public pause() {
this._video.pause()
}
2020-11-30 00:06:01 +13:00
public mute() {
this._video.muted = true
}
public unmute() {
this._video.muted = false
}
2020-11-11 07:57:52 +13:00
public setVolume(value: number) {
2020-11-09 10:02:00 +13:00
if (value < 0 || value > 1) {
throw new Error('Out of range. Value must be between 0 and 1.')
}
2020-11-11 07:57:52 +13:00
this._video.volume = value
2020-11-09 10:02:00 +13:00
}
2020-11-11 07:57:52 +13:00
public requestFullscreen() {
this._component.requestFullscreen()
2020-11-09 09:19:46 +13:00
}
2020-11-11 07:57:52 +13:00
public exitFullscreen() {
document.exitFullscreen()
2020-11-09 10:16:47 +13:00
}
2020-11-11 07:57:52 +13:00
public setScrollInverse(value: boolean = true) {
Vue.set(this.state.control.scroll, 'inverse', value)
2020-11-07 08:49:05 +13:00
}
2020-11-11 07:57:52 +13:00
public setScrollSensitivity(value: number) {
Vue.set(this.state.control.scroll, 'sensitivity', value)
2020-11-08 06:47:02 +13:00
}
2020-11-11 07:57:52 +13:00
public setScreenSize(width: number, height: number, rate: number) {
2020-12-27 11:59:29 +13:00
//this.api.room.screenConfigurationChange({ screenConfiguration: { width, height, rate } })
this.websocket.send('screen/set', { width, height, rate })
2020-11-07 08:49:05 +13:00
}
2020-12-27 11:21:49 +13:00
public get room(): RoomApi {
return this.api.room
2020-12-03 01:52:02 +13:00
}
2020-12-27 11:21:49 +13:00
public get members(): MembersApi {
return this.api.members
2020-12-03 01:52:02 +13:00
}
2021-01-09 09:36:59 +13:00
async uploadDrop({ x, y, files }: { x: number; y: number; files: Array<Blob> }) {
try {
this.events.emit('upload.drop.started')
await this.api.room.uploadDrop(x, y, files, {
onUploadProgress: (progressEvent: ProgressEvent) => {
this.events.emit('upload.drop.progress', progressEvent)
},
})
this.events.emit('upload.drop.finished', null)
} catch (err) {
this.events.emit('upload.drop.finished', err)
}
2021-01-09 08:51:38 +13:00
}
2020-11-11 07:57:52 +13:00
/////////////////////////////
// Component lifecycle
/////////////////////////////
mounted() {
// component size change
2020-11-07 08:49:05 +13:00
this.observer.observe(this._component)
2020-11-11 07:57:52 +13:00
// fullscreen change
2020-11-09 10:16:47 +13:00
this._component.addEventListener('fullscreenchange', () => {
2020-11-11 07:57:52 +13:00
Vue.set(this.state.video, 'fullscreen', document.fullscreenElement !== null)
2020-11-09 10:16:47 +13:00
this.onResize()
})
2020-11-11 07:57:52 +13:00
// video events
VideoRegister(this._video, this.state.video)
2020-11-09 09:19:46 +13:00
2020-11-09 10:44:37 +13:00
// websocket
2020-11-07 08:49:05 +13:00
this.websocket.on('message', async (event: string, payload: any) => {
switch (event) {
case 'signal/provide':
try {
let sdp = await this.webrtc.connect(payload.sdp, payload.lite, payload.ice)
2020-11-29 03:28:11 +13:00
this.websocket.send('signal/answer', { sdp })
2020-11-07 08:49:05 +13:00
} catch (e) {}
break
}
})
this.websocket.on('connecting', () => {
2020-11-09 07:38:14 +13:00
Vue.set(this.state.connection, 'websocket', 'connecting')
this.events.emit('internal.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')
this.events.emit('internal.websocket', 'connected')
2020-12-08 06:46:29 +13:00
this.webrtcConnect()
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')
this.events.emit('internal.websocket', 'disconnected')
2020-11-11 07:57:52 +13:00
2020-12-04 08:16:24 +13:00
this.webrtc.disconnect()
this.clearState()
2020-11-07 08:49:05 +13:00
})
2020-11-09 10:44:37 +13:00
// webrtc
2020-11-07 08:49:05 +13:00
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
2020-11-09 10:44:37 +13:00
// create stream
2020-11-11 07:57:52 +13:00
if ('srcObject' in this._video) {
this._video.srcObject = streams[0]
2020-11-07 08:49:05 +13:00
} else {
// @ts-ignore
2020-11-11 07:57:52 +13:00
this._video.src = window.URL.createObjectURL(streams[0]) // for older browsers
2020-11-07 08:49:05 +13:00
}
2020-11-11 07:57:52 +13:00
this._video.play()
2020-11-07 08:49:05 +13:00
})
this.webrtc.on('connecting', () => {
2020-11-09 07:38:14 +13:00
Vue.set(this.state.connection, 'webrtc', 'connecting')
this.events.emit('internal.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')
this.events.emit('internal.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')
this.events.emit('internal.webrtc', 'disconnected')
2020-12-08 06:44:14 +13:00
if (!this._video) return
2020-12-08 06:44:14 +13:00
// destroy stream
if ('srcObject' in this._video) {
this._video.srcObject = null
} else {
// @ts-ignore
this._video.removeAttribute('src')
}
2020-11-07 08:49:05 +13:00
})
2020-11-11 07:57:52 +13:00
// 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)
2020-11-07 08:49:05 +13:00
}
2020-11-11 07:57:52 +13:00
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()
}
2020-11-11 07:57:52 +13:00
@Watch('state.screen.size')
onResize() {
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
2020-11-09 10:44:37 +13:00
// 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`
}
2020-11-09 10:44:37 +13:00
// 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`
}
2020-11-09 10:44:37 +13:00
// no centering
2020-11-07 08:49:05 +13:00
else {
this._container.style.width = `${offsetWidth}px`
this._container.style.height = `${offsetHeight}px`
this._container.style.marginTop = `0px`
this._container.style.marginLeft = `0px`
}
}
2020-12-04 08:16:24 +13:00
clearState() {
Vue.set(this.state.control, 'clipboard', null)
Vue.set(this.state.control, 'host_id', null)
Vue.set(this.state.control, 'implicit_hosting', false)
Vue.set(this.state.screen, 'size', { width: 1280, height: 720, rate: 30 })
Vue.set(this.state.screen, 'configurations', [])
Vue.set(this.state, 'member_id', null)
Vue.set(this.state, 'members', {})
}
2020-11-07 08:49:05 +13:00
}
</script>