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 04:51:14 +12:00
|
|
|
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:23:09 +12:00
|
|
|
import { Reconnector, ReconnectorAbstract } from '../utils/reconnector'
|
2021-06-17 19:22:02 +12:00
|
|
|
|
2021-06-25 03:09:13 +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-27 09:23:09 +12:00
|
|
|
class WebsocketReconnector extends ReconnectorAbstract {
|
2021-07-18 01:17:56 +12:00
|
|
|
private _state: Connection
|
2021-07-16 09:58:45 +12:00
|
|
|
private _websocket: NekoWebSocket
|
|
|
|
|
2021-07-27 08:15:34 +12:00
|
|
|
private _onConnectHandle: () => void
|
|
|
|
private _onDisconnectHandle: (error?: Error) => void
|
|
|
|
|
2021-07-18 01:17:56 +12:00
|
|
|
constructor(state: Connection, websocket: NekoWebSocket) {
|
2021-07-16 09:58:45 +12:00
|
|
|
super()
|
|
|
|
|
2021-07-18 01:17:56 +12:00
|
|
|
this._state = state
|
2021-07-16 09:58:45 +12:00
|
|
|
this._websocket = websocket
|
2021-07-27 08:15:34 +12:00
|
|
|
|
|
|
|
this._onConnectHandle = () => this.emit('connect')
|
|
|
|
this._websocket.on('connected', this._onConnectHandle)
|
|
|
|
|
|
|
|
this._onDisconnectHandle = (error?: Error) => this.emit('disconnect', error)
|
|
|
|
this._websocket.on('disconnected', this._onDisconnectHandle)
|
2021-07-16 09:58:45 +12:00
|
|
|
}
|
|
|
|
|
2021-07-18 01:27:20 +12:00
|
|
|
public get connected() {
|
|
|
|
return this._websocket.connected
|
|
|
|
}
|
|
|
|
|
2021-07-18 01:45:41 +12:00
|
|
|
public connect() {
|
2021-07-18 01:17:56 +12:00
|
|
|
let url = this._state.url
|
|
|
|
url = url.replace(/^http/, 'ws').replace(/\/+$/, '') + '/api/ws'
|
|
|
|
|
|
|
|
const token = this._state.token
|
2021-07-16 09:58:45 +12:00
|
|
|
if (token) {
|
|
|
|
url += '?token=' + encodeURIComponent(token)
|
|
|
|
}
|
|
|
|
|
|
|
|
this._websocket.connect(url)
|
|
|
|
}
|
|
|
|
|
2021-07-18 01:45:41 +12:00
|
|
|
public disconnect() {
|
2021-07-16 09:58:45 +12:00
|
|
|
this._websocket.disconnect()
|
|
|
|
}
|
2021-07-27 08:15:34 +12:00
|
|
|
|
|
|
|
public destroy() {
|
|
|
|
this._websocket.off('connected', this._onConnectHandle)
|
|
|
|
this._websocket.off('disconnected', this._onDisconnectHandle)
|
|
|
|
}
|
2021-07-16 09:58:45 +12:00
|
|
|
}
|
|
|
|
|
2021-07-27 09:23:09 +12:00
|
|
|
class WebrtcReconnector extends ReconnectorAbstract {
|
2021-07-18 01:17:56 +12:00
|
|
|
private _state: Connection
|
2021-07-16 09:58:45 +12:00
|
|
|
private _websocket: NekoWebSocket
|
|
|
|
private _webrtc: NekoWebRTC
|
|
|
|
|
2021-07-27 08:15:34 +12:00
|
|
|
private _onConnectHandle: () => void
|
|
|
|
private _onDisconnectHandle: (error?: Error) => void
|
|
|
|
|
2021-07-18 01:17:56 +12:00
|
|
|
constructor(state: Connection, websocket: NekoWebSocket, webrtc: NekoWebRTC) {
|
2021-07-16 09:58:45 +12:00
|
|
|
super()
|
|
|
|
|
2021-07-18 01:17:56 +12:00
|
|
|
this._state = state
|
2021-07-16 09:58:45 +12:00
|
|
|
this._websocket = websocket
|
|
|
|
this._webrtc = webrtc
|
2021-07-27 08:15:34 +12:00
|
|
|
|
|
|
|
this._onConnectHandle = () => this.emit('connect')
|
|
|
|
this._webrtc.on('connected', this._onConnectHandle)
|
|
|
|
|
|
|
|
this._onDisconnectHandle = (error?: Error) => this.emit('disconnect', error)
|
|
|
|
this._webrtc.on('disconnected', this._onDisconnectHandle)
|
2021-07-16 09:58:45 +12:00
|
|
|
}
|
|
|
|
|
2021-07-18 01:27:20 +12:00
|
|
|
public get connected() {
|
|
|
|
return this._webrtc.connected
|
|
|
|
}
|
|
|
|
|
2021-07-18 01:45:41 +12:00
|
|
|
public connect() {
|
2021-07-18 01:30:37 +12:00
|
|
|
if (this._websocket.connected) {
|
|
|
|
this._websocket.send(EVENT.SIGNAL_REQUEST, { video: this._state.webrtc.video })
|
|
|
|
}
|
2021-07-16 09:58:45 +12:00
|
|
|
}
|
|
|
|
|
2021-07-18 01:45:41 +12:00
|
|
|
public disconnect() {
|
2021-07-16 09:58:45 +12:00
|
|
|
this._webrtc.disconnect()
|
|
|
|
}
|
2021-07-27 08:15:34 +12:00
|
|
|
|
|
|
|
public destroy() {
|
|
|
|
this._webrtc.off('connected', this._onConnectHandle)
|
|
|
|
this._webrtc.off('disconnected', this._onDisconnectHandle)
|
|
|
|
}
|
2021-07-16 09:58:45 +12:00
|
|
|
}
|
|
|
|
|
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()
|
2021-07-27 09:23:09 +12:00
|
|
|
public _websocket_reconn: Reconnector
|
2021-07-16 09:58:45 +12:00
|
|
|
|
2021-06-18 10:31:03 +12:00
|
|
|
public webrtc = new NekoWebRTC()
|
2021-07-27 09:23:09 +12:00
|
|
|
public _webrtc_reconn: Reconnector
|
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
|
2021-07-27 09:23:09 +12:00
|
|
|
this._websocket_reconn = new Reconnector(new WebsocketReconnector(state, this.websocket), state.websocket.config)
|
|
|
|
this._webrtc_reconn = new Reconnector(
|
|
|
|
new WebrtcReconnector(state, this.websocket, this.webrtc),
|
2021-07-18 01:17:56 +12:00
|
|
|
state.webrtc.config,
|
|
|
|
)
|
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
|
|
|
|
2021-07-18 01:45:41 +12:00
|
|
|
if (!this.webrtc.connected) {
|
2021-07-16 09:58:45 +12:00
|
|
|
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', () => {
|
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-07-16 09:58:45 +12:00
|
|
|
})
|
|
|
|
this._websocket_reconn.on('close', (error) => {
|
2021-07-18 02:08:12 +12:00
|
|
|
this.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', () => {
|
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-20 05:36:59 +12:00
|
|
|
|
2021-07-18 02:36:56 +12:00
|
|
|
Vue.set(this._state, 'type', 'fallback')
|
2021-07-16 09:58:45 +12:00
|
|
|
})
|
|
|
|
this._webrtc_reconn.on('close', (error) => {
|
2021-07-18 02:08:12 +12:00
|
|
|
this.disconnect(error)
|
2021-06-19 09:15:29 +12:00
|
|
|
})
|
2021-06-20 04:51:14 +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
|
2021-06-20 04:51:14 +12:00
|
|
|
|
|
|
|
// 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
|
2021-06-25 03:09:13 +12:00
|
|
|
if (stats.fps && stats.packetLoss < WEBRTC_RECONN_MAX_LOSS && !stats.muted) {
|
2021-07-18 02:36:56 +12:00
|
|
|
if (this._state.type === 'fallback') {
|
|
|
|
Vue.set(this._state, 'type', 'webrtc')
|
|
|
|
}
|
|
|
|
|
2021-06-20 04:51:14 +12:00
|
|
|
webrtcCongestion = 0
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-07-18 02:36:56 +12:00
|
|
|
if (this._state.type === 'webrtc') {
|
|
|
|
Vue.set(this._state, 'type', 'fallback')
|
|
|
|
}
|
|
|
|
|
2021-06-20 04:51:14 +12:00
|
|
|
// try to downgrade quality if it happend many times
|
2021-06-25 03:09:13 +12:00
|
|
|
if (++webrtcCongestion >= WEBRTC_RECONN_FAILED_ATTEMPTS) {
|
2021-06-28 08:07:42 +12:00
|
|
|
webrtcCongestion = 0
|
|
|
|
|
2021-06-25 01:15:47 +12:00
|
|
|
const quality = this._webrtcQualityDowngrade(this._state.webrtc.video)
|
2021-06-25 03:09:13 +12:00
|
|
|
|
|
|
|
// downgrade if lower video quality exists
|
|
|
|
if (quality && this.webrtc.connected) {
|
|
|
|
this.setVideo(quality)
|
2021-06-28 08:07:42 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
// try to perform ice restart, if available
|
|
|
|
if (this.webrtc.open) {
|
|
|
|
this.websocket.send(EVENT.SIGNAL_RESTART)
|
2021-06-25 03:09:13 +12:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// try to reconnect
|
2021-07-16 09:58:45 +12:00
|
|
|
this._webrtc_reconn.reconnect()
|
2021-06-20 04:51:14 +12:00
|
|
|
}
|
|
|
|
})
|
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-20 04:51:14 +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-18 01:45:41 +12:00
|
|
|
public connect(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-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-18 02:08:12 +12:00
|
|
|
public disconnect(error?: Error) {
|
2021-07-16 09:58:45 +12:00
|
|
|
this._websocket_reconn.close()
|
|
|
|
this._webrtc_reconn.close()
|
2021-06-20 05:36:59 +12:00
|
|
|
|
2021-07-18 02:36:56 +12:00
|
|
|
Vue.set(this._state, 'type', 'none')
|
2021-07-16 09:58:45 +12:00
|
|
|
Vue.set(this._state, 'status', 'disconnected')
|
2021-07-18 02:36:56 +12:00
|
|
|
|
2021-07-18 02:08:12 +12:00
|
|
|
this.emit('disconnect', error)
|
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() {
|
|
|
|
this._websocket_reconn.destroy()
|
|
|
|
this._webrtc_reconn.destroy()
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|