UNPKG

awrtc_browser

Version:

Compatible browser implementation to the Unity asset WebRTC Video Chat. Try examples in build folder

559 lines (461 loc) 19.5 kB
/* Copyright (c) 2019, because-why-not.com Limited All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import {ConnectionId, NetworkEvent, NetEventType, IBasicNetwork} from './INetwork' import { SLog } from './Helper'; export enum WebsocketConnectionStatus { Uninitialized, NotConnected, Connecting, Connected, Disconnecting //server will shut down, all clients disconnect, ... } export enum WebsocketServerStatus { Offline, Starting, Online, ShuttingDown } //TODO: handle errors if the socket connection failed //+ send back failed events for connected / serverstart events that are buffered export class WebsocketNetwork implements IBasicNetwork { public static readonly LOGTAG = "WebsocketNetwork"; //websocket. private mSocket: WebSocket; //currents status. will be updated based on update call private mStatus = WebsocketConnectionStatus.Uninitialized; public getStatus() { return this.mStatus;}; //queue to hold buffered outgoing messages private mOutgoingQueue = new Array<NetworkEvent>(); //buffer for incoming messages private mIncomingQueue = new Array<NetworkEvent>(); //Status of the server for incoming connections private mServerStatus = WebsocketServerStatus.Offline; //outgoing connections (just need to be stored to allow to send out a failed message //if the whole signaling connection fails private mConnecting = new Array<number>(); private mConnections = new Array<number>(); //next free connection id private mNextOutgoingConnectionId = new ConnectionId(1); /// <summary> /// Version of the protocol implemented here /// </summary> public static readonly PROTOCOL_VERSION = 2; /// <summary> /// Minimal protocol version that is still supported. /// V 1 servers won't understand heartbeat and version /// messages but would just log an unknown message and /// continue normally. /// </summary> public static readonly PROTOCOL_VERSION_MIN = 1; /// <summary> /// Assume 1 until message received /// </summary> private mRemoteProtocolVersion = 1; private mUrl: string = null; private mConfig: WebsocketNetwork.Configuration; private mLastHeartbeat: number; private mHeartbeatReceived = true; private mIsDisposed = false; public constructor(url: string, configuration?:WebsocketNetwork.Configuration) { this.mUrl = url; this.mStatus = WebsocketConnectionStatus.NotConnected; this.mConfig = configuration; if(!this.mConfig) this.mConfig = new WebsocketNetwork.Configuration(); this.mConfig.Lock(); } private WebsocketConnect(): void { this.mStatus = WebsocketConnectionStatus.Connecting; this.mSocket = new WebSocket(this.mUrl); this.mSocket.binaryType = "arraybuffer"; this.mSocket.onopen = () => { this.OnWebsocketOnOpen(); } this.mSocket.onerror = (error) => { this.OnWebsocketOnError(error); }; this.mSocket.onmessage = (e) => { this.OnWebsocketOnMessage(e); }; this.mSocket.onclose = (e) => { this.OnWebsocketOnClose(e); }; } private WebsocketCleanup() : void { this.mSocket.onopen = null; this.mSocket.onerror = null; this.mSocket.onmessage = null; this.mSocket.onclose = null; if (this.mSocket.readyState == this.mSocket.OPEN || this.mSocket.readyState == this.mSocket.CONNECTING) { this.mSocket.close(); } this.mSocket = null; } private EnsureServerConnection(): void { if (this.mStatus == WebsocketConnectionStatus.NotConnected) { //no server //no connection about to be established //no current connections //-> disconnect the server connection this.WebsocketConnect(); } } private UpdateHeartbeat():void{ if(this.mStatus == WebsocketConnectionStatus.Connected && this.mConfig.Heartbeat > 0) { let diff = Date.now() - this.mLastHeartbeat; if(diff > (this.mConfig.Heartbeat * 1000)) { //We trigger heatbeat timeouts only for protocol V2 //protocol 1 can receive the heatbeats but //won't send a reply //(still helpful to trigger TCP ACK timeout) if(this.mRemoteProtocolVersion > 1 && this.mHeartbeatReceived == false) { this.TriggerHeartbeatTimeout(); return; } this.mLastHeartbeat = Date.now(); this.mHeartbeatReceived = false; this.SendHeartbeat(); } } } private TriggerHeartbeatTimeout(){ SLog.L("Closing due to heartbeat timeout. Server didn't respond in time.", WebsocketNetwork.LOGTAG); this.Cleanup(); } private CheckSleep() : void { if (this.mStatus == WebsocketConnectionStatus.Connected && this.mServerStatus == WebsocketServerStatus.Offline && this.mConnecting.length == 0 && this.mConnections.length == 0) { //no server //no connection about to be established //no current connections //-> disconnect the server connection this.Cleanup(); } } private OnWebsocketOnOpen() { SLog.L('onWebsocketOnOpen', WebsocketNetwork.LOGTAG); this.mStatus = WebsocketConnectionStatus.Connected; this.mLastHeartbeat = Date.now(); this.SendVersion(); } private OnWebsocketOnClose(event: CloseEvent) { SLog.L('Closed: ' + JSON.stringify(event), WebsocketNetwork.LOGTAG); if(event.code != 1000) { SLog.LE("Websocket closed with code: " + event.code + " " + event.reason); } //ignore closed event if it was caused due to a shutdown (as that means we cleaned up already) if (this.mStatus == WebsocketConnectionStatus.Disconnecting || this.mStatus == WebsocketConnectionStatus.NotConnected) return; this.Cleanup(); this.mStatus = WebsocketConnectionStatus.NotConnected; } private OnWebsocketOnMessage(event) { if (this.mStatus == WebsocketConnectionStatus.Disconnecting || this.mStatus == WebsocketConnectionStatus.NotConnected) return; //browsers will have ArrayBuffer in event.data -> change to byte array let msg = new Uint8Array(event.data); this.ParseMessage(msg); } private OnWebsocketOnError(error) { //the error event doesn't seem to have any useful information? //browser is expected to call OnClose after this SLog.LE('WebSocket Error ' + error); } /// <summary> /// called during Disconnecting state either trough server connection failed or due to Shutdown /// /// Also used to switch to sleeping mode. In this case there connection isn't used as /// server and doesn't have any connections (established or connecting) thus /// only WebsocketCleanup is in effect. /// /// WebsocketNetwork has to be still usable after this call like a newly /// created connections (except with events in the message queue) /// </summary> private Cleanup(): void { //check if this was done already (or we are in the process of cleanup already) if (this.mStatus == WebsocketConnectionStatus.Disconnecting || this.mStatus == WebsocketConnectionStatus.NotConnected) return; this.mStatus = WebsocketConnectionStatus.Disconnecting; //throw connection failed events for each connection in mConnecting for (let conId of this.mConnecting) { //all connection it tries to establish right now fail due to shutdown this.EnqueueIncoming( new NetworkEvent(NetEventType.ConnectionFailed, new ConnectionId(conId), null)); } this.mConnecting = new Array<number>(); //throw disconnect events for all NewConnection events in the outgoing queue //ignore messages and everything else for (let conId of this.mConnections) { //all connection it tries to establish right now fail due to shutdown this.EnqueueIncoming( new NetworkEvent(NetEventType.Disconnected, new ConnectionId(conId), null)); } this.mConnections = new Array<number>(); if (this.mServerStatus == WebsocketServerStatus.Starting) { //if server was Starting -> throw failed event this.EnqueueIncoming( new NetworkEvent(NetEventType.ServerInitFailed, ConnectionId.INVALID, null)); } else if (this.mServerStatus == WebsocketServerStatus.Online) { //if server was Online -> throw close event this.EnqueueIncoming( new NetworkEvent(NetEventType.ServerClosed, ConnectionId.INVALID, null)); } else if (this.mServerStatus == WebsocketServerStatus.ShuttingDown) { //if server was ShuttingDown -> throw close event (don't think this can happen) this.EnqueueIncoming( new NetworkEvent(NetEventType.ServerClosed, ConnectionId.INVALID, null)); } this.mServerStatus = WebsocketServerStatus.Offline; this.mOutgoingQueue = new Array<NetworkEvent>(); this.WebsocketCleanup(); this.mStatus = WebsocketConnectionStatus.NotConnected; } private EnqueueOutgoing(evt: NetworkEvent): void { this.mOutgoingQueue.push(evt); } private EnqueueIncoming(evt: NetworkEvent): void { this.mIncomingQueue.push(evt); } private TryRemoveConnecting(id: ConnectionId): void { var index = this.mConnecting.indexOf(id.id); if (index != -1) { this.mConnecting.splice(index, 1); } } private TryRemoveConnection(id: ConnectionId): void { var index = this.mConnections.indexOf(id.id); if (index != -1) { this.mConnections.splice(index, 1); } } private ParseMessage(msg:Uint8Array):void{ if(msg.length == 0) { }else if(msg[0] == NetEventType.MetaVersion) { if (msg.length > 1) { this.mRemoteProtocolVersion = msg[1]; } else { SLog.LW("Received an invalid MetaVersion header without content."); } }else if(msg[0] == NetEventType.MetaHeartbeat) { this.mHeartbeatReceived = true; }else { let evt = NetworkEvent.fromByteArray(msg); this.HandleIncomingEvent(evt); } } private HandleIncomingEvent(evt: NetworkEvent) { if (evt.Type == NetEventType.NewConnection) { //removing connecting info this.TryRemoveConnecting(evt.ConnectionId); //add connection this.mConnections.push(evt.ConnectionId.id); } else if (evt.Type == NetEventType.ConnectionFailed) { //remove connecting info this.TryRemoveConnecting(evt.ConnectionId); } else if (evt.Type == NetEventType.Disconnected) { //remove from connections this.TryRemoveConnection(evt.ConnectionId); } else if (evt.Type == NetEventType.ServerInitialized) { this.mServerStatus = WebsocketServerStatus.Online; } else if (evt.Type == NetEventType.ServerInitFailed) { this.mServerStatus = WebsocketServerStatus.Offline; } else if (evt.Type == NetEventType.ServerClosed) { this.mServerStatus = WebsocketServerStatus.ShuttingDown; //any cleaning up to do? this.mServerStatus = WebsocketServerStatus.Offline; } this.EnqueueIncoming(evt); } private HandleOutgoingEvents(): void { while (this.mOutgoingQueue.length > 0) { var evt = this.mOutgoingQueue.shift(); this.SendNetworkEvent(evt); } } private SendHeartbeat() : void { let msg = new Uint8Array(1); msg[0] = NetEventType.MetaHeartbeat; this.InternalSend(msg); } private SendVersion() :void { let msg = new Uint8Array(2); msg[0] = NetEventType.MetaVersion; msg[1] = WebsocketNetwork.PROTOCOL_VERSION; this.InternalSend(msg); } private SendNetworkEvent(evt: NetworkEvent):void { var msg = NetworkEvent.toByteArray(evt); this.InternalSend(msg); } private InternalSend(msg: Uint8Array): void { this.mSocket.send(msg); } private NextConnectionId(): ConnectionId { var result = this.mNextOutgoingConnectionId; this.mNextOutgoingConnectionId = new ConnectionId(this.mNextOutgoingConnectionId.id + 1); return result; } private GetRandomKey(): string { var result = ""; for (var i = 0; i < 7; i++) { result += String.fromCharCode(65 + Math.round(Math.random() * 25)); } return result; } //interface implementation public Dequeue(): NetworkEvent { if (this.mIncomingQueue.length > 0) return this.mIncomingQueue.shift(); return null; } public Peek(): NetworkEvent { if (this.mIncomingQueue.length > 0) return this.mIncomingQueue[0]; return null; } public Update(): void { this.UpdateHeartbeat(); this.CheckSleep(); } public Flush(): void { //ideally we buffer everything and then flush when it is connected as //websockets aren't suppose to be used for realtime communication anyway if (this.mStatus == WebsocketConnectionStatus.Connected) this.HandleOutgoingEvents(); } public SendData(id: ConnectionId, data: Uint8Array, /*offset: number, length: number,*/ reliable: boolean): boolean { if (id == null || id.id == ConnectionId.INVALID.id) { SLog.LW("Ignored message. Invalid connection id."); return; } if (data == null || data.length == 0) { SLog.LW("Ignored message. Invalid data."); return; } var evt: NetworkEvent; if (reliable) { evt = new NetworkEvent(NetEventType.ReliableMessageReceived, id, data); } else { evt = new NetworkEvent(NetEventType.UnreliableMessageReceived, id, data); } this.EnqueueOutgoing(evt); return true; } public Disconnect(id: ConnectionId): void { var evt = new NetworkEvent(NetEventType.Disconnected, id, null); this.EnqueueOutgoing(evt); } public Shutdown(): void { this.Cleanup(); this.mStatus = WebsocketConnectionStatus.NotConnected; } public Dispose() { if (this.mIsDisposed == false) { this.Shutdown(); this.mIsDisposed = true; } } public StartServer(): void; public StartServer(address: string): void; public StartServer(address?: string): void { if (address == null) { address = "" + this.GetRandomKey(); } if (this.mServerStatus == WebsocketServerStatus.Offline) { this.EnsureServerConnection(); this.mServerStatus = WebsocketServerStatus.Starting; //TODO: address is a string but ubytearray is defined. will fail if binary this.EnqueueOutgoing(new NetworkEvent(NetEventType.ServerInitialized, ConnectionId.INVALID, address)); } else { this.EnqueueIncoming(new NetworkEvent(NetEventType.ServerInitFailed, ConnectionId.INVALID, address)); } } public StopServer(): void { this.EnqueueOutgoing(new NetworkEvent(NetEventType.ServerClosed, ConnectionId.INVALID, null)); } public Connect(address: string): ConnectionId { this.EnsureServerConnection(); var newConId = this.NextConnectionId(); this.mConnecting.push(newConId.id); var evt = new NetworkEvent(NetEventType.NewConnection, newConId, address); this.EnqueueOutgoing(evt); return newConId; } } export namespace WebsocketNetwork{ export class Configuration{ mHeartbeat:number = 30; get Heartbeat():number { return this.mHeartbeat; } set Heartbeat(value:number){ if(this.mLocked) { throw new Error("Can't change configuration once used."); } this.mHeartbeat = value; } mLocked = false; Lock():void { this.mLocked = true; } } } //Below tests only. Move out later function bufferToString(buffer: Uint8Array): string { let arr = new Uint16Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 2); return String.fromCharCode.apply(null, arr); } function stringToBuffer(str: string): Uint8Array { let buf = new ArrayBuffer(str.length * 2); let bufView = new Uint16Array(buf); for (var i = 0, strLen = str.length; i < strLen; i++) { bufView[i] = str.charCodeAt(i); } let result = new Uint8Array(buf); return result; }