Пишу, по большей части, про историю, свою жизнь и немного про программирование.

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

7 комментариев
greli (greli.livejournal.com) 2011

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

Евгений Степанищев (bolknote.ru) 2011

Комментарий для greli.livejournal.com:

И сборка наверное за седьмое октября :-).

Да, я спать уже очень хотел, когда это писал.

Думаю, это такой случай, когда багрепорты просто необходимо обновлять, что самим же потом легче было

Я уже написал аж в два места :)

Kildor (kildor.ya.ru) 2011

причём в сборке за седьмое октября userjs сломаны.

А я уж было хотел поставить викль вместо рабочей версии.

Эх, помню, когда викли только появились, можно было ставить их все один на один и на стабильную версию, не боясь больших глюков. А сейчас стараюсь дожидаться версии *.01, и ставить уже её, а то глючит-ведь.

// Впрочем шутка о том, что «релизом нужно считать версию .02» восходит как бы и не к версии 7.5, а то и раньше.

Евгений Степанищев (bolknote.ru) 2011

Комментарий для kildor.ya.ru:

А я уж было хотел поставить викль вместо рабочей версии.

Что такое викль?

Впрочем шутка о том, что «релизом нужно считать версию .02» восходит как бы и не к версии 7.5, а то и раньше.

Так это и не релиз. Двенадцатой версии нет ещё, только ночные сборки или как это у них там называется.

Kildor (kildor.ya.ru) 2011

Комментарий для Евгения Степанищева:

викль — снапшот, сборка. Когда они выходили по пятницам, раз в неделю, их называли weekly. Вот слово и привязалось.

Так это и не релиз. Двенадцатой версии нет ещё, только ночные сборки или как это у них там называется.

Ну, это да.
Просто одно время у них были очень стабильные снапшоты, при том что как раз релизы вида *.00 являли собой на редкость нестабильные поделия. Может оно и сейчас так, не сильно слежу за сборками.

funk_rabbit (funk-rabbit.livejournal.com) 2011

Комментарий для Евгения Степанищева:

а чего не хватает 11.52 для работы твоего скрипта?

Евгений Степанищев (bolknote.ru) 2011

Комментарий для funk-rabbit.livejournal.com:

Я уже сейчас и не вспомню.