You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android平台FFmpeg编码AAC音频时长与比特率异常问题求助

问题描述

我在Android平台使用FFmpeg库编码原始音频数据,通过Native代码读取外部设备的音频数据并将其编码为MP4容器中的AAC格式。音频在Groove Music等播放器可正常播放完整内容,但ffprobe检测到元数据异常:

  • 实际时长数十秒的音频,元数据仅显示0.05秒
  • 指定192kbps比特率,检测结果却约为65kbps
  • 更换Quicktime等播放器,仅能播放前0.05秒左右的内容

以下是移除错误检查后的核心代码(实际代码已校验所有调用,无报错):

初始化代码

void AudioWriter::initialise( const char *filePath ) {
    AVCodecID avCodecID = AVCodecID::AV_CODEC_ID_AAC;
    int bitRate = 192000;
    char *containerFormat = "mp4";
    int sampleRate = 48000;
    int nChannels = 2;
    mAvCodec = avcodec_find_encoder(avCodecID);
    mAvCodecContext = avcodec_alloc_context3(mAvCodec);
    mAvCodecContext->codec_id = avCodecID;
    mAvCodecContext->codec_type = AVMEDIA_TYPE_AUDIO;
    mAvCodecContext->sample_fmt = AV_SAMPLE_FMT_FLTP;
    mAvCodecContext->bit_rate = bitRate;
    mAvCodecContext->sample_rate = sampleRate;
    mAvCodecContext->channels = nChannels;
    mAvCodecContext->channel_layout = AV_CH_LAYOUT_STEREO;
    avcodec_open2( mAvCodecContext, mAvCodec, nullptr );
    mAvFormatContext = avformat_alloc_context();
    avformat_alloc_output_context2(&mAvFormatContext, nullptr, containerFormat, nullptr);
    mAvFormatContext->audio_codec = mAvCodec;
    mAvFormatContext->audio_codec_id = avCodecID;
    mAvOutputStream = avformat_new_stream(mAvFormatContext, mAvCodec);
    avcodec_parameters_from_context(mAvOutputStream->codecpar, mAvCodecContext);
    if (!(mAvFormatContext->oformat->flags & AVFMT_NOFILE)) {
        avio_open(&mAvFormatContext->pb, filePath, AVIO_FLAG_WRITE);
    }
    if ( mAvFormatContext->oformat->flags & AVFMT_GLOBALHEADER ) {
        mAvCodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    }
    avformat_write_header(mAvFormatContext, NULL);
    mAvAudioFrame = av_frame_alloc();
    mAvAudioFrame->nb_samples = mAvCodecContext->frame_size;
    mAvAudioFrame->format = mAvCodecContext->sample_fmt;
    mAvAudioFrame->channel_layout = mAvCodecContext->channel_layout;
    av_samples_get_buffer_size(NULL, mAvCodecContext->channels, mAvCodecContext->frame_size, mAvCodecContext->sample_fmt, 0);
    av_frame_get_buffer(mAvAudioFrame, 0);
    av_frame_make_writable(mAvAudioFrame);
    mAvPacket = av_packet_alloc();
}

编码代码

// SoundRecording是封装原始采样数据的自定义类
bool AudioWriter::encodeToContainer( SoundRecording *soundRecording ) {
    int ret;
    int frameCount = mAvCodecContext->frame_size;
    int nChannels = mAvCodecContext->channels;
    float *buf = new float[frameCount*nChannels];
    while ( soundRecording->hasReadableData() ) {
        //填充帧数据
        int samplesRead = soundRecording->read( buf, frameCount*nChannels );
        // 平面数据处理
        int nFrames = samplesRead/nChannels;
        for ( int i = 0; i < nFrames; ++i ) {
            for (int c = 0; c < nChannels; ++c ) {
                samples[c][i] = buf[nChannels*i +c];
            }
        }
        // 用静音填充剩余空间
        if ( samplesRead < frameCount*nChannels ) {
            for ( int i = samplesRead; i < frameCount*nChannels; ++i ) {
                for (int c = 0; c < nChannels; ++c ) {
                    samples[c][i] = 0.0;
                }
            }
        }
        encodeFrame( mAvAudioFrame );
    }
    finish();
}

bool AudioWriter::encodeFrame( AVFrame *frame ) {
    //发送帧进行编码
    int ret;
    if ( frame != nullptr ) {
        frame->pts = mAudFrameCounter++;
    }
    avcodec_send_frame(mAvCodecContext, frame );
    while (ret >= 0) {
        ret = avcodec_receive_packet(mAvCodecContext, mAvPacket);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF ) {
            break;
        } else if (ret < 0) {
            return false;
        }
        av_packet_rescale_ts(mAvPacket, mAvCodecContext->time_base, mAvOutputStream->time_base);
        mAvPacket->stream_index = mAvOutputStream->index;
        av_interleaved_write_frame(mAvFormatContext, mAvPacket);
        av_packet_unref(mAvPacket);
    }
    return true;
}

void AudioWriter::finish() {
    // 发送空帧刷新编码器
    encodeFrame( nullptr );
    av_write_trailer(mAvFormatContext);
}

我猜测时长与比特率异常是时间相关信息未被正确管理导致的。目前我只是用递增整数设置帧的pts,但不清楚时间戳与流索引设置代码的作用及必要性,恳请帮忙排查问题所在。


问题分析与修复方案

你的问题核心在于时间戳(PTS)计算错误,以及初始化流程中的几个关键顺序问题,这些导致FFmpeg无法正确计算音频的时长和比特率元数据,进而影响播放器的解析。

1. 核心问题:PTS计算错误

你当前用frame->pts = mAudFrameCounter++;设置时间戳,但FFmpeg的PTS是基于**时间基(time_base)**的,不是简单的递增整数。对于AAC编码器,每个帧的PTS应该对应实际的采样位置:

  • 音频的时间基是AVRational{1, sample_rate},即每个单位代表1个采样的时间
  • 每个帧包含frame_size个采样,所以每次递增的PTS应该是当前帧的样本数,而不是1

举个例子,48kHz采样率下,每个AAC帧默认是1024个采样,那么第一帧PTS是0,第二帧是1024,第三帧是2048,以此类推。这样FFmpeg才能正确计算出总时长(总采样数 / 采样率)。

2. 初始化顺序错误:GLOBAL_HEADER标志设置太晚

你在avcodec_open2之后才设置AV_CODEC_FLAG_GLOBAL_HEADER,这会导致编码器没有应用这个标志。MP4容器需要AAC的全局头部(ADTS头的简化版),正确的顺序应该是:

  • 先检查容器是否需要全局头部
  • 设置编码器上下文的标志
  • 再打开编码器

3. 平面数据填充逻辑错误

你的代码中samples[c][i]的索引有问题,而且没有正确关联到AVFrame的平面数据指针。AV_SAMPLE_FMT_FLTP格式下,frame->data[c]是每个声道的独立缓冲区,你应该直接操作这些指针,而不是用未定义的samples变量。

4. 最后一帧的样本数处理

当最后一次读取的样本数不足一帧时,你不应该强制填充到满帧(除非你确实需要静音),而是应该设置frame->nb_samples为实际读取的样本数,避免额外的静音时长影响总时长计算。


修正后的关键代码

初始化代码修正

void AudioWriter::initialise( const char *filePath ) {
    AVCodecID avCodecID = AVCodecID::AV_CODEC_ID_AAC;
    int bitRate = 192000;
    char *containerFormat = "mp4";
    int sampleRate = 48000;
    int nChannels = 2;
    mAvCodec = avcodec_find_encoder(avCodecID);
    mAvCodecContext = avcodec_alloc_context3(mAvCodec);
    mAvCodecContext->codec_id = avCodecID;
    mAvCodecContext->codec_type = AVMEDIA_TYPE_AUDIO;
    mAvCodecContext->sample_fmt = AV_SAMPLE_FMT_FLTP;
    mAvCodecContext->bit_rate = bitRate;
    mAvCodecContext->sample_rate = sampleRate;
    mAvCodecContext->channels = nChannels;
    mAvCodecContext->channel_layout = AV_CH_LAYOUT_STEREO;

    // 先初始化格式上下文,检查是否需要全局头部
    mAvFormatContext = avformat_alloc_context();
    avformat_alloc_output_context2(&mAvFormatContext, nullptr, containerFormat, filePath);
    if ( mAvFormatContext->oformat->flags & AVFMT_GLOBALHEADER ) {
        mAvCodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    }

    // 打开编码器
    avcodec_open2( mAvCodecContext, mAvCodec, nullptr );

    mAvOutputStream = avformat_new_stream(mAvFormatContext, mAvCodec);
    avcodec_parameters_from_context(mAvOutputStream->codecpar, mAvCodecContext);
    // 统一时间基:音频用1/采样率
    mAvOutputStream->time_base = {1, sampleRate};
    mAvCodecContext->time_base = mAvOutputStream->time_base;

    if (!(mAvFormatContext->oformat->flags & AVFMT_NOFILE)) {
        int ret = avio_open(&mAvFormatContext->pb, filePath, AVIO_FLAG_WRITE);
        // 实际代码中需检查ret是否成功
    }

    avformat_write_header(mAvFormatContext, NULL);

    mAvAudioFrame = av_frame_alloc();
    mAvAudioFrame->format = mAvCodecContext->sample_fmt;
    mAvAudioFrame->channel_layout = mAvCodecContext->channel_layout;
    mAvAudioFrame->sample_rate = sampleRate;
    // 初始分配最大帧大小的缓冲区
    mAvAudioFrame->nb_samples = mAvCodecContext->frame_size;
    av_frame_get_buffer(mAvAudioFrame, 0);

    mAvPacket = av_packet_alloc();
    // 初始化PTS计数器,基于采样数累计
    mAudPtsCounter = 0;
}

编码代码修正

bool AudioWriter::encodeToContainer( SoundRecording *soundRecording ) {
    int frameSize = mAvCodecContext->frame_size;
    int nChannels = mAvCodecContext->channels;
    float *buf = new float[frameSize * nChannels];

    while ( soundRecording->hasReadableData() ) {
        int samplesRead = soundRecording->read( buf, frameSize * nChannels );
        int actualSamples = samplesRead / nChannels;

        // 确保帧可写
        av_frame_make_writable(mAvAudioFrame);
        // 设置当前帧的实际样本数
        mAvAudioFrame->nb_samples = actualSamples;

        // 填充平面数据到AVFrame的对应声道缓冲区
        float** frameData = (float**)mAvAudioFrame->data;
        for (int c = 0; c < nChannels; ++c) {
            for (int i = 0; i < actualSamples; ++i) {
                frameData[c][i] = buf[i * nChannels + c];
            }
            // 可选:如果需要固定帧大小,用静音填充剩余空间
            for (int i = actualSamples; i < frameSize; ++i) {
                frameData[c][i] = 0.0f;
            }
        }

        encodeFrame( mAvAudioFrame );
        // 更新PTS计数器:累计当前帧的样本数
        mAudPtsCounter += actualSamples;
    }

    finish();
    delete[] buf;
    return true;
}

bool AudioWriter::encodeFrame( AVFrame *frame ) {
    int ret;
    if ( frame != nullptr ) {
        // 设置正确的PTS:基于累计采样数的时间戳
        frame->pts = mAudPtsCounter;
    }

    ret = avcodec_send_frame(mAvCodecContext, frame );
    if (ret < 0) {
        return false;
    }

    while (true) {
        ret = avcodec_receive_packet(mAvCodecContext, mAvPacket);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF ) {
            break;
        } else if (ret < 0) {
            return false;
        }

        // 时间戳转换(已统一时间基,可省略但保留更安全)
        av_packet_rescale_ts(mAvPacket, mAvCodecContext->time_base, mAvOutputStream->time_base);
        mAvPacket->stream_index = mAvOutputStream->index;

        ret = av_interleaved_write_frame(mAvFormatContext, mAvPacket);
        if (ret < 0) {
            // 实际代码中处理错误
        }
        av_packet_unref(mAvPacket);
    }
    return true;
}

void AudioWriter::finish() {
    encodeFrame( nullptr );
    av_write_trailer(mAvFormatContext);

    // 补充资源释放逻辑,避免内存泄漏
    if (!(mAvFormatContext->oformat->flags & AVFMT_NOFILE)) {
        avio_closep(&mAvFormatContext->pb);
    }
    avformat_free_context(mAvFormatContext);
    avcodec_free_context(&mAvCodecContext);
    av_frame_free(&mAvAudioFrame);
    av_packet_free(&mAvPacket);
}

关键说明

  • PTS计算:mAudPtsCounter每次递增当前帧的样本数,FFmpeg会根据总PTS值 / 采样率计算出准确的总时长。
  • 全局头部设置:提前在打开编码器前设置AV_CODEC_FLAG_GLOBAL_HEADER,确保MP4容器能正确识别AAC的配置信息。
  • 平面数据处理:直接操作frame->data指针,避免未定义变量导致的内存错误。
  • 资源释放:在finish中补充了完整的资源释放逻辑,避免内存泄漏。

修复后,ffprobe应该能正确识别音频的时长和比特率,Quicktime等播放器也能正常播放完整内容了。

内容的提问来源于stack exchange,提问作者Timmy K

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 08:32:19