Cast 지원 Android 앱

1. 개요

Google Cast 로고

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

Google Cast란 무엇인가요?

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

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

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

무엇을 빌드하게 되나요?

이 Codelab을 완료하면 Google Cast 지원 기기로 동영상을 전송할 수 있는 Android 동영상 앱이 생성됩니다.

학습할 내용

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

필요한 항목

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

환경

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

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

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

본인의 Android 앱 빌드 경험을 평가해 주세요.

초급 중급 고급

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

초급 중급 고급

2. 샘플 코드 가져오기

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

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

3. 샘플 앱 실행

한 쌍의 나침반 아이콘

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

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

시작 화면에서 Import Project 또는 File > New > Import Project... 메뉴 옵션을 선택합니다.

샘플 코드 폴더에서 폴더 아이콘app-done 디렉터리를 선택하고 OK를 클릭합니다.

File > Android 스튜디오의 'Sync Project with Gradle' 버튼 Sync Project with Gradle Files를 클릭합니다.

Android 기기에서 USB 디버깅 사용 설정: Android 4.2 이상 버전에서는 개발자 옵션 화면이 기본적으로 숨겨져 있습니다. 이 옵션을 표시하려면 설정 > 휴대전화 정보로 이동하여 빌드 번호를 일곱 번 탭합니다. 이전 화면으로 돌아가서 시스템 > 고급으로 이동하고 화면 하단의 개발자 옵션을 탭한 다음 USB 디버깅을 탭하여 사용 설정합니다.

Android 기기를 연결하고 Android 스튜디오에서 Android 스튜디오의 Run 버튼, 오른쪽을 가리키는 녹색 삼각형Run 버튼을 클릭합니다. 몇 초 후에 Cast Videos라는 동영상 앱이 표시됩니다.

동영상 앱에서 전송 버튼을 클릭하고 Google Cast 기기를 선택합니다.

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

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

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

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

이제 미니 컨트롤러가 화면 하단에 표시됩니다. '동영상 전송' 앱을 실행 중인 Android 휴대전화에서 화면 하단에 미니 컨트롤러가 표시된 일러스트레이션

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

휴대기기의 홈 버튼을 클릭합니다. 알림을 끌어서 내리면 Cast 세션에 관한 알림이 표시됩니다.

휴대전화를 잠갔다 잠금 해제하면 미디어 재생을 제어하거나 전송을 중단하기 위한 알림이 잠금 화면에 표시됩니다.

동영상 앱으로 돌아가서 전송 버튼을 클릭하여 Google Cast 기기로의 전송을 중지합니다.

자주 묻는 질문(FAQ)

4. 시작 프로젝트 준비

'Cast 동영상' 앱을 실행하는 Android 휴대전화 삽화

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

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

이제 Android 스튜디오를 사용하여 시작 프로젝트를 토대로 빌드할 준비가 되었습니다.

  1. 샘플 코드 다운로드에서 폴더 아이콘app-start 디렉터리를 선택합니다 (시작 화면에서 Import Project 선택 또는 File > New > Import Project... 메뉴 옵션 선택).
  2. Android 스튜디오의 'Sync Project with Gradle' 버튼 Sync Project with Gradle Files 버튼을 클릭합니다.
  3. Android 스튜디오의 Run 버튼, 오른쪽을 가리키는 녹색 삼각형Run 버튼을 클릭하여 앱을 실행하고 UI를 탐색합니다.

앱 디자인

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

앱은 두 가지 기본 활동 VideoBrowserActivityLocalPlayerActivity로 구성됩니다. Google Cast 기능을 통합하려면 활동AppCompatActivity 또는 상위 FragmentActivity에서 상속해야 합니다. 이러한 제한사항은 MediaRouteButton (MediaRouter 지원 라이브러리에서 제공됨)를 MediaRouteActionProvider로 추가해야 하기 때문에 존재하며 활동이 위에 언급된 클래스에서 상속되는 경우에만 작동합니다. MediaRouter 지원 라이브러리는 필수 클래스를 제공하는 AppCompat 지원 라이브러리에 종속됩니다.

VideoBrowserActivity

이 활동에는 Fragment (VideoBrowserFragment)가 포함되어 있습니다. 이 목록은 ArrayAdapter (VideoListAdapter)로 지원됩니다. 동영상 및 관련 메타데이터의 목록은 원격 서버에서 JSON 파일로 호스팅됩니다. AsyncTaskLoader (VideoItemLoader)는 이 JSON을 가져와서 처리하여 MediaItem 객체 목록을 빌드합니다.

MediaItem 객체는 동영상 및 관련 메타데이터(예: 제목, 설명, 스트림, URL, 지원 이미지 URL, 관련 텍스트 트랙(자막용))가 있는 경우 이를 모델링합니다. MediaItem 객체는 활동 간에 전달되므로 MediaItem에는 이를 Bundle로 변환하거나 그 반대로 변환하는 유틸리티 메서드가 있습니다.

로더가 MediaItems 목록을 빌드하면 이 목록을 전달받은 VideoListAdapterVideoBrowserFragmentMediaItems 목록을 표시합니다. 사용자에게는 각 동영상에 관한 간단한 설명과 함께 동영상 미리보기 이미지 목록이 표시됩니다. 항목을 선택하면 관련 MediaItemBundle로 변환되고 LocalPlayerActivity로 전달됩니다.

LocalPlayerActivity

이 활동은 특정 동영상에 관한 메타데이터를 표시하고 사용자가 휴대기기에서 로컬로 동영상을 재생할 수 있도록 합니다.

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

종속 항목

AppCompatActivity를 사용하고 있으므로 AppCompat 지원 라이브러리가 필요합니다. 동영상 목록을 관리하고 목록의 이미지를 비동기식으로 가져오기 위해 Volley 라이브러리를 사용합니다.

자주 묻는 질문(FAQ)

5. 전송 버튼 추가

전송 동영상 앱이 실행 중인 Android 휴대전화 상단 일러스트레이션. 화면 오른쪽 상단에 전송 버튼이 표시됨

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

종속 항목

필요한 라이브러리 종속 항목을 포함하도록 앱 build.gradle 파일을 업데이트합니다.

dependencies {
    implementation 'androidx.appcompat:appcompat:1.5.0'
    implementation 'androidx.mediarouter:mediarouter:1.3.1'
    implementation 'androidx.recyclerview:recyclerview:1.2.1'
    implementation 'com.google.android.gms:play-services-cast-framework:21.1.0'
    implementation 'com.android.volley:volley:1.2.1'
    implementation "androidx.core:core-ktx:1.8.0"
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}

프로젝트를 동기화하여 오류 없이 프로젝트가 빌드되는지 확인합니다.

초기화

Cast 프레임워크에는 모든 전송 상호작용을 조정하는 전역 싱글톤 객체 CastContext가 있습니다.

OptionsProvider 인터페이스를 구현하여 CastContext 싱글톤을 초기화하는 데 필요한 CastOptions를 제공해야 합니다. 가장 중요한 옵션은 Cast 기기 검색 결과를 필터링하고 Cast 세션이 시작될 때 수신기 애플리케이션을 실행하는 데 사용되는 수신기 애플리케이션 ID입니다.

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

다음과 같은 새 CastOptionsProvider.kt 파일을 프로젝트의 com.google.sample.cast.refplayer 패키지에 추가합니다.

package com.google.sample.cast.refplayer

import android.content.Context
import com.google.android.gms.cast.framework.OptionsProvider
import com.google.android.gms.cast.framework.CastOptions
import com.google.android.gms.cast.framework.SessionProvider

class CastOptionsProvider : OptionsProvider {
    override fun getCastOptions(context: Context): CastOptions {
        return CastOptions.Builder()
                .setReceiverApplicationId(context.getString(R.string.app_id))
                .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}

이제 앱 AndroidManifest.xml 파일의 'application' 태그 내에 OptionsProvider를 선언합니다.

<meta-data
    android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
    android:value="com.google.sample.cast.refplayer.CastOptionsProvider" />

VideoBrowserActivity onCreate 메서드에서 CastContext를 천천히 초기화합니다.

import com.google.android.gms.cast.framework.CastContext

private var mCastContext: CastContext? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.video_browser)
    setupActionBar()

    mCastContext = CastContext.getSharedInstance(this)
}

동일한 초기화 로직을 LocalPlayerActivity에 추가합니다.

전송 버튼

이제 CastContext가 초기화되었으므로 전송 버튼을 추가하여 사용자가 Cast 기기를 선택할 수 있도록 해야 합니다. 전송 버튼은 MediaRouter 지원 라이브러리의 MediaRouteButton를 통해 구현됩니다. 활동에 추가할 수 있는 다른 작업 아이콘 (ActionBar 또는 Toolbar 사용)과 마찬가지로 먼저 상응하는 메뉴 항목을 메뉴에 추가해야 합니다.

res/menu/browse.xml 파일을 수정하고 메뉴에서 설정 항목 앞에 MediaRouteActionProvider 항목을 추가합니다.

<item
    android:id="@+id/media_route_menu_item"
    android:title="@string/media_route_menu_title"
    app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
    app:showAsAction="always"/>

CastButtonFactory를 사용하여 VideoBrowserActivityonCreateOptionsMenu() 메서드를 재정의하여 MediaRouteButton를 Cast 프레임워크에 연결합니다.

import com.google.android.gms.cast.framework.CastButtonFactory

private var mediaRouteMenuItem: MenuItem? = null

override fun onCreateOptionsMenu(menu: Menu): Boolean {
     super.onCreateOptionsMenu(menu)
     menuInflater.inflate(R.menu.browse, menu)
     mediaRouteMenuItem = CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu,
                R.id.media_route_menu_item)
     return true
}

비슷한 방식으로 LocalPlayerActivityonCreateOptionsMenu를 재정의합니다.

Android 스튜디오의 Run 버튼, 오른쪽을 가리키는 녹색 삼각형Run 버튼을 클릭하여 휴대기기에서 앱을 실행합니다. 앱의 작업 모음에 전송 버튼이 표시되며, 이 버튼을 클릭하면 로컬 네트워크에 Cast 기기가 표시됩니다. 기기 검색은 CastContext에서 자동으로 관리됩니다. Cast 기기를 선택하면 샘플 수신기 앱이 Cast 기기에 로드됩니다. 탐색 활동과 로컬 플레이어 활동을 오가며 둘러볼 수 있으며 전송 버튼 상태는 동기화된 상태로 유지됩니다.

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

6. 동영상 콘텐츠 전송

&#39;Cast 동영상&#39; 앱을 실행하는 Android 휴대전화 삽화

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

미디어 전송

Cast 기기에서 미디어를 재생하려면 대략적으로 다음 단계를 따라야 합니다.

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

이전 섹션에서 이미 2단계를 완료했습니다. 3단계는 Cast 프레임워크에서 쉽게 할 수 있습니다. 1단계는 한 객체를 다른 객체에 매핑하는 것입니다. MediaInfo는 Cast 프레임워크가 이해할 수 있는 내용이고 MediaItem은 미디어 항목에 관한 앱의 캡슐화입니다. 따라서 MediaItemMediaInfo에 쉽게 매핑할 수 있습니다.

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

private var mLocation: PlaybackLocation? = null

enum class PlaybackLocation {
    LOCAL, REMOTE
}

enum class PlaybackState {
    PLAYING, PAUSED, BUFFERING, IDLE
}

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

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

전송 세션 관리

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

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

LocalPlayerActivitySessionManagerListener를 추가해 보겠습니다.

import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.cast.framework.SessionManagerListener
...

private var mSessionManagerListener: SessionManagerListener<CastSession>? = null
private var mCastSession: CastSession? = null
...

private fun setupCastListener() {
    mSessionManagerListener = object : SessionManagerListener<CastSession> {
        override fun onSessionEnded(session: CastSession, error: Int) {
            onApplicationDisconnected()
        }

        override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) {
            onApplicationConnected(session)
        }

        override fun onSessionResumeFailed(session: CastSession, error: Int) {
            onApplicationDisconnected()
        }

        override fun onSessionStarted(session: CastSession, sessionId: String) {
            onApplicationConnected(session)
        }

        override fun onSessionStartFailed(session: CastSession, error: Int) {
            onApplicationDisconnected()
        }

        override fun onSessionStarting(session: CastSession) {}
        override fun onSessionEnding(session: CastSession) {}
        override fun onSessionResuming(session: CastSession, sessionId: String) {}
        override fun onSessionSuspended(session: CastSession, reason: Int) {}
        private fun onApplicationConnected(castSession: CastSession) {
            mCastSession = castSession
            if (null != mSelectedMedia) {
                if (mPlaybackState == PlaybackState.PLAYING) {
                    mVideoView!!.pause()
                    loadRemoteMedia(mSeekbar!!.progress, true)
                    return
                } else {
                    mPlaybackState = PlaybackState.IDLE
                    updatePlaybackLocation(PlaybackLocation.REMOTE)
                }
            }
            updatePlayButton(mPlaybackState)
            invalidateOptionsMenu()
        }

        private fun onApplicationDisconnected() {
            updatePlaybackLocation(PlaybackLocation.LOCAL)
            mPlaybackState = PlaybackState.IDLE
            mLocation = PlaybackLocation.LOCAL
            updatePlayButton(mPlaybackState)
            invalidateOptionsMenu()
       }
   }
}

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

현재 활성 세션에는 SessionManager.getCurrentSession()으로 액세스할 수 있습니다. 세션은 사용자의 Cast 대화상자 상호작용에 반응하여 자동으로 생성되고 해제됩니다.

세션 리스너를 등록하고 활동에서 사용할 일부 변수를 초기화해야 합니다. LocalPlayerActivity onCreate 메서드를 다음으로 변경합니다.

import com.google.android.gms.cast.framework.CastContext
...

private var mCastContext: CastContext? = null
...

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    mCastContext = CastContext.getSharedInstance(this)
    mCastSession = mCastContext!!.sessionManager.currentCastSession
    setupCastListener()
    ...
    loadViews()
    ...
    val bundle = intent.extras
    if (bundle != null) {
        ....
        if (shouldStartPlayback) {
              ....

        } else {
            if (mCastSession != null && mCastSession!!.isConnected()) {
                updatePlaybackLocation(PlaybackLocation.REMOTE)
            } else {
                updatePlaybackLocation(PlaybackLocation.LOCAL)
            }
            mPlaybackState = PlaybackState.IDLE
            updatePlayButton(mPlaybackState)
        }
    }
    ...
}

미디어 로드

Cast SDK에서 RemoteMediaClient는 수신기에서 원격 미디어 재생을 편리하게 관리할 수 있는 API 집합을 제공합니다. 미디어 재생을 지원하는 CastSession의 경우 RemoteMediaClient 인스턴스가 SDK에 의해 자동으로 생성됩니다. 이 인스턴스에는 CastSession 인스턴스에서 getRemoteMediaClient() 메서드를 호출하여 액세스할 수 있습니다. 현재 선택한 동영상을 수신기에 로드하려면 LocalPlayerActivity에 다음 메서드를 추가합니다.

import com.google.android.gms.cast.framework.media.RemoteMediaClient
import com.google.android.gms.cast.MediaInfo
import com.google.android.gms.cast.MediaLoadOptions
import com.google.android.gms.cast.MediaMetadata
import com.google.android.gms.common.images.WebImage
import com.google.android.gms.cast.MediaLoadRequestData

private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    if (mCastSession == null) {
        return
    }
    val remoteMediaClient = mCastSession!!.remoteMediaClient ?: return
    remoteMediaClient.load( MediaLoadRequestData.Builder()
                .setMediaInfo(buildMediaInfo())
                .setAutoplay(autoPlay)
                .setCurrentTime(position.toLong()).build())
}

private fun buildMediaInfo(): MediaInfo? {
    val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)
    mSelectedMedia?.studio?.let { movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, it) }
    mSelectedMedia?.title?.let { movieMetadata.putString(MediaMetadata.KEY_TITLE, it) }
    movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia!!.getImage(0))))
    movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia!!.getImage(1))))
    return mSelectedMedia!!.url?.let {
        MediaInfo.Builder(it)
            .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
            .setContentType("videos/mp4")
            .setMetadata(movieMetadata)
            .setStreamDuration((mSelectedMedia!!.duration * 1000).toLong())
            .build()
    }
}

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

private fun play(position: Int) {
    startControllersTimer()
    when (mLocation) {
        PlaybackLocation.LOCAL -> {
            mVideoView!!.seekTo(position)
            mVideoView!!.start()
        }
        PlaybackLocation.REMOTE -> {
            mPlaybackState = PlaybackState.BUFFERING
            updatePlayButton(mPlaybackState)
            //seek to a new position within the current media item's new position 
            //which is in milliseconds from the beginning of the stream
            mCastSession!!.remoteMediaClient?.seek(position.toLong())
        }
        else -> {}
    }
    restartTrickplayTimer()
}
private fun togglePlayback() {
    ...
    PlaybackState.IDLE -> when (mLocation) {
        ...
        PlaybackLocation.REMOTE -> {
            if (mCastSession != null && mCastSession!!.isConnected) {
                loadRemoteMedia(mSeekbar!!.progress, true)
            }
        }
        else -> {}
    }
    ...
}
override fun onPause() {
    ...
    mCastContext!!.sessionManager.removeSessionManagerListener(
                mSessionManagerListener!!, CastSession::class.java)
}
override fun onResume() {
    Log.d(TAG, "onResume() was called")
    mCastContext!!.sessionManager.addSessionManagerListener(
            mSessionManagerListener!!, CastSession::class.java)
    if (mCastSession != null && mCastSession!!.isConnected) {
        updatePlaybackLocation(PlaybackLocation.REMOTE)
    } else {
        updatePlaybackLocation(PlaybackLocation.LOCAL)
    }
    super.onResume()
}

updatePlayButton 메서드의 경우 isConnected 변수의 값을 변경합니다.

private fun updatePlayButton(state: PlaybackState?) {
    ...
    val isConnected = (mCastSession != null
                && (mCastSession!!.isConnected || mCastSession!!.isConnecting))
    ...
}

이제 Android 스튜디오의 Run 버튼, 오른쪽을 가리키는 녹색 삼각형Run 버튼을 클릭하여 휴대기기에서 앱을 실행합니다. Cast 기기에 연결하여 동영상 재생을 시작합니다. 수신기에서 재생되는 동영상을 볼 수 있습니다.

7. 미니 컨트롤러

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

Cast 동영상 앱의 소형 플레이어를 보여주는 Android 휴대전화 하단 일러스트레이션

Cast SDK는 맞춤 뷰 MiniControllerFragment를 제공합니다. 이 뷰는 미니 컨트롤러를 표시하려는 활동의 앱 레이아웃 파일에 추가할 수 있습니다.

res/layout/player_activity.xmlres/layout/video_browser.xml의 하단에 다음 프래그먼트 정의를 추가합니다.

<fragment
    android:id="@+id/castMiniController"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:visibility="gone"
    class="com.google.android.gms.cast.framework.media.widget.MiniControllerFragment"/>

Android 스튜디오의 Run 버튼, 오른쪽을 가리키는 녹색 삼각형Run 버튼을 클릭하여 앱을 실행하고 동영상을 전송합니다. 수신기에서 재생이 시작되면 각 활동 하단에 미니 컨트롤러가 나타납니다. 미니 컨트롤러를 사용하여 원격 재생을 제어할 수 있습니다. 탐색 활동과 로컬 플레이어 활동 간에 이동하는 경우 미니 컨트롤러 상태는 수신기 미디어 재생 상태와 동기화된 상태로 유지되어야 합니다.

8. 알림 및 잠금 화면

Google Cast 디자인 체크리스트에 따르면 발신기 앱은 알림잠금 화면에서 미디어 컨트롤을 구현해야 합니다.

Android 휴대전화의 알림 영역에 미디어 컨트롤이 표시된 일러스트레이션

Cast SDK는 발신기 앱에서 알림 및 잠금 화면을 위한 미디어 컨트롤을 빌드할 수 있도록 MediaNotificationService를 제공합니다. 이 서비스는 Gradle에 의해 앱의 매니페스트에 자동으로 병합됩니다.

발신기에서 전송하는 중에 MediaNotificationService가 백그라운드에서 실행되며 현재 전송 항목, 재생/일시중지 버튼, 정지 버튼에 관한 미리보기 이미지 및 메타데이터가 포함된 알림이 표시됩니다.

CastContext를 초기화할 때 CastOptions를 사용하여 알림 및 잠금 화면 컨트롤을 사용 설정할 수 있습니다. 알림과 잠금 화면용 미디어 컨트롤은 기본적으로 사용 설정됩니다. 알림이 사용 설정되어 있으면 잠금 화면 기능도 사용 설정되어 있습니다.

CastOptionsProvider를 수정하고 getCastOptions 구현을 이 코드와 일치하도록 변경합니다.

import com.google.android.gms.cast.framework.media.CastMediaOptions
import com.google.android.gms.cast.framework.media.NotificationOptions

override fun getCastOptions(context: Context): CastOptions {
   val notificationOptions = NotificationOptions.Builder()
            .setTargetActivityClassName(VideoBrowserActivity::class.java.name)
            .build()
    val mediaOptions = CastMediaOptions.Builder()
            .setNotificationOptions(notificationOptions)
            .build()
   return CastOptions.Builder()
                .setReceiverApplicationId(context.getString(R.string.app_id))
                .setCastMediaOptions(mediaOptions)
                .build()
}

Android 스튜디오의 Run 버튼, 오른쪽을 가리키는 녹색 삼각형Run 버튼을 클릭하여 휴대기기에서 앱을 실행합니다. 동영상을 전송하고 샘플 앱을 벗어나 이동합니다. 그러면 현재 수신기에서 재생 중인 동영상에 관한 알림이 표시됩니다. 휴대기기를 잠그면 이제 잠금 화면에 Cast 기기의 미디어 재생에 관한 컨트롤이 표시됩니다.

잠금 화면에 미디어 컨트롤이 표시된 Android 휴대전화의 일러스트레이션

9. 소개 오버레이

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

Cast 동영상 Android 앱의 전송 버튼 주위에 소개된 전송 오버레이를 보여주는 그림

Cast SDK는 맞춤 보기 IntroductoryOverlay를 제공합니다. 이 보기는 사용자에게 처음 표시될 때 전송 버튼을 강조 표시하는 데 사용할 수 있습니다. VideoBrowserActivity에 다음 코드를 추가합니다.

import com.google.android.gms.cast.framework.IntroductoryOverlay
import android.os.Looper

private var mIntroductoryOverlay: IntroductoryOverlay? = null

private fun showIntroductoryOverlay() {
    mIntroductoryOverlay?.remove()
    if (mediaRouteMenuItem?.isVisible == true) {
       Looper.myLooper().run {
           mIntroductoryOverlay = com.google.android.gms.cast.framework.IntroductoryOverlay.Builder(
                    this@VideoBrowserActivity, mediaRouteMenuItem!!)
                   .setTitleText("Introducing Cast")
                   .setSingleTime()
                   .setOnOverlayDismissedListener(
                           object : IntroductoryOverlay.OnOverlayDismissedListener {
                               override fun onOverlayDismissed() {
                                   mIntroductoryOverlay = null
                               }
                          })
                   .build()
          mIntroductoryOverlay!!.show()
        }
    }
}

이제 onCreate 메서드를 수정하고 다음과 같이 onResumeonPause 메서드를 재정의하여 Cast 기기를 사용할 수 있을 때 CastStateListener를 추가하고 showIntroductoryOverlay 메서드를 호출합니다.

import com.google.android.gms.cast.framework.CastState
import com.google.android.gms.cast.framework.CastStateListener

private var mCastStateListener: CastStateListener? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.video_browser)
    setupActionBar()
    mCastStateListener = object : CastStateListener {
            override fun onCastStateChanged(newState: Int) {
                if (newState != CastState.NO_DEVICES_AVAILABLE) {
                    showIntroductoryOverlay()
                }
            }
        }
    mCastContext = CastContext.getSharedInstance(this)
}

override fun onResume() {
    super.onResume()
    mCastContext?.addCastStateListener(mCastStateListener!!)
}

override fun onPause() {
    super.onPause()
    mCastContext?.removeCastStateListener(mCastStateListener!!)
}

앱 데이터를 지우거나 기기에서 앱을 삭제합니다. 그런 다음 Android 스튜디오의 Run 버튼, 오른쪽을 가리키는 녹색 삼각형Run 버튼을 클릭하여 휴대기기에서 앱을 실행하면 소개 오버레이가 표시됩니다 (오버레이가 표시되지 않는 경우 앱 데이터 삭제).

10. 확장 컨트롤러

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

Android 휴대전화에서 재생되는 동영상에 확장 컨트롤러가 오버레이되는 삽화

Cast SDK는 ExpandedControllerActivity라는 확장 컨트롤러용 위젯을 제공합니다. 이는 추상 클래스로 Cast 버튼을 추가하기 위해 서브클래스로 선언해야 합니다.

먼저 확장된 컨트롤러에서 전송 버튼을 제공하도록 expanded_controller.xml이라는 새 메뉴 리소스 파일을 만듭니다.

<?xml version="1.0" encoding="utf-8"?>

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
            android:id="@+id/media_route_menu_item"
            android:title="@string/media_route_menu_title"
            app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
            app:showAsAction="always"/>

</menu>

com.google.sample.cast.refplayer 패키지에 새 패키지 expandedcontrols를 만듭니다. 다음으로 com.google.sample.cast.refplayer.expandedcontrols 패키지에 ExpandedControlsActivity.kt라는 새 파일을 만듭니다.

package com.google.sample.cast.refplayer.expandedcontrols

import android.view.Menu
import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity
import com.google.sample.cast.refplayer.R
import com.google.android.gms.cast.framework.CastButtonFactory

class ExpandedControlsActivity : ExpandedControllerActivity() {
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        super.onCreateOptionsMenu(menu)
        menuInflater.inflate(R.menu.expanded_controller, menu)
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
        return true
    }
}

이제 OPTIONS_PROVIDER_CLASS_NAME 위의 application 태그 내 AndroidManifest.xml에서 ExpandedControlsActivity를 선언합니다.

<application>
    ...
    <activity
        android:name="com.google.sample.cast.refplayer.expandedcontrols.ExpandedControlsActivity"
        android:label="@string/app_name"
        android:launchMode="singleTask"
        android:theme="@style/Theme.CastVideosDark"
        android:screenOrientation="portrait"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
        </intent-filter>
        <meta-data
            android:name="android.support.PARENT_ACTIVITY"
            android:value="com.google.sample.cast.refplayer.VideoBrowserActivity"/>
    </activity>
    ...
</application>

CastOptionsProvider를 수정하고 NotificationOptionsCastMediaOptions를 변경하여 타겟 활동을 ExpandedControlsActivity로 설정합니다.

import com.google.sample.cast.refplayer.expandedcontrols.ExpandedControlsActivity

override fun getCastOptions(context: Context): CastOptions {
    val notificationOptions = NotificationOptions.Builder()
            .setTargetActivityClassName(ExpandedControlsActivity::class.java.name)
            .build()
    val mediaOptions = CastMediaOptions.Builder()
            .setNotificationOptions(notificationOptions)
            .setExpandedControllerActivityClassName(ExpandedControlsActivity::class.java.name)
            .build()
    return CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setCastMediaOptions(mediaOptions)
            .build()
}

원격 미디어가 로드될 때 ExpandedControlsActivity를 표시하도록 LocalPlayerActivity loadRemoteMedia 메서드를 업데이트합니다.

import com.google.sample.cast.refplayer.expandedcontrols.ExpandedControlsActivity

private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    if (mCastSession == null) {
        return
    }
    val remoteMediaClient = mCastSession!!.remoteMediaClient ?: return
    remoteMediaClient.registerCallback(object : RemoteMediaClient.Callback() {
        override fun onStatusUpdated() {
            val intent = Intent(this@LocalPlayerActivity, ExpandedControlsActivity::class.java)
            startActivity(intent)
            remoteMediaClient.unregisterCallback(this)
        }
    })
    remoteMediaClient.load(MediaLoadRequestData.Builder()
                .setMediaInfo(buildMediaInfo())
                .setAutoplay(autoPlay)
                .setCurrentTime(position.toLong()).build())
}

Android 스튜디오의 Run 버튼, 오른쪽을 가리키는 녹색 삼각형Run 버튼을 클릭하여 휴대기기에서 앱을 실행하고 동영상을 전송합니다. 확장 컨트롤러가 표시됩니다. 동영상 목록으로 다시 돌아가 미니 컨트롤러를 클릭하면 확장된 컨트롤러가 다시 로드됩니다. 앱에서 벗어나 알림을 확인합니다. 알림 이미지를 클릭하여 확장된 컨트롤러를 로드합니다.

11. Cast Connect 지원 추가

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

종속 항목

참고: Cast Connect를 구현하려면 play-services-cast-framework19.0.0 이상이어야 합니다.

LaunchOptions

Android TV 애플리케이션(Android 수신기라고도 함)을 실행하려면 LaunchOptions 객체에서 setAndroidReceiverCompatible 플래그를 true로 설정해야 합니다. 이 LaunchOptions 객체는 broadcast receiver가 실행되는 방식을 지정하고 CastOptionsProvider 클래스에서 반환한 CastOptions에 전달됩니다. 위에서 언급된 플래그를 false로 설정하면 Cast Play Console에서 정의된 앱 ID의 웹 수신기가 실행됩니다.

CastOptionsProvider.kt 파일에서 getCastOptions 메서드에 다음을 추가합니다.

import com.google.android.gms.cast.LaunchOptions
...
val launchOptions = LaunchOptions.Builder()
            .setAndroidReceiverCompatible(true)
            .build()
return new CastOptions.Builder()
        .setLaunchOptions(launchOptions)
        ...
        .build()

실행 사용자 인증 정보 설정

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

출시 사용자 인증 정보를 설정하려면 CredentialsData를 정의하여 LaunchOptions 객체에 전달해야 합니다. 다음 코드를 CastOptionsProvider.kt 파일의 getCastOptions 메서드에 추가합니다.

import com.google.android.gms.cast.CredentialsData
...

val credentialsData = CredentialsData.Builder()
        .setCredentials("{\"userId\": \"abc\"}")
        .build()
val launchOptions = LaunchOptions.Builder()
       ...
       .setCredentialsData(credentialsData)
       .build()

LoadRequest에서 사용자 인증 정보 설정

웹 수신기 앱과 Android TV 앱이 credentials를 다르게 처리하는 경우 각각에 별도의 credentials를 정의해야 할 수도 있습니다. 이 문제를 처리하려면 LocalPlayerActivity.kt 파일의 loadRemoteMedia 함수 아래에 다음 코드를 추가합니다.

remoteMediaClient.load(MediaLoadRequestData.Builder()
       ...
       .setCredentials("user-credentials")
       .setAtvCredentials("atv-user-credentials")
       .build())

발신자가 전송하는 수신 앱에 따라 이제 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/android_codelab_src
  1. 다음을 실행하여 이 폴더의 .apk 파일을 Android TV에 설치합니다.
$ adb -s <device_ip_address>:5555 install android-tv-app.apk
  1. 이제 Android TV 기기의 내 앱 메뉴에 동영상 전송이라는 이름으로 앱이 표시됩니다.
  2. Android 스튜디오 프로젝트로 돌아간 다음 Run 버튼을 클릭하여 실제 휴대기기에 발신기 앱을 설치하고 실행합니다. 오른쪽 상단에서 전송 아이콘을 클릭하고 사용 가능한 옵션 중에서 Android TV 기기를 선택합니다. 이제 Android TV 기기에서 Android TV 앱이 실행되고, 동영상을 재생하면 Android TV 리모컨을 사용하여 동영상 재생을 제어할 수 있습니다.

12. Cast 위젯 맞춤설정

색상을 설정하고 버튼, 텍스트, 썸네일 이미지의 스타일을 지정하고, 표시할 버튼 유형을 선택하여 Cast 위젯을 맞춤설정할 수 있습니다.

res/values/styles_castvideo.xml 업데이트

<style name="Theme.CastVideosTheme" parent="Theme.AppCompat.Light.NoActionBar">
    ...
    <item name="mediaRouteTheme">@style/CustomMediaRouterTheme</item>
    <item name="castIntroOverlayStyle">@style/CustomCastIntroOverlay</item>
    <item name="castMiniControllerStyle">@style/CustomCastMiniController</item>
    <item name="castExpandedControllerStyle">@style/CustomCastExpandedController</item>
    <item name="castExpandedControllerToolbarStyle">
        @style/ThemeOverlay.AppCompat.ActionBar
    </item>
    ...
</style>

다음 맞춤 테마를 선언합니다.

<!-- Customize Cast Button -->
<style name="CustomMediaRouterTheme" parent="Theme.MediaRouter">
    <item name="mediaRouteButtonStyle">@style/CustomMediaRouteButtonStyle</item>
</style>
<style name="CustomMediaRouteButtonStyle" parent="Widget.MediaRouter.Light.MediaRouteButton">
    <item name="mediaRouteButtonTint">#EEFF41</item>
</style>

<!-- Customize Introductory Overlay -->
<style name="CustomCastIntroOverlay" parent="CastIntroOverlay">
    <item name="castButtonTextAppearance">@style/TextAppearance.CustomCastIntroOverlay.Button</item>
    <item name="castTitleTextAppearance">@style/TextAppearance.CustomCastIntroOverlay.Title</item>
</style>
<style name="TextAppearance.CustomCastIntroOverlay.Button" parent="android:style/TextAppearance">
    <item name="android:textColor">#FFFFFF</item>
</style>
<style name="TextAppearance.CustomCastIntroOverlay.Title" parent="android:style/TextAppearance.Large">
    <item name="android:textColor">#FFFFFF</item>
</style>

<!-- Customize Mini Controller -->
<style name="CustomCastMiniController" parent="CastMiniController">
    <item name="castShowImageThumbnail">true</item>
    <item name="castTitleTextAppearance">@style/TextAppearance.AppCompat.Subhead</item>
    <item name="castSubtitleTextAppearance">@style/TextAppearance.AppCompat.Caption</item>
    <item name="castBackground">@color/accent</item>
    <item name="castProgressBarColor">@color/orange</item>
</style>

<!-- Customize Expanded Controller -->
<style name="CustomCastExpandedController" parent="CastExpandedController">
    <item name="castButtonColor">#FFFFFF</item>
    <item name="castPlayButtonDrawable">@drawable/cast_ic_expanded_controller_play</item>
    <item name="castPauseButtonDrawable">@drawable/cast_ic_expanded_controller_pause</item>
    <item name="castStopButtonDrawable">@drawable/cast_ic_expanded_controller_stop</item>
</style>

13. 축하합니다

이제 Android에서 Cast SDK 위젯을 사용하여 동영상 앱이 전송을 지원하도록 하는 방법을 알아보았습니다.

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