Android OpenGL ES 学习(十二) 您所在的位置:网站首页 安卓视频取帧 Android OpenGL ES 学习(十二)

Android OpenGL ES 学习(十二)

2023-01-06 07:42| 来源: 网络整理| 查看: 265

OpenGL 学习教程 Android OpenGL ES 学习(一) – 基本概念 Android OpenGL ES 学习(二) – 图形渲染管线和GLSL Android OpenGL ES 学习(三) – 绘制平面图形 Android OpenGL ES 学习(四) – 正交投影 Android OpenGL ES 学习(五) – 渐变色 Android OpenGL ES 学习(六) – 使用 VBO、VAO 和 EBO/IBO 优化程序 Android OpenGL ES 学习(七) – 纹理 Android OpenGL ES 学习(八) –矩阵变换 Android OpenGL ES 学习(九) – 坐标系统和。实现3D效果 Android OpenGL ES 学习(十) – GLSurfaceView 源码解析GL线程以及自定义 EGL Android OpenGL ES 学习(十一) –渲染YUV视频以及视频抖音特效

代码工程地址: https://github.com/LillteZheng/OpenGLDemo.git 更多音视频,参考:Android 音视频入门/进阶教程

这是OpenGL 最后一篇教程了,待我把C/Jni/Ndk 相关的知识,再深入一遍,再来学习光照等知识。

前面我们学习了OpenGL是如何渲染 YUV 视频的Android OpenGL ES 学习(十一) –渲染YUV视频以及视频抖音特效 ,这一章,我们让OpenGL 与 MediaCodec 结合,实现解析 H264 文件,并实现抖音效果。效果如下: 在这里插入图片描述

MediaCodec 为Android 的硬编,在一些快速解码设备,我们都是使用MediaCodec,如果你对 MediaCodec 如何解码不熟悉,可以先阅读 Android 音视频编解码(一) – MediaCodec 初探 Android 音视频编解码(二) – MediaCodec 解码(同步和异步)

实际工作中,我们也会使用 MediaCodec 把其他设备传输过来的码流,通过与 OpenGL 结合,实现解码和滤镜效果,比如投屏,投屏的基础上,加一些滤镜和特效。

OpenGL 与 MediaCodec 结合,需要 OpenGL 提供一个 Surface ,让MediaCodec 把解码出来的 YUV 渲染出来,而这个 Surface 就是 SurfaceTexure

一. 外部纹理 SurfaceTexture

SurfaceTexture 是 Surface 与 OpenGL ES 的结合 ,与传统的纹理(GL_TEXTURE_2D)不同,它有以下特点:

SurfaceTexture 可以直接 BufferQueue 拿到数据并渲染,在拿到 BufferQueue实例时,会将使用者标志设置成 GRALLOC_USAGE_HW_TEXTURE ,以确保 SurfaceTexture 可以识别缓冲区的数据。与 GL_TEXTURE_2D 不同,需要使用 samplerExternalOES 去识别外部纹理。不能执行与 GL_TEXTURE_2D 相同的操作。 1.1 时间戳和转换

SurfaceTexture实例包括检索时间戳的getTimeStamp()方法和检索变换矩阵的getTransformMatrix()方法。调用updateTexImage()设置时间戳和转换矩阵

转换:比如某些情况下,接收端的数据是颠倒的,使用Matrix ,我们可以很容易把画面反转回来。时间戳:这个在相机会用的多,比如相机的每一帧,都需要带一个从捕获时拿到的演示时间戳,通过设置这个属性,我们能保证一致的时间戳。 1.2 数据回调

当你创建了SurfaceTexture ,也会创建一个待消耗的BufferQueue,当生产方(比如 MediaCodec )有新的缓冲数据加入队列,会回调 onFrameAvailable() 方法,表示已经消化了一帧。 当你调用了 updateTexImage() ,会释放当前的缓冲区,并从BufferQueue 拿到最新的缓冲区,这时会调用 EGL 的一些操作,使 GLES 可以将缓冲区作为外部纹理使用,即告知 OpenGL ,当前缓冲区可用,可进行一些操作。

二. 渲染视频

从上面的了解,我们可以得出MediaCodec , SurfaceTexture 和 OpenGL 结合的关系: 在这里插入图片描述 流程如下:

创建 SurfaceTexture,并把OpenGL的纹理 id 给到 SurfaceTexture创建 MediaCodec,并拿到 SurfaceTexture 的 Surface当第一次回调 onDrawFrame 时,会调用 updateTexture,刷新 BufferQueue,待 Mediacodec 生产数据时,更新 BufferQueue,会重触发 onDrawFrame ,循环至视频解码结束。 2.1 OpenGL 外部纹理

OpenGL 的外部纹理,使用的是GLES11Ext中的 samplerExternalOES:

uniform samplerExternalOES ourTexture;

因此,我们的片段着色器可以修改成:

/** * 片段着色器 */ private var FRAGMENT_SHADER = """#version 300 es precision mediump float; out vec4 FragColor; in vec2 vTexture; uniform samplerExternalOES ourTexture; void main() { FragColor = texture(ourTexture,vTexture); } """

纹理的绑定,需要注意的是使用 GLES11Ext :

GLES30.glGenTextures(1, textures, 0) GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textures[0]) //纹理环绕 GLES30.glTexParameteri( GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_REPEAT ) GLES30.glTexParameteri( GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_REPEAT ) //纹理过滤 GLES30.glTexParameteri( GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR ) GLES30.glTexParameteri( GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR ) //解绑纹理对象 GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0)

然后创建 SurfaceTexture:

surfaceTexture = SurfaceTexture(textures[0]).apply { setDefaultBufferSize(width, height) setOnFrameAvailableListener { } } 2.2 与MediaCodec 绑定

Mediacodec 解码H264比较简单,配置解码的属性,使用异步解码即可,不熟悉Mediacodec可以参考: Android 音视频编解码(一) – MediaCodec 初探 Android 音视频编解码(二) – MediaCodec 解码(同步和异步)

解码代码如下:

/** * @author by zhengshaorui 2022/12/26 * describe:视频解码 */ class VideoDncoder { companion object { internal val instance: VideoDncoder by lazy { VideoDncoder() } private const val MSG_INIT = 1; private const val MSG_QUERY = 2; private const val DECODE_NAME = "video/avc" private const val TAG = "VideoEncoder" } private var handleThread: HandlerThread? = null private var handler: Handler? = null private var surface: Surface? = null private var decoder: MediaCodec? = null private val indexQueue = LinkedBlockingDeque(); private val handlerCallback = Handler.Callback { msg -> when (msg.what) { MSG_INIT -> { configAndStart() } MSG_QUERY -> { // handler?.sendEmptyMessageDelayed(MSG_QUERY, 10) } } false } private var listener: IDecoderListener? = null fun start(surface: Surface, iDecoderListener: IDecoderListener) { listener = iDecoderListener this.surface = surface if (handleThread == null) { handleThread = HandlerThread("VideoEncoder").apply { start() handler = Handler(this.looper, handlerCallback) } } handler?.let { it.removeMessages(MSG_INIT) it.sendEmptyMessage(MSG_INIT) } } /** * 喂数据 */ fun feedData(buffer: ByteArray, offset: Int, length: Int) { val index = indexQueue.take() if (index != -1) { decoder?.let { it.getInputBuffer(index)?.apply { clear() val time = System.nanoTime() / 1000000 put(buffer, offset, length) it.queueInputBuffer(index, 0, length, time, 0) } } } } private fun configAndStart() { var width = getRealWidth(MainApplication.context) var height = getRealHeight(MainApplication.context) if (null == surface || !surface!!.isValid || width it.reset() it.configure(format, surface, null, 0) it.setCallback(decodeCallback) it.start() Log.d(TAG, "解码器启动成功") listener?.onReady() handler?.sendEmptyMessage(MSG_QUERY) } } public interface IDecoderListener { fun onReady() } private val decodeCallback = object : MediaCodec.Callback() { override fun onInputBufferAvailable(codec: MediaCodec, index: Int) { indexQueue.add(index) } override fun onOutputBufferAvailable( codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo ) { decoder?.releaseOutputBuffer(index, true) } override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) { Log.e(TAG, "onError() called with: codec = $codec, e = $e") } override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) { var width = format.getInteger(MediaFormat.KEY_WIDTH) if (format.containsKey("crop-left") && format.containsKey("crop-right")) { width = format.getInteger("crop-right") + 1 - format.getInteger("crop-left") } var height = format.getInteger(MediaFormat.KEY_HEIGHT) if (format.containsKey("crop-top") && format.containsKey("crop-bottom")) { height = format.getInteger("crop-bottom") + 1 - format.getInteger("crop-top") } Log.d(TAG, "视频解码后的宽高:$width,$height") } } fun release() { handleThread?.quitSafely() handleThread = null handler = null surface?.release() try { decoder?.let { it.stop() it.release() } } catch (e: Exception) { } } } 2.3 解析H264文件

接下来就是解析H264文件了,需要注意的是,喂给解码器的数据,要以一帧的结尾,不然会出现数据错乱,花屏的问题,如果你对H264不熟悉,可参考 Android 音视频编解码(三) – 视频编码和H264格式原理讲解

因此,我们读取H264每一帧的数据,然后一帧一帧喂给解码器,H264解析的简单代码如下:

/** * @author by zhengshaorui 2022/12/28 * describe:H264 帧解析类 */ class H264ParseThread(val inputStream: InputStream, val listener: IFrameListener) : Thread() { companion object { private const val TAG = "H264Parse" //一般H264帧大小不超过200k,如果解码失败可以尝试增大这个值 private const val FRAME_MAX_LEN = 300 * 1024 private const val P_FRAME = 0x01 private const val I_FRAME = 0x05 private const val SPS = 0x07 private const val PPS = 0x08 } private var isFinish = false public interface IFrameListener { fun onLog(msg: String) fun onFrame(byteArray: ByteArray, offset: Int, count: Int) } override fun run() { super.run() try { isFinish = false val header = ByteArray(4) val formatLength = getHeaderFormatLength(header, inputStream) if (formatLength val readLen = inputStream.read(readData) if (readLen //文件末尾 listener.onLog("文件末尾,大于预留数组,退出") isFinish = true return } //先把数据拷贝到帧数组 System.arraycopy(readData, 0, frame, frameLen, readLen) //修改当前帧的大小 frameLen += readLen //寻找第一帧 var firstHeadIndex = findHeader(frame, 0, frameLen) while (firstHeadIndex >= 0) { //找第二帧,从第一帧之后的间隔开始找 val secondFrameIndex = findHeader(frame, firstHeadIndex + 100, frameLen) if (secondFrameIndex > 0) { //找到第二帧 listener.onFrame(frame, firstHeadIndex, secondFrameIndex - firstHeadIndex) //把第二帧的数组数据,拷贝到前面,方便继续寻找下一帧 val temp = frame.copyOfRange(secondFrameIndex, frameLen) System.arraycopy(temp, 0, frame, 0, temp.size) //帧下表指向第二帧的数据 frameLen = temp.size //继续寻找下一帧 firstHeadIndex = findHeader(frame, 0, frameLen) } else { //没有找到,继续循环去找 firstHeadIndex = -1 } } } } catch (e: Exception) { listener.onLog("read file fail: $e") } } fun release() { isFinish = true } private fun findHeader(data: ByteArray, offset: Int, count: Int): Int { for (i in offset until count) { if (isFrameHeader(data, i)) { return i } } return -1 } private fun isFrameHeader(data: ByteArray, index: Int): Boolean { if (data.size return true }else if (isNaluHeader && data[index + 2].toInt() == 0 && data[index + 3].toInt() == 1 && isFrameHeadType(data[index + 4])){ return true } return false } /** * 解析的时候,找到I和P去解析即可 * 为啥使用and这个会导致播放卡顿?有大佬可以解释一下吗 */ private fun isSpecialFrame(byte: Byte): Boolean { val type = byte.toInt() and 0x11 return type == P_FRAME || type == I_FRAME || type == SPS || type == PPS } /** * 65 -- I帧/IDR帧 * 41/61 -- p帧 * 67 -- sps * 68 -- pps * */ fun isFrameHeadType(head: Byte): Boolean { // val type = byte.toInt() and 0x11 return head == 0x65.toByte() || head == 0x61.toByte() || head == 0x41.toByte() || head == 0x67.toByte() || head == 0x68.toByte() || head == 0x06.toByte() } private fun getHeaderFormatLength(header: ByteArray, inputStream: InputStream): Int { //先读取头部4个字节,判断h264 是哪种格式 if (header.size 3 } else if (h1 == 0 && h2 == 0 && h3 == 0 && h4 == 1) { 4 } else { //不符合H264文件规范 -1 } } } 2.3 渲染

综上,拿到H264数据后,直接丢给解码器:

decoder = VideoDncoder().apply { surfaceTexture = SurfaceTexture(textures[0]).apply { setDefaultBufferSize(width, height) setOnFrameAvailableListener { } } start(Surface(surfaceTexture), object : VideoDncoder.IDecoderListener { override fun onReady() { readFile() } }) } /** * 读取文件 */ private fun readFile() { val stream = context.resources.assets.open("video.h264") h264ParseThread = H264ParseThread(stream, object : H264ParseThread.IFrameListener { override fun onLog(msg: String) { Log.d(TAG, "H264ParseThread msg: $msg") } override fun onFrame(byteArray: ByteArray, offset: Int, count: Int) { decoder?.feedData(byteArray, offset, count) Thread.sleep(55) } }) h264ParseThread?.start() }

onDrawFrame 那里,更新缓冲区,updateTexImage:

override fun onDrawFrame(gl: GL10?) { //步骤1:使用glClearColor设置的颜色,刷新Surface GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT) surfaceTexture?.updateTexImage() GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textures[0]) GLES30.glBindVertexArray(vao[0]) GLES30.glDrawElements(GLES30.GL_TRIANGLE_STRIP, 6, GLES30.GL_UNSIGNED_INT, 0) }

记得释放:

private fun release() { h264ParseThread?.release() h264ParseThread = null decoder?.release() decoder = null surfaceTexture?.release() surfaceTexture = null }

这样,我们就完成 Mediacodec 和 OpenGL 的结合,实现了H264的解析。

四. 滤镜

从Android OpenGL ES 学习(十一) –渲染YUV视频以及视频抖音特效 知道,滤镜是对 rgb 的基础上,添加一些效果。 所以,在片段着色器拿到 rgb 的数据后,我们也能实现一些滤镜效果,如灰色:

#version 300 es precision mediump float; out vec4 FragColor; in vec2 vTexture; uniform samplerExternalOES ourTexture; void main() { vec4 temColor = texture(ourTexture,vTexture); float gray = temColor.r * 0.2126 + temColor.g * 0.7152 + temColor.b * 0.0722; FragColor = vec4(gray,gray,gray,1.0); }

其他效果,参考工程。

至此,OpenGL 教程,就暂时告一段了。

参考: https://source.android.google.cn/docs/core/graphics/arch-st



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有