التحكّم في تشغيل الإعلان والإعلان عنه باستخدام MediaSession

توفر جلسات الوسائط طريقة عالمية للتفاعل مع مشغّل صوت أو فيديو. في Media3، المشغِّل التلقائي هو الفئة ExoPlayer التي تنفّذ واجهة Player. إنّ ربط جلسة الوسائط بالمشغّل يتيح للتطبيق الإعلان عن تشغيل الوسائط خارجيًا وتلقّي أوامر التشغيل من مصادر خارجية.

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

الوقت المناسب لاختيار جلسة وسائط

عند تنفيذ MediaSession، تسمح للمستخدمين بالتحكّم في التشغيل:

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

وهذا أمر رائع للعديد من حالات الاستخدام. وبشكلٍ خاص، ننصحك بشدّة باستخدام MediaSession في الحالات التالية:

  • إذا كنت تبث محتوى فيديو طويل، مثل الأفلام أو البث التلفزيوني المباشر
  • إذا كنت تبث محتوى صوتي طويل، مثل ملفات البودكاست أو قوائم التشغيل الموسيقي.
  • أنت تنشئ تطبيق تلفزيون.

يُرجى العِلم أنّ بعض حالات الاستخدام لا تتوافق جيدًا مع MediaSession. يمكنك استخدام Player فقط في الحالات التالية:

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

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

إنشاء جلسة وسائط

يتم بث جلسة الوسائط إلى جانب المُشغِّل الذي تديره. يمكنك إنشاء جلسة وسائط باستخدام كائن Context وكائن Player. يجب إنشاء جلسة وسائط وإعدادها عند الحاجة، مثل طريقة دورة حياة onStart() أو onResume() الخاصة بطريقة Activity أو Fragment أو onCreate() الخاصة بـ Service التي تملك جلسة الوسائط والمشغِّل المرتبط بها.

لإنشاء جلسة وسائط، يجب إعداد Player وتقديمه إلى "MediaSession.Builder" على النحو التالي:

Kotlin

val player = ExoPlayer.Builder(context).build()
val mediaSession = MediaSession.Builder(context, player).build()

Java

ExoPlayer player = new ExoPlayer.Builder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();

المعالجة التلقائية للحالة

وتحدِّث مكتبة Media3 تلقائيًا جلسة تشغيل الوسائط باستخدام حالة المشغّل. وبناءً على ذلك، لا تحتاج إلى التعامل يدويًا مع التعيين من لاعب إلى جلسة.

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

المعرّف الفريد للجلسة

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

إذا أراد أحد التطبيقات إدارة مثيلات جلسات متعددة في الوقت نفسه، يجب أن يتأكد التطبيق من أن معرّف الجلسة لكل جلسة فريد. يمكن ضبط معرّف الجلسة عند إنشاء الجلسة باستخدام MediaSession.Builder.setId(String id).

إذا تسببت في تعطُّل تطبيقك بسبب IllegalStateException وظهور رسالة الخطأ IllegalStateException: Session ID must be unique. ID=، من المحتمل أن تكون الجلسة قد تم إنشاؤها بشكل غير متوقّع قبل أن يتم إصدار مثيل تم إنشاؤه سابقًا باستخدام المعرّف نفسه. لتجنُّب تسريب الجلسات بسبب خطأ في البرمجة، يتم رصد مثل هذه الحالات وإبلاغها من خلال طرح استثناء.

منح إمكانية التحكم لعملاء آخرين

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

رسم تخطيطي يوضح التفاعل بين MediaSession وMediaController.
الشكل 1: تسهّل وحدة التحكّم في الوسائط تمرير الأوامر من مصادر خارجية إلى جلسة الوسائط.

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

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

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

تعديل قائمة التشغيل

يمكن لجلسة وسائط تعديل قائمة التشغيل الخاصة بالمشغّل مباشرةً كما هو موضّح في دليل ExoPlayer لقوائم التشغيل. يمكن لوحدات التحكّم أيضًا تعديل قائمة التشغيل في حال توفّر COMMAND_SET_MEDIA_ITEM أو COMMAND_CHANGE_MEDIA_ITEMS لوحدة التحكّم.

عند إضافة عناصر جديدة إلى قائمة التشغيل، يحتاج المشغّل عادةً إلى نسخ MediaItem ذات معرّف موارد منتظم (URI) محدّد لإتاحة تشغيلها. تتم تلقائيًا إعادة توجيه العناصر المضافة حديثًا إلى طرق اللاعبين مثل player.addMediaItem إذا كان لها معرّف موارد منتظم (URI) محدّد.

إذا أردت تخصيص مثيلات الـ MediaItem المُضافة إلى المشغّل، يمكنك إلغاء onAddMediaItems(). يجب تنفيذ هذه الخطوة عندما تريد دعم وحدات التحكُّم التي تطلب الوسائط بدون معرّف موارد منتظم (URI) محدّد. بدلاً من ذلك، يحتوي MediaItem عادةً على حقل واحد أو أكثر من الحقول التالية التي تم ضبطها لوصف الوسائط المطلوبة:

  • MediaItem.id: معرّف عام يحدّد الوسائط
  • MediaItem.RequestMetadata.mediaUri: عنوان URI للطلب قد يستخدم مخططًا مخصّصًا وليس بالضرورة قابلاً للتشغيل بشكل مباشر من خلال المشغّل.
  • MediaItem.RequestMetadata.searchQuery: طلب بحث نصي، من "مساعد Google" مثلاً
  • MediaItem.MediaMetadata: البيانات الوصفية المنظَّمة مثل "العنوان" أو "الفنان"

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

إدارة التنسيق المخصّص والأوامر المخصّصة

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

تحديد تنسيق مخصص للجلسة

لإعلام تطبيقات العميل بعناصر التحكّم في التشغيل التي تريد عرضها للمستخدم، يمكنك ضبط التنسيق المخصّص للجلسة عند إنشاء MediaSession في طريقة onCreate() الخاصة بخدمتك.

Kotlin

override fun onCreate() {
  super.onCreate()

  val likeButton = CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build()
  val favoriteButton = CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(SessionCommand(SAVE_TO_FAVORITES, Bundle()))
    .build()

  session =
    MediaSession.Builder(this, player)
      .setCallback(CustomMediaSessionCallback())
      .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
      .build()
}

Java

@Override
public void onCreate() {
  super.onCreate();

  CommandButton likeButton = new CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build();
  CommandButton favoriteButton = new CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
    .build();

  Player player = new ExoPlayer.Builder(this).build();
  mediaSession =
      new MediaSession.Builder(this, player)
          .setCallback(new CustomMediaSessionCallback())
          .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
          .build();
}

تعريف المشغّل والطلبات المخصّصة المتاحة

يمكن لتطبيقات الوسائط تحديد أوامر مخصصة يمكن استخدامها مثلاً في تنسيق مخصص على سبيل المثال، قد ترغب في تنفيذ أزرار تتيح للمستخدم حفظ عنصر وسائط في قائمة العناصر المفضلة. يرسل MediaController أوامر مخصّصة ويستلمها MediaSession.Callback.

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

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  override fun onConnect(
    session: MediaSession,
    controller: MediaSession.ControllerInfo
  ): MediaSession.ConnectionResult {
    val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build()
  }
}

Java

class CustomMediaSessionCallback implements MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  @Override
  public ConnectionResult onConnect(
    MediaSession session,
    ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
}

لتلقّي طلبات الأوامر المخصّصة من MediaController، عليك إلغاء طريقة onCustomCommand() في Callback.

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: MediaSession.ControllerInfo,
    customCommand: SessionCommand,
    args: Bundle
  ): ListenableFuture<SessionResult> {
    if (customCommand.customAction == SAVE_TO_FAVORITES) {
      // Do custom logic here
      saveToFavorites(session.player.currentMediaItem)
      return Futures.immediateFuture(
        SessionResult(SessionResult.RESULT_SUCCESS)
      )
    }
    ...
  }
}

Java

class CustomMediaSessionCallback implements MediaSession.Callback {
  ...
  @Override
  public ListenableFuture<SessionResult> onCustomCommand(
    MediaSession session, 
    ControllerInfo controller,
    SessionCommand customCommand,
    Bundle args
  ) {
    if(customCommand.customAction.equals(SAVE_TO_FAVORITES)) {
      // Do custom logic here
      saveToFavorites(session.getPlayer().getCurrentMediaItem());
      return Futures.immediateFuture(
        new SessionResult(SessionResult.RESULT_SUCCESS)
      );
    }
    ...
  }
}

يمكنك تتبّع وحدة التحكّم في الوسائط التي ترسل طلبًا باستخدام السمة packageName للكائن MediaSession.ControllerInfo الذي يتم تمريره إلى طرق Callback. ويتيح لك ذلك ضبط سلوك تطبيقك للاستجابة إلى أمر معيّن إذا كان ينشأ من النظام أو تطبيقك أو تطبيقات عميل أخرى.

تعديل التنسيق المخصّص بعد تفاعل المستخدم

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

Kotlin

val removeFromFavoritesButton = CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(SessionCommand(REMOVE_FROM_FAVORITES, Bundle()))
  .build()
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton))

Java

CommandButton removeFromFavoritesButton = new CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(new SessionCommand(REMOVE_FROM_FAVORITES, new Bundle()))
  .build();
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton));

تخصيص سلوك أوامر التشغيل

لتخصيص سلوك الطلب المحدَّد في واجهة Player، مثل play() أو seekToNext()، عليك التفاف Player في ForwardingPlayer.

Kotlin

val player = ExoPlayer.Builder(context).build()

val forwardingPlayer = object : ForwardingPlayer(player) {
  override fun play() {
    // Add custom logic
    super.play()
  }

  override fun setPlayWhenReady(playWhenReady: Boolean) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady)
  }
}

val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()

Java

ExoPlayer player = new ExoPlayer.Builder(context).build();

ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player) {
  @Override
  public void play() {
    // Add custom logic
    super.play();
  }

  @Override
  public void setPlayWhenReady(boolean playWhenReady) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady);
  }
};

MediaSession mediaSession = 
  new MediaSession.Builder(context, forwardingPlayer).build();

لمزيد من المعلومات عن ForwardingPlayer، يُرجى الاطّلاع على دليل ExoPlayer حول التخصيص.

تحديد وحدة التحكم التي تطلب الأمر من اللاعب

عند إنشاء طلب بطريقة Player من خلال MediaController، يمكنك تحديد مصدر المنشأ من خلال MediaSession.controllerForCurrentRequest والحصول على ControllerInfo للطلب الحالي:

Kotlin

class CallerAwareForwardingPlayer(player: Player) :
  ForwardingPlayer(player) {

  override fun seekToNext() {
    Log.d(
      "caller",
      "seekToNext called from package ${session.controllerForCurrentRequest?.packageName}"
    )
    super.seekToNext()
  }
}

Java

public class CallerAwareForwardingPlayer extends ForwardingPlayer {
  public CallerAwareForwardingPlayer(Player player) {
    super(player);
  }

  @Override
  public void seekToNext() {
    Log.d(
        "caller",
        "seekToNext called from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    super.seekToNext();
  }
}

الاستجابة لأزرار الوسائط

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

ويمكن للتطبيق إلغاء السلوك التلقائي من خلال تجاوز MediaSession.Callback.onMediaButtonEvent(Intent). في هذه الحالة، يمكن أو يحتاج التطبيق إلى معالجة جميع تفاصيل واجهة برمجة التطبيقات من تلقاء نفسه.