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

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

استخدام MediaSessionService

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

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

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

فاستخدام MediaSessionService يتيح للعملاء الخارجيين، مثل "مساعد 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. تعرَّف على المزيد من المعلومات عن تخصيص عناصر التحكُّم في وسائط Android.

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

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

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

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

تعريف استقبال زر الوسائط 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. تتوافق منصة Media3 تمامًا مع الأنظمة القديمة مع المكتبة القديمة وتوفِّر إمكانية التشغيل التفاعلي مع واجهة برمجة التطبيقات لإطار عمل 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 منفصلة.