iOS面向編碼|iOSVideoToolbox:讀寫解碼回調函數CVImageBufferRef的YUV圖像 本文檔基於H.264的解碼,介紹讀寫Video Toolbox解碼回調函數參數CVImageBufferRef中的YUV或RGB數據的方法,並給出CVImageBufferRef生成灰度圖代碼、方便調試。同時,還介紹了Video Toolbox解碼回調中進行YUV處理時容易忽略的問題。文檔定位於iOS音視頻高級編程,致力於提供高參考價值的Core Video中文資料,最近也在StackOverflow上關注Core Video相關問題,學習並回饋社區。 目錄 |- 讀取CVImageBufferRef(CVPixelBufferRef) |- 寫入CVImageBufferRef(CVPixelBufferRef) |- CVPixelBufferPool內存池 |- CVPixelBuffer經過Core Graphics建立灰度圖 |- 坑 |-- 直接操做解碼回調的CVImageBuffer(CVPixelBuffer)存在的問題 |-- CVPixelBuffer上傳至GPU後圖像垂直鏡像問題 |- 參考與推薦閱讀 在實現全景視頻播放器及其關聯項目過程當中,我編寫了如下Video Toolbox相關文檔(因開發任務等緣由,部分文檔處於草稿狀態,以後會進行內容修訂): iOS VideoToolbox硬編H.265(HEVC)H.264(AVC):1 概述 【草稿】iOS VideoToolbox硬編H.265(HEVC)H.264(AVC):2 H264數據寫入文件 iOS VideoToolbox硬編H.265(HEVC)H.264(AVC):4 同步編碼 iOS 音視頻高級編程:AVAssetReaderTrackOutput改變CMFormatDescription致使Video Toolbox解碼失敗與不解碼GPU直接顯示H.264幀 iOS 音視頻高級編程:AVAsset、CoreVideo、VideoToolbox、FFmpeg與CMTime Video Toolbox Multi-pass Encoding 獲取VideoToolbox解碼直播等H.264流的顏色轉換矩陣 CVPixelBufferRef是CVImageBufferRef的別名,二者操做幾乎一致。 // CVPixelBuffer.h/* * CVPixelBufferRef * Based on the image buffer type. * The pixel buffer implements the memory storage for an image buffer. */typedef CVImageBufferRef CVPixelBufferRef; 雖然語法上CVPixelBufferRef是CVImageBufferRef的別名,它們在文檔中的說明卻有區別: Core Video image buffers provides a convenient interface for managing different types of image data. Pixel buffers and Core Video OpenGL buffers derive from the Core Video image buffer. CVImageBufferRef:A reference to a Core Video image buffer. An image buffer is an abstract type representing Core Video buffers that hold images. In Core Video, pixel buffers, OpenGL buffers, and OpenGL textures all derive from the image buffer type. CVPixelBufferRef :A reference to a Core Video pixel buffer object. The pixel buffer stores an image in main memory. 從上述可知,CVPixelBuffer『繼承了』CVImageBuffer,然而,因爲Core Video暴露出來的是Objective-C接口,意味着若想用C語言實現『面向對象的繼承』,則CVPixelBuffer的數據成員定義位置與CVImageBuffer基本保持一致且令編譯器進行相同的偏移以確保字節對齊,猶如FFmpeg中AVFrame可強制轉換成AVPicture,以FFmpeg 3.0源碼爲例。 typedef struct AVFrame { uint8_t *data[AV_NUM_DATA_POINTERS]; int linesize[AV_NUM_DATA_POINTERS]; uint8_t **extended_data; // 後續還有衆多字段}typedef struct AVPicture { ///< pointers to the image data planes uint8_t *data[AV_NUM_DATA_POINTERS]; ///< number of bytes per line int linesize[AV_NUM_DATA_POINTERS]; } AVPicture; 固然,從蘋果開源的某些框架上看,Core Video內部極有可能用Objective-C++實現,可能真正用了C++式繼承,在此不做過多猜想。 一、讀取CVImageBufferRef(CVPixelBufferRef) 在解碼回調中,傳遞過來的幀數據由CVImageBufferRef指向。若是需取出其中像素數據做進一步處理,得訪問其中真正存儲像素的內存。 VideoToolbox解碼後的圖像數據並不能直接給CPU訪問,需先用CVPixelBufferLockBaseAddress()鎖定地址才能從主存訪問,不然調用CVPixelBufferGetBaseAddressOfPlane等函數則返回NULL或無效值。值得注意的是,CVPixelBufferLockBaseAddress自身的調用並不消耗多少性能,通常狀況,鎖定以後,往CVPixelBuffer拷貝內存纔是相對耗時的操做,好比計算內存偏移。若是CVPixelBuffer的圖像須要顯示在屏幕上,建議用GPU實現圖像處理操做。下面展現讀寫左半圖像時的性能損耗(請忽略內存計算的粗暴代碼)。 讀取CVPixelBuffer圖像的性能消耗 寫入CVPixelBuffer圖像的性能消耗 然而,用CVImageBuffer -> CIImage -> UIImage則無需顯式調用鎖定基地址函數。 // CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); // 能夠不加CIImage *ciImage = [CIImage imageWithCVPixelBuffer:imageBuffer];CIContext *temporaryContext = [CIContext contextWithOptions:nil];CGImageRef videoImage = [temporaryContext createCGImage:ciImage fromRect:CGRectMake(0, 0, CVPixelBufferGetWidth(imageBuffer), CVPixelBufferGetHeight(imageBuffer))];UIImage *image = [[UIImage alloc] initWithCGImage:videoImage];UIImageView *imageView = [[UIImageView alloc] initWithImage:image];CGImageRelease(videoImage);// CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); CVPixelBufferIsPlanar可獲得像素的存儲方式是Planar或Chunky。如果Planar,則經過CVPixelBufferGetPlaneCount獲取YUV Plane數量。一般是兩個Plane,Y爲一個Plane,UV由VTDecompressionSessionCreate建立解碼會話時經過destinationImageBufferAttributes指定須要的像素格式(可不一樣於視頻源像素格式)決定是否同屬一個Plane,每一個Plane可看成表格按行列處理,像素是行順序填充的。下面以Planar Buffer存儲方式做說明。 CVPixelBufferGetPlaneCount獲得像素緩衝區平面數量,而後由CVPixelBufferGetBaseAddressOfPlane(索引)獲得相應的通道,通常是Y、U、V通道存儲地址,UV是否分開由解碼會話指定,如前面所述。而CVPixelBufferGetBaseAddress返回的對於Planar Buffer則是指向PlanarComponentInfo結構體的指針,相關定義以下: /* Planar pixel buffers have the following descriptor at their base address. Clients should generally use CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRowOfPlane, etc. instead of accessing it directly. */struct CVPlanarComponentInfo { /* offset from main base address to base address of this plane, big-endian */ int32_t offset; /* bytes per row of this plane, big-endian */ uint32_t rowBytes; };typedef struct CVPlanarComponentInfo CVPlanarComponentInfo;struct CVPlanarPixelBufferInfo { CVPlanarComponentInfo componentInfo[1]; };typedef struct CVPlanarPixelBufferInfo CVPlanarPixelBufferInfo;struct CVPlanarPixelBufferInfo_YCbCrPlanar { CVPlanarComponentInfo componentInfoY; CVPlanarComponentInfo componentInfoCb; CVPlanarComponentInfo componentInfoCr; };typedef struct CVPlanarPixelBufferInfo_YCbCrPlanar CVPlanarPixelBufferInfo_YCbCrPlanar;struct CVPlanarPixelBufferInfo_YCbCrBiPlanar { CVPlanarComponentInfo componentInfoY; CVPlanarComponentInfo componentInfoCbCr; };typedef struct CVPlanarPixelBufferInfo_YCbCrBiPlanar CVPlanarPixelBufferInfo_YCbCrBiPlanar; 根據CVPixelBufferGetPixelFormatType獲得像素格式,以對應的方式讀取,好比YUV420SP跨距讀取全部的U到一個緩衝區。 二、寫入CVImageBufferRef(CVPixelBufferRef) 下面代碼展現了以向Y、UV Planar拷貝數據的過程: NSDictionary *pixelAttributes = @{(id)kCVPixelBufferIOSurfacePropertiesKey : @{}}; CVPixelBufferRef pixelBuffer = NULL; CVReturn result = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, (__bridge CFDictionaryRef)pixelAttributes) &pixelBuffer); CVPixelBufferLockBaseAddress(pixelBuffer, 0);uint8_t *yDestPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);memcpy(yDestPlane, yPlane, width * height);uint8_t *uvDestPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);// numberOfElementsForChroma爲UV寬高乘積memcpy(uvDestPlane, uvPlane, numberOfElementsForChroma); CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);if (result != kCVReturnSuccess) { NSLog(@"Unable to create cvpixelbuffer %d", result); } CIImage *coreImage = [CIImage imageWithCVPixelBuffer:pixelBuffer]; CVPixelBufferRelease(pixelBuffer); 上述代碼經過- [CIImage imageWithCVPixelBuffer:]建立CIImage在iPad Air 二、iPhone 6p等真機上存在的問題: 一、當使用kCVPixelFormatType_420YpCbCr8PlanarFullRange時提示[CIImage initWithCVPixelBuffer:options:] failed because its pixel format f420 is not supported.,即不支持由YUV420P格式的CVPixelBuffer建立CIImage。 經測試,視頻源格式爲yuvj420p(pc, bt709),在VTDecompressionSessionCreate不指定destinationImageBufferAttributes的kCVPixelBufferPixelFormatTypeKey值時,Video Toolbox解碼出來的CVImageBufferRef對應爲f420。 當指定destinationImageBufferAttributes須要kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange時,解碼出來的ImageBuffer爲420v,而後建立YUV時指定PixelFormat爲f420會出現上述問題。緣由是,以420v方式拷貝YUV數據,其存儲佈局與f420不一樣,致使建立CIImage失敗。 二、決定CVPixelBufferCreate建立的格式是其參數pixelFormatType,而非參數pixelAttributes使用kCVPixelBufferPixelFormatTypeKey指定的像素格式。 下面介紹一些簡單的圖像處理辦法。 原始灰度圖 (一)水平鏡像 水平鏡像就是圖像繞圖像中間垂直線交換左右像素點位置,使用矩陣運行表示爲: [x, y, 1] -1 0 0 -> [x', y', 1] 0 1 0 width 0 1 對於CPU而言,矩陣運行一般沒GPU快,由於GPU作2x二、3x3等矩陣運算是硬件加速實現的,極可能就是一條指令處理完,而CPU每每是逐個元素進行計算,所以,目前你們傾向於GPU作矩陣運行。示例CPU實現代碼以下。 for (int line = 0; line < 480; ++line) { for (int col = 0; col < 960; ++col) { dst_buffer[line * 960 + col] = src_buffer[line * 960 + (960 - col)]; } } iOS面向編碼|iOSVideoToolbox:讀寫解碼回調函數CVImageBufferRef的YUV圖像 水平鏡像 (二)垂直鏡像 垂直鏡像就是圖像繞圖像中間水平線交換上下像素點位置,使用矩陣運行表示爲: [x, y, 1] 1 0 0 -> [x', y', 1] 0 -1 0 0 height 1 示例CPU實現代碼以下。 for (int line = 0; line < 480; ++line) { for (int col = 0; col < 960; ++col) { dst_buffer[(480 - line) * 960 + col] = src_buffer[line * 960 + col]; } } iOS面向編碼|iOSVideoToolbox:讀寫解碼回調函數CVImageBufferRef的YUV圖像 垂直鏡像 三、CVPixelBufferPool內存池 自行建立CVPixelBufferPool且經過CVPixelBufferPool建立CVPixelBuffer,容易出現CVPixelBuffer被錯誤釋放或意外增長引用計數致使內存泄露,以ijkplayer爲例演示CVPixelBubffer泄露的狀況。 iOS面向編碼|iOSVideoToolbox:讀寫解碼回調函數CVImageBufferRef的YUV圖像 CVPixelBuffer泄露 iOS面向編碼|iOSVideoToolbox:讀寫解碼回調函數CVImageBufferRef的YUV圖像 CVPixelBuffer結束引用時引用計數不爲0致使內存泄露 而自行建立CVPixelBuffer,則容易出現內存暴漲問題,如建立一個960x480的YUV420SP格式的CVPixelBuffer所佔內存爲700多M,若是是異步解碼且沒做內存大小限制,將致使應用崩潰。 iOS面向編碼|iOSVideoToolbox:讀寫解碼回調函數CVImageBufferRef的YUV圖像 CVPixelBufferCreate佔用的內存 若是不想自行建立CVPixelBufferPool,也不想本身建立CVPixelBuffer,取巧的辦法是,使用解碼回調函數的CVPixelBuffer,則無需擔憂內存消耗問題。在實踐過程當中,圖像處理後當即編碼,這樣使用的場合不會致使解碼器自身的緩存隊列數據出現圖像紊亂。前提是,修改後的像素數據在原數據的寬高範圍內。固然,這也會出現些問題,具體在文檔後續部分進行討論 對於解碼->圖像處理->編碼流程,且處理後的圖像與原圖像大小不一樣,則建立編碼器時再建立CVPixelBufferPool,讓系統管理CVPixelBuffer也是可靠的作法。 另外,在圖像處理過程當中,Video Toolbox不管指定FullRange仍是VideoRange,由此經過Core Graphics建立RGB圖像是正確的,和QuickTime播放時畫面保持一致。然而,解碼出來的YUV420SP數據通過拷貝,接着進行圖像處理,存在部分區域顏色有誤。經過指定Video Toolbox輸出YUV420P,再進行圖像處理則無顏色異常問題。固然,使用的算法也改變相應的YUV420P算法,由於我的認爲,這極有多是咱們團隊的YUV420SP拷貝及操做算法有誤。 四、CVPixelBuffer經過Core Graphics建立灰度圖 修改完YUV數據後,若是每次都須要GPU實現YUV轉換RGB,這比較麻煩,特別是轉碼等離線計算場合。下面,介紹一種實現CVPixelBuffer生成UIImage的辦法,只使用Y平面生成圖像,判斷圖像成像方面的處理結果是否符合預期。 // baseAddress爲Y平面地址,傳遞yuv420(s)p完整數據地址,則忽略uvUIImage* yuv420ToUIImage(void *baseAddress, size_t width, size_t height, size_t bytesPerRow) { // Create a device-dependent gray color space CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); // Create a bitmap graphics context with the sample buffer data CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, kCGImageAlphaNone); // Create a Quartz image from the pixel data in the bitmap graphics context CGImageRef quartzImage = CGBitmapContextCreateImage(context); // Free up the context and color space CGContextRelease(context); CGColorSpaceRelease(colorSpace); // Create an image object from the Quartz image UIImage *image = [UIImage imageWithCGImage:quartzImage]; return image; } 上述代碼可能會引發這樣的疑問:灰度圖爲什麼不須要U和V通道的數據。確實,此問題我最近特地查閱了些資料。建立灰度圖時,有些人還將U、V通道在偏置前(值範圍[-128, 127])設置爲0,或者偏置後(值範圍[0, 255])設置爲128,然而,建立灰度圖時,他們的代碼並未使用UV數據。另外,看到一種說法是: Y通道就是平時所說的灰度通道。 固然,以我有限的瞭解來看,我的不太承認這種說法。緣由是,Y通道是YUV的一個份量,而灰度是複合量,即便數值接近,在概念上應該也是有區別的。數值接近的意思是,以BT. 601轉換矩陣爲例進行證實: Y = 0.299R + 0.587G + 0.114B GrayScale = (R + G + B) / 3 可見,Y值在數值接近灰度值。下面,對建立圖像的代碼段進行簡要分析。 一些開源項目,如SDWebImage,它使用CGColorSpaceCreateDeviceRGB函數,是由於它的數據源是RGB,而咱們這裏的YUV數據須要通過顏色轉換矩陣運算才能獲得RGB,簡單起見,由CGColorSpaceCreateDeviceGray函數建立灰度圖可直接看到圖像發生的變化,缺點是,丟失了顏色信息。示意圖以下所示。 iOS面向編碼|iOSVideoToolbox:讀寫解碼回調函數CVImageBufferRef的YUV圖像 生成灰度圖 雖然,像素格式爲YUV的視頻解碼後幾乎均可生成灰度圖。然而,並非全部的圖像原始數據都能經過Core Graphics生成可視圖像,iOS支持的像素格式很是有限,以下所示。 iOS面向編碼|iOSVideoToolbox:讀寫解碼回調函數CVImageBufferRef的YUV圖像 生成灰度圖支持的像素格式 五、坑 操做CVImageBuffer(CVPixelBuffer)雖然看着沒什麼難度,然而,仍是有些大大小小的問題。若是對此不做描述,那麼本文檔的標題真是太標題黨了。下面,給出我在開發過程當中遇到並解決的狀況。 5.一、直接操做解碼回調的CVImageBuffer(CVPixelBuffer)存在的問題 在解碼回調函數中進行YUV處理,不管是否同步解碼,或者解碼與建立紋理、刷新界面是否爲同一線程。須要注意的是,解碼回調獲得的CVPixelBuffer中的圖像是上一次解碼回調中處理過的圖像,而非視頻壓縮數據經過解碼獲得的新的完整圖像。換句話說,在一個關鍵幀解碼成功後,其後續P幀之前一幀爲基礎,繼續解碼並將結果疊加到新畫面,而後傳遞到解碼回調函數。簡單示意之。 Decode Thread: VTDecompressionSessionDecodeFrame -> VTDecoderCallback (進行圖像處理) -> 添加到待顯示隊列 Rendering Thread: 讀取待顯示隊列、獲得已處理的CVPixelBuffer -> CVOpenGLESTextureCacheCreateTextureFromImage 下面,詳細討論上述狀況。進行YUV三個通道處理後,播放出來的畫面看着正常,相關資源佔用信息以下所示。然而,經輸出Video Toolbox回調函數傳遞過來的CVPixelBuffer或說CVImageBuffer,發現是以前咱們處理過的圖像,並在上一關鍵幀基礎上持續疊加P幀,把結果圖像做爲下一幀視頻。 iOS面向編碼|iOSVideoToolbox:讀寫解碼回調函數CVImageBufferRef的YUV圖像 CPU不超負荷的資源佔用 CPU不超負荷的GPU佔用 CPU不超負荷的Y通道圖 CPU不超負荷的解碼回調每幀圖像 可見,做爲一個關鍵幀間隔爲15的視頻序列,src_1.jpg與src_16.jpg因關鍵幀獲得一次當即刷新,隨後的圖像都在YUV處理的基礎上持續疊加。 5.二、CVPixelBuffer上傳至GPU後圖像垂直鏡像問題 對於CMVideoFormatDescription及指定輸出的CVPixelBuffer信息以下的解碼過程,在自行建立CVPixelBuffer後,將解碼回調函數的CVPixelBuffer數據拷貝到新CVPixelBuffer,一般會遇到圖像顛倒了,確切地說,圖像出現垂直鏡像問題。不過,使用前面生成灰度圖函數獲得的圖像都是正的,不存在顛倒,只有上傳到GPU裏才存在此現象。緣由是,計算機的圖像存儲時有本身的座標,這個座標與OpenGL ES的紋理座標的Y軸正好相反,故圖像在GPU中是顛倒的。 CMVideoFormatDescription { CVFieldCount = 1; CVImageBufferChromaLocationBottomField = Left; CVImageBufferChromaLocationTopField = Left; FullRangeVideo = 0; SampleDescriptionExtensionAtoms = { avcC = <01640033 ffe10014 67640033 ac1b4583 c0f68400 000fa000 03a98010 01000468 e923cbfd f8f800>; }; } destinationImageBufferAttributes = { OpenGLESCompatibility = 1; PixelFormatType = 2033463856; } 如今,嘗試使用Core Video接口處理此問題。首先,判斷源及目標圖像是否翻轉。 bool isFlipped = CVImageBufferIsFlipped(pixelBuffer);if (isFlipped) { NSLog(@"pixelBuffer is %s", isFlipped ? "flipped" : "not flipped"); } isFlipped = CVImageBufferIsFlipped(imageBuffer);if (isFlipped) { NSLog(@"imageBuffer is %s", isFlipped ? "flipped" : "not flipped"); } 發現圖像都是翻轉的,執行結果所下。 pixelBuffer is flipped imageBuffer is flipped 顯然,還須要更多信息去判斷。再獲取兩個緩衝區的ShouldNotPropagate屬性,發現都沒有值。可是,回調函數的像素緩衝區有ShouldPropagate屬性,而咱們自行建立的緩衝區則無此屬性,以下所示。 CVFieldCount = 1; CVImageBufferChromaLocationBottomField = Left; CVImageBufferChromaLocationTopField = Left; CVImageBufferColorPrimaries = "SMPTE_C"; CVImageBufferTransferFunction = "ITU_R_709_2"; CVImageBufferYCbCrMatrix = "ITU_R_601_4"; ColorInfoGuessedBy = VideoToolbox; 那麼,根據H.264文檔,CVFieldCount只是說明CVPixelBuffer只有一個訪問單元(Access Unit),而BottomField和TopField兩個域表達了圖像緩衝區兩個色度的位置,與圖像倒轉無關。其他參數,如YCbCrMatrix只是源視頻須要的YUV轉RGB矩陣。 因此,根據我對Core Video的瞭解,目前使用Core Video接口沒法處理此狀況,只能在GPU中經過鏡像紋理座標或者使用前面介紹的垂直鏡像方式解決