簡介
MediaCodec是Android提供的用於對音視頻進行編解碼的類,即編碼器/解碼器組件。它通過訪問底層的Codec來實現編解碼的功能。是Android media基礎框架的一部分,通常和 MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface和AudioTrack 一起使用,在視頻播放和視頻壓縮編碼中起到重要作用。詳細的api介紹請看官方文檔
工作流程
整體的流程上看,MediaCodec編解碼器是對輸入數據進行處理然後生成輸出數據,這個過程是異步的,並使用了一組輸入和輸出緩衝區。
流程如下圖:
- 客戶端從MediaCodec請求或接收一個空的輸入緩衝區(ByteBuffer),填充數據後將其發送到MediaCodec進行處理。
- 編解碼器處理數據後將其輸出到一個空的輸出緩衝區(ByteBuffer)。
- 客戶端從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