- // ==UserScript==
- // @name Youtube HD
- // @author adisib
- // @namespace namespace_adisib
- // @description Select a youtube resolution and resize the player.
- // @version 2025.04.13
- // @match https://*.youtube.com/*
- // @noframes
- // @grant GM.getValue
- // @grant GM.setValue
- // ==/UserScript==
-
- // 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, but there is experimental support for embeds.
- // 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
-
- // 2025.04.13
- // Add requested option "removeBlackBars" which restricts the height of the video if the screen width is smaller than player width
-
- // 2025.04.12
- // Fix resizing not working with some browsers/userscript managers (worked with greasemonkey and firefox, probably not with anything else)
- // Set the default target resolution to 4k now, since it has probably been long enough that the number of complaints about choosing 4k will be minimal
- // Thanks to ElectroKnight22 (https://greatest.deepsurf.us/en/scripts/498145-youtube-hd-premium) and all others who provide alternatives while this script was being poorly maintained
- // There could be other issues complained about that I didn't fix yet. Please keep complaining to me so that I will fix them.
-
-
- (function() {
- "use strict";
-
- // --- SETTINGS -------
-
- // PLEASE NOTE:
- // Settings will be saved the first time the script is loaded so that your changes aren't undone by an update.
- // If you want to make adjustments, please set "overwriteStoredSettings" to true.
- // Otherwise, your settings changes will NOT have an effect because it will used the saved settings.
- // After the script has next been run by loading a video with "overwriteStoredSettings" as true, your settings will be updated.
- // Then after that you can set it to false again to prevent your settings from being changed by an update.
-
- let settings = {
-
- // Target Resolution to always set to. If not available, the next best resolution will be used.
- changeResolution: true,
- preferPremium: true,
- targetRes: "hd2160",
- // 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 )
-
- // Target Resolution for high framerate (60 fps) videos
- // If null, it is the same as targetRes
- highFramerateTargetRes: null,
-
- // 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 variables (theater mode is always full page width).
- // If removeBlackBars is true, will try to cap the height based on the current video's aspect ratio
- // if the screen size is smaller than the requested resolution
- changePlayerSize: false,
- removeBlackBars: false,
- useCustomSize: false,
- customHeight: 600,
-
- // 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 before theater can be set.
- 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.
- flushBuffer: true,
-
- // Setting cookies can allow some operations to perform faster or without a delay (e.g. theater mode)
- // Some people don't like setting cookies, so this is false by default (which is the same as old behavior)
- allowCookies: false,
-
- // Tries to set the resolution as early as possible.
- // This might cause issues on youtube polymer layout, so disable if videos fail to load.
- // If videos load fine, leave as true or resolution may fail to set.
- setResolutionEarly: true,
-
- // Enables a temporary work around for an issue where users can get the wrong youtube error screen
- // (Youtube has two of them for some reason and changing to theater mode moves the wrong one to the front)
- // Try disabling if you can't interact with the video or you think you are missing an error message.
- enableErrorScreenWorkaround: true,
-
- // Use the iframe API to set resolution if possible. Otherwise uses simulated mouse clicks.
- useAPI: true,
-
- // Overwrite stored settings with the settings coded into the script, to apply changes.
- // Set and keep as true to have settings behave like before, where you can just edit the settings here to change them.
- overwriteStoredSettings: false
-
- };
-
- // --------------------
-
-
-
-
- // --- 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 has to be at least 480x270 for the player UI
- const heights = [4320, 2880, 2160, 1440, 1080, 720, 480, 360, 240, 144];
-
- let doc = document, win = window;
-
- // ID of the most recently played video
- let recentVideo = "";
-
- let foundHFR = false;
-
- let setHeight = 0;
-
-
- // --------------------
-
-
- function debugLog(message)
- {
- if (DEBUG)
- {
- console.log("YTHD | " + message);
- }
- }
-
-
- // --------------------
-
-
- // Used only for compatability with webextensions version of greasemonkey
- function unwrapElement(el)
- {
- if (el && el.wrappedJSObject)
- {
- return el.wrappedJSObject;
- }
- return el;
- }
-
-
- // --------------------
-
-
- // Get player object
- function getPlayer()
- {
- let ytPlayer = doc.getElementById("movie_player") || doc.getElementsByClassName("html5-video-player")[0];
- return unwrapElement(ytPlayer);
- }
-
-
- // --------------------
-
-
- // Get video ID from the currently loaded video (which might be different than currently loaded page)
- function getVideoIDFromURL(ytPlayer)
- {
- const idMatch = /(?:v=)([\w\-]+)/;
- let id = "ERROR: idMatch failed; youtube changed something";
- let matches = idMatch.exec(ytPlayer.getVideoUrl());
- if (matches)
- {
- id = matches[1];
- }
-
- return id;
- }
-
-
- // --------------------
-
-
- // Attempt to set the video resolution to desired quality or the next best quality
- function setResolution(ytPlayer, resolutionList)
- {
- debugLog("Setting Resolution...");
-
- const currentQuality = ytPlayer.getPlaybackQuality();
- let res = settings.targetRes;
-
- if (settings.highFramerateTargetRes && foundHFR)
- {
- res = settings.highFramerateTargetRes;
- }
-
- let shouldPremium = settings.preferPremium && [...ytPlayer.getAvailableQualityData()].some(q => q.quality == res && q.qualityLabel.includes("Premium") && q.isPlayable);
- let useButtons = !settings.useAPI || shouldPremium;
-
- // 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(res) < resolutionList.indexOf(currentQuality))
- {
- const end = resolutionList.length - 1;
- let nextBestIndex = Math.max(resolutionList.indexOf(res), 0);
- let ytResolutions = ytPlayer.getAvailableQualityLevels();
- debugLog("Available Resolutions: " + ytResolutions.join(", "));
-
- while ( (ytResolutions.indexOf(resolutionList[nextBestIndex]) === -1) && nextBestIndex < end )
- {
- ++nextBestIndex;
- }
-
- if (!useButtons && settings.flushBuffer && currentQuality !== resolutionList[nextBestIndex])
- {
- let id = getVideoIDFromURL(ytPlayer);
- if (id.indexOf("ERROR") === -1)
- {
- let pos = ytPlayer.getCurrentTime();
- ytPlayer.loadVideoById(id, pos, resolutionList[nextBestIndex]);
- }
-
- debugLog("ID: " + id);
- }
-
- res = resolutionList[nextBestIndex];
- }
-
- if (settings.useAPI)
- {
- if (ytPlayer.setPlaybackQualityRange !== undefined)
- {
- ytPlayer.setPlaybackQualityRange(res);
- }
- ytPlayer.setPlaybackQuality(res);
- debugLog("(API) Resolution Set To: " + res);
- }
- if (useButtons)
- {
- let resLabel = heights[resolutionList.indexOf(res)];
- if (shouldPremium)
- {
- resLabel = [...ytPlayer.getAvailableQualityData()].find(q => q.quality == res && q.qualityLabel.includes("Premium")).qualityLabel;
- }
-
- let settingsButton = doc.querySelector(".ytp-settings-button:not(#ScaleBtn)")[0];
- unwrapElement(settingsButton).click();
-
- let qualityMenuButton = document.evaluate('.//*[contains(text(),"Quality")]/ancestor-or-self::*[@class="ytp-menuitem-label"]', ytPlayer, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
- unwrapElement(qualityMenuButton).click();
-
- let qualityButton = document.evaluate('.//*[contains(text(),"' + heights[resolutionList.indexOf(res)] + '") and not(@class)]/ancestor::*[@class="ytp-menuitem"]', ytPlayer, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
- unwrapElement(qualityButton).click();
- debugLog("(Buttons) Resolution Set To: " + res);
- }
- }
-
-
- // --------------------
-
-
- // Set resolution, but only when API is ready (it should normally already be ready)
- function setResOnReady(ytPlayer, resolutionList)
- {
- if (settings.useAPI && (ytPlayer.getPlaybackQuality === undefined || ytPlayer.getPlaybackQuality() == "unknown"))
- {
- win.setTimeout(setResOnReady, 100, ytPlayer, resolutionList);
- }
- else
- {
- let framerateUpdate = false;
- if (settings.highFramerateTargetRes)
- {
- let features = ytPlayer.getVideoData().video_quality_features;
- if (features)
- {
- let isHFR = features.includes("hfr");
- framerateUpdate = isHFR && !foundHFR;
- foundHFR = isHFR;
- }
- }
-
- let curVid = getVideoIDFromURL(ytPlayer);
- if ((curVid !== recentVideo) || framerateUpdate)
- {
- recentVideo = curVid;
- setResolution(ytPlayer, resolutionList);
-
- let storedQuality = localStorage.getItem("yt-player-quality");
- if (!storedQuality || storedQuality.indexOf(settings.targetRes) === -1)
- {
- let tc = Date.now(), te = tc + 2592000000;
- localStorage.setItem("yt-player-quality","{\"data\":\"" + settings.targetRes + "\",\"expiration\":" + te + ",\"creation\":" + tc + "}");
- }
- }
- }
- }
-
-
- // --------------------
-
-
- function setTheaterMode(ytPlayer)
- {
- debugLog("Setting Theater Mode");
-
- if (win.location.href.indexOf("/watch") !== -1)
- {
- let pageManager = unwrapElement(doc.getElementsByTagName("ytd-watch-flexy")[0]);
-
- if (pageManager && !pageManager.hasAttribute("theater"))
- {
- if (settings.enableErrorScreenWorkaround)
- {
- const styleContent = "#error-screen { z-index: 42 !important } .ytp-error { display: none !important }";
-
- let errorStyle = doc.getElementById("ythdErrorWorkaroundStyleSheet");
- if (!errorStyle)
- {
- errorStyle = doc.createElement("style");
- errorStyle.type = "text/css";
- errorStyle.id = "ythdStyleSheet";
- errorStyle.textContent = styleContent;
- doc.head.appendChild(errorStyle);
- }
- else
- {
- errorStyle.textContent = styleContent;
- }
- }
-
- try
- {
- pageManager.setTheaterModeRequested(true);
- pageManager.updateTheaterModeState_(true);
- pageManager.onTheaterReduxValueUpdate(true);
- pageManager.setPlayerTheaterMode_();
- pageManager.dispatchEvent(new CustomEvent("yt-set-theater-mode-enabled", { detail: {enabled: true}, bubbles: true, cancelable: false} ));
- }
- catch {}
-
- let theaterButton;
- for (let i = 0; i < 3 && !pageManager.theaterValue; ++i)
- {
- debugLog("Clicking theater button to attempt to notify redux state");
- let theaterButton = theaterButton || unwrapElement(doc.getElementsByClassName("ytp-size-button")[0]);
- theaterButton.click();
- }
- }
- }
- }
-
-
- // --------------------
-
-
- function computeAndSetPlayerSize()
- {
- let height = settings.customHeight;
- if (!settings.useCustomSize)
- {
- // don't include youtube search bar as part of the space the video can try to fit in
- let heightOffsetEl = doc.getElementById("masthead");
- let mastheadContainerEl = doc.getElementById("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(settings.targetRes), 0);
- height = Math.min(heights[i], win.innerHeight - (mastheadHeight + mastheadPadding));
- height = Math.max(height, 270);
-
- if (settings.removeBlackBars)
- {
- let ytPlayer = getPlayer();
- if (ytPlayer !== undefined && ytPlayer.getVideoAspectRatio !== undefined)
- {
- height = Math.min(height, win.innerWidth / ytPlayer.getVideoAspectRatio());
- }
- }
- }
-
- resizePlayer(height);
- }
-
-
- // --------------------
-
-
- // resize the player
- function resizePlayer(height)
- {
- debugLog("Setting video player size to " + height);
-
- if (setHeight === height)
- {
- debugLog("Player size already set");
- return;
- }
-
- let styleContent = "\
- ytd-watch-flexy[theater]:not([fullscreen]) #player-theater-container.style-scope, \
- ytd-watch-flexy[theater]:not([fullscreen]) #player-wide-container.style-scope, \
- ytd-watch-flexy[theater]:not([fullscreen]) #full-bleed-container.style-scope { \
- min-height: " + height + "px !important; max-height: none !important; height: " + height + "px !important }";
-
- let ythdStyle = doc.getElementById("ythdStyleSheet");
- if (!ythdStyle)
- {
- ythdStyle = doc.createElement("style");
- ythdStyle.type = "text/css";
- ythdStyle.id = "ythdStyleSheet";
- ythdStyle.textContent = styleContent;
- doc.head.appendChild(ythdStyle);
- }
- else
- {
- ythdStyle.textContent = styleContent;
- }
-
- setHeight = height;
-
- win.dispatchEvent(new Event("resize"));
- }
-
-
- // --- MAIN -----------
-
-
- function main()
- {
- let ytPlayer = getPlayer();
-
- if (settings.autoTheater && ytPlayer)
- {
- if (settings.allowCookies && doc.cookie.indexOf("wide=1") === -1)
- {
- doc.cookie = "wide=1; domain=.youtube.com";
- }
-
- setTheaterMode(ytPlayer);
- }
-
- if (settings.changePlayerSize && win.location.host.indexOf("youtube.com") !== -1 && win.location.host.indexOf("gaming.") === -1)
- {
- computeAndSetPlayerSize();
- window.addEventListener("resize", computeAndSetPlayerSize, true);
- }
-
- if (settings.changeResolution && settings.setResolutionEarly && ytPlayer)
- {
- setResOnReady(ytPlayer, resolutions);
- }
-
- if (settings.changeResolution || settings.autoTheater)
- {
- win.addEventListener("loadstart", function(e) {
- if (!(e.target instanceof win.HTMLMediaElement))
- {
- return;
- }
-
- ytPlayer = getPlayer();
- if (ytPlayer)
- {
- debugLog("Loaded new video");
- if (settings.changeResolution)
- {
- setResOnReady(ytPlayer, resolutions);
- }
- if (settings.autoTheater)
- {
- setTheaterMode(ytPlayer);
- }
- }
- }, true );
- }
-
- // This will eventually be changed to use the "once" option, but I want to keep a large range of browser support.
- win.removeEventListener("yt-navigate-finish", main, true);
- }
-
- async function applySettings()
- {
- if (typeof GM != 'undefined' && GM.getValue && GM.setValue)
- {
- let settingsSaved = await GM.getValue("SettingsSaved");
-
- if (settings.overwriteStoredSettings || !settingsSaved)
- {
- Object.entries(settings).forEach(([k,v]) => GM.setValue(k, v));
-
- await GM.setValue("SettingsSaved", true);
- }
- else
- {
- await Promise.all(
- Object.keys(settings).map(k => { let newval = GM.getValue(k); return newval.then(v => [k,v]); })
- ).then((c) => c.forEach(([nk,nv]) => {
- if (settings[nk] !== null && nk !== "overwriteStoredSettings")
- {
- settings[nk] = nv;
- }
- }));
- }
-
- debugLog(Object.entries(settings).map(([k,v]) => k + " | " + v).join(", "));
- }
- }
-
- applySettings().then(() => {
- main();
- // Youtube doesn't load the page immediately in new version so you can watch before waiting for page load
- // But we can only set resolution until the page finishes loading
- win.addEventListener("yt-navigate-finish", main, true);
- });
- })();