Integrar o Google Cast ao seu app iOS

Neste guia do desenvolvedor, descrevemos como adicionar a compatibilidade com o Google Cast ao seu app remetente do iOS usando o SDK do remetente do iOS.

O dispositivo móvel ou laptop é o remetente que controla a reprodução, e o dispositivo com Google Cast é o receptor, que exibe o conteúdo na TV.

O framework do remetente se refere ao binário da biblioteca da classe Cast e aos recursos associados presentes no momento da execução no remetente. O app remetente ou o app Cast refere-se a um app que também está sendo executado no remetente. O app receptor da Web se refere ao aplicativo HTML em execução no receptor da Web.

O framework do remetente usa um design de callback assíncrono para informar o app remetente sobre eventos e fazer a transição entre vários estados do ciclo de vida do app Cast.

Fluxo de aplicativos

As etapas a seguir descrevem o fluxo de execução típico de alto nível para um app iOS do remetente:

  • O framework do Google Cast inicia GCKDiscoveryManager com base nas propriedades fornecidas em GCKCastOptions para iniciar a verificação para dispositivos.
  • Quando o usuário clica no botão "Transmitir", o framework apresenta a caixa de diálogo "Transmitir" com a lista de dispositivos de transmissão descobertos.
  • Quando o usuário seleciona um dispositivo de transmissão, o framework tenta iniciar o app receptor da Web no dispositivo de transmissão.
  • O framework invoca callbacks no app remetente para confirmar se o app Web Receiver foi iniciado.
  • O framework cria um canal de comunicação entre o remetente e os apps receptores da Web.
  • O framework usa o canal de comunicação para carregar e controlar a reprodução de mídia no receptor da Web.
  • O framework sincroniza o estado de reprodução de mídia entre o remetente e o receptor da Web: quando o usuário realiza ações da interface do remetente, o framework transmite essas solicitações de controle de mídia para o receptor da Web. Quando o receptor da Web envia atualizações de status da mídia, o framework atualiza o estado da interface do remetente.
  • Quando o usuário clica no botão "Transmitir" para se desconectar do dispositivo de transmissão, o framework desconecta o app remetente do receptor da Web.

Para resolver problemas com o remetente, ative o registro.

Para ver uma lista abrangente de todas as classes, métodos e eventos no framework do Google Cast para iOS, consulte a Referência da API Google Cast para iOS. As seções a seguir abordam as etapas para integrar o Google Cast ao seu app iOS.

Métodos de chamada da linha de execução principal

Inicializar o contexto de transmissão

O framework do Google Cast tem um objeto Singleton global, o GCKCastContext, que coordena todas as atividades do framework. Esse objeto precisa ser inicializado no início do ciclo de vida do aplicativo, normalmente no método -[application:didFinishLaunchingWithOptions:] do delegado do app, para que a retomada automática da sessão na reinicialização do app do remetente possa ser acionada corretamente.

Um objeto GCKCastOptions precisa ser fornecido ao inicializar o GCKCastContext. Essa classe contém opções que afetam o comportamento do framework. A mais importante delas é o ID do aplicativo receptor da Web, que é usado para filtrar os resultados de descoberta e iniciar esse app quando uma sessão do Google Cast é iniciada.

O método -[application:didFinishLaunchingWithOptions:] também é um bom lugar para configurar um delegado de geração de registros para receber as mensagens de geração de registros do framework. Eles podem ser úteis para depuração e solução de 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

Os widgets de UX do Cast

O SDK do Cast para iOS oferece estes widgets que obedecem à lista de verificação de design do Google Cast:

  • Sobreposição introdutória: a classe GCKCastContext tem um método, presentCastInstructionsViewControllerOnceWithCastButton, que pode ser usado para destacar o botão Transmitir na primeira vez que um Web Receiver está disponível. O app remetente pode personalizar o texto, a posição do texto do título e o botão "Dispensar".

  • Botão Transmitir: a partir do SDK de transmissão do Google Cast para iOS 4.6.0, o botão de transmissão estará sempre visível quando o dispositivo remetente estiver conectado ao Wi-Fi. Na primeira vez que o usuário toca no botão "Transmitir" após iniciar o app, uma caixa de diálogo de permissões é mostrada para que o usuário possa conceder ao app acesso à rede local aos dispositivos na rede. Em seguida, quando o usuário toca no botão Transmitir, uma caixa de diálogo de transmissão é exibida e lista os dispositivos descobertos. Quando o usuário toca no botão "Transmitir" enquanto o dispositivo está conectado, os metadados de mídia atuais são mostrados, como título, nome do estúdio de gravação e uma imagem em miniatura, ou permite que o usuário se desconecte do dispositivo de transmissão. Quando o usuário toca no botão de transmissão enquanto não há dispositivos disponíveis, uma tela é exibida com informações sobre por que os dispositivos não foram encontrados e como resolver problemas.

  • Minicontrolador: quando o usuário está transmitindo conteúdo e saiu da página de conteúdo atual ou do controle expandido para outra tela no app remetente, o minicontrole aparece na parte de baixo da tela para permitir que o usuário veja os metadados de mídia transmitidos no momento e controle a reprodução.

  • Controle expandido: quando o usuário estiver transmitindo conteúdo, se ele clicar na notificação de mídia ou no minicontrole, o controle expandido será iniciado, mostrando os metadados de mídia em reprodução no momento e fornecendo vários botões para controlar a reprodução da mídia.

Adicionar um botão Transmitir

O framework fornece um componente do botão Transmitir como uma subclasse de UIButton. Ele pode ser adicionado à barra de título do app colocando-o em um UIBarButtonItem. Uma subclasse UIViewController típica pode instalar um botão Transmitir da seguinte maneira:

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];

Por padrão, tocar no botão abrirá a caixa de diálogo "Transmitir" fornecida pelo framework.

Também é possível adicionar GCKUICastButton diretamente ao storyboard.

Configurar a descoberta de dispositivos

No framework, a descoberta de dispositivos acontece automaticamente. Não é necessário iniciar ou interromper explicitamente o processo de descoberta, a menos que você implemente uma interface personalizada.

A descoberta no framework é gerenciada pela classe GCKDiscoveryManager, que é uma propriedade de GCKCastContext. O framework fornece um componente padrão da caixa de diálogo do Google Cast para seleção e controle de dispositivos. A lista de dispositivos é ordenada lexicograficamente pelo nome de fácil utilização para o dispositivo.

Como funciona o gerenciamento de sessões

O SDK do Cast introduz o conceito de sessão do Cast, que combina as etapas para se conectar a um dispositivo, iniciar (ou participar) de um app receptor da Web, conectar-se a esse app e inicializar um canal de controle de mídia. Consulte o Guia do ciclo de vida do aplicativo do receptor da Web para mais informações sobre as sessões de transmissão e o ciclo de vida do receptor da Web.

As sessões são gerenciadas pela classe GCKSessionManager, que é uma propriedade da GCKCastContext. As sessões individuais são representadas por subclasses da classe GCKSession: por exemplo, GCKCastSession representa sessões com dispositivos de transmissão. Você pode acessar a sessão do Google Cast ativa no momento (se houver) como a propriedade currentCastSession de GCKSessionManager.

A interface GCKSessionManagerListener pode ser usada para monitorar eventos de sessão, como criação, suspensão, retomada e encerramento. O framework suspende automaticamente as sessões quando o app remetente está entrando em segundo plano e tenta retomá-las quando o app retorna ao primeiro plano (ou é reiniciado após um encerramento anormal/abrupto do app enquanto uma sessão estava ativa).

Se a caixa de diálogo "Transmitir" estiver sendo usada, as sessões serão criadas e desativadas automaticamente em resposta a gestos do usuário. Caso contrário, o app poderá iniciar e encerrar sessões explicitamente usando métodos em GCKSessionManager.

Se o app precisar fazer um processamento especial em resposta a eventos de ciclo de vida da sessão, ele poderá registrar uma ou mais instâncias de GCKSessionManagerListener com o GCKSessionManager. O GCKSessionManagerListener é um protocolo que define callbacks para eventos como início, fim e assim por diante.

Transferência de streaming

Preservar o estado da sessão é a base da transferência de stream, em que os usuários podem mover streams de áudio e vídeo existentes entre dispositivos usando comandos de voz, o app Google Home ou smart displays. A mídia é interrompida em um dispositivo (a origem) e continua em outro (o destino). Qualquer dispositivo de transmissão com o firmware mais recente pode servir como origens ou destinos em uma transferência de stream.

Para ter o novo dispositivo de destino durante a transferência de stream, use a propriedade GCKCastSession#device durante o callback [sessionManager:didResumeCastSession:].

Consulte Transferência de stream no receptor da Web para mais informações.

Reconexão automática

O framework do Cast adiciona a lógica de reconexão para processar automaticamente a reconexão em muitos casos sutis, como:

  • Recupere-se de uma perda temporária de Wi-Fi
  • Recuperar da suspensão do dispositivo
  • Recuperar do segundo plano
  • Recuperar em caso de falha do app

Como funciona o controle de mídia

Se uma sessão do Cast for estabelecida com um app receptor da Web compatível com o namespace de mídia, uma instância de GCKRemoteMediaClient será criada automaticamente pelo framework. Ela poderá ser acessada como a propriedade remoteMediaClient da instância GCKCastSession.

Todos os métodos no GCKRemoteMediaClient que emitem solicitações ao receptor da Web retornarão um objeto GCKRequest que pode ser usado para rastrear essa solicitação. Um GCKRequestDelegate pode ser atribuído a esse objeto para receber notificações sobre o eventual resultado da operação.

Espera-se que a instância de GCKRemoteMediaClient seja compartilhada por várias partes do app. Além disso, alguns componentes internos do framework, como a caixa de diálogo do Google Cast e os controles de minimídia, compartilhem a instância. Por isso, o GCKRemoteMediaClient oferece suporte ao registro de vários GCKRemoteMediaClientListeners.

Definir metadados de mídia

A classe GCKMediaMetadata representa informações sobre um item de mídia que você quer transmitir. O exemplo a seguir cria uma nova instância GCKMediaMetadata de um filme e define o título, o subtítulo, o nome do estúdio de gravação e as duas imagens.

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]];

Consulte a seção Seleção de imagens e armazenamento em cache sobre o uso de imagens com metadados de mídia.

Carregar mídia

Para carregar um item de mídia, crie uma instância GCKMediaInformation usando os metadados da mídia. Em seguida, acesse o GCKCastSession atual e use o GCKRemoteMediaClient dele para carregar a mídia no app receptor. Você pode usar o GCKRemoteMediaClient para controlar um app de player de mídia em execução no receptor, como para reproduzir, pausar e parar.

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

Consulte também a seção sobre como usar faixas de mídia.

Formato de vídeo 4K

Para determinar qual é o formato de vídeo da sua mídia, use a propriedade videoInfo de GCKMediaStatus para acessar a instância atual de GCKVideoInfo. Essa instância contém o tipo de formato de TV HDR e a altura e largura em pixels. Variantes do formato 4K são indicadas na propriedade hdrType pelos valores de enumeração GCKVideoInfoHDRType.

Adicionar minicontroles

De acordo com a Lista de verificação de design do Cast, um app remetente precisa fornecer um controle permanente conhecido como mini controlador, que aparece quando o usuário sai da página de conteúdo atual. O minicontrole oferece acesso instantâneo e um lembrete visível para a sessão atual do Google Cast.

O framework do Google Cast oferece uma barra de controle, GCKUIMiniMediaControlsViewController, que pode ser adicionada às cenas em que você quer mostrar o minicontrole.

Quando o app remetente estiver reproduzindo uma transmissão ao vivo de vídeo ou áudio, o SDK exibe automaticamente um botão "Reproduzir/parar" no lugar do botão "Reproduzir/pausar" no minicontrole.

Consulte Personalizar a interface do remetente do iOS para saber como o app de remetente pode configurar a aparência dos widgets do Google Cast.

Há duas maneiras de adicionar o minicontrole a um app remetente:

  • Deixe que o framework do Google Cast gerencie o layout do minicontrole unindo seu controlador de visualização existente com o próprio controlador de visualização.
  • Para gerenciar o layout do widget do minicontrole por conta própria, adicione-o ao controlador de visualização atual fornecendo uma subvisualização no storyboard.

Unir usando o GCKUICastContainerViewController

A primeira é usar o GCKUICastContainerViewController, que envolve outro controlador de visualização e adiciona um GCKUIMiniMediaControlsViewController na parte de baixo. Essa abordagem é limitada, porque não é possível personalizar a animação nem configurar o comportamento do controlador de visualização do contêiner.

Essa primeira maneira normalmente é feita no método -[application:didFinishLaunchingWithOptions:] do delegado do 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

Incorporar ao controlador de vista atual

A segunda maneira é adicionar o minicontrolador diretamente ao seu controlador de visualização atual usando createMiniMediaControlsViewController para criar uma instância GCKUIMiniMediaControlsViewController e, em seguida, adicioná-la ao controlador de visualização do contêiner como uma subvisualização.

Configure o controlador de visualização no delegado do 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;
}

No controlador de visualização raiz, crie uma instância GCKUIMiniMediaControlsViewController e adicione-a ao controlador de visualização do contêiner como uma subvisualização:

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

O GCKUIMiniMediaControlsViewControllerDelegate informa ao controlador de visualização do host quando o minicontrole precisa estar visível:

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

Adicionar controle expandido

A Lista de verificação de design do Google Cast exige que um app remetente forneça um controlador expandido para a mídia transmitida. O controle expandido é uma versão em tela cheia do minicontrole.

O controle expandido é uma visualização em tela cheia que oferece controle total da reprodução de mídia remota. Essa visualização permite que um app de transmissão gerencie todos os aspectos gerenciáveis de uma sessão de transmissão, exceto o controle de volume do receptor da Web e o ciclo de vida da sessão (conectar/interromper a transmissão). Ele também fornece todas as informações de status sobre a sessão de mídia (arte, título, subtítulo e assim por diante).

A funcionalidade dessa visualização é implementada pela classe GCKUIExpandedMediaControlsViewController.

A primeira coisa que você precisa fazer é ativar o controlador expandido padrão no contexto de transmissão. Modifique o delegado do app para ativar o controlador expandido padrão:

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

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true

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

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  ..
}

Adicione o seguinte código ao seu controlador de visualização para carregar o controle expandido quando o usuário começar a transmitir um vídeo:

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

O controle expandido também será iniciado automaticamente quando o usuário tocar no minicontrole.

Quando o app remetente estiver reproduzindo uma transmissão ao vivo de vídeo ou áudio, o SDK exibe automaticamente um botão "Reproduzir/parar" no lugar do botão "Reproduzir/pausar" no controle expandido.

Consulte Aplicar estilos personalizados ao app iOS para saber como o app remetente pode configurar a aparência dos widgets do Google Cast.

Controle do volume

O framework do Google Cast gerencia automaticamente o volume do app do remetente. O framework é sincronizado automaticamente com o volume do receptor da Web para os widgets de interface fornecidos. Para sincronizar um controle deslizante fornecido pelo app, use GCKUIDeviceVolumeController.

Controle de volume do botão físico

Os botões de volume físico no dispositivo remetente podem ser usados para mudar o volume da sessão de transmissão no receptor da Web usando a flag physicalVolumeButtonsWillControlDeviceVolume no GCKCastOptions, definido em 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];

Solucionar erros

É muito importante que os apps remetentes processem todos os callbacks de erro e decidam a melhor resposta para cada estágio do ciclo de vida do Cast. O app pode exibir caixas de diálogo de erro para o usuário ou encerrar a sessão de transmissão.

Geração de registros

GCKLogger é um Singleton usado para gerar registros pelo framework. Use o GCKLoggerDelegate para personalizar como você lida com as mensagens de registro.

Usando o GCKLogger, o SDK produz a saída de geração de registros na forma de mensagens de depuração, erros e avisos. Essas mensagens de registro ajudam na depuração e são úteis para solucionar problemas e identificar problemas. Por padrão, a saída do registro é suprimida, mas, ao atribuir um GCKLoggerDelegate, o app remetente pode receber essas mensagens do SDK e registrá-las no console do 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 ativar também mensagens detalhadas e de depuração, adicione esta linha ao código depois de definir o delegado (mostrado 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;

Também é possível filtrar as mensagens de registro produzidas por GCKLogger. Defina o nível mínimo de geração de registros por classe, por exemplo:

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;

Os nomes das classes podem ser nomes literais ou padrões glob, por exemplo, GCKUI\* e GCK\*Session.