前言
音视频播放是应用中一个很重要的功能。Android 提供了原生的 MediaPlayer
和 VideoView
工具,来分别完成音频、视频的播放操作。但这两者的坑很多,稍微使用不当,就会造成严重问题,比如应用崩溃、资源泄露等。
我们来了解一下 MediaPlayer
和 VideoView
的正确使用方法,再说说现代 Android 中推荐的Jetpack Media3 (ExoPlayer)。
播放音频:MediaPlayer
使用 MediaPlayer
,关键在于要了解其生命周期状态机 。几乎所有与 MediaPlayer
相关的崩溃问题都是因为在错误的状态下调用了方法。
图片来自于官方文档
核心方法
MediaPlayer 类中常用的控制方法:
方法名 | 功能描述 |
---|---|
setDataSource() |
[状态: Idle -> Initialized] 设置媒体源。可以是一个路径、URI或文件描述符。 |
prepare() |
[状态: Initialized -> Prepared] 同步准备媒体。会阻塞UI线程,请谨慎使用 |
prepareAsync() |
[状态: Initialized -> Preparing -> Prepared] (生产级推荐) 异步准备媒体,通过监听器回调来通知完成。 |
start() |
[状态: Prepared/Paused/PlaybackCompleted -> Started] 开始或恢复播放。 |
pause() |
[状态: Started -> Paused] 暂停播放。 |
seekTo(int msec) |
定位到指定的毫秒位置。此方法不改变状态。 |
stop() |
[状态: Started/Paused/Prepared/PlaybackCompleted -> Stopped] 停止播放。 |
reset() |
[状态: 任意 -> Idle] 将播放器重置到空闲状态。 |
release() |
[状态: 任意 -> End] (必须调用) 释放所有占用的资源,调用后对象就不可以再使用。 |
isPlaying() |
判断是否正在播放。在非法状态(如End)下调用会抛出异常。 |
getDuration() |
获取媒体总时长。必须在 Prepared 状态之后调用。 |
我们来说说 MediaPlayer
的标准工作流程:
首先,获取一个 MediaPlayer
实例,然后调用其 setDataSource()
方法设置音频文件的路径。接着,调用 prepareAsync()
方法进行异步准备,防止主线程卡顿。并且进行异步准备,需要设置 setOnPreparedListener
监听器来获取准备完成的通知。
最后,当播放器准备完成后,调用 start()
方法开始播放。在播放过程中,可以调用 pause()
方法暂停,调用 start
方法恢复。如果要完全停止播放,可以使用 stop()
方法。调用后播放器会进入 Stopped
状态,需要重新准备才能再次播放。
案例:简易音乐播放器
我们通过一个案例来演示一下。
首先,创建名为 PlayAudioTest
的 Empty Views Activity 项目。在其布局中,添加三个按钮,分别对应播放、暂停以及停止操作。
activity_main.xml
中的代码如下所示:
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/play"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Play" />
<Button
android:id="@+id/pause"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Pause" />
<Button
android:id="@+id/stop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Stop" />
</LinearLayout>
MediaPlayer 可播放网络、本地和应用程序安装包中的音频,这里为了简单起见,我们来播放应用程序安装包中的音频。
我们将准备好的音频文件放到 app/src/main/assets
目录下,因为这个目录会在项目打包时被一并打包到安装文件中。
资源下载 密码:snow
在 MainActivity
中,按照上述流程来完成音乐播放器的逻辑。代码如下:
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var mediaPlayer: MediaPlayer? = null
private var isPrepared = false // 是否准备完成
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 初始化媒体播放器
initMediaPlayer()
binding.play.setOnClickListener {
if (isPrepared && mediaPlayer?.isPlaying == false) {
mediaPlayer?.start()
}
}
binding.pause.setOnClickListener {
if (mediaPlayer?.isPlaying == true) {
mediaPlayer?.pause()
}
}
binding.stop.setOnClickListener {
if (mediaPlayer?.isPlaying == true || isPrepared) {
mediaPlayer?.stop()
// 此时播放器进入Stopped状态,需要重新准备
isPrepared = false
mediaPlayer?.prepareAsync()
}
}
}
private fun initMediaPlayer() {
try {
mediaPlayer = MediaPlayer()
// 获取 AssetManager 实例,用于读取assets目录下的资源
val assetManager = assets
// 打开
val fd = assetManager.openFd("music.mp3")
// 设置数据源
mediaPlayer?.setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
// 使用异步准备,避免阻塞UI
mediaPlayer?.prepareAsync()
// 设置监听器以获取播放器准备完成的回调
mediaPlayer?.setOnPreparedListener {
Log.d("MediaPlayer", "Prepared!")
isPrepared = true
}
// 获取播放器播放完成的回调
mediaPlayer?.setOnCompletionListener {
Toast.makeText(this, "播放完毕", Toast.LENGTH_SHORT).show()
// 播放完成后,直接移到开头并暂停
it.seekTo(0)
it.pause()
}
// 获取播放器错误的回调
mediaPlayer?.setOnErrorListener { mp, what, extra ->
Log.e("MediaPlayer", "发生错误: what=$what, extra=$extra")
isPrepared = false
// 发生错误时,释放资源
mp.release()
mediaPlayer = null
true // 表示已处理错误
}
} catch (e: IOException) {
Log.e("MediaPlayer", "设置数据源失败", e)
}
}
override fun onDestroy() {
super.onDestroy()
// 必须释放资源,防止内存泄漏
mediaPlayer?.release()
mediaPlayer = null
}
}
现在运行程序,点击播放按钮即可开始播放音乐。点击暂停按钮就会停止,再次点击播放按钮,会在之前暂停的位置接着播放。如果此时点击了停止按钮,会停止播放,但当再次播放音乐时,音乐会重新开始播放。
播放视频:VideoView
VideoView
是一个 UI 控件,它内部封装了 MediaPlayer
,可以完成视频的播放。
核心方法
VideoView
中的常用控制方法:
方法名 | 功能描述 |
---|---|
setVideoPath(String path) |
设置本地视频文件的路径。 |
setVideoURI(Uri uri) |
设置视频源的 URI,可用于本地 (android.resource:// ) 和网络视频。 |
start() / pause() |
开始/暂停播放。 |
seekTo(int msec) |
将播放头移动到指定的毫秒位置。 |
stopPlayback() |
停止播放并释放资源。 |
suspend() |
临时释放播放器资源。 |
resume() |
从暂停的位置恢复播放。 |
案例:简易视频播放器
同样地,通过一个例子来理解。
新建名为 PlayVideoTest
的 Empty Views Activity 项目。其布局中也是添加三个按钮,分别表示播放、暂停和重新播放,另外放置一个 VideoView
控件,用于显示视频。
activity_main.xml
中的代码如下所示:
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/play"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Play" />
<Button
android:id="@+id/pause"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Pause" />
<Button
android:id="@+id/replay"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Replay" />
</LinearLayout>
<VideoView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
我们将准备好的视频文件(video.mp4
)放在 res/raw
目录下。你可能会问:为什么不放在刚刚的 asserts
目录下呢?
因为 res/raw
目录下的资源可以通过 android.resource://
协议生成对一个对应的 Uri
,让 VideoView
进行播放。而 asserts
目录下的文件没有这样的 Uri
,播放起来比较复杂。
资源下载 密码:snow
MainActivity
中的代码:
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val videoView get() = binding.videoView
// 将视频资源解析成 Uri 对象
private val videoUri by lazy { Uri.parse("android.resource://$packageName/${R.raw.video}") }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 设置 Uri
videoView.setVideoURI(videoUri)
// 设置准备完成的监听器
videoView.setOnPreparedListener { mediaPlayer ->
// mediaPlayer 是 VideoView 内部的 MediaPlayer 实例
Log.d("VideoView", "Prepared!")
}
// 设置错误监听器
videoView.setOnErrorListener { _, what, extra ->
Toast.makeText(this, "播放出错,错误码: what=$what, extra=$extra", Toast.LENGTH_LONG)
.show()
Log.e("VideoView", "Error: what=$what, extra=$extra")
true // 返回 true 表示我们已经处理了该错误
}
// 设置播放完成监听器
videoView.setOnCompletionListener {
Toast.makeText(this, "播放完毕", Toast.LENGTH_SHORT).show()
}
binding.play.setOnClickListener {
if (!videoView.isPlaying) {
videoView.start()
}
}
binding.pause.setOnClickListener {
if (videoView.isPlaying) {
videoView.pause()
}
}
binding.replay.setOnClickListener {
// 停止当前播放并释放内部播放器
videoView.stopPlayback()
// 重新设置视频 URI,创建新的 MediaPlayer 实例
videoView.setVideoURI(videoUri)
// 开始播放
videoView.start()
}
}
override fun onDestroy() {
super.onDestroy()
// 停止播放并释放资源
videoView.stopPlayback()
}
}
现在运行程序,并点击播放按钮,就可以看到视频开始播放了。
VideoView
只是一个简单的视频播放工具类,并不能支持很多视频格式,以及视频播放效率较低。但你只是使用它来播放一些简单的视频,用它还是可以的。
使用 ExoPlayer
现在,谷歌推荐使用 ExoPlayer
作为音视频播放器,因为它支持的流媒体协议更多、易于扩展和定制、API 设计更可靠。对于大多数场景,应该优先使用它。