我可以在没有应用程序的情况下定义 MediaLibraryService 吗?

问题描述 投票:0回答:1

我想定义一个 MediaLibraryService,以便 Android Auto、Exoplayer、AIMP For Android 和其他应用程序可以访问和播放我的服务管理的媒体。

我认为不需要 UI/Activity。我只是想定义一个前台服务,然后让各个前端找到它。

我想弄清楚这是否是一个合理的方法。我可以这样做吗,还是我需要制作一个完整的应用程序,其中也包含带有 MediaController/MediaBrowser 的 Activity?

android android-service androidx media-library
1个回答
0
投票

不,Media3 为您提供了使该服务成为自己的独立音乐播放器所需的所有工具和 API,以便像 Android Auto 这样的控制器可以访问您的音乐并播放它,而不需要实际的活动和其他东西。这意味着您将播放列表存储在服务端,您在服务端执行所有操作,如果您愿意制作音乐播放器应用程序,则该 Activity 可以使用 Media3 提供的 API 从服务中获取信息比如

getChildren
...等等

该库的强大之处在于,您可以

sendCustomCommand
其中包含几乎所有您需要的内容,当您想要在活动和服务之间进行通信时,这将很有帮助,但在导航应用程序时完全没有必要通过 Android Auto 等控制器,而不是实际的移动应用程序。一首歌曲是由它的 MediaId 定义的,所以我相信这就是让一首歌曲可播放所需的全部,而且,服务为您提供了
applicationContext
,这样您就可以查询 MediaStore 所需的一切,并使用 Datastore 或 SharedPreferences 所需的一切.

这里有一个 MusicLibraryService 示例,您可以参考(这是一种高级情况,我使用两个单独的播放列表(一个曲目列表和一个实际播放列表,这两个列表都可以单独排队,具体取决于用户选择的播放列表)另外,它是一个 MedialibraryService,因此层次结构非常重要。最顶层的父根项是不可见的,但它对于服务的功能非常重要,因为整个层次结构从它传播,您可以在其正下方包含媒体项如果你有一个巨大的播放列表,但层次结构的级别越多,它就会变得越复杂。我建议你一步一步地使用一个根项来熟悉 MediaLibraryService,并首先直接在下面设置子项:

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()
                        [email protected]()

                        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 */
                                    [email protected]?.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 */
                                    [email protected]?.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()
                        [email protected]?.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()
    }
© www.soinside.com 2019 - 2024. All rights reserved.