neko/src/component/internal/connection.ts

220 lines
6.3 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'
import * as EVENT from '../types/events'
2021-06-17 19:22:02 +12:00
import { NekoWebSocket } from './websocket'
2021-07-18 01:17:56 +12:00
import { NekoWebRTC } from './webrtc'
import { Connection, WebRTCStats } from '../types/state'
2021-07-27 09:29:41 +12:00
import { Reconnector } from './reconnector'
import { WebsocketReconnector } from './reconnector/websocket'
import { WebrtcReconnector } from './reconnector/webrtc'
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-07-28 10:21:27 +12:00
const WEBRTC_FALLBACK_TIMEOUT_MS = 750
2021-06-17 19:22:02 +12:00
export interface NekoConnectionEvents {
2021-07-27 09:59:41 +12:00
close: (error?: Error) => void
2021-06-17 19:22:02 +12:00
}
export class NekoConnection extends EventEmitter<NekoConnectionEvents> {
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()
public webrtc = new NekoWebRTC()
2021-07-27 09:59:41 +12:00
private _reconnector: {
websocket: Reconnector
webrtc: Reconnector
}
2021-06-17 19:22:02 +12:00
2021-07-28 10:31:41 +12:00
private _onConnectHandle: () => void
private _onDisconnectHandle: () => void
private _onCloseHandle: (error?: Error) => void
private _webrtcCongestionControlHandle: (stats: WebRTCStats) => void
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
2021-07-27 09:59:41 +12:00
this._reconnector = {
websocket: new Reconnector(new WebsocketReconnector(state, this.websocket), state.websocket.config),
webrtc: new Reconnector(new WebrtcReconnector(state, this.websocket, this.webrtc), state.webrtc.config),
}
2021-06-19 09:15:29 +12:00
2021-07-28 10:31:41 +12:00
this._onConnectHandle = () => {
if (this._state.status !== 'connected' && 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
2021-07-28 10:31:41 +12:00
if (this._state.type !== 'webrtc' && this.webrtc.connected) {
Vue.set(this._state, 'type', 'webrtc')
}
if (this.websocket.connected && !this.webrtc.connected) {
2021-07-27 09:59:41 +12:00
this._reconnector.webrtc.connect()
2021-07-16 09:58:45 +12:00
}
2021-07-28 10:31:41 +12:00
}
this._onDisconnectHandle = () => {
2021-07-18 01:45:41 +12:00
if (this._state.status === 'connected' && this.activated) {
2021-07-16 09:58:45 +12:00
Vue.set(this._state, 'status', 'connecting')
2021-06-20 04:36:48 +12:00
}
2021-06-19 09:15:29 +12:00
2021-07-28 10:31:41 +12:00
if (this._state.type !== 'fallback' && !this.webrtc.connected) {
Vue.set(this._state, 'type', 'fallback')
2021-06-20 04:36:48 +12:00
}
2021-07-28 10:31:41 +12:00
}
2021-07-15 01:49:28 +12:00
2021-07-28 10:31:41 +12:00
this._onCloseHandle = this.close.bind(this)
2021-06-20 05:36:59 +12:00
2021-07-28 10:31:41 +12:00
// bind events to all reconnecters
Object.values(this._reconnector).forEach((r) => {
r.on('connect', this._onConnectHandle)
r.on('disconnect', this._onDisconnectHandle)
r.on('close', this._onCloseHandle)
2021-07-16 09:58:45 +12:00
})
2021-07-28 10:31:41 +12:00
//
// TODO: Use server side congestion control.
//
let webrtcCongestion: number = 0
2021-07-28 10:21:27 +12:00
let webrtcFallbackTimeout: number
2021-07-28 10:31:41 +12:00
this._webrtcCongestionControlHandle = (stats: WebRTCStats) => {
Vue.set(this._state.webrtc, 'stats', stats)
// if automatic quality adjusting is turned off
2021-07-27 09:59:41 +12:00
if (!this._state.webrtc.auto || !this._reconnector.webrtc.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) {
2021-07-28 10:21:27 +12:00
if (webrtcFallbackTimeout) {
window.clearTimeout(webrtcFallbackTimeout)
}
2021-07-18 02:36:56 +12:00
if (this._state.type === 'fallback') {
Vue.set(this._state, 'type', 'webrtc')
}
webrtcCongestion = 0
return
}
// try to downgrade quality if it happend many times
if (++webrtcCongestion >= WEBRTC_RECONN_FAILED_ATTEMPTS) {
2021-07-28 10:21:27 +12:00
webrtcFallbackTimeout = window.setTimeout(() => {
if (this._state.type === 'webrtc') {
Vue.set(this._state, 'type', 'fallback')
}
}, WEBRTC_FALLBACK_TIMEOUT_MS)
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
}
2021-07-27 09:59:41 +12:00
// try to reconnect webrtc
this._reconnector.webrtc.reconnect()
}
2021-07-28 10:31:41 +12:00
}
this.webrtc.on('stats', this._webrtcCongestionControlHandle)
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() {
2021-07-27 09:59:41 +12:00
// check if every reconnecter is open
return Object.values(this._reconnector).every((r) => r.isOpen)
2021-07-16 09:58:45 +12:00
}
public setVideo(video: string) {
if (!this._state.webrtc.videos.includes(video)) {
throw new Error('video id not found')
}
2021-07-18 01:17:56 +12:00
this.websocket.send(EVENT.SIGNAL_VIDEO, { video })
2021-06-20 05:36:59 +12:00
}
2021-07-27 09:59:41 +12:00
public open(video?: string) {
2021-07-18 01:17:56 +12:00
if (video) {
if (!this._state.webrtc.videos.includes(video)) {
throw new Error('video id not found')
}
Vue.set(this._state.webrtc, 'video', video)
}
2021-06-20 05:36:59 +12:00
2021-07-18 02:36:56 +12:00
Vue.set(this._state, 'type', 'fallback')
2021-07-18 02:08:12 +12:00
Vue.set(this._state, 'status', 'connecting')
2021-07-18 02:36:56 +12:00
2021-07-27 09:59:41 +12:00
// open all reconnecters
Object.values(this._reconnector).forEach((r) => r.open(true))
this._reconnector.websocket.connect()
2021-06-20 05:36:59 +12:00
}
2021-07-27 09:59:41 +12:00
public close(error?: Error) {
if (this.activated) {
Vue.set(this._state, 'type', 'none')
Vue.set(this._state, 'status', 'disconnected')
2021-06-20 05:36:59 +12:00
2021-07-27 09:59:41 +12:00
this.emit('close', error)
}
2021-07-18 02:36:56 +12:00
2021-07-27 09:59:41 +12:00
// close all reconnecters
Object.values(this._reconnector).forEach((r) => r.close())
2021-06-20 05:36:59 +12:00
}
2021-06-24 09:37:39 +12:00
2021-07-27 08:15:34 +12:00
public destroy() {
2021-07-28 10:31:41 +12:00
// TODO: Use server side congestion control.
this.webrtc.off('stats', this._webrtcCongestionControlHandle)
// unbind events from all reconnecters
Object.values(this._reconnector).forEach((r) => {
r.off('connect', this._onConnectHandle)
r.off('disconnect', this._onDisconnectHandle)
r.off('close', this._onCloseHandle)
})
2021-07-27 09:59:41 +12:00
// destroy all reconnecters
Object.values(this._reconnector).forEach((r) => r.destroy())
2021-07-27 09:35:19 +12:00
Vue.set(this._state, 'type', 'none')
Vue.set(this._state, 'status', 'disconnected')
2021-07-27 08:15:34 +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
}