MediaPlayer
Android多媒体框架支持播放各种常见的媒体类型,所以你可以很简单地集成视频,音频和图片到应用程序中。你可以播放来自存储在应用程序(raw资源),文件系统或网络数据流的音频或视频媒体文件。这些都可以使用MediaPlayer APIs。
本文展示如何编写一个与用户交互的媒体播放应用程序,以及系统如何获取好的性能和用户体验。
Note:你只能回放音频数据到标准输出设备中。目前有移动设备扬声器或蓝牙耳机。不能在来电话通话期间播放音频文件。
基础
以下是Android框架播放声音和视频使用类:
MediaPlayer
播放声音和视频的主类。
AudioManager
设备上管理音频资源和音频输出的类。
清单文件声明
在开始使用MediaPlayer开发应用程序之前,确保你清单文件有适当的声明来使用相关的特性:
- 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的使用
媒体框架最重要的组件之一就是MediaPlayer类。MediaPlayer可以以最少的设置获取,解码和播放音频跟视频。它支持几种不同的媒体资源,如:
- 本地资源
- 内部URIs,比如可能获取来自Content Resolver的URI
- 外部URLs(流)
获取Android支持的媒体格式,参看Android Supported Media Formats文档。
这里演示如何播放一个有效的本地raw音频资源(文件存储于应用程序res/raw/目录下):
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); // no need to call prepare(); create() does that for you
在这种情况中,系统不会用任何方式解析原生资源文件。这个资源的内容不应该是原音频。它应该被正确地编码和格式化为一种Android所支持的媒体文件格式。
这里演示如何播放来自系统本地有效的URI(比如通过Content Resolver获取的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();
Note:如果你传递一个URL将在线媒体文件转成流,这文件应该支持断点下载。
注意:当使用setDataSource()时需要捕获 IllegalArgumentException 和 IOException 异常,因为文件引用可能不存在。
异步准备
原则上是可以直截了当地使用MediaPlayer的。但是,有几件事要牢记,就是关于Android应用程序正确得集成MediaPlayer所需要做的事。比如,调用prepare()会占用很长的执行时间,因为它包含了获取以及解码媒体数据。所以,诸如任何这种可能占用长时间执行的方法,都不能在应用的UI线程中去调用。在主线程调用会导致在方法返回之前用户体验非常不好的UI阻塞,以及造成ANR(Application Not Responding)的错误。即使你想快速下载资源,也要记住任何超过10秒才响应UI的事都会引起明显的停顿,这让用户留下你的应用程序很慢的印象。
为了避免UI线程阻塞,开启另外一个线程准备MediaPlayer,并在准备结束后通知主线程。然而,当你自己可以使用MediaPlayer编写线程逻辑时,你会发现这个模板太常见了,因此Android框架提供了一个简便的方式通过使用prepareAsync()完成这个工作。这个方法在后台准备媒体并立即返回。当媒体准备好之后,会调用通过setOnPreparedListener() 配置的MediaPlayer.OnPreparedListener监听器中的onPrepared()方法。
管理状态
MediaPlayer另一个需要谨记的点就是它的基本状态。MediaPlayer有一个编写代码时总要意识的内部状态,因为某些操作只在播放器指定的状态才有效。如果你在一个错误的状态中实现一个操作,系统可能抛出异常或出现其他不可预期的表现。
MediaPlayer类的文档展示一个状态完成 图,它展示MediaPlayer从一个个状态变另一个状态。例如,当你创建一个新的MediaPlayer时,它在Idle状态。在那时候,你应该通过调用setDataSource()初始化,并进入Initialized 状态。在这之后,你得使用prepare()或prepareAsync()方法。当MediaPlayer结束准备阶段后,就进入 Prepared状态,这意味着你可以调用start()来播放媒体。正如完成状态图所示,在那时候,你可以通过调用如start(),pause(),seekTo()以及其它方法进入Started,Paused和PlaybackCompleted状态。当你调用stop()时,你会发现除非你的MediaPlayer再次准备,否则不能调用start()。
当需要用MediaPlayer对象编写交互代码时,牢记状态完成图。因为在不恰当的状态中调用它方法是一个常常出现的bugs。
释放MediaPlayer
MediaPlayer会消耗系统可用的资源。因此,你应该格外注意确保MediaPlayer实例没有占用额外的时间。当你留意到这点之后,你可以调用release()确保任何分配给MediaPlayer的系统资源能正确释放。例如,你正在使用一个MediaPlayer时,你的activity调用了onStop(),你必须释放MediaPlayer,因为当activity没有与用户交互时,持有这对象是没有意义的(除非你正在后台播放媒体,这情况将在下文讨论)。当你的activity调用了onResume()或onRestart()时,当然,你需要在重新回放之前创建一个新的MediaPlayer并再次prepare。
下面是如何释放并将MediaPlayer置为null的代码:
mediaPlayer.release();
mediaPlayer = null;
举个例子,考虑下出现这种问题的情况,假如当你的activity stop掉后,你忘记释放了MediaPlayer,但是在activity再次start后你又创建了一个新的MediaPlayer。 正如你所知,当用户改变屏幕方向(或通过另一种方式改变设备的配置),系统默认是通过restart activity处理的,所以你可能很快就消耗掉所有的系统资源了,如果用户前后水平和竖直不断旋转设备,因为每次方向的改变,你都会重新创建一个没有释放MediaPlayer。(获取更多关于运行时resstart的信息,参看Handling Runtime Changes)
你可能想知道当用户离开activity之后,你想跟内置音乐应用程序一样继续播放背景媒体会发生什么事。这种情况,你所需要的是一个受Service控制的MediaPlayer对象,这内容在下文讨论。
在Service中使用MediaPlayer
如果你想在后台播放媒体,即使应用程序不在当前屏幕显示时。比如用户跟其他应用程序交互时,你想让它继续播放。因此你必须开启一个Service并通过它控制MediaPlayer实例。
你需要将MediaPlayer嵌入到一个 MediaBrowserServiceCompats服务中,并通过MediaBrowserCompat让它在另一个activity中交互。
你应该注意客户/服务的建立。关于播放器如何在一个与系统交互的后台服务中运行是值得期望的。如果你的应用程序没有满足这些期望,用户可能有一个不好的体验。阅读Building an Audio App 获取全部细节。
这部分对MediaPlayer在一个service实现并对MediaPlayer的管理做了特别的介绍。
异步运行
首先,Service所有的工作像Activity一样默认是在一个单独的线程完成。事实上,如果你在同个应用程序中运行一个activity和一个Service,它们默认是使用相同的线程的(主线程)。因此,服务需要快速处理传入的intents,不能处理耗时长的计算。如果你想做一些耗时跟阻塞的工作,你必须异步做这些工作:可以在另一个线程中实现,或使用框架的其他接口实现异步处理。
比如,当你在主线程中使用MediaPlayer,你应该调用prepareAsync()而不是prepare(),并实现MediaPlayer.onPreparedListener以便在MediaPlayer准备结束时通知你可以开始播放。示例代码如:
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();
}
}
处理异步错误
在同步操作中,errors通常会用异常或错误的代码来提示,但是,无论何时使用异步资源,你都应该确保应用程序有适当的错误提示。MediaPlaye针对这种情况,通过实现MediaPlayer.OnErrorListener监听器来完成这个提示工作:
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会进入Error状态(参看MediaPlayer类的文档获取完整的状态图),你必须在再次使用它之前重置它。
使用唤醒锁
当设计在后台播放媒体的应用程序时,设备可能在service运行期间进入休眠状态。由于Android系统在设备进入休眠时会尝试着节约电池,所以系统会尝试停掉手机不需要的特性,包括CPU还有WiFi硬件。然而,如果你的service正在播放或流式传输音乐,你得防止系统对播放的干扰。
为了确保你的服务能持续运行在这种条件下,你不得不使用”唤醒锁”。唤醒锁是一种提示系统你的应用程序正在使用一些特性,即使手机进入空闲状态也需要保持活跃的方式。
Note:你应该谨慎地使用唤醒锁,并在真正需要的地方使用它们,因为唤醒锁会非常消耗设备的电量。
为了确保MediaPlayer在播放时CPU持续运行,在MediaPlayer初始化时调用setWakeMode()方法。一旦这么做,MediaPlayer会在播放时持有指定的锁,在暂停或停止时释放这个锁。
mMediaPlayer = new MediaPlayer();
// ... other initialization here ...
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
然而,例子中获取的唤醒锁只是保证CPU的清醒状态。如果你使用WiFi并通过网络流式化传输媒体,你可能也要持有WifiLock,这个锁也需要手动释放。所以,当你使用远程URL开始准备MediaPlayer时,你应该创建以及获取Wi-Fi 锁,比如:
WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock");
wifiLock.acquire();
当你暂停或停止媒体,或你不再需要需要网络时,你应该释放这个锁:
wifiLock.release();
执行清理
如前面所提及的,MediaPlayer对象会消耗大量的系统资源,所以你应该在需要的时候持有该对象,并在结束工作的时候release()资源。你应该执行这个清理方法而不是等系统gc,因为它可能在系统gc回收MediaPlayer之前占用很长的时间。因为它只对内存需求敏感,而不是缺少其他媒体相关资源。所以当你使用一个service时,你要重写onDestroy()方法确保释放MediaPlayer。
public class MyService extends Service {
MediaPlayer mMediaPlayer;
// ...
@Override
public void onDestroy() {
if (mMediaPlayer != null) mMediaPlayer.release();
}
}
除了MediaPlayer正在释放时,你应该寻找其他释放MediaPlayer的时机。例如,当你不在希望播放媒体例如,如果你希望长时间无法播放媒体(例如,在失去音频焦点之后),你应该肯定会释放现有的MediaPlayer并稍后重新创建。另一方面,如果你想停止回访一小会,你可能需要继续持有MediaPlayer对象避免再次创建和准备它的开销。
从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...
原文链接:https://developer.android.google.cn/guide/topics/media/mediaplayer.html