MediaPlayer 概览

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();

注意: 如果您要传递流式传输在线媒体文件的网址,则相应文件必须能够 渐进式下载。

注意:您必须捕获或传递 IllegalArgumentExceptionIOException(使用 setDataSource(),因为 您要引用的文件可能不存在。

异步准备

使用 MediaPlayer 非常简单: 原则。但请务必注意 将其与典型的 Android 应用正确集成。对于 例如,调用 prepare() 可以 因为需要很长时间才能执行完毕 可能涉及提取和解码媒体数据。因此,正如任何 方法,则切勿从 应用的界面线程。否则会导致界面挂起,直到方法返回。 这是一种非常糟糕的用户体验,可能会导致出现 ANR(应用无响应)错误。即使 但请记住,任何资源占用时间 会有明显的停顿, 让用户感觉您的应用运行缓慢

为避免界面线程挂起,请再生成另一个线程 准备 MediaPlayer,并在完成后通知主线程。不过,虽然 您可以编写线程逻辑 这种模式在使用 MediaPlayer 时很常见,以致于框架 提供了一种便捷的方法,即使用 prepareAsync() 方法。此方法 开始在后台准备媒体,并立即返回。当媒体 准备就绪时,onPrepared() MediaPlayer.OnPreparedListener 方法,通过 调用了 setOnPreparedListener()

管理状态

对于 MediaPlayer,您应该牢记的另一个方面是 是基于状态的也就是说,MediaPlayer 具有内部状态 编写代码时必须始终注意这一点, 仅在播放器处于特定状态时才有效。如果您在 错误状态,系统可能会抛出异常或导致其他不良行为。

MediaPlayer 类显示了一个完整状态图, 用于指明哪些方法可将 MediaPlayer 从一种状态变为另一种状态。 例如,当您创建新的 MediaPlayer 时,它处于 Idle 状态。此时,您应通过调用 setDataSource(),带来 更改为“Initialized”状态。然后,您需要使用 prepare()prepareAsync() 方法。时间 MediaPlayer 准备就绪后,就会进入 Prepared 状态,这意味着您可以调用 start()。 使其可以播放媒体内容此时,如图所示 您可以按如下方式在已开始已暂停播放完成状态之间切换 调用诸如 start(), pause()seekTo(), 等。当您 调用 stop(),但请注意, 在您呼叫start()之前, 再次准备 MediaPlayer

始终保持状态图 在编写与 MediaPlayer 对象,因为从错误的状态调用其方法 是导致错误的常见原因

释放 MediaPlayer

MediaPlayer可以使用 系统资源。 因此,您应始终采取额外的预防措施, MediaPlayer 实例的挂起时间超出必要时间。当您 则应始终调用 release(),以确保 系统资源才会被正确释放例如,如果你是 使用 MediaPlayer,并且您的 activity 收到对 onStop() 的调用时,您必须释放 MediaPlayer, 因为它 当您的 activity 未与产品互动时,就应该继续保留该 activity 并无意义。 用户(除非你正在后台播放媒体,这在下一部分中进行了介绍)。 当然,当 activity 恢复或重启时,您需要 请创建一个新的 MediaPlayer,并在继续播放之前重新对其进行准备。

以下代码段介绍了如何释放并取消 MediaPlayer

Kotlin

mediaPlayer?.release()
mediaPlayer = null

Java

mediaPlayer.release();
mediaPlayer = null;

以 当您的 activity 停止时忘记释放 MediaPlayer,但创建一个 并在 activity 再次启动时为新 activity。如您所知,当用户更改 屏幕方向(或以其他方式更改设备配置); 系统通过重启 activity(默认情况下)来处理该问题,因此 以用户的身份使用所有的系统资源 设备会在纵向和横向之间来回旋转,因为 因此会创建一个新的 MediaPlayer,而该 MediaPlayer 发布。(如需详细了解运行时重启,请参阅处理运行时更改。)

你可能想知道,如果想要继续玩下去会发生什么 "背景媒体"即使用户离开您的 activity 时, 内置音乐应用的行为方式。在本示例中,您需要 由 Service 控制的 MediaPlayer, 我们将在下一节讨论

在 Service 中使用 MediaPlayer

如果您希望媒体在后台播放 未在屏幕上展示,也就是说,您希望在用户 因此您必须启动 服务和控制 MediaPlayer 实例。 您需要将 MediaBrowserServiceCompat 服务中的 MediaPlayer,并具有 它会与 另一个 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 会变为错误状态(请参阅 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的身份使用 您必须手动获取和发布。因此,当您开始准备 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 保护的内容。它们类似于 MediaDrm,但它们是在更高级别运行的, 公开底层提取器、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,请执行以下步骤:

  1. 如果您希望应用执行自定义配置,请定义 OnDrmConfigHelper 接口,并将其附加到 播放器 setOnDrmConfigHelper()
  2. 调用 prepare()
  3. 调用 getDrmInfo()。如果来源具有 DRM 该方法会返回一个非 null 值 MediaPlayer.DrmInfo 值。

如果存在 MediaPlayer.DrmInfo

  1. 检查可用 UUID 的映射,然后选择一个。
  2. 通过调用 prepareDrm() 为当前来源准备 DRM 配置。
  3. 要获取不透明密钥请求字节数组以发送到许可服务器,请调用 getKeyRequest()
  4. 要将从许可服务器收到的密钥响应告知 DRM 引擎,请调用 provideKeyResponse()。结果取决于密钥请求的类型: <ph type="x-smartling-placeholder">
      </ph>
    • 如果响应针对的是离线密钥请求,则结果为密钥组标识符。您可以使用 可将此密钥集标识符与 restoreKeys() 搭配使用,以将密钥恢复到新的 会话。
    • 如果响应针对的是流式传输或释放请求,则结果为 null。

异步运行 prepareDrm()

默认情况下,prepareDrm() 同步运行,阻塞直至准备完成。不过, 新设备上首次进行 DRM 准备时,可能还需要进行配置, 由 prepareDrm()和 由于涉及网络操作,可能需要一些时间才能完成。您可以 避免屏蔽 prepareDrm() 作者 定义和设置 MediaPlayer.OnDrmPreparedListener

当您设置 OnDrmPreparedListener 时, prepareDrm() 并在后台执行预配(如果需要)和准备工作。时间 预配和准备完成后,系统会调用监听器。您应该 不要对调用序列或 运行监听器(除非已在处理程序线程中注册监听器)。 可在之前或之后调用监听器 prepareDrm() 回车。

异步设置 DRM

您可以通过创建并注册 MediaPlayer.OnDrmInfoListener(用于 DRM 准备)和 MediaPlayer.OnDrmPreparedListener,用于启动播放器。 它们与 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 也可以解密 通用加密架构 (CENC) 和 基本流类型的 HLS 样本级加密媒体 (METHOD=SAMPLE-AES) H.264 和 AAC。之前支持全分段加密媒体 (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...

了解详情

以下页面介绍了有关录制、存储以及播放音频和视频的主题。