Android OpenGL ES 学习 - MediaCodec + OpenGL 解析H264视频+滤镜
Posted 夏至的稻穗
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android OpenGL ES 学习 - MediaCodec + OpenGL 解析H264视频+滤镜相关的知识,希望对你有一定的参考价值。
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<Int>();
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 < 1 || height < 1)
throw IllegalArgumentException("Some argument is invalid");
Log.d(TAG, "configAndStart() called: $width,$height")
decoder = MediaCodec.createDecoderByType(DECODE_NAME)
val format = MediaFormat()
format.setString(MediaFormat.KEY_MIME, DECODE_NAME)
format.setInteger(MediaFormat.KEY_WIDTH, width)
format.setInteger(MediaFormat.KEY_HEIGHT, height)
decoder?.let
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 < 0)
listener.onLog("不符合H264文件规范: $formatLength")
return
//帧数组
val frame = ByteArray(FRAME_MAX_LEN)
//每次读取的数据
val readData = ByteArray(2 * 1024)
//把头部信息给到 frame
System.arraycopy(header, 0, frame, 0, header.size)
//开始肯定是 sps,所以,帧的起始位置为0,由于前面读取了头部,偏移量为4
var frameLen = 4
while (!isFinish)
val readLen = inputStream.read(readData)
if (readLen < 0)
//文件末尾
listener.onLog("文件末尾,退出")
isFinish = true
return
if (frameLen + readLen > FRAME_MAX_LEN)
//文件末尾
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 < 5)
return false
val d1 = data[index].toInt() == 0
val d2 = data[index + 1].toInt() == 0
val isNaluHeader = d1 && d2
if (isNaluHeader && data[index + 2].toInt() == 1 && isFrameHeadType(data[index + 3]))
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 ==以上是关于Android OpenGL ES 学习 - MediaCodec + OpenGL 解析H264视频+滤镜的主要内容,如果未能解决你的问题,请参考以下文章
Android OpenGL ES 学习 - MediaCodec + OpenGL 解析H264视频+滤镜