--- 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 В любой непонятной ситуации - [смотрите код](https://git.linuxdv.org/ad_user/livecam), там его совсем мало. 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]: их назначение примерно понятно из названий, но тем не менее