تشغيل المحتوى في الخلفية باستخدام 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 MediaSession 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()
  }

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

Java

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

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

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

Kotlin

@OptIn(UnstableApi::class)
override fun onTaskRemoved(rootIntent: Intent?) {
  pauseAllPlayersAndStopSelf()
}

Java

@OptIn(markerClass = UnstableApi.class)
@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

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

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

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

Kotlin

class PlaybackService : MediaSessionService() {

  // [...] lifecycle methods omitted

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

Java

class PlaybackService extends MediaSessionService {

  // [...] 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 في ملف البيان باستخدام intent filter 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();

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

يتم إنشاء الإشعار فور توفّر MediaItem نسخة من Player في قائمة التشغيل.

يتم إجراء جميع تعديلات الإشعارات تلقائيًا استنادًا إلى حالة 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: MediaSession.ControllerInfo,
  isForPlayback: Boolean,
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
  val settableFuture = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>()
  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.
      val resumptionPlaylist = restorePlaylist()
      settableFuture.set(resumptionPlaylist)
    },
    MoreExecutors.directExecutor(),
  )
  return settableFuture
}

Java

@Override
public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
    MediaSession mediaSession, ControllerInfo controller, boolean isForPlayback) {
  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 بعد إعادة تشغيل الجهاز مع ضبط isForPlayback على false. بالنسبة إلى الإشعارات الغنية، يُنصح بملء حقول MediaMetadata مثل title وartworkData أو artworkUri للعنصر الحالي باستخدام القيم المتاحة محليًا، لأنّ الوصول إلى الشبكة قد لا يكون متاحًا بعد. يمكنك أيضًا إضافة MediaConstants.EXTRAS_KEY_COMPLETION_STATUS وMediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE إلى MediaMetadata.extras للإشارة إلى موضع استئناف التشغيل.

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

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

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

تحديد وحدات التحكّم الموثوق بها

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

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

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

على سبيل المثال، يمكن أن يوفّر التطبيق تنفيذًا لـ MediaSession.Callback.onConnect() لضبط الأوامر المتاحة وإعدادات أزرار الوسائط المفضّلة خصيصًا لجلسة المنصة على النحو التالي:

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  if (session.isMediaNotificationController(controller)) {
    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 immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setMediaButtonPreferences(listOf(seekBackButton, seekForwardButton))
        .setAvailablePlayerCommands(playerCommands)
        .build()
    )
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

Java

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  if (session.isMediaNotificationController(controller)) {
    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 immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setMediaButtonPreferences(ImmutableList.of(seekBackButton, seekForwardButton))
            .setAvailablePlayerCommands(playerCommands)
            .build());
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session, controller).build());
}

السماح لـ Android Auto بإرسال أوامر مخصّصة

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

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build()
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setAvailableSessionCommands(sessionCommands)
        .build()
    )
  }
  // Default commands for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

Java

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  SessionCommands sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build();
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setAvailableSessionCommands(sessionCommands)
            .build());
  }
  // Default commands for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session, controller).build());
}

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