首先,這一系列文章均基於本身的理解和實踐,可能有不對的地方,歡迎你們指正。
其次,這是一個入門系列,涉及的知識也僅限於夠用,深刻的知識網上也有許許多多的博文供你們學習了。
最後,寫文章過程當中,會借鑑參考其餘人分享的文章,會在文章最後列出,感謝這些做者的分享。android
碼字不易,轉載請註明出處!git
教程代碼:【Github傳送門】 |
---|
本文主要簡介Android使用硬解碼API實現硬解碼的流程,包含MediaCodec輸入輸出緩衝、MediaCodec解碼流程、解碼代碼封裝和講解。github
MediaCodec 是Android 4.1(api 16)版本引入的編解碼接口,同時支持音視頻的編碼和解碼。api
必定要好好理解接下來這兩幅圖,由於後續的代碼就是基於這兩幅圖來編寫的。緩存
首先,來看看MediaCodec的數據流,也是官方Api文檔中的,不少文章都會引用。框架
仔細看一下,MediaCodec將數據分爲兩部分,分別爲input(左邊)和output(右邊),即輸入和輸出兩個數據緩衝區。異步
input:是給客戶端輸入須要解碼的數據(解碼時)或者須要編碼的數據(編碼時)。ide
output:是輸出解碼好(解碼時)或者編碼好(編碼時)的數據給客戶端。函數
MediaCodec內部使用異步的方式對input和output數據進行處理。MediaCodec將處理好input的數據,填充到output緩衝區,交給客戶端渲染或處理post
注:客戶端處理完數據後,必須手動釋放output緩衝區,不然將會致使MediaCodec輸出緩衝被佔用,沒法繼續解碼。
依然是一副來自官方的狀態圖
再仔細看看這幅圖,總體上分爲三個大的狀態:Sotpped、Executing、Released。
首先,新建MediaCodec後,會進入Uninitialized狀態;
其次,調用configure方法配置參數後,會進入Configured;
再次,調用start方法後,MediaCodec進入Flushed狀態;
接着,調用dequeueInputBuffer方法後,進入Running狀態;
最後,當解碼/編碼結束時,進入End of Stream(EOF)狀態。
這時,一個視頻就處理完成了。
那麼,Flushed是什麼狀態呢?
從圖中咱們能夠看到,在Running或者End of Stream狀態時,均可以調用flush方法,從新進入Flushed狀態。
當咱們在解碼過程當中,進入了End of Stream後,解碼器就再也不接收輸入了,這時候,須要調用flush方法,從新進入接收數據狀態。
或者,咱們在播放視頻過程當中,想進行跳播,這時候,咱們須要Seek到指定的時間點,這時候,也須要調用flush方法,清除緩衝,不然解碼時間戳會混亂。
再次強調一下,必定要好好理解這兩幅圖,由於後續的代碼就是基於這兩幅圖來編寫的。
MediaCodec有兩種工做模式,分別爲異步模式和同步模式,這裏咱們使用同步模式,異步模式能夠參考官網例子。
根據官方的數據流圖和狀態圖,畫出一個最基礎的解碼流程以下:
通過初始化和配置之後,進入循環解碼流程,不斷的輸入數據,而後獲取解碼完數據,最後渲染出來,直到全部數據解碼完成(End of Stream)。
根據上面的流程圖,能夠發現,不管音頻仍是視頻,解碼流程基本是一致的,不一樣的地方只在於【配置】、【渲染】兩個部分。
所以,咱們將整個解碼流程抽象爲一個解碼基類:BaseDecoder,爲了規範代碼和更好的拓展性,咱們先定義一個解碼器:IDecoder,繼承Runnable。
interface IDecoder: Runnable {
/** * 暫停解碼 */
fun pause()
/** * 繼續解碼 */
fun goOn()
/** * 中止解碼 */
fun stop()
/** * 是否正在解碼 */
fun isDecoding(): Boolean
/** * 是否正在快進 */
fun isSeeking(): Boolean
/** * 是否中止解碼 */
fun isStop(): Boolean
/** * 設置狀態監聽器 */
fun setStateListener(l: IDecoderStateListener?)
/** * 獲取視頻寬 */
fun getWidth(): Int
/** * 獲取視頻高 */
fun getHeight(): Int
/** * 獲取視頻長度 */
fun getDuration(): Long
/** * 獲取視頻旋轉角度 */
fun getRotationAngle(): Int
/** * 獲取音視頻對應的格式參數 */
fun getMediaFormat(): MediaFormat?
/** * 獲取音視頻對應的媒體軌道 */
fun getTrack(): Int
/** * 獲取解碼的文件路徑 */
fun getFilePath(): String
}
複製代碼
定義瞭解碼器的一些基礎操做,如暫停/繼續/中止解碼,獲取視頻的時長,視頻的寬高,解碼狀態等等
爲何繼承Runnable?
這裏使用的是同步模式解碼,須要不斷循環壓入和拉取數據,是一個耗時操做,所以,咱們將解碼器定義爲一個Runnable,最後放到線程池中執行。
接着,繼承IDecoder,定義基礎解碼器BaseDecoder。
首先來看下基礎參數:
abstract class BaseDecoder: IDecoder {
//-------------線程相關------------------------
/** * 解碼器是否在運行 */
private var mIsRunning = true
/** * 線程等待鎖 */
private val mLock = Object()
/** * 是否能夠進入解碼 */
private var mReadyForDecode = false
//---------------解碼相關-----------------------
/** * 音視頻解碼器 */
protected var mCodec: MediaCodec? = null
/** * 音視頻數據讀取器 */
protected var mExtractor: IExtractor? = null
/** * 解碼輸入緩存區 */
protected var mInputBuffers: Array<ByteBuffer>? = null
/** * 解碼輸出緩存區 */
protected var mOutputBuffers: Array<ByteBuffer>? = null
/** * 解碼數據信息 */
private var mBufferInfo = MediaCodec.BufferInfo()
private var mState = DecodeState.STOP
private var mStateListener: IDecoderStateListener? = null
/** * 流數據是否結束 */
private var mIsEOS = false
protected var mVideoWidth = 0
protected var mVideoHeight = 0
//省略後面的方法
....
}
複製代碼
首先,咱們定義了線程相關的資源,用於判斷是否持續解碼的mIsRunning,掛起線程的mLock等。
而後,就是解碼相關的資源了,好比MdeiaCodec自己,輸入輸出緩衝,解碼狀態等等。
其中,有一個解碼狀態DecodeState和音視頻數據讀取器IExtractor。
爲了方便記錄解碼狀態,這裏使用一個枚舉類表示
enum class DecodeState {
/**開始狀態*/
START,
/**解碼中*/
DECODING,
/**解碼暫停*/
PAUSE,
/**正在快進*/
SEEKING,
/**解碼完成*/
FINISH,
/**解碼器釋放*/
STOP
}
複製代碼
前面說過,MediaCodec須要咱們不斷地喂數據給輸入緩衝,那麼數據從哪裏來呢?確定是音視頻文件了,這裏的IExtractor就是用來提取音視頻文件中數據流。
Android自帶有一個音視頻數據讀取器MediaExtractor,一樣爲了方便維護和拓展性,咱們依然先定一個讀取器IExtractor。
interface IExtractor {
/** * 獲取音視頻格式參數 */
fun getFormat(): MediaFormat?
/** * 讀取音視頻數據 */
fun readBuffer(byteBuffer: ByteBuffer): Int
/** * 獲取當前幀時間 */
fun getCurrentTimestamp(): Long
/** * Seek到指定位置,並返回實際幀的時間戳 */
fun seek(pos: Long): Long
fun setStartPos(pos: Long)
/** * 中止讀取數據 */
fun stop()
}
複製代碼
最重要的一個方法就是readBuffer,用於讀取音視頻數據流
前面咱們只貼出瞭解碼器的參數部分,接下來,貼出最重要的部分,也就是解碼流程部分。
abstract class BaseDecoder: IDecoder {
//省略參數定義部分,見上
.......
final override fun run() {
mState = DecodeState.START
mStateListener?.decoderPrepare(this)
//【解碼步驟:1. 初始化,並啓動解碼器】
if (!init()) return
while (mIsRunning) {
if (mState != DecodeState.START &&
mState != DecodeState.DECODING &&
mState != DecodeState.SEEKING) {
waitDecode()
}
if (!mIsRunning ||
mState == DecodeState.STOP) {
mIsRunning = false
break
}
//若是數據沒有解碼完畢,將數據推入解碼器解碼
if (!mIsEOS) {
//【解碼步驟:2. 將數據壓入解碼器輸入緩衝】
mIsEOS = pushBufferToDecoder()
}
//【解碼步驟:3. 將解碼好的數據從緩衝區拉取出來】
val index = pullBufferFromDecoder()
if (index >= 0) {
//【解碼步驟:4. 渲染】
render(mOutputBuffers!![index], mBufferInfo)
//【解碼步驟:5. 釋放輸出緩衝】
mCodec!!.releaseOutputBuffer(index, true)
if (mState == DecodeState.START) {
mState = DecodeState.PAUSE
}
}
//【解碼步驟:6. 判斷解碼是否完成】
if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
mState = DecodeState.FINISH
mStateListener?.decoderFinish(this)
}
}
doneDecode()
//【解碼步驟:7. 釋放解碼器】
release()
}
/** * 解碼線程進入等待 */
private fun waitDecode() {
try {
if (mState == DecodeState.PAUSE) {
mStateListener?.decoderPause(this)
}
synchronized(mLock) {
mLock.wait()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
/** * 通知解碼線程繼續運行 */
protected fun notifyDecode() {
synchronized(mLock) {
mLock.notifyAll()
}
if (mState == DecodeState.DECODING) {
mStateListener?.decoderRunning(this)
}
}
/** * 渲染 */
abstract fun render(outputBuffers: ByteBuffer, bufferInfo: MediaCodec.BufferInfo)
/** * 結束解碼 */
abstract fun doneDecode()
}
複製代碼
在Runnable的run回調方法中,集成了整個解碼流程:
abstract class BaseDecoder: IDecoder {
//省略上面已有代碼
......
private fun init(): Boolean {
//1.檢查參數是否完整
if (mFilePath.isEmpty() || File(mFilePath).exists()) {
Log.w(TAG, "文件路徑爲空")
mStateListener?.decoderError(this, "文件路徑爲空")
return false
}
//調用虛函數,檢查子類參數是否完整
if (!check()) return false
//2.初始化數據提取器
mExtractor = initExtractor(mFilePath)
if (mExtractor == null ||
mExtractor!!.getFormat() == null) return false
//3.初始化參數
if (!initParams()) return false
//4.初始化渲染器
if (!initRender()) return false
//5.初始化解碼器
if (!initCodec()) return false
return true
}
private fun initParams(): Boolean {
try {
val format = mExtractor!!.getFormat()!!
mDuration = format.getLong(MediaFormat.KEY_DURATION) / 1000
if (mEndPos == 0L) mEndPos = mDuration
initSpecParams(mExtractor!!.getFormat()!!)
} catch (e: Exception) {
return false
}
return true
}
private fun initCodec(): Boolean {
try {
//1.根據音視頻編碼格式初始化解碼器
val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
mCodec = MediaCodec.createDecoderByType(type)
//2.配置解碼器
if (!configCodec(mCodec!!, mExtractor!!.getFormat()!!)) {
waitDecode()
}
//3.啓動解碼器
mCodec!!.start()
//4.獲取解碼器緩衝區
mInputBuffers = mCodec?.inputBuffers
mOutputBuffers = mCodec?.outputBuffers
} catch (e: Exception) {
return false
}
return true
}
/** * 檢查子類參數 */
abstract fun check(): Boolean
/** * 初始化數據提取器 */
abstract fun initExtractor(path: String): IExtractor
/** * 初始化子類本身特有的參數 */
abstract fun initSpecParams(format: MediaFormat)
/** * 初始化渲染器 */
abstract fun initRender(): Boolean
/** * 配置解碼器 */
abstract fun configCodec(codec: MediaCodec, format: MediaFormat): Boolean
}
複製代碼
初始化方法中,分爲5個步驟,看起很複雜,實際很簡單。
檢查參數是否完整:路徑是否有效等
初始化數據提取器:初始化Extractor
初始化參數:提取一些必須的參數:duration,width,height等
初始化渲染器:視頻不須要,音頻爲AudioTracker
初始化解碼器:初始化MediaCodec
在initCodec()中,
val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
mCodec = MediaCodec.createDecoderByType(type)
複製代碼
初始化MediaCodec的時候:
須要說明的是:因爲音頻和視頻的初始化稍有不一樣,因此定義了幾個虛函數,將不一樣的東西交給子類去實現。具體將在下一篇文章[音視頻播放:音視頻同步]說明。
直接進入pushBufferToDecoder方法中
abstract class BaseDecoder: IDecoder {
//省略上面已有代碼
......
private fun pushBufferToDecoder(): Boolean {
var inputBufferIndex = mCodec!!.dequeueInputBuffer(2000)
var isEndOfStream = false
if (inputBufferIndex >= 0) {
val inputBuffer = mInputBuffers!![inputBufferIndex]
val sampleSize = mExtractor!!.readBuffer(inputBuffer)
if (sampleSize < 0) {
//若是數據已經取完,壓入數據結束標誌:BUFFER_FLAG_END_OF_STREAM
mCodec!!.queueInputBuffer(inputBufferIndex, 0, 0,
0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
isEndOfStream = true
} else {
mCodec!!.queueInputBuffer(inputBufferIndex, 0,
sampleSize, mExtractor!!.getCurrentTimestamp(), 0)
}
}
return isEndOfStream
}
}
複製代碼
調用瞭如下方法:
var inputBufferIndex = mCodec!!.dequeueInputBuffer(2000)
複製代碼
val inputBuffer = mInputBuffers!![inputBufferIndex]
val sampleSize = mExtractor!!.readBuffer(inputBuffer)
複製代碼
mCodec!!.queueInputBuffer(inputBufferIndex, 0,
sampleSize, mExtractor!!.getCurrentTimestamp(), 0)
複製代碼
注意:若是SampleSize返回-1,說明沒有更多的數據了。
這個時候,queueInputBuffer的最後一個參數要傳入結束標記MediaCodec.BUFFER_FLAG_END_OF_STREAM。
直接進入pullBufferFromDecoder()
abstract class BaseDecoder: IDecoder {
//省略上面已有代碼
......
private fun pullBufferFromDecoder(): Int {
// 查詢是否有解碼完成的數據,index >=0 時,表示數據有效,而且index爲緩衝區索引
var index = mCodec!!.dequeueOutputBuffer(mBufferInfo, 1000)
when (index) {
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {}
MediaCodec.INFO_TRY_AGAIN_LATER -> {}
MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
mOutputBuffers = mCodec!!.outputBuffers
}
else -> {
return index
}
}
return -1
}
}
複製代碼
第1、調用dequeueOutputBuffer方法查詢是否有解碼完成的可用數據,其中mBufferInfo用於獲取數據幀信息,第二參數是等待時間,這裏等待1000ms,填入-1是無限等待。
var index = mCodec!!.dequeueOutputBuffer(mBufferInfo, 1000)
複製代碼
第2、判斷index類型:
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:輸出格式改變了
MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:輸入緩衝改變了
MediaCodec.INFO_TRY_AGAIN_LATER:沒有可用數據,等會再來
大於等於0:有可用數據,index就是輸出緩衝索引
這裏調用了一個虛函數render,也就是將渲染交給子類
調用releaseOutputBuffer方法, 釋放輸出緩衝區。
注:第二個參數,是個boolean,命名爲render,這個參數在視頻解碼時,用於決定是否要將這一幀數據顯示出來。
mCodec!!.releaseOutputBuffer(index, true)
複製代碼
還記得咱們在把數據壓入解碼器時,當sampleSize < 0 時,壓入了一個結束標記嗎?
當接收到這個標誌後,解碼器就知道全部數據已經接收完畢,在全部數據解碼完成之後,會在最後一幀數據加上結束標記信息,即
if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
mState = DecodeState.FINISH
mStateListener?.decoderFinish(this)
}
複製代碼
在while循環結束後,釋放掉全部的資源。至此,一次解碼結束。
abstract class BaseDecoder: IDecoder {
//省略上面已有代碼
......
private fun release() {
try {
mState = DecodeState.STOP
mIsEOS = false
mExtractor?.stop()
mCodec?.stop()
mCodec?.release()
mStateListener?.decoderDestroy(this)
} catch (e: Exception) {
}
}
}
複製代碼
最後,解碼器定義的其餘方法(如pause、goOn、stop等)再也不細說,可查看工程源碼。
原本打算把音頻和視頻播放部分也放到本篇來說,最後發現篇幅太長,不利於閱讀,看了會累。因此把真正實現播放部分和下一篇【音視頻播放:音視頻同步】作一個整合,內容和長度都會更合理。
so,下一篇見!