Android音視頻系列(五):使用MediaCodec播放視頻文件

前言

本片博客我們一起來研究Android系統音視頻api中,應該算是最難、最複雜的類:MediaCodec

相對於之前介紹過的MediaPlayer,AudioRecod等等來說,MediaCodec用法稍微複雜了一些,而且有一些小坑值得踩一踩。

首先熟悉一個MediaCodec的常用方法:

createEncoderByType(@NonNul String type) :靜態構造方法,type爲指定的音視頻格式,創建指定格式的編碼器

createDecoderByType(@NonNull String type):靜態構造方法,type爲指定的音視頻格式,創建指定格式的解碼器

MediaCodec的設置
configure(
       @Nullable MediaFormat format,    // 綁定編解碼的媒體格式
       @Nullable Surface surface,       // 綁定surface,可以直接完成數據的渲染
       @Nullable MediaCrypto crypto,	// 加密算法
       @ConfigureFlag int flags) 		// 加密的格式,如果不需要直接設置0即可

int dequeueInputBuffer(long timeoutUs) :timeoutUs等待時間,返回可以使用的輸入buffer的索引

// 設置指定索引位置的buffer的信息
 queueInputBuffer(
            int index,  		// 數組的索引值
            int offset, 		// 寫入buffer的起始位置
            int size,   		// 寫入的輸出的長度
            long presentationTimeUs,    // 該數據顯示的時間戳
            int flags   		// 該數據的標記位,例如關鍵幀,結束幀等等
 )

timeoutUs等待時間,返回可以讀取的buffer的索引
int dequeueOutputBuffer(
            @NonNull BufferInfo info,  // 這個BufferInfo需要自己手動創建,調用後,會把該索引的數據的信息寫在裏面
            long timeoutUs  		   // 等待時間
) 

releaseOutputBuffer(int index, boolean render):釋放指定索引位置的buffer
index:索引
render:如果綁定了surface,該數據是否要渲染到畫布上

MediaCodec是系統級別的編解碼庫,底層還是調用native方法,使用MediaCodec的基本流程是:

創建與文件相匹配的MediaCodec -> MediaCodec寫入數據,進行編碼/解碼 -> 讀取MediaCodec編/解碼結果

過程主要是分以上三步,今天我們以MediaCodec播放視頻文件爲例,學習MediaCodec的用法。

正文

首先我在我的手機提前錄好了一個視頻文件,大家可以自己下載demo,設置自己的視頻路徑。

 private val filePath = "${Environment.getExternalStorageDirectory()}/DCIM/Camera/test.mp4"

首先我們完成必要的準備工作:

創建一個SurfaceView用於顯示顯示視頻:

 val surfaceView = SurfaceView(this)
// 設置Surface不維護自己的緩衝區,等待屏幕的渲染引擎將內容推送到用戶面前
// 該api已經廢棄,這個編輯會自動設置
// surfaceView.holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
surfaceView.holder.addCallback(this)
setContentView(surfaceView)

/**
* 開始播放視頻
*/
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
		// 視頻解碼
        if (workerThread == null) {
            workerThread = VideoMediaCodecWorker(holder!!.surface, filePath)
            workerThread!!.start()
        }
		// 音頻解碼
        if (audioMediaCodecWorker == null) {
            audioMediaCodecWorker = AudioMediaCodecWorker(filePath)
            audioMediaCodecWorker!!.start()
        }
}

/**
* 停止播放視頻
*/
override fun surfaceDestroyed(holder: SurfaceHolder?) {
        if (workerThread != null) {
            workerThread!!.interrupt()
            workerThread = null
        }
        if (audioMediaCodecWorker != null) {
            audioMediaCodecWorker!!.interrupt()
            audioMediaCodecWorker = null
        }
}

我們先看視頻解碼,因爲MediaCodec可以直接和Surface綁定,自動完成畫面的渲染,相對來說比較簡單,我們之前已經分析了主要的三步:

創建指定格式的MediaCodec

我們必須要知道視頻文件的編碼格式,才能解碼,所以我們藉助MediaExtractor:

// 設置要解析的視頻文件地址
try {
       mediaExtractor.setDataSource(filePath)
} catch (e: IOException) {
       e.printStackTrace()
}

 // 遍歷數據視頻軌道,創建指定格式的MediaCodec
for (i in 0 until mediaExtractor.trackCount) {
       val mediaFormat = mediaExtractor.getTrackFormat(i)
       Log.e(TAG, ">> format i $i : $mediaFormat")
       val mime = mediaFormat.getString(MediaFormat.KEY_MIME)
       Log.e(TAG, ">> mime i $i : $mime")
       // 找到視頻軌道,並創建MediaCodec解碼器
       if (mime.startsWith("video/")) {
           mediaExtractor.selectTrack(i)
           try {
               mediaCodec = MediaCodec.createDecoderByType(mime)
           } catch (e: IOException) {
               e.printStackTrace()
           }
           mediaCodec!!.configure(mediaFormat, surface, null, 0)
       }
}
// 沒找到音頻軌道,直接返回
mediaCodec?.start() ?: return

MediaCodec寫入數據,進行編碼/解碼

在第一步中,我們已經找到了視頻的格式,並選中了文件的中的視頻軌道,第二步,要把數據寫入到MediaCodec中。

// 是否已經讀到了結束的位置
var isEOS = false
while (!interrupted()) {
	   // 開始寫入解碼器
       if (!isEOS) {
           // 返回使用有效輸出的Buffer索引,如果沒有相關Buffer可用,就返回-1
           // 如果傳入的timeoutUs爲0, 將立馬返回
           // 如果輸入的buffer可用,就無限期等待,timeoutUs的單位是us
           val inIndex = mediaCodec!!.dequeueInputBuffer(10000)
           if (inIndex > 0) {
               // 找到指定索引的buffer
               val buffer = mediaCodec!!.getInputBuffer(inIndex)?: continue
               Log.e(TAG, ">> buffer $buffer")
               // 把視頻的數據寫入到buffer中
               val sampleSize = mediaExtractor.readSampleData(buffer, 0)
               // 已經讀取結束
               if (sampleSize < 0) {
                   Log.e(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM")
                   mediaCodec!!.queueInputBuffer(
                       inIndex,
                       0,
                       0,
                       0,
                        MediaCodec.BUFFER_FLAG_END_OF_STREAM
                        )
                   isEOS = true
               }
               // 把buffer放入隊列中 
				else {
                   mediaCodec!!.queueInputBuffer(
                       inIndex,
                       0,
                       sampleSize,
                       mediaExtractor.sampleTime,
                       0
                   )
                   mediaExtractor.advance()
               }
           }
      }
      
      ....
}

MediaCodec的寫入的過程有點類似IO流,通過while循環,我們已經把視頻文件中的視頻數據都寫入到解碼器中了。

讀取MediaCodec編/解碼結果

想要知道解碼的結果,我們要再重新讀取一遍,過程和第二步幾乎是一樣的:

// 用於對準視頻的時間戳
val startMs = System.currentTimeMillis()
while (!interrupted()) {
       // 開始寫入解碼器
       ......

       // 每個buffer的元數據包括具體範圍的偏移及大小,以及有效數據中相關的解碼的buffer
       val info = MediaCodec.BufferInfo()
       when (val outIndex = mediaCodec!!.dequeueOutputBuffer(info, 10000)) {
           // 此類型已經廢棄,如果使用的是getOutputBuffer()可以忽略此狀態
           MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
               // 當buffer的格式發生改變,須指向新的buffer格式
           }
           MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
               // 當buffer的格式發生改變,須指向新的buffer格式
           }
           MediaCodec.INFO_TRY_AGAIN_LATER -> {
               // 當dequeueOutputBuffer超時時,會到達此case
               Log.e(TAG, ">> dequeueOutputBuffer timeout")
           }
           else -> {
               // val buffer = outputBuffers[outIndex]
               // 這裏使用簡單的時鐘方式保持視頻的fps,不然視頻會播放的很快
               sleepRender(info, startMs)
               mediaCodec!!.releaseOutputBuffer(outIndex, true)
           }
       }

       // 在所有解碼後的幀都被渲染後,就可以停止播放了
       if ((info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
           Log.e(TAG, "OutputBuffer BUFFER_FLAG_END_OF_STREAM")
           break
       }
}

mediaCodec!!.stop()
mediaCodec!!.release()
mediaExtractor.release()

/*
*  數據的時間戳對齊
*/
private fun sleepRender(audioBufferInfo: MediaCodec.BufferInfo, startMs: Long) {
   // 這裏的時間是 毫秒  presentationTimeUs 的時間是累加的 以微秒進行一幀一幀的累加
   val timeDifference = audioBufferInfo.presentationTimeUs / 1000 - (System.currentTimeMillis() - startMs)
   if (timeDifference > 0) {
       try {
           sleep(timeDifference)
       } catch (e: InterruptedException) {
           e.printStackTrace()
       }
   }
}

因爲是我們是綁定的Surface,所以處理數據的過程我們不需要寫,第三步主要是注意數據時間戳的對齊,否否則畫面的顯示速度會很快,這裏使用了sleep來保持時間戳,並且使用後的數據要及時調用releaseOutputBuffer釋放資源。

以上步驟我們完成了視頻文件播放視頻的功能,接下來是音頻,其實音頻的解碼過程大同小異,唯一的區別是播放音頻需要自己使用AudioTrack。

創建AudioTrack

for (i in 0 until mediaExtractor.trackCount) {
       // 遍歷數據音視頻軌跡
       val mediaFormat = mediaExtractor.getTrackFormat(i)
       val mime = mediaFormat.getString(MediaFormat.KEY_MIME)
       if (mime.startsWith("audio/")) {
           mediaExtractor.selectTrack(i)
           try {
               mediaCodec = MediaCodec.createDecoderByType(mime)
           } catch (e: IOException) {
               e.printStackTrace()
           }
           mediaCodec!!.configure(mediaFormat, null, null, 0)
           // 聲道數
           val audioChannels = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
           // 音軌的採樣率
           val mSampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
           // 創建音軌
           audioTrack = AudioTrack(
               AudioAttributes.Builder()
                   .setLegacyStreamType(AudioManager.STREAM_MUSIC)
                   .build(),
               Builder()
                   .setChannelMask(if (audioChannels == 1) CHANNEL_OUT_MONO else CHANNEL_OUT_STEREO)
                   .setEncoding(ENCODING_PCM_16BIT)
                   .setSampleRate(mSampleRate)
                   .build(),
               AudioRecord.getMinBufferSize(
                   mSampleRate,
                   if (audioChannels == 1) CHANNEL_IN_MONO else CHANNEL_IN_STEREO,
                   ENCODING_PCM_16BIT
               ),
               AudioTrack.MODE_STREAM,
               AudioManager.AUDIO_SESSION_ID_GENERATE
           )
       }
   }

首先找到音軌的格式,我們需要知道音頻的採樣率和聲道數,如果信息不準備,則會出現聲道播放異常的情況(過快、噪音、過慢等),至於編碼位數我們直接使用16位。另外注意:Builder參數中的setChannelMask要使用CHANNEL_OUT_XXX,在AudioRecord.getMinBufferSize中要是用CHANNEL_IN_XXX,千萬不要用錯了。

播放音頻

音頻我們要手動從MediaCode從中得出來寫到AudioTrack中:

// 每個buffer的元數據包括具體範圍的偏移及大小,以及有效數據中相關的解碼的buffer
val info = MediaCodec.BufferInfo()
when (val outIndex = mediaCodec!!.dequeueOutputBuffer(info, 0)) {
           // 此類型已經廢棄,如果使用的是getOutputBuffer()可以忽略此狀態
           MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
               // 當buffer的格式發生改變,須指向新的buffer格式
           }
           MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
               // 當buffer的格式發生改變,須指向新的buffer格式
           }
           MediaCodec.INFO_TRY_AGAIN_LATER -> {
               // 當dequeueOutputBuffer超時時,會到達此case
               Log.e(TAG, ">> dequeueOutputBuffer timeout")
           }
           else -> {
               val buffer = mediaCodec!!.getOutputBuffer(outIndex)?: continue@loop
               //用來保存解碼後的數據
               buffer.position(0)
               val outData = ByteArray(info.size)
               buffer.get(outData)
               //清空緩存
               buffer.clear()

               audioTrack?.write(outData, 0, outData.size)
               sleepRender(info, startMs)
               mediaCodec!!.releaseOutputBuffer(outIndex, true)
           }
}

處了AudioTrack.write,其他的都是一樣的,也要注意音頻戳的對齊。

總結

經過五個章節的學習,我們已經把Android系統的音頻API已經掌握的差不多了,接下來我們寫幾個小案例,進一步理解和加深他們的使用方法。

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