// ==UserScript==
// @name Youtube HD
// @author adisib
// @namespace namespace_adisib
// @description Select a youtube resolution and resize the player.
// @version 2016.10.02
// @include http://youtube.com/*
// @include http://www.youtube.com/*
// @include https://youtube.com/*
// @include https://www.youtube.com/*
// @noframes
// @grant none
// ==/UserScript==
// Only the html5 player is supported.
// The video will not resize when not in theater mode.
// Only supports youtube website, not players embeded on other websites.
(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 flushBuffer is false, then the very beginning of the video may not be the desired resolution
// If true, then the entire video will be guaranteed to be target resolution, but there may be
// a small additional delay before the video starts
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; let win = window;
// --------------------
// Attempt to set the video resolution to desired quality or the next best quality
function setResolution(ytPlayer, resolutionList)
{
if (DEBUG)
{
console.log("YTHD | 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);
if (DEBUG)
{
console.log("YTHD | Resolution Set To: " + targetRes);
}
}
else
{
let ytResolutions = ytPlayer.getAvailableQualityLevels();
let nextBestIndex = resolutionList.indexOf(targetRes) || 0;
let len = resolutionList.length;
if (DEBUG)
{
console.log("YTHD | Available Resolutions: " + ytResolutions.join(", "));
}
while ( (ytResolutions.indexOf(resolutionList[nextBestIndex]) === -1) && nextBestIndex < (len-1) )
{
++nextBestIndex;
}
if (flushBuffer && ytPlayer.getPlaybackQuality() !== resolutionList[nextBestIndex])
{
let id = getVideoIDFromPage();
let pos = ytPlayer.getCurrentTime();
ytPlayer.loadVideoById(id, pos, resolutionList[nextBestIndex]);
}
ytPlayer.setPlaybackQuality(resolutionList[nextBestIndex]);
if (DEBUG)
{
console.log("YTHD | Resolution Set To: " + resolutionList[nextBestIndex]);
}
}
}
// --------------------
// Get video ID from page div class
function getVideoIDFromPage()
{
let pageClass = document.getElementById("page").className;
let idMatch = /(?:video-)([\S]+)/;
let id = idMatch.exec(pageClass)[1] || "ERROR: idMatch needs modification";
return id;
}
// --------------------
// Set resolution, but only when API is ready
function setResOnReady(ytPlayer, resolutionList)
{
if ( (ytPlayer.getPlayerState === undefined)
// || (ytPlayer.getPlayerState() === -1) // This prevents a youtube bug where the video buffer gets stuck
)
{
win.setTimeout(setResOnReady, 100, ytPlayer, resolutionList);
}
else
{
setResolution(ytPlayer, resolutionList);
}
}
// --------------------
// resize the player
function resizePlayer(ytPlayer, width, height)
{
if (DEBUG)
{
console.log("YTHD | Setting video player size");
}
let ythdStyle = doc.getElementById("ythdStyleSheet");
if (!ythdStyle)
{
ythdStyle = doc.createElement("style");
ythdStyle.type = "text/css";
ythdStyle.id = "ythdStyleSheet";
doc.head.appendChild(ythdStyle);
}
let heightStr, widthStr, leftStr, playlistTop;
if (doc.getElementById("page").className.indexOf("watch-stage-mode") !== -1)
{
heightStr = height.toString() + "px !important";
widthStr = width.toString() + "px !important";
leftStr = (-width/2).toString() + "px !important";
// TODO: Do more research on this
playlistTop = (height - 360).toString() + "px !important";
}
else
{
heightStr = widthStr = leftStr = playlistTop = "0px";
}
let styleContent = ".player-height { min-height: " + heightStr + "; } \
.player-width { left: " + leftStr + "; min-width: " + widthStr + "; } \
#watch-appbar-playlist {top: " + playlistTop + "; } \
";
if (styleContent !== ythdStyle.innerHTML)
{
ythdStyle.innerHTML = styleContent;
// Youtube's video player wont resize itself until interacted with so remind it to resize on video page
if (ytPlayer)
{
ytPlayer.setSize(width, height);
}
}
}
// --------------------
// Set HD and Resize if appropriate
function runActions(width, height)
{
let ytPlayer = doc.getElementById("movie_player");
if (changePlayerSize)
{
resizePlayer(ytPlayer, width, height);
}
if (!ytPlayer || win.location.href.indexOf("/watch") === -1)
{
return;
}
if (changeResolution)
{
setResOnReady(ytPlayer, resolutions);
}
}
// --- MAIN -----------
let width, height;
if (useCustomSize)
{
height = customHeight;
width = customWidth;
}
else
{
let mastheadHeight = parseInt(win.getComputedStyle(doc.getElementById("masthead-positioner-height-offset")).height, 10) || 50;
let mastheadPadding = (parseInt(win.getComputedStyle(doc.getElementById("yt-masthead-container")).paddingBottom, 10) * 2) || 16;
let heightOffset = (mastheadHeight + mastheadPadding);
let i = resolutions.indexOf(targetRes) || 0;
height = Math.min(heights[i], win.innerHeight - heightOffset);
width = Math.min(widths[i], win.innerWidth);
}
runActions(width, height);
// This requires a check because youtube doesn't actually load a new video page instead using ajax
// The page div class holds the video id and stage mode state, so just check for changes on its class
let pageDiv = doc.getElementById("page");
if (pageDiv)
{
let prevClass = pageDiv.className;
let ytVidMutationObserver = new MutationObserver( function(mutations) {
// Fullscreen will rewrite className with same content
if(prevClass !== pageDiv.className)
{
prevClass = pageDiv.className;
runActions(width, height);
}
} );
let MOInitOps = { attributes: true, attributeFilter: ["class"] };
ytVidMutationObserver.observe(pageDiv, MOInitOps);
}
})();