一、Media3 的前世今生
Jetpack Media3 是媒体库的新家,为 Android 应用程序带来丰富的音频和视觉体验。 Media3 提供了一个具有强大的定制能力、可靠性极高和基于设备优化的简单架构,以消除碎片带来的复杂性。
今年谷歌为 Media3, 也就是安卓最新的媒体播放实现库推出了第一个稳定版本。Media3 比起先前的 Media2, Exoplayer 有着极高的可定制性。
在 Media3 这样的库发布之前之前写一个播放器是非常困难的@w@ 笔者之前的项目使用的是远古库 MediaPlayer, 不仅要手动处理播放清单、随机等逻辑, 和系统交互的 MediaSession 通知也需要自己构建。更不用提 MediaPlayer 还会因为各种奇奇怪怪的理由罢工 2333
感兴趣的读者可以点开上面的链接看看笔者之前的实现。到这个项目 Archive 为止,实际上部分关于媒体服务的 Impl 还没有写完 (蓝牙控制,播放恢复等)
Media3 为了规范播放器的架构将 Playback Notification / Resumption 的实现都包装在了一起, 让实现一个高效且标准的播放器非常容易。
二、Media3 的美好愿景
首先让我们来简单了解一下现在市面上大部分音乐/视频播放器所使用的软件结构:
服务层 活动(Activity)层
Player <-> MediaSession <--------> 媒体控制器 <-> UI
如上面的简例所示,服务层包含了 Player 本体、与 Android 系统 / 活动层 对接的 MediaSession。而活动层包含了与服务对接的媒体控制器还有呈现给用户看的UI界面。
但有些没有实战过的读者可能会问了:
这不是很简单吗???
实际上上面的示意图省去了很多部分,比如 MediaSession 和 Player 之间对接的控制器,媒体控制器还有自己对于播放的接口,需要UI去实现。这导致了一个非常大的问题,结构太过于复杂。代码的复杂性只会导致更多的Bug。
甚至在实现控制器的时候还有多个库可以使用 (ExoPlayer, Media2),这往往会给开发者造成困惑。
Media3 的处理是这样的:
服务层 活动(Activity)层
ExoPlayer <-> MediaSession <--------> 媒体控制器 <-> UI
^ ^
| |
----------------------> Player <------
在 Media3 的理想结构下, 四个部件都不用做连接器处理。它们都直接实现通用播放器的接口。这不仅省去了写一大堆连接器的麻烦, 而且让代码更加易于维护。可能之前我们用 MediaPlayer 实例的时候需要自己处理 Activity 与 Service 之间的通信, 但在 Media3 中, 这些都已经被预制的通用播放器解决了。
你还会发现, 当你采用了 Media3 的架构来写播放器的时候, UI 层和服务层是完全分离的。这意味着更少的错误和更好的独立性。对于一个存活的 Service, 可以有多个播放器界面存在。
同时, Media3 还整合了 ExoPlayer2 作为它可选的播放器实现。
在 Android 13 中, 安卓推送了一项新功能: Playback Resumption (也就是前文所说的播放恢复)。当使用 Playback Resumption 的时候, 应用程序生成的 Media Control 通知 应该常驻于快速设置的面板上, 这样即使播放器根本不在运行, 使用 Media Control 也能迅速开始播放退出时的播放清单。
一些比较知名的播放器,比如自称安卓第一的 Retro Music Player
, 还有 Oto Music
都没有把 Playback Resumption 做好。事实上,根据笔者的观察, 市面上除了笔者自己的 Gramophone
和 Spotify
之外似乎没有任何一个播放器正确的植入了 Playback Resumption。这导致 Playback Resumption 这一功能就等于作废, 停驻的 Media Control 通知并不会迅速恢复播放, 只会 **”假死”**。
幸运的是, Media3 发现了这一现状, 并且提供了相应的接口方便的供开发者来实现 Playback Resumption。
三、实现
服务端
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
class PlaybackService : MediaSessionService() {
private var mediaSession: MediaSession? = null
override fun onCreate() {
val player = ExoPlayer.Builder(this).build()
val audioAttributes: AudioAttributes = AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build()
player.setAudioAttributes(audioAttributes, true)
super.onCreate()
}
override fun onDestroy() {
mediaSession?.run {
player.release()
release()
mediaSession = null
}
super.onDestroy()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = mediaSession
}
如上面的代码段, 这便是一个最简单的 Media3 播放器实例了。我们这里使用的是实现播放功能最简单的 MediaSessionService, 但通常情况下最好使用 MediaLibraryService。MediaLibraryService 能够为其他的使用暴露播放器的媒体库 (Android Auto, 语音助手)。
让我们逐行分解。
- MediaSession
private var mediaSession: MediaSession? = null
/*
...
*/
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = mediaSession
我们在这里定义了一个 mediaSession。这个 session 将作为 Activity 的 UI 层与 Service 层交互的凭证。 Activity 层可以凭借这个 Session 来与播放器通讯, 添加播放清单等。
- Player
val player = ExoPlayer.Builder(this).build() // 处理 Audio Focus val audioAttributes: AudioAttributes = AudioAttributes.Builder() .setUsage(C.USAGE_MEDIA) .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) .build() player.setAudioAttributes(audioAttributes, true)
我们在这里创建了一个 ExoPlayer 的实例。请注意,此处的 ExoPlayer 非彼 ExoPlayer。 ExoPlayer 作为 Media3 的一部分被包含在了 Media3 当中。
而 AudioAttributes 是我们加给 ExoPlayer 的媒体信息,这样当其他的应用需要播放媒体的时候, ExoPlayer 便会自动暂停,当别的应用释放媒体焦点后 ExoPlayer 再自动获取。之前需要用到 AudioManager 来手动处理这一点,现在可以直接通过 AudioAttributes 直接解决。
活动端
活动端的实现很简单,我们只需要在 onStart 中添加以下获取 session 的方法即可。
sessionToken = SessionToken(this, ComponentName(this,GramophonePlaybackService::class.java))
controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
在这里我们获取了一个 controllerFuture,而我们可以通过它来获取 Player 本体。
val player = controllerFuture.get()
举个例子,假如我们想要暂停播放的话,就可以用 player.pause()
来实现。如果以前我们想要更新 UI 内容的话可能会要用到 Handler 或者 Thread。现在我们只需要给 Player 新增一个回调即可:
val playerListener = object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
}
}
controllerFuture.addListener(
{ controllerFuture.get().addListener(playerListener) },
MoreExecutors.directExecutor()
)