如何为基于ExoPlayer的音乐播放器服务添加自定义媒体通知操作?
如何为基于ExoPlayer的音乐播放器服务添加自定义媒体通知操作?
我看你已经在基于Java的Material 3音乐播放器里用了Media3(ExoPlayer)框架,接下来结合你现有的代码,一步步给你梳理怎么把自定义操作按钮加到媒体通知里:
1. 先在MediaSession回调中注册自定义命令
要让系统识别你的自定义操作,首先得在MediaSession.Callback里声明自定义命令,同时把这些命令和对应的按钮布局传给媒体控制器。这样通知里的自定义按钮才能和逻辑绑定:
public class CustomCallback implements MediaSession.Callback { // 定义你的自定义命令,action字符串要唯一,后续触发要对应 private final SessionCommand SHUFFLE_COMMAND = new SessionCommand("com.xmusic.action.SHUFFLE", Bundle.EMPTY); // 定义自定义按钮:图标、显示名称、绑定的命令、位置槽位 private final CommandButton SHUFFLE_BUTTON = new CommandButton.Builder(R.drawable.ic_shuffle) .setDisplayName("随机播放") .setSessionCommand(SHUFFLE_COMMAND) .setSlots(CommandButton.SLOT_FORWARD_SECONDARY) // 按钮在通知的次要位置 .build(); private final List<CommandButton> CUSTOM_LAYOUT = ImmutableList.of(SHUFFLE_BUTTON); // 声明可用的命令集合:系统默认命令 + 自定义命令 private final SessionCommands AVAILABLE_COMMANDS = SessionCommands.EMPTY.buildUpon() .addAll(Player.COMMANDS) // 包含播放/暂停/上一曲/下一曲等默认命令 .add(SHUFFLE_COMMAND) .build(); @Override public ConnectionResult onConnect(MediaSession mediaSession, ControllerInfo controllerInfo) { // 连接时告诉控制器我们支持的自定义命令和布局 return ConnectionResult.accept(AVAILABLE_COMMANDS, Player.COMMANDS) .setCustomLayout(CUSTOM_LAYOUT); } @Override public void onPostConnect(MediaSession mediaSession, ControllerInfo controllerInfo) { // 连接完成后再次同步自定义布局和命令,确保兼容性 mediaSession.setAvailableCommands(controllerInfo, AVAILABLE_COMMANDS, Player.COMMANDS); mediaSession.setCustomLayout(controllerInfo, CUSTOM_LAYOUT); } @Override public ListenableFuture<SessionResult> onCustomCommand(MediaSession mediaSession, ControllerInfo controllerInfo, SessionCommand sessionCommand, Bundle bundle) { // 处理自定义按钮的点击逻辑 if ("com.xmusic.action.SHUFFLE".equals(sessionCommand.customAction)) { // 这里写你的随机播放切换逻辑,比如: boolean isShuffling = mediaSession.getPlayer().getShuffleMode() == Player.SHUFFLE_MODE_ALL; mediaSession.getPlayer().setShuffleMode(isShuffling ? Player.SHUFFLE_MODE_NONE : Player.SHUFFLE_MODE_ALL); return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS)); } // 未知命令返回失败 return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_ERROR_UNKNOWN)); } }
重点:自定义命令的action字符串(比如com.xmusic.action.SHUFFLE)要全局唯一,点击通知按钮时会通过这个字符串匹配到对应的处理逻辑
2. 自定义NotificationProvider添加按钮到通知栏
你已经继承了DefaultMediaNotificationProvider,现在重写addNotificationActions方法,把自定义按钮加到通知Builder里:
public class CustomNotificationProvider extends DefaultMediaNotificationProvider { private final Context mContext; public CustomNotificationProvider(Context context) { super(context); mContext = context; } @Override public int[] addNotificationActions(MediaSession mediaSession, ImmutableList<CommandButton> mediaButtons, NotificationCompat.Builder builder, MediaNotification.ActionFactory actionFactory) { // 先添加系统默认的播放控制按钮(比如播放/暂停/上一曲/下一曲) int[] defaultActionIndices = super.addNotificationActions(mediaSession, mediaButtons, builder, actionFactory); // 添加我们的自定义随机播放按钮 CommandButton shuffleButton = new CommandButton.Builder(R.drawable.ic_shuffle) .setDisplayName("随机播放") .setSessionCommand(new SessionCommand("com.xmusic.action.SHUFFLE", Bundle.EMPTY)) .setSlots(CommandButton.SLOT_BACK_SECONDARY) .build(); // 用ActionFactory创建可触发自定义命令的Notification Action NotificationCompat.Action shuffleAction = actionFactory.createCustomAction( mediaSession, IconCompat.createWithResource(mContext, R.drawable.ic_shuffle), "随机播放", "com.xmusic.action.SHUFFLE", Bundle.EMPTY ); builder.addAction(shuffleAction); // 返回操作按钮的索引数组(如果需要调整顺序可以修改) return defaultActionIndices; } }
注意:如果不想保留系统默认按钮,可以跳过super.addNotificationActions这一步,直接添加自定义按钮
3. 在Service初始化时完成关联配置
最后在你的音乐播放器Service的onCreate方法里,把这些组件串联起来,确保MediaSession、NotificationProvider、前台服务都正确初始化:
@Override public void onCreate() { super.onCreate(); fallbackUri = Uri.parse("android.resource://" + this.getPackageName() + "/" + resId); // 1. 初始化ExoPlayer到后台线程 HandlerThread handlerThread = new HandlerThread("ExoPlayerThread"); handlerThread.start(); Looper backgroundLooper = handlerThread.getLooper(); Handler exoPlayerHandler = new Handler(backgroundLooper); DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this) .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON); Player player = new ExoPlayer.Builder(this, renderersFactory) .setLooper(backgroundLooper) .build(); // 配置音频属性 androidx.media3.common.AudioAttributes audioAttrs = new androidx.media3.common.AudioAttributes.Builder() .setUsage(C.USAGE_MEDIA) .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) .build(); exoPlayerHandler.post(() -> player.setAudioAttributes(audioAttrs, true)); // 2. 初始化MediaSession并绑定自定义Callback boolean isBuilt = false; if (!isBuilt) { setupMediaSession(player); isBuilt = true; } // 3. 配置通知相关 boolean isNotifDead = false; createNotificationChannel(); // 设置自定义通知提供者 setMediaNotificationProvider(new CustomNotificationProvider(this)); // 4. 启动前台服务(适配Android 13+的前台服务类型) Notification initialNotification = buildNotification("XMusic", "No song is playing", ""); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { startForeground(NOTIFICATION_ID, initialNotification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK); } else { startForeground(NOTIFICATION_ID, initialNotification); } // 其他初始化:音频焦点、AudioManager等(你已有的代码保留即可) AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); setupAudioFocusRequest(); } // 初始化MediaSession的方法 private void setupMediaSession(Player player) { MediaSession mediaSession = null; if (mediaSession != null) return; mediaSession = new androidx.media3.session.MediaSession.Builder(this, player) .setId("XMusicMediaSessionPrivate") .setCallback(new CustomCallback()) // 绑定我们的自定义回调 .build(); } // 基础通知构建方法(你已有的代码可以保留,注意适配Media3的API) private Notification buildNotification(String title, String artist, String cover) { MediaSession mediaSession = null; if (mediaSession == null) setupMediaSession(player); MediaStyleNotificationHelper.MediaStyle mediaStyle = new MediaStyleNotificationHelper.MediaStyle(mediaSession) .setShowCancelButton(true); Intent resumeIntent = getPackageManager().getLaunchIntentForPackage(getPackageName()); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, resumeIntent, PendingIntent.FLAG_IMMUTABLE); // 专辑封面Bitmap,确保已正确加载 Bitmap current = null; return new NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.mipmap.ic_launcher_foreground) .setContentTitle(title) .setContentText(artist) .setLargeIcon(current) .setStyle(mediaStyle) .setOngoing(true) .setContentIntent(contentIntent) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .build(); } // 创建通知通道(Android 8.0+必须) private void createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel( "XMusic_CHANNEL", "XMusic Playback", NotificationManager.IMPORTANCE_LOW ); channel.setDescription("Music playback controls"); NotificationManager notificationManager = getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } }
几个关键注意点
- SDK版本适配:Android 13(API 33)及以上需要指定
FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK类型,否则会崩溃。 - PendingIntent的Flag:Android 12(API 31)及以上必须用
FLAG_IMMUTABLE或FLAG_MUTABLE,这里用FLAG_IMMUTABLE即可,因为通知操作不需要修改Intent。 - 音频焦点:确保你已经正确申请和处理音频焦点(你代码里的
setupAudioFocusRequest),这是媒体播放器的标准流程。 - 资源命名:自定义按钮的图标资源(比如
ic_shuffle)要确保在drawable目录下存在,避免资源找不到的崩溃。
这样调整后,你的媒体通知里就会出现自定义的随机播放按钮,点击后会触发CustomCallback里的逻辑,完成对应的功能啦!




