Manage Audio

Media Playback

Android多媒体框架包涵了对播放多种通用媒体的类型的支持,所以你可以很容易的集成音频,视频和图像到你的应用中。你可以使用MediaPlayer APIs播放保存在应用raw资源中单独的媒体文件,或者通过网络连接下载的数据流

The Basics

以下的类在Android框架中被用于播放声音和视频

MediaPlayer 这个类主要用于播放声音和视频
AudioManager 这个类管理设备上的音频源和音频输出
Manifest Declarations

在使用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" />

Using MediaPlayer

media框架最重要的类之一就是MediaPlayer类。这个类的对象可以使用最少步骤获取,解析,播放音频和视频。它支持几个不同的媒体源。
比如:
本地资源
网络URIs,比如你从Content Resolver获取到的
外部URLs(流)
具体android支持的媒体格式,请查看文档
以下例子演示了怎么播放一个本地raw资源中可用的音频

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

以下是从本地播放可用URI(从Content Resolver获取到的)

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

以下是通过HTTP远程播放远程的流

String url = "http://........";
 // your URL hereMediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare(); // might take long! (for buffering, etc)
mediaPlayer.start();

注意:当你使用setDataSource时,你必须处理 IllegalArgumentException或IOException,因为你索引的文件可能不存在

Asynchronous Preparation

MediaPlayer原则上是可以直接使用的,但是有一些要点是需要记住的,比如,prepare()的调用可能会执行很长时间,因为它涉及到提取,解析媒体数据,所以,像其他需要长时间运行的方法,你应该避免在UI线程中调用。因为这样做会使UI线程挂起直到方法返回,这是非常不好的用户体验并且可能导致ANR错误。即使你预计你的资源会加载的很快,超多1/10秒才回应就会导致卡顿,这会给用户留下你的app很慢的映像
为了避免UI线程挂起,创建一个新的线程去prepare MediaPlayer并在完成时通知主线程,框架提供了一个便捷的方法prepareAsync()去完成这个任务,这个方法在后台prepare音频并在完成时立马返回,当音频完成preparing时,将调用通过setOnPreparedListener()配置的MediaPlayer.OnPreparedListener的onPrepared()方法。

Managing State

从MediaPlayer的另一个方面来说它是基于状态的。也就是说,MediaPlayer有一个内部状态,你在编写代码时必须始终注意,因为某些操作只有在播放器处于特定状态时才有效。如果在错误状态下执行操作,系统可能会抛出异常或导致其他不需要的行为。

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

当编写与MediaPlayer对象交互的代码时,始终记得状态图,因为从错误状态调用其方法是错误的常见原因。

Releasing the MediaPlayer

MediaPlayer会消耗有限的系统资源,所以,你应该预防在不需要的时候还持有Mediaplayer的实例,当你完成你的任务时,你应该调用release()去释放任何分配给你的资源,例如,如果你正在使用MediaPlayer但是你的Activity的onStop方法被回调了,那么你应该立即释放MediaPlayer,因为当你的活动没有与用户交互时(除非你在后台播放媒体,这将在下一节讨论),保持它是没有意义的。当你的Activity恢复或者重新启动了,你需要创建一个新的MediaPlayer并重新prapare

mediaPlayer.release();
mediaPlayer = null;

例如,如果你在活动停止时忘记释放MediaPlayer,但在活动再次启动时创建一个新的MediaPlayer,可能会发生的问题。我们都知道,当用户更改屏幕方向(或以其他方式更改设备配置)时,系统会通过重新启动活动(默认情况下)来处理,因此当设备在纵向和横向之间来回切换时会快速消耗所有系统资源,因为在每个方向更改时,你将创建一个新的MediaPlayer,但你从不释放。 (有关运行时重新启动的详细信息,请参阅处理运行时更改。)

你可能想知道,如果你想继续播放“后台媒体”,即使用户离开你的活动,很大程度上与内置音乐应用程序的行为相同的方式。 在这种情况下,你需要的是由服务控制的MediaPlayer,如在使用MediaPlayer中的服务中所述。

Using a Service with MediaPlayer

如果你希望你的媒体在后台播放,即使你的应用程序没有运行在当前屏幕上,也就是说,你希望你的媒体在用户与其他应用程序交互时继续播放 - 那么你必须启动一个服务并在服务中控制MediaPlayer实例。 你应该小心这个设置,因为用户和系统都期望运行后台服务的应用程序如何与系统的其余部分交互。 如果你的应用程序不满足这些期望,用户可能会有不好的体验。 本节介绍了你应该注意的主要问题,并提供有关如何处理这些问题的建议。

Running asynchronously

首先,像Activity一样,默认情况下服务中的所有工作都是在单个线程中完成的 - 事实上,如果你在同一个应用程序运行中一个活动和一个服务,默认他们在同一个线程(“主线程 “)中。 因此,服务需要快速处理传入意图,并且在响应它们时不执行冗长的计算。 如果有任何繁重的工作或阻塞调用,你必须异步地执行这些任务:从你自己实现的另一个线程,或使用框架的许多设施进行异步处理。
例如,当你在主线程中使用MediaPlayer,你应该调用prepareAsync()方法而不是prepare()方法,实现MediaPlayer.OnPreparedListener 是为了在preparation完成时可以被通知并且你可以开始播放。例如:

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();
    }
}
Handling asynchronous errors

在同步操作中,通常会使用异常或错误代码来报告错误,但是当使用异步资源时,你应该确保你的应用正确地收到错误通知。 在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会切换到错误状态(完整状态图的MediaPlayer类的文档),你必须重置它,然后才能再次使用它。

Using wake locks

当设计在后台播放媒体的应用程序时,设备可能会在服务运行时进入睡眠状态。 由于Android系统尝试在设备休眠时节省电池,因此系统会尝试关闭手机的任何不需要的功能,包括CPU和WiFi硬件。 但是,如果你的服务正在播放或流式传输音乐,则希望系统不要干扰你的播放。

为了确保你的服务在这些条件下继续运行,你必须使用“唤醒锁”。 唤醒锁是通知系统你的应用程序正在使用一些功能,系统应该保持可用,即使设备处于空闲状态。

注意:你应始终谨慎使用唤醒锁,并且只在真正必要的时间内保持它们,因为它们会显著缩短设备的电池寿命。

要确保CPU在播放MediaPlayer时继续运行,请在初始化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();
Running as a foreground service

服务通常用于执行后台任务,诸如获取电子邮件,同步数据,下载内容以及其它可能性。在这些情况下,用户没有主动地了解服务的执行,并且可能不会注意到这些服务是否被中断并且稍后重新启动。

考虑正在播放音乐的服务。这种服务用户可以清晰的意识到它的活动,任何中断的都会严重影响体验。此外,在服务执行期间用户可能希望与之交互。在这种情况下,服务应作为“前台服务”运行。前台服务在系统中重要性等级更高 - 系统几乎不会杀死服务,因为它对用户是直接重要的。当在前台运行时,服务还必须提供状态栏通知,以确保用户知道正在运行的服务并允许他们打开可与服务交互的Activity。

为了将你的服务切换到前台,你必须为状态栏创建通知,并且为服务调用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);

当你的服务在前台运行时,你配置的通知在设备的通知区域中可见。 如果用户选择通知,系统会调用你提供的PendingIntent。 在上面的示例中,它打开一个Activity(MainActivity)。

你应该只保持“前台服务”状态,而你的服务实际执行用户主动感知的东西。 一旦这不再是true,你应该通过调用stopForeground()释放它:

stopForeground(true);
Handling audio focus

Android是一个多任务环境,但在同一时间中只有一个活动可以在当前屏幕运行。这对使用音频的应用提出了特别的挑战,因为只有一个音频输出,并且可能存在若干媒体服务竞争其使用。在Android 2.2之前,没有内置的机制来解决这个问题,这在某些情况下可能会导致糟糕的用户体验。例如,当用户正在听音乐而此时另一应用需要向用户通知非常重要的事情时,由于大声的音乐,用户可能听不到通知铃声。从Android 2.2开始,平台为应用程序提供了一种方式,协商其使用设备的音频输出。这种机制称为音频焦点。

当你的应用程序需要输出音频(如音乐或通知)时,应始终请求音频焦点。一旦它有焦点,它可以自由地使用声音输出,但它应该总是监听焦点的更改。如果它被通知已经失去了音频焦点,它应该立即杀死音频或将音量降低到安静的级别(称为“ducking” - 有一个标志,指示哪一个是适当的),并且只有再次接收焦点才能恢复高声播放。

音频焦点应该是合作的。也就是说,应用程序需要(并高度鼓励)遵守音频焦点指南,但规则不是由系统强制执行。如果应用程序想要播放大声的音乐,即使失去音频焦点,系统中的任何东西都不会阻止。然而,用户更可能具有不良体验,并且将更可能卸载行为不当的应用程序

要请求音频焦点,您必须从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) {
    // could not get audio focus.
}

requestAudioFocus()的第一个参数是AudioManager.OnAudioFocusChangeListener,只要音频焦点发生变化,就会调用onAudioFocusChange()方法。 因此,你还应该为你的服务和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仅适用于API级别8(Android 2.2)及更高版本,因此如果你要支持以前的Android版本,则应采用向后兼容性策略,以便可以使用此功能(如果有)

你可以通过反射调用音频焦点方法或通过在单独的类(例如AudioFocusHelper)中实现所有音频焦点功能来实现向后兼容。 这里有一个这样的类的例子:

public class AudioFocusHelper implements AudioManager.OnAudioFocusChangeListener {
    AudioManager mAudioManager;
    // other fields here, you'll probably hold a reference to an interface
    // that you can use to communicate the focus changes to your Service
    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) {
        // let your service know about the focus change
    }
}

仅当检测到系统运行的API级别为8或更高时,才能创建AudioFocusHelper类的实例

if (android.os.Build.VERSION.SDK_INT >= 8) {
    mAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this);
} else {
    mAudioFocusHelper = null;
}
Performing cleanup

如前所述,MediaPlayer对象会消耗大量的系统资源,因此你应该在你需要时保留它,并在完成后调用release()。 显示的调用这个清除方法,而不是依赖系统垃圾收集器时很重要的,因为在垃圾收集器回收MediaPlayer前它可能需要花一些时间,因为它只对内存需求敏感,而不是缺乏其他媒体相关资源。 因此,在使用服务的情况下,您应该总是覆盖onDestroy()方法,以确保你释放MediaPlayer:

public class MyService extends Service {
   MediaPlayer mMediaPlayer;
   // ...
   @Override
   public void onDestroy() {
       if (mMediaPlayer != null) mMediaPlayer.release();
   }
}

除了在关闭时释放MediaPlayer以外,你也应该总是寻找其他机会释放你的MediaPlayer。 例如,如果你预计不能长时间播放媒体(例如,失去音频焦点后),你应该确保会释放您现有的MediaPlayer并稍后重新创建。 另一方面,如果你只希望短时间停止播放,那么你应该保存MediaPlayer,以避免创建和准备再次的开销。

Handling the AUDIO_BECOMING_NOISY Intent

许多精心编写的音频播放程序会在发生导致音频外放(通过外部扬声器输出)的事件时自动停止播放。 例如,可能发生当用户通过耳机收听音乐并意外地将耳机从设备拔掉这种情况。 但是,这种行为不会自动发生。 如果你不实现此功能,音频会从设备的外部扬声器播放,这可能不是用户想要的。

您可以通过处理ACTION_AUDIO_BECOMING_NOISY意图,确保你的应用在这些情况下停止播放音乐,您可以通过向清单添加以下内容来注册接收者:

<receiver android:name=".MusicIntentReceiver">
   <intent-filter>
      <action android:name="android.media.AUDIO_BECOMING_NOISY" />
   </intent-filter>
</receiver>

为ACTION_AUDIO_BECOMING_NOISY的intent注册MusicIntentReceiver类为广播接收器

public class MusicIntentReceiver extends android.content.BroadcastReceiver {
   @Override
   public void onReceive(Context ctx, Intent intent) {
      if (intent.getAction().equals(android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
          // signal your service to stop playback
          // (via an Intent, for instance)
      }
   }
}
Retrieving Media from a Content Resolver

对一个音频播放器app有用的另一个特性是可以在用户的设备上通过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());
}
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...

Manage Audio Playback

1.Controlling Your App’s Volume and Playback

良好的用户体验是可以预测的。如果你的app可以播放media,那么你的用户可以使用他们设备,蓝牙耳机,头戴耳机的硬件或者软件去控制你的app的音量是很重要的。
同样,通过你的app他们可以适当使用play,stop,pause,skip,和previous媒体播放键在音频流上做出各自的行为

Identify Which Audio Stream to Use

创建可预测音频体验的第一步是知道你的app将要使用的音频流,Android支持单独播放音乐,闹钟,通知,来电铃声,系统声音,呼叫中的声音,DTMF音。这主要是为了允许用户独立地控制每个流的音量。大部分音频流仅限于系统事件,所以除非你app替代闹钟,不然,你基本上都是使用STREAM_MUSIC播放你的音频

默认情况下,使用物理音量键区控制你app的音量,按下音量控制键调整正在播放的音频流的音量。如果你的app当前没有播放任何音频流,那么按下音量键调整的将是铃声的音量

如果你开发的是游戏或者音乐app,那么即使他们处于在歌曲切换时或游戏的当前时间点没有音乐,当用户按下音量键也表示他们想要控制游戏或者音乐的音量。

当你的音频流调整声音的时候你可能想要监听音量键的按下事件。Android提供了便利的方法setVolumeControlStream(int)去直接监听你指定的音频流的音量键的按下事件

确定你的应用将使用音频流,你因该设置方法到目标音频流上。该方法你应该在应用生命周期的早期调用,因为在Activity的生命周期你只需要调用一次,典型的,你应该在控制你音频流的Activity或者Fragment的onCreate方法中调用,这确保了一旦你的app可见,音量控制功能也如用户期望的可用

setVolumeControlStream(AudioManager.STREAM_MUSIC);
从这时起,当目标Activity或者Fragment可见的时候,在设备上按下音量键会影响你指定的音频流。使用物理播放键去控制你的App音频的播放

Use Hardware Playback Control Keys to Control Your App’s Audio Playback

当用户按下这些硬件按钮(比如:耳机,许多已连接或者无线连接的头戴耳机的播放,暂停,停止,下一首,上一首),系统会广播一个带ACTION_MEDIA_BUTTON行为的Intent,为了回应音频设备按钮的点击,你需要在你的manifest中注册一个BroadcastReceiver去监听这个行为的广播,如以下示例代码

<receiver android:name=".RemoteControlReceiver">
   <intent-filter>
       <action android:name="android.intent.action.MEDIA_BUTTON" />
   </intent-filter>
</receiver>

接收器实现本身需要提取哪个键被按下来引起的广播,Intent中的EXTRA_KEY_EVENT键下包括此键,而KeyEvent类中包括一系列表示媒体按钮的静态常量KEYCODE_MEDIA_ *列表,例如KEYCODE_MEDIA_PLAY_PAUSE和KEYCODE_MEDIA_NEXT。以下片段显示如何提取按下的媒体按钮,并相应地影响媒体播放。

public class RemoteControlReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
            KeyEvent event = (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
            if (KeyEvent.KEYCODE_MEDIA_PLAY == event.getKeyCode()) {
                // Handle key press.
            }
        }
    }
}

因为可能多个应用想要监听media按钮的按下,所以你还必须通过程序控制应用程序何时应该接收媒体按钮按下事件。
以下代码可在你的应用程序中使用AudioManager注册和注销媒体按钮事件接收器。 注册后,你的广播接收器是所有media按钮广播的专属接收器。

AudioManager am = mContext.getSystemService(Context.AUDIO_SERVICE);
...// Start listening for button
 pressesam.registerMediaButtonEventReceiver(RemoteControlReceiver);
...// Stop listening for button
 pressesam.unregisterMediaButtonEventReceiver(RemoteControlReceiver);

通常,apps应当在它变得非活动或者被可见的时候注销它们的接收器(比如onStop回调中),然而,对于媒体播放应用程序来说并不简单,事实上,当应用程序不可见且无法通过屏幕上的UI控制时,响应媒体播放按钮是最重要的。更好的方法是在应用程序获取音频焦点时注册media按钮事件接收器,丢失音频焦点时注销media按钮事件接收器。

2.Managing Audio Focus

有多个应用程序可能播放音频,重要的是要考虑他们应该如何交互。 为了避免每个音乐应用程序同时播放,Android使用音频焦点来控制音频播放 - 只有拥有音频焦点的应用程序才能播放音频。

在您的应用程序开始播放音频之前,应该请求并接收音频焦点。 同样,它应该知道如何监听音频焦点的丢失,并在发生这种情况时适当地做出反应。

Request the Audio Focus

在你的app开始播放任何音频前,app应该为将要播放的流获取到音频焦点。这是通过调用requestAudioFocus()完成的,如果您的请求成功,它返回AUDIOFOCUS_REQUEST_GRANTED。

你必须指定你要使用的流,以及是需要暂时还是永久的音频焦点。 当你希望只在短时间内播放音频时则请求暂时聚焦(例如在播放导航指示时)。 当您计划在可预见的未来播放音频时则请求永久音频聚焦(例如,在播放音乐时)。

以下片段会为音乐音频流请求的永久音频焦点。 你应该在开始播放之前立即请求音频焦点,例如当用户按下播放或下一个游戏关卡的背景音乐开始前。

AudioManager am = mContext.getSystemService(Context.AUDIO_SERVICE);
...// Request audio focus for playback
int result = am.requestAudioFocus(afChangeListener,
                                 // Use the music stream.
                                 AudioManager.STREAM_MUSIC,
                                 // Request permanent focus.
                                 AudioManager.AUDIOFOCUS_GAIN);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    am.registerMediaButtonEventReceiver(RemoteControlReceiver);
    // Start playback.
}

一旦你完成播放,请确定调用了abandonAudioFocus()方法,这将通知系统你不在需要焦点并注销相关联的AudioManager.OnAudioFocusChangeListener。在放弃暂时焦点的情况下,这允许任何被中断的app继续播放

// Abandon audio focus when playback
 completeam.abandonAudioFocus(afChangeListener);

当请求暂时音频焦点时,您有一个额外选项:是否要启用“低音”(“Ducking”)。 通常,当优秀的音频应用程序失去音频焦点时,它会立即停止它的播放。 通过请求一个暂时音频焦点,你告诉其他音频应用程序你可以接受他们继续播放,只要他们降低音量,直到焦点回到他们。

// Request audio focus for playback
int result = am.requestAudioFocus(afChangeListener,
                             // Use the music stream.
                             AudioManager.STREAM_MUSIC,
                             // Request permanent focus.
                             AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // Start playback.
}

低音尤其适用于间歇性使用音频流的应用程序,例如用于可听见的驾驶方向。

每当另一个应用程序像之前描述的那样请求音频焦点,你在请求焦点时注册的侦听器就会接收另一个应用选择的永久或暂时的音频焦点。

Handle the Loss of Audio Focus

如果你的应用程序可以请求音频焦点,那么当其他应用程序请求焦点时,它会转而失去焦点。 你的应用程序如何响应音频焦点的丢失取决于丢失的方式。

音频焦点改变监听器的回调方法onAudioFocusChange()有一个参数它描述了焦点改变事件。具体来说,可能的焦点丢失事件来自前焦点请求类型一部分 - 永久丢失,暂时丢失和暂时被允许的ducking。

如果音频焦点永久的丢失,假如另一个应用正在使用音频,那么你的app应该快速的结束音频的使用,实际上,这意味着停止播放,移除音频按钮监听器-允许新的音频播放器去单独处理这些事件-并丢弃你的音频焦点。这时,你将期待一个用户行为(在你的app中按下播放)去请求音频焦点在你恢复音频播放前

在以下的代码片段中,我们暂停播放或者我们的音频播放器暂时丢失了焦点,那么当我们获取焦点时恢复它。如果是永久丢失,那么注销我们的音频按钮事件接收器并停止监听音频焦点改变

AudioManager.OnAudioFocusChangeListener afChangeListener =    new AudioManager.OnAudioFocusChangeListener() {
        public void onAudioFocusChange(int focusChange) {
            if (focusChange == AUDIOFOCUS_LOSS_TRANSIENT) {
                // Pause playback
            } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
                // Resume playback
            } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
                am.unregisterMediaButtonEventReceiver(RemoteControlReceiver);
                am.abandonAudioFocus(afChangeListener);
                // Stop playback
            }
        }
    };

在ducking被允许的情况下暂时丢失音频焦点,相对于暂停播放,你可以使用duck代替

Duck!

低音是降低音频流输出音量的过程,使来自另一个应用程序暂时的音频更容易听到,而不会完全中断你自己的应用程序的音频。

在以下代码段中,当我们暂时失去音频焦点时,降低媒体播放器对象上的音量,然后在我们重新获得焦点时将其返回到原来的音量。

OnAudioFocusChangeListener afChangeListener = new OnAudioFocusChangeListener() {
    public void onAudioFocusChange(int focusChange) {
        if (focusChange == AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
            // Lower the volume
        } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
            // Raise it back to normal
        }
    }
};

3.Dealing With Audio Output Hardware

当用户从他们的Android设备享受音频时,用户有很多选择。 大多数设备都有内置扬声器,有线耳机的耳机插孔,许多设备还具有蓝牙连接和支持A2DP音频。

Check What Hardware is Being Used

你的应用程序的行为可能会受到路由到的硬件输出的影响。

您可以查询AudioManager以确定音频当前是否路由到设备扬声器,有线耳机或连接的蓝牙设备,如以下代码段所示:

if (isBluetoothA2dpOn()) {
    // Adjust output for Bluetooth.
} else if (isSpeakerphoneOn()) {
    // Adjust output for Speakerphone.
} else if (isWiredHeadsetOn()) {
    // Adjust output for headsets
} else {
    // If audio plays and noone can hear it, is it still playing?
}
Handle Changes in the Audio Output Hardware

当耳机拔掉或蓝牙设备断开连接时,音频流自动重新路由到内置扬声器。 如果你使用最大音量听你的音乐,这可能是一个噪音。

幸运的是,当发生这种情况时,系统广播ACTION_AUDIO_BECOMING_NOISY的Intent。 当你在播放音频时注册一个BroadcastReceiver监听这个意图是一个好习惯。 在音乐播放器的情况下,用户通常期望暂停播放,而对于游戏,可以选择显着降低音量。

private class NoisyAudioStreamReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
            // Pause the playback
        }
    }
}
private IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);

private void startPlayback() {
    registerReceiver(myNoisyAudioStreamReceiver(), intentFilter);
}

private void stopPlayback() {
    unregisterReceiver(myNoisyAudioStreamReceiver);
}

小实现

public class UseAudioActivity extends AppCompatActivity implements AudioManager.OnAudioFocusChangeListener {
    private Button playAudio,pauseAudio,stopAudio;
    private MediaPlayer mp;
    private AudioManager audioManager;
    private NoisyBroadCastReceiver receiver;
    private boolean isPrepared;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_use_audio);
        playAudio = (Button) findViewById(R.id.play_audio);
        pauseAudio = (Button) findViewById(R.id.pause_audio);
        stopAudio = (Button) findViewById(R.id.stop_audio);
        playAudio.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mp != null && isPrepared){
                    mp.start();
                }else{
                    requestAudioFocus();
                }
            }
        });
        pauseAudio.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mp != null){
                    mp.pause();
                }
            }
        });
        stopAudio.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mp != null){
                    mp.stop();
                    //if you do this,please prepare again
                }
            }
        });
        requestAudioFocus();
        IntentFilter filter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
        receiver = new NoisyBroadCastReceiver();
        this.registerReceiver(receiver,filter);
    }
    public void requestAudioFocus(){
        audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
        if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            // could not get audio focus.
            Toast.makeText(this,"can't play audio now,try later",Toast.LENGTH_SHORT).show();
        }else {
            initMediaPlayer();
        }
    }
    @Override
    public void onAudioFocusChange(int focusChange) {
        Log.e("Audio","is change focus " + focusChange);
        switch (focusChange) {
            case AudioManager.AUDIOFOCUS_GAIN:
                // resume playback
                if (mp == null){
                    initMediaPlayer();
                } else if (!mp.isPlaying()) {
                    mp.start();
                    mp.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 (mp != null) {
                    if (mp.isPlaying()) mp.stop();
                    mp.release();
                    mp = 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 (mp != null) {
                    if (mp.isPlaying()) mp.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 (mp != null) {
                    if (mp.isPlaying()) mp.setVolume(0.1f, 0.1f);
                }
                break;
        }
    }
    public void initMediaPlayer(){
        try {
            mp = new MediaPlayer();
            mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
            AssetFileDescriptor afd = this.getResources().openRawResourceFd(R.raw.neighborhood);
            if (afd == null){
                throw new Exception("res is not found");
            }
            mp.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
            afd.close();
            setPrepareListener();
            mp.prepareAsync();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public void setPrepareListener(){
        if (mp != null){
            mp.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                @Override
                public void onPrepared(MediaPlayer mp) {
                    isPrepared = true;
                }
            });
        }
    }
    public class NoisyBroadCastReceiver extends BroadcastReceiver{
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(AudioManager.ACTION_AUDIO_BECOMING_NOISY)){
                Log.e("Audio","ouch,the headset is out,did you do it?");
                if (mp != null && mp.isPlaying()){
                    mp.pause();
                }
            }
        }
    }
    @Override
    protected void onStop() {
        super.onStop();
        if (mp != null){
            if(mp.isPlaying()) mp.stop();
            mp.release();
            mp = null;
        }
        if(audioManager != null){
            audioManager.abandonAudioFocus(this);
        }
        if(receiver != null) {
            this.unregisterReceiver(receiver);
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容