to top

Media Playback (原文链接)

翻译:汉尼拔萝卜(https://github.com/gaojian3301)

Android 多媒体框架支持多种多样的多媒体文件,因此可以很轻松的将音频、视频、图片集成到应用中去。 通过使用 MediaPlayer APIs.

多媒体文件可以存储在应用程序的 raw 文件夹下,也可以存储在手机的文件系统中,甚至可以是来自与网络的流媒体。

这篇文章展示了如何写出一个性能不错,用户体验良好的多媒体用用程序。

注意: 你只能通过标准输出设备来播放音频文件。目前来讲,就是通过手机的扬声器和蓝牙耳机。 不能在通话时播放音频文件。

The Basics

下面的两个 class 在 Android 中被用来播放声音和视频:

MediaPlayer
这个类是播放音频和视频时使用最基本的 API.
AudioManager
这个类管理着设备上的音频源文件和音频输出。

Manifest Declarations

在使用 MediaPlayer 开发你的应用之前,必须在 mainfest 文件中声明需要使用哪些功能。

  • Internet Permission - 如果你打算使用 MediaPlayer 来播放网络流媒体内容,那么你的应用需要有这个权限
    <uses-permission android:name="android.permission.INTERNET" />
    
  • Wake Lock Permission - 如果你想要应用不灭屏或者进程不进入休眠状态,或者你像要在你的应用程序中使用 MediaPlayer.setScreenOnWhilePlaying() 方法或 MediaPlayer.setWakeMode() 方法, 你需要添加这个权限
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    

Using MediaPlayer

Android 多媒体框架中核心部分就是 MediaPlayer 这个类。通过它我们可以使用最少的步骤实现 音频和视频的获取、解码、播放。它支持下面几种多媒体资源:

  • 本地资源(我理解为只属于这个应用的)
  • Internal URIs, 比如可以通过 Content Resolver 获得的
  • External URLs (流媒体)

像了解更多 Android 的多媒体类型,请查看 Android Supported Media Formats

下面举个例子来展示如何播放本地(存储在 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” 资源文件。“raw” 不一定是音频文件,它可以是 Android 支持的多种多媒体格式中的一种。

接下来展示如何播放存储在手机中的 URI 资源文件(可以通过 Content Resolver 获取, 使用如下):

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

也可以像这样播放网络上的流媒体资源:

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();

Note: 如果想要解析一个网络上的 URL 资源文件,这个文件必须能够分段下载。

Caution: 当使用 setDataSource() 时必须要 catch 或者抛出 IllegalArgumentExceptionIOException 这两个异常,因为你想要获取的文件可能不存在。

Asynchronous Preparation

MediaPlayer 试用起来很简单。然而,当我们将它集成在应用中时,需要特别留意一些注意点。举个例子,prepare() 这个方法可能会执行很长时间,因为它会去获取并解析多媒体文件。所以这种耗时的操作,我们 千万不要在UI线程中去执行. 这样的操作会阻塞UI线程,这是非常差的用户体验,并且有可能造成程序ANR(应用程序无响应)。即使你觉得你的资源加载非常块,请记住任何花费 1/10 秒的操作都会造成可见的停滞,并且用户会觉得你的应用程序非常慢。

为了避免阻塞UI线程,可以开辟一个线程来准备 MediaPlayer,准备完成后通知UI线程。可以自己写线程实现这样的异步操作,然而 MediaPlayer 提供了 prepareAsync() 方法来让我们更加容易实现这样的逻辑。这个方法将在后台去准备多媒体的播放。当完成准备工作后,MediaPlayer.OnPreparedListener 接口中的 onPrepared() 方法将会执行,这个接口是通过 setOnPreparedListener() 方法来设置的。

Managing State

关于 MediaPlayer 需要记住的另一点是:它是基于状态的。也就是说,我们在写代码时必须要清楚 MediaPlayer 的状态,因为一些特定的操作只可以在特定的状态下才可以执行。如果你在某个状态下执行的错误的操作,系统可能会抛出异常或者造成其他不合理的行为。

MediaPlayer 类的文档展示了完整的状态图示,它清晰的描述了哪个方法能够改变 MediaPlayer 的状态。比如说,当创建了一个 MediaPlayer, 它处于 Idle 状态。这时候调用 setDataSource(), 将会进入 Initialized 状态。然后调用 prepare() 或者 prepareAsync() 方法进行初始化操作操作。当 MediaPlayer 完成初始化操作后,就进入了 Prepared 状态, 这时候就可以调用 start() 来播放了。这个状态下,可以通过调用 start(), pause(),和 seekTo() 去将状态改变为 Started, Paused and PlaybackCompleted。除非调用了 stop(), 否则不可以对同一个 MediaPlayer 调用多次 start()

在使用 MediaPlayer 开发应用程序时务必将 the state diagram 记在心里,因为在某个状态下调用了错误的方法将会造成bug.

Releasing the MediaPlayer

MediaPlayer 会消耗珍贵的系统资源。因此,必须要确保能够及时释放掉 MediaPlayer。当使用完 MediaPlayer 后,应该调用 release() 来确保分配的资源能够正确的得到释放。比如说,当我们在 Activity 中使用 MediaPlayer,当 Activity的 onStop() 方法被调用时, 这时候必须释放掉 MediaPlayer, 因为当 Activity 不再和用户交互时,维持着它的引用没有任何意义(除非在后台播放,这将在下一节讨论)。当 Activity resumed 或者 restarted, 必须创建新的 MediaPlayer ,准备好后才能重新播放。

下面是如何释放和销毁 MediaPlayer:

mediaPlayer.release();
mediaPlayer = null;

考虑下如果 activity stop时没有释放 MediaPlayer,再次启动 activity 又新建了一个,这会造成什么问题。你应该知道,当用户改变手机屏幕方向(或者以其他方式改变设备参数),系统会重新启动 activity(Android 默认行为)。当不停旋转屏幕时,系统资源将会很快被消耗殆尽,因为每次旋转屏幕都会创建一个新的 MediaPlayer,并且没有释放操作。(需要了解更多的重启相关的信息,请参考 Handling Runtime Changes.)

你或许想知道如果实现后台播放,就像很多音乐应用实现的那样。这种情况下,需要使用 Service 来控制 MediaPlayer,这部分在 Using a Service with MediaPlayer 中讨论。.

Using a Service with MediaPlayer

如果想在后台播放多媒体文件,即使你的应用不在屏幕上—也就是说,你想要持续播放,即使用户在与其他的应用程序交互—那么你必须启动个 Service 来控制 MediaPlayer。 这一步骤需要非常谨慎,因为用户和系统对应用程序后台服务与系统其他部分进行交互的结果都有自己的预期。如果你的应用程序不能满足这些预期,用户会有意个非常差的体验。这一节描述了需要注意的一些问题,并提供了一些意见。

Running asynchronously

首先,与 Activity一样, Service 中的所有操作默认都会在一个线程中完成—事实上,如果你从同一个应用中启动 activity 和 service,他们默认会在同一个线程中(即主线程)。所以,services 需要快速处理传过来的 intents,并且不能执行耗时的计算。如果需要进行繁重的操作或者需要调用阻塞方法,必须要异步进行:要么自己实现多线程,要么使用 frameworks 中的一些类来进行异步处理。

比如说,当在主线程中使用 MediaPlayer 时,应该调用 prepareAsync() 而不是 prepare(),需要实现 MediaPlayer.OnPreparedListener 接口来接受准备完成后的通知。 举例说明:

public class MyService extends Service implements MediaPlayer.OnPreparedListener {
    private static final 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();
    }
}

Handling asynchronous errors

在同步操作中,errors 通常以一个异常码或者错误码来表示。在异步处理时,要确保应用能够正确地接收错误和异常。在使用 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!
    }
}

需要谨记当 errors 发生时,MediaPlayer 就进入了 Error 状态 (查看 MediaPlayer 了解所有的状态), 必须在再次使用它之前释放掉它。

Using wake locks

当应用程序在后台播放多媒体时,设备可能会进入休眠状态即使 service 还在运行。因为Android系统通过设备休眠来节省电量,系统将会尽可能关闭不必要的模块,包括 CPU 和 Wifi硬件。如果想要service在后台播放或者缓冲音乐,并且不想系统干扰。

想要保证 service 持续运行,必须使用"wake locks." Wake lock 被用来通知应用程序正在使用一些功能,并且这些功能不应该在待机状态下被关闭。

Notice: 你应该谨慎使用 wake locks,并且使用时间需要尽可能短,因为他们会显著地减少电池使用寿命。

为了确保 MediaPlayer 播放时CPU持续工作,应该调用 setWakeMode() 方法当初始化 MediaPlayer的时候。一旦这么做了,MediaPlayer 将在播放时持有这个特殊的 lock,并在 paused或 stopped时释放它:

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

上面示例中的代码只能确保CPU会一直工作。如果你使用 Wi-Fi 播放流媒体,你还需要持有 WifiLock,需要手动获取和释放。当通过远程的 URL 来准备 MediaPlayer 时,应该创建并获取 Wi-Fi lock. 示例如下:

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

wifiLock.acquire();

当你暂、者停止了播放,或者不再需要连接网络,应该释放掉 lock:

wifiLock.release();

Running as a foreground service

Services 通常被用来执行一些后台操作,比如获取邮件、同步数据、下载文件等等。在这些操作中,用户需要清楚 service 的执行,甚至不需要介意一些 service 中段后又重新启动。

但是考虑一下使用 service 播放音乐的情形。很显然这个 service 与用户直接交互,如果 service 中断会很影响用户体验。此外,用户会想要在 service 执行过程中与它互动。这种情况下,就需要启动“前台服务”。前台服务在系统中享有很高的优先级—系统几乎不会杀死这个 service,因为它对用户来讲很重要。当启动前台服务时,service 必须提供一个 notification 来提示用户,并且用户可以通过 notification 去启动一个 activity 来与 service 交互。

为了将你的 service 启动为前台服务,必须在状态栏上创建一个 Notification 并且在 Service 调用 startForeground() 方法。示例如下:

String songName;
// assign the song name to songName
PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0,
                new Intent(getApplicationContext(), MainActivity.class),
                PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new Notification();
notification.tickerText = text;
notification.icon = R.drawable.play0;
notification.flags |= Notification.FLAG_ONGOING_EVENT;
notification.setLatestEventInfo(getApplicationContext(), "MusicPlayerSample",
                "Playing: " + songName, pi);
startForeground(NOTIFICATION_ID, notification);

当你的 service 在前台运行时,notification 会在设备上显示出来。如果用户选择了这个 notification,系统将会调用实现的 PendingIntent。在上面的示例中,它会启动一个 activity(MainActivity).

Figure 1 shows how your notification appears to the user:

  

Figure 1. Screenshots of a foreground service's notification, showing the notification icon in the status bar (left) and the expanded view (right).

只有 service 确实是在执行一些用户能够很明显感觉到的操作时,才需要设置成前台服务。一旦不需要,立马调用 stopForeground():

stopForeground(true);

需要了解更多信息,请参考 ServicesStatus Bar Notifications.

Handling audio focus

尽管在同一时间只有一个 activity 在运行,但是 Android 是一个支持多任务的系统。这给使用 audio 的程序带来一定的风险,因为可能会有几个多媒体 service 来竞争仅有的音频输出设备。在 Android2.2 之前,没有内置的机制来处理这个问题,这可能会导致非常差的用户体验。比如当用户在听音乐时,另外一个应用需要提示用户一个非常重要的通知,但是由于音乐声音太大用户无法听见通知的声音。Android2.2之后,平台提供了一种方式来协调设备的音频输出。这就是 Android 的 Audio Focus 机制。

当你的应用需要使用音频输出设备时,比如播放音乐、通知,需要先取得 audio focus.一旦获取了 audio focus,就可以使用音频设备了,但是应该监听 focus 的变化。如果丢失了 audio focus,应用应该立即停止播放或者静音(也就是"ducking"—有一个标志提示使用哪种方法更合适。),只有在重新获得 audio focus 时才会回复播放。

Audio Focus 是一种合作性的机制。也就是说,应用程序被期望(强烈推荐)与 audio focus 的机制融合,但是系统并没有强制程序遵守这些规则。如果应用程序想在失去 audio focus 时继续播放音乐,系统并不会阻止这样的行为。但是用户会觉得用户体验很差,并且可能会删掉这样的应用。

要获得 audio focus,必须调用 AudioManagerrequestAudioFocus() 方法,像下面的代码一样:

AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
    AudioManager.AUDIOFOCUS_GAIN);

if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // could not get audio focus.
}

requestAudioFocus() 的第一个参数是 AudioManager.OnAudioFocusChangeListener,这个接口中的 whose onAudioFocusChange() 方法将会在 audio focus 改变时调用。因此,你应该在 services 或者 activities 中实现这个接口。举个例子:

class MyService extends Service
                implements AudioManager.OnAudioFocusChangeListener {
    // ....
    public void onAudioFocusChange(int focusChange) {
        // Do something based on focus change...
    }
}

focusChange 参数告诉你 audio focus 发生了怎样的变化,它可能是下面的一些值(他们是 AudioManager 中定义的一些常量):

  • AUDIOFOCUS_GAIN: 获得了 audio focus.
  • AUDIOFOCUS_LOSS: 失去 audio focus 很长一段时间。必须停止所有的 audio 播放。因为很长一段时间内将不会再次获取 audio focus,此时应该尽可能地清理资源。比如,应该释放掉 MediaPlayer.
  • AUDIOFOCUS_LOSS_TRANSIENT: 暂时失去 audio focus,但是很快就会重新获得。应该停止所有的音频播放,但是可以不清里资源,因为可能很快就会再次获取 audio focus.
  • AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 暂时失去 audio focus,但是允许持续播放音频(以很小的声音),不需要完全停止播放。

实现示例:

public void onAudioFocusChange(int focusChange) {
    switch (focusChange) {
        case AudioManager.AUDIOFOCUS_GAIN:
            // resume playback
            if (mMediaPlayer == null) initMediaPlayer();
            else if (!mMediaPlayer.isPlaying()) mMediaPlayer.start();
            mMediaPlayer.setVolume(1.0f, 1.0f);
            break;

        case AudioManager.AUDIOFOCUS_LOSS:
            // Lost focus for an unbounded amount of time: stop playback and release media player
            if (mMediaPlayer.isPlaying()) mMediaPlayer.stop();
            mMediaPlayer.release();
            mMediaPlayer = null;
            break;

        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
            // Lost focus for a short time, but we have to stop
            // playback. We don't release the media player because playback
            // is likely to resume
            if (mMediaPlayer.isPlaying()) mMediaPlayer.pause();
            break;

        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
            // Lost focus for a short time, but it's ok to keep playing
            // at an attenuated level
            if (mMediaPlayer.isPlaying()) mMediaPlayer.setVolume(0.1f, 0.1f);
            break;
    }
}

需要记住 audio focus APIs 只能使用在 API level 8 (Android 2.2)以上,所以想要兼容 Android 后面的版本,必须要采取一个向后兼容的策略来使用 audio focus 机制。

可以通过反射或者在单独的类(AudioFocusHelper)中实现 audio focus 的功能来实现向后兼容的策略。下面给出一个这样类的例子:

public class AudioFocusHelper implements AudioManager.OnAudioFocusChangeListener {
    AudioManager mAudioManager;

    // other fields here, you'll probably hold a reference to an interface
    // that you can use to communicate the focus changes to your Service

    public AudioFocusHelper(Context ctx, /* other arguments here */) {
        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
        // ...
    }

    public boolean requestFocus() {
        return AudioManager.AUDIOFOCUS_REQUEST_GRANTED ==
            mAudioManager.requestAudioFocus(mContext, AudioManager.STREAM_MUSIC,
            AudioManager.AUDIOFOCUS_GAIN);
    }

    public boolean abandonFocus() {
        return AudioManager.AUDIOFOCUS_REQUEST_GRANTED ==
            mAudioManager.abandonAudioFocus(this);
    }

    @Override
    public void onAudioFocusChange(int focusChange) {
        // let your service know about the focus change
    }
}

可以在检测到 Android 版本大于或等于8时,创建一个 AudioFocusHelper 实例。示例如下:

if (android.os.Build.VERSION.SDK_INT >= 8) {
    mAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this);
} else {
    mAudioFocusHelper = null;
}

Performing cleanup

前面有提到,一个 MediaPlayer 实例会消耗很多的系统资源,所以应该尽可能做到只在必要的时候维持它的实例,并且一旦不需要了应该及时调用 release() 来释放掉资源。调用这个方法要比等待垃圾回收机号很多,因为可能需要等很久时间,垃圾回收机才会清理掉 MediaPlayer,因为垃圾回收机制只是针对内存而不是针对多媒体相关的资源。因此,在使用 service 时,必须要覆写 onDestroy() 方法来确保 MediaPlayer 得到释放:

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

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

除了在它关闭时,还应该寻找合适的机会去释放 MediaPlayer. 比如说,如果预期很长一段时间内(比如失去 audio focus 后)都不会再去播放多媒体文件,你应该释放掉当前的 MediaPlayer,稍后再重新创建。另一方面,如果只是短时间内暂停播放,那就应该维持着这个 MediaPlayer 来避免重新创建的开销。

Handling the AUDIO_BECOMING_NOISY Intent

许多优秀的音频应用能够在声音变得嘈杂(通过外部扬声器播放)时自动停止播放。比如说,当用户戴着耳机听歌时,突然拔出耳机。当然,这个行为不会自动发生。如果没有实现这个功能,将会通过外部扬声器来播放音频,这可能不是用户期望的行为。

你可以通过处理 ACTION_AUDIO_BECOMING_NOISY 来让应用停止播放音乐,可以在 manifest 中注册一个广播来实现:

<receiver android:name=".MusicIntentReceiver">
   <intent-filter>
      <action android:name="android.media.AUDIO_BECOMING_NOISY" />
   </intent-filter>
</receiver>

注册了 MusicIntentReceiver 广播来处理这个 intent.应该实现下面的逻辑:

public class MusicIntentReceiver implements android.content.BroadcastReceiver {
   @Override
   public void onReceive(Context ctx, Intent intent) {
      if (intent.getAction().equals(
                    android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
          // signal your service to stop playback
          // (via an Intent, for instance)
      }
   }
}

Retrieving Media from a Content Resolver

Media player应用中另外一个功能是来检索设备上存储的音频文件。可以通过查询 ContentResolver 来获取外部媒体文件:

ContentResolver contentResolver = getContentResolver();
Uri uri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
Cursor cursor = contentResolver.query(uri, null, null, null, null);
if (cursor == null) {
    // query failed, handle error.
} else if (!cursor.moveToFirst()) {
    // no media on the device
} else {
    int titleColumn = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media.TITLE);
    int idColumn = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media._ID);
    do {
       long thisId = cursor.getLong(idColumn);
       String thisTitle = cursor.getString(titleColumn);
       // ...process entry...
    } while (cursor.moveToNext());
}

想配合 MediaPlayer 使用,可以像下面代码这样做:

long id = /* retrieve it from somewhere */;
Uri contentUri = ContentUris.withAppendedId(
        android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);

mMediaPlayer = new MediaPlayer();
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setDataSource(getApplicationContext(), contentUri);

// ...prepare and start...