前言
本片博客我們一起來研究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已經掌握的差不多了,接下來我們寫幾個小案例,進一步理解和加深他們的使用方法。