Categories
android blog kotlin

Building a podcast app series: 4. Player service

How do we make sure that our app can run in the background and play a podcast without worrying about Android shutting the app down to claim resources? A foreground service to the rescue!

MediaBrowserServiceCompat

What the heck is a MediaBrowserServiceCompat? It’s a service that ExoPlayer ships with, and it can simplify the task of creating a background service for the player. Since Android O, any service that wants to be alive for extended periods of time should also show a notification and mark itself as a foreground service. To accomplish this we need to make use of PlayerNotificationManager class, also shipped with ExoPlayer, that given a media session token for our player will display a notification which will be in sync with the ExoPlayer. This is the most complicated part of the whole ExoPlayer setup because it works with a variety of Android APIs, and we all know how easy is it to use Android APIs.

PlayerNotificationManager

In order to utilize PlayerNotificationManager class, we need to create an instance of it and pass in a context, notification channel id, notification id, and a MediaDescriptionAdapter. The MediaDescriptionAdapter is how we tell the PlayerNotificationManager what is the title and image resource for our currently playing episode. Once we create an instance of this class, we can customize it in various ways. The most important method call after we create an instance is setPlayer(exoPlayerInstance). This is super important, given an ExoPlayer instance, PlayerNotificationManager will listen to that ExoPlayer instance changes and change the notification UI accordingly. This is a simple configuration of the PlayerNotificationManager

val playerNotificationManager = PlayerNotificationManager(context, channelId, notificationId, adapter)
playerNotificationManager.setPlayer(exoPlayer)
playerNotificationManager.setMediaSessionToken(it)

This leaves us with a question, who will supply an ExoPlayer instance? There can be a singleton that always returns a single instance, or even better, dependency injection can be used to provide the ExoPlyaer instance to different classes. That is what we will focus on the next, and it will be the last article in the series.

Notification channels

One thing we can do in this player service is to create a notification channel that PlayerNotificationManager will use to actually show a notification. Here is a simple way to create a new channel in case it is not created. Android O requires channels to be created before notifications can be presented to the user. We want to set the priority to low here to avoid any sounds or vibrations coming from the player notification when the user presses any action. Let’s look at an example

    private fun shouldCreateNowPlayingChannel(notificationManager: NotificationManagerCompat) =
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !nowPlayingChannelExists(
            notificationManager
        )

    @RequiresApi(Build.VERSION_CODES.O)
    private fun nowPlayingChannelExists(notificationManager: NotificationManagerCompat) =
        notificationManager.getNotificationChannel(nowPlayingChannelId) != null

    @RequiresApi(Build.VERSION_CODES.O)
    private fun createNowPlayingChannel(notificationManager: NotificationManagerCompat) {
        val notificationChannel = NotificationChannel(
            nowPlayingChannelId,
            getString(R.string.notification_channel),
            NotificationManager.IMPORTANCE_LOW
        ).apply {
           description = getString(R.string.notification_channel_description)
        }
         notificationManager.createNotificationChannel(notificationChannel)
    }

Checkout this Github link for the full player service code.

Stay tuned for the next article where we will explore a simple yet powerful dependency injection framework called Koin (technically it is a service locator but it enough to what we need for this app)

Categories
android blog kotlin

Building a podcast app series: 3. Exoplayer

What is a podcast app if not a wrapper around a media player? And, if we talk Android, there is the only one media player worth considering, ExoPlayer, made by Google engineers. Let’s look at how can we connect all the pieces we have built so far and actually connect the ExoPlayer with the view within the app.

ExoPlayer

Exoplayer is very powerful and modular, we are going to use just a fraction of what ExoPlayer really offers. ExoPlayer has a couple of core components that have to work together for it to play anything, in no particular order:

  1. ExoPlayer instance
  2. MediaSource

We need to create an ExoPlayer instance and hold it in memory since it is pretty expensive to create. We will reuse a single instance throughout the app. For it to play anything, we need to create a MediaSource. These sources are basically different types of streams that ExoPlayer has to read in order to fetch the audio data and play it. There are other important parts to the ExoPlayer ecosystem but for starters, we need those two basic things.

ExoPlayer instance

This is the heart of ExoPlayer and the object itself. We can simply create a single instance and pass or not pass a bunch of configuration options. Let’s keep it simple and create a basic instance:

val player = SimpleExoPlayer.Builder(context).build()

MediaSource

So the name of this class is pretty self-explanatory, we have to be aware of the fact that there are a couple of different media sources, depending on the actual source that serves the content:

  • DashMediaSource for DASH.
  • SsMediaSource for SmoothStreaming.
  • HlsMediaSource for HLS.
  • ProgressiveMediaSource for regular media files.

Let’s create a simple media source:

val mediaSource =
   ProgressiveMediaSource.Factory(dataSourceFactory).
        createMediaSource(Uri.parse(it.mp3Url))        
  exoPlayer.prepare(mediaSources)

And that’s it. We can now play an episode!

Since we are building a podcast player, it is safe to assume we will never play one episode at a time, so we need a way to tell ExoPlayer to play the next item when one item is finished playing. To handle that, ExoPlayer has a concept of the concatenated media source. We can bundle together a bunch of media sources, they don’t have to be of the same type, and attach that source to the ExoPlayer instance.

val mediaSources = (listOf(currentEpisode) + _playlist).map {
   ProgressiveMediaSource.Factory(dataSourceFactory).
        createMediaSource(Uri.parse(it.mp3Url))        
   }.toTypedArray()
exoPlayer.prepare(ConcatenatingMediaSource(*mediaSources)

So instead of passing just one media source, we can pass in a concatenating media source and the ExoPlayer will automatically play all episodes from that playlist.

Browse the full code on this link and in the next article, we will talk about a foreground service that needs to run our ExoPlayer and keep our app in the background so Android does not shut down our app to claim more resources :).