【Android 音視頻開發打怪升級:音視頻硬解碼篇】3、音視頻播放:音視頻同步

【聲 明】

首先,這一系列文章均基於本身的理解和實踐,可能有不對的地方,歡迎你們指正。
其次,這是一個入門系列,涉及的知識也僅限於夠用,深刻的知識網上也有許許多多的博文供你們學習了。
最後,寫文章過程當中,會借鑑參考其餘人分享的文章,會在文章最後列出,感謝這些做者的分享。php

碼字不易,轉載請註明出處!android

教程代碼:【Github傳送門

目錄

1、Android音視頻硬解碼篇:
2、使用OpenGL渲染視頻畫面篇
  • 1,初步瞭解OpenGL ES
  • 2,使用OpenGL渲染視頻畫面
  • 3,OpenGL渲染多視屏,實現畫中畫
  • 4,深刻了解OpenGL之EGL
  • 5,OpenGL FBO數據緩衝區
  • 6,Android音視頻硬編碼:生成一個MP4
3、Android FFmpeg音視頻解碼篇
  • 1,FFmpeg so庫編譯
  • 2,Android 引入FFmpeg
  • 3,Android FFmpeg視頻解碼播放
  • 4,Android FFmpeg+OpenSL ES音頻解碼播放
  • 5,Android FFmpeg+OpenGL ES播放視頻
  • 6,Android FFmpeg簡單合成MP4:視屏解封與從新封裝
  • 7,Android FFmpeg視頻編碼

本文你能夠了解到

上一篇文章,主要講了Android MediaCodec實現音視頻硬解碼的流程,搭建了基礎解碼框架。本文將講解具體的音視頻渲染,包括MediaCodec初始化、Surface初始化,AudioTrack初始化、音視頻數據流分離提取等,以及很是重要的音視頻同步。git

在上一篇文章定義的解碼流程框架基類中,預留了幾個虛函數,留給子類初始化本身的東西,本篇,就來看看如何實現。github

1、音視頻數據流分離提取器

上篇文章,屢次提到音視頻數據分離提取器,在實現音視頻解碼器子類以前,先把這個實現了。緩存

封裝Android原生提取器

以前提過,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

  • 【1,初始化】

很簡單,兩句代碼:新建,而後設置音視頻文件路徑函數

mExtractor = MediaExtractor()
mExtractor?.setDataSource(path)
複製代碼
  • 【2.1/2.2,獲取音視頻多媒體格式】

音頻和視頻是同樣的:工具

1)遍歷視頻文件中全部的通道,通常是音頻和視頻兩個通道;

2) 而後獲取對應通道的編碼格式,判斷是否包含"video/"或者"audio/"開頭的編碼格式;

3)最後經過獲取的索引,返回對應的音視頻多媒體格式信息。

  • 【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()
複製代碼
  • 【4,釋放提取器】

客戶端退出解碼的時候,須要調用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()
    }
}
複製代碼

2、視頻播放

咱們先來定義一個視頻解碼器子類,繼承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

  1. SurfaceView應該是你們比較熟悉的View了,最常使用的就是用來作MediaPlayer的顯示。固然也能夠繪製圖片、動畫等。
  2. Surface應該不是很經常使用了,這裏爲了支持後續使用OpenGL來渲染視頻,因此預先作了支持。
  • 生成數據提取器
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
    }
}
複製代碼
初始化Surface

就是由於考慮到一個問題,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)
複製代碼

3、音頻播放

有了上面視頻播放器的基礎之後,音頻播放器也是分分鐘搞定的事了。

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()
    }
}
複製代碼

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

1. 初始化解碼器

音頻不須要surface,直接傳null

codec.configure(format, null , null, 0)
複製代碼
2. 獲取參數不同

音頻播放須要獲取採樣率,通道數,採樣位數等

3. 須要初始化一個音頻渲染器:AudioTrack

因爲解碼出來的數據是PCM數據,因此直接使用AudioTrack播放便可。在initRender() 中對其進行初始化。

  • 根據通道數量配置單聲道和雙聲道
  • 根據採樣率、通道數、採樣位數計算獲取最小緩衝區
AudioTrack.getMinBufferSize(mSampleRate, channel, mPCMEncodeBit)
複製代碼
  • 建立AudioTrack,並啓動
mAudioTrack = AudioTrack(
            AudioManager.STREAM_MUSIC,//播放類型:音樂
            mSampleRate, //採樣率
            channel, //通道
            mPCMEncodeBit, //採樣位數
            minBufferSize, //緩衝區大小
            AudioTrack.MODE_STREAM) //播放模式:數據流動態寫入,另外一種是一次性寫入
            
mAudioTrack!!.play()
複製代碼
4. 手動渲染音頻數據,實現播放

最後就是將解碼出來的數據寫入AudioTrack,實現播放。

有一點注意的點是,須要把解碼數據由ByteBuffer類型轉換爲ShortBuffer,這時Short數據類型的長度要減半。

4、調用並播放

以上,基本實現了音視頻的播放流程,如無心外,在頁面上調用以上音視頻解碼器,就能夠實現播放了。

簡單看下頁面和相關調用。

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()
    }
}
複製代碼

至此,基本上實現音視頻的解碼和播放。可是若是你真正把代碼跑起來的話,你會發現:視頻和音頻爲何不一樣步啊,視頻就像倍速播放同樣,一下就播完了,可是音頻卻很正常。

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

5、音視頻同步

同步信號來源

因爲視頻和音頻是兩個獨立的任務在運行,視頻和音頻的解碼速度也不同,解碼出來的數據也不必定立刻就能夠顯示出來。

在第一篇文章的時候有說過,解碼有兩個重要的時間參數:PTS和DTS,分別用於表示渲染的時間和解碼時間,這裏就須要用到PTS。

播放器中通常存在三個時間,音頻的時間,視頻的時間,還有另一個就是系統時間。這樣能夠用來實現同步的時間源就有三個:

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

一般狀況下,因爲人類對聲音比較敏感,而且視頻解碼的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,不會涉及到編碼和解碼,只涉及數據的解封和封裝,爲後面的【解封裝->解碼->編輯->編碼->封裝】全流程做準備。

相關文章
相關標籤/搜索