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. A fast simple HTML5 direct playback (720p max) can be selected if available for the video.
  4. // @description:ru На порядок ускоряет время загрузки страниц с большим количеством вставленных Youtube-видео. С первого момента загрузки страницы появляются заглушки для видео, которые можно щелкнуть для загрузки плеера, и почти сразу же появляются кавер-картинки с названием видео. В опциях можно включить режим использования упрощенного браузерного плеера (макс. 720p).
  5. // @version 2.5.4
  6. // @include *
  7. // @exclude https://www.youtube.com/*
  8. // @author wOxxOm
  9. // @namespace wOxxOm.scripts
  10. // @license MIT License
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_addStyle
  14. // @grant GM_xmlhttpRequest
  15. // @connect www.youtube.com
  16. // @connect youtube.com
  17. // @run-at document-start
  18. // @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=
  19. // @compatible chrome
  20. // @compatible firefox
  21. // @compatible opera
  22. // ==/UserScript==
  23.  
  24. /* jshint lastsemic:true, multistr:true, laxbreak:true, -W030, -W041, -W084 */
  25.  
  26. var resizeMode = GM_getValue('resize', 'Fit to width');
  27. if (typeof resizeMode != 'string')
  28. resizeMode = resizeMode ? 'Fit to width' : 'Original';
  29.  
  30. var resizeWidth = GM_getValue('width', 1280) |0;
  31. var resizeHeight = GM_getValue('height', 720) |0;
  32. updateCustomSize();
  33.  
  34. var playDirectly = !!GM_getValue('playHTML5', false);
  35. var skipCustom = !!GM_getValue('skipCustom', true);
  36. var showStoryboard = !!GM_getValue('showStoryboard', true);
  37. var pinnable = !!GM_getValue('pinnable', true);
  38.  
  39. var _ = initTL();
  40.  
  41. var imageLoader = document.createElement('img');
  42. var imageLoader2 = document.createElement('img');
  43.  
  44. var fytedom = document.getElementsByClassName('instant-youtube-container');
  45. var iframes = document.getElementsByTagName('iframe');
  46. var oembeds = document.getElementsByTagName('embed');
  47. var persite = (function() {
  48. var rules = [
  49. {host: /(^|\.)google\.\w{2,3}(\.\w{2,3})?$/, class:'g-blk', query: 'a[href*="youtube.com/watch"][data-ved]', eatparent: 1},
  50. {host: 'pikabu.ru', class:'b-video', match: '[data-url*="youtube.com/embed"]', attr: 'data-url'},
  51. {host: 'androidauthority.com', tag:'iframe', match: '[src*="youtube.com/embed"]', eatparent: '.video-container'},
  52. {host: 'reddit.com', tag:'iframe',
  53. match: '[data-url*="youtube.com/"] [src*="/mediaembed"], [data-url*="youtu.be/"] [src*="/mediaembed"]',
  54. src: function(e) { return e.closest('[data-url*="youtube.com/"], [data-url*="youtu.be/"]').dataset.url }},
  55. ];
  56. for (var i=0, rule; (i<rules.length) && (rule=rules[i]); i++) {
  57. var rx = rule.host instanceof RegExp ? rule.host : new RegExp('(^|\\.)' + rule.host.replace(/\./g, '\\.') + '$', 'i');
  58. if (rx.test(location.hostname))
  59. return {
  60. nodes: rule.class ? document.getElementsByClassName(rule.class) : document.getElementsByTagName(rule.tag),
  61. match: rule.match ? function(e) { return e.matches(rule.match) ? e : null }
  62. : function(e) { return e.querySelector(rule.query) },
  63. attr: rule.attr,
  64. src: rule.src,
  65. eatparent: rule.eatparent,
  66. };
  67. }
  68. })();
  69.  
  70. findEmbeds();
  71. injectStylesIfNeeded();
  72. new MutationObserver(findEmbeds).observe(document, {subtree:true, childList:true});
  73.  
  74. document.addEventListener('DOMContentLoaded', function(e) {
  75. injectStylesIfNeeded();
  76. adjustNodesIfNeeded(e);
  77. });
  78. window.addEventListener('resize', adjustNodesIfNeeded, true);
  79. window.addEventListener('message', function(e) {
  80. if (e.data == 'iframe-allowfs') {
  81. $$('iframe:not([allowfullscreen])').some(function(iframe) {
  82. if (iframe.contentWindow == e.source) {
  83. iframe.allowFullscreen = true;
  84. return true;
  85. }
  86. });
  87. if (window != window.top)
  88. window.parent.postMessage('iframe-allowfs', '*');
  89. }
  90. });
  91.  
  92. function findEmbeds(mutations) {
  93. var i, len, e;
  94. if (mutations && mutations.length == 1 && !mutations[0].addedNodes.length)
  95. return;
  96. if (persite)
  97. for (i=0, len=persite.nodes.length; (i<len) && (e=persite.nodes[i]); i++)
  98. if (e = persite.match(e))
  99. processEmbed(e, persite.src && persite.src(e) || e.getAttribute(persite.attr));
  100. for (i=0, len=iframes.length; (i<len) && (e=iframes[i]); i++)
  101. if (/youtube\.com(\/|%2F)(embed|v)(\/|%2F)/i.test(e.src))
  102. processEmbed(e);
  103. for (i=0, len=oembeds.length; (i<len) && (e=oembeds[i]); i++)
  104. if (/youtube\.com(\/|%2F)(embed|v)\//i.test(e.src))
  105. processEmbed(e);
  106. }
  107.  
  108. function processEmbed(node, src) {
  109. function decodeEmbedUrl(url) {
  110. return url.indexOf('youtube.com%2Fembed') > 0
  111. ? decodeURIComponent(url.replace(/^.*?(http[^&?=]+?youtube.com%2Fembed[^&]+).*$/i, '$1'))
  112. : url;
  113. }
  114. src = src || node.src || node.href || '';
  115. var n = node;
  116. var np = n.parentNode, npw;
  117. var srcFixed = decodeEmbedUrl(src).replace(/\/(watch\?v=|v\/)/, '/embed/');
  118. if (src.indexOf('cdn.embedly.com/') > 0 ||
  119. resizeMode != 'Original' && np && np.children.length == 1 && !np.className && !np.id)
  120. {
  121. n = location.hostname == 'disqus.com' ? np.parentNode : np;
  122. np = n.parentElement;
  123. }
  124. if (!np ||
  125. !np.parentNode ||
  126. skipCustom && srcFixed.indexOf('enablejsapi=1') > 0 ||
  127. node.matches('.instant-youtube-embed, .YTLT-embed') ||
  128. node.onload // skip some retarded loaders
  129. )
  130. return;
  131.  
  132. var id = srcFixed.match(/(?:embed\/|v[=\/]|youtu\.be\/)([^\s,.()\[\]?]+?)(?:[&?\/].*|$)/);
  133. if (!id)
  134. return;
  135. id = id[1];
  136.  
  137. var autoplay = srcFixed.indexOf('autoplay=1') > 0;
  138.  
  139. if (np.localName == 'object')
  140. n = np, np = n.parentElement;
  141.  
  142. var eatparent = persite && persite.eatparent || 0;
  143. if (typeof eatparent == 'string')
  144. n = np.closest(eatparent) || n, np = n.parentElement;
  145. else
  146. while (eatparent--)
  147. n = np, np = n.parentElement;
  148.  
  149. injectStylesIfNeeded('force');
  150.  
  151. var div = document.createElement('div');
  152. div.className = 'instant-youtube-container';
  153. div.FYTE = {
  154. state: 'querying',
  155. srcEmbed: srcFixed.replace(/&$/, ''),
  156. videoID: id,
  157. originalWidth: /%/.test(node.width) ? 320 : node.width|0 || n.clientWidth|0,
  158. originalHeight: /%/.test(node.height) ? 200 : node.height|0 || n.clientHeight|0,
  159. };
  160. div.FYTE.srcEmbedFixed = div.FYTE.srcEmbed.replace(/^http:/, 'https:').replace(/&?wmode=\w+/, '').replace(/[?&]feature=oembed/, '');
  161. div.FYTE.srcWatchFixed = div.FYTE.srcEmbedFixed.replace(/\/embed\//, '/watch?v=');
  162.  
  163. var divSize = calcContainerSize(div, n);
  164. var origStyle = getComputedStyle(n);
  165. div.style.cssText = important('background-color:transparent; transition:background-color 2s;' +
  166. (origStyle.hasOwnProperty('position') ? Object.keys(origStyle) : Object.keys(origStyle.__proto__) /*FF*/)
  167. .filter(function(k) { return !!k.match(/^(position|left|right|top|bottom)$/) })
  168. .map(function(k) { return k + ':' + origStyle[k] })
  169. .join(';')
  170. .replace(/\b[^;:]+:\s*(auto|static|block)\s*(!\s*important)?;/g, '') +
  171. (origStyle.display == 'inline' ? ';display:inline-block;width:100%' : '') +
  172. ';min-width:' + Math.min(divSize.w, div.FYTE.originalWidth) + 'px' +
  173. ';min-height:' + Math.min(divSize.h, div.FYTE.originalHeight) + 'px' +
  174. (resizeMode == 'Fit to width' ? ';width:100%' : '') +
  175. ';max-width:' + divSize.w + 'px; height:' + divSize.h + 'px;');
  176. if (!autoplay) {
  177. setTimeout(function() { div.style.backgroundColor = '' }, 0);
  178. setTimeout(function() { div.style.transition = '' }, 2000);
  179. }
  180.  
  181. // consume parents of retardedly positioned videos
  182. if (div.style.position.match('absolute|relative')) {
  183. if (np.children.length == 1 && floatPadding(np, getComputedStyle(np, ':after'), 'Top') >= div.FYTE.originalHeight)
  184. n = np, np = n.parentElement;
  185. div.style.cssText = div.style.cssText.replace(/\b(position|left|top|right|bottom):[^;]+/g, '');
  186. }
  187.  
  188. var wrapper = div.appendChild(document.createElement('div'));
  189. wrapper.className = 'instant-youtube-wrapper';
  190.  
  191. var img = wrapper.appendChild(document.createElement('img'));
  192. img.className = 'instant-youtube-thumbnail';
  193. img.src = 'https://i.ytimg.com/vi/' + id + '/maxresdefault.jpg';
  194. img.style.cssText = important('transition:opacity 0.1s ease-out; opacity:0; padding:0; margin:auto; position:absolute; left:0; right:0; top:0; bottom:0; max-width:none; max-height:none;');
  195.  
  196. img.title = _('Shift-click to use alternative player');
  197. img.onload = function(e) {
  198. if (img.naturalWidth <= 120)
  199. return img.onerror(e);
  200. var fitToWidth = true;
  201. if (img.naturalHeight) {
  202. var ratio = img.naturalWidth / img.naturalHeight;
  203. if (ratio > 4.1/3 && ratio < divSize.w/divSize.h) {
  204. img.style.cssText += important('width:auto; height:100%;');
  205. fitToWidth = false;
  206. }
  207. }
  208. if (fitToWidth) {
  209. img.style.cssText += important('width:100%; height:auto;');
  210. }
  211. if (div.FYTE.videoWidth)
  212. fixThumbnailAR(div);
  213. if (!autoplay)
  214. img.style.opacity = 1;
  215. };
  216. img.onerror = function(e) {
  217. if (img.src.indexOf('maxresdefault') > 0)
  218. img.src = img.src.replace('maxresdefault','hqdefault');
  219. };
  220.  
  221. GM_xmlhttpRequest({
  222. method: 'GET',
  223. url: 'https://www.youtube.com/get_video_info?video_id=' + div.FYTE.videoID + '&el=detailpage',
  224. headers: {'Accept-Encoding': 'gzip'},
  225. context: div,
  226. onload: parseVideoInfo
  227. });
  228.  
  229. translateHTML(wrapper, 'beforeend', '\
  230. <a class="instant-youtube-title" target="_blank" href="' + div.FYTE.srcWatchFixed + '">&nbsp;</a>\
  231. <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"></path><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" points="33.3,41.4 33.3,17.75 56,29.6"></polygon></svg>\
  232. <span tl class="instant-youtube-link">' + (playDirectly ? 'Play with Youtube player' : 'Play directly (up to 720p)') + '</span>\
  233. <div tl class="instant-youtube-options-button">Options</div>\
  234. ');
  235.  
  236. np.insertBefore(div, n);
  237. n.remove();
  238.  
  239. if (autoplay)
  240. return startPlaying(div);
  241.  
  242. div.addEventListener('click', clickHandler);
  243. }
  244.  
  245. function adjustNodesIfNeeded(e) {
  246. if (!fytedom.length)
  247. return;
  248. if (adjustNodesIfNeeded.scheduled)
  249. clearTimeout(adjustNodesIfNeeded.scheduled);
  250. adjustNodesIfNeeded.scheduled = setTimeout(function() {
  251. adjustNodes(e);
  252. adjustNodesIfNeeded.scheduled = 0;
  253. }, 16);
  254. }
  255.  
  256. function adjustNodes(e, clickedContainer) {
  257. var force = !!clickedContainer;
  258. var nearest = force ? clickedContainer : null;
  259.  
  260. var vids = $$('.instant-youtube-container:not([pinned])');
  261.  
  262. if (!nearest && e.type != 'DOMContentLoaded') {
  263. var minDistance = window.innerHeight*3/4 |0;
  264. var nearTargetY = window.innerHeight/2;
  265. vids.forEach(function(n) {
  266. var bounds = n.getBoundingClientRect();
  267. var distance = Math.abs((bounds.bottom + bounds.top)/2 - nearTargetY);
  268. if (distance < minDistance) {
  269. minDistance = distance;
  270. nearest = n;
  271. }
  272. });
  273. }
  274.  
  275. if (nearest) {
  276. var bounds = nearest.getBoundingClientRect();
  277. var nearestCenterYpct = (bounds.top + bounds.bottom)/2 / window.innerHeight;
  278. }
  279.  
  280. var resized = false;
  281.  
  282. vids.forEach(function(n) {
  283. var size = calcContainerSize(n);
  284. var w = size.w, h = size.h;
  285.  
  286. // prevent parent clipping
  287. for (var e=n.parentElement, style; e; e=e.parentElement)
  288. if (e.style.overflow != 'visible' && (style=getComputedStyle(e)))
  289. if ((style.overflow+style.overflowX+style.overflowY).match(/hidden|scroll/))
  290. if (n.offsetTop < e.clientHeight / 2 && n.offsetTop + n.clientHeight > e.clientHeight)
  291. e.style.cssText = e.style.cssText.replace(/\boverflow(-[xy])?:[^;]+/g, '') +
  292. important('overflow:visible;overflow-x:visible;overflow-y:visible;');
  293.  
  294. if (force && Math.abs(w - parseFloat(n.style.maxWidth)) <= 2)
  295. return;
  296.  
  297. if (n.style.maxWidth != w + 'px') n.style.maxWidth = w + 'px';
  298. if (n.style.height != h + 'px') n.style.height = h + 'px';
  299. if (parseFloat(n.style.minWidth) > w) n.style.minWidth = n.style.maxWidth;
  300. if (parseFloat(n.style.minHeight) > h) n.style.minHeight = n.style.height;
  301.  
  302. fixThumbnailAR(n);
  303. resized = true;
  304. });
  305.  
  306. if (resized && nearest)
  307. setTimeout(function() {
  308. var bounds = nearest.getBoundingClientRect();
  309. var h = bounds.bottom - bounds.top;
  310. var projectedCenterY = nearestCenterYpct * window.innerHeight;
  311. var projectedTop = projectedCenterY - h/2;
  312. var safeTop = Math.min(Math.max(0, projectedTop), window.innerHeight - h);
  313. window.scrollBy(0, bounds.top - safeTop);
  314. }, 16);
  315. }
  316.  
  317. function calcContainerSize(div, origNode) {
  318. var w, h;
  319. origNode = origNode || div;
  320. switch (resizeMode) {
  321. case 'Original':
  322. w = div.FYTE.originalWidth;
  323. h = div.FYTE.originalHeight;
  324. break;
  325. case 'Custom':
  326. w = resizeWidth;
  327. h = resizeHeight;
  328. break;
  329. case '1080p':
  330. case '720p':
  331. case '480p':
  332. case '360p':
  333. h = parseInt(resizeMode);
  334. w = h / 9 * 16;
  335. break;
  336. default: // fit-to-width mode
  337. var n = origNode;
  338. do {
  339. n = n.parentElement;
  340. // find parent node with nonzero width (i.e. independent of our video element)
  341. } while (n && !(w = n.clientWidth));
  342. if (w)
  343. h = w / 16 * 9;
  344. else {
  345. w = origNode.clientWidth;
  346. h = origNode.clientHeight;
  347. }
  348. }
  349. var np = origNode.parentElement;
  350. var style = getComputedStyle(np);
  351. var parentWidth = parseFloat(style.width) - floatPadding(np, style, 'Left') - floatPadding(np, style, 'Right');
  352. if (parentWidth > 0 && parentWidth < w) {
  353. h = parentWidth / w * h;
  354. w = parentWidth;
  355. }
  356. if (resizeMode == 'Fit to width' && h < div.FYTE.originalHeight*0.9)
  357. h = Math.min(div.FYTE.originalHeight, w / div.FYTE.originalWidth * div.FYTE.originalHeight);
  358.  
  359. return {w: window.chrome ? w : Math.round(w), h:h};
  360. }
  361.  
  362. function parseVideoInfo(response) {
  363. var div = response.context;
  364. var txt = response.responseText;
  365. var info = tryCatch(function() { return JSON.parse(txt.replace(/(\w+)=?(.*?)(&|$)/g, '"$1":"$2",').replace(/^(.+?),?$/, '{$1}')) }) || {};
  366. var videoSources = [];
  367.  
  368. // parse width & height to adjust the thumbnail
  369. var m = decodeURIComponent(info.adaptive_fmts || '').match(/size=(\d+)x(\d+)\b/) ||
  370. decodeURIComponent(txt).match(/\/(\d+)x(\d+)\//);
  371. if (m)
  372. fixThumbnailAR(div, m[1]|0, m[2]|0);
  373.  
  374. // parse video sources
  375. if (info.url_encoded_fmt_stream_map && info.fmt_list) {
  376. var streams = {};
  377. decodeURIComponent(info.url_encoded_fmt_stream_map).split(',').forEach(function(stream) {
  378. var params = {};
  379. stream.split('&').forEach(function(kv) {
  380. params[kv.split('=')[0]] = decodeURIComponent(kv.split('=')[1]);
  381. });
  382. streams[params.itag] = params;
  383. });
  384. decodeURIComponent(info.fmt_list).split(',').forEach(function(fmt) {
  385. var itag = fmt.split('/')[0];
  386. var dimensions = fmt.split('/')[1];
  387. var stream = streams[itag];
  388. if (stream) {
  389. videoSources.push({
  390. src: stream.url,
  391. title: stream.quality + ', ' + dimensions + ', ' + stream.type
  392. });
  393. }
  394. });
  395. } else {
  396. var rx = /url=([^=]+?mime%3Dvideo%252F(?:mp4|webm)[^=]+?)(?:,quality|,itag|.u0026)/g;
  397. var text = decodeURIComponent(txt).split('url_encoded_fmt_stream_map')[1];
  398. while (m = rx.exec(text)) {
  399. videoSources.push({
  400. src: decodeURIComponent(decodeURIComponent(m[1]))
  401. });
  402. }
  403. }
  404.  
  405. var duration = div.FYTE.duration = info.length_seconds|0 || '';
  406. if (duration) {
  407. var d = new Date(null);
  408. d.setSeconds(duration);
  409. duration = d.toISOString().replace(/^.+?T[0:]{0,4}(.+?)\..+$/, '<span>$1</span>');
  410. }
  411. var title = decodeURIComponent(info.title || info.reason || '').replace(/\+/g, ' ');
  412. if (title || duration) {
  413. $(div, '.instant-youtube-title').innerHTML = (title ? '<strong>' + title + '</strong>' : '') + duration;
  414. }
  415. if (info.reason)
  416. div.setAttribute('disabled', '');
  417.  
  418. if (videoSources.length)
  419. div.FYTE.videoSources = videoSources;
  420.  
  421. if (info.storyboard_spec) {
  422. m = decodeURIComponent(decodeURIComponent(info.storyboard_spec)).split('|');
  423. div.FYTE.storyboard = JSON.parse(m[m.length-1].replace(/^(\d+)#(\d+)#(\d+)#(\d+)#(\d+)#.+$/, '{"w":$1, "h":$2, "len":$3, "rows":$4, "cols":$5}'));
  424. div.FYTE.storyboard.url = m[0].replace('$L/$N.jpg', (m.length-2) + '/M0.jpg?sigh=' + m[m.length-1].replace(/^.+?#([^#]+)$/, '$1'));
  425. $(div, '.instant-youtube-options-button').insertAdjacentHTML('beforebegin',
  426. '<div class="instant-youtube-storyboard"' + (showStoryboard ? '' : ' disabled') + '>' +
  427. important('<div style="width:' + (div.FYTE.storyboard.w-1) + 'px; height:' + div.FYTE.storyboard.h + 'px;') +
  428. '">&nbsp;</div>' +
  429. '</div>');
  430. if (showStoryboard)
  431. updateHoverHandler(div);
  432. }
  433.  
  434. injectStylesIfNeeded();
  435.  
  436. if (div.FYTE.state == 'scheduled play')
  437. setTimeout(function() { startPlayingDirectly(div) }, 0);
  438.  
  439. div.FYTE.state = '';
  440. }
  441.  
  442. function fixThumbnailAR(div, w, h) {
  443. var img = $(div, 'img');
  444. var thw = img.naturalWidth, thh = img.naturalHeight;
  445. if (w && h) { // means thumbnail is still loading
  446. div.FYTE.videoWidth = w;
  447. div.FYTE.videoHeight = h;
  448. } else {
  449. w = div.FYTE.videoWidth;
  450. h = div.FYTE.videoHeight;
  451. if (!w || !h)
  452. return;
  453. }
  454. var divw = div.clientWidth, divh = div.clientHeight;
  455. // if both video and thumbnail are 4:3, fit the image to height
  456. //console.log(div, divw, divh, thw, thh, w, h, h/w*divw / divh - 1, thh/thw*divw / divh - 1);
  457. if (Math.abs(h/w*divw / divh - 1) > 0.05 && Math.abs(thh/thw*divw / divh - 1) > 0.05) {
  458. img.style.maxHeight = img.clientHeight + 'px';
  459. if (!div.FYTE.videoWidth) // skip animation if thumbnail is already loaded
  460. img.style.transition = 'height 1s ease, margin-top 1s ease';
  461. setTimeout(function() {
  462. img.style.maxHeight = 'none';
  463. img.style.cssText += important(h/w >= divh/divw ? 'width:auto; height:100%;' : 'width:100%; height:auto;');
  464. setTimeout(function() {
  465. img.style.transition = '';
  466. }, 1000);
  467. }, 0);
  468. }
  469. }
  470.  
  471. function updateHoverHandler(div) {
  472. var sb = $(div, '.instant-youtube-storyboard');
  473. if (!showStoryboard) {
  474. if (!sb.getAttribute('disabled'))
  475. sb.setAttribute('disabled', '');
  476. return;
  477. }
  478. if (sb.hasAttribute('disabled'))
  479. sb.removeAttribute('disabled');
  480.  
  481. sb.addEventListener('click', storyboardClickHandler);
  482.  
  483. var oldIndex = null;
  484. var style = sb.firstElementChild.style;
  485. sb.addEventListener('mousemove', storyboardHoverHandler);
  486. sb.addEventListener('mouseout', storyboardHoverHandler);
  487.  
  488. div.addEventListener('mouseover', storyboardPreloader);
  489. div.addEventListener('mouseout', storyboardPreloader);
  490.  
  491. var spinner = document.createElement('span');
  492. spinner.className = 'instant-youtube-loading-button';
  493.  
  494. function storyboardClickHandler(e) {
  495. sb.removeEventListener('click', storyboardClickHandler);
  496. var offsetX = e.offsetX || e.clientX - this.getBoundingClientRect().left;
  497. div.FYTE.startAt = offsetX / this.clientWidth * div.FYTE.duration |0;
  498. div.FYTE.srcEmbedFixed = setUrlParams(div.FYTE.srcEmbedFixed, {start: div.FYTE.startAt});
  499. startPlaying(div, e.shiftKey);
  500. }
  501.  
  502. function storyboardPreloader(e) {
  503. if (e.type == 'mouseout') {
  504. imageLoader.onload = null; imageLoader.src = '';
  505. spinner.remove();
  506. return;
  507. }
  508. if (!div.FYTE.storyboard || div.FYTE.storyboard.preloaded)
  509. return;
  510. var lastpart = (div.FYTE.storyboard.len-1)/(div.FYTE.storyboard.rows * div.FYTE.storyboard.cols) |0;
  511. if (lastpart <= 0)
  512. return;
  513. var part = 0;
  514. imageLoader.src = setStoryboardUrl(part++);
  515. imageLoader.onload = function() {
  516. if (part > lastpart) {
  517. div.FYTE.storyboard.preloaded = true;
  518. div.removeEventListener('mouseover', storyboardPreloader);
  519. div.removeEventListener('mouseout', storyboardPreloader);
  520. imageLoader.onload = null;
  521. imageLoader.src = '';
  522. spinner.remove();
  523. return;
  524. }
  525. imageLoader.src = setStoryboardUrl(part++);
  526. };
  527. }
  528.  
  529. function setStoryboardUrl(part) {
  530. return div.FYTE.storyboard.url.replace(/M\d+\.jpg\?/, 'M' + part + '.jpg?');
  531. }
  532.  
  533. function storyboardHoverHandler(e) {
  534. if (!showStoryboard || !div.FYTE.storyboard)
  535. return;
  536. if (e.type == 'mouseout')
  537. return imageLoader2.onload && imageLoader2.onload();
  538. var w = div.FYTE.storyboard.w;
  539. var h = div.FYTE.storyboard.h;
  540. var cols = div.FYTE.storyboard.cols;
  541. var rows = div.FYTE.storyboard.rows;
  542. var len = div.FYTE.storyboard.len;
  543. var partlen = rows * cols;
  544.  
  545. var offsetX = e.offsetX || e.clientX - this.getBoundingClientRect().left;
  546. var left = Math.min(this.clientWidth - w, Math.max(0, offsetX - w)) |0;
  547. if (!style.left || parseInt(style.left) != left) {
  548. style.left = left + 'px';
  549. if (spinner.parentElement)
  550. spinner.style.cssText = important('left:' + (left + w/2 - 10) + 'px; right:auto;');
  551. }
  552.  
  553. var index = Math.min(offsetX / this.clientWidth * (len+1) |0, len - 1);
  554. if (index == oldIndex)
  555. return;
  556.  
  557. var part = index/partlen|0;
  558. if (!oldIndex || part != (oldIndex/partlen|0)) {
  559. style.cssText = style.cssText.replace(/$|background-image[^;]+;/,
  560. 'background-image: url(' + setStoryboardUrl(part) + ')!important;');
  561. if (!div.FYTE.storyboard.preloaded) {
  562. if (spinner.timer)
  563. clearTimeout(spinner.timer);
  564. spinner.timer = setTimeout(function() {
  565. spinner.timer = 0;
  566. if (!imageLoader2.src)
  567. return;
  568. this.appendChild(spinner);
  569. spinner.style.cssText = important('left:' + (left + w/2 - 10) + 'px; right:auto;');
  570. }.bind(this), 50);
  571. imageLoader2.onload = function() {
  572. clearTimeout(spinner.timer);
  573. spinner.remove();
  574. spinner.timer = 0;
  575. imageLoader2.onload = null;
  576. imageLoader2.src = '';
  577. };
  578. imageLoader2.src = setStoryboardUrl(part);
  579. }
  580. }
  581.  
  582. oldIndex = index;
  583. index = index % partlen;
  584. style.backgroundPosition = '-' + (index % cols) * w + 'px -' + (index / cols |0) * h + 'px';
  585. }
  586. }
  587.  
  588. function clickHandler(e) {
  589. if (e.target.href)
  590. return;
  591. if (e.target.matches('.instant-youtube-options-button')) {
  592. showOptions(e);
  593. e.preventDefault();
  594. e.stopPropagation();
  595. return;
  596. }
  597. if (e.target.matches('.instant-youtube-options, .instant-youtube-options *'))
  598. return;
  599.  
  600. e.preventDefault();
  601. e.stopPropagation();
  602.  
  603. var alternateMode = e.shiftKey || e.target.className == 'instant-youtube-link';
  604. startPlaying(e.target.closest('.instant-youtube-container'), alternateMode);
  605. }
  606.  
  607. function startPlaying(div, alternateMode) {
  608. div.removeEventListener('click', clickHandler);
  609.  
  610. $$(div, '.instant-youtube-wrapper > *:not(img):not(a)').forEach(function(e) { e.style.cssText = 'display:none!important' });
  611. $(div, 'svg').outerHTML = '<span class="instant-youtube-loading-button"></span>';
  612.  
  613. if (pinnable) {
  614. div.firstElementChild.insertAdjacentHTML('beforeend',
  615. '<div pin="top-left"></div><div pin="top-right"></div><div pin="bottom-right"></div><div pin="bottom-left"></div>');
  616. $$(div, '[pin]').forEach(function(pin) {
  617. pin.onclick = function(e) {
  618. var pinIt = !div.hasAttribute('pinned') || !pin.hasAttribute('active');
  619. var corner = pin.getAttribute('pin');
  620. var video = $(div, 'video');
  621. if (pinIt) {
  622. $$(div, '[pin][active]').forEach(function(p) { p.removeAttribute('active') });
  623. pin.setAttribute('active', '');
  624. if (!div.FYTE.unpinnedStyle) {
  625. div.FYTE.unpinnedStyle = div.style.cssText;
  626. var stub = div.cloneNode();
  627. var img = $(div, 'img').cloneNode();
  628. img.style.opacity = 1;
  629. img.style.display = 'block';
  630. img.title = '';
  631. stub.appendChild(img);
  632. stub.onclick = function(e) { $(div, '[pin][active]').onclick(e) };
  633. stub.style.cssText += 'opacity:0.3!important;';
  634. div.FYTE.stub = stub;
  635. div.parentNode.insertBefore(stub, div);
  636. }
  637. div.setAttribute('pinned', '');
  638. div.style.cssText = important(
  639. 'position: fixed;' +
  640. 'contain: inherit;' +
  641. 'width: 400px;' +
  642. 'z-index: 999999999;' +
  643. 'height:' + (400 / div.FYTE.videoWidth * div.FYTE.videoHeight) + 'px;' +
  644. 'top:' + (corner.indexOf('top') >= 0 ? '0' : 'auto') + ';' +
  645. 'bottom:' + (corner.indexOf('bottom') >= 0 ? '0' : 'auto') + ';' +
  646. 'left:' + (corner.indexOf('left') >= 0 ? '0' : 'auto') + ';' +
  647. 'right:' + (corner.indexOf('right') >= 0 ? '0' : 'auto') + ';'
  648. );
  649. if (video && document.body)
  650. document.body.appendChild(div);
  651. } else {
  652. pin.removeAttribute('active');
  653. div.removeAttribute('pinned');
  654. div.style.cssText = div.FYTE.unpinnedStyle;
  655. div.FYTE.unpinnedStyle = '';
  656. if (div.FYTE.stub) {
  657. if (video && document.body)
  658. div.FYTE.stub.parentNode.appendChild(div, div.FYTE.stub);
  659. div.FYTE.stub.remove();
  660. div.FYTE.stub = null;
  661. }
  662. }
  663. if (video && video.paused)
  664. video.play();
  665. };
  666. });
  667. }
  668.  
  669. if (window != window.top)
  670. window.parent.postMessage('iframe-allowfs', '*');
  671.  
  672. if ((!!playDirectly + !!alternateMode == 1) && (div.FYTE.videoSources || div.FYTE.state == 'querying')) {
  673. if (div.FYTE.videoSources)
  674. startPlayingDirectly(div);
  675. else {
  676. // playback will start in parseVideoInfo
  677. div.FYTE.state = 'scheduled play';
  678. // fallback to iframe in 5s
  679. setTimeout(function() {
  680. if (div.FYTE.state) {
  681. div.FYTE.state = '';
  682. switchToIFrame.call(div);
  683. }
  684. }, 5000);
  685. }
  686. }
  687. else
  688. switchToIFrame.call(div);
  689. }
  690.  
  691. function startPlayingDirectly(div) {
  692. var video = document.createElement('video');
  693. video.controls = true;
  694. video.autoplay = true;
  695. video.style.cssText = important(
  696. 'position:absolute; left:0; top:0; right:0; padding:0; margin:auto; opacity:0; transition:opacity 2s;' +
  697. 'width:100%; height:100%;');
  698. video.className = 'instant-youtube-embed';
  699. video.volume = GM_getValue('volume', 0.5);
  700.  
  701. div.FYTE.videoSources.forEach(function(src) {
  702. var srcdom = video.appendChild(document.createElement('source'));
  703. Object.keys(src).forEach(function(k) { srcdom[k] = src[k] });
  704. srcdom.onerror = switchToIFrame.bind(div);
  705. });
  706.  
  707.  
  708. if (window.chrome) {
  709. video.addEventListener('click', function(e) {
  710. this.paused ? this.play() : this.pause();
  711. });
  712. }
  713. video.interval = (function() {
  714. return setInterval(function() {
  715. if (video.volume != GM_getValue('volume', 0.5))
  716. GM_setValue('volume', video.volume);
  717. }, 1000);
  718. })();
  719. var title = $(div, '.instant-youtube-title');
  720. if (title) {
  721. video.onpause = function() { title.removeAttribute('hidden') };
  722. video.onplay = function() { title.setAttribute('hidden', true) };
  723. }
  724. video.onloadedmetadata = div.FYTE.startAt && function(e) {
  725. video.currentTime = div.FYTE.startAt;
  726. };
  727. video.onloadeddata = function(e) {
  728. pauseOtherVideos(this);
  729. div.style.cssText += 'contain:inherit!important'; // allow fullscreen
  730. div.firstElementChild.appendChild(this);
  731. div.setAttribute('playing', '');
  732. this.style.opacity = 1;
  733. var img = $(div, 'img');
  734. img.style.transition = 'opacity 1s';
  735. img.style.opacity = 0;
  736. };
  737. }
  738.  
  739. function switchToIFrame(e) {
  740. var div = this;
  741. var wrapper = div.firstElementChild;
  742. if (e) {
  743. console.log('[FYTE] Direct linking canceled on %s, switching to IFRAME player', div.FYTE.srcEmbed);
  744. var video = e.target ? e.target.closest('video') : e.path && e.path[e.path.length-1];
  745. while (video.lastElementChild)
  746. video.lastElementChild.remove();
  747. }
  748.  
  749. ($(div, '[pin]') || wrapper).insertAdjacentHTML(pinnable ? 'beforebegin' : 'beforeend',
  750. '<iframe class="instant-youtube-embed" allowtransparency="true" src="' +
  751. setUrlParams(div.FYTE.srcEmbedFixed, {
  752. html5: 1,
  753. autoplay: 1,
  754. autohide: 2,
  755. border: 0,
  756. controls: 1,
  757. fs: 1,
  758. showinfo: 1,
  759. ssl: 1,
  760. theme: 'dark',
  761. enablejsapi: 1,
  762. }) + '" frameborder="0" allowfullscreen width="100%" height="100%"></iframe>');
  763.  
  764. $(div, 'iframe').onload = function() {
  765. pauseOtherVideos(this);
  766. this.style.cssText = important(
  767. 'position:absolute; left:0; top:0; right:0; padding:0; margin:auto; opacity:1; transition:opacity 2s;');
  768.  
  769. div.setAttribute('iframe', '');
  770. div.setAttribute('playing', '');
  771. div.style.cssText += 'contain:none!important'; // allow fullscreen
  772. setTimeout(function() {
  773. $(div, 'img').style.display = 'none';
  774. var title = $(div, '.instant-youtube-title');
  775. if (title)
  776. title.remove();
  777. }, 2000);
  778. };
  779. }
  780.  
  781. function setUrlParams(url, params) {
  782. var names = Object.keys(params);
  783. url = url.replace(new RegExp('[?&](' + names.join('|') + ')(=[^?&]*)?', 'gi'), '');
  784. return url +
  785. (url.indexOf('?') > 0 ? '&' : '?') +
  786. names.map(function(n) { return n + '=' + params[n] }).join('&');
  787. }
  788.  
  789. function pauseOtherVideos(activePlayer) {
  790. $$(activePlayer.ownerDocument, '.instant-youtube-embed').forEach(function(v) {
  791. if (v == activePlayer)
  792. return;
  793. switch (v.localName) {
  794. case 'video':
  795. if (!v.paused)
  796. v.pause();
  797. break;
  798. case 'iframe':
  799. try { v.contentWindow.postMessage('{"event":"command", "func":"pauseVideo", "args":""}', '*') } catch(e) {}
  800. break;
  801. }
  802. });
  803. }
  804.  
  805. function showOptions(e) {
  806. var optionsButton = e.target;
  807. translateHTML(optionsButton, 'afterend', '\
  808. <div class="instant-youtube-options">\
  809. <label tl style="width: 100% !important;">Size:<br>\
  810. <select data-action="size-mode">\
  811. <option tl value="Original">Original\
  812. <option tl value="Fit to width">Fit to width\
  813. <option>360p\
  814. <option>480p\
  815. <option>720p\
  816. <option>1080p\
  817. <option tl value="Custom">Custom...\
  818. </select>\
  819. </label>\
  820. <label data-action="size-custom" ' + (resizeMode != 'Custom' ? 'disabled' : '') + '>\
  821. <input type="number" min="320" max="9999" tl-placeholder="width" data-action="width" step="1" value="' + (resizeWidth||'') + '">\
  822. x\
  823. <input type="number" min="240" max="9999" tl-placeholder="height" data-action="height" step="1" value="' + (resizeHeight||'') + '">\
  824. </label>\
  825. <label tl="content,title" title="Show storyboard preview on mouse hover at the bottom">\
  826. <input data-action="storyboard" type="checkbox" ' + (showStoryboard ? 'checked' : '') + '>\
  827. Storyboard thumbs\
  828. </label>\
  829. <label tl="content,title" title="Tip: shift-clicking thumbnails will use alternative player">\
  830. <input data-action="direct" type="checkbox" ' + (playDirectly ? 'checked' : '') + '>\
  831. Play directly\
  832. </label>\
  833. <label tl="content,title" title="Do not process customized videos with enablejsapi=1 parameter (requires page reload)">\
  834. <input data-action="safe" type="checkbox" ' + (skipCustom ? 'checked' : '') + '>\
  835. Safe\
  836. </label>\
  837. <label tl="content,title" title="Enable corner pinning controls when a video is playing">\
  838. <input data-action="pinnable" type="checkbox" ' + (pinnable ? 'checked' : '') + '>\
  839. Corner pins\
  840. </label>\
  841. <span data-action="buttons">\
  842. <button tl data-action="ok">OK</button>\
  843. <button tl data-action="cancel">Cancel</button>\
  844. </span>\
  845. </div>\
  846. ');
  847. var options = optionsButton.nextElementSibling;
  848.  
  849. options.addEventListener('keydown', function(e) {
  850. if (e.target.localName == 'input' &&
  851. !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey && e.key.match(/[.,]/))
  852. return false;
  853. });
  854.  
  855. $(options, '[data-action="size-mode"]').value = resizeMode;
  856. $(options, '[data-action="size-mode"]').addEventListener('change', function() {
  857. var v = this.value != 'Custom';
  858. var e = $(options, '[data-action="size-custom"]');
  859. e.children[0].disabled = e.children[1].disabled = v;
  860. v ? e.setAttribute('disabled', '') : e.removeAttribute('disabled');
  861. });
  862.  
  863. $(options, '[data-action="buttons"]').addEventListener('click', function(e) {
  864. if (e.target.dataset.action != 'ok') {
  865. options.remove();
  866. return;
  867. }
  868. var v, shouldAdjust;
  869. if (resizeMode != (v = $(options, '[data-action="size-mode"]').value)) {
  870. GM_setValue('resize', resizeMode = v);
  871. shouldAdjust = true;
  872. }
  873. if (resizeMode == 'Custom') {
  874. var w = $(options, '[data-action="width"]').value |0;
  875. var h = $(options, '[data-action="height"]').value |0;
  876. if (resizeWidth != w || resizeHeight != h) {
  877. updateCustomSize(w, h);
  878. GM_setValue('width', resizeWidth);
  879. GM_setValue('height', resizeHeight);
  880. shouldAdjust = true;
  881. }
  882. }
  883. if (showStoryboard != (v = $(options, '[data-action="storyboard"]').checked)) {
  884. GM_setValue('showStoryboard', showStoryboard = v);
  885. $$('.instant-youtube-container').forEach(updateHoverHandler);
  886. }
  887. if (playDirectly != (v = $(options, '[data-action="direct"]').checked)) {
  888. GM_setValue('playHTML5', playDirectly = v);
  889. $$('.instant-youtube-container .instant-youtube-link').forEach(function(e) {
  890. e.textContent = playDirectly ? 'Play with Youtube player' : 'Play directly (up to 720p)';
  891. });
  892. }
  893. if (skipCustom != (v = $(options, '[data-action="safe"]').checked)) {
  894. GM_setValue('skipCustom', skipCustom = v);
  895. }
  896. if (pinnable != (v = $(options, '[data-action="pinnable"]').checked)) {
  897. GM_setValue('pinnable', pinnable = v);
  898. }
  899.  
  900. options.remove();
  901.  
  902. if (shouldAdjust)
  903. adjustNodes(e, e.target.closest('.instant-youtube-container'));
  904. });
  905. }
  906.  
  907. function updateCustomSize(w, h) {
  908. resizeWidth = Math.min(9999, Math.max(320, w|0 || resizeWidth|0));
  909. resizeHeight = Math.min(9999, Math.max(240, h|0 || resizeHeight|0));
  910. }
  911.  
  912. function important(cssText) {
  913. return cssText.replace(/;/g, '!important;');
  914. }
  915.  
  916. function tryCatch(func) {
  917. try {
  918. return func();
  919. } catch(e) {
  920. console.log(e);
  921. }
  922. }
  923.  
  924. function getFunctionComment(fn) {
  925. return fn.toString().match(/\/\*([\s\S]*?)\*\/\s*\}$/)[1];
  926. }
  927.  
  928. function $(selORnode, sel) {
  929. return sel ? selORnode.querySelector(sel)
  930. : document.querySelector(selORnode);
  931. }
  932.  
  933. function $$(selORnode, sel) {
  934. return Array.prototype.slice.call(
  935. sel ? selORnode.querySelectorAll(sel)
  936. : document.querySelectorAll(selORnode));
  937. }
  938.  
  939. function overrideCSS(e, params) {
  940. var names = Object.keys(params);
  941. var style = e.style.cssText.replace(new RegExp('(^|\s|;)(' + names.join('|') + ')(:[^;]+)', 'gi'), '$1');
  942. e.style.cssText = style.replace(/[^;]\s*$/, '$&;').replace(/^\s*;\s*/, '') +
  943. names.map(function(n) { return n + ':' + params[n] + '!important' }).join(';') + ';';
  944. }
  945.  
  946. // fix dumb Firefox bug
  947. function floatPadding(node, style, dir) {
  948. var padding = style['padding' + dir];
  949. if (padding.indexOf('%') < 0)
  950. return parseFloat(padding);
  951. return parseFloat(padding) * (parseFloat(style.width) || node.clientWidth) / 100;
  952. }
  953.  
  954. function translateHTML(baseElement, place, html) {
  955. var tmp = document.createElement('div');
  956. tmp.innerHTML = html;
  957. $$(tmp, '[tl]').forEach(function(node) {
  958. (node.getAttribute('tl') || 'content').split(',').forEach(function(what) {
  959. var child, src, tl;
  960. if (what == 'content') {
  961. for (var i = node.childNodes.length-1, n; (i>=0) && (n=node.childNodes[i]); i--) {
  962. if (n.nodeType == Node.TEXT_NODE && n.textContent.trim()) {
  963. child = n;
  964. break;
  965. }
  966. }
  967. } else
  968. child = node.getAttributeNode(what);
  969. if (!child)
  970. return;
  971. src = child.textContent;
  972. srcTrimmed = src.trim();
  973. tl = src.replace(srcTrimmed, _(srcTrimmed));
  974. if (src != tl)
  975. child.textContent = tl;
  976. });
  977. });
  978. baseElement.insertAdjacentHTML(place, tmp.innerHTML);
  979. }
  980.  
  981. function initTL(src) {
  982. var tlSource = {
  983. 'watch on Youtube': {
  984. 'ru': 'открыть на Youtube',
  985. },
  986. 'Play with Youtube player': {
  987. 'ru': 'Включить плеер Youtube',
  988. },
  989. 'Play directly (up to 720p)': {
  990. 'ru': 'Включить напрямую (макс. 720p)',
  991. },
  992. 'Shift-click to use alternative player': {
  993. 'ru': 'Shift-клик для смены типа плеера',
  994. },
  995. 'Options': {
  996. 'ru': 'Опции',
  997. },
  998. 'Size:': {
  999. 'ru': 'Размер:',
  1000. },
  1001. 'Original': {
  1002. 'ru': 'Исходный',
  1003. },
  1004. 'Fit to width': {
  1005. 'ru': 'На всю ширину',
  1006. },
  1007. 'Custom...': {
  1008. 'ru': 'Настроить...',
  1009. },
  1010. 'width': {
  1011. 'ru': 'ширина',
  1012. },
  1013. 'height': {
  1014. 'ru': 'высота',
  1015. },
  1016. 'Storyboard thumbs': {
  1017. 'ru': 'Раскадровка',
  1018. },
  1019. 'Show storyboard preview on mouse hover at the bottom': {
  1020. 'ru': 'Показывать миникадры при наведении мыши на низ кавер-картинки',
  1021. },
  1022. 'Play directly': {
  1023. 'ru': 'Плеер браузера',
  1024. },
  1025. 'Tip: shift-clicking thumbnails will use alternative player': {
  1026. 'ru': 'Удерживайте клавишу Shift при щелчке на картинке для альтернативного плеера',
  1027. },
  1028. 'Safe': {
  1029. 'ru': 'Консервативный режим',
  1030. },
  1031. 'Corner pins': {
  1032. 'ru': 'Угловые шпильки',
  1033. },
  1034. 'Enable corner pinning controls when a video is playing': {
  1035. 'ru': 'Включить угловые шпильки для закрепления видео во время просмотра',
  1036. },
  1037. 'Do not process customized videos with enablejsapi=1 parameter (requires page reload)': {
  1038. 'ru': 'Не обрабатывать нестандартные видео с параметром enablejsapi=1 (подействует после обновления страницы)',
  1039. },
  1040. 'OK': {
  1041. 'ru': 'ОК',
  1042. },
  1043. 'Cancel': {
  1044. 'ru': 'Оменить',
  1045. },
  1046. };
  1047. var browserLang = navigator.language || navigator.languages && navigator.languages[0] || '';
  1048. var browserLangMajor = browserLang.replace(/-.+/, '');
  1049. var tl = {};
  1050. Object.keys(tlSource).forEach(function(k) {
  1051. var langs = tlSource[k];
  1052. var text = langs[browserLang] || langs[browserLangMajor];
  1053. if (text)
  1054. tl[k] = text;
  1055. });
  1056. return function(src) { return tl[src] || src };
  1057. }
  1058.  
  1059. function injectStylesIfNeeded(force) {
  1060. if (!fytedom.length && !force)
  1061. return;
  1062. var styledom = $('style#instant-youtube-styles');
  1063. if (styledom) {
  1064. // move our rules to the end of HEAD to increase CSS specificity
  1065. if (styledom.nextElementSibling && document.head)
  1066. document.head.insertBefore(styledom, null);
  1067. return;
  1068. }
  1069. styledom = (document.head || document.documentElement).appendChild(document.createElement('style'));
  1070. styledom.id = 'instant-youtube-styles';
  1071. styledom.textContent = important(getFunctionComment(function() { /*
  1072. .instant-youtube-container {
  1073. contain: strict;
  1074. position: relative;
  1075. overflow: hidden;
  1076. cursor: pointer;
  1077. padding: 0;
  1078. margin: 0;
  1079. font: normal 14px/1.0 sans-serif, Arial, Helvetica, Verdana;
  1080. text-align: center;
  1081. background: black;
  1082. }
  1083. .instant-youtube-container[disabled] {
  1084. background: #888;
  1085. }
  1086. .instant-youtube-container[disabled] .instant-youtube-storyboard {
  1087. display: none;
  1088. }
  1089. .instant-youtube-container .instant-youtube-wrapper {
  1090. width: 100%;
  1091. height: 100%;
  1092. }
  1093. .instant-youtube-container .instant-youtube-play-button {
  1094. display: block;
  1095. position: absolute;
  1096. width: 85px;
  1097. height: 60px;
  1098. left: 0;
  1099. right: 0;
  1100. top: 0;
  1101. bottom: 0;
  1102. margin: auto;
  1103. }
  1104. .instant-youtube-container .instant-youtube-loading-button {
  1105. display: block;
  1106. position: absolute;
  1107. width: 20px;
  1108. height: 20px;
  1109. left: 0;
  1110. right: 0;
  1111. top: 0;
  1112. bottom: 0;
  1113. padding: 0;
  1114. margin: auto;
  1115. pointer-events: none;
  1116. background: url("data:image/gif;base64,R0lGODlhFAAUAJEDAMzMzLOzs39/f////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQFCgADACwAAAAAFAAUAAACPJyPqcuNItyCUJoQBo0ANIxpXOctYHaQpYkiHfM2cUrCNT0nqr4uudsz/IC5na/2Mh4Hu+HR6YBaplRDAQAh+QQFCgADACwEAAIADAAGAAACFpwdcYupC8BwSogR46xWZHl0l8ZYQwEAIfkEBQoAAwAsCAACAAoACgAAAhccMKl2uHxGCCvO+eTNmishcCCYjWEZFgAh+QQFCgADACwMAAQABgAMAAACFxwweaebhl4K4VE6r61DiOd5SfiN5VAAACH5BAUKAAMALAgACAAKAAoAAAIYnD8AeKqcHIwwhGntEWLkO3CcB4biNEIFACH5BAUKAAMALAQADAAMAAYAAAIWnDSpAHa4GHgohCHbGdbipnBdSHphAQAh+QQFCgADACwCAAgACgAKAAACF5w0qXa4fF6KUoVQ75UaA7Bs3yeNYAkWACH5BAUKAAMALAIABAAGAAwAAAIXnCU2iMfaRghqTmMp1moAoHyfIYIkWAAAOw==");
  1117. }
  1118. .instant-youtube-container:hover .ytp-large-play-button-svg {
  1119. fill: #CC181E;
  1120. }
  1121. .instant-youtube-container .instant-youtube-link {
  1122. display: block;
  1123. position: absolute;
  1124. width: 20em;
  1125. height: 20px;
  1126. top: 50%;
  1127. left: 0;
  1128. right: 0;
  1129. margin: 60px auto;
  1130. padding: 0;
  1131. border: none;
  1132. text-align: center;
  1133. text-decoration: none;
  1134. text-shadow: 1px 1px 3px black;
  1135. font-weight: bold;
  1136. color: white;
  1137. }
  1138. .instant-youtube-container span.instant-youtube-link {
  1139. z-index: 8;
  1140. font-weight: normal;
  1141. font-size: 12px;
  1142. }
  1143. .instant-youtube-container .instant-youtube-link:hover {
  1144. text-decoration: underline;
  1145. color: white;
  1146. background: transparent;
  1147. }
  1148. .instant-youtube-container iframe {
  1149. z-index: 10;
  1150. }
  1151. .instant-youtube-container .instant-youtube-title {
  1152. z-index: 9;
  1153. display: block;
  1154. position: absolute;
  1155. width: auto;
  1156. top: 0;
  1157. left: 0;
  1158. right: 0;
  1159. margin: 0;
  1160. padding: 7px;
  1161. border: none;
  1162. text-shadow: 1px 1px 2px black;
  1163. text-align: center;
  1164. text-decoration: none;
  1165. color: white;
  1166. background-color: rgba(0, 0, 0, 0.5);
  1167. }
  1168. .instant-youtube-container .instant-youtube-title strong {
  1169. font: bold 14px/1.0 sans-serif, Arial, Helvetica, Verdana;
  1170. }
  1171. .instant-youtube-container .instant-youtube-title strong:after {
  1172. content: " - $tl:'watch on Youtube'";
  1173. font-weight: normal;
  1174. margin-right: 1ex;
  1175. }
  1176. .instant-youtube-container .instant-youtube-title span {
  1177. color: inherit;
  1178. }
  1179. .instant-youtube-container .instant-youtube-title span:before {
  1180. content: "(";
  1181. }
  1182. .instant-youtube-container .instant-youtube-title span:after {
  1183. content: ")";
  1184. }
  1185. @-webkit-keyframes instant-youtube-fadein {
  1186. from { opacity: 0 }
  1187. to { opacity: 1 }
  1188. }
  1189. @-moz-keyframes instant-youtube-fadein {
  1190. from { opacity: 0 }
  1191. to { opacity: 1 }
  1192. }
  1193. @keyframes instant-youtube-fadein {
  1194. from { opacity: 0 }
  1195. to { opacity: 1 }
  1196. }
  1197. .instant-youtube-container:not(:hover) .instant-youtube-title[hidden] {
  1198. display: none;
  1199. margin: 0;
  1200. }
  1201. .instant-youtube-container .instant-youtube-title:hover {
  1202. text-decoration: underline;
  1203. }
  1204. .instant-youtube-container .instant-youtube-title strong {
  1205. color: white;
  1206. }
  1207. .instant-youtube-container .instant-youtube-options-button {
  1208. opacity: 0.6;
  1209. position: absolute;
  1210. right: 0;
  1211. bottom: 0;
  1212. margin: 0;
  1213. padding: 1.5ex 2ex;
  1214. font-size: 11px;
  1215. text-shadow: 1px 1px 2px black;
  1216. color: white;
  1217. }
  1218. .instant-youtube-container .instant-youtube-options-button:hover {
  1219. opacity: 1;
  1220. background: rgba(0, 0, 0, 0.5);
  1221. }
  1222. .instant-youtube-container .instant-youtube-options {
  1223. display: flex;
  1224. position: absolute;
  1225. right: 0;
  1226. bottom: 0;
  1227. margin: 0;
  1228. padding: 2ex 1ex 2ex 2ex;
  1229. flex-direction: column;
  1230. align-items: flex-start;
  1231. line-height: 1.5;
  1232. text-align: left;
  1233. opacity: 1;
  1234. color: white;
  1235. background: black;
  1236. }
  1237. .instant-youtube-container .instant-youtube-options * {
  1238. width: auto;
  1239. height: auto;
  1240. margin: 0;
  1241. padding: 0;
  1242. font: inherit;
  1243. font-size: 13px;
  1244. vertical-align: middle;
  1245. text-transform: none;
  1246. text-align: left;
  1247. border-radius: 0;
  1248. text-decoration: none;
  1249. color: white;
  1250. background: black;
  1251. }
  1252. .instant-youtube-container .instant-youtube-options > label {
  1253. margin-top: 1ex;
  1254. }
  1255. .instant-youtube-container .instant-youtube-options > label > * {
  1256. display: inline;
  1257. }
  1258. .instant-youtube-container .instant-youtube-options select {
  1259. width: 20ex;
  1260. padding: .5ex .25ex;
  1261. border: 1px solid #444;
  1262. -webkit-appearance: menulist;
  1263. }
  1264. .instant-youtube-container .instant-youtube-options [data-action="size-custom"] input {
  1265. width: 9ex;
  1266. padding: .5ex .5ex .4ex;
  1267. border: 1px solid #666;
  1268. }
  1269. .instant-youtube-container .instant-youtube-options [data-action="buttons"] {
  1270. margin-top: 1em;
  1271. }
  1272. .instant-youtube-container .instant-youtube-options button {
  1273. margin: 0 1ex 0 0;
  1274. padding: .5ex 2ex;
  1275. border: 2px solid gray;
  1276. font-weight: bold;
  1277. }
  1278. .instant-youtube-container .instant-youtube-options button:hover {
  1279. border-color: white;
  1280. }
  1281. .instant-youtube-container .instant-youtube-options > [disabled] {
  1282. opacity: 0.25;
  1283. }
  1284. .instant-youtube-container .instant-youtube-storyboard {
  1285. height: 33%;
  1286. max-height: 90px;
  1287. display: block;
  1288. position: absolute;
  1289. left: 0;
  1290. right: 0;
  1291. bottom: 0;
  1292. overflow: visible;
  1293. overflow-x: visible;
  1294. overflow-y: visible;
  1295. }
  1296. .instant-youtube-container .instant-youtube-storyboard:hover {
  1297. background-color: rgba(0,0,0,0.3);
  1298. }
  1299. .instant-youtube-container .instant-youtube-storyboard[disabled] {
  1300. display:none;
  1301. }
  1302. .instant-youtube-container .instant-youtube-storyboard div {
  1303. display: block;
  1304. position: absolute;
  1305. bottom: 0px;
  1306. pointer-events: none;
  1307. border: 3px solid #888;
  1308. box-shadow: 2px 2px 10px black;
  1309. transition: opacity .25s ease;
  1310. background-color: transparent;
  1311. background-origin: content-box;
  1312. opacity: 0;
  1313. }
  1314. .instant-youtube-container .instant-youtube-storyboard:hover div {
  1315. opacity: 1;
  1316. }
  1317. .instant-youtube-container [pin] {
  1318. position: absolute;
  1319. width: 0;
  1320. height: 0;
  1321. margin: 0;
  1322. padding: 0;
  1323. opacity: 1;
  1324. top: auto; bottom: auto; left: auto; right: auto;
  1325. border-style: solid;
  1326. transition: opacity 3s;
  1327. opacity: 0;
  1328. z-index: 100;
  1329. }
  1330. .instant-youtube-container[playing]:hover [pin] {
  1331. opacity: 1;
  1332. }
  1333. .instant-youtube-container[playing] [pin]:hover {
  1334. cursor: alias;
  1335. }
  1336. .instant-youtube-container [pin=top-left][active] { border-top-color: green; }
  1337. .instant-youtube-container [pin=top-left]:hover { border-top-color: #fc0; }
  1338. .instant-youtube-container [pin=top-left] {
  1339. top: 0; left: 0;
  1340. border-width: 10px 10px 0 0;
  1341. border-color: red transparent transparent transparent;
  1342. }
  1343. .instant-youtube-container [pin=top-right][active] { border-right-color: green; }
  1344. .instant-youtube-container [pin=top-right]:hover { border-right-color: #fc0; }
  1345. .instant-youtube-container [pin=top-right] {
  1346. top: 0; right: 0;
  1347. border-width: 0 10px 10px 0;
  1348. border-color: transparent red transparent transparent;
  1349. }
  1350. .instant-youtube-container [pin=bottom-right][active] { border-bottom-color: green; }
  1351. .instant-youtube-container [pin=bottom-right]:hover { border-bottom-color: #fc0; }
  1352. .instant-youtube-container [pin=bottom-right] {
  1353. bottom: 0; right: 0;
  1354. border-width: 0 0 10px 10px;
  1355. border-color: transparent transparent red transparent;
  1356. }
  1357. .instant-youtube-container [pin=bottom-left][active] { border-left-color: green; }
  1358. .instant-youtube-container [pin=bottom-left]:hover { border-left-color: #fc0; }
  1359. .instant-youtube-container [pin=bottom-left] {
  1360. bottom: 0; left: 0;
  1361. border-width: 10px 0 0 10px;
  1362. border-color: transparent transparent transparent red;
  1363. }
  1364. */}).replace(/\$tl:'(.+?)'/g, function(m, m1) { return _(m1) })
  1365. );
  1366. }