简述
上一章,我们介绍了iOS采集相关细节。这一次,我来介绍下iOS硬编码相关知识。
首先,为什么需要编码,在上一次中,我们提到了一个东西,CMSampleBuffer,这个既可以用来封装ImageBuffer,也可以用于存储裸流数据,是一个通用的结构体。我先介绍一下这个是什么东西,之后就明白为啥需要编码了。
初识CVPixelBufferRef
在我们通常的颜色世界里面,我们都知道RGB三原色,使用这三种颜色的混合搭配,可以组成世界上的绝大多数色彩。 在计算机里面,我们称这种格式为kCVPixelFormatType_32RGBA,即每32位的数据里面,包含8位的R,8位的G,8位的B,8位的A。而通常,我们使用的却不是这个,而是另外一个,即kCVPixelFormatType_32BGRA。
到这里,我们能明白,如果使用RGBA来代表一张图,一个像素点就包含4字节,每一个字节代表个字位的原色。那么我们如果需要看一个60秒30fps720p的视频,那么我们需要720 * 1280 * 4 * 60 * 30 = 6635520000b数据,相当于791MB数据,我觉得大部分人的网速连10MB/s都可能达不到,更何况791MB/s,如果不经过视频编码,我们根本无法再现观看视频,更不用说直播了。
那在iOS里面是如何存储这些数据呢?那个对象,就是CVPixelBufferRef,CVPixelBufferRef实际上就是一张图画的像素点的布局。它可以是以下几种类型中的一种:
图像类型 | 含义 |
---|
kCVPixelFormatType_32RGBA | RGBA32位,布局为RGBA | kCVPixelFormatType_32BGRA | BGRA32为,布局为BGRA | kCVPixelFormatType_420YpCbCr8Planar / kCVPixelFormatType_420YpCbCr8PlanarFullRange | I420数据排列,布局为前面height * strides[0]为Y分量,中间height * strides[1] / 2为U分量,后面的数据为V分量 | kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange | NV12数据排列,布局为height * strides[0]为Y分量,height * strides[0]为UV分量,为比较常用的类型,运用于编码,解码,渲染中。UV数据范围为(luma=[16,235] chroma=[16,240]) | kCVPixelFormatType_420YpCbCr8BiPlanarFullRange | NV12数据排列,布局为height * strides[0]为Y分量,height * strides[0]为UV分量,为比较常用的类型,运用于编码,解码,渲染中。UV数据范围为(luma=[0,255] chroma=[0,255]) | kCVPixelFormatType_OneComponent8 | Gray分量,即Y分量,常用于端上画质增强的输入源。 | kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange / kCVPixelFormatType_420YpCbCr10BiPlanarFullRange | HDR10相关数据源,后期会解释如何解码得到这种数据源。 |
我们知道了CVPixelBufferRef是一个什么样的东西,那我们如何创建一个CVPixelBuffer呢?我们看下相关的函数原型
CV_EXPORT CVReturn CVPixelBufferCreate(
CFAllocatorRef CV_NULLABLE allocator,
size_t width,
size_t height,
OSType pixelFormatType,
CFDictionaryRef CV_NULLABLE pixelBufferAttributes,
CV_RETURNS_RETAINED_PARAMETER CVPixelBufferRef CV_NULLABLE * CV_NONNULL pixelBufferOut) __OSX_AVAILABLE_STARTING(__MAC_10_4,__IPHONE_4_0);
示例代码:
int width = 1280;
int height = 720;
OSType format = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
const void *keys[] = {
kCVPixelBufferOpenGLESCompatibilityKey,
kCVPixelBufferIOSurfacePropertiesKey
};
const void *values[] = {
(__bridge void *)[NSNumber numberWithBool:YES],
(__bridge void *)[NSDictionary dictionary]
};
CFDictionaryRef attributes = CFDictionaryCreate(NULL, keys, values, 2, NULL, NULL);
CVPixelBufferRef pixelBuffer = nil;
CVReturn ret = CVPixelBufferCreate(kCFAllocatorDefault,
width,
height,
format,
attributes,
&pixelBuffer);
我们已经可以自己创建CVPixelBufferRef了,yeah~不过,好像和我们本章的话题没啥关系。 其实也不然,在音视频里面,时常都需要进行图像的裁剪或者美颜,需要对图像能进行处理,而深入理解CVPixelBufferRef,有助于我们很好的处理这样的一种图源。接下来,我们来访问下这个CVPixelBufferRef相关的图源数据吧。 函数原型:
CV_EXPORT void * CV_NULLABLE CVPixelBufferGetBaseAddressOfPlane(CVPixelBufferRef CV_NONNULL pixelBuffer, size_t planeIndex ) __OSX_AVAILABLE_STARTING(__MAC_10_4,__IPHONE_4_0);
CV_EXPORT size_t CVPixelBufferGetBytesPerRowOfPlane( CVPixelBufferRef CV_NONNULL pixelBuffer, size_t planeIndex ) __OSX_AVAILABLE_STARTING(__MAC_10_4,__IPHONE_4_0);
CV_EXPORT size_t CVPixelBufferGetWidthOfPlane( CVPixelBufferRef CV_NONNULL pixelBuffer, size_t planeIndex ) __OSX_AVAILABLE_STARTING(__MAC_10_4,__IPHONE_4_0);
CV_EXPORT size_t CVPixelBufferGetHeightOfPlane( CVPixelBufferRef CV_NONNULL pixelBuffer, size_t planeIndex ) __OSX_AVAILABLE_STARTING(__MAC_10_4,__IPHONE_4_0);
示例代码:
void* address = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, n);
size_t bytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, n);
size_t height = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, n);
for (size_t i = 0; i < (n == 0 ? height : height / 2); ++i) {
for (size_t j = 0; j < bytes; ++j) {
}
}
至于CVPixelBufferRef,它本是一个c层的指针数据,不具有oc的内存自动回收机制,需要自己释放内存,释放内存的代码如下:
CVPixelBufferRelease(pixelBuffer);
到此,你已经初步的知道,如何生成CVPixelBufferRef,如何操作CVPixelBufferRef,如何销毁CVPixelBufferRef了。这对于我们后续接入x264软编码器还是很重要的。
VTCompressionSessionRef
既然我们理解了iOS是如何存储图像,那我们就来聊聊iOS的硬编码器。 其实,无论是哪个编码器,都变不了流程上的问题。编码的流程如下:
按着以上的流程,无论是Android编码器,还是软编码器,还是其他的编码器,其根本都是这样的,我们只要能找到相关的流程代码,就可以完成相关流程的代码了。
创建编码器
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) API_AVAILABLE(macosx(10.8), ios(8.0), tvos(10.2));
上面为编码器的创建函数原型,如何使用,可以参考LWVideoEncoder.m里面的createSession函数,下面为具体的代码示例:
void compressSessionOutputCallback(void *opaque,
void *sourceFrameRef,
OSStatus compressStatus,
VTEncodeInfoFlags infoFlags,
CMSampleBufferRef sampleBuf) {
}
long pixelBufferType = self.config.pixelBufferType;
CFDictionaryRef ioSurfaceValue = CFDictionaryCreate(kCFAllocatorDefault,
nil,
nil,
0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CFTypeRef pixelBufferFormatValue = (__bridge CFTypeRef)@(pixelBufferType);
CFTypeRef keys[3] = {
kCVPixelBufferOpenGLESCompatibilityKey, kCVPixelBufferIOSurfacePropertiesKey, kCVPixelBufferPixelFormatTypeKey
};
CFTypeRef values[3] = {
kCFBooleanTrue, ioSurfaceValue, pixelBufferFormatValue
};
CFDictionaryRef sourceAttributes = CFDictionaryCreate(kCFAllocatorDefault, keys, values, 3, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
if (ioSurfaceValue) {
CFRelease(ioSurfaceValue);
ioSurfaceValue = nil;
}
status = VTCompressionSessionCreate(nil,
self.config.width,
self.config.height,
self.config.codecType,
0,
sourceAttributes,
0,
compressSessionOutputCallback,
(__bridge void * _Nullable)(self),
&_encoderSession);
配置编码器
配置编码器则为编码器工作的主要任务,当然,也有一些开发人员需要对编码器内核进行修正,优化,当然,作为端上编码SDK的开发,日常还是调试参数而已。既然说到调试参数,就不得不把iOS硬编码器的所有参数都列举一遍,后面可以好好学习下。
配置变量 | 含义 |
---|
kVTCompressionPropertyKey_RealTime | 实时编码,1为开启,0为关闭。 开启,编码器会以速度为优先考虑,而不考虑画质,在这个模式里面,有可能出现中低码率马赛克问题,但是编码速度相比于关闭有了一点提升,常用于直播行业低延时要求。 关闭,速度上会得到一些限制,但是画质上得到了满足,可以缓解中低码率的马赛克问题,提供画质,常用于端上转码。 | kVTCompressionPropertyKey_AllowFrameReordering | 是否允许B帧编码,开启,则允许,否则,关闭B帧编码 | kVTCompressionPropertyKey_AllowTemporalCompression | 是否允许压缩,默认为是,可以编码P,B帧,否则,只编码I帧 | kVTCompressionPropertyKey_MaxKeyFrameInterval | 可以近似的认为是GOP,即多少个帧之后出来一个I帧 | kVTCompressionPropertyKey_AllowOpenGOP | 开放GOP,允许编码的时候前向参考I帧 | kVTCompressionPropertyKey_NumberOfPendingFrames | 设置缓冲队列的大小,设置为1,则每一帧都实时输出 | kVTCompressionPropertyKey_ProfileLevel | 设置session的profile和level,一般设置为kVTProfileLevel_H264_Baseline_AutoLevel, kVTProfileLevel_H264_Main_AutoLevel, kVTProfileLevel_H264_High_AutoLevel, kVTProfileLevel_HEVC_Main_AutoLevel这几个 | kVTCompressionPropertyKey_ExpectedFrameRate | 期望帧率,这东西,其实没啥用,输出是根据设置的dts/pts设置帧率的 | kVTEncodeFrameOptionKey_ForceKeyFrame | 是否强制为I帧 | kVTCompressionPropertyKey_AverageBitRate | 编码码率配置 |
上面就是目前用过的相关配置,下面给几个简单的示例代码:
VTSessionSetProperty(_encoderSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
VTSessionSetProperty(_encoderSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)@(value));
开始编码
CMTime pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
CMTime duration = CMSampleBufferGetDuration(sampleBuffer);
CVImageBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
VTEncodeInfoFlags infoFlags = 0;
OSStatus status = VTCompressionSessionEncodeFrame(_encoderSession, pixelBuffer, pts, duration, nil, sourceFrameRef, &infoFlags);
if (status != kCVReturnSuccess) {
if (status == kVTInvalidSessionErr) {
[self destroySession];
[self createSession:self.config];
}
return kLWVideoEncodeStatus_Err_Encode;
}
这样子,我们从采集拿到的CMSampleBufferRef就可以直接使用了。编码完毕之后,我们可以在compressSessionOutputCallback里面获取到对应的输出。到此,硬编码的流程就跑通了。
重置编码器
这一块比较常用的应该在设置码率上面,也可以从项目里面看到:
- (void)setBitrate:(int)bitrate {
bitrate = bitrate * 1024;
OSType status = VTSessionSetProperty(_encoderSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(bitrate));
if (status != kCVReturnSuccess) {
NSLog(@"%s:%d error with %d", __func__, __LINE__, status);
}
}
其他的,硬编码只能通过重启编码器来重置相关属性。
销毁编码器
- (void)destroySession {
if (_encoderSession) {
VTCompressionSessionCompleteFrames(_encoderSession, kCMTimeInvalid);
VTCompressionSessionInvalidate(_encoderSession);
CFRelease(_encoderSession);
_encoderSession = nil;
}
NSLog(@"%s:%d", __func__, __LINE__);
}
总结
到此,关于iOS硬编码相关的知识已经全部梳理完毕,小伙伴们快动手做做吧。下一期,我们来讲解下如何提取出输出的数据,转化为AVC或者HEVC,感兴趣的小伙伴也可以直接先动手查阅资料。
相关代码可查询这里
|