تشغيل المحتوى في الخلفية باستخدام MediaSessionService

غالبًا ما يكون من المفضّل تشغيل الوسائط عندما لا يكون التطبيق في المقدّمة. بالنسبة على سبيل المثال، يواصل مشغِّل الموسيقى بشكلٍ عام تشغيل الموسيقى عندما يكون المستخدم مقفلاً جهازه أو يستخدم تطبيقًا آخر. توفر مكتبة Media3 سلسلة من واجهات تسمح لك بتشغيل المحتوى في الخلفية.

استخدام MediaSessionService

لتفعيل التشغيل في الخلفية، يجب أن تحتوي على Player MediaSession داخل خدمة منفصلة. يسمح هذا الإجراء للجهاز بمواصلة عرض الوسائط حتى إذا لم يكن تطبيقك قيد التشغيل. المقدمة.

تسمح MediaSessionService بتشغيل جلسة الوسائط بشكل منفصل
  من نشاط التطبيق
الشكل 1: تسمح MediaSessionService بالوسائط جلسة منفصلة عن نشاط التطبيق

عند استضافة مشغّل داخل إحدى الخدمات، يجب استخدام MediaSessionService. لإجراء ذلك، يمكنك إنشاء صف يمتد إلى MediaSessionService وإنشاء جلسة وسائط متعددة داخله.

يتيح استخدام MediaSessionService للعملاء الخارجيين مثل Google يمكنك استكشاف تطبيقات "مساعد Google" أو عناصر التحكّم في وسائط النظام أو الأجهزة المصاحبة مثل Wear OS الخدمة، والاتصال بها، والتحكم في التشغيل، كل ذلك بدون الدخول إلى نشاط واجهة المستخدم للتطبيق على الإطلاق. في الواقع، يمكن أن يكون هناك العديد من تطبيقات العميل المرتبطة إلى MediaSessionService نفسه في الوقت نفسه، ولكل تطبيق خاص به MediaController

تنفيذ دورة حياة الخدمة

عليك تنفيذ ثلاث طرق لمراحل نشاط خدمتك:

  • يتم استدعاء onCreate() عندما توشك وحدة التحكم الأولى على الاتصال يتم إنشاء مثيل للخدمة وبدء تشغيلها. إنه أفضل مكان لبناء Player MediaSession
  • يتم استدعاء onTaskRemoved(Intent) عندما يغلق المستخدم التطبيق من المهام الأخيرة. إذا كان التشغيل مستمرًا، يمكن للتطبيق اختيار الاحتفاظ بالخدمة قيد التشغيل في المقدّمة. إذا تم إيقاف المشغّل مؤقتًا، فلن تكون الخدمة ضمن في المقدمة ويجب إيقافها.
  • يتم استدعاء onDestroy() عند إيقاف الخدمة. جميع الموارد بما في ذلك إصدار اللاعبين والجلسة.

Kotlin

class PlaybackService : MediaSessionService() {
  private var mediaSession: MediaSession? = null

  // Create your player and media session in the onCreate lifecycle event
  override fun onCreate() {
    super.onCreate()
    val player = ExoPlayer.Builder(this).build()
    mediaSession = MediaSession.Builder(this, player).build()
  }

  // The user dismissed the app from the recent tasks
  override fun onTaskRemoved(rootIntent: Intent?) {
    val player = mediaSession?.player!!
    if (!player.playWhenReady
        || player.mediaItemCount == 0
        || player.playbackState == Player.STATE_ENDED) {
      // Stop the service if not playing, continue playing in the background
      // otherwise.
      stopSelf()
    }
  }

  // Remember to release the player and media session in onDestroy
  override fun onDestroy() {
    mediaSession?.run {
      player.release()
      release()
      mediaSession = null
    }
    super.onDestroy()
  }
}

Java

public class PlaybackService extends MediaSessionService {
  private MediaSession mediaSession = null;

  // Create your Player and MediaSession in the onCreate lifecycle event
  @Override
  public void onCreate() {
    super.onCreate();
    ExoPlayer player = new ExoPlayer.Builder(this).build();
    mediaSession = new MediaSession.Builder(this, player).build();
  }

  // The user dismissed the app from the recent tasks
  @Override
  public void onTaskRemoved(@Nullable Intent rootIntent) {
    Player player = mediaSession.getPlayer();
    if (!player.getPlayWhenReady()
        || player.getMediaItemCount() == 0
        || player.getPlaybackState() == Player.STATE_ENDED) {
      // Stop the service if not playing, continue playing in the background
      // otherwise.
      stopSelf();
    }
  }

  // Remember to release the player and media session in onDestroy
  @Override
  public void onDestroy() {
    mediaSession.getPlayer().release();
    mediaSession.release();
    mediaSession = null;
    super.onDestroy();
  }
}

بدلاً من مواصلة التشغيل في الخلفية، يمكن للتطبيق إيقاف الخدمة في أي حالة عندما يرفض المستخدم التطبيق:

Kotlin

override fun onTaskRemoved(rootIntent: Intent?) {
  val player = mediaSession.player
  if (player.playWhenReady) {
    // Make sure the service is not in foreground.
    player.pause()
  }
  stopSelf()
}

Java

@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  Player player = mediaSession.getPlayer();
  if (player.getPlayWhenReady()) {
    // Make sure the service is not in foreground.
    player.pause();
  }
  stopSelf();
}

توفير الوصول إلى جلسة الوسائط

إلغاء طريقة onGetSession() لمنح البرامج الأخرى الإذن بالوصول إلى الوسائط الخاصة بك جلسة تم إنشاؤها عند إنشاء الخدمة.

Kotlin

class PlaybackService : MediaSessionService() {
  private var mediaSession: MediaSession? = null
  // [...] lifecycle methods omitted

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
    mediaSession
}

Java

public class PlaybackService extends MediaSessionService {
  private MediaSession mediaSession = null;
  // [...] lifecycle methods omitted

  @Override
  public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    return mediaSession;
  }
}

يُرجى تعريف الخدمة في البيان.

يتطلّب التطبيق إذنًا لتشغيل خدمة تعمل في المقدّمة. إضافة FOREGROUND_SERVICE في ملف البيان، وإذا كنت تستهدف واجهة برمجة التطبيقات 34 أعلى أيضًا FOREGROUND_SERVICE_MEDIA_PLAYBACK:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

عليك أيضًا الإفصاح عن الفئة Service في البيان باستخدام فلتر أهداف. من MediaSessionService.

<service
    android:name=".PlaybackService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="true">
    <intent-filter>
        <action android:name="androidx.media3.session.MediaSessionService"/>
    </intent-filter>
</service>

يجب عليك تحديد foregroundServiceType يتضمّن mediaPlayback عندما يعمل تطبيقك على جهاز Android. 10 (مستوى واجهة برمجة التطبيقات 29) والإصدارات الأحدث.

التحكّم في التشغيل باستخدام MediaController

في النشاط أو الجزء الذي يحتوي على واجهة مستخدم المشغل، يمكنك إنشاء رابط بين واجهة المستخدم وجلسة الوسائط باستخدام MediaController. تستخدمها واجهة المستخدم وحدة التحكم في الوسائط لإرسال الأوامر من واجهة المستخدم إلى المشغّل داخل جلسة المراجعة. يمكنك الاطّلاع على إنشاء MediaController دليل لمعرفة تفاصيل حول إنشاء MediaController واستخدامه.

التعامل مع أوامر واجهة المستخدم

يستقبل "MediaSession" الأوامر من وحدة التحكّم من خلال MediaSession.Callback يؤدي إعداد MediaSession إلى إنشاء إعداد تلقائي تنفيذ MediaSession.Callback الذي يعالج تلقائيًا كل الأوامر التي يرسلها MediaController إلى المشغّل.

إشعار

تنشئ MediaSessionService تلقائيًا MediaNotification لك ينبغي أن يعمل في معظم الحالات. يكون الإشعار المنشور تلقائيًا إشعار MediaStyle يظل على اطّلاع بأحدث المعلومات من جلسة الوسائط ويعرض عناصر التحكم في التشغيل. MediaNotification على عِلم بجلستك ويمكن استخدامها للتحكّم في تشغيل أي تطبيقات أخرى المرتبطة بالجلسة نفسها.

على سبيل المثال، سيُنشئ تطبيق لبث الموسيقى الذي يستخدم MediaSessionService MediaNotification التي تعرض العنوان والفنان وصورة الألبوم عنصر الوسائط الحالي الذي يتم تشغيله إلى جانب عناصر التحكم في التشغيل استنادًا إلى إعداد MediaSession.

يمكن تقديم البيانات الوصفية المطلوبة في وسائل الإعلام أو الإعلان عنها كجزء من ملف وسائط كما في المقتطف التالي:

Kotlin

val mediaItem =
    MediaItem.Builder()
      .setMediaId("media-1")
      .setUri(mediaUri)
      .setMediaMetadata(
        MediaMetadata.Builder()
          .setArtist("David Bowie")
          .setTitle("Heroes")
          .setArtworkUri(artworkUri)
          .build()
      )
      .build()

mediaController.setMediaItem(mediaItem)
mediaController.prepare()
mediaController.play()

Java

MediaItem mediaItem =
    new MediaItem.Builder()
        .setMediaId("media-1")
        .setUri(mediaUri)
        .setMediaMetadata(
            new MediaMetadata.Builder()
                .setArtist("David Bowie")
                .setTitle("Heroes")
                .setArtworkUri(artworkUri)
                .build())
        .build();

mediaController.setMediaItem(mediaItem);
mediaController.prepare();
mediaController.play();

يمكن للتطبيقات تخصيص أزرار الأوامر لعناصر التحكّم في Android Media. مزيد من المعلومات حول تخصيص Android Media عناصر التحكم.

تخصيص الإشعارات

لتخصيص الإشعار، يمكنك إنشاء MediaNotification.Provider مع DefaultMediaNotificationProvider.Builder أو عن طريق إنشاء تنفيذ مخصص لواجهة الموفر. إضافة مقدِّم الخدمة إلى MediaSessionService من خلال setMediaNotificationProvider

استئناف التشغيل

أزرار الوسائط هي أزرار أجهزة تظهر على أجهزة Android والأجهزة الملحقة الأخرى الأجهزة، مثل زر التشغيل أو الإيقاف المؤقت على سماعة الرأس التي تعمل بالبلوتوث. الوسائط 3 معالجة إدخالات أزرار الوسائط نيابةً عنك أثناء تشغيل الخدمة.

تعريف مستقبل زر الوسائط Media3

Media3 يتضمن واجهة برمجة تطبيقات لتمكين المستخدمين من الاستئناف التشغيل بعد إنهاء التطبيق وحتى بعد إغلاق الجهاز تمت إعادة التشغيل. ويكون استئناف التشغيل متوقفًا تلقائيًا. هذا يعني أن المستخدم تتعذر استئناف التشغيل عندما لا تكون الخدمة قيد التشغيل. لتفعيل الميزة، عليك البدء بيان MediaButtonReceiver في البيان:

<receiver android:name="androidx.media3.session.MediaButtonReceiver"
  android:exported="true">
  <intent-filter>
    <action android:name="android.intent.action.MEDIA_BUTTON" />
  </intent-filter>
</receiver>

تنفيذ معاودة الاتصال لاستئناف التشغيل

عند طلب استئناف التشغيل من خلال جهاز بلوتوث أو ميزة استئناف واجهة مستخدم نظام Android، onPlaybackResumption() استدعاء طريقة معاودة الاتصال.

Kotlin

override fun onPlaybackResumption(
    mediaSession: MediaSession,
    controller: ControllerInfo
): ListenableFuture<MediaItemsWithStartPosition> {
  val settable = SettableFuture.create<MediaItemsWithStartPosition>()
  scope.launch {
    // Your app is responsible for storing the playlist and the start position
    // to use here
    val resumptionPlaylist = restorePlaylist()
    settable.set(resumptionPlaylist)
  }
  return settable
}

Java

@Override
public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
    MediaSession mediaSession,
    ControllerInfo controller
) {
  SettableFuture<MediaItemsWithStartPosition> settableFuture = SettableFuture.create();
  settableFuture.addListener(() -> {
    // Your app is responsible for storing the playlist and the start position
    // to use here
    MediaItemsWithStartPosition resumptionPlaylist = restorePlaylist();
    settableFuture.set(resumptionPlaylist);
  }, MoreExecutors.directExecutor());
  return settableFuture;
}

إذا كنت قد خزّنت معلَمات أخرى، مثل سرعة التشغيل أو وضع التكرار وضع الترتيب العشوائي، onPlaybackResumption() هو مكان مناسب لإعداد المشغّل بهذه المعلمات قبل أن يجهّز Media3 المشغّل ويبدأ التشغيل عند يكتمل معاودة الاتصال.

التكوين المتقدم لوحدة التحكم والتوافق مع الأنظمة القديمة

هناك سيناريو شائع وهو استخدام MediaController في واجهة مستخدم التطبيق للتحكم في لتشغيل وعرض قائمة التشغيل. في الوقت نفسه، تظهر الجلسة للعملاء الخارجيين مثل عناصر التحكّم في وسائط Android و"مساعد Google" على الأجهزة الجوّالة أو التلفزيون Wear OS للساعات وAndroid Auto في السيارات التطبيق التجريبي لجلسة Media3 مثالاً على تطبيق ينفذ هذا السيناريو.

قد يستخدم هؤلاء العملاء الخارجيون واجهات برمجة تطبيقات مثل MediaControllerCompat من واجهات برمجة التطبيقات القديمة. مكتبة AndroidX أو android.media.session.MediaController من نظام Android إطار العمل. تتوافق الوسائط 3 تمامًا مع الأنظمة القديمة مع المكتبة القديمة توفّر إمكانية التشغيل التفاعلي مع واجهة برمجة تطبيقات إطار عمل Android.

استخدام وحدة التحكّم بإشعارات الوسائط

ومن المهم أن تفهم أن وحدات التحكم القديمة أو وحدات التحكم في إطارات العمل هذه تقرأ القيم نفسها من إطار العمل PlaybackState.getActions() PlaybackState.getCustomActions() لتحديد الإجراءات والإجراءات المخصصة لجلسة إطار العمل، يمكن للتطبيق استخدام وحدة التحكم في إشعارات الوسائط وتعيين أوامره المتاحة والتنسيق المخصص. تربط الخدمة الوسائط إلى جلستك، وستستخدم الجلسة ConnectionResult الذي تم عرضه من خلال onConnect() الخاص بمعاودة الاتصال لإعداده الإجراءات والإجراءات المخصصة لجلسة إطار العمل.

وبالنظر إلى سيناريو الهاتف المحمول فقط، يمكن للتطبيق توفير تنفيذ MediaSession.Callback.onConnect() لضبط الأوامر المتاحة تخطيط مخصص لجلسة إطار العمل على النحو التالي:

Kotlin

override fun onConnect(
  session: MediaSession,
  controller: MediaSession.ControllerInfo
): ConnectionResult {
  if (session.isMediaNotificationController(controller)) {
    val sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(customCommandSeekBackward)
        .add(customCommandSeekForward)
        .build()
    val playerCommands =
      ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
        .remove(COMMAND_SEEK_TO_PREVIOUS)
        .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
        .remove(COMMAND_SEEK_TO_NEXT)
        .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
        .build()
    // Custom layout and available commands to configure the legacy/framework session.
    return AcceptedResultBuilder(session)
      .setCustomLayout(
        ImmutableList.of(
          createSeekBackwardButton(customCommandSeekBackward),
          createSeekForwardButton(customCommandSeekForward))
      )
      .setAvailablePlayerCommands(playerCommands)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default custom layout for all other controllers.
  return AcceptedResultBuilder(session).build()
}

Java

@Override
public ConnectionResult onConnect(
    MediaSession session, MediaSession.ControllerInfo controller) {
  if (session.isMediaNotificationController(controller)) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS
            .buildUpon()
            .add(customCommandSeekBackward)
            .add(customCommandSeekForward)
            .build();
    Player.Commands playerCommands =
        ConnectionResult.DEFAULT_PLAYER_COMMANDS
            .buildUpon()
            .remove(COMMAND_SEEK_TO_PREVIOUS)
            .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
            .remove(COMMAND_SEEK_TO_NEXT)
            .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
            .build();
    // Custom layout and available commands to configure the legacy/framework session.
    return new AcceptedResultBuilder(session)
        .setCustomLayout(
            ImmutableList.of(
                createSeekBackwardButton(customCommandSeekBackward),
                createSeekForwardButton(customCommandSeekForward)))
        .setAvailablePlayerCommands(playerCommands)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands without default custom layout for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

السماح لتطبيق Android Auto بإرسال أوامر مخصَّصة

عند استخدام MediaLibraryService ولدعم Android Auto مع تطبيق الأجهزة الجوّالة، تتوفر وحدة التحكم Android Auto يتطلب أوامر متاحة مناسبة، وإلا فسيرفض Media3 الأوامر المخصصة الواردة من وحدة التحكم هذه:

Kotlin

override fun onConnect(
  session: MediaSession,
  controller: MediaSession.ControllerInfo
): ConnectionResult {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
      .add(customCommandSeekBackward)
      .add(customCommandSeekForward)
      .build()
  if (session.isMediaNotificationController(controller)) {
    // [...] See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available session commands to accept incoming custom commands from Auto.
    return AcceptedResultBuilder(session)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default custom layout for all other controllers.
  return AcceptedResultBuilder(session).build()
}

Java

@Override
public ConnectionResult onConnect(
    MediaSession session, MediaSession.ControllerInfo controller) {
  SessionCommands sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS
          .buildUpon()
          .add(customCommandSeekBackward)
          .add(customCommandSeekForward)
          .build();
  if (session.isMediaNotificationController(controller)) {
    // [...] See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands without default custom layout for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

يحتوي التطبيق التجريبي للجلسة على وحدة للسيارات الذي يوضّح توافقه مع نظام التشغيل Automotive الذي يتطلّب حزمة APK منفصلة.