Как интегрировать Cast в приложение для iOS

В этом руководстве для разработчиков рассказывается, как добавить поддержку Google Cast в приложение отправителя для iOS с помощью iOS Sender SDK.

Мобильное устройство или ноутбук – это отправитель, который управляет воспроизведением, а устройство с поддержкой Google Cast – получатель, который показывает контент на телевизоре.

Фреймворк отправителя – это двоичный файл библиотеки классов Cast и связанные с ним ресурсы, которые присутствуют во время выполнения на устройстве отправителя. Отправляющее приложение или приложение Cast – это приложение, которое также запущено на устройстве отправителя. Приложение Web Receiver – это HTML-приложение, которое выполняется на Web Receiver.

В фреймворке отправителя используется асинхронный обратный вызов, чтобы сообщать приложению отправителя о событиях и переходить между различными состояниями жизненного цикла приложения Cast.

Путь в приложении

Ниже описан типичный алгоритм работы приложения отправителя для iOS.

  • Платформа Cast начинает работу GCKDiscoveryManager на основе свойств, указанных в GCKCastOptions, чтобы начать поиск устройств.
  • Когда пользователь нажимает кнопку трансляции, фреймворк показывает диалоговое окно с найденными устройствами Cast.
  • Когда пользователь выбирает устройство Cast, фреймворк пытается запустить на нем приложение Web Receiver.
  • Фреймворк вызывает обратные вызовы в приложении отправителя, чтобы подтвердить запуск приложения Web Receiver.
  • Фреймворк создает канал связи между отправителем и приложениями Web Receiver.
  • Фреймворк использует канал связи для загрузки и управления воспроизведением медиаконтента на веб-приемнике.
  • Фреймворк синхронизирует состояние воспроизведения медиаконтента между отправителем и веб-приемником. Когда пользователь выполняет действия в интерфейсе отправителя, фреймворк передает запросы на управление медиаконтентом веб-приемнику. Когда веб-приемник отправляет обновления статуса медиаконтента, фреймворк обновляет состояние интерфейса отправителя.
  • Когда пользователь нажимает кнопку трансляции, чтобы отключиться от устройства для трансляции [контента], фреймворк отключает приложение отправителя от веб-приемника.

Чтобы устранить неполадки с отправителем, необходимо включить регистрацию.

Полный список всех классов, методов и событий в Google Cast iOS Framework приведен в справочной документации по Google Cast iOS API. В следующих разделах описаны шаги по интеграции Cast в приложение для iOS.

Вызов методов из основного потока

Инициализация контекста Cast

В фреймворке Cast есть глобальный объект-одиночка GCKCastContext, который координирует все действия фреймворка. Этот объект необходимо инициализировать на раннем этапе жизненного цикла приложения, как правило, в методе -[application:didFinishLaunchingWithOptions:] делегата приложения, чтобы при перезапуске приложения отправителя корректно возобновлялся сеанс.

При инициализации GCKCastContext необходимо указать объект GCKCastOptions. Этот класс содержит параметры, влияющие на поведение фреймворка. Самый важный из них – идентификатор приложения Web Receiver, который используется для фильтрации результатов поиска и запуска приложения Web Receiver при начале сеанса Cast.

Метод -[application:didFinishLaunchingWithOptions:] также подходит для настройки делегата ведения журнала, который будет получать сообщения журнала от фреймворка. Они могут быть полезны при отладке и устранении неполадок.

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

Виджеты Cast UX

В Cast iOS SDK есть следующие виджеты, соответствующие контрольному списку требований к дизайну Cast:

  • Вводный оверлей. У класса GCKCastContext есть метод presentCastInstructionsViewControllerOnceWithCastButton, который можно использовать, чтобы выделить кнопку трансляции при первом запуске веб-приемника. Приложение отправителя может настроить текст, положение текста заголовка и кнопки "Отклонить".

  • Кнопка трансляции. Начиная с версии 4.6.0 Cast iOS sender SDK, кнопка трансляции всегда видна, когда устройство отправителя подключено к сети Wi-Fi. Когда пользователь впервые нажимает кнопку трансляции после запуска приложения, появляется диалоговое окно с запросом разрешения на доступ к локальной сети, чтобы приложение могло подключаться к устройствам в сети. После этого, когда пользователь нажимает на кнопку трансляции, появляется диалоговое окно с обнаруженными устройствами. Когда пользователь нажимает на кнопку трансляции, устройство показывает текущие метаданные медиаконтента (например, название, имя студии звукозаписи и значок) или позволяет отключиться от устройства для трансляции. Если пользователь нажмет на кнопку трансляции, когда доступных устройств нет, на экране появится информация о том, почему устройства не найдены и как устранить неполадки.

  • Мини-контроллер. Если пользователь транслирует контент и перешел с текущей страницы контента или развернутого контроллера на другой экран в приложении отправителя, в нижней части экрана показывается мини-контроллер. Он позволяет просматривать метаданные транслируемого контента и управлять воспроизведением.

  • Расширенный контроллер. Когда пользователь транслирует контент и нажимает на уведомление о медиаконтенте или мини-контроллер, запускается расширенный контроллер, на котором отображаются метаданные воспроизводимого контента и кнопки управления воспроизведением.

Как добавить кнопку трансляции

В фреймворке есть компонент кнопки трансляции, который является подклассом UIButton. Его можно добавить в строку заголовка приложения, заключив в тег UIBarButtonItem. Типичный подкласс UIViewController может установить кнопку трансляции следующим образом:

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

По умолчанию при нажатии на кнопку открывается диалоговое окно трансляции, предоставляемое фреймворком.

GCKUICastButton также можно добавить непосредственно в раскадровку.

Как настроить обнаружение устройств

В фреймворке обнаружение устройств происходит автоматически. Вам не нужно запускать или останавливать процесс обнаружения, если вы не используете собственный интерфейс.

Обнаружение в фреймворке управляется классом GCKDiscoveryManager, который является свойством GCKCastContext. В фреймворке есть компонент диалогового окна Cast по умолчанию для выбора устройства и управления им. Список устройств отсортирован в лексикографическом порядке по понятным названиям устройств.

Как работает управление сеансами

В Cast SDK представлено понятие сеанса Cast, который устанавливается в результате подключения к устройству, запуска (или присоединения к) приложения Web Receiver, подключения к этому приложению и инициализации канала управления медиаконтентом. Подробнее о сеансах Cast и жизненном цикле получателя можно узнать из руководства по жизненному циклу приложения получателя.

Сеансами управляет класс GCKSessionManager, который является свойством класса GCKCastContext. Отдельные сеансы представлены подклассами класса GCKSession, например GCKCastSession представляет сеансы с Cast-устройствами. Вы можете получить доступ к активному сеансу Cast (если он есть) как к свойству currentCastSession объекта GCKSessionManager.

Интерфейс GCKSessionManagerListener можно использовать для отслеживания событий сеанса, таких как создание, приостановка, возобновление и завершение сеанса. Когда приложение отправителя переходит в фоновый режим, фреймворк автоматически приостанавливает сеансы и пытается возобновить их, когда приложение возвращается в активный режим (или перезапускается после неожиданного завершения работы, когда сеанс был активен).

Если используется диалоговое окно Cast, сеансы создаются и завершаются автоматически в ответ на действия пользователя. В противном случае приложение может запускать и завершать сеансы явным образом с помощью методов в GCKSessionManager.

Если приложению требуется выполнять специальные действия в ответ на события жизненного цикла сеанса, оно может зарегистрировать один или несколько экземпляров GCKSessionManagerListener с помощью GCKSessionManager. GCKSessionManagerListener – это протокол, который определяет обратные вызовы для таких событий, как начало и конец сеанса и т. д.

Перенос трансляции

Сохранение состояния сеанса – основа передачи потока, при которой пользователи могут переключать аудио- и видеопотоки между устройствами с помощью голосовых команд, приложения Google Home или умных дисплеев. Воспроизведение медиаконтента останавливается на одном устройстве (источнике) и продолжается на другом (целевом). Любое устройство для трансляции [контента] : Cast-устройство : [*] с последней версией встроенного ПО может быть источником или получателем при переносе трансляции.

Чтобы получить новое целевое устройство во время передачи трансляции, используйте свойство GCKCastSession#device во время обратного вызова [sessionManager:didResumeCastSession:].

Подробнее о передаче потока на веб-приемнике…

Автоматическое повторное подключение

В фреймворк Cast добавлена логика повторного подключения, которая автоматически обрабатывает множество нестандартных ситуаций, например:

  • Как восстановить подключение к Wi-Fi после временного сбоя
  • Как выйти из спящего режима
  • Как восстановить работу приложения после его сворачивания
  • Восстановление после сбоя приложения

Как работает управление мультимедиа

Если сеанс Cast устанавливается с помощью веб-приложения-приемника, поддерживающего пространство имен мультимедиа, фреймворк автоматически создает экземпляр GCKRemoteMediaClient. Доступ к нему можно получить как к свойству remoteMediaClient экземпляра GCKCastSession.

Все методы в GCKRemoteMediaClient, которые отправляют запросы на веб-приемник, возвращают объект GCKRequest, который можно использовать для отслеживания запроса. Объекту можно назначить GCKRequestDelegate, чтобы получать уведомления о результатах операции.

Ожидается, что экземпляр GCKRemoteMediaClient может использоваться несколькими частями приложения, и действительно некоторые внутренние компоненты фреймворка, такие как диалоговое окно Cast и мини-медиаконтроллеры, используют один экземпляр. Для этого в GCKRemoteMediaClient можно зарегистрировать несколько GCKRemoteMediaClientListener.

Как задать метаданные медиаконтента

Класс GCKMediaMetadata содержит информацию о медиаконтенте, который вы хотите транслировать. В приведенном ниже примере создается новый экземпляр объекта GCKMediaMetadata для фильма и задаются его название, подзаголовок, название студии звукозаписи и два изображения.

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

Подробнее об использовании изображений с метаданными медиаконтента рассказывается в разделе Выбор изображений и кеширование.

Загрузка медиафайлов

Чтобы загрузить медиаконтент, создайте экземпляр GCKMediaInformation, используя метаданные медиаконтента. Затем получите текущий GCKCastSession и используйте его GCKRemoteMediaClient, чтобы загрузить медиаконтент в приложение получателя. После этого вы сможете использовать GCKRemoteMediaClient для управления медиапроигрывателем, запущенным на устройстве получателя, например для воспроизведения, приостановки и остановки.

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

Также ознакомьтесь с разделом об использовании медиадорожек.

Формат видео 4K

Чтобы определить формат видео, используйте свойство videoInfo объекта GCKMediaStatus, чтобы получить текущий экземпляр объекта GCKVideoInfo. Этот экземпляр содержит тип формата HDR TV, а также высоту и ширину в пикселях. Варианты формата 4K указываются в свойстве hdrType с помощью значений перечисления GCKVideoInfoHDRType.

Как добавить мини-контроллеры

Согласно контрольному списку дизайна Cast, приложение-отправитель должно предоставлять постоянный элемент управления, известный как мини-контроллер, который должен появляться, когда пользователь покидает страницу текущего контента. Мини-пульт позволяет быстро получить доступ к текущей сессии Cast и увидеть напоминание о ней.

В фреймворке Cast есть панель управления – GCKUIMiniMediaControlsViewController, которую можно добавить в сцены, где вы хотите показывать мини-контроллер.

Когда приложение отправителя воспроизводит видео или аудио в прямом эфире, SDK автоматически показывает кнопку "Воспроизвести/остановить" вместо кнопки "Воспроизвести/пауза" в мини-контроллере.

Информацию о том, как настроить внешний вид виджетов Cast в приложении отправителя, можно найти в разделе Настройка интерфейса отправителя для iOS.

Добавить мини-контроллер в приложение отправителя можно двумя способами:

  • Позвольте фреймворку Cast управлять макетом мини-контроллера, обернув существующий контроллер представления собственным контроллером представления.
  • Вы можете управлять макетом виджета мини-контроллера, добавив его в существующий контроллер представления, предоставив дочернее представление в раскадровке.

Как использовать GCKUICastContainerViewController

Первый способ – использовать GCKUICastContainerViewController, который оборачивает другой контроллер представления и добавляет GCKUIMiniMediaControlsViewController внизу. Этот подход ограничен тем, что вы не можете настроить анимацию и поведение контроллера представления контейнера.

Первый способ обычно реализуется в методе -[application:didFinishLaunchingWithOptions:] делегата приложения:

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

Встроить в существующий контроллер представления

Второй способ – добавить мини-контроллер непосредственно в существующий контроллер представления, используя createMiniMediaControlsViewController для создания экземпляра GCKUIMiniMediaControlsViewController, а затем добавить его в контроллер представления контейнера в качестве дочернего представления.

Настройте контроллер представления в делегате приложения:

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

В корневом контроллере представления создайте экземпляр GCKUIMiniMediaControlsViewController и добавьте его в контроллер представления контейнера как дочернее представление:

Swift
let kCastControlBarsAnimationDuration: TimeInterval = 0.20

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

  var overriddenNavigationController: UINavigationController?

  override var navigationController: UINavigationController? {

    get {
      return overriddenNavigationController
    }

    set {
      overriddenNavigationController = newValue
    }
  }
  var miniMediaControlsItemEnabled = false

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

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

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

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

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

...
Objective-C

RootContainerViewController.h

static const NSTimeInterval kCastControlBarsAnimationDuration = 0.20;

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

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

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

@end

RootContainerViewController.m

@implementation RootContainerViewController

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

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

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

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

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

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

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

...

@end

Параметр GCKUIMiniMediaControlsViewControllerDelegate указывает контроллеру представления организатора, когда должен быть виден мини-контроллер:

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

Как добавить расширенный контроллер

Согласно контрольному списку требований к дизайну Google Cast, приложение-отправитель должно предоставлять расширенный контроллер для транслируемого медиаконтента. Развернутый контроллер – это полноэкранная версия мини-контроллера.

Развернутый контроллер – это полноэкранный просмотр экрана, в котором можно управлять воспроизведением удаленного медиаконтента. Этот режим позволяет приложению для трансляции управлять всеми аспектами сеанса, кроме громкости веб-приемника и жизненного цикла сеанса (подключение и остановка трансляции). Также в нем содержится вся информация о статусе медиасеанса (обложка, название, субтитры и т. д.).

Функциональность этого представления реализована в классе GCKUIExpandedMediaControlsViewController.

Первое, что вам нужно сделать, – включить контроллер по умолчанию в контексте Cast. Измените делегат приложения, чтобы включить контроллер по умолчанию:

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

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true

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

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  ..
}

Добавьте в контроллер представления следующий код, чтобы загружать расширенный контроллер, когда пользователь начинает транслировать видео:

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

Расширенный контроллер также будет запускаться автоматически, когда пользователь нажимает на мини-контроллер.

Когда приложение отправителя воспроизводит видео или аудиоконтент в режиме стриминга, SDK автоматически показывает кнопку "Воспроизвести/остановить" вместо кнопки "Воспроизвести/пауза" на развернутом контроллере.

Информацию о том, как приложение отправителя может настраивать внешний вид виджетов Cast, можно найти в разделе Как применить собственные стили к приложению для iOS.

Регулятор громкости

Фреймворк Cast автоматически управляет громкостью в приложении отправителя. Он также автоматически синхронизирует громкость веб-приемника с предоставленными виджетами интерфейса. Чтобы синхронизировать ползунок, предоставленный приложением, используйте GCKUIDeviceVolumeController.

Управление громкостью с помощью физических кнопок

Физические кнопки громкости на устройстве отправителя можно использовать для изменения громкости сеанса Cast на веб-приемнике с помощью флага physicalVolumeButtonsWillControlDeviceVolume в GCKCastOptions, который задается в 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];

Обработка ошибок

Приложения отправителя должны обрабатывать все обратные вызовы ошибок и определять оптимальный ответ для каждого этапа жизненного цикла Cast. Приложение может показывать пользователю диалоговые окна с сообщениями об ошибках или завершить сеанс трансляции.

Обратите внимание, что некоторые ошибки, в том числе GCKErrorCode GCKErrorCodeCancelled, являются ожидаемыми.

Не пытайтесь повторно подключиться к учащемуся (GCKErrorCodeCancelled), если подключение не удалось. Это может привести к неожиданным результатам.

Журналы

GCKLogger – это объект-одиночка, используемый фреймворком для ведения журналов. Используйте GCKLoggerDelegate, чтобы настроить обработку сообщений журнала.

С помощью GCKLogger SDK создает выходные данные журнала в виде сообщений отладки, ошибок и предупреждений. Эти сообщения помогают отлаживать код, а также выявлять и устранять неполадки. По умолчанию вывод журнала подавляется, но, назначив GCKLoggerDelegate, приложение отправителя может получать эти сообщения от SDK и регистрировать их в системной консоли.

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

Чтобы включить отладочные и подробные сообщения, добавьте в код после установки делегата (как показано выше) следующую строку:

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;

Вы также можете отфильтровать сообщения журнала, созданные GCKLogger. Укажите минимальный уровень ведения журнала для каждого класса, например:

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;

Названия классов могут быть как буквальными, так и шаблонами, например GCKUI\* и GCK\*Session.