媒体回放

Android多媒体框架包括对播放各种大众化的媒体类型的支持,因此能够很容易的把音频、视频、图片集成到应用程序中。你能够使用MediaPlayer的API,播放来自应用程序资源中存储的媒体文件(raw资源)、或是来自独立文件系统中的媒体文件、或是来自网络连接之上的数据流。
本文向你介绍如何编写跟用户交互的媒体播放应用程序,以及如何让系统获得良好的性能和用户体验。

注意:只能把音频数据播放到标准输出设备。当前,主要是移动设备的麦克风或蓝牙耳机。不能在音频会话期间播放声音文件。

基础

下面的类在Android框架中被用于播放声音和视频:

  • MediaPlayer

    这个类是播放声音和视频的主要API。

  • AudioManager

    这个类管理设备上的音频资源和音频输出。

清单声明

开始在应用程序上使用MediaPlayer进行开发之前,必须确保应用程序清单中有允许使用相关功能所对应声明。

  • Internet Permission:如果要使用MediaPlayer来播放基于互联网内容的流,那么应用程序就必须申请互联网访问的权限。
<uses-permission android:name="android.permission.INTERNET" />
  • Wake Lock Permission:如果播放器应用程序需要保持屏幕的调光状态、或者要保持处理器的休眠状态、或者要使用MediaPlayer.setScreenOnWhilePlaying()或MediaPlayer.setWakeMode()方法,就必须申请这个权限。
<uses-permissionandroid:name="android.permission.WAKE_LOCK"/>

使用MediaPlayer

媒体框架的最中重要的组件之一是MediaPlayer类。这个类的对象能够用最小的步骤来获取、解码和播放音视频。它支持以下几种不同的媒体来源:

  • 本地资源;

  • 内部的统一资源标识(URI),如可能从内容解析器中来获取;

  • 外部的URI(流)。

以下是一个如何播放本地原生的音频资源的示例,该资源保存在应用的res/raw/目录中。

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

在这个场景中,一个“原生”资源是一个系统不使用任何特殊方式来解析的文件。但是,这种资源的内容不应该是原始的音频,它应该是一个用其所支持的某种格式进行适当编码和格式化的媒体文件。

以下是一个如何使用系统中本地可用的URI来播放的示例:

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

以下是用远程的URL,通过HTTP流来播放的示例:

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异常。

异步准备

使用MediaPlayer在原理方面是简单的,但是要把它正确的跟一个典型的Android应用集成,就需要记住几件重要的事情。例如,对于prepare()方法的调用可能需要很长的执行时间,因为它可能需要获取和解码媒体数据。在这种情况下,任何方法都可能需要很长的执行时间,所以不应该在应用程序的UI线程中调用它。执行的的过程中可能导致UI的挂起,直到该方法返回,这种用户体验很坏,并且能够导致一个ANR(Application Not Responding)错误。即使你认为资源会被很快的加载,但是要记住任何超过十分之一秒响应,都会在UI界面上形成停顿,从而给用户带来应用程序执行慢的印象。

要避免UI线程的挂起,就要使用另一个线程来准备MediaPlayer,并且在执行完成的时候给主线程发一个通知。你能够编写自己的线程逻辑,但是这种使用MediaPlayer的方式很共同,因此框架通过使用prepareAsync()方法提供了一种便利的方式来完成这个任务。这个方法在后台启动媒体的准备过程,并立即返回。当媒体被准备完成后,通过setOnPreparedListener()方法所配置的MediaPlayer.OnPreparedListener的onPrepared()方法会被调用。

管理状态

要记住的MediaPlayer的另一个特点是:它是基于状态的。也就是说,MediaPlayer有一个内部状态,在编写自己的代码时必须要注意这个状态,因为某个操作可能只在特定的状态中才有效。如果在错误的状态执行了一个操作,系统会抛出一个异常或导致其他的不希望的行为发生。

在MediaPlayer类的文档中显示了一个完整的状态图,它阐明了把MediaPlayer从一个种状态转移到另一种状态的方法。例如,当创建一个新的MediaPlayer对象时,它是处于Idle状态。在这个时点,应该通过调用setDataSource()方法来初始化,接下来是Initialized状态。之后必须使用prepare()或prepareAsync()方法来准备媒体。在MediaPlayer完成准备工作时,它会进入Prepared状态,这意味着能够调用start()方法来播放媒体。这时,就像图中演示的那样,通过调用start()、pause()、和seekTo()方法在Started、Paused和PlaybackCompleted状态之间进行切换。当调用stop()方法时,要注意在再次准备MediaPlayer之前不能够再调用start()方法了。

在编写跟MediaPlayer对象交互的代码时,要始终记住这个状态图,因为在错误的状态下调用它的方法是最常见的Bug。

释放MediaPlayer对象

MediaPlayer能够消化有价值的系统资源。因此,始终需要另外的措施来确保MediaPlayer不会因为长时间的实例化而被挂起。当处理完成时,应该始终调用release()方法来确保其所占用的系统资源被正确的释放。例如,如果你正在使用MediaPlayer,并且Activity收到了一个onStop()调用,你就必须释放MediaPlayer对象,因为Activity已经不再跟用户进行交互了,所以在持有这个MediaPlayer对象已经毫无意义了(除非要在后台播放媒体)。当Activity处于恢复态或重启态的时候,你需要创建一个新的MediaPlayer对象,并且在恢复播放之前要再次准备它。

以下是应该如何释放和取消MediaPlayer对象的示例:

mediaPlayer.release();
mediaPlayer = null;

作为一个例子,也要考虑在Activity终止时忘记释放MediaPlayer对象所可能发生的问题,因为在该Activity每次重启时都要创建一个新的MediaPlayer对象。如你所知,当用户改变屏幕的方向时(或者用另一种方式来改变设备配置),系统都要通过重启Activity(默认)来处理这种改变,因此当用户反复在横竖屏之间切换时,就可能很快消耗所有的系统资源,因为每次方向的改变,都要创建一个新的MediaPlayer对象,而之前的还不曾释放掉。

你可能会想到,如果用户离开Activity,那么在后台播放媒体所发生的事情,内置的Music应用程序就使用了这种行为。在这种场景中,需要一个Service来控制MediaPlayer对象。

使用服务的MediaPlayer

如果想要的媒体在后台播放,即使应用程序不在屏幕上,也就是说在用户跟其他应用程序交互时,媒体文件也能继续播放,那么就必须启动一个服务(Service),并且在服务中控制MediaPlayer实例。你应该关注这种情况,因为用户和系统都希望运行后台服务的应用程序应该跟系统的空闲时间交互。如果应用程序不能满足这种期望,那么用户可能会有很坏的体验。本节会向你介绍这些主要的问题,并提供解决它们的方法。

异步运行

首先,实际上默认情况下,Activity以及在Service中的所有工作都是在一个单一线程中执行的,如果在同一个应用程序中运行一个Activity和Service,那么默认情况下,它们都会使用同一个线程(main线程)。因此,服务需要快速的处理输入的请求,并且在响应请求时,不要处理长时的计算处理。如果有繁重的工作或阻塞调用,就必须采用异步的方式:既可以采用另一个线程来执行你的处理,也可以使用很多由框架提供的便利的异步处理。

例如,在main线程中使用MediaPlayer时,应该调用prepareAsync()方法,而不是prepare(),并且为了在准备完成时便于通知你启动播放处理,你要实现MediaPlayer.OnPrepare监听器,例如:

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

处理异步错误

在同步操作上,通常错误会发出一个异常信号或错误代码,但是在使用异步资源时,应该确保应用程序获取正确的错误通知。在MediaPlayer场景中,通过实现MediaPlayer.OnError监听器,并把它设置给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会转移到Error状态,在你能够在再次使用它之前,你必须要对它进行重置。

使用唤醒锁(wake lock)

在设计在后台播放媒体的应用程序时,在服务还在运行时,设备可能进入休眠状态。因为Android系统会在设备休眠时来尝试保存电量,所以系统会尝试关掉所有不需要的电话功能,包括CPU和WiFi等硬件。但是,如果你的服务正在播放或流化音乐,就要防止系统对你的播放动作的干扰。

为了确保你的服务能够在这些条件下能够继续运行,就必须使用“wake locks”。唤醒锁是一种以信号的方式,把应用程序正在使用的功能发送给系统,要求系统即使是在系统空闲的时候也要保留这些功能的有效性。
注意:应该尽量少的使用wake locks,只在真正需要的时候才持有它们,因为它们会直接减少电池的使用时间。

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

作为前台服务来运行

服务经常被用来执行后台服务,如获取Mail,同步数据,下载内容等。在这些场景中,用户是不关心服务的执行,甚至是这些服务被中断然后在重新开始也不太关注。

但是在播放音乐的服务的场景中。用户能够清晰的感觉到这个一个服务,并且任何中断都会严重影响用户的体验。另外,该服务在执行期间,用户也希望与它进行交互。在这种情况下,服务应该作为前台服务(foreground service)来运行。前台服务在系统中会持有很高的重要性级别---系统几乎不会杀死这样的服务,因为对用户来说,它是非常重要的。当服务在前台运行时,该服务也必须提供状态栏通知,以确保用户能够感知到服务正在运行,并且允许用户能够打开与服务进行交互的Activity。

为了把服务转换成前台服务,必须给状态栏创建一个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);

当服务在前台运行时,配置的通知会显示在设备的通知区域。如果用户选择了该通知,那么系统就会调用你提供的PendingIntent对象。在上面的例子中,它会打开一个Activity(MainActivity)。

应该只有在服务是执行某些用户非常关注的动作时才使用前台服务(foreground service)。一旦不在需要,就应该调用stopForeground()方法释放它:

stopForeground(true);

处理音频焦点(Audio Focus)

在任何时候,即使只有一个Activity在运行,Android也是一个多任务的环境。这就给使用音频通道的应用带来一个特殊的竞争,因为只有一个音频输出通道,并且有可能会有几个媒体服务来竞争使用它。在Android2.2之前,没有内置的机制来处理这个问题,这样就导致了很坏的用户体验。例如,当用户正在听音乐,而另一个应用程序需要通知用户一些重要的事情,在音乐声音很大时,用户就可能听不到这个通知提示。从Android2.2开始,平台给应用程序提供了一种方式来协调设备音频输出通道使用,这种机制被叫做Audio Focus。

当应用程序需要输出音频,如音乐或通知时,应该始终要请求音频焦点(Audio Focus)。一旦请求到音频焦点,就可以自由的使用声音输出了,但是要始终监听音频焦点的改变。如果接到了失去音频焦点的通知,就应该立即中断该音频或把它降低到安静的级别(叫做ducking---它有一个标识,用于指示那种模式是合适的),并且在再次收到焦点后能够恢复大声播放。

Audio Focus是协商性的,也就是说,我们期望(并且高度保证)应用程序能够遵守Audio Focus的指南,但是这个规则不是被系统所强制的。如果一个应用程序即使在失去音频焦点时也想要大声的播放,那么系统是不会阻止它的。但是这将给用户带来很坏的体验,以至于卸载这种行为不端的应用程序。

要请求音频焦点,必须调用AudioManager中的requestAudioFocus()方法,实例演示如下:

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,它的onAudioFocusChange()方法在音频焦点改变时被调用。因此应该在Service和Activity上也实现这个接口。例如:

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

focusChange参数告你音频焦点时如何改变的,并且能够是下面的值之一(它们是所有的在AudioManager类中定义的常量):

  • AUDIOFOCUS_GAIN:你已经获得音频焦点;

  • AUDIOFOCUS_LOSS:你已经失去音频焦点很长时间了,必须终止所有的音频播放。因为长时间的失去焦点后,不应该在期望有焦点返回,这是一个尽可能清除不用资源的好位置。例如,应该在此时释放MediaPlayer对象;

  • AUDIOFOCUS_LOSS_TRANSIENT:这说明你临时失去了音频焦点,但是在不久就会再返回来。此时,你必须终止所有的音频播放,但是保留你的播放资源,因为可能不久就会返回来。

  • AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:这说明你已经临时失去了音频焦点,但允许你安静的播放音频(低音量),而不是完全的终止音频播放。

以下是一个例子的实现:

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;
    }
}

要记住:音频焦点只有在API Level8(Android2.2)以上的版本中才有效,因此如果想要支持Android以前的版本,就应该采用向后兼容的策略,如果要使用这个功能,那么就会丧失向后的兼容性。

你可以通过反射调用音频焦点的方法或通过在一个独立的类中实现所有的音频焦点的功能来达到向后兼容的目的。以下就是这样一个例子:

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
    }
}

你能够创建一个AudioFocusHelper类的实例来检查系统是否运行在API Level8以上版本,例如:

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

执行清理工作

正如前面提到的,MediaPlayer对象会消耗重要的系统资源,因此应该只是在需要的时候才保留它,并且在播放完成后立即调用release()方法来使释放它。明确的调用这种清理的方法是重要的,而不要依赖系统的垃圾回收,因为垃圾回收MediaPlayer对象之前可能需要一些时间,而且垃圾回收只是针对内存资源的,而对于其他媒体相关的资源却不处理。因此,在这种使用服务的场景中,应该始终重写onDestroy()方法来确保你释放了MediaPlayer对象:

public class MyService extends Service {
   MediaPlayer mMediaPlayer;
   // ...
,
   @Override
   public void onDestroy() {
       if (mMediaPlayer != null) mMediaPlayer.release();
   }
}

除了关机以外,应该始终寻找其他机会来释放MediaPlayer对象。例如,如果你不期望在一个较长的时间段内来播放媒体文件(例如,在失去音频焦点之后)就应该明确的释放这个MediaPlayer对象,并且在后续需要时再创建它。在另一方面,如果只是期望它停止播放很短的时间,那么你就应该持有这个MediaPlayer对象以避免反复的重建这个对象。

处理AUDIO_BECOMING_NOISY意图

一些精心编写的应用程序在发生音频吵杂的事件(通过外部扬声器输出)时,能够自动的终止播放。例如,在用户正在使用耳麦听音乐时,而耳麦偶然与设备的连接断开,就可能发生这种情况。但是这种行为不会自动发生。如果你没有实现这个功能,音频就可能通过外部扬声器来输出,这可能是用户不想要的。

通过处理ACTION_AUDIO_BECOMING_NOISY意图,你能够确保在发生这种情况时,终止音乐的播放。通过把下列内容添加到你应用清单中,就能够注册一个接受器:

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

这个注册的MusicIntenReceiver类做为该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)
      }
   }
}

从内容解析器(Content Resolver)中接收媒体数据

在媒体播放应用程序中,可以使用另外一种有用的功能来获取用户设备上的音乐。通过查询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 =newMediaPlayer();
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setDataSource(getApplicationContext(), contentUri);
// ...prepare and start...
Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1