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

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

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

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

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

Процесс в приложении

Ниже описаны основные этапы выполнения для приложения iOS-отправителя.

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

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

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

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

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

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

При инициализации GCKCastContext необходимо предоставить объект GCKCastOptions. Этот класс содержит параметры, влияющие на поведение фреймворка. Самый важный из них – идентификатор приложения веб-приемника, который используется для фильтрации результатов поиска и запуска приложения веб-приемника при начале сеанса 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];

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

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

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

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

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

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

В Cast SDK вводится понятие сеанса Cast, который включает в себя подключение к устройству, запуск веб-приложения получателя (или присоединение к нему), подключение к этому приложению и инициализацию канала управления медиаконтентом. Дополнительную информацию о сеансах 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 установлен с приложением Web Receiver, которое поддерживает пространство имен мультимедиа, фреймворк автоматически создаст экземпляр 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

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

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

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

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 Design Checklist, приложение-отправитель должно предоставлять расширенный контроллер для транслируемого контента. Развернутый контроллер – это полноэкранная версия мини-контроллера.

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

Функциональность этого представления реализована классом 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.