本文最后更新于:2025年4月9日 下午
前言
上一篇 使用 OpenGLES 渲染相机预览画面 实现了自定义相机画面渲染,使用自定义的图层替代了 AVFoundation 默认渲染图层,但还需要考虑的是预览和编码镜像的问题。由于现在图层使用的 Buffer 数据来自 AVCaptureVideoDataOutput ,我们可以通过设置 AVCaptureVideoDataOutput 链接的 AVCaptureConnection 的 videoMirrored 属性,并关闭 automaticallyAdjustsVideoMirroring 去统一调整预览和编码的镜像,但有些场景需要预览镜像编码不镜像,或预览不镜像编码镜像,所以就需要一个工具类去处理预览和编码镜像不一致的场景。
实现思路
大批量的像素翻转不适合在 CPU 上处理,因此考虑使用 OpenGL 的离屏渲染,将输出纹理绑定在帧缓冲区的颜色缓冲,在输入纹理绑定上下文后,通过翻转顶点着色器 gl_Position 的 X 坐标实现纹理镜像。
代码
首先是顶点坐标的计算,自定义一个 VerticesCoordinates,positionCoordinates 表示顶点坐标,textureCoordinates 表示纹理坐标,vertices 中的 4 个坐标分别是矩形的 4 个顶点,这里跟上一篇的顶点坐标有点不同,上一篇的顶点坐标用于屏幕渲染,会有纹理原点和屏幕原点不一致的情况,所以纹理坐标的 Y 做了翻转,我们这次的输出也是纹理,所以不需要做翻转,一一对应就可以。我们绘制用的是 glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);,即 4 个顶点绘制 2 个三角形。
| typedef struct { GLKVector2 positionCoordinates; GLKVector2 textureCoordinates; } VerticesCoordinates;
VerticesCoordinates vertices[] = { {{-1.0f, -1.0f}, {0.0f, 0.0f}}, {{ 1.0f, -1.0f}, {1.0f, 0.0f}}, {{-1.0f, 1.0f}, {0.0f, 1.0f}}, {{ 1.0f, 1.0f}, {1.0f, 1.0f}} };
|
为了方便,顶点着色器和片段着色器保存在字符串里。顶点着色器定义两个 attribute 属性用来读取顶点坐标和纹理坐标,v_texcoord 属性将纹理坐标传递给片段着色器,在 main 函数里,将 gl_Position 的 X 坐标做了翻转来实现镜像效果。片段着色器先声明了 float 使用中等精度,v_texcoord 与顶点着色器对应,tex 声明一个纹理采样器,最后在 main 函数里使用纹理采样器计算出像素颜色。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| static const char* vertex_shader_string = "attribute vec2 position;" "attribute vec2 texcoord;" "varying vec2 v_texcoord;" "void main() {" " gl_Position = vec4(-position.x, position.y, 0.0, 1.0);" " v_texcoord = texcoord;" "}";
static const char* fragment_shader_string = "precision mediump float;" "varying vec2 v_texcoord;" "uniform sampler2D tex;" "void main() {" " gl_FragColor = texture2D(tex, v_texcoord);" "}";
|
着色器的创建、编译和链接,着色器程序的生成代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| static inline GLuint compile_shader(GLuint type, const char* source) { GLuint shader = glCreateShader(type); glShaderSource(shader, 1, &source, NULL); glCompileShader(shader); GLint compiled; glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
#ifdef DEBUG if (!compiled) { GLint length; char* log; glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length); log = (char *)malloc(length); glGetShaderInfoLog(shader, length, &length, &log[0]); DLog("%s compilation error: %s\n", (type == GL_VERTEX_SHADER ? "GL_VERTEX_SHADER" : "GL_FRAGMENT_SHADER"), log); free(log); return 0; } #endif return shader; }
static inline GLuint build_program(const char* vertex_shader_string, const char* fragment_shader_string) { GLuint vshad, fshad, p; GLint len; #ifdef DEBUG char* log; #endif vshad = compile_shader(GL_VERTEX_SHADER, vertex_shader_string); fshad = compile_shader(GL_FRAGMENT_SHADER, fragment_shader_string); p = glCreateProgram(); glAttachShader(p, vshad); glAttachShader(p, fshad); glLinkProgram(p); glGetProgramiv(p, GL_INFO_LOG_LENGTH, &len); #ifdef DEBUG if (len) { log = (char *)malloc(len); glGetProgramInfoLog(p, len, &len, log); DLog("program log: %s\n", log); free(log); } #endif glDeleteShader(vshad); glDeleteShader(fshad); return p; }
|
着色器和顶点坐标准备完成后,可以初始化 OpenGL 环境了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| - (void)setupGL { _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; if (!_context) { _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; if (!_context) { NSLog(@"error: video mirror processor, Error! Unable to create an OpenGL ES Context!"); } } if (!_context || ![EAGLContext setCurrentContext:_context]) { NSLog(@"failed to setup EAGLContext"); } CVReturn ret = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, _context, NULL, &_textureCache); if (ret != kCVReturnSuccess) { NSLog(@"error: video mirror processor, Error! CVOpenGLESTextureCacheCreate failed %d", ret); } glGenBuffers(1, &_vbo); glBindBuffer(GL_ARRAY_BUFFER, _vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); _program = build_program(vertex_shader_string, fragment_shader_string); glUseProgram(_program); GLint samplerLocation = glGetUniformLocation(_program, "tex"); glUniform1f(samplerLocation, 0); GLint posLocation = glGetAttribLocation(_program, "position"); GLint texLocation = glGetAttribLocation(_program, "texcoord"); glEnableVertexAttribArray(posLocation); glEnableVertexAttribArray(texLocation); glVertexAttribPointer(posLocation, 2, GL_FLOAT, GL_FALSE, sizeof(VerticesCoordinates), (void *)offsetof(VerticesCoordinates, positionCoordinates)); glVertexAttribPointer(texLocation, 2, GL_FLOAT, GL_FALSE, sizeof(VerticesCoordinates), (void *)offsetof(VerticesCoordinates, textureCoordinates)); }
|
由于帧缓冲区输出的纹理依赖输入的 Pixel Buffer 的宽高,所以 fbo 和输出纹理放到 process 方法里动态创建。在初始化 OpenGL 环境后,就可以调用 process 来渲染了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| - (CVPixelBufferRef)process:(CVPixelBufferRef)pixelBuffer { [_renderLock lock]; if (_needStopDisplay) { [_renderLock unlock]; return _outputPixelBuffer; } if ([EAGLContext currentContext] != _context) { [EAGLContext setCurrentContext:_context]; } GLsizei pixelWidth = (GLsizei)CVPixelBufferGetWidth(pixelBuffer); GLsizei pixelHeight = (GLsizei)CVPixelBufferGetHeight(pixelBuffer); if (pixelWidth == 0 || pixelHeight == 0) { [_renderLock unlock]; return _outputPixelBuffer; } if (pixelWidth != _currentRenderSize.width || pixelHeight != _currentRenderSize.height) { _currentRenderSize = CGSizeMake(pixelWidth, pixelHeight); [self setupCVPixelBuffer:&_outputPixelBuffer]; _needUpdateFBO = YES; } if (_needUpdateFBO) { [self setupFBO]; _needUpdateFBO = NO; } if (_inputTexture) { CFRelease(_inputTexture); _inputTexture = NULL; } CVOpenGLESTextureCacheFlush(_textureCache, 0); CVPixelBufferLockBaseAddress(pixelBuffer, 0); CVReturn ret = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCache, pixelBuffer, NULL, GL_TEXTURE_2D, GL_RGBA, pixelWidth, pixelHeight, GL_BGRA, GL_UNSIGNED_BYTE, 0, &_inputTexture); if (ret != kCVReturnSuccess) { NSLog(@"error: _inTexture, CVOpenGLESTextureCacheCreateTextureFromImage, failed."); } CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); glBindTexture(CVOpenGLESTextureGetTarget(_inputTexture), CVOpenGLESTextureGetName(_inputTexture)); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glUseProgram(_program); glBindFramebuffer(GL_FRAMEBUFFER, _fbo); glBindBuffer(GL_ARRAY_BUFFER, _vbo); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glViewport(0, 0, _currentRenderSize.width, _currentRenderSize.height); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); glFlush(); glBindFramebuffer(GL_FRAMEBUFFER, 0); [_renderLock unlock]; return _outputPixelBuffer; }
|
27 行位置判断当前输入的 Pixel Buffer 宽高是否与上一次渲染的宽高一致,如果不一致会重新创建输出的 Pixel Buffer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| - (void)setupCVPixelBuffer:(CVPixelBufferRef *)pixelBuffer { NSDictionary *pixelBufferOptions = @{ (NSString *)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA), (NSString *)kCVPixelBufferWidthKey : @(_currentRenderSize.width), (NSString *)kCVPixelBufferHeightKey : @(_currentRenderSize.height), (NSString *)kCVPixelBufferOpenGLESCompatibilityKey : @YES, (NSString *)kCVPixelBufferIOSurfacePropertiesKey : @{} }; if (*pixelBuffer) { CVPixelBufferRelease(*pixelBuffer); *pixelBuffer = NULL; } CVReturn ret = CVPixelBufferCreate(kCFAllocatorDefault, _currentRenderSize.width, _currentRenderSize.height, kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef)pixelBufferOptions, pixelBuffer); if (ret != kCVReturnSuccess) { NSLog(@"error: video mirror processor, Unable to create cvpixelbuffer %d", ret); } }
|
随后会重新初始化帧缓冲区和输出纹理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| - (void)setupFBO { if ([EAGLContext currentContext] != _context) { [EAGLContext setCurrentContext:_context]; } if (_fbo > 0) { glDeleteFramebuffers(1, &_fbo); _fbo = 0; } glGenFramebuffers(1, &_fbo); if (_outputTexture) { CFRelease(_outputTexture); _outputTexture = NULL; } CVReturn ret = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCache, _outputPixelBuffer, NULL, GL_TEXTURE_2D, GL_RGBA, _currentRenderSize.width, _currentRenderSize.height, GL_BGRA, GL_UNSIGNED_BYTE, 0, &_outputTexture); if (ret != kCVReturnSuccess) { NSLog(@"error: _outputTexture, CVOpenGLESTextureCacheCreateTextureFromImage, failed."); } glBindTexture(CVOpenGLESTextureGetTarget(_outputTexture), CVOpenGLESTextureGetName(_outputTexture)); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glBindFramebuffer(GL_FRAMEBUFFER, _fbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, CVOpenGLESTextureGetName(_outputTexture), 0); if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { NSLog(@"error: failed to make complete framebuffer object %x", glCheckFramebufferStatus(GL_FRAMEBUFFER)); } glBindFramebuffer(GL_FRAMEBUFFER, 0); }
|
setupFBO 创建了帧缓冲,并将输出纹理绑定到帧缓冲的颜色缓冲上,处理完成后将 _outputPixelBuffer 返回给调用者。
另外还有一些前后台的处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| - (void)handleApplicationDidEnterBackground:(NSNotification *)notification { [_renderLock lock]; _needStopDisplay = YES; [self destroyGL]; glFinish(); [_renderLock unlock]; }
- (void)handleApplicationDidBecomeActive:(NSNotification *)notification { [_renderLock lock]; _needStopDisplay = NO; if (!_fbo) { _needUpdateFBO = YES; } [_renderLock unlock]; }
|
后记
代码完成后,就可以在 didOutputSampleBuffer 回调里使用了,这样,前后置预览镜像和编码镜像就都可以单独配置了,镜像后的数据可以输入到下一级的 processor 中做处理,最终吐给编码器编码。