Поддержка 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   http://bolknote.ru/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 сломаны.
9 октября 2011 05:16

greli (greli.livejournal.com)
9 октября 2011, 14:39

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

bolk (bolknote.ru)
9 октября 2011, 15:33, ответ предназначен greli (greli.livejournal.com):

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

Kildor (kildor.ya.ru)
9 октября 2011, 15:51

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

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

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

bolk (bolknote.ru)
9 октября 2011, 16:24, ответ предназначен Kildor (kildor.ya.ru):

А я уж было хотел поставить викль вместо рабочей версии.
Что такое викль?
Впрочем шутка о том, что «релизом нужно считать версию .02» восходит как бы и не к версии 7.5, а то и раньше.
Так это и не релиз. Двенадцатой версии нет ещё, только ночные сборки или как это у них там называется.

Kildor (kildor.ya.ru)
9 октября 2011, 21:28, ответ предназначен bolk (bolknote.ru):

викль — снапшот, сборка. Когда они выходили по пятницам, раз в неделю, их называли weekly. Вот слово и привязалось.
Так это и не релиз. Двенадцатой версии нет ещё, только ночные сборки или как это у них там называется.
Ну, это да.
Просто одно время у них были очень стабильные снапшоты, при том что как раз релизы вида *.00 являли собой на редкость нестабильные поделия. Может оно и сейчас так, не сильно слежу за сборками.

funk_rabbit (funk-rabbit.livejournal.com)
26 ноября 2011, 17:55, ответ предназначен bolk (bolknote.ru):

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

bolk (bolknote.ru)
27 ноября 2011, 10:54, ответ предназначен funk_rabbit (funk-rabbit.livejournal.com):

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

Ваше имя или адрес блога (можно OpenID):

Текст вашего комментария, не HTML:

Кому бы вы хотели ответить (или кликните на его аватару)