NekoX/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java

893 lines
33 KiB
Java

/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.analytics;
import android.graphics.SurfaceTexture;
import android.view.Surface;
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.Player.PlaybackSuppressionReason;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.Timeline.Window;
import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioListener;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.video.VideoListener;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* Data collector which is able to forward analytics events to {@link AnalyticsListener}s by
* listening to all available ExoPlayer listeners.
*/
public class AnalyticsCollector
implements Player.EventListener,
MetadataOutput,
AudioRendererEventListener,
VideoRendererEventListener,
MediaSourceEventListener,
BandwidthMeter.EventListener,
DefaultDrmSessionEventListener,
VideoListener,
AudioListener {
private final CopyOnWriteArraySet<AnalyticsListener> listeners;
private final Clock clock;
private final Window window;
private final MediaPeriodQueueTracker mediaPeriodQueueTracker;
private @MonotonicNonNull Player player;
/**
* Creates an analytics collector.
*
* @param clock A {@link Clock} used to generate timestamps.
*/
public AnalyticsCollector(Clock clock) {
this.clock = Assertions.checkNotNull(clock);
listeners = new CopyOnWriteArraySet<>();
mediaPeriodQueueTracker = new MediaPeriodQueueTracker();
window = new Window();
}
/**
* Adds a listener for analytics events.
*
* @param listener The listener to add.
*/
public void addListener(AnalyticsListener listener) {
listeners.add(listener);
}
/**
* Removes a previously added analytics event listener.
*
* @param listener The listener to remove.
*/
public void removeListener(AnalyticsListener listener) {
listeners.remove(listener);
}
/**
* Sets the player for which data will be collected. Must only be called if no player has been set
* yet or the current player is idle.
*
* @param player The {@link Player} for which data will be collected.
*/
public void setPlayer(Player player) {
Assertions.checkState(
this.player == null || mediaPeriodQueueTracker.mediaPeriodInfoQueue.isEmpty());
this.player = Assertions.checkNotNull(player);
}
// External events.
/**
* Notify analytics collector that a seek operation will start. Should be called before the player
* adjusts its state and position to the seek.
*/
public final void notifySeekStarted() {
if (!mediaPeriodQueueTracker.isSeeking()) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
mediaPeriodQueueTracker.onSeekStarted();
for (AnalyticsListener listener : listeners) {
listener.onSeekStarted(eventTime);
}
}
}
/**
* Resets the analytics collector for a new media source. Should be called before the player is
* prepared with a new media source.
*/
public final void resetForNewMediaSource() {
// Copying the list is needed because onMediaPeriodReleased will modify the list.
List<MediaPeriodInfo> mediaPeriodInfos =
new ArrayList<>(mediaPeriodQueueTracker.mediaPeriodInfoQueue);
for (MediaPeriodInfo mediaPeriodInfo : mediaPeriodInfos) {
onMediaPeriodReleased(mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId);
}
}
// MetadataOutput implementation.
@Override
public final void onMetadata(Metadata metadata) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onMetadata(eventTime, metadata);
}
}
// AudioRendererEventListener implementation.
@Override
public final void onAudioEnabled(DecoderCounters counters) {
// The renderers are only enabled after we changed the playing media period.
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_AUDIO, counters);
}
}
@Override
public final void onAudioDecoderInitialized(
String decoderName, long initializedTimestampMs, long initializationDurationMs) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDecoderInitialized(
eventTime, C.TRACK_TYPE_AUDIO, decoderName, initializationDurationMs);
}
}
@Override
public final void onAudioInputFormatChanged(Format format) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_AUDIO, format);
}
}
@Override
public final void onAudioSinkUnderrun(
int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onAudioUnderrun(eventTime, bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
}
}
@Override
public final void onAudioDisabled(DecoderCounters counters) {
// The renderers are disabled after we changed the playing media period on the playback thread
// but before this change is reported to the app thread.
EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_AUDIO, counters);
}
}
// AudioListener implementation.
@Override
public final void onAudioSessionId(int audioSessionId) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onAudioSessionId(eventTime, audioSessionId);
}
}
@Override
public void onAudioAttributesChanged(AudioAttributes audioAttributes) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onAudioAttributesChanged(eventTime, audioAttributes);
}
}
@Override
public void onVolumeChanged(float audioVolume) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onVolumeChanged(eventTime, audioVolume);
}
}
// VideoRendererEventListener implementation.
@Override
public final void onVideoEnabled(DecoderCounters counters) {
// The renderers are only enabled after we changed the playing media period.
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_VIDEO, counters);
}
}
@Override
public final void onVideoDecoderInitialized(
String decoderName, long initializedTimestampMs, long initializationDurationMs) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDecoderInitialized(
eventTime, C.TRACK_TYPE_VIDEO, decoderName, initializationDurationMs);
}
}
@Override
public final void onVideoInputFormatChanged(Format format) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_VIDEO, format);
}
}
@Override
public final void onDroppedFrames(int count, long elapsedMs) {
EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDroppedVideoFrames(eventTime, count, elapsedMs);
}
}
@Override
public final void onVideoDisabled(DecoderCounters counters) {
// The renderers are disabled after we changed the playing media period on the playback thread
// but before this change is reported to the app thread.
EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_VIDEO, counters);
}
}
@Override
public final void onRenderedFirstFrame(@Nullable Surface surface) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onRenderedFirstFrame(eventTime, surface);
}
}
// VideoListener implementation.
@Override
public final void onRenderedFirstFrame() {
// Do nothing. Already reported in VideoRendererEventListener.onRenderedFirstFrame.
}
@Override
public final void onVideoSizeChanged(
int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onVideoSizeChanged(
eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
}
}
@Override
public void onSurfaceSizeChanged(int width, int height) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onSurfaceSizeChanged(eventTime, width, height);
}
}
@Override
public boolean onSurfaceDestroyed(SurfaceTexture surfaceTexture) {
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
}
// MediaSourceEventListener implementation.
@Override
public final void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {
mediaPeriodQueueTracker.onMediaPeriodCreated(windowIndex, mediaPeriodId);
EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
for (AnalyticsListener listener : listeners) {
listener.onMediaPeriodCreated(eventTime);
}
}
@Override
public final void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {
EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
if (mediaPeriodQueueTracker.onMediaPeriodReleased(mediaPeriodId)) {
for (AnalyticsListener listener : listeners) {
listener.onMediaPeriodReleased(eventTime);
}
}
}
@Override
public final void onLoadStarted(
int windowIndex,
@Nullable MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData) {
EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
for (AnalyticsListener listener : listeners) {
listener.onLoadStarted(eventTime, loadEventInfo, mediaLoadData);
}
}
@Override
public final void onLoadCompleted(
int windowIndex,
@Nullable MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData) {
EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
for (AnalyticsListener listener : listeners) {
listener.onLoadCompleted(eventTime, loadEventInfo, mediaLoadData);
}
}
@Override
public final void onLoadCanceled(
int windowIndex,
@Nullable MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData) {
EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
for (AnalyticsListener listener : listeners) {
listener.onLoadCanceled(eventTime, loadEventInfo, mediaLoadData);
}
}
@Override
public final void onLoadError(
int windowIndex,
@Nullable MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData,
IOException error,
boolean wasCanceled) {
EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
for (AnalyticsListener listener : listeners) {
listener.onLoadError(eventTime, loadEventInfo, mediaLoadData, error, wasCanceled);
}
}
@Override
public final void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {
mediaPeriodQueueTracker.onReadingStarted(mediaPeriodId);
EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
for (AnalyticsListener listener : listeners) {
listener.onReadingStarted(eventTime);
}
}
@Override
public final void onUpstreamDiscarded(
int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {
EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
for (AnalyticsListener listener : listeners) {
listener.onUpstreamDiscarded(eventTime, mediaLoadData);
}
}
@Override
public final void onDownstreamFormatChanged(
int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {
EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
for (AnalyticsListener listener : listeners) {
listener.onDownstreamFormatChanged(eventTime, mediaLoadData);
}
}
// Player.EventListener implementation.
// TODO: Add onFinishedReportingChanges to Player.EventListener to know when a set of simultaneous
// callbacks finished. This helps to assign exactly the same EventTime to all of them instead of
// having slightly different real times.
@Override
public final void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
mediaPeriodQueueTracker.onTimelineChanged(timeline);
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onTimelineChanged(eventTime, reason);
}
}
@Override
public final void onTracksChanged(
TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onTracksChanged(eventTime, trackGroups, trackSelections);
}
}
@Override
public final void onLoadingChanged(boolean isLoading) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onLoadingChanged(eventTime, isLoading);
}
}
@Override
public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState);
}
}
@Override
public void onPlaybackSuppressionReasonChanged(
@PlaybackSuppressionReason int playbackSuppressionReason) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onPlaybackSuppressionReasonChanged(eventTime, playbackSuppressionReason);
}
}
@Override
public void onIsPlayingChanged(boolean isPlaying) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onIsPlayingChanged(eventTime, isPlaying);
}
}
@Override
public final void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onRepeatModeChanged(eventTime, repeatMode);
}
}
@Override
public final void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onShuffleModeChanged(eventTime, shuffleModeEnabled);
}
}
@Override
public final void onPlayerError(ExoPlaybackException error) {
EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onPlayerError(eventTime, error);
}
}
@Override
public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
mediaPeriodQueueTracker.onPositionDiscontinuity(reason);
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onPositionDiscontinuity(eventTime, reason);
}
}
@Override
public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onPlaybackParametersChanged(eventTime, playbackParameters);
}
}
@Override
public final void onSeekProcessed() {
if (mediaPeriodQueueTracker.isSeeking()) {
mediaPeriodQueueTracker.onSeekProcessed();
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onSeekProcessed(eventTime);
}
}
}
// BandwidthMeter.Listener implementation.
@Override
public final void onBandwidthSample(int elapsedMs, long bytes, long bitrate) {
EventTime eventTime = generateLoadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onBandwidthEstimate(eventTime, elapsedMs, bytes, bitrate);
}
}
// DefaultDrmSessionManager.EventListener implementation.
@Override
public final void onDrmSessionAcquired() {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDrmSessionAcquired(eventTime);
}
}
@Override
public final void onDrmKeysLoaded() {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDrmKeysLoaded(eventTime);
}
}
@Override
public final void onDrmSessionManagerError(Exception error) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDrmSessionManagerError(eventTime, error);
}
}
@Override
public final void onDrmKeysRestored() {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDrmKeysRestored(eventTime);
}
}
@Override
public final void onDrmKeysRemoved() {
EventTime eventTime = generateReadingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDrmKeysRemoved(eventTime);
}
}
@Override
public final void onDrmSessionReleased() {
EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onDrmSessionReleased(eventTime);
}
}
// 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. */
@RequiresNonNull("player")
protected EventTime generateEventTime(
Timeline timeline, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
if (timeline.isEmpty()) {
// Ensure media period id is only reported together with a valid timeline.
mediaPeriodId = null;
}
long realtimeMs = clock.elapsedRealtime();
long eventPositionMs;
boolean isInCurrentWindow =
timeline == player.getCurrentTimeline() && windowIndex == player.getCurrentWindowIndex();
if (mediaPeriodId != null && mediaPeriodId.isAd()) {
boolean isCurrentAd =
isInCurrentWindow
&& player.getCurrentAdGroupIndex() == mediaPeriodId.adGroupIndex
&& player.getCurrentAdIndexInAdGroup() == mediaPeriodId.adIndexInAdGroup;
// Assume start position of 0 for future ads.
eventPositionMs = isCurrentAd ? player.getCurrentPosition() : 0;
} else if (isInCurrentWindow) {
eventPositionMs = player.getContentPosition();
} else {
// Assume default start position for future content windows. If timeline is not available yet,
// assume start position of 0.
eventPositionMs =
timeline.isEmpty() ? 0 : timeline.getWindow(windowIndex, window).getDefaultPositionMs();
}
return new EventTime(
realtimeMs,
timeline,
windowIndex,
mediaPeriodId,
eventPositionMs,
player.getCurrentPosition(),
player.getTotalBufferedDuration());
}
private EventTime generateEventTime(@Nullable MediaPeriodInfo mediaPeriodInfo) {
Assertions.checkNotNull(player);
if (mediaPeriodInfo == null) {
int windowIndex = player.getCurrentWindowIndex();
mediaPeriodInfo = mediaPeriodQueueTracker.tryResolveWindowIndex(windowIndex);
if (mediaPeriodInfo == null) {
Timeline timeline = player.getCurrentTimeline();
boolean windowIsInTimeline = windowIndex < timeline.getWindowCount();
return generateEventTime(
windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null);
}
}
return generateEventTime(
mediaPeriodInfo.timeline, mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId);
}
private EventTime generateLastReportedPlayingMediaPeriodEventTime() {
return generateEventTime(mediaPeriodQueueTracker.getLastReportedPlayingMediaPeriod());
}
private EventTime generatePlayingMediaPeriodEventTime() {
return generateEventTime(mediaPeriodQueueTracker.getPlayingMediaPeriod());
}
private EventTime generateReadingMediaPeriodEventTime() {
return generateEventTime(mediaPeriodQueueTracker.getReadingMediaPeriod());
}
private EventTime generateLoadingMediaPeriodEventTime() {
return generateEventTime(mediaPeriodQueueTracker.getLoadingMediaPeriod());
}
private EventTime generateMediaPeriodEventTime(
int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
Assertions.checkNotNull(player);
if (mediaPeriodId != null) {
MediaPeriodInfo mediaPeriodInfo = mediaPeriodQueueTracker.getMediaPeriodInfo(mediaPeriodId);
return mediaPeriodInfo != null
? generateEventTime(mediaPeriodInfo)
: generateEventTime(Timeline.EMPTY, windowIndex, mediaPeriodId);
}
Timeline timeline = player.getCurrentTimeline();
boolean windowIsInTimeline = windowIndex < timeline.getWindowCount();
return generateEventTime(
windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null);
}
/** Keeps track of the active media periods and currently playing and reading media period. */
private static final class MediaPeriodQueueTracker {
// TODO: Investigate reporting MediaPeriodId in renderer events and adding a listener of queue
// 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;
@Nullable private MediaPeriodInfo lastPlayingMediaPeriod;
@Nullable private MediaPeriodInfo lastReportedPlayingMediaPeriod;
@Nullable private MediaPeriodInfo readingMediaPeriod;
private Timeline timeline;
private boolean isSeeking;
public MediaPeriodQueueTracker() {
mediaPeriodInfoQueue = new ArrayList<>();
mediaPeriodIdToInfo = new HashMap<>();
period = new Period();
timeline = Timeline.EMPTY;
}
/**
* Returns the {@link MediaPeriodInfo} of the media period in the front of the queue. This is
* the playing media period unless the player hasn't started playing yet (in which case it is
* the loading media period or null). While the player is seeking or preparing, this method will
* always return null to reflect the uncertainty about the current playing period. May also be
* null, if the timeline is empty or no media period is active yet.
*/
@Nullable
public MediaPeriodInfo getPlayingMediaPeriod() {
return mediaPeriodInfoQueue.isEmpty() || timeline.isEmpty() || isSeeking
? null
: mediaPeriodInfoQueue.get(0);
}
/**
* Returns the {@link MediaPeriodInfo} of the currently playing media period. This is the
* publicly reported period which should always match {@link Player#getCurrentPeriodIndex()}
* unless the player is currently seeking or being prepared in which case the previous period is
* reported until the seek or preparation is processed. May be null, if no media period is
* active yet.
*/
@Nullable
public MediaPeriodInfo getLastReportedPlayingMediaPeriod() {
return lastReportedPlayingMediaPeriod;
}
/**
* Returns the {@link MediaPeriodInfo} of the media period currently being read by the player.
* May be null, if the player is not reading a media period.
*/
@Nullable
public MediaPeriodInfo getReadingMediaPeriod() {
return readingMediaPeriod;
}
/**
* Returns the {@link MediaPeriodInfo} of the media period at the end of the queue which is
* currently loading or will be the next one loading. May be null, if no media period is active
* yet.
*/
@Nullable
public MediaPeriodInfo getLoadingMediaPeriod() {
return mediaPeriodInfoQueue.isEmpty()
? null
: mediaPeriodInfoQueue.get(mediaPeriodInfoQueue.size() - 1);
}
/** Returns the {@link MediaPeriodInfo} for the given {@link MediaPeriodId}. */
@Nullable
public MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId mediaPeriodId) {
return mediaPeriodIdToInfo.get(mediaPeriodId);
}
/** Returns whether the player is currently seeking. */
public boolean isSeeking() {
return isSeeking;
}
/**
* Tries to find an existing media period info from the specified window index. Only returns a
* non-null media period info if there is a unique, unambiguous match.
*/
@Nullable
public MediaPeriodInfo tryResolveWindowIndex(int windowIndex) {
MediaPeriodInfo match = null;
for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) {
MediaPeriodInfo info = mediaPeriodInfoQueue.get(i);
int periodIndex = timeline.getIndexOfPeriod(info.mediaPeriodId.periodUid);
if (periodIndex != C.INDEX_UNSET
&& timeline.getPeriod(periodIndex, period).windowIndex == windowIndex) {
if (match != null) {
// Ambiguous match.
return null;
}
match = info;
}
}
return match;
}
/** Updates the queue with a reported position discontinuity . */
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod;
}
/** Updates the queue with a reported timeline change. */
public void onTimelineChanged(Timeline timeline) {
for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) {
MediaPeriodInfo newMediaPeriodInfo =
updateMediaPeriodInfoToNewTimeline(mediaPeriodInfoQueue.get(i), timeline);
mediaPeriodInfoQueue.set(i, newMediaPeriodInfo);
mediaPeriodIdToInfo.put(newMediaPeriodInfo.mediaPeriodId, newMediaPeriodInfo);
}
if (readingMediaPeriod != null) {
readingMediaPeriod = updateMediaPeriodInfoToNewTimeline(readingMediaPeriod, timeline);
}
this.timeline = timeline;
lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod;
}
/** Updates the queue with a reported start of seek. */
public void onSeekStarted() {
isSeeking = true;
}
/** Updates the queue with a reported processed seek. */
public void onSeekProcessed() {
isSeeking = false;
lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod;
}
/** Updates the queue with a newly created media period. */
public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {
int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid);
boolean isInTimeline = periodIndex != C.INDEX_UNSET;
MediaPeriodInfo mediaPeriodInfo =
new MediaPeriodInfo(
mediaPeriodId,
isInTimeline ? timeline : Timeline.EMPTY,
isInTimeline ? timeline.getPeriod(periodIndex, period).windowIndex : windowIndex);
mediaPeriodInfoQueue.add(mediaPeriodInfo);
mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo);
lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0);
if (mediaPeriodInfoQueue.size() == 1 && !timeline.isEmpty()) {
lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod;
}
}
/**
* Updates the queue with a released media period. Returns whether the media period was still in
* the queue.
*/
public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId) {
MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId);
if (mediaPeriodInfo == null) {
// The media period has already been removed from the queue in resetForNewMediaSource().
return false;
}
mediaPeriodInfoQueue.remove(mediaPeriodInfo);
if (readingMediaPeriod != null && mediaPeriodId.equals(readingMediaPeriod.mediaPeriodId)) {
readingMediaPeriod = mediaPeriodInfoQueue.isEmpty() ? null : mediaPeriodInfoQueue.get(0);
}
if (!mediaPeriodInfoQueue.isEmpty()) {
lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0);
}
return true;
}
/** Update the queue with a change in the reading media period. */
public void onReadingStarted(MediaPeriodId mediaPeriodId) {
readingMediaPeriod = mediaPeriodIdToInfo.get(mediaPeriodId);
}
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;
}
}
}