1. Overview
This codelab will teach you how to modify an existing Android TV app to support casting and communication from your existing Cast sender apps.
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
- How to add the Cast Connect library to a sample ATV app.
- How to connect a Cast sender and launch the ATV app.
- How to initiate media playback on the ATV app from a Cast sender app.
- How to send media status from the ATV app to Cast sender apps.
What you'll need
- The latest Android SDK.
- The latest Android Studio. Specifically,
Chipmunk | 2021.2.1
or later versions. - An Android TV device that has enabled developer options and USB debugging.
- An Android phone that has enabled developer options and USB debugging.
- A USB data cable to connect your Android phone and Android TV devices to your development computer.
- Basic knowledge of developing Android applications using Kotlin.
2. Get the sample code
You can download all the sample code to your computer...
and unpack the downloaded zip file.
3. Run the sample app
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 as mobile-sender-0629.apk
file in the source code zip download. 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.
- Enable developer options and USB debugging on your Android phone.
- Plug in a USB data cable to connect your Android phone with your development computer.
- Install
mobile-sender-0629.apk
to your Android phone.
- 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:
- Select the Import Project on the welcome screen or the File > New > Import Project... menu options.
- Select the
app-done
directory from the sample code folder and click OK. - Click File > Sync Project with Gradle Files.
- Enable developer options and USB debugging on your Android TV device.
- ADB connect with your Android TV device, the device should show in Android Studio.
- Click the Run button, you should see the ATV app named Cast Connect Codelab appear after a few seconds.
Let's play Cast Connect with ATV app
- Go to Android TV Home Screen.
- Open Cast Videos sender app from your Android phone. Click on the Cast button and select your ATV device.
- 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 .
- Select a video from the ATV app and the video will start playing on your ATV.
- 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.
- 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.
- 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.
4. Prepare the start project
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:
- Select the Import Project on the welcome screen or the File > New > Import Project... menu options.
- Select the
app-start
directory from the sample code folder and click OK. - Click File > Sync Project with Gradle Files.
- Select ATV device and click the Run 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.
5. Configuring Cast Support
Dependencies
Update the app build.gradle
file to include the necessary library dependencies:
dependencies {
....
// Cast Connect libraries
implementation 'com.google.android.gms:play-services-cast-tv:20.0.0'
implementation 'com.google.android.gms:play-services-cast:21.1.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.kt
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.ReceiverOptionsProvider
import com.google.android.gms.cast.tv.CastReceiverOptions
class CastReceiverOptionsProvider : ReceiverOptionsProvider {
override fun getOptions(context: Context): CastReceiverOptions {
return 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.kt
file, 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
...
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
CastReceiverContext.initInstance(this)
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
}
class AppLifecycleObserver : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
Log.d(LOG_TAG, "onResume")
CastReceiverContext.getInstance().start()
}
override fun onPause(owner: LifecycleOwner) {
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. In PlaybackVideoFragment.kt
file, make sure the MediaSession
is initialized before setting the token to MediaManager
.
import com.google.android.gms.cast.tv.CastReceiverContext
import com.google.android.gms.cast.tv.media.MediaManager
...
class PlaybackVideoFragment : VideoSupportFragment() {
private var castReceiverContext: CastReceiverContext? = null
...
private fun initializePlayer() {
if (mPlayer == null) {
...
mMediaSession = MediaSessionCompat(getContext(), LOG_TAG)
...
castReceiverContext = CastReceiverContext.getInstance()
if (castReceiverContext != null) {
val 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 fun releasePlayer() {
mMediaSession?.release()
castReceiverContext?.mediaManager?.setSessionCompatToken(null)
...
}
Let's run the sample app
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.
6. Loading Media
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"
android:exported="true">
<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.kt
file to handle the intent containing the load request:
fun processIntent(intent: Intent?) {
val 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.kt
file:
import com.google.android.gms.cast.MediaLoadRequestData
import com.google.android.gms.cast.MediaInfo
import com.google.android.gms.cast.MediaMetadata
import com.google.android.gms.cast.MediaError
import com.google.android.gms.cast.tv.media.MediaException
import com.google.android.gms.cast.tv.media.MediaCommandCallback
import com.google.android.gms.cast.tv.media.QueueUpdateRequestData
import com.google.android.gms.cast.tv.media.MediaLoadCommandCallback
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.Tasks
import android.widget.Toast
...
private inner class MyMediaLoadCommandCallback : MediaLoadCommandCallback() {
override fun onLoad(
senderId: String?, mediaLoadRequestData: MediaLoadRequestData): Task<MediaLoadRequestData> {
Toast.makeText(activity, "onLoad()", Toast.LENGTH_SHORT).show()
return if (mediaLoadRequestData == null) {
// Throw MediaException to indicate load failure.
Tasks.forException(MediaException(
MediaError.Builder()
.setDetailedErrorCode(MediaError.DetailedErrorCode.LOAD_FAILED)
.setReason(MediaError.ERROR_REASON_INVALID_REQUEST)
.build()))
} else Tasks.call {
play(convertLoadRequestToMovie(mediaLoadRequestData)!!)
// Update media metadata and state
val mediaManager = castReceiverContext!!.mediaManager
mediaManager.setDataFromLoad(mediaLoadRequestData)
mediaLoadRequestData
}
}
}
private fun convertLoadRequestToMovie(mediaLoadRequestData: MediaLoadRequestData?): Movie? {
if (mediaLoadRequestData == null) {
return null
}
val mediaInfo: MediaInfo = mediaLoadRequestData.getMediaInfo() ?: return null
var videoUrl: String = mediaInfo.getContentId()
if (mediaInfo.getContentUrl() != null) {
videoUrl = mediaInfo.getContentUrl()
}
val metadata: MediaMetadata = mediaInfo.getMetadata()
val movie = Movie()
movie.videoUrl = videoUrl
movie.title = metadata?.getString(MediaMetadata.KEY_TITLE)
movie.description = metadata?.getString(MediaMetadata.KEY_SUBTITLE)
if(metadata?.hasImages() == true) {
movie.cardImageUrl = metadata.images[0].url.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 fun initializePlayer() {
if (mPlayer == null) {
...
mMediaSession = MediaSessionCompat(getContext(), LOG_TAG)
...
castReceiverContext = CastReceiverContext.getInstance()
if (castReceiverContext != null) {
val mediaManager: MediaManager = castReceiverContext.getMediaManager()
mediaManager.setSessionCompatToken(mMediaSession.getSessionToken())
mediaManager.setMediaLoadCommandCallback(MyMediaLoadCommandCallback())
}
}
}
Let's run the sample app
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.
7. Supporting Cast Control Commands
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 fun initializePlayer() {
...
castReceiverContext = CastReceiverContext.getInstance()
if (castReceiverContext != null) {
val mediaManager = castReceiverContext!!.mediaManager
...
mediaManager.setMediaCommandCallback(MyMediaCommandCallback())
}
}
Create MyMediaCommandCallback
class to override the methods, such as onQueueUpdate()
to support those Cast control commands:
private inner class MyMediaCommandCallback : MediaCommandCallback() {
override fun onQueueUpdate(
senderId: String?,
queueUpdateRequestData: QueueUpdateRequestData
): Task<Void> {
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)
}
}
8. Working with Media Status
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:
import com.google.android.gms.cast.MediaStatus
...
private class MyMediaLoadCommandCallback : MediaLoadCommandCallback() {
fun onLoad(
senderId: String?,
mediaLoadRequestData: MediaLoadRequestData
): Task<MediaLoadRequestData> {
Toast.makeText(getActivity(), "onLoad()", Toast.LENGTH_SHORT).show()
...
return Tasks.call({
play(convertLoadRequestToMovie(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.
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:
import com.google.android.gms.cast.tv.media.MediaManager.MediaStatusInterceptor
import com.google.android.gms.cast.tv.media.MediaStatusWriter
import org.json.JSONObject
import org.json.JSONException
...
private fun initializePlayer() {
if (mPlayer == null) {
...
if (castReceiverContext != null) {
...
val mediaManager: MediaManager = castReceiverContext.getMediaManager()
...
// Use MediaStatusInterceptor to process the MediaStatus before sending out.
mediaManager.setMediaStatusInterceptor(
MediaStatusInterceptor { mediaStatusWriter: MediaStatusWriter ->
try {
mediaStatusWriter.setCustomData(JSONObject("{myData: 'CustomData'}"))
} catch (e: JSONException) {
Log.e(LOG_TAG,e.message,e);
}
})
}
}
}
9. Congratulations
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: /cast/docs/android_tv_receiver.