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 时的一些特殊说明。
异步运行
首先,与Activity
类似,默认情况下,Service
中的所有工作都在单个线程中完成——事实上,如果您从同一个应用程序运行一个 Activity 和一个 Service,默认情况下它们使用同一个线程(“主线程”)。因此,服务需要快速处理传入的 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
会进入错误状态(请参阅MediaPlayer
类的文档以获取完整的状态图),并且您必须在再次使用它之前重置它。
使用唤醒锁
在设计在后台播放媒体的应用程序时,设备可能会在您的服务运行时进入睡眠状态。由于 Android 系统试图在设备休眠时节省电量,因此系统会尝试关闭所有不必要的手机功能,包括 CPU 和 WiFi 硬件。但是,如果您的服务正在播放或流式传输音乐,您希望阻止系统干扰您的播放。
为了确保您的服务在这些条件下继续运行,您必须使用“唤醒锁”。唤醒锁是一种向系统发出信号的方式,表明您的应用程序正在使用某些功能,即使手机处于空闲状态也应保持可用。
注意:您应该始终谨慎使用唤醒锁,并且只在真正需要时保持唤醒锁,因为它们会显着降低设备的电池寿命。
为了确保在您的MediaPlayer
播放时 CPU 继续运行,请在初始化您的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
值。
- 检查可用 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
您可以通过创建和注册 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...
了解更多
这些页面涵盖了与录制、存储和播放音频和视频相关的主题。