미디어 브라우저 서비스 빌드

앱은 manifest에서 인텐트 필터와 함께 MediaBrowserService를 선언해야 합니다. 서비스 이름을 선택할 수 있으며 다음 예에서는 'MediaPlaybackService'입니다.

<service android:name=".MediaPlaybackService">
  <intent-filter>
    <action android:name="android.media.browse.MediaBrowserService" />
  </intent-filter>
</service>

참고: MediaBrowserService의 권장되는 구현은 MediaBrowserServiceCompat입니다. media-compat 지원 라이브러리에 정의되어 있습니다. 이 페이지에서 'MediaBrowserService'라는 용어는 MediaBrowserServiceCompat의 인스턴스를 나타냅니다.

미디어 세션 초기화

서비스는 onCreate() 수명 주기 콜백 메서드를 수신하면 다음 단계를 실행해야 합니다.

다음 onCreate() 코드는 이러한 단계를 보여줍니다.

Kotlin

private const val MY_MEDIA_ROOT_ID = "media_root_id"
private const val MY_EMPTY_MEDIA_ROOT_ID = "empty_root_id"

class MediaPlaybackService : MediaBrowserServiceCompat() {

    private var mediaSession: MediaSessionCompat? = null
    private lateinit var stateBuilder: PlaybackStateCompat.Builder

    override fun onCreate() {
        super.onCreate()

        // Create a MediaSessionCompat
        mediaSession = MediaSessionCompat(baseContext, LOG_TAG).apply {

            // Enable callbacks from MediaButtons and TransportControls
            setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
                    or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
            )

            // Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player
            stateBuilder = PlaybackStateCompat.Builder()
                    .setActions(PlaybackStateCompat.ACTION_PLAY
                                    or PlaybackStateCompat.ACTION_PLAY_PAUSE
                    )
            setPlaybackState(stateBuilder.build())

            // MySessionCallback() has methods that handle callbacks from a media controller
            setCallback(MySessionCallback())

            // Set the session's token so that client activities can communicate with it.
            setSessionToken(sessionToken)
        }
    }
}

Java

public class MediaPlaybackService extends MediaBrowserServiceCompat {
    private static final String MY_MEDIA_ROOT_ID = "media_root_id";
    private static final String MY_EMPTY_MEDIA_ROOT_ID = "empty_root_id";

    private MediaSessionCompat mediaSession;
    private PlaybackStateCompat.Builder stateBuilder;

    @Override
    public void onCreate() {
        super.onCreate();

        // Create a MediaSessionCompat
        mediaSession = new MediaSessionCompat(context, LOG_TAG);

        // Enable callbacks from MediaButtons and TransportControls
        mediaSession.setFlags(
              MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
              MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);

        // Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player
        stateBuilder = new PlaybackStateCompat.Builder()
                            .setActions(
                                PlaybackStateCompat.ACTION_PLAY |
                                PlaybackStateCompat.ACTION_PLAY_PAUSE);
        mediaSession.setPlaybackState(stateBuilder.build());

        // MySessionCallback() has methods that handle callbacks from a media controller
        mediaSession.setCallback(new MySessionCallback());

        // Set the session's token so that client activities can communicate with it.
        setSessionToken(mediaSession.getSessionToken());
    }
}

클라이언트 연결 관리

MediaBrowserService에는 클라이언트 연결을 처리하는 두 가지 메서드가 있습니다. onGetRoot()는 서비스 액세스를 제어하고 onLoadChildren()는 클라이언트가 MediaBrowserService의 콘텐츠 계층 구조 메뉴를 빌드하고 표시하는 기능을 제공합니다.

onGetRoot()로 클라이언트 연결 제어

onGetRoot() 메서드는 콘텐츠 계층 구조의 루트 노드를 반환합니다. 메서드가 null을 반환하면 연결이 거부됩니다.

클라이언트가 서비스에 연결하고 미디어 콘텐츠를 탐색할 수 있도록 하려면 onGetRoot()는 콘텐츠 계층 구조를 나타내는 루트 ID인 null이 아닌 BrowserRoot를 반환해야 합니다.

클라이언트가 탐색 없이 MediaSession에 연결하도록 허용하려면 onGetRoot()는 계속해서 null이 아닌 BrowserRoot를 반환해야 하지만 루트 ID는 빈 콘텐츠 계층 구조를 나타내야 합니다.

onGetRoot()의 일반적인 구현은 다음과 같습니다.

Kotlin

override fun onGetRoot(
        clientPackageName: String,
        clientUid: Int,
        rootHints: Bundle?
): MediaBrowserServiceCompat.BrowserRoot {

    // (Optional) Control the level of access for the specified package name.
    // You'll need to write your own logic to do this.
    return if (allowBrowsing(clientPackageName, clientUid)) {
        // Returns a root ID that clients can use with onLoadChildren() to retrieve
        // the content hierarchy.
        MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null)
    } else {
        // Clients can connect, but this BrowserRoot is an empty hierarchy
        // so onLoadChildren returns nothing. This disables the ability to browse for content.
        MediaBrowserServiceCompat.BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null)
    }
}

Java

@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
    Bundle rootHints) {

    // (Optional) Control the level of access for the specified package name.
    // You'll need to write your own logic to do this.
    if (allowBrowsing(clientPackageName, clientUid)) {
        // Returns a root ID that clients can use with onLoadChildren() to retrieve
        // the content hierarchy.
        return new BrowserRoot(MY_MEDIA_ROOT_ID, null);
    } else {
        // Clients can connect, but this BrowserRoot is an empty hierarchy
        // so onLoadChildren returns nothing. This disables the ability to browse for content.
        return new BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null);
    }
}

경우에 따라 MediaBrowserService에 연결할 수 있는 사용자를 제어해야 할 수도 있습니다. 한 가지 방법은 허용되는 연결을 지정하거나 금지해야 하는 연결을 열거하는 액세스 제어 목록 (ACL)을 사용하는 것입니다. 특정 연결을 허용하는 ACL을 구현하는 방법의 예는 범용 Android 뮤직 플레이어 샘플 앱의 PackageValidator 클래스를 참고하세요.

쿼리를 실행하는 클라이언트의 유형에 따라 다른 콘텐츠 계층 구조를 제공하는 것이 좋습니다. 특히 Android Auto는 사용자가 오디오 앱과 상호작용하는 방식을 제한합니다. 자세한 내용은 자동으로 오디오 재생을 참고하세요. 연결 시 clientPackageName를 확인하여 클라이언트 유형을 확인하고 클라이언트에 따라 다른 BrowserRoot (또는 있는 경우 rootHints)를 반환할 수 있습니다.

onLoadChildren()으로 콘텐츠 전달

클라이언트는 연결 이후 MediaBrowserCompat.subscribe()의 반복 호출로 UI의 로컬 표현을 빌드하여 콘텐츠 계층 구조를 순회할 수 있습니다. subscribe() 메서드는 onLoadChildren() 콜백을 서비스로 전송합니다. 그러면 MediaBrowser.MediaItem 객체의 목록이 반환됩니다.

각 MediaItem에는 불투명한 토큰인 고유 ID 문자열이 있습니다. 클라이언트는 하위 메뉴를 열거나 항목을 재생하려는 경우 ID를 전달합니다. 서비스는 ID를 적절한 메뉴 노드 또는 콘텐츠 항목과 연결하는 역할을 합니다.

onLoadChildren()의 간단한 구현은 다음과 같습니다.

Kotlin

override fun onLoadChildren(
        parentMediaId: String,
        result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>
) {
    //  Browsing not allowed
    if (MY_EMPTY_MEDIA_ROOT_ID == parentMediaId) {
        result.sendResult(null)
        return
    }

    // Assume for example that the music catalog is already loaded/cached.

    val mediaItems = emptyList<MediaBrowserCompat.MediaItem>()

    // Check if this is the root menu:
    if (MY_MEDIA_ROOT_ID == parentMediaId) {
        // Build the MediaItem objects for the top level,
        // and put them in the mediaItems list...
    } else {
        // Examine the passed parentMediaId to see which submenu we're at,
        // and put the children of that menu in the mediaItems list...
    }
    result.sendResult(mediaItems)
}

Java

@Override
public void onLoadChildren(final String parentMediaId,
    final Result<List<MediaItem>> result) {

    //  Browsing not allowed
    if (TextUtils.equals(MY_EMPTY_MEDIA_ROOT_ID, parentMediaId)) {
        result.sendResult(null);
        return;
    }

    // Assume for example that the music catalog is already loaded/cached.

    List<MediaItem> mediaItems = new ArrayList<>();

    // Check if this is the root menu:
    if (MY_MEDIA_ROOT_ID.equals(parentMediaId)) {
        // Build the MediaItem objects for the top level,
        // and put them in the mediaItems list...
    } else {
        // Examine the passed parentMediaId to see which submenu we're at,
        // and put the children of that menu in the mediaItems list...
    }
    result.sendResult(mediaItems);
}

참고: MediaBrowserService에서 제공하는 MediaItem 객체에는 아이콘 비트맵이 포함되면 안 됩니다. 각 항목의 MediaDescription를 빌드할 때 setIconUri()를 호출하여 Uri를 대신 사용하세요.

onLoadChildren()을 구현하는 방법의 예는 유니버설 Android 음악 플레이어 샘플 앱을 참조하세요.

미디어 브라우저 서비스 수명 주기

Android 서비스의 동작은 서비스가 시작되었는지 또는 하나 이상의 클라이언트에 바인딩되었는지에 따라 다릅니다. 서비스를 만들었으면 시작하거나 바인딩하거나 둘 다 할 수 있습니다. 서비스는 이 모든 상태에서 완벽하게 작동하며 설계된 작업을 실행할 수 있습니다. 차이점은 서비스가 존재하는 기간입니다. 바인딩된 서비스는 바인딩된 클라이언트가 모두 바인딩 해제될 때까지 제거되지 않습니다. 시작된 서비스는 명시적으로 중지 및 제거할 수 있습니다(더 이상 어떤 클라이언트에도 바인딩되지 않았다고 가정).

다른 활동에서 실행 중인 MediaBrowserMediaBrowserService에 연결되면 활동을 서비스에 바인딩하여 서비스를 바인딩 상태로 만듭니다(그러나 시작되지는 않음). 이 기본 동작은 MediaBrowserServiceCompat 클래스에 빌드됩니다.

모든 클라이언트의 바인딩이 해제되면, 바인딩되었으나 시작되지 않은 서비스만 제거됩니다. 이 시점에서 UI 활동 연결이 해제되면 서비스가 제거됩니다. 아직 음악을 재생하지 않은 경우에는 문제가 되지 않습니다. 하지만 재생이 시작되면 사용자는 앱을 전환한 후에도 계속 들을 수 있을 것으로 기대합니다. 다른 앱을 사용하기 위해 UI 바인딩을 해제할 때 플레이어를 제거하고 싶지 않을 것입니다.

따라서 재생이 시작될 때 startService()를 호출하여 서비스가 시작되도록 해야 합니다. 시작된 서비스는 바인딩 여부와 관계없이 명시적으로 중지되어야 합니다. 이렇게 하면 제어 UI 활동의 바인딩이 해제되더라도 플레이어가 계속 게임을 실행할 수 있습니다.

시작된 서비스를 중지하려면 Context.stopService() 또는 stopSelf()를 호출합니다. 시스템은 가능한 한 빨리 서비스를 중지하고 제거합니다. 하지만 하나 이상의 클라이언트가 여전히 서비스에 바인딩된 경우 모든 클라이언트의 바인딩이 해제될 때까지 서비스 중지 호출이 지연됩니다.

MediaBrowserService의 수명 주기는 생성된 방식, 바인딩된 클라이언트 수 및 미디어 세션 콜백에서 수신하는 호출에 의해 제어됩니다. 요약:

  • 미디어 버튼에 대한 응답으로 시작될 때 또는 활동이 바인딩될 때(MediaBrowser를 통해 연결된 후) 서비스가 생성됩니다.
  • 미디어 세션 onPlay() 콜백에는 startService()를 호출하는 코드를 포함해야 합니다. 그러면 바인딩된 모든 UI MediaBrowser 활동의 바인딩이 해제되어도 서비스가 시작되고 계속 실행됩니다.
  • onStop() 콜백은 stopSelf()를 호출해야 합니다. 서비스가 시작되었다면 이를 통해 서비스가 중지됩니다. 또한 바인딩된 활동이 없으면 서비스가 제거됩니다. 아니면 모든 활동의 바인딩이 해제될 때까지 서비스가 바인딩된 상태로 유지됩니다. (서비스가 제거되기 전에 후속 startService() 호출이 수신되면 대기 중인 중지가 취소됩니다.)

다음 순서도는 서비스의 수명 주기 관리 방법을 보여줍니다. 변수 counter는 바인딩된 클라이언트 수를 추적합니다.

서비스 수명 주기

포그라운드 서비스에서 MediaStyle 알림 사용

재생 중인 서비스는 포그라운드에서 실행되어야 합니다. 그러면 시스템은 서비스가 유용한 기능을 실행 중임을 알게 되고 시스템 메모리가 부족해도 서비스를 종료하지 않습니다. 포그라운드 서비스는 사용자가 인지하고 선택적으로 제어할 수 있도록 알림을 표시해야 합니다. onPlay() 콜백은 서비스를 포그라운드에 두어야 합니다. (이는 '포그라운드'의 특별한 의미입니다. Android는 프로세스 관리를 위해 서비스가 포그라운드에 있는 것으로 간주하지만, 사용자 입장에서는 화면의 '포그라운드'에 다른 앱이 표시되면 플레이어는 백그라운드에서 재생되는 것입니다.)

서비스는 포그라운드에서 실행될 때 하나 이상의 전송 컨트롤과 함께 알림을 표시해야 합니다. 세션의 메타데이터에서 오는 유용한 정보도 알림에 포함해야 합니다.

플레이어가 재생을 시작할 때 알림을 빌드하고 표시하세요. 이 작업은 MediaSessionCompat.Callback.onPlay() 메서드 내부에서 실행하는 것이 가장 좋습니다.

아래 예에서는 미디어 앱용으로 설계된 NotificationCompat.MediaStyle를 사용합니다. 메타데이터 및 전송 컨트롤을 표시하는 알림의 빌드 방법을 보여줍니다. 편의 메서드 getController()를 사용하면 미디어 세션에서 직접 미디어 컨트롤러를 만들 수 있습니다.

Kotlin

// Given a media session and its context (usually the component containing the session)
// Create a NotificationCompat.Builder

// Get the session's metadata
val controller = mediaSession.controller
val mediaMetadata = controller.metadata
val description = mediaMetadata.description

val builder = NotificationCompat.Builder(context, channelId).apply {
    // Add the metadata for the currently playing track
    setContentTitle(description.title)
    setContentText(description.subtitle)
    setSubText(description.description)
    setLargeIcon(description.iconBitmap)

    // Enable launching the player by clicking the notification
    setContentIntent(controller.sessionActivity)

    // Stop the service when the notification is swiped away
    setDeleteIntent(
            MediaButtonReceiver.buildMediaButtonPendingIntent(
                    context,
                    PlaybackStateCompat.ACTION_STOP
            )
    )

    // Make the transport controls visible on the lockscreen
    setVisibility(NotificationCompat.VISIBILITY_PUBLIC)

    // Add an app icon and set its accent color
    // Be careful about the color
    setSmallIcon(R.drawable.notification_icon)
    color = ContextCompat.getColor(context, R.color.primaryDark)

    // Add a pause button
    addAction(
            NotificationCompat.Action(
                    R.drawable.pause,
                    getString(R.string.pause),
                    MediaButtonReceiver.buildMediaButtonPendingIntent(
                            context,
                            PlaybackStateCompat.ACTION_PLAY_PAUSE
                    )
            )
    )

    // Take advantage of MediaStyle features
    setStyle(android.support.v4.media.app.NotificationCompat.MediaStyle()
            .setMediaSession(mediaSession.sessionToken)
            .setShowActionsInCompactView(0)

            // Add a cancel button
            .setShowCancelButton(true)
            .setCancelButtonIntent(
                    MediaButtonReceiver.buildMediaButtonPendingIntent(
                            context,
                            PlaybackStateCompat.ACTION_STOP
                    )
            )
    )
}

// Display the notification and place the service in the foreground
startForeground(id, builder.build())

Java

// Given a media session and its context (usually the component containing the session)
// Create a NotificationCompat.Builder

// Get the session's metadata
MediaControllerCompat controller = mediaSession.getController();
MediaMetadataCompat mediaMetadata = controller.getMetadata();
MediaDescriptionCompat description = mediaMetadata.getDescription();

NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId);

builder
    // Add the metadata for the currently playing track
    .setContentTitle(description.getTitle())
    .setContentText(description.getSubtitle())
    .setSubText(description.getDescription())
    .setLargeIcon(description.getIconBitmap())

    // Enable launching the player by clicking the notification
    .setContentIntent(controller.getSessionActivity())

    // Stop the service when the notification is swiped away
    .setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(context,
       PlaybackStateCompat.ACTION_STOP))

    // Make the transport controls visible on the lockscreen
    .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)

    // Add an app icon and set its accent color
    // Be careful about the color
    .setSmallIcon(R.drawable.notification_icon)
    .setColor(ContextCompat.getColor(context, R.color.primaryDark))

    // Add a pause button
    .addAction(new NotificationCompat.Action(
        R.drawable.pause, getString(R.string.pause),
        MediaButtonReceiver.buildMediaButtonPendingIntent(context,
            PlaybackStateCompat.ACTION_PLAY_PAUSE)))

    // Take advantage of MediaStyle features
    .setStyle(new MediaStyle()
        .setMediaSession(mediaSession.getSessionToken())
        .setShowActionsInCompactView(0)

        // Add a cancel button
       .setShowCancelButton(true)
       .setCancelButtonIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(context,
           PlaybackStateCompat.ACTION_STOP)));

// Display the notification and place the service in the foreground
startForeground(id, builder.build());

MediaStyle 알림을 사용할 때 이러한 NotificationCompat 설정의 동작을 알아야 합니다.

  • setContentIntent()를 사용하는 경우 알림을 클릭하면 서비스가 자동으로 시작됩니다. 이는 편리한 기능입니다.
  • 잠금 화면과 같은 '신뢰할 수 없는' 상황에서 알림 콘텐츠의 기본 공개 상태는 VISIBILITY_PRIVATE입니다. 잠금 화면에서 전송 컨트롤을 확인하고 싶을 수 있으므로 VISIBILITY_PUBLIC를 사용하면 됩니다.
  • 배경색을 설정할 때는 주의해야 합니다. Android 버전 5.0 이상의 일반 알림에서는 색상이 작은 앱 아이콘의 배경에만 적용됩니다. 그러나 Android 7.0 이전의 MediaStyle 알림에서는 색상이 전체 알림 배경에 사용됩니다. 배경색을 테스트합니다. 눈을 조심스럽게 가라앉히고 지나치게 밝거나 형광색을 피합니다.

NotificationCompat.MediaStyle을 사용하는 경우에만 다음 설정을 사용할 수 있습니다.

  • setMediaSession()를 사용하여 알림을 세션에 연결합니다. 이렇게 하면 서드 파티 앱 및 호환 기기에서 세션에 액세스하고 세션을 제어할 수 있습니다.
  • setShowActionsInCompactView()를 사용하여 알림의 표준 크기 contentView에 표시할 작업을 최대 3개까지 추가합니다. (여기에 일시중지 버튼이 지정되어 있습니다.)
  • Android 5.0 (API 수준 21) 이상에서는 서비스가 포그라운드에서 더 이상 실행되지 않으면 알림을 스와이프하여 플레이어를 중지할 수 있습니다. 이전 버전에서는 이를 수행할 수 없습니다. Android 5.0 (API 수준 21) 이전에 사용자가 알림을 삭제하고 재생을 중지할 수 있도록 하려면 setShowCancelButton(true)setCancelButtonIntent()를 호출하여 알림의 오른쪽 상단에 취소 버튼을 추가하면 됩니다.

일시중지 및 취소 버튼을 추가할 때 재생 작업에 연결할 PendingIntent가 필요합니다. MediaButtonReceiver.buildMediaButtonPendingIntent() 메서드는 PlaybackState 작업을 PendingIntent로 변환하는 작업을 실행합니다.