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 {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 || n.className.indexOf('instant-youtube-') >= 0 || n.src.indexOf('autoplay=1') > 0)
  64. continue;
  65.  
  66. var id = n.src.match(/(?:embed\/|v[=\/])([^\s,.()\[\]?]+?)(?:[&?\/].*|$)/);
  67. if (!id)
  68. continue;
  69. id = id[1];
  70.  
  71. for (var np=n.parentNode, npw; np && !(npw=np.clientWidth); np=np.parentNode) {}
  72.  
  73. var containerWidth = resizeToFit ? npw : n.clientWidth;
  74. var containerHeight = resizeToFit ? npw / 16 * 9 : n.clientHeight;
  75.  
  76. var div = document.createElement('div');
  77. div.className = 'instant-youtube-container';
  78. div.srcEmbed = n.src;
  79. div.style.maxWidth = containerWidth + 'px';
  80. div.style.height = containerHeight + 'px';
  81. div.originalWidth = n.width;
  82. div.originalHeight = n.height;
  83.  
  84. var img = div.appendChild(document.createElement('img'));
  85. img.className = 'instant-youtube-thumbnail';
  86. img.src = 'https://i.ytimg.com/vi' + (window.chrome?'_webp':'') + '/' + id + '/maxresdefault.' + (window.chrome?'webp':'jpg');
  87. if (n.clientHeight) {
  88. img.style.maxWidth = 'auto';
  89. img.style.width = (containerHeight / 9 * 16) + 'px';
  90. img.style.marginLeft = Math.round((containerWidth - containerHeight / 9 * 16) / 2) + 'px';
  91. }
  92. img.title = 'Shift-click to use alternative player';
  93. img.onload = function(e) {
  94. var img = e.target;
  95. if (img.naturalWidth <= 120)
  96. img.onerror(e);
  97. else {
  98. img.style.marginTop = ((img.parentNode.clientHeight - img.clientHeight) / 2).toFixed(1) + 'px';
  99. img.style.setProperty('opacity', '1');
  100. }
  101. };
  102. img.onerror = function(e) {
  103. var img = e.target;
  104. if (img.src.indexOf('maxresdefault') > 0)
  105. img.src = img.src.replace('maxresdefault','hqdefault');
  106. else if (img.src.indexOf('hqdefault.webp') > 0)
  107. img.src = img.src.replace('_webp','').replace('.webp','.jpg');
  108. };
  109.  
  110. GM_xmlhttpRequest({
  111. method: 'GET',
  112. url: div.srcEmbed.replace(/\/(embed\/|v[=\/])/, '/watch?v='),
  113. context: div,
  114. onload: parseWatchPage
  115. });
  116.  
  117. var rnd = Math.random().toString().substr(3);
  118. div.insertAdjacentHTML('beforeend', '\
  119. <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>\
  120. <a class="instant-youtube-link" href="https://www.youtube.com/watch?v=' + id + '" target="_blank">Watch on Youtube</a>\
  121. <span class="instant-youtube-link" style="top:calc(50% + 2em)">' + (playHTML5 ? 'Play with Youtube player' : 'Play directly (up to 720p)') + '</span>\
  122. <div class="instant-youtube-options">\
  123. <label for="instant-youtube-options-resize-' + rnd + '">Resize to fit</label>\
  124. <input type="checkbox" id="instant-youtube-options-resize-' + rnd + '"' + (resizeToFit ? ' checked' : '') + '>\
  125. <label for="instant-youtube-options-html5-' + rnd + '" title="Tip: shift-clicking thumbnails will use alternative player">Play directly</label>\
  126. <input type="checkbox" id="instant-youtube-options-html5-' + rnd + '"' + (playHTML5 ? ' checked' : '') + '>\
  127. </div>\
  128. ');
  129.  
  130. if (n.parentNode.localName == 'object')
  131. n = n.parentNode;
  132. n.parentNode.insertBefore(div, n);
  133. n.remove();
  134.  
  135. div.addEventListener('click', clickHandler);
  136. }
  137.  
  138. return true;
  139. }
  140.  
  141. function adjustNodes() {
  142. [].forEach.call(document.getElementsByClassName('instant-youtube-container'), function(n) {
  143. if (Math.abs(n.parentNode.clientWidth - parseFloat(n.style.maxWidth)) > 2) {
  144. var img = n.getElementsByClassName('instant-youtube-thumbnail')[0];
  145. var npw = n.parentNode.clientWidth;
  146. var nph = npw / 16 * 9;
  147. n.style.maxWidth = img.style.width = npw + 'px';
  148. n.style.height = nph + 'px';
  149. img.style.marginLeft = Math.round((npw - nph / 9 * 16) / 2) + 'px';
  150. img.style.marginTop = ((nph - img.clientHeight) / 2).toFixed(1) + 'px';
  151. }
  152. });
  153. }
  154.  
  155. function parseWatchPage(response) {
  156. var div = response.context;
  157. var m = response.responseText.match(/ytplayer\.config\s*=\s*(\{.+?\});\s*ytplayer\.load/);
  158. var videoInfo = [];
  159. if (m) {
  160. var cfg = JSON.parse(m[1]), streams = {};
  161. cfg.args.url_encoded_fmt_stream_map.split(',').forEach(function(stream) {
  162. var params = {};
  163. stream.split('&').forEach(function(kv) { params[kv.split('=')[0]] = decodeURIComponent(kv.split('=')[1]) });
  164. streams[params.itag] = params;
  165. });
  166. cfg.args.fmt_list.split(',').forEach(function(fmt) {
  167. var itag = fmt.split('/')[0], dimensions = fmt.split('/')[1], stream = streams[itag];
  168. if (stream) {
  169. videoInfo.push({
  170. onerror: switchToIFrame.bind(div),
  171. src: stream.url,
  172. title: stream.quality + ', ' + dimensions + ', ' + stream.type
  173. });
  174. }
  175. });
  176. } else {
  177. var rx = /url=([^=]+?mime%3Dvideo%252F(?:mp4|webm)[^=]+?)(?:,quality|,itag|.u0026)/g;
  178. var text = response.responseText.split('url_encoded_fmt_stream_map')[1];
  179. while (m = rx.exec(text)) {
  180. videoInfo.push({
  181. onerror: switchToIFrame.bind(div),
  182. src: decodeURIComponent(decodeURIComponent(m[1]))
  183. });
  184. }
  185. }
  186.  
  187. var title = response.responseText.match(/<title>(.+?)(?:\s*-\s*YouTube)?<\/title>/);
  188. if (title) {
  189. var a = div.appendChild(document.createElement('a'));
  190. a.className = 'instant-youtube-persistent-title';
  191. a.innerHTML = '<strong>' + title[1] + '</strong> - watch on Youtube';
  192. a.href = div.querySelector('a.instant-youtube-link').href;
  193. ['animation', 'webkitAnimation', 'mozAnimation'].some(function(prop) {
  194. if (prop in a.style) {
  195. a.style[prop] = 'instant-youtube-fadein 1s ease-in';
  196. setTimeout(function() { a.style[prop] = '' }, 1000);
  197. return true;
  198. }
  199. });
  200. }
  201.  
  202. if (videoInfo.length)
  203. div.videoInfo = videoInfo;
  204. }
  205.  
  206. function clickHandler(e) {
  207. if (e.target.href)
  208. return;
  209. if (e.target.parentNode.className == 'instant-youtube-options')
  210. return options(e);
  211. for (var div = e.target; !div.srcEmbed; div = div.parentNode) {}
  212. div.removeEventListener('click', clickHandler);
  213. div.querySelector('svg').outerHTML = '<span class="instant-youtube-loading-button"></span>';
  214.  
  215. var alternateMode = e.target.className == 'instant-youtube-link' || e.shiftKey;
  216. if (!div.videoInfo || playHTML5 && alternateMode || !playHTML5 && !alternateMode) {
  217. switchToIFrame.call(div);
  218. return;
  219. }
  220.  
  221. div.querySelector('span.instant-youtube-link').style.display = 'none';
  222. div.querySelector('a.instant-youtube-link').style.display = 'none';
  223.  
  224. var video = document.createElement('video');
  225. video.controls = true;
  226. video.autoplay = true;
  227. video.width = div.clientWidth;
  228. video.height = div.clientHeight;
  229. video.className = 'instant-youtube-embed';
  230. video.volume = GM_getValue('volume', 0.5);
  231.  
  232. div.videoInfo.forEach(function(src) {
  233. var srcdom = video.appendChild(document.createElement('source'));
  234. Object.keys(src).forEach(function(k) { srcdom[k] = src[k] });
  235. });
  236.  
  237. if (window.chrome) {
  238. video.addEventListener('click', function(e) { if (this.paused) {this.play()} else {this.pause()} });
  239. }
  240. video.interval = (function() {
  241. return setInterval(function() {
  242. if (video.volume != GM_setValue('volume', 0.5))
  243. GM_setValue('volume', video.volume);
  244. }, 1000);
  245. })();
  246. var title = div.querySelector('.instant-youtube-persistent-title');
  247. if (title) {
  248. video.onpause = function() { title.removeAttribute('hidden') };
  249. video.onplay = function() { title.setAttribute('hidden', true) };
  250. }
  251. video.onloadeddata = function(e) {
  252. pauseOtherVideos(this);
  253. div.appendChild(this);
  254. this.style.opacity = 1;
  255. var img = div.querySelector('img');
  256. img.style.transition = 'opacity 1s';
  257. img.style.opacity = 0;
  258. };
  259. div.querySelector('.instant-youtube-options').style.display = 'none';
  260. }
  261.  
  262. function switchToIFrame(e) {
  263. var div = this;
  264. if (e) console.log('Direct linking failed on %s, switching to IFRAME player', div.srcEmbed);
  265. var iframeHTML = '<iframe class="instant-youtube-embed" allowtransparency="true" src="' + div.srcEmbed + (div.srcEmbed.indexOf('?') > 0 ? '' : '?') +
  266. '&autoplay=1' +
  267. '&autohide=2' +
  268. '&border=0' +
  269. '&controls=1' +
  270. '&fs=1' +
  271. '&showinfo=1' +
  272. '&ssl=1' +
  273. '&theme=dark' +
  274. '&enablejsapi=1' +
  275. '" frameborder="0" allowfullscreen="allowfullscreen" width="' + div.clientWidth + '" height="' + div.clientHeight + '"></iframe>';
  276. div.insertAdjacentHTML('beforeend', iframeHTML);
  277. div.lastElementChild.onload = function() {
  278. pauseOtherVideos(this);
  279. this.style.opacity = 1;
  280. this.ontransitionend = function() {
  281. this.closest('.instant-youtube-container').querySelector('img').style.display = 'none';
  282. };
  283. };
  284. if (e && e.target) {
  285. var video = e.target.parentNode;
  286. while (video.lastElementChild)
  287. video.lastElementChild.remove();
  288. }
  289. }
  290.  
  291. function pauseOtherVideos(activePlayer) {
  292. [].forEach.call(activePlayer.ownerDocument.getElementsByClassName('instant-youtube-embed'), function(v) {
  293. if (v == activePlayer)
  294. return;
  295. switch (v.localName) {
  296. case 'video':
  297. if (!v.paused)
  298. v.pause();
  299. break;
  300. case 'iframe':
  301. (function() { try { v.contentWindow.postMessage('{"event":"command", "func":"pauseVideo", "args":""}', '*') } catch(e) {} })();
  302. break;
  303. }
  304. });
  305. }
  306.  
  307. function options(e) {
  308. if (e.target.id.indexOf('instant-youtube-options-resize') == 0) {
  309. resizeToFit = e.target.checked;
  310. GM_setValue('resize', resizeToFit);
  311. [].forEach.call(document.querySelectorAll('div.instant-youtube-container'), function(n) {
  312. var w = n.originalWidth, h = n.originalHeight, img = n.querySelector('img');
  313. if (resizeToFit) {
  314. for (var np=n.parentNode, npw; np && !(npw=np.clientWidth); np=np.parentNode) {}
  315. w = npw;
  316. h = npw / 16 * 9;
  317. }
  318. n.style.maxWidth = w + 'px!important';
  319. n.style.height = h + 'px!important';
  320. img.style.width = (h / 9 * 16) + 'px!important';
  321. img.style.marginLeft = Math.round((w - h / 9 * 16) / 2) + 'px!important';
  322. img.style.marginTop = ((h - img.clientHeight) / 2).toFixed(1) + 'px!important';
  323. n.querySelector('input').checked = resizeToFit;
  324. var video = n.querySelector('video');
  325. if (video) {
  326. video.width = w;
  327. video.height = h;
  328. }
  329. });
  330. } else if (e.target.id.indexOf('instant-youtube-options-html5') == 0) {
  331. playHTML5 = e.target.checked;
  332. GM_setValue('playHTML5', playHTML5);
  333. [].forEach.call(document.querySelectorAll('div.instant-youtube-container'), function(div) {
  334. div.querySelector('span.instant-youtube-link').textContent = playHTML5 ? 'Play with Youtube player' : 'Play directly (up to 720p)';
  335. div.querySelector('[id^="instant-youtube-options-html5"]').checked = playHTML5;
  336. });
  337. }
  338. }