// ==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:ru На порядок ускоряет время загрузки страниц с большим количеством вставленных Youtube-видео. С первого момента загрузки страницы появляются заглушки для видео, которые можно щелкнуть для загрузки плеера, и почти сразу же появляются кавер-картинки с названием видео. В опциях можно включить режим использования упрощенного браузерного плеера (макс. 720p). // // @version 2.12.5 // // @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* // // @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,iVBORw0KGgoAAAANSUhEUgAAAIAAAACABAMAAAAxEHz4AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAwUExURUxpcQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJuxkb8AAAAPdFJOUwDvH0biMvjOZFW6pxJ6kh7r3iMAAAPDSURBVGje7ZlNaBNBFMeXhNDWpkKgFg9iYlBB6SGwiPQkftaCYATjTaRKiwi1xVaKXpqTpHhosR66p4pQhb209CQGbSweK/TiRYwfFy+NkWBM2pR2nHm73abJzuceRMj/kEzSvl92Z9689+atpjXUUEMN1WgpoRupbH41nTbNUaxzlkIhe0C+M810Ov8zmzL0RGeNeeDThUEkof72N/Fqe/8LJK07sR173yJS0EbEATxFSurZtm9DilqxAV9VAZuWfbPbLBOFqtSBP9f/WxIAV2Bc6H5owiKPG7p+IpFIRG11LsPbEfyVrhvTqeyX1dfmaBiM9gFgjgwrTzJSfncMFq7s3EExJuu5/rHte3hPBvfkff84sbuEBxPkUiLygCC5hDV7CvpUtt81axICZBN9UwHsxYalOMxhIaIC8IVhFlvJtlALIWQl57Um/LquBpjBpkOwin1qADKLB7RD9moqiPz2TcAMqQGa4OI9Av5op/DrMzXAHmz6mw4IxEQA67AW825/bhngAVoBMEHzZD+aFQCsQUCkAAor/M2wCYAVdwCqxJmANgD8cmJjPQDt5wK22AD0nAVoBsAiE1BMcgAbAJikAqoTYP1CA4BEtBgdgC6yARUuAC3QI7sDiLMAxUk2YAwiIwNAn4YAhGU+YKcOqAUMCgJQziugHGMALmNAhANAWxkaoEgABS4ADdMyiyiglPMIcJ0GKQAayDAAGQEAuu8VUB/gJAH1AS4IgLAwAA24AAoygAeuAPFbqHPHoNwc1HuCJCDncRl7NG8At7Ak48qugVEGsOBxO7snB58T0ngASlwWjomFpMegOusxrFOLBCexsFMbvUzxCyVXRqEkBpjlpXdOgcEqFlsEKpRynFviMIus0md+kcUEDAuUeaxCcysjUGgySt1yTKTUZRTbOaFim17unxUr92doBw4f9zTKObGInZl+//NTW592VP3g+Q4Onh6Ovjfgt5vsPoSCJuDuPRz/58CFmhEtKPIEvY8kZAd3VxRxRJJSyIXcUu0/VOz3okITJRC2ex9kGdB5ecBVZLtgCyt70fUB2nGTTjOu/HFZohsXXLoOrbQKfDps1ePtTj9wSter2oGWoBnYRZqB+bQ5OnLaShpnrNAz6N6R7OW1I1HJjnmPVFuit7eDV1jNvuAkpJNqgJ0DQPCHiv3dqmULfJe3P7hrB/oej3T0S/Tme7tf1Xp/MArPB/Ayp82X5OlAaJfI8wHsJ2/zWXg6EGV4XXB5CbuN3mUYxnQKNI6HU9i3op0y3tpQQw39b/oLfDt0HcsiqWsAAAAASUVORK5CYII= // // @compatible chrome // @compatible firefox // @compatible opera // @downloadURL none // ==/UserScript== 'use strict'; // keep video info cache for a month since last time it's shown const CACHE_STALE_DURATION = 30 * 24 * 3600e3; const cfg = { width: 1280, height: 720, invidious: false, resize: 'Fit to width', pinnable: 'on', pinnedWidth: 400, playHTML5: false, playHTML5Shown: false, showStoryboard: true, skipCustom: true, }; const checked = []; let _, fytedom, styledom, iframes, objects, persite, playbtn; if (location.hostname === 'www.youtube.com') { if ((unsafeWindow.chrome || 0).app && 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); setTimeout(cleanupCache, 60e3); }, {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 () { if (fsbtn[0]) { this.disconnect(); fsbtn[0].outerHTML = fsbtn[0].outerHTML.replace('aria-disabled="true"', ''); fsbtn[0].addEventListener('click', () => { window.parent.postMessage('FYTE-toggle-fullscreen', '*'); }); } }).observe(document, {subtree: true, childList: true}); }); } function getPersiteRule() { const h = '.' + location.hostname; const rule = h === '.developers.google.com' && { match: '[data-video-id]', src: e => '//youtube.com/embed/' + e.dataset.videoId, } || h === '.play.google.com' && { eatparent: 0, } || h.includes('.google.') && /(^|\.)google\.\w{2,3}(\.\w{2,3})?$/.test(h) && { id: 'wp-tabs-container', query: 'a[href*="youtube.com/watch"][data-ved]', eatparent: 1, } || h === '.pikabu.ru' && { cls: 'b-video', match: '[data-url*="youtube.com/embed"]', attr: 'data-url', } || h === '.androidauthority.com' && { eatparent: '.video-container', } || h === '.reddit.com' && { match: '[data-url*="youtube.com/"] [src*="/mediaembed"],' + '[data-url*="youtu.be/"] [src*="/mediaembed"]', src: e => e.closest('[data-url*="youtube.com/"], [data-url*="youtu.be/"]').dataset.url, } || h.endsWith('.theverge.com') && { eatparent: '.p-scalable-video', } || h === '.9gag.com' && { eatparent: 0, } || h === '.reddit.com' && { match: '[data-url*="youtube.com"] iframe[src*="redditmedia.com/mediaembed"]', src: e => e.closest('[data-url*="youtube.com"]').dataset.url, } || h === '.anilist.co' && { eatparent: '.youtube', }; if (rule) { let {cls, id, match, query, tag} = rule; if (!tag && !cls) tag = 'iframe'; if (!match && !query) match = '[src*="youtube.com/embed"]'; if (!id) rule.nodes = cls ? document.getElementsByClassName(cls) : document.getElementsByTagName(tag); rule.match = match ? e => e.matches(match) ? e : null : e => e.querySelector(query); 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); } function findEmbeds(mutations) { 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.id ? [document.getElementById(persite.id)] : persite.nodes) if (el && (el = persite.match(el))) processEmbed(el, persite.src && persite.src(el) || el.getAttribute(persite.attr)); for (const el of iframes) { if (!checked.includes(el)) { checked.push(el); const src = el.src || el.getAttribute('data-src') || ''; if (src.includes('yout') && /youtube(-nocookie)?\.com|youtu\.be/i.test(src)) processEmbed(el, src); } } for (const el of objects) { if (!checked.includes(el)) { checked.push(el); const {src, value} = $('embed, [value*="youtu.be"], [value*="youtube.com"]', el) || {}; if (src) processEmbed(el, src || el.getAttribute('data-src') || `https://${value.match(/youtu\.be.*|youtube\.com.*/)[0]}`); } } } function decodeEmbedUrl(url) { return /youtube(-nocookie)?\.com%2Fembed/.test(url) ? decodeURIComponent(url.replace(/^.*?(http[^&?=]+?youtube(-nocookie)?\.com%2Fembed[^&]+).*$/i, '$1')) : url; } function processEmbed(node, src) { src = src || node.src || node.href || ''; 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( /(?:^(?:https?:)?\/\/)(?:www\.)?(?:youtube(?:-nocookie)?\.com\/(?:embed\/(?:v=)?|\/.*?[&?/]v[=/])|youtu\.be\/)([^\s,.()[\]?]+?)(?:[&?/].*|$)/); 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; } } createFYTE(node, n, id, setUrl(srcFixed)); stopOriginalEmbed(node); } function createFYTE(node, n, id, srcFixed, force) { if (!document.contains(n)) return; const cache = tryJSONparse(localStorage[`FYTE-cache-${id}`]) || {id}; const autoplay = /[?&](autoplay=1|ps=play)(&|$)/.test(srcFixed); const img = $create('img.thumbnail'); if (!autoplay) { img.src = setUrl(cache.cover || `https://i.ytimg.com/vi/${id}/maxresdefault.jpg`); img.onerror = onCoverError; } if (document.readyState !== 'complete' && !force) { const args = [...arguments]; args[createFYTE.length - 1] = true; setTimeout(createFYTE, 0, ...args); return; } injectStylesIfNeeded('force'); const div = $create('div.container'); div.FYTE = { state: 'querying', srcEmbed: srcFixed.replace(/&$/, ''), originalWidth: /%/.test(node.width) ? 320 : node.width | 0 || n.clientWidth | 0, originalHeight: /%/.test(node.height) ? 200 : node.height | 0 || n.clientHeight | 0, cache: cache, }; div.FYTE.srcEmbedFixed = div.FYTE.srcEmbed.replace(/^http:/, 'https:') .replace(/([&?])(wmode=\w+|feature=oembed)&?/, '$1') .replace(/[&?]$/, ''); div.FYTE.srcWatchFixed = div.FYTE.srcEmbedFixed.replace('/embed/', '/watch?v=').replace(/(\?.*?)\?/, '$1&'); cache.lastUsed = Date.now(); localStorage[`FYTE-cache-${id}`] = JSON.stringify(cache); if (cache.reason) div.setAttribute('disabled', ''); const divSize = calcContainerSize(div, n); const origStyle = getComputedStyle(n); overrideCSS(div, Object.assign( { height: persite && persite.eatparent === 0 ? '100%' : divSize.h + 'px', 'min-width': Math.min(divSize.w, div.FYTE.originalWidth) + 'px', 'min-height': Math.min(divSize.h, div.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: div.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); overrideCSS(img, Object.assign({ position: 'absolute', margin: 'auto', padding: 0, top: 0, left: 0, right: 0, bottom: 0, 'max-width': 'none', 'max-height': 'none', }, !cache.cover && { transition: 'opacity 0.1s ease-out', opacity: 0, })); img.FYTE = [div, divSize, autoplay]; img.onload = onCoverLoad; if (cache.coverWidth || img.naturalWidth) img.onload(); n.parentNode.insertBefore(div, n); n.remove(); 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) { this.FYTE.mouseEvent = e; this.removeEventListener('mouseenter', fetchInfo); if (!this.FYTE.storyboard) { GM_xmlhttpRequest({ method: 'GET', url: 'https://www.youtube.com/get_video_info?video_id=' + this.FYTE.cache.id + '&hl=en_US&html5=1&el=embedded&eurl=' + encodeURIComponent(location.href), context: this, onload: parseVideoInfo, }); } } function onCoverLoad(e) { const data = [...this.FYTE || []]; const div = data.shift(); const cache = div.FYTE.cache; const divSize = data.shift(); const autoplay = data.shift(); if (this.naturalWidth <= 120 && !cache.cover) return this.onerror(e); // delete this.FYTE; let fitToWidth = true; if (this.naturalHeight || cache.coverHeight) { if (!cache.coverHeight) { cache.coverWidth = this.naturalWidth; cache.coverHeight = this.naturalHeight; localStorage[`FYTE-cache-${cache.id}`] = JSON.stringify(cache); } const ratio = cache.coverWidth / cache.coverHeight; if (ratio > 4.1 / 3 && ratio < divSize.w / divSize.h) { this.style.setProperty('width', 'auto', 'important'); this.style.setProperty('height', '100%', 'important'); fitToWidth = false; } } if (fitToWidth) { this.style.setProperty('width', '100%', 'important'); this.style.setProperty('height', 'auto', 'important'); } if (cache.videoWidth) fixThumbnailAR(div); if (!autoplay) this.style.opacity = 1; } function onCoverError() { const src = this.src; if (src.includes('maxresdefault')) this.src = src.replace('maxresdefault', 'sddefault'); else if (src.includes('sddefault')) this.src = src.replace('sddefault', 'hqdefault'); } function stopOriginalEmbed(node) { const src = 'data:,'; let n = node; while (n) { if (n.src) n.src = src; if (n.dataset.src) n.dataset.src = src; n = $('embed', n); } for (const el of $$('[value*="youtu.be"], [value*="youtube.com"]', node)) el.value = src; } 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, })); fixThumbnailAR(n); 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) { origNode = origNode || div; let w, h; const 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 (div.FYTE.originalWidth === 320 && div.FYTE.originalHeight === 200) { w = parentWidth; h = parentWidth / 16 * 9; } else { w = div.FYTE.originalWidth; h = div.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 let n = origNode; do { n = n.parentElement; // find parent node with nonzero width (i.e. independent of our video element) } while (n && !(w = n.clientWidth)); 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 < div.FYTE.originalHeight * 0.9) h = Math.min(div.FYTE.originalHeight, w / div.FYTE.originalWidth * div.FYTE.originalHeight); return {w: window.chrome ? w : Math.round(w), h: h}; } function parseVideoInfo(response) { const div = response.context; const txt = response.responseText; const reason = txt.match(/(^|&)reason=(.+?)(&|$)|$/)[2]; const info = tryJSONparse( decodeURIComponent(txt.match(/(^|&)player_response=(.+?)(&|$)|$/)[2] || '')) || {}; const vid = info.videoDetails || {}; const streams = info.streamingData || {}; const cache = div.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)) { fixThumbnailAR(div, fmts[0].width, 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 = div.FYTE.duration = vid.lengthSeconds | 0; if (duration) { duration = secondsToTimeString(duration); if (cache.duration !== duration) { cache.duration = duration; shouldUpdateCache = true; } } if (duration || fps) duration = `${duration}${fps ? `${fps}fps` : ''}`; const title = decodeURIComponent(vid.title || reason || '').replace(/\+/g, ' '); if (title) { $('.instant-youtube-title', div).innerHTML = (title ? `${title}` : '') + 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) div.FYTE.videoSources = videoSources; if (txt.includes('playerStoryboardSpecRenderer') && info.storyboards && div.FYTE.state !== 'scheduled play') { const m = info.storyboards.playerStoryboardSpecRenderer.spec.split('|'); const [w, h, len, rows, cols] = m[m.length - 1].split('#').map(Number); div.FYTE.storyboard = {w, h, len, rows, cols}; if (w * h > 2000) { div.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', {}, '\xA0'), { width: w - 1 + 'px', height: h + 'px', })); if (cfg.showStoryboard) updateHoverHandler(div); } } } injectStylesIfNeeded(); if (div.FYTE.state === 'scheduled play') setTimeout(startPlayingDirectly, 0, div); 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 = setUrl(cover); } } catch (e) { } if (shouldUpdateCache) localStorage[`FYTE-cache-${cache.id}`] = JSON.stringify(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 fixThumbnailAR(div, w, h) { const img = $('img', div); if (!img) return; const thw = img.naturalWidth; const thh = img.naturalHeight; if (w && h) { // means thumbnail is still loading div.FYTE.cache.videoWidth = w; div.FYTE.cache.videoHeight = h; } else { w = div.FYTE.cache.videoWidth; h = div.FYTE.cache.videoHeight; if (!w || !h) return; } const divw = div.clientWidth; const divh = div.clientHeight; // if both video and thumbnail are 4:3, fit the image to height //console.log(div, divw, divh, thw, thh, w, h, h/w*divw / divh - 1, thh/thw*divw / divh - 1); if (Math.abs(h / w * divw / divh - 1) > 0.05 && Math.abs(thh / thw * divw / divh - 1) > 0.05) { img.style.maxHeight = img.clientHeight + 'px'; if (!div.FYTE.cache.videoWidth) // skip animation if thumbnail is already loaded img.style.transition = 'height 1s ease, margin-top 1s ease'; setTimeout(() => { overrideCSS(img, Object.assign( {'max-height': 'none'}, h / w >= divh / divw ? {width: 'auto', height: '100%'} : {width: '100%', height: 'auto'})); setTimeout(() => img.style.removeProperty('transition'), 1000); }); } } function trackMouse(e) { this.FYTE.mouseEvent = e; } function updateHoverHandler(div) { const fyte = div.FYTE; 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 setUrl(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 = ''; if (cfg.pinnable !== 'off') { makePinnable(div); if (params && params.pin) $(`[pin="${params.pin}"]`, div).click(); } if (window !== top) parent.postMessage('iframe-allowfs', '*'); const fyte = div.FYTE; 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 (div.FYTE.state) { div.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 = div.FYTE; 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 div.FYTE.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 (window.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; if (e instanceof Event) { console.log('[FYTE] Direct linking canceled on %s, switching to IFRAME player', div.FYTE.srcEmbed); const video = e.target ? e.target.closest('video') : e.composedPath().pop(); video.textContent = ''; goFullscreen(video, false); video.remove(); } const url = setUrlParams(div.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}); } iframe.onload = () => { addEventListener('message', YTlistener); iframe.contentWindow.postMessage('{"event":"listening"}', '*'); if (cfg.invidious) { overrideCSS(iframe, {opacity: 1}); $('.instant-youtube-title', div).hidden = true; } }; setTimeout(() => { overrideCSS(iframe, {opacity: 1}); removeEventListener('message', YTlistener); }, 5000); function YTlistener(e) { if (e.source !== iframe.contentWindow || !e.data) return; const data = tryJSONparse(e.data); if (!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 setUrl(url) { if (cfg.invidious) { const u = new URL(url); u.hostname = 'invidio.us'; url = u.href.replace('/vi_webp/', '/vi/').replace('.webp', '.jpg'); } return url; } 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', '
' + '' + '' + '' + ''); 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; if (pinIt) { for (const p of $$('[pin][active]', div)) p.removeAttribute('active'); pin.setAttribute('active', ''); if (!div.FYTE.unpinnedStyle) { div.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', ''); div.FYTE.stub = stub; div.parentNode.insertBefore(stub, div); } const size = constrainPinnedSize(div, localStorage[`width#${location.hostname}`] || 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 = div.FYTE.unpinnedStyle; div.FYTE.unpinnedStyle = ''; if (div.FYTE.stub) { if (video && document.body) div.FYTE.stub.parentNode.replaceChild(div, div.FYTE.stub); div.FYTE.stub.remove(); div.FYTE.stub = null; } } if (paused) video.pause(); } function startResize(e) { const siteSaved = ('width#' + location.hostname) in localStorage; 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 = `