אירועי שחקן

שינויים במצב של הנגן (כמו התחלת הפעלה, טעינה מראש או שגיאות) מפעילים אירועים שנשלחים למופעים רשומים של Player.Listener. האירועים האלה מיוצגים על ידי קבועים של מספרים שלמים ומוגדרים על ידי Player.Event ו-Player.Events.

רישום של Player.Listener

הדיווח על פעולות במשחק נשלח למופעים רשומים של Player.Listener. כדי לרשום פונקציית 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 כדי להאזין לאירועים באמצעות coroutines. במקרה כזה, לא תצטרכו לרשום או לבטל את הרישום של Player.Listener באופן מפורש.

האזנה לאירועי הפעלה באמצעות Player.Listener

ל-Player.Listener יש שיטות ברירת מחדל ריקות, כך שצריך להטמיע רק את השיטות שמעניינות אתכם. ב-Javadoc מופיע תיאור מלא של השיטות ומתי הן נקראות. בהמשך נתאר בפירוט כמה מהשיטות החשובות ביותר.

המאזינים יכולים לבחור בין הטמעה של קריאות חוזרות (callbacks) של אירועים ספציפיים לבין קריאה חוזרת גנרית של 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() יכולים להשתנות בגלל הרבה סיבות: מעברים בין פלייליסטים, עדכונים של מטא-נתונים ב-In-stream או עדכון של MediaItem הנוכחי באמצע ההפעלה.

אם אתם רוצים לבצע שינויים במטא-נתונים, למשל לעדכן ממשק משתמש שמציג את השם הנוכחי, אתם יכולים להאזין ל-onMediaMetadataChanged.

מחפש

קריאה לשיטות של Player.seekTo גורמת לסדרה של קריאות חוזרות (callbacks) למופעים רשומים של Player.Listener:

  1. ‫onPositionDiscontinuity עם reason=DISCONTINUITY_REASON_SEEK. זה קורה בגלל הקריאה ל-Player.seekTo. הקריאה החוזרת (callback) כוללת את השדות PositionInfo למיקום לפני ואחרי החיפוש.
  2. onPlaybackStateChanged עם כל שינוי מיידי במצב שקשור לדילוג. חשוב לזכור שלא בטוח שיהיה שינוי כזה.

קריאות חוזרות (callbacks) נפרדות לעומת onEvents

המאזינים יכולים לבחור בין הטמעה של קריאות חוזרות (callbacks) נפרדות כמו 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.
  • המאזין פועל רק על הערכים החדשים שסופקו באמצעות פרמטרים של קריאה חוזרת (callback) או מפעיל משהו אחר שלא תלוי בפרמטרים של קריאה חוזרת (callback).
  • בהטמעה של המאזין, עדיף להשתמש בשם שיטה שמציין בצורה ברורה וקריאה מה הפעיל את האירוע.
  • המאזין מדווח למערכת ניתוח נתונים שצריכה לדעת על כל האירועים ושינויי המצב.

עדיף להשתמש ב-onEvents(Player player, Events events) הגנרי במקרים הבאים:

  • המאזין רוצה להפעיל את אותו לוגיקה עבור מספר אירועים. לדוגמה, עדכון של ממשק משתמש גם ל-onPlaybackStateChanged וגם ל-onPlayWhenReadyChanged.
  • המאזין צריך גישה לאובייקט Player כדי להפעיל אירועים נוספים, למשל חיפוש אחרי מעבר של קובץ מדיה.
  • המאזין מתכוון להשתמש בכמה ערכי מצב שמדווחים באמצעות קריאות חוזרות (callbacks) נפרדות ביחד, או בשילוב עם שיטות getter של Player. לדוגמה, השימוש ב-Player.getCurrentWindowIndex() עם Timeline שסופק ב-onTimelineChanged בטוח רק מתוך הקריאה החוזרת (callback) של onEvents.
  • המאזין מתעניין אם האירועים התרחשו יחד באופן הגיוני. לדוגמה, מ-onPlaybackStateChanged ל-STATE_BUFFERING בגלל מעבר בין קובצי מדיה.

במקרים מסוימים, יכול להיות שהמאזינים יצטרכו לשלב את פונקציות ה-callback הנפרדות עם פונקציית ה-callback הגנרית onEvents, למשל כדי לתעד את הסיבות לשינוי של קובץ המדיה באמצעות onMediaItemTransition, אבל לפעול רק אחרי שאפשר יהיה להשתמש בכל השינויים במצב ביחד ב-onEvents.

האזנה לאירועי הפעלה באמצעות פונקציות coroutine

לחלופין, אתם יכולים להפעיל coroutine של Kotlin באמצעות Player.listenTo ולציין את ה-Player.Event הרלוונטי:

שימו לב שאפשר לקרוא ל-Player.listen ול-Player.listenTo מכל שרשור, אבל פונקציית הלמבדה של הקריאה החוזרת תמיד מופעלת בשרשור שמשויך ל-Player.getApplicationLooper. לכן, אפשר לגשת בבטחה ל-methods ולמאפייני המצב של Player בתוך פונקציית הלמבדה של הקריאה החוזרת (callback), גם אם ה-coroutine הופעל בשרשור אחר.

שינויים במצב ההפעלה

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

קריאות חוזרות (callbacks) נפרדות לעומת onEvents

כשמאזינים לאירועים של הנגן בתוך coroutine, תמיד צריך לספק את ההטמעה של הקריאה החוזרת (callback) של onEvents, בניגוד לקריאה חוזרת (callback) נפרדת. אתם יכולים לבחור בין Player.listen לבין Player.listenTo, בהתאם לאירועים שצריכים להפעיל את פונקציית הלמבדה. אבל הפונקציות זהות בכל שאר המובנים:

האזנה

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. פונקציית הלמבדה תופעל בכל פעם שאחד מהאירועים האלה יתרחש, ותוכלו לבדוק את הפרמטר 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, הן לא מספקות גישה לארגומנטים הזמניים שמועברים לקריאות חוזרות (callbacks) ספציפיות, כמו הסיבה ב-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();