Migrate CCL Sender App to the Cast Application Framework (CAF)

The following procedure enables you to convert your Android sender app from Cast SDK v2 with CCL to CAF. All of the functionality of CCL has been implemented in CAF, so once you migrate, you will no longer need to use CCL.

The Cast CAF Sender SDK uses CastContext to manage the GoogleAPIClient on your behalf. CastContext manages lifecycles, errors and callbacks for you, which greatly simplifies developing a Cast app.

Introduction

  • Because the CAF Sender design was influenced by Cast Companion Library, the migration from CCL to CAF Sender involves mostly one-to-one mappings of classes and their methods.
  • CAF Sender is still distributed as part of Google Play services using the Android SDK manager.
  • New packages (com.google.android.gms.cast.framework.*) that have been added to CAF Sender, with functionality similar to CCL, take on responsibility for complying with the Google Cast Design checklist.
  • CAF Sender provides widgets that comply with the Cast UX requirements; these widgets are similar to those provided by CCL.
  • CAF Sender provides asynchronous callbacks that are similar to CCL, to track states and obtain data. Unlike CCL, CAF Sender does not provide any no-op implementations of the various interface methods.

In the following sections, we will mainly focus on the video­centric applications based on CCL's VideoCastManager, but in many cases, the same concepts apply to DataCastManager as well.

Dependencies

CCL and CAF have the same dependencies on the AppCompat support library, MediaRouter v7 support library and Google Play services. However, the difference is that CAF depends on the new Cast framework that is available in Google Play services 9.2.0 or later.

In your build.gradle file, remove the dependencies on com.google.android.gms:play-services-cast and com.google.android.libraries.cast.companionlibrary:ccl, then add the new Cast framework:

dependencies {
    compile 'com.android.support:appcompat-v7:23.4.0'
    compile 'com.android.support:mediarouter-v7:23.4.0'
    compile 'com.google.android.gms:play-services-cast-framework:9.4.0'
}

You can also remove the Google Play service metadata:

<metadata android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>

Any services, activities, and resources that are part of CAF are automatically merged with your app's manifest and resources.

The minimum Android SDK version that CAF supports is 9 (Gingerbread); CCL's minimum Android SDK version is 10.

CCL provides a convenience method, BaseCastManager.checkGooglePlayServices(activity), to verify that a compatible version of the Google Play services is available on the device. CAF does not provide this as part of the Cast SDK. Follow the procedure Ensure Devices Have the Google Play services APK to ensure that the correct Google Play services APK is installed on a user's device since updates might not reach all users immediately.

You are still required to use a variant of Theme.AppCompat for the application's theme.

Initialization

For CCL, VideoCastManager.initialize() was required to be called in the onCreate() method of the Applications instance. This logic should be removed from your Application class code.

In CAF, an explicit initialization step is also required for the Cast framework. This involves initializing the CastContext singleton, using an appropriate OptionsProvider to specify the receiver application ID and any other global options. The CastContext plays a similar role to CCL's VideoCastManager by providing a singleton that clients interact with. The OptionsProvider is similar to CCL's CastConfiguration to allow you to configure the Cast framework features.

If your current CCL CastConfiguration.Builder looks like this:

VideoCastManager.initialize(
   getApplicationContext(),
   new CastConfiguration.Builder(context.getString(R.string.app_id))
       .enableWifiReconnection()
       .enableAutoReconnect()
       .build());

Then in CAF the following CastOptionsProvider using the CastOptions.Builder would be similar:

public class CastOptionsProvider implements OptionsProvider {

    @Override
    public CastOptions getCastOptions(Context context) {
        return new CastOptions.Builder()
                .setReceiverApplicationId(context.getString(R.string.app_id))
                .build();
    }

    @Override
    public List<SessionProvider> getAdditionalSessionProviders(
            Context context) {
        return null;
    }
}

Take a look at our sample app for a complete implementation of the OptionsProvider.

Declare the OptionsProvider within the "application" element of the AndroidManifest.xml file:

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

Lazily initialize the CastContext in each Activity's onCreate method (and not the Application instance):

private CastContext mCastContext;

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.video_browser);
    setupActionBar();

    mCastContext = CastContext.getSharedInstance(this);
}

To access the CastContext singleton use:

mCastContext = CastContext.getSharedInstance(this);

Device discovery

CCL's VideoCastManager incrementUiCounter and decrementUiCounter should be removed from the onResume and onPause methods of your Activities.

In CAF, the discovery process is started and stopped automatically by the framework when the app comes to the foreground and goes to the background, respectively.

Cast button and Cast dialog

As with CCL, these components are provided by the MediaRouter v7 support library.

The Cast button is still implemented by the MediaRouteButton and can be added to your activity (using either an ActionBar or a Toolbar), as a menu item in your menu.

The declaration of the MediaRouteActionProvider in the menu xml is the same as with CCL:

<item
    android:id="@+id/media_route_menu_item"
    android:title="@string/media_route_menu_title"
    app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
    app:showAsAction="always"/>

Similar to CCL, override the onCreateOptionMenu() method of each Activity, but instead of using CastManager.addMediaRouterButton, use CAF's CastButtonFactory to wire up the MediaRouteButton to the Cast framework:

public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.browse, menu);
    CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                                                menu,
                                                R.id.media_route_menu_item);
    return true;
}

Device control

Similar to CCL, in CAF, device control is largely handled by the framework. The sender application does not need to handle (and should not try to handle) connecting to the device and launching the receiver application using GoogleApiClient.

Interaction between sender and receiver is now represented as a "session". The SessionManager class handles the session lifecycle and automatically starts and stops sessions in response to user gestures: a session is started when the user selects a Cast device in the Cast dialog and is ended when the user taps the "Stop Casting" button in the Cast dialog or when the sender app itself terminates.

In CCL you have to extend the VideoCastConsumerImpl class to track the cast session status:

private final VideoCastConsumer mCastConsumer = new VideoCastConsumerImpl() {
  public void onApplicationConnected(ApplicationMetadata appMetadata, 
                                     String sessionId,
                                     boolean wasLaunched) {}
  public void onDisconnectionReason(int reason) {}
  public void onDisconnected() {}
}

In CAF, the sender application can be notified of session lifecycle events by registering a SessionManagerListener with the SessionManager. The SessionManagerListener callbacks define callback methods for all session lifecycle events.

The following SessionManagerListener methods are mapped from CCL's VideoCastConsumer interface:

  • VideoCastConsumer.onApplicationConnected -> SessionManagerListener.onSessionStarted
  • VideoCastConsumer.onDisconnected -> SessionManagerListener.onSessionEnded

Declare a class that implements the SessionManagerListener interface and move the VideoCastConsumerImpl logic to the matching methods:

private class CastSessionManagerListener implements SessionManagerListener<CastSession> {
  public void onSessionEnded(CastSession session, int error) {}
  public void onSessionStarted(CastSession session, String sessionId) {}
  public void onSessionEnding(CastSession session) {}
  ...
}

The CastSession class represents a session with a Cast device. The class has methods for controlling the device volume and mute states, which CCL does in the BaseCastManager.

Instead of using the CCL VideoCastManager to add a consumer:

VideoCastManager.getInstance().addVideoCastConsumer(mCastConsumer);

Now register your SessionManagerListener:

mCastSessionManager = 
    CastContext.getSharedInstance(this).getSessionManager();
mCastSessionManagerListener = new CastSessionManagerListener();
mCastSessionManager.addSessionManagerListener(mCastSessionManagerListener,
                  CastSession.class);

To stop listening to events in CCL:

VideoCastManager.getInstance().removeVideoCastConsumer(mCastConsumer);

Now use the SessionManager to stop listening to session events:

mCastSessionManager.removeSessionManagerListener(mCastSessionManagerListener,
                    CastSession.class);

To explicitly disconnect from the Cast device, CCL used:

VideoCastManager.disconnectDevice(boolean stopAppOnExit, 
            boolean clearPersistedConnectionData,
            boolean setDefaultRoute)

For CAF, use the SessionManager:

CastContext.getSharedInstance(this).getSessionManager()
                                   .endCurrentSession(true);

To determine if the sender is connected to the receiver, CCL provides VideoCastManager.getInstance().isConnected(), but in CAF use the SessionManager:

public boolean isConnected() {
    CastSession castSession = CastContext.getSharedInstance(mAppContext)
                                  .getSessionManager()
                                  .getCurrentCastSession();
    return (castSession != null && castSession.isConnected());
}

In CAF, volume/mute state change notifications are still delivered via callback methods in the Cast.Listener; these listeners are registered with CastSession. All of the remaining device state notifications are delivered via CastStateListener callbacks; these listeners are registered with the CastSession. Make sure you still unregister listeners when the associated fragments, activities or apps go to the background.

Reconnection logic

CAF attempts to re-establish network connections that are lost due to temporary WiFi signal loss or other network errors. This is now done at the session level; a session can enter a "suspended" state when the connection is lost, and will transition back to a "connected" state when connectivity is restored. The framework takes care of reconnecting to the receiver application and reconnecting any Cast channels as part of this process.

CAF provides its own reconnection service, so you can remove the CCL ReconnectionService from your manifest:

<service android:name="com.google.android.libraries.cast.companionlibrary.cast.reconnection.ReconnectionService"/>

You also don't need the following permissions in your manifest for the reconnection logic:

<uses‐permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses‐permission android:name="android.permission.ACCESS_WIFI_STATE"/>

The CAF reconnection service is enabled by default but can be disabled using the CastOptions.

In addition, CAF also adds automatic session resumption which is enabled by default (and can be deactivated via CastOptions). If the sender application is sent to the background or is terminated (by swiping away or because of a crash) while a Cast session is in progress, the framework will attempt to resume that session when the sender application returns to the foreground or is relaunched; this is handled automatically by the SessionManager, which will issue the appropriate callbacks on any registered SessionManagerListener instances.

Custom channel registration

CCL provides two ways to create a custom message channel to the receiver:

  • CastConfiguration allows you to specify multiple namespaces and CCL will then create the channel for you.
  • DataCastManager is similar to VideoCastManager but focused on non-media use cases.

Neither of these ways of creating a custom channel is supported by CAF -- you have to instead follow the procedure Add a Custom Channel for your sender app.

Similar to CCL, for media applications, it is not necessary to explicitly register the media control channel.

Media control

In CAF, the RemoteMediaClient class is equivalent to the VideoCastManager media methods. The RemoteMediaClient.Listener is equivalent to VideoCastConsumer methods. In particular, the onRemoteMediaPlayerMetadataUpdated and onRemoteMediaPlayerStatusUpdated methods of VideoCastConsumer maps to the onMetadataUpdated and onStatusUpdated methods of RemoteMediaClient.Listener respectively:

private class CastMediaClientListener implements RemoteMediaClient.Listener {

    @Override
    public void onMetadataUpdated() {
        setMetadataFromRemote();
    }

    @Override
    public void onStatusUpdated() {
        updatePlaybackState();
    }

    @Override
    public void onSendingRemoteMediaRequest() {
    }

    @Override
    public void onQueueStatusUpdated() {
    }

    @Override
    public void onPreloadStatusUpdated() {
    }
}

It is not necessary to explicitly initialize or register the RemoteMediaClient object; the framework will automatically instantiate the object and register the underlying media channel at session start time if the receiver application being connected to supports the media namespace.

The RemoteMediaClient can be accessed as the getRemoteMediaClient method of the CastSession object.

CastSession castSession = CastContext.getSharedInstance(mAppContext)
                                     .getSessionManager()
                                     .getCurrentCastSession();
mRemoteMediaClient = castSession.getRemoteMediaClient();
mRemoteMediaClientListener = new CastMediaClientListener();

Instead of CCL's:

VideoCastManager.getInstance().addVideoCastConsumer(mCastConsumer);

Now use CAF:

mRemoteMediaClient.addListener(mRemoteMediaClientListener);

Any number of listeners can be registered with the RemoteMediaClient, which allows multiple sender components to share the single instance of RemoteMediaClient that is associated with the session.

CCL's VideoCastManager provides methods to handle media playback:

VideoCastManager manager = VideoCastManager.getInstance();
if (manager.isRemoteMediaLoaded()) {
    manager.pause();
    mCurrentPosition = (int) manager.getCurrentMediaPosition();
}

These are now implemented by RemoteMediaClient in CAF:

if (mRemoteMediaClient.hasMediaSession()) {
    mRemoteMediaClient.pause();
    mCurrentPosition = 
        (int)mRemoteMediaClient.getApproximateStreamPosition();
}

In CAF, all media requests issued on the RemoteMediaClient return a RemoteMediaClient.MediaChannelResult via a PendingResult callback which can be used to track the progress and eventual outcome of the request.

Both CCL and CAF use the MediaInfo and MediaMetadata classes to represent media items and to load media.

To load media in CCL, the VideoCastManager is used:

VideoCastManager.getInstance().loadMedia(media, autoPlay, mCurrentPosition, customData);

In CAF, the RemoteMediaClient is used to load the media:

mRemoteMediaClient.load(media, autoPlay, mCurrentPosition, customData);

To get the Media information and status of a current media session on the receiver, CCL uses the VideoCastManager:

MediaInfo mediaInfo = VideoCastManager.getInstance()
                                      .getRemoteMediaInformation();
int status = VideoCastManager.getInstance().getPlaybackStatus();
int idleReason = VideoCastManager.getInstance().getIdleReason();

In CAF, use the RemoteMediaClient to get the same information:

MediaInfo mediaInfo = mRemoteMediaClient.getMediaInfo();
int status = mRemoteMediaClient.getPlayerState();
int idleReason = mRemoteMediaClient.getIdleReason();

Introductory overlay

Similar to CCL, CAF provides a custom view IntroductoryOverlay to highlight the Cast button when it is first shown to users.

Instead of using CCL's VideoCastConsumer onCastAvailabilityChanged method to know when to display the overlay, declare a CastStateListener to determine when the Cast button becomes visible once Cast devices are discovered on the local network by the MediaRouter:

private IntroductoryOverlay mIntroductoryOverlay;
private MenuItem mMediaRouteMenuItem;

protected void onCreate(Bundle savedInstanceState) {
    ...
    mCastStateListener = new CastStateListener() {
        @Override
        public void onCastStateChanged(int newState) {
            if (newState != CastState.NO_DEVICES_AVAILABLE) {
                showIntroductoryOverlay();
            }
        }
    };
    mCastContext = CastContext.getSharedInstance(this);
    mCastContext.registerLifecycleCallbacksBeforeIceCreamSandwich(this, 
        savedInstanceState);
}

protected void onResume() {
    mCastContext.addCastStateListener(mCastStateListener);
    ...
}

protected void onPause() {
    mCastContext.removeCastStateListener(mCastStateListener);
    ...
}

Keep track of the MediaRouteMenuItem instance:

public boolean onCreateOptionsMenu(Menu menu) {
   super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.browse, menu);
    mMediaRouteMenuItem = CastButtonFactory.setUpMediaRouteButton(
            getApplicationContext(), menu,
            R.id.media_route_menu_item);
    showIntroductoryOverlay();
    return true;
}

Check if the MediaRouteButton is visible so the introductory overlay can be shown:

private void showIntroductoryOverlay() {
    if (mIntroductoryOverlay != null) {
        mIntroductoryOverlay.remove();
    }
    if ((mMediaRouteMenuItem != null) && mMediaRouteMenuItem.isVisible()) {
        new Handler().post(new Runnable() {
            @Override
            public void run() {
                mIntroductoryOverlay = new IntroductoryOverlay.Builder(
                        VideoBrowserActivity.this, mMediaRouteMenuItem)
                        .setTitleText(getString(R.string.introducing_cast))
                        .setOverlayColor(R.color.primary)
                        .setSingleTime()
                        .setOnOverlayDismissedListener(
                                new IntroductoryOverlay
                                    .OnOverlayDismissedListener() {
                                        @Override
                                        public void onOverlayDismissed() {
                                            mIntroductoryOverlay = null;
                                        }
                                })
                        .build();
                mIntroductoryOverlay.show();
            }
        });
    }
}

Take a look at our sample app for the complete working code for showing the introductory overlay.

To customize the styling of the introductory overlay, follow the procedure Customize Introductory Overlay.

Mini controller

Instead of CCL's MiniController, use CAF's MiniControllerFragment in your app layout file of the activities in which you want to show the mini controller:

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

CAF does not support the manual configuration supported by CCL's MiniController and also does not support the Autoplay feature.

To customize the styling and buttons of the mini controller, follow the procedure Customize Mini Controller.

Notification and lock screen

Similar to CCL's VideoCastNotificationService, CAF provides a MediaNotificationService to manage the display of media notifications when casting.

You need to remove the following from your manifest:

  • VideoIntentReceiver
  • VideoCastNotificationService

CCL supports providing a custom notification service with the CastConfiguration.Builder; that is not supported by CAF.

Consider the following CastManager initialization using CCL:

VideoCastManager.initialize(
   getApplicationContext(),
   new CastConfiguration.Builder(
           context.getString(R.string.app_id))
       .addNotificationAction(
           CastConfiguration.NOTIFICATION_ACTION_PLAY_PAUSE,true)
       .addNotificationAction(
           CastConfiguration.NOTIFICATION_ACTION_DISCONNECT,true)
       .build());

For the equivalent configuration in CAF, the SDK provides a NotificationsOptions.Builder to help you build media controls for the notification and lock screen into the sender app. The notification and lock screen controls can be enabled with the CastOptions when initializing the CastContext.

public CastOptions getCastOptions(Context context) {
    NotificationOptions notificationOptions = 
        new NotificationOptions.Builder()
            .setActions(Arrays.asList(
                MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK,
                MediaIntentReceiver.ACTION_STOP_CASTING), new int[]{0, 1})
            .build();
    CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
             .setNotificationOptions(notificationOptions)
             .build();
    return new CastOptions.Builder()
             .setReceiverApplicationId(context.getString(R.string.app_id))
             .setCastMediaOptions(mediaOptions)
             .build();
}

Notifications and lock screen controls are always enabled in CAF. Also, note that the play/pause and stop casting buttons are provided by default. CAF will automatically track the visibility of Activities for deciding when to display the media notification, except for Gingerbread. (For Gingerbread, see the earlier note on using registerLifecycleCallbacksBeforeIceCreamSandwich(); CCL's VideoCastManager incrementUiCounter and decrementUiCounter calls should be removed.)

To customize the buttons that are displayed in notifications, follow the procedure Add Media Controls to Notification and Lock Screen.

Expanded controller

CCL provides the VideoCastControllerActivity and VideoCastControllerFragment to display an expanded controller when casting media.

You can remove the VideoCastControllerActivity declaration in the manifest.

In CAF, you have to extend the ExpandedControllerActivity and add the Cast button.

To customize the styles and buttons that are displayed in the expanded controller, follow the procedure Customize Expanded Controller.

Audio focus

As with CCL, audio focus is managed automatically.

Volume control

For Gingerbread, dispatchKeyEvent is required as with CCL. In ICS and above for both CCL and CAF volume control is handled automatically.

CAF enables controlling the cast volume through the hard volume button on the phone inside your apps activities and also shows a visual volume bar when casting on supported versions. CAF also handles change of volume through the hard volume even if your app is not in front, is locked or even if the screen is off.

Closed captions

In Android KitKat and above, captions can be customized through Captions Settings, found under Settings ­> Accessibility. Earlier versions of Android, however, do not have this capability. CCL handles this by providing custom settings for earlier versions and delegating to the system settings on KitKat and above.

CAF does not provide custom settings to change the caption preferences. You should remove the CaptionsPreferenceActivity references in your manifest and your preferences XML.

CCL's TracksChooserDialog is no longer needed since changing the closed captions tracks is handled by the expanded controller UI.

The closed captioning API in CAF is similar to v2.

Debug logging

CAF does not provide debug logging settings.

Misc

The following CCL features are not supported in CAF:

  • Obtaining authorization prior to playback by providing a MediaAuthService
  • Configurable UI messages

Sample apps

Take a look at the diff for migrating our Universal Music Player for Android (uamp) sample app from CCL to CAF.

We also have codelab tutorials and sample apps that use CAF.