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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

إدارة الإعدادات المفضّلة لأزرار الوسائط

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

تحديد أزرار الأوامر

يتم استخدام مثيلات CommandButton لتحديد الإعدادات المفضّلة لأزرار الوسائط. يحدّد كل زر ثلاثة جوانب لعنصر في واجهة المستخدم المطلوب:

  1. الرمز، الذي يحدّد المظهر المرئي يجب ضبط الرمز على أحد الثوابت المحدّدة مسبقًا عند إنشاء CommandButton.Builder. يُرجى العِلم أنّ هذا ليس موردًا فعليًا لصورة أو Bitmap. يساعد الثابت العام وحدات التحكّم في اختيار مورد مناسب للحصول على مظهر وأسلوب متّسقَين ضمن واجهة المستخدم الخاصة بها. إذا لم يكن أي من ثوابت الرموز المحدّدة مسبقًا مناسبًا لحالة الاستخدام، يمكنك استخدام setCustomIconResId بدلاً من ذلك.
  2. الأمر: يحدّد الإجراء الذي يتم تنفيذه عندما يتفاعل المستخدم مع الزر. يمكنك استخدام setPlayerCommand لـ Player.Command، أو setSessionCommand لـ SessionCommand محدّد مسبقًا أو مخصّص.
  3. ‫Slot، لتحديد مكان الزر في واجهة مستخدم وحدة التحكّم هذا الحقل اختياري ويتم ضبطه تلقائيًا استنادًا إلى الرمز والطلب. على سبيل المثال، تتيح هذه السمة تحديد أنّه يجب عرض زر في منطقة التنقّل "للأمام" في واجهة المستخدم بدلاً من منطقة "الفيض" التلقائية.

Kotlin

val button =
  CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
    .setPlayerCommand(Player.COMMAND_SEEK_FORWARD)
    .setSlots(CommandButton.SLOT_FORWARD)
    .build()

Java

CommandButton button =
    new CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
        .setPlayerCommand(Player.COMMAND_SEEK_FORWARD)
        .setSlots(CommandButton.SLOT_FORWARD)
        .build();

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

  1. لكل CommandButton في إعدادات أزرار الوسائط المفضّلة، ضَع الزر في أول موضع متاح ومسموح به.
  2. إذا لم يتم ملء أي من الخانات المركزية والأمامية والخلفية بزر، أضِف أزرارًا تلقائية لهذه الخانة.

يمكنك استخدام CommandButton.DisplayConstraints لإنشاء معاينة لكيفية حلّ الإعدادات المفضّلة لأزرار الوسائط استنادًا إلى قيود عرض واجهة المستخدم.

ضبط الإعدادات المفضّلة لأزرار الوسائط

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

Kotlin

val mediaSession =
  MediaSession.Builder(context, player)
    .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
    .build()

Java

MediaSession mediaSession =
    new MediaSession.Builder(context, player)
        .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
        .build();

تعديل الإعدادات المفضّلة لأزرار الوسائط بعد تفاعل المستخدم

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

Kotlin

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(ImmutableList.of(likeButton, removeFromFavoritesButton))

Java

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(ImmutableList.of(likeButton, removeFromFavoritesButton));

إضافة أوامر مخصّصة وتخصيص الإعدادات التلقائية

يمكن توسيع نطاق أوامر المشغّل المتاحة من خلال أوامر مخصّصة، ويمكن أيضًا اعتراض أوامر المشغّل الواردة وأزرار الوسائط لتغيير الإعدادات التلقائية.

الإعلان عن الطلبات المخصّصة ومعالجتها

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

لتحديد الطلبات المخصّصة، عليك إلغاء MediaSession.Callback.onConnectAsync() لضبط الطلبات المخصّصة المتاحة لكل وحدة تحكّم متصلة.

Kotlin

private class CustomMediaSessionCallback : MediaSession.Callback {

  // Configure commands available to the controller in onConnect()
  override fun onConnectAsync(
    session: MediaSession,
    controller: ControllerInfo,
  ): ListenableFuture<ConnectionResult> {
    val sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return Futures.immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setAvailableSessionCommands(sessionCommands)
        .build()
    )
  }
}

Java

private static class CustomMediaSessionCallback implements MediaSession.Callback {

  // Configure commands available to the controller in onConnect()
  @Override
  public ListenableFuture<ConnectionResult> onConnectAsync(
      MediaSession session, ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS
            .buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return Futures.immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setAvailableSessionCommands(sessionCommands)
            .build());
  }
}

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

Kotlin

private class CustomCallback : MediaSession.Callback {
  // ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: 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))
    }
    // ...
    return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
  }
}

Java

private static class CustomCallback 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));
    }
    // ...
    return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
  }
}

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

تخصيص أوامر المشغّل التلقائية

يتم تفويض جميع الأوامر التلقائية ومعالجة الحالة إلى Player على MediaSession. لتخصيص سلوك أمر محدّد في واجهة Player، مثل play() أو seekToNext()، يمكنك تضمين Player في ForwardingSimpleBasePlayer قبل تمريره إلى MediaSession:

Kotlin

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

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

Java

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

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

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

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

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

Kotlin

private class CallerAwarePlayer(player: Player) : ForwardingSimpleBasePlayer(player) {
  private lateinit var session: MediaSession

  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

private static final class CallerAwarePlayer extends ForwardingSimpleBasePlayer {
  private MediaSession session;

  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 المناسبة في مشغّل الجلسة.

ننصحك بمعالجة جميع أحداث أزرار الوسائط الواردة في طريقة Player المقابلة. في حالات الاستخدام المتقدّمة، يمكن اعتراض أحداث زر الوسائط في MediaSession.Callback.onMediaButtonEvent().

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

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

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

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

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

إعداد خطأ مخصّص في المشغّل

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

عندما يضبط تطبيق ما PlaybackException باستخدام واجهة برمجة التطبيقات هذه:

  • سيتم إرسال إشعار إلى مثيلات MediaController المرتبطة. سيتم استدعاء عمليات الاستدعاء Listener.onPlayerError(PlaybackException) و Listener.onPlayerErrorChanged(@Nullable PlaybackException) على وحدة التحكّم مع الاستثناء المقدَّم.

  • ستعرض الطريقة MediaController.getPlayerError() القيمة PlaybackException التي حدّدها التطبيق.

  • ستتغيّر حالة التشغيل لوحدات التحكّم المتأثرة إلى Player.STATE_IDLE.

  • ستتم إزالة الطلبات المتاحة، ولن يتبقّى سوى طلبات القراءة، مثل COMMAND_GET_TIMELINE، في حال سبق منحها. على سبيل المثال، يتم تجميد حالة Timeline إلى الحالة التي تم فيها تطبيق الاستثناء على وحدة التحكّم. تتم إزالة الأوامر التي تحاول تغيير حالة المشغّل، مثل COMMAND_PLAY، إلى أن يزيل التطبيق استثناء التشغيل لوحدة التحكّم المحدّدة.

لمحو PlaybackException مخصّص تم ضبطه سابقًا واستعادة التقارير العادية لحالة المشغّل، يمكن للتطبيق استدعاء MediaSession.setPlaybackException(/* playbackException= */ null) أو MediaSession.setPlaybackException(ControllerInfo, /* playbackException= */ null).

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

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

Kotlin

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

Java

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

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

Kotlin

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

  override fun getState(): State {
    var state = super.getState()
    if (state.playerError != null) {
      state =
        state.buildUpon().setPlayerError(customizePlaybackException(state.playerError!!)).build()
    }
    return state
  }

  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)
      }
      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)
  }
}

Java

private static class ErrorForwardingPlayer extends ForwardingSimpleBasePlayer {

  private final Context context;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
  }

  @Override
  protected State getState() {
    State state = super.getState();
    if (state.playerError != null) {
      state =
          state.buildUpon().setPlayerError(customizePlaybackException(state.playerError)).build();
    }
    return state;
  }

  private PlaybackException customizePlaybackException(PlaybackException error) {
    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;
      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);
  }
}

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

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

Kotlin

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

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

// Option 2: Sending a nonfatal error to the media notification controller only
// 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));

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

// Option 2: Sending a nonfatal error to the media notification controller only
// 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);
}

عند إرسال خطأ غير فادح إلى وحدة التحكّم في إشعارات الوسائط، يتم نسخ رمز الخطأ ورسالة الخطأ إلى جلسة الوسائط على المنصة، بينما لا يتم تغيير PlaybackState.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.
              }
            });