Руководство по FFmpeg libav. Ремультиплексирование

Добавлено26 февраля 2022 в 23:26

Продолжение серии статей об основах работы с 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);
  • уровень формата – демультиплексирует контент, раскрывая метаданные и его потоки;
  • уровень кодека – декодирует потоки сжатых данных (необязателен);
  • уровень пикселей – также может применять какие-либо фильтры к необработанным кадрам (например, изменение размера) (необязателен);
  • а потом идет обратный путь
  • уровень кодека – кодирует (или перекодирует) необработанные кадры (необязателен);
  • уровень формата – мультиплексирует (или ремультиплексирует) необработанные потоки (сжатые данные);
  • уровень протокола – и, наконец, мультиплексированные данные отправляются на выход (другой файл или, возможно, удаленный сетевой сервер).
рабочий процесс ffmpeg libav
Рабочий процесс ffmpeg libav. Этот график сильно вдохновлен работами Leixiaohua и Slhck.

Теперь давайте напишем код примера, используя 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, и показать, что мы пропустили часть кодека.

компоненты 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.

обычный mp4

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

фрагментированный mp4

Теги

FFmpeglibavОбработка аудиоОбработка видеоПрограммированиеРемультиплексирование