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

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

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

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

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

  • من خلال سماعات الرأس. غالبًا ما تكون هناك أزرار أو تفاعلات لمس يمكن للمستخدم تشغيل الوسائط على سماعات الرأس لتشغيل الوسائط أو إيقافها مؤقتًا أو الانتقال إلى الفيديو التالي أو المقطع الصوتي السابق.
  • من خلال التحدّث إلى مساعد Google هناك نمط شائع وهو قول "حسنًا 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 على session 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.
              }
            });