Youtube HD

Select a youtube resolution and resize the player.

目前為 2018-03-30 提交的版本,檢視 最新版本

  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 2018.03.30
  7. // @include http://youtube.com/*
  8. // @include https://youtube.com/*
  9. // @include http://www.youtube.com/*
  10. // @include https://www.youtube.com/*
  11. // @include http://gaming.youtube.com/*
  12. // @include https://gaming.youtube.com/*
  13. // @noframes
  14. // @grant none
  15. // ==/UserScript==
  16.  
  17. // The video will only resize when in theater mode on the main youtube website.
  18. // By default only runs on youtube website, not players embeded on other websites, but there is experimental support for embeds.
  19. // To enable experimental support for embedded players outside of YouTube website, do the following steps:
  20. // add " @include * " to the script metadata
  21. // remove " @noframes " from the script metadata
  22.  
  23. // 2018.03.30
  24. // - Quick fix for youtube change today making script prevent manual resolution setting by freezing the video and causing some or all multi-process browsers to hang.
  25.  
  26. // 2017.12.09
  27. // - Fixed Resolution not setting because Youtube broke its API
  28.  
  29.  
  30. (function() {
  31.  
  32. "use strict";
  33.  
  34. // --- SETTINGS -------
  35.  
  36. // Target Resolution to always set to. If not available, the next best resolution will be used.
  37. const changeResolution = true;
  38. const targetRes = "hd1080";
  39. // Choices for targetRes are currently:
  40. // "highres" >= ( 8K / 4320p / QUHD )
  41. // "hd2880" = ( 5K / 2880p / UHD+ )
  42. // "hd2160" = ( 4K / 2160p / UHD )
  43. // "hd1440" = ( 1440p / QHD )
  44. // "hd1080" = ( 1080p / FHD )
  45. // "hd720" = ( 720p / HD )
  46. // "large" = ( 480p )
  47. // "medium" = ( 360p )
  48. // "small" = ( 240p )
  49. // "tiny" = ( 144p )
  50.  
  51. // If changePlayerSize is true, then the video's size will be changed on the page
  52. // instead of using youtube's default (if theater mode is enabled).
  53. // If useCustomSize is false, then the player will be resized to try to match the target resolution.
  54. // If true, then it will use the customHeight and customWidth variables.
  55. const changePlayerSize = false;
  56. const useCustomSize = false;
  57. const customHeight = 600, customWidth = 1280;
  58.  
  59. // If autoTheater is true, each video page opened will default to theater mode.
  60. // This means the video will always be resized immediately if you are changing the size.
  61. // NOTE: YouTube will not always allow theater mode immediately, the page must be fully loaded first.
  62. const autoTheater = false;
  63.  
  64. // If flushBuffer is false, then the first second or so of the video may not always be the desired resolution.
  65. // If true, then the entire video will be guaranteed to be the target resolution, but there may be
  66. // a very small additional delay before the video starts if the buffer needs to be flushed.
  67. const flushBuffer = true;
  68.  
  69. // Setting cookies can allow some operations to perform faster or without a delay (e.g. theater mode)
  70. // Some people don't like setting cookies, so this is false by default (which is the same as old behavior)
  71. const allowCookies = false;
  72.  
  73. // --------------------
  74.  
  75.  
  76.  
  77.  
  78. // --- GLOBALS --------
  79.  
  80.  
  81. const DEBUG = false;
  82.  
  83. // Possible resolution choices (in decreasing order, i.e. highres is the best):
  84. const resolutions = ['highres', 'hd2880', 'hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny'];
  85. // youtube is always 16:9 right now, but has to be at least 480x270 for the player UI
  86. const heights = [4320, 2880, 2160, 1440, 1080, 720, 480, 360, 270, 270];
  87. const widths = [7680, 5120, 3840, 2560, 1920, 1280, 854, 640, 480, 480];
  88.  
  89. let doc = document, win = window;
  90.  
  91.  
  92. // --------------------
  93.  
  94.  
  95. function debugLog(message)
  96. {
  97. if (DEBUG)
  98. {
  99. console.log("YTHD | " + message);
  100. }
  101. }
  102.  
  103.  
  104. // --------------------
  105.  
  106.  
  107. // Used only for compatability with webextensions version of greasemonkey
  108. function unwrapElement(el)
  109. {
  110. if (el && el.wrappedJSObject)
  111. {
  112. return el.wrappedJSObject;
  113. }
  114. return el;
  115. }
  116.  
  117.  
  118. // --------------------
  119.  
  120.  
  121. // Get video ID from the currently loaded video (which might be different than currently loaded page)
  122. function getVideoIDFromURL(ytPlayer)
  123. {
  124. const idMatch = /(?:v=)([\w\-]+)/;
  125. let videoURL = ytPlayer.getVideoUrl();
  126. let id = idMatch.exec(videoURL)[1] || "ERROR: idMatch failed; youtube changed something";
  127.  
  128. return id;
  129. }
  130.  
  131.  
  132. // --------------------
  133.  
  134.  
  135. // Attempt to set the video resolution to desired quality or the next best quality
  136. function setResolution(ytPlayer, resolutionList)
  137. {
  138. debugLog("Setting Resolution...");
  139.  
  140. // Youtube doesn't return "auto" for auto, so set to make sure that auto is not set by setting
  141. // even when already at target res or above, but do so without removing the buffer for this quality
  142. if (resolutionList.indexOf(targetRes) >= resolutionList.indexOf(ytPlayer.getPlaybackQuality()))
  143. {
  144. if (ytPlayer.setPlaybackQualityRange !== undefined)
  145. {
  146. ytPlayer.setPlaybackQualityRange(targetRes, targetRes);
  147. }
  148. ytPlayer.setPlaybackQuality(targetRes);
  149. debugLog("Resolution Set To: " + targetRes);
  150. return;
  151. }
  152.  
  153. const end = resolutionList.length - 1;
  154. let nextBestIndex = Math.max(resolutionList.indexOf(targetRes), 0);
  155. let ytResolutions = ytPlayer.getAvailableQualityLevels();
  156. debugLog("Available Resolutions: " + ytResolutions.join(", "));
  157.  
  158. while ( (ytResolutions.indexOf(resolutionList[nextBestIndex]) === -1) && nextBestIndex < end )
  159. {
  160. ++nextBestIndex;
  161. }
  162.  
  163. if (flushBuffer && ytPlayer.getPlaybackQuality() !== resolutionList[nextBestIndex])
  164. {
  165. let id = getVideoIDFromURL(ytPlayer);
  166. if (id.indexOf("ERROR: ") === -1)
  167. {
  168. let pos = ytPlayer.getCurrentTime();
  169. ytPlayer.loadVideoById(id, pos, resolutionList[nextBestIndex]);
  170. }
  171.  
  172. debugLog("ID: " + id);
  173. }
  174. if (ytPlayer.setPlaybackQualityRange !== undefined)
  175. {
  176. ytPlayer.setPlaybackQualityRange(resolutionList[nextBestIndex], resolutionList[nextBestIndex]);
  177. }
  178. ytPlayer.setPlaybackQuality(resolutionList[nextBestIndex]);
  179.  
  180. debugLog("Resolution Set To: " + resolutionList[nextBestIndex]);
  181. }
  182.  
  183.  
  184. // --------------------
  185.  
  186.  
  187. // Set resolution, but only when API is ready (it should normally already be ready)
  188. function setResOnReady(ytPlayer, resolutionList)
  189. {
  190. if (ytPlayer.getPlaybackQuality === undefined)
  191. {
  192. win.setTimeout(setResOnReady, 100, ytPlayer, resolutionList);
  193. }
  194. else
  195. {
  196. setResolution(ytPlayer, resolutionList);
  197.  
  198. let storedQuality = localStorage.getItem("yt-player-quality");
  199. if (!storedQuality || storedQuality.indexOf(targetRes) === -1)
  200. {
  201. let tc = Date.now(), te = tc + 2592000000;
  202. localStorage.setItem("yt-player-quality","{\"data\":\"" + targetRes + "\",\"expiration\":" + te + ",\"creation\":" + tc + "}");
  203. }
  204. }
  205. }
  206.  
  207.  
  208. // --------------------
  209.  
  210.  
  211. function setTheaterMode(ytPlayer)
  212. {
  213. debugLog("Setting Theater Mode");
  214.  
  215. if (win.location.href.indexOf("/watch") !== -1)
  216. {
  217. let page = unwrapElement(doc.getElementById("page"));
  218. let pageManager = unwrapElement(doc.getElementsByTagName("ytd-watch")[0]);
  219.  
  220. if (ytPlayer && page)
  221. {
  222. // Wait until youtube has already set the page class, so it doesn't overwrite the theater mode change
  223. let isLoaded = doc.body.classList.contains("page-loaded");
  224. if (page.className.indexOf(getVideoIDFromURL(ytPlayer)) === -1 || !isLoaded)
  225. {
  226. win.setTimeout(setTheaterMode, 250, ytPlayer);
  227. }
  228. if (isLoaded)
  229. {
  230. page.classList.remove("watch-non-stage-mode");
  231. page.classList.add("watch-stage-mode", "watch-wide");
  232. win.dispatchEvent(new Event("resize"));
  233. }
  234. }
  235. else if (pageManager)
  236. {
  237. pageManager.setAttribute("theater", "true");
  238. pageManager.setAttribute("theater-requested_", "true");
  239. win.dispatchEvent(new Event("resize"));
  240. }
  241. }
  242. }
  243.  
  244.  
  245. // --------------------
  246.  
  247.  
  248. // resize the player
  249. function resizePlayer(width, height)
  250. {
  251. debugLog("Setting video player size");
  252.  
  253. let left, playlistTop, playlistHeight;
  254. left = (-width / 2);
  255. playlistTop = (height - 360);
  256. playlistHeight = (height - 100);
  257.  
  258. let styleContent = " \
  259. #page.watch-stage-mode .player-height, ytd-watch[theater] #player.style-scope { min-height: " + height + "px !important; } \
  260. #page.watch-stage-mode .player-width, ytd-watch[theater] #player.style-scope { min-width: " + width + "px !important; } \
  261. #page.watch-stage-mode .player-width { left: " + left + "px !important; } \
  262. #page.watch-stage-mode #watch-appbar-playlist { top: " + playlistTop + "px !important; } \
  263. #page.watch-stage-mode #playlist-autoscroll-list { max-height: " + playlistHeight + "px !important; } \
  264. ";
  265.  
  266. let ythdStyle = doc.getElementById("ythdStyleSheet");
  267. if (!ythdStyle)
  268. {
  269. ythdStyle = doc.createElement("style");
  270. ythdStyle.type = "text/css";
  271. ythdStyle.id = "ythdStyleSheet";
  272. ythdStyle.innerHTML = styleContent;
  273. doc.head.appendChild(ythdStyle);
  274. }
  275. else
  276. {
  277. ythdStyle.innerHTML = styleContent;
  278. }
  279.  
  280. win.dispatchEvent(new Event("resize"));
  281. }
  282.  
  283.  
  284. // --- MAIN -----------
  285.  
  286.  
  287. function main()
  288. {
  289. let ytPlayer = doc.getElementById("movie_player") || doc.getElementsByClassName("html5-video-player")[0];
  290. let ytPlayerUnwrapped = unwrapElement(ytPlayer);
  291.  
  292. if (autoTheater && ytPlayerUnwrapped)
  293. {
  294. if (allowCookies && doc.cookie.indexOf("wide=1") === -1)
  295. {
  296. doc.cookie = "wide=1; domain=.youtube.com";
  297. }
  298.  
  299. setTheaterMode(ytPlayerUnwrapped);
  300. }
  301.  
  302. if (changePlayerSize && win.location.host.indexOf("youtube.com") !== -1 && win.location.host.indexOf("gaming.") === -1)
  303. {
  304. let width, height;
  305. if (useCustomSize)
  306. {
  307. height = customHeight;
  308. width = customWidth;
  309. }
  310. else
  311. {
  312. // don't include youtube search bar as part of the space the video can try to fit in
  313. let heightOffsetEl = doc.getElementById("masthead-positioner-height-offset") || doc.getElementById("masthead");
  314. let mastheadContainerEl = doc.getElementById("yt-masthead-container") || doc.getElementById("masthead-container");
  315. let mastheadHeight = 50, mastheadPadding = 16;
  316. if (heightOffsetEl && mastheadContainerEl)
  317. {
  318. mastheadHeight = parseInt(win.getComputedStyle(heightOffsetEl).height, 10);
  319. mastheadPadding = parseInt(win.getComputedStyle(mastheadContainerEl).paddingBottom, 10) * 2;
  320. }
  321.  
  322. let i = Math.max(resolutions.indexOf(targetRes), 0);
  323. height = Math.min(heights[i], win.innerHeight - (mastheadHeight + mastheadPadding));
  324. width = Math.min(widths[i], win.innerWidth);
  325. }
  326.  
  327. resizePlayer(width, height);
  328. }
  329.  
  330. if (changeResolution && ytPlayerUnwrapped)
  331. {
  332. setResOnReady(ytPlayerUnwrapped, resolutions);
  333. }
  334.  
  335. if (changeResolution || autoTheater)
  336. {
  337. let curVid = "";
  338. win.addEventListener("loadstart", function(e) {
  339. if (!(e.target instanceof win.HTMLMediaElement))
  340. {
  341. return;
  342. }
  343.  
  344. ytPlayer = doc.getElementById("movie_player") || doc.getElementsByClassName("html5-video-player")[0];
  345. ytPlayerUnwrapped = unwrapElement(ytPlayer);
  346. if (ytPlayerUnwrapped)
  347. {
  348. let oldVid = curVid;
  349. curVid = getVideoIDFromURL(ytPlayerUnwrapped);
  350. if (curVid != oldVid)
  351. {
  352. debugLog("Loaded new video");
  353. if (changeResolution)
  354. {
  355. setResOnReady(ytPlayerUnwrapped, resolutions);
  356. }
  357. if (autoTheater)
  358. {
  359. setTheaterMode(ytPlayerUnwrapped);
  360. }
  361. }
  362. }
  363. }, true );
  364. }
  365.  
  366. win.removeEventListener("yt-navigate-finish", main, true );
  367. }
  368.  
  369. main();
  370. // Youtube doesn't load the page immediately in new version so you can watch before waiting for page load
  371. // But we can only set resolution until the page finishes loading
  372. win.addEventListener("yt-navigate-finish", main, true );
  373.  
  374. })();