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

앱은 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)
        }
    }
}

자바

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() 제어 서비스 액세스 권한 <ph type="x-smartling-placeholder">onLoadChildren()</ph> 클라이언트가 MediaBrowserService의 콘텐츠 계층 구조 메뉴를 빌드하고 표시할 수 있는 기능을 제공합니다.

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

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

클라이언트가 서비스에 연결하고 미디어 콘텐츠를 탐색할 수 있도록 하려면 onGetRoot()는 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)
    }
}

자바

@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을 구현하는 방법의 예는 자세히 알아보려면 PackageValidator 유니버설 Android 뮤직 플레이어의 클래스 샘플 앱입니다

사용자의 콘텐츠 유형에 따라 다른 콘텐츠 계층 구조를 쿼리를 실행하는 클라이언트의 유형 특히 Android Auto는 사용자가 오디오 앱과 상호작용을 하게 됩니다 자세한 내용은 다음 기기에서 오디오 재생 자동. 나 연결 시 clientPackageName를 보고 클라이언트를 확인할 수 있습니다. 클라이언트 (또는 rootHints)에 따라 다른 BrowserRoot을 반환합니다. 해당하는 경우).

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

자바

@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 객체 아이콘 비트맵을 포함하지 않아야 합니다. 대신 다음을 호출하여 Uri 사용 setIconUri() MediaDescription 를 생성합니다.

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

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

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

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

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

이러한 이유로 서비스가 시작될 때 서비스가 시작되었는지 확인해야 합니다. startService()를 호출하여 재생할 수 있습니다. 가 바인드 여부에 관계없이 명시적으로 중지되어야 합니다. 이 제어 UI가 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())

자바

// 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()을(를) 사용하여 표시할 작업을 최대 3개까지 추가하세요. 알림의 표준 크기 contentView를 참조하세요. (여기서 일시중지 버튼은 지정합니다.)
  • Android 5.0 (API 레벨 21) 이상에서는 알림을 스와이프하여 없애고 서비스가 더 이상 포그라운드에서 실행되지 않으면 플레이어에서 재생될 수 있습니다. 할 수 없는 작업 이전 버전에서는 이를 살펴봤습니다 사용자가 알림을 삭제하고 재생을 중지할 수 있도록 허용하기 위해 Android 5.0 (API 레벨 21) 이전에는 오른쪽 상단에 취소 버튼을 추가하여 setShowCancelButton(true)setCancelButtonIntent()를 호출하여 알림을 전송합니다.

일시중지 버튼과 취소 버튼을 추가할 때는 연결할 PendingIntent가 필요합니다. 추가할 수 있습니다. MediaButtonReceiver.buildMediaButtonPendingIntent() 메서드는 PlaybackState 작업을 PendingIntent로 변환합니다.