Google Cast

Android Sender App Development

This overview shows how to build Google Cast sender applications for Android using the Google Cast SDK.

In this overview, sender application or Google Cast application refers to an app running on a mobile device (the sender device) and receiver application refers to an HTML application running on Chromecast or other Google Cast devices.

The Cast SDK uses an asynchronous callback design to inform the application of events and to move between various states of the Cast app life cycle.

Setup

Before you start

The Google Cast SDK for Android is part of the Google Play services SDK and does not need to be downloaded separately.

Library dependencies

The following libraries are required as dependencies for your app:

  • android-support-v7-appcompat which can be found at <SDK install location>/extras/android/support/v7/appcompat
  • android-support-v7-mediarouter which can be found at <SDK install location>/extras/android/support/v7/mediarouter (this has a dependency on android-support-v7-appcompat)
  • google-play-services_lib which can be found at <SDK install location>/extras/google/google_play_services/libproject/google-play-services_lib

    It is important for you to ensure that the correct Google Play services APK is installed on a user’s device since updates might not reach all users immediately.

Note: Since the libraries contribute resources, you cannot simply satisfy the dependencies by including their JAR files; instead you need to import them as library projects for your IDE.

For Eclipse, if you get errors when importing the libraries, try the following:

  • android-support-v7-mediarouter has a dependency on android-support-v7-appcompat, so make sure that is imported by selecting the android-support-v7-mediarouter project Properties, then select Android, and in the Libraries list, add android-support-v7-appcompat.
  • Ensure the build target for each of the imported libraries are correct: select the library project Properties and then select Android. Select a different Project Build Target, select "Apply", then re-select the desired target (> API 17) and hit "Apply" again.
  • In your code be careful to reference the MediaRouter classes from the v7 support version of the MediaRouter library (android.support.v7.media.*) and not the classes included in the Android framework (android.media.*).
  • You might have to clean your app project or restart Eclipse if the errors persists after trying all of the steps above.
)

Testing

Use a real Android device to test your code as you develop; do not use an emulator, as it does not support Cast functionality.

Development

Android manifest

Your AndroidManifest.xml file requires the following configuration to use the Cast SDK:

uses-sdk

The minimum Android SDK version that the Cast SDK supports is 9 (GingerBread).

<uses-sdk
        android:minSdkVersion="9"
        android:targetSdkVersion="19" />

meta-data

The Google Play services library need to be referenced inside the application element.

<meta-data
       android:name="com.google.android.gms.version"
       android:value="@integer/google_play_services_version" />

android:theme

The application’s theme needs to be correctly set based on the minimum Android SDK version. For example, you may need to use a variant of Theme.AppCompat.

<application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.AppCompat" >
       ...
    </application>

Typical sender application flow

The following sections cover the details of the typical execution flow for a sender application; here is a high-level list of the steps:

  • Sender app starts MediaRouter device discovery: MediaRouter.addCallback
  • MediaRouter informs sender app of the route the user selected: MediaRouter.Callback.onRouteSelected
  • Sender app retrieves CastDevice instance: CastDevice.getFromBundle
  • Sender app creates a GoogleApiClient: GoogleApiClient.Builder
  • Sender app connects the GoogleApiClient: GoogleApiClient.connect
  • SDK confirms that GoogleApiClient is connected: GoogleApiClient.ConnectionCallbacks.onConnected
  • Sender app launches the receiver app: Cast.CastApi.launchApplication
  • SDK confirms that the receiver app is connected: ResultCallback<Cast.ApplicationConnectionResult>
  • Sender app creates a communication channel: Cast.CastApi.setMessageReceivedCallbacks
  • Sender sends a message to the receiver over the communication channel: Cast.CastApi.sendMessage

For a comprehensive listing of all classes, methods and events in the Google Cast Android SDK, see the Google Cast Android API Reference.

Adding the Cast Button

Cast device discovery may be performed using the Android MediaRouter APIs in the Android Support Library, with compatibility back to Android 2.1.

For more information about the Android MediaRouter, see the MediaRouter Developer Guide.

The MediaRouter framework provides a Cast button and a list selection dialog for selecting a route. The MediaRouter framework interfaces with the Cast SDK via a MediaRouteProvider implementation to perform the discovery on behalf of the application.

According to the Google Cast UX Guidelines, the sender application must provide a top-level Cast button. There are three ways to support a Cast button:

  • Using the MediaRouter ActionBar provider: android.support.v7.app.MediaRouteActionProvider
  • Using the MediaRouter Cast button: android.support.v7.app.MediaRouteButton
  • Developing a custom UI with the MediaRouter API’s and MediaRouter.Callback

This document describes the use of the MediaRouteActionProvider to add the Cast button to the ActionBar.

The MediaRouter ActionBar provider needs to be added to the application’s menu hierarchy defined in XML:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" >
    <item
        android:id="@+id/media_route_menu_item"
        android:title="@string/media_route_menu_title"
        app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
        app:showAsAction="always"/>
    ...
</menu>

The application Activity needs to extend ActionBarActivity:

public class MainActivity extends ActionBarActivity {
...
}

The application needs to obtain an instance of the MediaRouter and needs to hold onto that instance for the lifetime of the sender application:

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  ...
  mMediaRouter = MediaRouter.getInstance(getApplicationContext());
}

The MediaRouter needs to filter discovery for Cast devices that can launch the receiver application associated with the sender app. For that a MediaRouteSelector is created by calling MediaRouteSelector.Builder:

mMediaRouteSelector = new MediaRouteSelector.Builder()
    .addControlCategory(CastMediaControlIntent.categoryForCast("YOUR_APPLICATION_ID"))
    .build();

The MediaRouteSelector is then assigned to the MediaRouteActionProvider in the ActionBar menu:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
  super.onCreateOptionsMenu(menu);
  getMenuInflater().inflate(R.menu.main, menu);
  MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item);
  MediaRouteActionProvider mediaRouteActionProvider = 
    (MediaRouteActionProvider) MenuItemCompat.getActionProvider(mediaRouteMenuItem);
  mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector);
  return true;
}

Now MediaRouter will use the selector to filter the devices that are displayed to the user when the Cast button in the ActionBar is pressed.

Handling device selection

When the user selects a device from the Cast button device list, the application is informed of the selected device by extending MediaRouter.Callback:

private class MyMediaRouterCallback extends MediaRouter.Callback {

  @Override
  public void onRouteSelected(MediaRouter router, RouteInfo info) {
    mSelectedDevice = CastDevice.getFromBundle(info.getExtras());
    String routeId = info.getId();
    ...
  }

  @Override
  public void onRouteUnselected(MediaRouter router, RouteInfo info) {
    teardown();
    mSelectedDevice = null;
  }
}

The application needs to trigger the discovery of devices by adding the MediaRouter.Callback to the MediaRouter instance. Typically this callback is assigned when the application Activity is active and then removed when the Activity goes into the background:

@Override
protected void onResume() {
  super.onResume();
  mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback,
				MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
}

@Override
protected void onPause() {
  if (isFinishing()) {
    mMediaRouter.removeCallback(mMediaRouterCallback);
  }
  super.onPause();
}

Launching the receiver

Once the application knows which Cast device the user selected, the sender application can launch the receiver application on that device.

The Cast SDK API’s are invoked using GoogleApiClient. A GoogleApiClient instance is created using the GoogleApiClient.Builder and requires various callbacks that are discussed later in this document:

Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions
		.builder(mSelectedDevice, mCastClientListener);

mApiClient = new GoogleApiClient.Builder(this)
			.addApi(Cast.API, apiOptionsBuilder.build())
			.addConnectionCallbacks(mConnectionCallbacks)
			.addOnConnectionFailedListener(mConnectionFailedListener)
			.build();

The application can then establish a connection using the GoogleApiClient instance:

mApiClient.connect();

The application needs to declare GoogleApiClient.ConnectionCallbacks and GoogleApiClient.OnConnectionFailedListener callbacks to be informed of the connection status. All of the Google Play services callbacks run on the main UI thread. Once the connection is confirmed, the application can launch the application by specifying the application ID issued for your app upon Registration:

private class ConnectionCallbacks implements
      GoogleApiClient.ConnectionCallbacks {
  @Override
  public void onConnected(Bundle connectionHint) {
    if (mWaitingForReconnect) {
      mWaitingForReconnect = false;
      reconnectChannels();
    } else {
      try {
        Cast.CastApi.launchApplication(mApiClient, "YOUR_APPLICATION_ID", false)
          .setResultCallback(
             new ResultCallback<Cast.ApplicationConnectionResult>() {
            @Override
            public void onResult(Cast.ApplicationConnectionResult result) {
                Status status = result.getStatus();
                if (status.isSuccess()) {
                  ApplicationMetadata applicationMetadata = 
                                                  result.getApplicationMetadata();
                  String sessionId = result.getSessionId();
                  String applicationStatus = result.getApplicationStatus();
                  boolean wasLaunched = result.getWasLaunched();
                  ...
                } else {
                  teardown();
                }
            }
        });

      } catch (Exception e) {
        Log.e(TAG, "Failed to launch application", e);
      }
    }
  }

  @Override
  public void onConnectionSuspended(int cause) {
    mWaitingForReconnect = true;
  }
}

private class ConnectionFailedListener implements
      GoogleApiClient.OnConnectionFailedListener {
  @Override
  public void onConnectionFailed(ConnectionResult result) {
    teardown();
  }
}

If GoogleApiClient.ConnectionCallbacks.onConnectionSuspended is invoked when the client is temporarily in a disconnected state, your application needs to track the state, so that if GoogleApiClient.ConnectionCallbacks.onConnected is subsequently invoked when the connection is established again, the application should be able to distinguish this from the initial connected state. It is important to re-create any channels when the connection is re-established.

The Cast.Listener callbacks are used to inform the sender application about receiver application events:

mCastClientListener = new Cast.Listener() {
  @Override
  public void onApplicationStatusChanged() {
    if (mApiClient != null) {
      Log.d(TAG, "onApplicationStatusChanged: "
       + Cast.CastApi.getApplicationStatus(mApiClient));
    }
  }

  @Override
  public void onVolumeChanged() {
    if (mApiClient != null) {
      Log.d(TAG, "onVolumeChanged: " + Cast.CastApi.getVolume(mApiClient));
    }
  }

  @Override
  public void onApplicationDisconnected(int errorCode) {
    teardown();
  }
};

Custom channel

For the sender application to communicate with the receiver application, a custom channel needs to be created. The sender can use the custom channel to send String messages to the receiver. Each custom channel is defined by a unique namespace and must start with the prefix urn:x-cast:, for example, urn:x-cast:com.example.custom. It is possible to have multiple custom channels, each with a unique namespace.

The custom channel is implemented with the Cast.MessageReceivedCallback interface:

class HelloWorldChannel implements Cast.MessageReceivedCallback {
  public String getNamespace() {
    return "urn:x-cast:com.example.custom";
  }

  @Override
  public void onMessageReceived(CastDevice castDevice, String namespace,
        String message) {
    Log.d(TAG, "onMessageReceived: " + message);
  }
}

Once the sender application is connected to the receiver application, the custom channel can be created using Cast.CastApi.setMessageReceivedCallbacks:

Cast.CastApi.launchApplication(mApiClient, "YOUR_APPLICATION_ID", false)
          .setResultCallback(
             new ResultCallback<Cast.ApplicationConnectionResult>() {
            @Override
            public void onResult(Cast.ApplicationConnectionResult result) {
                Status status = result.getStatus();
                if (status.isSuccess()) {
                  ApplicationMetadata applicationMetadata = 
                                                  result.getApplicationMetadata();
                  String sessionId = result.getSessionId();
                  String applicationStatus = result.getApplicationStatus();
                  boolean wasLaunched = result.getWasLaunched();
                  
                  mApplicationStarted = true;

                  mHelloWorldChannel = new HelloWorldChannel();
                  try {
                    Cast.CastApi.setMessageReceivedCallbacks(mApiClient,
                          mHelloWorldChannel.getNamespace(),
                          mHelloWorldChannel);
                  } catch (IOException e) {
                    Log.e(TAG, "Exception while creating channel", e);
                  }
                }
            }
        });

Once the custom channel is created, the sender can use that to send String messages to the receiver over that channel:

private void sendMessage(String message) {
 if (mApiClient != null && mHelloWorldChannel != null) {
  try {
    Cast.CastApi.sendMessage(mApiClient, mHelloWorldChannel.getNamespace(), message)
    .setResultCallback(
      new ResultCallback<Status>() {
        @Override
        public void onResult(Status result) {
          if (!result.isSuccess()) {
            Log.e(TAG, "Sending message failed");
          }
        }
      });
  } catch (Exception e) {
    Log.e(TAG, "Exception while sending message", e);
  }
 }
}

The application can encode JSON messages into a String, if needed, and then decode the JSON String in the receiver.

Media channel

The Google Cast SDK supports a media channel to play media on a receiver application. The media channel has a well-known namespace of urn:x-cast:com.google.cast.media.

To use the media channel create an instance of RemoteMediaPlayer and set the update listeners to receive media status updates:

mRemoteMediaPlayer = new RemoteMediaPlayer();
mRemoteMediaPlayer.setOnStatusUpdatedListener(
                           new RemoteMediaPlayer.OnStatusUpdatedListener() {
  @Override
  public void onStatusUpdated() {
    MediaStatus mediaStatus = mRemoteMediaPlayer.getMediaStatus();
    boolean isPlaying = mediaStatus.getPlayerState() == 
            MediaStatus.PLAYER_STATE_PLAYING;
    ...
  }
});

mRemoteMediaPlayer.setOnMetadataUpdatedListener(
                           new RemoteMediaPlayer.OnMetadataUpdatedListener() {
  @Override
  public void onMetadataUpdated() {
    MediaInfo mediaInfo = mRemoteMediaPlayer.getMediaInfo();
    MediaMetadata metadata = mediaInfo.getMetadata();
    ...
  }
});

Once the sender application is connected to the receiver application, the media channel can be created using Cast.CastApi.setMessageReceivedCallbacks:

try {
 Cast.CastApi.setMessageReceivedCallbacks(mApiClient,
         mRemoteMediaPlayer.getNamespace(), mRemoteMediaPlayer);
} catch (IOException e) {
  Log.e(TAG, "Exception while creating media channel", e);
}
mRemoteMediaPlayer
  .requestStatus(mApiClient)
  .setResultCallback(
    new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
      @Override
      public void onResult(MediaChannelResult result) {
        if (!result.getStatus().isSuccess()) {
          Log.e(TAG, "Failed to request status.");
        }
      }
    });

You need to call RemoteMediaPlayer.requestStatus() and wait for the OnStatusUpdatedListener callback. This will update the internal state of the RemoteMediaPlayer object with the current state of the receiver, including the current session ID.

To load media, the sender application needs to create a MediaInfo instance using MediaInfo.Builder. The MediaInfo instance is then used to load the media with the RemoteMediaPlayer instance:

MediaMetadata mediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
mediaMetadata.putString(MediaMetadata.KEY_TITLE, "My video");
MediaInfo mediaInfo = new MediaInfo.Builder(
    "http://your.server.com/video.mp4")
    .setContentType("video/mp4")
    .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
    .setMetadata(mediaMetadata)
              .build();
try {
  mRemoteMediaPlayer.load(mApiClient, mediaInfo, true)
     .setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
    @Override
    public void onResult(MediaChannelResult result) {
      if (result.getStatus().isSuccess()) {
        Log.d(TAG, "Media loaded successfully");
      }
    }
     });
} catch (IllegalStateException e) {
  Log.e(TAG, "Problem occurred with media during loading", e);
} catch (Exception e) {
  Log.e(TAG, "Problem opening media during loading", e);
}

Once the media is playing, the sender application can control the media playback using the RemoteMediaPlayer instance:

mRemoteMediaPlayer.pause(mApiClient).setResultCallback(
new ResultCallback<MediaChannelResult>() {
  @Override
  public void onResult(MediaChannelResult result) {
    Status status = result.getStatus();
    if (!status.isSuccess()) {
       Log.w(TAG, "Unable to toggle pause: "
               + status.getStatusCode());
    }
  }
});

Joining sessions

The Google Cast SDK APIs provide ways to support multiple sender devices:

  • Cast.CastApi.launchApplication() always attempts a join first, unless you tell it to relaunch unconditionally.
  • Cast.CastApi.joinApplication() with an application ID will only succeed if that particular application is still running on the receiver.

A running session can be re-joined by recalling the session ID supplied in the Cast.ApplicationConnectionResult.onResult ResultCallback:

  • Cast.CastApi.joinApplication() with both an application ID and a session ID will only succeed if that particular instance of that application is still running.

Restoring sessions

According to the UX Guidelines, if the sender application becomes disconnected from the media route, such as when the user or the operating system kills the application without the user first disconnecting from the Cast device, then the application must restore the session with the receiver when the sender application starts again.

To handle this use case the sender application has to persist the route ID and session ID during the Cast app life cycle. If the user explicitly disconnects from a Cast device, these persisted data should be cleared to avoid the automatic reconnection logic to be invoked when the application starts again.

Here are the steps that a sender application should implement when the main Activity is started:

  1. The application should determine if restoring the previous session should be attempted based on the presence of the persisted session data.
  2. Iterate through the list of routes already discovered using MediaRouter.getRoutes(). Determine if the persisted route ID matches any of these routes.
  3. If the persisted route ID could not be matched, skip to step 7.
  4. Store the RouteInfo instance of the route that matches the persisted route ID.
  5. Extract the CastDevice instance from the RouteInfo instance.
  6. Follow the previously documented instructions to connect to the CastDevice instance using GoogleApiClient.connect(). Attempt to join the running application calling Cast.CastApi.joinApplication() with the persisted session ID. If joining the application succeeds, it confirms that the same session is still running, and MediaRouter.selectRoute() can be called with the stored RouteInfo instance. However, if joining the application fails, a different session is now running on the device so you need to disconnect from the CastDevice instance. You are done!
  7. If the app couldn’t find a matching route from MediaRouter.getRoutes(), it could be that the route is not discovered yet by the asynchronous discovery process. Start a timer with a short life time, like 5 seconds, and start listening to the routes reported by MediaRouter.Callback.onRouteAdded. If the desired route doesn’t show up before the timer expires, you can stop the reconnection process.
  8. If the persisted route ID is matched before the timer expires, extract the CastDevice instance from that route. Connect to the CastDevice instance using GoogleApiClient.connect() and once connected, call Cast.CastApi.joinApplication() with the persisted session ID. If joining the application succeeds, it confirms that the same session is still running, and MediaRouter.selectRoute() can be called with the stored RouteInfo instance. However, if joining the application fails, a different session is now running on the device so you need to disconnect from the CastDevice instance.

A full implementation of this process is provided as a reference in the open sourced companion library used by the sample video player (see BaseCastManager.reconnectSessionIfPossible()).

Notifications

To implement support for notifications that comply with the UX Guidelines, you must create a service. This will handle use cases where the application is closed by the user, but media that was launched from that application is still casting on the receiver.

Lock screen

The lock screen has to be implemented for Android 4.1 and later using RemoteControlClient. For the RemoteControlClient to display on the lock screen while casting media, the application needs to grab transient or exclusive audio focus:

mAudioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC,
                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);

Volume

According to the UX Guidelines, sender applications must allow the user to control the device volume while the media is being cast. The sender application should allow the user to use the hardware volume buttons to set the receiver volume:

@Override
public boolean dispatchKeyEvent(KeyEvent event) {
  int action = event.getAction();
  int keyCode = event.getKeyCode();
  switch (keyCode) {
    case KeyEvent.KEYCODE_VOLUME_UP:
      if (action == KeyEvent.ACTION_DOWN) {
        if (mRemoteMediaPlayer != null) {
          double currentVolume = Cast.CastApi.getVolume(mApiClient);
          if (currentVolume < 1.0) {
            try {
              Cast.CastApi.setVolume(mApiClient, 
            Math.min(currentVolume + VOLUME_INCREMENT, 1.0));
            } catch (Exception e) {
              Log.e(TAG, "unable to set volume", e);
            }
          }
        } else {
          Log.e(TAG, "dispatchKeyEvent - volume up");
        }
      }
      return true;
    case KeyEvent.KEYCODE_VOLUME_DOWN:
      if (action == KeyEvent.ACTION_DOWN) {
        if (mRemoteMediaPlayer != null) {
          double currentVolume = Cast.CastApi.getVolume(mApiClient);
          if (currentVolume > 0.0) {
            try {
              Cast.CastApi.setVolume(mApiClient, 
            Math.max(currentVolume - VOLUME_INCREMENT, 0.0));
            } catch (Exception e) {
              Log.e(TAG, "unable to set volume", e);
            }
          }
        } else {
          Log.e(TAG, "dispatchKeyEvent - volume down");
        }
      }
      return true;
    default:
      return super.dispatchKeyEvent(event);
  }
}

Error handling

It is very important for sender applications to handle all error callbacks and decide the best response for each stage of the Cast life cycle. The application can display error dialogs to the user or it can decide to tear down the connection to the receiver. Tearing down the connection has to be done in a particular sequence:

private void teardown() {
  Log.d(TAG, "teardown");
    if (mApiClient != null) {
      if (mApplicationStarted) {
        if (mApiClient.isConnected()) {
          try {
            Cast.CastApi.stopApplication(mApiClient, mSessionId);
            if (mHelloWorldChannel != null) {
              Cast.CastApi.removeMessageReceivedCallbacks(
                mApiClient,
                mHelloWorldChannel.getNamespace());
              mHelloWorldChannel = null;
            }
          } catch (IOException e) {
                 Log.e(TAG, "Exception while removing channel", e);
          }
          mApiClient.disconnect();
        }
        mApplicationStarted = false;
      }
      mApiClient = null;
    }
  mSelectedDevice = null;
  mWaitingForReconnect = false;
  mSessionId = null;
}

Logging

The Cast SDK has some useful logging facilities that provide insight into what is happening and can help in debugging. This logging is disabled by default but can be programmatically enabled as follows:

Cast.CastOptions apiOptions = Cast.CastOptions.builder(mSelectedDevice,
  mCastClientListener)
  .setVerboseLoggingEnabled(true)
  .build();

Sample apps

Several Google Cast sample apps have been open sourced on GitHub. These include a video player, a hello world app and a companion library project that can be used by existing applications to add Cast support. The Android SDK also includes 2 sample apps (TicTacToe and DemoCastPlayer) at <SDK install location>/extras/google/google_play_services/samples/cast/. It is highly recommended that you import these apps into your development environment and get them running on your devices. This will ensure that your development environment is ready to create your own Cast apps.

Authentication required

You need to be signed in with Google+ to do that.

Signing you in...

Google Developers needs your permission to do that.