播放音频
本教程共 100 篇 · 第 88 篇 · 更新于 2026-07-28 · 约 8 分钟阅读
88. 播放音频
本节目标:学会用 Media3(ExoPlayer)播放音频,掌握 Player 创建、播放控制、音频焦点、前台服务通知,理解为什么不再用老的 MediaPlayer。
音频播放是常见需求:播客、音乐、音效。Android 这块从老的 MediaPlayer 演进到现代的 Media3(ExoPlayer)。这章讲现代方案。
为什么不用 MediaPlayer
MediaPlayer 是 Android 内置的播放器,问题不少:
- API 复杂,状态机难懂,容易踩坑。
- 不支持自适应流(DASH、HLS)。
- 错误处理弱,崩了不知原因。
- 不支持后台播放的完整方案。
ExoPlayer 是 Google 出的现代播放器,Media3 是它的最新封装( androidx.media3 命名空间)。优势:
- API 简洁,状态清晰。
- 支持自适应流(DASH/HLS/SmoothStreaming)。
- 支持缓冲控制、DRM、广告插入。
- 和 Compose 集成好。
- 支持后台播放 + 媒体通知 + 蓝牙按键。
Note老的
MediaPlayer没废弃,简单场景(播一个本地音效)还能用。新项目音视频播放首选 Media3。
加依赖
dependencies {
implementation("androidx.media3:media3-exoplayer:1.4.1")
implementation("androidx.media3:media3-ui:1.4.1") // UI 组件
implementation("androidx.media3:media3-session:1.4.1") // 媒体会话
implementation("androidx.media3:media3-common:1.4.1")
}
加权限:
<uses-permission android:name="android.permission.INTERNET" /> <!-- 播放在线音频 -->
创建 Player
val player = ExoPlayer.Builder(context)
.build()
// 设置媒体源
val mediaItem = MediaItem.fromUri("https://example.com/song.mp3")
player.setMediaItem(mediaItem)
// 准备并播放
player.prepare()
player.play()
本地文件用 RawResourceDataSource 或者文件 URI:
val mediaItem = MediaItem.fromUri("android.resource://$packageName/${R.raw.bg_music}")
player.setMediaItem(mediaItem)
播放控制
player.play() // 播放
player.pause() // 暂停
player.seekTo(30000) // 跳到 30 秒
player.stop() // 停止
player.release() // 释放资源
player.next() // 下一首
player.previous() // 上一首
设置多个媒体项(播放列表):
val items = listOf(
MediaItem.fromUri("https://example.com/song1.mp3"),
MediaItem.fromUri("https://example.com/song2.mp3")
)
player.setMediaItems(items)
player.prepare()
监听状态
player.addListener(object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
// 播放/暂停状态变化
}
override fun onPlaybackStateChanged(state: Int) {
when (state) {
Player.STATE_BUFFERING -> // 缓冲中
Player.STATE_READY -> // 准备好
Player.STATE_ENDED -> // 播放结束
Player.STATE_IDLE -> // 空闲
}
}
override fun onPlayerError(error: PlaybackException) {
// 出错了
Log.e("Player", error.message ?: "")
}
})
在 Compose 里用
把 Player 包装成状态:
@Composable
fun AudioPlayer(url: String) {
val context = LocalContext.current
val player = remember {
ExoPlayer.Builder(context).build().apply {
setMediaItem(MediaItem.fromUri(url))
prepare()
}
}
DisposableEffect(Unit) {
onDispose {
player.release()
}
}
var isPlaying by remember { mutableStateOf(false) }
LaunchedEffect(player) {
player.addListener(object : Player.Listener {
override fun onIsPlayingChanged(playing: Boolean) {
isPlaying = playing
}
})
}
Column {
Text(if (isPlaying) "播放中" else "已暂停")
Button(onClick = { if (isPlaying) player.pause() else player.play() }) {
Text(if (isPlaying) "暂停" else "播放")
}
}
}
WarningPlayer 用完必须
release(),否则持有资源泄漏。用DisposableEffect在 Composable 离开时释放。
音频焦点
多个应用同时发声会乱。Android 用「音频焦点」机制协调:播放前先抢焦点,丢了焦点就暂停。
val audioManager = context.getSystemService(AudioManager::class.java)
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
.setOnAudioFocusChangeListener { focusChange ->
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> player.play()
AudioManager.AUDIOFOCUS_LOSS -> player.stop()
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> player.pause()
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> // 降低音量
}
}
.build()
// 播放前抢焦点
val granted = audioManager.requestAudioFocus(focusRequest) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
if (granted) {
player.play()
}
NoteAndroid 8.0+ 用
AudioFocusRequest,老版本用requestAudioFocus(listener, ...)(已废弃)。
前台服务播放
切到后台还要播放(音乐应用),用前台服务 + MediaSession:
class PlaybackService : MediaSessionService() {
private var mediaSession: MediaSession? = null
override fun onCreate() {
super.onCreate()
val player = ExoPlayer.Builder(this).build()
mediaSession = MediaSession.Builder(this, player).build()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
return mediaSession
}
override fun onDestroy() {
mediaSession?.run {
player.release()
release()
}
super.onDestroy()
}
}
manifest 声明:
<service
android:name=".PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
</intent-filter>
</service>
权限:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
启动:
val sessionToken = SessionToken(context, ComponentName(context, PlaybackService::class.java))
val controllerFuture = MediaController.Builder(context, sessionToken).buildAsync()
Media3 自动处理媒体通知、蓝牙按键、锁屏控制。
视频播放
视频和音频基本一样,区别是要个 SurfaceView/TextureView 显示画面。Compose 用 androidx.media3:media3-ui-compose:
@Composable
fun VideoPlayer(url: String) {
val context = LocalContext.current
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
setMediaItem(MediaItem.fromUri(url))
prepare()
playWhenReady = true
}
}
DisposableEffect(Unit) {
onDispose { exoPlayer.release() }
}
AndroidView(
factory = { ctx ->
PlayerView(ctx).apply {
player = exoPlayer
}
}
)
}
配置优化
ExoPlayer.Builder(context)
.setSeekBackIncrementMs(5000) // 后退步长 5 秒
.setSeekForwardIncrementMs(5000)
.setHandleAudioBecomingNoisy(true) // 耳机拔出自动暂停
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.build(),
true // 自动管理音频焦点
)
.build()
setAudioAttributes(..., true) 让 ExoPlayer 自动管理音频焦点,不用手写。
常见坑
- Player 没 release:内存泄漏,崩溃。用
DisposableEffect或onCleared。 - 后台播放不开前台服务:Android 12+ 直接被杀。
- 前台服务不声明类型:Android 14+ 崩。
- 音频焦点不抢:和其他应用声音冲突,体验差。
- MediaItem URI 写错:状态卡在 BUFFERING。
- 播放本地 raw 资源 URI 写错:要用
android.resource://包名/资源id格式。
小结
用 Media3(ExoPlayer)替代老的 MediaPlayer,依赖简洁、API 友好、支持现代格式。ExoPlayer.Builder 创建、setMediaItem 设置源、prepare() + play() 播放。Player.Listener 监听状态。后台播放用 MediaSessionService + 前台服务,自动处理通知和蓝牙按键。视频用 PlayerView 显示画面。
下一章讲相机,看怎么拍照和录像。