• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.exoplayer2.analytics;
17 
18 import android.os.SystemClock;
19 import androidx.annotation.Nullable;
20 import com.google.android.exoplayer2.C;
21 import com.google.android.exoplayer2.ExoPlaybackException;
22 import com.google.android.exoplayer2.Format;
23 import com.google.android.exoplayer2.Player;
24 import com.google.android.exoplayer2.Timeline;
25 import com.google.android.exoplayer2.Timeline.Period;
26 import com.google.android.exoplayer2.analytics.PlaybackStats.EventTimeAndException;
27 import com.google.android.exoplayer2.analytics.PlaybackStats.EventTimeAndFormat;
28 import com.google.android.exoplayer2.analytics.PlaybackStats.EventTimeAndPlaybackState;
29 import com.google.android.exoplayer2.analytics.PlaybackStats.PlaybackState;
30 import com.google.android.exoplayer2.source.LoadEventInfo;
31 import com.google.android.exoplayer2.source.MediaLoadData;
32 import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
33 import com.google.android.exoplayer2.source.TrackGroupArray;
34 import com.google.android.exoplayer2.trackselection.TrackSelection;
35 import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
36 import com.google.android.exoplayer2.util.Assertions;
37 import com.google.android.exoplayer2.util.MimeTypes;
38 import com.google.android.exoplayer2.util.Util;
39 import java.io.IOException;
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.Collections;
43 import java.util.HashMap;
44 import java.util.List;
45 import java.util.Map;
46 
47 /**
48  * {@link AnalyticsListener} to gather {@link PlaybackStats} from the player.
49  *
50  * <p>For accurate measurements, the listener should be added to the player before loading media,
51  * i.e., {@link Player#getPlaybackState()} should be {@link Player#STATE_IDLE}.
52  *
53  * <p>Playback stats are gathered separately for each playback session, i.e. each window in the
54  * {@link Timeline} and each single ad.
55  */
56 public final class PlaybackStatsListener
57     implements AnalyticsListener, PlaybackSessionManager.Listener {
58 
59   /** A listener for {@link PlaybackStats} updates. */
60   public interface Callback {
61 
62     /**
63      * Called when a playback session ends and its {@link PlaybackStats} are ready.
64      *
65      * @param eventTime The {@link EventTime} at which the playback session started. Can be used to
66      *     identify the playback session.
67      * @param playbackStats The {@link PlaybackStats} for the ended playback session.
68      */
onPlaybackStatsReady(EventTime eventTime, PlaybackStats playbackStats)69     void onPlaybackStatsReady(EventTime eventTime, PlaybackStats playbackStats);
70   }
71 
72   private final PlaybackSessionManager sessionManager;
73   private final Map<String, PlaybackStatsTracker> playbackStatsTrackers;
74   private final Map<String, EventTime> sessionStartEventTimes;
75   @Nullable private final Callback callback;
76   private final boolean keepHistory;
77   private final Period period;
78 
79   private PlaybackStats finishedPlaybackStats;
80   @Nullable private String activeContentPlayback;
81   @Nullable private String activeAdPlayback;
82   private boolean playWhenReady;
83   @Player.State private int playbackState;
84   private boolean isSuppressed;
85   private float playbackSpeed;
86   private boolean onSeekStartedCalled;
87 
88   /**
89    * Creates listener for playback stats.
90    *
91    * @param keepHistory Whether the reported {@link PlaybackStats} should keep the full history of
92    *     events.
93    * @param callback An optional callback for finished {@link PlaybackStats}.
94    */
PlaybackStatsListener(boolean keepHistory, @Nullable Callback callback)95   public PlaybackStatsListener(boolean keepHistory, @Nullable Callback callback) {
96     this.callback = callback;
97     this.keepHistory = keepHistory;
98     sessionManager = new DefaultPlaybackSessionManager();
99     playbackStatsTrackers = new HashMap<>();
100     sessionStartEventTimes = new HashMap<>();
101     finishedPlaybackStats = PlaybackStats.EMPTY;
102     playWhenReady = false;
103     playbackState = Player.STATE_IDLE;
104     playbackSpeed = 1f;
105     period = new Period();
106     sessionManager.setListener(this);
107   }
108 
109   /**
110    * Returns the combined {@link PlaybackStats} for all playback sessions this listener was and is
111    * listening to.
112    *
113    * <p>Note that these {@link PlaybackStats} will not contain the full history of events.
114    *
115    * @return The combined {@link PlaybackStats} for all playback sessions.
116    */
getCombinedPlaybackStats()117   public PlaybackStats getCombinedPlaybackStats() {
118     PlaybackStats[] allPendingPlaybackStats = new PlaybackStats[playbackStatsTrackers.size() + 1];
119     allPendingPlaybackStats[0] = finishedPlaybackStats;
120     int index = 1;
121     for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) {
122       allPendingPlaybackStats[index++] = tracker.build(/* isFinal= */ false);
123     }
124     return PlaybackStats.merge(allPendingPlaybackStats);
125   }
126 
127   /**
128    * Returns the {@link PlaybackStats} for the currently playback session, or null if no session is
129    * active.
130    *
131    * @return {@link PlaybackStats} for the current playback session.
132    */
133   @Nullable
getPlaybackStats()134   public PlaybackStats getPlaybackStats() {
135     @Nullable
136     PlaybackStatsTracker activeStatsTracker =
137         activeAdPlayback != null
138             ? playbackStatsTrackers.get(activeAdPlayback)
139             : activeContentPlayback != null
140                 ? playbackStatsTrackers.get(activeContentPlayback)
141                 : null;
142     return activeStatsTracker == null ? null : activeStatsTracker.build(/* isFinal= */ false);
143   }
144 
145   /**
146    * Finishes all pending playback sessions. Should be called when the listener is removed from the
147    * player or when the player is released.
148    */
finishAllSessions()149   public void finishAllSessions() {
150     // TODO: Add AnalyticsListener.onAttachedToPlayer and onDetachedFromPlayer to auto-release with
151     // an actual EventTime. Should also simplify other cases where the listener needs to be released
152     // separately from the player.
153     EventTime dummyEventTime =
154         new EventTime(
155             SystemClock.elapsedRealtime(),
156             Timeline.EMPTY,
157             /* windowIndex= */ 0,
158             /* mediaPeriodId= */ null,
159             /* eventPlaybackPositionMs= */ 0,
160             /* currentPlaybackPositionMs= */ 0,
161             /* totalBufferedDurationMs= */ 0);
162     sessionManager.finishAllSessions(dummyEventTime);
163   }
164 
165   // PlaybackSessionManager.Listener implementation.
166 
167   @Override
onSessionCreated(EventTime eventTime, String session)168   public void onSessionCreated(EventTime eventTime, String session) {
169     PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime);
170     if (onSeekStartedCalled) {
171       tracker.onSeekStarted(eventTime, /* belongsToPlayback= */ true);
172     }
173     tracker.onPlaybackStateChanged(eventTime, playbackState, /* belongsToPlayback= */ true);
174     tracker.onPlayWhenReadyChanged(eventTime, playWhenReady, /* belongsToPlayback= */ true);
175     tracker.onIsSuppressedChanged(eventTime, isSuppressed, /* belongsToPlayback= */ true);
176     tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed);
177     playbackStatsTrackers.put(session, tracker);
178     sessionStartEventTimes.put(session, eventTime);
179   }
180 
181   @Override
onSessionActive(EventTime eventTime, String session)182   public void onSessionActive(EventTime eventTime, String session) {
183     Assertions.checkNotNull(playbackStatsTrackers.get(session)).onForeground(eventTime);
184     if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) {
185       activeAdPlayback = session;
186     } else {
187       activeContentPlayback = session;
188     }
189   }
190 
191   @Override
onAdPlaybackStarted(EventTime eventTime, String contentSession, String adSession)192   public void onAdPlaybackStarted(EventTime eventTime, String contentSession, String adSession) {
193     Assertions.checkState(Assertions.checkNotNull(eventTime.mediaPeriodId).isAd());
194     long contentPeriodPositionUs =
195         eventTime
196             .timeline
197             .getPeriodByUid(eventTime.mediaPeriodId.periodUid, period)
198             .getAdGroupTimeUs(eventTime.mediaPeriodId.adGroupIndex);
199     long contentWindowPositionUs =
200         contentPeriodPositionUs == C.TIME_END_OF_SOURCE
201             ? C.TIME_END_OF_SOURCE
202             : contentPeriodPositionUs + period.getPositionInWindowUs();
203     EventTime contentEventTime =
204         new EventTime(
205             eventTime.realtimeMs,
206             eventTime.timeline,
207             eventTime.windowIndex,
208             new MediaPeriodId(
209                 eventTime.mediaPeriodId.periodUid,
210                 eventTime.mediaPeriodId.windowSequenceNumber,
211                 eventTime.mediaPeriodId.adGroupIndex),
212             /* eventPlaybackPositionMs= */ C.usToMs(contentWindowPositionUs),
213             eventTime.currentPlaybackPositionMs,
214             eventTime.totalBufferedDurationMs);
215     Assertions.checkNotNull(playbackStatsTrackers.get(contentSession))
216         .onInterruptedByAd(contentEventTime);
217   }
218 
219   @Override
onSessionFinished(EventTime eventTime, String session, boolean automaticTransition)220   public void onSessionFinished(EventTime eventTime, String session, boolean automaticTransition) {
221     if (session.equals(activeAdPlayback)) {
222       activeAdPlayback = null;
223     } else if (session.equals(activeContentPlayback)) {
224       activeContentPlayback = null;
225     }
226     PlaybackStatsTracker tracker = Assertions.checkNotNull(playbackStatsTrackers.remove(session));
227     EventTime startEventTime = Assertions.checkNotNull(sessionStartEventTimes.remove(session));
228     if (automaticTransition) {
229       // Simulate ENDED state to record natural ending of playback.
230       tracker.onPlaybackStateChanged(eventTime, Player.STATE_ENDED, /* belongsToPlayback= */ false);
231     }
232     tracker.onFinished(eventTime);
233     PlaybackStats playbackStats = tracker.build(/* isFinal= */ true);
234     finishedPlaybackStats = PlaybackStats.merge(finishedPlaybackStats, playbackStats);
235     if (callback != null) {
236       callback.onPlaybackStatsReady(startEventTime, playbackStats);
237     }
238   }
239 
240   // AnalyticsListener implementation.
241 
242   @Override
onPlaybackStateChanged(EventTime eventTime, @Player.State int state)243   public void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) {
244     playbackState = state;
245     maybeAddSession(eventTime);
246     for (String session : playbackStatsTrackers.keySet()) {
247       boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
248       playbackStatsTrackers
249           .get(session)
250           .onPlaybackStateChanged(eventTime, playbackState, belongsToPlayback);
251     }
252   }
253 
254   @Override
onPlayWhenReadyChanged( EventTime eventTime, boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason)255   public void onPlayWhenReadyChanged(
256       EventTime eventTime, boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {
257     this.playWhenReady = playWhenReady;
258     maybeAddSession(eventTime);
259     for (String session : playbackStatsTrackers.keySet()) {
260       boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
261       playbackStatsTrackers
262           .get(session)
263           .onPlayWhenReadyChanged(eventTime, playWhenReady, belongsToPlayback);
264     }
265   }
266 
267   @Override
onPlaybackSuppressionReasonChanged( EventTime eventTime, @Player.PlaybackSuppressionReason int playbackSuppressionReason)268   public void onPlaybackSuppressionReasonChanged(
269       EventTime eventTime, @Player.PlaybackSuppressionReason int playbackSuppressionReason) {
270     isSuppressed = playbackSuppressionReason != Player.PLAYBACK_SUPPRESSION_REASON_NONE;
271     maybeAddSession(eventTime);
272     for (String session : playbackStatsTrackers.keySet()) {
273       boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
274       playbackStatsTrackers
275           .get(session)
276           .onIsSuppressedChanged(eventTime, isSuppressed, belongsToPlayback);
277     }
278   }
279 
280   @Override
onTimelineChanged(EventTime eventTime, @Player.TimelineChangeReason int reason)281   public void onTimelineChanged(EventTime eventTime, @Player.TimelineChangeReason int reason) {
282     sessionManager.handleTimelineUpdate(eventTime);
283     maybeAddSession(eventTime);
284     for (String session : playbackStatsTrackers.keySet()) {
285       if (sessionManager.belongsToSession(eventTime, session)) {
286         playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime, /* isSeek= */ false);
287       }
288     }
289   }
290 
291   @Override
onPositionDiscontinuity(EventTime eventTime, @Player.DiscontinuityReason int reason)292   public void onPositionDiscontinuity(EventTime eventTime, @Player.DiscontinuityReason int reason) {
293     sessionManager.handlePositionDiscontinuity(eventTime, reason);
294     maybeAddSession(eventTime);
295     if (reason == Player.DISCONTINUITY_REASON_SEEK) {
296       onSeekStartedCalled = false;
297     }
298     for (String session : playbackStatsTrackers.keySet()) {
299       if (sessionManager.belongsToSession(eventTime, session)) {
300         playbackStatsTrackers
301             .get(session)
302             .onPositionDiscontinuity(
303                 eventTime, /* isSeek= */ reason == Player.DISCONTINUITY_REASON_SEEK);
304       }
305     }
306   }
307 
308   @Override
onSeekStarted(EventTime eventTime)309   public void onSeekStarted(EventTime eventTime) {
310     maybeAddSession(eventTime);
311     for (String session : playbackStatsTrackers.keySet()) {
312       boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
313       playbackStatsTrackers.get(session).onSeekStarted(eventTime, belongsToPlayback);
314     }
315     onSeekStartedCalled = true;
316   }
317 
318   @Override
onPlayerError(EventTime eventTime, ExoPlaybackException error)319   public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {
320     maybeAddSession(eventTime);
321     for (String session : playbackStatsTrackers.keySet()) {
322       if (sessionManager.belongsToSession(eventTime, session)) {
323         playbackStatsTrackers.get(session).onFatalError(eventTime, error);
324       }
325     }
326   }
327 
328   @Override
onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed)329   public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) {
330     this.playbackSpeed = playbackSpeed;
331     maybeAddSession(eventTime);
332     for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) {
333       tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed);
334     }
335   }
336 
337   @Override
onTracksChanged( EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections)338   public void onTracksChanged(
339       EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
340     maybeAddSession(eventTime);
341     for (String session : playbackStatsTrackers.keySet()) {
342       if (sessionManager.belongsToSession(eventTime, session)) {
343         playbackStatsTrackers.get(session).onTracksChanged(eventTime, trackSelections);
344       }
345     }
346   }
347 
348   @Override
onLoadStarted( EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData)349   public void onLoadStarted(
350       EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
351     maybeAddSession(eventTime);
352     for (String session : playbackStatsTrackers.keySet()) {
353       if (sessionManager.belongsToSession(eventTime, session)) {
354         playbackStatsTrackers.get(session).onLoadStarted(eventTime);
355       }
356     }
357   }
358 
359   @Override
onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData)360   public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {
361     maybeAddSession(eventTime);
362     for (String session : playbackStatsTrackers.keySet()) {
363       if (sessionManager.belongsToSession(eventTime, session)) {
364         playbackStatsTrackers.get(session).onDownstreamFormatChanged(eventTime, mediaLoadData);
365       }
366     }
367   }
368 
369   @Override
onVideoSizeChanged( EventTime eventTime, int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio)370   public void onVideoSizeChanged(
371       EventTime eventTime,
372       int width,
373       int height,
374       int unappliedRotationDegrees,
375       float pixelWidthHeightRatio) {
376     maybeAddSession(eventTime);
377     for (String session : playbackStatsTrackers.keySet()) {
378       if (sessionManager.belongsToSession(eventTime, session)) {
379         playbackStatsTrackers.get(session).onVideoSizeChanged(eventTime, width, height);
380       }
381     }
382   }
383 
384   @Override
onBandwidthEstimate( EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate)385   public void onBandwidthEstimate(
386       EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {
387     maybeAddSession(eventTime);
388     for (String session : playbackStatsTrackers.keySet()) {
389       if (sessionManager.belongsToSession(eventTime, session)) {
390         playbackStatsTrackers.get(session).onBandwidthData(totalLoadTimeMs, totalBytesLoaded);
391       }
392     }
393   }
394 
395   @Override
onAudioUnderrun( EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs)396   public void onAudioUnderrun(
397       EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
398     maybeAddSession(eventTime);
399     for (String session : playbackStatsTrackers.keySet()) {
400       if (sessionManager.belongsToSession(eventTime, session)) {
401         playbackStatsTrackers.get(session).onAudioUnderrun();
402       }
403     }
404   }
405 
406   @Override
onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs)407   public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {
408     maybeAddSession(eventTime);
409     for (String session : playbackStatsTrackers.keySet()) {
410       if (sessionManager.belongsToSession(eventTime, session)) {
411         playbackStatsTrackers.get(session).onDroppedVideoFrames(droppedFrames);
412       }
413     }
414   }
415 
416   @Override
onLoadError( EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, boolean wasCanceled)417   public void onLoadError(
418       EventTime eventTime,
419       LoadEventInfo loadEventInfo,
420       MediaLoadData mediaLoadData,
421       IOException error,
422       boolean wasCanceled) {
423     maybeAddSession(eventTime);
424     for (String session : playbackStatsTrackers.keySet()) {
425       if (sessionManager.belongsToSession(eventTime, session)) {
426         playbackStatsTrackers.get(session).onNonFatalError(eventTime, error);
427       }
428     }
429   }
430 
431   @Override
onDrmSessionManagerError(EventTime eventTime, Exception error)432   public void onDrmSessionManagerError(EventTime eventTime, Exception error) {
433     maybeAddSession(eventTime);
434     for (String session : playbackStatsTrackers.keySet()) {
435       if (sessionManager.belongsToSession(eventTime, session)) {
436         playbackStatsTrackers.get(session).onNonFatalError(eventTime, error);
437       }
438     }
439   }
440 
maybeAddSession(EventTime eventTime)441   private void maybeAddSession(EventTime eventTime) {
442     boolean isCompletelyIdle = eventTime.timeline.isEmpty() && playbackState == Player.STATE_IDLE;
443     if (!isCompletelyIdle) {
444       sessionManager.updateSessions(eventTime);
445     }
446   }
447 
448   /** Tracker for playback stats of a single playback. */
449   private static final class PlaybackStatsTracker {
450 
451     // Final stats.
452     private final boolean keepHistory;
453     private final long[] playbackStateDurationsMs;
454     private final List<EventTimeAndPlaybackState> playbackStateHistory;
455     private final List<long[]> mediaTimeHistory;
456     private final List<EventTimeAndFormat> videoFormatHistory;
457     private final List<EventTimeAndFormat> audioFormatHistory;
458     private final List<EventTimeAndException> fatalErrorHistory;
459     private final List<EventTimeAndException> nonFatalErrorHistory;
460     private final boolean isAd;
461 
462     private long firstReportedTimeMs;
463     private boolean hasBeenReady;
464     private boolean hasEnded;
465     private boolean isJoinTimeInvalid;
466     private int pauseCount;
467     private int pauseBufferCount;
468     private int seekCount;
469     private int rebufferCount;
470     private long maxRebufferTimeMs;
471     private int initialVideoFormatHeight;
472     private long initialVideoFormatBitrate;
473     private long initialAudioFormatBitrate;
474     private long videoFormatHeightTimeMs;
475     private long videoFormatHeightTimeProduct;
476     private long videoFormatBitrateTimeMs;
477     private long videoFormatBitrateTimeProduct;
478     private long audioFormatTimeMs;
479     private long audioFormatBitrateTimeProduct;
480     private long bandwidthTimeMs;
481     private long bandwidthBytes;
482     private long droppedFrames;
483     private long audioUnderruns;
484     private int fatalErrorCount;
485     private int nonFatalErrorCount;
486 
487     // Current player state tracking.
488     private @PlaybackState int currentPlaybackState;
489     private long currentPlaybackStateStartTimeMs;
490     private boolean isSeeking;
491     private boolean isForeground;
492     private boolean isInterruptedByAd;
493     private boolean isFinished;
494     private boolean playWhenReady;
495     @Player.State private int playerPlaybackState;
496     private boolean isSuppressed;
497     private boolean hasFatalError;
498     private boolean startedLoading;
499     private long lastRebufferStartTimeMs;
500     @Nullable private Format currentVideoFormat;
501     @Nullable private Format currentAudioFormat;
502     private long lastVideoFormatStartTimeMs;
503     private long lastAudioFormatStartTimeMs;
504     private float currentPlaybackSpeed;
505 
506     /**
507      * Creates a tracker for playback stats.
508      *
509      * @param keepHistory Whether to keep a full history of events.
510      * @param startTime The {@link EventTime} at which the playback stats start.
511      */
PlaybackStatsTracker(boolean keepHistory, EventTime startTime)512     public PlaybackStatsTracker(boolean keepHistory, EventTime startTime) {
513       this.keepHistory = keepHistory;
514       playbackStateDurationsMs = new long[PlaybackStats.PLAYBACK_STATE_COUNT];
515       playbackStateHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
516       mediaTimeHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
517       videoFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
518       audioFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
519       fatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
520       nonFatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
521       currentPlaybackState = PlaybackStats.PLAYBACK_STATE_NOT_STARTED;
522       currentPlaybackStateStartTimeMs = startTime.realtimeMs;
523       playerPlaybackState = Player.STATE_IDLE;
524       firstReportedTimeMs = C.TIME_UNSET;
525       maxRebufferTimeMs = C.TIME_UNSET;
526       isAd = startTime.mediaPeriodId != null && startTime.mediaPeriodId.isAd();
527       initialAudioFormatBitrate = C.LENGTH_UNSET;
528       initialVideoFormatBitrate = C.LENGTH_UNSET;
529       initialVideoFormatHeight = C.LENGTH_UNSET;
530       currentPlaybackSpeed = 1f;
531     }
532 
533     /**
534      * Notifies the tracker of a playback state change event, including all playback state changes
535      * while the playback is not in the foreground.
536      *
537      * @param eventTime The {@link EventTime}.
538      * @param state The current {@link Player.State}.
539      * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback.
540      */
onPlaybackStateChanged( EventTime eventTime, @Player.State int state, boolean belongsToPlayback)541     public void onPlaybackStateChanged(
542         EventTime eventTime, @Player.State int state, boolean belongsToPlayback) {
543       playerPlaybackState = state;
544       if (state != Player.STATE_IDLE) {
545         hasFatalError = false;
546       }
547       if (state != Player.STATE_BUFFERING) {
548         isSeeking = false;
549       }
550       if (state == Player.STATE_IDLE || state == Player.STATE_ENDED) {
551         isInterruptedByAd = false;
552       }
553       maybeUpdatePlaybackState(eventTime, belongsToPlayback);
554     }
555 
556     /**
557      * Notifies the tracker of a play when ready change event, including all play when ready changes
558      * while the playback is not in the foreground.
559      *
560      * @param eventTime The {@link EventTime}.
561      * @param playWhenReady Whether the playback will proceed when ready.
562      * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback.
563      */
onPlayWhenReadyChanged( EventTime eventTime, boolean playWhenReady, boolean belongsToPlayback)564     public void onPlayWhenReadyChanged(
565         EventTime eventTime, boolean playWhenReady, boolean belongsToPlayback) {
566       this.playWhenReady = playWhenReady;
567       maybeUpdatePlaybackState(eventTime, belongsToPlayback);
568     }
569 
570     /**
571      * Notifies the tracker of a change to the playback suppression (e.g. due to audio focus loss),
572      * including all updates while the playback is not in the foreground.
573      *
574      * @param eventTime The {@link EventTime}.
575      * @param isSuppressed Whether playback is suppressed.
576      * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback.
577      */
onIsSuppressedChanged( EventTime eventTime, boolean isSuppressed, boolean belongsToPlayback)578     public void onIsSuppressedChanged(
579         EventTime eventTime, boolean isSuppressed, boolean belongsToPlayback) {
580       this.isSuppressed = isSuppressed;
581       maybeUpdatePlaybackState(eventTime, belongsToPlayback);
582     }
583 
584     /**
585      * Notifies the tracker of a position discontinuity or timeline update for the current playback.
586      *
587      * @param eventTime The {@link EventTime}.
588      * @param isSeek Whether the position discontinuity is for a seek.
589      */
onPositionDiscontinuity(EventTime eventTime, boolean isSeek)590     public void onPositionDiscontinuity(EventTime eventTime, boolean isSeek) {
591       if (isSeek && playerPlaybackState == Player.STATE_IDLE) {
592         isSeeking = false;
593       }
594       isInterruptedByAd = false;
595       maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
596     }
597 
598     /**
599      * Notifies the tracker of the start of a seek, including all seeks while the playback is not in
600      * the foreground.
601      *
602      * @param eventTime The {@link EventTime}.
603      * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback.
604      */
onSeekStarted(EventTime eventTime, boolean belongsToPlayback)605     public void onSeekStarted(EventTime eventTime, boolean belongsToPlayback) {
606       isSeeking = true;
607       maybeUpdatePlaybackState(eventTime, belongsToPlayback);
608     }
609 
610     /**
611      * Notifies the tracker of fatal player error in the current playback.
612      *
613      * @param eventTime The {@link EventTime}.
614      */
onFatalError(EventTime eventTime, Exception error)615     public void onFatalError(EventTime eventTime, Exception error) {
616       fatalErrorCount++;
617       if (keepHistory) {
618         fatalErrorHistory.add(new EventTimeAndException(eventTime, error));
619       }
620       hasFatalError = true;
621       isInterruptedByAd = false;
622       isSeeking = false;
623       maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
624     }
625 
626     /**
627      * Notifies the tracker that a load for the current playback has started.
628      *
629      * @param eventTime The {@link EventTime}.
630      */
onLoadStarted(EventTime eventTime)631     public void onLoadStarted(EventTime eventTime) {
632       startedLoading = true;
633       maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
634     }
635 
636     /**
637      * Notifies the tracker that the current playback became the active foreground playback.
638      *
639      * @param eventTime The {@link EventTime}.
640      */
onForeground(EventTime eventTime)641     public void onForeground(EventTime eventTime) {
642       isForeground = true;
643       maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
644     }
645 
646     /**
647      * Notifies the tracker that the current playback has been interrupted for ad playback.
648      *
649      * @param eventTime The {@link EventTime}.
650      */
onInterruptedByAd(EventTime eventTime)651     public void onInterruptedByAd(EventTime eventTime) {
652       isInterruptedByAd = true;
653       isSeeking = false;
654       maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
655     }
656 
657     /**
658      * Notifies the tracker that the current playback has finished.
659      *
660      * @param eventTime The {@link EventTime}. Not guaranteed to belong to the current playback.
661      */
onFinished(EventTime eventTime)662     public void onFinished(EventTime eventTime) {
663       isFinished = true;
664       maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ false);
665     }
666 
667     /**
668      * Notifies the tracker that the track selection for the current playback changed.
669      *
670      * @param eventTime The {@link EventTime}.
671      * @param trackSelections The new {@link TrackSelectionArray}.
672      */
onTracksChanged(EventTime eventTime, TrackSelectionArray trackSelections)673     public void onTracksChanged(EventTime eventTime, TrackSelectionArray trackSelections) {
674       boolean videoEnabled = false;
675       boolean audioEnabled = false;
676       for (TrackSelection trackSelection : trackSelections.getAll()) {
677         if (trackSelection != null && trackSelection.length() > 0) {
678           int trackType = MimeTypes.getTrackType(trackSelection.getFormat(0).sampleMimeType);
679           if (trackType == C.TRACK_TYPE_VIDEO) {
680             videoEnabled = true;
681           } else if (trackType == C.TRACK_TYPE_AUDIO) {
682             audioEnabled = true;
683           }
684         }
685       }
686       if (!videoEnabled) {
687         maybeUpdateVideoFormat(eventTime, /* newFormat= */ null);
688       }
689       if (!audioEnabled) {
690         maybeUpdateAudioFormat(eventTime, /* newFormat= */ null);
691       }
692     }
693 
694     /**
695      * Notifies the tracker that a format being read by the renderers for the current playback
696      * changed.
697      *
698      * @param eventTime The {@link EventTime}.
699      * @param mediaLoadData The {@link MediaLoadData} describing the format change.
700      */
onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData)701     public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {
702       if (mediaLoadData.trackType == C.TRACK_TYPE_VIDEO
703           || mediaLoadData.trackType == C.TRACK_TYPE_DEFAULT) {
704         maybeUpdateVideoFormat(eventTime, mediaLoadData.trackFormat);
705       } else if (mediaLoadData.trackType == C.TRACK_TYPE_AUDIO) {
706         maybeUpdateAudioFormat(eventTime, mediaLoadData.trackFormat);
707       }
708     }
709 
710     /**
711      * Notifies the tracker that the video size for the current playback changed.
712      *
713      * @param eventTime The {@link EventTime}.
714      * @param width The video width in pixels.
715      * @param height The video height in pixels.
716      */
onVideoSizeChanged(EventTime eventTime, int width, int height)717     public void onVideoSizeChanged(EventTime eventTime, int width, int height) {
718       if (currentVideoFormat != null && currentVideoFormat.height == Format.NO_VALUE) {
719         Format formatWithHeight =
720             currentVideoFormat.buildUpon().setWidth(width).setHeight(height).build();
721         maybeUpdateVideoFormat(eventTime, formatWithHeight);
722       }
723     }
724 
725     /**
726      * Notifies the tracker of a playback speed change, including all playback speed changes while
727      * the playback is not in the foreground.
728      *
729      * @param eventTime The {@link EventTime}.
730      * @param playbackSpeed The new playback speed.
731      */
onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed)732     public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) {
733       maybeUpdateMediaTimeHistory(eventTime.realtimeMs, eventTime.eventPlaybackPositionMs);
734       maybeRecordVideoFormatTime(eventTime.realtimeMs);
735       maybeRecordAudioFormatTime(eventTime.realtimeMs);
736       currentPlaybackSpeed = playbackSpeed;
737     }
738 
739     /** Notifies the builder of an audio underrun for the current playback. */
onAudioUnderrun()740     public void onAudioUnderrun() {
741       audioUnderruns++;
742     }
743 
744     /**
745      * Notifies the tracker of dropped video frames for the current playback.
746      *
747      * @param droppedFrames The number of dropped video frames.
748      */
onDroppedVideoFrames(int droppedFrames)749     public void onDroppedVideoFrames(int droppedFrames) {
750       this.droppedFrames += droppedFrames;
751     }
752 
753     /**
754      * Notifies the tracker of bandwidth measurement data for the current playback.
755      *
756      * @param timeMs The time for which bandwidth measurement data is available, in milliseconds.
757      * @param bytes The bytes transferred during {@code timeMs}.
758      */
onBandwidthData(long timeMs, long bytes)759     public void onBandwidthData(long timeMs, long bytes) {
760       bandwidthTimeMs += timeMs;
761       bandwidthBytes += bytes;
762     }
763 
764     /**
765      * Notifies the tracker of a non-fatal error in the current playback.
766      *
767      * @param eventTime The {@link EventTime}.
768      * @param error The error.
769      */
onNonFatalError(EventTime eventTime, Exception error)770     public void onNonFatalError(EventTime eventTime, Exception error) {
771       nonFatalErrorCount++;
772       if (keepHistory) {
773         nonFatalErrorHistory.add(new EventTimeAndException(eventTime, error));
774       }
775     }
776 
777     /**
778      * Builds the playback stats.
779      *
780      * @param isFinal Whether this is the final build and no further events are expected.
781      */
build(boolean isFinal)782     public PlaybackStats build(boolean isFinal) {
783       long[] playbackStateDurationsMs = this.playbackStateDurationsMs;
784       List<long[]> mediaTimeHistory = this.mediaTimeHistory;
785       if (!isFinal) {
786         long buildTimeMs = SystemClock.elapsedRealtime();
787         playbackStateDurationsMs =
788             Arrays.copyOf(this.playbackStateDurationsMs, PlaybackStats.PLAYBACK_STATE_COUNT);
789         long lastStateDurationMs = Math.max(0, buildTimeMs - currentPlaybackStateStartTimeMs);
790         playbackStateDurationsMs[currentPlaybackState] += lastStateDurationMs;
791         maybeUpdateMaxRebufferTimeMs(buildTimeMs);
792         maybeRecordVideoFormatTime(buildTimeMs);
793         maybeRecordAudioFormatTime(buildTimeMs);
794         mediaTimeHistory = new ArrayList<>(this.mediaTimeHistory);
795         if (keepHistory && currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING) {
796           mediaTimeHistory.add(guessMediaTimeBasedOnElapsedRealtime(buildTimeMs));
797         }
798       }
799       boolean isJoinTimeInvalid = this.isJoinTimeInvalid || !hasBeenReady;
800       long validJoinTimeMs =
801           isJoinTimeInvalid
802               ? C.TIME_UNSET
803               : playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND];
804       boolean hasBackgroundJoin =
805           playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND] > 0;
806       List<EventTimeAndFormat> videoHistory =
807           isFinal ? videoFormatHistory : new ArrayList<>(videoFormatHistory);
808       List<EventTimeAndFormat> audioHistory =
809           isFinal ? audioFormatHistory : new ArrayList<>(audioFormatHistory);
810       return new PlaybackStats(
811           /* playbackCount= */ 1,
812           playbackStateDurationsMs,
813           isFinal ? playbackStateHistory : new ArrayList<>(playbackStateHistory),
814           mediaTimeHistory,
815           firstReportedTimeMs,
816           /* foregroundPlaybackCount= */ isForeground ? 1 : 0,
817           /* abandonedBeforeReadyCount= */ hasBeenReady ? 0 : 1,
818           /* endedCount= */ hasEnded ? 1 : 0,
819           /* backgroundJoiningCount= */ hasBackgroundJoin ? 1 : 0,
820           validJoinTimeMs,
821           /* validJoinTimeCount= */ isJoinTimeInvalid ? 0 : 1,
822           pauseCount,
823           pauseBufferCount,
824           seekCount,
825           rebufferCount,
826           maxRebufferTimeMs,
827           /* adPlaybackCount= */ isAd ? 1 : 0,
828           videoHistory,
829           audioHistory,
830           videoFormatHeightTimeMs,
831           videoFormatHeightTimeProduct,
832           videoFormatBitrateTimeMs,
833           videoFormatBitrateTimeProduct,
834           audioFormatTimeMs,
835           audioFormatBitrateTimeProduct,
836           /* initialVideoFormatHeightCount= */ initialVideoFormatHeight == C.LENGTH_UNSET ? 0 : 1,
837           /* initialVideoFormatBitrateCount= */ initialVideoFormatBitrate == C.LENGTH_UNSET ? 0 : 1,
838           initialVideoFormatHeight,
839           initialVideoFormatBitrate,
840           /* initialAudioFormatBitrateCount= */ initialAudioFormatBitrate == C.LENGTH_UNSET ? 0 : 1,
841           initialAudioFormatBitrate,
842           bandwidthTimeMs,
843           bandwidthBytes,
844           droppedFrames,
845           audioUnderruns,
846           /* fatalErrorPlaybackCount= */ fatalErrorCount > 0 ? 1 : 0,
847           fatalErrorCount,
848           nonFatalErrorCount,
849           fatalErrorHistory,
850           nonFatalErrorHistory);
851     }
852 
maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlayback)853     private void maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlayback) {
854       @PlaybackState int newPlaybackState = resolveNewPlaybackState();
855       if (newPlaybackState == currentPlaybackState) {
856         return;
857       }
858       Assertions.checkArgument(eventTime.realtimeMs >= currentPlaybackStateStartTimeMs);
859 
860       long stateDurationMs = eventTime.realtimeMs - currentPlaybackStateStartTimeMs;
861       playbackStateDurationsMs[currentPlaybackState] += stateDurationMs;
862       if (firstReportedTimeMs == C.TIME_UNSET) {
863         firstReportedTimeMs = eventTime.realtimeMs;
864       }
865       isJoinTimeInvalid |= isInvalidJoinTransition(currentPlaybackState, newPlaybackState);
866       hasBeenReady |= isReadyState(newPlaybackState);
867       hasEnded |= newPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED;
868       if (!isPausedState(currentPlaybackState) && isPausedState(newPlaybackState)) {
869         pauseCount++;
870       }
871       if (newPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING) {
872         seekCount++;
873       }
874       if (!isRebufferingState(currentPlaybackState) && isRebufferingState(newPlaybackState)) {
875         rebufferCount++;
876         lastRebufferStartTimeMs = eventTime.realtimeMs;
877       }
878       if (isRebufferingState(currentPlaybackState)
879           && currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING
880           && newPlaybackState == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING) {
881         pauseBufferCount++;
882       }
883 
884       maybeUpdateMediaTimeHistory(
885           eventTime.realtimeMs,
886           /* mediaTimeMs= */ belongsToPlayback ? eventTime.eventPlaybackPositionMs : C.TIME_UNSET);
887       maybeUpdateMaxRebufferTimeMs(eventTime.realtimeMs);
888       maybeRecordVideoFormatTime(eventTime.realtimeMs);
889       maybeRecordAudioFormatTime(eventTime.realtimeMs);
890 
891       currentPlaybackState = newPlaybackState;
892       currentPlaybackStateStartTimeMs = eventTime.realtimeMs;
893       if (keepHistory) {
894         playbackStateHistory.add(new EventTimeAndPlaybackState(eventTime, currentPlaybackState));
895       }
896     }
897 
resolveNewPlaybackState()898     private @PlaybackState int resolveNewPlaybackState() {
899       if (isFinished) {
900         // Keep VIDEO_STATE_ENDED if playback naturally ended (or progressed to next item).
901         return currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED
902             ? PlaybackStats.PLAYBACK_STATE_ENDED
903             : PlaybackStats.PLAYBACK_STATE_ABANDONED;
904       } else if (isSeeking && isForeground) {
905         // Seeking takes precedence over errors such that we report a seek while in error state.
906         return PlaybackStats.PLAYBACK_STATE_SEEKING;
907       } else if (hasFatalError) {
908         return PlaybackStats.PLAYBACK_STATE_FAILED;
909       } else if (!isForeground) {
910         // Before the playback becomes foreground, only report background joining and not started.
911         return startedLoading
912             ? PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND
913             : PlaybackStats.PLAYBACK_STATE_NOT_STARTED;
914       } else if (isInterruptedByAd) {
915         return PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD;
916       } else if (playerPlaybackState == Player.STATE_ENDED) {
917         return PlaybackStats.PLAYBACK_STATE_ENDED;
918       } else if (playerPlaybackState == Player.STATE_BUFFERING) {
919         if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_NOT_STARTED
920             || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND
921             || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND
922             || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) {
923           return PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND;
924         }
925         if (!playWhenReady) {
926           return PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING;
927         }
928         return isSuppressed
929             ? PlaybackStats.PLAYBACK_STATE_SUPPRESSED_BUFFERING
930             : PlaybackStats.PLAYBACK_STATE_BUFFERING;
931       } else if (playerPlaybackState == Player.STATE_READY) {
932         if (!playWhenReady) {
933           return PlaybackStats.PLAYBACK_STATE_PAUSED;
934         }
935         return isSuppressed
936             ? PlaybackStats.PLAYBACK_STATE_SUPPRESSED
937             : PlaybackStats.PLAYBACK_STATE_PLAYING;
938       } else if (playerPlaybackState == Player.STATE_IDLE
939           && currentPlaybackState != PlaybackStats.PLAYBACK_STATE_NOT_STARTED) {
940         // This case only applies for calls to player.stop(). All other IDLE cases are handled by
941         // !isForeground, hasFatalError or isSuspended. NOT_STARTED is deliberately ignored.
942         return PlaybackStats.PLAYBACK_STATE_STOPPED;
943       }
944       return currentPlaybackState;
945     }
946 
maybeUpdateMaxRebufferTimeMs(long nowMs)947     private void maybeUpdateMaxRebufferTimeMs(long nowMs) {
948       if (isRebufferingState(currentPlaybackState)) {
949         long rebufferDurationMs = nowMs - lastRebufferStartTimeMs;
950         if (maxRebufferTimeMs == C.TIME_UNSET || rebufferDurationMs > maxRebufferTimeMs) {
951           maxRebufferTimeMs = rebufferDurationMs;
952         }
953       }
954     }
955 
maybeUpdateMediaTimeHistory(long realtimeMs, long mediaTimeMs)956     private void maybeUpdateMediaTimeHistory(long realtimeMs, long mediaTimeMs) {
957       if (!keepHistory) {
958         return;
959       }
960       if (currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PLAYING) {
961         if (mediaTimeMs == C.TIME_UNSET) {
962           return;
963         }
964         if (!mediaTimeHistory.isEmpty()) {
965           long previousMediaTimeMs = mediaTimeHistory.get(mediaTimeHistory.size() - 1)[1];
966           if (previousMediaTimeMs != mediaTimeMs) {
967             mediaTimeHistory.add(new long[] {realtimeMs, previousMediaTimeMs});
968           }
969         }
970       }
971       mediaTimeHistory.add(
972           mediaTimeMs == C.TIME_UNSET
973               ? guessMediaTimeBasedOnElapsedRealtime(realtimeMs)
974               : new long[] {realtimeMs, mediaTimeMs});
975     }
976 
guessMediaTimeBasedOnElapsedRealtime(long realtimeMs)977     private long[] guessMediaTimeBasedOnElapsedRealtime(long realtimeMs) {
978       long[] previousKnownMediaTimeHistory = mediaTimeHistory.get(mediaTimeHistory.size() - 1);
979       long previousRealtimeMs = previousKnownMediaTimeHistory[0];
980       long previousMediaTimeMs = previousKnownMediaTimeHistory[1];
981       long elapsedMediaTimeEstimateMs =
982           (long) ((realtimeMs - previousRealtimeMs) * currentPlaybackSpeed);
983       long mediaTimeEstimateMs = previousMediaTimeMs + elapsedMediaTimeEstimateMs;
984       return new long[] {realtimeMs, mediaTimeEstimateMs};
985     }
986 
maybeUpdateVideoFormat(EventTime eventTime, @Nullable Format newFormat)987     private void maybeUpdateVideoFormat(EventTime eventTime, @Nullable Format newFormat) {
988       if (Util.areEqual(currentVideoFormat, newFormat)) {
989         return;
990       }
991       maybeRecordVideoFormatTime(eventTime.realtimeMs);
992       if (newFormat != null) {
993         if (initialVideoFormatHeight == C.LENGTH_UNSET && newFormat.height != Format.NO_VALUE) {
994           initialVideoFormatHeight = newFormat.height;
995         }
996         if (initialVideoFormatBitrate == C.LENGTH_UNSET && newFormat.bitrate != Format.NO_VALUE) {
997           initialVideoFormatBitrate = newFormat.bitrate;
998         }
999       }
1000       currentVideoFormat = newFormat;
1001       if (keepHistory) {
1002         videoFormatHistory.add(new EventTimeAndFormat(eventTime, currentVideoFormat));
1003       }
1004     }
1005 
maybeUpdateAudioFormat(EventTime eventTime, @Nullable Format newFormat)1006     private void maybeUpdateAudioFormat(EventTime eventTime, @Nullable Format newFormat) {
1007       if (Util.areEqual(currentAudioFormat, newFormat)) {
1008         return;
1009       }
1010       maybeRecordAudioFormatTime(eventTime.realtimeMs);
1011       if (newFormat != null
1012           && initialAudioFormatBitrate == C.LENGTH_UNSET
1013           && newFormat.bitrate != Format.NO_VALUE) {
1014         initialAudioFormatBitrate = newFormat.bitrate;
1015       }
1016       currentAudioFormat = newFormat;
1017       if (keepHistory) {
1018         audioFormatHistory.add(new EventTimeAndFormat(eventTime, currentAudioFormat));
1019       }
1020     }
1021 
maybeRecordVideoFormatTime(long nowMs)1022     private void maybeRecordVideoFormatTime(long nowMs) {
1023       if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING
1024           && currentVideoFormat != null) {
1025         long mediaDurationMs = (long) ((nowMs - lastVideoFormatStartTimeMs) * currentPlaybackSpeed);
1026         if (currentVideoFormat.height != Format.NO_VALUE) {
1027           videoFormatHeightTimeMs += mediaDurationMs;
1028           videoFormatHeightTimeProduct += mediaDurationMs * currentVideoFormat.height;
1029         }
1030         if (currentVideoFormat.bitrate != Format.NO_VALUE) {
1031           videoFormatBitrateTimeMs += mediaDurationMs;
1032           videoFormatBitrateTimeProduct += mediaDurationMs * currentVideoFormat.bitrate;
1033         }
1034       }
1035       lastVideoFormatStartTimeMs = nowMs;
1036     }
1037 
maybeRecordAudioFormatTime(long nowMs)1038     private void maybeRecordAudioFormatTime(long nowMs) {
1039       if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING
1040           && currentAudioFormat != null
1041           && currentAudioFormat.bitrate != Format.NO_VALUE) {
1042         long mediaDurationMs = (long) ((nowMs - lastAudioFormatStartTimeMs) * currentPlaybackSpeed);
1043         audioFormatTimeMs += mediaDurationMs;
1044         audioFormatBitrateTimeProduct += mediaDurationMs * currentAudioFormat.bitrate;
1045       }
1046       lastAudioFormatStartTimeMs = nowMs;
1047     }
1048 
isReadyState(@laybackState int state)1049     private static boolean isReadyState(@PlaybackState int state) {
1050       return state == PlaybackStats.PLAYBACK_STATE_PLAYING
1051           || state == PlaybackStats.PLAYBACK_STATE_PAUSED
1052           || state == PlaybackStats.PLAYBACK_STATE_SUPPRESSED;
1053     }
1054 
isPausedState(@laybackState int state)1055     private static boolean isPausedState(@PlaybackState int state) {
1056       return state == PlaybackStats.PLAYBACK_STATE_PAUSED
1057           || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING;
1058     }
1059 
isRebufferingState(@laybackState int state)1060     private static boolean isRebufferingState(@PlaybackState int state) {
1061       return state == PlaybackStats.PLAYBACK_STATE_BUFFERING
1062           || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING
1063           || state == PlaybackStats.PLAYBACK_STATE_SUPPRESSED_BUFFERING;
1064     }
1065 
isInvalidJoinTransition( @laybackState int oldState, @PlaybackState int newState)1066     private static boolean isInvalidJoinTransition(
1067         @PlaybackState int oldState, @PlaybackState int newState) {
1068       if (oldState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND
1069           && oldState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND
1070           && oldState != PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) {
1071         return false;
1072       }
1073       return newState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND
1074           && newState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND
1075           && newState != PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD
1076           && newState != PlaybackStats.PLAYBACK_STATE_PLAYING
1077           && newState != PlaybackStats.PLAYBACK_STATE_PAUSED
1078           && newState != PlaybackStats.PLAYBACK_STATE_SUPPRESSED
1079           && newState != PlaybackStats.PLAYBACK_STATE_ENDED;
1080     }
1081   }
1082 }
1083