Pull to refresh

ffmpeg: сохраняем прогресс конвертации

Level of difficultyEasy
Reading time8 min
Views2.8K

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

Но у процесса конвертации есть нехорошая черта: он занимает много времени. Иногда очень много. И вот когда длительность переваливает за десяток часов, то хочется "точек сохранения".

Вот подходы к созданию точек сохранения для утилит 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.
Пример работы с утилитой с картинках:

Добавляем новое задание на конвертацию: команда add
Добавляем новое задание на конвертацию: команда add
Задание сразу же начинает выполняться. Можно его прервать в любой момент
Задание сразу же начинает выполняться. Можно его прервать в любой момент
Снова запускаем ffmpegrr и конвертация продолжается с последней точки сохранения
Снова запускаем 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.

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

Tags:
Hubs:
Total votes 3: ↑3 and ↓0+4
Comments21

Articles