diff options
| author | RblSb <msrblsb@gmail.com> | 2025-01-25 15:32:55 +0300 |
|---|---|---|
| committer | RblSb <msrblsb@gmail.com> | 2025-01-25 22:52:14 +0300 |
| commit | c7518e58788c17ad2ca8340ab5c7633489aa9518 (patch) | |
| tree | 79b283db70952fef4d9d930cb1dab4a554a377d1 | |
| parent | 6ead98595d71afba9d11d3300756b46f18fd6bda (diff) | |
Add experimental unpauseWithoutLeader option
Also extended requestLeaderOnPause, so you can unpause when someone else is a leader by video click with this one. Only works for raw videos and doesn't work in FF, so there is new unsafe option that can break everything with enough clients.
There is also leader button hints when you touch player as non-leader for easier ux.
| -rw-r--r-- | build/server.js | 9 | ||||
| -rw-r--r-- | default-config.json | 1 | ||||
| -rw-r--r-- | res/client.js | 111 | ||||
| -rw-r--r-- | res/css/des.css | 2 | ||||
| -rw-r--r-- | src/Types.hx | 1 | ||||
| -rw-r--r-- | src/client/Main.hx | 62 | ||||
| -rw-r--r-- | src/client/Player.hx | 60 | ||||
| -rw-r--r-- | src/client/Utils.hx | 5 | ||||
| -rw-r--r-- | src/client/players/Raw.hx | 2 | ||||
| -rw-r--r-- | src/client/players/Vk.hx | 2 | ||||
| -rw-r--r-- | src/client/players/Youtube.hx | 6 |
11 files changed, 202 insertions, 59 deletions
diff --git a/build/server.js b/build/server.js index 8b24397..e2f25cc 100644 --- a/build/server.js +++ b/build/server.js @@ -1624,7 +1624,7 @@ JsonParser_$62.__name__ = true; JsonParser_$62.__super__ = json2object_reader_BaseParser; JsonParser_$62.prototype = $extend(json2object_reader_BaseParser.prototype,{ onIncorrectType: function(pos,variable) { - this.errors.push(json2object_Error.IncorrectType(variable,"{ youtubePlaylistLimit : Int, youtubeApiKey : String, userVideoLimit : Int, totalVideoLimit : Int, templateUrl : String, serverChatHistory : Int, ?salt : Null<String>, requestLeaderOnPause : Bool, port : Int, permissions : Permissions, maxMessageLength : Int, maxLoginLength : Int, localNetworkOnly : Bool, localAdmins : Bool, ?isVerbose : Null<Bool>, filters : Array<Filter>, emotes : Array<Emote>, channelName : String, cacheStorageLimitGiB : Float, allowProxyIps : Bool }",pos)); + this.errors.push(json2object_Error.IncorrectType(variable,"{ youtubePlaylistLimit : Int, youtubeApiKey : String, userVideoLimit : Int, unpauseWithoutLeader : Bool, totalVideoLimit : Int, templateUrl : String, serverChatHistory : Int, ?salt : Null<String>, requestLeaderOnPause : Bool, port : Int, permissions : Permissions, maxMessageLength : Int, maxLoginLength : Int, localNetworkOnly : Bool, localAdmins : Bool, ?isVerbose : Null<Bool>, filters : Array<Filter>, emotes : Array<Emote>, channelName : String, cacheStorageLimitGiB : Float, allowProxyIps : Bool }",pos)); json2object_reader_BaseParser.prototype.onIncorrectType.call(this,pos,variable); } ,loadJsonNull: function(pos,variable) { @@ -1632,7 +1632,7 @@ JsonParser_$62.prototype = $extend(json2object_reader_BaseParser.prototype,{ } ,loadJsonObject: function(o,pos,variable) { var assigned = new haxe_ds_StringMap(); - this.objectSetupAssign(assigned,["allowProxyIps","cacheStorageLimitGiB","channelName","emotes","filters","isVerbose","localAdmins","localNetworkOnly","maxLoginLength","maxMessageLength","permissions","port","requestLeaderOnPause","salt","serverChatHistory","templateUrl","totalVideoLimit","userVideoLimit","youtubeApiKey","youtubePlaylistLimit"],[false,false,false,false,false,true,false,false,false,false,false,false,false,true,false,false,false,false,false,false]); + this.objectSetupAssign(assigned,["allowProxyIps","cacheStorageLimitGiB","channelName","emotes","filters","isVerbose","localAdmins","localNetworkOnly","maxLoginLength","maxMessageLength","permissions","port","requestLeaderOnPause","salt","serverChatHistory","templateUrl","totalVideoLimit","unpauseWithoutLeader","userVideoLimit","youtubeApiKey","youtubePlaylistLimit"],[false,false,false,false,false,true,false,false,false,false,false,false,false,true,false,false,false,false,false,false,false]); this.value = this.getAuto(); var _g = 0; while(_g < o.length) { @@ -1690,6 +1690,9 @@ JsonParser_$62.prototype = $extend(json2object_reader_BaseParser.prototype,{ case "totalVideoLimit": this.value.totalVideoLimit = this.loadObjectField(($_=new JsonParser_$54(this.errors,this.putils,1),$bind($_,$_.loadJson)),field,"totalVideoLimit",assigned,this.value.totalVideoLimit,pos); break; + case "unpauseWithoutLeader": + this.value.unpauseWithoutLeader = this.loadObjectField(($_=new JsonParser_$46(this.errors,this.putils,1),$bind($_,$_.loadJson)),field,"unpauseWithoutLeader",assigned,this.value.unpauseWithoutLeader,pos); + break; case "userVideoLimit": this.value.userVideoLimit = this.loadObjectField(($_=new JsonParser_$54(this.errors,this.putils,1),$bind($_,$_.loadJson)),field,"userVideoLimit",assigned,this.value.userVideoLimit,pos); break; @@ -1706,7 +1709,7 @@ JsonParser_$62.prototype = $extend(json2object_reader_BaseParser.prototype,{ this.objectErrors(assigned,pos); } ,getAuto: function() { - return { allowProxyIps : new JsonParser_$46([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), cacheStorageLimitGiB : new JsonParser_$47([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), channelName : new JsonParser_$45([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), emotes : new JsonParser_$67([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), filters : new JsonParser_$68([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), isVerbose : new JsonParser_$58([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), localAdmins : new JsonParser_$46([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), localNetworkOnly : new JsonParser_$46([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), maxLoginLength : new JsonParser_$54([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), maxMessageLength : new JsonParser_$54([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), permissions : new JsonParser_$70([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), port : new JsonParser_$54([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), requestLeaderOnPause : new JsonParser_$46([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), salt : new JsonParser_$50([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), serverChatHistory : new JsonParser_$54([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), templateUrl : new JsonParser_$45([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), totalVideoLimit : new JsonParser_$54([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), userVideoLimit : new JsonParser_$54([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), youtubeApiKey : new JsonParser_$45([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), youtubePlaylistLimit : new JsonParser_$54([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1)))}; + return { allowProxyIps : new JsonParser_$46([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), cacheStorageLimitGiB : new JsonParser_$47([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), channelName : new JsonParser_$45([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), emotes : new JsonParser_$67([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), filters : new JsonParser_$68([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), isVerbose : new JsonParser_$58([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), localAdmins : new JsonParser_$46([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), localNetworkOnly : new JsonParser_$46([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), maxLoginLength : new JsonParser_$54([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), maxMessageLength : new JsonParser_$54([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), permissions : new JsonParser_$70([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), port : new JsonParser_$54([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), requestLeaderOnPause : new JsonParser_$46([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), salt : new JsonParser_$50([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), serverChatHistory : new JsonParser_$54([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), templateUrl : new JsonParser_$45([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), totalVideoLimit : new JsonParser_$54([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), unpauseWithoutLeader : new JsonParser_$46([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), userVideoLimit : new JsonParser_$54([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), youtubeApiKey : new JsonParser_$45([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), youtubePlaylistLimit : new JsonParser_$54([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1)))}; } ,__class__: JsonParser_$62 }); diff --git a/default-config.json b/default-config.json index 15dfc6c..38fc271 100644 --- a/default-config.json +++ b/default-config.json @@ -7,6 +7,7 @@ "totalVideoLimit": 0, "userVideoLimit": 0, "requestLeaderOnPause": false, + "unpauseWithoutLeader": false, "localAdmins": true, "allowProxyIps": true, "localNetworkOnly": false, diff --git a/res/client.js b/res/client.js index febaff7..b009967 100644 --- a/res/client.js +++ b/res/client.js @@ -1290,7 +1290,9 @@ var client_Main = function() { this.filters = []; this.pageTitle = window.document.title; this.clients = []; + this.lastStateTimeStamp = 0.0; this.lastState = { time : 0, rate : 1.0, paused : false, pausedByServer : false}; + this.timeFromLastState = 0.0; this.showingServerPause = false; this.playersCacheSupport = []; this.isPlaylistOpen = true; @@ -1635,7 +1637,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 : 454, 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 : 458, 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) { @@ -1676,6 +1678,7 @@ client_Main.prototype = { this.lastState.paused = data.getTime.paused; this.lastState.pausedByServer = data.getTime.pausedByServer; this.lastState.rate = data.getTime.rate; + this.updateLastStateTime(); if(isPauseChanged) { this.updateUserList(); } @@ -1751,7 +1754,9 @@ client_Main.prototype = { this.showGuestPasswordPanel(); break; case "Pause": + this.lastState.time = data.pause.time; this.lastState.paused = true; + this.updateLastStateTime(); this.player.setPauseIndicator(this.lastState.paused); this.updateUserList(); if((this.personal.group & 4) != 0) { @@ -1761,7 +1766,9 @@ client_Main.prototype = { this.player.setTime(data.pause.time); break; case "Play": + this.lastState.time = data.play.time; this.lastState.paused = false; + this.updateLastStateTime(); this.player.setPauseIndicator(this.lastState.paused); this.updateUserList(); if((this.personal.group & 4) != 0) { @@ -1784,6 +1791,8 @@ client_Main.prototype = { } break; case "Rewind": + this.lastState.time = data.rewind.time; + this.updateLastStateTime(); this.player.setTime(data.rewind.time + 0.5); break; case "ServerMessage": @@ -1808,6 +1817,8 @@ client_Main.prototype = { this.player.setPlaybackRate(data.setRate.rate); break; case "SetTime": + this.lastState.time = data.setTime.time; + this.updateLastStateTime(); var synchThreshold = this.settings.synchThreshold; var newTime = data.setTime.time; if(Math.abs(this.player.getTime() - newTime) < synchThreshold) { @@ -1841,6 +1852,10 @@ client_Main.prototype = { this.player.setItems(data.updatePlaylist.videoList); break; case "VideoLoaded": + this.lastState.paused = false; + this.lastState.pausedByServer = false; + this.lastState.time = 0; + this.updateLastStateTime(); this.player.setTime(0); this.player.play(); if((this.personal.group & 4) != 0 && !this.player.isVideoLoaded()) { @@ -1849,6 +1864,13 @@ client_Main.prototype = { break; } } + ,updateLastStateTime: function() { + if(this.lastStateTimeStamp == 0) { + this.lastStateTimeStamp = HxOverrides.now() / 1000; + } + this.timeFromLastState = HxOverrides.now() / 1000 - this.lastStateTimeStamp; + this.lastStateTimeStamp = HxOverrides.now() / 1000; + } ,onConnected: function(data) { var connected = data.connected; this.settings.uuid = connected.uuid; @@ -1924,10 +1946,7 @@ client_Main.prototype = { window.document.querySelector("#requestLeaderHintButton").onclick = function(e) { window.scrollTo(0,0); if(client_Utils.isTouch()) { - window.document.querySelector("#leader_btn").classList.add("hint"); - haxe_Timer.delay(function() { - window.document.querySelector("#leader_btn").classList.remove("hint"); - },1000); + _gthis.blinkLeaderButton(); } }; window.document.querySelector("#requestLeaderHintButton").onpointerenter = function(e) { @@ -1962,6 +1981,12 @@ client_Main.prototype = { client_Settings.write(_gthis.settings); }; } + ,blinkLeaderButton: function() { + window.document.querySelector("#leader_btn").classList.add("hint"); + haxe_Timer.delay(function() { + window.document.querySelector("#leader_btn").classList.remove("hint"); + },500); + } ,onUserGroupChanged: function() { var button = window.document.querySelector("#queue_next"); if(this.personal.hasPermission("changeOrder",this.config.permissions)) { @@ -2006,6 +2031,7 @@ client_Main.prototype = { this.config = config; if(client_Utils.isTouch()) { config.requestLeaderOnPause = false; + config.unpauseWithoutLeader = false; } this.pageTitle = config.channelName; window.document.querySelector("#guestname").maxLength = config.maxLoginLength; @@ -2271,7 +2297,7 @@ client_Main.prototype = { _gthis.hideDynamicChin(); _gthis.send({ type : "SetLeader", setLeader : { clientName : _gthis.personal.name}}); client_JsApi.once("SetLeader",function(event) { - _gthis.send({ type : "SetLeader", setLeader : { clientName : ""}}); + _gthis.removeLeader(); }); }; chin.style.display = ""; @@ -2499,6 +2525,9 @@ client_Main.prototype = { this.setLeaderButton((this.personal.group & 4) == 0); this.send({ type : "SetLeader", setLeader : { clientName : (this.personal.group & 4) != 0 ? "" : this.personal.name}}); } + ,removeLeader: function() { + this.send({ type : "SetLeader", setLeader : { clientName : ""}}); + } ,toggleLeaderAndPause: function() { var _gthis = this; if((this.personal.group & 4) == 0) { @@ -2516,6 +2545,9 @@ client_Main.prototype = { ,hasLeaderOnPauseRequest: function() { return this.config.requestLeaderOnPause; } + ,hasUnpauseWithoutLeader: function() { + return this.config.unpauseWithoutLeader; + } ,getTemplateUrl: function() { return this.config.templateUrl; } @@ -2541,6 +2573,7 @@ client_Main.prototype = { } }; var client_Player = function(main) { + this.inUserInteraction = false; this.voiceOverVolume = 0.3; this.needsVolumeReset = false; this.isAudioTrackLoaded = false; @@ -2575,6 +2608,12 @@ var client_Player = function(main) { client_Buttons.onViewportResize(); }; } + this.playerEl.addEventListener("click",function(e) { + _gthis.inUserInteraction = true; + return haxe_Timer.delay(function() { + _gthis.inUserInteraction = false; + },350); + },{ }); }; client_Player.__name__ = true; client_Player.prototype = { @@ -2705,7 +2744,7 @@ client_Player.prototype = { return _gthis.isAudioTrackLoaded = true; }; this.audioTrack.onerror = function(e) { - haxe_Log.trace(e,{ fileName : "src/client/Player.hx", lineNumber : 205, className : "client.Player", methodName : "setExternalAudioTrack"}); + haxe_Log.trace(e,{ fileName : "src/client/Player.hx", lineNumber : 215, className : "client.Player", methodName : "setExternalAudioTrack"}); _gthis.audioTrack.oncanplay = null; _gthis.audioTrack.onerror = null; _gthis.isAudioTrackLoaded = false; @@ -2762,9 +2801,10 @@ client_Player.prototype = { if(!this.main.isSyncActive) { return; } - window.document.querySelector("#pause-indicator").setAttribute("name",isPause ? "pause" : "play"); + var state = isPause ? "pause" : "play"; + window.document.querySelector("#pause-indicator").setAttribute("name",state); var el2 = window.document.querySelector("#pause-indicator-portrait"); - el2.setAttribute("name","pause"); + el2.setAttribute("name",state); var tmp = isPause || this.main.hasLeader() ? "" : "none"; el2.style.display = tmp; } @@ -2780,14 +2820,24 @@ client_Player.prototype = { if(tmp != null) { tmp.play(); } + if(!this.isLoaded) { + return; + } + if(this.videoList.items.length == 0) { + return; + } + var hasAutoPause = this.main.hasLeaderOnPauseRequest(); if((this.main.personal.group & 4) == 0) { - if(this.main.lastState.paused) { + if(hasAutoPause && this.inUserInteraction || this.main.hasUnpauseWithoutLeader()) { + this.main.removeLeader(); + } else if(this.main.lastState.paused) { this.pause(); + this.main.blinkLeaderButton(); } return; } this.main.send({ type : "Play", play : { time : this.getTime()}}); - if(this.main.hasLeaderOnPauseRequest() && this.videoList.items.length > 0) { + if(hasAutoPause) { if(this.main.hasPermission("requestLeader")) { this.main.toggleLeader(); } @@ -2799,6 +2849,9 @@ client_Player.prototype = { if(tmp != null) { tmp.pause(); } + if(!this.isLoaded) { + return; + } var _this = this.videoList; var tmp = _this.items[_this.pos]; if(tmp == null) { @@ -2825,6 +2878,7 @@ client_Player.prototype = { if((this.main.personal.group & 4) == 0) { if(!this.main.lastState.paused) { this.play(); + this.main.blinkLeaderButton(); } return; } @@ -2838,7 +2892,17 @@ client_Player.prototype = { this.skipSetTime = false; return; } + if(this.videoList.items.length == 0) { + return; + } if((this.main.personal.group & 4) == 0) { + if(this.main.hasLeader() || this.main.lastState.pausedByServer) { + this.isPaused(); + var time = this.main.lastState.time; + this.getTime(); + this.setTime(time); + this.main.blinkLeaderButton(); + } return; } this.main.send({ type : "SetTime", setTime : { time : this.getTime()}}); @@ -2851,7 +2915,11 @@ client_Player.prototype = { this.skipSetRate = false; return; } + if(this.videoList.items.length == 0) { + return; + } if((this.main.personal.group & 4) == 0) { + this.main.blinkLeaderButton(); return; } this.main.send({ type : "SetRate", setRate : { rate : this.getPlaybackRate()}}); @@ -3168,7 +3236,7 @@ client_Player.prototype = { } }; http.onError = function(msg) { - haxe_Log.trace(msg,{ fileName : "src/client/Player.hx", lineNumber : 608, className : "client.Player", methodName : "skipAd"}); + haxe_Log.trace(msg,{ fileName : "src/client/Player.hx", lineNumber : 652, className : "client.Player", methodName : "skipAd"}); }; http.request(); } @@ -3310,13 +3378,6 @@ client_Utils.outerHeight = function(el) { var style = window.getComputedStyle(el); return el.getBoundingClientRect().height + parseFloat(style.marginTop) + parseFloat(style.marginBottom); }; -client_Utils.prepend = function(parent,child) { - if(parent.firstChild == null) { - parent.appendChild(child); - } else { - parent.insertBefore(child,parent.firstChild); - } -}; client_Utils.insertAtIndex = function(parent,child,i) { if(i >= parent.children.length) { parent.appendChild(child); @@ -3561,7 +3622,7 @@ client_players_Raw.prototype = { } callback({ duration : video.duration, title : title, subs : subs}); }; - client_Utils.prepend(this.playerEl,video); + this.playerEl.prepend(video); if(isHls) { this.initHlsSource(video,url); } @@ -4055,7 +4116,7 @@ client_players_Vk.prototype = { return; } var tempVideo = client_Utils.nodeFromString(StringTools.trim("<iframe id=\"temp-videoplayer\" src=\"https://vk.com/video_ext.php?oid=" + ids.oid + "&id=" + ids.id + "&hd=1&js_api=1\"\n\t\t\t\tallow=\"autoplay; encrypted-media; fullscreen; picture-in-picture;\"\n\t\t\t\tframeborder=\"0\" allowfullscreen>\n\t\t\t</iframe>")); - client_Utils.prepend(this.playerEl,tempVideo); + this.playerEl.prepend(tempVideo); var tempVkPlayer = this.createVkPlayer(tempVideo); tempVkPlayer.on("inited",function() { callback({ duration : tempVkPlayer.getDuration(), title : "VK media", url : url}); @@ -4320,7 +4381,7 @@ client_players_Youtube.prototype = { } var video = window.document.createElement("div"); video.id = "temp-videoplayer"; - client_Utils.prepend(this.playerEl,video); + this.playerEl.prepend(video); var tempYoutube = null; tempYoutube = new YT.Player(video.id,{ videoId : this.extractVideoId(url), playerVars : { modestbranding : 1, rel : 0, showinfo : 0}, events : { onReady : function(e) { if(_gthis.playerEl.contains(video)) { @@ -4358,11 +4419,13 @@ client_players_Youtube.prototype = { e.target.mute(); } _gthis.isLoaded = true; - _gthis.youtube.pauseVideo(); + if(_gthis.main.lastState.paused) { + _gthis.youtube.pauseVideo(); + } + _gthis.player.onCanBePlayed(); }, onStateChange : function(e) { switch(e.data) { case -1: - _gthis.player.onCanBePlayed(); break; case 0: break; diff --git a/res/css/des.css b/res/css/des.css index 10c246c..8c7f0cc 100644 --- a/res/css/des.css +++ b/res/css/des.css @@ -203,7 +203,7 @@ button span { #leader_btn { border: .125rem solid; border-color: transparent; - transition: border-color ease-in-out 800ms; + transition: border-color ease-in-out 500ms; } #leader_btn.hint { diff --git a/src/Types.hx b/src/Types.hx index fdb3ba9..ec09b5c 100644 --- a/src/Types.hx +++ b/src/Types.hx @@ -32,6 +32,7 @@ typedef Config = { totalVideoLimit:Int, userVideoLimit:Int, requestLeaderOnPause:Bool, + unpauseWithoutLeader:Bool, localAdmins:Bool, allowProxyIps:Bool, localNetworkOnly:Bool, diff --git a/src/client/Main.hx b/src/client/Main.hx index 62fa7de..6ec8727 100644 --- a/src/client/Main.hx +++ b/src/client/Main.hx @@ -40,6 +40,8 @@ class Main { public var isPlaylistOpen(default, null) = true; public var playersCacheSupport(default, null):Array<PlayerType> = []; public var showingServerPause(default, null) = false; + /** How much time passed from last GetTime **/ + public var timeFromLastState(default, null) = 0.0; public final lastState:GetTimeEvent = { time: 0, rate: 1.0, @@ -47,6 +49,8 @@ class Main { pausedByServer: false }; + var lastStateTimeStamp = 0.0; + final clients:Array<Client> = []; var pageTitle = document.title; var config:Null<Config>; @@ -510,6 +514,10 @@ class Main { if (player.itemsLength() == 1) player.setVideo(0); case VideoLoaded: + lastState.paused = false; + lastState.pausedByServer = false; + lastState.time = 0; + updateLastStateTime(); player.setTime(0); player.play(); // try to sync leader after with GetTime events @@ -524,7 +532,9 @@ class Main { if (player.isListEmpty()) player.pause(); case Pause: + lastState.time = data.pause.time; lastState.paused = true; + updateLastStateTime(); player.setPauseIndicator(lastState.paused); updateUserList(); if (isLeader()) return; @@ -532,7 +542,9 @@ class Main { player.setTime(data.pause.time); case Play: + lastState.time = data.play.time; lastState.paused = false; + updateLastStateTime(); player.setPauseIndicator(lastState.paused); updateUserList(); if (isLeader()) return; @@ -554,6 +566,7 @@ class Main { lastState.paused = data.getTime.paused; lastState.pausedByServer = data.getTime.pausedByServer; lastState.rate = data.getTime.rate; + updateLastStateTime(); if (isPauseChanged) updateUserList(); @@ -592,6 +605,8 @@ class Main { else player.setTime(newTime); case SetTime: + lastState.time = data.setTime.time; + updateLastStateTime(); final synchThreshold = settings.synchThreshold; final newTime = data.setTime.time; final time = player.getTime(); @@ -603,6 +618,8 @@ class Main { player.setPlaybackRate(data.setRate.rate); case Rewind: + lastState.time = data.rewind.time; + updateLastStateTime(); player.setTime(data.rewind.time + 0.5); case Flashback: // server-only @@ -640,6 +657,14 @@ class Main { } } + function updateLastStateTime():Void { + if (lastStateTimeStamp == 0) { + lastStateTimeStamp = Timer.stamp(); + } + timeFromLastState = Timer.stamp() - lastStateTimeStamp; + lastStateTimeStamp = Timer.stamp(); + } + function onConnected(data:WsEvent):Void { final connected = data.connected; @@ -713,10 +738,7 @@ class Main { } ge("#requestLeaderHintButton").onclick = (e:MouseEvent) -> { window.scrollTo(0, 0); - if (Utils.isTouch()) { - ge("#leader_btn").classList.add("hint"); - Timer.delay(() -> ge("#leader_btn").classList.remove("hint"), 1000); - } + if (Utils.isTouch()) blinkLeaderButton(); } ge("#requestLeaderHintButton").onpointerenter = e -> { if (Utils.isTouch()) return; @@ -744,6 +766,11 @@ class Main { } } + public function blinkLeaderButton():Void { + ge("#leader_btn").classList.add("hint"); + Timer.delay(() -> ge("#leader_btn").classList.remove("hint"), 500); + } + function onUserGroupChanged():Void { final button:ButtonElement = cast ge("#queue_next"); if (personal.hasPermission(ChangeOrderPerm, config.permissions)) { @@ -790,7 +817,10 @@ class Main { function setConfig(config:Config):Void { this.config = config; - if (Utils.isTouch()) config.requestLeaderOnPause = false; + if (Utils.isTouch()) { + config.requestLeaderOnPause = false; + config.unpauseWithoutLeader = false; + } pageTitle = config.channelName; final login:InputElement = cast ge("#guestname"); login.maxLength = config.maxLoginLength; @@ -1063,14 +1093,7 @@ class Main { clientName: personal.name } }); - JsApi.once(SetLeader, event -> { - send({ - type: SetLeader, - setLeader: { - clientName: "" - } - }); - }); + JsApi.once(SetLeader, event -> removeLeader()); } chin.style.display = ""; @@ -1318,6 +1341,15 @@ class Main { }); } + public function removeLeader():Void { + send({ + type: SetLeader, + setLeader: { + clientName: "" + } + }); + } + public function toggleLeaderAndPause():Void { if (!isLeader()) { JsApi.once(SetLeader, event -> { @@ -1336,6 +1368,10 @@ class Main { return config.requestLeaderOnPause; } + public function hasUnpauseWithoutLeader():Bool { + return config.unpauseWithoutLeader; + } + public function getTemplateUrl():String { return config.templateUrl; } diff --git a/src/client/Player.hx b/src/client/Player.hx index 911092e..92f34e9 100644 --- a/src/client/Player.hx +++ b/src/client/Player.hx @@ -12,6 +12,7 @@ import client.players.Vk; import client.players.Youtube; import haxe.Http; import haxe.Json; +import haxe.Timer; import js.html.Audio; import js.html.Element; import js.html.InputElement; @@ -37,6 +38,9 @@ class Player { var needsVolumeReset = false; final voiceOverVolume = 0.3; + /** If player was clicked and pause/play event was not generated by browser events. **/ + public var inUserInteraction = false; + public function new(main:Main):Void { this.main = main; youtube = new Youtube(main, this); @@ -57,12 +61,18 @@ class Player { if (resizeObserver != null) { resizeObserver.observe(playerEl); } else { - final timer = new haxe.Timer(50); + final timer = new Timer(50); timer.run = () -> { if (isLoaded) return; Buttons.onViewportResize(); } } + + playerEl.addEventListener("click", e -> { + inUserInteraction = true; + // for some reason Chrome has ~300ms event delay + Timer.delay(() -> inUserInteraction = false, 350); + }, {}); } function initItemButtons():Void { @@ -249,7 +259,7 @@ class Player { el.setAttribute("name", state); final el2 = ge("#pause-indicator-portrait"); - el2.setAttribute("name", "pause"); + el2.setAttribute("name", state); var isVisible = isPause || main.hasLeader(); el2.style.display = isVisible ? "" : "none"; } @@ -262,10 +272,23 @@ class Player { public function onPlay():Void { audioTrack?.play(); + if (!isLoaded) return; + if (videoList.length == 0) return; + final hasAutoPause = main.hasLeaderOnPauseRequest(); if (!main.isLeader()) { - // paused and no leader - instant pause - if (main.lastState.paused) pause(); + // user click, so we can unpause by removing leader + // (doesn't work in Firefox because of no video click propagation) + final allowUnpause = (hasAutoPause && inUserInteraction); + if (allowUnpause || main.hasUnpauseWithoutLeader()) { + main.removeLeader(); + } else { + // paused and no leader - instant pause + if (main.lastState.paused) { + pause(); + main.blinkLeaderButton(); + } + } return; } main.send({ @@ -274,7 +297,6 @@ class Player { time: getTime() } }); - final hasAutoPause = main.hasLeaderOnPauseRequest() && videoList.length > 0; if (hasAutoPause) { // do not remove leader if user cannot request it back if (main.hasPermission(RequestLeaderPerm)) main.toggleLeader(); @@ -284,6 +306,7 @@ class Player { public function onPause():Void { audioTrack?.pause(); + if (!isLoaded) return; final item = videoList.currentItem ?? return; // do not send pause if video is ended if (getTime() >= item.duration - 0.01) return; @@ -311,7 +334,10 @@ class Player { } if (!main.isLeader()) { // no pause and no permission - instant play - if (!main.lastState.paused) play(); + if (!main.lastState.paused) { + play(); + main.blinkLeaderButton(); + } return; } // we are leader, so just send pause @@ -332,7 +358,21 @@ class Player { skipSetTime = false; return; } - if (!main.isLeader()) return; + if (videoList.length == 0) return; + if (!main.isLeader()) { + if (main.hasLeader() || main.lastState.pausedByServer) { + final off = isPaused() ? 0 : main.timeFromLastState; + final time = main.lastState.time; + final delta = Math.abs(time - getTime()); + setTime(time); + main.blinkLeaderButton(); + } else { + // we dont want to seek back here because + // after seek can happen pause that will give auto-leader, + // so seeking will work + } + return; + } main.send({ type: SetTime, setTime: { @@ -349,7 +389,11 @@ class Player { skipSetRate = false; return; } - if (!main.isLeader()) return; + if (videoList.length == 0) return; + if (!main.isLeader()) { + main.blinkLeaderButton(); + return; + } main.send({ type: SetRate, setRate: { diff --git a/src/client/Utils.hx b/src/client/Utils.hx index 717d64f..4d85697 100644 --- a/src/client/Utils.hx +++ b/src/client/Utils.hx @@ -54,11 +54,6 @@ class Utils { + Std.parseFloat(style.marginBottom)); } - public static function prepend(parent:Element, child:Element):Void { - if (parent.firstChild == null) parent.appendChild(child); - else parent.insertBefore(child, parent.firstChild); - } - public static function insertAtIndex(parent:Element, child:Element, i:Int) { if (i >= parent.children.length) parent.appendChild(child); else parent.insertBefore(child, parent.children[i]); diff --git a/src/client/players/Raw.hx b/src/client/players/Raw.hx index 01752e7..f054f14 100644 --- a/src/client/players/Raw.hx +++ b/src/client/players/Raw.hx @@ -76,7 +76,7 @@ class Raw implements IPlayer { subs: subs, }); } - Utils.prepend(playerEl, video); + playerEl.prepend(video); if (isHls) initHlsSource(video, url); } diff --git a/src/client/players/Vk.hx b/src/client/players/Vk.hx index fbcf60c..599b5eb 100644 --- a/src/client/players/Vk.hx +++ b/src/client/players/Vk.hx @@ -107,7 +107,7 @@ class Vk implements IPlayer { frameborder="0" allowfullscreen> </iframe>'.trim() ); - Utils.prepend(playerEl, tempVideo); + playerEl.prepend(tempVideo); final tempVkPlayer = createVkPlayer(tempVideo); tempVkPlayer.on("inited", () -> { callback({ diff --git a/src/client/players/Youtube.hx b/src/client/players/Youtube.hx index b30dfb3..f5fb88c 100644 --- a/src/client/players/Youtube.hx +++ b/src/client/players/Youtube.hx @@ -175,7 +175,7 @@ class Youtube implements IPlayer { } final video = document.createDivElement(); video.id = "temp-videoplayer"; - Utils.prepend(playerEl, video); + playerEl.prepend(video); var tempYoutube:YoutubePlayer = null; tempYoutube = new YoutubePlayer(video.id, { videoId: extractVideoId(url), @@ -232,12 +232,12 @@ class Youtube implements IPlayer { onReady: e -> { if (!main.isAutoplayAllowed()) e.target.mute(); isLoaded = true; - youtube.pauseVideo(); + if (main.lastState.paused) youtube.pauseVideo(); + player.onCanBePlayed(); }, onStateChange: e -> { switch (e.data) { case UNSTARTED: - player.onCanBePlayed(); case ENDED: case PLAYING: player.onPlay(); |
