YouTube Live CPU Tamer

It reduces the high CPU usage on Super Chats with nothing to lose.

目前為 2020-05-28 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name YouTube Live CPU Tamer
  3. // @name:ja YouTube Live CPU Tamer
  4. // @name:zh-CN YouTube Live CPU Tamer
  5. // @description It reduces the high CPU usage on Super Chats with nothing to lose.
  6. // @description:ja スーパーチャットによる高いCPU使用率を削減します。見た目は何も変わりません。
  7. // @description:zh-CN 降低超级聊天的高CPU利用率。外观完全没有变化。
  8. // @namespace knoa.jp
  9. // @include https://www.youtube.com/live_chat*
  10. // @include https://www.youtube.com/live_chat_replay*
  11. // @version 2.0.5
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. (function(){
  16. const SCRIPTID = 'YouTubeLiveCpuTamer';
  17. const SCRIPTNAME = 'YouTube Live CPU Tamer';
  18. const DEBUG = false;/*
  19. [update] 2.0.5
  20. Adjusted border of button for new UI design.
  21.  
  22. [bug]
  23.  
  24. [todo]
  25.  
  26. [possible]
  27.  
  28. [research]
  29. Proxyを使うとbackgroundトリック不要?CPU使用に対する効果はある?
  30. 放送開始前の待機画面でもHelper(GPU)が食ってる件
  31. リアルタイム視聴時のほうがHelper(GPU)が消費する件は新着確認js処理のせいか
  32.  
  33. [memo]
  34. */
  35. if(console.time) console.time(SCRIPTID);
  36. const MS = 1, SECOND = 1000*MS, MINUTE = 60*SECOND, HOUR = 60*MINUTE, DAY = 24*HOUR, WEEK = 7*DAY, MONTH = 30*DAY, YEAR = 365*DAY;
  37. const THROTTLE = 1000*MS;
  38. const site = {
  39. targets: {
  40. itemsNode: () => $('yt-live-chat-ticker-renderer #items'),
  41. },
  42. get: {
  43. tickerItemInsideContainers: (items) => items.querySelectorAll('yt-live-chat-ticker-paid-message-item-renderer #container'),/* existing items */
  44. tickerItemInsideContainer: (node) => node.querySelector('yt-live-chat-ticker-paid-message-item-renderer #container'),/* for observer */
  45. },
  46. };
  47. let elements = {};
  48. const core = {
  49. initialize: function(){
  50. elements.html = document.documentElement;
  51. elements.html.classList.add(SCRIPTID);
  52. text.setup(texts, top.document.documentElement.lang);
  53. core.ready();
  54. core.addStyle('style');
  55. },
  56. ready: function(){
  57. core.getTargets(site.targets).then(() => {
  58. log("I'm ready.");
  59. core.observeTickerItems();
  60. core.prepareRemoveTickersButton();
  61. });
  62. },
  63. observeTickerItems: function(){
  64. let containers = site.get.tickerItemInsideContainers(elements.itemsNode);
  65. Array.from(containers).forEach(container => {
  66. core.observeTickerItemInsideContainer(container);
  67. });
  68. observe(elements.itemsNode, function(records){
  69. records.forEach(r => r.addedNodes.forEach(node => {
  70. let container = site.get.tickerItemInsideContainer(node);
  71. if(container) core.observeTickerItemInsideContainer(container);
  72. }));
  73. });
  74. },
  75. observeTickerItemInsideContainer: function(container){
  76. container.parentNode.style.background = container.style.background;
  77. let lastUpdated = Date.now();
  78. observe(container, function(records){
  79. let now = Date.now();
  80. if(now - lastUpdated < THROTTLE) return;
  81. lastUpdated = now;
  82. container.parentNode.style.background = container.style.background;
  83. }, {attributes: true, attributeFilter: ['style']});
  84. },
  85. prepareRemoveTickersButton: function(){
  86. let button = createElement(html.removeTickersButton());
  87. button.addEventListener('click', function(e){
  88. elements.itemsNode.parentNode.removeChild(elements.itemsNode);
  89. });
  90. elements.itemsNode.parentNode.appendChild(button);
  91. },
  92. getTarget: function(selector, retry = 10){
  93. const key = selector.name;
  94. const get = function(resolve, reject, retry){
  95. let selected = selector();
  96. if(selected && selected.length > 0) selected.forEach((s) => s.dataset.selector = key);/* elements */
  97. else if(selected instanceof HTMLElement) selected.dataset.selector = key;/* element */
  98. else if(--retry) return log(`Not found: ${key}, retrying... (${retry})`), setTimeout(get, 1000, resolve, reject, retry);
  99. else return reject(selector);
  100. elements[key] = selected;
  101. resolve(selected);
  102. };
  103. return new Promise(function(resolve, reject){
  104. get(resolve, reject, retry);
  105. }).catch(selector => {
  106. log(`Not found: ${key}, I give up.`);
  107. });
  108. },
  109. getTargets: function(selectors, retry = 10){
  110. return Promise.all(Object.values(selectors).map(selector => core.getTarget(selector, retry)));
  111. },
  112. addStyle: function(name = 'style'){
  113. if(html[name] === undefined) return;
  114. let style = createElement(html[name]());
  115. document.head.appendChild(style);
  116. if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]);
  117. elements[name] = style;
  118. },
  119. };
  120. const texts = {
  121. 'remove tickers by ${SCRIPTNAME}': {
  122. en: () => `remove tickers by ${SCRIPTNAME}`,
  123. ja: () => `履歴欄を削除 by ${SCRIPTNAME}`,
  124. zh: () => `删除历史记录栏 by ${SCRIPTNAME}`,
  125. },
  126. };
  127. const html = {
  128. removeTickersButton: () => `<button id="${SCRIPTID}-removeTickers" title="${text('remove tickers by ${SCRIPTNAME}')}">╳</button>`,
  129. style: () => `
  130. <style type="text/css">
  131. yt-live-chat-ticker-renderer #items > *{
  132. border-radius: 999px;
  133. }
  134. yt-live-chat-ticker-renderer #items > * > #container{
  135. background: none !important;
  136. }
  137. yt-live-chat-ticker-renderer #${SCRIPTID}-removeTickers{
  138. cursor: pointer;
  139. position: absolute;
  140. top: 50%;
  141. left: 5px;
  142. transform: translateY(-50%);
  143. border-radius: 100vmax;
  144. border: none;
  145. background: white;
  146. filter: drop-shadow(0px 0px 1px rgba(0,0,0,.25));
  147. height: 20px;
  148. width: 20px;
  149. padding: 0 !important;
  150. opacity: 0;
  151. transition: opacity 250ms;
  152. pointer-events: none;
  153. }
  154. yt-live-chat-ticker-renderer:hover #left-arrow-container[hidden] ~ #${SCRIPTID}-removeTickers{
  155. opacity: 1;
  156. pointer-events: auto;
  157. }
  158. yt-live-chat-ticker-renderer #items > *{
  159. transition: transform 250ms;
  160. }
  161. yt-live-chat-ticker-renderer:hover #items > *{
  162. transform: translateX(5px);
  163. }
  164. </style>
  165. `,
  166. };
  167. const text = function(key, ...args){
  168. if(text.texts[key] === undefined){
  169. log('Not found text key:', key);
  170. return key;
  171. }else return text.texts[key](args);
  172. };
  173. text.setup = function(texts, language){
  174. let languages = [...window.navigator.languages];
  175. if(language) languages.unshift(...String(language).split('-').map((p,i,a) => a.slice(0,1+i).join('-')).reverse());
  176. if(!languages.includes('en')) languages.push('en');
  177. languages = languages.map(l => l.toLowerCase());
  178. Object.keys(texts).forEach(key => {
  179. Object.keys(texts[key]).forEach(l => texts[key][l.toLowerCase()] = texts[key][l]);
  180. texts[key] = texts[key][languages.find(l => texts[key][l] !== undefined)] || (() => key);
  181. });
  182. text.texts = texts;
  183. };
  184. const $ = function(s, f){
  185. let target = document.querySelector(s);
  186. if(target === null) return null;
  187. return f ? f(target) : target;
  188. };
  189. const $$ = function(s, f){
  190. let targets = document.querySelectorAll(s);
  191. return f ? Array.from(targets).map(t => f(t)) : targets;
  192. };
  193. const createElement = function(html = '<span></span>'){
  194. let outer = document.createElement('div');
  195. outer.innerHTML = html;
  196. return outer.firstElementChild;
  197. };
  198. const observe = function(element, callback, options = {childList: true, attributes: false, characterData: false, subtree: false}){
  199. let observer = new MutationObserver(callback.bind(element));
  200. observer.observe(element, options);
  201. return observer;
  202. };
  203. const log = function(){
  204. if(!DEBUG) return;
  205. let l = log.last = log.now || new Date(), n = log.now = new Date();
  206. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  207. //console.log(error.stack);
  208. console.log(
  209. SCRIPTID + ':',
  210. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  211. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  212. /* :00 */ ':' + line,
  213. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  214. /* caller */ (callers[1] || '') + '()',
  215. ...arguments
  216. );
  217. };
  218. log.formats = [{
  219. name: 'Firefox Scratchpad',
  220. detector: /MARKER@Scratchpad/,
  221. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  222. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  223. }, {
  224. name: 'Firefox Console',
  225. detector: /MARKER@debugger/,
  226. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  227. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  228. }, {
  229. name: 'Firefox Greasemonkey 3',
  230. detector: /\/gm_scripts\//,
  231. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  232. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  233. }, {
  234. name: 'Firefox Greasemonkey 4+',
  235. detector: /MARKER@user-script:/,
  236. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  237. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  238. }, {
  239. name: 'Firefox Tampermonkey',
  240. detector: /MARKER@moz-extension:/,
  241. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
  242. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  243. }, {
  244. name: 'Chrome Console',
  245. detector: /at MARKER \(<anonymous>/,
  246. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  247. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  248. }, {
  249. name: 'Chrome Tampermonkey',
  250. detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?id=/,
  251. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 5,
  252. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  253. }, {
  254. name: 'Chrome Extension',
  255. detector: /at MARKER \(chrome-extension:/,
  256. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  257. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  258. }, {
  259. name: 'Edge Console',
  260. detector: /at MARKER \(eval/,
  261. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  262. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  263. }, {
  264. name: 'Edge Tampermonkey',
  265. detector: /at MARKER \(Function/,
  266. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  267. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  268. }, {
  269. name: 'Safari',
  270. detector: /^MARKER$/m,
  271. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  272. getCallers: (e) => e.stack.split('\n'),
  273. }, {
  274. name: 'Default',
  275. detector: /./,
  276. getLine: (e) => 0,
  277. getCallers: (e) => [],
  278. }];
  279. log.format = log.formats.find(function MARKER(f){
  280. if(!f.detector.test(new Error().stack)) return false;
  281. //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack);
  282. return true;
  283. });
  284. core.initialize();
  285. if(console.timeEnd) console.timeEnd(SCRIPTID);
  286. })();