使用 exoplayer 关闭应用程序时保持音乐播放器播放

问题描述

我有一个用 exoplayer 构建的音乐应用。

我想:

即使关闭应用程序也能继续播放歌曲。

我还想在通知托盘中添加一个关闭 (X) 图标(如果可能,用户可以从那里关闭媒体会话)

现在,当我关闭应用程序时。歌曲停止播放。

音乐服务

@AndroidEntryPoint
class MusicService : MediabrowserServiceCompat() {

@Inject
lateinit var dataSourceFactory: DefaultDataSourceFactory

@Inject
lateinit var exoPlayer: SimpleExoPlayer

@Inject
lateinit var musicSource: MusicSource

private lateinit var musicnotificationmanager: Musicnotificationmanager

private val serviceJob = Job()
private val serviceScope = Coroutinescope(dispatchers.Main + serviceJob)

private lateinit var mediaSession: MediaSessionCompat
private lateinit var mediaSessionConnector: MediaSessionConnector

var isForegroundService = false

private var currPlayingSong: MediaMetadataCompat? = null

private var isPlayerInitialized = false


private lateinit var musicPlayerEventListener: MusicPlayerEventListener

companion object {
    var curSongDuration = 0L
        private set
}


override fun onCreate() {
    super.onCreate()

    serviceScope.launch {
        musicSource.fetchMediaData()
    }


    val activityIntent = packageManager?.getLaunchIntentForPackage(packageName)?.let {
        PendingIntent.getActivity(this,it,0)
    }

    mediaSession = MediaSessionCompat(this,SERVICE_TAG).apply {
        setSessionActivity(activityIntent)
        isActive = true
    }

    sessionToken = mediaSession.sessionToken

    musicnotificationmanager = Musicnotificationmanager(
        this,mediaSession.sessionToken,MusicPlayerNotificationListener(this)
    ) {
        curSongDuration = exoPlayer.duration
    }

    val musicPlayBackPreparer = MusicPlaybackPreparer(musicSource) {
        currPlayingSong = it
        preparePlayer(
            musicSource.songs,true
        )
    }

    mediaSessionConnector = MediaSessionConnector(mediaSession)
    mediaSessionConnector.setPlaybackPreparer(musicPlayBackPreparer)
    mediaSessionConnector.setQueueNavigator(MusicQueueNavigator())
    mediaSessionConnector.setPlayer(exoPlayer)

    musicPlayerEventListener = MusicPlayerEventListener(this)
    exoPlayer.addListener(musicPlayerEventListener)
    musicnotificationmanager.showNotification(exoPlayer)

}


private inner class MusicQueueNavigator : TimelineQueueNavigator(mediaSession) {
    override fun getMediaDescription(player: Player,windowIndex: Int): MediaDescriptionCompat {
        return musicSource.songs[windowIndex].description
    }
}

private fun preparePlayer(
    songs: List<MediaMetadataCompat>,itemToPlay: MediaMetadataCompat?,playNow: Boolean
) {

    val curSongIndex = if (currPlayingSong == null) 0 else songs.indexOf(itemToPlay)
    exoPlayer.prepare(musicSource.asMediaSource(dataSourceFactory))
    exoPlayer.seekTo(curSongIndex,0L)
    exoPlayer.playWhenReady = playNow

}

override fun onTaskRemoved(rootIntent: Intent?) {
    super.onTaskRemoved(rootIntent)
    exoPlayer.stop()
}

override fun onDestroy() {
    super.onDestroy()
    serviceScope.cancel()

    exoPlayer.removeListener(musicPlayerEventListener)
    exoPlayer.release()

}

override fun onGetRoot(
    clientPackageName: String,clientUid: Int,rootHints: Bundle?
): browserRoot {
    return browserRoot(MEDIA_ROOT_ID,null)
}

override fun onLoadChildren(
    parentId: String,result: Result<MutableList<MediabrowserCompat.MediaItem>>
) {
    when (parentId) {
        MEDIA_ROOT_ID -> {
            val resultsSent = musicSource.whenReady { isInitialized ->
                if (isInitialized) {
                    result.sendResult(musicSource.asMediaItem())
                    if (!isInitialized && musicSource.songs.isNotEmpty()) {
                        preparePlayer(musicSource.songs,musicSource.songs[0],false)
                        isPlayerInitialized = true
                    }
                } else {
                    mediaSession.sendSessionEvent(NETWORK_ERROR,null)
                    result.sendResult(null)
                }
            }
            if (!resultsSent) {
                result.detach()
            }
        }
    }
 }
}

音乐服务连接

class MusicServiceConnection(context: Context) {

private val _isConnected = mutablelivedata<Event<Resource<Boolean>>>()
val isConnected: LiveData<Event<Resource<Boolean>>> = _isConnected

private val _networkError = mutablelivedata<Event<Resource<Boolean>>>()
val networkError: LiveData<Event<Resource<Boolean>>> = _networkError

private val _playbackState = mutablelivedata<PlaybackStateCompat?>()
val playbackState: LiveData<PlaybackStateCompat?> = _playbackState

private val _currentPlayingSong = mutablelivedata<MediaMetadataCompat?>()
val currentPlayingSong: LiveData<MediaMetadataCompat?> = _currentPlayingSong

lateinit var mediaControllerCompat: MediaControllerCompat

private val mediabrowserControllerCallback = MediabrowserConnectionCallback(context)

private val mediabrowser = MediabrowserCompat(
    context,ComponentName(
        context,MusicService::class.java
    ),mediabrowserControllerCallback,null
).apply { connect() }

val transportControls : MediaControllerCompat.TransportControls
get() = mediaControllerCompat.transportControls

fun subscribe(parentId: String,callback: MediabrowserCompat.SubscriptionCallback){
    mediabrowser.subscribe(parentId,callback)
}

fun unSubscribe(parentId: String,callback: MediabrowserCompat.SubscriptionCallback){
    mediabrowser.unsubscribe(parentId,callback)
}


private inner class MediabrowserConnectionCallback(
    private val context: Context
): MediabrowserCompat.ConnectionCallback(){

    override fun onConnected() {
        mediaControllerCompat = MediaControllerCompat(context,mediabrowser.sessionToken).apply {
            registerCallback(MediaControllerCallback())
        }
        _isConnected.postValue(Event(Resource.success(true)))
    }

    override fun onConnectionSuspended() {
        _isConnected.postValue(Event(Resource.error("Data was suspended",false)))
    }

    override fun onConnectionFailed() {
        _isConnected.postValue(Event(Resource.error("Couldn't connect",false)))
    }
}

private inner class MediaControllerCallback: MediaControllerCompat.Callback(){

    override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
        _playbackState.postValue(state)
    }

    override fun onMetadataChanged(Metadata: MediaMetadataCompat?) {
        _currentPlayingSong.postValue(Metadata)
    }

    override fun onSessionEvent(event: String?,extras: Bundle?) {
        super.onSessionEvent(event,extras)
        when(event){

            NETWORK_ERROR -> _networkError.postValue(
                Event(
                    Resource.error(
                        "Something went wrong",null
                    )
                )
            )

        }
    }


    override fun onSessionDestroyed() {
        mediabrowserControllerCallback.onConnectionSuspended()
    }
  }
}

Musicnotificationmanager

class Musicnotificationmanager(
private val context: Context,sessionToken: MediaSessionCompat.Token,notificationListener: Playernotificationmanager.NotificationListener,private val newSongCallback: () -> Unit
) {
private val notificationmanager: Playernotificationmanager
init {
    val mediaController = MediaControllerCompat(context,sessionToken)
    notificationmanager = Playernotificationmanager.createWithNotificationChannel(
        context,NOTIFICATION_CHANNEL_ID,R.string.notification_channel_name,R.string.notification_channel_description,NOTIFICATION_ID,DescriptionAdapter(mediaController),notificationListener
    ).apply {

        setSmallIcon(R.drawable.ic_logo_svg)
        setMediaSessionToken(sessionToken)
        setRewindIncrementMs(0)
        setUseNavigationActionsInCompactView(true)
        setFastForwardIncrementMs(0)
        setPriority(NotificationCompat.PRIORITY_HIGH)

    }

}

fun showNotification(player: Player) {
    notificationmanager.setPlayer(player)
}

private inner class DescriptionAdapter(
    private val mediaController: MediaControllerCompat
) : Playernotificationmanager.MediaDescriptionAdapter {

    override fun getCurrentContentTitle(player: Player): CharSequence {
        newSongCallback()
        return mediaController.Metadata.description.title.toString()
    }

    override fun createCurrentContentIntent(player: Player): PendingIntent? {
        return mediaController.sessionActivity
    }

    override fun getCurrentContentText(player: Player): CharSequence {
        return mediaController.Metadata.description.subtitle.toString()
    }

    override fun getCurrentLargeIcon(
        player: Player,callback: Playernotificationmanager.BitmapCallback
    ): Bitmap? {


        Glide.with(context).asBitmap()
            .load(mediaController.Metadata.description.iconUri)
            .into(object : CustomTarget<Bitmap>() {

                override fun onResourceReady(
                    resource: Bitmap,transition: Transition<in Bitmap>?
                ) {
                    callback.onBitmap(resource)
                }

                override fun onLoadCleared(placeholder: Drawable?) = Unit

            })
        return null
    }
 }
}

MusicPlayerNotificationListener

class MusicPlayerNotificationListener(
private val musicService: MusicService
) : Playernotificationmanager.NotificationListener {

override fun onNotificationCancelled(notificationId: Int,dismissedByUser: Boolean) {
    super.onNotificationCancelled(notificationId,dismissedByUser)
    musicService.apply {
        stopForeground(true)
        isForegroundService = false
        stopSelf()
    }
}

override fun onNotificationPosted(
    notificationId: Int,notification: Notification,ongoing: Boolean
) {
    super.onNotificationPosted(notificationId,notification,ongoing)
    musicService.apply {
        if(ongoing && !isForegroundService){
            ContextCompat.startForegroundService(this,Intent(applicationContext,this::class.java)
                )
            startForeground(NOTIFICATION_ID,notification)
            isForegroundService = true
        }
    }
 }
}

MusicPlayerEventListener

class MusicPlayerEventListener(
private val musicService: MusicService): Player.EventListener {
override fun onPlayerStateChanged(playWhenReady: Boolean,playbackState: Int) {
    super.onPlayerStateChanged(playWhenReady,playbackState)
    if (playbackState == Player.STATE_READY && !playWhenReady){
        musicService.stopForeground(false)
    }
}

override fun onPlayerError(error: ExoPlaybackException) {
    super.onPlayerError(error)
    Toast.makeText(musicService,"An unkNown error occurred",Toast.LENGTH_LONG).show()
 }
}

MusicPlayBackPreparer

class MusicPlaybackPreparer(
private val musicSource: MusicSource,private val playerPreparer: (MediaMetadataCompat?) -> Unit) : 
MediaSessionConnector.PlaybackPreparer {
override fun onCommand(
    player: Player,controldispatcher: Controldispatcher,command: String,extras: Bundle?,cb: ResultReceiver?
) = 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?) {
    musicSource.whenReady {
        val itemToPlay = musicSource.songs.find {
            mediaId == it.description.mediaId
        }
        playerPreparer(itemToPlay)
    }
}

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

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

我已为当前问题添加了所有可能的代码

我可以做什么:

  1. 防止媒体在应用关闭时停止。
  2. 通知托盘添加关闭 (X) 图标。点击时应停止媒体播放并关闭通知

解决方法

删除 exoPlayer.stop() 类中的 fun onTaskRemoved(rootIntent: Intent?) 表单 MusicService。这将使您的播放器在您关闭应用程序后继续播放。