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.
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.
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.
You can download all the sample code to your computer...
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.
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.
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.
mobile-sender.apk
to your Android phone.The following instructions describe how to open and run the completed sample app in Android Studio:
app-done
directory from the sample code folder and click OK.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:
app-start
directory from the sample code folder and click OK.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
.
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
.
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.
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.
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.
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>
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();
}
}
}
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);
}
...
}
Click the Run 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>
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());
}
}
}
Click the Run 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);
}
}
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;
});
}
}
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