YouTube JS Engine Tamer

To enhance YouTube performance by modifying YouTube JS Engine

当前为 2023-08-26 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube JS Engine Tamer
  3. // @namespace UserScripts
  4. // @match https://www.youtube.com/*
  5. // @version 0.1.0
  6. // @license MIT
  7. // @author CY Fung
  8. // @icon https://github.com/cyfung1031/userscript-supports/raw/main/icons/yt-engine.png
  9. // @description To enhance YouTube performance by modifying YouTube JS Engine
  10. // @grant none
  11. // @run-at document-start
  12. // @unwrap
  13. // @inject-into page
  14. // @allFrames true
  15. // ==/UserScript==
  16.  
  17. (() => {
  18.  
  19. const NATIVE_CANVAS_ANIMATION = true; // for #cinematics
  20. const FIX_schedulerInstanceInstance_ = true;
  21. const FIX_yt_player = true;
  22.  
  23. const Promise = (async () => { })().constructor;
  24.  
  25. let isMainWindow = false;
  26. try {
  27. isMainWindow = window.document === window.top.document
  28. } catch (e) { }
  29. const onRegistryReady = (callback) => {
  30. if (typeof customElements === 'undefined') {
  31. if (!('__CE_registry' in document)) {
  32. // https://github.com/webcomponents/polyfills/
  33. Object.defineProperty(document, '__CE_registry', {
  34. get() {
  35. // return undefined
  36. },
  37. set(nv) {
  38. if (typeof nv == 'object') {
  39. delete this.__CE_registry;
  40. this.__CE_registry = nv;
  41. this.dispatchEvent(new CustomEvent(EVENT_KEY_ON_REGISTRY_READY));
  42. }
  43. return true;
  44. },
  45. enumerable: false,
  46. configurable: true
  47. })
  48. }
  49. let eventHandler = (evt) => {
  50. document.removeEventListener(EVENT_KEY_ON_REGISTRY_READY, eventHandler, false);
  51. const f = callback;
  52. callback = null;
  53. eventHandler = null;
  54. f();
  55. };
  56. document.addEventListener(EVENT_KEY_ON_REGISTRY_READY, eventHandler, false);
  57. } else {
  58. callback();
  59. }
  60. };
  61.  
  62. const getZq = (_yt_player) => {
  63.  
  64.  
  65. for (const [k, v] of Object.entries(_yt_player)) {
  66.  
  67. const p = typeof v === 'function' ? v.prototype : 0;
  68. if (p
  69. && typeof p.start === 'function'
  70. && typeof p.isActive === 'function'
  71. && typeof p.stop === 'function') {
  72.  
  73. return k;
  74.  
  75. }
  76.  
  77. }
  78.  
  79.  
  80. }
  81.  
  82.  
  83. let foregroundPromise = null;
  84.  
  85. const getForegroundPromise = () => {
  86. if (document.visibilityState === 'visible') return Promise.resolve();
  87. else {
  88. return foregroundPromise = foregroundPromise || new Promise(resolve => {
  89. requestAnimationFrame(() => {
  90. foregroundPromise = null;
  91. resolve();
  92. });
  93. });
  94. }
  95. }
  96.  
  97. // << if FIX_schedulerInstanceInstance_ >>
  98.  
  99. let idleFrom = Date.now() + 2700;
  100. let slowMode = false;
  101.  
  102. let ytEvented = false;
  103.  
  104.  
  105. function setupEvents() {
  106.  
  107. document.addEventListener('yt-navigate', () => {
  108.  
  109. ytEvented = true;
  110. slowMode = false;
  111. idleFrom = Date.now() + 2700;
  112.  
  113. });
  114. document.addEventListener('yt-navigate-start', () => {
  115.  
  116. ytEvented = true;
  117. slowMode = false;
  118. idleFrom = Date.now() + 2700;
  119.  
  120. });
  121.  
  122. document.addEventListener('yt-page-type-changed', () => {
  123.  
  124. ytEvented = true;
  125. slowMode = false;
  126. idleFrom = Date.now() + 1700;
  127.  
  128. });
  129.  
  130.  
  131. document.addEventListener('yt-player-updated', () => {
  132.  
  133. ytEvented = true;
  134. slowMode = false;
  135. idleFrom = Date.now() + 1700;
  136.  
  137. });
  138.  
  139.  
  140. document.addEventListener('yt-page-data-fetched', () => {
  141.  
  142. ytEvented = true;
  143. slowMode = false;
  144. idleFrom = Date.now() + 1700;
  145.  
  146. });
  147.  
  148. document.addEventListener('yt-navigate-finish', () => {
  149.  
  150. ytEvented = true;
  151. slowMode = false;
  152. let t = Date.now() + 700;
  153. if (t > idleFrom) idleFrom = t;
  154.  
  155. });
  156.  
  157. document.addEventListener('yt-page-data-updated', () => {
  158.  
  159. ytEvented = true;
  160. slowMode = false;
  161. let t = Date.now() + 700;
  162. if (t > idleFrom) idleFrom = t;
  163.  
  164. });
  165.  
  166. document.addEventListener('yt-watch-comments-ready', () => {
  167.  
  168. ytEvented = true;
  169. slowMode = false;
  170. let t = Date.now() + 700;
  171. if (t > idleFrom) idleFrom = t;
  172.  
  173. });
  174. }
  175.  
  176.  
  177. // << end >>
  178.  
  179. const cleanContext = async (win) => {
  180. const waitFn = requestAnimationFrame; // shall have been binded to window
  181. try {
  182. let mx = 16; // MAX TRIAL
  183. const frameId = 'vanillajs-iframe-v1';
  184. /** @type {HTMLIFrameElement | null} */
  185. let frame = document.getElementById(frameId);
  186. let removeIframeFn = null;
  187. if (!frame) {
  188. frame = document.createElement('iframe');
  189. frame.id = 'vanillajs-iframe-v1';
  190. frame.sandbox = 'allow-same-origin'; // script cannot be run inside iframe but API can be obtained from iframe
  191. let n = document.createElement('noscript'); // wrap into NOSCRPIT to avoid reflow (layouting)
  192. n.appendChild(frame);
  193. while (!document.documentElement && mx-- > 0) await new Promise(waitFn); // requestAnimationFrame here could get modified by YouTube engine
  194. const root = document.documentElement;
  195. root.appendChild(n); // throw error if root is null due to exceeding MAX TRIAL
  196. removeIframeFn = (setTimeout) => {
  197. const removeIframeOnDocumentReady = (e) => {
  198. e && win.removeEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
  199. win = null;
  200. setTimeout(() => {
  201. n.remove();
  202. n = null;
  203. }, 200);
  204. }
  205. if (document.readyState !== 'loading') {
  206. removeIframeOnDocumentReady();
  207. } else {
  208. win.addEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
  209. }
  210. }
  211. }
  212. while (!frame.contentWindow && mx-- > 0) await new Promise(waitFn);
  213. const fc = frame.contentWindow;
  214. if (!fc) throw "window is not found."; // throw error if root is null due to exceeding MAX TRIAL
  215. const { requestAnimationFrame, setTimeout, cancelAnimationFrame, setInterval, clearInterval, requestIdleCallback } = fc;
  216. const res = { requestAnimationFrame, setTimeout, cancelAnimationFrame, setInterval, clearInterval, requestIdleCallback };
  217. for (let k in res) res[k] = res[k].bind(win); // necessary
  218. if (removeIframeFn) Promise.resolve(res.setTimeout).then(removeIframeFn);
  219. res.animate = HTMLElement.prototype.animate;
  220. return res;
  221. } catch (e) {
  222. console.warn(e);
  223. return null;
  224. }
  225. };
  226.  
  227.  
  228. const promiseForCustomYtElementsReady = new Promise(onRegistryReady);
  229.  
  230. cleanContext(window).then(__CONTEXT__ => {
  231. if (!__CONTEXT__) return null;
  232.  
  233. const { requestAnimationFrame, setTimeout, cancelAnimationFrame, setInterval, clearInterval, animate, requestIdleCallback } = __CONTEXT__;
  234.  
  235. const promiseForTamerTimeout = new Promise(resolve => {
  236. promiseForCustomYtElementsReady.then(() => {
  237. customElements.whenDefined('ytd-app').then(() => {
  238. setTimeout(resolve, 1200);
  239. });
  240. });
  241. });
  242.  
  243.  
  244. class RAFHub {
  245. constructor() {
  246. /** @type {number} */
  247. this.startAt = 8170;
  248. /** @type {number} */
  249. this.counter = 0;
  250. /** @type {number} */
  251. this.rid = 0;
  252. /** @type {Map<number, FrameRequestCallback>} */
  253. this.funcs = new Map();
  254. const funcs = this.funcs;
  255. /** @type {FrameRequestCallback} */
  256. this.bCallback = this.mCallback.bind(this);
  257. this.pClear = () => funcs.clear();
  258. }
  259. /** @param {DOMHighResTimeStamp} highResTime */
  260. mCallback(highResTime) {
  261. this.rid = 0;
  262. Promise.resolve().then(this.pClear);
  263. this.funcs.forEach(func => Promise.resolve(highResTime).then(func).catch(console.warn));
  264. }
  265. /** @param {FrameRequestCallback} f */
  266. request(f) {
  267. if (this.counter > 1e9) this.counter = 9;
  268. let cid = this.startAt + (++this.counter);
  269. this.funcs.set(cid, f);
  270. if (this.rid === 0) this.rid = requestAnimationFrame(this.bCallback);
  271. return cid;
  272. }
  273. /** @param {number} cid */
  274. cancel(cid) {
  275. cid = +cid;
  276. if (cid > 0) {
  277. if (cid <= this.startAt) {
  278. return cancelAnimationFrame(cid);
  279. }
  280. if (this.rid > 0) {
  281. this.funcs.delete(cid);
  282. if (this.funcs.size === 0) {
  283. cancelAnimationFrame(this.rid);
  284. this.rid = 0;
  285. }
  286. }
  287. }
  288. }
  289. }
  290.  
  291.  
  292.  
  293. NATIVE_CANVAS_ANIMATION && (() => {
  294.  
  295. HTMLCanvasElement.prototype.animate = animate;
  296.  
  297. let cid = setInterval(() => {
  298. HTMLCanvasElement.prototype.animate = animate;
  299. }, 1);
  300.  
  301. promiseForTamerTimeout.then(() => {
  302. clearInterval(cid)
  303. });
  304.  
  305. })();
  306.  
  307.  
  308. FIX_schedulerInstanceInstance_ && (async () => {
  309.  
  310.  
  311. const schedulerInstanceInstance_ = await new Promise(resolve => {
  312.  
  313. let cid = setInterval(() => {
  314. let t = (((window || 0).ytglobal || 0).schedulerInstanceInstance_ || 0);
  315. if (t) {
  316.  
  317. clearInterval(cid);
  318. resolve(t);
  319. }
  320. }, 1);
  321. promiseForTamerTimeout.then(() => {
  322. resolve(null)
  323. });
  324. });
  325.  
  326. if (!schedulerInstanceInstance_) return;
  327.  
  328.  
  329. if (!ytEvented) {
  330. idleFrom = Date.now() + 2700;
  331. slowMode = false; // integrity
  332. }
  333.  
  334. const checkOK = typeof schedulerInstanceInstance_.start === 'function' && !schedulerInstanceInstance_.start991 && !schedulerInstanceInstance_.stop && !schedulerInstanceInstance_.cancel && !schedulerInstanceInstance_.terminate && !schedulerInstanceInstance_.interupt;
  335. if (checkOK) {
  336.  
  337. schedulerInstanceInstance_.start991 = schedulerInstanceInstance_.start;
  338.  
  339. let requestingFn = null;
  340. let requestingArgs = null;
  341. let requestingDT = 0;
  342.  
  343. let timerId = null;
  344. const entries = [];
  345. const f = function () {
  346. requestingFn = this.fn;
  347. requestingArgs = [...arguments];
  348. requestingDT = Date.now();
  349. entries.push({
  350. fn: requestingFn,
  351. args: requestingArgs,
  352. t: requestingDT
  353. });
  354. // if (Date.now() < idleFrom) {
  355. // timerId = this.fn.apply(window, arguments);
  356. // } else {
  357. // timerId = this.fn.apply(window, arguments);
  358.  
  359. // }
  360. // timerId = 12377;
  361. return 12377;
  362. }
  363.  
  364.  
  365. const fakeFns = [
  366. f.bind({ fn: requestAnimationFrame }),
  367. f.bind({ fn: setInterval }),
  368. f.bind({ fn: setTimeout }),
  369. f.bind({ fn: requestIdleCallback })
  370. ]
  371.  
  372.  
  373.  
  374.  
  375. let timerResolve = null;
  376. setInterval(() => {
  377. timerResolve && timerResolve();
  378. timerResolve = null;
  379. if (!slowMode && Date.now() > idleFrom) slowMode = true;
  380. }, 250);
  381.  
  382. let mzt = 0;
  383.  
  384. let fnSelectorProp = null;
  385.  
  386. schedulerInstanceInstance_.start = function () {
  387.  
  388. const mk1 = window.requestAnimationFrame
  389. const mk2 = window.setInterval
  390. const mk3 = window.setTimeout
  391. const mk4 = window.requestIdleCallback
  392.  
  393. const tThis = this['$$12378$$'] || this;
  394.  
  395.  
  396. window.requestAnimationFrame = fakeFns[0]
  397. window.setInterval = fakeFns[1]
  398. window.setTimeout = fakeFns[2]
  399. window.requestIdleCallback = fakeFns[3]
  400.  
  401. fnSelectorProp = null;
  402.  
  403.  
  404. tThis.start991.call(new Proxy(tThis, {
  405. get(target, prop, receiver) {
  406. if (prop === '$$12377$$') return true;
  407. if (prop === '$$12378$$') return target;
  408.  
  409. // console.log('get',prop)
  410. return target[prop]
  411. },
  412. set(target, prop, value, receiver) {
  413. // console.log('set', prop, value)
  414.  
  415.  
  416. if (value >= 1 && value <= 4) fnSelectorProp = prop;
  417. if (value === 12377 && fnSelectorProp) {
  418.  
  419. const originalSelection = target[fnSelectorProp];
  420. const timerIdProp = prop;
  421.  
  422. /*
  423.  
  424.  
  425. case 1:
  426. var a = this.K;
  427. this.g = this.I ? window.requestIdleCallback(a, {
  428. timeout: 3E3
  429. }) : window.setTimeout(a, ma);
  430. break;
  431. case 2:
  432. this.g = window.setTimeout(this.M, this.N);
  433. break;
  434. case 3:
  435. this.g = window.requestAnimationFrame(this.L);
  436. break;
  437. case 4:
  438. this.g = window.setTimeout(this.J, 0)
  439. }
  440.  
  441. */
  442.  
  443. const doForegroundSlowMode = () => {
  444.  
  445. const tir = ++mzt;
  446. const f = requestingArgs[0];
  447.  
  448.  
  449. getForegroundPromise().then(() => {
  450.  
  451.  
  452. new Promise(r => {
  453. timerResolve = r
  454. }).then(() => {
  455. if (target[timerIdProp] === -tir) f();
  456. });
  457.  
  458. })
  459.  
  460. target[fnSelectorProp] = 931;
  461. target[prop] = -tir;
  462. }
  463.  
  464. if (target[fnSelectorProp] === 2 && requestingFn === setTimeout) {
  465. if (slowMode && !(requestingArgs[1] > 250)) {
  466.  
  467. doForegroundSlowMode();
  468.  
  469. } else {
  470. target[prop] = setTimeout.apply(window, requestingArgs);
  471.  
  472. }
  473.  
  474. } else if (target[fnSelectorProp] === 3 && requestingFn === requestAnimationFrame) {
  475.  
  476. if (slowMode) {
  477.  
  478. doForegroundSlowMode();
  479.  
  480. } else {
  481. target[prop] = requestAnimationFrame.apply(window, requestingArgs);
  482. }
  483.  
  484.  
  485. } else if (target[fnSelectorProp] === 4 && requestingFn === setTimeout && !requestingArgs[1]) {
  486.  
  487. const f = requestingArgs[0];
  488. const tir = ++mzt;
  489. Promise.resolve().then(() => {
  490. if (target[timerIdProp] === -tir) f();
  491. });
  492. target[fnSelectorProp] = 930;
  493. target[prop] = -tir;
  494.  
  495. } else if (target[fnSelectorProp] === 1 && (requestingFn === requestIdleCallback || requestingFn === setTimeout)) {
  496.  
  497. doForegroundSlowMode();
  498.  
  499. } else {
  500. // target[prop] = timerId;
  501. target[fnSelectorProp] = 0;
  502. target[prop] = 0;
  503. }
  504.  
  505. // *****
  506. // console.log('[[set]]', slowMode , prop, value, `fnSelectorProp: ${originalSelection} -> ${target[fnSelectorProp]}`)
  507. } else {
  508.  
  509. target[prop] = value;
  510. }
  511. // console.log('set',prop,value)
  512. return true;
  513. }
  514. }));
  515.  
  516. fnSelectorProp = null;
  517.  
  518.  
  519. window.requestAnimationFrame = mk1;
  520. window.setInterval = mk2
  521. window.setTimeout = mk3
  522. window.requestIdleCallback = mk4;
  523.  
  524.  
  525.  
  526. }
  527.  
  528. schedulerInstanceInstance_.start.toString = function () {
  529. return schedulerInstanceInstance_.start991.toString();
  530. }
  531.  
  532. // const funcNames = [...(schedulerInstanceInstance_.start + "").matchAll(/[\(,]this\.(\w{1,2})[,\)]/g)].map(e => e[1]).map(prop => ({
  533. // prop,
  534. // value: schedulerInstanceInstance_[prop],
  535. // type: typeof schedulerInstanceInstance_[prop]
  536.  
  537. // }));
  538. // console.log('fcc', funcNames)
  539.  
  540.  
  541.  
  542.  
  543. }
  544. })();
  545.  
  546.  
  547. FIX_yt_player && (async () => {
  548.  
  549.  
  550.  
  551. const rafHub = new RAFHub();
  552.  
  553.  
  554. const _yt_player = await new Promise(resolve => {
  555.  
  556. let cid = setInterval(() => {
  557. let t = (((window || 0)._yt_player || 0) || 0);
  558. if (t) {
  559.  
  560. clearInterval(cid);
  561. resolve(t);
  562. }
  563. }, 1);
  564.  
  565. promiseForTamerTimeout.then(() => {
  566. resolve(null)
  567. });
  568.  
  569. });
  570.  
  571.  
  572.  
  573. if (!_yt_player || typeof _yt_player !== 'object') return;
  574.  
  575.  
  576.  
  577. let keyZq = getZq(_yt_player);
  578. // _yt_player[keyZq] = g.k
  579.  
  580. if (!keyZq) return;
  581.  
  582.  
  583. const g = _yt_player
  584. let k = keyZq
  585.  
  586. const gk = g[k];
  587. if (typeof gk !== 'function') return;
  588.  
  589. let dummyObject = new gk;
  590. let nilFunc = () => { };
  591.  
  592. let nilObj = {};
  593.  
  594. // console.log(1111111111)
  595.  
  596. let keyBoolD = '';
  597. let keyWindow = '';
  598. let keyFuncC = '';
  599. let keyCidj = '';
  600.  
  601. for (const [t, y] of Object.entries(dummyObject)) {
  602. if (y instanceof Window) keyWindow = t;
  603. }
  604.  
  605. const dummyObjectProxyHandler = {
  606. get(target, prop) {
  607. let v = target[prop]
  608. if (v instanceof Window && !keyWindow) {
  609. keyWindow = t;
  610. }
  611. let y = typeof v === 'function' ? nilFunc : typeof v === 'object' ? nilObj : v;
  612. if (prop === keyWindow) y = {
  613. requestAnimationFrame(f) {
  614. return 3;
  615. },
  616. cancelAnimationFrame() {
  617.  
  618. }
  619. }
  620. if (!keyFuncC && typeof v === 'function' && !(prop in target.constructor.prototype)) {
  621. keyFuncC = prop;
  622. }
  623. // console.log('[get]', prop, typeof target[prop])
  624.  
  625.  
  626. return y;
  627. },
  628. set(target, prop, value) {
  629.  
  630. if (typeof value === 'boolean' && !keyBoolD) {
  631. keyBoolD = prop;
  632. }
  633. if (typeof value === 'number' && !keyCidj && value >= 2) {
  634. keyCidj = prop;
  635. }
  636.  
  637. // console.log('[set]', prop, value)
  638. target[prop] = value
  639.  
  640. return true;
  641. }
  642. };
  643.  
  644. dummyObject.start.call(new Proxy(dummyObject, dummyObjectProxyHandler))
  645.  
  646. /*
  647. console.log({
  648. keyBoolD,
  649. keyFuncC,
  650. keyWindow,
  651. keyCidj
  652. })
  653.  
  654. console.log( dummyObject[keyFuncC])
  655.  
  656.  
  657. console.log(2222222222)
  658. */
  659.  
  660.  
  661.  
  662.  
  663. g[k].prototype.start = function () {
  664. this.stop();
  665. this[keyBoolD] = true;
  666. this[keyCidj] = rafHub.request(this[keyFuncC]);
  667. }
  668. ;
  669. g[k].prototype.stop = function () {
  670. if (this.isActive() && this[keyCidj]) {
  671. rafHub.cancel(this[keyCidj]);
  672. }
  673. this[keyCidj] = null
  674. }
  675.  
  676.  
  677. /*
  678. g[k].start = function() {
  679. this.stop();
  680. this.D = true;
  681. var a = requestAnimationFrame
  682. , b = cancelAnimationFrame;
  683. this.j = a.call(this.B, this.C)
  684. }
  685. ;
  686. g[k].stop = function() {
  687. if (this.isActive()) {
  688. var a = requestAnimationFrame
  689. , b = cancelAnimationFrame;
  690. b.call(this.B, this.j)
  691. }
  692. this.j = null
  693. }
  694. */
  695.  
  696.  
  697.  
  698. })();
  699.  
  700.  
  701.  
  702.  
  703.  
  704.  
  705.  
  706. });
  707.  
  708.  
  709. setupEvents();
  710. if (isMainWindow) {
  711.  
  712. console.groupCollapsed(
  713. "%cYouTube JS Engine Tamer",
  714. "background-color: #EDE43B ; color: #000 ; font-weight: bold ; padding: 4px ;"
  715. );
  716.  
  717. console.log("Script is loaded.");
  718. console.log("This script changes the core mechanisms of the YouTube JS engine.");
  719.  
  720. console.log("This script is experimental and subject to further changes.");
  721.  
  722. console.log("This might boost your YouTube performance.");
  723.  
  724. console.log("CAUTION: This might break your YouTube.");
  725. console.groupEnd();
  726.  
  727. }
  728.  
  729.  
  730. })();