我创建了一个可以使用MediaBrowserServiceCompat和MediaSessionCompat播放音频的应用程序。按照android开发人员网站上的说明,我已经在MediaSessionCompat.Callback()。onPlay()方法中创建了一个通知,该方法使用MediaStyle提供传输控件,当提供了适当的令牌时,这些控件应该连接到我的媒体会话。应用内的控件可按预期播放和暂停工作,即使关闭并再次打开该应用也是如此。该服务似乎正在按预期运行。
但是,问题是,尽管通知按预期出现,但包含的暂停按钮似乎无法执行任何操作。尽管android开发人员示例表明应该显示一个取消按钮,但事实并非如此。此外,该示例还表明,应通过将通知刷掉而停止该服务,但事实并非如此。
可以说,以下代码段中的任何内容均未正常工作。事实上,除了该通知确实出现。
private NotificationCompat.Builder getMediaNotificationBuilder() {
Intent contentIntent = new Intent(mContext, MainActivity.class);
contentIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pendingContentIntent = PendingIntent.getActivity(mContext, 0, contentIntent, 0);
MediaControllerCompat controller = mMediaSession.getController();
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, "PODCAST");
builder
.setContentTitle("PODCAST")
.setContentText("THIS IS A PLACE HOLDER.")
.setSubText("Still a place holder.")
// Enable launching the player by clicking the notification
.setContentIntent(pendingContentIntent)
// Stop the service when the notification is swiped away
.setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(mContext, PlaybackStateCompat.ACTION_STOP))
// Make the transport controls visible on the lockscreen
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
// Add an app icon and set its accent color
// Be careful about the color
.setSmallIcon(R.drawable.ic_launcher_background)
.setColor(ContextCompat.getColor(mContext, R.color.colorPrimaryDark))
// Add a pause button
.addAction(new NotificationCompat.Action(
R.drawable.ic_pause, "Pause",
MediaButtonReceiver.buildMediaButtonPendingIntent(mContext,
PlaybackStateCompat.ACTION_PAUSE)))
// Take advantage of MediaStyle features
.setStyle(new android.support.v4.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mMediaSession.getSessionToken())
.setShowActionsInCompactView(0)
// Add a cancel button
.setShowCancelButton(true)
.setCancelButtonIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(mContext,
PlaybackStateCompat.ACTION_STOP)));
return builder;
}
然后我继续将此通知传递给
startForground(1, getMediaNotificationBuilder().build())
然后启动服务。
如有必要,我将很乐意分享整个应用程序源代码。我敢肯定,我在这里错过了一些非常简单的事情。
答案 0 :(得分:1)
我怀疑我错过了一些非常简单的东西。为了使MediaBrowserServiceCompat子类对通知控件做出反应,我需要从Service基类重写onStartCommand,并将其中的Intent传递给我的MediaSessionCompat对象。完成此操作后,假定已对其编程,MediaSessionCompat.Callback应该处理该命令。这就是我的MediaBrowserService类内部的代码。
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.e(LOG_TAG, "onStartCommand(): received intent " + intent.getAction() + " with flags " + flags + " and startId " + startId);
MediaButtonReceiver.handleIntent(mMediaSession, intent);
return super.onStartCommand(intent, flags, startId);
}
添加此代码后,您还应该在logcat中看到该方法。万一外面有人仍然缺少某些东西,您至少会知道代码正在响应您的按钮按下。
编辑:
关于通过滑动通知来停止服务,我误解了通知和用户之间的交互。通知可以被用户抹除,但前提是必须先暂停媒体。标准媒体播放器应用的通知控件进一步支持此范例。这是有道理的,因为在收听其他内容的过程中,用户可能会意外滑走控件。
此外,我已决定包括MediaBrowserServiceCompat类的整个源代码,希望这些附加信息能够 提供一些讨论的背景
public class MediaPlaybackService extends MediaBrowserServiceCompat {
private static final String LOG_TAG = "MediaPlaybackService";
private static final String MY_MEDIA_ROOT_ID = "media_root_id";
private static final String MY_EMPTY_MEDIA_ROOT_ID = "empty_root_id";
// Volume levels: Normal and Duck
// VOLUME_DUCK is the volume we set the media player to when we lose audio focus, but are allowed to reduce the volume instead of stopping playback.
public static final float VOLUME_DUCK = 0.2f;
public static final float VOLUME_NORMAL = 1.0f;
private MediaSessionCompat mMediaSession;
private MediaPlayer mMediaPlayer;
// Current local media player state
private PlaybackStateCompat.Builder mStateBuilder;
private int mState = PlaybackStateCompat.STATE_NONE;
private final class MediaSessionCallback extends MediaSessionCompat.Callback implements MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener, AudioManager.OnAudioFocusChangeListener{
private Context mContext;
private AudioManager mAudioManager;
// Declare the "SHIT THAT'S LOUD" intent, any broadcast receiver
// that is connected to it will trigger when the headphones come unplugged
private IntentFilter shitThatsLoudIntentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
private BroadcastReceiver shitThatsLoudBroadcastReceiver = new BroadcastReceiver() {
// TODO: Put me in a separate class
@Override
public void onReceive(Context context, Intent intent) {
Log.d(LOG_TAG, "SHIT THATS LOUD! The headphones have come unplugged!");
}
};
private MediaSessionCallback(Context context) {
super();
mContext = context;
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
initMediaPlayer();
}
private void initMediaPlayer() {
try {
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setDataSource("https://www.blogtalkradio.com/kylekulinski/2018/10/15/the-kyle-kulinski-show.mp3");
mMediaPlayer.setOnPreparedListener (this);
mMediaPlayer.setOnCompletionListener(this);
mMediaPlayer.setOnErrorListener (this);
mMediaPlayer.prepare();
} catch (IOException e) {
Log.e(LOG_TAG, ".initMediaPlayer(): IOException: "+e.toString());
}
}
private void mediaPlay() {
registerReceiver(shitThatsLoudBroadcastReceiver, shitThatsLoudIntentFilter);
if (mAudioManager.requestAudioFocus(getAudioFocusRequest()) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.d(LOG_TAG, "Audio focus request granted.");
mState = PlaybackStateCompat.STATE_PLAYING;
mStateBuilder.setActions(PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_STOP);
mStateBuilder.setState(mState, mMediaPlayer.getCurrentPosition(), 1.0f, SystemClock.elapsedRealtime());
mMediaSession.setPlaybackState(mStateBuilder.build());
mMediaSession.setActive(true);
mMediaPlayer.start();
startService(new Intent(mContext, MediaPlaybackService.class));
startForeground(1, getMediaNotificationBuilder().build());
}
}
private void mediaPause() {
unregisterReceiver(shitThatsLoudBroadcastReceiver);
mState = PlaybackStateCompat.STATE_PAUSED;
mStateBuilder.setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_STOP);
mStateBuilder.setState(mState, mMediaPlayer.getCurrentPosition(), 1.0f, SystemClock.elapsedRealtime());
mMediaSession.setPlaybackState(mStateBuilder.build());
mMediaPlayer.pause();
stopForeground(false);
}
private void releaseResources() {
mMediaSession.setActive(false);
mAudioManager.abandonAudioFocusRequest(getAudioFocusRequest());
unregisterReceiver(shitThatsLoudBroadcastReceiver);
if (mMediaPlayer != null) {
mMediaPlayer.stop();
mMediaPlayer.reset();
mMediaPlayer.release();
mMediaPlayer = null;
}
stopSelf();
stopForeground(true);
}
private NotificationCompat.Builder getMediaNotificationBuilder() {
Intent contentIntent = new Intent(mContext, MainActivity.class);
contentIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pendingContentIntent = PendingIntent.getActivity(mContext, 0, contentIntent, 0);
MediaControllerCompat controller = mMediaSession.getController();
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, "PODCAST");
builder
.setContentTitle("PODCAST")
.setContentText("THIS IS A PLACE HOLDER.")
.setSubText("Still a place holder.")
// Enable launching the player by clicking the notification
.setContentIntent(pendingContentIntent)
// Stop the service when the notification is swiped away
.setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(mContext, PlaybackStateCompat.ACTION_STOP))
// Make the transport controls visible on the lockscreen
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
// Add an app icon and set its accent color
// Be careful about the color
.setSmallIcon(R.drawable.ic_launcher_background)
.setColor(ContextCompat.getColor(mContext, R.color.colorPrimaryDark))
// Add a pause button
.addAction(new NotificationCompat.Action(
R.drawable.ic_pause, "Pause",
MediaButtonReceiver.buildMediaButtonPendingIntent(mContext,
PlaybackStateCompat.ACTION_PLAY_PAUSE)))
// Take advantage of MediaStyle features
.setStyle(new android.support.v4.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mMediaSession.getSessionToken())
.setShowActionsInCompactView(0)
// Add a cancel button
.setShowCancelButton(true)
.setCancelButtonIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(mContext,
PlaybackStateCompat.ACTION_STOP)));
return builder;
}
@Override
public void onPlay() {
super.onPlay();
Log.d(LOG_TAG, "I tried to play music");
mediaPlay();
}
@Override
public void onPause() {
super.onPause();
Log.d(LOG_TAG, "I Tried to pause");
mediaPause();
}
@Override
public void onStop() {
super.onStop();
releaseResources();
}
private AudioFocusRequest getAudioFocusRequest() {
// Request audio focus for playback, this registers the afChangeListener
AudioAttributes attrs = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
AudioFocusRequest audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setOnAudioFocusChangeListener(this)
.setAudioAttributes(attrs)
.build();
return audioFocusRequest;
}
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
Log.d(LOG_TAG, "Audio focus has been restored after it was transiently arrested by and intrusive app. We can now start playing audio normally again.");
mMediaPlayer.setVolume(VOLUME_NORMAL, VOLUME_NORMAL);
mediaPlay();
break;
case AudioManager.AUDIOFOCUS_LOSS:
Log.d(LOG_TAG, "Audio focus was lost flat out. Save what we were doing so we don't forget about it later.");
mediaPause();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
Log.d(LOG_TAG, "Audio focus was lost (Transient) but we might get it back later, still stop and save though.");
mediaPause();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
Log.d(LOG_TAG, "Audio focus was lost but was just need to keep it down instead of stopping.");
mMediaPlayer.setVolume(VOLUME_DUCK, VOLUME_DUCK);
break;
default:
Log.d(LOG_TAG, "Ignoring unsupported audio focus change: "+focusChange);
break;
}
}
@Override
public void onPrepared(MediaPlayer mp) {
Log.d(LOG_TAG, "MediaSessionCallback.onPrepared(): MediaPlayer is prepared!");
// The media player is done preparing. That means we can start playing if we
// have audio focus.
}
@Override
public void onCompletion(MediaPlayer mp) {
}
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
Log.e(LOG_TAG, "Media player error: what=" + what + ", extra=" + extra);
return false; // true indicates we handled the error
}
}
@Override
public void onCreate() {
super.onCreate();
// Create a MediaSessionCompat
mMediaSession = new MediaSessionCompat(this, LOG_TAG);
// Set the session's token so that client activities can communicate with it.
setSessionToken(mMediaSession.getSessionToken());
// MediaSessionCallback() has methods that handle callbacks from a media controller
mMediaSession.setCallback(new MediaSessionCallback(this));
// Enable callbacks from media buttons and transport controls
mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
);
// Set initial PlaybackState with ACTION_PLAY, so that media buttons start the player
mStateBuilder = new PlaybackStateCompat.Builder()
.setActions(
PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE
);
mMediaSession.setPlaybackState(mStateBuilder.build());
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.e(LOG_TAG, "onStartCommand(): received intent " + intent.getAction() + " with flags " + flags + " and startId " + startId);
MediaButtonReceiver.handleIntent(mMediaSession, intent);
return super.onStartCommand(intent, flags, startId);
}
@Nullable
@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
return new BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null);
}
@Override
public void onLoadChildren(@NonNull String parentMediaId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
// Browsing not allowed
if (TextUtils.equals(MY_EMPTY_MEDIA_ROOT_ID, parentMediaId)) {
result.sendResult(null);
return;
}
// TODO: If in the future we decide that we do want this class to handle the podcast metadata
// Then we must adapt what ever data podcastFactory produces into a List of MediaBrowserCompat.MediaItem objects
// The constructor of MediaItem requires that a MediaDescription object be passed to it.
// MediaDescription has a builder class which contains methods for setting Title, Artist, Uri, etc...
// MediaDescription.Builder mMediaDescriptionBuilder = new MediaDescription.Builder();
// mMediaDescriptionBuilder.setTitle(String);
// mMediaDescriptionBuilder.setMediaUri(String);
// MediaDescription mMediaDescription = mMediaDescriptionBuilder.build()
// MediaBrowserCompat.MediaItem mMediaItem =
// new MediaBrowserCompat.MediaItem(
// mMediaDescription,
// int flags -> FLAG_BROWSABLE and/or FLAG_PLAYABLE
// );
// add MediaItem to SomeList
// result.sendResult(SomeList);
}