美文网首页
直播(二)(视音频流播放--IOS)

直播(二)(视音频流播放--IOS)

作者: 孔雨露 | 来源:发表于2019-08-10 06:34 被阅读0次

    @TOC

    • 视频流播放通常用的是两种方式:
    1. 将yuv视频流转化成一帧帧图片,如yuv420转换为RGB
    2. 直接用openGL 用文理的方式渲染yuv视频流
      很显然,方式2省去了yuv转rgb的时间,而这个转化过程是非常消耗cpu性能的。
    • 音频流播放
    • 要理解视频播放原理需要先理解一些理论知识:
      yuv: 详细请参考我的另一篇博客:yuv 相关知识
      视频编解码
      音频编解码
      视音频同步

    IOS平台视频播放器简介

    IOS视频播放底层框架简介

    IOS 视频播放原理

    IOS 音频播放原理

    IOS 视频编解码

    IOS音频编解码

    IOS视频流播放代码

    1. opengl es 播放yuv视频流

    代码如下:

    #import "OpenglView.h"
    
        enum AttribEnum
        {
            ATTRIB_VERTEX,
            ATTRIB_TEXTURE,
            ATTRIB_COLOR,
        };
    
        //YUV数据枚举
        enum TextureType
        {
            TEXY = 0,
            TEXU,
            TEXV,
            TEXC
        };
    
        @implementation OpenglView
    
        #pragma mark -  初始化等操作
        - (BOOL)doInit{
            //用来显示opengl的图形
            CAEAGLLayer *eaglLayer = (CAEAGLLayer*) self.layer;
            //eaglLayer.opaque = YES;
            
            //设为不透明
            eaglLayer.opaque = YES;
            
            //设置描绘属性
            //    [eaglLayer setDrawableProperties:[NSDictionary dictionaryWithObjectsAndKeys:
            //                                     [NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking,
            //                                     kEAGLColorFormatRGB565, kEAGLDrawablePropertyColorFormat,
            //                                     //[NSNumber numberWithBool:YES], kEAGLDrawablePropertyRetainedBacking,
            //                                      nil]];
            
            eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
                                            [NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking,
                                            kEAGLColorFormatRGB565, kEAGLDrawablePropertyColorFormat,
                                            //[NSNumber numberWithBool:YES], kEAGLDrawablePropertyRetainedBacking,
                                            nil];
            
            //设置分辨率
            self.contentScaleFactor = [UIScreen mainScreen].scale;
            _viewScale = [UIScreen mainScreen].scale;
            
            //创建上下文
            _glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
            
            //[self debugGlError];
            
            //上下文创建失败则直接返回no
            if(!_glContext || ![EAGLContext setCurrentContext:_glContext])
            {
                return NO;
            }
            
            //创建纹理
            [self setupYUVTexture];
            
            //加载着色器
            [self loadShader];
            
            //像素数据对齐,第二个参数默认为4,一般为1或4
            glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
            
            //使用着色器
            glUseProgram(_program);
            
            //获取一致变量的存储位置
            GLuint textureUniformY = glGetUniformLocation(_program, "SamplerY");
            GLuint textureUniformU = glGetUniformLocation(_program, "SamplerU");
            GLuint textureUniformV = glGetUniformLocation(_program, "SamplerV");
            
            //对几个纹理采样器变量进行设置
            glUniform1i(textureUniformY, 0);
            glUniform1i(textureUniformU, 1);
            glUniform1i(textureUniformV, 2);
            
            return YES;
        }
    
        -(instancetype)initWithFrame:(CGRect)frame{
            
            self = [super initWithFrame:frame];
            
            if (self) {
                
                //没有初始化成功
                if (![self doInit]) {
                    
                    self = nil;
                }
            }
            
            return self;
        }
    
        -(instancetype)initWithCoder:(NSCoder *)aDecoder{
            
            self = [super initWithCoder:aDecoder];
            
            if (self) {
                
                //没有初始化成功
                if (![self doInit]) {
                    
                    self = nil;
                }
            }
            
            return self;
        }
    
        -(void)layoutSubviews{
            
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                
                //互斥锁
                @synchronized (self) {
                    
                    [EAGLContext setCurrentContext:_glContext];
                    
                    //清除缓冲区
                    [self destoryFrameAndRenderBuffer];
                    
                    //创建缓冲区
                    [self createFrameAndRenderBuffer];
                    
                }
                
                //把数据显示在这个视窗上
                glViewport(1, 1, self.bounds.size.width*_viewScale - 2, self.bounds.size.height*_viewScale - 2);
            });
        }
    
        #pragma mark -  设置opengl
    
        /**
         不写的话,设置描绘属性会崩溃
         
         @return <#return value description#>
         */
        + (Class)layerClass
        {
            return [CAEAGLLayer class];
        }
    
        /**
         创建缓冲区
         
         @return <#return value description#>
         */
        - (BOOL)createFrameAndRenderBuffer
        {
            //创建帧缓冲绑定
            glGenFramebuffers(1, &_framebuffer);
            //创建渲染缓冲
            glGenRenderbuffers(1, &_renderBuffer);
            
            //将之前用glGenFramebuffers创建的帧缓冲绑定为当前的Framebuffer(绑定到context上?).
            glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
            //Renderbuffer绑定到context上,此时当前Framebuffer完全由renderbuffer控制
            glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
            
            //分配空间
            if (![_glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer])
            {
                NSLog(@"attach渲染缓冲区失败");
            }
            
            //这个函数看起来有点复杂,但其实它很好理解的。它要做的全部工作就是把把前面我们生成的深度缓存对像与当前的FBO对像进行绑定,当然我们要注意一个FBO有多个不同绑定点,这里是要绑定在FBO的深度缓冲绑定点上。
            glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _renderBuffer);
            
            //检查当前帧缓存的关联图像和帧缓存参数
            if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
            {
                NSLog(@"创建缓冲区错误 0x%x", glCheckFramebufferStatus(GL_FRAMEBUFFER));
                return NO;
            }
            return YES;
        }
    
        /**
         清除缓冲区
         */
        - (void)destoryFrameAndRenderBuffer
        {
            if (_framebuffer)
            {
                //删除FBO
                glDeleteFramebuffers(1, &_framebuffer);
            }
            
            if (_renderBuffer)
            {
                //删除渲染缓冲区
                glDeleteRenderbuffers(1, &_renderBuffer);
            }
            
            _framebuffer = 0;
            _renderBuffer = 0;
        }
        /**
         创建纹理
         */
        - (void)setupYUVTexture{
            
            if (_textureYUV[TEXY])
            {
                //删除纹理
                glDeleteTextures(3, _textureYUV);
            }
            
            //生成纹理
            glGenTextures(3, _textureYUV);
            if (!_textureYUV[TEXY] || !_textureYUV[TEXU] || !_textureYUV[TEXV])
            {
                NSLog(@"<<<<<<<<<<<<纹理创建失败!>>>>>>>>>>>>");
                return;
            }
            
            //选择当前活跃单元
            glActiveTexture(GL_TEXTURE0);
            //绑定Y纹理
            glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXY]);
            //纹理过滤函数
            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_S, GL_CLAMP_TO_EDGE);//水平方向
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);//垂直方向
            
            glActiveTexture(GL_TEXTURE1);
            glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXU]);
            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_S, GL_CLAMP_TO_EDGE);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            
            glActiveTexture(GL_TEXTURE2);
            glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXV]);
            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_S, GL_CLAMP_TO_EDGE);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            
        }
    
        #define FSH @"varying lowp vec2 TexCoordOut;\
        \
        uniform sampler2D SamplerY;\
        uniform sampler2D SamplerU;\
        uniform sampler2D SamplerV;\
        \
        void main(void)\
        {\
        mediump vec3 yuv;\
        lowp vec3 rgb;\
        \
        yuv.x = texture2D(SamplerY, TexCoordOut).r;\
        yuv.y = texture2D(SamplerU, TexCoordOut).r - 0.5;\
        yuv.z = texture2D(SamplerV, TexCoordOut).r - 0.5;\
        \
        rgb = mat3( 1,       1,         1,\
        0,       -0.39465,  2.03211,\
        1.13983, -0.58060,  0) * yuv;\
        \
        gl_FragColor = vec4(rgb, 1);\
        \
        }"
    
        #define VSH @"attribute vec4 position;\
        attribute vec2 TexCoordIn;\
        varying vec2 TexCoordOut;\
        \
        void main(void)\
        {\
        gl_Position = position;\
        TexCoordOut = TexCoordIn;\
        }"
    
        /**
         加载着色器
         */
        - (void)loadShader{
            /**
             1 编译着色
             */
            GLuint vertexShader = [self compileShader:VSH withType:GL_VERTEX_SHADER];
            GLuint fragmentShader = [self compileShader:FSH withType:GL_FRAGMENT_SHADER];
            
            /**
             2
             */
            //创建程序容器
            _program = glCreateProgram();
            //绑定shader到program
            glAttachShader(_program, vertexShader);
            glAttachShader(_program, fragmentShader);
            
            /**
             绑定需要在link之前
             */
            //把顶点属性索引绑定到顶点属性名
            glBindAttribLocation(_program, ATTRIB_VERTEX, "position");
            glBindAttribLocation(_program, ATTRIB_TEXTURE, "TexCoordIn");
            
            //链接
            glLinkProgram(_program);
            
            /**
             3
             */
            GLint linkSuccess;
            //查询相关信息,并将数据返回到linkSuccess
            glGetProgramiv(_program, GL_LINK_STATUS, &linkSuccess);
            if (linkSuccess == GL_FALSE) {
                GLchar messages[256];
                glGetProgramInfoLog(_program, sizeof(messages), 0, &messages[0]);
                NSString *messageString = [NSString stringWithUTF8String:messages];
                NSLog(@"<<<<着色器连接失败 %@>>>", messageString);
                //exit(1);
            }
            
            if (vertexShader)
            /*
             释放内存不能立刻删除
             If a shader object to be deleted is attached to a program object, it will be flagged for deletion, but it will not be deleted until it is no longer attached to any program object…
             shader 删除该着色器对象(如果一个着色器对象在删除前已经链接到程序对象中,那么当执行glDeleteShader函数时不会立即被删除,而是该着色器对象将被标记为删除,器内存被释放一次,它不再链接到其他任何程序对象)
             */
                glDeleteShader(vertexShader);
            if (fragmentShader)
                glDeleteShader(fragmentShader);
        }
    
        /**
         编译着色代码,使用着色器
         
         @param shaderString 代码
         @param shaderType   类型
         
         @return 成功返回着色器,失败返回-1
         */
        - (GLuint)compileShader:(NSString*)shaderString withType:(GLenum)shaderType
        {
            
            /**
             1
             */
            if (!shaderString) {
                //        NSLog(@"Error loading shader: %@", error.localizedDescription);
                exit(1);
            }
            else
            {
                //NSLog(@"shader code-->%@", shaderString);
            }
            
            /**
             2 分别创建一个顶点着色器对象和一个片段着色器对象
             */
            GLuint shaderHandle = glCreateShader(shaderType);
            
            /**
             3 分别将顶点着色程序的源代码字符数组绑定到顶点着色器对象,将片段着色程序的源代码字符数组绑定到片段着色器对象
             */
            const char * shaderStringUTF8 = [shaderString UTF8String];
            int shaderStringLength = [shaderString length];
            glShaderSource(shaderHandle, 1, &shaderStringUTF8, &shaderStringLength);
            
            /**
             4 分别编译顶点着色器对象和片段着色器对象
             */
            glCompileShader(shaderHandle);
            
            /**
             5
             */
            GLint compileSuccess;
            
            //获取编译情况
            glGetShaderiv(shaderHandle, GL_COMPILE_STATUS, &compileSuccess);
            if (compileSuccess == GL_FALSE) {
                GLchar messages[256];
                glGetShaderInfoLog(shaderHandle, sizeof(messages), 0, &messages[0]);
                NSString *messageString = [NSString stringWithUTF8String:messages];
                NSLog(@"%@", messageString);
                exit(1);
            }
            
            return shaderHandle;
        }
    
        #pragma mark -  接口
    
        /**
         设置大小
         
         @param width  界面宽
         @param height 界面高
         */
        -(void)setVideoSize:(GLuint)width height:(GLuint)height{
            
            //给宽高赋值
            _videoH = height;
            _videoW = width;
            
            //开辟内存空间
            //为什么乘1.5而不是1: width * hight =Y(总和) U = Y / 4   V = Y / 4
            void *blackData = malloc(width * height * 1.5);
            
            if (blackData) {
                
                /**
                 对内存空间清零,作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
                 
                 @param __b#>   源数据 description#>
                 @param __c#>   填充数据 description#>
                 @param __len#> 长度 description#>
                 
                 @return <#return value description#>
                 */
                memset(blackData, 0x0, width * height * 1.5);
            }
            /*
             Apple平台不允许直接对Surface进行操作.这也就意味着在Apple中,不能通过调用eglSwapBuffers函数来直接实现渲染结果在目标surface上的更新.
             在Apple平台中,首先要创建一个EAGLContext对象来替代EGLContext (不能通过eglCreateContext来生成), EAGLContext的作用与EGLContext是类似的.
             然后,再创建相应的Framebuffer和Renderbuffer.
             Framebuffer象一个Renderbuffer集(它可以包含多个Renderbuffer对象).
             
             Renderbuffer有三种:  color Renderbuffer, depth Renderbuffer, stencil Renderbuffer.
             
             渲染结果是先输出到Framebuffer中,然后通过调用context的presentRenderbuffer,将Framebuffer上的内容提交给之前的CustumView.
             */
            
            //设置当前上下文
            [EAGLContext setCurrentContext:_glContext];
            
            
            /*
             target —— 纹理被绑定的目标,它只能取值GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D或者GL_TEXTURE_CUBE_MAP;
             texture —— 纹理的名称,并且,该纹理的名称在当前的应用中不能被再次使用。
             glBindTexture可以让你创建或使用一个已命名的纹理,调用glBindTexture方法,将target设置为GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D或者GL_TEXTURE_CUBE_MAP,并将texture设置为你想要绑定的新纹理的名称,即可将纹理名绑定至当前活动纹理单元目标。当一个纹理与目标绑定时,该目标之前的绑定关系将自动被打破。纹理的名称是一个无符号的整数。在每个纹理目标中,0被保留用以代表默认纹理。纹理名称与相应的纹理内容位于当前GL rendering上下文的共享对象空间中。
             */
            
            //绑定Y纹理
            glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXY]);
            
            
            /**
             根据像素数据,加载纹理
             
             @param target#>         指定目标纹理,这个值必须是GL_TEXTURE_2D。 description#>
             @param level#>          执行细节级别。0是最基本的图像级别,n表示第N级贴图细化级别 description#>
             @param internalformat#> 指定纹理中的颜色格式。可选的值有GL_ALPHA,GL_RGB,GL_RGBA,GL_LUMINANCE, GL_LUMINANCE_ALPHA 等几种。 description#>
             @param width#>          纹理的宽度 description#>
             @param height#>         高度 description#>
             @param border#>         纹理的边框宽度,必须为0 description#>
             @param format#>         像素数据的颜色格式, 不需要和internalformatt取值必须相同。可选的值参考internalformat。 description#>
             @param type#>           指定像素数据的数据类型。可以使用的值有GL_UNSIGNED_BYTE,GL_UNSIGNED_SHORT_5_6_5,GL_UNSIGNED_SHORT_4_4_4_4,GL_UNSIGNED_SHORT_5_5_5_1等。 description#>
             @param pixels#>         指定内存中指向图像数据的指针 description#>
             
             @return <#return value description#>
             */
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RED_EXT, width, height, 0, GL_RED_EXT, GL_UNSIGNED_BYTE, blackData);
            
            //绑定U纹理
            glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXU]);
            //加载纹理
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RED_EXT, width/2, height/2, 0, GL_RED_EXT, GL_UNSIGNED_BYTE, blackData + width * height);
            
            //绑定V数据
            glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXV]);
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RED_EXT, width/2, height/2, 0, GL_RED_EXT, GL_UNSIGNED_BYTE, blackData + width * height * 5 / 4);
            
            //释放malloc分配的内存空间
            free(blackData);
        }
    
        /**
         清除画面
         */
        -(void)clearFrame{
            
            if ([self window])
            {
                [EAGLContext setCurrentContext:_glContext];
                glClearColor(0.0, 0.0, 0.0, 1.0);
                glClear(GL_COLOR_BUFFER_BIT);
                glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
                [_glContext presentRenderbuffer:GL_RENDERBUFFER];
            }
        }
    
        /**
         显示YUV数据
         
         @param data YUV数据
         @param w    <#w description#>
         @param h    <#h description#>
         */
        -(void)displayYUV420pData:(void *)data width:(NSInteger)w height:(NSInteger)h{
            
            if (!self.window) {
                return;
            }
            
            //加互斥锁,防止其他线程访问
            @synchronized (self) {
                
                if (w != _videoW || h != _videoH) {
                    [self setVideoSize:(GLuint)w height:(GLuint)h];
                }
                
                //设置当前上下文
                [EAGLContext setCurrentContext:_glContext];
                
                //绑定
                glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXY]);
                
                /**
                 更新纹理
                 https://my.oschina.net/sweetdark/blog/175784
                 @param target#>  指定目标纹理,这个值必须是GL_TEXTURE_2D。 description#>
                 @param level#>   执行细节级别。0是最基本的图像级别,n表示第N级贴图细化级别 description#>
                 @param xoffset#> 纹理数据的偏移x值 description#>
                 @param yoffset#> 纹理数据的偏移y值 description#>
                 @param width#>   更新到现在的纹理中的纹理数据的规格宽 description#>
                 @param height#>  高 description#>
                 @param format#>  像素数据的颜色格式, 不需要和internalformatt取值必须相同。可选的值参考internalformat。 description#>
                 @param type#>    颜色分量的数据类型 description#>
                 @param pixels#>  指定内存中指向图像数据的指针 description#>
                 
                 @return <#return value description#>
                 */
                glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, (GLsizei)w, (GLsizei)h, GL_RED_EXT, GL_UNSIGNED_BYTE, data);
                
                glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXU]);
                glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, (GLsizei)w/2, (GLsizei)h/2, GL_RED_EXT, GL_UNSIGNED_BYTE, data + w * h);
                
                glBindTexture(GL_TEXTURE_2D, _textureYUV[TEXV]);
                glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, (GLsizei)w/2, (GLsizei)h/2, GL_RED_EXT, GL_UNSIGNED_BYTE, data + w * h * 5 / 4);
                
                //渲染
                [self render];
            }
            
        #ifdef DEBUG
            
            GLenum err = glGetError();
            if (err != GL_NO_ERROR)
            {
                printf("GL_ERROR=======>%d\n", err);
            }
            struct timeval nowtime;
            gettimeofday(&nowtime, NULL);
            if (nowtime.tv_sec != _time.tv_sec)
            {
                printf("视频 %ld 帧率:   %d\n", self.tag, _frameRate);
                memcpy(&_time, &nowtime, sizeof(struct timeval));
                _frameRate = 1;
            }
            else
            {
                _frameRate++;
            }
        #endif
        }
    
        /**
         渲染
         */
        -(void)render{
            
            //设置上下文
            [EAGLContext setCurrentContext:_glContext];
            
            CGSize size = self.bounds.size;
            
            //把数据显示在这个视窗上
            glViewport(1, 1, size.width * _viewScale -2, size.height * _viewScale -2);
            
            /*
             我们如果选定(0, 0), (0, 1), (1, 0), (1, 1)四个纹理坐标的点对纹理图像映射的话,就是映射的整个纹理图片。如果我们选择(0, 0), (0, 1), (0.5, 0), (0.5, 1) 四个纹理坐标的点对纹理图像映射的话,就是映射左半边的纹理图片(相当于右半边图片不要了),相当于取了一张320x480的图片。但是有一点需要注意,映射的纹理图片不一定是“矩形”的。实际上可以指定任意形状的纹理坐标进行映射。下面这张图就是映射了一个梯形的纹理到目标物体表面。这也是纹理(Texture)比上一篇文章中记录的表面(Surface)更加灵活的地方。
             */
            static const GLfloat squareVertices[] = {
                -1.0f, -1.0f,
                1.0f, -1.0f,
                -1.0f,  1.0f,
                1.0f,  1.0f,
            };
            
            
            static const GLfloat coordVertices[] = {
                0.0f, 1.0f,
                1.0f, 1.0f,
                0.0f,  0.0f,
                1.0f,  0.0f,
            };
            
            //更新属性值
            glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, squareVertices);
            //开启定点属性数组
            glEnableVertexAttribArray(ATTRIB_VERTEX);
            
            
            glVertexAttribPointer(ATTRIB_TEXTURE, 2, GL_FLOAT, 0, 0, coordVertices);
            glEnableVertexAttribArray(ATTRIB_TEXTURE);
            
            //绘制
            
            //当采用顶点数组方式绘制图形时,使用该函数。该函数根据顶点数组中的坐标数据和指定的模式,进行绘制。
            //绘制方式,从数组的哪一个点开始绘制(一般为0),顶点个数
            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
            
            //将该渲染缓冲区对象绑定到管线上
            glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
            
            //把缓冲区(render buffer和color buffer)的颜色呈现到UIView上
            [_glContext presentRenderbuffer:GL_RENDERBUFFER];
            
        }
        @end
    

    相关文章

      网友评论

          本文标题:直播(二)(视音频流播放--IOS)

          本文链接:https://www.haomeiwen.com/subject/lwrljctx.html