FYTE /Fast YouTube Embedded/ Player

Hugely improves load speed of pages with lots of embedded Youtube videos by instantly showing clickable and immediately accessible placeholders, then the thumbnails are loaded in background. Optionally a fast simple HTML5 direct playback (720p max) can be selected if available for the video.

当前为 2019-01-05 提交的版本,查看 最新版本

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