iOS 앱에서 Cast 지원 사용

1. 개요

Google Cast 로고

이 Codelab에서는 기존 iOS 동영상 앱을 수정하여 Google Cast 지원 기기에서 콘텐츠를 전송하는 방법을 알아봅니다.

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 지원 기기가 없는 경우 이 튜토리얼 끝부분의 Cast Connect 지원 추가 단계를 건너뛰어도 됩니다.

경험

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

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

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

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

초급 중급 고급

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

초보자 중급 숙련도

2. 샘플 코드 가져오기

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

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

3. 샘플 앱 실행

Apple iOS 로고

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

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

자주 묻는 질문(FAQ)

CocoaPods 설정

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

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 기기에서 동영상이 재생되기 시작합니다.

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

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

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

CastVideos 앱을 실행 중인 iPhone의 그림으로, 하단에 미니 컨트롤러가 표시되어 있습니다.

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

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

4. 시작 프로젝트 준비

iPhone에서 CastVideos 앱을 실행하는 모습을 보여주는 삽화

다운로드한 시작 앱에 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. 전송 버튼 추가

오른쪽 상단에 전송 버튼이 표시된 CastVideos 앱을 실행 중인 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. 동영상 콘텐츠 전송

CastVideos 앱을 실행 중인 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()
  }

  ...
}

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

현재 활성 세션에는 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 세션을 시각적으로 표시합니다.

미니 컨트롤러에 초점을 맞추고 있는 CastVideos 앱을 실행하는 iPhone의 하단 이미지

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

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

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를 처음 접하는 사용자도 지원한다는 것을 알려야 합니다.

전송 버튼 오버레이가 있는 CastVideos 앱을 실행하는 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 앱을 실행 중인 iPhone에서 하단에 확장 컨트롤러가 표시된 동영상을 재생하는 일러스트레이션

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

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

가장 먼저 해야 할 일은 확장된 기본 컨트롤러를 전송 컨텍스트에서 사용 설정하는 것입니다. 기본 확장 컨트롤러를 사용 설정하도록 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-sdk가 아래와 같이 4.4.8 이상을 가리키는지 확인합니다. 파일을 수정한 경우 콘솔에서 pod update를 실행하여 변경사항을 프로젝트와 동기화합니다.

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

GCKLaunchOptions

Android TV 애플리케이션(Android 수신기라고도 함)을 실행하려면 GCKLaunchOptions 객체에서 androidReceiverCompatible 플래그를 true로 설정해야 합니다. 이 GCKLaunchOptions 객체는 수신기가 실행되는 방식을 지정하고 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를 처리하려면 loadSelectedItem 함수의 MediaTableViewController.swift 클래스에 다음 코드를 추가합니다.

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 기기의 내 앱 메뉴에 Cast 동영상이라는 앱이 표시됩니다.
  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

필요한 경우 화면별로 기본값을 재정의할 수 있습니다. 예를 들어 확장된 미디어 컨트롤러의 아이콘 색조 색상에 대한 LightGrayColor를 재정의하려면 다음과 같이 합니다.

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 발신자 개발자 가이드를 참조하세요.