Руководство по FFmpeg libav. Ремультиплексирование
Продолжение серии статей об основах работы с FFmpeg libav.
Ремультиплексирование – это процесс перехода от одного формата (контейнера) к другому, например, с помощью FFmpeg мы можем без особых усилий преобразовать видео MPEG-4 в MPEG-TS:
ffmpeg input.mp4 -c copy output.ts
С помощью этой команды FFmpeg демультиплексирует mp4, но не декодирует и не кодирует его (-c copy
) и мультиплексирует его в файл mpegts. Если вы не укажете формат -f
, ffmpeg попытается угадать его на основе расширения файла.
Базовое использование FFmpeg или libav следует следующему шаблону/архитектуре или процессу:
- уровень протокола – принимает входные данные (например, файл, но это также может быть ввод RTMP или HTTP);
- уровень формата – демультиплексирует контент, раскрывая метаданные и его потоки;
- уровень кодека – декодирует потоки сжатых данных (необязателен);
- уровень пикселей – также может применять какие-либо фильтры к необработанным кадрам (например, изменение размера) (необязателен);
- а потом идет обратный путь
- уровень кодека – кодирует (или перекодирует) необработанные кадры (необязателен);
- уровень формата – мультиплексирует (или ремультиплексирует) необработанные потоки (сжатые данные);
- уровень протокола – и, наконец, мультиплексированные данные отправляются на выход (другой файл или, возможно, удаленный сетевой сервер).
Теперь давайте напишем код примера, используя libav, чтобы обеспечить тот же эффект, что и в ffmpeg input.mp4 -c copy output.ts
.
// на основе https://ffmpeg.org/doxygen/trunk/remuxing_8c-example.html
#include <libavutil/timestamp.h>
#include <libavformat/avformat.h>
int main(int argc, char **argv)
{
AVFormatContext *input_format_context = NULL, *output_format_context = NULL;
AVPacket packet;
const char *in_filename, *out_filename;
int ret, i;
int stream_index = 0;
int *streams_list = NULL;
int number_of_streams = 0;
int fragmented_mp4_options = 0;
if (argc < 3)
{
printf("You need to pass at least two parameters.\n");
return -1;
}
else if (argc == 4)
{
fragmented_mp4_options = 1;
}
in_filename = argv[1];
out_filename = argv[2];
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));
if (!streams_list)
{
ret = AVERROR(ENOMEM);
goto end;
}
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;
}
}
// https://ffmpeg.org/doxygen/trunk/group__lavf__misc.html#gae2645941f2dc779c307eb6314fd39f10
av_dump_format(output_format_context, 0, out_filename, 1);
// если это файл (мы поговорим об этом позже), запись на диск (FLAG_WRITE)
// но в основном это способ сохранить файл в буфер, чтобы вы могли его сохранить
// где угодно.
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;
}
}
AVDictionary* opts = NULL;
if (fragmented_mp4_options)
{
// https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API/Transcoding_assets_for_MSE
av_dict_set(&opts, "movflags", "frag_keyframe+empty_moov+default_base_moof", 0);
}
// https://ffmpeg.org/doxygen/trunk/group__lavf__encoding.html#ga18b7b10bb5b94c4842de18166bc677cb
ret = avformat_write_header(output_format_context, &opts);
if (ret < 0)
{
fprintf(stderr, "Error occurred when opening output file\n");
goto end;
}
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];
/* копирование пакета */
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);
}
//https://ffmpeg.org/doxygen/trunk/group__lavf__encoding.html#ga7f14007e7dc8f481f054b21614dfec13
av_write_trailer(output_format_context);
end:
avformat_close_input(&input_format_context);
/* close output */
if (output_format_context && !(output_format_context->oformat->flags & AVFMT_NOFILE))
avio_closep(&output_format_context->pb);
avformat_free_context(output_format_context);
av_freep(&streams_list);
if (ret < 0 && ret != AVERROR_EOF)
{
fprintf(stderr, "Error occurred: %s\n", av_err2str(ret));
return 1;
}
return 0;
}
Мы собираемся прочитать файл со входа (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];
/* копирование пакета */
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. Мы сделаем с помощью libav то же самое, что и через командную строку ffmpeg input.mp4 -c copy output.ts
.
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, и показать, что мы пропустили часть кодека.
Прежде чем мы закончим эту главу, я хотел бы показать важную часть процесса ремультиплексирования – вы можете передавать параметры мультиплексору. Допустим, мы хотим доставить формат 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.
Как вы можете видеть, у него один атом/блок mdat
, это место, где находятся видео и аудио кадры. Теперь загрузите фрагментированный mp4, чтобы увидеть, как он раскидывает поля mdat
.