FFMPEG: мультиплексирование потоков разной продолжительности

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

Я не буду помещать здесь весь код установки, он похож на muxing.c из последнего репозитория FFMPEG. Единственная разница в том, что я использую аудиопоток из файла, как я уже сказал, а не из синтетически сгенерированного закодированного кадра. Я почти уверен, что проблема в моей неправильной синхронизации во время цикла мультиплексора. Вот что я делаю:

void AudioSetup(const char* audioInFileName)
{
    AVOutputFormat* outputF = mOutputFormatContext->oformat;
    auto audioCodecId = outputF->audio_codec;

    if (audioCodecId == AV_CODEC_ID_NONE) {
        return false;
    }

    audio_codec = avcodec_find_encoder(audioCodecId);

    avformat_open_input(&mInputAudioFormatContext,
    audioInFileName, 0, 0);
    avformat_find_stream_info(mInputAudioFormatContext, 0);

    av_dump_format(mInputAudioFormatContext, 0, audioInFileName, 0);


    for (size_t i = 0; i < mInputAudioFormatContext->nb_streams; i++) {
        if (mInputAudioFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            inAudioStream = mInputAudioFormatContext->streams[i];

            AVCodecParameters *in_codecpar = inAudioStream->codecpar;
            mAudioOutStream.st = avformat_new_stream(mOutputFormatContext, NULL);
            mAudioOutStream.st->id = mOutputFormatContext->nb_streams - 1;
            AVCodecContext* c = avcodec_alloc_context3(audio_codec);
            mAudioOutStream.enc = c;
            c->sample_fmt = audio_codec->sample_fmts[0];
            avcodec_parameters_to_context(c, inAudioStream->codecpar);
            //copyparams from input to autput audio stream:
            avcodec_parameters_copy(mAudioOutStream.st->codecpar, inAudioStream->codecpar);

            mAudioOutStream.st->time_base.num = 1;
            mAudioOutStream.st->time_base.den = c->sample_rate;

            c->time_base = mAudioOutStream.st->time_base;

            if (mOutputFormatContext->oformat->flags & AVFMT_GLOBALHEADER) {
                c->flags |= CODEC_FLAG_GLOBAL_HEADER;
            }
            break;
        }
    }
}

void Encode()
{
    int cc = av_compare_ts(mVideoOutStream.next_pts, mVideoOutStream.enc->time_base,
    mAudioOutStream.next_pts, mAudioOutStream.enc->time_base);

    if (mAudioOutStream.st == NULL || cc <= 0) {
        uint8_t* data = GetYUVFrame();//returns ready video YUV frame to work with
        int ret = 0;
        AVPacket pkt = { 0 };
        av_init_packet(&pkt);
        pkt.size = packet->dataSize;
        pkt.data = data;
        const int64_t duration = av_rescale_q(1, mVideoOutStream.enc->time_base, mVideoOutStream.st->time_base);

        pkt.duration = duration;
        pkt.pts = mVideoOutStream.next_pts;
        pkt.dts = mVideoOutStream.next_pts;
        mVideoOutStream.next_pts += duration;

        pkt.stream_index = mVideoOutStream.st->index;
        ret = av_interleaved_write_frame(mOutputFormatContext, &pkt);
    } else
    if(audio_time <  video_time) {
        //5 -  duration of video in seconds
        AVRational r = {  60, 1 };

        auto cmp= av_compare_ts(mAudioOutStream.next_pts, mAudioOutStream.enc->time_base, 5, r);
        if (cmp >= 0) {
            mAudioOutStream.next_pts = (int64_t)std::numeric_limits<int64_t>::max();
            return true; //don't mux audio anymore
        }

        AVPacket a_pkt = { 0 };
        av_init_packet(&a_pkt);

        int ret = 0;
        ret = av_read_frame(mInputAudioFormatContext, &a_pkt);
        //if audio file is shorter than stop muxing when at the end of the file
        if (ret == AVERROR_EOF) {
            mAudioOutStream.next_pts = (int64_t)std::numeric_limits<int64_t>::max(); 
            return true;
        }
        a_pkt.stream_index = mAudioOutStream.st->index;

        av_packet_rescale_ts(&a_pkt, inAudioStream->time_base, mAudioOutStream.st->time_base);
        mAudioOutStream.next_pts += a_pkt.pts;

        ret = av_interleaved_write_frame(mOutputFormatContext, &a_pkt);
    }
}

Теперь видео часть безупречна. Но если звуковая дорожка длиннее, чем продолжительность видео, я увеличиваю общую длину видео примерно на 5-20%, и ясно, что звук способствует этому, поскольку видеокадры заканчиваются именно там, где должны быть.

Ближайший «хак», который я использовал, - это эта часть:

AVRational r = {  60 ,1 };
auto cmp= av_compare_ts(mAudioOutStream.next_pts, mAudioOutStream.enc->time_base, 5, r);
if (cmp >= 0) {
    mAudioOutStream.next_pts = (int64_t)std::numeric_limits<int64_t>::max();
    return true;
} 

Здесь я пытался сравнить next_pts аудиопотока с общим временем, установленным для видеофайла, которое составляет 5 секунд. Установив r = {60,1}, я конвертирую эти секунды по time_base аудиопотока. По крайней мере, я так считаю. С помощью этого хака я получаю очень небольшое отклонение от правильной длины фильма при использовании стандартных файлов AAC, это частота дискретизации 44100, стерео. Но если я тестирую с более проблемными семплами, такими как частота дискретизации AAC 16000, моно - тогда видеофайл добавляет почти целую секунду к своему размеру. Буду признателен, если кто-нибудь укажет, что я здесь делаю неправильно.

Важное примечание: я не устанавливаю продолжительность ни для одного из контекстов. Я контролирую завершение сеанса мультиплексирования, который основан на количестве кадров видео. Поток аудиовхода, конечно, имеет длительность, но мне это не помогает, поскольку продолжительность видео определяет длину фильма.

ОБНОВИТЬ:

Это вторая попытка награждения.

ОБНОВЛЕНИЕ 2:

На самом деле моя временная метка звука {den, num} была неправильной, тогда как {1,1} действительно подходит, как объясняется в ответе. Что мешало ему работать, так это ошибка в этой строке (моя ошибка):

     mAudioOutStream.next_pts += a_pkt.pts;

Что должно быть:

     mAudioOutStream.next_pts = a_pkt.pts;

Ошибка приводила к экспоненциальному увеличению количества точек, что приводило к очень раннему достижению конца потока (в терминах точек) и, следовательно, приводило к прерыванию аудиопотока намного раньше, чем предполагалось.


person Michael IV    schedule 17.03.2018    source источник
comment
Кому присудить? Неужели никто из SO не может ответить на этот вопрос?   -  person Michael IV    schedule 27.03.2018
comment
Усечь - не вариант. Второе - это то, что я пытаюсь сделать. Посмотри на мой код.   -  person Michael IV    schedule 09.04.2018
comment
Я не знаком с FFMPEG API - пожалуйста, я более чем знаком с API, и это нетривиальная проблема. Я бы не стал отдавать 250 баллов за что-нибудь простое. Кстати, я намеренно переворачиваю эту временную шкалу, так как это был единственный вариант, который дал мне что-то «относительно» правдоподобное для работы ... использование {1,1}, поскольку в этом примере в моем случае не возвращается ничего значимого. Я думаю, это потому, что мой аудиопоток не является синтетическим, как в примере. Временная база аудиопотока из файла задается контекстом кодека и может выглядеть довольно странно.   -  person Michael IV    schedule 09.04.2018
comment
«Пожалуйста» грубо? Давай, чувак, если ты не можешь помочь, не вини меня в этом. Я слишком долго занимаюсь этой ошибкой, чтобы не знать документации FFMPEG.   -  person Michael IV    schedule 09.04.2018
comment
Короче говоря, достижение чего-то похожего на stackoverflow.com/questions/13041061/ с кодом C ++?   -  person Tarun Lalwani    schedule 10.04.2018
comment
@TarunLalwani Правильно. Общая длина окончательного видеофайла должна определяться длиной видео, которая в моем случае контролируется моим приложением (подсчет количества закодированных видеокадров).   -  person Michael IV    schedule 10.04.2018


Ответы (1)


Проблема в том, что вы говорите ему сравнить заданное время звука с 5 тиками на 60 seconds per tick. Я действительно удивлен, что в некоторых случаях это работает, но я полагаю, что это действительно зависит от конкретного time_base данного аудиопотока.

Предположим, что звук имеет time_base 1/25, а поток занимает 6 секунд, что больше, чем вы хотите, поэтому вы хотите, чтобы av_compare_ts возвращал 0 или 1. При этих условиях у вас будут следующие значения:

mAudioOutStream.next_pts = 150
mAudioOutStream.enc->time_base = 1/25

Таким образом, вы вызываете av_compare_ts со следующими параметрами:

ts_a = 150
tb_a = 1/25
ts_b = 5
tb_b = 60/1

Теперь посмотрим на реализацию av_compare_ts:

int av_compare_ts(int64_t ts_a, AVRational tb_a, int64_t ts_b, AVRational tb_b)
{
    int64_t a = tb_a.num * (int64_t)tb_b.den;
    int64_t b = tb_b.num * (int64_t)tb_a.den;
    if ((FFABS(ts_a)|a|FFABS(ts_b)|b) <= INT_MAX)
        return (ts_a*a > ts_b*b) - (ts_a*a < ts_b*b);
    if (av_rescale_rnd(ts_a, a, b, AV_ROUND_DOWN) < ts_b)
        return -1;
    if (av_rescale_rnd(ts_b, b, a, AV_ROUND_DOWN) < ts_a)
        return 1;
    return 0;
}

Учитывая приведенные выше значения, вы получите:

a = 1 * 1 = 1
b = 60 * 25 = 1500

Затем вызывается av_rescale_rnd со следующими параметрами:

a = 150
b = 1
c = 1500
rnd = AV_ROUND_DOWN

Учитывая наши параметры, мы можем фактически разделить всю функцию av_rescale_rnd до следующей строки. (Я не буду копировать все тело функции для av_rescale_rnd, так как оно довольно длинное, но вы можете посмотреть его здесь.)

return (a * b) / c;

Это вернет (150 * 1) / 1500, то есть 0.

Таким образом, av_rescale_rnd(ts_a, a, b, AV_ROUND_DOWN) < ts_b преобразуется в true, потому что 0 меньше ts_b (5), и поэтому av_compare_ts вернет -1, что совсем не то, что вам нужно.

Если вы измените свой r на 1/1, он должен работать, потому что теперь ваш 5 будет фактически обрабатываться как 5 seconds:

ts_a = 150
tb_a = 1/25
ts_b = 5
tb_b = 1/1

В av_compare_ts теперь получаем:

a = 1 * 1 = 1
b = 1 * 25 = 25

Затем вызывается av_rescale_rnd со следующими параметрами:

a = 150
b = 1
c = 25
rnd = AV_ROUND_DOWN

Это вернет (150 * 1) / 25, то есть 6.

6 больше 5, условие не выполняется, и av_rescale_rnd вызывается снова, на этот раз с:

a = 5
b = 25
c = 1
rnd = AV_ROUND_DOWN

который вернет (5 * 25) / 1, то есть 125. Это меньше, чем 150, поэтому возвращается 1 и ваша проблема решена.

Если step_size больше 1

Если step_size вашего аудиопотока не 1, вам нужно изменить свой r, чтобы учесть это, например step_size = 1024:

r = { 1, 1024 };

Давайте быстро резюмируем, что происходит сейчас:

Примерно через 6 секунд:

mAudioOutStream.next_pts = 282
mAudioOutStream.enc->time_base = 1/48000

av_compare_ts получает следующие параметры:

ts_a = 282
tb_a = 1/48000
ts_b = 5
tb_b = 1/1024

Таким образом:

a = 1 * 1024 = 1024
b = 1 * 48000 = 48000

И в av_rescale_rnd:

a = 282
b = 1024
c = 48000
rnd = AV_ROUND_DOWN

(a * b) / c даст (282 * 1024) / 48000 = 288768 / 48000, что равно 6.

С r={1,1} вы бы снова получили 0, потому что он рассчитал бы (281 * 1) / 48000.

person Max Vollmer    schedule 15.04.2018
comment
Хорошо, позволь мне попробовать. Выглядит многообещающе. - person Michael IV; 15.04.2018
comment
Нет, не работает. В этом случае аудиопоток достигает конца буквально через 1-2 секунды. Теперь я вспоминаю, что именно по этой причине я не использовал отметку времени {1,1}. Вот подробности аудиопотока: timebase: {1.48000}, может быть, мой расчет next_pts неверен? Каждый шаг PTS равен 1024, временная метка кодека и временная метка потока также равны. - person Michael IV; 16.04.2018
comment
Ах, вам нужно умножить знаменатель в вашем r на размер шага. Вам нужно r={1,1024}. Я обновлю свой ответ. - person Max Vollmer; 16.04.2018
comment
Откажитесь от моего комментария. У меня была еще одна ошибка, из-за которой {1,1} не работал должным образом. Я поставлю это в свой вопрос. Спасибо! - person Michael IV; 16.04.2018
comment
Ну, по крайней мере, ваша редакция объясняет, почему 60,1 иногда работал. - person Max Vollmer; 16.04.2018