ffmpeg开发播放器学习笔记 - 硬解码,OpenGL渲染YUV

该节是ffmpeg开发播放器学习笔记的第四节《硬解码,OpenGL渲染YUV》

<font color="gray">硬解码基本上(这里也可以指特定的硬件)指的是GPU来完成解码。

CPU被设计成能用处理器,它有着高灵活性,高可移植性。而GPU则侧重于计算运算量大但任务相对单一的处理器,它有着极强的并行计算能力,利用GPU来完成视频帧的解码将会减少CPU的使用率。
硬解码利用了GPU特定的电路设计,所以不同平台的GPU支持的硬解码格式也是有限的。
比如iOS/macOS平台支持H264与H265的硬件解码,利用的是videotoolbox来完成;基于intel芯片的QSV,基于NVIDA的CUDA。</font>

image.png

✅ 第一节 - Hello FFmpeg
✅ 第二节 - 软解视频流,渲染 RGB24
✅ 第三节 - 认识YUV
🔔 第四节 - 硬解码,OpenGL渲染YUV
📗 第五节 - Metal 渲染YUV
📗 第六节 - 解码音频,使用AudioQueue 播放
📗 第七节 - 音视频同步
📗 第八节 - 完善播放控制
📗 第九节 - 倍速播放
📗 第十节 - 增加视频过滤效果
📗 第十一节 - 音频变声

该节 Demo 地址: https://github.com/czqasngit/ffmpeg-player/releases/tag/Hello-FFmpeg

实例代码提供了Objective-CSwift两种实现,为了方便说明,文章引用的是Objective-C代码,因为Swift代码指针看着不简洁。

该节最终效果如下图:

image

image

目标

  • 了解ffmpeg硬解码与软解码的差异
  • 添加硬解码功能
  • 了解OpenGL渲染流程
  • 搭建OpenGL环境
  • 利用OpenGL渲染YUV420P格式的数据

了解ffmpeg硬解码与软解码的差异

ffmpeg中支持的硬件类型定义如下:

enum AVHWDeviceType {
    AV_HWDEVICE_TYPE_NONE,
    AV_HWDEVICE_TYPE_VDPAU,
    AV_HWDEVICE_TYPE_CUDA,
    AV_HWDEVICE_TYPE_VAAPI,
    AV_HWDEVICE_TYPE_DXVA2,
    AV_HWDEVICE_TYPE_QSV,
    AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
    AV_HWDEVICE_TYPE_D3D11VA,
    AV_HWDEVICE_TYPE_DRM,
    AV_HWDEVICE_TYPE_OPENCL,
    AV_HWDEVICE_TYPE_MEDIACODEC,
    AV_HWDEVICE_TYPE_VULKAN,
};

下图为硬解码视频流完整流程图:


image

大致的差异如下:

  • 1.创建好AVCodecContext的时候设置它的硬解码上下文hw_device_ctx。
  • 2.可选设置AVCodecContext的目标格式回调函数get_format,在运行时告知ffmpeg解码器解码的目标格式
  • 3.将解码好的数据从显存中读取到内存

添加硬解码功能

1.完善硬解码时的初始化

在macOS中利用VideoToolBox完成硬解码,这里指定格式为: AV_HWDEVICE_TYPE_VIDEOTOOLBOX。
也可以通过以下函数来查找

 av_hwdevice_find_type_by_name("h264_videotoolbox")

VideoToolBox的硬解码器信息定义如下:

const AVHWAccel ff_h264_videotoolbox_hwaccel = {
    .name           = "h264_videotoolbox",
    .type           = AVMEDIA_TYPE_VIDEO,
    .id             = AV_CODEC_ID_H264,
    .pix_fmt        = AV_PIX_FMT_VIDEOTOOLBOX,
    .alloc_frame    = ff_videotoolbox_alloc_frame,
    .start_frame    = ff_videotoolbox_h264_start_frame,
    .decode_slice   = ff_videotoolbox_h264_decode_slice,
    .decode_params  = videotoolbox_h264_decode_params,
    .end_frame      = videotoolbox_h264_end_frame,
    .frame_params   = videotoolbox_frame_params,
    .init           = videotoolbox_common_init,
    .uninit         = videotoolbox_uninit,
    .priv_data_size = sizeof(VTContext),
};

判断当前运行环境下的AVCodec是否支持AV_HWDEVICE_TYPE_VIDEOTOOLBOX

int hwConfigIndex = 0;
    bool supportAudioToolBox = false;
    /// 判断当前解码器是否支持AV_HWDEVICE_TYPE_VIDEOTOOLBOX硬解
    /// 某些视频格式的视频解码器不支持
    while (true) {
        const AVCodecHWConfig *config = avcodec_get_hw_config(self->codec, hwConfigIndex);
        if(!config) break;
        if(config->device_type == AV_HWDEVICE_TYPE_VIDEOTOOLBOX) {
            supportAudioToolBox = true;
            break;
        }
        hwConfigIndex ++;
    }

通过调用avcodec_get_hw_config函数来枚举支持的硬件解码配置,读取AVCodecHWConfig是否支持AV_HWDEVICE_TYPE_VIDEOTOOLBOX。

创建硬件解码的实例,设置AVCodecContext->hw_device_ctx硬解码上下文。

AVBufferRef *hw_device_ctx = NULL;
if(supportAudioToolBox) {
    /// 创建硬件解码上下文,并指定硬件解码的格式
    /// 由于已经在上面判断了当前环境中是否支持AV_HWDEVICE_TYPE_VIDEOTOOLBOX,这里直接指定
    ret = av_hwdevice_ctx_create(&hwDeviceContext, AV_HWDEVICE_TYPE_VIDEOTOOLBOX, NULL, NULL, 0);
    if(ret != 0) goto fail;
    self->codecContext->hw_device_ctx = self->hwDeviceContext;
    /// 告知硬件解码器,解码输出格式
    /// 这个回调函数在被调用时会给出一组当前AVCodec支持的解码格式
    /// 这个数组按解码性能从高到低排列®
    /// 开发者可以按需返回一个最合适的
    /// decode时,开发者不设置则使用av_hwdevice_ctx_create创建时指定的格式
    /// 不能设置成NULL
    self->hwFrame = av_frame_alloc();
}

AVCodecContext中的get_format是一个回调函数,目的是告诉解码器(AVCodecContext)它的目标格式是什么。它的函数签名是这样的:

enum AVPixelFormat get_hw_format(struct AVCodecContext *s, const enum AVPixelFormat *fmt);

其中*fmt是一个数组首地址,AVCodec可能有多个支持的解码格式列表。可以这样来遍历找到你想要的那个格式

for (p = pix_fmts; *p != -1; p++) {
    AVPixelFormat fmt = *p;
    /// _hw_pix_fmt是你的当前硬件设备支持的并且是你想使用的格式
    /// 在iOS/macOS里它是AV_PIX_FMT_VIDEOTOOLBOX
    if (fmt == AV_HWDEVICE_TYPE_VIDEOTOOLBOX) {
    }
}

提供这个回调函数的意义在于当解码器支持多种格式时,可以根据适当的需要选择不同的格式。

这里需要说明的是,通过AVCodecHWConfig已经判断了当前AVCodec在当前的运行环境中支持AV_HWDEVICE_TYPE_VIDEOTOOLBOX,那get_format也可以不设置。ffmpeg会选择在av_hwdevice_ctx_create函数中指定的格式。

到此,添加硬解解码的初始化工作就完成了

2.完善硬解码功能

ret = avcodec_receive_frame(self.codecContext, self->hwFrame);
if(ret != 0) return NULL;
av_frame_unref(self->frame);
ret = av_hwframe_transfer_data(self->frame, self->hwFrame, 0);

最终hwFrame的数据帧格式是AV_HWDEVICE_TYPE_VIDEOTOOLBOX, frame是解码后的一帧数据,在macOS中利用VideoToolBox硬解出来的格式是NV12。它与YUV420P是兄弟,YUV420P将YUV分别 存储在三个平面,NV12将Y单独存储,UV数据存放到一个独立的平面交叉存储。

了解OpenGL渲染流程

OpenGL介绍

<font color="gray">
使用CPU处理与渲染图像数据会占据CPU大量的资源,相对于CPU,GPU更适合处理简单重复并发数多的任务。
要使用GPU来计算或者渲染图像有很多方案,OpenGL就是其中的一种。
OpenGL本身并不是一个API,它仅仅是一个由Khronos组织制定并维护的规范。实际的OpenGL库的开发者通常是显卡的生产商。你购买的显卡所支持的OpenGL版本都为这个系列的显卡专门开发的。当你使用Apple系统的时候,OpenGL库是由Apple自身维护的。在Linux下,有显卡生产商提供的OpenGL库,也有一些爱好者改编的版本。这也意味着任何时候OpenGL库表现的行为与规范规定的不一致时,基本都是库的开发者留下的bug。

OpenGL自身是一个状态机,每个状态都有不同的变量维护,而这些变量都维护在一个上下文环境中。状态不发生变化,绘制出来的图像就是一致的,需要改变某种绘制效果实际上就是改变它的某种状态。

早期的OpenGL使用的是固定渲染管线,这种方式使用起来很方便,很多实现细节都被隐藏了起来,但是灵活性不够,开发者想要更大的自由更灵活的功能。OpenGL 3.2开始就使用了可编程管线的方式,灵活性得到了大大的提高,但使用起来也更复杂了。</font>

渲染管线

<font color="gray">
在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。
3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。
图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。

图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。

些着色器允许开发者自己配置,这就允许我们用自己写的着色器来替换默认的。这样我们就可以更细致地控制图形渲染管线中的特定部分了,而且因为它们运行在GPU上,所以它们可以给我们节约宝贵的CPU时间。OpenGL着色器是用OpenGL着色器语言(OpenGL Shading Language, GLSL)写成的。
</font>

下面这张图说明了管线的流程:

image

顶点着色器: 接收一组顶点数据,可描述顶点的位置,颜色等。

图元装配: 将顶点着色器输出的所有顶点作为输入,并所有的点装配成指定图元的形状。


几何着色器: 接收来自图元装配生成的图像通过进一步计算,生成新图形。
光栅化: 会把图形映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段。在片段着色器运行之前会执行裁切。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

片段着色器:主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。

测试与混合:在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值(后面会讲),用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。


在整个流程中,开发者至少需要实现一个顶点着色器与一个片段着色器。

搭建OpenGL环境

<font color="gray">macOS环境下Apple已经不建议使用OpenGL了,推荐使用Metal。由于代码与理念是可以移植的,为了方便还是在macOS上继续使用OpenGL。</font>

1.创建NSOpenGLContext

macOS中可以利用NSOpenGLView来绘制OpenGL,NSOpenGLView也是一个NSView,所以它可以添加到任意的视图中,创建一个类继承至NSOpenGLView,并设置NSOpenGLContext。

- (NSOpenGLContext *)_createOpenGLContext {
    NSOpenGLPixelFormatAttribute attr[] = {
        NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
        NSOpenGLPFANoRecovery,
        NSOpenGLPFAAccelerated,
        NSOpenGLPFADoubleBuffer,
        NSOpenGLPFAColorSize, 24,
        0
    };
    NSOpenGLPixelFormat *pf = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr];
    if (!pf) {
        NSLog(@"No OpenGL pixel format");
    }
    NSOpenGLContext *openGLContext = [[NSOpenGLContext alloc] initWithFormat:pf shareContext: nil] ;
    return openGLContext;
}
[self setOpenGLContext:[ self _createOpenGLContext]];

2.绘制图像

OpenGL绘制的大致流程是这样的:

image
/// 由于OpenGL可以在子线程绘制,为了保证线程安全,这里将上下文上锁
CGLLockContext([self.openGLContext CGLContextObj]);
/// 设置当前OpenGL函数操作的上下文
[self.openGLContext makeCurrentContext];
/// 清屏
glClearColor(0.0, 0.0, 0, 0);
/// 清理颜色缓冲区
glClear(GL_COLOR_BUFFER_BIT);
/// 启用2D纹理
glEnable(GL_TEXTURE_2D);
/// xxx 这里是需要绘制的图像或者纹理的代码
/// 提交绘制
[self.openGLContext flushBuffer];
/// 解锁
CGLUnlockContext([self.openGLContext CGLContextObj]);

上面的代码,可以看到一个黑色的屏幕视图。这是OpenGL绘制每一帧必要的步骤

3.初始化OpenGL 小程序

OpenGL的顶点与片段着色器是使用GLSL语言来编写的,每个小程序都有一个main函数。


顶点着色器


#version 410

layout (location = 0) in vec3 pos;
layout (location = 1) in vec2 textPos;

out vec2 outTextPos;

void main() {
    gl_Position = vec4(pos, 1.0);
    outTextPos = textPos;
}

layout (location = 0): 定义了这个变量的位置,在后续编写的CPU代码中可以直接使用位置给指定顶点数据。

in: 表示这个变量的数据是从外面传过来的,它可以是从CPU过来的数据,也可以是从上一个着色器过来的数据。

vec3: 定义了一个三个变量的数据对象。

vec2: 定义了一个两个变量的数据对象。

out: 表示这个变量是输出到下一个着色器程序的,一接下来的着色器中使用同样名称的变量即可得到从当前着色器程序中传递出去的数据。

gl_Position:是顶点着色器程序内置的变量表示当前这个点的位置。



片段着色器

#version 410
out vec4 FragColor;
in vec2 outTextPos;
uniform sampler2D yTexture;
uniform sampler2D uTexture;
uniform sampler2D vTexture;
void main()
{
    float y = texture(yTexture, outTextPos).r;
    float cb = texture(uTexture, outTextPos).r;
    float cr = texture(vTexture, outTextPos).r;
    
    
    /// 按YCbCr转RGB的公式进行数据转换
    float r = y + 1.403 * (cr - 0.5);
    float g = y - 0.343 * (cb - 0.5) - 0.714 * (cr - 0.5);
    float b = y + 1.770 * (cb - 0.5);
    // 通过纹理坐标数据来获取对应坐标色值并传递
    FragColor = vec4(r, g, b, 1.0);
}

FragColor: 是空间中某个像素点的具体的颜色值,很早期的OpenGL版本中是通过片段着色器中内置gl_FragColor输出具体要显示的颜色值,现在的版本中只需要简单的定义一个输出变量即可。

uniform sampler2D: 定义了一个纹理变量,纹理变量可以理解成一个有形状的二进制数据格式。sampler2D表示一个有宽高的平面数据,sampler3D表面长宽高的3D文体数据。uniform 修饰的变量为用户传递给着色器的数据,它在所有可用的着色阶段都是不变的,它必须定义为全局变量,并且存储在程序对象(Program Object)中。

编译着色器
编写好的着色器程序源码需要使用OpenGL提供的编译函数进行编译

/// 编译着色器
- (GLuint)_compileShader:(NSString *)shaderName shaderType:(GLuint)shaderType {
    if(shaderName.length == 0) return -1;
    NSString *shaderPath = [[NSBundle mainBundle] pathForResource:shaderName ofType:@"glsl"];
    NSError *error;
    NSString *source = [NSString stringWithContentsOfFile:shaderPath encoding:NSUTF8StringEncoding error:&error];
    if(error) return -1;
    GLuint shader = glCreateShader(shaderType);
    const char *ss = [source UTF8String];
    glShaderSource(shader, 1, &ss, NULL);
    glCompileShader(shader);
    int  success;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
    if(!success) {
        char infoLog[512];
        glGetShaderInfoLog(shader, 512, NULL, infoLog);
        printf("shader error msg: %s \n", infoLog);
    }
    return shader;
}

链接着色器
编译好的shader(着色器)需要使用OpenGL提供的program来管理。也许你的程序可能会有多个绘制逻辑,你可以将上下文中的program切换成你需要使用的program,在绘制时就会使用当前状态设置的顶点与片段着色器。

- (BOOL)_setupOpenGLProgram {
    /// 设置当前OpenGL函数操作的上下文
    [self.openGLContext makeCurrentContext];
    /// 创建glProgram
    _glProgram = glCreateProgram();
    /// 编译顶点着色器 
    _vertextShader = [self _compileShader:@"vertex" shaderType:GL_VERTEX_SHADER];
    /// 编译片段着色器
    _fragmentShader = [self _compileShader:@"yuv_fragment" shaderType:GL_FRAGMENT_SHADER];
    /// 将顶点着色器添加到OpenGL小程序中
    glAttachShader(_glProgram, _vertextShader);
    /// 将片段着色器添加到OpenGL小程序中
    glAttachShader(_glProgram, _fragmentShader);
    /// 连接着色器程序
    glLinkProgram(_glProgram);
    GLint success;
    glGetProgramiv(_glProgram, GL_LINK_STATUS, &success);
    if(!success) {
        char infoLog[512];
        glGetProgramInfoLog(_glProgram, 512, NULL, infoLog);
        printf("Link shader error: %s \n", infoLog);
    }
    glDeleteShader(_vertextShader);
    glDeleteShader(_fragmentShader);
    return success;
}

4.初始化OpenGL对象

VBO(顶点缓冲对象)

在OpenGL中有很多的对象,首先需要使用的是VBO是OpenGL中的一个很重要的对象。
GPU不能读写内存数据,需要将要在着色器程序中使用的数据放到显存中,GPU才可以访问。VBO就是在显存里开辟了一块显存区域,CPU可以通过VBO将数据发送到显存中,在顶点着色器中就需要访问VBO中的顶点数据。
它的实现是这样的:

/// 创建顶点缓存对象
glGenBuffers(1, &_VBO);
/// 顶点数据
float vertices[] = {
    // positions        // texture coords
    1.0f,  1.0f, 0.0f,  1.0f, 0, // top right
    1.0f, -1.0f, 0.0f,  1.0f, 1, // bottom right
   -1.0f, -1.0f, 0.0f,  0.0f, 1, // bottom left
   -1.0f, -1.0f, 0.0f,  0.0f, 1, // bottom left
   -1.0f,  1.0f, 0.0f,  0.0f, 0, // top left
    1.0f,  1.0f, 0.0f,  1.0f, 0, // top right
};
/// 绑定顶点缓存对象到当前的顶点位置,之后对GL_ARRAY_BUFFER的操作即是对_VBO的操作
/// 同时也指定了_VBO的对象类型是一个顶点数据对象
glBindBuffer(GL_ARRAY_BUFFER, _VBO);
/// 将CPU数据发送到GPU,数据类型GL_ARRAY_BUFFER
/// GL_STATIC_DRAW 表示数据不会被修改,将其放置在GPU显存的更合适的位置,增加其读取速度
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

/// 指定顶点着色器位置为0的参数的数据读取方式与数据类型
/// 第一个参数: 参数位置
/// 第二个参数: 一次读取数据
/// 第三个参数: 数据类型
/// 第四个参数: 是否归一化数据
/// 第五个参数: 间隔多少个数据读取下一次数据
/// 第六个参数: 指定读取第一个数据在顶点数据中的偏移量
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
/// 启用顶点着色器中位置为0的参数
glEnableVertexAttribArray(0);

/// 指定纹理顶点的数据
/// 这里的纹理的Y坐标与是翻了的,因为在绘制纹理的时候坐标与图像坐标是颠倒的
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

VAO(顶点数据对象)

配置VBO,从CPU将数据发送到GPU这个过程逻辑固定,如果需要绘制的图形复杂,顶点很多的话每次都去重新配置一次数据就显得不是那么必要了。OpenGL提供了VAO来帮助开发者简化这一过程。

创建VAO,然后将当前上下文状态绑定成当前的VAO,那么接下来对VBO的操作都会被这个VAO对象记录下来。在绘制的时候就只需要配置绑定指定的VAO就可以了。这样做还有一个好处,如果绘制过程会涉及到多个VBO对象的数据切换,那么使用VAO提前记录下配置状态就显得更重要了,切换VAO就行了。
它的代码看上去是这样的:

/// 创建VAO
glGenVertexArrays(1, &_VAO);
/// 将当前上下文的VAO绑定成_VAO,接下来对_VBO的操作都将被_VAO记录下来
glBindVertexArray(_VAO);
"这里是VBO对象的数据配置过程"
/// 解除VAO的绑定
glBindVertexArray(0);

OpenGL中顶点数据都是标准化的,位置数据的取值范围是[-1, 1],纹理位置的取值范围是[0, 1]。
顶点数据使用了三个点来配置,其中Z轴始终都是0。这里也可以使用2个值 X,Y来配置,在顶点着色器中将Z轴的值 设置成0也是可以的。纹理绘制的是2D纹理,所以选择两个值来配置。

纹理对象

创建过程:

/// 创建纹理对象
glGenTextures(1, &_yTexture);
/// 将当前上下文纹理对象设置成_yTexture
glBindTexture(GL_TEXTURE_2D, _yTexture);
/// 设置纹理采样时超出与缩小图像时的采样方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
/// 设置纹理多级渐远
glGenerateMipmap(GL_TEXTURE_2D);
/// 解除绑定
glBindTexture(GL_TEXTURE_2D, 0);

• 1.创建纹理对象,在显存中开辟了一块内存用于创建纹理对象。

• 2.绑定当前上下文的纹理对象,绑定之后使用OpenGL函数对纹理对象的操作就变成了对绑定的纹理对象的操作。

• 3.设置纹理超出与缩小图像时的采样方式。

• 4.设置纹理多级渐远模式,在图像缩小时OpenGL采样会按人眼看远方的方式来对图像像素点进行采样。人眼看得越远图像看上去就越小,OpenGL会模拟这种特点,对图像像素点进行不同等级的远度进行不同比例的采样。当我们在缩小图像的时候,它看上去更真实。

• 5.解除绑定,由于可能对其它纹理进行设置,需要解除绑定,在绘制的时候再绑定。



Y、U、V三个纹理都按照同样的方式来创建即可。

纹理对象的绘制过程就是对二维图像数据的采样并转换成RGB显示的过程。采样看上去是这样的:
当绘制一个三角形时,根据配置的顶点与纹理坐标的对应关系,OpenGL决定对二维图像哪些点进行采样,采样完成后就绘制出来一个三角形纹理图像。

image

利用OpenGL渲染YUV420P格式的数据

VBO的数据配置在初始化的时候已经完成了,它是固定不变的。


配置纹理对象

找到片段中的着色器程序,并将数据发送到GPU

/// 激活纹理0,接下来的gl函数操作纹理相关的都是对纹理0的操作
glActiveTexture(GL_TEXTURE0);
/// 将yTexture变量与纹理0绑定,对纹理0的操作都是对yTexture代码的显存进行操作
glBindTexture(GL_TEXTURE_2D, _yTexture);
/// 将数据发送到GPU
/*
第一个参数target: 指定目标纹理,这个值必须是GL_TEXTURE_2D。
第二个参数level: 执行细节级别。0是最基本的图像级别,n表示第N级贴图细化级别。
第三个参数internalformat: 指定纹理中的颜色组件。可选的值有GL_ALPHA,GL_RGB,GL_RGBA,GL_LUMINANCE, GL_LUMINANCE_ALPHA 等几种。它指定了纹理在显存中的数据格式。
第四个参数width: 指定纹理图像的宽度,必须是2的n次方。纹理图片至少要支持64个材质元素的宽度
第五个参数height: 指定纹理图像的高度,必须是2的m次方。纹理图片至少要支持64个材质元素的高度
第六个参数border: 指定边框的宽度。必须为0。
第七个参数format: 像素数据的颜色格式, 不需要和internalformatt取值必须相同。可选的值参考internalformat。它指定了数据来源的数据格式。
第八个参数type: 指定像素数据的数据类型。可以使用的值有GL_UNSIGNED_BYTE,GL_UNSIGNED_SHORT_5_6_5,GL_UNSIGNED_SHORT_4_4_4_4,GL_UNSIGNED_SHORT_5_5_5_1。
第九个参数pixels: 指定内存中指向图像数据的指针
*/
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, videoWidth, videoHeight, 0, GL_RED, GL_UNSIGNED_BYTE, yuvFrame->data[0]);
/// 将纹理0与片段着色器中的yTexture变量进行关联
glUniform1i(glGetUniformLocation(_glProgram, "yTexture"), 0);

完整的绘制流程

int videoWidth = yuvFrame->width;
int videoHeight = yuvFrame->height;
CGLLockContext([self.openGLContext CGLContextObj]);
[self.openGLContext makeCurrentContext];
glClearColor(0.0, 0.0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
glEnable(GL_TEXTURE_2D);
glUseProgram(_glProgram);

/// Y
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, _yTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, videoWidth, videoHeight, 0, GL_RED, GL_UNSIGNED_BYTE, yuvFrame->data[0]);
glUniform1i(glGetUniformLocation(_glProgram, "yTexture"), 0);

/// U
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, _uTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, videoWidth / 2, videoHeight / 2, 0, GL_RED, GL_UNSIGNED_BYTE, yuvFrame->data[1]);
glUniform1i(glGetUniformLocation(_glProgram, "uTexture"), 1);

/// V
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, _vTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, videoWidth / 2, videoHeight / 2, 0, GL_RED, GL_UNSIGNED_BYTE, yuvFrame->data[2]);
glUniform1i(glGetUniformLocation(_glProgram, "vTexture"), 2);

glBindVertexArray(_VAO);
glDrawArrays(GL_TRIANGLES, 0, 6);
[self.openGLContext flushBuffer];

CGLUnlockContext([self.openGLContext CGLContextObj]);

如果按照上面配置的顶点数据绘制纹理图像,得到的渲染图像是这样的:


image

这是因为绘制三角形的0点坐标在左下角,而图像的起始坐标是在左上角。

将纹理坐标的Y值调到一下,0改成1 ,1改成0就可以了。

/// 顶点数据
float vertices[] = {
    // positions        // texture coords
    1.0f,  1.0f, 0.0f,  1.0f, 0, // top right
    1.0f, -1.0f, 0.0f,  1.0f, 1, // bottom right
   -1.0f, -1.0f, 0.0f,  0.0f, 1, // bottom left
   -1.0f, -1.0f, 0.0f,  0.0f, 1, // bottom left
   -1.0f,  1.0f, 0.0f,  0.0f, 0, // top left
    1.0f,  1.0f, 0.0f,  1.0f, 0, // top right
};

到此,使用OpenGL来加速渲染与使用GPU加速解码的大致流程与逻辑就完成了。利用了GPU来完成解码与渲染后,CPU的使用率大大下降了。

总结:

• 硬解码基本上指利用GPU特定的电路来完成指定数据格式的解码,不同的GPU支持的硬件解码格式有差异。

• CPU能用性强,覆盖面广。GPU对某种特定的功能性能上有优势,比如硬件解码。

• 在软解码的基础上完善硬件解码的初始化,解码及数据读取。

• 大致了解了OpenGL的渲染流程及特点。

• 在macOS平台上使用NSOpenGLView与NSOpenGLContext搭建OpenGL绘制环境。

• 了解着色器程序简单语法,编写着色器小程序,利用OpenGL渲染YUV420P格式的数据。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,585评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,923评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,314评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,346评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,718评论 3 291
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,828评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,020评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,758评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,486评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,722评论 2 251
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,196评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,546评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,211评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,132评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,916评论 0 200
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,904评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,758评论 2 274

推荐阅读更多精彩内容