Commit 27c239d6 by tonihei Committed by Ian Baker

Directly track playback queue in AnalyticsCollector.

We currently try to keep track of the playback queue (MediaPeriodQueue)
by listening to onMediaPeriodCreated/onMediaPeriodReleased events.
This approach has some problems:
 1. It's easily broken by custom MediaSources that don't report these
    events correctly.
 2. We need to make some assumptions about what the order of these
    events actually means. For example it is currently important that
    the playing period gets released last in MediaPeriodQueue.clear()
 3. We don't see batched events (like MediaPeriodQueue.clear()), so that
    it is impossible to keep the "last reading period" for example. This
    information is needed to correctly associate renderer errors to
    periods after the queue has been cleared.

All of these problems can be solved by directly tracking the queue.

This also makes the onMediaPeriodCreated/Released/ReadingStarted events
obsolete and they can be removed in a future change.

PiperOrigin-RevId: 319739993
parent b30e2b96
...@@ -173,7 +173,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -173,7 +173,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
this.pauseAtEndOfWindow = pauseAtEndOfWindow; this.pauseAtEndOfWindow = pauseAtEndOfWindow;
this.eventHandler = eventHandler; this.eventHandler = eventHandler;
this.clock = clock; this.clock = clock;
this.queue = new MediaPeriodQueue(); this.queue = new MediaPeriodQueue(analyticsCollector, eventHandler);
backBufferDurationUs = loadControl.getBackBufferDurationUs(); backBufferDurationUs = loadControl.getBackBufferDurationUs();
retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe();
......
...@@ -15,15 +15,18 @@ ...@@ -15,15 +15,18 @@
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import android.os.Handler;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.common.collect.ImmutableList;
/** /**
* Holds a queue of media periods, from the currently playing media period at the front to the * Holds a queue of media periods, from the currently playing media period at the front to the
...@@ -41,6 +44,8 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -41,6 +44,8 @@ import com.google.android.exoplayer2.util.Assertions;
private final Timeline.Period period; private final Timeline.Period period;
private final Timeline.Window window; private final Timeline.Window window;
@Nullable private final AnalyticsCollector analyticsCollector;
private final Handler analyticsCollectorHandler;
private long nextWindowSequenceNumber; private long nextWindowSequenceNumber;
private @RepeatMode int repeatMode; private @RepeatMode int repeatMode;
...@@ -52,8 +57,18 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -52,8 +57,18 @@ import com.google.android.exoplayer2.util.Assertions;
@Nullable private Object oldFrontPeriodUid; @Nullable private Object oldFrontPeriodUid;
private long oldFrontPeriodWindowSequenceNumber; private long oldFrontPeriodWindowSequenceNumber;
/** Creates a new media period queue. */ /**
public MediaPeriodQueue() { * Creates a new media period queue.
*
* @param analyticsCollector An optional {@link AnalyticsCollector} to be informed of queue
* changes.
* @param analyticsCollectorHandler The {@link Handler} to call {@link AnalyticsCollector} methods
* on.
*/
public MediaPeriodQueue(
@Nullable AnalyticsCollector analyticsCollector, Handler analyticsCollectorHandler) {
this.analyticsCollector = analyticsCollector;
this.analyticsCollectorHandler = analyticsCollectorHandler;
period = new Timeline.Period(); period = new Timeline.Period();
window = new Timeline.Window(); window = new Timeline.Window();
} }
...@@ -168,6 +183,7 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -168,6 +183,7 @@ import com.google.android.exoplayer2.util.Assertions;
oldFrontPeriodUid = null; oldFrontPeriodUid = null;
loading = newPeriodHolder; loading = newPeriodHolder;
length++; length++;
notifyQueueUpdate();
return newPeriodHolder; return newPeriodHolder;
} }
...@@ -203,6 +219,7 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -203,6 +219,7 @@ import com.google.android.exoplayer2.util.Assertions;
public MediaPeriodHolder advanceReadingPeriod() { public MediaPeriodHolder advanceReadingPeriod() {
Assertions.checkState(reading != null && reading.getNext() != null); Assertions.checkState(reading != null && reading.getNext() != null);
reading = reading.getNext(); reading = reading.getNext();
notifyQueueUpdate();
return reading; return reading;
} }
...@@ -228,6 +245,7 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -228,6 +245,7 @@ import com.google.android.exoplayer2.util.Assertions;
oldFrontPeriodWindowSequenceNumber = playing.info.id.windowSequenceNumber; oldFrontPeriodWindowSequenceNumber = playing.info.id.windowSequenceNumber;
} }
playing = playing.getNext(); playing = playing.getNext();
notifyQueueUpdate();
return playing; return playing;
} }
...@@ -241,6 +259,9 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -241,6 +259,9 @@ import com.google.android.exoplayer2.util.Assertions;
*/ */
public boolean removeAfter(MediaPeriodHolder mediaPeriodHolder) { public boolean removeAfter(MediaPeriodHolder mediaPeriodHolder) {
Assertions.checkState(mediaPeriodHolder != null); Assertions.checkState(mediaPeriodHolder != null);
if (mediaPeriodHolder.equals(loading)) {
return false;
}
boolean removedReading = false; boolean removedReading = false;
loading = mediaPeriodHolder; loading = mediaPeriodHolder;
while (mediaPeriodHolder.getNext() != null) { while (mediaPeriodHolder.getNext() != null) {
...@@ -253,22 +274,27 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -253,22 +274,27 @@ import com.google.android.exoplayer2.util.Assertions;
length--; length--;
} }
loading.setNext(null); loading.setNext(null);
notifyQueueUpdate();
return removedReading; return removedReading;
} }
/** Clears the queue. */ /** Clears the queue. */
public void clear() { public void clear() {
MediaPeriodHolder front = playing; if (length == 0) {
if (front != null) { return;
}
MediaPeriodHolder front = Assertions.checkStateNotNull(playing);
oldFrontPeriodUid = front.uid; oldFrontPeriodUid = front.uid;
oldFrontPeriodWindowSequenceNumber = front.info.id.windowSequenceNumber; oldFrontPeriodWindowSequenceNumber = front.info.id.windowSequenceNumber;
removeAfter(front); while (front != null) {
front.release(); front.release();
front = front.getNext();
} }
playing = null; playing = null;
loading = null; loading = null;
reading = null; reading = null;
length = 0; length = 0;
notifyQueueUpdate();
} }
/** /**
...@@ -392,6 +418,20 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -392,6 +418,20 @@ import com.google.android.exoplayer2.util.Assertions;
// Internal methods. // Internal methods.
private void notifyQueueUpdate() {
if (analyticsCollector != null) {
ImmutableList.Builder<MediaPeriodId> builder = ImmutableList.builder();
@Nullable MediaPeriodHolder period = playing;
while (period != null) {
builder.add(period.info.id);
period = period.getNext();
}
@Nullable MediaPeriodId readingPeriodId = reading == null ? null : reading.info.id;
analyticsCollectorHandler.post(
() -> analyticsCollector.updateMediaPeriodQueueInfo(builder.build(), readingPeriodId));
}
}
/** /**
* Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be * Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be
* played, returning an identifier for an ad group if one needs to be played before the specified * played, returning an identifier for an ad group if one needs to be played before the specified
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.analytics; package com.google.android.exoplayer2.analytics;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
...@@ -45,12 +47,12 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -45,12 +47,12 @@ import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.video.VideoListener; import com.google.android.exoplayer2.video.VideoListener;
import com.google.android.exoplayer2.video.VideoRendererEventListener; import com.google.android.exoplayer2.video.VideoRendererEventListener;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull;
...@@ -72,6 +74,7 @@ public class AnalyticsCollector ...@@ -72,6 +74,7 @@ public class AnalyticsCollector
private final CopyOnWriteArraySet<AnalyticsListener> listeners; private final CopyOnWriteArraySet<AnalyticsListener> listeners;
private final Clock clock; private final Clock clock;
private final Period period;
private final Window window; private final Window window;
private final MediaPeriodQueueTracker mediaPeriodQueueTracker; private final MediaPeriodQueueTracker mediaPeriodQueueTracker;
...@@ -84,10 +87,11 @@ public class AnalyticsCollector ...@@ -84,10 +87,11 @@ public class AnalyticsCollector
* @param clock A {@link Clock} used to generate timestamps. * @param clock A {@link Clock} used to generate timestamps.
*/ */
public AnalyticsCollector(Clock clock) { public AnalyticsCollector(Clock clock) {
this.clock = Assertions.checkNotNull(clock); this.clock = checkNotNull(clock);
listeners = new CopyOnWriteArraySet<>(); listeners = new CopyOnWriteArraySet<>();
mediaPeriodQueueTracker = new MediaPeriodQueueTracker(); period = new Period();
window = new Window(); window = new Window();
mediaPeriodQueueTracker = new MediaPeriodQueueTracker(period);
} }
/** /**
...@@ -116,8 +120,22 @@ public class AnalyticsCollector ...@@ -116,8 +120,22 @@ public class AnalyticsCollector
*/ */
public void setPlayer(Player player) { public void setPlayer(Player player) {
Assertions.checkState( Assertions.checkState(
this.player == null || mediaPeriodQueueTracker.mediaPeriodInfoQueue.isEmpty()); this.player == null || mediaPeriodQueueTracker.mediaPeriodQueue.isEmpty());
this.player = Assertions.checkNotNull(player); this.player = checkNotNull(player);
}
/**
* Updates the playback queue information used for event association.
*
* <p>Should only be called by the player controlling the queue and not from app code.
*
* @param queue The playback queue of media periods identified by their {@link MediaPeriodId}.
* @param readingPeriod The media period in the queue that is currently being read by renderers,
* or null if the queue is empty.
*/
public void updateMediaPeriodQueueInfo(
List<MediaPeriodId> queue, @Nullable MediaPeriodId readingPeriod) {
mediaPeriodQueueTracker.onQueueUpdated(queue, readingPeriod, checkNotNull(player));
} }
// External events. // External events.
...@@ -138,12 +156,7 @@ public class AnalyticsCollector ...@@ -138,12 +156,7 @@ public class AnalyticsCollector
/** Resets the analytics collector for a new playlist. */ /** Resets the analytics collector for a new playlist. */
public final void resetForNewPlaylist() { public final void resetForNewPlaylist() {
// Copying the list is needed because onMediaPeriodReleased will modify the list. // TODO: remove method.
List<MediaPeriodInfo> mediaPeriodInfos =
new ArrayList<>(mediaPeriodQueueTracker.mediaPeriodInfoQueue);
for (MediaPeriodInfo mediaPeriodInfo : mediaPeriodInfos) {
onMediaPeriodReleased(mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId);
}
} }
// MetadataOutput implementation. // MetadataOutput implementation.
...@@ -325,9 +338,17 @@ public class AnalyticsCollector ...@@ -325,9 +338,17 @@ public class AnalyticsCollector
@Override @Override
public final void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { public final void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {
mediaPeriodQueueTracker.onMediaPeriodCreated( // TODO: Remove this method, as it's no longer needed for queue tracking.
windowIndex, mediaPeriodId, Assertions.checkNotNull(player)); // We won't find this media period in the tracked queue yet because onQueueUpdated is called
EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); // after this method. Try to use the current timeline directly if possible.
Timeline timeline = checkNotNull(player).getCurrentTimeline();
EventTime eventTime =
timeline.getIndexOfPeriod(mediaPeriodId.periodUid) != C.INDEX_UNSET
? generateEventTime(
timeline,
timeline.getPeriodByUid(mediaPeriodId.periodUid, period).windowIndex,
mediaPeriodId)
: generateEventTime(Timeline.EMPTY, windowIndex, mediaPeriodId);
for (AnalyticsListener listener : listeners) { for (AnalyticsListener listener : listeners) {
listener.onMediaPeriodCreated(eventTime); listener.onMediaPeriodCreated(eventTime);
} }
...@@ -335,14 +356,12 @@ public class AnalyticsCollector ...@@ -335,14 +356,12 @@ public class AnalyticsCollector
@Override @Override
public final void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { public final void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {
// TODO: Remove this method, as it's no longer needed for queue tracking.
EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
if (mediaPeriodQueueTracker.onMediaPeriodReleased(
mediaPeriodId, Assertions.checkNotNull(player))) {
for (AnalyticsListener listener : listeners) { for (AnalyticsListener listener : listeners) {
listener.onMediaPeriodReleased(eventTime); listener.onMediaPeriodReleased(eventTime);
} }
} }
}
@Override @Override
public final void onLoadStarted( public final void onLoadStarted(
...@@ -396,7 +415,7 @@ public class AnalyticsCollector ...@@ -396,7 +415,7 @@ public class AnalyticsCollector
@Override @Override
public final void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) { public final void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {
mediaPeriodQueueTracker.onReadingStarted(mediaPeriodId); // TODO: Remove this method, as it's no longer needed for queue tracking.
EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
for (AnalyticsListener listener : listeners) { for (AnalyticsListener listener : listeners) {
listener.onReadingStarted(eventTime); listener.onReadingStarted(eventTime);
...@@ -429,7 +448,7 @@ public class AnalyticsCollector ...@@ -429,7 +448,7 @@ public class AnalyticsCollector
@Override @Override
public final void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { public final void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
mediaPeriodQueueTracker.onTimelineChanged(timeline, Assertions.checkNotNull(player)); mediaPeriodQueueTracker.onTimelineChanged(checkNotNull(player));
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) { for (AnalyticsListener listener : listeners) {
listener.onTimelineChanged(eventTime, reason); listener.onTimelineChanged(eventTime, reason);
...@@ -525,7 +544,7 @@ public class AnalyticsCollector ...@@ -525,7 +544,7 @@ public class AnalyticsCollector
if (reason == Player.DISCONTINUITY_REASON_SEEK) { if (reason == Player.DISCONTINUITY_REASON_SEEK) {
isSeeking = false; isSeeking = false;
} }
mediaPeriodQueueTracker.onPositionDiscontinuity(Assertions.checkNotNull(player)); mediaPeriodQueueTracker.onPositionDiscontinuity(checkNotNull(player));
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) { for (AnalyticsListener listener : listeners) {
listener.onPositionDiscontinuity(eventTime, reason); listener.onPositionDiscontinuity(eventTime, reason);
...@@ -626,10 +645,6 @@ public class AnalyticsCollector ...@@ -626,10 +645,6 @@ public class AnalyticsCollector
// Internal methods. // Internal methods.
/** Returns read-only set of registered listeners. */
protected Set<AnalyticsListener> getListeners() {
return Collections.unmodifiableSet(listeners);
}
/** Returns a new {@link EventTime} for the specified timeline, window and media period id. */ /** Returns a new {@link EventTime} for the specified timeline, window and media period id. */
@RequiresNonNull("player") @RequiresNonNull("player")
...@@ -659,7 +674,8 @@ public class AnalyticsCollector ...@@ -659,7 +674,8 @@ public class AnalyticsCollector
eventPositionMs = eventPositionMs =
timeline.isEmpty() ? 0 : timeline.getWindow(windowIndex, window).getDefaultPositionMs(); timeline.isEmpty() ? 0 : timeline.getWindow(windowIndex, window).getDefaultPositionMs();
} }
@Nullable MediaPeriodInfo currentInfo = mediaPeriodQueueTracker.getCurrentPlayerMediaPeriod(); @Nullable
MediaPeriodId currentMediaPeriodId = mediaPeriodQueueTracker.getCurrentPlayerMediaPeriod();
return new EventTime( return new EventTime(
realtimeMs, realtimeMs,
timeline, timeline,
...@@ -668,22 +684,27 @@ public class AnalyticsCollector ...@@ -668,22 +684,27 @@ public class AnalyticsCollector
eventPositionMs, eventPositionMs,
player.getCurrentTimeline(), player.getCurrentTimeline(),
player.getCurrentWindowIndex(), player.getCurrentWindowIndex(),
currentInfo == null ? null : currentInfo.mediaPeriodId, currentMediaPeriodId,
player.getCurrentPosition(), player.getCurrentPosition(),
player.getTotalBufferedDuration()); player.getTotalBufferedDuration());
} }
private EventTime generateEventTime(@Nullable MediaPeriodInfo mediaPeriodInfo) { private EventTime generateEventTime(@Nullable MediaPeriodId mediaPeriodId) {
Assertions.checkNotNull(player); checkNotNull(player);
if (mediaPeriodInfo == null) { @Nullable
Timeline knownTimeline =
mediaPeriodId == null
? null
: mediaPeriodQueueTracker.getMediaPeriodIdTimeline(mediaPeriodId);
if (mediaPeriodId == null || knownTimeline == null) {
int windowIndex = player.getCurrentWindowIndex(); int windowIndex = player.getCurrentWindowIndex();
Timeline timeline = player.getCurrentTimeline(); Timeline timeline = player.getCurrentTimeline();
boolean windowIsInTimeline = windowIndex < timeline.getWindowCount(); boolean windowIsInTimeline = windowIndex < timeline.getWindowCount();
return generateEventTime( return generateEventTime(
windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null); windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null);
} }
return generateEventTime( int windowIndex = knownTimeline.getPeriodByUid(mediaPeriodId.periodUid, period).windowIndex;
mediaPeriodInfo.timeline, mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId); return generateEventTime(knownTimeline, windowIndex, mediaPeriodId);
} }
private EventTime generateCurrentPlayerMediaPeriodEventTime() { private EventTime generateCurrentPlayerMediaPeriodEventTime() {
...@@ -704,11 +725,12 @@ public class AnalyticsCollector ...@@ -704,11 +725,12 @@ public class AnalyticsCollector
private EventTime generateMediaPeriodEventTime( private EventTime generateMediaPeriodEventTime(
int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
Assertions.checkNotNull(player); checkNotNull(player);
if (mediaPeriodId != null) { if (mediaPeriodId != null) {
MediaPeriodInfo mediaPeriodInfo = mediaPeriodQueueTracker.getMediaPeriodInfo(mediaPeriodId); boolean isInKnownTimeline =
return mediaPeriodInfo != null mediaPeriodQueueTracker.getMediaPeriodIdTimeline(mediaPeriodId) != null;
? generateEventTime(mediaPeriodInfo) return isInKnownTimeline
? generateEventTime(mediaPeriodId)
: generateEventTime(Timeline.EMPTY, windowIndex, mediaPeriodId); : generateEventTime(Timeline.EMPTY, windowIndex, mediaPeriodId);
} }
Timeline timeline = player.getCurrentTimeline(); Timeline timeline = player.getCurrentTimeline();
...@@ -720,161 +742,149 @@ public class AnalyticsCollector ...@@ -720,161 +742,149 @@ public class AnalyticsCollector
/** Keeps track of the active media periods and currently playing and reading media period. */ /** Keeps track of the active media periods and currently playing and reading media period. */
private static final class MediaPeriodQueueTracker { private static final class MediaPeriodQueueTracker {
// TODO: Investigate reporting MediaPeriodId in renderer events and adding a listener of queue // TODO: Investigate reporting MediaPeriodId in renderer events.
// changes, which would hopefully remove the need to track the queue here.
private final ArrayList<MediaPeriodInfo> mediaPeriodInfoQueue;
private final HashMap<MediaPeriodId, MediaPeriodInfo> mediaPeriodIdToInfo;
private final Period period; private final Period period;
@Nullable private MediaPeriodInfo currentPlayerMediaPeriod; private ImmutableList<MediaPeriodId> mediaPeriodQueue;
private @MonotonicNonNull MediaPeriodInfo playingMediaPeriod; private ImmutableMap<MediaPeriodId, Timeline> mediaPeriodTimelines;
private @MonotonicNonNull MediaPeriodInfo readingMediaPeriod; @Nullable private MediaPeriodId currentPlayerMediaPeriod;
private Timeline timeline; private @MonotonicNonNull MediaPeriodId playingMediaPeriod;
private @MonotonicNonNull MediaPeriodId readingMediaPeriod;
public MediaPeriodQueueTracker() { public MediaPeriodQueueTracker(Period period) {
mediaPeriodInfoQueue = new ArrayList<>(); this.period = period;
mediaPeriodIdToInfo = new HashMap<>(); mediaPeriodQueue = ImmutableList.of();
period = new Period(); mediaPeriodTimelines = ImmutableMap.of();
timeline = Timeline.EMPTY;
} }
/** /**
* Returns the {@link MediaPeriodInfo} of the media period corresponding the current position of * Returns the {@link MediaPeriodId} of the media period corresponding the current position of
* the player. * the player.
* *
* <p>May be null if no matching media period has been created yet. * <p>May be null if no matching media period has been created yet.
*/ */
@Nullable @Nullable
public MediaPeriodInfo getCurrentPlayerMediaPeriod() { public MediaPeriodId getCurrentPlayerMediaPeriod() {
return currentPlayerMediaPeriod; return currentPlayerMediaPeriod;
} }
/** /**
* Returns the {@link MediaPeriodInfo} of the media period at the front of the queue. If the * Returns the {@link MediaPeriodId} of the media period at the front of the queue. If the queue
* queue is empty, this is the last media period which was at the front of the queue. * is empty, this is the last media period which was at the front of the queue.
* *
* <p>May be null, if no media period has been created yet. * <p>May be null, if no media period has been created yet.
*/ */
@Nullable @Nullable
public MediaPeriodInfo getPlayingMediaPeriod() { public MediaPeriodId getPlayingMediaPeriod() {
return playingMediaPeriod; return playingMediaPeriod;
} }
/** /**
* Returns the {@link MediaPeriodInfo} of the media period currently being read by the player. * Returns the {@link MediaPeriodId} of the media period currently being read by the player. If
* the queue is empty, this is the last media period which was read by the player.
* *
* <p>May be null, if the player has not started reading any media period. * <p>May be null, if no media period has been created yet.
*/ */
@Nullable @Nullable
public MediaPeriodInfo getReadingMediaPeriod() { public MediaPeriodId getReadingMediaPeriod() {
return readingMediaPeriod; return readingMediaPeriod;
} }
/** /**
* Returns the {@link MediaPeriodInfo} of the media period at the end of the queue which is * Returns the {@link MediaPeriodId} of the media period at the end of the queue which is
* currently loading or will be the next one loading. * currently loading or will be the next one loading.
* *
* <p>May be null, if no media period is active yet. * <p>May be null, if no media period is active yet.
*/ */
@Nullable @Nullable
public MediaPeriodInfo getLoadingMediaPeriod() { public MediaPeriodId getLoadingMediaPeriod() {
return mediaPeriodInfoQueue.isEmpty() return mediaPeriodQueue.isEmpty() ? null : Iterables.getLast(mediaPeriodQueue);
? null
: mediaPeriodInfoQueue.get(mediaPeriodInfoQueue.size() - 1);
} }
/** Returns the {@link MediaPeriodInfo} for the given {@link MediaPeriodId}. */ /**
* Returns the most recent {@link Timeline} for the given {@link MediaPeriodId}, or null if no
* timeline is available.
*/
@Nullable @Nullable
public MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId mediaPeriodId) { public Timeline getMediaPeriodIdTimeline(MediaPeriodId mediaPeriodId) {
return mediaPeriodIdToInfo.get(mediaPeriodId); return mediaPeriodTimelines.get(mediaPeriodId);
} }
/** Updates the queue with a reported position discontinuity. */ /** Updates the queue tracker with a reported position discontinuity. */
public void onPositionDiscontinuity(Player player) { public void onPositionDiscontinuity(Player player) {
currentPlayerMediaPeriod = findMatchingMediaPeriodInQueue(player); currentPlayerMediaPeriod =
findCurrentPlayerMediaPeriodInQueue(player, mediaPeriodQueue, playingMediaPeriod, period);
} }
/** Updates the queue with a reported timeline change. */ /** Updates the queue tracker with a reported timeline change. */
public void onTimelineChanged(Timeline timeline, Player player) { public void onTimelineChanged(Player player) {
for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) { currentPlayerMediaPeriod =
MediaPeriodInfo newMediaPeriodInfo = findCurrentPlayerMediaPeriodInQueue(player, mediaPeriodQueue, playingMediaPeriod, period);
updateMediaPeriodInfoToNewTimeline(mediaPeriodInfoQueue.get(i), timeline); updateMediaPeriodTimelines(/* preferredTimeline= */ player.getCurrentTimeline());
mediaPeriodInfoQueue.set(i, newMediaPeriodInfo);
mediaPeriodIdToInfo.put(newMediaPeriodInfo.mediaPeriodId, newMediaPeriodInfo);
}
if (!mediaPeriodInfoQueue.isEmpty()) {
playingMediaPeriod = mediaPeriodInfoQueue.get(0);
} else if (playingMediaPeriod != null) {
playingMediaPeriod = updateMediaPeriodInfoToNewTimeline(playingMediaPeriod, timeline);
}
if (readingMediaPeriod != null) {
readingMediaPeriod = updateMediaPeriodInfoToNewTimeline(readingMediaPeriod, timeline);
} else if (playingMediaPeriod != null) {
readingMediaPeriod = playingMediaPeriod;
}
this.timeline = timeline;
currentPlayerMediaPeriod = findMatchingMediaPeriodInQueue(player);
} }
/** Updates the queue with a newly created media period. */ /** Updates the queue tracker to a new queue of media periods. */
public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId, Player player) { public void onQueueUpdated(
int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid); List<MediaPeriodId> queue, @Nullable MediaPeriodId readingPeriod, Player player) {
boolean isInTimeline = periodIndex != C.INDEX_UNSET; mediaPeriodQueue = ImmutableList.copyOf(queue);
MediaPeriodInfo mediaPeriodInfo = if (!queue.isEmpty()) {
new MediaPeriodInfo( playingMediaPeriod = queue.get(0);
mediaPeriodId, readingMediaPeriod = checkNotNull(readingPeriod);
isInTimeline ? timeline : Timeline.EMPTY,
isInTimeline ? timeline.getPeriod(periodIndex, period).windowIndex : windowIndex);
mediaPeriodInfoQueue.add(mediaPeriodInfo);
mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo);
playingMediaPeriod = mediaPeriodInfoQueue.get(0);
if (currentPlayerMediaPeriod == null && isMatchingPlayingMediaPeriod(player)) {
currentPlayerMediaPeriod = playingMediaPeriod;
} }
if (mediaPeriodInfoQueue.size() == 1) { if (currentPlayerMediaPeriod == null) {
readingMediaPeriod = playingMediaPeriod; currentPlayerMediaPeriod =
findCurrentPlayerMediaPeriodInQueue(
player, mediaPeriodQueue, playingMediaPeriod, period);
} }
updateMediaPeriodTimelines(/* preferredTimeline= */ player.getCurrentTimeline());
} }
/** private void updateMediaPeriodTimelines(Timeline preferredTimeline) {
* Updates the queue with a released media period. Returns whether the media period was still in ImmutableMap.Builder<MediaPeriodId, Timeline> builder = ImmutableMap.builder();
* the queue. if (mediaPeriodQueue.isEmpty()) {
*/ addTimelineForMediaPeriodId(builder, playingMediaPeriod, preferredTimeline);
public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId, Player player) { if (!Objects.equal(readingMediaPeriod, playingMediaPeriod)) {
@Nullable MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId); addTimelineForMediaPeriodId(builder, readingMediaPeriod, preferredTimeline);
if (mediaPeriodInfo == null) {
// The media period has already been removed from the queue in resetForNewPlaylist().
return false;
} }
mediaPeriodInfoQueue.remove(mediaPeriodInfo); if (!Objects.equal(currentPlayerMediaPeriod, playingMediaPeriod)
if (readingMediaPeriod != null && mediaPeriodId.equals(readingMediaPeriod.mediaPeriodId)) { && !Objects.equal(currentPlayerMediaPeriod, readingMediaPeriod)) {
readingMediaPeriod = addTimelineForMediaPeriodId(builder, currentPlayerMediaPeriod, preferredTimeline);
mediaPeriodInfoQueue.isEmpty() }
? Assertions.checkNotNull(playingMediaPeriod) } else {
: mediaPeriodInfoQueue.get(0); for (int i = 0; i < mediaPeriodQueue.size(); i++) {
addTimelineForMediaPeriodId(builder, mediaPeriodQueue.get(i), preferredTimeline);
} }
if (!mediaPeriodInfoQueue.isEmpty()) { if (!mediaPeriodQueue.contains(currentPlayerMediaPeriod)) {
playingMediaPeriod = mediaPeriodInfoQueue.get(0); addTimelineForMediaPeriodId(builder, currentPlayerMediaPeriod, preferredTimeline);
} }
if (currentPlayerMediaPeriod == null && isMatchingPlayingMediaPeriod(player)) {
currentPlayerMediaPeriod = playingMediaPeriod;
} }
return true; mediaPeriodTimelines = builder.build();
} }
/** Update the queue with a change in the reading media period. */ private void addTimelineForMediaPeriodId(
public void onReadingStarted(MediaPeriodId mediaPeriodId) { ImmutableMap.Builder<MediaPeriodId, Timeline> mediaPeriodTimelinesBuilder,
@Nullable MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.get(mediaPeriodId); @Nullable MediaPeriodId mediaPeriodId,
if (mediaPeriodInfo == null) { Timeline preferredTimeline) {
// The media period has already been removed from the queue in resetForNewPlaylist(). if (mediaPeriodId == null) {
return; return;
} }
readingMediaPeriod = mediaPeriodInfo; if (preferredTimeline.getIndexOfPeriod(mediaPeriodId.periodUid) != C.INDEX_UNSET) {
mediaPeriodTimelinesBuilder.put(mediaPeriodId, preferredTimeline);
} else {
@Nullable Timeline existingTimeline = mediaPeriodTimelines.get(mediaPeriodId);
if (existingTimeline != null) {
mediaPeriodTimelinesBuilder.put(mediaPeriodId, existingTimeline);
}
}
} }
@Nullable @Nullable
private MediaPeriodInfo findMatchingMediaPeriodInQueue(Player player) { private static MediaPeriodId findCurrentPlayerMediaPeriodInQueue(
Player player,
ImmutableList<MediaPeriodId> mediaPeriodQueue,
@Nullable MediaPeriodId playingMediaPeriod,
Period period) {
Timeline playerTimeline = player.getCurrentTimeline(); Timeline playerTimeline = player.getCurrentTimeline();
int playerPeriodIndex = player.getCurrentPeriodIndex(); int playerPeriodIndex = player.getCurrentPeriodIndex();
@Nullable @Nullable
...@@ -887,25 +897,21 @@ public class AnalyticsCollector ...@@ -887,25 +897,21 @@ public class AnalyticsCollector
.getPeriod(playerPeriodIndex, period) .getPeriod(playerPeriodIndex, period)
.getAdGroupIndexAfterPositionUs( .getAdGroupIndexAfterPositionUs(
C.msToUs(player.getCurrentPosition()) - period.getPositionInWindowUs()); C.msToUs(player.getCurrentPosition()) - period.getPositionInWindowUs());
for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) { for (int i = 0; i < mediaPeriodQueue.size(); i++) {
MediaPeriodInfo mediaPeriodInfo = mediaPeriodInfoQueue.get(i); MediaPeriodId mediaPeriodId = mediaPeriodQueue.get(i);
if (isMatchingMediaPeriod( if (isMatchingMediaPeriod(
mediaPeriodInfo, mediaPeriodId,
playerTimeline,
player.getCurrentWindowIndex(),
playerPeriodUid, playerPeriodUid,
player.isPlayingAd(), player.isPlayingAd(),
player.getCurrentAdGroupIndex(), player.getCurrentAdGroupIndex(),
player.getCurrentAdIndexInAdGroup(), player.getCurrentAdIndexInAdGroup(),
playerNextAdGroupIndex)) { playerNextAdGroupIndex)) {
return mediaPeriodInfo; return mediaPeriodId;
} }
} }
if (mediaPeriodInfoQueue.isEmpty() && playingMediaPeriod != null) { if (mediaPeriodQueue.isEmpty() && playingMediaPeriod != null) {
if (isMatchingMediaPeriod( if (isMatchingMediaPeriod(
playingMediaPeriod, playingMediaPeriod,
playerTimeline,
player.getCurrentWindowIndex(),
playerPeriodUid, playerPeriodUid,
player.isPlayingAd(), player.isPlayingAd(),
player.getCurrentAdGroupIndex(), player.getCurrentAdGroupIndex(),
...@@ -917,89 +923,23 @@ public class AnalyticsCollector ...@@ -917,89 +923,23 @@ public class AnalyticsCollector
return null; return null;
} }
private boolean isMatchingPlayingMediaPeriod(Player player) {
if (playingMediaPeriod == null) {
return false;
}
Timeline playerTimeline = player.getCurrentTimeline();
int playerPeriodIndex = player.getCurrentPeriodIndex();
@Nullable
Object playerPeriodUid =
playerTimeline.isEmpty() ? null : playerTimeline.getUidOfPeriod(playerPeriodIndex);
int playerNextAdGroupIndex =
player.isPlayingAd() || playerTimeline.isEmpty()
? C.INDEX_UNSET
: playerTimeline
.getPeriod(playerPeriodIndex, period)
.getAdGroupIndexAfterPositionUs(
C.msToUs(player.getCurrentPosition()) - period.getPositionInWindowUs());
return isMatchingMediaPeriod(
playingMediaPeriod,
playerTimeline,
player.getCurrentWindowIndex(),
playerPeriodUid,
player.isPlayingAd(),
player.getCurrentAdGroupIndex(),
player.getCurrentAdIndexInAdGroup(),
playerNextAdGroupIndex);
}
private static boolean isMatchingMediaPeriod( private static boolean isMatchingMediaPeriod(
MediaPeriodInfo mediaPeriodInfo, MediaPeriodId mediaPeriodId,
Timeline playerTimeline,
int playerWindowIndex,
@Nullable Object playerPeriodUid, @Nullable Object playerPeriodUid,
boolean isPlayingAd, boolean isPlayingAd,
int playerAdGroupIndex, int playerAdGroupIndex,
int playerAdIndexInAdGroup, int playerAdIndexInAdGroup,
int playerNextAdGroupIndex) { int playerNextAdGroupIndex) {
if (mediaPeriodInfo.timeline.isEmpty() if (!mediaPeriodId.periodUid.equals(playerPeriodUid)) {
|| !mediaPeriodInfo.timeline.equals(playerTimeline)
|| mediaPeriodInfo.windowIndex != playerWindowIndex
|| !mediaPeriodInfo.mediaPeriodId.periodUid.equals(playerPeriodUid)) {
return false; return false;
} }
// Timeline period matches. Still need to check ad information. // Timeline period matches. Still need to check ad information.
return (isPlayingAd return (isPlayingAd
&& mediaPeriodInfo.mediaPeriodId.adGroupIndex == playerAdGroupIndex && mediaPeriodId.adGroupIndex == playerAdGroupIndex
&& mediaPeriodInfo.mediaPeriodId.adIndexInAdGroup == playerAdIndexInAdGroup) && mediaPeriodId.adIndexInAdGroup == playerAdIndexInAdGroup)
|| (!isPlayingAd || (!isPlayingAd
&& mediaPeriodInfo.mediaPeriodId.adGroupIndex == C.INDEX_UNSET && mediaPeriodId.adGroupIndex == C.INDEX_UNSET
&& mediaPeriodInfo.mediaPeriodId.nextAdGroupIndex == playerNextAdGroupIndex); && mediaPeriodId.nextAdGroupIndex == playerNextAdGroupIndex);
}
private MediaPeriodInfo updateMediaPeriodInfoToNewTimeline(
MediaPeriodInfo info, Timeline newTimeline) {
int newPeriodIndex = newTimeline.getIndexOfPeriod(info.mediaPeriodId.periodUid);
if (newPeriodIndex == C.INDEX_UNSET) {
// Media period is not yet or no longer available in the new timeline. Keep it as it is.
return info;
}
int newWindowIndex = newTimeline.getPeriod(newPeriodIndex, period).windowIndex;
return new MediaPeriodInfo(info.mediaPeriodId, newTimeline, newWindowIndex);
}
}
/** Information about a media period and its associated timeline. */
private static final class MediaPeriodInfo {
/** The {@link MediaPeriodId} of the media period. */
public final MediaPeriodId mediaPeriodId;
/**
* The {@link Timeline} in which the media period can be found. Or {@link Timeline#EMPTY} if the
* media period is not part of a known timeline yet.
*/
public final Timeline timeline;
/**
* The window index of the media period in the timeline. If the timeline is empty, this is the
* prospective window index.
*/
public final int windowIndex;
public MediaPeriodInfo(MediaPeriodId mediaPeriodId, Timeline timeline, int windowIndex) {
this.mediaPeriodId = mediaPeriodId;
this.timeline = timeline;
this.windowIndex = windowIndex;
} }
} }
} }
...@@ -74,7 +74,9 @@ public final class MediaPeriodQueueTest { ...@@ -74,7 +74,9 @@ public final class MediaPeriodQueueTest {
@Before @Before
public void setUp() { public void setUp() {
mediaPeriodQueue = new MediaPeriodQueue(); mediaPeriodQueue =
new MediaPeriodQueue(
/* analyticsCollector= */ null, Util.createHandlerForCurrentOrMainLooper());
mediaSourceList = mediaSourceList =
new MediaSourceList( new MediaSourceList(
mock(MediaSourceList.MediaSourceListInfoRefreshListener.class), mock(MediaSourceList.MediaSourceListInfoRefreshListener.class),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment