Disable YouTube 60 FPS (Force 30 FPS)

Tells YouTube that your browser only supports videos at 30FPS or less, which switches all 60FPS videos to 30FPS and allows old computers to watch high-resolution videos without stutter!

目前为 2017-09-23 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Disable YouTube 60 FPS (Force 30 FPS)
  3. // @namespace SteveJobzniak
  4. // @version 1.9
  5. // @description Tells YouTube that your browser only supports videos at 30FPS or less, which switches all 60FPS videos to 30FPS and allows old computers to watch high-resolution videos without stutter!
  6. // @author SteveJobzniak
  7. // @match *://www.youtube.com/*
  8. // @exclude *://www.youtube.com/tv*
  9. // @exclude *://www.youtube.com/embed/*
  10. // @run-at document-start
  11. // @grant none
  12. // @noframes
  13. // ==/UserScript==
  14.  
  15. /*
  16. This script tells YouTube that your browser only supports 30FPS or less,
  17. which means that you will see regular 30FPS versions of all HD videos.
  18.  
  19. WHY DO THIS?:
  20. For my six year old laptop, switching from 1080p60 to 1080p30 reduces
  21. the CPU usage by 2-4x, and removes all CPU overloads that used to make
  22. my browser and video playback freeze! This means longer battery life,
  23. and a much happier video watching experience!
  24.  
  25. (Furthermore, most older graphics cards only support hardware acceleration
  26. of 1080p30 or lower, which means that using this script may allow your
  27. graphics card to perform the video decoding for great battery savings!)
  28.  
  29. INSTALLATION:
  30. Install the Tampermonkey (https://tampermonkey.net) extension for your
  31. specific browser, and then install this script into Tampermonkey.
  32.  
  33. This script has been tested and confirmed working in Safari 9/10+ for Mac
  34. and Google Chrome for Mac. But it should work in *all* browsers and OS's
  35. that support the Tampermonkey extension - on Windows, Mac and Linux!
  36.  
  37. However, I only officially guarantee that it works in Chrome and Safari!
  38.  
  39. (This script does NOT work via Chrome's own basic built-in script support!)
  40.  
  41. IMPORTANT NOTE TO ALL USERS:
  42. We DO NOT affect embedded YouTube videos, because embedded players
  43. *only* check for high-FPS support, so blocking those queries would
  44. mean completely losing *all* HD resolutions for embedded videos!
  45.  
  46. I suggest clicking the "Watch on YouTube" button to play embedded
  47. high-FPS videos directly on YouTube in 30FPS instead!
  48.  
  49. DO YOU WANT TO DONATE A BEER AS THANKS FOR MY WORK?:
  50. Totally optional. ;-)
  51.  
  52. * PayPal: https://www.paypal.me/Armindale/0usd
  53. * Bitcoin: 18XF1EmrkpYi4fqkR2XcHkcJxuTMYG4bcv
  54.  
  55. SCRIPT VERSION 1.3 NEWS:
  56. Now limits the maximum amount of injection (high-FPS blocking) retries
  57. to just two page reloads, and then displays a message bar which lets the
  58. user decide what to do if they've all failed. This is just for severely bugged
  59. web browsers, since most browsers always succeed on their first attempt!
  60.  
  61. SCRIPT VERSION 1.4 NEWS:
  62. Minor change to the address bar history handling.
  63.  
  64. SCRIPT VERSION 1.5 NEWS:
  65. Rewrote the "injection success?" detection method. Previously, we checked
  66. for the existence of YouTube's "window.ytplayer" object as a sign of too-late
  67. injection. But in Sept of 2017, they rewrote their site so that the object
  68. almost always exists by the time our userscript runs. Therefore, the method
  69. has instead been changed to now wait for a fraction of a second and then check
  70. if our code has actually blocked any formats. If so, we're sure that we've
  71. successfully injected the format blocker and disabled high-FPS!
  72.  
  73. SCRIPT VERSION 1.6 NEWS:
  74. As I've said above, I only officially support Chrome and Safari (because it's
  75. a lot of work to test different browsers). However, this new release improves
  76. the script's behavior in other browsers, at least for now. The script has been
  77. improved and verified to now also work in Firefox with Tampermonkey. But please
  78. be aware that Chrome and Safari remain the only officially supported browsers
  79. that I will test each release in. Feel free to report any problems you may
  80. discover in non-supported browsers, but please don't leave negative reviews
  81. regarding those other browsers since I only guarantee Chrome and Safari to work!
  82.  
  83. SCRIPT VERSION 1.7 NEWS:
  84. After some careful research, I've come up with an even more reliable and faster
  85. method for detecting injection success. It now uses a hybrid method, which first
  86. does a brand new, instant check that sees if YouTube's video player code has been
  87. loaded. If their player has already been loaded, then we treat that as an instant
  88. failure and perform a retry. But if there is no player yet, then we can be almost
  89. certain that we've successfully been injected into the page early enough. Then,
  90. we simply wait a while and perform a final verification (same as in v1.5+ but with
  91. a much longer delay) to confirm that we've actually blocked all high-FPS formats.
  92. This new method greatly enhances the chances of early success in all browsers. Enjoy!
  93.  
  94. SCRIPT VERSION 1.8 NEWS:
  95. Enhanced processing speed for the injection-counter in your browser's address bar.
  96.  
  97. SCRIPT VERSION 1.9 NEWS:
  98. Just a little bit of autumn cleaning (after all these recent changes), to improve
  99. the source code readability.
  100. */
  101. (function () {
  102. 'use strict';
  103.  
  104. var useQueryDebug = false; // Show all format checker queries.
  105. var useTimeElapsedDebug = false; // Show time elapsed from injection to 1st format blocking.
  106. var useCallstackDebug = false; // Show the call stack trace.
  107.  
  108. function createNewTypeChecker( originalChecker, fnQueryCallback, debugQueries, debugCallstack )
  109. {
  110. return function( videoType ) {
  111. if( debugCallstack ) { console.trace(); } // Shows who called our format checker.
  112. if( debugQueries ) { console.log( 'Format Query: "' + videoType + '", originalAnswer: "' + originalChecker( videoType ) + '"' ); }
  113. if( fnQueryCallback ) { fnQueryCallback( 'query', videoType ); }
  114.  
  115. if( videoType === undefined ) { return false; }
  116.  
  117. // Block all queries regarding high-framerate support.
  118. var matches = videoType.match( /framerate=(\d+)/ );
  119. if( matches && ( matches[1] > 30 ) ) {
  120. if( debugQueries ) { console.log( 'Blocking High-FPS format: "' + videoType + '"' ); }
  121. if( fnQueryCallback ) { fnQueryCallback( 'block', videoType ); }
  122. return false;
  123. }
  124.  
  125. // Let the browser answer all other codec queries.
  126. return originalChecker( videoType );
  127. };
  128. }
  129.  
  130. function isVideoPage()
  131. {
  132. return ( location.pathname && location.pathname === '/watch' );
  133. }
  134.  
  135. function getReloadCount()
  136. {
  137. // Get the current reload counter value if one exists.
  138. var fpsReloads = null;
  139. if( location.hash && location.hash !== '' && location.hash !== '#' ) {
  140. var matches = location.hash.match( /fpsreloads=(\d+)/ );
  141. if( matches ) {
  142. fpsReloads = parseInt( matches[1], 10 );
  143. }
  144. }
  145.  
  146. return fpsReloads;
  147. }
  148.  
  149. function setReloadCountAndRetryInjection(newReloadCount)
  150. {
  151. var oldHash = '', newHash = '';
  152.  
  153. // Get the current hash value, but ignore any "fpsreloads=X" value.
  154. if( location.hash && location.hash !== '' && location.hash !== '#' ) {
  155. oldHash = location.hash;
  156. if( oldHash ) {
  157. if( oldHash.charAt(0) === '#' ) {
  158. oldHash = oldHash.substr(1); // skip the leading # symbol
  159. }
  160. oldHash = oldHash.replace( /&?fpsreloads=\d+/, '' );
  161. }
  162. }
  163.  
  164. // Determine which new hash to use.
  165. if( oldHash !== '' ) {
  166. newHash = oldHash + '&fpsreloads=' + newReloadCount;
  167. } else {
  168. newHash = 'fpsreloads=' + newReloadCount;
  169. }
  170.  
  171. // Set the hash, which tracks the number of page reloads we've attempted.
  172. location.hash = newHash;
  173.  
  174. // Reload the current video page (since merely setting the hash is not enough to cause a reload in most browsers).
  175. location.reload();
  176. }
  177.  
  178. function deleteReloadCount()
  179. {
  180. // Removes any "fpsreloads=X" counter from the URL, without reloading the page.
  181. if( window.history && window.history.replaceState ) {
  182. var newUrl = location.href.replace( /(#?)fpsreloads=\d+&?/g, '$1' ).replace( /[#&]+$/, '' );
  183. if( location.href != newUrl ) {
  184. window.history.replaceState( {}, document.title, newUrl );
  185. }
  186. }
  187. }
  188.  
  189. /*
  190. This function handles automatic injection retries, which ensures that we've successfully
  191. injected our high-FPS format blocker BEFORE YouTube has performed its video format check.
  192.  
  193. If we've been injected too late (which can happen for reasons such as bad browser timing
  194. or bad Tampermonkey behavior), then this function automatically retries the injection.
  195.  
  196. Here's how the script injection and algorithm works:
  197.  
  198. * This script is specified to run at "document-start" (before anything
  199. else on the YouTube webpage is loaded). But whether that early injection
  200. actually succeeds or not depends 100% on the browser and Tampermonkey.
  201. * The YouTube format check is done by their "window.ytplayer.load()" function
  202. (or specifically, by a deeply internal function that it calls). So we need
  203. to know if the "load()"-function exists yet. If it doesn't exist, then
  204. we can of course be sure that it hasn't been called!
  205. * Even during early injection, the "window.ytplayer" object almost always
  206. exists (because YouTube's code creates it very early), but it's created
  207. as an empty object without any properties (so its "load" function or "config"
  208. array properties don't exist yet).
  209. * Therefore, our first success-check is: If the "window.ytplayer" object is
  210. missing OR completely empty (aka "has no load() function"), then we've definitely
  211. been injected early enough (to probably be guaranteed to block the formats in time).
  212. * Furthermore, even if we THINK we've injected early enough, we'll STILL check
  213. to be 100% sure, by launching a timer which waits a few seconds and then
  214. checks if our injected format blocker has blocked the high-FPS formats.
  215. If so, then we can be totally certain that we've successfully blocked
  216. all high-FPS videos in the current tab.
  217. * If either the initial "window.ytplayer" check or the timer detects a
  218. failure (meaning we were injected too late), then we perform a quick,
  219. automatic page reload to retry the injection. Almost all failures are
  220. due to being too late during the initial injection (so that the "load()"
  221. function has already been called), so we'll detect most failures instantly
  222. and reload instantly; faster than the user can even notice it happening!
  223. This automatic page reload mechanism is the ONLY way to retry late injection.
  224. * But, if we're on the YouTube homepage or a search results page, or any other
  225. non-"/watch" video page, then it doesn't MATTER if we're injected late,
  226. since those pages don't contain any video players. (And forcing an
  227. auto-reload in those cases would just needlessly annoy people since
  228. it would happen every time that they manually type in "youtube.com".)
  229. The reason that it doesn't matter when we inject on non-video pages,
  230. is that the injection will always succeed on non-video tabs, because the
  231. injected code will still be in memory when the user navigates to a video,
  232. since YouTube doesn't truly "go to" other pages; it simply "replaces the HTML".
  233. * Sometimes, your browser/tab is COMPLETELY bugged and can't be fixed.
  234. In that situation, we give up after too many reload attempts (to avoid
  235. an endless reload loop), and we instead notify the user that the FPS
  236. replacement has failed and that the tab is playing in 60fps if available.
  237. We also give them some options to help them retry the injection manually.
  238. */
  239. function handleInjectionRetries( isOkay, currentFPSReloads )
  240. {
  241. // If we're not currently on a video page, we must treat that as success too, because non-video pages
  242. // don't have any video player (so there's never any risk of being injected "too late" there).
  243. if( isOkay || ! isVideoPage() ) {
  244. // Success. Ensure that any possible lingering reload counter is erased from the address bar.
  245. deleteReloadCount();
  246. } else {
  247. // Failure (on a video page). Now determine how many times we've attempted injection into this tab.
  248. // Start reload counter at one if no value exists, otherwise increment current value by +1.
  249. var nextReloadCount = ( typeof currentFPSReloads === 'number' ? currentFPSReloads + 1 : 1 );
  250.  
  251. // We allow a limited amount of reload attempts to try to inject before the YouTube player.
  252. // The user can click the "failure" message at the bottom of the video to try again, if they want to.
  253. // NOTE: The 2nd "reload attempt" is actually the 3rd load of the page. And if we haven't successfully
  254. // injected before the YouTube player in 3 load attempts, then we have almost no chance of doing it
  255. // even with further reloads (there's only about a 10% chance that it would work within 9 loads in such
  256. // an incredibly bugged browser tab). Most proper tabs work within 1-2, maybe 3 loads. So we abort after 3.
  257. // That way, the user has a chance to decide quickly instead of waiting for reloads. Most videos are not high-FPS!
  258. if( nextReloadCount <= 2 ) { // 1 load + 2 reloads = 3 attempts total
  259. // Reload the page to perform the next injection attempt attempt.
  260. // NOTE: Waiting LONGER (via a timer) before reloading the page does NOT help the user's browser "react faster"
  261. // during the next reload (it STILL won't inject the script in time). The ONLY thing we can do is reload the page
  262. // repeatedly until we either succeed or give up.
  263. setReloadCountAndRetryInjection( nextReloadCount );
  264. } else {
  265. // It's time to give up. The repeatedly failed reloads are enough to know that the user's current browser tab
  266. // is totally bugged out and won't recover. So we'll stop trying and will tell the user instead.
  267.  
  268. // First, let's remove the "fpsreloads=X" value from the address bar, otherwise it will stay there forever.
  269. deleteReloadCount();
  270.  
  271. // Now create a nice, floating, fixed bar at the bottom of the YouTube video page. Most importantly,
  272. // the bar is non-blocking (unlike an alert()), meaning music playlists won't pause waiting for user input.
  273. var createErrorDiv = function() {
  274. if( !document.body ) { return false; }
  275. // Check for officially supported browsers. There is no guarantee that other browser engines
  276. // are capable of injecting this script early enough to block YouTube's formats.
  277. var isChrome = !!window.chrome && !!window.chrome.webstore;
  278. var isSafari = navigator.vendor.indexOf('Apple') > -1 && navigator.userAgent.indexOf('Safari') > -1;
  279. var isSupportedBrowser = isChrome || isSafari;
  280. // Create the error message.
  281. var errorMessage = '<p>Your browser failed to disable 60 FPS playback in this tab. Videos in this tab will play in 60 FPS if available.</p>' +
  282. '<p style="font-size:85%">You can try again by <a href="#reload" onclick="location.reload();return false" style="color:#fff;text-decoration:underline">reloading</a> the page, using a <a href="#newtab" onclick="var ytplayer=document.getElementById(\'movie_player\');if(ytplayer&&ytplayer.pauseVideo){ytplayer.pauseVideo();};window.open(location.href,\'_blank\');return false" style="color:#fff;text-decoration:underline">new tab</a> or restarting your browser.</p>' +
  283. ( !isSupportedBrowser ? '<p style="background-color:#000; margin-top:1em;">Special Notice: You are using an unsupported web browser. We can only guarantee success in Google Chrome and Safari.</p>' : '' );
  284. var errorDiv = document.createElement( 'div' );
  285. errorDiv.style.position = 'fixed';
  286. errorDiv.style.bottom = 0;
  287. errorDiv.style.left = 0;
  288. errorDiv.style.width = '100%';
  289. errorDiv.style.padding = '10px';
  290. errorDiv.style.textAlign = 'center';
  291. errorDiv.style.fontSize = '130%';
  292. errorDiv.style.fontWeight = 'bold';
  293. errorDiv.style.color = '#fff';
  294. errorDiv.style.backgroundColor = 'rgba(244, 67, 54, 0.9)';
  295. errorDiv.style.zIndex = '99999';
  296. errorDiv.innerHTML = errorMessage;
  297. document.body.appendChild( errorDiv );
  298. return true;
  299. };
  300.  
  301. // Because we're running at document-start, we may not have a DOM yet. So defer the creation
  302. // of the footer warning bar until the DOM has finished loading, if necessary.
  303. if( document.readyState !== 'interactive' && document.readyState !== 'complete' ) {
  304. var createdDiv = false;
  305. document.addEventListener( 'readystatechange', function( evt ) {
  306. if( !createdDiv && ( document.readyState === 'interactive' || document.readyState === 'complete' ) ) {
  307. createdDiv = createErrorDiv(); // Deferred creation.
  308. }
  309. }, false );
  310. } else {
  311. createErrorDiv(); // DOM was already loaded, so create the bar immediately.
  312. }
  313. }
  314. }
  315. }
  316.  
  317. /*
  318. Override the browser's Media Source Extensions codec checker,
  319. to tell YouTube that we don't support any formats above 30FPS.
  320.  
  321. (It's very important that this userscript is @run-at document-start,
  322. before any of the YouTube scripts have had time to query the MSE!)
  323. */
  324. if( window.MediaSource ) {
  325. // Read the current "fpsreloads=X" count from the URL, if a value exists.
  326. var currentFPSReloads = getReloadCount();
  327.  
  328. // This instantly detects whether our userscript has *definitely* been injected too late into the page:
  329. // All of YouTube's high-FPS format checks are performed by window.ytplayer.load()'s call-chain.
  330. // If we're injected early enough, the "window.ytplayer" object will either be totally missing (rarely),
  331. // or is completely empty with no properties or functions yet (this is the situation most of the time).
  332. var injectedTooLate = !( ! window.ytplayer || Object.getOwnPropertyNames( window.ytplayer ).length === 0 );
  333. if( isVideoPage() && injectedTooLate ) { // We only need to perform this check if we're on a video page.
  334. handleInjectionRetries( false, currentFPSReloads ); // Failed.
  335. return; // Skips the rest of the code below.
  336. }
  337.  
  338. // The YouTube player code hasn't been loaded yet, so it's almost certain that our injection will succeed.
  339. // We should therefore remove any "fpsreloads=X" value from the URL now, so that people can copy and paste
  340. // their video link to share it with others without sharing the "fpsreloads=X" value.
  341. deleteReloadCount();
  342.  
  343. // Even though we didn't see the YouTube player's "load()" function, we'll also set up a separate check which waits
  344. // a while and then verifies that we've successfully blocked high-FPS formats. This is just for extra safety.
  345. var injectedAtTime = performance.now(); // Debug variable.
  346. var blockCount = 0;
  347. var updateBlockCount = function( action, videoType ) {
  348. if( action == 'block' ) {
  349. blockCount++;
  350. if( useTimeElapsedDebug && blockCount === 1 ) {
  351. console.log( 'Time from injection until 1st blocked format: '+(performance.now() - injectedAtTime) );
  352. }
  353. }
  354. };
  355. var verifyBlockCount = function() {
  356. // As of 2017, we expect to block 5 formats under normal circumstances, but that number
  357. // may change. Either way, JavaScript is single-threaded, so if our overridden format
  358. // checking function has been queried by YouTube even a single time, it means that we've
  359. // successfully injected before YouTube's scripts did their format capability check!
  360. if( blockCount >= 1 ) {
  361. handleInjectionRetries( true, currentFPSReloads ); // Successfully injected.
  362. } else {
  363. handleInjectionRetries( false, currentFPSReloads ); // Failed.
  364. }
  365. };
  366.  
  367. // How long to wait until we confirm injection success by checking how many (if any) formats we've
  368. // blocked. This delay must be carefully balanced, so that the page has a chance to fully load and
  369. // perform its format check.
  370. //
  371. // The time it takes from injection until format blocking depends on how early the browser injects
  372. // us into the page, and how fast the browser executes the rest of YouTube's code after that!
  373. // It is also influenced by the user's computer speed.
  374. //
  375. // Here are browser measurements for how long after injection YouTube's format check usually happens:
  376. // - Safari: 200-775ms (Usually 250-550ms).
  377. // - Chrome: 700-2050ms (Usually around 1800ms, but sometimes frequents the 900ms range).
  378. // - Firefox: Not measured with the new timer code, but it was ~600-800ms longer than Chrome.
  379. //
  380. // Anyway, because of the fact that our "window.ytplayer" check above is so accurate at instantly
  381. // detecting almost 100% of all cases of too-late injection, we don't need this "was the injection
  382. // truly successful?" verification to be very fast, so we can afford to let it wait for a long time.
  383. // Because if we didn't see the "window.ytplayer.load()" function, then we can be almost totally
  384. // certain that we've been injected early enough and that our format-blocker will be successful.
  385. if( isVideoPage() ) {
  386. // Check for confirmed "format blocking success" 12 seconds after injection into this video page.
  387. setTimeout( verifyBlockCount, 12000 );
  388. } else {
  389. // We've definitely been injected into the site early enough, since we're on a non-video page!
  390. handleInjectionRetries( true, currentFPSReloads ); // Successfully injected.
  391. }
  392.  
  393. // Inject our video format blocker.
  394. var originalChecker = window.MediaSource.isTypeSupported.bind( window.MediaSource );
  395. window.MediaSource.isTypeSupported = createNewTypeChecker( originalChecker, updateBlockCount, useQueryDebug, useCallstackDebug );
  396. }
  397. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址