- // ==UserScript==
- // @name YouTube Ultrawide
- // @version 0.1
- // @description Crops the top and bottom to fit ultrawide
- // @author uxamend (modified by C4illin)
- // @match https://www.youtube.com/*
- // @match https://www.youtube-nocookie.com/embed/*
- // @exclude-match https://www.youtube.com/ad_frame*
- // @exclude-match https://www.youtube.com/ad_companion*
- // @exclude-match https://www.youtube.com/embed/
- // @exclude-match https://www.youtube.com/video_masthead*
- // @icon https://raw.githubusercontent.com/C4illin/Userscripts/master/Youtube-Ultrawide/icon.png
- // @grant none
- // @run-at document-idle
- // @license CC0-1.0
- // @compatible firefox version >=64 (older versions untested)
- // @compatible chrome version >=71 (older versions untested)
- // @namespace https://gf.qytechs.cn/users/217869
- // ==/UserScript==
-
- // 100% credit to uxamend: (I just changed a couple values to reverse everything)
- // https://gf.qytechs.cn/sv/scripts/375648-zoom-and-crop-youtube-videos-to-fill-screen-height
-
- "use strict"
-
- // ==================================
- // User-defined global parameters
- // ==================================
-
- // For wide screens: Sets the widest aspect ratio that videos will ever be
- // cropped to.
- var max_cropped_aspect = 21/9 // Express as a fraction, e.g. 21/9, not 21:9.
-
- // Sets the aspect ratio of the player for using zoom outside of full-screen.
- // This has no effect in full-screen mode.
- // To use the screen aspect ratio as the player aspect ratio, set this to zero.
- var player_aspect = 0 // Set to zero or a fraction, e.g. 16/9, not 16:9.
-
- // Default zoom state outside full-screen (true = enabled, false = disabled)
- var def_zoom_n = false
-
- // Default zoom state in full-screen (true = enabled, false = disabled)
- var def_zoom_f = false
-
- // Sets which key will be used to enable and disable zoom
- var zoom_shortcut_key = "z"
-
- // ==================================
-
-
- var debug_logging_on = false
- var debug_script_name = "Userscript: Zoom YouTube videos to fill screen width"
-
- function debug_log(message) {
- if(debug_logging_on){
- console.log("[" + debug_script_name + "] " + message)
- }
- }
-
- /**
- * set_zoom
- * Zooms a specified video to fill specified containing area dimensions.
- * Parameters:
- * video video to zoom
- * cw containing area width
- * ch containing area height
- * Global parameters read:
- * max_cropped_aspect minimum aspect ratio to crop videos down to
- */
- function set_zoom(video, cw, ch) {
- var vs = video.style
- var video_aspect = video.videoWidth/video.videoHeight
- var containing_aspect = cw/ch
-
- // Don't zoom if the endscreen is showing
- if(!video.ended) {
- // Only zoom and crop videos that are wide enough to crop
- if(video_aspect < containing_aspect && video_aspect < max_cropped_aspect) {
- debug_log("Video is wider than containing area and max_cropped_aspect. Setting zoom.")
-
- var vw = cw // vh = video height
-
- // Apply max_cropped_aspect constraint to video width
- if (max_cropped_aspect < containing_aspect) {
- vh = cw/max_cropped_aspect
- debug_log("max_cropped_aspect reached: " + max_cropped_aspect)
- }
-
- var vh = video_aspect * vw // vw = video width (including cropped portion)
-
- var vt = (ch-vh)/2 // vt = top edge position of video
- var vl = (cw-vw)/2 // vl = left edge position of video
-
- debug_log("Containing area dimensions: " + cw + "x" + ch + "." + "Aspect: " + containing_aspect)
- // debug_log("Calculated new video element dimensions: " + vw + "x" + vh + ", origin at " + vl + ", " + vt + ".")
- // debug_log("(Underlying video stream resolution: " + video.videoWidth + "x" + video.videoHeight + ".)")
- debug_log("max_cropped_aspect: " + max_cropped_aspect + "." )
- debug_log("video_aspect: " + video_aspect + ".")
-
- // This might appear to risk creating an endless loop via the mutation
- // observer. However, it doesn't. I'm guessing that changing the dimensions
- // doesn't constitute a mutation, but if it does it can result in at most one
- // superfluous execution of set_zoom(). If the first execution causes a
- // mutation by changing the video element's dimensions, then the second
- // execution, if it is surplus to requirements, should set them to the same
- // values, resulting in no mutation and no third execution (until genuinely
- // needed).
- vs.height = vh + "px"
- vs.width = vw + "px"
- vs.top = vt + "px"
- vs.left = vl + "px"
-
- } else {
- debug_log("Video is not wide enough to require zoom ("
- + video.videoWidth
- + "x"
- + video.videoHeight
- + "). Not setting zoom.")
- unzoom(video, cw, ch)
- }
-
- } else {
- debug_log("Video has ended. Not setting zoom. (Otherwise we mess with the endscreen.)")
- }
- }
-
- /**
- * unzoom
- * Undoes the visual effects of set_zoom.
- * Note that unzoom does not gurantee to return the video dimensions exactly to
- * their original values, but the visual appearance should be the same (or near
- * enough as makes no odds).
- * Parameters:
- * video video to unzoom
- * cw containing area width
- * ch containing area height
- */
- function unzoom(video, cw, ch) {
- // It would be better to somehow trigger YouTube's standard video sizing, but
- // in the absence of a way to trigger that, we'll just do this.
-
- var vs = video.style
- var video_aspect = video.videoWidth/video.videoHeight
- var containing_aspect = cw/ch
-
- // Don't unzoom if the endscreen is showing
- if(!video.ended) {
- debug_log("Unzooming video.")
-
- // Usually the player is sized to fit the video exactly in default view,
- // but not for narrow videos, which are pillarboxed with white bars. Rarely,
- // the player defaults to 16:9 for all videos, so that wide videos are
- // letterboxed with white bars.
- //
- // In theater mode and full-screen mode, the player has a fixed aspect and
- // the video is letter- or pillarboxed with black bars if it doesn't fit
- // exactly.
- //
- // To avoid black bars in default view, we must size the video to fill the
- // container in the video's longest dimension only. (Otherwise we could
- // just size it to fill the container in both dimensions.)
- var w, h, t, l
- if(video_aspect == containing_aspect) {
- // video that fits the container exactly
- w = cw; h = ch; t = 0; l = 0
- } else if(video_aspect > containing_aspect) {
- // letterboxed video
- w = cw; l = 0
- h = cw / video_aspect
- t = (ch - h) / 2
- } else {
- // pillarboxed video
- h = ch; t = 0
- w = ch * video_aspect
- l = (cw - w) / 2
- }
-
- vs.width = w + "px"
- vs.height = h + "px"
- vs.top = t + "px"
- vs.left = l + "px"
- } else {
- debug_log("Video has ended. Not unzooming. (Otherwise we mess with the endscreen.)")
- }
- }
-
- /**
- * in_theater_mode
- * Returns true if we're in Theater mode.
- */
- function in_theater_mode() {
- return (document.getElementById("player-theater-container") &&
- document.getElementById("player-theater-container").childElementCount > 0 &&
- !document.fullscreenElement)
- }
-
- /**
- * set_player_aspect
- * Changes the aspect ratio of the video player element to the specified aspect
- * ratio, as interpreted by YouTube's default CSS.
- * Parameters:
- * aspect aspect ratio to use
- * theater_default if true, set theater mode to the default aspect ratio
- * instead of the specified aspect ratio
- */
- function set_player_aspect(aspect, theater_default=false) {
- debug_log("Setting player aspect to " + aspect + ".")
-
- // We need to set overflow to hidden on the movie-player otherwise the video
- // overhangs in miniplayer mode. Get it by class name rather than id, for
- // compatibility with embedded videos
- document.getElementsByClassName("html5-video-player")[0].style.setProperty("overflow", "hidden")
-
- // For embedded videos, we don't need to do anything.
-
- // For default view
- if(document.getElementsByTagName("ytd-watch-flexy")[0]) {
- var ytdwfs = document.getElementsByTagName("ytd-watch-flexy")[0].style
- ytdwfs.setProperty("--ytd-watch-flexy-width-ratio", aspect)
- ytdwfs.setProperty("--ytd-watch-flexy-height-ratio", 1)
- }
-
- // For theater mode
- var ptc = document.getElementById("player-theater-container")
-
- if(in_theater_mode() && !theater_default) {
- debug_log("Setting theater mode height.")
- // 56px for masthead; --ytd-masthead-height is not always set, so can't use
- // that unfortunately
- ptc.style.setProperty("max-height", "calc(100vh - 56px)")
- ptc.style.setProperty("height", "calc((" + (1/aspect) + ") * 100vw)")
- } else {
- debug_log("Unsetting theater mode height.")
- if(ptc) {
- ptc.style.removeProperty("max-height")
- ptc.style.removeProperty("height")
- }
- }
- }
-
- /**
- * apply_player_aspect
- * To facilitate zoom and crop when the movie_player is not full-screen, this sets
- * the aspect ratio of the movie_player to follow the player_aspect setting.
- * Calling with the zoom parameter set to false returns the player to the YouTube
- * default of matching the video aspect ratio.
- * Parameters:
- * zoom if true, use player_aspect
- * if false, use the actual video aspect (YouTube default)
- * Global parameters read:
- * player_aspect the aspect ratio to use, or zero (indicating to use the
- * screen aspect)
- */
- function apply_player_aspect(zoom=true) {
- var video = document.getElementsByClassName("html5-main-video")[0]
- var video_aspect = video.videoWidth/video.videoHeight
-
- if(zoom) {
- if(player_aspect == 0) {
- debug_log("Adjusting player aspect ratio to match screen.")
- var screen_aspect = window.screen.width/window.screen.height
- if(video_aspect > screen_aspect) {
- set_player_aspect(screen_aspect)
- } else {
- debug_log("No need to change player aspect; video is not wide enough.")
- }
- } else {
- debug_log("Adjusting player aspect ratio to configured value.")
- if(video_aspect > player_aspect) {
- set_player_aspect(player_aspect)
- } else {
- debug_log("No need to change player aspect; video is not wide enough.")
- }
- }
- } else {
- debug_log("Restoring player aspect ratio to match video.")
- set_player_aspect(video_aspect, true)
- // N.B. If video_aspect is narrow, the expected behaviour of set_player_aspect
- // is that YouTube's CSS may result in the video being pillarboxed, due to the
- // maximum height constraint.
- }
- }
-
- /**
- * set_zoom_to_window
- * Zooms a video to fill the window dimensions.
- * Parameters:
- * video the video to set zoom for
- * zoom if false, will unzoom instead of setting zoom
- * Global parameters read:
- * max_cropped_aspect minimum aspect ratio to crop videos down to
- */
- function set_zoom_to_window(video, zoom=true) {
- if(zoom) {
- set_zoom(video,
- window.innerWidth,
- window.innerHeight)
- } else {
- unzoom(video,
- window.innerWidth,
- window.innerHeight)
- }
- }
-
- /**
- * set_zoom_to_movie_player
- * Zooms a video to fill its containing movie_player element in the DOM. When not in
- * full-screen mode, also changes the size of the movie_player to follow the
- * player_aspect setting (else there'll be no zoom and crop).
- * Parameters:
- * video the video to set zoom for
- * zoom if false, will unzoom instead of setting zoom
- * Global parameters read:
- * max_cropped_aspect minimum aspect ratio to crop videos down to
- * player_aspect aspect ratio setting for non-full-screen movie_player
- */
- function set_zoom_to_movie_player(video, zoom=true) {
- if(!document.fullscreenElement) {
- // The movie_player is the grandparent node of the video element.
- // Open question: Is it more likely for the ID of the relevant element to
- // change (so that selecting it as the grandparent is the best strategy), or
- // for its position in the DOM tree to change (so that selecting it by ID is
- // the best strategy)?
- if(zoom) {
- apply_player_aspect(true)
- set_zoom(video,
- video.parentNode.parentNode.clientWidth,
- video.parentNode.parentNode.clientHeight)
- } else {
- unzoom(video,
- video.parentNode.parentNode.clientWidth,
- video.parentNode.parentNode.clientHeight)
- }
- } else {
- apply_player_aspect(false)
-
- // In full-screen mode, the movie-player is not necessarily the same size as
- // the screen, which can cause a slight offset. Use set_zoom_to_window instead
- // for this case.
- set_zoom_to_window(video, zoom)
- }
- zoom_button.update()
- }
-
- /**
- * mo_callback
- * Callback function for mutation observer, to re-apply zoom if the video element
- * mutates. E.g. when an ad starts or stops playing, or in other circumstances
- * when the video might change dimensions or become reset to its default,
- * letterboxed state.
- */
- function mo_callback(mutation_list, observer) {
- mutation_list.forEach((mutation) => {
- if(mutation.type == "attributes"){
-
- // We have to check whether zoom "should" be on, because the
- // fullscreenchange event may not be fast enough, in which case we will
- // catch the mutations caused by exiting full-screen.
- if(zoom_should_be_on()) {
- debug_log("Video element mutated.")
- set_zoom_to_movie_player(mutation.target)
- } else {
- debug_log("Video element mutated but zoom should be off.")
- zoom_off()
- }
- }
- })
- }
-
- var mo = new MutationObserver(mo_callback)
-
- function observe_video_mutations(video) {
- mo.observe(video, {"attributes" : true})
- }
-
- /**
- * zoom_on
- * Unconditionally apply zoom and keep it applied until zoom_off is called.
- */
- function zoom_on() {
- debug_log("Turning zoom on.")
- var video = document.getElementsByClassName("html5-main-video")[0]
- set_zoom_to_movie_player(video)
- observe_video_mutations(video)
- }
-
- /**
- * zoom_off
- * Unconditionally stop applying zoom, until zoom_on is called.
- */
- function zoom_off() {
- debug_log("Turning zoom off.")
- mo.disconnect()
- var video = document.getElementsByClassName("html5-main-video")[0]
- apply_player_aspect(false)
- set_zoom_to_movie_player(video, false)
- }
-
- // Manual zoom state outside full-screen
- var man_enab_n = def_zoom_n
-
- // Manual zoom state in full-screen
- var man_enab_f = def_zoom_f
-
- /**
- * zoom_should_be_on
- * Returns true if we're in a state where zoom is supposed to currently be
- * enabled, else false.
- */
- function zoom_should_be_on() {
- return ((man_enab_n && !document.fullscreenElement)
- || (man_enab_f && document.fullscreenElement))
- }
-
- /**
- * zoom_on_or_off
- * Puts zoom into the correct on/off state, as per zoom_should_be_on.
- */
- function zoom_on_or_off() {
- if(zoom_should_be_on()) {
- setTimeout(zoom_on, 200)
- } else {
- zoom_off()
- }
- }
-
- /**
- * toggle_manual_enab
- * Changes the manual override zoom state for the current display mode; either
- * full-screen or non-full-screen.
- */
- function toggle_manual_enab() {
- debug_log("Toggling manual enable state.")
- if(document.fullscreenElement){
- man_enab_f = !man_enab_f
- if(man_enab_f) debug_log("Set full-screen zoom enabled.")
- else debug_log("Set full-screen zoom disabled.")
- } else {
- man_enab_n = !man_enab_n
- if(man_enab_n) debug_log("Set non-full-screen zoom enabled.")
- else debug_log("Set non-full-screen zoom disabled.")
- }
- zoom_on_or_off()
- }
-
- /**
- * handle_keydown
- * Event handler for any keydown events, to trigger appropriate actions.
- */
- function handle_keydown(e) {
- debug_log('"' + e.key + '" key was pressed.')
- if(e.key == zoom_shortcut_key.toLowerCase()) toggle_manual_enab()
- if(e.key == zoom_shortcut_key.toUpperCase()) toggle_manual_enab()
- if(e.key == "i") {
- // Workaround for bug: exiting miniplayer directly into normal view does not
- // trigger reapplication of zoom. Pressing 'i' seems to be the only way to
- // trigger this bug, so detecting the pressing of 'i' seems like a good way
- // to fix it.
- zoom_on_or_off()
- }
- }
-
- /**
- * watch_for_fullscreen
- * Start watching for changes to the full-screen state, and make sure the correct
- * zoom state is applied at each transition of full-screen state.
- * N.B. There may be a slight delay in reaction to changes in full-screen state.
- */
- function watch_for_fullscreen() {
- debug_log("Adding fullscreenchange event listener.")
- document.addEventListener(
- 'fullscreenchange',
- function() {
- debug_log("Full-screen state changed.")
- zoom_on_or_off()
- }
- )
- }
-
- /**
- * watch_for_keypresses
- * Start watching for keydown events and handle them when they occur.
- */
- function watch_for_keypresses() {
- debug_log("Adding keydown event listener.")
- document.addEventListener(
- 'keydown',
- handle_keydown
- )
- }
-
- /**
- * create_zoom_button
- * Adds a zoom button to the YouTube player controls, which toggles manual override of
- * zoom state.
- * Returns:
- * an object representing the button
- */
- function create_zoom_button() {
- var right_controls
- var size_button
- var tooltip_showing = false
- var button
- var icon_path
-
- /**
- * set_zoom_button_mode
- * Sets the zoom button to an appropriate mode for the current zoom state.
- */
- function set_zoom_button_mode() {
- var l
-
- if(zoom_should_be_on()) {
- icon_path.setAttribute("d",
- "m 8,11 0,14 20,0 0,-14 -20,0 z m 2,4 16,0 0,6 -16,0 0,-6 z"
- )
- l = "Normal (" + zoom_shortcut_key + ")"
- } else {
- icon_path.setAttribute("d",
- "m 4,11 0,14 3,0 0,-2 -1,0 0,-10 1,0 0,-2 -3,0 z\
- m 4,0 0,14 20,0 0,-14 -20,0 z\
- m 21,0 0,2 1,0 0,10 -1,0 0,2 3,0 0,-14 -3,0 z\
- m -19,2 16,0 0,10 -16,0 0,-10 z"
- )
- l = "Widescreen (" + zoom_shortcut_key + ")"
- }
-
- button.setAttribute("aria-label", l)
- button.setAttribute("title", l)
- }
-
- /**
- * create_zoom_button_icon
- * Adds the icon to the zoom button during initial creation of the button.
- */
- function create_zoom_button_icon() {
- // Create icon SVG element
- var s = document.createElementNS("http://www.w3.org/2000/svg", "svg")
- s.setAttribute("height", "100%")
- s.setAttribute("version", "1.1")
- s.setAttribute("viewBox", "0 0 36 36")
- s.setAttribute("width", "100%")
-
- var p_id = "zac-path-1"
-
- // Apply shadow
- var sh = document.createElementNS("http://www.w3.org/2000/svg", "use")
- sh.setAttribute("class", "ytp-svg-shadow")
- sh.setAttribute("href", "#" + p_id)
-
- // Create icon path
- var p = document.createElementNS("http://www.w3.org/2000/svg", "path")
- p.setAttribute("id", p_id)
- p.setAttribute("class", "ytp-svg-fill")
-
- // Append path and shadow to SVG
- s.appendChild(sh)
- s.appendChild(p)
-
- // Append icon to button
- button.appendChild(s)
-
- icon_path = p
- }
-
- /**
- * show_zoom_button_tooltip
- * Shows the tooltip associated with the zoom button in a style mimicking that
- * of the other YouTube player buttons.
- * Parameters:
- * show if false, the tooltip will be hidden instead of shown
- */
- function show_zoom_button_tooltip(show=true) {
- // Position calculations
- var bbcr = button.getBoundingClientRect()
- var tt_horiz_cen = bbcr.left + bbcr.width/2 // tooltip horizontal centre
-
- var tt_top_offset = 57 // How far above the button should the tooltip be?
-
- // For some reason, the existing tooltips are at a different offset in full-screen.
- if(document.fullscreenElement) {
- tt_top_offset = 75
- }
-
- var tt_top = bbcr.top + bbcr.height/2 - tt_top_offset // tooltip top
-
- // YouTube has an existing tooltip DOM structure that it reuses for all of its
- // player tooltips, but it's easier and more reliable to just create our own,
- // using the same classes.
-
- // Try to get our existing tooltip from DOM from previous run
- var tt = document.getElementById("zac-tt")
-
- var tt_text
-
- if(!tt) {
- // Create tool-tip DOM structure if not present.
- var mp = document.getElementsByClassName("html5-video-player")[0]
- var tt_text_wrapper = document.createElement("div")
- tt = document.createElement("div")
- tt_text = document.createElement("span")
-
- tt.setAttribute("class", "ytp-tooltip ytp-bottom")
- tt.setAttribute("id", "zac-tt")
- tt.style.setProperty("position", "fixed")
-
- tt_text_wrapper.setAttribute("class", "ytp-tooltip-text-wrapper")
-
- tt_text.setAttribute("class", "ytp-tooltip-text")
- tt_text.setAttribute("id", "zac-tt-text")
-
- tt.appendChild(tt_text_wrapper)
- tt_text_wrapper.appendChild(tt_text)
- mp.appendChild(tt)
- } else {
- // If DOM structure already present, get tooltip text.
- tt_text = document.getElementById("zac-tt-text")
- }
-
- if(show) { // show
- tt.style.setProperty("top", tt_top + "px")
- tt_text.innerHTML = button.getAttribute("aria-label")
- tt.style.removeProperty("display") // show the tooltip
-
- // Calculate horizontal position. Tooltip must be showing before
- // its width can be queried.
- var tt_width = tt.getBoundingClientRect().width
- tt.style.setProperty("left", tt_horiz_cen - tt_width / 2 + "px")
- debug_log("tt_width = " + tt_width)
- debug_log("tt_horiz_cen = " + tt_horiz_cen)
- debug_log("tt left position = " + (tt_horiz_cen - tt_width / 2))
-
- // Remove button title, else the browser may (will) display it as a
- // tooltip, in addition to ours.
- button.removeAttribute("title")
- } else { // hide
- tt.style.setProperty("display", "none")
- tt_text.innerHTML = ""
- button.setAttribute("title", button.getAttribute("aria-label"))
- }
-
- tooltip_showing = show
-
- // All of that just for a tooltip that matches the others. And it's
- // still not perfect. Sheesh.
- }
-
- /**
- * update
- * Ensures the zoom button reflects the current state.
- */
- function update() {
- set_zoom_button_mode()
- show_zoom_button_tooltip(tooltip_showing)
- }
-
- var button_object
-
- function add_button() {
- right_controls = document.getElementsByClassName("ytp-right-controls")[0]
- size_button = document.getElementsByClassName("ytp-size-button") [0]
-
- if(right_controls && size_button) {
- debug_log("Adding zoom and crop toggle button.")
-
- // Remove existing button if present (sometimes it persists even after a page reload)
- var existing_button = document.getElementById("zac-zoom-button")
- if(existing_button) {
- debug_log("Destroying old zoom and crop toggle button.")
- right_controls.removeChild(existing_button)
- }
-
- // Create button
- button = document.createElement("button")
- button.setAttribute("class", "ytp-button")
- button.setAttribute("id", "zac-zoom-button")
-
- create_zoom_button_icon()
- set_zoom_button_mode()
-
- // Add button to controls
- right_controls.insertBefore(button, size_button)
-
- // Set event handlers
- button.addEventListener("click", toggle_manual_enab)
- button.addEventListener("mouseover", function(){show_zoom_button_tooltip()})
- button.addEventListener("mouseout", function(){show_zoom_button_tooltip(false)})
- button.addEventListener("focus", function(){show_zoom_button_tooltip()})
- button.addEventListener("blur", function(){show_zoom_button_tooltip(false)})
-
- button_object = {
- update : update
- }
-
- } else {
- // Keep trying until we have somewhere to put the button.
- debug_log("Can't add zoom and crop toggle button yet. Retrying in 200ms.")
- setTimeout(add_button, 200)
- }
- }
-
- add_button()
- return button_object
- }
-
- // Initialise
- watch_for_fullscreen()
- watch_for_keypresses()
- var zoom_button = create_zoom_button()