媒体会话提供了一种与音频或视频播放器交互的通用方式。在 Media3 中,默认播放器是 ExoPlayer
类,它实现了 Player
接口。将媒体会话连接到播放器允许应用对外通告媒体播放,并接收来自外部源的播放命令。
命令可能来自物理按钮,例如耳机或电视遥控器上的播放按钮。它们也可能来自拥有媒体控制器的客户端应用,例如指示 Google 助理“暂停”。媒体会话将这些命令委托给媒体应用的播放器。
何时选择媒体会话
当您实现 MediaSession
时,您允许用户控制播放
- 通过他们的 耳机。用户通常可以在耳机上执行按钮或触摸交互,以播放或暂停媒体,或跳转到上一曲或下一曲。
- 通过与 Google 助理 对话。一种常见模式是说“Ok Google,暂停”以暂停设备上当前正在播放的任何媒体。
- 通过他们的 Wear OS 手表。这使得在手机上播放时更容易访问最常用的播放控件。
- 通过 媒体控件。此轮播会显示每个正在运行的媒体会话的控件。
- 在 电视 上。允许使用物理播放按钮、平台播放控制和电源管理(例如,如果电视、条形音箱或 A/V 接收器关闭或输入切换,应用中的播放应停止)进行操作。
- 以及任何其他需要影响播放的外部进程。
这对于许多用例都非常有用。特别是,在以下情况下,您应该强烈考虑使用 MediaSession
- 您正在流式传输 长篇视频内容,例如电影或直播电视。
- 您正在流式传输 长篇音频内容,例如播客或音乐播放列表。
- 您正在构建 电视应用。
然而,并非所有用例都非常适合 MediaSession
。在以下情况下,您可能只想使用 Player
- 您正在显示 短篇内容,无需外部控制或后台播放。
- 没有单个活动视频,例如用户正在滚动列表,并且 多个视频同时显示 在屏幕上。
- 您正在播放 一次性介绍或解释视频,您希望用户主动观看,而不需要外部播放控件。
- 您的内容是 隐私敏感的,您不希望外部进程访问媒体元数据(例如浏览器中的无痕模式)。
如果您的用例不符合上述任何情况,请考虑您的应用是否可以在用户未主动参与内容时继续播放。如果答案是肯定的,您可能希望选择 MediaSession
。如果答案是否定的,您可能希望改用 Player
。
创建媒体会话
媒体会话与其管理的播放器共存。您可以使用 Context
和 Player
对象构造媒体会话。您应该在需要时创建和初始化媒体会话,例如 Activity
或 Fragment
的 onStart()
或 onResume()
生命周期方法,或拥有媒体会话及其关联播放器的 Service
的 onCreate()
方法。
要创建媒体会话,请初始化 Player
并将其提供给 MediaSession.Builder
,如下所示
Kotlin
val player = ExoPlayer.Builder(context).build() val mediaSession = MediaSession.Builder(context, player).build()
Java
ExoPlayer player = new ExoPlayer.Builder(context).build(); MediaSession mediaSession = new MediaSession.Builder(context, player).build();
自动状态处理
Media3 库使用播放器的状态自动更新媒体会话。因此,您无需手动处理从播放器到会话的映射。
这与平台媒体会话不同,在平台媒体会话中,您需要独立于播放器本身创建和维护 PlaybackState
,例如指示任何错误。
唯一的会话 ID
默认情况下,MediaSession.Builder
创建一个会话,其会话 ID 为空字符串。如果应用只打算创建一个会话实例,这已足够,这也是最常见的情况。
如果应用想要同时管理多个会话实例,则应用必须确保每个会话的会话 ID 都是唯一的。会话 ID 可以在使用 MediaSession.Builder.setId(String id)
构建会话时设置。
如果您看到 IllegalStateException
导致您的应用崩溃,并显示错误消息 IllegalStateException: Session ID must be unique. ID=
,那么很可能是在具有相同 ID 的先前创建的实例发布之前意外创建了会话。为了避免编程错误导致会话泄露,此类情况会通过抛出异常来检测和通知。
授予其他客户端控制权
媒体会话是控制播放的关键。它使您能够将来自外部源的命令路由到执行媒体播放工作的播放器。这些源可以是物理按钮,例如耳机或电视遥控器上的播放按钮,也可以是间接命令,例如指示 Google 助理“暂停”。同样,您可能希望授予 Android 系统访问权限,以方便通知和锁屏控制,或授予 Wear OS 手表访问权限,以便您可以从表盘控制播放。外部客户端可以使用媒体控制器向您的媒体应用发出播放命令。这些命令由您的媒体会话接收,最终将命令委托给媒体播放器。

当控制器即将连接到您的媒体会话时,会调用 onConnect()
方法。您可以使用提供的 ControllerInfo
来决定是接受还是拒绝请求。有关接受连接请求的示例,请参阅声明自定义命令部分。
连接后,控制器可以向会话发送播放命令。然后,会话将这些命令委托给播放器。Player
接口中定义的播放和播放列表命令由会话自动处理。
其他回调方法允许您处理例如自定义命令和修改播放列表的请求。这些回调同样包含一个 ControllerInfo
对象,因此您可以根据每个控制器修改对每个请求的响应方式。
修改播放列表
媒体会话可以直接修改其播放器的播放列表,如 ExoPlayer 播放列表指南中所述。如果 COMMAND_SET_MEDIA_ITEM
或 COMMAND_CHANGE_MEDIA_ITEMS
可用于控制器,则控制器也可以修改播放列表。
添加新项目到播放列表时,播放器通常需要带有已定义 URI 的 MediaItem
实例,以便它们可播放。默认情况下,如果新添加的项目定义了 URI,它们会自动转发到播放器方法,例如 player.addMediaItem
。
如果您想自定义添加到播放器的 MediaItem
实例,可以覆盖 onAddMediaItems()
。当您想要支持请求媒体而没有定义 URI 的控制器时,需要此步骤。相反,MediaItem
通常设置以下一个或多个字段来描述所请求的媒体
MediaItem.id
:标识媒体的通用 ID。MediaItem.RequestMetadata.mediaUri
:可能使用自定义架构的请求 URI,不一定直接由播放器播放。MediaItem.RequestMetadata.searchQuery
:文本搜索查询,例如来自 Google 助理。MediaItem.MediaMetadata
:结构化元数据,如“标题”或“艺术家”。
对于完全新播放列表的更多自定义选项,您还可以覆盖 onSetMediaItems()
,它允许您定义播放列表中的起始项和位置。例如,您可以将单个请求的项扩展到整个播放列表,并指示播放器从最初请求的项的索引开始。会话演示应用中可以找到此功能的 onSetMediaItems() 的示例实现。
管理媒体按钮首选项
每个控制器,例如系统 UI、Android Auto 或 Wear OS,都可以自行决定向用户显示哪些按钮。要指示您希望向用户公开哪些播放控件,您可以在 MediaSession
上指定媒体按钮首选项。这些首选项包含一个有序的 CommandButton
实例列表,每个实例都定义了用户界面中按钮的首选项。
定义命令按钮
CommandButton
实例用于定义媒体按钮首选项。每个按钮定义所需 UI 元素的三个方面
- 图标,定义视觉外观。创建
CommandButton.Builder
时,图标必须设置为预定义常量之一。请注意,这不是实际的位图或图像资源。通用常量有助于控制器选择适当的资源,以在自己的 UI 中实现一致的外观和感觉。如果预定义的图标常量都不适合您的用例,您可以改用setCustomIconResId
。 - 命令,定义用户与按钮交互时触发的操作。您可以将
setPlayerCommand
用于Player.Command
,或将setSessionCommand
用于预定义或自定义的SessionCommand
。 - 插槽,定义按钮应在控制器 UI 中的位置。此字段是可选的,并根据图标和命令自动设置。例如,它允许指定按钮应显示在 UI 的“前进”导航区域,而不是默认的“溢出”区域。
Kotlin
val button = CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15) .setSessionCommand(SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY)) .setSlots(CommandButton.SLOT_FORWARD) .build()
Java
CommandButton button = new CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15) .setSessionCommand(new SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY)) .setSlots(CommandButton.SLOT_FORWARD) .build();
解决媒体按钮首选项时,将应用以下算法
- 对于媒体按钮首选项中的每个
CommandButton
,将按钮放置在第一个可用且允许的插槽中。 - 如果中央、前进和后退插槽中的任何一个没有填充按钮,则为此插槽添加默认按钮。
您可以使用 CommandButton.DisplayConstraints
生成媒体按钮首选项将根据 UI 显示约束如何解析的预览。
设置媒体按钮首选项
设置媒体按钮首选项的最简单方法是在构建 MediaSession
时定义列表。或者,您可以覆盖 MediaSession.Callback.onConnect
以自定义每个连接的控制器的媒体按钮首选项。
Kotlin
val mediaSession = MediaSession.Builder(context, player) .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton)) .build()
Java
MediaSession mediaSession = new MediaSession.Builder(context, player) .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton)) .build();
用户交互后更新媒体按钮首选项
处理与播放器的交互后,您可能希望更新控制器 UI 中显示的按钮。一个典型的例子是切换按钮,它在触发与此按钮关联的操作后更改其图标和操作。要更新媒体按钮首选项,您可以使用 MediaSession.setMediaButtonPreferences
来更新所有控制器或特定控制器的首选项
Kotlin
// Handle "favoritesButton" action, replace by opposite button mediaSession.setMediaButtonPreferences( ImmutableList.of(likeButton, removeFromFavoritesButton))
Java
// Handle "favoritesButton" action, replace by opposite button mediaSession.setMediaButtonPreferences( ImmutableList.of(likeButton, removeFromFavoritesButton));
添加自定义命令和自定义默认行为
可用的播放器命令可以通过自定义命令进行扩展,并且还可以拦截传入的播放器命令和媒体按钮以更改默认行为。
声明和处理自定义命令
媒体应用程序可以定义自定义命令,例如可以在媒体按钮首选项中使用。例如,您可能希望实现允许用户将媒体项保存到收藏夹列表的按钮。MediaController
发送自定义命令,MediaSession.Callback
接收它们。
要定义自定义命令,您需要覆盖 MediaSession.Callback.onConnect()
以设置每个连接的控制器可用的自定义命令。
Kotlin
private class CustomMediaSessionCallback: MediaSession.Callback { // Configure commands available to the controller in onConnect() override fun onConnect( session: MediaSession, controller: MediaSession.ControllerInfo ): MediaSession.ConnectionResult { val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY)) .build() return AcceptedResultBuilder(session) .setAvailableSessionCommands(sessionCommands) .build() } }
Java
class CustomMediaSessionCallback implements MediaSession.Callback { // Configure commands available to the controller in onConnect() @Override public ConnectionResult onConnect( MediaSession session, ControllerInfo controller) { SessionCommands sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle())) .build(); return new AcceptedResultBuilder(session) .setAvailableSessionCommands(sessionCommands) .build(); } }
要接收来自 MediaController
的自定义命令请求,请覆盖 Callback
中的 onCustomCommand()
方法。
Kotlin
private class CustomMediaSessionCallback: MediaSession.Callback { ... override fun onCustomCommand( session: MediaSession, controller: MediaSession.ControllerInfo, customCommand: SessionCommand, args: Bundle ): ListenableFuture<SessionResult> { if (customCommand.customAction == SAVE_TO_FAVORITES) { // Do custom logic here saveToFavorites(session.player.currentMediaItem) return Futures.immediateFuture( SessionResult(SessionResult.RESULT_SUCCESS) ) } ... } }
Java
class CustomMediaSessionCallback implements MediaSession.Callback { ... @Override public ListenableFuture<SessionResult> onCustomCommand( MediaSession session, ControllerInfo controller, SessionCommand customCommand, Bundle args ) { if(customCommand.customAction.equals(SAVE_TO_FAVORITES)) { // Do custom logic here saveToFavorites(session.getPlayer().getCurrentMediaItem()); return Futures.immediateFuture( new SessionResult(SessionResult.RESULT_SUCCESS) ); } ... } }
您可以通过使用传入 Callback
方法的 MediaSession.ControllerInfo
对象的 packageName
属性来跟踪哪个媒体控制器正在发出请求。这允许您根据命令是来自系统、您自己的应用还是其他客户端应用来调整您的应用行为。
自定义默认播放器命令
所有默认命令和状态处理都委托给 MediaSession
上的 Player
。要自定义 Player
接口中定义的命令行为,例如 play()
或 seekToNext()
,请在将 Player
传递给 MediaSession
之前,将其包装在 ForwardingSimpleBasePlayer
中
Kotlin
val player = (logic to build a Player instance) val forwardingPlayer = object : ForwardingSimpleBasePlayer(player) { // Customizations } val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()
Java
ExoPlayer player = (logic to build a Player instance) ForwardingSimpleBasePlayer forwardingPlayer = new ForwardingSimpleBasePlayer(player) { // Customizations }; MediaSession mediaSession = new MediaSession.Builder(context, forwardingPlayer).build();
有关 ForwardingSimpleBasePlayer
的更多信息,请参阅 ExoPlayer 指南中的自定义。
识别播放器命令的请求控制器
当 Player
方法的调用源自 MediaController
时,您可以使用 MediaSession.controllerForCurrentRequest
识别来源,并获取当前请求的 ControllerInfo
Kotlin
class CallerAwarePlayer(player: Player) : ForwardingSimpleBasePlayer(player) { override fun handleSeek( mediaItemIndex: Int, positionMs: Long, seekCommand: Int, ): ListenableFuture<*> { Log.d( "caller", "seek operation from package ${session.controllerForCurrentRequest?.packageName}", ) return super.handleSeek(mediaItemIndex, positionMs, seekCommand) } }
Java
public class CallerAwarePlayer extends ForwardingSimpleBasePlayer { public CallerAwarePlayer(Player player) { super(player); } @Override protected ListenableFuture<?> handleSeek( int mediaItemIndex, long positionMs, int seekCommand) { Log.d( "caller", "seek operation from package: " + session.getControllerForCurrentRequest().getPackageName()); return super.handleSeek(mediaItemIndex, positionMs, seekCommand); } }
自定义媒体按钮处理
媒体按钮是 Android 设备和其他外围设备上的硬件按钮,例如蓝牙耳机上的播放/暂停按钮。Media3 会在媒体按钮事件到达会话时为您处理它们,并调用 会话播放器上相应的 Player
方法。
建议在相应的 Player
方法中处理所有传入的媒体按钮事件。对于更高级的用例,可以在 MediaSession.Callback.onMediaButtonEvent(Intent)
中拦截媒体按钮事件。
错误处理和报告
会话会发出并向控制器报告两种类型的错误。致命错误报告会话播放器的技术播放失败,导致播放中断。致命错误在发生时会自动报告给控制器。非致命错误是非技术性或策略性错误,不会中断播放,由应用程序手动发送给控制器。
致命播放错误
播放器向会话报告致命播放错误,然后报告给控制器,通过 Player.Listener.onPlayerError(PlaybackException)
和 Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException)
调用。
在这种情况下,播放状态会转换为 STATE_IDLE
,并且 MediaController.getPlaybackError()
返回导致转换的 PlaybackException
。控制器可以检查 PlayerException.errorCode
以获取有关错误原因的信息。
为了互操作性,致命错误通过将其状态转换为 STATE_ERROR
并根据 PlaybackException
设置错误代码和消息来复制到平台会话。
致命错误自定义
为了向用户提供本地化和有意义的信息,致命播放错误的错误代码、错误消息和错误附加信息可以通过在构建会话时使用 ForwardingPlayer
进行自定义
Kotlin
val forwardingPlayer = ErrorForwardingPlayer(player) val session = MediaSession.Builder(context, forwardingPlayer).build()
Java
Player forwardingPlayer = new ErrorForwardingPlayer(player); MediaSession session = new MediaSession.Builder(context, forwardingPlayer).build();
转发播放器可以使用 ForwardingSimpleBasePlayer
拦截错误并自定义错误代码、消息或附加信息。同样,您还可以生成原始播放器中不存在的新错误
Kotlin
class ErrorForwardingPlayer (private val context: Context, player: Player) : ForwardingSimpleBasePlayer(player) { override fun getState(): State { var state = super.getState() if (state.playerError != null) { state = state.buildUpon() .setPlayerError(customizePlaybackException(state.playerError!!)) .build() } return state } fun customizePlaybackException(error: PlaybackException): PlaybackException { val buttonLabel: String val errorMessage: String when (error.errorCode) { PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> { buttonLabel = context.getString(R.string.err_button_label_restart_stream) errorMessage = context.getString(R.string.err_msg_behind_live_window) } else -> { buttonLabel = context.getString(R.string.err_button_label_ok) errorMessage = context.getString(R.string.err_message_default) } } val extras = Bundle() extras.putString("button_label", buttonLabel) return PlaybackException(errorMessage, error.cause, error.errorCode, extras) } }
Java
class ErrorForwardingPlayer extends ForwardingSimpleBasePlayer { private final Context context; public ErrorForwardingPlayer(Context context, Player player) { super(player); this.context = context; } @Override protected State getState() { State state = super.getState(); if (state.playerError != null) { state = state.buildUpon() .setPlayerError(customizePlaybackException(state.playerError)) .build(); } return state; } private PlaybackException customizePlaybackException(PlaybackException error) { String buttonLabel; String errorMessage; switch (error.errorCode) { case PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW: buttonLabel = context.getString(R.string.err_button_label_restart_stream); errorMessage = context.getString(R.string.err_msg_behind_live_window); break; default: buttonLabel = context.getString(R.string.err_button_label_ok); errorMessage = context.getString(R.string.err_message_default); break; } Bundle extras = new Bundle(); extras.putString("button_label", buttonLabel); return new PlaybackException(errorMessage, error.getCause(), error.errorCode, extras); } }
非致命错误
不源自技术异常的非致命错误可以由应用发送给所有控制器或特定控制器
Kotlin
val sessionError = SessionError( SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, context.getString(R.string.error_message_authentication_expired), ) // Option 1: Sending a nonfatal error to all controllers. mediaSession.sendError(sessionError) // Option 2: Sending a nonfatal error to the media notification controller only // to set the error code and error message in the playback state of the platform // media session. mediaSession.mediaNotificationControllerInfo?.let { mediaSession.sendError(it, sessionError) }
Java
SessionError sessionError = new SessionError( SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, context.getString(R.string.error_message_authentication_expired)); // Option 1: Sending a nonfatal error to all controllers. mediaSession.sendError(sessionError); // Option 2: Sending a nonfatal error to the media notification controller only // to set the error code and error message in the playback state of the platform // media session. ControllerInfo mediaNotificationControllerInfo = mediaSession.getMediaNotificationControllerInfo(); if (mediaNotificationControllerInfo != null) { mediaSession.sendError(mediaNotificationControllerInfo, sessionError); }
当非致命错误发送到媒体通知控制器时,错误代码和错误消息将复制到平台媒体会话,而 PlaybackState.state
不会更改为 STATE_ERROR
。
接收非致命错误
MediaController
通过实现 MediaController.Listener.onError
来接收非致命错误
Kotlin
val future = MediaController.Builder(context, sessionToken) .setListener(object : MediaController.Listener { override fun onError(controller: MediaController, sessionError: SessionError) { // Handle nonfatal error. } }) .buildAsync()
Java
MediaController.Builder future = new MediaController.Builder(context, sessionToken) .setListener( new MediaController.Listener() { @Override public void onError(MediaController controller, SessionError sessionError) { // Handle nonfatal error. } });