1. 개요
이 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 시청 관련 경험을 평가해 주세요.
<ph type="x-smartling-placeholder">2. 샘플 코드 가져오기
모든 샘플 코드를 컴퓨터에 다운로드할 수 있습니다.
그런 다음 다운로드한 ZIP 파일의 압축을 풉니다.
3. 샘플 앱 실행
먼저 완성된 샘플 앱이 어떤 모습인지 살펴보겠습니다. 기본 동영상 플레이어로 사용되는 앱입니다. 사용자가 목록에서 동영상을 선택한 다음 기기에서 로컬로 재생하거나 Google Cast 기기로 전송할 수 있습니다.
다음 안내에서는 코드를 다운로드한 상태에서 Android 스튜디오에서 완성된 샘플 앱을 열고 실행하는 방법을 설명합니다.
시작 화면에서 Import Project 또는 File > 신규 > Import Project... 메뉴 옵션을 사용합니다.
샘플 코드 폴더에서 app-done
디렉터리를 선택하고 OK를 클릭합니다.
파일 > Gradle 파일과 프로젝트 동기화.
Android 기기에서 USB 디버깅 사용 설정: Android 4.2 이상 버전에서는 개발자 옵션 화면이 기본적으로 숨겨져 있습니다. 이 옵션을 표시하려면 설정 > 휴대전화 정보로 이동하여 빌드 번호를 일곱 번 탭합니다. 이전 화면으로 돌아가서 시스템 > 고급으로 이동하고 화면 하단의 개발자 옵션을 탭한 다음 USB 디버깅을 탭하여 사용 설정합니다.
Android 기기를 연결하고 Android 스튜디오에서 Run 버튼을 클릭합니다. 몇 초 후에 Cast Videos라는 동영상 앱이 표시됩니다.
동영상 앱에서 전송 버튼을 클릭하고 Google Cast 기기를 선택합니다.
동영상을 선택하고 재생 버튼을 클릭합니다.
Google Cast 기기에서 동영상이 재생되기 시작합니다.
확장된 컨트롤러가 표시됩니다. 재생/일시중지 버튼을 사용하여 재생을 제어할 수 있습니다.
동영상 목록으로 다시 이동합니다.
이제 미니 컨트롤러가 화면 하단에 표시됩니다.
미니 컨트롤러에서 일시중지 버튼을 클릭하면 수신기에서 동영상이 일시중지됩니다. 동영상을 계속 재생하려면 미니 컨트롤러에서 재생 버튼을 클릭합니다.
휴대기기의 홈 버튼을 클릭합니다. 알림을 끌어서 내리면 Cast 세션에 관한 알림이 표시됩니다.
휴대전화를 잠갔다 잠금 해제하면 미디어 재생을 제어하거나 전송을 중단하기 위한 알림이 잠금 화면에 표시됩니다.
동영상 앱으로 돌아가서 전송 버튼을 클릭하여 Google Cast 기기로의 전송을 중지합니다.
자주 묻는 질문(FAQ)
4. 시작 프로젝트 준비
다운로드한 시작 앱에 Google Cast 지원 기능을 추가해야 합니다. 다음은 이 Codelab에서 사용할 Google Cast 용어입니다.
- 발신기 앱은 휴대기기 또는 노트북에서 실행됩니다.
- 수신기 앱은 Google Cast 기기에서 실행됩니다.
이제 Android 스튜디오를 사용하여 시작 프로젝트를 토대로 빌드할 준비가 되었습니다.
- 다운로드한 샘플 코드에서
app-start
디렉터리를 선택합니다 (시작 화면에서 Import Project 선택 또는 File > New > Import Project... 메뉴 옵션 선택). - Sync Project with Gradle Files 버튼을 클릭합니다.
- Run 버튼을 클릭하여 앱을 실행하고 UI를 살펴봅니다.
앱 디자인
앱이 원격 웹 서버에서 동영상 목록을 가져오고 사용자가 둘러볼 수 있도록 목록을 제공합니다. 사용자는 동영상을 선택하여 세부정보를 보거나 휴대기기에서 로컬로 동영상을 재생할 수 있습니다.
앱은 두 가지 기본 활동 VideoBrowserActivity
와 LocalPlayerActivity
로 구성됩니다. Google Cast 기능을 통합하려면 활동이 AppCompatActivity
또는 상위 요소인 FragmentActivity
에서 상속해야 합니다. 이 제한사항은 MediaRouter 지원 라이브러리에 제공된 MediaRouteButton
를 MediaRouteActionProvider
로 추가해야 하고 이는 활동이 위에 언급된 클래스에서 상속되는 경우에만 작동하기 때문에 존재합니다. MediaRouter 지원 라이브러리는 필수 클래스를 제공하는 AppCompat 지원 라이브러리에 종속됩니다.
VideoBrowserActivity
이 활동에는 Fragment
(VideoBrowserFragment
)가 포함되어 있습니다. 이 목록은 ArrayAdapter
(VideoListAdapter
)로 지원됩니다. 동영상 목록과 관련 메타데이터는 원격 서버에서 JSON 파일로 호스팅됩니다. AsyncTaskLoader
(VideoItemLoader
)는 이 JSON을 가져와서 처리하여 MediaItem
객체 목록을 빌드합니다.
MediaItem
객체는 동영상 및 관련 메타데이터(예: 제목, 설명, 스트림, URL, 지원 이미지 URL, 관련 텍스트 트랙(자막용))가 있는 경우 이를 모델링합니다. MediaItem
객체는 활동 간에 전달되므로 MediaItem
에는 Bundle
로 변환하거나 그 반대로 변환하는 유틸리티 메서드가 있습니다.
로더가 MediaItems
목록을 빌드하면 이 목록을 전달받은 VideoListAdapter
가 VideoBrowserFragment
에 MediaItems
목록을 표시합니다. 사용자에게는 각 동영상에 관한 간단한 설명과 함께 동영상 미리보기 이미지 목록이 표시됩니다. 항목을 선택하면 관련 MediaItem
이 Bundle
로 변환되고 LocalPlayerActivity
로 전달됩니다.
LocalPlayerActivity
이 활동은 특정 동영상에 관한 메타데이터를 표시하고 사용자가 휴대기기에서 로컬로 동영상을 재생할 수 있도록 합니다.
활동은 VideoView
, 일부 미디어 컨트롤, 선택한 동영상의 설명을 표시하는 텍스트 영역을 호스팅합니다. 플레이어는 화면 상단에 위치하며 아래에는 동영상에 관한 세부 설명을 위한 공간을 남겨둡니다. 사용자는 동영상을 재생/일시중지하거나 로컬에서 재생할 수 있습니다.
종속 항목
AppCompatActivity
를 사용하고 있으므로 AppCompat 지원 라이브러리가 필요합니다. 동영상 목록을 관리하고 목록의 이미지를 비동기식으로 가져오기 위해 Volley 라이브러리를 사용합니다.
자주 묻는 질문(FAQ)
5. 전송 버튼 추가
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 프레임워크에는 모든 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
를 사용하여 VideoBrowserActivity
의 onCreateOptionsMenu()
메서드를 재정의하여 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
}
비슷한 방식으로 LocalPlayerActivity
의 onCreateOptionsMenu
를 재정의합니다.
Run 버튼을 클릭하여 휴대기기에서 앱을 실행합니다. 앱의 작업 모음에 전송 버튼이 표시되며, 이 버튼을 클릭하면 로컬 네트워크에 Cast 기기가 표시됩니다. 기기 검색은 CastContext
에서 자동으로 관리됩니다. Cast 기기를 선택하면 샘플 수신기 앱이 Cast 기기에 로드됩니다. 탐색 활동과 로컬 플레이어 활동을 오가며 둘러볼 수 있으며 전송 버튼 상태는 동기화된 상태로 유지됩니다.
미디어 재생과 관련된 지원이 연결되지 않았으므로 아직 Cast 기기에서 동영상을 재생할 수 없습니다. 전송 버튼을 클릭하여 연결 해제합니다.
6. 동영상 콘텐츠 전송
Cast 기기에서 원격으로 동영상을 재생할 수 있도록 샘플 앱을 확장하겠습니다. 이를 처리하려면 Cast 프레임워크에서 생성된 다양한 이벤트를 수신 대기해야 합니다.
미디어 전송
Cast 기기에서 미디어를 재생하려면 대략적으로 다음 단계를 따라야 합니다.
- 미디어 항목을 모델링하는
MediaInfo
객체를 만듭니다. - Cast 기기에 연결하고 수신기 애플리케이션을 실행합니다.
MediaInfo
객체를 수신기에 로드하고 콘텐츠를 재생합니다.- 미디어 상태를 추적합니다.
- 사용자 상호작용에 따라 재생 명령어를 수신기로 전송합니다.
이전 섹션에서 이미 2단계를 완료했습니다. 3단계는 Cast 프레임워크에서 쉽게 할 수 있습니다. 1단계는 한 객체를 다른 객체에 매핑하는 것입니다. MediaInfo
는 Cast 프레임워크가 이해할 수 있는 내용이고 MediaItem
은 미디어 항목에 관한 앱의 캡슐화입니다. 따라서 MediaItem
을 MediaInfo
에 쉽게 매핑할 수 있습니다.
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에서 자동으로 처리됩니다.
LocalPlayerActivity
에 SessionManagerListener
를 추가해 보겠습니다.
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))
...
}
이제 Run 버튼을 클릭하여 휴대기기에서 앱을 실행합니다. Cast 기기에 연결하여 동영상 재생을 시작합니다. 수신기에서 재생되는 동영상을 볼 수 있습니다.
7. 미니 컨트롤러
Cast 디자인 체크리스트에 따르면 모든 Cast 앱은 사용자가 현재 콘텐츠 페이지에서 벗어나면 표시되는 미니 컨트롤러를 제공해야 합니다. 미니 컨트롤러는 즉시 액세스할 수 있으며 현재 Cast 세션을 시각적으로 표시합니다.
Cast SDK는 맞춤 뷰 MiniControllerFragment
를 제공합니다. 이 뷰는 미니 컨트롤러를 표시하려는 활동의 앱 레이아웃 파일에 추가할 수 있습니다.
res/layout/player_activity.xml
및 res/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"/>
Run 버튼을 클릭하여 앱을 실행하고 동영상을 전송합니다. 수신기에서 재생이 시작되면 각 활동 하단에 미니 컨트롤러가 나타납니다. 미니 컨트롤러를 사용하여 원격 재생을 제어할 수 있습니다. 탐색 활동과 로컬 플레이어 활동 간에 이동하는 경우 미니 컨트롤러 상태는 수신기 미디어 재생 상태와 동기화된 상태로 유지되어야 합니다.
8. 알림 및 잠금 화면
Google Cast 디자인 체크리스트에 따르면 발신기 앱이 알림 및 잠금 화면에서 미디어 제어를 구현해야 합니다.
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()
}
Run 버튼을 클릭하여 휴대기기에서 앱을 실행합니다. 동영상을 전송하고 샘플 앱을 벗어나 이동합니다. 그러면 현재 수신기에서 재생 중인 동영상에 관한 알림이 표시됩니다. 휴대기기를 잠그면 이제 잠금 화면에 Cast 기기의 미디어 재생에 관한 컨트롤이 표시됩니다.
9. 소개 오버레이
Google Cast 디자인 체크리스트에 따르면 발신기 앱은 기존 사용자에게 전송 버튼을 소개하여 이제 발신기 앱에서 전송 기능을 지원하고 Google Cast를 처음 접하는 사용자도 지원한다는 것을 알려야 합니다.
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()
}
}
}
이제 Cast 기기를 사용할 수 있을 때 onCreate
메서드를 수정하고 onResume
및 onPause
메서드를 다음과 일치하도록 재정의하여 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!!)
}
앱 데이터를 지우거나 기기에서 앱을 삭제합니다. 그런 다음 Run 버튼을 클릭하여 휴대기기에서 앱을 실행하면 소개 오버레이가 표시됩니다 (오버레이가 표시되지 않는 경우 앱 데이터 삭제).
10. 확장 컨트롤러
Google Cast 디자인 체크리스트에 따르면 발신기 앱에서 전송 중인 미디어에 확장된 컨트롤러를 제공해야 합니다. 확장된 컨트롤러는 미니 컨트롤러의 전체 화면 버전입니다.
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
를 수정하고 NotificationOptions
및 CastMediaOptions
를 변경하여 타겟 활동을 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())
}
Run 버튼을 클릭하여 휴대기기에서 앱을 실행하고 동영상을 전송합니다. 확장 컨트롤러가 표시됩니다. 동영상 목록으로 다시 돌아가 미니 컨트롤러를 클릭하면 확장된 컨트롤러가 다시 로드됩니다. 앱에서 벗어나 알림을 확인합니다. 알림 이미지를 클릭하여 확장된 컨트롤러를 로드합니다.
11. Cast Connect 지원 추가
Cast Connect 라이브러리를 사용하면 기존 발신기 애플리케이션이 Cast 프로토콜을 통해 Android TV 애플리케이션과 통신할 수 있습니다. Cast Connect는 Cast 인프라를 기반으로 빌드되며, Android TV 앱은 수신기 역할을 합니다.
종속 항목
참고: Cast Connect를 구현하려면 play-services-cast-framework
이 19.0.0
이상이어야 합니다.
LaunchOptions
Android 수신기라고도 하는 Android TV 애플리케이션을 실행하려면 LaunchOptions
객체에서 setAndroidReceiverCompatible
플래그를 true로 설정해야 합니다. 이 LaunchOptions
객체는 수신기가 실행되는 방식을 지정하고 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를 설치하는 방법
- Android TV 기기의 IP 주소를 찾습니다. 일반적으로 설정 > 네트워크 및 인터넷 > (기기가 연결된 네트워크 이름)을 선택합니다. 오른쪽에는 세부정보와 네트워크에 있는 기기의 IP가 표시됩니다.
- 터미널을 사용하여 ADB를 통해 기기의 IP 주소를 사용하여 연결합니다.
$ adb connect <device_ip_address>:5555
- 터미널 창에서 이 Codelab을 시작할 때 다운로드한 Codelab 샘플의 최상위 폴더로 이동합니다. 예를 들면 다음과 같습니다.
$ cd Desktop/android_codelab_src
- 다음을 실행하여 이 폴더의 .apk 파일을 Android TV에 설치합니다.
$ adb -s <device_ip_address>:5555 install android-tv-app.apk
- 이제 Android TV 기기의 내 앱 메뉴에서 Cast 동영상이라는 이름으로 앱을 볼 수 있습니다.
- 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 발신자 개발자 가이드를 참고하세요.