Android MediaPlayer 笔记

一、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 线程,直到准备完成。

    java 复制代码
    mediaPlayer.prepare(); // 可能耗时
  • 异步准备 (prepareAsync())用于网络流媒体或不希望阻塞 UI 线程的场景 ,通过监听器获取结果。

    java 复制代码
    mediaPlayer.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 的完整工作流程

将上述所有环节串联起来,一个完整的播放请求流程如下:

  1. 应用发起请求 : App 调用 mediaPlayer.start()
  2. JNI 转发 : Java 调用通过 JNI 进入 libmedia_jni.so
  3. Client IPC 调用 : libmedia.so (Client) 将 start 命令通过 Binder 发送给 mediaserver 进程中的 MediaPlayerService (Server)。
  4. 服务端处理 : MediaPlayerService 将命令转发给当前活跃的播放引擎(如 NuPlayer)。
  5. 引擎驱动解码 : NuPlayer 从 MediaExtractor(解封装器)获取数据,然后将压缩的数据包(ES 数据)发送给 MediaCodec 进行解码。
  6. 数据渲染 :
    • 音频 : 解码后的 PCM 数据被送入 AudioTrack,最终通过 AudioFlinger 混音后输出到扬声器或耳机。
    • 视频 : 解码后的视频帧被送到 SurfaceFlinger 进行合成,最后显示在 SurfaceViewTextureView 上。
  7. 状态回调 : 底层的状态变化(如播放完成、缓冲中、错误)会沿着 MediaPlayerService -> libmedia.so -> JNI -> Java 监听器 的路径回调给应用层。
相关推荐
Jony_2 小时前
Android 启动优化方案
android
阿巴斯甜2 小时前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 小时前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_6 小时前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android
_小马快跑_6 小时前
Kotlin | 从SparseArray、ArrayMap的set操作符看类型检查的不同
android
_小马快跑_6 小时前
Android | 为什么有了ArrayMap还要再设计SparseArray?
android
_小马快跑_6 小时前
Android TextView图标对齐优化:使用LayerList精准控制drawable位置
android
_小马快跑_6 小时前
Kotlin协程并发控制:多线程环境下的顺序执行
android
_小马快跑_6 小时前
Kotlin协程异常捕获陷阱:try-catch捕获异常失败了?
android