多媒体播放

来源:互联网 发布:提高淘宝搜索排名 编辑:程序博客网 时间:2024/05/01 02:38

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(例如网络流)

支持的格式:

Android支持的媒体格式

下面通过示例来说明几种不同源的多媒体播放.

本地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 = ....; //在这里初始化UriMediaPlayer mediaPlayer = new MediaPlayer();mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);mediaPlayer.setDataSource(getApplicationContext(), myUri);mediaPlayer.prepare();mediaPlayer.start();

外部URL

String url = "http://........"; // URLMediaPlayer 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...
0 0