使用WebRTC进行互动直播时,我们希望采集的画面可以添加美颜特效,现有两套解决方案:
方案一的思路是替换WebRTC的原生采集,使用GPUImageVideoCamera替换WebRTC中的视频采集,得到经过GPUImage添加美颜处理后的图像,发送给WebRTC的OnFrame方法。
方案二的思路是拿到WebRTC采集的原始视频帧数据,然后传给GPUImage库进行处理,最后把经过处理的视频帧传回WebRTC。
通过查阅WebRTC源码发现,WebRTC原生采集和后续处理的图像格式是NV12(YUV的一种),而GPUImage处理后的Pixel格式为BGRA,因此无论使用方案一还是方案二都需要进行像素格式转换。下面来介绍方案一的实现方法(方案二和方案一并无本质区别,可参考方案一的实现思路)。
在实现该方案前,我们先介绍几个必须掌握的知识:
#1. iOS中的像素帧格式梳理
iOS视频采集支持三种数据格式输出:420v,420f,BGRA。
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange = '420v', /* Bi-Planar Component Y'CbCr 8-bit 4:2:0, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct */
kCVPixelFormatType_420YpCbCr8BiPlanarFullRange = '420f', /* Bi-Planar Component Y'CbCr 8-bit 4:2:0, full-range (luma=[0,255] chroma=[1,255]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct */
kCVPixelFormatType_32BGRA = 'BGRA', /* 32 bit BGRA */
iOS系统像素格式名称说明:
kCVPixelFormatType_{长度|序列}{颜色空间}{'Planar'|'BiPlanar'}{'VideoRange'|'FullRange'}
以
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
为例,YpCbCr分别指Y、U、V三个分量,即YUV格式的数据,后面的8指以8bit来保存一个分量,420指使用YUV的4:2:0格式存储。BiPlanar指双平面模式,即将Y和UV分开存储,VideoRange指颜色空间。
420f
和420v
都是YUV格式的。YUV是一种颜色编码方法,分为三个分量,Y表示亮度(Luma),也称为灰度。U和V表示色度(chroma)描述色彩与饱和度。YUV的存储格式分为两大类:planar和packed。planar(平面)先连续存储所有像素点的Y,然后存储所有像素点的U,随后是所有像素点的V。packed是将每个像素点的Y,U,V交叉存储的。 我们最终需要的,用于WebRTC编解码的像素格式是kCVPixelFormatType_420YpCbCr8BiPlanarFullRange的,即双平面的YUV420,Y和UV分开存储,这对后面我们的格式转换非常重要。
420f和420v的区别在于Color Space。f指Full Range,v指Video Range。
Full Range的Y分量取值范围是[0,255]
Video Range的Y分量取值范围是[16,235]
从采集编码到解码渲染,整个过程中,颜色空间的设置都必须保持一致,如果采集用了Full Range 而播放端用Video Range,那么就有可能看到曝光过度的效果。
BRGA是RGB三个通道加上alpha通道,颜色空间对应的就是它在内存中的顺序。比如kCVPixelFormatType_32BGRA,内存中的顺序是 B G R A B G R A...。
各种编码器最适合编码的格式是YUV的NV12格式,因为其不需要像RGB一样占用三个通道,在传输过程中就节省了很多流量。并且NV12可以将图像与颜色分离,可以兼容黑白电视的显示。WebRTC处理的也正是这种格式。
#2. 大小端模式
大端模式(Big-endian),是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。
小端模式(Little-endian),是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。
iOS采用的是小端存储。
#3. libYUV格式转换库
LibYUV是Google开源的实现各种YUV与RGB之间相互转换、旋转、缩放的库。
上面提到WebRTC使用的图像格式为NV12,而通过GPUImage采集到的图像格式为BGRA,因此,就需要做BGRA→NV12的转换。
iOS中采用的是小端模式, libyuv中的格式都是大端模式。小端为BGRA,那么大端就是ARGB,所以我们使用libyuv::ARGBToNV12。
下面介绍方案一的具体实现:
1. 替换WebRTC的原生采集为GPUImage采集,得到经过GPUImage处理好的BGRA格式pixel;
修改avfoundationvideocapturer.mm
中的- (BOOL)setupCaptureSession
方法,启动GPUImage采集,在回调中拿到BGRA格式的CMSampleBuffer。并修改- (void)start
和- (void)stop
,确保采集的启停功能正常。
这里便得到了添加美颜等特效的BGRA源视频帧数据。
2. 使用ARGBToNV12将BGRA转换成NV12;
先获取BGRA格式的pixelBuffer首地址,并创建转换后NV12格式的内存地址*dstBuff,使用libyuv::ARGBToNV12进行转换,最终我们得到了存储NV12数据的内存地址dstBuff。
// 获取BGRA格式的pixel首地址
void *srcBuff = CVPixelBufferGetBaseAddress(pixelBuffer);
// 创建转换后NV12格式的pixel内存地址
unsigned char *dstBuff = (unsigned char *)malloc(total_size);
// 转换
ARGBToNV12(srcBuff, (int)bytesPerRow, dstBuff, (int)width, dstBuff + y_size, (int)width, (int)width, (int)height);
3. 创建NV12格式的CVPixelBufferRef NV12_pixel_buffer:
CVPixelBufferRef NV12_pixel_buffer = NULL;
NSDictionary *pixelBufferAttributes = @{ (NSString *)kCVPixelBufferIOSurfacePropertiesKey : @{} };
CVPixelBufferCreate(kCFAllocatorDefault,
width,
height,
kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
(__bridge CFDictionaryRef)(pixelBufferAttributes),
&NV12_pixel_buffer);
pixelBufferAttributes
这个参数是optional的,但是却非常重要。它指定创建时是否使用IOSurface框架,有无这个参数最后创建的Pixelbuffer略有不同,经测试,如不写这个参数,在iOS13中将无法创建正常可用的pixelBufferRef。
4. 使用memcpy将dstBuff的数据逐行拷贝到NV12_pixel_buffer:
上面提到,NV12是双平面的YUV420格式,即在dstBuff中Y和UV分开存储,因此我们需要分别逐行拷贝Y和UV。
注意:
在操作CVPixelBuffer之前,一定要记得先进行加锁,防止读写操作同时进行。
CVPixelBufferLockBaseAddress(NV12_pixel_buffer, 0);
//process buffer
CVPixelBufferUnlockBaseAddress(NV12_pixel_buffer, 0);
以UV拷贝为例:
//memcpy UV
size_t bytesPerRow_UV = CVPixelBufferGetBytesPerRowOfPlane(NV12_pixel_buffer, 1);
// long width_UV = CVPixelBufferGetWidthOfPlane(NV12_pixel_buffer, 1);
long height_UV = CVPixelBufferGetHeightOfPlane(NV12_pixel_buffer, 1);
uint8_t *dst_UV = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(NV12_pixel_buffer, 1);
memset(dst_UV, 0x80, height_UV * bytesPerRow_UV);
uint8_t *dstBuff_UV = dstBuff + y_size;
for (int row = 0; row < height_UV; ++row) {
memcpy(dst_UV + row * bytesPerRow_UV,
dstBuff_UV + row * width_Y,
width_Y);
}
这里便得到了NV12格式CVPixelBuffer。
5. 用生成的NV12_pixel_buffer,创建CMSampleBuffer:
最终交付给WebRTC处理的是CMSampleBuffer,因此我们需要做CVPixelBuffer→CMSampleBuffer的转换:
CVPixelBufferLockBaseAddress(yuv_pixel_buffer, 0);
CMVideoFormatDescriptionRef video_format = NULL;
OSStatus ret=CMVideoFormatDescriptionCreateForImageBuffer(NULL,
yuv_pixel_buffer,
&video_format);
if (ret!=noErr) {
NSLog(@"webrtc: video format create error:%d",(int)ret);
}
CMTime frameTime = CMSampleBufferGetDuration(sampleBuffer);
CMSampleTimingInfo timing_info = {frameTime,frameTime,kCMTimeInvalid};
CMSampleBufferRef videoSampleBuffer = NULL;
ret=CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault,
yuv_pixel_buffer,
YES,
NULL,
NULL,
video_format,
&timing_info,
&videoSampleBuffer);
if (ret!=noErr) {
NSLog(@"webrtc: videoSampleBuffer create error:%d",(int)ret);
}
CVPixelBufferUnlockBaseAddress(yuv_pixel_buffer, 0);
这里就得到了可用于WebRTC的经过GPUImage处理的CMSampleBuffer,然后将CMSampleBuffer传给WebRTC的OnFrame方法即可。
到这里就完成了为WebRTC的视频添加美颜等特效。其中的坑还是要自己踩过才印象深刻。其中要着重注意iOS13的崩溃问题。