diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java index bd8fb213ed..b370c893de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java @@ -38,8 +38,10 @@ public final class PlaybackStats { * #PLAYBACK_STATE_JOINING_FOREGROUND}, {@link #PLAYBACK_STATE_JOINING_BACKGROUND}, {@link * #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED}, {@link #PLAYBACK_STATE_SEEKING}, * {@link #PLAYBACK_STATE_BUFFERING}, {@link #PLAYBACK_STATE_PAUSED_BUFFERING}, {@link - * #PLAYBACK_STATE_SEEK_BUFFERING}, {@link #PLAYBACK_STATE_ENDED}, {@link - * #PLAYBACK_STATE_STOPPED}, {@link #PLAYBACK_STATE_FAILED} or {@link #PLAYBACK_STATE_SUSPENDED}. + * #PLAYBACK_STATE_SEEK_BUFFERING}, {@link #PLAYBACK_STATE_SUPPRESSED}, {@link + * #PLAYBACK_STATE_SUPPRESSED_BUFFERING}, {@link #PLAYBACK_STATE_ENDED}, {@link + * #PLAYBACK_STATE_STOPPED}, {@link #PLAYBACK_STATE_FAILED}, {@link + * #PLAYBACK_STATE_INTERRUPTED_BY_AD} or {@link #PLAYBACK_STATE_ABANDONED}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -54,10 +56,13 @@ public final class PlaybackStats { PLAYBACK_STATE_BUFFERING, PLAYBACK_STATE_PAUSED_BUFFERING, PLAYBACK_STATE_SEEK_BUFFERING, + PLAYBACK_STATE_SUPPRESSED, + PLAYBACK_STATE_SUPPRESSED_BUFFERING, PLAYBACK_STATE_ENDED, PLAYBACK_STATE_STOPPED, PLAYBACK_STATE_FAILED, - PLAYBACK_STATE_SUSPENDED + PLAYBACK_STATE_INTERRUPTED_BY_AD, + PLAYBACK_STATE_ABANDONED }) @interface PlaybackState {} /** Playback has not started (initial state). */ @@ -72,22 +77,28 @@ public final class PlaybackStats { public static final int PLAYBACK_STATE_PAUSED = 4; /** Playback is handling a seek. */ public static final int PLAYBACK_STATE_SEEKING = 5; - /** Playback is buffering to restart playback. */ + /** Playback is buffering to resume active playback. */ public static final int PLAYBACK_STATE_BUFFERING = 6; /** Playback is buffering while paused. */ public static final int PLAYBACK_STATE_PAUSED_BUFFERING = 7; /** Playback is buffering after a seek. */ public static final int PLAYBACK_STATE_SEEK_BUFFERING = 8; + /** Playback is suppressed (e.g. due to audio focus loss). */ + public static final int PLAYBACK_STATE_SUPPRESSED = 9; + /** Playback is suppressed (e.g. due to audio focus loss) while buffering to resume a playback. */ + public static final int PLAYBACK_STATE_SUPPRESSED_BUFFERING = 10; /** Playback has reached the end of the media. */ - public static final int PLAYBACK_STATE_ENDED = 9; - /** Playback is stopped and can be resumed. */ - public static final int PLAYBACK_STATE_STOPPED = 10; + public static final int PLAYBACK_STATE_ENDED = 11; + /** Playback is stopped and can be restarted. */ + public static final int PLAYBACK_STATE_STOPPED = 12; /** Playback is stopped due a fatal error and can be retried. */ - public static final int PLAYBACK_STATE_FAILED = 11; - /** Playback is suspended, e.g. because the user left or it is interrupted by another playback. */ - public static final int PLAYBACK_STATE_SUSPENDED = 12; + public static final int PLAYBACK_STATE_FAILED = 13; + /** Playback is interrupted by an ad. */ + public static final int PLAYBACK_STATE_INTERRUPTED_BY_AD = 14; + /** Playback is abandoned before reaching the end of the media. */ + public static final int PLAYBACK_STATE_ABANDONED = 15; /** Total number of playback states. */ - /* package */ static final int PLAYBACK_STATE_COUNT = 13; + /* package */ static final int PLAYBACK_STATE_COUNT = 16; /** Empty playback stats. */ public static final PlaybackStats EMPTY = merge(/* nothing */ ); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java index 8b9f1d1ced..6768677fa4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -81,6 +81,7 @@ public final class PlaybackStatsListener @Nullable private String activeAdPlayback; private boolean playWhenReady; @Player.State private int playbackState; + private boolean isSuppressed; private float playbackSpeed; /** @@ -205,7 +206,7 @@ public final class PlaybackStatsListener eventTime.currentPlaybackPositionMs, eventTime.totalBufferedDurationMs); Assertions.checkNotNull(playbackStatsTrackers.get(contentSession)) - .onSuspended(contentEventTime, /* belongsToPlayback= */ true); + .onInterruptedByAd(contentEventTime); } @Override @@ -222,7 +223,7 @@ public final class PlaybackStatsListener tracker.onPlayerStateChanged( eventTime, /* playWhenReady= */ true, Player.STATE_ENDED, /* belongsToPlayback= */ false); } - tracker.onSuspended(eventTime, /* belongsToPlayback= */ false); + tracker.onFinished(eventTime); PlaybackStats playbackStats = tracker.build(/* isFinal= */ true); finishedPlaybackStats = PlaybackStats.merge(finishedPlaybackStats, playbackStats); if (callback != null) { @@ -246,6 +247,19 @@ public final class PlaybackStatsListener } } + @Override + public void onPlaybackSuppressionReasonChanged( + EventTime eventTime, int playbackSuppressionReason) { + isSuppressed = playbackSuppressionReason != Player.PLAYBACK_SUPPRESSION_REASON_NONE; + sessionManager.updateSessions(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, int reason) { sessionManager.handleTimelineUpdate(eventTime); @@ -456,9 +470,11 @@ public final class PlaybackStatsListener private long currentPlaybackStateStartTimeMs; private boolean isSeeking; private boolean isForeground; - private boolean isSuspended; + 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; @@ -515,18 +531,32 @@ public final class PlaybackStatsListener hasFatalError = false; } if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED) { - isSuspended = false; + isInterruptedByAd = false; } 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}. */ public void onPositionDiscontinuity(EventTime eventTime) { - isSuspended = false; + isInterruptedByAd = false; maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); } @@ -561,7 +591,7 @@ public final class PlaybackStatsListener fatalErrorHistory.add(Pair.create(eventTime, error)); } hasFatalError = true; - isSuspended = false; + isInterruptedByAd = false; isSeeking = false; maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); } @@ -587,16 +617,24 @@ public final class PlaybackStatsListener } /** - * Notifies the tracker that the current playback has been suspended, e.g. for ad playback or - * permanently. + * Notifies the tracker that the current playback has been interrupted for ad playback. * * @param eventTime The {@link EventTime}. - * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. */ - public void onSuspended(EventTime eventTime, boolean belongsToPlayback) { - isSuspended = true; + public void onInterruptedByAd(EventTime eventTime) { + isInterruptedByAd = true; isSeeking = false; - maybeUpdatePlaybackState(eventTime, belongsToPlayback); + 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. + */ + public void onFinished(EventTime eventTime) { + isFinished = true; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ false); } /** @@ -809,8 +847,9 @@ public final class PlaybackStatsListener rebufferCount++; lastRebufferStartTimeMs = eventTime.realtimeMs; } - if (newPlaybackState == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING - && currentPlaybackState == PlaybackStats.PLAYBACK_STATE_BUFFERING) { + if (isRebufferingState(currentPlaybackState) + && currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING + && newPlaybackState == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING) { pauseBufferCount++; } @@ -829,11 +868,11 @@ public final class PlaybackStatsListener } private @PlaybackState int resolveNewPlaybackState() { - if (isSuspended) { + 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_SUSPENDED; + : PlaybackStats.PLAYBACK_STATE_ABANDONED; } else if (isSeeking) { // Seeking takes precedence over errors such that we report a seek while in error state. return PlaybackStats.PLAYBACK_STATE_SEEKING; @@ -844,26 +883,34 @@ public final class PlaybackStatsListener return startedLoading ? PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND : PlaybackStats.PLAYBACK_STATE_NOT_STARTED; + } else if (isInterruptedByAd) { + return PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD; } else if (playerPlaybackState == Player.STATE_ENDED) { return PlaybackStats.PLAYBACK_STATE_ENDED; } else if (playerPlaybackState == Player.STATE_BUFFERING) { if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_NOT_STARTED || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND - || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SUSPENDED) { + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) { return PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND; } if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING) { return PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING; } - return playWhenReady - ? PlaybackStats.PLAYBACK_STATE_BUFFERING - : PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; + if (!playWhenReady) { + return PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; + } + return isSuppressed + ? PlaybackStats.PLAYBACK_STATE_SUPPRESSED_BUFFERING + : PlaybackStats.PLAYBACK_STATE_BUFFERING; } else if (playerPlaybackState == Player.STATE_READY) { - return playWhenReady - ? PlaybackStats.PLAYBACK_STATE_PLAYING - : PlaybackStats.PLAYBACK_STATE_PAUSED; + if (!playWhenReady) { + return PlaybackStats.PLAYBACK_STATE_PAUSED; + } + return isSuppressed + ? PlaybackStats.PLAYBACK_STATE_SUPPRESSED + : PlaybackStats.PLAYBACK_STATE_PLAYING; } else if (playerPlaybackState == Player.STATE_IDLE && currentPlaybackState != PlaybackStats.PLAYBACK_STATE_NOT_STARTED) { // This case only applies for calls to player.stop(). All other IDLE cases are handled by @@ -974,7 +1021,8 @@ public final class PlaybackStatsListener private static boolean isReadyState(@PlaybackState int state) { return state == PlaybackStats.PLAYBACK_STATE_PLAYING - || state == PlaybackStats.PLAYBACK_STATE_PAUSED; + || state == PlaybackStats.PLAYBACK_STATE_PAUSED + || state == PlaybackStats.PLAYBACK_STATE_SUPPRESSED; } private static boolean isPausedState(@PlaybackState int state) { @@ -984,21 +1032,23 @@ public final class PlaybackStatsListener private static boolean isRebufferingState(@PlaybackState int state) { return state == PlaybackStats.PLAYBACK_STATE_BUFFERING - || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; + || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING + || state == PlaybackStats.PLAYBACK_STATE_SUPPRESSED_BUFFERING; } private static boolean isInvalidJoinTransition( @PlaybackState int oldState, @PlaybackState int newState) { if (oldState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND && oldState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND - && oldState != PlaybackStats.PLAYBACK_STATE_SUSPENDED) { + && oldState != PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) { return false; } return newState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND && newState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND - && newState != PlaybackStats.PLAYBACK_STATE_SUSPENDED + && newState != PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD && newState != PlaybackStats.PLAYBACK_STATE_PLAYING && newState != PlaybackStats.PLAYBACK_STATE_PAUSED + && newState != PlaybackStats.PLAYBACK_STATE_SUPPRESSED && newState != PlaybackStats.PLAYBACK_STATE_ENDED; } }