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

