MediaPlay 用来控制音频、视频文件和流的回放。
- 状态图
-
回放控制是通过一个状态机来管理的。
椭圆图代表MediaPlay实例可能存在的一个状态;
弧线代表使状态之间转换的操作;
单箭头代表同步的方法调用;
双箭头代表异步的方法调用。
一个MediaPlay实例有以下状态:
- 当new或者reset被调用,来创建MediaPlay实例后,它是Idle状态。
- 但release被调用后,它是end状态,在这两者之间是它的生命周期。
- 通过new来构造实例和通过reset来产生实例,是有区别的,通过new实际就是通过create为给定的uri来创建mediaplayer,如果创建成功,其prepare方法会被自动调用,也自动调用了setDataSource,即完成了初始化。Reset方法会把实例转到未初始化状态,在调用reset之后,需要再次通过setDataSource,prepare方法来初始化它。
- 在创建好MediaPlayer实例后,可以通过OnErrorListener.onError()指定错误的回调函数。当在不正确的状态下,调用某些方法时出错,会调用onError,同时把状态转到Error状态。
- Mediaplayer实例不在使用,要立即调用release释放相关的资源。Release之后回到end状态,它将不可再使用,也没有任何方法可以让它回到其他状态。
- 当有Error发生,就会转到Error状态,从Error状态恢复、在Error状态下再次使用,就要用reset来把它恢复到Idle状态。
- 一个很好的编程实践是要注册OnErrorListener,来提防来自内部播放引擎的错误提示。
- setDataSource会把状态转移到初始化状态。
- Mediaplayer在回放started之前,必须先进入Prepared状态。
- 有两种方式可以进入到Prepared状态,一种是同步的方式prepare(),一种是异步的方式prepareAsync()。Prepare()是同步的,对于文件来说,这个调用时Ok的,它会blocks直到mediaplayer准备好了回放;prepareAsync()是一步的,对于流类型的数据源,应该调用这个,它会立即返回,而不是blocking直到缓冲了足够的数据,当调用返回时它会先进入到Preparing状态,然后内部的播放引擎会继续完成剩下的准备工作,直到准备工作完成。当准备工作完成或者prepare()返回,如果注册了OnPreparedListener.OnPreapred()接口会被调用。在Prepared状态,一些属性才可以通过相关的方法调用。
- 调用start()开始回放,start()成功返回,就会进入到stared状态,isPlaying()可以被调用来检查当前是否在started状态。
- 在开始状态,播放引擎会调用用户提供的OnBufferingUpdateListener.onBufferingUpate()回调接口,当然前提是这个listener事先被注册了。当操作音视频流时,这个回调让应用程序可以记录跟踪缓冲区的状态。
- 通过pause(),让回放进入到暂停状态。
- 通过start(),可以恢复暂停状态的回放,然后状态回到Started状态。
- Stop(),让回放从started、Paused、Prepared、PlaybackCompleted状态到Stopped状态。一旦进入到Stopped状态,只有再次通过Prepare,进入到Prepared状态才能继续。
- 通过seekTo(long,int)调整播放位置。即使是异步调用,这个方法也是立即返回的,尤其是在流类型的情况下,实际的seek操作可能需要花些时间完成。当实际的seek操作完成,播放引擎调用注册的OnSeekComplete.onSeekComplete()回调。seekTo可以在prepared、Paused、playbackCompleted状态调用,当在这些状态调用时,如果这个流有视频帧,并且请求的位置有效,相应的那帧视频帧会被显示。
- 可以通过getCurrentPosition来检索当前的播放位置,这可以用于跟踪播放进度。
- 当回放到达流末尾,回放结束。如果通过setlooping设置了循环,mediaplayer应该停留在Started状态。如果回放是false,注册的OnCompletion.onCompletion()的回调被调用,这个调用表示现在是播放完成状态。
二,下面是怎样写一个媒体播放的应用。
MediaPlayer 是播放音频和视频的主要api。
AudioManager 管理设备上的音频源和音频输出通道。
Internet Permission,如果需要使用网络上的流,请求访问网络,需要添加权限
<uses-permission android:name=”android.permission.INTERNET”>
Wake Lock Permission,如果需要防止屏幕变暗,或者防止处理器休眠,或者使用MediaPlayer.setScreenOnWhilePlaying(),MediaPlayer.setWakeMode(),需要申请权限
<uses-permission android:name=”android.permission.WAKE_LOCK”>
MediaPlayer.java 可以获取,解码,播放音视频,支持的数据源:本地数据,来自数据库的URI,来自网络的URL。
播放一个本地元数据(保存在res/raw/目录下的资源):
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file);
mediaPlayer.start();
这里的raw数据源,是一个文件,系统不需要对它做任何解析,但是文件内容应该是有合适的编码,并且是被支持的格式格式化过的媒体数据。
播放一个URL资源,来自于ContentResolver:
Uri myUri = ;
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();
播放来自网络的资源:
String url = “http://”;
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare();//这个调用可能耗时,因为要缓冲…
mediaPlayer.start();
异步的Preparation。
Prepare()方法,可能执行较长时间,因为这可能涉及到获取、解码媒体数据,所以这个方法不应该在UI线程调用。相对的prepareAsync()可以在UI线程被调用,它在后台开始准备数据之后,立即返回了。
看下这两个方法的区别,java层没有太多注意的地方,直接看c++层代码:
mediaplayer.cpp
status_t MediaPlayer::prepareAsync()
{
ALOGV("prepareAsync");
Mutex::Autolock _l(mLock);
return prepareAsync_l();
}
status_t MediaPlayer::prepare()
{
ALOGV("prepare");
Mutex::Autolock _l(mLock);
mLockThreadId = getThreadId();
if (mPrepareSync) {
mLockThreadId = 0;
return -EALREADY;
}
mPrepareSync = true;
status_t ret = prepareAsync_l();
if (ret != NO_ERROR) {
mLockThreadId = 0;
return ret;
}
if (mPrepareSync) {
mSignal.wait(mLock); // wait for prepare done
mPrepareSync = false;
}
ALOGV("prepare complete - status=%d", mPrepareStatus);
mLockThreadId = 0;
return mPrepareStatus;
}
从以上源码可以看出,这两个方法都会调用prepareAsync_l,再往下调用播放引擎开始准备工作,区别是preapre()方法在调用prepareAsync_l之后,通过mSignal.wait(mLock)进入等待,唤醒的条件是prepare done,也即是收到MEDIA_PREPARED消息。
使用Service控制MediaPlayer。
如果希望app不在前台时,media可以在后台播放,需要启动一个service来控制mediaplayer实例。
异步的运行:
默认情况,所有service中的工作都在一个线程,这点跟activity类似。在同一个应用里面运行一个activity和一个service,他们默认是在同一个主线程中。因此,这种情况下,service需要快速的处理输入事件,在返回结果前,不应该做太多计算。如果有重量级的工作或者阻塞的调用,应该异步的来做:使用另外的线程,或者使用framework中的异步处理组件。
下面的例子,在主线程使用Mediaplayer,所以用prepareAsync,而不是prepare,同时应该实现MediaPlayer.onPreparedListener接口,以便在准备工作完成时被通知到。
Public class MyService extends Service implements MediaPlayer.OnPreparedListener {
Private static final String ACTION_PLAY= “com.example.action.PLAY”;
MediaPlay mMediaPlayer = null;
Public int onStartCommond(Intent intent, int flags, int startId){
If (intent.getAction().equals(ACTION_PLAY)){
mMediaPlayer = …//initialize it
mMediaPlayer.setOnPreparedListener(this);
mMediaPlayer.prepareAsync();//这里不会block。
}
}
//准备完成,会被调用
Public void onPrepared(MediaPlayer player){
Player.start();
}
}
处理异步操作的错误:
在同步调用时,错误会被正常的通知,同时带有exception信息,错误码。但是,在使用异步资源时,要确保错误可以适当的通知到应用,可以通过注册MediaPlayer.OnErrorListener来实现:
Public class MyService extends Service implements MediaPlayer.OnErrorListener{
MediaPlayer mMediaPlayer;
Public void initMediaPlayer(){
//initialize the mediaplayer
mMediaPlayer.setOnErrorListener(this);
}
@Override
Public Boolean onError(MediaPlayer mp, int what, in extra){
//mediaplayer moved to Error state.再次使用,必须reset。
}
}
使用唤醒锁。
当回放音视频时,不想让系统休眠,可以使用wake locks,它可以给系统发个信号,表示应用正在使用系统功能,即使当前手机是idle状态,也要保持可用。
在初始化MediaPlayer之后,可以通过setWakeMode来让cpu保持运行,在playing期间,mediaplayer会持有一个特定锁,在paused、stopped之后释放这个锁。
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManger.PARTIAL_WAKE_LOCK);
上面的方式,只是让cpu保持唤醒,如果需要使用网络资源,就要使用wifi-lock来保持wifi可用。这时要手动申请、释放。
在使用remote URL开始preparing时,创建、申请wifi-lock:
WifiLock wifiLock = ((WifiManager)getSystemService(Context. WIFI_SERVICE)).createWifiLock(WifiManager.WIFI_MODE_FULL, “myLock”);
WifiLock.acquire();
在pause、stop时,或不在需要网络时,释放:
wifiLock.release();
作为一个前台Servie来运行。
通常service是放到后台去执行,这种情况service被中断然后又重新执行,用户通常是意识不到的。但是,如果用service来播放音乐,任何中断,都会被明显的感受到。另外,用户可能需要在service执行的过程中与其交互。这些情况下,service应该以foreground service来运行,前台service有更高的优先级,系统尽量不去kill它。当运行与前台时,service必须提供一个状态栏提示,以确保用户可以意识到这个运行的service,允许用户跟这个service交互。
在创建一个Notification后,调用startForeground使service运行与前台:
String 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 |= Notificaiton.FLAG.ONGOING_EVENT;
Notification.setLatestEventInfo(getApplicationContext(), “MusicPlayerSample”,
“Playing:”+songName, pi);
StartForeground(NOTIFICATION_ID, notificaiton);
当点击状态栏的这个notification时,系统会调用PendingIntent,来启动指定的Activity。
不在需要这个service时,记得调用:
stopForeground(true);
处理音频焦点。
Android是一个多任务的环境,这对于使用audio的app是一个挑战,因为只有一个音频输出通道,会有多个媒体服务竞争它的使用。从android2.2开始,平台提供了一种方式来交涉设备的音频输出通道的使用,这个机制就是Audio Focus。
当app需要输出music或者notification这样的音频时,需要去请求audio focus,一旦获取到focus,就可以自由的使用音频的输出通道,但是要保持对focus changes的监听,一旦被通知失去了焦点,应该立即kill这个audio或者把它转为安静的级别(也叫ducking),再次得到焦点时,恢复正常的回放。
音频焦点本质上是协作的方式,也就是说,建议app去遵从音频焦点的准则,但是这个规则不会被系统强制执行。如果app在失去焦点后,仍然要大声的播放music,系统是不会做什么来阻止这个行为。但是,用户的体验会较差。
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.
}
requestFocus()的第一个参数是AudioManager.OnAudioFocusChangeListener,在audio focus有变化时,onAudioFocusChange()会被调用,在service和activity中最好实现这个接口。
Class MyService extends Servie implements AudioManager.OnAudioFocusChangeListener {
Public void onAudioFocusChange(int focusChange){
//
}
}
其中的参数focusChange,值是AudioManager中定义的一些常量:
AUDIOFOCUS_GAIN:已经获取了audio focus。
AUDIOFOCUS_LOSS:你会失去audio focus一段时间,必须停止所有的音频回放,因为你可能一段时间不会重新获取到焦点,尽可能在这个时候去清理资源,比如释放掉MediaPlayer实例。
AUDIOFOCUS_LOSS_TRANSIENT:短暂的失去焦点,很快会再次获取到,也应该停止掉所有的音频回放,但是可以保留所有的资源,因为可能很快再次获取焦点。
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:临时地失去焦点,但是允许继续播放音频(以较低的声音播放),而不是完全停止掉audio。
Public void onAudioFocusChange(int focusChange){
Switch (focusChange) {
Case AudioManager.AUDIOFOCUS_GAIN:
//resume playback
If (mMediaPlayer == null) initMediaPlayer();
Else if (!mMediaPlayer != null) mMediaPlayer.start();
mMediaPlayer.setVolume(1.0f, 1.0f);
break;
case AduioManager.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;
}
}
AudioFocus这个api要在Android2.2之后使用,如果要向前兼容,可以通过反射机制调用AudioFocus的方法,或者通过一个单独的类实现所有audio focus 的特性,如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
}
}
如果系统运行在api8之前,可以创建AudioFocusHelper实例。
If (android.os.Build.VERSION.SDK_INT >= 8)
MAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this);
Else
mAudioFocusHelper = null;
执行清理。
MediaPlayer实例,会消耗一些重要的系统资源,所以应该只在需要的时间内保留,在完成后调用release();明确的调用清除方法,比依赖系统的垃圾回收更重要,因为垃圾回收机制在回收mediaplayer之前会花些时间,因为他只对内存需求敏感,而对media相关的资源缺乏不敏感。所以,重写onDestory方法时有必要的。
Public class MyService extends Servie {
@Override
Public void onDestory(){
If (mMediaPlayer 1= null) mMediaPlayer.release();
}
}
当然,除了在关闭时释放,也应该寻找其他的机会释放mediaplayer。
处理AUDIO_BECOMING_NOISY intent。
优秀的app,当会让声音变成噪音的事件发生时(比如通过外放输出),应该要自动停止回放。比如当用耳机听音乐时,耳机突然跟设备断开了,不管怎样,这个行为不是自动发生的,如果没有实现这个特性,那么声音会通过外放输出,这个情况可能不是用户想要的。
通过在manifest注册这个intent,可以确保你的应用在这种情况下能停止音乐播放。
<receiver android:name = “.MusicIntentReceiver”>
<Intent-filter>
<action android:name=”android.media.AUDIO_BECOMING_NOISY”/>
</Intent-filter>
</receiver>
Public class MusicIntentReceiver extends android.content.BroadcastReceiver {
@Override
Public void onReceiver(Context ctx, Intent intent){
If (intent.gatAction().equals(
Android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
//single to stop playback
}
}
}
从content resolver检索media。
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.
} else if (!cursor.moveFirst()) {
//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);
}while (cursor.moveToNext())
}
Long id = //retrieve it from somewhere;
Uri contentUri = ContentUris.withAppendedId(
Android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
mMediaPlayer.setDataSource(getApplicationContext(), contentUri);