Android 媒体播放(Media Playback)

本文是 Android Developer上的官方文档,由于最近用到了Android音视频相关的技术,就把相关文档读了一下,并顺手“翻译”了过来。

我其实没有什么英文水平的,原文很多地方自己也看不明白,因此,“翻译”过来的内容估计更差,如果你看到这里,请注意本文内容仅供参考,不能保证其正确与准确性,请勿转载,以免误导他人,下面附上原文地址。

原文地址:http://developer.android.com/guide/topics/media/mediaplayer.html

Android多媒体框架包含了播放多种常见媒体格式的支持,因此你可以很方便地在应用中集成音频、视频和图片。要播放的音频或视频来源可以有以下几种:存储在应用资源文件中的媒体文件(raw resources);存储在文件系统中的独立文件;或者来自于网络连接的数据流,所有这些都会用到 MediaPlayer 接口。

本文档会向你展示如何编写一个媒体播放应用,该应用允许用户与系统进行交互以取得更好的用户体验。

注意:你只能向标准的输出设备中播放音频数据,一般情况下会是手机喇叭或者蓝牙耳机;另外,你不能在打电话时播放音频文件。

基础(The Basics)

下面的类在Android框架中是用来播放声音和视频的:

MediaPlayer
       该类是播放声音和视频的主要接口

AudioManager
       该类用来管理音频资源和音频输入到设备

Manifest声明(Manifest Declarations)

在开始使用MediaPlayer开发你的应用之前,需要确认你的清单文件(manifest)拥有合适的声明,以允许使用相关的手机特性。

  • Internet Permission
    如果你使用 MediaPlayer 加载网络内容,你的应用必须拥有网络访问权限
 <uses-permission android:name="android.permission.INTERNET" />
  • Wake Lock Permission
    如果,或者使用了 MediaPlayer.setScreenOnWhilePlaying() 方法或 MediaPlayer.setWakeMode() 方法,你必须申请此权限。
<uses-permission android:name="android.permission.WAKE_LOCK" />

使用MediaPlayer(Using MediaPlayer)

媒体框架最重要的一个组件就是 MediaPlayer 类,该类的对象可以获取、解码和播放音频及视频,它支持多种媒体资源,例如:

  • 本地资源
  • 内部URI,例如从Content Resolver中所获取
  • 外部URL(流)

Android 所支持的媒体格式列表,可以参看 Android Supported Media Formats 文档。

下面是一个示例,展示了如何播放作为本地原生资源的音频(保存在你应用的 res/raw 目录中):

MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); // no need to call prepare(); create() does that for you

在本例中,一个“原生”资源是一个不需要系统尝试解析的文件;不过,资源文件的内容不仅仅可以是原生音频,它也可以是正确编码的媒体格式文件,只要该文件是 Android 所支持的格式。

下面的例子向你展示了怎样播放一个 URI 代表的资源(假设你从 Content Resolver 中获取):

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

通过 HTTP 流从远程 URL 播放如下所示:

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

注意:如果你提供一个 URL 来播放在线媒体文件,该文件必须可以被下载。

 

警告:在使用 setDataSource() 方法时,你必须捕获或传递 IllegalArgumentExceptionIOException 两个异常,因为引用的文件有可能不存在。

异步准备(Asynchronous Preparation)

MediaPlayer 原则上使用很简单,但是在集成到典型的Android应用时有一些必要的注意事项。例如,prepare() 方法可能会执行较长时间,因为它会获取并解析媒体数据。因此,在这种任何方法有可能需要执行较长时间的情况下,你永远不在要应用的 UI 线程中调用它们。这样做为导致 UI 停止工作直到方法返回,这是非常糟糕的用户体验并且有可能导致 ANR (Application Not Responding)错误。即使你的资源加载很快,但也要记住,在 UI 中任何超过十分之一秒才产生响应的操作都会产生明显的卡顿,进而给用户以你的应用运行很慢的印象。

为了避免UI线程的挂起,可以创建另外的线程来准备 MediaPlayer 并在完成时通知主线程。虽然你可以自己来写这部分线程逻辑,但在使用 MediaPlayer 时这部分操作太常见了,因此框架提供了一个方便的 prepareAsync() 方法来完成这个任务。该方法开始在后台准备媒体并立即返回,准备操作完成时, 通过 setOnPreparedListener() 方法配置的 MediaPlayer.OnPreparedListener 接口的 onPrepared() 方法会被调用。

管理状态(Managing State)

MediaPlayer 另外一个你需要注意的方面就是它是基于状态的,也就是说,在写代码时你要时刻意识到 MediaPlayer 拥有一个内部状态,因为某些操作仅在特定状态下才是有效的。如果你在错误的状态下执行某个操作,系统会招聘一个异常或者导致其它不正确的行为。

MediaPlayer 类的文档展示了完整的状态图示,可以很清晰地表示出哪个方法会将 MediaPlayer 从一个状态改变为另一个状态。例如,当你创建一个新的 MediaPlayer 时,它会处于 Idle 状态;这时需要通过调用 setDataSource() 方法来初始化,这样会使其变为 Initialized 状态;然后你需要通过 prepare()prepareAsync() 方法来完成准备工作,当 MediaPlayer 准备完成时,它会变为 Prepared 状态;这就意味着你现在可以调用 start 方法来播放媒体资源了。这个时候,就如同文档状态图示中所描述,你可以通过调用 startpauseseekTo 等方法在 StartedPausedPlaybackCompleted 状态之间切换。注意,当你调用了 stop() 方法,在重新准备 MediaPlayer 之前不能再次调用 start 方法。

在编写 与 MediaPlayer 对象交互代码时时刻留心 the state diagram,因为在错误的状态下调用其方法通常会引发 Bug。

下面附上状态图示(the state diagram)

Android 媒体播放(Media Playback)_第1张图片

释放MediaPlayer(Releasing the MediaPlayer)

MediaPlayer 会消耗宝贵的系统资源,因此,要确定只有在必要时才会持有MediaPlayer 实例引用。当你使用完毕,必须调用 release 方法确保所有为它分配的资源都正确地释放。例如,你正在使用 MediaPlayer 时 Activity 收到了 onStop() 呼叫,你必须释放 MediaPlayer 实例,因为当 Activity 不再与用户交互时再持有该实例没有意义(除非是在后台播放,这是下一部分要讨论的)。当 Activity 继续(resume)或重新启动(restarted)时,你需要创建一个新的 MediaPlayer,并在播放前再次预备(prepare )。

下面展示如何释放并置空 MediaPlayer 实例:

mediaPlayer.release();
mediaPlayer = null;

作为一个示例,请考虑这样一个问题,当你的 Activity 停止时你忘记了释放 MediaPlayer,但当 Activity 再次启动时又创建一个新的。你可能了解过,当用户改变屏幕方向(或通过其它方式改变设备配置),系统默认情况下会通过重启 Activity 来处理这种情况,因此在用户来回地在横屏和竖屏之间旋转设备时你可能很快就会消耗掉所有的系统资源,因为每次屏幕方向改变都会创建一个新的 MediaPlayer 却从未得到释放。(关于运行时重启的更多信息,请参考 Handling Runtime Changes

你可能会希望即使用户离开你的 Activity ,仍然继续播放 “后台音乐(backgroud media)”,就像很多内置音乐播放器一样,你可能会对此过程心存疑惑;这种情况下,你需要一个在 Service 中控制的 MediaPlayer ,这将在下一部分讨论。

Using a Service with MediaPlayer

如果你希望在应用退出设备屏幕时媒体播放仍然在后台运行,也就是说你希望在用户转去与其它应用交互时继续播放应用中的媒体文件,这时你需要启动 Service 并且在其中控制 MediaPlayer 实例。这时你需要特别小心,因为用户和系统对一款应用如何运行一个需要与系统的其他部分进行交互的后台服务抱有某些期望。如果你的应用不能满足这种期望,用户可能会有坏的使用体验。本部分描述了你需要了解的主要问题并提供了如何处理这些问题的建议。

异步运行(Running asynchronously)

首先,跟 Activity 一样,Service 中所有的工作默认都会在单个线程中完成;实际上,如果你在同一个应用中运行 Activity 和 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();
    }
}

处理异步错误(Handling asynchronous errors)

在异步操作中,错误会以一个异常或者错误码的形式作为信号;不论你何时使用异步资源,都要确定所有的错误发生时你的应用都可以被正确地通知到。就 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!
    }
}

重要的是要记住,当一个错误发生时, MediaPlayer 会变成 Error 状态(查看完整的状态图示请到 MediaPlayer 文档),这时如果你要再次使用它必须首先将其重置。

使用叫醒锁(Using wake locks)

设计后台播放媒体资源的应用时要考虑到,当你的服务正在运行时设备有可能会休眠。因为 Android 系统在设备休眠时尝试保护电池(节省电量?),系统会关闭手机任何不必要的功能,包括 CPU 和 WiFi 硬件。可是你的服务正在播放或者加载音乐,你希望阻止系统介入你的播放。

为了保证你的服务在这种情况下继续运行,你需要使用 “wake locks”。叫醒锁(wake locks)是一种向系统发出即使在手机待机时你的应用所使用的功能也要保持可用的途径。

注意:你要总是谨慎地并且只是在真正必要时使用叫醒锁,因为它们会大大减少设备电池的使用寿命(电池电量的使用时间?)

为了保证当你的 MediaPlayer 播放时 CPU 继续运行,在初始化 MediaPlayer 时调用 setWakeMode 方法。一旦你做了这些,MediaPlayer 将在播放时持有指定的锁,暂停或停止时才释放这个锁。

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

然而,本便中获取的叫醒锁仅能保证 CPU 可用;如果你使用 Wi-Fi 从网络上加载媒体资源,还需要持有一个 WifiLock ,这个需要手动获取和释放。因此,当你使用远程 URL 准备 MediaPlayer 时,你需要创建并获取 Wi-Fi 锁。示例如下:

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

wifiLock.acquire();

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

wifiLock.release();

作为前台服务运行(Running as a foreground service)

服务通常情况被用于执行收邮件、同步数据、下载等后台任务,在这些情形,用户并不能清晰地了解服务的执行情况,甚至可能都注意不到某些服务被中断并重启。

但请考虑服务播放音乐这种情况,很明显这是一种用户需要主动了解的服务,并且任何中断都会严重影响用户体验。同时,这也是一种用户非常希望在其运行过程中可以互动的服务;这种情况下,服务应该以“前台服务”方式运行。前台服务在系统中拥有更高的重要性 - 系统几乎永远不会杀掉这种服务,因为这对用户来说至关重要。当作为前台服务运行时,服务需要提供一个状态栏通知来保证用户能知道正在运行的服务并且允许他们打开能与服务互动的界面(Activity)。

为了将服务作为前台服务运行,你必须创建一个状态栏的 Notification 并调用 ServicestartForeground 方法启动,示例如下:

String songName;
// assign the song name to 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);

当服务在前台运行,你启动服务时配置的通知会显示在设备的通知区域。如果用户选择了这个通知,系统会发起你提供的 PendingIntent 意图。在上面的示例中,这会打开一个Activity (MainActivity) 。

你应该只在用户清楚地知道服务正在执行的任务时保持服务的“前台服务”状态,一旦不再需要,应该通过调用 stopForeground() 方法释放资源:

stopForeground(true);

更多信息请参考 servicesStatus Bar Notifications 相关文档。

处理音频焦点(Handling audio focus)

即使同一时间内只能有一个活动在运行,但 Android 仍然是一个多任务的环境。这给使用音频的应用带来一个特别的挑战,因为只能有一个音频输出但却有可能有多个媒体服务为此竞争。在 Android 2.2 之前,因没有内置的机制来解决这个问题,在某些情况下会导致较差的用户体验。例如,当用户正在听音乐时,另一个应用需要通知用户一项重要的事项,用户很有可能因为音乐声音太大而听不到通知提示音。从 Android 2.2 开始,平台为应用提供了一种协调使用设备的音频输出的方法,这种机制称为音频焦点

当你的应用需要输出音乐或通知等音频时,你总是需要申请音频焦点。一旦应用拥有了焦点,就可以自由地使用声音输出,但需要时刻监听焦点变化 。如果被通知失去了音频焦点,应用应当立即停止音频或者静音直到其再次获得焦点才能恢复播放。

音频焦点是协调性质的机制。就是说,应用被预期(强烈鼓励)会顺从音频焦点方案,但规则并未由系统强制推行。如果应用想要在即使丢焦点后仍然大声地播放音频,在系统中并没有什么可以阻止这种行为。然而,用户很可能会拥有一个坏的体验进而卸载这种烦人的应用。

你需要通过调用 AudioManagerrequestAudioFocus 方法来获取音频焦点,示例程序:

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 方法在音频焦点有改变时被调用。因此,你总是需要在你的 Activity 或 Service 中实现这个接口,例如:

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

谨记音频焦点 APIs 只能应用在 API Level 8 (Android 2.2)及以上,因此如果你需要支持以前版本的 Android,应该采取一个向后兼容的策略,让你在该功能可用时使用,不可用时弃用。

你可以通过以反射或者在一单独的类(称为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 8 及以上时才创建一个 AudioFocusHelper 实例,例如:

if (android.os.Build.VERSION.SDK_INT >= 8) {
    mAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this);
} else {
    mAudioFocusHelper = null;
}

执行清理(Performing cleanup)

就像前面提到的,一个 MediaPlayer 对象会消耗大量的系统资源,因此你应该只有必要持有且使用完毕后调用 release 方法释放资源。显式地调用这个清理方法很重要,而不是依赖系统的垃圾回收机制,因为可能需要过一段时间垃圾收集器才会回收利用 MediaPlayer ,因为它只对内存需求敏感,而不关心其它媒体相关资源的缺乏。因此,当你使用服务时应该总是重写 onDestroy 并确保释放了 MediaPlayer

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

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

你应该除了关闭时释放外还随时寻找其它机会来释放你的 MediaPlayer。例如,你预期在一段较长的时间内不再播放(如丢失音频焦点后),你应该明确地释放 MediaPlayer ,随后需要时再重新创建。另一方面,如果你希望停止播放非常短的时间,你应该持有 MediaPlayer 避免重新创建和准备。

Handling the AUDIO_BECOMING_NOISY Intent

许多优秀的音频播放应用在某些事件发生而导致音频变成噪音时会自动停止播放(输出到外部喇叭)。例如,用户正在使用耳机听音乐,这时耳机意外脱离设备。如果没有处理这种特性,声音播放到设备的外置喇叭可能并非用户所希望看到的。

你可以通过处理 ACTION_AUDIO_BECOMING_NOISY 意图(Intent)来确保应用在遇到这种情况时停止播放音频,做到这点需要注册一个接收器:

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

这样注册了 MusicIntentReceiver 类作为一个广播接收器,接下要实现这个类:

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

Retrieving Media from a Content Resolver

另外一个在媒体播放应用中可能使用的特性是获取用户设备中的音乐,你可以通过查询 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) {
    // 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)