iOS WebRTC 杂谈之 视频采集添加美颜特效

使用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 < 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的崩溃问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容