1. // ==UserScript==
  2. // @name Fast Youtube embedded player
  3. // @description Fast HTML5 direct playback at 720p is used when selected and available. The script also 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.
  4. // @version 1.5
  5. // @include *
  6. // @author wOxxOm
  7. // @namespace wOxxOm.scripts
  8. // @license MIT License
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_addStyle
  12. // @grant GM_xmlhttpRequest
  13. // @connect-src www.youtube.com
  14. // @run-at document-start
  15. // @require https://greasyfork.org/scripts/12228/code/setMutationHandler.js
  16. // ==/UserScript==
  17.  
  18. /* jshint lastsemic:true, multistr:true, laxbreak:true, -W041, -W084 */
  19.  
  20. var resizeToFit = GM_getValue('resize', true);
  21. var playHTML5 = GM_getValue('playHTML5', false);
  22. var embedSelector = 'iframe[src*="youtube.com/embed"], embed[src*="youtube.com/v"]';
  23.  
  24. processNodes(document.querySelectorAll(embedSelector));
  25.  
  26. document.addEventListener('DOMContentLoaded', function() {
  27. if (resizeToFit)
  28. adjustNodes();
  29. processNodes(document.querySelectorAll(embedSelector));
  30. setMutationHandler(document, embedSelector, processNodes);
  31. GM_addStyle('\
  32. :root .instant-youtube-container {position:relative; overflow:hidden; cursor:pointer; background-color:black; padding:0; margin:0; font:normal 14px/1.0 sans-serif,Arial,Helvetica,Verdana;}\
  33. :root .instant-youtube-container .instant-youtube-thumbnail {transition:opacity 0.1s ease-out; opacity:0; padding:0; margin:0}\
  34. :root .instant-youtube-container .instant-youtube-play-button {position:absolute; left:0; right:0; top:0; bottom:0; margin:auto; width:85px; height:60px}\
  35. :root .instant-youtube-container .instant-youtube-loading-button {position:absolute; left:0; right:0; top:0; bottom:0; padding:0; margin:auto; display:block; width:20px; height:20px; background: url("data:image/gif;base64,R0lGODlhFAAUAJEDAMzMzLOzs39/f////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQFCgADACwAAAAAFAAUAAACPJyPqcuNItyCUJoQBo0ANIxpXOctYHaQpYkiHfM2cUrCNT0nqr4uudsz/IC5na/2Mh4Hu+HR6YBaplRDAQAh+QQFCgADACwEAAIADAAGAAACFpwdcYupC8BwSogR46xWZHl0l8ZYQwEAIfkEBQoAAwAsCAACAAoACgAAAhccMKl2uHxGCCvO+eTNmishcCCYjWEZFgAh+QQFCgADACwMAAQABgAMAAACFxwweaebhl4K4VE6r61DiOd5SfiN5VAAACH5BAUKAAMALAgACAAKAAoAAAIYnD8AeKqcHIwwhGntEWLkO3CcB4biNEIFACH5BAUKAAMALAQADAAMAAYAAAIWnDSpAHa4GHgohCHbGdbipnBdSHphAQAh+QQFCgADACwCAAgACgAKAAACF5w0qXa4fF6KUoVQ75UaA7Bs3yeNYAkWACH5BAUKAAMALAIABAAGAAwAAAIXnCU2iMfaRghqTmMp1moAoHyfIYIkWAAAOw==")}\
  36. :root .instant-youtube-container:hover .ytp-large-play-button-svg {fill:#CC181E}\
  37. :root .instant-youtube-container .instant-youtube-link, .instant-youtube-container .instant-youtube-link:link, .instant-youtube-container .instant-youtube-link:visited, .instant-youtube-container .instant-youtube-link:active, .instant-youtube-container .instant-youtube-link:focus {\
  38. position:absolute; top:50%; left:0; right:0; width:20em; height:1.7em; margin:60px auto; padding:0; border:none; \
  39. display:block; text-align:center; text-decoration:none; color:white; text-shadow:1px 1px 3px black; font-weight:bold;\
  40. }\
  41. :root .instant-youtube-container span.instant-youtube-link {font-weight:normal; font-size:12px}\
  42. :root .instant-youtube-container .instant-youtube-link:hover {color:white; text-decoration:underline; background:transparent}\
  43. :root .instant-youtube-container .instant-youtube-embed {z-index:10; position:absolute; left:0; top:0; padding:0; margin:0; height:inherit!important; opacity:0; transition:opacity 2s!important}\
  44. :root .instant-youtube-container .instant-youtube-persistent-title {\
  45. display:block; z-index:9; background-color:rgba(0,0,0,0.5); color: white;\
  46. width:100%; height: 1.9em; top:0; left:0; right:0; position:absolute; z-index: 9;\
  47. color:white; text-shadow:1px 1px 2px black; text-align:center; text-decoration:none;\
  48. margin:0; padding:0.5em 0.5em 0.2em;\
  49. }\
  50. @-webkit-keyframes instant-youtube-fadein { from {opacity:0} to {opacity:1} }\
  51. @-moz-keyframes instant-youtube-fadein { from {opacity:0} to {opacity:1} }\
  52. @keyframes instant-youtube-fadein { from {opacity:0} to {opacity:1} }\
  53. :root .instant-youtube-container:not(:hover) .instant-youtube-persistent-title[hidden] {display:none; margin:0}\
  54. :root .instant-youtube-container .instant-youtube-persistent-title:hover {text-decoration:underline}\
  55. :root .instant-youtube-container .instant-youtube-persistent-title strong {color:white}\
  56. :root .instant-youtube-container .instant-youtube-options {position:absolute; right:0; bottom:0; color:white; text-shadow:1px 1px 2px black; padding:0; margin:0 }\
  57. :root .instant-youtube-container .instant-youtube-options * {font-size:13px; vertical-align:middle; padding:0; margin:0}\
  58. ');
  59. });
  60.  
  61. function processNodes(nodes) {
  62. for (var i=0, nl=nodes.length, n; i<nl && (n=nodes[i]); i++) {
  63. if (!n.parentNode ||
  64. n.className.indexOf('instant-youtube-') >= 0 ||
  65. n.src.indexOf('autoplay=1') > 0 ||
  66. n.onload && getComputedStyle(n.parentNode).display == 'none' // skip some retarded loaders
  67. )
  68. continue;
  69.  
  70. var id = n.src.match(/(?:embed\/|v[=\/])([^\s,.()\[\]?]+?)(?:[&?\/].*|$)/);
  71. if (!id)
  72. continue;
  73. id = id[1];
  74.  
  75. for (var np=n.parentNode, npw; np && !(npw=np.clientWidth); np=np.parentNode) {}
  76.  
  77. var containerWidth = resizeToFit ? npw : n.clientWidth;
  78. var containerHeight = resizeToFit ? npw / 16 * 9 : n.clientHeight;
  79.  
  80. var div = document.createElement('div');
  81. div.className = 'instant-youtube-container';
  82. div.srcEmbed = n.src;
  83. div.style.maxWidth = containerWidth + 'px';
  84. div.style.height = containerHeight + 'px';
  85. div.originalWidth = n.width;
  86. div.originalHeight = n.height;
  87. div.videoID = id;
  88.  
  89. var img = div.appendChild(document.createElement('img'));
  90. img.className = 'instant-youtube-thumbnail';
  91. img.src = 'https://i.ytimg.com/vi' + (window.chrome?'_webp':'') + '/' + id + '/maxresdefault.' + (window.chrome?'webp':'jpg');
  92. if (n.clientHeight) {
  93. img.style.maxWidth = 'auto';
  94. img.style.width = (containerHeight / 9 * 16) + 'px';
  95. img.style.marginLeft = Math.round((containerWidth - containerHeight / 9 * 16) / 2) + 'px';
  96. }
  97. img.title = 'Shift-click to use alternative player';
  98. img.onload = function(e) {
  99. var img = e.target;
  100. if (img.naturalWidth <= 120)
  101. img.onerror(e);
  102. else {
  103. img.style.marginTop = ((img.parentNode.clientHeight - img.clientHeight) / 2).toFixed(1) + 'px';
  104. img.style.setProperty('opacity', '1');
  105. }
  106. if (img.parentNode.iframeHasLoader)
  107. img.parentNode.parentNode.style.display = '';
  108. };
  109. img.onerror = function(e) {
  110. var img = e.target;
  111. if (img.src.indexOf('maxresdefault') > 0)
  112. img.src = img.src.replace('maxresdefault','hqdefault');
  113. else if (img.src.indexOf('hqdefault.webp') > 0)
  114. img.src = img.src.replace('_webp','').replace('.webp','.jpg');
  115. };
  116.  
  117. GM_xmlhttpRequest({
  118. method: 'GET',
  119. url: div.srcEmbed.replace(/\/(embed\/|v[=\/])/, '/watch?v='),
  120. context: div,
  121. onload: parseWatchPage
  122. });
  123.  
  124. var rnd = Math.random().toString().substr(3);
  125. div.insertAdjacentHTML('beforeend', '\
  126. <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>\
  127. <span class="instant-youtube-link" style="top:calc(50% + 1em)">' + (playHTML5 ? 'Play with Youtube player' : 'Play directly (up to 720p)') + '</span>\
  128. <div class="instant-youtube-options">\
  129. <label for="instant-youtube-options-resize-' + rnd + '">Resize to fit</label>\
  130. <input type="checkbox" id="instant-youtube-options-resize-' + rnd + '"' + (resizeToFit ? ' checked' : '') + '>\
  131. <label for="instant-youtube-options-html5-' + rnd + '" title="Tip: shift-clicking thumbnails will use alternative player">Play directly</label>\
  132. <input type="checkbox" id="instant-youtube-options-html5-' + rnd + '"' + (playHTML5 ? ' checked' : '') + '>\
  133. </div>\
  134. ');
  135.  
  136. if (n.parentNode.localName == 'object')
  137. n = n.parentNode;
  138. n.parentNode.insertBefore(div, n);
  139. n.remove();
  140.  
  141. div.addEventListener('click', clickHandler);
  142. }
  143.  
  144. return true;
  145. }
  146.  
  147. function adjustNodes() {
  148. [].forEach.call(document.getElementsByClassName('instant-youtube-container'), function(n) {
  149. if (Math.abs(n.parentNode.clientWidth - parseFloat(n.style.maxWidth)) > 2) {
  150. var img = n.getElementsByClassName('instant-youtube-thumbnail')[0];
  151. var npw = n.parentNode.clientWidth;
  152. var nph = npw / 16 * 9;
  153. n.style.maxWidth = img.style.width = npw + 'px';
  154. n.style.height = nph + 'px';
  155. img.style.marginLeft = Math.round((npw - nph / 9 * 16) / 2) + 'px';
  156. img.style.marginTop = ((nph - img.clientHeight) / 2).toFixed(1) + 'px';
  157. }
  158. });
  159. }
  160.  
  161. function parseWatchPage(response) {
  162. var div = response.context;
  163. var m = response.responseText.match(/ytplayer\.config\s*=\s*(\{.+?\});\s*ytplayer\.load/);
  164. var videoInfo = [];
  165. if (m) {
  166. var cfg = JSON.parse(m[1]), streams = {};
  167. cfg.args.url_encoded_fmt_stream_map.split(',').forEach(function(stream) {
  168. var params = {};
  169. stream.split('&').forEach(function(kv) { params[kv.split('=')[0]] = decodeURIComponent(kv.split('=')[1]) });
  170. streams[params.itag] = params;
  171. });
  172. cfg.args.fmt_list.split(',').forEach(function(fmt) {
  173. var itag = fmt.split('/')[0], dimensions = fmt.split('/')[1], stream = streams[itag];
  174. if (stream) {
  175. videoInfo.push({
  176. onerror: switchToIFrame.bind(div),
  177. src: stream.url,
  178. title: stream.quality + ', ' + dimensions + ', ' + stream.type
  179. });
  180. }
  181. });
  182. } else {
  183. var rx = /url=([^=]+?mime%3Dvideo%252F(?:mp4|webm)[^=]+?)(?:,quality|,itag|.u0026)/g;
  184. var text = response.responseText.split('url_encoded_fmt_stream_map')[1];
  185. while (m = rx.exec(text)) {
  186. videoInfo.push({
  187. onerror: switchToIFrame.bind(div),
  188. src: decodeURIComponent(decodeURIComponent(m[1]))
  189. });
  190. }
  191. }
  192.  
  193. var title = response.responseText.match(/<title>(.+?)(?:\s*-\s*YouTube)?<\/title>/);
  194. if (title) {
  195. var duration = response.responseText.match(/<meta itemprop="duration" content="\D*(\d+.*?)S?">/);
  196. var a = div.appendChild(document.createElement('a'));
  197. a.className = 'instant-youtube-persistent-title';
  198. a.innerHTML = '<strong>' + title[1] + '</strong> - watch on Youtube' +
  199. (duration ? ' (' + duration[1].replace(/[HM]/, ':0').replace(/\b0(\d\d)/, '$1') + ')' : '');
  200. a.href = 'https://www.youtube.com/watch?v=' + div.videoID;
  201. a.target = '_blank';
  202. ['animation', 'webkitAnimation', 'mozAnimation'].some(function(prop) {
  203. if (prop in a.style) {
  204. a.style[prop] = 'instant-youtube-fadein 1s ease-in';
  205. setTimeout(function() { a.style[prop] = '' }, 1000);
  206. return true;
  207. }
  208. });
  209. }
  210.  
  211. if (videoInfo.length)
  212. div.videoInfo = videoInfo;
  213. }
  214.  
  215. function clickHandler(e) {
  216. if (e.target.href)
  217. return;
  218. if (e.target.parentNode.className == 'instant-youtube-options')
  219. return options(e);
  220. for (var div = e.target; !div.srcEmbed; div = div.parentNode) {}
  221. div.removeEventListener('click', clickHandler);
  222. div.querySelector('svg').outerHTML = '<span class="instant-youtube-loading-button"></span>';
  223.  
  224. var alternateMode = e.target.className == 'instant-youtube-link' || e.shiftKey;
  225. if (!div.videoInfo || playHTML5 && alternateMode || !playHTML5 && !alternateMode) {
  226. switchToIFrame.call(div);
  227. return;
  228. }
  229.  
  230. div.querySelector('span.instant-youtube-link').style.display = 'none';
  231.  
  232. var video = document.createElement('video');
  233. video.controls = true;
  234. video.autoplay = true;
  235. video.width = div.clientWidth;
  236. video.height = div.clientHeight;
  237. video.className = 'instant-youtube-embed';
  238. video.volume = GM_getValue('volume', 0.5);
  239.  
  240. div.videoInfo.forEach(function(src) {
  241. var srcdom = video.appendChild(document.createElement('source'));
  242. Object.keys(src).forEach(function(k) { srcdom[k] = src[k] });
  243. });
  244.  
  245. if (window.chrome) {
  246. video.addEventListener('click', function(e) { if (this.paused) {this.play()} else {this.pause()} });
  247. }
  248. video.interval = (function() {
  249. return setInterval(function() {
  250. if (video.volume != GM_setValue('volume', 0.5))
  251. GM_setValue('volume', video.volume);
  252. }, 1000);
  253. })();
  254. var title = div.querySelector('.instant-youtube-persistent-title');
  255. if (title) {
  256. video.onpause = function() { title.removeAttribute('hidden') };
  257. video.onplay = function() { title.setAttribute('hidden', true) };
  258. }
  259. video.onloadeddata = function(e) {
  260. pauseOtherVideos(this);
  261. div.appendChild(this);
  262. this.style.opacity = 1;
  263. var img = div.querySelector('img');
  264. img.style.transition = 'opacity 1s';
  265. img.style.opacity = 0;
  266. };
  267. div.querySelector('.instant-youtube-options').style.display = 'none';
  268. }
  269.  
  270. function switchToIFrame(e) {
  271. var div = this;
  272. if (e) console.log('Direct linking failed on %s, switching to IFRAME player', div.srcEmbed);
  273. var iframeHTML = '<iframe class="instant-youtube-embed" allowtransparency="true" src="' + div.srcEmbed + (div.srcEmbed.indexOf('?') > 0 ? '' : '?') +
  274. '&autoplay=1' +
  275. '&autohide=2' +
  276. '&border=0' +
  277. '&controls=1' +
  278. '&fs=1' +
  279. '&showinfo=1' +
  280. '&ssl=1' +
  281. '&theme=dark' +
  282. '" frameborder="0" allowfullscreen="allowfullscreen" width="' + div.clientWidth + '" height="' + div.clientHeight + '"></iframe>';
  283. div.insertAdjacentHTML('beforeend', iframeHTML);
  284. div.lastElementChild.onload = function() {
  285. pauseOtherVideos(this);
  286. this.style.opacity = 1;
  287. var div = this.closest('.instant-youtube-container');
  288. setTimeout(function() {
  289. div.querySelector('img').style.display = 'none';
  290. var title = div.querySelector('.instant-youtube-persistent-title');
  291. if (title)
  292. title.remove();
  293. }, 2000);
  294. };
  295. if (e && e.target) {
  296. var video = e.target.parentNode;
  297. while (video.lastElementChild)
  298. video.lastElementChild.remove();
  299. }
  300. }
  301.  
  302. function pauseOtherVideos(activePlayer) {
  303. [].forEach.call(activePlayer.ownerDocument.getElementsByClassName('instant-youtube-embed'), function(v) {
  304. if (v == activePlayer)
  305. return;
  306. switch (v.localName) {
  307. case 'video':
  308. if (!v.paused)
  309. v.pause();
  310. break;
  311. case 'iframe':
  312. (function() { try { v.contentWindow.postMessage('{"event":"command", "func":"pauseVideo", "args":""}', '*') } catch(e) {} })();
  313. break;
  314. }
  315. });
  316. }
  317.  
  318. function options(e) {
  319. if (e.target.id.indexOf('instant-youtube-options-resize') == 0) {
  320. resizeToFit = e.target.checked;
  321. GM_setValue('resize', resizeToFit);
  322. [].forEach.call(document.querySelectorAll('div.instant-youtube-container'), function(n) {
  323. var w = n.originalWidth, h = n.originalHeight, img = n.querySelector('img');
  324. if (resizeToFit) {
  325. for (var np=n.parentNode, npw; np && !(npw=np.clientWidth); np=np.parentNode) {}
  326. w = npw;
  327. h = npw / 16 * 9;
  328. }
  329. n.style.maxWidth = w + 'px!important';
  330. n.style.height = h + 'px!important';
  331. img.style.width = (h / 9 * 16) + 'px!important';
  332. img.style.marginLeft = Math.round((w - h / 9 * 16) / 2) + 'px!important';
  333. img.style.marginTop = ((h - img.clientHeight) / 2).toFixed(1) + 'px!important';
  334. n.querySelector('input').checked = resizeToFit;
  335. var video = n.querySelector('video');
  336. if (video) {
  337. video.width = w;
  338. video.height = h;
  339. }
  340. });
  341. } else if (e.target.id.indexOf('instant-youtube-options-html5') == 0) {
  342. playHTML5 = e.target.checked;
  343. GM_setValue('playHTML5', playHTML5);
  344. [].forEach.call(document.querySelectorAll('div.instant-youtube-container'), function(div) {
  345. div.querySelector('span.instant-youtube-link').textContent = playHTML5 ? 'Play with Youtube player' : 'Play directly (up to 720p)';
  346. div.querySelector('[id^="instant-youtube-options-html5"]').checked = playHTML5;
  347. });
  348. }
  349. }