From 6d92775ab546969230366b5bddbe51d36f73e7c7 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 2 May 2019 14:20:19 -0500 Subject: [PATCH] Add video previews --- assets/css/videojs-vtt-thumbnails.css | 7 ++ assets/js/videojs-vtt-thumbnails.min.js | 7 ++ src/invidious.cr | 89 ++++++++++++++++++- src/invidious/videos.cr | 7 +- src/invidious/views/components/player.ecr | 4 + .../views/components/player_sources.ecr | 2 + src/invidious/views/licenses.ecr | 14 +++ 7 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 assets/css/videojs-vtt-thumbnails.css create mode 100644 assets/js/videojs-vtt-thumbnails.min.js diff --git a/assets/css/videojs-vtt-thumbnails.css b/assets/css/videojs-vtt-thumbnails.css new file mode 100644 index 00000000..3d393958 --- /dev/null +++ b/assets/css/videojs-vtt-thumbnails.css @@ -0,0 +1,7 @@ +/** + * videojs-vtt-thumbnails + * @version 0.0.13 + * @copyright 2019 Chris Boustead + * @license MIT + */ +.video-js.vjs-vtt-thumbnails{display:block}.video-js .vjs-vtt-thumbnail-display{position:absolute;transition:transform .1s, opacity .2s;bottom:85%;pointer-events:none;box-shadow:0 0 7px rgba(0,0,0,0.6)} diff --git a/assets/js/videojs-vtt-thumbnails.min.js b/assets/js/videojs-vtt-thumbnails.min.js new file mode 100644 index 00000000..895458e8 --- /dev/null +++ b/assets/js/videojs-vtt-thumbnails.min.js @@ -0,0 +1,7 @@ +/** + * videojs-vtt-thumbnails + * @version 0.0.13 + * @copyright 2019 Chris Boustead + * @license MIT + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("video.js")):"function"==typeof define&&define.amd?define(["video.js"],e):t.videojsVttThumbnails=e(t.videojs)}(this,function(i){"use strict";i=i&&i.hasOwnProperty("default")?i.default:i;!function(){function l(t){this.value=t}function t(i){var o,n;function a(t,e){try{var r=i[t](e),s=r.value;s instanceof l?Promise.resolve(s.value).then(function(t){a("next",t)},function(t){a("throw",t)}):u(r.done?"return":"normal",r.value)}catch(t){u("throw",t)}}function u(t,e){switch(t){case"return":o.resolve({value:e,done:!0});break;case"throw":o.reject(e);break;default:o.resolve({value:e,done:!1})}(o=o.next)?a(o.key,o.arg):n=null}this._invoke=function(s,i){return new Promise(function(t,e){var r={key:s,arg:i,resolve:t,reject:e,next:null};n?n=n.next=r:(o=n=r,a(s,i))})},"function"!=typeof i.return&&(this.return=void 0)}"function"==typeof Symbol&&Symbol.asyncIterator&&(t.prototype[Symbol.asyncIterator]=function(){return this}),t.prototype.next=function(t){return this._invoke("next",t)},t.prototype.throw=function(t){return this._invoke("throw",t)},t.prototype.return=function(t){return this._invoke("return",t)}}();var o={},t=i.registerPlugin||i.plugin,e=function(r){var s=this;this.ready(function(){var t,e;t=s,e=i.mergeOptions(o,r),t.addClass("vjs-vtt-thumbnails"),t.vttThumbnails=new n(t,e)})},n=function(){function r(t,e){return function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,r),this.player=t,this.options=e,this.listenForDurationChange(),this.initializeThumbnails(),this.registeredEvents={},this}return r.prototype.src=function(t){this.resetPlugin(),this.options.src=t,this.initializeThumbnails()},r.prototype.detach=function(){this.resetPlugin()},r.prototype.resetPlugin=function(){this.thumbnailHolder&&this.thumbnailHolder.parentNode.removeChild(this.thumbnailHolder),this.progressBar&&this.progressBar.removeEventListener("mouseenter",this.registeredEvents.progressBarMouseEnter),this.progressBar&&this.progressBar.removeEventListener("mouseleave",this.registeredEvents.progressBarMouseLeave),this.progressBar&&this.progressBar.removeEventListener("mousemove",this.registeredEvents.progressBarMouseMove),delete this.registeredEvents.progressBarMouseEnter,delete this.registeredEvents.progressBarMouseLeave,delete this.registeredEvents.progressBarMouseMove,delete this.progressBar,delete this.vttData,delete this.thumbnailHolder,delete this.lastStyle},r.prototype.listenForDurationChange=function(){this.player.on("durationchange",function(){})},r.prototype.initializeThumbnails=function(){var e=this;if(this.options.src){var t=this.getBaseUrl(),r=this.getFullyQualifiedUrl(this.options.src,t);this.getVttFile(r).then(function(t){e.vttData=e.processVtt(t),e.setupThumbnailElement()})}},r.prototype.getBaseUrl=function(){return[window.location.protocol,"//",window.location.hostname,window.location.port?":"+window.location.port:"",window.location.pathname].join("").split(/([^\/]*)$/gi).shift()},r.prototype.getVttFile=function(s){var i=this;return new Promise(function(t,e){var r=new XMLHttpRequest;r.data={resolve:t},r.addEventListener("load",i.vttFileLoaded),r.open("GET",s),r.send()})},r.prototype.vttFileLoaded=function(){this.data.resolve(this.responseText)},r.prototype.setupThumbnailElement=function(t){var e=this,r=this.player.$(".vjs-mouse-display");this.progressBar=this.player.$(".vjs-progress-control");var s=document.createElement("div");s.setAttribute("class","vjs-vtt-thumbnail-display"),this.progressBar.appendChild(s),this.thumbnailHolder=s,r&&r.classList.add("vjs-hidden"),this.registeredEvents.progressBarMouseEnter=function(){return e.onBarMouseenter()},this.registeredEvents.progressBarMouseLeave=function(){return e.onBarMouseleave()},this.progressBar.addEventListener("mouseenter",this.registeredEvents.progressBarMouseEnter),this.progressBar.addEventListener("mouseleave",this.registeredEvents.progressBarMouseLeave)},r.prototype.onBarMouseenter=function(){var e=this;this.mouseMoveCallback=function(t){e.onBarMousemove(t)},this.registeredEvents.progressBarMouseMove=this.mouseMoveCallback,this.progressBar.addEventListener("mousemove",this.registeredEvents.progressBarMouseMove),this.showThumbnailHolder()},r.prototype.onBarMouseleave=function(){this.registeredEvents.progressBarMouseMove&&this.progressBar.removeEventListener("mousemove",this.registeredEvents.progressBarMouseMove),this.hideThumbnailHolder()},r.prototype.getXCoord=function(t,e){var r=t.getBoundingClientRect(),s=document.documentElement;return e-(r.left+(window.pageXOffset||s.scrollLeft||0))},r.prototype.onBarMousemove=function(t){this.updateThumbnailStyle(this.getXCoord(this.progressBar,t.clientX),this.progressBar.offsetWidth)},r.prototype.getStyleForTime=function(t){for(var e=0;e=r.start&&t ?)([0-9]{2}:)?([0-9]{2}:)?[0-9]{2}(.[0-9]{3})?[\r\n]{1}.*/gi)){var e=t.split(/[\r\n]/i),r=e[0].split(/ ?--> ?/i),s=r[0],i=r[1],o=e[1],n=a.getVttCss(o);u.push({start:a.getSecondsFromTimestamp(s),end:a.getSecondsFromTimestamp(i),css:n})}}),u},r.prototype.getFullyQualifiedUrl=function(t,e){return 0<=t.indexOf("//")?t:0===e.indexOf("//")?[e.replace(/\/$/gi,""),this.trim(t,"/")].join("/"):0 #{end_time}.000 + #{url}#xywh=#{storyboard[:width] * k},#{storyboard[:height] * j},#{storyboard[:width]},#{storyboard[:height]} + + + END_CUE + + start_time += storyboard[:interval].milliseconds + end_time += storyboard[:interval].milliseconds + end + end + end + + webvtt +end + get "/api/v1/captions/:id" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? @@ -3145,7 +3225,7 @@ get "/api/v1/captions/:id" do |env| text = "#{md["text"]}" end - webvtt = webvtt + <<-END_CUE + webvtt += <<-END_CUE #{start_time} --> #{end_time} #{text} @@ -5054,6 +5134,13 @@ get "/ggpht/*" do |env| end end +options "/sb/:id/:storyboard/:index" do |env| + env.response.headers.delete("Content-Type") + env.response.headers["Access-Control-Allow-Origin"] = "*" + env.response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS" + env.response.headers["Access-Control-Allow-Headers"] = "Content-Type, Range" +end + get "/sb/:id/:storyboard/:index" do |env| id = env.params.url["id"] storyboard = env.params.url["storyboard"] diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 755ee4d7..9a199fad 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -281,7 +281,7 @@ struct Video generate_thumbnails(json, self.id, config, kemal_config) end json.field "storyboards" do - generate_storyboards(json, self.storyboards, config, kemal_config) + generate_storyboards(json, self.id, self.storyboards, config, kemal_config) end description_html, description = html_to_content(self.description) @@ -1348,11 +1348,12 @@ def generate_thumbnails(json, id, config, kemal_config) end end -def generate_storyboards(json, storyboards, config, kemal_config) +def generate_storyboards(json, id, storyboards, config, kemal_config) json.array do storyboards.each do |storyboard| json.object do - json.field "url", storyboard[:url] + json.field "url", "/api/v1/storyboards/#{id}?width=#{storyboard[:width]}&height=#{storyboard[:height]}" + json.field "templateUrl", storyboard[:url] json.field "width", storyboard[:width] json.field "height", storyboard[:height] json.field "count", storyboard[:count] diff --git a/src/invidious/views/components/player.ecr b/src/invidious/views/components/player.ecr index 6ad70033..3afa2af8 100644 --- a/src/invidious/views/components/player.ecr +++ b/src/invidious/views/components/player.ecr @@ -217,6 +217,10 @@ if (bpb) { player.httpSourceSelector(); <% end %> +player.vttThumbnails({ + src: 'api/v1/storyboards/<%= video.id %>?height=90' +}); + <% if !params.listen && params.annotations %> var video_container = document.getElementById('player'); let xhr = new XMLHttpRequest(); diff --git a/src/invidious/views/components/player_sources.ecr b/src/invidious/views/components/player_sources.ecr index f446248e..2fbd41b0 100644 --- a/src/invidious/views/components/player_sources.ecr +++ b/src/invidious/views/components/player_sources.ecr @@ -2,12 +2,14 @@ + + <% if params.annotations %> diff --git a/src/invidious/views/licenses.ecr b/src/invidious/views/licenses.ecr index bab7762a..8561dee9 100644 --- a/src/invidious/views/licenses.ecr +++ b/src/invidious/views/licenses.ecr @@ -93,6 +93,20 @@ + + + videojs-vtt-thumbnails.min.js + + + + Expat + + + + <%= translate(locale, "source") %> + + + videojs-youtube-annotations.min.js