英文:
Can I define a MediaLibraryService without an app?
问题
我想要定义一个MediaLibraryService,使得Android Auto、Exoplayer、AIMP For Android和其他应用程序可以访问并播放我的服务管理的媒体。
我不觉得需要一个UI/Activity。我只想定义一个前台服务,并让各种前端应用找到它。
我正在尝试弄清楚这是否是一个合理的方法。我是否可以只这样做,还是需要创建一个完整的应用程序,其中包含一个带有MediaController/MediaBrowser的Activity?
英文:
I want to define a MediaLibraryService in such a way that Android Auto, Exoplayer, AIMP For Android and other apps can access and play the media my service manages.
I don't see a need for a UI/Activity. I just want to define a foreground service, and have the various front-ends find it.
I am trying to figure out if that is a reasonable approach. Can I just do that, or do I need to make a full app, with an activity with a MediaController/MediaBrowser in it too?
答案1
得分: 1
No, Media3提供了您所需的所有工具和API,使服务成为独立的音乐播放器,因此像Android Auto这样的控制器可以访问您的音乐并播放它,无需实际的活动和其他内容。这意味着您将播放列表存储在服务端,您在服务端执行所有操作,如果您愿意制作一个音乐播放器应用程序,活动可以使用Media3提供的API(例如getChildren
等)从服务中获取信息...
这个库的强大之处在于您可以发送包含几乎一切所需内容的sendCustomCommand
,当您希望在活动和服务之间进行通信时,这将非常有帮助,但是当应用程序通过Android Auto等控制器导航时,这是完全不必要的,不需要实际的移动应用程序。一首歌曲由其MediaId定义,所以我认为这是使歌曲可以播放的全部内容,此外,服务还提供了applicationContext
,因此您可以随心所欲地查询MediaStore,以及使用Datastore或SharedPreferences。
以下是一个示例的MusicLibraryService,您可以从中参考(这是一个稍微复杂的情况,我在其中使用了两个单独的播放列表(一个是tracklist,一个是实际的播放列表,根据用户选择的内容可以单独排队),而且这是一个MedialibraryService,所以层次结构非常重要。最顶层的父根项是不可见的,但它对服务的功能非常重要,因为整个层次结构都是从它开始传播的,如果您拥有一个巨大的播放列表,可以在其下直接包含媒体项,但是层次结构越多,它就越复杂。我建议您一步一步进行,首先使用一个根项并直接在下面添加子项:
class MusicPlayerService : MediaLibraryService() {
// 其他代码...
}
请注意,上述代码中的某些功能未在参考文档中定义,这是因为它们与我们的主题无直接关系,例如scanMusic
调用,它只是一个函数,我在其中查询MediaStore并返回带有查询的媒体项列表的lambda... 因此,请关注实际的Media3 API以及如何将所有内容组合在一起。
英文:
No, Media3 provides you with all the tools and APIs necessary to make the service its own standalone music player, so that controllers like Android Auto can access your music and play it, without the actual need for activities and other things. This means that you store your playlists on the service-side, you do everything on the service-side, and if you're willing to make a music player app, the activity can grab information from the service using APIs that are available by Media3 such as getChildren
...etc
The powerful thing about the library is that you can sendCustomCommand
which contain bundles of pretty much everything you need, this would prove helpful when you want to communicate between an activity and the service, but it's completely unnecessary when the app is being navigated via a controller like Android Auto and not the actual mobile app. A song is defined by its MediaId, so I believe that's all you need to make a song playable, also, services provide you with an applicationContext
so you can query MediaStore all you want, and use Datastore or SharedPreferences all you want as well.
Here's an example MusicLibraryService which you can reference from (this is a bit of an advanced case where I am using two separate playlists (a tracklist and an actual playlist, both of which can be queued separately depending on which one the user picks from), also, it's a MedialibraryService so the hierarchy is very important. The very top-level parent root item is invisible, but it's extremely capital to the functionality of the service, because the whole hierarchy propagates from it, you can include mediaitems right below it if you're having one huge playlist, but the more levels of hierarchy you have the more complicated it gets. I suggest you take it step by step and get yourself familiarzed with MediaLibraryService using one root item and have children below directly first:
class MusicPlayerService : MediaLibraryService() {
lateinit var player: Player
private var session: MediaLibrarySession? = null
private val serviceIOScope = CoroutineScope(Dispatchers.IO)
private val serviceMainScope = CoroutineScope(Dispatchers.Main)
/** This is the root item that is parent to our playlist.
* It is necessary to have a parent item otherwise there is no "library" */
val rootItem = MediaItem.Builder()
.setMediaId(nodeROOT)
.setMediaMetadata(
MediaMetadata.Builder()
.setIsBrowsable(false)
.setIsPlayable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED)
.setTitle("MyMusicAppRootWhichIsNotVisibleToControllers")
.build()
)
.build()
val subroot_TracklistItem = MediaItem.Builder()
.setMediaId(nodeTRACKLIST)
.setMediaMetadata(
MediaMetadata.Builder()
.setIsBrowsable(true)
.setIsPlayable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED)
.setTitle("Tracklist")
.setArtworkUri(
Uri.parse("android.resource://mpappc/drawable/ic_tracklist")
)
.build()
)
.build()
val subroot_PlaylistItem = MediaItem.Builder()
.setMediaId(nodePLAYLIST)
.setMediaMetadata(
MediaMetadata.Builder()
.setIsBrowsable(true)
.setIsPlayable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED)
.setTitle("Playlist")
.setArtworkUri(
Uri.parse("android.resource://mpappc/drawable/ic_tracklist")
)
.build()
)
.build()
val rootHierarchy = listOf(subroot_TracklistItem, subroot_PlaylistItem)
var tracklist = mutableListOf<MediaItem>()
var playlist = mutableListOf<MediaItem>()
var latestSearchResults = mutableListOf<MediaItem>()
/** This will fetch music from the source folder (or the entire device if not specified) */
private fun queryMusic(initial: Boolean = false) {
val sp = PreferenceManager.getDefaultSharedPreferences(applicationContext)
val src = sp.getString("music_src_folder", "") ?: ""
serviceIOScope.launch {
scanMusic(applicationContext, uri = if (src == "") null else src.toUri()) {
tracklist.clear()
tracklist.addAll(it)
if (initial) {
serviceMainScope.launch {
player.setMediaItems(tracklist)
}
}
session?.notifyChildrenChanged(nodeTRACKLIST, tracklist.size, null)
}
}
}
override fun onCreate() {
super.onCreate()
restorePlaylist(applicationContext) {
playlist.clear()
playlist.addAll(it)
session?.notifyChildrenChanged(nodePLAYLIST, playlist.size, null)
}
/** Building ExoPlayer to use FFmpeg Audio Renderer and also enable fast-seeking */
player = ExoPlayer.Builder(applicationContext)
.setSeekParameters(SeekParameters.CLOSEST_SYNC) /* Enabling fast seeking */
.setRenderersFactory(
DefaultRenderersFactory(this).setExtensionRendererMode(
DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER /* We prefer extensions, such as FFmpeg */
)
)
.setWakeMode(C.WAKE_MODE_LOCAL) /* Prevent the service from being killed during playback */
.setHandleAudioBecomingNoisy(true) /* Prevent annoying noise when changing devices */
.setAudioAttributes(AudioAttributes.DEFAULT, true)
.build()
player.repeatMode = Player.REPEAT_MODE_ALL
//Fetching music when the service starts
queryMusic(true)
/** Listening to some player events */
player.addListener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
super.onMediaItemTransition(mediaItem, reason)
//Controlling Android Auto queue here intelligently
if (mediaItem != null && player.mediaItemCount == 1) {
val playlistfootprint = mediaItem.mediaMetadata.extras?.getBoolean("isplaylist", false) == true
if (playlistfootprint && playlist.size > 1) {
val index = playlist.indexOfFirst { it.mediaId == mediaItem.mediaId }
player.setMediaItems(playlist, index, 0)
setPlaybackMode(PlayBackMode.PBM_PLAYLIST)
}
if (!playlistfootprint && tracklist.size > 1) {
val index = tracklist.indexOfFirst { it.mediaId == mediaItem.mediaId }
player.setMediaItems(tracklist, index, 0)
setPlaybackMode(PlayBackMode.PBM_TRACKLIST)
}
}
}
override fun onPlayerError(error: PlaybackException) {
error.printStackTrace()
Log.e("PLAYER", error.stackTraceToString())
}
})
/** Creating our MediaLibrarySession which is an advanced extension of a MediaSession */
session = MediaLibrarySession
.Builder(this, player, object : MediaLibrarySession.Callback {
override fun onGetItem(session: MediaLibrarySession, browser: MediaSession.ControllerInfo, mediaId: String): ListenableFuture<LibraryResult<MediaItem>> {
return super.onGetItem(session, browser, mediaId)
}
override fun onSetMediaItems(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItems: MutableList<MediaItem>,
startIndex: Int,
startPositionMs: Long
): ListenableFuture<MediaItemsWithStartPosition> {
val newItems = mediaItems.map {
it.buildUpon().setUri(it.mediaId).build()
}.toMutableList()
return super.onSetMediaItems(mediaSession, controller, newItems, startIndex, startPositionMs)
}
override fun onGetLibraryRoot(session: MediaLibrarySession, browser: MediaSession.ControllerInfo, params: LibraryParams?): ListenableFuture<LibraryResult<MediaItem>> {
return Futures.immediateFuture(LibraryResult.ofItem(rootItem, params))
}
override fun onGetChildren(
session: MediaLibrarySession, browser: MediaSession.ControllerInfo,
parentId: String, page: Int, pageSize: Int, params: LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
return Futures.immediateFuture(
LibraryResult.ofItemList(
when (parentId) {
nodeROOT -> rootHierarchy
nodeTRACKLIST -> tracklist
nodePLAYLIST -> playlist
else -> rootHierarchy
},
params
)
)
}
override fun onSubscribe(session: MediaLibrarySession, browser: MediaSession.ControllerInfo, parentId: String, params: LibraryParams?): ListenableFuture<LibraryResult<Void>> {
session.notifyChildrenChanged(
parentId,
when (parentId) {
nodeROOT -> 2
nodeTRACKLIST -> tracklist.size
nodePLAYLIST -> playlist.size
else -> 0
},
params
)
return Futures.immediateFuture(LibraryResult.ofVoid()) //super.onSubscribe(session, browser, parentId, params)
}
/** In order to end the service from our media browser side (UI side), we receive
* our own custom command (which is [CUSTOM_COM_END_SERVICE]). However, the session
* is not designed to accept foreign weird commands. So we edit the onConnect callback method
* to make sure it accepts it.
*/
override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
val sessionComs = super.onConnect(session, controller).availableSessionCommands
.buildUpon()
.add(CUSTOM_COM_PLAY_ITEM) //Command executed when an item is requested to play
.add(CUSTOM_COM_END_SERVICE) //This one is called to end the service manually from the UI
.add(CUSTOM_COM_PLAYLIST_ADD) //Command used when adding items to playlist
.add(CUSTOM_COM_PLAYLIST_REMOVE) //Command used when removing items from playlist
.add(CUSTOM_COM_PLAYLIST_CLEAR) //Command used when clearing all items from playlist
.add(CUSTOM_COM_SCAN_MUSIC) //Command use to execute a music scan
.add(CUSTOM_COM_TRACKLIST_FORGET) //Used when an item is to be forgotten (swipe left)
.build()
val playerComs = super.onConnect(session, controller).availablePlayerCommands
return MediaSession.ConnectionResult.accept(sessionComs, playerComs)
}
/** Receiving some custom commands such as the command that ends the service.
* In order to make the player accept newly customized foreign weird commands, we have
* to edit the onConnect callback method like we did above */
override fun onCustomCommand(
session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
/** When the controller tries to add an item to the playlist */
if (customCommand == CUSTOM_COM_PLAY_ITEM) {
args.getString("id")?.let { mediaid ->
if (args.getBoolean("playlist", false)) {
val i = playlist.indexOfFirst { it.mediaId == mediaid }
setPlaybackMode(PlayBackMode.PBM_PLAYLIST)
player.setMediaItems(playlist, i, 0)
} else {
val i = tracklist.indexOfFirst { it.mediaId == mediaid }
setPlaybackMode(PlayBackMode.PBM_TRACKLIST)
player.setMediaItems(tracklist, i, 0)
}
player.prepare()
player.play()
return Futures.immediateFuture(SessionResult(RESULT_SUCCESS))
}
}
/** When the controller (like the app) closes fully, we need to disconnect */
if (customCommand == CUSTOM_COM_END_SERVICE) {
session.release()
player.release()
this@MusicPlayerService.stopSelf()
return Futures.immediateFuture(SessionResult(RESULT_SUCCESS))
}
/** When the user changes the source folder */
if (customCommand == CUSTOM_COM_SCAN_MUSIC) {
queryMusic()
return Futures.immediateFuture(SessionResult(RESULT_SUCCESS))
}
/** When the controller tries to add an item to the playlist */
if (customCommand == CUSTOM_COM_PLAYLIST_ADD) {
args.getString("id")?.let { mediaid ->
tracklist.firstOrNull { it.mediaId == mediaid }?.let { itemToAdd ->
itemToAdd.playlistFootprint(true)
playlist.add(itemToAdd)
serviceIOScope.launch {
/** notifying UI-end that the playlist has been modified */
this@MusicPlayerService.session?.apply {
notifyChildrenChanged(
controller,
nodePLAYLIST,
playlist.size,
null
)
}
/** Saving the playlist to memory as it is now */
snapshotPlaylist(playlist)
}
return Futures.immediateFuture(SessionResult(RESULT_SUCCESS))
}
}
}
/** When the controller tries to remove an item from the playlist */
if (customCommand == CUSTOM_COM_PLAYLIST_REMOVE) {
args.getString("id")?.let { mediaid ->
playlist.firstOrNull { it.mediaId == mediaid }?.let { itemToRemove ->
playlist.remove(itemToRemove)
serviceIOScope.launch {
/** notifying UI-end that the playlist has been modified */
this@MusicPlayerService.session?.apply {
notifyChildrenChanged(
controller,
nodePLAYLIST,
playlist.size,
null
)
}
/** Saving the playlist to memory as it is now */
snapshotPlaylist(playlist)
}
return Futures.immediateFuture(SessionResult(RESULT_SUCCESS))
}
}
}
/** When the controller tries to clear the playlist */
if (customCommand == CUSTOM_COM_PLAYLIST_CLEAR) {
playlist.clear()
this@MusicPlayerService.session?.apply {
notifyChildrenChanged(
controller,
nodePLAYLIST,
0,
null
)
}
/** Saving the playlist to memory as it is now */
snapshotPlaylist(playlist)
return Futures.immediateFuture(SessionResult(RESULT_SUCCESS))
}
}
}
return super.onCustomCommand(session, controller, customCommand, args)
}
})
.build()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? {
return session
}
override fun onDestroy() {
snapshotPlaylist(playlist)
session?.run {
player.release()
release()
session = null
}
super.onDestroy()
}
Please note that some functions are not defined in the reference above, that's because they're not directly relevant to our topic, for example the scanMusic
call, it's just a function where I query MediaStore and return a lambda with a list of queried mediaitems... So do focus on the actual media3 APIs and how everything is put together
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论