在上一篇文章中,提到了MediaCodec的解码基础内容,实现音视频硬解码的过程。本文会对MediaCodec的初始化、Surface显示层的初始化、AudioTrack的初始化进行详细介绍,同时包括音视频的渲染和数据流分离与提取,更重要的是如何做到音视频同步。
在上一篇介绍解码流程的基础类BaseDecoder里面,有几个抽象方法是未说明的,需要子类自行实现,这篇文章就通过子类的实现来做下介绍。
一、音视频数据流分离提取器
既然称为分离器,其实可以有个简单的概念,就是音视频的数据其实是可以分为音频轨道和视频轨道的,从轨道中读取数据,比如轨道数、轨道格式、轨道时长等,就是分离器MediaExtractor要做的事情。在这里要明确分离器工作的环节,确切的说是属于解码前获取数据使用的,不属于解码,当然也不属于编码。
封装原生提取器MediaExtractor
在这里封装一个支持音视频提取的工具类MMExtrator
js
class MMExtractor(path: String?) {
/**音视频分离器*/
private var mExtractor: MediaExtractor? = null
/**音频通道索引*/
private var mAudioTrack = -1
/**视频通道索引*/
private var mVideoTrack = -1
/**当前帧时间戳*/
private var mCurSampleTime: Long = 0
/**当前帧标志*/
private var mCurSampleFlag: Int = 0
/**开始解码时间点*/
private var mStartPos: Long = 0
init {
mExtractor = MediaExtractor()
if (path != null) {
mExtractor?.setDataSource(path)
}
}
/**
* 获取视频格式参数
*/
fun getVideoFormat(): MediaFormat? {
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? {
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 {
byteBuffer.clear()
selectSourceTrack()
var readSampleCount = mExtractor!!.readSampleData(byteBuffer, 0)
if (readSampleCount < 0) {
return -1
}
//记录当前帧的时间戳
mCurSampleTime = mExtractor!!.sampleTime
mCurSampleFlag = mExtractor!!.sampleFlags
//进入下一帧
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() {
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
}
fun getSampleFlag(): Int {
return mCurSampleFlag
}
}
这里主要有5个部分,做下讲解
- 【1、初始化提取器】
继续沿用原生提取器,进行新建和设置音视频文件路径
js
mExtractor = MediaExtractor()
if (path != null) {
mExtractor?.setDataSource(path)
}
【2、音视频多媒体格式】
这个部分的处理流程一致
(1)通过提取器的mExtractor!!.trackCount方法获取通道数,遍历所有通道,一般也就只有音频和视频两个通道; (2)获取每一个通道的编码格式,这个根据前缀是否含有video/或者audio/来判断 (3)通过获取到的索引号,返回对应的音视频多媒体格式信息
【3、提取当前轨道数据】 看一下如何提取视频数据: (1)方法readBuffer(byteBuffer: ByteBuffer)中的参数就是解码器传进来的,用于存放待解码数据的缓冲区。 (2)方法readBuffer(byteBuffer: ByteBuffer)里面含有selectSourceTrack()方法,该方法会判断上面获取到的索引号,用于在视频和音频通道中只选择一个,调用mExtractor!!.selectTrack(mVideoTrack)或者mExtractor!!.selectTrack(mAudioTrack)将通道切换正确。 (3)然后读取当前轨道数据
js
var readSampleCount = mExtractor!!.readSampleData(byteBuffer, 0)
if (readSampleCount < 0) {
return -1
}
这时候,读取到的数据是音频或者视频轨道的数据流的大小,返回小于0的值,表示数据读取完毕 (4)进入下一帧 先记录当前帧的时间戳,然后调用advance进入下一帧,这时读取指针将自动移动到下一帧开头。
js
//记录当前帧的时间戳
mCurSampleTime = mExtractor!!.sampleTime
mCurSampleFlag = mExtractor!!.sampleFlags
//进入下一帧
mExtractor!!.advance()
【4、释放提取器】 这里提供了一个stop方法去停止数据的读取,在应用退出解码的时候,需要调用这个stop方法来释放提取器相关资源。
js
fun stop() {
mExtractor?.release()
mExtractor = null
}
以下这些内容是在其他博客中摘录的
说明:seek(pos: Long)方法,主要用于跳播,快速将数据定位到指定的播放位置,但是,由于视频中,除了I帧以外,PB帧都需要依赖其他的帧进行解码,所以,通常只能seek到I帧,但是I帧通常和指定的播放位置有一定误差,因此需要指定seek靠近哪个关键帧,有以下三种类型:
SEEK_TO_PREVIOUS_SYNC:跳播位置的上一个关键帧
SEEK_TO_NEXT_SYNC:跳播位置的下一个关键帧
SEEK_TO_CLOSEST_SYNC:距离跳播位置的最近的关键帧
到这里你就可以明白,为什么我们平时在看视频时,拖动进度条释放以后,视频通常会在你释放的位置往前一点
分别实现音频、视频提取器
上面大概讲了提取器能够做的事情,主要还是数据的提取,那么就看音视频的提取器是具体如何取数据的。在这里先进行接口的实现,也就是上一篇文章中剃刀的提取器模型:
js
interface IExtractor {
fun getFormat(): MediaFormat?
/**
* 读取音视频数据
*/
fun readBuffer(byteBuffer: ByteBuffer): Int
/**
* 获取当前帧时间
*/
fun getCurrentTimestamp(): Long
fun getSampleFlag(): Int
/**
* Seek到指定位置,并返回实际帧的时间戳
*/
fun seek(pos: Long): Long
fun setStartPos(pos: Long)
/**
* 停止读取数据
*/
fun stop()
}
视频提取器的实现如下:
js
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 getSampleFlag(): Int {
return mMediaExtractor.getSampleFlag()
}
override fun seek(pos: Long): Long {
return mMediaExtractor.seek(pos)
}
override fun setStartPos(pos: Long) {
return mMediaExtractor.setStartPos(pos)
}
override fun stop() {
mMediaExtractor.stop()
}
}
音频提取器的实现如下:
js
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 getSampleFlag(): Int {
return mMediaExtractor.getSampleFlag()
}
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
js
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 if (mSurfaceView?.holder?.surface != null) {
mSurface = mSurfaceView?.holder?.surface
configCodec(codec, format)
} 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(outputBuffer: ByteBuffer,
bufferInfo: MediaCodec.BufferInfo) {
}
override fun doneDecode() {
}
override fun pause() {
}
}
上一篇文章中,对解码流程已经进行了定义,子类只需要实现抽象函数实现对应的方法即可。
- 检查参数
视频解码支持SurfaceView、surface来渲染页面,归根到底,还是把Surface传递给MediaCodec
1、到这里的时候,突然明白了为什么在Android开发的时候一直说拿surfaceView去展示video、会制动画等
2、Surface没有那么常用,这里为了后续讲明使用OpenGL来渲染视频,所以预先也做了支持
在解码过程中,生成对应的视频数据提取器
js
override fun initExtractor(path: String): IExtractor {
return VideoExtractor(path)
}
- 配置解码器
解码器的配置如下:
js
codec.configure(format, mSurface , null, 0)
- 初始化Surface
由于surfaceView的创建耗时,并非创建后可以立即使用,所以需要通过回调callBack来监听状态,在此之前,需要使解码进入等待状态,如下面的waitDecode方法,将线程挂起。
kotlin
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初始化完毕后,再配置MediaCodec。
kotlin
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)
}
})
如果使用OpenGL直接传递surface进来,直接配置MediaCodec即可。
- 渲染
视频的渲染并不需要客户端手动去渲染,只需提供绘制的surface即可,渲染之后再去调用releaseOutputBuffer进行缓冲区释放,将第2个参数设置为true即可。
scss
//【解码步骤:4. 渲染】
if (mSyncRender) {// 如果只是用于编码合成新视频,无需渲染
render(mOutputBuffers!![index], mBufferInfo)
}
//将解码数据传递出去
val frame = Frame()
frame.buffer = mOutputBuffers!![index]
frame.setBufferInfo(mBufferInfo)
mStateListener?.decodeOneFrame(this, frame)
//【解码步骤:5. 释放输出缓冲】
mCodec!!.releaseOutputBuffer(index, true)
三、音频解码与播放
再来看下音频播放器如何创建。
kotlin
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
csharp
codec.configure(format, null , null, 0)
2. 获取参数不一样
音频播放需要获取采样率,通道数,采样位数等
3. 需要初始化一个音频渲染器:AudioTrack
解码出来的数据是PCM格式的数据,所以直接使用AudioTrack播放即可。在initRender() 中对其进行初始化。
- 根据通道数量配置单声道和双声道
- 根据采样率、通道数、采样位数计算获取最小缓冲区
scss
AudioTrack.getMinBufferSize(mSampleRate, channel, mPCMEncodeBit)
- 创建AudioTrack,并启动
arduino
mAudioTrack = AudioTrack(
AudioManager.STREAM_MUSIC,//播放类型:音乐
mSampleRate, //采样率
channel, //通道
mPCMEncodeBit, //采样位数
minBufferSize, //缓冲区大小
AudioTrack.MODE_STREAM) //播放模式:数据流动态写入,另一种是一次性写入
mAudioTrack!!.play()
4. 手动渲染音频数据,实现播放
进入Render阶段,将解码出来的数据写入AudioTrack,实现播放。
js
mAudioTrack!!.write(mAudioOutTempBuf!!, 0, bufferInfo.size / 2)
由于第一个参数类型是short,所以需要把解码数据由ByteBuffer类型转换为ShortBuffer,那么这时Short数据类型的长度要减半。
四、实现播放
音视频数据的提取、解码、释放等流程都做了介绍,下面就基于Android平台看下能不能正常进行音视频的播放。
activity_media_codec.xml
js
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
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"/>
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/sfv"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="12dp"
android:text="重打包"
android:onClick="clickRepack"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
MediaCodecActivity
js
package com.example.ffmpeglearn1.media
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Environment
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.cxp.learningvideo.media.decoder.AudioDecoder
import com.cxp.learningvideo.media.decoder.VideoDecoder
import com.cxp.learningvideo.media.muxer.MP4Repack
import com.example.ffmpeglearn1.R
import java.util.concurrent.Executors
open class MediaCodecActivity: AppCompatActivity() {
private val TAG = "MediaCodecActivity"
private val REQUEST_CODE: Int = 100
val path = Environment.getExternalStorageDirectory().absolutePath + "/one_piece.mp4"
lateinit var videoDecoder: VideoDecoder
lateinit var audioDecoder: AudioDecoder
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_media_codec)
// 检查是否已经授权
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// 请求授权
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
REQUEST_CODE);
} else {
// 权限已经授权,可以读取外部存储器中的文件
initPlayer()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 授权成功,可以读取外部存储器中的文件
initPlayer()
} else {
// 授权失败,无法读取外部存储器中的文件
}
}
}
private fun initPlayer() {
Log.w(TAG, "path = $path")
// val path = "file:///android_asset/byteflow/one_piece.mp4";
//创建线程池
val threadPool = Executors.newFixedThreadPool(2)
//创建视频解码器
videoDecoder = VideoDecoder(path, findViewById(R.id.sfv), null)
threadPool.execute(videoDecoder)
//创建音频解码器
audioDecoder = AudioDecoder(path)
threadPool.execute(audioDecoder)
//开启播放
videoDecoder.goOn()
audioDecoder.goOn()
}
fun clickRepack(view: View) {
repack()
}
private fun repack() {
val repack = MP4Repack(path)
repack.start()
}
override fun onDestroy() {
videoDecoder.stop()
audioDecoder.stop()
super.onDestroy()
}
}
此时可以看到视频播放界面了,并且带有声音
此时仍然有问题,就是音频和视频并不是同步的,视频可以很快就播放完毕,但是音频却在正常播放
五、音视频同步
同步信号来源
不学不知道,在不去了解音视频同步原理的时候,不知道怎么做,下面的内容也基本是参考他人博客内容,这里基本也是抄一遍,权当记忆。
由于视频和音频是两个独立的任务在运行,视频和音频的解码速度也不一样,解码出来的数据也不一定马上就可以显示出来。
在第一篇文章的时候有说过,解码有两个重要的时间参数:PTS和DTS,分别用于表示渲染的时间和解码时间,这里就需要用到PTS。
播放器中一般存在三个时间,音频的时间,视频的时间,还有另外一个就是系统时间。这样可以用来实现同步的时间源就有三个:
- 视频时间戳
- 音频时间戳
- 外部时间戳
- 视频PTS
通常情况下,由于人类对声音比较敏感,并且视频解码的PTS通常不是连续,而音频的PTS是比较连续的,如果以视频为同步信号源的话,基本上声音都会出现异常,而画面的播放也会像倍速播放一样。
- 音频PTS
那么剩下的两个选择中,以音频的PTS作为同步源,让画面适配音频是比较不错的一种选择。
但是这里不采用,而是使用系统时间作为同步信号源。因为如果以音频PTS作为同步源的话,需要比较复杂的同步机制,音频和视频两者之间也有比较多的耦合。
- 系统时间
而系统时间作为统一信号源则非常适合,音视频彼此独立互不干扰,同时又可以保证基本一致。
实现音视频同步
要实现音视频之间的同步,这里需要考虑的有两个点:
1. 比对
在解码数据出来以后,检查PTS时间戳和当前系统流过的时间差距,快则延时,慢则直接播放
2. 矫正
在进入暂停或解码结束,重新恢复播放时,需要将系统流过的时间做一下矫正,将暂停的时间减去,恢复真正的流逝时间,即已播放时间。
重新看回BaseDecoder解码流程:
scss
kotlin
复制代码
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. 将解码好的数据从缓冲区拉取出来】,这时如果数据是有效的,那么进入比对。
kotlin
kotlin
复制代码
// ---------【音视频同步】-------------
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。
这里采用第二种
scss
if (mState != DecodeState.START &&
mState != DecodeState.DECODING &&
mState != DecodeState.SEEKING) {
Log.i(TAG, "进入等待:$mState")
waitDecode()
// ---------【同步时间矫正】-------------
//恢复同步的起始时间,即去除等待流失的时间
mStartTimeForSync = System.currentTimeMillis() - getCurTimeStamp()
}
至此,从解码到播放,再到音视频同步,一个简单的播放器就做完了。
说在后面
按照大佬的文档整个过程理一遍,每个代码类都看一下,收获很大,尤其是当播放器完成,可以进行播放的时候,很有成就感,建议大家都去仔细看一遍。下面继续跟着大佬的步伐,学习Mp4文件的封装!