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

Вы написали subprocess.run(f"ffmpeg -i {path} out.mp4"), оно работало на тестовом ролике, а в проде зависло намертво на часовом видео. Или упало, но вы не поняли почему — stderr ушёл в никуда. Или пользователь передал имя файла с пробелом и ; rm -rf, и вы это выполнили. Ниже — как вызывать ffmpeg из Python так, чтобы этого не случилось, чем платит каждый подход и где он ломается.
Речь про ffmpeg 6.x/7.x и Python 3.10+. Код на чистой стандартной библиотеке — про сторонние обёртки поговорим отдельно и объясним, почему их обычно не стоит брать. Тема транскодирования и выбора кодеков здесь не рассматривается: считаем, что нужную команду ffmpeg вы уже собрали и вопрос только в том, как её надёжно запустить из Python.
Почему нельзя просто передать строку
Самая частая ошибка новичка — собрать команду как строку и передать в subprocess с shell=True. Три проблемы сразу:
- Инъекция. Любое имя файла с спецсимволами оболочки (
;,&&,$()) выполнится как команда. Если путь приходит от пользователя — это дыра. - Экранирование. Пробелы, кавычки и кириллица в путях ломают разбор строки оболочкой, и вы получаете
No such file or directoryна файле, который существует. - Кросс-платформенность. Правила экранирования в bash и в cmd.exe разные, строка перестаёт работать при переносе.
Правильный способ — список аргументов и shell=False (значение по умолчанию). Тогда каждый аргумент передаётся процессу как есть, оболочка не участвует:
# Python 3.10+, ffmpeg 6.x/7.x
import subprocess
def перекодировать(вход: str, выход: str) -> None:
команда = [
"ffmpeg",
"-y", # перезаписывать выходной файл без вопросов
"-i", вход, # входной файл отдельным аргументом
"-c:v", "libx264",
"-c:a", "aac",
выход,
]
subprocess.run(команда, check=True)
Обратите внимание: вход и выход — отдельные элементы списка, а не куски строки. Никакого экранирования вручную не нужно, спецсимволы в именах безопасны.
Проблема номер один: зависание на больших файлах
Классическая ловушка выглядит так:
# ТАК ДЕЛАТЬ НЕЛЬЗЯ на больших файлах
process = subprocess.Popen(команда,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process.wait() # зависнет
out = process.stdout.read()
ffmpeg пишет в stderr прогресс постоянно — много. Буфер канала (pipe) в ОС ограничен, обычно 64 КБ. Когда буфер заполнен, ffmpeg блокируется на записи и ждёт, пока кто-то прочитает. А вы ждёте в wait(), пока ffmpeg завершится. Классический дедлок: оба ждут друг друга. На коротком ролике буфер не переполнится, поэтому в тестах всё зелёное.
Правило: если вы открыли пайп на stdout или stderr, вы обязаны его читать по мере поступления, а не после завершения процесса. Иначе процесс встанет, как только буфер заполнится.
Есть два надёжных выхода.
Вариант А: не собирать вывод в память
Если прогресс вам не нужен, а stderr важен только при ошибке — используйте subprocess.run, он сам вычитывает потоки в отдельных потоках чтения и не блокируется:
результат = subprocess.run(
команда,
capture_output=True, # stdout и stderr в результат
text=True,
check=False,
)
if результат.returncode != 0:
# stderr ffmpeg — здесь ищите настоящую причину
raise RuntimeError(результат.stderr[-2000:])
capture_output=True читает потоки безопасно даже на многочасовом файле — переполнения не будет.
Вариант Б: читать прогресс построчно
Когда нужен прогресс, добавьте флаг -progress pipe:1 — ffmpeg будет печатать машиночитаемые пары ключ-значение в stdout, а -nostats уберёт человекочитаемый шум из stderr:
команда = [
"ffmpeg", "-y",
"-i", вход,
"-c:v", "libx264", "-c:a", "aac",
"-progress", "pipe:1", # прогресс в stdout построчно
"-nostats",
выход,
]
process = subprocess.Popen(
команда,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
# читаем построчно ПО МЕРЕ поступления — не после завершения
for строка in process.stdout:
строка = строка.strip()
if строка.startswith("out_time_ms="):
микросекунды = строка.split("=", 1)[1]
print("обработано, мкс:", микросекунды)
process.wait()
if process.returncode != 0:
print(process.stderr.read())
Ключ out_time_ms даёт текущую позицию в микросекундах. Разделите её на полную длительность (её узнают через ffprobe) — получите процент. Здесь мы читаем stdout в цикле, поэтому дедлока по нему нет. stderr остаётся риском: при большом объёме предупреждений он может переполниться, пока мы заняты stdout. Для полной надёжности stderr вычитывают в отдельном потоке или направляют в subprocess.DEVNULL, если он не нужен.
Таймаут: чтобы не висеть вечно
Битый файл или зависший поток заставят ffmpeg крутиться бесконечно. Задавайте таймаут — но помните, что при его срабатывании процесс нужно ещё и убить, иначе останется зомби:
try:
результат = subprocess.run(
команда,
capture_output=True, text=True,
timeout=3600, # секунды
check=True,
)
except subprocess.TimeoutExpired:
# subprocess.run сам убивает процесс при таймауте в Python 3.x,
# но дочерние процессы ffmpeg могут пережить — см. раздел про группы
raise
except subprocess.CalledProcessError as e:
raise RuntimeError(e.stderr[-2000:])
Сравнение подходов
| Подход | Прогресс | Риск дедлока | Когда брать |
|---|---|---|---|
run, вывод в файл/DEVNULL | нет | нет | фоновая пакетная обработка, вывод не нужен |
run(capture_output=True) | нет | нет | нужен stderr для диагностики ошибки |
Popen + чтение stdout построчно | да | есть по stderr, если его не читать | показ прогресса пользователю |
| обёртка вроде ffmpeg-python | частично | наследует те же проблемы | когда команда собирается динамически из графа фильтров |
Про обёртки: нужны ли они
Библиотеки-обёртки строят список аргументов за вас декларативно. Полезно, когда команда сложная и собирается программно. Но обёртка не отменяет физику пайпов: под капотом это тот же subprocess, и дедлок на невычитанном stderr там ровно тот же. Плюс появляется зависимость, которая может отставать от версии ffmpeg и не знать про новые флаги. Для 90% задач хватает списка аргументов и subprocess из стандартной библиотеки — меньше магии, понятнее ошибки. Точное имя и статус конкретной обёртки сверяйте в её репозитории перед выбором: API у них разный.
Где всё ломается в проде
- ffmpeg не в PATH. В контейнере или под другим пользователем
ffmpegможет не находиться. Проверяйте наличие черезshutil.which("ffmpeg")на старте или указывайте абсолютный путь. - Зомби-процессы при таймауте. ffmpeg может порождать дочерние процессы. Чтобы убить всё дерево на Linux, запускайте с
start_new_session=Trueи убивайте всю группу черезos.killpg. Иначе после таймаута дочерний процесс продолжит жечь CPU. - Кодировка вывода. С
text=TruePython декодирует stderr в локали системы. В контейнере сC-локалью кириллица в путях в тексте ошибки может прийти мусором — для разбора самого вывода это обычно не критично, но логи выглядят пугающе. - Параллельный запуск. Каждый ffmpeg по умолчанию занимает все ядра. Запустив десять процессов разом, вы получите не ускорение, а деградацию из-за борьбы за CPU. Ограничивайте параллельность и/или задавайте
-threads. - Диск и временные файлы.
-yперезаписывает молча. При падении на середине остаётся битый выходной файл — пишите во временное имя и переименовывайте после успешного завершения.
Что делать дальше
Соберите обёртку-функцию с тремя обязательными вещами: список аргументов вместо строки, обязательное чтение stderr при ненулевом коде возврата, таймаут с убийством группы процессов. Длительность входного файла для расчёта процента берите через ffprobe с флагом -print_format json — это отдельный вызов, разбирать его JSON проще, чем парсить stderr. В проде повесьте мониторинг на два показателя: доля процессов, упавших по таймауту, и средняя загрузка CPU при пакетной обработке — оба сразу покажут, что параллельность выкручена не туда.
Разбор научной статьи: Сложный текст — понятным языком, с терминами
Спросить за 12 ₽Источники: FFmpeg Documentation — main options, Python docs — subprocess
Частые вопросы
У меня другая версия ffmpeg, флаг -progress работает?
Скрипт работал на тестах, но завис на большом файле — что не так?
Как безопасно передавать пути от пользователя?
Процесс не убивается по таймауту, ffmpeg продолжает грузить CPU. Почему?
Стоит ли брать библиотеку-обёртку вместо subprocess?
Как узнать длительность файла для расчёта прогресса в процентах?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.