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.
// ==UserScript== // @name FYTE /Fast YouTube Embedded/ Player // @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. // @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. // @description:ru На порядок ускоряет время загрузки страниц с большим количеством вставленных Youtube-видео. С первого момента загрузки страницы появляются заглушки для видео, которые можно щелкнуть для загрузки плеера, и почти сразу же появляются кавер-картинки с названием видео. В опциях можно включить режим использования упрощенного браузерного плеера (макс. 720p). // // @version 2.15.1 // // @include * // @exclude /^https:\/\/(www\.)?youtube\.com\/(?!embed)/ // @exclude https://accounts.google.*/o/oauth2/postmessageRelay* // @exclude https://clients*.google.*/youtubei/* // @exclude https://clients*.google.*/static/proxy* // @exclude https://pikabu.ru/* // // @author wOxxOm // @namespace wOxxOm.scripts // @license MIT License // // @grant GM_getValue // @grant GM_listValues // @grant GM_deleteValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_xmlhttpRequest // // @connect www.youtube.com // @connect youtube.com // // @run-at document-start // // @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= // // @compatible chrome // @compatible firefox // @compatible opera // ==/UserScript== 'use strict'; let localStorage; try { ({localStorage} = window); } catch (e) { localStorage = {}; } // keep video info cache for a month since last time it's shown const CACHE_STALE_DURATION = 30 * 24 * 3600e3; const CACHE_PREFIX = 'FYTE-cache-'; const CACHE_PROPS = [ ['videoWidth', 0], ['videoHeight', 0], ['duration'], ['fps'], ['title'], ['cover'], ]; const CHROME = !CSS.supports('-moz-appearance', 'none'); const rxYoutubeId = /(?:https?:)?\/\/(?:www\.)?(?:youtube(?:-nocookie)?\.com(?=\/)(?:\/embed\/(?:v=)?|.*?[&?/]v[=/])|youtu\.be\/)([-\w]+)[^'"\s]*/; const rxYoutubeIdHtml = new RegExp(`${/(?:src|value)\s*=\s*["']\s*/.source}(${rxYoutubeId.source})['"]|$`, 'i'); const cfg = { width: 1280, height: 720, invidious: false, resize: 'Fit to width', rules: {}, pinnable: 'on', pinnedWidth: 400, playHTML5: false, playHTML5Shown: false, showStoryboard: true, skipCustom: true, }; const checked = new WeakMap(); const fyteMap = new WeakMap(); const dbCache = {}; const dbFlush = new Set(); const dbFlushDelay = 1000; let _, db, fytedom, styledom, iframes, objects, persite, playbtn; if (location.hostname === 'www.youtube.com') { if (CHROME && window !== top) setupYoutubeFullscreenRelay(); } else { for (const [k, def] of Object.entries(cfg)) { const v = GM_getValue(k, def); cfg[k] = typeof v === typeof def ? v : def; } _ = initTL(); persite = getPersiteRule(); fytedom = document.getElementsByClassName('instant-youtube-container'); iframes = document.getElementsByTagName('iframe'); objects = document.getElementsByTagName('object'); updateCustomSize(); findEmbeds([]); injectStylesIfNeeded(); new MutationObserver(findEmbeds) .observe(document, {subtree: true, childList: true}); document.addEventListener('DOMContentLoaded', e => { injectStylesIfNeeded(); adjustNodesIfNeeded(e); }, {once: true}); addEventListener('resize', adjustNodesIfNeeded, true); addEventListener('message', onMessageHost); } function setupYoutubeFullscreenRelay() { parent.postMessage('FYTE-toggle-fullscreen-init', '*'); addEventListener('message', function onMessage(e) { if (e.source !== parent || e.data !== 'FYTE-toggle-fullscreen-init-confirmed') return; removeEventListener('message', onMessage); const fsbtn = document.getElementsByClassName('ytp-fullscreen-button'); new MutationObserver(function () { const el = fsbtn[0]; if (el) { this.disconnect(); el.removeAttribute('aria-disabled'); el.replaceWith(el.cloneNode(true)); fsbtn[0].addEventListener('click', () => parent.postMessage('FYTE-toggle-fullscreen', '*')); } }).observe(document, {subtree: true, childList: true}); }); } function getPersiteRule() { const h = location.hostname; const rule = (cfg.rules || {})[h] || h === 'developers.google.com' && { test: '[data-video-id]', src: e => '//youtu.be/' + e.dataset.videoId, } || h === 'play.google.com' && { eatparent: 0, } || h === 'androidauthority.com' && { eatparent: '.video-container', } || h === 'reddit.com' && { test: '[data-url*="youtube.com/"], [data-url*="youtu.be/"]', has: '[src*="/mediaembed"]', attr: 'data-url', } || h === '9gag.com' && { eatparent: 0, } || h === 'anilist.co' && { eatparent: '.youtube', } || h === 'www.theverge.com' && { eatparent: '.p-scalable-video', } || /^www\.google\.\w{2,3}(\.\w{2,3})?$/.test(h) && $('html[itemtype$="SearchR###ltsPage"]') && { find: '#rcnt a[data-attrid="VisualDigestVideoR###lt"][href*="youtube.com/watch"]', test: '', eatparent: 2, }; if (!rule) return; Object.setPrototypeOf(rule, null); const {find = 'iframe', has, test: q = '[src*="youtube.com/embed"]'} = rule; if (!/^\.?[a-z][-a-z]*$/i.test(find)) Object.defineProperty(rule, 'find', {get: () => document.querySelectorAll(find)}); else rule.find = find[0] === '.' ? document.getElementsByClassName(find.slice(1)) : document.getElementsByTagName(find); if (q) rule.test = el => (el = el.matches(q) ? el : el.querySelector(q)) && (!has || el.querySelector(has)) && el; return rule; } function onMessageHost(e) { switch (e.data) { case 'FYTE-toggle-fullscreen-init': if (findFrameElement(e.source)) e.source.postMessage('FYTE-toggle-fullscreen-init-confirmed', '*'); break; case 'FYTE-toggle-fullscreen': { const el = findFrameElement(e.source); if (el) goFullscreen(el, !(document.fullscreenElement || document.fullScreen || document.mozFullScreen)); break; } case 'iframe-allowfs': $$('iframe:not([allowfullscreen])').some(iframe => { if (iframe.contentWindow === e.source) { iframe.allowFullscreen = true; return true; } }); if (window !== top) parent.postMessage('iframe-allowfs', '*'); break; } } function findFrameElement(frameWindow) { return $$('iframe[allowfullscreen]').find(el => el.contentWindow === frameWindow); } async function findEmbeds(mutations) { const found = []; if (mutations.length === 1) { const added = mutations[0].addedNodes; if (!added[0] || !added[1] && added[0].nodeType === 3) return; } if (persite) for (let el of persite.find) if (!persite.test || (el = persite.test(el))) processEmbed(found, el, persite.src ? persite.src(el) : el.getAttribute(persite.attr)); if (length && hasChildWindow()) { for (const el of iframes) { const src = rxYoutubeId.exec(el.dataset.src || el.src); if (src) processEmbed(found, el, src[0]); } for (const el of objects) { const src = el.innerHTML.match(rxYoutubeIdHtml)[1]; if (src) processEmbed(found, el, src); } } if (!found.length) return; const toRead = []; for (const [id] of found) if (!dbCache[id] && !toRead.includes(id)) toRead.push(id); if (toRead.length) await read(toRead); for (const r of found) createFYTE(...r); } function hasChildWindow() { for (let i = 0, w; i < length; i++) if ((w = unsafeWindow[i]) && typeof w === 'object' && !checked.has(w)) return checked.set(w, 0); } function decodeEmbedUrl(url) { return /youtube(-nocookie)?\.com%2Fembed/.test(url) ? decodeURIComponent(url.replace(/^.*?(http[^&?=]+?youtube(-nocookie)?\.com%2Fembed[^&]+).*$/i, '$1')) : url; } function processEmbed(res, node, src) { src = src || node.src || node.href || ''; if (!src || checked.get(node) === src) return; checked.set(node, src); let n = node; let np = n.parentNode; const srcFixed = decodeEmbedUrl(src) .replace(/\/(watch\?v=|v\/)/, '/embed/') .replace(/^([^?&]+)&/, '$1?'); if (src.indexOf('cdn.embedly.com/') > 0 || cfg.resize !== 'Original' && np && np.children.length === 1 && !np.className && !np.id) { n = location.hostname === 'disqus.com' ? np.parentNode : np; np = n.parentElement; } if (!np || !np.parentNode || cfg.skipCustom && srcFixed.includes('enablejsapi=1') || srcFixed.includes('/embed/videoseries') || node.matches('.instant-youtube-embed, .YTLT-embed, .ihvyoutube') || node.style.position === 'fixed' || node.onload // skip some retarded loaders ) return; let id = srcFixed.match(rxYoutubeId); if (!id) return; id = id[1]; if (np.localName === 'object') { n = np; np = n.parentElement; } let eatparent = persite && persite.eatparent || 0; if (typeof eatparent === 'string') { n = np.closest(eatparent) || n; } else { while (eatparent--) { n = np; np = n.parentElement; } } (n.contentWindow || {}).location = 'about:blank'; res.push([id, n, srcFixed]); } function createFYTE(id, n, srcFixed) { const cache = dbCache[id] || {id}; const autoplay = /[?&](autoplay=1|ps=play)(&|$)/.test(srcFixed); const div = $create('div.container'); const img = $create('img.thumbnail', {style: important('opacity:0;')}); if (!autoplay) { img.src = cache.cover || getCoverUrl(id, 'maxresdefault.jpg'); img.onload = onCoverLoad; img.onerror = onCoverError; } injectStylesIfNeeded('force'); const fyte = { state: 'querying', srcEmbed: srcFixed.replace(/&$/, ''), originalWidth: /%/.test(n.width) ? 320 : n.width | 0 || n.clientWidth | 0, originalHeight: /%/.test(n.height) ? 200 : n.height | 0 || n.clientHeight | 0, cache, }; fyteMap.set(div, fyte); fyte.srcEmbedFixed = fyte.srcEmbed.replace(/^http:/, 'https:') .replace(/([&?])(wmode=\w+|feature=oembed)&?/, '$1') .replace(/[&?]$/, ''); fyte.srcWatchFixed = fyte.srcEmbedFixed.replace('/embed/', '/watch?v=').replace(/(\?.*?)\?/, '$1&'); cache.lastUsed = new Date(); write(cache); if (cache.reason) div.setAttribute('disabled', ''); const divSize = calcContainerSize(div, n, fyte); const origStyle = getComputedStyle(n); overrideCSS(div, Object.assign( { height: persite && persite.eatparent === 0 ? '100%' : divSize.h + 'px', 'min-width': Math.min(divSize.w, fyte.originalWidth) + 'px', 'min-height': Math.min(divSize.h, fyte.originalHeight) + 'px', 'max-width': divSize.w + 'px', }, origStyle.transform && { transform: origStyle.transform, }, !autoplay && { 'background-color': 'transparent', transition: 'background-color 2s', }, // eslint-disable-next-line no-proto ...Object.keys(origStyle.hasOwnProperty('position') ? origStyle : origStyle.__proto__ /*FF*/) .filter(k => /^(position|left|right|top|bottom)$/.test(k) && !/^(auto|static|block)$/.test(origStyle[k])) .map(k => ({[k]: origStyle[k]})), origStyle.display === 'inline' && { display: 'inline-block', width: '100%', }, cfg.resize === 'Fit to width' && { width: '100%', })); if (!autoplay) { setTimeout(() => div.style.removeProperty('background-color')); setTimeout(() => div.style.removeProperty('transition'), 2000); } const wrapper = $create('div.wrapper', {}, [ img, $create('a.title', {target: '_blank', href: fyte.srcWatchFixed}, cache.title || cache.reason ? [ $create('strong', {}, cache.title || cache.reason || ''), cache.duration && $create('span', {}, cache.duration), cache.fps && $create('i', {}, `${cache.fps}fps`), ] : '\xA0'), (playbtn || initPlayButton()).cloneNode(true), $create('span.alternative', {}, _(`msgPlay${cfg.playHTML5 ? 'HTML5' : ''}`)), $create('div.storyboard', {hidden: !cfg.showStoryboard}), $create('div.options-button', {}, _('Options')), ]); div.appendChild(wrapper); fyteMap.set(img, autoplay); if (!autoplay && img.naturalWidth) img.onload(); n.replaceWith(div); if (!cache.title && !cache.reason || autoplay && cfg.playHTML5) fetchInfo.call(div); if (autoplay) { startPlaying(div); } else { div.addEventListener('click', clickHandler); div.addEventListener('mousedown', clickHandler); div.addEventListener('mouseenter', fetchInfo); } if (cfg.showStoryboard) div.addEventListener('mousemove', trackMouse); } function fetchInfo(e) { const fyte = fyteMap.get(this); fyte.mouseEvent = e; this.removeEventListener('mouseenter', fetchInfo); if (!fyte.storyboard) { const {id} = fyte.cache; GM_xmlhttpRequest({ method: 'GET', url: 'https://www.youtube.com/watch?v=' + id, context: this, onload: parseVideoInfo, }); } } function onCoverLoad(e) { if (this.naturalWidth <= 120) return this.onerror(e); this.style.opacity = ''; fyteMap.delete(this); } function onCoverError() { const {src} = this; const id = src.split('/')[4]; const src2 = getCoverUrl(id, 'sddefault.jpg'); this.src = src2 !== src ? src2 : getCoverUrl(id, 'hqdefault.jpg'); } function adjustNodesIfNeeded(e) { if (!fytedom[0]) return; if (adjustNodesIfNeeded.scheduled) clearTimeout(adjustNodesIfNeeded.scheduled); adjustNodesIfNeeded.scheduled = setTimeout(() => { adjustNodes(e); adjustNodesIfNeeded.scheduled = 0; }, 16); } function adjustNodes(event, clickedContainer) { const force = !!clickedContainer; let nearest = force ? clickedContainer : null; let nearestCenterYpct; const vids = $$('.instant-youtube-container:not([pinned]):not([stub])'); if (!nearest && event.type !== 'DOMContentLoaded') { let minDistance = window.innerHeight * 3 / 4 | 0; const nearTargetY = window.innerHeight / 2; for (const n of vids) { const bounds = n.getBoundingClientRect(); const distance = Math.abs((bounds.bottom + bounds.top) / 2 - nearTargetY); if (distance < minDistance) { minDistance = distance; nearest = n; } } } if (nearest) { const bounds = nearest.getBoundingClientRect(); nearestCenterYpct = (bounds.top + bounds.bottom) / 2 / window.innerHeight; } let resized = false; for (const n of vids) { const size = calcContainerSize(n); const w = size.w; const h = size.h; // prevent parent clipping for (let e = n.parentElement, style; e; e = e.parentElement) { if (e.style.overflow !== 'visible' && n.offsetTop < e.clientHeight / 2 && n.offsetTop + n.clientHeight > e.clientHeight && (style = getComputedStyle(e)) && /hidden|scroll/.test(style.overflow + style.overflowX + style.overflowY)) { overrideCSS(e, { overflow: 'visible', 'overflow-x': 'visible', 'overflow-y': 'visible', }); } } if (force && Math.abs(w - parseFloat(n.style.maxWidth)) <= 2) continue; overrideCSS(n, Object.assign({}, n.style.maxWidth !== `${w}px` && { 'max-width': `${w}px`, }, n.style.height !== h + 'px' && { height: h + 'px', }, parseFloat(n.style.minWidth) > w && { 'min-width': n.style.maxWidth, }, parseFloat(n.style.minHeight) > h && { 'min-height': n.style.height, })); resized = true; } if (resized && nearest) setTimeout(() => { const bounds = nearest.getBoundingClientRect(); const h = bounds.bottom - bounds.top; const projectedCenterY = nearestCenterYpct * window.innerHeight; const projectedTop = projectedCenterY - h / 2; const safeTop = Math.min(Math.max(0, projectedTop), window.innerHeight - h); window.scrollBy(0, bounds.top - safeTop); }, 16); } function calcContainerSize(div, origNode, fyte = fyteMap.get(div)) { if (!fyte) return; origNode = origNode || div; let w, h; let np = origNode.parentElement; const style = getComputedStyle(np); let parentWidth = parseFloat(style.width) - floatPadding(np, style, 'Left') - floatPadding(np, style, 'Right'); if (+style.columnCount > 1) parentWidth = (parentWidth + parseFloat(style.columnGap)) / style.columnCount - parseFloat(style.columnGap); switch (cfg.resize) { case 'Original': if (fyte.originalWidth === 320 && fyte.originalHeight === 200) { w = parentWidth; h = parentWidth / 16 * 9; } else { w = fyte.originalWidth; h = fyte.originalHeight; } break; case 'Custom': w = cfg.width; h = cfg.height; break; case '1080p': case '720p': case '480p': case '360p': h = parseInt(cfg.resize); w = h / 9 * 16; break; default: // fit-to-width mode // find parent node with nonzero width (i.e. independent of our video element) while (np && !(w = np.clientWidth)) np = np.parentNode; if (w) h = w / 16 * 9; else { w = origNode.clientWidth; h = origNode.clientHeight; } } if (parentWidth > 0 && parentWidth < w) { h *= parentWidth / w; w = parentWidth; } if (cfg.resize === 'Fit to width' && h < fyte.originalHeight * 0.9) h = Math.min(fyte.originalHeight, w / fyte.originalWidth * fyte.originalHeight); return {w: CHROME ? w : Math.round(w), h: h}; } function parseVideoInfo(response) { const div = response.context; const txt = response.responseText; const info = tryJSONparse(txt.match(/var\s+ytInitialPlayerResponse\s*=\s*({.+?});|$/)[1]) || {}; const {reason} = info.playabilityStatus || {}; const vid = info.videoDetails || {}; const streams = info.streamingData || {}; const fyte = fyteMap.get(div); const cache = fyte.cache; let shouldUpdateCache = false; const videoSources = []; const fmts = (streams.formats || streams.adaptiveFormats || []) .sort((a, b) => b.width - a.width || b.height - a.height); // parse width & height to adjust the thumbnail if (fmts.length && (cache.videoWidth !== fmts[0].width || cache.videoHeight !== fmts[0].height)) { cache.videoWidth = fmts[0].width; cache.videoHeight = fmts[0].height; shouldUpdateCache = true; } // parse video sources for (const f of fmts) { const codec = f.mimeType.match(/codecs="([^.]+)|$/)[1] || ''; const type = f.mimeType.split(/[/;]/)[1]; let src = f.url; if (!src && f.cipher) { const sp = {}; for (const str of f.cipher.split('&')) { const [k, v] = str.split('='); sp[k] = v; } src = decodeURIComponent(sp.url); if (sp.s) src += `&${sp.sp || 'sig'}=${decodeYoutubeSignature(sp.s)}`; } videoSources.push({ src, title: [ f.quality, f.qualityLabel !== f.quality ? f.qualityLabel : '', type + (codec ? `:${codec}` : ''), ].filter(Boolean).join(', '), }); } let fps = new Set(); for (const f of streams.adaptiveFormats || []) { if (f.fps) fps.add(f.fps); } fps = [...fps].join('/'); if (fps && cache.fps !== fps) { cache.fps = fps; shouldUpdateCache = true; } let duration = fyte.duration = vid.lengthSeconds | 0; if (duration) { duration = secondsToTimeString(duration); if (cache.duration !== duration) { cache.duration = duration; shouldUpdateCache = true; } } if (duration || fps) duration = `<span>${duration}</span>${fps ? `<i>${fps}fps</i>` : ''}`; const title = [ decodeURIComponent(vid.title || ''), reason && reason.replace(/\s*\.$/, ''), ].filter(Boolean).join(' | ').replace(/\+/g, ' '); if (title) { $('.instant-youtube-title', div).innerHTML = (title ? `<strong>${title}</strong>` : '') + duration; if (cache.title !== title) { cache.title = title; shouldUpdateCache = true; } } if (cfg.pinnable !== 'off' && vid.title) makeDraggable(div); if (reason) { div.setAttribute('disabled', ''); if (cache.reason !== reason) { cache.reason = reason; shouldUpdateCache = true; } } if (videoSources.length) fyte.videoSources = videoSources; if (txt.includes('playerStoryboardSpecRenderer') && info.storyboards && fyte.state !== 'scheduled play') { const m = info.storyboards.playerStoryboardSpecRenderer.spec.split('|'); const [w, h, len, rows, cols] = m[m.length - 1].split('#').map(Number); fyte.storyboard = {w, h, len, rows, cols}; if (w * h > 2000) { fyte.storyboard.url = m[0].replace('?', '&').replace( '$L/$N.jpg', `${m.length - 2}/M0.jpg?sigh=${m[m.length - 1].replace(/^.+?#([^#]+)$/, '$1')}`); const elSb = $('.instant-youtube-storyboard', div); if (elSb) { elSb.dataset.loaded = ''; elSb.appendChild(overrideCSS($create('div.sb-thumb', {}, '\xA0'), { width: w - 1 + 'px', height: h + 'px', })); if (cfg.showStoryboard) updateHoverHandler(div); } } } injectStylesIfNeeded(); if (fyte.state === 'scheduled play') setTimeout(startPlayingDirectly, 0, div); fyte.state = ''; try { const cover = vid.thumbnail.thumbnails.pop().url; if (cache.cover !== cover) { cache.cover = cover; shouldUpdateCache = true; const img = $('img', div); if (img.src && img.src !== cover) img.src = cover; } } catch (e) {} if (shouldUpdateCache) write(cache); } function decodeYoutubeSignature(s) { const a = s.split(''); a.reverse(); swap(a, 24); a.reverse(); swap(a, 41); a.reverse(); swap(a, 2); return a.join(''); } function swap(a, b) { const c = a[0]; a[0] = a[b % a.length]; a[b % a.length] = c; } function trackMouse(e) { fyteMap.get(this).mouseEvent = e; } function updateHoverHandler(div) { const fyte = fyteMap.get(div); const sb = fyte.storyboard; const elSb = $('.instant-youtube-storyboard', div); if (!cfg.showStoryboard) { elSb.hidden = true; return; } elSb.hidden = false; let oldIndex = null; const tracker = elSb.firstElementChild; const style = tracker.style; const sbImg = $create('img'); const spinner = $create('span.loading-spinner'); elSb.addEventListener('mousemove', storyboardHoverHandler); elSb.addEventListener('mouseout', storyboardHoverHandler); elSb.addEventListener('click', storyboardClickHandler, {once: true}); div.addEventListener('mouseover', storyboardPreloader); div.addEventListener('mouseout', storyboardPreloader); if (div.closest(':hover')) storyboardPreloader({}); function storyboardClickHandler(e) { const offsetX = e.offsetX || e.clientX - elSb.getBoundingClientRect().left; fyte.startAt = offsetX / elSb.clientWidth * fyte.duration | 0; fyte.srcEmbedFixed = setUrlParams(fyte.srcEmbedFixed, {start: fyte.startAt}); startPlaying(div, {alternateMode: e.shiftKey}); } function storyboardPreloader(e) { if (e.type === 'mouseout') { spinner.remove(); return; } const {len, rows, cols, preloaded} = sb || {}; const lastpart = (len - 1) / (rows * cols || 1) | 0; if (lastpart <= 0 || preloaded) return; let part = 0; $create('img', { src: setStoryboardUrl(part++), onload() { if (part <= lastpart) { this.src = setStoryboardUrl(part++); return; } sb.preloaded = true; div.removeEventListener('mouseover', storyboardPreloader); div.removeEventListener('mouseout', storyboardPreloader); this.onload = null; this.src = ''; spinner.remove(); }, }); if (elSb.matches(':hover') && fyte.mouseEvent) storyboardHoverHandler(fyte.mouseEvent); } function setStoryboardUrl(part) { return sb.url.replace(/M\d+\.jpg\?/, `M${part}.jpg?`); } function storyboardHoverHandler(e) { div.removeEventListener('mousemove', trackMouse); if (!cfg.showStoryboard || !sb) return; if (e.type === 'mouseout') { sbImg.onload && sbImg.onload(); return; } const {w, h, cols, rows, len, preloaded} = sb; const partlen = rows * cols; const offsetX = e.offsetX || e.clientX - elSb.getBoundingClientRect().left; const left = Math.min(elSb.clientWidth - w, Math.max(0, offsetX - w)) | 0; if (!style.left || parseInt(style.left) !== left) { style.left = `${left}px`; if (spinner.parentElement) spinner.style.cssText = important(`left:${left + w / 2 - 10}px; right:auto;`); } let index = Math.min(offsetX / elSb.clientWidth * (len + 1) | 0, len - 1); if (index === oldIndex) return; const part = index / partlen | 0; if (!oldIndex || part !== (oldIndex / partlen | 0)) { const url = setStoryboardUrl(part); style.setProperty('background-image', `url(${url})`, 'important'); if (!preloaded) { if (spinner.timer) clearTimeout(spinner.timer); spinner.timer = setTimeout(() => { spinner.timer = 0; if (!sbImg.src) return; elSb.appendChild(spinner); spinner.style.cssText = important(`left:${left + w / 2 - 10}px; right:auto;`); }, 50); sbImg.onload = () => { clearTimeout(spinner.timer); spinner.remove(); spinner.timer = 0; sbImg.onload = null; sbImg.src = ''; }; sbImg.src = url; } } tracker.dataset.time = secondsToTimeString(index / (len - 1 || 1) * fyte.duration | 0); oldIndex = index; index %= partlen; style.setProperty('background-position', `-${(index % cols) * w}px -${(index / cols | 0) * h}px`, 'important'); } } function clickHandler(e) { const el = e.target; if (el.closest('a') || e.type === 'mousedown' && e.button !== 1 || e.type === 'click' && el.matches('.instant-youtube-options, .instant-youtube-options *')) return; if (e.type === 'click' && el.matches('.instant-youtube-options-button')) { showOptions(e); e.preventDefault(); e.stopPropagation(); return; } e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); startPlaying(el.closest('.instant-youtube-container'), { alternateMode: e.shiftKey || el.matches('.instant-youtube-alternative'), fullscreen: e.button === 1, }); } function startPlaying(div, params) { div.removeEventListener('click', clickHandler); div.removeEventListener('mousedown', clickHandler); $$remove([ '.instant-youtube-alternative', '.instant-youtube-storyboard', '.instant-youtube-options-button', '.instant-youtube-options', ].join(','), div); $('svg', div).outerHTML = '<span class=instant-youtube-loading-spinner></span>'; if (cfg.pinnable !== 'off') { makePinnable(div); if (params && params.pin) $(`[pin="${params.pin}"]`, div).click(); } if (window !== top) parent.postMessage('iframe-allowfs', '*'); const fyte = fyteMap.get(div); if ((!!cfg.playHTML5 + !!(params && params.alternateMode) === 1) && (fyte.videoSources || fyte.state === 'querying')) { if (fyte.videoSources) startPlayingDirectly(div, params); else { // playback will start in parseVideoInfo fyte.state = 'scheduled play'; // fallback to iframe in 5s setTimeout(() => { if (fyte.state) { fyte.state = ''; switchToIFrame.call(div, params); } }, 5000); } } else switchToIFrame.call(div, params); } function startPlayingDirectly(div, params) { const switchTimer = setTimeout(switchToIFrame.bind(div, params), 5000); const video = $create('video.embed', { autoplay: true, controls: true, volume: GM_getValue('volume', 0.5), style: { position: 'absolute', left: 0, top: 0, right: 0, bottom: 0, padding: 0, margin: 'auto', opacity: 0, width: '100%', height: '100%', }, oncanplay() { this.oncanplay = null; const fyte = fyteMap.get(div); if (fyte.startAt && Math.abs(this.currentTime - fyte.startAt) > 1) this.currentTime = fyte.startAt; clearTimeout(switchTimer); pauseOtherVideos(this); if (params && params.fullscreen) return; div.setAttribute('playing', ''); div.firstElementChild.appendChild(this); overrideCSS(this, {opacity: 1}); }, onvolumechange() { GM_setValue('volume', this.volume); }, }); for (const src of fyteMap.get(div).videoSources || []) { video.appendChild($create('source', src)) .onerror = switchToIFrame.bind(div, params); } overrideCSS($('img', div), { transition: 'opacity 1s', opacity: '0', }); if (params && params.fullscreen) { div.firstElementChild.appendChild(video); div.setAttribute('playing', ''); video.style.opacity = 1; goFullscreen(video); } if (CHROME && +navigator.userAgent.match(/Chrom\D+(\d+)|$/)[1] < 74) video.addEventListener('click', () => setTimeout(() => video.paused ? video.play() : video.pause())); const title = $('.instant-youtube-title', div); if (title) { video.onpause = () => (title.hidden = false); video.onplay = () => (title.hidden = true); } } function switchToIFrame(params, e) { if (this.querySelector('iframe')) return; const div = this; const wrapper = div.firstElementChild; const fullscreen = params && params.fullscreen && !e; const fyte = fyteMap.get(div); if (e instanceof Event) { console.log('[FYTE] Direct linking canceled on %s, switching to IFRAME player', fyte.srcEmbed); const video = e.target ? e.target.closest('video') : e.composedPath().pop(); video.textContent = ''; goFullscreen(video, false); video.remove(); } const url = setUrlParams(fyte.srcEmbedFixed, { html5: 1, autoplay: 1, autohide: 2, border: 0, controls: 1, fs: 1, showinfo: 1, ssl: 1, theme: 'dark', enablejsapi: 1, local: 'true', quality: 'medium', FYTEfullscreen: fullscreen | 0, }); let iframe = $create('iframe.embed', { src: url, allow: 'autoplay; fullscreen', allowFullscreen: true, width: '100%', height: '100%', style: { position: 'absolute', top: 0, left: 0, right: 0, padding: 0, margin: 'auto', opacity: 0, border: 0, }, }); if (cfg.pinnable !== 'off') { $('[pin]', div).insertAdjacentElement('beforebegin', iframe); } else { wrapper.appendChild(iframe); } div.setAttribute('iframe', ''); div.setAttribute('playing', ''); iframe = $('iframe', div); if (fullscreen) { goFullscreen(iframe); overrideCSS(iframe, {opacity: 1}); } addEventListener('message', YTlistener); iframe.addEventListener('load', () => { iframe.contentWindow.postMessage('{"event":"listening"}', '*'); if (fyte.cache.reason) show(); }, {once: true}); setTimeout(show, 1000); function show() { overrideCSS(iframe, {opacity: 1}); $('.instant-youtube-title', div).hidden = true; } function YTlistener(e) { const data = e.source === iframe.contentWindow && e.data && tryJSONparse(e.data); if (!data || !data.info || data.info.playerState !== 1) return; removeEventListener('message', YTlistener); pauseOtherVideos(iframe); overrideCSS(iframe, {opacity: 1}); overrideCSS($('img', div), {display: 'none'}); $$remove('span, a', div); } } function getCoverUrl(id, name) { return `https://i.ytimg.com/vi${name.endsWith('.webp') ? '_webp' : ''}/${id}/${name}`; } function setUrlParams(url, params) { const u = new URL(url); for (const [k, v] of Object.entries(params)) u.searchParams.set(k, v); return u.href; } function pauseOtherVideos(activePlayer) { for (const v of $$('.instant-youtube-embed', activePlayer.ownerDocument)) { if (v === activePlayer) continue; switch (v.localName) { case 'video': if (!v.paused) v.pause(); break; case 'iframe': try { v.contentWindow.postMessage('{"event":"command", "func":"pauseVideo", "args":""}', '*'); } catch (e) {} break; } } } function goFullscreen(el, enable) { if (enable !== false) el.webkitRequestFullScreen && el.webkitRequestFullScreen() || el.mozRequestFullScreen && el.mozRequestFullScreen() || el.requestFullScreen && el.requestFullScreen(); else document.webkitCancelFullScreen && document.webkitCancelFullScreen() || document.mozCancelFullScreen && document.mozCancelFullScreen() || document.cancelFullScreen && document.cancelFullScreen(); } function makePinnable(div) { div.firstElementChild.insertAdjacentHTML('beforeend', '<div size-gripper></div>' + '<div pin="top-left"></div>' + '<div pin="top-right"></div>' + '<div pin="bottom-right"></div>' + '<div pin="bottom-left"></div>'); for (const pin of $$('[pin]', div)) { if (cfg.pinnable === 'hide') pin.setAttribute('transparent', ''); pin.onclick = pinClicked; } $('[size-gripper]', div).addEventListener('mousedown', startResize, true); function pinClicked() { const pin = this; const pinIt = !div.hasAttribute('pinned') || !pin.hasAttribute('active'); const corner = pin.getAttribute('pin'); const video = $('video', div); const paused = video.paused; const fyte = fyteMap.get(div); if (pinIt) { for (const p of $$('[pin][active]', div)) p.removeAttribute('active'); pin.setAttribute('active', ''); if (!fyte.unpinnedStyle) { fyte.unpinnedStyle = div.style.cssText; const stub = div.cloneNode(); const img = $('img', div).cloneNode(); img.style.opacity = 1; img.style.display = 'block'; img.title = ''; stub.appendChild(img); stub.onclick = e => $('[pin][active]', div).onclick(e); stub.style.setProperty('opacity', .3, 'important'); stub.setAttribute('stub', ''); fyte.stub = stub; div.parentNode.insertBefore(stub, div); } const size = constrainPinnedSize(div, localStorage.FYTEwidth || cfg.pinnedWidth); overrideCSS(div, { position: 'fixed', width: size.w + 'px', height: size.h + 'px', top: corner.includes('top') ? 0 : 'auto', left: corner.includes('left') ? 0 : 'auto', right: corner.includes('right') ? 0 : 'auto', bottom: corner.includes('bottom') ? 0 : 'auto', 'z-index': 999999999, }); adjustPinnedOffset(div, div, corner); div.setAttribute('pinned', corner); if (video && document.body) document.body.appendChild(div); } else { // unpin pin.removeAttribute('active'); div.removeAttribute('pinned'); div.style.cssText = fyte.unpinnedStyle; fyte.unpinnedStyle = ''; if (fyte.stub) { if (video && document.body) fyte.stub.parentNode.replaceChild(div, fyte.stub); fyte.stub.remove(); fyte.stub = null; } } if (paused) video.pause(); } function startResize(e) { const siteSaved = localStorage.FYTEwidth; let saveAs = siteSaved ? 'site' : 'global'; const oldSizeCSS = {w: div.style.width, h: div.style.height}; const oldDraggable = div.draggable; div.draggable = false; const gripper = this; gripper.removeAttribute('tried-exceeding'); gripper.innerHTML = `<div> <div save-as="${saveAs}"><b>S</b> = Site mode: <span>${getSiteOnlyText()}</span></div> ${!siteSaved ? '' : '<div><b>R</b> = Reset to global size</div>'} <div><b>Esc</b> = Cancel</div> </div>`; document.addEventListener('mousemove', resize); document.addEventListener('mouseup', resizeDone); document.addEventListener('keydown', resizeKeyDown); e.stopImmediatePropagation(); return false; function getSiteOnlyText() { return saveAs === 'site' ? `only ${location.hostname}` : 'global'; } function resize(e) { let deltaX = e.movementX || e.webkitMovementX || e.mozMovementX || 0; if (/right/.test(div.getAttribute('pinned'))) deltaX = -deltaX; const newSize = constrainPinnedSize(div, div.clientWidth + deltaX); if (newSize.w !== div.clientWidth) { div.style.setProperty('width', newSize.w + 'px', 'important'); div.style.setProperty('height', newSize.h + 'px', 'important'); gripper.removeAttribute('tried-exceeding'); } else if (newSize.triedExceeding) { gripper.setAttribute('tried-exceeding', ''); } window.getSelection().removeAllRanges(); return false; } function resizeDone() { div.draggable = oldDraggable; gripper.removeAttribute('tried-exceeding'); gripper.innerHTML = ''; document.removeEventListener('mousemove', resize); document.removeEventListener('mouseup', resizeDone); document.removeEventListener('keydown', resizeKeyDown); switch (saveAs) { case 'site': localStorage.FYTEwidth = div.clientWidth; break; case 'global': cfg.pinnedWidth = div.clientWidth; GM_setValue('pinnedWidth', cfg.pinnedWidth); // fallthrough to remove the locally saved value case 'reset': delete localStorage.FYTEwidth; break; case '': return false; } gripper.setAttribute('saveAs', saveAs); setTimeout(() => gripper.removeAttribute('saveAs'), 250); return false; } function resizeKeyDown(e) { switch (e.code) { case 'Escape': saveAs = 'cancel'; div.style.width = oldSizeCSS.w; div.style.height = oldSizeCSS.h; break; case 'KeyS': saveAs = saveAs === 'site' ? 'global' : 'site'; $('[save-as]', gripper).setAttribute('save-as', saveAs); $('[save-as] span', gripper).textContent = getSiteOnlyText(); return false; case 'KeyR': { if (!siteSaved) return; saveAs = 'reset'; const {w, h} = constrainPinnedSize(div, cfg.pinnedWidth); div.style.width = w; div.style.height = h; break; } default: return; } document.dispatchEvent(new MouseEvent('mouseup')); return false; } } } function makeDraggable(div) { div.draggable = true; div.addEventListener('dragstart', e => { const offsetY = e.offsetY || e.clientY - div.getBoundingClientRect().top; if (offsetY > div.clientHeight - 30) { e.preventDefault(); return; } e.dataTransfer.setData('text/plain', ''); let dropZone = $create('div.dragndrop-placeholder'); const fyte = fyteMap.get(div); const dropZoneHeight = 400 / fyte.cache.videoWidth * fyte.cache.videoHeight; document.body.addEventListener('dragenter', dragHandler); document.body.addEventListener('dragover', dragHandler); document.body.addEventListener('dragend', dragHandler); document.body.addEventListener('drop', dragHandler); function dragHandler(e) { e.stopImmediatePropagation(); e.stopPropagation(); e.preventDefault(); switch (e.type) { case 'dragover': { const playing = div.hasAttribute('playing'); const stub = e.target.closest('.instant-youtube-container[stub]') === fyte.stub && fyte.stub; const gizmo = playing && !stub ? {left: 0, top: 0, right: innerWidth, bottom: innerHeight} : (stub || div).getBoundingClientRect(); const x = e.clientX; const y = e.clientY; const cx = (gizmo.left + gizmo.right) / 2; const cy = (gizmo.top + gizmo.bottom) / 2; const stay = !!stub || y >= cy - 200 && y <= cy + 200 && x >= cx - 200 && x <= cx + 200; overrideCSS(dropZone, { top: y < cy || stay ? '0' : 'auto', bottom: y > cy || stay ? '0' : 'auto', left: x < cx || stay ? '0' : 'auto', right: x > cx || stay ? '0' : 'auto', width: playing && stay && stub ? stub.clientWidth + 'px' : '400px', height: playing && stay && stub ? stub.clientHeight + 'px' : dropZoneHeight + 'px', margin: playing && stay ? 'auto' : '0', position: !playing && stay || stub ? 'absolute' : 'fixed', 'background-color': stub ? 'rgba(0,0,255,0.5)' : stay ? 'rgba(255,255,0,0.4)' : 'rgba(0,255,0,0.2)', }); adjustPinnedOffset(dropZone, div); (stay && !playing || stub ? (stub || div) : document.body).appendChild(dropZone); break; } case 'dragend': case 'drop': { const corner = calcPinnedCorner(dropZone); dropZone.remove(); dropZone = null; document.body.removeEventListener('dragenter', dragHandler); document.body.removeEventListener('dragover', dragHandler); document.body.removeEventListener('dragend', dragHandler); document.body.removeEventListener('drop', dragHandler); if (e.type === 'dragend') break; if (div.hasAttribute('playing')) (corner ? $(`[pin="${corner}"]`, div) : fyte.stub).click(); else startPlaying(div, {pin: corner}); } } } }); } function adjustPinnedOffset(el, self, corner) { let offset = 0; if (!corner) corner = calcPinnedCorner(el); for (const pin of $$(`.instant-youtube-container[pinned] [pin="${corner}"][active]`)) { const container = pin.closest('[pinned]'); if (container !== el && container !== self) { const {top, bottom} = container.getBoundingClientRect(); offset = Math.max(offset, el.style.top === '0px' ? bottom : innerHeight - top); } } if (offset) el.style[el.style.top === '0px' ? 'top' : 'bottom'] = offset + 'px'; } function calcPinnedCorner(el) { const t = el.style.top !== 'auto'; const l = el.style.left !== 'auto'; const r = el.style.right !== 'auto'; const b = el.style.bottom !== 'auto'; return t && b && l && r ? '' : `${t ? 'top' : 'bottom'}-${l ? 'left' : 'right'}`; } function constrainPinnedSize(div, width) { const fyte = fyteMap.get(div); const maxWidth = window.innerWidth - 100 | 0; const triedExceeding = (width | 0) > maxWidth; width = Math.max(200, Math.min(maxWidth, width | 0)); return { w: width, h: width / fyte.cache.videoWidth * fyte.cache.videoHeight, triedExceeding, }; } function showOptions(e) { const [options] = translateHTML(` <div class=instant-youtube-options> <span> <label tl style="width: 100% !important;">Size: <select data-action=resize> <option tl value=Original>Original <option tl value="Fit to width">Fit to width <option>360p <option>480p <option>720p <option>1080p <option tl value=Custom>Custom... </select> </label> <label data-action=resize-custom ${cfg.resize !== 'Custom' ? 'disabled' : ''}> <input type=number min=320 max=9999 tl-placeholder=width data-action=width step=1> x <input type=number min=240 max=9999 tl-placeholder=height data-action=height step=1> </label> </span> <label tl=content,title title=msgStoryboardTip> <input data-action=showStoryboard type=checkbox> msgStoryboard </label> <span> <label tl=content,title title=msgDirectTip> <input data-action=playHTML5 type=checkbox> msgDirect </label> <label tl=content,title title=msgDirectTip> <input data-action=playHTML5Shown type=checkbox> msgDirectShown </label> </span> <label tl=content,title title=msgSafeTip> <input data-action=skipCustom type=checkbox> msgSafe </label> <table> <tr> <td><label tl=content,title title=msgPinningTip>msgPinning</label></td> <td> <select data-action=pinnable> <option tl value=on>msgPinningOn <option tl value=hide>msgPinningHover <option tl value=off>msgPinningOff </select> </td> </tr> </table> <span data-action=buttons> <button tl data-action=ok>OK</button> <button tl data-action=cancel>Cancel</button> </span> </div> `); for (const [k, v] of Object.entries(cfg)) { const el = $(`[data-action=${k}]`, options); if (el) el[el.type === 'checkbox' ? 'checked' : 'value'] = v; } $('[data-action=resize]', options).onchange = function () { const v = this.value !== 'Custom'; const e = $('[data-action=resize-custom]', options); e.children[0].disabled = e.children[1].disabled = v; v ? e.setAttribute('disabled', '') : e.removeAttribute('disabled'); }; $('[data-action=buttons]', options).onclick = e => { const btn = e.target; if (btn.dataset.action !== 'ok') { options.remove(); return; } let shouldAdjust; const oldCfg = Object.assign({}, cfg); for (const [k, v] of Object.entries(cfg)) { const el = $(`[data-action=${k}]`, options); const newVal = el && ( el.type === 'checkbox' ? el.checked : el.type === 'number' ? el.valueAsNumber : el.value); if (newVal != null && newVal !== v) { GM_setValue(k, newVal); cfg[k] = newVal; shouldAdjust = true; } } options.remove(); if (cfg.resize === 'Custom' && (cfg.width !== oldCfg.width || cfg.height !== oldCfg.height)) updateCustomSize(cfg.width, cfg.height); if (cfg.showStoryboard !== oldCfg.showStoryboard) $$('.instant-youtube-container').forEach(updateHoverHandler); if (cfg.playHTML5 !== oldCfg.playHTML5 && cfg.playHTML5Shown) { const alt = _(`msgPlay${cfg.playHTML5 ? '' : 'HTML5'}`); for (const e of $$('.instant-youtube-alternative')) e.textContent = alt; } if (cfg.playHTML5Shown !== oldCfg.playHTML5Shown) updateAltPlayerCSS(); if (shouldAdjust) adjustNodes(e, btn.closest('.instant-youtube-container')); }; e.target.insertAdjacentElement('afterend', options); } function updateCustomSize(w, h) { cfg.width = Math.min(9999, Math.max(320, w | 0 || cfg.width | 0)); cfg.height = Math.min(9999, Math.max(240, h | 0 || cfg.height | 0)); } function updateAltPlayerCSS() { const ALT = '.instant-youtube-alternative:not(#foo)'; styledom.textContent = styledom.textContent.split(ALT)[0] + /*language=CSS*/ `${ALT} { display: ${cfg.playHTML5Shown ? 'block' : 'none'} !important; }`; } function important(cssText, rx = /;/g) { return cssText.replace(rx, '!important;'); } function $(sel, base = document) { return base.querySelector(sel) || 0; } function $$(sel, base = document) { return [...base.querySelectorAll(sel)]; } function $create(tagCls, props, children) { const [tag, cls] = tagCls.split('.'); const el = Object.assign(document.createElement(tag), props); if (cls) el.className = `instant-youtube-${cls}`; if (props && typeof props.style === 'object') overrideCSS(el, props.style); if (children && typeof children !== 'object') children = document.createTextNode(children); if (children instanceof Node) el.appendChild(children); else if (Array.isArray(children)) el.append(...children.filter(Boolean)); return el; } function $$remove(sel, base = document) { for (const el of base.querySelectorAll(sel)) el.remove(); } function overrideCSS(el, props) { const names = Object.keys(props); el.style.cssText = el.style.cssText .replace(new RegExp(`(^|\\s|;)(${names.join('|')})(:[^;]+)`, 'gi'), '$1') .replace(/[^;]\s*$/, '$&;') .replace(/^\s*;\s*/, '') + names.map(n => `${n}:${props[n]}!important;`).join(' '); return el; } // fix dumb Firefox bug function floatPadding(node, style, dir) { const padding = style['padding' + dir]; if (padding.indexOf('%') < 0) return parseFloat(padding); return parseFloat(padding) * (parseFloat(style.width) || node.clientWidth) / 100; } async function cleanupStorage() { cleanupStorage.timer = 0; const cutoff = Date.now() - CACHE_STALE_DURATION; // TODO: remove localStorage in 2024 for (const k in localStorage) { if (k.startsWith(CACHE_PREFIX)) { try { const str = localStorage[k]; const isObj = str[0] === '{'; const val = isObj ? tryJSONparse(str) || false : str; const time = isObj ? val.lastUsed : parseInt(val, 36) * 1000; if (time > cutoff) { const res = isObj ? (val.lastUsed = new Date(time), val) : unpack(val, k.slice(CACHE_PREFIX.length)); write(res); } } catch (e) {} } } // TODO: remove GM_listValues in 2024 for (const k of GM_listValues()) if (k.startsWith('cache-')) GM_deleteValue(k); try { /** @type {IDBIndex} */ const store = await db({raw: true, write: true, index: 'lastUsed'}); const req = store.openCursor(IDBKeyRange.upperBound(new Date(cutoff))); req.onerror = console.warn; req.onsuccess = () => { const cur = /** @type {IDBCursorWithValue} */ req.r###lt; if (!cur) return; cur.delete().onerror = console.warn; cur.continue(); }; } catch (e) { console.warn(e); } await write().catch(console.warn); for (const k in localStorage) if (k.startsWith(CACHE_PREFIX)) delete localStorage[k]; } function initDB() { db = TinyIDB('FYTE', { onUpgrade() { this.r###lt .createObjectStore('data', {keyPath: 'id'}) .createIndex('lastUsed', 'u'); }, }); cleanupStorage.timer = setTimeout(cleanupStorage, 1e3); return db; } async function read(ids) { const toRead = []; const toWrite = []; for (const id of ids) { const str = localStorage[CACHE_PREFIX + id]; if (str) { toWrite.push(id); unpack(str, id); } else { toRead.push(id); } } if (toRead.length) { for (const val of await (db || initDB()).getMulti(toRead)) if (val) unpack(val); } if (toWrite.length) { if (!cleanupStorage.timer) cleanupStorage.timer = setTimeout(cleanupStorage, 1000); toWrite.forEach(write); } } function unpack(data, id) { const old = !!id; const arr = (old ? data : data.a).split('\n'); const obj = { id: id || (id = data.id), lastUsed: old ? new Date(parseInt(arr.shift(), 36) * 1000) : data.u, }; for (let j = 0; j < CACHE_PROPS.length; j++) { const [key, type] = CACHE_PROPS[j]; const v = arr[j] || ''; obj[key] = type === 0 ? parseInt(v, 36) || 0 : key === 'cover' ? getCoverUrl(id, v) || '' : v; } return (dbCache[id] = obj); } function write(data) { if (data) { if (!write.timer) write.timer = setTimeout(write, dbFlushDelay); const id = typeof data === 'object' ? data.id : data; dbCache[id] = data; dbFlush.add(id); return; } const toWrite = []; for (const id of dbFlush) { const obj = dbCache[id]; let res = ''; for (const [key, type] of CACHE_PROPS) { const v = obj[key]; // not storing array values separately to minimize byte size res += `${res ? '\n' : ''}${!v ? '' : type === 0 ? (+v).toString(36) : key === 'cover' ? v.split('?')[0].split('/').pop() : v.replace(/\n/g, ' ')}`; } toWrite.push({ a: res, u: obj.lastUsed, id, }); } dbFlush.clear(); write.timer = 0; return toWrite.length ? (db || initDB()).putMulti(toWrite) : Promise.resolve(); } function tryJSONparse(s) { try { return JSON.parse(s); } catch (e) {} } function secondsToTimeString(sec) { const h = sec / 3600 | 0; const m = (sec / 60 | 0) % 60; const s = sec % 60; return `${h ? h + ':' : ''}${h && m < 10 ? 0 : ''}${m}:${s < 10 ? 0 : ''}${s}`; } function translateHTML(html) { const tmp = $create('div', {innerHTML: html.trim().replace(/\n\s*/g, '')}); for (const node of $$('[tl]', tmp)) { for (const what of (node.getAttribute('tl') || 'content').split(',')) { let child; if (what === 'content') { for (const n of [...node.childNodes].reverse()) { if (n.nodeType === Node.TEXT_NODE && n.textContent.trim()) { child = n; break; } } } else child = node.getAttributeNode(what); if (!child) continue; const src = child.textContent; const srcTrimmed = src.trim(); const tl = src.replace(srcTrimmed, _(srcTrimmed)); if (src !== tl) child.textContent = tl; } } return [...tmp.childNodes]; } function initTL() { const tlSource = { msgWatch: { en: 'watch on Youtube', ru: 'открыть на Youtube', }, msgPlay: { en: 'Play with Youtube player', ru: 'Включить плеер Youtube', }, msgPlayHTML5: { en: 'Play directly (up to 720p)', ru: 'Включить напрямую (макс. 720p)', }, msgAltPlayerHint: { en: 'Shift-click to use alternative player', ru: 'Shift-клик для смены типа плеера', }, Options: { ru: 'Опции', }, 'Size:': { ru: 'Размер:', }, Original: { ru: 'Исходный', }, 'Fit to width': { ru: 'На всю ширину', }, 'Custom...': { ru: 'Настроить...', }, width: { ru: 'ширина', }, height: { ru: 'высота', }, msgStoryboard: { en: 'Storyboard thumbnails on hover', ru: 'Раскадровка при наведении курсора', }, msgStoryboardTip: { en: 'Show storyboard preview on mouse hover at the bottom', ru: 'Показывать миникадры при наведении мыши на низ кавер-картинки', }, msgDirect: { en: 'Play directly', ru: 'Встроенный плеер браузера', }, msgDirectTip: { en: 'Shift-click a thumbnail to use the alternative player', ru: 'Удерживайте клавишу Shift при щелчке на картинке для альтернативного плеера', }, msgDirectShown: { en: 'Show under play button', ru: 'Показывать под кнопкой ►', }, msgInvidious: { en: 'Use https://invidio.us to play videos', ru: 'Использовать https://invidio.us в плеере', }, msgSafe: { en: 'Safe (skip videos with enablejsapi=1)', ru: 'Консервативный режим', }, msgSafeTip: { en: 'Do not process customized videos with enablejsapi=1 parameter (requires page reload)', ru: 'Не обрабатывать нестандартные видео с параметром enablejsapi=1 ' + '(подействует после обновления страницы)', }, msgPinning: { en: 'Corner pinning', ru: 'Закрепление по углам', }, msgPinningTip: { en: 'Enable corner pinning controls when a video is playing.\n' + 'To restore the video click the active corner pin or the original video placeholder.', ru: 'Включить шпильки по углам для закрепления видео во время просмотра.\n' + 'Для отмены можно нажать еще раз на активированный угол или на заглушку, ' + 'где исходно было видео', }, msgPinningOn: { en: 'On', ru: 'Да', }, msgPinningHover: { en: 'On, hover a corner to show', ru: 'Да, при наведении курсора', }, msgPinningOff: { en: 'Off', ru: 'Нет', }, OK: { ru: 'ОК', }, Cancel: { ru: 'Оменить', }, }; const browserLang = navigator.language || navigator.languages && navigator.languages[0] || ''; const browserLangMajor = browserLang.replace(/-.+/, ''); const tl = {}; for (const k of Object.keys(tlSource)) { const langs = tlSource[k]; const text = langs[browserLang] || langs[browserLangMajor]; if (text) tl[k] = text; } return src => tl[src] || src; } function initPlayButton() { [playbtn] = translateHTML(` <svg class="instant-youtube-play-button"> <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> <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" points="33.3,41.4 33.3,17.75 56,29.6"></polygon> </svg>`); return playbtn; } /** * @param {string} dbName * @param {Object} [opts] * @param {string} [opts.store] * @param {string} [opts.index] * @param {number} [opts.timeout] * @param {number} [opts.version] * @param {(this:IDBOpenDBRequest, e:IDBVersionChangeEvent)=>void} [opts.onUpgrade] * @returns {TinyIDB} */ function TinyIDB(dbName, { store = 'data', index, version, timeout = 250, onUpgrade = function (evt) { if (store) evt.target.r###lt.createObjectStore(store); }, } = {}) { /** @typedef {TinyIDBSource | ((config?:TinyIDBConfig) => Promise<TinyIDBSource>)} TinyIDB */ /** @typedef {number|string|Date|BufferSource|IDBKeyRange} IDBKey */ /** @typedef {IDBObjectStore | IDBIndex | { * getMulti: (keys: IDBKey[]) => ?[] * putMulti: (values: Object[] | [val:?, key:IDBKey][]) => IDBKey[] * }} TinyIDBSource */ /** @typedef TinyIDBConfig * @prop {boolean} [raw] - returns the raw IDBObjectStore | IDBIndex object * @prop {boolean} [write] * @prop {string} [store] * @prop {string} [index] */ let timer; /** @type {IDBDatabase} */ let db; let dbPromise; const RW = ['add', 'clear', 'delete', 'put', 'putMulti']; const handler = { apply: proxyApply, get: (cfg, method) => proxyGet.bind(null, cfg, method), }; return new Proxy(TinyIDB, handler); function proxyApply(_, thisObj, [cfg]) { return cfg && cfg.raw ? proxyGet(cfg) : new Proxy(cfg || {}, handler); } /** * @param {TinyIDBConfig} cfg * @param {keyof IDBObjectStore | 'getMulti' | 'putMulti'} method * @param {...?[]} args * @return {Promise<?>} */ async function proxyGet(cfg, method, ...args) { if (!db) await (dbPromise || open()); const { raw, index: txIndex = index, store: txStore = store, write = RW.includes(method), } = cfg; const arg = args[0]; const isMulti = !raw && (method === 'getMulti' || method === 'putMulti'); if (isMulti) { if (!Array.isArray(arg)) throw new Error('Argument must be an array'); if (!arg.length) return []; } const tx = db.transaction(txStore, write ? 'readwrite' : 'readonly'); let source = tx.objectStore(txStore); if (timer) clearTimeout(timer); if (txIndex) source = source.index(txIndex); if (timeout) tx.oncomplete = tx.onerror = closeLater; if (raw) return source; let req, resolve, reject; const promise = new Promise((ok, ko) => (resolve = ok) && (reject = ko)); if (isMulti) { source._r###lts = []; method = method.slice(0, 3); const addKey = method === 'put' && !source.keyPath; for (let i = 0, val, len = arg.length; i < len; i++) { val = arg[i]; req = addKey ? source[method](val[0], val[1]) : source[method](val); req.onsuccess = onExecMulti; req.onerror = reject; } } else { req = source[method](...args); req.onsuccess = onExec; req.onerror = reject; } req._resolve = resolve; return promise; } function onExec() { this._resolve(this.r###lt); } function onExecMulti() { const {r###lt, _resolve: cb, source: {_r###lts: arr}} = this; arr.push(r###lt); if (cb) cb(arr); } function open() { dbPromise = new Promise((resolve, reject) => { const op = indexedDB.open(dbName, version); op.onupgradeneeded = onUpgrade; op.onsuccess = onOpened; op.onerror = reject; op._resolve = resolve; }); return dbPromise; } function onOpened() { this._resolve(db = this.r###lt); dbPromise = null; } function closeLater() { timer = setTimeout(closeNow, timeout); } function closeNow() { timer = 0; if (db) { db.close(); db = null; } } } function injectStylesIfNeeded(force) { if (!fytedom[0] && !force) return; styledom = styledom || GM_addStyle(/*language=CSS*/ ` [class^="instant-youtube-"]:not(#foo) { margin: 0; padding: 0; transform: none; } ` + important(/*language=CSS*/ ` .instant-youtube-container { contain: strict; display: block; position: relative; overflow: hidden; cursor: pointer; margin: auto; font: normal 14px/1.0 sans-serif, Arial, Helvetica, Verdana; text-align: center; background: black; break-inside: avoid-column; } .instant-youtube-container[disabled] { background: #888; } .instant-youtube-container[disabled] .instant-youtube-storyboard { display: none; } .instant-youtube-container[pinned] { box-shadow: 0 0 30px black; } .instant-youtube-container[playing] { contain: none; } .instant-youtube-wrapper { width: 100%; height: 100%; } .instant-youtube-play-button { display: block; position: absolute; width: 85px; height: 60px; left: 0; right: 0; top: 0; bottom: 0; margin: auto; } .instant-youtube-loading-spinner { display: block; position: absolute; width: 20px; height: 20px; left: 0; right: 0; top: 0; bottom: 0; margin: auto; pointer-events: none; background: url("data:image/gif;base64,R0lGODlhFAAUAJEDAMzMzLOzs39/f////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQFCgADACwAAAAAFAAUAAACPJyPqcuNItyCUJoQBo0ANIxpXOctYHaQpYkiHfM2cUrCNT0nqr4uudsz/IC5na/2Mh4Hu+HR6YBaplRDAQAh+QQFCgADACwEAAIADAAGAAACFpwdcYupC8BwSogR46xWZHl0l8ZYQwEAIfkEBQoAAwAsCAACAAoACgAAAhccMKl2uHxGCCvO+eTNmishcCCYjWEZFgAh+QQFCgADACwMAAQABgAMAAACFxwweaebhl4K4VE6r61DiOd5SfiN5VAAACH5BAUKAAMALAgACAAKAAoAAAIYnD8AeKqcHIwwhGntEWLkO3###4biNEIFACH5BAUKAAMALAQADAAMAAYAAAIWnDSpAHa4GHgohCHbGdbipnBdSHphAQAh+QQFCgADACwCAAgACgAKAAACF5w0qXa4fF6KUoVQ75UaA7Bs3yeNYAkWACH5BAUKAAMALAIABAAGAAwAAAIXnCU2iMfaRghqTmMp1moAoHyfIYIkWAAAOw=="); } .instant-youtube-container:hover .ytp-large-play-button-svg { fill: #CC181E; } .instant-youtube-alternative { display: block; position: absolute; width: 20em; height: 20px; top: 50%; left: 0; right: 0; margin: 60px auto; border: none; text-align: center; text-decoration: none; text-shadow: 1px 1px 3px black; color: white; z-index: 8; font-weight: normal; font-size: 12px; } .instant-youtube-alternative:hover { text-decoration: underline; color: white; background: transparent; } .instant-youtube-embed { z-index: 10; background: transparent; transition: opacity .25s; } .instant-youtube-title { z-index: 20; display: block; position: absolute; width: auto; top: 0; left: 0; right: 0; padding: 7px; border: none; text-shadow: 1px 1px 2px black; text-align: center; text-decoration: none; color: white; background-color: #0008; } .instant-youtube-title strong { font: bold 14px/1.0 sans-serif, Arial, Helvetica, Verdana; } .instant-youtube-title strong::after { content: " - ${_('msgWatch')}"; font-weight: normal; margin-right: 1ex; } .instant-youtube-title span { color: white; } .instant-youtube-title span::before { content: "("; } .instant-youtube-title span::after { content: ")"; } .instant-youtube-title i::before { content: ", "; } .instant-youtube-container .instant-youtube-title i { all: unset; opacity: .5; font-style: normal; color: white; } @-webkit-keyframes instant-youtube-fadein { from { opacity: 0 } to { opacity: 1 } } @-moz-keyframes instant-youtube-fadein { from { opacity: 0 } to { opacity: 1 } } @keyframes instant-youtube-fadein { from { opacity: 0 } to { opacity: 1 } } .instant-youtube-container:not(:hover) .instant-youtube-title[hidden] { display: none; } .instant-youtube-title:hover { text-decoration: underline; } .instant-youtube-title strong { color: white; } .instant-youtube-options-button { opacity: 0.6; position: absolute; right: 0; bottom: 0; padding: 1.5ex 2ex; font-size: 11px; text-shadow: 1px 1px 2px black; color: white; } .instant-youtube-options-button:hover { opacity: 1; background: rgba(0, 0, 0, 0.5); } .instant-youtube-options { display: flex; position: absolute; right: 0; bottom: 0; padding: 1ex 1ex 2ex 2ex; flex-direction: column; align-items: flex-start; line-height: 1.5; text-align: left; opacity: 1; color: white; background: black; z-index: 999; } .instant-youtube-options * { width: auto; height: auto; font: inherit; font-size: 13px; vertical-align: middle; text-transform: none; text-align: left; border-radius: 0; text-decoration: none; color: white; background: black; } .instant-youtube-options > * { margin-top: 1ex; } .instant-youtube-options table { all: unset; display: table; } .instant-youtube-options tr { all: unset; display: table-row; } .instant-youtube-options td { all: unset; display: table-cell; padding: 2px; } .instant-youtube-options label > * { display: inline; } .instant-youtube-options select { padding: .5ex .25ex; border: 1px solid #444; -webkit-appearance: menulist; } .instant-youtube-options [data-action="resize-custom"] input { width: 9ex; padding: .5ex .5ex .4ex; border: 1px solid #666; } .instant-youtube-options [data-action="buttons"] { margin-top: 1em; } .instant-youtube-options button { margin: 0 1ex 0 0; padding: .5ex 2ex; border: 2px solid gray; font-weight: bold; } .instant-youtube-options button:hover { border-color: white; } .instant-youtube-options label[disabled] { opacity: 0.25; } .instant-youtube-storyboard { height: 33%; max-height: 90px; display: block; position: absolute; left: 0; right: 0; bottom: 0; overflow: visible; transition: background-color .5s .25s; } .instant-youtube-storyboard[data-loaded]:hover { background-color: #0004; } .instant-youtube-storyboard div { display: block; position: absolute; bottom: 0; pointer-events: none; border: 3px solid #888; box-shadow: 2px 2px 10px black; transition: opacity .25s ease; background-color: transparent; background-origin: content-box; opacity: 0; } .instant-youtube-storyboard div::after { content: attr(data-time); opacity: .5; color: #fff; background-color: #000; font-weight: bold; font-size: 10px; position: absolute; bottom: 4px; left: 4px; padding: 1px 3px; } .instant-youtube-storyboard:hover div { opacity: 1; } .instant-youtube-thumbnail { all: initial; width: 100%; height: 100%; max-height: 100%; overflow: hidden; object-fit: cover; transition: opacity .5s ease-out; } .instant-youtube-container [pin] { display: block; position: absolute; width: 0; height: 0; top: auto; bottom: auto; left: auto; right: auto; border-style: solid; transition: opacity 2.5s ease-in, opacity 0.4s ease-out; opacity: 0; z-index: 100; } .instant-youtube-container[playing]:hover [pin]:not([transparent]) { opacity: 1; } .instant-youtube-container[playing] [pin]:hover { cursor: alias; opacity: 1; transition: opacity 0s; } .instant-youtube-container [pin=top-left][active] { border-top-color: green; } .instant-youtube-container [pin=top-left]:hover { border-top-color: #fc0; } .instant-youtube-container [pin=top-left] { top: 0; left: 0; border-width: 10px 10px 0 0; border-color: red transparent transparent transparent; } .instant-youtube-container [pin=top-left][transparent] { border-width: 10px 10px 0 0; } .instant-youtube-container [pin=top-right][active] { border-right-color: green; } .instant-youtube-container [pin=top-right]:hover { border-right-color: #fc0; } .instant-youtube-container [pin=top-right] { top: 0; right: 0; border-width: 0 10px 10px 0; border-color: transparent red transparent transparent; } .instant-youtube-container [pin=top-right][transparent] { border-width: 0 10px 10px 0; } .instant-youtube-container [pin=bottom-right][active] { border-bottom-color: green; } .instant-youtube-container [pin=bottom-right]:hover { border-bottom-color: #fc0; } .instant-youtube-container [pin=bottom-right] { bottom: 0; right: 0; border-width: 0 0 10px 10px; border-color: transparent transparent red transparent; } .instant-youtube-container [pin=bottom-right][transparent] { border-width: 0 0 10px 10px; } .instant-youtube-container [pin=bottom-left][active] { border-left-color: green; } .instant-youtube-container [pin=bottom-left]:hover { border-left-color: #fc0; } .instant-youtube-container [pin=bottom-left] { bottom: 0; left: 0; border-width: 10px 0 0 10px; border-color: transparent transparent transparent red; } .instant-youtube-container [pin=bottom-left][transparent] { border-width: 10px 0 0 10px; } .instant-youtube-dragndrop-placeholder { z-index: 999999999; background: rgba(0, 255, 0, 0.1); border: 2px dotted green; box-sizing: border-box; pointer-events: none; } .instant-youtube-container [size-gripper] { width: 0; position: absolute; top: 0; bottom: 0; cursor: e-resize; border-color: rgba(50,100,255,0.5); border-width: 12px; background: rgba(50,100,255,0.2); z-index: 99; opacity: 0; transition: opacity .1s ease-in-out, border-color .1s ease-in-out; } .instant-youtube-container[pinned*="right"] [size-gripper] { border-style: none none none solid; left: -4px; } .instant-youtube-container[pinned*="left"] [size-gripper] { border-style: none solid none none; right: -4px; } .instant-youtube-container [size-gripper]:hover { opacity: 1; } .instant-youtube-container [size-gripper]:active { opacity: 1; width: auto; left: -4px; right: -4px; } .instant-youtube-container [size-gripper][tried-exceeding] { border-color: rgba(255,0,0,0.5); } .instant-youtube-container [size-gripper][saveAs="global"] { border-color: rgba(0,255,0,0.5); } .instant-youtube-container [size-gripper][saveAs="site"] { border-color: rgba(0,255,255,0.5); } .instant-youtube-container [size-gripper][saveAs="reset"] { border-color: rgba(255,255,0,0.5); } .instant-youtube-container [size-gripper][saveAs="cancel"] { border-color: rgba(255,0,255,0.25); } .instant-youtube-container [size-gripper] > div { white-space: nowrap; color: white; font-weight: normal; line-height: 1.25; text-align: left; position: absolute; top: 50%; padding: 1ex 1em 1ex; background-color: rgba(80,150,255,0.5); } .instant-youtube-container [size-gripper] [save-as="site"] { font-weight: bold; color: yellow; } .instant-youtube-container[pinned*="left"] [size-gripper] > div { right: 0; } `, /;\n/g).replace(/(::\w+)?\s+{/g, ':not(#_)$1 {')); // move our rules to the end of HEAD to increase CSS specificity if (styledom.nextElementSibling && document.head) document.head.appendChild(styledom); updateAltPlayerCSS(); }