neko/src/component/internal/connection.ts

251 lines
6.5 KiB
TypeScript
Raw Normal View History

2021-06-19 09:15:29 +12:00
import Vue from 'vue'
2021-06-17 19:22:02 +12:00
import EventEmitter from 'eventemitter3'
2021-06-20 05:36:59 +12:00
import { Logger } from '../utils/logger'
import * as EVENT from '../types/events'
2021-06-17 19:22:02 +12:00
import { NekoWebSocket } from './websocket'
import { NekoWebRTC, WebRTCStats } from './webrtc'
2021-06-19 09:15:29 +12:00
import { Connection } from '../types/state'
2021-07-16 09:58:45 +12:00
import { Reconnecter, ReconnecterAbstract } from '../utils/reconnecter'
2021-06-17 19:22:02 +12:00
const WEBRTC_RECONN_MAX_LOSS = 25
const WEBRTC_RECONN_FAILED_ATTEMPTS = 5
2021-06-21 09:56:34 +12:00
2021-06-17 19:22:02 +12:00
export interface NekoConnectionEvents {
2021-06-20 04:44:43 +12:00
disconnect: (error?: Error) => void
2021-06-17 19:22:02 +12:00
}
2021-07-16 09:58:45 +12:00
class WebsocketReconnecter extends ReconnecterAbstract {
private _conn: NekoConnection
private _websocket: NekoWebSocket
constructor(conn: NekoConnection, websocket: NekoWebSocket) {
super()
// TODO: Antipattern.
this._conn = conn
// TODO: Unmount.
this._websocket = websocket
this._websocket.on('connected', () => this.emit('connect'))
this._websocket.on('disconnected', (error) => this.emit('disconnect', error))
}
public async connect() {
let url = this._conn.getUrl()
const token = this._conn.getToken()
if (token) {
url += '?token=' + encodeURIComponent(token)
}
this._websocket.connect(url)
}
public async disconnect() {
this._websocket.disconnect()
}
}
class WebrtcReconnecter extends ReconnecterAbstract {
private _conn: NekoConnection
private _websocket: NekoWebSocket
private _webrtc: NekoWebRTC
constructor(conn: NekoConnection, websocket: NekoWebSocket, webrtc: NekoWebRTC) {
super()
// TODO: Antipattern.
this._conn = conn
this._websocket = websocket
// TODO: Unmount.
this._webrtc = webrtc
this._webrtc.on('connected', () => this.emit('connect'))
this._webrtc.on('disconnected', (error) => this.emit('disconnect', error))
}
public async connect() {
this._websocket.send(EVENT.SIGNAL_REQUEST, { video: this._conn.getVideo() })
}
public async disconnect() {
this._webrtc.disconnect()
}
}
2021-06-17 19:22:02 +12:00
export class NekoConnection extends EventEmitter<NekoConnectionEvents> {
2021-06-24 07:24:34 +12:00
private _url = ''
private _token = ''
2021-07-16 09:58:45 +12:00
private _video = ''
2021-06-24 07:24:34 +12:00
private _log = new Logger('connection')
2021-06-19 09:15:29 +12:00
private _state: Connection
2021-06-17 19:22:02 +12:00
2021-06-18 10:31:03 +12:00
public websocket = new NekoWebSocket()
2021-07-16 09:58:45 +12:00
public _websocket_reconn = new Reconnecter(new WebsocketReconnecter(this, this.websocket), {
max_reconnects: 15,
timeout_ms: 5000,
backoff_ms: 1500,
})
2021-06-18 10:31:03 +12:00
public webrtc = new NekoWebRTC()
2021-07-16 09:58:45 +12:00
public _webrtc_reconn = new Reconnecter(new WebrtcReconnecter(this, this.websocket, this.webrtc), {
max_reconnects: 15,
timeout_ms: 10000,
backoff_ms: 1500,
})
2021-06-17 19:22:02 +12:00
2021-06-19 09:15:29 +12:00
constructor(state: Connection) {
2021-06-17 19:22:02 +12:00
super()
2021-06-19 09:15:29 +12:00
this._state = state
// initial state
2021-07-15 01:49:28 +12:00
Vue.set(this._state, 'type', 'screencast')
2021-06-20 04:36:48 +12:00
2021-06-19 09:15:29 +12:00
// websocket
2021-07-16 09:58:45 +12:00
this._websocket_reconn.on('connect', () => {
2021-06-20 05:36:59 +12:00
if (this.websocket.connected && this.webrtc.connected) {
2021-06-20 04:36:48 +12:00
Vue.set(this._state, 'status', 'connected')
}
2021-07-16 09:58:45 +12:00
if (!this._webrtc_reconn.isConnected) {
this._webrtc_reconn.connect()
}
2021-06-19 09:15:29 +12:00
})
2021-07-16 09:58:45 +12:00
this._websocket_reconn.on('disconnect', () => {
if (this._state.status === 'connected') {
2021-07-16 09:58:45 +12:00
Vue.set(this._state, 'status', 'connecting')
2021-06-20 04:36:48 +12:00
}
2021-07-16 09:58:45 +12:00
})
this._websocket_reconn.on('close', (error) => {
this.emit('disconnect', error)
2021-06-19 09:15:29 +12:00
})
// webrtc
2021-07-16 09:58:45 +12:00
this._webrtc_reconn.on('connect', () => {
2021-06-20 05:36:59 +12:00
if (this.websocket.connected && this.webrtc.connected) {
2021-06-20 04:36:48 +12:00
Vue.set(this._state, 'status', 'connected')
}
2021-07-15 01:49:28 +12:00
Vue.set(this._state, 'type', 'webrtc')
2021-06-19 09:15:29 +12:00
})
2021-07-16 09:58:45 +12:00
this._webrtc_reconn.on('disconnect', () => {
if (this._state.status === 'connected') {
2021-07-16 09:58:45 +12:00
Vue.set(this._state, 'status', 'connecting')
2021-06-20 04:36:48 +12:00
}
2021-06-20 05:36:59 +12:00
2021-07-15 01:49:28 +12:00
Vue.set(this._state, 'type', 'screencast')
2021-07-16 09:58:45 +12:00
})
this._webrtc_reconn.on('close', (error) => {
this.emit('disconnect', error)
2021-06-19 09:15:29 +12:00
})
let webrtcCongestion: number = 0
this.webrtc.on('stats', (stats: WebRTCStats) => {
Vue.set(this._state.webrtc, 'stats', stats)
// if automatic quality adjusting is turned off
2021-07-16 09:58:45 +12:00
if (!this._state.webrtc.auto || !this._webrtc_reconn.isOpen) return
// if there are no or just one quality, no switching can be done
if (this._state.webrtc.videos.length <= 1) return
// current quality is not known
if (this._state.webrtc.video == null) return
2021-06-25 01:25:56 +12:00
// check if video is not playing smoothly
if (stats.fps && stats.packetLoss < WEBRTC_RECONN_MAX_LOSS && !stats.muted) {
webrtcCongestion = 0
return
}
// try to downgrade quality if it happend many times
if (++webrtcCongestion >= WEBRTC_RECONN_FAILED_ATTEMPTS) {
webrtcCongestion = 0
2021-06-25 01:15:47 +12:00
const quality = this._webrtcQualityDowngrade(this._state.webrtc.video)
// downgrade if lower video quality exists
if (quality && this.webrtc.connected) {
this.setVideo(quality)
}
// try to perform ice restart, if available
if (this.webrtc.open) {
this.websocket.send(EVENT.SIGNAL_RESTART)
return
}
// try to reconnect
2021-07-16 09:58:45 +12:00
this._webrtc_reconn.reconnect()
}
})
2021-06-18 10:31:03 +12:00
}
2021-06-17 19:22:02 +12:00
2021-07-16 09:58:45 +12:00
public get activated() {
return this._websocket_reconn.isOpen && this._webrtc_reconn.isOpen
}
2021-06-18 10:31:03 +12:00
public setUrl(url: string) {
this._url = url.replace(/^http/, 'ws').replace(/\/+$/, '') + '/api/ws'
}
2021-06-17 19:22:02 +12:00
2021-07-12 08:41:10 +12:00
public getUrl(): string {
return this._url
}
2021-06-18 10:31:03 +12:00
public setToken(token: string) {
this._token = token
2021-06-17 19:22:02 +12:00
}
2021-07-12 08:41:10 +12:00
public getToken(): string {
return this._token
}
public setVideo(video: string) {
if (!this._state.webrtc.videos.includes(video)) {
throw new Error('video id not found')
}
2021-07-16 09:58:45 +12:00
if (this.websocket.connected) {
this.websocket.send(EVENT.SIGNAL_VIDEO, { video })
2021-06-20 05:36:59 +12:00
}
2021-07-16 09:58:45 +12:00
this._video = video
2021-06-20 05:36:59 +12:00
}
2021-07-16 09:58:45 +12:00
public getVideo() {
return this._video
2021-06-20 05:36:59 +12:00
}
2021-07-16 09:58:45 +12:00
public async connect(video?: string): Promise<void> {
if (video) this._video = video
2021-06-20 05:36:59 +12:00
2021-07-16 09:58:45 +12:00
this._webrtc_reconn.open(true)
this._websocket_reconn.open()
2021-06-20 05:36:59 +12:00
}
2021-07-16 09:58:45 +12:00
public disconnect() {
this._websocket_reconn.close()
this._webrtc_reconn.close()
2021-06-20 05:36:59 +12:00
2021-07-16 09:58:45 +12:00
Vue.set(this._state, 'status', 'disconnected')
this.emit('disconnect')
2021-06-20 05:36:59 +12:00
}
2021-06-24 09:37:39 +12:00
2021-06-25 01:15:47 +12:00
_webrtcQualityDowngrade(quality: string): string | undefined {
2021-06-24 09:37:39 +12:00
// get index of selected or surrent quality
2021-06-25 01:15:47 +12:00
const index = this._state.webrtc.videos.indexOf(quality)
2021-06-24 09:37:39 +12:00
// edge case: current quality is not in qualities list
if (index === -1) return
// current quality is the lowest one
if (index + 1 == this._state.webrtc.videos.length) return
// downgrade video quality
return this._state.webrtc.videos[index + 1]
}
2021-06-17 19:22:02 +12:00
}