news 2026/9/9 19:00:19

Android 13/14 媒体按键失效?MediaSession 适配指南与避坑实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android 13/14 媒体按键失效?MediaSession 适配指南与避坑实践

最近在做一个音乐播放器项目,把 targetSdk 升到 34 之后,用户反馈说蓝牙耳机和有线耳机的播放/暂停键都没反应了。我一开始以为是蓝牙协议栈或者耳机兼容性的问题,排查了一圈才发现根本不是——是 API 33 之后媒体按键的分发机制变了,MediaSession 压根没拿到按键事件。这个问题在 Android 13/14 上非常典型,尤其是从 targetSdk 32 以下升上来、或者新建项目直接跑在 API 33+ 设备上的场景,十有八九会踩中。

这篇文章不聊虚的,直接把我踩过的坑、验证过的方案、以及最后能稳定收到媒体按键的写法全部整理出来。整个排查过程涉及 MediaSession 的注册、通知权限、前台服务类型、PlaybackState 状态、音频焦点等几个关键环节,任何一个环节漏了,耳机按键都可能失灵。建议开发播放器、收音机、播客类 App 的同学仔细看一下,尤其是那些还在用旧版 registerMediaButtonEventReceiver 方案的老项目。

1. 问题本质:API 33 前后媒体按键处理机制到底改了什么

1.1 从“谁都能收”到“系统说了算”

在 Android 8.0(API 26)之前,媒体按键事件通过广播分发,应用可以注册MediaButtonReceiver来接收耳机按键,只要你的 receiver 在清单里声明了对应的 intent-filter,按键按下时系统广播就会发给你。但从 Android 8.0 开始,系统把媒体按键集中到了MediaSession上,不再向普通广播接收器分发按键事件。

到了 Android 13(API 33),这个趋势进一步加强。系统会只把媒体按键事件分发给“当前活跃的 MediaSession”,而不是所有注册过的 MediaSession。而且这个“活跃”是有明确条件的:必须是setActive(true)状态,必须设置了有效的PlaybackState(并且 state 不能是STATE_NONE),同时还需要持有音频焦点。最容易被忽略的一点是——从 API 33 开始,如果应用没有显示媒体通知(也就是没有MediaStyle通知),系统在部分场景下会直接不把这个会话当作可接收媒体按键的候选对象。

这就是很多 App 升级后按键失灵的根源:老代码里可能只调用了setActive(true),但没正确处理通知权限;或者通知权限被用户拒绝后,媒体通知根本发不出来,系统就认为这个 App 没有在播放媒体。

1.2 到底是 Permission 问题还是 Session 问题

我那次排查的第一个误区就是:只盯着权限看。API 33 新增了POST_NOTIFICATIONS运行时权限,很多人以为是这个权限没申请导致按键失效。但实际上这个权限影响的是“媒体通知能不能显示出来”,而不是“MediaSession 能不能注册”。

实际现象是这样:如果POST_NOTIFICATIONS权限被拒绝,应用仍然可以创建 MediaSession、可以调setActive(true),也可以正常播放音频。但在 Android 13 以上的部分版本和 ROM 上,系统 UI 的媒体控制中心(就是通知栏下拉后那个媒体卡片)不会显示这个会话,耳机的媒体按键也可能会被分发给其他 Session,甚至直接丢弃。

所以正确的理解是:通知权限是媒体按键链路上的一环,但不是唯一环节。真正决定按键能不能到你这边的,是系统端MediaSessionManager对“当前主媒体会话”的判定。这个判定涉及下面几个要素:

  • Session 是否setActive(true)
  • Session 是否设置了合法的 PlaybackState(尤其是 state 和 actions)
  • App 是否在前台或处于允许的后台运行状态
  • 是否有可见的媒体通知(API 33+ 较为关键)
  • 是否成功请求并持有音频焦点

任何一个环节出问题,MediaButton 事件就不会进入你的onMediaButtonEvent回调。

1.3 和旧代码的兼容性对比

我在 Stack Overflow 和各类技术社区看到了大量同类问题,基本上可以分为三类:

第一类是项目之前用了AudioManager.registerMediaButtonEventReceiver()或者MediaSessionManager的旧接口,这些接口在新系统上要么被废弃,要么行为不一致,导致按键事件丢失。第二类是项目引入了多个 MediaSession 实例(比如播放器一个、录音一个、甚至广告 SDK 一个),系统不知道把按键交给谁。第三类是播放器用了 Media3/ExoPlayer,它的内部会创建自己的 MediaSession,结果开发者自己又手动创建了一个,两个 Session 互相打架。

我自己的项目属于第一类加第三类的混合体。旧的逻辑里注册了一个MediaButtonReceiver,同时 ExoPlayer 内部又维护了一个 Session,升级之后新老机制互相干扰,最终表现就是什么都没收到。

所以在真正动手改代码之前,我建议你先做一件事:好好梳理一下当前项目里到底有哪几个地方能创建 MediaSession,有没有重复注册,有没有多个播放器同时争抢。

2. 核心细节解析与实操要点

2.1 检查你的 Manifest 有没有漏掉必要声明

如果你使用的是androidx.media:media:1.6.0或更高版本,并且 targetSdk 是 33 以上,这里有几个容易漏掉的清单配置。

第一个是前台服务类型。API 34(Android 14)对前台服务类型要求非常严格,播放音频必须声明foregroundServiceType="mediaPlayback"。API 33 虽然没有强制,但从 34 开始如果不声明,启动前台服务直接抛ForegroundServiceStartNotAllowedExceptionMissingForegroundServiceTypeException

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.WAKE_LOCK" />

Service 声明部分要这样加:

<service android:name=".PlaybackService" android:exported="true" android:foregroundServiceType="mediaPlayback"> <intent-filter> <action android:name="androidx.media3.session.MediaSessionService" /> <action android:name="android.media.browse.MediaBrowserService" /> </intent-filter> </service>

注意:如果你的 Service 要能被系统媒体中心发现,android:exported必须为true。很多开发者在适配的时候为了安全把exported设成false,结果媒体中心里永远看不到这个会话,按键自然也不会过来。

另外,如果你使用MediaSessionCompat(AndroidX 版本),还需要在 Manifest 里声明MediaButtonReceiver,但注意这个 Receiver 并不是用来接收按键的,而是用来在应用被系统启动时把Intent转交给MediaSessionCompat处理的。我见过不少项目在升级过程中把这个 Receiver 删了,导致按耳机键时应用能被拉起,但按键动作不生效。

2.2 通知权限的动态申请不能省略

从 API 33 开始,POST_NOTIFICATIONS是运行时权限,必须在代码里动态申请。这一步如果漏了,用户安装后默认是“拒绝”状态,媒体通知无法展示。

但这里有个坑:很多播放器是在用户点击播放按钮时才弹出权限弹窗,用户如果点了一次“拒绝”,之后再想申请就只能去设置里手动开启。如果你的用户量上去了,这个体验问题会被无限放大。

我的建议是:

  • 在第一次进入 App 时就用一个说明性的弹窗解释为什么要通知权限(“用于显示锁屏播放控制和媒体通知”),用户同意后再调用系统权限请求
  • 不要在用户刚打开 App 的第一秒就弹权限,那样拒绝率极高
  • 如果权限被拒绝,播放功能可以正常使用,但要明确告诉用户“锁屏控制不可用”这一后果

权限请求的标准写法(Kotlin):

private fun requestNotificationPermission() { if (Build.VERSION.SDK_INT >= 33) { if (ContextCompat.checkSelfPermission( this, Manifest.permission.POST_NOTIFICATIONS ) != PackageManager.PERMISSION_GRANTED ) { ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), REQUEST_CODE_POST_NOTIFICATIONS ) } } }

这个流程看起来简单,但和第 2.1 节的内容是配合使用的:没有权限 → 通知不显示 → 系统媒体中心看不到你 → 按键不给你。

2.3 PlaybackState 里的 Actions 决定了系统愿意给你发什么按键

这是一个非常隐蔽但极其关键的点。有些开发者把 PlaybackState 设置成了STATE_PLAYING,但actions字段里没有包含ACTION_PLAY_PAUSE,结果就是耳机上按播放/暂停键时,系统判断这个 Session 不支持该操作,就直接把按键丢掉了。

我建议你把这些 actions 全部加上,除非你的播放器真的不支持某些操作:

val actions = ( PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PAUSE or PlaybackStateCompat.ACTION_PLAY_PAUSE or PlaybackStateCompat.ACTION_STOP or PlaybackStateCompat.ACTION_SEEK_TO or PlaybackStateCompat.ACTION_SKIP_TO_NEXT or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or PlaybackStateCompat.ACTION_FAST_FORWARD or PlaybackStateCompat.ACTION_REWIND ).toLong()

注意actions要从BuildersetActions方法传入,并且state不能是STATE_NONE,否则系统会认为这是一个无效会话。

我自己的经验是:state要区分播放和暂停两种状态,不要永远停留在STATE_PLAYING。很多系统 ROM 在判断“当前主媒体会话”时,会优先选择正在STATE_PLAYING的那个会话。如果你的 App 实际上暂停了,但 state 还写PLAYING,会导致锁屏界面状态错乱,甚至出现两个 App 同时显示播放状态。

2.4 音频焦点:容易被忽略的按键分发前提

我遇到的一个诡异情况是:App 内播放正常,锁屏媒体卡片也显示了,但蓝牙耳机按键就是没反应。后来通过抓取系统日志发现,MediaSession 收到了按键,但在处理按键时因为音频焦点不在自己身上,被系统拦截了。

这里要理清两个概念:requestAudioFocus和 MediaSession 的setActive是两回事。MediaSession 的活跃状态负责“系统把按键分发给谁”,而音频焦点负责“按键分发过来之后,你的 onPlay/onPause 能不能顺利执行”。实际上在部分 ROM 上,音频焦点还会反向影响 MediaSession 的优先级。

我建议在播放开始时这样处理:

val audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes( AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build() ) .setOnAudioFocusChangeListener { focusChange -> when (focusChange) { AudioManager.AUDIOFOCUS_LOSS -> { // 暂停播放,并且可以降低 MediaSession 的 active 状态 pausePlayback() } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { // 暂停播放,等焦点恢复后再继续 pausePlayback() } AudioManager.AUDIOFOCUS_GAIN -> { // 焦点恢复,继续播放 resumePlayback() } } } .build() val result = audioManager.requestAudioFocus(audioFocusRequest)

这里有一个容易被忽略的点:AUDIOFOCUS_GAIN请求成功之后,要等一小段时间再设置 MediaSession 的PlaybackState。我遇到过在焦点还没完全授予时就去更新 Session 状态,结果 Session 被其他应用抢占的情况。稳妥的做法是在onAudioFocusChangeListener收到AUDIOFOCUS_GAIN回调后,再调用mediaSession.isActive = trueplaybackState的更新。

3. 实操过程与核心环节实现

3.1 一个最小可用的 MediaSession 实现

写一个能正常接收媒体按键的最小工程。这里我以普通的 Service 播放器为例子,用MediaSessionCompat来保证在 Android 13/14 上都能工作,同时兼容旧版本。

先看 Service 的核心部分:

class PlaybackService : Service() { private lateinit var mediaSession: MediaSessionCompat private lateinit var audioManager: AudioManager private var audioFocusRequest: AudioFocusRequest? = null override fun onCreate() { super.onCreate() audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager setupMediaSession() startAsForeground() } private fun setupMediaSession() { val componentName = ComponentName(this, MediaButtonReceiver::class.java) mediaSession = MediaSessionCompat(this, "PlaybackService", componentName, null) mediaSession.setCallback( object : MediaSessionCompat.Callback() { override fun onPlay() { super.onPlay() startPlayback() updatePlaybackState(PlaybackStateCompat.STATE_PLAYING) } override fun onPause() { super.onPause() pausePlayback() updatePlaybackState(PlaybackStateCompat.STATE_PAUSED) } override fun onSkipToNext() { super.onSkipToNext() playNext() } override fun onSkipToPrevious() { super.onSkipToPrevious() playPrevious() } override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { Log.d("MediaButtonTest", "onMediaButtonEvent: ${mediaButtonEvent.action}") return super.onMediaButtonEvent(mediaButtonEvent) } override fun onPlayFromMediaId(mediaId: String, extras: Bundle?) { super.onPlayFromMediaId(mediaId, extras) playSpecificMedia(mediaId) } } ) val mediaStyle = NotificationCompat.MediaStyle() mediaStyle.setMediaSession(mediaSession.sessionToken) mediaStyle.setShowActionsInCompactView(0, 1, 2) val notification = NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setContentTitle("正在播放") .setContentText("测试媒体会话") .setStyle(mediaStyle) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setPriority(NotificationCompat.PRIORITY_LOW) .setOnlyAlertOnce(true) .setOngoing(true) .build() mediaSession.isActive = true updatePlaybackState(PlaybackStateCompat.STATE_PLAYING) } private fun updatePlaybackState(state: Int) { val actions = ( PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PAUSE or PlaybackStateCompat.ACTION_PLAY_PAUSE or PlaybackStateCompat.ACTION_STOP or PlaybackStateCompat.ACTION_SKIP_TO_NEXT or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or PlaybackStateCompat.ACTION_SEEK_TO ).toLong() val playbackState = PlaybackStateCompat.Builder() .setActions(actions) .setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f) .build() mediaSession.setPlaybackState(playbackState) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { MediaButtonReceiver.handleIntent(mediaSession, intent) return START_NOT_STICKY } override fun onDestroy() { super.onDestroy() mediaSession.release() if (Build.VERSION.SDK_INT >= 26) { audioFocusRequest?.let { audioManager.abandonAudioFocusRequest(it) } } } override fun onBind(intent: Intent?): IBinder? = null }

注意几个关键点:

onStartCommand里调用了MediaButtonReceiver.handleIntent(mediaSession, intent),这一步是将系统通过PendingIntent发过来的Intent转成对应的onPlayonPause回调。如果不调用这个,你将只能收到onMediaButtonEvent,而onPlayonPause这些语义化回调不会被触发。

updatePlaybackState中的setState第三参数是播放速度。这个参数不能传 0,否则系统在计算播放位置时会产生异常行为,极端情况下会导致 Session 被系统判定为无效。播放中应该传1.0f,暂停时传0.0f。我见过有些人统一传1.0f,暂停时位置还在往前跑,锁屏界面进度条就乱了。

startAsForeground()方法里要传通知。有些人会在onCreate里先调startForeground,再创建 MediaSession,这个顺序没问题。但注意:不能先创建 MediaSession 再后补通知,也不要在通知还没有构建出来时调用 startForeground,否则服务会崩溃或者通知不显示。

3.2 用 adb 模拟媒体按键来验证问题

有些情况下没有实体耳机,或者蓝牙连接不稳定,怎么验证 MediaSession 能不能收到按键?用 adb 模拟是最快的方案。

# 模拟播放/暂停键按下 adb shell input keyevent 85 # 模拟下一曲 adb shell input keyevent 87 # 模拟上一曲 adb shell input keyevent 88 # 停止 adb shell input keyevent 86

按键事件发出后,你会希望看到 Logcat 里出现对应的日志。但这里有个大前提:adb 模拟的 keyevent 走的是系统输入管道,不一定会经过音频策略模块直接分发给 MediaSession。在部分设备上,adb shell input keyevent 85确实会触发 MediaSession 的onMediaButtonEvent,但在另外一些设备上毫无反应。

如果你发现 adb 模拟按键无效,不要急着下结论。更可靠的方法是直接用媒体控制器发送命令:

# 通过 cmd media_session 发送 play 命令 adb shell cmd media_session dispatch play # 发送 pause 命令 adb shell cmd media_session dispatch pause # 发送 next 命令 adb shell cmd media_session dispatch next

如果系统返回错误,可以查看帮助:

adb shell cmd media_session help

我通常用两种方式组合验证:先cmd media_session dispatch play确认 MediaSession 本身没问题,再用input keyevent 85模拟真实按键。前者验证回调链路,后者验证系统级按键分发。两者都通过,说明从 MediaSession 到业务逻辑这条链路是通的。

另外推荐用dumpsys media_session来确认会话状态:

adb shell dumpsys media_session

输出里重点看:

  • 当前是否有活跃的 MediaSession
  • 每个 Session 的 package name、isActive 状态
  • playbackState 的 state 和 actions
  • 是否有“Media button receiver”相关的信息

如果你在输出里看不到自己的包名,说明 MediaSession 根本没有成功注册。如果看到了,但 show 的内容里 state 是STATE_NONE,说明是 playbackState 设置有问题。

3.3 ExoPlayer/Media3 场景下的特殊处理

如果你的播放器是基于 ExoPlayer 或 Media3 的,实际上 Media3 内部已经帮你创建了MediaSession,你不需要自己再手动 new 一个。但很多老项目的架构是在 ExoPlayer 之外又包了一层自己定义的 MediaSession,两个 Session 同名甚至不同名同时存在,系统会随机选择一个进行分发。

我建议的方案是:

  • 如果使用 ExoPlayer,直接用MediaSession(来自androidx.media3:media3-session)包装你的Player实例
  • 不要自己再创建MediaSessionCompat
  • 旧代码里的MediaButtonReceiverAudioManager.registerMediaButtonEventReceiver逻辑全部移除

Media3 的具体写法:

// Player 是 ExoPlayer 的实例 mediaSession = MediaSession.Builder(context, player) .setCallback(object : MediaSession.Callback { override fun onMediaButtonEvent( session: MediaSession, intent: Intent ): Boolean { Log.d("Media3Test", "onMediaButtonEvent: ${intent.action}") return MediaSession.Callback.CONTINUE_PROPAGATION } }) .build()

如果你需要在收到按键时做一些自定义逻辑(比如记录打点),可以在Callback里拦截,然后返回CONTINUE_PROPAGATION让 Media3 继续处理。返回Handled或者返回 false 则会让 Media3 认为你已经消费了事件。

这里有个细节:Media3 的MediaSession.Callback默认会处理onPlayonPause等所有操作。如果你在自定义逻辑里把事件消费掉了,播放器就不会暂停/继续了,所以一般情况下回调处理完业务后要放行。

多 Session 并存的问题也是 Media3 场景常见的坑。如果你的项目里有多个Player实例(比如一个用于主播放,一个用于试听),那就对应多个MediaSession。此时要确保同一时间只有一个 Session 是active的。

fun switchToMainPlayer() { previewMediaSession?.isActive = false mainMediaSession?.isActive = true }

系统在处理媒体按键时,会优先选择 active 且有音频焦点的 Session。如果两个 Session 都是 active,那结果就是随机的,用户按键时有时这边反应、有时那边反应,非常难排查。

3.4 锁屏和通知栏控制中心的兼容适配

锁屏媒体控制这一块在 API 33 之后也变了不少。如果你发现耳机按键能收到,但锁屏页面没有出现媒体卡片,那问题多半出在通知的visibilitycategory上。

MediaStyle 通知需要满足这些条件才会出现在锁屏上:

  • 通知的 visibility 为VISIBILITY_PUBLIC
  • 通知的 category 设置为CATEGORY_TRANSPORT
  • 通知的 channel 优先级不能太低(不能是IMPORTANCE_NONE
  • 通知必须通过MediaStyle.setMediaSession()绑定 MediaSession

其中setCategory(NotificationCompat.CATEGORY_TRANSPORT)这一步经常被忽略。虽然有部分 ROM 在不设置时也能显示,但为了保证兼容性最好写上。

另外,如果你需要展示下一曲/上一曲按钮,通过addAction添加ACTION_MEDIA_PREVIOUSACTION_PLAY_PAUSEACTION_MEDIA_NEXT三个 action,然后在MediaStyle.setShowActionsInCompactView(0, 1, 2)里把这三个索引传进去。这样在锁屏/通知栏下拉时,会显示这三个按钮。

4. 常见问题与排查技巧实录

4.1 问题排查速查表

这张表是我在实际排障过程中整理出来的,基本涵盖了 API 33+ 设备上 MediaSession 监听不到媒体按键的绝大多数原因。

现象可能原因验证方法
耳机按键完全无反应,Logcat 无任何输出MediaSession 未注册成功或未 active执行adb shell dumpsys media_session查看包名是否在列表中
有反应但时灵时不灵多个 MediaSession 冲突,系统随机分发在 onCreate 打印所有 Session 的创建堆栈,确认只有一个实例
播放正常,但暂停后按键无法恢复播放PlaybackState 在暂停时 actions 不包含 ACTION_PLAY设置 PlaybackState 时把播放和暂停的 actions 同时加上
通知栏不显示媒体卡片POST_NOTIFICATIONS 权限被拒绝检查通知权限状态,在系统设置中手动开启
锁屏不显示媒体卡片,但通知栏显示通知未设置 visibility 或 category检查 MediaStyle 通知的 visibility/category 配置
有线耳机按键无效,蓝牙耳机无效但实体按键有效音频焦点未成功请求或 AudioAttributes 类型不对打印 requestAudioFocus 结果,确认是 AUDIOFOCUS_REQUEST_GRANTED
App 在前台时按键有效,退到后台后失效Service 被系统杀死或未正确 startForeground检查 Service 是否还在运行,查看adb shell dumpsys activity services
只有按一下有效,连续按第二下就失效通知的setOngoing(true)未设置,或通知被系统回收确保通知常驻,并设置 ongoing
按键事件触发了 App 启动,但没触发播放/暂停MediaButtonReceiver 没有调用 handleIntent检查 Service onStartCommand 里是否调用了MediaButtonReceiver.handleIntent

4.2 三步定位:日志、状态、系统分发

如果看了速查表还没定位到问题,我推荐按下面的顺序系统排查。

第一步:在onMediaButtonEvent回调里加日志。如果这个回调都没被触发,说明系统压根没把按键分发给你的 Session,问题出在注册/状态上。如果回调触发了但没有走到onPlay/onPause,问题出在 Intent 转交逻辑上。

第二步:通过dumpsys media_session检查 Session 的实时状态。注意看这些字段:

adb shell dumpsys media_session | grep -A 20 -i "your.package.name"

重点关注state=PlaybackState {state=3(3 是 STATE_PLAYING),以及isActive=true。如果 state 是 0(STATE_NONE)或者isActive=false,不管权限怎么配,按键都不该到你这边。

第三步:检查系统音频策略的按键分发日志。这一步对普通开发者来说有点黑盒,但可以试试:

adb shell logcat -d | grep -i "MediaSession" adb shell logcat -d | grep -i "MediaButton" adb shell logcat -d | grep -i "AudioService"

在一些原生系统上,你甚至能在 Logcat 里看到按键被分发给了哪个包名。比如看到dispatchMediaKeyEvent to com.spotify,就知道系统把按键给了别人,那就要检查你的 Session 优先级为什么比别人的低。

4.3 我踩过的一个非常隐蔽的坑:targetSdk 32 和 33 的行为差异

如果同一套代码在 API 33 上按键失效,但在 API 32 或更低版本上正常,那问题很可能是targetSdk升级导致的。注意,这里说的是 targetSdk 变化,不是系统版本变化。

举个例子:同一个 APK,targetSdk 32,跑在 Android 14 手机上,MediaSession 按键正常;但把 targetSdk 升到 34 后打出来的包,跑在同一台手机上,按键就失效了。

原因很可能是权限模型的差异。targetSdk 32 及以下,通知权限默认授予,系统不知道你没有POST_NOTIFICATIONS权限,媒体通知照常显示,媒体会话照常可见。targetSdk 33+ 后,通知权限默认拒绝,如果你的代码在权限弹窗没出现或者用户没授权的情况下继续播放,媒体通知就被系统隐藏了,媒体会话在一个“不可见”的状态下运行,按键行为就发生变化。

所以我在适配时总是先检查一个事情:targetSdk从 32 升到 33+ 之后,Manifest.permission.POST_NOTIFICATIONS有没有在代码里动态申请。如果只是在 Manifest 里声明了<uses-permission>但没动态申请,那等于没加,权限默认还是拒绝。

还有一种情况是权限弹窗出现了,但用户在弹窗出现前就点了播放。这会触发后续的时序问题:权限还没回调,播放已经开始,MediaSession 已经 active,但通知因为权限不足发不出来。当用户最终授权后,服务虽然还在跑,但通知和 Session 之间的绑定可能已经错位了。

我最终的解决办法是:在PlaybackServiceonCreate里先保证通知权限状态已知,再执行startForeground。如果权限没有授予,就先把播放操作挂起,等授权回调后再继续。虽然这样稍微复杂一点,但能够从根本上避免时序问题。

4.4 厂商 ROM 的额外“惊喜”

最后单独说一嘴厂商 ROM。国内各种定制 ROM 对后台播放、媒体按键的处理策略五花八门,同一个原生逻辑在不同 ROM 上表现差异巨大。

我遇到过的问题包括:小米手机在省电策略下会直接杀后台播放 Service,按键自然失效;vivo 的某个版本需要应用在“自启动管理”里被允许才能收到媒体按键;华为手机在 API 33 升级后,必须把应用加入“电池优化白名单”,否则后台播放一段时间后 MediaSession 会被系统回收。

这一块无法通过代码完全规避,但可以做到两点:一是保证START_STICKY和服务重建逻辑,让进程被杀后能快速恢复;二是在用户首次使用时引导用户进行电池策略设置。另外,如果测试时发现设备上按键时而有效时而无效,先别急着怀疑代码,建议在另一台不同品牌的设备上复测一遍,排除 ROM 干扰。

我在实测中还有一个体会:很多问题在原生 Android 模拟器上是完全复现不出来的,比如通知权限被拒后媒体卡片消失、MediaButtonReceiver 失效等。有条件还是多借几台不同厂商、不同 Android 版本的实体设备来测试,这比在模拟器上反复调代码有效得多。

4.5 从旧项目迁移的替代方案

如果你的项目比较大,不方便把所有逻辑都迁移到 Media3,那可以在保留MediaSessionCompat的基础上做好兼容。我给你一个可落地的改造清单:

  1. AndroidManifest.xml中保留MediaButtonReceiver声明,不要删除
  2. PlaybackService.onStartCommand中调用MediaButtonReceiver.handleIntent(mediaSession, intent)
  3. 动态申请POST_NOTIFICATIONS权限,并在权限回调后再展示媒体通知
  4. 确保MediaSession只在一个地方创建,并且播放状态切换时更新PlaybackState
  5. setCallback里实现onPlayonPauseonSkipToNext等核心方法
  6. 在播放和暂停时正确更新PlaybackState,不要停留在STATE_NONE
  7. 保持MediaStyle通知常驻,并设置setOngoing(true)

这套改造方案不依赖 Media3,纯用 AndroidX Media 库也可以稳定工作。我在一个线上音乐 App 里就是这么改的,改造完之后在 Android 13、14 上无论有线耳机还是蓝牙耳机,按键都能正常触发。

如果你有精力继续往下优化,建议后续把整个播放器迁移到 Media3,毕竟 Google 官方的新特性都在 Media3 上迭代,MediaSessionCompat 虽然暂时还能用,但维护精力会越来越大。

我个人在实际排障中的体会是:遇到 MediaSession 按键失效,先不要慌着改代码,把dumpsys media_session的状态看一遍,把onMediaButtonEvent的日志打出来,分清是“系统没发给你”还是“发给你了但你没处理”,这两个方向排查路径完全不同。很多时候,问题都不是出在“监听”本身,而是出在“系统认为你没有资格监听”。把 Session 的 active 状态、PlaybackState 的完整度、通知的可见性和音频焦点这几件事做对,按键自然就回来了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 18:59:34

Crawl4AI 的 WebScrapingStrategy 改为 LXML 实现后旧代码需要改什么

Crawl4AI 的 WebScrapingStrategy 改为 LXML 实现后旧代码需要改什么 【免费下载链接】crawl4ai &#x1f680;&#x1f916; Crawl4AI: Open-source LLM Friendly Web Crawler & Scraper. Dont be shy, join here: https://discord.gg/jP8KfhDhyN 项目地址: https://git…

作者头像 李华
网站建设 2026/9/9 18:57:02

STM32F103移植FreeRTOS:精简工程模板与避坑指南

简介&#xff1a;一份基于STM32F103与FreeRTOS的模板工程&#xff0c;面向需要快速搭建多任务实时系统的嵌入式开发者&#xff0c;适合工业控制、物联网终端等场景。工程将FreeRTOS内核与STM32固件库整合&#xff0c;包含初始化代码、任务定义、调度机制、中断处理等基础模块&a…

作者头像 李华
网站建设 2026/9/9 18:56:17

音视频调度矩阵选型实战指南:从需求到验收的完整流程

1. 先把需求吃透&#xff0c;再谈厂家选择1.1 场景决定架构&#xff1a;指挥中心、会议室、调度室各自的侧重点做工程商这些年&#xff0c;我最大的感受是&#xff1a;音视频调度矩阵这个品类&#xff0c;听起来是一个东西&#xff0c;实际在不同场景里完全是两码事。指挥中心要…

作者头像 李华
网站建设 2026/9/9 18:56:13

MinerU 在 Windows 直装后推理速度很慢怎么排查?

MinerU 在 Windows 直装后推理速度很慢怎么排查&#xff1f; 【免费下载链接】MinerU Transforms complex documents like PDFs and Office docs into LLM-ready markdown/JSON for your Agentic workflows. 项目地址: https://gitcode.com/GitHub_Trending/mi/MinerU 在…

作者头像 李华