التحكّم في تشغيل الإعلان والإعلان عنه باستخدام 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: بيانات وصفية منظَّمة، مثل "title" أو "artist"

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