Make use of position discontinuity changes.

The extended onPositionDiscontinuity callback can be used to improve some
listener classes:
 - Listening to onTimelineChanged to detect discontinuities is no longer needed.
 - Listening to onSeekStarted is no longer needed as the start position is part
   of the onPositionDiscontinuty callback.
 - The exact old position is also useful for media time history logging.

As a side effect, removing onSeekStarted handling from PlaybackStatsListener
fixes Issue: #8675 that was caused by the special EventTime handling for
onSeekStarted.

PiperOrigin-RevId: 365558959
This commit is contained in:
tonihei 2021-03-29 14:18:13 +01:00 committed by Oliver Woodman
parent 47af9a6889
commit 394ab7bcfd
4 changed files with 79 additions and 73 deletions

View File

@ -33,6 +33,7 @@ import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.Random; import java.util.Random;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** /**
* Default {@link PlaybackSessionManager} which instantiates a new session for each window in the * Default {@link PlaybackSessionManager} which instantiates a new session for each window in the
@ -184,7 +185,7 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag
} }
} }
} }
updateSessionsWithDiscontinuity(eventTime, Player.DISCONTINUITY_REASON_INTERNAL); updateCurrentSession(eventTime);
} }
@Override @Override
@ -208,6 +209,31 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag
} }
} }
} }
updateCurrentSession(eventTime);
}
@Override
@Nullable
public synchronized String getActiveSessionId() {
return currentSessionId;
}
@Override
public synchronized void finishAllSessions(EventTime eventTime) {
currentSessionId = null;
Iterator<SessionDescriptor> iterator = sessions.values().iterator();
while (iterator.hasNext()) {
SessionDescriptor session = iterator.next();
iterator.remove();
if (session.isCreated && listener != null) {
listener.onSessionFinished(
eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false);
}
}
}
@RequiresNonNull("listener")
private void updateCurrentSession(EventTime eventTime) {
@Nullable SessionDescriptor previousSessionDescriptor = sessions.get(currentSessionId); @Nullable SessionDescriptor previousSessionDescriptor = sessions.get(currentSessionId);
SessionDescriptor currentSessionDescriptor = SessionDescriptor currentSessionDescriptor =
getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId); getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId);
@ -234,20 +260,6 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag
} }
} }
@Override
public void finishAllSessions(EventTime eventTime) {
currentSessionId = null;
Iterator<SessionDescriptor> iterator = sessions.values().iterator();
while (iterator.hasNext()) {
SessionDescriptor session = iterator.next();
iterator.remove();
if (session.isCreated && listener != null) {
listener.onSessionFinished(
eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false);
}
}
}
private SessionDescriptor getOrAddSession( private SessionDescriptor getOrAddSession(
int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
// There should only be one matching session if mediaPeriodId is non-null. If mediaPeriodId is // There should only be one matching session if mediaPeriodId is non-null. If mediaPeriodId is

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.analytics; package com.google.android.exoplayer2.analytics;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;
@ -128,6 +129,13 @@ public interface PlaybackSessionManager {
*/ */
void updateSessionsWithDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason); void updateSessionsWithDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason);
/**
* Returns the session identifier of the session that is currently actively playing, or {@code
* null} if there no such session.
*/
@Nullable
String getActiveSessionId();
/** /**
* Finishes all existing sessions and calls their respective {@link * Finishes all existing sessions and calls their respective {@link
* Listener#onSessionFinished(EventTime, String, boolean)} callback. * Listener#onSessionFinished(EventTime, String, boolean)} callback.

View File

@ -79,19 +79,18 @@ public final class PlaybackStatsListener
private final Period period; private final Period period;
private PlaybackStats finishedPlaybackStats; private PlaybackStats finishedPlaybackStats;
@Nullable private String activeContentPlayback;
@Nullable private String activeAdPlayback;
@Nullable private EventTime onSeekStartedEventTime; @Nullable private String discontinuityFromSession;
@Player.DiscontinuityReason int discontinuityReason; private long discontinuityFromPositionMs;
int droppedFrames; @Player.DiscontinuityReason private int discontinuityReason;
@Nullable Exception nonFatalException; private int droppedFrames;
long bandwidthTimeMs; @Nullable private Exception nonFatalException;
long bandwidthBytes; private long bandwidthTimeMs;
@Nullable Format videoFormat; private long bandwidthBytes;
@Nullable Format audioFormat; @Nullable private Format videoFormat;
int videoHeight; @Nullable private Format audioFormat;
int videoWidth; private int videoHeight;
private int videoWidth;
/** /**
* Creates listener for playback stats. * Creates listener for playback stats.
@ -137,13 +136,10 @@ public final class PlaybackStatsListener
*/ */
@Nullable @Nullable
public PlaybackStats getPlaybackStats() { public PlaybackStats getPlaybackStats() {
@Nullable String activeSessionId = sessionManager.getActiveSessionId();
@Nullable @Nullable
PlaybackStatsTracker activeStatsTracker = PlaybackStatsTracker activeStatsTracker =
activeAdPlayback != null activeSessionId == null ? null : playbackStatsTrackers.get(activeSessionId);
? playbackStatsTrackers.get(activeAdPlayback)
: activeContentPlayback != null
? playbackStatsTrackers.get(activeContentPlayback)
: null;
return activeStatsTracker == null ? null : activeStatsTracker.build(/* isFinal= */ false); return activeStatsTracker == null ? null : activeStatsTracker.build(/* isFinal= */ false);
} }
@ -159,11 +155,6 @@ public final class PlaybackStatsListener
@Override @Override
public void onSessionActive(EventTime eventTime, String session) { public void onSessionActive(EventTime eventTime, String session) {
checkNotNull(playbackStatsTrackers.get(session)).onForeground(); checkNotNull(playbackStatsTrackers.get(session)).onForeground();
if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) {
activeAdPlayback = session;
} else {
activeContentPlayback = session;
}
} }
@Override @Override
@ -173,14 +164,11 @@ public final class PlaybackStatsListener
@Override @Override
public void onSessionFinished(EventTime eventTime, String session, boolean automaticTransition) { 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)); PlaybackStatsTracker tracker = checkNotNull(playbackStatsTrackers.remove(session));
EventTime startEventTime = checkNotNull(sessionStartEventTimes.remove(session)); EventTime startEventTime = checkNotNull(sessionStartEventTimes.remove(session));
tracker.onFinished(eventTime, automaticTransition); long discontinuityFromPositionMs =
session.equals(discontinuityFromSession) ? this.discontinuityFromPositionMs : C.TIME_UNSET;
tracker.onFinished(eventTime, automaticTransition, discontinuityFromPositionMs);
PlaybackStats playbackStats = tracker.build(/* isFinal= */ true); PlaybackStats playbackStats = tracker.build(/* isFinal= */ true);
finishedPlaybackStats = PlaybackStats.merge(finishedPlaybackStats, playbackStats); finishedPlaybackStats = PlaybackStats.merge(finishedPlaybackStats, playbackStats);
if (callback != null) { if (callback != null) {
@ -191,13 +179,16 @@ public final class PlaybackStatsListener
// AnalyticsListener implementation. // AnalyticsListener implementation.
@Override @Override
public void onPositionDiscontinuity(EventTime eventTime, @Player.DiscontinuityReason int reason) { public void onPositionDiscontinuity(
discontinuityReason = reason; EventTime eventTime,
Player.PositionInfo oldPositionInfo,
Player.PositionInfo newPositionInfo,
@Player.DiscontinuityReason int reason) {
if (discontinuityFromSession == null) {
discontinuityFromSession = sessionManager.getActiveSessionId();
discontinuityFromPositionMs = oldPositionInfo.positionMs;
} }
discontinuityReason = reason;
@Override
public void onSeekStarted(EventTime eventTime) {
onSeekStartedEventTime = eventTime;
} }
@Override @Override
@ -253,9 +244,7 @@ public final class PlaybackStatsListener
for (String session : playbackStatsTrackers.keySet()) { for (String session : playbackStatsTrackers.keySet()) {
Pair<EventTime, Boolean> eventTimeAndBelongsToPlayback = findBestEventTime(events, session); Pair<EventTime, Boolean> eventTimeAndBelongsToPlayback = findBestEventTime(events, session);
PlaybackStatsTracker tracker = playbackStatsTrackers.get(session); PlaybackStatsTracker tracker = playbackStatsTrackers.get(session);
boolean hasPositionDiscontinuity = boolean hasDiscontinuityToPlayback = hasEvent(events, session, EVENT_POSITION_DISCONTINUITY);
hasEvent(events, session, EVENT_POSITION_DISCONTINUITY)
|| hasEvent(events, session, EVENT_TIMELINE_CHANGED);
boolean hasDroppedFrames = hasEvent(events, session, EVENT_DROPPED_VIDEO_FRAMES); boolean hasDroppedFrames = hasEvent(events, session, EVENT_DROPPED_VIDEO_FRAMES);
boolean hasAudioUnderrun = hasEvent(events, session, EVENT_AUDIO_UNDERRUN); boolean hasAudioUnderrun = hasEvent(events, session, EVENT_AUDIO_UNDERRUN);
boolean startedLoading = hasEvent(events, session, EVENT_LOAD_STARTED); boolean startedLoading = hasEvent(events, session, EVENT_LOAD_STARTED);
@ -270,8 +259,8 @@ public final class PlaybackStatsListener
player, player,
/* eventTime= */ eventTimeAndBelongsToPlayback.first, /* eventTime= */ eventTimeAndBelongsToPlayback.first,
/* belongsToPlayback= */ eventTimeAndBelongsToPlayback.second, /* belongsToPlayback= */ eventTimeAndBelongsToPlayback.second,
/* seeked= */ onSeekStartedEventTime != null, session.equals(discontinuityFromSession) ? discontinuityFromPositionMs : C.TIME_UNSET,
hasPositionDiscontinuity, hasDiscontinuityToPlayback,
hasDroppedFrames ? droppedFrames : 0, hasDroppedFrames ? droppedFrames : 0,
hasAudioUnderrun, hasAudioUnderrun,
startedLoading, startedLoading,
@ -284,9 +273,9 @@ public final class PlaybackStatsListener
hasVideoSize ? videoHeight : Format.NO_VALUE, hasVideoSize ? videoHeight : Format.NO_VALUE,
hasVideoSize ? videoWidth : Format.NO_VALUE); hasVideoSize ? videoWidth : Format.NO_VALUE);
} }
onSeekStartedEventTime = null;
videoFormat = null; videoFormat = null;
audioFormat = null; audioFormat = null;
discontinuityFromSession = null;
if (events.contains(AnalyticsListener.EVENT_PLAYER_RELEASED)) { if (events.contains(AnalyticsListener.EVENT_PLAYER_RELEASED)) {
sessionManager.finishAllSessions(events.getEventTime(EVENT_PLAYER_RELEASED)); sessionManager.finishAllSessions(events.getEventTime(EVENT_PLAYER_RELEASED));
} }
@ -311,11 +300,8 @@ public final class PlaybackStatsListener
} }
private Pair<EventTime, Boolean> findBestEventTime(Events events, String session) { 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 = null;
@Nullable EventTime eventTime = onSeekStartedEventTime; boolean belongsToPlayback = false;
boolean belongsToPlayback =
onSeekStartedEventTime != null
&& sessionManager.belongsToSession(onSeekStartedEventTime, session);
for (int i = 0; i < events.size(); i++) { for (int i = 0; i < events.size(); i++) {
@EventFlags int event = events.get(i); @EventFlags int event = events.get(i);
EventTime newEventTime = events.getEventTime(event); EventTime newEventTime = events.getEventTime(event);
@ -463,15 +449,18 @@ public final class PlaybackStatsListener
* @param eventTime The {@link EventTime}. Does not belong to this playback. * @param eventTime The {@link EventTime}. Does not belong to this playback.
* @param automaticTransition Whether the playback finished because of an automatic transition * @param automaticTransition Whether the playback finished because of an automatic transition
* to the next playback item. * to the next playback item.
* @param discontinuityFromPositionMs The position before the discontinuity from this playback,
* {@link C#TIME_UNSET} if no discontinuity started from this playback.
*/ */
public void onFinished(EventTime eventTime, boolean automaticTransition) { public void onFinished(
EventTime eventTime, boolean automaticTransition, long discontinuityFromPositionMs) {
// Simulate state change to ENDED to record natural ending of playback. // Simulate state change to ENDED to record natural ending of playback.
@PlaybackState @PlaybackState
int finalPlaybackState = int finalPlaybackState =
currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED || automaticTransition currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED || automaticTransition
? PlaybackStats.PLAYBACK_STATE_ENDED ? PlaybackStats.PLAYBACK_STATE_ENDED
: PlaybackStats.PLAYBACK_STATE_ABANDONED; : PlaybackStats.PLAYBACK_STATE_ABANDONED;
maybeUpdateMediaTimeHistory(eventTime.realtimeMs, /* mediaTimeMs= */ C.TIME_UNSET); maybeUpdateMediaTimeHistory(eventTime.realtimeMs, discontinuityFromPositionMs);
maybeRecordVideoFormatTime(eventTime.realtimeMs); maybeRecordVideoFormatTime(eventTime.realtimeMs);
maybeRecordAudioFormatTime(eventTime.realtimeMs); maybeRecordAudioFormatTime(eventTime.realtimeMs);
updatePlaybackState(finalPlaybackState, eventTime); updatePlaybackState(finalPlaybackState, eventTime);
@ -483,8 +472,9 @@ public final class PlaybackStatsListener
* @param player The {@link Player}. * @param player The {@link Player}.
* @param eventTime The {@link EventTime} of the events. * @param eventTime The {@link EventTime} of the events.
* @param belongsToPlayback Whether the {@code eventTime} belongs to this playback. * @param belongsToPlayback Whether the {@code eventTime} belongs to this playback.
* @param seeked Whether a seek occurred. * @param discontinuityFromPositionMs The position before the discontinuity from this playback,
* @param positionDiscontinuity Whether a position discontinuity occurred for this playback. * or {@link C#TIME_UNSET} if no discontinuity started from this playback.
* @param hasDiscontinuity Whether a discontinuity to this playback occurred.
* @param droppedFrameCount The number of newly dropped frames 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 hasAudioUnderun Whether a new audio underrun occurred for this playback.
* @param startedLoading Whether this playback started loading. * @param startedLoading Whether this playback started loading.
@ -501,8 +491,8 @@ public final class PlaybackStatsListener
Player player, Player player,
EventTime eventTime, EventTime eventTime,
boolean belongsToPlayback, boolean belongsToPlayback,
boolean seeked, long discontinuityFromPositionMs,
boolean positionDiscontinuity, boolean hasDiscontinuity,
int droppedFrameCount, int droppedFrameCount,
boolean hasAudioUnderun, boolean hasAudioUnderun,
boolean startedLoading, boolean startedLoading,
@ -514,7 +504,8 @@ public final class PlaybackStatsListener
@Nullable Format audioFormat, @Nullable Format audioFormat,
int videoHeight, int videoHeight,
int videoWidth) { int videoWidth) {
if (seeked) { if (discontinuityFromPositionMs != C.TIME_UNSET) {
maybeUpdateMediaTimeHistory(eventTime.realtimeMs, discontinuityFromPositionMs);
isSeeking = true; isSeeking = true;
} }
if (player.getPlaybackState() != Player.STATE_BUFFERING) { if (player.getPlaybackState() != Player.STATE_BUFFERING) {
@ -523,7 +514,7 @@ public final class PlaybackStatsListener
int playerPlaybackState = player.getPlaybackState(); int playerPlaybackState = player.getPlaybackState();
if (playerPlaybackState == Player.STATE_IDLE if (playerPlaybackState == Player.STATE_IDLE
|| playerPlaybackState == Player.STATE_ENDED || playerPlaybackState == Player.STATE_ENDED
|| positionDiscontinuity) { || hasDiscontinuity) {
isInterruptedByAd = false; isInterruptedByAd = false;
} }
if (fatalError != null) { if (fatalError != null) {

View File

@ -187,11 +187,6 @@ public class EventLogger implements AnalyticsListener {
logd(eventTime, "positionDiscontinuity", builder.toString()); logd(eventTime, "positionDiscontinuity", builder.toString());
} }
@Override
public void onSeekStarted(EventTime eventTime) {
logd(eventTime, "seekStarted");
}
@Override @Override
public void onPlaybackParametersChanged( public void onPlaybackParametersChanged(
EventTime eventTime, PlaybackParameters playbackParameters) { EventTime eventTime, PlaybackParameters playbackParameters) {