Растянуть видео в браузере

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.


Очень часто видео в онлайн-кинотеатрах имеет соотношение сторон, отличное от соотношения сторон монитора. Поэтому иногда возникает желание сделать общий масштаб чуть крупнее за счет небольшой обрезки по краям. Или вовсе — вписать изображение в размеры экрана по меньшей стороне картинки. Особенно это актуально для маленьких экранов, а также, для старых мониторов 4:3. Я уж молчу о том, что оригинальное видео может быть вообще растянуто по одной из сторон и это необходимо как-то исправить.

Для решения данной проблемы я задумал написать браузерное расширение под Chrome и Firefox. Идея такая: при проигрывании любого браузерного видео вызывается экранное меню, которое позволяет произвольно менять масштаб и соотношение сторон картинки.

iframe


Первая проблема, с которой я столкнулся, заключается в том, что видео на сайтах вовсе не обязательно располагается на основной странице, а может быть запрятано глубоко во вложенных iframe. Я решил просканировать все iframe-элементы и найти в каждом из них все элементы video. Кстати, этим решается и другая проблема — никогда не знаешь, где рекламное видео, а где сам фильм. Давайте для начала найдем их всех.

Функция getVideos вызывает рекурсивно сама себя до тех пор, пока в последнем iframe не будут найдены все элементы video. Все видео добавляются в массив ap_ext_space.videos. В качестве входного параметра функция getVideos принимает документ текущей страницы. При первом запуске берется главный документ. По ходу еще на каждое видео навешиваются обработчики, но об этом ниже.

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);
		};
	};
},

Экранное меню



Хорошо, список всех видео-элементов у нас есть. Теперь как отобразить экранное меню? Просто добавим его блочный элемент к каждому видео. Да, тогда у нас будет много экранных меню, но в один момент времени все равно отображается только одно видео: один из рекламных роликов либо сам фильм. И меню вместе с ними будет показываться только одно.

Видео, как правило, располагается в родительском div-элементе. Добавим к нему в качестве последнего child наш div-элемент меню. Таким образом, экранное меню всегда будет отображаться поверх видео.

Изображение экранного меню закодируем в base64 в формате png с прозрачным альфа-каналом и поместим в ap_ext_space.imgUR, так как браузер не позволит нам подгрузить изображение с другого домена. Создание меню для каждого видео:

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);
},

Если добавлять div-элемент экранного меню к видео таким образом: videoEl.parentNode.appendChild(div), то он будет отображаться поверх видео даже в полноэкранном режиме. Осталось только отцентрировать его, а точнее, сделать это со всеми привязанными к видео-элементам блочными элементами меню (они имеют размер 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);
},

Кстати, в итоге я решил вообще скрыть меню в оконном режиме и разрешить управление размерами видео только в полноэкранном режиме. В оконном в этом нет смысла.

Обработчики


Здесь, думаю, и так все понятно. На каждую кнопку экранного меню навешены обработчики клика, тача и еще — нажатия соответствующего сочетания клавиш, чтобы управлять видео даже при скрытом меню. Кнопки управляют величинами масштабов: ap_ext_space.scale, ap_ext_space.scalew и ap_ext_space.scaleh, увеличивая или уменьшая эти значения, а затем изменяется размер каждого найденного выше видео-элемента следующим образом:

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 + ')';
};

Кроме того, я также повесил на обработчики событий видео seeked, abort, pause, play, playing, seeked на каждый video-элемент (в функции getVideos() выше) вызов единственной функции, которая перерисовывает экранное меню с пересчетом его координат, так как иногда оно «уезжает» при некоторых действиях пользователя. То же сделал и для события изменения размеров окна браузера.

Пространство имен


Вообще, что это за ap_ext_space такой? Дело в том, что все функции, которые используются для изменения размера видео, должны быть внедрены в соответствующую страницу (либо в основную, либо — в iframe). Поэтому я просто объединил эти функции, а также, вместе с ними — и фон экранного меню в формате base64 в единое пространстве имен. Инжектируется все это в код текущей вкладки браузера из бэкграунд-скрипта следующим образом:

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() {
			//...
		},

		//...
	};

};

Ну а внутри ap_ext_space уже срабатывает поиск всех iframe, затем — всех video внутри каждого из них, строится экранное меню с обработчиками и так далее.

Как пользоваться


Запустить видео. Кликнуть на иконку расширения. Развернуть видео на полный экран. Настраивать масштаб и соотношение сторон. Меню можно скрыть сочетанием клавиш ctrl+0.

Итог


Расширение называется Browser Video Tuner, оно бесплатное и в данный момент доступно в магазинах расширений Chrome и Firefox. Также, его, естественно, можно установить и во все Chrome-совместимые браузеры типа Opera, Yandex Browser и так далее. Стоит отметить, что расширение срабатывает не на всех сайтах с видео. Там, где доступ к iframe-элементам извне защищен политикой безопасности, то ни одного видео просто не будет найдено. И в консоли появится соответствующее предупреждение об этом. Меню в этом случае просто не отобразится. Но на Youtube и на многих онлайн-кинотеатрах все работает.

С некоторыми браузерами замечены небольшие проблемы. Например, в Yandex Browser выводимое изображение как-то портится и напоминает сильно пережатый jpeg. Но на функциональность это никак не влияет


Я искал способ выводить экранное меню в полноэкранном режиме просто поверх всего документа без внедрения его внутрь iframe-ов, чтобы не зависеть от политики безопасности браузера, и попробовать управлять размерами всего документа в целом, но пока мне это не удалось. Думаю, в дальнейшем расширение будет дополняться новыми функциями.
Источник: https://habr.com/ru/post/528788/


Интересные статьи

Интересные статьи

Классический CDN — anycast, GeoDNS, веб-сервер с кешем — отлично работает с простыми файлами и небольшим количеством пользователей. Но если возникает необходимость раздавать потоковое...
Этой весной мы оказались в очень весёлых условиях. Из-за пандемии стало ясно, что наши летние конференции необходимо переносить в онлайн. А чтобы провести их в онлайне качественно, на...
Появление эксплойта checkm8 можно назвать одним из важнейших событий прошедшего года для исследователей продукции Apple. Ранее мы уже опубликовали технический анализ этого эксплойта. Сейчас соо...
На сегодняшний день у сервиса «Битрикс24» нет сотен гигабит трафика, нет огромного парка серверов (хотя и существующих, конечно, немало). Но для многих клиентов он является основным инструментом ...
Сейчас, в 2019 году видеокассета потеряла всякую актуальность. Когда год назад я решил оцифровать свои старые записи, и не без труда вывел картинку с видеомагнитофона на современную метровую ЖК-п...