直播SDK加入GPU自定義美顏

ChokWah發表於2019-03-02

前言

直播SDK提供了預設美顏,效果一般,不支援Mac。專案要求能自定義美顏,並支援Mac,iOS雙端。找了好多的文章,試了都不行,谷歌了幾篇文章,加上自己摸索,湊起來總算搞通了。寫下來給要接直播美顏SDK的夥伴捋一捋,PS:玩音視訊的各路大神別見笑。

瞭解情況

查了SDK暴露出來的介面,跟系統AVFoundation的回撥方法一致,直播SDK提供了Block回撥,返回封裝好的CMSampleBufferRef資料,然後在Block裡把CMSampleBufferRef資料傳輸到SDK內部方法推流。

- (void)processVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer {
  // do something 
 [[SDK sharedSDK].netCallManager sendVideoSampleBuffer:sampleBuffer];
}
複製程式碼

大概思路:

  1. 取出影像資料CVPixelBufferRef,時間戳CMSampleTimingInfo
  2. 對CVPixelBufferRef進行美顏處理
  3. 重新封裝CMSampleBufferRef傳送到SDK方法內推流

採取的美顏濾鏡(非第三方SDK,支援Mac,iOS):

  • iOS自帶的CIFilter (單一濾鏡多,貌似沒現成組合美顏濾鏡)
  • 強大的GPUImage(有現成組合過成的GPUImageBeautifyFilter美顏濾鏡,且Demo有各種效果預覽)

GPUImage用法 input -》do something -》output

網上很多關於GPUImage的用法是對視訊檔案,或者直接用Camera作為輸入源。缺少直接對一幀影像進行處理,所以問題在於如何對CVPixelBufferRef進行濾鏡處理,然後得到處理完的CVPixelBufferRef資料。對為此特意查閱很多資料(參考的網址在最後),並翻了翻GPUImage的程式碼。

  • GPUImage Framework的檔案很清晰,Sources輸入源,Filters是各種濾鏡,Outputs輸出源。

    用法基本可以列為管道式,addTarget 相當於 -》: 
    [Sources初始化物件 addTarget:Filter]; // 輸入源 -》濾鏡
    [filter addTarget:output輸出源物件];  // 濾鏡 -》輸出源	
    複製程式碼
  • 輸入源採用GPUImageMovie,因為裡面有一個- (void)processMovieFrame:(CMSampleBufferRef)movieSampleBuffer方法,支援輸入一幀進行處理。(注意初始化Movie一定要用initWithAsset的方法,傳入nil,否則內部的GPUImageContext不會初始化)

  • 濾鏡採取已經組合成美顏濾鏡的GPUImageBeautifyFilter,它是一個FilterGroup。

  • 輸出的時候,使用GPUImageRawDataOutput,newFrameAvailableBlock裡取出影像資料,螢幕一片灰色。如果用GPUImageView直接addSubview顯示,能成功顯示用濾鏡處理過的畫面。

證明資料已經成功處理,在GPUImageMovie的processMovieFrame方法裡也找到newFrameAvailableBlock的回撥,回撥確實有執行。

問題則是我在lockFramebufferForReading和unlockFramebufferAfterReading中間讀取幀資料的操作不對,想到這邊讀資料這麼複雜,就想著這個渲染完的資料到底放在哪裡了,有沒有別的辦法取出?

在前面說過GPUImageBeautifyFilter是一個GPUImageFilterGroup,GPUImageFilterGroup的父類是GPUImageOutput,這就意味著,即使我不新增任何output的target,資料也是可以拿到的。

再翻了下資料,GPUImageOutput的標頭檔案,發現一個frameProcessingCompletionBlock,這個跟我原來的processMovieFrame好像很對應。網上也有說怎麼從GPUImageOutput取出幀資料。那就行了,直接從GPUImageBeautifyFilter裡取,不用設定output的Target,大功告成。

程式碼程式碼程式碼

**說了一堆廢話,不好意思。獻上程式碼:**GPUImageBeautifyFilter程式碼在參考連結的最後一個

    // 初始化Filter和GPUImageMovie
    _beautifyFilter = [[GPUImageBeautifyFilter alloc] init];
	_gpumovie = [[GPUImageMovie alloc] initWithAsset:nil]; // 初始化內部資料結構
	[_gpumovie addTarget:_beautifyFilter]; //連線過濾器
複製程式碼

// 重新封裝CMSampleBufferRef,並交給SDK推流
// 如果是rtmp協議傳輸視訊流,自己用VideoToolBox封裝。
- (void)sendVideoSampleBuffer:(CVPixelBufferRef)bufferRef time:(CMSampleTimingInfo)timingInfo  {
       CMSampleBufferRef newSampleBuffer = NULL;
       CMFormatDescriptionRef outputFormatDescription = NULL;
       CMVideoFormatDescriptionCreateForImageBuffer( kCFAllocatorDefault, bufferRef, &outputFormatDescription );
	   OSStatus err = CMSampleBufferCreateForImageBuffer( kCFAllocatorDefault, bufferRef, true, NULL, NULL, outputFormatDescription, &timingInfo, &newSampleBuffer );
       if(newSampleBuffer) {
          [[SDK sharedSDK].netCallManager sendVideoSampleBuffer:newSampleBuffer];
       }else {
          NSString *exceptionReason = [NSString stringWithFormat:@"sample buffer create failed (%i)", (int)err];
          @throw [NSException exceptionWithName:NSInvalidArgumentException reason:exceptionReason userInfo:nil];
       }
}
複製程式碼

最重要的處理部分:

// 此方法由直播SDK負責回撥或者代理執行
// 自定義美顏時最好把直播SDK預設美顏關閉
- (void)processVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer {

    CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
	//取出幀資料/時間戳 -》處理幀資料美顏 -》根據時間戳與畫素資料重新封裝包:
    [_gpumovie processMovieFrame:sampleBuffer]; // kCVPixelFormatType_420YpCbCr8BiPlanarFullRange 可能需要檢查

    CMSampleTimingInfo timingInfo = {
		.duration               = CMSampleBufferGetDuration(sampleBuffer),
		.presentationTimeStamp  =   CMSampleBufferGetPresentationTimeStamp(sampleBuffer),
        .decodeTimeStamp        = CMSampleBufferGetDecodeTimeStamp(sampleBuffer)
    };
    __weak typeof(self) weakSelf = self;
    [_beautifyFilter setFrameProcessingCompletionBlock:^(GPUImageOutput *output, CMTime time) {
 		GPUImageFramebuffer *imageFramebuffer = output.framebufferForOutput;
  		glFinish();
		[weakSelf sendVideoSampleBuffer: [imageFramebuffer getRenderTarget] time:timingInfo];
	 }];
}
複製程式碼

參考:

相關文章