Register now for Android Dev Summit 2019!

Now Playing 카드 표시

TV 앱이 런처 또는 백그라운드에서 미디어를 재생하는 경우 Now Playing 카드를 표시해야 합니다. 이 카드를 통해 사용자는 현재 미디어를 재생하고 있는 앱으로 돌아갈 수 있습니다.

Android 프레임워크는 활성 MediaSession이 있으면 홈 화면에 Now Playing 카드를 표시합니다. 이 카드에는 앨범아트, 제목, 앱 아이콘과 같은 미디어 메타데이터가 포함되어 있습니다. 사용자가 카드를 선택하면 시스템에서 앱을 엽니다.

이 과정에서는 MediaSession 클래스를 사용하여 Now Playing 카드를 구현하는 방법을 보여줍니다.

그림 1. 백그라운드에서 미디어를 재생하는 경우 Now Playing 카드가 표시됩니다.

미디어 세션 시작

앱에서 미디어 재생을 준비하는 경우 MediaSession을 만듭니다. 다음 코드 스니펫은 적절한 콜백 및 플래그를 설정하는 방법의 예입니다.

Kotlin

    session = MediaSession(this, "MusicService").apply {
        setCallback(MediaSessionCallback())
        setFlags(
                MediaSession.FLAG_HANDLES_MEDIA_BUTTONS or MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS
        )
    }
    

자바

    session = new MediaSession(this, "MusicService");
    session.setCallback(new MediaSessionCallback());
    session.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
            MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
    

참고: Now Playing 카드는 FLAG_HANDLES_TRANSPORT_CONTROLS 플래그가 설정된 미디어 세션에서만 표시됩니다.

Now Playing 카드 표시

Now Playing 카드는 활성 세션에서만 표시됩니다. 재생이 시작되면 setActive(true)를 호출해야 합니다. 또한, 앱은 오디오 포커스 관리에 설명된 대로 오디오 포커스를 요청해야 합니다.

Kotlin

    private fun handlePlayRequest() {

        tryToGetAudioFocus()

        if (!session.isActive) {
            session.isActive = true
        }
        ...
    }
    

자바

    private void handlePlayRequest() {

        tryToGetAudioFocus();

        if (!session.isActive()) {
            session.setActive(true);
        }
        ...
    }
    

이 카드는 setActive(false) 호출이 미디어 세션을 비활성화하거나 다른 앱이 미디어 재생을 시작하면 런처 화면에서 제거됩니다. 재생이 완전히 중지되고 활성 미디어가 없으면 앱은 즉시 미디어 세션을 비활성화해야 합니다. 재생이 일시중지된 경우에는 일반적으로 5분에서 30분 사이의 지연 시간이 경과한 후에 앱이 미디어 세션을 비활성화합니다.

재생 상태 업데이트

카드가 현재 미디어의 상태를 표시할 수 있도록 MediaSession의 재생 상태를 업데이트합니다.

Kotlin

    private fun updatePlaybackState() {
        val position: Long =
                mediaPlayer
                        ?.takeIf { it.isPlaying }
                        ?.currentPosition?.toLong()
                        ?: PlaybackState.PLAYBACK_POSITION_UNKNOWN

        val stateBuilder = PlaybackState.Builder()
                .setActions(getAvailableActions()).apply {
                    setState(mState, position, 1.0f)
                }
        session.setPlaybackState(stateBuilder.build())
    }

    private fun getAvailableActions(): Long {
        var actions = (PlaybackState.ACTION_PLAY_PAUSE
                or PlaybackState.ACTION_PLAY_FROM_MEDIA_ID
                or PlaybackState.ACTION_PLAY_FROM_SEARCH)

        playingQueue?.takeIf { it.isNotEmpty() }?.apply {
            actions = if (mState == PlaybackState.STATE_PLAYING) {
                actions or PlaybackState.ACTION_PAUSE
            } else {
                actions or PlaybackState.ACTION_PLAY
            }
            if (currentIndexOnQueue > 0) {
                actions = actions or PlaybackState.ACTION_SKIP_TO_PREVIOUS
            }
            if (currentIndexOnQueue < size - 1) {
                actions = actions or PlaybackState.ACTION_SKIP_TO_NEXT
            }
        }
        return actions
    }
    

자바

    private void updatePlaybackState() {
        long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN;
        if (mediaPlayer != null && mediaPlayer.isPlaying()) {
            position = mediaPlayer.getCurrentPosition();
        }
        PlaybackState.Builder stateBuilder = new PlaybackState.Builder()
                .setActions(getAvailableActions());
        stateBuilder.setState(mState, position, 1.0f);
        session.setPlaybackState(stateBuilder.build());
    }

    private long getAvailableActions() {
        long actions = PlaybackState.ACTION_PLAY_PAUSE |
                PlaybackState.ACTION_PLAY_FROM_MEDIA_ID |
                PlaybackState.ACTION_PLAY_FROM_SEARCH;
        if (playingQueue == null || playingQueue.isEmpty()) {
            return actions;
        }
        if (mState == PlaybackState.STATE_PLAYING) {
            actions |= PlaybackState.ACTION_PAUSE;
        } else {
            actions |= PlaybackState.ACTION_PLAY;
        }
        if (currentIndexOnQueue > 0) {
            actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS;
        }
        if (currentIndexOnQueue < playingQueue.size() - 1) {
            actions |= PlaybackState.ACTION_SKIP_TO_NEXT;
        }
        return actions;
    }
    

미디어 메타데이터 표시

MediaMetadatasetMetadata() 메서드를 사용해 설정합니다. 이 미디어 세션 객체의 메서드를 통해 제목, 자막, 다양한 아이콘 등 트랙에 관한 정보를 Now Playing 카드에 제공할 수 있습니다. 다음 예에서는 트랙 데이터가 사용자설정 데이터 클래스 MediaData에 저장되어 있다고 가정합니다.

Kotlin

    private fun updateMetadata(myData: MediaData) {
        val metadataBuilder = MediaMetadata.Builder().apply {
            // To provide most control over how an item is displayed set the
            // display fields in the metadata
            putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, myData.displayTitle)
            putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, myData.displaySubtitle)
            putString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, myData.artUri)
            // And at minimum the title and artist for legacy support
            putString(MediaMetadata.METADATA_KEY_TITLE, myData.title)
            putString(MediaMetadata.METADATA_KEY_ARTIST, myData.artist)
            // A small bitmap for the artwork is also recommended
            putBitmap(MediaMetadata.METADATA_KEY_ART, myData.artBitmap)
            // Add any other fields you have for your data as well
        }
        session.setMetadata(metadataBuilder.build())
    }
    

자바

    private void updateMetadata(MediaData myData) {
        MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder();
        // To provide most control over how an item is displayed set the
        // display fields in the metadata
        metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE,
                myData.displayTitle);
        metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE,
                myData.displaySubtitle);
        metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI,
                myData.artUri);
        // And at minimum the title and artist for legacy support
        metadataBuilder.putString(MediaMetadata.METADATA_KEY_TITLE,
                myData.title);
        metadataBuilder.putString(MediaMetadata.METADATA_KEY_ARTIST,
                myData.artist);
        // A small bitmap for the artwork is also recommended
        metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_ART,
                myData.artBitmap);
        // Add any other fields you have for your data as well
        session.setMetadata(metadataBuilder.build());
    }
    

사용자 작업에 반응

사용자가 Now Playing 카드를 선택하면 시스템에서 해당 세션을 소유한 앱을 엽니다. 앱이 PendingIntentsetSessionActivity()에 제공하면 아래 설명된 대로 지정된 활동을 시스템에서 실행합니다. 앱에서 인텐트를 제공하지 않으면 기본 시스템 인텐트가 열립니다. 지정된 활동은 사용자가 재생을 일시 중지하거나 중지할 수 있는 재생 컨트롤을 제공해야 합니다.

Kotlin

    val pi: PendingIntent = Intent(context, MyActivity::class.java).let { intent ->
        PendingIntent.getActivity(
                context, 99 /*request code*/,
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT
        )
    }
    session.setSessionActivity(pi)
    

자바

    Intent intent = new Intent(context, MyActivity.class);
    PendingIntent pi = PendingIntent.getActivity(context, 99 /*request code*/,
            intent, PendingIntent.FLAG_UPDATE_CURRENT);
    session.setSessionActivity(pi);