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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章