Дата публикации
ai_products

Как Домклик делает потоковую конвертацию видео без диска: Python + FFmpeg

Что появилось / что изменилось

Команда Домклик собрала отдельный микросервис для конвертации видео — Transcode Backend. Он написан на Python и использует FFmpeg, но главное здесь не язык, а способ работы:

  • Конвертация идёт потоками (chunked streaming), без сохранения исходного или итогового файла на диск.
  • FFmpeg читает видео по HTTP Range прямо из файлового хранилища и сразу пишет результат в stdout.
  • Python-сервис читает stdout FFmpeg чанками по 8 КБ и тут же заливает результат обратно в хранилище, тоже частями.
  • На пике нагрузки в системе не возникает двух полных копий одного и того же видео.
  • Не нужны временные файлы и логика их очистки после сбоев.

Для настройки качества автор собрал пять пресетов под разные сценарии: от «high_quality» (1920×1080, 4000 kbit/s, CRF 18, preset slow) до «mobile_low_size» (854×480, 800 kbit/s, CRF 30, preset slow). Все они используют кодек H.264 (libx264) и управляют качеством через CRF:

  • CRF 18 — визуально близко к исходнику.
  • CRF 23 — стандартное качество.
  • CRF 28+ — заметно более жёсткое сжатие.

Как это работает

В основе — три ключевых компонента.

1. FS Proxy как файловое хранилище

FS Proxy — это HTTP-сервис, который хранит файлы и поддерживает запросы с заголовком Range. Пример запроса:

GET /srv/v1/files/{urn}/download
Range: bytes=0-1048575

HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1048575/32925286

За счёт этого Transcode Backend может:

  • скачивать исходное видео чанками;
  • заливать сконвертированный результат обратно тоже по частям.

2. VideoConverter — обёртка над FFmpeg

Класс VideoConverter запускает FFmpeg как подпроцесс и отдаёт результат как AsyncIterable[bytes]:

async def __aenter__(self):
    cmd = preset.render_cmd(
        headers=input.headers,
        input_url=input.url,
        ffmpeg_path=self._ffmpeg_path,
    )
    self._proc = await asyncio.create_subprocess_exec(
        *cmd,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
        stdin=asyncio.subprocess.DEVNULL,
    )
    return self

async def convert(self) -> AsyncIterable[bytes]:
    while True:
        chunk = await self._proc.stdout.read(8192)
        if not chunk:
            break
        yield chunk

FFmpeg пишет выходной поток в pipe:1, то есть в stdout. Python читает его кусками по 8192 байта и может сразу отправлять дальше — в сеть или в другое хранилище. Если потребитель читает медленно, запись в pipe блокирует FFmpeg, и он замедляется сам по себе — получается естественный backpressure.

3. Пресеты и генерация команд FFmpeg

Каждый пресет сам собирает команду для FFmpeg. В неё входят:

  • заголовки HTTP для доступа к FS Proxy (-headers),
  • URL исходного видео (-i),
  • фильтр масштабирования -vf с логикой: если ширина больше высоты — ограничиваем по ширине, иначе по высоте, вторую сторону считаем автоматически,
  • кодек libx264, параметры -preset, -crf, -b:v, -profile:v, -level.

Пример фрагмента:

cmd = [
    ffmpeg_path,
    '-timeout', str(self.timeout),
    '-threads', str(self.threads),
    '-headers', self._build_headers_string(headers),
    '-i', str(input_url),
    '-vf', "scale='if(gt(iw,ih),min(1920,iw),-2)':'if(gt(iw,ih),-2,min(1080,ih))'",
    '-c:v', 'libx264',
    '-preset', 'fast',
    '-crf', '23',
    '-b:v', '2500k',
]

Что это значит для вас

Где такой подход полезен:

  • Массовая перекодировка пользовательских видео, когда нельзя раздувать дисковое хранилище временными файлами.
  • Обработка длинных роликов, где целиком держать файл ни в памяти, ни на диске неудобно или дорого.
  • Сценарии, где важно не дублировать исходник и результат одновременно (например, при жёстких лимитах на объём хранилища).

Когда это решение особенно уместно:

  • У вас уже есть HTTP-хранилище с поддержкой Range-запросов или вы готовы его внедрить.
  • Вы хотите централизованно управлять качеством через набор пресетов (разные разрешения и битрейты под веб, мобильные клиенты, «лёгкие» превью).
  • Важна простая интеграция: микросервис слушает очередь, принимает задачу, конвертирует и возвращает URN готового файла.

Когда лучше подумать о другом варианте:

  • Нужен классический live-стриминг с HLS/DASH и плейлистами — здесь описан именно chunked streaming для файлов, а не живой эфир.
  • Ваша инфраструктура не позволяет работать по HTTP Range или вы завязаны на локальные файлы без сетевого доступа.

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

Место на рынке

Transcode Backend использует классический стек: Python + FFmpeg + HTTP-хранилище. Это не конкурирует с SaaS-платформами для видео вроде готовых CDN-сервисов — это внутренняя инфраструктурная деталь.

По сути, Домклик показывает рабочий паттерн:

  • FFmpeg как движок кодирования,
  • микросервис как оркестратор и транспорт,
  • файловое хранилище с HTTP Range как источник и приёмник.

Если вы уже используете FFmpeg в виде CLI-скриптов и временных файлов, этот подход можно рассматривать как эволюцию: тот же движок, но без промежуточного диска и с потоковой обработкой через асинхронный Python.


Читайте также

Как Домклик делает потоковую конвертацию видео без диска: Python + FFmpeg — VogueTech | VogueTech