diff options
| author | RblSb <msrblsb@gmail.com> | 2024-08-14 19:54:25 +0300 |
|---|---|---|
| committer | RblSb <msrblsb@gmail.com> | 2024-08-14 19:54:25 +0300 |
| commit | 38cc0a1d9b4b146af7110c681389378fd26761fa (patch) | |
| tree | e4a0a35e4a518508c4127fdb368f388ce130e2a0 | |
| parent | 097cf610e664c922c1c618b11a582bb6a1d959f8 (diff) | |
Seamless reconnection for first 5 seconds
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | build/server.js | 70 | ||||
| -rw-r--r-- | res/client.js | 50 | ||||
| -rw-r--r-- | src/client/Main.hx | 39 | ||||
| -rw-r--r-- | src/import.hx | 1 | ||||
| -rw-r--r-- | src/server/Main.hx | 24 | ||||
| -rw-r--r-- | src/tools/ArrayTools.hx | 116 | ||||
| -rw-r--r-- | src/utils/ArrayKeyValueReverseIterator.hx | 19 | ||||
| -rw-r--r-- | src/utils/ArrayReverseIterator.hx | 19 |
9 files changed, 292 insertions, 50 deletions
@@ -4,8 +4,7 @@ Lightweight modern implementation and very easy way to run locally. Default channel example: https://synctube.onrender.com/
-### New features
-- Reworked layout and theme
+### Features
- Multi-Language support
- Hotkeys (`Alt-P` for global play/pause, [etc](https://github.com/RblSb/SyncTube/blob/d3f6d4e6434527569d13f211a0eb074c5a11992e/src/client/Buttons.hx#L303-L314))
- Mobile view with page fullscreen
@@ -18,6 +17,7 @@ Default channel example: https://synctube.onrender.com/ ### Supported players
- Youtube (videos, shorts, streams and playlists)
+- [Streamable](https://streamable.com)
- Raw mp4 videos and m3u8 playlists (or any other media format supported in browser)
- Iframes (without sync)
diff --git a/build/server.js b/build/server.js index 3868c84..076051d 100644 --- a/build/server.js +++ b/build/server.js @@ -1,4 +1,4 @@ -// Generated by Haxe 4.3.5 +// Generated by Haxe 4.3.6 (function ($global) { "use strict"; var $estr = function() { return js_Boot.__string_rec(this,''); },$hxEnums = $hxEnums || {},$_; function $extend(from, fields) { @@ -4578,7 +4578,7 @@ var server_Main = function(opts) { preparePort = function() { server_Utils.isPortFree(_gthis.port,function(isFree) { if(!isFree && attempts > 0) { - haxe_Log.trace("Warning: port " + _gthis.port + " is already in use. Changed to " + (_gthis.port + 1),{ fileName : "src/server/Main.hx", lineNumber : 113, className : "server.Main", methodName : "new"}); + haxe_Log.trace("Warning: port " + _gthis.port + " is already in use. Changed to " + (_gthis.port + 1),{ fileName : "src/server/Main.hx", lineNumber : 122, className : "server.Main", methodName : "new"}); attempts -= 1; _gthis.port++; preparePort(); @@ -4596,13 +4596,13 @@ server_Main.main = function() { server_Main.prototype = { runServer: function() { var _gthis = this; - haxe_Log.trace("Local: http://" + this.localIp + ":" + this.port,{ fileName : "src/server/Main.hx", lineNumber : 126, className : "server.Main", methodName : "runServer"}); + haxe_Log.trace("Local: http://" + this.localIp + ":" + this.port,{ fileName : "src/server/Main.hx", lineNumber : 135, className : "server.Main", methodName : "runServer"}); if(this.config.localNetworkOnly) { - haxe_Log.trace("Global network is disabled in config",{ fileName : "src/server/Main.hx", lineNumber : 128, className : "server.Main", methodName : "runServer"}); + haxe_Log.trace("Global network is disabled in config",{ fileName : "src/server/Main.hx", lineNumber : 137, className : "server.Main", methodName : "runServer"}); } else if(!this.isNoState) { server_Utils.getGlobalIp(function(ip) { _gthis.globalIp = ip; - haxe_Log.trace("Global: http://" + _gthis.globalIp + ":" + _gthis.port,{ fileName : "src/server/Main.hx", lineNumber : 132, className : "server.Main", methodName : "runServer"}); + haxe_Log.trace("Global: http://" + _gthis.globalIp + ":" + _gthis.port,{ fileName : "src/server/Main.hx", lineNumber : 141, className : "server.Main", methodName : "runServer"}); }); } var dir = "" + this.rootDir + "/res"; @@ -4687,7 +4687,7 @@ server_Main.prototype = { var field = _g1[_g]; ++_g; if(Reflect.field(config,field) == null) { - haxe_Log.trace("Warning: config field \"" + field + "\" is unknown",{ fileName : "src/server/Main.hx", lineNumber : 201, className : "server.Main", methodName : "getUserConfig"}); + haxe_Log.trace("Warning: config field \"" + field + "\" is unknown",{ fileName : "src/server/Main.hx", lineNumber : 210, className : "server.Main", methodName : "getUserConfig"}); } config[field] = Reflect.field(customConfig,field); } @@ -4698,14 +4698,14 @@ server_Main.prototype = { var emote = _g1[_g]; ++_g; if(emoteCopies_h[emote.name]) { - haxe_Log.trace("Warning: emote name \"" + emote.name + "\" has copy",{ fileName : "src/server/Main.hx", lineNumber : 207, className : "server.Main", methodName : "getUserConfig"}); + haxe_Log.trace("Warning: emote name \"" + emote.name + "\" has copy",{ fileName : "src/server/Main.hx", lineNumber : 216, className : "server.Main", methodName : "getUserConfig"}); } emoteCopies_h[emote.name] = true; if(!this.verbose) { continue; } if(emoteCopies_h[emote.image]) { - haxe_Log.trace("Warning: emote url of name \"" + emote.name + "\" has copy",{ fileName : "src/server/Main.hx", lineNumber : 211, className : "server.Main", methodName : "getUserConfig"}); + haxe_Log.trace("Warning: emote url of name \"" + emote.name + "\" has copy",{ fileName : "src/server/Main.hx", lineNumber : 220, className : "server.Main", methodName : "getUserConfig"}); } emoteCopies_h[emote.image] = true; } @@ -4743,7 +4743,7 @@ server_Main.prototype = { js_node_Fs.writeFileSync("" + folder + "/users.json",JSON.stringify({ admins : users1, bans : _g, salt : users.salt},null,"\t")); } ,saveState: function() { - haxe_Log.trace("Saving state...",{ fileName : "src/server/Main.hx", lineNumber : 250, className : "server.Main", methodName : "saveState"}); + haxe_Log.trace("Saving state...",{ fileName : "src/server/Main.hx", lineNumber : 259, className : "server.Main", methodName : "saveState"}); var json = JSON.stringify(this.getCurrentState(),null,"\t"); js_node_Fs.writeFileSync(this.statePath,json); this.writeUsers(this.userList); @@ -4758,7 +4758,7 @@ server_Main.prototype = { if(!sys_FileSystem.exists(this.statePath)) { return; } - haxe_Log.trace("Loading state...",{ fileName : "src/server/Main.hx", lineNumber : 273, className : "server.Main", methodName : "loadState"}); + haxe_Log.trace("Loading state...",{ fileName : "src/server/Main.hx", lineNumber : 282, className : "server.Main", methodName : "loadState"}); var state = JSON.parse(js_node_Fs.readFileSync(this.statePath,{ encoding : "utf8"})); this.videoList.setItems(state.videoList); this.videoList.isOpen = state.isPlaylistOpen; @@ -4777,7 +4777,7 @@ server_Main.prototype = { this.videoTimer.pause(); } ,logError: function(type,data) { - haxe_Log.trace(type,{ fileName : "src/server/Main.hx", lineNumber : 291, className : "server.Main", methodName : "logError", customParams : [data]}); + haxe_Log.trace(type,{ fileName : "src/server/Main.hx", lineNumber : 300, className : "server.Main", methodName : "logError", customParams : [data]}); var crashesFolder = "" + this.rootDir + "/user/crashes"; server_Utils.ensureDir(crashesFolder); var name = DateTools.format(new Date(),"%Y-%m-%d_%H_%M_%S") + "-" + type; @@ -4795,7 +4795,7 @@ server_Main.prototype = { if(_gthis.clients.length == 0) { return; } - haxe_Log.trace("Ping " + url,{ fileName : "src/server/Main.hx", lineNumber : 308, className : "server.Main", methodName : "initIntergationHandlers"}); + haxe_Log.trace("Ping " + url,{ fileName : "src/server/Main.hx", lineNumber : 317, className : "server.Main", methodName : "initIntergationHandlers"}); js_node_Http.get(url,null,function(r) { }); }; @@ -4815,13 +4815,13 @@ server_Main.prototype = { password += this.config.salt; var hash = haxe_crypto_Sha256.encode(password); this.userList.admins.push({ name : name, hash : hash}); - haxe_Log.trace("Admin " + name + " added.",{ fileName : "src/server/Main.hx", lineNumber : 331, className : "server.Main", methodName : "addAdmin"}); + haxe_Log.trace("Admin " + name + " added.",{ fileName : "src/server/Main.hx", lineNumber : 340, className : "server.Main", methodName : "addAdmin"}); } ,removeAdmin: function(name) { HxOverrides.remove(this.userList.admins,Lambda.find(this.userList.admins,function(item) { return item.name == name; })); - haxe_Log.trace("Admin " + name + " removed.",{ fileName : "src/server/Main.hx", lineNumber : 338, className : "server.Main", methodName : "removeAdmin"}); + haxe_Log.trace("Admin " + name + " removed.",{ fileName : "src/server/Main.hx", lineNumber : 347, className : "server.Main", methodName : "removeAdmin"}); } ,replayLog: function(events) { var _gthis = this; @@ -4865,7 +4865,7 @@ server_Main.prototype = { var ip = this.clientIp(req); var id = this.freeIds.length > 0 ? this.freeIds.shift() : this.clients.length; var name = "Guest " + (id + 1); - haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 376, className : "server.Main", methodName : "onConnect", customParams : ["" + name + " connected (" + ip + ")"]}); + haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 385, className : "server.Main", methodName : "onConnect", customParams : ["" + name + " connected (" + ip + ")"]}); var isAdmin = this.config.localAdmins && req.socket.localAddress == ip; var client = new Client(ws,req,id,name,0); client.setGroupFlag(ClientGroup.Admin,isAdmin); @@ -4878,7 +4878,7 @@ server_Main.prototype = { var obj = _gthis.wsEventParser.fromJson(data.toString()); if(_gthis.wsEventParser.errors.length > 0 || _gthis.noTypeObj(obj)) { var errors = "" + ("Wrong request for type \"" + obj.type + "\":") + "\n" + json2object_ErrorUtils.convertErrorArray(_gthis.wsEventParser.errors); - haxe_Log.trace(errors,{ fileName : "src/server/Main.hx", lineNumber : 392, className : "server.Main", methodName : "onConnect"}); + haxe_Log.trace(errors,{ fileName : "src/server/Main.hx", lineNumber : 401, className : "server.Main", methodName : "onConnect"}); _gthis.serverMessage(client,errors); return; } @@ -5012,6 +5012,10 @@ server_Main.prototype = { if(!internal) { return; } + var tmp = this.emptyRoomCallbackTimer; + if(tmp != null) { + tmp.stop(); + } if(this.clients.length == 1 && this.videoList.items.length > 0) { if(this.videoTimer.isPaused()) { this.videoTimer.play(); @@ -5025,7 +5029,7 @@ server_Main.prototype = { if(!internal) { return; } - haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 452, className : "server.Main", methodName : "onMessage", customParams : ["Client " + client.name + " disconnected"]}); + haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 462, className : "server.Main", methodName : "onMessage", customParams : ["Client " + client.name + " disconnected"]}); server_Utils.sortedPush(this.freeIds,client.id); HxOverrides.remove(this.clients,client); this.sendClientList(); @@ -5035,10 +5039,20 @@ server_Main.prototype = { } } if(this.clients.length == 0) { - if(this.waitVideoStart != null) { - this.waitVideoStart.stop(); + var tmp = this.emptyRoomCallbackTimer; + if(tmp != null) { + tmp.stop(); } - this.videoTimer.pause(); + this.emptyRoomCallbackTimer = haxe_Timer.delay(function() { + if(_gthis.clients.length > 0) { + return; + } + var tmp = _gthis.waitVideoStart; + if(tmp != null) { + tmp.stop(); + } + _gthis.videoTimer.pause(); + },5000); } haxe_Timer.delay(function() { if(Lambda.exists(_gthis.clients,function(i) { @@ -5156,7 +5170,7 @@ server_Main.prototype = { this.send(client,{ type : "LoginError"}); return; } - haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 538, className : "server.Main", methodName : "onMessage", customParams : ["Client " + client.name + " logged as " + name]}); + haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 552, className : "server.Main", methodName : "onMessage", customParams : ["Client " + client.name + " logged as " + name]}); client.name = name; client.setGroupFlag(ClientGroup.User,true); this.checkBan(client); @@ -5169,7 +5183,7 @@ server_Main.prototype = { var oldName = client.name; client.name = "Guest " + (this.clients.indexOf(client) + 1); client.setGroupFlag(ClientGroup.User,false); - haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 559, className : "server.Main", methodName : "onMessage", customParams : ["Client " + oldName + " logout to " + client.name]}); + haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 573, className : "server.Main", methodName : "onMessage", customParams : ["Client " + oldName + " logout to " + client.name]}); this.send(client,{ type : data.type, logout : { oldClientName : oldName, clientName : client.name, clients : this.clientList()}}); this.sendClientListExcept(client); break; @@ -5484,7 +5498,7 @@ server_Main.prototype = { client.setGroupFlag(ClientGroup.Banned,!isOutdated); if(isOutdated) { HxOverrides.remove(this.userList.bans,ban); - haxe_Log.trace("" + client.name + " ban removed",{ fileName : "src/server/Main.hx", lineNumber : 971, className : "server.Main", methodName : "checkBan"}); + haxe_Log.trace("" + client.name + " ban removed",{ fileName : "src/server/Main.hx", lineNumber : 985, className : "server.Main", methodName : "checkBan"}); this.sendClientList(); } break; @@ -5512,8 +5526,9 @@ server_Main.prototype = { } ,restartWaitTimer: function() { this.videoTimer.stop(); - if(this.waitVideoStart != null) { - this.waitVideoStart.stop(); + var tmp = this.waitVideoStart; + if(tmp != null) { + tmp.stop(); } this.waitVideoStart = haxe_Timer.delay($bind(this,this.startVideoPlayback),3000); } @@ -5530,8 +5545,9 @@ server_Main.prototype = { } } ,startVideoPlayback: function() { - if(this.waitVideoStart != null) { - this.waitVideoStart.stop(); + var tmp = this.waitVideoStart; + if(tmp != null) { + tmp.stop(); } this.loadedClientsCount = 0; this.broadcast({ type : "VideoLoaded"}); diff --git a/res/client.js b/res/client.js index 80f7597..fc9bc48 100644 --- a/res/client.js +++ b/res/client.js @@ -1,4 +1,4 @@ -// Generated by Haxe 4.3.5 +// Generated by Haxe 4.3.6 (function ($hx_exports, $global) { "use strict"; $hx_exports["client"] = $hx_exports["client"] || {}; $hx_exports["client"]["JsApi"] = $hx_exports["client"]["JsApi"] || {}; @@ -1254,6 +1254,7 @@ var client_Main = function() { this.matchSimpleDate = new EReg("^-?([0-9]+d)?([0-9]+h)?([0-9]+m)?([0-9]+s?)?$",""); this.mask = new EReg("\\${([0-9]+)-([0-9]+)}","g"); this.disabledReconnection = false; + this.gotInitialConnection = false; this.isConnected = false; this.personal = new Client("Unknown",0); this.filters = []; @@ -1355,19 +1356,34 @@ client_Main.prototype = { this.ws = new WebSocket("" + protocol + "//" + this.host + colonPort + path); this.ws.onmessage = $bind(this,this.onMessage); this.ws.onopen = function() { + var tmp = _gthis.disconnectNotification; + if(tmp != null) { + tmp.stop(); + } + _gthis.disconnectNotification = null; _gthis.chatMessageConnected(); + _gthis.gotInitialConnection = true; return _gthis.isConnected = true; }; this.ws.onclose = function() { - if(_gthis.isConnected) { - _gthis.chatMessageDisconnected(); - } _gthis.isConnected = false; - _gthis.player.pause(); + var notificationDelay = _gthis.gotInitialConnection ? 5000 : 0; + if(_gthis.disabledReconnection) { + notificationDelay = 0; + } + if(_gthis.disconnectNotification == null) { + _gthis.disconnectNotification = haxe_Timer.delay(function() { + if(_gthis.isConnected) { + return; + } + _gthis.chatMessageDisconnected(); + _gthis.player.pause(); + },notificationDelay); + } if(_gthis.disabledReconnection) { return; } - haxe_Timer.delay($bind(_gthis,_gthis.openWebSocket),2000); + haxe_Timer.delay($bind(_gthis,_gthis.openWebSocket),_gthis.gotInitialConnection ? 1000 : 2000); }; } ,initListeners: function() { @@ -1593,7 +1609,7 @@ client_Main.prototype = { var data = JSON.parse(e.data); if(this.config != null && this.config.isVerbose) { var t = data.type; - haxe_Log.trace("Event: " + data.type,{ fileName : "src/client/Main.hx", lineNumber : 404, className : "client.Main", methodName : "onMessage", customParams : [Reflect.field(data,t.charAt(0).toLowerCase() + HxOverrides.substr(t,1,null))]}); + haxe_Log.trace("Event: " + data.type,{ fileName : "src/client/Main.hx", lineNumber : 418, className : "client.Main", methodName : "onMessage", customParams : [Reflect.field(data,t.charAt(0).toLowerCase() + HxOverrides.substr(t,1,null))]}); } client_JsApi.fireOnceEvent(data); switch(data.type) { @@ -2029,19 +2045,35 @@ client_Main.prototype = { this.ws.send(JSON.stringify(data)); } ,chatMessageConnected: function() { + var msgBuf = window.document.querySelector("#messagebuffer"); + if(this.isLastMessageConnectionStatus()) { + msgBuf.removeChild(msgBuf.lastChild); + } var div = window.document.createElement("div"); div.className = "server-msg-reconnect"; div.textContent = Lang.get("msgConnected"); - window.document.querySelector("#messagebuffer").appendChild(div); + msgBuf.appendChild(div); this.scrollChatToEnd(); } ,chatMessageDisconnected: function() { + var msgBuf = window.document.querySelector("#messagebuffer"); + if(this.isLastMessageConnectionStatus()) { + msgBuf.removeChild(msgBuf.lastChild); + } var div = window.document.createElement("div"); div.className = "server-msg-disconnect"; div.textContent = Lang.get("msgDisconnected"); - window.document.querySelector("#messagebuffer").appendChild(div); + msgBuf.appendChild(div); this.scrollChatToEnd(); } + ,isLastMessageConnectionStatus: function() { + var tmp = window.document.querySelector("#messagebuffer").lastElementChild; + if(tmp != null) { + return StringTools.startsWith(tmp.className,"server-msg"); + } else { + return null; + } + } ,updateUserList: function() { window.document.querySelector("#usercount").textContent = this.clients.length + " " + Lang.get("online"); window.document.title = this.getPageTitle(); diff --git a/src/client/Main.hx b/src/client/Main.hx index cb74ad8..8cdd914 100644 --- a/src/client/Main.hx +++ b/src/client/Main.hx @@ -41,7 +41,9 @@ class Main { final filters:Array<{regex:EReg, replace:String}> = []; var personal = new Client("Unknown", 0); var isConnected = false; + var gotInitialConnection = false; var disabledReconnection = false; + var disconnectNotification:Null<Timer>; var ws:WebSocket; final player:Player; var onTimeGet:Timer; @@ -125,17 +127,29 @@ class Main { ws = new WebSocket('$protocol//$host$colonPort$path'); ws.onmessage = onMessage; ws.onopen = () -> { + disconnectNotification?.stop(); + disconnectNotification = null; chatMessageConnected(); + gotInitialConnection = true; isConnected = true; } + // if initial connection refused, or server/client is offline ws.onclose = () -> { - // if initial connection refused - // or server/client offline - if (isConnected) chatMessageDisconnected(); isConnected = false; - player.pause(); + var notificationDelay = gotInitialConnection ? 5000 : 0; + if (disabledReconnection) notificationDelay = 0; + + if (disconnectNotification == null) { + disconnectNotification = Timer.delay(() -> { + if (isConnected) return; + chatMessageDisconnected(); + player.pause(); + }, notificationDelay); + } + if (disabledReconnection) return; - Timer.delay(openWebSocket, 2000); + final reconnectionDelay = gotInitialConnection ? 1000 : 2000; + Timer.delay(openWebSocket, reconnectionDelay); } } @@ -803,23 +817,34 @@ class Main { } function chatMessageConnected():Void { + final msgBuf = ge("#messagebuffer"); + if (isLastMessageConnectionStatus()) { + msgBuf.removeChild(msgBuf.lastChild); + } final div = document.createDivElement(); div.className = "server-msg-reconnect"; div.textContent = Lang.get("msgConnected"); - final msgBuf = ge("#messagebuffer"); msgBuf.appendChild(div); scrollChatToEnd(); } function chatMessageDisconnected():Void { + final msgBuf = ge("#messagebuffer"); + if (isLastMessageConnectionStatus()) { + msgBuf.removeChild(msgBuf.lastChild); + } final div = document.createDivElement(); div.className = "server-msg-disconnect"; div.textContent = Lang.get("msgDisconnected"); - final msgBuf = ge("#messagebuffer"); msgBuf.appendChild(div); scrollChatToEnd(); } + function isLastMessageConnectionStatus():Bool { + final msgBuf = ge("#messagebuffer"); + return msgBuf.lastElementChild?.className.startsWith("server-msg"); + } + public static function serverMessage(text:String, isText = true, withTimestamp = true):Void { final div = document.createDivElement(); final time = Date.now().toString().split(" ")[1]; diff --git a/src/import.hx b/src/import.hx index 5517d1e..75f7907 100644 --- a/src/import.hx +++ b/src/import.hx @@ -1,2 +1,3 @@ using Lambda; using StringTools; +using tools.ArrayTools; diff --git a/src/server/Main.hx b/src/server/Main.hx index 1560f53..2b1aafa 100644 --- a/src/server/Main.hx +++ b/src/server/Main.hx @@ -33,6 +33,7 @@ class Main { static inline var VIDEO_SKIP_DELAY = 1000; static inline var FLASHBACKS_COUNT = 50; static inline var FLASHBACK_DIST = 30; + static inline var EMPTY_ROOM_CALLBACK_DELAY = 5000; final rootDir = '$__dirname/..'; @@ -56,6 +57,14 @@ class Main { final messages:Array<Message> = []; final flashbacks:Array<FlashbackItem> = []; final logger:Logger; + /** + Stop video timer after `EMPTY_ROOM_CALLBACK_DELAY` in case + if server loses connection to all clients for a moment. + + This allows seamless reconnection without rewinds + to stopped server time. + **/ + var emptyRoomCallbackTimer:Null<Timer>; static function main():Void { new Main({ @@ -426,6 +435,7 @@ class Main { switch (data.type) { case Connected: if (!internal) return; + emptyRoomCallbackTimer?.stop(); if (clients.length == 1 && videoList.length > 0) { if (videoTimer.isPaused()) videoTimer.play(); } @@ -457,8 +467,12 @@ class Main { if (videoTimer.isPaused()) videoTimer.play(); } if (clients.length == 0) { - if (waitVideoStart != null) waitVideoStart.stop(); - videoTimer.pause(); + emptyRoomCallbackTimer?.stop(); + emptyRoomCallbackTimer = Timer.delay(() -> { + if (clients.length > 0) return; + waitVideoStart?.stop(); + videoTimer.pause(); + }, EMPTY_ROOM_CALLBACK_DELAY); } Timer.delay(() -> { if (clients.exists(i -> i.name == client.name)) return; @@ -987,12 +1001,12 @@ class Main { return false; } - var waitVideoStart:Timer; + var waitVideoStart:Null<Timer>; var loadedClientsCount = 0; function restartWaitTimer():Void { videoTimer.stop(); - if (waitVideoStart != null) waitVideoStart.stop(); + waitVideoStart?.stop(); waitVideoStart = Timer.delay(startVideoPlayback, VIDEO_START_MAX_DELAY); } @@ -1004,7 +1018,7 @@ class Main { } function startVideoPlayback():Void { - if (waitVideoStart != null) waitVideoStart.stop(); + waitVideoStart?.stop(); loadedClientsCount = 0; broadcast({type: VideoLoaded}); videoTimer.start(); diff --git a/src/tools/ArrayTools.hx b/src/tools/ArrayTools.hx new file mode 100644 index 0000000..63b4839 --- /dev/null +++ b/src/tools/ArrayTools.hx @@ -0,0 +1,116 @@ +package tools; + +import utils.ArrayKeyValueReverseIterator; +import utils.ArrayReverseIterator; + +class ArrayTools { + public static function last<T>(arr:Array<T>):Null<T> { + return arr[arr.length - 1]; + } + + public static function min<T:Float>(arr:Array<T>, ?maxValue:T):T { + var min = arr[0] ?? maxValue; + for (value in arr) if (value < min) min = value; + return min; + } + + public static function max<T:Float>(arr:Array<T>, ?minValue:T):T { + var max = arr[0] ?? minValue; + for (value in arr) if (value > max) max = value; + return max; + } + + public static function indexOfMax<T:Float>(arr:Array<T>, ?minValue:T):Int { + if (arr.length == 0) return -1; + var max = arr[0] ?? minValue; + var maxIndex = 0; + for (i in 1...arr.length) { + if (arr[i] > max) { + maxIndex = i; + max = arr[i]; + } + } + return maxIndex; + } + + public static function sum<T:Float>(arr:Array<T>):T { + var total:T = cast 0; + for (value in arr) total += value; + return total; + } + + public static function shuffle<T>(arr:Array<T>):Void { + for (i => a in arr) { + final n = Std.random(arr.length); + final b = arr[n]; + arr[i] = b; + arr[n] = a; + } + } + + public static inline function reversed<T>(arr:Array<T>) { + return new ArrayReverseIterator(arr); + } + + /** Key-value reversed array iterator **/ + public static inline function reversedKV<T>(arr:Array<T>) { + return new ArrayKeyValueReverseIterator(arr); + } + + public static inline function findMin<T>( + arr:Array<T>, f:(item:T) -> Float, maxValue:Float + ):Null<T> { + var result:Null<T> = null; + for (item in arr) { + final dist = f(item); + if (dist > maxValue) continue; + maxValue = dist; + result = item; + } + return result; + } + + extern overload public static inline function inlineFind<T>(it:Array<T>, f:(item:T) -> Bool):Null<T> { + var result:Null<T> = null; + for (v in it) { + if (f(v)) { + result = v; + break; + } + } + return result; + } + + extern overload public static inline function inlineFind<T>(it:Iterable<T>, f:(item:T) -> Bool):Null<T> { + var result:Null<T> = null; + for (v in it) { + if (f(v)) { + result = v; + break; + } + } + return result; + } + + extern overload public static inline function inlineExists<T>(it:Array<T>, f:(item:T) -> Bool):Bool { + var result = false; + for (v in it) { + if (f(v)) { + result = true; + break; + } + } + return result; + } + + extern overload public static inline function inlineExists<T>(it:Iterable<T>, f:(item:T) -> Bool):Bool { + var result = false; + for (v in it) { + if (f(v)) { + result = true; + break; + } + } + return result; + } +} diff --git a/src/utils/ArrayKeyValueReverseIterator.hx b/src/utils/ArrayKeyValueReverseIterator.hx new file mode 100644 index 0000000..ad84e73 --- /dev/null +++ b/src/utils/ArrayKeyValueReverseIterator.hx @@ -0,0 +1,19 @@ +package utils; + +class ArrayKeyValueReverseIterator<T> { + final arr:Array<T>; + var i:Int; + + public inline function new(arr:Array<T>) { + this.arr = arr; + this.i = this.arr.length - 1; + } + + public inline function hasNext() { + return i > -1; + } + + public inline function next() { + return {value: arr[i], key: i--}; + } +} diff --git a/src/utils/ArrayReverseIterator.hx b/src/utils/ArrayReverseIterator.hx new file mode 100644 index 0000000..9976c6a --- /dev/null +++ b/src/utils/ArrayReverseIterator.hx @@ -0,0 +1,19 @@ +package utils; + +class ArrayReverseIterator<T> { + final arr:Array<T>; + var i:Int; + + public inline function new(arr:Array<T>) { + this.arr = arr; + this.i = this.arr.length - 1; + } + + public inline function hasNext() { + return i > -1; + } + + public inline function next() { + return arr[i--]; + } +} |
