安卓通过MediaExtrator和MediaCodec播放本地视频
作者:访客发布时间:2023-12-15分类:程序开发学习浏览:108
本文主要介绍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
实例,一个提取视频,一个提取音频.
二、媒体编解码器
通过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
只能播放pcm
音频裸数据.提供Builder
模式和构造器来构建AudioTrack
实例,下面通过其构造函数来创建AudioTrack
实例.
AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat,int bufferSizeInBytes, int mode)
其中参数:
Stream类型:表示音频流的类型。
- AudioManager_STREAM_VOICE_CALL
- 音频管理器.STREAM_SYSTEM
- 音频管理器.STREAM_RING
- AudioManager.STREAM_MUSIC
- AudioManager.STREAM_ALARM
- 音频管理器.STREAM_NOTIFICATION
采样率采样率,一般而言,采样率和设备适配最合适,通常为44.1 kHz或48 kHz。
声道配置,常用单声道配置通道
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
}
}
四、曲面查看
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()
}
音频与视频解码的主要区别就是媒体编解码器解码后的数据如何处理。
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()
}
}
}
七、总结
本文主要学习安卓如何MediaExtrator提取视频中画面在Surface查看中进行展示,音频通过音频曲目进行播放。同时可以接触音视频领域的一些知识点,如音频的采样率、码率、PTS、编解码等内容,作为学习音视频领域的指引.
所得:刚开始,对安卓播放本地视频能力已经掌握,但相关知识点不够清晰,流程不够了解。通过整理本文,查阅到很多优秀的文章,也帮自己理清楚一些音视频概念和流程,本文很多知识点也来源其他文章.可能还存在很多误区或错点,如有指导,乃为贵人.
源码地址
播放器技术分享(3):音画同步
安卓音视频系列(五):使用MediaCodec播放视频文件
相关推荐
- 如何在视频嵌入周围添加IFRAME边框
- 如何从YouTube视频自动创建wordpress帖子
- 如何在wordPress中添加共享按钮作为Youtube视频的覆盖
- 9个有用的Youtube小贴士,用视频给你的wordpress网站增添情趣
- 如何在WordPress中添加特色视频缩略图
- WordPress的9个最佳YouTube视频集插件
- 如何使用WordPress在线销售视频
- MediaCodec(2)-音视频同步进行播放
- 在 Android 上使用 MediaExtractor 和 MediaMuxer 提取视频_提取音频_转封装_添加音频等操作
- 如何修复WordPress中的500内部服务器错误
- 程序开发学习排行
-
- 1鸿蒙HarmonyOS:Web组件网页白屏检测
- 2HTTPS协议是安全传输,为啥还要再加密?
- 3HarmonyOS鸿蒙应用开发——数据持久化Preferences
- 4记解决MaterialButton背景颜色与设置值不同
- 5鸿蒙HarmonyOS实战-ArkUI组件(RelativeContainer)
- 6鸿蒙HarmonyOS实战-ArkUI组件(Stack)
- 7[Android][NDK][Cmake]一文搞懂Android项目中的Cmake
- 8Android广播如何解决Sending non-protected broadcast问题
- 9鸿蒙HarmonyOS实战-ArkUI组件(mediaquery)
- 最近发表
-
- WooCommerce最好的WordPress常用插件下载博客插件模块的相关产品
- 羊驼机器人最好的WordPress常用插件下载博客插件模块
- IP信息记录器最好的WordPress常用插件下载博客插件模块
- Linkly for WooCommerce最好的WordPress常用插件下载博客插件模块
- 元素聚合器Forms最好的WordPress常用插件下载博客插件模块
- Promaker Chat 最好的WordPress通用插件下载 博客插件模块
- 自动更新发布日期最好的WordPress常用插件下载博客插件模块
- WordPress官方最好的获取回复WordPress常用插件下载博客插件模块
- Img to rss最好的wordpress常用插件下载博客插件模块
- WPMozo为Elementor最好的WordPress常用插件下载博客插件模块添加精简版