一、MediaPlayer 架构设计
MediaPlayer 的整体架构采用了 C/S(客户端/服务器)架构,并严格遵循 Android 的层次化设计理念。这种设计将应用层与底层多媒体服务解耦,保证了系统的稳定性和可扩展性。
其架构从上到下主要分为四个层次:
flowchart TD
A["应用层 (Java)"] --> B["框架层 (Java & JNI)"]
B --> C["本地框架层 (C++)"]
C --> D["多媒体服务层 (C++)"]
subgraph A [应用层]
A1["你的App\n(调用MediaPlayer API)"]
end
subgraph B [框架层]
B1["android.media.MediaPlayer\n(Java API)"]
B2["libmedia_jni.so\n(JNI 桥接层)"]
end
subgraph C [本地框架层]
C1["libmedia.so\n(客户端)"]
end
subgraph D [多媒体服务层]
D1["MediaPlayerService\n(服务端)"]
D2["NuPlayer / StagefrightPlayer\n(引擎)"]
D3["OpenCore (旧版)"]
D4["Codec (H.264, AAC等)\n通过MediaCodec实现"]
end
B1 --> B2
B2 --> C1
C1 -- "Binder IPC" --> D1
D1 --> D2
D2 --> D4
D2 -.-> D3
1. 应用层
你的 App 直接调用 android.media.MediaPlayer 类提供的 start()、pause()、seekTo() 等 API。
2. 框架层 (Java & JNI)
- Java API :
frameworks/base/media/java/android/media/MediaPlayer.java提供了给开发者使用的接口。 - JNI :
frameworks/base/media/jni/android_media_MediaPlayer.cpp。Java 层的方法通过 JNI 调用到 Native 层。同时,libmedia_jni.so负责加载这个 JNI 库。
3. 本地框架层 (C++)
- libmedia.so : 这是 MediaPlayer 在客户端的 Native 部分。它通过 Binder(Android 的 IPC 机制)与远端的多媒体服务进行通信。你在 Java 层创建的
MediaPlayer对象,在 Native 层会对应创建一个sp<MediaPlayer>的 C++ 对象。
4. 多媒体服务层 (C++)
- MediaPlayerService : 运行在一个独立的守护进程 (
mediaserver) 中,是所有多媒体播放操作的服务端。 - 播放引擎 : 早期 Android 使用 OpenCore (PacketVideo) 作为底层引擎。从 Android 2.3 开始,逐步替换为 Google 自研的 Stagefright 引擎,现在则主要使用更现代的 NuPlayer(特别是在 Android L 之后)。这些引擎负责解析容器格式(如 MP4、MKV)和驱动解码器。
- 解码器 : 最终通过
MediaCodec调用硬件或软件解码器,将压缩的数据解码为原始 PCM 音频或 YUV 视频帧。
二、核心原理:状态机与音视频同步
1. 状态机
MediaPlayer 的内部是一个严格的状态机,这是理解它的关键。几乎所有 API 都只能在特定状态下调用,否则会抛出异常。
下图展示了 MediaPlayer 的生命周期状态变迁:
stateDiagram-v2
[*] --> Idle: reset()
Idle --> Initialized: setDataSource()
Initialized --> Preparing: prepareAsync()
Preparing --> Prepared: 准备完成(异步)
Initialized --> Prepared: prepare()(同步)
Prepared --> Started: start()
Started --> Started: seekTo() (播放中跳转)
Started --> Paused: pause()
Paused --> Started: start() (继续)
Started --> PlaybackCompleted: 播放完成
PlaybackCompleted --> Started: start() (重新播放)
PlaybackCompleted --> Prepared: stop()
Paused --> Prepared: stop()
Started --> Prepared: stop()
Prepared --> Prepared: seekTo() (准备就绪后跳转)
Started --> Error: 错误发生
Paused --> Error: 错误发生
Error --> Idle: reset()
Error --> [*]: release()
PlaybackCompleted --> [*]: release()
Idle --> [*]: release()
Prepared --> [*]: release()
Started --> [*]: release()
Paused --> [*]: release()
- Idle 状态 :
new MediaPlayer()或reset()后的状态。 - Initialized 状态 :
setDataSource()成功后进入。 - Prepared 状态 :调用
prepareAsync()或prepare()成功后进入。必须进入 Prepared 状态,才能调用start()。 - Started 状态:正在播放。
- Paused 状态:暂停播放。
- Stop 状态 :停止后,不能直接
start(),需要重新prepare()。 - PlaybackCompleted 状态:文件正常播放完毕。
- Error 状态 :当发生错误时进入。可以通过
setOnErrorListener()监听,并通常需要调用reset()来恢复。
2. 音视频同步
MediaPlayer 底层引擎(如 NuPlayer)内部实现了复杂的音视频同步逻辑,通常遵循 "视频同步到音频" 的策略。它会将解码后的音频和视频帧的时间戳(PTS)与音频硬件时钟进行比较,动态决定是丢弃视频帧(落后时)还是重复渲染视频帧(超前时),以保证声画同步。
三、使用详解与代码示例
1. 基本使用流程
播放一个音频或视频,通常遵循以下标准流程:
第一步:创建 MediaPlayer 实例
java
MediaPlayer mediaPlayer = new MediaPlayer();
第二步:设置数据源与参数
java
try {
// 设置音频流类型(可选,但建议)
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
// 设置数据源:可以是本地路径、网络URL或FileDescriptor
mediaPlayer.setDataSource("http://example.com/music.mp3");
// 或者 mediaPlayer.setDataSource("/sdcard/music.mp3");
} catch (IOException e) {
e.printStackTrace();
}
第三步:准备播放器
-
同步准备 (
prepare()) :用于本地文件,会阻塞 UI 线程,直到准备完成。javamediaPlayer.prepare(); // 可能耗时 -
异步准备 (
prepareAsync()) :用于网络流媒体或不希望阻塞 UI 线程的场景 ,通过监听器获取结果。javamediaPlayer.prepareAsync(); mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { // 准备完成,可以开始播放了 mp.start(); } });
第四步:控制播放
java
// 开始/继续
mediaPlayer.start();
// 暂停
mediaPlayer.pause();
// 跳转 (单位:毫秒)
mediaPlayer.seekTo(60000);
// 停止
mediaPlayer.stop();
第五步:释放资源
java
@Override
protected void onDestroy() {
super.onDestroy();
if (mediaPlayer != null) {
mediaPlayer.release(); // 必须调用,释放解码器等底层资源
mediaPlayer = null;
}
}
2. 不同场景的数据源设置
| 场景 | 代码示例 | 说明 |
|---|---|---|
| 播放 Raw 资源 | MediaPlayer.create(context, R.raw.sound); |
create() 方法会自动执行 prepare,直接 start() 即可。 |
| 播放本地文件 | setDataSource("/sdcard/music.mp3"); |
需要申请 READ_EXTERNAL_STORAGE 权限(Android 10 及以下)。 |
| 播放网络流 | setDataSource("http://example.com/stream.mp3"); |
需要 INTERNET 权限,且必须使用 prepareAsync()。支持渐进式下载。 |
| 播放 assets 文件 | AssetFileDescriptor fd = getAssets().openFd("music.mp3"); setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength()); |
通过 FileDescriptor 设置。 |
3. 重要的事件监听
为了构建一个健壮的播放器,务必实现以下监听器:
setOnPreparedListener(): 异步准备完成。setOnCompletionListener(): 播放结束。setOnErrorListener(): 必须实现。当发生错误时,在这里进行重试或重置操作。setOnSeekCompleteListener(): 跳转完成。setOnInfoListener(): 接收缓冲开始/结束等提示信息。
四、MediaPlayer 的完整工作流程
将上述所有环节串联起来,一个完整的播放请求流程如下:
- 应用发起请求 : App 调用
mediaPlayer.start()。 - JNI 转发 : Java 调用通过 JNI 进入
libmedia_jni.so。 - Client IPC 调用 :
libmedia.so(Client) 将start命令通过 Binder 发送给mediaserver进程中的MediaPlayerService(Server)。 - 服务端处理 :
MediaPlayerService将命令转发给当前活跃的播放引擎(如 NuPlayer)。 - 引擎驱动解码 : NuPlayer 从
MediaExtractor(解封装器)获取数据,然后将压缩的数据包(ES 数据)发送给MediaCodec进行解码。 - 数据渲染 :
- 音频 : 解码后的 PCM 数据被送入
AudioTrack,最终通过 AudioFlinger 混音后输出到扬声器或耳机。 - 视频 : 解码后的视频帧被送到
SurfaceFlinger进行合成,最后显示在SurfaceView或TextureView上。
- 音频 : 解码后的 PCM 数据被送入
- 状态回调 : 底层的状态变化(如播放完成、缓冲中、错误)会沿着
MediaPlayerService->libmedia.so-> JNI -> Java 监听器 的路径回调给应用层。