---
title: Небольшой проектик JFF на perl+mojo
tags: xbmc, perl, mojo
---
Сиё творение родилось из простенькой, казалось бы, задачи: прицепить подрядовские вебкамеры к медиацентру.
Что хотелось видеть изначально: плейлист с перечнем камер со ссылками вида ``http://.*\.jpeg``. Открываешь его, тыкаешь в нужную камеру м смотришь обстановку на дороге. В крайнем случае - пачку плейлистов, ровно с одним пунктом. Как показывает опыт, xbmc просто до жопы хочет обязательно постоить превьюшку к каждому пункту плейлиста, и начинает их дёргать все сразу, что неприемлемо и в данном случае - не по джентельменски. Из-за дикого количества запросов на сервер подряда, нас могут просто рубануть по ip или useragent'у.
---
Грабли с превьюшками - это полбеды. Как выяснилось в дальнейшем, xbmc прекрасно понимает http, и пришедший по нему jpeg. НО! обрабатывает их не как картинку, а как видео из одного кадра. Повезёт, если увидишь как там что-то мелькнуло в 1/25ю секунды[^fn1].
Поэтому, возникла гениальная идея - быстренько забацать аналог udpxy, который:
- можно насиловать запросами сколько угодно и с какой угодно частотой,
- может отдавать не обязательно jpeg.
Из требований - возможно меньше зависимостей, кроме perl'а.
Кодинг
-------
В репозиториях debian'а есть и perl и [mojolicious](https://metacpan.org/pod/Mojolicious), поэтому установку я расписывать не буду, перейдём сразу к коду.
$ mojo generate app Livecam
Структура проекта:
$ tree -A -S -n livecam
livecam/
├── cache # кэш списка каналов и изображений с камер
├── lib
│ ├── Livecam
│ │ └── Main.pm # контроллер
│ └── Livecam.pm # роутер
├── public
├── script
│ └── livecam # скрипт запуска
├── t # здесь должны быть тесты, но по факту - не используется
│ └── basic.t
└── templates # не используется, у нас все ответы - plain http
└── layouts
└── default.html.ep
В роутере нужно дописать 2 пути и повесить Mojo::UserAgent как ресурс приложения, чтобы не инициализировать его каждый раз.
$r->get('/playlist') -> to('main#playlist');
$r->get('/livecam') -> to('main#livecam');
$self->app->attr(ua => sub {
require Mojo::UserAgent;
my $ua = Mojo::UserAgent->new;
# поставить любой, похожий на настоящий
$ua->name(qq{Mozilla/5.0 (X11; Linux i686; rv:24.0) Gecko/20140925});
return $ua;
});
В контроллере кода побольше. Перечень функций с их назначением [^fn2]:
# private, работа со списком камер
_pls_cache_path
_pls_cache_save
_pls_cache_load
# private, работа с изображениями камер
_cam_cache_path
_cam_cache_save
_cam_cache_load
# и 2 метода, вызываемых из роутера, которые мы рассмотрим подробнее
playlist
livecam
В любой непонятной ситуации - [смотрите код](http://linuxdv.org/git/?p=livecam.git;a=summary), там его совсем мало.
playlist()
----------
Назначение - получить полный список камер с сайта подряда (и закешировать его), отдать его в виде плейлиста m3u.
Камеры раскиданы по 4 страницам, поэтому получим каждую и пройдёмся по ним парсером. Здесь нам поможет Mojo::DOM, совершенно замечательная штука, этакое jquery для perl'а (селекторы и манипуляция элементами, без ajax'а, которым занимается уже Mojo::UserAgent с Mojo::IOLoop).
Итак, структура html'я каждой камеры довольно проста: три div'а, враппер, имя камеры и ссылка с превью.
# перебор камер на странице
$self->app->ua->get($url)->res->dom('div.cam-preview')->each(sub {<...>});
# ...и пример вытаскивания данных внутри обработчика в each();
# из такого: Title
my $title = $_->at('div.title a')->text;
Всё остальное здесь не представляет особого интереса.
livecam()
---------
Назначение - получить изображение с камеры, отдать перекодированное и ограничить частоту запроса к внешнему серверу за счёт кеширования вывода.
Изначально, я хотел использовать для вывода gif, но в итоге отказался (+зависимость от gd2/im::m/g::m и всё равно - генерация заголовка gif'а руками). В итоге остановился на mjpeg over http. В отличие от просто mjpeg, с точки зрения браузера это выглядит так:
* в заголовках посылается 'Content-Type: multipart/x-mixed-replace;boundary=--Frame'
* далее, пока страница не закрыта/держится соединение, периодически посылаются порции данных такого вида:
--Frame\r\n
Content-Type: image/jpg\r\n
Content-Length: $size\r\n
\r\n
\r\n
\r\n
С точки зрения mojo, это реализуется так:
* пишется обработчик с такой логикой:
* достать из кэша/получить извне кадр с внешней камеры с таким-то id
* сформировать "кадр" из примера выше, отдать его клиенту
* повесить обработчик на повторное выполнение каждые N секунд
* дёрнуть обработчик в первый раз, чтобы клиент не ждал до срабатывания периодического таймера
* висеть, пока таймер периодически кормит клиента кадрами
* при отвале клиента - аккуратно снять таймер, закрыть соединение
Всё, теперь мы можем дёргать наш сервер хоть 50 раз в секунду, запросы наружу пойдут только по мере устаревания картинки в кеше.
Эпилог
------
И всё таки она верт^W^W оно не работает. Потому что в xbmc не настолько продвинутый http-клиент. Картинку кажет, достаточно долго, но вот обновлять - хрен. Зато в браузере работает только так. Кроме того, обнаружился неприятный баг - плейлист самопроизвольно удаляется при закрытии через 'stop'.
Напишу в их багзиллу, пусть думают.
[^fn1]: 25fps - берётся по умолчанию
[^fn2]: их назначение примерно понятно из названий, но тем не менее