Руководство по 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Обработка аудиоОбработка видеоПрограммированиеРемультиплексирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.