Обновление содержимого подписки MediaBrowserService
TL;DR: я успешно создал и связал (через подписку) действие со службой медиабраузера. Эта медиабраузерная служба может продолжать работать и воспроизводить музыку в фоновом режиме. Я хотел бы иметь возможность обновлять содержимое на каком-то этапе, когда приложение снова выходит на передний план или во время события SwipeRefreshLayout.
У меня есть следующие функции, которые я хотел бы реализовать:
- Запустите службу MediaBrowserServiceCompat.
- Из действия подключитесь и подпишитесь на службу медиабраузера.
- Разрешить службе продолжать работать и воспроизводить музыку, пока приложение закрыто.
- На более позднем этапе или в событии SwipeRefreshLayout переподключитесь и подпишитесь на службу, чтобы получить свежий контент.
Проблема, которую я получаю, заключается в том, что в MediaBrowserService (после создания подписки) вы можете вызывать sendResult() только один раз из метода onLoadChildren(), поэтому при следующей попытке подписаться на службу медиабраузера, используя тот же корень вы получаете следующее исключение, когда 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)
}
}
}}
Другие исследования:
Я сослался на код Google Samples на Github, а также...
- https://github.com/googlesamples/android-MediaBrowserService
- https://github.com/moondroid/UniversalMusicPlayer
Похоже, что ни одно из вышеприведенных репозиториев не решает проблему обновления контента после того, как медиа-браузер был создан, а действие подписано хотя бы один раз - я бы хотел избежать перезапуска сервиса, чтобы музыка могла продолжать проигрываться в фоновом режиме.
Возможные связанные проблемы:
3 ответа
Звоню вам реализации музыкального сервиса notifyChildrenChanged(String parentId)
вызовет onLoadChildren
и внутри вы можете отправить другой результат с помощью result.sendResult()
.
Что я сделал, так это то, что добавил BroadcastReceiver
в свой музыкальный сервис и внутри я просто позвонил в notifyChildrenChanged(String parentId)
. А внутри своей Activity я отправлял трансляцию, когда менял список музыки.
Необязательно (не рекомендуется) Быстрое исправление
Музыкальный Сервис ->
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)
}
}
ViewModel ->
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
}
ViewModel
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()
}
}
}
}
Моя проблема не была связана с классом MediaBrowserServiceCompat. Проблема возникла, потому что я звонил result.detach()
для того, чтобы реализовать некоторую асинхронную выборку данных, и слушатель, которого я использовал, имел оба parentId
а также result
переменные из метода onLoadChildren, переданные и присвоенные final val
скорее, чем var
,
Я до сих пор не до конца понимаю, почему это происходит, является ли это основным результатом использования Player.EventListener
в другом слушателе асинхронного сетевого вызова, но решением было создать и назначить переменную (и, возможно, кто-то еще может объяснить это явление):
// 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
}
}
...
}
}
}
}