mirror of
https://github.com/samsonjs/media.git
synced 2026-04-01 10:35:48 +00:00
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:
parent
77595da159
commit
fd1179aaa1
4 changed files with 842 additions and 17 deletions
|
|
@ -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)).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue