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.

目前为 2024-05-26 提交的版本。查看 最新版本

  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:en 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.
  5. // @description:ru На порядок ускоряет время загрузки страниц с большим количеством вставленных Youtube-видео. С первого момента загрузки страницы появляются заглушки для видео, которые можно щелкнуть для загрузки плеера, и почти сразу же появляются кавер-картинки с названием видео. В опциях можно включить режим использования упрощенного браузерного плеера (макс. 720p).
  6. //
  7. // @version 2.13.1
  8. //
  9. // @include *
  10. // @exclude /^https:\/\/(www\.)?youtube\.com\/(?!embed)/
  11. // @exclude https://accounts.google.*/o/oauth2/postmessageRelay*
  12. // @exclude https://clients*.google.*/youtubei/*
  13. // @exclude https://clients*.google.*/static/proxy*
  14. //
  15. // @author wOxxOm
  16. // @namespace wOxxOm.scripts
  17. // @license MIT License
  18. //
  19. // @grant GM_getValue
  20. // @grant GM_listValues
  21. // @grant GM_deleteValue
  22. // @grant GM_setValue
  23. // @grant GM_addStyle
  24. // @grant GM_xmlhttpRequest
  25. //
  26. // @connect www.youtube.com
  27. // @connect youtube.com
  28. //
  29. // @run-at document-start
  30. //
  31. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAZdEVYdFNvZnR3YXJlAEFkb2JlIEltYWdlUmVhZHlxyWU8AAAARVBMVEVMaXEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8SEhLg4OC5ubnNzc1jY2Pt7e0xMTGbm5uFhYWqqqpFRUXPnCirAAAACnRSTlMAGWQyT86G4vOxURbENgAAA3xJREFUeNrtm+eSrCAQhQeMYEAQ5v0f9eKuIOoEidbW5fzaMMJXTbBpzjweWVlZWVlZWVkHAVAUpRSE9SKkVK1qdmql9n9Rn1PP/bRSQ7i0WRQAfO69qJFsE8fSQovq4m33JYrXt4mBytexRziZ0IuRKBucUM0pCDVOrPrm/g8E0Pbp/p0s2oDG6mt3bTP2fD6nRZzzeZ6FEOMi8qvhKEqp8dv6qeUB+aB8XDaytCXbZMwEbLf1aMz/pxhoF0+DmLa+qvMA9GMXXSM7DgLQC5ANXQINT70YwT4AafrvOsr2IdAzgHSJRHazQC+BqUumyVwIagT6IR3A0BtjgNIHYAsBkgDV+rNICSC2SaAXIUkJoKZhCx5gnYM9TQlAew2gFgHrkorpZXAvAC4epXoLpQVQ23HpAkDGGACTzRriwTaCUm+EFgCjjBcJBAB1NmgHgHseBqDWANwOwDcI/AQw2wLgfr4ZQAbB/QU6awDkAeARhFm/DpH9y3A0cuvJMQjiBDC6AbgGIRyAYxDGEwBxBsC98AGo/AFcgkB0ShQEwD4IoQFkEOjNAJiNbgAqJx18AeyCoAAaBdAHALAJgjqahAWwCEIsgMtBiAYgEwvqBkBDAWBGbga4lK5FBbiSrlEN0MYA+J5i6sNhJAAmLgNEGYLv2wGNOQeu7AUxAS7thvFWwcXMIBrA1dwo0lZ8PTGKA2CRGsYAsDohDKeUzBvALjMOnhPaHpBCA1gXC8ICOJwPgwK41ErGcIdTt2pROADHUpEIVaBwLZYFAmDOlbKtRONTpOLuJf45QJWM+ZSMuX+hcvK64TgDWJZqmWfFfCvVuhWrJ98Lnq1Y7VKuD3DHPflcWAwBrji9bkxCiGmA//zWTALcfnF579Xt4qGo7Pdif82Gg+GW6/uncX0PcfoxIKaJBNxp4QA7E8uYqn+BjRF46L0wmYuEaEPXauqrNAFJ27/ycpUBruGub0F8M7SVL8xsjBNKI22KlA6EM8PYudlJm4NX8GzoM/x8wwdtVr7RtPJNi5WPHeyGjWEsLS3ttN5mxmUJ7mylMImhd9c/PJhqExO0J1ttWaXsv3phrwZ1siC0NXjjLE9j7f7gLgcQVT+O+UjW9rapEARf/f17g39t+vuVpf+dtMHf9PYb5n7g+JWDHxXFL9gquJPxj+Vz6yP56xpZWVlZWVl/Qf8Ao4r0HI4dIbEAAAAASUVORK5CYII=
  32. //
  33. // @compatible chrome
  34. // @compatible firefox
  35. // @compatible opera
  36. // ==/UserScript==
  37.  
  38. 'use strict';
  39.  
  40. let localStorage;
  41. try { ({localStorage} = window); } catch (e) { localStorage = {}; }
  42. // keep video info cache for a month since last time it's shown
  43. const CACHE_STALE_DURATION = 30 * 24 * 3600e3;
  44. const CACHE_PREFIX = 'FYTE-cache-';
  45. const CACHE_PROPS = [
  46. ['videoWidth', 0],
  47. ['videoHeight', 0],
  48. ['duration'],
  49. ['fps'],
  50. ['title'],
  51. ['coverWidth', 0],
  52. ['coverHeight', 0],
  53. ['cover'],
  54. ];
  55. const rxYoutubeId = /(?:https?:)?\/\/(?:www\.)?(?:youtube(?:-nocookie)?\.com(?=\/)(?:\/embed\/(?:v=)?|.*?[&?/]v[=/])|youtu\.be\/)([-\w]+)[^'"\s]*/;
  56. const rxYoutubeIdHtml = new RegExp(`${/(?:src|value)\s*=\s*["']\s*/.source}(${rxYoutubeId.source})['"]|$`, 'i');
  57. const cfg = {
  58. width: 1280,
  59. height: 720,
  60. invidious: false,
  61. resize: 'Fit to width',
  62. rules: {},
  63. pinnable: 'on',
  64. pinnedWidth: 400,
  65. playHTML5: false,
  66. playHTML5Shown: false,
  67. showStoryboard: true,
  68. skipCustom: true,
  69. };
  70. const checked = new WeakMap();
  71. const dbCache = {};
  72. const dbFlush = new Set();
  73. const dbFlushDelay = 1000;
  74. let _, db, fytedom, styledom, iframes, objects, persite, playbtn;
  75.  
  76. if (location.hostname === 'www.youtube.com') {
  77. if ((unsafeWindow.chrome || 0).app && window !== top)
  78. setupYoutubeFullscreenRelay();
  79. } else {
  80. for (const [k, def] of Object.entries(cfg)) {
  81. const v = GM_getValue(k, def);
  82. cfg[k] = typeof v === typeof def ? v : def;
  83. }
  84. _ = initTL();
  85. persite = getPersiteRule();
  86. fytedom = document.getElementsByClassName('instant-youtube-container');
  87. iframes = document.getElementsByTagName('iframe');
  88. objects = document.getElementsByTagName('object');
  89. updateCustomSize();
  90. findEmbeds([]);
  91. injectStylesIfNeeded();
  92. new MutationObserver(findEmbeds)
  93. .observe(document, {subtree: true, childList: true});
  94. document.addEventListener('DOMContentLoaded', e => {
  95. injectStylesIfNeeded();
  96. adjustNodesIfNeeded(e);
  97. }, {once: true});
  98. addEventListener('resize', adjustNodesIfNeeded, true);
  99. addEventListener('message', onMessageHost);
  100. }
  101.  
  102. function setupYoutubeFullscreenRelay() {
  103. parent.postMessage('FYTE-toggle-fullscreen-init', '*');
  104. addEventListener('message', function onMessage(e) {
  105. if (e.source !== parent ||
  106. e.data !== 'FYTE-toggle-fullscreen-init-confirmed')
  107. return;
  108. removeEventListener('message', onMessage);
  109. const fsbtn = document.getElementsByClassName('ytp-fullscreen-button');
  110. new MutationObserver(function () {
  111. const el = fsbtn[0];
  112. if (el) {
  113. this.disconnect();
  114. el.removeAttribute('aria-disabled');
  115. el.replaceWith(el.cloneNode(true));
  116. fsbtn[0].addEventListener('click', () => parent.postMessage('FYTE-toggle-fullscreen', '*'));
  117. }
  118. }).observe(document, {subtree: true, childList: true});
  119. });
  120. }
  121.  
  122. function getPersiteRule() {
  123. const h = location.hostname;
  124. const rule =
  125. (cfg.rules || {})[h] ||
  126. h === 'developers.google.com' && {
  127. test: '[data-video-id]',
  128. src: e => '//youtu.be/' + e.dataset.videoId,
  129. } ||
  130. h === 'play.google.com' && {
  131. eatparent: 0,
  132. } ||
  133. h === 'androidauthority.com' && {
  134. eatparent: '.video-container',
  135. } ||
  136. h === 'reddit.com' && {
  137. test: '[data-url*="youtube.com/"], [data-url*="youtu.be/"]',
  138. has: '[src*="/mediaembed"]',
  139. attr: 'data-url',
  140. } ||
  141. h === '9gag.com' && {
  142. eatparent: 0,
  143. } ||
  144. h === 'anilist.co' && {
  145. eatparent: '.youtube',
  146. } ||
  147. h === 'www.theverge.com' && {
  148. eatparent: '.p-scalable-video',
  149. } ||
  150. /^www\.google\.\w{2,3}(\.\w{2,3})?$/.test(h) && $('html[itemtype$="SearchResultsPage"]') && {
  151. find: '#rcnt a[data-attrid="VisualDigestVideoResult"][href*="youtube.com/watch"]',
  152. test: '',
  153. eatparent: 2,
  154. };
  155. if (!rule)
  156. return;
  157. Object.setPrototypeOf(rule, null);
  158. const {find = 'iframe', has, test: q = '[src*="youtube.com/embed"]'} = rule;
  159. if (!/^\.?[a-z][-a-z]*$/i.test(find))
  160. Object.defineProperty(rule, 'find', {get: () => document.querySelectorAll(find)});
  161. else
  162. rule.find = find[0] === '.'
  163. ? document.getElementsByClassName(find.slice(1))
  164. : document.getElementsByTagName(find);
  165. if (q)
  166. rule.test = el =>
  167. (el = el.matches(q) ? el : el.querySelector(q)) &&
  168. (!has || el.querySelector(has)) && el;
  169. return rule;
  170. }
  171.  
  172. function onMessageHost(e) {
  173. switch (e.data) {
  174. case 'FYTE-toggle-fullscreen-init':
  175. if (findFrameElement(e.source))
  176. e.source.postMessage('FYTE-toggle-fullscreen-init-confirmed', '*');
  177. break;
  178. case 'FYTE-toggle-fullscreen': {
  179. const el = findFrameElement(e.source);
  180. if (el)
  181. goFullscreen(el,
  182. !(document.fullscreenElement || document.fullScreen || document.mozFullScreen));
  183. break;
  184. }
  185. case 'iframe-allowfs':
  186. $$('iframe:not([allowfullscreen])').some(iframe => {
  187. if (iframe.contentWindow === e.source) {
  188. iframe.allowFullscreen = true;
  189. return true;
  190. }
  191. });
  192. if (window !== top)
  193. parent.postMessage('iframe-allowfs', '*');
  194. break;
  195. }
  196. }
  197.  
  198. function findFrameElement(frameWindow) {
  199. return $$('iframe[allowfullscreen]').find(el => el.contentWindow === frameWindow);
  200. }
  201.  
  202. async function findEmbeds(mutations) {
  203. const found = [];
  204. if (mutations.length === 1) {
  205. const added = mutations[0].addedNodes;
  206. if (!added[0] || !added[1] && added[0].nodeType === 3)
  207. return;
  208. }
  209. if (persite)
  210. for (let el of persite.find)
  211. if (!persite.test || (el = persite.test(el)))
  212. processEmbed(found, el, persite.src ? persite.src(el) : el.getAttribute(persite.attr));
  213. if (length && hasChildWindow()) {
  214. for (const el of iframes) {
  215. const src = rxYoutubeId.exec(el.dataset.src || el.src);
  216. if (src) processEmbed(found, el, src[0]);
  217. }
  218. for (const el of objects) {
  219. const src = el.innerHTML.match(rxYoutubeIdHtml)[1];
  220. if (src) processEmbed(found, el, src);
  221. }
  222. }
  223. if (!found.length)
  224. return;
  225. const toRead = [];
  226. for (const [id] of found)
  227. if (!dbCache[id] && !toRead.includes(id))
  228. toRead.push(id);
  229. if (toRead.length)
  230. await read(toRead);
  231. for (const r of found)
  232. createFYTE(...r);
  233. }
  234.  
  235. function hasChildWindow() {
  236. for (let i = 0, w; i < length; i++)
  237. if ((w = unsafeWindow[i]) && typeof w === 'object' && !checked.has(w))
  238. return checked.set(w, 0);
  239. }
  240.  
  241. function decodeEmbedUrl(url) {
  242. return /youtube(-nocookie)?\.com%2Fembed/.test(url) ?
  243. decodeURIComponent(url.replace(/^.*?(http[^&?=]+?youtube(-nocookie)?\.com%2Fembed[^&]+).*$/i, '$1')) :
  244. url;
  245. }
  246.  
  247. function processEmbed(res, node, src) {
  248. src = src || node.src || node.href || '';
  249. if (!src || checked.get(node) === src)
  250. return;
  251. checked.set(node, src);
  252. let n = node;
  253. let np = n.parentNode;
  254. const srcFixed = decodeEmbedUrl(src)
  255. .replace(/\/(watch\?v=|v\/)/, '/embed/')
  256. .replace(/^([^?&]+)&/, '$1?');
  257. if (src.indexOf('cdn.embedly.com/') > 0 ||
  258. cfg.resize !== 'Original' && np && np.children.length === 1 && !np.className && !np.id) {
  259. n = location.hostname === 'disqus.com' ? np.parentNode : np;
  260. np = n.parentElement;
  261. }
  262. if (!np ||
  263. !np.parentNode ||
  264. cfg.skipCustom && srcFixed.includes('enablejsapi=1') ||
  265. srcFixed.includes('/embed/videoseries') ||
  266. node.matches('.instant-youtube-embed, .YTLT-embed, .ihvyoutube') ||
  267. node.style.position === 'fixed' ||
  268. node.onload // skip some retarded loaders
  269. )
  270. return;
  271.  
  272. let id = srcFixed.match(rxYoutubeId);
  273. if (!id)
  274. return;
  275. id = id[1];
  276.  
  277. if (np.localName === 'object') {
  278. n = np;
  279. np = n.parentElement;
  280. }
  281.  
  282. let eatparent = persite && persite.eatparent || 0;
  283. if (typeof eatparent === 'string') {
  284. n = np.closest(eatparent) || n;
  285. } else {
  286. while (eatparent--) {
  287. n = np;
  288. np = n.parentElement;
  289. }
  290. }
  291. res.push([id, node, n, getUrl(srcFixed)]);
  292. stopOriginalEmbed(node);
  293. }
  294.  
  295. function createFYTE(id, node, n, srcFixed) {
  296. const cache = dbCache[id] || {id};
  297. const autoplay = /[?&](autoplay=1|ps=play)(&|$)/.test(srcFixed);
  298. const div = $create('div.container');
  299. const img = $create('img.thumbnail');
  300. if (!autoplay) {
  301. img.src = getUrl(cache.cover) || getCoverUrl(id, 'maxresdefault.jpg');
  302. img.onerror = onCoverError;
  303. }
  304. injectStylesIfNeeded('force');
  305. div.FYTE = {
  306. state: 'querying',
  307. srcEmbed: srcFixed.replace(/&$/, ''),
  308. originalWidth: /%/.test(node.width) ? 320 : node.width | 0 || n.clientWidth | 0,
  309. originalHeight: /%/.test(node.height) ? 200 : node.height | 0 || n.clientHeight | 0,
  310. cache: cache,
  311. };
  312. div.FYTE.srcEmbedFixed =
  313. div.FYTE.srcEmbed.replace(/^http:/, 'https:')
  314. .replace(/([&?])(wmode=\w+|feature=oembed)&?/, '$1')
  315. .replace(/[&?]$/, '');
  316. div.FYTE.srcWatchFixed =
  317. div.FYTE.srcEmbedFixed.replace('/embed/', '/watch?v=').replace(/(\?.*?)\?/, '$1&');
  318.  
  319. cache.lastUsed = new Date();
  320. write(cache);
  321.  
  322. if (cache.reason)
  323. div.setAttribute('disabled', '');
  324.  
  325. const divSize = calcContainerSize(div, n);
  326. const origStyle = getComputedStyle(n);
  327. overrideCSS(div, Object.assign(
  328. {
  329. height: persite && persite.eatparent === 0 ? '100%' : divSize.h + 'px',
  330. 'min-width': Math.min(divSize.w, div.FYTE.originalWidth) + 'px',
  331. 'min-height': Math.min(divSize.h, div.FYTE.originalHeight) + 'px',
  332. 'max-width': divSize.w + 'px',
  333. },
  334. origStyle.transform && {
  335. transform: origStyle.transform,
  336. },
  337. !autoplay && {
  338. 'background-color': 'transparent',
  339. transition: 'background-color 2s',
  340. },
  341. // eslint-disable-next-line no-proto
  342. ...Object.keys(origStyle.hasOwnProperty('position') ? origStyle : origStyle.__proto__ /*FF*/)
  343. .filter(k => /^(position|left|right|top|bottom)$/.test(k) &&
  344. !/^(auto|static|block)$/.test(origStyle[k]))
  345. .map(k => ({[k]: origStyle[k]})),
  346. origStyle.display === 'inline' && {
  347. display: 'inline-block',
  348. width: '100%',
  349. },
  350. cfg.resize === 'Fit to width' && {
  351. width: '100%',
  352. }));
  353. if (!autoplay) {
  354. setTimeout(() => div.style.removeProperty('background-color'));
  355. setTimeout(() => div.style.removeProperty('transition'), 2000);
  356. }
  357.  
  358. const wrapper = $create('div.wrapper', {}, [
  359. img,
  360. $create('a.title', {target: '_blank', href: div.FYTE.srcWatchFixed},
  361. cache.title || cache.reason
  362. ? [
  363. $create('strong', {}, cache.title || cache.reason || ''),
  364. cache.duration && $create('span', {}, cache.duration),
  365. cache.fps && $create('i', {}, `${cache.fps}fps`),
  366. ]
  367. : '\xA0'),
  368. (playbtn || initPlayButton()).cloneNode(true),
  369. $create('span.alternative', {}, _(`msgPlay${cfg.playHTML5 ? 'HTML5' : ''}`)),
  370. $create('div.storyboard', {hidden: !cfg.showStoryboard}),
  371. $create('div.options-button', {}, _('Options')),
  372. ]);
  373. div.appendChild(wrapper);
  374.  
  375. overrideCSS(img, Object.assign({
  376. position: 'absolute',
  377. margin: 'auto',
  378. padding: 0,
  379. top: 0,
  380. left: 0,
  381. right: 0,
  382. bottom: 0,
  383. 'max-width': 'none',
  384. 'max-height': 'none',
  385. }, !cache.cover && {
  386. transition: 'opacity 0.1s ease-out',
  387. opacity: 0,
  388. }));
  389. img.FYTE = [div, divSize, autoplay];
  390. img.onload = onCoverLoad;
  391. if (cache.coverWidth || img.naturalWidth)
  392. img.onload();
  393.  
  394. n.parentNode.insertBefore(div, n);
  395. n.remove();
  396.  
  397. if (!cache.title && !cache.reason || autoplay && cfg.playHTML5)
  398. fetchInfo.call(div);
  399.  
  400. if (autoplay) {
  401. startPlaying(div);
  402. } else {
  403. div.addEventListener('click', clickHandler);
  404. div.addEventListener('mousedown', clickHandler);
  405. div.addEventListener('mouseenter', fetchInfo);
  406. }
  407. if (cfg.showStoryboard)
  408. div.addEventListener('mousemove', trackMouse);
  409. }
  410.  
  411. function fetchInfo(e) {
  412. this.FYTE.mouseEvent = e;
  413. this.removeEventListener('mouseenter', fetchInfo);
  414. if (!this.FYTE.storyboard) {
  415. const {id} = this.FYTE.cache;
  416. GM_xmlhttpRequest({
  417. method: 'GET',
  418. url: 'https://www.youtube.com/watch?v=' + id,
  419. context: this,
  420. onload: parseVideoInfo,
  421. });
  422. }
  423. }
  424.  
  425. function onCoverLoad(e) {
  426. const data = [...this.FYTE || []];
  427. const div = data.shift();
  428. const cache = div.FYTE.cache;
  429. const divSize = data.shift();
  430. const autoplay = data.shift();
  431. if (this.naturalWidth <= 120)
  432. return this.onerror(e);
  433. // delete this.FYTE;
  434. let fitToWidth = true;
  435. if (this.naturalHeight || cache.coverHeight) {
  436. if (!cache.coverHeight) {
  437. cache.coverWidth = this.naturalWidth;
  438. cache.coverHeight = this.naturalHeight;
  439. write(cache);
  440. }
  441. const ratio = cache.coverWidth / cache.coverHeight;
  442. if (ratio > 4.1 / 3 && ratio < divSize.w / divSize.h) {
  443. this.style.setProperty('width', 'auto', 'important');
  444. this.style.setProperty('height', '100%', 'important');
  445. fitToWidth = false;
  446. }
  447. }
  448. if (fitToWidth) {
  449. this.style.setProperty('width', '100%', 'important');
  450. this.style.setProperty('height', 'auto', 'important');
  451. }
  452. if (cache.videoWidth)
  453. fixThumbnailAR(div);
  454. if (!autoplay)
  455. this.style.opacity = 1;
  456. }
  457.  
  458. function onCoverError() {
  459. const {src} = this;
  460. const id = src.split('/')[4];
  461. const src2 = getCoverUrl(id, 'sddefault.jpg');
  462. this.src = src2 !== src ? src2 : getCoverUrl(id, 'hqdefault.jpg');
  463. }
  464.  
  465. function stopOriginalEmbed(node) {
  466. const src = 'data:,';
  467. let n = node;
  468. while (n) {
  469. if (n.src)
  470. n.src = src;
  471. if (n.dataset.src)
  472. n.dataset.src = src;
  473. n = $('embed', n);
  474. }
  475. for (const el of $$('[value*="youtu.be"], [value*="youtube.com"]', node))
  476. el.value = src;
  477. for (const el of $$('embed', node))
  478. el.src = src;
  479. }
  480.  
  481. function adjustNodesIfNeeded(e) {
  482. if (!fytedom[0])
  483. return;
  484. if (adjustNodesIfNeeded.scheduled)
  485. clearTimeout(adjustNodesIfNeeded.scheduled);
  486. adjustNodesIfNeeded.scheduled = setTimeout(() => {
  487. adjustNodes(e);
  488. adjustNodesIfNeeded.scheduled = 0;
  489. }, 16);
  490. }
  491.  
  492. function adjustNodes(event, clickedContainer) {
  493. const force = !!clickedContainer;
  494. let nearest = force ? clickedContainer : null;
  495. let nearestCenterYpct;
  496.  
  497. const vids = $$('.instant-youtube-container:not([pinned]):not([stub])');
  498.  
  499. if (!nearest && event.type !== 'DOMContentLoaded') {
  500. let minDistance = window.innerHeight * 3 / 4 | 0;
  501. const nearTargetY = window.innerHeight / 2;
  502. for (const n of vids) {
  503. const bounds = n.getBoundingClientRect();
  504. const distance = Math.abs((bounds.bottom + bounds.top) / 2 - nearTargetY);
  505. if (distance < minDistance) {
  506. minDistance = distance;
  507. nearest = n;
  508. }
  509. }
  510. }
  511.  
  512. if (nearest) {
  513. const bounds = nearest.getBoundingClientRect();
  514. nearestCenterYpct = (bounds.top + bounds.bottom) / 2 / window.innerHeight;
  515. }
  516.  
  517. let resized = false;
  518.  
  519. for (const n of vids) {
  520. const size = calcContainerSize(n);
  521. const w = size.w;
  522. const h = size.h;
  523.  
  524. // prevent parent clipping
  525. for (let e = n.parentElement, style; e; e = e.parentElement) {
  526. if (e.style.overflow !== 'visible' &&
  527. n.offsetTop < e.clientHeight / 2 &&
  528. n.offsetTop + n.clientHeight > e.clientHeight &&
  529. (style = getComputedStyle(e)) &&
  530. /hidden|scroll/.test(style.overflow + style.overflowX + style.overflowY)) {
  531. overrideCSS(e, {
  532. overflow: 'visible',
  533. 'overflow-x': 'visible',
  534. 'overflow-y': 'visible',
  535. });
  536. }
  537. }
  538.  
  539. if (force && Math.abs(w - parseFloat(n.style.maxWidth)) <= 2)
  540. continue;
  541.  
  542. overrideCSS(n, Object.assign({},
  543. n.style.maxWidth !== `${w}px` && {
  544. 'max-width': `${w}px`,
  545. },
  546. n.style.height !== h + 'px' && {
  547. height: h + 'px',
  548. },
  549. parseFloat(n.style.minWidth) > w && {
  550. 'min-width': n.style.maxWidth,
  551. },
  552. parseFloat(n.style.minHeight) > h && {
  553. 'min-height': n.style.height,
  554. }));
  555.  
  556. fixThumbnailAR(n);
  557. resized = true;
  558. }
  559.  
  560. if (resized && nearest)
  561. setTimeout(() => {
  562. const bounds = nearest.getBoundingClientRect();
  563. const h = bounds.bottom - bounds.top;
  564. const projectedCenterY = nearestCenterYpct * window.innerHeight;
  565. const projectedTop = projectedCenterY - h / 2;
  566. const safeTop = Math.min(Math.max(0, projectedTop), window.innerHeight - h);
  567. window.scrollBy(0, bounds.top - safeTop);
  568. }, 16);
  569. }
  570.  
  571. function calcContainerSize(div, origNode) {
  572. origNode = origNode || div;
  573. let w, h;
  574. const np = origNode.parentElement;
  575. const style = getComputedStyle(np);
  576. let parentWidth = parseFloat(style.width) -
  577. floatPadding(np, style, 'Left') -
  578. floatPadding(np, style, 'Right');
  579. if (+style.columnCount > 1)
  580. parentWidth = (parentWidth + parseFloat(style.columnGap)) / style.columnCount -
  581. parseFloat(style.columnGap);
  582. switch (cfg.resize) {
  583. case 'Original':
  584. if (div.FYTE.originalWidth === 320 && div.FYTE.originalHeight === 200) {
  585. w = parentWidth;
  586. h = parentWidth / 16 * 9;
  587. } else {
  588. w = div.FYTE.originalWidth;
  589. h = div.FYTE.originalHeight;
  590. }
  591. break;
  592. case 'Custom':
  593. w = cfg.width;
  594. h = cfg.height;
  595. break;
  596. case '1080p':
  597. case '720p':
  598. case '480p':
  599. case '360p':
  600. h = parseInt(cfg.resize);
  601. w = h / 9 * 16;
  602. break;
  603. default: { // fit-to-width mode
  604. let n = origNode;
  605. do {
  606. n = n.parentElement;
  607. // find parent node with nonzero width (i.e. independent of our video element)
  608. } while (n && !(w = n.clientWidth));
  609. if (w)
  610. h = w / 16 * 9;
  611. else {
  612. w = origNode.clientWidth;
  613. h = origNode.clientHeight;
  614. }
  615. }
  616. }
  617. if (parentWidth > 0 && parentWidth < w) {
  618. h *= parentWidth / w;
  619. w = parentWidth;
  620. }
  621. if (cfg.resize === 'Fit to width' && h < div.FYTE.originalHeight * 0.9)
  622. h = Math.min(div.FYTE.originalHeight, w / div.FYTE.originalWidth * div.FYTE.originalHeight);
  623.  
  624. return {w: window.chrome ? w : Math.round(w), h: h};
  625. }
  626.  
  627. function parseVideoInfo(response) {
  628. const div = response.context;
  629. const txt = response.responseText;
  630. const info = tryJSONparse(txt.match(/var\s+ytInitialPlayerResponse\s*=\s*({.+?});|$/)[1]) || {};
  631. const {reason} = info.playabilityStatus || {};
  632. const vid = info.videoDetails || {};
  633. const streams = info.streamingData || {};
  634. const cache = div.FYTE.cache;
  635. let shouldUpdateCache = false;
  636.  
  637. const videoSources = [];
  638. const fmts = (streams.formats || streams.adaptiveFormats || [])
  639. .sort((a, b) => b.width - a.width || b.height - a.height);
  640. // parse width & height to adjust the thumbnail
  641. if (fmts.length &&
  642. (cache.videoWidth !== fmts[0].width || cache.videoHeight !== fmts[0].height)) {
  643. fixThumbnailAR(div, fmts[0].width, fmts[0].height);
  644. cache.videoWidth = fmts[0].width;
  645. cache.videoHeight = fmts[0].height;
  646. shouldUpdateCache = true;
  647. }
  648.  
  649. // parse video sources
  650. for (const f of fmts) {
  651. const codec = f.mimeType.match(/codecs="([^.]+)|$/)[1] || '';
  652. const type = f.mimeType.split(/[/;]/)[1];
  653. let src = f.url;
  654. if (!src && f.cipher) {
  655. const sp = {};
  656. for (const str of f.cipher.split('&')) {
  657. const [k, v] = str.split('=');
  658. sp[k] = v;
  659. }
  660. src = decodeURIComponent(sp.url);
  661. if (sp.s) src += `&${sp.sp || 'sig'}=${decodeYoutubeSignature(sp.s)}`;
  662. }
  663. videoSources.push({
  664. src,
  665. title: [
  666. f.quality,
  667. f.qualityLabel !== f.quality ? f.qualityLabel : '',
  668. type + (codec ? `:${codec}` : ''),
  669. ].filter(Boolean).join(', '),
  670. });
  671. }
  672.  
  673. let fps = new Set();
  674. for (const f of streams.adaptiveFormats || []) {
  675. if (f.fps)
  676. fps.add(f.fps);
  677. }
  678. fps = [...fps].join('/');
  679. if (fps && cache.fps !== fps) {
  680. cache.fps = fps;
  681. shouldUpdateCache = true;
  682. }
  683.  
  684. let duration = div.FYTE.duration = vid.lengthSeconds | 0;
  685. if (duration) {
  686. duration = secondsToTimeString(duration);
  687. if (cache.duration !== duration) {
  688. cache.duration = duration;
  689. shouldUpdateCache = true;
  690. }
  691. }
  692. if (duration || fps)
  693. duration = `<span>${duration}</span>${fps ? `<i>${fps}fps</i>` : ''}`;
  694.  
  695. const title = [
  696. decodeURIComponent(vid.title || ''),
  697. reason && reason.replace(/\s*\.$/, ''),
  698. ].filter(Boolean).join(' | ').replace(/\+/g, ' ');
  699. if (title) {
  700. $('.instant-youtube-title', div).innerHTML =
  701. (title ? `<strong>${title}</strong>` : '') + duration;
  702. if (cache.title !== title) {
  703. cache.title = title;
  704. shouldUpdateCache = true;
  705. }
  706. }
  707. if (cfg.pinnable !== 'off' && vid.title)
  708. makeDraggable(div);
  709.  
  710. if (reason) {
  711. div.setAttribute('disabled', '');
  712. if (cache.reason !== reason) {
  713. cache.reason = reason;
  714. shouldUpdateCache = true;
  715. }
  716. }
  717.  
  718. if (videoSources.length)
  719. div.FYTE.videoSources = videoSources;
  720.  
  721. if (txt.includes('playerStoryboardSpecRenderer') &&
  722. info.storyboards &&
  723. div.FYTE.state !== 'scheduled play') {
  724. const m = info.storyboards.playerStoryboardSpecRenderer.spec.split('|');
  725. const [w, h, len, rows, cols] = m[m.length - 1].split('#').map(Number);
  726. div.FYTE.storyboard = {w, h, len, rows, cols};
  727. if (w * h > 2000) {
  728. div.FYTE.storyboard.url = m[0].replace('?', '&').replace(
  729. '$L/$N.jpg',
  730. `${m.length - 2}/M0.jpg?sigh=${m[m.length - 1].replace(/^.+?#([^#]+)$/, '$1')}`);
  731. const elSb = $('.instant-youtube-storyboard', div);
  732. if (elSb) {
  733. elSb.dataset.loaded = '';
  734. elSb.appendChild(overrideCSS($create('div.sb-thumb', {}, '\xA0'), {
  735. width: w - 1 + 'px',
  736. height: h + 'px',
  737. }));
  738. if (cfg.showStoryboard)
  739. updateHoverHandler(div);
  740. }
  741. }
  742. }
  743.  
  744. injectStylesIfNeeded();
  745.  
  746. if (div.FYTE.state === 'scheduled play')
  747. setTimeout(startPlayingDirectly, 0, div);
  748.  
  749. div.FYTE.state = '';
  750.  
  751. try {
  752. const cover = vid.thumbnail.thumbnails.pop().url;
  753. if (cache.cover !== cover) {
  754. cache.cover = cover;
  755. shouldUpdateCache = true;
  756. const img = $('img', div);
  757. if (img.src && img.src !== cover)
  758. img.src = getUrl(cover);
  759. }
  760. } catch (e) {}
  761. if (shouldUpdateCache)
  762. write(cache);
  763. }
  764.  
  765. function decodeYoutubeSignature(s) {
  766. const a = s.split('');
  767. a.reverse();
  768. swap(a, 24);
  769. a.reverse();
  770. swap(a, 41);
  771. a.reverse();
  772. swap(a, 2);
  773. return a.join('');
  774. }
  775.  
  776. function swap(a, b) {
  777. const c = a[0];
  778. a[0] = a[b % a.length];
  779. a[b % a.length] = c;
  780. }
  781.  
  782. function fixThumbnailAR(div, w, h) {
  783. const img = $('img', div);
  784. if (!img)
  785. return;
  786. const thw = img.naturalWidth;
  787. const
  788. thh = img.naturalHeight;
  789. if (w && h) { // means thumbnail is still loading
  790. div.FYTE.cache.videoWidth = w;
  791. div.FYTE.cache.videoHeight = h;
  792. } else {
  793. w = div.FYTE.cache.videoWidth;
  794. h = div.FYTE.cache.videoHeight;
  795. if (!w || !h)
  796. return;
  797. }
  798. const divw = div.clientWidth;
  799. const
  800. divh = div.clientHeight;
  801. // if both video and thumbnail are 4:3, fit the image to height
  802. //console.log(div, divw, divh, thw, thh, w, h, h/w*divw / divh - 1, thh/thw*divw / divh - 1);
  803. if (Math.abs(h / w * divw / divh - 1) > 0.05 && Math.abs(thh / thw * divw / divh - 1) > 0.05) {
  804. img.style.maxHeight = img.clientHeight + 'px';
  805. if (!div.FYTE.cache.videoWidth) // skip animation if thumbnail is already loaded
  806. img.style.transition = 'height 1s ease, margin-top 1s ease';
  807. setTimeout(() => {
  808. overrideCSS(img, Object.assign(
  809. {'max-height': 'none'},
  810. h / w >= divh / divw
  811. ? {width: 'auto', height: '100%'}
  812. : {width: '100%', height: 'auto'}));
  813. setTimeout(() => img.style.removeProperty('transition'), 1000);
  814. });
  815. }
  816. }
  817.  
  818. function trackMouse(e) {
  819. this.FYTE.mouseEvent = e;
  820. }
  821.  
  822. function updateHoverHandler(div) {
  823. const fyte = div.FYTE;
  824. const sb = fyte.storyboard;
  825. const elSb = $('.instant-youtube-storyboard', div);
  826. if (!cfg.showStoryboard) {
  827. elSb.hidden = true;
  828. return;
  829. }
  830. elSb.hidden = false;
  831. let oldIndex = null;
  832. const tracker = elSb.firstElementChild;
  833. const style = tracker.style;
  834. const sbImg = $create('img');
  835. const spinner = $create('span.loading-spinner');
  836. elSb.addEventListener('mousemove', storyboardHoverHandler);
  837. elSb.addEventListener('mouseout', storyboardHoverHandler);
  838. elSb.addEventListener('click', storyboardClickHandler, {once: true});
  839. div.addEventListener('mouseover', storyboardPreloader);
  840. div.addEventListener('mouseout', storyboardPreloader);
  841. if (div.closest(':hover'))
  842. storyboardPreloader({});
  843.  
  844. function storyboardClickHandler(e) {
  845. const offsetX = e.offsetX || e.clientX - elSb.getBoundingClientRect().left;
  846. fyte.startAt = offsetX / elSb.clientWidth * fyte.duration | 0;
  847. fyte.srcEmbedFixed = setUrlParams(fyte.srcEmbedFixed, {start: fyte.startAt});
  848. startPlaying(div, {alternateMode: e.shiftKey});
  849. }
  850.  
  851. function storyboardPreloader(e) {
  852. if (e.type === 'mouseout') {
  853. spinner.remove();
  854. return;
  855. }
  856. const {len, rows, cols, preloaded} = sb || {};
  857. const lastpart = (len - 1) / (rows * cols || 1) | 0;
  858. if (lastpart <= 0 || preloaded)
  859. return;
  860. let part = 0;
  861. $create('img', {
  862. src: setStoryboardUrl(part++),
  863. onload() {
  864. if (part <= lastpart) {
  865. this.src = setStoryboardUrl(part++);
  866. return;
  867. }
  868. sb.preloaded = true;
  869. div.removeEventListener('mouseover', storyboardPreloader);
  870. div.removeEventListener('mouseout', storyboardPreloader);
  871. this.onload = null;
  872. this.src = '';
  873. spinner.remove();
  874. },
  875. });
  876. if (elSb.matches(':hover') && fyte.mouseEvent)
  877. storyboardHoverHandler(fyte.mouseEvent);
  878. }
  879.  
  880. function setStoryboardUrl(part) {
  881. return getUrl(sb.url.replace(/M\d+\.jpg\?/, `M${part}.jpg?`));
  882. }
  883.  
  884. function storyboardHoverHandler(e) {
  885. div.removeEventListener('mousemove', trackMouse);
  886. if (!cfg.showStoryboard || !sb)
  887. return;
  888. if (e.type === 'mouseout') {
  889. sbImg.onload && sbImg.onload();
  890. return;
  891. }
  892. const {w, h, cols, rows, len, preloaded} = sb;
  893. const partlen = rows * cols;
  894.  
  895. const offsetX = e.offsetX || e.clientX - elSb.getBoundingClientRect().left;
  896. const left = Math.min(elSb.clientWidth - w, Math.max(0, offsetX - w)) | 0;
  897. if (!style.left || parseInt(style.left) !== left) {
  898. style.left = `${left}px`;
  899. if (spinner.parentElement)
  900. spinner.style.cssText = important(`left:${left + w / 2 - 10}px; right:auto;`);
  901. }
  902.  
  903. let index = Math.min(offsetX / elSb.clientWidth * (len + 1) | 0, len - 1);
  904. if (index === oldIndex)
  905. return;
  906.  
  907. const part = index / partlen | 0;
  908. if (!oldIndex || part !== (oldIndex / partlen | 0)) {
  909. const url = setStoryboardUrl(part);
  910. style.setProperty('background-image', `url(${url})`, 'important');
  911. if (!preloaded) {
  912. if (spinner.timer)
  913. clearTimeout(spinner.timer);
  914. spinner.timer = setTimeout(() => {
  915. spinner.timer = 0;
  916. if (!sbImg.src)
  917. return;
  918. elSb.appendChild(spinner);
  919. spinner.style.cssText = important(`left:${left + w / 2 - 10}px; right:auto;`);
  920. }, 50);
  921. sbImg.onload = () => {
  922. clearTimeout(spinner.timer);
  923. spinner.remove();
  924. spinner.timer = 0;
  925. sbImg.onload = null;
  926. sbImg.src = '';
  927. };
  928. sbImg.src = url;
  929. }
  930. }
  931.  
  932. tracker.dataset.time = secondsToTimeString(index / (len - 1 || 1) * fyte.duration | 0);
  933. oldIndex = index;
  934. index %= partlen;
  935. style.setProperty('background-position',
  936. `-${(index % cols) * w}px -${(index / cols | 0) * h}px`, 'important');
  937. }
  938. }
  939.  
  940. function clickHandler(e) {
  941. const el = e.target;
  942. if (el.closest('a') ||
  943. e.type === 'mousedown' && e.button !== 1 ||
  944. e.type === 'click' && el.matches('.instant-youtube-options, .instant-youtube-options *'))
  945. return;
  946. if (e.type === 'click' && el.matches('.instant-youtube-options-button')) {
  947. showOptions(e);
  948. e.preventDefault();
  949. e.stopPropagation();
  950. return;
  951. }
  952.  
  953. e.preventDefault();
  954. e.stopPropagation();
  955. e.stopImmediatePropagation();
  956.  
  957. startPlaying(el.closest('.instant-youtube-container'), {
  958. alternateMode: e.shiftKey || el.matches('.instant-youtube-alternative'),
  959. fullscreen: e.button === 1,
  960. });
  961. }
  962.  
  963. function startPlaying(div, params) {
  964. div.removeEventListener('click', clickHandler);
  965. div.removeEventListener('mousedown', clickHandler);
  966.  
  967. $$remove([
  968. '.instant-youtube-alternative',
  969. '.instant-youtube-storyboard',
  970. '.instant-youtube-options-button',
  971. '.instant-youtube-options',
  972. ].join(','), div);
  973. $('svg', div).outerHTML = '<span class=instant-youtube-loading-spinner></span>';
  974.  
  975. if (cfg.pinnable !== 'off') {
  976. makePinnable(div);
  977. if (params && params.pin)
  978. $(`[pin="${params.pin}"]`, div).click();
  979. }
  980.  
  981. if (window !== top)
  982. parent.postMessage('iframe-allowfs', '*');
  983.  
  984. const fyte = div.FYTE;
  985. if ((!!cfg.playHTML5 + !!(params && params.alternateMode) === 1) &&
  986. (fyte.videoSources || fyte.state === 'querying')) {
  987. if (fyte.videoSources)
  988. startPlayingDirectly(div, params);
  989. else {
  990. // playback will start in parseVideoInfo
  991. fyte.state = 'scheduled play';
  992. // fallback to iframe in 5s
  993. setTimeout(() => {
  994. if (div.FYTE.state) {
  995. div.FYTE.state = '';
  996. switchToIFrame.call(div, params);
  997. }
  998. }, 5000);
  999. }
  1000. } else
  1001. switchToIFrame.call(div, params);
  1002. }
  1003.  
  1004. function startPlayingDirectly(div, params) {
  1005. const switchTimer = setTimeout(switchToIFrame.bind(div, params), 5000);
  1006. const video = $create('video.embed', {
  1007. autoplay: true,
  1008. controls: true,
  1009. volume: GM_getValue('volume', 0.5),
  1010. style: {
  1011. position: 'absolute',
  1012. left: 0,
  1013. top: 0,
  1014. right: 0,
  1015. bottom: 0,
  1016. padding: 0,
  1017. margin: 'auto',
  1018. opacity: 0,
  1019. width: '100%',
  1020. height: '100%',
  1021. },
  1022. oncanplay() {
  1023. this.oncanplay = null;
  1024. const fyte = div.FYTE;
  1025. if (fyte.startAt && Math.abs(this.currentTime - fyte.startAt) > 1)
  1026. this.currentTime = fyte.startAt;
  1027. clearTimeout(switchTimer);
  1028. pauseOtherVideos(this);
  1029. if (params && params.fullscreen)
  1030. return;
  1031. div.setAttribute('playing', '');
  1032. div.firstElementChild.appendChild(this);
  1033. overrideCSS(this, {opacity: 1});
  1034. },
  1035. onvolumechange() {
  1036. GM_setValue('volume', this.volume);
  1037. },
  1038. });
  1039.  
  1040. for (const src of div.FYTE.videoSources || []) {
  1041. video.appendChild($create('source', src))
  1042. .onerror = switchToIFrame.bind(div, params);
  1043. }
  1044.  
  1045. overrideCSS($('img', div), {
  1046. transition: 'opacity 1s',
  1047. opacity: '0',
  1048. });
  1049.  
  1050. if (params && params.fullscreen) {
  1051. div.firstElementChild.appendChild(video);
  1052. div.setAttribute('playing', '');
  1053. video.style.opacity = 1;
  1054. goFullscreen(video);
  1055. }
  1056.  
  1057. if (window.chrome && +navigator.userAgent.match(/Chrom\D+(\d+)|$/)[1] < 74)
  1058. video.addEventListener('click', () =>
  1059. setTimeout(() =>
  1060. video.paused ?
  1061. video.play() :
  1062. video.pause()));
  1063.  
  1064. const title = $('.instant-youtube-title', div);
  1065. if (title) {
  1066. video.onpause = () => (title.hidden = false);
  1067. video.onplay = () => (title.hidden = true);
  1068. }
  1069. }
  1070.  
  1071. function switchToIFrame(params, e) {
  1072. if (this.querySelector('iframe'))
  1073. return;
  1074. const div = this;
  1075. const wrapper = div.firstElementChild;
  1076. const fullscreen = params && params.fullscreen && !e;
  1077. if (e instanceof Event) {
  1078. console.log('[FYTE] Direct linking canceled on %s, switching to IFRAME player',
  1079. div.FYTE.srcEmbed);
  1080. const video = e.target ? e.target.closest('video') : e.composedPath().pop();
  1081. video.textContent = '';
  1082. goFullscreen(video, false);
  1083. video.remove();
  1084. }
  1085.  
  1086. const url = setUrlParams(div.FYTE.srcEmbedFixed, {
  1087. html5: 1,
  1088. autoplay: 1,
  1089. autohide: 2,
  1090. border: 0,
  1091. controls: 1,
  1092. fs: 1,
  1093. showinfo: 1,
  1094. ssl: 1,
  1095. theme: 'dark',
  1096. enablejsapi: 1,
  1097. local: 'true',
  1098. quality: 'medium',
  1099. FYTEfullscreen: fullscreen | 0,
  1100. });
  1101.  
  1102. let iframe = $create('iframe.embed', {
  1103. src: url,
  1104. allow: 'autoplay; fullscreen',
  1105. allowFullscreen: true,
  1106. width: '100%',
  1107. height: '100%',
  1108. style: {
  1109. position: 'absolute',
  1110. top: 0,
  1111. left: 0,
  1112. right: 0,
  1113. padding: 0,
  1114. margin: 'auto',
  1115. opacity: 0,
  1116. border: 0,
  1117. },
  1118. });
  1119.  
  1120. if (cfg.pinnable !== 'off') {
  1121. $('[pin]', div).insertAdjacentElement('beforebegin', iframe);
  1122. } else {
  1123. wrapper.appendChild(iframe);
  1124. }
  1125.  
  1126. div.setAttribute('iframe', '');
  1127. div.setAttribute('playing', '');
  1128.  
  1129. iframe = $('iframe', div);
  1130. if (fullscreen) {
  1131. goFullscreen(iframe);
  1132. overrideCSS(iframe, {opacity: 1});
  1133. }
  1134. addEventListener('message', YTlistener);
  1135. iframe.addEventListener('load', () => {
  1136. iframe.contentWindow.postMessage('{"event":"listening"}', '*');
  1137. if (cfg.invidious || div.FYTE.cache.reason)
  1138. show();
  1139. }, {once: true});
  1140. setTimeout(show, 1000);
  1141.  
  1142. function show() {
  1143. overrideCSS(iframe, {opacity: 1});
  1144. $('.instant-youtube-title', div).hidden = true;
  1145. }
  1146.  
  1147. function YTlistener(e) {
  1148. const data = e.source === iframe.contentWindow && e.data && tryJSONparse(e.data);
  1149. if (!data || !data.info || data.info.playerState !== 1)
  1150. return;
  1151. removeEventListener('message', YTlistener);
  1152. pauseOtherVideos(iframe);
  1153. overrideCSS(iframe, {opacity: 1});
  1154. overrideCSS($('img', div), {display: 'none'});
  1155. $$remove('span, a', div);
  1156. }
  1157. }
  1158.  
  1159. function getUrl(url) {
  1160. if (cfg.invidious) {
  1161. const u = new URL(url);
  1162. u.hostname = 'invidio.us';
  1163. url = u.href.replace('/vi_webp/', '/vi/').replace('.webp', '.jpg');
  1164. }
  1165. return url;
  1166. }
  1167.  
  1168. function getCoverUrl(id, name) {
  1169. return getUrl(`https://i.ytimg.com/vi${name.endsWith('.webp') ? '_webp' : ''}/${id}/${name}`);
  1170. }
  1171.  
  1172. function setUrlParams(url, params) {
  1173. const u = new URL(url);
  1174. for (const [k, v] of Object.entries(params))
  1175. u.searchParams.set(k, v);
  1176. return u.href;
  1177. }
  1178.  
  1179. function pauseOtherVideos(activePlayer) {
  1180. for (const v of $$('.instant-youtube-embed', activePlayer.ownerDocument)) {
  1181. if (v === activePlayer)
  1182. continue;
  1183. switch (v.localName) {
  1184. case 'video':
  1185. if (!v.paused)
  1186. v.pause();
  1187. break;
  1188. case 'iframe':
  1189. try {
  1190. v.contentWindow.postMessage('{"event":"command", "func":"pauseVideo", "args":""}', '*');
  1191. } catch (e) {}
  1192. break;
  1193. }
  1194. }
  1195. }
  1196.  
  1197. function goFullscreen(el, enable) {
  1198. if (enable !== false)
  1199. el.webkitRequestFullScreen && el.webkitRequestFullScreen() ||
  1200. el.mozRequestFullScreen && el.mozRequestFullScreen() ||
  1201. el.requestFullScreen && el.requestFullScreen();
  1202. else
  1203. document.webkitCancelFullScreen && document.webkitCancelFullScreen() ||
  1204. document.mozCancelFullScreen && document.mozCancelFullScreen() ||
  1205. document.cancelFullScreen && document.cancelFullScreen();
  1206. }
  1207.  
  1208. function makePinnable(div) {
  1209. div.firstElementChild.insertAdjacentHTML('beforeend',
  1210. '<div size-gripper></div>' +
  1211. '<div pin="top-left"></div>' +
  1212. '<div pin="top-right"></div>' +
  1213. '<div pin="bottom-right"></div>' +
  1214. '<div pin="bottom-left"></div>');
  1215.  
  1216. for (const pin of $$('[pin]', div)) {
  1217. if (cfg.pinnable === 'hide')
  1218. pin.setAttribute('transparent', '');
  1219. pin.onclick = pinClicked;
  1220. }
  1221. $('[size-gripper]', div).addEventListener('mousedown', startResize, true);
  1222.  
  1223. function pinClicked() {
  1224. const pin = this;
  1225. const pinIt = !div.hasAttribute('pinned') || !pin.hasAttribute('active');
  1226. const corner = pin.getAttribute('pin');
  1227. const video = $('video', div);
  1228. const paused = video.paused;
  1229. if (pinIt) {
  1230. for (const p of $$('[pin][active]', div))
  1231. p.removeAttribute('active');
  1232. pin.setAttribute('active', '');
  1233. if (!div.FYTE.unpinnedStyle) {
  1234. div.FYTE.unpinnedStyle = div.style.cssText;
  1235. const stub = div.cloneNode();
  1236. const img = $('img', div).cloneNode();
  1237. img.style.opacity = 1;
  1238. img.style.display = 'block';
  1239. img.title = '';
  1240. stub.appendChild(img);
  1241. stub.onclick = e => $('[pin][active]', div).onclick(e);
  1242. stub.style.setProperty('opacity', .3, 'important');
  1243. stub.setAttribute('stub', '');
  1244. div.FYTE.stub = stub;
  1245. div.parentNode.insertBefore(stub, div);
  1246. }
  1247. const size = constrainPinnedSize(div,
  1248. localStorage.FYTEwidth || cfg.pinnedWidth);
  1249. overrideCSS(div, {
  1250. position: 'fixed',
  1251. width: size.w + 'px',
  1252. height: size.h + 'px',
  1253. top: corner.includes('top') ? 0 : 'auto',
  1254. left: corner.includes('left') ? 0 : 'auto',
  1255. right: corner.includes('right') ? 0 : 'auto',
  1256. bottom: corner.includes('bottom') ? 0 : 'auto',
  1257. 'z-index': 999999999,
  1258. });
  1259. adjustPinnedOffset(div, div, corner);
  1260. div.setAttribute('pinned', corner);
  1261. if (video && document.body)
  1262. document.body.appendChild(div);
  1263. } else { // unpin
  1264. pin.removeAttribute('active');
  1265. div.removeAttribute('pinned');
  1266. div.style.cssText = div.FYTE.unpinnedStyle;
  1267. div.FYTE.unpinnedStyle = '';
  1268. if (div.FYTE.stub) {
  1269. if (video && document.body)
  1270. div.FYTE.stub.parentNode.replaceChild(div, div.FYTE.stub);
  1271. div.FYTE.stub.remove();
  1272. div.FYTE.stub = null;
  1273. }
  1274. }
  1275. if (paused)
  1276. video.pause();
  1277. }
  1278.  
  1279. function startResize(e) {
  1280. const siteSaved = localStorage.FYTEwidth;
  1281. let saveAs = siteSaved ? 'site' : 'global';
  1282. const oldSizeCSS = {w: div.style.width, h: div.style.height};
  1283. const oldDraggable = div.draggable;
  1284. div.draggable = false;
  1285.  
  1286. const gripper = this;
  1287. gripper.removeAttribute('tried-exceeding');
  1288. gripper.innerHTML = `<div>
  1289. <div save-as="${saveAs}"><b>S</b> = Site mode: <span>${getSiteOnlyText()}</span></div>
  1290. ${!siteSaved ? '' : '<div><b>R</b> = Reset to global size</div>'}
  1291. <div><b>Esc</b> = Cancel</div>
  1292. </div>`;
  1293. document.addEventListener('mousemove', resize);
  1294. document.addEventListener('mouseup', resizeDone);
  1295. document.addEventListener('keydown', resizeKeyDown);
  1296. e.stopImmediatePropagation();
  1297. return false;
  1298.  
  1299. function getSiteOnlyText() {
  1300. return saveAs === 'site' ? `only ${location.hostname}` : 'global';
  1301. }
  1302.  
  1303. function resize(e) {
  1304. let deltaX = e.movementX || e.webkitMovementX || e.mozMovementX || 0;
  1305. if (/right/.test(div.getAttribute('pinned')))
  1306. deltaX = -deltaX;
  1307. const newSize = constrainPinnedSize(div, div.clientWidth + deltaX);
  1308. if (newSize.w !== div.clientWidth) {
  1309. div.style.setProperty('width', newSize.w + 'px', 'important');
  1310. div.style.setProperty('height', newSize.h + 'px', 'important');
  1311. gripper.removeAttribute('tried-exceeding');
  1312. } else if (newSize.triedExceeding) {
  1313. gripper.setAttribute('tried-exceeding', '');
  1314. }
  1315. window.getSelection().removeAllRanges();
  1316. return false;
  1317. }
  1318.  
  1319. function resizeDone() {
  1320. div.draggable = oldDraggable;
  1321. gripper.removeAttribute('tried-exceeding');
  1322. gripper.innerHTML = '';
  1323. document.removeEventListener('mousemove', resize);
  1324. document.removeEventListener('mouseup', resizeDone);
  1325. document.removeEventListener('keydown', resizeKeyDown);
  1326. switch (saveAs) {
  1327. case 'site':
  1328. localStorage.FYTEwidth = div.clientWidth;
  1329. break;
  1330. case 'global':
  1331. cfg.pinnedWidth = div.clientWidth;
  1332. GM_setValue('pinnedWidth', cfg.pinnedWidth);
  1333. // fallthrough to remove the locally saved value
  1334. case 'reset':
  1335. delete localStorage.FYTEwidth;
  1336. break;
  1337. case '':
  1338. return false;
  1339. }
  1340. gripper.setAttribute('saveAs', saveAs);
  1341. setTimeout(() => gripper.removeAttribute('saveAs'), 250);
  1342. return false;
  1343. }
  1344.  
  1345. function resizeKeyDown(e) {
  1346. switch (e.code) {
  1347. case 'Escape':
  1348. saveAs = 'cancel';
  1349. div.style.width = oldSizeCSS.w;
  1350. div.style.height = oldSizeCSS.h;
  1351. break;
  1352. case 'KeyS':
  1353. saveAs = saveAs === 'site' ? 'global' : 'site';
  1354. $('[save-as]', gripper).setAttribute('save-as', saveAs);
  1355. $('[save-as] span', gripper).textContent = getSiteOnlyText();
  1356. return false;
  1357. case 'KeyR': {
  1358. if (!siteSaved)
  1359. return;
  1360. saveAs = 'reset';
  1361. const {w, h} = constrainPinnedSize(div, cfg.pinnedWidth);
  1362. div.style.width = w;
  1363. div.style.height = h;
  1364. break;
  1365. }
  1366. default:
  1367. return;
  1368. }
  1369. document.dispatchEvent(new MouseEvent('mouseup'));
  1370. return false;
  1371. }
  1372. }
  1373. }
  1374.  
  1375. function makeDraggable(div) {
  1376. div.draggable = true;
  1377. div.addEventListener('dragstart', e => {
  1378. const offsetY = e.offsetY || e.clientY - div.getBoundingClientRect().top;
  1379. if (offsetY > div.clientHeight - 30) {
  1380. e.preventDefault();
  1381. return;
  1382. }
  1383.  
  1384. e.dataTransfer.setData('text/plain', '');
  1385.  
  1386. let dropZone = $create('div.dragndrop-placeholder');
  1387. const dropZoneHeight = 400 / div.FYTE.cache.videoWidth * div.FYTE.cache.videoHeight;
  1388.  
  1389. document.body.addEventListener('dragenter', dragHandler);
  1390. document.body.addEventListener('dragover', dragHandler);
  1391. document.body.addEventListener('dragend', dragHandler);
  1392. document.body.addEventListener('drop', dragHandler);
  1393.  
  1394. function dragHandler(e) {
  1395. e.stopImmediatePropagation();
  1396. e.stopPropagation();
  1397. e.preventDefault();
  1398. switch (e.type) {
  1399. case 'dragover': {
  1400. const playing = div.hasAttribute('playing');
  1401. const stub = e.target.closest('.instant-youtube-container[stub]') === div.FYTE.stub &&
  1402. div.FYTE.stub;
  1403. const gizmo = playing && !stub
  1404. ? {left: 0, top: 0, right: innerWidth, bottom: innerHeight}
  1405. : (stub || div).getBoundingClientRect();
  1406. const x = e.clientX;
  1407. const y = e.clientY;
  1408. const cx = (gizmo.left + gizmo.right) / 2;
  1409. const cy = (gizmo.top + gizmo.bottom) / 2;
  1410. const stay = !!stub || y >= cy - 200 && y <= cy + 200 && x >= cx - 200 && x <= cx + 200;
  1411. overrideCSS(dropZone, {
  1412. top: y < cy || stay ? '0' : 'auto',
  1413. bottom: y > cy || stay ? '0' : 'auto',
  1414. left: x < cx || stay ? '0' : 'auto',
  1415. right: x > cx || stay ? '0' : 'auto',
  1416. width: playing && stay && stub ? stub.clientWidth + 'px' : '400px',
  1417. height: playing && stay && stub ? stub.clientHeight + 'px' : dropZoneHeight + 'px',
  1418. margin: playing && stay ? 'auto' : '0',
  1419. position: !playing && stay || stub ? 'absolute' : 'fixed',
  1420. 'background-color': stub ?
  1421. 'rgba(0,0,255,0.5)' :
  1422. stay ? 'rgba(255,255,0,0.4)' : 'rgba(0,255,0,0.2)',
  1423. });
  1424. adjustPinnedOffset(dropZone, div);
  1425. (stay && !playing || stub ? (stub || div) : document.body).appendChild(dropZone);
  1426. break;
  1427. }
  1428. case 'dragend':
  1429. case 'drop': {
  1430. const corner = calcPinnedCorner(dropZone);
  1431. dropZone.remove();
  1432. dropZone = null;
  1433. document.body.removeEventListener('dragenter', dragHandler);
  1434. document.body.removeEventListener('dragover', dragHandler);
  1435. document.body.removeEventListener('dragend', dragHandler);
  1436. document.body.removeEventListener('drop', dragHandler);
  1437. if (e.type === 'dragend')
  1438. break;
  1439. if (div.hasAttribute('playing'))
  1440. (corner ? $(`[pin="${corner}"]`, div) : div.FYTE.stub).click();
  1441. else
  1442. startPlaying(div, {pin: corner});
  1443. }
  1444. }
  1445. }
  1446. });
  1447. }
  1448.  
  1449. function adjustPinnedOffset(el, self, corner) {
  1450. let offset = 0;
  1451. if (!corner) corner = calcPinnedCorner(el);
  1452. for (const pin of $$(`.instant-youtube-container[pinned] [pin="${corner}"][active]`)) {
  1453. const container = pin.closest('[pinned]');
  1454. if (container !== el && container !== self) {
  1455. const {top, bottom} = container.getBoundingClientRect();
  1456. offset = Math.max(offset, el.style.top === '0px' ? bottom : innerHeight - top);
  1457. }
  1458. }
  1459. if (offset)
  1460. el.style[el.style.top === '0px' ? 'top' : 'bottom'] = offset + 'px';
  1461. }
  1462.  
  1463. function calcPinnedCorner(el) {
  1464. const t = el.style.top !== 'auto';
  1465. const l = el.style.left !== 'auto';
  1466. const r = el.style.right !== 'auto';
  1467. const b = el.style.bottom !== 'auto';
  1468. return t && b && l && r ? '' : `${t ? 'top' : 'bottom'}-${l ? 'left' : 'right'}`;
  1469. }
  1470.  
  1471. function constrainPinnedSize(div, width) {
  1472. const maxWidth = window.innerWidth - 100 | 0;
  1473. const triedExceeding = (width | 0) > maxWidth;
  1474. width = Math.max(200, Math.min(maxWidth, width | 0));
  1475. return {
  1476. w: width,
  1477. h: width / div.FYTE.cache.videoWidth * div.FYTE.cache.videoHeight,
  1478. triedExceeding,
  1479. };
  1480. }
  1481.  
  1482. function showOptions(e) {
  1483. const [options] = translateHTML(`
  1484. <div class=instant-youtube-options>
  1485. <span>
  1486. <label tl style="width: 100% !important;">Size:&nbsp;
  1487. <select data-action=resize>
  1488. <option tl value=Original>Original
  1489. <option tl value="Fit to width">Fit to width
  1490. <option>360p
  1491. <option>480p
  1492. <option>720p
  1493. <option>1080p
  1494. <option tl value=Custom>Custom...
  1495. </select>
  1496. </label>&nbsp;
  1497. <label data-action=resize-custom ${cfg.resize !== 'Custom' ? 'disabled' : ''}>
  1498. <input type=number min=320 max=9999 tl-placeholder=width data-action=width step=1> x
  1499. <input type=number min=240 max=9999 tl-placeholder=height data-action=height step=1>
  1500. </label>
  1501. </span>
  1502. <label tl=content,title title=msgStoryboardTip>
  1503. <input data-action=showStoryboard type=checkbox>
  1504. msgStoryboard
  1505. </label>
  1506. <span>
  1507. <label tl=content,title title=msgDirectTip>
  1508. <input data-action=playHTML5 type=checkbox>
  1509. msgDirect
  1510. </label>
  1511. &nbsp;
  1512. <label tl=content,title title=msgDirectTip>
  1513. <input data-action=playHTML5Shown type=checkbox>
  1514. msgDirectShown
  1515. </label>
  1516. </span>
  1517. <label tl=content>
  1518. <input data-action=invidious type=checkbox>
  1519. msgInvidious
  1520. </label>
  1521. <label tl=content,title title=msgSafeTip>
  1522. <input data-action=skipCustom type=checkbox>
  1523. msgSafe
  1524. </label>
  1525. <table>
  1526. <tr>
  1527. <td><label tl=content,title title=msgPinningTip>msgPinning</label></td>
  1528. <td>
  1529. <select data-action=pinnable>
  1530. <option tl value=on>msgPinningOn
  1531. <option tl value=hide>msgPinningHover
  1532. <option tl value=off>msgPinningOff
  1533. </select>
  1534. </td>
  1535. </tr>
  1536. </table>
  1537. <span data-action=buttons>
  1538. <button tl data-action=ok>OK</button>
  1539. <button tl data-action=cancel>Cancel</button>
  1540. </span>
  1541. </div>
  1542. `);
  1543. for (const [k, v] of Object.entries(cfg)) {
  1544. const el = $(`[data-action=${k}]`, options);
  1545. if (el) el[el.type === 'checkbox' ? 'checked' : 'value'] = v;
  1546. }
  1547. $('[data-action=resize]', options).onchange = function () {
  1548. const v = this.value !== 'Custom';
  1549. const e = $('[data-action=resize-custom]', options);
  1550. e.children[0].disabled = e.children[1].disabled = v;
  1551. v ? e.setAttribute('disabled', '') : e.removeAttribute('disabled');
  1552. };
  1553. $('[data-action=buttons]', options).onclick = e => {
  1554. const btn = e.target;
  1555. if (btn.dataset.action !== 'ok') {
  1556. options.remove();
  1557. return;
  1558. }
  1559. let shouldAdjust;
  1560. const oldCfg = Object.assign({}, cfg);
  1561. for (const [k, v] of Object.entries(cfg)) {
  1562. const el = $(`[data-action=${k}]`, options);
  1563. const newVal = el && (
  1564. el.type === 'checkbox' ? el.checked :
  1565. el.type === 'number' ? el.valueAsNumber :
  1566. el.value);
  1567. if (newVal != null && newVal !== v) {
  1568. GM_setValue(k, newVal);
  1569. cfg[k] = newVal;
  1570. shouldAdjust = true;
  1571. }
  1572. }
  1573. options.remove();
  1574. if (cfg.resize === 'Custom' && (cfg.width !== oldCfg.width || cfg.height !== oldCfg.height))
  1575. updateCustomSize(cfg.width, cfg.height);
  1576. if (cfg.showStoryboard !== oldCfg.showStoryboard)
  1577. $$('.instant-youtube-container').forEach(updateHoverHandler);
  1578. if (cfg.playHTML5 !== oldCfg.playHTML5 && cfg.playHTML5Shown) {
  1579. const alt = _(`msgPlay${cfg.playHTML5 ? '' : 'HTML5'}`);
  1580. for (const e of $$('.instant-youtube-alternative'))
  1581. e.textContent = alt;
  1582. }
  1583. if (cfg.playHTML5Shown !== oldCfg.playHTML5Shown)
  1584. updateAltPlayerCSS();
  1585. if (shouldAdjust)
  1586. adjustNodes(e, btn.closest('.instant-youtube-container'));
  1587. };
  1588. e.target.insertAdjacentElement('afterend', options);
  1589. }
  1590.  
  1591. function updateCustomSize(w, h) {
  1592. cfg.width = Math.min(9999, Math.max(320, w | 0 || cfg.width | 0));
  1593. cfg.height = Math.min(9999, Math.max(240, h | 0 || cfg.height | 0));
  1594. }
  1595.  
  1596. function updateAltPlayerCSS() {
  1597. const ALT = '.instant-youtube-alternative:not(#foo)';
  1598. styledom.textContent = styledom.textContent.split(ALT)[0] + /*language=CSS*/ `${ALT} {
  1599. display: ${cfg.playHTML5Shown ? 'block' : 'none'} !important;
  1600. }`;
  1601. }
  1602.  
  1603. function important(cssText, rx = /;/g) {
  1604. return cssText.replace(rx, '!important;');
  1605. }
  1606.  
  1607. function $(sel, base = document) {
  1608. return base.querySelector(sel) || 0;
  1609. }
  1610.  
  1611. function $$(sel, base = document) {
  1612. return [...base.querySelectorAll(sel)];
  1613. }
  1614.  
  1615. function $create(tagCls, props, children) {
  1616. const [tag, cls] = tagCls.split('.');
  1617. const el = Object.assign(document.createElement(tag), props);
  1618. if (cls)
  1619. el.className = `instant-youtube-${cls}`;
  1620. if (props && typeof props.style === 'object')
  1621. overrideCSS(el, props.style);
  1622. if (children && typeof children !== 'object')
  1623. children = document.createTextNode(children);
  1624. if (children instanceof Node)
  1625. el.appendChild(children);
  1626. else if (Array.isArray(children))
  1627. el.append(...children.filter(Boolean));
  1628. return el;
  1629. }
  1630.  
  1631. function $$remove(sel, base = document) {
  1632. for (const el of base.querySelectorAll(sel))
  1633. el.remove();
  1634. }
  1635.  
  1636. function overrideCSS(el, props) {
  1637. const names = Object.keys(props);
  1638. el.style.cssText = el.style.cssText
  1639. .replace(new RegExp(`(^|\\s|;)(${names.join('|')})(:[^;]+)`, 'gi'), '$1')
  1640. .replace(/[^;]\s*$/, '$&;')
  1641. .replace(/^\s*;\s*/, '') + names.map(n => `${n}:${props[n]}!important;`).join(' ');
  1642. return el;
  1643. }
  1644.  
  1645. // fix dumb Firefox bug
  1646. function floatPadding(node, style, dir) {
  1647. const padding = style['padding' + dir];
  1648. if (padding.indexOf('%') < 0)
  1649. return parseFloat(padding);
  1650. return parseFloat(padding) * (parseFloat(style.width) || node.clientWidth) / 100;
  1651. }
  1652.  
  1653. async function cleanupStorage() {
  1654. cleanupStorage.timer = 0;
  1655. const cutoff = Date.now() - CACHE_STALE_DURATION;
  1656. // TODO: remove localStorage in 2024
  1657. for (const k in localStorage) {
  1658. if (k.startsWith(CACHE_PREFIX)) {
  1659. try {
  1660. const str = localStorage[k];
  1661. const isObj = str[0] === '{';
  1662. const val = isObj ? tryJSONparse(str) || false : str;
  1663. const time = isObj ? val.lastUsed : parseInt(val, 36) * 1000;
  1664. if (time > cutoff) {
  1665. const res = isObj
  1666. ? (val.lastUsed = new Date(time), val)
  1667. : unpack(val, k.slice(CACHE_PREFIX.length));
  1668. write(res);
  1669. }
  1670. } catch (e) {}
  1671. }
  1672. }
  1673. // TODO: remove GM_listValues in 2024
  1674. for (const k of GM_listValues())
  1675. if (k.startsWith('cache-'))
  1676. GM_deleteValue(k);
  1677. try {
  1678. /** @type {IDBIndex} */
  1679. const store = await db({raw: true, write: true, index: 'lastUsed'});
  1680. const req = store.openCursor(IDBKeyRange.upperBound(new Date(cutoff)));
  1681. req.onerror = console.warn;
  1682. req.onsuccess = () => {
  1683. const cur = /** @type {IDBCursorWithValue} */ req.result; if (!cur) return;
  1684. cur.delete().onerror = console.warn;
  1685. cur.continue();
  1686. };
  1687. } catch (e) { console.warn(e); }
  1688. await write().catch(console.warn);
  1689. for (const k in localStorage)
  1690. if (k.startsWith(CACHE_PREFIX))
  1691. delete localStorage[k];
  1692. }
  1693.  
  1694. function initDB() {
  1695. db = TinyIDB('FYTE', {
  1696. onUpgrade() {
  1697. this.result
  1698. .createObjectStore('data', {keyPath: 'id'})
  1699. .createIndex('lastUsed', 'u');
  1700. },
  1701. });
  1702. cleanupStorage.timer = setTimeout(cleanupStorage, 1e3);
  1703. return db;
  1704. }
  1705.  
  1706. async function read(ids) {
  1707. const toRead = [];
  1708. const toWrite = [];
  1709. for (const id of ids) {
  1710. const str = localStorage[CACHE_PREFIX + id];
  1711. if (str) {
  1712. toWrite.push(id);
  1713. unpack(str, id);
  1714. } else {
  1715. toRead.push(id);
  1716. }
  1717. }
  1718. if (toRead.length) {
  1719. for (const val of await (db || initDB()).getMulti(toRead))
  1720. if (val) unpack(val);
  1721. }
  1722. if (toWrite.length) {
  1723. if (!cleanupStorage.timer) cleanupStorage.timer = setTimeout(cleanupStorage, 1000);
  1724. toWrite.forEach(write);
  1725. }
  1726. }
  1727.  
  1728. function unpack(data, id) {
  1729. const old = !!id;
  1730. const arr = (old ? data : data.a).split('\n');
  1731. const obj = {
  1732. id: id || (id = data.id),
  1733. lastUsed: old ? new Date(parseInt(arr.shift(), 36) * 1000) : data.u,
  1734. };
  1735. for (let j = 0; j < CACHE_PROPS.length; j++) {
  1736. const [key, type] = CACHE_PROPS[j];
  1737. const v = arr[j] || '';
  1738. obj[key] = type === 0 ? parseInt(v, 36) || 0
  1739. : key === 'cover' ? getCoverUrl(id, v) || ''
  1740. : v;
  1741. }
  1742. return (dbCache[id] = obj);
  1743. }
  1744.  
  1745. function write(data) {
  1746. if (data) {
  1747. if (!write.timer) write.timer = setTimeout(write, dbFlushDelay);
  1748. const id = typeof data === 'object' ? data.id : data;
  1749. dbCache[id] = data;
  1750. dbFlush.add(id);
  1751. return;
  1752. }
  1753. const toWrite = [];
  1754. for (const id of dbFlush) {
  1755. const obj = dbCache[id];
  1756. let res = '';
  1757. for (const [key, type] of CACHE_PROPS) {
  1758. const v = obj[key];
  1759. // not storing array values separately to minimize byte size
  1760. res += `${res ? '\n' : ''}${!v ? ''
  1761. : type === 0 ? (+v).toString(36)
  1762. : key === 'cover' ? v.split('?')[0].split('/').pop()
  1763. : v.replace(/\n/g, ' ')}`;
  1764. }
  1765. toWrite.push({
  1766. a: res,
  1767. u: obj.lastUsed,
  1768. id,
  1769. });
  1770. }
  1771. dbFlush.clear();
  1772. write.timer = 0;
  1773. return toWrite.length ? (db || initDB()).putMulti(toWrite) : Promise.resolve();
  1774. }
  1775.  
  1776. function tryJSONparse(s) {
  1777. try {
  1778. return JSON.parse(s);
  1779. } catch (e) {}
  1780. }
  1781.  
  1782. function secondsToTimeString(sec) {
  1783. const h = sec / 3600 | 0;
  1784. const m = (sec / 60 | 0) % 60;
  1785. const s = sec % 60;
  1786. return `${h ? h + ':' : ''}${h && m < 10 ? 0 : ''}${m}:${s < 10 ? 0 : ''}${s}`;
  1787. }
  1788.  
  1789. function translateHTML(html) {
  1790. const tmp = $create('div', {innerHTML: html.trim().replace(/\n\s*/g, '')});
  1791. for (const node of $$('[tl]', tmp)) {
  1792. for (const what of (node.getAttribute('tl') || 'content').split(',')) {
  1793. let child;
  1794. if (what === 'content') {
  1795. for (const n of [...node.childNodes].reverse()) {
  1796. if (n.nodeType === Node.TEXT_NODE && n.textContent.trim()) {
  1797. child = n;
  1798. break;
  1799. }
  1800. }
  1801. } else
  1802. child = node.getAttributeNode(what);
  1803. if (!child)
  1804. continue;
  1805. const src = child.textContent;
  1806. const srcTrimmed = src.trim();
  1807. const tl = src.replace(srcTrimmed, _(srcTrimmed));
  1808. if (src !== tl)
  1809. child.textContent = tl;
  1810. }
  1811. }
  1812. return [...tmp.childNodes];
  1813. }
  1814.  
  1815. function initTL() {
  1816. const tlSource = {
  1817. msgWatch: {
  1818. en: 'watch on Youtube',
  1819. ru: 'открыть на Youtube',
  1820. },
  1821. msgPlay: {
  1822. en: 'Play with Youtube player',
  1823. ru: 'Включить плеер Youtube',
  1824. },
  1825. msgPlayHTML5: {
  1826. en: 'Play directly (up to 720p)',
  1827. ru: 'Включить напрямую (макс. 720p)',
  1828. },
  1829. msgAltPlayerHint: {
  1830. en: 'Shift-click to use alternative player',
  1831. ru: 'Shift-клик для смены типа плеера',
  1832. },
  1833. Options: {
  1834. ru: 'Опции',
  1835. },
  1836. 'Size:': {
  1837. ru: 'Размер:',
  1838. },
  1839. Original: {
  1840. ru: 'Исходный',
  1841. },
  1842. 'Fit to width': {
  1843. ru: 'На всю ширину',
  1844. },
  1845. 'Custom...': {
  1846. ru: 'Настроить...',
  1847. },
  1848. width: {
  1849. ru: 'ширина',
  1850. },
  1851. height: {
  1852. ru: 'высота',
  1853. },
  1854. msgStoryboard: {
  1855. en: 'Storyboard thumbnails on hover',
  1856. ru: 'Раскадровка при наведении курсора',
  1857. },
  1858. msgStoryboardTip: {
  1859. en: 'Show storyboard preview on mouse hover at the bottom',
  1860. ru: 'Показывать миникадры при наведении мыши на низ кавер-картинки',
  1861. },
  1862. msgDirect: {
  1863. en: 'Play directly',
  1864. ru: 'Встроенный плеер браузера',
  1865. },
  1866. msgDirectTip: {
  1867. en: 'Shift-click a thumbnail to use the alternative player',
  1868. ru: 'Удерживайте клавишу Shift при щелчке на картинке для альтернативного плеера',
  1869. },
  1870. msgDirectShown: {
  1871. en: 'Show under play button',
  1872. ru: 'Показывать под кнопкой ►',
  1873. },
  1874. msgInvidious: {
  1875. en: 'Use https://invidio.us to play videos',
  1876. ru: 'Использовать https://invidio.us в плеере',
  1877. },
  1878. msgSafe: {
  1879. en: 'Safe (skip videos with enablejsapi=1)',
  1880. ru: 'Консервативный режим',
  1881. },
  1882. msgSafeTip: {
  1883. en: 'Do not process customized videos with enablejsapi=1 parameter (requires page reload)',
  1884. ru: 'Не обрабатывать нестандартные видео с параметром enablejsapi=1 ' +
  1885. '(подействует после обновления страницы)',
  1886. },
  1887. msgPinning: {
  1888. en: 'Corner pinning',
  1889. ru: 'Закрепление по углам',
  1890. },
  1891. msgPinningTip: {
  1892. en: 'Enable corner pinning controls when a video is playing.\n' +
  1893. 'To restore the video click the active corner pin or the original video placeholder.',
  1894. ru: 'Включить шпильки по углам для закрепления видео во время просмотра.\n' +
  1895. 'Для отмены можно нажать еще раз на активированный угол или на заглушку, ' +
  1896. 'где исходно было видео',
  1897. },
  1898. msgPinningOn: {
  1899. en: 'On',
  1900. ru: 'Да',
  1901. },
  1902. msgPinningHover: {
  1903. en: 'On, hover a corner to show',
  1904. ru: 'Да, при наведении курсора',
  1905. },
  1906. msgPinningOff: {
  1907. en: 'Off',
  1908. ru: 'Нет',
  1909. },
  1910. OK: {
  1911. ru: 'ОК',
  1912. },
  1913. Cancel: {
  1914. ru: 'Оменить',
  1915. },
  1916. };
  1917. const browserLang = navigator.language || navigator.languages && navigator.languages[0] || '';
  1918. const browserLangMajor = browserLang.replace(/-.+/, '');
  1919. const tl = {};
  1920. for (const k of Object.keys(tlSource)) {
  1921. const langs = tlSource[k];
  1922. const text = langs[browserLang] || langs[browserLangMajor];
  1923. if (text)
  1924. tl[k] = text;
  1925. }
  1926. return src => tl[src] || src;
  1927. }
  1928.  
  1929. function initPlayButton() {
  1930. [playbtn] = translateHTML(`
  1931. <svg class="instant-youtube-play-button">
  1932. <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"><title tl>msgAltPlayerHint</title></path>
  1933. <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF"
  1934. points="33.3,41.4 33.3,17.75 56,29.6"></polygon>
  1935. </svg>`);
  1936. return playbtn;
  1937. }
  1938.  
  1939. /**
  1940. * @param {string} dbName
  1941. * @param {Object} [opts]
  1942. * @param {string} [opts.store]
  1943. * @param {string} [opts.index]
  1944. * @param {number} [opts.timeout]
  1945. * @param {number} [opts.version]
  1946. * @param {(this:IDBOpenDBRequest, e:IDBVersionChangeEvent)=>void} [opts.onUpgrade]
  1947. * @returns {TinyIDB}
  1948. */
  1949. function TinyIDB(dbName, {
  1950. store = 'data',
  1951. index,
  1952. version,
  1953. timeout = 250,
  1954. onUpgrade = function (evt) {
  1955. if (store) evt.target.result.createObjectStore(store);
  1956. },
  1957. } = {}) {
  1958. /** @typedef {TinyIDBSource | ((config?:TinyIDBConfig) => Promise<TinyIDBSource>)} TinyIDB */
  1959. /** @typedef {number|string|Date|BufferSource|IDBKeyRange} IDBKey */
  1960. /** @typedef {IDBObjectStore | IDBIndex | {
  1961. * getMulti: (keys: IDBKey[]) => ?[]
  1962. * putMulti: (values: Object[] | [val:?, key:IDBKey][]) => IDBKey[]
  1963. * }} TinyIDBSource */
  1964. /** @typedef TinyIDBConfig
  1965. * @prop {boolean} [raw] - returns the raw IDBObjectStore | IDBIndex object
  1966. * @prop {boolean} [write]
  1967. * @prop {string} [store]
  1968. * @prop {string} [index]
  1969. */
  1970. let timer;
  1971. /** @type {IDBDatabase} */
  1972. let db;
  1973. let dbPromise;
  1974. const RW = ['add', 'clear', 'delete', 'put', 'putMulti'];
  1975. const handler = {
  1976. apply: proxyApply,
  1977. get: (cfg, method) => proxyGet.bind(null, cfg, method),
  1978. };
  1979. return new Proxy(TinyIDB, handler);
  1980.  
  1981. function proxyApply(_, thisObj, [cfg]) {
  1982. return cfg && cfg.raw
  1983. ? proxyGet(cfg)
  1984. : new Proxy(cfg || {}, handler);
  1985. }
  1986. /**
  1987. * @param {TinyIDBConfig} cfg
  1988. * @param {keyof IDBObjectStore | 'getMulti' | 'putMulti'} method
  1989. * @param {...?[]} args
  1990. * @return {Promise<?>}
  1991. */
  1992. async function proxyGet(cfg, method, ...args) {
  1993. if (!db) await (dbPromise || open());
  1994. const {
  1995. raw,
  1996. index: txIndex = index,
  1997. store: txStore = store,
  1998. write = RW.includes(method),
  1999. } = cfg;
  2000. const arg = args[0];
  2001. const isMulti = !raw && (method === 'getMulti' || method === 'putMulti');
  2002. if (isMulti) {
  2003. if (!Array.isArray(arg)) throw new Error('Argument must be an array');
  2004. if (!arg.length) return [];
  2005. }
  2006. const tx = db.transaction(txStore, write ? 'readwrite' : 'readonly');
  2007. let source = tx.objectStore(txStore);
  2008. if (timer) clearTimeout(timer);
  2009. if (txIndex) source = source.index(txIndex);
  2010. if (timeout) tx.oncomplete = tx.onerror = closeLater;
  2011. if (raw) return source;
  2012. let req, resolve, reject;
  2013. const promise = new Promise((ok, ko) => (resolve = ok) && (reject = ko));
  2014. if (isMulti) {
  2015. source._results = [];
  2016. method = method.slice(0, 3);
  2017. const addKey = method === 'put' && !source.keyPath;
  2018. for (let i = 0, val, len = arg.length; i < len; i++) {
  2019. val = arg[i];
  2020. req = addKey
  2021. ? source[method](val[0], val[1])
  2022. : source[method](val);
  2023. req.onsuccess = onExecMulti;
  2024. req.onerror = reject;
  2025. }
  2026. } else {
  2027. req = source[method](...args);
  2028. req.onsuccess = onExec;
  2029. req.onerror = reject;
  2030. }
  2031. req._resolve = resolve;
  2032. return promise;
  2033. }
  2034. function onExec() {
  2035. this._resolve(this.result);
  2036. }
  2037. function onExecMulti() {
  2038. const {result, _resolve: cb, source: {_results: arr}} = this;
  2039. arr.push(result);
  2040. if (cb) cb(arr);
  2041. }
  2042. function open() {
  2043. dbPromise = new Promise((resolve, reject) => {
  2044. const op = indexedDB.open(dbName, version);
  2045. op.onupgradeneeded = onUpgrade;
  2046. op.onsuccess = onOpened;
  2047. op.onerror = reject;
  2048. op._resolve = resolve;
  2049. });
  2050. return dbPromise;
  2051. }
  2052. function onOpened() {
  2053. this._resolve(db = this.result);
  2054. dbPromise = null;
  2055. }
  2056. function closeLater() {
  2057. timer = setTimeout(closeNow, timeout);
  2058. }
  2059. function closeNow() {
  2060. timer = 0;
  2061. if (db) {
  2062. db.close();
  2063. db = null;
  2064. }
  2065. }
  2066. }
  2067.  
  2068. function injectStylesIfNeeded(force) {
  2069. if (!fytedom[0] && !force)
  2070. return;
  2071. styledom = styledom || GM_addStyle(/*language=CSS*/ `
  2072. [class^="instant-youtube-"]:not(#foo) {
  2073. margin: 0;
  2074. padding: 0;
  2075. transform: none;
  2076. }
  2077. ` + important(/*language=CSS*/ `
  2078. .instant-youtube-container {
  2079. contain: strict;
  2080. display: block;
  2081. position: relative;
  2082. overflow: hidden;
  2083. cursor: pointer;
  2084. padding: 0;
  2085. margin: auto;
  2086. font: normal 14px/1.0 sans-serif, Arial, Helvetica, Verdana;
  2087. text-align: center;
  2088. background: black;
  2089. break-inside: avoid-column;
  2090. }
  2091. .instant-youtube-container[disabled] {
  2092. background: #888;
  2093. }
  2094. .instant-youtube-container[disabled] .instant-youtube-storyboard {
  2095. display: none;
  2096. }
  2097. .instant-youtube-container[pinned] {
  2098. box-shadow: 0 0 30px black;
  2099. }
  2100. .instant-youtube-container[playing] {
  2101. contain: none;
  2102. }
  2103. .instant-youtube-wrapper {
  2104. width: 100%;
  2105. height: 100%;
  2106. }
  2107. .instant-youtube-play-button {
  2108. display: block;
  2109. position: absolute;
  2110. width: 85px;
  2111. height: 60px;
  2112. left: 0;
  2113. right: 0;
  2114. top: 0;
  2115. bottom: 0;
  2116. margin: auto;
  2117. }
  2118. .instant-youtube-loading-spinner {
  2119. display: block;
  2120. position: absolute;
  2121. width: 20px;
  2122. height: 20px;
  2123. left: 0;
  2124. right: 0;
  2125. top: 0;
  2126. bottom: 0;
  2127. padding: 0;
  2128. margin: auto;
  2129. pointer-events: none;
  2130. background: url("data:image/gif;base64,R0lGODlhFAAUAJEDAMzMzLOzs39/f////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQFCgADACwAAAAAFAAUAAACPJyPqcuNItyCUJoQBo0ANIxpXOctYHaQpYkiHfM2cUrCNT0nqr4uudsz/IC5na/2Mh4Hu+HR6YBaplRDAQAh+QQFCgADACwEAAIADAAGAAACFpwdcYupC8BwSogR46xWZHl0l8ZYQwEAIfkEBQoAAwAsCAACAAoACgAAAhccMKl2uHxGCCvO+eTNmishcCCYjWEZFgAh+QQFCgADACwMAAQABgAMAAACFxwweaebhl4K4VE6r61DiOd5SfiN5VAAACH5BAUKAAMALAgACAAKAAoAAAIYnD8AeKqcHIwwhGntEWLkO3CcB4biNEIFACH5BAUKAAMALAQADAAMAAYAAAIWnDSpAHa4GHgohCHbGdbipnBdSHphAQAh+QQFCgADACwCAAgACgAKAAACF5w0qXa4fF6KUoVQ75UaA7Bs3yeNYAkWACH5BAUKAAMALAIABAAGAAwAAAIXnCU2iMfaRghqTmMp1moAoHyfIYIkWAAAOw==");
  2131. }
  2132. .instant-youtube-container:hover .ytp-large-play-button-svg {
  2133. fill: #CC181E;
  2134. }
  2135. .instant-youtube-alternative {
  2136. display: block;
  2137. position: absolute;
  2138. width: 20em;
  2139. height: 20px;
  2140. top: 50%;
  2141. left: 0;
  2142. right: 0;
  2143. margin: 60px auto;
  2144. padding: 0;
  2145. border: none;
  2146. text-align: center;
  2147. text-decoration: none;
  2148. text-shadow: 1px 1px 3px black;
  2149. color: white;
  2150. z-index: 8;
  2151. font-weight: normal;
  2152. font-size: 12px;
  2153. }
  2154. .instant-youtube-alternative:hover {
  2155. text-decoration: underline;
  2156. color: white;
  2157. background: transparent;
  2158. }
  2159. .instant-youtube-embed {
  2160. z-index: 10;
  2161. background: transparent;
  2162. transition: opacity .25s;
  2163. }
  2164. .instant-youtube-title {
  2165. z-index: 20;
  2166. display: block;
  2167. position: absolute;
  2168. width: auto;
  2169. top: 0;
  2170. left: 0;
  2171. right: 0;
  2172. margin: 0;
  2173. padding: 7px;
  2174. border: none;
  2175. text-shadow: 1px 1px 2px black;
  2176. text-align: center;
  2177. text-decoration: none;
  2178. color: white;
  2179. background-color: #0008;
  2180. }
  2181. .instant-youtube-title strong {
  2182. font: bold 14px/1.0 sans-serif, Arial, Helvetica, Verdana;
  2183. }
  2184. .instant-youtube-title strong::after {
  2185. content: " - ${_('msgWatch')}";
  2186. font-weight: normal;
  2187. margin-right: 1ex;
  2188. }
  2189. .instant-youtube-title span {
  2190. color: white;
  2191. }
  2192. .instant-youtube-title span::before {
  2193. content: "(";
  2194. }
  2195. .instant-youtube-title span::after {
  2196. content: ")";
  2197. }
  2198. .instant-youtube-title i::before {
  2199. content: ", ";
  2200. }
  2201. .instant-youtube-container .instant-youtube-title i {
  2202. all: unset;
  2203. opacity: .5;
  2204. font-style: normal;
  2205. color: white;
  2206. }
  2207. @-webkit-keyframes instant-youtube-fadein {
  2208. from { opacity: 0 }
  2209. to { opacity: 1 }
  2210. }
  2211. @-moz-keyframes instant-youtube-fadein {
  2212. from { opacity: 0 }
  2213. to { opacity: 1 }
  2214. }
  2215. @keyframes instant-youtube-fadein {
  2216. from { opacity: 0 }
  2217. to { opacity: 1 }
  2218. }
  2219. .instant-youtube-container:not(:hover) .instant-youtube-title[hidden] {
  2220. display: none;
  2221. margin: 0;
  2222. }
  2223. .instant-youtube-title:hover {
  2224. text-decoration: underline;
  2225. }
  2226. .instant-youtube-title strong {
  2227. color: white;
  2228. }
  2229. .instant-youtube-options-button {
  2230. opacity: 0.6;
  2231. position: absolute;
  2232. right: 0;
  2233. bottom: 0;
  2234. margin: 0;
  2235. padding: 1.5ex 2ex;
  2236. font-size: 11px;
  2237. text-shadow: 1px 1px 2px black;
  2238. color: white;
  2239. }
  2240. .instant-youtube-options-button:hover {
  2241. opacity: 1;
  2242. background: rgba(0, 0, 0, 0.5);
  2243. }
  2244. .instant-youtube-options {
  2245. display: flex;
  2246. position: absolute;
  2247. right: 0;
  2248. bottom: 0;
  2249. margin: 0;
  2250. padding: 1ex 1ex 2ex 2ex;
  2251. flex-direction: column;
  2252. align-items: flex-start;
  2253. line-height: 1.5;
  2254. text-align: left;
  2255. opacity: 1;
  2256. color: white;
  2257. background: black;
  2258. z-index: 999;
  2259. }
  2260. .instant-youtube-options * {
  2261. width: auto;
  2262. height: auto;
  2263. margin: 0;
  2264. padding: 0;
  2265. font: inherit;
  2266. font-size: 13px;
  2267. vertical-align: middle;
  2268. text-transform: none;
  2269. text-align: left;
  2270. border-radius: 0;
  2271. text-decoration: none;
  2272. color: white;
  2273. background: black;
  2274. }
  2275. .instant-youtube-options > * {
  2276. margin-top: 1ex;
  2277. }
  2278. .instant-youtube-options table {
  2279. all: unset;
  2280. display: table;
  2281. }
  2282. .instant-youtube-options tr {
  2283. all: unset;
  2284. display: table-row;
  2285. }
  2286. .instant-youtube-options td {
  2287. all: unset;
  2288. display: table-cell;
  2289. padding: 2px;
  2290. }
  2291. .instant-youtube-options label > * {
  2292. display: inline;
  2293. }
  2294. .instant-youtube-options select {
  2295. padding: .5ex .25ex;
  2296. border: 1px solid #444;
  2297. -webkit-appearance: menulist;
  2298. }
  2299. .instant-youtube-options [data-action="resize-custom"] input {
  2300. width: 9ex;
  2301. padding: .5ex .5ex .4ex;
  2302. border: 1px solid #666;
  2303. }
  2304. .instant-youtube-options [data-action="buttons"] {
  2305. margin-top: 1em;
  2306. }
  2307. .instant-youtube-options button {
  2308. margin: 0 1ex 0 0;
  2309. padding: .5ex 2ex;
  2310. border: 2px solid gray;
  2311. font-weight: bold;
  2312. }
  2313. .instant-youtube-options button:hover {
  2314. border-color: white;
  2315. }
  2316. .instant-youtube-options label[disabled] {
  2317. opacity: 0.25;
  2318. }
  2319. .instant-youtube-storyboard {
  2320. height: 33%;
  2321. max-height: 90px;
  2322. display: block;
  2323. position: absolute;
  2324. left: 0;
  2325. right: 0;
  2326. bottom: 0;
  2327. overflow: visible;
  2328. transition: background-color .5s .25s;
  2329. }
  2330. .instant-youtube-storyboard[data-loaded]:hover {
  2331. background-color: #0004;
  2332. }
  2333. .instant-youtube-storyboard div {
  2334. display: block;
  2335. position: absolute;
  2336. bottom: 0;
  2337. pointer-events: none;
  2338. border: 3px solid #888;
  2339. box-shadow: 2px 2px 10px black;
  2340. transition: opacity .25s ease;
  2341. background-color: transparent;
  2342. background-origin: content-box;
  2343. opacity: 0;
  2344. }
  2345. .instant-youtube-storyboard div::after {
  2346. content: attr(data-time);
  2347. opacity: .5;
  2348. color: #fff;
  2349. background-color: #000;
  2350. font-weight: bold;
  2351. font-size: 10px;
  2352. position: absolute;
  2353. bottom: 4px;
  2354. left: 4px;
  2355. padding: 1px 3px;
  2356. }
  2357. .instant-youtube-storyboard:hover div {
  2358. opacity: 1;
  2359. }
  2360. .instant-youtube-container [pin] {
  2361. display: block;
  2362. position: absolute;
  2363. width: 0;
  2364. height: 0;
  2365. margin: 0;
  2366. padding: 0;
  2367. top: auto; bottom: auto; left: auto; right: auto;
  2368. border-style: solid;
  2369. transition: opacity 2.5s ease-in, opacity 0.4s ease-out;
  2370. opacity: 0;
  2371. z-index: 100;
  2372. }
  2373. .instant-youtube-container[playing]:hover [pin]:not([transparent]) {
  2374. opacity: 1;
  2375. }
  2376. .instant-youtube-container[playing] [pin]:hover {
  2377. cursor: alias;
  2378. opacity: 1;
  2379. transition: opacity 0s;
  2380. }
  2381. .instant-youtube-container [pin=top-left][active] { border-top-color: green; }
  2382. .instant-youtube-container [pin=top-left]:hover { border-top-color: #fc0; }
  2383. .instant-youtube-container [pin=top-left] {
  2384. top: 0; left: 0;
  2385. border-width: 10px 10px 0 0;
  2386. border-color: red transparent transparent transparent;
  2387. }
  2388. .instant-youtube-container [pin=top-left][transparent] {
  2389. border-width: 10px 10px 0 0;
  2390. }
  2391. .instant-youtube-container [pin=top-right][active] { border-right-color: green; }
  2392. .instant-youtube-container [pin=top-right]:hover { border-right-color: #fc0; }
  2393. .instant-youtube-container [pin=top-right] {
  2394. top: 0; right: 0;
  2395. border-width: 0 10px 10px 0;
  2396. border-color: transparent red transparent transparent;
  2397. }
  2398. .instant-youtube-container [pin=top-right][transparent] {
  2399. border-width: 0 10px 10px 0;
  2400. }
  2401. .instant-youtube-container [pin=bottom-right][active] { border-bottom-color: green; }
  2402. .instant-youtube-container [pin=bottom-right]:hover { border-bottom-color: #fc0; }
  2403. .instant-youtube-container [pin=bottom-right] {
  2404. bottom: 0; right: 0;
  2405. border-width: 0 0 10px 10px;
  2406. border-color: transparent transparent red transparent;
  2407. }
  2408. .instant-youtube-container [pin=bottom-right][transparent] {
  2409. border-width: 0 0 10px 10px;
  2410. }
  2411. .instant-youtube-container [pin=bottom-left][active] { border-left-color: green; }
  2412. .instant-youtube-container [pin=bottom-left]:hover { border-left-color: #fc0; }
  2413. .instant-youtube-container [pin=bottom-left] {
  2414. bottom: 0; left: 0;
  2415. border-width: 10px 0 0 10px;
  2416. border-color: transparent transparent transparent red;
  2417. }
  2418. .instant-youtube-container [pin=bottom-left][transparent] {
  2419. border-width: 10px 0 0 10px;
  2420. }
  2421. .instant-youtube-dragndrop-placeholder {
  2422. z-index: 999999999;
  2423. margin: 0;
  2424. padding: 0;
  2425. background: rgba(0, 255, 0, 0.1);
  2426. border: 2px dotted green;
  2427. box-sizing: border-box;
  2428. pointer-events: none;
  2429. }
  2430. .instant-youtube-container [size-gripper] {
  2431. width: 0;
  2432. position: absolute;
  2433. top: 0;
  2434. bottom: 0;
  2435. cursor: e-resize;
  2436. border-color: rgba(50,100,255,0.5);
  2437. border-width: 12px;
  2438. background: rgba(50,100,255,0.2);
  2439. z-index: 99;
  2440. opacity: 0;
  2441. transition: opacity .1s ease-in-out, border-color .1s ease-in-out;
  2442. }
  2443. .instant-youtube-container[pinned*="right"] [size-gripper] {
  2444. border-style: none none none solid;
  2445. left: -4px;
  2446. }
  2447. .instant-youtube-container[pinned*="left"] [size-gripper] {
  2448. border-style: none solid none none;
  2449. right: -4px;
  2450. }
  2451. .instant-youtube-container [size-gripper]:hover {
  2452. opacity: 1;
  2453. }
  2454. .instant-youtube-container [size-gripper]:active {
  2455. opacity: 1;
  2456. width: auto;
  2457. left: -4px;
  2458. right: -4px;
  2459. }
  2460. .instant-youtube-container [size-gripper][tried-exceeding] {
  2461. border-color: rgba(255,0,0,0.5);
  2462. }
  2463. .instant-youtube-container [size-gripper][saveAs="global"] {
  2464. border-color: rgba(0,255,0,0.5);
  2465. }
  2466. .instant-youtube-container [size-gripper][saveAs="site"] {
  2467. border-color: rgba(0,255,255,0.5);
  2468. }
  2469. .instant-youtube-container [size-gripper][saveAs="reset"] {
  2470. border-color: rgba(255,255,0,0.5);
  2471. }
  2472. .instant-youtube-container [size-gripper][saveAs="cancel"] {
  2473. border-color: rgba(255,0,255,0.25);
  2474. }
  2475. .instant-youtube-container [size-gripper] > div {
  2476. white-space: nowrap;
  2477. color: white;
  2478. font-weight: normal;
  2479. line-height: 1.25;
  2480. text-align: left;
  2481. position: absolute;
  2482. top: 50%;
  2483. padding: 1ex 1em 1ex;
  2484. background-color: rgba(80,150,255,0.5);
  2485. }
  2486. .instant-youtube-container [size-gripper] [save-as="site"] {
  2487. font-weight: bold;
  2488. color: yellow;
  2489. }
  2490. .instant-youtube-container[pinned*="left"] [size-gripper] > div {
  2491. right: 0;
  2492. }
  2493. `, /;\n/g));
  2494. // move our rules to the end of HEAD to increase CSS specificity
  2495. if (styledom.nextElementSibling && document.head)
  2496. document.head.appendChild(styledom);
  2497. updateAltPlayerCSS();
  2498. }