一、引言:为什么需要MediaSession架构?
在Android
媒体应用开发中,我们经常面临这样的挑战:
- 如何统一处理来自不同来源的控制指令(通知栏、蓝牙设备、语音命令等)?
- 如何确保播放状态在各种场景下同步更新?
- 如何遵循
Android
的最佳实践构建健壮的媒体应用?
我们先来看看如何设计一款音乐播放App的架构,假如要求音频可以后台继续播放。传统的做法是这样的:
- 注册一个Service,用于异步获取音乐库数据、音乐控制等,在Service中我们可能还需要自定义一些状态值和回调接口用于流程控制
- 把Player放置在这个Service中,Service提供一个Binder或者广播(其他方式如接口、Messenger都可以)
Activity` 和 `Service
之间的通信,使得用户可以通过界面上的组件控制音乐的播放、暂停、拖动进度条等操作
如果我们的音乐播放器还需要支持通知栏快捷控制音乐播放的功能,那么又得新增一套广播和相应的接口去响应通知栏按钮的事件
MediaSession
和MediaController
就是Android为解决这些问题提供的标准化架构。它们将播放控制抽象为服务端(MediaSession) 和客户端(MediaController) 的分离设计,让你的播放器轻松集成到Android媒体生态中。

二、核心概念解析
MediaSession - 媒体服务的"大脑"
位置:通常存在于Service(如MediaBrowserService
)
职责:
- 管理播放状态(播放/暂停/停止)
- 存储媒体元数据(标题、专辑等)
- 响应来自控制器的操作指令
- 与系统媒体控制中心通信
MediaController - UI层的"遥控器"
位置:存在于Activity/Fragment
职责:
- 向
MediaSession
发送操作指令 - 接收
MediaSession
的状态更新 - 同步显示媒体元数据
三、MediaSession架构的优势
MediaSession框架规范了音视频应用中界面 与播放器 之间的通信接口,属于典型的 C/S 架构 ,实现界面与播放器之间的完全解耦。框架定义了两个重要的类媒体会话 和媒体控制器,它们为构建多媒体播放器应用提供了一个完善的结构。
媒体会话和媒体控制器通过以下方式相互通信:使用与标准播放器操作(播放、暂停、停止等)相对应的预定义回调,以及用于定义应用独有的特殊行为的可扩展自定义调用。
四、服务端实现:MediaBrowserServiceCompat(推荐)
1. 创建并激活 MediaSession
Java
public class MediaPlaybackService extends MediaBrowserServiceCompat {
private MediaSessionCompat mediaSession;
private MediaPlayer mediaPlayer;
private AudioManager audioManager;
@Override
public void onCreate() {
super.onCreate();
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
mediaSession = new MediaSessionCompat(this, "MusicPlayerSession");
mediaSession.setCallback(new MediaSessionCallback());
mediaSession.setFlags(
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
);
mediaSession.setActive(true);
setSessionToken(mediaSession.getSessionToken());
initializeMediaPlayer();
createNotificationChannel();
}
}
✅ 推荐使用
MediaBrowserServiceCompat
,支持媒体浏览结构,兼容 Android Auto/Wear OS。
2. 实现回调处理
Java
private class MediaSessionCallback extends MediaSessionCompat.Callback {
@Override public void onPlay() {
if (requestAudioFocus()) {
mediaPlayer.start();
updatePlaybackState(PlaybackStateCompat.STATE_PLAYING);
}
}
@Override public void onPause() {
mediaPlayer.pause();
updatePlaybackState(PlaybackStateCompat.STATE_PAUSED);
}
@Override public void onSkipToNext() {
// 加载下一首
updateMetadata();
onPlay();
}
@Override public void onSeekTo(long pos) {
mediaPlayer.seekTo((int) pos);
updatePlaybackState(currentState, pos);
}
}
⚠️ 注意:
MediaSession.Callback
默认在主线程调用,若播放逻辑在子线程,请手动切换。
3. 设置媒体元数据
Java
private void updateMetadata() {
MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, "示例歌曲")
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, "示例艺术家")
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, mediaPlayer.getDuration())
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArtBitmap)
.build();
mediaSession.setMetadata(metadata);
}
4. 创建通知栏控制卡片(MediaStyle)
Java
private Notification createNotification() {
Intent intent = new Intent(this, MainActivity.class);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("示例歌曲")
.setContentText("示例艺术家")
.setSmallIcon(R.drawable.ic_music)
.setLargeIcon(albumArtBitmap)
.setContentIntent(contentIntent)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.addAction(R.drawable.ic_prev, "上一首", createAction(ACTION_SKIP_PREVIOUS))
.addAction(R.drawable.ic_pause, "暂停", createAction(ACTION_PAUSE))
.addAction(R.drawable.ic_next, "下一首", createAction(ACTION_SKIP_NEXT))
.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSession.getSessionToken())
.setShowActionsInCompactView(0, 1, 2))
.setOngoing(isPlaying());
return builder.build();
}
⚠️ Android 13+ 需动态申请
POST_NOTIFICATIONS
权限,否则通知可能不显示。
五、客户端实现:MediaController
实现一个 MediaController 需要以下关键三步
1. 连接 MediaSession
Java
public class PlayerActivity extends AppCompatActivity {
private MediaControllerCompat mediaController;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_player);
SessionToken token = new SessionToken(this,
new ComponentName(this, MediaPlaybackService.class));
try {
mediaController = new MediaControllerCompat(this, token);
MediaControllerCompat.setMediaController(this, mediaController);
} catch (RemoteException e) {
Toast.makeText(this, "无法连接到播放服务", Toast.LENGTH_SHORT).show();
}
}
}
2. 发送控制指令
Java
binding.btnPlay.setOnClickListener(v -> {
MediaControllerCompat controller = MediaControllerCompat.getMediaController(this);
if (controller != null) {
controller.getTransportControls().play();
}
});
3. 监听状态变化
播放过程中有状态变化,可以通过MediaControllerCompat.Callback
监听播放状态,进而更新 UI 上的状态
Java
private final MediaControllerCompat.Callback callback = new MediaControllerCompat.Callback() {
@Override public void onPlaybackStateChanged(PlaybackStateCompat state) {
updatePlayButton(state);
}
@Override public void onMetadataChanged(MediaMetadataCompat metadata) {
updateTrackInfo(metadata);
}
};
@Override
protected void onStart() {
super.onStart();
MediaControllerCompat controller = MediaControllerCompat.getMediaController(this);
if (controller != null) {
controller.registerCallback(callback);
}
}
@Override
protected void onStop() {
super.onStop();
MediaControllerCompat controller = MediaControllerCompat.getMediaController(this);
if (controller != null) {
controller.unregisterCallback(callback);
}
}
六、监听系统活跃 Session
应用可以通过监听系统里活跃的 MediaSession,获取到相应的MediaController
方案一:使用 MediaSessionManager.OnActiveSessionsChangedListener
1. 基本说明
- 类名 :
android.media.session.MediaSessionManager.OnActiveSessionsChangedListener
- 作用 :当系统中活跃的
MediaSession
列表发生变化时,回调此方法,返回当前活跃的MediaController
列表。 - 权限要求 :需要系统级权限
android.permission.MEDIA_CONTENT_CONTROL
,或使用已启用的NotificationListenerService
。
2. 实现步骤
2.1 获取 MediaSessionManager
实例
Java
MediaSessionManager sessionManager =
(MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
2.2 创建监听器
Java
MediaSessionManager.OnActiveSessionsChangedListener sessionsChangedListener =
new MediaSessionManager.OnActiveSessionsChangedListener() {
@Override
public void onActiveSessionsChanged(@Nullable List<MediaController> controllers) {
if (controllers == null) return;
for (MediaController controller : controllers) {
// 示例:打印当前活跃会话的包名
Log.d("MediaMonitor", "Active session: " + controller.getPackageName());
}
}
};
2.3 注册监听器
⚠️ 注意 :注册时需传入已启用的
NotificationListenerService
的ComponentName
,否则将抛出SecurityException
。
Java
ComponentName listenerComponent =
new ComponentName(context, MyNotificationListenerService.class);
sessionManager.addOnActiveSessionsChangedListener(
sessionsChangedListener,
listenerComponent
);
2.4 反注册监听器(防止内存泄漏)
Java
sessionManager.removeOnActiveSessionsChangedListener(sessionsChangedListener);
3. 完整示例(Kotlin)
Kotlin
class MyNotificationListenerService : NotificationListenerService() {
private val sessionManager by lazy {
getSystemService(Context.MEDIA_SESSION_SERVICE) as MediaSessionManager
}
private val listener = MediaSessionManager.OnActiveSessionsChangedListener { controllers ->
controllers?.forEach { controller ->
Log.d("MediaMonitor", "Active: ${controller.packageName}")
}
}
override fun onListenerConnected() {
super.onListenerConnected()
val component = ComponentName(this, this::class.java)
sessionManager.addOnActiveSessionsChangedListener(listener, component)
}
override fun onDestroy() {
super.onDestroy()
sessionManager.removeOnActiveSessionsChangedListener(listener)
}
}
4. 权限与兼容性说明
项目 | 说明 |
---|---|
权限 | 需 MEDIA_CONTENT_CONTROL (系统应用)或启用 NotificationListenerService (用户授权) |
最低版本 | Android 5.0(API 21) |
推荐场景 | 系统工具、车载系统、全局媒体控制器 |
不推荐场景 | 普通应用(权限受限,易被系统拒绝) |
方案二:NotificationListenerService
Java
class MediaNotificationListener : NotificationListenerService() {
override fun onListenerConnected() {
val tokens = activeNotifications
.mapNotNull { it.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION, MediaSession.Token::class.java) }
val controllers = tokens.map { MediaControllerCompat(this, it) }// 可监听这些控制器的状态}}
⚠️ 无需系统权限,用户授权即可使用,优于
MediaSessionManager
。
方案对比
特性 | MediaSessionManager |
NotificationListenerService |
获取方式 | getActiveSessions() 或 OnActiveSessionsChangedListener |
activeNotifications |
权限要求 | 系统级或通知监听权限 | 用户授权通知监听权限 |
实时性 | 高(回调实时) | 中(需轮询或监听通知) |
适用场景 | 系统级工具、车载 | 普通应用、第三方控制器 |
建议:对于普通应用,推荐使用
NotificationListenerService
方案; 对于系统级应用或 OEM 厂商,可使用MediaSessionManager
实现更精细控制。
七、实战:实现通知栏控制媒体播放
目标:在下拉状态栏里生成一张带封面、歌曲信息、播放/暂停/切歌按钮的媒体控制卡片,且与系统锁屏、蓝牙、Wear OS 等组件无缝衔接。
关键步骤
步骤 | 目的 | 关键代码 |
① 创建通知通道 | 适配 8.0+ | NotificationChannel |
② 构建 MediaStyle 通知 | 生成控制卡片 | NotificationCompat.MediaStyle() |
③ 绑定 MediaSession | 让系统识别 | setMediaSession(token) |
④ 更新状态/元数据 | 卡片实时同步 | mediaSession.setPlaybackState() & setMetadata() |
代码示例
Java
// 1. 创建通知通道(只需一次)
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID, "媒体播放", NotificationManager.IMPORTANCE_LOW);
channel.setDescription("音乐播放控制");
channel.setShowBadge(false);
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(channel);
}
// 2. 生成 MediaStyle 通知
private Notification buildMediaNotification() {
Intent openApp = new Intent(this, MainActivity.class);
PendingIntent contentIntent = PendingIntent.getActivity(
this, 0, openApp, PendingIntent.FLAG_IMMUTABLE);
// 播放/暂停按钮
int playPauseIcon = isPlaying()
? R.drawable.ic_pause : R.drawable.ic_play;
PendingIntent playPauseAction = createAction(
isPlaying() ? ACTION_PAUSE : ACTION_PLAY);
return new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_music)
.setContentTitle(getCurrentTitle())
.setContentText(getCurrentArtist())
.setLargeIcon(getCurrentAlbumArt())
.setContentIntent(contentIntent)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
.setOngoing(isPlaying())
.addAction(R.drawable.ic_prev, "上一首", createAction(ACTION_SKIP_PREVIOUS))
.addAction(playPauseIcon, isPlaying() ? "暂停" : "播放", playPauseAction)
.addAction(R.drawable.ic_next, "下一首", createAction(ACTION_SKIP_NEXT))
.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSession.getSessionToken())
.setShowActionsInCompactView(0, 1, 2)) // 折叠时显示 3 个按钮
.build();
}
// 3. 启动前台服务并显示通知
private void updateNotification() {
Notification notification = buildMediaNotification();
startForeground(NOTIFICATION_ID, notification);
}
// 4. 工具方法:创建 PendingIntent
private PendingIntent createAction(String action) {
Intent intent = new Intent(this, MediaPlaybackService.class).setAction(action);
return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
}
参考文档
MediaSession框架的介绍和使用音视频组成播放器和界面 播放音频或视频的多媒体应用通常由两部分组成: 播放器,用 - 掘金