Media and Camera
Media Playback
Android的多媒体框架支持各种格式的媒体类型,你可以很容易的集成音频,视频,图像到你的应用中,通过 MediaPlayer的API,你可以从你的应用资源的媒体文件,或者文件系统的文件,或者网络连接的数据流。播放音频或者视频。
该文章主要展示如何写一个媒体播放的应用,在用户和系统之间交互,从而获得一个更好的用户体验。
注意:你只能使用标准输出来播放音频数据,也就是扬声器或者蓝牙耳机,你不能在通话过程中播放音频文件
基础
下面两个类在Android中用来播放音视频
- MediaPlayer
播放音视频的基本API
- AudioManager
用来管理音频源和设备的音频输出路径
Manifest
在使用MediaPlayer之前,需要manifest 中声明权限
- 网络权限
如果MediaPlayer 播放网络的数据流,需要申请网络权限
<uses-permission android:name="android.permission.INTERNET" />
- Wake Lock权限(Wake Lock Permission )
如果应用需要保持屏幕常亮,或者阻止处理器睡眠(或者调用了MediaPlayer.setScreenOnWhilePlaying() , MediaPlayer.setWakeMode() 方法)。你需要声明如下权限
<uses-permission android:name="android.permission.WAKE_LOCK" />
使用MediaPlayer
MediaPlayer是Android多媒体框架中非常重要的一个组件,MediaPlayer可以使用最少的代码—获取,解码,播放音频和视频。它支持多种不同的多媒体源:
- 本地资源
- 内部URI,例如通过Content Resolver 获取资源
- 外部URL(例如网络流)
下面通过示例来说明几种不同源的多媒体播放.
本地Raw资源(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’资源是系统不用特别解析的文件(比如layout目录下的资源,是需要系统解析的)。不过,这里音频不是raw音频,而一般是经过编码压缩的—注意区别这里raw的意思。
本地URI
Uri myUri = ....; //在这里初始化Uri
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();
外部URL
String url = "http://........"; // URL
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare(); // 可能会很长时间
mediaPlayer.start();
(关键在setDataSource,设定什么数据源)
注意:
使用setDataSource()时,你必须捕获或者抛出 IllegalArgumentException 或者IOException。因为你引用的文件可能不存在
异步准备
使用MediaPlayer 只需要简单的遵守其规则即可,但是有几个很重要的点。例如:
调用prepare()方法需要占用一段时间,因为它需要获取并解码多媒体数据,所以不要在应用的UI线程中调用。以免引起ANR。
你需要启动一个新的线程来处理准备工作,在完成后通知主线程,你可以自己实现自己的线程逻辑,但是更通用的做法是使用Android框架提供好的prepareAsync() 方法,该方法会在后台准备多媒体,然后马上返回,当准备结束后,通过setOnPreparedListener() 方法设定的MediaPlayer.OnPreparedListener中的onPrepared() 会被调用。然后就知道已经准备好了。
管理状态
另一个需要注意的方面是MediaPlayer是基于状态的,所以你在写代码的时候就要注意他的内部状态,因为个别操作只能在特定的状态下操作。如果你在错误的状态进行了操作,系统会抛出一个exception或者导致预想不到的行为。
MediaPlayer 类的文档展示了一个完整的状态图,如下:
当你新建一个 MediaPlayer ,就处于Idle状态,这时,你应该调用 setDataSource()来初始化。从而进入Initized状态。
然后通过调用 prepare() 或 prepareAsync() 来准备,准备结束后,会进入Prepared状态。这时你可以调用start()方法来播放媒体。
这时,如图表所示,你可以调用 start(), pause(), 或 seekTo()来转移到 Started, Paused 或PlaybackCompleted 状态。
需要注意的是,如果你调用了stop(),你不能在调用start()重新播放,而是必须再次prepare好了才可以播放。
你需要在写代码的时候,时刻注意这个状态图,如果在错误的状态调用了不该调用的方法,是导致bug的主要原因。
释放 MediaPlayer
MediaPlayer需要消耗系统资源,因此,没必要的话,就不要让MediaPlayer实例再存在了,你可以调用release()方法来确保所有分配给MediaPlayer的系统资源全部释放掉。例如,你在使用MediaPlayer,在你的activity的onStop()方法中,你可以释放掉MediaPlayer。(除非是后台播放媒体,下一章节讨论。)。当你的activity恢复或者重新启动时,你需要重新创建一个新的MediaPlayer实例,并在播放前再次调用prepare。
示例代码:
mediaPlayer.release();
mediaPlayer = null;
如果你忘记在activity停止后释放 MediaPlayer 。但是activity再次启动的时候又重新创建了一个MediaPlayer.(在系统屏幕旋转时,或者configuration改变的时候,系统会默认重新启动activity)如果用户来回的旋转手机,很快系统资源就会想因为创建了太多MediaPlayer实例而消耗掉过多系统资源。
使用Service播放多媒体
如果想应用不在屏幕显示的时候,能够后台播放多媒体,你可以启动一个Service,并在Service里控制MediaPlayer实例。
用户对应用在后台运行时,如何和系统的其他部分交互有自己的期待,你的应用如果不能够满足这些期待,用户可能会觉得体验不好,该章节讨论你应该注意的问题和如何解决它们的建议。
异步运行
首先,就像Activity,Service中的工作都是默认运行在单线程中的,实际上Activity和Service都是默认运行在同一个线程中的——————主线程。因此,Service必须快速的处理业务,并且不能进行耗时的计算。如果必须做耗时的计算,你需要异步的处理这些tasks——可以是你自己实现另一个线程,或者使用框架提供好的异步处理机制。
当在你的主线程中使用MediaPlayer时,你应该优先使用 prepareAsync() 而非prepare(),然互实现MediaPlayer.OnPreparedListener 接口来接收prepare准备好了的事件,然后开始播放。
示例代码:
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();
}
}
示例中的自定义Service实现了MediaPlayer.OnPreparedListener 接口,并在onPrepared方法中来调用MediaPlayer的start方法来播放。
(注意需要调用mMediaPlayer.setOnPreparedListener(this)来绑定监听器)。
处理异步的错误
当进行同步操作时,一般错误会通过异常来通知,或者返回错误码。当使用异步资源是,你应该确保你的应用能够妥善的处理错误。在 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!
}
}
上述代码中自定义Service 实现MediaPlayer.OnErrorListener 。并通过mMediaPlayer.setOnErrorListener(this);
进行绑定。在接口的onError进行错误的处理。
注意:
一旦错误产生,MediaPlayer就会进入状态图中的**Error**状态。你再次使用MediaPlayer前需要重建MediaPlayer
使用wake locks
当设计一个后台播放媒体的应用时,设备可能会在你的Service运行时进入睡眠状态,因为Android系统为在睡眠的时候节省电池。系统会尝试任何非必须的特性。包括CPU和WiFi硬件。但是,如果你的service在播放或者读取音乐,你还是希望能够阻止系统妨碍你的播放。
为此,你应该使用wake locks。wake lock可以理解为休眠锁,用来通知系统你的应用需要使用到一些系统特性,这样当手机进入空闲状态是依旧可用。
注意:
一定要保守的使用休眠锁,当真的需要时再持有它们,否则会大量消耗电量
为了保证CPU在MediaPlayer 播放时继续运行,调用在初始化MediaPlayer 是调用setWakeMode() 方法。 在MediaPlayer 播放的时候持有该锁,在暂停或者停止时释放该锁。
示例代码:
mMediaPlayer = new MediaPlayer();
// ... other initialization here ...
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
但是,上面的例子只能保证CPU不睡眠,如果你的多媒体需要使用到网络,并且使用的是WiFi.你还需要持有WifiLock—-也需要你手动的获取和释放。
示例代码:
WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock");
wifiLock.acquire();
暂停或停止播放的时候,或者不再使用网络时,记得释放该锁:
wifiLock.release();
作为前台Service运行
Service一般是用来运行后台任务,比如获取邮件,异步数据,下载内容等。这种情况,用户不会意识到Service的运行,也可能即使Service被打断并重启也不会知道。
但是对于播放音乐这种Service而言,用户是很清楚的知道Service的运行的,所以播放期间是不能被打断的。并且用户希望能够在Service运行期间能够进行交互,这种情况下,Service应该作为“前台Service”来运行。前台Service在系统中具有较高的优先级—–系统几乎不会kill掉这种Service。当在前台运行时,Service还需要提供一个状态条来保证用户能够了解到运行中的Service的状态,并允许用户打开一个activity来和Service交互。
为了把你的Service变成前台Service。你需要创建一个通知( Notification)来展示状态条,并在Service中调用 startForeground()来设为前台Service。
示例代码:
String songName;
// 把歌曲名称赋给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在前台运行时,你设置的通知是在通知栏中可见的,如果用户点击了这个通知,系统会调用你设定的PendingIntent (在上述代码中,会启动MainActivity)。
下图展示了通知是如何显示的:
只有Service确实在运行时,才应该设定通知。如果Service不再跑了,需要通过调用stopForeground()来释放:
stopForeground(true);
更多信息参考Service和通知栏章节。
操作音频焦点(Handling audio focus)
尽管同一时刻只能有一个Activity运行,但Android是一个多任务系统,这给使用音频的应用带来了特殊的挑战,因为只有一个音频输出,但是可能会有多个媒体Service在竞争使用。在Android2.2之前,并没有内置的机制来处理这个问题,导致一些情况体验不太好。比如,在用户听音乐时,另一个应用需要通知重要事件,用户可能因为大声的音乐在播放而没有听到事件通知。
从Android2.2开始,系统提供了让应用协商的机制来获得设备的音频输出。这种机制被称作音频焦点(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.
}
首先获得AudioManager服务,然后获取音频焦点,在确定获取后,播放音频。
requestAudioFocus()的第一个参数是一个AudioManager.OnAudioFocusChangeListener监听器,监听器的 onAudioFocusChange()方法会在音频焦点变化时被调用。所以你可以在你的Service或者Activity中实现这个监听器接口,并在焦点变化时做相应处理。
示例代码:
class MyService extends Service
implements AudioManager.OnAudioFocusChangeListener {
// ....
public void onAudioFocusChange(int focusChange) {
// Do something based on focus change...
}
}
代码中onAudioFocusChange(int focusChange)的focusChange 参数告诉你音频焦点是如何变化的,其值可能是下面中的一种:
focusChange | 含义 |
AUDIOFOCUS_GAIN | 你已经获得了音频焦点 |
AUDIOFOCUS_LOSS | 你大概已经失去音频焦点有段时间了,你必须停止音频的播放,因为你需要假设在很长一段时间里得不到焦点,这里比较适合清除你的资源,比如:释放你的MediaPlayer |
AUDIOFOCUS_LOSS_TRANSIENT | 你暂时失去了音频焦点,但是应该很快就可以再次得到它,你必须停止你的音频播放,但是可以保持你的资源,再次获得焦点后可以快速再次播放 |
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK | 你暂时失去了音频焦点,但是被允许悄悄的播放音频(低音量) |
代码示例:
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:
//停止播放,释放资源
if (mMediaPlayer.isPlaying()) mMediaPlayer.stop();
mMediaPlayer.release();
mMediaPlayer = null;
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// 停止播放
// 但是不释放资源
// 因为可能很快再次得到焦点
if (mMediaPlayer.isPlaying()) mMediaPlayer.pause();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// 失去焦点,但是继续播放
// 把声音音量关小
if (mMediaPlayer.isPlaying()) mMediaPlayer.setVolume(0.1f, 0.1f);
break;
}
}
一定记住Android2.2(API leve8)之后才可以使用音频焦点的API。如果你想支持较早版本的Android。你应该设定一个反相兼容机制来在API不可用的时候正常运行。
为了实现反相兼容,你可以通过反射来调用音频焦点的方法,或者通过自定义一个单独的类来实现音频焦点的特性。
如下面实现的AudioFocusHelper类所示:
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
}
}
然后你可以只在检测到系统运行API level及以上的时候才在创建一个AudioFocusHelper 的实例。示例代码:
if (android.os.Build.VERSION.SDK_INT >= 8) {
mAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this);
} else {
mAudioFocusHelper = null;
}
清理
如前面所提到的,MediaPlayer对象会消耗数量可观的系统资源,所以你应该只有在需要的时候保持它,不需要的时候即使调用release()方法来释放。明确的调用这个清理的方法比依赖系统垃圾回收机制重要得多,因为系统垃圾回收肯呢过需要很长时间才能回收MediaPlayer。
所以在使用Service的时候,需要重写onDestroy()方法来保证Service结束时顺利释放MediaPlayer。
示例代码:
public class MyService extends Service {
MediaPlayer mMediaPlayer;
// ...
@Override
public void onDestroy() {
if (mMediaPlayer != null) mMediaPlayer.release();
}
}
除此之外,还应该在任何需要释放MediaPlayer的地方都做到有效释放。比如:
如果打算一段时间内不再播放音频(或者失去了音频焦点)。你definitely应该释放存在的MediaPlayer对象,并在之后需要时重新创建。
当然,如果你只是打算停止播放音频一小会,那你可以不用释放它,省得频繁创建并准备。
处理AUDIO_BECOMING_NOISY Intent
许多非常好的应用在播放音频时,会在发生意外导致音频变成噪声时(扬声器输出),停止播放。
例如,当用户用耳机听音乐时,不小心耳机断开了。
不过这种机制不会自动发生,需要程序猿去实现。如果你不去实现,音频就可能通过扬声器大声的播放出来。
你可以通过处理 ACTION_AUDIO_BECOMING_NOISY intent来在这种情况下停止音乐。
首先,你需要在manifest文件中注册一个接受该intent的receiver。
<receiver android:name=".MusicIntentReceiver">
<intent-filter>
<action android:name="android.media.AUDIO_BECOMING_NOISY" />
</intent-filter>
</receiver>
然后实现这个broadcast receiver :
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)) {
// 停止音频播放
// 比如,可以通过Intent来停止
}
}
}
通过Content Resolver获取多媒体资源
一个媒体播放器应用的另一个很有用的特性是:能够获取用户设备上的音乐资源。你可以通过查询external media的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()) {
// 设备上没有media资源
} 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 = /* 例如上面代码中获取的某个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);
// ...prepare and start...