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/%d\n", what, o->time_base.num, o->time_base.den);
else
printf("%s timebase: null object\n", 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: %s\n", 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: %s\n", 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 для внутреннего использования, отсюда и преобразования.
  • Причина, по которой я генерирую странный паттерн в outframeвместо использования, например, av_copy_frame, потому что я просто хотел, чтобы тестовый шаблон хорошо сжимался с помощью Quicktime RLE (в противном случае мой тестовый ввод заканчивал тем, что генерировал выходной файл 1,7 ГБ).
  • Входное видео, которое я использую, «wildlife.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: База времени выходного потока, который я создаю. Что мне установить?
  • 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.  
    • В этом e.stream->time_base = d.stream->time_base
    • В этом e.codecx->time_base = d.codecx->time_base
    • Задавать d.rgbframe->pts = packet.dts в read_frame
    • Задавать outframe->pts = inframe->pts в write_frame
    • Результат: предупреждение о том, что база времени энкодера не установлена ​​(так как d.codecx->time_base was 0/1), ошибка вины.
  2.  
    • В этом e.stream->time_base = d.stream->time_base
    • В этом e.codecx->time_base = d.stream->time_base
    • Задавать d.rgbframe->pts = packet.dts в read_frame
    • Задавать outframe->pts = inframe->pts в write_frame
    • Результат: предупреждений нет, но VLC сообщает частоту кадров 480.048 (не знаю, откуда взялась эта цифра), и файл воспроизводится слишком быстро. Также кодер устанавливает все поля синхронизации в packet до 0, что было не то, что я ожидал. (Редактировать: Оказывается, это потому, что av_interleaved_write_frame, В отличие от av_write_frame, становится владельцем пакета и заменяет его пустым, и я печатал значения после этот звонок. Поэтому они не игнорируются.)
  3.  
    • В этом e.stream->time_base = d.stream->time_base
    • В этом e.codecx->time_base = d.stream->time_base
    • Задавать d.rgbframe->pts = packet.dts в read_frame
    • Установите любой из pts / dts / duration в packet в write_frame ни к чему.
    • Результат: Предупреждения о временных отметках пакета не установлены. Кажется, что кодировщик сбрасывает все поля синхронизации пакетов на 0, поэтому ни одно из этих действий не оказывает никакого влияния.
  4.  
    • В этом e.stream->time_base = d.stream->time_base
    • В этом e.codecx->time_base = d.stream->time_base
    • Я нашел эти поля, pkt_pts, pkt_dts, а также pkt_duration в AVFrame после прочтения эта почта, так что я попытался скопировать их до outframe,
    • Результат: На самом деле мои надежды оправдались, но в результате были получены те же результаты, что и при попытке 3 (временная метка пакета не установила предупреждение, неверные результаты).

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

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


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

  • Стол здесь так как он был слишком велик для этого поста.

9

Решение

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

  • 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, То же, что и с декодером
  • e.stream->codec->time_base, То же, что и с демультиплексором — игнорируйте этот.
  • e.codecx->time_base — так же, как с демультиплексором

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

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

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

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

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


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

10

Другие решения

Итак, спасибо 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;
    

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

Тогда при декодировании:

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

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

    d.rgbframe->pts = d.rawframe->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, что я не могу понять. Но, похоже, все отлично играет во всех медиаплеерах, с которыми я тестировал.

4

По вопросам рекламы [email protected]