Android 媒体篇|吃透 MediaSession 与 MediaController

一、引言:为什么需要MediaSession架构?

Android媒体应用开发中,我们经常面临这样的挑战:

  • 如何统一处理来自不同来源的控制指令(通知栏、蓝牙设备、语音命令等)?
  • 如何确保播放状态在各种场景下同步更新?
  • 如何遵循 Android的最佳实践构建健壮的媒体应用?

我们先来看看如何设计一款音乐播放App的架构,假如要求音频可以后台继续播放。传统的做法是这样的:

  • 注册一个Service,用于异步获取音乐库数据、音乐控制等,在Service中我们可能还需要自定义一些状态值和回调接口用于流程控制
  • 把Player放置在这个Service中,Service提供一个Binder或者广播(其他方式如接口、Messenger都可以)Activity` 和 `Service之间的通信,使得用户可以通过界面上的组件控制音乐的播放、暂停、拖动进度条等操作

如果我们的音乐播放器还需要支持通知栏快捷控制音乐播放的功能,那么又得新增一套广播和相应的接口去响应通知栏按钮的事件

MediaSessionMediaController就是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 注册监听器

⚠️ 注意 :注册时需传入已启用的 NotificationListenerServiceComponentName,否则将抛出 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框架的介绍和使用音视频组成播放器和界面 播放音频或视频的多媒体应用通常由两部分组成: 播放器,用 - 掘金

Android车载多媒体开发MediaSession框架理解(建议收藏).md

相关推荐
希望_睿智1 小时前
实战设计模式之解释器模式
c++·设计模式·架构
高阳言编程1 小时前
8. 数据流计算机和规约机
架构
雨白1 小时前
Android 自定义 View:彻底搞懂 Xfermode 与官方文档陷阱
android
piaoyunlive2 小时前
Base64 编码优化 Web 图片加载:异步响应式架构(Java 后端 + 前端全流程实现)
java·前端·架构
_小马快跑_2 小时前
从VSync心跳到SurfaceFlinger合成:拆解 Choreographer与Display刷新流程
android
_小马快跑_2 小时前
Android | 视图渲染:从invalidate()到屏幕刷新的链路解析
android
Xの哲學3 小时前
Linux PCI 子系统:工作原理与实现机制深度分析
linux·网络·算法·架构·边缘计算
Monkey-旭5 小时前
Android 定位技术全解析:从基础实现到精准优化
android·java·kotlin·地图·定位
qb5 小时前
vue3.5.18源码:computed 在发布订阅者模式中的双重角色
前端·vue.js·架构
PetterHillWater6 小时前
关于系统设计原则回顾
架构