引言
在移動應用中,實時視頻處理已成為視頻拍攝、短視頻、直播、美顏相機等功能的核心技術之一。從簡單的濾鏡疊加,到復雜的美顏、AR 特效,背后都離不開對每一幀圖像的高效采集與處理。在前幾篇文章中,我們已經實現了基本的視頻采集、人臉識別等功能,而本篇將邁出更進一步的一步 ——?實時處理每一幀圖像,打造動態視覺效果。
本篇內容將圍繞?AVCaptureVideoDataOutput?展開,講解如何獲取原始視頻幀,并借助 CoreImage 或 Metal 實現濾鏡、美顏等實時圖像處理效果。最后,我們還將以一個“實時美顏相機”為示例,串聯起采集、處理與渲染的完整流程,幫助你搭建具備實用價值的實時視頻處理系統。
采集視頻幀:AVCaptureVideoDataOutput
在使用 AVFoundation 進行圖像采集時,無論是拍照、錄像、還是視頻幀處理,整體的配置流程幾乎一致。我們依然需要:
- 創建?AVCaptureSession。
- 添加輸入設備(通常是攝像頭)。
- 添加輸出對象。
- 啟動會話。
唯一的區別在于?輸出類型的不同。在拍照場景中,我們使用的是?AVCapturePhotoOutput;錄制視頻則使用?AVCaptureMovieFileOutput。而本篇重點關注的實時視頻幀處理,需要使用的是:AVCaptureVideoDataOutput。
AVCaptureVideoDataOutput?負責將攝像頭捕捉到的原始幀(CVPixelBuffer)逐幀輸出給我們,這種輸出是“實時的”,每一幀都會通過代理方法交付給我們進行處理,非常適合用于:
- 添加濾鏡
- 美顏處理
- 實時圖像識別
類的基本結構
我們先來看一下?PHCaptureVideoController 的基本結構:
import UIKit
import AVFoundationclass PHCaptureVideoController: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {/// 會話let session = AVCaptureSession()/// 輸出private let videoDataOutput = AVCaptureVideoDataOutput()/// 輸入private var captureDeviceInput: AVCaptureDeviceInput?/// 隊列private let sessionQueue = DispatchQueue(label: "com.example.captureSession")/// 代理weak var delegate: PHCaptureProtocol?/// 配置會話func setupConfigureSession() {session.beginConfiguration()// 1.設置會話預設setupSessionPreset()// 2.設置會話輸入if !setupSessionInput(device: self.getDefaultCameraDevice()) {delegate?.captureError(NSError(domain: "PHCaptureController", code: 1001, userInfo: [NSLocalizedDescriptionKey: "Failed to add input"]))return}// 3.設置會話輸出if !setupSessionOutput() {delegate?.captureError(NSError(domain: "PHCaptureController", code: 1002, userInfo: [NSLocalizedDescriptionKey: "Failed to add output"]))return}session.commitConfiguration()}/// 設置會話話預設private func setupSessionPreset() {session.sessionPreset = .high}/// 設置會話輸入private func setupSessionInput(device: AVCaptureDevice? = nil) -> Bool {return true}/// 設置會話輸出private func setupSessionOutput() -> Bool {return true}/// 啟動會話func startSession() {}/// 停止會話func stopSession() {}//MARK: private/// 獲取默認攝像頭private func getDefaultCameraDevice() -> AVCaptureDevice? {return getCameraDevice(position: .back)}/// 獲取指定攝像頭private func getCameraDevice(position: AVCaptureDevice.Position) -> AVCaptureDevice? {let devices = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devicesreturn devices.first}//MARK: - AVCaptureVideoDataOutputSampleBufferDelegate/// 捕獲輸出func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {// 處理視頻數據delegate?.captureVideo(sampleBuffer)}/// 捕獲輸出丟失func captureOutput(_ output: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {}}
整個類中劃分為了幾個方法:
- 配置會話。
- 設置會話輸入。
- 設置會話輸出。
- 啟動會話、停止會話。
- 捕捉到視頻幀的回調、丟失視頻幀的回調。
配置會話
我們在?setupConfigureSession 方法中需要完成三個操作:
- 設置會話預設。
- 添加攝像頭輸入。
- 添加照片輸出。
并且保證這些操作需要在會話?beginConfiguration 與?commitConfiguration 方法之間執行。
/// 配置會話func setupConfigureSession() {session.beginConfiguration()// 1.設置會話預設setupSessionPreset()// 2.設置會話輸入if !setupSessionInput(device: self.getDefaultCameraDevice()) {delegate?.captureError(NSError(domain: "PHCaptureController", code: 1001, userInfo: [NSLocalizedDescriptionKey: "Failed to add input"]))return}// 3.設置會話輸出if !setupSessionOutput() {delegate?.captureError(NSError(domain: "PHCaptureController", code: 1002, userInfo: [NSLocalizedDescriptionKey: "Failed to add output"]))return}session.commitConfiguration()}
/// 設置會話話預設private func setupSessionPreset() {session.sessionPreset = .high}
會話輸入
添加攝像頭設備,并包裝一層 AVCaptureDeviceInput 添加到會話中。
/// 設置會話輸入private func setupSessionInput(device: AVCaptureDevice? = nil) -> Bool {// 1.獲取攝像頭guard let device = device else { return false }do {captureDeviceInput = try AVCaptureDeviceInput(device: device)if session.canAddInput(captureDeviceInput!) {session.addInput(captureDeviceInput!)} else {return false}} catch {delegate?.captureError(error)return false}return true}
會話輸出
會話輸出使用AVCaptureVideoDataOutput輸出,設置像素合適以及檢查是否可以添加。
/// 設置會話輸出
private func setupSessionOutput() -> Bool {videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]videoDataOutput.setSampleBufferDelegate(self, queue: sessionQueue)if session.canAddOutput(videoDataOutput) {session.addOutput(videoDataOutput)} else {return false}return true
}
- 設置像素格式:我們選擇?kCVPixelFormatType_32BGRA,這是 CoreImage 和 Metal 最常用、兼容性最強的格式;
- 設置代理與處理隊列:setSampleBufferDelegate(_:queue:)?會將每一幀回調給你指定的隊列處理,避免阻塞主線程;
- 檢查輸出是否可添加:通過?canAddOutput?判斷 session 是否支持添加該輸出類型,確保穩定性。
啟動、停止會話
在自定義的串行隊列中執行啟動和停止會話。
/// 啟動會話func startSession() {sessionQueue.async {if !self.session.isRunning {self.session.startRunning()}}}/// 停止會話func stopSession() {sessionQueue.async {if self.session.isRunning {self.session.stopRunning()}}}
視頻幀數據處理
接下來我需要實現AVCaptureVideoDataOutputSampleBufferDelegate的代理方法,并處理回調中的視頻數據。
回調方法
AVCaptureVideoDataOutputSampleBufferDelegate提供了兩個代理方法,一個用于捕獲實時輸出的視頻幀數據,一個用來捕獲丟失的幀數據。
我們在捕獲視頻幀數據的方法中將 CMSampleBuffer 數據回調到視圖控制器。
//MARK: - AVCaptureVideoDataOutputSampleBufferDelegate/// 捕獲輸出func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {// 處理視頻數據delegate?.captureVideo(sampleBuffer)}
添加濾鏡
在這里我們就采用一個最簡單的方式為實時預覽添加一個濾鏡,通過CIFilter來創建。它支持很多類型的濾鏡,比如顏色翻轉、漫畫風格、色彩分層、像素化等等。
// 視頻幀func captureVideo(_ sampleBuffer: CMSampleBuffer) {// 處理視頻幀guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }// 處理像素緩沖區let ciImage = CIImage(cvPixelBuffer: pixelBuffer)// 添加濾鏡let filter = CIFilter(name: "CIComicEffect")filter?.setValue(ciImage, forKey: kCIInputImageKey)guard let outputImage = filter?.outputImage,let cgImage = ciContext.createCGImage(outputImage, from: ciImage.extent) else {return}let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right)// 顯示到預覽圖層DispatchQueue.main.async {self.previewImageView.image = uiImage}}
最終效果如下:
結語
通過本文,我們實現了使用?AVCaptureVideoDataOutput?獲取原始視頻幀,并結合 Core Image 對其進行實時處理的完整流程。無論是美顏、濾鏡,還是圖像分析,這種方式都為實時圖像處理提供了極大的靈活性和可擴展性。
不過,需要注意的是,CoreImage?雖然上手簡單、易于調試,但在處理高分辨率視頻幀或多個濾鏡疊加時,性能可能會成為瓶頸。如果你希望在性能上進一步優化,或者實現更加復雜、專業的圖像處理效果,推薦使用更底層的圖形處理框架,例如?Metal?或?OpenGLES,它們可以更精細地控制渲染流程、內存使用和 GPU 資源調度,是構建高性能視頻應用的不二之選。