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

от автора

Очень часто видео в онлайн-кинотеатрах имеет соотношение сторон, отличное от соотношения сторон монитора. Поэтому иногда возникает желание сделать общий масштаб чуть крупнее за счет небольшой обрезки по краям. Или вовсе — вписать изображение в размеры экрана по меньшей стороне картинки. Особенно это актуально для маленьких экранов, а также, для старых мониторов 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), то он будет отображаться поверх видео даже в полноэкранном режиме. Осталось только отцентрировать его, а точнее, сделать это со всеми привязанными к видео-элементам блочными элементами меню (они имеют размер 520×410):

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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *