FYTE /Fast YouTube Embedded/ Player

Hugely improves load speed of pages with lots of embedded Youtube videos by instantly showing clickable and immediately accessible placeholders, then the thumbnails are loaded in background. Optionally a fast simple HTML5 direct playback (720p max) can be selected if available for the video.

当前为 2020-03-25 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name FYTE /Fast YouTube Embedded/ Player
  3. // @description Hugely improves load speed of pages with lots of embedded Youtube videos by instantly showing clickable and immediately accessible placeholders, then the thumbnails are loaded in background. Optionally a fast simple HTML5 direct playback (720p max) can be selected if available for the video.
  4. // @description:ru На порядок ускоряет время загрузки страниц с большим количеством вставленных Youtube-видео. С первого момента загрузки страницы появляются заглушки для видео, которые можно щелкнуть для загрузки плеера, и почти сразу же появляются кавер-картинки с названием видео. В опциях можно включить режим использования упрощенного браузерного плеера (макс. 720p).
  5. // @version 2.11.0
  6. // @include *
  7. // @exclude /^https:\/\/(www\.)?youtube\.com\/(?!embed)/
  8. // @exclude https://accounts.google.*/o/oauth2/postmessageRelay*
  9. // @exclude https://clients*.google.*/youtubei/*
  10. // @exclude https://clients*.google.*/static/proxy*
  11. // @author wOxxOm
  12. // @namespace wOxxOm.scripts
  13. // @license MIT License
  14. // @grant GM_getValue
  15. // @grant GM_listValues
  16. // @grant GM_deleteValue
  17. // @grant GM_setValue
  18. // @grant GM_addStyle
  19. // @grant GM_xmlhttpRequest
  20. // @connect www.youtube.com
  21. // @connect youtube.com
  22. // @run-at document-start
  23. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACABAMAAAAxEHz4AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAwUExURUxpcQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJuxkb8AAAAPdFJOUwDvH0biMvjOZFW6pxJ6kh7r3iMAAAPDSURBVGje7ZlNaBNBFMeXhNDWpkKgFg9iYlBB6SGwiPQkftaCYATjTaRKiwi1xVaKXpqTpHhosR66p4pQhb209CQGbSweK/TiRYwfFy+NkWBM2pR2nHm73abJzuceRMj/kEzSvl92Z9689+atpjXUUEMN1WgpoRupbH41nTbNUaxzlkIhe0C+M810Ov8zmzL0RGeNeeDThUEkof72N/Fqe/8LJK07sR173yJS0EbEATxFSurZtm9DilqxAV9VAZuWfbPbLBOFqtSBP9f/WxIAV2Bc6H5owiKPG7p+IpFIRG11LsPbEfyVrhvTqeyX1dfmaBiM9gFgjgwrTzJSfncMFq7s3EExJuu5/rHte3hPBvfkff84sbuEBxPkUiLygCC5hDV7CvpUtt81axICZBN9UwHsxYalOMxhIaIC8IVhFlvJtlALIWQl57Um/LquBpjBpkOwin1qADKLB7RD9moqiPz2TcAMqQGa4OI9Av5op/DrMzXAHmz6mw4IxEQA67AW825/bhngAVoBMEHzZD+aFQCsQUCkAAor/M2wCYAVdwCqxJmANgD8cmJjPQDt5wK22AD0nAVoBsAiE1BMcgAbAJikAqoTYP1CA4BEtBgdgC6yARUuAC3QI7sDiLMAxUk2YAwiIwNAn4YAhGU+YKcOqAUMCgJQziugHGMALmNAhANAWxkaoEgABS4ADdMyiyiglPMIcJ0GKQAayDAAGQEAuu8VUB/gJAH1AS4IgLAwAA24AAoygAeuAPFbqHPHoNwc1HuCJCDncRl7NG8At7Ak48qugVEGsOBxO7snB58T0ngASlwWjomFpMegOusxrFOLBCexsFMbvUzxCyVXRqEkBpjlpXdOgcEqFlsEKpRynFviMIus0md+kcUEDAuUeaxCcysjUGgySt1yTKTUZRTbOaFim17unxUr92doBw4f9zTKObGInZl+//NTW592VP3g+Q4Onh6Ovjfgt5vsPoSCJuDuPRz/58CFmhEtKPIEvY8kZAd3VxRxRJJSyIXcUu0/VOz3okITJRC2ex9kGdB5ecBVZLtgCyt70fUB2nGTTjOu/HFZohsXXLoOrbQKfDps1ePtTj9wSter2oGWoBnYRZqB+bQ5OnLaShpnrNAz6N6R7OW1I1HJjnmPVFuit7eDV1jNvuAkpJNqgJ0DQPCHiv3dqmULfJe3P7hrB/oej3T0S/Tme7tf1Xp/MArPB/Ayp82X5OlAaJfI8wHsJ2/zWXg6EGV4XXB5CbuN3mUYxnQKNI6HU9i3op0y3tpQQw39b/oLfDt0HcsiqWsAAAAASUVORK5CYII=
  24. // @compatible chrome
  25. // @compatible firefox
  26. // @compatible opera
  27. // ==/UserScript==
  28.  
  29. 'use strict';
  30.  
  31. // keep video info cache for a month since last time it's shown
  32. var CACHE_STALE_DURATION = 30 * 24 * 3600e3;
  33.  
  34. if (/^https:\/\/(www\.)?youtube\.com\//.test(location)) {
  35. if (!window.chrome || window === window.parent)
  36. return;
  37. parent.postMessage('FYTE-toggle-fullscreen-init', '*');
  38. addEventListener('message', function onMessage(e) {
  39. if (e.source !== parent || e.data !== 'FYTE-toggle-fullscreen-init-confirmed')
  40. return;
  41. removeEventListener('message', onMessage);
  42. var fsbtn = document.getElementsByClassName('ytp-fullscreen-button');
  43. new MutationObserver(function() {
  44. if (fsbtn[0]) {
  45. this.disconnect();
  46. // noinspection SillyAssignmentJS
  47. fsbtn[0].outerHTML = fsbtn[0].outerHTML.replace('aria-disabled="true"', '');
  48. fsbtn[0].addEventListener('click', function() {
  49. window.parent.postMessage('FYTE-toggle-fullscreen', '*');
  50. });
  51. }
  52. }).observe(document, {subtree:true, childList:true});
  53. });
  54. return;
  55. }
  56.  
  57. var resizeMode = GM_getValue('resize', 'Fit to width');
  58. if (typeof resizeMode !== 'string')
  59. resizeMode = resizeMode ? 'Fit to width' : 'Original';
  60.  
  61. var resizeWidth = GM_getValue('width', 1280) |0;
  62. var resizeHeight = GM_getValue('height', 720) |0;
  63. updateCustomSize();
  64.  
  65. var pinnedWidth = GM_getValue('pinnedWidth', 400) |0;
  66.  
  67. var playDirectly = !!GM_getValue('playHTML5', false);
  68. var playDirectlyShown = !!GM_getValue('playHTML5shown', false);
  69. var skipCustom = !!GM_getValue('skipCustom', true);
  70. var showStoryboard = !!GM_getValue('showStoryboard', true);
  71. var pinnable = GM_getValue('pinnable', 'on');
  72. if (!/^(on|hide|off)$/.test(pinnable))
  73. pinnable = !!pinnable ? 'on' : 'hide';
  74. var useInvidious = GM_getValue('invidious', false);
  75.  
  76. var _ = initTL();
  77.  
  78. var imageLoader = document.createElement('img');
  79. var imageLoader2 = document.createElement('img');
  80.  
  81. var fytedom = document.getElementsByClassName('instant-youtube-container');
  82. var iframes = document.getElementsByTagName('iframe');
  83. var objects = document.getElementsByTagName('object');
  84. var checked = [];
  85. var persite = (function() {
  86. var rules = [
  87. {host: 'developers.google.com',
  88. match: '[data-video-id]',
  89. src: function(e) { return '//youtube.com/embed/' + e.dataset.videoId }},
  90. {host: 'play.google.com', eatparent: 0},
  91. {host: /(^|\.)google\.\w{2,3}(\.\w{2,3})?$/, class:'g-blk', query: 'a[href*="youtube.com/watch"][data-ved]', eatparent: 1},
  92. {host: 'pikabu.ru', class:'b-video', match: '[data-url*="youtube.com/embed"]', attr: 'data-url'},
  93. {host: 'androidauthority.com', eatparent: '.video-container'},
  94. {host: 'reddit.com',
  95. match: '[data-url*="youtube.com/"] [src*="/mediaembed"], [data-url*="youtu.be/"] [src*="/mediaembed"]',
  96. src: function(e) { return e.closest('[data-url*="youtube.com/"], [data-url*="youtu.be/"]').dataset.url }},
  97. {host: /(www\.)?theverge\.com$/, eatparent: '.p-scalable-video'},
  98. {host: '9gag.com', eatparent: 0},
  99. {host: 'reddit.com', match: '[data-url*="youtube.com"] iframe[src*="redditmedia.com/mediaembed"]',
  100. src: function(e) { return e.closest('[data-url*="youtube.com"]').dataset.url }},
  101. {host: 'anilist.co', eatparent: '.youtube'},
  102. ];
  103. for (var i=0, rule; (i<rules.length) && (rule=rules[i]); i++) {
  104. var rx = rule.host instanceof RegExp ? rule.host : new RegExp('(^|\\.)' + rule.host.replace(/\./g, '\\.') + '$', 'i');
  105. if (rx.test(location.hostname)) {
  106. if (!rule.tag && !rule.class)
  107. rule.tag = 'iframe';
  108. if (!rule.match && !rule.query)
  109. rule.match = '[src*="youtube.com/embed"]';
  110. return {
  111. nodes: rule.class ? document.getElementsByClassName(rule.class) : document.getElementsByTagName(rule.tag),
  112. match: rule.match ?
  113. function(e) {
  114. // noinspection JSReferencingMutableVariableFromClosure
  115. return e.matches(rule.match) ? e : null;
  116. } :
  117. function(e) {
  118. // noinspection JSReferencingMutableVariableFromClosure
  119. return e.querySelector(rule.query);
  120. },
  121. attr: rule.attr,
  122. src: rule.src,
  123. eatparent: rule.eatparent,
  124. };
  125. }
  126. }
  127. })();
  128.  
  129. findEmbeds([]);
  130. injectStylesIfNeeded();
  131. new MutationObserver(findEmbeds).observe(document, {subtree:true, childList:true});
  132.  
  133. document.addEventListener('DOMContentLoaded', function(e) {
  134. injectStylesIfNeeded();
  135. adjustNodesIfNeeded(e);
  136. setTimeout(cleanupCache, 60e3);
  137. });
  138.  
  139. window.addEventListener('resize', adjustNodesIfNeeded, true);
  140.  
  141. window.addEventListener('message', function(e) {
  142. switch (e.data) {
  143. case 'FYTE-toggle-fullscreen-init':
  144. if (e.source)
  145. e.source.postMessage('FYTE-toggle-fullscreen-init-confirmed', '*');
  146. break;
  147. case 'FYTE-toggle-fullscreen':
  148. $$('iframe[allowfullscreen]').some(function (iframe) {
  149. if (iframe.contentWindow === e.source) {
  150. goFullscreen(iframe, !(document.webkitIsFullScreen || document.mozIsFullScreen || document.isFullScreen));
  151. return true;
  152. }
  153. });
  154. break;
  155. case 'iframe-allowfs':
  156. $$('iframe:not([allowfullscreen])').some(function (iframe) {
  157. if (iframe.contentWindow === e.source) {
  158. iframe.allowFullscreen = true;
  159. return true;
  160. }
  161. });
  162. if (window !== window.top)
  163. window.parent.postMessage('iframe-allowfs', '*');
  164. break;
  165. }
  166. });
  167.  
  168. function findEmbeds(mutations) {
  169. var i, len, e, m;
  170. if (mutations.length === 1) {
  171. var added = mutations[0].addedNodes;
  172. if (!added[0] || !added[1] && added[0].nodeType === 3)
  173. return;
  174. }
  175. if (persite)
  176. for (i=0, len=persite.nodes.length; (i<len) && (e=persite.nodes[i]); i++)
  177. if ((e = persite.match(e)))
  178. processEmbed(e, persite.src && persite.src(e) || e.getAttribute(persite.attr));
  179. for (i=0, len=iframes.length; (i<len) && (e=iframes[i]); i++) {
  180. if (checked.includes(e)) continue;
  181. checked.push(e);
  182. if (/youtube(-nocookie)?\.com|youtu\.be/i.test(e.src || e.dataset.src))
  183. processEmbed(e, e.src || e.dataset.src);
  184. }
  185. for (i=0, len=objects.length; (i<len) && (e=objects[i]); i++) {
  186. if (checked.includes(e)) continue;
  187. checked.push(e);
  188. if ((m = e.querySelector('embed, [value*="youtu.be"], [value*="youtube.com"]')))
  189. processEmbed(e, m.src || e.dataset.src || 'https://' + m.value.match(/youtu\.be.*|youtube\.com.*/)[0]);
  190. }
  191. }
  192.  
  193. function processEmbed(node, src) {
  194. function decodeEmbedUrl(url) {
  195. return /youtube(-nocookie)?\.com%2Fembed/.test(url)
  196. ? decodeURIComponent(url.replace(/^.*?(http[^&?=]+?youtube(-nocookie)?\.com%2Fembed[^&]+).*$/i, '$1'))
  197. : url;
  198. }
  199. src = src || node.src || node.href || '';
  200. var n = node;
  201. var np = n.parentNode;
  202. var srcFixed = decodeEmbedUrl(src).replace(/\/(watch\?v=|v\/)/, '/embed/').replace(/^([^?&]+)&/, '$1?');
  203. if (src.indexOf('cdn.embedly.com/') > 0 ||
  204. resizeMode !== 'Original' && np && np.children.length === 1 && !np.className && !np.id) {
  205. n = location.hostname === 'disqus.com' ? np.parentNode : np;
  206. np = n.parentElement;
  207. }
  208. if (!np ||
  209. !np.parentNode ||
  210. skipCustom && srcFixed.indexOf('enablejsapi=1') > 0 ||
  211. srcFixed.indexOf('/embed/videoseries') > 0 ||
  212. node.matches('.instant-youtube-embed, .YTLT-embed, .ihvyoutube') ||
  213. node.style.position === 'fixed' ||
  214. node.onload // skip some retarded loaders
  215. )
  216. return;
  217.  
  218. var id = srcFixed.match(/(?:^(?:https?:)?\/\/)(?:www\.)?(?:youtube(?:-nocookie)?\.com\/(?:embed\/(?:v=)?|\/.*?[&?\/]v[=\/])|youtu\.be\/)([^\s,.()\[\]?]+?)(?:[&?\/].*|$)/);
  219. if (!id)
  220. return;
  221. id = id[1];
  222.  
  223. if (np.localName === 'object')
  224. n = np, np = n.parentElement;
  225.  
  226. var eatparent = persite && persite.eatparent || 0;
  227. if (typeof eatparent === 'string')
  228. n = np.closest(eatparent) || n, np = n.parentElement;
  229. else
  230. while (eatparent--)
  231. n = np, np = n.parentElement;
  232.  
  233. createFYTE(node, n, id, setUrl(srcFixed));
  234. stopOriginalEmbed(node);
  235. }
  236.  
  237. function createFYTE(node, n, id, srcFixed, force) {
  238. if (!document.contains(n)) return;
  239. var cache = tryJSONparse(localStorage['FYTE-cache-' + id]) || {id: id};
  240. var autoplay = /[?&](autoplay=1|ps=play)(&|$)/.test(srcFixed);
  241. var img = document.createElement('img');
  242. if (!autoplay) {
  243. img.src = setUrl(cache.cover || 'https://i.ytimg.com/vi/' + id + '/maxresdefault.jpg');
  244. img.onerror = onCoverError;
  245. }
  246.  
  247. if (document.readyState !== 'complete' && !force) {
  248. var args = [createFYTE, 0];
  249. args.push.apply(args, arguments);
  250. args[createFYTE.length + 1] = true;
  251. setTimeout.apply(window, args);
  252. return;
  253. }
  254.  
  255. injectStylesIfNeeded('force');
  256.  
  257. var div = document.createElement('div');
  258. div.className = 'instant-youtube-container';
  259. div.FYTE = {
  260. state: 'querying',
  261. srcEmbed: srcFixed.replace(/&$/, ''),
  262. originalWidth: /%/.test(node.width) ? 320 : node.width|0 || n.clientWidth|0,
  263. originalHeight: /%/.test(node.height) ? 200 : node.height|0 || n.clientHeight|0,
  264. cache: cache,
  265. };
  266. div.FYTE.srcEmbedFixed = div.FYTE.srcEmbed.replace(/^http:/, 'https:').replace(/([&?])(wmode=\w+|feature=oembed)&?/, '$1').replace(/[&?]$/, '');
  267. div.FYTE.srcWatchFixed = div.FYTE.srcEmbedFixed.replace(/\/embed\//, '/watch?v=').replace(/(\?.*?)\?/, '$1&');
  268.  
  269. cache.lastUsed = Date.now();
  270. localStorage['FYTE-cache-' + id] = JSON.stringify(cache);
  271.  
  272. if (cache.reason)
  273. div.setAttribute('disabled', '');
  274.  
  275. var divSize = calcContainerSize(div, n);
  276. var origStyle = getComputedStyle(n);
  277. div.style.cssText = important(
  278. (origStyle.transform ? 'transform:' + origStyle.transform + ';' : '') +
  279. (autoplay ? '' : 'background-color:transparent; transition:background-color 2s;') +
  280. Object.keys(origStyle.hasOwnProperty('position') ? origStyle : origStyle.__proto__ /*FF*/)
  281. .filter(function(k) { return /^(position|left|right|top|bottom)$/.test(k) })
  282. .map(function(k) { return k + ':' + origStyle[k] })
  283. .join(';')
  284. .replace(/\b[^;:]+:\s*(auto|static|block)\s*(!\s*important)?;/g, '') +
  285. (origStyle.display === 'inline' ? ';display:inline-block;width:100%' : '') +
  286. ';min-width:' + Math.min(divSize.w, div.FYTE.originalWidth) + 'px' +
  287. ';min-height:' + Math.min(divSize.h, div.FYTE.originalHeight) + 'px' +
  288. (resizeMode === 'Fit to width' ? ';width:100%' : '') +
  289. ';max-width:' + divSize.w + 'px; height:' + (persite && persite.eatparent === 0 ? '100%;' : divSize.h + 'px;'));
  290. if (!autoplay) {
  291. setTimeout(function() { div.style.backgroundColor = '' }, 0);
  292. setTimeout(function() { div.style.transition = '' }, 2000);
  293. }
  294.  
  295. var wrapper = div.appendChild(document.createElement('div'));
  296. wrapper.className = 'instant-youtube-wrapper';
  297.  
  298. wrapper.appendChild(img);
  299. img.className = 'instant-youtube-thumbnail';
  300. img.style.cssText = important((cache.cover ? '' : 'transition:opacity 0.1s ease-out; opacity:0; ') +
  301. 'padding:0; margin:auto; position:absolute; left:0; right:0; top:0; bottom:0; max-width:none; max-height:none;');
  302.  
  303. img.FYTE = [div, divSize, autoplay]
  304. img.onload = onCoverLoad;
  305. if (cache.coverWidth || img.naturalWidth)
  306. img.onload();
  307.  
  308. translateHTML(wrapper, 'beforeend', '\
  309. <a class="instant-youtube-title" target="_blank" href="' + div.FYTE.srcWatchFixed + '">' +
  310. (cache.title || cache.reason ? '<strong>' + (cache.title || cache.reason || '') + '</strong>' +
  311. (cache.duration ? '<span>' + cache.duration + '</span>' : '') +
  312. (cache.fps ? '<i>, ' + cache.fps + 'fps</i>' : '')
  313. : '&nbsp;') + '</a>\
  314. <svg class="instant-youtube-play-button">\
  315. <path fill-rule="evenodd" clip-rule="evenodd" fill="#1F1F1F" class="ytp-large-play-button-svg" d="M84.15,26.4v6.35c0,2.833-0.15,5.967-0.45,9.4c-0.133,1.7-0.267,3.117-0.4,4.25l-0.15,0.95c-0.167,0.767-0.367,1.517-0.6,2.25c-0.667,2.367-1.533,4.083-2.6,5.15c-1.367,1.4-2.967,2.383-4.8,2.95c-0.633,0.2-1.316,0.333-2.05,0.4c-0.767,0.1-1.3,0.167-1.6,0.2c-4.9,0.367-11.283,0.617-19.15,0.75c-2.434,0.034-4.883,0.067-7.35,0.1h-2.95C38.417,59.117,34.5,59.067,30.3,59c-8.433-0.167-14.05-0.383-16.85-0.65c-0.067-0.033-0.667-0.117-1.8-0.25c-0.9-0.133-1.683-0.283-2.35-0.45c-2.066-0.533-3.783-1.5-5.15-2.9c-1.033-1.067-1.9-2.783-2.6-5.15C1.317,48.867,1.133,48.117,1,47.35L0.8,46.4c-0.133-1.133-0.267-2.55-0.4-4.25C0.133,38.717,0,35.583,0,32.75V26.4c0-2.833,0.133-5.95,0.4-9.35l0.4-4.25c0.167-0.966,0.417-2.05,0.75-3.25c0.7-2.333,1.567-4.033,2.6-5.1c1.367-1.434,2.967-2.434,4.8-3c0.633-0.167,1.333-0.3,2.1-0.4c0.4-0.066,0.917-0.133,1.55-0.2c4.9-0.333,11.283-0.567,19.15-0.7C35.65,0.05,39.083,0,42.05,0L45,0.05c2.467,0,4.933,0.034,7.4,0.1c7.833,0.133,14.2,0.367,19.1,0.7c0.3,0.033,0.833,0.1,1.6,0.2c0.733,0.1,1.417,0.233,2.05,0.4c1.833,0.566,3.434,1.566,4.8,3c1.066,1.066,1.933,2.767,2.6,5.1c0.367,1.2,0.617,2.284,0.75,3.25l0.4,4.25C84,20.45,84.15,23.567,84.15,26.4z M33.3,41.4L56,29.6L33.3,17.75V41.4z">\
  316. <title tl>msgAltPlayerHint</title>\
  317. </path>\
  318. <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" points="33.3,41.4 33.3,17.75 56,29.6"></polygon>\
  319. </svg>\
  320. <span tl class="instant-youtube-alternative">' + (playDirectly ? 'Play with Youtube player' : 'Play directly (up to 720p)') + '</span>\
  321. <div tl class="instant-youtube-options-button">Options</div>\
  322. ');
  323.  
  324. n.parentNode.insertBefore(div, n);
  325. n.remove();
  326.  
  327. if (!cache.title && !cache.reason || autoplay && playDirectly)
  328. fetchInfo.call(div);
  329.  
  330. if (autoplay)
  331. return startPlaying(div);
  332.  
  333. div.addEventListener('click', clickHandler);
  334. div.addEventListener('mousedown', clickHandler);
  335. div.addEventListener('mouseenter', fetchInfo);
  336. }
  337.  
  338. function fetchInfo() {
  339. this.removeEventListener('mouseenter', fetchInfo);
  340. if (!this.FYTE.storyboard) {
  341. GM_xmlhttpRequest({
  342. method: 'GET',
  343. url: 'https://www.youtube.com/get_video_info?video_id=' + this.FYTE.cache.id +
  344. '&hl=en_US&html5=1&el=embedded&eurl=' + encodeURIComponent(location.href),
  345. context: this,
  346. onload: parseVideoInfo
  347. });
  348. }
  349. }
  350.  
  351. function onCoverLoad(e) {
  352. var data = (this.FYTE || []).slice(),
  353. div = data.shift(), cache = div.FYTE.cache,
  354. divSize = data.shift(),
  355. autoplay = data.shift();
  356. if (this.naturalWidth <= 120 && !cache.cover)
  357. return this.onerror(e);
  358. // delete this.FYTE;
  359. var fitToWidth = true;
  360. if (this.naturalHeight || cache.coverHeight) {
  361. if (!cache.coverHeight) {
  362. cache.coverWidth = this.naturalWidth;
  363. cache.coverHeight = this.naturalHeight;
  364. localStorage['FYTE-cache-' + cache.id] = JSON.stringify(cache);
  365. }
  366. var ratio = cache.coverWidth / cache.coverHeight;
  367. if (ratio > 4.1 / 3 && ratio < divSize.w / divSize.h) {
  368. this.style.cssText += important('width:auto; height:100%;');
  369. fitToWidth = false;
  370. }
  371. }
  372. if (fitToWidth) {
  373. this.style.cssText += important('width:100%; height:auto;');
  374. }
  375. if (cache.videoWidth)
  376. fixThumbnailAR(div);
  377. if (!autoplay)
  378. this.style.opacity = 1;
  379. }
  380.  
  381. function onCoverError() {
  382. var src = this.src;
  383. if (src.indexOf('maxresdefault') > 0)
  384. this.src = src.replace('maxresdefault', 'sddefault');
  385. else if (src.indexOf('sddefault') > 0)
  386. this.src = src.replace('sddefault', 'hqdefault');
  387. // else
  388. // delete this.FYTE;
  389. }
  390.  
  391. function stopOriginalEmbed(node) {
  392. var src = 'data:,', n = node;
  393. while (n) {
  394. if (n.src) n.src = src;
  395. if (n.dataset.src) n.dataset.src = src;
  396. n = $(n, 'embed');
  397. }
  398. $$(node, '[value*="youtu.be"], [value*="youtube.com"]').forEach(function (el) {
  399. el.value = src;
  400. });
  401. }
  402.  
  403. function adjustNodesIfNeeded(e) {
  404. if (!fytedom[0])
  405. return;
  406. if (adjustNodesIfNeeded.scheduled)
  407. clearTimeout(adjustNodesIfNeeded.scheduled);
  408. adjustNodesIfNeeded.scheduled = setTimeout(function() {
  409. adjustNodes(e);
  410. adjustNodesIfNeeded.scheduled = 0;
  411. }, 16);
  412. }
  413.  
  414. function adjustNodes(e, clickedContainer) {
  415. var force = !!clickedContainer;
  416. var nearest = force ? clickedContainer : null;
  417.  
  418. var vids = $$('.instant-youtube-container:not([pinned]):not([stub])');
  419.  
  420. if (!nearest && e.type !== 'DOMContentLoaded') {
  421. var minDistance = window.innerHeight*3/4 |0;
  422. var nearTargetY = window.innerHeight/2;
  423. vids.forEach(function(n) {
  424. var bounds = n.getBoundingClientRect();
  425. var distance = Math.abs((bounds.bottom + bounds.top)/2 - nearTargetY);
  426. if (distance < minDistance) {
  427. minDistance = distance;
  428. nearest = n;
  429. }
  430. });
  431. }
  432.  
  433. if (nearest) {
  434. var bounds = nearest.getBoundingClientRect();
  435. var nearestCenterYpct = (bounds.top + bounds.bottom)/2 / window.innerHeight;
  436. }
  437.  
  438. var resized = false;
  439.  
  440. vids.forEach(function(n) {
  441. var size = calcContainerSize(n);
  442. var w = size.w, h = size.h;
  443.  
  444. // prevent parent clipping
  445. for (var e=n.parentElement, style; e; e=e.parentElement)
  446. if (e.style.overflow !== 'visible' && (style=getComputedStyle(e)))
  447. if ((style.overflow+style.overflowX+style.overflowY).match(/hidden|scroll/))
  448. if (n.offsetTop < e.clientHeight / 2 && n.offsetTop + n.clientHeight > e.clientHeight)
  449. e.style.cssText = e.style.cssText.replace(/\boverflow(-[xy])?:[^;]+/g, '') +
  450. important('overflow:visible;overflow-x:visible;overflow-y:visible;');
  451.  
  452. if (force && Math.abs(w - parseFloat(n.style.maxWidth)) <= 2)
  453. return;
  454.  
  455. if (n.style.maxWidth !== w + 'px') overrideCSS(n, {'max-width': w + 'px'});
  456. if (n.style.height !== h + 'px') overrideCSS(n, {'height': h + 'px'});
  457. if (parseFloat(n.style.minWidth) > w) overrideCSS(n, {'min-width': n.style.maxWidth});
  458. if (parseFloat(n.style.minHeight) > h) overrideCSS(n, {'min-height': n.style.height});
  459.  
  460. fixThumbnailAR(n);
  461. resized = true;
  462. });
  463.  
  464. if (resized && nearest)
  465. setTimeout(function() {
  466. var bounds = nearest.getBoundingClientRect();
  467. var h = bounds.bottom - bounds.top;
  468. var projectedCenterY = nearestCenterYpct * window.innerHeight;
  469. var projectedTop = projectedCenterY - h/2;
  470. var safeTop = Math.min(Math.max(0, projectedTop), window.innerHeight - h);
  471. window.scrollBy(0, bounds.top - safeTop);
  472. }, 16);
  473. }
  474.  
  475. function calcContainerSize(div, origNode) {
  476. origNode = origNode || div;
  477. var w, h;
  478. var np = origNode.parentElement;
  479. var style = getComputedStyle(np);
  480. var parentWidth = parseFloat(style.width) - floatPadding(np, style, 'Left') - floatPadding(np, style, 'Right');
  481. if (+style.columnCount > 1)
  482. parentWidth = (parentWidth + parseFloat(style.columnGap)) / style.columnCount - parseFloat(style.columnGap);
  483. switch (resizeMode) {
  484. case 'Original':
  485. if (div.FYTE.originalWidth === 320 && div.FYTE.originalHeight === 200) {
  486. w = parentWidth;
  487. h = parentWidth / 16 * 9;
  488. } else {
  489. w = div.FYTE.originalWidth;
  490. h = div.FYTE.originalHeight;
  491. }
  492. break;
  493. case 'Custom':
  494. w = resizeWidth;
  495. h = resizeHeight;
  496. break;
  497. case '1080p':
  498. case '720p':
  499. case '480p':
  500. case '360p':
  501. h = parseInt(resizeMode);
  502. w = h / 9 * 16;
  503. break;
  504. default: // fit-to-width mode
  505. var n = origNode;
  506. do {
  507. n = n.parentElement;
  508. // find parent node with nonzero width (i.e. independent of our video element)
  509. } while (n && !(w = n.clientWidth));
  510. if (w)
  511. h = w / 16 * 9;
  512. else {
  513. w = origNode.clientWidth;
  514. h = origNode.clientHeight;
  515. }
  516. }
  517. if (parentWidth > 0 && parentWidth < w) {
  518. h = parentWidth / w * h;
  519. w = parentWidth;
  520. }
  521. if (resizeMode === 'Fit to width' && h < div.FYTE.originalHeight*0.9)
  522. h = Math.min(div.FYTE.originalHeight, w / div.FYTE.originalWidth * div.FYTE.originalHeight);
  523.  
  524. return {w: window.chrome ? w : Math.round(w), h:h};
  525. }
  526.  
  527. function parseVideoInfo(response) {
  528. var div = response.context;
  529. var txt = response.responseText;
  530. var reason = txt.match(/(^|&)reason=(.+?)(&|$)|$/)[2];
  531. var info = tryJSONparse(decodeURIComponent(txt.match(/(^|&)player_response=(.+?)(&|$)|$/)[2] || '')) || {};
  532. var vid = info.videoDetails || {};
  533. var streams = info.streamingData || {};
  534. var cache = div.FYTE.cache;
  535. var shouldUpdateCache = false;
  536.  
  537. var videoSources = [];
  538. var fmts = (streams.formats || streams.adaptiveFormats || [])
  539. .sort(function (a, b) { return b.width - a.width || b.height - a.height });
  540. // parse width & height to adjust the thumbnail
  541. if (fmts.length && (cache.videoWidth !== fmts[0].width || cache.videoHeight !== fmts[0].height)) {
  542. fixThumbnailAR(div, fmts[0].width, fmts[0].height);
  543. cache.videoWidth = fmts[0].width;
  544. cache.videoHeight = fmts[0].height;
  545. shouldUpdateCache = true;
  546. }
  547.  
  548. // parse video sources
  549. fmts.forEach(function (f) {
  550. var codec = f.mimeType.match(/codecs="([^.]+)|$/)[1] || '';
  551. var type = f.mimeType.split(/[/;]/)[1];
  552. var src = f.url;
  553. if (!src && f.cipher) {
  554. var sp = {};
  555. f.cipher.split('&').forEach(function (str) {
  556. var kv = str.split('=');
  557. sp[kv[0]] = kv[1];
  558. });
  559. src = decodeURIComponent(sp.url);
  560. if (sp.s)
  561. src += '&' + (sp.sp || 'sig') + '=' + decodeYoutubeSignature(sp.s);
  562. }
  563. videoSources.push({
  564. src: src,
  565. title: [
  566. f.quality,
  567. f.qualityLabel !== f.quality ? f.qualityLabel : '',
  568. type + (codec ? ':' + codec : ''),
  569. ].filter(Boolean).join(', '),
  570. });
  571. });
  572.  
  573. var fps = {};
  574. (streams.adaptiveFormats || []).forEach(function (f) {
  575. if (f.fps) fps[f.fps] = true;
  576. });
  577. fps = Object.keys(fps).join('/');
  578. if (fps && cache.fps !== fps) {
  579. cache.fps = fps;
  580. shouldUpdateCache = true;
  581. }
  582.  
  583. var duration = div.FYTE.duration = vid.lengthSeconds |0;
  584. if (duration) {
  585. var d = new Date(null);
  586. d.setSeconds(duration);
  587. duration = d.toISOString().replace(/^.+?T[0:]{0,4}(.+?)\..+$/, '$1');
  588. if (cache.duration !== duration) {
  589. cache.duration = duration;
  590. shouldUpdateCache = true;
  591. }
  592. }
  593. if (duration || fps)
  594. duration = '<span>' + duration + '</span>' +
  595. (fps ? '<i>, ' + fps + 'fps</i>' : '');
  596.  
  597. var title = decodeURIComponent(vid.title || reason || '').replace(/\+/g, ' ');
  598. if (title) {
  599. $(div, '.instant-youtube-title').innerHTML = (title ? '<strong>' + title + '</strong>' : '') + duration;
  600. if (cache.title !== title) {
  601. cache.title = title;
  602. shouldUpdateCache = true;
  603. }
  604. }
  605. if (pinnable !== 'off' && vid.title)
  606. makeDraggable(div);
  607.  
  608. if (reason) {
  609. div.setAttribute('disabled', '');
  610. if (cache.reason !== reason) {
  611. cache.reason = reason;
  612. shouldUpdateCache = true;
  613. }
  614. }
  615.  
  616. if (videoSources.length)
  617. div.FYTE.videoSources = videoSources;
  618.  
  619. if (txt.includes('playerStoryboardSpecRenderer') &&
  620. info.storyboards &&
  621. div.FYTE.state !== 'scheduled play') {
  622. var m = info.storyboards.playerStoryboardSpecRenderer.spec.split('|');
  623. div.FYTE.storyboard = tryJSONparse(
  624. m[m.length-1].replace(
  625. /^(\d+)#(\d+)#(\d+)#(\d+)#(\d+)#.+$/,
  626. '{"w":$1, "h":$2, "len":$3, "rows":$4, "cols":$5}'
  627. ));
  628. if (div.FYTE.storyboard.w * div.FYTE.storyboard.h > 2000) {
  629. div.FYTE.storyboard.url = m[0].replace('?', '&').replace('$L/$N.jpg',
  630. (m.length-2) + '/M0.jpg?sigh=' + m[m.length-1].replace(/^.+?#([^#]+)$/, '$1'));
  631. var btn = $(div, '.instant-youtube-options-button');
  632. if (btn) btn.insertAdjacentHTML('beforebegin',
  633. '<div class="instant-youtube-storyboard"' + (showStoryboard ? '' : ' disabled') + '>' +
  634. important('<div style="width:' + (div.FYTE.storyboard.w-1) + 'px; height:' + div.FYTE.storyboard.h + 'px;') +
  635. '">&nbsp;</div>' +
  636. '</div>');
  637. if (btn && showStoryboard)
  638. updateHoverHandler(div);
  639. }
  640. }
  641.  
  642. injectStylesIfNeeded();
  643.  
  644. if (div.FYTE.state === 'scheduled play')
  645. setTimeout(function() { startPlayingDirectly(div) }, 0);
  646.  
  647. div.FYTE.state = '';
  648.  
  649. try {
  650. var cover = vid.thumbnail.thumbnails.pop().url;
  651. if (cache.cover !== cover) {
  652. cache.cover = cover;
  653. shouldUpdateCache = true;
  654. var img = $(div, 'img');
  655. if (img.src && img.src !== cover) img.src = setUrl(cover);
  656. }
  657. } catch (e) {}
  658. if (shouldUpdateCache)
  659. localStorage['FYTE-cache-' + cache.id] = JSON.stringify(cache);
  660. }
  661.  
  662. function decodeYoutubeSignature(s) {
  663. var a = s.split('');
  664. a.reverse();
  665. swap(a, 24);
  666. a.reverse();
  667. swap(a, 41);
  668. a.reverse();
  669. swap(a, 2);
  670. return a.join('');
  671. function swap(a, b) {
  672. var c = a[0];
  673. a[0] = a[b % a.length];
  674. a[b % a.length] = c
  675. }
  676. }
  677.  
  678. function fixThumbnailAR(div, w, h) {
  679. var img = $(div, 'img');
  680. if (!img)
  681. return;
  682. var thw = img.naturalWidth, thh = img.naturalHeight;
  683. if (w && h) { // means thumbnail is still loading
  684. div.FYTE.cache.videoWidth = w;
  685. div.FYTE.cache.videoHeight = h;
  686. } else {
  687. w = div.FYTE.cache.videoWidth;
  688. h = div.FYTE.cache.videoHeight;
  689. if (!w || !h)
  690. return;
  691. }
  692. var divw = div.clientWidth, divh = div.clientHeight;
  693. // if both video and thumbnail are 4:3, fit the image to height
  694. //console.log(div, divw, divh, thw, thh, w, h, h/w*divw / divh - 1, thh/thw*divw / divh - 1);
  695. if (Math.abs(h/w*divw / divh - 1) > 0.05 && Math.abs(thh/thw*divw / divh - 1) > 0.05) {
  696. img.style.maxHeight = img.clientHeight + 'px';
  697. if (!div.FYTE.cache.videoWidth) // skip animation if thumbnail is already loaded
  698. img.style.transition = 'height 1s ease, margin-top 1s ease';
  699. setTimeout(function() {
  700. img.style.maxHeight = 'none';
  701. img.style.cssText += important(h/w >= divh/divw ? 'width:auto; height:100%;' : 'width:100%; height:auto;');
  702. setTimeout(function() {
  703. img.style.transition = '';
  704. }, 1000);
  705. }, 0);
  706. }
  707. }
  708.  
  709. function updateHoverHandler(div) {
  710. var sb = $(div, '.instant-youtube-storyboard');
  711. if (!showStoryboard) {
  712. if (!sb.getAttribute('disabled'))
  713. sb.setAttribute('disabled', '');
  714. return;
  715. }
  716. if (sb.hasAttribute('disabled'))
  717. sb.removeAttribute('disabled');
  718.  
  719. sb.addEventListener('click', storyboardClickHandler);
  720.  
  721. var oldIndex = null;
  722. var style = sb.firstElementChild.style;
  723. sb.addEventListener('mousemove', storyboardHoverHandler);
  724. sb.addEventListener('mouseout', storyboardHoverHandler);
  725.  
  726. div.addEventListener('mouseover', storyboardPreloader);
  727. div.addEventListener('mouseout', storyboardPreloader);
  728.  
  729. var spinner = document.createElement('span');
  730. spinner.className = 'instant-youtube-loading-spinner';
  731.  
  732. function storyboardClickHandler(e) {
  733. sb.removeEventListener('click', storyboardClickHandler);
  734. var offsetX = e.offsetX || e.clientX - this.getBoundingClientRect().left;
  735. div.FYTE.startAt = offsetX / this.clientWidth * div.FYTE.duration |0;
  736. div.FYTE.srcEmbedFixed = setUrlParams(div.FYTE.srcEmbedFixed, {start: div.FYTE.startAt});
  737. startPlaying(div, {alternateMode: e.shiftKey});
  738. }
  739.  
  740. function storyboardPreloader(e) {
  741. if (e.type === 'mouseout') {
  742. imageLoader.onload = null; imageLoader.src = '';
  743. spinner.remove();
  744. return;
  745. }
  746. if (!div.FYTE.storyboard || div.FYTE.storyboard.preloaded)
  747. return;
  748. var lastpart = (div.FYTE.storyboard.len-1)/(div.FYTE.storyboard.rows * div.FYTE.storyboard.cols) |0;
  749. if (lastpart <= 0)
  750. return;
  751. var part = 0;
  752. imageLoader.src = setStoryboardUrl(part++);
  753. imageLoader.onload = function() {
  754. if (part > lastpart) {
  755. div.FYTE.storyboard.preloaded = true;
  756. div.removeEventListener('mouseover', storyboardPreloader);
  757. div.removeEventListener('mouseout', storyboardPreloader);
  758. imageLoader.onload = null;
  759. imageLoader.src = '';
  760. spinner.remove();
  761. return;
  762. }
  763. imageLoader.src = setStoryboardUrl(part++);
  764. };
  765. }
  766.  
  767. function setStoryboardUrl(part) {
  768. return setUrl(div.FYTE.storyboard.url.replace(/M\d+\.jpg\?/, 'M' + part + '.jpg?'));
  769. }
  770.  
  771. function storyboardHoverHandler(e) {
  772. if (!showStoryboard || !div.FYTE.storyboard)
  773. return;
  774. if (e.type === 'mouseout')
  775. return imageLoader2.onload && imageLoader2.onload();
  776. var w = div.FYTE.storyboard.w;
  777. var h = div.FYTE.storyboard.h;
  778. var cols = div.FYTE.storyboard.cols;
  779. var rows = div.FYTE.storyboard.rows;
  780. var len = div.FYTE.storyboard.len;
  781. var partlen = rows * cols;
  782.  
  783. var offsetX = e.offsetX || e.clientX - this.getBoundingClientRect().left;
  784. var left = Math.min(this.clientWidth - w, Math.max(0, offsetX - w)) |0;
  785. if (!style.left || parseInt(style.left) !== left) {
  786. style.left = left + 'px';
  787. if (spinner.parentElement)
  788. spinner.style.cssText = important('left:' + (left + w/2 - 10) + 'px; right:auto;');
  789. }
  790.  
  791. var index = Math.min(offsetX / this.clientWidth * (len+1) |0, len - 1);
  792. if (index === oldIndex)
  793. return;
  794.  
  795. var part = index/partlen|0;
  796. if (!oldIndex || part !== (oldIndex/partlen|0)) {
  797. style.cssText = style.cssText.replace(/$|background-image[^;]+;/,
  798. 'background-image: url(' + setStoryboardUrl(part) + ')!important;');
  799. if (!div.FYTE.storyboard.preloaded) {
  800. if (spinner.timer)
  801. clearTimeout(spinner.timer);
  802. spinner.timer = setTimeout(function() {
  803. spinner.timer = 0;
  804. if (!imageLoader2.src)
  805. return;
  806. this.appendChild(spinner);
  807. spinner.style.cssText = important('left:' + (left + w/2 - 10) + 'px; right:auto;');
  808. }.bind(this), 50);
  809. imageLoader2.onload = function() {
  810. clearTimeout(spinner.timer);
  811. spinner.remove();
  812. spinner.timer = 0;
  813. imageLoader2.onload = null;
  814. imageLoader2.src = '';
  815. };
  816. imageLoader2.src = setStoryboardUrl(part);
  817. }
  818. }
  819.  
  820. oldIndex = index;
  821. index = index % partlen;
  822. style.backgroundPosition = '-' + (index % cols) * w + 'px -' + (index / cols |0) * h + 'px';
  823. }
  824. }
  825.  
  826. function clickHandler(e) {
  827. if (e.target.closest('a')
  828. || e.type === 'mousedown' && e.button !== 1
  829. || e.type === 'click' && e.target.matches('.instant-youtube-options, .instant-youtube-options *'))
  830. return;
  831. if (e.type === 'click' && e.target.matches('.instant-youtube-options-button')) {
  832. showOptions(e);
  833. e.preventDefault();
  834. e.stopPropagation();
  835. return;
  836. }
  837.  
  838. e.preventDefault();
  839. e.stopPropagation();
  840. e.stopImmediatePropagation();
  841.  
  842. startPlaying(e.target.closest('.instant-youtube-container'), {
  843. alternateMode: e.shiftKey || e.target.matches('.instant-youtube-alternative'),
  844. fullscreen: e.button === 1,
  845. });
  846. }
  847.  
  848. function startPlaying(div, params) {
  849. div.removeEventListener('click', clickHandler);
  850. div.removeEventListener('mousedown', clickHandler);
  851.  
  852. $$remove(div, '.instant-youtube-alternative, .instant-youtube-storyboard, .instant-youtube-options-button, .instant-youtube-options');
  853. $(div, 'svg').outerHTML = '<span class="instant-youtube-loading-spinner"></span>';
  854.  
  855. if (pinnable !== 'off') {
  856. makePinnable(div);
  857. if (params && params.pin)
  858. $(div, '[pin="' + params.pin + '"]').click();
  859. }
  860.  
  861. if (window !== window.top)
  862. window.parent.postMessage('iframe-allowfs', '*');
  863.  
  864. if ((!!playDirectly + !!(params && params.alternateMode) === 1)
  865. && (div.FYTE.videoSources || div.FYTE.state === 'querying')) {
  866. if (div.FYTE.videoSources)
  867. startPlayingDirectly(div, params);
  868. else {
  869. // playback will start in parseVideoInfo
  870. div.FYTE.state = 'scheduled play';
  871. // fallback to iframe in 5s
  872. setTimeout(function() {
  873. if (div.FYTE.state) {
  874. div.FYTE.state = '';
  875. switchToIFrame.call(div, params);
  876. }
  877. }, 5000);
  878. }
  879. }
  880. else
  881. switchToIFrame.call(div, params);
  882. }
  883.  
  884. function startPlayingDirectly(div, params) {
  885. var video = document.createElement('video');
  886. video.controls = true;
  887. video.autoplay = true;
  888. video.style.cssText = important('position:absolute; left:0; top:0; right:0; bottom:0; padding:0; margin:auto; opacity:0; width:100%; height:100%;');
  889. video.className = 'instant-youtube-embed';
  890. video.volume = GM_getValue('volume', 0.5);
  891.  
  892. (div.FYTE.videoSources || []).forEach(function(src) {
  893. var srcdom = video.appendChild(document.createElement('source'));
  894. Object.keys(src).forEach(function(k) { srcdom[k] = src[k] });
  895. srcdom.onerror = switchToIFrame.bind(div, params);
  896. });
  897.  
  898. overrideCSS($(div, 'img'), {transition: 'opacity 1s', opacity: '0'});
  899.  
  900. if (params && params.fullscreen) {
  901. div.firstElementChild.appendChild(video);
  902. div.setAttribute('playing', '');
  903. video.style.opacity = 1;
  904. goFullscreen(video);
  905. }
  906.  
  907. if (window.chrome && Number(navigator.userAgent.match(/Chrom\D+(\d+)|$/)[1]) < 74) {
  908. video.addEventListener('click', function onClick(debounced) {
  909. if (debounced === true)
  910. video.paused ? video.play() : video.pause();
  911. else
  912. setTimeout(onClick, 0, true);
  913. });
  914. }
  915.  
  916. var title = $(div, '.instant-youtube-title');
  917. if (title) {
  918. video.onpause = function() { title.removeAttribute('hidden') };
  919. video.onplay = function() { title.setAttribute('hidden', true) };
  920. }
  921.  
  922. var switchTimer = setTimeout(switchToIFrame.bind(div, params), 5000);
  923.  
  924. video.onloadedmetadata = div.FYTE.startAt && function() {
  925. clearTimeout(switchTimer);
  926. video.currentTime = div.FYTE.startAt;
  927. };
  928.  
  929. video.onloadeddata = function() {
  930. clearTimeout(switchTimer);
  931. pauseOtherVideos(video);
  932. video.interval = setInterval(function() {
  933. if (video.volume !== GM_getValue('volume', 0.5))
  934. GM_setValue('volume', video.volume);
  935. }, 1000);
  936. if (params && params.fullscreen)
  937. return;
  938. div.setAttribute('playing', '');
  939. div.firstElementChild.appendChild(video);
  940. video.style.opacity = 1;
  941. };
  942. }
  943.  
  944. function switchToIFrame(params, e) {
  945. if (this.querySelector('iframe'))
  946. return;
  947. var div = this;
  948. var wrapper = div.firstElementChild;
  949. var fullscreen = params && params.fullscreen && !e;
  950. if (e instanceof Event) {
  951. console.log('[FYTE] Direct linking canceled on %s, switching to IFRAME player', div.FYTE.srcEmbed);
  952. var video = e.target ? e.target.closest('video') : e.path && e.path[e.path.length-1];
  953. while (video.lastElementChild)
  954. video.lastElementChild.remove();
  955. goFullscreen(video, false);
  956. video.remove();
  957. }
  958.  
  959. var url = setUrlParams(div.FYTE.srcEmbedFixed, {
  960. html5: 1,
  961. autoplay: 1,
  962. autohide: 2,
  963. border: 0,
  964. controls: 1,
  965. fs: 1,
  966. showinfo: 1,
  967. ssl: 1,
  968. theme: 'dark',
  969. enablejsapi: 1,
  970. local: 'true',
  971. quality: 'medium',
  972. FYTEfullscreen: fullscreen|0,
  973. });
  974.  
  975. var iframe = document.createElement('iframe');
  976. iframe.src = url;
  977. iframe.className = 'instant-youtube-embed';
  978. iframe.style = important('position:absolute; left:0; top:0; right:0; padding:0; margin:auto; opacity:0;');
  979. iframe.frameBorder = 0;
  980. iframe.allow = 'autoplay; fullscreen';
  981. iframe.setAttribute('allowtransparency', 'true');
  982. iframe.setAttribute('allowfullscreen', 'true');
  983. iframe.setAttribute('width', '100%');
  984. iframe.setAttribute('height', '100%');
  985.  
  986. if (pinnable !== 'off') {
  987. var pin = $(div, '[pin]');
  988. pin.parentNode.insertBefore(iframe, pin);
  989. } else {
  990. wrapper.appendChild(iframe);
  991. }
  992.  
  993. div.setAttribute('iframe', '');
  994. div.setAttribute('playing', '');
  995.  
  996. iframe = $(div, 'iframe');
  997. if (fullscreen) {
  998. goFullscreen(iframe);
  999. iframe.style.opacity = 1;
  1000. }
  1001.  
  1002. iframe.onload = function() {
  1003. window.addEventListener('message', YTlistener);
  1004. iframe.contentWindow.postMessage('{"event":"listening"}', '*');
  1005. if (useInvidious) {
  1006. iframe.style.opacity = 1;
  1007. $(div, '.instant-youtube-title').hidden = true;
  1008. }
  1009. };
  1010. setTimeout(function() {
  1011. iframe.style.opacity = 1;
  1012. window.removeEventListener('message', YTlistener);
  1013. }, 5000);
  1014.  
  1015. function YTlistener(e) {
  1016. if (e.source !== iframe.contentWindow || !e.data)
  1017. return;
  1018. var data = tryJSONparse(e.data);
  1019. if (!data.info || data.info.playerState !== 1)
  1020. return;
  1021. window.removeEventListener('message', YTlistener);
  1022. pauseOtherVideos(iframe);
  1023. iframe.style.opacity = 1;
  1024. $$remove(div, 'span, a');
  1025. $(div, 'img').style.display = 'none';
  1026. }
  1027. }
  1028.  
  1029. function setUrl(url) {
  1030. if (useInvidious) {
  1031. var u = new URL(url);
  1032. u.hostname = 'invidio.us';
  1033. url = u.href.replace('/vi_webp/', '/vi/').replace('.webp', '.jpg');
  1034. }
  1035. return url;
  1036. }
  1037.  
  1038. function setUrlParams(url, params) {
  1039. var names = Object.keys(params);
  1040. var parts = url.split('?', 2)
  1041. var query = (parts[1] || '').replace(new RegExp('(?:[?&]|^)(?:' + names.join('|') + ')(?:=[^?&]*)?', 'gi'), '');
  1042. return parts[0] + '?' + query + (query ? '&' : '?') +
  1043. names.map(function(n) { return n + '=' + params[n] }).join('&');
  1044. }
  1045.  
  1046. function pauseOtherVideos(activePlayer) {
  1047. $$(activePlayer.ownerDocument, '.instant-youtube-embed').forEach(function(v) {
  1048. if (v === activePlayer)
  1049. return;
  1050. switch (v.localName) {
  1051. case 'video':
  1052. if (!v.paused)
  1053. v.pause();
  1054. break;
  1055. case 'iframe':
  1056. try { v.contentWindow.postMessage('{"event":"command", "func":"pauseVideo", "args":""}', '*') } catch(e) {}
  1057. break;
  1058. }
  1059. });
  1060. }
  1061.  
  1062. function goFullscreen(el, enable) {
  1063. if (enable !== false)
  1064. el.webkitRequestFullScreen && el.webkitRequestFullScreen()
  1065. || el.mozRequestFullScreen && el.mozRequestFullScreen()
  1066. || el.requestFullScreen && el.requestFullScreen();
  1067. else
  1068. document.webkitCancelFullScreen && document.webkitCancelFullScreen()
  1069. || document.mozCancelFullScreen && document.mozCancelFullScreen()
  1070. || document.cancelFullScreen && document.cancelFullScreen();
  1071. }
  1072.  
  1073. function makePinnable(div) {
  1074. div.firstElementChild.insertAdjacentHTML('beforeend',
  1075. '<div size-gripper></div>' +
  1076. '<div pin="top-left"></div><div pin="top-right"></div><div pin="bottom-right"></div><div pin="bottom-left"></div>');
  1077.  
  1078. $$(div, '[pin]').forEach(function(pin) {
  1079. if (pinnable === 'hide')
  1080. pin.setAttribute('transparent', '');
  1081. pin.onclick = pinClicked;
  1082. });
  1083. $(div, '[size-gripper]').addEventListener('mousedown', startResize, true);
  1084. $(div, '[size-gripper]').addEventListener('mousedown', function() { return false });
  1085.  
  1086. function pinClicked() {
  1087. var pin = this;
  1088. var pinIt = !div.hasAttribute('pinned') || !pin.hasAttribute('active');
  1089. var corner = pin.getAttribute('pin');
  1090. var video = $(div, 'video');
  1091. if (pinIt) {
  1092. $$(div, '[pin][active]').forEach(function(p) { p.removeAttribute('active') });
  1093. pin.setAttribute('active', '');
  1094. if (!div.FYTE.unpinnedStyle) {
  1095. div.FYTE.unpinnedStyle = div.style.cssText;
  1096. var stub = div.cloneNode();
  1097. var img = $(div, 'img').cloneNode();
  1098. img.style.opacity = 1;
  1099. img.style.display = 'block';
  1100. img.title = '';
  1101. stub.appendChild(img);
  1102. stub.onclick = function(e) { $(div, '[pin][active]').onclick(e) };
  1103. stub.style.cssText += 'opacity:0.3!important;';
  1104. stub.setAttribute('stub', '');
  1105. div.FYTE.stub = stub;
  1106. div.parentNode.insertBefore(stub, div);
  1107. }
  1108. var size = constrainPinnedSize(div, localStorage['width#' + location.hostname] || pinnedWidth);
  1109. div.style.cssText = important(
  1110. 'position: fixed;' +
  1111. 'width: ' + size.w + 'px;' +
  1112. 'z-index: 999999999;' +
  1113. 'height:' + size.h + 'px;' +
  1114. 'top:' + (corner.indexOf('top') >= 0 ? '0' : 'auto') + ';' +
  1115. 'bottom:' + (corner.indexOf('bottom') >= 0 ? '0' : 'auto') + ';' +
  1116. 'left:' + (corner.indexOf('left') >= 0 ? '0' : 'auto') + ';' +
  1117. 'right:' + (corner.indexOf('right') >= 0 ? '0' : 'auto') + ';'
  1118. );
  1119. adjustPinnedOffset(div, div, corner);
  1120. div.setAttribute('pinned', corner);
  1121. if (video && document.body)
  1122. document.body.appendChild(div);
  1123. }
  1124. else { // unpin
  1125. pin.removeAttribute('active');
  1126. div.removeAttribute('pinned');
  1127. div.style.cssText = div.FYTE.unpinnedStyle;
  1128. div.FYTE.unpinnedStyle = '';
  1129. if (div.FYTE.stub) {
  1130. if (video && document.body)
  1131. div.FYTE.stub.parentNode.replaceChild(div, div.FYTE.stub);
  1132. div.FYTE.stub.remove();
  1133. div.FYTE.stub = null;
  1134. }
  1135. }
  1136. if (video && video.paused)
  1137. video.play();
  1138. }
  1139.  
  1140. function startResize(e) {
  1141. var siteSaved = ('width#' + location.hostname) in localStorage;
  1142. var saveAs = siteSaved ? 'site' : 'global';
  1143. var oldSizeCSS = {w: div.style.width, h: div.style.height};
  1144. var oldDraggable = div.draggable;
  1145. div.draggable = false;
  1146.  
  1147. var gripper = this;
  1148. gripper.removeAttribute('tried-exceeding');
  1149. gripper.innerHTML = '<div>' +
  1150. '<div save-as="' + saveAs + '"><b>S</b> = Site mode: <span>' + getSiteOnlyText() + '</span></div>' +
  1151. (!siteSaved ? '' : '<div><b>R</b> = Reset to global size</div>') +
  1152. '<div><b>Esc</b> = Cancel</div>' +
  1153. '</div>';
  1154.  
  1155. document.addEventListener('mousemove', resize);
  1156. document.addEventListener('mouseup', resizeDone);
  1157. document.addEventListener('keydown', resizeKeyDown);
  1158. e.stopImmediatePropagation();
  1159. return false;
  1160.  
  1161. function getSiteOnlyText() {
  1162. return saveAs === 'site' ? 'only ' + location.hostname : 'global';
  1163. }
  1164.  
  1165. function resize(e) {
  1166. var deltaX = e.movementX || e.webkitMovementX || e.mozMovementX || 0;
  1167. if (/right/.test(div.getAttribute('pinned')))
  1168. deltaX = -deltaX;
  1169. var newSize = constrainPinnedSize(div, div.clientWidth + deltaX);
  1170. if (newSize.w !== div.clientWidth) {
  1171. div.style.setProperty('width', newSize.w + 'px', 'important');
  1172. div.style.setProperty('height', newSize.h + 'px', 'important');
  1173. gripper.removeAttribute('tried-exceeding');
  1174. } else if (newSize.triedExceeding) {
  1175. gripper.setAttribute('tried-exceeding', '');
  1176. }
  1177. window.getSelection().removeAllRanges();
  1178. return false;
  1179. }
  1180.  
  1181. function resizeDone() {
  1182. div.draggable = oldDraggable;
  1183. gripper.removeAttribute('tried-exceeding');
  1184. gripper.innerHTML = '';
  1185. document.removeEventListener('mousemove', resize);
  1186. document.removeEventListener('mouseup', resizeDone);
  1187. document.removeEventListener('keydown', resizeKeyDown);
  1188. switch (saveAs) {
  1189. case 'site':
  1190. localStorage['width#' + location.hostname] = div.clientWidth;
  1191. break;
  1192. case 'global':
  1193. pinnedWidth = div.clientWidth;
  1194. GM_setValue('pinnedWidth', pinnedWidth);
  1195. // fallthrough to remove the locally saved value
  1196. case 'reset':
  1197. localStorage.removeItem('width#' + location.hostname);
  1198. break;
  1199. case '':
  1200. return false;
  1201. }
  1202. gripper.setAttribute('saveAs', saveAs);
  1203. setTimeout(function() { gripper.removeAttribute('saveAs'); }, 250);
  1204. return false;
  1205. }
  1206.  
  1207. function resizeKeyDown(e) {
  1208. switch (e.keyCode) {
  1209. case 27: // Esc
  1210. saveAs = 'cancel';
  1211. div.style.width = oldSizeCSS.w;
  1212. div.style.height = oldSizeCSS.h;
  1213. break;
  1214. case 83: // S
  1215. saveAs = saveAs === 'site' ? 'global' : 'site';
  1216. $(gripper, '[save-as]').setAttribute('save-as', saveAs);
  1217. $(gripper, '[save-as] span').textContent = getSiteOnlyText();
  1218. return false;
  1219. case 82: // R
  1220. if (!siteSaved)
  1221. return;
  1222. saveAs = 'reset';
  1223. var size = constrainPinnedSize(div, pinnedWidth);
  1224. div.style.width = size.w;
  1225. div.style.height = size.h;
  1226. break;
  1227. default:
  1228. return;
  1229. }
  1230. document.dispatchEvent(new MouseEvent('mouseup'));
  1231. return false;
  1232. }
  1233. }
  1234. }
  1235.  
  1236. function makeDraggable(div) {
  1237. div.draggable = true;
  1238. div.addEventListener('dragstart', function(e) {
  1239. var offsetY = e.offsetY || e.clientY - div.getBoundingClientRect().top;
  1240. if (offsetY > div.clientHeight - 30)
  1241. return e.preventDefault();
  1242.  
  1243. e.dataTransfer.setData('text/plain', '');
  1244.  
  1245. var dropZone = document.createElement('div');
  1246. var dropZoneHeight = 400 / div.FYTE.cache.videoWidth * div.FYTE.cache.videoHeight;
  1247. dropZone.className = 'instant-youtube-dragndrop-placeholder';
  1248.  
  1249. document.body.addEventListener('dragenter', dragHandler);
  1250. document.body.addEventListener('dragover', dragHandler);
  1251. document.body.addEventListener('dragend', dragHandler);
  1252. document.body.addEventListener('drop', dragHandler);
  1253. function dragHandler(e) {
  1254. e.stopImmediatePropagation();
  1255. e.stopPropagation();
  1256. e.preventDefault();
  1257. switch (e.type) {
  1258. case 'dragover':
  1259. var playing = div.hasAttribute('playing');
  1260. var stub = e.target.closest('.instant-youtube-container[stub]') === div.FYTE.stub && div.FYTE.stub;
  1261. var gizmo = playing && !stub
  1262. ? {left:0, top:0, right:innerWidth, bottom:innerHeight}
  1263. : (stub || div).getBoundingClientRect();
  1264. var x = e.clientX, y = e.clientY;
  1265. var cx = (gizmo.left + gizmo.right) / 2;
  1266. var cy = (gizmo.top + gizmo.bottom) / 2;
  1267. var stay = !!stub || y >= cy-200 && y <= cy+200 && x >= cx-200 && x <= cx+200;
  1268. overrideCSS(dropZone, {
  1269. top: y < cy || stay ? '0' : 'auto',
  1270. bottom: y > cy || stay ? '0' : 'auto',
  1271. left: x < cx || stay ? '0' : 'auto',
  1272. right: x > cx || stay ? '0' : 'auto',
  1273. width: playing && stay && stub ? stub.clientWidth+'px' : '400px',
  1274. height: playing && stay && stub ? stub.clientHeight+'px' : dropZoneHeight + 'px',
  1275. margin: playing && stay ? 'auto' : '0',
  1276. position: !playing && stay || stub ? 'absolute' : 'fixed',
  1277. 'background-color': stub ? 'rgba(0,0,255,0.5)' : stay ? 'rgba(255,255,0,0.4)' : 'rgba(0,255,0,0.2)',
  1278. });
  1279. adjustPinnedOffset(dropZone, div);
  1280. (stay && !playing || stub ? (stub || div) : document.body).appendChild(dropZone);
  1281. break;
  1282. case 'dragend':
  1283. case 'drop':
  1284. var corner = calcPinnedCorner(dropZone);
  1285. dropZone.remove();
  1286. dropZone = null;
  1287. document.body.removeEventListener('dragenter', dragHandler);
  1288. document.body.removeEventListener('dragover', dragHandler);
  1289. document.body.removeEventListener('dragend', dragHandler);
  1290. document.body.removeEventListener('drop', dragHandler);
  1291. if (e.type === 'dragend')
  1292. break;
  1293. if (div.hasAttribute('playing'))
  1294. (corner ? $(div, '[pin="' + corner + '"]') : div.FYTE.stub).click();
  1295. else
  1296. startPlaying(div, {pin: corner});
  1297. }
  1298. }
  1299. });
  1300. }
  1301.  
  1302. function adjustPinnedOffset(el, self, corner) {
  1303. var offset = 0;
  1304. $$('.instant-youtube-container[pinned] [pin="' + (corner || calcPinnedCorner(el)) + '"][active]').forEach(function(pin) {
  1305. var container = pin.closest('[pinned]');
  1306. if (container !== el && container !== self) {
  1307. var bounds = container.getBoundingClientRect();
  1308. offset = Math.max(offset, el.style.top === '0px' ? bounds.bottom : innerHeight - bounds.top);
  1309. }
  1310. });
  1311. if (offset)
  1312. el.style[el.style.top === '0px' ? 'top' : 'bottom'] = offset + 'px';
  1313. }
  1314.  
  1315. function calcPinnedCorner(el) {
  1316. var t = el.style.top !== 'auto';
  1317. var b = el.style.bottom !== 'auto';
  1318. var l = el.style.left !== 'auto';
  1319. var r = el.style.right !== 'auto';
  1320. return t && b && l && r ? '' : (t ? 'top' : 'bottom') + '-' + (l ? 'left' : 'right');
  1321. }
  1322.  
  1323. function constrainPinnedSize(div, width) {
  1324. var maxWidth = window.innerWidth - 100 |0;
  1325. var triedExceeding = (width|0) > maxWidth;
  1326. width = Math.max(200, Math.min(maxWidth, width|0));
  1327. return {
  1328. w: width,
  1329. h: width / div.FYTE.cache.videoWidth * div.FYTE.cache.videoHeight,
  1330. triedExceeding: triedExceeding,
  1331. };
  1332. }
  1333.  
  1334. function showOptions(e) {
  1335. var optionsButton = e.target;
  1336. translateHTML(optionsButton, 'afterend', '\
  1337. <div class="instant-youtube-options">\
  1338. <span>\
  1339. <label tl style="width: 100% !important;">Size:&nbsp;\
  1340. <select data-action="size-mode">\
  1341. <option tl value="Original">Original\
  1342. <option tl value="Fit to width">Fit to width\
  1343. <option>360p\
  1344. <option>480p\
  1345. <option>720p\
  1346. <option>1080p\
  1347. <option tl value="Custom">Custom...\
  1348. </select>\
  1349. </label>&nbsp;\
  1350. <label data-action="size-custom" ' + (resizeMode !== 'Custom' ? 'disabled' : '') + '>\
  1351. <input type="number" min="320" max="9999" tl-placeholder="width" data-action="width" step="1" value="' + (resizeWidth||'') + '">\
  1352. x\
  1353. <input type="number" min="240" max="9999" tl-placeholder="height" data-action="height" step="1" value="' + (resizeHeight||'') + '">\
  1354. </label>\
  1355. </span>\
  1356. <label tl="content,title" title="msgStoryboardTip">\
  1357. <input data-action="storyboard" type="checkbox" ' + (showStoryboard ? 'checked' : '') + '>\
  1358. msgStoryboard\
  1359. </label>\
  1360. <span>\
  1361. <label tl="content,title" title="msgDirectTip">\
  1362. <input data-action="direct" type="checkbox" ' + (playDirectly ? 'checked' : '') + '>\
  1363. msgDirect\
  1364. </label>\
  1365. &nbsp;\
  1366. <label tl="content,title" title="msgDirectTip">\
  1367. <input data-action="direct-shown" type="checkbox" ' + (playDirectlyShown ? 'checked' : '') + '>\
  1368. msgDirectShown\
  1369. </label>\
  1370. </span>\
  1371. <label tl="content">\
  1372. <input data-action="invidious" type="checkbox" ' + (useInvidious ? 'checked' : '') + '>\
  1373. msgInvidious\
  1374. </label>\
  1375. <label tl="content,title" title="msgSafeTip">\
  1376. <input data-action="safe" type="checkbox" ' + (skipCustom ? 'checked' : '') + '>\
  1377. msgSafe\
  1378. </label>\
  1379. <table>\
  1380. <tr>\
  1381. <td><label tl="content,title" title="msgPinningTip">msgPinning</label></td>\
  1382. <td>\
  1383. <select data-action="pinnable">\
  1384. <option tl value="on">msgPinningOn\
  1385. <option tl value="hide">msgPinningHover\
  1386. <option tl value="off">msgPinningOff\
  1387. </select>\
  1388. </td>\
  1389. </tr>\
  1390. </table>\
  1391. <span data-action="buttons">\
  1392. <button tl data-action="ok">OK</button>\
  1393. <button tl data-action="cancel">Cancel</button>\
  1394. </span>\
  1395. </div>\
  1396. ');
  1397. var options = optionsButton.nextElementSibling;
  1398.  
  1399. options.addEventListener('keydown', function(e) {
  1400. if (e.target.localName === 'input' &&
  1401. !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey && e.key.match(/[.,]/))
  1402. return false;
  1403. });
  1404.  
  1405. $(options, '[data-action="size-mode"]').value = resizeMode;
  1406. $(options, '[data-action="size-mode"]').addEventListener('change', function() {
  1407. var v = this.value !== 'Custom';
  1408. var e = $(options, '[data-action="size-custom"]');
  1409. e.children[0].disabled = e.children[1].disabled = v;
  1410. v ? e.setAttribute('disabled', '') : e.removeAttribute('disabled');
  1411. });
  1412.  
  1413. $(options, '[data-action="pinnable"]').value = pinnable;
  1414.  
  1415. $(options, '[data-action="buttons"]').addEventListener('click', function(e) {
  1416. if (e.target.dataset.action !== 'ok') {
  1417. options.remove();
  1418. return;
  1419. }
  1420. var v, shouldAdjust;
  1421. if (resizeMode !== (v = $(options, '[data-action="size-mode"]').value)) {
  1422. GM_setValue('resize', resizeMode = v);
  1423. shouldAdjust = true;
  1424. }
  1425. if (resizeMode === 'Custom') {
  1426. var w = $(options, '[data-action="width"]').value |0;
  1427. var h = $(options, '[data-action="height"]').value |0;
  1428. if (resizeWidth !== w || resizeHeight !== h) {
  1429. updateCustomSize(w, h);
  1430. GM_setValue('width', resizeWidth);
  1431. GM_setValue('height', resizeHeight);
  1432. shouldAdjust = true;
  1433. }
  1434. }
  1435. if (showStoryboard !== (v = $(options, '[data-action="storyboard"]').checked)) {
  1436. GM_setValue('showStoryboard', showStoryboard = v);
  1437. $$('.instant-youtube-container').forEach(updateHoverHandler);
  1438. }
  1439. if (playDirectly !== (v = $(options, '[data-action="direct"]').checked)) {
  1440. GM_setValue('playHTML5', playDirectly = v);
  1441. if (playDirectlyShown) {
  1442. var newAltText = _(playDirectly ? 'Play with Youtube player' : 'Play directly (up to 720p)');
  1443. $$('.instant-youtube-alternative').forEach(function(e) {
  1444. e.textContent = newAltText;
  1445. });
  1446. }
  1447. }
  1448. if (playDirectlyShown !== (v = $(options, '[data-action="direct-shown"]').checked)) {
  1449. GM_setValue('playHTML5', playDirectlyShown = v);
  1450. updateAltPlayerCSS();
  1451. }
  1452. if (skipCustom !== (v = $(options, '[data-action="safe"]').checked)) {
  1453. GM_setValue('skipCustom', skipCustom = v);
  1454. }
  1455. if (pinnable !== (v = $(options, '[data-action="pinnable"]').value)) {
  1456. GM_setValue('pinnable', pinnable = v);
  1457. }
  1458. if (useInvidious !== (v = $(options, '[data-action="invidious"]').checked)) {
  1459. GM_setValue('invidious', useInvidious = v);
  1460. }
  1461. options.remove();
  1462.  
  1463. if (shouldAdjust)
  1464. adjustNodes(e, e.target.closest('.instant-youtube-container'));
  1465. });
  1466. }
  1467.  
  1468. function updateCustomSize(w, h) {
  1469. resizeWidth = Math.min(9999, Math.max(320, w|0 || resizeWidth|0));
  1470. resizeHeight = Math.min(9999, Math.max(240, h|0 || resizeHeight|0));
  1471. }
  1472.  
  1473. function updateAltPlayerCSS() {
  1474. var s = '.instant-youtube-alternative { display:' + (playDirectlyShown ? 'block' : 'none') + '!important}';
  1475. $('style#instant-youtube-styles').textContent += s;
  1476. return s;
  1477. }
  1478.  
  1479. function important(cssText) {
  1480. return cssText.replace(/;/g, '!important;');
  1481. }
  1482.  
  1483. function tryCatch(func) {
  1484. try {
  1485. return func();
  1486. } catch(e) {
  1487. console.log(e);
  1488. }
  1489. }
  1490.  
  1491. function getFunctionComment(fn) {
  1492. return fn.toString().match(/\/\*([\s\S]*?)\*\/\s*}$/)[1];
  1493. }
  1494.  
  1495. function $(selORnode, sel) {
  1496. return sel ? selORnode.querySelector(sel)
  1497. : document.querySelector(selORnode);
  1498. }
  1499.  
  1500. function $$(selORnode, sel) {
  1501. return Array.prototype.slice.call(
  1502. sel ? selORnode.querySelectorAll(sel)
  1503. : document.querySelectorAll(selORnode));
  1504. }
  1505.  
  1506. function $$remove(selORnode, sel) {
  1507. Array.prototype.forEach.call(
  1508. sel ? selORnode.querySelectorAll(sel)
  1509. : document.querySelectorAll(selORnode),
  1510. function(e) { e.remove() }
  1511. );
  1512. }
  1513.  
  1514. function overrideCSS(e, params) {
  1515. var names = Object.keys(params);
  1516. var style = e.style.cssText.replace(new RegExp('(^|\s|;)(' + names.join('|') + ')(:[^;]+)', 'gi'), '$1');
  1517. e.style.cssText = style.replace(/[^;]\s*$/, '$&;').replace(/^\s*;\s*/, '') +
  1518. names.map(function(n) { return n + ':' + params[n] + '!important' }).join(';') + ';';
  1519. }
  1520.  
  1521. // fix dumb Firefox bug
  1522. function floatPadding(node, style, dir) {
  1523. var padding = style['padding' + dir];
  1524. if (padding.indexOf('%') < 0)
  1525. return parseFloat(padding);
  1526. return parseFloat(padding) * (parseFloat(style.width) || node.clientWidth) / 100;
  1527. }
  1528.  
  1529. function cleanupCache() {
  1530. for (var k in localStorage) {
  1531. if (k.lastIndexOf('FYTE-cache-', 0) === 0
  1532. && Date.now() - Number((tryJSONparse(localStorage[k]) || {}).lastUsed || 0) > CACHE_STALE_DURATION) {
  1533. delete localStorage[k];
  1534. }
  1535. }
  1536. GM_listValues().forEach(function(k) {
  1537. if (k.lastIndexOf('cache-', 0) === 0) {
  1538. GM_deleteValue(k);
  1539. }
  1540. });
  1541. }
  1542.  
  1543. function tryJSONparse(s) {
  1544. try { return JSON.parse(s) }
  1545. catch (e) {}
  1546. }
  1547.  
  1548. function translateHTML(baseElement, place, html) {
  1549. var tmp = document.createElement('div');
  1550. tmp.innerHTML = html;
  1551. $$(tmp, '[tl]').forEach(function(node) {
  1552. (node.getAttribute('tl') || 'content').split(',').forEach(function(what) {
  1553. var child, src, tl;
  1554. if (what === 'content') {
  1555. for (var i = node.childNodes.length-1, n; (i>=0) && (n=node.childNodes[i]); i--) {
  1556. if (n.nodeType === Node.TEXT_NODE && n.textContent.trim()) {
  1557. child = n;
  1558. break;
  1559. }
  1560. }
  1561. } else
  1562. child = node.getAttributeNode(what);
  1563. if (!child)
  1564. return;
  1565. src = child.textContent;
  1566. var srcTrimmed = src.trim();
  1567. tl = src.replace(srcTrimmed, _(srcTrimmed));
  1568. if (src !== tl)
  1569. child.textContent = tl;
  1570. });
  1571. });
  1572. baseElement.insertAdjacentHTML(place, tmp.innerHTML);
  1573. }
  1574.  
  1575. function initTL() {
  1576. var tlSource = {
  1577. 'watch on Youtube': {
  1578. 'ru': 'открыть на Youtube',
  1579. },
  1580. 'Play with Youtube player': {
  1581. 'ru': 'Включить плеер Youtube',
  1582. },
  1583. 'Play directly (up to 720p)': {
  1584. 'ru': 'Включить напрямую (макс. 720p)',
  1585. },
  1586. msgAltPlayerHint: {
  1587. 'en': 'Shift-click to use alternative player',
  1588. 'ru': 'Shift-клик для смены типа плеера',
  1589. },
  1590. 'Options': {
  1591. 'ru': 'Опции',
  1592. },
  1593. 'Size:': {
  1594. 'ru': 'Размер:',
  1595. },
  1596. 'Original': {
  1597. 'ru': 'Исходный',
  1598. },
  1599. 'Fit to width': {
  1600. 'ru': 'На всю ширину',
  1601. },
  1602. 'Custom...': {
  1603. 'ru': 'Настроить...',
  1604. },
  1605. 'width': {
  1606. 'ru': 'ширина',
  1607. },
  1608. 'height': {
  1609. 'ru': 'высота',
  1610. },
  1611. msgStoryboard: {
  1612. 'en': 'Storyboard thumbnails on hover',
  1613. 'ru': 'Раскадровка при наведении курсора',
  1614. },
  1615. msgStoryboardTip: {
  1616. 'en': 'Show storyboard preview on mouse hover at the bottom',
  1617. 'ru': 'Показывать миникадры при наведении мыши на низ кавер-картинки',
  1618. },
  1619. msgDirect: {
  1620. 'en': 'Play directly',
  1621. 'ru': 'Встроенный плеер браузера',
  1622. },
  1623. msgDirectTip: {
  1624. 'en': 'Note: Shift-click a thumbnail to use alternative player',
  1625. 'ru': 'Напоминание: удерживайте клавишу Shift при щелчке на картинке для альтернативного плеера',
  1626. },
  1627. msgDirectShown: {
  1628. 'en': 'Show under play button',
  1629. 'ru': 'Показывать под кнопкой ►',
  1630. },
  1631. msgInvidious: {
  1632. 'en': 'Use https://invidio.us to play videos',
  1633. 'ru': 'Использовать https://invidio.us в плеере',
  1634. },
  1635. msgSafe: {
  1636. 'en': 'Safe (skip videos with enablejsapi=1)',
  1637. 'ru': 'Консервативный режим',
  1638. },
  1639. msgSafeTip: {
  1640. 'en': 'Do not process customized videos with enablejsapi=1 parameter (requires page reload)',
  1641. 'ru': 'Не обрабатывать нестандартные видео с параметром enablejsapi=1 (подействует после обновления страницы)',
  1642. },
  1643. msgPinning: {
  1644. 'en': 'Corner pinning',
  1645. 'ru': 'Закрепление по углам',
  1646. },
  1647. msgPinningTip: {
  1648. 'en': 'Enable corner pinning controls when a video is playing. \nTo restore the video click the active corner pin or the original video placeholder.',
  1649. 'ru': 'Включить шпильки по углам для закрепления видео во время просмотра. \nДля отмены можно нажать еще раз на активированный уголЪ или на заглушку, где исходно было видео',
  1650. },
  1651. msgPinningOn: {
  1652. 'en': 'On',
  1653. 'ru': 'Да',
  1654. },
  1655. msgPinningHover: {
  1656. 'en': 'On, hover a corner to show',
  1657. 'ru': 'Да, при наведении курсора',
  1658. },
  1659. msgPinningOff: {
  1660. 'en': 'Off',
  1661. 'ru': 'Нет',
  1662. },
  1663. 'OK': {
  1664. 'ru': 'ОК',
  1665. },
  1666. 'Cancel': {
  1667. 'ru': 'Оменить',
  1668. },
  1669. };
  1670. var browserLang = navigator.language || navigator.languages && navigator.languages[0] || '';
  1671. var browserLangMajor = browserLang.replace(/-.+/, '');
  1672. var tl = {};
  1673. Object.keys(tlSource).forEach(function(k) {
  1674. var langs = tlSource[k];
  1675. var text = langs[browserLang] || langs[browserLangMajor];
  1676. if (text)
  1677. tl[k] = text;
  1678. });
  1679. return function(src) { return tl[src] || src };
  1680. }
  1681.  
  1682. function injectStylesIfNeeded(force) {
  1683. if (!fytedom[0] && !force)
  1684. return;
  1685. var styledom = $('style#instant-youtube-styles');
  1686. if (styledom) {
  1687. // move our rules to the end of HEAD to increase CSS specificity
  1688. if (styledom.nextElementSibling && document.head)
  1689. document.head.insertBefore(styledom, null);
  1690. return;
  1691. }
  1692. styledom = (document.head || document.documentElement).appendChild(document.createElement('style'));
  1693. styledom.id = 'instant-youtube-styles';
  1694. styledom.textContent = important(getFunctionComment(function() { /*
  1695. .instant-youtube-container,
  1696. .instant-youtube-wrapper * {
  1697. transform: translate3D(0,0,0);
  1698. }
  1699. .instant-youtube-container {
  1700. all: unset;
  1701. contain: strict;
  1702. display: block;
  1703. position: relative;
  1704. overflow: hidden;
  1705. cursor: pointer;
  1706. padding: 0;
  1707. margin: auto;
  1708. font: normal 14px/1.0 sans-serif, Arial, Helvetica, Verdana;
  1709. text-align: center;
  1710. background: black;
  1711. break-inside: avoid-column;
  1712. }
  1713. .instant-youtube-container[disabled] {
  1714. background: #888;
  1715. }
  1716. .instant-youtube-container[disabled] .instant-youtube-storyboard {
  1717. display: none;
  1718. }
  1719. .instant-youtube-container[pinned] {
  1720. box-shadow: 0 0 30px black;
  1721. }
  1722. .instant-youtube-container[playing] {
  1723. contain: none;
  1724. }
  1725. .instant-youtube-wrapper {
  1726. width: 100%;
  1727. height: 100%;
  1728. }
  1729. .instant-youtube-play-button {
  1730. display: block;
  1731. position: absolute;
  1732. width: 85px;
  1733. height: 60px;
  1734. left: 0;
  1735. right: 0;
  1736. top: 0;
  1737. bottom: 0;
  1738. margin: auto;
  1739. }
  1740. .instant-youtube-loading-spinner {
  1741. display: block;
  1742. position: absolute;
  1743. width: 20px;
  1744. height: 20px;
  1745. left: 0;
  1746. right: 0;
  1747. top: 0;
  1748. bottom: 0;
  1749. padding: 0;
  1750. margin: auto;
  1751. pointer-events: none;
  1752. background: url("data:image/gif;base64,R0lGODlhFAAUAJEDAMzMzLOzs39/f////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQFCgADACwAAAAAFAAUAAACPJyPqcuNItyCUJoQBo0ANIxpXOctYHaQpYkiHfM2cUrCNT0nqr4uudsz/IC5na/2Mh4Hu+HR6YBaplRDAQAh+QQFCgADACwEAAIADAAGAAACFpwdcYupC8BwSogR46xWZHl0l8ZYQwEAIfkEBQoAAwAsCAACAAoACgAAAhccMKl2uHxGCCvO+eTNmishcCCYjWEZFgAh+QQFCgADACwMAAQABgAMAAACFxwweaebhl4K4VE6r61DiOd5SfiN5VAAACH5BAUKAAMALAgACAAKAAoAAAIYnD8AeKqcHIwwhGntEWLkO3CcB4biNEIFACH5BAUKAAMALAQADAAMAAYAAAIWnDSpAHa4GHgohCHbGdbipnBdSHphAQAh+QQFCgADACwCAAgACgAKAAACF5w0qXa4fF6KUoVQ75UaA7Bs3yeNYAkWACH5BAUKAAMALAIABAAGAAwAAAIXnCU2iMfaRghqTmMp1moAoHyfIYIkWAAAOw==");
  1753. }
  1754. .instant-youtube-container:hover .ytp-large-play-button-svg {
  1755. fill: #CC181E;
  1756. }
  1757. .instant-youtube-alternative {
  1758. display: block;
  1759. position: absolute;
  1760. width: 20em;
  1761. height: 20px;
  1762. top: 50%;
  1763. left: 0;
  1764. right: 0;
  1765. margin: 60px auto;
  1766. padding: 0;
  1767. border: none;
  1768. text-align: center;
  1769. text-decoration: none;
  1770. text-shadow: 1px 1px 3px black;
  1771. font-weight: bold;
  1772. color: white;
  1773. z-index: 8;
  1774. font-weight: normal;
  1775. font-size: 12px;
  1776. }
  1777. .instant-youtube-alternative:hover {
  1778. text-decoration: underline;
  1779. color: white;
  1780. background: transparent;
  1781. }
  1782. .instant-youtube-embed {
  1783. z-index: 10;
  1784. background: transparent;
  1785. transition: opacity .25s;
  1786. }
  1787. .instant-youtube-title {
  1788. z-index: 20;
  1789. display: block;
  1790. position: absolute;
  1791. width: auto;
  1792. top: 0;
  1793. left: 0;
  1794. right: 0;
  1795. margin: 0;
  1796. padding: 7px;
  1797. border: none;
  1798. text-shadow: 1px 1px 2px black;
  1799. text-align: center;
  1800. text-decoration: none;
  1801. color: white;
  1802. background-color: rgba(0, 0, 0, 0.5);
  1803. }
  1804. .instant-youtube-title strong {
  1805. font: bold 14px/1.0 sans-serif, Arial, Helvetica, Verdana;
  1806. }
  1807. .instant-youtube-title strong:after {
  1808. content: " - $tl:'watch on Youtube'";
  1809. font-weight: normal;
  1810. margin-right: 1ex;
  1811. }
  1812. .instant-youtube-title span {
  1813. color: white;
  1814. }
  1815. .instant-youtube-title span:before {
  1816. content: "(";
  1817. }
  1818. .instant-youtube-title span:after {
  1819. content: ")";
  1820. }
  1821. .instant-youtube-container .instant-youtube-title i {
  1822. all: unset;
  1823. opacity: .5;
  1824. font-style: normal;
  1825. color: white;
  1826. }
  1827. @-webkit-keyframes instant-youtube-fadein {
  1828. from { opacity: 0 }
  1829. to { opacity: 1 }
  1830. }
  1831. @-moz-keyframes instant-youtube-fadein {
  1832. from { opacity: 0 }
  1833. to { opacity: 1 }
  1834. }
  1835. @keyframes instant-youtube-fadein {
  1836. from { opacity: 0 }
  1837. to { opacity: 1 }
  1838. }
  1839. .instant-youtube-container:not(:hover) .instant-youtube-title[hidden] {
  1840. display: none;
  1841. margin: 0;
  1842. }
  1843. .instant-youtube-title:hover {
  1844. text-decoration: underline;
  1845. }
  1846. .instant-youtube-title strong {
  1847. color: white;
  1848. }
  1849. .instant-youtube-options-button {
  1850. opacity: 0.6;
  1851. position: absolute;
  1852. right: 0;
  1853. bottom: 0;
  1854. margin: 0;
  1855. padding: 1.5ex 2ex;
  1856. font-size: 11px;
  1857. text-shadow: 1px 1px 2px black;
  1858. color: white;
  1859. }
  1860. .instant-youtube-options-button:hover {
  1861. opacity: 1;
  1862. background: rgba(0, 0, 0, 0.5);
  1863. }
  1864. .instant-youtube-options {
  1865. display: flex;
  1866. position: absolute;
  1867. right: 0;
  1868. bottom: 0;
  1869. margin: 0;
  1870. padding: 1ex 1ex 2ex 2ex;
  1871. flex-direction: column;
  1872. align-items: flex-start;
  1873. line-height: 1.5;
  1874. text-align: left;
  1875. opacity: 1;
  1876. color: white;
  1877. background: black;
  1878. z-index: 999;
  1879. }
  1880. .instant-youtube-options * {
  1881. width: auto;
  1882. height: auto;
  1883. margin: 0;
  1884. padding: 0;
  1885. font: inherit;
  1886. font-size: 13px;
  1887. vertical-align: middle;
  1888. text-transform: none;
  1889. text-align: left;
  1890. border-radius: 0;
  1891. text-decoration: none;
  1892. color: white;
  1893. background: black;
  1894. }
  1895. .instant-youtube-options > * {
  1896. margin-top: 1ex;
  1897. }
  1898. .instant-youtube-options table {
  1899. all: unset;
  1900. display: table;
  1901. }
  1902. .instant-youtube-options tr {
  1903. all: unset;
  1904. display: table-row;
  1905. }
  1906. .instant-youtube-options td {
  1907. all: unset;
  1908. display: table-cell;
  1909. padding: 2px;
  1910. }
  1911. .instant-youtube-options label > * {
  1912. display: inline;
  1913. }
  1914. .instant-youtube-options select {
  1915. padding: .5ex .25ex;
  1916. border: 1px solid #444;
  1917. -webkit-appearance: menulist;
  1918. }
  1919. .instant-youtube-options [data-action="size-custom"] input {
  1920. width: 9ex;
  1921. padding: .5ex .5ex .4ex;
  1922. border: 1px solid #666;
  1923. }
  1924. .instant-youtube-options [data-action="buttons"] {
  1925. margin-top: 1em;
  1926. }
  1927. .instant-youtube-options button {
  1928. margin: 0 1ex 0 0;
  1929. padding: .5ex 2ex;
  1930. border: 2px solid gray;
  1931. font-weight: bold;
  1932. }
  1933. .instant-youtube-options button:hover {
  1934. border-color: white;
  1935. }
  1936. .instant-youtube-options label[disabled] {
  1937. opacity: 0.25;
  1938. }
  1939. .instant-youtube-storyboard {
  1940. height: 33%;
  1941. max-height: 90px;
  1942. display: block;
  1943. position: absolute;
  1944. left: 0;
  1945. right: 0;
  1946. bottom: 0;
  1947. overflow: visible;
  1948. overflow-x: visible;
  1949. overflow-y: visible;
  1950. }
  1951. .instant-youtube-storyboard:hover {
  1952. background-color: rgba(0,0,0,0.3);
  1953. }
  1954. .instant-youtube-storyboard[disabled] {
  1955. display:none;
  1956. }
  1957. .instant-youtube-storyboard div {
  1958. display: block;
  1959. position: absolute;
  1960. bottom: 0px;
  1961. pointer-events: none;
  1962. border: 3px solid #888;
  1963. box-shadow: 2px 2px 10px black;
  1964. transition: opacity .25s ease;
  1965. background-color: transparent;
  1966. background-origin: content-box;
  1967. opacity: 0;
  1968. }
  1969. .instant-youtube-storyboard:hover div {
  1970. opacity: 1;
  1971. }
  1972. .instant-youtube-container [pin] {
  1973. position: absolute;
  1974. width: 0;
  1975. height: 0;
  1976. margin: 0;
  1977. padding: 0;
  1978. top: auto; bottom: auto; left: auto; right: auto;
  1979. border-style: solid;
  1980. transition: opacity 2.5s ease-in, opacity 0.4s ease-out;
  1981. opacity: 0;
  1982. z-index: 100;
  1983. }
  1984. .instant-youtube-container[playing]:hover [pin]:not([transparent]) {
  1985. opacity: 1;
  1986. }
  1987. .instant-youtube-container[playing] [pin]:hover {
  1988. cursor: alias;
  1989. opacity: 1;
  1990. transition: opacity 0s;
  1991. }
  1992. .instant-youtube-container [pin=top-left][active] { border-top-color: green; }
  1993. .instant-youtube-container [pin=top-left]:hover { border-top-color: #fc0; }
  1994. .instant-youtube-container [pin=top-left] {
  1995. top: 0; left: 0;
  1996. border-width: 10px 10px 0 0;
  1997. border-color: red transparent transparent transparent;
  1998. }
  1999. .instant-youtube-container [pin=top-left][transparent] {
  2000. border-width: 10px 10px 0 0;
  2001. }
  2002. .instant-youtube-container [pin=top-right][active] { border-right-color: green; }
  2003. .instant-youtube-container [pin=top-right]:hover { border-right-color: #fc0; }
  2004. .instant-youtube-container [pin=top-right] {
  2005. top: 0; right: 0;
  2006. border-width: 0 10px 10px 0;
  2007. border-color: transparent red transparent transparent;
  2008. }
  2009. .instant-youtube-container [pin=top-right][transparent] {
  2010. border-width: 0 10px 10px 0;
  2011. }
  2012. .instant-youtube-container [pin=bottom-right][active] { border-bottom-color: green; }
  2013. .instant-youtube-container [pin=bottom-right]:hover { border-bottom-color: #fc0; }
  2014. .instant-youtube-container [pin=bottom-right] {
  2015. bottom: 0; right: 0;
  2016. border-width: 0 0 10px 10px;
  2017. border-color: transparent transparent red transparent;
  2018. }
  2019. .instant-youtube-container [pin=bottom-right][transparent] {
  2020. border-width: 0 0 10px 10px;
  2021. }
  2022. .instant-youtube-container [pin=bottom-left][active] { border-left-color: green; }
  2023. .instant-youtube-container [pin=bottom-left]:hover { border-left-color: #fc0; }
  2024. .instant-youtube-container [pin=bottom-left] {
  2025. bottom: 0; left: 0;
  2026. border-width: 10px 0 0 10px;
  2027. border-color: transparent transparent transparent red;
  2028. }
  2029. .instant-youtube-container [pin=bottom-left][transparent] {
  2030. border-width: 10px 0 0 10px;
  2031. }
  2032. .instant-youtube-dragndrop-placeholder {
  2033. z-index: 999999999;
  2034. margin: 0;
  2035. padding: 0;
  2036. background: rgba(0, 255, 0, 0.1);
  2037. border: 2px dotted green;
  2038. box-sizing: border-box;
  2039. pointer-events: none;
  2040. }
  2041. .instant-youtube-container [size-gripper] {
  2042. width: 0;
  2043. position: absolute;
  2044. top: 0;
  2045. bottom: 0;
  2046. cursor: e-resize;
  2047. border-color: rgba(50,100,255,0.5);
  2048. border-width: 12px;
  2049. background: rgba(50,100,255,0.2);
  2050. z-index: 99;
  2051. opacity: 0;
  2052. transition: opacity .1s ease-in-out, border-color .1s ease-in-out;
  2053. }
  2054. .instant-youtube-container[pinned*="right"] [size-gripper] {
  2055. border-style: none none none solid;
  2056. left: -4px;
  2057. }
  2058. .instant-youtube-container[pinned*="left"] [size-gripper] {
  2059. border-style: none solid none none;
  2060. right: -4px;
  2061. }
  2062. .instant-youtube-container [size-gripper]:hover {
  2063. opacity: 1;
  2064. }
  2065. .instant-youtube-container [size-gripper]:active {
  2066. opacity: 1;
  2067. width: auto;
  2068. left: -4px;
  2069. right: -4px;
  2070. }
  2071. .instant-youtube-container [size-gripper][tried-exceeding] {
  2072. border-color: rgba(255,0,0,0.5);
  2073. }
  2074. .instant-youtube-container [size-gripper][saveAs="global"] {
  2075. border-color: rgba(0,255,0,0.5);
  2076. }
  2077. .instant-youtube-container [size-gripper][saveAs="site"] {
  2078. border-color: rgba(0,255,255,0.5);
  2079. }
  2080. .instant-youtube-container [size-gripper][saveAs="reset"] {
  2081. border-color: rgba(255,255,0,0.5);
  2082. }
  2083. .instant-youtube-container [size-gripper][saveAs="cancel"] {
  2084. border-color: rgba(255,0,255,0.25);
  2085. }
  2086. .instant-youtube-container [size-gripper] > div {
  2087. white-space: nowrap;
  2088. color: white;
  2089. font-weight: normal;
  2090. line-height: 1.25;
  2091. text-align: left;
  2092. position: absolute;
  2093. top: 50%;
  2094. padding: 1ex 1em 1ex;
  2095. background-color: rgba(80,150,255,0.5);
  2096. }
  2097. .instant-youtube-container [size-gripper] [save-as="site"] {
  2098. font-weight: bold;
  2099. color: yellow;
  2100. }
  2101. .instant-youtube-container[pinned*="left"] [size-gripper] > div {
  2102. right: 0;
  2103. }
  2104. */}).replace(/\$tl:'(.+?)'/g, function(m, m1) { return _(m1) })
  2105. ) +
  2106. updateAltPlayerCSS();
  2107. }