HEVC (High Efficiency Video Coding — высокоэффективное кодирование видеоизображений), также известный как H.265, это видеокодек, широко используемый, в том числе, в системах видеонаблюдения. До недавнего времени веб браузеры практически не поддерживали этот формат. Но ситуация изменилась с выходом браузеров Chrome/Chromium версии 106. Это событие показалось мне достойным упоминания на Хабре, и в этой части статьи я расскажу, почему поддержка HEVC важна, о своих попытках подружить IP камеры с браузером и что из этого получилось.
В первой части я рассказывал о проксировании видеопотоков, во второй — о мобильном приложении. В этой статье будут использованы данные из обеих частей.
Итак, по данным caniuse HEVC работает в браузерах на основе Chrome/Chromium не ниже 106 версии (для Linux — не ниже 108 версии) при наличии аппаратного ускорения видео.
Меня интересует, в первую очередь, Linux. Но тут придется немного повозиться. Мне удалось запустить аппаратное декодирование для интегрированных видеокарт Intel HD Graphics 530 и Iris Plus Graphics 655, только указав переменные окружения LIBVA_DRIVER_NAME и LIBVA_DRIVERS_PATH (пример для Дебиан):
export LIBVA_DRIVER_NAME=iHD
export LIBVA_DRIVERS_PATH=/usr/lib/x86_64-linux-gnu/dri
В Федоре путь другой: /usr/lib64/dri, в Арче — /usr/lib/dri.
Кроме того, мне пришлось включить API vulkan в настройках chrome://flags/#enable‑vulkan (или можно запускать браузер с флагами ‑enable‑features=Vulkan,VaapiVideoDecoder).
Конечно, в системе должен быть установлен нужный драйвер видеокарты. Вывод команды vainfo у меня выглядит примерно так:
$ vainfo
Trying display: wayland
vainfo: VA-API version: 1.17 (libva 2.17.1)
vainfo: Driver version: Intel iHD driver for Intel(R) Gen Graphics - 23.1.0 ()
vainfo: Supported profile and entrypoints
...
VAProfileHEVCMain: VAEntrypointVLD
VAProfileHEVCMain: VAEntrypointEncSlice
VAProfileHEVCMain: VAEntrypointFEI
VAProfileHEVCMain10: VAEntrypointVLD
VAProfileHEVCMain10: VAEntrypointEncSlice
...
После этого на странице chrome://gpu/ браузера в разделе Video Acceleration Information должно появиться упоминание HEVC:
К сожалению, это работало только в браузерах на основе Chromium 108. В 109-й версии поддержку libva в «никсах» сломали, и мне пока приходится пользоваться старой версией Brave (я его просто не обновляю). Следить за состоянием поддержки аппаратного декодирования можно, например, тут. В Windows и на Андроиде таких проблем я не наблюдал.
Тестирование MP4
Теперь, наконец, можно проверить, сможет ли браузер воспроизводить запись с IP камеры в формате H.265. Записываю тестовый фрагмент
ffmpeg -i rtsp://<ip_камеры>:554 -c copy -t 10 265.mp4
и открываю его в браузере — работает. Теперь можно рассмотреть варианты доставки видеопотока пользователю, используя только элемент <video>, без сторонних библиотек и зависимостей и не устанавливая на клиентское устройство никаких дополнительных программ и плагинов.
HLS, MPEG-DASH и MSE
Браузер может «нативно» воспроизводить HLS (HTTP Live Streaming) — протокол потоковой передачи от Apple, это не опечатка. Дело в том, что HLS поддерживает совместимый с MPEG‑DASH формат m4s. Записать такой поток можно примерно так:
ffmpeg -i rtsp://<ip_камеры>:554 -c:v copy -an -strftime 1 -strftime_mkdir 1 \
-f hls -hls_segment_type fmp4 -hls_segment_filename %Y%m%d/%H%M%S.m4s live.m3u8
Полученные сегменты можно воспроизвести в браузере, используя MSE (Media Source Extensions).
Пример
const baseUrl = 'http://0.0.0.0:8000';
const initUrl = baseUrl + '/init.mp4';
const templateUrl = baseUrl + '/20230201/$dt$.m4s';
const sourceType = 'video/mp4; codecs="hev1.1.6.L120.0"';
let datetime = '095227';
const mediaSource = new MediaSource();
if ('MediaSource' in window && MediaSource.isTypeSupported(sourceType)) {
mediaSource.addEventListener('sourceopen', onSourceOpen, { once: true });
window.video.src = URL.createObjectURL(mediaSource);
} else {
console.error('Unsupported MIME type or codec: ', sourceType);
}
function onSourceOpen() {
const sourceBuffer = mediaSource.addSourceBuffer(sourceType);
fetch(initUrl)
.then(response => response.arrayBuffer())
.then(data => {
sourceBuffer.appendBuffer(data);
sourceBuffer.addEventListener('updateend', fetchNextSegment, { once: true });
});
}
function fetchNextSegment() {
fetch(templateUrl.replace('$dt$', datetime))
.then(response => response.arrayBuffer())
.then(data => {
const sourceBuffer = mediaSource.sourceBuffers[0];
sourceBuffer.appendBuffer(data);
// TODO: Fetch further segment and append it.
});
}
API mse требует явного указания кодека, в этом примере — hev1.1.6.L120.0. Получить его можно так:
MP4Box -info init.mp4 2>&1 | grep RFC6381 | awk '{print $4}' | paste -sd , -
Впрочем, в моем случае браузер не декодирует поток, поэтому сработает любой совместимый кодек.
Аналогичным образом можно использовать и технологию MPEG‑DASH (Dynamic Adaptive Streaming over HTTP). Команда
ffmpeg -i rtsp://<ip_камеры>:554 -c:v copy -an -f dash -t 10 playlist.mpd
создаст похожую структуру (только strftime тут не поддерживается), сегменты так же будут записаны в файлы .m4s, для воспроизведения можно так же использовать MSE.
Но для воспроизведения сегментов m4s необходим файл инициализации и плейлист, поэтому воспроизвести их напрямую не получится. Мне же хотелось иметь архив, который можно воспроизводить любыми подручными средствами, поэтому я стал поглядывать в сторону «чистого» mp4.
MP4
Сегменты mp4 можно записывать, например, так:
ffmpeg -i rtsp://<ip_камеры>:554 -c:v copy -an -f segment -reset_timestamps 1 \
-strftime 1 %Y%m%d/%H/%M/%S.mp4
Их можно воспроизводить чем угодно, а в браузере не нужен даже MSE. Это меня устроило, и я написал небольшое веб приложение для записи и воспроизведения видеопотоков с камер в режиме почти реального времени.
При создании приложения я использовал наработки проектов python-rtsp-server и Камеры. Не уверен, есть ли смысл дублировать листинги файлов здесь, исходники доступны на гитхабе. Пожалуй, детали реализации не так важны (в проекте около двадцати небольших файлов). Если возникнут вопросы, прошу задавать их в комментариях.
Итак, поскольку я не буду использовать MSE, сегменты придется «склеивать» в непрерывный видеопоток. Просто менять атрибут src тега <video> не получится, даже если предварительно загружать сегменты в blob — изображение при этом мигает. Поэтому для «бесшовного» воспроизведения я использовал два элемента <video>, расположенные друг над другом (с разными значениями z-index). По окончании воспроизведения сегмента элементы просто меняются местами. Атрибут src нижнего (невидимого) элемента меняется заранее, и браузер асинхронно выполняет предварительную загрузку изображения (preload). Это, пожалуй, единственная тонкость в клиентской части приложения.
Вот что получилось в итоге:
Линия с ползунком внизу экрана — шкала времени. По умолчанию ползунок находится в крайнем правом положении, что соответствует прямой трансляции. Доступ ко всему видеоархиву предоставляется с этого же экрана, точки на шкале времени помогают ориентироваться по дням. Здесь, пожалуй, нужно подробнее остановиться на упомянутой выше команде ffmpeg и том, что и как она делает.
Файлы
Ffmpeg просто записывает файлы сегментов, последовательно и непрерывно. Длительность каждого сегмента равна длине GOP (Group of Pictures ), а точнее, интервалу между ключевыми I-кадрами (intra-coded frame). Это минимально возможная длительность сегмента, задается она в настройках камеры и для моего парка камер обычно равна 4 секундам, что при частоте кадров 25 fps соответствует 100 кадрам. Таким образом, при заданной вложенности папок ГГГГММДД/ЧЧ/MM/CC.mp4 каждая конечная папка должна содержать 15 файлов CC.mp4.
Эти файлы не удаляются в течение заданного в файле server/_config.py периода времени (по умолчанию 14 дней) и просто загружаются в элементы <video> клиентской стороны в нужном порядке.
Клиенты не обращаются к камерам напрямую, а читают уже записанные статические сегменты. Таким образом выполняется одно из главных условий — к камере должно быть лишь одно подключение (в первой части статьи я упоминал, что камеры плохо поддерживают множественные подключения).
Размер файла сегмента составляет в среднем 100...200 кБ в зависимости о освещенности сцены и активности движения. Это связано с особенностями алгоритма сжатия HEVC. Впрочем, к сжатию я вернусь позже, а сейчас важно то, что сегменты имеют небольшой размер, а это, в свою очередь, обеспечивает быструю загрузку видео в браузер. Очень быструю загрузку. В среднем в локальной сети (а это наиболее типичный случай применения) со скоростью соединения 1 Гбит и при использовании SSD на сервере сегмент загружается около 50 мс. Это дает возможность менять кадры с частотой до 20 fps при движении ползунка времени, то есть фактически без видимых задержек вручную двигать время в нужном направлении и с нужной скоростью. Такая отзывчивость действительно впечатляет!
Правда, при проксировании трафика через интернет скорость может значительно падать (у меня частота кадров опускалась до 2 fps). Это же относится и к мобильным устройствам, у которых не хватает ни вычислительных ресурсов, ни ширины канала. Впрочем, загружается сегмент все равно значительно быстрее, чем при подключении по rtsp.
Нагрузка
Важным достоинством такой схемы является, помимо скорости, практически нулевая нагрузка и на сервер, и на клиента. Дело в том, что видеопоток передается от камеры до клиента без транскодирования. То есть, грубо говоря, видеопроцессор клиентского устройства воспроизводит видео в том виде, в котором его передает камера. Средняя нагрузка на процессор сервера, по моим наблюдениям, составляет около 0.05 % в зависимости от производительности оборудования. Это дает возможность свободно подключить к одному серверу более сотни камер.
Объяснять важность снижения нагрузки на клиентское устройство, особенно на смартфон, я думаю, не нужно.
Место на диске
При почти нулевой нагрузке и высокой скорости узким местом серверной стороны приложения становится место на диске, дефицит которого заставляет ограничивать либо количество камер, либо длительность видеоархива.
И тут на помощь снова приходит HEVC, а точнее, его модификация H.265+ — cовременный метод компрессии в видеонаблюдении, впервые предложенный компанией Hikvision. Эта технология, в числе прочего, позволяет существенно (по данным компании на 30-50%) сэкономить место на жестком диске. Но и это еще не всё, позже я рассмотрю эту тему под другим углом, а сейчас вернусь к интерфейсу приложения.
Интерфейс
При ширине экрана 1920 пикселей и продолжительности видеоархива 14 дней точность позиционирования шкалы времени (под капотом там обычный <input type="range">) составляет около 10 минут. Это позволяет очень быстро найти нужное событие, если известно его время или событие оставило визуальный след. Быстро, но не очень точно. Чтобы добраться до нужного сегмента, можно воспользоваться дополнительными стрелками над шкалой времени.
Шкала перемотки почти логарифмическая: 1 сегмент (~4 секунды), 1 минута, 10 минут, 1час. Это позволяет попасть в нужное время за ограниченное число последовательных итераций.
Молния в центре — переключатель ускоренного воспроизведения, а значок, похожий на Wi-Fi — детектор движения. Тут, пожалуй, стоит сделать еще одно отступление.
Детектор движения
Эксплуатирует ещё одну особенность алгоритма сжатия Н.265+. Дело в том, что упомянутый выше ключевой кадр, упрощенно говоря, содержит полное изображение сцены, а дальнейшие элементы GOP — лишь её изменение. Это означает, что при появлении в кадре движущегося объекта размер файла сегмента растет, и в случае сжатия Н.265+ растет настолько значительно, что это увеличение размера достаточно достоверно можно считать признаком движения. Поэтому в режиме «детектора движения» я просто сравниваю размер текущего файла с предыдущим и, в случае превышения порогового значения, такой сегмент попадает в на экран. Удивительно, но несмотря на примитивнейший алгоритм, это сработало. Безусловно, этот алгоритм можно и нужно улучшить, но в рамках моего эксперимента по велосипедостроению достаточно просто знать, что это возможно.
Кстати, в режиме H.265+ камеры Hikvision не поддерживают доступ к API встроенной системы оповещения (по крайней мере, мои экземпляры), поэтому такой способ обнаружения вполне имеет право на жизнь.
PWA и SSL
Получившееся приложение я оформил в виде PWA (progressive web app), в основном, ради мобильных устройств. Одним из обязательных требований для работы PWA является наличие валидного SSL сертификата. Но если для обычного интернет ресурса можно использовать, например, сертификат letsencrypt, то в локальной сети это проблематично. Ну и ладно, создам самозаверенный сертификат, примерно так:
sudo openssl genrsa -out rootCA.key 4096
sudo openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 3650 \
-subj "/C=ХХ/L=Nsk/O=R&K/OU=R&K/CN=R&K" -out rootCA.crt
sudo chown $(whoami):$(whoami) rootCA.key
openssl genrsa -out localhost.key 2048
openssl req -new -sha256 -key localhost.key \
-subj "/C=ХХ/L=Nsk/O=localhost/OU=localhost/CN=localhost" -out localhost.csr
openssl x509 -req -sha256 -in localhost.csr -out localhost.crt -days 3650 \
-CAkey rootCA.key -CA rootCA.crt -CAcreateserial -extensions SAN \
-extfile <(printf "[SAN]\nsubjectAltName=DNS:localhost,DNS:ваш-домен,IP:127.0.0.1,IP:ваш-ip")
sudo chown root:root rootCA.key
В этом примере известная корпорация «Рога и копыта» из страны ХХ выдает сертификат не менее известной организации Локалхост. После этого файлы localhost.crt и localhost.key нужно поместить в папку server приложения, а корневой сертификат rootCA.crt импортировать в браузер в разделе chrome://settings/security — Настроить сертификаты — Центры сертификации — Импорт. Конечно, ключ rootCA.key нужно хранить в максимально защищенном месте.
Теперь в меню браузера должен появиться пункт «Установить приложение». Полную информацию о соответствии приложения требованиям PWA можно посмотреть, выполнив анализ страницы на вкладке Lighthouse в инструментах разработчика.
Организация приложения
Настройки приложения находятся в файле server/_config.py и, в целом, аналогичны настройкам python-rtsp-server.
Настройки групп временно находятся в этом же файле. Поясню: группа объединяет несколько камер на одном экране, это важнейшая часть приложения. Позволяет одновременно показывать объект с нескольких ракурсов или несколько объектов. Это самый востребованный экран для оперативного выяснения обстановки.
Приложение целиком закрыто авторизацией. В конфигурации задается мастер-пароль для доступа ко всем камерам и группам и пароль для доступа к отдельным камерам. Для тестирования мне этого достаточно.
Итоги
Прошу не считать сказанное выше непреложной истиной — это всего лишь моё личное мнение, основанное на личном опыте. Приложение находится в стадии раннего альфа-тестирования и ни в коем случае не готово к промышленной эксплуатации. Но у него уже есть очевидные преимущества и недостатки. Чтобы было понятней, сравню cams-pwa с предыдущим проектом Камеры.
Преимущества:
Несмотря на статус эксперимента, в этом приложении уже сейчас больше функционала. По сути, это «киллер-фича» проекта. Похоже, получив свободный доступ к видеоархиву, моя мини-фокус-группа не собирается возвращаться к предыдущей версии:)
Работает на любых устройствах, где можно установить свежий Хром.
Скорость. Подключение по протоколу rtsp может «зависнуть», пока не будет получен ключевой кадр. В зависимости от длины GOP, подключение может занимать более 10 секунд. Это много. Проксирование через сегменты гарантирует почти мгновенное получение картинки.
Простые настройки на стороне клиента. Пользователю не нужно вводить страшные rtsp адреса, айпи, каналы и прочие непонятные штуки.
«Нативное» масштабирование. Из‑за особенностей реализации плагина libvlc, используемого в Камерах, поверхность изображения увеличивается «как есть». Это приводит к тому, что при просмотре камер высокого разрешения на устройствах с более низкой плотностью пикселей увеличенное изображение смазывается. Масштабирование средствами браузера, в числе прочего, решает эту проблему.
Недостатки:
Требуется серверная часть и специалист для её настройки.
Более высокая задержка (отставание) воспроизведения в «прямом эфире». Поскольку в этом проекте воспроизводится последний законченный файл, задержка может составлять от одного до двух GOP. Пока я с этим ничего не делал. Не очень критично из-за того, что отчасти компенсируется временем на открытие приложения, но всё же требует внимания.
Нет звука. Для записи аудиодорожки нужен отдельный канал, этот функционал я (пока) не реализовал.
Сохранение сегментов средствами ffmpeg чувствительно к качеству оборудования, надёжности питания, сетевому соединению и даже производительности файловой системы сервера (но это не точно), особенно при адаптивных алгоритмах сжатия. Ffmpeg имеет тенденцию портить ключевой кадр сегмента. Визуально это проявляется в виде цветных пятен на нижней части изображения или его пикселизации. В равной степени относится к MP4, HLS и DASH.
***
Ну вот, теперь, кажется, предупредил обо всём. Если что-то забыл или в чём-то ошибся, прошу высказываться в комментариях. Конструктивная критика, замечания, пожелания и предложения, как всегда, приветствуются!
UPD: дублирую ссылку на github здесь, чтобы не искать по тексту.
Комментарии (6)
andreymal
06.02.2023 12:27+1Более высокая задержка воспроизведения в «прямом эфире».
Я когда-то пробовал делать очень маленький размер mp4-сегмента (около 100мс и меньше - сегменты даже без ключевых кадров, ага) и отсылать их в браузер через вебсокет - получал задержку около 300мс на стабильном соединении
vladpen Автор
06.02.2023 14:40Спасибо, мысль интересная. Но начать воспроизведение не получится без ключевого кадра. Значит, минимально возможное отставание времени всё же будет от нуля до 1 GOP в теории, если не двигать вперёд время воспроизведения элемента video.
Мы ведь говорим об отставании текущего кадра от реального времени, верно? Можете рассказать подробней?
andreymal
06.02.2023 14:45В своих экспериментах я проблему начала воспроизведения никак не решал: браузер просто ничего не показывал до момента получения сегмента с ключевым кадром. Думаю, теоретически возможно прислать браузеру крайний доступный GOP и двинуть-таки вперёд, но проверять на практике мне лень
Maxim_Q
06.02.2023 17:14Новые версии для приложения Камеры будут?
Я вижу вы сделали версию 2.0 и больше проект за 3 последних месяца увы не развивается.
vladpen Автор
07.02.2023 06:30Камеры поддерживаются, обновляются и находятся в актуальном состоянии, критичных багов нет. Времени на всё не хватает, но проект открыт, пулл реквесты принимаются;).
Какое именно развитие вы хотели бы видеть?
hssergey
Если не нужно непрерывное видео, а достаточно обновляемой раз в несколько секунд картинки, то можно периодически брать картинку с камеры и отсылать на сервер, а на вэб-странице обновлять картинку джаваскриптом.