שינויים במצב של נגן (למשל התחלת הפעלה, אחסון בזיכרון או שגיאות) מפעילים אירועים שנשלחים למופעים רשומים של 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:
-
onPositionDiscontinuityעםreason=DISCONTINUITY_REASON_SEEK. זוהי התוצאה הישירה של הקריאה ל-Player.seekTo. פונקציית הקריאה החוזרת כוללתPositionInfoשדות למיקום לפני ואחרי ההזזה. -
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();