Всем доброго дня! Как и обещал, вдогонку к предыдущей статье, осуществим создание еще одного конкретного примера с модификацией приложений стека 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;
};



