本文主要介绍Android使用MediaCodec、MediaExtrator、AudioTrack和SurfaceView实现本地视频播放。更多介绍这5个类如何串在一起使用,而 忽略掉一些细节。
MediaExtrator主要是用来从视频资源中提取画面和音频交给MediaCodec进行解码,将解码后画面数据交给SurfaceView进行展示,解码后的音频交给AudioTrack进行播放,需要通过时间戳进行音视频同步。
一、MediaExtrator
MediaExtrator可以从音视频资源中提取出视频和音频数据,这种数据通常是经过编码的。
extractor = MediaExtractor()
extractor.setDataSource(filePath)
for (track in 0 until extractor.trackCount) {
val mediaFormat = extractor.getTrackFormat(track)
val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
if (mimeType?.startsWith("audio/") == true) {
extractor.selectTrack(track)
return
}
}
上面代码先是创建MediaExtractor实例extractor,然后通过setDataSource函数给extractor设置资源路径,该函数有多个重载函数,方便我们以不同方式设置资源,例如uri,asserts。
我们通过遍历MediaExtrator所有路资源,寻找合适的媒体类型mineType,并通过selectTrack函数选择该路数据。例如上面就是寻找音频的方式。如果是匹配视频类型,将"audio/"修改成"video/"即可。另外,一个MediaExtrator实例只能分解一路资源,也就是播放本地视频,我们需要创建两个MediaExtrator实例,一个提取视频,一个提取音频。
二、MediaCodec
通过MediaExtrator,我们可以获取音频和视频两路资源的数据和信息MediaFormat,mediaFormat包含音频和视频相关信息,例如采样率、比特率、分辨率等等。
for (track in 0 until extractor.trackCount) {
val mediaFormat = extractor.getTrackFormat(track)
val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
if (mimeType?.startsWith("audio/") == true) {
extractor.selectTrack(track)
//创建MediaCodec实例,解码音频
audioCodec = MediaCodec.createDecoderByType(mimeType)
audioCodec.configure(mediaFormat, null, null, 0)
return
}
}
通过匹配"audio/"开头的媒体类型来创建音频解码器MediaoCodec实例audioCodec,用来解码音频。并调用MediaoCodec的configure函数来配置MediaCodec。这里configure函数第一个参数是MediaFornmat,这里传递从资源解析到的mediaFormat即可,第二个参数为Surface类型,因为这里是音频解码,所以不需要Surface实例,如果视频解码,则需要Surface,例如SurfaceView的Surface实例。
for (track in 0 until extractor.trackCount) {
val mediaFormat = extractor.getTrackFormat(track)
val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
if (mimeType?.startsWith("video/") == true) {
extractor.selectTrack(track)
videoCodec = MediaCodec.createDecoderByType(mimeType)
videoCodec.configure(mediaFormat, surface, null, 0)
return
}
}
三、AudioTrack
AudioTrack只能播放pcm音频裸数据。提供Builder模式和构造器来构建AudioTrack实例,下面通过其构造函数来创建AudioTrack实例。
AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat,int bufferSizeInBytes, int mode)
其中参数:
-
streamType:表示音频流的类型。
- AudioManager.STREAM_VOICE_CALL
- AudioManager.STREAM_SYSTEM
- AudioManager.STREAM_RING
- AudioManager.STREAM_MUSIC
- AudioManager.STREAM_ALARM
- AudioManager.STREAM_NOTIFICATION
-
sampleRateInHz 采样率,一般而言,采样率和设备适配最合适,通常为44.1kHz或48kHz。
-
channelConfig 声道配置,常用单声道
AudioFormat.CHANNEL_OUT_MONO,立体声AudioFormat.CHANNEL_OUT_STEREO。类似的环绕、5.1、7.1声道都是支持配置的。 -
audioFormat 音频信息
-
bufferSizeInBytes缓冲区大小,一般通过AudioTrack.getMinBufferSize函数获得。 -
mode数据模式,表示设置给AudioTrack的数据类型,有MODE_STATIC和MODE_STREAM,前者表示一次性把数据写给AudioTrack,后者表示以流的形式一段一段写给AudioTrack。
因为播放本地视频,相关信息我们可以通过MediaExtractor进行提取。
for (track in 0 until extractor.trackCount) {
val mediaFormat = extractor.getTrackFormat(track)
val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
if (mimeType?.startsWith("audio/") == true) {
extractor.selectTrack(track)
//创建MediaCodec实例,解码音频
audioCodec = MediaCodec.createDecoderByType(mimeType)
audioCodec.configure(mediaFormat, null, null, 0)
val sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
//获取声道数
val channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
//获取比特率
val bitRate = try {
//没有该参数时,会抛异常。
mediaFormat.getInteger(MediaFormat.KEY_PCM_ENCODING)
} catch (e: Exception) {
e.printStackTrace()
//设置为16bit
AudioFormat.ENCODING_PCM_16BIT
}
//获取声道配置
val channel = if (channelCount == 1) AudioFormat.CHANNEL_OUT_MONO else AudioFormat.CHANNEL_OUT_STEREO
//获取AudioTrack支持最小缓冲区大小
miniBufferSize = AudioTrack.getMinBufferSize(
sampleRate, channel, bitRate
)
//创建AudioTrack
audioTrack = AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channel, bitRate, miniBufferSize, AudioTrack.MODE_STREAM)
return
}
}
四、SurfaceView
AudioTrack用来播放视频资源的音频,而SurfaceView用来展示视频资源的画面,为MediaCodec提供Surface,用于渲染画面。
在布局文件声明:
<FrameLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/surface"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
设置SurfaceHolder.Callback回调,并在surfaceCreated开始整个流程。
binding.surface.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
Log.d(TAG,"surfaceCreated")
surfaceHolder = holder
//设置视频资源给MediaExtractor,SurfaceView的Surface给MediaCodec
val path="/sdcard/ucspace/video/oceans.mp4"
extractor.setDataSource(path, holder.surface)
extractor.start()
//音频解码
audioExtractor.setDataSource(path)
audioExtractor.start()
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
surfaceHolder = holder
Log.d(TAG,"surfaceChanged")
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
Log.d(TAG,"surfaceDestroyed")
}
})
五、数据解析
现在视频资源有了,音视和视频提取器MediaExtrator、编解码器MediaCodec、音频播放器AudioTrack、画面展示Surface也有了。就差最后一步:数据流动。
fun start() {
//如果onSurfaceCreated函数回调调用,不起新线程,会屏幕空白
GlobalScope.launch(Dispatchers.IO) {
var isEOS = false
videoCodec.start() //视频开始解码
val info = BufferInfo()
while (!isCodec) {
if (!isEOS) {
//获取MediaCodec输入缓冲区索引
val index = videoCodec.dequeueInputBuffer(10000)
if (index > 0) {
//获取MediaCodec的输入缓冲区
val buffer = videoCodec.getInputBuffer(index)
//将MediaExtractor提取的数据写入MeidaCodec的输入缓冲区
val size = extractor.readSampleData(buffer!!, 0)
//将输入缓冲区交给MediaCodec队列,等待解码
if (size < 0) {
videoCodec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
isEOS = true
} else {
videoCodec.queueInputBuffer(index, 0, size, extractor.sampleTime, 0)
extractor.advance()
}
}
}
//获取MediaCodec解码后的输出缓冲区索引,因为这里数据是直接通过Surface展示,没有做特别的处理
val outIndex = videoCodec.dequeueOutputBuffer(info, 10000)
if (outIndex >= 0) {
//将输出缓冲区交还MediaCodec进行重复使用
videoCodec.releaseOutputBuffer(outIndex, true)
}
}
release()
}
}
fun release() {
videoCodec.stop()
videoCodec.release()
extractor.release()
}
音频与视频解码的主要区别就是MediaCodec解码后的数据如何处理。
val outIndex = audioCodec.dequeueOutputBuffer(info, 10000)
if (outIndex >= 0) {
//获取缓冲区
val outData = audioCodec.outputBuffers[outIndex]
outData.position(0)
//将缓冲区数据写入buffer缓存
outData.asShortBuffer().get(buffer, 0, info.size / 2)
//将缓冲区数据写入AudioTrack播放
audioTrack.write(buffer, 0, info.size / 2)
sleepRender(info, startMs)
audioCodec.releaseOutputBuffer(outIndex, true)
}
数据流动把上面四大类串在了一起,实现了本地视频的播放。但在实际播放的过程,会发现视频画面播放很快,于是要控制下速度。
六、音视频同步
音频和视频数据在播放的过程,应该保持同步,才不会被察觉到异常。这个同步动作需要靠每帧音频和视频数据在生产的时候都会打上时间戳PTS。然后根据两者时间戳差来控制在一定的范围,以确保正确同步。按照RFC-1359标准,音频与视频的时间戳在-100ms到25ms之间,我们是无法察觉到音频和画面有异常,也就是音频和画面是同步的。
音视频同步有三种方式:
- 视频同步到音频
- 音频同步到视频
- 音频和视频同步到外部时间钟。
比较常见的就是视频同步到音频。因为音频通常是流式的,按照规律的均速播放,才能更加平滑。而视频播放时一帧帧图像,其调整相对音频来说会更简单。
由于音频和视频的解码在不同的协程进行,AudioTrack正常读取MediaCodec解码后的音频数据播放,而画面播放时间戳即可同步到音频即可,这里采用开始的时间戳进行比对。
fun start() {
GlobalScope.launch(Dispatchers.IO) {
var isEOS = false
videoCodec.start()
//开始的时候增加时间戳
val startMs = System.currentTimeMillis()
val info = BufferInfo()
while (!isCodec) {
if (!isEOS) {
......
val outIndex = videoCodec.dequeueOutputBuffer(info, 10000)
//进行时间戳对齐
sleepRender(info, startMs)
if (outIndex >= 0) {
videoCodec.releaseOutputBuffer(outIndex, true)
}
}
release()
}
}
//时间戳对齐函数
private fun sleepRender(info: BufferInfo, startMs: Long) {
//当前输出缓冲区帧数据时间戳与流逝时间戳的差
val timeDifference = info.presentationTimeUs / 1000 - (System.currentTimeMillis() - startMs)
if (timeDifference > 0) {
try {
Thread.sleep(timeDifference)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
}
七、总结
本文主要学习Android如何Medi aExtrator提取视频中画面在SurfaceView中进行展示,音频通过AudioTrack进行播放。同时可以接触音视频领域的一些知识点,如音频的采样率、码率、PTS、编解码等内容,作为学习音视频领域的指引。
所得:刚开始,对Android播放本地视频能力已经掌握,但相关知识点不够清晰,流程不够了解。通过整理本文,查阅到很多优秀的文章,也帮自己理清楚一些音视频概念和流程,本文很多知识点也来源其他文章。可能还存在很多误区或错点,如有指导,乃为贵人。