googlecastnew500.png

This codelab will teach you how to modify an existing Android TV app so your existing Cast sender apps can cast content and communicate with the Android TV app.

What is Google Cast and Cast Connect?

Google Cast allows users to cast content from a mobile device to a TV. A typical Google Cast session consists of two components — a sender and a receiver application. Sender applications, like a mobile app or website such as Youtube.com, initiate and control the playback of a Cast receiver application. Cast receiver applications are HTML 5 apps that run on Chromecast and Android TV devices.

Almost all of the state in a Cast session is stored on the receiver application. When the state updates, for example if a new media item is loaded, a media status is broadcasted to all senders. These broadcasts contain the current state of the Cast session. Sender applications use this media status to display playback information in their UI.

Cast Connect builds on top of this infrastructure, with your Android TV app acting as a receiver. The Cast Connect library allows your Android TV app to receive messages and broadcast media status as if it were a cast receiver application.

What are we going to be building?

When you have completed this codelab, you will be able to use Cast sender apps to cast videos to an Android TV app. The Android TV app can also communicate with sender apps via the Cast protocol.

What you'll learn

What you'll need

You can download all the sample code to your computer...

Download Source Code

and unpack the downloaded zip file.

First, let's see what the completed sample app looks like. The Android TV app uses the Leanback UI and a basic video player. The user can select a video from a list which then plays on the TV when selected. With the accompanying mobile sender app, a user can also cast a video to the Android TV app.

Register developer devices

In order to enable Cast Connect capabilities for application development you must register the serial number of the Android TV device's built-in Chromecast that you are going to use 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. Note that this is different from your physical device's serial number and must be obtained from the method described above.

Without registration, Cast Connect will only work for apps installed from the Google Play Store due to security reasons. After 15 minutes of starting the registration process, restart your device.

Install the Android sender app

To test sending requests from a mobile device we have provided a simple sender application called Cast Videos. We will be leveraging ADB to install the APK. If you have already installed a different version of Cast Videos, please uninstall that version from all profiles located on the device prior to continuing.

  1. Enable developer options and USB debugging on your Android phone.
  2. Plug in a USB data cable to connect your Android phone with your development computer.
  3. Install mobile-sender.apk to your Android phone.

  1. You can find the Cast Videos sender app on your Android phone.

Install the Android TV app

The following instructions describe how to open and run the completed sample app in Android Studio:

  1. Select the Import Project on the welcome screen or the File > New > Import Project... menu options.
  2. Select the android_studio_folder.pngapp-done directory from the sample code folder and click OK.
  3. Click File > Sync Project with Gradle Files.
  4. Enable developer options and USB debugging on your Android TV device.
  5. ADB connect with your Android TV device, the device should show in Android Studio.
  6. Click the execute.pngRun button, you should see the ATV app named Cast Connect Codelab appear after a few seconds.

Let's play Cast Connect with ATV app

  1. Go to Android TV Home Screen.
  2. Open Cast Videos sender app from your Android phone. Click on the Cast button and select your ATV device.
  3. The Cast Connect Codelab ATV app will be launched on your ATV and the Cast button in your sender will indicate that it is connected .
  4. Select a video from the ATV app and the video will start playing on your ATV.
  5. On your mobile phone, a mini controller is now visible at the bottom of your sender app. You can use the play/pause button to control the playback.
  6. Select a video from the mobile phone and play. The video will start playing on your ATV and the expanded controller will be displayed on your mobile sender.
  7. Lock your phone and when you unlock it, you should see a notification on the lock screen to control the media playback or stop casting.

Now that we have verified the completed app's Cast Connect integration we need to add support for Cast Connect to the start app you downloaded. Now you're ready to build on top of the starter project using Android Studio:

  1. Select the Import Project on the welcome screen or the File > New > Import Project... menu options.
  2. Select the android_studio_folder.pngapp-start directory from the sample code folder and click OK.
  3. Click File > Sync Project with Gradle Files.
  4. Select ATV device and click the execute.pngRun button to run the app and explore the UI.

App design

The app provides a list of videos for the user to browse. Users can select a video to play on the Android TV. The app consists of two main activities: MainActivity and PlaybackActivity.

MainActivity

This activity contains a Fragment (MainFragment). The list of videos and their associated metadata are configured in MovieList class and setupMovies() method is called to build a list of Movie objects.

A Movie object represents a video entity with title, description, image thumbs and video url. Each Movie object is bound to a CardPresenter to present the video thumbnail with title and studio and passed to the ArrayObjectAdapter.

When an item is selected, the corresponding Movie object is passed to the PlaybackActivity.

PlaybackActivity

This activity contains a Fragment (PlaybackVideoFragment) which hosts a VideoView with ExoPlayer, some media controls, and a text area to show the description of the selected video and allows the user to play the video on the Android TV. The user can use remote control to play/pause or seek the playback of videos.

Prerequisites of Cast Connect

Cast Connect uses new versions of Google Play Services that require your ATV app to have been updated to use the AndroidX namespace.

In order to support Cast Connect in your Android TV app, you must create and support events from a media session. Cast Connect library generates media status based on the status of the media session. Your media session is also used by the Cast Connect library to signal when it has received certain messages from a sender, like pause.

Dependencies

Update the app build.gradle file to include the necessary library dependencies:

dependencies {
    ....

    // Cast Connect libraries and dependencies
    implementation 'com.google.android.gms:play-services-cast-tv:17.0.0'
    implementation 'com.google.android.gms:play-services-cast:19.0.0'
}

Sync the project to confirm the project builds without errors.

Initialization

CastReceiverContext is a singleton object to coordinate all the Cast interactions. You must implement the ReceiverOptionsProvider interface to provide the CastReceiverOptions when CastReceiverContext is initialized.

Create CastReceiverOptionsProvider.java file and add the following class to the project:

package com.google.sample.cast.castconnect;

import android.content.Context;
import com.google.android.gms.cast.tv.CastReceiverOptions;
import com.google.android.gms.cast.tv.ReceiverOptionsProvider;

public class CastReceiverOptionsProvider implements ReceiverOptionsProvider {
    @Override
    public CastReceiverOptions getOptions(Context context) {
        return new CastReceiverOptions.Builder(context)
                .setStatusText("Cast Connect Codelab")
                .build();
    }
}

Then specify the receiver options provider within the <application> tag of the app AndroidManifest.xml file:

<application>
  ...

  <meta-data
    android:name="com.google.android.gms.cast.tv.RECEIVER_OPTIONS_PROVIDER_CLASS_NAME"
    android:value="com.google.sample.cast.castconnect.CastReceiverOptionsProvider" />
</application>

To connect with your ATV app from your Cast sender, select an activity you want to launch. In this codelab, we will launch the MainActivity of the app when a Cast session is started. In the AndroidManifest.xml file, add the launch intent filter in the MainActivity.

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

Cast Receiver Context Lifecycle

You should start the CastReceiverContext when your app is launched and stop the CastReceiverContext when your app is moved to the background. We recommend that you use the LifecycleObserver from the androidx.lifecycle library to manage calling CastReceiverContext.start() and CastReceiverContext.stop()

Open MyApplication.java, initialize the cast context by calling initInstance() in the onCreate method of the application. In the AppLifeCycleObserver class start() the CastReceiverContext when the application is resumed and stop() it when the application is paused:

package com.google.sample.cast.castconnect;

...

import com.google.android.gms.cast.tv.CastReceiverContext;

public class MyApplication extends Application {

    private static final String LOG_TAG = "MyApplication";

    @Override
    public void onCreate() {
        super.onCreate();
        CastReceiverContext.initInstance(this);
        ProcessLifecycleOwner.get().getLifecycle().addObserver(new AppLifecycleObserver());
    }

    public static class AppLifecycleObserver implements DefaultLifecycleObserver {
        @Override
        public void onResume(@NonNull LifecycleOwner owner) {
            Log.d(LOG_TAG, "onResume");
            CastReceiverContext.getInstance().start();
        }

        @Override
        public void onPause(@NonNull LifecycleOwner owner) {
            Log.d(LOG_TAG, "onPause");
            CastReceiverContext.getInstance().stop();
        }
    }
}

Connecting MediaSession to MediaManager

MediaManager is a property of the CastReceiverContext singleton, it manages the media status, handles the load intent, translates the media namespace messages from senders into media commands, and sends media status back to the senders.

When you create a MediaSession, you also need to provide the current MediaSession token to MediaManager so it knows where to send the commands and retrieve the media playback state. Make sure the MediaSession is initialized before setting the token to MediaManager.

import com.google.android.gms.cast.tv.CastReceiverContext;
...

public class PlaybackVideoFragment extends VideoSupportFragment {

    private CastReceiverContext castReceiverContext;
    ...

    private void initializePlayer() {
        if (mPlayer == null) {
            ...
            mMediaSession = new MediaSessionCompat(getContext(), LOG_TAG);
            ...

            castReceiverContext = CastReceiverContext.getInstance();
            if (castReceiverContext != null) {
                MediaManager mediaManager = castReceiverContext.getMediaManager();
                mediaManager.setSessionCompatToken(mMediaSession.getSessionToken());
            }
        }
    }
}

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

private void releasePlayer() {
    if (mMediaSession != null) {
        mMediaSession.release();
    }
    if (castReceiverContext != null) {
        MediaManager mediaManager = castReceiverContext.getMediaManager();
        mediaManager.setSessionCompatToken(null);
    }
    ...
}

Let's run the sample app

Click the execute.pngRun button to deploy the app on your ATV device, close the app and return to ATV Home Screen. From your sender, click on the Cast button and select your ATV device. You will see the ATV app is launched on the ATV device and Cast button state is connected.

The load command is sent via an intent with the package name you defined in the developer console. You need to add the following predefined intent filter in your Android TV app to specify the target activity that will receive this intent. In AndroidManifest.xml file, add the load intent filter to PlayerActivity:

<activity android:name="com.google.sample.cast.castconnect.PlaybackActivity"
          android:launchMode="singleTask">
  <intent-filter>
     <action android:name="com.google.android.gms.cast.tv.action.LOAD"/>
     <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>

Handling Load Requests on Android TV

Now that the activity is configured to receive this intent containing a load request we will need to handle it.

The app calls a private method called processIntent when the activity starts. This method contains the logic for processing incoming intents. To handle a load request we will modify this method and send the intent to be further processed by calling the MediaManager instance's onNewIntent method. If MediaManager detects the intent is a load request, it extracts the MediaLoadRequestData object from the intent and invokes MediaLoadCommandCallback.onLoad(). Modify the processIntent method in PlaybackVideoFragment to handle the intent containing the load request:

public void processIntent(Intent intent) {
    MediaManager mediaManager = CastReceiverContext.getInstance().getMediaManager();
    // Pass intent to Cast SDK
    if (mediaManager.onNewIntent(intent)) {
        return;
    }

    // Clears all overrides in the modifier.
    mediaManager.getMediaStatusModifier().clear();

    // If the SDK doesn't recognize the intent, handle the intent with your own logic.
    ...
}

Next we will extend the abstract class MediaLoadCommandCallback which will override the onLoad() method called by MediaManager. This method receives the load request's data and converts it to a Movie object. Once converted, the movie is played by the local player. The MediaManager is then updated with the MediaLoadRequest and broadcasts the MediaStatus to the connected senders. Create a nested private class called MyMediaLoadCommandCallback in the PlaybackVideoFragment:

private class MyMediaLoadCommandCallback extends MediaLoadCommandCallback {
    @Override
    public Task<MediaLoadRequestData> onLoad(String senderId, MediaLoadRequestData mediaLoadRequestData) {
        Toast.makeText(getActivity(), "onLoad()", Toast.LENGTH_SHORT).show();

        if (mediaLoadRequestData == null) {
            // Throw MediaException to indicate load failure.
            return Tasks.forException(new MediaException(
                    new MediaError.Builder()
                            .setDetailedErrorCode(MediaError.DetailedErrorCode.LOAD_FAILED)
                            .setReason(MediaError.ERROR_REASON_INVALID_REQUEST)
                            .build()));
        }

        return Tasks.call(() -> {
            play(convertLoadRequestToMovie(mediaLoadRequestData));

            // Update media metadata and state
            MediaManager mediaManager = castReceiverContext.getMediaManager();
            mediaManager.setDataFromLoad(mediaLoadRequestData);
            mediaManager.broadcastMediaStatus();

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

private Movie convertLoadRequestToMovie(MediaLoadRequestData mediaLoadRequestData) {
    if (mediaLoadRequestData == null) {
        return null;
    }
    MediaInfo mediaInfo = mediaLoadRequestData.getMediaInfo();
    if (mediaInfo == null) {
        return null;
    }

    String videoUrl = mediaInfo.getContentId();
    if (mediaInfo.getContentUrl() != null) {
        videoUrl = mediaInfo.getContentUrl();
    }

    MediaMetadata metadata = mediaInfo.getMetadata();
    Movie movie = new Movie();
    movie.setVideoUrl(videoUrl);
    if (metadata != null) {
        movie.setTitle(metadata.getString(MediaMetadata.KEY_TITLE));
        movie.setDescription(metadata.getString(MediaMetadata.KEY_SUBTITLE));
        movie.setCardImageUrl(metadata.getImages().get(0).getUrl().toString());
    }
    return movie;
}

Now that the Callback has been defined, we need to register it to the MediaManager. The callback must be registered before MediaManager.onNewIntent() is called. Add setMediaLoadCommandCallback when the player is initialized:

private void initializePlayer() {
    if (mPlayer == null) {
        ...
        mMediaSession = new MediaSessionCompat(getContext(), LOG_TAG);
        ...

        castReceiverContext = CastReceiverContext.getInstance();
        if (castReceiverContext != null) {
            MediaManager mediaManager = castReceiverContext.getMediaManager();
            mediaManager.setSessionCompatToken(mMediaSession.getSessionToken());
            mediaManager.setMediaLoadCommandCallback(new MyMediaLoadCommandCallback());
        }
    }
}

Let's run the sample app

Click the execute.pngRun button to deploy the app on your ATV device. From your sender, click on the Cast button and select your ATV device. The ATV app will be launched on the ATV device. Select a video on mobile, the video will start playing on the ATV. Check whether you receive a notification on your phone where you have playback controls. Try using the controls such as pause, video on the ATV device should be paused.

The current application now supports basic commands that are compatible with a media session, such as play, pause, and seek. However, there are some Cast control commands that are not available in media session. You need to register a MediaCommandCallback to support those Cast control commands.

Add MyMediaCommandCallback to the MediaManager instance using setMediaCommandCallback when the player is initialized:

private void initializePlayer() {
        ...

        castReceiverContext = CastReceiverContext.getInstance();
        if (castReceiverContext != null) {
            MediaManager mediaManager = castReceiverContext.getMediaManager();
            ...
            mediaManager.setMediaCommandCallback(new MyMediaCommandCallback());
        }
}

Create MyMediaCommandCallback class to override the methods, such as onQueueUpdate() to support those Cast control commands:

private class MyMediaCommandCallback extends MediaCommandCallback {
        @Override
        public Task<Void> onQueueUpdate(String senderId, QueueUpdateRequestData queueUpdateRequestData) {
            Toast.makeText(getActivity(), "onQueueUpdate()", Toast.LENGTH_SHORT).show();

            // Queue Prev / Next
            if (queueUpdateRequestData.getJump() != null) {
                Toast.makeText(getActivity(),
                        "onQueueUpdate(): Jump = " + queueUpdateRequestData.getJump(),
                        Toast.LENGTH_SHORT).show();
            }

            return super.onQueueUpdate(senderId, queueUpdateRequestData);
        }
}

Modifying Media Status

Cast Connect gets the base media status from the media session. To support advanced features, your Android TV app can specify and override additional status properties via a MediaStatusModifier. MediaStatusModifier will always operate on the MediaSession which you have set in CastReceiverContext.

For example, to specify setMediaCommandSupported when onLoad callback is triggered:

private class MyMediaLoadCommandCallback extends MediaLoadCommandCallback {
    @Override
    public Task<MediaLoadRequestData> onLoad(String senderId, MediaLoadRequestData mediaLoadRequestData) {
        Toast.makeText(getActivity(), "onLoad()", Toast.LENGTH_SHORT).show();

        ... 

        return Tasks.call(() -> {
            play(convertLoadRequestToMovie(mediaLoadRequestData));

            // Update media metadata and state
            MediaManager mediaManager = castReceiverContext.getMediaManager();
            mediaManager.setDataFromLoad(mediaLoadRequestData);

            // Use MediaStatusModifier to provide additional information for Cast senders.
            mediaManager.getMediaStatusModifier()
                        .setMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT, true)
                        .setIsPlayingAd(false);

            mediaManager.broadcastMediaStatus();

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

Intercepting MediaStatus Before Sending Out

Similar to the Web receiver SDK's MessageInterceptor, you can specify a MediaStatusWriter in your MediaManager to perform additional modifications to your MediaStatus before it is broadcast to the connected senders.

For example, you can set custom data in the MediaStatus before sending out to mobile senders:

MediaManager mediaManager = castReceiverContext.getMediaManager();
...

// Use MediaStatusInterceptor to process the MediaStatus before sending out.
mediaManager.setMediaStatusInterceptor(mediaStatusWriter -> {
    try {
        mediaStatusWriter.setCustomData(new JSONObject("{myData: 'CustomData'}"));
    } catch (JSONException e) {
        e.printStackTrace();
    }
});

You now know how to Cast-enable an Android TV app using the Cast Connect Library.

Take a look at developer guide for more details: https://developers.google.com/cast/docs/android_tv_receiver