YouTube: Quality Auto Max

To make Quality Auto Max

  1. // ==UserScript==
  2. // @name YouTube: Quality Auto Max
  3. // @namespace UserScripts
  4. // @match https://www.youtube.com/*
  5. // @version 0.3.2
  6. // @author CY Fung
  7. // @license MIT
  8. // @description To make Quality Auto Max
  9. // @grant none
  10. // @run-at document-start
  11. // @unwrap
  12. // @inject-into page
  13. //
  14. // ==/UserScript==
  15.  
  16. (() => {
  17.  
  18. const Promise = (async () => { })().constructor;
  19.  
  20. const PromiseExternal = ((resolve_, reject_) => {
  21. const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
  22. return class PromiseExternal extends Promise {
  23. constructor(cb = h) {
  24. super(cb);
  25. if (cb === h) {
  26. /** @type {(value: any) => void} */
  27. this.resolve = resolve_;
  28. /** @type {(reason?: any) => void} */
  29. this.reject = reject_;
  30. }
  31. }
  32. };
  33. })();
  34.  
  35. const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0);
  36.  
  37. const getResValue = (m) => {
  38.  
  39. return m.width < m.height ? m.width : m.height
  40. }
  41.  
  42. const observablePromise = (proc, timeoutPromise) => {
  43. let promise = null;
  44. return {
  45. obtain() {
  46. if (!promise) {
  47. promise = new Promise(resolve => {
  48. let mo = null;
  49. const f = () => {
  50. let t = proc();
  51. if (t) {
  52. mo.disconnect();
  53. mo.takeRecords();
  54. mo = null;
  55. resolve(t);
  56. }
  57. }
  58. mo = new MutationObserver(f);
  59. mo.observe(document, { subtree: true, childList: true })
  60. f();
  61. timeoutPromise && timeoutPromise.then(() => {
  62. resolve(null)
  63. });
  64. });
  65. }
  66. return promise
  67. }
  68. }
  69. }
  70.  
  71. const addProtoToArr = (parent, key, arr) => {
  72.  
  73.  
  74. let isChildProto = false;
  75. for (const sr of arr) {
  76. if (parent[key].prototype instanceof parent[sr]) {
  77. isChildProto = true;
  78. break;
  79. }
  80. }
  81.  
  82. if (isChildProto) return;
  83.  
  84. arr = arr.filter(sr => {
  85. if (parent[sr].prototype instanceof parent[key]) {
  86. return false;
  87. }
  88. return true;
  89. });
  90.  
  91. arr.push(key);
  92.  
  93. return arr;
  94.  
  95.  
  96. }
  97.  
  98. const getuU = (_yt_player) => {
  99. const w = 'uU';
  100.  
  101. let arr = [];
  102. let brr = new Map();
  103.  
  104. for (const [k, v] of Object.entries(_yt_player)) {
  105.  
  106. const p = typeof v === 'function' ? v.prototype : 0;
  107. if (p) {
  108. let q = 0;
  109. if (typeof p.setPlaybackQualityRange === 'function' && p.setPlaybackQualityRange.length === 3) q += 200;
  110. if (typeof p.updateVideoData === 'function' && p.updateVideoData.length === 2) q += 80;
  111. if (p.getVideoAspectRatio) q += 20;
  112. if (p.getStreamTimeOffset) q += 20;
  113. // if (typeof p.updatePlaylist ==='function' && p.updatePlaylist.length===1)q += 80;
  114.  
  115. if (q < 200) continue; // p.setPlaybackQualityRange must be available
  116.  
  117. if (q > 0) arr = addProtoToArr(_yt_player, k, arr) || arr;
  118.  
  119. if (q > 0) brr.set(k, q);
  120.  
  121. }
  122.  
  123. }
  124.  
  125. if (arr.length === 0) {
  126.  
  127. console.warn(`Key does not exist. [${w}]`);
  128. } else {
  129.  
  130. arr = arr.map(key => [key, (brr.get(key) || 0)]);
  131.  
  132. if (arr.length > 1) arr.sort((a, b) => b[1] - a[1]);
  133.  
  134. console.log(`[${w}]`, arr);
  135. return arr[0][0];
  136. }
  137.  
  138.  
  139.  
  140. }
  141.  
  142. const getL0 = (_yt_player) => {
  143. const w = 'L0';
  144.  
  145. let arr = [];
  146.  
  147. for (const [k, v] of Object.entries(_yt_player)) {
  148.  
  149. const p = typeof v === 'function' ? v.prototype : 0;
  150. if (p) {
  151. let q = 0;
  152. if (typeof p.getPreferredQuality === 'function' && p.getPreferredQuality.length === 0) q += 200;
  153. if (typeof p.getVideoData === 'function' && p.getVideoData.length === 0) q += 80;
  154. if (typeof p.isPlaying === 'function' && p.isPlaying.length === 0) q += 2;
  155.  
  156. if (typeof p.getPlayerState === 'function' && p.getPlayerState.length === 0) q += 2;
  157.  
  158. if (typeof p.getPlayerType === 'function' && p.getPlayerType.length === 0) q += 2;
  159.  
  160. if (q < 280) continue; // p.getPreferredQuality and p.getVideoData must be available
  161.  
  162. if (q > 0) arr.push([k, q])
  163.  
  164. }
  165.  
  166. }
  167.  
  168. if (arr.length === 0) {
  169.  
  170. console.warn(`Key does not exist. [${w}]`);
  171. } else {
  172.  
  173. if (arr.length > 1) arr.sort((a, b) => b[1] - a[1]);
  174.  
  175.  
  176. console.log(`[${w}]`, arr);
  177. return arr[0][0];
  178. }
  179.  
  180.  
  181.  
  182. }
  183.  
  184.  
  185. const getZf = (vL0) => {
  186. const w = 'vL0';
  187.  
  188. let arr = [];
  189.  
  190. for (const [k, v] of Object.entries(vL0)) {
  191.  
  192. // console.log(k,v)
  193.  
  194. const p = v;
  195. if (p) {
  196. let q = 0;
  197. if (typeof p.videoData === 'object' && p.videoData) {
  198.  
  199. if (Object.keys(p).length === 2) q += 200;
  200.  
  201. }
  202.  
  203.  
  204. if (q > 0) arr.push([k, q])
  205.  
  206. }
  207.  
  208. }
  209.  
  210. if (arr.length === 0) {
  211.  
  212. // console.warn(`Key does not exist. [${w}]`);
  213. } else {
  214.  
  215. if (arr.length > 1) arr.sort((a, b) => b[1] - a[1]);
  216.  
  217.  
  218. console.log(`[${w}]`, arr);
  219. return arr[0][0];
  220. }
  221.  
  222.  
  223.  
  224. }
  225.  
  226. const cleanContext = async (win) => {
  227. const waitFn = requestAnimationFrame; // shall have been binded to window
  228. try {
  229. let mx = 16; // MAX TRIAL
  230. const frameId = 'vanillajs-iframe-v1';
  231. /** @type {HTMLIFrameElement | null} */
  232. let frame = document.getElementById(frameId);
  233. let removeIframeFn = null;
  234. if (!frame) {
  235. frame = document.createElement('iframe');
  236. frame.id = frameId;
  237. const blobURL = typeof webkitCancelAnimationFrame === 'function' && typeof kagi === 'undefined' ? (frame.src = URL.createObjectURL(new Blob([], { type: 'text/html' }))) : null; // avoid Brave Crash
  238. frame.sandbox = 'allow-same-origin'; // script cannot be run inside iframe but API can be obtained from iframe
  239. let n = document.createElement('noscript'); // wrap into NOSCRPIT to avoid reflow (layouting)
  240. n.appendChild(frame);
  241. while (!document.documentElement && mx-- > 0) await new Promise(waitFn); // requestAnimationFrame here could get modified by YouTube engine
  242. const root = document.documentElement;
  243. root.appendChild(n); // throw error if root is null due to exceeding MAX TRIAL
  244. if (blobURL) Promise.resolve().then(() => URL.revokeObjectURL(blobURL));
  245.  
  246. removeIframeFn = (setTimeout) => {
  247. const removeIframeOnDocumentReady = (e) => {
  248. e && win.removeEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
  249. win = null;
  250. const m = n;
  251. n = null;
  252. setTimeout(() => m.remove(), 200);
  253. }
  254. if (document.readyState !== 'loading') {
  255. removeIframeOnDocumentReady();
  256. } else {
  257. win.addEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
  258. }
  259. }
  260. }
  261. while (!frame.contentWindow && mx-- > 0) await new Promise(waitFn);
  262. const fc = frame.contentWindow;
  263. if (!fc) throw "window is not found."; // throw error if root is null due to exceeding MAX TRIAL
  264. const { requestAnimationFrame, setTimeout, cancelAnimationFrame, setInterval, clearInterval, requestIdleCallback, getComputedStyle } = fc;
  265. const res = { requestAnimationFrame, setTimeout, cancelAnimationFrame, setInterval, clearInterval, requestIdleCallback, getComputedStyle };
  266. for (let k in res) res[k] = res[k].bind(win); // necessary
  267. if (removeIframeFn) Promise.resolve(res.setTimeout).then(removeIframeFn);
  268. res.animate = fc.HTMLElement.prototype.animate;
  269. return res;
  270. } catch (e) {
  271. console.warn(e);
  272. return null;
  273. }
  274. };
  275.  
  276.  
  277. const isUrlInEmbed = location.href.includes('.youtube.com/embed/');
  278. const isAbortSignalSupported = typeof AbortSignal !== "undefined";
  279.  
  280. cleanContext(window).then(__CONTEXT__ => {
  281. if (!__CONTEXT__) return null;
  282.  
  283. const { setTimeout } = __CONTEXT__;
  284.  
  285. const promiseForTamerTimeout = new Promise(resolve => {
  286. !isUrlInEmbed && isAbortSignalSupported && document.addEventListener('yt-action', function () {
  287. setTimeout(resolve, 480);
  288. }, { capture: true, passive: true, once: true });
  289. !isUrlInEmbed && isAbortSignalSupported && typeof customElements === "object" && customElements.whenDefined('ytd-app').then(() => {
  290. setTimeout(resolve, 1200);
  291. });
  292. setTimeout(resolve, 3000);
  293. });
  294.  
  295.  
  296. let resultantQualities = null;
  297. let byPass = false;
  298.  
  299. // @@ durationchange @@
  300. let pm2 = new PromiseExternal();
  301. let lastURL = null;
  302.  
  303. let activatedOnce = false;
  304.  
  305. const fn = async (evt) => {
  306. try {
  307. const target = (evt || 0).target
  308. if (!(target instanceof HTMLMediaElement)) return;
  309. pm2.resolve();
  310. const pm1 = pm2 = new PromiseExternal();
  311. const mainMedia = await observablePromise(() => {
  312. return isUrlInEmbed ? document.querySelector('#movie_player .html5-main-video') : document.querySelector('ytd-player#ytd-player #movie_player .html5-main-video')
  313. }, pm2.then()).obtain();
  314. if (!mainMedia) return;
  315. if (pm1 !== pm2) return;
  316. const ytdPlayerElm = isUrlInEmbed ? mainMedia.closest('#movie_player') : mainMedia.closest('ytd-player#ytd-player');
  317. if (!ytdPlayerElm) return;
  318.  
  319. let player_
  320. for (let i = 10; --i;) {
  321. player_ = isUrlInEmbed ? ytdPlayerElm : await ((insp(ytdPlayerElm) || 0).player_ || 0);
  322. if (player_) break;
  323. if (pm1 !== pm2) return;
  324. await new Promise(r => setTimeout(r, 18));
  325. }
  326.  
  327. if (!player_) return;
  328. for (let i = 10; --i;) {
  329. if (player_.setPlaybackQualityRange) break;
  330. if (pm1 !== pm2) return;
  331. await new Promise(r => setTimeout(r, 18));
  332. }
  333.  
  334. if (pm1 !== pm2) return;
  335. if (typeof player_.setPlaybackQualityRange !== 'function') return;
  336.  
  337. let _url = lastURL;
  338. let url = mainMedia.src;
  339. if (url === _url) return;
  340. lastURL = url;
  341.  
  342. if (resultantQualities) {
  343. !activatedOnce && console.log('YouTube Quality Auto Max is activated.');
  344. activatedOnce = true;
  345. let resultantQuality;
  346. let qualityThreshold = +localStorage.qualityThreshold || 0;
  347. if (!(qualityThreshold > 60)) qualityThreshold = 0;
  348. for (const entry of resultantQualities) {
  349. const entryRes = getResValue(entry);
  350.  
  351. if (entryRes > 60 && entry.quality && typeof entry.quality === 'string') {
  352.  
  353. if (qualityThreshold === 0 || (qualityThreshold > 60 && entryRes <= qualityThreshold)) {
  354. resultantQuality = entry.quality;
  355. break;
  356. }
  357.  
  358. }
  359. }
  360. if (resultantQuality) {
  361. byPass = true;
  362. const setItemN = function (a, b) {
  363. // console.log('setItem', ...arguments)
  364. // yt-player-quality
  365. // yt-player-performance-cap
  366. // yt-player-performance-cap-active-set
  367. };
  368. const pd = Object.getOwnPropertyDescriptor(localStorage.constructor.prototype, 'setItem');
  369. if (pd && pd.configurable) {
  370. delete localStorage.constructor.prototype.setItem;
  371. Object.defineProperty(localStorage.constructor.prototype, 'setItem', {
  372. get() {
  373. return setItemN
  374. },
  375. set(nv) {
  376. return true;
  377. },
  378. enumerable: false,
  379. configurable: true,
  380. });
  381. }
  382. player_.setPlaybackQualityRange(resultantQuality, resultantQuality);
  383. if (pd && pd.configurable && setItemN === localStorage.setItem) {
  384. delete localStorage.constructor.prototype.setItem;
  385. Object.defineProperty(localStorage.constructor.prototype, 'setItem', pd);
  386. }
  387. byPass = false;
  388. console.log('YouTube Quality Auto Max sets Quality to ', resultantQuality);
  389. }
  390. }
  391. } catch (e) {
  392. console.warn(e)
  393. }
  394. };
  395. // document.addEventListener('loadstart', fn, true)
  396. document.addEventListener('durationchange', fn, true);
  397.  
  398. // @@ durationchange @@
  399.  
  400. (async () => {
  401.  
  402. try {
  403.  
  404.  
  405. const _yt_player = await observablePromise(() => {
  406. return (((window || 0)._yt_player || 0) || 0);
  407. }, promiseForTamerTimeout).obtain();
  408.  
  409. if (!_yt_player || typeof _yt_player !== 'object') return;
  410.  
  411. const vmHash = new WeakSet();
  412.  
  413. const g = _yt_player;
  414. const keyuU = getuU(_yt_player);
  415. const keyL0 = getL0(_yt_player);
  416.  
  417. if (keyuU) {
  418.  
  419. let k = keyuU;
  420. let gk = g[k];
  421. let gkp = g[k].prototype;
  422.  
  423. if(typeof gkp.setPlaybackQualityRange132 !== "function" && typeof gkp.setPlaybackQualityRange === "function"){
  424.  
  425. gkp.setPlaybackQualityRange132 = gkp.setPlaybackQualityRange;
  426. gkp.setPlaybackQualityRange = function (...args) {
  427. if (!byPass && resultantQualities && document.visibilityState === 'visible') {
  428. if (args[0] === args[1] && typeof args[0] === 'string' && args[0]) {
  429. const selectionEntry = resultantQualities.filter(e => e.quality === args[0])[0] || 0
  430. const selectionHeight = selectionEntry ? getResValue(selectionEntry) : 0;
  431. if (selectionHeight > 60) {
  432. localStorage.qualityThreshold = selectionHeight;
  433. }
  434. } else if (!args[0] && !args[1]) {
  435. delete localStorage.qualityThreshold;
  436. }
  437. }
  438. return this.setPlaybackQualityRange132(...args)
  439. };
  440.  
  441. console.log('YouTube Quality Auto Max - function modified [setPlaybackQualityRange]')
  442.  
  443. }
  444.  
  445. }
  446.  
  447. if (keyL0) {
  448. let k = keyL0;
  449. let gk = g[k];
  450. let gkp = g[k].prototype;
  451.  
  452. let keyZf = null;
  453.  
  454. if (typeof gkp.getVideoData31 !== "function" && typeof gkp.getVideoData === "function" && typeof gkp.setupOnNewVideoData61 !== "function") {
  455.  
  456. gkp.getVideoData31 = gkp.getVideoData;
  457. gkp.setupOnNewVideoData61 = function () {
  458.  
  459. keyZf = getZf(this);
  460. if (!keyZf) return;
  461.  
  462. const tZf = this[keyZf];
  463.  
  464. if (!tZf) return;
  465.  
  466. let keyJ = Object.keys(tZf).filter(e => e !== 'videoData')[0]
  467.  
  468. const tZfJ = tZf[keyJ];
  469. const videoData = tZf.videoData;
  470. if (!tZfJ || !videoData || !tZfJ.videoInfos) return;
  471.  
  472.  
  473. let videoTypes = tZfJ.videoInfos.map(info => info.video);
  474.  
  475.  
  476. // console.log(videoTypes)
  477. if (!videoTypes[0] || !videoTypes[0].quality || !getResValue(videoTypes[0])) return;
  478.  
  479. let highestQuality = videoTypes[0].quality
  480.  
  481. // console.log('highestQuality', highestQuality)
  482.  
  483. let keyLists = new Set();
  484. let keyLists2 = new Set();
  485. const o = {
  486. [keyZf]: {
  487. videoData: new Proxy(videoData, {
  488. get(obj, key) {
  489. keyLists.add(key);
  490. const v = obj[key];
  491. if (typeof v === 'object') return new Proxy(v, {
  492. get(obj, key) {
  493. keyLists2.add(key);
  494. return obj[key]
  495. }
  496. })
  497. return v
  498. }
  499. })
  500. }
  501. }
  502.  
  503. this.getPreferredQuality.call(o)
  504. // console.log(keyLists.size, keyLists2.size)
  505. if (keyLists.size !== 2) return;
  506. if (keyLists2.size < 3) return;
  507.  
  508.  
  509.  
  510. /*
  511. * 1080p Premium
  512. g.k.Nj = function(a) {
  513. h_a(this);
  514. this.options[a].element.setAttribute("aria-checked", "true");
  515. this.Yd(this.Dk(a));
  516. this.C = a
  517. }
  518. */
  519.  
  520. /*
  521. TP = function(a) {
  522. return SP[a.j || a.B] || "auto"
  523. }
  524. */
  525.  
  526. const [keyAy, keyxU] = [...keyLists];
  527. const keyLs = [...keyLists2]
  528. const keyPs = [keyAy, keyxU]
  529.  
  530. let cz = 0;
  531. function inc() {
  532. for (const pey of keyPs) {
  533.  
  534. for (const ley of keyLs) {
  535. const val = videoData[pey][ley]
  536. if (typeof val === 'number' && val >= 0 && ~~val === val) {
  537. if (!cz) cz = ley;
  538. if (cz === ley) {
  539. // videoData[pey][ley] = 5120;
  540. // videoData[pey][ley] = videoTypes[0].height;
  541. continue
  542. }
  543. videoData[pey][ley] = getResValue(videoTypes[0]);
  544. // videoData[pey][ley]='1080p Premium'
  545. // videoData[pey][ley] = '1080p';
  546. videoData[pey]['reason'] = 'm'
  547. } else if (typeof val === 'boolean' && val === false) {
  548. videoData[pey][ley] = true;
  549. }
  550. }
  551.  
  552. }
  553. }
  554.  
  555. // console.log(22, this)
  556.  
  557. // const keyyU=getyU(_yt_player);
  558. // _yt_player[keyyU].prototype.
  559.  
  560. resultantQualities = videoTypes;
  561.  
  562.  
  563. console.log('YouTube Quality Auto Max - resultantQualities is detected.');
  564.  
  565. // inc();
  566. // console.log(this.getPreferredQuality())
  567. // inc();
  568. // console.log(this.getPreferredQuality())
  569. // console.log(videoData, keyxU)
  570.  
  571. // console.log(this)
  572. // console.log(1237, keyZf, keyJ, this[keyZf], videoTypes, videoData[keyAy], videoData[keyxU], keyLists2)
  573.  
  574. }
  575. gkp.getVideoData = function () {
  576. const vd = this.getVideoData31();;
  577. if (!vd || typeof vd !== 'object') return vd;
  578. if (!vmHash.has(vd)) {
  579. vmHash.add(vd);
  580. this.setupOnNewVideoData61();
  581. if (!keyZf) vmHash.delete(vd)
  582. }
  583. return vd;
  584. }
  585.  
  586. console.log('YouTube Quality Auto Max - function modified [getVideoData]')
  587. }
  588.  
  589. }
  590.  
  591.  
  592.  
  593.  
  594. } catch (e) {
  595. console.warn(e)
  596. }
  597.  
  598.  
  599.  
  600. })();
  601.  
  602. });
  603.  
  604. })();