aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorRblSb <msrblsb@gmail.com>2025-01-12 19:35:56 +0300
committerRblSb <msrblsb@gmail.com>2025-01-12 22:35:22 +0300
commitf84fdc40ba817b6a2d907484b1e1500197ceeafe (patch)
tree73a5b81e082d0ac1741c24742db12e6c2bd54249 /src
parent25b7ecb45d43018235c6a8eb5b4ce833f2dec668 (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).
Diffstat (limited to 'src')
-rw-r--r--src/Types.hx3
-rw-r--r--src/client/Buttons.hx1
-rw-r--r--src/client/IPlayer.hx4
-rw-r--r--src/client/Main.hx29
-rw-r--r--src/client/Player.hx124
-rw-r--r--src/client/players/Iframe.hx15
-rw-r--r--src/client/players/Raw.hx19
-rw-r--r--src/client/players/Youtube.hx26
-rw-r--r--src/server/Main.hx2
9 files changed, 210 insertions, 13 deletions
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');
});
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage