前言
本章内容为Android开发者指南的 Framework Topics/Multimedia and Camera/Media Playback章节,译为"媒体播放",版本为Android 4.0 r1,翻译来自:"呆呆大虾",欢迎访问他的微博:"http://weibo.com/popapa",再次感谢"呆呆大虾" !期待你一起参与翻译Android的相关资料,联系我[email protected]。
媒体播放
译者署名: 呆呆大虾
版本:Android 4.0 r1
原文
http://developer.android.com/guide/topics/media/mediaplayer.html
在本文中
Audio Focus的处理
关键类
参阅
Android的多媒体框架支持多种通用媒体的播放,因此能够很容易地在程序中集成音频、视频和图片信息。利用MediaPlayer API,可以播放多种来源的音视频数据,包括存储于程序资源(裸资源)中的媒体文件、文件系统中的独立文件、通过网络连接读取的数据流。
本文演示了如何编写一个媒体播放程序。为了兼顾良好的性能和舒适的用户体验,它还实现了播放期间用户和系统之间的交互。
注意: 只能在标准的输出设备上播放音频数据,目前即为移动设备的扬声器或蓝牙耳机。并且不能在通话期间同时播放音频文件。
简介
下列类用于在Android框架中播放音视频:
本类是播放音视频的主要API。
本类管理音频源和设备的音频输出。
Manifest声明
在开始开发MediaPlayer的应用程序之前,请确保manifest已经正确地声明了以下相关feature:
· 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" />
媒体播放器的使用
媒体框架中最重要的组件之一就是MediaPlayer类。经过一些很少量的设置,此对象即能够读取、解码并播放音视频内容。它能支持如下多种不同的媒体来源:
· 本地资源
· 内部URI,比如可能来自Content Resolver
· 外部URL(流)
关于Android支持的媒体格式,请参阅文档Android支持的媒体格式。
下面的例子展示了如何播放本地以裸资源方式提供的音频(保存于程序的res/raw/目录下):
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); // 不必调用prepare(); create()会自动调用
这里的“裸”资源是指系统不会以任何特定方式进行解析的文件。当然,这个资源的内容不应该是原始音频数据,而应是用所支持的格式正确编码并格式化过的媒体文件。
下面是如何播放来自系统本地提供的URI资源(比如通过Content Resolver获取的):
Uri myUri = ....; // 在此初始化Uri
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();
下例是播放来自远程URL的HTTP流:
String url = "http://........"; // 在此指定URL
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare(); // 可能会耗时很长! (需创建缓存等)mediaPlayer.start();
注意:如果通过URL来传送一个来自在线媒体文件的数据流,则该文件必须支持渐进下载(progressive download)。
警告:因为所引用的文件有可能会不存在,所以使用setDataSource()时必须捕捉并且传递IllegalArgumentException和IOException异常。
原则上说,MediaPlayer的使用可以非常简单。不过有一点很重要,请记住还有一些工作必须正确地加入到典型的Android程序中去。比如,因为要读取并解码媒体数据,prepare()的调用可能会运行很长时间。因此在执行这种耗时很长的方法时,应该永远避免从程序的UI线程中调用此类方法。那会导致在方法返回之前用户界面UI都处于挂起状态,这样用户体验会十分糟糕,并可能会引发ANR(程序没有响应)错误。即使预计到资源会迅速装载完毕,也请记住在用户界面上任何响应时间超过1/10秒的工作都会导致很明显的停顿,并且会给用户留下一个程序很慢的印象。
为了避免用户界面UI线程的挂起,请启动另一个线程来准备MediaPlayer并在完成后通知主线程。不过这就可能要自行编写线程逻辑,这也是使用MediaPlayer时的通常做法,利用其prepareAsync()方法,框架提供一种便利的途径来完成此类任务。这个方法在后台进行媒体的准备工作,并且立即返回。媒体准备完毕后,将会调用MediaPlayer.OnPreparedListener的onPrepared()方法,该Listener通过setOnPreparedListener()指定。
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不与用户交互时没必要再保持播放器的运行(除非在后台播放媒体,这会在下节讨论)。当然,如果activity再次被激活或者再次被启动,则需要创建一个新的MediaPlayer并再次准备之后才能恢复播放。
下面是释放并注销MediaPlayer的语句:
mediaPlayer.release();
mediaPlayer = null;
举个例子,假如停止activity时忘记释放MediaPlayer了,但在activity再次启动时又创建了一个新的播放器,看看可能会产生的问题。众所周知,用户切换屏幕方向(或者其它方式改变设备设置)时,系统默认会重启activity,这样系统资源可能会由于用户在横向纵向间来回旋转而很快耗尽。因为每改变一次方向,就会创建一个永远不会释放的新MediaPlayer。(关于运行时的重启,详见运行时变化的处理。)
如果期望在用户离开activity后还能继续播放“后台媒体”,正如系统内置音乐播放器那样,则需要通过Service来控制MediaPlayer,这在使用带MediaPlayer的服务中讨论。
使用带MediaPlayer的服务
如果程序需要在不显示时还能在后台播放媒体——也就是说期望在用户操作其它程序时也能继续播放——那就必须启动一个Service并从服务中控制MediaPlayer实例。这种情况下应该十分小心,因为用户和系统都期望运行后台服务的应用应该能与系统其它功能同时运行。如果应用不能满足这个要求,用户体验将会很糟糕。本节描述了应注意的主要问题,并提供解决建议。
首先,如同Activity,所有Service默认运行在单个线程中——事实上,如果从同一个应用程序运行activity和服务,它们默认会使用同一个进程(“主进程”)。因此,服务就需要快速处理传入的意图,并且响应这些意图时还不能执行耗时较长的计算工作。如果需要执行繁重的工作或者阻塞调用,必须以异步方式执行这类任务:创建另一个线程来执行,或利用框架提供的异步处理功能。
比如,假设在主线程中用到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 = ... // 在此初始化
mMediaPlayer.setOnPreparedListener(this);
mMediaPlayer.prepareAsync();
// 为了不阻塞主线程而异步准备
}
}
/** 由MediaPlayer准备完毕后调用 */
public void onPrepared(MediaPlayer player) {
player.start();
}
}
在同步操作时,错误通常会以异常或错误代码的方式展现出来;但如果用到了异步资源,应该确保应用程序每次都能正确地获得错误通知。在使用MediaPlayer的时候,可以通过实现一个MediaPlayer.OnErrorListener来达到以上目标,当然还要在MediaPlayer实例中进行设定才行:
public class MyService extends Service implements MediaPlayer.OnErrorListener {
MediaPlayer mMediaPlayer;
public void initMediaPlayer() {
// ...在此初始化MediaPlayer...
mMediaPlayer.setOnErrorListener(this);
}
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
// ... 合适地处理 ...
// MediaPlayer已经切换到Error状态,必须重启!
}
}
记住这点非常重要:当发生错误时,MediaPlayer将切换到Error状态(关于完整的状态图请参阅MediaPlayer类的文档),再次使用前必须重启播放器。
Wake Lock的使用
如果应用程序是为后台播放媒体而设计的,那么即使服务仍在运行,但设备可能会进入休眠状态。因为设备休眠时Android系统会尝试节省电力,任何不必要的手机功能将会关闭,包括CPU和WiFi部件。但是,如果服务正在播放音乐或读取音乐数据流,就需要防止系统对播放进行干扰。
为了确保服务在上述情况下能维持正常运行,必须使用“唤醒锁”(wake lock)。wake lock是一种通知系统的途径:表示应用程序需要用到一些手机空闲时也保持可用的功能。
注意: 应该尽量少用wake lock,并且仅当确实需要时才保持锁定状态,因为它会显著减少设备的电池寿命。
为了确保MediaPlayer播放时CPU能够保持工作,应在初始化MediaPlayer时调用setWakeMode()方法。一旦调用完成,MediaPlayer会在播放过程中保持这个特殊的锁,并在暂停和停止时释放该锁:
mMediaPlayer = new MediaPlayer();
// ... 在此执行其它初始化工作 ...
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
不过,本例中申请的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();
服务经常用于执行一些后台任务,比如读取邮件、同步数据、下载数据等。这些情况下,用户不会明显察觉服务正在运行,甚至可能都不会注意到某些服务曾被中止而过段时间又重新开始运行。
但是就播放音乐的服务而言,显然这是用户能明显察觉的服务,任何中断都会显著影响到用户的体验。此外,该服务还是用户可能期望与其交互的服务。在这种情况下,此服务应该作为“后台服务”来运行。后台服务保持较高的系统重要性级别——系统几乎永远都不会关闭服务,因为服务对于用户而言至关重要。即使是运行在后台,服务仍必须提供状态栏通知,以保证用户知晓服务正在运行,并允许用户打开与服务交互的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);
服务在后台运行期间,此notification将显示在设备的通知区域。如果用户选中此通知,系统会提交一个预先已实现的PendingIntent。以下例子将打开一个activity(MainActivity)。
图1展示了通知如何显示给用户:
图1.后台服务通知的屏幕截图,左图是状态栏的通知图标,右图是展开的View。
只有那些确实运行着用户明显关注任务的服务,才应该保持为“后台服务”状态。一旦不再需要,就应该调用stopForeground()进行释放:
stopForeground(true);
Audio Focus的处理
虽然在任一给定时刻只能运行一个activity,Android仍是一个多任务环境。这向使用音频的应用程序提出了一个特殊的挑战,因为系统只有一路音频输出,但可能会存在多个媒体服务,它们会相互争夺这个音频输出的使用权。Android 2.2之前,没有什么内部机制来解决这个问题,某些情况下这可能会导致用户体验很糟糕。比如在用户听音乐时,其它应用程序需要通知用户一个非常重要的事件,用户可能会由于音乐声音较大而听不到通知提示音。自Android 2.2开始,系统为应用程序提供了一种使用设备音频输出的协调机制。这种机制叫做Audio Focus。
当应用程序需要输出诸如音乐或通知音之类的音频时,应该总是提出audio focus请求。一但获得了focus,就可以自由使用音频输出,但应该时刻注意focus的变化情况。一旦接到放弃audio focus的通知,就应该立即关闭音频或者把音量调低至静音状态(正如“ducking”——此标志表明哪一个更合适),并且在再次获得focus之后才能恢复正常音量播放。
Audio Focus事实上具有良好的协作性。也就是说,虽然期望(强烈建议)应用程序能遵守audio focus规则,但此规则并不是系统强制要求的。如果应用程序需要在失去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) {
// 无法获取audio focus.
}
requestAudioFocus()的第一个参数是AudioManager.OnAudioFocusChangeListener对象,每次audio focus发生改变时都会调用该对象的onAudioFocusChange()方法。因此,必须在服务和activity中都实现这个接口。例如:
class MyService extends Service
implements AudioManager.OnAudioFocusChangeListener {
// ....
public void onAudioFocusChange(int focusChange) {
// 根据focus的改变进行处理...
}
}
focusChange参数标明了audio focus已经发生改变,它可能会是以下值之一(在AudioManager类中定义了所有下列常数):
· AUDIOFOCUS_GAIN:已经获得了audio focus。
· AUDIOFOCUS_LOSS:似乎已失去audio focus较长时间了。这时必须停止所有的音频播放。因为不应该会较长时间地失去focus,这是尽可能多地清理资源的绝好时机。比如应该释放MediaPlayer。
· AUDIOFOCUS_LOSS_TRANSIENT:暂时失去了audio focus,但应该会马上取回来。这时必须停止所有的音频播放工作,但因为可能马上再次获得focus,所以可以保持资源。
· AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:暂时失去了audio focus,但允许继续安静(小声)地播放音频,而不是完全关闭音频。
以下是实现的例子:
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
// 恢复播放
if (mMediaPlayer == null) initMediaPlayer();
else if (!mMediaPlayer.isPlaying()) mMediaPlayer.start();
mMediaPlayer.setVolume(1.0f, 1.0f);
break;
case AudioManager.AUDIOFOCUS_LOSS:
// 长时间失去focus:停止播放并释放media player
if (mMediaPlayer.isPlaying()) mMediaPlayer.stop();
mMediaPlayer.release();
mMediaPlayer = null;
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// 暂时失去focus,但必须停止播放
// 可能会很快恢复播放,所以不释放media player
if (mMediaPlayer.isPlaying()) mMediaPlayer.pause();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// 暂时失去focus,但可以保持较低级别的播放
if (mMediaPlayer.isPlaying()) mMediaPlayer.setVolume(0.1f, 0.1f);
break;
}
}
请记住audio focus API在API level 8 (Android 2.2)以上版本才可用。假如需要支持较低版本的Android,应该采取向后兼容的方案,使得程序能在获得支持时使用此功能、未获支持时则向下平滑过渡。
通过反射机制调用audio focus方法,或者在单独的类中(叫做AudioFocusHelper)实现全部audio focus功能,就可以获得良好的向后兼容性。以下是这种类的一个示例:
public class AudioFocusHelper implements AudioManager.OnAudioFocusChangeListener {
AudioManager mAudioManager;
// 在此写入其它内容,
// 将保存一个接口的引用,用于通知服务focus 已发生改变。
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) {
// 服务获知focus的变动
}
}
仅当检测到系统运行于API level 8以上版本时,才可以创建AudioFocusHelper类的实例。比如:
if (android.os.Build.VERSION.SDK_INT >= 8) {
mAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this);
} else {
mAudioFocusHelper = null;
}
前面提到过,对象可能会消耗相当多的系统资源,因此应该按需保持其运行并在用完后及时调用release()。请显式地调用这个清理方法而不要依赖于系统的垃圾回收机制,这点非常重要。因为等到垃圾回收器回收MediaPlayer时可能已经过去相当长的时间了,回收器是只对内存需求敏感的,而对其他媒体相关的资源短缺是不做判断的。因此,在使用服务的时候,应该总是覆盖onDestroy()方法,以确保释放MediaPlayer:
public class MyService extends Service {
MediaPlayer mMediaPlayer;
// ...
@Override
public void onDestroy() {
if (mMediaPlayer != null) mMediaPlayer.release();
}
}
除了在关闭时释放掉之外,还应该总是寻找其它合适的机会来释放MediaPlayer。例如,当预计到较长时间内无法播放媒体时(比如失去audio focus后),应该明确地释放已有的MediaPlayer并稍后再重新创建。另一方面,如果只是想停止播放一会儿,则应该保持住MediaPlayer,以避免创建和再次准备的开销。
对意图AUDIO_BECOMING_NOISY的处理
在事件到来时,很多编码优秀的音频播放程序都会自动停止播放,因为事件会让音频输出产生噪音(通过外部扬声器播放出来)。比如,用户用耳机听音乐时突然把耳机从设备上拔出来,就可能会产生噪音。不过好在这种现象不会自动发生。如果不实现暂停播放,声音就会从外部扬声器中传出,这可能是用户不愿意听到的。
这种场合下,可以通过处理ACTION_AUDIO_BECOMING_NOISY意图来确保应用程序停止播放音乐,在manifest中加入以下代码,可以注册一个处理意图的接收器:
<receiver android:name=".MusicIntentReceiver">
<intent-filter>
<action android:name="android.media.AUDIO_BECOMING_NOISY" />
</intent-filter>
</receiver>
以下代码注册了MusicIntentReceiver类,用作该意图的广播接收器:
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)) {
// 通知服务来停止播放
// (比如通过一个意图)
}
}
}
从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) {
// 查询失败,处理错误。
} else if (!cursor.moveToFirst()) {
//设备上不存在媒体文件
} 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);
// ...开始处理...
} while (cursor.moveToNext());
}
按以下步骤与MediaPlayer一起使用:
long id = /* 从某处读取 */;
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);
// ...准备并开始播放...