睿诚科技协会

Android网络MP3如何播放下载?

核心概念

  1. 下载: 从网络 URL 获取 MP3 文件数据,Android 提供了多种方式,如 HttpURLConnection (传统)、OkHttp (现代推荐)。
  2. 播放: 将下载到的 MP3 数据流或文件传递给 Android 的媒体播放器进行播放。MediaPlayer 是最核心的类。
  3. 缓存: 为了提升用户体验,避免重复下载,我们需要将下载的 MP3 文件缓存到本地存储中,可以使用 File 或专门的缓存库如 DiskLruCache
  4. 后台播放: 即使 App 进入后台,也要能继续播放音乐,这需要使用 MediaSessionNotification 来创建媒体通知,并与系统媒体中心交互。

完整实现步骤

我们将使用 OkHttp 进行网络请求,MediaPlayer 进行播放,并实现一个简单的本地缓存。

Android网络MP3如何播放下载?-图1
(图片来源网络,侵删)

第 1 步:添加网络权限和依赖

app/build.gradle 文件中添加 OkHttp 依赖:

dependencies {
    implementation 'com.squareup.okhttp3:okhttp:4.12.0' // 使用最新稳定版本
}

AndroidManifest.xml 中添加网络权限:

<uses-permission android:name="android.permission.INTERNET" />
<!-- 如果需要下载到外部存储,还需要这个权限 -->
<!-- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> -->
<!-- Android 13 (API 33+) 及以上版本需要申请媒体权限 -->
<!-- <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> -->

注意: 从 Android 10 (API 29) 开始,应用默认不能在外部存储(Environment.getExternalStorageDirectory())创建自己的文件,推荐使用应用专属的内部缓存目录 getExternalFilesDir(null)

第 2 步:创建缓存工具类

这个类负责管理 MP3 文件的下载和缓存。

Android网络MP3如何播放下载?-图2
(图片来源网络,侵删)
import android.content.Context;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class Mp3CacheManager {
    private static final String TAG = "Mp3CacheManager";
    private static final String CACHE_DIR_NAME = "mp3_cache";
    private final Context context;
    private final OkHttpClient okHttpClient;
    public Mp3CacheManager(Context context) {
        this.context = context.getApplicationContext();
        this.okHttpClient = new OkHttpClient();
    }
    /**
     * 获取或下载MP3文件
     * @param mp3Url MP3文件的URL
     * @return 文件对象,如果下载失败则返回null
     */
    public File getCachedOrDownloadFile(String mp3Url) {
        // 1. 检查缓存中是否已存在
        File cacheFile = new File(getCacheDir(), getFileNameFromUrl(mp3Url));
        if (cacheFile.exists()) {
            Log.d(TAG, "File found in cache: " + cacheFile.getAbsolutePath());
            return cacheFile;
        }
        // 2. 如果不存在,则下载
        Log.d(TAG, "Downloading file from: " + mp3Url);
        try (InputStream inputStream = downloadFile(mp3Url);
             FileOutputStream outputStream = new FileOutputStream(cacheFile)) {
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
            Log.d(TAG, "File downloaded successfully: " + cacheFile.getAbsolutePath());
            return cacheFile;
        } catch (IOException e) {
            Log.e(TAG, "Failed to download file", e);
            if (cacheFile.exists()) {
                cacheFile.delete(); // 下载失败,删除不完整的文件
            }
            return null;
        }
    }
    /**
     * 使用OkHttp下载文件流
     */
    private InputStream downloadFile(String fileUrl) throws IOException {
        Request request = new Request.Builder()
                .url(fileUrl)
                .build();
        Response response = okHttpClient.newCall(request).execute();
        if (!response.isSuccessful()) {
            throw new IOException("Unexpected code " + response);
        }
        return response.body().byteStream();
    }
    /**
     * 从URL中提取文件名
     */
    private String getFileNameFromUrl(String url) {
        return url.substring(url.lastIndexOf('/') + 1);
    }
    /**
     * 获取缓存目录
     */
    private File getCacheDir() {
        File cacheDir = new File(context.getExternalFilesDir(null), CACHE_DIR_NAME);
        if (!cacheDir.exists()) {
            cacheDir.mkdirs();
        }
        return cacheDir;
    }
}

第 3 步:创建 MediaPlayer 管理器

这个类负责管理 MediaPlayer 的生命周期、播放、暂停等操作。

import android.content.Context;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.PowerManager;
import android.util.Log;
import java.io.IOException;
public class Mp3PlayerManager implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener {
    private static final String TAG = "Mp3PlayerManager";
    private MediaPlayer mediaPlayer;
    private Context context;
    private PlaybackListener listener;
    public interface PlaybackListener {
        void onPrepared();
        void onError(String error);
        void onCompletion();
        void onProgressUpdate(int progress, int duration);
    }
    public Mp3PlayerManager(Context context) {
        this.context = context;
    }
    public void setPlaybackListener(PlaybackListener listener) {
        this.listener = listener;
    }
    public void play(String mp3Url) {
        // 如果正在播放,先停止并释放
        if (mediaPlayer != null) {
            mediaPlayer.stop();
            mediaPlayer.release();
            mediaPlayer = null;
        }
        mediaPlayer = new MediaPlayer();
        mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); // 防止屏幕关闭时播放停止
        mediaPlayer.setOnPreparedListener(this);
        mediaPlayer.setOnErrorListener(this);
        mediaPlayer.setOnCompletionListener(this);
        try {
            // 使用缓存管理器获取文件
            Mp3CacheManager cacheManager = new Mp3CacheManager(context);
            File mp3File = cacheManager.getCachedOrDownloadFile(mp3Url);
            if (mp3File != null) {
                // 从本地文件播放
                mediaPlayer.setDataSource(context, Uri.fromFile(mp3File));
                mediaPlayer.prepareAsync(); // 异步准备,避免阻塞UI线程
            } else {
                if (listener != null) {
                    listener.onError("Failed to download or find the MP3 file.");
                }
            }
        } catch (IOException e) {
            Log.e(TAG, "Error setting data source", e);
            if (listener != null) {
                listener.onError("IO Error: " + e.getMessage());
            }
        }
    }
    public void pause() {
        if (mediaPlayer != null && mediaPlayer.isPlaying()) {
            mediaPlayer.pause();
        }
    }
    public void resume() {
        if (mediaPlayer != null && !mediaPlayer.isPlaying()) {
            mediaPlayer.start();
        }
    }
    public void stop() {
        if (mediaPlayer != null) {
            mediaPlayer.stop();
            mediaPlayer.release();
            mediaPlayer = null;
        }
    }
    public int getDuration() {
        return mediaPlayer != null ? mediaPlayer.getDuration() : 0;
    }
    public int getCurrentPosition() {
        return mediaPlayer != null ? mediaPlayer.getCurrentPosition() : 0;
    }
    public void seekTo(int position) {
        if (mediaPlayer != null) {
            mediaPlayer.seekTo(position);
        }
    }
    @Override
    public void onPrepared(MediaPlayer mp) {
        Log.d(TAG, "MediaPlayer prepared");
        mp.start();
        if (listener != null) {
            listener.onPrepared();
        }
    }
    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        Log.e(TAG, "MediaPlayer error. What: " + what + ", Extra: " + extra);
        if (listener != null) {
            listener.onError("MediaPlayer Error");
        }
        return true; // 表示我们已经处理了错误
    }
    @Override
    public void onCompletion(MediaPlayer mp) {
        Log.d(TAG, "Playback completed");
        if (listener != null) {
            listener.onCompletion();
        }
    }
}

第 4 步:在 Activity 或 Fragment 中使用

import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity implements Mp3PlayerManager.PlaybackListener {
    private static final String MP3_URL = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3"; // 示例MP3链接
    private Mp3PlayerManager playerManager;
    private Button playPauseButton;
    private Button stopButton;
    private TextView statusText;
    private ProgressBar progressBar;
    private Handler progressHandler = new Handler(Looper.getMainLooper());
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        playPauseButton = findViewById(R.id.btn_play_pause);
        stopButton = findViewById(R.id.btn_stop);
        statusText = findViewById(R.id.tv_status);
        progressBar = findViewById(R.id.progress_bar);
        playerManager = new Mp3PlayerManager(this);
        playerManager.setPlaybackListener(this);
        playPauseButton.setOnClickListener(v -> {
            if (playerManager.getDuration() > 0 && !playerManager.isPlaying()) {
                playerManager.resume();
                playPauseButton.setText("Pause");
            } else {
                playerManager.pause();
                playPauseButton.setText("Play");
            }
        });
        stopButton.setOnClickListener(v -> {
            playerManager.stop();
            playPauseButton.setText("Play");
            progressBar.setProgress(0);
            statusText.setText("Stopped");
        });
    }
    @Override
    public void onPrepared() {
        runOnUiThread(() -> {
            playPauseButton.setText("Pause");
            statusText.setText("Playing...");
            updateProgress();
        });
    }
    @Override
    public void onError(String error) {
        runOnUiThread(() -> {
            Toast.makeText(this, "Error: " + error, Toast.LENGTH_SHORT).show();
            statusText.setText("Error");
        });
    }
    @Override
    public void onCompletion() {
        runOnUiThread(() -> {
            playPauseButton.setText("Play");
            statusText.setText("Completed");
            progressHandler.removeCallbacksAndMessages(null); // 停止进度更新
        });
    }
    private void updateProgress() {
        if (playerManager == null) return;
        int duration = playerManager.getDuration();
        int currentPos = playerManager.getCurrentPosition();
        if (duration > 0) {
            int progress = (int) (1000 * (float) currentPos / duration); // 假设进度条最大为1000
            progressBar.setProgress(progress);
            statusText.setText(String.format("Playing... %d/%d ms", currentPos, duration));
        }
        // 每秒更新一次
        progressHandler.postDelayed(this::updateProgress, 1000);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (playerManager != null) {
            playerManager.stop();
        }
        progressHandler.removeCallbacksAndMessages(null); // 防止内存泄漏
    }
}

对应的 activity_main.xml 布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">
    <TextView
        android:id="@+id/tv_status"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Ready to play"
        android:textSize="18sp" />
    <ProgressBar
        android:id="@+id/progress_bar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:max="1000" />
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:orientation="horizontal">
        <Button
            android:id="@+id/btn_play_pause"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Play" />
        <Button
            android:id="@+id/btn_stop"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Stop" />
    </LinearLayout>
</LinearLayout>

进阶与最佳实践

后台播放与媒体通知

为了让音乐在后台播放,你需要:

  1. 创建一个 Service: 创建一个继承自 Service 的类,MusicService,在这个 Service 中初始化并管理 MediaPlayer
  2. 使用 MediaSession: 创建一个 MediaSession 来与系统媒体中心交互,这允许锁屏控制、通知控制以及与其他音乐 App 的协调。
  3. 创建通知: 使用 MediaStyle 创建一个通知,显示播放/暂停按钮和歌曲信息,当通知被点击时,能将 App 拉到前台。

这是一个比较复杂的主题,Android 官方提供了 MediaSessionMediaController 的详细指南。

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

使用 ExoPlayer (强烈推荐)

对于任何复杂的音频/视频播放需求,Google 的 ExoPlayer 是比 MediaPlayer 更现代、更强大的选择。

为什么推荐 ExoPlayer?

  • 高度可定制: 可以自定义组件,如加载器、解码器、渲染器。
  • 支持现代协议: 原生支持 DASH, HLS, SmoothStreaming 等流媒体协议,也能很好地处理普通 HTTP/HTTPS 下载。
  • 更稳定的性能: 经过 Google 大量项目验证,性能和稳定性远超 MediaPlayer
  • 先进的特性: 支持自适应比特率、硬件加速、字幕等。

ExoPlayer 基本用法示例:

// build.gradle
implementation "androidx.media3:media3-exoplayer:1.3.1"
implementation "androidx.media3:media3-exoplayer-hls:1.3.1" // 如果需要播放HLS流
implementation "androidx.media3:media3-ui:1.3.1" // PlayerView
<!-- activity_main.xml -->
<androidx.media3.ui.PlayerView
    android:id="@+id/player_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:use_controller="true" />
// MainActivity.java
import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.MediaItem;
import androidx.media3.exoplayer.ExoPlayer;
public class MainActivity extends AppCompatActivity {
    private ExoPlayer player;
    private androidx.media3.ui.PlayerView playerView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        playerView = findViewById(R.id.player_view);
        player = new ExoPlayer.Builder(this).build();
        playerView.setPlayer(player);
        // 准备要播放的媒体项目
        MediaItem mediaItem = MediaItem.fromUri("https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3");
        player.setMediaItem(mediaItem);
        // 准备并播放
        player.prepare();
        player.play();
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        player.release(); // 释放播放器资源
    }
}

权限处理

  • INTERNET: 在 AndroidManifest.xml 中声明即可。
  • WRITE_EXTERNAL_STORAGE: 如果需要将文件下载到共享的外部存储,需要动态请求此权限(针对 Android 6.0+),但从 Android 10 开始,强烈建议使用 getExternalFilesDir(),它不需要任何权限。
  • FOREGROUND_SERVICE: 如果你的后台 Service 需要显示前台通知,需要此权限(针对 Android 9+)。
  • READ_MEDIA_AUDIO: 如果你的 App 需要读取用户的音频文件(在播放列表中显示),在 Android 13+ 上需要此权限。
方案 优点 缺点 适用场景
MediaPlayer 简单易用,Android 系统原生支持 功能有限,不支持现代流媒体协议,Bug 较多,难以定制 非常简单的音频播放,如提示音、短音效。
ExoPlayer 功能强大,性能优异,高度可定制,协议支持好 学习曲线稍陡,集成相对复杂 专业级音频/视频 App,流媒体 App,对播放体验要求高的 App。

对于网络 MP3 播放,强烈建议直接使用 ExoPlayer,虽然它比 MediaPlayer 复杂一些,但为你省去了大量处理边缘情况和兼容性问题的麻烦,提供了更好的用户体验和未来的可扩展性,上面的 MediaPlayer 示例可以帮助你理解基本流程,但新项目应优先考虑 ExoPlayer。

分享:
扫描分享到社交APP
上一篇
下一篇