FFMPEG:多路流混合,时长不同

22
我正在复用视频和音频流。视频流来自生成的图像数据,音频流来自aac文件。有些音频文件比我设置的总视频时间长,因此我的策略是当音频流复用器的时间大于总视频时间时停止它的复用(最后一个由编码视频帧控制)。我不会在这里放置整个安装代码,但它类似于最新FFMPEG存储库中的muxing.c示例。唯一的区别是我使用来自文件的音频流,而不是从合成生成的编码帧。我非常确定问题在于我在复用器循环期间的错误同步。以下是我的做法:
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%,很明显音频对此有贡献,因为视频帧恰好完成在它们应该完成的位置。
我找到的最接近的“hack”是这一部分:
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},我将这些秒数转换为音频流的时间基准。至少我认为我是在做这个。通过这种方法,当使用标准AAC文件(即采样率为44100,立体声)时,我得到了非常小的偏差,从而使电影长度与正确的长度相差不大。但是,如果我使用更具问题性的样本进行测试,例如AAC采样率为16000,单声道,则视频文件的大小增加了将近一秒钟。
如果有人能指出我在这里做错了什么,我将不胜感激。
重要提示:我没有为任何上下文设置持续时间。我控制复合会话的终止,该终止基于视频帧计数。音频输入流当然有持续时间,但对我来说并没有帮助,因为视频持续时间定义了电影长度。
更新:
这是第二次赏金尝试。
更新2:
实际上,我的音频时间戳{den,num}是错误的,如答案所解释的那样,{1,1}确实是正确的方式。阻止其正常工作的是这行中的一个错误(我的错):
     mAudioOutStream.next_pts += a_pkt.pts;

必须是:

     mAudioOutStream.next_pts = a_pkt.pts;

这个bug导致pts指数级增长,导致流(以pts为衡量)非常早就到达了尽头,因此音频流比预期提前终止。


授予谁?难道 Stack Overflow 上真的没有人能回答这个问题吗? - Michael IV
截断 - 不是一个选项。第二件事 - 这就是我正在尝试做的事情。看看我的代码。 - Michael IV
我不熟悉FFMPEG API - 请注意,我非常熟悉API,这不是一个简单的问题。我不会为了一些简单的事情而放弃250分。顺便说一句,我故意翻转了时间基准,因为它是唯一一个给我带来“相对”可行性的变量...在那个例子中使用{1,1}并不能返回任何有意义的东西。我想这是因为我的音频流不像示例中使用的合成音频流那样。文件中音频流的时间基准由编解码器上下文设置,可能看起来很奇怪。 - Michael IV
“请”这个词很无礼吗?得了,伙计,如果你帮不了忙,就别怪我。我已经陷入这个错误太久了,不可能不熟悉FFMPEG的文档。 - Michael IV
简而言之,使用C++代码实现类似于https://dev59.com/Amcs5IYBdhLWcg3wOBRC的功能? - Tarun Lalwani
@TarunLalwani 正确。最终视频文件的总长度必须由视频长度决定,而我的应用程序控制着视频长度(计算编码视频帧数)。 - Michael IV
1个回答

4
问题在于你让它将给定的音频时间与每个滴答60秒进行比较。我很惊讶在某些情况下它能够工作,但我猜这确实取决于给定音频流的特定时间基准。
假设音频的时间基准是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)&lt; ts_b 将解析为true,因为0小于ts_b5),所以av_compare_ts将返回-1,这完全不是你想要的。
如果您将r更改为1/1,则应该可以正常工作,因为现在您的5将实际被视为5秒
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,即66大于5,条件不成立,然后再次调用av_rescale_rnd函数,这次使用的参数是:
a = 5
b = 25
c = 1
rnd = AV_ROUND_DOWN

这将返回(5 * 25) / 1,即125。这比150小,因此返回1,问题得到解决。

如果步长大于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


好的,让我试一下。看起来很有前途。 - Michael IV
不行,它不起作用。在这种情况下,音频流只在1-2秒后到达结尾。现在我想起来了,这就是我没有使用{1,1}时间戳的原因。以下是音频流的详细信息:时间基准:{1.48000},也许我的next_pts计算有误?每个PTS步骤为1024,编解码器时间戳和流时间戳也相等。 - Michael IV
啊,你需要在r中将分母乘以步长。你需要r={1,1024}。我会更新我的答案。 - Max Vollmer
请忽略我的评论。我有另一个错误导致 {1,1} 无法正常工作。我会在我的问题中提到它。谢谢! - Michael IV
1
好吧,至少你的编辑解释了为什么60,1有时会起作用。 - Max Vollmer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接