Android音视频开发(九)MediaCodec解码播放视频

简介

MediaCodec是Android提供的用于对音视频进行编解码的类,即编码器/解码器组件。它通过访问底层的Codec来实现编解码的功能。是Android media基础框架的一部分,通常和 MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface和AudioTrack 一起使用,在视频播放和视频压缩编码中起到重要作用。详细的api介绍请看官方文档

工作流程

整体的流程上看,MediaCodec编解码器是对输入数据进行处理然后生成输出数据,这个过程是异步的,并使用了一组输入和输出缓冲区。
流程如下图:
MediaCodec

  1. 客户端从MediaCodec请求或接收一个空的输入缓冲区(ByteBuffer),填充数据后将其发送到MediaCodec进行处理。
  2. 编解码器处理数据后将其输出到一个空的输出缓冲区(ByteBuffer)。
  3. 客户端从MediaCodec获取已填充的输出缓冲区,获取其内容并使用,然后将其释放回编解码器。

常用api介绍

  • createDecoderByType
    根据MimeType创建一个解码器

  • configure
    配置MediaCodec

  • getInputBuffer(index)
    获取需要编码数据的输入流队列,返回一个ByteBuffer

  • dequeueInputBuffer(long timeoutUs)
    返回有效数据填充的输入缓冲区的索引;如果当前没有可用的缓冲区,则返回-1。如果timeoutUs == 0,则此方法将立即返回;如果timeoutUs <0,则无限期等待输入缓冲区的可用性;如果timeoutUs> 0,则等待直至“ timeoutUs”微秒。

  • queueInputBuffer
    在指定索引处填充一定范围的数据到输入缓冲区

  • dequeueOutputBuffer
    从输出缓冲区取出数据,返回数据索引或一些定义的状态常量

  • getOutputBuffer(index)
    返回输出缓冲区队列中指定索引的ByteBuffer

  • releaseOutputBuffer
    处理完成,释放ByteBuffer数据

使用

下面介绍下MediaCodec+MediaExrtractor+SurfaceView进行视频播放的示例:
注意的是:音频和视频需要分开播放

第一步,初始化MediaExrtractor,从视频文件中获取视频轨道信息

private fun init() {
    
    
        try {
    
    
                //创建MediaExtractor对象
                videoExtractor = MediaExtractor()
                //设置视频数据源,可以是本地文件也可以是网络文件
                //注意,安卓9.0以上不允许http明文链接请求的地址,需要适配
                videoExtractor?.setDataSource(videoPath!!)
                val count = videoExtractor!!.trackCount //获取轨道数量
                //视频
                for (i in 0 until count) {
    
    
                    val mediaFormat = videoExtractor!!.getTrackFormat(i)
                    val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
                    if (mimeType.startsWith("video/")) {
    
    //获取到视频轨道
                        videoExtractor?.selectTrack(i)
                        initVideo(mediaFormat)
                        break
                    }
                }
        } catch (e: Exception) {
    
    
            Log.e("Test", "出错了", e)
        }
    }

第二步,初始化MediaCodec视频解码器

private fun initVideo(mediaFormat: MediaFormat) {
    
    
        val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
        val codecInfo = getCodecInfo(mimeType)//获取支持的解码格式

        if (codecInfo != null) {
    
    
            handler.sendMessage(Message.obtain(handler, 100, mediaFormat))
            //根据MimeType创建解码器
            videoCodec = MediaCodec.createDecoderByType(mimeType)
            //配置,在此关联SurfaceView
            //参数说明:mediaFormat:视频信息,surface:surface容器,crypto:数据加密 flags:解码器/编码器
            videoCodec?.configure(mediaFormat, sv_video.holder.surface, null, 0)
            videoCodec?.start()//开始解码

        } else {
    
    
            Log.e("Test", "格式错误")
        }
    }

第三步,创建线程,进行解码播放操作

  private val decodeVideoRunnable = Runnable {
    
    
        try {
    
    
            //存放目标文件的数据
            var byteBuffer: ByteBuffer? = null
            //解码后的数据,包含每一个buffer的元数据信息,例如偏差,在相关解码器中有效的数据大小
            val info = MediaCodec.BufferInfo()
//            videoCodec!!.getOutputBuffers()
            var first = false
            var startWhen: Long = 0
            var isInput = true
            while (isDecoding) {
    
    
//            Thread.sleep(20)//可以控制慢放
                if (isInput) {
    
    
                    //1.准备一个输入缓冲区
                    val inIndex = videoCodec!!.dequeueInputBuffer(TIMEOUT)

                    if (inIndex >= 0) {
    
    
                        //2.准备填充数据
                        byteBuffer = videoCodec!!.getInputBuffer(inIndex)
                        //使用MediaExtractor读取视频数据
                        val sampleSize = videoExtractor!!.readSampleData(byteBuffer!!, 0)

                        if (videoExtractor!!.advance() && sampleSize > 0) {
    
     //有数据,数据可用
                            //3.写入数据到输入缓冲区
                            videoCodec!!.queueInputBuffer(
                                inIndex,
                                0,
                                sampleSize,
                                videoExtractor!!.sampleTime,
                                0
                            )

                        } else {
    
     //没有数据了,停止输入处理
                            videoCodec!!.queueInputBuffer(
                                inIndex,
                                0,
                                0,
                                0,
                                MediaCodec.BUFFER_FLAG_END_OF_STREAM
                            )
                            isInput = false
                        }
                    } else {
    
    
                        continue
                    }
                }
                //4 获取一个输出缓冲区,开始解码
                val outIndex = videoCodec!!.dequeueOutputBuffer(info, TIMEOUT)

                if (outIndex >= 0) {
    
    
                    when (outIndex) {
    
    
                        MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
    
    
                            Log.d("Test", "INFO_OUTPUT_BUFFERS_CHANGED")
                            videoCodec!!.getOutputBuffer(outIndex)
                        }
                        MediaCodec.INFO_OUTPUT_FORMAT_CHANGED ->
                            Log.d(
                                "Test",
                                "INFO_OUTPUT_FORMAT_CHANGED format : " + videoCodec!!.getOutputFormat()
                            )
                        MediaCodec.INFO_TRY_AGAIN_LATER -> {
    
    
                        }
                        else -> {
    
    
                            if (!first) {
    
    
                                startWhen = System.currentTimeMillis()
                                first = true
                            }
                            try {
    
    
                                val sleepTime: Long =
                                    info.presentationTimeUs / 1000 - (System.currentTimeMillis() - startWhen)
                                if (sleepTime > 0) Thread.sleep(sleepTime)
                            } catch (e: InterruptedException) {
    
    
                                e.printStackTrace()
                            }
                            //对outputbuffer的处理完后,调用这个函数把buffer重新返回给codec类。
                            //调用这个api之后,SurfaceView才有图像
                            videoCodec!!.releaseOutputBuffer(outIndex, true)
                        }
                    }
                }
            }
            //解码完毕,释放资源
            videoCodec?.stop()
            videoCodec?.release()
            videoExtractor?.release()
        } catch (e: Exception) {
    
    
            Log.e("Test", "", e)
        }
    }

至此,视频播放就搞定了,下面讲一些音频播放,为了不冲突,这里使用了不同的MediaExtractor和MediaCodec
配置和启动过程其实跟视频解码差不多,不同点就是多了AudioTrack

//解码音频,进行播放
 private val decodeAudioRunnable = Runnable {
    
    
        try {
    
    
            val inputBuffers: Array<ByteBuffer> = audioCodec!!.getInputBuffers()
            var outputBuffers: Array<ByteBuffer> = audioCodec!!.getOutputBuffers()
            val info = BufferInfo()
            val buffsize = AudioTrack.getMinBufferSize(
                sampleRate,
                CHANNEL_OUT_STEREO,
                ENCODING_PCM_16BIT
            )

            // 创建AudioTrack对象
            var audioTrack: AudioTrack? = AudioTrack(
                AudioManager.STREAM_MUSIC, sampleRate,
                CHANNEL_OUT_STEREO,
                ENCODING_PCM_16BIT,
                buffsize,
                MODE_STREAM
            )
            //启动AudioTrack
            audioTrack!!.play()
            while (isDecoding) {
    
    
                val inIndex: Int = audioCodec!!.dequeueInputBuffer(TIMEOUT)
                if (inIndex >= 0) {
    
    
                    val buffer = inputBuffers[inIndex]
                    //从MediaExtractor中读取待解数据
                    val sampleSize = audioExtractor!!.readSampleData(buffer, 0)
                    if (sampleSize < 0) {
    
    
                        audioCodec!!.queueInputBuffer(
                            inIndex, 0, 0, 0,
                            MediaCodec.BUFFER_FLAG_END_OF_STREAM
                        )
                    } else {
    
     //向MediaDecoder输入待解码数据
                        audioCodec!!.queueInputBuffer(
                            inIndex,
                            0,
                            sampleSize,
                            videoExtractor!!.sampleTime,
                            0
                        )
                        audioExtractor!!.advance()
                    }
                    //从输出缓冲区队列取出解码后的数据
                    val outIndex: Int = audioCodec!!.dequeueOutputBuffer(info, TIMEOUT)
                    when (outIndex) {
    
    
                        MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
    
    
                            outputBuffers = audioCodec!!.getOutputBuffers()
                        }
                        MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
    
    
                            val format: MediaFormat = audioCodec!!.getOutputFormat()
                            audioTrack.playbackRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
                        }
                        MediaCodec.INFO_TRY_AGAIN_LATER -> Log.d(
                            "Test",
                            "dequeueOutputBuffer timed out!"
                        )
                        else -> {
    
    
                            val outBuffer = outputBuffers[outIndex]
                            //Log.v(TAG, "outBuffer: " + outBuffer);
                            val chunk = ByteArray(info.size)
                            // Read the buffer all at once
                            outBuffer[chunk]
                            //清空buffer,否则下一次得到的还会得到同样的buffer
                            outBuffer.clear()
                            // AudioTrack write data
                            audioTrack.write(chunk, info.offset, info.offset + info.size)
                            audioCodec!!.releaseOutputBuffer(outIndex, false)
                        }
                    }
                    // 所有帧都解码、播放完之后退出循环
                    if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
    
    
                        break
                    }
                }
            }
            //释放MediaDecoder资源
            audioCodec?.stop()
            audioCodec?.release()
            audioCodec = null
            audioExtractor?.release()
            audioExtractor = null
            //释放AudioTrack资源
            audioTrack.stop()
            audioTrack.release()
            audioTrack = null
        } catch (e: Exception) {
    
    
            Log.e("Test", "", e)
        }
    }

最后,说明一下一个关键点:视频宽高的确定。
我这里使用MediaExtractor获取到MediaFormat来获取视频宽高。实际开发中,一般是固定一个播放器的宽高,然后将SurfaceView进行缩放填充。

 /**
     * 根据视频大小改变SurfaceView大小
     */
    private fun changeVideoSize(mediaFormat: MediaFormat) {
    
    
        var videoWidth = mediaFormat.getInteger(MediaFormat.KEY_WIDTH) //获取高度
        var videoHeight = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT) //获取高度
        val surfaceWidth = sv_video.measuredWidth
        val surfaceHeight = sv_video.measuredHeight
        //根据视频尺寸去计算->视频可以在sufaceView中放大的最大倍数。
        var maxSize: Double
        maxSize =
            if (resources.configuration.orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
    
    
                //竖屏模式下按视频宽度计算放大倍数值
                max(videoWidth / surfaceWidth.toDouble(), videoHeight / surfaceHeight.toDouble())
            } else {
    
    
                //横屏模式下按视频高度计算放大倍数值
                max(videoWidth / surfaceHeight.toDouble(), videoHeight / surfaceWidth.toDouble())
            }

        //视频宽高分别/最大倍数值 计算出放大后的视频尺寸
        videoWidth = Math.ceil(videoWidth / maxSize).toInt();
        videoHeight = Math.ceil(videoHeight / maxSize).toInt();

        //将计算出的视频尺寸设置到surfaceView 让视频自动填充。
        sv_video.layoutParams = ConstraintLayout.LayoutParams(videoWidth, videoHeight);
    }

完整代码请看:VideoDemo

猜你喜欢

转载自blog.csdn.net/gs12software/article/details/106349092