Add remaining PlaybackStatsListener metrics.

This adds all the non-playback-state metrics, like format, error, bandwidth and
renderer performance metrics.

PiperOrigin-RevId: 250668854
This commit is contained in:
tonihei 2019-05-30 13:02:01 +01:00 committed by Toni
parent 77595da159
commit fd1179aaa1
4 changed files with 842 additions and 17 deletions

View file

@ -2,6 +2,8 @@
### dev-v2 (not yet released) ###
* Add `PlaybackStatsListener` to collect `PlaybackStats` for playbacks analysis
and analytics reporting (TODO: link to developer guide page/blog post).
* Add basic DRM support to the Cast demo app.
* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s
([#5779](https://github.com/google/ExoPlayer/issues/5779)).

View file

@ -1373,6 +1373,38 @@ public final class Format implements Parcelable {
accessibilityChannel);
}
public Format copyWithVideoSize(int width, int height) {
return new Format(
id,
label,
selectionFlags,
roleFlags,
bitrate,
codecs,
metadata,
containerMimeType,
sampleMimeType,
maxInputSize,
initializationData,
drmInitData,
subsampleOffsetUs,
width,
height,
frameRate,
rotationDegrees,
pixelWidthHeightRatio,
projectionData,
stereoMode,
colorInfo,
channelCount,
sampleRate,
pcmEncoding,
encoderDelay,
encoderPadding,
language,
accessibilityChannel);
}
/**
* Returns the number of pixels if this is a video format whose {@link #width} and {@link #height}
* are known, or {@link #NO_VALUE} otherwise

View file

@ -19,6 +19,7 @@ import android.os.SystemClock;
import androidx.annotation.IntDef;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
@ -27,6 +28,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Collections;
import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Statistics about playbacks. */
public final class PlaybackStats {
@ -109,12 +111,31 @@ public final class PlaybackStats {
int backgroundJoiningCount = 0;
long totalValidJoinTimeMs = C.TIME_UNSET;
int validJoinTimeCount = 0;
int pauseCount = 0;
int pauseBufferCount = 0;
int seekCount = 0;
int rebufferCount = 0;
int totalPauseCount = 0;
int totalPauseBufferCount = 0;
int totalSeekCount = 0;
int totalRebufferCount = 0;
long maxRebufferTimeMs = C.TIME_UNSET;
int adCount = 0;
int adPlaybackCount = 0;
long totalVideoFormatHeightTimeMs = 0;
long totalVideoFormatHeightTimeProduct = 0;
long totalVideoFormatBitrateTimeMs = 0;
long totalVideoFormatBitrateTimeProduct = 0;
long totalAudioFormatTimeMs = 0;
long totalAudioFormatBitrateTimeProduct = 0;
int initialVideoFormatHeightCount = 0;
int initialVideoFormatBitrateCount = 0;
int totalInitialVideoFormatHeight = C.LENGTH_UNSET;
long totalInitialVideoFormatBitrate = C.LENGTH_UNSET;
int initialAudioFormatBitrateCount = 0;
long totalInitialAudioFormatBitrate = C.LENGTH_UNSET;
long totalBandwidthTimeMs = 0;
long totalBandwidthBytes = 0;
long totalDroppedFrames = 0;
long totalAudioUnderruns = 0;
int fatalErrorPlaybackCount = 0;
int fatalErrorCount = 0;
int nonFatalErrorCount = 0;
for (PlaybackStats stats : playbackStats) {
playbackCount += stats.playbackCount;
for (int i = 0; i < PLAYBACK_STATE_COUNT; i++) {
@ -135,21 +156,53 @@ public final class PlaybackStats {
totalValidJoinTimeMs += stats.totalValidJoinTimeMs;
}
validJoinTimeCount += stats.validJoinTimeCount;
pauseCount += stats.totalPauseCount;
pauseBufferCount += stats.totalPauseBufferCount;
seekCount += stats.totalSeekCount;
rebufferCount += stats.totalRebufferCount;
totalPauseCount += stats.totalPauseCount;
totalPauseBufferCount += stats.totalPauseBufferCount;
totalSeekCount += stats.totalSeekCount;
totalRebufferCount += 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;
adPlaybackCount += stats.adPlaybackCount;
totalVideoFormatHeightTimeMs += stats.totalVideoFormatHeightTimeMs;
totalVideoFormatHeightTimeProduct += stats.totalVideoFormatHeightTimeProduct;
totalVideoFormatBitrateTimeMs += stats.totalVideoFormatBitrateTimeMs;
totalVideoFormatBitrateTimeProduct += stats.totalVideoFormatBitrateTimeProduct;
totalAudioFormatTimeMs += stats.totalAudioFormatTimeMs;
totalAudioFormatBitrateTimeProduct += stats.totalAudioFormatBitrateTimeProduct;
initialVideoFormatHeightCount += stats.initialVideoFormatHeightCount;
initialVideoFormatBitrateCount += stats.initialVideoFormatBitrateCount;
if (totalInitialVideoFormatHeight == C.LENGTH_UNSET) {
totalInitialVideoFormatHeight = stats.totalInitialVideoFormatHeight;
} else if (stats.totalInitialVideoFormatHeight != C.LENGTH_UNSET) {
totalInitialVideoFormatHeight += stats.totalInitialVideoFormatHeight;
}
if (totalInitialVideoFormatBitrate == C.LENGTH_UNSET) {
totalInitialVideoFormatBitrate = stats.totalInitialVideoFormatBitrate;
} else if (stats.totalInitialVideoFormatBitrate != C.LENGTH_UNSET) {
totalInitialVideoFormatBitrate += stats.totalInitialVideoFormatBitrate;
}
initialAudioFormatBitrateCount += stats.initialAudioFormatBitrateCount;
if (totalInitialAudioFormatBitrate == C.LENGTH_UNSET) {
totalInitialAudioFormatBitrate = stats.totalInitialAudioFormatBitrate;
} else if (stats.totalInitialAudioFormatBitrate != C.LENGTH_UNSET) {
totalInitialAudioFormatBitrate += stats.totalInitialAudioFormatBitrate;
}
totalBandwidthTimeMs += stats.totalBandwidthTimeMs;
totalBandwidthBytes += stats.totalBandwidthBytes;
totalDroppedFrames += stats.totalDroppedFrames;
totalAudioUnderruns += stats.totalAudioUnderruns;
fatalErrorPlaybackCount += stats.fatalErrorPlaybackCount;
fatalErrorCount += stats.fatalErrorCount;
nonFatalErrorCount += stats.nonFatalErrorCount;
}
return new PlaybackStats(
playbackCount,
playbackStateDurationsMs,
/* playbackStateHistory */ Collections.emptyList(),
/* mediaTimeHistory= */ Collections.emptyList(),
firstReportedTimeMs,
foregroundPlaybackCount,
abandonedBeforeReadyCount,
@ -157,12 +210,35 @@ public final class PlaybackStats {
backgroundJoiningCount,
totalValidJoinTimeMs,
validJoinTimeCount,
pauseCount,
pauseBufferCount,
seekCount,
rebufferCount,
totalPauseCount,
totalPauseBufferCount,
totalSeekCount,
totalRebufferCount,
maxRebufferTimeMs,
adCount);
adPlaybackCount,
/* videoFormatHistory= */ Collections.emptyList(),
/* audioFormatHistory= */ Collections.emptyList(),
totalVideoFormatHeightTimeMs,
totalVideoFormatHeightTimeProduct,
totalVideoFormatBitrateTimeMs,
totalVideoFormatBitrateTimeProduct,
totalAudioFormatTimeMs,
totalAudioFormatBitrateTimeProduct,
initialVideoFormatHeightCount,
initialVideoFormatBitrateCount,
totalInitialVideoFormatHeight,
totalInitialVideoFormatBitrate,
initialAudioFormatBitrateCount,
totalInitialAudioFormatBitrate,
totalBandwidthTimeMs,
totalBandwidthBytes,
totalDroppedFrames,
totalAudioUnderruns,
fatalErrorPlaybackCount,
fatalErrorCount,
nonFatalErrorCount,
/* fatalErrorHistory= */ Collections.emptyList(),
/* nonFatalErrorHistory= */ Collections.emptyList());
}
/** The number of individual playbacks for which these stats were collected. */
@ -175,6 +251,12 @@ public final class PlaybackStats {
* active and the {@link PlaybackState}.
*/
public final List<Pair<EventTime, @PlaybackState Integer>> playbackStateHistory;
/**
* The media time history as an ordered list of long[2] arrays with [0] being the realtime as
* returned by {@code SystemClock.elapsedRealtime()} and [1] being the media time at this
* realtime, in milliseconds.
*/
public final List<long[]> mediaTimeHistory;
/**
* 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.
@ -223,12 +305,108 @@ public final class PlaybackStats {
/** The number of ad playbacks. */
public final int adPlaybackCount;
// Format stats.
/**
* The video format history as ordered pairs of the {@link EventTime} at which a format started
* being used and the {@link Format}. The {@link Format} may be null if no video format was used.
*/
public final List<Pair<EventTime, @NullableType Format>> videoFormatHistory;
/**
* The audio format history as ordered pairs of the {@link EventTime} at which a format started
* being used and the {@link Format}. The {@link Format} may be null if no audio format was used.
*/
public final List<Pair<EventTime, @NullableType Format>> audioFormatHistory;
/** The total media time for which video format height data is available, in milliseconds. */
public final long totalVideoFormatHeightTimeMs;
/**
* The accumulated sum of all video format heights, in pixels, times the time the format was used
* for playback, in milliseconds.
*/
public final long totalVideoFormatHeightTimeProduct;
/** The total media time for which video format bitrate data is available, in milliseconds. */
public final long totalVideoFormatBitrateTimeMs;
/**
* The accumulated sum of all video format bitrates, in bits per second, times the time the format
* was used for playback, in milliseconds.
*/
public final long totalVideoFormatBitrateTimeProduct;
/** The total media time for which audio format data is available, in milliseconds. */
public final long totalAudioFormatTimeMs;
/**
* The accumulated sum of all audio format bitrates, in bits per second, times the time the format
* was used for playback, in milliseconds.
*/
public final long totalAudioFormatBitrateTimeProduct;
/** The number of playbacks with initial video format height data. */
public final int initialVideoFormatHeightCount;
/** The number of playbacks with initial video format bitrate data. */
public final int initialVideoFormatBitrateCount;
/**
* The total initial video format height for all playbacks, in pixels, or {@link C#LENGTH_UNSET}
* if no initial video format data is available.
*/
public final int totalInitialVideoFormatHeight;
/**
* The total initial video format bitrate for all playbacks, in bits per second, or {@link
* C#LENGTH_UNSET} if no initial video format data is available.
*/
public final long totalInitialVideoFormatBitrate;
/** The number of playbacks with initial audio format bitrate data. */
public final int initialAudioFormatBitrateCount;
/**
* The total initial audio format bitrate for all playbacks, in bits per second, or {@link
* C#LENGTH_UNSET} if no initial audio format data is available.
*/
public final long totalInitialAudioFormatBitrate;
// Bandwidth stats.
/** The total time for which bandwidth measurement data is available, in milliseconds. */
public final long totalBandwidthTimeMs;
/** The total bytes transferred during {@link #totalBandwidthTimeMs}. */
public final long totalBandwidthBytes;
// Renderer quality stats.
/** The total number of dropped video frames. */
public final long totalDroppedFrames;
/** The total number of audio underruns. */
public final long totalAudioUnderruns;
// Error stats.
/**
* The total number of playback with at least one fatal error. Errors are fatal if playback
* stopped due to this error.
*/
public final int fatalErrorPlaybackCount;
/** The total number of fatal errors. Errors are fatal if playback stopped due to this error. */
public final int fatalErrorCount;
/**
* The total number of non-fatal errors. Error are non-fatal if playback can recover from the
* error without stopping.
*/
public final int nonFatalErrorCount;
/**
* The history of fatal errors as ordered pairs of the {@link EventTime} at which an error
* occurred and the error. Errors are fatal if playback stopped due to this error.
*/
public final List<Pair<EventTime, Exception>> fatalErrorHistory;
/**
* The history of non-fatal errors as ordered pairs of the {@link EventTime} at which an error
* occurred and the error. Error are non-fatal if playback can recover from the error without
* stopping.
*/
public final List<Pair<EventTime, Exception>> nonFatalErrorHistory;
private final long[] playbackStateDurationsMs;
/* package */ PlaybackStats(
int playbackCount,
long[] playbackStateDurationsMs,
List<Pair<EventTime, @PlaybackState Integer>> playbackStateHistory,
List<long[]> mediaTimeHistory,
long firstReportedTimeMs,
int foregroundPlaybackCount,
int abandonedBeforeReadyCount,
@ -241,10 +419,34 @@ public final class PlaybackStats {
int totalSeekCount,
int totalRebufferCount,
long maxRebufferTimeMs,
int adPlaybackCount) {
int adPlaybackCount,
List<Pair<EventTime, @NullableType Format>> videoFormatHistory,
List<Pair<EventTime, @NullableType Format>> audioFormatHistory,
long totalVideoFormatHeightTimeMs,
long totalVideoFormatHeightTimeProduct,
long totalVideoFormatBitrateTimeMs,
long totalVideoFormatBitrateTimeProduct,
long totalAudioFormatTimeMs,
long totalAudioFormatBitrateTimeProduct,
int initialVideoFormatHeightCount,
int initialVideoFormatBitrateCount,
int totalInitialVideoFormatHeight,
long totalInitialVideoFormatBitrate,
int initialAudioFormatBitrateCount,
long totalInitialAudioFormatBitrate,
long totalBandwidthTimeMs,
long totalBandwidthBytes,
long totalDroppedFrames,
long totalAudioUnderruns,
int fatalErrorPlaybackCount,
int fatalErrorCount,
int nonFatalErrorCount,
List<Pair<EventTime, Exception>> fatalErrorHistory,
List<Pair<EventTime, Exception>> nonFatalErrorHistory) {
this.playbackCount = playbackCount;
this.playbackStateDurationsMs = playbackStateDurationsMs;
this.playbackStateHistory = Collections.unmodifiableList(playbackStateHistory);
this.mediaTimeHistory = Collections.unmodifiableList(mediaTimeHistory);
this.firstReportedTimeMs = firstReportedTimeMs;
this.foregroundPlaybackCount = foregroundPlaybackCount;
this.abandonedBeforeReadyCount = abandonedBeforeReadyCount;
@ -258,6 +460,29 @@ public final class PlaybackStats {
this.totalRebufferCount = totalRebufferCount;
this.maxRebufferTimeMs = maxRebufferTimeMs;
this.adPlaybackCount = adPlaybackCount;
this.videoFormatHistory = Collections.unmodifiableList(videoFormatHistory);
this.audioFormatHistory = Collections.unmodifiableList(audioFormatHistory);
this.totalVideoFormatHeightTimeMs = totalVideoFormatHeightTimeMs;
this.totalVideoFormatHeightTimeProduct = totalVideoFormatHeightTimeProduct;
this.totalVideoFormatBitrateTimeMs = totalVideoFormatBitrateTimeMs;
this.totalVideoFormatBitrateTimeProduct = totalVideoFormatBitrateTimeProduct;
this.totalAudioFormatTimeMs = totalAudioFormatTimeMs;
this.totalAudioFormatBitrateTimeProduct = totalAudioFormatBitrateTimeProduct;
this.initialVideoFormatHeightCount = initialVideoFormatHeightCount;
this.initialVideoFormatBitrateCount = initialVideoFormatBitrateCount;
this.totalInitialVideoFormatHeight = totalInitialVideoFormatHeight;
this.totalInitialVideoFormatBitrate = totalInitialVideoFormatBitrate;
this.initialAudioFormatBitrateCount = initialAudioFormatBitrateCount;
this.totalInitialAudioFormatBitrate = totalInitialAudioFormatBitrate;
this.totalBandwidthTimeMs = totalBandwidthTimeMs;
this.totalBandwidthBytes = totalBandwidthBytes;
this.totalDroppedFrames = totalDroppedFrames;
this.totalAudioUnderruns = totalAudioUnderruns;
this.fatalErrorPlaybackCount = fatalErrorPlaybackCount;
this.fatalErrorCount = fatalErrorCount;
this.nonFatalErrorCount = nonFatalErrorCount;
this.fatalErrorHistory = Collections.unmodifiableList(fatalErrorHistory);
this.nonFatalErrorHistory = Collections.unmodifiableList(nonFatalErrorHistory);
}
/**
@ -289,6 +514,41 @@ public final class PlaybackStats {
return state;
}
/**
* Returns the estimated media time at the given realtime, in milliseconds, or {@link
* C#TIME_UNSET} if the media time history is unknown.
*
* @param realtimeMs The realtime as returned by {@link SystemClock#elapsedRealtime()}.
* @return The estimated media time in milliseconds at this realtime, {@link C#TIME_UNSET} if no
* estimate can be given.
*/
public long getMediaTimeMsAtRealtimeMs(long realtimeMs) {
if (mediaTimeHistory.isEmpty()) {
return C.TIME_UNSET;
}
int nextIndex = 0;
while (nextIndex < mediaTimeHistory.size()
&& mediaTimeHistory.get(nextIndex)[0] <= realtimeMs) {
nextIndex++;
}
if (nextIndex == 0) {
return mediaTimeHistory.get(0)[1];
}
if (nextIndex == mediaTimeHistory.size()) {
return mediaTimeHistory.get(mediaTimeHistory.size() - 1)[1];
}
long prevRealtimeMs = mediaTimeHistory.get(nextIndex - 1)[0];
long prevMediaTimeMs = mediaTimeHistory.get(nextIndex - 1)[1];
long nextRealtimeMs = mediaTimeHistory.get(nextIndex)[0];
long nextMediaTimeMs = mediaTimeHistory.get(nextIndex)[1];
long realtimeDurationMs = nextRealtimeMs - prevRealtimeMs;
if (realtimeDurationMs == 0) {
return prevMediaTimeMs;
}
float fraction = (float) (realtimeMs - prevRealtimeMs) / realtimeDurationMs;
return prevMediaTimeMs + (long) ((nextMediaTimeMs - prevMediaTimeMs) * fraction);
}
/**
* 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
@ -564,4 +824,147 @@ public final class PlaybackStats {
public float getMeanTimeBetweenRebuffers() {
return 1f / getRebufferRate();
}
/**
* Returns the mean initial video format height, in pixels, or {@link C#LENGTH_UNSET} if no video
* format data is available.
*/
public int getMeanInitialVideoFormatHeight() {
return initialVideoFormatHeightCount == 0
? C.LENGTH_UNSET
: totalInitialVideoFormatHeight / initialVideoFormatHeightCount;
}
/**
* Returns the mean initial video format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if
* no video format data is available.
*/
public int getMeanInitialVideoFormatBitrate() {
return initialVideoFormatBitrateCount == 0
? C.LENGTH_UNSET
: (int) (totalInitialVideoFormatBitrate / initialVideoFormatBitrateCount);
}
/**
* Returns the mean initial audio format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if
* no audio format data is available.
*/
public int getMeanInitialAudioFormatBitrate() {
return initialAudioFormatBitrateCount == 0
? C.LENGTH_UNSET
: (int) (totalInitialAudioFormatBitrate / initialAudioFormatBitrateCount);
}
/**
* Returns the mean video format height, in pixels, or {@link C#LENGTH_UNSET} if no video format
* data is available. This is a weighted average taking the time the format was used for playback
* into account.
*/
public int getMeanVideoFormatHeight() {
return totalVideoFormatHeightTimeMs == 0
? C.LENGTH_UNSET
: (int) (totalVideoFormatHeightTimeProduct / totalVideoFormatHeightTimeMs);
}
/**
* Returns the mean video format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if no
* video format data is available. This is a weighted average taking the time the format was used
* for playback into account.
*/
public int getMeanVideoFormatBitrate() {
return totalVideoFormatBitrateTimeMs == 0
? C.LENGTH_UNSET
: (int) (totalVideoFormatBitrateTimeProduct / totalVideoFormatBitrateTimeMs);
}
/**
* Returns the mean audio format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if no
* audio format data is available. This is a weighted average taking the time the format was used
* for playback into account.
*/
public int getMeanAudioFormatBitrate() {
return totalAudioFormatTimeMs == 0
? C.LENGTH_UNSET
: (int) (totalAudioFormatBitrateTimeProduct / totalAudioFormatTimeMs);
}
/**
* Returns the mean network bandwidth based on transfer measurements, in bits per second, or
* {@link C#LENGTH_UNSET} if no transfer data is available.
*/
public int getMeanBandwidth() {
return totalBandwidthTimeMs == 0
? C.LENGTH_UNSET
: (int) (totalBandwidthBytes * 8000 / totalBandwidthTimeMs);
}
/**
* Returns the mean rate at which video frames are dropped, in dropped frames per play time
* second, or {@code 0.0} if no time was spent playing.
*/
public float getDroppedFramesRate() {
long playTimeMs = getTotalPlayTimeMs();
return playTimeMs == 0 ? 0f : 1000f * totalDroppedFrames / playTimeMs;
}
/**
* Returns the mean rate at which audio underruns occurred, in underruns per play time second, or
* {@code 0.0} if no time was spent playing.
*/
public float getAudioUnderrunRate() {
long playTimeMs = getTotalPlayTimeMs();
return playTimeMs == 0 ? 0f : 1000f * totalAudioUnderruns / playTimeMs;
}
/**
* Returns the ratio of foreground playbacks which experienced fatal errors, or {@code 0.0} if no
* playback has been in foreground.
*/
public float getFatalErrorRatio() {
return foregroundPlaybackCount == 0
? 0f
: (float) fatalErrorPlaybackCount / foregroundPlaybackCount;
}
/**
* Returns the rate of fatal errors, in errors per play time second, or {@code 0.0} if no time was
* spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenFatalErrors()}.
*/
public float getFatalErrorRate() {
long playTimeMs = getTotalPlayTimeMs();
return playTimeMs == 0 ? 0f : 1000f * fatalErrorCount / playTimeMs;
}
/**
* Returns the mean play time between fatal errors, in seconds. This is equivalent to 1.0 / {@link
* #getFatalErrorRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}.
*/
public float getMeanTimeBetweenFatalErrors() {
return 1f / getFatalErrorRate();
}
/**
* Returns the mean number of non-fatal errors per foreground playback, or {@code 0.0} if no
* playback has been in foreground.
*/
public float getMeanNonFatalErrorCount() {
return foregroundPlaybackCount == 0 ? 0f : (float) nonFatalErrorCount / foregroundPlaybackCount;
}
/**
* Returns the rate of non-fatal errors, in errors per play time second, or {@code 0.0} if no time
* was spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenNonFatalErrors()}.
*/
public float getNonFatalErrorRate() {
long playTimeMs = getTotalPlayTimeMs();
return playTimeMs == 0 ? 0f : 1000f * nonFatalErrorCount / playTimeMs;
}
/**
* Returns the mean play time between non-fatal errors, in seconds. This is equivalent to 1.0 /
* {@link #getNonFatalErrorRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}.
*/
public float getMeanTimeBetweenNonFatalErrors() {
return 1f / getNonFatalErrorRate();
}
}

View file

@ -20,6 +20,8 @@ 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.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
@ -27,13 +29,20 @@ 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.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
* {@link AnalyticsListener} to gather {@link PlaybackStats} from the player.
@ -72,6 +81,7 @@ public final class PlaybackStatsListener
@Nullable private String activeAdPlayback;
private boolean playWhenReady;
@Player.State private int playbackState;
private float playbackSpeed;
/**
* Creates listener for playback stats.
@ -89,6 +99,7 @@ public final class PlaybackStatsListener
finishedPlaybackStats = PlaybackStats.EMPTY;
playWhenReady = false;
playbackState = Player.STATE_IDLE;
playbackSpeed = 1f;
period = new Period();
sessionManager.setListener(this);
}
@ -158,6 +169,7 @@ public final class PlaybackStatsListener
PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime);
tracker.onPlayerStateChanged(
eventTime, playWhenReady, playbackState, /* belongsToPlayback= */ true);
tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed);
playbackStatsTrackers.put(session, tracker);
sessionStartEventTimes.put(session, eventTime);
}
@ -286,6 +298,27 @@ public final class PlaybackStatsListener
}
}
@Override
public void onPlaybackParametersChanged(
EventTime eventTime, PlaybackParameters playbackParameters) {
playbackSpeed = playbackParameters.speed;
sessionManager.updateSessions(eventTime);
for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) {
tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed);
}
}
@Override
public void onTracksChanged(
EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
sessionManager.updateSessions(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onTracksChanged(eventTime, trackSelections);
}
}
}
@Override
public void onLoadStarted(
EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
@ -297,6 +330,88 @@ public final class PlaybackStatsListener
}
}
@Override
public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {
sessionManager.updateSessions(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onDownstreamFormatChanged(eventTime, mediaLoadData);
}
}
}
@Override
public void onVideoSizeChanged(
EventTime eventTime,
int width,
int height,
int unappliedRotationDegrees,
float pixelWidthHeightRatio) {
sessionManager.updateSessions(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onVideoSizeChanged(eventTime, width, height);
}
}
}
@Override
public void onBandwidthEstimate(
EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {
sessionManager.updateSessions(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onBandwidthData(totalLoadTimeMs, totalBytesLoaded);
}
}
}
@Override
public void onAudioUnderrun(
EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
sessionManager.updateSessions(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onAudioUnderrun();
}
}
}
@Override
public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {
sessionManager.updateSessions(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onDroppedVideoFrames(droppedFrames);
}
}
}
@Override
public void onLoadError(
EventTime eventTime,
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData,
IOException error,
boolean wasCanceled) {
sessionManager.updateSessions(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onNonFatalError(eventTime, error);
}
}
}
@Override
public void onDrmSessionManagerError(EventTime eventTime, Exception error) {
sessionManager.updateSessions(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onNonFatalError(eventTime, error);
}
}
}
/** Tracker for playback stats of a single playback. */
private static final class PlaybackStatsTracker {
@ -304,6 +419,11 @@ public final class PlaybackStatsListener
private final boolean keepHistory;
private final long[] playbackStateDurationsMs;
private final List<Pair<EventTime, @PlaybackState Integer>> playbackStateHistory;
private final List<long[]> mediaTimeHistory;
private final List<Pair<EventTime, @NullableType Format>> videoFormatHistory;
private final List<Pair<EventTime, @NullableType Format>> audioFormatHistory;
private final List<Pair<EventTime, Exception>> fatalErrorHistory;
private final List<Pair<EventTime, Exception>> nonFatalErrorHistory;
private final boolean isAd;
private long firstReportedTimeMs;
@ -315,6 +435,21 @@ public final class PlaybackStatsListener
private int seekCount;
private int rebufferCount;
private long maxRebufferTimeMs;
private int initialVideoFormatHeight;
private long initialVideoFormatBitrate;
private long initialAudioFormatBitrate;
private long videoFormatHeightTimeMs;
private long videoFormatHeightTimeProduct;
private long videoFormatBitrateTimeMs;
private long videoFormatBitrateTimeProduct;
private long audioFormatTimeMs;
private long audioFormatBitrateTimeProduct;
private long bandwidthTimeMs;
private long bandwidthBytes;
private long droppedFrames;
private long audioUnderruns;
private int fatalErrorCount;
private int nonFatalErrorCount;
// Current player state tracking.
@PlaybackState private int currentPlaybackState;
@ -327,6 +462,11 @@ public final class PlaybackStatsListener
private boolean hasFatalError;
private boolean startedLoading;
private long lastRebufferStartTimeMs;
@Nullable private Format currentVideoFormat;
@Nullable private Format currentAudioFormat;
private long lastVideoFormatStartTimeMs;
private long lastAudioFormatStartTimeMs;
private float currentPlaybackSpeed;
/**
* Creates a tracker for playback stats.
@ -338,12 +478,21 @@ public final class PlaybackStatsListener
this.keepHistory = keepHistory;
playbackStateDurationsMs = new long[PlaybackStats.PLAYBACK_STATE_COUNT];
playbackStateHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
mediaTimeHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
videoFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
audioFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
fatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
nonFatalErrorHistory = 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();
initialAudioFormatBitrate = C.LENGTH_UNSET;
initialVideoFormatBitrate = C.LENGTH_UNSET;
initialVideoFormatHeight = C.LENGTH_UNSET;
currentPlaybackSpeed = 1f;
}
/**
@ -407,6 +556,10 @@ public final class PlaybackStatsListener
* @param eventTime The {@link EventTime}.
*/
public void onFatalError(EventTime eventTime, Exception error) {
fatalErrorCount++;
if (keepHistory) {
fatalErrorHistory.add(Pair.create(eventTime, error));
}
hasFatalError = true;
isSuspended = false;
isSeeking = false;
@ -446,6 +599,115 @@ public final class PlaybackStatsListener
maybeUpdatePlaybackState(eventTime, belongsToPlayback);
}
/**
* Notifies the tracker that the track selection for the current playback changed.
*
* @param eventTime The {@link EventTime}.
* @param trackSelections The new {@link TrackSelectionArray}.
*/
public void onTracksChanged(EventTime eventTime, TrackSelectionArray trackSelections) {
boolean videoEnabled = false;
boolean audioEnabled = false;
for (TrackSelection trackSelection : trackSelections.getAll()) {
if (trackSelection != null && trackSelection.length() > 0) {
int trackType = MimeTypes.getTrackType(trackSelection.getFormat(0).sampleMimeType);
if (trackType == C.TRACK_TYPE_VIDEO) {
videoEnabled = true;
} else if (trackType == C.TRACK_TYPE_AUDIO) {
audioEnabled = true;
}
}
}
if (!videoEnabled) {
maybeUpdateVideoFormat(eventTime, /* newFormat= */ null);
}
if (!audioEnabled) {
maybeUpdateAudioFormat(eventTime, /* newFormat= */ null);
}
}
/**
* Notifies the tracker that a format being read by the renderers for the current playback
* changed.
*
* @param eventTime The {@link EventTime}.
* @param mediaLoadData The {@link MediaLoadData} describing the format change.
*/
public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {
if (mediaLoadData.trackType == C.TRACK_TYPE_VIDEO
|| mediaLoadData.trackType == C.TRACK_TYPE_DEFAULT) {
maybeUpdateVideoFormat(eventTime, mediaLoadData.trackFormat);
} else if (mediaLoadData.trackType == C.TRACK_TYPE_AUDIO) {
maybeUpdateAudioFormat(eventTime, mediaLoadData.trackFormat);
}
}
/**
* Notifies the tracker that the video size for the current playback changed.
*
* @param eventTime The {@link EventTime}.
* @param width The video width in pixels.
* @param height The video height in pixels.
*/
public void onVideoSizeChanged(EventTime eventTime, int width, int height) {
if (currentVideoFormat != null && currentVideoFormat.height == Format.NO_VALUE) {
Format formatWithHeight = currentVideoFormat.copyWithVideoSize(width, height);
maybeUpdateVideoFormat(eventTime, formatWithHeight);
}
}
/**
* Notifies the tracker of a playback speed change, including all playback speed changes while
* the playback is not in the foreground.
*
* @param eventTime The {@link EventTime}.
* @param playbackSpeed The new playback speed.
*/
public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) {
maybeUpdateMediaTimeHistory(eventTime.realtimeMs, eventTime.eventPlaybackPositionMs);
maybeRecordVideoFormatTime(eventTime.realtimeMs);
maybeRecordAudioFormatTime(eventTime.realtimeMs);
currentPlaybackSpeed = playbackSpeed;
}
/** Notifies the builder of an audio underrun for the current playback. */
public void onAudioUnderrun() {
audioUnderruns++;
}
/**
* Notifies the tracker of dropped video frames for the current playback.
*
* @param droppedFrames The number of dropped video frames.
*/
public void onDroppedVideoFrames(int droppedFrames) {
this.droppedFrames += droppedFrames;
}
/**
* Notifies the tracker of bandwidth measurement data for the current playback.
*
* @param timeMs The time for which bandwidth measurement data is available, in milliseconds.
* @param bytes The bytes transferred during {@code timeMs}.
*/
public void onBandwidthData(long timeMs, long bytes) {
bandwidthTimeMs += timeMs;
bandwidthBytes += bytes;
}
/**
* Notifies the tracker of a non-fatal error in the current playback.
*
* @param eventTime The {@link EventTime}.
* @param error The error.
*/
public void onNonFatalError(EventTime eventTime, Exception error) {
nonFatalErrorCount++;
if (keepHistory) {
nonFatalErrorHistory.add(Pair.create(eventTime, error));
}
}
/**
* Builds the playback stats.
*
@ -453,6 +715,7 @@ public final class PlaybackStatsListener
*/
public PlaybackStats build(boolean isFinal) {
long[] playbackStateDurationsMs = this.playbackStateDurationsMs;
List<long[]> mediaTimeHistory = this.mediaTimeHistory;
if (!isFinal) {
long buildTimeMs = SystemClock.elapsedRealtime();
playbackStateDurationsMs =
@ -460,6 +723,12 @@ public final class PlaybackStatsListener
long lastStateDurationMs = Math.max(0, buildTimeMs - currentPlaybackStateStartTimeMs);
playbackStateDurationsMs[currentPlaybackState] += lastStateDurationMs;
maybeUpdateMaxRebufferTimeMs(buildTimeMs);
maybeRecordVideoFormatTime(buildTimeMs);
maybeRecordAudioFormatTime(buildTimeMs);
mediaTimeHistory = new ArrayList<>(this.mediaTimeHistory);
if (keepHistory && currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING) {
mediaTimeHistory.add(guessMediaTimeBasedOnElapsedRealtime(buildTimeMs));
}
}
boolean isJoinTimeInvalid = this.isJoinTimeInvalid || !hasBeenReady;
long validJoinTimeMs =
@ -472,6 +741,7 @@ public final class PlaybackStatsListener
/* playbackCount= */ 1,
playbackStateDurationsMs,
isFinal ? playbackStateHistory : new ArrayList<>(playbackStateHistory),
mediaTimeHistory,
firstReportedTimeMs,
/* foregroundPlaybackCount= */ isForeground ? 1 : 0,
/* abandonedBeforeReadyCount= */ hasBeenReady ? 0 : 1,
@ -484,7 +754,30 @@ public final class PlaybackStatsListener
seekCount,
rebufferCount,
maxRebufferTimeMs,
/* adPlaybackCount= */ isAd ? 1 : 0);
/* adPlaybackCount= */ isAd ? 1 : 0,
isFinal ? videoFormatHistory : new ArrayList<>(videoFormatHistory),
isFinal ? audioFormatHistory : new ArrayList<>(audioFormatHistory),
videoFormatHeightTimeMs,
videoFormatHeightTimeProduct,
videoFormatBitrateTimeMs,
videoFormatBitrateTimeProduct,
audioFormatTimeMs,
audioFormatBitrateTimeProduct,
/* initialVideoFormatHeightCount= */ initialVideoFormatHeight == C.LENGTH_UNSET ? 0 : 1,
/* initialVideoFormatBitrateCount= */ initialVideoFormatBitrate == C.LENGTH_UNSET ? 0 : 1,
initialVideoFormatHeight,
initialVideoFormatBitrate,
/* initialAudioFormatBitrateCount= */ initialAudioFormatBitrate == C.LENGTH_UNSET ? 0 : 1,
initialAudioFormatBitrate,
bandwidthTimeMs,
bandwidthBytes,
droppedFrames,
audioUnderruns,
/* fatalErrorPlaybackCount= */ fatalErrorCount > 0 ? 1 : 0,
fatalErrorCount,
nonFatalErrorCount,
fatalErrorHistory,
nonFatalErrorHistory);
}
private void maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlayback) {
@ -517,7 +810,12 @@ public final class PlaybackStatsListener
pauseBufferCount++;
}
maybeUpdateMediaTimeHistory(
eventTime.realtimeMs,
/* mediaTimeMs= */ belongsToPlayback ? eventTime.eventPlaybackPositionMs : C.TIME_UNSET);
maybeUpdateMaxRebufferTimeMs(eventTime.realtimeMs);
maybeRecordVideoFormatTime(eventTime.realtimeMs);
maybeRecordAudioFormatTime(eventTime.realtimeMs);
currentPlaybackState = newPlaybackState;
currentPlaybackStateStartTimeMs = eventTime.realtimeMs;
@ -581,6 +879,96 @@ public final class PlaybackStatsListener
}
}
private void maybeUpdateMediaTimeHistory(long realtimeMs, long mediaTimeMs) {
if (currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PLAYING) {
if (mediaTimeMs == C.TIME_UNSET) {
return;
}
if (!mediaTimeHistory.isEmpty()) {
long previousMediaTimeMs = mediaTimeHistory.get(mediaTimeHistory.size() - 1)[1];
if (previousMediaTimeMs != mediaTimeMs) {
mediaTimeHistory.add(new long[] {realtimeMs, previousMediaTimeMs});
}
}
}
mediaTimeHistory.add(
mediaTimeMs == C.TIME_UNSET
? guessMediaTimeBasedOnElapsedRealtime(realtimeMs)
: new long[] {realtimeMs, mediaTimeMs});
}
private long[] guessMediaTimeBasedOnElapsedRealtime(long realtimeMs) {
long[] previousKnownMediaTimeHistory = mediaTimeHistory.get(mediaTimeHistory.size() - 1);
long previousRealtimeMs = previousKnownMediaTimeHistory[0];
long previousMediaTimeMs = previousKnownMediaTimeHistory[1];
long elapsedMediaTimeEstimateMs =
(long) ((realtimeMs - previousRealtimeMs) * currentPlaybackSpeed);
long mediaTimeEstimateMs = previousMediaTimeMs + elapsedMediaTimeEstimateMs;
return new long[] {realtimeMs, mediaTimeEstimateMs};
}
private void maybeUpdateVideoFormat(EventTime eventTime, @Nullable Format newFormat) {
if (Util.areEqual(currentVideoFormat, newFormat)) {
return;
}
maybeRecordVideoFormatTime(eventTime.realtimeMs);
if (newFormat != null) {
if (initialVideoFormatHeight == C.LENGTH_UNSET && newFormat.height != Format.NO_VALUE) {
initialVideoFormatHeight = newFormat.height;
}
if (initialVideoFormatBitrate == C.LENGTH_UNSET && newFormat.bitrate != Format.NO_VALUE) {
initialVideoFormatBitrate = newFormat.bitrate;
}
}
currentVideoFormat = newFormat;
if (keepHistory) {
videoFormatHistory.add(Pair.create(eventTime, currentVideoFormat));
}
}
private void maybeUpdateAudioFormat(EventTime eventTime, @Nullable Format newFormat) {
if (Util.areEqual(currentAudioFormat, newFormat)) {
return;
}
maybeRecordAudioFormatTime(eventTime.realtimeMs);
if (newFormat != null
&& initialAudioFormatBitrate == C.LENGTH_UNSET
&& newFormat.bitrate != Format.NO_VALUE) {
initialAudioFormatBitrate = newFormat.bitrate;
}
currentAudioFormat = newFormat;
if (keepHistory) {
audioFormatHistory.add(Pair.create(eventTime, currentAudioFormat));
}
}
private void maybeRecordVideoFormatTime(long nowMs) {
if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING
&& currentVideoFormat != null) {
long mediaDurationMs = (long) ((nowMs - lastVideoFormatStartTimeMs) * currentPlaybackSpeed);
if (currentVideoFormat.height != Format.NO_VALUE) {
videoFormatHeightTimeMs += mediaDurationMs;
videoFormatHeightTimeProduct += mediaDurationMs * currentVideoFormat.height;
}
if (currentVideoFormat.bitrate != Format.NO_VALUE) {
videoFormatBitrateTimeMs += mediaDurationMs;
videoFormatBitrateTimeProduct += mediaDurationMs * currentVideoFormat.bitrate;
}
}
lastVideoFormatStartTimeMs = nowMs;
}
private void maybeRecordAudioFormatTime(long nowMs) {
if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING
&& currentAudioFormat != null
&& currentAudioFormat.bitrate != Format.NO_VALUE) {
long mediaDurationMs = (long) ((nowMs - lastAudioFormatStartTimeMs) * currentPlaybackSpeed);
audioFormatTimeMs += mediaDurationMs;
audioFormatBitrateTimeProduct += mediaDurationMs * currentAudioFormat.bitrate;
}
lastAudioFormatStartTimeMs = nowMs;
}
private static boolean isReadyState(@PlaybackState int state) {
return state == PlaybackStats.PLAYBACK_STATE_PLAYING
|| state == PlaybackStats.PLAYBACK_STATE_PAUSED;