This codelab will teach you how to modify an existing iOS video app to play content on a Google Cast device.

What is Google Cast?

Google Cast allows users to cast content from a mobile device to a TV. Users can then use their mobile device as a remote control for media playback on the TV.

The Google Cast SDK lets you extend your app to control a TV or sound system. The Cast SDK allows you to add the necessary UI components based on the Google Cast Design Checklist.

The Google Cast Design Checklist is provided to make the Cast user experience simple and predictable across all supported platforms.

What are we going to be building?

When you have completed this codelab, you will have an iOS video app that will be able to Cast videos to a Google Cast device.

What you'll learn

What you'll need

Experience

How will you use this tutorial?

Read it through only Read it and complete the exercises

How would you rate your experience with building iOS apps?

Novice Intermediate Proficient

How would you rate your experience with watching TV?

Novice Intermediate Proficient

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

Download Source Code

and unpack the downloaded zip file.

...or clone the GitHub repository from the command line.

$ git clone https://github.com/googlecodelabs/cast-videos-ios.git

First, let's see what the completed sample app looks like. The app is a basic video player. The user can select a video from a list and can then play the video locally on the device or Cast it to a Google Cast device.

With the code downloaded, the following instructions describe how to open and run the completed sample app in Xcode:

Select the File > Open... menu options.

Select the CastVideos-ios.xcodeproj file from the app-done directory in the sample code folder.

Xcode Setup

To setup Xcode, follow the instructions in the official documentation and download the dependencies into the app-done Frameworks directory.

After completing the setup for the library, add the GoogleCast.framework resource bundle to your app. In the Build Phases for your target, add a new entry in the Copy Bundle Resources section, and select Add Other.

On the above pop-up window, press "Add Other..." and then find the GoogleCast.framework folder, and inside it you will find the file GoogleCastResources.bundle. Select it and click "Open".

You can check the option "Copy items if needed" and leave the "Create folders references" option selected. Press "Finish" and the resource bundle will be added to the app.

Run the App

Select the device to run the app, and then run the app:

You should see the video app appear after a few seconds.

Click the Cast button and select your Google Cast device.

Select a video, click on the play button.

The video will start playing on your Google Cast device.

The expanded controller will be displayed. You can use the play/pause button to control the playback.

Navigate back to the list of videos.

A mini controller is now visible at the bottom of the screen.

Click on the pause button in the mini controller to pause the video on the receiver. Click on the play button in the mini controller to continue playing the video again.

Click on the Cast button to stop casting to the Google Cast device.

Frequently Asked Questions

We need to add support for Google Cast to the start app you downloaded. Here are some Google Cast terminology that we will be using in this codelab:

Now you're ready to build on top of the starter project using Xcode:

  1. Select the app-start directory from your sample code download (File > Open... > CastVideos-ios.xcodeproj).
  2. Run the app and explore the UI.

App Design

The app fetches a list of videos from a remote web server and provides a list for the user to browse. Users can select a video to see the details or play the video locally on the mobile device.

The app consists of two main view controllers: MediaTableViewController and MediaViewController.

MediaTableViewController

This UITableViewController displays a list of videos from a MediaListModel instance. The list of videos and their associated metadata are hosted on a remote server as a JSON file. MediaListModel fetches this JSON and processes it to build a list of MediaItem objects.

A MediaItem object models a video and its associated metadata, such as its title, description, URL for an image, and URL for the stream.

MediaTableViewController creates a MediaListModel instance and then registers itself as a MediaListModelDelegate to be informed when the media metadata has been downloaded so it can load the table view.

The user is presented with a list of video thumbnails with a short description for each video. When an items is selected, the corresponding MediaItem is passed to the MediaViewController.

MediaViewController

This view controller displays the metadata about a particular video and allows the user to play the video locally on the mobile device.

The view controller hosts a LocalPlayerView, some media controls, and a text area to show the description of the selected video. The player covers the top portion of screen, leaving room for the detailed description of the video beneath The user can play/pause or seek the local video playback.

Frequently Asked Questions

A Cast-enabled application displays the Cast button in each of its view controllers. Clicking on the Cast button displays a list of Cast devices which a user can select. If the user was playing content locally on the sender device, selecting a Cast device starts or resumes playback on that Cast device. At any time during a Cast session, the user can click on the Cast button and stop casting your application to the Cast device. The user must be able to connect to or disconnect from the Cast device while in any screen of your application, as described in the Google Cast Design Checklist.

Configuration

The start project requires the same dependencies and Xcode setup as you did for the completed sample app. Return to that section and follow the same steps to add the GoogleCast.framework to the start app project.

Initialization

The Cast framework has a global singleton object, the GCKCastContext, which coordinates all of the framework's activities. This object must be initialized early in the application's lifecycle, typically in the -[application:didFinishLaunchingWithOptions:] method of the app delegate, so that automatic session resumption on the sender application restart can trigger properly and scanning for devices can start.

A GCKCastOptions object must be supplied when initializing the GCKCastContext. This class contains options that affect the behavior of the framework. The most important of these is the receiver application ID, which is used to filter Cast device discovery results and to launch the receiver application when a Cast session is started.

The -[application:didFinishLaunchingWithOptions:] method is also a good place to set up a logging delegate to receive the logging messages from Cast framework. These can be useful for debugging and troubleshooting.

When you develop your own Cast-enabled app, you have to register as a Cast developer and then obtain an application ID for your app. For this codelab, we will be using a sample app ID.

Add the following code to AppDelegate.m to initialize GCKCastContext with the application ID from the user defaults, and add a logger for the Google Cast framework:

#import <GoogleCast/Googlecast.h>

@interface AppDelegate ()<GCKLoggerDelegate> {
...
}

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
...
  GCKCastOptions *options =
      [[GCKCastOptions alloc] initWithReceiverApplicationID:applicationID];
  [GCKCastContext setSharedInstanceWithOptions:options];

[[GCKLogger sharedInstance] setDelegate:self];
...
}

- (void)logMessage:(NSString *)message fromFunction:(NSString *)function {
  if (_enableSDKLogging) {
    // Send SDK's log messages directly to the console.
    NSLog(@"%@  %@", function, message);
  }
}

Cast Button

Now that the GCKCastContext is initialized, we need to add the Cast button to allow the user to select a Cast device. The Cast SDK provides a Cast button component called GCKUICastButton as a UIButton subclass. It can be added to the application's title bar by wrapping it in a UIBarButtonItem. We need to add the Cast button to both the MediaTableViewController and the MediaViewController.

Add the following code to MediaTableViewController.m and MediaViewController.m:

#import <GoogleCast/GoogleCast.h>

- (void)viewDidLoad {
  [super viewDidLoad];
...
  GCKUICastButton *castButton =
      [[GCKUICastButton alloc] initWithFrame:CGRectMake(0, 0, 24, 24)];
  castButton.tintColor = [UIColor whiteColor];
  self.navigationItem.rightBarButtonItem =
      [[UIBarButtonItem alloc] initWithCustomView:castButton];
...
}

Now run the app on your mobile device. You should see a Cast button in the app's navigation bar and when you click on it, it will list the Cast devices on your local network. Device discovery is managed automatically by the GCKCastContext. Select your Cast device and the sample receiver app will load on the Cast device. You can navigate between the browse activity and the local player activity and the Cast button state is kept in sync.

We haven't hooked up any support for media playback, so you can't play videos on the Cast device yet. Click on the Cast button to stop casting.

We will extend the sample app to also play videos remotely on a Cast device. To do that we need to listen to the various events generated by the Cast framework.

Casting Media

At a high level, if you want to play a media on a Cast device, the following needs to happen:

  1. Create a GCKMediaInformation object from the Cast SDK that models a media item.
  2. The user connects to the Cast device to launch your receiver application.
  3. Load the GCKMediaInformation object into your receiver and play the content.
  4. Track the media status.
  5. Send playback commands to the receiver based on user interactions.

Step 1 amounts to mapping one object to another; GCKMediaInformation is something that the Cast SDK understands and MediaItem is our app's encapsulation for a media item; we can easily map a MediaItem to a GCKMediaInformation. We have already done the Step 2 in the previous section. Step 3 is easy to do with the Cast SDK.

The sample app MediaViewController already distinguishes between local vs remote playback by using this enum:

typedef NS_ENUM(NSInteger, PlaybackMode) {
  PlaybackModeNone = 0,
  PlaybackModeLocal,
  PlaybackModeRemote
};

PlaybackMode _playbackMode;

It's not important in this codelab for you to understand exactly how all the sample player logic works. It is important to understand that your app's media player will have to be modified to be aware of the two playback locations in a similar way.

At the moment the local player is always in the local playback state since it doesn't know anything about the Casting states yet. We need to update the UI based on state transitions that happen in the Cast framework. For example, if we start casting, we need to stop the local playback and disable some controls. Similarly, if we stop casting when we are in this view controller, we need to transition to local playback. To handle that we need to listen to the various events generated by the Cast framework.

Cast Session Management

For the Cast framework a Cast session combines the steps of connecting to a device, launching (or joining), connecting to a receiver application, and initializing a media control channel if appropriate. The media control channel is how the Cast framework sends and receives messages from the receiver media player.

The Cast session will be started automatically when user select a device from the Cast button, and will be stopped automatically when user disconnects. Reconnecting to a receiver session due to networking issues is also automatically handled by the Cast framework.

Cast sessions are managed by the GCKSessionManager, which can be accessed via [GCKCastContext sharedInstance].sessionManager. The GCKSessionManagerListener callbacks can be used to monitor session events, such as creation, suspension, resumption, and termination.

First we need to import the Google Cast framework in MediaViewController.h:

...
  #import <GoogleCast/GoogleCast.h>
...

Then we need to register our session listener and initialize some variables in MediaViewController.m:

@interface MediaViewController ()<GCKSessionManagerListener,
                                  LocalPlayerViewDelegate> {
...
  GCKSessionManager *_sessionManager;
...
}


- (void)viewDidLoad {
...
  _sessionManager = [GCKCastContext sharedInstance].sessionManager;

  [_sessionManager addListener:self];

...
}

# pragma mark GCKSessionManager methods

- (void)sessionManager:(GCKSessionManager *)sessionManager
       didStartSession:(GCKSession *)session {
  NSLog(@"MediaViewController: sessionManager didStartSession %@", session);
  [self switchToRemotePlayback];
}

- (void)sessionManager:(GCKSessionManager *)sessionManager
      didResumeSession:(GCKSession *)session {
  NSLog(@"MediaViewController: sessionManager didResumeSession %@", session);
  [self switchToRemotePlayback];
}

- (void)sessionManager:(GCKSessionManager *)sessionManager
         didEndSession:(GCKSession *)session
             withError:(NSError *)error {
  NSLog(@"session ended with error: %@", error);
  NSString *message =
      [NSString stringWithFormat:@"The Casting session has ended.\n%@",
                                 [error description]];

  [Toast displayToastMessage:message
             forTimeInterval:3
                      inView:[UIApplication sharedApplication].delegate.window];
  [self switchToLocalPlayback];
}

- (void)sessionManager:(GCKSessionManager *)sessionManager
    didFailToStartSessionWithError:(NSError *)error {
  [self showAlertWithTitle:@"Failed to start a session"
                   message:[error description]];
}

In MediaViewController, we are interested to be informed when we get connected or disconnected from the Cast device so we can switch to or from the local player. Note that connectivity can be disrupted not only by the instance of your application running on your mobile device, but it can also be disrupted by another instance of your (or another) application running on a different mobile device.

The currently active session is accessible as [GCKCastContext sharedInstance].sessionManager.currentCastSession. Sessions are created and torn down automatically in response to user gestures from the Cast dialogs.

Loading Media

In the Cast SDK, the GCKRemoteMediaClient provides a set of convenient APIs for managing the remote media playback on the receiver. For a GCKCastSession that supports media playback, an instance of GCKRemoteMediaClient will be created automatically by the SDK. It can be accessed as the remoteMediaClient property of the GCKCastSession instance.

Add the following code to MediaViewController.m to load the currently selected video on the receiver:

@interface MediaViewController ()<GCKSessionManagerListener,
                                  LocalPlayerViewDelegate
> {
...
  GCKCastSession *_castSession;
  GCKUIMediaController *_castMediaController;
...
}

...

#pragma mark - MediaInfo

- (GCKMediaInformation *)buildMediaInformation {
  GCKMediaMetadata *metadata =
      [[GCKMediaMetadata alloc] initWithMetadataType:GCKMediaMetadataTypeMovie];
  [metadata setString:self.mediaInfo.title forKey:kGCKMetadataKeyTitle];
  [metadata setString:self.mediaInfo.subtitle forKey:kMediaKeyDescription];
  [metadata setString:self.mediaInfo.studio forKey:kGCKMetadataKeyStudio];

  [metadata addImage:[[GCKImage alloc] initWithURL:self.mediaInfo.imageURL
                                             width:480
                                            height:720]];
  [metadata addImage:[[GCKImage alloc] initWithURL:self.mediaInfo.posterURL
                                             width:480
                                            height:720]];

  GCKMediaInformation *mediaInfo = [[GCKMediaInformation alloc]
      initWithContentID:[self.mediaInfo.url absoluteString]
             streamType:GCKMediaStreamTypeBuffered
            contentType:@"video/mp4"
               metadata:metadata
         streamDuration:self.mediaInfo.duration
            mediaTracks:nil
         textTrackStyle:nil
             customData:nil];
  return mediaInfo;
}

- (void)playSelectedItemRemotely {
  GCKCastSession *castSession =
      [GCKCastContext sharedInstance].sessionManager.currentCastSession;
  if (castSession) {
    [castSession.remoteMediaClient loadMedia:[self buildMediaInformation]
                                    autoplay:YES];
  } else {
    NSLog(@"no castSession!");
  }
}

Now update various existing methods to use the Cast session logic to support remote playback:

- (void)viewDidLoad:(BOOL)animated {
...
  _castMediaController = [[GCKUIMediaController alloc] init];
...
}

- (void)switchToLocalPlayback {
  NSLog(@"switchToLocalPlayback");

  if (_playbackMode == PlaybackModeLocal) {
    return;
  }

  NSTimeInterval playPosition = 0;
  BOOL paused = NO;
  BOOL ended = NO;
  if (_playbackMode == PlaybackModeRemote) {
    playPosition = _castMediaController.lastKnownStreamPosition;
    paused = (_castMediaController.lastKnownPlayerState ==
              GCKMediaPlayerStatePaused);
    ended =
        (_castMediaController.lastKnownPlayerState == GCKMediaPlayerStateIdle);
    NSLog(@"last player state: %ld, ended: %d",
          (long)_castMediaController.lastKnownPlayerState, ended);
  }

  [self populateMediaInfo:(!paused && !ended) playPosition:playPosition];

  [_castSession.remoteMediaClient removeListener:self];
  _castSession = nil;

  _playbackMode = PlaybackModeLocal;
}

- (void)switchToRemotePlayback {
  NSLog(@"switchToRemotePlayback; mediaInfo is %@", self.mediaInfo);

  if (_playbackMode == PlaybackModeRemote) {
    return;
  }

  _castSession = _sessionManager.currentCastSession;

  // If we were playing locally, load the local media on the remote player
  if ((_playbackMode == PlaybackModeLocal) &&
      (_localPlayerView.playerState != LocalPlayerStateStopped) &&
      self.mediaInfo) {
    NSLog(@"loading media: %@", self.mediaInfo);
    NSTimeInterval playPosition = _localPlayerView.streamPosition;
    BOOL paused = (_localPlayerView.playerState == LocalPlayerStatePaused);
    GCKMediaQueueItemBuilder *builder = [[GCKMediaQueueItemBuilder alloc] init];
    builder.mediaInformation = [self buildMediaInformation];
    builder.autoplay = !paused;
    builder.preloadTime =
        [[NSUserDefaults standardUserDefaults] integerForKey:kPrefPreloadTime];
    GCKMediaQueueItem *item = [builder build];

    [_castSession.remoteMediaClient queueLoadItems:@[ item ]
                                        startIndex:0
                                      playPosition:playPosition
                                        repeatMode:GCKMediaRepeatModeOff
                                        customData:nil];
  }
  [_localPlayerView stop];
  [_localPlayerView showSplashScreen];
  _playbackMode = PlaybackModeRemote;
}

...

- (BOOL)continueAfterPlayButtonClicked {
  NSLog(@"continueAfterPlayButtonClicked");
  BOOL hasConnectedCastSession =
      [GCKCastContext sharedInstance].sessionManager.hasConnectedCastSession;
  if (self.mediaInfo && hasConnectedCastSession) {
    [self playSelectedItemRemotely];
    return NO;
  }
  return YES;
}

Now, run the app on your mobile device. Connect to your Cast device and start playing a video. You should see the video playing on the receiver.

The Cast Design Checklist requires that all Cast apps provide mini controller to appear when the user navigates away from the current content page. The mini controller provide instant access and a visible reminder for the current Cast session.

The Cast SDK provides a control bar, GCKUIMiniMediaControlsViewController, which can be added to the scenes in which you want to show the persistent controls.

For the sample app, we are going to use the GCKUICastContainerViewController which wraps another view controller and adds a GCKUIMiniMediaControlsViewController at the bottom.

Modify the AppDelegate.m file:

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
...
  UIStoryboard *appStoryboard =
      [UIStoryboard storyboardWithName:@"Main" bundle:nil];
  UINavigationController *navigationController =
      [appStoryboard instantiateViewControllerWithIdentifier:@"MainNavigation"];
  GCKUICastContainerViewController *castContainerVC;
  castContainerVC = [[GCKCastContext sharedInstance]
      createCastContainerControllerForViewController:navigationController];
  castContainerVC.miniMediaControlsItemEnabled = YES;
  self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
  self.window.rootViewController = castContainerVC;
  [self.window makeKeyAndVisible];
...
}

Add these methods to control the visibility of the mini controller (we will use these in a later section):

- (void)setCastControlBarsEnabled:(BOOL)notificationsEnabled {
  GCKUICastContainerViewController *castContainerVC;
  castContainerVC =
      (GCKUICastContainerViewController *)self.window.rootViewController;
  castContainerVC.miniMediaControlsItemEnabled = notificationsEnabled;
}

- (BOOL)castControlBarsEnabled {
  GCKUICastContainerViewController *castContainerVC;
  castContainerVC =
      (GCKUICastContainerViewController *)self.window.rootViewController;
  return castContainerVC.miniMediaControlsItemEnabled;
}

Run the app and cast a video. When playback starts on the receiver you should see the mini controller appear at the bottom of each scene. You can control the remote playback using the mini controller. If you navigate between the browse activity and the local player activity, the mini controller state should stay in sync with the receiver media playback status.

The Google Cast design checklist requires a sender app to introduce the Cast button to existing users to let them know that the sender app now supports Casting and also helps users new to Google Cast.

The GCKCastContext class has a method, presentCastInstructionsViewControllerOnce, that can be used to highlight the Cast button when it is first shown to users. Add the following code to MediaTableViewController.m:

- (void)viewDidAppear:(BOOL)animated {
  [super viewDidAppear:animated];
  [[GCKCastContext sharedInstance] presentCastInstructionsViewControllerOnce];
}

Run the app on your mobile device and you should seen the introductory overlay.

Note: You might need to uninstall the app from the device before running it again to be able to see the overlay.

The Google Cast design checklist requires a sender app to provide expanded controller for the media being cast. The expanded controller is a full screen version of the mini controller.

The expanded controller is a full screen view which offers full control of the remote media playback. This view should allow a casting app to manage every manageable aspect of a cast session, with the exception of receiver volume control and session lifecycle (connect/stop casting). It also provides all the status information about the media session (artwork, title, subtitle, and so forth).

The functionality of this view is implemented by the GCKUIExpandedMediaControlsViewController class.

The first thing you have to do is enable the default expanded controller in the cast context. Modify AppDelegate.m to enable the default expanded controller:

#import <GoogleCast/Googlecast.h>

@interface AppDelegate ()<GCKLoggerDelegate> {
...
}

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
...

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;
...
}

Add the following code to MediaViewController.m to load the expanded controller when the user starts to cast a video:

- (void)playSelectedItemRemotely {
...
  self.navigationItem.backBarButtonItem =
      [[UIBarButtonItem alloc] initWithTitle:@""
                                       style:UIBarButtonItemStylePlain
                                      target:nil
                                      action:nil];
  if (appDelegate.castControlBarsEnabled) {
    appDelegate.castControlBarsEnabled = NO;
  }
  [[GCKCastContext sharedInstance] presentDefaultExpandedMediaControls];
}

The expanded controller will also be launched automatically when the user taps the mini controller.

Run the app on your mobile device and cast a video. You should see the expanded controller. Navigate back to the list of videos and when you click on the mini controller, the expanded controller will be loaded again.

Congratulations

You now know how to Cast-enable a video app using the Cast SDK widgets.

Take a look at our sample apps on GitHub: github.com/googlecast and join our Google Cast Developer community: g.co/googlecastdev