Всем доброго дня! Как и обещал, вдогонку к предыдущей статье, осуществим создание еще одного конкретного примера с модификацией приложений стека libcamera. Давайте без лишних вводных слов перейдем к решению сегодняшней задачи. Я снова, в отличие от обычных статей, не буду максимально погружаться в детали, любые возникающие вопросы смело накидывайте в комментарии, это всячески приветствуется 👍
Один из режимов работы libcamera-vid
заключается в использовании кольцевого буфера. В данном случае поток с камеры направляется прямиком в этот буфер, размер которого можно задать отдельно. А при закрытии утилиты libcamera-vid
содержимое буфера сбрасывается в файл. Выглядит все это дело следующим образом:
./libcamera-vid -t 0 --keypress --inline --circular -o test_video.h264
Здесь неявная отсылка к предыдущей статье, поскольку команду выполняем из каталога my-build, в котором мы произвели кастомную сборку libcamera-apps из исходников. К слову, размер буфера по умолчанию составляет 4 МБ, задается после --circular
(также в МБ):
./libcamera-vid -t 0 --keypress --inline --circular 100 -o test_video.h264
В данном случае имеем 100 МБ. С этим все понятно, продолжаем продвигаться к сути. А суть заключается в том, что данный режим бесспорно полезен и хорош, но мне потребовалось иное. А именно запись потока в кольцевой буфер с периодическим сохранением видео-файлов. При этом временные рамки файлов должны пересекаться, лучше всего это рассмотреть на простой наглядной графике:
То есть в результате нужно иметь 4 файла, которые по мере течения времени перезаписываются. Допустим, запустили процесс, прошло 30 секунд, тогда файлы должны охватывать следующие временные интервалы (числовые значения здесь просто для примера):
- Видео 1: 00:00 - 00:15
- Видео 2: 00:05 - 00:20
- Видео 3: 00:10 - 00:25
- Видео 4: 00:15 - 00:30
Дальше опять по кругу пишется файл 1 и так далее. Именно это и реализуем в этом посте и редакции будет подвергнут класс CircularBuffer
, сосредоточенный тут:
- my-libcamera-apps\libcamera-apps\output\circular_output.cpp
- my-libcamera-apps\libcamera-apps\output\circular_output.hpp
Структура каталогов осталась из предыдущей статьи (и снова ссылка).
Первым делом расширим функционал кольцевого буфера, если быть точнее, то понадобится доступ к указателю записи (tail) и получение значения указателя чтения (head), соответственно get-set методы:
void SetRptr(size_t value)
size_t GetWptr()
Реализация:
void SetRptr(size_t value) { rptr_ = value; } size_t GetWptr() { return wptr_; }
В этом же файле circular_output.hpp заодно добавим к классу CircularBuffer
необходимые члены, а также функцию записи в файл:
private: void saveToFile(const char *name); CircularBuffer cb_; FILE *fp_; std::vector<size_t> readPointers; unsigned int readPointerIndex; unsigned int preStartCounter; unsigned int outputBufferPeriodMs; unsigned int filesGapCounter; unsigned int filesGapCounterLimit; unsigned int fileIndex; unsigned int writeCounter;
Все, переходим в circular_output.cpp. Полный код будет традиционно в конце статьи, поэтому в тексте я кратко-бегло пройдусь по избранным кускам. Последовательно определим константы:
fps
- величина fps, которую будем использовать для записи. Функционирование стека у нас завязано на этом значении, поэтому его необходимо знать для установки временных интервалов.- Далее
fileSegmentsNum
- количество итоговых файлов, их будет 4 штуки. - Время “между файлами” -
filesGapPeriodMs
- физически это равно интервалу между точками t_1 и t_2 на графической схеме выше. - И дополнительно зададим требуемые имена файлов, в данном случае они фиксированные, запрашивать их у пользователя не нужно.
В конструкторе только выполняем минимальные расчеты, все остальное выкидываем:
CircularOutput::CircularOutput(VideoOptions const *options) : Output(options), cb_(options->circular<<20) { for (unsigned int i = 0; i < fileSegmentsNum; i++) { readPointers.push_back(0); } readPointerIndex = 0; filesGapCounter = 0; fileIndex = 0; writeCounter = 0; outputBufferPeriodMs = 1000 / fps; preStartCounter = filesGapPeriodMs * (fileSegmentsNum - 1) / outputBufferPeriodMs; filesGapCounterLimit = filesGapPeriodMs / outputBufferPeriodMs; }
Изначально запись в файл производится из деструктора, теперь нам это не требуется, так что деструктор чистим:
CircularOutput::~CircularOutput() { }
В двух словах о самом важном – об алгоритме, в соответствии с которым все это будет работать. Во-первых, мы обеспечили себя отправной точкой, значением outputBufferPeriodMs
. Переменная определяет период, с которым будем вызываться метод outputBuffer()
, от этого и будем плясать во всех временных процессах.
Следующий момент заключается в том, что после запуска выждем некоторое время, чтобы заполнить буфер данными, которые уже можно сбрасывать в файл. Это обеспечит счетчик preStartCounter
. И, наконец, filesGapCounterLimit
- это просто величина, определяющая, сколько раз произойдет вызов
за время outputBuffer()
filesGapPeriodMs
.
Как действуем дальше… Каждые 8 секунд (filesGapPeriodMs
) сохраняем в вектор текущее значение указателя записи (tail) кольцевого буфера:
void CircularOutput::outputBuffer(void *mem, size_t size, int64_t timestamp_us, uint32_t flags) { // First make sure there's enough space. int pad = (ALIGN - size) & (ALIGN - 1); while (size + pad + sizeof(Header) > cb_.Available()) { if (cb_.Empty()) throw std::runtime_error("circular buffer too small"); Header header; uint8_t *dst = (uint8_t *)&header; cb_.Read( [&dst](void *src, int n) { memcpy(dst, src, n); dst += n; }, sizeof(header)); cb_.Skip((header.length + ALIGN - 1) & ~(ALIGN - 1)); } if (writeCounter == filesGapCounterLimit) { writeCounter = 0; readPointers[readPointerIndex] = cb_.GetWptr(); readPointerIndex++; if (readPointerIndex == fileSegmentsNum) { readPointerIndex = 0; } } Header header = { static_cast<unsigned int>(size), !!(flags & FLAG_KEYFRAME), timestamp_us }; cb_.Write(&header, sizeof(header)); cb_.Write(mem, size); cb_.Pad(pad); // ...............
Далее обслуживаем счетчики и целевая работа - сохраняем данные в файл. Для этого по указателю head помещаем одно из сохраненных в readPointers
значений и производим запись в файл:
writeCounter++; if (preStartCounter > 0) { preStartCounter--; return; } filesGapCounter++; if (filesGapCounter == filesGapCounterLimit) { filesGapCounter = 0; cb_.SetRptr(readPointers[fileIndex]); saveToFile(fileNames[fileIndex]); fileIndex++; if (fileIndex == fileSegmentsNum) { fileIndex = 0; } }
Итогом будет сохранение в файл данных, начиная с указателя и до текущей позиции буфера, запись в который продолжается. Отдельно привожу одним куском функцию записи:
void CircularOutput::saveToFile(const char *name) { unsigned int total = 0, frames = 0; bool seen_keyframe = false; Header header; int cnt = 0; fp_ = fopen(name, "w"); FILE *fp = fp_; while (!cb_.Empty()) { uint8_t *dst = (uint8_t *)&header; cb_.Read( [&dst](void *src, int n) { memcpy(dst, src, n); dst += n; }, sizeof(header)); seen_keyframe |= header.keyframe; if (seen_keyframe) { cb_.Read([fp](void *src, int n) { fwrite(src, 1, n, fp); }, header.length); cb_.Skip((ALIGN - header.length) & (ALIGN - 1)); total += header.length; if (fp_timestamps_) { Output::timestampReady(header.timestamp); } frames++; } else cb_.Skip((header.length + ALIGN - 1) & ~(ALIGN - 1)); cnt++; } fclose(fp_); LOG(1, "Wrote " << total << " bytes (" << frames << " frames)"); }
Вот вроде бы и все. Пример я пишу параллельно с текстом статьи, на скорую руку и на коленке, поэтому что-то мог упустить/не учесть/не оптимизировать. Переходим к компиляции и проверке, собираем:
make -j4
Запускаем:
./libcamera-vid --framerate 24 -t 0 --circular 40 --inline
Имена файлов заданы в коде, поэтому передавать их тут нет необходимости. Задаем же дополнительно fps и размер кольцевого буфера. Запустили, подождали, на выходе получили 4 файла, пока все идет по плану. Проверяем, какие временные промежутки оказались охвачены этими файлами:
То есть в итоге:
- Файл custom_video_0.h264: 10:52-11:14
- Файл custom_video_1.h264: 11:00-11:22
- Файл custom_video_2.h264: 11:08-11:30
- Файл custom_video_3.h264: 11:16-11:38
Ровно то, что и требовалось, прекрасно 👍 На этой мажорной ноте на сегодня завершаем, всем спасибо за внимание и прочтение, до скорого 🤝
/* SPDX-License-Identifier: BSD-2-Clause */ /* * Copyright (C) 2020, Raspberry Pi (Trading) Ltd. * * circular_output.cpp - Write output to circular buffer which we save on exit. */ #include "circular_output.hpp" // We're going to align the frames within the buffer to friendly byte boundaries static constexpr int ALIGN = 16; // power of 2, please static constexpr unsigned int fps = 24; static constexpr unsigned int fileSegmentsNum = 4; static constexpr unsigned int filesGapPeriodMs = 8000; const char* fileNames[4] = {"custom_video_0.h264", "custom_video_1.h264", "custom_video_2.h264", "custom_video_3.h264"}; struct Header { unsigned int length; bool keyframe; int64_t timestamp; }; static_assert(sizeof(Header) % ALIGN == 0, "Header should have aligned size"); // Size of buffer (options->circular) is given in megabytes. CircularOutput::CircularOutput(VideoOptions const *options) : Output(options), cb_(options->circular<<20) { for (unsigned int i = 0; i < fileSegmentsNum; i++) { readPointers.push_back(0); } readPointerIndex = 0; filesGapCounter = 0; fileIndex = 0; writeCounter = 0; outputBufferPeriodMs = 1000 / fps; preStartCounter = filesGapPeriodMs * (fileSegmentsNum - 1) / outputBufferPeriodMs; filesGapCounterLimit = filesGapPeriodMs / outputBufferPeriodMs; } CircularOutput::~CircularOutput() { } void CircularOutput::saveToFile(const char *name) { unsigned int total = 0, frames = 0; bool seen_keyframe = false; Header header; int cnt = 0; fp_ = fopen(name, "w"); FILE *fp = fp_; while (!cb_.Empty()) { uint8_t *dst = (uint8_t *)&header; cb_.Read( [&dst](void *src, int n) { memcpy(dst, src, n); dst += n; }, sizeof(header)); seen_keyframe |= header.keyframe; if (seen_keyframe) { cb_.Read([fp](void *src, int n) { fwrite(src, 1, n, fp); }, header.length); cb_.Skip((ALIGN - header.length) & (ALIGN - 1)); total += header.length; if (fp_timestamps_) { Output::timestampReady(header.timestamp); } frames++; } else cb_.Skip((header.length + ALIGN - 1) & ~(ALIGN - 1)); cnt++; } fclose(fp_); LOG(1, "Wrote " << total << " bytes (" << frames << " frames)"); } void CircularOutput::outputBuffer(void *mem, size_t size, int64_t timestamp_us, uint32_t flags) { // First make sure there's enough space. int pad = (ALIGN - size) & (ALIGN - 1); while (size + pad + sizeof(Header) > cb_.Available()) { if (cb_.Empty()) throw std::runtime_error("circular buffer too small"); Header header; uint8_t *dst = (uint8_t *)&header; cb_.Read( [&dst](void *src, int n) { memcpy(dst, src, n); dst += n; }, sizeof(header)); cb_.Skip((header.length + ALIGN - 1) & ~(ALIGN - 1)); } if (writeCounter == filesGapCounterLimit) { writeCounter = 0; readPointers[readPointerIndex] = cb_.GetWptr(); readPointerIndex++; if (readPointerIndex == fileSegmentsNum) { readPointerIndex = 0; } } Header header = { static_cast<unsigned int>(size), !!(flags & FLAG_KEYFRAME), timestamp_us }; cb_.Write(&header, sizeof(header)); cb_.Write(mem, size); cb_.Pad(pad); writeCounter++; if (preStartCounter > 0) { preStartCounter--; return; } filesGapCounter++; if (filesGapCounter == filesGapCounterLimit) { filesGapCounter = 0; cb_.SetRptr(readPointers[fileIndex]); saveToFile(fileNames[fileIndex]); fileIndex++; if (fileIndex == fileSegmentsNum) { fileIndex = 0; } } } void CircularOutput::timestampReady(int64_t timestamp) { // Don't want to save every timestamp as we go along, only outputs them at the end }
/* SPDX-License-Identifier: BSD-2-Clause */ /* * Copyright (C) 2020, Raspberry Pi (Trading) Ltd. * * circular_output.hpp - Write output to a circular buffer. */ #pragma once #include "output.hpp" // A simple circular buffer implementation used by the CircularOutput class. class CircularBuffer { public: CircularBuffer(size_t size) : size_(size), buf_(size), rptr_(0), wptr_(0) {} bool Empty() const { return rptr_ == wptr_; } size_t Available() const { return wptr_ == rptr_ ? size_ - 1 : (size_ - wptr_ + rptr_) % size_ - 1; } void Skip(unsigned int n) { rptr_ = (rptr_ + n) % size_; } // The dst function allows bytes read to go straight to memory or a file etc. void Read(std::function<void(void *src, unsigned int n)> dst, unsigned int n) { if (rptr_ + n >= size_) { dst(&buf_[rptr_], size_ - rptr_); n -= size_ - rptr_; rptr_ = 0; } dst(&buf_[rptr_], n); rptr_ += n; } void Pad(unsigned int n) { wptr_ = (wptr_ + n) % size_; } void Write(const void *ptr, unsigned int n) { if (wptr_ + n >= size_) { memcpy(&buf_[wptr_], ptr, size_ - wptr_); n -= size_ - wptr_; ptr = static_cast<const uint8_t *>(ptr) + size_ - wptr_; wptr_ = 0; } memcpy(&buf_[wptr_], ptr, n); wptr_ += n; } void SetRptr(size_t value) { rptr_ = value; } size_t GetWptr() { return wptr_; } private: const size_t size_; std::vector<uint8_t> buf_; size_t rptr_, wptr_; }; // Write frames to a circular buffer, and dump them to disk when we quit. class CircularOutput : public Output { public: CircularOutput(VideoOptions const *options); ~CircularOutput(); protected: void outputBuffer(void *mem, size_t size, int64_t timestamp_us, uint32_t flags) override; void timestampReady(int64_t timestamp) override; private: void saveToFile(const char *name); CircularBuffer cb_; FILE *fp_; std::vector<size_t> readPointers; unsigned int readPointerIndex; unsigned int preStartCounter; unsigned int outputBufferPeriodMs; unsigned int filesGapCounter; unsigned int filesGapCounterLimit; unsigned int fileIndex; unsigned int writeCounter; };