返回介绍

媒体播放

发布于 2025-03-09 16:39:54 字数 27846 浏览 0 评论 0 收藏 0

媒体播放

译者微博: http://weibo.com/popapa

版本:Android 4.0 r1

原文

http://developer.android.com/guide/topics/media/mediaplayer.html

在本文中

简介

Manifest 声明

MediaPlayer 的使用

异步准备

状态管理

释放 MediaPlayer

使用带 MediaPlayer 的服务

异步运行

异步错误的处理

Wake Lock 的使用

作为后台服务运行

Audio Focus 的处理

进行清理

意图 AUDIO_BECOMING_NOISY 的处理

从 Content Resolver 中读取媒体

关键类

MediaPlayer

AudioManager

SoundPool

参阅

JetPlayer

音频捕获

Android 支持的媒体格式

数据存储

Android 的多媒体框架支持多种通用媒体的播放,因此能够很容易地在程序中集成音频、视频和图片信息。利用 MediaPlayer API,可以播放多种来源的音视频数据,包括存储于程序资源(裸资源)中的媒体文件、文件系统中的独立文件、通过网络连接读取的数据流。

本文演示了如何编写一个媒体播放程序。为了兼顾良好的性能和舒适的用户体验,它还实现了播放期间用户和系统之间的交互。

注意: 只能在标准的输出设备上播放音频数据,目前即为移动设备的扬声器或蓝牙耳机。并且不能在通话期间同时播放音频文件。

简介

下列类用于在 Android 框架中播放音视频:

MediaPlayer

本类是播放音视频的主要 API。

AudioManager

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

Manifest 声明

在开始开发 MediaPlayer 的应用程序之前,请确保 manifest 已经正确地声明了以下相关 feature:

· Internet Permission —— 如果正在用 MediaPlayer 来播放基于网络的流媒体,应用程序必须请求网络访问权限。

<uses-permission android:name="android.permission.INTERNET" />

· Wake Lock Permission —— 如果应用程序需要防止屏幕变暗或处理器休眠,或是用到了 MediaPlayer.setScreenOnWhilePlaying()MediaPlayer.setWakeMode() 方法,则必须请求本权限。

<uses-permission android:name="android.permission.WAKE_LOCK" />

媒体播放器的使用

媒体框架中最重要的组件之一就是 MediaPlayer 类。经过一些很少量的设置,此对象即能够读取、解码并播放音视频内容。它能支持如下多种不同的媒体来源:

· 本地资源

· 内部 URI,比如可能来自 Content Resolver

· 外部 URL(流)

关于 Android 支持的媒体格式,请参阅文档 Android 支持的媒体格式

下面的例子展示了如何播放本地以裸资源方式提供的音频(保存于程序的 res/raw/目录下):

MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);

mediaPlayer.start(); // 不必调用 prepare(); create() 会自动调用

这里的“裸”资源是指系统不会以任何特定方式进行解析的文件。当然,这个资源的内容不应该是原始音频数据,而应是用所支持的格式正确编码并格式化过的媒体文件。

下面是如何播放来自系统本地提供的 URI 资源(比如通过 Content Resolver 获取的):

Uri myUri = ....; // 在此初始化 Uri

MediaPlayer mediaPlayer = new MediaPlayer();

mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);

mediaPlayer.setDataSource(getApplicationContext(), myUri);

mediaPlayer.prepare();

mediaPlayer.start();

下例是播放来自远程 URL 的 HTTP 流:

String url = "http://........"; // 在此指定 URL

MediaPlayer mediaPlayer = new MediaPlayer();

mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);

mediaPlayer.setDataSource(url);

mediaPlayer.prepare(); // 可能会耗时很长! (需创建缓存等)mediaPlayer.start();

注意: 如果通过 URL 来传送一个来自在线媒体文件的数据流,则该文件必须支持渐进下载(progressive download)。

警告: 因为所引用的文件有可能会不存在,所以使用 setDataSource() 时必须捕捉并且传递 IllegalArgumentExceptionIOException 异常。

异步准备

原则上说, MediaPlayer 的使用可以非常简单。不过有一点很重要,请记住还有一些工作必须正确地加入到典型的 Android 程序中去。比如,因为要读取并解码媒体数据, prepare() 的调用可能会运行很长时间。因此在执行这种耗时很长的方法时,应该 永远避免从程序的 UI 线程中调用此类方法 。那会导致在方法返回之前用户界面 UI 都处于挂起状态,这样用户体验会十分糟糕,并可能会引发 ANR(程序没有响应)错误。即使预计到资源会迅速装载完毕,也请记住在用户界面上任何响应时间超过 1/10 秒的工作都会导致很明显的停顿,并且会给用户留下一个程序很慢的印象。

为了避免用户界面 UI 线程的挂起,请启动另一个线程来准备 MediaPlayer 并在完成后通知主线程。不过这就可能要自行编写线程逻辑,这也是使用 MediaPlayer 时的通常做法,利用其 prepareAsync() 方法,框架提供一种便利的途径来完成此类任务。这个方法在后台进行媒体的准备工作,并且立即返回。媒体准备完毕后,将会调用 MediaPlayer.OnPreparedListeneronPrepared() 方法,该 Listener 通过 setOnPreparedListener() 指定。

状态管理

MediaPlayer 另一个应该被关注的要点是其状态模型。也就是说, MediaPlayer 拥有一个内部状态,在编写代码时必须时刻注意这个内部状态,因为播放器在某个给定状态下只允许进行特定的操作。如果在错误的状态下执行操作,系统可能会抛出异常或导致其它不可预知的现象发生。

MediaPlayer 类的文档中已展示了完整的 状态图 ,上面标明了哪些方法会使 MediaPlayer 转换状态。比如,新的 MediaPlayer 被创建时,处于 Idle 状态。这时,应通过调用 setDataSource() 进行初始化,进入 Initialized 状态。然后必须用 prepare()prepareAsync() 进行准备工作。待到 MediaPlayer 准备完毕后,将会进入 Prepared 状态,这就意味着可以调用 start() 来播放媒体了。如状态图所示,这时可以调用 start()pause()seekTo() 在 Started 、 Paused 和 PlaybackCompleted 状态之间进行切换。不过请注意,一旦调用了 stop() ,在下次 MediaPlayer 准备好之前就不能再次调用 start() 了。

在编写有关 MediaPlayer 对象的代码时请时刻牢记 状态图 ,因为常见的 bug 原因就是在错误的状态下调用了不合适的方法。

释放 MediaPlayer

MediaPlayer 可能会消耗较多的系统资源。因此应该时刻注意,避免不必要时还维持 MediaPlayer 实例的运行。应该总是在用完后及时调用 release() ,以确保所申请的系统资源得到有效释放。比如,正在使用 MediaPlayer 时 activity 收到了一个 onStop() 调用,这时就必须释放 MediaPlayer ,因为 activity 不与用户交互时没必要再保持播放器的运行(除非在后台播放媒体,这会在下节讨论)。当然,如果 activity 再次被激活或者再次被启动,则需要创建一个新的 MediaPlayer 并再次准备之后才能恢复播放。

下面是释放并注销 MediaPlayer 的语句:

mediaPlayer.release();

mediaPlayer = null;

举个例子,假如停止 activity 时忘记释放 MediaPlayer 了,但在 activity 再次启动时又创建了一个新的播放器,看看可能会产生的问题。众所周知,用户切换屏幕方向(或者其它方式改变设备设置)时,系统默认会重启 activity,这样系统资源可能会由于用户在横向纵向间来回旋转而很快耗尽。因为每改变一次方向,就会创建一个永远不会释放的新 MediaPlayer 。(关于运行时的重启,详见 运行时变化的处理 。)

如果期望在用户离开 activity 后还能继续播放“后台媒体”,正如系统内置音乐播放器那样,则需要通过 Service 来控制 MediaPlayer ,这在 使用带 MediaPlayer 的服务 中讨论。

使用带 MediaPlayer 的服务

如果程序需要在不显示时还能在后台播放媒体——也就是说期望在用户操作其它程序时也能继续播放——那就必须启动一个 Service 并从服务中控制 MediaPlayer 实例。这种情况下应该十分小心,因为用户和系统都期望运行后台服务的应用应该能与系统其它功能同时运行。如果应用不能满足这个要求,用户体验将会很糟糕。本节描述了应注意的主要问题,并提供解决建议。

异步运行

首先,如同 Activity ,所有 Service 默认运行在单个线程中——事实上,如果从同一个应用程序运行 activity 和服务,它们默认会使用同一个进程(“主进程”)。因此,服务就需要快速处理传入的意图,并且响应这些意图时还不能执行耗时较长的计算工作。如果需要执行繁重的工作或者阻塞调用,必须以异步方式执行这类任务:创建另一个线程来执行,或利用框架提供的异步处理功能。

比如,假设在主线程中用到 MediaPlayer ,就应该用 prepareAsync() 来代替 prepare() ,并实现 MediaPlayer.OnPreparedListener 以便在准备完毕后得到通知,然后就可以开始播放了。示例如下:

public class MyService extends Service implements MediaPlayer.OnPreparedListener {

private static final 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 = ... // 在此初始化

mMediaPlayer.setOnPreparedListener(this);

mMediaPlayer.prepareAsync();

// 为了不阻塞主线程而异步准备

}

}

/** 由 MediaPlayer 准备完毕后调用 */

public void onPrepared(MediaPlayer player) {

player.start();

}

}

异步错误的处理

在同步操作时,错误通常会以异常或错误代码的方式展现出来;但如果用到了异步资源,应该确保应用程序每次都能正确地获得错误通知。在使用 MediaPlayer 的时候,可以通过实现一个 MediaPlayer.OnErrorListener 来达到以上目标,当然还要在 MediaPlayer 实例中进行设定才行:

public class MyService extends Service implements MediaPlayer.OnErrorListener {

MediaPlayer mMediaPlayer;

public void initMediaPlayer() {

// ...在此初始化 MediaPlayer...

mMediaPlayer.setOnErrorListener(this);

}

@Override

public boolean onError(MediaPlayer mp, int what, int extra) {

// ... 合适地处理 ...

// MediaPlayer 已经切换到 Error 状态,必须重启!

}

}

记住这点非常重要:当发生错误时, MediaPlayer 将切换到 Error 状态(关于完整的状态图请参阅 MediaPlayer 类的文档),再次使用前必须重启播放器。

Wake Lock 的使用

如果应用程序是为后台播放媒体而设计的,那么即使服务仍在运行,但设备可能会进入休眠状态。因为设备休眠时 Android 系统会尝试节省电力,任何不必要的手机功能将会关闭,包括 CPU 和 WiFi 部件。但是,如果服务正在播放音乐或读取音乐数据流,就需要防止系统对播放进行干扰。

为了确保服务在上述情况下能维持正常运行,必须使用“唤醒锁”(wake lock)。wake lock 是一种通知系统的途径:表示应用程序需要用到一些手机空闲时也保持可用的功能。

注意: 应该尽量少用 wake lock,并且仅当确实需要时才保持锁定状态,因为它会显著减少设备的电池寿命。

为了确保 MediaPlayer 播放时 CPU 能够保持工作,应在初始化 MediaPlayer 时调用 setWakeMode() 方法。一旦调用完成, MediaPlayer 会在播放过程中保持这个特殊的锁,并在暂停和停止时释放该锁:

mMediaPlayer = new MediaPlayer();

// ... 在此执行其它初始化工作 ...

mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);

不过,本例中申请的 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();

作为后台服务运行

服务经常用于执行一些后台任务,比如读取邮件、同步数据、下载数据等。这些情况下,用户不会明显察觉服务正在运行,甚至可能都不会注意到某些服务曾被中止而过段时间又重新开始运行。

但是就播放音乐的服务而言,显然这是用户能明显察觉的服务,任何中断都会显著影响到用户的体验。此外,该服务还是用户可能期望与其交互的服务。在这种情况下,此服务应该作为“后台服务”来运行。后台服务保持较高的系统重要性级别——系统几乎永远都不会关闭服务,因为服务对于用户而言至关重要。即使是运行在后台,服务仍必须提供状态栏通知,以保证用户知晓服务正在运行,并允许用户打开与服务交互的 activity。

为了把服务切换到后台,必须为状态栏创建一个 Notification ,并且从 Service 中调用 startForeground() 。例如:

String songName;

// assign the song name to songName

PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0,

new Intent(getApplicationContext(), MainActivity.class),

PendingIntent.FLAG_UPDATE_CURRENT);

Notification notification = new Notification();

notification.tickerText = text;

notification.icon = R.drawable.play0;

notification.flags |= Notification.FLAG_ONGOING_EVENT;

notification.setLatestEventInfo(getApplicationContext(), "MusicPlayerSample",

"Playing: " + songName, pi);

startForeground(NOTIFICATION_ID, notification);

服务在后台运行期间,此 notification 将显示在设备的通知区域。如果用户选中此通知,系统会提交一个预先已实现的 PendingIntent 。以下例子将打开一个 activity(MainActivity)。

图 1 展示了通知如何显示给用户:

图 1. 后台服务通知的屏幕截图,左图是状态栏的通知图标,右图是展开的 View。

只有那些确实运行着用户明显关注任务的服务,才应该保持为“后台服务”状态。一旦不再需要,就应该调用 stopForeground() 进行释放:

stopForeground(true);

详情请参阅 服务状态栏通知 的文档。

Audio Focus 的处理

虽然在任一给定时刻只能运行一个 activity,Android 仍是一个多任务环境。这向使用音频的应用程序提出了一个特殊的挑战,因为系统只有一路音频输出,但可能会存在多个媒体服务,它们会相互争夺这个音频输出的使用权。Android 2.2 之前,没有什么内部机制来解决这个问题,某些情况下这可能会导致用户体验很糟糕。比如在用户听音乐时,其它应用程序需要通知用户一个非常重要的事件,用户可能会由于音乐声音较大而听不到通知提示音。自 Android 2.2 开始,系统为应用程序提供了一种使用设备音频输出的协调机制。这种机制叫做 Audio Focus。

当应用程序需要输出诸如音乐或通知音之类的音频时,应该总是提出 audio focus 请求。一但获得了 focus,就可以自由使用音频输出,但应该时刻注意 focus 的变化情况。一旦接到放弃 audio focus 的通知,就应该立即关闭音频或者把音量调低至静音状态(正如“ducking”——此标志表明哪一个更合适),并且在再次获得 focus 之后才能恢复正常音量播放。

Audio Focus 事实上具有良好的协作性。也就是说,虽然期望(强烈建议)应用程序能遵守 audio focus 规则,但此规则并不是系统强制要求的。如果应用程序需要在失去 audio focus 后也能以正常音量播放音乐,系统也不会阻止。但是用户体验很可能会很糟糕,并且很可能会删除这种不够礼貌的应用程序。

要发起 audio focus 请求,必须调用 AudioManager 中的 requestAudioFocus() ,示例如下:

AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);

int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,

AudioManager.AUDIOFOCUS_GAIN);

if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {

// 无法获取 audio focus.

}

requestAudioFocus() 的第一个参数是 AudioManager.OnAudioFocusChangeListener 对象,每次 audio focus 发生改变时都会调用该对象的 onAudioFocusChange() 方法。因此,必须在服务和 activity 中都实现这个接口。例如:

class MyService extends Service

implements AudioManager.OnAudioFocusChangeListener {

// ....

public void onAudioFocusChange(int focusChange) {

// 根据 focus 的改变进行处理...

}

}

focusChange 参数标明了 audio focus 已经发生改变,它可能会是以下值之一(在 AudioManager 类中定义了所有下列常数):

· AUDIOFOCUS_GAIN :已经获得了 audio focus。

· AUDIOFOCUS_LOSS :似乎已失去 audio focus 较长时间了。这时必须停止所有的音频播放。因为不应该会较长时间地失去 focus,这是尽可能多地清理资源的绝好时机。比如应该释放 MediaPlayer

· AUDIOFOCUS_LOSS_TRANSIENT :暂时失去了 audio focus,但应该会马上取回来。这时必须停止所有的音频播放工作,但因为可能马上再次获得 focus,所以可以保持资源。

· AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK :暂时失去了 audio focus,但允许继续安静(小声)地播放音频,而不是完全关闭音频。

以下是实现的例子:

public void onAudioFocusChange(int focusChange) {

switch (focusChange) {

case AudioManager.AUDIOFOCUS_GAIN:

// 恢复播放

if (mMediaPlayer == null) initMediaPlayer();

else if (!mMediaPlayer.isPlaying()) mMediaPlayer.start();

mMediaPlayer.setVolume(1.0f, 1.0f);

break;

case AudioManager.AUDIOFOCUS_LOSS:

// 长时间失去 focus:停止播放并释放 media player

if (mMediaPlayer.isPlaying()) mMediaPlayer.stop();

mMediaPlayer.release();

mMediaPlayer = null;

break;

case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:

// 暂时失去 focus,但必须停止播放

// 可能会很快恢复播放,所以不释放 media player

if (mMediaPlayer.isPlaying()) mMediaPlayer.pause();

break;

case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:

// 暂时失去 focus,但可以保持较低级别的播放

if (mMediaPlayer.isPlaying()) mMediaPlayer.setVolume(0.1f, 0.1f);

break;

}

}

请记住 audio focus API 在 API level 8 (Android 2.2) 以上版本才可用。假如需要支持较低版本的 Android,应该采取向后兼容的方案,使得程序能在获得支持时使用此功能、未获支持时则向下平滑过渡。

通过反射机制调用 audio focus 方法,或者在单独的类中(叫做 AudioFocusHelper)实现全部 audio focus 功能,就可以获得良好的向后兼容性。以下是这种类的一个示例:

public class AudioFocusHelper implements AudioManager.OnAudioFocusChangeListener {

AudioManager mAudioManager;

// 在此写入其它内容,

// 将保存一个接口的引用,用于通知服务 focus 已发生改变。

public AudioFocusHelper(Context ctx, /* other arguments here */) {

mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);

// ...

}

public boolean requestFocus() {

return AudioManager.AUDIOFOCUS_REQUEST_GRANTED ==

mAudioManager.requestAudioFocus(mContext, AudioManager.STREAM_MUSIC,

AudioManager.AUDIOFOCUS_GAIN);

}

public boolean abandonFocus() {

return AudioManager.AUDIOFOCUS_REQUEST_GRANTED ==

mAudioManager.abandonAudioFocus(this);

}

@Override

public void onAudioFocusChange(int focusChange) {

// 服务获知 focus 的变动

}

}

仅当检测到系统运行于 API level 8 以上版本时,才可以创建 AudioFocusHelper 类的实例。比如:

if (android.os.Build.VERSION.SDK_INT >= 8) {

mAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this);

} else {

mAudioFocusHelper = null;

}

进行清理

前面提到过,对象可能会消耗相当多的系统资源,因此应该按需保持其运行并在用完后及时调用 release() 。请显式地调用这个清理方法而不要依赖于系统的垃圾回收机制,这点非常重要。因为等到垃圾回收器回收 MediaPlayer 时可能已经过去相当长的时间了,回收器是只对内存需求敏感的,而对其他媒体相关的资源短缺是不做判断的。因此,在使用服务的时候,应该总是覆盖 onDestroy() 方法,以确保释放 MediaPlayer

public class MyService extends Service {

MediaPlayer mMediaPlayer;

// ...

@Override

public void onDestroy() {

if (mMediaPlayer != null) mMediaPlayer.release();

}

}

除了在关闭时释放掉之外,还应该总是寻找其它合适的机会来释放 MediaPlayer 。例如,当预计到较长时间内无法播放媒体时(比如失去 audio focus 后),应该明确地释放已有的 MediaPlayer 并稍后再重新创建。另一方面,如果只是想停止播放一会儿,则应该保持住 MediaPlayer ,以避免创建和再次准备的开销。

对意图 AUDIO_BECOMING_NOISY 的处理

在事件到来时,很多编码优秀的音频播放程序都会自动停止播放,因为事件会让音频输出产生噪音(通过外部扬声器播放出来)。比如,用户用耳机听音乐时突然把耳机从设备上拔出来,就可能会产生噪音。不过好在这种现象不会自动发生。如果不实现暂停播放,声音就会从外部扬声器中传出,这可能是用户不愿意听到的。

这种场合下,可以通过处理 ACTION_AUDIO_BECOMING_NOISY 意图来确保应用程序停止播放音乐,在 manifest 中加入以下代码,可以注册一个处理意图的接收器:

<receiver android:name=".MusicIntentReceiver">

<intent-filter>

<action android:name="android.media.AUDIO_BECOMING_NOISY" />

</intent-filter>

</receiver>

以下代码注册了 MusicIntentReceiver 类,用作该意图的广播接收器:

public class MusicIntentReceiver implements android.content.BroadcastReceiver {

@Override

public void onReceive(Context ctx, Intent intent) {

if (intent.getAction().equals(

android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {

// 通知服务来停止播放

// (比如通过一个意图)

}

}

}

从 Content Resolver 中读取媒体

媒体播放程序中另一个可能有用的功能就是从用户设备上读取音乐。通过查询外部媒体的 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) {

// 查询失败,处理错误。

} else if (!cursor.moveToFirst()) {

//设备上不存在媒体文件

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

// ...开始处理...

} while (cursor.moveToNext());

}

按以下步骤与 MediaPlayer 一起使用:

long id = /* 从某处读取 */;

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

// ...准备并开始播放...

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文