Greasy Fork 还支持 简体中文。

YouTube Web Tweaks

This script optimizes YouTube's performance by modified configs, shorts redirect and much more!

  1. // ==UserScript==
  2. // @name YouTube Web Tweaks
  3. // @version 4.2.0
  4. // @description This script optimizes YouTube's performance by modified configs, shorts redirect and much more!
  5. // @author Magma_Craft
  6. // @license MIT
  7. // @match *://www.youtube.com/*
  8. // @namespace https://greasyfork.org/en/users/933798
  9. // @icon https://www.youtube.com/favicon.ico
  10. // @unwrap
  11. // @run-at document-end
  12. // @unwrap
  13. // @grant none
  14. // @require https://cdnjs.cloudflare.com/ajax/libs/arrive/2.4.1/arrive.min.js
  15. // ==/UserScript==
  16.  
  17. // Enable strict mode to catch common coding mistakes
  18. "use strict";
  19.  
  20. // Define the flags to assign to the EXPERIMENT_FLAGS object
  21. const flagsToAssign = {
  22. // Standard tweaks (YT config editor + Disable animations)
  23. IS_TABLET: true,
  24. DISABLE_YT_IMG_DELAY_LOADING: true,
  25. polymer_verifiy_app_state: false,
  26. desktop_delay_player_resizing: false,
  27. web_animated_actions: false,
  28. web_animated_like: false,
  29. web_animated_like_lazy_load: false,
  30. render_unicode_emojis_as_small_images: true,
  31. smartimation_background: false,
  32. kevlar_refresh_on_theme_change: false,
  33. // Disable cinematics (aka ambient lighting)
  34. kevlar_measure_ambient_mode_idle: false,
  35. kevlar_watch_cinematics_invisible: false,
  36. web_cinematic_theater_mode: false,
  37. web_cinematic_fullscreen: false,
  38. enable_cinematic_blur_desktop_loading: false,
  39. kevlar_watch_cinematics: false,
  40. web_cinematic_masthead: false,
  41. web_watch_cinematics_preferred_reduced_motion_default_disabled: false
  42. };
  43.  
  44. const updateFlags = () => {
  45. // Check if the EXPERIMENT_FLAGS object exists in the window.yt.config_ property chain
  46. const expFlags = window?.yt?.config_?.EXPERIMENT_FLAGS;
  47.  
  48. // If EXPERIMENT_FLAGS is not found, exit the function
  49. if (!expFlags) return;
  50.  
  51. // Assign the defined flags to the EXPERIMENT_FLAGS object
  52. Object.assign(expFlags, flagsToAssign);
  53. };
  54.  
  55. // Create a MutationObserver that calls the updateFlags function when changes occur in the document's subtree
  56. const mutationObserver = new MutationObserver(updateFlags);
  57. mutationObserver.observe(document, { subtree: true, childList: true });
  58.  
  59. // Fully replace shorts links with regular videos
  60. /**
  61. * Shorts URL redirect.
  62. *
  63. * This is called on initial visit only. Successive navigations
  64. * are managed by modifying the YouTube Desktop application.
  65. */
  66. (function(){
  67. /** @type {string} */
  68. var path = window.location.pathname;
  69. if (0 == path.search("/shorts"))
  70. {
  71. // Extract the video ID from the shorts link and redirect.
  72. /** @type {string} */
  73. var id = path.replace(/\/|shorts|\?.*/g, "");
  74. window.location.replace("https://www.youtube.com/watch?v=" + id);
  75. }
  76. })();
  77. /**
  78. * YouTube Desktop Shorts remover.
  79. *
  80. * If the initial URL was not a shorts link, traditional redirection
  81. * will not work. This instead modifies video elements to replace them with
  82. * regular links.
  83. */
  84. (function(){
  85. /**
  86. * @param {string} selector (CSS-style) of the element
  87. * @return {Promise<Element>}
  88. */
  89. async function querySelectorAsync(selector)
  90. {
  91. while (null == document.querySelector(selector))
  92. {
  93. // Pause for a frame and let other code go on.
  94. await new Promise(r => requestAnimationFrame(r));
  95. }
  96. return document.querySelector(selector);
  97. }
  98. /**
  99. * Small toolset for interacting with the Polymer
  100. * YouTube Desktop application.
  101. *
  102. * @author Taniko Yamamoto <kirasicecreamm@gmail.com>
  103. * @version 1.0
  104. */
  105. class YtdTools
  106. {
  107. /** @type {string} Page data updated event */
  108. static EVT_DATA_UPDATE = "yt-page-data-updated";
  109. /** @type {Element} Main YT Polymer manager */
  110. static YtdApp;
  111. /** @type {bool} */
  112. static hasInitialLoaded = false;
  113. /** @return {Promise<bool>} */
  114. static async isPolymer()
  115. {
  116. /** @return {Promise<void>} */
  117. function waitForBody() // nice hack lazy ass
  118. {
  119. return new Promise(r => {
  120. document.addEventListener("DOMContentLoaded", function a(){
  121. document.removeEventListener("DOMContentLoaded", a);
  122. r();
  123. });
  124. });
  125. }
  126. await waitForBody();
  127. if ("undefined" != typeof document.querySelector("ytd-app"))
  128. {
  129. this.YtdApp = document.querySelector("ytd-app");
  130. return true;
  131. }
  132. return false;
  133. }
  134. /** @async @return {Promise<void|string>} */
  135. static waitForInitialLoad()
  136. {
  137. var updateEvent = this.EVT_DATA_UPDATE;
  138. return new Promise((resolve, reject) => {
  139. if (!this.isPolymer())
  140. {
  141. reject("Not Polymer :(");
  142. }
  143. function _listenerCb()
  144. {
  145. document.removeEventListener(updateEvent, _listenerCb);
  146. resolve();
  147. }
  148. document.addEventListener(updateEvent, _listenerCb);
  149. });
  150. }
  151. /** @return {string} */
  152. static getPageType()
  153. {
  154. return this.YtdApp.data.page;
  155. }
  156. }
  157. class ShortsTools
  158. {
  159. /** @type {MutationObserver} */
  160. static mo = new MutationObserver(muts => {
  161. muts.forEach(mut => {
  162. Array.from(mut.addedNodes).forEach(node => {
  163. if (node instanceof HTMLElement) {
  164. this.onMutation(node);
  165. }
  166. });
  167. });
  168. });
  169. /** @return {void} */
  170. static watchForShorts()
  171. {
  172. /*
  173. this.mo.observe(YtdTools.YtdApp, {
  174. childList: true,
  175. subtree: true
  176. });
  177. */
  178. var me = this;
  179. YtdTools.YtdApp.arrive("ytd-video-renderer, ytd-grid-video-renderer", function() {
  180. me.onMutation(this);
  181. // This is literally the worst hack I ever wrote, but it works ig...
  182. (new MutationObserver(function(){
  183. if (me.isShortsRenderer(this))
  184. {
  185. me.onMutation(this);
  186. }
  187. }.bind(this))).observe(this, {"subtree": true, "childList": true, "characterData": "true"});
  188. });
  189. }
  190. /** @return {void} */
  191. static stopWatchingForShorts()
  192. {
  193. this.mo.disconnect();
  194. }
  195. /**
  196. * @param {HTMLElement} node
  197. * @return {void}
  198. */
  199. static onMutation(node)
  200. {
  201. if (node.tagName.search("VIDEO-RENDERER") > -1 && this.isShortsRenderer(node))
  202. {
  203. this.transformShortsRenderer(node);
  204. }
  205. }
  206. /** @return {bool} */
  207. static isShortsRenderer(videoRenderer)
  208. {
  209. return "WEB_PAGE_TYPE_SHORTS" == videoRenderer?.data?.navigationEndpoint?.commandMetadata?.webCommandMetadata?.webPageType;
  210. }
  211. /** @return {string} */
  212. static extractLengthFromA11y(videoData)
  213. {
  214. // A11y = {title} by {creator} {date} {*length*} {viewCount} - play Short
  215. // tho hopefully this works in more than just English
  216. var a11yTitle = videoData.title.accessibility.accessibilityData.label;
  217. var publishedTimeText = videoData.publishedTimeText.simpleText;
  218. var viewCountText = videoData.viewCountText.simpleText;
  219. var isolatedLengthStr = a11yTitle.split(publishedTimeText)[1].split(viewCountText)[0]
  220. .replace(/\s/g, "");
  221. var numbers = isolatedLengthStr.split(/\D/g);
  222. var string = "";
  223. // Remove all empties before iterating it
  224. for (var i = 0; i < numbers.length; i++)
  225. {
  226. if ("" === numbers[i])
  227. {
  228. numbers.splice(i, 1);
  229. i--;
  230. }
  231. }
  232. for (var i = 0; i < numbers.length; i++)
  233. {
  234. // Lazy 0 handling idc im tired
  235. if (1 == numbers.length)
  236. {
  237. string += "0:";
  238. if (1 == numbers[i].length)
  239. {
  240. string += "0" + numbers[i];
  241. }
  242. else
  243. {
  244. string += numbers[i];
  245. }
  246. break;
  247. }
  248. if (0 != i) string += ":";
  249. if (0 != i && 1 == numbers[i].length) string += "0";
  250. string += numbers[i];
  251. }
  252. return string;
  253. }
  254. /**
  255. * @param {HTMLElement} videoRenderer
  256. * @return {void}
  257. */
  258. static transformShortsRenderer(videoRenderer)
  259. {
  260. /** @type {string} */
  261. var originalOuterHTML = videoRenderer.outerHTML;
  262. /** @type {string} */
  263. var lengthText = videoRenderer.data?.lengthText?.simpleText ?? this.extractLengthFromA11y(videoRenderer.data);
  264. /** @type {string} */
  265. var lengthA11y = videoRenderer.data?.lengthText?.accessibility?.accessibilityData?.label ?? "";
  266. /** @type {string} */
  267. var originalHref = videoRenderer.data.navigationEndpoint.commandMetadata.webCommandMetadata.url;
  268. var href = "/watch?v=" + originalHref.replace(/\/|shorts|\?.*/g, "");
  269. var reelWatchEndpoint = videoRenderer.data.navigationEndpoint.reelWatchEndpoint;
  270. var i;
  271. videoRenderer.data.thumbnailOverlays.forEach((a, index) =>{
  272. if ("thumbnailOverlayTimeStatusRenderer" in a)
  273. {
  274. i = index;
  275. }
  276. });
  277. // Set the thumbnail overlay style
  278. videoRenderer.data.thumbnailOverlays[i].thumbnailOverlayTimeStatusRenderer.style = "DEFAULT";
  279. delete videoRenderer.data.thumbnailOverlays[i].thumbnailOverlayTimeStatusRenderer.icon;
  280. // Set the thumbnail overlay text
  281. videoRenderer.data.thumbnailOverlays[i].thumbnailOverlayTimeStatusRenderer.text.simpleText = lengthText;
  282. // Set the thumbnail overlay accessibility label
  283. videoRenderer.data.thumbnailOverlays[i].thumbnailOverlayTimeStatusRenderer.text.accessibility.accessibilityData.label = lengthA11y;
  284. // Set the navigation endpoint metadata (used for middle click)
  285. videoRenderer.data.navigationEndpoint.commandMetadata.webCommandMetadata.webPageType = "WEB_PAGE_TYPE_WATCH";
  286. videoRenderer.data.navigationEndpoint.commandMetadata.webCommandMetadata.url = href;
  287. videoRenderer.data.navigationEndpoint.watchEndpoint = {
  288. "videoId": reelWatchEndpoint.videoId,
  289. "playerParams": reelWatchEndpoint.playerParams,
  290. "params": reelWatchEndpoint.params
  291. };
  292. delete videoRenderer.data.navigationEndpoint.reelWatchEndpoint;
  293. //var _ = videoRenderer.data; videoRenderer.data = {}; videoRenderer.data = _;
  294. // Sometimes the old school data cycle trick fails,
  295. // however this always works.
  296. var _ = videoRenderer.cloneNode();
  297. _.data = videoRenderer.data;
  298. for (var i in videoRenderer.properties)
  299. {
  300. _[i] = videoRenderer[i];
  301. }
  302. videoRenderer.insertAdjacentElement("afterend", _);
  303. videoRenderer.remove();
  304. }
  305. }
  306. /**
  307. * Sometimes elements are reused on page updates, so fix that
  308. *
  309. * @return {void}
  310. */
  311. function onDataUpdate()
  312. {
  313. var videos = document.querySelectorAll("ytd-video-renderer, ytd-grid-video-renderer");
  314. for (var i = 0, l = videos.length; i < l; i++) if (ShortsTools.isShortsRenderer(videos[i]))
  315. {
  316. ShortsTools.transformShortsRenderer(videos[i]);
  317. }
  318. }
  319. /**
  320. * I hope she makes lotsa spaghetti :D
  321. * @async @return {Promise<void>}
  322. */
  323. async function main()
  324. {
  325. // If not Polymer, nothing happens
  326. if (await YtdTools.isPolymer())
  327. {
  328. ShortsTools.watchForShorts();
  329. document.addEventListener("yt-page-data-updated", onDataUpdate);
  330. }
  331. }
  332. main();
  333. })();
  334.  
  335. // Other tweaks to be added (CSS codes, skip ads, etc...)
  336. (function() {
  337. let css = `
  338. /* Remove 'Shorts' tab */
  339. #endpoint.yt-simple-endpoint.ytd-guide-entry-renderer.style-scope[title="Shorts"],
  340. a.yt-simple-endpoint.style-scope.ytd-mini-guide-entry-renderer[title="Shorts"] {
  341. display: none !important;
  342. }
  343.  
  344. /* Remove filter categories on search results and playlists to make the UI less usable on low-entry machines */
  345. ytd-item-section-renderer.style-scope.ytd-section-list-renderer[page-subtype="playlist"] > #header.ytd-item-section-renderer > ytd-feed-filter-chip-bar-renderer {
  346. display: none !important;
  347. }
  348.  
  349. div#chip-bar.style-scope.ytd-search-header-renderer > yt-chip-cloud-renderer.style-scope.ytd-search-header-renderer > div#container.style-scope.yt-chip-cloud-renderer {
  350. display: none !important;
  351. }
  352.  
  353. /* Remove all annoyances (excludes 'YT TV and Premium' banners) */
  354. ytd-action-companion-ad-renderer, ytd-display-ad-renderer, ytd-video-masthead-ad-advertiser-info-renderer, ytd-video-masthead-ad-primary-video-renderer, ytd-in-feed-ad-layout-renderer, ytd-ad-slot-renderer, yt-about-this-ad-renderer, yt-mealbar-promo-renderer, ytd-ad-slot-renderer, ytd-in-feed-ad-layout-renderer, .ytd-video-masthead-ad-v3-renderer, div#root.style-scope.ytd-display-ad-renderer.yt-simple-endpoint, div#sparkles-container.style-scope.ytd-promoted-sparkles-web-renderer, div#main-container.style-scope.ytd-promoted-video-renderer, #player-ads, .ytwPanelAdHeaderImageLockupViewModelHost, ytd-ads-engagement-panel-content-renderer, #content.ytd-ads-engagement-panel-content-renderer, ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"], ad-slot-renderer, ytm-promoted-sparkles-web-renderer, masthead-ad, #masthead-ad, ytd-video-quality-promo-renderer, #yt-lang-alert-container, .YtmPaidContentOverlayHost, .ytd-primetime-promo-renderer, ytd-brand-video-singleton-renderer, #yt-feedback, #yt-hitchhiker-feedback, ytd-merch-shelf-renderer, ytd-enforcement-message-view-model, div[is-shared-heimdall], tp-yt-iron-overlay-backdrop.opened, ytd-promoted-sparkles-web-renderer, ytd-text-image-no-button-layout-renderer, #cinematics.ytd-watch-flexy {
  355. display: none !important
  356. }
  357. #movie_player.ad-showing video {
  358. filter: blur(100px) opacity(0.25) grayscale(0.5);
  359. }
  360. #movie_player.ad-showing .ytp-title,
  361. #movie_player.ad-showing .ytp-title-channel,
  362. .ytp-visit-advertiser-link,
  363. .ytp-ad-visit-advertiser-button,
  364. ytmusic-app:has(#movie_player.ad-showing)
  365. ytmusic-player-bar
  366. :is(.title, .subtitle) {
  367. filter: blur(4px) opacity(0.5) grayscale(0.5);
  368. transition: 0.05s filter linear;
  369. }
  370. :is(#movie_player.ad-showing .ytp-title,#movie_player.ad-showing .ytp-title-channel,.ytp-visit-advertiser-link,.ytp-ad-visit-advertiser-button,ytmusic-app:has(#movie_player.ad-showing) ytmusic-player-bar :is(.title,.subtitle)):is(:hover,:focus-within) {
  371. filter: none;
  372. }
  373. .ytp-suggested-action-badge {
  374. visibility: hidden !important;
  375. }
  376. ytd-watch-metadata.ytd-watch-flexy {
  377. padding-bottom: 36px !important;
  378. }
  379.  
  380. /* Disable infinite scrolling (partially broken when YouTube changes their backend) */
  381. #related ytd-compact-video-renderer, #related ytd-compact-playlist-renderer, #related ytd-compact-radio-renderer, #related ytd-compact-movie-renderer, #related yt-lockup-view-model, #related ytd-channel-renderer, #related ytd-continuation-item-renderer, #related #continuations {
  382. display: none !important
  383. }
  384.  
  385. #related ytd-compact-video-renderer:nth-of-type(1), #related yt-lockup-view-model:nth-of-type(1), #related ytd-compact-video-renderer:nth-of-type(2), #related yt-lockup-view-model:nth-of-type(2), #related ytd-compact-video-renderer:nth-of-type(3), #related yt-lockup-view-model:nth-of-type(3), #related ytd-compact-video-renderer:nth-of-type(4), #related yt-lockup-view-model:nth-of-type(4), #related ytd-compact-video-renderer:nth-of-type(5), #related yt-lockup-view-model:nth-of-type(5), #related ytd-compact-video-renderer:nth-of-type(6), #related yt-lockup-view-model:nth-of-type(6), #secondary #related ytd-compact-video-renderer:nth-of-type(7), #secondary #related yt-lockup-view-model:nth-of-type(7), #secondary #related ytd-compact-video-renderer:nth-of-type(8), #secondary #related yt-lockup-view-model:nth-of-type(8), #secondary #related ytd-compact-video-renderer:nth-of-type(9), #secondary #related yt-lockup-view-model:nth-of-type(9), #secondary #related ytd-compact-video-renderer:nth-of-type(10), #secondary #related yt-lockup-view-model:nth-of-type(10) {
  386. display: flex !important
  387. }
  388.  
  389. /* More tweaks to the UI (this was meant for older browsers without uBlock Origin) */
  390. #secondary.ytd-watch-grid {
  391. width: 402px !important;
  392. min-width: 300px !important
  393. }
  394.  
  395. ytd-watch-flexy[default-layout][reduced-top-margin] #primary.ytd-watch-flexy, ytd-watch-flexy[default-layout][reduced-top-margin] #secondary.ytd-watch-flexy {
  396. padding-top: var(--ytd-margin-6x) !important
  397. }
  398.  
  399. ytd-watch-metadata[title-headline-xs] h1.ytd-watch-metadata, ytd-watch-metadata[title-headline-m] h1.ytd-watch-metadata {
  400. font-size: 2rem !important;
  401. line-height: 2.8rem !important
  402. }
  403.  
  404. ytd-search ytd-video-renderer, ytd-search ytd-channel-renderer, ytd-search ytd-playlist-renderer, ytd-search ytd-radio-renderer, ytd-search ytd-movie-renderer, ytd-video-renderer.style-scope.ytd-item-section-renderer, ytd-playlist-renderer.style-scope.ytd-item-section-renderer, ytd-search .lockup.ytd-item-section-renderer {
  405. margin-top: 16px !important
  406. }
  407.  
  408. ytd-compact-video-renderer.style-scope.ytd-item-section-renderer, #related .lockup.ytd-item-section-renderer {
  409. margin-top: 8px !important
  410. }`;
  411. if (typeof GM_addStyle !== "undefined") {
  412. GM_addStyle(css);
  413. } else {
  414. let styleNode = document.createElement("style");
  415. styleNode.appendChild(document.createTextNode(css));
  416. (document.querySelector("head") || document.documentElement).appendChild(styleNode);
  417. }
  418. })();
  419.  
  420. var oldHref = document.location.href;
  421. if (window.location.href.indexOf('youtube.com/shorts') > -1) {
  422. window.location.replace(window.location.toString().replace('/shorts/', '/watch?v='));
  423. }
  424. window.onload = function() {
  425. var bodyList = document.querySelector("body")
  426. var observer = new MutationObserver(function(mutations) {
  427. mutations.forEach(function(mutation) {
  428. if (oldHref != document.location.href) {
  429. oldHref = document.location.href;
  430. console.log('location changed!');
  431. if (window.location.href.indexOf('youtube.com/shorts') > -1) {
  432. window.location.replace(window.location.toString().replace('/shorts/', '/watch?v='));
  433. }
  434. }
  435. });
  436. });
  437. var config = {
  438. childList: true,
  439. subtree: true
  440. };
  441. observer.observe(bodyList, config);
  442. };
  443. (() => {
  444. let popupState = 0;
  445. let popupElement = null;
  446. const rate = 1;
  447. const Promise = (async () => { })().constructor;
  448. const PromiseExternal = ((resolve_, reject_) => {
  449. const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
  450. return class PromiseExternal extends Promise {
  451. constructor(cb = h) {
  452. super(cb);
  453. if (cb === h) {
  454. /** @type {(value: any) => void} */
  455. this.resolve = resolve_;
  456. /** @type {(reason?: any) => void} */
  457. this.reject = reject_;
  458. }
  459. }
  460. };
  461. })();
  462. const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0);
  463. let vload = null;
  464. const fastSeekFn = HTMLVideoElement.prototype.fastSeek || null;
  465. const addEventListenerFn = HTMLElement.prototype.addEventListener;
  466. if (!addEventListenerFn) return;
  467. const removeEventListenerFn = HTMLElement.prototype.removeEventListener;
  468. if (!removeEventListenerFn) return;
  469. const ytPremiumPopupSelector = 'yt-mealbar-promo-renderer.style-scope.ytd-popup-container:not([hidden])';
  470. const DEBUG = 0;
  471. const rand = (a, b) => a + Math.random() * (b - a);
  472. const log = DEBUG ? console.log.bind(console) : () => 0;
  473. //$0.$['dismiss-button'].click()
  474. const ytPremiumPopupClose = function () {
  475. const popup = document.querySelector(ytPremiumPopupSelector);
  476. if (popup instanceof HTMLElement) {
  477. if (HTMLElement.prototype.closest.call(popup, '[hidden]')) return;
  478. const cnt = insp(popup);
  479. const btn = cnt.$ ? cnt.$['dismiss-button'] : 0;
  480. if (btn instanceof HTMLElement && HTMLElement.prototype.closest.call(btn, '[hidden]')) return;
  481. btn && btn.click();
  482. }
  483. }
  484. //div.video-ads.ytp-ad-module
  485. const clickSkip = function () {
  486. // ytp-ad-skip-button
  487. const isAdsContainerContainsButton = document.querySelector('.video-ads.ytp-ad-module button');
  488. if (isAdsContainerContainsButton) {
  489. const btnFilter = e => HTMLElement.prototype.matches.call(e, ".ytp-ad-overlay-close-button, .ytp-ad-skip-button-modern, .ytp-ad-skip-button") && !HTMLElement.prototype.closest.call(e, '[hidden]');
  490. const btns = [...document.querySelectorAll('.video-ads.ytp-ad-module button[class*="ytp-ad-"]')].filter(btnFilter);
  491. console.log('# of ads skip btns', btns.length);
  492. if (btns.length !== 1) return;
  493. const btn = btns[0];
  494. if (btn instanceof HTMLElement) {
  495. btn.click();
  496. }
  497. }
  498. };
  499. const adsEndHandlerHolder = function (evt) {
  500. adsEndHandler && adsEndHandler(evt);
  501. }
  502. let adsEndHandler = null;
  503. const videoPlayingHandler = async function (evt) {
  504. try {
  505. if (!evt || !evt.target || !evt.isTrusted || !(evt instanceof Event)) return;
  506. const video = evt.target;
  507. const checkPopup = popupState === 1;
  508. popupState = 0;
  509. const popupElementValue = popupElement;
  510. popupElement = null;
  511. if (video.duration < 0.8) return;
  512. await vload.then();
  513. if (!video.isConnected) return;
  514. const ytplayer = HTMLElement.prototype.closest.call(video, 'ytd-player, ytmusic-player');
  515. if (!ytplayer || !ytplayer.is) return;
  516. const ytplayerCnt = insp(ytplayer);
  517. const player_ = await (ytplayerCnt.player_ || ytplayer.player_ || ytplayerCnt.playerApi || ytplayer.playerApi || 0);
  518. if (!player_) return;
  519. if (typeof ytplayerCnt.getPlayer === 'function' && !ytplayerCnt.getPlayer()) {
  520. await new Promise(r => setTimeout(r, 40));
  521. }
  522. const playerController = await ytplayerCnt.getPlayer() || player_;
  523. if (!video.isConnected) return;
  524. if ('getPresentingPlayerType' in playerController && 'getDuration' in playerController) {
  525. const ppType = await playerController.getPresentingPlayerType();
  526. log('m02a', ppType);
  527. if (ppType === 1 || typeof ppType !== 'number') return; // ads shall be ppType === 2
  528. // const progressState = player_.getProgressState();
  529. // log('m02b', progressState);
  530. // if(!progressState) return;
  531. // const q = progressState.duration;
  532. // if (popupState === 1) console.debug('m05b:ytPremiumPopup', document.querySelector(ytPremiumPopupSelector))
  533. const q = video.duration;
  534. const ytDuration = await playerController.getDuration();
  535. log('m02c', q, ytDuration, Math.abs(ytDuration - q));
  536. if (q > 0.8 && ytDuration > 2.5 && Math.abs(ytDuration - q) > 1.4) {
  537. try {
  538. log('m02s', 'fastSeek', q);
  539. video.muted = true;
  540. const w = Math.round(rand(582, 637) * rate);
  541. const sq = q - w / 1000;
  542. adsEndHandler = null;
  543. const expired = Date.now() + 968;
  544. removeEventListenerFn.call(video, 'ended', adsEndHandlerHolder, false);
  545. removeEventListenerFn.call(video, 'suspend', adsEndHandlerHolder, false);
  546. removeEventListenerFn.call(video, 'durationchange', adsEndHandlerHolder, false);
  547. addEventListenerFn.call(video, 'ended', adsEndHandlerHolder, false);
  548. addEventListenerFn.call(video, 'suspend', adsEndHandlerHolder, false);
  549. addEventListenerFn.call(video, 'durationchange', adsEndHandlerHolder, false);
  550. adsEndHandler = async function (evt) {
  551. adsEndHandler = null;
  552. removeEventListenerFn.call(video, 'ended', adsEndHandlerHolder, false);
  553. removeEventListenerFn.call(video, 'suspend', adsEndHandlerHolder, false);
  554. removeEventListenerFn.call(video, 'durationchange', adsEndHandlerHolder, false);
  555. if (Date.now() < expired) {
  556. const delay = Math.round(rand(92, 117));
  557. await new Promise(r => setTimeout(r, delay));
  558. Promise.resolve().then(() => {
  559. clickSkip();
  560. }).catch(console.warn);
  561. checkPopup && Promise.resolve().then(() => {
  562. const currentPopup = document.querySelector(ytPremiumPopupSelector);
  563. if (popupElementValue ? currentPopup === popupElementValue : currentPopup) {
  564. ytPremiumPopupClose();
  565. }
  566. }).catch(console.warn);
  567. }
  568. };
  569. if (fastSeekFn) fastSeekFn.call(video, sq);
  570. else video.currentTime = sq;
  571. } catch (e) {
  572. console.warn(e);
  573. }
  574. }
  575. }
  576. } catch (e) {
  577. console.warn(e);
  578. }
  579. };
  580. document.addEventListener('loadedmetadata', async function (evt) {
  581. try {
  582. if (!evt || !evt.target || !evt.isTrusted || !(evt instanceof Event)) return;
  583. const video = evt.target;
  584. if (video.nodeName !== "VIDEO") return;
  585. if (video.duration < 0.8) return;
  586. if (!video.matches('.video-stream.html5-main-video')) return;
  587. popupState = 0;
  588. vload = new PromiseExternal();
  589. popupElement = document.querySelector(ytPremiumPopupSelector);
  590. removeEventListenerFn.call(video, 'playing', videoPlayingHandler, { passive: true, capture: false });
  591. addEventListenerFn.call(video, 'playing', videoPlayingHandler, { passive: true, capture: false });
  592. popupState = 1;
  593. let trial = 6;
  594. await new Promise(resolve => {
  595. let io = new IntersectionObserver(entries => {
  596. if (trial-- <= 0 || (entries && entries.length >= 1 && video.matches('ytd-player video, ytmusic-player video'))) {
  597. resolve();
  598. io.disconnect();
  599. io = null;
  600. }
  601. });
  602. io.observe(video);
  603. });
  604. vload.resolve();
  605. } catch (e) {
  606. console.warn(e);
  607. }
  608. }, true);
  609. })();
  610.  
  611. Object.defineProperties(document, { /*'hidden': {value: false},*/ 'webkitHidden': {value: false}, 'visibilityState': {value: 'visible'}, 'webkitVisibilityState': {value: 'visible'} });
  612.  
  613. setInterval(function(){
  614. document.dispatchEvent( new KeyboardEvent( 'keyup', { bubbles: true, cancelable: true, keyCode: 143, which: 143 } ) );
  615. }, 60000);
  616.  
  617. //
  618. // What is this userscript trying to address?
  619. // When playing a video, only a small part of the video loads, and the subsequent
  620. // parts do not load afterward.
  621.  
  622. (function () {
  623. "use strict";
  624.  
  625. const originalGetContext = HTMLCanvasElement.prototype.getContext;
  626. HTMLCanvasElement.prototype.getContext = function (contextType) {
  627. if (contextType === "webgl" || contextType === "webgl2") {
  628. console.log("WebGL is disabled by Tampermonkey");
  629. return null;
  630. }
  631. return originalGetContext.apply(this, arguments);
  632. };
  633. })();