Metal 案例05:視頻采集 & 實(shí)時渲染

OpenGL + OpenGL ES +Metal 系列文章匯總

本案例主要是利用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、通過CoreVideoCMSampleBufferRef中存儲的圖像數(shù)據(jù),轉(zhuǎn)換為Metal可以直接使用的紋理
  • 3、將Metal紋理進(jìn)行渲染,并即刻顯示到屏幕上

在實(shí)際的開發(fā)應(yīng)用中,AVFoundation提供了一個layer,即AVCaptureVideoPreviewLayer 預(yù)覽層,我們可以使用預(yù)覽層直接預(yù)覽視頻采集后的即可渲染,用于替代思路中2、3步。
根據(jù)官方文檔-AVCaptureVideoPreviewLayer說明,AVCaptureVideoPreviewLayerCALayer 的子類,用于在輸入設(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ù)的流程如圖所示


viewDidLoad函數(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 的基本操作以外. 還需要多下面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管理輸入和輸出圖示
關(guān)于串行隊列在官方文檔中的描述
 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中
      在添加之前,需要通過captureSessioncanAddInput函數(shù)判斷是否可以添加輸入設(shè)備,如果可以,則通過session的addInput函數(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ù)流程如下

didOutputSampleBuffer代理方法流程

主要分為以下幾步:

  • sampleBuffer中獲取位圖
    通過CMSampleBufferGetImageBuffer函數(shù)從sampleBuffer形參中獲取視頻像素緩存區(qū)對象,即視頻幀數(shù)據(jù),平常所說的位圖
//    1、從sampleBuffer 獲取視頻像素緩存區(qū)對象,即獲取位圖
    CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
  • 獲取捕捉視頻幀的寬高
    通過CoreVideo中的CVPixelBufferGetWidthCVPixelBufferGetHeight函數(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í)行流程

drawInMTKView代理方法流程

主要有以下幾步

  • 判斷紋理是否獲取成功
    即紋理不為空,如果紋理為空,則沒必要執(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容