Android MediaCodec 简明教程(四):使用 MediaCodec 将视频解码到 Surface,并使用 SurfaceView 播放视频

系列文章目录

  1. Android MediaCodec 简明教程(一):使用 MediaCodecList 查询 Codec 信息,并创建 MediaCodec 编解码器
  2. Android MediaCodec 简明教程(二):使用 MediaCodecInfo.CodecCapabilities 查询 Codec 支持的宽高,颜色空间等能力
  3. Android MediaCodec 简明教程(三):详解如何在同步与异步模式下,使用MediaCodec将视频解码到ByteBuffers,并在ImageView上展示


前言

在上一个教程 Android MediaCodec 简明教程(三) 中,我们学会了使用 MediaCodec 解码到 ByteBuffers 上,包括同步模式和异步模式。本章将讨论 MediaCodec 解码到 Surface 的相关知识点。Google 推荐使用 Surface 进行编解码操作,这样效率更高。

一、Surface 是什么?

Android Surface 是一个复杂的概念,它涉及到 Android 图形系统,网络已经有很多相关的博文,例如:

Surface 是 Android 图形架构的一部分,它与 SurfaceFlinger、SurfaceView、SurfaceTexture、SurfaceHolder 等组件一起,构成了 Android 的图形系统。
Surface 的主要作用是存储图形数据。当你在一个 Surface 上绘制图像时,这些图像会被存储在 Surface 的缓冲区中。然后,这个 Surface 可以被提交给 SurfaceFlinger,SurfaceFlinger 会将这个 Surface 的内容合成到屏幕上。
Surface 通常与其他组件一起使用。例如,你可以使用 Canvas 在一个 Surface 上绘制2D图像,或者使用 OpenGL ES 在一个 Surface 上绘制3D图像。你也可以使用 MediaCodec 或者 Camera 将视频帧输出到一个 Surface 上。
Surface 还有一些其他的特性。例如,它可以被多个进程共享,这使得跨进程的图形数据传输成为可能。它还支持 vsync 信号,这可以帮助你实现平滑的动画效果。

这部分对于我来说过于复杂了,并且这也不是目前要关心的内容,我们抓一个重点即可:Surface 中有一个 BufferQueue,里头存放着图像数据,生产者将数据送入 Queue 中,而消费者从 Queue 获取数据。在 Android 中,Surface 可以被看作是一个画布,你可以在上面绘制图像,然后这些图像会被显示到屏幕上。这个画布并不是一个实体,而是一个虚拟的概念,它代表了一个可以被绘制的区域。

你可以把 Surface 想象成一个电子屏幕上的窗户。你可以在这个窗户上画画,然后这些画就会显示在电子屏幕上。这个窗户就像是你和电子屏幕之间的一个桥梁,你通过这个窗户,将你的画送到电子屏幕上。在这个比喻中,你的画就是图像数据,电子屏幕就是 Android 设备的显示屏,而窗户就是 Surface。你将图像数据(即你的画)送入 Surface(即窗户),然后这些数据就会被渲染到显示屏(即电子屏幕)上。

Android Surface的理解和应用 中对 Surface 中 生产者 - 消费者 架构做了较为详细的说明,不再赘述。

二、MediaCodec 解码到 Surface

像上一个教程一样,我们将使用 MediaExtractor 和 MediaCodec 解码视频,不同的是,解码后的视频帧画面将使用 SurfaceView 显示。

2.1 Surface 从哪来?

获取 Surface 最简单的方式是:使用 SurfaceView。例如:

val surfaceView = findViewById<SurfaceView>(R.id.surface_view)
val surface = surfaceView.holder.surface

2. 2 MediaCodec 与 Surface 共享内存,实现零拷贝

MediaCodec 使用 Surface 进行解码时,效率更高的原因是因为 Android 使用了共享内存技术。具体来说,MediaCodec 解码后的数据会直接传递给 Surface 的 BufferQueue,而不是通过拷贝的方式传递给 Surface。这样就避免了从 MediaCodec 到 Surface 之间的数据拷贝,提高了解码效率。

为了实现这种共享内存的方式,我们在调用 configure 方法时需要传入一个 Surface 对象,这样才能让 MediaCodec 和 Surface 共享同一个 BufferQueue。这种优化方式可以有效地减少数据拷贝的次数,提高解码效率,从而更好地满足视频播放等应用的需求。

codec.configure(videoFormat, surface, null, 0)

2.3 数据流动

在解码的过程涉及到三个对象: MediaExtractor、MediaCodec 和 Surface,数据就像流水线一样从一个地方流向另一个地方,并最终被消费。以 SurfaceView 为例,最终的消费者是 SurfaceFlinger,流水线如下图所示:
在这里插入图片描述

2.4 ReleaseOutputBuffer,控制水流速度

MediaCodec 的 releaseOutputBuffer 方法就像一个水龙头,你可以通过它来控制解码的速度。这个方法有两个版本:

  1. releaseOutputBuffer(int index, boolean render):这个版本的方法中,如果 render 参数为 true,那么解码后的数据(Buffer)会立即被送到 Surface 进行渲染(播放),播放完后,这个 Buffer 就会被标记为可用,返回给 MediaCodec。如果 render 参数为 false,那么这个 Buffer 不会被送到 Surface,而是直接被释放,然后被标记为可用,返回给 MediaCodec。

  2. releaseOutputBuffer(int index, long renderTimestampNs):这个版本的方法中,你可以传入一个时间戳 renderTimestampNs。如果这个时间戳小于当前的系统时间,那么 Buffer 的内容会立即被渲染。如果这个时间戳大于当前的系统时间,那么系统会等待,直到系统时间达到这个时间戳,然后再进行渲染。这个特性非常有用,特别是当你需要精确控制音频或视频的播放时间,或者需要同步多个音频或视频流的播放时。

总的来说,releaseOutputBuffer 方法就像一个控制解码速度的调节器,你可以通过它来精确控制音视频的播放。

2.5 Show me the code

所有代码你可以在 DecodeUsingSurfaceActivity 中找到,下面代码中,给出了同步和异步两种实现。


    private fun decodeToSurface(surface: Surface){
        // 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)

        // create and configure media codec
        val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
        val codecName = codecList.findDecoderForFormat(videoFormat)
        val codec = MediaCodec.createByCodecName(codecName)
        // configure with surface
        codec.configure(videoFormat, surface, null, 0)

        // start decoding
        val maxInputSize = videoFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
        val inputBuffer = ByteBuffer.allocate(maxInputSize)
        val bufferInfo = MediaCodec.BufferInfo()
        val timeoutUs = 10000L // 10ms
        var inputEnd = false
        var outputEnd = false

        codec.start()
        while (!outputEnd && !stopDecoding) {
            val isExtractorReadEnd =
                getInputBufferFromExtractor(mediaExtractor, inputBuffer, bufferInfo)
            if (isExtractorReadEnd) {
                inputEnd = true
            }

            // get codec input buffer and fill it with data from extractor
            // timeoutUs is -1L means wait forever
            val inputBufferId = codec.dequeueInputBuffer(-1L)
            if (inputBufferId >= 0) {
                if (inputEnd) {
                    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,
                        0
                    )
                }
            }

            // get output buffer from codec and render it to image view
            // NOTE! dequeueOutputBuffer with -1L is will stuck here,  so wait 10ms here
            val outputBufferId = codec.dequeueOutputBuffer(bufferInfo, timeoutUs)
            if (outputBufferId >= 0) {
                if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
                    outputEnd = true
                }
                if (bufferInfo.size > 0) {
                    val pts = bufferInfo.presentationTimeUs * 1000L + startTime
                    codec.releaseOutputBuffer(outputBufferId, pts)
                }
            }

            mediaExtractor.advance()
        }


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

    private fun decodeToSurfaceAsync(surface: Surface) {
        // 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)

        // 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) {
                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){
                    // render the decoded frame
                    val pts = info.presentationTimeUs * 1000L + startTime
                    codec.releaseOutputBuffer(outputBufferId, pts)
                }
            }

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

            override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
                // do nothing
            }
        })

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

        // start decoding
        codec.start()

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

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

参考

  • 16
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个使用 MediaCodec 进行视频编码的示例代码,你可以参考一下: ```java import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.media.MediaMuxer; import android.os.Environment; import android.util.Log; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; public class VideoEncoder { private static final String TAG = "VideoEncoder"; // 编码器 private MediaCodec mEncoder; // 复用器 private MediaMuxer mMuxer; // 视频轨道 private int mTrackIndex; // 是否已经开始编码 private boolean mIsStarted; // 视频宽度 private int mWidth; // 视频高度 private int mHeight; // 视频帧率 private int mFrameRate; // 视频码率 private int mBitRate; // 编码器输出的 buffer 信息 private MediaCodec.BufferInfo mBufferInfo; // 用于存储编码后的数据 private ByteBuffer mOutputBuffer; public VideoEncoder(int width, int height, int frameRate, int bitRate) { mWidth = width; mHeight = height; mFrameRate = frameRate; mBitRate = bitRate; } /** * 开始编码 */ public void start() { try { // 初始化编码器 mEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate); format.setInteger(MediaFormat.KEY_FRAME_RATE, mFrameRate); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); // 创建一个文件用于存储编码后的数据 String outputPath = new File(Environment.getExternalStorageDirectory(), "output.mp4").getAbsolutePath(); mMuxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); // 开始编码 mEncoder.start(); mIsStarted = true; } catch (IOException e) { e.printStackTrace(); } } /** * 编码一帧数据 * * @param input 输入的数据 * @param pts 每一帧的时间戳 */ public void encodeFrame(byte[] input, long pts) { if (!mIsStarted) { Log.e(TAG, "Encoder is not started."); return; } // 取出可用的输入 buffer int inputBufferIndex = mEncoder.dequeueInputBuffer(-1); if (inputBufferIndex >= 0) { ByteBuffer inputBuffer = mEncoder.getInputBuffer(inputBufferIndex); inputBuffer.clear(); inputBuffer.put(input); // 将输入的数据喂给编码器 mEncoder.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0); } // 取出可用的输出 buffer int outputBufferIndex = mEncoder.dequeueOutputBuffer(mBufferInfo, 0); while (outputBufferIndex >= 0) { mOutputBuffer = mEncoder.getOutputBuffer(outputBufferIndex); mOutputBuffer.position(mBufferInfo.offset); mOutputBuffer.limit(mBufferInfo.offset + mBufferInfo.size); // 写入到复用器 mMuxer.writeSampleData(mTrackIndex, mOutputBuffer, mBufferInfo); // 释放输出 buffer mEncoder.releaseOutputBuffer(outputBufferIndex, false); outputBufferIndex = mEncoder.dequeueOutputBuffer(mBufferInfo, 0); } } /** * 停止编码 */ public void stop() { if (!mIsStarted) { Log.e(TAG, "Encoder is not started."); return; } mEncoder.stop(); mEncoder.release(); mMuxer.stop(); mMuxer.release(); mIsStarted = false; } /** * 初始化视频轨道 */ public void initTrack() { if (!mIsStarted) { Log.e(TAG, "Encoder is not started."); return; } mTrackIndex = mMuxer.addTrack(mEncoder.getOutputFormat()); mMuxer.start(); mBufferInfo = new MediaCodec.BufferInfo(); } } ``` 这段代码中,我们使用 MediaCodec 进行视频编码,并将编码后的数据通过 MediaMuxer 写入到文件中。 需要注意的是,编码器的输入数据必须是 YUV 格式的数据,而不是一般的 RGB 格式,因此你需要将原始数据转化为 YUV 格式,然后再进行编码。除此之外,还需要注意编码器的配置参数,例如视频宽度、高度、帧率、码率等等。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值