译:Android MediaPlayer

MediaPlayer 概括

Android多媒体框架支持多种公共媒体类型的播放,你可以很容易的将音频,视频,图片等多媒体资源整合进你的应用中。
使用MediaPlayer APIs,你可以播放位于应用资源文件,独立的文件系统的文件或者网络连接的数据流的视频或音频文件。
该文档展示如何编写用户与系统交互的多媒体应用,以获得良好的性能和愉快的用户体验

*注意:目前,你只能在移动设备的扬声器或蓝牙耳机等标准的输出设备中播放音频数据。在通话期间,你无法在谈话中播放声音文件

基础

  • MediaPlayer
    提供用于播放音视频的API
  • AudioManager
    提供管理音频源和输出

清单声明

在你的应用中使用MediaPlayer前,确保在你的清单文件中声明相关feature的使用权

  • 网络权限 - 如果你使用MediaPlayer播放基于网络的内容,需要声明一下权限

  • 睡眠锁权限 - 如果你的播放应用需要保持屏幕亮度或禁止进程休眠,或需要使用 MediaPlayer.setScreenOnWhilePlaying()和 MediaPlayer.setWakeMode()
    两个API,你需要请求以下权限

MediaPlayer 使用

MediaPlayer是多媒体框架中最重要的组件之一。它可以用最小的设置来获取,解码并播放音视频,支持一下几种不同的媒体资源

  • 本地资源
  • 内部URIs,比如从ContentResolver中获取
  • 外部URLs(流)
    了解Android支持的媒体格式列表,请访问格式支持页面
    以下是播放本地本地原生资源(保存在应用res/raw目录)的例子
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); // 无需调用preare()方法,create()方法中已调用

这种方式,原生资源是系统不用特殊方法解析的一种文件。但是,资源不能是原始的音频资源。他应该是支持的格式中的一种适当的编码格式。

以下是使用系统本地的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();

播放远程URL比如HTTP 流

String url = "http://........";
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare(); // might take long!
mediaPlayer.start();
  • 注意:如果你解析网上的媒体文件URL为流,该文件必须支持渐进式下载
  • 注意:setDataSource()需要捕获或传递IllegalArgumentException 和 IOException,因为文件可能不存在
异步准备

原则上,MediaPlayer的使用很简单。然而,一定要记住在典型的Android应用中正确整合的一些注意事项。比如,prepare()方法的调用会花一些时间,因为它可能涉及获取和解码媒体数据,因此,与任何可能需要很长时间才能执行的方法一样,不能在UI线程调用该方法。这样会导致UI线程挂起直到该方法返回,
这会导致糟糕的用户体验并引发ANR错误。即使你期望你的资源加载很快。记住在UI中响应时间超过十分之一秒的任何内容都会导致显著的卡顿,并给用户应用运行缓慢的印象。
为了避免UI线程挂起,在另一个线程准备MediaPlayer并当它完成时通知UI线程。但是,虽然你可以书写自己的线程逻辑,但这种模式在使用MediaPlayer时非常常见,框架提供了另一个方便的方法prepareAsync()去完成这个任务。该方法在后台准备媒体并且立即返回。当媒体准备完成,通过setOnPreparedListener设置的
MediaPlayer.OnPrepareListener.onPrepared()的方法被调用。

状态管理

另一方面你应该记住,MediaPlayer是基于状态的。这是说,MediaPlayer拥有内部状态,当你写代码时,你需要时刻知道当前的状态,因为特殊的操作仅在播放器在特殊的状态下有效。当你在错误的状态下执行操作,系统可能会抛出异常或者导致另一些不可取的行为。
MediaPlayer文档展示了一个完整的状态图来说明一些使MediaPlayer状态转换的方法。比如,当你创建一个新的MediaPlayer,它处于空闲状态。之后,你调用prepare()
或prepareAsync()方法准备播放器,当MediaPlayer准备完毕,它进入准备状态,然后,可以调用start()方法去播放媒体。在这个时候,就如图所显示的,你可以调用
start(),pause(),seekTo()包括其他的方法在开始,暂停和播放完毕等状态中切换。但是,注意你不应该再次调用start()方法知道你再次准备MediaPlayer。
在你编写与MediaPlayer交互的代码时要时刻记住状态图,因为在错误的状态下调用方法是导致bug的共性问题。

释放MediaPlayer

MediaPlayer占用系统资源。因此,你应该经常需要额外注意保证在不需要MediaPlayer实例时不再持有它。当你使用完它后,你应该调用release()方法保证所有分配给它的系统资源正确释放。比如,当使用MediaPlayer的Activity回调onStop()时,你必须释放该MediaPlayer,因为当你的activity不再与用户交互后再持有它基本是没有意义的(除非你在后台播放媒体,这将在下一小节讨论)。当然,当你的activity恢复或重启时,你需要再创建一个MediaPlayer并在开始播放前再次准备它。
以下是你如何释放并置空MediaPlayer

mediaPlayer.release();
mediaPlayer = null;

作为一个例子,考虑这个问题,当activity已停止,但你忘记释放MediaPlayer,但是activity重启后你又再次创建了一个实例。就如你可能知道的,当用户旋转屏幕(或者使用其他方法改变了设备配置),系统捕获通过重启activity(默认)来捕获该行为,所以当用户来回操作设备你可能很快消耗所有的系统资源,因为每次的方向转换,你都会创建一个新的永远不会释放的MediaPlayer。(更多关于运行时重启问题,详见运行时处理改变)

你可能想知道,即使用户离开activity,如果你想继续播放背景媒体时会发生什么,这与内置音乐应用程序的行为非常相似。在这种情况下,你需要通过Service控制播放,我们在下一节讨论,

在Service中使用MediaPlayer

如果你想在后台播放媒体,即使你的应用已不再屏幕上显示。也就是说,你希望当用户在和其他应用交互式时,你的应用能够继续播放媒体。
那样的话,你必须启动一个Service去控制该MediaPlayer实例。你需要在MediaBrowserServiceCompat service中嵌入MediaPlayer并在另一个activity中与MediaBrowserCompat交互。
你应该注意这个客户端/服务器设置。这里有一些关于在后台服务中运行的播放器如何与剩余系统交互的期望。如果你的应用没有旅行这些期望,用户可能会有糟糕的体验。更多细节请查看构建音频应用
这一小节描述了一些关于在后台服务中管理MediaPlayer的特殊命令。

异步运行

首先,类似与activity,service的所有工作默认在一个单独的线程中进行。事实上,如果你在相同的应用中运行activity和service,他们默认使用相同的线程(主线程)。因此,服务需要快速处理传入的意图,并且在响应时不要执行冗长的计算。如果预期有大量的工作或阻塞调用,你必须在异步任务中完成这些操作。可以自行创建线程或者使用框架中异步操作的工具。
例如,在主线程中使用MediaPlayer,你应该调用prepareAysnc()而不是
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(); // 异步的准备不会阻塞UI线程
        }
    }

    /** MediaPlayer准备完毕后调用 */
    public void onPrepared(MediaPlayer player) {
        player.start();
    }
}

处理异步错误

在异步操作中,错误通常以异常或一个错误码的方式标记,但是无论何时你使用异步资源时,你应该保证你的应用有合适的错误提醒。基于这一点,你应该实现MediaPlayer.OnErrorListener并且用MediaPlayer实例设置它来完成。

public class MyService extends Service implements MediaPlayer.OnErrorListener {
    MediaPlayer mMediaPlayer;

    public void initMediaPlayer() {
        // 初始化MediaPlayer

        mMediaPlayer.setOnErrorListener(this);
    }

    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        // ... react appropriately ...
        // MediaPlayer异常,请重置
    }
}

一定要记住,当MediaPlayer出现错误,即MediaPlayer转换为错误状态(详见文档中的状态图),你必须在在此使用它之前重置。

使用睡眠锁

设计在后台播放媒体的应用,当你的服务在运行期时,设备可能会休眠。因为Android系统尝试在设备休眠时节电,系统会尝试关闭手机的不必要的功能,
包括CPU和WIFI硬件模块。然而,如果你在运行播放服务或在播放音乐,则需要放置系统干扰播放。
为了保证你的服务在这种情况下继续运行,你必须使用休眠锁。休眠锁是一种向系统发出信号的方式,即你的应用正在使用某些功能,即使手机处于空闲状态也应保持可用。

*注意:你应该保守的使用休眠锁并在真正需要时使用,因为它会显著的降低电池寿命。

为了保证当你的MediaPlayer在播放时保持CPU继续运行,当你初始化MediaPlayer时调用[setWakeMode()](https://developer.android.google.cn/reference/android/media/MediaPlayer.html#setWakeMode(android.content.Context, int))
一旦你这样做,MediaPlayer会在播放时保持锁定,在暂停和停止后释放锁。

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

然而,以上例子中获取的睡眠锁只能保证CPU保持唤醒状态。如果你在使用wifi播放网络流媒体,你可能也想要保持WifiLock,你必须手动获取和释放。所以,当你启动使用远程URL资源
的MediaPlayer,你应该创建并获取WIFI 锁,例如

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

wifiLock.acquire();

当你暂停或停止媒体,或者当你不再使用网络时,你应该释放锁

wifiLock.release();

执行清理

之前有提到过,MediaPlayer对象会消耗大量的系统资源,所以你应该仅仅在使用它时保持并在使用完后调用release()释放。手动调用清理方法而不是依赖系统垃圾收集器是很重要的,因为在垃圾收集器会花一些时间去回收MediaPlayer,因为它只是对内存需求敏感,而不是缺少其他与媒体相关的资源。所以,基于这个原因,当你使用service时,你应该重写onDestroy()
方法并保证释放MediaPlayer

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

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

除了在关闭时释放它,你也应该会经常看到其他释放MediaPlayer的时机。比如,你期望在一段较长的时间内不再播放媒体(比如,失去播放焦点),你应该毫无疑问的释放存在的MediaPlayer,然后稍后在创建它。另一方面,如果你仅在一小段时间内停止播放,你获取应该继续持有MediaPlayer避免再次创建和准备它的开销

数字版本管理

从Android8.0开始,MediaPlayer加入了支持播放受DRM保护的素材的API。它和MediaDrm提供的低级别API相似,但是它在一个更高级别进行操作,不会暴露底层的提取器,drm和加密对象。
虽然MediaPlayer DRM API并不支持完整的MediaDrm功能,但它提供了最常见的用例。当前实现可以处理以下内容类型:

  • Widevine-protected 本地媒体文件
  • Widevine-protected 远程或流媒体文件
    以下的代码片段演示了如何在简单的同步实现中使用新的DRM MediaPlayer方法。
    要管理DRM控制的媒体,您需要将新方法与通常的MediaPlayer调用流一起包括在内,如下所示:
setDataSource();
setOnDrmConfigHelper(); // 可选,自定义配置
prepare();
if (getDrmInfo() != null) {
  prepareDrm();
  getKeyRequest();
  provideKeyResponse();
}

// MediaPlayer已准备好,可以使用
start();
// ...play/pause/resume...
stop();
releaseDrm();

像平常一样,从初始化MediaPlayer对象并使用setDataSource()方法设置源开始,然后,使用以下步骤,使用DRM。

  1. 如果你想app执行自定义的配置,定义OnDrmConfigHelper接口,
    并使用setOnDrmConfigHelper()
    关联播放器。
  2. 调用prepare()
  3. 调用getDrmInfo(),如果视频源包含DRM内容,方法会返回非空的MediaPlayer.DrmInfo值

如果MediaPlayer.DrmInfo存在:

  1. 检查UUID的映射并选择一个
  2. 调用prepareDrm()给当前的视频源准备DRM配置
  • 如果你创建并注册了OnDrmConfigHelper回调,当prepareDrm()执行时会回调。这会让你在打开DRM session前执行自定义的DRM属性配置。回调会在调用prepareDrm()的线程同步的调用。
    使用getDrmPropertyString()和setDrmPropertyString() 来访问DRM属性。避免执行冗余的操作
  • 如果尚未设置设备,则prepareDrm()还会访问配置服务器以配置设备。 这可能需要不同的时间,具体取决于网络连接
  1. 调用getKeyRequest()来获取要发送到许可证服务器的不透明密钥请求字节数组
  2. 调用[provideKeyResponse()](https://developer.android.google.cn/reference/android/media/MediaPlayer.html#provideKeyResponse(byte[], byte[]))来提醒DRM引擎已收到来自许可证服务器的响应,
    返回值取决于请求的key的类型
  • 如果响应是一个脱机秘钥请求,则结果是一个秘钥集标识符。你可以将次秘钥集标识符与restoreKeys()一起使用,以将秘钥还原到新会话
  • 如果响应是针对流式传输或释放请求,则结果为null。
异步执行prepareDrm()

默认的,prepareDrm()同步执行,阻塞知道准备完成。然而,在一个新设备上首次DRM准备需要配置,使用prepareDrm()响应处理可能会因为涉及网络操作的原因导致耗时。你可以通过定义和配置MediaPlayer.OnDrmPreparedListener
避免使用阻塞式的prepareDrm()
当你设置了OnDrmPreparedListener,parepareDrm()会在后台执行设置(如果需要的话)和准备,当设置和准备完成后,监听器会被回调。你不应该对调用序列或侦听器运行的线程做出任何假设(除非监听器在handler线程注册)。该监听器会在prepareDrm()返回前后被调用。

异步设置DRM

你可以通过创建和注册MediaPlayer.OnDrmInfoListener准备DRM来异步的初始化DRM并使用MediaPlayer.OnDrmPreparedListener来启动播放器,他们与prepareAsync()一起使用,如下:

setOnPreparedListener();
setOnDrmInfoListener();
setDataSource();
prepareAsync();
// ....

// 如果数据源内容被保护,你会收到onDrmInfo()的回调.
onDrmInfo() {
  prepareDrm();
  getKeyRequest();
  provideKeyResponse();
}

// 当prepareAsync()调用完毕,你会收到onPrepared()回调。
// 如果视频源手DRM保护,onDrmInfo会在该回调之前设置它,所以你可以直接启动播放器
onPrepared() {

start();
}

处理加密媒体

从Android 8.0(API级别26)开始,MediaPlayer还可以为基本流类型H.264和AAC解密公共加密方案(CENC)和HLS样本级加密媒体(METHOD = SAMPLE-AES)。 之前支持全段加密媒体(METHOD = AES-128)。

从ContentResolver中获取媒体

MediaPlayer的另一个有用的功能点是获取用户设备中的音乐。你可以通过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使用ContentResolver,如下:

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- SimpleMediaPlayer展示如何构建独立的播放器。android-BasicMediaDecoder和 android-DeviceOwner深入展示了本页面所涉及到的API

更多

以下页面的话题设计到录音,摄像,播放音视频

  • 支持的格式
  • MediaRecorder
  • 数据存储

官方文档链接

你可能感兴趣的:(译:Android MediaPlayer)