// ==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 // @downloadURL 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); } })();