Загрузка 0
ПОДЕЛИТЬСЯ

Мой блог

Листай вниз

Архитектура компактного рекордера: как записывать видео с экрана при 40 МБ ОЗУ вместо 500 МБ в OBS

Архитектура компактного рекордера: как записывать видео с экрана при 40 МБ ОЗУ вместо 500 МБ в OBS

Когда речь заходит о записи рабочего стола или геймплея, большинство пользователей сразу думает про OBS Studio. Однако OBS — это мощный комбайн, который даже в простое легко отбирает от 400 до 500 МБ оперативной памяти и создает ощутимую нагрузку на систему. В этой статье я хочу подробно разобрать архитектурные решения лёгкого опенсорсного рекордера HomRec, который справляется с той же задачей, укладываясь всего в 40 МБ ОЗУ. На примере этой программы мы рассмотрим, за счёт чего достигается столь высокая экономия ресурсов без потери качества и плавной частоты кадров.

Архитектура монолита и работа с динамическими библиотеками

Раньше ключевой функционал программы — непосредственно захват экрана, вспомогательные функции кодирования и встроенный секундомер — был разнесён по отдельным DLL-библиотекам (hr_dxgi_capture.dll, hr_encoder_helpers.dll, hr_stopwatch.dll). Главный исполняемый файл hr.exe подгружал их в режиме реального времени через стандартный механизм LoadLibrary. В свежих версиях от такой структуры отказались: всю логику скомпилировали напрямую в единый бинарный файл.

Интерфейс и настройки программы HomRec
Внешний вид интерфейса компактной программы для видеозаписи.
Сервер с двумя картами Nvidia Tesla P100 для нейросетей
Конфигурация локального сервера с видеокартами Tesla P100 для запуска ИИ-моделей.

При этом рядом с исполняемым файлом всё ещё располагаются внешние библиотеки DLL. Это не забытый разработчиками технический долг, а вполне осознанный шаг. Графический фреймворк wxWidgets собирается динамически с флагом -DWXUSINGDLL. Через него в систему подтягиваются компоненты для обработки и декодирования изображений (например, libjpeg, libpng, libtiff, libwebp), а также рантайм MinGW. Таким образом, понятие «единый бинарник» здесь относится к ядру самого рекордера, а не к полному отсутствию внешних зависимостей в папке приложения.

Реклама

Как кадры попадают в программу: GDI против DXGI Desktop Duplication

В операционной системе Windows существуют два традиционных подхода к захвату содержимого экрана. Первый — использование устаревшего интерфейса GDI. Он работает крайне медленно и создаёт высокую системную задержку. Второй вариант — современный интерфейс DXGI Desktop Duplication API, на котором и построен HomRec.

Принципиальное преимущество DXGI заключается в том, что графический процессор самостоятельно формирует и отдаёт готовую текстуру рабочего стола. Программе не требуется вычитывать пиксели по частям через центральный процессор. Вызов функции AcquireNextFrame возвращает текстуру прямо из видеопамяти. Если картинка на экране остаётся статичной и ничего не меняется, API возвращает таймаут, позволяя повторно использовать предыдущий кадр и не тратить вычислительные ресурсы понапрасну.

Схема двойной буферизацией кадров в HomRec
Иллюстрация процесса двойной буферизации для исключения задержек при видеозахвате.

Однако здесь возникает техническое препятствие: исходная GPU-текстура недоступна для прямого чтения из оперативной памяти процессора. Её необходимо сначала скопировать со специального графического ресурса в так называемую «staging-текстуру» и выполнить процедуру отображения памяти (Map). Именно на этом этапе кроется важная оптимизация.

Реклама

Трюк с двойной буферизацией: ликвидация фризов на раскалённой GPU

Самый простой и наивный алгоритм захвата выглядит следующим образом: получить новый кадр от видеокарты, скопировать его в staging-текстуру, замапить память, перенести пиксели во внутренний буфер приложения и отпустить исходный кадр. При такой последовательной схеме каждый шаг ожидает завершения предыдущего, что неизбежно приводит к микрозадержкам.

Для решения проблемы применён метод двойной буферизации со сдвигом на один кадр. Программа выделяет две staging-текстуры. Когда поступает кадр N, он мгновенно копируется в staging[write_idx], после чего DXGI-кадр сразу освобождается без ожидания операций Map/Unmap. Это освобождает захват DXGI и позволяет графической системе параллельно готовить следующий кадр. При этом наружу отдаётся отлежавшийся кадр из второго буфера — staging[pending_idx], где процедура копирования гарантированно завершилась. Конвейер захвата работает со сдвигом в один шаг назад, что полностью устраняет задержки ввода-вывода.

Не менее важной деталью оказалась разность таймаутов. Для получения кадра AcquireNextFrame используется короткий интервал ожидания (в доли кадра), а вот для вызова Map() выделен щедрый бюджет в 250 миллисекунд. В ранних версиях для обеих операций применялся одинаковый короткий таймаут. В результате при записи требовательных 3D-игр перегруженная видеокарта не успевала завершить копирование памяти за 8–33 мс. Программа ошибочно считала, что кадр пропущен, и дублировала старое изображение. Видео замирало на несколько секунд, тогда как аудиопоток, записываемый через службы WASAPI со своими независимыми часами, продолжал идти вперед. Увеличение лимита ожидания для чтения из памяти полностью решило проблему рассинхронизации звука и видео.

Преобразование BGRA в YUV420p на процессоре

Кадры, поступающие с экрана через DXGI, имеют формат цветности BGRA. Однако большинство кодеков для сжатия видео требуют формат YUV420p. Преобразование цветов можно было бы переложить на шейдеры GPU или сторонние библиотеки, однако в HomRec конвертация намеренно выполняется вручную на центральном процессоре.

Схема передачи видеопотока из HomRec в процесс FFmpeg
Конвейер передачи сырых YUV-кадров в FFmpeg через асинхронный именованный канал.

Кадр разбивается по высоте на несколько горизонтальных полос (bands). Каждая полоса отправляется на обработку отдельному потоку из компактного пула воркеров. Такое решение продиктовано желанием сохранить работоспособность программы на слабых или встроенных графических чипах (включая интегрированную графику Intel UHD). На подобных системах передача дополнительных вычислительных задач на GPU вызывает просадку FPS в играх, тогда как несколько свободных потоков многоядерного ЦП справляются с такой задачей без лишних задержек. Кроме того, избавившись от лишних шейдеров, программа становится полностью независимой от особенностей и версий видеодрайверов.

Передача кадров в FFmpeg: отказ от временных файлов и синхронных пайпов

После завершения конвертации готовый кадр в формате YUV необходимо передать кодировщику FFmpeg. Вариант со сохранением промежуточных данных во временные файлы на диске был сразу отклонён из-за высоких задержек и лишнего износа накопителя. HomRec транслирует сырой видеопоток непосредственно в стандартный поток ввода (stdin) фонового процесса FFmpeg с параметрами -f rawvideo -pixel_format yuv420p -i pipe:0.

На раннем этапе здесь тоже проявился серьезный баг. Первоначально для связи процессов использовался обычный анонимный канал Windows (CreatePipe), поддерживающий исключительно блокирующий ввод-вывод. Когда кодировщик FFmpeg на мгновение задерживал чтение данных из-за пиковой нагрузки, поток записи намертво замирал в ожидании функции WriteFile, что периодически приводило к аварийному завершению программы через std::terminate().

Реклама

Проблему решили переходом на именованные каналы с флагом асинхронной работы FILE_FLAG_OVERLAPPED. Со стороны FFmpeg канал по-прежнему выглядит как стандартный синхронный поток. Однако теперь пишущая сторона HomRec может отправлять данные с ограничением по времени и штатно отменять зависшие операции, предотвращая блокировку всей системы захвата.

Мгновенный повтор (Instant Replay) через кольцевой буфер

Функционал записи мгновенных повторов (Save Replay по клавише F8) позволяет сохранять последние секунды происходящего на экране без предварительного запуска постоянной записи. Эта возможность реализована без создания отдельной подсистемы — используется тот же механизм и тот же именованный канал передачи данных в FFmpeg.

Разница заключается в аргументах запуска кодировщика. Вместо обычной записи в один файл FFmpeg запускается с сегментным мультиплексором: -f segment -segment_time 5 -segment_wrap N -reset_timestamps 1. Кодировщик нарезает поступающий видеопоток на небольшие ролики по 5 секунд (seg_000.mp4, seg_001.mp4 и так далее) и перезаписывает их по кругу. На диске всегда сохраняется фиксированный буфер последних секунд видео.

При нажатии hotkey F8 происходит следующее:

  • Текущий процесс сегментации останавливается для корректной финализации последнего файла.
  • Мгновенно запускается новый процесс FFmpeg во временной папке с новым порядковым номером.
  • Накопленные сегменты из предыдущей папки отсортировываются по времени модификации файла (mtime), обрезаются под нужную хронометрию и объединяются в итоговый видеоролик с помощью демультиплексора concat.

Оверлеи: нюансы интеграции веб-камеры и GIF-анимаций

В ранних версиях оверлей веб-камеры отображался в окне предварительного просмотра, но не попадал в итоговый видеофайл. Проблему исправили путём внедрения современного фреймворка Media Foundation (интерфейс IMFSourceReader) взамен устаревшего DirectShow, который вызывает проблемы в современных 64-битных версиях Windows.

Кадры с веб-камеры считываются в отдельном потоке. Компоновщик оверлеев забирает самый последний готовый кадр в момент сборки финального изображения. Для анимаций в формате GIF применяется иной подход: все кадры сразу декодируются в память через Windows Imaging Component (WIC). Метаданные (задержка кадра, параметры очистки) вычитываются один раз при загрузке, а во время записи нужный кадр выбирается строго по системным часам, что гарантирует правильную скорость воспроизведения анимации вне зависимости от текущего FPS записи.

Модуль плагинов: почему выбор пал на Lua 5.4

Первоначально рассматривалась идея добавить поддержку скриптов на Python. Однако необходимость поставлять громоздкий интерпретатор Python вместе со всеми библиотеками сводила на нет главную цель проекта — минимальное потребление системных ресурсов. В итоге выбор сделали в пользу встроенного движка Lua 5.4, который компилируется непосредственно в исполняемый файл и практически не увеличивает размер программы.

Плагин представляет собой каталог с конфигурационным файлом plugin.json и скриптом main.lua. Скрипты получают доступ к обработчикам событий (на старт и остановку записи), сетевым запросам и системе уведомлений. Для удобства плагины упаковываются в архивы с расширением .hrp, которые можно устанавливать вручную или через встроенный консольный менеджер пакетов hom install.

Локальный ИИ класса Opus 4.6 «задешево»: две Tesla P100, 28-поточный Xeon и тесты на реальных задачах

Помимо оптимизации десктопного ПО, представляет интерес тема локального развёртывания нейросетей для независимости от облачных сервисов. Для таких задач отлично подходит сборка домашнего сервера на базе двух серверных ускорителей Nvidia Tesla P100 (суммарно 32 ГБ видеопамяти VRAM) и 28-поточного процессора Intel Xeon.

Подобная конфигурация потребляет около киловатта электроэнергии и требует хорошего охлаждения (из-за чего сервер разумно разместить в отдельном помещении или на кухне), но обеспечивает скорость от 10 до 50 токенов в секунду в зависимости от размера выбранной языковой модели. Хотя облачные решения уровня DeepSeek могут работать быстрее, свой локальный сервер гарантирует абсолютную приватность данных и доступность 24/7 без подписок и лимитов.

Источник: habr.com

Реклама
01.