Integrate Cast v3 into your Android app

This developer guide describes how to add Google Cast support to your Android sender app using Google Cast SDK v3.

The mobile device or laptop is the sender which controls the playback, and the Google Cast device is the receiver which displays the content on the TV.

The sender framework refers to the Cast class library binary and associated resources present at runtime on the sender. The sender app or Cast app refers to an app also running on the sender. The receiver app refers to the HTML application running on the receiver.

The sender framework uses an asynchronous callback design to inform the sender app of events and to transition between various states of the Cast app life cycle.

App flow

The following steps describe the typical high-level execution flow for a sender Android app:

  • The Cast framework automatically starts MediaRouter device discovery based on the Activity lifecycle.
  • When the user clicks on the Cast button, the framework presents the Cast dialog with the list of discovered Cast devices.
  • When the user selects a Cast device as a route, the framework attempts to launch the receiver app on the Cast device.
  • The framework invokes callbacks in the sender app to confirm that the receiver app was launched.
  • The framework creates a communication channel between the sender and receiver apps.
  • The framework uses the communication channel to load and control media playback on the receiver.
  • The framework synchronizes the media playback state between sender and receiver: when the user makes sender UI actions, the framework passes those media control requests to the receiver, and when the receiver sends media status updates, the framework updates the state of the sender UI.
  • When the user clicks on the Cast button to disconnect from the Cast device, the framework will disconnect the sender app from the receiver.

For a comprehensive list of all classes, methods and events in the Google Cast Android SDK, see the Google Cast Sender API Reference for Android. The following sections cover the steps for you to add Cast to your Android app.

Configure the Android manifest

Your app's AndroidManifest.xml file requires you to configure the following elements for the Cast SDK:

uses-sdk

Set the minimum Android SDK level that the Cast SDK supports is 9 (Gingerbread).

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

android:theme

Set your app's theme based on the minimum Android SDK version. For example, if you are not implementing your own theme, you should use a variant of Theme.AppCompat when targeting a minimum Android SDK version that is pre-Lollipop.

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

Initialize the Cast Context

The framework has a global singleton object, the CastContext, which coordinates all the framework's interactions.

Your app must implement the OptionsProvider interface to supply options needed to initialize the CastContext singleton. OptionsProvider provides an instance of CastOptions which contains options that affect the behavior of the framework. The most important of these is the receiver application ID, which is used to filter discovery results and to launch the receiver app when a cast session is started.

class CastOptionsProvider implements OptionsProvider {
    @Override
    public CastOptions getCastOptions(Context context) {
        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .build();
        return castOptions;
    }
    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

You must declare the fully qualified name of the implemented OptionsProvider as a metadata field in the AndroidManifest.xml file of the sender app:

<application>
    ...
    <meta-data
        android:name=
            "com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
        android:value="com.foo.CastOptionsProvider" />
</application>

CastContext is lazily initialized when the CastContext.getSharedInstance() is called.

public class MyActivity extends FragmentActivity {

    @Override
    public void onCreate() {
        CastContext castContext = CastContext.getSharedInstance(this);
    }
}

The Cast UX Widgets

The Cast framework provides the widgets that comply with the Cast Design Checklist:

  • Introductory Overlay: The framework provides a custom View, IntroductoryOverlay, that is shown to the user to call attention to the Cast button the first time a Cast receiver is available. The Sender app can customize the text and the position of the title text.

  • Cast Button: The cast button is visible when a receiver is discovered that supports your app. When the user first clicks on the cast button, a cast dialog is displayed which lists the discovered devices. When the user clicks on the cast button while the device is connected, it displays the current media metadata (such as title, name of the recording studio and a thumbnail image) or allows the user to disconnect from the cast device.

  • Mini Controller: When the user is casting content and has navigated away from the current content page or expanded controller to another screen in the sender app, the mini controller is displayed at the bottom of the screen to allow the user to see the currently casting media metadata and to control the playback.

  • Expanded Controller: When the user is casting content, if they click on the media notification or mini controller, the expanded controller launches, which displays the currently playing media metadata and provides several buttons to control the media playback.

  • Notification: Android only. When the user is casting content and navigates away from the sender app, a media notification is displayed that shows the currently casting media metadata and playback controls.

  • Lock Screen: Android only. When the user is casting content and navigates (or the device times out) to the lock screen, a media lock screen control is displayed that shows the currently casting media metadata and playback controls.

The following guide includes descriptions of how to add these widgets to your app.

Add a Cast Button

The Android MediaRouter APIs are designed to enable media display and playback on secondary devices. Android apps that use the MediaRouter API should include a Cast button as part of their user interface, to allow users to select a media route to play media on a secondary device such as a Cast device.

The framework makes adding a MediaRouteButton as a Cast button very easy. You should first add a menu item or a MediaRouteButton in the xml file that defines your menu, and use CastButtonFactory to wire it up with the framework.

// To add a Cast button, add the following snippet.
// menu.xml
<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" />

// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.java
@Override public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.main, menu);
    CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                                            menu,
                                            R.id.media_route_menu_item);
    return true;
}

Then, if your Activity inherits from FragmentActivity, you can add a MediaRouteButton to your layout.

// activity_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:gravity="center_vertical"
   android:orientation="horizontal" >

   <android.support.v7.app.MediaRouteButton
       android:id="@+id/media_route_button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_weight="1"
       android:mediaRouteTypes="user"
       android:visibility="gone" />

</LinearLayout>

// MyActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_layout);

   mMediaRouteButton = (MediaRouteButton) findViewById(R.id.media_route_button);
   CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), mMediaRouteButton);

   mCastContext = CastContext.getSharedInstance(this);
}

To set the appearance of the Cast button using a theme, see Customize Cast Button.

Configure device discovery

Device discovery is completely managed by the CastContext. When initializing the CastContext, the sender app specifies the receiver application ID, and can optionally request namespace filtering by setting supportedNamespaces in CastOptions. CastContext holds a reference to the MediaRouter internally, and will start the discovery process when the sender app enters the foreground, and stop when the sender app enters background.

class CastOptionsProvider implements OptionsProvider {
    public static final String CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace";
    @Override
    public CastOptions getCastOptions(Context appContext) {
        List<String> supportedNamespaces = new ArrayList<>();
        supportedNamespaces.add(CUSTOM_NAMESPACE);
        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build();
        return castOptions;
    }
    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

Support Auto-Managed Discovery in Gingerbread

Gingerbread lacks certain methods used by the framework to perform automatic discovery. If the sender app needs to support Gingerbread, then it needs to call CastContext method registerLifecycleCallbacksBeforeIceCreamSandwich() in the onCreate() method for each and every one of its Activities, so that the framework knows when to start and stop discovery.

class MyActivity extends FragmentActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        CastContext.getSharedInstance(this)
            .registerLifecycleCallbacksBeforeIceCreamSandwich(this,
                    savedInstanceState);
    }
}

How session management works

The Cast framework introduces the concept of a session, which includes connecting to a device, launching (or joining) a receiver app, connecting to that app, and initializing a media control channel, if appropriate. From the user's perspective, a session starts when the user chooses a receiver from the Cast dialog and stops when either a user chooses Stop Casting or until another sender casts something to the same device.

Sessions are managed by the class SessionManager, which your app can access via CastContext.getSessionManager(). Individual sessions are represented by subclasses of the class Session. For example, CastSession represents sessions with Cast devices. Your app can access the currently active Cast session as SessionManager.getCurrentCastSession().

Your app can use the SessionManagerListener class to monitor session events, such as creation, suspension, resumption, and termination. The framework automatically attempts to resume from an abnormal/abrupt termination while a session was active.

Sessions are created and torn down automatically in response to user gestures from the MediaRouter dialogs.

If you need to be aware of the state changes for the session, you can implement a SessionManagerListener. This example listens to the availability of a CastSession in an Activity.

public class MyActivity extends Activity {
    private CastSession mCastSession;
    private SessionManager mSessionManager;
    private final SessionManagerListener mSessionManagerListener =
            new SessionManagerListenerImpl();
    private class SessionManagerListenerImpl implements SessionManagerListener {
        @Override
        public void onSessionStarted(Session session, String sessionId) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionResumed(Session session, boolean wasSuspended) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionEnded(Session session, int error) {
            finish();
        }
    }
    @Override
    protected void onCreate() {
        mSessionManager = CastContext.getSharedInstance(this).getSessionManager();
        super.onCreate();
    }
    @Override
    protected void onResume() {
        mCastSession = mSessionManager.getCurrentCastSession();
        mSessionManager.addSessionManagerListener(mSessionManagerListener);
        super.onResume();
    }
    @Override
    protected void onPause() {
        super.onPause();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener);
        mCastSession = null;
    }
}

Automatic Reconnection

The framework provides a ReconnectionService which can be enabled by the sender app to handle reconnection in many subtle corner cases, such as:

  • Recover from a temporary loss of WiFi
  • Recover from device sleep
  • Recover from backgrounding the app
  • Recover if the app crashed

This service is turned on by default, and can be turned off in CastOptions.Builder.

This service can be automatically merged into your app's manifest if auto-merge is enabled in your gradle file.

The framework will start the service when there is a media session, and stop it when the media session ends.

How Media Control works

Cast v3 framework deprecates the RemoteMediaPlayer class from Cast 2.x in favor of a new class RemoteMediaClient, which provides the same functionality in a set of more convenient APIs, and avoids having to pass in a GoogleApiClient.

When your app establishes a CastSession with a receiver app that supports the media namespace, an instance of RemoteMediaClient will automatically be created by the framework; your app can access it by calling getRemoteMediaClient() method on the CastSession instance.

All methods of RemoteMediaClient that issue requests to the receiver will return a PendingResult object that can be used to track that request.

It is expected that the instance of RemoteMediaClient may be shared by multiple parts of your app, and indeed some internal components of the framework, such as the persistent mini controllers and the notification service. To that end, this instance supports registration of multiple instances of RemoteMediaClient.Listener.

Set Media Metadata

The MediaMetadata class represents the information about a media item you want to cast. The following example creates a new MediaMetadata instance of a movie and sets the title, subtitle and two images.

MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle());
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio());
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(0))));
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(1))));

See Image Selection on the use of images with media metadata.

Load Media

Your app can load a media item, as shown in the following code. First use MediaInfo.Builder with the media's metadata to build a MediaInfo instance. Get the RemoteMediaClient from the current CastSession, then load the MediaInfo into that RemoteMediaClient. Use RemoteMediaClient to play, pause, and otherwise control a media player app running on the receiver.

MediaInfo mediaInfo = new MediaInfo.Builder(mSelectedMedia.getUrl())
                .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
                .setContentType("videos/mp4")
                .setMetadata(movieMetadata)
                .setStreamDuration(mSelectedMedia.getDuration() * 1000)
                .build();
RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
remoteMediaClient.load(mediaInfo, autoPlay, position);

Also see the section on using media tracks.

4K Video Format

To check what video format your media is, use getVideoInfo() in MediaStatus to get the current instance of VideoInfo. This instance contains the type of HDR TV format and the display height and width in pixels. Variants of 4K format are indicated by constants HDR_TYPE_*.

Remote Control Notifications to Multiple Devices

When a user is casting, other Android devices on the same network will get a notification to also let them control the playback. Anyone whose device receives such notifications can turn them off for that device in the Settings app at Google > Google Cast > Show remote control notifications. (The notifications include a shortcut to the Settings app.) For more detail, see Cast remote control notifications.

Add Mini Controller

According to the Cast Design Checklist, a sender app should provide a persistent control known as the mini controller that should appear when the user navigates away from the current content page to another part of the sender app. The mini controller provides a visible reminder to the user of the current Cast session. By tapping on the mini controller, the user can return to the Cast full-screen expanded controller view.

The framework provides a custom View, MiniControllerFragment, which you can add to the bottom of the layout file of each activity in which you want to show the mini controller.

<fragment
    android:id="@+id/castMiniController"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:visibility="gone"
    class="com.google.android.gms.cast.framework.media.widget.
           MiniControllerFragment" />

When your sender app is playing a video or audio live stream, the SDK automatically displays a play/stop button in place of the play/pause button in the mini controller.

To set the text appearance of the title and subtitle of this custom view, and to choose buttons, see Customize Mini Controller.

Add Expanded Controller

The Google Cast Design Checklist requires that a sender app provide an expanded controller for the media being cast. The expanded controller is a full screen version of the mini controller.

The Cast SDK provides a widget for the expanded controller called ExpandedControllerActivity. This is an abstract class you have to subclass to add a Cast button.

First, create a new menu resource file for the expanded controller to provide the Cast button:

<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>

Create a new class that extends ExpandedControllerActivity in the com.google.sample.cast.refplayer.expandedcontrols package.

public class ExpandedControlsActivity extends ExpandedControllerActivity {

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.expanded_controller, menu);
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
        return true;
    }
}

Now declare your new activity in the app manifest within the application tag:

<application>
...
<activity
        android:name=".expandedcontrols.ExpandedControlsActivity"
        android:label="@string/app_name"
        android:launchMode="singleTask"
        android:theme="@style/Theme.CastVideosDark"
        android:screenOrientation="portrait">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
    </intent-filter>
    <meta-data
            android:name="android.support.PARENT_ACTIVITY"
            android:value="com.google.sample.cast.refplayer.VideoBrowserActivity"/>
</activity>
...
</application>

Edit the CastOptionsProvider and change NotificationOptions and CastMediaOptions to set the target activity to your new activity:

public CastOptions getCastOptions(Context context) {
    NotificationOptions notificationOptions = new NotificationOptions.Builder()
            .setTargetActivityClassName(ExpandedControlsActivity.class.getName())
            .build();
    CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
            .setNotificationOptions(notificationOptions)
            .setExpandedControllerActivityClassName(ExpandedControlsActivity.class.getName())
            .build();

    return new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setCastMediaOptions(mediaOptions)
            .build();
}

Update the LocalPlayerActivity loadRemoteMedia method to display your new activity when the remote media is loaded:

private void loadRemoteMedia(int position, boolean autoPlay) {
    if (mCastSession == null) {
        return;
    }
    final RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
    if (remoteMediaClient == null) {
        return;
    }
    remoteMediaClient.addListener(new RemoteMediaClient.Listener() {
        @Override
        public void onStatusUpdated() {
            Intent intent = new Intent(LocalPlayerActivity.this, ExpandedControlsActivity.class);
            startActivity(intent);
            remoteMediaClient.removeListener(this);
        }

        @Override
        public void onMetadataUpdated() {
        }

        @Override
        public void onQueueStatusUpdated() {
        }

        @Override
        public void onPreloadStatusUpdated() {
        }

        @Override
        public void onSendingRemoteMediaRequest() {
        }
    });
    remoteMediaClient.load(mSelectedMedia, autoPlay, position);
}

When your sender app is playing a video or audio live stream, the SDK automatically displays a play/stop button in place of the play/pause button in the expanded controller.

To set the appearance using themes, choose which buttons to display, and add custom buttons, see Customize Expanded Controller.

Add Media Controls to Notification and Lock Screen

On Android only, the Google Cast Design Checklist requires a sender app to implement media controls in a notification and in the lock screen, where the sender is casting but the sender app does not have focus. The framework provides MediaNotificationService and MediaIntentReceiver to help the sender app build media controls in a notification and in the lock screen.

MediaNotificationService runs in the background when the sender is casting but does not have focus, and will show a notification with image thumbnail and information about the current casting item, a play/pause button and a stop button.

MediaIntentReceiver is a BroadcastReceiver that handles user actions from the notification.

Your app can configure notification and media control from lock screen through NotificationOptions. Your app can configure what control buttons to show in the notification, and which Activity to open when the notification is tapped by the user. If actions are not explicitly provided, the default values, MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK and MediaIntentReceiver.ACTION_STOP_CASTING will be used.

// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop
// casting".
List<String> buttonActions = new ArrayList<>();
buttonActions.add(MediaIntentReceiver.ACTION_REWIND);
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK);
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD);
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING);
// Showing "play/pause" and "stop casting" in the compat view of the notification.
int[] compatButtonActionsIndicies = new int[]{ 1, 3 };
// Builds a notification with the above actions. Each tap on the "rewind" and
// "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
NotificationOptions notificationOptions = new NotificationOptions.Builder()
        .setActions(buttonActions, compatButtonActionsIndicies)
        .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
        .setTargetActivityClassName(VideoBrowserActivity.class.getName())
    .build();

Showing media controls from notification and lock screen are turned on by default, and can disabled by calling setNotificationOptions with null in CastMediaOptions.Builder. Currently, the lock screen feature is turned on as long as notification is turned on.

// ... continue with the NotificationOptions built above
CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
        .setNotificationOptions(notificationOptions)
        .build();
CastOptions castOptions = new CastOptions.Builder()
        .setReceiverApplicationId(context.getString(R.string.app_id))
        .setCastMediaOptions(mediaOptions)
        .build();

When your sender app is playing a video or audio live stream, the SDK automatically displays a play/stop button in place of the play/pause button on the notification control but not the lock screen control.

Note: To display lock screen controls on pre-Lollipop devices, RemoteMediaClient will automatically request audio focus on your behalf.

Handle errors

It is very important for sender apps to handle all error callbacks and decide the best response for each stage of the Cast life cycle. The app can display error dialogs to the user or it can decide to tear down the connection to the receiver.

Next Step

To add more Cast v3 features to your Android sender app, visit Add Advanced Features.