YouTube EXPERIMENT_FLAGS Tamer (Basic)

Adjust EXPERIMENT_FLAGS

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name        YouTube EXPERIMENT_FLAGS Tamer (Basic)
// @namespace   UserScripts
// @match       https://www.youtube.com/*
// @version     0.4.8.104
// @license     MIT
// @author      CY Fung
// @icon        https://raw.githubusercontent.com/cyfung1031/userscript-supports/main/icons/yt-engine.png
// @description Adjust EXPERIMENT_FLAGS
// @grant       none
// @unwrap
// @run-at      document-start
// @allFrames   true
// @inject-into page
// ==/UserScript==

((__CONTEXT__) => {

  // Purpose 1: Remove Obsolete Flags
  // Purpose 2: Remove Flags bring no visual difference
  // Purpose 3: Enable Flags bring performance boost

  const DISABLE_CINEMATICS = false; // standard design
  const NO_SerializedExperiment = false;
  const KEEP_PLAYER_QUALITY_STICKY = true; // see https://greasyfork.org/scripts/471033/
  const DISABLE_serializedExperimentIds = true;
  const DISABLE_serializedExperimentFlags = true;


  // const ALLOW_FLAGS_202404_flags11 = new Set([
  //   // 'use_core_sm',
  //   // 'use_new_cml',
  //   // 'web_api_url',
  // ]);

  const ENABLE_EXPERIMENT_FLAGS_MAINTAIN_STABLE_LIST = {
    defaultValue: true, // performance boost
    useExternal: () => typeof localStorage.EXPERIMENT_FLAGS_MAINTAIN_STABLE_LIST !== 'undefined',
    externalValue: () => (+localStorage.EXPERIMENT_FLAGS_MAINTAIN_STABLE_LIST ? true : false)
  };
  const ENABLE_EXPERIMENT_FLAGS_MAINTAIN_REUSE_COMPONENTS = {
    defaultValue: true, // not sure
    useExternal: () => typeof localStorage.EXPERIMENT_FLAGS_MAINTAIN_REUSE_COMPONENTS !== 'undefined',
    externalValue: () => (+localStorage.EXPERIMENT_FLAGS_MAINTAIN_REUSE_COMPONENTS ? true : false)
  };
  const ENABLE_EXPERIMENT_FLAGS_DEFER_DETACH = {
    defaultValue: true, // not sure
    useExternal: () => typeof localStorage.ENABLE_EXPERIMENT_FLAGS_DEFER_DETACH !== 'undefined',
    externalValue: () => (+localStorage.ENABLE_EXPERIMENT_FLAGS_DEFER_DETACH ? true : false)
  };

  const ALLOW_ALL_LIVE_CHATS_FLAGS = true;



  // TBC
  // kevlar_tuner_should_always_use_device_pixel_ratio
  // kevlar_tuner_should_clamp_device_pixel_ratio
  // kevlar_tuner_clamp_device_pixel_ratio
  // kevlar_tuner_should_use_thumbnail_factor
  // kevlar_tuner_thumbnail_factor
  // kevlar_tuner_min_thumbnail_quality
  // kevlar_tuner_max_thumbnail_quality

  // kevlar_tuner_should_test_visibility_time_between_jobs
  // kevlar_tuner_visibility_time_between_jobs_ms

  // kevlar_tuner_default_comments_delay
  // kevlar_tuner_run_default_comments_delay

  let settled = null;
  // cinematic feature is no longer an experimential feature.
  // It has been officially implemented.
  // To disable cinematics, the user shall use other userscripts or just turn off the option in the video options.

  const getSettingValue = (fm) => fm.useExternal() ? fm.externalValue() : fm.defaultValue;

  const win = this instanceof Window ? this : window;

  // Create a unique key for the script and check if it is already running
  const hkey_script = 'jmimcvowrlzl';
  if (win[hkey_script]) throw new Error('Duplicated Userscript Calling'); // avoid duplicated scripting
  win[hkey_script] = true;

  /** @type {globalThis.PromiseConstructor} */
  const Promise = ((async () => { })()).constructor;

  let isMainWindow = false;
  let mzFlagDetected = new Set();
  let zPlayerKevlar = false;
  try {
    isMainWindow = window.document === window.top.document
  } catch (e) { }

  function fixSerializedExperiment(conf) {

    if (DISABLE_serializedExperimentIds && typeof conf.serializedExperimentIds === 'string') {
      let ids = conf.serializedExperimentIds.split(',');
      let newIds = [];
      for (const id of ids) {
        let keep = false;
        if (keep) {
          newIds.push(id);
        }
      }
      conf.serializedExperimentIds = newIds.join(',');
    }

    if (DISABLE_serializedExperimentFlags && typeof conf.serializedExperimentFlags === 'string') {
      const fg = conf.serializedExperimentFlags;
      const rx = /(^|&)(\w+)=([^=&|\s\{\}\[\]\(\)?]*)/g;
      let res = [];
      for (let m; m = rx.exec(fg);) {
        let key = m[2];
        let value = m[3];
        let keep = false;
        if (KEEP_PLAYER_QUALITY_STICKY) {
          if (key === 'html5_exponential_memory_for_sticky' || key.startsWith('h5_expr_')) {
            keep = true;
          }
        }
        if (!DISABLE_CINEMATICS) {
          if (key === 'web_cinematic_watch_settings') {
            keep = true;
          }
        }
        if (keep) res.push(`${key}=${value}`);
      }
      conf.serializedExperimentFlags = res.join('&');
    }

  }

  const cachedSetFn=(o) => {

    const { use_maintain_stable_list, use_maintain_reuse_components, use_defer_detach } = o;

    const BY_PASS = [

      'enable_profile_cards_on_comments',

      'suppress_error_204_logging',
      ...(!DISABLE_CINEMATICS ? [

        'kevlar_measure_ambient_mode_idle',
        'kevlar_watch_cinematics_invisible',
        'web_cinematic_theater_mode',
        'web_cinematic_fullscreen',

        'enable_cinematic_blur_desktop_loading',
        'kevlar_watch_cinematics',
        'web_cinematic_masthead',
        'web_watch_cinematics_preferred_reduced_motion_default_disabled'

      ] : []),

      'live_chat_web_enable_command_handler',
      'live_chat_channel_activity',
      'live_chat_web_input_update',


      ...(ALLOW_ALL_LIVE_CHATS_FLAGS ? [

        'live_chat_banner_expansion_fix',
        'live_chat_enable_mod_view',
        'live_chat_enable_qna_banner_overflow_menu_actions',
        'live_chat_enable_qna_channel',
        'live_chat_enable_send_button_in_slow_mode',
        'live_chat_filter_emoji_suggestions',
        'live_chat_increased_min_height',
        'live_chat_over_playlist',
        'live_chat_web_use_emoji_manager_singleton',
        'live_chat_whole_message_clickable',

        'live_chat_emoji_picker_toggle_state',
        'live_chat_enable_command_handler_resolver_map',
        'live_chat_enable_controller_extraction',
        'live_chat_enable_rta_manager',
        'live_chat_require_space_for_autocomplete_emoji',
        'live_chat_unclickable_message',

      ]:[]),

      'kevlar_rendererstamper_event_listener', // https://github.com/cyfung1031/userscript-supports/issues/11

      // kevlar_enable_up_arrow - no use
      // kevlar_help_use_locale - might use
      // kevlar_refresh_gesture - might use
      // kevlar_smart_downloads - might use
      // kevlar_thumbnail_fluid
      'kevlar_ytb_live_badges',

      ...(!use_maintain_stable_list ? [
        'kevlar_tuner_should_test_maintain_stable_list',
        'kevlar_should_maintain_stable_list',
        'kevlar_tuner_should_maintain_stable_list', // fallback


      ] : []),


      ...(!use_maintain_reuse_components ? [

        'kevlar_tuner_should_test_reuse_components',
        'kevlar_tuner_should_reuse_components',
        'kevlar_should_reuse_components' // fallback

      ] : []),


      'kevlar_system_icons',

      // 'kevlar_prefetch_data_augments_network_data' continue;

      // home page / watch page icons
      'kevlar_three_dot_ink',
      'kevlar_use_wil_icons',
      'kevlar_home_skeleton',

      'kevlar_fluid_touch_scroll',
      'kevlar_watch_color_update',
      'kevlar_use_vimio_behavior', // home page - channel icon

      // collapsed meta; no teaser, use latest collapsed meta design
      'kevlar_structured_description_content_inline',
      'kevlar_watch_metadata_refresh',

      'kevlar_watch_js_panel_height', // affect Tabview Youtube

      'shorts_desktop_watch_while_p2',
      'web_button_rework',
      'web_darker_dark_theme_live_chat',
      'web_darker_dark_theme', // it also affect cinemtaics

      // modern menu
      'web_button_rework_with_live',
      'web_fix_fine_scrubbing_drag',

      // full screen -buggy
      'external_fullscreen',

      // minimize menu
      'web_modern_buttons',
      'web_modern_dialogs',

      // Tabview Youtube - multiline transcript
      'enable_mixed_direction_formatted_strings',


      // Notification Menu
      "kevlar_service_command_check",

      // Live ChatRoom Visibility
      "live_chat_cow_visibility_set_up",

    ].concat(
      [
      ]
    )

    const s = new Set(BY_PASS);

    return s;

  };
  let cachedSet = null;

  const hLooper = ((fn) => {

    let nativeFnLoaded = false;
    let kc1 = 0;

    const setIntervalW = setInterval;
    const clearIntervalW = clearInterval;
    let microDisconnectFn = null;
    let fStopLooper = false;
    const looperFn = () => {
      if (fStopLooper) return;

      let config_ = null;
      let EXPERIMENT_FLAGS = null;
      try {
        config_ = yt.config_;
        EXPERIMENT_FLAGS = config_.EXPERIMENT_FLAGS
      } catch (e) { }

      if (EXPERIMENT_FLAGS) {

        fn(EXPERIMENT_FLAGS, config_);

        if (microDisconnectFn) {
          let isYtLoaded = false;
          try {
            isYtLoaded = typeof ytcfg.set === 'function';
          } catch (e) { }
          if (isYtLoaded) {
            microDisconnectFn();
          }
        }

      }

      let playerKevlar = null;

      try {
        playerKevlar = ytcfg.data_.WEB_PLAYER_CONTEXT_CONFIGS.WEB_PLAYER_CONTEXT_CONFIG_ID_KEVLAR_WATCH;
      } catch (e) { }

      if (playerKevlar && !zPlayerKevlar) {
        zPlayerKevlar = true;

        if (NO_SerializedExperiment && typeof playerKevlar.serializedExperimentFlags === 'string' && typeof playerKevlar.serializedExperimentIds === 'string') {
          fixSerializedExperiment(playerKevlar);
        }


      }



    };

    const controller = {
      start() {
        kc1 = setIntervalW(looperFn, 1);
        (async () => {
          while (true && !nativeFnLoaded) {
            looperFn();
            if (fStopLooper) break;
            await (new Promise(requestAnimationFrame));
          }
        })();
        looperFn();
      },
      /**
       *
       * @param {Window} __CONTEXT__
       */
      setupForCleanContext(__CONTEXT__) {

        const { requestAnimationFrame, setInterval, clearInterval, setTimeout, clearTimeout } = __CONTEXT__;

        (async () => {
          while (true) {
            looperFn();
            if (fStopLooper) break;
            await (new Promise(requestAnimationFrame));
          }
        })();

        let kc2 = setInterval(looperFn, 1);

        const marcoDisconnectFn = () => {
          if (fStopLooper) return;
          Promise.resolve().then(() => {
            if (kc1 || kc2) {
              kc1 && clearIntervalW(kc1); kc1 = 0;
              kc2 && clearInterval(kc2); kc2 = 0;
              looperFn();
            }
            fStopLooper = true;
          });
          document.removeEventListener('yt-page-data-fetched', marcoDisconnectFn, false);
          document.removeEventListener('yt-navigate-finish', marcoDisconnectFn, false);
          document.removeEventListener('spfdone', marcoDisconnectFn, false);
        };
        document.addEventListener('yt-page-data-fetched', marcoDisconnectFn, false);
        document.addEventListener('yt-navigate-finish', marcoDisconnectFn, false);
        document.addEventListener('spfdone', marcoDisconnectFn, false);


        function onReady() {
          if (!fStopLooper) {
            setTimeout(() => {
              !fStopLooper && marcoDisconnectFn();
            }, 1000);
          }
        }

        Promise.resolve().then(() => {
          if (document.readyState !== 'loading') {
            onReady();
          } else {
            window.addEventListener("DOMContentLoaded", onReady, false);
          }
        });

        nativeFnLoaded = true;

        microDisconnectFn = () => Promise.resolve(marcoDisconnectFn).then(setTimeout);

      }
    };

    return controller;
  })((EXPERIMENT_FLAGS, config_) => {

    if (!EXPERIMENT_FLAGS) return;

    if (!settled) {
      settled = {
        use_maintain_stable_list: getSettingValue(ENABLE_EXPERIMENT_FLAGS_MAINTAIN_STABLE_LIST),
        use_maintain_reuse_components: getSettingValue(ENABLE_EXPERIMENT_FLAGS_MAINTAIN_REUSE_COMPONENTS),
        use_defer_detach: getSettingValue(ENABLE_EXPERIMENT_FLAGS_DEFER_DETACH),
      }
      if (settled.use_maintain_stable_list) Promise.resolve().then(() => console.debug("use_maintain_stable_list"));
      if (settled.use_maintain_reuse_components) Promise.resolve().then(() => console.debug("use_maintain_reuse_components"));
      if (settled.use_defer_detach) Promise.resolve().then(() => console.debug("use_defer_detach"));
    }
    const { use_maintain_stable_list, use_maintain_reuse_components, use_defer_detach } = settled;

    cachedSet = cachedSet || cachedSetFn({ use_maintain_stable_list, use_maintain_reuse_components, use_defer_detach });

    let mps = [];

    setTimeout(async ()=>{

      if(!mps.length) return;
      let ezz = new Set();
      let e1 = 999;
      let e2 = -999;
      for(const mp of mps){
        for(const k of mp){
          ezz.add(k);
          const kl= k.length;
          if(kl<e1) e1=kl;
          if(kl>e2) e2=kl;
        }
      }
      mps.length = 0;

      if(!ezz.size) return;

      await new Promise(r => window.setTimeout(r, 1));
      let qt = Date.now();

      console.log('EXPERIMENT_FLAGS', [e1,e2, ezz.size]);

      let mf = false;
      const obj = JSON.parse(localStorage['bpghn01'] || '{}');
      for(const e of ezz){
        if(obj[e])continue;
        obj[e] = qt;
        mf= true;
      }
      if(mf){

      localStorage['bpghn01'] = JSON.stringify( obj);
      }

      // await new Promise(r => window.setTimeout(r, 1));

      const getEFT = function(after, offset){


        after = typeof after === 'string' ? new Date(after) : after;
        let afterValue = +after;


        let arr = Object.entries(obj).map(e=>{
          return {key: e[0], date: e[1], len:e[0].length };
        }).sort((a,b)=>{
          return a.date < b.date ? 1 : a.date>b.date ? -1 : a.len < b.len ? 1 :a.len>b.len ? -1 : `${a.key}`.localeCompare(`${b.key}`) ;
        });

        if (afterValue > 0) {
          arr = arr.filter(e => {
            return e.date >= afterValue + offset;
          })
        }

        return [arr, after, afterValue];

      }

      window.log_EXPERIMENT_FLAGS_Tamer = function(after, toString){

        let [arr, after_, afterValue] =getEFT(after, -86400000);

        const r = {
          "!log": arr,
          after: afterValue > 0 ? new Date(afterValue) : null
        };
        console.log("log_EXPERIMENT_FLAGS_Tamer", toString ? JSON.stringify(r) : r);
        
      }

      window.kl_EXPERIMENT_FLAGS_Tamer = function (after, kl) {


        let [arr, after_, afterValue] =getEFT(after, -86400000);

        arr = arr.filter(e => {
          return e.len === kl
        });

        return arr.map(e => e.key).join('|')


      }


    }, 800);

    const setFalseFn = (EXPERIMENT_FLAGS) => {

      let ezz = new Set();

      for (const [key, value] of Object.entries(EXPERIMENT_FLAGS)) {


        if (value === true) {
          // if(key.indexOf('modern')>=0 || key.indexOf('enable')>=0 || key.indexOf('theme')>=0 || key.indexOf('skip')>=0  || key.indexOf('ui')>=0 || key.indexOf('observer')>=0 || key.indexOf('polymer')>=0 )continue;

          if (mzFlagDetected.has(key)) continue;
          mzFlagDetected.add(key);
          const kl = key.length;
          const kl7 = kl % 7;
          const kl5 = kl % 5;
          const kl3 = kl % 3;
          const kl2 = kl % 2;

          if(cachedSet.has(key)) continue;

          ezz.add(key);

          // console.log(key)
          EXPERIMENT_FLAGS[key] = false;
        }
      }
      mps.push(ezz);
      ezz = null;

    }

    setFalseFn(EXPERIMENT_FLAGS);
    if (config_.EXPERIMENTS_FORCED_FLAGS) setFalseFn(config_.EXPERIMENTS_FORCED_FLAGS);

    EXPERIMENT_FLAGS.desktop_delay_player_resizing = false;
    EXPERIMENT_FLAGS.web_animated_like = false;
    EXPERIMENT_FLAGS.web_animated_like_lazy_load = false;

    if (use_maintain_stable_list) {
      EXPERIMENT_FLAGS.kevlar_tuner_should_test_maintain_stable_list = true;
      EXPERIMENT_FLAGS.kevlar_should_maintain_stable_list = true;
      EXPERIMENT_FLAGS.kevlar_tuner_should_maintain_stable_list = true; // fallback
    }

    if (use_maintain_reuse_components) {
      EXPERIMENT_FLAGS.kevlar_tuner_should_test_reuse_components = true;
      EXPERIMENT_FLAGS.kevlar_tuner_should_reuse_components = true;
      EXPERIMENT_FLAGS.kevlar_should_reuse_components = true; // fallback
    }

    if (use_defer_detach) {
      EXPERIMENT_FLAGS.kevlar_tuner_should_defer_detach = true;
    }

    // EXPERIMENT_FLAGS.kevlar_prefetch_data_augments_network_data = true; // TBC
  });

  hLooper.start();


  const cleanContext = async (win) => {
    const waitFn = requestAnimationFrame; // shall have been binded to window
    try {
      let mx = 16; // MAX TRIAL
      const frameId = 'vanillajs-iframe-v1'
      let frame = document.getElementById(frameId);
      let removeIframeFn = null;
      if (!frame) {
        frame = document.createElement('iframe');
        frame.id = 'vanillajs-iframe-v1';
        frame.sandbox = 'allow-same-origin'; // script cannot be run inside iframe but API can be obtained from iframe
        let n = document.createElement('noscript'); // wrap into NOSCRPIT to avoid reflow (layouting)
        n.appendChild(frame);
        while (!document.documentElement && mx-- > 0) await new Promise(waitFn); // requestAnimationFrame here could get modified by YouTube engine
        const root = document.documentElement;
        root.appendChild(n); // throw error if root is null due to exceeding MAX TRIAL
        removeIframeFn = (setTimeout) => {
          const removeIframeOnDocumentReady = (e) => {
            e && win.removeEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
            win = null;
            setTimeout(() => {
              n.remove();
              n = null;
            }, 200);
          }
          if (document.readyState !== 'loading') {
            removeIframeOnDocumentReady();
          } else {
            win.addEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
          }
        }
      }
      while (!frame.contentWindow && mx-- > 0) await new Promise(waitFn);
      const fc = frame.contentWindow;
      if (!fc) throw "window is not found."; // throw error if root is null due to exceeding MAX TRIAL
      const { requestAnimationFrame, setInterval, setTimeout, clearInterval, clearTimeout } = fc;
      const res = { requestAnimationFrame, setInterval, setTimeout, clearInterval, clearTimeout };
      for (let k in res) res[k] = res[k].bind(win); // necessary
      if (removeIframeFn) Promise.resolve(res.setTimeout).then(removeIframeFn);
      return res;
    } catch (e) {
      console.warn(e);
      return null;
    }
  };

  cleanContext(win).then(__CONTEXT__ => {

    const { requestAnimationFrame, setInterval, clearInterval, setTimeout, clearTimeout } = __CONTEXT__;

    hLooper.setupForCleanContext(__CONTEXT__)

  });


  if (isMainWindow) {

    console.groupCollapsed(
      "%cYouTube EXPERIMENT_FLAGS Tamer",
      "background-color: #EDE43B ; color: #000 ; font-weight: bold ; padding: 4px ;"
    );

    console.log("Script is loaded.");
    console.log("This might affect the new features when YouTube rolls them out to general users.");
    console.log("If you found any issue in using YouTube, please disable this script to check whether the issue is due to this script or not.");

    console.groupEnd();

  }

})(null);