Фикс растущего рассинхрона через ffmpeg 06.04.2019


Скачал я значит видео из Вконтактика с помощью небезызвестного инструмента youtube-dl, и получил mp4 файл который довольно неплохо воспрозводился в плеере mpv. Но при загрузке этого файла на Youtube он воспроизводился не так, как на компьютере - каждые несколько секунд (5-7, что-то около того) звук прерывался на долю секунды, и продолжался дальше. При этом длина видео на ютубе совпадала с длиной видео на компьютере.

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

Собственно, на ютубе произошло почти то же самое, но немного иначе - при кодировании каждый сегмент аудиодорожки привязался по timestamp-ам к соответствующему видеосегменту - изначально-то на серверах ВК видео хранится в контейнере MPEG-TS, т.е. последовательно склееные сегменты, содержащие вместе кусочек аудио и кусочек видео. И получилось, что почти в каждом таком сегменте имелся лишний видеокадр, и когда он проигрывался, как раз получался разрыв в проигрывании аудио, т.е. тишина. Таким образом получилось, что реальная длина видео больше, чем аудио. А конкретно, при длине видео около двух часов разница составила где-то полторы минуты.

На компьютере файл нормально проигрывался, потому что плеер учитывал частоту кадров, объявленную в метаданных (30 fps) и временные метки (timestamp-ы), и отбрасывал все лишние кадры, которые не успели воспроизвестись до наступления следующего timestamp-а, а аудио сегменты наоборот растягивал для их достижения. Это не воспринимается за 5-7 секунд (длина TS сегмента), поэтому кажется, что видео в порядке. Проверить это можно, например, плеером FFPlay, который является частью пакета ffmpeg. В консоль он выводит параметр fd= (frames dropped), который показывает количество кадров, которые пришлось отбросить. При воспроизведении это число только увеличивается, что спасает от рассинхрона. Но при извлечении и возвращении обратно аудиодорожки, временные метки с нее теряются, и синхронизировать становится не по чем.

Мне на выходе надо было получить исходное, нетронутое видео (для достижения наилучшего качества) с обработанным звуком. Для этого:

  1. Я извлек аудиодорожку, которую потом обработал:
ffmpeg -i input.ts -c copy input.aac
  1. Извлек чистый видео поток:
ffmpeg -i input.ts -c copy input.h264

На данном этапе уже можно увидеть в выводе команд, как отличаются продолжительности этих двух потоков.

  1. Так как мы хотим избавиться от лишнего кадра, но при этом все их сохранить, мы должны интерпретировать входной видеопоток как поток с увеличенной частотой кадров в секунду. При одном и том же количестве кадров, чем больше частота кадров в секунду, тем меньше продолжительность видео. А нам как раз это и надо - привести продолжительность видео к продолжительности аудио, чтобы не менять длину самого аудио (без потерь это сделать не получится). Но какую частоту кадров выбрать? Сначала я попробовал 31 fps:
ffmpeg -r 31 -i input.h264 -i input.aac -c copy output.mp4

На выходе получилось видео длиной ровно как моё аудио! Совпадение? Не думаю - просто по умолчанию FFMpeg дожидается окончания самого длинного потока, и только тогда завершает файл. А длинным потоком тут было аудио, а видео потеряло в длине около 4 минут - это можно проверить, если не добавлять аудио в команду выше - тогда результирующий mp4 файл будет иметь только видео поток и можно легко увидеть его длину. Кстати, контейнер MP4 в данном случае важен - он переваривает сырой h264 поток без таймстемпов (видимо, подставляя их самостоятельно). Итак, нужно найти дробную частоту кадров между 30 и 31 для достижения нужной длины. Так как у нас есть данные по длине видео при частоте 30 fps и 31 fps, будет несложно по пропорции посчитать fps для длины аудио потока. У меня получилось 30.3245

ffmpeg -r 30.3245 -i input.h264 -i input.aac -c copy output.mp4

Так как невозможно в один момент показать какую-то долю кадра, иногда у нас в секунде будет показываться 30 кадров, а иногда 31 - тот самый лишний кадр, но уже с корректными временными метками. Про простой способ проставления временных меток я уже писал, но в данном случае он не подошел - частота кадров все еще остается 30, поэтому длина видео остается большой, и временные метки выставляются опять же по этой "большой" длине.

В результате я без перекодирования видео получил файл, корректно воспроизводящийся и на компьютере, и на Ютубе, и длина его была равна длине аудио, т.е. это реальная длина того, что происходило на видео.

UPD: Если задача стоит просто избавиться от пауз в видео при заливке на Ютуб, и при этом чистота исходного аудиопотока не важна, то самый простой способ - это подогнать аудио под временные метки путем ресемплинга (растяжения/сжатия аудио, которое влечет изменение частоты). Это делается аудиофильтром aresample. В качестве параметра можно передать максимальное количество семплов в секунду, которые будут изменены. В документации в качестве примера приведено значение 1000, и это довольно неплохое значение, которое дает средненький результат. Чем больше это значение, тем плавнее частота дискретизации будет возвращаться к своему исходному значению, а чем меньше - тем резче будет скачок (или провал) частоты, но зато больше аудиоматериала останется в исходном виде. Если уменьшить параметр почти до нуля, в обработанной дорожке останутся паузы. Я для себя подобрал значение, при котором паузы и провалы частоты наименее заметны - 500 семплов в секунду:

ffmpeg -i input.ts -c copy -c:a aac -b:a 320k -af aresample=async=500 output.ts


Теги: FFMpeg, заметки на полях