Media3 MediaSession

55 阅读3分钟

Media3 MediaSession

MediaLibrarySession extends MediaSession

MediaSession 是一种会话,允许媒体应用程序向其他进程(包括Android框架和其他应用程序)公开其播放器功能、播放列表信息和当前正在播放的媒体项目。

MediaLibrarySession 增加了媒体内容浏览功能。

MediaLibrarySession.Callback extends MediaSession.Callback

MediaSession.Callback 中需要关注的有

  • onPlaybackResumption(),和 MediaButtonReceiver 一同实现恢复播放功能。

MediaLibrarySession.Callback 中需要关注的有

  • onGetLibraryRoot(),用于在客户端请求内容树的根 MediaItem 时。
  • onGetChildren(),用于在客户端请求内容树中 MediaItem 的子项时。
  • onGetSearchResult(),用于在客户端请求内容树中针对给定查询的搜索结果时。

MediaLibraryService extends MediaSessionService extends Service

MediaSessionService 用于在后台播放音频。 使用 ArrayMap 管理访问过来的 MediaSession 。

MediaLibraryService 提供媒体内容库。

MediaBrowser extends MediaController

二者都是通过 Context#bindService() 连接对应服务。


使用 MediaSessionService 实现在后台播放音频。

使用 MediaSessionService 可以让 Google 助理、系统媒体控件、外围设备上的媒体按钮或 Wear OS 等配套设备等外部客户端发现您的服务、连接到该服务并控制播放,而根本无需访问应用的界面 activity。

支持多个客户端应用同时连接到同一个 MediaSessionService。

MediaSessionService 继承自 Service。

public abstract class MediaSessionService extends Service { }

需要关注的生命周期有

  • onCreate
  • onDestroy

分析 MediaSessionService 源码发现。

在 onCreate 中创建了 MediaSessionServiceStub。

在 onDestroy 中释放了 MediaSessionServiceStub。

MediaSessionServiceStub 是一个 AIDL 接口。

private static final class MediaSessionServiceStub extends IMediaSessionService.Stub { }

用于 MediaController 到 MediaSessionService 的通信,即客户端和 MediaSessionService 的进程间通信。

IMediaSessionService 中提供了客户端即 IMediaController 访问到本服务的接口。

import androidx.media3.session.IMediaController;

oneway interface IMediaSessionService {
  void connect(IMediaController caller, in Bundle connectionRequest) = 3000;
}

查看 connect 的具体实现,就可以知道 MediaSessionServiceStub 是如何处理不同进程下的客户端了。

  • IMediaController 为空时直接返回,不做任何处理。请求信息为空时,触发 IMediaController 的断开连接。
if (caller == null || connectionRequestBundle == null) {
  SessionUtil.disconnectIMediaController(caller);
  return;
}
  • 解析请求信息出错时,即未按格式要求传递信息时,触发 IMediaController 的断开连接。
ConnectionRequest request;
try {
  request = ConnectionRequest.fromBundle(connectionRequestBundle);
} catch (RuntimeException e) {
  // Malformed call from potentially malicious controller.
  Log.w(TAG, "Ignoring malformed Bundle for ConnectionRequest", e);
  SessionUtil.disconnectIMediaController(caller);
  return;
}
  • 获取弱引用中的 MediaSessionService,若获取失败即 MediaSessionService 执行 onDestroy 周期后,触发 IMediaController 的断开连接。
@Nullable MediaSessionService mediaSessionService = serviceReference.get();
if (mediaSessionService == null) {
  SessionUtil.disconnectIMediaController(caller);
  return;
}
  • 使用调用方即客户端的 pid、uid、包名 来区分不同的 用户信息。
int callingPid = Binder.getCallingPid();
int uid = Binder.getCallingUid();
long token = Binder.clearCallingIdentity();
int pid = (callingPid != 0) ? callingPid : request.pid;
MediaSessionManager.RemoteUserInfo remoteUserInfo =
  new MediaSessionManager.RemoteUserInfo(request.packageName, pid, uid);
  • 判断当前用户是否值得信任,即是否具有MEDIA_CONTENT_CONTROL权限或者STATUS_BAR_SERVICE权限,或者是系统应用,或者是该应用 enabled_notification_listeners。
boolean isTrusted =
  MediaSessionManager.getSessionManager(mediaSessionService.getApplicationContext())
    .isTrustedForMediaControl(remoteUserInfo);
  • 将跨进程调用过来的客户端统一放入线程安全的 HashSet 中。
// private final Set<IMediaController> pendingControllers = Collections.synchronizedSet(new HashSet<>());
pendingControllers.add(caller);
  • 通过 Handler 交给 MediaSessionService 的主线程逐一处理。
// private final Handler handler = new Handler(context.getMainLooper());
handler.post(()->{
    // 处理其他进程调用过来的 IMediaController
})
  • 处理来自MediaSessionService的控制器连接请求。
// 通过 synchronized (mutex) {return c.remove(o);} 保障移除时的线程安全。
pendingControllers.remove(caller);
boolean connected = false;
try {
  @Nullable MediaSessionService service = serviceReference.get();
  if (service == null) {
    return;
  }
  // 调用方进程里的 控制器信息
  ControllerInfo controllerInfo =
      new ControllerInfo(
        remoteUserInfo,
        request.libraryVersion,
        request.controllerInterfaceVersion,
        isTrusted,
        new MediaSessionStub.Controller2Cb(
            caller, request.controllerInterfaceVersion),
        request.connectionHints,
        request.maxCommandsForMediaItems);

  // 需子类具体实现,获取一个媒体会话。
  @Nullable MediaSession session = service.onGetSession(controllerInfo);
  if (session == null) {
    return;
  }
  // 将有效的媒体会话统一放入ArrayMap中管理。
  service.addSession(session);
  // 处理来自MediaSessionService的控制器连接请求。
  session.handleControllerConnectionFromService(caller, controllerInfo);
  connected = true;
} catch (Exception e) {
  // Don't propagate exception in service to the controller.
  Log.w(TAG, "Failed to add a session to session service", e);
} finally {
  if (!connected) {
    SessionUtil.disconnectIMediaController(caller);
  }
}