Android 音频播放器开发全指南:从 MediaPlayer 到 ExoPlayer
在 Android 应用开发中,音频播放是一个常见且至关重要的功能。无论是音乐流媒体应用、播客客户端、有声读物,还是简单的提示音效,一个稳定、高效、用户体验良好的音频播放器都是核心。Android 系统提供了多种音频播放的解决方案,从简单易用的 MediaPlayer 到功能强大、可高度定制的 ExoPlayer。
本文将深入探讨 Android 音频播放器的开发,涵盖核心概念、两种主要实现方式(MediaPlayer 和 ExoPlayer)、最佳实践以及常见问题的处理。无论你是初学者还是有经验的开发者,都能从中获得实用的知识。
目录#
核心概念与准备工作#
在开始编码之前,需要了解一些核心概念并完成基本配置。
核心概念#
- 音频源:可以是本地资源 (
res/raw)、本地文件、ContentProvider 中的文件或网络流 (HTTP/HTTPS)。 - 流媒体协议:如 HLS、DASH、SmoothStreaming,用于自适应码率流,ExoPlayer 对其有很好的支持。
- 音频焦点:当多个应用同时请求播放音频时,系统需要协调。应用应遵守音频焦点管理,确保良好的用户体验。
- 后台播放:允许应用在后台或屏幕关闭时继续播放音频,需要
Service和MediaSession的支持。 - 媒体样式通知:在通知栏显示播放控件、专辑封面等信息。
权限配置#
根据音频源的不同,你可能需要在 AndroidManifest.xml 中声明权限。
<!-- 如果需要播放网络音频 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 如果需要读取设备上的音频文件 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 在 Android 10 (API 29) 及以上,访问媒体文件推荐使用媒体存储 API,而非 READ_EXTERNAL_STORAGE -->使用 Android MediaPlayer#
MediaPlayer 是 Android 框架内置的类,适用于简单的音频播放场景。它易于上手,但功能和扩展性有限。
MediaPlayer 基本用法#
以下是一个播放本地 Raw 资源的基本示例。
class SimpleMediaPlayerActivity : AppCompatActivity() {
private var mediaPlayer: MediaPlayer? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_simple_media_player)
// 初始化 MediaPlayer 并设置数据源
mediaPlayer = MediaPlayer.create(this, R.raw.sample_audio)
// 设置循环播放
mediaPlayer?.isLooping = true
button_play.setOnClickListener {
mediaPlayer?.start() // 开始或恢复播放
}
button_pause.setOnClickListener {
mediaPlayer?.pause() // 暂停播放
}
button_stop.setOnClickListener {
mediaPlayer?.stop() // 停止播放,之后需要重新准备
// 停止后,如果需要再次播放,必须调用 prepareAsync() 或 prepare()
// mediaPlayer?.prepareAsync()
}
}
override fun onDestroy() {
super.onDestroy()
// 释放 MediaPlayer 资源,这是非常重要的步骤!
mediaPlayer?.release()
mediaPlayer = null
}
}播放网络音频的步骤类似,但需要异步准备:
val mediaPlayer = MediaPlayer()
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC)
try {
mediaPlayer.setDataSource("https://example.com/audio.mp3")
mediaPlayer.prepareAsync() // 异步准备,避免阻塞主线程
mediaPlayer.setOnPreparedListener { preparedPlayer ->
preparedPlayer.start() // 准备完成后开始播放
}
} catch (e: IOException) {
e.printStackTrace()
}MediaPlayer 状态管理#
MediaPlayer 有一个明确的状态机。不正确的调用顺序(如在 Idle 状态调用 start())会抛出异常。理解其状态转换至关重要。
MediaPlayer 最佳实践与局限性#
最佳实践:
- 总是调用
release():在 Activity/Fragment 的onDestroy()或不再需要播放器时,必须调用release()来释放底层资源。 - 使用
prepareAsync():对于网络或耗时较长的数据源,使用prepareAsync()而非prepare()以避免阻塞 UI 线程。 - 处理错误:设置
OnErrorListener来捕获和处理播放错误。mediaPlayer?.setOnErrorListener { mp, what, extra -> Log.e("MediaPlayer", "Error: what=$what, extra=$extra") true // 返回 true 表示错误已处理 }
局限性:
- 不支持高级流媒体格式(如 HLS, DASH)。
- 自定义扩展能力差(如自定义音频渲染器、格式支持)。
- 某些设备上的兼容性问题可能更难处理。
使用 ExoPlayer#
ExoPlayer 是 Google 开发的一个开源、应用级的媒体播放库,构建在 Android 的低级媒体 API 之上。
为何选择 ExoPlayer?#
- 支持丰富的格式:对 HLS, DASH, SmoothStreaming 等自适应格式有原生支持。
- 高度可定制和可扩展:你可以自定义加载器、渲染器、提取器等组件。
- 定期更新:由 Google 团队积极维护,能快速支持新的媒体格式和特性。
- 与 MediaSession 无缝集成:便于实现后台播放和通知栏控件。
ExoPlayer 集成与基本用法#
1. 添加依赖
在 app/build.gradle 文件中添加依赖:
dependencies {
implementation 'com.google.android.exoplayer:exoplayer:2.X.X' // 请使用最新版本
// 如果需要核心功能以外的模块,如 HLS
implementation 'com.google.android.exoplayer:exoplayer-hls:2.X.X'
}2. 基本播放示例
class SimpleExoPlayerActivity : AppCompatActivity() {
private var exoPlayer: SimpleExoPlayer? = null
private var playWhenReady = true
private var currentWindow = 0
private var playbackPosition = 0L
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_simple_exo_player)
initializePlayer()
}
private fun initializePlayer() {
// 1. 创建 ExoPlayer 实例
val trackSelector = DefaultTrackSelector(this).apply {
setParameters(buildUponParameters().setMaxVideoSizeSd())
}
exoPlayer = SimpleExoPlayer.Builder(this)
.setTrackSelector(trackSelector)
.build()
// 2. 将播放器绑定到 UI(PlayerView)
player_view.player = exoPlayer
// 3. 准备媒体源(MediaSource)
val mediaItem = MediaItem.fromUri("https://example.com/audio.mp3")
// 对于 HLS 流,可以使用 HlsMediaSource.Factory
val mediaSource = ProgressiveMediaSource.Factory(
DefaultDataSource.Factory(this)
).createMediaSource(mediaItem)
exoPlayer?.playWhenReady = playWhenReady
exoPlayer?.seekTo(currentWindow, playbackPosition)
exoPlayer?.setMediaSource(mediaSource)
exoPlayer?.prepare()
}
private fun releasePlayer() {
exoPlayer?.let { player ->
playbackPosition = player.currentPosition
currentWindow = player.currentWindowIndex
playWhenReady = player.playWhenReady
player.release()
}
exoPlayer = null
}
override fun onStart() {
super.onStart()
if (Util.SDK_INT >= 24) {
initializePlayer()
}
}
override fun onResume() {
super.onResume()
if (Util.SDK_INT < 24 || exoPlayer == null) {
initializePlayer()
}
}
override fun onPause() {
super.onPause()
if (Util.SDK_INT < 24) {
releasePlayer()
}
}
override fun onStop() {
super.onStop()
if (Util.SDK_INT >= 24) {
releasePlayer()
}
}
}ExoPlayer 核心组件与高级功能#
- MediaSource:代表要播放的媒体。
ProgressiveMediaSource用于普通文件/流,HlsMediaSource用于 HLS 流。 - TrackSelector:用于选择媒体中的轨道(如音轨、字幕)。
- LoadControl:控制媒体数据的缓冲方式。
- 监听事件:通过
exoPlayer.addListener(...)监听播放状态、缓冲进度、错误等。
exoPlayer?.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
when (state) {
Player.STATE_READY -> { /* 播放器已准备好 */ }
Player.STATE_BUFFERING -> { /* 正在缓冲 */ }
Player.STATE_ENDED -> { /* 播放结束 */ }
Player.STATE_IDLE -> { /* 空闲状态 */ }
}
}
override fun onPlayerError(error: PlaybackException) {
// 处理播放错误
Log.e("ExoPlayer", "Playback error", error)
}
})ExoPlayer 最佳实践#
- 生命周期管理:在
onStart/onStop或onResume/onPause中初始化和释放播放器(根据 API 级别调整)。 - 使用
PlayerView:它提供了现成的播放控制 UI(播放/暂停按钮、进度条等)。 - 缓存媒体:使用
CacheDataSource.Factory来实现媒体缓存,提升重复播放体验和节省流量。val cache = SimpleCache( File(cacheDir, "exoplayer-cache"), LeastRecentlyUsedCacheEvictor(100 * 1024 * 1024) // 100MB ) val dataSourceFactory = DefaultDataSource.Factory(this) val cacheDataSourceFactory = CacheDataSource.Factory() .setCache(cache) .setUpstreamDataSourceFactory(dataSourceFactory) val mediaSource = ProgressiveMediaSource.Factory(cacheDataSourceFactory) .createMediaSource(mediaItem) - 处理音频焦点:(见下文)
后台播放与媒体会话#
为了实现后台播放,你需要将播放逻辑放在一个 Service(通常是 MediaBrowserService)中,并使用 MediaSession 进行通信。
核心步骤:
- 创建
MediaSession:在 Service 中创建并初始化。 - 实现
MediaSession.Callback:在这里处理播放、暂停、跳转等控制命令。 - 将
MediaSession的 Token 传递给 UI:以便 Activity 可以创建MediaController来控制 Service 中的播放器。 - 管理 Service 生命周期:使用
startService()或ContextCompat.startForegroundService()启动服务,并在播放时启动前台服务以避免被系统杀死。
这是 ExoPlayer 与 MediaSession 集成的推荐模式,ExoPlayer 提供了 PlayerNotificationManager 来简化通知栏控件的创建。
处理音频焦点#
当你的应用开始播放音频时,应该请求音频焦点。如果其他应用获得焦点,你的应用应该暂停播放或降低音量。
class AudioFocusHelper(private val context: Context, private val player: SimpleExoPlayer) {
private val audioManager: AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
private var audioFocusRequest: AudioFocusRequest? = null
private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> {
// 重新获得焦点,恢复播放
player.playWhenReady = true
player.volume = 1.0f
}
AudioManager.AUDIOFOCUS_LOSS -> {
// 永久失去焦点,停止播放
player.pause()
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
// 暂时失去焦点,暂停播放
player.playWhenReady = false
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
// 暂时失去焦点,可以降低音量
player.volume = 0.2f
}
}
}
fun requestAudioFocus(): Boolean {
val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
setOnAudioFocusChangeListener(audioFocusChangeListener)
build()
}
audioManager.requestAudioFocus(audioFocusRequest!!)
} else {
@Suppress("DEPRECATION")
audioManager.requestAudioFocus(
audioFocusChangeListener,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
)
}
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
}
fun abandonAudioFocus() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
audioFocusRequest?.let { audioManager.abandonAudioFocusRequest(it) }
} else {
@Suppress("DEPRECATION")
audioManager.abandonAudioFocus(audioFocusChangeListener)
}
}
}在播放开始前调用 requestAudioFocus(),在播放结束或释放播放器时调用 abandonAudioFocus()。
通知栏媒体控件#
使用 MediaSession 和 PlayerNotificationManager(ExoPlayer 库的一部分)可以轻松创建符合 Material Design 的媒体通知。
// 在你的 Service 中
class PlaybackService : Service() {
private lateinit var playerNotificationManager: PlayerNotificationManager
override fun onCreate() {
super.onCreate()
// ... 初始化 ExoPlayer 和 MediaSession ...
playerNotificationManager = PlayerNotificationManager.Builder(
this,
NOTIFICATION_ID,
CHANNEL_ID
).setMediaDescriptionAdapter(/* ... */) // 自定义通知内容
.setSmallIconResourceId(R.drawable.ic_music_note)
.build().apply {
setPlayer(exoPlayer)
setMediaSessionToken(mediaSession.sessionToken)
}
}
}总结#
开发一个健壮的 Android 音频播放器需要考虑多个方面:
- 简单场景:对于基础需求,
MediaPlayer是一个不错的选择。 - 复杂与高级需求:对于流媒体、自定义需求或更好的用户体验,ExoPlayer 是事实上的标准。
- 后台播放:必须结合
Service和MediaSession。 - 系统集成:务必处理好音频焦点和通知栏控件,以提供符合平台规范的良好用户体验。
- 资源管理:始终记得在适当的生命周期释放播放器资源。
希望这篇指南能为你开发 Android 音频播放功能提供清晰的路径和坚实的起点。