Android MediaCodec 简明教程(八):使用 MediaCodec 解码到纹理,使用 OpenGL ES 进行处理并显示在 GLSurfaceView 上

系列文章目录

  1. Android MediaCodec 简明教程(一):使用 MediaCodecList 查询 Codec 信息,并创建 MediaCodec 编解码器
  2. Android MediaCodec 简明教程(二):使用 MediaCodecInfo.CodecCapabilities 查询 Codec 支持的宽高,颜色空间等能力
  3. Android MediaCodec 简明教程(三):详解如何在同步与异步模式下,使用MediaCodec将视频解码到ByteBuffers,并在ImageView上展示
  4. Android MediaCodec 简明教程(四):使用 MediaCodec 将视频解码到 Surface,并使用 SurfaceView 播放视频
  5. Android MediaCodec 简明教程(五):使用 MediaCodec 编码 ByteBuffer 数据,并保存为 MP4 文件
  6. Android MediaCodec 简明教程(六):使用 EGL 和 OpenGL 绘制图像到 Surface 上,并通过 MediaCodec 编码 Surface 数据,并保存到 MP4 文件
  7. Android MediaCodec 简明教程(七):使用 MediaCodec 解码到 OES 纹理上

前言

在之前的教程中,我们已经学习了如何使用 MediaCodec 解码视频到 OES 纹理。在这篇文章中,我们将进一步探讨如何使用 OpenGL ES 将彩色图像转换为灰色图像,并在 GLSurfaceView 上显示。
本文所有代码你可以在 DecodeEditPlay.kt 中找到

数据流

在这里插入图片描述
上图描述了视频数据的生产-消费链路,其中生产者和消费者用不同颜色进行标识。

  1. MediaCodec 解码器将视频数据写入 Surface 的 Buffer 中;SurfaceTexture 作为消费者获取到 Buffer 后,将视频数据绘制到 OES 纹理上。
  2. 接着,在 GL 线程下,使用 OpenGL ES API 将 OES 纹理绘制到 Surface 上;SurfaceFinger 作为消费者从 GLSurfaceView 的 Buffer 中获取视频,并最终呈现在屏幕上。

Show me the code

GLSurfaceView

关于 GLSurfaceView 的使用本文就不再多说,网上有很多参考资料了,本人写过相关的博客有: LearnOpenGL - Android OpenGL ES 3.0 绘制三角形。简单来说:

  1. 你需要写一个 Render,它实现了 GLSurfaceView.Renderer 的三个接口:onSurfaceCreatedonSurfaceChangedonDrawFrame
  2. GLSurfaceView 会创建一个 GL 线程,并在该线程中去回调 GLSurfaceView.Renderer 三个接口
  3. onDrawFrame 方法中,你可以调用 OpenGL ES 的绘制接口来实现画面的更新

看下我们的 Render 的逻辑:

inner class MyGLSurfaceRender : GLSurfaceView.Renderer {
        override fun onSurfaceCreated(
            gl: GL10?,
            config: javax.microedition.khronos.egl.EGLConfig?
        ) {
            mTextureRenderer = TextureRenderer2()
            Matrix.setIdentityM(texMatrix, 0)

            Thread {
                decodeSync()
            }.start()
        }

        override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        }

        override fun onDrawFrame(gl: GL10?) {
            Log.d(TAG, "onDrawFrame")
            if(frameAvailable){
                mSurfaceTexture?.updateTexImage()
                mSurfaceTexture?.getTransformMatrix(texMatrix)
            }

            // draw oes texture to screen
            mTextureRenderer?.draw(width, height, texMatrix, getMvp())
        }
    }
  1. onSurfaceCreated方法:当Surface被创建时,这个方法会被调用。在这个方法中,它创建了一个TextureRenderer2对象,并设置了一个单位矩阵。然后,它在一个新的线程中调用了decodeSync方法。

    • TextureRenderer2 对象作用是将 OES 纹理绘制到 Surface 上。
    • 起了一个新的线程,是为了不阻塞当前的 GL 回调线程。
  2. onSurfaceChanged方法:当Surface的大小发生改变时,这个方法会被调用。在这个方法中,它没有做任何事情。

  3. onDrawFrame方法:这个方法在每一帧需要被绘制时被调用。首先,它检查是否有新的帧可用,如果有,它会更新SurfaceTexture的图像,并获取变换矩阵。然后,它调用TextureRenderer的draw方法,将OES纹理绘制到屏幕上。

decodeSync

接下来,看 decodeSync 方法逻辑:

private fun decodeSync() {
        mSurfaceTexture = SurfaceTexture(mTextureRenderer!!.texId)
        mSurfaceTexture!!.setOnFrameAvailableListener(this)
        mOutputSurface = Surface(mSurfaceTexture)

        // create and configure media extractor
        val mediaExtractor = MediaExtractor()
        resources.openRawResourceFd(R.raw.h264_720p).use {
            mediaExtractor.setDataSource(it)
        }
        val videoTrackIndex = 0
        mediaExtractor.selectTrack(videoTrackIndex)
        val videoFormat = mediaExtractor.getTrackFormat(videoTrackIndex)
        width = videoFormat.getInteger(MediaFormat.KEY_WIDTH)
        height = videoFormat.getInteger(MediaFormat.KEY_HEIGHT)

        // create and configure media codec
        val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
        val codecName = codecList.findDecoderForFormat(videoFormat)
        val codec = MediaCodec.createByCodecName(codecName)

        val maxInputSize = videoFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
        val inputBuffer = ByteBuffer.allocate(maxInputSize)
        val bufferInfo = MediaCodec.BufferInfo()
        val inputEnd = AtomicBoolean(false)
        val outputEnd = AtomicBoolean(false)

        // set codec callback in async mode
        codec.setCallback(object : MediaCodec.Callback() {
            override fun onInputBufferAvailable(codec: MediaCodec, inputBufferId: Int) {
                Log.d(TAG, "onInputBufferAvailable")
                val isExtractorReadEnd =
                    getInputBufferFromExtractor(mediaExtractor, inputBuffer, bufferInfo)
                if (isExtractorReadEnd) {
                    inputEnd.set(true)
                    codec.queueInputBuffer(
                        inputBufferId, 0, 0, 0,
                        MediaCodec.BUFFER_FLAG_END_OF_STREAM
                    )
                } else {
                    val codecInputBuffer = codec.getInputBuffer(inputBufferId)
                    codecInputBuffer!!.put(inputBuffer)
                    codec.queueInputBuffer(
                        inputBufferId,
                        0,
                        bufferInfo.size,
                        bufferInfo.presentationTimeUs,
                        bufferInfo.flags
                    )
                    mediaExtractor.advance()
                }
            }

            override fun onOutputBufferAvailable(
                codec: MediaCodec,
                outputBufferId: Int,
                info: MediaCodec.BufferInfo
            ) {
                if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
                    outputEnd.set(true)
                }
                if (info.size > 0) {
                    Log.i(TAG, "onOutputBufferAvailable")
                    codec.releaseOutputBuffer(outputBufferId, true)

                    // sleep for 30ms to simulate 30fps
                    Thread.sleep(30)
                }
            }

            override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
                e.printStackTrace()
            }

            override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
                Log.e(TAG, "onOutputFormatChanged")
            }
        })

        // configure with surface
        codec.configure(videoFormat, mOutputSurface, null, 0)

        // start decoding
        codec.start()

        // wait for processing to complete
        while (!outputEnd.get()) {
            Thread.sleep(10)
        }

        mediaExtractor.release()
        codec.stop()
        codec.release()

        mOutputSurface!!.release()
        mOutputSurface = null
        mSurfaceTexture!!.release()
        mSurfaceTexture = null
  1. decodeSync方法首先创建了一个SurfaceTexture对象,并将其与TextureRenderer的纹理ID关联。然后,它设置了一个帧可用监听器,并使用SurfaceTexture创建了一个Surface对象。

  2. 接下来,它创建并配置了一个MediaExtractor对象,用于从资源文件中提取视频数据。它选择了视频轨道,并获取了视频格式,包括视频的宽度和高度。

  3. 然后,它创建并配置了一个MediaCodec对象,用于解码视频数据。它获取了视频格式的最大输入大小,并分配了一个ByteBuffer用于存储输入数据。它还创建了一个BufferInfo对象,用于存储关于输入数据的信息。然后,它设置了一个回调,用于处理输入缓冲区可用、输出缓冲区可用、错误和输出格式改变等事件。

  4. 在回调的onInputBufferAvailable方法中,它从MediaExtractor中获取输入数据,并将其放入输入缓冲区。然后,它将输入缓冲区的数据排入MediaCodec的输入队列。

  5. 在回调的onOutputBufferAvailable方法中,它检查输出数据是否已经结束,如果结束,它将设置outputEnd标志。如果输出数据的大小大于0,它将释放输出缓冲区,并使其在Surface上可见。然后,它将线程休眠30毫秒,以模拟30帧每秒的帧率。

  6. 在所有的输出数据都已经处理完毕后,它将释放MediaExtractor和MediaCodec对象,并释放Surface和SurfaceTexture对象。

线程模型

梳理下上述代码的线程模型,其中重要的线程有:

  1. GLSurfaceView 创建的 GL 线程,它负责调用 Render 的三个回调方法,其中 onDrawFrame 负责绘制图像
  2. MediaCodec 异步的回调线程,负责调用 onOutputBufferAvailable 和通知 onFrameAvailable 回调方法

理论上,顺利解码一帧数据后,也就是 onFrameAvailable 被调用后,GL 线程 onDrawFrame 方法中调用 updateTexImage 来更新 OES 中的数据,然后使用 TextureRenderer2.draw 方法将 OES 纹理绘制到 Surface 上。实际上,在现有的代码框架下按这个逻辑来实现的,你会发现画面会一卡一卡的。在我们的实现代码中,取了个巧,onFrameAvailable 被调用后 frameAvailable 被设置为 true,而接下来 frameAvailable 将一直都是 true,也就是说 onDrawFrame 方法中,每次都会更新 OES 纹理信息。这样一来画面会流畅很多。

参考

  • 15
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的Android JNI代码示例,演示如何使用C++类MediaCodec和MediaFormat来解码视频文件并获取输出缓冲区: ```c++ #include <jni.h> #include <android/log.h> #include <android/native_window_jni.h> #include <media/NdkMediaCodec.h> #define LOG_TAG "MediaCodecExample" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) extern "C" { JNIEXPORT void JNICALL Java_com_example_MediaCodecUtils_decode(JNIEnv *env, jobject instance, jstring filePath_, jobject surface) { const char *filePath = env->GetStringUTFChars(filePath_, NULL); FILE *fp = fopen(filePath, "rb"); if (!fp) { LOGD("Failed to open file: %s", filePath); return; } // 创建解码器 AMediaCodec *codec = AMediaCodec_createDecoderByType("video/avc"); AMediaFormat *format = AMediaFormat_new(); AMediaFormat_setString(format, AMEDIAFORMAT_KEY_MIME, "video/avc"); AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_WIDTH, 1920); AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_HEIGHT, 1080); AMediaFormat_setBuffer(format, "csd-0", (void *) csd_data, csd_size); AMediaCodec_configure(codec, format, ANativeWindow_fromSurface(env, surface), NULL /* crypto */, 0 /* flags */); AMediaCodec_start(codec); // 循环解码 bool inputDone = false; bool outputDone = false; while (!outputDone) { if (!inputDone) { // 获取可用输入缓冲区的索引 ssize_t inputIndex = AMediaCodec_dequeueInputBuffer(codec, 2000); if (inputIndex >= 0) { AMediaCodecBufferInfo inputBufferInfo; auto inputBuffer = AMediaCodec_getInputBuffer(codec, inputIndex); size_t bytesRead = fread(inputBuffer->data, 1, inputBuffer->capacity, fp); if (bytesRead > 0) { // 提交输入缓冲区 AMediaCodec_queueInputBuffer(codec, inputIndex, 0, bytesRead, 0, inputDone ? AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM : 0); } else { inputDone = true; AMediaCodec_queueInputBuffer(codec, inputIndex, 0, 0, 0, AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM); } } } // 获取可用输出缓冲区的索引 AMediaCodecBufferInfo outputBufferInfo; ssize_t outputIndex = AMediaCodec_dequeueOutputBuffer(codec, &outputBufferInfo, 0); if (outputIndex == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) { // 更新输出格式 format = AMediaCodec_getOutputFormat(codec); LOGD("Output format changed: %s", AMediaFormat_toString(format)); } else if (outputIndex == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED) { // 忽略此消息 LOGD("Output buffers changed"); } else if (outputIndex >= 0) { auto outputBuffer = AMediaCodec_getOutputBuffer(codec, outputIndex); // 处理输出缓冲区 LOGD("Output buffer %zd, size %d, flags %d, pts %lld, dts %lld", outputIndex, outputBufferInfo.size, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, outputBufferInfo.decodingTimeUs); // 获取输出缓冲区的数据 uint8_t *data = outputBuffer->data; int dataSize = outputBufferInfo.size; // 处理数据... // 处理完输出缓冲区后,释放缓冲区 AMediaCodec_releaseOutputBuffer(codec, outputIndex, true /* render */); outputDone = (outputBufferInfo.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) != 0; } } // 释放资源 AMediaCodec_stop(codec); AMediaCodec_delete(codec); fclose(fp); env->ReleaseStringUTFChars(filePath_, filePath); } } ``` 这个例子中,我们使用AMediaCodec_createDecoderByType函数创建一个H.264视频解码器,然后使用AMediaFormat来指定视频的宽度、高度、MIME类型和CSD数据。在configure方法中,我们将解码器与一个Surface关联起来,以便输出图像可以在屏幕上渲染。在循环中,我们首先获取可用的输入缓冲区索引,并从输入文件中读取数据填充缓冲区。然后,我们提交输入缓冲区,并等待解码器输出缓冲区。当我们获取可用的输出缓冲区索引时,我们可以从输出缓冲区中获取数据并进行处理。最后,我们释放输出缓冲区并检查是否到达了输入文件的结尾。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值