android音视频指南-MediaPlayer概述

9,169

翻译自MediaPlayer overview

Android多媒体框架支持播放各种常见媒体类型,因此您可以轻松地将音频、视频和图像集成到应用程序中。您可以使用MediaPlayer api从存储在应用程序资源(原始资源)中的媒体文件、文件系统中的独立文件或通过网络连接到达的数据流中播放音频或视频。

本文向您展示了如何编写与用户和系统交互的媒体播放应用程序,以获得良好的性能和愉快的用户体验。

注意:您只能将音频数据回放到标准输出设备。目前,这是移动设备扬声器或蓝牙耳机。您不能在通话期间播放通话音频中的声音文件。

最基本的

在Android框架中使用以下类播放声音和视频:

MediaPlayer

这个类是播放声音和视频的主要API。

AudioManager

该类管理设备上的音频源和音频输出。

清单声明

在使用MediaPlayer对应用程序进行开发之前,请确保清单中有适当的声明,允许使用相关特性。

  • Internet权限——如果您正在使用MediaPlayer来播放流基于网络的内容,那么您的应用程序必须请求网络访问。
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

使用MediaPlayer

媒体框架最重要的组件之一是MediaPlayer类。这个类的对象可以使用最少的设置获取、解码和播放音频和视频。它支持几种不同的媒体来源,如:

  • 本地资源
  • 内部uri,例如您可能从contentProvider获得的uri
  • 外部url(流) 有关Android支持的媒体格式列表,请参阅支持的媒体格式页面。

下面是如何播放本地音频资源(保存在您的应用程序的res/raw/目录中):

MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); // no need to call prepare(); create() does that for you

在本例中,“raw”资源是系统不尝试以任何特定方式解析的文件。然而,这个资源的内容不应该是原始音频。它应该是一个以支持的格式之一适当编码和格式化的媒体文件。

下面是您如何从系统中本地可用的URI(例如,您通过内容解析器获得的URI)进行播放:

Uri myUri = ....; // initialize Uri here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();

通过HTTP流媒体从远程URL播放如下:

String url = "http://........"; // your URL here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
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有一个内部状态,在编写代码时必须始终注意到,因为只有当player处于特定状态时,某些操作才有效。如果在错误的状态下执行操作,系统可能会抛出异常或引发其他不希望看到的行为。

MediaPlayer类中的文档显示了一个完整的状态机,它阐明了哪些方法将MediaPlayer从一个状态移动到另一个状态。例如,当您创建一个新的MediaPlayer时,它处于空闲状态。这时,您应该通过调用setDataSource()来初始化它,使它处于初始化状态。之后,您必须使用prepare()prepareAsync()方法来准备它。当MediaPlayer完成准备工作时,它进入准备状态,这意味着您可以调用start()来让它播放媒体。此时,您可以通过调用start()、pause()seekTo()等方法在start、pause()和PlaybackCompleted状态之间切换。但是,当您调用stop()时,请注意,在重新准备MediaPlayer之前,您不能再次调用start()。

在编写与MediaPlayer对象交互的代码时,一定要记住状态图,因为从错误的状态调用其方法是导致错误的常见原因。

MediaPlayer Diagram

释放媒体播放器

MediaPlayer可能会消耗有价值的系统资源。因此,您应该始终采取额外的预防措施,以确保您没有过多地依赖MediaPlayer实例。处理完它之后,应该始终调用release(),以确保分配给它的任何系统资源都被正确释放。例如,如果您使用的是一个媒体播放器和活动接收onStop()调用,您必须释放媒体播放器,因为当你的活动不与用户进行交互,继续持有实例毫无意义(除非你是在后台播放媒体,这是在下一节中讨论)。当您的活动恢复或重新启动时,当然,您需要创建一个新的MediaPlayer,并在恢复回放之前重新准备。

下面是您应该如何释放并取消MediaPlayer:

mediaPlayer.release();
mediaPlayer = null;

作为一个例子,考虑一下如果您在活动停止时忘记释放MediaPlayer,而在活动重新开始时创建一个新的,可能会发生的问题。正如你可能知道的,当用户更改屏幕的方向(或更改设备配置以另一种方式),系统处理,通过重新启动活动(默认情况下),所以你可能会很快消耗掉所有系统资源的用户旋转设备之间来回的肖像和风景,因为在每一个方向变化,您创建一个新的媒体播放器,你永远不会释放。(有关运行时重新启动的更多信息,请参见处理运行时更改。)

您可能想知道,在用户离开您的活动时如果您想继续播放“背景媒体”,会发生什么,这与内置音乐应用程序的表现非常类似。在这种情况下,您需要的是一个由服务控制的MediaPlayer,下一节将对此进行讨论.

在服务中使用MediaPlayer

如果您想要您的媒体在后台播放,即使您的应用程序不是在屏幕上——也就是说,您想要它在用户与其他应用程序交互时继续播放——那么您必须启动一个服务并从那里控制MediaPlayer实例。您需要将MediaPlayer嵌入到MediaBrowserServiceCompat服务中,并让它与另一个活动中的MediaBrowserCompat交互。

要小心这个client/server设置。人们对在后台服务中运行的播放器如何与系统的其他部分进行交互抱有期望。如果您的应用程序没有满足这些期望,用户可能会有一个糟糕的体验。阅读建立一个音频应用程序的完整细节。

本节描述了在服务中实现MediaPlayer时管理它的特殊说明。

异步运行

首先,与活动一样,服务中的所有工作在默认情况下都是在单个线程中完成的——事实上,如果您从同一个应用程序运行活动和服务,默认情况下它们使用相同的线程(“主线程”)。因此,服务需要快速处理传入意图,并且在响应它们时从不执行冗长的计算。如果预期有任何繁重的工作或阻塞调用,您必须异步执行这些任务:要么从另一个您自己实现的线程执行,要么使用框架的许多异步处理工具。

例如,在使用主线程中的MediaPlayer时,应该调用prepareAsync()而不是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 = ... // initialize it here
            mMediaPlayer.setOnPreparedListener(this);
            mMediaPlayer.prepareAsync(); // prepare async to not block main thread
        }
    }

    /** Called when MediaPlayer is ready */
    public void onPrepared(MediaPlayer player) {
        player.start();
    }
}

处理异步错误

在同步操作中,错误通常会以异常或错误代码发出信号,但无论何时使用异步资源,都应该确保将错误通知给应用程序。对于MediaPlayer,您可以通过实现MediaPlayer.OnErrorListener并将其设置到MediaPlayer实例中来解决该问题。

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

    public void initMediaPlayer() {
        // ...initialize the MediaPlayer here...
        mMediaPlayer.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将切换到错误状态,您必须在再次使用它之前重置它。

使用‘唤醒锁 wake locks’

当设计在后台播放媒体的应用程序时,设备可能会在服务运行时休眠。由于Android系统试图在设备处于休眠状态时节省电池,所以系统试图关闭手机的任何不必要的功能,包括CPU和WiFi硬件。然而,如果您的服务正在播放或流媒体音乐,您希望防止系统干扰您的播放。

为了确保您的服务在这些条件下继续运行,您必须使用“唤醒锁”。唤醒锁是一种向系统发出信号的方式,即您的应用程序正在使用某些特性,即使手机处于空闲状态,这些特性也应该保持可用。

注意:你应该尽量少用唤醒锁,并且只在必要的时候使用它们,因为它们会大大减少设备的电池寿命。

要确保在MediaPlayer播放时CPU继续运行,在初始化MediaPlayer时调用setWakeMode()方法。一旦你这样做了,MediaPlayer会在播放时持有指定的锁,并在暂停或停止时释放锁:

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

但是,在本例中获得的唤醒锁只保证CPU保持清醒。如果你是通过网络流媒体,使用的是Wi-Fi,你可能也想要一个WifiLock,你必须手动获取和释放它。因此,当您开始使用远程URL准备MediaPlayer时,您应该创建并获得Wi-Fi锁。例如:

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

wifiLock.acquire();

当你暂停或停止你的媒体,或当你不再需要网络,你应该释放锁:

wifiLock.release();

执行清理

如前所述,MediaPlayer对象可能会消耗大量的系统资源,所以您应该只在需要的时候使用它,并且在使用之后调用release()。显式调用这种清理方法而不是依赖于系统垃圾收集是很重要的,因为垃圾收集器重新声明MediaPlayer可能需要一些时间,因为它只对内存需求敏感,而不缺乏其他与媒体相关的资源。因此,在使用服务时,您应该总是重写onDestroy()方法,以确保释放MediaPlayer:

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

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

数码版权管理(DRM)

从Android 8.0 (API级别26)开始,MediaPlayer就包含了支持drm保护材料回放的API。它们类似于MediaDrm提供的低级API,但是它们在更高级别上操作,并且不公开底层提取器、drm和加密对象。

尽管MediaPlayer DRM API没有提供MediaDrm的全部功能,但它支持最常见的用例。当前实现可以处理以下内容类型:

  • 受广泛保护的本地媒体文件
  • 宽带保护远程/流媒体文件

下面的代码片段演示了如何在简单的同步实现中使用新的DRM MediaPlayer方法。

要管理drm控制的媒体,您需要在通常的MediaPlayer调用流之外包括新的方法,如下所示:

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内容,该方法将返回一个非空MediaPlayer.DrmInfo价值。

如果MediaPlayer.DrmInfo存在:

  1. 检查可用uuid的映射并选择一个。
  2. 通过调用prepareDrm()为当前源程序准备DRM配置。
    • 如果您创建并注册了一个OnDrmConfigHelper回调,它将在prepareDrm()执行时被调用。这允许您在打开DRM会话之前执行DRM属性的自定义配置。在调用prepareDrm()的线程中同步调用回调函数。要访问DRM属性,可以调用getDrmPropertyString()和setDrmPropertyString()。避免执行冗长的操作。
    • 如果设备还没有提供好,那么prepareDrm()也会访问配置服务器来提供设备。这可能需要可变的时间,这取决于网络连接。
  3. 要将不透明的键请求字节数组发送到许可证服务器,请调用getKeyRequest()
  4. 要将从许可证服务器接收到的密钥响应通知DRM引擎,请调用provideKeyResponse()。结果取决于键请求的类型:
    • 如果响应是脱机键请求,则结果是键集标识符。您可以通过restoreKeys()使用这个键集标识符将键恢复到新会话。
    • 如果响应是流请求或发布请求,则结果为空。

异步运行prepareDrm()

默认情况下,prepareDrm()同步运行,阻塞直到准备工作完成。但是,在新设备上进行的第一次DRM准备也可能需要进行准备,准备工作由prepareDrm()内部处理,由于涉及网络操作,可能需要一些时间才能完成。通过定义和设置MediaPlayer.OnDrmPreparedListener,可以避免在prepareDrm()上阻塞。

当您设置OnDrmPreparedListener时,prepareDrm()在后台执行请求(如果需要)和准备。当drm准备就绪时,将调用侦听器。您不应该对调用序列或侦听器运行的线程做任何假设(除非侦听器注册到handler thread)。侦听器在prepareDrm()返回之前或之后能被调用。

异步设置DRM

您可以异步地初始化DRM,通过创建和注册MediaPlayer.OnDrmInfoListener用于DRM准备和MediaPlayer.OnDrmPreparedListener去启动播放器。它们与prepareAsync()协同工作,如下所示:

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来实现:

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中使用

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...

Sample

SimpleMediaPlayer代码示例展示了如何构建独立播放器。android-BasicMediaDecoderandroid-DeviceOwner示例进一步演示了本页所述api的使用。

了解更多

这些页面涵盖了有关录音、存储和回放音频和视频的主题。

支持的媒体格式

MediaRecorder

数据存储