Android 앱에 Cast 통합

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

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

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

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

앱 흐름

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

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

Google Cast Android SDK의 모든 클래스, 메서드, 이벤트의 전체 목록은 Android용 Google Cast Sender API 참조를 확인하세요. 다음 섹션에서는 Android 앱에 Cast를 추가하는 단계를 설명합니다.

Android 매니페스트 구성

앱의 AndroidManifest.xml 파일에서 Cast SDK의 다음 요소를 구성해야 합니다.

uses-sdk

Cast SDK가 지원하는 최소 및 대상 Android API 수준을 설정합니다. 현재 최솟값은 API 수준 21이고 타겟은 API 수준 28입니다.

<uses-sdk
        android:minSdkVersion="21"
        android:targetSdkVersion="28" />

android:theme

최소 Android SDK 버전을 기반으로 앱의 테마를 설정합니다. 예를 들어 테마를 직접 구현하지 않는 경우 Lollipop 이전인 최소 Android SDK 버전을 타겟팅할 때 Theme.AppCompat의 변형을 사용해야 합니다.

<application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.AppCompat" >
       ...
</application>

Cast 컨텍스트 초기화

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

앱에서 OptionsProvider 인터페이스를 구현하여 CastContext 싱글톤을 초기화하는 데 필요한 옵션을 제공해야 합니다. OptionsProvider는 프레임워크 동작에 영향을 미치는 옵션이 포함된 CastOptions의 인스턴스를 제공합니다. 이 중 가장 중요한 것은 검색 결과를 필터링하고 전송 세션이 시작될 때 웹 수신기 앱을 실행하는 데 사용되는 웹 수신기 애플리케이션 ID입니다.

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

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
자바
public class CastOptionsProvider implements OptionsProvider {
    @Override
    public CastOptions getCastOptions(Context context) {
        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .build();
        return castOptions;
    }
    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

구현된 OptionsProvider의 정규화된 이름을 발신자 앱의 AndroidManifest.xml 파일에서 메타데이터 필드로 선언해야 합니다.

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

CastContextCastContext.getSharedInstance()가 호출될 때 지연 초기화됩니다.

Kotlin
class MyActivity : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        val castContext = CastContext.getSharedInstance(this)
    }
}
자바
public class MyActivity extends FragmentActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        CastContext castContext = CastContext.getSharedInstance(this);
    }
}

Cast UX 위젯

Cast 프레임워크는 Cast 디자인 체크리스트를 준수하는 위젯을 제공합니다.

  • 소개 오버레이: 프레임워크는 수신기를 처음 사용할 때 전송 버튼에 주의를 환기하도록 사용자에게 표시되는 맞춤 뷰 IntroductoryOverlay를 제공합니다. 발신기 앱은 텍스트와 제목 텍스트의 위치를 맞춤설정할 수 있습니다.

  • 전송 버튼: Cast 기기의 사용 가능 여부와 관계없이 전송 버튼이 표시됩니다. 사용자가 전송 버튼을 처음 클릭하면 검색된 기기가 나열된 전송 대화상자가 표시됩니다. 기기가 연결된 상태에서 사용자가 전송 버튼을 클릭하면 현재 미디어 메타데이터 (예: 제목, 녹음 스튜디오 이름, 썸네일 이미지)가 표시되거나 사용자가 Cast 기기에서 연결 해제할 수 있습니다. '전송 버튼'을 '전송 아이콘'이라고도 합니다.

  • 미니 컨트롤러: 사용자가 콘텐츠를 전송하는 중에 현재 콘텐츠 페이지나 확장된 컨트롤러에서 발신기 앱의 다른 화면으로 이동하면 미니 컨트롤러가 화면 하단에 표시되어 사용자가 현재 전송 중인 미디어 메타데이터를 확인하고 재생을 제어할 수 있습니다.

  • 확장 컨트롤러: 사용자가 콘텐츠를 전송할 때 미디어 알림이나 미니 컨트롤러를 클릭하면 확장된 컨트롤러가 실행됩니다. 확장 컨트롤러는 현재 재생 중인 미디어 메타데이터를 표시하고 미디어 재생을 제어하는 여러 버튼을 제공합니다.

  • 알림: Android 전용. 사용자가 콘텐츠를 전송하는 중에 발신자 앱에서 벗어나면 현재 전송 중인 미디어 메타데이터와 재생 컨트롤을 보여주는 미디어 알림이 표시됩니다.

  • 잠금 화면: Android 전용입니다. 사용자가 콘텐츠를 전송하는 중에 잠금 화면으로 이동하거나 기기가 타임아웃되면 현재 전송 중인 미디어 메타데이터와 재생 컨트롤을 보여주는 미디어 잠금 화면 컨트롤이 표시됩니다.

다음 가이드에는 이러한 위젯을 앱에 추가하는 방법이 설명되어 있습니다.

전송 버튼 추가

Android MediaRouter API는 보조 기기에서 미디어 표시 및 재생을 사용 설정하도록 설계되었습니다. MediaRouter API를 사용하는 Android 앱은 사용자가 Cast 기기와 같은 보조 기기에서 미디어를 재생하는 미디어 경로를 선택할 수 있도록 사용자 인터페이스의 일부로 전송 버튼을 포함해야 합니다.

이 프레임워크를 사용하면 MediaRouteButtonCast button로 쉽게 추가할 수 있습니다. 먼저 메뉴를 정의하는 xml 파일에 메뉴 항목 또는 MediaRouteButton를 추가하고 CastButtonFactory를 사용하여 프레임워크에 연결해야 합니다.

// To add a Cast button, add the following snippet.
// menu.xml
<item
    android:id="@+id/media_route_menu_item"
    android:title="@string/media_route_menu_title"
    app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
    app:showAsAction="always" />
Kotlin
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.kt
override fun onCreateOptionsMenu(menu: Menu): Boolean {
    super.onCreateOptionsMenu(menu)
    menuInflater.inflate(R.menu.main, menu)
    CastButtonFactory.setUpMediaRouteButton(
        applicationContext,
        menu,
        R.id.media_route_menu_item
    )
    return true
}
자바
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.java
@Override public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    getMenuInflater().inflate(R.menu.main, menu);
    CastButtonFactory.setUpMediaRouteButton(getApplicationContext(),
                                            menu,
                                            R.id.media_route_menu_item);
    return true;
}

그런 다음 ActivityFragmentActivity에서 상속되는 경우 레이아웃에 MediaRouteButton를 추가할 수 있습니다.

// activity_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:gravity="center_vertical"
   android:orientation="horizontal" >

   <androidx.mediarouter.app.MediaRouteButton
       android:id="@+id/media_route_button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_weight="1"
       android:mediaRouteTypes="user"
       android:visibility="gone" />

</LinearLayout>
Kotlin
// MyActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_layout)

    mMediaRouteButton = findViewById<View>(R.id.media_route_button) as MediaRouteButton
    CastButtonFactory.setUpMediaRouteButton(applicationContext, mMediaRouteButton)

    mCastContext = CastContext.getSharedInstance(this)
}
자바
// MyActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_layout);

   mMediaRouteButton = (MediaRouteButton) findViewById(R.id.media_route_button);
   CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), mMediaRouteButton);

   mCastContext = CastContext.getSharedInstance(this);
}

테마를 사용하여 전송 버튼의 모양을 설정하려면 전송 버튼 맞춤설정을 참고하세요.

기기 검색 구성

기기 검색은 완전히 CastContext에 의해 관리됩니다. CastContext를 초기화할 때 발신자 앱은 웹 수신기 애플리케이션 ID를 지정하며, 선택적으로 CastOptions에서 supportedNamespaces를 설정하여 네임스페이스 필터링을 요청할 수 있습니다. CastContext는 내부적으로 MediaRouter 참조를 보유하며 다음 조건에서 검색 프로세스를 시작합니다.

  • 기기 검색 지연 시간과 배터리 사용량의 균형을 맞추도록 설계된 알고리즘을 기반으로 하여, 발신자 앱이 포그라운드로 전환될 때 검색이 자동으로 시작되는 경우도 있습니다.
  • 전송 대화상자가 열렸습니다.
  • Cast SDK가 전송 세션 복구를 시도하고 있습니다.

전송 대화상자가 닫히거나 발신자 앱이 백그라운드로 전환되면 검색 프로세스가 중지됩니다.

Kotlin
class CastOptionsProvider : OptionsProvider {
    companion object {
        const val CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace"
    }

    override fun getCastOptions(appContext: Context): CastOptions {
        val supportedNamespaces: MutableList<String> = ArrayList()
        supportedNamespaces.add(CUSTOM_NAMESPACE)

        return CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
자바
class CastOptionsProvider implements OptionsProvider {
    public static final String CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace";

    @Override
    public CastOptions getCastOptions(Context appContext) {
        List<String> supportedNamespaces = new ArrayList<>();
        supportedNamespaces.add(CUSTOM_NAMESPACE);

        CastOptions castOptions = new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build();
        return castOptions;
    }

    @Override
    public List<SessionProvider> getAdditionalSessionProviders(Context context) {
        return null;
    }
}

세션 관리 작동 방식

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

세션은 앱이 CastContext.getSessionManager()를 통해 액세스할 수 있는 SessionManager 클래스에 의해 관리됩니다. 개별 세션은 Session 클래스의 서브클래스로 표현됩니다. 예를 들어 CastSession는 Cast 기기가 있는 세션을 나타냅니다. 앱은 SessionManager.getCurrentCastSession()를 통해 현재 활성 상태인 전송 세션에 액세스할 수 있습니다.

앱은 SessionManagerListener 클래스를 사용하여 생성, 정지, 재개, 종료와 같은 세션 이벤트를 모니터링할 수 있습니다. 프레임워크는 세션이 활성화된 상태에서 비정상적/갑작스러운 종료로부터 자동으로 다시 시작하려고 시도합니다.

세션은 MediaRouter 대화상자의 사용자 동작에 응답하여 자동으로 생성되고 해제됩니다.

전송 시작 오류를 더 잘 이해하기 위해 앱은 CastContext#getCastReasonCodeForCastStatusCode(int)를 사용하여 세션 시작 오류를 CastReasonCodes로 변환할 수 있습니다. 일부 세션 시작 오류 (예: CastReasonCodes#CAST_CANCELLED)는 의도된 동작이므로 오류로 로깅하면 안 됩니다.

세션의 상태 변경사항을 알고 있어야 한다면 SessionManagerListener를 구현하면 됩니다. 이 예에서는 Activity에서 CastSession의 사용 가능 여부를 수신 대기합니다.

Kotlin
class MyActivity : Activity() {
    private var mCastSession: CastSession? = null
    private lateinit var mCastContext: CastContext
    private lateinit var mSessionManager: SessionManager
    private val mSessionManagerListener: SessionManagerListener<CastSession> =
        SessionManagerListenerImpl()

    private inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> {
        override fun onSessionStarting(session: CastSession?) {}

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

        override fun onSessionStartFailed(session: CastSession?, error: Int) {
            val castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error)
            // Handle error
        }

        override fun onSessionSuspended(session: CastSession?, reason Int) {}

        override fun onSessionResuming(session: CastSession?, sessionId: String) {}

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

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

        override fun onSessionEnding(session: CastSession?) {}

        override fun onSessionEnded(session: CastSession?, error: Int) {
            finish()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mCastContext = CastContext.getSharedInstance(this)
        mSessionManager = mCastContext.sessionManager
    }

    override fun onResume() {
        super.onResume()
        mCastSession = mSessionManager.currentCastSession
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

    override fun onPause() {
        super.onPause()
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession::class.java)
        mCastSession = null
    }
}
자바
public class MyActivity extends Activity {
    private CastContext mCastContext;
    private CastSession mCastSession;
    private SessionManager mSessionManager;
    private SessionManagerListener<CastSession> mSessionManagerListener =
            new SessionManagerListenerImpl();

    private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> {
        @Override
        public void onSessionStarting(CastSession session) {}
        @Override
        public void onSessionStarted(CastSession session, String sessionId) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionStartFailed(CastSession session, int error) {
            int castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error);
            // Handle error
        }
        @Override
        public void onSessionSuspended(CastSession session, int reason) {}
        @Override
        public void onSessionResuming(CastSession session, String sessionId) {}
        @Override
        public void onSessionResumed(CastSession session, boolean wasSuspended) {
            invalidateOptionsMenu();
        }
        @Override
        public void onSessionResumeFailed(CastSession session, int error) {}
        @Override
        public void onSessionEnding(CastSession session) {}
        @Override
        public void onSessionEnded(CastSession session, int error) {
            finish();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCastContext = CastContext.getSharedInstance(this);
        mSessionManager = mCastContext.getSessionManager();
    }
    @Override
    protected void onResume() {
        super.onResume();
        mCastSession = mSessionManager.getCurrentCastSession();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }
    @Override
    protected void onPause() {
        super.onPause();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class);
        mCastSession = null;
    }
}

스트림 이전

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

스트림 전송 또는 확장 중에 새 대상 기기를 가져오려면 CastSession#addCastListener를 사용하여 Cast.Listener를 등록하세요. 그런 다음 onDeviceNameChanged 콜백 중에 CastSession#getCastDevice()를 호출합니다.

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

자동 재연결

프레임워크는 다음과 같은 여러 미묘한 경우 재연결을 처리하기 위해 발신기 앱에서 사용 설정할 수 있는 ReconnectionService를 제공합니다.

  • 일시적인 Wi-Fi 연결 끊김에서 복구하기
  • 기기 절전 모드에서 복구
  • 앱 백그라운드 상태에서 복구
  • 앱이 비정상 종료된 경우 복구

이 서비스는 기본적으로 사용 설정되어 있으며 CastOptions.Builder에서 사용 중지할 수 있습니다.

이 서비스는 gradle 파일에서 자동 병합이 사용 설정된 경우 앱의 매니페스트에 자동으로 병합될 수 있습니다.

프레임워크는 미디어 세션이 있으면 서비스를 시작하고 미디어 세션이 끝나면 서비스를 중지합니다.

미디어 컨트롤 작동 방식

Cast 프레임워크는 Cast 2.x의 RemoteMediaPlayer 클래스를 지원 중단하고 더 편리한 API 집합에서 동일한 기능을 제공하는 새로운 클래스인 RemoteMediaClient로 대체하고 GoogleApiClient를 전달할 필요가 없습니다.

앱이 미디어 네임스페이스를 지원하는 웹 수신기 앱으로 CastSession를 설정하면 프레임워크에서 RemoteMediaClient 인스턴스를 자동으로 만듭니다. 앱은 CastSession 인스턴스에서 getRemoteMediaClient() 메서드를 호출하여 이 인스턴스에 액세스할 수 있습니다.

웹 수신기에 요청을 실행하는 RemoteMediaClient의 모든 메서드는 해당 요청을 추적하는 데 사용할 수 있는 PendingResult 객체를 반환합니다.

RemoteMediaClient의 인스턴스는 앱의 여러 부분과 실제로 프레임워크의 일부 내부 구성요소(예: 영구 미니 컨트롤러알림 서비스)에 의해 공유될 수 있습니다. 이를 위해 이 인스턴스는 RemoteMediaClient.Listener의 여러 인스턴스를 등록할 수 있도록 지원합니다.

미디어 메타데이터 설정

MediaMetadata 클래스는 전송하려는 미디어 항목에 관한 정보를 나타냅니다. 다음 예에서는 영화의 새 MediaMetadata 인스턴스를 만들고 제목, 부제목, 이미지 2개를 설정합니다.

Kotlin
val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle())
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio())
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(0))))
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(1))))
자바
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle());
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio());
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(0))));
movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(1))));

미디어 메타데이터와 함께 이미지를 사용하는 방법은 이미지 선택을 참고하세요.

미디어 로드

다음 코드와 같이 앱이 미디어 항목을 로드할 수 있습니다. 먼저 미디어 메타데이터와 함께 MediaInfo.Builder를 사용하여 MediaInfo 인스턴스를 빌드합니다. 현재 CastSession에서 RemoteMediaClient를 가져온 다음 MediaInfoRemoteMediaClient에 로드합니다. RemoteMediaClient를 사용하여 웹 수신기에서 실행 중인 미디어 플레이어 앱을 재생, 일시중지 또는 제어합니다.

Kotlin
val mediaInfo = MediaInfo.Builder(mSelectedMedia.getUrl())
    .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
    .setContentType("videos/mp4")
    .setMetadata(movieMetadata)
    .setStreamDuration(mSelectedMedia.getDuration() * 1000)
    .build()
val remoteMediaClient = mCastSession.getRemoteMediaClient()
remoteMediaClient.load(MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build())
자바
MediaInfo mediaInfo = new MediaInfo.Builder(mSelectedMedia.getUrl())
        .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
        .setContentType("videos/mp4")
        .setMetadata(movieMetadata)
        .setStreamDuration(mSelectedMedia.getDuration() * 1000)
        .build();
RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
remoteMediaClient.load(new MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build());

미디어 트랙 사용 섹션도 참고하세요.

4K 동영상 형식

미디어의 동영상 형식을 확인하려면 MediaStatus에서 getVideoInfo()를 사용하여 VideoInfo의 현재 인스턴스를 가져옵니다. 이 인스턴스에는 HDR TV 형식의 유형과 픽셀 단위의 디스플레이 높이와 너비가 포함됩니다. 4K 형식의 변형은 상수 HDR_TYPE_*로 표시됩니다.

여러 기기로 알림 원격 제어

사용자가 전송 중일 때 동일한 네트워크의 다른 Android 기기에도 재생을 제어할 수 있는 알림이 전송됩니다. 이러한 알림을 수신하는 기기 사용자는 설정 앱에서 Google > Google Cast > 리모컨 알림 표시로 이동하여 해당 기기의 알림을 사용 중지할 수 있습니다. 알림에는 설정 앱 바로가기가 포함됩니다. 자세한 내용은 Cast 리모컨 알림을 참고하세요.

미니 컨트롤러 추가

Cast 설계 체크리스트에 따라 발신기 앱은 사용자가 현재 콘텐츠 페이지에서 발신기 앱의 다른 부분으로 이동할 때 표시되는 미니 컨트롤러라는 지속적인 컨트롤을 제공해야 합니다. 미니 컨트롤러는 현재 Cast 세션의 사용자에게 시각적 알림을 제공합니다. 미니 컨트롤러를 탭하면 사용자는 Cast 전체 화면으로 확장된 컨트롤러 뷰로 돌아갈 수 있습니다.

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

<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" />

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

이 맞춤 뷰의 제목 및 부제목의 텍스트 모양을 설정하고 버튼을 선택하려면 미니 컨트롤러 맞춤설정을 참고하세요.

확장 컨트롤러 추가

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

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

먼저 확장 컨트롤러에서 전송 버튼을 제공할 새 메뉴 리소스 파일을 만듭니다.

<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>

ExpandedControllerActivity를 확장하는 새 클래스를 만듭니다.

Kotlin
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
    }
}
자바
public class ExpandedControlsActivity extends ExpandedControllerActivity {
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.expanded_controller, menu);
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
        return true;
    }
}

이제 application 태그 내의 앱 매니페스트에서 새 활동을 선언합니다.

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

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

Kotlin
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()
}
자바
public CastOptions getCastOptions(Context context) {
    NotificationOptions notificationOptions = new NotificationOptions.Builder()
            .setTargetActivityClassName(ExpandedControlsActivity.class.getName())
            .build();
    CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
            .setNotificationOptions(notificationOptions)
            .setExpandedControllerActivityClassName(ExpandedControlsActivity.class.getName())
            .build();

    return new CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setCastMediaOptions(mediaOptions)
            .build();
}

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

Kotlin
private fun loadRemoteMedia(position: Int, autoPlay: Boolean) {
    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(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position.toLong()).build()
    )
}
자바
private void loadRemoteMedia(int position, boolean autoPlay) {
    if (mCastSession == null) {
        return;
    }
    final RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient();
    if (remoteMediaClient == null) {
        return;
    }
    remoteMediaClient.registerCallback(new RemoteMediaClient.Callback() {
        @Override
        public void onStatusUpdated() {
            Intent intent = new Intent(LocalPlayerActivity.this, ExpandedControlsActivity.class);
            startActivity(intent);
            remoteMediaClient.unregisterCallback(this);
        }
    });
    remoteMediaClient.load(new MediaLoadRequestData.Builder()
            .setMediaInfo(mSelectedMedia)
            .setAutoplay(autoPlay)
            .setCurrentTime(position).build());
}

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

테마를 사용하여 모양을 설정하고, 표시할 버튼을 선택하고, 맞춤 버튼을 추가하려면 확장 컨트롤러 맞춤설정을 참고하세요.

볼륨 컨트롤

프레임워크는 발신자 앱의 볼륨을 자동으로 관리합니다. 프레임워크는 발신기 앱과 웹 수신기 앱을 자동으로 동기화하므로 발신자 UI가 항상 웹 수신기에서 지정한 볼륨을 보고합니다.

실제 버튼 볼륨 제어

Android에서는 발신기 기기의 실제 버튼을 사용하여 기본적으로 Jelly Bean 이상을 사용하는 모든 기기에서 웹 수신기의 전송 세션 볼륨을 변경할 수 있습니다.

Jelly Bean 이전의 물리적 버튼 볼륨 제어

Jelly Bean 이전의 Android 기기에서 실제 볼륨 키를 사용하여 웹 수신기 기기 볼륨을 제어하려면 발신 앱이 활동에서 dispatchKeyEvent를 재정의하고 CastContext.onDispatchVolumeKeyEventBeforeJellyBean()를 호출해야 합니다.

Kotlin
class MyActivity : FragmentActivity() {
    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
        return (CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
                || super.dispatchKeyEvent(event))
    }
}
자바
class MyActivity extends FragmentActivity {
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        return CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
            || super.dispatchKeyEvent(event);
    }
}

알림 및 잠금 화면에 미디어 컨트롤 추가

Google Cast 디자인 체크리스트에 따르면 발신기 앱은 Android에서만 전송되고 있지만 발신기 앱에는 포커스가 없는 알림과 잠금 화면에서 미디어 컨트롤을 구현해야 합니다. 프레임워크는 발신 앱이 알림과 잠금 화면에서 미디어 컨트롤을 빌드할 수 있도록 MediaNotificationServiceMediaIntentReceiver를 제공합니다.

MediaNotificationService는 발신자가 전송 중일 때 실행되며 이미지 썸네일, 현재 전송 항목, 재생/일시중지 버튼, 중지 버튼 관련 정보가 포함된 알림을 표시합니다.

MediaIntentReceiver는 알림에서 사용자 작업을 처리하는 BroadcastReceiver입니다.

앱은 NotificationOptions를 통해 잠금 화면에서 알림 및 미디어 제어를 구성할 수 있습니다. 앱은 알림에 표시할 컨트롤 버튼과 사용자가 알림을 탭할 때 열 Activity를 구성할 수 있습니다. 작업이 명시적으로 제공되지 않으면 기본값인 MediaIntentReceiver.ACTION_TOGGLE_PLAYBACKMediaIntentReceiver.ACTION_STOP_CASTING가 사용됩니다.

Kotlin
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
val buttonActions: MutableList<String> = ArrayList()
buttonActions.add(MediaIntentReceiver.ACTION_REWIND)
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK)
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD)
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING)

// Showing "play/pause" and "stop casting" in the compat view of the notification.
val compatButtonActionsIndices = intArrayOf(1, 3)

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
val notificationOptions = NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity::class.java.name)
    .build()
자바
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
List<String> buttonActions = new ArrayList<>();
buttonActions.add(MediaIntentReceiver.ACTION_REWIND);
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK);
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD);
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING);

// Showing "play/pause" and "stop casting" in the compat view of the notification.
int[] compatButtonActionsIndices = new int[]{1, 3};

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
NotificationOptions notificationOptions = new NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity.class.getName())
    .build();

알림 및 잠금 화면에서 미디어 컨트롤 표시는 기본적으로 사용 설정되며 CastMediaOptions.Builder에서 null로 setNotificationOptions를 호출하여 사용 중지할 수 있습니다. 현재는 알림이 사용 설정되어 있는 한 잠금 화면 기능도 사용 설정됩니다.

Kotlin
// ... continue with the NotificationOptions built above
val mediaOptions = CastMediaOptions.Builder()
    .setNotificationOptions(notificationOptions)
    .build()
val castOptions: CastOptions = Builder()
    .setReceiverApplicationId(context.getString(R.string.app_id))
    .setCastMediaOptions(mediaOptions)
    .build()
자바
// ... continue with the NotificationOptions built above
CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
        .setNotificationOptions(notificationOptions)
        .build();
CastOptions castOptions = new CastOptions.Builder()
        .setReceiverApplicationId(context.getString(R.string.app_id))
        .setCastMediaOptions(mediaOptions)
        .build();

발신기 앱이 동영상 또는 오디오 라이브 스트림을 재생 중일 때 SDK는 알림 컨트롤의 재생/일시중지 버튼 대신 재생/중지 버튼을 자동으로 표시하지만 잠금 화면 컨트롤에는 표시하지 않습니다.

참고: Lollipop 이전 기기에 잠금 화면 컨트롤을 표시하기 위해 RemoteMediaClient에서 자동으로 오디오 포커스를 요청합니다.

오류 처리

발신기 앱은 모든 오류 콜백을 처리하고 Cast 수명 주기의 각 단계에 가장 적합한 응답을 결정하는 것이 매우 중요합니다. 앱은 사용자에게 오류 대화상자를 표시하거나 웹 수신기 연결을 끊을 수 있습니다.