Очень часто видео в онлайн-кинотеатрах имеет соотношение сторон, отличное от соотношения сторон монитора. Поэтому иногда возникает желание сделать общий масштаб чуть крупнее за счет небольшой обрезки по краям. Или вовсе — вписать изображение в размеры экрана по меньшей стороне картинки. Особенно это актуально для маленьких экранов, а также, для старых мониторов 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-ов, чтобы не зависеть от политики безопасности браузера, и попробовать управлять размерами всего документа в целом, но пока мне это не удалось. Думаю, в дальнейшем расширение будет дополняться новыми функциями.
upd
Благодаря комментариям к статье удалось улучшить расширение. Теперь видео распознается практически на всех сайтах. Для этого после клика на иконке расширения происходит встраивание кода не только в основной документ активной вкладки но и во все ее дочерние элементы iframe.
Версия 1.4 для Firefox уже доступна. Эта же версия для Chrome в данный момент находится на модерации.
aamonster
Когда инжектите скрипт в табы – поставьте allFrames=true (или вообще можете инжектить content script штатным образом, через манифест). Не надо сканировать все фреймы – за вас это сделает браузер, и политики безопасности не помешают.
Kempston Автор
Спасибо за наводку на этот флаг. Пока получилось декларативно через манифест. Через свойство allFrames:true почему-то все равно не дает доступ к фреймам. А хотелось бы дать пользователю выбор, использовать ли расширение на конкретной странице или нет, а не подключать его везде принудительно… Буду дальше разбираться с этим.
Похоже, в скором времени выйдет новая версия расширения.
aamonster
Если вы из одного фрейма пытаетесь дотянуться до другого – может не прокатить, тут вы на равных со скриптами с сайтов. Всегда работает общение между контент-скриптами и бэкграундом, хотите из главного фрейма управлять iframe – посылайте сообщения в бэкграунд и из него в нужный. (ну или ещё window.postMessage работает, но посылаемые им сообщения не только вашему скрипту приходят)
Kempston Автор
Удалось получить доступ к видео через allFrames:true. Для этого надо было еще в манифест добавить дополнительное разрешение… Теперь видео распознается практически на всех сайтах. Дополнил статью.