前言
直播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];
}
複製程式碼
大概思路:
- 取出影像資料CVPixelBufferRef,時間戳CMSampleTimingInfo
- 對CVPixelBufferRef進行美顏處理
- 重新封裝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];
}];
}
複製程式碼
參考:
- stackoverflow.com/questions/2…
- www.jianshu.com/p/dde412cab…
- www.jianshu.com/p/a20995e1a…
- blog.csdn.net/weixin_4243… (GPUImageBeautifyFilter程式碼)