使用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指颜色空间。

420f420v都是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 

这里便得到了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的崩溃问题。

文章来源于互联网,如有雷同请联系站长删除:iOS WebRTC 杂谈之 视频采集添加美颜特效

发表评论