aboutsummaryrefslogtreecommitdiffstats
path: root/src/client
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 /src/client
parent82a6c65d46e2583883b1b01d706145386308d19e (diff)
Add /volume command
Diffstat (limited to 'src/client')
-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
6 files changed, 168 insertions, 38 deletions
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, {
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage