四、视频的编解码-编码篇
Demo地址 在此之前我们通常使用的FFmpeg多媒体库,利用CPU来进行视频的编解码,占用CPU资源,效率低下,俗称软编解码.而苹果在2014年的iOS8中,开放了VideoToolbox.framwork框架,此框架使用GPU或专用的处理器来进行编解码,俗称硬编解码.而此框架在此之前只有MAC OS系统中可以使用,在iOS作为私有框架.终于苹果在iOS8.0中得到开放引入.
2014年的WWDCDirect Access to Video Encoding and Decoding中,苹果介绍了使用videoToolbox硬编解码. 使用硬编解码有几个优点:
- 提高性能;
- 增加效率;
- 延长电量的使用
对于编解码,AVFoundation框架只有以下几个功能:
- 直接解压后显示;
- 直接压缩到一个文件当中;
而对于Video Toolbox,我们可以通过以下功能获取到数据,进行网络流传输等多种保存:
- 解压为图像的数据结构;
压缩为视频图像的容器数据结构.
一、videoToolbox的基本数据
Video Toolbox视频编解码前后需要应用的数据结构进行说明。
CVPixelBuffer:编码前和解码后的图像数据结构。此内容包含一系列的CVPixelBufferPool内容
CMTime、CMClock和CMTimebase:时间戳相关。时间以64-bit/32-bit的形式出现。
pixelBufferAttributes:字典设置.可能包括Width/height、pixel format type、• Compatibility (e.g., OpenGL ES, Core Animation)
CMBlockBuffer:编码后,结果图像的数据结构。
CMVideoFormatDescription:图像存储方式,编解码器等格式描述。
(CMSampleBuffer:存放编解码前后的视频图像的容器数据结构。
CMClock
CMTimebase: 关于CMClock的一个控制视图,包含CMClock、时间映射(Time mapping)、速率控制(Rate control)
由二、采集视频数据可知,我们获取到的数据(CMSampleBufferRef)sampleBuffer
为未编码的数据;
图1.1
上图中,编码前后的视频图像都封装在CMSampleBuffer
中,编码前以CVPixelBuffer
进行存储;编码后以CMBlockBuffer
进行存储。除此之外两者都包括CMTime
、CMVideoFormatDesc
.
二、视频数据流编码并上传到服务器
1.将CVPixelBuffer使用VTCompressionSession进行数据流的硬编码。
(1)初始化VTCompressionSession
VT_EXPORT OSStatus
VTCompressionSessionCreate(
CM_NULLABLE CFAllocatorRef allocator,
int32_t width,
int32_t height,
CMVideoCodecType codecType,
CM_NULLABLE CFDictionaryRef encoderSpecification,
CM_NULLABLE CFDictionaryRef sourceImageBufferAttributes,
CM_NULLABLE CFAllocatorRef compressedDataAllocator,
CM_NULLABLE VTCompressionOutputCallback outputCallback,
void * CM_NULLABLE outputCallbackRefCon,
CM_RETURNS_RETAINED_PARAMETER CM_NULLABLE VTCompressionSessionRef * CM_NONNULL compressionSessionOut)
__OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_8_0);
VTCompressionSession的初始化参数说明:
- allocator:分配器,设置NULL为默认分配
- width: 宽
- height: 高
- codecType: 编码类型,如kCMVideoCodecType_H264
- encoderSpecification: 编码规范。设置NULL由videoToolbox自己选择
- sourceImageBufferAttributes: 源像素缓冲区属性.设置NULL不让videToolbox创建,而自己创建
- compressedDataAllocator: 压缩数据分配器.设置NULL,默认的分配
- outputCallback: 当VTCompressionSessionEncodeFrame被调用压缩一次后会被异步调用.注:当你设置NULL的时候,你需要调用VTCompressionSessionEncodeFrameWithOutputHandler方法进行压缩帧处理,支持iOS9.0以上
- outputCallbackRefCon: 回调客户定义的参考值.
- compressionSessionOut: 压缩会话变量。
(2)配置VTCompressionSession
使用VTSessionSetProperty()调用进行配置compression。
- kVTCompressionPropertyKey_AllowFrameReordering: 允许帧重新排序.默认为true
- kVTCompressionPropertyKey_AverageBitRate: 设置需要的平均编码率
- kVTCompressionPropertyKey_H264EntropyMode:H264的熵编码模式。有两种模式:一种基于上下文的二进制算数编码CABAC和可变长编码VLC.在slice层之上(picture和sequence)使用定长或变长的二进制编码,slice层及其以下使用VLC或CABAC.详情请参考
- kVTCompressionPropertyKey_RealTime: 视频编码压缩是否是实时压缩。可设置CFBoolean或NULL.默认为NULL
- kVTCompressionPropertyKey_ProfileLevel: 对于编码流指定配置和标准 .比如kVTProfileLevel_H264_Main_AutoLevel
配置过VTCompressionSession后,可以可选的调用VTCompressionSessionPrepareToEncodeFrames
进行准备工作编码帧。
(3)开始硬编码流入的数据
使用VTCompressionSessionEncodeFrame
方法进行编码.当编码结束后调用outputCallback回调函数。
VT_EXPORT OSStatus
VTCompressionSessionEncodeFrame(
CM_NONNULL VTCompressionSessionRef session,
CM_NONNULL CVImageBufferRef imageBuffer,
CMTime presentationTimeStamp,
CMTime duration, // may be kCMTimeInvalid
CM_NULLABLE CFDictionaryRef frameProperties,
void * CM_NULLABLE sourceFrameRefCon,
VTEncodeInfoFlags * CM_NULLABLE infoFlagsOut )
__OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_8_0);
- presentationTimeStamp: 获取到的这个sample buffer数据的展示时间戳。每一个传给这个session的时间戳都要大于前一个展示时间戳.
- duration: 对于获取到sample buffer数据,这个帧的展示时间.如果没有时间信息,可设置
kCMTimeInvalid
. - frameProperties: 包含这个帧的属性.帧的改变会影响后边的编码帧.
- sourceFrameRefCon: 回调函数会引用你设置的这个帧的参考值.
- infoFlagsOut: 指向一个
VTEncodeInfoFlags
来接受一个编码操作.如果使用异步运行,kVTEncodeInfo_Asynchronous
被设置;同步运行,kVTEncodeInfo_FrameDropped
被设置;设置NULL为不想接受这个信息.
(4)执行VTCompressionOutputCallback回调函数
typedef void (*VTCompressionOutputCallback)(
void * CM_NULLABLE outputCallbackRefCon,
void * CM_NULLABLE sourceFrameRefCon,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CM_NULLABLE CMSampleBufferRef sampleBuffer );
- outputCallbackRefCon: 回调函数的参考值
- sourceFrameRefCon: VTCompressionSessionEncodeFrame函数中设置的帧的参考值
- status: 压缩的成功为noErr,如失败有错误码
- infoFlags: 包含编码操作的信息标识
- sampleBuffer: 如果压缩成功或者帧不丢失,则包含这个已压缩的数据CMSampleBuffer,否则为NULL
(5)将压缩成功的sampleBuffer数据进行处理为基本流NSData上传到服务器
MPEG-4是一套用于音频、视频信息的压缩编码标准.
由图1.1可知,已压缩 $$CMSampleBuffer = CMTime(可选) + CMBlockBuffer + CMVideoFormatDesc$$。
5.1 先判断压缩的数据是否正确
//不存在则代表压缩不成功或帧丢失
if(!sampleBuffer) return;
if (status != noErr) return;
//返回sampleBuffer中包括可变字典的不可变数组,如果有错误则为NULL
CFArrayRef array = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
if (!array) return;
CFDictionaryRef dic = CFArrayGetValueAtIndex(array, 0);
if (!dic) return;
//issue 3:kCMSampleAttachmentKey_NotSync:没有这个键意味着同步, yes: 异步. no:同步
BOOL keyframe = !CFDictionaryContainsKey(dic, kCMSampleAttachmentKey_NotSync); //此代表为同步
而对于issue 3从字面意思理解即为以上的说明,但是网上看到很多都是做为查询是否是视频关键帧,而查询文档看到有此关键帧key值kCMSampleBufferAttachmentKey_ForceKeyFrame
存在,因此对此值如若有了解情况者敬请告知详情.
5.2 获取CMVideoFormatDesc数据 由三、解码篇可知CMVideoFormatDesc 包括编码所用的profile,level,图像的宽和高,deblock滤波器等.具体包含第一个NALU的SPS(Sequence Parameter Set)和第二个NALU的PPS(Picture Parameter Set).
//
if (keyframe && !encoder -> sps) {
//获取sample buffer 中的 CMVideoFormatDesc
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
//获取H264参数集合中的SPS和PPS
const uint8_t * sparameterSet;
size_t sparameterSetSize,sparameterSetCount ;
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0);
if (statusCode == noErr) {
size_t pparameterSetSize, pparameterSetCount;
const uint8_t *pparameterSet;
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);
if (statusCode == noErr) {
encoder->sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
encoder->pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
}
}
}
5.3 获取CMBlockBuffer并转换成数据
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
size_t lengthAtOffset,totalLength;
char *dataPointer;
//接收到的数据展示
OSStatus blockBufferStatus = CMBlockBufferGetDataPointer(blockBuffer, 0, &lengthAtOffset, &totalLength, &dataPointer);
if (blockBufferStatus != kCMBlockBufferNoErr)
{
size_t bufferOffset = 0;
static const int AVCCHeaderLength = 4;
while (bufferOffset < totalLength - AVCCHeaderLength) {
// Read the NAL unit length
uint32_t NALUnitLength = 0;
/**
* void *memcpy(void *dest, const void *src, size_t n);
* 从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中
*/
memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
//字节从高位反转到低位
NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
RTAVVideoFrame * frame = [RTAVVideoFrame new];
frame.sps = encoder -> sps;
frame.pps = encoder -> pps;
frame.data = [NSData dataWithBytes:(dataPointer+bufferOffset+AVCCHeaderLength) length:NALUnitLength];
bufferOffset += NALUnitLength + AVCCHeaderLength;
}
}
此得到的H264数据应用于后面的RTMP协议做推流准备。