Поддержка MJPEG в «Опере»
Как я уже писал, «Опера» иначе показывает MJPEG, не как остальные браузеры. Если открыть какую-нибудь веб-камеру, использующую этот формат, в десятой версии, то «Опера» сначала задумается, потом покажет какой-то кадр, 11 и 12 версии показывают ещё хуже — подобие потоковой передачи уже есть, но кадры лепятся один на другой и ничего разглядеть невозможно.
А мне хочется смотреть в веб-камеру на микрорайон, где у нас с женой квартира в Москве, через любимый браузер.
Вдохновившись библиотекой добавляющей нативную поддержку APNG в браузер, я подумал, что я ничем не хуже и смогу добавить поддержку MJPEG в «Оперу». Получился небольшой userjs, который я, естественно, выложил на «Гитхаб».
Идея такая — находим картинку во всех тегах IMG, у которой расширение «mjpg». Через XHR получаем с сервера видеопоток, копим кадр, подменяем найденную картинку и так далее.
Работает начиная с 12 версии браузера, которая ещё не вышла (кстати, в позавчерашней сборке сломали userjs напрочь) — в предыдущих версиях XHR слабоват по возможностям для этого.
Больше всего я бился с декодированием бинарных данных. Последняя версия «Оперы» поддерживает тип ответа arraybuffer (а вот blob не поддерживается), но если его выбрать, то данные в цикле почему-то перестают накапливаться, вероятно какой-то баг браузера. Я долго мучался, прежде чем придумал следующий хак — если сделать overrideMimeType(’image/jpeg’), то данные приходят в двухбайтовой кодировке UCS-2, которую несложно декодировать.
Кстати, быстрее всего у меня получилось декодировать не циклом, а регулярными выражениями, дальше тормозит уже не мой код, а способ получения отдельного кадра — каждый раз я соединяюсь с сервером заново. Можно бы накапливать кадры пачкой и потом их сбрасывать, но у меня нет задачи смотреть трансляции футбола, у меня дом, который не бегает и полученной скорости мне вполне хватает (где-то 2-3 кадра в секунду).
В общем, получился довольно небольшой код — 94 строки.
// ==UserScript==
// @name MJPEG+Opera - Add to Opera MJPEG support
// @author Evgeny Stepanischev aka Bolk
// @version 1.00
// @namespace /files/opera-mjpeg.js
// @modified 2011-10-09
// @include *
// ==/UserScript==
opera.addEventListener('BeforeEvent.DOMContentLoaded', function() {
if (opera.version() < 12) return;
var BolkMJpeg = function (url, img) {
var req = new XMLHttpRequest();
img.src = 'about:blank'; // Сброс картинки, чтобы прекратить передачу
// Асинхронно забираем кадр при помощи XHR, нам придёт видеопоток
var start = function (url) {
req.open("GET", url, true);
// Свежепридуманный хитрый хак, позволяет получить данные в UCS-2
req.overrideMimeType('image/jpeg');
req.send(null);
}
// Хитрыми путями восстанавливаем порядок байт в reponse, там сейчас UCS-2,
// получаем просто последовательность байт
var reqbytes = function (n, l) {
return escape(req.response.substr(n, l)).
replace(/([^%]|%[^u].|%u....)/g, '<$1>').
replace(/<%u(..)(..)>/g, '%$2%$1').
replace(/<%(..)>/g, '%$1%00').
replace(/<(.)>/g, '$1%00');
}
// заменяем картинку на бинарные данные
var draw = function (bin) {
img.src = 'data:image/jpeg,' + bin;
}
// На изменение состояние входных данных собираем по частям JPEG
req.onreadystatechange = function () {
// состояние «3» — данные пошли, но ещё не кончились
if (req.readyState === 3) {
var hnd = setInterval(function () {
// по заголовку получаем размер данных
var header = unescape(reqbytes(0, 300)).toLowerCase();
var idx = header.indexOf('content-length:');
if (idx > -1) {
// 15 — длина заголовка
var len = parseInt(header.substr(idx + 15), 10);
var headend = header.indexOf("\r\n\r\n");
// дожидаемся конца заголовка
if (headend > -1) {
var resp = reqbytes(0, len);
// умножаем всё на три, так как reqbytes даёт esc-последовательность
// вида %XX%XX…
len *= 3;
headend = (headend + 4) * 3;
// если собрали весь JPEG, обрываем соединение
// и начинаем новый цикл. Мы могли бы собирать кадры и дальше, но
// это съест всю память со временем
if (resp.length >= len + headend) {
req.abort();
resp = resp.substr(headend, len);
draw(resp);
clearInterval(hnd);
start(url);
}
}
}
}, 100);
}
}
start(url);
};
// ищем картинку, у которой расширение mjpg
var imgs = document.getElementsByTagName('IMG');
for (var i = 0, len = imgs.length; i<len; i++) {
if (/\.mjpg$/.test(imgs[i].src)) {
BolkMJpeg(imgs[i].src, imgs[i]);
break;
}
}
}, true);
Если вы не в курсе как устанавливаются скрипты userjs, то это очень просто. В интернете есть много инструкций на эту тему. Только напоминаю, что «Опера» нужна двенадцатая, причём в сборке за седьмое октября userjs сломаны.
Думаю, это такой случай, когда багрепорты просто необходимо обновлять, что самим же потом легче было. И сборка наверное за седьмое октября :-).
Комментарий для greli.livejournal.com:
Да, я спать уже очень хотел, когда это писал.
Я уже написал аж в два места :)
А я уж было хотел поставить викль вместо рабочей версии.
Эх, помню, когда викли только появились, можно было ставить их все один на один и на стабильную версию, не боясь больших глюков. А сейчас стараюсь дожидаться версии *.01, и ставить уже её, а то глючит-ведь.
// Впрочем шутка о том, что «релизом нужно считать версию .02» восходит как бы и не к версии 7.5, а то и раньше.
Комментарий для kildor.ya.ru:
Что такое викль?
Так это и не релиз. Двенадцатой версии нет ещё, только ночные сборки или как это у них там называется.
Комментарий для Евгения Степанищева:
викль — снапшот, сборка. Когда они выходили по пятницам, раз в неделю, их называли weekly. Вот слово и привязалось.
Ну, это да.
Просто одно время у них были очень стабильные снапшоты, при том что как раз релизы вида *.00 являли собой на редкость нестабильные поделия. Может оно и сейчас так, не сильно слежу за сборками.
Комментарий для Евгения Степанищева:
а чего не хватает 11.52 для работы твоего скрипта?
Комментарий для funk-rabbit.livejournal.com:
Я уже сейчас и не вспомню.