YouTube Live CPU Tamer

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

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        YouTube Live CPU Tamer
// @name:ja     YouTube Live CPU Tamer
// @name:zh-CN  YouTube Live CPU Tamer
// @description It reduces the high CPU usage on Super Chats with nothing to lose.
// @description:ja スーパーチャットによる高いCPU使用率を削減します。見た目は何も変わりません。
// @description:zh-CN 降低超级聊天的高CPU利用率。外观完全没有变化。
// @namespace   knoa.jp
// @include     https://www.youtube.com/live_chat*
// @include     https://www.youtube.com/live_chat_replay*
// @version     2.0.2
// @grant       none
// ==/UserScript==

(function(){
  const SCRIPTID = 'YouTubeLiveCpuTamer';
  const SCRIPTNAME = 'YouTube Live CPU Tamer';
  const DEBUG = false;/*
[update] 2.0.2
Added "remove tickers" button for further CPU usage reduction. + minor fix.

[bug]

[todo]

[possible]

[research]
放送開始前の待機画面でもHelper(GPU)が食ってる件
リアルタイム視聴時のほうがHelper(GPU)が消費する件は新着確認js処理のせいか

[memo]
  */
  if(console.time) console.time(SCRIPTID);
  const MS = 1, SECOND = 1000*MS, MINUTE = 60*SECOND, HOUR = 60*MINUTE, DAY = 24*HOUR, WEEK = 7*DAY, MONTH = 30*DAY, YEAR = 365*DAY;
  const THROTTLE = 1000;
  const site = {
    targets: {
      itemsNode: () => $('yt-live-chat-ticker-renderer #items'),
    },
    get: {
      tickerItemInsideContainers: (items) => items.querySelectorAll('yt-live-chat-ticker-paid-message-item-renderer #container'),/* existing items */
      tickerItemInsideContainer: (node) => node.querySelector('yt-live-chat-ticker-paid-message-item-renderer #container'),/* for observer */
    },
  };
  let elements = {};
  const core = {
    initialize: function(){
      elements.html = document.documentElement;
      elements.html.classList.add(SCRIPTID);
      core.ready();
      core.addStyle('style');
    },
    ready: function(){
      core.getTargets(site.targets).then(() => {
        log("I'm ready.");
        core.observeTickerItems();
        core.prepareRemoveTickersButton();
      });
    },
    observeTickerItems: function(){
      Array.from(site.get.tickerItemInsideContainers(elements.itemsNode)).forEach(container => {
        core.tickerItemInsideContainer(container);
      });
      observe(elements.itemsNode, function(records){
        records.forEach(r => r.addedNodes.forEach(node => {
          let container = site.get.tickerItemInsideContainer(node);
          if(container) core.observeTickerItemInsideContainer(container);
        }));
      });
    },
    observeTickerItemInsideContainer: function(container){
      container.parentNode.style.background = container.style.background;
      let lastUpdated = Date.now();
      observe(container, function(records){
        let now = Date.now();
        if(now - lastUpdated < THROTTLE) return;
        lastUpdated = now;
        container.parentNode.style.background = container.style.background;
      }, {attributes: true, attributeFilter: ['style']});
    },
    prepareRemoveTickersButton: function(){
      let button = createElement(html.removeTickersButton());
      button.addEventListener('click', function(e){
        elements.itemsNode.parentNode.removeChild(elements.itemsNode);
      });
      elements.itemsNode.parentNode.appendChild(button);
    },
    getTarget: function(selector, retry = 10){
      const key = selector.name;
      const get = function(resolve, reject, retry){
        let selected = selector();
        if(selected && selected.length > 0) selected.forEach((s) => s.dataset.selector = key);/* elements */
        else if(selected instanceof HTMLElement) selected.dataset.selector = key;/* element */
        else if(--retry) return log(`Not found: ${key}, retrying... (${retry})`), setTimeout(get, 1000, resolve, reject, retry);
        else return reject(selector);
        elements[key] = selected;
        resolve(selected);
      };
      return new Promise(function(resolve, reject){
        get(resolve, reject, retry);
      }).catch(selector => {
        log(`Not found: ${key}, I give up.`);
      });
    },
    getTargets: function(targets, retry = 10){
      return Promise.all(Object.values(targets).map(selector => core.getTarget(selector, retry)));
    },
    addStyle: function(name = 'style'){
      if(html[name] === undefined) return;
      let style = createElement(html[name]());
      document.head.appendChild(style);
      if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]);
      elements[name] = style;
    },
  };
  const texts = {
    'remove tickers by ${SCRIPTNAME}': {
      en: () => `remove tickers by ${SCRIPTNAME}`,
      ja: () => `履歴欄を削除 by ${SCRIPTNAME}`,
      zh: () => `删除历史记录栏 by ${SCRIPTNAME}`,
    },
  };
  const html = {
    removeTickersButton: () => `<button id="${SCRIPTID}-removeTickers" title="${text('remove tickers by ${SCRIPTNAME}')}">╳</button>`,
    style: () => `
      <style type="text/css">
        yt-live-chat-ticker-renderer #items > *{
          border-radius: 999px;
        }
        yt-live-chat-ticker-renderer #items > * > #container{
          background: none !important;
        }
        yt-live-chat-ticker-renderer #${SCRIPTID}-removeTickers{
          cursor: pointer;
          position: absolute;
          top: 50%;
          left: 5px;
          transform: translateY(-50%);
          border-radius: 100vmax;
          background: white;
          filter: drop-shadow(0px 0px 1px rgba(0,0,0,.25));
          height: 20px;
          width: 20px;
          padding: 0 !important;
          opacity: 0;
          transition: opacity 250ms;
          pointer-events: none;
        }
        yt-live-chat-ticker-renderer:hover #left-arrow-container[hidden] ~ #${SCRIPTID}-removeTickers{
          opacity: 1;
          pointer-events: auto;
        }
        yt-live-chat-ticker-renderer #items > *{
          transition: transform 250ms;
        }
        yt-live-chat-ticker-renderer:hover #items > *{
          transform: translateX(5px);
        }
      </style>
    `,
  };
  const text = function(key, ...args){
    if(text.texts[key] === undefined){
      log('Not found text key:', key);
      return key;
    }else return text.texts[key](args);
  };
  text.defaultlanguage = 'en';
  text.setup = function(texts, languages){
    languages = languages.map(l => l.toLowerCase());
    if(languages.includes(text.defaultlanguage) === false) languages.push(text.defaultlanguage);
    Object.keys(texts).forEach(key => {
      Object.keys(texts[key]).forEach(l => texts[key][l.toLowerCase()] = texts[key][l]);
      texts[key] = texts[key][languages.find(l => texts[key][l] !== undefined)] || (() => key);
    });
    text.texts = texts;
  };
  text.setup(texts, window.navigator.languages);
  const $ = function(s, f){
    let target = document.querySelector(s);
    if(target === null) return null;
    return f ? f(target) : target;
  };
  const $$ = function(s, f){
    let targets = document.querySelectorAll(s);
    return f ? Array.from(targets).map(t => f(t)) : targets;
  };
  const createElement = function(html = '<span></span>'){
    let outer = document.createElement('div');
    outer.innerHTML = html;
    return outer.firstElementChild;
  };
  const observe = function(element, callback, options = {childList: true, attributes: false, characterData: false, subtree: false}){
    let observer = new MutationObserver(callback.bind(element));
    observer.observe(element, options);
    return observer;
  };
  const log = function(){
    if(!DEBUG) return;
    let l = log.last = log.now || new Date(), n = log.now = new Date();
    let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
    //console.log(error.stack);
    console.log(
      SCRIPTID + ':',
      /* 00:00:00.000  */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
      /* +0.000s       */ '+' + ((n-l)/1000).toFixed(3) + 's',
      /* :00           */ ':' + line,
      /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
      /* caller        */ (callers[1] || '') + '()',
      ...arguments
    );
  };
  log.formats = [{
      name: 'Firefox Scratchpad',
      detector: /MARKER@Scratchpad/,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Firefox Console',
      detector: /MARKER@debugger/,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Firefox Greasemonkey 3',
      detector: /\/gm_scripts\//,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Firefox Greasemonkey 4+',
      detector: /MARKER@user-script:/,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Firefox Tampermonkey',
      detector: /MARKER@moz-extension:/,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Chrome Console',
      detector: /at MARKER \(<anonymous>/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
    }, {
      name: 'Chrome Tampermonkey',
      detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?id=/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 5,
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
    }, {
      name: 'Chrome Extension',
      detector: /at MARKER \(chrome-extension:/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
    }, {
      name: 'Edge Console',
      detector: /at MARKER \(eval/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
    }, {
      name: 'Edge Tampermonkey',
      detector: /at MARKER \(Function/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
    }, {
      name: 'Safari',
      detector: /^MARKER$/m,
      getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
      getCallers: (e) => e.stack.split('\n'),
    }, {
      name: 'Default',
      detector: /./,
      getLine: (e) => 0,
      getCallers: (e) => [],
    }];
  log.format = log.formats.find(function MARKER(f){
    if(!f.detector.test(new Error().stack)) return false;
    //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack);
    return true;
  });
  core.initialize();
})();