iOS 앱에서 Cast 지원 사용

1. 개요

Google Cast 로고

이 Codelab에서는 Google Cast 지원 기기에서 콘텐츠를 전송하도록 기존 iOS 동영상 앱을 수정하는 방법을 설명합니다.

Google Cast란 무엇인가요?

사용자는 Google Cast를 사용하여 휴대기기의 콘텐츠를 TV로 전송할 수 있습니다. 그런 다음, 휴대기기를 리모컨으로 사용해 TV에서 재생 중인 미디어를 제어할 수 있습니다.

Google Cast SDK를 사용하면 앱을 확장하여 Google Cast 지원 기기 (예: TV 또는 사운드 시스템)를 제어할 수 있습니다. Cast SDK를 사용하면 Google Cast 디자인 체크리스트에 따라 필요한 UI 구성요소를 추가할 수 있습니다.

Google Cast 디자인 체크리스트는 지원되는 모든 플랫폼에서 Cast 사용자 환경을 간단하고 예측 가능하게 만들기 위해 제공됩니다.

무엇을 빌드하게 되나요?

이 Codelab을 완료하면 Google Cast 기기로 동영상을 전송할 수 있는 iOS 동영상 앱이 만들어집니다.

학습할 내용

  • 샘플 동영상 앱에 Google Cast SDK를 추가하는 방법
  • Google Cast 기기를 선택할 수 있는 전송 버튼을 추가하는 방법
  • Cast 기기에 연결하고 미디어 수신기를 실행하는 방법
  • 동영상을 전송하는 방법
  • 앱에 Cast 미니 컨트롤러를 추가하는 방법
  • 확장 컨트롤러를 추가하는 방법
  • 소개 오버레이를 제공하는 방법
  • Cast 위젯을 맞춤설정하는 방법
  • Cast Connect 통합 방법

필요한 항목

  • 최신 Xcode
  • iOS 9 이상이 설치된 휴대기기 (또는 Xcode 시뮬레이터)
  • 휴대기기를 개발용 컴퓨터에 연결하는 USB 데이터 케이블 (기기를 사용하는 경우)
  • Chromecast 또는 Android TV와 같이 인터넷에 액세스할 수 있도록 설정된 Google Cast 기기
  • HDMI 입력 단자가 있는 TV 또는 모니터
  • Cast Connect 통합을 테스트하려면 Chromecast with Google TV가 필요하지만 Codelab의 나머지 부분에서는 선택사항입니다. 계정이 없는 경우 이 튜토리얼의 끝부분에 있는 Cast Connect 지원 추가 단계를 건너뛰어도 됩니다.

환경

  • iOS 개발에 관한 사전 지식이 있어야 합니다.
  • 또한 TV 시청에 관한 사전 지식도 필요합니다. :)

본 가이드를 어떻게 사용하실 계획인가요?

읽기만 할 계획입니다 읽은 다음 연습 활동을 완료할 계획입니다

귀하의 iOS 앱 빌드 관련 경험을 평가해 주세요.

초급 중급 고급

TV 시청 관련 경험을 평가해 주세요.

초급 중급 고급

2. 샘플 코드 가져오기

모든 샘플 코드를 컴퓨터로 다운로드할 수 있습니다...

그런 다음 다운로드한 ZIP 파일의 압축을 풉니다.

3. 샘플 앱 실행

Apple iOS 로고

먼저 완성된 샘플 앱이 어떤 모습인지 살펴보겠습니다. 기본 동영상 플레이어로 사용되는 앱입니다. 사용자가 목록에서 동영상을 선택한 다음 기기에서 로컬로 재생하거나 Google Cast 기기로 전송할 수 있습니다.

다음 안내에서는 코드를 다운로드한 후 Xcode에서 완성된 샘플 앱을 열고 실행하는 방법을 설명합니다.

자주 묻는 질문(FAQ)

CocoaPods 설정

CocoaPods를 설정하려면 콘솔로 이동하여 macOS에서 사용 가능한 기본 Ruby를 사용하여 CocoaPods를 설치합니다.

sudo gem install cocoapods

문제가 발생하면 공식 문서를 참고하여 종속 항목 관리자를 다운로드하고 설치하세요.

프로젝트 설정

  1. 터미널로 이동하여 Codelab 디렉터리로 이동합니다.
  2. Podfile에서 종속 항목을 설치합니다.
cd app-done
pod update
pod install
  1. Xcode를 열고 Open other project...(다른 프로젝트 열기...)를 선택합니다.
  2. 샘플 코드 폴더의 폴더 아이콘app-done 디렉터리에서 CastVideos-ios.xcworkspace 파일을 선택합니다.

앱 실행

타겟과 시뮬레이터를 선택한 후 앱을 실행합니다.

XCode 앱 시뮬레이터 툴바

몇 초 후에 동영상 앱이 표시됩니다.

수신 네트워크 연결 수락에 관한 알림이 표시되면 '허용'을 클릭해야 합니다. 이 옵션이 허용되지 않으면 전송 아이콘이 표시되지 않습니다.

인바운드 네트워크 연결을 수락할 권한을 요청하는 확인 대화상자

전송 버튼을 클릭하고 Google Cast 기기를 선택합니다.

동영상을 선택하고 재생 버튼을 클릭합니다.

Google Cast 기기에서 동영상이 재생되기 시작합니다.

확장된 컨트롤러가 표시됩니다. 재생/일시중지 버튼을 사용하여 재생을 제어할 수 있습니다.

동영상 목록으로 다시 이동합니다.

이제 미니 컨트롤러가 화면 하단에 표시됩니다.

Cast동영상 앱을 실행 중인 iPhone의 일러스트레이션과 하단에 미니 컨트롤러가 표시되어 있습니다.

미니 컨트롤러에서 일시중지 버튼을 클릭하면 수신기에서 동영상이 일시중지됩니다. 동영상을 계속 재생하려면 미니 컨트롤러에서 재생 버튼을 클릭합니다.

Google Cast 기기로의 전송을 중지하려면 전송 버튼을 클릭합니다.

4. 시작 프로젝트 준비

Cast동영상 앱을 실행하는 iPhone을 표현한 그림

다운로드한 시작 앱에 Google Cast 지원 기능을 추가해야 합니다. 다음은 이 Codelab에서 사용할 Google Cast 용어입니다.

  • 발신기 앱은 휴대기기 또는 노트북에서 실행됩니다.
  • 수신기 앱은 Google Cast 기기에서 실행됩니다.

프로젝트 설정

이제 Xcode를 사용하여 시작 프로젝트 위에 빌드할 준비가 되었습니다.

  1. 터미널로 이동하여 Codelab 디렉터리로 이동합니다.
  2. Podfile에서 종속 항목을 설치합니다.
cd app-start
pod update
pod install
  1. Xcode를 열고 Open other project...(다른 프로젝트 열기...)를 선택합니다.
  2. 샘플 코드 폴더의 폴더 아이콘app-start 디렉터리에서 CastVideos-ios.xcworkspace 파일을 선택합니다.

앱 디자인

앱이 원격 웹 서버에서 동영상 목록을 가져오고 사용자가 둘러볼 수 있도록 목록을 제공합니다. 사용자는 동영상을 선택하여 세부정보를 보거나 휴대기기에서 로컬로 동영상을 재생할 수 있습니다.

앱은 두 가지 기본 뷰 컨트롤러(MediaTableViewControllerMediaViewController.)로 구성됩니다.

MediaTableViewController

이 UITableViewController는 MediaListModel 인스턴스의 동영상 목록을 표시합니다. 동영상 목록 및 관련 메타데이터는 원격 서버에서 JSON 파일로 호스팅됩니다. MediaListModel는 이 JSON을 가져와서 처리하여 MediaItem 객체 목록을 빌드합니다.

MediaItem 객체는 동영상 및 관련 메타데이터(예: 제목, 설명, 이미지 URL, 스트림 URL)를 모델링합니다.

MediaTableViewControllerMediaListModel 인스턴스를 만든 다음 스스로를 MediaListModelDelegate로 등록하여 미디어 메타데이터가 다운로드되면 알림을 받도록 합니다. 그러면 테이블 뷰를 로드할 수 있습니다.

사용자에게는 각 동영상에 관한 간단한 설명과 함께 동영상 미리보기 이미지 목록이 표시됩니다. 항목이 선택되면 상응하는 MediaItemMediaViewController에 전달됩니다.

MediaViewController

이 보기 컨트롤러는 특정 동영상에 대한 메타데이터를 표시하고 사용자가 휴대기기에서 로컬로 동영상을 재생할 수 있게 해 줍니다.

뷰 컨트롤러는 LocalPlayerView, 일부 미디어 컨트롤, 선택된 동영상의 설명을 표시하는 텍스트 영역을 호스팅합니다. 플레이어는 화면 상단을 차지하며, 아래에 동영상 자세한 설명을 위한 공간을 남겨둡니다. 사용자는 로컬 동영상 재생을 재생/일시중지하거나 탐색할 수 있습니다.

자주 묻는 질문(FAQ)

5. 전송 버튼 추가

Cast동영상 앱을 실행 중인 iPhone의 상단 3분의 1에 표시된 오른쪽 상단에 전송 버튼이 표시된 일러스트레이션

Cast 지원 애플리케이션은 각 보기 컨트롤러에 전송 버튼을 표시합니다. 전송 버튼을 클릭하면 사용자가 선택할 수 있는 Cast 기기 목록이 표시됩니다. 사용자가 발신기 기기에서 로컬로 콘텐츠를 재생 중인 경우 Cast 기기를 선택하면 Cast 기기에서 재생이 시작되거나 재개됩니다. 사용자는 Cast 세션 중 언제든지 전송 버튼을 클릭하여 애플리케이션의 Cast 기기 전송을 중지할 수 있습니다. Google Cast 디자인 체크리스트에 설명된 대로 사용자는 애플리케이션의 모든 화면에서 Cast 기기에 연결하거나 연결 해제할 수 있어야 합니다.

구성

시작 프로젝트에는 완료된 샘플 앱과 동일한 종속 항목 및 Xcode 설정이 필요합니다. 해당 섹션으로 돌아가서 동일한 단계에 따라 GoogleCast.framework를 시작 앱 프로젝트에 추가합니다.

초기화

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

GCKCastContext를 초기화할 때 GCKCastOptions 객체를 제공해야 합니다. 이 클래스에는 프레임워크 동작에 영향을 미치는 옵션이 있습니다. 이 중 가장 중요한 것은 수신기 애플리케이션 ID로, Cast 기기 검색 결과를 필터링하고 Cast 세션이 시작될 때 수신기 애플리케이션을 실행하는 데 사용됩니다.

application(_:didFinishLaunchingWithOptions:) 메서드는 Cast 프레임워크에서 로깅 메시지를 수신하도록 로깅 위임을 설정하기에 좋은 위치입니다. 이는 디버깅 및 문제 해결에 유용할 수 있습니다.

자체 Cast 지원 앱을 개발하는 경우 Cast 개발자로 등록한 다음 앱의 애플리케이션 ID를 받아야 합니다. 이 Codelab에서는 샘플 앱 ID를 사용합니다.

AppDelegate.swift에 다음 코드를 추가하여 사용자 기본값의 애플리케이션 ID로 GCKCastContext를 초기화하고 Google Cast 프레임워크용 로거를 추가합니다.

import GoogleCast

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  fileprivate var enableSDKLogging = true

  ...

  func application(_: UIApplication,
                   didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    ...
    let options = GCKCastOptions(discoveryCriteria: GCKDiscoveryCriteria(applicationID: kReceiverAppID))
    options.physicalVolumeButtonsWillControlDeviceVolume = true
    GCKCastContext.setSharedInstanceWith(options)

    window?.clipsToBounds = true
    setupCastLogging()
    ...
  }
  ...
  func setupCastLogging() {
    let logFilter = GCKLoggerFilter()
    let classesToLog = ["GCKDeviceScanner", "GCKDeviceProvider", "GCKDiscoveryManager", "GCKCastChannel",
                        "GCKMediaControlChannel", "GCKUICastButton", "GCKUIMediaController", "NSMutableDictionary"]
    logFilter.setLoggingLevel(.verbose, forClasses: classesToLog)
    GCKLogger.sharedInstance().filter = logFilter
    GCKLogger.sharedInstance().delegate = self
  }
}

...

// MARK: - GCKLoggerDelegate

extension AppDelegate: GCKLoggerDelegate {
  func logMessage(_ message: String,
                  at _: GCKLoggerLevel,
                  fromFunction function: String,
                  location: String) {
    if enableSDKLogging {
      // Send SDK's log messages directly to the console.
      print("\(location): \(function) - \(message)")
    }
  }
}

전송 버튼

이제 GCKCastContext가 초기화되었으므로 전송 버튼을 추가하여 사용자가 Cast 기기를 선택할 수 있도록 해야 합니다. Cast SDK는 GCKUICastButton라는 전송 버튼 구성요소를 UIButton 서브클래스로 제공합니다. UIBarButtonItem에 래핑하여 애플리케이션의 제목 표시줄에 추가할 수 있습니다. 전송 버튼을 MediaTableViewControllerMediaViewController에 모두 추가해야 합니다.

다음 코드를 MediaTableViewController.swiftMediaViewController.swift에 추가합니다.

import GoogleCast

@objc(MediaTableViewController)
class MediaTableViewController: UITableViewController, GCKSessionManagerListener,
  MediaListModelDelegate, GCKRequestDelegate {
  private var castButton: GCKUICastButton!
  ...
  override func viewDidLoad() {
    print("MediaTableViewController - viewDidLoad")
    super.viewDidLoad()

    ...
    castButton = GCKUICastButton(frame: CGRect(x: CGFloat(0), y: CGFloat(0),
                                               width: CGFloat(24), height: CGFloat(24)))
    // Overwrite the UIAppearance theme in the AppDelegate.
    castButton.tintColor = UIColor.white
    navigationItem.rightBarButtonItem = UIBarButtonItem(customView: castButton)

    ...
  }
  ...
}

다음으로, MediaViewController.swift에 다음 코드를 추가합니다.

import GoogleCast

@objc(MediaViewController)
class MediaViewController: UIViewController, GCKSessionManagerListener, GCKRemoteMediaClientListener,
  LocalPlayerViewDelegate, GCKRequestDelegate {
  private var castButton: GCKUICastButton!
  ...
  override func viewDidLoad() {
    super.viewDidLoad()
    print("in MediaViewController viewDidLoad")
    ...
    castButton = GCKUICastButton(frame: CGRect(x: CGFloat(0), y: CGFloat(0),
                                               width: CGFloat(24), height: CGFloat(24)))
    // Overwrite the UIAppearance theme in the AppDelegate.
    castButton.tintColor = UIColor.white
    navigationItem.rightBarButtonItem = UIBarButtonItem(customView: castButton)

    ...
  }
  ...
}

이제 앱을 실행합니다. 앱의 탐색 메뉴에 전송 버튼이 표시되며 이 버튼을 클릭하면 로컬 네트워크에 Cast 기기가 표시됩니다. 기기 검색은 GCKCastContext에서 자동으로 관리됩니다. Cast 기기를 선택하면 샘플 수신기 앱이 Cast 기기에 로드됩니다. 탐색 활동과 로컬 플레이어 활동을 오가며 둘러볼 수 있으며 전송 버튼 상태는 동기화된 상태로 유지됩니다.

미디어 재생과 관련된 지원이 연결되지 않았으므로 아직 Cast 기기에서 동영상을 재생할 수 없습니다. 전송을 중지하려면 전송 버튼을 클릭하세요.

6. 동영상 콘텐츠 전송

Cast동영상 앱을 실행 중인 iPhone의 일러스트레이션으로, 특정 동영상 ('Tears of Steel')의 세부정보를 표시합니다. 하단에는 미니플레이어가 있습니다.

Cast 기기에서 원격으로 동영상을 재생할 수 있도록 샘플 앱을 확장하겠습니다. 이를 처리하려면 Cast 프레임워크에서 생성된 다양한 이벤트를 수신 대기해야 합니다.

미디어 전송

Cast 기기에서 미디어를 재생하려면 상위 수준에서 다음을 실행해야 합니다.

  1. Cast SDK에서 미디어 항목을 모델링하는 GCKMediaInformation 객체를 만듭니다.
  2. 사용자가 Cast 기기에 연결하여 수신기 애플리케이션을 실행합니다.
  3. GCKMediaInformation 객체를 수신기에 로드하고 콘텐츠를 재생합니다.
  4. 미디어 상태를 추적합니다.
  5. 사용자 상호작용에 따라 재생 명령어를 수신기로 전송합니다.

1단계는 한 객체를 다른 객체에 매핑하는 것과 같습니다. GCKMediaInformation은 Cast SDK가 이해하는 것이고 MediaItem는 앱의 미디어 항목 캡슐화입니다. MediaItemGCKMediaInformation에 쉽게 매핑할 수 있습니다. 이전 섹션에서 이미 2단계를 완료했습니다. 3단계는 Cast SDK로 쉽게 할 수 있습니다.

MediaViewController 샘플 앱은 이미 다음 enum을 사용하여 로컬 재생과 원격 재생을 구분합니다.

enum PlaybackMode: Int {
  case none = 0
  case local
  case remote
}

private var playbackMode = PlaybackMode.none

이 Codelab에서 모든 샘플 플레이어 로직의 작동 방식을 정확히 이해하는 것은 중요하지 않습니다. 이와 유사한 방식으로 두 가지 재생 위치를 인식하도록 앱의 미디어 플레이어가 수정되어야 한다는 점을 이해하는 것이 중요합니다.

현재 로컬 플레이어는 전송 상태에 관한 정보를 모르므로 항상 로컬 재생 상태입니다. Cast 프레임워크에서 발생하는 상태 전환에 따라 UI를 업데이트해야 합니다. 예를 들어 전송을 시작하면 로컬 재생을 중지하고 일부 컨트롤을 사용 중지해야 합니다. 마찬가지로, 이 뷰 컨트롤러에서 전송을 중지하면 로컬 재생으로 전환해야 합니다. 이를 처리하려면 Cast 프레임워크에서 생성된 다양한 이벤트를 수신 대기해야 합니다.

전송 세션 관리

Cast 프레임워크의 경우 전송 세션에 기기 연결, 실행(또는 연결), 수신기 애플리케이션 연결, 필요한 경우 미디어 제어 채널 초기화 단계가 결합되어 있습니다. 미디어 제어 채널은 Cast 프레임워크가 수신기 미디어 플레이어에서 메시지를 주고받는 방법입니다.

사용자가 전송 버튼에서 기기를 선택하면 전송 세션이 자동으로 시작되고 사용자 연결 해제 시 자동으로 중지됩니다. 네트워킹 문제로 인해 수신기 세션에 다시 연결하는 경우도 Cast 프레임워크에서 자동으로 처리합니다.

전송 세션은 GCKCastContext.sharedInstance().sessionManager를 통해 액세스할 수 있는 GCKSessionManager에서 관리합니다. GCKSessionManagerListener 콜백은 생성, 정지, 재개, 종료와 같은 세션 이벤트를 모니터링하는 데 사용할 수 있습니다.

먼저 세션 리스너를 등록하고 몇 가지 변수를 초기화해야 합니다.

class MediaViewController: UIViewController, GCKSessionManagerListener,
  GCKRemoteMediaClientListener, LocalPlayerViewDelegate, GCKRequestDelegate {

  ...
  private var sessionManager: GCKSessionManager!
  ...

  required init?(coder: NSCoder) {
    super.init(coder: coder)

    sessionManager = GCKCastContext.sharedInstance().sessionManager

    ...
  }

  override func viewWillAppear(_ animated: Bool) {
    ...

    let hasConnectedSession: Bool = (sessionManager.hasConnectedSession())
    if hasConnectedSession, (playbackMode != .remote) {
      populateMediaInfo(false, playPosition: 0)
      switchToRemotePlayback()
    } else if sessionManager.currentSession == nil, (playbackMode != .local) {
      switchToLocalPlayback()
    }

    sessionManager.add(self)

    ...
  }

  override func viewWillDisappear(_ animated: Bool) {
    ...

    sessionManager.remove(self)
    sessionManager.currentCastSession?.remoteMediaClient?.remove(self)
    ...
    super.viewWillDisappear(animated)
  }

  func switchToLocalPlayback() {
    ...

    sessionManager.currentCastSession?.remoteMediaClient?.remove(self)

    ...
  }

  func switchToRemotePlayback() {
    ...

    sessionManager.currentCastSession?.remoteMediaClient?.add(self)

    ...
  }


  // MARK: - GCKSessionManagerListener

  func sessionManager(_: GCKSessionManager, didStart session: GCKSession) {
    print("MediaViewController: sessionManager didStartSession \(session)")
    setQueueButtonVisible(true)
    switchToRemotePlayback()
  }

  func sessionManager(_: GCKSessionManager, didResumeSession session: GCKSession) {
    print("MediaViewController: sessionManager didResumeSession \(session)")
    setQueueButtonVisible(true)
    switchToRemotePlayback()
  }

  func sessionManager(_: GCKSessionManager, didEnd _: GCKSession, withError error: Error?) {
    print("session ended with error: \(String(describing: error))")
    let message = "The Casting session has ended.\n\(String(describing: error))"
    if let window = appDelegate?.window {
      Toast.displayMessage(message, for: 3, in: window)
    }
    setQueueButtonVisible(false)
    switchToLocalPlayback()
  }

  func sessionManager(_: GCKSessionManager, didFailToStartSessionWithError error: Error?) {
    if let error = error {
      showAlert(withTitle: "Failed to start a session", message: error.localizedDescription)
    }
    setQueueButtonVisible(false)
  }

  func sessionManager(_: GCKSessionManager,
                      didFailToResumeSession _: GCKSession, withError _: Error?) {
    if let window = UIApplication.shared.delegate?.window {
      Toast.displayMessage("The Casting session could not be resumed.",
                           for: 3, in: window)
    }
    setQueueButtonVisible(false)
    switchToLocalPlayback()
  }

  ...
}

Cast 기기에 연결되거나 연결 해제될 때 로컬 플레이어와 전환할 수 있도록 MediaViewController에서 알림을 받고 싶습니다. 사용자의 휴대기기에서 실행 중인 애플리케이션의 인스턴스뿐만 아니라 다른 휴대기기에서 실행 중인 사용자 또는 다른 사람의 애플리케이션 인스턴스로부터 연결이 방해를 받을 수 있습니다.

현재 활성 세션에는 GCKCastContext.sharedInstance().sessionManager.currentCastSession으로 액세스할 수 있습니다. 세션은 전송 대화상자에서의 사용자 동작에 응답하여 자동으로 생성되고 해제됩니다.

미디어 로드

Cast SDK에서 GCKRemoteMediaClient는 수신기에서 원격 미디어 재생을 편리하게 관리할 수 있는 API 집합을 제공합니다. 미디어 재생을 지원하는 GCKCastSession의 경우 GCKRemoteMediaClient 인스턴스가 SDK에 의해 자동으로 생성됩니다. GCKCastSession 인스턴스의 remoteMediaClient 속성으로 액세스할 수 있습니다.

MediaViewController.swift에 다음 코드를 추가하여 현재 선택된 동영상을 수신기에 로드합니다.

@objc(MediaViewController)
class MediaViewController: UIViewController, GCKSessionManagerListener,
  GCKRemoteMediaClientListener, LocalPlayerViewDelegate, GCKRequestDelegate {
  ...

  @objc func playSelectedItemRemotely() {
    loadSelectedItem(byAppending: false)
  }

  /**
   * Loads the currently selected item in the current cast media session.
   * @param appending If YES, the item is appended to the current queue if there
   * is one. If NO, or if
   * there is no queue, a new queue containing only the selected item is created.
   */
  func loadSelectedItem(byAppending appending: Bool) {
    print("enqueue item \(String(describing: mediaInfo))")
    if let remoteMediaClient = sessionManager.currentCastSession?.remoteMediaClient {
      let mediaQueueItemBuilder = GCKMediaQueueItemBuilder()
      mediaQueueItemBuilder.mediaInformation = mediaInfo
      mediaQueueItemBuilder.autoplay = true
      mediaQueueItemBuilder.preloadTime = TimeInterval(UserDefaults.standard.integer(forKey: kPrefPreloadTime))
      let mediaQueueItem = mediaQueueItemBuilder.build()
      if appending {
        let request = remoteMediaClient.queueInsert(mediaQueueItem, beforeItemWithID: kGCKMediaQueueInvalidItemID)
        request.delegate = self
      } else {
        let queueDataBuilder = GCKMediaQueueDataBuilder(queueType: .generic)
        queueDataBuilder.items = [mediaQueueItem]
        queueDataBuilder.repeatMode = remoteMediaClient.mediaStatus?.queueRepeatMode ?? .off

        let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
        mediaLoadRequestDataBuilder.mediaInformation = mediaInfo
        mediaLoadRequestDataBuilder.queueData = queueDataBuilder.build()

        let request = remoteMediaClient.loadMedia(with: mediaLoadRequestDataBuilder.build())
        request.delegate = self
      }
    }
  }
  ...
}

이제 Cast 세션 로직을 사용하도록 다양한 기존 메서드를 업데이트하여 원격 재생을 지원합니다.

required init?(coder: NSCoder) {
  super.init(coder: coder)
  ...
  castMediaController = GCKUIMediaController()
  ...
}

func switchToLocalPlayback() {
  print("switchToLocalPlayback")
  if playbackMode == .local {
    return
  }
  setQueueButtonVisible(false)
  var playPosition: TimeInterval = 0
  var paused: Bool = false
  var ended: Bool = false
  if playbackMode == .remote {
    playPosition = castMediaController.lastKnownStreamPosition
    paused = (castMediaController.lastKnownPlayerState == .paused)
    ended = (castMediaController.lastKnownPlayerState == .idle)
    print("last player state: \(castMediaController.lastKnownPlayerState), ended: \(ended)")
  }
  populateMediaInfo((!paused && !ended), playPosition: playPosition)
  sessionManager.currentCastSession?.remoteMediaClient?.remove(self)
  playbackMode = .local
}

func switchToRemotePlayback() {
  print("switchToRemotePlayback; mediaInfo is \(String(describing: mediaInfo))")
  if playbackMode == .remote {
    return
  }
  // If we were playing locally, load the local media on the remote player
  if playbackMode == .local, (_localPlayerView.playerState != .stopped), (mediaInfo != nil) {
    print("loading media: \(String(describing: mediaInfo))")
    let paused: Bool = (_localPlayerView.playerState == .paused)
    let mediaQueueItemBuilder = GCKMediaQueueItemBuilder()
    mediaQueueItemBuilder.mediaInformation = mediaInfo
    mediaQueueItemBuilder.autoplay = !paused
    mediaQueueItemBuilder.preloadTime = TimeInterval(UserDefaults.standard.integer(forKey: kPrefPreloadTime))
    mediaQueueItemBuilder.startTime = _localPlayerView.streamPosition ?? 0
    let mediaQueueItem = mediaQueueItemBuilder.build()

    let queueDataBuilder = GCKMediaQueueDataBuilder(queueType: .generic)
    queueDataBuilder.items = [mediaQueueItem]
    queueDataBuilder.repeatMode = .off

    let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
    mediaLoadRequestDataBuilder.queueData = queueDataBuilder.build()

    let request = sessionManager.currentCastSession?.remoteMediaClient?.loadMedia(with: mediaLoadRequestDataBuilder.build())
    request?.delegate = self
  }
  _localPlayerView.stop()
  _localPlayerView.showSplashScreen()
  setQueueButtonVisible(true)
  sessionManager.currentCastSession?.remoteMediaClient?.add(self)
  playbackMode = .remote
}

/* Play has been pressed in the LocalPlayerView. */
func continueAfterPlayButtonClicked() -> Bool {
  let hasConnectedCastSession = sessionManager.hasConnectedCastSession
  if mediaInfo != nil, hasConnectedCastSession() {
    // Display an alert box to allow the user to add to queue or play
    // immediately.
    if actionSheet == nil {
      actionSheet = ActionSheet(title: "Play Item", message: "Select an action", cancelButtonText: "Cancel")
      actionSheet?.addAction(withTitle: "Play Now", target: self,
                             selector: #selector(playSelectedItemRemotely))
    }
    actionSheet?.present(in: self, sourceView: _localPlayerView)
    return false
  }
  return true
}

이제 휴대기기에서 앱을 실행합니다. Cast 기기에 연결하여 동영상 재생을 시작합니다. 수신기에서 재생되는 동영상을 볼 수 있습니다.

7. 미니 컨트롤러

Cast 디자인 체크리스트에 따르면 사용자가 현재 콘텐츠 페이지에서 벗어날 때 모든 Cast 앱이 미니 컨트롤러를 표시해야 합니다. 미니 컨트롤러는 즉시 액세스할 수 있으며 현재 Cast 세션을 시각적으로 표시합니다.

Castvideo 앱을 실행 중인 iPhone 하단 부분의 일러스트레이션으로 미니 컨트롤러에 초점이 맞춰져 있습니다.

Cast SDK는 영구 컨트롤을 표시하려는 장면에 추가할 수 있는 컨트롤 바 GCKUIMiniMediaControlsViewController를 제공합니다.

샘플 앱에서는 GCKUICastContainerViewController를 사용하여 다른 뷰 컨트롤러를 래핑하고 하단에 GCKUIMiniMediaControlsViewController를 추가합니다.

AppDelegate.swift 파일을 수정하고 다음 메서드의 if useCastContainerViewController 조건에 다음 코드를 추가합니다.

func application(_: UIApplication,
                 didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  ...
  let appStoryboard = UIStoryboard(name: "Main", bundle: nil)
  guard let navigationController = appStoryboard.instantiateViewController(withIdentifier: "MainNavigation")
    as? UINavigationController else { return false }
  let castContainerVC = GCKCastContext.sharedInstance().createCastContainerController(for: navigationController)
    as GCKUICastContainerViewController
  castContainerVC.miniMediaControlsItemEnabled = true
  window = UIWindow(frame: UIScreen.main.bounds)
  window?.rootViewController = castContainerVC
  window?.makeKeyAndVisible()
  ...
}

이 속성과 setter/getter를 추가하여 미니 컨트롤러의 공개 상태를 제어합니다 (이후 섹션에서 사용하겠습니다).

var isCastControlBarsEnabled: Bool {
    get {
      if useCastContainerViewController {
        let castContainerVC = (window?.rootViewController as? GCKUICastContainerViewController)
        return castContainerVC!.miniMediaControlsItemEnabled
      } else {
        let rootContainerVC = (window?.rootViewController as? RootContainerViewController)
        return rootContainerVC!.miniMediaControlsViewEnabled
      }
    }
    set(notificationsEnabled) {
      if useCastContainerViewController {
        var castContainerVC: GCKUICastContainerViewController?
        castContainerVC = (window?.rootViewController as? GCKUICastContainerViewController)
        castContainerVC?.miniMediaControlsItemEnabled = notificationsEnabled
      } else {
        var rootContainerVC: RootContainerViewController?
        rootContainerVC = (window?.rootViewController as? RootContainerViewController)
        rootContainerVC?.miniMediaControlsViewEnabled = notificationsEnabled
      }
    }
  }

앱을 실행하고 동영상을 전송합니다. 수신기에서 재생이 시작되면 각 장면 하단에 미니 컨트롤러가 표시됩니다. 미니 컨트롤러를 사용하여 원격 재생을 제어할 수 있습니다. 탐색 활동과 로컬 플레이어 활동 간에 이동하는 경우 미니 컨트롤러 상태는 수신기 미디어 재생 상태와 동기화된 상태로 유지되어야 합니다.

8. 소개 오버레이

Google Cast 디자인 체크리스트에 따르면 발신기 앱은 기존 사용자에게 전송 버튼을 소개하여 이제 발신기 앱에서 전송을 지원하며 Google Cast를 처음 사용하는 사용자도 지원한다는 것을 알려야 합니다.

Cast 버튼 오버레이가 있는 Cast동영상 앱을 실행하는 iPhone의 일러스트레이션. 전송 버튼이 강조 표시되고 '터치하여 TV 및 스피커로 미디어 전송' 메시지가 표시됨

GCKCastContext 클래스에는 전송 버튼이 사용자에게 처음 표시될 때 이를 강조 표시하는 데 사용할 수 있는 presentCastInstructionsViewControllerOnce 메서드가 있습니다. 다음 코드를 MediaViewController.swiftMediaTableViewController.swift에 추가합니다.

override func viewDidLoad() {
  ...

  NotificationCenter.default.addObserver(self, selector: #selector(castDeviceDidChange),
                                         name: NSNotification.Name.gckCastStateDidChange,
                                         object: GCKCastContext.sharedInstance())
}

@objc func castDeviceDidChange(_: Notification) {
  if GCKCastContext.sharedInstance().castState != .noDevicesAvailable {
    // You can present the instructions on how to use Google Cast on
    // the first time the user uses you app
    GCKCastContext.sharedInstance().presentCastInstructionsViewControllerOnce(with: castButton)
  }
}

휴대기기에서 앱을 실행하면 소개 오버레이가 표시됩니다.

9. 확장 컨트롤러

Google Cast 디자인 체크리스트에 따르면 발신기 앱은 전송 중인 미디어에 맞는 확장 컨트롤러를 제공해야 합니다. 확장된 컨트롤러는 미니 컨트롤러의 전체 화면 버전입니다.

동영상이 재생되고 있는 CastVideos 앱을 실행하며 하단에 펼쳐진 컨트롤러의 일러스트레이션

확장된 컨트롤러는 원격 미디어 재생을 완전히 제어할 수 있는 전체 화면 보기입니다. 이 뷰에서는 전송 앱이 전송 세션의 관리 가능한 모든 측면을 관리할 수 있어야 합니다. 단, 수신기 볼륨 조절 및 세션 수명 주기 (전송 연결/중지)는 예외입니다. 미디어 세션 (아트워크, 제목, 자막 등)에 관한 모든 상태 정보도 제공합니다.

이 뷰의 기능은 GCKUIExpandedMediaControlsViewController 클래스로 구현됩니다.

가장 먼저 해야 할 일은 Cast 컨텍스트에서 확장된 기본 컨트롤러를 사용 설정하는 것입니다. 확장된 기본 컨트롤러를 사용 설정하도록 AppDelegate.swift를 수정합니다.

import GoogleCast

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  ...

  func application(_: UIApplication,
                   didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    ...
    // Add after the setShareInstanceWith(options) is set.
    GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true
    ...
  }
  ...
}

사용자가 동영상 전송을 시작하면 확장된 컨트롤러를 로드하도록 MediaViewController.swift에 다음 코드를 추가합니다.

@objc func playSelectedItemRemotely() {
  ...
  appDelegate?.isCastControlBarsEnabled = false
  GCKCastContext.sharedInstance().presentDefaultExpandedMediaControls()
}

또한 사용자가 미니 컨트롤러를 탭하면 확장된 컨트롤러가 자동으로 실행됩니다.

앱을 실행하고 동영상을 전송합니다. 확장 컨트롤러가 표시됩니다. 동영상 목록으로 다시 돌아가 미니 컨트롤러를 클릭하면 확장된 컨트롤러가 다시 로드됩니다.

10. Cast Connect 지원 추가

Cast Connect 라이브러리를 사용하면 기존 발신기 애플리케이션이 Cast 프로토콜을 통해 Android TV 애플리케이션과 통신할 수 있습니다. Cast Connect는 Cast 인프라를 기반으로 빌드되며, Android TV 앱은 수신기 역할을 합니다.

종속 항목

Podfile에서 google-cast-sdk4.4.8 이상을 가리키는지 확인합니다(아래 목록 참고). 파일을 수정한 경우 콘솔에서 pod update를 실행하여 변경사항을 프로젝트와 동기화합니다.

pod 'google-cast-sdk', '>=4.4.8'

GCKLaunchOptions

Android TV 애플리케이션(Android 수신기라고도 함)을 실행하려면 GCKLaunchOptions 객체에서 androidReceiverCompatible 플래그를 true로 설정해야 합니다. 이 GCKLaunchOptions 객체는 broadcast receiver가 실행되는 방식을 지정하고 GCKCastContext.setSharedInstanceWith를 사용하여 공유 인스턴스에 설정된 GCKCastOptions에 전달됩니다.

AppDelegate.swift에 다음 줄을 추가합니다.

let options = GCKCastOptions(discoveryCriteria:
                          GCKDiscoveryCriteria(applicationID: kReceiverAppID))
...
/** Following code enables CastConnect */
let launchOptions = GCKLaunchOptions()
launchOptions.androidReceiverCompatible = true
options.launchOptions = launchOptions

GCKCastContext.setSharedInstanceWith(options)

실행 사용자 인증 정보 설정

발신자 측에서 GCKCredentialsData를 지정하여 세션에 참여하는 사람을 나타낼 수 있습니다. credentials은 ATV 앱에서 이해할 수 있는 한 사용자가 정의할 수 있는 문자열입니다. GCKCredentialsData는 실행 또는 참여 시간에만 Android TV 앱으로 전달됩니다. 연결된 상태에서 다시 설정하면 Android TV 앱으로 전송되지 않습니다.

사용자 인증 정보 시작을 설정하려면 GCKLaunchOptions가 설정된 후 언제든지 GCKCredentialsData을 정의해야 합니다. 이를 설명하기 위해 Creds 버튼의 로직을 추가하여 세션이 설정될 때 함께 전달될 사용자 인증 정보를 설정해 보겠습니다. 다음 코드를 MediaTableViewController.swift에 추가합니다.

class MediaTableViewController: UITableViewController, GCKSessionManagerListener, MediaListModelDelegate, GCKRequestDelegate {
  ...
  private var credentials: String? = nil
  ...
  override func viewDidLoad() {
    ...
    navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Creds", style: .plain,
                                                       target: self, action: #selector(toggleLaunchCreds))
    ...
    setLaunchCreds()
  }
  ...
  @objc func toggleLaunchCreds(_: Any){
    if (credentials == nil) {
        credentials = "{\"userId\":\"id123\"}"
    } else {
        credentials = nil
    }
    Toast.displayMessage("Launch Credentials: "+(credentials ?? "Null"), for: 3, in: appDelegate?.window)
    print("Credentials set: "+(credentials ?? "Null"))
    setLaunchCreds()
  }
  ...
  func setLaunchCreds() {
    GCKCastContext.sharedInstance()
        .setLaunch(GCKCredentialsData(credentials: credentials))
  }
}

로드 요청에서 사용자 인증 정보 설정

웹 앱과 Android TV 수신기 앱에서 모두 credentials를 처리하려면 MediaTableViewController.swift 클래스의 loadSelectedItem 함수 아래에 다음 코드를 추가합니다.

let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
...
mediaLoadRequestDataBuilder.credentials = credentials
...

발신자가 전송하는 수신자 앱에 따라 SDK가 위의 사용자 인증 정보를 진행 중인 세션에 자동으로 적용합니다.

Cast Connect 테스트

Chromecast with Google TV에 Android TV APK를 설치하는 단계

  1. Android TV 기기의 IP 주소를 찾습니다. 일반적으로 설정 > 네트워크 및 인터넷 > (기기가 연결된 네트워크 이름)에서 사용할 수 있습니다. 오른쪽에는 세부정보와 네트워크에 연결된 기기의 IP가 표시됩니다.
  2. 기기의 IP 주소를 사용하여 터미널을 통해 ADB를 통해 기기에 연결합니다.
$ adb connect <device_ip_address>:5555
  1. 터미널 창에서 이 Codelab을 시작할 때 다운로드한 Codelab 샘플의 최상위 폴더로 이동합니다. 예를 들면 다음과 같습니다.
$ cd Desktop/ios_codelab_src
  1. 다음을 실행하여 이 폴더의 .apk 파일을 Android TV에 설치합니다.
$ adb -s <device_ip_address>:5555 install android-tv-app.apk
  1. 이제 Android TV 기기의 내 앱 메뉴에 동영상 전송이라는 이름으로 앱이 표시됩니다.
  2. 완료되면 에뮬레이터나 휴대기기에서 앱을 빌드하고 실행합니다. Android TV 기기로 전송 세션을 설정하면 Android TV에서 Android 수신기 애플리케이션이 실행됩니다. iOS 모바일 발신기에서 동영상을 재생하면 Android 수신기에서 동영상이 실행되고 Android TV 기기용 리모컨으로 재생을 제어할 수 있어야 합니다.

11. Cast 위젯 맞춤설정

초기화

App-Done 폴더로 시작합니다. AppDelegate.swift 파일의 applicationDidFinishLaunchingWithOptions 메서드에 다음을 추가합니다.

func application(_: UIApplication,
                 didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  ...
  let styler = GCKUIStyle.sharedInstance()
  ...
}

이 Codelab의 나머지 부분에서 언급한 대로 하나 이상의 맞춤설정을 적용한 후에는 아래 코드를 호출하여 스타일을 커밋합니다.

styler.apply()

Cast 보기 맞춤설정

여러 뷰에 기본 스타일 지정 가이드라인을 적용하여 Cast 애플리케이션 프레임워크가 관리하는 모든 뷰를 맞춤설정할 수 있습니다. 예를 들어 아이콘 색조 색상을 변경해 보겠습니다.

styler.castViews.iconTintColor = .lightGray

필요한 경우 화면별로 기본값을 재정의할 수 있습니다. 예를 들어 확장된 미디어 컨트롤러만을 대상으로 아이콘 색조 색상의 LightgreyColor를 재정의하는 것입니다.

styler.castViews.mediaControl.expandedController.iconTintColor = .green

색상 변경

모든 보기의 배경 색상을 맞춤설정하거나 보기마다 개별적으로 맞춤설정할 수 있습니다. 다음 코드는 모든 Cast 애플리케이션 프레임워크 제공 보기에서 배경색을 파란색으로 설정합니다.

styler.castViews.backgroundColor = .blue
styler.castViews.mediaControl.miniController.backgroundColor = .yellow

글꼴 변경

전송 보기 내에 표시되는 다양한 라벨의 글꼴을 맞춤설정할 수 있습니다. 설명을 위해 모든 글꼴을 ‘Courier-Oblique'로 설정해 보겠습니다.

styler.castViews.headingTextFont = UIFont.init(name: "Courier-Oblique", size: 16) ?? UIFont.systemFont(ofSize: 16)
styler.castViews.mediaControl.headingTextFont = UIFont.init(name: "Courier-Oblique", size: 6) ?? UIFont.systemFont(ofSize: 6)

기본 버튼 이미지 변경

프로젝트에 자체 커스텀 이미지를 추가하고 스타일을 지정할 수 있도록 버튼에 이미지를 할당합니다.

let muteOnImage = UIImage.init(named: "yourImage.png")
if let muteOnImage = muteOnImage {
  styler.castViews.muteOnImage = muteOnImage
}

전송 버튼 테마 변경

UIAppearance 프로토콜을 사용하여 Cast 위젯의 테마를 설정할 수도 있습니다. 다음 코드는 표시되는 모든 뷰에서 GCKUICastButton의 테마를 설정합니다.

GCKUICastButton.appearance().tintColor = UIColor.gray

12. 축하합니다

지금까지 iOS에서 Cast SDK 위젯을 사용하여 동영상 앱을 Cast 지원으로 사용 설정하는 방법을 알아보았습니다.

자세한 내용은 iOS Sender 개발자 가이드를 참고하세요.