5

TL;DR:我已成功创建并(通过订阅)将活动耦合到媒体浏览器服务。此媒体浏览器服务可以继续运行并在后台播放音乐。我希望能够在某个阶段刷新内容,无论是在应用程序再次进入前台时还是在 SwipeRefreshLayout 事件期间。

我想实现以下功能:

  1. 启动 MediaBrowserServiceCompat 服务。
  2. 从活动中,连接并订阅媒体浏览器服务。
  3. 允许该服务在应用关闭时继续运行和播放音乐。
  4. 在稍后阶段,或在 SwipeRefreshLayout 事件中,重新连接并订阅服务以获取新鲜内容。

我收到的问题是在 MediaBrowserService 中(在创建订阅后)您只能从 onLoadChildren() 方法调用一次 sendResult() ,因此下次尝试使用相同的根订阅媒体浏览器服务时,第二次调用 sendResult() 时会出现以下异常:

E/UncaughtException: java.lang.IllegalStateException: sendResult() called when either sendResult() or sendError() had already been called for: MEDIA_ID_ROOT
                                                    at android.support.v4.media.MediaBrowserServiceCompat$Result.sendResult(MediaBrowserServiceCompat.java:602)
                                                    at com.roostermornings.android.service.MediaService.loadChildrenImpl(MediaService.kt:422)
                                                    at com.roostermornings.android.service.MediaService.access$loadChildrenImpl(MediaService.kt:50)
                                                    at com.roostermornings.android.service.MediaService$onLoadChildren$1$onSyncFinished$playerEventListener$1.onPlayerStateChanged(MediaService.kt:376)
                                                    at com.google.android.exoplayer2.ExoPlayerImpl.handleEvent(ExoPlayerImpl.java:422)
                                                    at com.google.android.exoplayer2.ExoPlayerImpl$1.handleMessage(ExoPlayerImpl.java:103)
                                                    at android.os.Handler.dispatchMessage(Handler.java:102)
                                                    at android.os.Looper.loop(Looper.java:150)
                                                    at android.app.ActivityThread.main(ActivityThread.java:5665)
                                                    at java.lang.reflect.Method.invoke(Native Method)
                                                    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:822)
                                                    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:712)

我调用以下方法来连接和断开媒体浏览器(同样,在第一次连接时一切运行顺利,但在第二次连接时,我不确定如何通过订阅刷新内容):

override fun onStart() {
        super.onStart()

        mMediaBrowser = MediaBrowserCompat(this, ComponentName(this, MediaService::class.java), connectionCallback, null)

        if (!mMediaBrowser.isConnected)
            mMediaBrowser.connect()
}

override fun onPause() {
        super.onPause()

        //Unsubscribe and unregister MediaControllerCompat callbacks
        MediaControllerCompat.getMediaController(this@DiscoverFragmentActivity)?.unregisterCallback(mediaControllerCallback)
        if (mMediaBrowser.isConnected) {
            mMediaBrowser.unsubscribe(mMediaBrowser.root, subscriptionCallback)
            mMediaBrowser.disconnect()
        }
}

我在 onPause() 而不是 onDestroy() 中取消订阅并断开连接,以便即使活动保留在后台堆栈上也会重新创建订阅。

分别在活动和服务中用于滑动刷新的实际方法:

活动

if (mMediaBrowser.isConnected)
        mMediaController?.sendCommand(MediaService.Companion.CustomCommand.REFRESH.toString(), null, null)

服务

inner class MediaPlaybackPreparer : MediaSessionConnector.PlaybackPreparer {

    ...

    override fun onCommand(command: String?, extras: Bundle?, cb: ResultReceiver?) {
        when(command) {
            // Refresh media browser content and send result to subscribers
            CustomCommand.REFRESH.toString() -> {
                notifyChildrenChanged(MEDIA_ID_ROOT)
            }
        }
    }}

其他研究:

我参考了 Github 上的 Google Samples 代码,以及...

在创建媒体浏览器服务并且活动至少订阅一次后,上述存储库似乎都没有处理刷新内容的问题 - 我想避免重新启动服务,以便音乐可以在后台继续播放。

可能的相关问题:

4

3 回答 3

3

调用你的音乐服务实现notifyChildrenChanged(String parentId)将触发onLoadChildren和 在里面,你可以用 . 发送不同的结果result.sendResult()

我所做的是在BroadcastReceiver我的音乐服务中添加了一个,并且在其中,我只是调用了notifyChildrenChanged(String parentId). 在我的活动中,我在更改音乐列表时发送了一个广播。

于 2019-05-10T07:51:49.980 回答
1

可选(不推荐)快速修复

音乐服务 ->

companion object {
    var musicServiceInstance:MusicService?=null
}

override fun onCreate() {
    super.onCreate()
    musicServiceInstance=this
}

//api call
fun fetchSongs(params:Int){
    serviceScope.launch {
        firebaseMusicSource.fetchMediaData(params)

        //Edit Data or Change Data
         notifyChildrenChanged(MEDIA_ROOT_ID)
    }
}

视图模型->

fun fetchSongs(){
    MusicService.musicServiceInstance?.let{
      it.fetchSongs(params)
     }
}

可选(推荐)

音乐播放准备器

class MusicPlaybackPreparer (
private val firebaseMusicSource: FirebaseMusicSource,
private val serviceScope: CoroutineScope,
private val exoPlayer: SimpleExoPlayer,
private val playerPrepared: (MediaMetadataCompat?) -> Unit

) : MediaSessionConnector.PlaybackPreparer {

override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?
): Boolean {
    when(command){
         //edit data or fetch more data from api
        "Add Songs"->{
            serviceScope.launch {
                firebaseMusicSource.fetchMediaData()
            }
         }
       
    }
    return false
}


override fun getSupportedPrepareActions(): Long {
    return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
            PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
}

override fun onPrepare(playWhenReady: Boolean) = Unit

override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
    firebaseMusicSource.whenReady {
        val itemToPlay = firebaseMusicSource.songs.find { mediaId == it.description.mediaId }
        playerPrepared(itemToPlay)
    }
}

override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) = Unit

override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) = Unit

}

音乐服务连接

fun sendCommand(command: String, parameters: Bundle?) =
    sendCommand(command, parameters) { _, _ -> }

private fun sendCommand(
    command: String,
    parameters: Bundle?,
    resultCallback: ((Int, Bundle?) -> Unit)
) = if (mediaBrowser.isConnected) {
    mediaController.sendCommand(command, parameters, object : ResultReceiver(Handler()) {
        override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
            resultCallback(resultCode, resultData)
        }
    })
    true
} else {
    false
}

视图模型

 fun fetchSongs(){
    val args = Bundle()
    args.putInt("nRecNo", 2)
    musicServiceConnection.sendCommand("Add Songs", args )
}

音乐服务 ->

 override fun onLoadChildren(
    parentId: String,
    result: Result<MutableList<MediaBrowserCompat.MediaItem>>
) {
    when(parentId) {
        MEDIA_ROOT_ID -> {
            val resultsSent = firebaseMusicSource.whenReady { isInitialized ->
                if(isInitialized) {
                    try {
                        result.sendResult(firebaseMusicSource.asMediaItems())
                        if(!isPlayerInitialized && firebaseMusicSource.songs.isNotEmpty()) {
                            preparePlayer(firebaseMusicSource.songs, firebaseMusicSource.songs[0], true)
                            isPlayerInitialized = true
                        }
                    }
                   catch (exception: Exception){
                       // not recommend to notify here , instead notify when you 
                       // change existing list in MusicPlaybackPreparer onCommand()
                       notifyChildrenChanged(MEDIA_ROOT_ID)
                   }
                } else {
                    result.sendResult(null)
                }
            }
            if(!resultsSent) {
                result.detach()
            }
        }
    }
}
于 2021-02-11T10:43:14.613 回答
0

我的问题与 MediaBrowserServiceCompat 类无关。问题的出现是因为我正在调用result.detach()以实现一些异步数据获取,而我使用的侦听器同时传入了 onLoadChildren 方法中的parentId和变量,并分配了 final而不是.resultvalvar

我仍然不完全理解为什么会发生这种情况,是否是Player.EventListener在另一个异步网络调用侦听器中使用 a 的潜在结果,但解决方案是创建并分配一个变量(也许其他人可以解释这种现象):

// Create variable
var currentResult: Result<List<MediaBrowserCompat.MediaItem>>? = null

override fun onLoadChildren(parentId: String, result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>) {
    // Use result.detach to allow calling result.sendResult from another thread
    result.detach()
    // Assign returned result to temporary variable
    currentResult = result
    currentParentId = parentId

    // Create listener for network call
    ChannelManager.onFlagChannelManagerDataListener = object : ChannelManager.Companion.OnFlagChannelManagerDataListener {
       override fun onSyncFinished() {
            // Create a listener to determine when player is prepared
            val playerEventListener = object : Player.EventListener {

                override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
                     when(playbackState) {
                        Player.STATE_READY -> {
                            if(mPlayerPreparing) {
                                // Prepare content to send to subscribed content
                                loadChildrenImpl(currentParentId, currentResult as MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>)
                                mPlayerPreparing = false
                            }
                        }
                        ...
                     }
                }
       }

    }
于 2017-10-09T10:17:50.097 回答