UNPKG

mudb

Version:

Real-time database for multiplayer games

343 lines (309 loc) 12.1 kB
import { MuScheduler } from '../../scheduler/scheduler'; import { MuSystemScheduler } from '../../scheduler/system'; import { MuLogger, MuDefaultLogger } from '../../logger'; import { MuSocketServer, MuSocketServerState, MuSocketServerSpec, MuSocket, MuSocketState, MuSocketSpec, MuConnectionHandler, MuCloseHandler, MuSessionId, MuData } from '../socket'; import { MuRTCBinding, MuRTCConfiguration, browserRTC, MuRTCOfferAnswerOptions } from './rtc'; import { makeError } from '../../util/error'; const error = makeError('socket/webrtc/server'); const isBrowser = typeof self !== undefined && !!self && self['Object'] === Object; function noop () { } export class MuRTCSocketClient implements MuSocket { public readonly sessionId:MuSessionId; private _state = MuSocketState.INIT; public state () { return this._state; } private _pc:RTCPeerConnection; private _answerOpts:RTCOfferAnswerOptions; private _signal:(data:object) => void = noop; private _onMessage:(data:MuData, unreliable:boolean) => void = noop; private _onClose:() => void = noop; private _serverClose:() => void = noop; private _reliableChannel:RTCDataChannel|null = null; private _unreliableChannel:RTCDataChannel|null = null; private _logger:MuLogger; constructor ( sessionId:MuSessionId, pc:RTCPeerConnection, answerOpts:RTCOfferAnswerOptions, signal:(data:object, session:MuSessionId) => void, connection:(conn:MuRTCSocketClient) => void, serverClose:() => void, logger:MuLogger, ) { this.sessionId = sessionId; this._pc = pc; this._answerOpts = answerOpts; this._signal = (data) => { signal(data, sessionId); }; this._serverClose = serverClose; this._logger = logger; pc.onicecandidate = ({ candidate }) => { if (this._state === MuSocketState.INIT) { if (candidate) { this._signal(candidate.toJSON()); } } }; pc.oniceconnectionstatechange = () => { if (this._state === MuSocketState.INIT) { if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'closed') { logger.error(`ICE connection ${pc.iceConnectionState}`); this.close(); } else { logger.log(`${sessionId} ICE connection state: ${pc.iceConnectionState}`); } } }; pc.onconnectionstatechange = () => { if (this._state !== MuSocketState.CLOSED) { if (pc.connectionState === 'failed') { logger.error(`connection failed`); this.close(); } } }; pc.ondatachannel = ({ channel }) => { if (this._state !== MuSocketState.INIT) { return; } if (!channel) { this._logger.error('`channel` property is missing`'); this.close(); return; } channel.onopen = () => { if (this._state === MuSocketState.CLOSED) { return; } this._logger.log(`${channel.label} channel is open`); if (this._reliableChannel && this._unreliableChannel && this._reliableChannel.readyState === 'open' && this._unreliableChannel.readyState === 'open' ) { connection(this); } }; channel.onerror = (e) => { if (this._state !== MuSocketState.CLOSED) { this.close(e); } }; channel.onclose = () => { if (this._state !== MuSocketState.CLOSED) { this.close(`${channel.label} channel closed unexpectedly`); } }; if (/unreliable/.test(channel.label)) { this._unreliableChannel = channel; this._unreliableChannel.binaryType = 'arraybuffer'; this._unreliableChannel.onmessage = ({ data }) => { if (typeof data !== 'string') { this._onMessage(new Uint8Array(data).subarray(0), true); } else { this._onMessage(data, true); } }; } else if (/reliable/.test(channel.label)) { this._reliableChannel = channel; this._reliableChannel.binaryType = 'arraybuffer'; this._reliableChannel.onmessage = ({ data }) => { if (typeof data !== 'string') { this._onMessage(new Uint8Array(data).subarray(0), false); } else { this._onMessage(data, false); } }; } }; } public handleSignal (data:RTCIceCandidateInit|RTCSessionDescriptionInit) { if (this._state !== MuSocketState.INIT) { return; } if ('sdp' in data) { this._pc.setRemoteDescription(data) .then(() => { this._pc.createAnswer(this._answerOpts) .then((answer) => { this._pc.setLocalDescription(answer) .then(() => { this._signal(answer); }).catch((e) => this.close(e)); }).catch((e) => this.close(e)); }).catch((e) => this.close(e)); } else if ('candidate' in data) { this._pc.addIceCandidate(data).catch((e) => this.close(e)); } else { this._logger.error(`invalid negotiation message: ${data}`); } } public open (spec:MuSocketSpec) { if (this._state !== MuSocketState.INIT) { throw error(`socket had been opened`); } this._state = MuSocketState.OPEN; this._onMessage = spec.message; this._onClose = spec.close; spec.ready(); } public send (data:MuData, unreliable?:boolean) { if (this._state !== MuSocketState.OPEN) { return; } if (unreliable && this._unreliableChannel) { this._unreliableChannel.send(<any>data); } else if (this._reliableChannel) { this._reliableChannel.send(<any>data); } } public close (e?:any) { if (this._state === MuSocketState.CLOSED) { return; } if (e) { this._logger.exception(e); } this._state = MuSocketState.CLOSED; this._pc.close(); this._pc.onicecandidate = null; this._pc.oniceconnectionstatechange = null; this._pc.onconnectionstatechange = null; this._pc.ondatachannel = null; if (this._reliableChannel) { this._reliableChannel.onopen = null; this._reliableChannel.onmessage = null; this._reliableChannel.onerror = null; this._reliableChannel.onclose = null; this._reliableChannel = null; } if (this._unreliableChannel) { this._unreliableChannel.onopen = null; this._unreliableChannel.onmessage = null; this._unreliableChannel.onerror = null; this._unreliableChannel.onclose = null; this._unreliableChannel = null; } this._pc = <any>null; this._onClose(); this._serverClose(); } public reliableBufferedAmount () { return 0; } public unreliableBufferedAmount () { return 0; } } export class MuRTCSocketServer implements MuSocketServer { private _state = MuSocketServerState.INIT; public state () { return this._state; } public clients:MuRTCSocketClient[] = []; public readonly wrtc:MuRTCBinding; private _pcConfig:MuRTCConfiguration; private _answerOpts:MuRTCOfferAnswerOptions; private _signal:(data:object, sessionId:MuSessionId) => void; private _scheduler:MuScheduler; private _logger:MuLogger; private _onConnection:MuConnectionHandler = noop; private _onClose:MuCloseHandler = noop; constructor (spec:{ signal:(data:object, sessionId:MuSessionId) => void, wrtc?:MuRTCBinding, pcConfig?:MuRTCConfiguration, answerOpts?:RTCOfferAnswerOptions, scheduler?:MuScheduler, logger?:MuLogger, }) { if (isBrowser && !browserRTC()) { throw error(`browser doesn't support WebRTC`); } if (!isBrowser && !spec.wrtc) { throw error(`specify WebRTC binding via spec.wrtc`); } this.wrtc = browserRTC() || <MuRTCBinding>spec.wrtc; this._signal = spec.signal; this._pcConfig = spec.pcConfig || { iceServers: [ { urls: 'stun:global.stun.twilio.com:3478' }, ], }; this._pcConfig.sdpSemantics = 'unified-plan'; this._answerOpts = spec.answerOpts || {}; this._scheduler = spec.scheduler || MuSystemScheduler; this._logger = spec.logger || MuDefaultLogger; } public start (spec:MuSocketServerSpec) { if (this._state !== MuSocketServerState.INIT) { throw error(`attempt to start when server is ${this._state === MuSocketServerState.RUNNING ? 'running' : 'shut down'}`); } this._scheduler.setTimeout(() => { if (this._state !== MuSocketServerState.INIT) { return; } this._state = MuSocketServerState.RUNNING; this._onConnection = spec.connection; this._onClose = spec.close; spec.ready(); }, 0); } private _pendingClients:MuRTCSocketClient[] = []; public handleSignal (packet:string) { if (this._state !== MuSocketServerState.RUNNING) { return; } function findClient (sessionId:MuSessionId, clients:MuRTCSocketClient[]) : MuRTCSocketClient|null { for (let i = clients.length - 1; i >= 0; --i) { if (clients[i].sessionId === sessionId) { return clients[i]; } } return null; } try { const data = JSON.parse(packet); if (!data.sid) { this._logger.error(`no session id in negotiation message`); return; } const sessionId:MuSessionId = data.sid; delete data.sid; let client = findClient(sessionId, this._pendingClients); if (!client) { client = new MuRTCSocketClient( sessionId, new this.wrtc.RTCPeerConnection(this._pcConfig), this._answerOpts, this._signal, () => { if (client) { this._onConnection(client); this.clients.push(client); this._pendingClients.splice(this._pendingClients.indexOf(client), 1); } }, () => { if (client) { this.clients.splice(this.clients.indexOf(client), 1); } }, this._logger, ); this._pendingClients.push(client); } client.handleSignal(data); } catch (e) { this._logger.exception(e); } } public close () { if (this._state === MuSocketServerState.SHUTDOWN) { return; } this._state = MuSocketServerState.SHUTDOWN; for (let i = 0; i < this.clients.length; ++i) { this.clients[i].close(); } this.clients.length = 0; this._onClose(); } }