Android 多媒体框架支持播放各种常见媒体类型,以便您轻松地将音频、视频和图片集成到应用中。您可以通过 MediaPlayer
API 播放存储在应用资源中的媒体文件(原始资源)、文件系统中的独立文件或者通过网络连接收到的数据流中的音频或视频。
本文档介绍了如何使用 MediaPlayer
编写与用户和系统交互的媒体播放应用,以便获得良好的性能和愉悦的用户体验。或者,您可能想要使用 ExoPlayer,它是一个可自定义的开源库,支持 MediaPlayer
中未提供的高性能功能
注意:您只能通过标准输出设备播放音频数据。目前,标准输出设备是移动设备的扬声器或蓝牙耳机。您无法在通话期间播放对话音频中的声音文件。
基础知识
以下类用于在 Android 框架中播放声音和视频:
MediaPlayer
- 此类是用于播放声音和视频的主要 API。
AudioManager
- 此类用于管理设备上的音频源和音频输出。
清单声明
在使用 MediaPlayer 开发应用之前,请确保您的清单具有适当的声明,以允许使用相关功能。
- 互联网权限 - 如果您使用 MediaPlayer 流式传输基于网络的内容,则您的应用必须请求网络访问权限。
<uses-permission android:name="android.permission.INTERNET" />
- 唤醒锁定权限 - 如果您的播放器应用需要防止屏幕变暗或处理器进入休眠状态,或者使用
MediaPlayer.setScreenOnWhilePlaying()
或MediaPlayer.setWakeMode()
方法,您必须请求此权限。<uses-permission android:name="android.permission.WAKE_LOCK" />
使用 MediaPlayer
媒体框架最重要的组件之一是 MediaPlayer
类。此类的对象只需极少的设置即可提取、解码和播放音频和视频。它支持多种不同的媒体来源,例如:
- 本地资源
- 内部 URI,例如您可能从内容解析器那获取的 URI
- 外部网址(流式传输)
如需查看 Android 支持的媒体格式列表,请参阅支持的媒体格式页面。
以下示例展示了如何播放作为本地原始资源(保存在应用的 res/raw/
目录中)提供的音频:
Kotlin
var mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1) mediaPlayer.start() // no need to call prepare(); create() does that for you
Java
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1); mediaPlayer.start(); // no need to call prepare(); create() does that for you
在本例中,“原始”资源是指系统不会尝试以任何特定方式解析的文件。但是,此资源的内容不应是原始音频。它应该是采用某种支持的格式且经过适当编码和格式的媒体文件。
以下是从系统中本地可用的 URI(例如,您通过内容解析器获取)进行播放的方法:
Kotlin
val myUri: Uri = .... // initialize Uri here val mediaPlayer = MediaPlayer().apply { setAudioAttributes( AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setUsage(AudioAttributes.USAGE_MEDIA) .build() ) setDataSource(applicationContext, myUri) prepare() start() }
Java
Uri myUri = ....; // initialize Uri here MediaPlayer mediaPlayer = new MediaPlayer(); mediaPlayer.setAudioAttributes( new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setUsage(AudioAttributes.USAGE_MEDIA) .build() ); mediaPlayer.setDataSource(getApplicationContext(), myUri); mediaPlayer.prepare(); mediaPlayer.start();
通过 HTTP 流式传输并播放远程网址上的内容如下所示:
Kotlin
val url = "http://........" // your URL here val mediaPlayer = MediaPlayer().apply { setAudioAttributes( AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setUsage(AudioAttributes.USAGE_MEDIA) .build() ) setDataSource(url) prepare() // might take long! (for buffering, etc) start() }
Java
String url = "http://........"; // your URL here MediaPlayer mediaPlayer = new MediaPlayer(); mediaPlayer.setAudioAttributes( new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setUsage(AudioAttributes.USAGE_MEDIA) .build() ); mediaPlayer.setDataSource(url); mediaPlayer.prepare(); // might take long! (for buffering, etc) mediaPlayer.start();
注意:如果您要传递用于流式传输在线媒体文件的网址,则文件必须支持渐进式下载。
注意:使用 setDataSource()
时,您必须捕获或传递 IllegalArgumentException
和 IOException
,因为您引用的文件可能不存在。
异步准备
原则上,使用 MediaPlayer
非常简单。不过,请务必注意,要将其与典型的 Android 应用正确集成,还需要完成一些准备工作。例如,对 prepare()
的调用可能需要很长时间才能完成,因为这可能涉及提取和解码媒体数据。因此,与任何可能需要很长时间来执行的方法一样,切勿从应用的界面线程调用它。这样做会导致界面挂起,直到该方法返回为止,这是一种非常糟糕的用户体验,并且可能会导致 ANR(应用无响应)错误。即使您预计资源会快速加载,也请谨记,在界面中响应任何时间超过十分之一秒的任何事件都会导致明显的暂停,并让用户觉得应用运行缓慢。
为避免界面线程挂起,请生成另一个线程来准备 MediaPlayer
,并在完成后通知主线程。不过,虽然您可以自行编写线程逻辑,但这种模式在使用 MediaPlayer
时非常常见,因此框架提供了使用 prepareAsync()
方法完成此任务的便捷方式。此方法会在后台开始准备媒体,并立即返回。当媒体准备就绪后,系统会调用通过 setOnPreparedListener()
配置的 MediaPlayer.OnPreparedListener
的 onPrepared()
方法。
管理状态
您应该牢记的 MediaPlayer
的另一个方面是它基于状态。也就是说,MediaPlayer
具有一种内部状态,您在编写代码时必须始终注意该状态,因为某些操作仅在播放器处于特定状态时有效。如果您在错误状态下执行操作,系统可能会抛出异常或导致其他不良行为。
MediaPlayer
类中的文档显示了一个完整的状态图,该图阐明了哪些方法可将 MediaPlayer
从一种状态转换为另一种状态。例如,当您创建新的 MediaPlayer
时,它处于空闲状态。此时,您应通过调用 setDataSource()
并使其处于“已初始化”状态对其进行初始化。然后,您必须使用 prepare()
或 prepareAsync()
方法进行准备。当 MediaPlayer
准备就绪后,它会进入“Prepared”状态,这意味着您可以调用 start()
使其播放媒体内容。这时,如图所示,您就可以调用 start()
、pause()
和 seekTo()
等方法,在 Started、Pause 和 PlaybackCompleted 状态之间切换。不过,当您调用 stop()
时,请注意,除非您再次准备 MediaPlayer
,否则将无法再次调用 start()
。
在编写与 MediaPlayer
对象交互的代码时,请始终牢记此状态图,因为从错误的状态调用其方法是导致 bug 的常见原因。
释放 MediaPlayer
MediaPlayer
会消耗宝贵的系统资源。因此,您应始终采取额外的预防措施,确保继续使用 MediaPlayer
实例的时间不会过长。完成之后,您应始终调用 release()
,以确保正确释放分配给它的所有系统资源。例如,如果您使用的是 MediaPlayer
,并且您的 activity 收到对 onStop()
的调用,则必须释放 MediaPlayer
,因为在 activity 未与用户交互时按住它几乎没有意义(除非您在后台播放媒体,下一部分将对此进行讨论)。
当然,当 activity 恢复或重启时,您需要先创建一个新的 MediaPlayer
并再次做好准备,然后才能恢复播放。
以下代码段介绍了如何释放并取消 MediaPlayer
:
Kotlin
mediaPlayer?.release() mediaPlayer = null
Java
mediaPlayer.release(); mediaPlayer = null;
例如,如果您忘记在 activity 停止时释放 MediaPlayer
,但在 activity 重新启动时又新建一个,可能会出现的问题。如您所知,当用户更改屏幕方向(或以其他方式更改设备配置)时,系统会通过重启 activity(默认)来处理这种情况,因此当用户在竖屏和横屏之间来回旋转设备时,您可能会快速消耗所有系统资源,因为每次更改屏幕方向时,您都会创建一个永远不会释放的新 MediaPlayer
。(如需详细了解运行时重启,请参阅处理运行时更改。)
您可能想知道,如果您在用户离开 activity 后仍继续播放“后台媒体”,会发生什么情况,这与内置音乐应用的行为方式大致相同。在这种情况下,您需要的是由 Service 控制的 MediaPlayer
,如下一部分所述
在 Service 中使用 MediaPlayer
如果您希望即使应用未在屏幕上播放,也想在后台继续播放媒体内容(也就是说,您希望在用户与其他应用交互时继续播放),您必须启动一个 Service 并从该 Service 控制 MediaPlayer
实例。您需要将 MediaPlayer 嵌入到 MediaBrowserServiceCompat
服务中,并让其与其他 activity 中的 MediaBrowserCompat
互动。
您应该注意这种客户端/服务器设置。关于在后台服务中运行的播放器如何与系统的其他部分交互,需要满足一些要求。如果您的应用未满足这些预期,用户的体验可能会很差。如需了解完整详情,请参阅构建音频应用。
本部分介绍了在 Service 内部实现 MediaPlayer 时如何对其进行管理的特殊说明。
异步运行
首先,与 Activity
一样,Service
中的所有工作默认在单个线程中完成。事实上,如果您从同一应用运行 activity 和服务,它们将默认使用相同的线程(“主线程”)。因此,服务需要快速处理传入的 intent,并且在响应它们时从不执行冗长的计算。如果预计会执行繁重的工作或阻塞调用,您必须异步执行这些任务:要么从您自己实现的另一个线程执行,要么使用框架的诸多工具进行异步处理。
例如,从主线程中使用 MediaPlayer
时,您应该调用 prepareAsync()
而不是 prepare()
,并实现 MediaPlayer.OnPreparedListener
,以便在准备完成并可以开始游戏时收到通知。例如:
Kotlin
private const val ACTION_PLAY: String = "com.example.action.PLAY" class MyService: Service(), MediaPlayer.OnPreparedListener { private var mMediaPlayer: MediaPlayer? = null override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { ... val action: String = intent.action when(action) { ACTION_PLAY -> { mMediaPlayer = ... // initialize it here mMediaPlayer?.apply { setOnPreparedListener(this@MyService) prepareAsync() // prepare async to not block main thread } } } ... } /** Called when MediaPlayer is ready */ override fun onPrepared(mediaPlayer: MediaPlayer) { mediaPlayer.start() } }
Java
public class MyService extends Service implements MediaPlayer.OnPreparedListener { private static final String ACTION_PLAY = "com.example.action.PLAY"; MediaPlayer mediaPlayer = null; public int onStartCommand(Intent intent, int flags, int startId) { ... if (intent.getAction().equals(ACTION_PLAY)) { mediaPlayer = ... // initialize it here mediaPlayer.setOnPreparedListener(this); mediaPlayer.prepareAsync(); // prepare async to not block main thread } } /** Called when MediaPlayer is ready */ public void onPrepared(MediaPlayer player) { player.start(); } }
处理异步错误
在同步操作中,系统通常通过异常或错误代码来指示错误,但每当使用异步资源时,都应确保应用以适当的方式收到错误通知。对于 MediaPlayer
,您可以通过实现 MediaPlayer.OnErrorListener
并在 MediaPlayer
实例中进行设置来实现此目的:
Kotlin
class MyService : Service(), MediaPlayer.OnErrorListener { private var mediaPlayer: MediaPlayer? = null fun initMediaPlayer() { // ...initialize the MediaPlayer here... mediaPlayer?.setOnErrorListener(this) } override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean { // ... react appropriately ... // The MediaPlayer has moved to the Error state, must be reset! } }
Java
public class MyService extends Service implements MediaPlayer.OnErrorListener { MediaPlayer mediaPlayer; public void initMediaPlayer() { // ...initialize the MediaPlayer here... mediaPlayer.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
类的文档),您必须对其进行重置,然后才能再次使用它。
使用唤醒锁定
在设计在后台播放媒体内容的应用时,设备可能会在服务运行时进入休眠状态。由于 Android 系统会在设备处于休眠状态时尝试节省电池电量,因此系统会尝试关闭手机的所有不必要的功能,包括 CPU 和 Wi-Fi 硬件。但是,如果您的服务正在播放或流式传输音乐,您需要防止系统干扰播放。
为了确保您的服务在这些条件下继续运行,您必须使用“唤醒锁定”。唤醒锁定是一种向系统发出信号的方式,即您的应用正在使用某项即使手机处于空闲状态也应保持可用的功能。
注意:您应始终谨慎使用唤醒锁定,并且仅将其保留至真正需要的时间,因为它们会显著缩短设备的电池续航时间。
为了确保 CPU 在 MediaPlayer
播放时继续运行,请在初始化 MediaPlayer
时调用 setWakeMode()
方法。完成该操作后,MediaPlayer
会在播放时保持指定的锁,并在暂停或停止时释放该锁:
Kotlin
mediaPlayer = MediaPlayer().apply { // ... other initialization here ... setWakeMode(applicationContext, PowerManager.PARTIAL_WAKE_LOCK) }
Java
mediaPlayer = new MediaPlayer(); // ... other initialization here ... mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
不过,此示例中获取的唤醒锁定只能保证 CPU 保持唤醒状态。如果您通过网络在线播放媒体内容,并且使用的是 Wi-Fi,则可能也希望保留 WifiLock
,您必须手动获取并释放它。因此,当您开始使用远程网址准备 MediaPlayer
时,应创建并获取 Wi-Fi 锁。例如:
Kotlin
val wifiManager = getSystemService(Context.WIFI_SERVICE) as WifiManager val wifiLock: WifiManager.WifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock") wifiLock.acquire()
Java
WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE)) .createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock"); wifiLock.acquire();
当您暂停或停止媒体,或者当您不再需要网络时,应释放锁定:
Kotlin
wifiLock.release()
Java
wifiLock.release();
进行清理
如前所述,MediaPlayer
对象会消耗大量系统资源,因此您应该仅保留其需要的时间,并在使用完对象后调用 release()
。请务必明确调用此清理方法,而不是依赖于系统垃圾回收,因为垃圾回收器可能需要一些时间才能回收 MediaPlayer
,因为它只对内存需求敏感,不对其他媒体相关资源不足。因此,当您使用某项服务时,应始终替换 onDestroy()
方法,以确保释放 MediaPlayer
:
Kotlin
class MyService : Service() { private var mediaPlayer: MediaPlayer? = null // ... override fun onDestroy() { super.onDestroy() mediaPlayer?.release() } }
Java
public class MyService extends Service { MediaPlayer mediaPlayer; // ... @Override public void onDestroy() { super.onDestroy(); if (mediaPlayer != null) mediaPlayer.release(); } }
除了在关闭时释放 MediaPlayer
之外,您还应始终寻找其他释放机会。例如,如果您预计长时间(例如在失去音频焦点后)无法播放媒体,请务必释放现有的 MediaPlayer
,并在稍后重新创建。另一方面,如果您仅希望停止播放一小段时间,则应该使用 MediaPlayer
,以避免再次创建和准备它的开销。
数字版权管理 (DRM)
从 Android 8.0(API 级别 26)开始,MediaPlayer
包含支持播放受 DRM 保护的材料的 API。它们与 MediaDrm
提供的低级别 API 类似,但它们在较高级别运行,并且不会公开底层提取器、DRM 和加密对象。
虽然 MediaPlayer DRM API 不提供 MediaDrm
的完整功能,但支持最常见的用例。当前实现可以处理以下内容类型:
- 受 Widevine 保护的本地媒体文件
- 受 Widevine 保护的远程/流式传输媒体文件
以下代码段演示了如何在简单的同步实现中使用新的 DRM MediaPlayer 方法。
若要管理受 DRM 控制的媒体,您需要在 MediaPlayer 调用的常规流程之外添加新方法,如下所示:
Kotlin
mediaPlayer?.apply { setDataSource() setOnDrmConfigHelper() // optional, for custom configuration prepare() drmInfo?.also { prepareDrm() getKeyRequest() provideKeyResponse() } // MediaPlayer is now ready to use start() // ...play/pause/resume... stop() releaseDrm() }
Java
setDataSource(); setOnDrmConfigHelper(); // optional, for custom configuration prepare(); if (getDrmInfo() != null) { prepareDrm(); getKeyRequest(); provideKeyResponse(); } // MediaPlayer is now ready to use start(); // ...play/pause/resume... stop(); releaseDrm();
首先,照常初始化 MediaPlayer
对象并使用 setDataSource()
设置其来源。然后,如需使用 DRM,请执行以下步骤:
- 如果您希望应用执行自定义配置,请定义
OnDrmConfigHelper
接口,并使用setOnDrmConfigHelper()
将其附加到播放器。 - 调用
prepare()
。 - 调用
getDrmInfo()
。如果来源具有 DRM 内容,则该方法会返回非 nullMediaPlayer.DrmInfo
值。
如果存在 MediaPlayer.DrmInfo
:
- 检查可用 UUID 的映射,然后选择一个。
- 通过调用
prepareDrm()
为当前来源准备 DRM 配置。 - 如果您创建并注册了
OnDrmConfigHelper
回调,则系统会在执行prepareDrm()
时调用该回调。这样,您就可以在打开 DRM 会话之前执行 DRM 属性的自定义配置。该回调将在调用prepareDrm()
的线程中同步调用。如需访问 DRM 属性,请调用getDrmPropertyString()
和setDrmPropertyString()
。 避免执行冗长的操作。 - 如果尚未配置设备,
prepareDrm()
还会访问配置服务器来配置设备。此过程所需的时间因网络连接而异。 - 如需获取要发送到许可服务器的不透明密钥请求字节数组,请调用
getKeyRequest()
。 - 如需将从许可服务器收到的密钥响应告知 DRM 引擎,请调用
provideKeyResponse()
。结果取决于密钥请求的类型:- 如果响应针对的是离线密钥请求,则结果为密钥组标识符。您可以将此密钥集标识符与
restoreKeys()
结合使用,将密钥恢复到新会话。 - 如果响应针对的是流式传输或释放请求,则结果为 null。
- 如果响应针对的是离线密钥请求,则结果为密钥组标识符。您可以将此密钥集标识符与
异步运行 prepareDrm()
默认情况下,prepareDrm()
会同步运行,阻塞会持续到准备完成为止。不过,在新设备上进行首次 DRM 准备可能还需要进行预配,而这由 prepareDrm()
在内部处理,并且由于涉及到网络操作,可能需要一些时间才能完成。您可以通过定义和设置 MediaPlayer.OnDrmPreparedListener
来避免阻塞 prepareDrm()
。
当您设置 OnDrmPreparedListener
时,prepareDrm()
会在后台执行预配(如果需要)和准备。预配和准备完成后,系统会调用监听器。您不应对调用顺序或运行监听器的线程做任何假设(除非监听器已在处理程序线程中注册)。您可以在 prepareDrm()
返回之前或之后调用监听器。
异步设置 DRM
您可以通过创建和注册用于 DRM 准备的 MediaPlayer.OnDrmInfoListener
以及用于启动播放器的 MediaPlayer.OnDrmPreparedListener
来异步初始化 DRM。它们可与 prepareAsync()
结合使用,如下所示:
Kotlin
setOnPreparedListener() setOnDrmInfoListener() setDataSource() prepareAsync() // ... // If the data source content is protected you receive a call to the onDrmInfo() callback. override fun onDrmInfo(mediaPlayer: MediaPlayer, drmInfo: MediaPlayer.DrmInfo) { mediaPlayer.apply { prepareDrm() getKeyRequest() provideKeyResponse() } } // When prepareAsync() finishes, you receive a call to the onPrepared() callback. // If there is a DRM, onDrmInfo() sets it up before executing this callback, // so you can start the player. override fun onPrepared(mediaPlayer: MediaPlayer) { mediaPlayer.start() }
Java
setOnPreparedListener(); setOnDrmInfoListener(); setDataSource(); prepareAsync(); // ... // If the data source content is protected you receive a call to the onDrmInfo() callback. onDrmInfo() { prepareDrm(); getKeyRequest(); provideKeyResponse(); } // When prepareAsync() finishes, you receive a call to the onPrepared() callback. // If there is a DRM, onDrmInfo() sets it up before executing this callback, // so you can start the player. onPrepared() { start(); }
处理加密媒体
从 Android 8.0(API 级别 26)开始,MediaPlayer
还可以为基本流类型 H.264 和 AAC 解密通用加密方案 (CENC) 和 HLS 样本级加密媒体 (METHOD=SAMPLE-AES)。之前支持全分段加密媒体 (METHOD=AES-128)。
从 ContentResolver 检索媒体
媒体播放器应用中可能实用的另一项功能是能够检索用户在设备上拥有的音乐。您可以通过查询 ContentResolver
找到外部媒体来完成该操作:
Kotlin
val resolver: ContentResolver = contentResolver val uri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI val cursor: Cursor? = resolver.query(uri, null, null, null, null) when { cursor == null -> { // query failed, handle error. } !cursor.moveToFirst() -> { // no media on the device } else -> { val titleColumn: Int = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media.TITLE) val idColumn: Int = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media._ID) do { val thisId = cursor.getLong(idColumn) val thisTitle = cursor.getString(titleColumn) // ...process entry... } while (cursor.moveToNext()) } } cursor?.close()
Java
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
结合使用,您可以执行以下操作:
Kotlin
val id: Long = /* retrieve it from somewhere */ val contentUri: Uri = ContentUris.withAppendedId(android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id ) mediaPlayer = MediaPlayer().apply { setAudioAttributes( AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setUsage(AudioAttributes.USAGE_MEDIA) .build() ) setDataSource(applicationContext, contentUri) } // ...prepare and start...
Java
long id = /* retrieve it from somewhere */; Uri contentUri = ContentUris.withAppendedId( android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id); mediaPlayer = new MediaPlayer(); mediaPlayer.setAudioAttributes( new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setUsage(AudioAttributes.USAGE_MEDIA) .build() ); mediaPlayer.setDataSource(getApplicationContext(), contentUri); // ...prepare and start...
了解详情
以下页面介绍了有关录制、存储以及播放音频和视频的主题。