Если их пара. А если их пара десятков? А если несколько сотен? А если они еще и в разных форматах? Идея загонять все в видеоредактор отпала почти сразу. Но в начале — небольшая предыстория.
За каждым великим мужчиной стоит женщина, стоит и ворчит. (народная мудрость, отцензурировано)
Как и в прошлый раз, проблема возникла не на пустом месте. С эрой доступной цифровой фотографии началась эра доступного цифрового видео (ну ладно, не сразу, на пару лет позже). И, помимо фотографий, на домашнем сервере начали накапливаться видеофайлы. В данный момент их 299 362 штуки, накопленных примерно за 20 лет, в среднем 40 в день. Но не надо думать, что все это длинные клипы. В 90% случаев это очень короткие ролики вроде: «ребенок сидит», «ребенок идет» и моя «любимая» категория видео — «фотография из семейного альбома с голосовой заметкой» (ну т. е. мы смотрим семейный альбом и жена просто снимает на видео, то, что мы смотрим озвучивая при этом свои мысли; нет чтобы просто записать текстом, займет 300 байт на диске, но нет — появляется видео на 300 мегабайт).
Единственный формат просмотра всей этой коллекции который оказался возможным, — ретроспектива: а как мы провели этот день год/два/десять назад? Но отдельные мелкие ролики все испытанные мной проигрыватели (а уж тем более приставки и телевизоры) воспроизводят с паузой между ними. К тому же стабилизация видео в телефоны пришла далеко не сразу, так что большинство роликов еще и довольно дерганые, плюс многие были сняты еще на древние камеры с чересстрочной разверткой (интересно, остались ли такие на рынке?), так что привет «расческа». В общем, родилась идея, что перед просмотром такие ролики лучше склеивать в один, да еще и желательно с прогонкой деинтерлейсером и стабилизатором.
Первые попытки были через видео редактор, но, по вполне очевидным причинам, такой процесс требует много ручного труда, так что это, как говориться, не наш метод!
Наш метод это: консоль, ffmpeg (и питон)!
Если погуглить «как объединить видеофайлы в командной строке», то можно будет увидеть что‑то вроде:
Создайте файл video.txt с содержимым:
file video1.mp4
file video2.mp4
Затем запустите:
ffmpeg -f concat -safe 0 -i video.txt -c copy output.mp4
Не совсем в одну строку, «такое мы не любим».
Добавляем в запрос «в одну строку» и видим уже ужас типа такого:
ffmpeg -i video1 -i video2-filter_complex "[0:0][0:1][1:0][1:1]concat=n=2:v=1:a=1[outv][outa]" -map "[outv]" -map "[outa]" output.mp4
Не могу сказать, что я разобрался во всех деталях, но монструозность конструкции совсем не порадовала, да и работает через раз. Но главная проблема в том, что оба способа работают только на однотипных файлах. В моем случае видео не только не однотипные, но и включают в себя (о бич современного мира мобильных телефонов!) вертикальные видео, так как в один день съемка могла быть с нескольких разных устройств.
После недолгих раздумий я начал поиск библиотек на python которые могли бы удовлетворить мои нужды. Самый популярный вариант — MoviePy. Написал скрипт, который загружал описание всех видео, подбирал выходное разрешение, поворачивал вертикальное видео с добавлением полей и в конченом итоге выдавал один файл после запуска одной команды.
И все было хорошо, пока скрипт не дошел до 12 декабря 2010 года… Не знаю, чем вы занимались в этот день, но моей видеотеке за этот день оказался 321 видео файл и как говорится «бобик сдох».
Ну, справедливости ради, я почти сразу заметил непомерные аппетиты. Правда, казалось, что 640 Кбайт памяти хватит всем 32GB установленных на моем медиа сервере должно хватить, но не хватило. Сервер ушел в глубокий swap, а я — в глубокие раздумья, так как после более детального анализа выяснилось, что это в этот день количество видео файлов было совсем не рекордным — 30 дней содержали более 500 видео файлов.
Стало ясно, что без промежуточного сохранения файлов не обойтись, и по началу я натыкал костылей в виде вызовов ffmpeg вместе с MoviePy, но вскоре отказался и переписал все на использование только чистого ffmpeg напрямую.
И так, какие же шаги проходят видео файлы, прежде чем стать одним роликом?
Финальная последовательность действий такая:
Анализ с помощью утилиты ffprobe из пакета ffmpeg. Получаем данные о размере файлов, ориентации, частоте кадров и наличии интерлейса.
ffprobe -v error -show_format -show_streams -print_format json входной_файл
Если есть интерлейс, то делаем… барабанная дробь… деинтрелейс!
ffmpeg -i входной_файл -vf yadif -qp 0 -preset ultrafast -c:a flac -c:v libx264 выходной_файл
Где:
-vf yadif: собственно деинтерлейс
-qp 0: сохранение без потери качества
-preset ultrafast: поскольку это временный файл, то выбираем скорость в ущерб размеру
-c:a flac: переводим аудио в flac чтобы не терять качество в промежуточных файлах
-c:v libx264: видео кодек
Стабилизация видео
Оказалось, очень важно делать её именно на этом шаге, так как если сделать её до деинтерлейса, то интерлейс уже не сможет нормально отработать, и «расческа» останется. А если сделать её после изменения размеров и поворота вертикальных видео, то при стабилизации поля начинают «плавать» вместе со стабилизируемым объектом.
Стабилизация делается в два этапа:
На первом этапе создается файл с описанием трансформации:
ffmpeg -i входной_файл -vf vidstabdetect=stepsize=32:shakiness=10:accuracy=10:result=transforms.txt -f null -
На втором этапе‑ непосредственно стабилизация:
ffmpeg -i входной_файл -vf vidstabtransform=input=transforms.txt:zoom=0:smoothing=10,unsharp=5:5:0.8:3:3:0.4 -qp 0 -preset ultrafast -c:a flac -strict experimental -c:v libx264 -f выходной_файл
Параметры сохранения видео такие же как и на предыдущем шаге.
Поворот, изменение размера с добавлением черных полей, если пропорции видео отличаются от заданного размера + унификация частоты кадров:
ffmpeg -i входной_файл -f “scale=w='if(gt(a,{w}/{h}),{w},trunc(oh*a/2)*2)':h='if(gt(a,{w}/{h}),trunc(ow/a/2)*2,{h})',pad={w}:{h}:(ow-iw)/2:(oh-ih)/2:black” -qp 0 -preset ultrafast -c:a flac -strict experimental -c:v libx264 -r {fr} выходной_файл
Где:
{w} - ширина итогового видео,
{h} - высота итогового видео,
{fr} - частота кадров.
Плюс те же параметры для сохранения временного файла, что и на предыдущих шагах
К этому моменту у нас имеется N однотипных видео файлов, которые мы можем склеить той самой командой из начала статьи, но с небольшими доработками в виде задания необходимых параметров для оптимального кодирования:
ffmpeg -f concat -safe 0 -i video.txt -c copy -c:v libx264 -crf 23 -c:a aac -q:a 1 output.mp4
Где:
-c:v libx264: видео кодек
-crf 23: стандартное качество, можно заменить на 18, тогда это будет визуально без потерь, но итоговой размер в несколько раз больше
-c:a aac: аудиокодек
-q:a 1: максимальное качество звука, тут особо экономить не на чем, все равно аудио занимает места на пару порядков меньше чем видео.
Ну и для тех, у кого есть похожая задача и не хочется набивать себе мои шишки, — вот готовая библиотека, которую можно использовать и как часть проекта, и как самостоятельную утилиту.
Установка через pip:
pip install mixvideoconcat
Она реализует все шаги, описанные в статье, проста в использовании и, главное, делает то, что мне нужно, в одну строку (ну ок, две с импортом):
from mixvideoconcat import concat
concat(['video1.mp4', 'video2.mov', 'video3.avi'], 'output.mp4')
Либо в консоли (тут честная одна строка):
mixvideoconcat video1.mp4 video2.mov video3.avi output.mp4
Все исходники можно найти тут: https://github.com/sashacmc/mixvideoconcat
PS: Изначально я сделал ее как часть более общего проекта по управлению всем моим «видеохозяйством». Но потом решил выделить ее отдельно и как библиотеку, и как статью, так что продолжение следует… Ставьте лайки, подписывайтесь на канал, включайте колокольчик, ой это не сюда, про борьбу с субтитрами ютуба будет потом, в следующей статье ?
Комментарии (17)
arteast
08.11.2024 08:31Не пробовали делать libx265 + qp 0 для финального результата? У меня всегда получался размер меньше исходного (исходный после декодирования уже урезанный по качеству, и кодек эти узкие места эффективно использует, не привнося новых)
sashacmc Автор
08.11.2024 08:31Пробовал, да. Размер меньше получается, но и время обработки существенно увеличивается. А в поставленной задаче для меня скорость была немного важнее чем финальный размер файла.
(PS: в утилите и библиотеке можно задать кодек через переменную окружения FFMPEG_CODEC)arteast
08.11.2024 08:31Тогда можно попробовать libx264 + qp0 + какой-нибудь не совсем душманский пресет. Подозреваю, что и тогда размер будет не сильно больше, чем было изначально. Ну тут смотря, как отработали фильтры - `-r ...` не должен сильно влиять на размер сжатого; правда, результат работы такого фильтра очень не очень, это ж просто дублирование/удаление кадров со всеми вытекающими (minterpolate есть для желающих лучшего)...
voldemar_d
08.11.2024 08:31исходный после декодирования уже урезанный по качеству
Просто декодирование качество разве урезает?
Кодек H265 размер сжатого файла по сравнению с H264 может уменьшить при том же визуальном качестве, но не в разы, а раза в полтора-два обычно.
arteast
08.11.2024 08:31Нет, урезает конечно кодирование. Но повторное перекодирование тем же кодеком в режиме "без потерь" должно дать результат, сопоставимый по размеру с исходным файлом (а с лучшим кодеком - меньшего размера). Я все это к тому, что потеря качества была один раз; потом деинтерлейсер, стабилизатор и фреймрейт какие-то свои изменения привнесли, но основной источник потери качества и уменьшения энтропии (квантизация) остался, и повторное сжатие результата даже без потерь должно бы быть сопоставимым по размеру с исходным файлом. Я все это к тому, что наверное нет смысла повторно пережимать с потерями.
Mingun
08.11.2024 08:31А зачем вы на каждом шаге аудио во flac перекодируете? Не проще ли вообще его выкинуть из временных файлов и добавить лишь на последнем (или предпоследнем) шаге, когда ролик готов к склейке с остальными? Ну или хотя бы просто копировать аудио без его перекодирования?
sashacmc Автор
08.11.2024 08:31Возможно я что-то делал не так, но в форматах без сжатия у меня терялась синхронизация звука и видео (например при смене частоты кадров видео). Flac без потерь и на фоне операции перекодирования видео, перекодирование аудио вообще не заметно. При этом можно быть уверенным, что ничего нигде не потеряется.
ToSHiC
08.11.2024 08:31ffmpeg умеет чейнить фильтры, так что деиниерлейс, стабилизацию и поворот/пад можно сделать за один заход, сэкономить время. Так же уже на этой стадии можно закодировать в таргетный кодек видео, чтобы потом можно было конкатерировать ролики без ещё одной стадии перекодирования, для этого в кодек x264 надо передать опцию stitchable, чтобы он не оптимизировал sps/pps.
делать так же для аудио не рекомендую - там будут нюансы с дополнительными кадрами тишины, проще в промежуточном видео использовать flac.
sashacmc Автор
08.11.2024 08:31Проблема в том, что стабилизация выполняется в два прохода (Можно ли их чейнить? У меня не получилось.) и она должна быть между этапами интерлейса и поворота, так что их тоже не объединить.
С кодированием на последнем этапе в финальный кодек, я игрался, но есть пара нюансов, для разных видео фрагментов (напоминаю, что они очень разнородные) этапы могут различаться. Плюс к этому вылезали разные артефакты, например были проблемы с расчетом длины видео. Но я не знал про stitchable, попробую, спасибо, возможно мне как раз этого не хватало.
DhiKKi
08.11.2024 08:31А что мешало закинуть всё в вегас крайних версий?
sashacmc Автор
08.11.2024 08:31Здравый смысл, скоротечность человеческой жизни и отсутствие желания посвятить этому монотонному ручному процессу несколько лет.
DhiKKi
08.11.2024 08:31Не представляю честно говоря каким образом это может занять много времени если можно просто выделить все видео и закинуть их на таймлайн+на весь таймлайн накинуть деинтерлейсер+стабилизатор, рендер займёт некоторое количество часов, ну тут уж что поделаешь, если видосы все по папкам/архивам распиханы и на каждый день своя папка/архив тут уже да, согласен.
sashacmc Автор
08.11.2024 08:31Да, возможно я не совсем точно описал исходную задачу: ~300.000 исходных файлов, из которых должно получиться ~4000 выходных видео
voldemar_d
Непонятно, почему на таком количестве файлов память заканчивается. Каждый файл в память целиком декодируется?
sashacmc Автор
Не совсем, но похоже, на каждый файл MoviePy запускает отдельный экземпляр ffmpeg.
voldemar_d
Не знаю, как этот MoviePy устроен. Если бы я делал свою программу для склейки, то можно делать так: разбить последовательность файлов, которые нужно склеить, на фрагменты, допустим, по 10 штук. Каждый фрагмент склеить в промежуточный файл с перекодированием в один и тот же выходной формат. В конце все полученные промежуточные файлы склеил в один выходной уже без перекодирования.
А то и просто каждый файл по одному в промежуточный файл с одним и тем же выходным форматом перекодировать, а потом их разом в один конечный файл склеить без перекодирования через
ffmpeg -f concat
- в текстовый файл можно хоть 500 видеороликов засунуть.sashacmc Автор
Да, примерно так я и сделал