Search code examples
androidandroid-jetpackexoplayer

Play music in background with media3


I have been trying to play audio using exoplayer media3 lib in background. So, far I was able to play the audio and it is playing in background too but I have kept url to be a test url which is static. Now, I want to pass the url to my service class which I am not able to understand how to do.

Attaching my code files here..

class PlaybackService : MediaSessionService() {
    // Create your Player and MediaSession in the onCreate lifecycle event
    lateinit var player: Player
    private var mediaSession: MediaSession? = null
    override fun onCreate() {
        super.onCreate()
        player = ExoPlayer.Builder(this).build()
        mediaSession = MediaSession.Builder(this, player).build()
        player.prepare()
        player.playWhenReady = true
        player.seekTo(0, 0)
        val mediaItem =
            MediaItem.fromUri("https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4")
        player.setMediaItem(mediaItem)
    }

    // Return a MediaSession to link with the MediaController that is making
    // this request.
    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
        mediaSession

    override fun onDestroy() {
        mediaSession?.run {
            player.release()
            release()
            mediaSession = null
        }
    }
}

Here is my fragment

class MusicPlayerFragment : BaseFragment() {


private lateinit var controllerFuture: ListenableFuture<MediaController>
private val controller: MediaController?
    get() = if (controllerFuture.isDone) controllerFuture.get() else null

private val currentWindow = 0
var playerStopped = true
private val playbackPosition: Long = 0


override fun onStart() {
    super.onStart()
    playMedia()
}

private fun playMedia() {
    val sessionToken = SessionToken(
        requireContext(),
        ComponentName(requireContext(), PlaybackService::class.java)
    )
    controllerFuture =
        MediaController.Builder(requireContext(), sessionToken)
            .buildAsync()
    controllerFuture.addListener({
        fragmentMusicPlayerBinding.playerView.player = controllerFuture.get()
        fragmentMusicPlayerBinding.controls.player = controllerFuture.get()
    }, MoreExecutors.directExecutor())
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
   
    fragmentMusicPlayerBinding.imgPlay.setOnClickListener {
        if (playerStopped) {
            playerStopped = false
            playMedia()
        }
    }
    fragmentMusicPlayerBinding.imgStop.setOnClickListener {
        stopMediaPlayer()
    }

    fragmentMusicPlayerBinding.imgPause.setOnClickListener {
        fragmentMusicPlayerBinding.playerView.player?.pause()
    }

    fragmentMusicPlayerBinding.imgRewind.setOnClickListener {
        fragmentMusicPlayerBinding.playerView.player?.seekTo(0)
    }
    
}

private fun stopMediaPlayer() {
    controllerFuture.get().stop()
    controllerFuture.get().playWhenReady = false
    MediaController.releaseFuture(controllerFuture)
    playerStopped = true
    fragmentMusicPlayerBinding.playerView.player = null
}

}

Problems I am facing

  1. I want to set media uri dynamically which I will get from api. So, how to do that?

  2. Using this code I get a mediapplayer notification in my notification center, when I play or pause from there music get stops or play but at the same time when I stop from notification and play from fragment music doesn't plays.

  3. Whenever I stop music from fragment, then too music keeps playing in background and notification is still there

  4. When I clear out the music notification from the notification center then also music keeps playing.

Please help me out with the solutions to the problem I facing if anyone have faced any. TIA


Solution

  • After many tries and lots of research, I found out the answer to my own question. Posting the answer as it can help someone out in the community finding the same answer. No hard feelings but after 3 days I am posting the answer to my own question but no one here came to help me out.

    Note: Exoplayer(Mediaplayer) used in my code is media3 lib which is in beta mode. Code is tested in android 11 and android 12 and is working fine. So, go ahead and use it.

    This youtube video helped me a lot with my answer.

    I modified my code slightly as per the instructions in video and here is my final code.

        class MusicPlayerActivity : BaseActivity() {
    
        lateinit var musicPlayerBinding: ActivityMusicPlayerBinding
        var player: Player? = null
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            musicPlayerBinding = ActivityMusicPlayerBinding.inflate(layoutInflater)
            setContentView(musicPlayerBinding.root)
            bindMusicService()
        }
    
        override fun onBackPressed() {
            unbindService(playerServiceConnection)
            if (player?.isPlaying!!) {
                player?.stop()
            }
            player?.release()
            player = null
            super.onBackPressed()
        }
        
        private fun bindMusicService() {
            val intent = Intent(this, PlayerService::class.java)
            bindService(intent, playerServiceConnection, Context.BIND_AUTO_CREATE)
        }
    
        private val playerServiceConnection = object : ServiceConnection {
            override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
                val binder = service as PlayerService.ServiceBinder
                player = binder.getPlayerService().player!!
                player?.prepare()
                player?.playWhenReady = false
                player?.seekTo(0, 0)
                val mediaMetaData = MediaMetadata.Builder()
                    .setAlbumTitle(MusicPlayerData.audioTitle)
                    .build()
                val mediaItem =
                    MediaItem.Builder().setMediaMetadata(mediaMetaData)
                        .setUri(MusicPlayerData.audioUri)
                        .build()
                player?.setMediaItem(mediaItem)
                setPlayerControls()
            }
    
            override fun onServiceDisconnected(name: ComponentName?) {
                TODO("Not yet implemented")
            }
        }
    
        private fun setPlayerControls() {
            musicPlayerBinding.playerView.player = player
            musicPlayerBinding.controls.player = player
            player?.addListener(object : Player.Listener {
                override fun onPlaybackStateChanged(playbackState: Int) {
                    super.onPlaybackStateChanged(playbackState)
                    when (playbackState) {
                        Player.STATE_BUFFERING -> show(musicPlayerBinding.progressCircular)
                        Player.STATE_READY -> {
                            player?.playWhenReady = true
                            hide(musicPlayerBinding.progressCircular)
                        }
                    }
                }
            })
        }
    }
    

    Here is my service class

    class PlayerService : Service() {
    
    private val iBinder = ServiceBinder()
    var player: Player? = null
    private var mediaSession: MediaSession? = null
    lateinit var notificationManager: PlayerNotificationManager
    
    inner class ServiceBinder : Binder() {
        fun getPlayerService(): PlayerService = this@PlayerService
    }
    
    
    override fun onBind(intent: Intent?): IBinder {
        return iBinder
    }
    
    override fun onCreate() {
        super.onCreate()
        player = ExoPlayer.Builder(this).build()
        mediaSession =
            MediaSession.Builder(this, player!!).setSessionActivity(pendingIntent()!!).setId(Random(5).toString())
                .build()
        notificationManager = PlayerNotificationManager.Builder(this, 111, "Music Channel")
            .setChannelImportance(IMPORTANCE_HIGH)
            .setSmallIconResourceId(R.drawable.music)
            .setChannelDescriptionResourceId(R.string.app_name)
            .setChannelNameResourceId(R.string.app_name)
            .setMediaDescriptionAdapter(audioDescriptor)
            .setNotificationListener(notificationListener)
            .build()
    
    
        notificationManager.setPlayer(player)
        notificationManager.setPriority(PRIORITY_MAX)
        notificationManager.setUseRewindAction(true)
        notificationManager.setUseFastForwardAction(false)
        notificationManager.setUsePreviousAction(false)
        notificationManager.setUsePlayPauseActions(true)
    }
    
    
    override fun onDestroy() {
        if (player?.isPlaying!!) {
            player?.stop()
        }
        notificationManager.setPlayer(null)
        player?.release()
        player = null
        stopForeground(true)
        stopSelf()
        super.onDestroy()
    }
    
    private val notificationListener = object : PlayerNotificationManager.NotificationListener {
        override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
            super.onNotificationCancelled(notificationId, dismissedByUser)
            stopForeground(true)
            if (player?.isPlaying!!) {
                player?.stop()
                player?.release()
            }
        }
    
        override fun onNotificationPosted(
            notificationId: Int,
            notification: Notification,
            ongoing: Boolean
        ) {
            super.onNotificationPosted(notificationId, notification, ongoing)
            startForeground(notificationId, notification)
        }
    }
    
    private val audioDescriptor = object : PlayerNotificationManager.MediaDescriptionAdapter {
        override fun getCurrentContentTitle(player: Player): CharSequence {
            return player.currentMediaItem?.mediaMetadata?.albumTitle!!
        }
    
        override fun createCurrentContentIntent(player: Player): PendingIntent? {
            return pendingIntent()
        }
    
        override fun getCurrentContentText(player: Player): CharSequence? {
            return ""
        }
    
        override fun getCurrentLargeIcon(
            player: Player,
            callback: PlayerNotificationManager.BitmapCallback
        ): Bitmap? {
            val bitmapDrawable: BitmapDrawable =
                ContextCompat.getDrawable(
                    applicationContext,
                    R.drawable.cma_logo_render
                ) as BitmapDrawable
            return bitmapDrawable.bitmap
        }
    }
    
    private fun pendingIntent(): PendingIntent? {
        val intent = Intent(applicationContext, MusicPlayerActivity::class.java)
        return PendingIntent.getActivity(
            applicationContext,
            0,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
    }
    

    Problems I am facing

    1. I want to set media uri dynamically which I will get from api. So, how to do that? -----> No need of setting the media uri in service class, we can get instance of player from service class in our activity and do all the functionalities in activity class only

    2. Using this code I get a mediapplayer notification in my notification center, when I play or pause from there music get stops or play but at the same time when I stop from notification and play from fragment music doesn't plays. ----->problem solved after modifying my service class

    3. Whenever I stop music from fragment, then too music keeps playing in background and notification is still there ----->problem solved after modifying my service class

    4. When I clear out the music notification from the notification center then also music keeps playing.
      -----> for this I have set session activity to open activity from the current playing state

    After implementing the things like above,

    1. activity returns to it's current state
    2. playing music from notification and from the activity is in sync
    3. music plays in background also
    4. after activity comes to resume state after going back to background, play pause works perfectly either from the UI or notification center.
    5. one can play, pause, stop and rewind
    6. set the music title and icon in audio descriptor if required otherwise can be left empty.
    7. notification doesn't clears out when the song is playing
    8. if the app is killed then song stops playing and notification is also gone