aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRblSb <msrblsb@gmail.com>2025-02-12 11:33:18 +0300
committerRblSb <msrblsb@gmail.com>2025-02-13 06:42:50 +0300
commit74244ebef3e3e4cbd9af50ca19af2affb39ba0f1 (patch)
treebcb763cc38a7c69c748a5523f3e1db7aa8f05d32
parent82a6c65d46e2583883b1b01d706145386308d19e (diff)
Add /volume command
-rw-r--r--README.md6
-rw-r--r--build-client.hxml2
-rw-r--r--build-server.hxml2
-rw-r--r--res/client.js271
-rw-r--r--res/css/des.css2
-rw-r--r--src/client/Main.hx17
-rw-r--r--src/client/Player.hx9
-rw-r--r--src/client/Utils.hx7
-rw-r--r--src/client/players/Raw.hx169
-rw-r--r--src/client/players/Vk.hx2
-rw-r--r--src/client/players/Youtube.hx2
-rw-r--r--tests.hxml2
12 files changed, 400 insertions, 91 deletions
diff --git a/README.md b/README.md
index c10895c..1af37ff 100644
--- a/README.md
+++ b/README.md
@@ -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, {
diff --git a/tests.hxml b/tests.hxml
index 97efb41..7541cc2 100644
--- a/tests.hxml
+++ b/tests.hxml
@@ -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
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage