[Перевод] Руководство по FFmpeg libav

fdea2eqs62xxuxzko84c8mvj7z4.png


Долго искал книгу, в которой было бы разжёвано, как использовать FFmpeg в качестве библиотеки (в дальнейшем в тексте — libav, т.е. library audio video). Обнаружил учебник «Как написать видеоплеер и уложиться в менее чем тысячу строк». К сожалению, информация там устаревшая, так что пришлось создавать мануал своими силами.

Большая часть кода будет на C, однако не волнуйтесь: Вы легко всё поймёте и сможете применить на любимом языке. У FFmpeg libav уйма привязок ко многим языкам (в том числе и к Python и к Go). Но даже если Ваш язык прямой совместимости не имеет, всё равно можно привязаться через ffi (вот пример с Lua).

Начнём с краткого экскурса о том, что такое видео, аудио, кодеки и контейнеры. Затем перейдем к ускоренному курсу, посвященному использованию командной строки FFmpeg, и, наконец, напишем код. Не стесняйтесь переходить сразу в раздел «Изучаем FFmpeg libav — Трудный путь».

Есть мнение (и не только моё), что потоковое интернет-видео уже приняло эстафету от традиционного телевидения. Как бы то ни было, FFmpeg точно достоин изучения.

Оглавление
EDISON Software - web-development
Статья переведена при поддержке компании EDISON.

Разрабатываем приложения для видеонаблюдения, потокового видео, а также занимаемся видеозаписью в хирургической комнате.

Мы очень любим работать с видео! ;-)

5klznusfiwgawobmtbcrpdboscw.jpeg

Вступление ↑


Видео — это то, что ты видишь! ↑


Если последовательность изображений менять с заданной частотой (скажем, 24 изображения в секунду), то создаётся иллюзия движения. Это и есть основная идея видео: серия изображений (кадров), движущихся с заданной скоростью.

Иллюстрация 1886 года.

Аудио — это то, что ты слышишь! ↑


Хотя немое видео может вызывать самые разные чувства, добавление звука резко повышает степень удовольствия.

Звук — это колебательные волны, распространяемые в воздухе или в любой других средах передачи (таких как газ, жидкость или твердое вещество).

В цифровой аудиосистеме микрофон преобразует звук в аналоговый электрический сигнал. Затем аналого-цифровой преобразователь (АЦП) — обычно с использованием импульсной кодовой модуляции (ИКМ) — преобразует аналоговый сигнал в цифровой.

eugb_mwqa5x5yctawseum7qdtss.png


Кодек — сжатие данных ↑


Кодек — это электронная схема или программное обеспечение, сжимающее или распаковывающее цифровое аудио/видео. Он преобразует необработанное (несжатое) цифровое аудио/видео в сжатый формат (или наоборот).

Но если мы решим упаковать миллионы изображений в один файл и назовем его фильмом, у нас может получиться огромный файл. Давайте посчитаем:

Допустим, создаём видео с разрешением 1080×1920 (высота × ширина). Тратим 3 байта на пиксель (минимальную точку на экране) для цветового кодирования (24-битного цвета, что дает нам 16 777 216 разных цветов). Это видео работает со скоростью 24 кадра в секунду, общая продолжительность 30 минут.

toppf = 1080 * 1920 //Всего пикселей в одном кадре
cpp = 3 // Стоимость пикселя
tis = 30 * 60 // Время в секундах
fps = 24 // Кадров в секунду

required_storage = tis * fps * toppf * cpp


Для этого видео потребуется приблизительно 250,28 Гб памяти или 1,11 Гбит/с! Вот поэтому и придётся использовать кодек.

Контейнер — удобный способ хранения аудио/видео ↑


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

Это единый файл, содержащий все потоки (в основном, аудио и видео), обеспечивающий синхронизацию, содержащий общие метаданные (такие как заголовок, разрешение) и т.п.

Обычно формат файла определяется по его расширению: например, video.webm — это, скорее всего, видео с использованием контейнера webm.

ayduqaajbhuh59tah4vdgcu2sj0.png


Командная строка FFmpeg↑


Самодостаточное кроссплатформенное решение для записи, конвертации и потоковой передачи аудио/видео.

Для работы с мультимедиа у нас есть восхитительный инструмент — библиотека под названием FFmpeg. Даже если Вы не используете её в своём программном коде, то всё равно используете её (Вы ведь используете Chrome?).

В библиотеке есть консольная программка для ввода командной строки под названием ffmpeg (маленькими буквами, в отличие от названия самой библиотеки). Это простой и мощный бинарник. Например, можно конвертировать из mp4 в avi, просто набрав такую команду:

$ ffmpeg -i input.mp4 output.avi


Мы только что сделали ремиксинг — сконвертировали из одного контейнера в другой. Технически FFmpeg также может выполнять транскодирование, но об этом позже.

Инструмент командной строки FFmpeg 101 ↑


У FFmpeg есть документация, где всё отлично объяснено, как что работает.

Схематично, программа командной строки FFmpeg ожидает, что следующий формат аргументов выполнит свои действия — ffmpeg {1} {2} -i {3} {4} {5}, где:

{1} — глобальные параметры
{2} — параметры входного файла
{3} — входящий URL
{4} — параметры выходного файла
{5} — исходящий URL

В частях {2}, {3}, {4}, {5} указывается столько аргументов, сколько нужно. Проще понять формат передачи аргументов на примере:

# ПРЕДУПРЕЖДЕНИЕ: файл по ссылке весит 300 МБ

$ wget -O bunny_1080p_60fps.mp4 http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_60fps_normal.mp4

$ ffmpeg \
-y \ # глобальные параметры
-c: libfdk_aac -c: v libx264 \ # параметры ввода
-i bunny_1080p_60fps.mp4 \ # входной URL
-c: v libvpx-vp9 -c: параметры вывода libvorbis \ #
bunny_1080p_60fps_vp9.webm # выходной URL


Эта команда берет входящий mp4-файл, содержащий два потока (аудио, закодированный с помощью кодека aac, и видео, закодированный с использованием кодека h264), и преобразует его в webm, изменяя также кодеки аудио и видео.

Если упростить приведенную выше команду, то следует учесть, что FFmpeg примет значения по умолчанию вместо Вас. Например, если просто набрать

ffmpeg -i input.avi output.mp4


то, какой аудио/видео кодек он использует для создания output.mp4?

Вернер Робица написал руководство по чтению/исполнению, посвященное кодированию и редактированию с помощью FFmpeg.

Основные операции над видео↑


При работе с аудио/видео мы обычно выполняем ряд задач связанных с мультимедиа.

Транскодирование (перекодирование)↑

ufflcrik3ha4z6tt5axevtdfv9u.png


Что это? Процесс преобразования потокового или аудио или видео (или и то и другое одновременно) из одного кодека в другой. Формат файла (контейнер) при этом не меняется.

Для чего? Бывает, что некоторые устройства (телевизоры, смартфоны, консоли и т. д.) не поддерживают формат аудио/видео X, но поддерживают формат аудио/видео Y. Или же более новые кодеки предпочтительнее, поскольку обеспечивают лучшую степень сжатия.

Как? Преобразуем, к примеру, видео H264 (AVC) в H265 (HEVC):

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-c:v libx265 \
bunny_1080p_60fps_h265.mp4


Трансмультиплексирование↑


esvpgmlkjcj38alzp2sbrdckpne.png


Что это? Преобразование из одного формата (контейнера) в другой.

Для чего? Бывает, что некоторые устройства (телевизоры, смартфоны, консоли и т. д.) не поддерживают формат файла X, но поддерживают формат файла Y. Или же более новые контейнеры, в отличие от устаревших, предоставляют современные требуемые функции.

Как? Конвертируем a mp4 в webm:

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-c copy \ # just saying to ffmpeg to skip encoding
bunny_1080p_60fps.webm


Трансрейтинг↑


evhxcp7a6y5fkjjq9s53hyfs3iq.png


Что это? Изменение скорости передачи данных или создание другого представления.

Для чего? Пользователь может смотреть Ваше видео как в сети 2G на маломощном смартфоне, так и через оптоволоконную интернет-связь на 4K-телевизоре. Поэтому следует предлагать более одного вариант воспроизведения одного и того же видео с разными скоростями передачи данных.

Как? производит воспроизведение с битрейтом между 3856K и 2000K.

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-minrate 964K -maxrate 3856K -bufsize 2000K \
bunny_1080p_60fps_transrating_964_3856.mp4


Обычно трансрейтинг осуществляется в связке с перекалибровкой. Вернер Робица написал еще одну обязательную для ознакомления статью о контроле скорости FFmpeg.

Трансайзинг (перекалибровка)↑


gjc24kcqli3iwwdkzbb70pbue-k.png


Что это? Изменение разрешающей способности. Как сказано выше, транссайзинг часто проводится одновременно с трансрейтингом.

Для чего? По тем же причинам, что и с трансрейтингом.

Как? Уменьшим разрешение 1080 до 480:

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-vf scale=480:-1 \
bunny_1080p_60fps_transsizing_480.mp4


Бонус: адаптивный стриминг↑


3zazlgpe3wjx3zvvhapi0z3ur68.png


Что это? Создание множества разрешений (битрейтов) и разбиение медиа на части и их передача по протоколу http.

Для чего? Ради обеспечения гибкого мультимедиа, которое можно просматривать хоть на бюджетном смартфоне хоть на 4K-плазме, чтобы можно было легко масштабировать и развертывать (но это может добавить задержку).

Как? Создадим адаптивный WebM с использованием DASH:

# video streams
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 160x90 -b:v 250k -keyint_min 150 -g 150 -an -f webm -dash 1 video_160x90_250k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 320x180 -b:v 500k -keyint_min 150 -g 150 -an -f webm -dash 1 video_320x180_500k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 640x360 -b:v 750k -keyint_min 150 -g 150 -an -f webm -dash 1 video_640x360_750k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 640x360 -b:v 1000k -keyint_min 150 -g 150 -an -f webm -dash 1 video_640x360_1000k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 1280x720 -b:v 1500k -keyint_min 150 -g 150 -an -f webm -dash 1 video_1280x720_1500k.webm

# audio streams
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:a libvorbis -b:a 128k -vn -f webm -dash 1 audio_128k.webm

# the DASH manifest
$ ffmpeg \
 -f webm_dash_manifest -i video_160x90_250k.webm \
 -f webm_dash_manifest -i video_320x180_500k.webm \
 -f webm_dash_manifest -i video_640x360_750k.webm \
 -f webm_dash_manifest -i video_640x360_1000k.webm \
 -f webm_dash_manifest -i video_1280x720_500k.webm \
 -f webm_dash_manifest -i audio_128k.webm \
 -c copy -map 0 -map 1 -map 2 -map 3 -map 4 -map 5 \
 -f webm_dash_manifest \
 -adaptation_sets "id=0,streams=0,1,2,3,4 id=1,streams=5" \
 manifest.mpd


P.S.: я утащил этот пример из инструкции по воспроизведению Adaptive WebM с использованием DASH.

Выходя за рамки↑


Несть числа другим применениям FFmpeg. Я использую его вместе с iMovie для создания/правки некоторых видео для YouTube. И Вам, безусловно, использовать его профессионально тоже ничего не препятствует.

Тернистый путь изучения FFmpeg libav↑

Разве время от времени не поразительно то, что воспринимается через слух и зрение?

Биолог Дэвид Роберт Джонс

FFmpeg крайне полезен как инструмент в виде командной строки для выполнения важных операций с мультимедийными файлами. Может и в программах его тоже получится использовать?

FFmpeg состоит из нескольких библиотек, которые можно интегрировать в наши собственные программы. Обычно, при установке FFmpeg, автоматически устанавливаются все эти библиотеки. Я буду ссылаться на набор этих библиотек как FFmpeg libav.

Название раздела является данью уважения серии Зеда Шоу «Тернистый путь обучения […]», в частности его книге «Тернистый путь обучения языку C».

Глава 0 — простенький «Hello World»↑


В нашем Hello World на самом деле не будет приветствовать мир на языке консоли. Вместо этого распечатаем следующую информацию о видео: формат (контейнер), продолжительность, разрешение, аудиоканалы и, напоследок, расшифруем некоторые кадры и сохраним их как файлы изображений.

Архитектура FFmpeg libav↑


Но прежде чем начнём писать код, давайте посмотрим, как вообще работает архитектура FFmpeg libav и как ее компоненты взаимодействуют с другими.

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

ej1deksvwwqdctppaujla5v5tog.png


Сначала медиафайл загружается в компонент по имени AVFormatContext (контейнер видео также является форматом). На самом деле он не полностью загружает весь файл: часто читается только заголовок.

Как только загрузили минимальный заголовок нашего контейнера, можно получить доступ к его потокам (их можно представить как элементарные аудио- и видео-данные). Каждый поток будет доступен в компоненте AVStream.

Предположим, наше видео имеет два потока: аудио, закодированное с помощью кодека AAC, и видео, закодированное с помощью кодека H264 (AVC). Из каждого потока можем извлечь фрагменты данных, называемые пакетами, которые загружаются в компоненты, называемые AVPacket.

Данные внутри пакетов по-прежнему кодируются (сжимаются), и для декодирования пакетов нам необходимо передать их конкретному AVCodec.

AVCodec декодирует их в AVFrame, в результате чего этот компонент выдает нам несжатый кадр. Отметим, что терминология и процесс одинаковы как для аудио- так и видео-потока.

Требования↑


Так как иногда возникают проблемы при компиляции или запуске примеров, мы будем использовать Docker в качестве среды разработки/выполнения. Также будем использовать видео с большим кроликом, поэтому, если у вас его нет на локальном компьютере, просто проведите в консоли команду make fetch_small_bunny_video.

Собственно, код↑


TLDR; покажи мне пример выполянемого кода, бро:

$ make run_hello


Мы опустим некоторые детали, но не волнуйтесь: исходный код доступен на github.

Мы собираемся выделить память для компонента AVFormatContext, который будет содержать информацию о формате (контейнере).

AVFormatContext *pFormatContext = avformat_alloc_context();


Теперь мы собираемся открыть файл, прочитать его заголовок и заполнить AVFormatContext минимальной информацией о формате (обратите внимание, что обычно кодеки не открываются). Для этого используется функция avformat_open_input. Он ожидает AVFormatContext, имя файла и два необязательных аргумента: AVInputFormat (если вы передадите NULL, FFmpeg определит формат) и AVDictionary (которые являются опциями демультиплексора).

avformat_open_input(&pFormatContext, filename, NULL, NULL);


Также можно напечатать название формата и длительность медиа:

printf("Format %s, duration %lld us", pFormatContext->iformat->long_name, pFormatContext->duration);


Чтобы получить доступ к потокам, нам нужно прочитать данные с носителя. Это делает функция avformat_find_stream_info. Теперь pFormatContext→ nb_streams будет содержать количество потоков, а pFormatContext→ streams[i] даст нам i-й по счёту поток (AVStream).

avformat_find_stream_info(pFormatContext,  NULL);


Пройдемся в цикле по всем потокам:

for(int i = 0; i < pFormatContext->nb_streams; i++) {
  //
}


Для каждого потока мы собираемся сохранить AVCodecParameters, описывающий свойства кодека, используемого i-м потоком:

AVCodecParameters *pLocalCodecParameters = pFormatContext->streams[i]->codecpar;

Используя свойства кодеков можем найти соответствующий, запрашивая функцию avcodec_find_decoder, также можем найти зарегистрированный декодер для идентификатора кодека и вернуть AVCodec — компонент, который знает, как кодировать и декодировать поток:

AVCodec *pLocalCodec = avcodec_find_decoder(pLocalCodecParameters->codec_id);


Теперь мы можем распечатать информацию о кодеках:

// specific for video and audio
if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_VIDEO) {
  printf("Video Codec: resolution %d x %d", pLocalCodecParameters->width, pLocalCodecParameters->height);
} else if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_AUDIO) {
  printf("Audio Codec: %d channels, sample rate %d", pLocalCodecParameters->channels, pLocalCodecParameters->sample_rate);
}
// general
printf("\tCodec %s ID %d bit_rate %lld", pLocalCodec->long_name, pLocalCodec->id, pCodecParameters->bit_rate);


С помощью кодека выделяем память для AVCodecContext, который будет содержать контекст для нашего процесса декодирования/кодирования. Но затем нужно заполнить этот контекст кодека параметрами CODEC — мы делаем это с помощью avcodec_parameters_to_context.

После того, как мы заполнили контекст кодека, необходимо открыть кодек. Вызываем функцию avcodec_open2 и затем можем ее использовать:

AVCodecContext *pCodecContext = avcodec_alloc_context3(pCodec);
avcodec_parameters_to_context(pCodecContext, pCodecParameters);
avcodec_open2(pCodecContext, pCodec, NULL);


Теперь мы собираемся прочитать пакеты из потока и декодировать их в кадры, но сначала нам нужно выделить память для обоих компонентов (AVPacket и AVFrame).

AVPacket *pPacket = av_packet_alloc();
AVFrame *pFrame = av_frame_alloc();


Давайте скормим наши пакеты из потоков функции av_read_frame, пока у нее есть пакеты:

while(av_read_frame(pFormatContext, pPacket) >= 0) {
  //...
}


Теперь отправим пакет необработанных данных (сжатый кадр) в декодер через контекст кодека, используя функцию avcodec_send_packet:

avcodec_send_packet(pCodecContext, pPacket);


И давайте получим кадр необработанных данных (несжатый кадр) от декодера через тот же контекст кодека, используя функцию avcodec_receive_frame:

avcodec_receive_frame(pCodecContext, pFrame);


Мы можем напечатать номер кадра, PTS, DTS, тип кадра и т.д.:

printf(
    "Frame %c (%d) pts %d dts %d key_frame %d [coded_picture_number %d, display_picture_number %d]",
    av_get_picture_type_char(pFrame->pict_type),
    pCodecContext->frame_number,
    pFrame->pts,
    pFrame->pkt_dts,
    pFrame->key_frame,
    pFrame->coded_picture_number,
    pFrame->display_picture_number
);


И напоследок, можем сохранить наш декодированный кадр в простое серое изображение. Процесс очень прост: мы будем использовать pFrame→data, где индекс связан с цветовыми пространствами Y, Cb и Cr. Просто выбираем 0 (Y), чтобы сохранить наше серое изображени:

save_gray_frame(pFrame->data[0], pFrame->linesize[0], pFrame->width, pFrame->height, frame_filename);

static void save_gray_frame(unsigned char *buf, int wrap, int xsize, int ysize, char *filename)
{
    FILE *f;
    int i;
    f = fopen(filename,"w");
    // writing the minimal required header for a pgm file format
    // portable graymap format -> https://en.wikipedia.org/wiki/Netpbm_format#PGM_example
    fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255);

    // writing line by line
    for (i = 0; i < ysize; i++)
        fwrite(buf + i * wrap, 1, xsize, f);
    fclose(f);
}


И вуаля! Теперь у нас есть полутоновое изображение размером 2Мб:

gktfi9xb3ylqmmzu927uc7aon9w.png


Глава 1 — синхронизация аудио и видео↑

Быть в игре — это когда юный JS-разработчик пишет новый MSE-видеоплеер.

Прежде чем перейдем написанию кода транскодирования, давайте поговорим о синхронизации или о том, как видеоплеер узнаёт правильное время для воспроизведения кадра.

В предыдущем примере мы сохранили несколько кадров:
gjyz7h4agcr6n2hdkmt9w9ko5ho.pngisclyy5ckz1_4w2mepv4msr2dba.png7ztrmukulalxo5qihbly0cpm_5w.pngu3q8pqb2qxhroy1ycatplyeveco.pnguulhihectxop1sphmmss_ipvfpc.png9xssbmfyxzra5frtcob9dirkxt8.png

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

Поэтому нам нужно определить некую логику для плавного воспроизведения каждого кадра. В этом отношении каждый кадр имеет временнýю метку представления (PTS — от presentation timestamp), которая представляет собой увеличивающееся число, учитываемое в переменной timebase, которая представляет собой рациональное число (где знаменатель известен как временно́й масштаб — timescale), делимое на частоту кадров (fps).

Проще понять на примерах. Давайте смоделируем некоторые сценарии.

Для fps = 60/1 и timebase = 1/60000 каждый PTS будет увеличивать timescale / fps = 1000, поэтому реальное время PTS для каждого кадра может быть (при условии, что оно начинается с 0):

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 1000, PTS_TIME = PTS * timebase = 0.016
frame=2, PTS = 2000, PTS_TIME = PTS * timebase = 0.033


Почти по тому же сценарию, но с timescale, равной 1/60:

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 1, PTS_TIME = PTS * timebase = 0.016
frame=2, PTS = 2, PTS_TIME = PTS * timebase = 0.033
frame=3, PTS = 3, PTS_TIME = PTS * timebase = 0.050


Для fps = 25/1 и timebase = 1/75 каждая PTS будет увеличивать timescale / fps = 3, и время PTS может быть:

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 3, PTS_TIME = PTS * timebase = 0.04
frame=2, PTS = 6, PTS_TIME = PTS * timebase = 0.08
frame=3, PTS = 9, PTS_TIME = PTS * timebase = 0.12
...
frame=24, PTS = 72, PTS_TIME = PTS * timebase = 0.96
...
frame=4064, PTS = 12192, PTS_TIME = PTS * timebase = 162.56


Теперь с pts_time мы можем найти способ визуализировать это синхронизированным со звуком pts_time или с системными часами. FFmpeg libav предоставляет эту информацию через свой API:

fps = AVStream->avg_frame_rate
tbr = AVStream->r_frame_rate
tbn = AVStream->time_base

Просто из любопытства, сохраненные нами кадры были отправлены в порядке DTS (кадры: 1, 6, 4, 2, 3, 5), но воспроизведены в порядке PTS (кадры: 1, 2, 3, 4, 5). Также обратите внимание, насколько дешевле обходятся B-кадры по сравнению с P или I-кадрами:

LOG: AVStream->r_frame_rate 60/1
LOG: AVStream->time_base 1/60000
...
LOG: Frame 1 (type=I, size=153797 bytes) pts 6000 key_frame 1 [DTS 0]
LOG: Frame 2 (type=B, size=8117 bytes) pts 7000 key_frame 0 [DTS 3]
LOG: Frame 3 (type=B, size=8226 bytes) pts 8000 key_frame 0 [DTS 4]
LOG: Frame 4 (type=B, size=17699 bytes) pts 9000 key_frame 0 [DTS 2]
LOG: Frame 5 (type=B, size=6253 bytes) pts 10000 key_frame 0 [DTS 5]
LOG: Frame 6 (type=P, size=34992 bytes) pts 11000 key_frame 0 [DTS 1]


Глава 2 — ремультиплексирование↑


Ремультиплексирование (перекомпоновка, remuxing) — переход от одного формата (контейнера) к другому. Например, мы можем без особого труда заменить видео MPEG-4 на MPEG-TS с помощью FFmpeg:

ffmpeg input.mp4 -c copy output.ts


MP4-файл будет демультиплексирован, при этом файл не будет декодирован или кодирован (-c copy), и, в конце концов, на выходет получим mpegts-файл. Если не указывать формат -f, ffmpeg попытается угадать его на основании расширения файла.

Общее использование FFmpeg или libav следует такому шаблону/архитектуре или рабочему процессу:

  • уровень протокола — принятие входных данных (например, файл, но это может быть также загрузка по rtmp или HTTP)
  • уровень формата — демультиплексирование контента, открытие, в основном, метаданных и потоков
  • уровень кодека — декодирование данных для сжатых потоков при необходимости
  • уровень пикселей — также возможно применение некоторых фильтров к необработанным кадрам (например, изменение размера), необязательный пункт
  • …, а затем он происходит движение в обратном направлении:
  • уровень кодека — кодирвание (или перекодирование или даже транскодирование) необработанных кадров
  • уровень формата — мультиплексирование (или ремультиплексирование) необработанных потоков (сжатых данных)
  • уровень протокола — и, наконец, мультиплексированные данные отправляются на выход (в другой файл или, возможно, на сетевой удаленный сервер)


u6x4g3oijeetm1lpdvxnjo9hwls.png
(Этот график сильно вдохновлен работами Leixiaohua и Slhck)

Теперь давайте создадим пример с использованием libav, чтобы обеспечить тот же эффект, что и при выполнении такой команды:

ffmpeg input.mp4 -c copy output.ts


Мы собираемся читать из ввода (input_format_context) и изменять его на другой вывод (output_format_context):

AVFormatContext *input_format_context = NULL;
AVFormatContext *output_format_context = NULL;


Обычно начинаем с того, что выделяем память и открываем формат ввода. Для этого конкретного случая мы собираемся открыть входной файл и выделить память для выходного файла:

if ((ret = avformat_open_input(&input_format_context, in_filename, NULL, NULL)) < 0) {
  fprintf(stderr, "Could not open input file '%s'", in_filename);
  goto end;
}
if ((ret = avformat_find_stream_info(input_format_context, NULL)) < 0) {
  fprintf(stderr, "Failed to retrieve input stream information");
  goto end;
}

avformat_alloc_output_context2(&output_format_context, NULL, NULL, out_filename);
if (!output_format_context) {
  fprintf(stderr, "Could not create output context\n");
  ret = AVERROR_UNKNOWN;
  goto end;
}


Будем ремультиплексировать только потоки видео, аудио и субтитров. Поэтому фиксируем, какие потоки будем использовать, в массив индексов:

number_of_streams = input_format_context->nb_streams;
streams_list = av_mallocz_array(number_of_streams, sizeof(*streams_list));


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

for (i = 0; i < input_format_context->nb_streams; i++) {
  AVStream *out_stream;
  AVStream *in_stream = input_format_context->streams[i];
  AVCodecParameters *in_codecpar = in_stream->codecpar;
  if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
      in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
      in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
    streams_list[i] = -1;
    continue;
  }
  streams_list[i] = stream_index++;
  out_stream = avformat_new_stream(output_format_context, NULL);
  if (!out_stream) {
    fprintf(stderr, "Failed allocating output stream\n");
    ret = AVERROR_UNKNOWN;
    goto end;
  }
  ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
  if (ret < 0) {
    fprintf(stderr, "Failed to copy codec parameters\n");
    goto end;
  }
}


Теперь создаём выходной файл:

if (!(output_format_context->oformat->flags & AVFMT_NOFILE)) {
  ret = avio_open(&output_format_context->pb, out_filename, AVIO_FLAG_WRITE);
  if (ret < 0) {
    fprintf(stderr, "Could not open output file '%s'", out_filename);
    goto end;
  }
}

ret = avformat_write_header(output_format_context, NULL);
if (ret < 0) {
  fprintf(stderr, "Error occurred when opening output file\n");
  goto end;
}


После этого можно копировать потоки, пакет за пакетом, из нашего ввода в наши выходные потоки. Это происходит в цикле, пока есть пакеты (av_read_frame), для каждого пакета нужно пересчитать PTS и DTS, чтобы наконец записать его (av_interleaved_write_frame) в наш контекст выходного формата.

while (1) {
  AVStream *in_stream, *out_stream;
  ret = av_read_frame(input_format_context, &packet);
  if (ret < 0)
    break;
  in_stream  = input_format_context->streams[packet.stream_index];
  if (packet.stream_index >= number_of_streams || streams_list[packet.stream_index] < 0) {
    av_packet_unref(&packet);
    continue;
  }
  packet.stream_index = streams_list[packet.stream_index];
  out_stream = output_format_context->streams[packet.stream_index];
  /* copy packet */
  packet.pts = av_rescale_q_rnd(packet.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
  packet.dts = av_rescale_q_rnd(packet.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
  packet.duration = av_rescale_q(packet.duration, in_stream->time_base, out_stream->time_base);
  // https://ffmpeg.org/doxygen/trunk/structAVPacket.html#ab5793d8195cf4789dfb3913b7a693903
  packet.pos = -1;

  //https://ffmpeg.org/doxygen/trunk/group__lavf__encoding.html#ga37352ed2c63493c38219d935e71db6c1
  ret = av_interleaved_write_frame(output_format_context, &packet);
  if (ret < 0) {
    fprintf(stderr, "Error muxing packet\n");
    break;
  }
  av_packet_unref(&packet);
}


Для завершения нам нужно записать трейлер потока в выходной медиафайл с помощью функции av_write_trailer:

av_write_trailer(output_format_context);


Теперь мы готовы протестировать код. И первым тестом будет преобразование формата (видео-контейнера) из MP4 в видеофайл MPEG-TS. В основном мы создаем командную строку ffmpeg input.mp4 -c для копирования output.ts с помощью libav.

make run_remuxing_ts


Это работает! Не верите?! Проверьте с помощью ffprobe:

ffprobe -i remuxed_small_bunny_1080p_60fps.ts

Input #0, mpegts, from 'remuxed_small_bunny_1080p_60fps.ts':
  Duration: 00:00:10.03, start: 0.000000, bitrate: 2751 kb/s
  Program 1
    Metadata:
      service_name    : Service01
      service_provider: FFmpeg
    Stream #0:0[0x100]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 60 fps, 60 tbr, 90k tbn, 120 tbc
    Stream #0:1[0x101]: Audio: ac3 ([129][0][0][0] / 0x0081), 48000 Hz, 5.1(side), fltp, 320 kb/s


Подводя итог тому, что мы сделали, теперь можем вернуться к нашей первоначальной идее о том, как работает libav. Но мы пропустили часть кодека, что отображено на схеме.

_ivzywrkd4le_hfwqyf-7hhrcb8.png


Прежде чем закончим эту главу, хотелось бы показать такую важную часть процесса ремультиплексрования, где можно передавать параметры мультиплексору. Допустим, надо предоставить формат MPEG-DASH, поэтому нужно использовать фрагментированный mp4 (иногда называемый fmp4) вместо MPEG-TS или обычного MPEG-4.

С помощью командной строки это легко:

ffmpeg -i non_fragmented.mp4 -movflags frag_keyframe+empty_moov+default_base_moof fragmented.mp4


Почти так же просто это и в libav-версии, просто передаём опции при записи выходного заголовка, непосредственно перед копированием пакетов:

AVDictionary* opts = NULL;
av_dict_set(&opts, "movflags", "frag_keyframe+empty_moov+default_base_moof", 0);
ret = avformat_write_header(output_format_context, &opts);

Теперь можем сгенерировать этот фрагментированный mp4-файл:

make run_remuxing_fragmented_mp4


Чтобы убедиться, что тут всё по-честному, Вы можете использовать удивительный сайт-инструмент gpac/mp4box.js или сайт http://mp4parser.com/, дабы увидеть различия — сначала загрузите mp4.

x5hw9uxcdgvl36yvns8hkquzlca.png

nzx-lgxsv_fsqphsci0i8gabhfi.gif

Как видно, он имеет один неделимый блок mdat — это место, где находятся видео и аудио кадры. Теперь загрузите фрагментированный mp4, чтобы увидеть, как он расширяет блоки mdat:

pubvmm5ogancxewrdxfgdth1d34.png

nzx-lgxsv_fsqphsci0i8gabhfi.gif

Глава 3 — транскодирование↑


TLDR; покажи мне код и исполнение:

$ make run_transcoding

Мы пропустим некоторые детали, но не волнуйтесь: исходный код доступен на github.

В этой главе создадим минималистичннй транскодер, написанный на C, который может конвертировать видео из H264 в H265 с использованием библиотек FFmpeg libav, в частности libavcodec, libavformat и libavutil.

pwzjys-eok7ggnpkztlv7n60njm.png


AVFormatContext — это абстракция для формата медиа-файла, т.е. для контейнера (MKV, MP4, Webm, TS)
AVStream представляет каждый тип данных для данного формата (например: аудио, видео, субтитры, метаданные)
AVPacket — это фрагмент сжатых данных, полученных из AVStream, которые могут быть декодированы с помощью AVCodec (например: av1, h264, vp9, hevc), генерирующих необработанные данные, называемые AVFrame.

Трансмультиплексирование↑


Начнем с простого преобразования, затем загрузим входной файл.

// Allocate an AVFormatContext
avfc = avformat_alloc_context();
// Open an input stream and read the header.
avformat_open_input(avfc, in_filename, NULL, NULL);
// Read packets of a media file to get stream information.
avformat_find_stream_info(avfc, NULL);


Теперь настроим декодер. AVFormatContext предоставит нам доступ ко всем компонентам AVStream, и для каждого из которых можем получить их AVCodec и создать конкретный AVCodecContext. И, наконец, можем открыть данный кодек, чтобы перейти к процессу декодирования.

AVCodecContext содержит данные о конфигурации мультимедиа, такие как скорость передачи данных, частота кадров, частота дискретизации, каналы, высота и многие другие.

for(int i = 0; i < avfc->nb_streams; i++) {
  AVStream *avs = avfc->streams[i];
  AVCodec *avc = avcodec_find_decoder(avs->codecpar->codec_id);
  AVCodecContext *avcc = avcodec_alloc_context3(*avc);
  avcodec_parameters_to_context(*avcc, avs->codecpar);
  avcodec_open2(*avcc, *avc, NULL);
}


Также нужно подготовить выходной медиа-файл для преобразования. Сначала выделим память для выходного AVFormatContext. Создадим каждый поток в выходном формате. Чтобы правильно упаковать поток, копируем параметры кодека из декодера.

Устанавливаем флаг AV_CODEC_FLAG_GLOBAL_HEADER, который сообщает кодировщику, что он может использовать глобальные заголовки, и, наконец, открываем выходной файл для записи и сохраняем заголовки:

avformat_alloc_output_context2(&encoder_avfc, NULL, NULL, out_filename);

AVStream *avs = avformat_new_stream(encoder_avfc, NULL);
avcodec_parameters_copy(avs->codecpar, decoder_avs->codecpar);

if (encoder_avfc->oformat->flags & AVFMT_GLOBALHEADER)
  encoder_avfc->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

avio_open(&encoder_avfc->pb, encoder->filename, AVIO_FLAG_WRITE);
avformat_write_header(encoder->avfc, &muxer_opts);


Получаем AVPacket от декодера, корректируем метки времени и записываем пакет правильно в выходной файл. Несмотря на то, что функция av_interleaved_write_frame сообщает »write frame», сохраняем пакет. Заканчиваем процесс перестановки, записывая трейлер потока в файл.

AVFrame *input_frame = av_frame_alloc();
AVPacket *input_packet = av_packet_alloc();

while(av_read_frame(decoder_avfc, input_packet) >= 0) {
  av_packet_rescale_ts(input_packet, decoder_video_avs->time_base, encoder_video_avs->time_base);
  av_interleaved_write_frame(*avfc, input_packet) < 0));
}

av_write_trailer(encoder_avfc);


Транскодирование↑


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

После того, как подготовлен декодер, но перед тем, как организовать выходной медиафайл, настроим кодировщик.

  • Создаём видео AVStream в кодировщике avformat_new_stream.
  • Используем AVCodec с именем libx265, avcodec_find_encoder_by_name.
  • Создаём AVCodecContext на основе созданного кодека avcodec_alloc_context3.
  • Устанавливаем основные атрибуты для сеанса транскодирования и…
  • … открываем кодек и копируем параметры из контекста в поток (avcodec_open2 и avcodec_parameters_from_context).
AVRational input_framerate = av_guess_frame_rate(decoder_avfc, decoder_video_avs, NULL);
AVStream *video_avs = avformat_new_stream(encoder_avfc, NULL);

char *codec_name = "libx265";
char *codec_priv_key = "x265-params";
// we're going to use internal options for the x265
// it disables the scene change detection and fix then
// GOP on 60 frames.
char *codec_priv_value = "keyint=60:min-keyint=60:scenecut=0";

AVCodec *video_avc = avcodec_find_encoder_by_name(codec_name);
AVCodecContext *video_avcc = avcodec_alloc_context3(video_avc);
// encoder codec params
av_opt_set(sc->video_avcc->priv_data, codec_priv_key, codec_priv_value, 0);
video_avcc->height = decoder_ctx->height;
video_avcc->width = decoder_ctx->width;
video_avcc->pix_fmt = video_avc->pix_fmts[0];
// control rate
video_avcc->bit_rate = 2 * 1000 * 1000;
video_avcc->rc_buffer_size = 4 * 1000 * 1000;
video_avcc->rc_max_rate = 2 * 1000 * 1000;
video_avcc->rc_min_rate = 2.5 * 1000 * 1000;
// time base
video_avcc->time_base = av_inv_q(input_framerate);
video_avs->time_base = sc->video_avcc->time_base;

avcodec_open2(sc->video_avcc, sc->video_avc, NULL);
avcodec_parameters_from_context(sc->video_avs->codecpar, sc->video_avcc);


Необходимо расширить цикл декодирования для транскодирования видеопотока:

  • Отправляем пустой AVPacket декодеру (avcodec_send_packet).
  • Получаем несжатый AVFrame (avcodec_receive_frame).
  • Начинаем перекодирование необработанного кадра.
  • Отправляем необработанный кадр (avcodec_send_frame).
  • Получаем сжатие, основанное на нашем кодеке AVPacket (avcodec_receive_packet).
  • Установливаем отметку времени (av_packet_rescale_ts).
  • Записываем в выходной файл (av_interleaved_write_frame).
AVFrame *input_frame = av_frame_alloc();
AVPacket *input_packet = av_packet_alloc();

while (av_read_frame(decoder_avfc, input_packet) >= 0)
{
  int response = avcodec_send_packet(decoder_video_avcc, input_packet);
  while (response >= 0) {
    response = avcodec_receive_frame(decoder_video_avcc, input_frame);
    if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) {
      break;
    } else if (response < 0) {
      return response;
    }
    if (response >= 0) {
      encode(encoder_avfc, decoder_video_avs, encoder_video_avs, decoder_video_avcc, input_packet->stream_index);
    }
    av_frame_unref(input_frame);
  }
  av_packet_unref(input_packet);
}
av_write_trailer(encoder_avfc);

// used function
int encode(AVFormatContext *avfc, AVStream *dec_video_avs, AVStream *enc_video_avs, AVCodecContext video_avcc int index) {
  AVPacket *output_packet = av_packet_alloc();
  int response = avcodec_send_frame(video_avcc, input_frame);

  while (response >= 0) {
    response = avcodec_receive_packet(video_avcc, output_packet);
    if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) {
      break;
    } else if (response < 0) {
      return -1;
    }

    output_packet->stream_index = index;
    output_packet->duration = enc_video_avs->time_base.den / enc_video_avs->time_base.num / dec_video_avs->avg_frame_rate.num * dec_video_avs->avg_frame_rate.den;

    av_packet_rescale_ts(output_packet, dec_video_avs->time_base, enc_video_avs->time_base);
    response = av_interleaved_write_frame(avfc, output_packet);
  }
  av_packet_unref(output_packet);
  av_packet_free(&output_packet);
  return 0;
}


Мы преобразовали поток мультимедиа из h264 в h265. Как и ожидалось, версия медиа-файла h265 меньше, чем h264, при этом у программы широкие возможности:

  /*
   * H264 -> H265
   * Audio -> remuxed (untouched)
   * MP4 - MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx265";
  sp.codec_priv_key = "x265-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> remuxed (untouched)
   * MP4 - MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> remuxed (untouched)
   * MP4 - fragmented MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";
  sp.muxer_opt_key = "movflags";
  sp.muxer_opt_value = "frag_keyframe+empty_moov+default_
    
            

© Habrahabr.ru