移动设备最流行的用途之一是通过音乐流服务,下载的播客或任何其他数量的音频源播放音频。 虽然这是一个相当普遍的功能,但是很难实现,为了给用户带来完整的Android体验,需要正确构建许多不同的部分。
在本教程中,您将从Android支持库中了解MediaSessionCompat
,以及如何将其用于为用户创建适当的背景音频服务。
建立
您需要做的第一件事是将Android支持库包含到您的项目中。 这可以通过在依赖项节点下的模块的build.gradle文件中添加以下行来完成。
compile 'com.android.support:support-v13:24.2.1'
同步项目后,创建一个新的Java类。 对于此示例,我将调用类BackgroundAudioService
。 此类将需要扩展MediaBrowserServiceCompat
。 我们还将实现以下接口: MediaPlayer.OnCompletionListener
和AudioManager.OnAudioFocusChangeListener
。
现在,您的MediaBrowserServiceCompat
实现已创建,让我们花点时间更新AndroidManifest.xml,然后返回该类。 在课程的顶部,您将需要请求WAKE_LOCK
权限。
<uses-permission android:name="android.permission.WAKE_LOCK" />
接下来,在application
节点中,使用以下intent-filter
项声明新服务。 这些将使您的服务能够拦截设备的控制按钮,耳机事件和媒体浏览,例如Android Auto(尽管本教程不会使用Android Auto进行任何操作,但MediaBrowserServiceCompat
仍需要一些基本支持)。
<service android:name=".BackgroundAudioService">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.media.AUDIO_BECOMING_NOISY" />
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
最后,您需要从Android支持库中声明MediaButtonReceiver
的使用。 这将允许您在运行KitKat及更早版本的设备上拦截媒体控制按钮的交互作用和耳机事件。
<receiver android:name="android.support.v4.media.session.MediaButtonReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.media.AUDIO_BECOMING_NOISY" />
</intent-filter>
</receiver>
现在,您的AndroidManifest.xml文件已完成,您可以将其关闭。 我们还将创建另一个名为MediaStyleHelper
类,该类由Google开发人员倡导者Ian Lake编写,以清理媒体样式通知的创建。
public class MediaStyleHelper {
/**
* Build a notification using the information from the given media session. Makes heavy use
* of {@link MediaMetadataCompat#getDescription()} to extract the appropriate information.
* @param context Context used to construct the notification.
* @param mediaSession Media session to get information.
* @return A pre-built notification with information from the given media session.
*/
public static NotificationCompat.Builder from(
Context context, MediaSessionCompat mediaSession) {
MediaControllerCompat controller = mediaSession.getController();
MediaMetadataCompat mediaMetadata = controller.getMetadata();
MediaDescriptionCompat description = mediaMetadata.getDescription();
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
builder
.setContentTitle(description.getTitle())
.setContentText(description.getSubtitle())
.setSubText(description.getDescription())
.setLargeIcon(description.getIconBitmap())
.setContentIntent(controller.getSessionActivity())
.setDeleteIntent(
MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_STOP))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
return builder;
}
}
创建完成后,继续并关闭文件。 在下一节中,我们将重点介绍后台音频服务。
建立后台音频服务
现在是时候深入探讨创建媒体应用程序的核心了。 首先需要为该示例应用程序声明一些成员变量:用于实际回放的MediaPlayer
,以及将管理元数据和回放控件/状态的MediaSessionCompat
对象。
private MediaPlayer mMediaPlayer;
private MediaSessionCompat mMediaSessionCompat;
另外,您将需要一个BroadcastReceiver
来监听耳机状态的变化。 为简单MediaPlayer
,此接收器将暂停MediaPlayer
(如果正在播放)。
private BroadcastReceiver mNoisyReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if( mMediaPlayer != null && mMediaPlayer.isPlaying() ) {
mMediaPlayer.pause();
}
}
};
对于最后一个成员变量,您将创建一个MediaSessionCompat.Callback
对象,该对象用于在发生媒体会话操作时处理播放状态。
private MediaSessionCompat.Callback mMediaSessionCallback = new MediaSessionCompat.Callback() {
@Override
public void onPlay() {
super.onPlay();
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
super.onPlayFromMediaId(mediaId, extras);
}
};
我们将在本教程的后面部分重新介绍上述每种方法,因为它们将用于驱动媒体应用程序中的操作。
我们还需要声明两种方法,尽管就本教程而言,它们不需要执行任何操作: onGetRoot()
和onLoadChildren()
。 您可以将以下代码用作默认值。
@Nullable
@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
if(TextUtils.equals(clientPackageName, getPackageName())) {
return new BrowserRoot(getString(R.string.app_name), null);
}
return null;
}
//Not important for general audio service, required for class
@Override
public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
result.sendResult(null);
}
最后,您需要覆盖onStartCommand()
方法,该方法是Service
的入口点。 此方法将采用传递给Service
的Intent并将其发送给MediaButtonReceiver
类。
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
MediaButtonReceiver.handleIntent(mMediaSessionCompat, intent);
return super.onStartCommand(intent, flags, startId);
}
初始化所有事物
现在已经创建了基本成员变量,现在该初始化所有内容了。 我们将通过在onCreate()
调用各种辅助方法来实现此目的。
@Override
public void onCreate() {
super.onCreate();
initMediaPlayer();
initMediaSession();
initNoisyReceiver();
}
第一个方法initMediaPlayer()
将初始化我们在类顶部创建的MediaPlayer
对象,请求部分唤醒锁定(这就是为什么我们需要AndroidManifest.xml中的该权限),并设置播放器的音量。
private void initMediaPlayer() {
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setVolume(1.0f, 1.0f);
}
下一个方法initMediaSession()
,是我们初始化MediaSessionCompat
对象并将其连接到媒体按钮和控制方法的位置,该方法允许我们处理播放和用户输入。 此方法首先创建一个ComponentName
对象,该对象指向Android支持库的MediaButtonReceiver
类,然后使用该对象创建新的MediaSessionCompat
。 然后,我们将之前创建的MediaSession.Callback
对象传递给它,并设置接收媒体按钮输入和控制信号所需的标志。 接下来,我们创建一个新的Intent
来处理Lollipop之前的设备上的媒体按钮输入,并为我们的服务设置媒体会话令牌。
private void initMediaSession() {
ComponentName mediaButtonReceiver = new ComponentName(getApplicationContext(), MediaButtonReceiver.class);
mMediaSessionCompat = new MediaSessionCompat(getApplicationContext(), "Tag", mediaButtonReceiver, null);
mMediaSessionCompat.setCallback(mMediaSessionCallback);
mMediaSessionCompat.setFlags( MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS );
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
mediaButtonIntent.setClass(this, MediaButtonReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, 0);
mMediaSessionCompat.setMediaButtonReceiver(pendingIntent);
setSessionToken(mMediaSessionCompat.getSessionToken());
}
最后,我们将在类的顶部注册创建的BroadcastReceiver
,以便我们可以侦听耳机更改事件。
private void initNoisyReceiver() {
//Handles headphones coming unplugged. cannot be done through a manifest receiver
IntentFilter filter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
registerReceiver(mNoisyReceiver, filter);
}
处理音频焦点
既然您已经完成了BroadcastReceiver
, MediaSessionCompat
和MediaPlayer
对象的初始化,现在该考虑处理音频焦点了。
尽管我们可能认为自己的音频应用程序目前是最重要的,但设备上的其他应用程序将竞争自己发出的声音,例如电子邮件通知或手机游戏。 为了处理各种情况,Android系统使用音频焦点来确定应如何处理音频。
我们要处理的第一种情况是开始播放并尝试接收设备的焦点。 在MediaSessionCompat.Callback
对象中,进入onPlay()
方法并添加以下条件检查。
@Override
public void onPlay() {
super.onPlay();
if( !successfullyRetrievedAudioFocus() ) {
return;
}
}
上面的代码将调用一个辅助方法,该方法尝试获取焦点,如果不能找到焦点,它将简单地返回。 在真实的应用中,您希望更优雅地处理音频播放失败的情况。 successfullyRetrievedAudioFocus()
将获取对系统AudioManager
的引用,并尝试请求流音频的音频焦点。 然后,它将返回一个boolean
表示请求是否成功。
private boolean successfullyRetrievedAudioFocus() {
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int result = audioManager.requestAudioFocus(this,
AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
return result == AudioManager.AUDIOFOCUS_GAIN;
}
你会发现,我们也通过this
入requestAudioFocus()
方法,该关联OnAudioFocusChangeListener
我们的服务。 为了成为设备应用程序生态系统中的“好公民”,您需要听几种不同的状态。
-
AudioManager.AUDIOFOCUS_LOSS
:当另一个应用程序请求音频焦点时,会发生这种情况。 发生这种情况时,您应该停止在应用中播放音频。 -
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
:当另一个应用程序要播放音频时进入此状态,但是它只预计需要短时间聚焦。 您可以使用此状态暂停音频播放。 -
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
:当请求音频焦点但抛出“ can duck”状态时,表示可以继续播放,但应将音量调低一点。 当设备播放通知声音时,可能会发生这种情况。
-
AudioManager.AUDIOFOCUS_GAIN
:我们将讨论的最终状态是AUDIOFOCUS_GAIN
。 这是可插入式音频播放完成后的状态,您的应用可以恢复之前的级别。
简化的onAudioFocusChange()
回调可能看起来像这样:
@Override
public void onAudioFocusChange(int focusChange) {
switch( focusChange ) {
case AudioManager.AUDIOFOCUS_LOSS: {
if( mMediaPlayer.isPlaying() ) {
mMediaPlayer.stop();
}
break;
}
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: {
mMediaPlayer.pause();
break;
}
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: {
if( mMediaPlayer != null ) {
mMediaPlayer.setVolume(0.3f, 0.3f);
}
break;
}
case AudioManager.AUDIOFOCUS_GAIN: {
if( mMediaPlayer != null ) {
if( !mMediaPlayer.isPlaying() ) {
mMediaPlayer.start();
}
mMediaPlayer.setVolume(1.0f, 1.0f);
}
break;
}
}
}
了解MediaSessionCompat.Callback
现在,您已经为Service
拥有了一个通用结构,是时候深入研究MediaSessionCompat.Callback
。 在上一节中,您向onPlay()
添加了一些代码来检查是否已授予音频焦点。 在条件语句下,您需要将MediaSessionCompat
对象设置为active,使其状态为STATE_PLAYING
,并分配必要的适当操作,以在棒棒糖锁屏之前的控件,电话和Android Wear通知上创建暂停按钮。
@Override
public void onPlay() {
super.onPlay();
if( !successfullyRetrievedAudioFocus() ) {
return;
}
mMediaSessionCompat.setActive(true);
setMediaPlaybackState(PlaybackStateCompat.STATE_PLAYING);
...
}
上面的setMediaPlaybackState()
方法是一个辅助方法,该方法创建一个PlaybackStateCompat.Builder
对象并为其提供适当的操作和状态,然后构建一个PlaybackStateCompat
并将其与MediaSessionCompat
对象相关联。
private void setMediaPlaybackState(int state) {
PlaybackStateCompat.Builder playbackstateBuilder = new PlaybackStateCompat.Builder();
if( state == PlaybackStateCompat.STATE_PLAYING ) {
playbackstateBuilder.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PAUSE);
} else {
playbackstateBuilder.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY);
}
playbackstateBuilder.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 0);
mMediaSessionCompat.setPlaybackState(playbackstateBuilder.build());
}
这是要注意的是,您将需要两个重要ACTION_PLAY_PAUSE
,要么ACTION_PAUSE
或ACTION_PLAY
为了得到Android Wear上的适当控制标志在你的行动。
返回onPlay()
,您将想要通过使用我们之前定义的MediaStyleHelper
类来显示与MediaSessionCompat
对象关联的播放通知,然后显示该通知。
private void showPlayingNotification() {
NotificationCompat.Builder builder = MediaStyleHelper.from(BackgroundAudioService.this, mMediaSessionCompat);
if( builder == null ) {
return;
}
builder.addAction(new NotificationCompat.Action(android.R.drawable.ic_media_pause, "Pause", MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_PLAY_PAUSE)));
builder.setStyle(new NotificationCompat.MediaStyle().setShowActionsInCompactView(0).setMediaSession(mMediaSessionCompat.getSessionToken()));
builder.setSmallIcon(R.mipmap.ic_launcher);
NotificationManagerCompat.from(BackgroundAudioService.this).notify(1, builder.build());
}
最后,您将在onPlay()
的末尾启动MediaPlayer
。
@Override
public void onPlay() {
super.onPlay();
...
showPlayingNotification();
mMediaPlayer.start();
}
当回调收到暂停命令时,将调用onPause()
。 在这里,您将暂停MediaPlayer
,将状态设置为STATE_PAUSED
,并显示已暂停的通知。
@Override
public void onPause() {
super.onPause();
if( mMediaPlayer.isPlaying() ) {
mMediaPlayer.pause();
setMediaPlaybackState(PlaybackStateCompat.STATE_PAUSED);
showPausedNotification();
}
}
我们的showPausedNotification()
辅助方法将类似于showPlayNotification()
方法。
private void showPausedNotification() {
NotificationCompat.Builder builder = MediaStyleHelper.from(this, mMediaSessionCompat);
if( builder == null ) {
return;
}
builder.addAction(new NotificationCompat.Action(android.R.drawable.ic_media_play, "Play", MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_PLAY_PAUSE)));
builder.setStyle(new NotificationCompat.MediaStyle().setShowActionsInCompactView(0).setMediaSession(mMediaSessionCompat.getSessionToken()));
builder.setSmallIcon(R.mipmap.ic_launcher);
NotificationManagerCompat.from(this).notify(1, builder.build());
}
我们将讨论的回调中的下一个方法onPlayFromMediaId()
,将String
和Bundle
作为参数。 这是可用于在应用程序中更改音轨/内容的回调方法。
对于本教程,我们将简单地接受原始资源ID并尝试播放它,然后重新初始化会话的元数据。 由于允许将Bundle
传递给此方法,因此可以使用它自定义媒体播放的其他方面,例如为轨道设置自定义背景声音。
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
super.onPlayFromMediaId(mediaId, extras);
try {
AssetFileDescriptor afd = getResources().openRawResourceFd(Integer.valueOf(mediaId));
if( afd == null ) {
return;
}
try {
mMediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
} catch( IllegalStateException e ) {
mMediaPlayer.release();
initMediaPlayer();
mMediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
}
afd.close();
initMediaSessionMetadata();
} catch (IOException e) {
return;
}
try {
mMediaPlayer.prepare();
} catch (IOException e) {}
//Work with extras here if you want
}
既然我们已经讨论了将在应用程序中使用的此回调中的两个主要方法,那么重要的是要知道,还有其他可选方法可用于定制服务。 一些方法包括onSeekTo()
和onCommand()
,其中onSeekTo()
允许您更改内容的播放位置, onCommand()
可以接受表示命令类型的String
,有关命令的更多信息的Bundle
和ResultReceiver
回调,将允许您将自定义命令发送到Service
。
@Override
public void onCommand(String command, Bundle extras, ResultReceiver cb) {
super.onCommand(command, extras, cb);
if( COMMAND_EXAMPLE.equalsIgnoreCase(command) ) {
//Custom command here
}
}
@Override
public void onSeekTo(long pos) {
super.onSeekTo(pos);
}
撕下
音频文件完成后,我们将要决定下一步将要执行的操作。 虽然您可能想在应用程序中播放下一首曲目,但我们将使事情变得简单并发布MediaPlayer
。
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
if( mMediaPlayer != null ) {
mMediaPlayer.release();
}
}
最后,我们将要在Service
的onDestroy()
方法中做一些事情。 首先,获取对系统服务的AudioManager
的引用,并使用我们的AudioFocusChangeListener
作为参数调用abandonAudioFocus()
,这将通知设备上的其他应用程序您正在放弃音频焦点。 接下来,取消注册为侦听耳机更改而设置的BroadcastReceiver
,并释放MediaSessionCompat
对象。 最后,您将要取消播放控制通知。
@Override
public void onDestroy() {
super.onDestroy();
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
audioManager.abandonAudioFocus(this);
unregisterReceiver(mNoisyReceiver);
mMediaSessionCompat.release();
NotificationManagerCompat.from(this).cancel(1);
}
此时,您应该具有使用MediaSessionCompat
的正常工作的基本背景音频Service
以便跨设备进行播放控制。 尽管创建服务已经涉及很多工作,但是您应该能够控制应用程序的播放,通知,棒棒糖之前的设备上的锁屏控件(棒棒糖及更高版本将在锁屏上使用通知), Service
启动后,即可从外围设备(例如Android Wear)启动。
从活动开始和控制内容
尽管大多数控件都是自动的,但您仍然需要做一些工作才能通过应用内控件启动和控制媒体会话。 至少,您需要在应用程序中创建一个MediaBrowserCompat.ConnectionCallback
, MediaControllerCompat.Callback
, MediaBrowserCompat
和MediaControllerCompat
对象。
MediaControllerCompat.Callback
将具有一个名为onPlaybackStateChanged()
的方法,该方法接收播放状态的更改,并可用于使UI保持同步。
private MediaControllerCompat.Callback mMediaControllerCompatCallback = new MediaControllerCompat.Callback() {
@Override
public void onPlaybackStateChanged(PlaybackStateCompat state) {
super.onPlaybackStateChanged(state);
if( state == null ) {
return;
}
switch( state.getState() ) {
case PlaybackStateCompat.STATE_PLAYING: {
mCurrentState = STATE_PLAYING;
break;
}
case PlaybackStateCompat.STATE_PAUSED: {
mCurrentState = STATE_PAUSED;
break;
}
}
}
};
MediaBrowserCompat.ConnectionCallback
具有onConnected()
方法,当创建并连接新的MediaBrowserCompat
对象时将调用该方法。 您可以使用此方法初始化MediaControllerCompat
对象,将其链接到MediaControllerCompat.Callback
,并将其与Service
MediaSessionCompat
关联。 完成后,您可以从此方法开始播放音频。
private MediaBrowserCompat.ConnectionCallback mMediaBrowserCompatConnectionCallback = new MediaBrowserCompat.ConnectionCallback() {
@Override
public void onConnected() {
super.onConnected();
try {
mMediaControllerCompat = new MediaControllerCompat(MainActivity.this, mMediaBrowserCompat.getSessionToken());
mMediaControllerCompat.registerCallback(mMediaControllerCompatCallback);
setSupportMediaController(mMediaControllerCompat);
getSupportMediaController().getTransportControls().playFromMediaId(String.valueOf(R.raw.warner_tautz_off_broadway), null);
} catch( RemoteException e ) {
}
}
};
您会注意到,以上代码片段使用getSupportMediaController().getTransportControls()
与媒体会话进行通信。 使用相同的技术,可以在音频服务的MediaSessionCompat.Callback
对象中调用onPlay()
和onPause()
。
if( mCurrentState == STATE_PAUSED ) {
getSupportMediaController().getTransportControls().play();
mCurrentState = STATE_PLAYING;
} else {
if( getSupportMediaController().getPlaybackState().getState() == PlaybackStateCompat.STATE_PLAYING ) {
getSupportMediaController().getTransportControls().pause();
}
mCurrentState = STATE_PAUSED;
}
完成音频播放后,您可以暂停音频服务并断开MediaBrowserCompat
对象的连接,在销毁此Activity
情况下,我们将在本教程中这样做。
@Override
protected void onDestroy() {
super.onDestroy();
if( getSupportMediaController().getPlaybackState().getState() == PlaybackStateCompat.STATE_PLAYING ) {
getSupportMediaController().getTransportControls().pause();
}
mMediaBrowserCompat.disconnect();
}
包起来
ew! 如您所见,正确地创建和使用背景音频服务涉及许多活动。
在本教程中,您已经创建了一个服务,该服务播放一个简单的音频文件,侦听音频焦点的变化,并链接到MediaSessionCompat
以在Android设备(包括手机和Android Wear)上提供通用播放控制。 如果您在学习本教程时遇到障碍,我强烈建议您在Envato Tuts +的GitHub上查看相关的Android项目代码 。
翻译自: https://code.tutsplus.com/tutorials/background-audio-in-android-with-mediasessioncompat--cms-27030