diff options
| author | RblSb <msrblsb@gmail.com> | 2025-01-12 19:35:56 +0300 |
|---|---|---|
| committer | RblSb <msrblsb@gmail.com> | 2025-01-12 22:35:22 +0300 |
| commit | f84fdc40ba817b6a2d907484b1e1500197ceeafe (patch) | |
| tree | 73a5b81e082d0ac1741c24742db12e6c2bd54249 | |
| parent | 25b7ecb45d43018235c6a8eb5b4ce833f2dec668 (diff) | |
External audiotrack support
This works as voice over if video also has audio, changing video volume to 0.3.
Also improve autoplay by playing videos muted and unmute on first page click.
There is no mute if you use Firefox and allow autoplay on page (navigator.getAutoplayPolicy check).
| -rw-r--r-- | .vscode/extensions.json | 6 | ||||
| -rw-r--r-- | build/server.js | 46 | ||||
| -rw-r--r-- | res/client.js | 252 | ||||
| -rw-r--r-- | res/css/des.css | 6 | ||||
| -rw-r--r-- | res/index.html | 3 | ||||
| -rw-r--r-- | res/langs/en.json | 1 | ||||
| -rw-r--r-- | res/langs/ru.json | 5 | ||||
| -rw-r--r-- | src/Types.hx | 3 | ||||
| -rw-r--r-- | src/client/Buttons.hx | 1 | ||||
| -rw-r--r-- | src/client/IPlayer.hx | 4 | ||||
| -rw-r--r-- | src/client/Main.hx | 29 | ||||
| -rw-r--r-- | src/client/Player.hx | 124 | ||||
| -rw-r--r-- | src/client/players/Iframe.hx | 15 | ||||
| -rw-r--r-- | src/client/players/Raw.hx | 19 | ||||
| -rw-r--r-- | src/client/players/Youtube.hx | 26 | ||||
| -rw-r--r-- | src/server/Main.hx | 2 |
16 files changed, 481 insertions, 61 deletions
diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index fc94805..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "recommendations": [ - "nadako.vshaxe", - "hookyqr.beautify" - ] -} diff --git a/build/server.js b/build/server.js index cc6ce37..2646c9f 100644 --- a/build/server.js +++ b/build/server.js @@ -1314,7 +1314,7 @@ JsonParser_$43.__name__ = true; JsonParser_$43.__super__ = json2object_reader_BaseParser; JsonParser_$43.prototype = $extend(json2object_reader_BaseParser.prototype,{ onIncorrectType: function(pos,variable) { - this.errors.push(json2object_Error.IncorrectType(variable,"{ url : String, title : String, ?subs : Null<String>, isTemp : Bool, isIframe : Bool, duration : Float, author : String }",pos)); + this.errors.push(json2object_Error.IncorrectType(variable,"{ ?voiceOverTrack : Null<String>, url : String, title : String, ?subs : Null<String>, isTemp : Bool, isIframe : Bool, duration : Float, author : String }",pos)); json2object_reader_BaseParser.prototype.onIncorrectType.call(this,pos,variable); } ,loadJsonNull: function(pos,variable) { @@ -1322,7 +1322,7 @@ JsonParser_$43.prototype = $extend(json2object_reader_BaseParser.prototype,{ } ,loadJsonObject: function(o,pos,variable) { var assigned = new haxe_ds_StringMap(); - this.objectSetupAssign(assigned,["author","duration","isIframe","isTemp","subs","title","url"],[false,false,false,false,true,false,false]); + this.objectSetupAssign(assigned,["author","duration","isIframe","isTemp","subs","title","url","voiceOverTrack"],[false,false,false,false,true,false,false,true]); this.value = this.getAuto(); var _g = 0; while(_g < o.length) { @@ -1350,6 +1350,9 @@ JsonParser_$43.prototype = $extend(json2object_reader_BaseParser.prototype,{ case "url": this.loadObjectFieldReflect(($_=new JsonParser_$44(this.errors,this.putils,1),$bind($_,$_.loadJson)),field,"url",assigned,pos); break; + case "voiceOverTrack": + this.value.voiceOverTrack = this.loadObjectField(($_=new JsonParser_$48(this.errors,this.putils,1),$bind($_,$_.loadJson)),field,"voiceOverTrack",assigned,this.value.voiceOverTrack,pos); + break; default: this.errors.push(json2object_Error.UnknownVariable(field.name,this.putils.convertPosition(field.namePos))); } @@ -1357,7 +1360,7 @@ JsonParser_$43.prototype = $extend(json2object_reader_BaseParser.prototype,{ this.objectErrors(assigned,pos); } ,getAuto: function() { - return { author : new JsonParser_$44([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), duration : new JsonParser_$45([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), isIframe : new JsonParser_$46([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), isTemp : new JsonParser_$46([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), subs : new JsonParser_$48([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), title : new JsonParser_$44([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), url : new JsonParser_$44([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1)))}; + return { author : new JsonParser_$44([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), duration : new JsonParser_$45([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), isIframe : new JsonParser_$46([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), isTemp : new JsonParser_$46([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), subs : new JsonParser_$48([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), title : new JsonParser_$44([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), url : new JsonParser_$44([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1))), voiceOverTrack : new JsonParser_$48([],this.putils,0).loadJson(new hxjsonast_Json(hxjsonast_JsonValue.JNull,new hxjsonast_Position("",0,1)))}; } ,__class__: JsonParser_$43 }); @@ -2801,7 +2804,7 @@ StringTools.hex = function(n,digits) { var _$Types_VideoItemTools = function() { }; _$Types_VideoItemTools.__name__ = true; _$Types_VideoItemTools.withUrl = function(item,url) { - return { url : url, title : item.title, author : item.author, duration : item.duration, subs : item.subs, isTemp : item.isTemp, isIframe : item.isIframe}; + return { url : url, title : item.title, author : item.author, duration : item.duration, subs : item.subs, voiceOverTrack : item.voiceOverTrack, isTemp : item.isTemp, isIframe : item.isIframe}; }; var VideoList = function() { this.items = []; @@ -4606,8 +4609,11 @@ server_Main.prototype = { haxe_Log.trace("Global network is disabled in config",{ fileName : "src/server/Main.hx", lineNumber : 139, className : "server.Main", methodName : "runServer"}); } else if(!this.isNoState) { server_Utils.getGlobalIp(function(ip) { + if(ip.indexOf(":") != -1) { + ip = "[" + ip + "]"; + } _gthis.globalIp = ip; - haxe_Log.trace("Global: http://" + _gthis.globalIp + ":" + _gthis.port,{ fileName : "src/server/Main.hx", lineNumber : 143, className : "server.Main", methodName : "runServer"}); + haxe_Log.trace("Global: http://" + _gthis.globalIp + ":" + _gthis.port,{ fileName : "src/server/Main.hx", lineNumber : 145, className : "server.Main", methodName : "runServer"}); }); } var dir = "" + this.rootDir + "/res"; @@ -4692,7 +4698,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 : 212, className : "server.Main", methodName : "getUserConfig"}); + haxe_Log.trace("Warning: config field \"" + field + "\" is unknown",{ fileName : "src/server/Main.hx", lineNumber : 214, className : "server.Main", methodName : "getUserConfig"}); } config[field] = Reflect.field(customConfig,field); } @@ -4703,14 +4709,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 : 218, className : "server.Main", methodName : "getUserConfig"}); + haxe_Log.trace("Warning: emote name \"" + emote.name + "\" has copy",{ fileName : "src/server/Main.hx", lineNumber : 220, 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 : 222, className : "server.Main", methodName : "getUserConfig"}); + haxe_Log.trace("Warning: emote url of name \"" + emote.name + "\" has copy",{ fileName : "src/server/Main.hx", lineNumber : 224, className : "server.Main", methodName : "getUserConfig"}); } emoteCopies_h[emote.image] = true; } @@ -4748,7 +4754,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 : 261, className : "server.Main", methodName : "saveState"}); + haxe_Log.trace("Saving state...",{ fileName : "src/server/Main.hx", lineNumber : 263, className : "server.Main", methodName : "saveState"}); var json = JSON.stringify(this.getCurrentState(),null,"\t"); js_node_Fs.writeFileSync(this.statePath,json); this.writeUsers(this.userList); @@ -4763,7 +4769,7 @@ server_Main.prototype = { if(!sys_FileSystem.exists(this.statePath)) { return; } - haxe_Log.trace("Loading state...",{ fileName : "src/server/Main.hx", lineNumber : 284, className : "server.Main", methodName : "loadState"}); + haxe_Log.trace("Loading state...",{ fileName : "src/server/Main.hx", lineNumber : 286, 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; @@ -4782,7 +4788,7 @@ server_Main.prototype = { this.videoTimer.pause(); } ,logError: function(type,data) { - haxe_Log.trace(type,{ fileName : "src/server/Main.hx", lineNumber : 302, className : "server.Main", methodName : "logError", customParams : [data]}); + haxe_Log.trace(type,{ fileName : "src/server/Main.hx", lineNumber : 304, 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; @@ -4800,7 +4806,7 @@ server_Main.prototype = { if(_gthis.clients.length == 0) { return; } - haxe_Log.trace("Ping " + url,{ fileName : "src/server/Main.hx", lineNumber : 319, className : "server.Main", methodName : "initIntergationHandlers"}); + haxe_Log.trace("Ping " + url,{ fileName : "src/server/Main.hx", lineNumber : 321, className : "server.Main", methodName : "initIntergationHandlers"}); js_node_Http.get(url,null,function(r) { }); }; @@ -4820,13 +4826,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 : 342, className : "server.Main", methodName : "addAdmin"}); + haxe_Log.trace("Admin " + name + " added.",{ fileName : "src/server/Main.hx", lineNumber : 344, 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 : 349, className : "server.Main", methodName : "removeAdmin"}); + haxe_Log.trace("Admin " + name + " removed.",{ fileName : "src/server/Main.hx", lineNumber : 351, className : "server.Main", methodName : "removeAdmin"}); } ,replayLog: function(events) { var _gthis = this; @@ -4893,7 +4899,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 : 408, className : "server.Main", methodName : "onConnect", customParams : ["" + name + " connected (" + ip + ")"]}); + haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 410, 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.uuid = uuid; @@ -4907,7 +4913,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 : 425, className : "server.Main", methodName : "onConnect"}); + haxe_Log.trace(errors,{ fileName : "src/server/Main.hx", lineNumber : 427, className : "server.Main", methodName : "onConnect"}); _gthis.serverMessage(client,errors); return; } @@ -5058,7 +5064,7 @@ server_Main.prototype = { if(!internal) { return; } - haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 487, className : "server.Main", methodName : "onMessage", customParams : ["Client " + client.name + " disconnected"]}); + haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 489, className : "server.Main", methodName : "onMessage", customParams : ["Client " + client.name + " disconnected"]}); server_Utils.sortedPush(this.freeIds,client.id); HxOverrides.remove(this.clients,client); this.sendClientList(); @@ -5199,7 +5205,7 @@ server_Main.prototype = { this.send(client,{ type : "LoginError"}); return; } - haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 575, 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 : 577, className : "server.Main", methodName : "onMessage", customParams : ["Client " + client.name + " logged as " + name]}); client.name = name; client.setGroupFlag(ClientGroup.User,true); this.checkBan(client); @@ -5212,7 +5218,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 : 596, 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 : 598, 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; @@ -5527,7 +5533,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 : 1008, className : "server.Main", methodName : "checkBan"}); + haxe_Log.trace("" + client.name + " ban removed",{ fileName : "src/server/Main.hx", lineNumber : 1010, className : "server.Main", methodName : "checkBan"}); this.sendClientList(); } break; diff --git a/res/client.js b/res/client.js index 5bab611..d26396f 100644 --- a/res/client.js +++ b/res/client.js @@ -484,7 +484,7 @@ StringTools.hex = function(n,digits) { var _$Types_VideoItemTools = function() { }; _$Types_VideoItemTools.__name__ = true; _$Types_VideoItemTools.withUrl = function(item,url) { - return { url : url, title : item.title, author : item.author, duration : item.duration, subs : item.subs, isTemp : item.isTemp, isIframe : item.isIframe}; + return { url : url, title : item.title, author : item.author, duration : item.duration, subs : item.subs, voiceOverTrack : item.voiceOverTrack, isTemp : item.isTemp, isIframe : item.isIframe}; }; var VideoList = function() { this.items = []; @@ -764,6 +764,8 @@ client_Buttons.init = function(main) { var isRawSingleVideo = value != "" && main.isRawPlayerLink(value) && main.isSingleVideoLink(value); window.document.querySelector("#mediatitleblock").style.display = isRawSingleVideo ? "" : "none"; window.document.querySelector("#subsurlblock").style.display = isRawSingleVideo ? "" : "none"; + var tmp = value.length > 0 ? "" : "none"; + window.document.querySelector("#voiceoverblock").style.display = tmp; var panel = window.document.querySelector("#addfromurl"); var oldH = panel.style.height; panel.style.height = ""; @@ -1253,6 +1255,7 @@ client_JsApi.fireVideoRemoveEvents = function(item) { 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.gotFirstPageInteraction = false; this.disabledReconnection = false; this.gotInitialConnection = false; this.isConnected = false; @@ -1290,6 +1293,7 @@ var client_Main = function() { _gthis.openWebSocket(); }); client_JsApi.init(this,this.player); + window.document.addEventListener("click",$bind(this,this.onFirstInteraction)); }; client_Main.__name__ = true; client_Main.main = function() { @@ -1317,7 +1321,19 @@ client_Main.serverMessage = function(text,isText,withTimestamp) { msgBuf.scrollTop = msgBuf.scrollHeight; }; client_Main.prototype = { - settingsPatcher: function(data,version) { + onFirstInteraction: function() { + if(this.gotFirstPageInteraction) { + return; + } + if(!this.player.isVideoLoaded()) { + return; + } + this.gotFirstPageInteraction = true; + this.player.unmute(); + this.player.play(); + window.document.removeEventListener("click",$bind(this,this.onFirstInteraction)); + } + ,settingsPatcher: function(data,version) { switch(version) { case 1: data.hotkeysEnabled = true; @@ -1540,7 +1556,7 @@ client_Main.prototype = { } data.title = data.title != null ? data.title : Lang.get("rawVideo"); data.url = data.url != null ? data.url : url; - _gthis.send({ type : "AddVideo", addVideo : { item : { url : data.url, title : data.title, author : _gthis.personal.name, duration : data.duration, isTemp : isTemp, subs : data.subs, isIframe : data.isIframe == true}, atEnd : atEnd}}); + _gthis.send({ type : "AddVideo", addVideo : { item : { url : data.url, title : data.title, author : _gthis.personal.name, duration : data.duration, isTemp : isTemp, subs : data.subs, voiceOverTrack : data.voiceOverTrack, isIframe : data.isIframe == true}, atEnd : atEnd}}); if(callback != null) { callback(); } @@ -1609,7 +1625,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 : 420, 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 : 433, 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) { @@ -1663,9 +1679,11 @@ client_Main.prototype = { if(this.player.getDuration() <= this.player.getTime() + synchThreshold) { return; } - if(!data.getTime.paused) { - this.player.play(); - } else { + if(this.player.isPaused()) { + if(!data.getTime.paused) { + this.player.play(); + } + } else if(data.getTime.paused) { this.player.pause(); } this.player.setPauseIndicator(!data.getTime.paused); @@ -2361,6 +2379,13 @@ client_Main.prototype = { ,getYoutubePlaylistLimit: function() { return this.config.youtubePlaylistLimit; } + ,isAutoplayAllowed: function() { + var navigator = $global.navigator; + if(navigator.getAutoplayPolicy != null) { + return navigator.getAutoplayPolicy("mediaelement"); + } + return this.gotFirstPageInteraction; + } ,isVerbose: function() { return this.config.isVerbose; } @@ -2370,6 +2395,10 @@ client_Main.prototype = { } }; var client_Player = function(main) { + this.voiceOverVolume = 0.3; + this.needsVolumeReset = false; + this.isAudioTrackLoaded = false; + this.voiceOverInput = window.document.querySelector("#voiceoverurl"); this.skipSetRate = false; this.skipSetTime = false; this.isLoaded = false; @@ -2378,7 +2407,8 @@ var client_Player = function(main) { this.videoList = new VideoList(); this.main = main; this.youtube = new client_players_Youtube(main,this); - this.players = [this.youtube,new client_players_Streamable(main,this)]; + this.streamable = new client_players_Streamable(main,this); + this.players = [this.youtube,this.streamable]; this.iframePlayer = new client_players_Iframe(main,this); this.rawPlayer = new client_players_Raw(main,this); this.initItemButtons(); @@ -2426,21 +2456,31 @@ client_Player.prototype = { var _this = this.videoList; client_JsApi.fireVideoRemoveEvents(_this.items[_this.pos]); this.player.removeVideo(); + this.removeExternalAudioTrack(); } this.main.blinkTabWithTitle("*Video*"); } this.player = newPlayer; } - ,getVideoData: function(data,callback) { + ,getVideoData: function(req,callback) { + var _gthis = this; var player = Lambda.find(this.players,function(player) { - return player.isSupportedLink(data.url); + return player.isSupportedLink(req.url); }); if(player == null) { player = this.rawPlayer; } - player.getVideoData(data,callback); + player.getVideoData(req,function(data) { + var voiceOverTrack = StringTools.trim(_gthis.voiceOverInput.value); + data.voiceOverTrack = voiceOverTrack; + _gthis.voiceOverInput.value = ""; + callback(data); + }); } ,isRawPlayerLink: function(url) { + if(this.streamable.isSupportedLink(url)) { + return true; + } return !Lambda.exists(this.players,function(player) { return player.isSupportedLink(url); }); @@ -2460,12 +2500,55 @@ client_Player.prototype = { this.isLoaded = false; if(this.main.isVideoEnabled) { this.player.loadVideo(item); + this.setExternalAudioTrack(item); } else { this.onCanBePlayed(); } client_JsApi.fireVideoChangeEvents(item); window.document.querySelector("#currenttitle").textContent = item.title; } + ,setExternalAudioTrack: function(item) { + var _gthis = this; + this.removeExternalAudioTrack(); + var tmp = item.voiceOverTrack; + if(tmp == null) { + return; + } + if(tmp.length == 0) { + return; + } + this.audioTrack = new Audio(tmp); + if(!this.main.isAutoplayAllowed()) { + this.audioTrack.muted = true; + } + this.audioTrack.oncanplay = function() { + _gthis.audioTrack.oncanplay = null; + _gthis.audioTrack.onerror = null; + return _gthis.isAudioTrackLoaded = true; + }; + this.audioTrack.onerror = function(e) { + haxe_Log.trace(e,{ fileName : "src/client/Player.hx", lineNumber : 174, className : "client.Player", methodName : "setExternalAudioTrack"}); + _gthis.audioTrack.oncanplay = null; + _gthis.audioTrack.onerror = null; + _gthis.isAudioTrackLoaded = false; + _gthis.audioTrack = null; + _gthis.setVolume(1); + }; + } + ,removeExternalAudioTrack: function() { + this.isAudioTrackLoaded = false; + this.needsVolumeReset = false; + if(this.audioTrack == null) { + return; + } + var tmp = this.audioTrack; + if(tmp != null) { + tmp.pause(); + } + this.audioTrack.src = null; + this.audioTrack = null; + this.needsVolumeReset = true; + } ,setSupportedPlayer: function(url,isIframe) { var currentPlayer = Lambda.find(this.players,function(p) { return p.isSupportedLink(url); @@ -2515,6 +2598,10 @@ client_Player.prototype = { this.isLoaded = true; } ,onPlay: function() { + var tmp = this.audioTrack; + if(tmp != null) { + tmp.play(); + } if((this.main.personal.group & 4) == 0) { return; } @@ -2527,6 +2614,10 @@ client_Player.prototype = { } ,onPause: function() { var _gthis = this; + var tmp = this.audioTrack; + if(tmp != null) { + tmp.pause(); + } var _this = this.videoList; var tmp = _this.items[_this.pos]; if(tmp == null) { @@ -2540,7 +2631,7 @@ client_Player.prototype = { return; } } - if(this.main.hasLeaderOnPauseRequest() && this.videoList.items.length > 0 && this.getTime() > 1 && !this.main.hasLeader()) { + if(this.main.hasLeaderOnPauseRequest() && this.videoList.items.length > 0 && this.getTime() > 1 && this.isLoaded && !this.main.hasLeader()) { client_JsApi.once("SetLeader",function(event) { if(event.setLeader.clientName != _gthis.main.personal.name) { return; @@ -2557,6 +2648,9 @@ client_Player.prototype = { this.main.send({ type : "Pause", pause : { time : this.getTime()}}); } ,onSetTime: function() { + if(this.audioTrack != null) { + this.audioTrack.currentTime = this.getTime(); + } if(this.skipSetTime) { this.skipSetTime = false; return; @@ -2567,6 +2661,9 @@ client_Player.prototype = { this.main.send({ type : "SetTime", setTime : { time : this.getTime()}}); } ,onRateChange: function() { + if(this.audioTrack != null) { + this.audioTrack.playbackRate = this.getPlaybackRate(); + } if(this.skipSetRate) { this.skipSetRate = false; return; @@ -2758,6 +2855,9 @@ client_Player.prototype = { return _this.items[_this.pos].duration; } ,isVideoLoaded: function() { + if(this.player == null) { + return false; + } return this.player.isVideoLoaded(); } ,play: function() { @@ -2771,6 +2871,16 @@ client_Player.prototype = { return; } this.player.play(); + if(this.needsVolumeReset) { + this.setVolume(1); + } + if(this.audioTrack != null) { + this.setVolume(0.3); + var tmp = this.audioTrack; + if(tmp != null) { + tmp.play(); + } + } } ,pause: function() { if(!this.main.isSyncActive) { @@ -2783,6 +2893,10 @@ client_Player.prototype = { return; } this.player.pause(); + var tmp = this.audioTrack; + if(tmp != null) { + tmp.pause(); + } } ,getTime: function() { if(this.player == null) { @@ -2808,6 +2922,9 @@ client_Player.prototype = { } this.skipSetTime = isLocal; this.player.setTime(time); + if(this.audioTrack != null) { + this.audioTrack.currentTime = time; + } } ,getPlaybackRate: function() { if(this.player == null) { @@ -2833,6 +2950,9 @@ client_Player.prototype = { } this.skipSetRate = isLocal; this.player.setPlaybackRate(rate); + if(this.audioTrack != null) { + this.audioTrack.playbackRate = rate; + } } ,skipAd: function() { var _gthis = this; @@ -2865,10 +2985,59 @@ client_Player.prototype = { } }; http.onError = function(msg) { - haxe_Log.trace(msg,{ fileName : "src/client/Player.hx", lineNumber : 484, className : "client.Player", methodName : "skipAd"}); + haxe_Log.trace(msg,{ fileName : "src/client/Player.hx", lineNumber : 564, className : "client.Player", methodName : "skipAd"}); }; http.request(); } + ,isPaused: function() { + if(this.player == null) { + return true; + } + if(!this.player.isVideoLoaded()) { + return true; + } + return this.player.isPaused(); + } + ,getVolume: function() { + if(this.player == null) { + return 1; + } + if(!this.player.isVideoLoaded()) { + return 1; + } + return this.player.getVolume(); + } + ,setVolume: function(volume) { + if(this.player == null) { + return; + } + if(!this.player.isVideoLoaded()) { + return; + } + this.player.setVolume(volume); + } + ,unmute: function() { + if(this.player == null) { + return; + } + if(!this.player.isVideoLoaded()) { + return; + } + this.player.unmute(); + if(this.audioTrack != null) { + this.audioTrack.muted = false; + } + if(this.audioTrack == null && this.almostEq(this.getVolume(),this.voiceOverVolume,0.01)) { + this.setVolume(1); + } + } + ,almostEq: function(a,b,diff) { + if(a > b - diff) { + return a < b + diff; + } else { + return false; + } + } }; var client_Settings = function() { }; client_Settings.__name__ = true; @@ -3109,6 +3278,9 @@ client_players_Iframe.prototype = { } ,pause: function() { } + ,isPaused: function() { + return false; + } ,getTime: function() { return 0; } @@ -3119,6 +3291,13 @@ client_players_Iframe.prototype = { } ,setPlaybackRate: function(rate) { } + ,getVolume: function() { + return 1; + } + ,setVolume: function(volume) { + } + ,unmute: function() { + } }; var client_players_Raw = function(main,player) { this.isHlsLoaded = false; @@ -3233,6 +3412,9 @@ client_players_Raw.prototype = { }; this.video.onpause = ($_=this.player,$bind($_,$_.onPause)); this.video.onratechange = ($_=this.player,$bind($_,$_.onRateChange)); + if(!this.main.isAutoplayAllowed()) { + this.video.muted = true; + } this.playerEl.appendChild(this.video); } if(isHls) { @@ -3321,6 +3503,9 @@ client_players_Raw.prototype = { ,pause: function() { this.video.pause(); } + ,isPaused: function() { + return this.video.paused; + } ,getTime: function() { return this.video.currentTime; } @@ -3333,6 +3518,15 @@ client_players_Raw.prototype = { ,setPlaybackRate: function(rate) { this.video.playbackRate = rate; } + ,getVolume: function() { + return this.video.volume; + } + ,setVolume: function(volume) { + this.video.volume = volume; + } + ,unmute: function() { + this.video.muted = false; + } }; var client_players_RawSubs = function() { }; client_players_RawSubs.__name__ = true; @@ -3677,7 +3871,8 @@ client_players_Youtube.prototype = { var title = item.snippet.title; var duration = _gthis.convertTime(item.contentDetails.duration); if(duration == 0) { - callback({ duration : 356400, title : title, url : "<iframe src=\"https://www.youtube.com/embed/" + id + "\" frameborder=\"0\"\n\t\t\t\t\t\t\tallow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\"\n\t\t\t\t\t\t\tallowfullscreen></iframe>", isIframe : true}); + var mute = _gthis.main.isAutoplayAllowed() ? "" : "&mute=1"; + callback({ duration : 356400, title : title, url : "<iframe src=\"https://www.youtube.com/embed/" + id + "?autoplay=1" + mute + "\" frameborder=\"0\"\n\t\t\t\t\t\t\tallow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\"\n\t\t\t\t\t\t\tallowfullscreen></iframe>", isIframe : true}); continue; } callback({ duration : duration, title : title, url : url}); @@ -3756,7 +3951,7 @@ client_players_Youtube.prototype = { } callback({ duration : _gthis.tempYoutube.getDuration()}); }, onError : function(e) { - haxe_Log.trace("Error " + e.data,{ fileName : "src/client/players/Youtube.hx", lineNumber : 186, className : "client.players.Youtube", methodName : "getRemoteDataFallback"}); + haxe_Log.trace("Error " + e.data,{ fileName : "src/client/players/Youtube.hx", lineNumber : 187, className : "client.players.Youtube", methodName : "getRemoteDataFallback"}); if(_gthis.playerEl.contains(video)) { _gthis.playerEl.removeChild(video); } @@ -3779,7 +3974,10 @@ client_players_Youtube.prototype = { this.video = window.document.createElement("div"); this.video.id = "videoplayer"; this.playerEl.appendChild(this.video); - this.youtube = new YT.Player(this.video.id,{ videoId : this.extractVideoId(item.url), playerVars : { autoplay : 1, playsinline : 1, modestbranding : 1, rel : 0, showinfo : 0}, events : { onReady : function(e) { + this.youtube = new YT.Player(this.video.id,{ videoId : this.extractVideoId(item.url), playerVars : { autoplay : 1, playsinline : 1, rel : 0}, events : { onReady : function(e) { + if(!_gthis.main.isAutoplayAllowed()) { + e.target.mute(); + } _gthis.isLoaded = true; _gthis.youtube.pauseVideo(); }, onStateChange : function(e) { @@ -3804,7 +4002,7 @@ client_players_Youtube.prototype = { }, onPlaybackRateChange : function(e) { _gthis.player.onRateChange(); }, onError : function(e) { - haxe_Log.trace("Error " + e.data,{ fileName : "src/client/players/Youtube.hx", lineNumber : 243, className : "client.players.Youtube", methodName : "loadVideo"}); + haxe_Log.trace("Error " + e.data,{ fileName : "src/client/players/Youtube.hx", lineNumber : 245, className : "client.players.Youtube", methodName : "loadVideo"}); var tmp = _gthis.player.getCurrentItem(); if(tmp == null) { return; @@ -3818,8 +4016,8 @@ client_players_Youtube.prototype = { var info = event.getYoutubeVideoInfo.response; var tmp = _gthis.getBestStreamFormat(info); if(tmp == null) { - haxe_Log.trace("format not found in response info:",{ fileName : "src/client/players/Youtube.hx", lineNumber : 256, className : "client.players.Youtube", methodName : "rawSourceFallback"}); - haxe_Log.trace(info,{ fileName : "src/client/players/Youtube.hx", lineNumber : 257, className : "client.players.Youtube", methodName : "rawSourceFallback"}); + haxe_Log.trace("format not found in response info:",{ fileName : "src/client/players/Youtube.hx", lineNumber : 258, className : "client.players.Youtube", methodName : "rawSourceFallback"}); + haxe_Log.trace(info,{ fileName : "src/client/players/Youtube.hx", lineNumber : 259, className : "client.players.Youtube", methodName : "rawSourceFallback"}); return; } _gthis.player.changeVideoSrc(tmp.url); @@ -3830,6 +4028,7 @@ client_players_Youtube.prototype = { info.formats = info.formats != null ? info.formats : []; info.adaptiveFormats = info.adaptiveFormats != null ? info.adaptiveFormats : []; var formats = info.adaptiveFormats.concat(info.formats); + haxe_Log.trace(formats,{ fileName : "src/client/players/Youtube.hx", lineNumber : 276, className : "client.players.Youtube", methodName : "getBestStreamFormat"}); var qPriority = [1080,720,480,360,240]; var _g = 0; while(_g < qPriority.length) { @@ -3872,6 +4071,9 @@ client_players_Youtube.prototype = { ,pause: function() { this.youtube.pauseVideo(); } + ,isPaused: function() { + return this.youtube.getPlayerState() == 2; + } ,getTime: function() { return this.youtube.getCurrentTime(); } @@ -3884,6 +4086,18 @@ client_players_Youtube.prototype = { ,setPlaybackRate: function(rate) { this.youtube.setPlaybackRate(rate); } + ,getVolume: function() { + if(this.youtube.isMuted()) { + return 0; + } + return this.youtube.getVolume() / 100; + } + ,setVolume: function(volume) { + this.youtube.setVolume(volume * 100 | 0); + } + ,unmute: function() { + this.youtube.unMute(); + } }; var haxe_Exception = function(message,previous,native) { Error.call(this,message); diff --git a/res/css/des.css b/res/css/des.css index 343f0ad..bd5606d 100644 --- a/res/css/des.css +++ b/res/css/des.css @@ -124,6 +124,7 @@ button { button:not(:first-child) { margin-left: .5rem; } + .server-whisper button { margin-left: 0; font-style: italic; @@ -140,6 +141,7 @@ button.active { button:hover:not(.active) { background-color: var(--background-chat); } + .info header button:hover:not(.active) { background-color: transparent; } @@ -180,6 +182,7 @@ button span { border-color: transparent; transition: border-color ease-in-out 800ms; } + #leader_btn.hint { border-radius: .5rem; border: .125rem solid; @@ -413,7 +416,8 @@ header h4 { } #mediatitle, -#subsurl { +#subsurl, +#voiceoverurl { margin-left: 2rem; flex-grow: 1; } diff --git a/res/index.html b/res/index.html index 9ab9d03..ec557c6 100644 --- a/res/index.html +++ b/res/index.html @@ -96,6 +96,9 @@ <div id="subsurlblock" class="display-flex" style="display: none;"> <input id="subsurl" type="text" placeholder="${subtitlesUrlOptional}"> </div> + <div id="voiceoverblock" class="display-flex" style="display: none;"> + <input id="voiceoverurl" type="text" placeholder="${voiceOverAudioTrackUrlOptional}"> + </div> <div> <label> <input class="add-temp" type="checkbox" checked="checked">${addAsTemporary} diff --git a/res/langs/en.json b/res/langs/en.json index 3f5d47a..488af78 100644 --- a/res/langs/en.json +++ b/res/langs/en.json @@ -82,6 +82,7 @@ "mediaUrl": "Media URL", "optionalTitle": "Title (optional)", "subtitlesUrlOptional": "Subtitles URL (optional)", + "voiceOverAudioTrackUrlOptional": "Voice-over audio URL (optional)", "addTemplateUrl": "Add template URL", "queueNext": "Queue next", "queueLast": "Queue last", diff --git a/res/langs/ru.json b/res/langs/ru.json index 932fb94..49260cb 100644 --- a/res/langs/ru.json +++ b/res/langs/ru.json @@ -80,8 +80,9 @@ "voteForSkip": "Голосовать за пропуск", "addAsTemporary": "Добавить как временный", "mediaUrl": "Ссылка на видео", - "optionalTitle": "Заголовок (необязательно)", - "subtitlesUrlOptional": "Ссылка на субтитры (необязательно)", + "optionalTitle": "Заголовок (опционально)", + "subtitlesUrlOptional": "Ссылка на субтитры (опционально)", + "voiceOverAudioTrackUrlOptional": "Ссылка на аудиодорожку (опционально)", "addTemplateUrl": "Добавить пример ссылки", "queueNext": "След.", "queueLast": "В конец", diff --git a/src/Types.hx b/src/Types.hx index 0e89b85..8567f7b 100644 --- a/src/Types.hx +++ b/src/Types.hx @@ -13,6 +13,7 @@ typedef VideoData = { var ?title:String; var ?url:String; var ?subs:String; + var ?voiceOverTrack:String; var ?isIframe:Bool; } @@ -106,6 +107,7 @@ typedef VideoItem = { var author:String; var duration:Float; var ?subs:String; + var ?voiceOverTrack:String; var isTemp:Bool; var isIframe:Bool; } @@ -118,6 +120,7 @@ private class VideoItemTools { author: item.author, duration: item.duration, subs: item.subs, + voiceOverTrack: item.voiceOverTrack, isTemp: item.isTemp, isIframe: item.isIframe }; diff --git a/src/client/Buttons.hx b/src/client/Buttons.hx index de3aa27..c3581e2 100644 --- a/src/client/Buttons.hx +++ b/src/client/Buttons.hx @@ -218,6 +218,7 @@ class Buttons { && main.isSingleVideoLink(value); ge("#mediatitleblock").style.display = isRawSingleVideo ? "" : "none"; ge("#subsurlblock").style.display = isRawSingleVideo ? "" : "none"; + ge("#voiceoverblock").style.display = value.length > 0 ? "" : "none"; final panel = ge("#addfromurl"); final oldH = panel.style.height; // save for animation panel.style.height = ""; // to calculate height from content diff --git a/src/client/IPlayer.hx b/src/client/IPlayer.hx index 903902e..032ac97 100644 --- a/src/client/IPlayer.hx +++ b/src/client/IPlayer.hx @@ -12,8 +12,12 @@ interface IPlayer { function isVideoLoaded():Bool; function play():Void; function pause():Void; + function isPaused():Bool; function getTime():Float; function setTime(time:Float):Void; function getPlaybackRate():Float; function setPlaybackRate(rate:Float):Void; + function getVolume():Float; + function setVolume(volume:Float):Void; + function unmute():Void; } diff --git a/src/client/Main.hx b/src/client/Main.hx index 2850c75..248e205 100644 --- a/src/client/Main.hx +++ b/src/client/Main.hx @@ -48,6 +48,7 @@ class Main { final player:Player; var onTimeGet:Timer; var onBlinkTab:Null<Timer>; + var gotFirstPageInteraction = false; static function main():Void { new Main(); @@ -94,6 +95,17 @@ class Main { openWebSocket(); }); JsApi.init(this, player); + + document.addEventListener("click", onFirstInteraction); + } + + function onFirstInteraction():Void { + if (gotFirstPageInteraction) return; + if (!player.isVideoLoaded()) return; + gotFirstPageInteraction = true; + player.unmute(); + player.play(); + document.removeEventListener("click", onFirstInteraction); } function settingsPatcher(data:Any, version:Int):Any { @@ -319,6 +331,7 @@ class Main { duration: data.duration, isTemp: isTemp, subs: data.subs, + voiceOverTrack: data.voiceOverTrack, isIframe: data.isIframe == true }, atEnd: atEnd @@ -526,8 +539,11 @@ class Main { } if (player.isVideoLoaded()) forceSyncNextTick = false; if (player.getDuration() <= player.getTime() + synchThreshold) return; - if (!data.getTime.paused) player.play(); - else player.pause(); + if (player.isPaused()) { + if (!data.getTime.paused) player.play(); + } else { + if (data.getTime.paused) player.pause(); + } player.setPauseIndicator(!data.getTime.paused); if (Math.abs(time - newTime) < synchThreshold) return; // +0.5s for buffering @@ -1199,6 +1215,15 @@ class Main { return config.youtubePlaylistLimit; } + public function isAutoplayAllowed():Bool { + final navigator:{ + getAutoplayPolicy:(type:String) -> Bool + } = cast Browser.navigator; + if (navigator.getAutoplayPolicy != null) return + navigator.getAutoplayPolicy("mediaelement"); + return gotFirstPageInteraction; + } + public function isVerbose():Bool { return config.isVerbose; } diff --git a/src/client/Player.hx b/src/client/Player.hx index bc64053..f40c34c 100644 --- a/src/client/Player.hx +++ b/src/client/Player.hx @@ -10,7 +10,9 @@ import client.players.Streamable; import client.players.Youtube; import haxe.Http; import haxe.Json; +import js.html.Audio; import js.html.Element; +import js.html.InputElement; class Player { final main:Main; @@ -25,13 +27,21 @@ class Player { var isLoaded = false; var skipSetTime = false; var skipSetRate = false; + var streamable:Streamable; + + final voiceOverInput:InputElement = cast ge("#voiceoverurl"); + var audioTrack:Null<Audio>; + var isAudioTrackLoaded = false; + var needsVolumeReset = false; + final voiceOverVolume = 0.3; public function new(main:Main):Void { this.main = main; youtube = new Youtube(main, this); + streamable = new Streamable(main, this); players = [ youtube, - new Streamable(main, this) + streamable ]; iframePlayer = new Iframe(main, this); rawPlayer = new Raw(main, this); @@ -97,19 +107,26 @@ class Player { if (player != null) { JsApi.fireVideoRemoveEvents(videoList.currentItem); player.removeVideo(); + removeExternalAudioTrack(); } main.blinkTabWithTitle("*Video*"); } player = newPlayer; } - public function getVideoData(data:VideoDataRequest, callback:(data:VideoData) -> Void):Void { - var player = players.find(player -> player.isSupportedLink(data.url)); + public function getVideoData(req:VideoDataRequest, callback:(data:VideoData) -> Void):Void { + var player = players.find(player -> player.isSupportedLink(req.url)); player ??= rawPlayer; - player.getVideoData(data, callback); + player.getVideoData(req, data -> { + final voiceOverTrack = voiceOverInput.value.trim(); + data.voiceOverTrack = voiceOverTrack; + voiceOverInput.value = ""; + callback(data); + }); } public function isRawPlayerLink(url:String):Bool { + if (streamable.isSupportedLink(url)) return true; return !players.exists(player -> player.isSupportedLink(url)); } @@ -129,6 +146,7 @@ class Player { isLoaded = false; if (main.isVideoEnabled) { player.loadVideo(item); + setExternalAudioTrack(item); } else { onCanBePlayed(); } @@ -136,6 +154,42 @@ class Player { ge("#currenttitle").textContent = item.title; } + function setExternalAudioTrack(item:VideoItem):Void { + removeExternalAudioTrack(); + final voiceOverTrack = item.voiceOverTrack ?? return; + if (voiceOverTrack.length == 0) return; + audioTrack = new Audio(voiceOverTrack); + if (!main.isAutoplayAllowed()) { + audioTrack.muted = true; + } + inline function cleanAudioEvents() { + audioTrack.oncanplay = null; + audioTrack.onerror = null; + } + audioTrack.oncanplay = () -> { + cleanAudioEvents(); + isAudioTrackLoaded = true; + } + audioTrack.onerror = e -> { + trace(e); + cleanAudioEvents(); + isAudioTrackLoaded = false; + audioTrack = null; + setVolume(1); + } + } + + function removeExternalAudioTrack():Void { + isAudioTrackLoaded = false; + needsVolumeReset = false; + if (audioTrack == null) return; + + audioTrack?.pause(); + audioTrack.src = null; + audioTrack = null; + needsVolumeReset = true; + } + function setSupportedPlayer(url:String, isIframe:Bool):Void { final currentPlayer = players.find(p -> p.isSupportedLink(url)); if (currentPlayer != null) setPlayer(currentPlayer); @@ -171,6 +225,8 @@ class Player { } public function onPlay():Void { + audioTrack?.play(); + if (!main.isLeader()) return; main.send({ type: Play, @@ -186,6 +242,8 @@ class Player { } public function onPause():Void { + audioTrack?.pause(); + final item = videoList.currentItem ?? return; // do not send pause if video is ended if (getTime() >= item.duration - 0.01) return; @@ -193,8 +251,10 @@ class Player { if (player == rawPlayer && youtube.isSupportedLink(item.url)) { if (getTime() >= item.duration - 1) return; } - final hasAutoPause = main.hasLeaderOnPauseRequest() && videoList.length > 0 - && getTime() > 1; + final hasAutoPause = main.hasLeaderOnPauseRequest() + && videoList.length > 0 + && getTime() > 1 + && isLoaded; if (hasAutoPause && !main.hasLeader()) { JsApi.once(SetLeader, event -> { final name = event.setLeader.clientName; @@ -220,6 +280,10 @@ class Player { } public function onSetTime():Void { + if (audioTrack != null) { + audioTrack.currentTime = getTime(); + } + if (skipSetTime) { skipSetTime = false; return; @@ -234,6 +298,9 @@ class Player { } public function onRateChange():Void { + if (audioTrack != null) { + audioTrack.playbackRate = getPlaybackRate(); + } if (skipSetRate) { skipSetRate = false; return; @@ -410,6 +477,7 @@ class Player { } public function isVideoLoaded():Bool { + if (player == null) return false; return player.isVideoLoaded(); } @@ -418,6 +486,12 @@ class Player { if (player == null) return; if (!player.isVideoLoaded()) return; player.play(); + if (needsVolumeReset) setVolume(1); + + if (audioTrack != null) { + setVolume(0.3); + audioTrack?.play(); + } } public function pause():Void { @@ -425,6 +499,8 @@ class Player { if (player == null) return; if (!player.isVideoLoaded()) return; player.pause(); + + audioTrack?.pause(); } public function getTime():Float { @@ -439,6 +515,8 @@ class Player { if (!player.isVideoLoaded()) return; skipSetTime = isLocal; player.setTime(time); + + if (audioTrack != null) audioTrack.currentTime = time; } public function getPlaybackRate():Float { @@ -453,6 +531,8 @@ class Player { if (!player.isVideoLoaded()) return; skipSetRate = isLocal; player.setPlaybackRate(rate); + + if (audioTrack != null) audioTrack.playbackRate = rate; } public function skipAd():Void { @@ -484,4 +564,36 @@ class Player { http.onError = msg -> trace(msg); http.request(); } + + public function isPaused():Bool { + if (player == null) return true; + if (!player.isVideoLoaded()) return true; + return player.isPaused(); + } + + public function getVolume():Float { + if (player == null) return 1; + if (!player.isVideoLoaded()) return 1; + return player.getVolume(); + } + + public function setVolume(volume:Float):Void { + if (player == null) return; + if (!player.isVideoLoaded()) return; + player.setVolume(volume); + } + + public function unmute():Void { + if (player == null) return; + if (!player.isVideoLoaded()) return; + player.unmute(); + if (audioTrack != null) audioTrack.muted = false; + if (audioTrack == null && almostEq(getVolume(), voiceOverVolume, 0.01)) { + setVolume(1); + } + } + + function almostEq(a:Float, b:Float, diff:Float):Bool { + return a > b - diff && a < b + diff; + } } diff --git a/src/client/players/Iframe.hx b/src/client/players/Iframe.hx index e07f814..56cf319 100644 --- a/src/client/players/Iframe.hx +++ b/src/client/players/Iframe.hx @@ -34,7 +34,8 @@ class Iframe implements IPlayer { function isValidIframe(iframe:Element):Bool { if (iframe.children.length != 1) return false; - return (iframe.firstChild.nodeName == "IFRAME" || iframe.firstChild.nodeName == "OBJECT"); + return (iframe.firstChild.nodeName == "IFRAME" + || iframe.firstChild.nodeName == "OBJECT"); } public function loadVideo(item:VideoItem):Void { @@ -66,6 +67,10 @@ class Iframe implements IPlayer { public function pause():Void {} + public function isPaused():Bool { + return false; + } + public function getTime():Float { return 0; } @@ -77,4 +82,12 @@ class Iframe implements IPlayer { } public function setPlaybackRate(rate:Float):Void {} + + public function getVolume():Float { + return 1; + } + + public function setVolume(volume:Float) {} + + public function unmute():Void {} } diff --git a/src/client/players/Raw.hx b/src/client/players/Raw.hx index 3f69958..5c5b7a4 100644 --- a/src/client/players/Raw.hx +++ b/src/client/players/Raw.hx @@ -67,7 +67,7 @@ class Raw implements IPlayer { callback({ duration: video.duration, title: title, - subs: subs + subs: subs, }); } Utils.prepend(playerEl, video); @@ -115,6 +115,7 @@ class Raw implements IPlayer { } video.onpause = player.onPause; video.onratechange = player.onRateChange; + if (!main.isAutoplayAllowed()) video.muted = true; playerEl.appendChild(video); } if (isHls) initHlsSource(video, url); @@ -185,6 +186,10 @@ class Raw implements IPlayer { video.pause(); } + public function isPaused():Bool { + return video.paused; + } + public function getTime():Float { return video.currentTime; } @@ -200,4 +205,16 @@ class Raw implements IPlayer { public function setPlaybackRate(rate:Float):Void { video.playbackRate = rate; } + + public function getVolume():Float { + return video.volume; + } + + public function setVolume(volume:Float):Void { + video.volume = volume; + } + + public function unmute():Void { + video.muted = false; + } } diff --git a/src/client/players/Youtube.hx b/src/client/players/Youtube.hx index cde2ef8..3ad9030 100644 --- a/src/client/players/Youtube.hx +++ b/src/client/players/Youtube.hx @@ -86,10 +86,11 @@ class Youtube implements IPlayer { final duration = convertTime(duration); // duration is PT0S for streams if (duration == 0) { + final mute = main.isAutoplayAllowed() ? "" : "&mute=1"; callback({ duration: 99 * 60 * 60, title: title, - url: '<iframe src="https://www.youtube.com/embed/$id" frameborder="0" + url: '<iframe src="https://www.youtube.com/embed/$id?autoplay=1$mute" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>', isIframe: true @@ -211,13 +212,14 @@ class Youtube implements IPlayer { videoId: extractVideoId(item.url), playerVars: { autoplay: 1, + // play videos inline instead of fullscreen on iOS playsinline: 1, - modestbranding: 1, + // related videos only from same channel rel: 0, - showinfo: 0 }, events: { onReady: e -> { + if (!main.isAutoplayAllowed()) e.target.mute(); isLoaded = true; youtube.pauseVideo(); }, @@ -271,6 +273,7 @@ class Youtube implements IPlayer { info.formats ??= []; info.adaptiveFormats ??= []; final formats = info.adaptiveFormats.concat(info.formats); + trace(formats); final qPriority = [1080, 720, 480, 360, 240]; for (q in qPriority) { final quality = '${q}p'; @@ -304,6 +307,10 @@ class Youtube implements IPlayer { youtube.pauseVideo(); } + public function isPaused():Bool { + return youtube.getPlayerState() == PAUSED; + } + public function getTime():Float { return youtube.getCurrentTime(); } @@ -319,4 +326,17 @@ class Youtube implements IPlayer { public function setPlaybackRate(rate:Float):Void { youtube.setPlaybackRate(rate); } + + public function getVolume():Float { + if (youtube.isMuted()) return 0; + return youtube.getVolume() / 100; + } + + public function setVolume(volume:Float):Void { + youtube.setVolume(Std.int(volume * 100)); + } + + public function unmute():Void { + youtube.unMute(); + } } diff --git a/src/server/Main.hx b/src/server/Main.hx index a0b255e..9c2f6e2 100644 --- a/src/server/Main.hx +++ b/src/server/Main.hx @@ -139,6 +139,8 @@ class Main { trace("Global network is disabled in config"); } else { if (!isNoState) Utils.getGlobalIp(ip -> { + final isIp6 = ip.contains(":"); + if (isIp6) ip = '[$ip]'; globalIp = ip; trace('Global: http://$globalIp:$port'); }); |
