Stretch video in browser





Very often, video in online movie theaters has an aspect ratio that is different from that of the monitor. Therefore, sometimes there is a desire to make the overall scale a little larger by cropping a little at the edges. Or even - fit the image to the screen size on the smaller side of the picture. This is especially true for small screens, as well as for older 4: 3 monitors. I am already keeping quiet about the fact that the original video can be generally stretched on one side and this needs to be corrected somehow.



To solve this problem, I decided to write a browser extension for Chrome and Firefox. The idea is this: when playing any browser video, an on-screen menu is called up, which allows you to arbitrarily change the scale and aspect ratio of the picture.



iframe



The first problem I ran into is that videos on websites are not necessarily located on the main page, but can be hidden deep in nested iframes. I decided to scan all iframes and find all video elements in each one. By the way, this also solves another problem - you never know where the advertising video is, and where the film itself is. Let's find them all first.



The getVideos function calls itself recursively until all video elements are found in the last iframe. All videos are added to the ap_ext_space.videos array. The getVideos function takes the document of the current page as an input parameter. At the first launch, the main document is taken. Along the way, handlers are added to each video, but more on that below.



getVideos: function (srcDoc) {
	if (!srcDoc) {
		srcDoc = document;
		window.onkeydown = function (event) {
			var e = event || window.event;
			ap_ext_space.keyDn(e);
		};
	};

	var els = srcDoc.getElementsByTagName('video');
	for (var i = 0; i < els.length; i++) {
		els[i].addEventListener("seeked", function () {ap_ext_space.zoomw(); console.log('seeked'); }, true);
		els[i].addEventListener("abort", function () {ap_ext_space.zoomw(); console.log('abort'); }, true);
		els[i].addEventListener("pause", function () {ap_ext_space.zoomw(); console.log('pause'); }, true);
		els[i].addEventListener("play", function () {ap_ext_space.zoomw(); console.log('play'); }, true);
		els[i].addEventListener("playing", function () {ap_ext_space.zoomw(); console.log('playing'); }, true);
		els[i].addEventListener("seeked", function () {ap_ext_space.zoomw(); console.log('seeked'); }, true);

		ap_ext_space.videos.push(els[i]);
		ap_ext_space.menu(els[i], srcDoc);
	};
	console.log('all videos:', ap_ext_space.videos);

	var ifrs = srcDoc.getElementsByTagName("iframe");
	console.log('iframes:', ifrs);

	var ifr;
	for (var i = 0; i < ifrs.length; i++) {
		ifr = ifrs[i];
		try {
			var innerDoc = (ifr.contentDocument || ifr.contentWindow.document);
			var innerWindow = (ifr.contentWindow || ifr);
			innerWindow.onkeydown = function (event) {
				var e = event || window.event;
				ap_ext_space.keyDn(e);
			};
			ap_ext_space.getVideos(innerDoc);
		} catch (err) {
			console.log('err', err);
		};
	};
},


OSD Menu





Okay, we have a list of all the video elements. Now how to display the OSD menu? Let's just add its block element to each video. Yes, then we will have a lot of on-screen menus, but at one time only one video is displayed: one of the commercials or the movie itself. And only one menu will be shown with them.



The video is usually located in the parent div. Let's add our menu div element to it as the last child. Thus, the OSD will always be displayed over the video.



The OSD image is encoded in base64 in png format with a transparent alpha channel and placed in ap_ext_space.imgUR, since the browser will not allow us to load the image from another domain. Create a menu for each video:



menu: function(videoEl, doc) {

	//  div   video 
	//  ,       ( menuInside)
	var els = videoEl.parentNode.getElementsByTagName('div');
	var menuInside = false;
	for (var j = 0; j < els.length; j++) {
		if (els[j].id == 'ap_ext_space_container') {
			menuInside = true;
			ap_ext_space.menus.push(els[j]);
		};
	};

	if (menuInside == false) {

		//   
		var div = doc.createElement('div');
		div.innerHTML = ap_ext_space.html();
		videoEl.parentNode.appendChild(div);
		div.style.width = '520px';
		div.style.height = '410px';
		div.style.display = 'block';
		div.style.position = 'absolute';
		div.id = 'ap_ext_space_container';
		var url = "url('" + ap_ext_space.imgURL + "')";
		div.style.backgroundImage = url;
		div.style.opacity = 0.95;
		ap_ext_space.menus.push(div);

		//   
		div.addEventListener("dblclick", function(e) {
			e.preventDefault();
			e.stopPropagation();
		}, true);

		div.addEventListener("mouseover", function(e) {
			e.preventDefault();
			e.stopPropagation();

			var elem, evt = e ? e : event;
			if (evt.srcElement) {
				elem = evt.srcElement;
			} else if (evt.target) {
				elem = evt.target;
			};

			//     
			var pos = {
				ap_ext_space_num7: [520 + 134, 82],
				ap_ext_space_num8: [520 + 134 + 90, 82],
				ap_ext_space_num9: [520 + 134 + 90 + 90, 82],
				ap_ext_space_num4: [520 + 134, 82 + 90],
				ap_ext_space_num5: [520 + 134 + 90, 82 + 90],
				ap_ext_space_num6: [520 + 134 + 90 + 90, 82 + 90],
				ap_ext_space_num1: [520 + 134, 82 + 90 + 90],
				ap_ext_space_num2: [520 + 134 + 90, 82 + 90 + 90],
				ap_ext_space_num3: [520 + 134 + 90 + 90, 82 + 90 + 90]
			};
			var key, el;
			for (var j = 1; j < 10; j++) {
				key = 'ap_ext_space_num' + j;
				if (elem.id == key) {
					elem.style.backgroundImage = "url('" + ap_ext_space.imgURL + "')";
					elem.style.backgroundPosition = -pos[key][0] + 'px ' + -pos[key][1] + 'px';
				};
			};
		}, true);

		div.addEventListener("mouseout", function(e) {
			e.preventDefault();
			e.stopPropagation();

			var elem, evt = e ? e : event;
			if (evt.srcElement) {
				elem = evt.srcElement;
			} else if (evt.target) {
				elem = evt.target;
			};

			var key, el;
			for (var j = 1; j < 10; j++) {
				key = 'ap_ext_space_num' + j;
				if (elem.id == key) {
					elem.style.backgroundImage = "none";
				};
			};
		}, true);

		div.addEventListener("click", function(e) {
			e.preventDefault();
			e.stopPropagation();
			var elem, evt = e ? e : event;
			if (evt.srcElement) {
				elem = evt.srcElement;
			} else if (evt.target) {
				elem = evt.target;
			};
			ap_ext_space.clickHandler(elem);
		}, true);

		div.addEventListener("touchstart", function(e) {
			e.preventDefault();
			e.stopPropagation();
			var elem, evt = e ? e : event;
			if (evt.srcElement) {
				elem = evt.srcElement;
			} else if (evt.target) {
				elem = evt.target;
			};
			ap_ext_space.clickHandler(elem);
		}, true);

		div.addEventListener("touchend", function(e) {
			e.preventDefault();
		}, true);

		div.addEventListener("touchmove", function(e) {
			e.preventDefault();
		}, true);

		//     ( )
		ap_ext_space.menuPos();

	};
	console.log('all menus:', ap_ext_space.menus);
},


If you add an OSD div to a video like this: videoEl.parentNode.appendChild (div), it will appear on top of the video even in full screen mode. It remains only to center it, or rather, to do it with all the block menu items attached to the video elements (they have a size of 520x410):



menuPos: function() {

	if (ap_ext_space.isFullScreen()) {

		var sc = ap_ext_space.scale;
		var iw = window.innerWidth,
			ih = window.innerHeight;
		var w = iw * sc;
		var h = w / 16 * 9;

		for (var i = 0; i < ap_ext_space.menus.length; i++) {
			ap_ext_space.menus[i].style.marginLeft = (iw - 520) / 2 + 'px';
			ap_ext_space.menus[i].style.marginTop = (-h - 410) / 2 + 'px';
		};

	} else {

		ap_ext_space.scale = 1;

		for (var i = 0; i < ap_ext_space.menus.length; i++) {
			ap_ext_space.menus[i].style.marginLeft = '0px';
			ap_ext_space.menus[i].style.marginTop = '0px';
		};
	};

},

isFullScreen: function() {
	return !!(document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
},


By the way, in the end, I decided to completely hide the menu in windowed mode and allow video size control only in full screen mode. In the window, it makes no sense.



Handlers



Here, I think, everything is clear. On each button of the on-screen menu, click handlers, a wheelbarrow, and also pressing the corresponding key combination are hung to control the video even with the menu hidden. The buttons control the scale values: ap_ext_space.scale, ap_ext_space.scalew and ap_ext_space.scaleh, increasing or decreasing these values, and then resizing each video element found above as follows:



var sc = ap_ext_space.scale;
var iw = window.innerWidth,
	ih = window.innerHeight;
var w = iw * sc;
var h = w / 16 * 9;

for (var i = 0; i < ap_ext_space.videos.length; i++) {
	el = ap_ext_space.videos[i];
	el.style.position = 'initial';
	el.style.width = (w) + 'px';
	el.style.height = (h) + 'px';
	el.style.marginLeft = -(w - iw) / 2 + 'px';
	el.style.marginTop = -(h - ih) / 2 + 'px';
	el.style.transform = 'scaleX(' + ap_ext_space.scalew + ') scaleY(' + ap_ext_space.scaleh + ')';
};


In addition, I also hung on the video event handlers seeked, abort, pause, play, playing, seeked on each video element (in the getVideos () function above) a call to the only function that redraws the on-screen menu with recalculating its coordinates, since sometimes it "leaves" with some user action. I did the same for the browser window resizing event.



Namespace



In general, what kind of ap_ext_space is this? The fact is that all the functions that are used to resize the video must be embedded in the corresponding page (either in the main page or in the iframe). So I just bundled these functions and, along with them, the base64 OSD background into a single namespace. All this is injected into the code of the current browser tab from the background script as follows:



var codeString = ap_ext_space_f.toString() + '; ap_ext_space_f(); ap_ext_space.init()';
chrome.tabs.executeScript({
	code: codeString
});

function ap_ext_space_f() {

	ap_ext_space = {

		init: function() {
			//...
		},

		//...
	};

};


Well, inside ap_ext_space the search for all iframes is already triggered, then - for all videos inside each of them, an on-screen menu with handlers is built, and so on.



How to use



Play video. Click on the extension icon. Expand the video to full screen. Adjust scale and aspect ratio. The menu can be hidden with the keyboard shortcut ctrl + 0.



Outcome



The extension is called Browser Video Tuner, it's free and is currently available in the Chrome and Firefox extension stores. Also, of course, it can be installed in all Chrome-compatible browsers like Opera, Yandex Browser and so on. It should be noted that the extension does not work on all video sites. Where access to iframe elements from the outside is protected by security policy, then no video will simply be found. And a corresponding warning about this will appear in the console. In this case, the menu will simply not be displayed. But on Youtube and many online cinemas, everything works.



Minor problems were noticed with some browsers. For example, in Yandex Browser, the displayed image somehow deteriorates and resembles heavily compressed jpeg. But this does not affect functionality in any way.





I was looking for a way to display the on-screen menu in full screen mode simply on top of the entire document without embedding it inside iframes, so as not to depend on the browser's security policy, and try to control the size of the entire document as a whole, but so far I have not succeeded. I think in the future the expansion will be supplemented with new functions.



All Articles