Многие из нас периодически сталкиваются с необходимостью конвертации видео: в другое разрешение, в другой формат или др.
Но у процесса конвертации есть нехорошая черта: он занимает много времени. Иногда очень много. И вот когда длительность переваливает за десяток часов, то хочется "точек сохранения".
Вот подходы к созданию точек сохранения для утилит ffmpeg и рассмотрены в статье.
Краткий резюм: Создана программа для сохранения прогресса при работе с ffmpeg. Поддерживается сборка под Windows, Linux, MacOS.
Код можно взять здесь: ffmpegrr на github, ffmpegrr на gitflic.
Описание здесь: https://apoheliy.com/ffmpegrr.
Итак
Чтобы сохранить прогресс конвертации видеофайла сразу же возникает идея с разбиением общей конвертации на кусочки, которые обрабатываются по-отдельности, и потом всё это нужно сложить в единый файл. Да, ничего оригинального :).
Такой подход позволяет и останавливать процесс в любом месте. И при крэшах или других неприятностях позволяет не потерять прогресс.
Формат и данные
Сначала определимся с форматом, что делаем, и данными, которые получаем. Так как при разбиении и конвертации отдельных фрагментов получится очень много промежуточных файлов, то их нужно где-то хранить. Причём хранить их рядом с исходным или результирующим файлом не очень хочется: будет помойка из файлов, и если потребуется ручная очистка от промежуточных файлов, то можно удалить полезные файлы.
Поэтому все файлы будут храниться в домашней директории пользователя. Она [практически] всегда доступна на чтение и запись (актуально для linux и macos). Там можно создать отдельную папочку (например, .ffmpegrr), и в случае необходимости всё подчистить можно просто удалить эту папку. Для получения пути к домашней директории пользователя будем использовать библиотеку home-dir на github. Она кроссплатформенная и скрывает весь платформозависимый код.
Ещё одна тонкость связана с количеством текущих конвертаций. Если мы хотим сделать конвертацию, а потом проверить результат и возможно даже перезапустить часть конвертации, то желательно сделать пару вещей:
завершённую конвертацию сразу не удалять. Лучше даже сделать отдельную команду, по которой пользователь будет очищать промежуточные файлы от уже законченных конвертаций;
сделать поддержку нескольких конвертаций: одна завершилась, но ещё не удалена; другая добавилась в работу. Такой подход потребует идеологии "заданий" (task): каждая конвертация представляет отдельное задание и у каждой своя отдельная папка с промежуточными файлами и настройками. Пользователь создаёт задания, они последовательно выполняются.
Теперь формат: так как по функционалу вполне допустим консольный вариант, то он и будет реализовываться. Это самый переносимый вариант, который работает на большинстве ОС, и не нужно тащить за собой жирный Qt. И да, это требует других (кросс-платформенных!) решений по работе с директориями для пользовательских файлов и запуска внешних процессов.
Вариант 1: Используем разбивку по времени
Выбираем небольшой интервал, например 1 минута (предполагается, что конвертация одной минуты не будет очень долгой). Далее находим длину видеофайла и нарезаем эту длину на нужно количество фрагментов. Результат потом склеиваем.
На практике это выглядит как запуск внешних утилит с нужными аргументами. Для запуска используется кросс-платформенная библиотека reproc на github.
Получение длины: запускаем ffprobe и задаём ей аргументы:
-show_entries format=duration -sexagesimal -of default=noprint_wrappers=1:nokey=1
Так получим длину без лишней разметки. Причём длину запрашиваем в формате sexagesimal.
Лирическое отступление про время
Время в утилитах ffmpeg вводится и выводится в двух разных форматах:
[-][HH:]MM:SS[.m...]
и
[-]S+[.m...][s|ms|us]
Вот первый формат с минутами, секундами и есть sexagesimal. Чем он хорош? В нём лучше детектируются ошибки - по наличию двоеточия. Причём такая проверка необходима, так как иногда утилиты вместо правильных данных (не обязательно время) могут сказать N/A, и чем проще сделать проверку на ошибку - тем лучше. Также отметим, что во времени везде десятичные точки. Никаких локалей, запятых и других вариаций.
Ещё одна загвоздка с временем: минимальное деление, минимальный кусочек времени. Судя по формату, там могут быть и миллиардные доли микросекунд. Для наших задач такая универсальность это очень плохо (как и считать время в формате числа с плавающей запятой). Для работы интервалами лучше всего использовать числа с фиксированной точкой - тогда они хорошо складываются и точность не страдает. Но что взять за одно деление?
И тут можно обратить внимание, что ffmpeg точнее микросекунд ничего не измеряют. Да, явно об этом у ffmpeg не говориться. Согласен, можно придумать вариант, где звук и видео будет с мегагерцовой частотой дискретизации, однако это явно не "бытовой" уровень и на практике микросекундной точности хватает. Поэтому все времянки переводятся в количество микросекунд - и это работает.
Для разбиения общего процесса на отдельные фрагменты по времени в утилиту конвертации (ffmpeg) добавляем аргументы перед указанием входного файла:
-ss время_начала -t интервал
Здесь интервал уже можно указывать в любом удобном виде, например как число - утилиты разберут и точность будет правильная. Также вместо указания интервала можно использовать параметр -to время_окончания, но есть два момента: во первых, -t имеет приоритет. Во вторых, явно не указано: время окончания включая указанное время или не включая, и интервал здесь видится более понятным.
Итак, общую длительность разрезали на кусочки, каждый кусочек конвертировали по-отдельности, с правильными интервалами. Теперь нужно всё собрать.
Для сборки фрагментов в единый файл используем ffmpeg и в качестве входного формата указываем "concat":
-f concat -i list_file_name
Формат concat подразумевает, что в качестве входного файла используется текстовый файл со списком фрагментов. ffmpeg будет вставлять их последовательно и всё объединит в выходном файле.
Теперь к результатам: если это всё сделать, то иногда можно налететь на неприятную штуку: при воспроизведении возникают паузы на границах разделения фрагментов. Причём жирные паузы: доходит до 8 секунд.
Что же там такого можно на 8 секунд добавиться? Оказывается, звуковой поток не хочет разрезаться на заданные кусочки. Звуковой! То есть он, конечно, режется. Но может в конце добавить несколько секунд тишины. И в дальнейшем, при склейке, эта "финальная" пауза появляется между фрагментами. Честно говоря, не понимаю, что там такого может навыравниваться на такую паузу, звук в кодеках режется на очень мелкие блоки. Но, как есть.
Вариант 2: Отрезаем звук и другое
Чтобы обойти проблемы с разделением звука можно его вообще не делить. Вот брать, и конвертировать согласно настройкам/параметрам. И к звуку можно присовокупить субтитры и потоки данных, если они есть. Тем более, что обычно конвертация звука/субтитров/данных не съедает много времени и ресурсов по сравнению с видеопотоком.
Поэтому подход к "точкам сохранения": все не-видео потоки конвертируем отдельно, без нарезок. Видеопоток конвертируем с нарезкой на фрагменты.
На практике все разбиения по потокам делаются через добавление аргументов:
-vn для работы без видео;
-an -sn -dn для работы только с видео.
Правда, и тут не обошлось без ложки дёгтя:
Если в результате конвертации не получится аудио/субтитров/данных, то ffmpeg выдаст ошибку. И не создаст никакого файла. Причём код ошибки (1) там такой-же, как и для других ошибок.
Причём предварительно просчитать такую ситуацию может быть проблематично, так как возможны варианты: входной файл только с видеопотоком; заданы аргументы, что ничего кроме видео не пройдёт, например есть мапирование потоков (аргумент -map).
Чтобы обойти эту проблему используем костыль: перехватываем поток вывода ошибок от ffmpeg и там ловим определённые слова ("does not contain any stream" и всё такое). Очевидно, что это костыль, и работает на английском языке и пока не поменяются описания. И если кто знает, как сделать хорошо - напишите в комментариях.
Вырезание потоков и конвертацию фрагментов обсудили выше. Для объединения стримов используем ffmpeg, в неё заводим несколько входных файлов и через мапирование указываем откуда какие потоки брать.
-map 0:v? -c:v copy -map 1:a? -c:a copy ...
Отмечу, что при мапировании используется ? и это позволяет нам не разбираться с вопросам "а существует ли аудиопоток? Или субтитры?" и не заниматься лишними разборами. В общем, это удобно.
Что в результате: используя выделение не-видеопотоков и разбиение видеопотока на фрагменты, мы избавляемся от пауз между фрагментами. Да, паузы ушли.
Но есть ещё шероховатости: в зависимости от длины медиафайла, кодирования и др. можно получить рассинхронизацию видеопотока со звуковым. Да, это не явные паузы. Это плавное расползание одного потока от другого. Но оно может достигать единиц секунд - и это нехорошо.
Вариант 3: нарезаем правильно
Изучение рассинхронизации потоков показывает, что разрезание на кусочки работает не всегда точно и при конвертации могут добавляться "доли кадра": лишние 10-20 миллисекунд на фрагмент. Когда количество фрагментов небольшое это не сильно заметно. Но если число фрагментов переваливает за несколько десятков - то тут уже всё грустно.
Поэтому напрашивается вариант нарезки по кадрам: вот есть кадр, у него есть метка времени и по этим меткам делаем разрезание. Ещё лучше, если кадр будет ключевой, но на практике у меня особой разницы не было.
Получить список ключевых кадров можно через ffprobe:
ffprobe -select_streams v -skip_frame nokey -show_frames -show_entries frame=pkt_pts_time,pict_type input.mp4
если добавить аргументы формата вывода
-sexagesimal -of csv
то выдача будет в удобном разбираемом формате и время там будет правильно представленное.
Теперь тонкости процесса.
Иногда список ключевых кадров выводится очень долго, и даже не в консоль всё равно долго. Да, часто он выстреливает, как из пулемёта. Однако у меня был файлик (всего десятки гигов), на котором только получение получение списка ключевых кадров (в файл, не на консоль) заняло почти полчаса (точнее 27 минут 39 секунд и полученный файл был 530 кбайт). Поэтому вытаскивать все ключевые кадры - это плохой вариант, нужно как-то этот процесс ускорять.
Для ускорения процесса есть параметр -read_intervals, для которого можно задать интервал, в которых нужно искать ключевые кадры. Например, вот так:
-read_intervals 150%+10
При таком аргументе будут обрабатываться кадры с 150 секунд от начала и длительность окна 10 секунд. Естественно, стартовая метка (150 с) она очень условная, и может сдвигаться на 5-10 секунд легко.
При этом есть пара замечаний:
1. согласно документации в параметр read_intervals можно задать несколько интервалов через запятую. Так вот это не работает: утилита не ругается на дополнительные интервалы (не выдаёт ошибку аргументов); выдаёт кадры из первого интервала; но не выдаёт кадры из следующих интервалов. Поэтому параметр используем, но запускаем утилиту несколько раз на разные интервалы.
2. на некоторых файлах время работы по получению временных меток кадров зависит от ширины интервала поиска. И времена там начинают измеряться секундами: 3 секунды, 5 ... И если интервал сделать не 10 секунд, а 5 - то время работы может примерно в два раза и сократиться. Причём время работы не зависит от расположения интервала: делай запрос из начала файла, с конца файла - всё одинаково. Чтобы всё совместить: и кадры найти, и сделать это быстро - пришлось уменьшать интервал поиска. Хороший вариант получается на 100 мс: кадры находятся, скорость достаточная. При таком подходе поиск ведётся для любых кадров (и ключевых, и обычных): если нашлись ключевые, то используются ключевые, если нашлись только обычные, то используются обычные.
Примечание: на практике у меня найденные кадры всегда начинались с ключевых, так что возможно это излишнее усложнение.
Что получилось в итоге: ищём времена ключевых или обычных кадров и на их основе формируем фрагменты для нарезки. Понятно, что такой способ хорошо работает только для одной видеодорожки, т.к. при нарезке нескольких дорожек можно получить микропаузы в видеопотоке - и это приведёт к рассинхрону.
Итак, что получилось: Получилось вполне рабочий механизм (также см. ложку дёгтя ниже) и по нему была написана утилита ffmpegrr.
Пример работы с утилитой с картинках:



Код ffmpegrr можно взять здесь: ffmpegrr на github, ffmpegrr на gitflic.
Описание здесь: https://apoheliy.com/ffmpegrr.
Ложка дёгтя
Файлы всякие бывают. И иногда попадаются такие, где детекция кадров говорит, что кадров как-бы и нет. Понятно, что сами кадры там есть: медиафайл проигрывается и перемотка на нём хорошо работает. Только ffprobe ничего полезного сказать не может.
Например, его ответ может выглядеть так:
[mpeg4 @ 0x5555555e0d40] Video uses a non-standard and wasteful way to store B-frames ('packed B-frames'). Consider using the mpeg4_unpack_bframes bitstream filter without encoding but stream copy to fix it.
И здесь, возможно, поможет пара лишних параметров или предварительных конвертаций. А возможно, это не единственная причина, по которой невозможно получить информацию по кадрам. Поэтому, чтобы не гадать на кофейной гуще, используется подход: вот тебе результат, и ты на него посмотри. Если всё устроит, отлично - пользуйся. Не устроит - всегда есть вариант цельной конвертации.
Комментарии (21)
nidalee
03.02.2025 22:17За инициативность плюс, но разве в ffmpeg не работает кнопка Pause/Break на клавиатуре? :)
Память это не освободит, на на ПК пригодном для конвертации видео ее все равно должно быть более чем. Не придется изобретать велосипед.
Mingun
03.02.2025 22:17И что, после перезагрузки компа она тоже заработает?
nidalee
03.02.2025 22:17Нет конечно, а зачем перезагружать компьютер в середине процесса конвертации? Поставил на паузу, сделал дела, продолжил конвертацию.
В статье про перезагрузку тоже ничего не сказано.
Mingun
03.02.2025 22:17Затем, что зачем мне её просто на паузу ставить? Чтобы что? Она ещё дольше бы выполнялась? Как правило, продолжать нужно по той причине, что она прервалась по не зависящим от тебя обстоятельствам (например, комп в перезагрузку ушёл или просто вырубился посреди процесса) и хотелось бы, чтобы работа не пропадала вхолостую.
nidalee
03.02.2025 22:17Затем, что зачем мне её просто на паузу ставить? Чтобы что?
Например, кодирование какого-нибудь HEVC по медленному профилю может длиться сутками. Я нередко заряжал такие энкоды на время своего отсутствия, возвращался, делал свои дела с кодированием на паузе, а потом возобновлял процесс.
Она ещё дольше бы выполнялась?
В какой-то момент процесс становится настолько длительным, что несколько часов простоя банально не влияют на общую картину. В 3Д, например, тоже. Я вам больше скажу: если у вас кодирование длится 4 часа, то вам вообще велосипед из статьи ни к чему, проще перетерпеть.
Как правило, продолжать нужно по той причине
Это все проблемы, которые нужно побороть на корню, и я объясню почему:
например, комп в перезагрузку ушёл
Нужно использовать ОС, которая сама не перезагружается. Или привыкнуть откладывать естественные процессы ОС (я сам виндузятник по большей части).
просто вырубился посреди процесса
Если он вырубился, то вы либо не прошли стресс-тест кодированием (и тогда вам нужно разобраться, что у вас в системе нестабильно, а не городить костыли), либо у вас проблема с электричеством (ее тоже можно и нужно решить, как минимум ИБП).
А все эти проблемы нужно решать не костылем из статьи по одной простой причине: вот это все городится исключительно под ffmpeg, но упомянутые выше проблемы вас будут преследовать буквально во всем софте. В какой-то момент костылей станет так много, что продираться через них уже будет сильно сложнее, чем просто решить проблему.
Mingun
03.02.2025 22:17просто решить проблему
Правильно. В ffmpeg уже давно бы пора решить проблему (например, сохраняя (можно по флагу, у него уже флагов столько, что от ещё одного хуже не станет) промежуточные вычисления куда-то на диск (все эти таблицы квантования/Хаффмана или что там ещё используется). Ну а пока приходится довольствоваться костылями (ИБП — это тоже костыль, кстати, физический).
nidalee
03.02.2025 22:17промежуточные вычисления куда-то на диск
ЕМНИП, log с 2-pass так и работает. Но он настолько никому не нужен, что я даже сейчас сходу не могу нагуглить синтаксис. Лет 10 назад пользовался.
(ИБП — это тоже костыль, кстати, физический).
ИБП - это еще и про защиту техники от инфаркта, при софтовом падении конкретно энкодера пострадает разве что гордость. На рабочих лошадках костылей будь здоров - тот же ECC. Но это как раз потому, что железные костыли лучше работают и универсальные.
Apoheliy Автор
03.02.2025 22:17Основная цель утилиты из статьи - это именно сохранить состояние, а не поставить на паузу. И да, она отлично переживает перезагрузку.
Более того, [путём переименований папок] можно хранить такое сохранённое состояние очень долго (просто сохранить до лучших времён) и в это время гонять другие конвертации или работу.
Из моей практики приведу несколько примеров, когда пауза может не сработать:
хочется выключить компьютер (ну чтобы не гудел или по другим причинам);
другая работа на компьютере приведёт к перезагрузке. Например, я разрабатываю модули ядра (windows: драйвера). Что-то можно сделать через виртуалку, что-то - нет. И вот когда "нет", любой kernel panic (windows: BSOD) может всё поломать (даже конвертация в виртуалке или докере может не спасти - поломается всё). Да, можно завести отдельный компьютер для конвертаций, но это как-то жирно.
сама конвертация по каким-то причинам поломалась. Например, у меня были случаи аварийного останова. Причём повторный прогон отрабатывал нормально.
Как я уже писал, все углы можно обойти. Но, например, для меня поднимать виртуалку или докер, чтобы сконвертировать видеофайл это неизи. Если есть другой способ (например, моя утилита) - то это гуд.
nidalee
03.02.2025 22:17Не, я костыли уважаю и даже сам пишу. Ну как "сам" - с ChatGPT недавно сделал soundboard для Mumble, например.
Например, у меня были случаи аварийного останова.
Вот это вам нужно разобраться почему происходит, ffmpeg достаточно стабилен, чтобы не падать посреди процесса.
AuToMaton
03.02.2025 22:17Мне с детства казалось, что если я в одном месте, то всякая дурацкая деятельность должна быть в другом. Повзрослев, я понял что лучшее место для работы, в смысле не для игр, в Windows - Parallels на Маке. Она тебе - не выключайте компьютер!!!, а ты ей не глядя - suspend, и домой с внешним диском в кармане.
Применительно к этой вашей ffmpeg - есть же Docker со товарищи…
m6atom
03.02.2025 22:17Если любому человеку… молдаванин, женщина, не важно. Нужно… А человеку только надо было всего лишь один… Кампучия, да? Тридцать восьмой год. А? Красавцы… А он говорит: «нет»… а мы не хотим, а мы не хотим, мы хотим чтобы русский балет, чтобы он развивался... эскорт… она приехала да, пачка… все оп ух быль-были-были-у ууп, все… русский балет
simplepersonru
03.02.2025 22:17интересно, сможет ли https://docs.docker.com/reference/cli/docker/checkpoint/ решить эту задачку, вроде по известным ограничениям проходит. Делать чекпоинты условно каждую минуту с перезаписью старого.
Sap_ru
Все эти ваши нерезки ломают ключевые кадры )(и во входном потоке и в выходном), битовый буфер (который используется для выравнивания битрейта) и разбиение на сцены. А уж при многопроходном сжатии (а качественное сжатие можно получить только при нескольких проходах) ваша схема вообще работать не будет.
nidalee
Вообще, кодирование каждой сцены по отдельности - это своего рода bleeding edge. Ну, был в 2017. Но это жуткий геморрой, и заниматься таким серьезно стоит только если большие бабки на кону.
https://slhck.info/video/2017/03/01/rate-control.html
ahabreader
Можно разбить задачу на три части:
Научиться резать, кодировать кусками и склеивать
Перебирать параметры для максимизации метрики качества (VMAF). Кодируем кусок, смотрим на VMAF, повторяем с другими настройками (QP в статье netflix)
Повторять для разных разрешений, выбирать куски по разным критериям ("minimizing bitrate for a given average quality or maximizing quality for a given average bitrate")
Первые два пункта реализованы в av1an, а третий вроде бы нужен только видеохостингу.
nidalee
Будем честны: первые два нужны тоже только видеохостингу, дома никто не будет перебирать десятки вариантов кодирования сцен :)
ahabreader
Первый нужен, если вдруг энкодер плохо параллелится.
Нужен второй или не нужен, но чтобы взять готовое-открытое-бесплатное решение по ссылке, много сил не надо.
ahabreader
Не-не-не, истина
где-то рядомпосередине.TL;DR: резать видео на куски - это нормальный подход, который должен хорошо работать в CRF-режимах и который используется для распараллеливания кодирования AV1. Только резать не по ключевым кадрам, а по scenecut'ам, как это делает утилита av1an.
___
Если при кодировании целиться в определённый уровень качества, а не в размер файла (не в средний битрейт), то тогда алгоритмы rate control'а не должны делать ничего важного на длинных промежутках.
Допустим, кодируем по отдельности 2 сцены с разной сложностью по 5 минут каждая.
Хотим получить файл в X мегабайт - придётся сцены кодировать в файлы X/2 мегабайт - битрейт распределяется неоптимально (сложная сцена недополучает, лёгкая - получает избыточный).
Хотим получить X единиц качества (--crf, --cq-level, VMAF) - тогда сцены друг от друга почти не зависят, раздельное кодирование почти никак не вредит.
Примерно поэтому разработчики x264 настаивали на том, что в их энкодере однопроходный CRF-режим - лучший по качеству. 2pass чуть-чуть выигрывает от того, что алгоритмы алгоритмы rate control'а видят весь файл (словно у нас CRF +
‑-rc‑lookahead ∞
), но время, затраченное на второй проход, в x264 выгоднее потратить иначе (вместо 2pass выбрать CRF с более медленным пресетом).Другая часть истории - гугл. Он стал делать кодеки. И эти его кодеки плохо параллелятся на все ядра. Поэтому в AV1 кодирование кусками (chunking) и склеивание - это чуть ли не ожидаемый сценарий использования. Поэтому появилась утилита av1an - она режет видео на куски по scenecut'ам и запускает пул из N штук ffmpeg'ов (ну и делает вид, что её работа гораздо сложнее, чем на самом деле).
___
Разрезание по ключевым кадрам автоматически достигается в ffmpeg через
-noaccurate_seek
: Previous behavior (seek only to nearest preceding keyframe, despite inaccuracy) can be restored with "-noaccurate_seek" - https://trac.ffmpeg.org/wiki/Seeking___
Чтобы меньше вредить качеству, режут по переходам между сценами и shot'ами. Дальше можно выбирать наиболее резкие переходы и наложить ограничение на минимальную длину куска. Методы: использовать выхлоп встроенного ффмпеговского scene detector'а или использовать сторонний, как делает av1an.
___
А если мы в месте склейки вылезем за ограничения, которые важны аппаратным декодерам? Если мы заморачивались со всякими --level, --bluray-compat, --vbv-maxrate и хотим сохранить гарантии совместимости?
То мы слишком много хотим, то это тонкие материи, наверное, есть платные инструменты для точного контроля совместимости. Нас бы и без склейки мог, например, обломать один из многочисленных багов с упоминанием VBV в истории версий x265.ahabreader
Считайте, что я этого не говорил, это глупость.
Поизучать поведение можно на тестовом файле, где I/P/B-кадры стоят на удобных timestamp'ах и на видео наложен тип и номер кадра.
Опасно
Переносы (
^
) и экранирование (%->%%
) виндовые, не для bash.-vf
накладывает на кадр свойства%{pict_type}
,%{frame_num}
,%{pts} через 3 вызова drawtext.
ffmpeg совсем не годится для такого, выглядит и отлаживается отвратительно, это работа для vapour/avisynth, но зато без лишних зависимостей
Если кратко:
интервал работает так: [-ss; -to), -t тоже задаёт открытый интервал
но только не с
-c copy
, тогда интервалы работают... как-то сложноконец интервала с
-c copy
может быть P-кадром, может быть битым (выкинул 5 B-кадров перед последним P-кадром)-noaccurate_seek -ss _ -i _
делает фигню (у меня добавление опции захватывает ещё один кадр перед -ss, независимо от типа кадра)автопоиск ключевых кадров для обоих концов интервала есть разве что в
-f segment -segment_times _,_,... -c copy