YouTube JS Engine Tamer

To enhance YouTube performance by modifying YouTube JS Engine

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

  1. // ==UserScript==
  2. // @name YouTube JS Engine Tamer
  3. // @namespace UserScripts
  4. // @match https://www.youtube.com/*
  5. // @version 0.2.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 = fc.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. setTimeout(resolve, 3000);
  242. });
  243.  
  244.  
  245. class RAFHub {
  246. constructor() {
  247. /** @type {number} */
  248. this.startAt = 8170;
  249. /** @type {number} */
  250. this.counter = 0;
  251. /** @type {number} */
  252. this.rid = 0;
  253. /** @type {Map<number, FrameRequestCallback>} */
  254. this.funcs = new Map();
  255. const funcs = this.funcs;
  256. /** @type {FrameRequestCallback} */
  257. this.bCallback = this.mCallback.bind(this);
  258. this.pClear = () => funcs.clear();
  259. }
  260. /** @param {DOMHighResTimeStamp} highResTime */
  261. mCallback(highResTime) {
  262. this.rid = 0;
  263. Promise.resolve().then(this.pClear);
  264. this.funcs.forEach(func => Promise.resolve(highResTime).then(func).catch(console.warn));
  265. }
  266. /** @param {FrameRequestCallback} f */
  267. request(f) {
  268. if (this.counter > 1e9) this.counter = 9;
  269. let cid = this.startAt + (++this.counter);
  270. this.funcs.set(cid, f);
  271. if (this.rid === 0) this.rid = requestAnimationFrame(this.bCallback);
  272. return cid;
  273. }
  274. /** @param {number} cid */
  275. cancel(cid) {
  276. cid = +cid;
  277. if (cid > 0) {
  278. if (cid <= this.startAt) {
  279. return cancelAnimationFrame(cid);
  280. }
  281. if (this.rid > 0) {
  282. this.funcs.delete(cid);
  283. if (this.funcs.size === 0) {
  284. cancelAnimationFrame(this.rid);
  285. this.rid = 0;
  286. }
  287. }
  288. }
  289. }
  290. }
  291.  
  292.  
  293.  
  294. NATIVE_CANVAS_ANIMATION && (() => {
  295.  
  296. HTMLCanvasElement.prototype.animate = animate;
  297.  
  298. let cid = setInterval(() => {
  299. HTMLCanvasElement.prototype.animate = animate;
  300. }, 1);
  301.  
  302. promiseForTamerTimeout.then(() => {
  303. clearInterval(cid)
  304. });
  305.  
  306. })();
  307.  
  308.  
  309. FIX_schedulerInstanceInstance_ && (async () => {
  310.  
  311.  
  312. const schedulerInstanceInstance_ = await new Promise(resolve => {
  313.  
  314. let cid = setInterval(() => {
  315. let t = (((window || 0).ytglobal || 0).schedulerInstanceInstance_ || 0);
  316. if (t) {
  317.  
  318. clearInterval(cid);
  319. resolve(t);
  320. }
  321. }, 1);
  322. promiseForTamerTimeout.then(() => {
  323. resolve(null)
  324. });
  325. });
  326.  
  327. if (!schedulerInstanceInstance_) return;
  328.  
  329.  
  330. if (!ytEvented) {
  331. idleFrom = Date.now() + 2700;
  332. slowMode = false; // integrity
  333. }
  334.  
  335. const checkOK = typeof schedulerInstanceInstance_.start === 'function' && !schedulerInstanceInstance_.start991 && !schedulerInstanceInstance_.stop && !schedulerInstanceInstance_.cancel && !schedulerInstanceInstance_.terminate && !schedulerInstanceInstance_.interupt;
  336. if (checkOK) {
  337.  
  338. schedulerInstanceInstance_.start991 = schedulerInstanceInstance_.start;
  339.  
  340. let requestingFn = null;
  341. let requestingArgs = null;
  342. let requestingDT = 0;
  343.  
  344. let timerId = null;
  345. const entries = [];
  346. const f = function () {
  347. requestingFn = this.fn;
  348. requestingArgs = [...arguments];
  349. requestingDT = Date.now();
  350. entries.push({
  351. fn: requestingFn,
  352. args: requestingArgs,
  353. t: requestingDT
  354. });
  355. // if (Date.now() < idleFrom) {
  356. // timerId = this.fn.apply(window, arguments);
  357. // } else {
  358. // timerId = this.fn.apply(window, arguments);
  359.  
  360. // }
  361. // timerId = 12377;
  362. return 12377;
  363. }
  364.  
  365.  
  366. const fakeFns = [
  367. f.bind({ fn: requestAnimationFrame }),
  368. f.bind({ fn: setInterval }),
  369. f.bind({ fn: setTimeout }),
  370. f.bind({ fn: requestIdleCallback })
  371. ]
  372.  
  373.  
  374.  
  375.  
  376. let timerResolve = null;
  377. setInterval(() => {
  378. timerResolve && timerResolve();
  379. timerResolve = null;
  380. if (!slowMode && Date.now() > idleFrom) slowMode = true;
  381. }, 250);
  382.  
  383. let mzt = 0;
  384.  
  385. let fnSelectorProp = null;
  386.  
  387. schedulerInstanceInstance_.start = function () {
  388.  
  389. const mk1 = window.requestAnimationFrame
  390. const mk2 = window.setInterval
  391. const mk3 = window.setTimeout
  392. const mk4 = window.requestIdleCallback
  393.  
  394. const tThis = this['$$12378$$'] || this;
  395.  
  396.  
  397. window.requestAnimationFrame = fakeFns[0]
  398. window.setInterval = fakeFns[1]
  399. window.setTimeout = fakeFns[2]
  400. window.requestIdleCallback = fakeFns[3]
  401.  
  402. fnSelectorProp = null;
  403.  
  404.  
  405. tThis.start991.call(new Proxy(tThis, {
  406. get(target, prop, receiver) {
  407. if (prop === '$$12377$$') return true;
  408. if (prop === '$$12378$$') return target;
  409.  
  410. // console.log('get',prop)
  411. return target[prop]
  412. },
  413. set(target, prop, value, receiver) {
  414. // console.log('set', prop, value)
  415.  
  416.  
  417. if (value >= 1 && value <= 4) fnSelectorProp = prop;
  418. if (value === 12377 && fnSelectorProp) {
  419.  
  420. const originalSelection = target[fnSelectorProp];
  421. const timerIdProp = prop;
  422.  
  423. /*
  424.  
  425.  
  426. case 1:
  427. var a = this.K;
  428. this.g = this.I ? window.requestIdleCallback(a, {
  429. timeout: 3E3
  430. }) : window.setTimeout(a, ma);
  431. break;
  432. case 2:
  433. this.g = window.setTimeout(this.M, this.N);
  434. break;
  435. case 3:
  436. this.g = window.requestAnimationFrame(this.L);
  437. break;
  438. case 4:
  439. this.g = window.setTimeout(this.J, 0)
  440. }
  441.  
  442. */
  443.  
  444. const doForegroundSlowMode = () => {
  445.  
  446. const tir = ++mzt;
  447. const f = requestingArgs[0];
  448.  
  449.  
  450. getForegroundPromise().then(() => {
  451.  
  452.  
  453. new Promise(r => {
  454. timerResolve = r
  455. }).then(() => {
  456. if (target[timerIdProp] === -tir) f();
  457. });
  458.  
  459. })
  460.  
  461. target[fnSelectorProp] = 931;
  462. target[prop] = -tir;
  463. }
  464.  
  465. if (target[fnSelectorProp] === 2 && requestingFn === setTimeout) {
  466. if (slowMode && !(requestingArgs[1] > 250)) {
  467.  
  468. doForegroundSlowMode();
  469.  
  470. } else {
  471. target[prop] = setTimeout.apply(window, requestingArgs);
  472.  
  473. }
  474.  
  475. } else if (target[fnSelectorProp] === 3 && requestingFn === requestAnimationFrame) {
  476.  
  477. if (slowMode) {
  478.  
  479. doForegroundSlowMode();
  480.  
  481. } else {
  482. target[prop] = requestAnimationFrame.apply(window, requestingArgs);
  483. }
  484.  
  485.  
  486. } else if (target[fnSelectorProp] === 4 && requestingFn === setTimeout && !requestingArgs[1]) {
  487.  
  488. const f = requestingArgs[0];
  489. const tir = ++mzt;
  490. Promise.resolve().then(() => {
  491. if (target[timerIdProp] === -tir) f();
  492. });
  493. target[fnSelectorProp] = 930;
  494. target[prop] = -tir;
  495.  
  496. } else if (target[fnSelectorProp] === 1 && (requestingFn === requestIdleCallback || requestingFn === setTimeout)) {
  497.  
  498. doForegroundSlowMode();
  499.  
  500. } else {
  501. // target[prop] = timerId;
  502. target[fnSelectorProp] = 0;
  503. target[prop] = 0;
  504. }
  505.  
  506. // *****
  507. // console.log('[[set]]', slowMode , prop, value, `fnSelectorProp: ${originalSelection} -> ${target[fnSelectorProp]}`)
  508. } else {
  509.  
  510. target[prop] = value;
  511. }
  512. // console.log('set',prop,value)
  513. return true;
  514. }
  515. }));
  516.  
  517. fnSelectorProp = null;
  518.  
  519.  
  520. window.requestAnimationFrame = mk1;
  521. window.setInterval = mk2
  522. window.setTimeout = mk3
  523. window.requestIdleCallback = mk4;
  524.  
  525.  
  526.  
  527. }
  528.  
  529. schedulerInstanceInstance_.start.toString = function () {
  530. return schedulerInstanceInstance_.start991.toString();
  531. }
  532.  
  533. // const funcNames = [...(schedulerInstanceInstance_.start + "").matchAll(/[\(,]this\.(\w{1,2})[,\)]/g)].map(e => e[1]).map(prop => ({
  534. // prop,
  535. // value: schedulerInstanceInstance_[prop],
  536. // type: typeof schedulerInstanceInstance_[prop]
  537.  
  538. // }));
  539. // console.log('fcc', funcNames)
  540.  
  541.  
  542.  
  543.  
  544. }
  545. })();
  546.  
  547.  
  548. FIX_yt_player && (async () => {
  549.  
  550.  
  551.  
  552. const rafHub = new RAFHub();
  553.  
  554.  
  555. const _yt_player = await new Promise(resolve => {
  556.  
  557. let cid = setInterval(() => {
  558. let t = (((window || 0)._yt_player || 0) || 0);
  559. if (t) {
  560.  
  561. clearInterval(cid);
  562. resolve(t);
  563. }
  564. }, 1);
  565.  
  566. promiseForTamerTimeout.then(() => {
  567. resolve(null)
  568. });
  569.  
  570. });
  571.  
  572.  
  573.  
  574. if (!_yt_player || typeof _yt_player !== 'object') return;
  575.  
  576.  
  577.  
  578. let keyZq = getZq(_yt_player);
  579. // _yt_player[keyZq] = g.k
  580.  
  581. if (!keyZq) return;
  582.  
  583.  
  584. const g = _yt_player
  585. let k = keyZq
  586.  
  587. const gk = g[k];
  588. if (typeof gk !== 'function') return;
  589.  
  590. let dummyObject = new gk;
  591. let nilFunc = () => { };
  592.  
  593. let nilObj = {};
  594.  
  595. // console.log(1111111111)
  596.  
  597. let keyBoolD = '';
  598. let keyWindow = '';
  599. let keyFuncC = '';
  600. let keyCidj = '';
  601.  
  602. for (const [t, y] of Object.entries(dummyObject)) {
  603. if (y instanceof Window) keyWindow = t;
  604. }
  605.  
  606. const dummyObjectProxyHandler = {
  607. get(target, prop) {
  608. let v = target[prop]
  609. if (v instanceof Window && !keyWindow) {
  610. keyWindow = t;
  611. }
  612. let y = typeof v === 'function' ? nilFunc : typeof v === 'object' ? nilObj : v;
  613. if (prop === keyWindow) y = {
  614. requestAnimationFrame(f) {
  615. return 3;
  616. },
  617. cancelAnimationFrame() {
  618.  
  619. }
  620. }
  621. if (!keyFuncC && typeof v === 'function' && !(prop in target.constructor.prototype)) {
  622. keyFuncC = prop;
  623. }
  624. // console.log('[get]', prop, typeof target[prop])
  625.  
  626.  
  627. return y;
  628. },
  629. set(target, prop, value) {
  630.  
  631. if (typeof value === 'boolean' && !keyBoolD) {
  632. keyBoolD = prop;
  633. }
  634. if (typeof value === 'number' && !keyCidj && value >= 2) {
  635. keyCidj = prop;
  636. }
  637.  
  638. // console.log('[set]', prop, value)
  639. target[prop] = value
  640.  
  641. return true;
  642. }
  643. };
  644.  
  645. dummyObject.start.call(new Proxy(dummyObject, dummyObjectProxyHandler))
  646.  
  647. /*
  648. console.log({
  649. keyBoolD,
  650. keyFuncC,
  651. keyWindow,
  652. keyCidj
  653. })
  654.  
  655. console.log( dummyObject[keyFuncC])
  656.  
  657.  
  658. console.log(2222222222)
  659. */
  660.  
  661.  
  662.  
  663.  
  664. g[k].prototype.start = function () {
  665. this.stop();
  666. this[keyBoolD] = true;
  667. this[keyCidj] = rafHub.request(this[keyFuncC]);
  668. }
  669. ;
  670. g[k].prototype.stop = function () {
  671. if (this.isActive() && this[keyCidj]) {
  672. rafHub.cancel(this[keyCidj]);
  673. }
  674. this[keyCidj] = null
  675. }
  676.  
  677.  
  678. /*
  679. g[k].start = function() {
  680. this.stop();
  681. this.D = true;
  682. var a = requestAnimationFrame
  683. , b = cancelAnimationFrame;
  684. this.j = a.call(this.B, this.C)
  685. }
  686. ;
  687. g[k].stop = function() {
  688. if (this.isActive()) {
  689. var a = requestAnimationFrame
  690. , b = cancelAnimationFrame;
  691. b.call(this.B, this.j)
  692. }
  693. this.j = null
  694. }
  695. */
  696.  
  697.  
  698.  
  699. })();
  700.  
  701.  
  702.  
  703.  
  704.  
  705.  
  706.  
  707. });
  708.  
  709.  
  710. setupEvents();
  711. if (isMainWindow) {
  712.  
  713. console.groupCollapsed(
  714. "%cYouTube JS Engine Tamer",
  715. "background-color: #EDE43B ; color: #000 ; font-weight: bold ; padding: 4px ;"
  716. );
  717.  
  718. console.log("Script is loaded.");
  719. console.log("This script changes the core mechanisms of the YouTube JS engine.");
  720.  
  721. console.log("This script is experimental and subject to further changes.");
  722.  
  723. console.log("This might boost your YouTube performance.");
  724.  
  725. console.log("CAUTION: This might break your YouTube.");
  726. console.groupEnd();
  727.  
  728. }
  729.  
  730.  
  731. })();