OpenGL ES渲染视频之数据流

2022-12-19  本文已影响0人  会上网的井底之蛙

相关概念

视频数据流

视频从视频文件到屏幕显示出来,Surface起到了非常重要的作用,在视频解码时,需要一个Surface作为已解码数据的载体,保存播放器解码后的数据,在显示时,需要另一个Surface,作为已渲染数据的载体,保存OpenGL渲染后的数据,最后通过EGL显示在屏幕上,数据流如下图所示:

  1. 解码器(MediaCodec或FFmpge等)将视频文件中的视频编码数据解码成图像流放到Surface中
  2. SurfaceTexture把Surface生成的图像流,转换为纹理
  3. OpenGL ES 渲染纹理生成图形数据,放到GLSurfaceView中的Surface中
  4. EGL从Surface中取出图形数据,显示到屏幕上

解码器与Surface绑定

MediaPlayer:

val surface:Surface = createSurface()
val mediaplayer = MediaPlayer()

//设置Surface,MediaPlayer解码后的数据将写入到此Surface中
mediaplayer.setSurface(surface)

MediaCodec:


val surface:Surface = createSurface()
//比如视频类型为video/avc
val type = "video/avc" 
val format:MediaFormat

val mediaCodec = MediaCodec.createDecoderByType(type)

//将Surface配置到MediaCodec对象中,MediaCodec解码后的数据将写入到此Surface中
mediaCodec.configure(format, surface , null, 0)

Surface与SurfaceTexture绑定

通过SurfaceTexture创建Surface对象即可。Surface也可以与SurfaceView绑定,拿SurfaceTexture与SurfaceView作比较,SurfaceView是将Surface的图像流直接显示出来,而SurfaceTexture是将图像流转换为OpenGL的外部纹理,转换的目的就是可以拿出这个外部纹理进行二次处理,比如利用OpenGL ES进行各种变换

val oesTextureId:Int = createTextureId() 

//构造一个绑定了OES纹理的SurfaceTexture
val surfaceTexture = SurfaceTexture(oesTextureId)

//通过SurfaceTexture对象来创建构造一个输出Surface,当解码结果写入到 Surface 的 BufferQueue 之后,再利用 //SurfaceTexture 将结果从 BufferQueue 渲染到OES纹理上
val surface = Surface(surfaceTexture)

说明:之所以使用OES纹理,是因为视频解码的格式是YUV的,而屏幕的画面是RGB的,OES纹理实现了YUV格式到RGB的自动转化,这样就不用在着色器的GLSL中写YUV转RGB的代码

SurfaceTexture与OpenGL ES绑定

OpenGL ES中的一个TextureId创建SurfaceTexture,所以纹理就是SurfaceTexture与OpenGL ES进行联系的纽带,视频数据由SurfaceTexture转换为外纹理后,绑定到OpenGL纹理对象,最后由OpenGL ES将纹理对象中的视频数据进行渲染变换生成新的视频数据

val oesTextureId:Int = createTextureId() 

//构造一个绑定了OES纹理的SurfaceTexture
val surfaceTexture = SurfaceTexture(oesTextureId)


fun createTextureId:Int {
    val textures = IntArray(1)
    //生成纹理
    GLES20.glGenTextures(1, textures, 0) 
    return textures[0]
}
class render: GLSurfaceView.Renderer {
    ...
    
    override fun onDrawFrame(gl: GL10?) {
       ...

       //updateTexImage更新接收到的数据并将其更新到OpenGL纹理对象中
       surfaceTexture.updateTexImage()
       ...  
       //激活指定纹理单元
       GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
       //绑定纹理对象到纹理目标
       GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, oesTextureId) 
       ...
    }
    
    ...
}

onDrawFrame函数中的逻辑也揭示了数据从SurfaceView到OpenGL ES的过程:

  1. surfaceTexture.updateTexImage

    将最新的图像流数据更新到GL纹理对象中(纹理对象由oesTextureId表示)

  2. GLES20.glActiveTexture

    激活指定纹理单元,后续的glBindTexture(GL_TEXTURE_EXTERNAL_OES, oesTextureId)作用于此所选的纹理单元

  3. GLES20.glBindTexture

    绑定纹理对象到上一步中激活的纹理单元的纹理目标,到这一步时OpenGL ES就可以从纹理单元中获取到纹理对象中的图片纹理。glBindTexture函数还能在多个纹理之间实现切换,这样在多个纹理对象绑定到同一纹理单元的同一个纹理目标时,也能很好的控制从多个不同纹理对象中取出图片纹理数据

  4. GLES20.glUniform1i

    将当前纹理单元绑定的纹理对象的数据传递到着色器,OpenGL ES会在着色器进行瑄染

    这一步在此不是必须的,因为作为数据的载体SurfaceTexture 绑定的是OES纹理,所以片元着色器(Fragment Shader)中需要使用OES 纹理,在片元着色器脚本的头部增加扩展纹理的声明:

    #extension GL_OES_EGL_image_external : require
    

    这样一来不需要调用glUniform1i,纹理对象的数据会自动传递到片元着色器

如何将OpenGL ES处理后的数据交给GLSurfaceView

  1. GLSurfaceView中初始化EGL环境,管理EGLSurface、EGLDisplay、EGLContext
  2. EGLSurface、EGLContext、EGLDisplay 三者会绑定一起
  3. EGLDisplay 为 OpenGL ES 的渲染目标,可以接收到 OpenGl ES 渲染出来的图形数据
  4. EGLDisplay让OpenGl ES把内容渲染到EGLSurface中
  5. EGLSurface是一块特殊的内存,实质就是Surface,能直接排版到Android的视图View上

理解了视频数据流的变换过程与对纹理对象的操作方法,就能利用OpenGL ES对视频数据进行加工,实现我们想要的效果,下面举了两个例子,加以说明

实现画中画

主要是实现两个不同视频绘制到同一视图上,可叠加显示

说明:

  1. 只需要一个GLSurfaceView,在GLSurfaceView中绘制两个视频画面
  2. 为了避免发生遮挡,而是要生成叠加的效果,需要启动OpenGL混合功能
  3. TextureID、SurfaceTexture与Surface三者一起作为一个组合,需要创建了两套这样的组合,每一套的渲染过程是按顺序进行,互相独立的
  4. SurfaceTexture是纹理对象与Surface的纽带,Surface的图形数据经SurfaceTexture转为纹理,交给OpenGL ES加工
  5. 每一次onDrawFrame,会按序绘制两个纹理对象
  6. 绘制时可通过矩阵变换,改变画面的大小与透明度等
  7. 该功能两个纹理对象能绑定到同一纹理单元的同一个目标(比如两个TextureID都绑定到纹理单元GLES20.GL_TEXTURE0 的 GLES11Ext.GL_TEXTURE_EXTERNAL_OES纹理目标上),因为在绘制流程时都会进行重新绑定,所以不会出现混乱

实现双屏同步播放

主要实现同一个视频同步播放到两个屏或者两个视图

说明:

  1. 需要两个GLSurfaceView,作为两个播放窗口,共享同一个播放源,即共享同一个SurfaceTexture
  2. 创建了3个纹理对象,SurfaceTexture对象绑定的TextureID与两个GLSurfaceView绑定的TextureID是无关的,所以Surface的图形数据无法直接给到OpenGL ES加工
  3. attachToGLContext函数将SurfaceTexture附加到调用线程上当前的OpenGL ES上下文,这样updateTexImage会将Surface的图形数据给到OpenGL ES加工
  4. 因为是双屏,所以每次绘制的最后,需要调用detachFromGLContext() ,从当前OpenGL ES上下文中分离SurfaceTexture,以便另一屏再进行绘制
  5. GLSurfaceView的绘制是在自己的子线程中,两个GLSurfaceView同时绘制,共享一个SurfaceTexture,需要做好多线程同步
上一篇 下一篇

猜你喜欢

热点阅读