En esta guía para desarrolladores, se describe cómo agregar compatibilidad con Google Cast a tu app de remitente de iOS mediante el SDK de Sendero de iOS.
El dispositivo móvil o la laptop es el remitente que controla la reproducción, y el dispositivo Google Cast es el receptor que muestra el contenido en la TV.
El framework del remitente hace referencia al objeto binario de la biblioteca de la clase Cast y los recursos asociados presentes en el tiempo de ejecución en el remitente. La app del remitente o la app de Cast se refiere a una app que también se ejecuta en el remitente. La app del receptor web se refiere a la aplicación HTML que se ejecuta en el receptor web.
El framework del remitente usa un diseño de devolución de llamada asíncrono para informar a la app emisora sobre eventos y para hacer 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 de alto nivel típico de una app emisora para iOS:
- El framework de Cast inicia
GCKDiscoveryManager
en función de las propiedades proporcionadas enGCKCastOptions
para comenzar a buscar dispositivos. - Cuando el usuario hace clic en el botón para transmitir, el framework presenta el diálogo de transmisión con la lista de dispositivos de transmisión detectados.
- Cuando el usuario selecciona un dispositivo de transmisión, el framework intenta iniciar la app del receptor web en ese dispositivo.
- El framework invoca las devoluciones de llamada en la app emisora para confirmar que se haya iniciado la app del receptor web.
- El framework crea un canal de comunicación entre las apps del remitente y el receptor 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 remitente y el receptor web: cuando el usuario realiza acciones de la IU de remitente, el framework pasa esas solicitudes de control de contenido multimedia al receptor web, y cuando el receptor web envía actualizaciones de estado del contenido multimedia, el framework actualiza el estado de la IU del remitente.
- Cuando el usuario haga clic en el botón para transmitir a fin de desconectarse del dispositivo de transmisión, el framework desconectará la app emisora del receptor web.
Para solucionar problemas relacionados con el remitente, debes habilitar el registro.
Para obtener una lista completa de todas las clases, los métodos y los eventos del framework de Google Cast para iOS, consulta la Referencia de la API de Google Cast para iOS. En las siguientes secciones, se describen los pasos para integrar Cast en tu app para iOS.
Métodos de llamada desde el subproceso principal
Cómo inicializar el contexto de transmisión
El framework de Cast tiene un objeto singleton global, el GCKCastContext
, que coordina todas las actividades del framework. Este objeto debe inicializarse 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 pueda activarse correctamente.
Se debe proporcionar un objeto GCKCastOptions
cuando se inicializa GCKCastContext
.
Esta clase contiene opciones que afectan el comportamiento del framework. El más importante de estos es el ID de aplicación del receptor web, que se usa para filtrar los resultados de descubrimiento y para iniciar la app del 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.
Pueden ser útiles para depurar y solucionar problemas.
@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) } } }
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
Los 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 transmisión:
Superposición introductoria: La clase
GCKCastContext
tiene un método,presentCastInstructionsViewControllerOnceWithCastButton
, que se puede usar para destacar el botón para transmitir la primera vez que haya un receptor web 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 remitente de Cast 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, aparecerá un diálogo de permisos para que el usuario pueda otorgar a la app acceso a la red local a los dispositivos de la red. Luego, cuando el usuario presione el botón para transmitir, se mostrará un diálogo de transmisión en el que se enumeran los dispositivos detectados. 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 hay dispositivos disponibles, se mostrará una pantalla en la que se le brindará información sobre los motivos por los que no se encuentran los dispositivos y cómo solucionar el problema.
Minicontrol: Cuando el usuario transmite contenido y salió de la página de contenido actual o del control expandido a otra pantalla en la app emisora, el minicontrol se muestra en la parte inferior de la pantalla para permitirle ver los metadatos del contenido multimedia que se están transmitiendo y controlar la reproducción.
Control expandido: Cuando el usuario transmite contenido y hace clic en la notificación multimedia o el minicontrol, se inicia el control 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 UIButton
. Se puede agregar a la barra de título de la app uniéndola a una UIBarButtonItem
. Una subclase UIViewController
típica puede instalar un botón para transmitir de la siguiente manera:
let castButton = GCKUICastButton(frame: CGRect(x: 0, y: 0, width: 24, height: 24)) castButton.tintColor = UIColor.gray navigationItem.rightBarButtonItem = UIBarButtonItem(customView: castButton)
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 transmisión que proporciona el framework.
GCKUICastButton
también se puede agregar directamente al guion gráfico.
Cómo configurar la detección de dispositivos
En el framework, la detección de dispositivos ocurre automáticamente. No es necesario iniciar ni detener de forma explícita el proceso de descubrimiento, a menos que implementes una IU personalizada.
La clase GCKDiscoveryManager
, que es una propiedad de GCKCastContext
, administra el descubrimiento en el framework. El framework proporciona un componente de diálogo de Cast predeterminado para la selección y el control de dispositivos. La lista de dispositivos está ordenada de forma lexicográfica según el nombre descriptivo.
Cómo funciona la administración de sesiones
El SDK de Cast presenta el concepto de una sesión de transmisión, cuyo establecimiento combina los pasos para conectarse a un dispositivo, iniciar (o unirse) una app receptora web, conectarse a ella e 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 transmisión y el ciclo de vida del receptor web.
La clase GCKSessionManager
, que es una propiedad de GCKCastContext
, administra las sesiones.
Las sesiones individuales se representan mediante subclases de la clase GCKSession
; por ejemplo, GCKCastSession
representa sesiones con dispositivos de transmisión. Puedes acceder a la sesión de transmisión activa actualmente (si la hay), como la propiedad currentCastSession
de GCKSessionManager
.
Se puede usar la interfaz de GCKSessionManagerListener
para supervisar los eventos de sesión, como la creación, la suspensión, la reanudación y la finalización de una sesión. El framework suspende automáticamente las sesiones cuando la app emisora pasa a segundo plano y trata de reanudarlas cuando la app vuelve al primer plano (o se reinicia después de una cancelación anormal o abrupta de la app mientras una sesión estaba activa).
Si se usa el diálogo de transmisión, las sesiones se crean y borran automáticamente en respuesta a los gestos del usuario. De lo contrario, la app puede iniciar y finalizar sesiones de manera explícita a través de los métodos de 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 la finalización de una sesión, entre otros.
Transferencia de transmisión
La preservación del estado de la sesión es la base de la transferencia de transmisiones, en la que los usuarios pueden transferir transmisiones de audio y video existentes entre dispositivos mediante 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 de transmisión con el firmware más reciente puede funcionar como fuentes o destinos 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 límite sutiles, como los siguientes:
- Cómo recuperarse de una pérdida temporal de Wi-Fi
- Cómo recuperarse del dispositivo
- Cómo recuperarse de la app en segundo plano
- Cómo recuperar el estado si la app falló
Cómo funciona el control multimedia
Si se establece una sesión de transmisión con una app del receptor web que admite el espacio de nombres multimedia, el framework creará automáticamente una instancia de GCKRemoteMediaClient
. Se puede acceder a ella como la propiedad remoteMediaClient
de la instancia GCKCastSession
.
Todos los métodos de GCKRemoteMediaClient
que envían solicitudes al receptor web mostrarán un objeto GCKRequest
que se puede usar para hacer un seguimiento de esa solicitud. Se puede asignar una 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 que, de hecho, algunos componentes internos del framework, como el diálogo de transmisión y los minicontroles multimedia, compartan la instancia. Para ello, GCKRemoteMediaClient
admite el registro de varios GCKRemoteMediaClientListener
.
Configura metadatos de contenido multimedia
La clase GCKMediaMetadata
representa información sobre un elemento multimedia que deseas transmitir. En el siguiente ejemplo, se crea una nueva instancia de GCKMediaMetadata
de una película y se establece el título, el subtítulo, el nombre del estudio de grabación y dos imágenes.
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))
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 de imágenes y almacenamiento en caché para obtener información sobre el uso de imágenes con metadatos de contenido multimedia.
Cargar 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 receptora. Luego, puedes usar GCKRemoteMediaClient
para controlar una app de reproducción multimedia que se ejecuta en la app receptora, por ejemplo, para reproducir, pausar y detener contenido.
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 }
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 qué formato de video es 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
.
Agrega 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 salga de la página de contenido actual. El minicontrol 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 quieres mostrar el minicontrol.
Cuando tu app emisora reproduce una transmisión en vivo de audio o video, el SDK muestra automáticamente un botón de reproducción/detención en lugar del botón de reproducción/pausa en el minicontrol.
Consulta Cómo personalizar la IU de remitente de iOS para saber cómo tu app emisora puede configurar la apariencia de los widgets de Cast.
Hay dos formas de agregar el minicontrolador a una app emisora:
- Deja que el framework de Cast administre el diseño del minicontrolador uniendo tu controlador de vistas existente con su propio controlador de vistas.
- Administra por tu cuenta el diseño del widget del minicontrolador. Para ello, agrégalo a tu controlador de vistas existente y proporciona una subvista en el guion gráfico.
Une con GCKUICastContainerViewController
La primera es usar el GCKUICastContainerViewController
, que se une a otro controlador de vista y agrega un GCKUIMiniMediaControlsViewController
en la parte inferior. Este enfoque se ve limitado en el sentido de 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:
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() ... }
- (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]; ... }
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 } } }
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
Cómo incorporar en el controlador de vistas existente
La segunda forma es agregar el minicontrolador directamente a tu controlador de vista existente usando createMiniMediaControlsViewController
para crear una instancia GCKUIMiniMediaControlsViewController
y, luego, agregarla al controlador de vista del contenedor como una subvista.
Configura el controlador de vistas en el delegado de la app:
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 }
- (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 subvista:
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) } } ...
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
El objeto GCKUIMiniMediaControlsViewControllerDelegate
le indica al controlador de vista del host cuándo debe estar visible el minicontrolador:
func miniMediaControlsViewController(_: GCKUIMiniMediaControlsViewController, shouldAppear _: Bool) { updateControlBarsVisibility() }
- (void)miniMediaControlsViewController: (GCKUIMiniMediaControlsViewController *)miniMediaControlsViewController shouldAppear:(BOOL)shouldAppear { [self updateControlBarsVisibility]; }
Agregar control expandido
La lista de tareas de diseño de Google Cast requiere que una app emisora proporcione un control expandido para el contenido multimedia que se transmite. El control expandido es una versión de pantalla completa del minicontrol.
El control 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 manejables de una sesión de transmisión, excepto el control de volumen del receptor web y el ciclo de vida de la sesión (conectar/detener transmisión). También proporciona toda la información de estado de la sesión multimedia (arte gráfico, título y subtítulo, entre otros).
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 el delegado de la app para habilitar el control expandido predeterminado:
func applicationDidFinishLaunching(_ application: UIApplication) { .. GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true ... }
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { ... [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES; .. }
Agrega el siguiente código a tu controlador de vista para cargar el control expandido cuando el usuario comience a transmitir un video:
func playSelectedItemRemotely() { GCKCastContext.sharedInstance().presentDefaultExpandedMediaControls() ... // Load your media sessionManager.currentSession?.remoteMediaClient?.loadMedia(mediaInformation) }
- (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 minicontrol.
Cuando tu app emisora reproduce una transmisión en vivo de audio o video, 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 a fin de obtener información sobre cómo tu app emisora puede configurar la apariencia de los widgets de Cast.
Control de volumen
El framework de Cast administra automáticamente el volumen de la app emisora. El framework se sincroniza automáticamente con el volumen del receptor web para los widgets de la IU proporcionados. Para sincronizar un control deslizante que proporciona la app, usa GCKUIDeviceVolumeController
.
Control de volumen del botón físico
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
de GCKCastOptions
, que se establece en GCKCastContext
.
let criteria = GCKDiscoveryCriteria(applicationID: kReceiverAppID) let options = GCKCastOptions(discoveryCriteria: criteria) options.physicalVolumeButtonsWillControlDeviceVolume = true GCKCastContext.setSharedInstanceWith(options)
GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc] initWithApplicationID:kReceiverAppID]; GCKCastOptions *options = [[GCKCastOptions alloc] initWithDiscoveryCriteria :criteria]; options.physicalVolumeButtonsWillControlDeviceVolume = YES; [GCKCastContext setSharedInstanceWithOptions:options];
Cómo solucionar errores
Es muy importante que las apps emisoras manejen todas las devoluciones de llamada de errores y decidan la mejor respuesta para cada etapa del ciclo de vida de Cast. La app puede mostrarle diálogos de error al usuario o decidir finalizar la sesión de transmisión.
Logging
GCKLogger
es un singleton que el framework usa para el registro. Usa GCKLoggerDelegate
para personalizar la forma en que administras los mensajes de registro.
Con GCKLogger
, el SDK produce resultados del registro en forma de mensajes de depuración, errores y advertencias. Estos mensajes de registro ayudan con la depuración y son útiles para identificar y solucionar problemas. De forma predeterminada, se suprime el resultado del registro, pero, cuando se asigna un GCKLoggerDelegate
, la app emisora puede recibir estos mensajes del SDK y registrarlos en la consola del sistema.
@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) } } }
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 (que se mostró antes):
let filter = GCKLoggerFilter.init() filter.minimumLevel = GCKLoggerLevel.verbose GCKLogger.sharedInstance().filter = filter
GCKLoggerFilter *filter = [[GCKLoggerFilter alloc] init]; [filter setMinimumLevel:GCKLoggerLevelVerbose]; [GCKLogger sharedInstance].filter = filter;
También puedes filtrar los mensajes de registro producidos por GCKLogger
.
Establece el nivel de registro mínimo por clase, por ejemplo:
let filter = GCKLoggerFilter.init() filter.setLoggingLevel(GCKLoggerLevel.verbose, forClasses: ["GCKUICastButton", "GCKUIImageCache", "NSMutableDictionary"]) GCKLogger.sharedInstance().filter = filter
GCKLoggerFilter *filter = [[GCKLoggerFilter alloc] init]; [filter setLoggingLevel:GCKLoggerLevelVerbose forClasses:@[@"GCKUICastButton", @"GCKUIImageCache", @"NSMutableDictionary" ]]; [GCKLogger sharedInstance].filter = filter;
Los nombres de clase pueden ser nombres literales o patrones glob, por ejemplo, GCKUI\*
y GCK\*Session
.