التحكّم في تشغيل الإعلان والإعلان عنه باستخدام 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 نُسخًا تتضمّن معرّف موارد موحّد لتشغيلها. يتم تلقائيًا إعادة توجيه العناصر المُضافة حديثًا إلى طرق المشغّل، مثل 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 في ForwardingSimpleBasePlayer قبل تمريره إلى MediaSession.

Kotlin

val player = (logic to build a Player instance)

val forwardingPlayer = object : ForwardingSimpleBasePlayer(player) {
  // Customizations
}

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

Java

ExoPlayer player = (logic to build a Player instance)

ForwardingSimpleBasePlayer forwardingPlayer =
    new ForwardingSimpleBasePlayer(player) {
      // Customizations
    };

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

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

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

عندما يبدأ طلب Player من MediaController، يمكنك تحديد مصدره باستخدام MediaSession.controllerForCurrentRequest والحصول على ControllerInfo للطلب الحالي:

Kotlin

class CallerAwarePlayer(player: Player) :
  ForwardingSimpleBasePlayer(player) {

  override fun handleSeek(
    mediaItemIndex: Int,
    positionMs: Long,
    seekCommand: Int,
  ): ListenableFuture<*> {
    Log.d(
      "caller",
      "seek operation from package ${session.controllerForCurrentRequest?.packageName}",
    )
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand)
  }
}

Java

public class CallerAwarePlayer extends ForwardingSimpleBasePlayer {
  public CallerAwarePlayer(Player player) {
    super(player);
  }

  @Override
  protected ListenableFuture<?> handleSeek(
        int mediaItemIndex, long positionMs, int seekCommand) {
    Log.d(
        "caller",
        "seek operation from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand);
  }
}

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

أزرار الوسائط هي أزرار أجهزة خارجية موجودة على أجهزة 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.
              }
            });