أحداث اللاعبين

تؤدي التغييرات في حالة المشغّل (مثل بدء التشغيل أو التخزين المؤقت أو الأخطاء) إلى تشغيل الأحداث التي يتم إرسالها إلى مثيلات Player.Listener المسجّلة. يتم تمثيل هذه الأحداث بثوابت عددية ويتم تحديدها من خلال Player.Event وPlayer.Events.

تسجيل Player.Listener

يتم إرسال تقارير أحداث اللاعبين إلى مثيلات Player.Listener المسجَّلة. لتسجيل أداة معالجة لتلقّي هذه الأحداث، اتّبِع الخطوات التالية:

Kotlin

// Add a listener to receive events from the player.
player.addListener(listener)

Java

// Add a listener to receive events from the player.
player.addListener(listener);

إذا كنت تستخدم Kotlin، يمكنك أيضًا استخدام دوال الإضافات المعلقة التي توفّرها وحدة media3-common-ktx للاستماع إلى الأحداث باستخدام الكوروتين. في هذه الحالة، لن تحتاج إلى تسجيل Player.Listener أو إلغاء تسجيله بشكل صريح.

الاستماع إلى أحداث التشغيل باستخدام Player.Listener

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

يمكن للمستمعين الاختيار بين تنفيذ عمليات استدعاء فردية للأحداث أو استدعاء عام لـ onEvents يتم استدعاؤه بعد وقوع حدث واحد أو أكثر معًا. يُرجى الاطّلاع على Individual callbacks vs onEvents للحصول على شرح حول الخيار الذي يجب تفضيله في حالات الاستخدام المختلفة.

تغييرات حالة التشغيل

يمكن تلقّي التغييرات في حالة المشغّل من خلال تنفيذ onPlaybackStateChanged(@State int state) في Player.Listener مسجَّل. يمكن أن يكون المشغّل في إحدى حالات التشغيل الأربع التالية:

  • ‫Player.STATE_IDLE: هذه هي الحالة الأولية، أي الحالة التي يكون فيها المشغّل متوقفًا، وعند تعذُّر التشغيل. سيحتفظ اللاعب بموارد محدودة فقط في هذه الحالة.
  • ‫Player.STATE_BUFFERING: لا يمكن للمشغّل تشغيل الفيديو على الفور من موضعه الحالي. يحدث هذا في الغالب بسبب الحاجة إلى تحميل المزيد من البيانات.
  • ‫Player.STATE_READY: يمكن للمشغّل تشغيل الفيديو فورًا من موضعه الحالي.
  • Player.STATE_ENDED: انتهى المشغّل من تشغيل جميع الوسائط.

بالإضافة إلى هذه الحالات، يتضمّن المشغّل علامة playWhenReady للإشارة إلى نية المستخدم في تشغيل المحتوى. يمكن تلقّي التغييرات في هذه العلامة من خلال تنفيذ onPlayWhenReadyChanged(playWhenReady, @PlayWhenReadyChangeReason int reason).

يكون المشغّل قيد التشغيل (أي أنّ موضعه يتقدّم ويتم عرض الوسائط للمستخدم) عند استيفاء الشروط الثلاثة التالية:

  • المشغّل في حالة Player.STATE_READY
  • playWhenReady هو true
  • لم يتم إيقاف التشغيل لسبب تم عرضه من قِبل Player.getPlaybackSuppressionReason

بدلاً من التحقّق من هذه السمات بشكل فردي، يمكن استدعاء Player.isPlaying. يمكن تلقّي التغييرات في هذه الحالة من خلال تنفيذ onIsPlayingChanged(boolean isPlaying):

Kotlin

player.addListener(
  object : Player.Listener {
    override fun onIsPlayingChanged(isPlaying: Boolean) {
      if (isPlaying) {
        // Active playback.
      } else {
        // Not playing because playback is paused, ended, suppressed, or the player
        // is buffering, stopped or failed. Check player.playWhenReady,
        // player.playbackState, player.playbackSuppressionReason and
        // player.playerError for details.
      }
    }
  }
)

Java

player.addListener(
    new Player.Listener() {
      @Override
      public void onIsPlayingChanged(boolean isPlaying) {
        if (isPlaying) {
          // Active playback.
        } else {
          // Not playing because playback is paused, ended, suppressed, or the player
          // is buffering, stopped or failed. Check player.getPlayWhenReady,
          // player.getPlaybackState, player.getPlaybackSuppressionReason and
          // player.getPlaybackError for details.
        }
      }
    });

أخطاء التشغيل

يمكن تلقّي الأخطاء التي تؤدي إلى تعذُّر التشغيل من خلال تنفيذ onPlayerError(PlaybackException error) في Player.Listener مسجَّل. عند حدوث عطل، سيتم استدعاء هذه الطريقة مباشرةً قبل انتقال حالة التشغيل إلى Player.STATE_IDLE. يمكن إعادة محاولة عمليات التشغيل التي تعذّر إجراؤها أو التي تم إيقافها من خلال استدعاء ExoPlayer.prepare.

يُرجى العِلم أنّ بعض عمليات تنفيذ Player تمرّر نُسخًا من الفئات الفرعية من PlaybackException لتقديم معلومات إضافية عن الخطأ. على سبيل المثال، يمرر ExoPlayer ExoPlaybackException، الذي يحتوي على type وrendererIndex وحقول أخرى خاصة بـ ExoPlayer.

يوضّح المثال التالي كيفية رصد تعذُّر التشغيل بسبب مشكلة في شبكة HTTP:

Kotlin

player.addListener(
  object : Player.Listener {
    override fun onPlayerError(error: PlaybackException) {
      val cause = error.cause
      if (cause is HttpDataSourceException) {
        // An HTTP error occurred.
        val httpError = cause
        // It's possible to find out more about the error both by casting and by querying
        // the cause.
        if (httpError is InvalidResponseCodeException) {
          // Cast to InvalidResponseCodeException and retrieve the response code, message
          // and headers.
        } else {
          // Try calling httpError.getCause() to retrieve the underlying cause, although
          // note that it may be null.
        }
      }
    }
  }
)

Java

player.addListener(
    new Player.Listener() {
      @Override
      public void onPlayerError(PlaybackException error) {
        @Nullable Throwable cause = error.getCause();
        if (cause instanceof HttpDataSourceException) {
          // An HTTP error occurred.
          HttpDataSourceException httpError = (HttpDataSourceException) cause;
          // It's possible to find out more about the error both by casting and by querying
          // the cause.
          if (httpError instanceof HttpDataSource.InvalidResponseCodeException) {
            // Cast to InvalidResponseCodeException and retrieve the response code, message
            // and headers.
          } else {
            // Try calling httpError.getCause() to retrieve the underlying cause, although
            // note that it may be null.
          }
        }
      }
    });

الانتقالات في قوائم التشغيل

عندما ينتقل المشغّل إلى ملف وسائط جديد في قائمة التشغيل، يتم استدعاء onMediaItemTransition(MediaItem mediaItem, @MediaItemTransitionReason int reason) في عناصر Player.Listener المسجَّلة. يشير السبب إلى ما إذا كان هذا الانتقال تلقائيًا أو ناتجًا عن عملية بحث (على سبيل المثال، بعد استدعاء player.next()) أو تكرار للعنصر نفسه أو ناتجًا عن تغيير في قائمة التشغيل (على سبيل المثال، إذا تمت إزالة العنصر الذي يتم تشغيله حاليًا).

البيانات الوصفية

يمكن أن تتغيّر البيانات الوصفية التي يتم عرضها من player.getCurrentMediaMetadata() لأسباب عديدة، مثل الانتقال بين قوائم التشغيل أو تعديل البيانات الوصفية أثناء عرض المحتوى أو تعديل MediaItem الحالي أثناء التشغيل.

إذا كنت مهتمًا بالتغييرات في البيانات الوصفية، على سبيل المثال لتعديل واجهة مستخدم تعرض العنوان الحالي، يمكنك الاستماع إلى onMediaMetadataChanged.

جارٍ تفعيل عناصر الانتقال

يؤدي استدعاء طرق Player.seekTo إلى سلسلة من عمليات الاستدعاء لمثيلات Player.Listener المسجَّلة:

  1. ‫onPositionDiscontinuity مع reason=DISCONTINUITY_REASON_SEEK هذه هي النتيجة المباشرة لاستدعاء Player.seekTo. يحتوي ردّ الاتصال على حقلَين (PositionInfo) للموضع قبل البحث وبعده.
  2. onPlaybackStateChanged مع أي تغيير فوري في الحالة مرتبط بعملية البحث. يُرجى العِلم أنّه قد لا يكون هناك تغيير من هذا النوع.

في حال تفعيل وضع التصفّح السريع باستخدام ExoPlayer.setScrubbingModeEnabled لسحب شريط التقديم/الترجيع التفاعلي، يوقف ExoPlayer التشغيل ويستدعي onPlaybackSuppressionReasonChanged باستخدام Player.PLAYBACK_SUPPRESSION_REASON_SCRUBBING. يؤدي كل استدعاء لـ Player.seekTo إلى تشغيل onPositionDiscontinuity، حتى عندما يتجاهل ExoPlayer عمليات البحث الوسيطة التي تم استبدالها ويبلغ عنها إلى AnalyticsListener.onDroppedSeeksWhileScrubbing.

عمليات معاودة الاتصال الفردية مقارنةً بـ onEvents

يمكن للمستمعين الاختيار بين تنفيذ عمليات استدعاء فردية مثل onIsPlayingChanged(boolean isPlaying)، وعملية الاستدعاء العامة onEvents(Player player, Events events). تتيح دالة الاستدعاء العامة الوصول إلى عنصر Player وتحدّد مجموعة events التي حدثت معًا. يتم دائمًا استدعاء دالة الاستدعاء هذه بعد دوال الاستدعاء التي تتوافق مع الأحداث الفردية.

Kotlin

override fun onEvents(player: Player, events: Player.Events) {
  if (
    events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) ||
      events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)
  ) {
    uiModule.updateUi(player)
  }
}

Java

@Override
public void onEvents(Player player, Events events) {
  if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)
      || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
    uiModule.updateUi(player);
  }
}

يجب تفضيل الأحداث الفردية في الحالات التالية:

  • يهتم المستمع بأسباب التغييرات. على سبيل المثال، الأسباب المقدَّمة لـ onPlayWhenReadyChanged أو onMediaItemTransition
  • لا يعمل المستمع إلا على القيم الجديدة التي يتم توفيرها من خلال معلمات رد الاتصال أو يؤدي إلى تشغيل شيء آخر لا يعتمد على معلمات رد الاتصال.
  • يفضّل تنفيذ المستمع إشارة واضحة قابلة للقراءة لما أدى إلى تشغيل الحدث في اسم الطريقة.
  • يُرسل المستمع تقارير إلى نظام إحصاءات يحتاج إلى معرفة جميع الأحداث الفردية وتغييرات الحالة.

يجب تفضيل onEvents(Player player, Events events) العام في الحالات التالية:

  • يريد المستمع تشغيل نفس المنطق لأحداث متعددة. على سبيل المثال، تعديل واجهة مستخدم لكل من onPlaybackStateChanged وonPlayWhenReadyChanged.
  • يجب أن يتمكّن المستمع من الوصول إلى عنصر Player لتشغيل أحداث أخرى، مثل البحث بعد انتقال ملف وسائط.
  • يهدف المستمع إلى استخدام قيم حالة متعددة يتم الإبلاغ عنها من خلال عمليات استدعاء منفصلة معًا، أو بالاشتراك مع طرق الحصول على Player. على سبيل المثال، استخدام Player.getCurrentWindowIndex() مع Timeline المقدَّم في onTimelineChanged يكون آمنًا فقط من داخل دالة الاستدعاء onEvents.
  • يهتم المستمع بما إذا كانت الأحداث قد وقعت معًا بشكل منطقي. على سبيل المثال، من onPlaybackStateChanged إلى STATE_BUFFERING بسبب انتقال ملف وسائط.

In some cases, listeners may need to combine the individual callbacks with the generic onEvents callback, for example to record media item change reasons with onMediaItemTransition, but only act once all state changes can be used together in onEvents.

الاستماع إلى أحداث التشغيل باستخدام الكوروتين

بدلاً من ذلك، يمكنك تشغيل دالة Kotlin coroutine باستخدام Player.listenTo وتحديد Player.Event ذات الصلة:

يُرجى العِلم أنّه يمكن استدعاء Player.listen وPlayer.listenTo من أي سلسلة محادثات، بينما يتم دائمًا استدعاء دالة ردّ الاتصال lambda في سلسلة المحادثات المرتبطة بـ Player.getApplicationLooper. لذلك، من الآمن الوصول إلى طرق Player وخصائص الحالة داخل دالة lambda لردّ الاتصال حتى إذا تم تشغيل الكوروتين في سلسلة محادثات مختلفة.

تغييرات حالة التشغيل

coroutineScope.launch {
  player.listenTo(Player.EVENT_IS_PLAYING_CHANGED) {
    // `Player` is a receiver scope for this trailing lambda
    if (isPlaying) {
      // Active playback.
    } else {
      // Not playing.
    }
  }
}

أخطاء التشغيل

coroutineScope.launch {
  player.listenTo(Player.EVENT_PLAYER_ERROR) {
    val error = playerError ?: return@listenTo
    val cause = error.cause
    if (cause is HttpDataSourceException) {
      // An HTTP error occurred.
      if (cause is InvalidResponseCodeException) {
        // Retrieve the response code, message and headers
      } else {
        // Try calling cause.cause to retrieve the underlying cause
      }
    }
  }
}

عمليات معاودة الاتصال الفردية مقارنةً بـ onEvents

عند الاستماع إلى أحداث اللاعبين داخل دالة coroutine، عليك دائمًا توفير تنفيذ لعملية الاستدعاء onEvents، بدلاً من عملية استدعاء فردية. يمكنك الاختيار بين Player.listen وPlayer.listenTo، حسب الأحداث التي يجب أن تؤدي إلى استدعاء دالة lambda. ولكن الدوال متكافئة بخلاف ذلك:

سماع طريقة اللفظ

coroutineScope.launch {
  player.listen { events ->
    // `Player` is a receiver scope for this trailing lambda
    if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
      // Access the player state directly from the receiver
      updateUi(playbackState)
    }

    if (events.contains(Player.EVENT_PLAYER_ERROR)) {
      // Access the error directly from the player
      handleError(playerError)
    }
  }
}

listenTo

coroutineScope.launch {
  player.listenTo(Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAYER_ERROR) { events ->
    // `Player` is a receiver scope for this trailing lambda
    if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
      // Access the player state directly from the receiver
      updateUi(playbackState)
    }

    if (events.contains(Player.EVENT_PLAYER_ERROR)) {
      // Access the error directly from the player
      handleError(playerError)
    }
  }
}

إذا كنت مهتمًا بأنواع أحداث متعددة، يمكنك تمرير قائمة بالأحداث إلى Player.listenTo. سيتم استدعاء دالة lambda عند وقوع أي من هذه الأحداث، ويمكنك فحص المَعلمة Events للتحقّق من الأحداث التي تم تشغيلها فعليًا:

coroutineScope.launch {
  player.listenTo(Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAYER_ERROR) { events ->
    // Unclear which event got triggered without querying `events` parameter
    // The following function will fire whenever either one is caught
    updateUiAndHandleError(playbackState, playerError)
  }
}

بما أنّ هذه الدوال تعمل على onEvents، فهي لا تتيح الوصول إلى الوسيطات المؤقتة التي يتم تمريرها إلى عمليات الاستدعاء الفردية، مثل السبب في onMediaItemTransition(..., int reason) أو oldPosition في onPositionDiscontinuity(...). إذا كان منطقك يعتمد على هذه الوسيطات المحدّدة (وهي غير متاحة كـ سمات حالة في Player)، عليك استخدام واجهة Player.Listener العادية بدلاً من ذلك.

استخدام ميزة "AnalyticsListener"

عند استخدام ExoPlayer، يمكن تسجيل AnalyticsListener مع المشغّل من خلال طلب addAnalyticsListener. يمكن لعمليات تنفيذ AnalyticsListener الاستماع إلى الأحداث التفصيلية التي قد تكون مفيدة لأغراض الإحصاءات والتسجيل. لمزيد من التفاصيل، يُرجى الانتقال إلى صفحة الإحصاءات.

استخدام ميزة "EventLogger"

‫EventLogger هو AnalyticsListener توفّره المكتبة مباشرةً لأغراض التسجيل. أضِف EventLogger إلى ExoPlayer لتفعيل التسجيل الإضافي المفيد باستخدام سطر واحد:

Kotlin

player.addAnalyticsListener(EventLogger())

Java

player.addAnalyticsListener(new EventLogger());

لمزيد من التفاصيل، يُرجى الاطّلاع على صفحة تسجيل تصحيح الأخطاء.

إطلاق الأحداث في مواضع تشغيل محدّدة

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

Kotlin

player
  .createMessage { messageType: Int, payload: Any? -> }
  .setLooper(Looper.getMainLooper())
  .setPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 120000)
  .setPayload(customPayloadData)
  .setDeleteAfterDelivery(false)
  .send()

Java

player
    .createMessage((messageType, payload) -> {
      // Do something at the specified playback position.
    })
    .setLooper(Looper.getMainLooper())
    .setPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 120_000)
    .setPayload(customPayloadData)
    .setDeleteAfterDelivery(false)
    .send();