2023-10-15 11:39:43 +02:00
|
|
|
// ==UserScript==
|
|
|
|
// @name Youtube HD
|
|
|
|
// @author adisib
|
|
|
|
// @namespace namespace_adisib
|
|
|
|
// @description Select a youtube resolution and resize the player.
|
|
|
|
// @version 2023.08.22
|
|
|
|
// @match https://*.youtube.com/*
|
|
|
|
// @noframes
|
|
|
|
// @grant none
|
|
|
|
// ==/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
|
|
|
|
|
|
|
|
// 2023.08.19 + 2023.08.22
|
|
|
|
// Fix youtube breaking resize, again
|
|
|
|
|
|
|
|
|
|
|
|
(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 )
|
|
|
|
|
|
|
|
// Target Resolution for high framerate (60 fps) videos
|
|
|
|
// If null, it is the same as targetRes
|
|
|
|
const 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).
|
|
|
|
const changePlayerSize = false;
|
|
|
|
const useCustomSize = false;
|
|
|
|
const 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.
|
2023-10-15 14:43:53 +02:00
|
|
|
const autoTheater = true;
|
2023-10-15 11:39:43 +02:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
const 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.
|
|
|
|
const 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.
|
|
|
|
const enableErrorScreenWorkaround = 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 has to be at least 480x270 for the player UI
|
|
|
|
const heights = [4320, 2880, 2160, 1440, 1080, 720, 480, 360, 270, 270];
|
|
|
|
|
|
|
|
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 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 = targetRes;
|
|
|
|
|
|
|
|
if (highFramerateTargetRes && foundHFR)
|
|
|
|
{
|
|
|
|
res = highFramerateTargetRes;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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))
|
|
|
|
{
|
|
|
|
if (ytPlayer.setPlaybackQualityRange !== undefined)
|
|
|
|
{
|
|
|
|
ytPlayer.setPlaybackQualityRange(res);
|
|
|
|
}
|
|
|
|
ytPlayer.setPlaybackQuality(res);
|
|
|
|
debugLog("Resolution Set To: " + res);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
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 (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);
|
|
|
|
}
|
|
|
|
if (ytPlayer.setPlaybackQualityRange !== undefined)
|
|
|
|
{
|
|
|
|
ytPlayer.setPlaybackQualityRange(resolutionList[nextBestIndex]);
|
|
|
|
}
|
|
|
|
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
|
|
|
|
{
|
|
|
|
let framerateUpdate = false;
|
|
|
|
if (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(targetRes) === -1)
|
|
|
|
{
|
|
|
|
let tc = Date.now(), te = tc + 2592000000;
|
|
|
|
localStorage.setItem("yt-player-quality","{\"data\":\"" + 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)
|
|
|
|
{
|
|
|
|
if (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.innerHTML = styleContent;
|
|
|
|
doc.head.appendChild(errorStyle);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
errorStyle.innerHTML = styleContent;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
try
|
|
|
|
{
|
|
|
|
pageManager.theaterModeChanged_(true);
|
|
|
|
}
|
|
|
|
catch (e)
|
|
|
|
{ /* Ignore internal youtube exceptions. */ }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --------------------
|
|
|
|
|
|
|
|
|
|
|
|
function computeAndSetPlayerSize()
|
|
|
|
{
|
|
|
|
let height = customHeight;
|
|
|
|
if (!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(targetRes), 0);
|
|
|
|
height = Math.min(heights[i], win.innerHeight - (mastheadHeight + mastheadPadding));
|
|
|
|
}
|
|
|
|
|
|
|
|
resizePlayer(height);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --------------------
|
|
|
|
|
|
|
|
|
|
|
|
// resize the player
|
|
|
|
function resizePlayer(height)
|
|
|
|
{
|
|
|
|
debugLog("Setting video player size");
|
|
|
|
|
|
|
|
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.innerHTML = styleContent;
|
|
|
|
doc.head.appendChild(ythdStyle);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
ythdStyle.innerHTML = styleContent;
|
|
|
|
}
|
|
|
|
|
|
|
|
setHeight = height;
|
|
|
|
|
|
|
|
win.dispatchEvent(new Event("resize"));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- MAIN -----------
|
|
|
|
|
|
|
|
|
|
|
|
function main()
|
|
|
|
{
|
|
|
|
let ytPlayer = doc.getElementById("movie_player") || doc.getElementsByClassName("html5-video-player")[0];
|
|
|
|
let ytPlayerUnwrapped = unwrapElement(ytPlayer);
|
|
|
|
|
|
|
|
if (autoTheater && ytPlayerUnwrapped)
|
|
|
|
{
|
|
|
|
if (allowCookies && doc.cookie.indexOf("wide=1") === -1)
|
|
|
|
{
|
|
|
|
doc.cookie = "wide=1; domain=.youtube.com";
|
|
|
|
}
|
|
|
|
|
|
|
|
setTheaterMode(ytPlayerUnwrapped);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (changePlayerSize && win.location.host.indexOf("youtube.com") !== -1 && win.location.host.indexOf("gaming.") === -1)
|
|
|
|
{
|
|
|
|
computeAndSetPlayerSize();
|
|
|
|
window.addEventListener("resize", computeAndSetPlayerSize, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (changeResolution && setResolutionEarly && ytPlayerUnwrapped)
|
|
|
|
{
|
|
|
|
setResOnReady(ytPlayerUnwrapped, resolutions);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (changeResolution || autoTheater)
|
|
|
|
{
|
|
|
|
win.addEventListener("loadstart", function(e) {
|
|
|
|
if (!(e.target instanceof win.HTMLMediaElement))
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
ytPlayer = doc.getElementById("movie_player") || doc.getElementsByClassName("html5-video-player")[0];
|
|
|
|
ytPlayerUnwrapped = unwrapElement(ytPlayer);
|
|
|
|
if (ytPlayerUnwrapped)
|
|
|
|
{
|
|
|
|
debugLog("Loaded new video");
|
|
|
|
if (changeResolution)
|
|
|
|
{
|
|
|
|
setResOnReady(ytPlayerUnwrapped, resolutions);
|
|
|
|
}
|
|
|
|
if (autoTheater)
|
|
|
|
{
|
|
|
|
setTheaterMode(ytPlayerUnwrapped);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, 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);
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
})();
|