В одной (и даже не в одной, а в нескольких) из предыдущих статей мы разбирали совместную работу камеры High Quality Camera с Raspberry Pi посредством стека libcamera. Так вот сегодня в некотором роде продолжим эту тему и рассмотрим, как собрать приложения libcamera из исходников, и, что не менее важно, разберем пример, чтобы понять, зачем это вообще может быть нужно.
Собирать будем именно libcamera-apps, так как это более востребовано на практике, к этому и переходим. Зафиксируем исходные данные – у меня плата Raspberry Pi 4 Model B 8 Gb и операционная система на данный момент следующая:
Raspberry Pi OS Release date: September 22nd 2022 System: 32-bit Kernel version: 5.15 Debian version: 11 (bullseye)
Это не критично, но чуть другой набор может потребовать незначительных изменений в осуществляемых процессах. А может и не потребовать. Если что, пишите в комментариях – с любыми нюансами разберемся, это не проблема. Все, приступаем.
Сборка libcamera-apps.
Начало традиционное – устанавливаем необходимые пакеты:
sudo apt install -y libcamera-dev libepoxy-dev libjpeg-dev libtiff5-dev sudo apt install -y qtbase5-dev libqt5core5a libqt5gui5 libqt5widgets5 sudo apt install -y cmake libboost-program-options-dev libdrm-dev libexif-dev
Update
В определенный момент в libcamera-apps обновилась система сборки (где-то в районе мая 2023), поэтому добавляю обновленные же команды/инструкции. Соответственно, если используем обновленную версию, дальше по тексту нас интересуют секции с обновленными командами 👍 Итак необходимые пакеты:
sudo apt install -y cmake libboost-program-options-dev libdrm-dev libexif-dev sudo pip3 install ninja meson
Далее клонируем исходники:
mkdir my-libcamera-apps cd my-libcamera-apps git clone https://github.com/raspberrypi/libcamera-apps.git cd libcamera-apps
В результате получили каталог:
Создаем отдельный каталог для сборки, в который и переходим:
mkdir my-build cd my-build
В общем, стандартные, не обременительные шаги 👍 Далее нужно запустить cmake
, скормив ему необходимые флаги. Допустимые опции:
-DENABLE_COMPILE_FLAGS_FOR_TARGET=armv8-neon | Пост-обработка может происходить быстрее на 32-битной ОС (для Raspberry Pi 3 и Raspberry Pi 4) |
-DENABLE_DRM=1 / -DENABLE_DRM=0 | DRM/KMS рендеринг превью |
-DENABLE_X11=1 / -DENABLE_X11=0 | Превью на базе X Windows |
-DENABLE_QT=1 / -DENABLE_QT=0 | Поддержка превью-окна на базе Qt. Обычно не рекомендуется включать |
-DENABLE_OPENCV=1 / -DENABLE_OPENCV=0 | Пост-обработка на базе OpenCV |
-DENABLE_TFLITE=1 / -DENABLE_TFLITE=0 | Включение / отключение TensorFlow Lite пост-обработки |
Собственно, короткий путь – официальная документация предлагает два конкретных варианта, для Raspberry Pi OS:
cmake .. -DENABLE_DRM=1 -DENABLE_X11=1 -DENABLE_QT=1 -DENABLE_OPENCV=0 -DENABLE_TFLITE=0
Для Raspberry Pi OS Lite:
cmake .. -DENABLE_DRM=1 -DENABLE_X11=0 -DENABLE_QT=0 -DENABLE_OPENCV=0 -DENABLE_TFLITE=0
Update
И обновленные варианты, для Raspberry Pi OS:
meson setup build -Denable_libav=true -Denable_drm=true -Denable_egl=true -Denable_qt=true -Denable_opencv=false -Denable_tflite=false
Для Raspberry Pi OS Lite:
meson setup build -Denable_libav=false -Denable_drm=true -Denable_egl=false -Denable_qt=false -Denable_opencv=false -Denable_tflite=false
Первый случай мне отлично подходит, нет поводов им не воспользоваться, само собой, все что указано в таблице выше по-прежнему в силе. Я от случая к случаю использую разные ОС, так что, по итогу, выбираем опции в соответствии с таблицей и своим конкретным случаем, и все будет в порядке.
Приготовления завершены, непосредственно сборка:
make -j4
Update
meson compile -C build
Для предыдущих модификаций Raspberry Pi, среди которых в данном контексте Raspberry Pi 3 и более ранние, используем:
make -j1
Update
meson compile -j1 -C build
Здесь мы не собираем libcamera вручную, нам нужны только libcamera-apps, поэтому существует вероятность несовместимости текущей версии пакета libcamera-dev и самой актуальной версии libcamera-apps, которую мы взяли с github'а. В данном случае самым правильным будет собрать libcamera также руками. Если будет необходимость, добавлю аналогичные инструкции для сборки отдельным постом, пишите в комментарии. В случае ошибок также пишите туда же )
Вновь собранные приложения получаем в libcamera-apps/my-build, как и ожидалось:
Далее официальный манускрипт рекомендует выполнить установку:
sudo make install sudo ldconfig # this is only necessary on the first build
Update
sudo meson install -C build sudo ldconfig # this is only necessary on the first build
Но это такое себе дело, особенно в современных дистрибутивах, в общем, данная операция требует как минимум вдумчивости и осознанности, так что смотрите сами, надо вам это или нет. Моя рекомендация – с этим не спешить, так что этот этап пропускаем.
И на данный момент мы имеем базовую версию libcamera-vid
, которая чаще всего по умолчанию уже установлена в ОС и нашу кастомную версию, собранную руками. Внесем простейшие тестовые изменения в:
my-libcamera-apps/libcamera-apps/apps/libcamera_vid.cpp
Кинем в лог:
LOG(1, "This is my custom libcamera-vid version");
try { LOG(1, "This is my custom libcamera-vid version"); LibcameraEncoder app; VideoOptions *options = app.GetOptions(); if (options->Parse(argc, argv)) { if (options->verbose >= 2) options->Print(); event_loop(app); } } catch (std::exception const &e) { LOG_ERROR("ERROR: *** " << e.what() << " ***"); return -1; }
Пересобираем:
make -j4
Update
meson compile -C build
Переходим в my-build и запускаем кастомную версию:
./libcamera-vid -t 2000
В итоге в консоли видим вывод:
Отлично, проверим работоспособность базовой версии, сохранившей свою первозданность:
libcamera-vid -t 2000
В данном случае вывод в консоль будет иметь первоначальный вид. Все, работоспособность не нарушена, процессы осуществлены успешно. Переходим к парочке конкретных случаев-примеров, как и зачем это может использоваться.
Практический пример использования.
Собственно, даже нет особой необходимости заниматься придумыванием искусственных задач – реальный пример из недавних. Мне нужно разветвить поток с камеры и вывести в два отдельных окна. При всем при этом в одном окне должно быть live-preview, то есть изображение с камеры в реальном времени. Во втором же необходимо выводить то же самое, но с задержкой.
Непосредственно для формирования задержки один из вариантов – использовать команду delay
. Представляет она из себя следующее:
delay introduces a constant delay between its standard input and its standard output. The data from its stdin will be stored until it has been written to stdout.
Лучше и не скажешь, устанавливаем:
sudo apt -y install delay
Ах да, я забыл упомянуть, что параллельно необходимо записывать видео в файл. Ключевой нюанс 🙂 Если бы не это, то можно было бы просто перенаправить поток в stdout
и воспроизвести хоть тем же mpv:
./libcamera-vid -t 0 -o /dev/stdout | delay 2s | mpv /dev/stdin
Аналогичный эффект дает упрощенно-сокращенная запись:
./libcamera-vid -t 0 -o - | delay 2s | mpv -
Величина на практике будет иной, то есть ровно 2 секунды мы таким образом не получим, здесь в случае необходимости нужно провести дополнительную настройку/калибровку. Суть в том, что работает все корректно:
Но(!) Как мы выяснили, параллельно нужно скинуть видео в файл, то есть будем использовать:
./libcamera-vid -t 0 -o test_video.h264
А для решения полной задачи отредактируем libcamera-vid
так, чтобы поток выводился в stdout
всегда, вне зависимости от аргументов. Досконально описывать не буду, в случае чего, пишите в комментарии или на форуме, по мере возможности я всегда стараюсь помочь. В область нашего интереса, в первую очередь, попадает класс FileOutput
:
- my-libcamera-apps\libcamera-apps\output\file_output.cpp
- my-libcamera-apps\libcamera-apps\output\file_output.hpp
По умолчанию поток отправляется в файл:
FILE *fp_;
Добавляем свой:
FILE *fp_stdout;
В конструкторе:
fp_stdout = stdout;
Все максимально просто и логично 👍 Основную работу произведем в функции:
void FileOutput::outputBuffer(void *mem, size_t size, int64_t timestamp_us, uint32_t flags)
Она вызывается после каждого фрейма – то что надо:
void FileOutput::outputBuffer(void *mem, size_t size, int64_t timestamp_us, uint32_t flags) { if (fp_stdout && size) { if (fwrite(mem, size, 1, fp_stdout) != 1) throw std::runtime_error("failed to write output bytes to stdout"); if (options_->flush) fflush(fp_stdout); } // We need to open a new file if we're in "segment" mode and our segment is full // (though we have to wait for the next I frame), or if we're in "split" mode // and recording is being restarted (this is necessarily an I-frame already). if (fp_ == nullptr || (options_->segment && (flags & FLAG_KEYFRAME) && timestamp_us / 1000 - file_start_time_ms_ > options_->segment) || (options_->split && (flags & FLAG_RESTART))) { closeFile(); openFile(timestamp_us); } LOG(2, "FileOutput: output buffer " << mem << " size " << size); if (fp_ && size) { if (fwrite(mem, size, 1, fp_) != 1) throw std::runtime_error("failed to write output bytes"); if (options_->flush) fflush(fp_); } }
Ничего лишнего, только необходимое. Пробуем собрать и запустить, сборка по-прежнему командой:
make -j4
И запуск:
./libcamera-vid -t 0 -o test_video.h264 | delay 2s | mpv -
Результат:
Иначе как успехом не назовешь, все в соответствии с ТЗ:
- Превью в реальном времени
- Отложенное видео в отдельном окне
- Параллельно все пишется в файл
Таким образом, внеся незначительные изменения и проведя модификации, получили решение конкретной имеющейся задачи. Еще один пример, пожалуй, приведу в отдельной статье, чтобы не перегружать объемом эту, так что оставайтесь на связи!
/* SPDX-License-Identifier: BSD-2-Clause */ /* * Copyright (C) 2020, Raspberry Pi (Trading) Ltd. * * file_output.cpp - Write output to file. */ #include "file_output.hpp" FileOutput::FileOutput(VideoOptions const *options) : Output(options), fp_(nullptr), count_(0), file_start_time_ms_(0) { fp_stdout = stdout; } FileOutput::~FileOutput() { closeFile(); } void FileOutput::outputBuffer(void *mem, size_t size, int64_t timestamp_us, uint32_t flags) { if (fp_stdout && size) { if (fwrite(mem, size, 1, fp_stdout) != 1) throw std::runtime_error("failed to write output bytes to stdout"); if (options_->flush) fflush(fp_stdout); } // We need to open a new file if we're in "segment" mode and our segment is full // (though we have to wait for the next I frame), or if we're in "split" mode // and recording is being restarted (this is necessarily an I-frame already). if (fp_ == nullptr || (options_->segment && (flags & FLAG_KEYFRAME) && timestamp_us / 1000 - file_start_time_ms_ > options_->segment) || (options_->split && (flags & FLAG_RESTART))) { closeFile(); openFile(timestamp_us); } LOG(2, "FileOutput: output buffer " << mem << " size " << size); if (fp_ && size) { if (fwrite(mem, size, 1, fp_) != 1) throw std::runtime_error("failed to write output bytes"); if (options_->flush) fflush(fp_); } } void FileOutput::openFile(int64_t timestamp_us) { if (options_->output == "-") fp_ = stdout; else if (!options_->output.empty()) { // Generate the next output file name. char filename[256]; int n = snprintf(filename, sizeof(filename), options_->output.c_str(), count_); count_++; if (options_->wrap) count_ = count_ % options_->wrap; if (n < 0) throw std::runtime_error("failed to generate filename"); fp_ = fopen(filename, "w"); if (!fp_) throw std::runtime_error("failed to open output file " + std::string(filename)); LOG(2, "FileOutput: opened output file " << filename); file_start_time_ms_ = timestamp_us / 1000; } } void FileOutput::closeFile() { if (fp_ && fp_ != stdout) fclose(fp_); fp_ = nullptr; }
/* SPDX-License-Identifier: BSD-2-Clause */ /* * Copyright (C) 2020, Raspberry Pi (Trading) Ltd. * * file_output.hpp - Write output to file. */ #pragma once #include "output.hpp" class FileOutput : public Output { public: FileOutput(VideoOptions const *options); ~FileOutput(); protected: void outputBuffer(void *mem, size_t size, int64_t timestamp_us, uint32_t flags) override; private: void openFile(int64_t timestamp_us); void closeFile(); FILE *fp_; FILE *fp_stdout; unsigned int count_; int64_t file_start_time_ms_; };