[译]Android 多媒体播放

Android 多媒体框架包含了支持播放的一系列常见多媒体类型,以此可以很容易地整合诸如音频、视频、图片到你的应用程序。资源文件、本地和网络中的视频、音频都可以通过Media Player播放。

本文展示如何写一个高性能并且体验良好的多媒体播放应用。

基础知识

下边是两个Android中用来播放声音、视频的类

  • Media Player
    提供播放声音、视频的API。
  • AudioManager
    管理设备上的音频源和音频输出

Manifest 声明

使用Media Player 开发之前,确定已经在清单文件Manifest 的正确位置声明了所需要的权限。

  1. 如果需要播放网络数据,需要声明网络权限
    <uses-permission android:name="android.permission.INTERNET" />
  2. 如果需要屏幕常亮,需要声明<uses-permission android:name="android.permission.WAKE_LOCK" />

使用Media Player

Media Player 是多媒体框架中的一个重要组件。这个类的实例可以通过最少的设置获取、解码以及播发音视频,支持下面集中不同的播放源:

  1. 本地资源
  2. 内部URI
  3. 外部URL(流)
  • 播放本地资源文件(res/raw 目录下):
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); //不需要调用prepare()方法,因为在create中已经执行了。
  • 播放系统返回的Uri
Uri myUri = ....; // initialize Uri here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();
  • 通过url播放Http流
String url = "http://........"; 
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare(); // 耗时操作
mediaPlayer.start();

如果您正在通过一个网址来流一个在线媒体文件,该文件必须是能够进行下载的。

因为文件可能不存在,所以在setDataSource()时需要处理IOException和IllegalArgumentException

异步准备操作

原则上使用Media Player是简单直接的,但是在将它整合进一个Android 应用的时需要谨记一些额外的东西。例如,因为prepare()会获取并解码多媒体数据,是一个耗时操作,所以不能在UI线程调用,否则会造成线程阻塞,程序无响应,降低用户体验。即使加载资源文件特别快,UI响应耗时超过一秒就会有一个明显的卡顿,给用户一种程序运行卡慢的感觉。
为了避免线程阻塞,需要另开线程准备MediaPlayer,并在准备完成后通知主线程。prepareAsync()方法在后台准备media并且立即返回。media准备完会调用MediaPlayer.OnPrepareListener 的onPrepared()方法,通过setOnPrepareListener()方法可以给MediaPlayer设置。

管理状态

MediaPlayer另一个需要注意的方面是它是基于状态的。这就是说,写代码的时候需要知道MediaPlayer有自己的内部状态,因为指定的操作只有在player处于特定状态的时候有效。如果在错误的状态下执行操作,系统会抛异常或者导致其他不可预期的行为。
MediaPlayer的API文档展示了完整的状态表。状态表阐明了哪个方法把MediaPlayer从一个状态改变成另一种状态。例如:当你新创建一个MediaPlayer,它处在空闲状态(Idle state),这时应该调用setDataSource()方法初始化它,状态改为初始化状态。之后应该通过prepare()或prepareAsync()方法准备。MediaPlayer准备完毕后,进入已准备状态(Prepared state),这时就可以调用start()方法播放了。这时,可以通过调用start()、pause()和seekTo()方法将状态在已开始(Started)、暂停(Paused)和播放完成(PlaybackCompleted)之间转换了。调用stop()后,只有重新准备才能再次start。
操作MediaPlayer的实例时应时刻谨记状态表,因为经常会在错误的状态调用方法造成bug。

释放MediaPlayer

MediaPlayer会消耗宝贵的系统资源,因此,你应该采取额外措施防止在不必要时拥有MediaPlayer实例。用完之后需要调用release()方法来确保系统合适回收分配给它的资源。例如,在Activity中使用MediaPlayer 时,在onStop()中必须释放MediaPlayer,因为当Activity失去焦点时会保持MediaPlayer的实例(除非你想在后台播放,但是系统不推荐这样)。当Activity被唤醒(Resumed)或重启(Restarted)时,你需要新创建一个MediaPlayer实例并在播放前准备。

mediaPlayer.release();
mediaPlayer = null;

举个例子,假设你在Activity 开始的时候新创建了一个MediaPlayer实例,但是Activity 停止时忘记释放MediaPlayer。当横竖屏来回切换时,默认系统会重新启动Activity,因为每次启动都会新创建一个MediaPlayer实例,这将很快消耗完系统内存。

如果想在用户离开当前页面后仍然像音乐播放器那样播放音视频,应该在Service中控制MediaPlayer。

Service中使用MediaPlayer

如果想在应用进入后台时仍然播放,就必须要启动一个Service来控制一个MediaPlayer 的实例了。这时应该小心,因为用户和系统期望在应用后台运行的同时可以与其它应用互相影响,如果没有考虑这些,就会降低用户体验,这块主要描述你应该知道并提供建议达到它。

异步运行

首先,跟Activity 一样,Service 中所有的工作都在一个线程中完成,实际上,如果在同一个应用中启动一个Activity 和一个Service ,他们运行在同一个线程,也就是主线程。因此,Service 需要快速响应传递进来的Intent ,响应时不进行耗时操作。如果任何繁重的工作或阻塞调用,你必须异步完成这些任务:无论是从另一个线程、自己实现或使用该框架的许多异步处理设施。

例如,在主线程使用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 = ... //
            mMediaPlayer.setOnPreparedListener(this);
            mMediaPlayer.prepareAsync();
        }
    }

    /** MediaPlayer prepare完成后回调 */
    public void onPrepared(MediaPlayer player) {
        player.start();
    }
}

处理异步error

异步操作时,error通常会通过异常或错误码的形式通知,但是,不管什么时候使用异步资源,应该确定应用在适当的时候提示错误。针对MediaPlayer,你可以通过给MediaPlayer实例设置MediaPlayer.OnErroristener接口并实现接口来完成。

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!
    }
}

error产生时,MediaPlayer会变成错误状态(Error state),必须重置之后才能再次使用。

使用唤醒锁(wake locks)

设计后台播放应用时,设备可能在service 运行的时候睡眠,Android 系统会尽量保持电量,所以就会停止一些不必要的手机功能,如果这时你的service 在播放音频或网络音频,你应该防止系统干挠播放。
为了保证在这些条件下service 仍然运行,需要使用“wake locks”。wake locks会通知系统你的应用需要保持可用以执行某些功能,即使手机是空闲的。

必要时使用唤醒锁,因为它会降低电池使用寿命。

调用setWakeMode()方法初始化MediaPlayer 可以确保播放过程中CPU 持续运行,这个MediaPlayer 在播放时会拥有特定的锁,当Activity 暂停时会释放锁。

mMediaPlayer = new MediaPlayer();
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);

唤醒锁只会保证CPU 唤醒,但是使用Wi-Fi 播放网络音频时,同样需要手动获取释放Wi-Fi 锁。

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

wifiLock.acquire();

暂停、停止播放或者不需要网络时应该释放

wifiLock.release();

处理音频焦点

虽然在任何给定的时间只有一个活动可以运行,但是Android是一个多任务环境。这带来了一个特定的挑战,使用音频的应用程序,因为只有一个音频输出,但有可能是几个竞争使用媒体服务。在安卓2.2之前,没有内置的机制来解决这个问题,这可能在某些情况下导致一个坏的用户体验。例如,当一个用户正在听音乐而另一个应用程序需要通知用户的一些非常重要的东西时,用户可能由于大声的音乐听不到通知的声音。从安卓2.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) {
    // 没有获取焦点
}

第一个参数是AudioManager.OnAudioFocusChangeListener ,音频焦点变化后会回调它的onAudioFocusChange()方法,所以你需要在Activity 或Service 实现这个接口并重写onAudioFocusChange()方法。

class MyService extends Service
                implements AudioManager.OnAudioFocusChangeListener {
    // ....
    public void onAudioFocusChange(int focusChange) {
        // Do something based on focus change...
    }
}

方法参数中的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:
            // 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;
    }
}

注意音频焦点只在Android 2.2以及以上的系统可用。

清理

如前所述,MediaPlayer实例会消耗很多系统资源,所以应该只是在需要时拥有实例,并在完成时调用release()方法释放。调用release()方法而不是依靠系统的垃圾收集是很重要的,因为它是敏感的内存需求,而不是其他媒体相关资源短缺,垃圾回收器自动回收MediaPlayer会有一段时间。因此当你使用Service时,你应该重写ondestroy()方法确保你释放MediaPlayer:

public class MyService extends Service {
   MediaPlayer mMediaPlayer;
   // ...

   @Override
   public void onDestroy() {
       if (mMediaPlayer != null) mMediaPlayer.release();
   }
}

处理AUDIO_BECOMING_NOISY Intent

编码良好的应用在音频变成噪音时会自动停止播放,可以通过处理AUDIO_BECOMING_NOISY 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 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)
      }
   }
}

从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, 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...

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 144,376评论 18 621
  • Media Playback Android多媒体框架包涵了对播放多种通用媒体的类型的支持,所以你可以很容易的集成...
    VegetableAD阅读 372评论 0 0
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 4,305评论 0 17
  • 你浪了很多年, 谈了很多场恋爱 他们会说喜欢你, 甚至爱你, 却没有人真正理解你懂你, 所以你学会了可有可无的陪伴...
    南方白烛阅读 37评论 0 2
  • 现在的姑娘们都不怎么看一个男人是否真的爱她,只想着她爱上了个男人就毫不犹豫的扑上了。 如果一个男人做到了一下几点说...
    故留情阅读 58评论 0 0