媒体会话提供了一种与音频或视频进行互动的通用方式
。在 Media3 中,默认播放器是 ExoPlayer
类,它会实现
Player
接口。将媒体会话连接到播放器可让应用
从外部通告媒体播放,以及从
外部来源。
命令可能源自实体按钮,例如设备上的播放按钮 耳机或电视遥控器。它们也可能来自 媒体控制器,例如指示“暂停”Google 助理。媒体 会将这些命令委托给媒体应用的播放器。
何时选择媒体会话
实现 MediaSession
时,您可以允许用户控制播放:
- 通过耳机。应用中通常会有按钮或触摸交互 用户可在耳机上执行各种操作,以播放/暂停媒体或前往下一个 或上一首曲目
- 通过与 Google 助理对话。常见的模式是说“好的 Google 暂停”,即可暂停设备上当前正在播放的任何媒体。
- 通过 Wear OS 手表操作。这样更便于访问 。
- 通过媒体控件进行设置。此轮播界面显示了 运行中的媒体会话。
- 在电视上播放。允许使用实体播放按钮、平台播放来执行操作 控制和电源管理(例如电视、条形音箱或 A/V 接收器 关闭或切换输入,则播放应在应用中停止)。
- 以及需要影响播放的任何其他外部进程。
这在许多用例中都非常有用。特别要指出的是
使用 MediaSession
:
- 您正在在线播放长视频内容,例如电影或直播电视。
- 您正在在线播放长音频内容,例如播客或音乐 播放列表。
- 您正在构建 TV 应用。
不过,并非所有用例都适合 MediaSession
。你可能需要
在以下情况下,请仅使用 Player
:
- 您正在展示短视频内容,其中涉及用户互动和互动 至关重要
- 没有一个处于活跃状态的视频,例如用户滚动浏览列表 屏幕上会同时显示多个视频
- 您正在播放一次性介绍或解释视频 希望用户积极观看
- 您的内容涉及隐私,并且您不希望外部流程 访问媒体元数据(例如浏览器中的无痕模式)
如果您的用例不属于上述任何情况,请考虑您是否
您的应用在用户未主动互动时继续播放是否合适
与内容关联。如果答案是肯定的,您可以选择
MediaSession
。如果答案是否定的,您可能需要使用 Player
。
创建媒体会话
媒体会话与其管理的播放器共存。您可以构建一个
带有 Context
和 Player
对象的媒体会话。您应该创建
在需要时初始化媒体会话,例如 onStart()
或
Activity
、Fragment
或 onCreate()
的 onResume()
生命周期方法
拥有媒体会话及其关联播放器的 Service
的方法。
如需创建媒体会话,请初始化 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 助理。同样,您可能希望授予 以便更轻松地控制通知和锁定屏幕,或提供给 Wear OS Watch,以便您可以通过表盘控制播放。外部客户端可以 使用媒体控制器向您的媒体应用发出播放命令。这些是 这些命令最终会将命令委派给 媒体播放器。
![演示 MediaSession 和 MediaController 之间的交互的图表。](https://developer.android.google.cn/static/guide/topics/media/images/backgroundcontrols.png?authuser=5&hl=lt)
当控制器即将连接到您的媒体会话时,
onConnect()
方法。您可以使用提供的 ControllerInfo
然后决定是否接受
或拒绝
请求。请参阅有关接受连接请求的示例,请参阅声明
可用命令部分。
连接后,控制器可以向会话发送播放命令。通过
然后将这些命令委托给玩家。播放和播放列表
Player
接口中定义的命令由
会话。
例如,您可以使用其他回调方法处理对
自定义播放命令和
修改播放列表)。
这些回调同样包含一个 ControllerInfo
对象,因此您可以修改
如何针对每个控制方响应每个请求。
修改播放列表
媒体会话可以直接修改其播放器的播放列表,具体说明请参见
该
播放列表的 ExoPlayer 指南。
在以下情况下,控制器也能够修改播放列表:
COMMAND_SET_MEDIA_ITEM
或 COMMAND_CHANGE_MEDIA_ITEMS
可供控制器使用。
向播放列表添加新项目时,播放器通常需要 MediaItem
具有
定义的 URI
让它们具有可玩性默认情况下,新添加的内容会自动转发
player.addMediaItem
等玩家方法(如果定义了 URI)。
如果您要自定义添加到播放器的 MediaItem
实例,则可以执行以下操作:
覆盖
onAddMediaItems()
。
如果您希望支持请求媒体的控制器,则需要执行此步骤
。相反,MediaItem
通常具有
设置以下一个或多个字段,以描述所请求的媒体:
MediaItem.id
:用于标识媒体的通用 ID。MediaItem.RequestMetadata.mediaUri
:可以使用自定义 并且不一定可供播放器直接播放。MediaItem.RequestMetadata.searchQuery
:文本搜索查询,例如 。MediaItem.MediaMetadata
:结构化元数据,例如“title”或“artist”。
如需为全新播放列表提供更多自定义选项,您可以
此外,还可以覆盖
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) ); } ... } }
您可以使用
MediaSession.ControllerInfo
对象的 packageName
属性,
传递给 Callback
方法。这样,您可以根据自己的需求
对特定命令做出响应的行为(如果该命令来自于系统、您的
自己的应用或其他客户端应用
在用户进行互动后更新自定义布局
在处理自定义指令或与播放器的任何其他互动后,
可能需要更新控制器界面中显示的布局。典型示例
是一个切换按钮,可在触发与之相关联的操作后更改其图标
。如需更新布局,您可以使用
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 指南
自定义。
识别发出请求的玩家命令控制器
当 MediaController
发起对 Player
方法的调用时,您可以
使用 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
,以获取
用于发送自定义错误代码、消息或 extra 的监听器:
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. } });