// ==UserScript== // @name Zoom and crop YouTube videos to fill screen height // @version 0.5 // @description Removes letterboxing by cropping the left and right edges off of full-screen YouTube videos and zooming to fill the screen height. // @author uxamend // @namespace Violentmonkey Scripts // @match https://www.youtube.com/* // @exclude https://www.youtube.com/ad_frame* // @grant none // @run-at document-idle // @license CC0-1.0 // @compatible firefox version >=64 (older versions untested) // @compatible chrome version >=71 (older versions untested) // @downloadURL none // ==/UserScript== "use strict"; var debug_logging_on = false; var debug_script_name = "Userscript: Zoom YouTube videos to fill screen height" // For tall screens: Sets the narrowest aspect ratio that videos will ever be cropped to. var min_cropped_aspect = 16/10; // Express as a fraction, e.g. 16/10, not 16:10. // For very wide videos: The maximum proportion of video width to crop off var max_crop_proportion = 1; function debug_log(message) { if(debug_logging_on){ console.log("[" + debug_script_name + "] " + message); } } // Define a mutation observer and callback to observe video element 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(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}); } function set_zoom(video) { var vs = video.style; var w = window.innerWidth; var h = window.innerHeight; var video_aspect = video.videoWidth/video.videoHeight; var screen_aspect = w/h; // 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 > screen_aspect && video_aspect > min_cropped_aspect) { debug_log("Video is wider than screen and min_cropped_aspect. Setting zoom."); var vh = h; // height of video // Apply min_cropped_aspect constraint to video height if (min_cropped_aspect > screen_aspect) vh = w/min_cropped_aspect; var vw = video_aspect * vh; // width of video, including cropped portion // Apply max_crop_proportion constraint to video width if (w/vw < 1-max_crop_proportion) vw = w + vw * max_crop_proportion; var vt = (h-vh)/2; // top edge position of video var vl = (w-vw)/2; // left edge position of video debug_log("Screen dimensions: " + w + "x" + h + "."); debug_log("Calculated new video element dimensions: " + vw + "x" + vh + ", origin at " + vt + ", " + vl + "."); debug_log("(Underlying video stream resolution: " + video.videoWidth + "x" + video.videoHeight + ".)"); debug_log("screen_aspect: " + screen_aspect + "."); debug_log("min_cropped_aspect: " + min_cropped_aspect + "." ); debug_log("video_aspect: " + video_aspect + "."); debug_log("max_crop_proportion: " + max_crop_proportion + "."); // Note: This might appear to risk creating an endless loop via the mutation observer. // However, it should 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."); } } else { debug_log("Video has ended. Not setting zoom. (Otherwise we mess with the endscreen.)") } } function zoom_on() { debug_log("Turning zoom on."); var video = document.getElementsByClassName("html5-main-video")[0]; set_zoom(video); observe_video_mutations(video); } function zoom_off() { debug_log("Turning zoom off."); mo.disconnect(); } function zoom_should_be_on() { // Double 'not' to turn object presence into boolean return !(!(document.fullscreenElement)); } function zoom_on_or_off() { if(zoom_should_be_on()) { setTimeout(zoom_on, 200); } else { zoom_off(); } } 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_fullscreen();