MediaPlayer介绍

Android 多媒体框架

Android 多媒体开发涉及的内容非常广泛且深入,主要包括以下几个核心方面:

1. 媒体播放 (Playback):

  • 核心组件: MediaPlayer, MediaCodec, SurfaceView, TextureView, ExoPlayer(Google推荐的强大开源库)
  • 关键功能:
    本地文件、资源文件、网络流(HTTP, RTSP等)的播放
    播放控制(开始、暂停、停止、跳转、循环)
    音频焦点管理(处理多个应用同时播放声音)
    音频特效(如均衡器、响度控制)
    显示视频

2. 媒体录制 (Recording):

  • 核心组件: MediaRecorder, AudioRecord, Camera API, Camera2 API, CameraX(Jetpack库,简化相机开发)
  • 关键功能:
    音频录制(麦克风输入,支持不同格式/质量)
    视频录制(使用相机,设置分辨率、帧率、比特率)
    音频源选择(麦克风、通话音频等)
    录像文件格式(MP4, 3GP等)

3. 媒体存储与访问 (Storage & Access):

  • 核心机制: ContentProvider, MediaStore, FileProvider, Storage Access Framework

4. 媒体编解码与处理 (Codec & Processing):

  • 核心组件 MediaCodec, MediaFormat, MediaExtractor, MediaMuxer, OpenGL ES, NDK, FFmpeg
  • 关键功能 :
    音视频编解码/转码
    媒体容器复用解复用(MediaExtractor/MediaMuxer)
    音视频处理(滤镜/变声)
    能处理方案(NDK/OpenGL ES)
    方库集成(FFmpeg)

5. 图像处理与显示:
6. 音频管理 (Audio Management)

MediaPlayer在Android多媒体中角色

文章主要介绍Android 的媒体播放内容,首先就先介绍开发者最常接触的MeidaPlayer。 对于App开发者或初学者来说可能会认为MeidaPlayer就是播放器。严格来说,MeidaPlayer 仅仅是Android提供给app调用的api

什么是播放器

我们先来了解下什么是播放器,MediaPlayer跟我常说的播放器有什么关系

"播放器"是一个广泛使用的概念,其核心含义是:​​一个能够读取、解码、渲染并输出音频或视频内容的软件或硬件系统。播放器的实现是比较复杂的,但不管怎么复杂,主要的核心功能都是以下几个方面

  • ​解析​ 媒体文件或网络流。
  • ​解封装​ 解析封装格式,提取对应的音视频数据,字幕等。
  • ​解码​ 压缩的音视频数据。
  • ​音频渲染:​ 将解码后的音频 PCM 数据送给系统的音频输出设备(扬声器、耳机)。
  • ​视频渲染(如果需要):​ 将解码后的视频帧发送到你提供的 Surface(通常是 SurfaceViewTextureView)。视频播放时,真正的渲染 发生在你设置的 Surface之上,但解码和帧同步等核心播放逻辑仍在底层引擎中进行。
  • 管理网络 ​缓冲​​同步​ 音视频、处理 ​DRM​ 等复杂任务

MediaPlayer跟播放器的关系

MediaPlayer是 "播放器" 的标准化接口和控制器, 而真正实现播放器功能的是位于native层的nuplayer。可以看到要实现整个播放功能涉及很多模块,关于这部分内容会在其他章节介绍

MediaPlayer使用

其实理解Android MediaPlayer最直接的方法是看google的官方文档 developer.android.google.cn/reference/a... 因为已经有大量的文章转载和翻译,所以这里暂时不做介绍和翻译,可能会在后续实现接口的过程中引用和参考里面的内容

学习MediaPlayer主要是两方面,一是熟悉api,二是状态机

核心使用步骤流程:​

使用 MediaPlayer需要遵循一个明确的生命周期状态机。主要步骤包括:

  1. ​创建实例:​ ​ 初始化 MediaPlayer对象。

    ini 复制代码
    MediaPlayer mediaPlayer = new MediaPlayer();
  2. ​设置数据源:​ ​ 告知 MediaPlayer媒体内容的来源。这是关键的分水岭,决定了后续操作。

    • ​本地文件:​

      go 复制代码
      mediaPlayer.setDataSource("/sdcard/music.mp3"); // 需要文件读写权限
      // 或者通过 FileDescriptor (更推荐用于res/raw)
      FileInputStream fis = new FileInputStream(new File("/sdcard/video.mp4"));
      mediaPlayer.setDataSource(fis.getFD());
      fis.close(); // 注意关闭流
    • ​应用资源 (raw 或 assets):​

      ini 复制代码
      // 1. res/raw 资源 (如 sound.mp3)
      Uri myUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.sound);
      mediaPlayer.setDataSource(context, myUri); // context 是有效的 Context 对象
      
      // 2. assets 资源 (需使用 AssetFileDescriptor)
      AssetFileDescriptor afd = getAssets().openFd("sound.mp3");
      mediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
      afd.close();
    • ​网络流媒体:​

      arduino 复制代码
      mediaPlayer.setDataSource("https://example.com/stream.mp3"); // 需要 INTERNET 权限
      // 注意:网络播放需要使用 prepareAsync()!
    • ​ContentProvider URI:​

      ini 复制代码
      Uri contentUri = ...; // e.g., from MediaStore
      mediaPlayer.setDataSource(context, contentUri);
  3. ​准备播放:​ ​ 让 MediaPlayer解析媒体格式、分配资源、连接解码器。​​绝对不要在 UI 线程用同步 prepare()加载大文件或网络源!​

    • ​同步准备 (适用于小文件或res/raw):​

      scss 复制代码
      try {
          mediaPlayer.prepare(); // 阻塞调用,直到准备好或出错
      } catch (IOException e) {
          e.printStackTrace(); // 处理异常
      }
    • ​异步准备 (强烈推荐,尤其网络或大文件):​

      typescript 复制代码
      mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
          @Override
          public void onPrepared(MediaPlayer mp) {
              // 准备完成,可以开始播放了
              mp.start();
          }
      });
      mediaPlayer.prepareAsync(); // 非阻塞,立即返回。准备完成会回调 onPrepared
  4. ​(视频专属) 设置显示 Surface:​​ 视频播放需要一个可视化的容器。

    java 复制代码
    // 通常在 SurfaceView 或 TextureView 的 SurfaceHolder.Callback 中设置
    SurfaceHolder holder = surfaceView.getHolder();
    holder.addCallback(new SurfaceHolder.Callback() {
        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            if (mediaPlayer != null) {
                mediaPlayer.setDisplay(holder); // 将 MediaPlayer 绑定到 SurfaceView
                // 如果已设置数据源但未prepare,可以在此时 prepareAsync()
            }
        }
        // ... surfaceChanged, surfaceDestroyed 略
    });
    • 也可以在 onPrepared之后设置: mediaPlayer.setDisplay(holder);
  5. ​开始播放:​

    scss 复制代码
    mediaPlayer.start(); // 播放或从暂停状态恢复
  6. ​控制播放:​

    scss 复制代码
    mediaPlayer.pause(); // 暂停播放
    mediaPlayer.seekTo(msec); // 跳转到指定毫秒位置 (如 seekTo(30000) => 跳到30秒)
    mediaPlayer.stop(); // 停止播放。注意:stop()后必须重新 prepare() 才能再 start()
    boolean isPlaying = mediaPlayer.isPlaying(); // 检查是否正在播放
    int currentPosition = mediaPlayer.getCurrentPosition(); // 获取当前位置(ms)
    int duration = mediaPlayer.getDuration(); // 获取总时长(ms), -1 表示未知(如直播流)
    mediaPlayer.setLooping(true); // 设置循环播放
    mediaPlayer.setVolume(leftVolume, rightVolume); // 设置音量 (0.0f - 1.0f)
  7. ​设置事件监听器 (重要!):​

    • OnPreparedListener:​​ 异步准备完成时回调 (见步骤3)。

    • OnCompletionListener:​​ 媒体播放自然结束时回调。

      less 复制代码
      mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
          @Override
          public void onCompletion(MediaPlayer mp) {
              // 播放结束逻辑,如播放下一个、重置进度条、释放播放器等
          }
      });
    • OnErrorListener:​ ​ 发生错误时回调。​​返回 true 表示错误已处理,不会触发默认错误行为;返回 false 会触发 OnCompletionListener。​

      java 复制代码
      mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
          @Override
          public boolean onError(MediaPlayer mp, int what, int extra) {
              Log.e("MediaPlayer", "Error! what=" + what + ", extra=" + extra);
              // 分析错误 (what: MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_IO 等; extra: 更具体错误码)
              // 尝试恢复(如release后重新初始化)或告知用户
              return true; // 处理了错误
          }
      });
    • OnBufferingUpdateListener(网络流重要):​​ 报告缓冲进度百分比。

      java 复制代码
      mediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
          @Override
          public void onBufferingUpdate(MediaPlayer mp, int percent) {
              // percent: 0-100, 表示缓冲完成的百分比
              // 可以在 UI 上显示缓冲进度
          }
      });
    • OnSeekCompleteListener:​seekTo()操作完成时回调。

  8. ​状态管理和重置:​

    • reset():​ ​ 将播放器重置回空闲(Idle)状态(类似于刚创建时)。这会清除数据源和当前状态。通常在播放不同文件前调用。

      scss 复制代码
      mediaPlayer.reset();
      // 之后可以重新 setDataSource 和 prepare
  9. ​释放资源 (绝对重要!!!):​ ​ 当播放器不再需要时(例如在 Activity 的 onDestroy()、Fragment 的 onDestroyView()中),​​必须​ ​调用 release()释放它持有的所有资源(解码器、内存、网络连接等)。不释放会导致严重的内存泄漏甚至应用崩溃。

    typescript 复制代码
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mediaPlayer != null) {
            mediaPlayer.release(); // 这是关键!!!
            mediaPlayer = null; // 防止后续误操作
        }
    }

核心状态机(必须理解):

MediaPlayer的内部状态决定了你能做什么操作。比如 setDataSource()只能在 IdleInitialized状态之间转换,start()只能在 PreparedStartedPausedPlaybackCompleted状态调用。

简单理解主要状态:

  1. ​Idle (空闲):​ 刚创建或 reset()后。
  2. ​Initialized (已初始化):​ 成功调用 setDataSource()后。
  3. ​Preparing (准备中):​ prepareAsync()调用后,准备完成前。
  4. ​Prepared (已准备):​ prepare()prepareAsync()成功完成 (onPrepared回调)。
  5. ​Started (已开始):​ start()调用后。
  6. ​Paused (已暂停):​ pause()调用后(需处于 Started状态)。
  7. ​PlaybackCompleted (播放完成):​ 播放自然结束 (onCompletion回调),但 looping=true时会自动重新开始。
  8. ​Stopped (已停止):​ stop()调用后(需处于 PreparedStartedPausedPlaybackCompleted状态)。​必须重新 prepare()才能再次 start()。​
  9. ​Error (错误):​ 发生错误时。​唯一方法恢复是 reset()。​
  10. ​End (结束):​ release()调用后。对象完全失效

最佳实践与注意事项:​

  1. ​始终异步准备:​ ​ 对除小型 res/raw 文件外的所有情况,优先使用 prepareAsync()+ OnPreparedListener,避免阻塞 UI 线程导致 ANR。

  2. ​及时释放资源:​release()是​​强制性的且至关重要的​​!在任何可能不再需要播放器的地方(尤其是 Activity/Fragment 销毁时)调用它。

  3. ​错误处理:​ ​ 总是设置 OnErrorListener以优雅处理故障。记录错误信息并向用户提供反馈。

  4. ​网络播放:​

    • ​必须 prepareAsync()。​
    • 设置 OnBufferingUpdateListener更新 UI。
    • 处理网络错误和重试逻辑。
    • 考虑缓冲策略(在 setDataSource前可设置 mediaPlayer.setBuffer(MediaPlayer.BUFFER_CACHE_ALL)等模式)。
  5. ​视频播放:​ ​ 确保在 Surface (通过 setDisplay) 有效可用后再开始播放或准备。在 surfaceDestroyed回调中暂停播放或释放播放器。

  6. ​权限:​ ​ 访问网络 ( INTERNET) 或本地文件 ( READ_EXTERNAL_STORAGE/ WRITE_EXTERNAL_STORAGE) 需要相应权限。

  7. ​主线程限制:​ ​ 虽然 MediaPlayer方法本身可以从主线程调用,但涉及文件 IO 或网络的操作(如 setDataSourceprepare)的实际工作可能会在后台线程进行(尤其是使用 prepareAsync时)。重要的是不要在主线程使用阻塞性的 prepare()

下面是一个简单的播放本地视频的demo

Activity代码如下:

java 复制代码
public class MainActivity extends Activity  implements SurfaceHolder.Callback {
    private MediaPlayer mediaPlayer;
    private SurfaceView surfaceView;
    private SurfaceHolder surfaceHolder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        View view = getWindow().getDecorView();
        view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN
                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_IMMERSIVE);

        surfaceView = findViewById(R.id.sv2);
        surfaceHolder = surfaceView.getHolder();
        surfaceHolder.addCallback(this);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        try {
            // 初始化MediaPlayer
            mediaPlayer = new MediaPlayer();
            // 设置数据源:从assets中读取视频文件
            AssetFileDescriptor afd = getAssets().openFd("waves.mp4");
            mediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
            afd.close();

            // 设置显示在SurfaceView上
            mediaPlayer.setDisplay(holder);
            mediaPlayer.setLooping(true);
            mediaPlayer.prepareAsync(); // 异步准备,避免阻塞UI线程

            // 设置准备完成的监听
            mediaPlayer.setOnPreparedListener(mp -> {
                // 获取视频的宽高
                int videoWidth = mp.getVideoWidth();
                int videoHeight = mp.getVideoHeight();
                // 根据视频宽高调整SurfaceView的布局参数(可选,这里全屏播放,可以不做调整)
                // 开始播放
                mp.start();
            });

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        // 当Surface发生变化时,可以在此处调整
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        // 当Surface销毁时,释放MediaPlayer资源
        if (mediaPlayer != null) {
            mediaPlayer.release();
            mediaPlayer = null;
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        // 在Activity暂停时暂停播放,并释放资源(也可以选择暂停而不释放)
        if (mediaPlayer != null && mediaPlayer.isPlaying()) {
            mediaPlayer.pause();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mediaPlayer != null) {
            mediaPlayer.release();
            mediaPlayer = null;
        }
    }

xml文件如下

js 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity" >
    <SurfaceView
        android:id="@+id/sv2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        />

</LinearLayout>

Android MediaPlayer 的局限性

虽然 MediaPlayer是 Android 框架内置、易于上手的基础组件,适用于简单场景,但它存在一系列显著的限制,特别是在构建现代、高性能或功能丰富的媒体应用时会显得捉襟见肘:

  1. ​对现代自适应流媒体 (ABR) 支持不足或不灵活:​

    • MediaPlayer对 ​​DASH​ ​ 和 ​​HLS​ ​ 这类主流自适应比特率流协议的支持是​​基础且黑盒​​的。开发者很难或无法:

      • ​精细控制自适应逻辑:​ 无法自定义在不同网络条件下如何选择不同码率的轨道(视频、音频)。
      • ​访问轨道信息:​ 难以获取流中可用的音轨、视频轨、字幕轨及其属性列表。
      • ​手动切换轨道:​ 无法在播放过程中根据应用逻辑(如用户选择或特定条件)精确切换到指定的音轨、视频轨或字幕轨。
      • ​响应式调整策略:​ 对网络波动、缓冲预测的自适应策略不够透明或可定制。
  2. ​功能相对有限和定制性差:​

    • ​细粒度控制缺失:​ 缺乏对诸如精确下载管理(只下载特定部分)、边播边存、播放速度平滑过渡等高级功能的原生支持。
    • ​渲染和输出定制困难:​ 想要自定义视频渲染(如添加滤镜、水印、特效)或进行高级音频处理(如均衡器、音效),操作非常复杂且受限。它主要通过 SurfaceViewTextureView输出视频,对底层渲染管道控制很少。
    • ​拼接与编辑功能弱:​ 实现多个片段无缝拼接、复杂的剪辑播放(如播放列表不同片段的交叉淡化)等功能非常笨拙或无法实现。
    • ​可扩展性不足:​ 很难集成自定义的容器格式解析器 (Extractor)、数据源 (DataSource) 或渲染器 (Renderer)。
  3. ​DRM (数字版权管理) 支持有限且复杂:​

    • 支持 Widevine 等 DRM 方案,但集成和使用通常较为繁琐。
    • 对于需要高级 DRM 功能或需要支持多种 DRM 系统的复杂应用,其接口和流程不够友好和强大。

主流替代方案

鉴于 MediaPlayer的局限性,以下替代方案在 Android 开发中得到了广泛应用和推荐:

  1. ​ExoPlayer (官方推荐)​​:

    • ​开发者:​​ Google 开源。

    • ​定位:​MediaPlayer的高性能、可扩展性、功能强大的现代替代品。​​是目前 Android 平台最主流的媒体播放库之一。​

    • ​核心优势:​

      • ​强大的流媒体支持:​ 对 DASH, HLS, SmoothStreaming 提供了​一流的、透明且可定制​的支持。开发者可以深度控制 ABR 策略、轨道选择等。
      • ​高度模块化:​ 采用组件化设计 (DataSource, Extractor, Renderer),开发者可以轻松替换或扩展各个部分(如自定义数据源、处理新格式、添加滤镜)。
      • ​丰富的功能集:​ 内置支持背景播放、广告插入、无缝拼接、边播边存、高级事件分析、DRM (Widevine, Clearkey, PlayReady)、自定义负载请求头等。
      • ​更好的性能与缓冲控制:​ 提供更多可配置的缓冲参数和更智能的缓冲算法,提升弱网体验。
      • ​更清晰现代的 API 和文档:​ 设计更符合现代开发习惯,文档和社区资源非常丰富。
      • ​持续更新:​ Google 和社区持续投入,快速适配新平台特性和媒体格式。
      • ​透明性:​ 开源且高度可定制,开发者可以深入了解其运行机制。
      • ​兼容性好:​ 在 API Level 16 (Android 4.1, Jelly Bean) 及以上的设备上运行良好。
    • ​缺点:​

      • 需要作为依赖库引入项目(增加 APK 尺寸)。
      • 入门学习曲线相对 MediaPlayer稍陡峭(但文档齐全)。
      • 对于​极其简单​的本地音频播放需求(如按钮音效),可能显得有些"重"(但通常仍是更优选择)。
  2. ​Jetpack Media3 (官方现代集成)​​:

    • ​开发者:​​ Google (Android Jetpack 组件之一)。

    • ​定位:​ExoPlayer的现代化包装库,旨在​​统一媒体播放、媒体会话控制和 Cast (Chromecast) 集成​​的开发者体验。它代表了 Google 推荐的未来发展方向。

    • ​核心优势:​

      • ​以 ExoPlayer 为核心引擎:​ 直接内置或依赖于 ExoPlayer提供强大的播放能力,继承了其几乎所有优势。
      • ​简洁一致的 API (Player接口):​ 提供了 MediaPlayerExoPlayer接口的​抽象层​ ,让开发者使用一套更现代化、更符合 Kotlin 习惯的 API 来操作不同的播放器实现(目前主要是 ExoPlayer)。
      • ​与系统深度集成:​ 简化了与 MediaSession的集成,用于系统控件(通知栏、蓝牙设备、锁屏控件)、后台播放和与其他应用共享媒体控制权。同时也简化了 Chromecast 集成。
      • ​推荐实践:​ 代表了 Google 在媒体播放、媒体浏览器服务、会话控制等方面的最新最佳实践。
    • ​组件:​ ​ 包含 media3-exoplayer(播放核心), media3-ui(预制 UI 控件), media3-session(媒体会话), media3-cast(Chromecast), media3-transformer(媒体转换) 等库。

    • ​适用场景:​ ​ ​​对于新项目或想要升级现有播放功能到最新标准、并且需要后台控制/投屏的应用,Media3是首选推荐。​

  3. ​其他第三方播放器 SDK​​:

    • ​如 VLC for Android 的 LibVLC:​ 开源,基于知名的 VLC 播放器核心,支持​极其广泛的媒体格式和协议​ 。优势在于超强的兼容性(尤其在处理怪异格式时)。但集成文档、社区支持可能不如 ExoPlayer/Media3丰富,API 风格也不同。
    • ​商业 SDK:​ 市场上存在一些商业播放器 SDK,它们可能在特定功能(如特定的 DRM 方案支持、高级分析、专业级的播放性能优化、客户支持)上有所特长。选择商业 SDK 需要权衡成本和特定需求。
相关推荐
77qqqiqi24 分钟前
解决忘记修改配置密码而无法连接nacos的问题
java·数据库·docker·微服务
ALLSectorSorft37 分钟前
相亲小程序用户注册与登录系统模块搭建
java·大数据·服务器·数据库·python
琢磨先生David1 小时前
Java 垃圾回收机制:自动化内存管理的艺术与科学
java
岁忧1 小时前
(nice!!!)(LeetCode 每日一题) 2561. 重排水果 (哈希表 + 贪心)
java·c++·算法·leetcode·go·散列表
阿华的代码王国1 小时前
【Android】RecyclerView实现新闻列表布局(1)适配器使用相关问题
android·xml·java·前端·后端
码农BookSea2 小时前
自研 DSL 神器:万字拆解 ANTLR 4 核心原理与高级应用
java·后端
lovebugs2 小时前
Java并发编程:深入理解volatile与指令重排
java·后端·面试
慕y2742 小时前
Java学习第九十一部分——OkHttp
java·开发语言·学习
caisexi2 小时前
Windows批量启动java服务bat脚本
java·windows·python
NullPointerExpection2 小时前
win10 环境删除文件提示文件被使用无法删除怎么办?
java·ide·windows·win10