Небольшой проектик JFF на perl+mojo
Сиё творение родилось из простенькой, казалось бы, задачи: прицепить подрядовские вебкамеры к медиацентру.
Что хотелось видеть изначально: плейлист с перечнем камер со ссылками вида http://.*\.jpeg
. Открываешь его, тыкаешь в нужную камеру м смотришь обстановку на дороге. В крайнем случае - пачку плейлистов, ровно с одним пунктом. Как показывает опыт, xbmc просто до жопы хочет обязательно постоить превьюшку к каждому пункту плейлиста, и начинает их дёргать все сразу, что неприемлемо и в данном случае - не по джентельменски. Из-за дикого количества запросов на сервер подряда, нас могут просто рубануть по ip или useragent'у.
Грабли с превьюшками - это полбеды. Как выяснилось в дальнейшем, xbmc прекрасно понимает http, и пришедший по нему jpeg. НО! обрабатывает их не как картинку, а как видео из одного кадра. Повезёт, если увидишь как там что-то мелькнуло в 1/25ю секунды1.
Поэтому, возникла гениальная идея - быстренько забацать аналог udpxy, который:
- можно насиловать запросами сколько угодно и с какой угодно частотой,
- может отдавать не обязательно jpeg.
Из требований - возможно меньше зависимостей, кроме perl'а.
Кодинг
В репозиториях debian'а есть и perl и 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;
});
В контроллере кода побольше. Перечень функций с их назначением 2:
# private, работа со списком камер
_pls_cache_path
_pls_cache_save
_pls_cache_load
# private, работа с изображениями камер
_cam_cache_path
_cam_cache_save
_cam_cache_load
# и 2 метода, вызываемых из роутера, которые мы рассмотрим подробнее
playlist
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();
# из такого: <a href="<...>" class="<...>">Title</a>
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'.
Напишу в их багзиллу, пусть думают.