Commit 77595da1 by tonihei Committed by Toni

Add initial PlaybackStats listener version.

This version includes all playback state related metrics and the general
listener set-up.

PiperOrigin-RevId: 250668729
parent c09a6eb8
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.analytics;
import android.os.SystemClock;
import androidx.annotation.IntDef;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Collections;
import java.util.List;
/** Statistics about playbacks. */
public final class PlaybackStats {
/**
* State of a playback. One of {@link #PLAYBACK_STATE_NOT_STARTED}, {@link
* #PLAYBACK_STATE_JOINING_FOREGROUND}, {@link #PLAYBACK_STATE_JOINING_BACKGROUND}, {@link
* #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED}, {@link #PLAYBACK_STATE_SEEKING},
* {@link #PLAYBACK_STATE_BUFFERING}, {@link #PLAYBACK_STATE_PAUSED_BUFFERING}, {@link
* #PLAYBACK_STATE_SEEK_BUFFERING}, {@link #PLAYBACK_STATE_ENDED}, {@link
* #PLAYBACK_STATE_STOPPED}, {@link #PLAYBACK_STATE_FAILED} or {@link #PLAYBACK_STATE_SUSPENDED}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@IntDef({
PLAYBACK_STATE_NOT_STARTED,
PLAYBACK_STATE_JOINING_BACKGROUND,
PLAYBACK_STATE_JOINING_FOREGROUND,
PLAYBACK_STATE_PLAYING,
PLAYBACK_STATE_PAUSED,
PLAYBACK_STATE_SEEKING,
PLAYBACK_STATE_BUFFERING,
PLAYBACK_STATE_PAUSED_BUFFERING,
PLAYBACK_STATE_SEEK_BUFFERING,
PLAYBACK_STATE_ENDED,
PLAYBACK_STATE_STOPPED,
PLAYBACK_STATE_FAILED,
PLAYBACK_STATE_SUSPENDED
})
@interface PlaybackState {}
/** Playback has not started (initial state). */
public static final int PLAYBACK_STATE_NOT_STARTED = 0;
/** Playback is buffering in the background for initial playback start. */
public static final int PLAYBACK_STATE_JOINING_BACKGROUND = 1;
/** Playback is buffering in the foreground for initial playback start. */
public static final int PLAYBACK_STATE_JOINING_FOREGROUND = 2;
/** Playback is actively playing. */
public static final int PLAYBACK_STATE_PLAYING = 3;
/** Playback is paused but ready to play. */
public static final int PLAYBACK_STATE_PAUSED = 4;
/** Playback is handling a seek. */
public static final int PLAYBACK_STATE_SEEKING = 5;
/** Playback is buffering to restart playback. */
public static final int PLAYBACK_STATE_BUFFERING = 6;
/** Playback is buffering while paused. */
public static final int PLAYBACK_STATE_PAUSED_BUFFERING = 7;
/** Playback is buffering after a seek. */
public static final int PLAYBACK_STATE_SEEK_BUFFERING = 8;
/** Playback has reached the end of the media. */
public static final int PLAYBACK_STATE_ENDED = 9;
/** Playback is stopped and can be resumed. */
public static final int PLAYBACK_STATE_STOPPED = 10;
/** Playback is stopped due a fatal error and can be retried. */
public static final int PLAYBACK_STATE_FAILED = 11;
/** Playback is suspended, e.g. because the user left or it is interrupted by another playback. */
public static final int PLAYBACK_STATE_SUSPENDED = 12;
/** Total number of playback states. */
/* package */ static final int PLAYBACK_STATE_COUNT = 13;
/** Empty playback stats. */
public static final PlaybackStats EMPTY = merge(/* nothing */ );
/**
* Returns the combined {@link PlaybackStats} for all input {@link PlaybackStats}.
*
* <p>Note that the full history of events is not kept as the history only makes sense in the
* context of a single playback.
*
* @param playbackStats Array of {@link PlaybackStats} to combine.
* @return The combined {@link PlaybackStats}.
*/
public static PlaybackStats merge(PlaybackStats... playbackStats) {
int playbackCount = 0;
long[] playbackStateDurationsMs = new long[PLAYBACK_STATE_COUNT];
long firstReportedTimeMs = C.TIME_UNSET;
int foregroundPlaybackCount = 0;
int abandonedBeforeReadyCount = 0;
int endedCount = 0;
int backgroundJoiningCount = 0;
long totalValidJoinTimeMs = C.TIME_UNSET;
int validJoinTimeCount = 0;
int pauseCount = 0;
int pauseBufferCount = 0;
int seekCount = 0;
int rebufferCount = 0;
long maxRebufferTimeMs = C.TIME_UNSET;
int adCount = 0;
for (PlaybackStats stats : playbackStats) {
playbackCount += stats.playbackCount;
for (int i = 0; i < PLAYBACK_STATE_COUNT; i++) {
playbackStateDurationsMs[i] += stats.playbackStateDurationsMs[i];
}
if (firstReportedTimeMs == C.TIME_UNSET) {
firstReportedTimeMs = stats.firstReportedTimeMs;
} else if (stats.firstReportedTimeMs != C.TIME_UNSET) {
firstReportedTimeMs = Math.min(firstReportedTimeMs, stats.firstReportedTimeMs);
}
foregroundPlaybackCount += stats.foregroundPlaybackCount;
abandonedBeforeReadyCount += stats.abandonedBeforeReadyCount;
endedCount += stats.endedCount;
backgroundJoiningCount += stats.backgroundJoiningCount;
if (totalValidJoinTimeMs == C.TIME_UNSET) {
totalValidJoinTimeMs = stats.totalValidJoinTimeMs;
} else if (stats.totalValidJoinTimeMs != C.TIME_UNSET) {
totalValidJoinTimeMs += stats.totalValidJoinTimeMs;
}
validJoinTimeCount += stats.validJoinTimeCount;
pauseCount += stats.totalPauseCount;
pauseBufferCount += stats.totalPauseBufferCount;
seekCount += stats.totalSeekCount;
rebufferCount += stats.totalRebufferCount;
if (maxRebufferTimeMs == C.TIME_UNSET) {
maxRebufferTimeMs = stats.maxRebufferTimeMs;
} else if (stats.maxRebufferTimeMs != C.TIME_UNSET) {
maxRebufferTimeMs = Math.max(maxRebufferTimeMs, stats.maxRebufferTimeMs);
}
adCount += stats.adPlaybackCount;
}
return new PlaybackStats(
playbackCount,
playbackStateDurationsMs,
/* playbackStateHistory */ Collections.emptyList(),
firstReportedTimeMs,
foregroundPlaybackCount,
abandonedBeforeReadyCount,
endedCount,
backgroundJoiningCount,
totalValidJoinTimeMs,
validJoinTimeCount,
pauseCount,
pauseBufferCount,
seekCount,
rebufferCount,
maxRebufferTimeMs,
adCount);
}
/** The number of individual playbacks for which these stats were collected. */
public final int playbackCount;
// Playback state stats.
/**
* The playback state history as ordered pairs of the {@link EventTime} at which a state became
* active and the {@link PlaybackState}.
*/
public final List<Pair<EventTime, @PlaybackState Integer>> playbackStateHistory;
/**
* The elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} of the first
* reported playback event, or {@link C#TIME_UNSET} if no event has been reported.
*/
public final long firstReportedTimeMs;
/** The number of playbacks which were the active foreground playback at some point. */
public final int foregroundPlaybackCount;
/** The number of playbacks which were abandoned before they were ready to play. */
public final int abandonedBeforeReadyCount;
/** The number of playbacks which reached the ended state at least once. */
public final int endedCount;
/** The number of playbacks which were pre-buffered in the background. */
public final int backgroundJoiningCount;
/**
* The total time spent joining the playback, in milliseconds, or {@link C#TIME_UNSET} if no valid
* join time could be determined.
*
* <p>Note that this does not include background joining time. A join time may be invalid if the
* playback never reached {@link #PLAYBACK_STATE_PLAYING} or {@link #PLAYBACK_STATE_PAUSED}, or
* joining was interrupted by a seek, stop, or error state.
*/
public final long totalValidJoinTimeMs;
/**
* The number of playbacks with a valid join time as documented in {@link #totalValidJoinTimeMs}.
*/
public final int validJoinTimeCount;
/** The total number of times a playback has been paused. */
public final int totalPauseCount;
/** The total number of times a playback has been paused while rebuffering. */
public final int totalPauseBufferCount;
/**
* The total number of times a seek occurred. This includes seeks happening before playback
* resumed after another seek.
*/
public final int totalSeekCount;
/**
* The total number of times a rebuffer occurred. This excludes initial joining and buffering
* after seek.
*/
public final int totalRebufferCount;
/**
* The maximum time spent during a single rebuffer, in milliseconds, or {@link C#TIME_UNSET} if no
* rebuffer occurred.
*/
public final long maxRebufferTimeMs;
/** The number of ad playbacks. */
public final int adPlaybackCount;
private final long[] playbackStateDurationsMs;
/* package */ PlaybackStats(
int playbackCount,
long[] playbackStateDurationsMs,
List<Pair<EventTime, @PlaybackState Integer>> playbackStateHistory,
long firstReportedTimeMs,
int foregroundPlaybackCount,
int abandonedBeforeReadyCount,
int endedCount,
int backgroundJoiningCount,
long totalValidJoinTimeMs,
int validJoinTimeCount,
int totalPauseCount,
int totalPauseBufferCount,
int totalSeekCount,
int totalRebufferCount,
long maxRebufferTimeMs,
int adPlaybackCount) {
this.playbackCount = playbackCount;
this.playbackStateDurationsMs = playbackStateDurationsMs;
this.playbackStateHistory = Collections.unmodifiableList(playbackStateHistory);
this.firstReportedTimeMs = firstReportedTimeMs;
this.foregroundPlaybackCount = foregroundPlaybackCount;
this.abandonedBeforeReadyCount = abandonedBeforeReadyCount;
this.endedCount = endedCount;
this.backgroundJoiningCount = backgroundJoiningCount;
this.totalValidJoinTimeMs = totalValidJoinTimeMs;
this.validJoinTimeCount = validJoinTimeCount;
this.totalPauseCount = totalPauseCount;
this.totalPauseBufferCount = totalPauseBufferCount;
this.totalSeekCount = totalSeekCount;
this.totalRebufferCount = totalRebufferCount;
this.maxRebufferTimeMs = maxRebufferTimeMs;
this.adPlaybackCount = adPlaybackCount;
}
/**
* Returns the total time spent in a given {@link PlaybackState}, in milliseconds.
*
* @param playbackState A {@link PlaybackState}.
* @return Total spent in the given playback state, in milliseconds
*/
public long getPlaybackStateDurationMs(@PlaybackState int playbackState) {
return playbackStateDurationsMs[playbackState];
}
/**
* Returns the {@link PlaybackState} at the given time.
*
* @param realtimeMs The time as returned by {@link SystemClock#elapsedRealtime()}.
* @return The {@link PlaybackState} at that time, or {@link #PLAYBACK_STATE_NOT_STARTED} if the
* given time is before the first known playback state in the history.
*/
@PlaybackState
public int getPlaybackStateAtTime(long realtimeMs) {
@PlaybackState int state = PLAYBACK_STATE_NOT_STARTED;
for (Pair<EventTime, @PlaybackState Integer> timeAndState : playbackStateHistory) {
if (timeAndState.first.realtimeMs > realtimeMs) {
break;
}
state = timeAndState.second;
}
return state;
}
/**
* Returns the mean time spent joining the playback, in milliseconds, or {@link C#TIME_UNSET} if
* no valid join time is available. Only includes playbacks with valid join times as documented in
* {@link #totalValidJoinTimeMs}.
*/
public long getMeanJoinTimeMs() {
return validJoinTimeCount == 0 ? C.TIME_UNSET : totalValidJoinTimeMs / validJoinTimeCount;
}
/**
* Returns the total time spent joining the playback in foreground, in milliseconds. This does
* include invalid join times where the playback never reached {@link #PLAYBACK_STATE_PLAYING} or
* {@link #PLAYBACK_STATE_PAUSED}, or joining was interrupted by a seek, stop, or error state.
*/
public long getTotalJoinTimeMs() {
return getPlaybackStateDurationMs(PLAYBACK_STATE_JOINING_FOREGROUND);
}
/** Returns the total time spent actively playing, in milliseconds. */
public long getTotalPlayTimeMs() {
return getPlaybackStateDurationMs(PLAYBACK_STATE_PLAYING);
}
/**
* Returns the mean time spent actively playing per foreground playback, in milliseconds, or
* {@link C#TIME_UNSET} if no playback has been in foreground.
*/
public long getMeanPlayTimeMs() {
return foregroundPlaybackCount == 0
? C.TIME_UNSET
: getTotalPlayTimeMs() / foregroundPlaybackCount;
}
/** Returns the total time spent in a paused state, in milliseconds. */
public long getTotalPausedTimeMs() {
return getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED)
+ getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED_BUFFERING);
}
/**
* Returns the mean time spent in a paused state per foreground playback, in milliseconds, or
* {@link C#TIME_UNSET} if no playback has been in foreground.
*/
public long getMeanPausedTimeMs() {
return foregroundPlaybackCount == 0
? C.TIME_UNSET
: getTotalPausedTimeMs() / foregroundPlaybackCount;
}
/**
* Returns the total time spent rebuffering, in milliseconds. This excludes initial join times,
* buffer times after a seek and buffering while paused.
*/
public long getTotalRebufferTimeMs() {
return getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING);
}
/**
* Returns the mean time spent rebuffering per foreground playback, in milliseconds, or {@link
* C#TIME_UNSET} if no playback has been in foreground. This excludes initial join times, buffer
* times after a seek and buffering while paused.
*/
public long getMeanRebufferTimeMs() {
return foregroundPlaybackCount == 0
? C.TIME_UNSET
: getTotalRebufferTimeMs() / foregroundPlaybackCount;
}
/**
* Returns the mean time spent during a single rebuffer, in milliseconds, or {@link C#TIME_UNSET}
* if no rebuffer was recorded. This excludes initial join times and buffer times after a seek.
*/
public long getMeanSingleRebufferTimeMs() {
return totalRebufferCount == 0
? C.TIME_UNSET
: (getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING)
+ getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED_BUFFERING))
/ totalRebufferCount;
}
/**
* Returns the total time spent from the start of a seek until playback is ready again, in
* milliseconds.
*/
public long getTotalSeekTimeMs() {
return getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING)
+ getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING);
}
/**
* Returns the mean time spent per foreground playback from the start of a seek until playback is
* ready again, in milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground.
*/
public long getMeanSeekTimeMs() {
return foregroundPlaybackCount == 0
? C.TIME_UNSET
: getTotalSeekTimeMs() / foregroundPlaybackCount;
}
/**
* Returns the mean time spent from the start of a single seek until playback is ready again, in
* milliseconds, or {@link C#TIME_UNSET} if no seek occurred.
*/
public long getMeanSingleSeekTimeMs() {
return totalSeekCount == 0 ? C.TIME_UNSET : getTotalSeekTimeMs() / totalSeekCount;
}
/**
* Returns the total time spent actively waiting for playback, in milliseconds. This includes all
* join times, rebuffer times and seek times, but excludes times without user intention to play,
* e.g. all paused states.
*/
public long getTotalWaitTimeMs() {
return getPlaybackStateDurationMs(PLAYBACK_STATE_JOINING_FOREGROUND)
+ getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING)
+ getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING)
+ getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING);
}
/**
* Returns the mean time spent actively waiting for playback per foreground playback, in
* milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground. This includes all
* join times, rebuffer times and seek times, but excludes times without user intention to play,
* e.g. all paused states.
*/
public long getMeanWaitTimeMs() {
return foregroundPlaybackCount == 0
? C.TIME_UNSET
: getTotalWaitTimeMs() / foregroundPlaybackCount;
}
/** Returns the total time spent playing or actively waiting for playback, in milliseconds. */
public long getTotalPlayAndWaitTimeMs() {
return getTotalPlayTimeMs() + getTotalWaitTimeMs();
}
/**
* Returns the mean time spent playing or actively waiting for playback per foreground playback,
* in milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground.
*/
public long getMeanPlayAndWaitTimeMs() {
return foregroundPlaybackCount == 0
? C.TIME_UNSET
: getTotalPlayAndWaitTimeMs() / foregroundPlaybackCount;
}
/** Returns the total time covered by any playback state, in milliseconds. */
public long getTotalElapsedTimeMs() {
long totalTimeMs = 0;
for (int i = 0; i < PLAYBACK_STATE_COUNT; i++) {
totalTimeMs += playbackStateDurationsMs[i];
}
return totalTimeMs;
}
/**
* Returns the mean time covered by any playback state per playback, in milliseconds, or {@link
* C#TIME_UNSET} if no playback was recorded.
*/
public long getMeanElapsedTimeMs() {
return playbackCount == 0 ? C.TIME_UNSET : getTotalElapsedTimeMs() / playbackCount;
}
/**
* Returns the ratio of foreground playbacks which were abandoned before they were ready to play,
* or {@code 0.0} if no playback has been in foreground.
*/
public float getAbandonedBeforeReadyRatio() {
int foregroundAbandonedBeforeReady =
abandonedBeforeReadyCount - (playbackCount - foregroundPlaybackCount);
return foregroundPlaybackCount == 0
? 0f
: (float) foregroundAbandonedBeforeReady / foregroundPlaybackCount;
}
/**
* Returns the ratio of foreground playbacks which reached the ended state at least once, or
* {@code 0.0} if no playback has been in foreground.
*/
public float getEndedRatio() {
return foregroundPlaybackCount == 0 ? 0f : (float) endedCount / foregroundPlaybackCount;
}
/**
* Returns the mean number of times a playback has been paused per foreground playback, or {@code
* 0.0} if no playback has been in foreground.
*/
public float getMeanPauseCount() {
return foregroundPlaybackCount == 0 ? 0f : (float) totalPauseCount / foregroundPlaybackCount;
}
/**
* Returns the mean number of times a playback has been paused while rebuffering per foreground
* playback, or {@code 0.0} if no playback has been in foreground.
*/
public float getMeanPauseBufferCount() {
return foregroundPlaybackCount == 0
? 0f
: (float) totalPauseBufferCount / foregroundPlaybackCount;
}
/**
* Returns the mean number of times a seek occurred per foreground playback, or {@code 0.0} if no
* playback has been in foreground. This includes seeks happening before playback resumed after
* another seek.
*/
public float getMeanSeekCount() {
return foregroundPlaybackCount == 0 ? 0f : (float) totalSeekCount / foregroundPlaybackCount;
}
/**
* Returns the mean number of times a rebuffer occurred per foreground playback, or {@code 0.0} if
* no playback has been in foreground. This excludes initial joining and buffering after seek.
*/
public float getMeanRebufferCount() {
return foregroundPlaybackCount == 0 ? 0f : (float) totalRebufferCount / foregroundPlaybackCount;
}
/**
* Returns the ratio of wait times to the total time spent playing and waiting, or {@code 0.0} if
* no time was spend playing or waiting. This is equivalent to {@link #getTotalWaitTimeMs()} /
* {@link #getTotalPlayAndWaitTimeMs()} and also to {@link #getJoinTimeRatio()} + {@link
* #getRebufferTimeRatio()} + {@link #getSeekTimeRatio()}.
*/
public float getWaitTimeRatio() {
long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs();
return playAndWaitTimeMs == 0 ? 0f : (float) getTotalWaitTimeMs() / playAndWaitTimeMs;
}
/**
* Returns the ratio of foreground join time to the total time spent playing and waiting, or
* {@code 0.0} if no time was spend playing or waiting. This is equivalent to {@link
* #getTotalJoinTimeMs()} / {@link #getTotalPlayAndWaitTimeMs()}.
*/
public float getJoinTimeRatio() {
long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs();
return playAndWaitTimeMs == 0 ? 0f : (float) getTotalJoinTimeMs() / playAndWaitTimeMs;
}
/**
* Returns the ratio of rebuffer time to the total time spent playing and waiting, or {@code 0.0}
* if no time was spend playing or waiting. This is equivalent to {@link
* #getTotalRebufferTimeMs()} / {@link #getTotalPlayAndWaitTimeMs()}.
*/
public float getRebufferTimeRatio() {
long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs();
return playAndWaitTimeMs == 0 ? 0f : (float) getTotalRebufferTimeMs() / playAndWaitTimeMs;
}
/**
* Returns the ratio of seek time to the total time spent playing and waiting, or {@code 0.0} if
* no time was spend playing or waiting. This is equivalent to {@link #getTotalSeekTimeMs()} /
* {@link #getTotalPlayAndWaitTimeMs()}.
*/
public float getSeekTimeRatio() {
long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs();
return playAndWaitTimeMs == 0 ? 0f : (float) getTotalSeekTimeMs() / playAndWaitTimeMs;
}
/**
* Returns the rate of rebuffer events, in rebuffers per play time second, or {@code 0.0} if no
* time was spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenRebuffers()}.
*/
public float getRebufferRate() {
long playTimeMs = getTotalPlayTimeMs();
return playTimeMs == 0 ? 0f : 1000f * totalRebufferCount / playTimeMs;
}
/**
* Returns the mean play time between rebuffer events, in seconds. This is equivalent to 1.0 /
* {@link #getRebufferRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}.
*/
public float getMeanTimeBetweenRebuffers() {
return 1f / getRebufferRate();
}
}
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.analytics;
import android.os.SystemClock;
import androidx.annotation.Nullable;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.analytics.PlaybackStats.PlaybackState;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData;
import com.google.android.exoplayer2.util.Assertions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* {@link AnalyticsListener} to gather {@link PlaybackStats} from the player.
*
* <p>For accurate measurements, the listener should be added to the player before loading media,
* i.e., {@link Player#getPlaybackState()} should be {@link Player#STATE_IDLE}.
*
* <p>Playback stats are gathered separately for all playback session, i.e. each window in the
* {@link Timeline} and each single ad.
*/
public final class PlaybackStatsListener
implements AnalyticsListener, PlaybackSessionManager.Listener {
/** A listener for {@link PlaybackStats} updates. */
public interface Callback {
/**
* Called when a playback session ends and its {@link PlaybackStats} are ready.
*
* @param eventTime The {@link EventTime} at which the playback session started. Can be used to
* identify the playback session.
* @param playbackStats The {@link PlaybackStats} for the ended playback session.
*/
void onPlaybackStatsReady(EventTime eventTime, PlaybackStats playbackStats);
}
private final PlaybackSessionManager sessionManager;
private final Map<String, PlaybackStatsTracker> playbackStatsTrackers;
private final Map<String, EventTime> sessionStartEventTimes;
@Nullable private final Callback callback;
private final boolean keepHistory;
private final Period period;
private PlaybackStats finishedPlaybackStats;
@Nullable private String activeContentPlayback;
@Nullable private String activeAdPlayback;
private boolean playWhenReady;
@Player.State private int playbackState;
/**
* Creates listener for playback stats.
*
* @param keepHistory Whether the reported {@link PlaybackStats} should keep the full history of
* events.
* @param callback An optional callback for finished {@link PlaybackStats}.
*/
public PlaybackStatsListener(boolean keepHistory, @Nullable Callback callback) {
this.callback = callback;
this.keepHistory = keepHistory;
sessionManager = new DefaultPlaybackSessionManager();
playbackStatsTrackers = new HashMap<>();
sessionStartEventTimes = new HashMap<>();
finishedPlaybackStats = PlaybackStats.EMPTY;
playWhenReady = false;
playbackState = Player.STATE_IDLE;
period = new Period();
sessionManager.setListener(this);
}
/**
* Returns the combined {@link PlaybackStats} for all playback sessions this listener was and is
* listening to.
*
* <p>Note that these {@link PlaybackStats} will not contain the full history of events.
*
* @return The combined {@link PlaybackStats} for all playback sessions.
*/
public PlaybackStats getCombinedPlaybackStats() {
PlaybackStats[] allPendingPlaybackStats = new PlaybackStats[playbackStatsTrackers.size() + 1];
allPendingPlaybackStats[0] = finishedPlaybackStats;
int index = 1;
for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) {
allPendingPlaybackStats[index++] = tracker.build(/* isFinal= */ false);
}
return PlaybackStats.merge(allPendingPlaybackStats);
}
/**
* Returns the {@link PlaybackStats} for the currently playback session, or null if no session is
* active.
*
* @return {@link PlaybackStats} for the current playback session.
*/
@Nullable
public PlaybackStats getPlaybackStats() {
PlaybackStatsTracker activeStatsTracker =
activeAdPlayback != null
? playbackStatsTrackers.get(activeAdPlayback)
: activeContentPlayback != null
? playbackStatsTrackers.get(activeContentPlayback)
: null;
return activeStatsTracker == null ? null : activeStatsTracker.build(/* isFinal= */ false);
}
/**
* Finishes all pending playback sessions. Should be called when the listener is removed from the
* player or when the player is released.
*/
public void finishAllSessions() {
// TODO: Add AnalyticsListener.onAttachedToPlayer and onDetachedFromPlayer to auto-release with
// an actual EventTime. Should also simplify other cases where the listener needs to be released
// separately from the player.
HashMap<String, PlaybackStatsTracker> trackerCopy = new HashMap<>(playbackStatsTrackers);
EventTime dummyEventTime =
new EventTime(
SystemClock.elapsedRealtime(),
Timeline.EMPTY,
/* windowIndex= */ 0,
/* mediaPeriodId= */ null,
/* eventPlaybackPositionMs= */ 0,
/* currentPlaybackPositionMs= */ 0,
/* totalBufferedDurationMs= */ 0);
for (String session : trackerCopy.keySet()) {
onSessionFinished(dummyEventTime, session, /* automaticTransition= */ false);
}
}
// PlaybackSessionManager.Listener implementation.
@Override
public void onSessionCreated(EventTime eventTime, String session) {
PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime);
tracker.onPlayerStateChanged(
eventTime, playWhenReady, playbackState, /* belongsToPlayback= */ true);
playbackStatsTrackers.put(session, tracker);
sessionStartEventTimes.put(session, eventTime);
}
@Override
public void onSessionActive(EventTime eventTime, String session) {
Assertions.checkNotNull(playbackStatsTrackers.get(session)).onForeground(eventTime);
if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) {
activeAdPlayback = session;
} else {
activeContentPlayback = session;
}
}
@Override
public void onAdPlaybackStarted(EventTime eventTime, String contentSession, String adSession) {
Assertions.checkState(Assertions.checkNotNull(eventTime.mediaPeriodId).isAd());
long contentPositionUs =
eventTime
.timeline
.getPeriodByUid(eventTime.mediaPeriodId.periodUid, period)
.getAdGroupTimeUs(eventTime.mediaPeriodId.adGroupIndex);
EventTime contentEventTime =
new EventTime(
eventTime.realtimeMs,
eventTime.timeline,
eventTime.windowIndex,
new MediaPeriodId(
eventTime.mediaPeriodId.periodUid,
eventTime.mediaPeriodId.windowSequenceNumber,
eventTime.mediaPeriodId.adGroupIndex),
/* eventPlaybackPositionMs= */ C.usToMs(contentPositionUs),
eventTime.currentPlaybackPositionMs,
eventTime.totalBufferedDurationMs);
Assertions.checkNotNull(playbackStatsTrackers.get(contentSession))
.onSuspended(contentEventTime, /* belongsToPlayback= */ true);
}
@Override
public void onSessionFinished(EventTime eventTime, String session, boolean automaticTransition) {
if (session.equals(activeAdPlayback)) {
activeAdPlayback = null;
} else if (session.equals(activeContentPlayback)) {
activeContentPlayback = null;
}
PlaybackStatsTracker tracker = Assertions.checkNotNull(playbackStatsTrackers.remove(session));
EventTime startEventTime = Assertions.checkNotNull(sessionStartEventTimes.remove(session));
if (automaticTransition) {
// Simulate ENDED state to record natural ending of playback.
tracker.onPlayerStateChanged(
eventTime, /* playWhenReady= */ true, Player.STATE_ENDED, /* belongsToPlayback= */ false);
}
tracker.onSuspended(eventTime, /* belongsToPlayback= */ false);
PlaybackStats playbackStats = tracker.build(/* isFinal= */ true);
finishedPlaybackStats = PlaybackStats.merge(finishedPlaybackStats, playbackStats);
if (callback != null) {
callback.onPlaybackStatsReady(startEventTime, playbackStats);
}
}
// AnalyticsListener implementation.
@Override
public void onPlayerStateChanged(
EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) {
this.playWhenReady = playWhenReady;
this.playbackState = playbackState;
sessionManager.updateSessions(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
playbackStatsTrackers
.get(session)
.onPlayerStateChanged(eventTime, playWhenReady, playbackState, belongsToPlayback);
}
}
@Override
public void onTimelineChanged(EventTime eventTime, int reason) {
sessionManager.handleTimelineUpdate(eventTime);
sessionManager.updateSessions(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime);
}
}
}
@Override
public void onPositionDiscontinuity(EventTime eventTime, int reason) {
sessionManager.handlePositionDiscontinuity(eventTime, reason);
sessionManager.updateSessions(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime);
}
}
}
@Override
public void onSeekStarted(EventTime eventTime) {
sessionManager.updateSessions(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onSeekStarted(eventTime);
}
}
}
@Override
public void onSeekProcessed(EventTime eventTime) {
sessionManager.updateSessions(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onSeekProcessed(eventTime);
}
}
}
@Override
public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {
sessionManager.updateSessions(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onFatalError(eventTime, error);
}
}
}
@Override
public void onLoadStarted(
EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
sessionManager.updateSessions(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onLoadStarted(eventTime);
}
}
}
/** Tracker for playback stats of a single playback. */
private static final class PlaybackStatsTracker {
// Final stats.
private final boolean keepHistory;
private final long[] playbackStateDurationsMs;
private final List<Pair<EventTime, @PlaybackState Integer>> playbackStateHistory;
private final boolean isAd;
private long firstReportedTimeMs;
private boolean hasBeenReady;
private boolean hasEnded;
private boolean isJoinTimeInvalid;
private int pauseCount;
private int pauseBufferCount;
private int seekCount;
private int rebufferCount;
private long maxRebufferTimeMs;
// Current player state tracking.
@PlaybackState private int currentPlaybackState;
private long currentPlaybackStateStartTimeMs;
private boolean isSeeking;
private boolean isForeground;
private boolean isSuspended;
private boolean playWhenReady;
@Player.State private int playerPlaybackState;
private boolean hasFatalError;
private boolean startedLoading;
private long lastRebufferStartTimeMs;
/**
* Creates a tracker for playback stats.
*
* @param keepHistory Whether to keep a full history of events.
* @param startTime The {@link EventTime} at which the playback stats start.
*/
public PlaybackStatsTracker(boolean keepHistory, EventTime startTime) {
this.keepHistory = keepHistory;
playbackStateDurationsMs = new long[PlaybackStats.PLAYBACK_STATE_COUNT];
playbackStateHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
currentPlaybackState = PlaybackStats.PLAYBACK_STATE_NOT_STARTED;
currentPlaybackStateStartTimeMs = startTime.realtimeMs;
playerPlaybackState = Player.STATE_IDLE;
firstReportedTimeMs = C.TIME_UNSET;
maxRebufferTimeMs = C.TIME_UNSET;
isAd = startTime.mediaPeriodId != null && startTime.mediaPeriodId.isAd();
}
/**
* Notifies the tracker of a player state change event, including all player state changes while
* the playback is not in the foreground.
*
* @param eventTime The {@link EventTime}.
* @param playWhenReady Whether the playback will proceed when ready.
* @param playbackState The current {@link Player.State}.
* @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback.
*/
public void onPlayerStateChanged(
EventTime eventTime,
boolean playWhenReady,
@Player.State int playbackState,
boolean belongsToPlayback) {
this.playWhenReady = playWhenReady;
playerPlaybackState = playbackState;
if (playbackState != Player.STATE_IDLE) {
hasFatalError = false;
}
if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED) {
isSuspended = false;
}
maybeUpdatePlaybackState(eventTime, belongsToPlayback);
}
/**
* Notifies the tracker of a position discontinuity or timeline update for the current playback.
*
* @param eventTime The {@link EventTime}.
*/
public void onPositionDiscontinuity(EventTime eventTime) {
isSuspended = false;
maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
}
/**
* Notifies the tracker of the start of a seek in the current playback.
*
* @param eventTime The {@link EventTime}.
*/
public void onSeekStarted(EventTime eventTime) {
isSeeking = true;
maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
}
/**
* Notifies the tracker of a seek has been processed in the current playback.
*
* @param eventTime The {@link EventTime}.
*/
public void onSeekProcessed(EventTime eventTime) {
isSeeking = false;
maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
}
/**
* Notifies the tracker of fatal player error in the current playback.
*
* @param eventTime The {@link EventTime}.
*/
public void onFatalError(EventTime eventTime, Exception error) {
hasFatalError = true;
isSuspended = false;
isSeeking = false;
maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
}
/**
* Notifies the tracker that a load for the current playback has started.
*
* @param eventTime The {@link EventTime}.
*/
public void onLoadStarted(EventTime eventTime) {
startedLoading = true;
maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
}
/**
* Notifies the tracker that the current playback became the active foreground playback.
*
* @param eventTime The {@link EventTime}.
*/
public void onForeground(EventTime eventTime) {
isForeground = true;
maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
}
/**
* Notifies the tracker that the current playback has been suspended, e.g. for ad playback or
* permanently.
*
* @param eventTime The {@link EventTime}.
* @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback.
*/
public void onSuspended(EventTime eventTime, boolean belongsToPlayback) {
isSuspended = true;
isSeeking = false;
maybeUpdatePlaybackState(eventTime, belongsToPlayback);
}
/**
* Builds the playback stats.
*
* @param isFinal Whether this is the final build and no further events are expected.
*/
public PlaybackStats build(boolean isFinal) {
long[] playbackStateDurationsMs = this.playbackStateDurationsMs;
if (!isFinal) {
long buildTimeMs = SystemClock.elapsedRealtime();
playbackStateDurationsMs =
Arrays.copyOf(this.playbackStateDurationsMs, PlaybackStats.PLAYBACK_STATE_COUNT);
long lastStateDurationMs = Math.max(0, buildTimeMs - currentPlaybackStateStartTimeMs);
playbackStateDurationsMs[currentPlaybackState] += lastStateDurationMs;
maybeUpdateMaxRebufferTimeMs(buildTimeMs);
}
boolean isJoinTimeInvalid = this.isJoinTimeInvalid || !hasBeenReady;
long validJoinTimeMs =
isJoinTimeInvalid
? C.TIME_UNSET
: playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND];
boolean hasBackgroundJoin =
playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND] > 0;
return new PlaybackStats(
/* playbackCount= */ 1,
playbackStateDurationsMs,
isFinal ? playbackStateHistory : new ArrayList<>(playbackStateHistory),
firstReportedTimeMs,
/* foregroundPlaybackCount= */ isForeground ? 1 : 0,
/* abandonedBeforeReadyCount= */ hasBeenReady ? 0 : 1,
/* endedCount= */ hasEnded ? 1 : 0,
/* backgroundJoiningCount= */ hasBackgroundJoin ? 1 : 0,
validJoinTimeMs,
/* validJoinTimeCount= */ isJoinTimeInvalid ? 0 : 1,
pauseCount,
pauseBufferCount,
seekCount,
rebufferCount,
maxRebufferTimeMs,
/* adPlaybackCount= */ isAd ? 1 : 0);
}
private void maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlayback) {
@PlaybackState int newPlaybackState = resolveNewPlaybackState();
if (newPlaybackState == currentPlaybackState) {
return;
}
Assertions.checkArgument(eventTime.realtimeMs >= currentPlaybackStateStartTimeMs);
long stateDurationMs = eventTime.realtimeMs - currentPlaybackStateStartTimeMs;
playbackStateDurationsMs[currentPlaybackState] += stateDurationMs;
if (firstReportedTimeMs == C.TIME_UNSET) {
firstReportedTimeMs = eventTime.realtimeMs;
}
isJoinTimeInvalid |= isInvalidJoinTransition(currentPlaybackState, newPlaybackState);
hasBeenReady |= isReadyState(newPlaybackState);
hasEnded |= newPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED;
if (!isPausedState(currentPlaybackState) && isPausedState(newPlaybackState)) {
pauseCount++;
}
if (newPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING) {
seekCount++;
}
if (!isRebufferingState(currentPlaybackState) && isRebufferingState(newPlaybackState)) {
rebufferCount++;
lastRebufferStartTimeMs = eventTime.realtimeMs;
}
if (newPlaybackState == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING
&& currentPlaybackState == PlaybackStats.PLAYBACK_STATE_BUFFERING) {
pauseBufferCount++;
}
maybeUpdateMaxRebufferTimeMs(eventTime.realtimeMs);
currentPlaybackState = newPlaybackState;
currentPlaybackStateStartTimeMs = eventTime.realtimeMs;
if (keepHistory) {
playbackStateHistory.add(Pair.create(eventTime, currentPlaybackState));
}
}
@PlaybackState
private int resolveNewPlaybackState() {
if (isSuspended) {
// Keep VIDEO_STATE_ENDED if playback naturally ended (or progressed to next item).
return currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED
? PlaybackStats.PLAYBACK_STATE_ENDED
: PlaybackStats.PLAYBACK_STATE_SUSPENDED;
} else if (isSeeking) {
// Seeking takes precedence over errors such that we report a seek while in error state.
return PlaybackStats.PLAYBACK_STATE_SEEKING;
} else if (hasFatalError) {
return PlaybackStats.PLAYBACK_STATE_FAILED;
} else if (!isForeground) {
// Before the playback becomes foreground, only report background joining and not started.
return startedLoading
? PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND
: PlaybackStats.PLAYBACK_STATE_NOT_STARTED;
} else if (playerPlaybackState == Player.STATE_ENDED) {
return PlaybackStats.PLAYBACK_STATE_ENDED;
} else if (playerPlaybackState == Player.STATE_BUFFERING) {
if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_NOT_STARTED
|| currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND
|| currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND
|| currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SUSPENDED) {
return PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND;
}
if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING
|| currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING) {
return PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING;
}
return playWhenReady
? PlaybackStats.PLAYBACK_STATE_BUFFERING
: PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING;
} else if (playerPlaybackState == Player.STATE_READY) {
return playWhenReady
? PlaybackStats.PLAYBACK_STATE_PLAYING
: PlaybackStats.PLAYBACK_STATE_PAUSED;
} else if (playerPlaybackState == Player.STATE_IDLE
&& currentPlaybackState != PlaybackStats.PLAYBACK_STATE_NOT_STARTED) {
// This case only applies for calls to player.stop(). All other IDLE cases are handled by
// !isForeground, hasFatalError or isSuspended. NOT_STARTED is deliberately ignored.
return PlaybackStats.PLAYBACK_STATE_STOPPED;
}
return currentPlaybackState;
}
private void maybeUpdateMaxRebufferTimeMs(long nowMs) {
if (isRebufferingState(currentPlaybackState)) {
long rebufferDurationMs = nowMs - lastRebufferStartTimeMs;
if (maxRebufferTimeMs == C.TIME_UNSET || rebufferDurationMs > maxRebufferTimeMs) {
maxRebufferTimeMs = rebufferDurationMs;
}
}
}
private static boolean isReadyState(@PlaybackState int state) {
return state == PlaybackStats.PLAYBACK_STATE_PLAYING
|| state == PlaybackStats.PLAYBACK_STATE_PAUSED;
}
private static boolean isPausedState(@PlaybackState int state) {
return state == PlaybackStats.PLAYBACK_STATE_PAUSED
|| state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING;
}
private static boolean isRebufferingState(@PlaybackState int state) {
return state == PlaybackStats.PLAYBACK_STATE_BUFFERING
|| state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING;
}
private static boolean isInvalidJoinTransition(
@PlaybackState int oldState, @PlaybackState int newState) {
if (oldState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND
&& oldState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND
&& oldState != PlaybackStats.PLAYBACK_STATE_SUSPENDED) {
return false;
}
return newState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND
&& newState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND
&& newState != PlaybackStats.PLAYBACK_STATE_SUSPENDED
&& newState != PlaybackStats.PLAYBACK_STATE_PLAYING
&& newState != PlaybackStats.PLAYBACK_STATE_PAUSED
&& newState != PlaybackStats.PLAYBACK_STATE_ENDED;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment