YouTube Cinema Mode

Maximizes YouTube's video player to fill the entire browser viewport and fixes a few minor annoyances

  1. // ==UserScript==
  2. // @name YouTube Cinema Mode
  3. // @description Maximizes YouTube's video player to fill the entire browser viewport and fixes a few minor annoyances
  4. // @license MIT
  5. // @author Rotem Dan <rotemdan@gmail.com>
  6. // @match https://www.youtube.com/*
  7. // @version 0.2.4
  8. // @run-at document-start
  9. // @grant none
  10. // @require https://code.jquery.com/jquery-2.1.4.min.js
  11. // @namespace https://github.com/rotemdan
  12. // @homepageURL https://github.com/rotemdan/YouTubeCinemaMode
  13. // ==/UserScript==
  14.  
  15. ////////////////////////////////////////////////////////////////
  16. // Main Operations
  17. ////////////////////////////////////////////////////////////////
  18.  
  19. // Detect the type of YouTube page loaded
  20. var isWatchPage = location.href.indexOf("https://www.youtube.com/watch?") === 0;
  21.  
  22. // Start or install operation handlers
  23. onDocumentStart();
  24. $(document).on("ready", onDocumentEnd);
  25. $(window).on("load", onWindowLoad);
  26.  
  27. // Operations that are run as early as possible during page load
  28. function onDocumentStart() {
  29. installUnsafewindowPolyfill();
  30. disableSPF();
  31.  
  32. if (isWatchPage) {
  33. disableMatchMedia();
  34. installFullSizePlayerStylesheet();
  35. }
  36. }
  37.  
  38. // Operations that run when DOMContentLoaded event is fired
  39. function onDocumentEnd() {
  40. if (isWatchPage) {
  41. installTopBarAutohide();
  42. expandVideoDescription();
  43. installPlayerAutoFocus();
  44. installPlayerKeyboardShortcutExtensions();
  45. installPlaylistRepositioner();
  46. }
  47.  
  48. installPlayerAutoPause();
  49. }
  50.  
  51. // Operations that run when window is loaded (after YouTube scripts are run)
  52. function onWindowLoad() {
  53. if (isWatchPage) {
  54. setTimeout(switchPlayerToTheaterMode, 2000);
  55. }
  56. }
  57.  
  58. ////////////////////////////////////////////////////////////////
  59. // Methods
  60. ////////////////////////////////////////////////////////////////
  61.  
  62. // Ensure unsafeWindow object is available both in Firefox and Chrome
  63. function installUnsafewindowPolyfill() {
  64. if (typeof unsafeWindow === 'undefined') {
  65. if (typeof XPCNativeWrapper === 'function' && typeof XPCNativeWrapper.unwrap === 'function')
  66. unsafeWindow = XPCNativeWrapper.unwrap(window);
  67. else if (window.wrappedJSObject)
  68. unsafeWindow = window.wrappedJSObject;
  69. }
  70. }
  71.  
  72. // Disable SPF (Structured Page Fragments), which prevents properly attaching to page load events when navigation occurs
  73. // Will also disable the red loading bar.
  74. function disableSPF() {
  75. if (unsafeWindow._spf_state && unsafeWindow._spf_state.config) {
  76. unsafeWindow._spf_state.config['navigate-limit'] = 0;
  77. unsafeWindow._spf_state.config['navigate-part-received-callback'] = function (targetUrl) { location.href = targetUrl; }
  78. }
  79.  
  80. setTimeout(disableSPF, 50);
  81. }
  82.  
  83. // Disable matchMedia - allow proper resizing of the video player and its contents.
  84. function disableMatchMedia() {
  85. window.matchMedia = undefined;
  86. }
  87.  
  88. // Add full-size player page stylesheet (works correctly only when the player is in "theater" mode).
  89. function installFullSizePlayerStylesheet() {
  90. var runningInChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
  91.  
  92. var styleSheet =
  93. "<style type='text/css'>\n" +
  94. "body { overflow-x: hidden !important; }\n" +
  95. "#masthead-positioner-height-offset { display: none }\n" +
  96. "#masthead-positioner { visibility: hidden; opacity: 0; transition: opacity 0.2s ease-in-out; }\n" +
  97. ".player-width { width: 100% !important; margin: 0px !important; left: 0px !important; right: 0px !important; }\n" +
  98. ".player-height { height: 100vh !important; }\n" +
  99. ":focus { outline: 0; }\n";
  100.  
  101. //if (!runningInChrome)
  102. styleSheet += "body { overflow-y: scroll !important; }\n";
  103.  
  104. styleSheet += "</style>\n";
  105.  
  106. $("head").append(styleSheet);
  107. }
  108.  
  109. // Switch player to theater mode if in default mode.
  110. function switchPlayerToTheaterMode() {
  111. if ($("div#page").hasClass("watch-non-stage-mode"))
  112. $("button.ytp-size-button, div.ytp-size-toggle-large").click();
  113. }
  114.  
  115. // Automatically shows/hides the top bar based on different properties of the view.
  116. function installTopBarAutohide() {
  117. var topBar = getTopBar();
  118. var videoPlayer = getVideoPlayer();
  119. var videoPlayerElement = videoPlayer[0];
  120.  
  121. function topBarIsVisible() {
  122. return topBar.css("visibility") === "visible";
  123. }
  124.  
  125. function showTopBar() {
  126. topBar.css("visibility", "visible");
  127. topBar.css("opacity", "1");
  128. }
  129.  
  130. function hideTopBar() {
  131. topBar.css("opacity", "0");
  132. topBar.css("visibility", "hidden");
  133. }
  134.  
  135. function toggleTopBar() {
  136. if (topBarIsVisible())
  137. hideTopBar();
  138. else
  139. showTopBar();
  140. }
  141.  
  142. function getScrollTop() {
  143. return $(document).scrollTop();
  144. }
  145.  
  146. function onPageScroll() {
  147. if (getScrollTop() > 0)
  148. showTopBar();
  149. else
  150. hideTopBar();
  151. }
  152.  
  153. var searchInput = $("input#masthead-search-term");
  154. function onKeyDown(e) {
  155. if (e.which === 27) { // Handle escape key
  156. if (getScrollTop() === 0) {
  157. toggleTopBar();
  158.  
  159. if (topBarIsVisible())
  160. setTimeout(function () { searchInput.focus() }, 200);
  161.  
  162. e.stopPropagation();
  163. }
  164. }
  165. }
  166.  
  167. searchInput.on("keydown", onKeyDown);
  168. $(document).on("keydown", onKeyDown);
  169. $(document).on("scroll", onPageScroll);
  170. }
  171.  
  172. // Continuously auto-focus the player when some conditions are met.
  173. function installPlayerAutoFocus() {
  174. function autoFocusIfNeeded() {
  175. var topbarVisibility = getTopBar().css("visibility");
  176. if (topbarVisibility !== undefined) {
  177. if (topbarVisibility === "hidden") {
  178. $(".html5-video-player").focus();
  179. }
  180. //else if ($(".html5-video-player").is(":focus") && $(document).scrollTop() > 0)
  181. //{
  182. // $(".html5-video-player").blur();
  183. //}
  184. }
  185.  
  186. setTimeout(autoFocusIfNeeded, 20);
  187. }
  188.  
  189. autoFocusIfNeeded();
  190. }
  191.  
  192. function installPlayerKeyboardShortcutExtensions() {
  193. // Install keyboard shortcut extensions
  194. function onPlayerKeyDown(e) {
  195. if ($(".html5-video-player").is(":focus")) {
  196. if (e.ctrlKey) {
  197. if (e.which === 37) { // Handle ctl + left key
  198. var previousButton = $("a.ytp-prev-button, div.ytp-button-prev")[0];
  199. if (previousButton)
  200. previousButton.click();
  201. }
  202. else if (e.which === 39) { // Handle ctl + right key
  203. var nextButton = $("a.ytp-next-button, div.ytp-button-next")[0]
  204. if (nextButton)
  205. nextButton.click();
  206. }
  207. }
  208. }
  209. }
  210.  
  211. $(document).on("keydown", onPlayerKeyDown);
  212.  
  213. $(".html5-video-player").on("keydown", function (e) {
  214. if (e.which === 38 || e.which === 40) {
  215. console.log("up or down");
  216. e.stopPropagation();
  217. }
  218. });
  219. }
  220.  
  221. // Expands video description
  222. function expandVideoDescription() {
  223. $("#action-panel-details").removeClass("yt-uix-expander-collapsed");
  224. }
  225.  
  226. // Correct the positioning of the playlist on window resize.
  227. function installPlaylistRepositioner() {
  228. $("#watch-appbar-playlist").removeClass("player-height");
  229.  
  230. function onResize() {
  231. var playlistContainer = $("#watch-appbar-playlist");
  232.  
  233. if (playlistContainer.length === 0)
  234. return;
  235.  
  236. var playlistOffset = playlistContainer.offset();
  237. playlistOffset.top = $("#watch-header").offset().top;
  238. playlistContainer.offset(playlistOffset);
  239. }
  240.  
  241. onResize();
  242. $(window).resize(onResize);
  243. }
  244.  
  245. // Pauses playing videos in other tabs when a video play event is detected (works in both watch and channel page videos)
  246. function installPlayerAutoPause() {
  247. var videoPlayer = getVideoPlayer();
  248.  
  249. if (videoPlayer.length === 0) {
  250. //console.log("Player not found, retrying in 100ms..");
  251. setTimeout(installPlayerAutoPause, 100);
  252. return;
  253. }
  254.  
  255. var videoPlayerElement = videoPlayer.get(0);
  256.  
  257. // Generate a random script instance ID
  258. var instanceID = Math.random().toString();
  259.  
  260. function onVideoPlay() {
  261. localStorage["YouTubeCinemaMode_PlayingInstanceID"] = instanceID;
  262.  
  263. function pauseWhenAnotherPlayerStartsPlaying() {
  264. if (localStorage["YouTubeCinemaMode_PlayingInstanceID"] !== instanceID)
  265. videoPlayerElement.pause();
  266. else
  267. setTimeout(pauseWhenAnotherPlayerStartsPlaying, 10);
  268. }
  269.  
  270. pauseWhenAnotherPlayerStartsPlaying();
  271. }
  272.  
  273. // If video isn't paused on startup, fire the handler immediately
  274. if (!videoPlayerElement.paused)
  275. onVideoPlay();
  276.  
  277. // Add event handler for the "play" event.
  278. videoPlayer.on("play", onVideoPlay);
  279. }
  280.  
  281. // Get the video player element
  282. function getVideoPlayer() {
  283. // Note: the channel page has another hidden video except the main one (if it exists). The hidden video doesn't have an "src" attribute.
  284. return $('video.html5-main-video').filter(function (index) { return $(this).attr("src") !== undefined });
  285. }
  286.  
  287. // Get the top bar element
  288. function getTopBar() {
  289. return $("#masthead-positioner");
  290. }