Improve PlaybackStatsListener by using onEvents callback
Previously the PlaybackStatsListener needed to handle all events individually, which required to keep some state of the player and to resolve potentially transient state changes. Using onEvents allows to channel all simultanous updates through one method so that no transient player state and other inconsistencies need to be handled. This makes the logic easier to read. In addition it also allows to resolve all simultaneous events to use one EventTime (with one timestamp). #exofixit PiperOrigin-RevId: 344415459
This commit is contained in:
parent
f1cf3d98d8
commit
3f6ec59868
@ -15,14 +15,15 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.analytics;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.util.Pair;
|
||||
import androidx.annotation.Nullable;
|
||||
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;
|
||||
@ -33,9 +34,7 @@ import com.google.android.exoplayer2.analytics.PlaybackStats.PlaybackState;
|
||||
import com.google.android.exoplayer2.source.LoadEventInfo;
|
||||
import com.google.android.exoplayer2.source.MediaLoadData;
|
||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||
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;
|
||||
@ -82,11 +81,17 @@ public final class PlaybackStatsListener
|
||||
private PlaybackStats finishedPlaybackStats;
|
||||
@Nullable private String activeContentPlayback;
|
||||
@Nullable private String activeAdPlayback;
|
||||
private boolean playWhenReady;
|
||||
@Player.State private int playbackState;
|
||||
private boolean isSuppressed;
|
||||
private float playbackSpeed;
|
||||
private boolean onSeekStartedCalled;
|
||||
|
||||
@Nullable private EventTime onSeekStartedEventTime;
|
||||
@Player.DiscontinuityReason int discontinuityReason;
|
||||
int droppedFrames;
|
||||
@Nullable Exception nonFatalException;
|
||||
long bandwidthTimeMs;
|
||||
long bandwidthBytes;
|
||||
@Nullable Format videoFormat;
|
||||
@Nullable Format audioFormat;
|
||||
int videoHeight;
|
||||
int videoWidth;
|
||||
|
||||
/**
|
||||
* Creates listener for playback stats.
|
||||
@ -102,9 +107,6 @@ public final class PlaybackStatsListener
|
||||
playbackStatsTrackers = new HashMap<>();
|
||||
sessionStartEventTimes = new HashMap<>();
|
||||
finishedPlaybackStats = PlaybackStats.EMPTY;
|
||||
playWhenReady = false;
|
||||
playbackState = Player.STATE_IDLE;
|
||||
playbackSpeed = 1f;
|
||||
period = new Period();
|
||||
sessionManager.setListener(this);
|
||||
}
|
||||
@ -172,20 +174,13 @@ public final class PlaybackStatsListener
|
||||
@Override
|
||||
public void onSessionCreated(EventTime eventTime, String session) {
|
||||
PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime);
|
||||
if (onSeekStartedCalled) {
|
||||
tracker.onSeekStarted(eventTime, /* belongsToPlayback= */ true);
|
||||
}
|
||||
tracker.onPlaybackStateChanged(eventTime, playbackState, /* belongsToPlayback= */ true);
|
||||
tracker.onPlayWhenReadyChanged(eventTime, playWhenReady, /* belongsToPlayback= */ true);
|
||||
tracker.onIsSuppressedChanged(eventTime, isSuppressed, /* belongsToPlayback= */ true);
|
||||
tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed);
|
||||
playbackStatsTrackers.put(session, tracker);
|
||||
sessionStartEventTimes.put(session, eventTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionActive(EventTime eventTime, String session) {
|
||||
Assertions.checkNotNull(playbackStatsTrackers.get(session)).onForeground(eventTime);
|
||||
checkNotNull(playbackStatsTrackers.get(session)).onForeground();
|
||||
if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) {
|
||||
activeAdPlayback = session;
|
||||
} else {
|
||||
@ -195,17 +190,177 @@ public final class PlaybackStatsListener
|
||||
|
||||
@Override
|
||||
public void onAdPlaybackStarted(EventTime eventTime, String contentSession, String adSession) {
|
||||
Assertions.checkState(Assertions.checkNotNull(eventTime.mediaPeriodId).isAd());
|
||||
checkNotNull(playbackStatsTrackers.get(contentSession)).onInterruptedByAd();
|
||||
}
|
||||
|
||||
@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 = checkNotNull(playbackStatsTrackers.remove(session));
|
||||
EventTime startEventTime = checkNotNull(sessionStartEventTimes.remove(session));
|
||||
tracker.onFinished(eventTime, automaticTransition);
|
||||
PlaybackStats playbackStats = tracker.build(/* isFinal= */ true);
|
||||
finishedPlaybackStats = PlaybackStats.merge(finishedPlaybackStats, playbackStats);
|
||||
if (callback != null) {
|
||||
callback.onPlaybackStatsReady(startEventTime, playbackStats);
|
||||
}
|
||||
}
|
||||
|
||||
// AnalyticsListener implementation.
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(EventTime eventTime, @Player.DiscontinuityReason int reason) {
|
||||
discontinuityReason = reason;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSeekStarted(EventTime eventTime) {
|
||||
onSeekStartedEventTime = eventTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {
|
||||
this.droppedFrames = droppedFrames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadError(
|
||||
EventTime eventTime,
|
||||
LoadEventInfo loadEventInfo,
|
||||
MediaLoadData mediaLoadData,
|
||||
IOException error,
|
||||
boolean wasCanceled) {
|
||||
nonFatalException = error;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmSessionManagerError(EventTime eventTime, Exception error) {
|
||||
nonFatalException = error;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBandwidthEstimate(
|
||||
EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {
|
||||
bandwidthTimeMs = totalLoadTimeMs;
|
||||
bandwidthBytes = totalBytesLoaded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {
|
||||
if (mediaLoadData.trackType == C.TRACK_TYPE_VIDEO
|
||||
|| mediaLoadData.trackType == C.TRACK_TYPE_DEFAULT) {
|
||||
videoFormat = mediaLoadData.trackFormat;
|
||||
} else if (mediaLoadData.trackType == C.TRACK_TYPE_AUDIO) {
|
||||
audioFormat = mediaLoadData.trackFormat;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(
|
||||
EventTime eventTime, int width, int height, int rotationDegrees, float pixelRatio) {
|
||||
videoWidth = width;
|
||||
videoHeight = height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvents(Player player, Events events) {
|
||||
if (events.size() == 0) {
|
||||
return;
|
||||
}
|
||||
maybeAddSessions(player, events);
|
||||
for (String session : playbackStatsTrackers.keySet()) {
|
||||
Pair<EventTime, Boolean> eventTimeAndBelongsToPlayback = findBestEventTime(events, session);
|
||||
PlaybackStatsTracker tracker = playbackStatsTrackers.get(session);
|
||||
boolean hasPositionDiscontinuity =
|
||||
hasEvent(events, session, EVENT_POSITION_DISCONTINUITY)
|
||||
|| hasEvent(events, session, EVENT_TIMELINE_CHANGED);
|
||||
boolean hasDroppedFrames = hasEvent(events, session, EVENT_DROPPED_VIDEO_FRAMES);
|
||||
boolean hasAudioUnderrun = hasEvent(events, session, EVENT_AUDIO_UNDERRUN);
|
||||
boolean startedLoading = hasEvent(events, session, EVENT_LOAD_STARTED);
|
||||
boolean hasFatalError = hasEvent(events, session, EVENT_PLAYER_ERROR);
|
||||
boolean hasNonFatalException =
|
||||
hasEvent(events, session, EVENT_LOAD_ERROR)
|
||||
|| hasEvent(events, session, EVENT_DRM_SESSION_MANAGER_ERROR);
|
||||
boolean hasBandwidthData = hasEvent(events, session, EVENT_BANDWIDTH_ESTIMATE);
|
||||
boolean hasFormatData = hasEvent(events, session, EVENT_DOWNSTREAM_FORMAT_CHANGED);
|
||||
boolean hasVideoSize = hasEvent(events, session, EVENT_VIDEO_SIZE_CHANGED);
|
||||
tracker.onEvents(
|
||||
player,
|
||||
/* eventTime= */ eventTimeAndBelongsToPlayback.first,
|
||||
/* belongsToPlayback= */ eventTimeAndBelongsToPlayback.second,
|
||||
/* seeked= */ onSeekStartedEventTime != null,
|
||||
hasPositionDiscontinuity,
|
||||
hasDroppedFrames ? droppedFrames : 0,
|
||||
hasAudioUnderrun,
|
||||
startedLoading,
|
||||
hasFatalError ? player.getPlayerError() : null,
|
||||
hasNonFatalException ? nonFatalException : null,
|
||||
hasBandwidthData ? bandwidthTimeMs : 0,
|
||||
hasBandwidthData ? bandwidthBytes : 0,
|
||||
hasFormatData ? videoFormat : null,
|
||||
hasFormatData ? audioFormat : null,
|
||||
hasVideoSize ? videoHeight : Format.NO_VALUE,
|
||||
hasVideoSize ? videoWidth : Format.NO_VALUE);
|
||||
}
|
||||
onSeekStartedEventTime = null;
|
||||
videoFormat = null;
|
||||
audioFormat = null;
|
||||
}
|
||||
|
||||
private void maybeAddSessions(Player player, Events events) {
|
||||
if (player.getCurrentTimeline().isEmpty() && player.getPlaybackState() == Player.STATE_IDLE) {
|
||||
// Player is completely idle. Don't add new sessions.
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < events.size(); i++) {
|
||||
@EventFlags int event = events.get(i);
|
||||
EventTime eventTime = events.getEventTime(event);
|
||||
if (event == EVENT_TIMELINE_CHANGED) {
|
||||
sessionManager.updateSessionsWithTimelineChange(eventTime);
|
||||
} else if (event == EVENT_POSITION_DISCONTINUITY) {
|
||||
sessionManager.updateSessionsWithDiscontinuity(eventTime, discontinuityReason);
|
||||
} else {
|
||||
sessionManager.updateSessions(eventTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Pair<EventTime, Boolean> findBestEventTime(Events events, String session) {
|
||||
// Check all event times of the events as well as the event time when a seek started.
|
||||
@Nullable EventTime eventTime = onSeekStartedEventTime;
|
||||
boolean belongsToPlayback =
|
||||
onSeekStartedEventTime != null
|
||||
&& sessionManager.belongsToSession(onSeekStartedEventTime, session);
|
||||
for (int i = 0; i < events.size(); i++) {
|
||||
@EventFlags int event = events.get(i);
|
||||
EventTime newEventTime = events.getEventTime(event);
|
||||
boolean newBelongsToPlayback = sessionManager.belongsToSession(newEventTime, session);
|
||||
if (eventTime == null
|
||||
|| (newBelongsToPlayback && !belongsToPlayback)
|
||||
|| (newBelongsToPlayback == belongsToPlayback
|
||||
&& newEventTime.realtimeMs > eventTime.realtimeMs)) {
|
||||
// Prefer event times for the current playback and prefer later timestamps.
|
||||
eventTime = newEventTime;
|
||||
belongsToPlayback = newBelongsToPlayback;
|
||||
}
|
||||
}
|
||||
checkNotNull(eventTime);
|
||||
if (!belongsToPlayback && eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) {
|
||||
// Replace ad event time with content event time unless it's for the ad playback itself.
|
||||
long contentPeriodPositionUs =
|
||||
eventTime
|
||||
.timeline
|
||||
.getPeriodByUid(eventTime.mediaPeriodId.periodUid, period)
|
||||
.getAdGroupTimeUs(eventTime.mediaPeriodId.adGroupIndex);
|
||||
long contentWindowPositionUs =
|
||||
contentPeriodPositionUs == C.TIME_END_OF_SOURCE
|
||||
? C.TIME_END_OF_SOURCE
|
||||
: contentPeriodPositionUs + period.getPositionInWindowUs();
|
||||
EventTime contentEventTime =
|
||||
if (contentPeriodPositionUs == C.TIME_END_OF_SOURCE) {
|
||||
contentPeriodPositionUs = period.durationUs;
|
||||
}
|
||||
long contentWindowPositionUs = contentPeriodPositionUs + period.getPositionInWindowUs();
|
||||
eventTime =
|
||||
new EventTime(
|
||||
eventTime.realtimeMs,
|
||||
eventTime.timeline,
|
||||
@ -220,239 +375,14 @@ public final class PlaybackStatsListener
|
||||
eventTime.currentMediaPeriodId,
|
||||
eventTime.currentPlaybackPositionMs,
|
||||
eventTime.totalBufferedDurationMs);
|
||||
Assertions.checkNotNull(playbackStatsTrackers.get(contentSession))
|
||||
.onInterruptedByAd(contentEventTime);
|
||||
belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
|
||||
}
|
||||
return Pair.create(eventTime, belongsToPlayback);
|
||||
}
|
||||
|
||||
@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.onPlaybackStateChanged(eventTime, Player.STATE_ENDED, /* belongsToPlayback= */ false);
|
||||
}
|
||||
tracker.onFinished(eventTime);
|
||||
PlaybackStats playbackStats = tracker.build(/* isFinal= */ true);
|
||||
finishedPlaybackStats = PlaybackStats.merge(finishedPlaybackStats, playbackStats);
|
||||
if (callback != null) {
|
||||
callback.onPlaybackStatsReady(startEventTime, playbackStats);
|
||||
}
|
||||
}
|
||||
|
||||
// AnalyticsListener implementation.
|
||||
|
||||
@Override
|
||||
public void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) {
|
||||
playbackState = state;
|
||||
maybeAddSession(eventTime);
|
||||
for (String session : playbackStatsTrackers.keySet()) {
|
||||
boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
|
||||
playbackStatsTrackers
|
||||
.get(session)
|
||||
.onPlaybackStateChanged(eventTime, playbackState, belongsToPlayback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayWhenReadyChanged(
|
||||
EventTime eventTime, boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {
|
||||
this.playWhenReady = playWhenReady;
|
||||
maybeAddSession(eventTime);
|
||||
for (String session : playbackStatsTrackers.keySet()) {
|
||||
boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
|
||||
playbackStatsTrackers
|
||||
.get(session)
|
||||
.onPlayWhenReadyChanged(eventTime, playWhenReady, belongsToPlayback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackSuppressionReasonChanged(
|
||||
EventTime eventTime, @Player.PlaybackSuppressionReason int playbackSuppressionReason) {
|
||||
isSuppressed = playbackSuppressionReason != Player.PLAYBACK_SUPPRESSION_REASON_NONE;
|
||||
maybeAddSession(eventTime);
|
||||
for (String session : playbackStatsTrackers.keySet()) {
|
||||
boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
|
||||
playbackStatsTrackers
|
||||
.get(session)
|
||||
.onIsSuppressedChanged(eventTime, isSuppressed, belongsToPlayback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(EventTime eventTime, @Player.TimelineChangeReason int reason) {
|
||||
sessionManager.updateSessionsWithTimelineChange(eventTime);
|
||||
for (String session : playbackStatsTrackers.keySet()) {
|
||||
if (sessionManager.belongsToSession(eventTime, session)) {
|
||||
playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime, /* isSeek= */ false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(EventTime eventTime, @Player.DiscontinuityReason int reason) {
|
||||
boolean isCompletelyIdle = eventTime.timeline.isEmpty() && playbackState == Player.STATE_IDLE;
|
||||
if (!isCompletelyIdle) {
|
||||
sessionManager.updateSessionsWithDiscontinuity(eventTime, reason);
|
||||
}
|
||||
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
||||
onSeekStartedCalled = false;
|
||||
}
|
||||
for (String session : playbackStatsTrackers.keySet()) {
|
||||
if (sessionManager.belongsToSession(eventTime, session)) {
|
||||
playbackStatsTrackers
|
||||
.get(session)
|
||||
.onPositionDiscontinuity(
|
||||
eventTime, /* isSeek= */ reason == Player.DISCONTINUITY_REASON_SEEK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSeekStarted(EventTime eventTime) {
|
||||
maybeAddSession(eventTime);
|
||||
for (String session : playbackStatsTrackers.keySet()) {
|
||||
boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
|
||||
playbackStatsTrackers.get(session).onSeekStarted(eventTime, belongsToPlayback);
|
||||
}
|
||||
onSeekStartedCalled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {
|
||||
maybeAddSession(eventTime);
|
||||
for (String session : playbackStatsTrackers.keySet()) {
|
||||
if (sessionManager.belongsToSession(eventTime, session)) {
|
||||
playbackStatsTrackers.get(session).onFatalError(eventTime, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackParametersChanged(
|
||||
EventTime eventTime, PlaybackParameters playbackParameters) {
|
||||
playbackSpeed = playbackParameters.speed;
|
||||
maybeAddSession(eventTime);
|
||||
for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) {
|
||||
tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(
|
||||
EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
maybeAddSession(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) {
|
||||
maybeAddSession(eventTime);
|
||||
for (String session : playbackStatsTrackers.keySet()) {
|
||||
if (sessionManager.belongsToSession(eventTime, session)) {
|
||||
playbackStatsTrackers.get(session).onLoadStarted(eventTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {
|
||||
maybeAddSession(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) {
|
||||
maybeAddSession(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) {
|
||||
maybeAddSession(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) {
|
||||
maybeAddSession(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) {
|
||||
maybeAddSession(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) {
|
||||
maybeAddSession(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) {
|
||||
maybeAddSession(eventTime);
|
||||
for (String session : playbackStatsTrackers.keySet()) {
|
||||
if (sessionManager.belongsToSession(eventTime, session)) {
|
||||
playbackStatsTrackers.get(session).onNonFatalError(eventTime, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeAddSession(EventTime eventTime) {
|
||||
boolean isCompletelyIdle = eventTime.timeline.isEmpty() && playbackState == Player.STATE_IDLE;
|
||||
if (!isCompletelyIdle) {
|
||||
sessionManager.updateSessions(eventTime);
|
||||
}
|
||||
private boolean hasEvent(Events events, String session, @EventFlags int event) {
|
||||
return events.contains(event)
|
||||
&& sessionManager.belongsToSession(events.getEventTime(event), session);
|
||||
}
|
||||
|
||||
/** Tracker for playback stats of a single playback. */
|
||||
@ -500,10 +430,6 @@ public final class PlaybackStatsListener
|
||||
private boolean isSeeking;
|
||||
private boolean isForeground;
|
||||
private boolean isInterruptedByAd;
|
||||
private boolean isFinished;
|
||||
private boolean playWhenReady;
|
||||
@Player.State private int playerPlaybackState;
|
||||
private boolean isSuppressed;
|
||||
private boolean hasFatalError;
|
||||
private boolean startedLoading;
|
||||
private long lastRebufferStartTimeMs;
|
||||
@ -530,7 +456,6 @@ public final class PlaybackStatsListener
|
||||
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();
|
||||
@ -540,150 +465,99 @@ public final class PlaybackStatsListener
|
||||
currentPlaybackSpeed = 1f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the tracker of a playback state change event, including all playback state changes
|
||||
* while the playback is not in the foreground.
|
||||
*
|
||||
* @param eventTime The {@link EventTime}.
|
||||
* @param state The current {@link Player.State}.
|
||||
* @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback.
|
||||
*/
|
||||
public void onPlaybackStateChanged(
|
||||
EventTime eventTime, @Player.State int state, boolean belongsToPlayback) {
|
||||
playerPlaybackState = state;
|
||||
if (state != Player.STATE_IDLE) {
|
||||
hasFatalError = false;
|
||||
}
|
||||
if (state != Player.STATE_BUFFERING) {
|
||||
isSeeking = false;
|
||||
}
|
||||
if (state == Player.STATE_IDLE || state == Player.STATE_ENDED) {
|
||||
isInterruptedByAd = false;
|
||||
}
|
||||
maybeUpdatePlaybackState(eventTime, belongsToPlayback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the tracker of a play when ready change event, including all play when ready changes
|
||||
* while the playback is not in the foreground.
|
||||
*
|
||||
* @param eventTime The {@link EventTime}.
|
||||
* @param playWhenReady Whether the playback will proceed when ready.
|
||||
* @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback.
|
||||
*/
|
||||
public void onPlayWhenReadyChanged(
|
||||
EventTime eventTime, boolean playWhenReady, boolean belongsToPlayback) {
|
||||
this.playWhenReady = playWhenReady;
|
||||
maybeUpdatePlaybackState(eventTime, belongsToPlayback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the tracker of a change to the playback suppression (e.g. due to audio focus loss),
|
||||
* including all updates while the playback is not in the foreground.
|
||||
*
|
||||
* @param eventTime The {@link EventTime}.
|
||||
* @param isSuppressed Whether playback is suppressed.
|
||||
* @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback.
|
||||
*/
|
||||
public void onIsSuppressedChanged(
|
||||
EventTime eventTime, boolean isSuppressed, boolean belongsToPlayback) {
|
||||
this.isSuppressed = isSuppressed;
|
||||
maybeUpdatePlaybackState(eventTime, belongsToPlayback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the tracker of a position discontinuity or timeline update for the current playback.
|
||||
*
|
||||
* @param eventTime The {@link EventTime}.
|
||||
* @param isSeek Whether the position discontinuity is for a seek.
|
||||
*/
|
||||
public void onPositionDiscontinuity(EventTime eventTime, boolean isSeek) {
|
||||
if (isSeek && playerPlaybackState == Player.STATE_IDLE) {
|
||||
isSeeking = false;
|
||||
}
|
||||
isInterruptedByAd = false;
|
||||
maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the tracker of the start of a seek, including all seeks while the playback is not in
|
||||
* the foreground.
|
||||
*
|
||||
* @param eventTime The {@link EventTime}.
|
||||
* @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback.
|
||||
*/
|
||||
public void onSeekStarted(EventTime eventTime, boolean belongsToPlayback) {
|
||||
isSeeking = true;
|
||||
maybeUpdatePlaybackState(eventTime, belongsToPlayback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the tracker of fatal player error in the current playback.
|
||||
*
|
||||
* @param eventTime The {@link EventTime}.
|
||||
*/
|
||||
public void onFatalError(EventTime eventTime, Exception error) {
|
||||
fatalErrorCount++;
|
||||
if (keepHistory) {
|
||||
fatalErrorHistory.add(new EventTimeAndException(eventTime, error));
|
||||
}
|
||||
hasFatalError = true;
|
||||
isInterruptedByAd = 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) {
|
||||
/** Notifies the tracker that the current playback became the active foreground playback. */
|
||||
public void onForeground() {
|
||||
isForeground = true;
|
||||
maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the tracker that the current playback has been interrupted for ad playback.
|
||||
*
|
||||
* @param eventTime The {@link EventTime}.
|
||||
*/
|
||||
public void onInterruptedByAd(EventTime eventTime) {
|
||||
/** Notifies the tracker that the current playback is interrupted by an ad. */
|
||||
public void onInterruptedByAd() {
|
||||
isInterruptedByAd = true;
|
||||
isSeeking = false;
|
||||
maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the tracker that the current playback has finished.
|
||||
*
|
||||
* @param eventTime The {@link EventTime}. Not guaranteed to belong to the current playback.
|
||||
* @param eventTime The {@link EventTime}. Does not belong to this playback.
|
||||
* @param automaticTransition Whether the playback finished because of an automatic transition
|
||||
* to the next playback item.
|
||||
*/
|
||||
public void onFinished(EventTime eventTime) {
|
||||
isFinished = true;
|
||||
maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ false);
|
||||
public void onFinished(EventTime eventTime, boolean automaticTransition) {
|
||||
// Simulate state change to ENDED to record natural ending of playback.
|
||||
@PlaybackState
|
||||
int finalPlaybackState =
|
||||
currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED || automaticTransition
|
||||
? PlaybackStats.PLAYBACK_STATE_ENDED
|
||||
: PlaybackStats.PLAYBACK_STATE_ABANDONED;
|
||||
maybeUpdateMediaTimeHistory(eventTime.realtimeMs, /* mediaTimeMs= */ C.TIME_UNSET);
|
||||
maybeRecordVideoFormatTime(eventTime.realtimeMs);
|
||||
maybeRecordAudioFormatTime(eventTime.realtimeMs);
|
||||
updatePlaybackState(finalPlaybackState, eventTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the tracker that the track selection for the current playback changed.
|
||||
* Notifies the tracker of new events.
|
||||
*
|
||||
* @param eventTime The {@link EventTime}.
|
||||
* @param trackSelections The new {@link TrackSelectionArray}.
|
||||
* @param player The {@link Player}.
|
||||
* @param eventTime The {@link EventTime} of the events.
|
||||
* @param belongsToPlayback Whether the {@code eventTime} belongs to this playback.
|
||||
* @param seeked Whether a seek occurred.
|
||||
* @param positionDiscontinuity Whether a position discontinuity occurred for this playback.
|
||||
* @param droppedFrameCount The number of newly dropped frames for this playback.
|
||||
* @param hasAudioUnderun Whether a new audio underrun occurred for this playback.
|
||||
* @param startedLoading Whether this playback started loading.
|
||||
* @param fatalError A fatal error for this playback, or null.
|
||||
* @param nonFatalException A non-fatal exception for this playback, or null.
|
||||
* @param bandwidthTimeMs The time in milliseconds spent loading for this playback.
|
||||
* @param bandwidthBytes The number of bytes loaded for this playback.
|
||||
* @param videoFormat A reported downstream video format for this playback, or null.
|
||||
* @param audioFormat A reported downstream audio format for this playback, or null.
|
||||
* @param videoHeight The reported video height for this playback, or {@link Format#NO_VALUE}.
|
||||
* @param videoWidth The reported video width for this playback, or {@link Format#NO_VALUE}.
|
||||
*/
|
||||
public void onTracksChanged(EventTime eventTime, TrackSelectionArray trackSelections) {
|
||||
public void onEvents(
|
||||
Player player,
|
||||
EventTime eventTime,
|
||||
boolean belongsToPlayback,
|
||||
boolean seeked,
|
||||
boolean positionDiscontinuity,
|
||||
int droppedFrameCount,
|
||||
boolean hasAudioUnderun,
|
||||
boolean startedLoading,
|
||||
@Nullable ExoPlaybackException fatalError,
|
||||
@Nullable Exception nonFatalException,
|
||||
long bandwidthTimeMs,
|
||||
long bandwidthBytes,
|
||||
@Nullable Format videoFormat,
|
||||
@Nullable Format audioFormat,
|
||||
int videoHeight,
|
||||
int videoWidth) {
|
||||
if (seeked) {
|
||||
isSeeking = true;
|
||||
}
|
||||
if (player.getPlaybackState() != Player.STATE_BUFFERING) {
|
||||
isSeeking = false;
|
||||
}
|
||||
int playerPlaybackState = player.getPlaybackState();
|
||||
if (playerPlaybackState == Player.STATE_IDLE
|
||||
|| playerPlaybackState == Player.STATE_ENDED
|
||||
|| positionDiscontinuity) {
|
||||
isInterruptedByAd = false;
|
||||
}
|
||||
if (fatalError != null) {
|
||||
hasFatalError = true;
|
||||
fatalErrorCount++;
|
||||
if (keepHistory) {
|
||||
fatalErrorHistory.add(new EventTimeAndException(eventTime, fatalError));
|
||||
}
|
||||
} else if (player.getPlayerError() == null) {
|
||||
hasFatalError = false;
|
||||
}
|
||||
if (isForeground && !isInterruptedByAd) {
|
||||
boolean videoEnabled = false;
|
||||
boolean audioEnabled = false;
|
||||
for (TrackSelection trackSelection : trackSelections.getAll()) {
|
||||
for (TrackSelection trackSelection : player.getCurrentTrackSelections().getAll()) {
|
||||
if (trackSelection != null && trackSelection.length() > 0) {
|
||||
int trackType = MimeTypes.getTrackType(trackSelection.getFormat(0).sampleMimeType);
|
||||
if (trackType == C.TRACK_TYPE_VIDEO) {
|
||||
@ -700,87 +574,47 @@ public final class PlaybackStatsListener
|
||||
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);
|
||||
if (videoFormat != null) {
|
||||
maybeUpdateVideoFormat(eventTime, videoFormat);
|
||||
}
|
||||
if (audioFormat != null) {
|
||||
maybeUpdateAudioFormat(eventTime, audioFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.buildUpon().setWidth(width).setHeight(height).build();
|
||||
maybeUpdateVideoFormat(eventTime, formatWithHeight);
|
||||
if (currentVideoFormat != null
|
||||
&& currentVideoFormat.height == Format.NO_VALUE
|
||||
&& videoHeight != Format.NO_VALUE) {
|
||||
Format formatWithHeightAndWidth =
|
||||
currentVideoFormat.buildUpon().setWidth(videoWidth).setHeight(videoHeight).build();
|
||||
maybeUpdateVideoFormat(eventTime, formatWithHeightAndWidth);
|
||||
}
|
||||
if (startedLoading) {
|
||||
this.startedLoading = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
if (hasAudioUnderun) {
|
||||
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) {
|
||||
this.droppedFrames += droppedFrameCount;
|
||||
this.bandwidthTimeMs += bandwidthTimeMs;
|
||||
this.bandwidthBytes += bandwidthBytes;
|
||||
if (nonFatalException != null) {
|
||||
nonFatalErrorCount++;
|
||||
if (keepHistory) {
|
||||
nonFatalErrorHistory.add(new EventTimeAndException(eventTime, error));
|
||||
nonFatalErrorHistory.add(new EventTimeAndException(eventTime, nonFatalException));
|
||||
}
|
||||
}
|
||||
|
||||
@PlaybackState int newPlaybackState = resolveNewPlaybackState(player);
|
||||
float newPlaybackSpeed = player.getPlaybackParameters().speed;
|
||||
if (currentPlaybackState != newPlaybackState || currentPlaybackSpeed != newPlaybackSpeed) {
|
||||
maybeUpdateMediaTimeHistory(
|
||||
eventTime.realtimeMs,
|
||||
belongsToPlayback ? eventTime.eventPlaybackPositionMs : C.TIME_UNSET);
|
||||
maybeRecordVideoFormatTime(eventTime.realtimeMs);
|
||||
maybeRecordAudioFormatTime(eventTime.realtimeMs);
|
||||
}
|
||||
currentPlaybackSpeed = newPlaybackSpeed;
|
||||
if (currentPlaybackState != newPlaybackState) {
|
||||
updatePlaybackState(newPlaybackState, eventTime);
|
||||
}
|
||||
}
|
||||
|
||||
@ -860,13 +694,8 @@ public final class PlaybackStatsListener
|
||||
nonFatalErrorHistory);
|
||||
}
|
||||
|
||||
private void maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlayback) {
|
||||
@PlaybackState int newPlaybackState = resolveNewPlaybackState();
|
||||
if (newPlaybackState == currentPlaybackState) {
|
||||
return;
|
||||
}
|
||||
private void updatePlaybackState(@PlaybackState int newPlaybackState, EventTime eventTime) {
|
||||
Assertions.checkArgument(eventTime.realtimeMs >= currentPlaybackStateStartTimeMs);
|
||||
|
||||
long stateDurationMs = eventTime.realtimeMs - currentPlaybackStateStartTimeMs;
|
||||
playbackStateDurationsMs[currentPlaybackState] += stateDurationMs;
|
||||
if (firstReportedTimeMs == C.TIME_UNSET) {
|
||||
@ -890,13 +719,7 @@ public final class PlaybackStatsListener
|
||||
&& newPlaybackState == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING) {
|
||||
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;
|
||||
@ -905,13 +728,9 @@ public final class PlaybackStatsListener
|
||||
}
|
||||
}
|
||||
|
||||
private @PlaybackState int resolveNewPlaybackState() {
|
||||
if (isFinished) {
|
||||
// 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_ABANDONED;
|
||||
} else if (isSeeking && isForeground) {
|
||||
private @PlaybackState int resolveNewPlaybackState(Player player) {
|
||||
@Player.State int playerPlaybackState = player.getPlaybackState();
|
||||
if (isSeeking && isForeground) {
|
||||
// Seeking takes precedence over errors such that we report a seek while in error state.
|
||||
return PlaybackStats.PLAYBACK_STATE_SEEKING;
|
||||
} else if (hasFatalError) {
|
||||
@ -932,17 +751,17 @@ public final class PlaybackStatsListener
|
||||
|| currentPlaybackState == PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) {
|
||||
return PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND;
|
||||
}
|
||||
if (!playWhenReady) {
|
||||
if (!player.getPlayWhenReady()) {
|
||||
return PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING;
|
||||
}
|
||||
return isSuppressed
|
||||
return player.getPlaybackSuppressionReason() != Player.PLAYBACK_SUPPRESSION_REASON_NONE
|
||||
? PlaybackStats.PLAYBACK_STATE_SUPPRESSED_BUFFERING
|
||||
: PlaybackStats.PLAYBACK_STATE_BUFFERING;
|
||||
} else if (playerPlaybackState == Player.STATE_READY) {
|
||||
if (!playWhenReady) {
|
||||
if (!player.getPlayWhenReady()) {
|
||||
return PlaybackStats.PLAYBACK_STATE_PAUSED;
|
||||
}
|
||||
return isSuppressed
|
||||
return player.getPlaybackSuppressionReason() != Player.PLAYBACK_SUPPRESSION_REASON_NONE
|
||||
? PlaybackStats.PLAYBACK_STATE_SUPPRESSED
|
||||
: PlaybackStats.PLAYBACK_STATE_PLAYING;
|
||||
} else if (playerPlaybackState == Player.STATE_IDLE
|
||||
|
@ -17,71 +17,60 @@ package com.google.android.exoplayer2.analytics;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.robolectric.shadows.ShadowLooper.runMainLooperToNextTask;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||
import com.google.android.exoplayer2.testutil.TestExoPlayerBuilder;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.stream.Collectors;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
/** Unit test for {@link PlaybackStatsListener}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class PlaybackStatsListenerTest {
|
||||
|
||||
private static final AnalyticsListener.EventTime EMPTY_TIMELINE_EVENT_TIME =
|
||||
new AnalyticsListener.EventTime(
|
||||
/* realtimeMs= */ 500,
|
||||
Timeline.EMPTY,
|
||||
/* windowIndex= */ 0,
|
||||
/* mediaPeriodId= */ null,
|
||||
/* eventPlaybackPositionMs= */ 0,
|
||||
/* currentTimeline= */ Timeline.EMPTY,
|
||||
/* currentWindowIndex= */ 0,
|
||||
/* currentMediaPeriodId= */ null,
|
||||
/* currentPlaybackPositionMs= */ 0,
|
||||
/* totalBufferedDurationMs= */ 0);
|
||||
private static final Timeline TEST_TIMELINE = new FakeTimeline();
|
||||
private static final MediaSource.MediaPeriodId TEST_MEDIA_PERIOD_ID =
|
||||
new MediaSource.MediaPeriodId(
|
||||
TEST_TIMELINE.getPeriod(/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true)
|
||||
.uid,
|
||||
/* windowSequenceNumber= */ 42);
|
||||
private static final AnalyticsListener.EventTime TEST_EVENT_TIME =
|
||||
new AnalyticsListener.EventTime(
|
||||
/* realtimeMs= */ 500,
|
||||
TEST_TIMELINE,
|
||||
/* windowIndex= */ 0,
|
||||
TEST_MEDIA_PERIOD_ID,
|
||||
/* eventPlaybackPositionMs= */ 123,
|
||||
TEST_TIMELINE,
|
||||
/* currentWindowIndex= */ 0,
|
||||
TEST_MEDIA_PERIOD_ID,
|
||||
/* currentPlaybackPositionMs= */ 123,
|
||||
/* totalBufferedDurationMs= */ 456);
|
||||
private SimpleExoPlayer player;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
player = new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
player.release();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void events_duringInitialIdleState_dontCreateNewPlaybackStats() {
|
||||
PlaybackStatsListener playbackStatsListener =
|
||||
new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null);
|
||||
player.addAnalyticsListener(playbackStatsListener);
|
||||
|
||||
playbackStatsListener.onPositionDiscontinuity(
|
||||
EMPTY_TIMELINE_EVENT_TIME, Player.DISCONTINUITY_REASON_SEEK);
|
||||
playbackStatsListener.onPlaybackParametersChanged(
|
||||
EMPTY_TIMELINE_EVENT_TIME, new PlaybackParameters(/* speed= */ 2.0f));
|
||||
playbackStatsListener.onPlayWhenReadyChanged(
|
||||
EMPTY_TIMELINE_EVENT_TIME,
|
||||
/* playWhenReady= */ true,
|
||||
Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
|
||||
player.seekTo(/* positionMs= */ 1234);
|
||||
runMainLooperToNextTask();
|
||||
player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f));
|
||||
runMainLooperToNextTask();
|
||||
player.play();
|
||||
runMainLooperToNextTask();
|
||||
|
||||
assertThat(playbackStatsListener.getPlaybackStats()).isNull();
|
||||
}
|
||||
@ -90,8 +79,10 @@ public final class PlaybackStatsListenerTest {
|
||||
public void stateChangeEvent_toNonIdle_createsInitialPlaybackStats() {
|
||||
PlaybackStatsListener playbackStatsListener =
|
||||
new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null);
|
||||
player.addAnalyticsListener(playbackStatsListener);
|
||||
|
||||
playbackStatsListener.onPlaybackStateChanged(EMPTY_TIMELINE_EVENT_TIME, Player.STATE_BUFFERING);
|
||||
player.prepare();
|
||||
runMainLooperToNextTask();
|
||||
|
||||
assertThat(playbackStatsListener.getPlaybackStats()).isNotNull();
|
||||
}
|
||||
@ -100,21 +91,25 @@ public final class PlaybackStatsListenerTest {
|
||||
public void timelineChangeEvent_toNonEmpty_createsInitialPlaybackStats() {
|
||||
PlaybackStatsListener playbackStatsListener =
|
||||
new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null);
|
||||
player.addAnalyticsListener(playbackStatsListener);
|
||||
|
||||
playbackStatsListener.onTimelineChanged(
|
||||
TEST_EVENT_TIME, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
|
||||
player.setMediaItem(MediaItem.fromUri("http://test.org"));
|
||||
runMainLooperToNextTask();
|
||||
|
||||
assertThat(playbackStatsListener.getPlaybackStats()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void playback_withKeepHistory_updatesStats() {
|
||||
public void playback_withKeepHistory_updatesStats() throws Exception {
|
||||
PlaybackStatsListener playbackStatsListener =
|
||||
new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null);
|
||||
player.addAnalyticsListener(playbackStatsListener);
|
||||
|
||||
playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_BUFFERING);
|
||||
playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_READY);
|
||||
playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_ENDED);
|
||||
player.setMediaSource(new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)));
|
||||
player.prepare();
|
||||
player.play();
|
||||
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
runMainLooperToNextTask();
|
||||
|
||||
@Nullable PlaybackStats playbackStats = playbackStatsListener.getPlaybackStats();
|
||||
assertThat(playbackStats).isNotNull();
|
||||
@ -122,13 +117,16 @@ public final class PlaybackStatsListenerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void playback_withoutKeepHistory_updatesStats() {
|
||||
public void playback_withoutKeepHistory_updatesStats() throws Exception {
|
||||
PlaybackStatsListener playbackStatsListener =
|
||||
new PlaybackStatsListener(/* keepHistory= */ false, /* callback= */ null);
|
||||
player.addAnalyticsListener(playbackStatsListener);
|
||||
|
||||
playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_BUFFERING);
|
||||
playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_READY);
|
||||
playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_ENDED);
|
||||
player.setMediaSource(new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)));
|
||||
player.prepare();
|
||||
player.play();
|
||||
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
runMainLooperToNextTask();
|
||||
|
||||
@Nullable PlaybackStats playbackStats = playbackStatsListener.getPlaybackStats();
|
||||
assertThat(playbackStats).isNotNull();
|
||||
@ -140,53 +138,45 @@ public final class PlaybackStatsListenerTest {
|
||||
PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class);
|
||||
PlaybackStatsListener playbackStatsListener =
|
||||
new PlaybackStatsListener(/* keepHistory= */ true, callback);
|
||||
player.addAnalyticsListener(playbackStatsListener);
|
||||
|
||||
// Create session with an event and finish it by simulating removal from playlist.
|
||||
playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_BUFFERING);
|
||||
// Create session with some events and finish it by removing it from the playlist.
|
||||
player.setMediaSource(new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)));
|
||||
player.prepare();
|
||||
runMainLooperToNextTask();
|
||||
verify(callback, never()).onPlaybackStatsReady(any(), any());
|
||||
playbackStatsListener.onTimelineChanged(
|
||||
EMPTY_TIMELINE_EVENT_TIME, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
|
||||
player.clearMediaItems();
|
||||
runMainLooperToNextTask();
|
||||
|
||||
verify(callback).onPlaybackStatsReady(eq(TEST_EVENT_TIME), any());
|
||||
verify(callback).onPlaybackStatsReady(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void finishAllSessions_callsAllPendingCallbacks() {
|
||||
AnalyticsListener.EventTime eventTimeWindow0 =
|
||||
new AnalyticsListener.EventTime(
|
||||
/* realtimeMs= */ 0,
|
||||
Timeline.EMPTY,
|
||||
/* windowIndex= */ 0,
|
||||
/* mediaPeriodId= */ null,
|
||||
/* eventPlaybackPositionMs= */ 0,
|
||||
Timeline.EMPTY,
|
||||
/* currentWindowIndex= */ 0,
|
||||
/* currentMediaPeriodId= */ null,
|
||||
/* currentPlaybackPositionMs= */ 0,
|
||||
/* totalBufferedDurationMs= */ 0);
|
||||
AnalyticsListener.EventTime eventTimeWindow1 =
|
||||
new AnalyticsListener.EventTime(
|
||||
/* realtimeMs= */ 0,
|
||||
Timeline.EMPTY,
|
||||
/* windowIndex= */ 1,
|
||||
/* mediaPeriodId= */ null,
|
||||
/* eventPlaybackPositionMs= */ 0,
|
||||
Timeline.EMPTY,
|
||||
/* currentWindowIndex= */ 1,
|
||||
/* currentMediaPeriodId= */ null,
|
||||
/* currentPlaybackPositionMs= */ 0,
|
||||
/* totalBufferedDurationMs= */ 0);
|
||||
public void finishAllSessions_callsAllPendingCallbacks() throws Exception {
|
||||
PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class);
|
||||
PlaybackStatsListener playbackStatsListener =
|
||||
new PlaybackStatsListener(/* keepHistory= */ true, callback);
|
||||
playbackStatsListener.onPlaybackStateChanged(eventTimeWindow0, Player.STATE_BUFFERING);
|
||||
playbackStatsListener.onPlaybackStateChanged(eventTimeWindow1, Player.STATE_BUFFERING);
|
||||
player.addAnalyticsListener(playbackStatsListener);
|
||||
|
||||
MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1));
|
||||
player.setMediaSources(ImmutableList.of(mediaSource, mediaSource));
|
||||
player.prepare();
|
||||
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
|
||||
// Play close to the end of the first item to ensure the second session is already created, but
|
||||
// the first one isn't finished yet.
|
||||
TestPlayerRunHelper.playUntilPosition(
|
||||
player, /* windowIndex= */ 0, /* positionMs= */ player.getDuration());
|
||||
runMainLooperToNextTask();
|
||||
playbackStatsListener.finishAllSessions();
|
||||
|
||||
verify(callback, times(2)).onPlaybackStatsReady(any(), any());
|
||||
verify(callback).onPlaybackStatsReady(eq(eventTimeWindow0), any());
|
||||
verify(callback).onPlaybackStatsReady(eq(eventTimeWindow1), any());
|
||||
ArgumentCaptor<AnalyticsListener.EventTime> eventTimeCaptor =
|
||||
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
|
||||
verify(callback, times(2)).onPlaybackStatsReady(eventTimeCaptor.capture(), any());
|
||||
assertThat(
|
||||
eventTimeCaptor.getAllValues().stream()
|
||||
.map(eventTime -> eventTime.windowIndex)
|
||||
.collect(Collectors.toList()))
|
||||
.containsExactly(0, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -194,14 +184,17 @@ public final class PlaybackStatsListenerTest {
|
||||
PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class);
|
||||
PlaybackStatsListener playbackStatsListener =
|
||||
new PlaybackStatsListener(/* keepHistory= */ true, callback);
|
||||
playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_BUFFERING);
|
||||
SystemClock.setCurrentTimeMillis(TEST_EVENT_TIME.realtimeMs + 100);
|
||||
player.addAnalyticsListener(playbackStatsListener);
|
||||
|
||||
player.setMediaItem(MediaItem.fromUri("http://test.org"));
|
||||
runMainLooperToNextTask();
|
||||
playbackStatsListener.finishAllSessions();
|
||||
// Simulate removing the playback item to ensure the session would finish if it hadn't already.
|
||||
playbackStatsListener.onTimelineChanged(
|
||||
EMPTY_TIMELINE_EVENT_TIME, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
|
||||
|
||||
// Simulate removing the playback item to ensure the session would finish if it hadn't already.
|
||||
player.clearMediaItems();
|
||||
runMainLooperToNextTask();
|
||||
|
||||
// Verify the callback was called once only.
|
||||
verify(callback).onPlaybackStatsReady(any(), any());
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user