本案例主要是利用Metal實(shí)現(xiàn)攝像頭采集內(nèi)容的即刻渲染處理,理解視頻采集、處理及渲染的流程
視頻實(shí)時采集并渲染的效果圖如下,以下效果是由于設(shè)置了高斯模糊濾鏡,其中高斯模糊濾鏡的sigma參數(shù)值越高,圖像越模糊

視頻渲染的實(shí)現(xiàn)思路主要有以下三步
- 1、通過
AVFoundation進(jìn)行視頻數(shù)據(jù)的采集,并將采集到的原始數(shù)據(jù)存儲到CMSampleBufferRef中,即視頻幀數(shù)據(jù)(視頻幀其實(shí)本質(zhì)也是一張圖片) - 2、通過
CoreVideo將CMSampleBufferRef中存儲的圖像數(shù)據(jù),轉(zhuǎn)換為Metal可以直接使用的紋理 - 3、將Metal
紋理進(jìn)行渲染,并即刻顯示到屏幕上
在實(shí)際的開發(fā)應(yīng)用中,
AVFoundation提供了一個layer,即AVCaptureVideoPreviewLayer預(yù)覽層,我們可以使用預(yù)覽層直接預(yù)覽視頻采集后的即可渲染,用于替代思路中2、3步。
根據(jù)官方文檔-AVCaptureVideoPreviewLayer說明,AVCaptureVideoPreviewLayer是CALayer的子類,用于在輸入設(shè)備捕獲視頻時顯示視頻,此預(yù)覽圖層與捕獲會話結(jié)合使用,主要有以下三步:
- 創(chuàng)建預(yù)覽層對象
- 將預(yù)覽層與captureSession鏈接
- 將預(yù)覽層加到view的子layer中
//創(chuàng)建一個預(yù)覽層。
let previewLayer = AVCaptureVideoPreviewLayer()
//將預(yù)覽層與捕獲會話連接。
previewLayer.session = captureSession
//將預(yù)覽圖層添加到視圖的圖層層次結(jié)構(gòu)中
view.layer.addSublayer(previewLayer)
下面來說下視頻渲染的實(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)建命令隊列:通過MTKView中的device創(chuàng)建
self.commandQueue = [self.mtkView.device newCommandQueue];
- 設(shè)置MTKView的讀寫操作 & 創(chuàng)建紋理緩沖區(qū)
- MTKView中的
framebufferOnly屬性,默認(rèn)的幀緩存是只讀的即YES,由于view需要顯示紋理,所以需要該屬性改為可讀寫即NO - 通過
CVMetalTextureCacheCreate方法創(chuàng)建CoreVideo中的metal紋理緩存區(qū),因?yàn)椴杉囊曨l數(shù)據(jù)是通過CoreVideo轉(zhuǎn)換為metal紋理的,主要的用于存儲轉(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)建紋理選項的字典. 使用默認(rèn)選項NULL
參數(shù)5: cacheOut 返回時,包含新創(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)建串行隊列
串行隊列創(chuàng)建的目的在于處理captureSession的交互時,不會影響主隊列,在蘋果官方文檔中有如下圖示,表示captureSession是如何管理設(shè)備的輸入 & 輸出,以及與主隊列之間的關(guān)系
session管理輸入和輸出圖示

self.mProcessQueue = dispatch_queue_create("mProcessQueue", DISPATCH_QUEUE_SERIAL);
- 設(shè)置輸入設(shè)備
- 獲取后置攝像頭設(shè)備
AVCaptureDevice
通過獲取設(shè)備數(shù)組,循環(huán)判斷找到后置攝像頭,將后置攝像頭設(shè)備為當(dāng)前的輸入設(shè)備 - 通過攝像頭設(shè)備創(chuàng)建
AVCaptureDeviceInput
將AVCaptureDevice 轉(zhuǎn)換為 AVCaptureDeviceInput,主要是因?yàn)?AVCaptureSession無法直接使用AVCaptureDevice,所以需要將device轉(zhuǎn)換為deviceInput - 輸入設(shè)備添加到captureSession中
在添加之前,需要通過captureSession的canAddInput函數(shù)判斷是否可以添加輸入設(shè)備,如果可以,則通過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 無法直接使用 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對象,即輸出設(shè)備
- 設(shè)置輸出設(shè)備的
setAlwaysDiscardsLateVideoFrames屬性(表示視頻幀延時使是否丟棄數(shù)據(jù))為NO-
YES:處理現(xiàn)有幀的調(diào)度隊列在captureOutput:didOutputSampleBuffer:FromConnection:Delegate方法中被阻止時,對象會立即丟棄捕獲的幀 -
NO:在丟棄新幀之前,允許委托有更多的時間處理舊幀,但這樣可能會內(nèi)存增加
-
- 設(shè)置輸出設(shè)備的
setVideoSettings屬性(即像素格式),表示每一個像素點(diǎn)顏色保存的格式,且設(shè)置的格式是BGRA,而不是YUV,主要是為了避免Shader轉(zhuǎn)換,如果使用了YUV格式,就需要編寫shader來進(jìn)行顏色格式轉(zhuǎn)換 - 設(shè)置輸出設(shè)備的視頻捕捉輸出的
delegate - 將輸出設(shè)備添加到captureSession中
// 6、創(chuàng)建AVCaptureVideoDataOutput對象,即輸出 & 設(shè)置輸出相關(guān)屬性
self.mCaptureDeviceOutput = [[AVCaptureVideoDataOutput alloc] init];
/*設(shè)置視頻幀延遲到底時是否丟棄數(shù)據(jù).
YES: 處理現(xiàn)有幀的調(diào)度隊列在captureOutput:didOutputSampleBuffer:FromConnection:Delegate方法中被阻止時,對象會立即丟棄捕獲的幀。
NO: 在丟棄新幀之前,允許委托有更多的時間處理舊幀,但這樣可能會內(nèi)存增加.
*/
//視頻幀延遲是否需要丟幀
[self.mCaptureDeviceOutput setAlwaysDiscardsLateVideoFrames:NO];
//設(shè)置像素格式:每一個像素點(diǎn)顏色保存的格式
//這里設(shè)置格式為BGRA,而不用YUV的顏色空間,避免使用Shader轉(zhuǎn)換,如果使用YUV格式,需要編寫shade來進(jìn)行顏色格式轉(zhuǎn)換
//注意:這里必須和后面CVMetalTextureCacheCreateTextureFromImage 保存圖像像素存儲格式保持一致.否則視頻會出現(xiàn)異?,F(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è)置視頻輸出方向
通過AVCaptureConnection鏈接輸入和輸出,并設(shè)置connect的視頻輸出方向,即設(shè)置videoOrientation屬性
// 8、輸入與輸出鏈接
AVCaptureConnection *connection = [self.mCaptureDeviceOutput connectionWithMediaType:AVMediaTypeVideo];
// 9、設(shè)置視頻輸出方向
//注意: 一定要設(shè)置視頻方向.否則視頻會是朝向異常的.
[connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
- 開始捕捉,即開始視頻采集
也可以通過一個按鈕來控制視頻采集的開始與停止-
startRunning:開啟捕捉 -
stopRunning:停止捕捉
-
// 10、開始捕捉
[self.mCaptureSession startRunning];
AVCaptureVideoDataOutputSampleBufferDelegate協(xié)議方法
在視頻采集的同時,采集到的視頻數(shù)據(jù),即視頻幀會自動回調(diào)視頻采集回調(diào)方法captureOutput:didOutputSampleBuffer:fromConnection:,在該方法中處理采集到的原始視頻數(shù)據(jù),將其轉(zhuǎn)換為metal紋理
didOutputSampleBuffer代理方法
主要是獲取視頻的幀數(shù)據(jù),將其轉(zhuǎn)換為metal紋理,函數(shù)流程如下

主要分為以下幾步:
- 從
sampleBuffer中獲取位圖
通過CMSampleBufferGetImageBuffer函數(shù)從sampleBuffer形參中獲取視頻像素緩存區(qū)對象,即視頻幀數(shù)據(jù),平常所說的位圖
// 1、從sampleBuffer 獲取視頻像素緩存區(qū)對象,即獲取位圖
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
- 獲取捕捉視頻幀的寬高
通過CoreVideo中的CVPixelBufferGetWidth和CVPixelBufferGetHeight函數(shù)獲取寬高
size_t width = CVPixelBufferGetWidth(pixelBuffer);
size_t height = CVPixelBufferGetHeight(pixelBuffer);
- 將位圖轉(zhuǎn)換為metal紋理
- 通過
CVMetalTextureRef創(chuàng)建臨時紋理 - 通過
CVMetalTextureCacheCreateTextureFromImage函數(shù)創(chuàng)建metal紋理緩沖區(qū),賦值給臨時紋理 - 判斷臨時紋理是否創(chuàng)建成功,如果臨時紋理創(chuàng)建成功,則繼續(xù)往下執(zhí)行
- 設(shè)置MTKView中的
drawableSize屬性,即表示可繪制紋理的大小 - 通過
CVMetalTextureGetTexture函數(shù),獲取紋理緩沖區(qū)的metal紋理對象 - 釋放臨時紋理
- 通過
// 3、將位圖轉(zhuǎn)換為紋理
//方法來自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ū)對象
參數(shù)3: sourceImage 視頻圖像緩沖區(qū)
參數(shù)4: textureAttributes 紋理參數(shù)字典.默認(rèn)為NULL
參數(shù)5: pixelFormat 圖像緩存區(qū)數(shù)據(jù)的Metal 像素格式常量.注意如果MTLPixelFormatBGRA8Unorm和攝像頭采集時設(shè)置的顏色格式不一致,則會出現(xiàn)圖像異常的情況;
參數(shù)6: width,紋理圖像的寬度(像素)
參數(shù)7: height,紋理圖像的高度(像素)
參數(shù)8: planeIndex 顏色通道.如果圖像緩沖區(qū)是平面的,則為映射紋理數(shù)據(jù)的平面索引。對于非平面圖像緩沖區(qū)忽略。
參數(shù)9: textureOut,返回時,返回創(chuàng)建的Metal紋理緩沖區(qū)。
*/
//創(chuàng)建臨時紋理
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紋理對象
self.texture = CVMetalTextureGetTexture(tmpTexture);
// 7、使用完畢,釋放tmptexture
CFRelease(tmpTexture);
}
MTKViewDelegate協(xié)議方法
接下來就是將獲取的metal紋理即刻渲染并顯示到屏幕上,這里是通過MTKViewDelegate協(xié)議的drawInMTKView代理方法渲染并顯示
drawInMTKView代理方法
MTKView默認(rèn)的幀速率與屏幕刷新頻率一致,所以每當(dāng)屏幕刷新時,都會回調(diào) 視頻采集方法 和 視圖渲染方法,以下是視圖渲染方法執(zhí)行流程

主要有以下幾步
判斷紋理是否獲取成功
即紋理不為空,如果紋理為空,則沒必要執(zhí)行視圖渲染流程通過commandQueue創(chuàng)建commandBuffer命令緩存區(qū)
將MTKView的紋理作為目標(biāo)渲染紋理
即獲取view中紋理對象-
設(shè)置高斯模糊濾鏡
-
MetalPerformanceShaders是Metal的一個集成庫,有一些濾鏡處理的Metal實(shí)現(xiàn), - 此時的濾鏡就等價于Metal中的
MTLRenderCommandEncoder渲染命令編碼器,類似于GLSL中program - 高斯模糊濾鏡在渲染時,會觸發(fā)離屏渲染,且其中的sigma值設(shè)置的越高,圖像越模糊,就如文章開頭展示的效果圖
-
// 4、設(shè)置濾鏡(Metal封裝了一些濾鏡)
//高斯模糊 渲染時,會觸發(fā) 離屏渲染
/*
MetalPerformanceShaders是Metal的一個集成庫,有一些濾鏡處理的Metal實(shí)現(xiàn);
MPSImageGaussianBlur 高斯模糊處理;
*/
//創(chuàng)建高斯濾鏡處理filter
//注意:sigma值可以修改,sigma值越高圖像越模糊;
MPSImageGaussianBlur *filter = [[MPSImageGaussianBlur alloc] initWithDevice:self.mtkView.device sigma:5];
// 5、MPSImageGaussianBlur以一個Metal紋理作為輸入,以一個Metal紋理作為輸出;
//輸入:攝像頭采集的圖像 self.texture
//輸出:創(chuàng)建的紋理 drawingTexture(其實(shí)就是view.currentDrawable.texture)
//filter等價于Metal中的MTLRenderCommandEncoder 渲染命令編碼器,類似于GLSL中的program
[filter encodeToCommandBuffer:commandBuffer sourceTexture:self.texture destinationTexture:drawingTexture];
- 將獲取的紋理顯示到屏幕上
- 將commandBuffer通過
commit提交給GPU - 清空當(dāng)前紋理,為下一次紋理數(shù)據(jù)讀取做準(zhǔn)備
如果不清空,也是可以的,下一次的紋理數(shù)據(jù)會將上次的數(shù)據(jù)覆蓋
// 6、展示顯示的內(nèi)容
[commandBuffer presentDrawable:view.currentDrawable];
// 7、提交命令
[commandBuffer commit];
// 8、清空當(dāng)前紋理,準(zhǔn)備下一次的紋理數(shù)據(jù)讀取,
//如果不清空,也是可以的,下一次的紋理數(shù)據(jù)會將上次的數(shù)據(jù)覆蓋
self.texture = NULL;
總結(jié)
視頻采集流程總結(jié)
根據(jù)上述流程的解析,視頻的采集主要有以下幾步:
- 1、設(shè)置session
- 2、創(chuàng)建串行隊列
- 3、設(shè)置輸入設(shè)備
- 4、設(shè)置輸出設(shè)備
- 5、輸入與輸出鏈接
- 6、設(shè)置視頻輸出方向
- 7、開始捕捉,即開始視頻采集
- 8、
AVCaptureVideoDataOutputSampleBufferDelegate協(xié)議處理采集后的視頻數(shù)據(jù)
如何判斷采集的數(shù)據(jù)是音頻還是視頻?
主要有以下兩種判斷方式:
- 1、通過
AVCaptureConnection判斷-
視頻:包含視頻輸入設(shè)備 & 視頻輸出設(shè)備,通過AVCaptureConnection鏈接起來 -
音頻:包含音頻輸入設(shè)備 & 音頻輸出設(shè)備,同樣通過AVCaptureConnection鏈接起來
-
如果需要判斷當(dāng)前采集的輸出是視頻還是音頻,需要將connect對象設(shè)置為全局變量,然后在采集回調(diào)方法captureOutput:didOutputSampleBuffer:fromConnection:中判斷全局的connection 是否等于 代理方法參數(shù)中的coneection ,如果相等,就是視頻,反之是音頻
- 2、通過
AVCaptureOutput判斷
在采集回調(diào)方法captureOutput:didOutputSampleBuffer:fromConnection:中判斷output形參的類型,如果是AVCaptureVideoDataOutput 類型則是視頻,反之,是音頻
完整的代碼見Github :21_21_Metal_視頻渲染_OC
