Integra Cast en tu app para iOS

En esta guía para desarrolladores, se describe cómo agregar compatibilidad con Google Cast a tu app emisora para iOS con el SDK de iOS Sender.

El dispositivo móvil o la laptop es el emisor que controla la reproducción, y el dispositivo Google Cast es el receptor que muestra el contenido en la TV.

El framework de remitente hace referencia al objeto binario de la biblioteca de clases de Cast y a los recursos asociados presentes en el entorno de ejecución del remitente. La app emisora o app de Cast hace referencia a una app que también se ejecuta en el emisor. La app de receptor web hace referencia a la aplicación HTML que se ejecuta en el receptor web.

El framework de remitente usa un diseño de devolución de llamada asíncrono para informar a la app emisora sobre los eventos y realizar la transición entre varios estados del ciclo de vida de la app de Cast.

Flujo de la app

En los siguientes pasos, se describe el flujo de ejecución típico de alto nivel para una app emisora para iOS:

  • El framework de Cast inicia GCKDiscoveryManager según las propiedades proporcionadas en GCKCastOptions para comenzar a buscar dispositivos.
  • Cuando el usuario hace clic en el botón para transmitir, el framework presenta el diálogo de Cast con la lista de dispositivos de transmisión descubiertos.
  • Cuando el usuario selecciona un dispositivo de transmisión, el framework intenta iniciar la app de receptor web en el dispositivo de transmisión.
  • El framework invoca devoluciones de llamada en la app emisora para confirmar que se inició la app de Web Receiver.
  • El framework crea un canal de comunicación entre las apps emisora y receptora web.
  • El framework usa el canal de comunicación para cargar y controlar la reproducción de contenido multimedia en el receptor web.
  • El framework sincroniza el estado de reproducción de contenido multimedia entre el emisor y el receptor web: cuando el usuario realiza acciones en la IU del emisor, el framework pasa esas solicitudes de control de contenido multimedia al receptor web, y cuando el receptor web envía actualizaciones de estado de contenido multimedia, el framework actualiza el estado de la IU del emisor.
  • Cuando el usuario haga clic en el botón para transmitir para desconectarse del dispositivo de transmisión, el framework desconectará la app emisora del receptor web.

Para solucionar problemas de tu remitente, debes habilitar el registro.

Para obtener una lista completa de todas las clases, los métodos y los eventos del framework de iOS de Google Cast, consulta la referencia de la API de iOS de Google Cast. En las siguientes secciones, se describen los pasos para integrar Cast en tu app para iOS.

Llama a métodos desde el subproceso principal

Inicializa el contexto de Cast

El framework de Cast tiene un objeto singleton global, el GCKCastContext, que coordina todas las actividades del framework. Este objeto se debe inicializar al principio del ciclo de vida de la aplicación, por lo general, en el método -[application:didFinishLaunchingWithOptions:] del delegado de la app, para que la reanudación automática de la sesión cuando se reinicie la app emisora se pueda activar correctamente.

Se debe proporcionar un objeto GCKCastOptions cuando se inicializa el GCKCastContext. Esta clase contiene opciones que afectan el comportamiento del framework. El más importante de ellos es el ID de aplicación del receptor web, que se usa para filtrar los resultados de descubrimiento y para iniciar la app de receptor web cuando se inicia una sesión de transmisión.

El método -[application:didFinishLaunchingWithOptions:] también es un buen lugar para configurar un delegado de registro que reciba los mensajes de registro del framework. Estos pueden ser útiles para depurar y solucionar problemas.

Swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, GCKLoggerDelegate {
  let kReceiverAppID = kGCKDefaultMediaReceiverApplicationID
  let kDebugLoggingEnabled = true

  var window: UIWindow?

  func applicationDidFinishLaunching(_ application: UIApplication) {
    let criteria = GCKDiscoveryCriteria(applicationID: kReceiverAppID)
    let options = GCKCastOptions(discoveryCriteria: criteria)
    GCKCastContext.setSharedInstanceWith(options)

    // Enable logger.
    GCKLogger.sharedInstance().delegate = self

    ...
  }

  // MARK: - GCKLoggerDelegate

  func logMessage(_ message: String,
                  at level: GCKLoggerLevel,
                  fromFunction function: String,
                  location: String) {
    if (kDebugLoggingEnabled) {
      print(function + " - " + message)
    }
  }
}
Objective-C

AppDelegate.h

@interface AppDelegate () <GCKLoggerDelegate>
@end

AppDelegate.m

@implementation AppDelegate

static NSString *const kReceiverAppID = @"AABBCCDD";
static const BOOL kDebugLoggingEnabled = YES;

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc]
                                    initWithApplicationID:kReceiverAppID];
  GCKCastOptions *options = [[GCKCastOptions alloc] initWithDiscoveryCriteria:criteria];
  [GCKCastContext setSharedInstanceWithOptions:options];

  // Enable logger.
  [GCKLogger sharedInstance].delegate = self;

  ...

  return YES;
}

...

#pragma mark - GCKLoggerDelegate

- (void)logMessage:(NSString *)message
           atLevel:(GCKLoggerLevel)level
      fromFunction:(NSString *)function
          location:(NSString *)location {
  if (kDebugLoggingEnabled) {
    NSLog(@"%@ - %@, %@", function, message, location);
  }
}

@end

Widgets de UX de Cast

El SDK de Cast para iOS proporciona estos widgets que cumplen con la lista de tareas de diseño de Cast:

  • Superposición de introducción: La clase GCKCastContext tiene un método, presentCastInstructionsViewControllerOnceWithCastButton, que se puede usar para destacar el botón para transmitir la primera vez que un receptor de Web está disponible. La app emisora puede personalizar el texto, la posición del texto del título y el botón Descartar.

  • Botón para transmitir: A partir del SDK de la app emisora de Cast para iOS 4.6.0, el botón para transmitir siempre está visible cuando el dispositivo emisor está conectado a Wi-Fi. La primera vez que el usuario presiona el botón para transmitir después de iniciar la app, aparece un diálogo de permisos para que el usuario pueda otorgar a la app acceso a la red local para los dispositivos de la red. Posteriormente, cuando el usuario presiona el botón para transmitir, se muestra un diálogo de transmisión en el que se enumeran los dispositivos descubiertos. Cuando el usuario presiona el botón para transmitir mientras el dispositivo está conectado, se muestran los metadatos del contenido multimedia actual (como el título, el nombre del estudio de grabación y una imagen en miniatura) o se le permite al usuario desconectarse del dispositivo de transmisión. Cuando el usuario presione el botón para transmitir mientras no haya dispositivos disponibles, se mostrará una pantalla con información sobre por qué no se encuentran dispositivos y cómo solucionar el problema.

  • Controlador mini: Cuando el usuario transmite contenido y sale de la página de contenido actual o del controlador expandido para ir a otra pantalla de la app emisora, el controlador mini se muestra en la parte inferior de la pantalla para que el usuario pueda ver los metadatos del contenido multimedia que se está transmitiendo y controlar la reproducción.

  • Controlador expandido: Cuando el usuario transmite contenido, si hace clic en la notificación de contenido multimedia o en el minicontrolador, se inicia el controlador expandido, que muestra los metadatos del contenido multimedia que se está reproduciendo y proporciona varios botones para controlar la reproducción de contenido multimedia.

Agrega un botón para transmitir

El framework proporciona un componente de botón para transmitir como una subclase de UIButton. Se puede agregar a la barra de título de la app si se lo une a un UIBarButtonItem. Una subclase UIViewController típica puede instalar un botón para transmitir de la siguiente manera:

Swift
let castButton = GCKUICastButton(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
castButton.tintColor = UIColor.gray
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: castButton)
Objective-C
GCKUICastButton *castButton = [[GCKUICastButton alloc] initWithFrame:CGRectMake(0, 0, 24, 24)];
castButton.tintColor = [UIColor grayColor];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:castButton];

De forma predeterminada, si presionas el botón, se abrirá el diálogo de Cast que proporciona el framework.

GCKUICastButton también se puede agregar directamente al guion gráfico.

Configura la detección de dispositivos

En el framework, el descubrimiento de dispositivos se produce automáticamente. No es necesario iniciar o detener de forma explícita el proceso de descubrimiento, a menos que implementes una IU personalizada.

El descubrimiento en el framework se administra con la clase GCKDiscoveryManager, que es una propiedad de GCKCastContext. El framework proporciona un componente de diálogo de Cast predeterminado para la selección y el control de dispositivos. La lista de dispositivos se ordena de forma lexicográfica por el nombre descriptivo del dispositivo.

Cómo funciona la administración de sesiones

El SDK de Cast presenta el concepto de sesión de Cast, cuyo establecimiento combina los pasos para conectarse a un dispositivo, iniciar una app de receptor web (o unirse a ella), conectarse a esa app y, luego, inicializar un canal de control de contenido multimedia. Consulta la guía del ciclo de vida de la aplicación del receptor web para obtener más información sobre las sesiones de Cast y el ciclo de vida del receptor web.

Las sesiones se administran con la clase GCKSessionManager, que es una propiedad de GCKCastContext. Las sesiones individuales se representan con subclases de la clase GCKSession: por ejemplo, GCKCastSession representa sesiones con dispositivos de transmisión. Puedes acceder a la sesión de Cast activa (si la hay) como la propiedad currentCastSession de GCKSessionManager.

La interfaz GCKSessionManagerListener se puede usar para supervisar eventos de sesión, como la creación, la suspensión, la reanudación y la finalización de sesiones. El framework suspende automáticamente las sesiones cuando la app emisora pasa a segundo plano y, luego, intenta reanudarlas cuando la app vuelve al primer plano (o se reinicia después de una finalización anormal o abrupta de la app mientras una sesión estaba activa).

Si se usa el diálogo de Cast, las sesiones se crean y se cierran automáticamente en respuesta a los gestos del usuario. De lo contrario, la app puede iniciar y finalizar sesiones de forma explícita a través de métodos en GCKSessionManager.

Si la app necesita realizar un procesamiento especial en respuesta a los eventos del ciclo de vida de la sesión, puede registrar una o más instancias de GCKSessionManagerListener con GCKSessionManager. GCKSessionManagerListener es un protocolo que define devoluciones de llamada para eventos como el inicio y el final de la sesión, entre otros.

Transferencia de transmisión

La preservación del estado de la sesión es la base de la transferencia de transmisión, en la que los usuarios pueden mover transmisiones de audio y video existentes entre dispositivos con comandos por voz, la app de Google Home o pantallas inteligentes. El contenido multimedia deja de reproducirse en un dispositivo (la fuente) y continúa en otro (el destino). Cualquier dispositivo Cast con el firmware más reciente puede funcionar como origen o destino en una transferencia de transmisión.

Para obtener el nuevo dispositivo de destino durante la transferencia de transmisión, usa la propiedad GCKCastSession#device durante la devolución de llamada [sessionManager:didResumeCastSession:].

Consulta Transferencia de transmisión en el receptor web para obtener más información.

Reconexión automática

El framework de Cast agrega lógica de reconexión para controlar automáticamente la reconexión en muchos casos extremos sutiles, como los siguientes:

  • Recuperarse de una pérdida temporal de Wi-Fi
  • Cómo recuperar el dispositivo del modo de suspensión
  • Recuperación después de que la app se ejecuta en segundo plano
  • Recuperación si la app falló

Cómo funciona el control de contenido multimedia

Si se establece una sesión de transmisión con una app de receptor web que admite el espacio de nombres de contenido multimedia, el framework creará automáticamente una instancia de GCKRemoteMediaClient, a la que se puede acceder como la propiedad remoteMediaClient de la instancia de GCKCastSession.

Todos los métodos de GCKRemoteMediaClient que emiten solicitudes al receptor web devolverán un objeto GCKRequest que se puede usar para hacer un seguimiento de esa solicitud. Se puede asignar un GCKRequestDelegate a este objeto para recibir notificaciones sobre el resultado final de la operación.

Se espera que varias partes de la app compartan la instancia de GCKRemoteMediaClient y, de hecho, algunos componentes internos del framework, como el diálogo de Cast y los controles multimedia en miniatura, comparten la instancia. Para ello, GCKRemoteMediaClient admite el registro de varios GCKRemoteMediaClientListener.

Configura los metadatos de contenido multimedia

La clase GCKMediaMetadata representa información sobre un elemento multimedia que quieres transmitir. En el siguiente ejemplo, se crea una nueva instancia GCKMediaMetadata de una película y se establecen el título, el subtítulo, el nombre del estudio de grabación y dos imágenes.

Swift
let metadata = GCKMediaMetadata()
metadata.setString("Big Buck Bunny (2008)", forKey: kGCKMetadataKeyTitle)
metadata.setString("Big Buck Bunny tells the story of a giant rabbit with a heart bigger than " +
  "himself. When one sunny day three rodents rudely harass him, something " +
  "snaps... and the rabbit ain't no bunny anymore! In the typical cartoon " +
  "tradition he prepares the nasty rodents a comical revenge.",
                   forKey: kGCKMetadataKeySubtitle)
metadata.addImage(GCKImage(url: URL(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg")!,
                           width: 480,
                           height: 360))
Objective-C
GCKMediaMetadata *metadata = [[GCKMediaMetadata alloc]
                                initWithMetadataType:GCKMediaMetadataTypeMovie];
[metadata setString:@"Big Buck Bunny (2008)" forKey:kGCKMetadataKeyTitle];
[metadata setString:@"Big Buck Bunny tells the story of a giant rabbit with a heart bigger than "
 "himself. When one sunny day three rodents rudely harass him, something "
 "snaps... and the rabbit ain't no bunny anymore! In the typical cartoon "
 "tradition he prepares the nasty rodents a comical revenge."
             forKey:kGCKMetadataKeySubtitle];
[metadata addImage:[[GCKImage alloc]
                    initWithURL:[[NSURL alloc] initWithString:@"https://commondatastorage.googleapis.com/"
                                 "gtv-videos-bucket/sample/images/BigBuckBunny.jpg"]
                    width:480
                    height:360]];

Consulta la sección Selección y almacenamiento en caché de imágenes para obtener información sobre el uso de imágenes con metadatos de contenido multimedia.

Carga contenido multimedia

Para cargar un elemento multimedia, crea una instancia de GCKMediaInformation con los metadatos del contenido multimedia. Luego, obtén el GCKCastSession actual y usa su GCKRemoteMediaClient para cargar el contenido multimedia en la app de receptor. Luego, puedes usar GCKRemoteMediaClient para controlar una app de reproductor multimedia que se ejecute en el receptor, por ejemplo, para reproducir, pausar y detener.

Swift
let url = URL.init(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")
guard let mediaURL = url else {
  print("invalid mediaURL")
  return
}

let mediaInfoBuilder = GCKMediaInformationBuilder.init(contentURL: mediaURL)
mediaInfoBuilder.streamType = GCKMediaStreamType.none;
mediaInfoBuilder.contentType = "video/mp4"
mediaInfoBuilder.metadata = metadata;
mediaInformation = mediaInfoBuilder.build()

guard let mediaInfo = mediaInformation else {
  print("invalid mediaInformation")
  return
}

if let request = sessionManager.currentSession?.remoteMediaClient?.loadMedia(mediaInfo) {
  request.delegate = self
}
Objective-C
GCKMediaInformationBuilder *mediaInfoBuilder =
  [[GCKMediaInformationBuilder alloc] initWithContentURL:
   [NSURL URLWithString:@"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"]];
mediaInfoBuilder.streamType = GCKMediaStreamTypeNone;
mediaInfoBuilder.contentType = @"video/mp4";
mediaInfoBuilder.metadata = metadata;
self.mediaInformation = [mediaInfoBuilder build];

GCKRequest *request = [self.sessionManager.currentSession.remoteMediaClient loadMedia:self.mediaInformation];
if (request != nil) {
  request.delegate = self;
}

Consulta también la sección sobre cómo usar pistas multimedia.

Formato de video 4K

Para determinar el formato de video de tu contenido multimedia, usa la propiedad videoInfo de GCKMediaStatus para obtener la instancia actual de GCKVideoInfo. Esta instancia contiene el tipo de formato de TV HDR, y la altura y el ancho en píxeles. Las variantes del formato 4K se indican en la propiedad hdrType con los valores de enumeración GCKVideoInfoHDRType.

Cómo agregar minicontroles

Según la lista de tareas de diseño de Cast, una app emisora debe proporcionar un control persistente conocido como minicontrolador, que debería aparecer cuando el usuario sale de la página de contenido actual. El minicontrolador proporciona acceso instantáneo y un recordatorio visible para la sesión de transmisión actual.

El framework de Cast proporciona una barra de control, GCKUIMiniMediaControlsViewController, que se puede agregar a las escenas en las que deseas mostrar el minicontrolador.

Cuando tu app emisora reproduce una transmisión en vivo de video o audio, el SDK muestra automáticamente un botón de reproducción/detención en lugar del botón de reproducción/pausa en el minicontrolador.

Consulta Cómo personalizar la IU de la app emisora para iOS para saber cómo tu app emisora puede configurar la apariencia de los widgets de Cast.

Existen dos maneras de agregar el minicontrolador a una app emisora:

  • Permite que el framework de Cast administre el diseño del minicontrolador uniendo tu controlador de vista existente con su propio controlador de vista.
  • Para administrar el diseño del widget del minicontrolador, agrégalo a tu controlador de vista existente proporcionando una vista secundaria en el guion gráfico.

Une con GCKUICastContainerViewController

La primera forma es usar el GCKUICastContainerViewController, que une otro controlador de vista y agrega un GCKUIMiniMediaControlsViewController en la parte inferior. Este enfoque es limitado, ya que no puedes personalizar la animación ni configurar el comportamiento del controlador de vista del contenedor.

Por lo general, esta primera forma se realiza en el método -[application:didFinishLaunchingWithOptions:] del delegado de la app:

Swift
func applicationDidFinishLaunching(_ application: UIApplication) {
  ...

  // Wrap main view in the GCKUICastContainerViewController and display the mini controller.
  let appStoryboard = UIStoryboard(name: "Main", bundle: nil)
  let navigationController = appStoryboard.instantiateViewController(withIdentifier: "MainNavigation")
  let castContainerVC =
          GCKCastContext.sharedInstance().createCastContainerController(for: navigationController)
  castContainerVC.miniMediaControlsItemEnabled = true
  window = UIWindow(frame: UIScreen.main.bounds)
  window!.rootViewController = castContainerVC
  window!.makeKeyAndVisible()

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

  // Wrap main view in the GCKUICastContainerViewController and display the mini controller.
  UIStoryboard *appStoryboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
  UINavigationController *navigationController =
          [appStoryboard instantiateViewControllerWithIdentifier:@"MainNavigation"];
  GCKUICastContainerViewController *castContainerVC =
          [[GCKCastContext sharedInstance] createCastContainerControllerForViewController:navigationController];
  castContainerVC.miniMediaControlsItemEnabled = YES;
  self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
  self.window.rootViewController = castContainerVC;
  [self.window makeKeyAndVisible];
  ...

}
Swift
var castControlBarsEnabled: Bool {
  set(enabled) {
    if let castContainerVC = self.window?.rootViewController as? GCKUICastContainerViewController {
      castContainerVC.miniMediaControlsItemEnabled = enabled
    } else {
      print("GCKUICastContainerViewController is not correctly configured")
    }
  }
  get {
    if let castContainerVC = self.window?.rootViewController as? GCKUICastContainerViewController {
      return castContainerVC.miniMediaControlsItemEnabled
    } else {
      print("GCKUICastContainerViewController is not correctly configured")
      return false
    }
  }
}
Objective-C

AppDelegate.h

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (nonatomic, strong) UIWindow *window;
@property (nonatomic, assign) BOOL castControlBarsEnabled;

@end

AppDelegate.m

@implementation AppDelegate

...

- (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;
}

...

@end

Incorpora el SDK en un controlador de vista existente

La segunda forma es agregar el minicontrolador directamente a tu controlador de vista existente con createMiniMediaControlsViewController para crear una instancia de GCKUIMiniMediaControlsViewController y, luego, agregarla al controlador de vista del contenedor como una vista secundaria.

Configura tu controlador de vistas en el delegado de la app:

Swift
func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
  ...

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true
  window?.clipsToBounds = true

  let rootContainerVC = (window?.rootViewController as? RootContainerViewController)
  rootContainerVC?.miniMediaControlsViewEnabled = true

  ...

  return true
}
Objective-C
- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  self.window.clipsToBounds = YES;

  RootContainerViewController *rootContainerVC;
  rootContainerVC =
      (RootContainerViewController *)self.window.rootViewController;
  rootContainerVC.miniMediaControlsViewEnabled = YES;

  ...

  return YES;
}

En el controlador de vista raíz, crea una instancia de GCKUIMiniMediaControlsViewController y agrégala al controlador de vista del contenedor como una vista secundaria:

Swift
let kCastControlBarsAnimationDuration: TimeInterval = 0.20

@objc(RootContainerViewController)
class RootContainerViewController: UIViewController, GCKUIMiniMediaControlsViewControllerDelegate {
  @IBOutlet weak private var _miniMediaControlsContainerView: UIView!
  @IBOutlet weak private var _miniMediaControlsHeightConstraint: NSLayoutConstraint!
  private var miniMediaControlsViewController: GCKUIMiniMediaControlsViewController!
  var miniMediaControlsViewEnabled = false {
    didSet {
      if self.isViewLoaded {
        self.updateControlBarsVisibility()
      }
    }
  }

  var overriddenNavigationController: UINavigationController?

  override var navigationController: UINavigationController? {

    get {
      return overriddenNavigationController
    }

    set {
      overriddenNavigationController = newValue
    }
  }
  var miniMediaControlsItemEnabled = false

  override func viewDidLoad() {
    super.viewDidLoad()
    let castContext = GCKCastContext.sharedInstance()
    self.miniMediaControlsViewController = castContext.createMiniMediaControlsViewController()
    self.miniMediaControlsViewController.delegate = self
    self.updateControlBarsVisibility()
    self.installViewController(self.miniMediaControlsViewController,
                               inContainerView: self._miniMediaControlsContainerView)
  }

  func updateControlBarsVisibility() {
    if self.miniMediaControlsViewEnabled && self.miniMediaControlsViewController.active {
      self._miniMediaControlsHeightConstraint.constant = self.miniMediaControlsViewController.minHeight
      self.view.bringSubview(toFront: self._miniMediaControlsContainerView)
    } else {
      self._miniMediaControlsHeightConstraint.constant = 0
    }
    UIView.animate(withDuration: kCastControlBarsAnimationDuration, animations: {() -> Void in
      self.view.layoutIfNeeded()
    })
    self.view.setNeedsLayout()
  }

  func installViewController(_ viewController: UIViewController?, inContainerView containerView: UIView) {
    if let viewController = viewController {
      self.addChildViewController(viewController)
      viewController.view.frame = containerView.bounds
      containerView.addSubview(viewController.view)
      viewController.didMove(toParentViewController: self)
    }
  }

  func uninstallViewController(_ viewController: UIViewController) {
    viewController.willMove(toParentViewController: nil)
    viewController.view.removeFromSuperview()
    viewController.removeFromParentViewController()
  }

  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "NavigationVCEmbedSegue" {
      self.navigationController = (segue.destination as? UINavigationController)
    }
  }

...
Objective-C

RootContainerViewController.h

static const NSTimeInterval kCastControlBarsAnimationDuration = 0.20;

@interface RootContainerViewController () <GCKUIMiniMediaControlsViewControllerDelegate> {
  __weak IBOutlet UIView *_miniMediaControlsContainerView;
  __weak IBOutlet NSLayoutConstraint *_miniMediaControlsHeightConstraint;
  GCKUIMiniMediaControlsViewController *_miniMediaControlsViewController;
}

@property(nonatomic, weak, readwrite) UINavigationController *navigationController;

@property(nonatomic, assign, readwrite) BOOL miniMediaControlsViewEnabled;
@property(nonatomic, assign, readwrite) BOOL miniMediaControlsItemEnabled;

@end

RootContainerViewController.m

@implementation RootContainerViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  GCKCastContext *castContext = [GCKCastContext sharedInstance];
  _miniMediaControlsViewController =
      [castContext createMiniMediaControlsViewController];
  _miniMediaControlsViewController.delegate = self;

  [self updateControlBarsVisibility];
  [self installViewController:_miniMediaControlsViewController
              inContainerView:_miniMediaControlsContainerView];
}

- (void)setMiniMediaControlsViewEnabled:(BOOL)miniMediaControlsViewEnabled {
  _miniMediaControlsViewEnabled = miniMediaControlsViewEnabled;
  if (self.isViewLoaded) {
    [self updateControlBarsVisibility];
  }
}

- (void)updateControlBarsVisibility {
  if (self.miniMediaControlsViewEnabled &&
      _miniMediaControlsViewController.active) {
    _miniMediaControlsHeightConstraint.constant =
        _miniMediaControlsViewController.minHeight;
    [self.view bringSubviewToFront:_miniMediaControlsContainerView];
  } else {
    _miniMediaControlsHeightConstraint.constant = 0;
  }
  [UIView animateWithDuration:kCastControlBarsAnimationDuration
                   animations:^{
                     [self.view layoutIfNeeded];
                   }];
  [self.view setNeedsLayout];
}

- (void)installViewController:(UIViewController *)viewController
              inContainerView:(UIView *)containerView {
  if (viewController) {
    [self addChildViewController:viewController];
    viewController.view.frame = containerView.bounds;
    [containerView addSubview:viewController.view];
    [viewController didMoveToParentViewController:self];
  }
}

- (void)uninstallViewController:(UIViewController *)viewController {
  [viewController willMoveToParentViewController:nil];
  [viewController.view removeFromSuperview];
  [viewController removeFromParentViewController];
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
  if ([segue.identifier isEqualToString:@"NavigationVCEmbedSegue"]) {
    self.navigationController =
        (UINavigationController *)segue.destinationViewController;
  }
}

...

@end

GCKUIMiniMediaControlsViewControllerDelegate le indica al controlador de vista del host cuándo debe estar visible el minicontrolador:

Swift
  func miniMediaControlsViewController(_: GCKUIMiniMediaControlsViewController,
                                       shouldAppear _: Bool) {
    updateControlBarsVisibility()
  }
Objective-C
- (void)miniMediaControlsViewController:
            (GCKUIMiniMediaControlsViewController *)miniMediaControlsViewController
                           shouldAppear:(BOOL)shouldAppear {
  [self updateControlBarsVisibility];
}

Agrega un controlador expandido

La lista de tareas de diseño de Google Cast requiere que una app emisora proporcione un controlador expandido para el contenido multimedia que se transmite. El control expandido es una versión en pantalla completa del control mini.

El controlador expandido es una vista de pantalla completa que ofrece control total de la reproducción de contenido multimedia remoto. Esta vista debería permitir que una app de transmisión administre todos los aspectos administrables de una sesión de transmisión, con la excepción del control de volumen del receptor web y el ciclo de vida de la sesión (conectar o detener la transmisión). También proporciona toda la información de estado sobre la sesión multimedia (imagen, título, subtítulo, etcétera).

La funcionalidad de esta vista se implementa con la clase GCKUIExpandedMediaControlsViewController.

Lo primero que debes hacer es habilitar el controlador expandido predeterminado en el contexto de transmisión. Modifica el delegado de la app para habilitar el controlador expandido predeterminado:

Swift
func applicationDidFinishLaunching(_ application: UIApplication) {
  ..

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true

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

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  ..
}

Agrega el siguiente código a tu controlador de vista para cargar el controlador expandido cuando el usuario comience a transmitir un video:

Swift
func playSelectedItemRemotely() {
  GCKCastContext.sharedInstance().presentDefaultExpandedMediaControls()

  ...

  // Load your media
  sessionManager.currentSession?.remoteMediaClient?.loadMedia(mediaInformation)
}
Objective-C
- (void)playSelectedItemRemotely {
  [[GCKCastContext sharedInstance] presentDefaultExpandedMediaControls];

  ...

  // Load your media
  [self.sessionManager.currentSession.remoteMediaClient loadMedia:mediaInformation];
}

El control expandido también se iniciará automáticamente cuando el usuario presione el control mini.

Cuando tu app emisora reproduce una transmisión en vivo de video o audio, el SDK muestra automáticamente un botón de reproducción/detención en lugar del botón de reproducción/pausa en el control expandido.

Consulta Cómo aplicar estilos personalizados a tu app para iOS para saber cómo tu app emisora puede configurar el aspecto de los widgets de Cast.

Control de volumen

El framework de Cast administra automáticamente el volumen de la app emisora y se sincroniza de forma automática con el volumen del receptor web para los widgets de IU proporcionados. Para sincronizar un control deslizante proporcionado por la app, usa GCKUIDeviceVolumeController.

Control de volumen con botones físicos

Los botones de volumen físicos del dispositivo emisor se pueden usar para cambiar el volumen de la sesión de transmisión en el receptor web con la marca physicalVolumeButtonsWillControlDeviceVolume en GCKCastOptions, que se establece en GCKCastContext.

Swift
let criteria = GCKDiscoveryCriteria(applicationID: kReceiverAppID)
let options = GCKCastOptions(discoveryCriteria: criteria)
options.physicalVolumeButtonsWillControlDeviceVolume = true
GCKCastContext.setSharedInstanceWith(options)
Objective-C
GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc]
                                          initWithApplicationID:kReceiverAppID];
GCKCastOptions *options = [[GCKCastOptions alloc]
                                          initWithDiscoveryCriteria :criteria];
options.physicalVolumeButtonsWillControlDeviceVolume = YES;
[GCKCastContext setSharedInstanceWithOptions:options];

Soluciona errores

Es muy importante que las apps emisoras controlen todas las devoluciones de llamada de error y decidan la mejor respuesta para cada etapa del ciclo de vida de Cast. La app puede mostrar diálogos de error al usuario o decidir finalizar la sesión de Cast.

Ten en cuenta que algunos errores, incluido el GCKErrorCode GCKErrorCodeCancelled, son un comportamiento previsto.

No intentes volver a establecer una conexión que falló con GCKErrorCodeCancelled. Si lo haces, es posible que se produzca un comportamiento inesperado.

Logging

GCKLogger es un singleton que el framework usa para el registro. Usa el GCKLoggerDelegate para personalizar la forma en que manejas los mensajes de registro.

Con GCKLogger, el SDK produce un resultado de registro en forma de mensajes de depuración, errores y advertencias. Estos mensajes de registro ayudan a depurar y son útiles para solucionar problemas y detectar errores. De forma predeterminada, se suprime el resultado del registro, pero, si se asigna un GCKLoggerDelegate, la app emisora puede recibir estos mensajes del SDK y registrarlos en la consola del sistema.

Swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, GCKLoggerDelegate {
  let kReceiverAppID = kGCKDefaultMediaReceiverApplicationID
  let kDebugLoggingEnabled = true

  var window: UIWindow?

  func applicationDidFinishLaunching(_ application: UIApplication) {
    ...

    // Enable logger.
    GCKLogger.sharedInstance().delegate = self

    ...
  }

  // MARK: - GCKLoggerDelegate

  func logMessage(_ message: String,
                  at level: GCKLoggerLevel,
                  fromFunction function: String,
                  location: String) {
    if (kDebugLoggingEnabled) {
      print(function + " - " + message)
    }
  }
}
Objective-C

AppDelegate.h

@interface AppDelegate () <GCKLoggerDelegate>
@end

AppDelegate.m

@implementation AppDelegate

static NSString *const kReceiverAppID = @"AABBCCDD";
static const BOOL kDebugLoggingEnabled = YES;

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

  // Enable logger.
  [GCKLogger sharedInstance].delegate = self;

  ...

  return YES;
}

...

#pragma mark - GCKLoggerDelegate

- (void)logMessage:(NSString *)message
           atLevel:(GCKLoggerLevel)level
      fromFunction:(NSString *)function
          location:(NSString *)location {
  if (kDebugLoggingEnabled) {
    NSLog(@"%@ - %@, %@", function, message, location);
  }
}

@end

Para habilitar también los mensajes de depuración y detallados, agrega esta línea al código después de configurar el delegado (como se mostró anteriormente):

Swift
let filter = GCKLoggerFilter.init()
filter.minimumLevel = GCKLoggerLevel.verbose
GCKLogger.sharedInstance().filter = filter
Objective-C
GCKLoggerFilter *filter = [[GCKLoggerFilter alloc] init];
[filter setMinimumLevel:GCKLoggerLevelVerbose];
[GCKLogger sharedInstance].filter = filter;

También puedes filtrar los mensajes de registro que produce GCKLogger. Establece el nivel de registro mínimo por clase, por ejemplo:

Swift
let filter = GCKLoggerFilter.init()
filter.setLoggingLevel(GCKLoggerLevel.verbose, forClasses: ["GCKUICastButton",
                                                            "GCKUIImageCache",
                                                            "NSMutableDictionary"])
GCKLogger.sharedInstance().filter = filter
Objective-C
GCKLoggerFilter *filter = [[GCKLoggerFilter alloc] init];
[filter setLoggingLevel:GCKLoggerLevelVerbose
             forClasses:@[@"GCKUICastButton",
                          @"GCKUIImageCache",
                          @"NSMutableDictionary"
                          ]];
[GCKLogger sharedInstance].filter = filter;

Los nombres de las clases pueden ser nombres literales o patrones globales, por ejemplo, GCKUI\* y GCK\*Session.