Android中带有MediaSessionCompat的背景音频

本文档详细介绍了如何使用Android支持库中的MediaSessionCompat创建后台音频服务,包括初始化服务、处理音频焦点、控制播放以及在不同设备上实现跨设备播放控制。通过对音频焦点、MediaSessionCompat.Callback的深入理解,实现完整的背景音频播放体验。
摘要由CSDN通过智能技术生成

移动设备最流行的用途之一是通过音乐流服务,下载的播客或任何其他数量的音频源播放音频。 虽然这是一个相当普遍的功能,但是很难实现,为了给用户带来完整的Android体验,需要正确构建许多不同的部分。

在本教程中,您将从Android支持库中了解MediaSessionCompat ,以及如何将其用于为用户创建适当的背景音频服务。

建立

您需要做的第一件事是将Android支持库包含到您的项目中。 这可以通过在依赖项节点下的模块的build.gradle文件中添加以下行来完成。

compile 'com.android.support:support-v13:24.2.1'

同步项目后,创建一个新的Java类。 对于此示例,我将调用类BackgroundAudioService 。 此类将需要扩展MediaBrowserServiceCompat 。 我们还将实现以下接口: MediaPlayer.OnCompletionListenerAudioManager.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);
}

处理音频焦点

既然您已经完成了BroadcastReceiverMediaSessionCompatMediaPlayer对象的初始化,现在该考虑处理音频焦点了。

尽管我们可能认为自己的音频应用程序目前是最重要的,但设备上的其他应用程序将竞争自己发出的声音,例如电子邮件通知或手机游戏。 为了处理各种情况,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;
}

你会发现,我们也通过thisrequestAudioFocus()方法,该关联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_PAUSEACTION_PLAY为了得到Android Wear上的适当控制标志在你的行动。

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();
}
Android Nougat设备上的媒体控制通知

当回调收到暂停命令时,将调用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() ,将StringBundle作为参数。 这是可用于在应用程序中更改音轨/内容的回调方法。

对于本教程,我们将简单地接受原始资源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 ,有关命令的更多信息的BundleResultReceiver回调,将允许您将自定义命令发送到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();
    }
}

最后,我们将要在ServiceonDestroy()方法中做一些事情。 首先,获取对系统服务的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)启动。

Android Kit Kat上的媒体锁定屏幕控件

从活动开始和控制内容

尽管大多数控件都是自动的,但您仍然需要做一些工作才能通过应用内控件启动和控制媒体会话。 至少,您需要在应用程序中创建一个MediaBrowserCompat.ConnectionCallbackMediaControllerCompat.CallbackMediaBrowserCompatMediaControllerCompat对象。

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值