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!

As of 2016-11-30. See the latest version.

// ==UserScript==
// @name         Disable YouTube 60 FPS (Force 30 FPS)
// @namespace    SteveJobzniak
// @version      1.4
// @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!
// @author       SteveJobzniak
// @match        *://www.youtube.com/*
// @exclude      *://www.youtube.com/tv*
// @exclude      *://www.youtube.com/embed/*
// @run-at       document-start
// @grant        none
// @noframes
// ==/UserScript==

/*
    This script tells YouTube that your browser only supports 30FPS or less,
    which means that you will see regular 30FPS versions of all HD videos.

    WHY DO THIS?:
      For my six year old laptop, switching from 1080p60 to 1080p30 reduces
      the CPU usage by 2-4x, and removes all CPU overloads that used to make
      my browser and video playback freeze! This means longer battery life,
      and a much happier video watching experience!

      (Furthermore, most older graphics cards only support hardware acceleration
      of 1080p30 or lower, which means that using this script may allow your
      graphics card to perform the video decoding for great battery savings!)

    INSTALLATION:
      Install the Tampermonkey (https://tampermonkey.net) extension for your
      specific browser, and then install this script into Tampermonkey.

      This script has been tested and confirmed working in Safari 9 for Mac
      and Google Chrome for Mac. But it should work in *all* browsers and OS's
      that support the Tampermonkey extension - on Windows, Mac and Linux!

      (This script does NOT work via Chrome's own basic built-in script support!)

    IMPORTANT NOTE TO ALL USERS:
      We DO NOT affect embedded YouTube videos, because embedded players
      *only* check for high-FPS support, so blocking those queries would
      mean completely losing *all* HD resolutions for embedded videos!

      I suggest clicking the "Watch on YouTube" button to play embedded
      high-FPS videos directly on YouTube in 30FPS instead!

    VERY IMPORTANT NOTE FOR SAFARI BROWSER USERS:
      This script contains a workaround for a Safari bug. The Safari browser
      uses "lazy" extension loading, which means that your extensions (such
      as Tampermonkey) don't start running in new tabs until you actually
      visit a webpage in your new tab. And if the first page you visit in your
      new tab is YouTube, it may perform its codec check *before* the extension
      has been fully loaded, in which case YouTube will revert to giving you high-FPS
      videos in that tab. This "too late extension loading" will happen every time
      you write/paste a YouTube video URL in a brand new empty tab, or right-click
      a video link and choose "open in new tab/window", or use the History menu
      to browse to a video page. In other words, it's quite rare but common
      enough to be annoying. Our chosen workaround is that whenever we detect
      that Safari has loaded Tampermonkey too late in the current tab, we perform
      a very quick reload of the current YouTube page so that the extension can
      run before YouTube does its codec check. This reloading is automatic and
      happens so quickly that most people won't even notice anything at all.
      I just mention it here for the few people who pay attention to such things.

    SCRIPT VERSION 1.3 NEWS (AFFECTS APPLE'S SAFARI BROWSER):
      The Safari web browser's extension loading bug is now much more common and much more
      severe since Safari 10 (and it affects ALL extensions, such as adblockers too,
      which is why you might now see ads in your Safari tabs). To prevent an endless
      loop of page reloads when your browser tab is completely bugged, this script
      will now (from v1.3) count the page reloads and will quickly give up if the
      current web tab seems to be hopelessly bugged. In that case, it displays a message bar
      letting you know that videos in the tab will play in high FPS mode if available,
      and it gives you some options to either retry or to restart your browser
      to fix the Safari bug. This is the best we can do for all of you Safari users! :)
      If you don't like it, complain to APPLE. They're the ones responsible for the browser bug!
*/
(function () {
    'use strict';

    function createNewTypeChecker( originalChecker, debugLogging )
    {
        return function( videoType ) {
            if( videoType === undefined ) { return false; }
            if( debugLogging ) { console.log( 'Format Query: "' + videoType + '", originalAnswer: "' + originalChecker( videoType ) + '"' ); }

            // Block all queries regarding high-framerate support.
            var matches = videoType.match( /framerate=(\d+)/ );
            if( matches && ( matches[1] > 30 ) ) {
                if( debugLogging ) { console.log( 'Blocking High-FPS format: "' + videoType + '"' ); }
                return false;
            }

            // Let the browser answer all other codec queries.
            return originalChecker( videoType );
        };
    }

    /*
        Override the browser's Media Source Extensions codec checker,
        to tell YouTube that we don't support any formats above 30FPS.

        (It's very important that this userscript is @run-at document-start,
        before any of the YouTube scripts have had time to query the MSE!)
    */
    if( window.MediaSource ) {
        var originalChecker = window.MediaSource.isTypeSupported.bind( window.MediaSource );
        window.MediaSource.isTypeSupported = createNewTypeChecker( originalChecker, false );
    }

    /*
        Safari browser bug workaround (and any other browsers with the same bug):

        In the Safari browser, ALL new tabs/windows begin their life without
        *any* extensions loaded into memory. They're only loaded *after* you've
        actually visited at least one webpage in that tab. Then they stay loaded
        in that tab, except if you manually type a URL into the location bar or
        change webpage via the History menu, in which case Safari often unloads
        and then re-loads all extensions in that tab again.

        The result of this Safari behavior is that Tampermonkey MAY not
        always be immediately ready when you're visiting a YouTube video page.
        And if the extension is loaded *after* YouTube has already checked for
        supported video formats, then you'll be served with high-FPS video in
        that tab until you reload the webpage. Annoying!

        It happens in quite a LOT of situations, such as opening a brand new
        tab and typing/pasting the URL of a video into it and pressing enter,
        or visiting a video via your History menu, or right-clicking a video
        link and choosing "open in new tab/window", etc.

        The user can solve it by manually reloading the webpage after Tampermonkey
        is done initializing itself in that new tab, but it's such a common
        issue in Safari (it never happens in Google Chrome) that I decided
        to automate the page reloading process as follows:

        * This script is specified to run at "document-start" (before anything
          else on the YouTube webpage is loaded).
        * If we can find the "window.ytplayer" object, it means that we've been
          loaded TOO LATE, after YouTube has already performed its codec detection.
        * But if we're on the YouTube homepage or a search results page, then
          it doesn't MATTER if we're loaded late, since those pages don't contain
          any video players. (And forcing an auto-reload in those cases would
          just needlessly annoy people since it would happen every time time
          they manually type in "youtube.com".)
        * If we're loaded late on a video player page (/watch URL), then
          we automatically reload the webpage to try again. Because the ONLY
          way to solve the late injection is to perform a page reload. It causes
          a very, very brief (almost unnoticeable) flicker as it rapidly reloads
          that tab. That's the low price of fixing this annoying Safari bug! ;)
        * Sometimes, the Safari tab is COMPLETELY bugged and can't be fixed (this
          happens more and more frequently since Safari 10). In that situation,
          we give up after too many reload attempts (to avoid an endless reload loop),
          and we instead notify the user that the FPS replacement has failed and
          that the tab is playing in 60fps if available. We also give them
          some options to help them resolve the Safari bug manually.
    */
    var removeReloadCountHash = true;
    if( window.ytplayer ) {
        if( location.pathname && location.pathname === '/watch' ) {
            // Start reload counter at one.
            var nextReloadCount = 1, oldHash = '', newHash = '';

            // Get the current hash value and reload counter if one exists.
            if( location.hash && location.hash !== '' && location.hash !== '#' ) {
                oldHash = location.hash;
                if( oldHash ) {
                    if( oldHash.charAt(0) === '#' ) {
                        oldHash = oldHash.substr(1); // skip the leading # symbol
                    }
                    var matches = oldHash.match( /fpsreloads=(\d+)/ );
                    if( matches ) {
                        // We found an existing count, so increment to prepare for reload.
                        nextReloadCount = parseInt( matches[1], 10 );
                        nextReloadCount++;
                        oldHash = oldHash.replace( /&?fpsreloads=\d+/, '' );
                    }
                }
            }

            // We allow a limited amount of reload attempts to try to inject before the YouTube player.
            // The user can click the "failure" message at the bottom of the video to try again, if they want to.
            // NOTE: The 2nd "reload attempt" is actually the 3rd load of the page. And if we haven't successfully
            // injected before the YouTube player in 3 load attempts, then we have almost no chance of doing it
            // even with further reloads (there's only about a 10% chance that it would work within 9 loads in such
            // an incredibly bugged browser tab). Most proper tabs work within 1-2, maybe 3 loads. So we abort after 3.
            // That way, the user has a chance to decide quickly instead of waiting for reloads. Most videos are not high-FPS!
            if( nextReloadCount <= 2 ) { // 1 load + 2 reloads = 3 attempts total
                // Determine which new hash to use.
                if( oldHash !== '' ) {
                    newHash = oldHash + '&fpsreloads=' + nextReloadCount;
                } else {
                    newHash = 'fpsreloads=' + nextReloadCount;
                }

                // Tell ourselves that we don't want to remove the reload count from the hash.
                removeReloadCountHash = false;

                // Set the hash, which will track the number of page reloads we've attempted.
                location.hash = newHash;

                // Reload the current video page (since merely setting the hash is not enough to cause a reload in most browsers).
                // NOTE: Waiting LONGER (via a timer) before reloading the page does NOT help the user's browser "react faster"
                // during the next reload (it STILL won't inject the script in time). The ONLY thing we can do is reload the page
                // repeatedly until we either succeed or give up. Because if we've seen window.ytplayer, it means we were injected
                // AFTER the codec check and therefore TOO LATE to fix the framerate on the video page. We MUST reload.
                // ALSO NOTE: In Safari 10 on Mac, it can take anywhere from 2 to 8 reloads in *some* cases (usually 1-3, avg 2), and in very
                // rare cases it can't be fixed *at all* without closing that tab and opening the video in a new tab or restarting Safari.
                location.reload();
            } else {
                // It's time to give up. The repeatedly failed reloads are enough to know that the user's current browser tab
                // is totally bugged out and won't recover. So we'll stop trying and will tell the user instead.
                // This creates a nice, floating, fixed bar at the bottom of the YouTube video page. Most importantly,
                // the bar is non-blocking (unlike an alert()), which means music playlists won't pause waiting for user input.
                var errorDiv = document.createElement( 'div' );
                errorDiv.style.position = 'fixed';
                errorDiv.style.bottom = 0;
                errorDiv.style.left = 0;
                errorDiv.style.width = '100%';
                errorDiv.style.padding = '10px';
                errorDiv.style.textAlign = 'center';
                errorDiv.style.fontSize = '120%';
                errorDiv.style.fontWeight = 'bold';
                errorDiv.style.color = '#fff';
                errorDiv.style.backgroundColor = 'rgba(244, 67, 54, 0.9)';
                errorDiv.style.zIndex = '99999';
                errorDiv.innerHTML = '<p>Your browser failed to disable 60 FPS playback in this tab. Videos in this tab will play in 60 FPS if available.</p><p style="font-size:80%">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>';
                document.body.appendChild( errorDiv );
            }
        }
    }
    if( removeReloadCountHash ) {
        // We'll remove the "fpsreloads=X" location hash from the location (without causing a page reload) if we're either done injecting,
        // OR if the injection reloads have failed too many times in a row (due to a bad browser with late extension injection).
        // This means that people can copy and paste the video link to share it with others without sharing the "fpsreloads=X" value.
        if( window.history && window.history.replaceState ) {
            var newUrl = location.href.replace( /(#?)fpsreloads=\d+&?/g, '$1' ).replace( /[#&]+$/, '' );
            if( location.href != newUrl ) {
                window.history.replaceState( {}, document.title, newUrl );
            }
        }
    }
})();