一、Metal 實現視頻預覽
首先我們知道視頻其實就是一幀幀的圖片。
渲染業務流程:
(注:AVFoundation 有提供的預覽圖層: AVCaptureVideoPreviewLayer)
0、初始化工作
// 1.獲取 MTKView 預覽圖層 self.mtkView = [[MTKView alloc] initWithFrame:self.view.bounds]; self.mtkView.device = MTLCreateSystemDefaultDevice(); [self.view insertSubview:self.mtkView atIndex:0]; self.mtkView.delegate = self; // 2.創建命令隊列 self.commandQueue = [self.mtkView.device newCommandQueue]; // 3.注意: 在初始化MTKView 的基本操作以外. 還需要多下面2行代碼. /* 1. 設置MTKView 的drawable 紋理是可讀寫的(默認是只讀); 2. 創建CVMetalTextureCacheRef _textureCache; 這是Core Video的Metal紋理緩存 */ // 允許讀寫操作 self.mtkView.framebufferOnly = NO; /* CVMetalTextureCacheCreate(CFAllocatorRef allocator, CFDictionaryRef cacheAttributes, id <MTLDevice> metalDevice, CFDictionaryRef textureAttributes, CVMetalTextureCacheRef * CV_NONNULL cacheOut ) 功能: 創建紋理緩存區 參數1: allocator 內存分配器.默認即可.NULL 參數2: cacheAttributes 緩存區行為字典.默認為NULL 參數3: metalDevice 參數4: textureAttributes 緩存創建紋理選項的字典. 使用默認選項NULL 參數5: cacheOut 返回時,包含新創建的紋理緩存。 */ CVMetalTextureCacheCreate(NULL, NULL, self.mtkView.device, NULL, &_textureCache);
1、通過 AVFoundation 進行視頻采集
1 // 1.創建mCaptureSession 2 self.mCaptureSession = [[AVCaptureSession alloc] init]; 3 // 設置視頻采集的分辨率 4 self.mCaptureSession.sessionPreset = AVCaptureSessionPreset1920x1080; 5 6 // 2.創建串行隊列 7 self.mProcessQueue = dispatch_queue_create("mProcessQueue", DISPATCH_QUEUE_SERIAL); 8 9 // 3.獲取攝像頭設備(前置/后置攝像頭) 10 NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; 11 AVCaptureDevice *inputCamera = nil; 12 // 循環設備數組,找到后置攝像頭.設置為當前inputCamera 13 for (AVCaptureDevice *device in devices) { 14 if ([device position] == AVCaptureDevicePositionBack) { 15 inputCamera = device; 16 } 17 } 18 19 // 4.將 AVCaptureDevice 轉換為 AVCaptureDeviceInput 20 self.mCaptureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:inputCamera error:nil]; 21 22 // 5. 將設備添加到 mCaptureSession 中 23 if ([self.mCaptureSession canAddInput:self.mCaptureDeviceInput]) { 24 [self.mCaptureSession addInput:self.mCaptureDeviceInput]; 25 } 26 27 // 6.創建 AVCaptureVideoDataOutput 對象 --> 輸出 28 self.mCaptureDeviceOutput = [[AVCaptureVideoDataOutput alloc] init]; 29 30 /*設置視頻幀延遲到底時是否丟棄數據. 31 YES: 處理現有幀的調度隊列在captureOutput:didOutputSampleBuffer:FromConnection:Delegate方法中被阻止時,對象會立即丟棄捕獲的幀。 32 NO: 在丟棄新幀之前,允許委托有更多的時間處理舊幀,但這樣可能會內存增加. 33 */ 34 [self.mCaptureDeviceOutput setAlwaysDiscardsLateVideoFrames:NO]; 35 36 // 這里設置格式為 BGRA,而不用YUV的顏色空間,避免使用Shader轉換 37 // 注意: 這里必須和 CVMetalTextureCacheCreateTextureFromImage 保存圖像像素存儲格式保持一致,否則視頻會出現異常現象 38 [self.mCaptureDeviceOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]]; 39 40 // 設置視頻捕捉輸出的代理方法 41 [self.mCaptureDeviceOutput setSampleBufferDelegate:self queue:self.mProcessQueue]; 42 43 // 7.添加輸出 44 if ([self.mCaptureSession canAddOutput:self.mCaptureDeviceOutput]) { 45 [self.mCaptureSession addOutput:self.mCaptureDeviceOutput]; 46 } 47 48 // 8.輸入與輸出鏈接 49 AVCaptureConnection *connection = [self.mCaptureDeviceOutput connectionWithMediaType:AVMediaTypeVideo]; 50 51 // 9.設置視頻方向 52 // 注意: 一定要設置視頻方向,否則視頻會是朝向異常的 53 [connection setVideoOrientation:AVCaptureVideoOrientationPortrait]; 54 55 // 10.開始捕捉 56 [self.mCaptureSession startRunning];
2、Metal 進行視圖的渲染 -- 2個 delegate 方法
#pragma mark - AVFoundation Delegate - // AVFoundation 視頻采集回調方法 - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { // 1.從sampleBuffer 獲取視頻像素緩存區對象 CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); // 2.獲取捕捉視頻的寬和高 size_t width = CVPixelBufferGetWidth(pixelBuffer); size_t height = CVPixelBufferGetHeight(pixelBuffer); /* 3. 根據視頻像素緩存區 創建 Metal 紋理緩存區 CVReturn CVMetalTextureCacheCreateTextureFromImage(CFAllocatorRef allocator, CVMetalTextureCacheRef textureCache, CVImageBufferRef sourceImage, CFDictionaryRef textureAttributes, MTLPixelFormat pixelFormat, size_t width, size_t height, size_t planeIndex, CVMetalTextureRef *textureOut); 功能: 從現有圖像緩沖區創建核心視頻Metal紋理緩沖區。 參數1: allocator 內存分配器,默認kCFAllocatorDefault 參數2: textureCache 紋理緩存區對象 參數3: sourceImage 視頻圖像緩沖區 參數4: textureAttributes 紋理參數字典.默認為NULL 參數5: pixelFormat 圖像緩存區數據的Metal 像素格式常量.注意如果MTLPixelFormatBGRA8Unorm和攝像頭采集時設置的顏色格式不一致,則會出現圖像異常的情況; 參數6: width,紋理圖像的寬度(像素) 參數7: height,紋理圖像的高度(像素) 參數8: planeIndex.如果圖像緩沖區是平面的,則為映射紋理數據的平面索引。對於非平面圖像緩沖區忽略。 參數9: textureOut,返回時,返回創建的Metal紋理緩沖區。 // Mapping a BGRA buffer: CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, NULL, MTLPixelFormatBGRA8Unorm, width, height, 0, &outTexture); // Mapping the luma plane of a 420v buffer: CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, NULL, MTLPixelFormatR8Unorm, width, height, 0, &outTexture); // Mapping the chroma plane of a 420v buffer as a source texture: CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, NULL, MTLPixelFormatRG8Unorm width/2, height/2, 1, &outTexture); // Mapping a yuvs buffer as a source texture (note: yuvs/f and 2vuy are unpacked and resampled -- not colorspace converted) CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, NULL, MTLPixelFormatGBGR422, width, height, 1, &outTexture); */ CVMetalTextureRef tmpTexture = NULL; CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.textureCache, pixelBuffer, NULL, MTLPixelFormatBGRA8Unorm, width, height, 0, &tmpTexture); // 4.判斷tmpTexture 是否創建成功 if(status == kCVReturnSuccess) { // 5.設置可繪制紋理的當前大小。 self.mtkView.drawableSize = CGSizeMake(width, height); // 6.返回紋理緩沖區的Metal紋理對象。 self.texture = CVMetalTextureGetTexture(tmpTexture); // 7.使用完畢,則釋放tmpTexture CFRelease(tmpTexture); } } #pragma mark - MTKView Delegate - // 視圖渲染則會調用此方法 - (void)drawInMTKView:(MTKView *)view { // 1.判斷是否獲取了AVFoundation 采集的紋理數據 if (self.texture) { // 2.創建指令緩沖 id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer]; // 3.將MTKView 作為目標渲染紋理 id<MTLTexture> drawingTexture = view.currentDrawable.texture; // 4.設置濾鏡 /* MetalPerformanceShaders是Metal的一個集成庫,有一些濾鏡處理的Metal實現; MPSImageGaussianBlur 高斯模糊處理; */ // 創建高斯濾鏡處理 filter // sigma 值越高圖像越模糊 MPSImageGaussianBlur *filter = [[MPSImageGaussianBlur alloc] initWithDevice:self.mtkView.device sigma:1]; // 5.MPSImageGaussianBlur 以一個 Metal 紋理作為輸入,以一個Metal 紋理作為輸出; // 輸入:攝像頭采集的圖像 self.texture // 輸出:創建的紋理 drawingTexture (其實就是view.currentDrawable.texture) [filter encodeToCommandBuffer:commandBuffer sourceTexture:self.texture destinationTexture:drawingTexture]; // 6.展示顯示的內容 [commandBuffer presentDrawable:view.currentDrawable]; // 7.提交命令 [commandBuffer commit]; // 8.清空當前紋理,准備下一次的紋理數據讀取. self.texture = NULL; } } // MTKView - 視圖大小發生改變時.會調用此方法 - (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size {}
二、視頻處理
1、一張圖片的大小
圖片的渲染:jpg/png文件 --> 解壓成位圖 --> 片元着色器 對每個像素處理 --> 渲染顯示
視頻的渲染:視頻文件 --> 解碼(解壓縮) --> 視頻 60幀/s, 60張圖片的處理 --> 片元着色器
舉例:RGB 編碼的一張 1280*720 的圖片(這里忽略透明度A):
有1280*720個像素點,每個像素點有紅綠藍3個原色,每個原色占 8bit(1字節),那么一個像素即 3*8=24bit,即 3字節
--> so 一張圖片占用存儲空間:1280*720 * 3 /1024/1024 = 2.63MB.
對視頻來講,這種空間占用率太高,消耗太大了。
如何解決此問題?
1)視頻壓縮(編碼) --> H264 格式視頻(視頻壓縮) --> 壓縮比可達到 102:1
2)采集時,降低數據量 --> YUV 格式保存視頻
2、YUV
2.1)YUV是什么
YUV 顏色編碼和 RGB顏色編碼的紅綠藍三原色組合 不同,YUV采用的是 明亮度 和 色度 來指定像素的顏色。
Y:明亮度(Luminance、Luma)
U、V:色度(Chrominance、Chroma) -->
色度又定義了顏色的:色調 和 飽和度
為什么用 YUV --> 1、節省帶寬 2、比 RGBA 占用 內存空間 更少
2.2)YUV 采樣格式
和 RGB 表示圖像類似,每個像素點都包含 YUV 三個分量。但它的Y分量和UV分量是可以拆分開的,沒有UV 分量也是可以顯示一張完整圖片的,不過圖片會是沒有色彩的黑白的。
對於 YUV 格式圖像來說,並非每個像素點都包含了完整的 YUV 三個分量,根據不同的采樣格式,每個 Y 分量可以對應自身的 UV 分量,也可以幾個 Y 共用 UV 分量。
a、采樣格式 YUV4:4:4
YUV4:4:4 采樣,YUV 三個分量的采樣比例相同,在生成的圖像里,每個像素點的三分量信息都是完整的8bit。
采樣碼流 為:Y0 U0 V0 Y1 U2 V1 Y2 U2 V2 Y3 U3 V3
映射還原的像素點:[Y0 U0 V0] [Y1 U2 V1] [Y2 U2 V2] [Y3 U3 V3]
圖片所占空間: 1280*720 * 3 /1024/1024 = 2.63MB,與RGB格式相同。此種采樣格式實際業務中並不會使用。
b、采樣格式 YUV4:2:2
YUV4:2:2 即,UV 分量是 Y 分量的一半,Y 和 UV 按照 2:1 的比例采樣。如果水平方向有10個像素點,那么采樣了 10 個 Y,5個UV。采樣方式如上圖,UV分量隔一個采一個,相當於前后2個像素 每次都借用彼此的V/U分量。
采樣碼流:Y0 U0 Y1 V1 Y2 U2 Y3 V3
映射還原像素點:[Y0 U0 V1] [Y1 U0 V1] [Y2 U2 V3] [Y3 U2 V3]
此時,圖片所占空間: 1280*720 * (8bit + 0.5*8 * 2) / 8 /1024/1024 = 1.76MB。相較RGB模型圖像節省了 1/3 的存儲空間,傳遞中占用帶寬也隨之減少。
c、采樣格式 YUV4:2:0
YUV4:2:0 采樣,在每一行掃描時,只掃一種色度分量(U 或 V),和Y 按 2:1 的方式采樣。比如,第一行 YU 按 2:1 的方式采樣,那么第二行則 YV 按 2:1 的方式采樣。 對於每個色度分量來說,水平和豎直方向上的采樣 和 Y 相比 都是 2:1。此種方式必粗要掃2行才能組成完整的UV分量。如上圖,也可以理解為,4個一組,UV 分量共用。
采樣碼流:Y0 U0 Y1 Y2 U2 Y3
: Y4 V4 Y5 Y6 V6 Y7
映射還原像素點:[Y0 U0 V4] [Y1 U0 V4] [Y2 U2 V6] [Y3 U2 V6]
[Y4 U0 V4] [Y5 U0 V4] [Y6 U2 V6] [Y7 U2 V6]
此時,圖片占用空間大小:1280*720 * (8 + 0.25*8 *2) / 8 /1024/1024 = 1.32M,進一步的縮小了占據中間,傳輸時帶寬占用更少。
YUV4:2:0 采樣是目前業務開發中最常用的方式。
視頻捕獲(采集)過程中,只需在片元着色器中,將RGBA轉化成 YUV 即可。
3、YUV 與 RGBA 轉換
對於圖像顯示器來說,是通過 RGB 模型來顯示圖像的,而在傳輸圖像數據時,我們使用的是 YUV 模型,因為 YUV 可以節省帶寬。因此,采集圖像時,我們會將RGB 轉換到YUV,但是,顯示時我們要將 YUV 轉回 RGB。
RGB 到 YUV 的轉換,即 將圖像所有的像素點的 RGB 分量轉換到 YUV 分量。
RGB ==> YUV:
Y = 0.299 * R + 0.587 * G + 0.114 * B
U = -0.147 * R - 0.289 * G + 0.436 * B
V = 0.615 * R - 0.515 * G - 0.100 * B
YUV ==> RGB:
R = Y + 1.14 * V
G = Y - 0.39 * U - 0.58 * V
B = Y + 2.03 * U
以上。
YUV 處理后,雖然圖片占用空間已相對縮小,但,仍需要繼續 對視頻壓縮處理,2者結合,進一步優化。