YouTube: Quality Auto Max

To make Quality Auto Max

目前為 2023-12-30 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        YouTube: Quality Auto Max
// @namespace   UserScripts
// @match       https://www.youtube.com/*
// @version     0.2.2
// @author      CY Fung
// @license     MIT
// @description To make Quality Auto Max
// @grant       none
// @run-at      document-start
// @unwrap
// @inject-into page
// ==/UserScript==

(() => {


  const Promise = (async () => { })().constructor;

  const PromiseExternal = ((resolve_, reject_) => {
    const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
    return class PromiseExternal extends Promise {
      constructor(cb = h) {
        super(cb);
        if (cb === h) {
          /** @type {(value: any) => void} */
          this.resolve = resolve_;
          /** @type {(reason?: any) => void} */
          this.reject = reject_;
        }
      }
    };
  })();

  const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0);

  const getResValue = (m) => {

    return m.width < m.height ? m.width : m.height
  }

  const observablePromise = (proc, timeoutPromise) => {
    let promise = null;
    return {
      obtain() {
        if (!promise) {
          promise = new Promise(resolve => {
            let mo = null;
            const f = () => {
              let t = proc();
              if (t) {
                mo.disconnect();
                mo.takeRecords();
                mo = null;
                resolve(t);
              }
            }
            mo = new MutationObserver(f);
            mo.observe(document, { subtree: true, childList: true })
            f();
            timeoutPromise && timeoutPromise.then(() => {
              resolve(null)
            });
          });
        }
        return promise
      }
    }
  }

  const addProtoToArr = (parent, key, arr) => {


    let isChildProto = false;
    for (const sr of arr) {
      if (parent[key].prototype instanceof parent[sr]) {
        isChildProto = true;
        break;
      }
    }

    if (isChildProto) return;

    arr = arr.filter(sr => {
      if (parent[sr].prototype instanceof parent[key]) {
        return false;
      }
      return true;
    });

    arr.push(key);

    return arr;


  }

  const getuU = (_yt_player) => {
    const w = 'uU';

    let arr = [];

    for (const [k, v] of Object.entries(_yt_player)) {

      const p = typeof v === 'function' ? v.prototype : 0;
      if (p) {
        let q = 0;
        if (typeof p.setPlaybackQualityRange === 'function' && p.setPlaybackQualityRange.length === 3) q += 200;
        if (typeof p.updateVideoData === 'function' && p.updateVideoData.length === 2) q += 80;
        if (p.getVideoAspectRatio) q += 20;
        if (p.getStreamTimeOffset) q += 20;
        // if (typeof p.updatePlaylist ==='function' && p.updatePlaylist.length===1)q += 80;

        if (q > 0) arr = addProtoToArr(_yt_player, k, arr) || arr;

      }

    }

    if (arr.length === 0) {

      console.warn(`Key does not exist. [${w}]`);
    } else {

      if (arr.length > 1) arr.sort((a, b) => b[1] - a[1]);


      console.log(`[${w}]`, arr);
      return arr[0];
    }



  }

  const getL0 = (_yt_player) => {
    const w = 'L0';

    let arr = [];

    for (const [k, v] of Object.entries(_yt_player)) {

      const p = typeof v === 'function' ? v.prototype : 0;
      if (p) {
        let q = 0;
        if (typeof p.getPreferredQuality === 'function' && p.getPreferredQuality.length === 0) q += 200;
        if (typeof p.getVideoData === 'function' && p.getVideoData.length === 0) q += 80;
        if (typeof p.isPlaying === 'function' && p.isPlaying.length === 0) q += 2;

        if (typeof p.getPlayerState === 'function' && p.getPlayerState.length === 0) q += 2;

        if (typeof p.getPlayerType === 'function' && p.getPlayerType.length === 0) q += 2;


        if (q > 0) arr.push([k, q])

      }

    }

    if (arr.length === 0) {

      console.warn(`Key does not exist. [${w}]`);
    } else {

      if (arr.length > 1) arr.sort((a, b) => b[1] - a[1]);


      console.log(`[${w}]`, arr);
      return arr[0][0];
    }



  }


  const getZf = (vL0) => {
    const w = 'vL0';

    let arr = [];

    for (const [k, v] of Object.entries(vL0)) {

      // console.log(k,v)

      const p = v;
      if (p) {
        let q = 0;
        if (typeof p.videoData === 'object' && p.videoData) {

          if (Object.keys(p).length === 2) q += 200;

        }


        if (q > 0) arr.push([k, q])

      }

    }

    if (arr.length === 0) {

      // console.warn(`Key does not exist. [${w}]`);
    } else {

      if (arr.length > 1) arr.sort((a, b) => b[1] - a[1]);


      console.log(`[${w}]`, arr);
      return arr[0][0];
    }



  }




  const onRegistryReady = (callback) => {
    if (typeof customElements === 'undefined') {
      if (!('__CE_registry' in document)) {
        // https://github.com/webcomponents/polyfills/
        Object.defineProperty(document, '__CE_registry', {
          get() {
            // return undefined
          },
          set(nv) {
            if (typeof nv == 'object') {
              delete this.__CE_registry;
              this.__CE_registry = nv;
              this.dispatchEvent(new CustomEvent(EVENT_KEY_ON_REGISTRY_READY));
            }
            return true;
          },
          enumerable: false,
          configurable: true
        })
      }
      let eventHandler = (evt) => {
        document.removeEventListener(EVENT_KEY_ON_REGISTRY_READY, eventHandler, false);
        const f = callback;
        callback = null;
        eventHandler = null;
        f();
      };
      document.addEventListener(EVENT_KEY_ON_REGISTRY_READY, eventHandler, false);
    } else {
      callback();
    }
  };

  const cleanContext = async (win) => {
    const waitFn = requestAnimationFrame; // shall have been binded to window
    try {
      let mx = 16; // MAX TRIAL
      const frameId = 'vanillajs-iframe-v1';
      /** @type {HTMLIFrameElement | null} */
      let frame = document.getElementById(frameId);
      let removeIframeFn = null;
      if (!frame) {
        frame = document.createElement('iframe');
        frame.id = frameId;
        const blobURL = typeof webkitCancelAnimationFrame === 'function' ? (frame.src = URL.createObjectURL(new Blob([], { type: 'text/html' }))) : null; // avoid Brave Crash
        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
        if (blobURL) Promise.resolve().then(() => URL.revokeObjectURL(blobURL));

        removeIframeFn = (setTimeout) => {
          const removeIframeOnDocumentReady = (e) => {
            e && win.removeEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
            win = null;
            const m = n;
            n = null;
            setTimeout(() => m.remove(), 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, setTimeout, cancelAnimationFrame, setInterval, clearInterval, requestIdleCallback, getComputedStyle } = fc;
      const res = { requestAnimationFrame, setTimeout, cancelAnimationFrame, setInterval, clearInterval, requestIdleCallback, getComputedStyle };
      for (let k in res) res[k] = res[k].bind(win); // necessary
      if (removeIframeFn) Promise.resolve(res.setTimeout).then(removeIframeFn);
      res.animate = fc.HTMLElement.prototype.animate;
      return res;
    } catch (e) {
      console.warn(e);
      return null;
    }
  };


  const promiseForCustomYtElementsReady = new Promise(onRegistryReady);

  cleanContext(window).then(__CONTEXT__ => {
    if (!__CONTEXT__) return null;

    const { setTimeout } = __CONTEXT__;




    const promiseForTamerTimeout = new Promise(resolve => {
      promiseForCustomYtElementsReady.then(() => {
        customElements.whenDefined('ytd-app').then(() => {
          setTimeout(resolve, 1200);
        });
      });
      setTimeout(resolve, 3000);
    });


    let resultantQualities = null;
    let byPass = false;

    (async () => {


      let pm2 = new PromiseExternal();
      let lastURL = null;

      const fn = async (evt) => {
        try {
          const target = (evt || 0).target
          if (!(target instanceof HTMLVideoElement)) return;
          let _url = lastURL;
          let url = target.src;
          if (url === _url) return;
          lastURL = url;

          pm2.resolve();
          pm2 = new PromiseExternal();
          const ytdPlayerElm = await observablePromise(() => {
            return target.closest('ytd-player#ytd-player')
          }, pm2.then()).obtain();

          if (!ytdPlayerElm) return;
          let player_
          for (let i = 10; --i;) {
            player_ = await ((insp(ytdPlayerElm) || 0).player_ || 0);
            if (player_) break;
            await new Promise(r => setTimeout(r));
          }

          if (!player_) return;
          for (let i = 10; --i;) {
            if (player_.setPlaybackQualityRange) break;
            await new Promise(r => setTimeout(r));
          }

          if (!player_.setPlaybackQualityRange) return;
          if (resultantQualities) {
            let resultantQuality;
            let qualityThreshold = +localStorage.qualityThreshold || 0;
            if (!(qualityThreshold > 60)) qualityThreshold = 0;
            for (const entry of resultantQualities) {
              const entryRes = getResValue(entry);

              if (entryRes > 60 && entry.quality && typeof entry.quality === 'string') {


                if (qualityThreshold === 0 || (qualityThreshold > 60 && entryRes <= qualityThreshold)) {
                  resultantQuality = entry.quality;
                  break;
                }

              }
            }
            if (resultantQuality) {
              byPass = true;

              player_.setPlaybackQualityRange(resultantQuality, resultantQuality)
              byPass = false;
            }
          }
        } catch (e) {
          console.warn(e)
        }
      };
      // document.addEventListener('loadstart', fn, true)
      document.addEventListener('durationchange', fn, true);
    })();


    (async () => {

      try {

        const observablePromise = (proc, timeoutPromise) => {
          let promise = null;
          return {
            obtain() {
              if (!promise) {
                promise = new Promise(resolve => {
                  let mo = null;
                  const f = () => {
                    let t = proc();
                    if (t) {
                      mo.disconnect();
                      mo.takeRecords();
                      mo = null;
                      resolve(t);
                    }
                  }
                  mo = new MutationObserver(f);
                  mo.observe(document, { subtree: true, childList: true })
                  f();
                  timeoutPromise && timeoutPromise.then(() => {
                    resolve(null)
                  });
                });
              }
              return promise
            }
          }
        }


        const _yt_player = await observablePromise(() => {
          return (((window || 0)._yt_player || 0) || 0);
        }, promiseForTamerTimeout).obtain();

        if (!_yt_player || typeof _yt_player !== 'object') return;

        const vmHash = new WeakSet();

        const g = _yt_player;
        const keyuU = getuU(_yt_player);
        const keyL0 = getL0(_yt_player);

        if (keyuU) {

          let k = keyuU;
          let gk = g[k];
          let gkp = g[k].prototype;

          gkp.setPlaybackQualityRange132 = gkp.setPlaybackQualityRange;
          gkp.setPlaybackQualityRange = function (...args) {
            if (!byPass && resultantQualities && document.visibilityState === 'visible') {
              if (args[0] === args[1] && typeof args[0] === 'string' && args[0]) {
                const selectionEntry = resultantQualities.filter(e => e.quality === args[0])[0] || 0
                const selectionHeight = selectionEntry ? getResValue(selectionEntry) : 0;
                if (selectionHeight > 60) {
                  localStorage.qualityThreshold = selectionHeight;
                }
              } else if (!args[0] && !args[1]) {
                delete localStorage.qualityThreshold;
              }
            }
            return this.setPlaybackQualityRange132(...args)
          }
        }

        if (keyL0) {
          let k = keyL0;
          let gk = g[k];
          let gkp = g[k].prototype;

          let keyZf = null;

          gkp.getVideoData31 = gkp.getVideoData;
          gkp.setupOnNewVideoData61 = function () {

            keyZf = getZf(this);
            if (!keyZf) return;

            const tZf = this[keyZf];

            if (!tZf) return;

            let keyJ = Object.keys(tZf).filter(e => e !== 'videoData')[0]

            const tZfJ = tZf[keyJ];
            const videoData = tZf.videoData;
            if (!tZfJ || !videoData || !tZfJ.videoInfos) return;


            let videoTypes = tZfJ.videoInfos.map(info => info.video);


            // console.log(videoTypes)
            if (!videoTypes[0] || !videoTypes[0].quality || !getResValue(videoTypes[0])) return;

            let highestQuality = videoTypes[0].quality

            // console.log('highestQuality', highestQuality)

            let keyLists = new Set();
            let keyLists2 = new Set();
            const o = {
              [keyZf]: {
                videoData: new Proxy(videoData, {
                  get(obj, key) {
                    keyLists.add(key);
                    const v = obj[key];
                    if (typeof v === 'object') return new Proxy(v, {
                      get(obj, key) {
                        keyLists2.add(key);
                        return obj[key]
                      }
                    })
                    return v
                  }
                })
              }
            }

            this.getPreferredQuality.call(o)
            // console.log(keyLists.size, keyLists2.size)
            if (keyLists.size !== 2) return;
            if (keyLists2.size < 3) return;



            /*
             * 1080p Premium

                g.k.Nj = function(a) {
                    h_a(this);
                    this.options[a].element.setAttribute("aria-checked", "true");
                    this.Yd(this.Dk(a));
                    this.C = a
                }

            */

            /*
                TP = function(a) {
                    return SP[a.j || a.B] || "auto"
                }
            */

            const [keyAy, keyxU] = [...keyLists];
            const keyLs = [...keyLists2]
            const keyPs = [keyAy, keyxU]

            let cz = 0;
            function inc() {
              for (const pey of keyPs) {

                for (const ley of keyLs) {
                  const val = videoData[pey][ley]
                  if (typeof val === 'number' && val >= 0 && ~~val === val) {
                    if (!cz) cz = ley;
                    if (cz === ley) {
                      // videoData[pey][ley]  = 5120;
                      // videoData[pey][ley] = videoTypes[0].height;
                      continue
                    }
                    videoData[pey][ley] = getResValue(videoTypes[0]);
                    // videoData[pey][ley]='1080p Premium'
                    // videoData[pey][ley] = '1080p';
                    videoData[pey]['reason'] = 'm'
                  } else if (typeof val === 'boolean' && val === false) {
                    videoData[pey][ley] = true;
                  }
                }

              }
            }

            // console.log(22, this)

            // const keyyU=getyU(_yt_player);
            // _yt_player[keyyU].prototype.

            resultantQualities = videoTypes;

            // inc();
            // console.log(this.getPreferredQuality())
            // inc();
            // console.log(this.getPreferredQuality())
            // console.log(videoData, keyxU)

            // console.log(this)
            // console.log(1237, keyZf, keyJ, this[keyZf], videoTypes, videoData[keyAy], videoData[keyxU], keyLists2)

          }
          gkp.getVideoData = function () {
            const vd = this.getVideoData31();;
            if (!vd || typeof vd !== 'object') return vd;
            if (!vmHash.has(vd)) {
              vmHash.add(vd);
              this.setupOnNewVideoData61();
              if (!keyZf) vmHash.delete(vd)
            }
            return vd;
          }


        }




      } catch (e) {
        console.warn(e)
      }



    })();

  });

})();