iOS WebRTC 雜談之 視頻采集添加美顏特效

使用WebRTC進(jìn)行互動(dòng)直播時(shí),我們希望采集的畫(huà)面可以添加美顏特效,現(xiàn)有兩套解決方案:

方案一的思路是替換WebRTC的原生采集,使用GPUImageVideoCamera替換WebRTC中的視頻采集,得到經(jīng)過(guò)GPUImage添加美顏處理后的圖像,發(fā)送給WebRTC的OnFrame方法。
方案二的思路是拿到WebRTC采集的原始視頻幀數(shù)據(jù),然后傳給GPUImage庫(kù)進(jìn)行處理,最后把經(jīng)過(guò)處理的視頻幀傳回WebRTC。

通過(guò)查閱WebRTC源碼發(fā)現(xiàn),WebRTC原生采集和后續(xù)處理的圖像格式是NV12(YUV的一種),而GPUImage處理后的Pixel格式為BGRA,因此無(wú)論使用方案一還是方案二都需要進(jìn)行像素格式轉(zhuǎn)換。下面來(lái)介紹方案一的實(shí)現(xiàn)方法(方案二和方案一并無(wú)本質(zhì)區(qū)別,可參考方案一的實(shí)現(xiàn)思路)。

在實(shí)現(xiàn)該方案前,我們先介紹幾個(gè)必須掌握的知識(shí):

#1. iOS中的像素幀格式梳理

iOS視頻采集支持三種數(shù)據(jù)格式輸出: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系統(tǒng)像素格式名稱說(shuō)明:

kCVPixelFormatType_{長(zhǎng)度|序列}{顏色空間}{'Planar'|'BiPlanar'}{'VideoRange'|'FullRange'}

kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange為例,YpCbCr分別指Y、U、V三個(gè)分量,即YUV格式的數(shù)據(jù),后面的8指以8bit來(lái)保存一個(gè)分量,420指使用YUV的4:2:0格式存儲(chǔ)。BiPlanar指雙平面模式,即將Y和UV分開(kāi)存儲(chǔ),VideoRange指顏色空間。

420f420v都是YUV格式的。YUV是一種顏色編碼方法,分為三個(gè)分量,Y表示亮度(Luma),也稱為灰度。U和V表示色度(chroma)描述色彩與飽和度。YUV的存儲(chǔ)格式分為兩大類:planar和packed。planar(平面)先連續(xù)存儲(chǔ)所有像素點(diǎn)的Y,然后存儲(chǔ)所有像素點(diǎn)的U,隨后是所有像素點(diǎn)的V。packed是將每個(gè)像素點(diǎn)的Y,U,V交叉存儲(chǔ)的。 我們最終需要的,用于WebRTC編解碼的像素格式是kCVPixelFormatType_420YpCbCr8BiPlanarFullRange的,即雙平面的YUV420,Y和UV分開(kāi)存儲(chǔ),這對(duì)后面我們的格式轉(zhuǎn)換非常重要。

420f和420v的區(qū)別在于Color Space。f指Full Range,v指Video Range。
Full Range的Y分量取值范圍是[0,255]
Video Range的Y分量取值范圍是[16,235]
從采集編碼到解碼渲染,整個(gè)過(guò)程中,顏色空間的設(shè)置都必須保持一致,如果采集用了Full Range 而播放端用Video Range,那么就有可能看到曝光過(guò)度的效果。

BRGA是RGB三個(gè)通道加上alpha通道,顏色空間對(duì)應(yīng)的就是它在內(nèi)存中的順序。比如kCVPixelFormatType_32BGRA,內(nèi)存中的順序是 B G R A B G R A...。

各種編碼器最適合編碼的格式是YUV的NV12格式,因?yàn)槠洳恍枰馬GB一樣占用三個(gè)通道,在傳輸過(guò)程中就節(jié)省了很多流量。并且NV12可以將圖像與顏色分離,可以兼容黑白電視的顯示。WebRTC處理的也正是這種格式。

#2. 大小端模式

大端模式(Big-endian),是指數(shù)據(jù)的高字節(jié)保存在內(nèi)存的低地址中,而數(shù)據(jù)的低字節(jié)保存在內(nèi)存的高地址中,這樣的存儲(chǔ)模式有點(diǎn)兒類似于把數(shù)據(jù)當(dāng)作字符串順序處理:地址由小向大增加,而數(shù)據(jù)從高位往低位放;這和我們的閱讀習(xí)慣一致。
小端模式(Little-endian),是指數(shù)據(jù)的高字節(jié)保存在內(nèi)存的高地址中,而數(shù)據(jù)的低字節(jié)保存在內(nèi)存的低地址中,這種存儲(chǔ)模式將地址的高低和數(shù)據(jù)位權(quán)有效地結(jié)合起來(lái),高地址部分權(quán)值高,低地址部分權(quán)值低。
iOS采用的是小端存儲(chǔ)。

#3. libYUV格式轉(zhuǎn)換庫(kù)

LibYUV是Google開(kāi)源的實(shí)現(xiàn)各種YUV與RGB之間相互轉(zhuǎn)換、旋轉(zhuǎn)、縮放的庫(kù)。
上面提到WebRTC使用的圖像格式為NV12,而通過(guò)GPUImage采集到的圖像格式為BGRA,因此,就需要做BGRA→NV12的轉(zhuǎn)換。

iOS中采用的是小端模式, libyuv中的格式都是大端模式。小端為BGRA,那么大端就是ARGB,所以我們使用libyuv::ARGBToNV12。

下面介紹方案一的具體實(shí)現(xiàn):

1. 替換WebRTC的原生采集為GPUImage采集,得到經(jīng)過(guò)GPUImage處理好的BGRA格式pixel;

修改avfoundationvideocapturer.mm中的- (BOOL)setupCaptureSession方法,啟動(dòng)GPUImage采集,在回調(diào)中拿到BGRA格式的CMSampleBuffer。并修改- (void)start- (void)stop,確保采集的啟停功能正常。
這里便得到了添加美顏等特效的BGRA源視頻幀數(shù)據(jù)。

2. 使用ARGBToNV12將BGRA轉(zhuǎn)換成NV12;

先獲取BGRA格式的pixelBuffer首地址,并創(chuàng)建轉(zhuǎn)換后NV12格式的內(nèi)存地址*dstBuff,使用libyuv::ARGBToNV12進(jìn)行轉(zhuǎn)換,最終我們得到了存儲(chǔ)NV12數(shù)據(jù)的內(nèi)存地址dstBuff。

// 獲取BGRA格式的pixel首地址
void *srcBuff = CVPixelBufferGetBaseAddress(pixelBuffer);
// 創(chuàng)建轉(zhuǎn)換后NV12格式的pixel內(nèi)存地址
unsigned char *dstBuff = (unsigned char *)malloc(total_size);
// 轉(zhuǎn)換
ARGBToNV12(srcBuff, (int)bytesPerRow, dstBuff, (int)width, dstBuff + y_size, (int)width, (int)width, (int)height);
3. 創(chuàng)建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這個(gè)參數(shù)是optional的,但是卻非常重要。它指定創(chuàng)建時(shí)是否使用IOSurface框架,有無(wú)這個(gè)參數(shù)最后創(chuàng)建的Pixelbuffer略有不同,經(jīng)測(cè)試,如不寫(xiě)這個(gè)參數(shù),在iOS13中將無(wú)法創(chuàng)建正??捎玫膒ixelBufferRef。

4. 使用memcpy將dstBuff的數(shù)據(jù)逐行拷貝到NV12_pixel_buffer:

上面提到,NV12是雙平面的YUV420格式,即在dstBuff中Y和UV分開(kāi)存儲(chǔ),因此我們需要分別逐行拷貝Y和UV。
注意:在操作CVPixelBuffer之前,一定要記得先進(jìn)行加鎖,防止讀寫(xiě)操作同時(shí)進(jìn)行。

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,創(chuàng)建CMSampleBuffer:

最終交付給WebRTC處理的是CMSampleBuffer,因此我們需要做CVPixelBuffer→CMSampleBuffer的轉(zhuǎn)換:

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的經(jīng)過(guò)GPUImage處理的CMSampleBuffer,然后將CMSampleBuffer傳給WebRTC的OnFrame方法即可。
到這里就完成了為WebRTC的視頻添加美顏等特效。其中的坑還是要自己踩過(guò)才印象深刻。其中要著重注意iOS13的崩潰問(wèn)題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容