iOS 앱에 Cast 통합

이 개발자 가이드에서는 iOS 발신기 SDK를 사용하여 iOS 발신기 앱에 Google Cast 지원을 추가하는 방법을 설명합니다.

휴대기기 또는 노트북은 재생을 제어하는 발신기 이고 Google Cast 기기는 TV에 콘텐츠를 표시하는 수신기 입니다.

발신기 프레임워크 는 발신기의 런타임에 있는 Cast 클래스 라이브러리 바이너리 및 관련 리소스를 의미합니다. 발신기 앱 또는 Cast 앱 은 발신기에서 실행되는 앱을 의미합니다. 웹 수신기 앱 은 웹 수신기에서 실행되는 HTML 애플리케이션을 의미합니다.

발신기 프레임워크는 비동기 콜백 디자인을 사용하여 발신기 앱에 이벤트를 알리고 Cast 앱 수명 주기의 다양한 상태 간에 전환합니다.

앱 흐름

다음 단계에서는 발신기 iOS 앱의 일반적인 고급 실행 흐름을 설명합니다.

  • Cast 프레임워크는 GCKDiscoveryManager 기기 검색을 시작하기 위해 GCKCastOptions에 제공된 속성을 기반으로 를 시작합니다.
  • 사용자가 전송 버튼을 클릭하면 프레임워크는 검색된 Cast 기기 목록이 포함된 전송 대화상자를 표시합니다.
  • 사용자가 Cast 기기를 선택하면 프레임워크는 Cast 기기에서 웹 수신기 앱을 실행하려고 시도합니다.
  • 프레임워크는 발신기 앱에서 콜백을 호출하여 웹 수신기 앱이 실행되었는지 확인합니다.
  • 프레임워크는 발신기 앱과 웹 수신기 앱 간에 통신 채널을 만듭니다.
  • 프레임워크는 통신 채널을 사용하여 웹 수신기에서 미디어 재생을 로드하고 제어합니다.
  • 프레임워크는 발신기와 웹 수신기 간에 미디어 재생 상태를 동기화합니다. 사용자가 발신기 UI 작업을 실행하면 프레임워크는 이러한 미디어 제어 요청을 웹 수신기로 전달하고 웹 수신기가 미디어 상태 업데이트를 전송하면 프레임워크는 발신기 UI의 상태를 업데이트합니다.
  • 사용자가 전송 버튼을 클릭하여 Cast 기기에서 연결을 해제하면 프레임워크는 발신기 앱을 웹 수신기에서 연결 해제합니다.

발신기 문제를 해결하려면 로깅을 사용 설정해야 합니다.

Google Cast iOS 프레임워크의 모든 클래스, 메서드, 이벤트의 전체 목록은 Google Cast iOS API 참조를 참고하세요. 다음 섹션에서는 iOS 앱에 Cast를 통합하는 단계를 설명합니다.

기본 스레드에서 메서드 호출

Cast 컨텍스트 초기화

Cast 프레임워크에는 프레임워크의 모든 활동을 조정하는 전역 싱글톤 객체 GCKCastContext가 있습니다. 이 객체는 일반적으로 앱 대리자의 -[application:didFinishLaunchingWithOptions:] 메서드에서 애플리케이션의 수명 주기 초기에 초기화되어야 합니다. 그래야 발신기 앱이 다시 시작할 때 자동 세션 재개가 적절히 트리거될 수 있기 때문입니다.

GCKCastContext를 초기화할 때 GCKCastOptions 객체를 제공해야 합니다. 이 클래스에는 프레임워크 동작에 영향을 미치는 옵션이 포함되어 있습니다. 그중 가장 중요한 옵션은 웹 수신기 애플리케이션 ID입니다. 검색 결과를 필터링하고 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 메서드가 있습니다. 발신기 앱은 텍스트, 제목 텍스트의 위치, 닫기 버튼을 맞춤설정할 수 있습니다.

  • 전송 버튼: Cast iOS 발신기 SDK 4.6.0부터 발신기 기기가 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 은 스토리보드에 직접 추가할 수도 있습니다.

기기 검색 구성

프레임워크에서 기기 검색은 자동으로 이루어집니다. 맞춤 UI를 구현하지 않는 한 검색 프로세스를 명시적으로 시작하거나 중지할 필요가 없습니다.

프레임워크의 검색은 GCKDiscoveryManager 클래스에서 관리하며, 이는 GCKCastContext의 속성입니다. 프레임워크는 기기 선택 및 제어를 위한 기본 전송 대화상자 구성요소를 제공합니다. 기기 목록은 기기 친화적 이름으로 사전순으로 정렬됩니다.

세션 관리 작동 방식

Cast SDK는 기기 연결, 웹 수신기 앱 실행 (또는 연결), 앱 연결, 미디어 제어 채널 초기화 단계를 결합하는 Cast 세션의 개념을 도입합니다. Cast 세션 및 웹 수신기 수명 주기에 관한 자세한 내용은 웹 수신기 애플리케이션 수명 주기 가이드 를 참고하세요.

세션은 GCKSessionManager 클래스에서 관리하며, 이는 GCKCastContext의 속성입니다. 개별 세션은 클래스 GCKSession의 서브클래스로 표현됩니다. 예를 들어 GCKCastSession 은 Cast 기기가 있는 세션을 나타냅니다. 현재 활성 Cast 세션 (있는 경우)에는 GCKSessionManagercurrentCastSession 속성으로 액세스할 수 있습니다.

GCKSessionManagerListener 인터페이스는 세션 생성, 정지, 재개, 종료와 같은 세션 이벤트를 모니터링하는 데 사용될 수 있습니다. 프레임워크는 발신기 앱이 백그라운드로 전환될 때 세션을 자동으로 정지하고 앱이 포그라운드로 돌아오거나 세션이 활성 상태일 때 앱이 비정상적으로 종료된 후 다시 실행되면 세션을 재개하려고 시도합니다.

전송 대화상자가 사용 중인 경우 세션은 사용자 동작에 관한 응답으로 자동으로 생성되고 중단됩니다. 그렇지 않으면 앱은 세션을 명시적으로 시작하고 종료할 수 있습니다. GCKSessionManager

앱이 세션 수명 주기 이벤트에 관한 응답으로 특별한 처리를 해야 하는 경우 GCKSessionManager에 하나 이상의 GCKSessionManagerListener 인스턴스를 등록할 수 있습니다. GCKSessionManagerListener 는 세션 시작, 세션 종료 등과 같은 이벤트의 콜백을 정의하는 프로토콜입니다.

스트림 이전

세션 상태 유지는 스트림 이전의 기본입니다. 사용자는 음성 명령, Google Home 앱 또는 스마트 디스플레이를 사용하여 기기 간에 기존 오디오 및 동영상 스트림을 이동할 수 있습니다. 미디어가 한 기기 (소스)에서 재생을 중지하고 다른 기기 (대상)에서 계속 재생됩니다. 최신 펌웨어가 있는 모든 Cast 기기는 스트림 이전에서 소스 또는 대상으로 사용할 수 있습니다.

스트림 이전 중에 새 대상 기기를 가져오려면 GCKCastSession#device 콜백 중에 [sessionManager:didResumeCastSession:] 속성을 사용합니다.

자세한 내용은 웹 수신기에서 스트림 이전 을 참고하세요.

자동 재연결

Cast 프레임워크는 다음과 같은 여러 미묘한 코너 케이스에서 재연결을 자동으로 처리하는 재연결 로직을 추가합니다.

  • 일시적인 Wi-Fi 손실에서 복구
  • 기기 절전 모드에서 복구
  • 앱 백그라운드 처리에서 복구
  • 앱이 비정상 종료된 경우 복구

미디어 제어 작동 방식

미디어 네임스페이스를 지원하는 웹 수신기 앱으로 Cast 세션이 설정되면 프레임워크에서 GCKRemoteMediaClient 인스턴스를 자동으로 만듭니다. 이 인스턴스는 remoteMediaClient 속성으로 액세스할 수 있습니다. GCKCastSession

웹 수신기에 요청을 실행하는 GCKRemoteMediaClient의 모든 메서드는 요청을 추적하는 데 사용할 수 있는 GCKRequest 객체를 반환합니다. GCKRequestDelegate 작업의 최종 결과에 관한 알림을 수신하도록 이 객체에 할당할 수 있습니다.

GCKRemoteMediaClient 인스턴스는 앱의 여러 부분에서 공유될 수 있으며 실제로 전송 대화상자 및 미니 미디어 컨트롤과 같은 프레임워크의 일부 내부 구성요소는 인스턴스를 공유합니다. 이를 위해 GCKRemoteMediaClient 여러 GCKRemoteMediaClientListener의 등록을 지원합니다.

미디어 메타데이터 설정

The 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 Design 체크리스트에 따르면 발신기 앱은 사용자가 현재 콘텐츠 페이지에서 이동할 때 표시되는 미니 컨트롤러라는 영구 컨트롤을 제공해야 합니다. 미니 컨트롤러는 즉시 액세스할 수 있으며 현재 Cast 세션을 시각적으로 표시합니다.

Cast 프레임워크는 미니 컨트롤러를 표시할 장면에 추가할 수 있는 컨트롤 바 GCKUIMiniMediaControlsViewController, 를 제공합니다.

발신기 앱이 동영상 또는 오디오 라이브 스트림을 재생할 때 SDK는 미니 컨트롤러의 재생/일시중지 버튼 대신 재생/중지 버튼을 자동으로 표시합니다.

발신기 앱에서 Cast 위젯의 모양을 구성하는 방법은 iOS 발신기 UI 맞춤설정을 참고하세요.

발신기 앱에 미니 컨트롤러를 추가하는 방법에는 두 가지가 있습니다.

  • Cast 프레임워크가 자체 뷰 컨트롤러로 기존 뷰 컨트롤러를 래핑하여 미니 컨트롤러의 레이아웃을 관리하도록 합니다.
  • 스토리보드에서 하위 뷰를 제공하여 기존 뷰 컨트롤러에 미니 컨트롤러 위젯을 추가하여 미니 컨트롤러 위젯의 레이아웃을 직접 관리합니다.

GCKUICastContainerViewController를 사용하여 래핑

첫 번째 방법은 다른 뷰 컨트롤러를 래핑하고 하단에 GCKUIMiniMediaControlsViewController 를 추가하는 GCKUICastContainerViewController 를 사용하는 것입니다. 이 접근 방식은 애니메이션을 맞춤설정할 수 없고 컨테이너 뷰 컨트롤러의 동작을 구성할 수 없다는 점에서 제한적입니다.

이 첫 번째 방법은 일반적으로 앱 대리자의 -[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 프레임워크는 발신기 앱의 볼륨을 자동으로 관리합니다. 프레임워크는 제공된 UI 위젯의 웹 수신기 볼륨과 자동으로 동기화됩니다. 앱에서 제공하는 슬라이더를 동기화하려면 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 수명 주기의 각 단계에 가장 적합한 응답을 결정하는 것이 매우 중요합니다. 앱은 사용자에게 오류 대화상자를 표시하거나 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;

클래스 이름은 리터럴 이름 또는 glob 패턴일 수 있습니다(예: GCKUI\*, GCK\*Session).