Предыстория
Некоторое время назад я загорелся желанием “улучшить” танк из известного набора “Танковый бой”, добавив возможность играть, как «если бы я был водителем танка». Идея появилась после прочтения нескольких статей на Хабре (например здесь: geektimes.ru/post/257528), в них же я нашел, как это можно сделать имея маленький WiFi-роутер и USB-камеру. Решение выглядело подкупающе простым: роутер прошивается специальной прошивкой, к нему подключается камера, танк управляется родным пультом, а видео смотрится в браузере. Быстро собрав прототип, я обнаружил, что видео захватывается в отвратительном качестве. Это было либо 320х240х30, либо 640х480х30. При включении режима 1280х720 в лучшем случае было рваное видео с артефактами, в худшем — его не было вообще. Режим 1920х1080 не работал в принципе. Меня это сильно расстроило, так как на PC камера поддерживала режимы вплоть до 1920х1080х30 и имела аппаратное MJPG сжатие. Моя интуиция подсказывала, что реализация далека от совершенства.
Цели
- Видео в разрешении FullHD (1920Х1080) или HD (1280х720) и нормальная частота кадров (чтобы можно было играть).
- Игрушку я планировал отдать детям, поэтому нужен был автостарт и поддержка подключения/отключения камеры.
В общем хотелось что-то вроде этого:
Ограничения
Я не собирался искать решение, которое работает всегда и везде. Следующие ограничения меня вполне устраивали:
- Хороший WiFi сигнал.
- Ограниченное число подключений, приоритет отдавался случаю, когда есть всего один клиент.
- Камера поддерживает режим MJPG.
HW и SW
- Видеокамера Logitech B910 HD (http://www.logitech.com/ru-ru/product/b910-hd-webcam).
- Роутер TP-LINK TL-MR3020. Этот малыш имеет следующее железо: CPU MIPS 24K 400MHz, RAM 32 MiB, Flash 4 MiB, Ethernet 100 Mbit, USB 2.0 (http://wiki.openwrt.org/ru/toh/tp-link/tl-mr3020).
- Прошивка для роутера. Я начал с OR-WRT (http://roboforum.ru/wiki/OR-WRT), но закончил с OpenWRT (http://openwrt.org/, версии 12.07 и 15.05).
- Браузер. Конечно это не лучший вариант, но очень удобно для начала.
- Набор “Танковый бой”.
Предварительный анализ
В общем-то, это действительно слабая конфигурация, особенно если вспомнить, что кадр в формате YUV420 размером 1920Х1080 занимает 4 MiB (2 байта на пиксель). Меня обнадеживало то, что камера поддерживает аппаратное MJPG сжатие. Эксперименты показали, что сжатый FullHD кадр обычно < 500 KiB. Поэтому я решил продолжить исследования. Выяснилось, что для для захвата видео и стримминга его через HTTP используется mjpg-streamer (http://sourceforge.net/projects/mjpg-streamer/). Анализ его кода показал, что он использует 1 поток для захвата видео + отдельный поток для каждого клиента. Это не лучшее решение для одно-ядерной системы, так как необходима синхронизация потоков и память для стека на каждый поток. Также он копировал захваченные кадры. В общем, mjpg-streamer стал подозреваемым №1.
Интересная находка
Изучая mjpg-streamer я выяснил, что захват видео на Linux делается с помощью библиотеки v4l2 и для захвата используется очередь буферов. Отлаживая инициализацию этих буферов в mjpg-streamer, я обнаружил, что даже для режима MJPG их размер очень большой и неожиданно совпадает с размером несжатого кадра. Так я стал подозревать, что придется залезть в код драйвера UVC, который отвечает за поддержку камер.
Анализ кода драйвера и первый успех
Изучая код я пришел к выводу, что размер буфера спрашивается у камеры и моя камера возвращала размер несжатого кадра. Наверное это самое безопасное решение с точки зрения разработчиков камеры. Но оно же самое не оптимальное. Я решил, что для своего случая можно скорректировать необходимый размер буфера, используя экспериментальный коэффициент минимального сжатия. Я выбрал k=5. С таким значением у меня был запас порядка 20%.
Небольшое отступление.
Строго говоря есть камеры, которые позволяют задать уровень сжатия JPG. Наверное это более правильный способ для определения минимального коэффициента сжатия. Но моя камера не поддерживала эту опцию, и я был вынужден опираться на экспериментальные значения.
Код UVC драйвера оказался готов к добавлению различного рода “специальных” решений, и я легко нашел место, где надо скорректировать размер буфера (функция uvc_fixup_video_ctrl()). Более того, драйвер поддерживает набор quirks, которые позволяют поддерживать камеры с разного рода отклонениями от стандарта UVC. В общем, разработчики драйвера сделали лучшее, что возможно для поддержки зоопарка камер.
Добавив коррекцию размера буфера, я получил стабильную работу в режиме 1280х720 и даже в режиме 1920х1080. Ура! Половина задачи решена!
В поисках новых приключений
Немного порадовавшись первой удаче, я вспомнил, что mjpg-streamer далек от совершенства. Наверняка можно сделать что-то простое, не такое универсальное как mjpg-streamer, но более подходящее для моих условий. Так я решил сделать uvc2http.
В mjpg-streamer мне не понравилось использование нескольких потоков и копирование буферов. Это определило архитектуру решения: 1 поток и никакого копирования. Используя non-blocking IO, это делается достаточно просто: захватываем кадр и без копирования отсылаем его клиенту. Есть небольшая проблема: пока мы отсылаем данные из буфера, мы не можем вернуть буфер обратно в очередь. А пока буфер не в очереди, драйвер не может положить в него новый кадр. Но если размер очереди > 1, то это становится возможным. Число буферов определяет максимальное количество подключений, которое можно гарантированно обслуживать. Т.е., если я хочу гарантированно поддерживать 1 клиента, то 3-х буферов достаточно (в один буфер пишет драйвер, из второго отсылаем данные, третий в запасе, чтобы избежать конкуренции с драйвером за буфер при попытке получить новый кадр).
Uvc2http
Uvc2http состоит из двух компонентов: UvcGrabber и HttpStreamer. Первый отвечает за получение буферов (кадров) из очереди и возврат их обратно в очередь. Второй отвечает за обслуживание клиентов по HTTP. Есть еще немного кода, который связывает эти компоненты. Подробности можно посмотреть в исходниках.
Неожиданная проблема
Все было замечательно: приложение работало и в разрешении 1280х720 выдавало 20+ кадров/сек. Я делал косметические изменения в коде. После очередной порции изменений я замерил частоту кадров. Результат был удручающий — меньше 15 кадров. Я бросился искать, что же привело к деградации. Я потратил, наверное, 2 часа в течение которых частота уменьшалась с каждым замером до значения 7 кадров/сек. В голову лезли разные мысли о деградации из-за долгой работы роутера, из-за его перегрева. Это было что-то непонятное. В какой-то момент я отключил стримминг и увидел, что просто один захват (без стримминга) давал те же 7 кадров. Я даже начал подозревать проблемы с камерой. В общем какая-то чушь. Дело было вечером и камера, повернутая в окно, показывала что-то серое. Дабы сменить мрачное изображение я повернул камеру внутрь комнаты. И, о чудо! Частота кадров увеличилась до 15 и я все понял. Камера автоматически подстраивала время экспозиции и в какой-то момент это время стало больше длительности кадра при заданной частоте. За эти два часа случилось следующее: сначала плавно темнело (это был вечер), а потом я повернул камеру внутрь освещенной комнаты. Направив камеру на люстру я получил 20+ кадров/сек. Ура.
Другие проблемы и нюансы использования
- Автофокус может раздражать. Я задал фиксированный фокус и значение подобрал чтобы было хорошо видно в диапазоне 1-1.5 метра.
- Разные камеры поддерживают разные опции. Чтобы понять, что поддерживает ваша камера, можно воспользоваться утилитой qv4l2, подобрать нужные вам параметры и затем добавить настройку в утилиту. Но бывают сюрпризы: одни и те же настройки могут работать по-разному на разных платформах. В моем случае я столкнулся с разным поведением при одном и том же значении времени экспозиции.
- Питание. Камера питается через USB порт роутера и если напряжение не стабильное, (как например при питании от аккумуляторов) то камера может отключаться (особенно если включен автофокус). Мне помог простой USB хаб (без внешнего питания).
- На роутере очень мало памяти и дискового пространства. По этой причине я отказался от OR-WRT и собрал свой образ OpenWRT, убрав из него все лишнее.
Результаты
Ниже табличка с результатами сравнения mjpg-streamer и uvc2http. Если коротко — есть значительный выигрыш в потреблении памяти и небольшой выигрыш в частоте кадров и загрузке CPU.
1280x720 | 1920x1080 | |||||||||||
VSZ, KB, 1 client | VSZ, KB, 2 clients | CPU, %, 1 client | CPU, %, 2 clients | FPS, f/s, 1 client | FPS, f/s, 2 clients | VSZ, KB, 1 client | VSZ, KB, 2 clients | CPU, %, 1 client | CPU, %, 2 clients | FPS, f/s, 1 client | FPS, f/s, 2 clients | |
Mjpg-streamer | 16860 | 19040 | 26 | 43 | 17.6 | 15 | 25456 | 25812 | 28 | 50 | 13.8 | 10 |
uvc2http | 3960 | 3960 | 26 | 43 | 22 | 19.6 | 7576 | 7576 | 28 | 43 | 15.5 | 12.2 |
Ну и конечно же видео, которое я сделал вместе с детьми:
Фото получившегося танка (получилось что-то вроде цыганской телеги):
Использование
Исходники находятся здесь. Для использования на PC Linux надо всего лишь собрать (при условии что вы не хотите патчить драйвер UVC). Утилита собирается с помощью CMake стандартным способом. Если же надо использовать в OpenWRT, то надо сделать дополнительные шаги:
- Скопировать содержимое директории OpenWrt-15.05 в корень репозитория OpenWRT. Эти файлы только для OpenWRT 15.05. Они описывают новый пакет для OpenWRT и патч для драйвера UVC.
- Если ваша камера также возвращает завышенный размер необходимого буфера, то надо добавить использование quirk UVC_QUIRK_COMPRESSION_RATE для вашей камеры в файле uvc_driver.c. Для этого надо сделать собственный патч для драйвера UVC. Как это сделать, описано здесь wiki.openwrt.org/doc/devel/patches. Вам необходимо добавить описание вашей камеры в массив uvc_ids. В качестве примера можно посмотреть на описание моей камеры:
/* Logitech B910 HD Webcam */ { .match_flags = USB_DEVICE_ID_MATCH_DEVICE | USB_DEVICE_ID_MATCH_INT_INFO, .idVendor = 0x046d, .idProduct = 0x0823, .bInterfaceClass = USB_CLASS_VIDEO, .bInterfaceSubClass = 1, .bInterfaceProtocol = 0, .driver_info = UVC_QUIRK_RESTORE_CTRLS_ON_INIT | UVC_QUIRK_COMPRESSION_RATE }, // Enable buffer correction for compressed modes
- Настроить сборку OpenWRT стандартный методом (http://wiki.openwrt.org/doc/howto/build). При настройке необходимо выбрать пакет uvc2http в меню Multimedia.
- Собрать пакет uvc2http или полный образ (обязательно если вам необходим патч драйвера) для вашей целевой платформы. Если установить утилиту как пакет, то она будет запускаться при старте.
- Установить пакет на устройство/обновить систему
Что дальше
Решение состоит из двух частей: патч драйвера и другой алгоритм стримминга. Патч драйвера можно было бы включить в новую версию ядра линукса, но это спорное решение, так как оно основано на предположении о минимальном коэффициенте сжатия. Утилита же, на мой взгляд, хорошо подходит для использования на слабых системах (игрушках, домашних системах видеонаблюдения), и ее можно немного улучшить, добавив возможность задавать настройки камеры через параметры.
Алгоритм стримминга можно улучшить так как есть запас по загрузке CPU и по ширине канала (я легко получал с роутера 50+ MBit подключая десяток клиентов). Также можно добавить поддержку звука.