睿诚科技协会

Android MP3网络播放如何实现?

核心概念

在 Android 中处理网络 MP3,主要涉及以下几个关键点:

Android MP3网络播放如何实现?-图1
(图片来源网络,侵删)
  1. 网络请求:如何从服务器获取 MP3 文件的数据流。
  2. 数据流处理:如何将获取到的网络数据流传递给音频播放器。
  3. 音频播放:如何使用 Android 的 MediaPlayerExoPlayer 来播放音频流。
  4. 下载管理:如何将 MP3 文件完整下载到设备存储中。
  5. 后台播放:如何让应用在后台继续播放音乐(需要使用 MediaSessionService)。

使用 MediaPlayer 进行在线流式播放

这是最简单直接的方式,适合不需要下载、只想在线播放的场景。MediaPlayer 可以直接处理网络 URI。

优点

  • 简单易用:API 简单,几行代码就能实现播放。
  • 无需下载:不占用本地存储空间。

缺点

  • 功能有限:对音频格式、编码的支持不如 ExoPlayer 强大。
  • 性能较差:对于不稳定的网络或复杂的流媒体协议,容易出现卡顿和崩溃。
  • 后台播放控制弱:实现健壮的后台播放和媒体控制中心相对复杂。

实现步骤

  1. 添加网络权限AndroidManifest.xml 中必须声明网络权限。

    <uses-permission android:name="android.permission.INTERNET" />
    <!-- 对于 Android 9 (API 28) 及以上,明文流量也需要声明 -->
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  2. AndroidManifest.xml 中声明 android:usesCleartextTraffic="true" 从 Android 9 (API 28) 开始,默认禁止使用 HTTP 明文流量,如果你的 MP3 URL 是 http 开头的,而不是 https,必须在 application 标签或 activity 标签中添加此属性。

    <application
        ...
        android:usesCleartextTraffic="true">
        ...
    </application>
  3. Java/Kotlin 代码实现

    Android MP3网络播放如何实现?-图2
    (图片来源网络,侵删)
    import android.media.MediaPlayer
    import android.net.Uri
    import android.os.Bundle
    import android.widget.Button
    import android.widget.Toast
    import androidx.appcompat.app.AppCompatActivity
    import java.io.IOException
    class MainActivity : AppCompatActivity() {
        private lateinit var mediaPlayer: MediaPlayer
        private val mp3Url = "https://example.com/path/to/your/song.mp3" // 替换成你的MP3 URL
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            val playButton: Button = findViewById(R.id.play_button)
            val pauseButton: Button = findViewById(R.id.pause_button)
            val stopButton: Button = findViewById(R.id.stop_button)
            mediaPlayer = MediaPlayer().apply {
                setOnPreparedListener { mp ->
                    // 准备完成后可以开始播放
                    mp.start()
                    Toast.makeText(this@MainActivity, "开始播放", Toast.LENGTH_SHORT).show()
                }
                setOnCompletionListener { mp ->
                    // 播放完成
                    Toast.makeText(this@MainActivity, "播放完成", Toast.LENGTH_SHORT).show()
                }
                setOnErrorListener { mp, what, extra ->
                    // 发生错误
                    Toast.makeText(this@MainActivity, "播放错误: $what", Toast.LENGTH_SHORT).show()
                    false
                }
            }
            playButton.setOnClickListener {
                try {
                    // 重置 MediaPlayer,防止重复播放
                    if (mediaPlayer.isPlaying) {
                        mediaPlayer.stop()
                    }
                    mediaPlayer.reset()
                    // 设置数据源为网络URL
                    mediaPlayer.setDataSource(this, Uri.parse(mp3Url))
                    // 异步准备,这是一个耗时操作
                    mediaPlayer.prepareAsync()
                } catch (e: IOException) {
                    e.printStackTrace()
                    Toast.makeText(this, "无法播放: ${e.message}", Toast.LENGTH_LONG).show()
                }
            }
            pauseButton.setOnClickListener {
                if (mediaPlayer.isPlaying) {
                    mediaPlayer.pause()
                    Toast.makeText(this, "暂停播放", Toast.LENGTH_SHORT).show()
                }
            }
            stopButton.setOnClickListener {
                if (mediaPlayer.isPlaying) {
                    mediaPlayer.stop()
                    // 注意:stop后需要reset才能再次播放
                    mediaPlayer.reset()
                    Toast.makeText(this, "停止播放", Toast.LENGTH_SHORT).show()
                }
            }
        }
        override fun onDestroy() {
            super.onDestroy()
            // 释放资源,防止内存泄漏
            if (mediaPlayer != null) {
                mediaPlayer.release()
                mediaPlayer = null
            }
        }
    }

使用 ExoPlayer (推荐)

ExoPlayer 是 Google 官方推荐的新一代媒体播放器,它是开源的,高度可定制,功能强大,性能优异。**强烈推荐在新项目中使用它,`

优点

  • 功能强大:支持几乎所有常见的媒体格式、协议(如 DASH, HLS, SmoothStreaming)。
  • 性能优异:基于现代的 MediaCodec API,硬件解码效率高,更稳定。
  • 高度可定制:可以自定义 UI、渲染器、加载器等。
  • 更好的后台播放支持:与 MediaSessionMedia3 库集成得更好。

缺点

  • 学习曲线稍陡:API 相比 MediaPlayer 更复杂。

实现步骤

  1. 添加依赖build.gradle (Module level) 文件中添加 ExoPlayer 核心库和扩展库。

    dependencies {
        def exoplayer_version = "1.4.1" // 请使用最新版本
        // ExoPlayer 核心库
        implementation "androidx.media3:media3-exoplayer:$exoplayer_version"
        // ExoPlayer 媒体Session 集成
        implementation "androidx.media3:media3-exoplayer-session:$exoplayer_version"
        // ExoPlayer UI 组件 (可选)
        implementation "androidx.media3:media3-ui:$exoplayer_version"
        // ExoPlayer I/O 扩展 (用于HTTP请求等)
        implementation "androidx.media3:media3-exoplayer-hls:$exoplayer_version" // 如果播放HLS流需要
        implementation "androidx.media3:media3-exoplayer-dash:$exoplayer_version" // 如果播放DASH流需要
    }
  2. Java/Kotlin 代码实现

    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.view.View
    import android.widget.Button
    import android.widget.Toast
    import androidx.media3.common.MediaItem
    import androidx.media3.common.Player
    import androidx.media3.exoplayer.ExoPlayer
    class ExoPlayerActivity : AppCompatActivity() {
        private lateinit var player: ExoPlayer
        private val mp3Url = "https://example.com/path/to/your/song.mp3"
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_exo_player)
            val playButton: Button = findViewById(R.id.play_button)
            val pauseButton: Button = findViewById(R.id.pause_button)
            val stopButton: Button = findViewById(R.id.stop_button)
            // 1. 创建 ExoPlayer 实例
            player = ExoPlayer.Builder(this).build().apply {
                // 添加监听器
                addListener(object : Player.Listener {
                    override fun onPlaybackStateChanged(playbackState: Int) {
                        when (playbackState) {
                            Player.STATE_READY -> {
                                Toast.makeText(this@ExoPlayerActivity, "准备就绪", Toast.LENGTH_SHORT).show()
                            }
                            Player.STATE_BUFFERING -> {
                                Toast.makeText(this@ExoPlayerActivity, "缓冲中...", Toast.LENGTH_SHORT).show()
                            }
                            Player.STATE_ENDED -> {
                                Toast.makeText(this@ExoPlayerActivity, "播放完成", Toast.LENGTH_SHORT).show()
                            }
                            Player.STATE_IDLE -> {
                                // 播放器空闲
                            }
                        }
                    }
                })
            }
            // 2. 创建 MediaItem 并设置给 Player
            val mediaItem = MediaItem.fromUri(mp3Url)
            player.setMediaItem(mediaItem)
            playButton.setOnClickListener {
                if (!player.isPlaying) {
                    player.prepare() // 准备播放
                    player.play()    // 开始播放
                }
            }
            pauseButton.setOnClickListener {
                if (player.isPlaying) {
                    player.pause()
                }
            }
            stopButton.setOnClickListener {
                player.stop() // 停止播放,会重置位置
                // 如果需要重新从头开始播放,需要再次调用 setMediaItem
            }
        }
        override fun onStart() {
            super.onStart()
            // 当Activity可见时,让Player开始播放
            if (player.playWhenReady) {
                player.prepare()
            }
        }
        override fun onStop() {
            super.onStop()
            // 当Activity不可见时,释放Player资源
            player.release()
        }
    }

下载 MP3 文件到本地

如果你希望让用户下载歌曲以便离线收听,或者需要本地播放以获得更好的性能,可以实现下载功能。

Android MP3网络播放如何实现?-图3
(图片来源网络,侵删)

实现思路

  1. 创建下载目录:使用 getExternalFilesDir() 获取应用私有目录,避免需要 READ_EXTERNAL_STORAGE 权限。
  2. 发起网络请求:使用 OkHttpHttpURLConnection 获取网络输入流。
  3. 写入文件:使用 FileOutputStream 将输入流的数据写入本地文件。
  4. 显示进度:使用 ProgressBarAsyncTask (或 Coroutine) 来更新下载进度。

示例代码 (使用 OkHttp + Coroutine)

import android.os.Bundle
import android.widget.Button
import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
class DownloadActivity : AppCompatActivity() {
    private lateinit var progressBar: ProgressBar
    private lateinit var downloadButton: Button
    private val mp3Url = "https://example.com/path/to/your/song.mp3"
    private val fileName = "my_song.mp3"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_download)
        progressBar = findViewById(R.id.progress_bar)
        downloadButton = findViewById(R.id.download_button)
        downloadButton.setOnClickListener {
            downloadFile()
        }
    }
    private fun downloadFile() {
        downloadButton.isEnabled = false
        progressBar.progress = 0
        lifecycleScope.launch(Dispatchers.IO) {
            val client = OkHttpClient()
            val request = Request.Builder().url(mp3Url).build()
            try {
                client.newCall(request).execute().use { response ->
                    if (!response.isSuccessful) throw IOException("Unexpected code $response")
                    val contentLength = response.body?.contentLength() ?: 0L
                    val inputStream = response.body?.byteStream() ?: throw IOException("Response body is null")
                    val outputDir = ContextCompat.getExternalFilesDirs(this@DownloadActivity, null).first()
                    val outputFile = File(outputDir, fileName)
                    val outputStream = FileOutputStream(outputFile)
                    val buffer = ByteArray(4096)
                    var bytesRead: Int
                    var totalBytesRead = 0L
                    while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                        outputStream.write(buffer, 0, bytesRead)
                        totalBytesRead += bytesRead
                        val progress = (totalBytesRead * 100 / contentLength).toInt()
                        // 更新UI必须在主线程
                        withContext(Dispatchers.Main) {
                            progressBar.progress = progress
                        }
                    }
                    outputStream.close()
                    inputStream.close()
                    withContext(Dispatchers.Main) {
                        Toast.makeText(this@DownloadActivity, "下载完成!文件保存在: ${outputFile.absolutePath}", Toast.LENGTH_LONG).show()
                        downloadButton.isEnabled = true
                    }
                }
            } catch (e: Exception) {
                e.printStackTrace()
                withContext(Dispatchers.Main) {
                    Toast.makeText(this@DownloadActivity, "下载失败: ${e.message}", Toast.LENGTH_LONG).show()
                    downloadButton.isEnabled = true
                }
            }
        }
    }
}

后台播放

音乐播放器通常需要在后台继续工作,这需要结合 ServiceMediaSession

  1. MediaService:一个前台服务,负责在后台控制 ExoPlayer 的生命周期,使用 startForeground() 可以防止系统在内存不足时杀死它。
  2. MediaSession:创建一个媒体会话,将播放器的状态(正在播放、暂停、歌曲信息等)暴露给系统,这样,锁屏界面、通知栏和最近任务列表就可以显示标准的媒体控制器。
  3. MediaBrowserService:更高级的服务,允许其他应用(如 Android Auto)发现和控制你的媒体内容。

这是一个相对复杂的主题,通常推荐使用 Google 的 Media3 库来简化开发,它封装了这些复杂性。


总结与建议

特性 MediaPlayer ExoPlayer
易用性 ,API 简单 ,API 更丰富但稍复杂
功能/格式支持 有限,依赖系统解码器 强大,支持几乎所有格式和流媒体协议
性能/稳定性 一般,易崩溃 优秀,基于硬件解码,更稳定
可定制性 ,可深度定制
后台播放 可实现,但麻烦 推荐,与 Media3 集成完美
适用场景 简单的在线播放、演示项目 所有生产环境项目,尤其是音乐、视频应用

最终建议:

  • 对于新项目直接使用 ExoPlayer,它代表了 Android 平台媒体播放的未来,功能、性能和可维护性都远超 MediaPlayer
  • 对于快速原型或简单需求:如果只是做一个临时的、功能简单的播放器,MediaPlayer 可以快速实现。
  • 对于下载功能:使用 OkHttp 或 Retrofit 配合协程来处理网络请求和文件写入,这是目前最现代和高效的方式。
  • 对于后台播放:务必使用 Service + MediaSession(最好是 Media3 库)来实现,这是提供良好用户体验的标准做法。
分享:
扫描分享到社交APP
上一篇
下一篇