处理音频焦点
尽管某个时刻只有一个activity可以运行,Android却是一个多任务环境.这对使用音频的应用带来了特殊的挑战,因为只有一个音频输出而可能多个媒体都想用它.在Android2.2之前,没有内建的机制来处理这个问题,所以可能在某些情况下导致坏的用户体验.例如,当一个用户正在听音乐而另一个应用需要通知用户一些重要的事情时,用户可能由于音乐声音大而不能听的通知.从Android2.2开始,平台为应用提供了一个协商它们如何使用设备音频输出的途径,这个机制叫做音频焦点.
当你的应用需要输出像乐音和通知之类的音频时,你应该总是请求音频焦点.一旦应用具有了焦点,它就可以自由的使用音频输出.但它总是应该监听焦点的变化.如果被通知丢失焦点,它应该立即杀死声音或降低到静音水平(有一个标志表明应选择哪一个)并且仅当重新获得焦点后才恢复大声播放.
将来的音频焦点是合作的.所以,应用被希望(并被强列鼓励)遵守音频焦点的方针,但是却不是被系统强制的.如果一个应用在丢失音频焦点后依然想大声播放音乐,系统不会去阻止它.然而用户却体验很坏并且很想把这鸟应用卸载.
要请求音频焦点,你必须从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) {
// 不能获得音频焦点
}
|
requestAudioFocus()的第一个参数是一个AudioManager.OnAudioFocusChangeListener,它的onAudioFocusChange()方法在音频焦点发改变时被调用.因此,你也应该在你的service和activity上实现此接口.例如:
|
class MyService extends Service
implements AudioManager.OnAudioFocusChangeListener {
// ....
public void onAudioFocusChange(int focusChange) {
// Do something based on focus change...
}
}
|
参数focusChange告诉你音频焦点如何发生了变化,它可以是以上几种值(它们都是定义在AudioManager中的常量):
- AUDIOFOCUS_GAIN:你已获得了音频焦点.
- AUDIOFOCUS_LOSS:你已经丢失了音频焦点比较长的时间了.你必须停止所有的音频播放.因为预料到你可能很长时间也不能再获音频焦点,所以这里是清理你的资源的好地方.比如,你必须释放MediaPlayer.
- AUDIOFOCUS_LOSS_TRANSIENT:你临时性的丢掉了音频焦点,很快就会重新获得.你必须停止所有的音频播放,但是可以保留你的资源,因为你可能很快就能重新获得焦点.
- AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:你临时性的丢掉了音频焦点,但是你被允许继续以低音量播放,而不是完全停止.
下面是一个例子:
|
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
// resume playback
if (mMediaPlayer == null) initMediaPlayer();
else if (!mMediaPlayer.isPlaying()) mMediaPlayer.start();
mMediaPlayer.setVolume(1.0f, 1.0f);
break;
case AudioManager.AUDIOFOCUS_LOSS:
// Lost focus for an unbounded amount of time: stop playback and release media player
if (mMediaPlayer.isPlaying()) mMediaPlayer.stop();
mMediaPlayer.release();
mMediaPlayer = null;
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// Lost focus for a short time, but we have to stop
// playback. We don't release the media player because playback
// is likely to resume
if (mMediaPlayer.isPlaying()) mMediaPlayer.pause();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// Lost focus for a short time, but it's ok to keep playing
// at an attenuated level
if (mMediaPlayer.isPlaying()) mMediaPlayer.setVolume(0.1f, 0.1f);
break;
}
} |
记住音频焦点API仅在APIlevel 8 (Android2.2)及更高版本上可以,所以如果你想支持更早的Android版本,你必须在可能时采取兼容性的策略使用特性,如果不可能,you should adopt a backward compatibility strategy that allows you touse this feature if available, and fall back seamlessly if not.
你可以用反射的方式调用音频焦点方法或自己在一个单独的类(叫做AudioFocusHelper)中实现所有的音频焦点功能来达到向前兼容.下面是一个这样的类:
|
public class AudioFocusHelper implements AudioManager.OnAudioFocusChangeListener {
AudioManager mAudioManager;
// 这里是其它的字段,你可能要保存一个接口的引用,这个接口
// 被用于与你的service通讯以报告焦点的变化.
public AudioFocusHelper(Context ctx, /* 其它的参数 */) {
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) {
// 让你的service知道焦点变化了
}
}
你可以仅在检测到系统运行的是API level 8 或更早的版本时才创建AudioFocusHelper类的实例.例如:
if (android.os.Build.VERSION.SDK_INT >= 8) {
mAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this);
} else {
mAudioFocusHelper = null;
} |
清理
前面提到过,一个MediaPlayer对象可以消耗掉大量的系统资源,所以你应该仅在需要它时保持它并在用完时立即释放.明确的调用清理方法而不是依靠系统的垃圾收集机制是很重要的,因为在被收集之前MediaPlayer可能会存在很长时间,虽然此时它只是占用内存而不影响其它的媒体相关的资源.所以,当你使用一个service时,你应该总四重写onDestroy()方法来保证释放MediaPlayer.
|
public class MyService extends Service {
MediaPlayer mMediaPlayer;
// ...
@Override
public void onDestroy() {
if (mMediaPlayer != null) mMediaPlayer.release();
}
} |
你也应该寻找其它需要释放你的MediaPlayer的时机.例如,如果你预料到长时间不能播放媒体(比如丢掉音频焦点以后),你应该明确地释放你的MediaPlayer,然后在后面重新创建它.反过来,如果你预测到只会短时间停止播放,你应该保持你的MediaPlayer来避免过多的创建,而不是重新"准备"它.
处理AUDIO_BECOMING_NOISYIntent
很多良好的音频播放的应用都会在那些导致声音变为噪音(通过外部扬声器输出)的事件发生时自动停止播放.例如,这可能发生在当一个用户用耳机听音乐时忽然断开了耳机连接.音频从扬声器播放可能不是用户期望的.
你可以通过处理ACTION_AUDIO_BECOMING_NOISYintent 来保证你的应用在此情况下停止播放音乐,你可以把如下代码添加到你的manifest来注册一个receiver.
|
<receiver android:name=".MusicIntentReceiver">
<intent-filter>
<action android:name="android.media.AUDIO_BECOMING_NOISY" />
</intent-filter>
</receiver> |
此段把MusicIntentReceiver类作为这个intent的一个广播接收器(broadcastreceiver)进行注册,下面就是要实现这个类:
|
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)) {
// 通知你的service停止播放
// (比如通过一个Intent)
}
}
} |
从一个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) {
// 查询失败,处理错误
} 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);
// ...process entry...
} while (cursor.moveToNext());
}
|
要在MediaPlayer中播放获取到的媒体,你可以这样做:
|
long id = /* 上面获取到的某个条目的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);
// ...prepare and start... |
JetPlayer
Android平台包含一个JET引擎,用它可以添加交互式播放的JET音频内容到你的应用.你可以使用SDK所带的JetCreator应用来创建JET内容.要播放和管理JET内容,使用JetPlayer类.
播放JET内容
本节教给你如何写出,配置和播放JET内容.JET的概念,介绍以及如何使用JetCreator工具创建JET内容,请看JetCreator用户手册,这里不叨叨.这个工具可以在Windows,OS X, 和Linux平台(Linux上不能像Windows和OSX上那样对导入的资产进行试听)上使用.
下面是如何对存储在SD卡上的一个.jet文件配置JET回放:
|
JetPlayer jetPlayer = JetPlayer.getJetPlayer();
jetPlayer.loadJetFile("/sdcard/level1.jet");
byte segmentId = 0;
// queue segment 5, repeat once, use General MIDI, transpose by -1 octave
jetPlayer.queueJetSegment(5, -1, 1, -1, 0, segmentId++);
// queue segment 2
jetPlayer.queueJetSegment(2, -1, 0, 0, 0, segmentId++);
jetPlayer.play(); |
SDK包含一个例子— JetBoy —它向你演示了如何使用JetPlayer在你的游戏中创建一个交互式音轨.它也演示了如何使用JET事件来同步音乐和游戏逻辑.
|
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// 不能获得音频焦点
}
|
相机
Android框架架包含了各种相机和相机功能的支持,使你可以在你的应用中捕获图像和视频.本文档讨论一个简单快速的获取图像和视频的方法,并概述一个创建自定义用户相机体验的高级方法.
想一想
在使你的应用能使用设备上的相机之前,你应该先想一想你的应用将来会如何使用此硬件.
- Camera必须 -相机是必须的,你不希望你的应用安装到一个没有相机的设备上.你应该在manifest文件中声明需要相机.
- 快速图像或自定义特性 -你的应用将如何使用相机?你仅仅是抓取一个快速图片或视频剪辑,还是提供一个新的使用方式?前者请考虑使用现有的相机应用.后者请阅读后面的"创建一个相机应用"一节.
- 存储 -你的应用产生的图像和视频要给自己看还是共享给其它应用?你想在你的应用被删除后所创建的图像和视频仍然存在吗?请阅读后面的"保存媒体文件"一节来学习如何实现这些操作.
基础知识
Android框架支持通过CameraAPI或Cemeraintent来抓取图像和视频.下面就是相关的类们:
- Camera 此类是控制设备相机的主要API.此类用于在创建相机应用时获取图片和视频.
- SurfaceView 此类为用户提供camera的实时图像预览.
- MediaRecorder 此类用于从camera录制视频.
- Intent 一个MediaStore.ACTION_IMAGE_CAPTURE或MediaStore.ACTION_VIDEO_CAPTURE型的intent,可以使用它来抓取图像或视频,而不用操作Camera对象们.
Manifest中的声明
在使用CameraAPI开发你的应用之前,你需保证在你的manifest中声明了合适的条目使得有权使用相机和其它相关特性.
- CameraPermission - 你的应用必须请求使用设备相机的取限.
<uses-permissionandroid:name="android.permission.CAMERA" />
注:如果你通intent使用camera,你的应用不必请求此权限.
- CameraFeatures - 你的应用必须也要声明要使用的相机特性,比如:
<uses-featureandroid:name="android.hardware.camera" />
对于相机特性列表,请见manifestFeatures Reference.
添加相机特性到你的manifest导致Android市场不会将你的应用安装到没有相机相机特性达不到你所声明要求的设备上.
如果你的应用要使用相机或相机的一些特性,但又不是必须的,你应该在manifest中指定这些需求,但把android:required属性置为false:
<uses-featureandroid:name="android.hardware.camera"android:required="false" />
- StoragePermission - 如果你的应用要存储图像或视频到外部存储上(SD卡),你必须也声明此权限.
<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
- AudioRecording Permission - 在使用视频捕获设备来录制音频时,你的应用必须请求音频捕获权限.
<uses-permissionandroid:name="android.permission.RECORD_AUDIO" />
使用现有的相机应用
一个不用写代码来获取图片和视频的快速方法是使用intent来调用现有的Androidcamera 应用.一个cameraintent调用现存的相机应用抓取图片或视频剪辑然后返回的你的应用.本节向你演示如何使用此技术获取图片和视频.
调用一个相机intent,主要有以下步骤:
- 组建一个相机Intent– 创建一个请求图片或视频的Intent,使用以下intent类型中的一个:
- MediaStore.ACTION_IMAGE_CAPTURE- 从已存在的相机应用中请求一个图片.
- MediaStore.ACTION_VIDEO_CAPTURE- 从已存在的相机应用中请求一个视频.
- 启动这个相机Intent-使用startActivityForResult()方法来执行相机intent.在你启动intent后,相机应用的界面会出现在设备屏幕上,然后用户就可以用它来获取图片或视频.
- 接收Intent结果-在你的应用中设置一个onActivityResult()方法来接收从相机intent来的回调和数据.当用户获取了一个图或视频之后(或取消了操作),系统就会调用此方法.
图像获取intent
使用相机intent获取图像是使用最少代码获取图像的捷迳.一个图像获取intent包含以下额外信息:
- MediaStore.EXTRA_OUTPUT-此设置需要一个Uri对象,这个对象指定了一个保存图像的路径和文件名.此设置是可选的,但是强烈建议使用之.如果你没有指定此值,相机应用就会把图像以默认的名字保存到默认的位置.
下面的例子演示了形成一个图像获取intent并执行的方法.此例子中的getOutputMediaFileUri()方法是引用的”保存媒体文件”一节中的例子代码.
|
private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 100;
private Uri fileUri;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// 创建一个获取图像的Intent
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
fileUri = getOutputMediaFileUri(MEDIA_TYPE_IMAGE); // 创建一个文件来保存图像
intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); // 设置图像文件名
// 开始图像获取Intent
startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE);
} |
当startActivityForResult()方法被执行,看到一个相机应用的界面.当用户获取了一个图像(或取消了操作),用户界面返回到你的应用,你必须拦截onActivityResult()方法来接收intent的结果然后再继续执行你的应用.
视频获取intent
使用相机intent获取视频是使用最少代码使得你的应用获取视频的捷径.一个视频获取intent可以包含以下额外信息:
- MediaStore.EXTRA_OUTPUT-此设置需要一个保存视频的路径和文件名的Uri.此设置是可选的但是强列推荐的.如果你不指定此值,相机应用就把请求到的图像以默认的文件名保存到默认的文件夹下,这些信息保存在返回的intent的Intent.getData()字段中.
- MediaStore.EXTRA_VIDEO_QUALITY- 此值在最低质量最小文件尺寸时是0,在最高质量最大文件尺寸时是1.
- MediaStore.EXTRA_DURATION_LIMIT- 此值设置获取视频的长度,以秒为单位.
- MediaStore.EXTRA_SIZE_LIMIT- 此值设置获取视频文件的大小,以字节为单位.
下面的例子演示了如何构建一个视频获取intent并执行它.此例子中的getOutputMediaFileUri()方法是引用的”保存媒体文件”一节中的例子代码.
|
private static final int CAPTURE_VIDEO_ACTIVITY_REQUEST_CODE = 200;
private Uri fileUri;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//create new Intent
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
fileUri = getOutputMediaFileUri(MEDIA_TYPE_VIDEO); // create a file to save the video
intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); // set the image file name
intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1); // set the video image quality to high
// start the Video Capture Intent
startActivityForResult(intent, CAPTURE_VIDEO_ACTIVITY_REQUEST_CODE);
} |
当startActivityForResult()方法执行后,用户看到了一个改良的相机应用界面.在用户完成视频获取(或取消了操作)之后,用户界面返回到你的应用,你必须拦截onActivityResult()方法来接收intent的结果并且继续执行你的应用.
android 多媒体和相机详解(一) android 多媒体和相机详解(二) |