Android 音视频知识点(五)

278 阅读2分钟

本位着重介绍 MediaCodec 的多媒体解码,并输出数据的使用过程

整体数据流转说明

截屏2025-04-23 23.56.19.png

MediaCodec 的数据分为两个部分,从数据的输入到编解码后的数据的输出:

  • input : MediaCodec 会通过getInputBuffer(int bufferId) 去拿到一个空的 ByteBuffer , 用来给客户端去填入数据(比如解码,编码的数据),MediaCodec 会用这些数据进行解码/编码处理
  • output : MediaCodec 会把解码/编码的数据填充到一个空的 buffer 中,然后把这个填满数据的buffer给到客户端,之后需要释放这个 buffer,MediaCodec 才能继续填充数据。

状态变化过程:

截屏2025-04-23 23.57.09.png

实现过程: 1、使用 SurfaceView 作为容器

    <SurfaceView
            android:id="@+id/surface_view"
            android:layout_width="0dp"
            android:layout_height="250dp"
            app:layout_constraintTop_toBottomOf="@id/toolbar"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>

2、将 SurfaceView 中的 Surface 作为参数,传递给 mediaCodec 作为画布 整个流程就是解码一帧,渲染一帧的过程,mediaCodec 内部会维护一个队列进行数据的入队出队

class VideoDecoder(val surface: Surface, val videoPath: String) {

    var mediaExtractor: MediaExtractor? = null
    var mediaCodec: MediaCodec? = null
    var isRunning = false

    fun start() {
        mediaExtractor = MediaExtractor().apply {
            setDataSource(videoPath)
        }

        // 查找视频轨道
        val videoTrackIndex = (0 until mediaExtractor!!.trackCount)
            .firstOrNull { i ->
                mediaExtractor!!.getTrackFormat(i).getString(MediaFormat.KEY_MIME)?.startsWith("video/") ?: false
            } ?: throw IllegalStateException("No video track found")

        mediaExtractor!!.selectTrack(videoTrackIndex)

        val videoFormat = mediaExtractor!!.getTrackFormat(videoTrackIndex)

        // 初始化解码器
        mediaCodec = MediaCodec.createDecoderByType(videoFormat.getString(MediaFormat.KEY_MIME)
        !!).apply {
            configure(videoFormat, surface, null, 0)
            start()
        }

        isRunning = true

        CoroutineScope(Dispatchers.IO).launch {
            decodeLoop()
        }
    }

    suspend fun decodeLoop() {
        val codec = mediaCodec ?: return
        val extractor = mediaExtractor ?: return

        val bufferInfo = MediaCodec.BufferInfo()

        var startTime = System.nanoTime()

        while (isRunning) {
            // 提交数据到解码器输入缓冲区
            val inputBufferId = codec.dequeueInputBuffer(1000)
            if (inputBufferId >= 0) {
                val inputBuffer = codec.getInputBuffer(inputBufferId)!!
                val sampleSize = extractor.readSampleData(inputBuffer, 0)

                if (sampleSize >= 0) {
                    codec.queueInputBuffer(
                        inputBufferId,
                        0,
                        sampleSize,
                        extractor.sampleTime,
                        extractor.sampleFlags
                    )
                    extractor.advance()
                } else {
                    // 输入结束
                    codec.queueInputBuffer(
                        inputBufferId,
                        0,
                        0,
                        0,
                        MediaCodec.BUFFER_FLAG_END_OF_STREAM
                    )
                }
            }

            // 处理解码器输出
            val outputBufferId = codec.dequeueOutputBuffer(bufferInfo, 1000)
            when {
                outputBufferId >= 0 -> {
                    // 渲染到 Surface
                    val ptsUs = bufferInfo.presentationTimeUs
                    val nowUs = (System.nanoTime() - startTime) / 1000
                    if (ptsUs > nowUs) {
                        delay((ptsUs - nowUs)/1000)
                    }

                    codec.releaseOutputBuffer(outputBufferId, true)
                }
                outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
                    // 格式变化(通常首次回调)
                    val newFormat = codec.outputFormat
                }
            }

            // 检查结束标志
            if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
                break
            }
        }

        release()
    }

    fun release() {
        isRunning = false
        mediaCodec?.stop()
        mediaCodec?.release()
        mediaExtractor?.release()
    }

}

3、注意渲染到界面的时候,不能来一帧数据就渲染一帧数据,否则会造成画面快进的现象。 因为解码的速度会比视频的时间轴更快,所以渲染画面的实际应该跟随视频的时间轴进行渲染

以下这段逻辑就是判断时间轴的时间是否大于解码的时间,如果是,则等待,以时间轴的时间为准进行画面渲染


    var startTime = System.nanoTime()

    val ptsUs = bufferInfo.presentationTimeUs
    val nowUs = (System.nanoTime() - startTime) / 1000
    if (ptsUs > nowUs) {
        delay((ptsUs - nowUs) / 1000)
    }