Lazy Embedded Video

Lazy load embedded videos from Youtube/Dailymotion/Vimeo/Rutube/Twitch/Coub/Rumble

  1. // ==UserScript==
  2. // @name Lazy Embedded Video
  3. // @namespace zeusex81@gmail.com
  4. // @description Lazy load embedded videos from Youtube/Dailymotion/Vimeo/Rutube/Twitch/Coub/Rumble
  5. // @version 4.3
  6. // @include *
  7. // @icon https://i.imgur.com/rf0mFDM.png
  8. // @license MIT
  9. // @grant GM.getValue
  10. // @grant GM.setValue
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @run-at document-start
  14. // ==/UserScript==
  15.  
  16. (async function() {
  17. var a = document.createElement("A");
  18. try { a.href = top.location.href; }
  19. catch(e) { a.href = document.referrer || location.href; }
  20. if(a.hostname && /(youtube|dailymotion|vimeo|rutube|twitch|coub|rumble)\.[^.]+$/.test(a.hostname))
  21. return;
  22. var getValue = typeof(GM) != "undefined" ? GM.getValue : typeof(GM_getValue) != "undefined" ? GM_getValue :
  23. function(name, value) { return localStorage.getItem(name) || value; };
  24. var setValue = typeof(GM) != "undefined" ? GM.setValue : typeof(GM_setValue) != "undefined" ? GM_setValue :
  25. function(name, value) { localStorage.setItem(name, value); };
  26.  
  27. var settings = JSON.parse(await getValue("zeusLEV", '[false,true,[]]'));
  28. while(typeof(settings) == "string") settings = JSON.parse(settings);
  29. if(settings[2].includes(location.hostname))
  30. return;
  31.  
  32. var listener = false;
  33. var createListener = function() {
  34. if(listener) return;
  35. listener = true;
  36. window.addEventListener("message", async function(e) {
  37. if(!e.data.startsWith("lazyVideo")) return;
  38. var data = e.data.split(' ');
  39. switch(data[1]) {
  40. case "CSP" : iframes[parseInt(data[2])].dataset.lazyVideo = 3; break;
  41. case "play" : iframes[parseInt(data[2])].removeAttribute("srcdoc");
  42. iframes[parseInt(data[2])].src = data[3]; break;
  43. case "settings" :
  44. settings = JSON.parse(await getValue("zeusLEV", '[false,true,[]]'));
  45. while(typeof(settings) == "string") settings = JSON.parse(settings);
  46. switch(data[2]) {
  47. case "autoplay" : settings[0] = data[3] == 'true'; break;
  48. case "flash" : settings[1] = data[3] == 'true'; break;
  49. case "reset" : settings[2] = []; break;
  50. case "whitelist" :
  51. observer.disconnect();
  52. cancelAnimationFrame(animation);
  53. if(!settings[2].includes(location.hostname))
  54. settings[2].push(location.hostname);
  55. for(var i = 0; i < iframes.length; i++)
  56. iframes[i].removeAttribute("srcdoc");
  57. break;
  58. }
  59. setValue("zeusLEV", JSON.stringify(settings));
  60. break;
  61. }
  62. });
  63. }
  64.  
  65. var html, iframes = [];
  66. var createHtml = function(url, src, api, background_img) {
  67. /(.*\/\/(?:[^.\/]+\.)?([^.\/]+)\.[^.\/]+)\//i.test(url);
  68. var provider_url = RegExp.$1, provider_name = RegExp.$2, data_convert = "", button_color = "";
  69. if(api) api += '&callback=jsonpCallback';
  70. switch(provider_name) {
  71. case "youtube" :
  72. button_color = "#c22";
  73. data_convert += 'data = {'+
  74. 'thumbnail_url: (data.items[0].snippet.thumbnails.maxres || data.items[0].snippet.thumbnails.standard || '+
  75. 'data.items[0].snippet.thumbnails.high || data.items[0].snippet.thumbnails.medium || '+
  76. 'data.items[0].snippet.thumbnails.default).url,'+
  77. 'title: data.items[0].snippet.title,'+
  78. 'author_url: "https://www.youtube.com/channel/"+data.items[0].snippet.channelId,'+
  79. 'author_name: data.items[0].snippet.channelTitle,'+
  80. 'duration: data.items[0].contentDetails ? /PT(\\d+H)?(\\d+M)?(\\d+S)?/i.test(data.items[0].contentDetails.duration) && '+
  81. '(parseInt(RegExp.$1||0)*3600+parseInt(RegExp.$2||0)*60+parseInt(RegExp.$3||0)) || "LIVE" : "PLAYLIST"'+
  82. '};';
  83. break;
  84. case "dailymotion" :
  85. button_color = "#fd5";
  86. data_convert += 'data.author_url = data["owner.url"];'+
  87. 'data.author_name = data["owner.screenname"];'+
  88. 'if(!data.duration) data.duration = "LIVE";';
  89. break;
  90. case "vimeo" :
  91. button_color = "#5af";
  92. data_convert += 'if(data.thumbnail_url) data.thumbnail_url = data.thumbnail_url.replace(/_\\d+x\\d+/i, "");';
  93. break;
  94. case "rutube" :
  95. button_color = "#444";
  96. data_convert += 'if(data.thumbnail_url) data.thumbnail_url = data.thumbnail_url.replace(/\\?.+/i, "");';
  97. break;
  98. case "twitch" :
  99. button_color = "#548";
  100. break;
  101. case "coub" :
  102. button_color = "#04f";
  103. data_convert += 'if(data.channel_url) data.author_url = data.channel_url;';
  104. break;
  105. case "rumble" :
  106. button_color = "#8c4";
  107. break;
  108. }
  109. if(!html) html = [
  110. '<!doctype html>'+
  111. '<html>'+
  112. '<head>'+
  113. '<title>Lazy Embedded Video</title>'+
  114. '<style>'+
  115. 'html { height:100%; } '+
  116. 'body { margin:0; height:100%; color:white; font:14px sans-serif; '+
  117. '--thumbnail_url:', background_img, '; background:black var(--thumbnail_url) center/100% no-repeat; } '+
  118. 'a { color:inherit; font-weight:bold; text-decoration:none; } '+
  119. 'a:hover { text-decoration:underline; } '+
  120. '#interface { position:absolute; width:100%; height:100%; overflow:hidden; opacity:0.9; '+
  121. '-moz-user-select:none; -webkit-user-select:none; -ms-user-select:none; user-select:none; } '+
  122. '#playButton { display:flex; height:100%; cursor:pointer; } '+
  123. '#playButton > div { width:70px; height:70px; margin:auto; border-radius:50%; background-color:black; } '+
  124. '#playButton:hover > div { background-color:', button_color, '; } '+
  125. '#playButton > div > div { width:0; height:0; margin:20px 0 0 25px; border:solid transparent; '+
  126. 'border-width:14px 0px 14px 28px; border-left-color:white; } '+
  127. '#infobar { position:absolute; top:0px; width:100%; height:32px; display:flex; '+
  128. 'box-sizing:border-box; background:black; border:0px solid grey; border-bottom-width:1px; } '+
  129. '#author, #title, #duration { overflow:hidden; white-space:nowrap; margin:auto 8px; } '+
  130. '#author { flex-shrink:0; max-width:30%; color:', button_color ,'; } '+
  131. '#space { flex-grow:1; } '+
  132. '#duration { flex-shrink:0; } '+
  133. '#settingsButton { flex-basis:32px; flex-shrink:0; font:bold 20px sans-serif; text-align:center; cursor:pointer; } '+
  134. '#settingsButton:hover { color:', button_color, '; } '+
  135. '#settingsButton.active { background-color:', button_color, '; color:black; } '+
  136. '#settingsPanel { position:absolute; right:0px; top:31px; max-height:100%; margin:0px; list-style:none; padding:8px; '+
  137. 'border:solid grey; border-width:0px 0px 1px 1px; background-color:', button_color ,'; color:black; cursor:default; visibility:hidden; } '+
  138. '#settingsPanel.active { visibility:visible; } '+
  139. '#settingsPanel label { display:inline-block; width:144px; vertical-align:top; } '+
  140. '#settingsPanel button { width:100%; }'+
  141. '</style>'+
  142. '</head>'+
  143. '<body>'+
  144. '<div id=interface>'+
  145. '<div id=playButton><div><div></div></div></div>'+
  146. '<div id=infobar>'+
  147. '<a id=author target=_blank onmouseenter="this.title = this.scrollWidth > this.clientWidth ? this.textContent : \'\';" href="', provider_url, '">', provider_name, '</a>'+
  148. '<a id=title target=_blank onmouseenter="this.title = this.scrollWidth > this.clientWidth ? this.textContent : \'\';" href="', url, '">', url, '</a>'+
  149. '<div id=space></div>'+
  150. '<span id=settingsButton>⚙</span>'+
  151. '</div>'+
  152. '<ul id=settingsPanel>'+
  153. '<li><label>Allow autoplay:</label><input class=setting type=checkbox', '', '></li>'+
  154. '<li><label>Legacy Flash support:</label><input class=setting type=checkbox', '', '></li>'+
  155. '<li><button class=setting>Whitelist this site</button>'+
  156. '<li><button class=setting>Clear whitelist</button>'+
  157. '</ul>'+
  158. '</div>'+
  159. '<script>'+
  160. 'parent.postMessage("lazyVideo CSP ', iframes.length, '", "'+location.href+'");'+
  161. 'document.getElementById("playButton").onclick = function() {'+
  162. 'parent.postMessage("lazyVideo play ', iframes.length, ' ', src, '", "'+location.href+'");'+
  163. '};'+
  164. 'var settingsButton = document.getElementById("settingsButton");'+
  165. 'var settingsPanel = document.getElementById("settingsPanel");'+
  166. 'settingsButton.onclick = function() {'+
  167. 'settingsButton.classList.toggle("active");'+
  168. 'settingsPanel.classList.toggle("active");'+
  169. '};'+
  170. '[].slice.call(settingsPanel.getElementsByClassName("setting")).forEach(function(e, i) {'+
  171. 'switch(i) {'+
  172. 'case 0: e.onchange = function() { parent.postMessage("lazyVideo settings autoplay "+e.checked, "'+location.href+'"); }; break;'+
  173. 'case 1: e.onchange = function() { parent.postMessage("lazyVideo settings flash "+e.checked , "'+location.href+'"); }; break;'+
  174. 'case 2: e.onclick = function() { parent.postMessage("lazyVideo settings whitelist" , "'+location.href+'"); }; break;'+
  175. 'case 3: e.onclick = function() { parent.postMessage("lazyVideo settings reset" , "'+location.href+'"); }; break;'+
  176. '}'+
  177. '});'+
  178. 'function removeProtocol(url) { return url.replace(/^[a-z]+:/i, ""); }'+
  179. 'function jsonpCallback(data) {',
  180. data_convert,
  181. 'if(data.thumbnail_url) document.body.style.setProperty("--thumbnail_url", "url("+data.thumbnail_url+")");'+
  182. 'if(data.thumbnail_url) document.body.style.setProperty("--darkreader-bgimg--thumbnail_url", "url("+data.thumbnail_url+")");'+
  183. 'if(data.url) document.getElementById("title").href = removeProtocol(data.url);'+
  184. 'if(data.title) document.getElementById("title").textContent = data.title;'+
  185. 'if(data.author_url) document.getElementById("author").href = removeProtocol(data.author_url);'+
  186. 'if(data.author_name) document.getElementById("author").textContent = data.author_name;'+
  187. 'if(data.duration) document.getElementById("space").insertAdjacentHTML("afterend",'+
  188. '"<div id=duration>"+(Number(data.duration) ? new Date(data.duration*1000).toISOString().substr(11,8) : data.duration)+"</div>");'+
  189. '}'+
  190. '</script>'+
  191. '<script id=api src="', api, '"></script>'+
  192. '</body>'+
  193. '</html>'
  194. ];
  195. html[ 1] = background_img || 'none';
  196. html[ 3] = button_color;
  197. html[ 5] = button_color;
  198. html[ 7] = button_color;
  199. html[ 9] = button_color;
  200. html[11] = button_color;
  201. html[13] = provider_url;
  202. html[15] = provider_name;
  203. html[17] = url;
  204. html[19] = url;
  205. html[21] = settings[0] ? ' checked' : '';
  206. html[23] = settings[1] ? ' checked' : '';
  207. html[25] = iframes.length;
  208. html[27] = iframes.length;
  209. html[29] = src;
  210. html[31] = data_convert;
  211. html[33] = api;
  212. };
  213.  
  214. var createOembed = function(api, url) { return api+encodeURIComponent(url); };
  215. // var createNOembed = function(api, url) { return createOembed("https://noembed.com/embed?url=", url); };
  216. var createJOembed = function(api, url) { return createOembed("https://json2jsonp.com/?url=", createOembed(api, url)); };
  217.  
  218. var createLazyVideo = function(elem) {
  219. var id, args = "", url, src = a.href = elem.src || elem.data || elem.dataset.src;
  220. if(!a.hostname || elem.dataset.lazyVideo) return;
  221. elem.dataset.lazyVideo = 1;
  222. switch(a.hostname.match(/([^.]+)\.[^.]+$/)[1]) {
  223. case "youtube" :
  224. case "youtube-nocookie" :
  225. if(/\/(?:p\/|embed\/videoseries)([^&]*)/i.test(a.pathname)) {
  226. id = RegExp.$1 || (/[?&]list=([^&]+)/i.test(a.search) && RegExp.$1);
  227. if(!id || (settings[0] && a.search.includes("autoplay=1"))) return;
  228. if(/[?&](v=[^&]+)/i.test(a.search)) args += "&"+RegExp.$1;
  229. if(/[?&](index=[^&]+)/i.test(a.search)) args += "&"+RegExp.$1;
  230. if(/[?&](start=[^&]+)/i.test(a.search)) args += "&"+RegExp.$1;
  231. createHtml(
  232. url = /[?&]v=([^&]+)/i.test(a.search) ? "https://www.youtube.com/watch?list="+id+args : "https://www.youtube.com/playlist?list="+id,
  233. src = "https://www.youtube-nocookie.com/embed/videoseries?autoplay=1&list="+id+args,
  234. "https://www.googleapis.com/youtube/v3/playlists?part=snippet&fields=items/snippet(channelId,title,thumbnails,channelTitle)&key=AIzaSyBJ-o6n51GQ6jEqjvEN0bI1KdX5CHZQy5E&id="+id,
  235. /[?&]v=([^&]+)/i.test(a.search) ? "url(https://i.ytimg.com/vi/"+RegExp.$1+"/hqdefault.jpg)" : null
  236. );
  237. } else {
  238. if(/\/(?:v|embed)\/([^&]*)/i.test(a.pathname)) id = RegExp.$1 || (/[?&]v=([^&]+)/i.test(a.search) && RegExp.$1);
  239. if(!id || (settings[0] && a.search.includes("autoplay=1"))) return;
  240. if(/[?&](start=[^&]+)/i.test(a.search)) args += "&"+RegExp.$1;
  241. createHtml(
  242. url = "https://www.youtube.com/watch?v="+id+args,
  243. src = "https://www.youtube-nocookie.com/embed/"+id+"?autoplay=1"+args,
  244. // createNOembed("https://www.youtube.com/oembed?format=json&url=", url),
  245. "https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&fields=items(snippet(channelId,title,thumbnails,channelTitle),contentDetails/duration)&key=AIzaSyBJ-o6n51GQ6jEqjvEN0bI1KdX5CHZQy5E&id="+id,
  246. "url(https://i.ytimg.com/vi/"+id+"/hqdefault.jpg)"
  247. );
  248. }
  249. break;
  250. case "dailymotion" :
  251. if(/\/(?:swf|embed)\/(?:video\/)?([^&_]+)/i.test(a.pathname)) id = RegExp.$1;
  252. if(!id || (settings[0] && a.search.includes("autoplay=1"))) return;
  253. // if(/[?&](mute=[^&]+)/i.test(a.search)) args += "&"+RegExp.$1;
  254. if(/[?&](start=[^&]+)/i.test(a.search)) args += "&"+RegExp.$1;
  255. createHtml(
  256. url = "https://www.dailymotion.com/video/"+id+"?"+args,
  257. src = "https://www.dailymotion.com/embed/video/"+id+"?autoplay=1"+args,
  258. // createOembed("https://www.dailymotion.com/services/oembed?format=json&url=", url),
  259. "https://api.dailymotion.com/video/"+id+"?fields=owner.screenname,owner.url,title,url,duration,thumbnail_url",
  260. "url(https://www.dailymotion.com/thumbnail/video/"+id+")"
  261. );
  262. break;
  263. case "vimeo" :
  264. if(/\/(?:moogaloop\.swf|video\/)([^&]*)/i.test(a.pathname))
  265. id = RegExp.$1 || (/[?&]clip_id=([^&]+)/i.test(a.search) && RegExp.$1);
  266. if(!id || (settings[0] && a.search.includes("autoplay=1"))) return;
  267. if(/[?&](loop=[^&]+)/i.test(a.search)) args += "&"+RegExp.$1;
  268. if(/(\#t=[\dhms]+)/i.test(a.hash)) args += RegExp.$1;
  269. createHtml(
  270. url = "https://vimeo.com/"+id+"?"+args,
  271. src = "https://player.vimeo.com/video/"+id+"?autoplay=1"+args,
  272. createOembed("https://vimeo.com/api/oembed.json?url=", url)
  273. );
  274. break;
  275. case "rutube" :
  276. if(/(?:\/play)?\/embed\/([^&.\/]+)/i.test(a.pathname)) id = RegExp.$1;
  277. else if(/[?&]pl_video=([^&]+)/i.test(a.search)) id = RegExp.$1;
  278. if(!id || (settings[0] && a.search.includes("autoStart=1"))) return;
  279. if(/[?&](bmstart=[^&]+)/i.test(a.search)) args += "&"+RegExp.$1;
  280. createHtml(
  281. url = "https://rutube.ru/"+(isNaN(id) ? "video/"+id+"/" : "tracks/"+id+".html"),
  282. src = "https://rutube.ru/"+(a.pathname.includes("/embed/") ? "play/embed/"+id+"?autoStart=1"+args : "pl/?pl_video="+id),
  283. createJOembed("https://rutube.ru/api/oembed/?format=json&url=", url)
  284. );
  285. break;
  286. case "twitch" :
  287. if(/(channel|video|clip)=([^&]+)/i.test(a.search)) {args = RegExp.$1; id = RegExp.$2;}
  288. else if(/(stream=.+&channelId)=([^&]+)/i.test(a.search)) {args = RegExp.$1; id = RegExp.$2;}
  289. else if(/\/(.+)\/embed/i.test(a.pathname)) {args = "channel"; id = RegExp.$1;}
  290. if(!id || (settings[0] && a.search.includes("autoplay=true"))) return;
  291. createHtml(
  292. url = "https://"+(args=="clip" ? "clips" : "www")+".twitch.tv/"+(args=="video" ? "videos/" : "")+id,
  293. src = a.href.split('?')[0]+"?autoplay=true&"+args+"="+id+"&parent="+location.hostname,
  294. null,
  295. "url(https://static-cdn.jtvnw.net/previews-ttv/live_user_"+(args=="channel" ? id : 0)+"-0x0.jpg)"
  296. );
  297. break;
  298. case "coub" :
  299. if(/\/embed\/([^&]+)/i.test(a.pathname)) id = RegExp.$1;
  300. if(!id || (settings[0] && a.search.includes("autostart=true"))) return;
  301. createHtml(
  302. url = "https://coub.com/view/"+id,
  303. src = "https://coub.com/embed/"+id+"?startWithHD=true&autostart=true",
  304. createJOembed("https://coub.com/api/oembed.json?url=", url)
  305. );
  306. break;
  307. case "rumble" :
  308. if(/\/embed\/([^\/]+)/i.test(a.pathname)) id = RegExp.$1;
  309. if(!id || (settings[0] && a.search.includes("autoplay=2"))) return;
  310. if(/[?&](start=[^&]+)/i.test(a.search)) args += "&"+RegExp.$1;
  311. createHtml(
  312. url = "https://rumble.com/"+id+"-.html",
  313. src = "https://rumble.com/embed/"+id+"/?autoplay=2"+args,
  314. createJOembed("https://rumble.com/api/Media/oembed.json?url=", src)
  315. );
  316. break;
  317. default :
  318. return;
  319. }
  320. if(elem.tagName != "IFRAME") {
  321. if(elem.parentNode.tagName == "OBJECT")
  322. elem = elem.parentNode;
  323. var iframe = document.createElement("IFRAME");
  324. iframe.src = src;
  325. iframe.id = elem.id;
  326. iframe.name = elem.name;
  327. iframe.className = elem.className;
  328. iframe.style.cssText = elem.style.cssText;
  329. iframe.width = elem.width;
  330. iframe.height = elem.height;
  331. iframe.frameBorder = elem.border;
  332. iframe.align = elem.align;
  333. elem.parentNode.replaceChild(iframe, elem);
  334. elem = iframe;
  335. }
  336. createListener();
  337. elem.dataset.lazyVideo = 2;
  338. elem.allowFullscreen = true;
  339. elem.srcdoc = html.join("");
  340. iframes.push(elem);
  341. setTimeout(function() {
  342. if(elem.dataset.lazyVideo != 3)
  343. elem.removeAttribute("srcdoc");
  344. }, 15000);
  345. };
  346.  
  347. var observer, animation;
  348. var update = function() {
  349. if(!document.body) {
  350. animation = requestAnimationFrame(update);
  351. } else if(!observer) {
  352. observer = new MutationObserver(function() { if(!animation) animation = requestAnimationFrame(update); });
  353. observer.observe(document.body, {childList: true, attributes: false, characterData: false, subtree: true});
  354. animation = requestAnimationFrame(update);
  355. } else {
  356. var nodes = document.querySelectorAll(settings[1] ? "IFRAME, EMBED, OBJECT" : "IFRAME");
  357. for(var i = 0; i < nodes.length; i++)
  358. createLazyVideo(nodes[i]);
  359. animation = null;
  360. }
  361. };
  362. update();
  363. })();