美文网首页
直播(二)(视音频流播放--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)

    @TOC 视频流播放通常用的是两种方式: 将yuv视频流转化成一帧帧图片,如yuv420转换为RGB 直接用ope...

  • iOS直播延时问题的解决方案

    《直播疑难杂症排查》之四:延时高 ijkplayer--简易ios播放器和参数设置 ijkplay播放直播流延时控...

  • 视频播放及FFmpeg学习笔记

    播放流程 视频播放器播放网络视频,需要经过以下步骤:解协议,解封装,解码视音频,视音频同步。如果播放本地视频则不需...

  • Android直播之基本概念(二)

    1.直播的采集端(推流端)和播放端 直播的流程: 播放流程: 获取流–>解码–>播放录制播放路程: 录制音频视频–...

  • 直播(一)(视音频流缓存)

    @TOC 缓存队列实现 源码下载 视频音频缓存队列实现源码demo 1.原理 初始化固定数量的结点装入空闲队列,当...

  • IJKMediaFramework、LFLiveKit实现视频直

    视频直播 拉流: 从服务器获取视频直播流地址,播放直播 使用IJKMediaFramework.framework...

  • 视频播放器的基本原理

    视频播放器播放一个互联网上的视频文件,需要经过以下几个步骤:解协议,解封装,解码视音频,视音频同步。如果播放本地文...

  • 多媒体编程学习01

    视频播放器播放一个互联网上的视频文件,需要经过以下几个步骤:解协议,解封装,解码视音频,视音频同步。如果播放本地文...

  • 视音频播放

    视音频播放可以说是视频开发中比较简单的了,只要创建几个对象,监听一些属性值即可完成一个视音频播放器。 我们需要了解...

  • ios-后台播放视频、直播流

    关于ios-后台播放 后台播放的不是新的技术;后台播放在音乐播放器上得到了广泛的运用;想了解和实现的原因:看到B站...

网友评论

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

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