iOS视觉-- (09) OpenGL ES+GLSL实现摄像头录制渲染解析

前面都是讲一些关于图片的一些操作,下一步进击视频相关的东西。由浅入深的学习,因为万事开头难,不要想着一步登天。静下心来一步一步的往上爬。每天能学到一点东西就是进步,持之以恒才是最重要的。千里之行始于足下...
本文借鉴:落影大神-摄像头采集数据和渲染

本文Demo

首先我们明确一下我们要实现的东西:
  • 1.摄像头录制
  • 2.使用OpenGL ES渲染视频帧
  • 1. 摄像头录制

这里我们主要是学习OpenGL ES怎么渲染视频帧的,摄像头录制这方面,网上这里有很多写录制的逻辑与流程的,这里就不再赘述了。有兴趣请自行百度、谷歌。这里直接上代码了

class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {
    
    var mCaptureSession: AVCaptureSession! //负责输入和输出设备之间的数据传递
    var mCaptureDeviceInput: AVCaptureDeviceInput! //负责从AVCaptureDevice获得输入数据
    var mCaptureDeviceOutput: AVCaptureVideoDataOutput! //output
    var mProcessQueue: DispatchQueue!


    @IBOutlet var renderView: DDView!
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.mCaptureSession = AVCaptureSession()
        self.mCaptureSession.sessionPreset = AVCaptureSession.Preset.high
        
        mProcessQueue = DispatchQueue(label: "mProcessQueue")
        
        var inputCamera: AVCaptureDevice!
        let devices = AVCaptureDevice.devices(for: AVMediaType.video)
        for device in devices {
            if (device.position == AVCaptureDevice.Position.back)
            {
                inputCamera = device;
            }
        }
        
        self.mCaptureDeviceInput = try? AVCaptureDeviceInput(device: inputCamera)//[[ alloc] initWithDevice:inputCamera error:nil];
        
        if (self.mCaptureSession.canAddInput(self.mCaptureDeviceInput)) {
            self.mCaptureSession.addInput(self.mCaptureDeviceInput)
        }

        
        self.mCaptureDeviceOutput = AVCaptureVideoDataOutput()
        self.mCaptureDeviceOutput.alwaysDiscardsLateVideoFrames = false
        
//        self.mGLView.isFullYUVRange = YES;
        //kCVPixelFormatType_32BGRA
        self.mCaptureDeviceOutput.videoSettings = [String(kCVPixelBufferPixelFormatTypeKey) : kCVPixelFormatType_420YpCbCr8BiPlanarFullRange]
        self.mCaptureDeviceOutput.setSampleBufferDelegate(self, queue: self.mProcessQueue)
        
        if (self.mCaptureSession.canAddOutput(self.mCaptureDeviceOutput)) {
            self.mCaptureSession.addOutput(self.mCaptureDeviceOutput)
        }
        
        let connection: AVCaptureConnection = self.mCaptureDeviceOutput.connection(with: AVMediaType.video)!
//        connection.isVideoMirrored = false
        connection.videoOrientation = AVCaptureVideoOrientation.portrait
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        self.mCaptureSession.startRunning()
    }

    
    //MARK: - AVCaptureVideoDataOutputSampleBufferDelegate
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        DispatchQueue.main.async {
            let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
            self.renderView.renderBuffer(pixelBuffer: pixelBuffer!)  
        }
    }
    
}

注意⚠️:
这里有一个注意点就是视频帧格式(kCVPixelBufferPixelFormatTypeKey)的配置,OpenGL 要以对应的格式去取才能取到正确的视频帧,否则会出现黑屏
iOS通常支持三种格式:
1、kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
2、kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
3、kCVPixelFormatType_32BGRA

  • 2.使用OpenGL ES渲染视频帧

之前我们把图片加载成纹理的时候都是使用 glTexImage2D方式,视频本身不过是一系列静止图像的组合而已。

图片转纹理

但是摄像机录制的是 CMSampleBuffer,如何将CMSampleBuffer转成纹理呢?

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) 

苹果为我们封装了一个通过 CVImageBuffer 创建 CVOpenGLESTexture (纹理)的方法,如下:

CVOpenGLESTextureCacheCreateTextureFromImage(_ allocator: CFAllocator?, _ textureCache: CVOpenGLESTextureCache, _ sourceImage: CVImageBuffer, _ textureAttributes: CFDictionary?, _ target: GLenum, _ internalFormat: GLint, _ width: GLsizei, _ height: GLsizei, _ format: GLenum, _ type: GLenum, _ planeIndex: Int, _ textureOut: UnsafeMutablePointer<CVOpenGLESTexture?>)
CMSampleBuffer转纹理
  • CVImageBuffer是何物?它与CVPixelBuffer又是什么关系?
    CVPixelBuffer 解析:
    CVPixelBuffer 给的官方解释,是其主内存存储所有像素点数据的一个对象.那么什么是主内存了?
    其实它并不是我们平常所操作的内存,它指的是存储区域存在于缓存之中. 我们在访问这个块内存区域,需要先锁定这块内存区域.
//1.锁定内存区域:
    CVPixelBufferLockBaseAddress(pixel_buffer,0);
 //2.读取该内存区域数据到NSData对象中
    Void *data = CVPixelBufferGetBaseAddress(pixel_buffer);
 //3.数据读取完毕后,需要释放锁定区域
    CVPixelBufferRelease(pixel_buffer); 

public typealias CVPixelBuffer = CVImageBuffer,CVPixelBuffer是CVImageBuffer的别名

  • 1、如果照相机设置的视频帧格式是 kCVPixelFormatType_32BGRA,那么读取的方式就是:GL_BGRA
  • 2、如果是其他两个,那么它们录制的视频是YUV格式的视频。YUV视频帧分为亮度和色度两个纹理,分别用GL_LUMINANCE格式和GL_LUMINANCE_ALPHA格式读取。
  • 部分核心代码:
//设置纹理
    func renderBuffer(pixelBuffer: CVPixelBuffer) {
        if (self.textureCache != nil) {//注意⚠️:释放内存,要不然会卡住
            if textureY != nil { textureY = nil }
            if textureUV != nil { textureUV = nil }
            CVOpenGLESTextureCacheFlush(self.textureCache!, 0)
        }

        let colorAttachments: CFTypeRef = CVBufferGetAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey, nil)!.takeRetainedValue()
        //"\(colorAttachments)" == String(kCVImageBufferYCbCrMatrix_ITU_R_601_4)
        if (CFEqual(colorAttachments, kCVImageBufferYCbCrMatrix_ITU_R_601_4)) {
            if (self.isFullYUVRange) {
                preferredConversion = kColorConversion601FullRange
            }
            else {
                preferredConversion = kColorConversion601
            }
        }
        else {
            preferredConversion = kColorConversion709
        }

        glActiveTexture(GLenum(GL_TEXTURE0))
        // Create a CVOpenGLESTexture from the CVImageBuffer
        let frameWidth = CVPixelBufferGetWidth(pixelBuffer)
        let frameHeight = CVPixelBufferGetHeight(pixelBuffer)

        //亮度纹理 使用:GL_LUMINANCE
        let ret: CVReturn = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                                                         textureCache!,
                                                                         pixelBuffer,
                                                                         nil,
                                                                         GLenum(GL_TEXTURE_2D),
                                                                         GL_LUMINANCE,
                                                                         GLsizei(frameWidth),
                                                                         GLsizei(frameHeight),
                                                                         GLenum(GL_LUMINANCE),
                                                                         GLenum(GL_UNSIGNED_BYTE),
                                                                         0,
                                                                         &textureY)
        if ((ret) != 0) {
            NSLog("CVOpenGLESTextureCacheCreateTextureFromImage ret: %d", ret)
            /*
             ⚠️注意:error: -6683 是录制时配置的 kCVPixelBufferPixelFormatTypeKey 与获取的颜色格式不对应
             1、kCVPixelFormatType_32BGRA -->
                CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                                                              textureCache!,
                                                                              pixelBuffer,
                                                                              nil,
                                                                              GLenum(GL_TEXTURE_2D),
                                                                              GL_RGBA,
                                                                              GLsizei(frameWidth),
                                                                              GLsizei(frameHeight),
                                                                              GLenum(GL_BGRA),
                                                                              GLenum(GL_UNSIGNED_BYTE),
                                                                              0,
                                                                              &texture);

             */
            return
        }
        glBindTexture(CVOpenGLESTextureGetTarget(textureY!), CVOpenGLESTextureGetName(textureY!))
        glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
        glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR)
        glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GL_CLAMP_TO_EDGE)
        glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GL_CLAMP_TO_EDGE)


        glActiveTexture(GLenum(GL_TEXTURE1))
        //色度纹理 使用:GL_LUMINANCE_ALPHA
        let retUV: CVReturn = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                                                         textureCache!,
                                                                         pixelBuffer,
                                                                         nil,
                                                                         GLenum(GL_TEXTURE_2D),
                                                                         GL_LUMINANCE_ALPHA,
                                                                         GLsizei(frameWidth / 2),
                                                                         GLsizei(frameHeight / 2),
                                                                         GLenum(GL_LUMINANCE_ALPHA),
                                                                         GLenum(GL_UNSIGNED_BYTE),
                                                                         1,
                                                                         &textureUV)
        if ((retUV) != 0) {
            NSLog("CVOpenGLESTextureCacheCreateTextureFromImage retUV: %d", retUV)
            return
        }
        glBindTexture(CVOpenGLESTextureGetTarget(textureUV!), CVOpenGLESTextureGetName(textureUV!))
        glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
        glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR)
        glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GL_CLAMP_TO_EDGE)
        glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GL_CLAMP_TO_EDGE)

        //绘制
        renderLayer()
        
    }

  • 有同学可能会有疑问?

CVOpenGLESTextureCacheCreateTextureFromImageglTexImage2D 这两个区别是什么?
我的理解是:glTexImage2D是标准的OpenGL ES的API,而 CVOpenGLESTextureCacheCreateTextureFromImage是苹果对其进行上层封装的API,所以我们也可以通过glTexImage2D实现渲染

转换

    /// CVPixelBuffer -> UIImage
    class func pixelBufferToImage(pixelBuffer: CVPixelBuffer, outputSize: CGSize? = nil) -> UIImage? {
//        let type = CVPixelBufferGetPixelFormatType(pixelBuffer)
        
        let width = CVPixelBufferGetWidth(pixelBuffer)
        let height = CVPixelBufferGetHeight(pixelBuffer)
        let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
        
        CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
        guard let context = CGContext(data: CVPixelBufferGetBaseAddress(pixelBuffer),
                                      width: width,
                                      height: height,
                                      bitsPerComponent: 8,
                                      bytesPerRow: bytesPerRow,
                                      space: CGColorSpaceCreateDeviceRGB(),
                                      bitmapInfo: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.noneSkipFirst.rawValue),
            let imageRef = context.makeImage() else
        {
                return nil
        }
        
        let newImage = outputSize != nil ? UIImage(cgImage: imageRef, scale: 1, orientation: UIImage.Orientation.up).resizedImage(outputSize: outputSize!) : UIImage(cgImage: imageRef, scale: 1, orientation: UIImage.Orientation.up)
        CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
        
        return newImage
    }
    

2022.11.24 补充

上面的方式通过了2次 CGContext 解码,当时就觉得奇怪,只怪自己才疏学浅,后面再学习中发现有更简单的方式,直接跳过转成UIImage的方式,如下图


直接把CVPixelBufferGetBaseAddress(pixelBuffer)传给glTexImage2D即可,已在项目里更新

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容