Если их пара. А если их пара десятков? А если несколько сотен? А если они еще и в разных форматах? Идея загонять все в видеоредактор отпала почти сразу. Но в начале — небольшая предыстория.

За каждым великим мужчиной стоит женщина, стоит и ворчит. (народная мудрость, отцензурировано)

Как и в прошлый раз, проблема возникла не на пустом месте. С эрой доступной цифровой фотографии началась эра доступного цифрового видео (ну ладно, не сразу, на пару лет позже). И, помимо фотографий, на домашнем сервере начали накапливаться видеофайлы. В данный момент их 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 напрямую.

И так, какие же шаги проходят видео файлы, прежде чем стать одним роликом?

Финальная последовательность действий такая:

  1. Анализ с помощью утилиты ffprobe из пакета ffmpeg. Получаем данные о размере файлов, ориентации, частоте кадров и наличии интерлейса.

ffprobe -v error -show_format -show_streams -print_format json входной_файл
  1. Если есть интерлейс, то делаем… барабанная дробь… деинтрелейс!

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: видео кодек
  1. Стабилизация видео

Оказалось, очень важно делать её именно на этом шаге, так как если сделать её до деинтерлейса, то интерлейс уже не сможет нормально отработать, и «расческа» останется. А если сделать её после изменения размеров и поворота вертикальных видео, то при стабилизации поля начинают «плавать» вместе со стабилизируемым объектом.

Стабилизация делается в два этапа:

На первом этапе создается файл с описанием трансформации:

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 выходной_файл

Параметры сохранения видео такие же как и на предыдущем шаге.
  1. Поворот, изменение размера с добавлением черных полей, если пропорции видео отличаются от заданного размера + унификация частоты кадров:

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} - частота кадров.
Плюс те же параметры для сохранения временного файла, что и на предыдущих шагах
  1. К этому моменту у нас имеется 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: Изначально я сделал ее как часть более общего проекта по управлению всем моим «видеохозяйством». Но потом решил выделить ее отдельно и как библиотеку, и как статью, так что продолжение следует… Ставьте лайки, подписывайтесь на канал, включайте колокольчик, ой это не сюда, про борьбу с субтитрами ютуба будет потом, в следующей статье ?

Комментарии (9)


  1. voldemar_d
    08.11.2024 08:31

    за этот день оказался 321 видео файл и как говорится «бобик сдох».

    Непонятно, почему на таком количестве файлов память заканчивается. Каждый файл в память целиком декодируется?


    1. sashacmc Автор
      08.11.2024 08:31

      Не совсем, но похоже, на каждый файл MoviePy запускает отдельный экземпляр ffmpeg.


      1. voldemar_d
        08.11.2024 08:31

        Не знаю, как этот MoviePy устроен. Если бы я делал свою программу для склейки, то можно делать так: разбить последовательность файлов, которые нужно склеить, на фрагменты, допустим, по 10 штук. Каждый фрагмент склеить в промежуточный файл с перекодированием в один и тот же выходной формат. В конце все полученные промежуточные файлы склеил в один выходной уже без перекодирования.

        А то и просто каждый файл по одному в промежуточный файл с одним и тем же выходным форматом перекодировать, а потом их разом в один конечный файл склеить без перекодирования через ffmpeg -f concat - в текстовый файл можно хоть 500 видеороликов засунуть.


        1. sashacmc Автор
          08.11.2024 08:31

          Да, примерно так я и сделал


  1. arteast
    08.11.2024 08:31

    Не пробовали делать libx265 + qp 0 для финального результата? У меня всегда получался размер меньше исходного (исходный после декодирования уже урезанный по качеству, и кодек эти узкие места эффективно использует, не привнося новых)


    1. sashacmc Автор
      08.11.2024 08:31

      Пробовал, да. Размер меньше получается, но и время обработки существенно увеличивается. А в поставленной задаче для меня скорость была немного важнее чем финальный размер файла.
      (PS: в утилите и библиотеке можно задать кодек через переменную окружения FFMPEG_CODEC)


      1. arteast
        08.11.2024 08:31

        Тогда можно попробовать libx264 + qp0 + какой-нибудь не совсем душманский пресет. Подозреваю, что и тогда размер будет не сильно больше, чем было изначально. Ну тут смотря, как отработали фильтры - `-r ...` не должен сильно влиять на размер сжатого; правда, результат работы такого фильтра очень не очень, это ж просто дублирование/удаление кадров со всеми вытекающими (minterpolate есть для желающих лучшего)...


    1. voldemar_d
      08.11.2024 08:31

      исходный после декодирования уже урезанный по качеству

      Просто декодирование качество разве урезает?

      Кодек H265 размер сжатого файла по сравнению с H264 может уменьшить при том же визуальном качестве, но не в разы, а раза в полтора-два обычно.


      1. arteast
        08.11.2024 08:31

        Нет, урезает конечно кодирование. Но повторное перекодирование тем же кодеком в режиме "без потерь" должно дать результат, сопоставимый по размеру с исходным файлом (а с лучшим кодеком - меньшего размера). Я все это к тому, что потеря качества была один раз; потом деинтерлейсер, стабилизатор и фреймрейт какие-то свои изменения привнесли, но основной источник потери качества и уменьшения энтропии (квантизация) остался, и повторное сжатие результата даже без потерь должно бы быть сопоставимым по размеру с исходным файлом. Я все это к тому, что наверное нет смысла повторно пережимать с потерями.