前言
前文講到Android音視頻開發(fā)框架中的上半段:音視頻的創(chuàng)建,編碼,保存,這個屬于音視頻資源生產(chǎn)端的過程。在消費(fèi)端,還需要經(jīng)歷讀取,解碼,播放這三個節(jié)點(diǎn)。
音視頻讀取
在前文中,我們可以打通從攝像頭+麥克風(fēng)-編碼數(shù)據(jù)-保存文件這個過程,假如一切順利,那么可以在磁盤中保存一個MP4文件。但是想要消費(fèi)這段影片,首先要做的就是提取文件里的編碼過的音頻和視頻信息。這個工作主要依賴于MediaExtractor類。
MediaExtractor的主要方法如下:
// 設(shè)置數(shù)據(jù)源
mediaExtractor.setDataSource()
// 獲取軌道數(shù)(音頻軌道,視頻軌道,字幕軌道等)
mediaExtractor.getTrackCount()
// 獲取該軌道的格式類型(是音頻還是視頻)
mediaExtractor.getTrackFormat()
// 選擇軌道(確定讀取哪個軌道的數(shù)據(jù))
mediaExtractor.selectTrack()
// 讀取采樣數(shù)據(jù)到數(shù)組
mediaExtractor.readSampleData()
// 進(jìn)入下一個采樣,readSampleData之后需要調(diào)用advance推動指針往前挪動
mediaExtractor.advance()
// 返回當(dāng)前軌道索引
mediaExtractor.getSampleTrackIndex()
// 返回當(dāng)前采樣的顯示時間
mediaExtractor.getSampleTime()
// seek到對應(yīng)時間
mediaExtractor.seekTo()
// 釋放資源
mediaExtractor.release()
我們可以把MediaExtrtactor看作是MediaMuxer的逆過程,后者是把音頻視頻封裝寫入文件,前者是讀取文件,解封裝獲取獨(dú)立的音頻和視頻。
音頻和視頻分別是獨(dú)立線程編解碼的,那么讀取自然在分在兩個線程中分別讀取互不干擾。而且由于操作的相似性,我們可以對它的操作進(jìn)行一定的封裝:
class MExtractor(filePath:String) {
companion object{
val EXTRACTOR_TAG = "extractor_tag"
}
private var audioTrackIndex = -1
private var videoTrackIndex = -1
private val mediaExtractor:MediaExtractor by lazy {
MediaExtractor()
}
init {
try {
mediaExtractor.setDataSource(filePath)
}catch (e:IOException){
e.printStackTrace()
Log.e(EXTRACTOR_TAG,"${e.message}")
}
}
// 選擇音頻軌道
fun selectAudioTrack(){
val index = getAudioTrack()
if (index == -1) return
mediaExtractor.selectTrack(index)
}
// 選擇視頻軌道
fun selectVideoTrack(){
val index = getVideoTrack()
if (index == -1) return
mediaExtractor.selectTrack(index)
}
// 讀?。▽?yīng)軌道的)數(shù)據(jù)
fun readSampleData(byteBuf: ByteBuffer, offset:Int):Pair<Int,Long>{
//讀取一塊數(shù)據(jù)
val readSize = mediaExtractor.readSampleData(byteBuf, offset)
// 獲取這塊數(shù)據(jù)對應(yīng)的時間錯
val sampleTimeValue = mediaExtractor.sampleTime
//指針往前移動
mediaExtractor.advance()
return Pair(readSize,sampleTimeValue)
}
...
...
fun getAudioTrack():Int{
if (audioTrackIndex != -1){
return audioTrackIndex
}
for (i in 0..mediaExtractor.trackCount) {
val format = mediaExtractor.getTrackFormat(i)
if (format.getString(MediaFormat.KEY_MIME)?.startsWith("audio/") == true){
Log.i(EXTRACTOR_TAG,"selected format: $format track: $i")
audioTrackIndex = i
return i
}
}
return -1;
}
fun getVideoTrack():Int{
if (mediaExtractor.trackCount == 0){
return -1
}
if (videoTrackIndex != -1){
return videoTrackIndex
}
for (i in 0..mediaExtractor.trackCount) {
val format = mediaExtractor.getTrackFormat(i)
Log.i(EXTRACTOR_TAG,"video index: $i format: $format")
if (format.getString(MediaFormat.KEY_MIME)?.startsWith("video/") == true){
Log.i(EXTRACTOR_TAG,"format: $format")
videoTrackIndex = i
return i
}
}
return -1
}
}
以上基本上就是MediaExtractor的全部了,他往往需要配合其他的組件使用。
音視頻解碼
有了MediaExtractor的幫助,我們已經(jīng)可以 從文件中獲取數(shù)據(jù)源,接著我們還是使用異步模式來開啟解碼過程
視頻
private val videoHandlerThread: HandlerThread = HandlerThread("video-thread").apply { start() }
private val videoHandler = Handler(videoHandlerThread.looper)
private val mediaExtractor: MExtractor by lazy {
MExtractor(fileData.filePath)
}
// 異步模式的回調(diào)
private val videoCallback = object : CodecCallback() {
override fun onInputBufferAvailableWrapper(codec: MediaCodec, index: Int) {
if (isSignalEOF || mediaExtractor.getSampleTrackIndex() == -1) {
return
}
pauseIfNeed()
val inputBuffer = codec.getInputBuffer(index) ?: return
inputBuffer.clear()
// 選擇視頻軌道
mediaExtractor.selectVideoTrack()
//讀取數(shù)據(jù)
// sampleTime 視頻的PTS
var (readSize, sampleTime) = mediaExtractor.readSampleData(inputBuffer, 0)
if (readSize < 0) {
inputBuffer.limit(0)
codec.queueInputBuffer(index, 0, 0, 0, 0)
isSignalEOF = true
} else {
codec.queueInputBuffer(index, 0, readSize, sampleTime, 0)
}
}
override fun onOutputBufferAvailableWrapper(
codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo
) {
if (isOutputEOF) {
return
}
isOutputEOF = (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0
trySleep(info.presentationTimeUs)
// index 是解碼后的數(shù)據(jù)緩存空間下標(biāo)
// 第二個參數(shù)表示是否渲染(如果提前設(shè)置了輸出端的Surface的話,填true)
codec.releaseOutputBuffer(index, true)
}
...
...
}
...
// configure
mediaExtractor.getVideoFormat()?.let {
val mime = it.getString(MediaFormat.KEY_MIME)
mime?.let {m->
videoDecoder = MediaCodec.createDecoderByType(m)
// 這個surface來自于播放器(SurfaceView或者TextureView)
videoDecoder?.configure(it, surface, null, 0)
videoDecoder?.setCallback(videoCallback, videoHandler)
}
}
// 開始解碼
videoDecoder?.start()
...
...
// release
videoDecoder?.stop()
videoDecoder?.release()
對于視頻的解碼過程,輸出端我們?nèi)匀豢梢允褂肧urface來簡化我們的輸出操作,MediaCodec提供了直接輸出數(shù)據(jù)到Surface的過程,因此我們把播放端的SurfaceView或者TextureView中的surface傳入進(jìn)來,那么數(shù)據(jù)就可以直接打通了。
音頻
音頻的解碼過程和視頻解碼大差不差
private val audioHandlerThread: HandlerThread = HandlerThread("audio-thread").apply { start() }
private val audioHandler = Handler(audioHandlerThread.looper)
private val mediaExtractor: MExtractor by lazy {
MExtractor(fileData.filePath)
}
// 解碼異步模式回調(diào)
private val audioCallback = object : CodecCallback() {
override fun onInputBufferAvailableWrapper(codec: MediaCodec, index: Int) {
if (isEOF || mediaExtractor.getSampleTrackIndex() == -1) {
return
}
pauseIfNeed()
val inputBuffer = codec.getInputBuffer(index) ?: return
inputBuffer.clear()
mediaExtractor.selectAudioTrack()
// 讀取采樣數(shù)據(jù)到buffer,獲取采樣時間,同時指針向前推進(jìn)
// sampleTimeValue就是當(dāng)前數(shù)據(jù)的PTS,這個直接從mediaExtractor中獲取,從0開始
val (readSize, sampleTimeValue) = mediaExtractor.readSampleData(inputBuffer, 0)
if (readSize < 0) {
codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
isEOF = true
} else {
codec.queueInputBuffer(index, 0, readSize, sampleTimeValue, 0)
}
}
override fun onOutputBufferAvailableWrapper(
codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo
) {
val outputBuffer = codec.getOutputBuffer(index)
outputBuffer?.let {
it.position(info.offset)
it.limit(info.offset + info.size)
...
// 向音頻播放設(shè)備寫入數(shù)據(jù)
...
}
trySleep(info.presentationTimeUs)
codec.releaseOutputBuffer(index, false) // 重要
}
...
...
}
// configure
mediaExtractor.getAudioFormat()?.let {
val mime = it.getString(MediaFormat.KEY_MIME) ?: ""
audioDecoder = MediaCodec.createDecoderByType(mime)
audioDecoder?.configure(it, null, null, 0)
audioDecoder?.setCallback(audioCallback, audioHandler)
Log.i(TAG, "audio inputbuffer mime: $mime")
}
// start
audioDecoder?.start()
...
...
// release
audioDecoder?.stop()
audioDecoder?.release()
音視頻播放
音視頻播放其實(shí)是完全不同的路徑,視頻播放依賴TextureView等的view展示,而音頻播放則是依賴音頻設(shè)備。
對于視頻而言,我們需要在UI中插入TextureView(SurfaceView也一樣),然后在TextureView中設(shè)置SurfaceTextureListener,等待SUrface的創(chuàng)建成功,接著把SUrface傳入解碼器
dataBinding.textureview.surfaceTextureListener = object :SurfaceTextureListener{
override fun onSurfaceTextureAvailable(
surfaceTexture: SurfaceTexture,
width: Int,
height: Int
) {
Log.i(TAG,"onSurfaceTextureAvailable $width $height $surfaceTexture")
val surface = Surface(surfaceTexture)
startDecodeVideo(surface) // 傳入解碼模塊
startDecodeAudio() // 一般也可以在此時觸發(fā)音頻的解碼
}
override fun onSurfaceTextureSizeChanged(
surface: SurfaceTexture,
width: Int,
height: Int
) {
Log.i(TAG,"onSurfaceTextureSizeChanged $width $height $surface")
}
override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
curSurface?.release()
Log.i(TAG,"onSurfaceTextureDestroyed $surfaceTexture")
return true
}
override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) {
Log.i(TAG,"onSurfaceTextureUpdated $surfaceTexture")
}
}
這樣,解碼的視頻幀就可以顯示在textureView上了。
但是音頻的播放過程則完全在后臺進(jìn)行
// 創(chuàng)建音頻播放設(shè)備
mediaExtractor.getAudioFormat()?.let {
// 初始化配置
val audioAttr = AudioAttributes.Builder()
.setContentType(CONTENT_TYPE_MOVIE)
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
.setUsage(USAGE_MEDIA)
.build()
val sampleRate = it.getInteger(MediaFormat.KEY_SAMPLE_RATE)
var channelMask = if (it.containsKey(MediaFormat.KEY_CHANNEL_MASK)) {
it.getInteger(MediaFormat.KEY_CHANNEL_MASK)
} else {
null
}
var channelCount = 1
if (it.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) {
channelCount = it.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
}
val channelConfig =
if (channelCount == 1) AudioFormat.CHANNEL_OUT_MONO else AudioFormat.CHANNEL_OUT_STEREO
if (channelMask == null) {
channelMask = channelConfig
}
val formatInt = if (it.containsKey(MediaFormat.KEY_PCM_ENCODING)) {
it.getInteger(MediaFormat.KEY_PCM_ENCODING)
} else {
AudioFormat.ENCODING_PCM_16BIT
}
val audioFormat = AudioFormat.Builder()
.setChannelMask(channelMask)
.setEncoding(formatInt)
.setSampleRate(sampleRate)
.build()
bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, formatInt)
// 創(chuàng)建音頻播放設(shè)備
audioTrack = AudioTrack(
audioAttr,
audioFormat,
bufferSize,
AudioTrack.MODE_STREAM,
audioManager.generateAudioSessionId()
)
}
//開始播放,和audioDecode.start同時調(diào)用即可
audioTrack?.play()
// 在合適的時機(jī)寫入音頻數(shù)據(jù)(一般就放在解碼完成輸出之后寫入即可)
audioTrack?.write(...)
// 釋放資源
audioTrack?.stop()
audioTrack?.release()
以上就是音頻播放設(shè)備的使用方式。
你以為這樣就結(jié)束了么?天真。
如果按照正常操作視頻的解碼速度會很快,你會發(fā)現(xiàn)視頻像走馬燈一樣播放完了,音頻還在播放,因此我們需要對音視頻進(jìn)行同步。
音視頻同步
由于每一幀音頻或者視頻數(shù)據(jù)都有PTS,也就是說已經(jīng)設(shè)定好了這一幀數(shù)據(jù)應(yīng)該播放的時間點(diǎn),而音視頻同步要做的就是,當(dāng)解碼出來的幀的時間戳還沒到播放的時間節(jié)點(diǎn)時,我們需要等待,一直等到播放的時間節(jié)點(diǎn)到來。
音視頻同步的方法不止一種,我選擇大家比較容易理解的一種來描述:選擇一條獨(dú)立的時間軸,每次音頻或者視頻解碼出來之后的時間戳與獨(dú)立時間軸的當(dāng)前時間戳進(jìn)行比較,如果大于當(dāng)前時間戳,表示該幀數(shù)據(jù)還沒有到展示的時候,需要等待,否則就直接展示。
如何實(shí)現(xiàn)呢?比較簡單,在開始解碼時的時間設(shè)為獨(dú)立時間軸的起點(diǎn)startPresentationTimeUs,后續(xù)的解碼回調(diào)中和這個時間起點(diǎn)進(jìn)行比較即可
// 開始解碼時調(diào)用,并記錄一下時間起點(diǎn)
@CallSuper
override fun start() {
if (startPresentationTimeUs == -1L){
startPresentationTimeUs = getMicroSecondTime()
}
}
protected fun getMicroSecondTime():Long{
return System.nanoTime()/1000L
}
// 每次準(zhǔn)備播放音頻或者視頻時調(diào)用一次,
protected fun trySleep(sampleTime:Long){
val standAlonePassTime = getMicroSecondTime()-startPresentationTimeUs
if (sampleTime>standAlonePassTime){
try {
val sleepTime = (sampleTime-standAlonePassTime)/1000
Log.i(TAG,"sleep time $sampleTime ${sleepTime}ms $this")
// 如果時間不夠,就休眠
Thread.sleep(sleepTime)
}catch (e:InterruptedException){
e.printStackTrace()
}
}
}
這就實(shí)現(xiàn)了一個簡單的音視頻同步的邏輯了,我相信理解起來沒有太大的難度。當(dāng)然,如果系統(tǒng)有支持的方法我們自然不必親自實(shí)現(xiàn)同步邏輯,在Android體系中,有MediaSync可以幫助我們實(shí)現(xiàn)音視頻播放同步的邏輯,使用起來不算太復(fù)雜,不過它也同樣深度嵌套到音視頻的解碼過程中去了,這個留給大家去熟悉吧。
除了音視頻同步這個重要內(nèi)容外,其實(shí)還有播放/暫停,這個過程也會影響到音視頻同步的邏輯,因?yàn)椴シ艜和r,每幀數(shù)據(jù)的顯示時間戳PTS不會變,但是我們建立的獨(dú)立時間軸的時間會繼續(xù)流逝,等恢復(fù)之后,在比較時間戳就完全錯誤了,因此我們需要在暫停和恢復(fù)時記錄一下暫停的時長,然后在比較時減去這段時間,又或者直接把獨(dú)立時間軸的起點(diǎn)時間往后挪動暫停時長即可。
此外,播放過程中獲取預(yù)覽圖,播放進(jìn)度條等內(nèi)容也是基本內(nèi)容,我認(rèn)為它們并沒有比音視頻同步更難以理解,因此不一一說明了。
Android當(dāng)然有支持較好的播放器可以同時播放音頻和視頻,而且還能自動幫助我們解碼數(shù)據(jù),這些我相信大家是更了解的。
總結(jié)
到此,Android的音視頻開發(fā)框架基本描述完整了,它涵蓋了音視頻的創(chuàng)建,編碼,保存,提取,解碼,播放的全過程,當(dāng)然每個部分只是囫圇吞棗的介紹,代碼也不是完整,其實(shí)這里里面很多內(nèi)容都可以單列一章來講,細(xì)節(jié)頗多,不過我認(rèn)為作為一個簡介性質(zhì)的文章深度是夠了的,主要側(cè)重于介紹概念和使用方法。后續(xù)深入研究還靠自己,本身的水平也有限。