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

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

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

حالات اختيار جلسة وسائط

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

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

وهذا مفيد في العديد من حالات الاستخدام. وعلى وجه الخصوص، ننصحك بشدة باستخدام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 المتوفر لتحديد ما إذا كنت تريد قبول الطلب أو رفضه. يمكنك الاطّلاع على مثال لقبول طلب ربط في قسم Declare available commands (تعريف الأوامر المتاحة).

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

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

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

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

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

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

معالجة الأخطاء والإبلاغ عنها

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

أخطاء فادحة في التشغيل

يبلّغ المشغّل الجلسة بخطأ فادح في التشغيل، ثم يبلّغ عنه وحدات التحكم لإجراء طلب من خلال Player.Listener.onPlayerError(PlaybackException) وPlayer.Listener.onPlayerErrorChanged(@Nullable PlaybackException).

في هذه الحالة، يتم نقل حالة التشغيل إلى STATE_IDLE وMediaController.getPlaybackError() يعرض PlaybackException الذي تسبب في النقل. ويمكن لوحدة التحكّم هذه فحص PlayerException.errorCode للحصول على معلومات حول سبب الخطأ.

بالنسبة إلى إمكانية التشغيل التفاعلي، يتم تكرار خطأ فادح في PlaybackStateCompat لجلسة النظام الأساسي من خلال نقل حالته إلى STATE_ERROR وضبط رمز الخطأ والرسالة وفقًا لـ PlaybackException.

تخصيص خطأ خطير

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

Kotlin

val forwardingPlayer = ErrorForwardingPlayer(player)
val session = MediaSession.Builder(context, forwardingPlayer).build()

Java

Player forwardingPlayer = new ErrorForwardingPlayer(player);
MediaSession session =
    new MediaSession.Builder(context, forwardingPlayer).build();

يسجِّل مشغّل التقديم/الترجيع Player.Listener للمشغّل الفعلي ويعترض عمليات الاستدعاء التي تُبلغ عن خطأ. بعد ذلك، يتم تفويض PlaybackException مخصّص للمستمعين الذين تم تسجيلهم على مشغّل الإعادة. لكي يعمل هذا الإجراء، يجب أن تلغي وحدة إعادة التوجيه العنصرَين Player.addListener وPlayer.removeListener للوصول إلى المستمعين الذين يمكن إرسال رمز خطأ أو رسالة أو عناصر إضافية مخصّصة إليهم:

Kotlin

class ErrorForwardingPlayer(private val context: Context, player: Player) :
  ForwardingPlayer(player) {

  private val listeners: MutableList<Player.Listener> = mutableListOf()

  private var customizedPlaybackException: PlaybackException? = null

  init {
    player.addListener(ErrorCustomizationListener())
  }

  override fun addListener(listener: Player.Listener) {
    listeners.add(listener)
  }

  override fun removeListener(listener: Player.Listener) {
    listeners.remove(listener)
  }

  override fun getPlayerError(): PlaybackException? {
    return customizedPlaybackException
  }

  private inner class ErrorCustomizationListener : Player.Listener {

    override fun onPlayerErrorChanged(error: PlaybackException?) {
      customizedPlaybackException = error?.let { customizePlaybackException(it) }
      listeners.forEach { it.onPlayerErrorChanged(customizedPlaybackException) }
    }

    override fun onPlayerError(error: PlaybackException) {
      listeners.forEach { it.onPlayerError(customizedPlaybackException!!) }
    }

    private fun customizePlaybackException(
      error: PlaybackException,
    ): PlaybackException {
      val buttonLabel: String
      val errorMessage: String
      when (error.errorCode) {
        PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> {
          buttonLabel = context.getString(R.string.err_button_label_restart_stream)
          errorMessage = context.getString(R.string.err_msg_behind_live_window)
        }
        // Apps can customize further error messages by adding more branches.
        else -> {
          buttonLabel = context.getString(R.string.err_button_label_ok)
          errorMessage = context.getString(R.string.err_message_default)
        }
      }
      val extras = Bundle()
      extras.putString("button_label", buttonLabel)
      return PlaybackException(errorMessage, error.cause, error.errorCode, extras)
    }

    override fun onEvents(player: Player, events: Player.Events) {
      listeners.forEach {
        it.onEvents(player, events)
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

Java

private static class ErrorForwardingPlayer extends ForwardingPlayer {

  private final Context context;
  private List<Player.Listener> listeners;
  @Nullable private PlaybackException customizedPlaybackException;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
    listeners = new ArrayList<>();
    player.addListener(new ErrorCustomizationListener());
  }

  @Override
  public void addListener(Player.Listener listener) {
    listeners.add(listener);
  }

  @Override
  public void removeListener(Player.Listener listener) {
    listeners.remove(listener);
  }

  @Nullable
  @Override
  public PlaybackException getPlayerError() {
    return customizedPlaybackException;
  }

  private class ErrorCustomizationListener implements Listener {

    @Override
    public void onPlayerErrorChanged(@Nullable PlaybackException error) {
      customizedPlaybackException =
          error != null ? customizePlaybackException(error, context) : null;
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerErrorChanged(customizedPlaybackException);
      }
    }

    @Override
    public void onPlayerError(PlaybackException error) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerError(checkNotNull(customizedPlaybackException));
      }
    }

    private PlaybackException customizePlaybackException(
        PlaybackException error, Context context) {
      String buttonLabel;
      String errorMessage;
      switch (error.errorCode) {
        case PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW:
          buttonLabel = context.getString(R.string.err_button_label_restart_stream);
          errorMessage = context.getString(R.string.err_msg_behind_live_window);
          break;
        // Apps can customize further error messages by adding more case statements.
        default:
          buttonLabel = context.getString(R.string.err_button_label_ok);
          errorMessage = context.getString(R.string.err_message_default);
          break;
      }
      Bundle extras = new Bundle();
      extras.putString("button_label", buttonLabel);
      return new PlaybackException(errorMessage, error.getCause(), error.errorCode, extras);
    }

    @Override
    public void onEvents(Player player, Events events) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onEvents(player, events);
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

الأخطاء غير المميتة

بالنسبة إلى الأخطاء غير الفادحة التي لا تنشأ عن استثناء فني، يمكن أن يرسلها التطبيق إلى الكل أو إلى مسؤول تحكّم معيَّن:

Kotlin

val sessionError = SessionError(
  SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
  context.getString(R.string.error_message_authentication_expired),
)

// Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError)

// Interoperability: Sending a nonfatal error to the media notification controller to set the
// error code and error message in the playback state of the platform media session.
mediaSession.mediaNotificationControllerInfo?.let {
  mediaSession.sendError(it, sessionError)
}

Java

SessionError sessionError = new SessionError(
    SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
    context.getString(R.string.error_message_authentication_expired));

// Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError);

// Interoperability: Sending a nonfatal error to the media notification controller to set the
// error code and error message in the playback state of the platform media session.
ControllerInfo mediaNotificationControllerInfo =
    mediaSession.getMediaNotificationControllerInfo();
if (mediaNotificationControllerInfo != null) {
  mediaSession.sendError(mediaNotificationControllerInfo, sessionError);
}

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

تلقّي أخطاء غير قاتلة

يتلقّى MediaController خطأً غير مميت من خلال تنفيذ MediaController.Listener.onError:

Kotlin

val future = MediaController.Builder(context, sessionToken)
  .setListener(object : MediaController.Listener {
    override fun onError(controller: MediaController, sessionError: SessionError) {
      // Handle nonfatal error.
    }
  })
  .buildAsync()

Java

MediaController.Builder future =
    new MediaController.Builder(context, sessionToken)
        .setListener(
            new MediaController.Listener() {
              @Override
              public void onError(MediaController controller, SessionError sessionError) {
                // Handle nonfatal error.
              }
            });