使用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指顏色空間。
420f和420v都是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)題。