media and camera 框架之一: media playback

来源:互联网 发布:数据库主键有什么用 编辑:程序博客网 时间:2024/05/09 14:42

Android多媒体框架包括支持播放多种通用的媒体类型,因此开发者可以很容易地整合音频、视频和图片到你的应用中。来自你的应用程序资源(如raw文件夹中资源)、android文件系统中独立文件或者来自基于网络连接数据流的音频和视频文件,都可以通过调用MediaPlayer接口播放。

这篇文档向你展示了如何开发一个具有良好的系统性能和良好的用户体验媒体播放类应用。

注意:你只能在标准的媒体输出设备上播放音频数据。一般来说,这些设备是移动设备的扬声器或者蓝牙耳机。当在打电话会话状态时,不可以播放音频文件。

基本要素:

在android架构中以下两个类被用于播放音频和视频:

MediaPlayer: 该类包含播放音频和视频的主要的接口。

AudioManager:该类管理设备上的音频来源和音频输出。

manifest声明:

在开始在应用程序上开发媒体播放之前,务必确保你的manifest包含合适的相关特性的用户许可声明:

internet许可:如果你使用MediaPlayer播放基于网络的数据流,你的应用程序必须请求连接网络的权限。

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

wake lock许可:如果你的播放类应用需要阻止屏幕变暗或者在休眠中保持运行,或者需要使用MediaPlayer.setScreenOnWhilePlaying()或MediaPlayer.setWakeMode()这些方法,你必须请求下面的许可:

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

使用MediaPlayer:

媒体框架的其中一个重要组件就是MediaPlayer类.该类的一个实例对象以最简单的设置就可以获取、解码和播放音频和媒体资源。它还支持几种不同的媒体资源,比如:

本地资源

内部的URIs 比如从content resolver里获取的一个uri

外部URIs(如stream)

android支持的媒体格式类型可以参考 Android Supported Media Formats文档。

下面是一个示例,展示播放作为一个本地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资源是一个文件,系统不会尝试使用特殊方法解析的它。 但是,该资源的内容不能是原始的音频数据。它必须是一种系统支持的经过合适编码和格式化的媒体文件。

下面展示如何播放来自系统本地可用的URI资源(举例如从content resolver获取资源):

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

通过http流播放来自远程的URL资源例子如下:

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

通过http流播放来自远程的URL资源例子如下:

String url = "http://........"; // your URL hereMediaPlayer mediaPlayer = new MediaPlayer();mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);mediaPlayer.setDataSource(url);mediaPlayer.prepare(); // might take long! (for buffering, etc)mediaPlayer.start();

注意:如果你传递一个在线的媒体文件URL数据流,那么该文件必须能够渐进式下载。

警告:当你使用setDataSource()方法的时候必须捕获或者传递IllegalArgumentExceptionIOException异常,因为你应用的文件可以不存在。


异步preparation:

原理上可以直接使用MediaPlayer。但是,记住对于典型的android 应用来说有几个方法需要正确集合在一起使用这一点非常重要。比如,调用prepare()就需要在一段时间后才能执行,因为它可能包含获取和解码媒体数据。所以,与任何其他方法一样,它需要一段时间来执行,你不能从应用程序的主线程中调用它。如果那样做了会导致主线程挂起直到它执行完毕,那将会是非常糟糕的用户体验,而且可能会导致ANR错误。即使预期你的资源加载非常快,但记住任何在主线程中响应时间超过十分之一毫秒的事情都会产生明显的停顿,以至于会给用户产生一种你的应用运行很慢的印象。

为了避免主线程被挂起,需要建立新的线程直线MediaPlayer的prepare并在该线程结束时通知主线程。而且,当你自己去写线程逻辑时,你会发现通过使用框架提供的repareAsyn()这个方便的方法去完成该线程任务是一种如此常见的使用MediPlayer的模式。该方法在后台开始准备媒体,结束时立即返回。当媒体准备完毕,通过调用setOnPreparedListerner()方法来配置MediaPlayer.OnPreparedListerner的onPrepared()方法。

状态管理:

MediaPlayer的另一面你需要记住的是它是基于状态的。也就是说,MediaPlayer包含一个内部状态,在你写代码的时候必须意识到这点。因为一些操作只在一些特殊的状态才有效。如果在错误的状态执行了一个操作,系统可能会抛出异常或者导致一些意想不到的行为。

在MediaPlayer类中的文档展示了一张完整的状态图,它阐明了哪些方法可以从一个状态转移到另外一个状态。比如,当你创建一个新的MediaPlayer对象,它是在IDLE状态。在那个状态,你可以通过调用setDataSource()来初始化该对象,从而使对象变为初始化状态。初始化之后,必须通过调用prepare()方法或者prepareAsync()方法来准备该对象。当MediaPlayer准备完毕之后,该对象将进入到Prepared状态,那就意味着你可以调用start()方法来播放媒体了。在那时,如表中举例说明,你可以通过来回调用start()、pause()、seekTo()方法及其他方法,实现在Started,Paused和PlaybackCompleted状态之间切换。当你调用stop()方法时,请注意你不能在调用start()方法直到你再次准备好MediaPlayer。

当你在写MediaPlayer对象的交互代码时要一直把该状态图记在心里,因为在一些不该调用该方法的状态中调用了该方法是引起bugs的常见原因。

释放MediaPlayer:

一个MedaiPlayer对象是非常消耗珍贵的系统资源的。因此,你需要而外注意确保没有挂起不再需要的MediaPlayer对象。当确定这样做时,你应该经常调用release()方法确保系统分配给它的资源完全地释放。例如,当你正在使用MediaPlayer而你的活动页面收到调用stop()的方法,你必须要释放MediaPlayer,因为当你的活动页面不再和用户交互时(除非你在后台播放媒体,这中情况将在下面一部分讲述),继续保留该媒体对象没有一点意义。当你的活动页面恢复或者重新开始时,你需要创建一个新的MediaPlayer且在继续播放之前再次准备好媒体。

下面代码展示了如何释放和注销你的MediaPlayer对象:

mediaPlayer.release();mediaPlayer=null;
作为一个示例,它考虑到了当活动页面再次开始创建了一个新对象但在活动页面停止时你忘记释放该对象可能会导致的一些问题。也许你可能知道,当用户切换屏幕方向(或者通过另外的方式改变设备设置)时,系统是通过重新启动活动页面(通常做法)来处理这种变化,因此当用户在设备前后的横向与纵向来回切换时,将可能会很快消耗完系统资源,因为在每一次方向变化时,你都创建了新的MediaPlayer对象且都没有被释放(关于更多运行时重新重启的信息请查看Handling Runtime Changes)。

你可能会疑惑如果你还想继续播放"后台媒体"即使用户离开了活动页面会怎么样呢,嵌入的音乐类应用的运转就类似这种方式。在这种情况下,你需要通过Service来控制MediaPlayer对象,正如下面将要论述的通过Service方式使用MediaPlayer.


通过Service方式使用MediaPlayer:

如果你想让你的媒体在后台播放甚至当你的应用程序没有在打开在屏幕上----也就是,你想它继续播放它,当使用者在与其他应用交互时,那么你需要启动一个Service来控制你的MediaPlayer实例。你应该谨慎地做这样的设置,因为用户和系统对于一个应用程序后台运行的服务该如何在系剩余的资源中进行交互有一些预期。如果你的应用程序没有达到这些预期,用户可能会有些不好的体验。这部分将描述些你需要注意到的主要的问题,且提供一些如何着手处理他们的意见。

异步运行:

首先,像一个活动页面,一般来说一个Service中所有的工作都可以在一个单独的线程中完成,事实上如果你正在运行在同一个应用程序中的一个活动页面和一个Service,一般来说它们是使用同一个线程(即主线程)。因此,服务进程需要即时处理请求接入的客户端且当相应他们时不能执行漫长的计算操作。如果任何可预见的含有大量的工作或者阻塞方法的任务被执行,你必须异步执行这些任务:可以通过自定义新的线程来实现,也可以通过直接调用框架中现有的异步处理工具来实现。

从实例中看到,当在主线程中使用MediaPlayer对象时,你应该调用prepareAsync()方法而不是prepare()方法,并且需要实现MediaPlayer.OnPreparedListener()方法来静听媒体准备完毕时的通知,然后你可以开始播放媒体。例如:

public class MyService extends Service implements MediaPlayer.OnPreparedListener {    private static final String 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.OnErrorListener接口来实现提示的功能,并把该接口的实现类设置到实例上:

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状态改变错误(请看MediaPlayer类中的全状态图的文档),你必须要重置该对象之后才能重新使用该对象。

使用唤醒锁:

当设计在后台播放媒体的应用程序时,当你的服务程序正在运行时设备有可能进入休眠状态。因为在设备休眠时android系统会尽可能地节省耗电量,系统试图关闭所有不需要的手机特性,包括cpu和wifi的硬件资源。然而,如果你的服务进程正在播放或输出音乐,你需要阻止系统干涉你的播放。

在这种情况下为了确保你的服务进程继续运行,你必须使用“唤醒锁”,唤醒锁是一种向系统发送信号的一种方式,告诉系统你的应用使用了一些需要在手机休眠的时保存有效的特性。

注意:你应该总是保守地使用"唤醒锁",且在确实需要的时候保留他们,因为他们会明显地减少设备的电池寿命。

为了确保当你的MediaPlayer播放时cpu一直运行,应该调用setWakeMode()方法当你初始化MediaPlayer对象时。一旦你这样做了,MediaPlayer对象在播放时保持这个特殊的锁在停顿或者停止时会释放该锁:

mMediaPlayer = new MediaPlayer();// ... other initialization here ...mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);

然而,在上面的例子中唤醒锁只在CPU运行状态时得到保证。如果你正在通过网络传输媒体而wifi此时也被占用,你可能也想持有这个wifi锁,但这个锁你必须手动捕获和释放它。所以,当你使用远程URL来开始准备MediaPlayere对象时,你应该创建和捕获WIFI锁。例如:

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

当你暂停或停止媒体,或者不再需要网络时,你应该释放该锁对象:

wifiLock.release();

像前台服务一样运行

服务进程通常被用于执行后台人后,如捕获邮件,同步加载数据,下载内容及其他可能的事情。在这些情况中,用户不主动了解进程服务的执行,而且可能根本不会注意一些进程服务被中断或者重启。
但是仔细想一下到服务进程播放音乐的情况。很清楚地发现,这是一个用户会主动注意的且会受到其他中断严重干扰的服务进程。此外,用户可能希望在服务进程运行期间与它进行交互。在这种情况下,服务进程应该像“前台服务”。一个前台服务在系统中拥有较高的重要性——一个是系统可能永远不会杀死的进程,因为它对用户来说有着直接的重要性。当在前台运行时,服务都必须提供状态通知栏确保用户明确看到运行的服务进程且允许他们打开活动页面与这些服务进程进行交互。
为了把你的服务进程变为其前台服务进程,你必须向状态栏提供一个新的Notification对象,再调用service中的startForeground().例如:
String songName;// assign the song name to songNamePendingIntent 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。在上面的例子中,它就打开了一个活动页面(如MainActivity)

图1 展示了你的通知如何呈现给用户


图1 前台服务进程通知的屏幕截图,展示了状态栏中的通知图标(左图)和展开后的图示(右图)

你应该只在当你的服务进程确实执行一些用户主动关注的动作时保持在“前台服务”状态。一旦不再需要,你应该调用stopForeground()来释放掉它。

处理音频焦点

即使只能有一个活动页面可以在任意给定的时间运行,但android任然是多任务同时运行的环境。这里对应用程序使用音频提出一个特殊的挑战,因为只有一个音频输出并且同时可能有几个媒体服务进程来竞相使用它。在Android2.2之前,没有内置的机制来提出这个问题,从而在一些情况下导致了糟糕的用户体验。例如,当用户正在听音乐时此时别的应用需要通知用户一些重要的事情,用户可能会没有听到通知的提示音因为大声的音乐声。从Android2.2开始,平台提供为应用程序提供一种方式去协商使用设备的音频输出。这种机制被叫做音频焦点。

当你的应用程序需要输出音频时,如音频或者通知体提示音,你应该一直请求音频焦点。一旦它获取到焦点,它就可以自如地使用音频输出,但是它应该一直监听焦点变化。如果收到了失去音频焦点的通知,它应该立即杀死音频或者减低音频的分贝到安静水平(被称为“淹没”——有一个标志暗示哪个水平是合适的)且只能在它重新获得焦点之后才能恢复大声播放。

音频焦点的本质是协同运行。也就是说,应用程序被期望(和强烈地鼓励)遵守音频焦点的规则,但是这些规则不被系统所执行。如果一个应用程序想大声播放音乐即时它失去了音频焦点,系统也不会做任何事情去阻止。但是,用户体验越糟糕,用户则更容易卸载行为不友好的应用程序。

为了请求音频焦点,你必须调用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() 方法将会被调用。因此,你应该实现这个接口在你的服务进程和活动页面中。例如:

class MyService extends Service                implements AudioManager.OnAudioFocusChangeListener {    // ....    public void onAudioFocusChange(int focusChange) {        // Do something based on focus change...    }}
foucsChange参数告诉你音频焦点如何变化,可以是下面任意一个值(他们都是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 8版本(Android2.2)或者之上可用,所以如果你想在较早的版本上支持该它,你应该采可用的支持该特性的向后兼容的策略,反之只能采用之前的方式。

你可通过反射调用音频焦点的方法或者建立单独的类实现音频焦点的特性(也就是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 8或者之上,你只要创建一个AudioFocusHelper类的实例。例如:

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 intent

很多写的非常好的播放音频的应用程序可以自动停止播放当一个事件发生导致音频文件变为噪音时(通过外部的扬声器输出)。例如,当用户正在使用耳机听音乐此时设备偶然地断开与耳机的联系可能会导致上述情况。然而,这种情况是无意识地发生的。如果你没有实现设备的外部扬声器输出音频这一特性,这可能不是用户所期望的。

你务必确保你的app在这些情况下通过操作ACTION_AUDIO_BECOMING_NOISY意图来停止播放音乐,你可以通过添加一个Manifest注册一个接收器来实现:

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


MusicIntentReceiver类注册为ACTION_AUDIO_BECOMING_NOISY意图的广播接收器。接着你应该实现这个类:

public class MusicIntentReceiver extends 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)      }   }}

从内容解析器取回媒体文件

在媒体播放应用中另外一个有用的特性是有能力获取设备上用户已有的音乐。你可以通过查询外部的内容解析器来实现:
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 = new MediaPlayer();mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);mMediaPlayer.setDataSource(getApplicationContext(), contentUri);// ...prepare and start...

                                             
0 0
原创粉丝点击