Maximizer For YouTube™

Maximizes the YouTube player to fill the entire browser viewport when in theater mode, plus a few other enhancements.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        Maximizer For YouTube™
// @description Maximizes the YouTube player to fill the entire browser viewport when in theater mode, plus a few other enhancements.
// @license     MIT
// @author      Rotem Dan <[email protected]>
// @match       https://www.youtube.com/*
// @version     0.2.9
// @run-at      document-start
// @grant       none
// @namespace   https://github.com/rotemdan
// @homepageURL https://github.com/rotemdan/MaximizerForYouTube
// @require     https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js
// @require     https://code.jquery.com/jquery-3.7.1.min.js
// ==/UserScript==

////////////////////////////////////////////////////////////////////////////////////////////////////////
// Utility definitions
////////////////////////////////////////////////////////////////////////////////////////////////////////

const debugModeEnabled = false

function log(...args) {
	if (debugModeEnabled) {
		console.log('[MaximizerForYoutube]', ...args)
	}
}

// Try to emulate setImmediate() like execution:
function setImmediate(func) {
	const channel = new MessageChannel()
	channel.port1.onmessage = () => func()
	channel.port2.postMessage('')
}

////////////////////////////////////////////////////////////////////////////////////////////////////////
// Core script modification functions
////////////////////////////////////////////////////////////////////////////////////////////////////////

// Install or uninstall full-size player page stylesheet if needed
function installOrUninstallPlayerModIfNeeded() {
	if (inWatchPage() && theaterModeEnabled()) {
		if ($('#MaximizerForYouTube_PlayerMod').length == 0) {
			const styleSheet = `
					<style id='MaximizerForYouTube_PlayerMod' type='text/css'>
						ytd-page-manager { margin-top: 0px !important; }

						#masthead-container { visibility: hidden; opacity: 0; transition: opacity 0.2s ease-in-out; }

						#full-bleed-container { height: 100vh !important; min-height: 0vh !important; max-height: 100vh !important; }

						:focus { outline: 0; }

						#movie_player { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; }

						/*
						body {
							scrollbar-width: 10px;
							scrollbar-color: #000000;
						}
						*/

						body::-webkit-scrollbar {
							width: 10px;
							background-color: #000000;
						}

						body::-webkit-scrollbar-track {
							border-radius: 10px;
							background: rgba(0,0,0,0.1);
							border: 1px solid #383838;
						}

						body::-webkit-scrollbar-thumb {
							border-radius: 10px;
							background: linear-gradient(left, #fff, #e4e4e4);
							border: 1px solid #5b5b5b;
						}

						body::-webkit-scrollbar-thumb:hover {
							background: #fff;
						}
					</style>`

			$('head').append(styleSheet)

			log('Player mod installed')
		}

		if (pageScrolledToTop()) {
			hideTopBar()
		}
	} else {
		if ($('#MaximizerForYouTube_PlayerMod').length > 0) {
			$('#MaximizerForYouTube_PlayerMod').remove()
			showTopBar()

			log('Player mod uninstalled')
		}
	}
}

// Automatically shows/hides the top bar based on different properties of the view.
function installTopBarAutohide() {
	function onPageScroll() {
		if (!inWatchPage()) {
			return
		}

		if (pageScrolledToTop() && theaterModeEnabled()) {
			hideTopBar()
		} else {
			showTopBar()
		}
	}

	function onKeyDown(e) {
		if (!inWatchPage() || !theaterModeEnabled()) {
			return
		}

		if (e.which === 27) { // Handle escape key
			log('esc')

			if (pageScrolledToTop()) {
				if (topBarIsVisible()) {
					hideTopBar()
					e.stopPropagation()
				} else {
					showTopBar()

					setTimeout(() => $('input#search').focus(), 50)
				}
			}
		}
	}

	function installEscHandlerToSearchInput() {
		let inputElement = $('input#search')

		if (inputElement.length > 0) {
			inputElement.on('keydown', onKeyDown)
			//inputElement[0].addEventListener('keydown', onKeyDown, true)
			log('Esc handler installed on search input')
		} else {
			setTimeout(() => installEscHandlerToSearchInput(), 50)
		}
	}

	installEscHandlerToSearchInput()
	$(document).on('keydown', onKeyDown)
	$(document).on('scroll', onPageScroll)
}

// Continuously auto-focus the player keyboard input when some conditions are met.
function installPlayerInputAutoFocus() {
	function autoFocusIfNeeded() {
		if (inWatchPage() && !topBarIsVisible()) {
			getVideoContainer().focus()
		}

		setTimeout(autoFocusIfNeeded, 20)
	}

	autoFocusIfNeeded()
}

function installPlayerKeyboardShortcutExtensions() {
	// Install keyboard shortcut extensions
	function onKeyDown(e) {
		if (!inWatchPage()) {
			return
		}

		if (getVideoContainer().is(':focus')) {
			if (e.ctrlKey) {
				if (e.which === 37) { // Handle ctl + left key
					const previousButton = $('a.ytp-prev-button')[0]

					if (previousButton) {
						previousButton.click()
					}
				} else if (e.which === 39) { // Handle ctl + right key
					const nextButton = $('a.ytp-next-button')[0]

					if (nextButton) {
						nextButton.click()
					}
				}
			}
		}
	}

	$(document).on('keydown', onKeyDown)
}

// Expands video description
function ensureExpandedVideoDescription() {
	setInterval(() => {
		if (!inWatchPage()) {
			return
		}

		if ($('#ytd-watch-info-text').attr('detailed') == null) {
			$('#description-inline-expander tp-yt-paper-button#expand').click()
		}
	}, 50)
}

// Expands video description
function ensureModdedTheaterModeButton() {
	setInterval(() => {
		if (!inWatchPage()) {
			return
		}

		const playerModeButton = $('button.ytp-size-button')

		if (playerModeButton.length === 0 || playerModeButton.hasClass('MaximizerForYouTube_PlayerMod_Modded')) {
			return
		}

		playerModeButton.on('click', () => {
			setTimeout(() => {
				installOrUninstallPlayerModIfNeeded()

				const resizeEvent = new Event('resize')
				window.dispatchEvent(resizeEvent)
			}, 0)
		})

		playerModeButton.addClass('MaximizerForYouTube_PlayerMod_Modded')
	}, 50)
}

function ensurePlayerIsAlwaysPaused() {
	setInterval(() => {
		const player = getVideoPlayer().get(0)

		if (player) {
			player.pause()
		}
	}, 1000)
}

function hideSPFLoadingBar() {
	$('head').append('<style>#progress, yt-page-navigation-progress {display: none !important}</style>')
}

// Pauses playing videos in other tabs when a video play event is detected (works in both watch and channel page videos)
function ensurePlayerAutoPause() {
	const videoPlayer = getVideoPlayer()

	if (videoPlayer.length > 0 &&
		!videoPlayer.hasClass('MaximizerForYouTube_Modded_Autopause') &&
		!inHomePage() &&
		!inFeedPage() &&
		!inSearchPage()) {
		// Generate a random script instance ID
		const instanceID = Math.random().toString()

		function onVideoPlay() {
			log('onVideoPlay')

			localStorage['MaximizerForYouTube_PlayingInstanceID'] = instanceID

			function pauseWhenAnotherPlayerStartsPlaying() {
				if (localStorage['MaximizerForYouTube_PlayingInstanceID'] !== instanceID) {
					videoPlayer[0].pause()
				} else {
					setTimeout(pauseWhenAnotherPlayerStartsPlaying, 20)
				}
			}

			pauseWhenAnotherPlayerStartsPlaying()
		}

		// If video isn't paused on startup, fire the handler immediately
		if (!videoPlayer[0].paused) {
			onVideoPlay()
		}

		// Add event handler for the 'play' event.
		videoPlayer.on('play', onVideoPlay)

		// Mark the player as modded to ensure the autopause mod isn't installed again
		videoPlayer.addClass('MaximizerForYouTube_Modded_Autopause')
	}

	setTimeout(ensurePlayerAutoPause, 50)
}

////////////////////////////////////////////////////////////////////////////////////////////////////////
// Utility functions
////////////////////////////////////////////////////////////////////////////////////////////////////////

// Get the video player element
function getVideoContainer() {
	// Note: the channel page has another hidden video except the main one (if it exists). The hidden video doesn't have an 'src' attribute.
	return $('div.html5-video-player')
}

// Get the video player element
function getVideoPlayer() {
	// Note: the channel page has another hidden video except the main one (if it exists). The hidden video doesn't have an 'src' attribute.
	return $('.html5-main-video').filter(function (index) {
		return $(this).attr('src') !== undefined
	})
}

// Get the top bar element
function getTopBar() {
	return $('#masthead-container')
}

function showTopBar() {
	getTopBar().css('visibility', 'visible')
	getTopBar().css('opacity', '1')
}

function hideTopBar() {
	getTopBar().css('opacity', '0')
	getTopBar().css('visibility', 'hidden')
}

function pageScrolledToTop() {
	return $(document).scrollTop() === 0
}

function scrollPageToTopIfNeeded() {
	setTimeout(() => {
		if (inWatchPage() && $(document).scrollTop() > 0) {
			log('Scrolling page to top')

			$(document).scrollTop(0)
		}
	}, 20)
}

function topBarIsVisible() {
	return getTopBar().css('visibility') === 'visible'
}

function inWatchPage() {
	return locationPathname() == '/watch'
}

function inSearchPage() {
	return locationPathname().startsWith('/results')
}

function inFeedPage() {
	return locationPathname().startsWith('/feed')
}

function inHomePage() {
	return locationPathname() == '/'
}

function locationPathname() {
	return (new URL(location.href)).pathname
}

function theaterModeEnabled() {
	return Cookies.get('wide') === '1'
}

////////////////////////////////////////////////////////////////////////////////////////////////////////
// Event handlers
////////////////////////////////////////////////////////////////////////////////////////////////////////

let installInterval

function onDocumentStart() {
	log('onDocumentStart')

	log('Script loaded, theater mode enabled:', theaterModeEnabled())

	//installOrUninstallPlayerModIfNeeded()

	installInterval = setInterval(() => {
		installOrUninstallPlayerModIfNeeded()
		//log('Trying to install mod')
	}, 10)
}

function onDocumentEnd() {
	log('onDocumentEnd')

	clearInterval(installInterval)
	installOrUninstallPlayerModIfNeeded()

	//ensurePlayerIsAlwaysPaused()

	hideSPFLoadingBar()
	installTopBarAutohide()
	installPlayerInputAutoFocus()
	installPlayerKeyboardShortcutExtensions()
	ensureExpandedVideoDescription()
}

function onWindowLoad() {
	log('onWindowLoad')

	ensureModdedTheaterModeButton()
	ensurePlayerAutoPause()
}

function onNavigation() {
	log('onNavigation, new location:', location.href)

	//scrollPageToTopIfNeeded()
	installOrUninstallPlayerModIfNeeded()
}

////////////////////////////////////////////////////////////////////////////////////////////////////////
// Install event handlers and start script
////////////////////////////////////////////////////////////////////////////////////////////////////////

function startScript() {
	document.addEventListener('DOMContentLoaded', onDocumentEnd, false)

	$(window).on('load', onWindowLoad)

	$(window).on('yt-navigate-start', () => {
		log('yt-navigate-start')
		onNavigation()
	})

	$(window).on('popstate', () => {
		log('popstate')
		onNavigation()
	})

	onDocumentStart()
}

if (window.self === window.top) {
	startScript()
}