基于安卓原生做一個視頻播放器

Android原生自帶有一個MediaExtractor永乌,用于音視頻數(shù)據(jù)分離和提取吨述,接來下就基于這個橙弱,做一個支持音視頻提取的工具類MMExtractor:


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)
    }

    /**
     * 獲取視頻格式參數(shù)
     */
    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
    }

    /**
     * 獲取音頻格式參數(shù)
     */
    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
    }

    /**
     * 讀取視頻數(shù)據(jù)
     */
    fun readBuffer(byteBuffer: ByteBuffer): Int {
        //【3,提取數(shù)據(jù)】
        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
    }

    /**
     * 停止讀取數(shù)據(jù)
     */
    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
    }
}

關(guān)鍵部分有5個,做一下簡單講解:

【1净当,初始化】
很簡單内斯,兩句代碼:新建,然后設(shè)置音視頻文件路徑

mExtractor = MediaExtractor()
mExtractor?.setDataSource(path)

【2.1/2.2像啼,獲取音視頻多媒體格式】
音頻和視頻是一樣的:

1)遍歷視頻文件中所有的通道俘闯,一般是音頻和視頻兩個通道;

2) 然后獲取對應(yīng)通道的編碼格式忽冻,判斷是否包含"video/"或者"audio/"開頭的編碼格式真朗;

3)最后通過獲取的索引,返回對應(yīng)的音視頻多媒體格式信息僧诚。

【3遮婶,提取數(shù)據(jù)】
重點看看如何提取數(shù)據(jù):

1)readBuffer(byteBuffer: ByteBuffer)中的參數(shù)就是解碼器傳進來的蝗碎,用于存放待解碼數(shù)據(jù)的緩沖區(qū)。

2)selectSourceTrack()方法中旗扑,根據(jù)當前選擇的通道(同時只選擇一個音/視頻通道)蹦骑,調(diào)用mExtractor!!.selectTrack(mAudioTrack)將通道切換正確。

3)然后讀取數(shù)據(jù):

var readSampleCount = mExtractor!!.readSampleData(byteBuffer, 0)
此時臀防,將返回讀取到的音視頻數(shù)據(jù)流的大小眠菇,小于0表示數(shù)據(jù)已經(jīng)讀完。

4)進入下一幀:先記錄當前幀的時間戳袱衷,然后調(diào)用advance進入下一幀捎废,這時讀取指針將自動移動到下一幀開頭。

//記錄當前幀的時間戳
mCurSampleTime = mExtractor!!.sampleTime
//進入下一幀
mExtractor!!.advance()
【4祟昭,釋放提取器】
客戶端退出解碼的時候缕坎,需要調(diào)用stop是否提取器相關(guān)資源。

說明:seek(pos: Long)方法篡悟,主要用于跳播谜叹,快速將數(shù)據(jù)定位到指定的播放位置,但是搬葬,由于視頻中荷腊,除了I幀以外,PB幀都需要依賴其他的幀進行解碼急凰,所以女仰,通常只能seek到I幀,但是I幀通常和指定的播放位置有一定誤差抡锈,因此需要指定seek靠近哪個關(guān)鍵幀疾忍,有以下三種類型:
SEEK_TO_PREVIOUS_SYNC:跳播位置的上一個關(guān)鍵幀
SEEK_TO_NEXT_SYNC:跳播位置的下一個關(guān)鍵幀
SEEK_TO_CLOSEST_SYNC:距離跳播位置的最近的關(guān)鍵幀

到這里你就可以明白,為什么我們平時在看視頻時床三,拖動進度條釋放以后一罩,視頻通常會在你釋放的位置往前一點

封裝音頻和視頻提取器
上面封裝的工具中,可以支持音頻和視頻的數(shù)據(jù)提取撇簿,下面我們將利用這個工具聂渊,用于分別提取音頻和視頻的數(shù)據(jù)。

先回顧一下四瘫,上篇文章定義的提取器模型:

interface IExtractor {

    fun getFormat(): MediaFormat?

    /**
     * 讀取音視頻數(shù)據(jù)
     */
    fun readBuffer(byteBuffer: ByteBuffer): Int

    /**
     * 獲取當前幀時間
     */
    fun getCurrentTimestamp(): Long

    /**
     * Seek到指定位置汉嗽,并返回實際幀的時間戳
     */
    fun seek(pos: Long): Long

    fun setStartPos(pos: Long)

    /**
     * 停止讀取數(shù)據(jù)
     */
    fun stop()
}

有了上面封裝的工具,一切就變得很簡單了找蜜,做一個代理轉(zhuǎn)接就行了饼暑。

視頻提取器

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() {
    }
}

迈着,定義好了解碼流程框架,子類定義就很簡單清晰了邪码,只需按部就班裕菠,填寫基類中預(yù)留的虛函數(shù)即可。

檢查參數(shù)
可以看到闭专,視頻解碼支持兩種類型渲染表面奴潘,一個是SurfaceView,一個Surface影钉。當其實最后都是傳遞Surface給MediaCodec

SurfaceView應(yīng)該是大家比較熟悉的View了画髓,最常使用的就是用來做MediaPlayer的顯示。當然也可以繪制圖片平委、動畫等奈虾。
Surface應(yīng)該不是很常用了,這里為了支持后續(xù)使用OpenGL來渲染視頻廉赔,所以預(yù)先做了支持肉微。
生成數(shù)據(jù)提取器

override fun initExtractor(path: String): IExtractor {
    return VideoExtractor(path)
}

配置解碼器
解碼器的配置只需一句代碼:

codec.configure(format, mSurface , null, 0)

在BaseDecoder初始化解碼器的方法initCodec()中, 調(diào)用了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的創(chuàng)建是有一個時間過程的劳曹,并非馬上可以使用,需要通過CallBack來監(jiān)聽它的狀態(tài)琅摩。

在surface初始化完畢后铁孵,再配置MediaCodec。

override fun surfaceCreated(holder: SurfaceHolder) {
    mSurface = holder.surface
    configCodec(codec, format)
}

如果使用OpenGL直接傳遞surface進來房资,直接配置MediaCodec即可蜕劝。

渲染
視頻的渲染并不需要客戶端手動去渲染,只需提供繪制表面surface志膀,調(diào)用releaseOutputBuffer,將2個參數(shù)設(shè)置為true即可鳖擒。所以溉浙,這里也不用在做什么操作了。

mCodec!!.releaseOutputBuffer(index, true)

三蒋荚、音頻播放
有了上面視頻播放器的基礎(chǔ)以后戳稽,音頻播放器也是分分鐘搞定的事了。

class AudioDecoder(path: String): BaseDecoder(path) {
    /**采樣率*/
    private var mSampleRate = -1
    
    /**聲音通道數(shù)量*/
    private var mChannels = 1

    /**PCM采樣位數(shù)*/
    private var mPCMEncodeBit = AudioFormat.ENCODING_PCM_16BIT

    /**音頻播放器*/
    private var mAudioTrack: AudioTrack? = null

    /**音頻數(shù)據(jù)緩存*/
    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 {
                //如果沒有這個參數(shù),默認為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
        }

        //獲取最小緩沖區(qū)
        val minBufferSize = AudioTrack.getMinBufferSize(mSampleRate, channel, mPCMEncodeBit)

        mAudioOutTempBuf = ShortArray(minBufferSize/2)

        mAudioTrack = AudioTrack(
            AudioManager.STREAM_MUSIC,//播放類型:音樂
            mSampleRate, //采樣率
            channel, //通道
            mPCMEncodeBit, //采樣位數(shù)
            minBufferSize, //緩沖區(qū)大小
            AudioTrack.MODE_STREAM) //播放模式:數(shù)據(jù)流動態(tài)寫入惊奇,另一種是一次性寫入
            
        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()
    }
}

初始化流程和視頻是一樣的互躬,不一樣的地方有三個:

  1. 初始化解碼器
    音頻不需要surface,直接傳null
codec.configure(format, null , null, 0)
  1. 獲取參數(shù)不一樣
    音頻播放需要獲取采樣率颂郎,通道數(shù)吼渡,采樣位數(shù)等

  2. 需要初始化一個音頻渲染器:AudioTrack
    由于解碼出來的數(shù)據(jù)是PCM數(shù)據(jù),所以直接使用AudioTrack播放即可乓序。在initRender()
    中對其進行初始化寺酪。

根據(jù)通道數(shù)量配置單聲道和雙聲道
根據(jù)采樣率、通道數(shù)替劈、采樣位數(shù)計算獲取最小緩沖區(qū)

AudioTrack.getMinBufferSize(mSampleRate, channel, mPCMEncodeBit)

創(chuàng)建AudioTrack寄雀,并啟動

mAudioTrack = AudioTrack(
            AudioManager.STREAM_MUSIC,//播放類型:音樂
            mSampleRate, //采樣率
            channel, //通道
            mPCMEncodeBit, //采樣位數(shù)
            minBufferSize, //緩沖區(qū)大小
            AudioTrack.MODE_STREAM) //播放模式:數(shù)據(jù)流動態(tài)寫入,另一種是一次性寫入
            
mAudioTrack!!.play()
  1. 手動渲染音頻數(shù)據(jù)陨献,實現(xiàn)播放
    最后就是將解碼出來的數(shù)據(jù)寫入AudioTrack盒犹,實現(xiàn)播放。

有一點注意的點是眨业,需要把解碼數(shù)據(jù)由ByteBuffer類型轉(zhuǎn)換為ShortBuffer急膀,這時Short數(shù)據(jù)類型的長度要減半。

四坛猪、調(diào)用并播放
以上脖阵,基本實現(xiàn)了音視頻的播放流程,如無意外墅茉,在頁面上調(diào)用以上音視頻解碼器命黔,就可以實現(xiàn)播放了。

簡單看下頁面和相關(guān)調(diào)用就斤。

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"
        
        //創(chuàng)建線程池
        val threadPool = Executors.newFixedThreadPool(2)
        
        //創(chuàng)建視頻解碼器
        val videoDecoder = VideoDecoder(path, sfv, null)
        threadPool.execute(videoDecoder)

        //創(chuàng)建音頻解碼器
        val audioDecoder = AudioDecoder(path)
        threadPool.execute(audioDecoder)
        
        //開啟播放
        videoDecoder.goOn()
        audioDecoder.goOn()
    }
}

至此悍募,基本上實現(xiàn)音視頻的解碼和播放。但是如果你真正把代碼跑起來的話洋机,你會發(fā)現(xiàn):視頻和音頻為什么不同步啊坠宴,視頻就像倍速播放一樣,一下就播完了绷旗,但是音頻卻很正常喜鼓。

這就要引出下一個不可避免的問題了,那就是音視頻同步衔肢。

五庄岖、音視頻同步

同步信號來源

由于視頻和音頻是兩個獨立的任務(wù)在運行,視頻和音頻的解碼速度也不一樣角骤,解碼出來的數(shù)據(jù)也不一定馬上就可以顯示出來隅忿。

在第一篇文章的時候有說過,解碼有兩個重要的時間參數(shù):PTS和DTS,分別用于表示渲染的時間和解碼時間背桐,這里就需要用到PTS优烧。

播放器中一般存在三個時間,音頻的時間链峭,視頻的時間畦娄,還有另外一個就是系統(tǒng)時間。這樣可以用來實現(xiàn)同步的時間源就有三個:

視頻時間戳
音頻時間戳
外部時間戳
視頻PTS

通常情況下熏版,由于人類對聲音比較敏感纷责,并且視頻解碼的PTS通常不是連續(xù),而音頻的PTS是比較連續(xù)的撼短,如果以視頻為同步信號源的話再膳,基本上聲音都會出現(xiàn)異常,而畫面的播放也會像倍速播放一樣曲横。

音頻PTS

那么剩下的兩個選擇中喂柒,以音頻的PTS作為同步源,讓畫面適配音頻是比較不錯的一種選擇禾嫉。

但是這里不采用灾杰,而是使用系統(tǒng)時間作為同步信號源。因為如果以音頻PTS作為同步源的話熙参,需要比較復(fù)雜的同步機制艳吠,音頻和視頻兩者之間也有比較多的耦合。

系統(tǒng)時間

而系統(tǒng)時間作為統(tǒng)一信號源則非常適合孽椰,音視頻彼此獨立互不干擾昭娩,同時又可以保證基本一致。

實現(xiàn)音視頻同步

要實現(xiàn)音視頻之間的同步黍匾,這里需要考慮的有兩個點:

  1. 比對

在解碼數(shù)據(jù)出來以后栏渺,檢查PTS時間戳和當前系統(tǒng)流過的時間差距,快則延時锐涯,慢則直接播放

  1. 矯正

在進入暫涂恼铮或解碼結(jié)束,重新恢復(fù)播放時纹腌,需要將系統(tǒng)流過的時間做一下矯正霎终,將暫停的時間減去,恢復(fù)真正的流逝時間升薯,即已播放時間莱褒。

重新看回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()
                
                // ---------【同步時間矯正】-------------
                //恢復(fù)同步的起始時間保礼,即去除等待流失的時間
                mStartTimeForSync = System.currentTimeMillis() - getCurTimeStamp()
            }

            if (!mIsRunning ||
                mState == DecodeState.STOP) {
                mIsRunning = false
                break
            }

            if (mStartTimeForSync == -1L) {
                mStartTimeForSync = System.currentTimeMillis()
            }

            //如果數(shù)據(jù)沒有解碼完畢,將數(shù)據(jù)推入解碼器解碼
            if (!mIsEOS) {
                //【解碼步驟:2. 見數(shù)據(jù)壓入解碼器輸入緩沖】
                mIsEOS = pushBufferToDecoder()
            }

            //【解碼步驟:3. 將解碼好的數(shù)據(jù)從緩沖區(qū)拉取出來】
            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, "解碼結(jié)束")
                mState = DecodeState.FINISH
                mStateListener?.decoderFinish(this)
            }
        }
        doneDecode()
        release()
    }
}

在不考慮暫停责语、恢復(fù)的情況下炮障,什么時候進行時間同步呢?
答案是:數(shù)據(jù)解碼出來以后坤候,渲染之前胁赢。

解碼器進入解碼狀態(tài)以后,來到【解碼步驟:3. 將解碼好的數(shù)據(jù)從緩沖區(qū)拉取出來】白筹,這時如果數(shù)據(jù)是有效的智末,那么進入比對。

// ---------【音視頻同步】-------------
final override fun run() {
    
    //......
    
    //【解碼步驟:3. 將解碼好的數(shù)據(jù)從緩沖區(qū)拉取出來】
    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
}

同步的原理如下:

進入解碼前徒河,獲取當前系統(tǒng)時間系馆,存放在mStartTimeForSync,一幀數(shù)據(jù)解碼出來以后顽照,計算當前系統(tǒng)時間和mStartTimeForSync的距離由蘑,也就是已經(jīng)播放的時間,如果當前幀的PTS大于流失的時間代兵,進入sleep尼酿,否則直接渲染。

考慮暫停情況下的時間矯正
在進入暫停以后植影,由于系統(tǒng)時間一直在走裳擎,而mStartTimeForSync并沒有隨著系統(tǒng)時間累加,所以當恢復(fù)播放以后思币,重新將mStartTimeForSync加上這段暫停的時間段鹿响。

只不過計算方法有多種:

一種是記錄暫停的時間,恢復(fù)時用系統(tǒng)時間減去暫停時間支救,就是暫停的時間段抢野,然后用mStartTimeForSync加上這段暫停的時間段,就是新的mStartTimeForSync各墨;

另一個種是用恢復(fù)播放時的系統(tǒng)時間指孤,減去當前正要播放的幀的PTS,得出的值就是mStartTimeForSync贬堵。

這里采用第二種

if (mState != DecodeState.START &&
mState != DecodeState.DECODING &&
mState != DecodeState.SEEKING) {
Log.i(TAG, "進入等待:$mState")

waitDecode()

// ---------【同步時間矯正】-------------
//恢復(fù)同步的起始時間恃轩,即去除等待流失的時間
mStartTimeForSync = System.currentTimeMillis() - getCurTimeStamp()

}
至此,從解碼到播放黎做,再到音視頻同步叉跛,一個簡單的播放器就做完了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蒸殿,一起剝皮案震驚了整個濱河市筷厘,隨后出現(xiàn)的幾起案子鸣峭,更是在濱河造成了極大的恐慌,老刑警劉巖酥艳,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摊溶,死亡現(xiàn)場離奇詭異,居然都是意外死亡充石,警方通過查閱死者的電腦和手機莫换,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來骤铃,“玉大人拉岁,你說我怎么就攤上這事《枧溃” “怎么了喊暖?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長撕瞧。 經(jīng)常有香客問我哄啄,道長,這世上最難降的妖魔是什么风范? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任咨跌,我火速辦了婚禮,結(jié)果婚禮上硼婿,老公的妹妹穿的比我還像新娘锌半。我一直安慰自己,他們只是感情好寇漫,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布刊殉。 她就那樣靜靜地躺著,像睡著了一般州胳。 火紅的嫁衣襯著肌膚如雪记焊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天栓撞,我揣著相機與錄音遍膜,去河邊找鬼。 笑死瓤湘,一個胖子當著我的面吹牛瓢颅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播弛说,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼挽懦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了木人?” 一聲冷哼從身側(cè)響起信柿,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤冀偶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后渔嚷,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蔫磨,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年圃伶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒲列。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡窒朋,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蝗岖,到底是詐尸還是另有隱情侥猩,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布抵赢,位于F島的核電站欺劳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏铅鲤。R本人自食惡果不足惜划提,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望邢享。 院中可真熱鬧鹏往,春花似錦、人聲如沸骇塘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽款违。三九已至唐瀑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間插爹,已是汗流浹背哄辣。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留赠尾,地道東北人柔滔。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像萍虽,于是被迫代替她去往敵國和親睛廊。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容