[关闭]
@cxm-2016 2016-12-15T09:19:48.000000Z 字数 11567 阅读 2775

Android:媒体播放器

Android

版本:2
作者:陈小默
声明:禁止商业,禁止转载

原文地址:Android开发者API指南-媒体播放

Android的多媒体框架支持多种通用格式的播放,所以你可以很方便的在你的应用中集成音频、视频和图像资源。这篇文档向你展示了应该如何编写一款能够与用户互动的具有良好系统表现和用户满意度的媒体播放应用。

注意: 你能够通过任何标准输出设备播放音频,但不能在通话时播放声音文件。


基础


下面是Android框架中常被用来播放声音和视频的类:

MediaPlayer

AudioManager

清单配置文件的声明


在使用MediaPlayer开发应用之前,请确保你的清单文件拥有了合适的声明去允许你使用相应的特性。

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

使用MediaPlayer


多媒体框架中最重要的一个组件是MediaPlayer类。一个该类的对象可以通过最小限度的配置来获取、编码和播放音视频资源。它支持多种不同来源的媒体文件,例如:

至于Android设备支持的媒体文件格式列表,请参阅Android支持媒体文件格式文档

这是一个展示如何播放本地原生资源(被保存在你应用的 res/raw/ 目录下)的例子:

  1. MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
  2. mediaPlayer.start(); // 不需要调用prepare(); create() 方法已经为你完成了这些操作

在某种意义上,一个“raw”资源是一个系统不会尝试使用特殊方式解析的文件。所以这个资源不能是原生音频文件,而应该是适合编码的和被Android所支持的类型格式化了的资源文件。

这里有一个如何使用系统中可用本地资源的Uri播放的例子(可以通过Content Resolver获取实例)

  1. Uri myUri = ....; // 在这里实例化Uri
  2. MediaPlayer mediaPlayer = new MediaPlayer();
  3. mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
  4. mediaPlayer.setDataSource(getApplicationContext(), myUri);
  5. mediaPlayer.prepare();
  6. mediaPlayer.start();

通过远程URL播放HTTP数据流的例子请看这里:

  1. String url = "http://........"; // URL网络资源
  2. MediaPlayer mediaPlayer = new MediaPlayer();
  3. mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
  4. mediaPlayer.setDataSource(url);
  5. mediaPlayer.prepare(); // 会花费较长时间! (缓存等)
  6. mediaPlayer.start();

注意:如果你通过URL获取网络媒体文件的数据流,你需要保证这个文件支持断点续传。

警告:你在使用setDataSource()方法时必须捕获或者重新抛出IllegalArgumentExceptionIOException异常,因为引用的这些文件可能并不存在。

异步的准备方法

使用MediaPlayer的方式很简单。但是,在正确集成一个典型的Android应用之前我们有几点重要的事项需要注意。例如,prepare()方法的调用会花费大量的执行时间,因为其中包含了获取和解码媒体数据的过程。所以,像这种需要花费大量时间去执行的方法,不能在应用的主线程(UI线程)中去执行。像这种在主线程中调用会导致其挂起的方法的做法,会导致差劲的用户体验并且会导致ANR(Application Not Responding,应用无响应)异常。需要记住一件事,哪怕你的UI响应时间仅仅花费超过十分之一秒也会导致画面明显的暂停从而给用户造成一种你的应用很慢的的印象。

为了避免UI线程挂起,你应该应该创建另外一个线程去执行MediaPlayer的准备方法并且在其完成时通知主线程。虽然这需要我们自己去写线程逻辑,然而,我们可以使用MediaPlayer架构支持的一种更加方便的方式去实现这一模式,就是prepareAsync()方法。这个方法会在后台执行媒体文件的准备操作并且该方法是立即返回的。当媒体文件准备完毕后,其注册的MediaPlayer.OnPreparedListener监听器的onPrepare()方法就会被执行。

状态管理

另一方面,你需要牢牢记住MediaPlayer的基础状态。也就是说,当你编写代码的时候必须知道MediaPlayer的内部状态,因为当MediaPlayer处于某些特殊状态的时候只有对应的某些操作是有效的。如果你在一个错误的状态下执行了一段操作,这可能会导致系统抛出异常或者是让程序产生出乎意料的行为。

MediaPlayer类的文档展示了其完整的状态图表,这个图标清晰显示了MediaPlayer中方法对于状态的影响。例如,当你创建了一个新的MediaPlayer,此时它处于闲置(Idle)状态。在这个状态下,你应该通过setDateSource()方法去进行初始化操作,这个操作导致它进入初始化(Initialized)状态。在此之后,你不得不去使用prepare()或者prepareAsync()方法去装载数据,当MediaPlayer装载完成之后,它会进入已装载(Prepared)状态,这意味着它能够调用start()方法去播放媒体文件。在这里,就像图表上说明的那样,你可以通过start()、pause()、seekTo()等其他方法使得MediaPlayer的状态在启动(Started)、暂停(Paused)和播放完成(PlaybackCompleted)状态中切换。然而,需要注意的是,当你调用stop()方法之后,你就不能再次调用start()方法了,除非你再次装载你的媒体文件。

在编写与MediaPlayer对象交互的代码时,要时刻牢记其状态图表,因为在错误的状态下调用其方法通常会产生错误。

释放MediaPlayer

一个MediaPlayer会消耗宝贵的系统资源。因此,你需要始终保持额外的警惕来保证你没有在不必要时持有MediaPlayer的实例。当你使用完毕时,你需要调用release()方法确保这些系统资源被正确的释放。举个例子,假如你正在使用MediaPlayer并且此时你的Activity接受到了onStop()调用,那么你必须释放MediaPlayer,这将确保在你的Activity停止与用户交互的任何场景都不会继续持有实例(除非你希望能够继续后台播放,这个将会在下一节讨论)。当然,在你的Activity处于交互(resumed)或者重新启动(restarted)状态时,你可以在继续播放之前再一次的创建一个新的MediaPlayer并且执行准备操作。

这里展示了你应该如何释放MediaPlayer的资源和引用:

  1. mediaPlayer.release();
  2. mediaPlayer = null;

举个例子,考虑一个可能会发生的问题:如果当你在Activity**关闭**(stopped)时忘记了释放资源,并且在Activity重新启动时又创建了一个新的MediaPlayer会怎样。正如你知道的那样,在默认情况下,当用户改变了屏幕的方向或者通过其他方式改变了设备配置时,系统会重新启动这个Activity,那么当用户在横竖屏之间来回切换时就会快速消耗掉全部的系统资源,因为每一次的方向改变,你都创建了一个新的MediaPlayer却没有释放。

你可能想要在用户离开Activity时能够继续后台播放或者以同样的方式构建一款具有该行为的音乐播放器应用。在这种情况下,你需要通过服务(Service)作为MediaPlayer的控制器。接下来我们讨论如何使用Service控制MediaPlayer。

在Service中使用MediaPlayer


如果你希望当你的应用不在屏幕上显示时(也就是当用户和其他应用交互时)能够继续在后台播放。那么你必须启动一个服务并在其中操作MediaPlayer实例。因为用户期待你的应用在后台运行时能够继续交互。如果你的应用没有满足用户的期望,会让用户产生糟糕的体验。

异步调用

就像Activity一样,在Service中完成的操作都是在默认线程中执行(事实上,从统一个应用中启动的Activity和Service运行在名为“main”的相同线程中)。因此,因此Service需要快速处理Intent并且不能在需要响应时执行耗时的计算指令。假如你期望完成重量级的工作或大范围的方法调用,那么你必须对这些任务使用异步操作:运行在另一个你自己实现的线程,或者使用框架中的工具。

例如,当你需要在主线程中使用MediaPlayer时,你需要调用prepareAsync()方法而不是prepare()方法,并且你需要实现一个MediaPlayer.OnPreparedListener接口来接收准备工作完成的通知,之后你才可以执行播放操作,就像下面这样:

  1. public class MyService extends Service implements MediaPlayer.OnPreparedListener {
  2. private static final String ACTION_PLAY = "com.example.action.PLAY";
  3. MediaPlayer mMediaPlayer = null;
  4. public int onStartCommand(Intent intent, int flags, int startId) {
  5. ...
  6. if (intent.getAction().equals(ACTION_PLAY)) {
  7. mMediaPlayer = ... // initialize it here
  8. mMediaPlayer.setOnPreparedListener(this);
  9. mMediaPlayer.prepareAsync(); // prepare async to not block main thread
  10. }
  11. }
  12. /** Called when MediaPlayer is ready */
  13. public void onPrepared(MediaPlayer player) {
  14. player.start();
  15. }
  16. }

处理异步错误

在同步操作中,错误通常会使用异常或者是一段代码标记。然而在有些需要使用异步资源的时候,你就要确保你的应用在合适的时候显示错误信息。在这种情况下,你需要给你的MediaPlayer实例实现MediaPlayer.OnErrorListener接口。

  1. public class MyService extends Service implements MediaPlayer.OnErrorListener {
  2. MediaPlayer mMediaPlayer;
  3. public void initMediaPlayer() {
  4. // ...在这里初始化MediaPlayer
  5. mMediaPlayer.setOnErrorListener(this);
  6. }
  7. @Override
  8. public boolean onError(MediaPlayer mp, int what, int extra) {
  9. // ... 进行处理 ...
  10. // MediaPlayer进入了错误的状态必须重启
  11. }
  12. }

这里需要注意的是,在错误发生时,MediaPlayer将会进入错误状态并且你必须在再次使用它之前重启它。

唤醒锁机制

当我们设计一款后台媒体播放应用的时候需要注意设备可能会在运行时进入休眠状态。因为Android系统会尝试通过休眠的方式去节省电量,此时系统会试图关闭手机中所有不必要的功能,甚至包括CPU和WiFi硬件,如果你的设备正在播放音乐,并且你希望防止系统休眠对媒体播放的影响,那么你必须阻止系统干涉你的后台。

为了确保设备能够在上述条件下继续运行,你需要使用“唤醒锁机制(wake locks)”。唤醒锁是一种通信机制,用来在系统空闲时与其通信。

注意:你需要节约唤醒锁资源并且在真正需要时持有它们,因为他们会消耗设备大量的电量。

为了确保CPU能够在你进行后台播放的时候持续运行,你需要在MediaPlayer初始化的使用调用setWakeMode()方法。当你这么做了,这个MediaPlayer就会获得一个在播放状态和停止状态之间存在的锁。

  1. mMediaPlayer = new MediaPlayer();
  2. // ... 在这里进行其他操作 ...
  3. mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);

然而,这个唤醒锁仅仅能够要求CPU在播放期间保持唤醒状态。如果你播放的是从网络获取的流媒体或者说正在使用WiFi,那么你可能就要持有一个WiFi锁。所以,当你使用远程URL地址启动MediaPlayer的准备状态的时候,你需要创建和获取Wi-Fi lock,就像下面这样:

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

当你暂停或停止你的播放器或者长时间不使用网络的时候,你需要释放掉这个锁:

  1. wifiLock.release();

像一个前台服务那样运行

服务通常被用来做一些后台任务,例如收发邮件、同步数据、下载内容或者其他可能。在这种情况下,用户可能都不会意识到服务的执行,甚至不会意识到某些服务的中断和稍后的重启。

考虑到使用服务播放音乐的特殊性。很显然,这个服务的任何中断都会影响到用户体验。除此之外,用户还可能会希望在服务执行期间与之交互。在这种情况下,这个服务还需要运行一个“前台服务”。这个前台服务在系统中拥有更高的重要等级以至于系统几乎不会杀死这个服务。当前台服务运行的时候,还需要提供一个通知状态栏来确保用户能够与之交互。

为了将你的服务转换为前台服务,你必须在状态栏创建一个通知,然后调用Service的startForeground()方法,例如:

  1. String songName;
  2. // assign the song name to songName
  3. PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0,
  4. new Intent(getApplicationContext(), MainActivity.class),
  5. PendingIntent.FLAG_UPDATE_CURRENT);
  6. Notification notification = new Notification();
  7. notification.tickerText = text;
  8. notification.icon = R.drawable.play0;
  9. notification.flags |= Notification.FLAG_ONGOING_EVENT;
  10. notification.setLatestEventInfo(getApplicationContext(), "MusicPlayerSample",
  11. "Playing: " + songName, pi);
  12. startForeground(NOTIFICATION_ID, notification);

当你的服务运行在前台的时候,你配置的通知在设备的通知区域可见。如果用户选择了你的通知,系统就会调用你配置的PendingIntent,用来打开一个Activity。

在需要停止服务的场景下,调用stopForeground():

  1. stopForeground(true);

处理音频焦点

在任何时间都只能有一个Activity在运行,尽管Android是一个多任务的环境。这对使用音频的的应用造成了极大的挑战,因为只有一个音频输出但是却又许多的媒体服务竞争使用。在Android2.2之前,并没有一种机制能够处理这种问题,这就导致了相当糟糕的用户体验。比如用户正在听音乐,此时另一个应用需要通知用户一些非常重要的事情,那么用户可能会因为音乐声太大而听不到通知铃声。从Android2.2开始,Android平台提供了一种新的方式来转让设备的音频输出。这种方式叫做音频焦点。

当你的应用需要输出音频的时候,比如播放音乐或者发送通知,你始终需要请求音频焦点。一旦拥有了音频焦点,你就可以自由的使用音频输出了,但是应当始终监听焦点的改变。如果被通知失去了音频的焦点,就需要立即杀死音频或者将当前的音频的声音降低到一个较低的级别(这被称为闪避-有一个标志位证明哪一个音频服务的占用)并且在重新获得焦点的时候继续播放。

音频焦点能够非常自然的写作,也就是说,希望(强烈建议)应用能够遵循音频焦点的指导方式,但是这个规则并不是系统强制的。如果一个应用想要在失去焦点之后继续大声的播放音乐,系统不会做任何事情去阻止它。然而,用户就会获得一个糟糕的用户体验导致用户很可能会卸载该应用。

如果要请求音频焦点,你必须调用AudioManager的requestAudioFocus()方法,就像下面这个例子那样:

  1. AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
  2. int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
  3. AudioManager.AUDIOFOCUS_GAIN);
  4. if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
  5. // could not get audio focus.
  6. }

requestAudioFocus()方法的第一个参数是AudioManager.OnAudioFocusChangeListener。其中的onAudioFocusChange()方法会在音频焦点改变时被调用。因此你需要在你的service或者activities中去实现这个接口,举个例子:

  1. class MyService extends Service
  2. implements AudioManager.OnAudioFocusChangeListener {
  3. // ....
  4. public void onAudioFocusChange(int focusChange) {
  5. // Do something based on focus change...
  6. }
  7. }

focusChange的参数告诉你音频焦点的改变,其参数值会是下列之一(这些都被定义在AudioManager中):

这里有一个实现:

  1. public void onAudioFocusChange(int focusChange) {
  2. switch (focusChange) {
  3. case AudioManager.AUDIOFOCUS_GAIN:
  4. // resume playback
  5. if (mMediaPlayer == null) initMediaPlayer();
  6. else if (!mMediaPlayer.isPlaying()) mMediaPlayer.start();
  7. mMediaPlayer.setVolume(1.0f, 1.0f);
  8. break;
  9. case AudioManager.AUDIOFOCUS_LOSS:
  10. // Lost focus for an unbounded amount of time: stop playback and release media player
  11. if (mMediaPlayer.isPlaying()) mMediaPlayer.stop();
  12. mMediaPlayer.release();
  13. mMediaPlayer = null;
  14. break;
  15. case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
  16. // Lost focus for a short time, but we have to stop
  17. // playback. We don't release the media player because playback
  18. // is likely to resume
  19. if (mMediaPlayer.isPlaying()) mMediaPlayer.pause();
  20. break;
  21. case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
  22. // Lost focus for a short time, but it's ok to keep playing
  23. // at an attenuated level
  24. if (mMediaPlayer.isPlaying()) mMediaPlayer.setVolume(0.1f, 0.1f);
  25. break;
  26. }
  27. }

执行清理操作

就像之前说的那样,MediaPlayer对象能够会消耗掉大量的系统资源,所以你需要仅在使用时持有对象并且在使用完成后调用release()方法释放资源。这个释放资源的方法比垃圾回收器更精确,因为系统的垃圾回收机制并不总是在执行。所以当我们使用service(activity)的时候,应该覆盖onDestroy()来确保MediaPlayer被释放:

  1. public class MyService extends Service {
  2. MediaPlayer mMediaPlayer;
  3. // ...
  4. @Override
  5. public void onDestroy() {
  6. if (mMediaPlayer != null) mMediaPlayer.release();
  7. }
  8. }

你可以在任何合适的时机去释放MediaPlayer,而不仅仅是在关闭时。比如,你并不期望在某一个时间段内进行播放(比如失去焦点之后),你需要立即释放已经存在的MediaPlayer对象并且在使用时再次创建。另一方面,如果你仅仅是停止播放很短的时间,就没有必要去销毁、创建和准备了。

处理AUDIO_BECOMING_NOISY意图

许多写的很好的应用会在突然后台播放突然变成噪音的时候自动停止播放(比如通过外部扬声器输出时)。举个例子,这可能发生在用户用户正在使用耳机听音乐突然与设备断开连接的时候。然而,这个行为不是自发的。如果你没有实现下面这个功能,在使用外部设备播放时就可能不会产生预期的效果。

你需要确保你的应用在收到ACTION_AUDIO_BECOMING_NOISY意图的时候能够停止播放音乐,那么,你需要宰你的清单文件中添加如下语句来注册一个广播接收者:

  1. <receiver android:name=".MusicIntentReceiver">
  2. <intent-filter>
  3. <action android:name="android.media.AUDIO_BECOMING_NOISY" />
  4. </intent-filter>
  5. </receiver>

这里的MusicIntentReceiver类为接受AUDIO_BECOMING_NOISY意图的广播接收者。你需要实现这样实现这个类:

  1. public class MusicIntentReceiver extends android.content.BroadcastReceiver {
  2. @Override
  3. public void onReceive(Context ctx, Intent intent) {
  4. if (intent.getAction().equals(
  5. android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
  6. // signal your service to stop playback
  7. // (via an Intent, for instance)
  8. }
  9. }
  10. }

通过内容解析器(Content Resolver)获取媒体资源

另一个在媒体播放应用上比较有用的功能是能够获取到用户设备上已有的资源。你可以通过ContentResolver来查询外部的媒体文件:

  1. ContentResolver contentResolver = getContentResolver();
  2. Uri uri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
  3. Cursor cursor = contentResolver.query(uri, null, null, null, null);
  4. if (cursor == null) {
  5. // 查询失败,处理错误
  6. } else if (!cursor.moveToFirst()) {
  7. // 媒体文件不存在
  8. } else {
  9. int titleColumn = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media.TITLE);
  10. int idColumn = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media._ID);
  11. do {
  12. long thisId = cursor.getLong(idColumn);
  13. String thisTitle = cursor.getString(titleColumn);
  14. // ...处理条目...
  15. } while (cursor.moveToNext());
  16. }

和MediaPlayer一起使用的时候,你还可以这么做:

  1. long id = /* retrieve it from somewhere */;
  2. Uri contentUri = ContentUris.withAppendedId(
  3. android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
  4. mMediaPlayer = new MediaPlayer();
  5. mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
  6. mMediaPlayer.setDataSource(getApplicationContext(), contentUri);
  7. // ...准备和启动...
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注