媒体会话提供了一种与音频或视频播放器交互的通用方式。在 Media3 中,默认播放器是 ExoPlayer
类,它实现了 Player
接口。将媒体会话连接到播放器允许应用在外部宣传媒体播放,并接收来自外部来源的播放命令。
命令可能源于物理按钮,例如耳机或电视遥控器上的播放按钮。它们也可能来自具有媒体控制器的客户端应用,例如指示 Google 助理“暂停”。媒体会话将这些命令委派给媒体应用的播放器。
何时选择媒体会话
当您实现 MediaSession
时,您允许用户控制播放
- 通过他们的耳机。用户通常可以在耳机上执行按钮或触摸交互以播放或暂停媒体,或切换到下一首或上一首曲目。
- 通过与Google 助理对话。常见的模式是说“好的 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 库使用播放器的状态自动更新媒体会话。因此,您无需手动处理从播放器到会话的映射。
这与旧方法不同,在旧方法中,您需要独立于播放器本身创建和维护PlaybackStateCompat
,例如指示任何错误。
唯一的会话 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()
的示例实现。
管理自定义布局和自定义命令
以下部分介绍如何向客户端应用宣传自定义命令按钮的自定义布局,以及如何授权控制器发送自定义命令。
定义会话的自定义布局
要指示客户端应用希望向用户显示哪些播放控件,请在服务onCreate()
方法中构建MediaSession
时设置会话的自定义布局。
Kotlin
override fun onCreate() { super.onCreate() val likeButton = CommandButton.Builder() .setDisplayName("Like") .setIconResId(R.drawable.like_icon) .setSessionCommand(SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING)) .build() val favoriteButton = CommandButton.Builder() .setDisplayName("Save to favorites") .setIconResId(R.drawable.favorite_icon) .setSessionCommand(SessionCommand(SAVE_TO_FAVORITES, Bundle())) .build() session = MediaSession.Builder(this, player) .setCallback(CustomMediaSessionCallback()) .setCustomLayout(ImmutableList.of(likeButton, favoriteButton)) .build() }
Java
@Override public void onCreate() { super.onCreate(); CommandButton likeButton = new CommandButton.Builder() .setDisplayName("Like") .setIconResId(R.drawable.like_icon) .setSessionCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING)) .build(); CommandButton favoriteButton = new CommandButton.Builder() .setDisplayName("Save to favorites") .setIconResId(R.drawable.favorite_icon) .setSessionCommand(new SessionCommand(SAVE_TO_FAVORITES, new Bundle())) .build(); Player player = new ExoPlayer.Builder(this).build(); mediaSession = new MediaSession.Builder(this, player) .setCallback(new CustomMediaSessionCallback()) .setCustomLayout(ImmutableList.of(likeButton, favoriteButton)) .build(); }
声明可用的播放器和自定义命令
媒体应用可以定义自定义命令,例如,可以在自定义布局中使用这些命令。例如,您可能希望实现按钮,允许用户将媒体项目保存到收藏列表中。 MediaController
发送自定义命令,而 MediaSession.Callback
接收这些命令。
当 MediaController
连接到您的媒体会话时,您可以定义哪些自定义会话命令可供其使用。您可以通过重写 MediaSession.Callback.onConnect()
来实现此目的。在 onConnect
回调方法中,配置并返回可用的命令集,以接受来自 MediaController
的连接请求。
Kotlin
private inner 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 inner 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
属性来跟踪哪个媒体控制器正在发出请求。这使您可以根据给定命令的来源(来自系统、您自己的应用或其他客户端应用)来调整应用的行为。
用户交互后更新自定义布局
在处理自定义命令或与播放器进行任何其他交互后,您可能希望更新控制器 UI 中显示的布局。一个典型的示例是一个切换按钮,在触发与此按钮关联的操作后,它会更改其图标。要更新布局,您可以使用 MediaSession.setCustomLayout
。
Kotlin
val removeFromFavoritesButton = CommandButton.Builder() .setDisplayName("Remove from favorites") .setIconResId(R.drawable.favorite_remove_icon) .setSessionCommand(SessionCommand(REMOVE_FROM_FAVORITES, Bundle())) .build() mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton))
Java
CommandButton removeFromFavoritesButton = new CommandButton.Builder() .setDisplayName("Remove from favorites") .setIconResId(R.drawable.favorite_remove_icon) .setSessionCommand(new SessionCommand(REMOVE_FROM_FAVORITES, new Bundle())) .build(); mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton));
自定义播放命令行为
要自定义 Player
接口中定义的命令(如 play()
或 seekToNext()
)的行为,请将您的 Player
包装在一个 ForwardingPlayer
中。
Kotlin
val player = ExoPlayer.Builder(context).build() val forwardingPlayer = object : ForwardingPlayer(player) { override fun play() { // Add custom logic super.play() } override fun setPlayWhenReady(playWhenReady: Boolean) { // Add custom logic super.setPlayWhenReady(playWhenReady) } } val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()
Java
ExoPlayer player = new ExoPlayer.Builder(context).build(); ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player) { @Override public void play() { // Add custom logic super.play(); } @Override public void setPlayWhenReady(boolean playWhenReady) { // Add custom logic super.setPlayWhenReady(playWhenReady); } }; MediaSession mediaSession = new MediaSession.Builder(context, forwardingPlayer).build();
有关 ForwardingPlayer
的更多信息,请参阅 ExoPlayer 指南中的 自定义 部分。
识别播放器命令的请求控制器
当对 Player
方法的调用源于 MediaController
时,您可以使用 MediaSession.controllerForCurrentRequest
识别源,并获取当前请求的 ControllerInfo
。
Kotlin
class CallerAwareForwardingPlayer(player: Player) : ForwardingPlayer(player) { override fun seekToNext() { Log.d( "caller", "seekToNext called from package ${session.controllerForCurrentRequest?.packageName}" ) super.seekToNext() } }
Java
public class CallerAwareForwardingPlayer extends ForwardingPlayer { public CallerAwareForwardingPlayer(Player player) { super(player); } @Override public void seekToNext() { Log.d( "caller", "seekToNext called from package: " + session.getControllerForCurrentRequest().getPackageName()); super.seekToNext(); } }
响应媒体按钮
媒体按钮是 Android 设备和其他外围设备(例如蓝牙耳机上的播放/暂停按钮)上的硬件按钮。Media3 会在媒体按钮事件到达会话时为您处理这些事件,并在 会话播放器 上调用相应的 Player
方法。
应用可以通过重写 MediaSession.Callback.onMediaButtonEvent(Intent)
来覆盖默认行为。在这种情况下,应用可以/需要自行处理所有 API 细节。
错误处理和报告
会话会发出两种类型的错误并将其报告给控制器。严重错误报告会话播放器的技术播放故障,该故障会中断播放。严重错误会在发生时自动报告给控制器。非严重错误是非技术性或策略性错误,不会中断播放,并且由应用手动发送给控制器。
严重播放错误
播放器会将严重播放错误报告给会话,然后通过 Player.Listener.onPlayerError(PlaybackException)
和 Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException)
调用报告给控制器。
在这种情况下,播放状态将转换为 STATE_IDLE
,并且 MediaController.getPlaybackError()
返回导致转换的 PlaybackException
。控制器可以检查 PlayerException.errorCode
以获取有关错误原因的信息。
为了实现互操作性,严重错误会复制到平台会话的 PlaybackStateCompat
中,方法是将其状态转换为 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();
转发播放器会向实际播放器注册 Player.Listener
,并拦截报告错误的回调。然后,自定义的 PlaybackException
将委派给注册在转发播放器上的侦听器。为了使此方法有效,转发播放器会重写 Player.addListener
和 Player.removeListener
以访问用于发送自定义错误代码、消息或附加信息的侦听器。
Kotlin
class ErrorForwardingPlayer(private val context: Context, player: Player) : ForwardingPlayer(player) { private val listeners: MutableList<Player.Listener> = mutableListOf() private var customizedPlaybackException: PlaybackException? = null init { player.addListener(ErrorCustomizationListener()) } override fun addListener(listener: Player.Listener) { listeners.add(listener) } override fun removeListener(listener: Player.Listener) { listeners.remove(listener) } override fun getPlayerError(): PlaybackException? { return customizedPlaybackException } private inner class ErrorCustomizationListener : Player.Listener { override fun onPlayerErrorChanged(error: PlaybackException?) { customizedPlaybackException = error?.let { customizePlaybackException(it) } listeners.forEach { it.onPlayerErrorChanged(customizedPlaybackException) } } override fun onPlayerError(error: PlaybackException) { listeners.forEach { it.onPlayerError(customizedPlaybackException!!) } } private 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) } // Apps can customize further error messages by adding more branches. 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) } override fun onEvents(player: Player, events: Player.Events) { listeners.forEach { it.onEvents(player, events) } } // Delegate all other callbacks to all listeners without changing arguments like onEvents. } }
Java
private static class ErrorForwardingPlayer extends ForwardingPlayer { private final Context context; private List<Player.Listener> listeners; @Nullable private PlaybackException customizedPlaybackException; public ErrorForwardingPlayer(Context context, Player player) { super(player); this.context = context; listeners = new ArrayList<>(); player.addListener(new ErrorCustomizationListener()); } @Override public void addListener(Player.Listener listener) { listeners.add(listener); } @Override public void removeListener(Player.Listener listener) { listeners.remove(listener); } @Nullable @Override public PlaybackException getPlayerError() { return customizedPlaybackException; } private class ErrorCustomizationListener implements Listener { @Override public void onPlayerErrorChanged(@Nullable PlaybackException error) { customizedPlaybackException = error != null ? customizePlaybackException(error, context) : null; for (int i = 0; i < listeners.size(); i++) { listeners.get(i).onPlayerErrorChanged(customizedPlaybackException); } } @Override public void onPlayerError(PlaybackException error) { for (int i = 0; i < listeners.size(); i++) { listeners.get(i).onPlayerError(checkNotNull(customizedPlaybackException)); } } private PlaybackException customizePlaybackException( PlaybackException error, Context context) { 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; // Apps can customize further error messages by adding more case statements. 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); } @Override public void onEvents(Player player, Events events) { for (int i = 0; i < listeners.size(); i++) { listeners.get(i).onEvents(player, events); } } // Delegate all other callbacks to all listeners without changing arguments like onEvents. } }
非严重错误
应用可以将并非源自技术异常的非严重错误发送到所有控制器或特定控制器。
Kotlin
val sessionError = SessionError( SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, context.getString(R.string.error_message_authentication_expired), ) // Sending a nonfatal error to all controllers. mediaSession.sendError(sessionError) // Interoperability: Sending a nonfatal error to the media notification controller 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)); // Sending a nonfatal error to all controllers. mediaSession.sendError(sessionError); // Interoperability: Sending a nonfatal error to the media notification controller 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); }
发送到媒体通知控制器的非严重错误会复制到平台会话的 PlaybackStateCompat
中。因此,只有错误代码和错误消息会相应地设置到 PlaybackStateCompat
中,而 PlaybackStateCompat.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. } });