OpenGL ES渲染视频之数据流

相关概念

  • 纹理

    • 纹理(Texture)

      纹理是一种图形数据,在OpenGL中可以简单理解成是一张图片

    • 纹理单元

      纹理的操作容器,值依次为GLES20.GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2等。纹理单元数量在GPU上是确定的,一般OpenGL ES20至少保证有32个纹理单元,意味着能同时操作32个纹理

    • 纹理目标

      一个纹理单元包含多个不同类型的纹理目标,例如GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP等

    • 纹理ID(TextureID)

      纹理对象的引用

      OpenGL要操作一个纹理,需要把纹理对象绑定到一个纹理单元上

    • 纹理单元和纹理对象的关系

      因为片元着色器(fragment shader)通过纹理单元得到纹理对象,所以需要将纹理对象绑定到纹理单元上。每个纹理单元可以同时绑定多个纹理对象,绑定一个纹理对象到一个纹理单元的时候,还需要指定纹理目标,比如可以将纹理对象1绑定到纹理单元TEXTURE0的1D目标上,同时可以将纹理对象2绑定到该纹理单元的2D目标上,不仅如此,两个纹理对象还能绑定到同一纹理单元的同一个目标上

  • Surface

    Surface就是内存中的一段绘图缓冲区,即一段图片buffer。Surface通常由图片buffers的消费者创建,如SurfaceTexture、MediaRecorder等创建,被给到生产者,如OpenGL、MediaPlayer、MediaCodec等,由生产者往其中写入数据

  • SurfaceTexture

    SurfaceTexture可以把Surface生成的图像流,转换为纹理。SurfaceTexture从图像流(来自Camera预览,视频解码,GL绘制场景等)中获得帧数据,当调用updateTexImage()时,根据内容流中最近的图像更新SurfaceTexture对应的GL纹理对象,随后可以操作这些GL纹理对象。

    SurfaceTexture在创建的时候需要一个Texture对象,而Surface又可以由SurfaceTexture创建,所以SurfaceTexture将Surface和Texture对象绑定在了一起,从而实现将Surface的图像流更新到对应的Texture对象上的目的

    //生成纹理
    val textureId:Int = createTextureId() //函数createTextureId中会利用GLES20.glGenTextures函数生成纹理
    
    //创建SurfaceTexture对象,SurfaceTexture作为桥梁,将Texture与Surface联系在了一起
    val surfaceTexture = SurfaceTexture(textureId)
    
    //创建Surface对象
    val surface = Surface(surfaceTexture)
    

    本质上SurfaceTexture实现了共享纹理,即实现了CPU与GPU对同一资源的访问,无需拷贝的数据共享,可降低功耗与提高性能

  • GLSurfaceView

    GLSurfaceView作为SurfaceView的补充,在SurfaceView的基础上加入了EGL,可方便地使用 OpenGL ES API 进行渲染绘制

  • EGL

    全称:Embedded Graphic Interface(嵌入式图形接口),是 OpenGL ES 渲染 API 和本地窗口系统(native platform window system)之间的一个中间接口层,它主要由系统制造商实现。通过 OpenGL ES 操作GPU将图形数据计算结果保存在Surface的buffer 中,再利用EGL取出buffer中的图形信息显示到手机屏幕上

  • GLSL

    全称:OpenGL Shading Language(OpenGL ES着色语言),用这个语言可以编写小程序运行在GPU上。GPU 擅长处理大规模数据,使用同一个算法进行计算,而这个算法,就是使用 GLSL 写成 Shader,供 GPU 运算使用

  • OpenGL ES的作用

    OpenGL ES不是用来显示视频的,它主要是用来处理图形数据的。

    具体来说,它的主要作用是提供了一套在上层应用程序中操作GPU的接口,利用GPU的强大的图形数据处理功能,对图像数据进行处理,就视频数据来说,本来SurfaceView就能直接播放视频,但是利用OpenGL ES,能对视频图像数据进行二次加工,从而实现很多特效,比如灵魂出窍、水印、美白、滤镜、画中画等等,处理过的数据最终还是要利用EGL显示出来,比如利用GLSurfaceView显示到屏幕上

    实际上我们设计的界面能显示到手机屏幕上,其底层也是用到了Surface与OpenGL ES

视频数据流

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

推荐阅读更多精彩内容