diff options
| author | RblSb <msrblsb@gmail.com> | 2025-02-12 11:33:18 +0300 |
|---|---|---|
| committer | RblSb <msrblsb@gmail.com> | 2025-02-13 06:42:50 +0300 |
| commit | 74244ebef3e3e4cbd9af50ca19af2affb39ba0f1 (patch) | |
| tree | bcb763cc38a7c69c748a5523f3e1db7aa8f05d32 | |
| parent | 82a6c65d46e2583883b1b01d706145386308d19e (diff) | |
Add /volume command
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | build-client.hxml | 2 | ||||
| -rw-r--r-- | build-server.hxml | 2 | ||||
| -rw-r--r-- | res/client.js | 271 | ||||
| -rw-r--r-- | res/css/des.css | 2 | ||||
| -rw-r--r-- | src/client/Main.hx | 17 | ||||
| -rw-r--r-- | src/client/Player.hx | 9 | ||||
| -rw-r--r-- | src/client/Utils.hx | 7 | ||||
| -rw-r--r-- | src/client/players/Raw.hx | 169 | ||||
| -rw-r--r-- | src/client/players/Vk.hx | 2 | ||||
| -rw-r--r-- | src/client/players/Youtube.hx | 2 | ||||
| -rw-r--r-- | tests.hxml | 2 |
12 files changed, 400 insertions, 91 deletions
@@ -68,9 +68,11 @@ It's just works, but you can also check [user/ folder](/user/README.md) for serv - If you want to restrict permissions or add admins/emotes, see `Configuration` above
## Chat commands
-- `/-1h9m54` - Command format to rewind video **back** by `1 hour 9 minutes 54 seconds`
+- `/1h9m54` - Command format to rewind video by `1 hour 9 minutes 54 seconds`
+- `/-1h9m54` - Same, but rewinds back
- `/ad` - Rewind sponsored block in active YouTube video
-- `/fb` (`/flashback`) - rewind video to a prev time if someone rewinded/restarted video accidentally
+- `/fb` (`/flashback`) - Rewind video to a prev time if someone rewinded/restarted video accidentally
+- `/volume 2.6` - Change player volume in `0-1` range or boost it in `0-3` range for quiet videos
- `/clear` - Clear chat. Admin clears chat globally
- `/help` - Show initial tutorial message
diff --git a/build-client.hxml b/build-client.hxml index 6bae635..c3dcd74 100644 --- a/build-client.hxml +++ b/build-client.hxml @@ -1,5 +1,5 @@ --library youtubeIFramePlayer:git:https://github.com/okawa-h/youtubeIFramePlayer-externs.git
---library hls.js-extern:git:https://github.com/grosmar/hls.js-haxe-extern.git
+--library hls.js-extern:git:https://github.com/zoldesi-andor/hls.js-haxe-extern.git
--class-path src
--main client.Main
-D analyzer-optimize
diff --git a/build-server.hxml b/build-server.hxml index 25aa12c..fdf77c0 100644 --- a/build-server.hxml +++ b/build-server.hxml @@ -4,7 +4,7 @@ -D junsafe_compiler_cache
# Client libs for completion
--library youtubeIFramePlayer:git:https://github.com/okawa-h/youtubeIFramePlayer-externs.git
---library hls.js-extern:git:https://github.com/grosmar/hls.js-haxe-extern.git
+--library hls.js-extern:git:https://github.com/zoldesi-andor/hls.js-haxe-extern.git
--library utest
--class-path src
--main server.Main
diff --git a/res/client.js b/res/client.js index e818b1f..7946acf 100644 --- a/res/client.js +++ b/res/client.js @@ -2090,7 +2090,7 @@ client_Main.prototype = { return; } if(name.length == 0) { - return; + name = this.settings.name; } var hash = haxe_crypto_Sha256.encode(password + this.config.salt); this.loginRequest(name,hash); @@ -2517,6 +2517,27 @@ client_Main.prototype = { this.mergeRedundantArgs(args,0,1); this.send({ type : "BanClient", banClient : { name : args[0], time : 0}}); return true; + case "volume": + var v = parseFloat(args[0]); + if(isNaN(v)) { + v = 1; + } + if(v < 0) { + v = 0; + } else if(v > 3) { + v = 3; + } + var wasNotFull = this.player.getVolume() < 1; + this.player.setVolume(v < 0 ? 0 : v > 1 ? 1 : v); + if(this.player.getPlayerType() != "RawType") { + return true; + } + if(wasNotFull && v > 1) { + this.serverMessage("Volume was not maxed yet to be boosted, you can send command again."); + return true; + } + this.player.rawPlayer.boostVolume(v); + return true; } if(this.matchSimpleDate.match(command)) { this.send({ type : "Rewind", rewind : { time : this.parseSimpleDate(command)}}); @@ -2740,6 +2761,10 @@ client_Player.prototype = { var el = this.videoItemsEl.children[pos]; this.setItemElementType(el,this.videoList.items[pos].isTemp); } + ,getCurrentItem: function() { + var _this = this.videoList; + return _this.items[_this.pos]; + } ,setPlayer: function(newPlayer) { if(this.player != newPlayer) { if(this.player != null) { @@ -2768,6 +2793,12 @@ client_Player.prototype = { callback(data); }); } + ,getPlayerType: function() { + if(this.player == null) { + return null; + } + return this.player.getPlayerType(); + } ,getLinkPlayerType: function(url) { var player = Lambda.find(this.players,function(player) { return player.isSupportedLink(url); @@ -2836,7 +2867,7 @@ client_Player.prototype = { return _gthis.isAudioTrackLoaded = true; }; this.audioTrack.onerror = function(e) { - haxe_Log.trace(e,{ fileName : "src/client/Player.hx", lineNumber : 215, className : "client.Player", methodName : "setExternalAudioTrack"}); + haxe_Log.trace(e,{ fileName : "src/client/Player.hx", lineNumber : 220, className : "client.Player", methodName : "setExternalAudioTrack"}); _gthis.audioTrack.oncanplay = null; _gthis.audioTrack.onerror = null; _gthis.isAudioTrackLoaded = false; @@ -3339,7 +3370,7 @@ client_Player.prototype = { } }; http.onError = function(msg) { - haxe_Log.trace(msg,{ fileName : "src/client/Player.hx", lineNumber : 661, className : "client.Player", methodName : "skipAd"}); + haxe_Log.trace(msg,{ fileName : "src/client/Player.hx", lineNumber : 666, className : "client.Player", methodName : "skipAd"}); }; http.request(); } @@ -3630,6 +3661,18 @@ client_Utils.saveFile = function(name,mime,data) { client_Utils.createResizeObserver = function(callback) { return null; }; +client_Utils.createAudioContext = function() { + var w = window; + var ctx; + var tmp = w.AudioContext; + var tmp1 = tmp != null ? tmp : w.webkitAudioContext; + if(tmp1 != null) { + ctx = tmp1; + } else { + return null; + } + return new ctx(); +}; var client_players_Iframe = function(main,player) { this.playerEl = window.document.querySelector("#ytapiplayer"); this.main = main; @@ -3712,7 +3755,7 @@ client_players_Iframe.prototype = { } }; var client_players_Raw = function(main,player) { - this.isHlsLoaded = false; + this.isHlsPluginLoaded = false; this.playAllowed = true; this.matchName = new EReg("^(.+)\\.(.+)",""); this.subsInput = window.document.querySelector("#subsurl"); @@ -3729,27 +3772,27 @@ client_players_Raw.prototype = { ,isSupportedLink: function(url) { return true; } + ,isHlsItem: function(url,title) { + if(url.indexOf("m3u8") == -1) { + return StringTools.endsWith(title,"m3u8"); + } else { + return true; + } + } ,getVideoData: function(data,callback) { var _gthis = this; var url = data.url; - var decodedUrl = decodeURIComponent(url.split("+").join(" ")); - var optTitle = StringTools.trim(this.titleInput.value); - var title = HxOverrides.substr(decodedUrl,decodedUrl.lastIndexOf("/") + 1,null); - var isNameMatched = this.matchName.match(title); - if(optTitle != "") { - title = optTitle; - } else if(isNameMatched) { - title = this.matchName.matched(1); - } else { - title = Lang.get("rawVideo"); - } - var isHls = false; - if(isNameMatched) { - isHls = this.matchName.matched(2).indexOf("m3u8") != -1; - } else { - isHls = StringTools.endsWith(title,"m3u8"); + var title = StringTools.trim(this.titleInput.value); + if(title.length == 0) { + var decodedUrl = decodeURIComponent(url.split("+").join(" ")); + if(this.matchName.match(HxOverrides.substr(decodedUrl,decodedUrl.lastIndexOf("/") + 1,null))) { + title = this.matchName.matched(1); + } else { + title = Lang.get("rawVideo"); + } } - if(isHls && !this.isHlsLoaded) { + var isHls = this.isHlsItem(url,title); + if(isHls && !this.isHlsPluginLoaded) { this.loadHlsPlugin(function() { _gthis.getVideoData(data,callback); }); @@ -3758,62 +3801,119 @@ client_players_Raw.prototype = { this.titleInput.value = ""; var subs = StringTools.trim(this.subsInput.value); this.subsInput.value = ""; + this.getVideoDuration(url,isHls,function(duration) { + if(duration == 0) { + callback({ duration : duration}); + return; + } + callback({ duration : duration, title : title, subs : subs}); + }); + } + ,getVideoDuration: function(url,isHls,callback,isAnonCrossOrigin) { + if(isAnonCrossOrigin == null) { + isAnonCrossOrigin = false; + } + var _gthis = this; var video = window.document.createElement("video"); - video.id = "temp-videoplayer"; + if(isAnonCrossOrigin) { + video.crossOrigin = "anonymous"; + } + video.className = "temp-videoplayer"; video.src = url; + var tempHls = null; video.onerror = function(e) { + callback(0); if(_gthis.playerEl.contains(video)) { _gthis.playerEl.removeChild(video); } - callback({ duration : 0}); + video.onerror = null; + video.onloadedmetadata = null; + if(tempHls != null) { + tempHls.destroy(); + } + video.pause(); + video.removeAttribute("src"); + video.load(); }; video.onloadedmetadata = function() { + callback(video.duration); if(_gthis.playerEl.contains(video)) { _gthis.playerEl.removeChild(video); } - callback({ duration : video.duration, title : title, subs : subs}); + video.onerror = null; + video.onloadedmetadata = null; + if(tempHls != null) { + tempHls.destroy(); + } + video.pause(); + video.removeAttribute("src"); + video.load(); }; - this.playerEl.prepend(video); if(isHls) { - this.initHlsSource(video,url); + tempHls = this.initHlsSource(video,url); + tempHls.on(Hls.Events.ERROR,function(errorType,e) { + callback(0); + if(_gthis.playerEl.contains(video)) { + _gthis.playerEl.removeChild(video); + } + video.onerror = null; + video.onloadedmetadata = null; + if(tempHls != null) { + tempHls.destroy(); + } + video.pause(); + video.removeAttribute("src"); + video.load(); + }); } + this.playerEl.prepend(video); } ,loadHlsPlugin: function(callback) { var _gthis = this; client_JsApi.addScriptToHead("https://cdn.jsdelivr.net/npm/hls.js@latest",function() { - _gthis.isHlsLoaded = true; + _gthis.isHlsPluginLoaded = true; callback(); }); } - ,initHlsSource: function(video,url) { + ,initHlsSource: function(video,url,hls) { if(!Hls.isSupported()) { - return; + return null; + } + if(hls != null) { + hls.detachMedia(); + } + if(hls == null) { + hls = new Hls(); } - var hls = new Hls(); hls.loadSource(url); hls.attachMedia(video); + return hls; } ,loadVideo: function(item) { var _gthis = this; var url = this.main.tryLocalIp(item.url); - var isHls = url.indexOf("m3u8") != -1 || StringTools.endsWith(item.title,"m3u8"); - if(isHls && !this.isHlsLoaded) { + var isHls = this.isHlsItem(url,item.title); + if(isHls && !this.isHlsPluginLoaded) { this.loadHlsPlugin(function() { _gthis.loadVideo(item); }); return; } + if(this.audioCtx != null) { + this.removeVideo(); + } if(this.video != null) { + var tmp = this.hls; + if(tmp != null) { + tmp.detachMedia(); + } this.video.src = url; - var _g = 0; - var _g1 = this.video.children; - while(_g < _g1.length) { - var element = _g1[_g]; - ++_g; - if(element.nodeName != "TRACK") { - continue; + var i = this.video.children.length; + while(i-- > 0) { + var child = this.video.children[i]; + if(child.nodeName == "TRACK") { + child.remove(); } - element.remove(); } } else { this.video = window.document.createElement("video"); @@ -3834,7 +3934,7 @@ client_players_Raw.prototype = { this.playerEl.appendChild(this.video); } if(isHls) { - this.initHlsSource(this.video,url); + this.hls = this.initHlsSource(this.video,url,this.hls); } this.restartControlsHider(); var subsUrl; @@ -3873,8 +3973,9 @@ client_players_Raw.prototype = { if(client_Utils.isTouch()) { return; } - if(this.controlsHider != null) { - this.controlsHider.stop(); + var tmp = this.controlsHider; + if(tmp != null) { + tmp.stop(); } this.controlsHider = haxe_Timer.delay(function() { if(_gthis.video == null) { @@ -3883,17 +3984,93 @@ client_players_Raw.prototype = { _gthis.video.controls = false; },3000); this.video.onmousemove = function(e) { - if(_gthis.controlsHider != null) { - _gthis.controlsHider.stop(); + var tmp = _gthis.controlsHider; + if(tmp != null) { + tmp.stop(); } _gthis.video.controls = true; return _gthis.video.onmousemove = null; }; } + ,boostVolume: function(volume) { + var _gthis = this; + if(this.gainNode != null) { + this.gainNode.gain.value = volume; + return; + } + if(volume <= 1) { + return; + } + if(this.video.crossOrigin != "anonymous") { + var tmp = this.player.getCurrentItem(); + if(tmp == null) { + return; + } + var isHls = this.isHlsItem(tmp.url,tmp.title); + this.getVideoDuration(tmp.url,isHls,function(duration) { + if(duration == 0) { + _gthis.main.serverMessage("Cannot boost volume for this video, no CORS access."); + } else { + _gthis.video.crossOrigin = "anonymous"; + _gthis.boostVolume(volume); + } + },true); + return; + } + var tmp; + if(this.audioCtx != null) { + tmp = this.audioCtx; + } else { + var tmp1 = client_Utils.createAudioContext(); + if(tmp1 != null) { + tmp = tmp1; + } else { + return; + } + } + this.audioCtx = tmp; + var sourceNode = this.audioCtx.createMediaElementSource(this.video); + this.gainNode = this.audioCtx.createGain(); + this.gainNode.gain.value = volume; + sourceNode.connect(this.gainNode); + this.gainNode.connect(this.audioCtx.destination); + var analyzer = this.audioCtx.createAnalyser(); + analyzer.fftSize = 256; + sourceNode.connect(analyzer); + var arrayBuffer = new Uint8Array(256); + haxe_Timer.delay(function() { + analyzer.getByteFrequencyData(arrayBuffer); + var sum = 0; + var _g = 0; + while(_g < arrayBuffer.length) sum += arrayBuffer[_g++]; + if(sum == 0) { + var tmp = _gthis.player.getCurrentItem(); + if(tmp == null) { + return; + } + _gthis.video.src = tmp.url; + _gthis.isHlsItem(tmp.url,tmp.title); + _gthis.initHlsSource(_gthis.video,tmp.url,_gthis.hls); + } + },300); + } + ,destroyAudioContext: function() { + if(this.audioCtx == null) { + return; + } + this.gainNode = null; + this.audioCtx.close(); + this.audioCtx = null; + } ,removeVideo: function() { if(this.video == null) { return; } + this.destroyAudioContext(); + var tmp = this.hls; + if(tmp != null) { + tmp.detachMedia(); + } this.video.pause(); this.video.removeAttribute("src"); this.video.load(); @@ -4266,7 +4443,7 @@ client_players_Vk.prototype = { callback({ duration : 0}); 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>")); + var tempVideo = client_Utils.nodeFromString(StringTools.trim("<iframe class=\"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>")); this.playerEl.prepend(tempVideo); var tempVkPlayer = this.createVkPlayer(tempVideo); tempVkPlayer.on("inited",function() { @@ -4531,7 +4708,7 @@ client_players_Youtube.prototype = { return; } var video = window.document.createElement("div"); - video.id = "temp-videoplayer"; + video.className = "temp-videoplayer"; 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) { diff --git a/res/css/des.css b/res/css/des.css index 0a88856..5d90b2c 100644 --- a/res/css/des.css +++ b/res/css/des.css @@ -448,7 +448,7 @@ header h4 { max-height: 80vh; } -#temp-videoplayer { +.temp-videoplayer { display: none; } diff --git a/src/client/Main.hx b/src/client/Main.hx index 72a619a..660693b 100644 --- a/src/client/Main.hx +++ b/src/client/Main.hx @@ -824,7 +824,7 @@ class Main { public function userLogin(name:String, password:String):Void { if (config.salt == null) return; if (password.length == 0) return; - if (name.length == 0) return; + if (name.length == 0) name = settings.name; final hash = Sha256.encode(password + config.salt); loginRequest(name, hash); settings.hash = hash; @@ -1277,6 +1277,21 @@ class Main { case "ad": player.skipAd(); return false; + case "volume": + var v = Std.parseFloat(args[0]); + if (Math.isNaN(v)) v = 1; + v = v.clamp(0, 3); + final wasNotFull = player.getVolume() < 1; + player.setVolume(v.clamp(0, 1)); + + if (player.getPlayerType() != RawType) return true; + if (wasNotFull && v > 1) { + serverMessage("Volume was not maxed yet to be boosted, you can send command again."); + return true; + } + final rawPlayer = @:privateAccess player.rawPlayer; + rawPlayer.boostVolume(v); + return true; case "dump": send({type: Dump}); return true; diff --git a/src/client/Player.hx b/src/client/Player.hx index 64248fe..6be8a07 100644 --- a/src/client/Player.hx +++ b/src/client/Player.hx @@ -21,8 +21,8 @@ class Player { final main:Main; final youtube:Youtube; final players:Array<IPlayer>; - final iframePlayer:IPlayer; - final rawPlayer:IPlayer; + final iframePlayer:Iframe; + final rawPlayer:Raw; final videoList = new VideoList(); final videoItemsEl = getEl("#queue"); final playerEl = getEl("#ytapiplayer"); @@ -153,6 +153,11 @@ class Player { }); } + public function getPlayerType():Null<PlayerType> { + if (player == null) return null; + return player.getPlayerType(); + } + public function getLinkPlayerType(url:String):PlayerType { final player = players.find(player -> player.isSupportedLink(url)); if (player == null) return rawPlayer.getPlayerType(); diff --git a/src/client/Utils.hx b/src/client/Utils.hx index a120166..9217e07 100644 --- a/src/client/Utils.hx +++ b/src/client/Utils.hx @@ -7,6 +7,7 @@ import js.Browser.window; import js.html.Element; import js.html.FileReader; import js.html.URL; +import js.html.audio.AudioContext; import js.lib.ArrayBuffer; class Utils { @@ -205,4 +206,10 @@ class Utils { final observer = (window : Dynamic).ResizeObserver ?? return null; return js.Syntax.code("new ResizeObserver({0})", callback); } + + public static function createAudioContext():Null<AudioContext> { + final w:Dynamic = js.Browser.window; + final ctx = w.AudioContext ?? w.webkitAudioContext ?? return null; + return js.Syntax.code("new {0}()", ctx); + } } diff --git a/src/client/players/Raw.hx b/src/client/players/Raw.hx index e3337ae..f51bd9e 100644 --- a/src/client/players/Raw.hx +++ b/src/client/players/Raw.hx @@ -13,6 +13,9 @@ import js.html.Element; import js.html.InputElement; import js.html.URL; import js.html.VideoElement; +import js.html.audio.AudioContext; +import js.html.audio.GainNode; +import js.lib.Uint8Array; class Raw implements IPlayer { final main:Main; @@ -24,7 +27,10 @@ class Raw implements IPlayer { var controlsHider:Timer; var playAllowed = true; var video:VideoElement; - var isHlsLoaded = false; + var isHlsPluginLoaded = false; + var hls:Hls; + var audioCtx:AudioContext; + var gainNode:GainNode; public function new(main:Main, player:Player) { this.main = main; @@ -39,21 +45,23 @@ class Raw implements IPlayer { return true; } + public function isHlsItem(url:String, title:String):Bool { + return url.contains("m3u8") || title.endsWith("m3u8"); + } + public function getVideoData(data:VideoDataRequest, callback:(data:VideoData) -> Void):Void { final url = data.url; - final decodedUrl = url.urlDecode(); - final optTitle = titleInput.value.trim(); - var title = decodedUrl.substr(decodedUrl.lastIndexOf("/") + 1); - final isNameMatched = matchName.match(title); - if (optTitle != "") title = optTitle; - else if (isNameMatched) title = matchName.matched(1); - else title = Lang.get("rawVideo"); + var title = titleInput.value.trim(); + if (title.length == 0) { + final decodedUrl = url.urlDecode(); + final lastPart = decodedUrl.substr(decodedUrl.lastIndexOf("/") + 1); + if (matchName.match(lastPart)) title = matchName.matched(1); + else title = Lang.get("rawVideo"); + } - var isHls = false; - if (isNameMatched) isHls = matchName.matched(2).contains("m3u8"); - else isHls = title.endsWith("m3u8"); - if (isHls && !isHlsLoaded) { + final isHls = isHlsItem(url, title); + if (isHls && !isHlsPluginLoaded) { loadHlsPlugin(() -> getVideoData(data, callback)); return; } @@ -61,52 +69,88 @@ class Raw implements IPlayer { titleInput.value = ""; final subs = subsInput.value.trim(); subsInput.value = ""; + + getVideoDuration(url, isHls, duration -> { + if (duration == 0) { + callback({duration: duration}); + return; + } + callback({ + duration: duration, + title: title, + subs: subs, + }); + }); + } + + function getVideoDuration(url:String, isHls:Bool, callback:(duration:Float) -> + Void, isAnonCrossOrigin = false):Void { final video = document.createVideoElement(); - video.id = "temp-videoplayer"; + if (isAnonCrossOrigin) video.crossOrigin = "anonymous"; + video.className = "temp-videoplayer"; video.src = url; - video.onerror = e -> { + var tempHls:Hls = null; + inline function dispose():Void { if (playerEl.contains(video)) playerEl.removeChild(video); - callback({duration: 0}); + video.onerror = null; + video.onloadedmetadata = null; + tempHls?.destroy(); + video.pause(); + video.removeAttribute("src"); + video.load(); + } + video.onerror = e -> { + callback(0); + dispose(); } video.onloadedmetadata = () -> { - if (playerEl.contains(video)) playerEl.removeChild(video); - callback({ - duration: video.duration, - title: title, - subs: subs, + callback(video.duration); + dispose(); + } + if (isHls) { + tempHls = initHlsSource(video, url); + tempHls.on(Hls.Events.ERROR, (errorType, e) -> { + callback(0); + dispose(); }); } playerEl.prepend(video); - if (isHls) initHlsSource(video, url); } function loadHlsPlugin(callback:() -> Void):Void { final url = "https://cdn.jsdelivr.net/npm/hls.js@latest"; JsApi.addScriptToHead(url, () -> { - isHlsLoaded = true; + isHlsPluginLoaded = true; callback(); }); } - function initHlsSource(video:VideoElement, url:String):Void { - if (!Hls.isSupported()) return; - final hls = new Hls(); + function initHlsSource(video:VideoElement, url:String, ?hls:Hls):Null<Hls> { + if (!Hls.isSupported()) return null; + hls?.detachMedia(); + hls ??= new Hls(); hls.loadSource(url); hls.attachMedia(video); + return hls; } public function loadVideo(item:VideoItem):Void { final url = main.tryLocalIp(item.url); - final isHls = url.contains("m3u8") || item.title.endsWith("m3u8"); - if (isHls && !isHlsLoaded) { + final isHls = isHlsItem(url, item.title); + if (isHls && !isHlsPluginLoaded) { loadHlsPlugin(() -> loadVideo(item)); return; } + // we need to fully reset element if we had audio handling + if (audioCtx != null) removeVideo(); if (video != null) { + hls?.detachMedia(); video.src = url; - for (element in video.children) { - if (element.nodeName != "TRACK") continue; - element.remove(); + + var i = video.children.length; + while (i-- > 0) { + final child = video.children[i]; + if (child.nodeName == "TRACK") child.remove(); } } else { video = document.createVideoElement(); @@ -124,7 +168,7 @@ class Raw implements IPlayer { if (!main.isAutoplayAllowed()) video.muted = true; playerEl.appendChild(video); } - if (isHls) initHlsSource(video, url); + if (isHls) hls = initHlsSource(video, url, hls); restartControlsHider(); var subsUrl = item.subs ?? return; @@ -153,20 +197,79 @@ class Raw implements IPlayer { function restartControlsHider():Void { video.controls = true; if (Utils.isTouch()) return; - if (controlsHider != null) controlsHider.stop(); + controlsHider?.stop(); controlsHider = Timer.delay(() -> { if (video == null) return; video.controls = false; }, 3000); video.onmousemove = e -> { - if (controlsHider != null) controlsHider.stop(); + controlsHider?.stop(); video.controls = true; video.onmousemove = null; } } + public function boostVolume(volume:Float):Void { + if (gainNode != null) { + gainNode.gain.value = volume; + return; + } + if (volume <= 1) return; + if (video.crossOrigin != "anonymous") { + final item = player.getCurrentItem() ?? return; + final isHls = isHlsItem(item.url, item.title); + getVideoDuration(item.url, isHls, duration -> { + if (duration == 0) { + main.serverMessage("Cannot boost volume for this video, no CORS access."); + } else { + video.crossOrigin = "anonymous"; + boostVolume(volume); + } + }, true); + return; + } + audioCtx ??= Utils.createAudioContext() ?? return; + final sourceNode = audioCtx.createMediaElementSource(video); + gainNode = audioCtx.createGain(); + gainNode.gain.value = volume; + sourceNode.connect(gainNode); + gainNode.connect(audioCtx.destination); + + // we need silence check if audio context is too picky about cors + final analyzer = audioCtx.createAnalyser(); + final bufferSize = 256; + analyzer.fftSize = bufferSize; + sourceNode.connect(analyzer); + final arrayBuffer = new Uint8Array(bufferSize); + inline function isSilence():Bool { + analyzer.getByteFrequencyData(arrayBuffer); + var sum = 0; + for (i in arrayBuffer) sum += i; + return sum == 0; + } + // src refresh should be enough since video with + // crossOrigin="anonymous" loads finely + Timer.delay(() -> { + if (isSilence()) { + final item = player.getCurrentItem() ?? return; + video.src = item.url; + final isHls = isHlsItem(item.url, item.title); + initHlsSource(video, item.url, hls); + } + }, 300); + } + + function destroyAudioContext():Void { + if (audioCtx == null) return; + gainNode = null; + audioCtx.close(); + audioCtx = null; + } + public function removeVideo():Void { if (video == null) return; + destroyAudioContext(); + hls?.detachMedia(); video.pause(); video.removeAttribute("src"); video.load(); diff --git a/src/client/players/Vk.hx b/src/client/players/Vk.hx index 99643de..f1976ca 100644 --- a/src/client/players/Vk.hx +++ b/src/client/players/Vk.hx @@ -102,7 +102,7 @@ class Vk implements IPlayer { final oid = ids.oid; final id = ids.id; final tempVideo = Utils.nodeFromString( - '<iframe id="temp-videoplayer" src="https://vk.com/video_ext.php?oid=$oid&id=$id&hd=1&js_api=1" + '<iframe class="temp-videoplayer" src="https://vk.com/video_ext.php?oid=$oid&id=$id&hd=1&js_api=1" allow="autoplay; encrypted-media; fullscreen; picture-in-picture;" frameborder="0" allowfullscreen> </iframe>'.trim() diff --git a/src/client/players/Youtube.hx b/src/client/players/Youtube.hx index 7c851fb..6865394 100644 --- a/src/client/players/Youtube.hx +++ b/src/client/players/Youtube.hx @@ -174,7 +174,7 @@ class Youtube implements IPlayer { return; } final video = document.createDivElement(); - video.id = "temp-videoplayer"; + video.className = "temp-videoplayer"; playerEl.prepend(video); var tempYoutube:YoutubePlayer = null; tempYoutube = new YoutubePlayer(video.id, { @@ -3,7 +3,7 @@ --library json2object # Client libs for completion --library youtubeIFramePlayer:git:https://github.com/okawa-h/youtubeIFramePlayer-externs.git ---library hls.js-extern:git:https://github.com/grosmar/hls.js-haxe-extern.git +--library hls.js-extern:git:https://github.com/zoldesi-andor/hls.js-haxe-extern.git --library utest --class-path src --class-path test |
