Youtube HD

Select a youtube resolution and resize the player.

目前為 2017-05-19 提交的版本,檢視 最新版本

// ==UserScript==
// @name          Youtube HD
// @author        adisib
// @namespace     namespace_adisib
// @description   Select a youtube resolution and resize the player.
// @version       2017.05.19
// @include       http://youtube.com/*
// @include       https://youtube.com/*
// @include       http://www.youtube.com/*
// @include       https://www.youtube.com/*
// @include       http://gaming.youtube.com/*
// @include       https://gaming.youtube.com/*
// @noframes
// @grant         none
// ==/UserScript==

// Only the html5 player is supported.
// The video will only resize when in theater mode on the main youtube website.
// By default only runs on youtube website, not players embeded on other websites.
// YouTube players embeded on other websites should work if enabled, but this feature is not supported.

// To enable experimental support for embedded players outside of YouTube website, do the following steps:
//   add  " @include * "  to the script metadata
//   remove  " @noframes "  from the script metadata

// 2017.05.19
// - Fixed very rare issue where Auto HD and Auto Theater wouldn't run

// 2017.05.09
// - Fixed problems with the auto theater feature, should work perfectly now
// - Various minor improvements

(function() {

    "use strict";

    // --- SETTINGS -------

    // Target Resolution to always set to. If not available, the next best resolution will be used.
    const changeResolution = true;
    const targetRes = "hd1080";
    // Choices for targetRes are currently:
    //   "highres" >= ( 8K / 4320p / QUHD  )
    //   "hd2880"   = ( 5K / 2880p /  UHD+ )
    //   "hd2160"   = ( 4K / 2160p /  UHD  )
    //   "hd1440"   = (      1440p /  QHD  )
    //   "hd1080"   = (      1080p /  FHD  )
    //   "hd720"    = (       720p /   HD  )
    //   "large"    = (       480p         )
    //   "medium"   = (       360p         )
    //   "small"    = (       240p         )
    //   "tiny"     = (       144p         )

    // If changePlayerSize is true, then the video's size will be changed on the page
    //   instead of using youtube's default (if theater mode is enabled).
    // If useCustomSize is false, then the player will be resized to try to match the target resolution.
    //   If true, then it will use the customHeight and customWidth variables.
    const changePlayerSize = false;
    const useCustomSize = false;
    const customHeight = 600, customWidth = 1280;

    // If autoTheater is true, each video page opened will default to theater mode.
    // This means the video will always be resized immediately if you are changing the size.
    // NOTE: YouTube will not always allow theater mode immediately, the page must be fully loaded first.
    const autoTheater = false;

    // If flushBuffer is false, then the first second or so of the video may not always be the desired resolution.
    //   If true, then the entire video will be guaranteed to be the target resolution, but there may be
    //   a very small additional delay before the video starts if the buffer needs to be flushed.
    const flushBuffer = true;

    // --------------------




    // --- GLOBALS --------


    const DEBUG = false;

    // Possible resolution choices (in decreasing order, i.e. highres is the best):
    const resolutions = ['highres', 'hd2880', 'hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny'];
    // youtube is always 16:9 right now, but has to be at least 480x270 for the player UI
    const heights = [4320, 2880, 2160, 1440, 1080, 720, 480, 360, 270, 270];
    const widths = [7680, 5120, 3840, 2560, 1920, 1280, 854, 640, 480, 480];

    let doc = document, win = window;


    // --------------------


    function debugLog(message)
    {
        if (DEBUG)
        {
            console.log("YTHD | " + message);
        }
    }


    // --------------------


    // Get video ID from the currently loaded video (which might be different than currently loaded page)
    function getVideoIDFromURL(ytPlayer)
    {
        const idMatch = /(?:v=)([\w\-]+)/;
        let videoURL = ytPlayer.getVideoUrl();
        let id = idMatch.exec(videoURL)[1] || "ERROR: idMatch failed; youtube changed something";

        return id;
    }


    // --------------------


    // Attempt to set the video resolution to desired quality or the next best quality
    function setResolution(ytPlayer, resolutionList)
    {
        debugLog("Setting Resolution...");

        // Youtube doesn't return "auto" for auto, so set to make sure that auto is not set by setting
        //   even when already at target res or above, but do so without removing the buffer for this quality
        if (resolutionList.indexOf(targetRes) >= resolutionList.indexOf(ytPlayer.getPlaybackQuality()))
        {
            ytPlayer.setPlaybackQuality(targetRes);

            debugLog("Resolution Set To: " + targetRes);
        }
        else
        {
            const end = resolutionList.length - 1;
            let nextBestIndex = Math.max(resolutionList.indexOf(targetRes), 0);
            let ytResolutions = ytPlayer.getAvailableQualityLevels();

            debugLog("Available Resolutions: " + ytResolutions.join(", "));

            while ( (ytResolutions.indexOf(resolutionList[nextBestIndex]) === -1) && nextBestIndex < end )
            {
                ++nextBestIndex;
            }

            if (flushBuffer && ytPlayer.getPlaybackQuality() !== resolutionList[nextBestIndex])
            {
                let id = getVideoIDFromURL(ytPlayer);
                if (id.indexOf("ERROR: ") === -1)
                {
                    let pos = ytPlayer.getCurrentTime();
                    ytPlayer.loadVideoById(id, pos, resolutionList[nextBestIndex]);
                }

                debugLog("ID: " + id);
            }
            ytPlayer.setPlaybackQuality(resolutionList[nextBestIndex]);

            debugLog("Resolution Set To: " + resolutionList[nextBestIndex]);
        }

    }


    // --------------------


    // Set resolution, but only when API is ready (it should normally already be ready)
    function setResOnReady(ytPlayer, resolutionList)
    {
        if (ytPlayer.getPlaybackQuality === undefined)
        {
            win.setTimeout(setResOnReady, 100, ytPlayer, resolutionList);
        }
        else
        {
            setResolution(ytPlayer, resolutionList);
        }
    }


    // --------------------


    function setTheaterMode(ytPlayer)
    {
        debugLog("Setting Theater Mode");

        let page = doc.getElementById("page");
        if (ytPlayer && page && win.location.href.indexOf("/watch") !== -1)
        {
            // Wait until youtube has already set the page class, so it doesn't overwrite the theater mode change
            let isLoaded = doc.body.classList.contains("page-loaded");
            if (page.className.indexOf(getVideoIDFromURL(ytPlayer)) === -1 || !isLoaded)
            {
                win.setTimeout(setTheaterMode, 400, ytPlayer);
            }
            if (isLoaded)
            {
                page.classList.remove("watch-non-stage-mode");
                page.classList.add("watch-stage-mode", "watch-wide");
                win.dispatchEvent(new Event("resize"));
            }
        }
    }


    // --------------------


    // resize the player
    function resizePlayer(width, height)
    {
        debugLog("Setting video player size");

        let left, playlistTop, playlistHeight;
        left = (-width / 2);
        playlistTop = (height - 360);
        playlistHeight = (height - 100);

        let styleContent = " \
        #page.watch-stage-mode .player-height { min-height: " + height + "px !important; } \
        #page.watch-stage-mode .player-width { left: " + left + "px !important; min-width: " + width + "px !important; } \
        #page.watch-stage-mode #watch-appbar-playlist { top: " + playlistTop + "px !important; } \
        #page.watch-stage-mode #playlist-autoscroll-list { max-height: " + playlistHeight + "px !important; } \
        ";

        let ythdStyle = doc.getElementById("ythdStyleSheet");
        if (!ythdStyle)
        {
            ythdStyle = doc.createElement("style");
            ythdStyle.type = "text/css";
            ythdStyle.id = "ythdStyleSheet";
            ythdStyle.innerHTML = styleContent;
            doc.head.appendChild(ythdStyle);
        }
        else
        {
            ythdStyle.innerHTML = styleContent;
        }

        // Youtube's video player wont resize itself until resize event is fired
        win.dispatchEvent(new Event("resize"));
    }


    // --- MAIN -----------

    let ytPlayer = doc.getElementById("movie_player") || doc.getElementsByClassName("html5-video-player")[0];

    if (autoTheater && ytPlayer)
    {
        setTheaterMode(ytPlayer);
    }

    if (changePlayerSize && win.location.host.indexOf("youtube.com") !== -1 && win.location.host.indexOf("gaming.") === -1)
    {
        let width, height;
        if (useCustomSize)
        {
            height = customHeight;
            width = customWidth;
        }
        else
        {
            // don't include youtube search bar as part of the space the video can try to fit in
            let heightOffsetEl = doc.getElementById("masthead-positioner-height-offset");
            let mastheadContainerEl = doc.getElementById("yt-masthead-container");
            let mastheadHeight = 50, mastheadPadding = 16;
            if (heightOffsetEl && mastheadContainerEl)
            {
                mastheadHeight = parseInt(win.getComputedStyle(heightOffsetEl).height, 10);
                mastheadPadding = parseInt(win.getComputedStyle(mastheadContainerEl).paddingBottom, 10) * 2;
            }

            let i = Math.max(resolutions.indexOf(targetRes), 0);
            height = Math.min(heights[i], win.innerHeight - (mastheadHeight + mastheadPadding));
            width = Math.min(widths[i], win.innerWidth);
        }

        resizePlayer(width, height);
    }

    if (changeResolution && ytPlayer)
    {
        setResOnReady(ytPlayer, resolutions);
    }

    if (changeResolution || autoTheater)
    {
        win.addEventListener("loadstart", function(e) {
            ytPlayer = doc.getElementById("movie_player") || doc.getElementsByClassName("html5-video-player")[0];
            if (ytPlayer && (e.target instanceof win.HTMLMediaElement))
            {
                debugLog("Loaded new video");
                if (changeResolution)
                {
                    setResOnReady(ytPlayer, resolutions);
                }
                if (autoTheater)
                {
                    setTheaterMode(ytPlayer);
                }
            }
        }, true );
    }

})();