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