diff --git a/bin/build-player.js b/bin/build-player.js
index db4112de..83bfcf14 100755
--- a/bin/build-player.js
+++ b/bin/build-player.js
@@ -22,6 +22,7 @@ var order = [
'rtmp.coffee',
'hls.coffee',
'twitchclip.coffee',
+ 'peertube.coffee',
'update.coffee'
];
diff --git a/player/peertube.coffee b/player/peertube.coffee
new file mode 100644
index 00000000..3fab9c76
--- /dev/null
+++ b/player/peertube.coffee
@@ -0,0 +1,113 @@
+PEERTUBE_EMBED_WARNING = 'This channel is embedding PeerTube content from %link%.
+ PeerTube instances may use P2P technology that will expose your IP address to third parties, including but not
+ limited to other users in this channel. It is also conceivable that if the content in question is in violation of
+ copyright laws your IP address could be potentially be observed by legal authorities monitoring the tracker of
+ this PeerTube instance. The operators of %site% are not responsible for the data sent by the embedded player to
+ third parties on your behalf. If you understand the risks, wish to assume all liability, and continue to
+ the content, click "Embed" below to allow the content to be embedded.
'
+
+window.PeerPlayer = class PeerPlayer extends Player
+ constructor: (data) ->
+ if not (this instanceof PeerPlayer)
+ return new PeerPlayer(data)
+
+ @warn(data)
+
+ warn: (data) ->
+ site = new URL(document.URL).hostname
+ embedSrc = data.meta.embed.domain
+ link = "#{embedSrc} "
+ alert = makeAlert('Privacy Advisory', PEERTUBE_EMBED_WARNING.replace('%link%', link).replace('%site%', site),
+ 'alert-warning')
+ .removeClass('col-md-12')
+ $(' ').addClass('btn btn-default')
+ .text('Embed')
+ .on('click', =>
+ @load(data)
+ )
+ .appendTo(alert.find('.alert'))
+ removeOld(alert)
+
+ load: (data) ->
+ @setMediaProperties(data)
+
+ waitUntilDefined(window, 'PeerTubePlayer', =>
+ video = $('')
+ removeOld(video)
+ video.attr(
+ src: "https://#{data.meta.embed.domain}/videos/embed/#{data.meta.embed.uuid}?api=1"
+ allow: 'autoplay; fullscreen'
+ )
+
+ if USEROPTS.wmode_transparent
+ video.attr('wmode', 'transparent')
+
+ @peertube = new PeerTubePlayer(video[0])
+
+ @peertube.addEventListener('playbackStatusChange', (status) =>
+ @paused = status == 'paused'
+ if CLIENT.leader
+ sendVideoUpdate()
+ )
+
+ @peertube.addEventListener('playbackStatusUpdate', (status) =>
+ @peertube.currentTime = status.position
+
+ if status.playbackState == "ended" and CLIENT.leader
+ socket.emit('playNext')
+ )
+
+ @peertube.addEventListener('volumeChange', (volume) =>
+ VOLUME = volume
+ setOpt("volume", VOLUME)
+ )
+
+ @play()
+ @setVolume(VOLUME)
+ )
+
+ play: ->
+ @paused = false
+ if @peertube and @peertube.ready
+ @peertube.play().catch((error) ->
+ console.error('PeerTube::play():', error)
+ )
+
+ pause: ->
+ @paused = true
+ if @peertube and @peertube.ready
+ @peertube.pause().catch((error) ->
+ console.error('PeerTube::pause():', error)
+ )
+
+ seekTo: (time) ->
+ if @peertube and @peertube.ready
+ @peertube.seek(time)
+
+ getVolume: (cb) ->
+ if @peertube and @peertube.ready
+ @peertube.getVolume().then((volume) ->
+ cb(parseFloat(volume))
+ ).catch((error) ->
+ console.error('PeerTube::getVolume():', error)
+ )
+ else
+ cb(VOLUME)
+
+ setVolume: (volume) ->
+ if @peertube and @peertube.ready
+ @peertube.setVolume(volume).catch((error) ->
+ console.error('PeerTube::setVolume():', error)
+ )
+
+ getTime: (cb) ->
+ if @peertube and @peertube.ready
+ cb(@peertube.currentTime)
+ else
+ cb(0)
+
+ setQuality: (quality) ->
+ # USEROPTS.default_quality
+ # @peertube.getResolutions()
+ # @peertube.setResolution(resolutionId : number)
+
diff --git a/player/update.coffee b/player/update.coffee
index 8faefd2f..62c15753 100644
--- a/player/update.coffee
+++ b/player/update.coffee
@@ -14,6 +14,7 @@ TYPE_MAP =
sb: StreamablePlayer
tc: TwitchClipPlayer
cm: VideoJSPlayer
+ pt: PeerPlayer
window.loadMediaPlayer = (data) ->
try
diff --git a/src/get-info.js b/src/get-info.js
index acdaa0df..1a73f694 100644
--- a/src/get-info.js
+++ b/src/get-info.js
@@ -6,6 +6,7 @@ const ffmpeg = require("./ffmpeg");
const mediaquery = require("@cytube/mediaquery");
const YouTube = require("@cytube/mediaquery/lib/provider/youtube");
const Vimeo = require("@cytube/mediaquery/lib/provider/vimeo");
+const PeerTube = require("@cytube/mediaquery/lib/provider/peertube");
const Streamable = require("@cytube/mediaquery/lib/provider/streamable");
const TwitchVOD = require("@cytube/mediaquery/lib/provider/twitch-vod");
const TwitchClip = require("@cytube/mediaquery/lib/provider/twitch-clip");
@@ -366,6 +367,16 @@ var Getters = {
});
},
+ /* PeerTube network */
+ pt: function (id, callback) {
+ PeerTube.lookup(id).then(video => {
+ video = new Media(video.id, video.title, video.duration, "pt", video.meta);
+ callback(null, video);
+ }).catch(error => {
+ callback(error.message || error);
+ });
+ },
+
/* custom media - https://github.com/calzoneman/sync/issues/655 */
cm: async function (id, callback) {
try {
diff --git a/src/utilities.js b/src/utilities.js
index 66bb251d..5ffc4bea 100644
--- a/src/utilities.js
+++ b/src/utilities.js
@@ -205,6 +205,9 @@
return "https://clips.twitch.tv/" + id;
case "cm":
return id;
+ case "pt":
+ const [domain,uuid] = id.split(';');
+ return `https://${domain}/videos/watch/${uuid}`;
default:
return "";
}
diff --git a/templates/channel.pug b/templates/channel.pug
index e6e641d5..593fc432 100644
--- a/templates/channel.pug
+++ b/templates/channel.pug
@@ -252,6 +252,7 @@ html(lang="en")
script(defer, src="https://www.youtube.com/iframe_api")
script(defer, src="https://api.dmcdn.net/all.js")
script(defer, src="https://player.vimeo.com/api/player.js")
+ script(defer, src="/js/peertube.js")
script(defer, src="/js/sc.js")
script(defer, src="/js/video.js")
script(defer, src="/js/videojs-contrib-hls.min.js")
diff --git a/www/js/peertube.js b/www/js/peertube.js
new file mode 100644
index 00000000..804f06fc
--- /dev/null
+++ b/www/js/peertube.js
@@ -0,0 +1 @@
+(()=>{var e={991:function(e){e.exports=function(){"use strict";return function(){var e=Math.floor(1000001*Math.random()),t={};function n(e){return Array.isArray?Array.isArray(e):-1!=e.constructor.toString().indexOf("Array")}var r={},i=function(e){try{var n=JSON.parse(e.data);if("object"!=typeof n||null===n)throw"malformed"}catch(e){return}var i,o,s,a=e.source,u=e.origin;if("string"==typeof n.method){var c=n.method.split("::");2==c.length?(i=c[0],s=c[1]):s=n.method}if(void 0!==n.id&&(o=n.id),"string"==typeof s){var l=!1;if(t[u]&&t[u][i])for(var d=0;d1)throw"scope may not contain double colons: '::'"}else i.scope="__default";var u=function(){for(var e="",t="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",n=0;n<5;n++)e+=t.charAt(Math.floor(Math.random()*t.length));return e}(),c={},l={},d={},f=!1,h=[],p=[],g=function(e,t,s){if("function"==typeof i.gotMessageObserver)try{i.gotMessageObserver(e,s)}catch(e){o("gotMessageObserver() raised an exception: "+e.toString())}if(s.id&&t){d[s.id]={};var a=function(e,t,n){var r=!1,i=!1;return{origin:t,invoke:function(t,r){if(!d[e])throw"attempting to invoke a callback of a nonexistent transaction: "+e;for(var i=!1,o=0;o0)for(var u=0;u=0)throw"params cannot be a recursive data structure";if(t&&o.push(t),"object"==typeof t)for(var r in t)if(t.hasOwnProperty(r)){var a=e+(e.length?"/":"")+r;"function"==typeof t[r]?(n[a]=t[r],i.push(a),delete t[r]):"object"==typeof t[r]&&s(a,t[r])}};s("",t.params);var a,u,c,d={id:e,method:y(t.method),params:t.params};i.length&&(d.callbacks=i),t.timeout&&(a=e,u=t.timeout,c=y(t.method),window.setTimeout((function(){if(l[a]){var e="timeout ("+u+"ms) exceeded on method '"+c+"'";l[a].error&&l[a].error("timeout_error",e),delete l[a],delete r[a]}}),u)),l[e]={callbacks:n,error:t.error,success:t.success},r[e]=g,e++,v(d)},notify:function(e){if(!e)throw"missing arguments to notify function";if(!e.method||"string"!=typeof e.method)throw"'method' argument to notify must be string";v({method:y(e.method),params:e.params})},destroy:function(){(function(e,n,r){for(var i=t[n][r],o=0;o0&&v({method:y("__ready"),params:{type:"publish-request",publish:p}},!0)}),0),b}}}()}()},625:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.EventRegistrar=void 0;var n=function(){function e(){this.eventRegistrations={}}return e.prototype.bindToChannel=function(e){for(var t=this,n=function(n){e.bind(n,(function(e,r){return t.fire(n,r)}))},r=0,i=Object.keys(this.eventRegistrations);r0&&i[i.length-1])||6!==o[0]&&2!==o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1] `[0-9a-f]{${x}}`).join('-');
+ const regShort = '[a-zA-Z0-9]{22}';
+ const pattern = new RegExp(`(?:/w/|/videos/watch/)(?:(?${regShort})|(?${regLong}))`);
+ if((m = data.pathname.match(pattern))) {
+ return {
+ id: `${data.hostname};${m.groups.short || m.groups.long}`,
+ type: "pt"
+ };
+ }
+ }
+
/* Raw file (server will check) */
if (data.protocol.match(/^http/)) {
return {