Cómo habilitar la transmisión de una app para iOS

1. Descripción general

Logotipo de Google Cast

En este codelab, aprenderás a modificar una app de video para iOS existente a fin de transmitir contenido en un dispositivo compatible con Google Cast.

¿Qué es Google Cast?

Google Cast permite a los usuarios transmitir contenido desde un dispositivo móvil a una TV. De esa manera, los usuarios pueden usar su dispositivo móvil como control remoto de modo que se reproduzca contenido multimedia en la TV.

El SDK de Google Cast te permite ampliar tu app para controlar dispositivos compatibles con Google Cast (como una TV o un sistema de sonido). Este SDK te permite agregar los componentes de IU necesarios según la lista de tareas de diseño de Google Cast.

Te proporcionamos la lista de tareas de diseño de Google Cast con el fin de que la experiencia del usuario de Cast resulte sencilla y predecible en todas las plataformas compatibles.

¿Qué compilaremos?

Cuando completes este codelab, tendrás una app de video para iOS que podrá transmitir videos a un dispositivo Google Cast.

Qué aprenderás

  • Cómo agregar el SDK de Google Cast a una app de video de muestra
  • Cómo agregar el botón para transmitir a fin de seleccionar un dispositivo Google Cast
  • Cómo conectarse a un dispositivo de transmisión e iniciar un receptor de contenido multimedia
  • Cómo transmitir un video
  • Cómo agregar un minicontrolador de transmisión a tu app
  • Cómo agregar un control expandido
  • Cómo proporcionar una superposición introductoria
  • Cómo personalizar los widgets de Cast
  • Cómo integrar Cast Connect

Requisitos

  • El Xcode más reciente
  • Un dispositivo móvil con iOS 9 o versiones posteriores (o el simulador de Xcode)
  • Un cable de datos USB para conectar tu dispositivo móvil a la computadora de desarrollo (si usas un dispositivo)
  • Un dispositivo Google Cast, como Chromecast o Android TV, que esté configurado con acceso a Internet
  • Una TV o un monitor con entrada HDMI
  • Se requiere un Chromecast con Google TV para probar la integración de Cast Connect, pero es opcional para el resto del codelab. Si no tienes una, puedes omitir el paso Agregar compatibilidad con Cast Connect al final de este instructivo.

Experiencia

  • Debes tener conocimientos previos sobre desarrollo para iOS.
  • También deberás tener experiencia como usuario de TV :)

¿Cómo usarás este instructivo?

Ler Leer y completar los ejercicios

¿Cómo calificarías tu experiencia con la compilación de apps para iOS?

Principiante Intermedio Avanzado

¿Cómo calificarías tu experiencia cuando miras TV?

Principiante Intermedio Avanzado

2. Obtén el código de muestra

Puedes descargar el código de muestra completo a tu computadora...

y descomprimir el archivo ZIP que se descargó.

3. Ejecuta la app de muestra

Logotipo de Apple iOS

Primero, veamos el aspecto de la app de muestra completa. La app es un reproductor de video básico. El usuario podrá seleccionar un video de una lista y, a continuación, reproducirlo en un dispositivo local o transmitirlo a uno compatible con Google Cast.

Una vez que hayas descargado el código, sigue las siguientes instrucciones para abrir y ejecutar la app de ejemplo completa en Xcode:

Preguntas frecuentes

Configuración de CocoaPods

Para configurar CocoaPods, ve a la consola y realiza la instalación con la versión predeterminada de Ruby disponible en macOS:

sudo gem install cocoapods

Si tienes algún problema, consulta la documentación oficial para descargar e instalar el administrador de dependencias.

Configuración del proyecto

  1. Ve a tu terminal y navega al directorio del codelab.
  2. Instala las dependencias desde el Podfile.
cd app-done
pod update
pod install
  1. Abre Xcode y selecciona Abrir otro proyecto...
  2. Selecciona el archivo CastVideos-ios.xcworkspace del directorio ícono de carpetaapp-done de la carpeta del código de muestra.

Ejecuta la app

Selecciona el destino y el simulador y, luego, ejecuta la app:

Barra de herramientas del simulador de apps de Xcode

La app de video debería aparecer después de unos segundos.

Asegúrate de hacer clic en "Permitir" cuando aparezca la notificación sobre la aceptación de conexiones de red entrantes. El ícono para transmitir no aparecerá si no aceptas esta opción.

Diálogo de confirmación que solicita permiso para aceptar conexiones de red entrantes

Haz clic en el botón para transmitir y selecciona tu dispositivo Google Cast.

Selecciona un video y haz clic en el botón de reproducción.

El video comenzará a reproducirse en tu dispositivo Google Cast.

Se mostrará el control expandido. Puedes usar el botón de reproducción/pausa para controlar la reproducción.

Regresa a la lista de videos.

Ahora verás un minicontrol en la parte inferior de la pantalla.

Ilustración de un iPhone ejecutando la app CastVideos con el minicontrol en la parte inferior

Haz clic en el botón de pausa del minicontrolador a fin de pausar el video en el receptor. Haz clic en el botón de reproducción del minicontrolador para reanudar la reproducción del video.

Haz clic en el botón para transmitir a fin de detener la transmisión al dispositivo Google Cast.

4. Prepara el proyecto inicial

Ilustración de un iPhone ejecutando la app CastVideos

Debemos agregar compatibilidad con Google Cast a la app inicial que descargaste. Estos son algunos términos relacionados con Google Cast que usaremos en este codelab:

  • una app emisora se ejecuta en un dispositivo móvil o una laptop.
  • una app receptora se ejecuta en el dispositivo Google Cast.

Configuración del proyecto

Ya tienes todo listo para compilar sobre el proyecto inicial con Xcode:

  1. Ve a tu terminal y navega al directorio del codelab.
  2. Instala las dependencias desde el Podfile.
cd app-start
pod update
pod install
  1. Abre Xcode y selecciona Abrir otro proyecto...
  2. Selecciona el archivo CastVideos-ios.xcworkspace del directorio ícono de carpetaapp-start de la carpeta del código de muestra.

Diseño de apps

La app recuperará una lista de videos de un servidor web remoto y proporcionará una lista para que el usuario explore. Los usuarios podrán seleccionar un video de forma que vean los detalles o reproducirlo localmente en el dispositivo móvil.

La app consta de dos controladores de vista principales: MediaTableViewController y MediaViewController..

MediaTableViewController

El controlador UITableViewController muestra una lista de videos de una instancia de MediaListModel. La lista de videos y los metadatos asociados se alojan en un servidor remoto como un archivo JSON. MediaListModel recupera este JSON y lo procesa para compilar una lista de objetos MediaItem.

Un objeto MediaItem modela un video y los metadatos asociados, como el título, la descripción, la URL de una imagen y la URL de la transmisión.

MediaTableViewController crea una instancia de MediaListModel y, luego, se registra como MediaListModelDelegate para recibir información cuando se descarguen los metadatos multimedia, de modo que pueda cargar la vista de tabla.

Se le mostrará al usuario una lista de miniaturas de video con una descripción breve de cada uno. Cuando se selecciona un elemento, el MediaItem correspondiente se pasa a MediaViewController.

MediaViewController

Este controlador de vista muestra los metadatos de un video específico y permite que el usuario reproduzca el video de forma local en el dispositivo móvil.

El controlador de vista aloja un LocalPlayerView, algunos controles multimedia y un área de texto para mostrar la descripción del video seleccionado. El reproductor cubre la parte superior de la pantalla y debajo se deja espacio para la descripción detallada del video. El usuario puede reproducir, pausar o saltar a la reproducción local del video.

Preguntas frecuentes

5. Agrega el botón para transmitir

Ilustración del tercio superior de un iPhone que ejecuta la app CastVideos, en la que se muestra el botón para transmitir en la esquina superior derecha

Una app compatible con Cast muestra el botón para transmitir en cada uno de sus controladores de vista. Al hacer clic en ese botón, se mostrará la lista de dispositivos de transmisión que un usuario puede seleccionar. Si el usuario estaba reproduciendo contenido de forma local en el dispositivo emisor, al seleccionar un dispositivo de transmisión podrá iniciar o reanudar la reproducción en ese dispositivo. En cualquier momento de una sesión de transmisión, el usuario podrá hacer clic en el botón para transmitir y dejar de transmitir tu aplicación al dispositivo de transmisión. El usuario debe poder conectarse al dispositivo de transmisión o desconectarse de él desde cualquier pantalla de la aplicación, como se describe en la Lista de tareas de diseño de Google Cast.

Configuración

El proyecto de inicio requiere las mismas dependencias y configuración de Xcode que la app de ejemplo completa. Regresa a esa sección y sigue los mismos pasos para agregar el GoogleCast.framework al proyecto de la app de inicio.

Inicialización

El framework de Cast tiene un objeto singleton global, el GCKCastContext, que coordina todas las actividades del framework. Este objeto debe inicializarse con anticipación en el ciclo de vida de la aplicación, por lo general, en el método application(_:didFinishLaunchingWithOptions:) del delegado de la app, de modo que la reanudación automática de la sesión cuando se reinicie la aplicación emisora pueda activarse correctamente y comenzar la búsqueda de dispositivos.

Se debe proporcionar un objeto GCKCastOptions cuando se inicializa GCKCastContext. Esta clase contiene opciones que afectan el comportamiento del framework. El más importante es el ID de aplicación receptora, que se usa para filtrar los resultados de la detección de dispositivos de transmisión y para iniciar la aplicación receptora 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 a fin de recibir los mensajes de registro del framework de Cast. Estos pueden ser útiles para depurar y solucionar problemas.

Cuando desarrolles tu propia app compatible con Cast, tendrás que registrarte como desarrollador de Cast y, luego, obtener el ID de aplicación correspondiente a tu app. Para este codelab, usaremos un ID de app de muestra.

Agrega el siguiente código a AppDelegate.swift para inicializar GCKCastContext con el ID de aplicación de los valores predeterminados del usuario y agrega un registrador para el framework de Google Cast:

import GoogleCast

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  fileprivate var enableSDKLogging = true

  ...

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

    ...
    let options = GCKCastOptions(discoveryCriteria: GCKDiscoveryCriteria(applicationID: kReceiverAppID))
    options.physicalVolumeButtonsWillControlDeviceVolume = true
    GCKCastContext.setSharedInstanceWith(options)

    window?.clipsToBounds = true
    setupCastLogging()
    ...
  }
  ...
  func setupCastLogging() {
    let logFilter = GCKLoggerFilter()
    let classesToLog = ["GCKDeviceScanner", "GCKDeviceProvider", "GCKDiscoveryManager", "GCKCastChannel",
                        "GCKMediaControlChannel", "GCKUICastButton", "GCKUIMediaController", "NSMutableDictionary"]
    logFilter.setLoggingLevel(.verbose, forClasses: classesToLog)
    GCKLogger.sharedInstance().filter = logFilter
    GCKLogger.sharedInstance().delegate = self
  }
}

...

// MARK: - GCKLoggerDelegate

extension AppDelegate: GCKLoggerDelegate {
  func logMessage(_ message: String,
                  at _: GCKLoggerLevel,
                  fromFunction function: String,
                  location: String) {
    if enableSDKLogging {
      // Send SDK's log messages directly to the console.
      print("\(location): \(function) - \(message)")
    }
  }
}

Botón para transmitir

Ahora que se inicializó GCKCastContext, debemos agregar el botón para transmitir a fin de permitir que el usuario seleccione un dispositivo de transmisión. El SDK de Cast proporciona un componente del botón para transmitir llamado GCKUICastButton como una subclase UIButton. Se puede agregar a la barra de título de la aplicación si la unes en una UIBarButtonItem. Debemos agregar el botón para transmitir a MediaTableViewController y MediaViewController.

Agrega el siguiente código a MediaTableViewController.swift y MediaViewController.swift:

import GoogleCast

@objc(MediaTableViewController)
class MediaTableViewController: UITableViewController, GCKSessionManagerListener,
  MediaListModelDelegate, GCKRequestDelegate {
  private var castButton: GCKUICastButton!
  ...
  override func viewDidLoad() {
    print("MediaTableViewController - viewDidLoad")
    super.viewDidLoad()

    ...
    castButton = GCKUICastButton(frame: CGRect(x: CGFloat(0), y: CGFloat(0),
                                               width: CGFloat(24), height: CGFloat(24)))
    // Overwrite the UIAppearance theme in the AppDelegate.
    castButton.tintColor = UIColor.white
    navigationItem.rightBarButtonItem = UIBarButtonItem(customView: castButton)

    ...
  }
  ...
}

A continuación, agrega el siguiente código a tu MediaViewController.swift:

import GoogleCast

@objc(MediaViewController)
class MediaViewController: UIViewController, GCKSessionManagerListener, GCKRemoteMediaClientListener,
  LocalPlayerViewDelegate, GCKRequestDelegate {
  private var castButton: GCKUICastButton!
  ...
  override func viewDidLoad() {
    super.viewDidLoad()
    print("in MediaViewController viewDidLoad")
    ...
    castButton = GCKUICastButton(frame: CGRect(x: CGFloat(0), y: CGFloat(0),
                                               width: CGFloat(24), height: CGFloat(24)))
    // Overwrite the UIAppearance theme in the AppDelegate.
    castButton.tintColor = UIColor.white
    navigationItem.rightBarButtonItem = UIBarButtonItem(customView: castButton)

    ...
  }
  ...
}

Ahora, ejecuta la app. Deberías ver el botón para transmitir en la barra de navegación de la app. Cuando hagas clic en él, se mostrará una lista de los dispositivos de transmisión de tu red local. GCKCastContext administra la detección de dispositivos automáticamente. Selecciona tu dispositivo de transmisión. La app receptora de muestra se cargará en él. Puedes navegar entre la actividad de navegación y la actividad del reproductor local. El estado del botón para transmitir se mantendrá sincronizado.

Aún no se incluyó ningún tipo de compatibilidad para reproducir contenido multimedia, por lo que aún no podrás reproducir videos en el dispositivo de transmisión. Haz clic en el botón para transmitir a fin de detener la transmisión.

6. Transmite contenido de video

Ilustración de un iPhone que ejecuta la app CastVideos, en la que se muestran detalles de un video específico ("Lágrimas de acero"). En la parte inferior,
se encuentra el reproductor en miniatura.

Extenderemos la app de muestra de modo que también reproduzca videos de forma remota en un dispositivo de transmisión. Para ello, tenemos que escuchar los diferentes eventos generados por el framework de Cast.

Transmisión de contenido multimedia

En términos generales, si quieres reproducir contenido multimedia en un dispositivo de transmisión, debe suceder lo siguiente:

  1. Crea un objeto GCKMediaInformation desde el SDK de Cast que modele un elemento multimedia.
  2. El usuario se conecta al dispositivo de transmisión para iniciar la aplicación receptora.
  3. Cargar el objeto GCKMediaInformation en tu receptor y reproducir el contenido
  4. Realizar un seguimiento del estado del contenido multimedia
  5. Enviar comandos de reproducción al receptor según las interacciones del usuario

El paso 1 equivale a asignar un objeto a otro. GCKMediaInformation es algo que el SDK de Cast comprende y MediaItem es el encapsulamiento de nuestra app para un elemento multimedia. Podemos asignar fácilmente un MediaItem a un GCKMediaInformation. Ya realizamos el paso 2 en la sección anterior. El paso 3 es fácil de completar con el SDK de Cast.

La app de muestra MediaViewController ya distingue entre la reproducción local y la remota a través de esta enumeración:

enum PlaybackMode: Int {
  case none = 0
  case local
  case remote
}

private var playbackMode = PlaybackMode.none

En este codelab, no es importante que comprendas exactamente cómo funciona toda la lógica del reproductor de muestra. Sí resulta importante que comprendas que el reproductor multimedia de tu app deberá modificarse para que tenga en cuenta las dos ubicaciones de reproducción de manera similar.

Por el momento, el reproductor local siempre estará en el estado de reproducción local porque aún no conoce los estados de transmisión. Deberemos actualizar la IU en función de las transiciones de estado que ocurran en el framework de Cast. Por ejemplo, si comenzamos a transmitir, deberemos detener la reproducción local e inhabilitar algunos controles. Del mismo modo, si dejamos de transmitir cuando estamos en este controlador de vista, debemos hacer la transición a la reproducción local. Para ello, tenemos que escuchar los diferentes eventos generados por el framework de Cast.

Administración de sesiones de transmisión

En el framework de Cast, una sesión de transmisión combina los pasos para conectarse a un dispositivo, iniciar (o unirse a) una aplicación receptora y conectarse a ella e inicializar un canal de control de contenido multimedia, si corresponde. Este canal es la forma en la que el framework de Cast envía y recibe mensajes del reproductor multimedia del receptor.

La sesión de transmisión se iniciará automáticamente cuando el usuario seleccione un dispositivo desde el botón para transmitir y se detendrá automáticamente cuando el usuario se desconecte. El framework de Cast también controla automáticamente la reconexión a la sesión de un receptor debido a problemas de red.

GCKSessionManager administra las sesiones de transmisión, a las que se puede acceder a través de GCKCastContext.sharedInstance().sessionManager. Las devoluciones de llamada de GCKSessionManagerListener se pueden usar para supervisar los eventos de sesión, como la creación, suspensión, reanudación y finalización.

Primero, debemos registrar nuestro objeto de escucha de la sesión y, luego, inicializar algunas variables:

class MediaViewController: UIViewController, GCKSessionManagerListener,
  GCKRemoteMediaClientListener, LocalPlayerViewDelegate, GCKRequestDelegate {

  ...
  private var sessionManager: GCKSessionManager!
  ...

  required init?(coder: NSCoder) {
    super.init(coder: coder)

    sessionManager = GCKCastContext.sharedInstance().sessionManager

    ...
  }

  override func viewWillAppear(_ animated: Bool) {
    ...

    let hasConnectedSession: Bool = (sessionManager.hasConnectedSession())
    if hasConnectedSession, (playbackMode != .remote) {
      populateMediaInfo(false, playPosition: 0)
      switchToRemotePlayback()
    } else if sessionManager.currentSession == nil, (playbackMode != .local) {
      switchToLocalPlayback()
    }

    sessionManager.add(self)

    ...
  }

  override func viewWillDisappear(_ animated: Bool) {
    ...

    sessionManager.remove(self)
    sessionManager.currentCastSession?.remoteMediaClient?.remove(self)
    ...
    super.viewWillDisappear(animated)
  }

  func switchToLocalPlayback() {
    ...

    sessionManager.currentCastSession?.remoteMediaClient?.remove(self)

    ...
  }

  func switchToRemotePlayback() {
    ...

    sessionManager.currentCastSession?.remoteMediaClient?.add(self)

    ...
  }


  // MARK: - GCKSessionManagerListener

  func sessionManager(_: GCKSessionManager, didStart session: GCKSession) {
    print("MediaViewController: sessionManager didStartSession \(session)")
    setQueueButtonVisible(true)
    switchToRemotePlayback()
  }

  func sessionManager(_: GCKSessionManager, didResumeSession session: GCKSession) {
    print("MediaViewController: sessionManager didResumeSession \(session)")
    setQueueButtonVisible(true)
    switchToRemotePlayback()
  }

  func sessionManager(_: GCKSessionManager, didEnd _: GCKSession, withError error: Error?) {
    print("session ended with error: \(String(describing: error))")
    let message = "The Casting session has ended.\n\(String(describing: error))"
    if let window = appDelegate?.window {
      Toast.displayMessage(message, for: 3, in: window)
    }
    setQueueButtonVisible(false)
    switchToLocalPlayback()
  }

  func sessionManager(_: GCKSessionManager, didFailToStartSessionWithError error: Error?) {
    if let error = error {
      showAlert(withTitle: "Failed to start a session", message: error.localizedDescription)
    }
    setQueueButtonVisible(false)
  }

  func sessionManager(_: GCKSessionManager,
                      didFailToResumeSession _: GCKSession, withError _: Error?) {
    if let window = UIApplication.shared.delegate?.window {
      Toast.displayMessage("The Casting session could not be resumed.",
                           for: 3, in: window)
    }
    setQueueButtonVisible(false)
    switchToLocalPlayback()
  }

  ...
}

En MediaViewController, nos interesa recibir un aviso cuando nos conectemos al dispositivo de transmisión o nos desconectemos de él, de modo que podamos cambiar al reproductor local o desde este. Ten en cuenta que no solo la instancia de tu aplicación que se ejecute en tu dispositivo móvil interrumpirá la conectividad, sino que también puede hacerlo otra instancia de tu aplicación (o bien otra) que se ejecute en un dispositivo móvil diferente.

Se puede acceder a la sesión activa en ese momento como GCKCastContext.sharedInstance().sessionManager.currentCastSession. Las sesiones se crean y se eliminan automáticamente en respuesta a los gestos del usuario provenientes de los diálogos de la transmisión.

Carga de contenido multimedia

En el SDK de Cast, el GCKRemoteMediaClient proporciona un conjunto de APIs convenientes para administrar la reproducción de contenido multimedia remoto en la app receptora. Para una GCKCastSession compatible con la reproducción de contenido multimedia, el SDK creará automáticamente una instancia de GCKRemoteMediaClient. Se puede acceder a ella como la propiedad remoteMediaClient de la instancia GCKCastSession.

Agrega el siguiente código a MediaViewController.swift para cargar el video que se encuentra seleccionado en la app receptora:

@objc(MediaViewController)
class MediaViewController: UIViewController, GCKSessionManagerListener,
  GCKRemoteMediaClientListener, LocalPlayerViewDelegate, GCKRequestDelegate {
  ...

  @objc func playSelectedItemRemotely() {
    loadSelectedItem(byAppending: false)
  }

  /**
   * Loads the currently selected item in the current cast media session.
   * @param appending If YES, the item is appended to the current queue if there
   * is one. If NO, or if
   * there is no queue, a new queue containing only the selected item is created.
   */
  func loadSelectedItem(byAppending appending: Bool) {
    print("enqueue item \(String(describing: mediaInfo))")
    if let remoteMediaClient = sessionManager.currentCastSession?.remoteMediaClient {
      let mediaQueueItemBuilder = GCKMediaQueueItemBuilder()
      mediaQueueItemBuilder.mediaInformation = mediaInfo
      mediaQueueItemBuilder.autoplay = true
      mediaQueueItemBuilder.preloadTime = TimeInterval(UserDefaults.standard.integer(forKey: kPrefPreloadTime))
      let mediaQueueItem = mediaQueueItemBuilder.build()
      if appending {
        let request = remoteMediaClient.queueInsert(mediaQueueItem, beforeItemWithID: kGCKMediaQueueInvalidItemID)
        request.delegate = self
      } else {
        let queueDataBuilder = GCKMediaQueueDataBuilder(queueType: .generic)
        queueDataBuilder.items = [mediaQueueItem]
        queueDataBuilder.repeatMode = remoteMediaClient.mediaStatus?.queueRepeatMode ?? .off

        let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
        mediaLoadRequestDataBuilder.mediaInformation = mediaInfo
        mediaLoadRequestDataBuilder.queueData = queueDataBuilder.build()

        let request = remoteMediaClient.loadMedia(with: mediaLoadRequestDataBuilder.build())
        request.delegate = self
      }
    }
  }
  ...
}

A continuación, actualiza varios métodos existentes para usar la lógica de la sesión de transmisión y admitir la reproducción remota:

required init?(coder: NSCoder) {
  super.init(coder: coder)
  ...
  castMediaController = GCKUIMediaController()
  ...
}

func switchToLocalPlayback() {
  print("switchToLocalPlayback")
  if playbackMode == .local {
    return
  }
  setQueueButtonVisible(false)
  var playPosition: TimeInterval = 0
  var paused: Bool = false
  var ended: Bool = false
  if playbackMode == .remote {
    playPosition = castMediaController.lastKnownStreamPosition
    paused = (castMediaController.lastKnownPlayerState == .paused)
    ended = (castMediaController.lastKnownPlayerState == .idle)
    print("last player state: \(castMediaController.lastKnownPlayerState), ended: \(ended)")
  }
  populateMediaInfo((!paused && !ended), playPosition: playPosition)
  sessionManager.currentCastSession?.remoteMediaClient?.remove(self)
  playbackMode = .local
}

func switchToRemotePlayback() {
  print("switchToRemotePlayback; mediaInfo is \(String(describing: mediaInfo))")
  if playbackMode == .remote {
    return
  }
  // If we were playing locally, load the local media on the remote player
  if playbackMode == .local, (_localPlayerView.playerState != .stopped), (mediaInfo != nil) {
    print("loading media: \(String(describing: mediaInfo))")
    let paused: Bool = (_localPlayerView.playerState == .paused)
    let mediaQueueItemBuilder = GCKMediaQueueItemBuilder()
    mediaQueueItemBuilder.mediaInformation = mediaInfo
    mediaQueueItemBuilder.autoplay = !paused
    mediaQueueItemBuilder.preloadTime = TimeInterval(UserDefaults.standard.integer(forKey: kPrefPreloadTime))
    mediaQueueItemBuilder.startTime = _localPlayerView.streamPosition ?? 0
    let mediaQueueItem = mediaQueueItemBuilder.build()

    let queueDataBuilder = GCKMediaQueueDataBuilder(queueType: .generic)
    queueDataBuilder.items = [mediaQueueItem]
    queueDataBuilder.repeatMode = .off

    let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
    mediaLoadRequestDataBuilder.queueData = queueDataBuilder.build()

    let request = sessionManager.currentCastSession?.remoteMediaClient?.loadMedia(with: mediaLoadRequestDataBuilder.build())
    request?.delegate = self
  }
  _localPlayerView.stop()
  _localPlayerView.showSplashScreen()
  setQueueButtonVisible(true)
  sessionManager.currentCastSession?.remoteMediaClient?.add(self)
  playbackMode = .remote
}

/* Play has been pressed in the LocalPlayerView. */
func continueAfterPlayButtonClicked() -> Bool {
  let hasConnectedCastSession = sessionManager.hasConnectedCastSession
  if mediaInfo != nil, hasConnectedCastSession() {
    // Display an alert box to allow the user to add to queue or play
    // immediately.
    if actionSheet == nil {
      actionSheet = ActionSheet(title: "Play Item", message: "Select an action", cancelButtonText: "Cancel")
      actionSheet?.addAction(withTitle: "Play Now", target: self,
                             selector: #selector(playSelectedItemRemotely))
    }
    actionSheet?.present(in: self, sourceView: _localPlayerView)
    return false
  }
  return true
}

Ahora, ejecuta la app en tu dispositivo móvil. Conéctate a tu dispositivo de transmisión y comienza a reproducir un video. Deberías ver que el video se reproduce en el receptor.

7. Minicontrolador

La lista de tareas para el diseño de Cast requiere que todas las apps de transmisión proporcionen un minicontrolador que aparezca cuando el usuario salga de la página de contenido actual. El minicontrolador proporcionará acceso instantáneo y un recordatorio visible para la sesión de transmisión en curso.

Ilustración de la parte inferior de un iPhone que ejecuta la app CastVideos, con énfasis en el minicontrolador

El SDK de Cast proporciona una barra de control, GCKUIMiniMediaControlsViewController, que se puede agregar a las escenas en las que quieres mostrar los controles persistentes.

Para la app de ejemplo, usaremos el elemento GCKUICastContainerViewController, que une otro controlador de vistas y agrega un elemento GCKUIMiniMediaControlsViewController en la parte inferior.

Modifica el archivo AppDelegate.swift y agrega el siguiente código para la condición if useCastContainerViewController en el siguiente método:

func application(_: UIApplication,
                 didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  ...
  let appStoryboard = UIStoryboard(name: "Main", bundle: nil)
  guard let navigationController = appStoryboard.instantiateViewController(withIdentifier: "MainNavigation")
    as? UINavigationController else { return false }
  let castContainerVC = GCKCastContext.sharedInstance().createCastContainerController(for: navigationController)
    as GCKUICastContainerViewController
  castContainerVC.miniMediaControlsItemEnabled = true
  window = UIWindow(frame: UIScreen.main.bounds)
  window?.rootViewController = castContainerVC
  window?.makeKeyAndVisible()
  ...
}

Agrega esta propiedad y un método set/get para controlar la visibilidad del minicontrolador (los usaremos en una sección posterior):

var isCastControlBarsEnabled: Bool {
    get {
      if useCastContainerViewController {
        let castContainerVC = (window?.rootViewController as? GCKUICastContainerViewController)
        return castContainerVC!.miniMediaControlsItemEnabled
      } else {
        let rootContainerVC = (window?.rootViewController as? RootContainerViewController)
        return rootContainerVC!.miniMediaControlsViewEnabled
      }
    }
    set(notificationsEnabled) {
      if useCastContainerViewController {
        var castContainerVC: GCKUICastContainerViewController?
        castContainerVC = (window?.rootViewController as? GCKUICastContainerViewController)
        castContainerVC?.miniMediaControlsItemEnabled = notificationsEnabled
      } else {
        var rootContainerVC: RootContainerViewController?
        rootContainerVC = (window?.rootViewController as? RootContainerViewController)
        rootContainerVC?.miniMediaControlsViewEnabled = notificationsEnabled
      }
    }
  }

Ejecuta la app y transmite un video. Cuando se inicie la reproducción en el receptor, deberías ver que el minicontrol aparece en la parte inferior de cada escena. Podrás controlar la reproducción remota con el minicontrolador. Si navegas entre la actividad de navegación y la actividad del reproductor local, el estado del minicontrolador debería mantenerse sincronizado con el estado de reproducción de contenido multimedia del receptor.

8. Superposición introductoria

La lista de tareas de diseño de Google Cast requiere que las apps emisoras presenten el botón para transmitir a los usuarios existentes a fin de informarles que la app emisora ahora admite la transmisión y también ayuda a los usuarios nuevos de Google Cast.

Ilustración de un iPhone que ejecuta la app CastVideos con el botón para transmitir superpuesto, en el que se destaca el botón para transmitir y se muestra el mensaje "Toca para transmitir contenido multimedia a tu TV y tus bocinas"

La clase GCKCastContext tiene un método, presentCastInstructionsViewControllerOnce, que se puede usar a fin de destacar el botón para transmitir cuando se muestra por primera vez a los usuarios. Agrega el siguiente código a MediaViewController.swift y MediaTableViewController.swift:

override func viewDidLoad() {
  ...

  NotificationCenter.default.addObserver(self, selector: #selector(castDeviceDidChange),
                                         name: NSNotification.Name.gckCastStateDidChange,
                                         object: GCKCastContext.sharedInstance())
}

@objc func castDeviceDidChange(_: Notification) {
  if GCKCastContext.sharedInstance().castState != .noDevicesAvailable {
    // You can present the instructions on how to use Google Cast on
    // the first time the user uses you app
    GCKCastContext.sharedInstance().presentCastInstructionsViewControllerOnce(with: castButton)
  }
}

Ejecuta la app en tu dispositivo móvil. Deberías ver la superposición introductoria.

9. Control expandido

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

Ilustración de un iPhone que ejecuta la app CastVideos y reproduce un video, con el control expandido que aparece en la parte inferior

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

La clase GCKUIExpandedMediaControlsViewController implementa la funcionalidad de esta vista.

Lo primero que debes hacer es habilitar el control expandido predeterminado en el contexto de transmisión. Modifica AppDelegate.swift para habilitar el control expandido predeterminado:

import GoogleCast

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  ...

  func application(_: UIApplication,
                   didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    ...
    // Add after the setShareInstanceWith(options) is set.
    GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true
    ...
  }
  ...
}

Agrega el siguiente código a MediaViewController.swift para cargar el control expandido cuando el usuario comience a transmitir un video:

@objc func playSelectedItemRemotely() {
  ...
  appDelegate?.isCastControlBarsEnabled = false
  GCKCastContext.sharedInstance().presentDefaultExpandedMediaControls()
}

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

Ejecuta la app y transmite un video. Deberías ver el control expandido. Vuelve a la lista de videos. Cuando hagas clic en el minicontrolador, se volverá a cargar el control expandido.

10. Cómo agregar compatibilidad con Cast Connect

La biblioteca de Cast Connect permite que las aplicaciones emisoras existentes se comuniquen con aplicaciones de Android TV a través del protocolo de transmisión. Cast Connect se basa en la infraestructura de Cast, y tu app de Android TV actúa como receptor.

Dependencias

En tu Podfile, asegúrate de que google-cast-sdk apunte a 4.4.8 o un nivel superior, como se indica a continuación. Si modificaste el archivo, ejecuta pod update desde la consola para sincronizar el cambio con tu proyecto.

pod 'google-cast-sdk', '>=4.4.8'

GCKLaunchOptions

Para iniciar la aplicación de Android TV, también conocida como receptor de Android, debemos establecer la marca androidReceiverCompatible como verdadera en el objeto GCKLaunchOptions. Este objeto GCKLaunchOptions determina cómo se inicia el receptor y se pasa a GCKCastOptions, que se establecen en la instancia compartida mediante GCKCastContext.setSharedInstanceWith.

Agrega las siguientes líneas a tu AppDelegate.swift:

let options = GCKCastOptions(discoveryCriteria:
                          GCKDiscoveryCriteria(applicationID: kReceiverAppID))
...
/** Following code enables CastConnect */
let launchOptions = GCKLaunchOptions()
launchOptions.androidReceiverCompatible = true
options.launchOptions = launchOptions

GCKCastContext.setSharedInstanceWith(options)

Establece credenciales de inicio

Del lado del remitente, puedes especificar GCKCredentialsData para que represente quién se une a la sesión. credentials es una cadena que puede definir el usuario, siempre que tu app de ATV pueda comprenderla. El GCKCredentialsData solo se pasa a tu app de Android TV durante el inicio o la hora de unirse a la app. Si la vuelves a configurar mientras tienes conexión, no se pasará a la app de Android TV.

Para establecer las credenciales de lanzamiento, se debe definir GCKCredentialsData en cualquier momento después de establecer GCKLaunchOptions. Para demostrar esto, agreguemos lógica al botón Creds para establecer las credenciales que se pasarán cuando se establezca la sesión. Agrega el siguiente código a tu MediaTableViewController.swift:

class MediaTableViewController: UITableViewController, GCKSessionManagerListener, MediaListModelDelegate, GCKRequestDelegate {
  ...
  private var credentials: String? = nil
  ...
  override func viewDidLoad() {
    ...
    navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Creds", style: .plain,
                                                       target: self, action: #selector(toggleLaunchCreds))
    ...
    setLaunchCreds()
  }
  ...
  @objc func toggleLaunchCreds(_: Any){
    if (credentials == nil) {
        credentials = "{\"userId\":\"id123\"}"
    } else {
        credentials = nil
    }
    Toast.displayMessage("Launch Credentials: "+(credentials ?? "Null"), for: 3, in: appDelegate?.window)
    print("Credentials set: "+(credentials ?? "Null"))
    setLaunchCreds()
  }
  ...
  func setLaunchCreds() {
    GCKCastContext.sharedInstance()
        .setLaunch(GCKCredentialsData(credentials: credentials))
  }
}

Configurar credenciales en una solicitud de carga

Para controlar credentials en tus apps receptoras de Android TV y la Web, agrega el siguiente código en tu clase MediaTableViewController.swift, en la función loadSelectedItem:

let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
...
mediaLoadRequestDataBuilder.credentials = credentials
...

Según la app receptora a la que el remitente esté transmitiendo, el SDK aplicará automáticamente las credenciales anteriores a la sesión en curso.

Cómo probar Cast Connect

Pasos para instalar el APK de Android TV en Chromecast con Google TV

  1. Busca la dirección IP del dispositivo Android TV. Por lo general, está disponible en Configuración > Internet y redes > (Nombre de la red a la que está conectado tu dispositivo). A la derecha, mostrará los detalles y la IP de tu dispositivo en la red.
  2. Usa la dirección IP de tu dispositivo para conectarte a ella a través de ADB con el terminal:
$ adb connect <device_ip_address>:5555
  1. Desde la ventana de la terminal, navega a la carpeta de nivel superior de las muestras del codelab que descargaste al comienzo. Por ejemplo:
$ cd Desktop/ios_codelab_src
  1. Instala el archivo .apk de esta carpeta en tu Android TV. Para ello, ejecuta el siguiente comando:
$ adb -s <device_ip_address>:5555 install android-tv-app.apk
  1. Ahora deberías poder ver una app con el nombre Transmitir videos en el menú Tus apps del dispositivo Android TV.
  2. Cuando termines, compila y ejecuta la app en un emulador o dispositivo móvil. Cuando establezcas una sesión de transmisión con tu dispositivo Android TV, debería iniciarse la aplicación receptora de Android en tu Android TV. Cuando reproduces un video desde una emisora de dispositivos móviles iOS, debes iniciarlo en la app receptora de Android y permitirte controlar la reproducción con el control remoto de tu dispositivo Android TV.

11. Personaliza los widgets de Cast

Inicialización

Comienza con la carpeta App-Done. Agrega lo siguiente al método applicationDidFinishLaunchingWithOptions en tu archivo AppDelegate.swift.

func application(_: UIApplication,
                 didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  ...
  let styler = GCKUIStyle.sharedInstance()
  ...
}

Una vez que termines de aplicar una o más personalizaciones, como se menciona en el resto de este codelab, llama al código que aparece a continuación para confirmar los estilos

styler.apply()

Personaliza las vistas de transmisión

Puedes personalizar todas las vistas que administra el framework de aplicaciones de Cast al tener lineamientos de diseño predeterminados en todas ellas. A modo de ejemplo, cambiemos el color de tono del ícono.

styler.castViews.iconTintColor = .lightGray

Si es necesario, puedes anular los valores predeterminados en cada pantalla. Por ejemplo, a fin de anular el gris claro (lightGrayColor) para el tono de color del ícono solo para el control multimedia expandido.

styler.castViews.mediaControl.expandedController.iconTintColor = .green

Cambia el color

Puedes personalizar el color de fondo de todas las vistas (o de forma individual para cada vista). El siguiente código establece el color de fondo en azul para todas las vistas del framework de aplicaciones de Cast.

styler.castViews.backgroundColor = .blue
styler.castViews.mediaControl.miniController.backgroundColor = .yellow

Cómo cambiar las fuentes

Puedes personalizar las fuentes de diferentes etiquetas que se ven en las vistas de transmisión. Para fines ilustrativos, configuremos todas las fuentes en "Courier-Oblique".

styler.castViews.headingTextFont = UIFont.init(name: "Courier-Oblique", size: 16) ?? UIFont.systemFont(ofSize: 16)
styler.castViews.mediaControl.headingTextFont = UIFont.init(name: "Courier-Oblique", size: 6) ?? UIFont.systemFont(ofSize: 6)

Cambia las imágenes de botones predeterminadas

Agrega tus propias imágenes personalizadas al proyecto y asígnalas a tus botones para darles estilo.

let muteOnImage = UIImage.init(named: "yourImage.png")
if let muteOnImage = muteOnImage {
  styler.castViews.muteOnImage = muteOnImage
}

Cómo cambiar el tema del botón para transmitir

También puedes aplicar un tema a los widgets de Cast mediante el protocolo UIAppearance. En el siguiente código, se aplica un tema al GCKUICastButton en todas las vistas en las que aparece:

GCKUICastButton.appearance().tintColor = UIColor.gray

12. Felicitaciones

Ahora sabes cómo hacer que una app de video sea compatible con Cast mediante los widgets del SDK de Cast en iOS.

Para obtener más detalles, consulta la guía para desarrolladores de remitente de iOS.