YouTube Live CPU Tamer

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

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

  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.2
  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.2
  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.prepareRemoveTickersButton();
  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. prepareRemoveTickersButton: function(){
  83. let button = createElement(html.removeTickersButton());
  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. removeTickersButton: () => `<button id="${SCRIPTID}-removeTickers" 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}-removeTickers{
  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. pointer-events: none;
  149. }
  150. yt-live-chat-ticker-renderer:hover #left-arrow-container[hidden] ~ #${SCRIPTID}-removeTickers{
  151. opacity: 1;
  152. pointer-events: auto;
  153. }
  154. yt-live-chat-ticker-renderer #items > *{
  155. transition: transform 250ms;
  156. }
  157. yt-live-chat-ticker-renderer:hover #items > *{
  158. transform: translateX(5px);
  159. }
  160. </style>
  161. `,
  162. };
  163. const text = function(key, ...args){
  164. if(text.texts[key] === undefined){
  165. log('Not found text key:', key);
  166. return key;
  167. }else return text.texts[key](args);
  168. };
  169. text.defaultlanguage = 'en';
  170. text.setup = function(texts, languages){
  171. languages = languages.map(l => l.toLowerCase());
  172. if(languages.includes(text.defaultlanguage) === false) languages.push(text.defaultlanguage);
  173. Object.keys(texts).forEach(key => {
  174. Object.keys(texts[key]).forEach(l => texts[key][l.toLowerCase()] = texts[key][l]);
  175. texts[key] = texts[key][languages.find(l => texts[key][l] !== undefined)] || (() => key);
  176. });
  177. text.texts = texts;
  178. };
  179. text.setup(texts, window.navigator.languages);
  180. const $ = function(s, f){
  181. let target = document.querySelector(s);
  182. if(target === null) return null;
  183. return f ? f(target) : target;
  184. };
  185. const $$ = function(s, f){
  186. let targets = document.querySelectorAll(s);
  187. return f ? Array.from(targets).map(t => f(t)) : targets;
  188. };
  189. const createElement = function(html = '<span></span>'){
  190. let outer = document.createElement('div');
  191. outer.innerHTML = html;
  192. return outer.firstElementChild;
  193. };
  194. const observe = function(element, callback, options = {childList: true, attributes: false, characterData: false, subtree: false}){
  195. let observer = new MutationObserver(callback.bind(element));
  196. observer.observe(element, options);
  197. return observer;
  198. };
  199. const log = function(){
  200. if(!DEBUG) return;
  201. let l = log.last = log.now || new Date(), n = log.now = new Date();
  202. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  203. //console.log(error.stack);
  204. console.log(
  205. SCRIPTID + ':',
  206. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  207. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  208. /* :00 */ ':' + line,
  209. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  210. /* caller */ (callers[1] || '') + '()',
  211. ...arguments
  212. );
  213. };
  214. log.formats = [{
  215. name: 'Firefox Scratchpad',
  216. detector: /MARKER@Scratchpad/,
  217. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  218. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  219. }, {
  220. name: 'Firefox Console',
  221. detector: /MARKER@debugger/,
  222. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  223. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  224. }, {
  225. name: 'Firefox Greasemonkey 3',
  226. detector: /\/gm_scripts\//,
  227. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  228. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  229. }, {
  230. name: 'Firefox Greasemonkey 4+',
  231. detector: /MARKER@user-script:/,
  232. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  233. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  234. }, {
  235. name: 'Firefox Tampermonkey',
  236. detector: /MARKER@moz-extension:/,
  237. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
  238. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  239. }, {
  240. name: 'Chrome Console',
  241. detector: /at MARKER \(<anonymous>/,
  242. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  243. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  244. }, {
  245. name: 'Chrome Tampermonkey',
  246. detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?id=/,
  247. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 5,
  248. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  249. }, {
  250. name: 'Chrome Extension',
  251. detector: /at MARKER \(chrome-extension:/,
  252. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  253. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  254. }, {
  255. name: 'Edge Console',
  256. detector: /at MARKER \(eval/,
  257. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  258. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  259. }, {
  260. name: 'Edge Tampermonkey',
  261. detector: /at MARKER \(Function/,
  262. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  263. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  264. }, {
  265. name: 'Safari',
  266. detector: /^MARKER$/m,
  267. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  268. getCallers: (e) => e.stack.split('\n'),
  269. }, {
  270. name: 'Default',
  271. detector: /./,
  272. getLine: (e) => 0,
  273. getCallers: (e) => [],
  274. }];
  275. log.format = log.formats.find(function MARKER(f){
  276. if(!f.detector.test(new Error().stack)) return false;
  277. //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack);
  278. return true;
  279. });
  280. core.initialize();
  281. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址