Top.Mail.Ru

Запись видео в кольцевой буфер и сегментирование файлов.

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

0 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x