1 заметка с тегом

userjs

Поддержка 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 сломаны.