Youtube HD

Select a youtube resolution and resize the player.

目前为 2023-06-29 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Youtube HD
  3. // @author adisib
  4. // @namespace namespace_adisib
  5. // @description Select a youtube resolution and resize the player.
  6. // @version 2023.06.29
  7. // @match https://www.youtube.com/*
  8. // @noframes
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. // The video will only resize when in theater mode on the main youtube website.
  13. // By default only runs on youtube website, not players embeded on other websites, but there is experimental support for embeds.
  14. // To enable experimental support for embedded players outside of YouTube website, do the following steps:
  15. // add " @include * " to the script metadata
  16. // remove " @noframes " from the script metadata
  17.  
  18. // 2022.01.09
  19. // - Fix some javascript exceptions. These didn't affect the script functionality at all.
  20.  
  21. // 2022.02.27
  22. // - Fix potential issue of infinite video reload when tagetRes is >= 720 and highFramerateTargetRes <= large
  23.  
  24.  
  25. (function() {
  26. "use strict";
  27.  
  28. // --- SETTINGS -------
  29.  
  30. // Target Resolution to always set to. If not available, the next best resolution will be used.
  31. const changeResolution = true;
  32. const targetRes = "hd1080";
  33. // Choices for targetRes are currently:
  34. // "highres" >= ( 8K / 4320p / QUHD )
  35. // "hd2880" = ( 5K / 2880p / UHD+ )
  36. // "hd2160" = ( 4K / 2160p / UHD )
  37. // "hd1440" = ( 1440p / QHD )
  38. // "hd1080" = ( 1080p / FHD )
  39. // "hd720" = ( 720p / HD )
  40. // "large" = ( 480p )
  41. // "medium" = ( 360p )
  42. // "small" = ( 240p )
  43. // "tiny" = ( 144p )
  44.  
  45. // Target Resolution for high framerate (60 fps) videos
  46. // If null, it is the same as targetRes
  47. const highFramerateTargetRes = null;
  48.  
  49. // If changePlayerSize is true, then the video's size will be changed on the page
  50. // instead of using youtube's default (if theater mode is enabled).
  51. // If useCustomSize is false, then the player will be resized to try to match the target resolution.
  52. // If true, then it will use the customHeight variables (theater mode is always full page width).
  53. const changePlayerSize = false;
  54. const useCustomSize = false;
  55. const customHeight = 600;
  56.  
  57. // If autoTheater is true, each video page opened will default to theater mode.
  58. // This means the video will always be resized immediately if you are changing the size.
  59. // NOTE: YouTube will not always allow theater mode immediately, the page must be fully loaded before theater can be set.
  60. const autoTheater = false;
  61.  
  62. // If flushBuffer is false, then the first second or so of the video may not always be the desired resolution.
  63. // If true, then the entire video will be guaranteed to be the target resolution, but there may be
  64. // a very small additional delay before the video starts if the buffer needs to be flushed.
  65. const flushBuffer = true;
  66.  
  67. // Setting cookies can allow some operations to perform faster or without a delay (e.g. theater mode)
  68. // Some people don't like setting cookies, so this is false by default (which is the same as old behavior)
  69. const allowCookies = false;
  70.  
  71. // Tries to set the resolution as early as possible.
  72. // This might cause issues on youtube polymer layout, so disable if videos fail to load.
  73. // If videos load fine, leave as true or resolution may fail to set.
  74. const setResolutionEarly = true;
  75.  
  76. // Enables a temporary work around for an issue where users can get the wrong youtube error screen
  77. // (Youtube has two of them for some reason and changing to theater mode moves the wrong one to the front)
  78. // Try disabling if you can't interact with the video or you think you are missing an error message.
  79. const enableErrorScreenWorkaround = true;
  80.  
  81. // --------------------
  82.  
  83.  
  84.  
  85.  
  86. // --- GLOBALS --------
  87.  
  88.  
  89. const DEBUG = false;
  90.  
  91. // Possible resolution choices (in decreasing order, i.e. highres is the best):
  92. const resolutions = ['highres', 'hd2880', 'hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny'];
  93. // youtube has to be at least 480x270 for the player UI
  94. const heights = [4320, 2880, 2160, 1440, 1080, 720, 480, 360, 270, 270];
  95.  
  96. let doc = document, win = window;
  97.  
  98. // ID of the most recently played video
  99. let recentVideo = "";
  100.  
  101. let foundHFR = false;
  102.  
  103. let setHeight = 0;
  104.  
  105.  
  106. // --------------------
  107.  
  108.  
  109. function debugLog(message)
  110. {
  111. if (DEBUG)
  112. {
  113. console.log("YTHD | " + message);
  114. }
  115. }
  116.  
  117.  
  118. // --------------------
  119.  
  120.  
  121. // Used only for compatability with webextensions version of greasemonkey
  122. function unwrapElement(el)
  123. {
  124. if (el && el.wrappedJSObject)
  125. {
  126. return el.wrappedJSObject;
  127. }
  128. return el;
  129. }
  130.  
  131.  
  132. // --------------------
  133.  
  134.  
  135. // Get video ID from the currently loaded video (which might be different than currently loaded page)
  136. function getVideoIDFromURL(ytPlayer)
  137. {
  138. const idMatch = /(?:v=)([\w\-]+)/;
  139. let id = "ERROR: idMatch failed; youtube changed something";
  140. let matches = idMatch.exec(ytPlayer.getVideoUrl());
  141. if (matches)
  142. {
  143. id = matches[1];
  144. }
  145.  
  146. return id;
  147. }
  148.  
  149.  
  150. // --------------------
  151.  
  152.  
  153. // Attempt to set the video resolution to desired quality or the next best quality
  154. function setResolution(ytPlayer, resolutionList)
  155. {
  156. debugLog("Setting Resolution...");
  157.  
  158. const currentQuality = ytPlayer.getPlaybackQuality();
  159. let res = targetRes;
  160.  
  161. if (highFramerateTargetRes && foundHFR)
  162. {
  163. res = highFramerateTargetRes;
  164. }
  165.  
  166. // Youtube doesn't return "auto" for auto, so set to make sure that auto is not set by setting
  167. // even when already at target res or above, but do so without removing the buffer for this quality
  168. if (resolutionList.indexOf(res) >= resolutionList.indexOf(currentQuality))
  169. {
  170. if (ytPlayer.setPlaybackQualityRange !== undefined)
  171. {
  172. ytPlayer.setPlaybackQualityRange(res);
  173. }
  174. ytPlayer.setPlaybackQuality(res);
  175. debugLog("Resolution Set To: " + res);
  176. return;
  177. }
  178.  
  179. const end = resolutionList.length - 1;
  180. let nextBestIndex = Math.max(resolutionList.indexOf(res), 0);
  181. let ytResolutions = ytPlayer.getAvailableQualityLevels();
  182. debugLog("Available Resolutions: " + ytResolutions.join(", "));
  183.  
  184. while ( (ytResolutions.indexOf(resolutionList[nextBestIndex]) === -1) && nextBestIndex < end )
  185. {
  186. ++nextBestIndex;
  187. }
  188.  
  189. if (flushBuffer && currentQuality !== resolutionList[nextBestIndex])
  190. {
  191. let id = getVideoIDFromURL(ytPlayer);
  192. if (id.indexOf("ERROR") === -1)
  193. {
  194. let pos = ytPlayer.getCurrentTime();
  195. ytPlayer.loadVideoById(id, pos, resolutionList[nextBestIndex]);
  196. }
  197.  
  198. debugLog("ID: " + id);
  199. }
  200. if (ytPlayer.setPlaybackQualityRange !== undefined)
  201. {
  202. ytPlayer.setPlaybackQualityRange(resolutionList[nextBestIndex]);
  203. }
  204. ytPlayer.setPlaybackQuality(resolutionList[nextBestIndex]);
  205.  
  206. debugLog("Resolution Set To: " + resolutionList[nextBestIndex]);
  207. }
  208.  
  209.  
  210. // --------------------
  211.  
  212.  
  213. // Set resolution, but only when API is ready (it should normally already be ready)
  214. function setResOnReady(ytPlayer, resolutionList)
  215. {
  216. if (ytPlayer.getPlaybackQuality === undefined)
  217. {
  218. win.setTimeout(setResOnReady, 100, ytPlayer, resolutionList);
  219. }
  220. else
  221. {
  222. let framerateUpdate = false;
  223. if (highFramerateTargetRes)
  224. {
  225. let features = ytPlayer.getVideoData().video_quality_features;
  226. if (features)
  227. {
  228. let isHFR = features.includes("hfr");
  229. framerateUpdate = isHFR && !foundHFR;
  230. foundHFR = isHFR;
  231. }
  232. }
  233.  
  234. let curVid = getVideoIDFromURL(ytPlayer);
  235. if ((curVid !== recentVideo) || framerateUpdate)
  236. {
  237. recentVideo = curVid;
  238. setResolution(ytPlayer, resolutionList);
  239.  
  240. let storedQuality = localStorage.getItem("yt-player-quality");
  241. if (!storedQuality || storedQuality.indexOf(targetRes) === -1)
  242. {
  243. let tc = Date.now(), te = tc + 2592000000;
  244. localStorage.setItem("yt-player-quality","{\"data\":\"" + targetRes + "\",\"expiration\":" + te + ",\"creation\":" + tc + "}");
  245. }
  246. }
  247. }
  248. }
  249.  
  250.  
  251. // --------------------
  252.  
  253.  
  254. function setTheaterMode(ytPlayer)
  255. {
  256. debugLog("Setting Theater Mode");
  257.  
  258. if (win.location.href.indexOf("/watch") !== -1)
  259. {
  260. let pageManager = unwrapElement(doc.getElementsByTagName("ytd-watch-flexy")[0]);
  261.  
  262. if (pageManager)
  263. {
  264. if (enableErrorScreenWorkaround)
  265. {
  266. const styleContent = "#error-screen { z-index: 42 !important } .ytp-error { display: none !important }";
  267.  
  268. let errorStyle = doc.getElementById("ythdErrorWorkaroundStyleSheet");
  269. if (!errorStyle)
  270. {
  271. errorStyle = doc.createElement("style");
  272. errorStyle.type = "text/css";
  273. errorStyle.id = "ythdStyleSheet";
  274. errorStyle.innerHTML = styleContent;
  275. doc.head.appendChild(errorStyle);
  276. }
  277. else
  278. {
  279. errorStyle.innerHTML = styleContent;
  280. }
  281. }
  282.  
  283. try
  284. {
  285. pageManager.theaterModeChanged_(true);
  286. }
  287. catch (e)
  288. { /* Ignore internal youtube exceptions. */ }
  289. }
  290. }
  291. }
  292.  
  293.  
  294. // --------------------
  295.  
  296.  
  297. function computeAndSetPlayerSize()
  298. {
  299. let height = customHeight;
  300. if (!useCustomSize)
  301. {
  302. // don't include youtube search bar as part of the space the video can try to fit in
  303. let heightOffsetEl = doc.getElementById("masthead");
  304. let mastheadContainerEl = doc.getElementById("masthead-container");
  305. let mastheadHeight = 50, mastheadPadding = 16;
  306. if (heightOffsetEl && mastheadContainerEl)
  307. {
  308. mastheadHeight = parseInt(win.getComputedStyle(heightOffsetEl).height, 10);
  309. mastheadPadding = parseInt(win.getComputedStyle(mastheadContainerEl).paddingBottom, 10) * 2;
  310. }
  311.  
  312. let i = Math.max(resolutions.indexOf(targetRes), 0);
  313. height = Math.min(heights[i], win.innerHeight - (mastheadHeight + mastheadPadding));
  314. }
  315.  
  316. resizePlayer(height);
  317. }
  318.  
  319.  
  320. // --------------------
  321.  
  322.  
  323. // resize the player
  324. function resizePlayer(height)
  325. {
  326. debugLog("Setting video player size");
  327.  
  328. if (setHeight === height)
  329. {
  330. debugLog("Player size already set");
  331. return;
  332. }
  333.  
  334. let styleContent = "\
  335. ytd-watch-flexy[theater]:not([fullscreen]) #player-theater-container.style-scope { \
  336. min-height: " + height + "px !important; max-height: none !important; height: " + height + "px !important } \
  337. ytd-watch-flexy[theater]:not([fullscreen]) #player-wide-container.style-scope { \
  338. min-height: " + height + "px !important; max-height: none !important; height: " + height + "px !important }";
  339.  
  340. let ythdStyle = doc.getElementById("ythdStyleSheet");
  341. if (!ythdStyle)
  342. {
  343. ythdStyle = doc.createElement("style");
  344. ythdStyle.type = "text/css";
  345. ythdStyle.id = "ythdStyleSheet";
  346. ythdStyle.innerHTML = styleContent;
  347. doc.head.appendChild(ythdStyle);
  348. }
  349. else
  350. {
  351. ythdStyle.innerHTML = styleContent;
  352. }
  353.  
  354. setHeight = height;
  355.  
  356. win.dispatchEvent(new Event("resize"));
  357. }
  358.  
  359.  
  360. // --- MAIN -----------
  361.  
  362.  
  363. function main()
  364. {
  365. let ytPlayer = doc.getElementById("movie_player") || doc.getElementsByClassName("html5-video-player")[0];
  366. let ytPlayerUnwrapped = unwrapElement(ytPlayer);
  367.  
  368. if (autoTheater && ytPlayerUnwrapped)
  369. {
  370. if (allowCookies && doc.cookie.indexOf("wide=1") === -1)
  371. {
  372. doc.cookie = "wide=1; domain=.youtube.com";
  373. }
  374.  
  375. setTheaterMode(ytPlayerUnwrapped);
  376. }
  377.  
  378. if (changePlayerSize && win.location.host.indexOf("youtube.com") !== -1 && win.location.host.indexOf("gaming.") === -1)
  379. {
  380. computeAndSetPlayerSize();
  381. window.addEventListener("resize", computeAndSetPlayerSize, true);
  382. }
  383.  
  384. if (changeResolution && setResolutionEarly && ytPlayerUnwrapped)
  385. {
  386. setResOnReady(ytPlayerUnwrapped, resolutions);
  387. }
  388.  
  389. if (changeResolution || autoTheater)
  390. {
  391. win.addEventListener("loadstart", function(e) {
  392. if (!(e.target instanceof win.HTMLMediaElement))
  393. {
  394. return;
  395. }
  396.  
  397. ytPlayer = doc.getElementById("movie_player") || doc.getElementsByClassName("html5-video-player")[0];
  398. ytPlayerUnwrapped = unwrapElement(ytPlayer);
  399. if (ytPlayerUnwrapped)
  400. {
  401. debugLog("Loaded new video");
  402. if (changeResolution)
  403. {
  404. setResOnReady(ytPlayerUnwrapped, resolutions);
  405. }
  406. if (autoTheater)
  407. {
  408. setTheaterMode(ytPlayerUnwrapped);
  409. }
  410. }
  411. }, true );
  412. }
  413.  
  414. // This will eventually be changed to use the "once" option, but I want to keep a large range of browser support.
  415. win.removeEventListener("yt-navigate-finish", main, true);
  416. }
  417.  
  418. main();
  419. // Youtube doesn't load the page immediately in new version so you can watch before waiting for page load
  420. // But we can only set resolution until the page finishes loading
  421. win.addEventListener("yt-navigate-finish", main, true);
  422.  
  423. })();
  424.