diff options
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | res/client.js | 143 | ||||
| -rw-r--r-- | src/client/Main.hx | 4 | ||||
| -rw-r--r-- | src/client/Player.hx | 6 | ||||
| -rw-r--r-- | src/client/players/Peertube.hx | 136 |
5 files changed, 281 insertions, 9 deletions
@@ -25,6 +25,7 @@ Default channel example: https://synctube.onrender.com/ - Youtube (videos, shorts, streams and playlists)
- [Streamable](https://streamable.com)
- [VK](https://vk.com/video)
+- [Peertube](https://joinpeertube.org) (with `pt:` url prefix)
- Raw mp4 videos and m3u8 playlists (or any other media format supported in browser)
- Iframes (without sync)
diff --git a/res/client.js b/res/client.js index 7946acf..fa4631f 100644 --- a/res/client.js +++ b/res/client.js @@ -1598,8 +1598,10 @@ client_Main.prototype = { var port = $global.location.port; url = "" + protocol + "//" + host + (port.length > 0 ? ":" + port : port) + url; } - if(!StringTools.startsWith(url,"http")) { - url = "" + protocol + "//" + url; + if(!StringTools.startsWith(url,"pt:")) { + if(!StringTools.startsWith(url,"http")) { + url = "" + protocol + "//" + url; + } } this.player.getVideoData({ url : url, atEnd : atEnd},function(data) { if(data.duration == 0) { @@ -1677,7 +1679,7 @@ client_Main.prototype = { var data = JSON.parse(e.data); if(this.config != null && this.config.isVerbose) { var t = data.type; - haxe_Log.trace("Event: " + data.type,{ fileName : "src/client/Main.hx", lineNumber : 457, className : "client.Main", methodName : "onMessage", customParams : [Reflect.field(data,t.charAt(0).toLowerCase() + HxOverrides.substr(t,1,null))]}); + haxe_Log.trace("Event: " + data.type,{ fileName : "src/client/Main.hx", lineNumber : 459, className : "client.Main", methodName : "onMessage", customParams : [Reflect.field(data,t.charAt(0).toLowerCase() + HxOverrides.substr(t,1,null))]}); } client_JsApi.fireEvents(data); switch(data.type) { @@ -2700,8 +2702,7 @@ var client_Player = function(main) { var _gthis = this; this.main = main; this.youtube = new client_players_Youtube(main,this); - this.streamable = new client_players_Streamable(main,this); - this.players = [this.youtube,new client_players_Vk(main,this),this.streamable]; + this.players = [this.youtube,new client_players_Vk(main,this),new client_players_Streamable(main,this),new client_players_Peertube(main,this)]; this.iframePlayer = new client_players_Iframe(main,this); this.rawPlayer = new client_players_Raw(main,this); this.initItemButtons(); @@ -4121,6 +4122,138 @@ client_players_Raw.prototype = { this.video.muted = false; } }; +var client_players_Peertube = function(main,player) { + this.matchOldPeertube = new EReg("^pt:.+/videos/watch/([-A-z0-9]+)","g"); + this.matchPeertube = new EReg("^pt:.+/w/(p/)?([A-z0-9]+)","g"); + client_players_Raw.call(this,main,player); +}; +client_players_Peertube.__name__ = true; +client_players_Peertube.__super__ = client_players_Raw; +client_players_Peertube.prototype = $extend(client_players_Raw.prototype,{ + isSupportedLink: function(url) { + return this.extractVideoId(url).length > 0; + } + ,extractVideoId: function(url) { + if(this.matchPeertube.match(url)) { + return this.matchPeertube.matched(2); + } + if(this.matchOldPeertube.match(url)) { + return this.matchOldPeertube.matched(1); + } + return ""; + } + ,getVideoData: function(data,callback) { + var _gthis = this; + if(!this.isSupportedLink(data.url)) { + this.getRawVideoData(data,callback); + return; + } + this.getPeertubeVideoData(data.url,function(info) { + if(info == null) { + callback({ duration : 0}); + return; + } + _gthis.getRawVideoData({ url : info.url, atEnd : data.atEnd},function(data) { + data.url = info.url; + data.title = info.title; + callback(data); + }); + }); + } + ,getRawVideoData: function(data,callback) { + client_players_Raw.prototype.getVideoData.call(this,data,callback); + } + ,getPeertubeVideoData: function(url,callback) { + var _gthis = this; + var id = this.extractVideoId(url); + url = StringTools.replace(url,"pt:",""); + if(!StringTools.startsWith(url,"http")) { + url = "https://" + url; + } + var urlObj; + try { + urlObj = new URL(url); + } catch( _g ) { + haxe_Log.trace(haxe_Exception.caught(_g),{ fileName : "src/client/players/Peertube.hx", lineNumber : 53, className : "client.players.Peertube", methodName : "getPeertubeVideoData"}); + callback(null); + return; + } + var host = urlObj.host; + if(url.indexOf("/p/") != -1) { + var http = new haxe_http_HttpJs("https://" + host + "/api/v1/video-playlists/" + id + "/videos"); + http.onData = function(data) { + var arr = JSON.parse(data).data; + var tmp = urlObj.searchParams.get("playlistPosition"); + var pos = Std.parseInt(tmp != null ? tmp : "1"); + var tmp = Lambda.find(arr,function(item) { + return item.position == pos; + }); + var item = tmp != null ? tmp : arr[0]; + var uuid; + var tmp = item.video; + var tmp1 = tmp != null ? tmp.uuid : null; + if(tmp1 != null) { + uuid = tmp1; + } else { + haxe_Log.trace(item,{ fileName : "src/client/players/Peertube.hx", lineNumber : 69, className : "client.players.Peertube", methodName : "getPeertubeVideoData"}); + callback(null); + return; + } + _gthis.getPeertubeVideoData("pt:https://" + host + "/videos/watch/" + uuid,callback); + }; + http.onError = function(err) { + haxe_Log.trace(err,{ fileName : "src/client/players/Peertube.hx", lineNumber : 76, className : "client.players.Peertube", methodName : "getPeertubeVideoData"}); + callback(null); + }; + http.request(); + return; + } + var http = new haxe_http_HttpJs("https://" + host + "/api/v1/videos/" + id); + http.onData = function(data) { + try { + var json = JSON.parse(data); + var tmp = json.name; + var title = tmp != null ? tmp : "PeerTube video"; + var playlistUrl = _gthis.getBestHlsPlaylistUrl(json); + if(playlistUrl != null) { + callback({ url : playlistUrl, title : title}); + return; + } + var mp4Url = _gthis.getBestMp4Url(json); + if(mp4Url != null) { + callback({ url : mp4Url, title : title}); + return; + } + callback(null); + } catch( _g ) { + haxe_Log.trace(haxe_Exception.caught(_g),{ fileName : "src/client/players/Peertube.hx", lineNumber : 109, className : "client.players.Peertube", methodName : "getPeertubeVideoData"}); + callback(null); + } + }; + http.onError = function(err) { + haxe_Log.trace(err,{ fileName : "src/client/players/Peertube.hx", lineNumber : 115, className : "client.players.Peertube", methodName : "getPeertubeVideoData"}); + callback(null); + }; + http.request(); + } + ,getBestHlsPlaylistUrl: function(json) { + var playlists = json.streamingPlaylists; + if(playlists == null || playlists.length == 0) { + return null; + } + return playlists[0].playlistUrl; + } + ,getBestMp4Url: function(json) { + var files = json.files; + if(files == null || files.length == 0) { + return null; + } + files.sort(function(a,b) { + return b.resolution.id - a.resolution.id; + }); + return files[0].fileDownloadUrl; + } +}); var client_players_RawSubs = function() { }; client_players_RawSubs.__name__ = true; client_players_RawSubs.loadSubs = function(subsUrl,video) { diff --git a/src/client/Main.hx b/src/client/Main.hx index 660693b..42030dd 100644 --- a/src/client/Main.hx +++ b/src/client/Main.hx @@ -330,7 +330,9 @@ class Main { final colonPort = port.length > 0 ? ':$port' : port; url = '$protocol//$host$colonPort$url'; } - if (!url.startsWith("http")) url = '$protocol//$url'; + if (!url.startsWith("pt:")) { + if (!url.startsWith("http")) url = '$protocol//$url'; + } final obj:VideoDataRequest = { url: url, diff --git a/src/client/Player.hx b/src/client/Player.hx index 6be8a07..9b22748 100644 --- a/src/client/Player.hx +++ b/src/client/Player.hx @@ -6,6 +6,7 @@ import Types.VideoDataRequest; import Types.VideoItem; import client.Main.getEl; import client.players.Iframe; +import client.players.Peertube; import client.players.Raw; import client.players.Streamable; import client.players.Vk; @@ -30,7 +31,6 @@ class Player { var isLoaded = false; var skipSetTime = false; var skipSetRate = false; - var streamable:Streamable; final voiceOverInput:InputElement = getEl("#voiceoverurl"); var audioTrack:Null<Audio>; @@ -44,11 +44,11 @@ class Player { public function new(main:Main):Void { this.main = main; youtube = new Youtube(main, this); - streamable = new Streamable(main, this); players = [ youtube, new Vk(main, this), - streamable, + new Streamable(main, this), + new Peertube(main, this), ]; iframePlayer = new Iframe(main, this); rawPlayer = new Raw(main, this); diff --git a/src/client/players/Peertube.hx b/src/client/players/Peertube.hx new file mode 100644 index 0000000..e869980 --- /dev/null +++ b/src/client/players/Peertube.hx @@ -0,0 +1,136 @@ +package client.players; + +import Types.VideoData; +import Types.VideoDataRequest; +import haxe.Http; +import haxe.Json; +import js.html.URL; + +class Peertube extends Raw { + final matchPeertube = ~/^pt:.+\/w\/(p\/)?([A-z0-9]+)/g; + final matchOldPeertube = ~/^pt:.+\/videos\/watch\/([-A-z0-9]+)/g; + + override function isSupportedLink(url:String):Bool { + return extractVideoId(url).length > 0; + } + + public function extractVideoId(url:String):String { + if (matchPeertube.match(url)) return matchPeertube.matched(2); + if (matchOldPeertube.match(url)) return matchOldPeertube.matched(1); + return ""; + } + + override function getVideoData(data:VideoDataRequest, callback:(data:VideoData) -> Void) { + if (!isSupportedLink(data.url)) { + getRawVideoData(data, callback); + return; + } + getPeertubeVideoData(data.url, info -> { + if (info == null) { + callback({duration: 0}); + return; + } + getRawVideoData({url: info.url, atEnd: data.atEnd}, data -> { + data.url = info.url; + data.title = info.title; + callback(data); + }); + }); + } + + function getRawVideoData(data:VideoDataRequest, callback:(data:VideoData) -> Void) { + super.getVideoData(data, callback); + } + + function getPeertubeVideoData(url:String, callback:(info:Null<{url:String, title:String}>) -> Void) { + final id = extractVideoId(url); + + url = url.replace("pt:", ""); + if (!url.startsWith("http")) url = 'https://$url'; + final urlObj = try { + new URL(url); + } catch (e) { + trace(e); + callback(null); + return; + } + final host = urlObj.host; + + final isPlaylistItem = url.contains("/p/"); + if (isPlaylistItem) { + final apiUrl = 'https://$host/api/v1/video-playlists/$id/videos'; + final http = new Http(apiUrl); + http.onData = data -> { + final json:{data:Array<Dynamic>, total:Int} = Json.parse(data); + final arr = json.data; + final pos = Std.parseInt(urlObj.searchParams.get("playlistPosition") ?? "1"); + final item:Dynamic = arr.find(item -> item.position == pos) ?? arr[0]; + final uuid:String = item.video?.uuid ?? { + trace(item); + callback(null); + return; + }; + getPeertubeVideoData('pt:https://$host/videos/watch/$uuid', callback); + } + http.onError = err -> { + trace(err); + callback(null); + } + http.request(); + return; + } + + final apiUrl = 'https://$host/api/v1/videos/$id'; + final http = new Http(apiUrl); + http.onData = data -> { + try { + final json = Json.parse(data); + final title = json.name ?? "PeerTube video"; + final playlistUrl = getBestHlsPlaylistUrl(json); + if (playlistUrl != null) { + callback({ + url: playlistUrl, + title: title + }); + return; + } + + final mp4Url = getBestMp4Url(json); + if (mp4Url != null) { + callback({ + url: mp4Url, + title: title + }); + return; + } + + callback(null); + } catch (e) { + trace(e); + callback(null); + } + } + + http.onError = err -> { + trace(err); + callback(null); + } + + http.request(); + } + + function getBestHlsPlaylistUrl(json:Dynamic):Null<String> { + final playlists:Array<Dynamic> = json.streamingPlaylists; + if (playlists == null || playlists.length == 0) return null; + return playlists[0].playlistUrl; + } + + function getBestMp4Url(json:Dynamic):Null<String> { + final files:Array<Dynamic> = json.files; + if (files == null || files.length == 0) return null; + files.sort(function(a, b) { + return b.resolution.id - a.resolution.id; + }); + return files[0].fileDownloadUrl; + } +} |
