此文中的音頻編碼部分存在問題,詳見下一篇:
OS使用FFmpeg進行音頻編碼
一.背景說明
在iOS開發(fā)中,音視頻采集原始數(shù)據(jù)后,一般使用系統(tǒng)庫VideoToolbox
和AudioToolbox
進行音視頻的硬編碼。而本文將使用FFmpeg
框架實現(xiàn)音視頻的軟編碼惹挟,音頻支持acc編碼,視頻支持h264,h265編碼缝驳。
軟件編碼(簡稱軟編):使用CPU進行編碼甜害。
硬件編碼(簡稱硬編):不使用CPU進行編碼到逊,使用顯卡GPU,專用的DSP只恨、FPGA灵巧、ASIC芯片等硬件進行編碼。優(yōu)缺點:
軟編:實現(xiàn)直接夏伊、簡單摇展,參數(shù)調(diào)整方便,升級易溺忧,但CPU負載重咏连,性能較硬編碼低,低碼率下質(zhì)量通常比硬編碼要好一點鲁森。
硬編:性能高祟滴,低碼率下通常質(zhì)量低于硬編碼器,但部分產(chǎn)品在GPU硬件平臺移植了優(yōu)秀的軟編碼算法(如X264)的歌溉,質(zhì)量基本等同于軟編碼垄懂。
二.編碼流程
三.初始化編碼環(huán)境,配置編碼參數(shù)。
1.初始化AVFormatContext
:
_pFormatCtx = avformat_alloc_context();
2.初始化音頻流/視頻流AVStream
:
_pStream = avformat_new_stream(_pFormatCtx, NULL);
3.創(chuàng)建編碼器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
埠偿,并配置參數(shù):需要注意的是舊版是通過_pStream->codec
來獲取編碼器上下文,新版此方法已廢棄榜晦,使用avcodec_alloc_context3
方法來創(chuàng)建冠蒋,配置完參數(shù)后使用avcodec_parameters_from_context
方法將參數(shù)復(fù)制到AVStream->codecpar
中。
//設(shè)置acc編碼器上下文參數(shù)
_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;
//設(shè)置h264,h265編碼器上下文參數(shù)
_pCodecContext->codec_type = AVMEDIA_TYPE_VIDEO;
_pCodecContext->width = 720;
_pCodecContext->height = 1280;
(省略)
5.打開編碼器:
if (avcodec_open2(_pCodecContext, _pCodec, NULL) < 0) {
return ;
}
6.將AVCodecContext
中設(shè)置的參數(shù)復(fù)制到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數(shù)據(jù)存入AVFrame->data[0]
,然后通過avcodec_send_frame
和avcodec_receive_packet
方法編碼识窿,從AVPacket
中獲取編碼后數(shù)據(jù)斩郎。舊版本的avcodec_encode_audio2
方法已經(jīng)廢棄。
- (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數(shù)據(jù)喻频,如果是YUV格式缩宜,則將YUV分量分別存入AVFrame->data[0]
,AVFrame->data[1]
甥温,AVFrame->data[2]
中锻煌;如是RGB格式,則存入AVFrame->data[0]
姻蚓。
CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
// 鎖定imageBuffer內(nèi)存地址開始進行編碼
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數(shù)據(jù)轉(zhuǎn)成YUV420P數(shù)據(jù)
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的數(shù)據(jù)
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
// 設(shè)置當(dāng)前幀
_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數(shù)據(jù)
free(yuv420_data);
}
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
五.結(jié)束編碼
1.沖洗編碼器:目的是將編碼器上下文中的數(shù)據(jù)沖洗出來宋梧,避免造成丟幀。方法是使用avcodec_send_frame
方法向編碼器上下文發(fā)送NULL狰挡,如果avcodec_receive_packet
方法返回值是0捂龄,則從AVPacket
中取出編碼后數(shù)據(jù),如果返回值是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.釋放內(nèi)存:
if (_pStream) {
avcodec_close(_pCodecContext);
av_free(_pFrame);
}
avformat_free_context(_pFormatCtx);
六.總結(jié)
1.FFmpeg中的編碼是將采集到的pcm
和yuv
等原始數(shù)據(jù)存入AVFrame
中,然后將其發(fā)送給編碼器它匕,從AVPacket
中獲取編碼后的數(shù)據(jù)刀脏。
FFmpeg中的解碼是編碼的逆過程,使用av_read_frame
方法從音視頻文件中獲取AVPacket
超凳,然后將其發(fā)送給解碼器愈污,從AVFrame
中獲取解碼后的pcm
和yuv
數(shù)據(jù)。
2.以上視頻的編碼轮傍,獲取的是Annex B格式的H264/H265碼流暂雹,其中SPS,PPS,(VPS)和IDR幀等都是在AVPacket
里面返回,此方式適合寫入文件创夜。
如果是推流場景杭跪,要獲取SPS,PPS,(VPS)等信息,則需要設(shè)置:
_pCodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
這樣在編碼返回時,會將視頻頭信息放在extradata中涧尿,而不是每個關(guān)鍵幀前面系奉。可以通過AVCodecContext
中的extradata
和extradata_size
獲取SPS,PPS,(VPS)的數(shù)據(jù)和長度姑廉。數(shù)據(jù)也是Annex B格式缺亮,按照H264/H265的相關(guān)協(xié)議提取即可。