From 600101bb1d093c2f0402ddf38a407f140c4329ed Mon Sep 17 00:00:00 2001 From: RblSb Date: Fri, 17 Jan 2025 04:11:46 +0300 Subject: VK player support --- README.md | 1 + build/server.js | 2 +- res/client.js | 170 ++++++++++++++++++++++++++++++++++- src/Types.hx | 1 + src/client/Player.hx | 2 + src/client/players/Vk.hx | 228 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 401 insertions(+), 3 deletions(-) create mode 100644 src/client/players/Vk.hx diff --git a/README.md b/README.md index f3f9725..09a3a47 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Default channel example: https://synctube.onrender.com/ ### Supported players - Youtube (videos, shorts, streams and playlists) - [Streamable](https://streamable.com) +- [VK](https://vk.com/video) - Raw mp4 videos and m3u8 playlists (or any other media format supported in browser) - Iframes (without sync) diff --git a/build/server.js b/build/server.js index 12effdf..6c00510 100644 --- a/build/server.js +++ b/build/server.js @@ -1399,7 +1399,7 @@ JsonParser_$45.prototype = $extend(json2object_reader_BaseParser.prototype,{ this.value = null; } ,loadJsonString: function(s,pos,variable) { - this.value = this.loadString(s,pos,variable,["RawType","YoutubeType","IframeType"],"RawType"); + this.value = this.loadString(s,pos,variable,["RawType","YoutubeType","VkType","IframeType"],"RawType"); } ,__class__: JsonParser_$45 }); diff --git a/res/client.js b/res/client.js index 338ed34..efcd05c 100644 --- a/res/client.js +++ b/res/client.js @@ -2453,7 +2453,7 @@ var client_Player = function(main) { this.main = main; this.youtube = new client_players_Youtube(main,this); this.streamable = new client_players_Streamable(main,this); - this.players = [this.youtube,this.streamable]; + this.players = [this.youtube,new client_players_Vk(main,this),this.streamable]; this.iframePlayer = new client_players_Iframe(main,this); this.rawPlayer = new client_players_Raw(main,this); this.initItemButtons(); @@ -2587,7 +2587,7 @@ client_Player.prototype = { return _gthis.isAudioTrackLoaded = true; }; this.audioTrack.onerror = function(e) { - haxe_Log.trace(e,{ fileName : "src/client/Player.hx", lineNumber : 189, className : "client.Player", methodName : "setExternalAudioTrack"}); + haxe_Log.trace(e,{ fileName : "src/client/Player.hx", lineNumber : 191, className : "client.Player", methodName : "setExternalAudioTrack"}); _gthis.audioTrack.oncanplay = null; _gthis.audioTrack.onerror = null; _gthis.isAudioTrackLoaded = false; @@ -3859,6 +3859,172 @@ client_players_Streamable.prototype = $extend(client_players_Raw.prototype,{ http.request(); } }); +var client_players_Vk = function(main,player) { + this.matchIds = new EReg("video(-?[0-9]+)_([0-9]+)","g"); + this.matchVk = new EReg("(vk.com/video|vkvideo)","g"); + this.isApiLoaded = false; + this.isLoaded = false; + this.playerEl = window.document.querySelector("#ytapiplayer"); + this.main = main; + this.player = player; +}; +client_players_Vk.__name__ = true; +client_players_Vk.prototype = { + getPlayerType: function() { + return "VkType"; + } + ,isSupportedLink: function(url) { + if(this.matchVk.match(url)) { + return this.getVideoIds(url) != null; + } else { + return false; + } + } + ,getVideoIds: function(url) { + if(!this.matchIds.match(url)) { + haxe_Log.trace("Cannot extract /video-oid_id values from url:",{ fileName : "src/client/players/Vk.hx", lineNumber : 68, className : "client.players.Vk", methodName : "getVideoIds"}); + return null; + } + return { oid : this.matchIds.matched(1), id : this.matchIds.matched(2)}; + } + ,loadApi: function(callback) { + var _gthis = this; + client_JsApi.addScriptToHead("https://vk.com/js/api/videoplayer.js",function() { + _gthis.isApiLoaded = true; + callback(); + }); + } + ,createVkPlayer: function(iframe) { + return VK.VideoPlayer(iframe); + } + ,getVideoData: function(data,callback) { + var _gthis = this; + if(!this.isApiLoaded) { + this.loadApi(function() { + _gthis.getVideoData(data,callback); + }); + return; + } + var url = data.url; + var video = window.document.createElement("div"); + video.id = "temp-videoplayer"; + var ids = this.getVideoIds(url); + if(ids == null) { + callback({ duration : 0}); + return; + } + video.innerHTML = StringTools.trim("\n\t\t\t\n\t\t"); + client_Utils.prepend(this.playerEl,video); + var tempVkPlayer = this.createVkPlayer(video.firstChild); + tempVkPlayer.on("inited",function() { + callback({ duration : tempVkPlayer.getDuration(), title : "VK media", url : url}); + tempVkPlayer.destroy(); + if(_gthis.playerEl.contains(video)) { + _gthis.playerEl.removeChild(video); + } + }); + } + ,loadVideo: function(item) { + var _gthis = this; + if(!this.isApiLoaded) { + this.loadApi(function() { + _gthis.loadVideo(item); + }); + return; + } + this.removeVideo(); + var tmp = this.getVideoIds(item.url); + if(tmp == null) { + return; + } + this.video = window.document.createElement("div"); + this.video.id = "videoplayer"; + this.video.innerHTML = StringTools.trim("\n\t\t\t\n\t\t"); + this.playerEl.appendChild(this.video); + this.vkPlayer = this.createVkPlayer(this.video.firstChild); + this.vkPlayer.on("inited",function() { + if(!_gthis.main.isAutoplayAllowed()) { + _gthis.vkPlayer.mute(); + } + _gthis.isLoaded = true; + _gthis.vkPlayer.pause(); + _gthis.setTime(0); + _gthis.player.onCanBePlayed(); + }); + this.vkPlayer.on("started",function() { + _gthis.player.onPlay(); + }); + this.vkPlayer.on("resumed",function() { + _gthis.player.onPlay(); + }); + this.vkPlayer.on("paused",function() { + _gthis.player.onPause(); + }); + this.vkPlayer.on("error",function(e) { + haxe_Log.trace("Error " + e,{ fileName : "src/client/players/Vk.hx", lineNumber : 166, className : "client.players.Vk", methodName : "loadVideo"}); + }); + var prevTime = 0.0; + this.vkPlayer.on("timeupdate",function(e) { + var diff = Math.abs(prevTime - e.time); + prevTime = e.time; + if(diff > 1) { + _gthis.player.onSetTime(); + } + }); + } + ,removeVideo: function() { + if(this.video == null) { + return; + } + this.isLoaded = false; + this.vkPlayer.destroy(); + this.vkPlayer = null; + if(this.playerEl.contains(this.video)) { + this.playerEl.removeChild(this.video); + } + this.video = null; + } + ,isVideoLoaded: function() { + return this.isLoaded; + } + ,play: function() { + this.vkPlayer.play(); + } + ,pause: function() { + this.vkPlayer.pause(); + } + ,isPaused: function() { + var state = this.vkPlayer.getState(); + if(state != "unstarted") { + return state == "paused"; + } else { + return true; + } + } + ,getTime: function() { + return this.vkPlayer.getCurrentTime(); + } + ,setTime: function(time) { + this.vkPlayer.seek(time); + } + ,getPlaybackRate: function() { + return 1; + } + ,setPlaybackRate: function(rate) { + } + ,getVolume: function() { + if(this.vkPlayer.isMuted()) { + return 0; + } + return this.vkPlayer.getVolume(); + } + ,setVolume: function(volume) { + this.vkPlayer.setVolume(volume); + } + ,unmute: function() { + this.vkPlayer.unmute(); + } +}; var client_players_Youtube = function(main,player) { this.matchSeconds = new EReg("([0-9]+)S",""); this.matchMinutes = new EReg("([0-9]+)M",""); diff --git a/src/Types.hx b/src/Types.hx index 0f56392..3d25fda 100644 --- a/src/Types.hx +++ b/src/Types.hx @@ -5,6 +5,7 @@ import Client.ClientData; enum abstract PlayerType(String) { var RawType; var YoutubeType; + var VkType; var IframeType; } diff --git a/src/client/Player.hx b/src/client/Player.hx index 70efcf9..f51d017 100644 --- a/src/client/Player.hx +++ b/src/client/Player.hx @@ -8,6 +8,7 @@ import client.Main.ge; import client.players.Iframe; import client.players.Raw; import client.players.Streamable; +import client.players.Vk; import client.players.Youtube; import haxe.Http; import haxe.Json; @@ -42,6 +43,7 @@ class Player { streamable = new Streamable(main, this); players = [ youtube, + new Vk(main, this), streamable, ]; iframePlayer = new Iframe(main, this); diff --git a/src/client/players/Vk.hx b/src/client/players/Vk.hx new file mode 100644 index 0000000..a2af220 --- /dev/null +++ b/src/client/players/Vk.hx @@ -0,0 +1,228 @@ +package client.players; + +import Types.PlayerType; +import Types.VideoData; +import Types.VideoDataRequest; +import Types.VideoItem; +import client.Main.ge; +import haxe.Constraints.Function; +import js.Browser.document; +import js.html.Element; +import js.html.Node; + +private enum abstract VkPlayerState(String) { + var Uninited = "uninited"; + var Unstarted = "unstarted"; + var Playing = "playing"; + var Paused = "paused"; + var Ended = "ended"; + var Error = "error"; +} + +private extern class VkPlayer { + function play():Void; + function pause():Void; + function seek(time:Float):Void; + function seekLive():Void; + function setVolume(volume:Float):Void; + function getVolume():Float; + function getCurrentTime():Float; + function getDuration():Int; + function getQuality():Int; // 480, etc + function mute():Void; + function unmute():Void; + function isMuted():Bool; + function getState():VkPlayerState; + function on(event:String, listener:Function):Void; + function off(event:String, listener:Function):Void; + function destroy():Void; +} + +class Vk implements IPlayer { + final main:Main; + final player:Player; + final playerEl:Element = ge("#ytapiplayer"); + var video:Element; + var vkPlayer:VkPlayer; + var isLoaded = false; + var isApiLoaded = false; + + public function new(main:Main, player:Player) { + this.main = main; + this.player = player; + } + + public function getPlayerType():PlayerType { + return VkType; + } + + final matchVk = ~/(vk.com\/video|vkvideo)/g; + final matchIds = ~/video(-?[0-9]+)_([0-9]+)/g; + + public function isSupportedLink(url:String):Bool { + return matchVk.match(url) && getVideoIds(url) != null; + } + + function getVideoIds(url:String):Null<{oid:String, id:String}> { + if (!matchIds.match(url)) { + trace("Cannot extract /video-oid_id values from url:"); + return null; + } + final oid = matchIds.matched(1); + final id = matchIds.matched(2); + return {oid: oid, id: id}; + } + + function loadApi(callback:() -> Void):Void { + final url = "https://vk.com/js/api/videoplayer.js"; + JsApi.addScriptToHead(url, () -> { + isApiLoaded = true; + callback(); + }); + } + + function createVkPlayer(iframe:Node):VkPlayer { + return untyped VK.VideoPlayer(iframe); + } + + public function getVideoData(data:VideoDataRequest, callback:(data:VideoData) -> Void):Void { + if (!isApiLoaded) { + loadApi(() -> { + getVideoData(data, callback); + }); + return; + } + final url = data.url; + + final video = document.createDivElement(); + video.id = "temp-videoplayer"; + final ids = getVideoIds(url); + if (ids == null) { + callback({duration: 0}); + return; + } + final oid = ids.oid; + final id = ids.id; + video.innerHTML = ' + + '.trim(); + Utils.prepend(playerEl, video); + final tempVkPlayer = createVkPlayer(video.firstChild); + tempVkPlayer.on("inited", () -> { + callback({ + duration: tempVkPlayer.getDuration(), + title: "VK media", + url: url + }); + tempVkPlayer.destroy(); + if (playerEl.contains(video)) playerEl.removeChild(video); + }); + } + + public function loadVideo(item:VideoItem):Void { + if (!isApiLoaded) { + loadApi(() -> { + loadVideo(item); + }); + return; + } + + removeVideo(); + + final ids = getVideoIds(item.url) ?? return; + video = document.createDivElement(); + video.id = "videoplayer"; + final oid = ids.oid; + final id = ids.id; + video.innerHTML = ' + + '.trim(); + playerEl.appendChild(video); + vkPlayer = createVkPlayer(video.firstChild); + vkPlayer.on("inited", () -> { + if (!main.isAutoplayAllowed()) vkPlayer.mute(); + isLoaded = true; + vkPlayer.pause(); + setTime(0); + player.onCanBePlayed(); + }); + + vkPlayer.on("started", () -> { + player.onPlay(); + }); + vkPlayer.on("resumed", () -> { + player.onPlay(); + }); + vkPlayer.on("paused", () -> { + player.onPause(); + }); + vkPlayer.on("error", e -> { + trace('Error $e'); + }); + var prevTime = 0.0; + vkPlayer.on("timeupdate", (e:{time:Float}) -> { + final diff = Math.abs(prevTime - e.time); + prevTime = e.time; + if (diff > 1) player.onSetTime(); + }); + } + + public function removeVideo():Void { + if (video == null) return; + isLoaded = false; + vkPlayer.destroy(); + vkPlayer = null; + if (playerEl.contains(video)) playerEl.removeChild(video); + video = null; + } + + public function isVideoLoaded():Bool { + return isLoaded; + } + + public function play():Void { + vkPlayer.play(); + } + + public function pause():Void { + vkPlayer.pause(); + } + + public function isPaused():Bool { + final state = vkPlayer.getState(); + return state == Unstarted || state == Paused; + } + + public function getTime():Float { + return vkPlayer.getCurrentTime(); + } + + public function setTime(time:Float):Void { + vkPlayer.seek(time); + } + + public function getPlaybackRate():Float { + return 1; + } + + public function setPlaybackRate(rate:Float):Void {} + + public function getVolume():Float { + if (vkPlayer.isMuted()) return 0; + return vkPlayer.getVolume(); + } + + public function setVolume(volume:Float):Void { + vkPlayer.setVolume(volume); + } + + public function unmute():Void { + vkPlayer.unmute(); + } +} -- cgit v1.2.3