首先,這一系列文章均基於本身的理解和實踐,可能有不對的地方,歡迎你們指正。
其次,這是一個入門系列,涉及的知識也僅限於夠用,深刻的知識網上也有許許多多的博文供你們學習了。
最後,寫文章過程當中,會借鑑參考其餘人分享的文章,會在文章最後列出,感謝這些做者的分享。php
碼字不易,轉載請註明出處!android
教程代碼:【Github傳送門】 |
---|
上一篇文章,主要講了Android MediaCodec實現音視頻硬解碼的流程,搭建了基礎解碼框架。本文將講解具體的音視頻渲染,包括MediaCodec初始化、Surface初始化,AudioTrack初始化、音視頻數據流分離提取等,以及很是重要的音視頻同步。git
在上一篇文章定義的解碼流程框架基類中,預留了幾個虛函數,留給子類初始化本身的東西,本篇,就來看看如何實現。github
上篇文章,屢次提到音視頻數據分離提取器,在實現音視頻解碼器子類以前,先把這個實現了。緩存
以前提過,Android原生自帶有一個MediaExtractor,用於音視頻數據分離和提取,接來下就基於這個,作一個支持音視頻提取的工具類MMExtractor:app
class MMExtractor(path: String?) {
/**音視頻分離器*/
private var mExtractor: MediaExtractor? = null
/**音頻通道索引*/
private var mAudioTrack = -1
/**視頻通道索引*/
private var mVideoTrack = -1
/**當前幀時間戳*/
private var mCurSampleTime: Long = 0
/**開始解碼時間點*/
private var mStartPos: Long = 0
init {
//【1,初始化】
mExtractor = MediaExtractor()
mExtractor?.setDataSource(path)
}
/** * 獲取視頻格式參數 */
fun getVideoFormat(): MediaFormat? {
//【2.1,獲取視頻多媒體格式】
for (i in 0 until mExtractor!!.trackCount) {
val mediaFormat = mExtractor!!.getTrackFormat(i)
val mime = mediaFormat.getString(MediaFormat.KEY_MIME)
if (mime.startsWith("video/")) {
mVideoTrack = i
break
}
}
return if (mVideoTrack >= 0)
mExtractor!!.getTrackFormat(mVideoTrack)
else null
}
/** * 獲取音頻格式參數 */
fun getAudioFormat(): MediaFormat? {
//【2.2,獲取音頻頻多媒體格式】
for (i in 0 until mExtractor!!.trackCount) {
val mediaFormat = mExtractor!!.getTrackFormat(i)
val mime = mediaFormat.getString(MediaFormat.KEY_MIME)
if (mime.startsWith("audio/")) {
mAudioTrack = i
break
}
}
return if (mAudioTrack >= 0) {
mExtractor!!.getTrackFormat(mAudioTrack)
} else null
}
/** * 讀取視頻數據 */
fun readBuffer(byteBuffer: ByteBuffer): Int {
//【3,提取數據】
byteBuffer.clear()
selectSourceTrack()
var readSampleCount = mExtractor!!.readSampleData(byteBuffer, 0)
if (readSampleCount < 0) {
return -1
}
mCurSampleTime = mExtractor!!.sampleTime
mExtractor!!.advance()
return readSampleCount
}
/** * 選擇通道 */
private fun selectSourceTrack() {
if (mVideoTrack >= 0) {
mExtractor!!.selectTrack(mVideoTrack)
} else if (mAudioTrack >= 0) {
mExtractor!!.selectTrack(mAudioTrack)
}
}
/** * Seek到指定位置,並返回實際幀的時間戳 */
fun seek(pos: Long): Long {
mExtractor!!.seekTo(pos, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
return mExtractor!!.sampleTime
}
/** * 中止讀取數據 */
fun stop() {
//【4,釋放提取器】
mExtractor?.release()
mExtractor = null
}
fun getVideoTrack(): Int {
return mVideoTrack
}
fun getAudioTrack(): Int {
return mAudioTrack
}
fun setStartPos(pos: Long) {
mStartPos = pos
}
/** * 獲取當前幀時間 */
fun getCurrentTimestamp(): Long {
return mCurSampleTime
}
}
複製代碼
比較簡單,直接把代碼貼出來了。框架
關鍵部分有5個,作一下簡單講解:ide
很簡單,兩句代碼:新建,而後設置音視頻文件路徑函數
mExtractor = MediaExtractor()
mExtractor?.setDataSource(path)
複製代碼
音頻和視頻是同樣的:工具
1)遍歷視頻文件中全部的通道,通常是音頻和視頻兩個通道;
2) 而後獲取對應通道的編碼格式,判斷是否包含"video/"或者"audio/"開頭的編碼格式;
3)最後經過獲取的索引,返回對應的音視頻多媒體格式信息。
重點看看如何提取數據:
1)readBuffer(byteBuffer: ByteBuffer)中的參數就是解碼器傳進來的,用於存放待解碼數據的緩衝區。
2)selectSourceTrack()方法中,根據當前選擇的通道(同時只選擇一個音/視頻通道),調用mExtractor!!.selectTrack(mAudioTrack)將通道切換正確。
3)而後讀取數據:
var readSampleCount = mExtractor!!.readSampleData(byteBuffer, 0)
複製代碼
此時,將返回讀取到的音視頻數據流的大小,小於0表示數據已經讀完。
4)進入下一幀:先記錄當前幀的時間戳,而後調用advance進入下一幀,這時讀取指針將自動移動到下一幀開頭。
//記錄當前幀的時間戳
mCurSampleTime = mExtractor!!.sampleTime
//進入下一幀
mExtractor!!.advance()
複製代碼
客戶端退出解碼的時候,須要調用stop是否提取器相關資源。
說明:seek(pos: Long)方法,主要用於跳播,快速將數據定位到指定的播放位置,可是,因爲視頻中,除了I幀之外,PB幀都須要依賴其餘的幀進行解碼,因此,一般只能seek到I幀,可是I幀一般和指定的播放位置有必定偏差,所以須要指定seek靠近哪一個關鍵幀,有如下三種類型:
SEEK_TO_PREVIOUS_SYNC:跳播位置的上一個關鍵幀
SEEK_TO_NEXT_SYNC:跳播位置的下一個關鍵幀
SEEK_TO_CLOSEST_SYNC:距離跳播位置的最近的關鍵幀
到這裏你就能夠明白,爲何咱們平時在看視頻時,拖動進度條釋放之後,視頻一般會在你釋放的位置往前一點
上面封裝的工具中,能夠支持音頻和視頻的數據提取,下面咱們將利用這個工具,用於分別提取音頻和視頻的數據。
先回顧一下,上篇文章定義的提取器模型:
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()
}
複製代碼
有了上面封裝的工具,一切就變得很簡單了,作一個代理轉接就好了。
class VideoExtractor(path: String): IExtractor {
private val mMediaExtractor = MMExtractor(path)
override fun getFormat(): MediaFormat? {
return mMediaExtractor.getVideoFormat()
}
override fun readBuffer(byteBuffer: ByteBuffer): Int {
return mMediaExtractor.readBuffer(byteBuffer)
}
override fun getCurrentTimestamp(): Long {
return mMediaExtractor.getCurrentTimestamp()
}
override fun seek(pos: Long): Long {
return mMediaExtractor.seek(pos)
}
override fun setStartPos(pos: Long) {
return mMediaExtractor.setStartPos(pos)
}
override fun stop() {
mMediaExtractor.stop()
}
}
複製代碼
class AudioExtractor(path: String): IExtractor {
private val mMediaExtractor = MMExtractor(path)
override fun getFormat(): MediaFormat? {
return mMediaExtractor.getAudioFormat()
}
override fun readBuffer(byteBuffer: ByteBuffer): Int {
return mMediaExtractor.readBuffer(byteBuffer)
}
override fun getCurrentTimestamp(): Long {
return mMediaExtractor.getCurrentTimestamp()
}
override fun seek(pos: Long): Long {
return mMediaExtractor.seek(pos)
}
override fun setStartPos(pos: Long) {
return mMediaExtractor.setStartPos(pos)
}
override fun stop() {
mMediaExtractor.stop()
}
}
複製代碼
咱們先來定義一個視頻解碼器子類,繼承BaseDecoder
class VideoDecoder(path: String,
sfv: SurfaceView?,
surface: Surface?): BaseDecoder(path) {
private val TAG = "VideoDecoder"
private val mSurfaceView = sfv
private var mSurface = surface
override fun check(): Boolean {
if (mSurfaceView == null && mSurface == null) {
Log.w(TAG, "SurfaceView和Surface都爲空,至少須要一個不爲空")
mStateListener?.decoderError(this, "顯示器爲空")
return false
}
return true
}
override fun initExtractor(path: String): IExtractor {
return VideoExtractor(path)
}
override fun initSpecParams(format: MediaFormat) {
}
override fun configCodec(codec: MediaCodec, format: MediaFormat): Boolean {
if (mSurface != null) {
codec.configure(format, mSurface , null, 0)
notifyDecode()
} else {
mSurfaceView?.holder?.addCallback(object : SurfaceHolder.Callback2 {
override fun surfaceRedrawNeeded(holder: SurfaceHolder) {
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
}
override fun surfaceCreated(holder: SurfaceHolder) {
mSurface = holder.surface
configCodec(codec, format)
}
})
return false
}
return true
}
override fun initRender(): Boolean {
return true
}
override fun render(outputBuffers: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
}
override fun doneDecode() {
}
}
複製代碼
上篇文章中,定義好了解碼流程框架,子類定義就很簡單清晰了,只需循序漸進,填寫基類中預留的虛函數便可。
能夠看到,視頻解碼支持兩種類型渲染表面,一個是SurfaceView,一個Surface。當其實最後都是傳遞Surface給MediaCodec
override fun initExtractor(path: String): IExtractor {
return VideoExtractor(path)
}
複製代碼
解碼器的配置只需一句代碼:
codec.configure(format, mSurface , null, 0)
複製代碼
不知道在上一篇文章,你有沒有發現,在BaseDecoder初始化解碼器的方法initCodec()中, 調用了configCodec方法後,會進入waitDecode方法,將線程掛起。
abstract class BaseDecoder(private val mFilePath: String): IDecoder {
//省略其餘
......
private fun initCodec(): Boolean {
try {
val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
mCodec = MediaCodec.createDecoderByType(type)
if (!configCodec(mCodec!!, mExtractor!!.getFormat()!!)) {
waitDecode()
}
mCodec!!.start()
mInputBuffers = mCodec?.inputBuffers
mOutputBuffers = mCodec?.outputBuffers
} catch (e: Exception) {
return false
}
return true
}
}
複製代碼
就是由於考慮到一個問題,SurfaceView的建立是有一個時間過程的,並不是立刻可使用,須要經過CallBack來監聽它的狀態。
在surface初始化完畢後,再配置MediaCodec。
override fun surfaceCreated(holder: SurfaceHolder) {
mSurface = holder.surface
configCodec(codec, format)
}
複製代碼
若是使用OpenGL直接傳遞surface進來,直接配置MediaCodec便可。
上文提到過,視頻的渲染並不須要客戶端手動去渲染,只需提供繪製表面surface,調用releaseOutputBuffer,將2個參數設置爲true便可。因此,這裏也不用在作什麼操做了。
mCodec!!.releaseOutputBuffer(index, true)
複製代碼
有了上面視頻播放器的基礎之後,音頻播放器也是分分鐘搞定的事了。
class AudioDecoder(path: String): BaseDecoder(path) {
/**採樣率*/
private var mSampleRate = -1
/**聲音通道數量*/
private var mChannels = 1
/**PCM採樣位數*/
private var mPCMEncodeBit = AudioFormat.ENCODING_PCM_16BIT
/**音頻播放器*/
private var mAudioTrack: AudioTrack? = null
/**音頻數據緩存*/
private var mAudioOutTempBuf: ShortArray? = null
override fun check(): Boolean {
return true
}
override fun initExtractor(path: String): IExtractor {
return AudioExtractor(path)
}
override fun initSpecParams(format: MediaFormat) {
try {
mChannels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
mSampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
mPCMEncodeBit = if (format.containsKey(MediaFormat.KEY_PCM_ENCODING)) {
format.getInteger(MediaFormat.KEY_PCM_ENCODING)
} else {
//若是沒有這個參數,默認爲16位採樣
AudioFormat.ENCODING_PCM_16BIT
}
} catch (e: Exception) {
}
}
override fun configCodec(codec: MediaCodec, format: MediaFormat): Boolean {
codec.configure(format, null , null, 0)
return true
}
override fun initRender(): Boolean {
val channel = if (mChannels == 1) {
//單聲道
AudioFormat.CHANNEL_OUT_MONO
} else {
//雙聲道
AudioFormat.CHANNEL_OUT_STEREO
}
//獲取最小緩衝區
val minBufferSize = AudioTrack.getMinBufferSize(mSampleRate, channel, mPCMEncodeBit)
mAudioOutTempBuf = ShortArray(minBufferSize/2)
mAudioTrack = AudioTrack(
AudioManager.STREAM_MUSIC,//播放類型:音樂
mSampleRate, //採樣率
channel, //通道
mPCMEncodeBit, //採樣位數
minBufferSize, //緩衝區大小
AudioTrack.MODE_STREAM) //播放模式:數據流動態寫入,另外一種是一次性寫入
mAudioTrack!!.play()
return true
}
override fun render(outputBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
if (mAudioOutTempBuf!!.size < bufferInfo.size / 2) {
mAudioOutTempBuf = ShortArray(bufferInfo.size / 2)
}
outputBuffer.position(0)
outputBuffer.asShortBuffer().get(mAudioOutTempBuf, 0, bufferInfo.size/2)
mAudioTrack!!.write(mAudioOutTempBuf!!, 0, bufferInfo.size / 2)
}
override fun doneDecode() {
mAudioTrack?.stop()
mAudioTrack?.release()
}
}
複製代碼
初始化流程和視頻是同樣的,不同的地方有三個:
音頻不須要surface,直接傳null
codec.configure(format, null , null, 0)
複製代碼
音頻播放須要獲取採樣率,通道數,採樣位數等
因爲解碼出來的數據是PCM數據,因此直接使用AudioTrack播放便可。在initRender() 中對其進行初始化。
AudioTrack.getMinBufferSize(mSampleRate, channel, mPCMEncodeBit)
複製代碼
mAudioTrack = AudioTrack(
AudioManager.STREAM_MUSIC,//播放類型:音樂
mSampleRate, //採樣率
channel, //通道
mPCMEncodeBit, //採樣位數
minBufferSize, //緩衝區大小
AudioTrack.MODE_STREAM) //播放模式:數據流動態寫入,另外一種是一次性寫入
mAudioTrack!!.play()
複製代碼
最後就是將解碼出來的數據寫入AudioTrack,實現播放。
有一點注意的點是,須要把解碼數據由ByteBuffer類型轉換爲ShortBuffer,這時Short數據類型的長度要減半。
以上,基本實現了音視頻的播放流程,如無心外,在頁面上調用以上音視頻解碼器,就能夠實現播放了。
簡單看下頁面和相關調用。
main_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">
<SurfaceView android:id="@+id/sfv" app:layout_constraintTop_toTopOf="parent" android:layout_width="match_parent" android:layout_height="200dp"/>
</android.support.constraint.ConstraintLayout>
複製代碼
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initPlayer()
}
private fun initPlayer() {
val path = Environment.getExternalStorageDirectory().absolutePath + "/mvtest.mp4"
//建立線程池
val threadPool = Executors.newFixedThreadPool(2)
//建立視頻解碼器
val videoDecoder = VideoDecoder(path, sfv, null)
threadPool.execute(videoDecoder)
//建立音頻解碼器
val audioDecoder = AudioDecoder(path)
threadPool.execute(audioDecoder)
//開啓播放
videoDecoder.goOn()
audioDecoder.goOn()
}
}
複製代碼
至此,基本上實現音視頻的解碼和播放。可是若是你真正把代碼跑起來的話,你會發現:視頻和音頻爲何不一樣步啊,視頻就像倍速播放同樣,一下就播完了,可是音頻卻很正常。
這就要引出下一個不可避免的問題了,那就是音視頻同步。
因爲視頻和音頻是兩個獨立的任務在運行,視頻和音頻的解碼速度也不同,解碼出來的數據也不必定立刻就能夠顯示出來。
在第一篇文章的時候有說過,解碼有兩個重要的時間參數:PTS和DTS,分別用於表示渲染的時間和解碼時間,這裏就須要用到PTS。
播放器中通常存在三個時間,音頻的時間,視頻的時間,還有另一個就是系統時間。這樣能夠用來實現同步的時間源就有三個:
一般狀況下,因爲人類對聲音比較敏感,而且視頻解碼的PTS一般不是連續,而音頻的PTS是比較連續的,若是以視頻爲同步信號源的話,基本上聲音都會出現異常,而畫面的播放也會像倍速播放同樣。
那麼剩下的兩個選擇中,以音頻的PTS做爲同步源,讓畫面適配音頻是比較不錯的一種選擇。
可是這裏不採用,而是使用系統時間做爲同步信號源。由於若是以音頻PTS做爲同步源的話,須要比較複雜的同步機制,音頻和視頻二者之間也有比較多的耦合。
而系統時間做爲統一信號源則很是適合,音視頻彼此獨立互不干擾,同時又能夠保證基本一致。
要實現音視頻之間的同步,這裏須要考慮的有兩個點:
1. 比對
在解碼數據出來之後,檢查PTS時間戳和當前系統流過的時間差距,快則延時,慢則直接播放
2. 矯正
在進入暫停或解碼結束,從新恢復播放時,須要將系統流過的時間作一下矯正,將暫停的時間減去,恢復真正的流逝時間,即已播放時間。
從新看回BaseDecoder解碼流程:
abstract class BaseDecoder(private val mFilePath: String): IDecoder {
//省略其餘
......
/** * 開始解碼時間,用於音視頻同步 */
private var mStartTimeForSync = -1L
final override fun run() {
if (mState == DecodeState.STOP) {
mState = DecodeState.START
}
mStateListener?.decoderPrepare(this)
//【解碼步驟:1. 初始化,並啓動解碼器】
if (!init()) return
Log.i(TAG, "開始解碼")
while (mIsRunning) {
if (mState != DecodeState.START &&
mState != DecodeState.DECODING &&
mState != DecodeState.SEEKING) {
Log.i(TAG, "進入等待:$mState")
waitDecode()
// ---------【同步時間矯正】-------------
//恢復同步的起始時間,即去除等待流失的時間
mStartTimeForSync = System.currentTimeMillis() - getCurTimeStamp()
}
if (!mIsRunning ||
mState == DecodeState.STOP) {
mIsRunning = false
break
}
if (mStartTimeForSync == -1L) {
mStartTimeForSync = System.currentTimeMillis()
}
//若是數據沒有解碼完畢,將數據推入解碼器解碼
if (!mIsEOS) {
//【解碼步驟:2. 見數據壓入解碼器輸入緩衝】
mIsEOS = pushBufferToDecoder()
}
//【解碼步驟:3. 將解碼好的數據從緩衝區拉取出來】
val index = pullBufferFromDecoder()
if (index >= 0) {
// ---------【音視頻同步】-------------
if (mState == DecodeState.DECODING) {
sleepRender()
}
//【解碼步驟: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) {
Log.i(TAG, "解碼結束")
mState = DecodeState.FINISH
mStateListener?.decoderFinish(this)
}
}
doneDecode()
release()
}
}
複製代碼
答案是:數據解碼出來之後,渲染以前。
解碼器進入解碼狀態之後,來到【解碼步驟:3. 將解碼好的數據從緩衝區拉取出來】,這時若是數據是有效的,那麼進入比對。
// ---------【音視頻同步】-------------
final override fun run() {
//......
//【解碼步驟:3. 將解碼好的數據從緩衝區拉取出來】
val index = pullBufferFromDecoder()
if (index >= 0) {
// ---------【音視頻同步】-------------
if (mState == DecodeState.DECODING) {
sleepRender()
}
//【解碼步驟:4. 渲染】
render(mOutputBuffers!![index], mBufferInfo)
//【解碼步驟:5. 釋放輸出緩衝】
mCodec!!.releaseOutputBuffer(index, true)
if (mState == DecodeState.START) {
mState = DecodeState.PAUSE
}
}
//......
}
private fun sleepRender() {
val passTime = System.currentTimeMillis() - mStartTimeForSync
val curTime = getCurTimeStamp()
if (curTime > passTime) {
Thread.sleep(curTime - passTime)
}
}
override fun getCurTimeStamp(): Long {
return mBufferInfo.presentationTimeUs / 1000
}
複製代碼
同步的原理以下:
進入解碼前,獲取當前系統時間,存放在mStartTimeForSync,一幀數據解碼出來之後,計算當前系統時間和mStartTimeForSync的距離,也就是已經播放的時間,若是當前幀的PTS大於流失的時間,進入sleep,不然直接渲染。
在進入暫停之後,因爲系統時間一直在走,而mStartTimeForSync並無隨着系統時間累加,因此當恢復播放之後,從新將mStartTimeForSync加上這段暫停的時間段。
只不過計算方法有多種:
一種是記錄暫停的時間,恢復時用系統時間減去暫停時間,就是暫停的時間段,而後用mStartTimeForSync加上這段暫停的時間段,就是新的mStartTimeForSync;
另外一個種是用恢復播放時的系統時間,減去當前正要播放的幀的PTS,得出的值就是mStartTimeForSync。
這裏採用第二種
if (mState != DecodeState.START &&
mState != DecodeState.DECODING &&
mState != DecodeState.SEEKING) {
Log.i(TAG, "進入等待:$mState")
waitDecode()
// ---------【同步時間矯正】-------------
//恢復同步的起始時間,即去除等待流失的時間
mStartTimeForSync = System.currentTimeMillis() - getCurTimeStamp()
}
複製代碼
至此,從解碼到播放,再到音視頻同步,一個簡單的播放器就作完了。
下一篇,將會簡單介紹如何使用Android提供的MediaMuxer封裝Mp4,不會涉及到編碼和解碼,只涉及數據的解封和封裝,爲後面的【解封裝->解碼->編輯->編碼->封裝】全流程做準備。