Engineering

Integrating FireStick Cast into Android App with Chromecast

Integrating FireStick blog AppUnite
Integrating FireStick blog AppUnite

Context

One of the most important features of media apps on the market is the possibility of casting their content. This also refers to the project I am working on. There was a story point in it, in which we had already integrated Google's Chromecast in our app, but got lots of requests to integrate Amazon’s FireStick. Below, you will find how we handled it, and the most important lessons we have learned.

While planning integration, we had 2 options to choose from:

  1. Using Google's Cast Framework. A very important thing to mention is that in order to add Google Cast to your app, you need to use Cast Framework for Android. What is great, it lets you define other cast receiver devices, apart from Chromecast.
  2. Handle all FireStick session management by hand, not using Google's Cast Framework.

Because we had already been using Google's Cast Framework and concluded that it would make integration much easier, we decided to use option number 1.

We also needed a way to manage routing events and communicate with the receiver. For this, we chose MediaRouter.

Great cooperation of Google's cast framework and MediaRouter handles a lot of things out of the box:

  • lots of UI-related tasks are handled automatically. MediaRouter manages cast devices picker dialog and cast button state.
    • chooser dialog chooser.png
    • cast button cast button
  • having Google Cast already integrated, it is much easier to add FireStick as a cast framework's external provider, because there is no need to rewrite existing code. It needs just little adjustments, as FireStick from outside is handled exactly in the same way as Chromecast
  • handling every cast receiving device differently could be error-prone and would require maintaining a much bigger codebase

There is one more important point. While integrating FireStick, you have two options for displaying content on the receiver device:

  1. you can write your own receiver application
  2. you can use the default built-in receiver but note that it is not possible to control volume using it. You can only do this by using the TV remote control to which FiretSick is connected.

In our case, we picked option number 2 as we decided that writing and maintaining a custom receiver application would be too time-consuming.

FireStick integration from MediaRouter perspective

As mentioned before, we used MediaRouter for routing management. You can find its basics in the FireStick context in this Amazon's tutorial. In Amazon's docs, you can also find a tutorial on integrating FireStick into an app with Google Cast already integrated, but it was not useful for us (API it showed was outdated and we were already using Cast SDK v3, so we used it).

Pro-tips first:

  • note, that in order to use FireStick in your app, you need to download AmazonFling and WhisperPlay jars from here and include them in your project.

  • while launching the app after integrating FireStick you can get a similar crash:

    java.lang.NoClassDefFoundError: Failed resolution of: Lorg/apache/http/conn/util/InetAddressUtils;
          at com.amazon.whisperlink.android.util.RouteUtil.createRoute(RouteUtil.java:78)
          at com.amazon.whisperlink.android.util.RouteUtil.createRoute(RouteUtil.java:51)
                ...
    Caused by: java.lang.ClassNotFoundException: Didn't find class "org.apache.http.conn.util.InetAddressUtils"
    

It is caused by lack of apache networking library in android runtime, since Android 6. To fix it, add the following code to your AndroidManifest.

<application>
    <uses-library android:name="org.apache.http.legacy" android:required="false" />
</application>

Next, to make MediaRouter discover your FireTv Stick you need to add Fling SDK provider to it, see the example below. Thanks to that, we will be able to see FireStick in the route chooser dialog.

In the FlingMediaRouteProvider constructor, you must specify the receiver app id which runs on Firestick, which, in our case, is the default one. It's a similar concept to the Chromecast appId.

val defaultBuiltInServiceId = "amzn.thin.pl"
val mediaRouter: MediaRouter = MediaRouter.getInstance(context).apply{
        addProvider(FlingMediaRouteProvider(context, defaultBuiltInServiceId))
}

Then, you can handle all media routing changes using MediaRouter.Callback just like in Chromecast.

FireStick integration from Google's Cast Framework perspective

Now we will set up the way of how our custom FireStick cast session will be managed by the cast framework. In order to do that, we need to fulfill the following points, by using a few components from cast framework API:

  • find a class implementing the OptionsProvider interface in your codebase (having Google Cast already integrated, you should have it). You can also find it by reference in your AndroidManifest in meta-data tag under name of com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME. In its fun getAdditionalSessionProviders, return an instance of your SessionProvider subclass, which in our case is CustomCastSessionProvider (you can find a detailed description below).
class CustomOptionsProvider : OptionsProvider {

    override fun getCastOptions(context: Context): CastOptions {
        return CastOptions.Builder()
            ...
            .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider> {
        return listOf(CustomCastSessionProvider(context))
    }
}
  • Now let's consider CustomCastSessionProvider mentioned before. Its most important duty is the creation of new sessions, that is why we return our custom Session subclass in fun createSession (which is CustomCastSession).

The next very important point is a need to provide custom and unique category id. It has to be the same as the one from FlingMediaRouteProvider. You can find it in the recompiled source of that class, by searching the addCategory phrase. You should find code similar to this:

IntentFilter remotePlayback = new IntentFilter();
remotePlayback.addCategory("category-id-you-need");

You also need to ensure that the found id is unique and differs from the one used by Chromecast's SessionProvider. If that is not the case, you will face problems in the situation where 2 different phones try to cast content, one to FireStick and the other to Chromecast, while being connected to the same Wi-Fi network.

Now, use found id in your custom SessionProvider:

class CustomSessionProvider(
        context: Context
) : SessionProvider(context, "category-id-you-need") {

    override fun createSession(sessionId: String?): Session {
        return CustomCastSession(context,category, sessionId ?: UUID.randomUUID().toString())
    }

    override fun isSessionRecoverable(): Boolean {
        return false
    }
}
  • Now, what is CustomCastSession? It is a subclass of the cast framework's Session class. It lets you override three key events connected to the cast session lifecycles: start, resume, and end. This is the place where you need to provide code used to directly manage FireStick. For that, we used RemotePlaybackClient, described in the next paragraph.
class CustomCastSession(
    context: Context,
    category: String,
    sessionId: String
) : Session(context, category, sessionId) {

    override fun start(routeInfoExtra: Bundle?) {
        // handle session start
    }

    override fun resume(routeInfoExtra: Bundle?) {
        // handle session resume
    }

    override fun end(stopCasting: Boolean) {
        // handle session end
    }
}

Communication with FireStick

The last step is communication setup, with FireStick. For that purpose, we used RemotePlaybackClient from MediaRouter (which uses the remote playback protocol defined by MediaControlIntent). As you can see in docs, it delivers all necessary API helpful for the session (startSession, endSession) and playback (play, pause, stop, etc..) management. We managed sessions from CustomCastSession shown above, by creating proper logic for its start, resume and end functions, whereas playback management was implemented using our custom player (based on ExoPlayer's BasePlayer).

Summary

I hope that the above tips will save you a lot of time! In case you have any questions, do not hesitate to leave a comment below.

FYI: there is the second part of the article planned, regarding FireStick integration on iOS, so check our blog from time to time, if you need it! 😉

Thanks for your time and happy coding!