Youtube Storyboard

mouse-over video thumbnail to preview video by cycling through the storyboard

  1. // ==UserScript==
  2. // @name Youtube Storyboard
  3. // @namespace hbb_works
  4. // @description mouse-over video thumbnail to preview video by cycling through the storyboard
  5. // @version 1.3.1r1605101704
  6. // @include http://*youtube.com*
  7. // @include https://*youtube.com*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11.  
  12. const INTERVAL = 200; // default interval in milliseconds
  13. var div;
  14. var loader;
  15. var animator;
  16. var mousePos = { x: 0, y: 0 };
  17.  
  18. function dbg() {
  19. //console.log.apply(console, arguments);
  20. }
  21.  
  22. function getPos(element) {
  23. var ref = document.body.getBoundingClientRect();
  24. var rel = element.getBoundingClientRect();
  25. return {
  26. left: rel.left - ref.left,
  27. top: rel.top - ref.top
  28. };
  29. }
  30.  
  31. function setMousePos(event) {
  32. mousePos = { x: event.pageX, y: event.pageY };
  33. }
  34.  
  35. (function init() {
  36. dbg("init youtube storyboard");
  37. document.addEventListener('mouseover', onMouseOver, false);
  38. loader = document.createElement('img');
  39. div = document.createElement('div');
  40. div.id = 'storyboardPlayer';
  41. div.style.display = 'none';
  42. div.style.position = 'absolute';
  43. div.style.zIndex = 99;
  44. var a = document.createElement('a');
  45. var innerDiv = document.createElement('div');
  46. innerDiv.style.height = '100%';
  47. innerDiv.style.width = '100%';
  48. a.appendChild(innerDiv);
  49. div.appendChild(a);
  50. document.getElementById('page') .appendChild(div);
  51. div.addEventListener('mouseout', function () {
  52. animator.stop();
  53. });
  54. document.addEventListener('mousemove', function (event) {
  55. setMousePos(event);
  56. });
  57. })();
  58.  
  59. function onMouseOver(event) {
  60. setMousePos(event);
  61. var target = event.target;
  62. var src = target.src;
  63. if (target.nodeName == 'IMG' && src && (/default\.jpg/.exec(src) || /0\.jpg/.exec(src)))
  64. {
  65. if (animator) {
  66. animator.stop();
  67. }
  68. animator = new Animator(target);
  69. animator.getStoryboardSpecs(function () {
  70. animator.start();
  71. });
  72. }
  73. }
  74.  
  75. var Animator = function (target) {
  76. this.id = Math.floor(Math.random() * 10000);
  77. dbg("new " + this.id);
  78. this.target = target;
  79. this.frame = 0;
  80. this.sheet = 0;
  81. this.interval = INTERVAL;
  82. this.src = target.src
  83. // get video id
  84. this.v = (/vi\/(.*?)\//.exec(this.src) || []) [1];
  85. if (this.v) {
  86. this.width = target.width;
  87. this.height = target.height;
  88. }
  89. var p = getPos(target);
  90. div.style.top = p.top + 'px';
  91. div.style.left = p.left + 'px';
  92. div.style.width = this.width + 'px';
  93. div.style.height = this.height + 'px';
  94. this.x = p.left;
  95. this.y = p.top;
  96. // adjustments for small thumbnails in list of related videos
  97. if (target.parentElement.classList.contains('yt-uix-simple-thumb-wrap')) {
  98. var spanHeight = target.parentElement.getBoundingClientRect().height;
  99. div.style.top = p.top + ((target.height - spanHeight) / 2) + 'px';
  100. div.style.height = spanHeight + 'px';
  101. this.height = spanHeight;
  102. }
  103. var parent = target.parentElement;
  104. while (parent.tagName != 'A') {
  105. parent = parent.parentElement;
  106. }
  107. div.children[0].href = parent.href;
  108. div.style.backgroundRepeat = 'no-repeat';
  109. div.style.backgroundPosition = 'center';
  110. div.style.backgroundSize = null;
  111. // loading indicator
  112. div.style.backgroundImage ='url(\'data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==\')';
  113. div.style.display = null;
  114. };
  115. Animator.prototype.parseSpec = function (storyboard_spec) {
  116. dbg("parseSpec " + this.id);
  117. var lines = storyboard_spec.split('|');
  118. var q;
  119. if (!this.quality) {
  120. this.quality = - 1;
  121. do {
  122. this.quality++;
  123. q = lines[this.quality + 1].split('#');
  124. } while (parseInt(q[0], 10) < this.width && this.quality + 2 < lines.length);
  125. }
  126. var q = lines[this.quality + 1].split('#');
  127. var s = {
  128. url: lines[0].replace('$L', this.quality) .replace('$N', q[6]),
  129. width: parseInt(q[0], 10),
  130. height: parseInt(q[1], 10),
  131. count: parseInt(q[2], 10),
  132. cols: parseInt(q[3], 10),
  133. rows: parseInt(q[4], 10),
  134. sigh: q[7]
  135. };
  136. s.sheetSize = s.cols * s.rows;
  137. s.sheetCount = ((s.count / s.sheetSize) | 0) + 1; // bitwise OR to loose decimals
  138. s.countOnLastSheet = ((s.count - 1) % s.sheetSize) + 1;
  139. return this.spec = s;
  140. };
  141. Animator.prototype.loadImage = function (callback) {
  142. dbg("loadImage " + this.id);
  143. var onLoad = (function () {
  144. div.style.backgroundImage = 'url(' + loader.src + ')';
  145. loader.removeEventListener('load', onLoad);
  146. callback.apply(this);
  147. }).bind(this);
  148. loader.addEventListener('load', onLoad);
  149. loader.src = this.spec.url.replace('$M', this.sheet) + '?sigh=' + this.spec.sigh;
  150. };
  151. Animator.prototype.getStoryboardSpecs = function (callback, fromWatch) {
  152. dbg("getStoryboardSpecs (fromWatch: " + fromWatch + ") " + this.id);
  153. this.xhr = new XMLHttpRequest();
  154. this.xhr.onload = (function () {
  155. if (this.isMouseOver()) {
  156. if (fromWatch) {
  157. var spec = (/"storyboard_spec":\s*(".*?")/.exec(this.xhr.responseText) || []) [1];
  158. }
  159. else {
  160. var spec = (/&storyboard_spec=(.*?)&/.exec(this.xhr.responseText) || []) [1];
  161. }
  162. if (spec) {
  163. if (fromWatch) {
  164. spec = eval(spec); // remove backslashes
  165. }
  166. this.parseSpec(decodeURIComponent(spec));
  167. callback.apply(this);
  168. }
  169. else if (!fromWatch) {
  170. this.getStoryboardSpecs(callback, true);
  171. }
  172. else {
  173. div.style.background = null;
  174. }
  175. }
  176. else {
  177. this.stop();
  178. }
  179. }).bind(this);
  180. if (fromWatch) {
  181. this.xhr.open('GET', '/watch?v=' + this.v, true);
  182. }
  183. else {
  184. this.xhr.open('GET', '/get_video_info?video_id=' + this.v, true);
  185. }
  186. this.xhr.send();
  187. };
  188. Animator.prototype.start = function () {
  189. dbg("start " + this.id);
  190. this.frame = 0;
  191. this.sheet = 0;
  192. var lastTick;
  193. var next = function () {
  194. dbg("next " + this.id + " token " + this.token);
  195. if (this.isMouseOver()) {
  196. if (this.spec.fit == 'height') {
  197. var w = this.spec.width * this.height / this.spec.height;
  198. var offset = (this.width - w) / 2;
  199. div.style.backgroundPosition = -((this.frame % this.spec.cols) * w - offset) + 'px ' +
  200. -(((this.frame / this.spec.cols) | 0) * this.height) + 'px'; // bitwise OR with 0 to loose decimals
  201. }
  202. else {
  203. var h = this.spec.height * this.width / this.spec.width;
  204. var offset = (this.height - h) / 2;
  205. div.style.backgroundPosition = -((this.frame % this.spec.cols) * this.width) + 'px ' +
  206. -(((this.frame / this.spec.cols) | 0) * h - offset) + 'px';
  207. }
  208. this.frame++;
  209. if ((this.frame == this.spec.sheetSize) || (this.sheet == this.spec.sheetCount - 1 && this.frame == this.spec.countOnLastSheet))
  210. {
  211. clearInterval(this.token);
  212. dbg("clear " + this.id + " token " + this.token);
  213. this.loadImage(function () {
  214. this.frame = 0;
  215. this.sheet = (this.sheet + 1) % this.spec.sheetCount;
  216. this.token = setInterval(next, this.interval);
  217. dbg("set " + this.id + " token " + this.token);
  218. });
  219. }
  220. }
  221. else {
  222. this.stop();
  223. }
  224. }.bind(this);
  225. this.loadImage(function () {
  226. if (this.isMouseOver()) {
  227. if ((this.width / this.height) <= (this.spec.width / this.spec.height)) {
  228. this.spec.fit = 'height';
  229. div.style.backgroundSize = ((this.spec.width * this.height / this.spec.height) * this.spec.cols) + 'px ' +
  230. (this.height * this.spec.rows) + 'px';
  231. }
  232. else {
  233. this.spec.fit = 'width';
  234. div.style.backgroundSize = (this.width * this.spec.cols) + 'px ' +
  235. ((this.spec.height * this.width / this.spec.width) * this.spec.rows) + 'px';
  236. }
  237. this.token = setInterval(next, this.interval);
  238. dbg("set " + id + " token " + this.token);
  239. }
  240. else {
  241. this.stop();
  242. }
  243. });
  244. };
  245. Animator.prototype.stop = function () {
  246. dbg("stop " + this.id);
  247. if (!this.stopped) {
  248. clearInterval(this.token);
  249. dbg("clear " + this.id + " token " + this.token);
  250. div.style.display = 'none';
  251. }
  252. this.stopped = true;
  253. };
  254. Animator.prototype.isMouseOver = function () {
  255. return mousePos.x >= this.x && mousePos.x <= this.x + this.width &&
  256. mousePos.y >= this.y && mousePos.y <= this.y + this.height;
  257. };