本案例主要是利用Metal實(shí)現(xiàn)攝像頭采集內(nèi)容的即刻渲染處理鳖敷,理解視頻采集、處理及渲染的流程
視頻實(shí)時(shí)采集并渲染的效果圖如下牛哺,以下效果是由于設(shè)置了高斯模糊濾鏡达传,其中高斯模糊濾鏡的sigma
參數(shù)值越高篙耗,圖像越模糊
視頻渲染的實(shí)現(xiàn)思路主要有以下三步
- 1、通過(guò)
AVFoundation
進(jìn)行視頻數(shù)據(jù)的采集宪赶,并將采集到的原始數(shù)據(jù)存儲(chǔ)到CMSampleBufferRef
中宗弯,即視頻幀數(shù)據(jù)(視頻幀其實(shí)本質(zhì)也是一張圖片) - 2、通過(guò)
CoreVideo
將CMSampleBufferRef
中存儲(chǔ)的圖像數(shù)據(jù)搂妻,轉(zhuǎn)換為Metal
可以直接使用的紋理
- 3蒙保、將Metal
紋理
進(jìn)行渲染
,并即刻顯示到屏幕上
在實(shí)際的開發(fā)應(yīng)用中欲主,
AVFoundation
提供了一個(gè)layer邓厕,即AVCaptureVideoPreviewLayer
預(yù)覽層,我們可以使用預(yù)覽層直接預(yù)覽視頻采集后的即可渲染扁瓢,用于替代思路中2详恼、3步。
根據(jù)官方文檔-AVCaptureVideoPreviewLayer說(shuō)明引几,AVCaptureVideoPreviewLayer
是CALayer
的子類昧互,用于在輸入設(shè)備捕獲視頻時(shí)顯示視頻
,此預(yù)覽圖層與捕獲會(huì)話
結(jié)合使用,主要有以下三步:
- 創(chuàng)建預(yù)覽層對(duì)象
- 將預(yù)覽層與captureSession鏈接
- 將預(yù)覽層加到view的子layer中
//創(chuàng)建一個(gè)預(yù)覽層敞掘。
let previewLayer = AVCaptureVideoPreviewLayer()
//將預(yù)覽層與捕獲會(huì)話連接叽掘。
previewLayer.session = captureSession
//將預(yù)覽圖層添加到視圖的圖層層次結(jié)構(gòu)中
view.layer.addSublayer(previewLayer)
下面來(lái)說(shuō)下視頻渲染的實(shí)現(xiàn),其整體的實(shí)現(xiàn)流程如圖所示
主要分為3部分
- viewDidLoad函數(shù):初始化Metal和視頻采集的準(zhǔn)備工作
- MTKViewDelegate協(xié)議方法:視頻采集數(shù)據(jù)轉(zhuǎn)換為紋理
- AVCaptureVideoDataOutputSampleBufferDelegate協(xié)議方法:將采集轉(zhuǎn)換后的紋理渲染到屏幕上
viewDidLoad函數(shù)
該函數(shù)中主要是設(shè)置Metal的相關(guān)初始化操作渐逃,以及視頻采集前的準(zhǔn)備工作够掠,函數(shù)的流程如圖所示
分為以下兩部分
- setupMetal函數(shù)
- setupCaptureSession函數(shù)
setupMetal函數(shù)
Metal的準(zhǔn)備工作,主要需要初始化以下3部分
- 初始化MTKView茄菊,用于顯示視頻采集數(shù)據(jù)轉(zhuǎn)換后的紋理
self.mtkView = [[MTKView alloc] initWithFrame:self.view.bounds device:MTLCreateSystemDefaultDevice()];
[self.view insertSubview:self.mtkView atIndex:0];
self.mtkView.delegate = self;
- 創(chuàng)建命令隊(duì)列:通過(guò)MTKView中的device創(chuàng)建
self.commandQueue = [self.mtkView.device newCommandQueue];
- 設(shè)置MTKView的讀寫操作 & 創(chuàng)建紋理緩沖區(qū)
- MTKView中的
framebufferOnly
屬性疯潭,默認(rèn)的幀緩存是只讀
的即YES
,由于view需要顯示紋理面殖,所以需要該屬性改為可讀寫
即NO
- 通過(guò)
CVMetalTextureCacheCreate
方法創(chuàng)建CoreVideo
中的metal紋理緩存區(qū)
竖哩,因?yàn)椴杉囊曨l數(shù)據(jù)是通過(guò)CoreVideo轉(zhuǎn)換為metal紋理的,主要的用于存儲(chǔ)轉(zhuǎn)換后的metal紋理
- MTKView中的
//注意: 在初始化MTKView 的基本操作以外. 還需要多下面2行代碼.
/*
1\. 設(shè)置MTKView 的drawable 紋理是可讀寫的(默認(rèn)是只讀);
2\. 創(chuàng)建CVMetalTextureCacheRef _textureCache; 這是Core Video的Metal紋理緩存
*/
//允許讀寫操作
self.mtkView.framebufferOnly = NO;
/*
CVMetalTextureCacheCreate(CFAllocatorRef allocator,
CFDictionaryRef cacheAttributes,
id <MTLDevice> metalDevice,
CFDictionaryRef textureAttributes,
CVMetalTextureCacheRef * CV_NONNULL cacheOut )
功能: 創(chuàng)建紋理緩存區(qū)
參數(shù)1: allocator 內(nèi)存分配器.默認(rèn)即可.NULL
參數(shù)2: cacheAttributes 緩存區(qū)行為字典.默認(rèn)為NULL
參數(shù)3: metalDevice
參數(shù)4: textureAttributes 緩存創(chuàng)建紋理選項(xiàng)的字典. 使用默認(rèn)選項(xiàng)NULL
參數(shù)5: cacheOut 返回時(shí)脊僚,包含新創(chuàng)建的紋理緩存相叁。
*/
CVMetalTextureCacheCreate(NULL, NULL, self.mtkView.device, NULL, &_textureCache);
setupCaptureSession函數(shù)
初始化視頻采集的準(zhǔn)備工作,以及開始視頻采集辽幌,主要分為以下幾步:
- 設(shè)置
AVCaptureSession
& 視頻采集的分辨率
1增淹、設(shè)置AVCaptureSession & 視頻采集的分辨率
self.mCaptureSession = [[AVCaptureSession alloc] init];
self.mCaptureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
-
創(chuàng)建串行隊(duì)列
串行隊(duì)列創(chuàng)建的目的在于處理captureSession的交互時(shí),不會(huì)影響主隊(duì)列乌企,在蘋果官方文檔中有如下圖示虑润,表示captureSession是如何管理設(shè)備的輸入 & 輸出,以及與主隊(duì)列之間的關(guān)系
self.mProcessQueue = dispatch_queue_create("mProcessQueue", DISPATCH_QUEUE_SERIAL);
- 設(shè)置輸入設(shè)備
- 獲取后置攝像頭設(shè)備
AVCaptureDevice
通過(guò)獲取設(shè)備數(shù)組加酵,循環(huán)判斷找到后置攝像頭拳喻,將后置攝像頭設(shè)備為當(dāng)前的輸入設(shè)備 - 通過(guò)攝像頭設(shè)備創(chuàng)建
AVCaptureDeviceInput
將AVCaptureDevice 轉(zhuǎn)換為 AVCaptureDeviceInput,主要是因?yàn)?AVCaptureSession
無(wú)法直接使用AVCaptureDevice
猪腕,所以需要將device轉(zhuǎn)換為deviceInput - 輸入設(shè)備添加到captureSession中
在添加之前冗澈,需要通過(guò)captureSession
的canAddInput
函數(shù)判斷是否可以添加輸入設(shè)備,如果可以陋葡,則通過(guò)session的addInput
函數(shù)添加輸入設(shè)備
- 獲取后置攝像頭設(shè)備
// 3亚亲、獲取攝像頭設(shè)備(前置/后置攝像頭設(shè)備)
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
AVCaptureDevice *inputCamera = nil;
//循環(huán)設(shè)備數(shù)組,找到后置攝像頭.設(shè)置為當(dāng)前inputCamera
for (AVCaptureDevice *device in devices) {
if ([device position] == AVCaptureDevicePositionBack) {
//拿到后置攝像頭
inputCamera = device;
}
}
// 4、將AVCaptureDevice 轉(zhuǎn)換為 AVCaptureDeviceInput脖岛,即輸入
// AVCaptureSession 無(wú)法直接使用 AVCaptureDevice朵栖,所喲需要將device轉(zhuǎn)換為deviceInput
self.mCaptureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:inputCamera error:nil];
// 5、將設(shè)備添加到captureSession中,需要先判斷能否添加輸入
if ([self.mCaptureSession canAddInput:self.mCaptureDeviceInput]) {
[self.mCaptureSession addInput:self.mCaptureDeviceInput];
}
- 設(shè)置輸出設(shè)備
- 創(chuàng)建AVCaptureVideoDataOutput對(duì)象柴梆,即輸出設(shè)備
- 設(shè)置輸出設(shè)備的
setAlwaysDiscardsLateVideoFrames
屬性(表示視頻幀延時(shí)使是否丟棄數(shù)據(jù))為NO
-
YES
:處理現(xiàn)有幀的調(diào)度隊(duì)列在captureOutput:didOutputSampleBuffer:FromConnection:
Delegate方法中被阻止時(shí),對(duì)象會(huì)立即丟棄
捕獲的幀 -
NO
:在丟棄新幀之前终惑,允許委托有更多的時(shí)間處理舊幀绍在,但這樣可能會(huì)內(nèi)存增加
-
- 設(shè)置輸出設(shè)備的
setVideoSettings
屬性(即像素格式),表示每一個(gè)像素點(diǎn)顏色保存的格式,且設(shè)置的格式是BGRA
偿渡,而不是YUV
臼寄,主要是為了避免Shader轉(zhuǎn)換
,如果使用了YUV
格式溜宽,就需要編寫shader來(lái)進(jìn)行顏色格式轉(zhuǎn)換
- 設(shè)置輸出設(shè)備的視頻捕捉輸出的
delegate
- 將輸出設(shè)備添加到captureSession中
// 6吉拳、創(chuàng)建AVCaptureVideoDataOutput對(duì)象,即輸出 & 設(shè)置輸出相關(guān)屬性
self.mCaptureDeviceOutput = [[AVCaptureVideoDataOutput alloc] init];
/*設(shè)置視頻幀延遲到底時(shí)是否丟棄數(shù)據(jù).
YES: 處理現(xiàn)有幀的調(diào)度隊(duì)列在captureOutput:didOutputSampleBuffer:FromConnection:Delegate方法中被阻止時(shí)适揉,對(duì)象會(huì)立即丟棄捕獲的幀留攒。
NO: 在丟棄新幀之前,允許委托有更多的時(shí)間處理舊幀嫉嘀,但這樣可能會(huì)內(nèi)存增加.
*/
//視頻幀延遲是否需要丟幀
[self.mCaptureDeviceOutput setAlwaysDiscardsLateVideoFrames:NO];
//設(shè)置像素格式:每一個(gè)像素點(diǎn)顏色保存的格式
//這里設(shè)置格式為BGRA炼邀,而不用YUV的顏色空間,避免使用Shader轉(zhuǎn)換剪侮,如果使用YUV格式拭宁,需要編寫shade來(lái)進(jìn)行顏色格式轉(zhuǎn)換
//注意:這里必須和后面CVMetalTextureCacheCreateTextureFromImage 保存圖像像素存儲(chǔ)格式保持一致.否則視頻會(huì)出現(xiàn)異常現(xiàn)象.
[self.mCaptureDeviceOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
//設(shè)置視頻捕捉輸出的代理方法:將采集的視頻數(shù)據(jù)輸出
[self.mCaptureDeviceOutput setSampleBufferDelegate:self queue:self.mProcessQueue];
// 7瓣俯、添加輸出杰标,即添加到captureSession中
if ([self.mCaptureSession canAddOutput:self.mCaptureDeviceOutput]) {
[self.mCaptureSession addOutput:self.mCaptureDeviceOutput];
}
- 輸入與輸出鏈接 & 設(shè)置視頻輸出方向
通過(guò)AVCaptureConnection
鏈接輸入和輸出,并設(shè)置connect的視頻輸出方向彩匕,即設(shè)置videoOrientation
屬性
// 8腔剂、輸入與輸出鏈接
AVCaptureConnection *connection = [self.mCaptureDeviceOutput connectionWithMediaType:AVMediaTypeVideo];
// 9、設(shè)置視頻輸出方向
//注意: 一定要設(shè)置視頻方向.否則視頻會(huì)是朝向異常的.
[connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
- 開始捕捉推掸,即開始視頻采集
也可以通過(guò)一個(gè)按鈕來(lái)控制視頻采集的開始與停止-
startRunning
:開啟捕捉 -
stopRunning
:停止捕捉
-
// 10桶蝎、開始捕捉
[self.mCaptureSession startRunning];
AVCaptureVideoDataOutputSampleBufferDelegate協(xié)議方法
在視頻采集的同時(shí),采集到的視頻數(shù)據(jù)谅畅,即視頻幀會(huì)自動(dòng)回調(diào)視頻采集回調(diào)方法captureOutput:didOutputSampleBuffer:fromConnection:
登渣,在該方法中處理采集到的原始視頻數(shù)據(jù),將其轉(zhuǎn)換為metal紋理
didOutputSampleBuffer代理方法
主要是獲取視頻的幀數(shù)據(jù)毡泻,將其轉(zhuǎn)換為metal紋理胜茧,函數(shù)流程如下
主要分為以下幾步:
- 從
sampleBuffer
中獲取位圖
通過(guò)CMSampleBufferGetImageBuffer
函數(shù)從sampleBuffer
形參中獲取視頻像素緩存區(qū)對(duì)象,即視頻幀數(shù)據(jù)仇味,平常所說(shuō)的位圖
// 1呻顽、從sampleBuffer 獲取視頻像素緩存區(qū)對(duì)象,即獲取位圖
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
- 獲取捕捉視頻幀的寬高
通過(guò)CoreVideo中的CVPixelBufferGetWidth
和CVPixelBufferGetHeight
函數(shù)獲取寬高
size_t width = CVPixelBufferGetWidth(pixelBuffer);
size_t height = CVPixelBufferGetHeight(pixelBuffer);
- 將位圖轉(zhuǎn)換為metal紋理
- 通過(guò)
CVMetalTextureRef
創(chuàng)建臨時(shí)紋理 - 通過(guò)
CVMetalTextureCacheCreateTextureFromImage
函數(shù)創(chuàng)建metal紋理緩沖區(qū)丹墨,賦值給臨時(shí)紋理 - 判斷臨時(shí)紋理是否創(chuàng)建成功廊遍,如果臨時(shí)紋理創(chuàng)建成功,則繼續(xù)往下執(zhí)行
- 設(shè)置MTKView中的
drawableSize
屬性贩挣,即表示可繪制紋理的大小 - 通過(guò)
CVMetalTextureGetTexture
函數(shù)喉前,獲取紋理緩沖區(qū)的metal紋理對(duì)象 - 釋放臨時(shí)紋理
- 通過(guò)
// 3没酣、將位圖轉(zhuǎn)換為紋理
//方法來(lái)自CoreVideo
/*3\. 根據(jù)視頻像素緩存區(qū) 創(chuàng)建 Metal 紋理緩存區(qū)
CVReturn CVMetalTextureCacheCreateTextureFromImage(CFAllocatorRef allocator, CVMetalTextureCacheRef textureCache,
CVImageBufferRef sourceImage,
CFDictionaryRef textureAttributes,
MTLPixelFormat pixelFormat,
size_t width,
size_t height,
size_t planeIndex,
CVMetalTextureRef *textureOut);
功能: 從現(xiàn)有圖像緩沖區(qū)創(chuàng)建核心視頻Metal紋理緩沖區(qū)。
參數(shù)1: allocator 內(nèi)存分配器,默認(rèn)kCFAllocatorDefault
參數(shù)2: textureCache 紋理緩存區(qū)對(duì)象
參數(shù)3: sourceImage 視頻圖像緩沖區(qū)
參數(shù)4: textureAttributes 紋理參數(shù)字典.默認(rèn)為NULL
參數(shù)5: pixelFormat 圖像緩存區(qū)數(shù)據(jù)的Metal 像素格式常量.注意如果MTLPixelFormatBGRA8Unorm和攝像頭采集時(shí)設(shè)置的顏色格式不一致卵迂,則會(huì)出現(xiàn)圖像異常的情況裕便;
參數(shù)6: width,紋理圖像的寬度(像素)
參數(shù)7: height,紋理圖像的高度(像素)
參數(shù)8: planeIndex 顏色通道.如果圖像緩沖區(qū)是平面的,則為映射紋理數(shù)據(jù)的平面索引见咒。對(duì)于非平面圖像緩沖區(qū)忽略偿衰。
參數(shù)9: textureOut,返回時(shí),返回創(chuàng)建的Metal紋理緩沖區(qū)改览。
*/
//創(chuàng)建臨時(shí)紋理
CVMetalTextureRef tmpTexture = NULL;
CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.textureCache, pixelBuffer, NULL, MTLPixelFormatBGRA8Unorm, width, height, 0, &tmpTexture);
// 4下翎、判斷tmpTexture 是否創(chuàng)建成功
if (status == kCVReturnSuccess) {//創(chuàng)建成功
// 5、設(shè)置可繪制紋理的大小
self.mtkView.drawableSize = CGSizeMake(width, height);
// 6恃疯、返回紋理緩沖區(qū)的metal紋理對(duì)象
self.texture = CVMetalTextureGetTexture(tmpTexture);
// 7漏设、使用完畢,釋放tmptexture
CFRelease(tmpTexture);
}
MTKViewDelegate協(xié)議方法
接下來(lái)就是將獲取的metal紋理即刻渲染并顯示到屏幕上今妄,這里是通過(guò)MTKViewDelegate
協(xié)議的drawInMTKView
代理方法渲染并顯示
drawInMTKView代理方法
MTKView默認(rèn)的幀速率與屏幕刷新頻率一致郑口,所以每當(dāng)屏幕刷新時(shí),都會(huì)回調(diào) 視頻采集方法 和 視圖渲染方法盾鳞,以下是視圖渲染方法執(zhí)行流程
主要有以下幾步
判斷紋理是否獲取成功
即紋理不為空犬性,如果紋理為空,則沒必要執(zhí)行視圖渲染流程通過(guò)commandQueue創(chuàng)建commandBuffer命令緩存區(qū)
將MTKView的紋理作為目標(biāo)渲染紋理
即獲取view中紋理對(duì)象-
設(shè)置高斯模糊濾鏡
-
MetalPerformanceShaders
是Metal的一個(gè)集成庫(kù)腾仅,有一些濾鏡處理的Metal實(shí)現(xiàn)乒裆, - 此時(shí)的濾鏡就等價(jià)于Metal中的
MTLRenderCommandEncoder
渲染命令編碼器,類似于GLSL中program
- 高斯模糊濾鏡在渲染時(shí)推励,會(huì)觸發(fā)離屏渲染鹤耍,且其中的sigma值設(shè)置的越高,圖像越模糊验辞,就如文章開頭展示的效果圖
-
// 4稿黄、設(shè)置濾鏡(Metal封裝了一些濾鏡)
//高斯模糊 渲染時(shí),會(huì)觸發(fā) 離屏渲染
/*
MetalPerformanceShaders是Metal的一個(gè)集成庫(kù)跌造,有一些濾鏡處理的Metal實(shí)現(xiàn);
MPSImageGaussianBlur 高斯模糊處理;
*/
//創(chuàng)建高斯濾鏡處理filter
//注意:sigma值可以修改杆怕,sigma值越高圖像越模糊;
MPSImageGaussianBlur *filter = [[MPSImageGaussianBlur alloc] initWithDevice:self.mtkView.device sigma:5];
// 5、MPSImageGaussianBlur以一個(gè)Metal紋理作為輸入壳贪,以一個(gè)Metal紋理作為輸出陵珍;
//輸入:攝像頭采集的圖像 self.texture
//輸出:創(chuàng)建的紋理 drawingTexture(其實(shí)就是view.currentDrawable.texture)
//filter等價(jià)于Metal中的MTLRenderCommandEncoder 渲染命令編碼器,類似于GLSL中的program
[filter encodeToCommandBuffer:commandBuffer sourceTexture:self.texture destinationTexture:drawingTexture];
- 將獲取的紋理顯示到屏幕上
- 將commandBuffer通過(guò)
commit
提交給GPU - 清空當(dāng)前紋理违施,為下一次紋理數(shù)據(jù)讀取做準(zhǔn)備
如果不清空互纯,也是可以的,下一次的紋理數(shù)據(jù)會(huì)將上次的數(shù)據(jù)覆蓋
// 6磕蒲、展示顯示的內(nèi)容
[commandBuffer presentDrawable:view.currentDrawable];
// 7伟姐、提交命令
[commandBuffer commit];
// 8收苏、清空當(dāng)前紋理亿卤,準(zhǔn)備下一次的紋理數(shù)據(jù)讀取愤兵,
//如果不清空,也是可以的排吴,下一次的紋理數(shù)據(jù)會(huì)將上次的數(shù)據(jù)覆蓋
self.texture = NULL;
總結(jié)
視頻采集流程總結(jié)
根據(jù)上述流程的解析秆乳,視頻的采集主要有以下幾步:
- 1、設(shè)置session
- 2钻哩、創(chuàng)建串行隊(duì)列
- 3屹堰、設(shè)置輸入設(shè)備
- 4、設(shè)置輸出設(shè)備
- 5街氢、輸入與輸出鏈接
- 6扯键、設(shè)置視頻輸出方向
- 7、開始捕捉珊肃,即開始視頻采集
- 8荣刑、
AVCaptureVideoDataOutputSampleBufferDelegate
協(xié)議處理采集后的視頻數(shù)據(jù)
如何判斷采集的數(shù)據(jù)是音頻還是視頻?
主要有以下兩種判斷方式:
- 1伦乔、通過(guò)
AVCaptureConnection
判斷-
視頻
:包含視頻輸入設(shè)備 & 視頻輸出設(shè)備厉亏,通過(guò)AVCaptureConnection
鏈接起來(lái) -
音頻
:包含音頻輸入設(shè)備 & 音頻輸出設(shè)備,同樣通過(guò)AVCaptureConnection
鏈接起來(lái)
-
如果需要判斷當(dāng)前采集的輸出是視頻還是音頻烈和,需要將connect對(duì)象設(shè)置為全局變量爱只,然后在采集回調(diào)方法captureOutput:didOutputSampleBuffer:fromConnection:
中判斷全局的connection 是否等于 代理方法參數(shù)中的coneection ,如果相等招刹,就是視頻恬试,反之是音頻
- 2、通過(guò)
AVCaptureOutput
判斷
在采集回調(diào)方法captureOutput:didOutputSampleBuffer:fromConnection:
中判斷output
形參的類型疯暑,如果是AVCaptureVideoDataOutput
類型則是視頻训柴,反之,是音頻
完整的代碼見Github :21_21_Metal_視頻渲染_OC