首页 / Android 入门教程 / 播放音频

Android 入门教程

播放音频

本教程共 100 篇 · 第 88 篇 · 更新于 2026-07-28 · 约 8 分钟阅读

AndroidAndroid 入门教程Media3ExoPlayer音频媒体

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 "播放")
        }
    }
}
Warning

Player 用完必须 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()
}
Note

Android 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 自动管理音频焦点,不用手写。

常见坑

  1. Player 没 release:内存泄漏,崩溃。用 DisposableEffectonCleared
  2. 后台播放不开前台服务:Android 12+ 直接被杀。
  3. 前台服务不声明类型:Android 14+ 崩。
  4. 音频焦点不抢:和其他应用声音冲突,体验差。
  5. MediaItem URI 写错:状态卡在 BUFFERING。
  6. 播放本地 raw 资源 URI 写错:要用 android.resource://包名/资源id 格式。

小结

用 Media3(ExoPlayer)替代老的 MediaPlayer,依赖简洁、API 友好、支持现代格式。ExoPlayer.Builder 创建、setMediaItem 设置源、prepare() + play() 播放。Player.Listener 监听状态。后台播放用 MediaSessionService + 前台服务,自动处理通知和蓝牙按键。视频用 PlayerView 显示画面。

下一章讲相机,看怎么拍照和录像。