android - 使用 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, 0, 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,
it,
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()
}
}
}
音乐通知管理器
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
}
}
}
}
音乐播放器事件监听器
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, playWhenReady: Boolean, extras: Bundle?) = Unit
override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) = Unit
}
我已经为我当前的问题添加了所有可能的代码。
我能做些什么:
- 防止应用程序关闭时媒体停止。
- 向通知托盘添加关闭 (X) 图标。单击时应停止媒体播放并关闭通知。
解决方案
删除班级内的exoPlayer.stop()
表格 。这使您的播放器在您关闭应用程序后继续播放。fun onTaskRemoved(rootIntent: Intent?)
MusicService
推荐阅读
- python - 从数据框中获取文本的最佳方法,按句子然后按单词进行标记
- ios - 自定义栏按钮项目图像框架太宽
- php - php-fpm主进程重启后执行PHP脚本
- oracle - 无法删除 sql developer 中的现有主表
- linux - 我试图通过串口发送命令列表,但设备只占用第一行,其他行被忽略
- javascript - 声明一个全局 google maps 对象
- javascript - 如何通过 HTML 中的开关使用 onchange 更改 BACK
- html - 如何从日期输入字段中删除不需要的标记?
- laravel - “如何修复 laravel 作业已尝试太多次或运行时间过长”
- java - 使用java合并solr中重复的Faceitfields列表