Last active
March 22, 2021 02:49
-
-
Save Hri7566/bd72610fdfbb5aae4d1acd441ec52d26 to your computer and use it in GitHub Desktop.
MPP TypeScript Client for Deno
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { WebSocketClient, StandardWebSocketClient } from "https://deno.land/x/[email protected]/mod.ts"; | |
| import { EventEmitter } from "https://deno.land/[email protected]/node/events.ts"; | |
| export default class Client extends EventEmitter { | |
| uri: string; | |
| ws: WebSocketClient | any; | |
| serverTimeOffset: number; | |
| user: any; | |
| participantId: any; | |
| channel: any; | |
| ppl: any; | |
| connectionTime: any; | |
| connectionAttempts: any; | |
| desiredChannelId: any; | |
| desiredChannelSettings: any; | |
| pingInterval: any; | |
| canConnect: boolean; | |
| noteBuffer: any; | |
| noteBufferTime: any; | |
| noteFlushInterval: any; | |
| ['๐']: any; | |
| offlineParticipant: any; | |
| offlineChannelSettings: any; | |
| constructor (uri: string) { | |
| super(); | |
| this.uri = uri; | |
| this.ws = undefined; | |
| this.serverTimeOffset = 0; | |
| this.user = undefined; | |
| this.participantId = undefined; | |
| this.channel = undefined; | |
| this.ppl = {}; | |
| this.connectionTime = undefined; | |
| this.connectionAttempts = 0; | |
| this.desiredChannelId = undefined; | |
| this.desiredChannelSettings = undefined; | |
| this.pingInterval = undefined; | |
| this.canConnect = false; | |
| this.noteBuffer = []; | |
| this.noteBufferTime = 0; | |
| this.noteFlushInterval = undefined; | |
| this['๐'] = 0; | |
| this.offlineParticipant = { | |
| _id: "", | |
| name: "", | |
| color: "#777" | |
| }; | |
| this.offlineChannelSettings = { | |
| color:"#ecfaed" | |
| }; | |
| this.bindEventListeners(); | |
| this.emit("status", "(Offline mode)"); | |
| } | |
| bindEventListeners() { | |
| var self = this; | |
| this.on("hi", function(msg) { | |
| self.user = msg.u; | |
| self.receiveServerTime(msg.t, msg.e || undefined); | |
| if(self.desiredChannelId) { | |
| self.setChannel(); | |
| } | |
| }); | |
| this.on("t", function(msg) { | |
| self.receiveServerTime(msg.t, msg.e || undefined); | |
| }); | |
| this.on("ch", function(msg) { | |
| self.desiredChannelId = msg.ch._id; | |
| self.desiredChannelSettings = msg.ch.settings; | |
| self.channel = msg.ch; | |
| if(msg.p) self.participantId = msg.p; | |
| self.setParticipants(msg.ppl); | |
| }); | |
| this.on("p", function(msg) { | |
| self.participantUpdate(msg); | |
| self.emit("participant update", self.findParticipantById(msg.id)); | |
| }); | |
| this.on("m", function(msg) { | |
| if(self.ppl.hasOwnProperty(msg.id)) { | |
| self.participantUpdate(msg); | |
| } | |
| }); | |
| this.on("bye", function(msg) { | |
| self.removeParticipant(msg.p); | |
| }); | |
| } | |
| receiveServerTime(time: any, echo: any) { | |
| var self = this; | |
| var now = Date.now(); | |
| var target = time - now; | |
| //console.log("Target serverTimeOffset: " + target); | |
| var duration = 1000; | |
| var step = 0; | |
| var steps = 50; | |
| var step_ms = duration / steps; | |
| var difference = target - this.serverTimeOffset; | |
| var inc = difference / steps; | |
| var iv: any; | |
| iv = setInterval(function() { | |
| self.serverTimeOffset += inc; | |
| if(++step >= steps) { | |
| clearInterval(iv); | |
| //console.log("serverTimeOffset reached: " + self.serverTimeOffset); | |
| self.serverTimeOffset=target; | |
| } | |
| }, step_ms); | |
| // smoothen | |
| //this.serverTimeOffset = time - now; // mostly time zone offset ... also the lags so todo smoothen this | |
| // not smooth: | |
| //if(echo) this.serverTimeOffset += echo - now; // mostly round trip time offset | |
| } | |
| setChannel(id?: string, set?: any) { | |
| this.desiredChannelId = id || this.desiredChannelId || "lobby"; | |
| this.desiredChannelSettings = set || this.desiredChannelSettings || undefined; | |
| this.sendArray([{m: "ch", _id: this.desiredChannelId, set: this.desiredChannelSettings}]); | |
| } | |
| sendArray(arr: any) { | |
| this.send(JSON.stringify(arr)); | |
| } | |
| send(raw: string) { | |
| if(this.isConnected()) this.ws.webSocket.send(raw); | |
| } | |
| isConnected() { | |
| return this.isSupported() && this.ws && this.ws.webSocket.readyState === 1; | |
| } | |
| isConnecting() { | |
| return this.isSupported() && this.ws && this.ws.webSocket.readyState === 0; | |
| } | |
| isSupported() { | |
| return typeof WebSocket === "function"; | |
| } | |
| start() { | |
| this.canConnect = true; | |
| this.connect(); | |
| } | |
| stop() { | |
| this.canConnect = false; | |
| this.ws.webSocket.close(); | |
| } | |
| connect() { | |
| if(!this.canConnect || !this.isSupported() || this.isConnected() || this.isConnecting()) return; | |
| this.emit("status", "Connecting..."); | |
| this.ws = new StandardWebSocketClient(this.uri); | |
| var self = this; | |
| this.ws.webSocket.addEventListener("close", function(evt: any) { | |
| self.user = undefined; | |
| self.participantId = undefined; | |
| self.channel = undefined; | |
| self.setParticipants([]); | |
| clearInterval(self.pingInterval); | |
| clearInterval(self.noteFlushInterval); | |
| self.emit("disconnect", evt); | |
| self.emit("status", "Offline mode"); | |
| // reconnect! | |
| if(self.connectionTime) { | |
| self.connectionTime = undefined; | |
| self.connectionAttempts = 0; | |
| } else { | |
| ++self.connectionAttempts; | |
| } | |
| var ms_lut = [50, 2950, 7000, 10000]; | |
| var idx = self.connectionAttempts; | |
| if(idx >= ms_lut.length) idx = ms_lut.length - 1; | |
| var ms = ms_lut[idx]; | |
| setTimeout(self.connect.bind(self), ms); | |
| }); | |
| this.ws.webSocket.addEventListener("error", function(err: any) { | |
| self.emit("wserror", err); | |
| self.ws.webSocket.close(); // self.ws.emit("close"); | |
| }); | |
| this.ws.webSocket.addEventListener("open", function(evt: any) { | |
| self.connectionTime = Date.now(); | |
| self.sendArray([{"m": "hi", "๐": self['๐']++ || undefined }]); | |
| self.pingInterval = setInterval(function() { | |
| self.sendArray([{m: "t", e: Date.now()}]); | |
| }, 20000); | |
| //self.sendArray([{m: "t", e: Date.now()}]); | |
| self.noteBuffer = []; | |
| self.noteBufferTime = 0; | |
| self.noteFlushInterval = setInterval(function() { | |
| if(self.noteBufferTime && self.noteBuffer.length > 0) { | |
| self.sendArray([{m: "n", t: self.noteBufferTime + self.serverTimeOffset, n: self.noteBuffer}]); | |
| self.noteBufferTime = 0; | |
| self.noteBuffer = []; | |
| } | |
| }, 200); | |
| self.emit("connect"); | |
| self.emit("status", "Joining channel..."); | |
| }); | |
| this.ws.webSocket.addEventListener("message", function(evt: any) { | |
| var transmission = JSON.parse(evt.data); | |
| for(var i = 0; i < transmission.length; i++) { | |
| var msg = transmission[i]; | |
| self.emit(msg.m, msg); | |
| } | |
| }); | |
| } | |
| setParticipants(ppl: any) { | |
| for(var id in this.ppl) { | |
| if(!this.ppl.hasOwnProperty(id)) continue; | |
| var found = false; | |
| for(var j = 0; j < ppl.length; j++) { | |
| if(ppl[j].id === id) { | |
| found = true; | |
| break; | |
| } | |
| } | |
| if(!found) { | |
| this.removeParticipant(id); | |
| } | |
| } | |
| // update all | |
| for(var i = 0; i < ppl.length; i++) { | |
| this.participantUpdate(ppl[i]); | |
| } | |
| } | |
| participantUpdate(update: any) { | |
| var part = this.ppl[update.id] || null; | |
| if(part === null) { | |
| part = update; | |
| this.ppl[part.id] = part; | |
| this.emit("participant added", part); | |
| this.emit("count", this.countParticipants()); | |
| } else { | |
| if(update.x) part.x = update.x; | |
| if(update.y) part.y = update.y; | |
| if(update.color) part.color = update.color; | |
| if(update.name) part.name = update.name; | |
| } | |
| } | |
| findParticipantById(id: string) { | |
| return this.ppl[id] || this.offlineParticipant; | |
| } | |
| removeParticipant(id: string) { | |
| if(this.ppl.hasOwnProperty(id)) { | |
| var part = this.ppl[id]; | |
| delete this.ppl[id]; | |
| this.emit("participant removed", part); | |
| this.emit("count", this.countParticipants()); | |
| } | |
| } | |
| countParticipants() { | |
| var count = 0; | |
| for(var i in this.ppl) { | |
| if(this.ppl.hasOwnProperty(i)) ++count; | |
| } | |
| return count; | |
| } | |
| getChannelSetting(key: any) { | |
| if(!this.isConnected() || !this.channel || !this.channel.settings) { | |
| return this.offlineChannelSettings[key]; | |
| } | |
| return this.channel.settings[key]; | |
| } | |
| setChannelSettings(settings: any) { | |
| if(!this.isConnected() || !this.channel || !this.channel.settings) { | |
| return; | |
| } | |
| if(this.desiredChannelSettings){ | |
| for(var key in settings) { | |
| this.desiredChannelSettings[key] = settings[key]; | |
| } | |
| this.sendArray([{m: "chset", set: this.desiredChannelSettings}]); | |
| } | |
| } | |
| getOwnParticipant() { | |
| return this.findParticipantById(this.participantId); | |
| } | |
| isOwner() { | |
| return this.channel && this.channel.crown && this.channel.crown.participantId === this.participantId; | |
| } | |
| preventsPlaying() { | |
| return this.isConnected() && !this.isOwner() && this.getChannelSetting("crownsolo") === true; | |
| } | |
| startNote(note: any, v: number) { | |
| if(this.isConnected()) { | |
| var vel: any = typeof v === "undefined" ? undefined : +v.toFixed(3); | |
| if(!this.noteBufferTime) { | |
| this.noteBufferTime = Date.now(); | |
| this.noteBuffer.push({n: note, v: vel}); | |
| } else { | |
| this.noteBuffer.push({d: Date.now() - this.noteBufferTime, n: note, v: vel}); | |
| } | |
| } | |
| } | |
| stopNote(note: any) { | |
| if(this.isConnected()) { | |
| if(!this.noteBufferTime) { | |
| this.noteBufferTime = Date.now(); | |
| this.noteBuffer.push({n: note, s: 1}); | |
| } else { | |
| this.noteBuffer.push({d: Date.now() - this.noteBufferTime, n: note, s: 1}); | |
| } | |
| } | |
| } | |
| } | |
| export { Client }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment