Catframes.py

Скрипт, который соединяет (англ. concatenate) кадры (англ. frames).

Сокращение cat в этом контексте знакомо любому пользователю Unix-подобных систем.

Примеры использования

В первом примере вы можете убедиться в нескольких вещах: стандартные настройки качества подойдут практически для всего, кадры с отличающимся соотношением сторон выравниваются по центру и цвет полей у этих кадров совпадает с цветом фона.

Анимация была отрендерена Блендером в серию PNG-изображений с прозрачным фоном, после чего первые несколько кадров обрезаны сверху и снизу, чтобы продемонстрировать поля.

catframes.py --margin-color='#a143ac' monkey/ monkey.webm
catframes.py --margin-color='#a143ac' monkey/ monkey.mp4

catframes.py --margin-color='#a143ac' -q high monkey/ monkey_hq.webm
catframes.py --margin-color='#a143ac' -q high monkey/ monkey_hq.mp4

catframes.py --margin-color='#a143ac' -q poor monkey/ monkey_pq.webm
catframes.py --margin-color='#a143ac' -q poor monkey/ monkey_pq.mp4
http://itustinov.ru/cona/monkey_preview.jpg

monkey_hq.webm (76M), monkey.webm (21M), monkey_pq.webm (9.2M)

monkey_hq.mp4 (61M), monkey.mp4 (22M), monkey_pq.mp4 (12M)

Стоп-кадры для сравнения качества:

monkey_hq.webm.png, monkey.webm.png, monkey_pq.webm.png

monkey_hq.mp4.png, monkey.mp4.png, monkey_pq.mp4.png

monkey_orig.png

Во втором примере склеивается последовательность каталогов в заданном порядке, причем у содержимого этих каталогов отличаются разрешения, что отражено в названиях. Вы можете наблюдать, что положение надписей не зависит ни от разрешений, ни от соотношений сторон; надписи легко читаются как на светлом, так и на тёмном фоне; скрипт сам выбирает разрешение (в данном случае 1280x960).

catframes.py -r 60 \
  --bottom='{catframes}\n' \
  06_1280_720 \
  07_1280_640 \
  08_800_600 \
  09_1024_768 \
  10_640_480 \
  11_720_1280 \
  12_1440_1080 \
  13_1440_1080 \
  14_1280_960 \
  15_1280_960 \
  16_1280_960 \
  17_800_600 \
  18_800_600 \
  19_800_600 \
  mixed_resolutions.webm
http://itustinov.ru/cona/mixed_resolutions_preview.png

mixed_resolutions.webm (113M), mixed_resolutions.mp4 (383M)

Перейдём ближе к реальной практике. Для примера я выставил из окна веб-камеру и направил её в небо, чтобы не вторгаться ни в чью частную жизнь. На компьютер круглосуточно сохранялись снимки. В реальной системе папки с ними пережимались бы по расписанию примерно следующей командой.

catframes.py \
  --left-top='{dir}/{fn} (кадр {frame:video})\n\nПоследнее изменение: {mtime:%d.%m.%Y %H:%M:%S}\nРазмер файла, байты: {size}' \
  --top='' --right-top='WARN' \
  --right='{symlink:L}' \
  --bottom-left='Архивировано {vtime:%d.%m.%Y} в {vtime:%H:%M} с помощью {catframes}.' \
  /путь/к/кадрам/2022-08-27T00/ \
  /путь/к/кадрам/2022-08-27T01/ \
  /путь/к/кадрам/2022-08-27T02/ \
  /путь/к/кадрам/2022-08-27T03/ \
  /путь/к/кадрам/2022-08-27T04/ \
  /путь/к/кадрам/2022-08-27T05/ \
  /путь/к/кадрам/2022-08-27T06/ \
  /путь/к/кадрам/2022-08-27T07/ \
  /путь/к/кадрам/2022-08-27T08/ \
  /путь/к/кадрам/2022-08-27T09/ \
  /путь/к/кадрам/2022-08-27T10/ \
  /путь/к/кадрам/2022-08-27T11/ \
  /путь/к/кадрам/2022-08-27T12/ \
  /путь/к/кадрам/2022-08-27T13/ \
  /путь/к/кадрам/2022-08-27T14/ \
  /путь/к/кадрам/2022-08-27T15/ \
  /путь/к/кадрам/2022-08-27T16/ \
  /путь/к/кадрам/2022-08-27T17/ \
  /путь/к/кадрам/2022-08-27T18/ \
  /путь/к/кадрам/2022-08-27T19/ \
  /путь/к/кадрам/2022-08-27T20/ \
  /путь/к/кадрам/2022-08-27T21/ \
  /путь/к/кадрам/2022-08-27T22/ \
  /путь/к/кадрам/2022-08-27T23/ \
  /путь/к/архивам/2022-08-27.webm

Предупреждение

Скрипт завершается с ненулевым статусом, если какие-то папки с кадрами не существуют.

Я не настраивал у себя архивацию по расписанию, а выбрал интересный фрагмент и сжал его отдельно.

catframes.py \
  --left-top='{dir}/{fn} (кадр {frame:video})\n\nПоследнее изменение: {mtime:%d.%m.%Y %H:%M:%S}\nРазмер файла, байты: {size}' \
  --top='' --right-top='WARN' \
  --right='{symlink:L}' \
  --bottom-left='Архивировано {vtime:%d.%m.%Y} в {vtime:%H:%M} с помощью {catframes}.' \
  2022-08-26T23/ \
  2022-08-27T00/ \
  2022-08-27T01/ \
  2022-08-27T02/ \
  2022-08-27.webm
http://itustinov.ru/cona/2022-08-27.webm_preview.png

2022-08-27.webm (13M)

Любые данные, оказывающиеся на периферии нашего внимания, чтобы быть по́нятыми, требуют встраивания в некоторый контекст, от которого зависит, как мы видим и формулируем факты. Видео — не исключение. Если описать это видео вне контекста, получится что-то вроде «на записи перемещается едва заметная точка».

Обратимся к напечатанной на кадрах метаинформации. В ней не сказано, где находится камера, но я сам недавно её ставил и помню, что она находилась в Казани и была направлена на юг и вверх примерно под 45 градусов. На кадрах зафиксировано время съёмки (имена файлов). Через эти отметки времени и горизонтальный угол обзора (около 45 градусов, я измерял) можно оценить угловую скорость, а с помощью перемотки и линейки, приложенной к экрану, — траекторию.

Нетрудно убедиться, что точка движется почти по идеальной прямой, и время прохода перед камерой равнялось примерно трём часам. Это исключает практически любые атмосферные объекты, включая распространённые виды летательных аппаратов, и всё, что находится на низких орбитах. Объект двигался с востока на запад, и с учётом скорости прохождения поля зрения, вероятно, он находился в дальнем космосе, а его движение на видео — иллюзия, вызванная вращением Земли.

Воспользуемся виртуальным планетарием, например https://stellarium-web.org/, укажем в нём время и место и убедимся, что как раз тогда на юге проходил Юпитер, один из самых ярких астрономических объектов.

Совет

Самая важная метаинформация у видео — время и место съёмки.

Пример также демонстрирует фиксацию подозрительных действий на сервере. Я нарочно

  1. отредактировал три кадра, начиная с 2022-08-27-00-00-01.jpg (см. время модификации),

  2. следующий отредактировал прямо во время архивирования (об этом сигнализирует предупреждение),

  3. один кадр заменил симлинком (буква L справа),

  4. один кадр во время архивирования удалил (красный экран),

  5. один кадр во время архивирования запретил на чтение (красный экран),

  6. один кадр запретил на чтение еще до запуска скрипта (красный экран).

Обратите внимание, сколько пришлось привнести сторонней информации для построения непротиворечивой интерпретационной модели, даже несмотря на эти индикаторы. Я апеллировал в основном к личному опыту, что не гарантирует достоверность и не всегда возможно. Вопрос критериев отбора такой информации остаётся открытым.

Пользовательский интерфейс

class catframes.OverLang

Модуль разбора шаблонов оверлеев.

classmethod compile(template: str) OverlayTemplate

Оверлей может быть описан одним из двух способов.

Первый способ — написать только WARN и ничего больше (любым регистром). В этом случае, вся эта область будет показывать предупреждение о кадре, если оно есть.

Второй способ — использовать произвольный текст с вычисляемыми выражениями в фигурных скобках вида {[[выравнивание]ширина[!]:]функция[:аргумент]}.

Ширина — целое число больше нуля. Подразумевается минимальная ширина, но если добавить восклицательный знак, лишние символы будут отрезаны. Выравнивание может быть по левому краю (по-умолчанию), по правому краю (>) и приблизительно по центру (^).

Перечень доступных функций:

Функция

Описание

{catframes}

Название и версия скрипта.

{machine}

Значение platform.machine(). Обычно это архитектура процессора. Может быть пустой строкой, если не удаётся определить.

{node}

Значение platform.node(). Сетевое имя компьютера. Может быть пустой строкой, если не удаётся определить.

{vtime[:формат]}

Приблизительно соответствует времени запуска скрипта.

Синтаксис формата соответствует используемому в методе datetime.datetime.strftime(). Если не указывать, используется ISO 8601 с миллисекундами (обычно 23 символа).

{fn}

Имя кадра целиком (не обязательно всегда указывать, поскольку предупреждения и ошибки обычно содержат имя файла).

{dir}

Название директории, где непосредственно находится текущий кадр.

{frame:контекст}

Номер кадра. Контекст определяет точку отсчёта.

Возможные значения:

  • dir — текущая директория;

  • dirs — все директории;

  • video — видеозапись: нумерация начинается с единицы даже при использовании опции --trim-start.

{mtime[:формат]}

Местное время последнего изменения кадра на диске. Пустая строка, если не получается определить.

Синтаксис формата соответствует используемому в методе datetime.datetime.strftime(). Если не указывать, используется ISO 8601 с миллисекундами.

{size}

Размер кадра на диске в байтах.

{resolution}

Исходное разрешение кадра.

{symlink[:вид]}

Отметка о том, что кадр является симлинком или пустая строка. Вид по-умолчанию — symlink.

static is_warning(template: str)

Означает ли этот шаблон место, зарезервированное для предупреждений.

class catframes.ConsoleInterface

Интерфейс пользователя.

Превращает аргументы командной строки в настройки и наборы данных. И тут также выводится информация в стандартный вывод по ходу анализа параметров скрипта.

show_options()

Чтобы пользователь видел, как проинтерпретированы его аргументы.

show_splitter()

Визуально отделить всё, что было выведено в консоль выше.

get_input_sequence() Sequence[Frame]

Возвращает отсортированную и пронумерованную последовательность кадров, которую попросил пользователь: параметры --trim-start и --trim-end уже применены.

Исключение:

ValueError – не удалось прочитать список файлов или в указанных директориях нет ни одного изображения.

get_output_options() OutputOptions

Возвращает настройки сохранения видео.

Исключение:

ValueError – пользователь указал файл с недопустимым расширением и т.п.

property statistics_only: bool

Пользователь не хочет пока делать видео, только посмотреть логику выбора разрешения.

property margin_color: str

В случае полупрозрачных кадров, это будет также цвет фона.

property layout: Layout

План наложения оверлеев.

static list_resolutions(resolutions: ResolutionStatistics, limit: int = 10)

Перечислить разрешения от самых частых к самым редким.

Сетевые возможности

Catframes использует протокол HTTP, чтобы передавать кадры в FFmpeg.

catframes.WebApp

Первый аргумент — WSGI environment (включает всю информацию о запросе), второй — функция начала ответа, которая принимает статус HTTP-ответа и список HTTP-заголовков. В терминах MVC, это еще неразграниченные роутинг с контроллерами.

Подробнее читайте в официальной документации: wsgiref.simple_server.make_server().

alias of Callable[[dict, Callable], Iterable[bytes]]

catframes.WebJob

Функция, которая работает с локальным веб-приложением. Аргумент — номер порта.

alias of Callable[[int], None]

class catframes.JobServer(app: WebApp, job: WebJob)

Механизм межпроцессного взаимодействия на основе HTTP. Открывает порт и обслуживает другие программы, которые контролируются из этого же сервера.

Грубо говоря, эта штука позволяет делать что-то вроде динамической файловой системы, если считать URL файлами: в тот момент, когда что-то запрашивается сторонней программой, оно подготавливается на лету. Не тратится дисковое пространство, а если не использовать временные файлы при обработке запросов, то и ресурс SSD.

В целях безопасности:

  1. на компьютере следует использовать файрвол;

  2. лучше не передавать списки URL по HTTP-каналу.

Номер порта выбирается автоматически. Значения ниже десяти тысяч не используются, чтобы не мешать всяким Томкэтам и Ноджиэсам.

Параметры:
  • app – Функция, которая обслуживает HTTP-запросы.

  • job – Запускается один раз. Сервер ждёт завершения. Выброшенные исключения приводят к остановке сервера и вылетают из метода run().

run()

Запускает сервер, ждёт завершения работ, останавливает сервер.

Исключение:

Exception – если исключение вылетело из джобы.

Файловая система

Архивирование видео может запускаться автоматически и выполняться без присмотра. При этом нежелательно, чтобы сжатие данных за целый день аварийно остановилось, если что-то не получится сделать с одним кадром. Поэтому здесь реализованы обёртки над системными функциями для максимальной предсказуемости.

class catframes.FileUtils

Модуль вспомогательных функций, связанных с файловой системой.

static get_checksum(path: Path) Optional[str]

Функция не выбрасывает исключения.

static get_mtime(path: Path) Optional[datetime]

Функция не выбрасывает исключения.

static get_file_size(path: Path) Optional[int]

Функция не выбрасывает исключения.

Функция не выбрасывает исключения.

static list_images(path: Path) List[Path]

Набор файлов JPEG и PNG в порядке, предоставляемом pathlib (не определён в документации и, скорее всего, зависит от операционной системы). Файлы определяются по расширениям (суффиксам имён). Вложенные папки игнорируются.

Исключение:
  • ValueError – путь не является директорией.

  • OSError – не удалось получить список файлов.

static sort_natural(files: List[Path])

В Linux также известен как version sort. Многосимвольные десятичные числа считаются за один символ и сортируются в зависимости от значения числа.

Функция сортирует файлы или симлинки по именам, игнорируя их местоположения. Список сортируется на месте, чтобы экономить память.

Способ сортировки имён похож на используемый в команде sort из GNU Coreutils, если использовать её как echo СПИСОК | sort -Vs, но не совпадает полностью.

Основные сущности

class catframes.Resolution(width: int, height: int)

Ненулевое разрешение в пикселях.

__str__()

Возвращает строку вида ШxВ. Икс в качестве разделителя выбран из соображений совместимости с максимальным числом шрифтов оверлеев и кодировок терминалов.

property ratio: float

Соотношение сторон. Всегда больше нуля.

__eq__(other)

Return self==value.

class catframes.Frame(path: Path)

Кадр на диске. Сырьё для FrameView. Конструктор создаёт объект, даже если путь ведёт в никуда. Не иммутабельная сущность, некоторые поля могут обновляться.

numdir: int

Каким он будет по счету в своей папке, если её содержимое отсортировать.

numdirs: int

Каким он будет по счету, если отсортировать изображения в папках и склеить списки.

numvideo: int

Как numdirs, но если пользователь просит отрезать сколько-то кадров из начала последовательности, нумерация всё равно начинается с единицы, т.к. это первый кадр в видео.

property path: Path

Путь к файлу в директории, указанной пользователем. Может быть симлинком.

property name: str

Имя файла или симлинка.

property folder: str

Имя папки с файлом (для демонстрации пользователю).

property checksum: str | None

Незаполнено, если не удалось прочитать файл в момент создания объекта.

property resolution: catframes.Resolution | None

Незаполнено, если не удалось прочитать файл в момент создания объекта.

class catframes.OverlayModel(warning: str, filename: str, foldername: str, symlink: bool, mtime: datetime.datetime | None, size: int | None, resolution: catframes.Resolution | None, numdir: int, numdirs: int, numvideo: int, vtime: datetime, machine: str, node: str)

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

warning: str

Как правило это пустая строка. Задача этого поля — сообщать пользователю об очень-очень странных вещах вроде подмены кадра на диске прямо перед встраиванием в видео. Если сюда что-то записано, значит это серьёзно, но это не значит, что нужно останавливать сжатие. Напротив, об инциденте нужно рассказать всем.

Не заполняется, когда картинку не удалось открыть: вместо картинки будет показана одноцветная заглушка с названием ошибки по центру. Оверлеи будут нанесены на эту заглушку как обычно.

filename: str

Имя файла целиком, без пути.

foldername: str

Только имя папки, где кадр находится непосредственно.

Является ли файл симлинком в данный момент.

mtime: datetime.datetime | None

Местное время последнего изменения файла на текущий момент.

Не заполняется, если файла не оказалось на диске.

size: int | None

Размер файла в байтах на текущий момент.

Не заполняется, если файла не оказалось на диске.

resolution: catframes.Resolution | None

Исходное разрешение кадра на текущий момент.

Не заполняется, если файла нет или его не удалось открыть.

numdir: int

Номер кадра в текущей директории, начиная с единицы. Если мы пропускаем N кадров опцией --trim-start=N, и N меньше числа кадров в первой директории, у первого кадра видео этот номер будет равен N+1.

numdirs: int

Номер кадра во всех директориях (сплошная нумерация). Если мы пропускаем N кадров опцией --trim-start=N, первый кадр видео всегда будет под номером N+1.

numvideo: int

Сплошная нумерация, но всегда начинается с единицы.

vtime: datetime

Приблизительное местное время создания видео.

machine: str

Типа машины, накотором создаётся видео. Пустая строка, если не удаётся определить.

node: str

Сетевое имя компьютера, где создаётся видео. Пустая строка, если не удаётся определить.

catframes.OverlayTemplate

Скомпилированный в функцию шаблон, по которому формируется текст оверлея.

alias of Callable[[OverlayModel], str]

class catframes.Layout

Кадр как таблица три-на-три. Назначаемые пользователем оверлеи располагаются в боковых ячейках, выровненные в сторону края. Отсчёт индексов ячеек ведётся от левого верхнего угла. Центр занять нельзя по очевидным причинам.

put(xpos: int, ypos: int, overlay: OverlayTemplate)

Установить в ячейку шаблон оверлея.

Исключение:

ValueError – ячейка уже занята.

get(xpos: int, ypos: int) Optional[OverlayTemplate]

Получить шаблон.

Работа с размерами

class catframes.ResolutionUtils

Модуль вспомогательных функций, связанных с разрешениями.

static round(value: float) int

Округляет вычисленный размер стороны. Поддерживаемые форматы видео могут иметь ограничения, поэтому это округление не обязательно идёт до ближайшего целого. Имеет смысл использовать как финальный этап выбора разрешения видео.

static get_scale_size(src: Resolution, goal: Resolution) Optional[Resolution]

Пропорционально меняет разрешение, чтобы исходник вписался в целевое разрешение без зазора. None означает либо рекомендацию кадрировать картинку вместо масштабирования, либо что она уже идеально вписывается.

static get_crop_size(src: Resolution, goal: Resolution) Resolution

Следует применять только если get_scale_size() вернул None. Если соотношение сторон отличается, размер будет меньше целевого по одной стороне.

class catframes.ResolutionStatistics(frames: Sequence[Frame])

Знает, какие разрешения как часто используются.

sort_by_count_desc() Sequence[Tuple[Resolution, int]]

Возвращает разрешения в порядке убывания числа кадров.

choose() Resolution

Решает, какое разрешение лучше использовать для видео.

Совмещает самую частую ширину и самую частую высоту, которые не меньше, чем средние этих показателей. Если есть несколько значений ширины или высоты с одинаковой частотой, выбираются максимальные числа.

Если соотношение сторон непостоянно, этот метод приведёт к добавлению полей, возможно, на всех кадрах, но почти все кадры впишутся в итоговое разрешение без снижения качества.

Пример:

Ширина

Высота

Количество кадров

1280

720

3000

800

800

2000

Поскольку

  • средневзвешенная ширина равна 1088,

  • средневзвешенная высота — 752,

  • 800 меньше 1088,

  • 720 меньше 752,

выбрано может быть только разрешение 1280×800.

У кадров 800×800 появятся поля по бокам, у кадров 1280×720 — сверху и снизу.

Рендеринг

class catframes.FrameResponse(data: bytes, content_type: str)

Ответ сервера на запрос кадра. Предполагается, что раз ответ есть, это HTTP OK.

class catframes.FrameView(resolution: Resolution)

Абстрактное представление кадра. Отвечает как за подгонку разрешения (обязательно), а также за любую другую обработку: добавление надписей, подстраивание яркости и контрастности и т.п.

abstract apply(frame: Frame) FrameResponse

Получить оформленный кадр как набор байт. Выбрасывание исключений здесь приведёт к пятисотой ошибке в HTTP-ответе.

class catframes.PillowFrameView(resolution: Resolution)

Базовые классы: FrameView

Каркас для гарантированно однопоточного рендеринга библиотекой Pillow. Синхронизация позволяет использовать один и тот же холст многократно, не нагружая кучу и сборщик мусора.

_image: Image

Холст для заполнения методом render.

_draw: ImageDraw

2D-контекст для рисования на холсте.

apply(frame: Frame) FrameResponse

Создаёт картинку прямо в ОЗУ.

abstract _render(frame: Frame)

Рисует на холсте исходный кадр и что угодно поверх него. Выбрасывание исключений приведёт к пятисотой ошибке. FFmpeg при получении такого статуса прекращает работу, так что лучше делать метод устойчивым к любым проблемам.

_clear(color)

Тип аргумента допустим любой из тех, что понимает Pillow.

_paste(source: Image)

Вписывает отмасштабированную картинку по центру поверх текущего содержимого.

_find_font(size: int) FreeTypeFont

Ищет в системе что-нибудь из популярных юникодных моноширинных TrueType-шрифтов.

В документации Pillow сказано, что файлы TrueType-шрифтов могут оставаться открытыми всё время существования объекта шрифта. Вроде бы, это не страшно, ведь файл открывается на чтение, но в Windows есть лимит одновременно открытых программой файлов, поэтому, думаю, лучше использовать один объект для обработки всех кадров.

Результат:

первый попавшийся шрифт.

Исключение:

ValueError – ни один из ожидаемых шрифтов не найден.

class catframes.DefaultFrameView(resolution: Resolution, margin_color: str, layout: Layout)

Базовые классы: PillowFrameView

Масштабирует, добавляет поля при необходимости, накладывает текстовые индикаторы, а если файл внезапно стал недоступен, создаёт красный кадр-заглушку с названием ошибки по центру.

Исключение:

ValueError – ошибки в параметрах или в системе не найден шрифт.

Сохранение видео

class catframes.Quality(value)

Абстракция над бесконечными настройками качества FFmpeg.

HIGH = (8, 4)

Очень высокое, но всё же с потерями. Подходит для художественных таймлапсов, где важно сохранить текстуру, световые переливы, зернистость камеры. Битрейт — как у JPEG 75.

MEDIUM = (16, 18)

Подойдёт почти для любых задач. Зернистость видео пропадает, градиенты становятся чуть грубее, картинка может быть чуть мутнее, но детали легко узнаваемы.

POOR = (22, 31)

Некоторые мелкие детали становятся неразличимыми.

get_h264_crf(fps: int) int

Constant Rate Factor меняет битрейт для поддержания постоянного уровня качества. Метрика качества в кодеке связана с движением — медленные объекты считаются более заметными.

При повышении частоты кадров, детали в каждом отдельном кадре становятся всё менее различимыми для зрителей, поэтому кодек при том же CRF порождает меньшие файлы.

Всё это логично для фильмов, но плохо для видеонаблюдения, где важен каждый кадр. Данный метод корректирует CRF обратно по частоте смены кадров.

get_vp9_crf() int

Мои тесты показали, что опция CRF в VP9 не связана с частотой кадров.

class catframes.OutputOptions(frame_rate: int, quality: Quality, destination: Path, overwrite: bool, limit_seconds: int | None)

Это опции сохранения видеозаписи. Грубо говоря, опции FFmpeg. Они не влияют ни на выбор разрешения, ни на обработку кадров. Ограничение длины не влияет на выбор разрешения, т.к. цель опции — заранее посмотреть, как будет выглядеть результат, поэтому и влияние на результат должно быть минимальным.

static get_supported_suffixes() Sequence[str]

Возвращает поддерживаемые расширения файлов.

make(frames: Sequence[Frame], view: FrameView)

Обрабатывает с помощью FrameView и соединяет кадры последовательности в видеозапись. Сортировка последовательности, как и валидация всех опций, должны быть сделаны заранее.

Всё происходит в ОЗУ. Диск используется только для

  1. чтения кадров,

  2. хранения списка кадров в текстовом временном файле,

  3. сохранения видео.

Скрипт ответственнен примерно за одну пятую суммарного с FFmpeg потребления памяти.