diff options
Diffstat (limited to 'src')
| -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 |
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'); }); |
