אירועי שחקן

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

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

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

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

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

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

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

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

אפשרות נוספת היא להפעיל Kotlin coroutine באמצעות Player.listenTo ולהגדיר את Player.Event הרלוונטי:

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

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

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

שימוש ב-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();