Android 音频开发之 MediaPlayer

虽然直播落幕,但 Android 的音、视频技术仍然倍受关注!

一、引言

Android 提供了常见的音频、视频的编码、解码机制。借助于多媒体类 MediaPlayer 的支持,开发人员可以很方便地在应用中播放音频、视频。只不过使用 MediaPlayer 播放视频时,没有提供图像输出界面。

二、MediaPlayer 概述

Android 框架中使用以下类播放音频和视频:

MediaPlayer : 这个类是播放音频和视频的主要API

AudioManager : 该类管理设备上的音频源和音频输出

Android 下对于音频、视频的支持均需要使用到 MediaPlayer,它主要用来控制 Android 下播放文件或流的类。MediaPlayer 处于 Android 多媒体包下 "android.media.MediaPlayer",仅有一个无参的构造函数,虽然仅为我们提供了一个无参的构造函数,但为了开发方便,还为我们提供了几个静态的 create() 方法用于完成MediaPlayer 初始化的工作。

static MediaPlayer create(Context context,int resid):通过音频资源的 Id 来创建一个 MediaPlayer 实例
static MediaPlayer create(Context context,Uri uri):通过一个音频资源的 Uri 地址来创建一个 MediaPlayer 实例

MediaPlayer 除了通过上面两个 create() 方法在初始化的时候指定媒体资源,还可以通过 MediaPlayer.setDataSource() 方法为初始化后的 MediaPlayer 设置媒体资源,setDataSource() 具有多个重载函数,适用于不同的媒体资源来源,以下讲解几个常用的,其他的可以查阅官方文档。

  • void setDataSource(String path):通过一个媒体资源的地址指定 MediaPlayer 的数据源,这里的 path 可以是一个本地路径,也可以是网络路径
  • void setDataSource(Context context,Uri uri):通过一个 Uri 指定 MediaPlayer 的数据源,这里的 Uri 可以是网络路径或这一个内容提供者的 Uri
  • void setDataSource(FileDescriptor fd):通过一个 FileDescriptor 指定一个 MediaPlayer 的数据源

MediaPlayer 支持的数据源有:本地文件、内部的 Uri(内容提供者)、外部 Uri。

三、清单声明

在使用 MediaPlayer 对应用程序进行开发之前,请确保清单中有适当的声明,允许使用相关特性。

Internet 权限——如果您正在使用 MediaPlayer 来播放流基于网络的内容,那么应用程序必须请求网络访问。

<uses-permission android:name="android.permission.INTERNET" />

Wake Lock 权限——如果您的播放器应用程序需要阻止屏幕变暗或处理器休眠,或 MediaPlayer.setWakeMode() 方法,必须请求此权限。

<uses-permission android:name="android.permission.WAKE_LOCK" />

四、MediaPlayer 状态管理

MediaPlayer 状态机图

MediaPlayer 类中的文档显示了一个完整的状态机,它阐明了哪些方法将 MediaPlayer 从一个状态移动到另一个状态。例如,当您创建一个新的 MediaPlayer 时,它处于空闲状态。这时,您应该通过调用 setDataSource() 来初始化它,使它处于初始化状态。之后,您必须使用 prepare() 或 prepareAsync() 方法来准备它。当 MediaPlayer 完成准备工作时,它进入准备状态,这意味着您可以调用 start() 来让它播放媒体。此时,您可以通过调用 start()、pause() 和 seekTo() 等方法在 start、pause() 和 PlaybackCompleted 状态之间切换。但是,当您调用 stop() 时,请注意,在重新准备 MediaPlayer 之前,您不能再次调用 start()。

MediaPlayer 是基于状态的。也就是说,MediaPlayer 有一个内部状态,与 MediaPlayer 对象交互的代码时,一定要记住状态图,在编写代码时必须始终注意到,因为只有当 player 处于特定状态时,某些操作才有效。如果在错误的状态下执行操作,系统可能会抛出异常或引发其他不希望看到的行为。

五、使用 MediaPlayer 播放音乐

MediaPlayer 其实是一个封装的很好的音频、视频流媒体操作类,其内部是调用的 native 方法,所以它其实是有 C++ 实现的。既然是一个流媒体操作类,那么必然涉及到,播放、暂停、停止等操作,实际上 MediaPlayer 也为我们提供了相应的方法来直接操作流媒体。

  • void start():开始或恢复播放
  • void stop():停止播放
  • void pause():暂停播放

通过上面三个方法,只要设定好流媒体数据源,即可在应用中播放流媒体资源,为了更好的操作流媒体,MediaPlayer 还为我们提供了一些其他的方法,这里列出一些常用的,详细内容参阅官方文档。

  • int getDuration():获取流媒体的总播放时长,单位是毫秒
  • int getCurrentPosition():获取当前流媒体的播放的位置,单位是毫秒
  • void seekTo(int msec):设置当前MediaPlayer的播放位置,单位是毫秒
  • void setLooping(boolean looping):设置是否循环播放
  • boolean isLooping():判断是否循环播放
  • boolean isPlaying():判断是否正在播放
  • void prepare():同步的方式装载流媒体文件
  • void prepareAsync():异步的方式装载流媒体文件
  • void release ():回收流媒体资源
  • void setAudioStreamType(int streamtype):设置播放流媒体类型
  • void setWakeMode(Context context, int mode):设置 CPU 唤醒的状态
  • setNextMediaPlayer(MediaPlayer next):设置当前流媒体播放完毕,下一个播放的 MediaPlayer

大部分方法看方法名就可以理解,但是有几个方法需要单独说明一下。

在使用 MediaPlayer 播放一段流媒体的时候,需要使用 prepare() 或 prepareAsync() 方法把流媒体装载进 MediaPlayer,才可以调用 start() 方法播放流媒体。

setAudioStreamType() 方法用于指定播放流媒体的类型,它传递的是一个 int 类型的数据,均以常量定义在 AudioManager 类中, 一般我们播放音频文件,设置为AudioManager.STREAM_MUSIC 即可。

除了上面介绍的一些方法外,MediaPlayer 还提供了一些事件的回调函数,这里介绍几个常用的:

  • setOnCompletionListener(MediaPlayer.OnCompletionListener listener):当流媒体播放完毕的时候回调
  • setOnErrorListener(MediaPlayer.OnErrorListener listener):当播放中发生错误的时候回调
  • setOnPreparedListener(MediaPlayer.OnPreparedListener listener):当装载流媒体完毕的时候回调
  • setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener listener):当使用 seekTo() 设置播放位置的时候回调

六、使用 MediaPlayer

媒体框架最重要的组件之一是 MediaPlayer 类,这个类的对象可以使用最少的设置获取、解码和播放音频和视频。它支持几种不同的媒体来源,如:

  • 本地资源
  • 内部 uri,例如您可能从 contentProvider 获得的 uri
  • 外部 url(流)

有关 Android 支持的媒体格式列表,请参阅支持的媒体格式页面。

下面是如何播放本地音频资源(保存在您的应用程序的 res/raw/目录中):

MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); // no need to call prepare(); create() does that for you

在本例中,“raw” 资源是系统不尝试以任何特定方式解析的文件。然而,这个资源的内容不应该是原始音频。它应该是一个以支持的格式之一适当编码和格式化的媒体文件。

6.1 同步方式

下面是您如何从系统中本地可用的 URI(例如,您通过内容解析器获得的 URI)进行播放:

Uri myUri = ....; // initialize Uri here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();

通过 HTTP 流媒体从远程 URL 播放如下:

String url = "http://........"; // your URL here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare(); // might take long! (for buffering, etc)
mediaPlayer.start();

注意:

  • 如果要通过一个 URL 来传输流媒体在线文件,该文件必须能够逐步下载
  • 在使用 setDataSource() 时,您必须捕获或传递 IllegalArgumentException 和 IOException,因为您引用的文件可能不存在

6.2 异步的准备

使用 MediaPlayer 原则上很简单。但是,需要记住的是,要将它正确地集成到典型的 Android 应用程序中,还需要做一些其他的事情。例如,prepare() 的调用可能需要很长时间执行,因为它可能涉及到获取和解码媒体数据。因此,就像任何需要很长时间才能执行的方法一样,永远不要从应用程序的UI线程调用它。这样做会导致 UI 挂起,直到方法返回,这是一种非常糟糕的用户体验,并可能导致 ANR(应用程序没有响应)错误。即使您希望您的资源能够快速加载,也要记住,在 UI 中任何需要超过十分之一秒才能响应的内容都会引起明显的暂停,并给用户留下您的应用程序很慢的印象。

为了避免挂起UI线程,生成另一个线程来准备 MediaPlayer,并在完成时通知主线程。然而,虽然您可以自己编写线程逻辑,但是在使用 MediaPlayer 时,这种模式非常常见,因此框架提供了一种方便的方法来通过使用 prepareAsync() 方法来完成此任务。该方法开始在后台准备媒体并立即返回。当媒体完成准备工作时,将调用 通过 setOnPreparedListener() 配置的 MediaPlayer.OnPreparedListener() 的 onPrepared() 方法。

6.3 释放媒体播放器

MediaPlayer 可能会消耗有价值的系统资源。因此,您应该始终采取额外的预防措施,以确保您没有过多地依赖 MediaPlayer 实例。处理完它之后,应该始终调用 release(),以确保分配给它的任何系统资源都被正确释放。例如,如果您使用的是一个媒体播放器和活动接收 onStop() 调用,您必须释放媒体播放器,因为当你的活动不与用户进行交互,继续持有实例毫无意义(除非你是在后台播放媒体)。当您的活动恢复或重新启动时,当然,您需要创建一个新的 MediaPlayer,并在恢复回放之前重新准备。

下面是应该如何释放并取消 MediaPlayer:

mediaPlayer.release();
mediaPlayer = null;

作为一个例子,考虑一下如果您在活动停止时忘记释放 MediaPlayer,而在活动重新开始时创建一个新的,可能会发生的问题。正如你可能知道的,当用户更改屏幕的方向(或更改设备配置以另一种方式),系统处理,通过重新启动活动(默认情况下),所以你可能会很快消耗掉所有系统资源的用户旋转设备之间来回的肖像和风景,因为在每一个方向变化,您创建一个新的媒体播放器,你永远不会释放。

七、在服务中使用 MediaPlayer

如果您想要您的媒体在后台播放,即使您的应用程序不是在屏幕上——也就是说,您想要它在用户与其他应用程序交互时继续播放——那么您必须启动一个服务并从那里控制 MediaPlayer 实例。您需要将 MediaPlayer 嵌入到 MediaBrowserServiceCompat 服务中,并让它与另一个活动中的 MediaBrowserCompat 交互。

要小心这个 client/server 设置。人们对在后台服务中运行的播放器如何与系统的其他部分进行交互抱有期望。如果您的应用程序没有满足这些期望,用户可能会有一个糟糕的体验。阅读建立一个音频应用程序的完整细节。

7.1 异步运行

首先,与活动一样,服务中的所有工作在默认情况下都是在单个线程中完成的——事实上,如果您从同一个应用程序运行活动和服务,默认情况下它们使用相同的线程(“主线程”)。因此,服务需要快速处理传入意图,并且在响应它们时从不执行冗长的计算。如果预期有任何繁重的工作或阻塞调用,您必须异步执行这些任务:要么从另一个您自己实现的线程执行,要么使用框架的许多异步处理工具。

例如,在使用主线程中的 MediaPlayer 时,应该调用 prepareAsync() 而不是 prepare(),并实现 MediaPlayer.OnPreparedListener 目的是在准备完成后开始播放时得到通知。例如:

public class MyService extends Service implements MediaPlayer.OnPreparedListener {
    private static final String ACTION_PLAY = "com.example.action.PLAY";
    MediaPlayer mMediaPlayer = null;

    public int onStartCommand(Intent intent, int flags, int startId) {
        ...
        if (intent.getAction().equals(ACTION_PLAY)) {
            mMediaPlayer = ... // initialize it here
            mMediaPlayer.setOnPreparedListener(this);
            mMediaPlayer.prepareAsync(); // prepare async to not block main thread
        }
    }

    /** Called when MediaPlayer is ready */
    public void onPrepared(MediaPlayer player) {
        player.start();
    }
}

7.2 处理异步错误

在同步操作中,错误通常会以异常或错误代码发出信号,但无论何时使用异步资源,都应该确保将错误通知给应用程序。对于 MediaPlayer,您可以通过实现MediaPlayer.OnErrorListener 并将其设置到 MediaPlayer 实例中来解决该问题。

public class MyService extends Service implements MediaPlayer.OnErrorListener {
    MediaPlayer mMediaPlayer;

    public void initMediaPlayer() {
        // ...initialize the MediaPlayer here...
        mMediaPlayer.setOnErrorListener(this);
    }

    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        // ... react appropriately ...
        // The MediaPlayer has moved to the Error state, must be reset!
    }
}

要记住,当发生错误时,MediaPlayer 将切换到错误状态,必须在再次使用它之前重置它。

7.3 使用‘唤醒锁 wake locks’

当设计在后台播放媒体的应用程序时,设备可能会在服务运行时休眠。由于 Android 系统试图在设备处于休眠状态时节省电池,所以系统试图关闭手机的任何不必要的功能,包括 CPU 和WiFi 硬件。然而,如果您的服务正在播放或流媒体音乐,您希望防止系统干扰您的播放。
为了确保您的服务在这些条件下继续运行,您必须使用“唤醒锁”。唤醒锁是一种向系统发出信号的方式,即您的应用程序正在使用某些特性,即使手机处于空闲状态,这些特性也应该保持可用。

注意:你应该尽量少用唤醒锁,并且只在必要的时候使用它们,因为它们会大大减少设备的电池寿命。

要确保在 MediaPlayer 播放时 CPU 继续运行,在初始化 MediaPlayer 时调用 setWakeMode() 方法。一旦你这样做了,MediaPlayer 会在播放时持有指定的锁,并在暂停或停止时释放锁:

mMediaPlayer = new MediaPlayer(); // ... other initialization here ...
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);

但是,在本例中获得的唤醒锁只保证 CPU 保持清醒。如果你是通过网络流媒体,使用的是 Wi-Fi,你可能也想要一个 WifiLock,你必须手动获取和释放它。因此,当您开始使用远程 URL 准备 MediaPlayer 时,您应该创建并获得 Wi-Fi 锁。例如:

WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
    .createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock");
wifiLock.acquire();

当你暂停或停止你的媒体,或当你不再需要网络,你应该释放锁:

wifiLock.release();

7.4 执行清理

如前所述,MediaPlayer 对象可能会消耗大量的系统资源,所以您应该只在需要的时候使用它,并且在使用之后调用 release()。显式调用这种清理方法而不是依赖于系统垃圾收集是很重要的,因为垃圾收集器重新声明 MediaPlayer 可能需要一些时间,因为它只对内存需求敏感,而不缺乏其他与媒体相关的资源。因此,在使用服务时,您应该总是重写 onDestroy() 方法,以确保释放 MediaPlayer:

public class MyService extends Service {
   MediaPlayer mMediaPlayer;
   // ...

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

八、MediaPlayer 使用技巧

在使用 MediaPlayer 的使用过程中,有些小技巧需要说明一下:

8.1 在使用 start() 播放流媒体之前,需要装载流媒体资源

这里最好使用 prepareAsync() 用异步的方式装载流媒体资源。因为流媒体资源的装载是会消耗系统资源的,在一些硬件不理想的设备上,如果使用 prepare() 同步的方式装载资源,可能会造成 UI 界面的卡顿,这是非常影响用于体验的。因为推荐使用异步装载的方式,为了避免还没有装载完成就调用 start() 而报错的问题,需要绑定MediaPlayer.setOnPreparedListener() 事件,它将在异步装载完成之后回调。异步装载还有一个好处就是避免装载超时引发 ANR((Application Not Responding)错误。

    mediaPlayer = new MediaPlayer();
    mediaPlayer.setDataSource(path);
    mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); // 通过异步的方式装载媒体资源
    mediaPlayer.prepareAsync();
    mediaPlayer.setOnPreparedListener(new OnPreparedListener() { 
         @Override
        publicvoid onPrepared(MediaPlayer mp) {
            mediaPlayer.start();
         }
     });

8.2 使用完 MediaPlayer 需要回收资源

MediaPlayer 是很消耗系统资源的,所以在使用完 MediaPlayer,不要等待系统自动回收,最好是主动回收资源。

if (mediaPlayer != null && mediaPlayer.isPlaying()) {
    mediaPlayer.stop();
    mediaPlayer.release();
    mediaPlayer = null;
}

8.3 使用 MediaPlayer 最好使用一个Service来使用

实际上,就算是直接使用 Activity 承载 MediaPlayer,也最好在销毁的时候判断一下 MediaPlayer 是否被回收,如果未被回收,回收其资源,因为底层调用的 native 方法,如果不销毁还是会在底层继续播放,而承载的组件已经被销毁了,这个时候就无法获取到这个 MediaPlayer 进而控制它。

@Override
protectedvoid onDestroy() {
    if (mediaPlayer != null && mediaPlayer.isPlaying()) {
        mediaPlayer.stop();
        mediaPlayer.release();
        mediaPlayer = null;
    }

    super.onDestroy();
}

8.4 单曲循环之类的操作

对于单曲循环之类的操作,除了可以使用 setLooping() 方法进行设置之外,还可以为 MediaPlayer 注册回调函数,MediaPlayer.setOnCompletionListener(),它会在 MediaPlayer 播放完毕被回调。

// 设置循环播放
// mediaPlayer.setLooping(true);
mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
     @Override
    publicvoid onCompletion(MediaPlayer mp) {
        // 在播放完毕被回调
        play();
    }
});

8.5 流媒体播放异常处理

因为 MediaPlayer 一直操作的是一个流媒体,所以无可避免的可能一段流媒体资源,前半段可以正常播放,而中间一段因为解析或者源文件错误等问题,造成中间一段无法播放问题,需要我们处理这个错误,否则会影响 UX(用户体验)。可以为 MediaPlayer 注册回调函数 setOnErrorListener() 来设置出错之后的解决办法,一般重新播放或者播放下一个流媒体即可。

mediaPlayer.setOnErrorListener(new OnErrorListener() {
    @Override
    publicboolean onError(MediaPlayer mp, int what, int extra) {
        play();
        returnfalse;
    }
});

九、总结

Android 多媒体框架支持播放各种常见媒体类型,因此您可以轻松地将音频、视频和图像集成到应用程序中。您可以使用 MediaPlayer api 从存储在应用程序资源(原始资源)中的媒体文件、文件系统中的独立文件或通过网络连接到达的数据流中播放音频或视频。