Android 音频播放器开发全指南:从 MediaPlayer 到 ExoPlayer

在 Android 应用开发中,音频播放是一个常见且至关重要的功能。无论是音乐流媒体应用、播客客户端、有声读物,还是简单的提示音效,一个稳定、高效、用户体验良好的音频播放器都是核心。Android 系统提供了多种音频播放的解决方案,从简单易用的 MediaPlayer 到功能强大、可高度定制的 ExoPlayer

本文将深入探讨 Android 音频播放器的开发,涵盖核心概念、两种主要实现方式(MediaPlayerExoPlayer)、最佳实践以及常见问题的处理。无论你是初学者还是有经验的开发者,都能从中获得实用的知识。

目录#

  1. 核心概念与准备工作
  2. 使用 Android MediaPlayer
  3. 使用 ExoPlayer
  4. 后台播放与媒体会话
  5. 处理音频焦点
  6. 通知栏媒体控件
  7. 总结
  8. 参考

核心概念与准备工作#

在开始编码之前,需要了解一些核心概念并完成基本配置。

核心概念#

  • 音频源:可以是本地资源 (res/raw)、本地文件、ContentProvider 中的文件或网络流 (HTTP/HTTPS)。
  • 流媒体协议:如 HLS、DASH、SmoothStreaming,用于自适应码率流,ExoPlayer 对其有很好的支持。
  • 音频焦点:当多个应用同时请求播放音频时,系统需要协调。应用应遵守音频焦点管理,确保良好的用户体验。
  • 后台播放:允许应用在后台或屏幕关闭时继续播放音频,需要 ServiceMediaSession 的支持。
  • 媒体样式通知:在通知栏显示播放控件、专辑封面等信息。

权限配置#

根据音频源的不同,你可能需要在 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 最佳实践与局限性#

最佳实践:

  1. 总是调用 release():在 Activity/Fragment 的 onDestroy() 或不再需要播放器时,必须调用 release() 来释放底层资源。
  2. 使用 prepareAsync():对于网络或耗时较长的数据源,使用 prepareAsync() 而非 prepare() 以避免阻塞 UI 线程。
  3. 处理错误:设置 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 最佳实践#

  1. 生命周期管理:在 onStart/onStoponResume/onPause 中初始化和释放播放器(根据 API 级别调整)。
  2. 使用 PlayerView:它提供了现成的播放控制 UI(播放/暂停按钮、进度条等)。
  3. 缓存媒体:使用 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)
  4. 处理音频焦点:(见下文)

后台播放与媒体会话#

为了实现后台播放,你需要将播放逻辑放在一个 Service(通常是 MediaBrowserService)中,并使用 MediaSession 进行通信。

核心步骤:

  1. 创建 MediaSession:在 Service 中创建并初始化。
  2. 实现 MediaSession.Callback:在这里处理播放、暂停、跳转等控制命令。
  3. MediaSession 的 Token 传递给 UI:以便 Activity 可以创建 MediaController 来控制 Service 中的播放器。
  4. 管理 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()


通知栏媒体控件#

使用 MediaSessionPlayerNotificationManager(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 是事实上的标准
  • 后台播放:必须结合 ServiceMediaSession
  • 系统集成:务必处理好音频焦点通知栏控件,以提供符合平台规范的良好用户体验。
  • 资源管理:始终记得在适当的生命周期释放播放器资源。

希望这篇指南能为你开发 Android 音频播放功能提供清晰的路径和坚实的起点。


参考#

  1. Android Developer Guide - MediaPlayer
  2. ExoPlayer Developer Guide
  3. ExoPlayer GitHub Repository
  4. Android Developer Guide - Media Apps Overview
  5. Android Developer Guide - Managing Audio Focus
  6. Android Developer Guide - MediaSession