On-demand 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.

当前为 2015-05-13 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name On-demand 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.
  4. // @version 1.1.0
  5. // @author wOxxOm
  6. // @namespace wOxxOm.scripts
  7. // @license MIT License
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @grant GM_addStyle
  11. // @grant GM_xmlhttpRequest
  12. // @run-at document-start
  13. // ==/UserScript==
  14.  
  15. var resizeToFit = GM_getValue('resize', true);
  16. var embedSelector = 'iframe[src*="youtube.com/embed"], embed[src*="youtube.com/v"]';
  17.  
  18. processNodes(null, document.querySelectorAll(embedSelector));
  19. document.addEventListener('DOMContentLoaded', function() {
  20. processNodes(null, document.querySelectorAll(embedSelector));
  21. setMutationHandler(document, embedSelector, processNodes);
  22. GM_addStyle('\
  23. .instant-youtube-container:not(small) {position:relative; overflow:hidden; cursor:pointer; background-color:black; padding:0; margin:0}\
  24. .instant-youtube-container:not(small) .instant-youtube-thumbnail {transition:opacity 0.1s ease-out; opacity:0; padding:0; margin:0}\
  25. .instant-youtube-container:not(small) .instant-youtube-play-button {position:absolute; left:0; right:0; top:0; bottom:0; margin:auto; width:85px; height:60px}\
  26. .instant-youtube-container:not(small) .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==")}\
  27. .instant-youtube-container:hover:not(small) .ytp-large-play-button-svg {fill:#CC181E}\
  28. .instant-youtube-container:not(small) .instant-youtube-link {\
  29. position:absolute; top:50%; left:0; right:0; width:20em; height:1.7em; margin:60px auto; padding:0;\
  30. display:block; text-align:center; text-decoration:none; color:white; text-shadow:1px 1px 3px black;\
  31. font:14px/1.0 bold Arial,Helvetica,Verdana,Sans-serif;\
  32. }\
  33. .instant-youtube-container:not(small) .instant-youtube-link:hover { color:white; text-decoration:underline; background:transparent}\
  34. .instant-youtube-container:not(small) .instant-youtube-embed {position:absolute; left:0; top:0; padding:0; margin:0}\
  35. .instant-youtube-container:not(small) .instant-youtube-link2 {\
  36. display:none; z-index:9; background-color:rgba(0,0,0,0.5); color: white;\
  37. width:100%; height: 1.7em; top:0; left:0; right:0; position:absolute; z-index: 9;\
  38. color:white; text-shadow:1px 1px 2px black; text-align:center; text-decoration:none;\
  39. margin:0; padding:0.5em 0.5em 0.2em; font:14px/1.0 normal Arial,Helvetica,Verdana,Sans-serif;\
  40. }\
  41. .instant-youtube-container:not(small):hover .instant-youtube-link2 {display:block; margin:0}\
  42. .instant-youtube-container:not(small) .instant-youtube-link2:hover {text-decoration:underline}\
  43. .instant-youtube-container:not(small) .instant-youtube-options {position:absolute; right:0; bottom:0; color:white; text-shadow:1px 1px 2px black; padding:0; margin:0 }\
  44. .instant-youtube-container:not(small) .instant-youtube-options * {font-size:13px; vertical-align:middle; padding:0; margin:0}\
  45. ');
  46. });
  47.  
  48. function processNodes(observer, nodes) {
  49. for (var i=0, nl=nodes.length, n; i<nl && (n=nodes[i]); i++) {
  50. if (!n.parentNode || n.className.indexOf('instant-youtube-') >= 0 || n.src.indexOf('autoplay=1') > 0)
  51. continue;
  52.  
  53. var id = n.src.match(/(?:embed\/|v[=\/])(.+?)(?:[&?\/].*|$)/);
  54. if (!id)
  55. continue;
  56. id = id[1];
  57.  
  58. for (var np=n.parentNode, npw; np && !(npw=np.clientWidth); np=np.parentNode) {}
  59. var containerWidth = resizeToFit ? npw : n.clientWidth;
  60. var containerHeight = resizeToFit ? npw / 16 * 9 : n.clientHeight;
  61.  
  62. var div = document.createElement('div');
  63. div.className = 'instant-youtube-container';
  64. div.srcEmbed = n.src;
  65. div.style.maxWidth = containerWidth + 'px';
  66. div.style.height = containerHeight + 'px';
  67. div.originalWidth = n.width;
  68. div.originalHeight = n.height;
  69.  
  70. var img = div.appendChild(document.createElement('img'));
  71. img.className = 'instant-youtube-thumbnail';
  72. img.src = 'https://i.ytimg.com/vi' + (window.chrome?'_webp':'') + '/' + id + '/maxresdefault.' + (window.chrome?'webp':'jpg');
  73. if (n.clientHeight) {
  74. img.style.maxWidth = 'auto';
  75. img.style.width = (containerHeight / 9 * 16) + 'px';
  76. img.style.marginLeft = Math.round((containerWidth - containerHeight / 9 * 16) / 2) + 'px';
  77. }
  78. img.title = 'Shift-click to play directly as HTML5 video';
  79. img.onload = function(e) {
  80. var img = e.target;
  81. if (img.naturalWidth <= 120)
  82. img.onerror(e);
  83. else {
  84. if (img.src.indexOf('maxresdefault') < 0)
  85. img.style.marginTop = ((img.parentNode.clientHeight - img.clientHeight) / 2).toFixed(1) + 'px';
  86. img.style.setProperty('opacity', '1');
  87. }
  88. }
  89. img.onerror = function(e) {
  90. var img = e.target;
  91. if (img.src.indexOf('maxresdefault') > 0)
  92. img.src = img.src.replace('maxresdefault','hqdefault');
  93. else if (img.src.indexOf('hqdefault.webp') > 0)
  94. img.src = img.src.replace('_webp','').replace('.webp','.jpg');
  95. };
  96.  
  97. div.insertAdjacentHTML('beforeend', '\
  98. <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>\
  99. <a class="instant-youtube-link" href="https://www.youtube.com/watch?v=' + id + '" target="_blank"><b>Watch on Youtube</b></a>\
  100. <span class="instant-youtube-link" style="margin-top:6em">Watch as HTML5</span>\
  101. <div class="instant-youtube-options">\
  102. <label for="instant-youtube-options-resize">Resize to fit</label>\
  103. <input type="checkbox" id="instant-youtube-options-resize"' + (resizeToFit ? ' checked' : '') + '>\
  104. </div>\
  105. ');
  106.  
  107. n.parentNode.insertBefore(div, n);
  108. n.remove();
  109.  
  110. div.addEventListener('click', function clickHandler(e) {
  111. if (e.target.href)
  112. return;
  113. if (e.target.parentNode.className == 'instant-youtube-options') {
  114. if (e.target.id == 'instant-youtube-options-resize') {
  115. resizeToFit = e.target.checked;
  116. GM_setValue('resize', resizeToFit);
  117. [].forEach.call(document.querySelectorAll('div.instant-youtube-container'), function(n) {
  118. var w = n.originalWidth, h = n.originalHeight, img = n.querySelector('img');
  119. if (resizeToFit) {
  120. for (var np=n.parentNode, npw; np && !(npw=np.clientWidth); np=np.parentNode) {}
  121. w = npw;
  122. h = npw / 16 * 9;
  123. }
  124. n.style.maxWidth = w + 'px';
  125. n.style.height = h + 'px';
  126. img.style.width = (h / 9 * 16) + 'px';
  127. img.style.marginLeft = Math.round((w - h / 9 * 16) / 2) + 'px';
  128. img.style.marginTop = ((h - img.clientHeight) / 2).toFixed(1) + 'px';
  129. n.querySelector('input').checked = resizeToFit;
  130. var video = n.querySelector('video');
  131. if (video) {
  132. video.width = w;
  133. video.height = h;
  134. }
  135. });
  136. }
  137. return;
  138. }
  139. for (var div = e.target; !div.srcEmbed; div = div.parentNode) {}
  140. var iframeHTML = '<iframe class="instant-youtube-embed" src="' + div.srcEmbed + (div.srcEmbed.indexOf('?') > 0 ? '' : '?') +
  141. '&autoplay=1' +
  142. '&autohide=2' +
  143. '&border=0' +
  144. '&controls=1' +
  145. '&fs=1' +
  146. '&showinfo=1' +
  147. '&ssl=1' +
  148. '&theme=dark' +
  149. '" frameborder="0" allowfullscreen="allowfullscreen" width="' + div.clientWidth + '" height="' + div.clientHeight + '"></iframe>';
  150. div.removeEventListener('click', clickHandler);
  151. div.querySelector('svg').outerHTML = '<span class="instant-youtube-loading-button"></span>';
  152. if (!e.shiftKey && e.target.className != 'instant-youtube-link')
  153. div.insertAdjacentHTML('beforeend', iframeHTML);
  154. else {
  155. div.querySelector('span.instant-youtube-link').style.display = 'none';
  156. GM_xmlhttpRequest({
  157. method: 'GET',
  158. url: div.srcEmbed.replace(/\/(embed\/|v[=\/])/, '/watch?v='),
  159. onload: function(response) {
  160. var video;
  161. video = div.appendChild(document.createElement('video'));
  162. video.controls = true;
  163. video.autoplay = true;
  164. video.width = div.clientWidth;
  165. video.height = div.clientHeight;
  166. video.className = 'instant-youtube-embed';
  167. video.volume = GM_getValue('volume', 0.5);
  168. var m;
  169. if (m = response.responseText.match(/ytplayer\.config\s*=\s*(\{.+?\});/)) {
  170. var cfg = JSON.parse(m[1]), streams = {};
  171. cfg.args.url_encoded_fmt_stream_map.split(',').forEach(function(stream){
  172. var params = {}
  173. stream.split('&').forEach(function(kv){ params[kv.split('=')[0]] = decodeURIComponent(kv.split('=')[1]) });
  174. streams[params.itag] = params;
  175. });
  176. cfg.args.fmt_list.split(',').forEach(function(fmt){
  177. var itag = fmt.split('/')[0], dimensions = fmt.split('/')[1], stream = streams[itag];
  178. if (stream) {
  179. var source = video.appendChild(document.createElement('source'));
  180. source.src = stream.url;
  181. source.title = stream.quality + ', ' + dimensions + ', ' + stream.type;
  182. }
  183. });
  184. } else {
  185. var rx = /url=([^=]+?mime%3Dvideo%252F(?:mp4|webm)[^=]+?)(?:,quality|,itag|.u0026)/g;
  186. var text = response.responseText.split('url_encoded_fmt_stream_map')[1];
  187. while (m = rx.exec(text)) {
  188. video.appendChild(document.createElement('source')).src = decodeURIComponent(decodeURIComponent(m[1]));
  189. }
  190. }
  191. if (video.children.length) {
  192. if (window.chrome) {
  193. video.addEventListener('click', function(e) { if (e.target.paused) {e.target.play()} else {e.target.pause()} });
  194. }
  195. video.interval = (function() {
  196. return setInterval(function() {
  197. if (video.paused)
  198. clearInterval(video.interval);
  199. else
  200. GM_setValue('volume', video.volume);
  201. }, 1000);
  202. })();
  203. var title = response.responseText.match(/<title>(.+?)(?:\s*-\s*YouTube)?<\/title>/);
  204. if (title) {
  205. var a = div.querySelector('.instant-youtube-link');
  206. a.innerHTML = '<b>' + title[1] + '</b> - watch on Youtube';
  207. a.className = 'instant-youtube-link2';
  208. }
  209. div.querySelector('img').style.display = 'none';
  210. } else {
  211. video.remove();
  212. div.insertAdjacentHTML('beforeend', iframeHTML);
  213. }
  214. }
  215. });
  216. }
  217. });
  218. }
  219. return true;
  220. }
  221.  
  222. function setMutationHandler(baseNode, selector, cb) {
  223. var ob = new MutationObserver(function(mutations){
  224. for (var i=0, ml=mutations.length, m; (i<ml) && (m=mutations[i]); i++)
  225. for (var j=0, nodes=m.addedNodes, nl=nodes.length, n; (j<nl) && (n=nodes[j]); j++)
  226. if (n.nodeType == 1)
  227. if ((n = n.matches(selector) ? [n] : n.querySelectorAll(selector)) && n.length)
  228. if (!cb(ob, n))
  229. return;
  230. });
  231. ob.observe(baseNode, {subtree:true, childList:true});
  232. }