Creating a MediaRouteProvider

As was noted earlier in the book, you can use MediaRouter to identify media routes, such as those published by devices like Google’s Chromecast. Specifically, remote playback routes let you write apps that tell other devices, like the Chromecast, to play back media on your behalf.

However, not only can you write clients for remote playback routes, you can write providers of those routes. Perhaps you are working with a hardware manufacturer that is creating a Chromecast-like device. Perhaps you want to allow your app, running on a Fire TV or an Android HDMI stick, to be controlled by a user’s phone or tablet. Or perhaps you are trying to tie Android into specialized media hardware that does not communicate by conventional means (e.g., wireless speakers that do not use normal Bluetooth profiles).

This chapter will outline how you can create code that will publish media routes to users of MediaRouter, so that you can then take those requests and forward them to a remote device.

Prerequisites

This chapter assumes that you have read the chapter on MediaRouter.

Terminology

For the purposes of this chapter:

DIY Chromecast

Google’s Chromecast is a nice little device. However, it has issues:

Some of these issues can be mitigated by the use of MediaRouter and RemotePlaybackClient instead of the proprietary Cast SDK. You are not bound by any particular license terms (beyond the norm for Android development) and the implementation of the media framework is open.

However, to make this work, the client device needs to know how to talk to the player device.

The good news is that the media routing framework in Android supports plug-in media route providers for just this purpose. The OS ships with such a provider for the Chromecast, and you can create your own providers to talk to whatever else you would like to talk to. The user can then install a small app on their client device that implements this media route provider, and any apps already on their client device that use classes like RemotePlaybackClient will automatically be able to cast their desired content to the player device.

MediaRouteProvider

The guts of this come in the form of a MediaRouteProvider. Your custom subclass of MediaRouteProvider will:

Depending upon your use case, you could elect to keep the MediaRouteProvider private to your application. That way, your app can cast to the player device, but no other apps can. Or, you can make your MediaRouteProvider available to all apps on the device, with the media routing framework taking care of the IPC details to have those apps tell your MediaRouteProvider what the player device should do.

Player Device… and Maybe a Player App

Of course, this assumes the existence of some player device that is not supported by Android out of the box. Since Android only really supports Chromecast, external displays (e.g., HDMI, MHL, Miracast), and some Bluetooth options (e.g., external speakers) for media routes, there are countless player devices that need additional help. These will run the gamut from devices from major players (e.g., Amazon’s Fire TV) to no-name devices (e.g., Android HDMI “sticks”).

Some player devices will run Android. In that case, you would be writing a player app that would run on the player device that would be the recipient of commands sent to it from your MediaRouteProvider on the client device. For example, if you write a video player app, you could augment it with remote control capability driven by a MediaRouteProvider on a client device, turning your player app and anything it can run on (e.g., Fire TV, OUYA game console) into a Chromecast-like environment.

Some player devices will not run Android. If they offer some existing remote control over-the-air protocol, you could create a MediaRouteProvider that speaks that protocol. Or, perhaps the player devices are programmable, just not via Android (e.g., a Linux program for XMBC), in which case you might be able to write both ends of the communications channel.

Communications Protocol

Somehow, the data from the MediaRouteProvider needs to get to the player device (and, where relevant, the player app). Likely candidates include Bluetooth, regular WiFi (if both devices are on the same network), and WiFi Direct.

However, in principle, anything is possible. For example, there is nothing stopping you from sending MediaRouteProvider commands to some Web server out on the Internet, which forwards them to some distant location for use. That would be a bit unusual – normally, the user of the client device is controlling something she can see — but it certainly could be done.

The biggest thing to watch out for is the addressability of the media to be played back. There is little point in connecting a MediaRouteProvider to some player device, then not have the ability for the player device to access the media that the client device is requesting. The expected pattern is that the media is hosted in some (relatively) central location, like a Web server. However, once again, anything is possible. If you want to have some sort of server on the client device, to allow the player device to play back media from it, and you believe that you can adequate secure this, you are welcome to do so.

Creating the MediaRouteProvider

As noted earlier, the core of all of this is a custom MediaRouteProvider. Google supplies a sample application for creating such a MediaRouteProvider. However, it is overly complex, and it is undocumented.

This chapter will focus instead on the MediaRouter/RouteProvider sample project. This is a clone of the MediaRouter/RemotePlayback sample project covered earlier in this book, with the addition of a custom MediaRouteProvider.

Defining the Supported Actions

A MediaRouteProvider advertises — whether to its own app’s MediaRouter or to the entire device — what sorts of actions it can perform. For example, a remote playback route provider needs to support actions like play, pause, resume, and stop of some piece of media.

The way this is handled in the media routing framework is via a series of IntentFilter objects.

Since IntentFilter objects do not need a Context to be created, it is safe to define them statically, if desired. That’s what we do in DemoRouteProvider, a custom subclass of MediaRouteProvider. It declares a pair of static final IntentFilter objects, ifPlay and ifControl, which are then configured in a static initialization block:

  private static final IntentFilter ifPlay=new IntentFilter();
  private static final IntentFilter ifControl=new IntentFilter();

  static {
    ifPlay.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
    ifPlay.addAction(MediaControlIntent.ACTION_PLAY);
    ifPlay.addDataScheme("http");
    ifPlay.addDataScheme("https");
    ifPlay.addDataScheme("rtsp");

    try {
      ifPlay.addDataType("video/*");
    }
    catch (MalformedMimeTypeException e) {
      throw new RuntimeException("Exception setting MIME type", e);
    }

    ifControl.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
    ifControl.addAction(MediaControlIntent.ACTION_PAUSE);
    ifControl.addAction(MediaControlIntent.ACTION_RESUME);
    ifControl.addAction(MediaControlIntent.ACTION_STOP);
    ifControl.addAction(MediaControlIntent.ACTION_GET_STATUS);
    ifControl.addAction(MediaControlIntent.ACTION_SEEK);
  }
(from MediaRouter/RouteProvider/app/src/main/java/com/commonsware/android/mrp/DemoRouteProvider.java)

Both stipulate that they are looking for Intent objects in the MediaControlIntent.CATEGORY_REMOTE_PLAYBACK category. This category is used for all media routing Intents that form the foundation of the routing framework.

ifPlay is defined as supporting MediaControlIntent.ACTION_PLAY, stating that we know how to play back some content. The qualifications for “some content” are handled via scheme and type constraints placed on the IntentFilter. Here, we limit the content to be URLs that might be reachable by a playback device (http, https, rtsp) and have a MIME type matching video/*. Hence, we are stating that we can play back streaming video.

ifControl sets up the remaining actions that we support:

These are placed on an independent IntentFilter because, technically, we can support these actions on any type of media. In the case of this specific example, the only media we support is streaming video. But, we could configure other IntentFilter objects, like ifPlay was, stating yet other media types that we handle.

To fully comply with the RemotePlaybackClient API, we must advertise that we handle all of those actions… even if our intended client will not use all of them.

We could also:

All of those are demonstrated in Google’s sample app.

Creating the Descriptors

Just because we have some static IntentFilter objects does not mean that anything will pay attention to them. We need to actually register them with the media routing framework, wrapped in a pair of “descriptor” objects. DemoRouteProvider calls a private handleDiscovery() method from the constructor, where handleDiscovery() sets up the descriptors:

  private void handleDiscovery() {
    MediaRouteDescriptor.Builder mrdBuilder=
        new MediaRouteDescriptor.Builder(DEMO_ROUTE_ID, "Demo Route");

    mrdBuilder.setDescription("The description of a demo route")
              .addControlFilter(ifPlay)
              .addControlFilter(ifControl)
              .setPlaybackStream(AudioManager.STREAM_MUSIC)
              .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
              .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED);

    MediaRouteProviderDescriptor.Builder mrpdBuilder=
        new MediaRouteProviderDescriptor.Builder();

    mrpdBuilder.addRoute(mrdBuilder.build());

    setDescriptor(mrpdBuilder.build());
  }
(from MediaRouter/RouteProvider/app/src/main/java/com/commonsware/android/mrp/DemoRouteProvider.java)

In the end, we need to provide a MediaRouteProviderDescriptor to the MediaRouteProvider by means of a setDescriptor() method. MediaRouteProviderDescriptor is, in effect, metadata about the MediaRouteProvider itself. At the present time, the only thing this holds is a set of MediaRouteDescriptor objects, one for each media route that the MediaRouteProvider claims to support.

A MediaRouteProvider is made up of several pieces of information, including:

These are all configured on a MediaRouteProvider by creating a MediaRouteProvider.Builder and supplying the values either in the Builder constructor or via fluent setter methods. In the particular case of our simple demo provider, we:

It is very likely that you will elect to have several MediaRouteDescriptor objects for different client application scenarios. Google’s sample app uses a total of four MediaRouteDescriptor objects:

Receiving the Actions

Now, we have told the media routing framework what actions we support. Some app will then try to use RemotePlaybackClient and ask us to perform those actions. Hence, we need to find out when this happens, so we can do the actual work of having the playback device actually play back the media, pause the media, etc.

To do this, we need to create a custom subclass of MediaRouteProvider.RouteController. This contains a series of callback methods which we can override to find out when various events occur.

There are four such callback methods that the DemoRouteController subclass of MediaRouteProvider.RouteController implements:

The DemoRouteController just logs a message to Logcat for the first three callbacks:

  @Override
  public void onRelease() {
    Log.d(getClass().getSimpleName(), "released");
  }

  @Override
  public void onSelect() {
    Log.d(getClass().getSimpleName(), "selected");
  }

  @Override
  public void onUnselect() {
    Log.d(getClass().getSimpleName(), "unselected");
  }
(from MediaRouter/RouteProvider/app/src/main/java/com/commonsware/android/mrp/DemoRouteController.java)

The onControlRequest() method is a bit more complex, as all control requests route through here: play, pause, resume, stop, etc. onControlRequest() is passed the Intent identifying the particular action that should be performed, and we can examine the Intent action string to determine what needs to be done. In this case, onControlRequest() delegates the real work to action-specific methods like onPlayRequest():

  @Override
  public boolean onControlRequest(Intent i, ControlRequestCallback cb) {
    if (i.hasCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
      if (MediaControlIntent.ACTION_PLAY.equals(i.getAction())) {
        return(onPlayRequest(i, cb));
      }
      else if (MediaControlIntent.ACTION_PAUSE.equals(i.getAction())) {
        return(onPauseRequest(i, cb));
      }
      else if (MediaControlIntent.ACTION_RESUME.equals(i.getAction())) {
        return(onResumeRequest(i, cb));
      }
      else if (MediaControlIntent.ACTION_STOP.equals(i.getAction())) {
        return(onStopRequest(i, cb));
      }
      else if (MediaControlIntent.ACTION_GET_STATUS.equals(i.getAction())) {
        return(onGetStatusRequest(i, cb));
      }
      else if (MediaControlIntent.ACTION_SEEK.equals(i.getAction())) {
        return(onSeekRequest(i, cb));
      }
    }

    Log.w(getClass().getSimpleName(), "unexpected control request"
        + i.toString());

    return(false);
  }
(from MediaRouter/RouteProvider/app/src/main/java/com/commonsware/android/mrp/DemoRouteController.java)

onControlRequest() should return true if we agree to perform the action and will use the supplied ControlRequestCallback object to asynchronously deliver our results. If onControlRequest() returns false, that means that we are rejecting the action for some reason, such as it being one that is unrecognized. In DemoRouteController, that will occur if the category or the action on the Intent is not one of the supported options.

Note that if you opted into variable volume, there are onSetVolume() and onUpdateVolume() callback methods that will give you access to those events.

Handling the Actions

For those actions that you advertise and receive in onControlRequest(), you need to actually do the work for those actions. The details of this will vary widely depending upon your playback device and playback app that you are supporting. For example, you might establish a WiFi Direct connection in onSelect(), then use that connection in handling play, pause, etc. actions.

However, a few aspects of handling these actions will be in common across all implementations:

The details of what that Bundle must contain are documented on the MediaControlIntent class, on the definition of each action string (e.g., ACTION_PLAY).

With that in mind, let’s look at the six actions supported by DemoRouteController.

Play

The Bundle passed to onResult() of the ControlRequestCallback, when the action is ACTION_PLAY, needs three values:

You create a MediaItemStatus object via a MediaItemStatus.Builder, where you can pass into the constructor a value indicating the overall status (e.g., MediaItemStatus.PLAYBACK_STATE_PLAYING), plus use fluent setter methods to define additional characteristics of the status, such as the current seek position.

The DemoRouteController logic for ACTION_PLAY, in the onPlayRequest() method, logs the event to Logcat and crafts a valid-but-meaningless result Bundle for use with onResult():

  private boolean onPlayRequest(Intent i, ControlRequestCallback cb) {
    Log.d(getClass().getSimpleName(), "play: "
        + i.getData().toString());

    MediaItemStatus.Builder statusBuilder=
        new MediaItemStatus.Builder(
                                    MediaItemStatus.PLAYBACK_STATE_PLAYING);

    Bundle b=new Bundle();

    b.putString(MediaControlIntent.EXTRA_SESSION_ID, DemoRouteProvider.DEMO_SESSION_ID);
    b.putString(MediaControlIntent.EXTRA_ITEM_ID, DemoRouteProvider.DEMO_ITEM_ID);
    b.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
                statusBuilder.build().asBundle());

    cb.onResult(b);

    return(true);
  }
(from MediaRouter/RouteProvider/app/src/main/java/com/commonsware/android/mrp/DemoRouteController.java)

Pause, Resume, and Stop

The Bundle passed to onResult() of the ControlRequestCallback, when the action is ACTION_PAUSE, ACTION_RESUME, or ACTION_STOP, does not need any particular values at the present time. Hence, the DemoRouteController methods for those actions just log the event to Logcat and pass an empty Bundle to onResult():

  private boolean onPauseRequest(Intent i, ControlRequestCallback cb) {
    Log.d(getClass().getSimpleName(), "pause");

    cb.onResult(new Bundle());

    return(true);
  }

  private boolean onResumeRequest(Intent i, ControlRequestCallback cb) {
    Log.d(getClass().getSimpleName(), "resume");

    cb.onResult(new Bundle());

    return(true);
  }

  private boolean onStopRequest(Intent i, ControlRequestCallback cb) {
    Log.d(getClass().getSimpleName(), "stop");

    cb.onResult(new Bundle());

    return(true);
  }
(from MediaRouter/RouteProvider/app/src/main/java/com/commonsware/android/mrp/DemoRouteController.java)

Get Status and Seek

The Bundle passed to onResult() of the ControlRequestCallback, when the action is ACTION_GET_STATUS or ACTION_SEEK, must contain the same sort of MediaItemStatus-built nested Bundle representing the current status. For ACTION_GET_STATUS, the only “work” to be done is to pass back the status; for ACTION_SEEK, you should move the playback position to the location indicated by an extra on the Intent, then return the revised status.

In the case of DemoRouteController, both just log a message to Logcat and return a fairly pointless status:

  private boolean onGetStatusRequest(Intent i,
                                     ControlRequestCallback cb) {
    Log.d(getClass().getSimpleName(), "get-status");

    MediaItemStatus.Builder statusBuilder=
        new MediaItemStatus.Builder(
                                    MediaItemStatus.PLAYBACK_STATE_PLAYING);

    Bundle b=new Bundle();

    b.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
                statusBuilder.build().asBundle());

    cb.onResult(b);

    return(true);
  }

  private boolean onSeekRequest(Intent i, ControlRequestCallback cb) {
    Log.d(getClass().getSimpleName(), "seek");

    MediaItemStatus.Builder statusBuilder=
        new MediaItemStatus.Builder(
                                    MediaItemStatus.PLAYBACK_STATE_PLAYING);

    Bundle b=new Bundle();

    b.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
                statusBuilder.build().asBundle());

    cb.onResult(b);

    return(true);
  }
(from MediaRouter/RouteProvider/app/src/main/java/com/commonsware/android/mrp/DemoRouteController.java)

Publishing the Controller

While we have defined our RouteController, we still need to teach our MediaRouteProvider about it. That is through overriding the onCreateRouteController() method and returning an instance of RouteController:

  @Override
  public RouteController onCreateRouteController(String routeId) {
    return(new DemoRouteController());
  }
(from MediaRouter/RouteProvider/app/src/main/java/com/commonsware/android/mrp/DemoRouteProvider.java)

onCreateRouteController() is passed the route ID String used in the MediaRouteDescriptor. You can either use that to instantiate a different RouteProvider, pass the String into a common RouteProvider so it knows what to do, or ignore it entirely if you have only one published route. In the case of DemoRouteProvider, we ignore the route ID and always return a DemoRouteController.

Handling Discovery Requests

DemoRouteProvider is always available, largely because it does not do much of anything.

In the real world, your MediaRouteProvider may not always be relevant. For example, the TV you are set up to talk to may be powered down. Or, the user may not be at home where the TV is, so the client device and the TV are not on the same network.

Rather than constantly polling the outside world to see if a route is possible, we only do this when a client app requests “route discovery”, such as by providing the MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY flag on an addCallback() call to a MediaRouter. That in turn triggers an onDiscoveryRequestChanged() call on our MediaRouteProvider.

There, and in our constructor-triggered setup, we should do work to determine if a route is currently possible and set up our descriptors. This work should be done in a background thread if it involves network I/O.

Note that onDiscoveryRequestChanged() is passed a MediaRouteDiscoveryRequest object, describing what the consuming app is looking for. If the request is irrelevant for your provider (e.g., the app wants a local audio route, and you provide remote playback routes), simply ignore it.

The onDiscoveryRequestChanged() implementation in DemoRouteProvider just calls the same handleDiscovery() method that the constructor does.

Consuming the MediaRouteProvider

Having a MediaRouteProvider is nice, but it is useless if apps are not going to know about it.

You have two main options for consuming the MediaRouteProvider: use it only within your own app, or publish it to all apps on the device.

Private Provider

Using a MediaRouteProvider for your own app is very simple. Just add a single call to addProvider() on your MediaRouter, supplying an instance of your MediaRouteProvider.

Since our sample project is a fork of the original RemotePlaybackClient sample, we still have a PlaybackFragment that sets up the MediaRouter and MediaRouteActionProvider. In onAttach() of that PlaybackFragment, we can configure our MediaRouterProvider after obtaining the MediaRouter instance:

  @Override
  public void onAttach(Activity host) {
    super.onAttach(host);

    router=MediaRouter.getInstance(host);
    provider=new DemoRouteProvider(getActivity());
    router.addProvider(provider);
  }
(from MediaRouter/RouteProvider/app/src/main/java/com/commonsware/android/mrp/PlaybackFragment.java)

At this point, our DemoRouteProvider will be available as an option for the user, along with any other eligible media routes:

MediaRouteProvider Demo, on a Nexus 4, Showing Available Routes
Figure 818: MediaRouteProvider Demo, on a Nexus 4, Showing Available Routes

Choosing the DemoRouteProvider (“Demo Route” in the screenshot) will allow you to use it just like you do a Chromecast… if you do not mind the fact that nothing shows up on your television:

MediaRouteProvider Demo, on a Nexus 4, After Several Commands
Figure 819: MediaRouteProvider Demo, on a Nexus 4, After Several Commands

As it turns out, the DemoRouteProvider works better than Google’s own MediaRouteProvider for the Chromecast, insofar as more of the callbacks work. Specifically, we actually receive callbacks for pause, resume, and stop events, as opposed to having to just assume that those events completed.

Also, we remove the demo provider in onDetach():

  @Override
  public void onDetach() {
    router.removeProvider(provider);

    super.onDetach();
  }
(from MediaRouter/RouteProvider/app/src/main/java/com/commonsware/android/mrp/PlaybackFragment.java)

Among other things, this allows us to correctly handle configuration changes — if we fail to call removeProvider() and blindly add another provider in onAttach(), we wind up with multiple providers, because our MediaRouter is a framework-provided singleton and is not re-created with the new fragment.

Public Provider

If you want your MediaRouteProvider to be used by other apps, you will need to create one more Java class: a subclass of MediaRouteProviderService. This requires only one method, onCreateMediaRouteProvider(), where you return an instance of your MediaRouteProvider:

package com.commonsware.android.mrp;

import android.support.v7.media.MediaRouteProvider;
import android.support.v7.media.MediaRouteProviderService;

public class DemoRouteProviderService extends MediaRouteProviderService {
  @Override
  public MediaRouteProvider onCreateMediaRouteProvider() {
    return(new DemoRouteProvider(this));
  }
}
(from MediaRouter/RouteProvider/app/src/main/java/com/commonsware/android/mrp/DemoRouteProviderService.java)

This also needs to be added to your manifest, like any other Service. Give it an <intent-filter> looking for the android.media.MediaRouteProviderService action, so the media routing framework knows that it can obtain a MediaRouteProvider from it:


<service
  android:name="DemoRouteProviderService"
  tools:ignore="ExportedService">
  <intent-filter>
    <action android:name="android.media.MediaRouteProviderService"/>
  </intent-filter>
</service>

However, do not do both addProvider() and have the <service> element. If you use the <service> element, your app can use the MediaRouteProvider, just as can any other app on the device. Hence, in the published source code for this sample, the <service> element is commented out — you will need to uncomment it, and comment out the addProvider() call, to test the DemoRouteProvider with other apps.

Implementing This “For Realz”

Of course, DemoRouteProvider is just a demo and does not actually play any media anywhere. It is here to give you the basic steps for responding to RemotePlaybackClient requests. For a production MediaRouteProvider, in addition to the usual tightening-up of the code (e.g., better exception handling), you will need to work on other areas as well, ones that are beyond the scope of the sample app.

Communicating with the Playback Device

Of course, the big one is passing the actions over to the playback device, so you actually do play back media.

If you are the developer of the playback device and its protocols (e.g., it is an Android device, and you are writing the playback app for it), then you can choose how you wish to handle the communications. You can work with low-level socket protocols directly, or you can leverage libraries like AllJoyn or ZeroMQ.

If the playback device “is what it is”, and you cannot change it, then you will need to determine what protocols it offers and how best to map the MediaControlIntent actions to that protocol.

Also note that onControlRequest() is designed for asynchronous operation. The sample app just invoked the ControlRequestCallback during the onControlRequest() processing. Usually, though, your communications with the playback device will not be as fast as a call to Log.d(). You should arrange to do those communications in a background thread, perhaps via a single-thread thread pool as an ExecutorService. Simply pass the ControlRequestCallback to that thread along with the rest of the action’s data (e.g., the URL of the media to load), and the thread can call onResult() or onError() as needed.

Handling Other Actions/Protocols

As was noted in the description of the sample app, that app avoids:

Any of those may be of interest to your users, and so you may need to consider offering them at some point. Also note that some potential client apps might need those capabilities and therefore will not see or use your published media routes without them.

Custom Actions

When setting up the MediaRouteProvider, we create one or more MediaRouteDescriptor objects wrapped around one or more IntentFilter objects. Those IntentFilter objects indicate what actions we support. The DemoRouteProvider uses standard actions (e.g., ACTION_PLAY) in a standard category (CATEGORY_REMOTE_PLAYBACK).

However, you are not limited to that.

You are welcome to also support custom actions in a custom category, to represent other things that your particular MediaRouteProvider offers. You can then use those actions from your own client app, or document them for use by third-party apps.

The client app can use supportsControlRequest() and sendControlRequest() to determine whether a particular media route supports a particular Intent that represents an action to be performed by that route’s MediaRouteProvider. This way, a client app can work both with your custom MediaRouteProvider (taking advantage of your custom actions) and with regular providers that lack such support, assuming that the client can gracefully degrade its functionality.

Google’s sample app defines a custom ACTION_GET_STATISTICS action that their sample client requests where available and their sample provider implements.