Libav (ffmpeg) копирование декодированных меток времени видео в кодер

я пишу приложение, которое декодирует один видеопоток из входного файла (любой кодек, любой контейнер), выполняет кучу обработки изображений и кодирует результаты в выходной файл (один видеопоток, Quicktime RLE, MOV). Я использую ffmpeg libav 3.1.5 (Windows build на данный момент, но приложение будет кросс-платформенным).

существует соответствие 1: 1 между входным и выходным кадрами, и я хочу, чтобы время кадра на выходе было идентичным входу. Я я, действительно, действительно трудно выполнить это. Поэтому мой общий вопрос:как я надежно (как и во всех случаях входов) устанавливаю время выходного кадра, идентичное входу?

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

#include <cstdio>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}

using namespace std;


struct DecoderStuff {
    AVFormatContext *formatx;
    int nstream;
    AVCodec *codec;
    AVStream *stream;
    AVCodecContext *codecx;
    AVFrame *rawframe;
    AVFrame *rgbframe;
    SwsContext *swsx;
};


struct EncoderStuff {
    AVFormatContext *formatx;
    AVCodec *codec;
    AVStream *stream;
    AVCodecContext *codecx;
};


template <typename T>
static void dump_timebase (const char *what, const T *o) {
    if (o)
        printf("%s timebase: %d/%dn", what, o->time_base.num, o->time_base.den);
    else
        printf("%s timebase: null objectn", what);
}


// reads next frame into d.rawframe and d.rgbframe. returns false on error/eof.
static bool read_frame (DecoderStuff &d) {

    AVPacket packet;
    int err = 0, haveframe = 0;

    // read
    while (!haveframe && err >= 0 && ((err = av_read_frame(d.formatx, &packet)) >= 0)) {
       if (packet.stream_index == d.nstream) {
           err = avcodec_decode_video2(d.codecx, d.rawframe, &haveframe, &packet);
       }
       av_packet_unref(&packet);
    }

    // error output
    if (!haveframe && err != AVERROR_EOF) {
        char buf[500];
        av_strerror(err, buf, sizeof(buf) - 1);
        buf[499] = 0;
        printf("read_frame: %sn", buf);
    }

    // convert to rgb
    if (haveframe) {
        sws_scale(d.swsx, d.rawframe->data, d.rawframe->linesize, 0, d.rawframe->height,
                  d.rgbframe->data, d.rgbframe->linesize);
    }

    return haveframe;

}


// writes an output frame, returns false on error.
static bool write_frame (EncoderStuff &e, AVFrame *inframe) {

    // see note in so post about outframe here
    AVFrame *outframe = av_frame_alloc();
    outframe->format = inframe->format;
    outframe->width = inframe->width;
    outframe->height = inframe->height;
    av_image_alloc(outframe->data, outframe->linesize, outframe->width, outframe->height,
                   AV_PIX_FMT_RGB24, 1);
    //av_frame_copy(outframe, inframe);
    static int count = 0;
    for (int n = 0; n < outframe->width * outframe->height; ++ n) {
        outframe->data[0][n*3+0] = ((n+count) % 100) ? 0 : 255;
        outframe->data[0][n*3+1] = ((n+count) % 100) ? 0 : 255;
        outframe->data[0][n*3+2] = ((n+count) % 100) ? 0 : 255;
    }
    ++ count;

    AVPacket packet;
    av_init_packet(&packet);
    packet.size = 0;
    packet.data = NULL;

    int err, havepacket = 0;
    if ((err = avcodec_encode_video2(e.codecx, &packet, outframe, &havepacket)) >= 0 && havepacket) {
        packet.stream_index = e.stream->index;
        err = av_interleaved_write_frame(e.formatx, &packet);
    }

    if (err < 0) {
        char buf[500];
        av_strerror(err, buf, sizeof(buf) - 1);
        buf[499] = 0;
        printf("write_frame: %sn", buf);
    }

    av_packet_unref(&packet);
    av_freep(&outframe->data[0]);
    av_frame_free(&outframe);

    return err >= 0;

}


int main (int argc, char *argv[]) {

    const char *infile = "wildlife.wmv";
    const char *outfile = "test.mov";
    DecoderStuff d = {};
    EncoderStuff e = {};

    av_register_all();

    // decoder
    avformat_open_input(&d.formatx, infile, NULL, NULL);
    avformat_find_stream_info(d.formatx, NULL);
    d.nstream = av_find_best_stream(d.formatx, AVMEDIA_TYPE_VIDEO, -1, -1, &d.codec, 0);
    d.stream = d.formatx->streams[d.nstream];
    d.codecx = avcodec_alloc_context3(d.codec);
    avcodec_parameters_to_context(d.codecx, d.stream->codecpar);
    avcodec_open2(d.codecx, NULL, NULL);
    d.rawframe = av_frame_alloc();
    d.rgbframe = av_frame_alloc();
    d.rgbframe->format = AV_PIX_FMT_RGB24;
    d.rgbframe->width = d.codecx->width;
    d.rgbframe->height = d.codecx->height;
    av_frame_get_buffer(d.rgbframe, 1);
    d.swsx = sws_getContext(d.codecx->width, d.codecx->height, d.codecx->pix_fmt,
                            d.codecx->width, d.codecx->height, AV_PIX_FMT_RGB24,
                            SWS_POINT, NULL, NULL, NULL);
    //av_dump_format(d.formatx, 0, infile, 0);
    dump_timebase("in stream", d.stream);
    dump_timebase("in stream:codec", d.stream->codec); // note: deprecated
    dump_timebase("in codec", d.codecx);

    // encoder
    avformat_alloc_output_context2(&e.formatx, NULL, NULL, outfile);
    e.codec = avcodec_find_encoder(AV_CODEC_ID_QTRLE);
    e.stream = avformat_new_stream(e.formatx, e.codec);
    e.codecx = avcodec_alloc_context3(e.codec);
    e.codecx->bit_rate = 4000000; // arbitrary for qtrle
    e.codecx->width = d.codecx->width;
    e.codecx->height = d.codecx->height;
    e.codecx->gop_size = 30; // 99% sure this is arbitrary for qtrle
    e.codecx->pix_fmt = AV_PIX_FMT_RGB24;
    e.codecx->time_base = d.stream->time_base; // ???
    e.codecx->flags |= (e.formatx->flags & AVFMT_GLOBALHEADER) ? AV_CODEC_FLAG_GLOBAL_HEADER : 0;
    avcodec_open2(e.codecx, NULL, NULL);
    avcodec_parameters_from_context(e.stream->codecpar, e.codecx); 
    //av_dump_format(e.formatx, 0, outfile, 1);
    dump_timebase("out stream", e.stream);
    dump_timebase("out stream:codec", e.stream->codec); // note: deprecated
    dump_timebase("out codec", e.codecx);

    // open file and write header
    avio_open(&e.formatx->pb, outfile, AVIO_FLAG_WRITE); 
    avformat_write_header(e.formatx, NULL);

    // frames
    while (read_frame(d) && write_frame(e, d.rgbframe))
        ;

    // write trailer and close file
    av_write_trailer(e.formatx);
    avio_closep(&e.formatx->pb); 

}

несколько заметок об этом:

  • так как все мои попытки в кадре время до сих пор не удалось, я удалил почти все связанные с временем вещи из этого кода, чтобы начать с чистого листа.
  • почти все проверки ошибок и очистки опущены для краткости.
  • причина, по которой я выделяю новый выходной кадр с новым буфером в write_frame, а не inframe непосредственно, потому что это более репрезентативно того, что делает мое реальное приложение. Мое реальное приложение также использует rgb24 внутри, следовательно, преобразования здесь.
  • причина I создать странный узор в outframe, вместо использования, например,av_copy_frame, потому что я просто хотел тестовый шаблон, который хорошо сжимается с Quicktime RLE (мой тестовый вход в конечном итоге генерирует выходной файл 1.7 GB в противном случае).
  • входное видео, которое я использую, " дикая природа.wmv", можно найти здесь. Я жестко закодированные имена.
  • я знаю, что avcodec_decode_video2 и avcodec_encode_video2 устарели, но мне все равно. Они работают отлично, я уже слишком много боролся, чтобы получить свое. голова вокруг последней версии API, ffmpeg изменяет свой API почти с каждым выпуском, и я действительно не чувствую, что имею дело с avcodec_send_* и avcodec_receive_* прямо сейчас.
  • я думаю, что я должен закончить с передача нулевого кадра в avcodec_encode_video2 чтобы смыть некоторые буферы или что-то еще, но я немного смущен этим. Если кто-то не хочет объяснить, что давайте пока проигнорируем это, это отдельный вопрос. Документы так же расплывчаты об этом точка, как они обо всем остальном.
  • частота кадров моего тестового входного файла составляет 29.97.

теперь, что касается моих текущих попыток. В приведенном выше коде присутствуют следующие поля, связанные с синхронизацией, с деталями/путаницей жирным шрифтом. Их много, потому что API умопомрачительно запутан:

  • main: d.stream->time_base: база времени входного видеопотока. для моего тестового входного файла это 1/1000.
  • main: d.stream->codec->time_base: не уверен, что это (я никогда не мог понять, почему AVStream есть AVCodecContext поле, когда вы всегда используете свой собственный новый контекст в любом случае), а также codec поле устарело. для моего тестового входного файла это 1/1000.
  • main: d.codecx->time_base: база времени контекста входного кодека. для моего тестового входного файла это 0/1. Я должен его установить?
  • main: e.stream->time_base: база времени выходного потока I создавать. что я должен установить это?
  • main: e.stream->codec->time_base: временная база устаревшего и таинственного поля кодека выходного потока, который я создаю. мне установить ее?
  • main: e.codecx->time_base: временная база контекста кодировщика, который я создаю. что я должен установить это?
  • read_frame: packet.dts: декодирование метки времени чтения пакета.
  • read_frame: packet.pts: временная метка представления прочитанного пакета.
  • read_frame: packet.duration: Продолжительность чтения пакета.
  • read_frame: d.rawframe->pts: презентации метки сырого фрейма. это всегда 0. Почему его не читает декодер?..?
  • read_frame: d.rgbframe->pts / write_frame: inframe->pts: метка времени представления декодированного кадра, преобразованного в RGB. В настоящее время ничего не установлено.
  • read_frame: d.rawframe->pkt_*: поля, скопированные из пакета, обнаруженные после чтения этот пост. Они установлены правильно, но я не знаю, если они полезный.
  • write_frame: outframe->pts: метка времени представления кодируемого кадра. я это к чему-то?
  • write_frame: outframe->pkt_*: поля синхронизации из пакета. должен ли я установить их? Похоже, кодировщик их игнорирует.
  • write_frame: packet.dts: декодирование метки времени кодирования пакета. что это?
  • write_frame: packet.pts: метка времени представления кодируемого пакета. что мне установить чтобы?
  • write_frame: packet.duration: длительность кодирования пакета. что это?

я попробовал следующее, с описанными результатами. Обратите внимание, что inframe и d.rgbframe:

  1.  
    • Init e.stream->time_base = d.stream->time_base
    • Init e.codecx->time_base = d.codecx->time_base
    • Set d.rgbframe->pts = packet.dts на read_frame
    • Set outframe->pts = inframe->pts на write_frame
    • результат: предупреждение о том, что база времени кодировщика не установлена (с d.codecx->time_base was 0/1), seg fault.
  2.  
    • Init e.stream->time_base = d.stream->time_base
    • Init e.codecx->time_base = d.stream->time_base
    • Set d.rgbframe->pts = packet.dts на read_frame
    • Set outframe->pts = inframe->pts на write_frame
    • результат: нет предупреждений, но VLC сообщает частоту кадров как 480.048 (не знаю, откуда взялось это число), и файл играет слишком быстро. также кодировщик устанавливает все поля синхронизации в packet до 0, что было не то, что я ожидал. (Edit: Повороты это потому что av_interleaved_write_frame в отличие от av_write_frame, берет на себя ответственность за пакет и меняет его на пустой, и я печатал значения после этот призыв. Поэтому они не игнорируются.)
  3.  
    • Init e.stream->time_base = d.stream->time_base
    • Init e.codecx->time_base = d.stream->time_base
    • Set d.rgbframe->pts = packet.dts на read_frame
    • установите любой из Pts / DTS / duration в packet на write_frame ничего.
    • результат: предупреждения о пакете метки не поставил. Кодировщик, похоже, сбрасывает все поля синхронизации пакетов до 0, поэтому ничто из этого не имеет никакого эффекта.
  4.  
    • Init e.stream->time_base = d.stream->time_base
    • Init e.codecx->time_base = d.stream->time_base
    • я нашел эти поля, pkt_pts, pkt_dts и pkt_duration на AVFrame после прочтения этот пост, поэтому я попытался скопировать их до outframe.
    • результат: на самом деле я надеялся, но в итоге получил те же результаты, что и попытка 3 (метка времени пакета не установлена предупреждение, неправильные результаты).

я пробовал различные другие ручные перестановки выше, и ничего не получилось. Что Я!--65-->хочу чтобы сделать, это создать выходной файл, который воспроизводится с той же частотой и частотой кадров, что и вход (29.97 постоянная частота кадров в этом случае).

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


для справки, вот таблица всех временных меток пакета и кадра, считанных из видеопотока моего тестового входного файла, чтобы дать представление о том, как выглядит мой тестовый файл. Ни один из входных пакетов pts ' не установлен, то же самое с фреймом pts, и по какой-то причине продолжительность первого 108 кадров-0. VLC воспроизводит файл отлично и сообщает частоту кадров как 29.9700089:

2 ответов


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

  • d.stream->time_base: Input video stream time base. Это разрешение временных меток во входном контейнере. Кодированный кадр, возвращенный из av_read_frame будет иметь свои метки времени в этом разрешении.
  • d.stream->codec->time_base: Not sure what this is. Это старый API, оставленный здесь для совместимости API; вы используете параметры кодека, поэтому игнорируйте его.
  • d.codecx->time_base: Input codec context time-base. For my test input file this is 0/1. Am I supposed to set it? Это разрешение временных меток для кодека (в отличие от контейнера). Кодек будет считать, что его входной кодированный кадр имеет свои временные метки в этом разрешении, а также он установит временные метки в выходном декодированном кадре в этом разрешении.
  • e.stream->time_base: Time base of the output stream I create. То же, что и с decoder
  • e.stream->codec->time_base. То же, что и с demuxer-игнорируйте это.
  • e.codecx->time_base - то же, что и с demuxer

Итак, вам нужно сделать следующее:

  • открыть демультиплексора. Эта часть работает
  • установите временную базу декодера на некоторую " вменяемую" значение, потому что декодер может этого не сделать, и 0/1-это плохо. Вещи не работают так, как должны если любой из timebases на любой из компонентов не установлены. Проще всего просто скопировать базу времени из demuxer
  • открыть декодером. Он может изменить свою временную базу, а может и нет.
  • установить энкодер частоты. Проще всего скопировать временную базу из (теперь открыт) декодера, так как вы не меняете частоту кадров или что-то еще.
  • открыть регулятор. Это может изменить его timebase
  • установить базу времени muxer. Опять же, проще всего скопировать временную базу из encoder
  • открыть muxer. Он также может изменить свою временную базу.

теперь для каждого кадра:

  • прочитайте это из demuxer
  • преобразовать временные метки от демультиплексора к timebases дешифратора. Есть av_packet_rescale_ts чтобы помочь вам сделать это
  • декодирования пакетов
  • установить временную метку кадра (pts) к значению, возвращаемому av_frame_get_best_effort_timestamp
  • преобразовать метку кадр из декодер энкодера timebases. Использовать av_rescale_q или av_rescale_q_rnd
  • кодировать пакет
  • преобразовать временные метки кодер мультиплексор timebases. Опять же, используйте av_packet_rescale_ts

это может быть излишним, в частности, возможно, кодеры не изменяют свою временную базу при открытии (в этом случае вам не нужно конвертировать необработанные кадры'pts).


относительно топить-рамки вы проходите к кодировщику не обязательно кодировать и выводить сразу, так что да, вы должны позвонить avcodec_encode_video2 С NULL в качестве кадра, чтобы кодировщик знал, что вы сделали, и заставил его вывести все оставшиеся данные (которые вам нужно передать через muxer, как и со всеми другими пакетами). Фактически, вы должны делать это неоднократно, пока он не перестанет извергать пакеты. См. один из примеров кодирования в doc/examples папка внутри ffmpeg для некоторых образцов.


Итак, спасибо 100%удивительно ясный и полезный ответ Андрея Туркина, у меня это работает правильно, я хотел бы поделиться точными вещами, которые я сделал:

во время инициализации, с пониманием того, что любая из этих начальных баз времени может быть изменена libav в какой-то момент:

  • инициализировать базу времени контекста кодека декодера к чему-то разумному сразу после выделения контекста кодека. Я пошел на суб-миллисекундных разрешение:

    d.codecx->time_base = { 1, 10000 };
    
  • инициализировать базу времени потока кодировщика сразу после создания нового потока (примечание: в случае QtRLE, если я оставлю этот {0,0}, он будет установлен кодировщиком в {0,90000} после написания заголовка, но я не знаю, будут ли другие ситуации такими же кооперативными, поэтому я инициализирую его здесь). На данный момент безопасно просто скопировать из входного потока, хотя я заметил, что могу также инициализировать его произвольно (например, {1,10000}), и он все равно будет работать позже:

    e.stream->time_base = d.stream->time_base;
    
  • инициализируйте базу времени контекста кодека кодека сразу после ее выделения. Та же сделка, что и потоковая временная база, что и копирование из декодера:

    e.codecx->time_base = d.codecx->time_base;
    

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

затем при декодировании:

  • все, что мне нужно сделать, это заполнить декодированные кадры pts вручную. The pkt_* поля игнорируемый:

    d.rawframe->pts = av_frame_get_best_effort_timestamp(d.rawframe);
    
  • и поскольку я конвертирую форматы, я также копирую его в преобразованный кадр:

    d.rgbframe->pts = d.rawframe->pts;
    

затем кодировки:

  • необходимо установить только pts кадра. Либав разберется с пакетом. Так просто до кодирования кадр:

    outframe->pts = inframe->pts;
    
  • тем не менее, мне все еще нужно вручную конвертировать временные метки пакетов, что кажется странным, но все это довольно странно, поэтому я думаю, что это нормально для курса. Временная метка кадра все еще находится в базе времени потока декодера, поэтому после кодирования кадра, но непосредственно перед записью пакета:

    av_packet_rescale_ts(&packet, d.stream->time_base, e.stream->time_base);
    

и он работает как шарм, в основном: я заметил, что VLC сообщает вход как 29.97 FPS, но выход на 30.03 FPS, который Я не могу понять. Но, кажется, все отлично играет во всех медиа-плеерах, с которыми я тестировал.