用OpenGLES实现yuv420p视频播放界面
来源:互联网 发布:出库软件哪个好 编辑:程序博客网 时间:2024/06/14 18:58
背景
例子TFLive这个项目里,是我按着ijkPlayer写的直播播放器,要运行需要编译ffmpeg的库,网盘里存了一份, 提取码:vjce。OpenGL ES播放相关的在在OpenGLES的文件夹里。
learnOpenGL学到会使用纹理就可以了。
播放视频,就是把画面一副一副的显示,跟帧动画那样。在解码视频帧数据之后得到的就是某种格式的一段内存,这段数据构成了一副画面所需的颜色信息,比如yuv420p。图文详解YUV420数据格式这篇写的很好。
YUV和RGB这些都叫颜色空间,我的理解便是:它们是一种约定好的颜色值的排列方式。比如RGB,便是红绿蓝三种颜色分量依次排列,一般每个颜色分量就占一个字节,值为0-255。
YUV420p, 是YUV三个分量分别三层,就像:YYYYUUVV。就是Y全部在一起,而RGB是RGBRGBRGB这样混合的。每个分量各自在一起的就是有平面(Plane)的。而420样式是4个Y分量和一对UV分量组合,节省空间。
要显示YUV420p的图像,需要转化yuv到rgba,因为OpenGL输出只认rgba。
iOS上准备工作
OpenGL部分在各平台逻辑是一致的,不在iOS上的可以跳过这段。
使用frameBuffer来显示:
- 新建一个UIView子类,修改layer为
CAEAGLLayer
:+(Class)layerClass{ return [CAEAGLLayer class];}
开始绘制前构建Context:
-(BOOL)setupOpenGLContext{ _renderLayer = (CAEAGLLayer *)self.layer; _renderLayer.opaque = YES; _renderLayer.contentsScale = [UIScreen mainScreen].scale; _renderLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil]; _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; //_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; if (!_context) { NSLog(@"alloc EAGLContext failed!"); return false; } EAGLContext *preContex = [EAGLContext currentContext]; if (![EAGLContext setCurrentContext:_context]) { NSLog(@"set current EAGLContext failed!"); return false; } [self setupFrameBuffer]; [EAGLContext setCurrentContext:preContex]; return true;}
opaque
设为YES是为了不做图层混合,去掉不必要的性能消耗。contentsScale
保持跟手机主屏幕一致,在不同手机上自适应。kEAGLDrawablePropertyRetainedBacking
为YES的时候会保存渲染之后数据不变,我们不需要这个,一帧视频数据显示完就没用了,所以这个功能关闭,去掉不必要的性能消耗。
有了这个context,并且把它设为CurrentContext
,那么在绘制过程里的那些OpenGL代码才能在这个context生效,它才能把结果输出到需要的地方。
构建frameBuffer,它是输出结果:
-(void)setupFrameBuffer{ glGenBuffers(1, &_frameBuffer); glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer); glGenRenderbuffers(1, &_colorBuffer); glBindRenderbuffer(GL_RENDERBUFFER, _colorBuffer); [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_renderLayer]; glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorBuffer); GLint width,height; glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &width); glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &height); _bufferSize.width = width; _bufferSize.height = height; glViewport(0, 0, _bufferSize.width, _bufferSize.height); GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER) ; if(status != GL_FRAMEBUFFER_COMPLETE) { NSLog(@"failed to make complete framebuffer object %x", status); }}
- 建一个framebuffer
- 建一个存储颜色的renderBuffer,但是它的内存是由contex来分配:
[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_renderLayer];
这一句比较关键。因为它,renderBuffer、context和layer才联系到了一起。根据Apple文档,负责显示的layer和renderbuffer是共用内存的,这样输出到renderBuffer里的内容,layer才显示。
OpenGL部分
分为两部分:第一次绘制开始前准备数据和每次绘制循环。
准备部分
使用OpenGL显示的逻辑是:画一个正方形,然后把输出的视频帧数据制作成纹理(texture)给这个正方形,把纹理显示处理就OK里。
所以绘制的图形是不变的,那么shader和数据(AVO等)都是固定的,在第一次开始前搞定后面就不需要变了。
if (!_renderConfiged) { [self configRenderData]; }
-(BOOL)configRenderData{ if (_renderConfiged) { return true; } GLfloat vertices[] = { -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, //left top -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, //left bottom 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, //right top 1.0f, -1.0f, 0.0f, 1.0f, 1.0f, //right bottom };// NSString *vertexPath = [[NSBundle mainBundle] pathForResource:@"frameDisplay" ofType:@"vs"];// NSString *fragmentPath = [[NSBundle mainBundle] pathForResource:@"frameDisplay" ofType:@"fs"]; //_frameProgram = new TFOPGLProgram(std::string([vertexPath UTF8String]), std::string([fragmentPath UTF8String])); _frameProgram = new TFOPGLProgram(TFVideoDisplay_common_vs, TFVideoDisplay_yuv420_fs); glGenVertexArrays(1, &VAO); glBindVertexArray(VAO); glGenBuffers(1, &VBO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5*sizeof(GL_FLOAT), 0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5*sizeof(GL_FLOAT), (void*)(3*(sizeof(GL_FLOAT)))); glEnableVertexAttribArray(1); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); //gen textures glGenTextures(TFMAX_TEXTURE_COUNT, textures); for (int i = 0; i<TFMAX_TEXTURE_COUNT; i++) { glBindTexture(GL_TEXTURE_2D, textures[i]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); } _renderConfiged = YES; return YES;}
- vertices 是正方形4个角的顶点坐标数据,每个点5个float数,前3个是xyz坐标,后两个是纹理坐标(uv)。xyz范围[-1, 1], uv范围[0, 1]。
- 加载shader、编译,链接program,都在
TFOPGLProgram
这个类里做了。 - 然后生成一个VAO和VBO绑定数据。
- 最后构建几个纹理,虽然这时还没有数据,先占个位置。
绘制
先上shader:
const GLchar *TFVideoDisplay_common_vs =" \n\#version 300 es \n\ \n\layout (location = 0) in highp vec3 position; \n\layout (location = 1) in highp vec2 inTexcoord; \n\ \n\out highp vec2 texcoord; \n\ \n\void main() \n\{ \n\gl_Position = vec4(position, 1.0); \n\texcoord = inTexcoord; \n\} \n\";
const GLchar *TFVideoDisplay_yuv420_fs =" \n\#version 300 es \n\precision highp float; \n\ \n\in vec2 texcoord; \n\out vec4 FragColor; \n\uniform lowp sampler2D yPlaneTex; \n\uniform lowp sampler2D uPlaneTex; \n\uniform lowp sampler2D vPlaneTex; \n\ \n\void main() \n\{ \n\ // (1) y - 16 (2) rgb * 1.164 \n\ vec3 yuv; \n\ yuv.x = texture(yPlaneTex, texcoord).r; \n\ yuv.y = texture(uPlaneTex, texcoord).r - 0.5f; \n\ yuv.z = texture(vPlaneTex, texcoord).r - 0.5f; \n\ \n\ mat3 trans = mat3(1, 1 ,1, \n\ 0, -0.34414, 1.772, \n\ 1.402, -0.71414, 0 \n\ ); \n\ \n\ FragColor = vec4(trans*yuv, 1.0); \n\} \n\";
vertex shader就是输出一下gl_Position然后把纹理坐标传给fragment shader。
fragment shader是重点,因为要在这里完成从yuv到rgb的转换。
- 因为yuv420p是yuv3个分量分层存放的,如果将整个yuv数据作为整个纹理加载进来,那么用一个纹理坐标想取到3个分量,计算起来就比较麻烦了,每个fragment都需要计算。
YyYYYYYY
YYYYYYYY
uUUUvVVV
yuv420p的样子是这样的,加入你要取(2,1)这个坐标的颜色信息,那么y在(2,1),u在(1,3),v在(5,3)。而且高宽比例会影响布局:
YyYYYYYY
YYYYYYYY
YyYYYYYY
YYYYYYYY
uUUUuUUU
vVVVvVVV
这样uv不在同一行了。
所以采用每个分量单独的纹理。这样厉害的地方就是他们可以共用同一个纹理坐标:
glBindTexture(GL_TEXTURE_2D, textures[0]); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, overlay->pixels[0]); glGenerateMipmap(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, textures[1]); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width/2, height/2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, overlay->pixels[1]); glGenerateMipmap(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, textures[2]); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width/2, height/2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, overlay->pixels[2]); glGenerateMipmap(GL_TEXTURE_2D);
- 3个纹理,y的纹理和图像大小一样,u和v的高宽都减半。
overlay
只是用来打包视频帧数据的一个结构体,pixels的0、1、2分别就是yuv3个分量的平面的开始位置。- 有一个关键点是纹理格式使用
GL_LUMINANCE
,也就是单颜色通道。看网上的例子,之前写的是GL_RED
的是不行的。 - 因为威力坐标是一个相对坐标,是映射到[0, 1]范围内的。所以对于纹理坐标[x, y],在u和v纹理的上取到的点跟y纹理坐标上[2x, 2y]是对应的,而这正是yuv420需要的:4个y对应一组uv。
最后用的把yuv转成rgb,用的公式:
R = Y + 1.402 (Cr-128)G = Y - 0.34414 (Cb-128) - 0.71414 (Cr-128)B = Y + 1.772 (Cb-128)
这里还有一个注意的就是,YUV和YCrCb的区别:
YCrCb是YUV的一个偏移版本,所以需要减去0.5(因为都映射到0-1范围了128就是0.5)。当然我觉得这个公式还是要看编码的时候设置了什么格式,视频拍摄的时候是怎么把rgb转成yuv的,两者配套就ok了!
绘制正方形
glBindFramebuffer(GL_FRAMEBUFFER, self.frameBuffer); glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); _frameProgram->use(); _frameProgram->setTexture("yPlaneTex", GL_TEXTURE_2D, textures[0], 0); _frameProgram->setTexture("uPlaneTex", GL_TEXTURE_2D, textures[1], 1); _frameProgram->setTexture("vPlaneTex", GL_TEXTURE_2D, textures[2], 2); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); glBindRenderbuffer(GL_RENDERBUFFER, self.colorBuffer); [self.context presentRenderbuffer:GL_RENDERBUFFER];
- 开启program,并把三个纹理输入
- 使用GL_TRIANGLE_STRIP绘制,这样可以更简单些,用GL_TRIANGLES就得两个三角形了。因为这个,所以vertices的4个点是左上、左下、右上、右下的顺序,具体规律看【OpenGL】理解GL_TRIANGLE_STRIP等绘制三角形序列的三种方式。
细节处理
- 监测一下app前后台切换,后台就不要渲染了:
[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(catchAppResignActive) name:UIApplicationWillResignActiveNotification object:nil];[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(catchAppBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];......-(void)catchAppResignActive{ _appIsUnactive = YES;}-(void)catchAppBecomeActive{ _appIsUnactive = NO;}.......if (self.appIsUnactive) { return; //绘制之前检查,直接取消}
把绘制移到副线程
iOS中OpenGL ES的的这些操纵是可以全部放到副线程处理的,包括最后的presentRenderbuffer
。关键是context构建、数组准备(VAO texture等)、渲染这些得在一个线程里,当然也可以多线程操作,但对于视屏播放而言没有必要,去除没必要的性能消耗吧,锁都不用加了。layer的frame改变处理
-(void)layoutSubviews{ [super layoutSubviews]; //If context has setuped and layer's size has changed, realloc renderBuffer. if (self.context && !CGSizeEqualToSize(self.layer.frame.size, self.bufferSize)) { _needReallocRenderBuffer = YES; }}...........if (_needReallocRenderBuffer) { [self reallocRenderBuffer]; _needReallocRenderBuffer = NO;}.........-(void)reallocRenderBuffer{ glBindRenderbuffer(GL_RENDERBUFFER, _colorBuffer); [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_renderLayer]; glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorBuffer); ......}
- 改变之后,重新分配render buffer的内存
- 为了在同一个线程里处理,所以没有直接在
layoutSubviews
里重新分配render buffer,这里肯定是主线程。所以只是做了个标记 - 在渲染的方法里,先查看_needReallocRenderBuffer,然后realloc render buffer.
最后
重点是fragment shader里对yuv分量的读取:
- 采取3个纹理
- 使用同一个纹理坐标
- 构建纹理是使用
GL_LUMINANCE
, u、v纹理宽高相对y都减半。
作者:find_1991
链接:http://www.jianshu.com/p/9e5588bb4c2f
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- 用OpenGLES实现yuv420p视频播放界面
- 打造视频播放界面
- 播放视频的引导界面
- Android 用service实现视频播放器
- SurfaceView实现视频播放
- HTML5实现视频播放
- 如何实现视频播放?
- videoview实现视频播放
- SurfaceView实现视频播放
- opencv实现视频播放
- DirectX实现视频播放
- unity3d实现视频播放
- video实现视频播放
- MediaPlayer实现视频播放
- AVPlayer实现视频播放
- TextureView实现视频播放
- unity3d实现视频播放
- videoview实现视频播放
- Window系统中MySql 5.7.19 忘记密码该怎么改回来
- HTML-子div在父div中垂直居中
- python002 Python3 基础语法
- KNN算法 代码详细解释
- socket.gaierror Errno -3 Temporary failure in name resolution
- 用OpenGLES实现yuv420p视频播放界面
- HDU2040亲和数
- Unity_NGUI基础控件_Tween_043
- linux下freeTDS的安装
- 滑块运动 — 到目标位置高度展开
- ios-block补充
- 【资源共享】RK3399《Rockchip USB 开发指南 V1.0》
- jQuery封装的ajax+h5实现简易上传进度条
- 背包(理解)