ffmpeg из Python: рабочие приёмы без утечек и зависаний
Как правильно вызывать ffmpeg из Python-кода: почему subprocess зависает на больших файлах, как читать прогресс и не оставлять зомби-процессы при обрыве.

Типичная задача: пользователь загрузил видео, надо перекодировать его в H.264, вырезать фрагмент, наложить водяной знак и отдать результат. Библиотек-обёрток над ffmpeg много, но в проде почти всегда всё сводится к одному: вы формируете список аргументов и запускаете внешний процесс через subprocess. И вот тут начинаются грабли — процесс зависает на длинном ролике, прогресс не читается, а при обрыве соединения на сервере копятся мёртвые ffmpeg. Разберём приёмы, которые снимают эти проблемы.
Статья про то, как вызывать ffmpeg из Python-скрипта или веб-бэкенда. Версии: Python 3.10+, ffmpeg 6.x (проверьте ffmpeg -version). Работу с фильтрами ffmpeg детально не разбираем — фокус на управлении процессом из кода.
Почему прямой subprocess.run зависает
Самая частая ошибка — запустить ffmpeg и одновременно перехватить и stdout, и stderr в PIPE, не читая их:
import subprocess
# ТАК ДЕЛАТЬ НЕЛЬЗЯ на больших файлах
result = subprocess.run(
["ffmpeg", "-i", "input.mp4", "-c:v", "libx264", "output.mp4"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
На коротком файле это отработает. На часовом видео процесс встанет намертво. Причина в буфере пайпа операционной системы: ffmpeg пишет прогресс в stderr, буфер (обычно 64 КБ на Linux) заполняется, ffmpeg блокируется на записи и ждёт, пока кто-то прочитает. А ваш run() ждёт завершения процесса, чтобы прочитать. Классический дедлок.
Если стоит выбор «перехватить вывод» или «не зависнуть» — либо читайте пайпы в реальном времени, либо не открывайте их как PIPE вовсе.
Самое простое лекарство, когда вывод ffmpeg вам не нужен, — перенаправить его в DEVNULL:
import subprocess
subprocess.run(
["ffmpeg", "-y", "-i", "input.mp4",
"-c:v", "libx264", "-preset", "fast", "output.mp4"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True, # бросит CalledProcessError при ненулевом коде возврата
)
Флаг -y заставляет ffmpeg перезаписывать выходной файл без вопроса — иначе на существующем output процесс встанет в интерактивный запрос и будет ждать нажатия y, которого от бэкенда никогда не дождётся.
Как читать прогресс, а не гадать
Прогресс из stderr парсить можно, но формат там человекочитаемый и меняется. Надёжнее использовать штатный ключ -progress, который пишет машиночитаемые пары ключ=значение. Направим его в stdout, а stderr отправим в DEVNULL:
import subprocess
def encode_with_progress(input_path, output_path, total_seconds):
cmd = [
"ffmpeg", "-y",
"-i", input_path,
"-c:v", "libx264", "-preset", "fast",
"-progress", "pipe:1", # прогресс в stdout
"-nostats",
output_path,
]
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
# ffmpeg отдаёт время как out_time_ms (микросекунды, несмотря на имя)
for line in process.stdout:
line = line.strip()
if line.startswith("out_time_ms="):
value = line.split("=", 1)[1]
if value.isdigit():
done = int(value) / 1_000_000 # в секунды
percent = min(100, done / total_seconds * 100)
print(f"\rГотово: {percent:5.1f}%", end="")
code = process.wait()
print()
if code != 0:
raise RuntimeError(f"ffmpeg вышел с кодом {code}")
Тонкость: имя ключа out_time_ms исторически неточное, значение приходит в микросекундах. Проверьте на своей версии, поделив на длительность файла — если проценты уходят в тысячи, делитель не тот. Общую длительность total_seconds берём заранее через ffprobe.
Длительность через ffprobe
ffprobe идёт в комплекте с ffmpeg и умеет отдавать JSON — это надёжнее, чем регуляркой выдирать Duration из вывода:
import subprocess, json
def get_duration(path):
out = subprocess.run(
["ffprobe", "-v", "error",
"-show_entries", "format=duration",
"-of", "json", path],
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
text=True, check=True,
)
data = json.loads(out.stdout)
return float(data["format"]["duration"])
У некоторых потоковых форматов поля duration в секции format может не быть — тогда придётся смотреть длительность конкретного видеопотока через -show_entries stream=duration -select_streams v:0. Обработайте KeyError, а не считайте, что поле всегда есть.
Обрыв, таймаут и зомби-процессы
На веб-бэкенде клиент может отвалиться посреди перекодирования. Если вы просто бросите Python-задачу, ffmpeg продолжит жечь CPU в фоне. И перекодирование иногда встаёт на битом файле — нужен таймаут.
subprocess.run(..., timeout=...) при срабатывании таймаута убивает процесс, но у ffmpeg с дочерними процессами (например, аппаратное кодирование) бывают потомки, которые переживают родителя. Надёжнее запускать процесс в отдельной группе и убивать всю группу:
import subprocess, os, signal
def run_ffmpeg(cmd, timeout=3600):
process = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True, # своя группа процессов (Linux/macOS)
)
try:
code = process.wait(timeout=timeout)
except subprocess.TimeoutExpired:
# убиваем всю группу, а не только сам ffmpeg
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
process.wait()
raise
if code != 0:
raise RuntimeError(f"ffmpeg код {code}")
Параметр start_new_session=True и вызов os.killpg работают на Linux и macOS. На Windows группы процессов устроены иначе — там используйте creationflags=subprocess.CREATE_NEW_PROCESS_GROUP и process.terminate(); проверьте поведение на своей платформе.
Вежливее слать сначала SIGTERM: ffmpeg по нему корректно закрывает выходной файл и завершается. SIGKILL оставит недописанный битый файл — но гарантированно снимет зависший процесс.
Стоит ли брать обёртку ffmpeg-python
Библиотека ffmpeg-python строит команду через цепочку методов и сама вызывает subprocess. Она удобна для сборки сложных графов фильтров, но не избавляет от описанных проблем: прогресс, таймаут и группы процессов остаются на вас. Сравним подходы.
| Критерий | Ручной subprocess | ffmpeg-python |
|---|---|---|
| Контроль над процессом | Полный | Прячет Popen, прогресс сложнее |
| Сложные фильтр-графы | Строки аргументов руками | Читаемая цепочка методов |
| Зависимости | Только сам ffmpeg | + пакет из PyPI |
| Отладка команды | Видите список аргументов как есть | Нужно вызывать .compile() |
| Когда брать | Простые операции, полный контроль | Много фильтров, наложений, микширования |
Если у вас две-три несложные операции — обрезка, перекодирование, извлечение звука — ручной subprocess проще и прозрачнее. Как только начинается наложение нескольких дорожек с фильтрами, читаемость цепочки методов окупает лишнюю зависимость.
Не собирайте команду через строку и shell
Соблазн написать subprocess.run(f"ffmpeg -i {path} out.mp4", shell=True) велик, но это дыра: если path приходит от пользователя и содержит ; rm -rf или пробелы, вы получите инъекцию команд или сломанный вызов. Всегда передавайте аргументы списком и без shell=True — тогда пробелы и спецсимволы в путях обрабатываются корректно.
Список аргументов вместо строки — не вопрос стиля, а граница безопасности. Один пользовательский путь с точкой с запятой превращает удобный shell=True в удалённое выполнение команд.
Что настроить дальше
- Вынесите тяжёлое перекодирование в очередь задач (Celery, RQ) — держать ffmpeg внутри HTTP-запроса нельзя, воркер отвалится по таймауту.
- Ограничьте нагрузку: параллельные ffmpeg легко забивают все ядра. Считайте одновременные задачи и ставьте лимит, иначе сервер ляжет под десятью загрузками разом.
- Логируйте stderr в файл, а не в DEVNULL, когда ловите нестабильные падения — без него причину сбоя не восстановить.
- Для аппаратного кодирования (NVENC, QSV) сверьтесь с документацией ffmpeg по доступности энкодеров на вашей сборке:
ffmpeg -encodersпокажет, что реально скомпилировано.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: FFmpeg Documentation — main options, Python subprocess — Subprocess management
Частые вопросы
У меня другая версия ffmpeg, ключ -progress работает иначе?
Прогресс не читается, цикл по process.stdout зависает — что делать?
На Windows os.killpg падает с ошибкой, как убивать процесс там?
Почему ffmpeg зависает и ничего не пишет, хотя файл нормальный?
Можно ли обрабатывать видео прямо в обработчике HTTP-запроса?
Как понять, сколько ядер съест ffmpeg и не положить сервер?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.