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

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

استخدام MediaSessionService

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

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

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

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

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

عليك تنفيذ طريقتَين لدورة حياة خدمتك:

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

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

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

  // 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();
  }

  // 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?) {
  pauseAllPlayersAndStopSelf()
}

Java

@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

بالنسبة إلى أي عملية تنفيذ يدوي أخرى لـ onTaskRemoved، يمكنك استخدام isPlaybackOngoing() للتحقّق مما إذا كان التشغيل مستمرًا وبدأت خدمة تعمل في المقدّمة.

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

يمكنك إلغاء طريقة 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 وFOREGROUND_SERVICE_MEDIA_PLAYBACK لتشغيل خدمة تعمل في المقدّمة خاصة بالتشغيل:

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

يجب أيضًا تعريف الفئة Service في ملف البيان باستخدام فلتر أهداف بالقيمة MediaSessionService وforegroundServiceType يتضمّن mediaPlayback.

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

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

في النشاط أو الجزء الذي يحتوي على واجهة مستخدم المشغّل، يمكنك إنشاء رابط بين واجهة المستخدم وجلسة الوسائط باستخدام 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();

مراحل نشاط الإشعارات

يتم إنشاء الإشعار فور أن يتضمّن Player MediaItem مثيلاً في قائمة التشغيل.

يتم تعديل جميع الإشعارات تلقائيًا استنادًا إلى حالة Player وMediaSession.

لا يمكن إزالة الإشعار أثناء تشغيل الخدمة التي تعمل في المقدّمة. لإزالة الإشعار على الفور، عليك الاتصال بالرقم Player.release() أو محو قائمة التشغيل باستخدام Player.clearMediaItems().

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

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

يمكن تخصيص البيانات الوصفية الخاصة بالعنصر الذي يتم تشغيله حاليًا من خلال تعديل MediaItem.MediaMetadata. إذا أردت تعديل البيانات الوصفية لعنصر حالي، يمكنك استخدام Player.replaceMediaItem لتعديل البيانات الوصفية بدون مقاطعة التشغيل.

يمكنك أيضًا تخصيص بعض الأزرار المعروضة في الإشعار من خلال ضبط الإعدادات المفضّلة المخصّصة لأزرار الوسائط في عناصر التحكّم في الوسائط على Android. مزيد من المعلومات حول تخصيص عناصر التحكّم في الوسائط على Android

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

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

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

تعريف أداة استقبال زر الوسائط في 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, metadata (like title
    // and artwork) of the current item 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, metadata (like title
    // and artwork) of the current item and the start position to use here.
    MediaItemsWithStartPosition resumptionPlaylist = restorePlaylist();
    settableFuture.set(resumptionPlaylist);
  }, MoreExecutors.directExecutor());
  return settableFuture;
}

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

يتم استدعاء هذا الإجراء أثناء وقت التشغيل لإنشاء إشعار استئناف واجهة مستخدم نظام Android بعد إعادة تشغيل الجهاز. بالنسبة إلى الإشعارات التفاعلية، يُنصح بملء حقول MediaMetadata مثل title وartworkData أو artworkUri للعنصر الحالي بقيم متاحة محليًا، لأنّه قد لا يكون الوصول إلى الشبكة متاحًا بعد. يمكنك أيضًا إضافة MediaConstants.EXTRAS_KEY_COMPLETION_STATUS و MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE إلى MediaMetadata.extras للإشارة إلى موضع استئناف التشغيل.

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

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

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

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

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

على سبيل المثال، يمكن لأحد التطبيقات توفير تنفيذ 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 button preferences and commands to configure the platform session.
    return AcceptedResultBuilder(session)
      .setMediaButtonPreferences(
        ImmutableList.of(
          createSeekBackwardButton(customCommandSeekBackward),
          createSeekForwardButton(customCommandSeekForward))
      )
      .setAvailablePlayerCommands(playerCommands)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default button preferences 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 button preferences and commands to configure the platform session.
    return new AcceptedResultBuilder(session)
        .setMediaButtonPreferences(
            ImmutableList.of(
                createSeekBackwardButton(customCommandSeekBackward),
                createSeekForwardButton(customCommandSeekForward)))
        .setAvailablePlayerCommands(playerCommands)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands with default button preferences 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 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 for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

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