Analytics

‫ExoPlayer תומך במגוון רחב של צורכי ניתוח נתוני הפעלה. בסופו של דבר, ניתוח הנתונים הוא תהליך של איסוף, פרשנות, צבירה וסיכום של נתונים מהפעלות. אפשר להשתמש בנתונים האלה במכשיר – למשל, לרישום ביומן, לניפוי באגים או כדי לקבל החלטות לגבי הפעלה עתידית – או לדווח עליהם לשרת כדי לעקוב אחר הפעלות בכל המכשירים.

בדרך כלל, מערכת ניתוח נתונים צריכה קודם לאסוף אירועים, ואז לעבד אותם כדי להפיק מהם תובנות:

  • איסוף אירועים: אפשר לעשות זאת על ידי רישום AnalyticsListener במופע ExoPlayer. פונקציות analytics listener רשומות מקבלות אירועים כשהם מתרחשים במהלך השימוש בנגן. כל אירוע משויך לפריט המדיה המתאים בפלייליסט, וגם למטא-נתונים של מיקום ההפעלה וחותמת הזמן.
  • עיבוד אירועים: מערכות ניתוח נתונים מסוימות מעלות אירועים גולמיים לשרת, וכל עיבוד האירועים מתבצע בצד השרת. אפשר גם לעבד אירועים במכשיר, וזה יכול להיות פשוט יותר או להקטין את כמות המידע שצריך להעלות. ‫ExoPlayer מספק PlaybackStatsListener, שמאפשר לבצע את שלבי העיבוד הבאים:
    1. פרשנות של אירועים: כדי שהאירועים יהיו שימושיים למטרות ניתוח, צריך לפרש אותם בהקשר של הפעלה יחידה. לדוגמה, אירוע הגולמי של שינוי מצב הנגן ל-STATE_BUFFERING עשוי להתאים לאגירה ראשונית בזיכרון, לאגירה חוזרת בזיכרון או לאגירה בזיכרון שמתרחשת אחרי מעבר למיקום אחר בסרטון.
    2. מעקב אחר מצב: בשלב הזה, המערכת ממירה את האירועים למונים. לדוגמה, אפשר להמיר אירועים של שינוי מצב למונים שעוקבים אחרי משך הזמן שחלף בכל מצב הפעלה. התוצאה היא קבוצה בסיסית של ערכי נתוני ניתוח לנגינה יחידה.
    3. צבירה: בשלב הזה, נתוני הניתוח משולבים בין כמה הפעלות חוזרות, בדרך כלל על ידי חיבור של מוניטורים.
    4. חישוב של מדדי סיכום: הרבה מהמדדים הכי שימושיים הם מדדים שמחשבים ממוצעים או משלבים את ערכי הנתונים הבסיסיים של Analytics בדרכים אחרות. אפשר לחשב מדדי סיכום להפעלות בודדות או לכמה הפעלות.

איסוף אירועים באמצעות AnalyticsListener

אירועי הפעלה גולמיים מהנגן מדווחים להטמעות של AnalyticsListener. אתם יכולים להוסיף בקלות מאזין משלכם ולשנות רק את השיטות שמעניינות אתכם:

Kotlin

exoPlayer.addAnalyticsListener(
  object : AnalyticsListener {
    override fun onPlaybackStateChanged(eventTime: EventTime, @Player.State state: Int) {}

    override fun onDroppedVideoFrames(
      eventTime: EventTime,
      droppedFrames: Int,
      elapsedMs: Long,
    ) {}
  }
)

Java

exoPlayer.addAnalyticsListener(
    new AnalyticsListener() {
      @Override
      public void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) {}

      @Override
      public void onDroppedVideoFrames(
          EventTime eventTime, int droppedFrames, long elapsedMs) {}
    });

הפרמטר EventTime שמועבר לכל קריאה חוזרת משייך את האירוע לפריט מדיה בפלייליסט, וגם למטא-נתונים של מיקום ההפעלה וחותמת הזמן:

  • realtimeMs: השעה של האירוע לפי שעון קיר.
  • timeline, windowIndex ו-mediaPeriodId: מגדירים את הפלייליסט ואת הפריט בפלייליסט שאליו שייך האירוע. המאפיין mediaPeriodId מכיל מידע נוסף אופציונלי. לדוגמה, הוא מציין אם האירוע שייך למודעה בתוך הפריט.
  • eventPlaybackPositionMs: מיקום ההפעלה בפריט בזמן שהאירוע התרחש.
  • currentTimeline, currentWindowIndex, currentMediaPeriodId ו-currentPlaybackPositionMs: כמו למעלה, אבל לפריט שמופעל כרגע. הפריט שמופעל כרגע יכול להיות שונה מהפריט שאליו משויך האירוע. לדוגמה, אם האירוע מתאים להכנה מראש של הפריט הבא שיופעל.

עיבוד אירועים באמצעות PlaybackStatsListener

PlaybackStatsListener הוא AnalyticsListener שמטמיע עיבוד אירועים במכשיר. הערך PlaybackStats מחושב באמצעות מדדים נגזרים ומונים, כולל:

  • מדדי סיכום, לדוגמה, זמן ההפעלה הכולל.
  • מדדים של איכות הפעלה דינמית, למשל הרזולוציה הממוצעת של הסרטון.
  • מדדים של איכות הרינדור, לדוגמה, קצב הפריימים שהושמטו.
  • מדדי השימוש במשאבים, לדוגמה מספר הבייטים שנקראו ברשת.

רשימה מלאה של המדדים הנגזרים והספירות הזמינות מופיעה ב-PlaybackStats Javadoc.

PlaybackStatsListener מחשב ערכי PlaybackStats נפרדים לכל פריט מדיה בפלייליסט, וגם לכל מודעה בצד הלקוח שמוכנסת בתוך הפריטים האלה. אפשר לספק קריאה חוזרת ל-PlaybackStatsListener כדי לקבל מידע על השמעה שהסתיימה, ולהשתמש ב-EventTime שמועבר לקריאה החוזרת כדי לזהות איזו השמעה הסתיימה. אפשר לצבור את נתוני הניתוח של כמה הפעלות. אפשר גם לשלוח שאילתה אל PlaybackStats לגבי סשן ההפעלה הנוכחי בכל שלב באמצעות PlaybackStatsListener.getPlaybackStats().

Kotlin

exoPlayer.addAnalyticsListener(
  PlaybackStatsListener(/* keepHistory= */ true) {
    eventTime: EventTime?,
    playbackStats: PlaybackStats?
    -> // Analytics data for the session started at `eventTime` is ready.
  }
)

Java

exoPlayer.addAnalyticsListener(
    new PlaybackStatsListener(
        /* keepHistory= */ true,
        (eventTime, playbackStats) -> {
          // Analytics data for the session started at `eventTime` is ready.
        }));

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

שימו לב שב-PlaybackStats נעשה שימוש בקבוצה מורחבת של מצבים כדי לציין לא רק את מצב המדיה, אלא גם את כוונת המשתמש להפעיל את המדיה ומידע מפורט יותר, כמו הסיבה להפסקה או לסיום של ההפעלה:

מצב הפעלה כוונת המשתמש לשחק אין כוונה להפעיל
לפני ההפעלה JOINING_FOREGROUND NOT_STARTED, JOINING_BACKGROUND
הפעלה פעילה PLAYING
ההפעלה הופסקה BUFFERING, SEEKING PAUSED, PAUSED_BUFFERING, SUPPRESSED, SUPPRESSED_BUFFERING, INTERRUPTED_BY_AD
מצבי סיום ENDED, STOPPED, FAILED, ABANDONED

הכוונה של המשתמש לשחק חשובה כדי להבחין בין מקרים שבהם המשתמש המתין באופן פעיל להמשך ההפעלה לבין זמני המתנה פסיביים. לדוגמה, הפונקציה PlaybackStats.getTotalWaitTimeMs מחזירה את הזמן הכולל שחלף במצבים JOINING_FOREGROUND, BUFFERING ו-SEEKING, אבל לא את הזמן שחלף כשההפעלה הייתה מושהית. באופן דומה, הפונקציה PlaybackStats.getTotalPlayAndWaitTimeMs תחזיר את הזמן הכולל שבו משתמש התכוון לשחק, כלומר את זמן ההמתנה הפעיל הכולל ואת הזמן הכולל שבו המשתמש היה במצב PLAYING.

אירועים שעברו עיבוד ופירוש

אפשר לתעד אירועים שעברו עיבוד ופירוש באמצעות PlaybackStatsListener עם keepHistory=true. התוצאה PlaybackStats תכיל את רשימות האירועים הבאות:

  • playbackStateHistory: רשימה מסודרת של מצבי הפעלה מורחבים עם EventTime שבו הם התחילו לחול. אפשר גם להשתמש ב-PlaybackStats.getPlaybackStateAtTime כדי לחפש את המצב בשעה מסוימת בשעון.
  • mediaTimeHistory: היסטוריה של זוגות של זמן שעון קיר וזמן מדיה, שמאפשרת לשחזר אילו חלקים של המדיה הופעלו באיזה זמן. אפשר גם להשתמש ב-PlaybackStats.getMediaTimeMsAtRealtimeMs כדי לחפש את מיקום ההפעלה בשעה מסוימת.
  • videoFormatHistory ו-audioFormatHistory: רשימות מסודרות של פורמטים של וידאו ואודיו שנעשה בהם שימוש במהלך ההפעלה, עם EventTime שבו התחיל השימוש בהם.
  • fatalErrorHistory ו-nonFatalErrorHistory: רשימות מסודרות של שגיאות קריטיות ושגיאות לא קריטיות עם התג EventTime שבו הן התרחשו. שגיאות קריטיות הן שגיאות שהפסיקו את ההפעלה, בעוד ששגיאות לא קריטיות הן שגיאות שאפשר היה לתקן.

נתוני ניתוח של הפעלה יחידה

הנתונים האלה נאספים באופן אוטומטי אם אתם משתמשים ב-PlaybackStatsListener, גם אם אתם משתמשים ב-keepHistory=false. הערכים הסופיים הם השדות הציבוריים שאפשר למצוא ב-PlaybackStats Javadoc ובמשכי הזמן של מצב ההפעלה שמוחזרים על ידי getPlaybackStateDurationMs. לנוחותכם, יש גם שיטות כמו getTotalPlayTimeMs ו-getTotalWaitTimeMs שמחזירות את משך הזמן של שילובים ספציפיים של מצבי הפעלה.

Kotlin

Log.d(
  "DEBUG",
  "Playback summary: " +
    "play time = " +
    playbackStats.totalPlayTimeMs +
    ", rebuffers = " +
    playbackStats.totalRebufferCount,
)

Java

Log.d(
    "DEBUG",
    "Playback summary: "
        + "play time = "
        + playbackStats.getTotalPlayTimeMs()
        + ", rebuffers = "
        + playbackStats.totalRebufferCount);

נתוני ניתוח מצטברים של כמה הפעלות

אפשר לשלב כמה PlaybackStats יחד על ידי התקשרות אל PlaybackStats.merge. התוצאה PlaybackStats תכיל את הנתונים המצטברים של כל ההפעלות שמוזגו. הערה: הדוח לא יכלול את ההיסטוריה של אירועי הפעלה נפרדים, כי אי אפשר לצבור אותם.

אפשר להשתמש ב-PlaybackStatsListener.getCombinedPlaybackStats כדי לקבל תצוגה מצטברת של כל נתוני הניתוח שנאספו במהלך חייו של PlaybackStatsListener.

מדדי סיכום מחושבים

בנוסף לנתוני הניתוח הבסיסיים, PlaybackStats מספק הרבה שיטות לחישוב מדדי סיכום.

Kotlin

Log.d(
  "DEBUG",
  "Additional calculated summary metrics: " +
    "average video bitrate = " +
    playbackStats.meanVideoFormatBitrate +
    ", mean time between rebuffers = " +
    playbackStats.meanTimeBetweenRebuffers,
)

Java

Log.d(
    "DEBUG",
    "Additional calculated summary metrics: "
        + "average video bitrate = "
        + playbackStats.getMeanVideoFormatBitrate()
        + ", mean time between rebuffers = "
        + playbackStats.getMeanTimeBetweenRebuffers());

נושאים מתקדמים

שיוך נתוני ניתוח למטא-נתונים של הפעלה

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

מומלץ להגדיר מטא-נתונים ספציפיים למדיה באמצעות MediaItem.Builder.setTag. תג המדיה הוא חלק מהאירוע EventTime שמדווח על אירועים גולמיים, וכשהם מסתיימים, כך שאפשר לאחזר אותו בקלות כשמטפלים בנתוני הניתוח המתאימים:PlaybackStats

Kotlin

PlaybackStatsListener(/* keepHistory= */ false) {
  eventTime: EventTime,
  playbackStats: PlaybackStats ->
  val mediaTag =
    eventTime.timeline
      .getWindow(eventTime.windowIndex, Timeline.Window())
      .mediaItem
      .localConfiguration
      ?.tag
  // Report playbackStats with mediaTag metadata.
}

Java

new PlaybackStatsListener(
    /* keepHistory= */ false,
    (eventTime, playbackStats) -> {
      Object mediaTag =
          eventTime.timeline.getWindow(eventTime.windowIndex, new Timeline.Window())
              .mediaItem
              .localConfiguration
              .tag;
      // Report playbackStats with mediaTag metadata.
    });

דיווח על אירועים מותאמים אישית של Analytics

אם אתם צריכים להוסיף אירועים מותאמים אישית לנתוני הניתוח, אתם צריכים לשמור את האירועים האלה במבנה נתונים משלכם ולשלב אותם עם הנתונים שדווחו על PlaybackStats בהמשך. אם זה עוזר, אפשר להרחיב את DefaultAnalyticsCollector כדי ליצור מופעים של EventTime לאירועים בהתאמה אישית ולשלוח אותם למאזינים שכבר רשומים, כמו בדוגמה הבאה.

Kotlin

@OptIn(UnstableApi::class)
private interface ExtendedListener : AnalyticsListener {
  fun onCustomEvent(eventTime: EventTime)
}

@OptIn(UnstableApi::class)
private class ExtendedCollector : DefaultAnalyticsCollector(Clock.DEFAULT) {

  fun customEvent() {
    val eventTime = super.generateCurrentPlayerMediaPeriodEventTime()
    super.sendEvent(eventTime, CUSTOM_EVENT_ID) { listener: AnalyticsListener ->
      if (listener is ExtendedListener) {
        listener.onCustomEvent(eventTime)
      }
    }
  }
}

@OptIn(UnstableApi::class)
fun useExtendedAnalyticsCollector(context: Context) {
  // Usage - Setup and listener registration.
  val player = ExoPlayer.Builder(context).setAnalyticsCollector(ExtendedCollector()).build()
  player.addAnalyticsListener(
    object : ExtendedListener {
      override fun onCustomEvent(eventTime: EventTime) {
        // Save custom event for analytics data.
      }
    }
  )
  // Usage - Triggering the custom event.
  (player.analyticsCollector as ExtendedCollector).customEvent()
}

Java

@OptIn(markerClass = UnstableApi.class)
private interface ExtendedListener extends AnalyticsListener {
  void onCustomEvent(EventTime eventTime);
}

@OptIn(markerClass = UnstableApi.class)
private static class ExtendedCollector extends DefaultAnalyticsCollector {
  public ExtendedCollector() {
    super(Clock.DEFAULT);
  }

  public void customEvent() {
    AnalyticsListener.EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        CUSTOM_EVENT_ID,
        listener -> {
          if (listener instanceof ExtendedListener) {
            ((ExtendedListener) listener).onCustomEvent(eventTime);
          }
        });
  }
}

@OptIn(markerClass = UnstableApi.class)
public static void useExtendedAnalyticsCollector(Context context) {
  // Usage - Setup and listener registration.
  ExoPlayer player =
      new ExoPlayer.Builder(context).setAnalyticsCollector(new ExtendedCollector()).build();
  player.addAnalyticsListener(
      (ExtendedListener)
          eventTime -> {
            // Save custom event for analytics data.
          });
  // Usage - Triggering the custom event.
  ((ExtendedCollector) player.getAnalyticsCollector()).customEvent();
}