- // ==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.7
- //
- // @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
- // ==/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"] img',
- src: e => e.closest('a').href,
- eatparent: 2,
- } ||
- 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 = `<span>${duration}</span>${fps ? `<i>${fps}fps</i>` : ''}`;
-
- const title = decodeURIComponent(vid.title || reason || '').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)
- 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.sb-thumb', {}, '\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 = '<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 = 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',
- '<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;
- 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 = `<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[`width#${location.hostname}`] = div.clientWidth;
- break;
- case 'global':
- cfg.pinnedWidth = div.clientWidth;
- GM_setValue('pinnedWidth', cfg.pinnedWidth);
- // fallthrough to remove the locally saved value
- case 'reset':
- localStorage.removeItem(`width#${location.hostname}`);
- 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 dropZoneHeight = 400 / div.FYTE.cache.videoWidth * div.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]') === div.FYTE.stub &&
- div.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) : 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 maxWidth = window.innerWidth - 100 | 0;
- const triedExceeding = (width | 0) > maxWidth;
- width = Math.max(200, Math.min(maxWidth, width | 0));
- return {
- w: width,
- h: width / div.FYTE.cache.videoWidth * div.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>
- <input data-action=invidious type=checkbox>
- msgInvidious
- </label>
- <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 {sheet} = styledom;
- const ALT = '.instant-youtube-alternative';
- let len = sheet.cssRules.length;
- if (sheet.cssRules[len - 1].selectorText === ALT)
- sheet.deleteRule(--len);
- sheet.insertRule(/*language=CSS*/ `${ALT} {
- display: ${cfg.playHTML5Shown ? 'block' : 'none'} !important;
- }`, len);
- }
-
- 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;
- }
-
- function cleanupCache() {
- const cutoff = Date.now() - CACHE_STALE_DURATION;
- for (const k in localStorage) {
- if (k.startsWith('FYTE-cache-')) {
- const {lastUsed} = tryJSONparse(localStorage[k]) || {};
- if (!lastUsed || lastUsed < cutoff)
- delete localStorage[k];
- }
- }
- for (const k of GM_listValues())
- if (k.startsWith('cache-'))
- GM_deleteValue(k);
- }
-
- 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;
- }
-
- 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;
- padding: 0;
- 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;
- padding: 0;
- margin: auto;
- pointer-events: none;
- background: url("data:image/gif;base64,R0lGODlhFAAUAJEDAMzMzLOzs39/f////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQFCgADACwAAAAAFAAUAAACPJyPqcuNItyCUJoQBo0ANIxpXOctYHaQpYkiHfM2cUrCNT0nqr4uudsz/IC5na/2Mh4Hu+HR6YBaplRDAQAh+QQFCgADACwEAAIADAAGAAACFpwdcYupC8BwSogR46xWZHl0l8ZYQwEAIfkEBQoAAwAsCAACAAoACgAAAhccMKl2uHxGCCvO+eTNmishcCCYjWEZFgAh+QQFCgADACwMAAQABgAMAAACFxwweaebhl4K4VE6r61DiOd5SfiN5VAAACH5BAUKAAMALAgACAAKAAoAAAIYnD8AeKqcHIwwhGntEWLkO3CcB4biNEIFACH5BAUKAAMALAQADAAMAAYAAAIWnDSpAHa4GHgohCHbGdbipnBdSHphAQAh+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;
- padding: 0;
- 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;
- margin: 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;
- margin: 0;
- }
- .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;
- margin: 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;
- margin: 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;
- margin: 0;
- padding: 0;
- 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-container [pin] {
- display: block;
- position: absolute;
- width: 0;
- height: 0;
- margin: 0;
- padding: 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;
- margin: 0;
- padding: 0;
- 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));
- // move our rules to the end of HEAD to increase CSS specificity
- if (styledom.nextElementSibling && document.head)
- document.head.appendChild(styledom);
- updateAltPlayerCSS();
- }