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.

当前为 2022-03-10 提交的版本,查看 最新版本

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