此文中的音频编码部分存在问题,详见下一篇:
OS使用FFmpeg进行音频编码
一.背景说明
在iOS开发中,音视频采集原始数据后,一般使用系统库VideoToolbox
和AudioToolbox
进行音视频的硬编码。而本文将使用FFmpeg
框架实现音视频的软编码,音频支持acc编码,视频支持h264,h265编码。
软件编码(简称软编):使用CPU进行编码。
硬件编码(简称硬编):不使用CPU进行编码,使用显卡GPU,专用的DSP、FPGA、ASIC芯片等硬件进行编码。优缺点:
软编:实现直接、简单,参数调整方便,升级易,但CPU负载重,性能较硬编码低,低码率下质量通常比硬编码要好一点。
硬编:性能高,低码率下通常质量低于硬编码器,但部分产品在GPU硬件平台移植了优秀的软编码算法(如X264)的,质量基本等同于软编码。
二.编码流程
三.初始化编码环境,配置编码参数。
1.初始化AVFormatContext
:
_pFormatCtx = avformat_alloc_context();
2.初始化音频流/视频流AVStream
:
_pStream = avformat_new_stream(_pFormatCtx, NULL);
3.创建编码器AVCodec
:
//aac编码器
_pCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);
//h264编码器
_pCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
av_dict_set(¶m, "preset", "slow", 0);
av_dict_set(¶m, "tune", "zerolatency", 0);
//h265编码器
_pCodec = avcodec_find_encoder(AV_CODEC_ID_HEVC);
av_dict_set(¶m, "preset", "ultrafast", 0);
av_dict_set(¶m, "tune", "zero-latency", 0);
4.初始化编码器上下文AVCodecContext
,并配置参数:需要注意的是旧版是通过_pStream->codec
来获取编码器上下文,新版此方法已废弃,使用avcodec_alloc_context3
方法来创建,配置完参数后使用avcodec_parameters_from_context
方法将参数复制到AVStream->codecpar
中。
//设置acc编码器上下文参数
_pCodecContext = avcodec_alloc_context3(_pCodec);
_pCodecContext->codec_type = AVMEDIA_TYPE_AUDIO;
_pCodecContext->sample_fmt = AV_SAMPLE_FMT_S16;
_pCodecContext->sample_rate = 44100;
_pCodecContext->channel_layout = AV_CH_LAYOUT_STEREO;
_pCodecContext->channels = av_get_channel_layout_nb_channels(_pCodecContext->channel_layout);
_pCodecContext->bit_rate = 64000;
//设置h264,h265编码器上下文参数
_pCodecContext->codec_type = AVMEDIA_TYPE_VIDEO;
_pCodecContext->width = 720;
_pCodecContext->height = 1280;
(省略)
5.打开编码器:
if (avcodec_open2(_pCodecContext, _pCodec, NULL) < 0) {
return ;
}
6.将AVCodecContext
中设置的参数复制到AVStream->codecpar
中
avcodec_parameters_from_context(_audioStream->codecpar, _pCodecContext);
7.初始化AVFrame
和AVPacket
:其中需要注意的是avpicture_get_size
方法被av_image_get_buffer_size
方法替代,avpicture_fill
方法被av_image_fill_arrays
方法替代。
//aac
_pFrame = av_frame_alloc();
_pFrame->nb_samples = _pCodecContext->frame_size;
_pFrame->format = _pCodecContext->sample_fmt;
int size = av_samples_get_buffer_size(NULL, _pCodecContext->channels, _pCodecContext->frame_size, _pCodecContext->sample_fmt, 1);
uint8_t *buffer = av_malloc(size);
avcodec_fill_audio_frame(_pFrame, _pCodecContext->channels, _pCodecContext->sample_fmt, buffer, size, 1);
av_new_packet(&_packet, size);
//h264 h265
_pFrame = av_frame_alloc();
_pFrame->width = _pCodecContext->width;
_pFrame->height = _pCodecContext->height;
_pFrame->format = _pCodecContext->sample_fmt;
int size = av_image_get_buffer_size(_pCodecContext->pix_fmt, _pCodecContext->width, _pCodecContext->width, 1);
uint8_t *buffer = av_malloc(size);
av_image_fill_arrays(_pFrame->data, NULL, buffer, _pCodecContext->pix_fmt, _pCodecContext->width, _pCodecContext->height, 1);
av_new_packet(&_packet, size);
四.音视频编码
1.音频编码,将采集到的pcm数据存入AVFrame->data[0]
,然后通过avcodec_send_frame
和avcodec_receive_packet
方法编码,从AVPacket
中获取编码后数据。旧版本的avcodec_encode_audio2
方法已经废弃。
- (void)encodeAudioWithSourceBuffer:(void *)sourceBuffer
sourceBufferSize:(UInt32)sourceBufferSize
pts:(int64_t)pts
{
int ret;
_pFrame->data[0] = sourceBuffer;
_pFrame->pts = pts;
ret = avcodec_send_frame(_pCodecContext, _pFrame);
if (ret < 0) {
return;
}
while (1) {
ret = avcodec_receive_packet(_pCodecContext, &_packet);
if (ret < 0) {
break;
}
if ([self.delegate respondsToSelector:@selector(receiveAudioEncoderData:size:)]) {
[self.delegate receiveAudioEncoderData:_packet.data size:_packet.size];
}
av_packet_unref(&_packet);
}
}
2.视频编码:需要从采集到的CMSampleBufferRef
中提取YUV或RGB数据,如果是YUV格式,则将YUV分量分别存入AVFrame->data[0]
,AVFrame->data[1]
,AVFrame->data[2]
中;如是RGB格式,则存入AVFrame->data[0]
。
CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
// 锁定imageBuffer内存地址开始进行编码
if (CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess) {
// Y
UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
// UV
UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
// Y分量长度
size_t bytesrow0 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,0);
size_t bytesrow1 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,1);
UInt8 *yuv420_data = (UInt8 *)malloc(width * height * 3 / 2);
// 将NV12数据转成YUV420P数据
UInt8 *pY = bufferPtr;
UInt8 *pUV = bufferPtr1;
UInt8 *pU = yuv420_data + width * height;
UInt8 *pV = pU + width * height / 4;
for(int i =0;i<height;i++)
{
memcpy(yuv420_data+i*width,pY+i*bytesrow0,width);
}
for(int j = 0;j<height/2;j++)
{
for(int i =0;i<width/2;i++)
{
*(pU++) = pUV[i<<1];
*(pV++) = pUV[(i<<1) + 1];
}
pUV += bytesrow1;
}
// 分别读取YUV的数据
picture_buf = yuv420_data;
_pFrame->data[0] = picture_buf; // Y
_pFrame->data[1] = picture_buf + width * height; // U
_pFrame->data[2] = picture_buf + width * height * 5 / 4; // V
// 设置当前帧
_pFrame->pts = frameCount;
int ret = avcodec_send_frame(_pCodecCtx, _pFrame);
if (ret < 0) {
printf("Failed to encode! \n");
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
return;
}
while (1) {
_packet.stream_index = _pStream->index;
ret = avcodec_receive_packet(_pCodecContext, &_packet);
if (ret < 0) {
break;
}
frameCount ++;
if ([self.delegate respondsToSelector:@selector(receiveAudioEncoderData:size:)]) {
[self.delegate receiveAudioEncoderData:_packet.data size:_packet.size];
}
av_packet_unref(&_packet);
}
// 释放yuv数据
free(yuv420_data);
}
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
五.结束编码
1.冲洗编码器:目的是将编码器上下文中的数据冲洗出来,避免造成丢帧。方法是使用avcodec_send_frame
方法向编码器上下文发送NULL,如果avcodec_receive_packet
方法返回值是0,则从AVPacket
中取出编码后数据,如果返回值是AVERROR_EOF
,则表示冲洗完成。
- (void)flushEncoder
{
int ret;
AVPacket packet;
if (_pCodec->capabilities & AV_CODEC_CAP_DELAY) {
return;
}
ret = avcodec_send_frame(_pCodecContext, NULL);
if (ret < 0) {
return;
}
while (1) {
packet.data = NULL;
packet.size = 0;
ret = avcodec_receive_packet(_pCodecContext, &packet);
if (ret < 0) {
break;
}
if ([self.delegate respondsToSelector:@selector(receiveAudioEncoderData:size:)]) {
[self.delegate receiveAudioEncoderData:packet.data size:packet.size];
}
av_packet_unref(&packet);
}
}
2.释放内存:
if (_pStream) {
avcodec_close(_pCodecContext);
av_free(_pFrame);
}
avformat_free_context(_pFormatCtx);
六.总结
1.FFmpeg中的编码是将采集到的pcm
和yuv
等原始数据存入AVFrame
中,然后将其发送给编码器,从AVPacket
中获取编码后的数据。
FFmpeg中的解码是编码的逆过程,使用av_read_frame
方法从音视频文件中获取AVPacket
,然后将其发送给解码器,从AVFrame
中获取解码后的pcm
和yuv
数据。
2.以上视频的编码,获取的是Annex B格式的H264/H265码流,其中SPS,PPS,(VPS)和IDR帧等都是在AVPacket
里面返回,此方式适合写入文件。
如果是推流场景,要获取SPS,PPS,(VPS)等信息,则需要设置:
_pCodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
这样在编码返回时,会将视频头信息放在extradata中,而不是每个关键帧前面。可以通过AVCodecContext
中的extradata
和extradata_size
获取SPS,PPS,(VPS)的数据和长度。数据也是Annex B格式,按照H264/H265的相关协议提取即可。
参考资料:
雷霄骅:Fmpeg源代码结构图 - 编码