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
- 外部 URL(流媒体)
有关 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(例如,您通过内容解析器获得的 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 流媒体从远程 URL 播放如下所示
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();
注意:如果您传递 URL 来流式传输在线媒体文件,则该文件必须能够进行渐进式下载。
警告:在使用setDataSource()
时,您必须捕获或传递IllegalArgumentException
和IOException
,因为您引用的文件可能不存在。
异步准备
原则上,使用MediaPlayer
可能很简单。但是,必须记住,为了将其正确集成到典型的 Android 应用程序中,还需要做一些额外的事情。例如,对prepare()
的调用可能需要很长时间才能执行,因为它可能涉及获取和解码媒体数据。因此,与可能需要很长时间才能执行的任何方法一样,您**永远不要从应用程序的 UI 线程调用它**。这样做会导致 UI 挂起,直到方法返回,这是一种非常糟糕的用户体验,并且可能导致 ANR(应用程序无响应)错误。即使您预计您的资源加载速度很快,也要记住,任何在 UI 中响应时间超过十分之一秒的内容都会导致明显的暂停,并让用户感觉您的应用程序速度很慢。
为了避免挂起 UI 线程,请生成另一个线程来准备MediaPlayer
并在完成后通知主线程。但是,虽然您可以自己编写线程逻辑,但在使用MediaPlayer
时,这种模式非常常见,因此框架提供了一种方便的方法来完成此任务,方法是使用prepareAsync()
方法。此方法在后台开始准备媒体并立即返回。媒体准备完成后,将调用通过setOnPreparedListener()
配置的MediaPlayer.OnPreparedListener
的onPrepared()
方法。
管理状态
您在编写代码时应牢记MediaPlayer
的另一个方面是它基于状态。也就是说,MediaPlayer
具有内部状态,在编写代码时必须始终注意它,因为某些操作仅在播放器处于特定状态时才有效。如果您在错误的状态下执行操作,系统可能会抛出异常或导致其他不良行为。
MediaPlayer
类中的文档显示了一个完整的状态图,该图阐明了哪些方法将MediaPlayer
从一个状态移动到另一个状态。例如,当您创建一个新的MediaPlayer
时,它处于空闲状态。此时,您应该通过调用setDataSource()
来初始化它,使其进入已初始化状态。之后,您必须使用prepare()
或prepareAsync()
方法来准备它。当MediaPlayer
完成准备时,它将进入已准备状态,这意味着您可以调用start()
使其播放媒体。此时,如该图所示,您可以通过调用诸如start()
、pause()
和seekTo()
等方法在已启动、已暂停和播放完成状态之间切换。但是,当您调用stop()
时,请注意,在再次准备MediaPlayer
之前,您无法再次调用start()
。
在编写与MediaPlayer
对象交互的代码时,始终牢记状态图,因为从错误的状态调用其方法是导致错误的常见原因。
释放 MediaPlayer
MediaPlayer
可能会消耗宝贵的系统资源。因此,您应该始终采取额外的预防措施,以确保您不会比必要的时间更长地保留MediaPlayer
实例。完成后,您应该始终调用release()
,以确保分配给它的任何系统资源都得到正确释放。例如,如果您正在使用MediaPlayer
并且您的活动收到对onStop()
的调用,则必须释放MediaPlayer
,因为在您的活动没有与用户交互时保留它没有意义(除非您正在后台播放媒体,这将在下一节中讨论)。当您的活动恢复或重新启动时,当然,您需要创建一个新的MediaPlayer
并在恢复播放之前再次准备它。
以下是如何释放然后使您的MediaPlayer
无效的示例
Kotlin
mediaPlayer?.release() mediaPlayer = null
Java
mediaPlayer.release(); mediaPlayer = null;
例如,考虑一下如果您忘记在活动停止时释放MediaPlayer
,但在活动再次启动时创建了一个新的MediaPlayer
,可能会发生什么问题。您可能知道,当用户更改屏幕方向(或以其他方式更改设备配置)时,系统会通过重新启动活动来处理这种情况(默认情况下),因此当用户在纵向和横向之间旋转设备时,您可能会很快消耗所有系统资源,因为在每次方向更改时,您都会创建一个新的MediaPlayer
,而您从未释放它。(有关运行时重启的更多信息,请参阅处理运行时更改)。
您可能想知道,如果即使在用户离开您的活动后您也希望继续播放“后台媒体”,就像内置的音乐应用程序的行为一样,该怎么办。在这种情况下,您需要的是由服务控制的MediaPlayer
,如下一节所述
在服务中使用 MediaPlayer
如果您希望媒体即使在您的应用程序不在屏幕上时也能在后台播放——也就是说,您希望它在用户与其他应用程序交互时继续播放——那么您必须启动一个服务并从那里控制MediaPlayer
实例。您需要将 MediaPlayer 嵌入到MediaBrowserServiceCompat
服务中,并使其与另一个活动中的MediaBrowserCompat
交互。
您应该小心这种客户端/服务器设置。对在后台服务中运行的播放器如何与系统的其余部分交互有一些期望。如果您的应用程序没有满足这些期望,则用户可能会遇到糟糕的体验。阅读构建音频应用程序以获取完整详细信息。
本节介绍在服务内部实现 MediaPlayer 时管理 MediaPlayer 的特殊说明。
异步运行
首先,与Activity
一样,Service
中的所有工作默认情况下都在单个线程中完成——事实上,如果您从同一个应用程序运行活动和服务,则默认情况下它们使用相同的线程(“主线程”)。因此,服务需要快速处理传入的意图,并且在响应它们时永远不要执行冗长的计算。如果预计任何繁重的工作或阻塞调用,则必须异步执行这些任务:要么来自您自己实现的另一个线程,要么使用框架用于异步处理的众多工具。
例如,当从主线程使用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
会切换到 错误 状态(有关完整的状态图,请参阅 MediaPlayer
类的文档),并且必须在再次使用它之前重置它。
使用唤醒锁
在设计在后台播放媒体的应用程序时,设备可能会在您的服务运行时进入睡眠状态。由于 Android 系统尝试在设备休眠时节省电量,因此系统会尝试关闭所有不必要的手机功能,包括 CPU 和 WiFi 硬件。但是,如果您的服务正在播放或流式传输音乐,则需要阻止系统干扰您的播放。
为了确保您的服务在这些条件下继续运行,您必须使用“唤醒锁”。唤醒锁是一种向系统发出信号的方式,表明您的应用程序正在使用某些功能,即使手机处于空闲状态也应保持可用。
注意:您应该始终谨慎使用唤醒锁,并且仅在真正必要时才持有它们,因为它们会显著降低设备的电池寿命。
为了确保 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
,您必须手动获取和释放它。因此,当您开始使用远程 URL 准备 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 内容,则该方法会返回一个非空MediaPlayer.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()
上阻塞。
当您设置 MediaPlayer.OnDrmPreparedListener
时,prepareDrm()
会在后台执行配置(如果需要)和准备。配置和准备完成后,将调用侦听器。您不应对调用顺序或侦听器运行的线程做出任何假设(除非侦听器已在处理程序线程中注册)。侦听器可以在 prepareDrm()
返回之前或之后调用。
异步设置 DRM
您可以通过创建和注册 MediaPlayer.OnDrmInfoListener
用于 DRM 准备以及 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...
了解更多
这些页面涵盖了与录制、存储和播放音频和视频相关的主题。