Add Core Features to Your Android TV Receiver

This page contains code snippets and descriptions of the features available for customizing an Android TV Receiver app.

Getting started

Before you get started, review the sample code:

Sample Code

The code samples include two Android projects. CastConnect-AndroidTV contains a simple 'hello world' version of Cast Connect integration. The CastConnect-TestHarness folder contains a Web sender, Android sender, iOS sender and Android TV receiver application. The Android Sender and Android TV receiver are a combined project that demonstrates how to use more advanced Cast Connect features like queueing and the media status modifiers.

You can also check out the Cast Connect codelab with step-by-step instructions that take you through the process of enabling Cast Connect support to an ATV app.

Configuring libraries

To make Cast Connect APIs available to your Android TV app:

  1. Open the build.gradle file inside your application module directory.
  2. Verify that google() is included in the listed repositories.
    repositories {
        google()
    }
  3. Add the latest versions of play-services-cast-tv (17.0.0 or higher) and play-services-cast (19.0.0 or higher) to your dependencies:
    dependencies {
        implementation 'com.google.android.gms:play-services-cast-tv:17.0.0'
        implementation 'com.google.android.gms:play-services-cast:19.0.0'
    }
    Be sure you update this version number each time the services are updated.
  4. Save the changes and click Sync Project with Gradle Files in the toolbar.

AndroidX requirement

New versions of Google Play Services require an app to have been updated to use the androidx namespace. Follow the instructions for migrating to AndroidX.

Android TV app—prerequisites

In order to support Cast Connect in your Android TV app, you must create and support events from a media session. The data provided by your media session provides the basic information—for example, position, playback state, etc.—for your media status. Your media session also is used by the Cast Connect library to signal when it has received certain messages from a sender, like pause.

For more information on media session and how to initialize a media session, see the working with a media session guide.

Media session lifecycle

Your app should create a media session when playback starts and release it when it can’t be controlled any more. For example, if your app is a video app, you should release the session when the user exits the playback activity—either by selecting 'back' to browse other content or by backgrounding the app. If your app is a music app, you should release it when your app is no longer playing any media.

Updating session status

The data in your media session should be kept up-to-date with the status of your player. For example, when playback is paused, you should update the playback state as well as the supported actions. The following tables list what states you are responsible for keeping up to date.

MediaMetadataCompat

Metadata Field Description
METADATA_KEY_DISPLAY_TITLE/METADATA_KEY_TITLE (required) The media title.
METADATA_KEY_DISPLAY_SUBTITLE The subtitle.
METADATA_KEY_DISPLAY_ICON_URI The icon URL.
METADATA_KEY_DURATION (required) Media duration.
METADATA_KEY_MEDIA_URI The Content ID.
METADATA_KEY_ARTIST The artist.
METADATA_KEY_ALBUM The album.

PlaybackStateCompat

Required Method Description
setActions() Sets supported media commands.
setState() Set the playing state and current position.

MediaSessionCompat

Required Method Description
setRepeatMode() Sets repeat mode.
setShuffleMode() Sets shuffle mode.
setMetadata() Sets media metadata.
setPlaybackState() Sets playback state.
private void updateMediaSession() {
  MediaMetadataCompat metadata =
      new MediaMetadataCompat.Builder()
          .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, "title")
          .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "subtitle")
          .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,mMovie.getCardImageUrl())
          .build();

  PlaybackStateCompat playbackState =
      new PlaybackStateCompat.Builder()
          .setState(
               PlaybackStateCompat.STATE_PLAYING,
               player.getPosition(),
               player.getPlaybackSpeed(),
               System.currentTimeMillis())
          .build();

  mediaSession.setMetadata(metadata);
  mediaSession.setPlaybackState(playbackState);
}

Handling transport control

Your app should implement media session transport control callback. The following table shows what transport control actions they need to handle:

Actions Description
onPlay() Resume
onPause() Pause
onSeekTo() Seek to a position
onStop() Stop the current media
public MyMediaSessionCallback extends MediaSessionCompat.Callback {

  public void onPause() {
    // Pause the player and update the play state.
    ...
  }

  public void onPlay() {
    // Resume the player and update the play state.
    ...
  }

  public void onSeekTo (long pos) {
    // Seek and update the play state.
    ...
  }
  ...
}

mediaSession.setCallback(new MyMediaSessionCallback());

Configuring Cast support

Android TV setup

Adding a launch intent filter

Add a new intent filter to the activity that you want to handle the launch intent from your sender app:

<activity android:name="com.example.activity">
  <intent-filter>
      <action android:name="com.google.android.gms.cast.tv.action.LAUNCH" />
      <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>

Specify receiver options provider

You need to implement a ReceiverOptionsProvider to provide CastReceiverOptions:

public class MyReceiverOptionsProvider implements ReceiverOptionsProvider {
  @Override
  public CastReceiverOptions getOptions(Context context) {
    return new CastReceiverOptions.Builder(context)
        .setStatusText("My App")
        .build();
  }
}

Then specify the options provider in your AndroidManifest:

 <meta-data
    android:name="com.google.android.gms.cast.tv.RECEIVER_OPTIONS_PROVIDER_CLASS_NAME"
    android:value="com.example.mysimpleatvapplication.MyReceiverOptionsProvider" />

The ReceiverOptionsProvider is used to provide the CastReceiverOptions when CastReceiverContext is initialized.

Cast receiver context

Initialize the CastReceiverContext when your app is created:

@Override
public void onCreate() {
  CastReceiverContext.initInstance(this);

  ...
}

Start the CastReceiverContext when your app moves to the foreground:

CastReceiverContext.getInstance().start();

Call stop() on the CastReceiverContext after the app goes into the background for video apps or apps that don't support background playback:

// Player has stopped.
CastReceiverContext.getInstance().stop();

Additionally, if your app does support playing in the background, call stop() on the CastReceiverContext when it stops playing while in the background.

We strongly recommend you to use the LifecycleObserver from the androidx.lifecycle library to manage calling CastReceiverContext.start() and CastReceiverContext.stop(), especially if your native app has multiple activities. This avoids a race conditions when you call start() and stop() from different activities.

// Create a LifecycleObserver class.
public class MyLifecycleObserver implements DefaultLifecycleObserver {
  @Override
  public void onStart(LifecycleOwner owner) {
    // App prepares to enter foreground.
    CastReceiverContext.getInstance().start();
  }

  @Override
  public void onStop(LifecycleOwner owner) {
    // App has moved to the background or has terminated.
    CastReceiverContext.getInstance().stop();
  }
}

// Add the observer when your application is being created.
public class MyApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();

    // Initialize CastReceiverContext.
    CastReceiverContext.initInstance(this /* android.content.Context */);

    // Register LifecycleObserver
    ProcessLifecycleOwner.get().getLifecycle().addObserver(
        new MyLifecycleObserver());
  }
}

// In AndroidManifest.xml set MyApplication as the application class
<application
    ...
    android:name=".MyApplication">

Connecting MediaSession to MediaManager

When you create a MediaSession, you also need to provide the current MediaSession token to CastReceiverContext so it knows where to send the commands and retrieve the media playback state:

MediaManager mediaManager = receiverContext.getMediaManager();
mediaManager.setSessionCompatToken(currentMediaSession.getSessionToken());

When you release your MediaSession due to inactive playback, you should set a null token on MediaManager:

myPlayer.stop();
mediaSession.release();
mediaManager.setSessionCompatToken(null);

If your app supports playing media while your app is in the background, instead of calling CastReceiverContext.stop() when your app goes background, you should call it only when your app is in the background and no longer plays media. For example:

public class MyLifecycleObserver implements DefaultLifecycleObserver {
  ...
  // App has moved to the background.
  @Override
  public void onPause(LifecycleOwner owner) {
    mIsBackground = true;

    myStopCastReceiverContextIfNeeded();
  }
}

// Stop playback on the player.
private void myStopPlayback() {
  myPlayer.stop();

  myStopCastReceiverContextIfNeeded();
}

// Stop the CastReceiverContext when both the player has stopped and the app has moved to the background.
private void myStopCastReceiverContextIfNeeded() {
  if (mIsBackground && myPlayer.isStopped()) {
    CastReceiverContext.getInstance().stop();
  }
}

Using Exoplayer with Cast Connect

If you are using Exoplayer, you can use the MediaSessionConnector to automatically maintain the session and all related information including the the playback state instead of tracking the changes manually.

MediaSessionConnector.MediaButtonEventHandler can be used to handle MediaButton events by calling setMediaButtonEventHandler(MediaButtonEventHandler) which are otherwise handled by MediaSessionCompat.Callback by default.

To integrate MediaSessionConnector in your app, add the following to your player activity class or wherever you manage your media session:

public class PlayerActivity extends Activity {

  private MediaSessionCompat mMediaSession;
  private MediaSessionConnector mMediaSessionConnector;
  private MediaManager mMediaManager;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ...
    mMediaSession = new MediaSessionCompat(this, LOG_TAG);
    mMediaSessionConnector = new MediaSessionConnector(mMediaSession);
    ...
  }

  @Override
  protected void onStart() {
    ...
    mMediaManager = receiverContext.getMediaManager();
    mMediaManager.setSessionCompatToken(currentMediaSession.getSessionToken());

    mMediaSessionConnector.setPlayer(mExoPlayer);
    mMediaSessionConnector.setMediaMetadataProvider(mMediaMetadataProvider);
    mMediaSession.setActive(true);
    ...
  }

  @Override
  protected void onStop() {
    ...
    mMediaSessionConnector.setPlayer(null);
    mMediaSession.release();
    mMediaManager.setSessionCompatToken(null);
    ...
  }
}

Sender app setup

Enable Cast Connect support

Once you have updated your sender app with Cast Connect support, you can declare the readiness by setting the androidReceiverCompatible flag to true.

Android

Requires play-services-cast-framework version 19.0.0 or higher.

The androidReceiverCompatible flag is set in LaunchOptions (which is part of CastOptions):

public class CastOptionsProvider implements OptionsProvider {
  @Override
  public CastOptions getCastOptions(Context context) {
    LaunchOptions launchOptions = new LaunchOptions.Builder()
              .setAndroidReceiverCompatible(true)
              .build();
    return new CastOptions.Builder()
        .setLaunchOptions(launchOptions)
        ...
        .build();
  }
}
    
iOS

Requires google-cast-sdk version v4.4.8 or higher.

The androidReceiverCompatible flag is set in GCKLaunchOptions (which is part of GCKCastOptions):

let options = GCKCastOptions(discoveryCriteria: GCKDiscoveryCriteria(applicationID: kReceiverAppID))
...
let launchOptions = GCKLaunchOptions()
launchOptions.androidReceiverCompatible = true
options.launchOptions = launchOptions
GCKCastContext.setSharedInstanceWith(options)
    
Chrome

Requires Chrome browser version M87 or higher (available by downloading Chrome Canary and enabling the chrome://flags/#cast-media-route-provider flag).

const context = cast.framework.CastContext.getInstance();
const castOptions = new cast.framework.CastOptions();
castOptions.receiverApplicationId = kReceiverAppID;
castOptions.androidReceiverCompatible = true;
context.setOptions(castOptions);
    

Cast Developer Console setup

Configure the Android TV app

Add the package name of your Android TV app in Cast Developer Console to associate it with your Cast App ID.

Register developer devices

Register the serial number of the Android TV device that you are going to use for development in the Cast Developer Console.

You can find the serial number by going to Settings > Device Preferences > Chromecast built-in > Serial number on your Android TV.

Without registration, Cast Connect will only work for apps installed from the Google Play Store due to security reasons.

For further information about registering a Cast or Android TV device for Cast development, see the registration page.

Loading media

If you have already implemented deep link support in your Android TV app, then you should have a similar definition configured in your Android TV Manifest:

<activity android:name="com.example.activity">
  <intent-filter>
     <action android:name="android.intent.action.VIEW" />
     <category android:name="android.intent.category.DEFAULT" />
     <data android:scheme="https"/>
     <data android:host="www.example.com"/>
     <data android:pathPattern=".*"/>
  </intent-filter>
</activity>

Load by entity on sender

On the senders, you can pass the deep link by setting the entity in the load request:

Android
MediaInfo mediaToLoad =
    new MediaInfo.Builder("some-id")
        .setEntity("https://example.com/watch/some-id")
        ...
        .build();
MediaLoadRequestData loadRequest =
    new MediaLoadRequestData.Builder()
        .setMediaInfo(mediaToLoad)
        .setCredentials("user-credentials")
        ...
        .build();
remoteMediaClient.load(loadRequest);
    
iOS
let mediaInfoBuilder = GCKMediaInformationBuilder(entity: "https://example.com/watch/some-id")
...
mediaInformation = mediaInfoBuilder.build()

let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
mediaLoadRequestDataBuilder.mediaInformation = mediaInformation
mediaLoadRequestDataBuilder.credentials = "user-credentials"
...
let mediaLoadRequestData = mediaLoadRequestDataBuilder.build()

remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
    
Chrome

Requires Chrome browser version M87 or higher (available by downloading Chrome Canary and enabling the chrome://flags/#cast-media-route-provider flag).

let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4');
mediaInfo.entity = 'https://example.com/watch/some-id';
...

let request = new chrome.cast.media.LoadRequest(mediaInfo);
request.credentials = 'user-credentials';
...

cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);
    

The load command is sent via an intent with your deep link and the package name you defined in the developer console.

Setting ATV credentials on sender

It is possible that your Web Receiver app and Android TV app support different deep links and credentials (for example if you are handling authentication differently on the two platforms). To address this, you can provide alternate entity and credentials for Android TV:

Android
MediaInfo mediaToLoad =
    new MediaInfo.Builder("some-id")
        .setEntity("https://example.com/watch/some-id")
        .setAtvEntity("myscheme://example.com/atv/some-id")
        ...
        .build();
MediaLoadRequestData loadRequest =
    new MediaLoadRequestData.Builder()
        .setMediaInfo(mediaToLoad)
        .setCredentials("user-credentials")
        .setAtvCredentials("atv-user-credentials")
        ...
        .build();
remoteMediaClient.load(loadRequest);
    
iOS
let mediaInfoBuilder = GCKMediaInformationBuilder(entity: "https://example.com/watch/some-id")
mediaInfoBuilder.atvEntity = "myscheme://example.com/atv/some-id"
...
mediaInformation = mediaInfoBuilder.build()

let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
mediaLoadRequestDataBuilder.mediaInformation = mediaInformation
mediaLoadRequestDataBuilder.credentials = "user-credentials"
mediaLoadRequestDataBuilder.atvCredentials = "atv-user-credentials"
...
let mediaLoadRequestData = mediaLoadRequestDataBuilder.build()

remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
    
Chrome

Requires Chrome browser version M87 or higher (available by downloading Chrome Canary and enabling the chrome://flags/#cast-media-route-provider flag).

let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4');
mediaInfo.entity = 'https://example.com/watch/some-id';
mediaInfo.atvEntity = 'myscheme://example.com/atv/some-id';
...

let request = new chrome.cast.media.LoadRequest(mediaInfo);
request.credentials = 'user-credentials';
request.atvCredentials = 'atv-user-credentials';
...

cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);
    

If the web receiver app is launched, the CAF SDK will still use the entity and credentials in the MediaLoadRequestData. However if your Android TV app is launched, we’ll override the entity and credentials with your atvEntity and atvCredentials (if specified).

Loading by Content ID or MediaQueueData

If you are not using entity or atvEntity, and are using Content ID or Content URL in your Media Information or use the more detailed Media Load Request Data, you need to add the following predefined intent filter in your Android TV app:

<activity android:name="com.example.activity">
  <intent-filter>
     <action android:name="com.google.android.gms.cast.tv.action.LOAD"/>
     <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>

On the sender side, similar to load by entity, you can create a MediaLoadRequestData with your content information and call load().

Android
MediaInfo mediaToLoad =
    new MediaInfo.Builder("some-id").build();
MediaLoadRequestData loadRequest =
    new MediaLoadRequestData.Builder()
        .setMediaInfo(mediaToLoad)
        .setCredentials("user-credentials")
        ...
        .build();
remoteMediaClient.load(loadRequest);
    
iOS
let mediaInfoBuilder = GCKMediaInformationBuilder(contentId: "some-id")
...
mediaInformation = mediaInfoBuilder.build()

let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
mediaLoadRequestDataBuilder.mediaInformation = mediaInformation
mediaLoadRequestDataBuilder.credentials = "user-credentials"
...
let mediaLoadRequestData = mediaLoadRequestDataBuilder.build()

remoteMediaClient?.loadMedia(with: mediaLoadRequestData)
    
Chrome

Requires Chrome browser version M87 or higher (available by downloading Chrome Canary and enabling the chrome://flags/#cast-media-route-provider flag).

let mediaInfo = new chrome.cast.media.MediaInfo('some-id"', 'video/mp4');
...

let request = new chrome.cast.media.LoadRequest(mediaInfo);
...

cast.framework.CastContext.getInstance().getCurrentSession().loadMedia(request);
    

Handling load requests

In your activity, to handle these load requests, you need to handle the intents in your activity lifecycle callbacks:

public class MyActivity extends Activity {
  @Override
  protected void onStart() {
    super.onStart();

    MediaManager mediaManager =
        CastReceiverContext.getInstance().getMediaManager();
    // Pass the intent to the SDK. You can also do this in onCreate().
    if (mediaManager.onNewIntent(getIntent())) {
      // If the SDK recognizes the intent, you should early return.
      return;
    }
    // If the SDK doesn't recognize the intent, you can handle the intent with
    // your own logic.
    ...
  }

  // For some cases, a new load intent triggers onNewIntent() instead of
  // onStart().
  @Override
  protected void onNewIntent(Intent intent) {
    MediaManager mediaManager =
        CastReceiverContext.getInstance().getMediaManager();
    // Pass the intent to the SDK. You can also do this in onCreate().
    if (mediaManager.onNewIntent(intent)) {
      // If the SDK recognizes the intent, you should early return.
      return;
    }
    // If the SDK doesn't recognize the intent, you can handle the intent with
    // your own logic.
    ...
  }
}

If MediaManager detects the intent is a load intent, it extracts a MediaLoadRequestData object from the intent, and invoke MediaLoadCommandCallback.onLoad(). You need to override this method to handle the load request. The callback must be registered before MediaManager.onNewIntent() is called (it’s recommended to be on an Activity or Application onCreate() method).

public class MyActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    MediaManager mediaManager =
        CastReceiverContext.getInstance().getMediaManager();
    mediaManager.setMediaLoadCommandCallback(new MyMediaLoadCommandCallback());
  }
}

public class MyMediaLoadCommandCallback extends MediaLoadCommandCallback {
  @Override
  public Task<MediaLoadRequestData> onLoad(String senderId, MediaLoadRequestData loadRequestData) {
    return Tasks.call(() -> {
        // Resolve the entity into your data structure and load media.
        MediaInfo mediaInfo = loadRequestData.getMediaInfo();
        if (!checkMediaInfoSupported(mediaInfo)) {
          // Throw MediaException to indicate load failure.
          throw new MediaException(
              new MediaError.Builder()
                  .setDetailedErrorCode(DetailedErrorCode.LOAD_FAILED)
                  .setReason(MediaError.ERROR_REASON_INVALID_REQUEST)
                  .build());
        }
        myFillMediaInfo(new MediaInfoWriter(mediaInfo));
        myPlayerLoad(mediaInfo.getContentUrl());

        // Update media metadata and state (this clears all previous status
        // overrides).
        castReceiverContext.getMediaManager()
            .setDataFromLoad(loadRequestData);
        ...
        castReceiverContext.getMediaManager().broadcastMediaStatus();

        // Return the resolved MediaLoadRequestData to indicate load success.
        return loadRequestData;
    });
}

private void myPlayerLoad(String contentURL) {
  myPlayer.load(contentURL);

  // Update the MediaSession state.
  PlaybackStateCompat playbackState =
      new PlaybackStateCompat.Builder()
          .setState(
              player.getState(), player.getPosition(), System.currentTimeMillis())
          ...
          .build();
  mediaSession.setPlaybackState(playbackState);
}

To process the load intent, you can parse the intent into the data structures we defined (MediaLoadRequestData for load requests).

Supporting media commands

Basic playback control support

Basic integration commands includes the commands that are compatible with media session. These commands are notified via media session callbacks. You need to register a callback to media session to support this (you might be doing this already).

private class MyMediaSessionCallback extends MediaSessionCompat.Callback {

  public void onPause() {
    // Pause the player and update the play state.
    myPlayer.pause();
  }

  public void onPlay() {
    // Resume the player and update the play state.
    myPlayer.play();
  }

  public void onSeekTo(long pos) {
    // Seek and update the play state.
    myPlayer.seekTo(pos);
  }

  ...
}

mediaSession.setCallback(new MyMediaSessionCallback());

Supporting Cast control commands

There are some Cast commands that are not available in MediaSession, such as skipAd() or setActiveMediaTracks(). Also, some queue commands needs to be implemented here because the Cast queue is not fully compatible with MediaSession queue.

public MyMediaCommandCallback extends MediaCommandCallback {
  @Override
  public Task<Void> onSkipAd(RequestData requestData) {
    // Skip your ad
    ...
    return Tasks.forResult(null);
  }
}

MediaManager mediaManager =
    CastReceiverContext.getInstance().getMediaManager();
mediaManager.setMediaCommandCallback(new MyMediaCommandCallback());

Specify supported media commands

As with your Cast receiver, your Android TV app should specify which commands are supported, so senders can enable or disable certain UI controls. For commands that are part of MediaSession, specify the commands in PlaybackStateCompat. Additional commands should be specified in the MediaStatusModifier.

// Set media session supported commands
PlaybackStateCompat playbackState =
    new PlaybackStateCompat.Builder()
        .setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE)
        .setState(PlaybackStateCompat.STATE_PLAYING)
        .build();

mediaSession.setPlaybackState(playbackState);

// Set additional commands in MediaStatusModifier
MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager();
mediaManager.getMediaStatusModifier()
            .setMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT);

Hide unsupported buttons

If your Android TV app only supports basic media control but your Cast receiver app supports more advanced control, you should make sure your sender app behave correctly when casting to the Android TV app. For example, if your Android TV app doesn’t support changing playback rate while your Web Receiver app does, you should set the supported actions correctly on each platform and make sure your sender app renders UI properly.

Modifying MediaStatus

To support advanced features like tracks, ads, live, and queueing, your Android TV app needs to provide additional information that can't be ascertained via MediaSession.

We provide the MediaStatusModifier class for you to achieve this. MediaStatusModifier will always operate on the MediaSession which you have set in CastReceiverContext.

To create and broadcast MediaStatus:

MediaManager mediaManager = castReceiverContext.getMediaManager();
MediaStatusModifier statusModifier = mediaManager.getMediaStatusModifier();

statusModifier
    .setLiveSeekableRange(seekableRange)
    .setAdBreakStatus(adBreakStatus)
    .setCustomData(customData);

mediaManager.broadcastMediaStatus();

Our client library will get the base MediaStatus from MediaSession, your Android TV app can specify additional status and override status via a MediaStatus modifier.

Some states and metadata can set both in MediaSession and MediaStatusModifier. We strongly recommend you only set them in MediaSession. You can still use the modifier to override the states in MediaSession—this is discouraged because the status in the modifier always have a higher priority than values provided by MediaSession.

Intercepting MediaStatus before sending out

Same as the web receiver SDK, if you want to do some finishing touches before sending out, you can specify a MediaStatusInterceptor to process the MediaStatus to be sent. We pass in a MediaStatusWriter to manipulate the MediaStatus before it is sent out.

mediaManager.setMediaStatusInterceptor(new MediaStatusInterceptor() {
    @Override
    public void intercept(MediaStatusWriter mediaStatusWriter) {
        // Perform customization.
        mediaStatusWriter.setCustomData(new JSONObject("{data: \"my Hello\"}"));
    }
});

Handling user credentials

Your Android TV app might only allow certain users to launch or join the app session. For example, only allow a sender to launch or join if:

  • The sender app is logged into same account and profile as ATV app.
  • The sender app is logged into same account, but different profile as ATV app.

If your app can handle multiple or anonymous users, you may allow additional any user to join the ATV session. If the user provides credentials, your ATV app needs to handle their credentials so their progress and other user data can be properly tracked.

When your sender app launches or joins your Android TV app, your sender app should provide the credentials that represents who is joining the session.

Before a sender launches and joins your Android TV app, you can specify a launch checker to see if the sender credentials are allowed. If not, the Cast Connect SDK will fall back to launching your web receiver.

Sender app launch credentials data

On the sender side, you can specify the CredentialsData to represent who is joining the session.

The credentials is a string which can be user-defined, as long as your ATV app can understand it. The credentialsType defines which platform the CredentialsData is coming from or can be a custom value. By default it is set to the platform that it is being sent from.

The CredentialsData is only passed to your Android TV app during launch or join time. If you set it again while you are connected, it won't be passed to your Android TV app. If your sender switches the profile while connected, you could either stay in the session, or call SessionManager.endCurrentCastSession() if you think the new profile is incompatible with the session.

The SDK keeps the CredentialsData for each sender in SenderInfo, which can be accessed via the CastReceiverContext.

Android

Requires play-services-cast-framework version 19.0.0 or higher.

CastContext.getSharedInstance().setLaunchCredentialsData(
    new CredentialsData.Builder()
        .setCredentials("{\"userId\": \"abc\"}")
        // Optional, will provide smart defaults
        .setCredentialsType(CredentialsData.CREDENTIALS_TYPE_ANDROID)
        .build());
    
iOS

Requires google-cast-sdk version v4.4.8 or higher.

Must be set after GCKCastContext.setSharedInstanceWith(options).

GCKCastContext.sharedInstance().launchCredentialsData(
    GCKCredentialsData(credentials: "{\"userId\": \"abc\"}",
   credentialsType: "ios")
Chrome

Requires Chrome browser version M87 or higher (available by downloading Chrome Canary and enabling the chrome://flags/#cast-media-route-provider flag).

Must be set after cast.framework.CastContext.getInstance().setOptions(options);.

let credentialsData =
    new chrome.cast.CredentialsData("{\"userId\": \"abc\"}",
        "web"); // Optional, will provide smart defaults
cast.framework.CastContext.getInstance().setLaunchCredentialsData(credentialsData);
    

Implementing ATV launch request checker

The CredentialsData is passed to your Android TV app when a sender tries to launch or join. You can implement a LaunchRequestChecker to allow or reject this request.

If a request is rejected, the web receiver is loaded instead of launching natively into the ATV app. You should reject a request if your ATV is unable to handle the user requesting to launch or join. Examples could be that a different user is logged into the ATV app than is requesting and your app is unable to handle switching credentials, or there is not a user currently logged into the ATV app.

If a request is allowed, the ATV app launches. You can customize this behavior depending on if your app supports sending load requests when a user is not logged into the ATV app or if there is a user mismatch. This behavior is fully cusomizable in the LoadRequestChecker.

Create a class implementing the LoadRequestChecker interface:

public class MyLaunchRequestChecker
    implements CastReceiverOptions.LaunchRequestChecker {
  @Override
  public Task<Boolean> checkLaunchRequestSupported(CastLaunchRequest launchRequest) {
    return Tasks.call(() -> myCheckLaunchRequest(launchRequest));
  }
}

private boolean myCheckLaunchRequest(CastLaunchRequest launchRequest) {
  CredentialsData credentialsData = launchRequest.getCredentialsData();
  if (credentialsData == null) {
    return false;  // or true if you allow anonymous users to join.
  }

  // The request comes from a mobile device, e.g. checking user match.
  if (credentialsData.getCredentialsType().equals(CredentialsData.CREDENTIALS_TYPE_ANDROID)) {
    return myCheckMobileCredentialsAllowed(credentialsData.getCredentials());
  }

  // Unrecognized credentials type.
  return false;
}

Then set it in your ReceiverOptionsProvider:

public class MyReceiverOptionsProvider implements ReceiverOptionsProvider {
  @Override
  public CastReceiverOptions getOptions(Context context) {
    return new CastReceiverOptions.Builder(context)
        ...
        .setLaunchRequestChecker(new MyLaunchRequestChecker())
        .build();
  }
}

Resolving true in the LaunchRequestChecker launches the ATV app and false launches your web receiver app.

Sending & Receiving Custom Messages

The Cast protocol allows you to send custom string messages between senders and your receiver application. You must register a namespace (channel) to send messages across before initializing your CastReceiverContext.

Android TV—Specify Custom Namespace

You need to specify your supported namespaces in your CastReceiverOptions during setup:

public class MyReceiverOptionsProvider implements ReceiverOptionsProvider {
  @Override
  public CastReceiverOptions getOptions(Context context) {
    return new CastReceiverOptions.Builder(context)
        .setCustomNamespaces(
              Arrays.asList("urn:x-cast:com.example.cast.mynamespace"))
        .build();
  }
}

Android TV—Sending Messages

// If senderId is null, then the message is broadcasted to all senders.
CastReceiverContext.getInstance().sendMessage(
    "urn:x-cast:com.example.cast.mynamespace", senderId, customString);

Android TV—Receive Custom Namespace Messages

class MyCustomMessageListener implements Cast.MessageReceivedListener{

  @Override
  public void onMessageReceived(
      String namespace, String senderId, String message) {
    ...
  }
}

CastReceiverContext.getInstance().MessageReceivedCallback(
    "urn:x-cast:com.example.cast.mynamespace", new MyCustomMessageCallback());

Troubleshooting

Web receiver opens on the Android TV

If the web receiver opens on the Android TV instead of your native app unexpectedly, there are a few steps you can take to troubleshoot the issue:

  1. If your app was not installed from the Play Store, check the serial number of your Android TV device to see if it is listed in the Cast Developer Console. See the registration page for instructions on how to register your device if it is not listed.
    • This serial number can change. It is a software serial number and is not the one written on the device.
  2. Restart your Android TV.
  3. Ensure that the installed sender app has Cast Connect support enabled as detailed in the sender app setup section.
  4. Ensure that the package name in the Cast Developer Console, as shown in the Cast Developer Console setup section, matches the app installed on your Android TV.
  5. Verify you are logged into your Android TV app using the same account that is logged in on the sender. Depending on the app, it will fall back to the web receiver in this situation.