FFmpeg学习之二 (yuv视频渲染)

yuv简介

1.yuv是什么

YUV是一种颜色编码方式,主要用于电视系统以及模拟视频领域,它将亮度信息(Y)与色彩信息(UV)分离,没有UV信息一样可以显示完整的图像,只不过是黑白的,这样的设计很好地解决了彩色电视机与黑白电视兼容的问题。
YUV不像传统RGB那样要求三个独立的视频信号同时传输,因此YUV方式传输视频占用较少带宽。

在过去,YUV 和 Y’UV被用作电视系统中颜色信息的特定模拟信息编码。而YCbCr被用作颜色信息的数字编码,通常适用于视频和静态图像的压缩和传输(MPEG, JPEG)。
今天,YUV通常用被用在计算机行业描述使用YCbCr编码的文件格式。

Y:表示明亮度(Luminance,Luma),也就是灰度值
U和V:色度(Chrominance,Chroma),描述影像色彩及饱和度。

2.yuv采集方式

如下图:实心圆圈代表Y,空心圆圈代表UV

  • YUV4:4:4 (每一个Y对应一组UV分量)
    在这里插入图片描述
  • YUV4:2:2 (每两个Y共用一组UV分量)
    在这里插入图片描述
  • YUV4:2:0 (每四个Y共用一组UV分量)

在这里插入图片描述

3.yuv存储方式

在以上三张图中,实心圆圈代表一个Y分量,空心圆圈代表一个UV分量,而因为又分三种不同的采集方式,即1个Y对应一组UV分量,2个Y共用一组UV分量,4个Y共用一组UV分量,
以YUV4:2:0为例,它又被分为YUV420P与YUV420SP,它们都是YUV420格式。

  • YUV420P, Plane模式(Y,U,V三个plane)将Y,U,V分量分别打包,依次存储。下图为I420.
    I420 : YYYYYYYY UU VV
    YV12 : YYYYYYYY VV UU

在这里插入图片描述

  • YUV420SP(NV12/NV21): two-plane模式,即Y和UV分为两个Plane,但是UV(CbCr)为交错存储,而不是分为三个plane. 下图为NV12
    NV12: YYYYYYYY UVUV
    NV21: YYYYYYYY VUVU

在这里插入图片描述

根据以上两幅图,我们以分辨率为640*480的图片为例,则它的大小为 Y : width(640) * height(480) 可认为每个Y即为每个像素点,又因为每4个Y共用一组UV,所以,U和V的大小都为: width(640) * height(480) * (1 / 4 ). 所以图片真正的大小为 Y+U+V = 3 / 2 * (width(640) * height(480)).

在程序中,比如一张图片的分辨率为640*480,如果该图片的格式为YUV420P,则我们可以很轻松的算出这张图片的Y,U,V三个分量。 我们用数组来存储该图像的大小byte[] src 则(I420)

 Y = src[width * height];
  U = Y + scr[1/4 * width * height];
  V = U + scr[1/4 * width * height];

4.yuv格式

YUV格式可分为两大类:打包(packed) , 平面(planar)

  • 打包(packed) : 将YUV分量存放在同一个数组中,通常是几个相邻的像素组成的一个宏像素(macro-pixel);
  • 平面(planar) : 使用三个数组分开存放YUV三个分量,就像一个三维平面。

更多YUV知识请参考大神文档:
参考文档:
详解YUV数据格式
图文详解YUV420数据格式
YUV

yuv视频渲染

1. iOS YUV视频渲染

1.1 IOS利用opengles 通过纹理方式渲染yuv视频

先通过一张图了解一下IOS 系统图形渲染架构
在这里插入图片描述

由于苹果已经在2018年底层渲染放弃了opengl,改用自己的渲染引擎Metal,推荐后续项目改成Metal来渲染,Metal将苹果的硬件能力发挥到了极致,效率比opengl高。

OpenGL ES是OpenGL的精简版本,主要针对于手机、游戏主机等嵌入式设备,它提供了一套设备图形硬件的软件接口,通过直接操作图形硬件,使我们能够高效地绘制图形。OpenGL在iOS架构中属于媒体层,与quartz(core graphics)类似,是相对底层的技术,可以控制每一帧的图形绘制。由于图形渲染是通过图形硬件(GPU)来完成的,相对于使用CPU,能够获得更高的帧率同时不会因为负载过大而造成卡顿。

openGL 渲染yuv视频流,主要是通过纹理方式,也就是将yuv转换成纹理显示出来。

这里简要介绍一下纹理

  • 纹理
    我们需要将YUV数据纹理的方式加载到OpenGL,再将纹理贴到之前创建矩形上,完成绘制。
    将每个顶点赋予一个纹理坐标,OpenGL会根据纹理坐标插值得到图形内部的像素值。OpenGL的纹理坐标系是归一化的,取值范围是0 - 1,左下角是原点。

在这里插入图片描述
三角形贴上纹理需要的纹理坐标

纹理目标是显卡的软件接口中定义的句柄,指向要进行当前操作的显存。
纹理对象是我们创建的用来存储纹理的显存,在实际使用过程中使用的是创建后返回的ID。
纹理单元是显卡中所有的可用于在shader中进行纹理采样的显存,数量与显卡类型相关,至少16个。在激活某个纹理单元后,纹理目标就该纹理单元,默认激活的是GL_TEXTURE0。

可以这么想象,纹理目标是转轮手枪正对弹膛的单孔,纹理对象就是子弹,纹理单元是手枪的六个弹孔。下面用代码说明它们之间的关系。

//创建一个纹理对象数组,数组里是纹理对象的ID
GLuint texture[3];
//创建纹理对象,第一个参数是要创建的数量,第二个参数是数组的基址
glGenTextures(3, &texture);
//激活GL_TEXTURE0这个纹理单元,用于之后的纹理采样
glActiveTexture(GL_TEXTURE0);
//绑定纹理对象texture[0]到纹理目标GL_TEXTURE_2D,接下来对纹理目标的操作都发生在此对象上
glBindTexture(GL_TEXTURE_2D, texture[0]);
//创建图像,采样工作在GL_TEXTURE0中完成,图像数据存储在GL_TEXTURE_2D绑定的对象,即texture[0]中。
glTexImage(GL_TEXTURE_2D, ...);
//解除绑定,此时再对GL_TEXTURE_2D不会影响到texture[0],texture[0]的内存不会回收。
glBindTexture(GL_TEXTURE_2D, 0);
//可以不断创建新的纹理对象,直到显存耗净


ios yuv视频渲染demo 下载

1.1.1 流程
  • 创建EAGLContext上下文对象
  1. 创建OpenGL预览层
    CAEAGLLayer *eaglLayer       = (CAEAGLLayer *)self.layer;
    eaglLayer.opaque = YES;
    eaglLayer.drawableProperties = @{
   
   kEAGLDrawablePropertyRetainedBacking   : [NSNumber numberWithBool:NO],
                                     kEAGLDrawablePropertyColorFormat       : kEAGLColorFormatRGBA8};
  1. 创建OpenGL上下文对象
    EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    [EAGLContext setCurrentContext:context];
  1. 设置上下文渲染缓冲区
- (void)setupBuffersWithContext:(EAGLContext *)context width:(int *)width height:(int *)height colorBufferHandle:(GLuint *)colorBufferHandle frameBufferHandle:(GLuint *)frameBufferHandle {
   
   
    glDisable(GL_DEPTH_TEST);
    
    glEnableVertexAttribArray(ATTRIB_VERTEX);
    glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), 0);
    
    glEnableVertexAttribArray(ATTRIB_TEXCOORD);
    glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), 0);
    
    glGenFramebuffers(1, frameBufferHandle);
    glBindFramebuffer(GL_FRAMEBUFFER, *frameBufferHandle);
    
    glGenRenderbuffers(1, colorBufferHandle);
    glBindRenderbuffer(GL_RENDERBUFFER, *colorBufferHandle);
    
    [context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer];
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH , width);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, height);
    
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, *colorBufferHandle);
}
  1. 加载着色器
  • 修改shader
    如果对GLSL语法与YUV不熟悉,可以看OpenGL的着色语言:GLSL和YUV颜色编码解析。

(1)顶点着色器
vertex shader

//vertex shader
ttribute vec4 position;
attribute mediump vec2 textureCoordinate;//要获取的纹理坐标
varying mediump vec2 coordinate;//传递给fragm shader的纹理坐标,会自动插值
 
void main(void) {
   
    
    gl_Position = vertexPosition; 
    coordinate = textureCoordinate;
}


(2)片源着色器
fragment shader

//fragment shader
precision mediump float;
 
uniform sampler2D SamplerY;//sample2D的常量,用来获取I420数据的Y平面数据
uniform sampler2D SamplerU;//U平面
uniform sampler2D SamplerV;//V平面
 
uniform sampler2D SamplerNV12_Y;//NV12数据的Y平面
uniform sampler2D SamplerNV12_UV;//NV12数据的UV平面
 
varying highp vec2 coordinate;//纹理坐标
 
uniform int yuvType;//0 代表 I420, 1 代表 NV12
 
//用来做YUV --> RGB 的变换矩阵
const vec3 delyuv = vec3(-0.0/255.0,-128.0/255.0,-128.0/255.0);
const vec3 matYUVRGB1 = vec3(1.0,0.0,1.402);
const vec3 matYUVRGB2 = vec3(1.0,-0.344,-0.714);
const vec3 matYUVRGB3 = vec3(1.0,1.772,0.0);
 
void main()
{
   
   
    vec3 CurResult;
    highp vec3 yuv;
 
    if (yuvType == 0){
   
   
        yuv.x = texture2D(SamplerY, coordinate).r;//因为是YUV的一个平面,所以采样后的r,g,b,a这四个参数的数值是一样的
        yuv.y = texture2D(SamplerU, coordinate).r;
        yuv.z = texture2D(SamplerV, coordinate).r;
    }
    else{
   
   
        yuv.x = texture2D(SamplerY, coordinate).r;
        yuv.y = texture2D(SamplerUV, coordinate).r;//因为NV12是2平面的,对于UV平面,在加载纹理时,会指定格式,让U值存在r,g,b中,V值存在a中。
        yuv.z = texture2D(SamplerUV, coordinate).a;//这里会在下面解释
    }
 
    yuv += delyuv;//读取值得范围是0-255,读取时要-128回归原值
    //用数量积来模拟矩阵变换,转换成RGB值
    CurResult.x = dot(yuv,matYUVRGB1);
    CurResult.y = dot(yuv,matYUVRGB2);
    CurResult.z = dot(yuv,matYUVRGB3);
    //输出像素值给光栅器
    gl_FragColor = vec4(CurResult.rgb, 1);
}


  • 加载shader
- (void)loadShaderWithBufferType:(KYLPixelBufferType)type {
   
   
    GLuint vertShader, fragShader;
    NSURL  *vertShaderURL, *fragShaderURL;
    
    NSString *shaderName;
    GLuint   program;
    program = glCreateProgram();
    
    if (type == KYLPixelBufferTypeNV12) {
   
   
        shaderName = @"KYLPreviewNV12Shader";
        _nv12Program = program;
    } else if (type == KYLPixelBufferTypeRGB) {
   
   
        shaderName = @"KYLPreviewRGBShader";
        _rgbProgram = program;
    }
    
    vertShaderURL = [[NSBundle mainBundle] URLForResource:shaderName withExtension:@"vsh"];
    if (![self compileShader:&vertShader type:GL_VERTEX_SHADER URL:vertShaderURL]) {
   
   
        log4cplus_error(kModuleName, "Failed to compile vertex shader");
        return;
    }
    
    fragShaderURL = [[NSBundle mainBundle] URLForResource:shaderName withExtension:@"fsh"];
    if (![self compileShader:&fragShader type:GL_FRAGMENT_SHADER URL:fragShaderURL]) {
   
   
        log4cplus_error(kModuleName, "Failed to compile fragment shader");
        return;
    }
    
    glAttachShader(program, vertShader);
    glAttachShader(program, fragShader);
    
    glBindAttribLocation(program, ATTRIB_VERTEX  , "position");
    glBindAttribLocation(program, ATTRIB_TEXCOORD, "inputTextureCoordinate");
    
    if (![self linkProgram:program]) {
   
   
        if (vertShader) {
   
   
            glDeleteShader(vertShader);
            vertShader = 0;
        }
        if (fragShader) {
   
   
            glDeleteShader(fragShader);
            fragShader = 0;
        }
        if (program) {
   
   
            glDeleteProgram(program);
            program = 0;
        }
        return;
    }
    
    if (type == KYLPixelBufferTypeNV12) {
   
   
        uniforms[UNIFORM_Y] = glGetUniformLocation(program , "luminanceTexture");
        uniforms[UNIFORM_UV] = glGetUniformLocation(program, "chrominanceTexture");
        uniforms[UNIFORM_COLOR_CONVERSION_MATRIX] = glGetUniformLocation(program, "colorConversionMatrix");
    } else if (type == XDXPixelBufferTypeRGB) {
   
   
        _displayInputTextureUniform = glGetUniformLocation(program, "inputImageTexture");
    }
    
    if (vertShader) {
   
   
        glDetachShader(program, vertShader);
        glDeleteShader(vertShader);
    }
    if (fragShader) {
   
   
        glDetachShader(program, fragShader);
        glDeleteShader(fragShader);
    }
}

- (BOOL)compileShader:(GLuint *)shader type:(GLenum)type URL:(NSURL *)URL {
   
   
    NSError *error;
    NSString *sourceString = [[NSString alloc] initWithContentsOfURL:URL
                                                            encoding:NSUTF8StringEncoding
                                                               error:&error];
    if (sourceString == nil) {
   
   
        log4cplus_error(kModuleName, "Failed to load vertex shader: %s", [error localizedDescription].UTF8String);
        return NO;
    }
    
    GLint status;
    const GLchar *source;
    source = (GLchar *)[sourceString UTF8String];
    
    *shader = glCreateShader(type);
    glShaderSource(*shader, 1, &source, NULL);
    glCompileShader(*shader);
    
    glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);
    if (status == 0) {
   
   
        glDeleteShader(*shader);
        return NO;
    }
    return YES;
}

- (BOOL)linkProgram:(GLuint)prog {
   
   
    GLint status;
    glLinkProgram(prog);
    
    glGetProgramiv(prog, GL_LINK_STATUS, &status);
    if (status == 0) {
   
   
        return NO;
    }
    return YES;
}
  1. 创建视频纹理缓存区
    if (!*videoTextureCache) {
   
   
        CVReturn err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, context, NULL, videoTextureCache);
        if (err != noErr)
            log4cplus_error(kModuleName, "Error at CVOpenGLESTextureCacheCreate %d",err);
    }
  • 加载YUV数据到纹理对象
//创建纹理对象,需要3个纹理对象来获取不同平面的数据
-(void)setupTexture{
   
   
    _planarTextureHandles = (GLuint *)malloc(3*sizeof(GLuint));
    glGenTextures(3, _planarTextureHandles);
}
-(void)feedTextureWithImageData:(Byte*)imageData imageSize:(CGSize)imageSize type:(NSInteger)type{
   
   
    //根据YUV编码的特点,获得不同平面的基址
    Byte * yPlane =  imageData;
    Byte * uPlane =  imageData + imageSize.width*imageSize.height;
    Byte * vPlane =  imageData + imageSize.width*imageSize.height * 5 / 4;
    if (type == 0) {
   
   
        [self textureYUV:yPlane widthType:imageSize.width heightType:imageSize.height index:0];
        [self textureYUV:uPlane widthType:imageSize.width/2 heightType:imageSize.height/2 index:1];
        [self textureYUV:vPlane widthType:imageSize.width/2 heightType:imageSize.height/2 index:2];
    }else{
   
   
        [self textureYUV:yPlane widthType:imageSize.width heightType:imageSize.height index:0];
        [self textureNV12:uPlane widthType:imageSize.width/2 heightType:imageSize.height/2 index:1];
    }
}
- (void) textureYUV: (Byte*)imageData widthType: (int) width heightType: (int) height index: (int) index
{
   
   
    //将纹理对象绑定到纹理目标
    glBindTexture(GL_TEXTURE_2D, _planarTextureHandles[index]);
    //设置放大和缩小时,纹理的过滤选项为:线性过滤
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    //设置纹理X,Y轴的纹理环绕选项为:边缘像素延伸
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    //加载图像数据到纹理,GL_LUMINANCE指明了图像数据的像素格式为只有亮度,虽然第三个和第七个参数都使用了GL_LUMINANCE,
    //但意义是不一样的,前者指明了纹理对象的颜色分量成分,后者指明了图像数据的像素格式
    //获得纹理对象后,其每个像素的r,g,b,a值都为相同,为加载图像的像素亮度,在这里就是YUV某一平面的分量值
    glTexImage2D( GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, imageData );
    //解绑
    glBindTexture(GL_TEXTURE_2D, 0);
}


  • CADisplayLink定时绘制

现在已经能够将YUV数据加载到纹理对象了,下一步来改造render方法,将其绘制到屏幕上。可以用CADisplayLink定时调用render方法,可以根据屏幕刷新频率来控制YUV视频流的帧率。

- (void)render {
   
   
    //绘制黑色背景
    glClearColor(0, 0, 0
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值