YouTube Live Cpu Tamer

降低超级聊天的高CPU利用率。外观完全没有变化。

目前为 2020-05-08 提交的版本。查看 最新版本

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