public class

DefaultAnalyticsCollector

extends java.lang.Object

implements AnalyticsCollector

 java.lang.Object

↳androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-exoplayer', version: '1.5.0-alpha01'

  • groupId: androidx.media3
  • artifactId: media3-exoplayer
  • version: 1.5.0-alpha01

Artifact androidx.media3:media3-exoplayer:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)

Overview

Data collector that forwards analytics events to AnalyticsListeners.

Summary

Constructors
publicDefaultAnalyticsCollector(Clock clock)

Creates an analytics collector.

Methods
public voidaddListener(AnalyticsListener listener)

protected final AnalyticsListener.EventTimegenerateCurrentPlayerMediaPeriodEventTime()

Generates an AnalyticsListener.EventTime for the currently playing item in the player.

protected final AnalyticsListener.EventTimegenerateEventTime(Timeline timeline, int windowIndex, MediaSource.MediaPeriodId mediaPeriodId)

Returns a new AnalyticsListener.EventTime for the specified timeline, window and media period id.

public final voidnotifySeekStarted()

public final voidonAudioAttributesChanged(AudioAttributes audioAttributes)

public final voidonAudioCodecError(java.lang.Exception audioCodecError)

public final voidonAudioDecoderInitialized(java.lang.String decoderName, long initializedTimestampMs, long initializationDurationMs)

public final voidonAudioDecoderReleased(java.lang.String decoderName)

public final voidonAudioDisabled(DecoderCounters counters)

public final voidonAudioEnabled(DecoderCounters counters)

public final voidonAudioInputFormatChanged(Format format, DecoderReuseEvaluation decoderReuseEvaluation)

public final voidonAudioPositionAdvancing(long playoutStartSystemTimeMs)

public final voidonAudioSessionIdChanged(int audioSessionId)

public final voidonAudioSinkError(java.lang.Exception audioSinkError)

public voidonAudioTrackInitialized(AudioSink.AudioTrackConfig audioTrackConfig)

public voidonAudioTrackReleased(AudioSink.AudioTrackConfig audioTrackConfig)

public final voidonAudioUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs)

public voidonAvailableCommandsChanged(Player.Commands availableCommands)

public final voidonBandwidthSample(int elapsedMs, long bytesTransferred, long bitrateEstimate)

public voidonCues(CueGroup cueGroup)

public voidonCues(java.util.List<Cue> cues)

public voidonDeviceInfoChanged(DeviceInfo deviceInfo)

public voidonDeviceVolumeChanged(int volume, boolean muted)

public final voidonDownstreamFormatChanged(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData)

public final voidonDrmKeysLoaded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId)

public final voidonDrmKeysRemoved(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId)

public final voidonDrmKeysRestored(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId)

public final voidonDrmSessionAcquired(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, int state)

public final voidonDrmSessionManagerError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, java.lang.Exception error)

public final voidonDrmSessionReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId)

public final voidonDroppedFrames(int count, long elapsedMs)

public voidonEvents(Player player, Player.Events events)

public final voidonIsLoadingChanged(boolean isLoading)

public voidonIsPlayingChanged(boolean isPlaying)

public final voidonLoadCanceled(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData)

public final voidonLoadCompleted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData)

public final voidonLoadError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, java.io.IOException error, boolean wasCanceled)

public voidonLoadingChanged(boolean isLoading)

public final voidonLoadStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData)

public voidonMaxSeekToPreviousPositionChanged(long maxSeekToPreviousPositionMs)

public final voidonMediaItemTransition(MediaItem mediaItem, int reason)

public voidonMediaMetadataChanged(MediaMetadata mediaMetadata)

public final voidonMetadata(Metadata metadata)

public final voidonPlaybackParametersChanged(PlaybackParameters playbackParameters)

public final voidonPlaybackStateChanged(int playbackState)

public final voidonPlaybackSuppressionReasonChanged(int playbackSuppressionReason)

public final voidonPlayerError(PlaybackException error)

public voidonPlayerErrorChanged(PlaybackException error)

public final voidonPlayerStateChanged(boolean playWhenReady, int playbackState)

public voidonPlaylistMetadataChanged(MediaMetadata playlistMetadata)

public final voidonPlayWhenReadyChanged(boolean playWhenReady, int reason)

public voidonPositionDiscontinuity(int reason)

public final voidonPositionDiscontinuity(Player.PositionInfo oldPosition, Player.PositionInfo newPosition, int reason)

public voidonRenderedFirstFrame()

public final voidonRenderedFirstFrame(java.lang.Object output, long renderTimeMs)

public voidonRendererReadyChanged(int rendererIndex, int rendererTrackType, boolean isRendererReady)

public final voidonRepeatModeChanged(int repeatMode)

public voidonSeekBackIncrementChanged(long seekBackIncrementMs)

public voidonSeekForwardIncrementChanged(long seekForwardIncrementMs)

public final voidonShuffleModeEnabledChanged(boolean shuffleModeEnabled)

public final voidonSkipSilenceEnabledChanged(boolean skipSilenceEnabled)

public final voidonSurfaceSizeChanged(int width, int height)

public final voidonTimelineChanged(Timeline timeline, int reason)

public voidonTracksChanged(Tracks tracks)

public voidonTrackSelectionParametersChanged(TrackSelectionParameters parameters)

public final voidonUpstreamDiscarded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData)

public final voidonVideoCodecError(java.lang.Exception videoCodecError)

public final voidonVideoDecoderInitialized(java.lang.String decoderName, long initializedTimestampMs, long initializationDurationMs)

public final voidonVideoDecoderReleased(java.lang.String decoderName)

public final voidonVideoDisabled(DecoderCounters counters)

public final voidonVideoEnabled(DecoderCounters counters)

public final voidonVideoFrameProcessingOffset(long totalProcessingOffsetUs, int frameCount)

public final voidonVideoInputFormatChanged(Format format, DecoderReuseEvaluation decoderReuseEvaluation)

public final voidonVideoSizeChanged(VideoSize videoSize)

public final voidonVolumeChanged(float volume)

public voidrelease()

public voidremoveListener(AnalyticsListener listener)

protected final voidsendEvent(AnalyticsListener.EventTime eventTime, int eventFlag, ListenerSet.Event<AnalyticsListener> eventInvocation)

Sends an event to registered listeners.

public voidsetPlayer(Player player, Looper looper)

public voidsetThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread)

Sets whether methods throw when using the wrong thread.

public final voidupdateMediaPeriodQueueInfo(java.util.List<MediaSource.MediaPeriodId> queue, MediaSource.MediaPeriodId readingPeriod)

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public DefaultAnalyticsCollector(Clock clock)

Creates an analytics collector.

Parameters:

clock: A Clock used to generate timestamps.

Methods

public void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread)

Deprecated: Do not use this method and ensure all calls are made from the correct thread.

Sets whether methods throw when using the wrong thread.

Do not use this method unless to support legacy use cases.

Parameters:

throwsWhenUsingWrongThread: Whether to throw when using the wrong thread.

public void addListener(AnalyticsListener listener)

public void removeListener(AnalyticsListener listener)

public void setPlayer(Player player, Looper looper)

public void release()

public final void updateMediaPeriodQueueInfo(java.util.List<MediaSource.MediaPeriodId> queue, MediaSource.MediaPeriodId readingPeriod)

public final void notifySeekStarted()

public void onRendererReadyChanged(int rendererIndex, int rendererTrackType, boolean isRendererReady)

public final void onAudioEnabled(DecoderCounters counters)

public final void onAudioDecoderInitialized(java.lang.String decoderName, long initializedTimestampMs, long initializationDurationMs)

public final void onAudioInputFormatChanged(Format format, DecoderReuseEvaluation decoderReuseEvaluation)

public final void onAudioPositionAdvancing(long playoutStartSystemTimeMs)

public final void onAudioUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs)

public final void onAudioDecoderReleased(java.lang.String decoderName)

public final void onAudioDisabled(DecoderCounters counters)

public final void onAudioSinkError(java.lang.Exception audioSinkError)

public final void onAudioCodecError(java.lang.Exception audioCodecError)

public void onAudioTrackInitialized(AudioSink.AudioTrackConfig audioTrackConfig)

public void onAudioTrackReleased(AudioSink.AudioTrackConfig audioTrackConfig)

public final void onVolumeChanged(float volume)

public final void onVideoEnabled(DecoderCounters counters)

public final void onVideoDecoderInitialized(java.lang.String decoderName, long initializedTimestampMs, long initializationDurationMs)

public final void onVideoInputFormatChanged(Format format, DecoderReuseEvaluation decoderReuseEvaluation)

public final void onDroppedFrames(int count, long elapsedMs)

public final void onVideoDecoderReleased(java.lang.String decoderName)

public final void onVideoDisabled(DecoderCounters counters)

public final void onRenderedFirstFrame(java.lang.Object output, long renderTimeMs)

public final void onVideoFrameProcessingOffset(long totalProcessingOffsetUs, int frameCount)

public final void onVideoCodecError(java.lang.Exception videoCodecError)

public final void onSurfaceSizeChanged(int width, int height)

public final void onLoadStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData)

public final void onLoadCompleted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData)

public final void onLoadCanceled(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData)

public final void onLoadError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, java.io.IOException error, boolean wasCanceled)

public final void onUpstreamDiscarded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData)

public final void onDownstreamFormatChanged(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData)

public final void onTimelineChanged(Timeline timeline, int reason)

public final void onMediaItemTransition(MediaItem mediaItem, int reason)

public void onTracksChanged(Tracks tracks)

public void onLoadingChanged(boolean isLoading)

public final void onIsLoadingChanged(boolean isLoading)

public void onAvailableCommandsChanged(Player.Commands availableCommands)

public final void onPlayerStateChanged(boolean playWhenReady, int playbackState)

public final void onPlaybackStateChanged(int playbackState)

public final void onPlayWhenReadyChanged(boolean playWhenReady, int reason)

public final void onPlaybackSuppressionReasonChanged(int playbackSuppressionReason)

public void onIsPlayingChanged(boolean isPlaying)

public final void onRepeatModeChanged(int repeatMode)

public final void onShuffleModeEnabledChanged(boolean shuffleModeEnabled)

public final void onPlayerError(PlaybackException error)

public void onPlayerErrorChanged(PlaybackException error)

public void onPositionDiscontinuity(int reason)

public final void onPositionDiscontinuity(Player.PositionInfo oldPosition, Player.PositionInfo newPosition, int reason)

public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters)

public void onSeekBackIncrementChanged(long seekBackIncrementMs)

public void onSeekForwardIncrementChanged(long seekForwardIncrementMs)

public void onMaxSeekToPreviousPositionChanged(long maxSeekToPreviousPositionMs)

public void onMediaMetadataChanged(MediaMetadata mediaMetadata)

public void onPlaylistMetadataChanged(MediaMetadata playlistMetadata)

public final void onMetadata(Metadata metadata)

public void onCues(java.util.List<Cue> cues)

public void onCues(CueGroup cueGroup)

public final void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled)

public final void onAudioSessionIdChanged(int audioSessionId)

public final void onAudioAttributesChanged(AudioAttributes audioAttributes)

public final void onVideoSizeChanged(VideoSize videoSize)

public void onTrackSelectionParametersChanged(TrackSelectionParameters parameters)

public void onDeviceInfoChanged(DeviceInfo deviceInfo)

public void onDeviceVolumeChanged(int volume, boolean muted)

public void onRenderedFirstFrame()

public void onEvents(Player player, Player.Events events)

public final void onBandwidthSample(int elapsedMs, long bytesTransferred, long bitrateEstimate)

public final void onDrmSessionAcquired(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, int state)

public final void onDrmKeysLoaded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId)

public final void onDrmSessionManagerError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, java.lang.Exception error)

public final void onDrmKeysRestored(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId)

public final void onDrmKeysRemoved(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId)

public final void onDrmSessionReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId)

protected final void sendEvent(AnalyticsListener.EventTime eventTime, int eventFlag, ListenerSet.Event<AnalyticsListener> eventInvocation)

Sends an event to registered listeners.

Parameters:

eventTime: The AnalyticsListener.EventTime to report.
eventFlag: An integer flag indicating the type of the event, or C.INDEX_UNSET to report this event without flag.
eventInvocation: The event.

protected final AnalyticsListener.EventTime generateCurrentPlayerMediaPeriodEventTime()

Generates an AnalyticsListener.EventTime for the currently playing item in the player.

protected final AnalyticsListener.EventTime generateEventTime(Timeline timeline, int windowIndex, MediaSource.MediaPeriodId mediaPeriodId)

Returns a new AnalyticsListener.EventTime for the specified timeline, window and media period id.

Source

/*
 * Copyright (C) 2022 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 androidx.media3.exoplayer.analytics;

import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;

import android.os.Looper;
import android.util.SparseArray;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.DeviceInfo;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Metadata;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.Player.DiscontinuityReason;
import androidx.media3.common.Player.PlaybackSuppressionReason;
import androidx.media3.common.Timeline;
import androidx.media3.common.Timeline.Period;
import androidx.media3.common.Timeline.Window;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.DecoderCounters;
import androidx.media3.exoplayer.DecoderReuseEvaluation;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.analytics.AnalyticsListener.EventTime;
import androidx.media3.exoplayer.audio.AudioSink;
import androidx.media3.exoplayer.drm.DrmSession;
import androidx.media3.exoplayer.source.LoadEventInfo;
import androidx.media3.exoplayer.source.MediaLoadData;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
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.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;

/**
 * Data collector that forwards analytics events to {@link AnalyticsListener AnalyticsListeners}.
 */
@UnstableApi
public class DefaultAnalyticsCollector implements AnalyticsCollector {

  private final Clock clock;
  private final Period period;
  private final Window window;
  private final MediaPeriodQueueTracker mediaPeriodQueueTracker;
  private final SparseArray<EventTime> eventTimes;

  private ListenerSet<AnalyticsListener> listeners;
  private @MonotonicNonNull Player player;
  private @MonotonicNonNull HandlerWrapper handler;
  private boolean isSeeking;

  /**
   * Creates an analytics collector.
   *
   * @param clock A {@link Clock} used to generate timestamps.
   */
  public DefaultAnalyticsCollector(Clock clock) {
    this.clock = checkNotNull(clock);
    listeners = new ListenerSet<>(Util.getCurrentOrMainLooper(), clock, (listener, flags) -> {});
    period = new Period();
    window = new Window();
    mediaPeriodQueueTracker = new MediaPeriodQueueTracker(period);
    eventTimes = new SparseArray<>();
  }

  /**
   * Sets whether methods throw when using the wrong thread.
   *
   * <p>Do not use this method unless to support legacy use cases.
   *
   * @param throwsWhenUsingWrongThread Whether to throw when using the wrong thread.
   * @deprecated Do not use this method and ensure all calls are made from the correct thread.
   */
  @SuppressWarnings("deprecation") // Calling deprecated method.
  @Deprecated
  public void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread) {
    listeners.setThrowsWhenUsingWrongThread(throwsWhenUsingWrongThread);
  }

  @Override
  @CallSuper
  public void addListener(AnalyticsListener listener) {
    checkNotNull(listener);
    listeners.add(listener);
  }

  @Override
  @CallSuper
  public void removeListener(AnalyticsListener listener) {
    listeners.remove(listener);
  }

  @Override
  @CallSuper
  public void setPlayer(Player player, Looper looper) {
    checkState(this.player == null || mediaPeriodQueueTracker.mediaPeriodQueue.isEmpty());
    this.player = checkNotNull(player);
    handler = clock.createHandler(looper, null);
    listeners =
        listeners.copy(
            looper,
            (listener, flags) ->
                listener.onEvents(player, new AnalyticsListener.Events(flags, eventTimes)));
  }

  @Override
  @CallSuper
  public void release() {
    // Release lazily so that all events that got triggered as part of player.release()
    // are still delivered to all listeners and onPlayerReleased() is delivered last.
    checkStateNotNull(handler).post(this::releaseInternal);
  }

  @Override
  public final void updateMediaPeriodQueueInfo(
      List<MediaPeriodId> queue, @Nullable MediaPeriodId readingPeriod) {
    mediaPeriodQueueTracker.onQueueUpdated(queue, readingPeriod, checkNotNull(player));
  }

  // External events.

  @Override
  @SuppressWarnings("deprecation") // Calling deprecated listener method.
  public final void notifySeekStarted() {
    if (!isSeeking) {
      EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
      isSeeking = true;
      sendEvent(
          eventTime, /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onSeekStarted(eventTime));
    }
  }

  @Override
  public void onRendererReadyChanged(
      int rendererIndex, @C.TrackType int rendererTrackType, boolean isRendererReady) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_RENDERER_READY_CHANGED,
        listener ->
            listener.onRendererReadyChanged(
                eventTime, rendererIndex, rendererTrackType, isRendererReady));
  }

  // Audio events.

  @Override
  public final void onAudioEnabled(DecoderCounters counters) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_AUDIO_ENABLED,
        listener -> listener.onAudioEnabled(eventTime, counters));
  }

  @SuppressWarnings("deprecation") // Calling deprecated listener method.
  @Override
  public final void onAudioDecoderInitialized(
      String decoderName, long initializedTimestampMs, long initializationDurationMs) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_AUDIO_DECODER_INITIALIZED,
        listener -> {
          listener.onAudioDecoderInitialized(eventTime, decoderName, initializationDurationMs);
          listener.onAudioDecoderInitialized(
              eventTime, decoderName, initializedTimestampMs, initializationDurationMs);
        });
  }

  @Override
  public final void onAudioInputFormatChanged(
      Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_AUDIO_INPUT_FORMAT_CHANGED,
        listener -> listener.onAudioInputFormatChanged(eventTime, format, decoderReuseEvaluation));
  }

  @Override
  public final void onAudioPositionAdvancing(long playoutStartSystemTimeMs) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_AUDIO_POSITION_ADVANCING,
        listener -> listener.onAudioPositionAdvancing(eventTime, playoutStartSystemTimeMs));
  }

  @Override
  public final void onAudioUnderrun(
      int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_AUDIO_UNDERRUN,
        listener ->
            listener.onAudioUnderrun(eventTime, bufferSize, bufferSizeMs, elapsedSinceLastFeedMs));
  }

  @Override
  public final void onAudioDecoderReleased(String decoderName) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_AUDIO_DECODER_RELEASED,
        listener -> listener.onAudioDecoderReleased(eventTime, decoderName));
  }

  @Override
  public final void onAudioDisabled(DecoderCounters counters) {
    EventTime eventTime = generatePlayingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_AUDIO_DISABLED,
        listener -> listener.onAudioDisabled(eventTime, counters));
  }

  @Override
  public final void onAudioSinkError(Exception audioSinkError) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_AUDIO_SINK_ERROR,
        listener -> listener.onAudioSinkError(eventTime, audioSinkError));
  }

  @Override
  public final void onAudioCodecError(Exception audioCodecError) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_AUDIO_CODEC_ERROR,
        listener -> listener.onAudioCodecError(eventTime, audioCodecError));
  }

  @Override
  public void onAudioTrackInitialized(AudioSink.AudioTrackConfig audioTrackConfig) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_AUDIO_TRACK_INITIALIZED,
        listener -> listener.onAudioTrackInitialized(eventTime, audioTrackConfig));
  }

  @Override
  public void onAudioTrackReleased(AudioSink.AudioTrackConfig audioTrackConfig) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_AUDIO_TRACK_RELEASED,
        listener -> listener.onAudioTrackReleased(eventTime, audioTrackConfig));
  }

  @Override
  public final void onVolumeChanged(float volume) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_VOLUME_CHANGED,
        listener -> listener.onVolumeChanged(eventTime, volume));
  }

  // Video events.

  @Override
  public final void onVideoEnabled(DecoderCounters counters) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_VIDEO_ENABLED,
        listener -> listener.onVideoEnabled(eventTime, counters));
  }

  @Override
  @SuppressWarnings("deprecation") // Calling deprecated listener method.
  public final void onVideoDecoderInitialized(
      String decoderName, long initializedTimestampMs, long initializationDurationMs) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_VIDEO_DECODER_INITIALIZED,
        listener -> {
          listener.onVideoDecoderInitialized(eventTime, decoderName, initializationDurationMs);
          listener.onVideoDecoderInitialized(
              eventTime, decoderName, initializedTimestampMs, initializationDurationMs);
        });
  }

  @Override
  public final void onVideoInputFormatChanged(
      Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_VIDEO_INPUT_FORMAT_CHANGED,
        listener -> listener.onVideoInputFormatChanged(eventTime, format, decoderReuseEvaluation));
  }

  @Override
  public final void onDroppedFrames(int count, long elapsedMs) {
    EventTime eventTime = generatePlayingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_DROPPED_VIDEO_FRAMES,
        listener -> listener.onDroppedVideoFrames(eventTime, count, elapsedMs));
  }

  @Override
  public final void onVideoDecoderReleased(String decoderName) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_VIDEO_DECODER_RELEASED,
        listener -> listener.onVideoDecoderReleased(eventTime, decoderName));
  }

  @Override
  public final void onVideoDisabled(DecoderCounters counters) {
    EventTime eventTime = generatePlayingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_VIDEO_DISABLED,
        listener -> listener.onVideoDisabled(eventTime, counters));
  }

  @Override
  public final void onRenderedFirstFrame(Object output, long renderTimeMs) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_RENDERED_FIRST_FRAME,
        listener -> listener.onRenderedFirstFrame(eventTime, output, renderTimeMs));
  }

  @Override
  public final void onVideoFrameProcessingOffset(long totalProcessingOffsetUs, int frameCount) {
    EventTime eventTime = generatePlayingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_VIDEO_FRAME_PROCESSING_OFFSET,
        listener ->
            listener.onVideoFrameProcessingOffset(eventTime, totalProcessingOffsetUs, frameCount));
  }

  @Override
  public final void onVideoCodecError(Exception videoCodecError) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_VIDEO_CODEC_ERROR,
        listener -> listener.onVideoCodecError(eventTime, videoCodecError));
  }

  @Override
  public final void onSurfaceSizeChanged(int width, int height) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_SURFACE_SIZE_CHANGED,
        listener -> listener.onSurfaceSizeChanged(eventTime, width, height));
  }

  // MediaSourceEventListener implementation.

  @Override
  public final void onLoadStarted(
      int windowIndex,
      @Nullable MediaPeriodId mediaPeriodId,
      LoadEventInfo loadEventInfo,
      MediaLoadData mediaLoadData) {
    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_LOAD_STARTED,
        listener -> listener.onLoadStarted(eventTime, loadEventInfo, mediaLoadData));
  }

  @Override
  public final void onLoadCompleted(
      int windowIndex,
      @Nullable MediaPeriodId mediaPeriodId,
      LoadEventInfo loadEventInfo,
      MediaLoadData mediaLoadData) {
    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_LOAD_COMPLETED,
        listener -> listener.onLoadCompleted(eventTime, loadEventInfo, mediaLoadData));
  }

  @Override
  public final void onLoadCanceled(
      int windowIndex,
      @Nullable MediaPeriodId mediaPeriodId,
      LoadEventInfo loadEventInfo,
      MediaLoadData mediaLoadData) {
    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_LOAD_CANCELED,
        listener -> 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);
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_LOAD_ERROR,
        listener ->
            listener.onLoadError(eventTime, loadEventInfo, mediaLoadData, error, wasCanceled));
  }

  @Override
  public final void onUpstreamDiscarded(
      int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {
    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_UPSTREAM_DISCARDED,
        listener -> listener.onUpstreamDiscarded(eventTime, mediaLoadData));
  }

  @Override
  public final void onDownstreamFormatChanged(
      int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {
    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_DOWNSTREAM_FORMAT_CHANGED,
        listener -> listener.onDownstreamFormatChanged(eventTime, mediaLoadData));
  }

  // Player.Listener implementation.

  // TODO: Use Player.Listener.onEvents 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(checkNotNull(player));
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_TIMELINE_CHANGED,
        listener -> listener.onTimelineChanged(eventTime, reason));
  }

  @Override
  public final void onMediaItemTransition(
      @Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_MEDIA_ITEM_TRANSITION,
        listener -> listener.onMediaItemTransition(eventTime, mediaItem, reason));
  }

  @Override
  public void onTracksChanged(Tracks tracks) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_TRACKS_CHANGED,
        listener -> listener.onTracksChanged(eventTime, tracks));
  }

  @SuppressWarnings("deprecation") // Implementing deprecated method.
  @Override
  public void onLoadingChanged(boolean isLoading) {
    // Do nothing. Handled by non-deprecated onIsLoadingChanged.
  }

  @SuppressWarnings("deprecation") // Calling deprecated listener method.
  @Override
  public final void onIsLoadingChanged(boolean isLoading) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_IS_LOADING_CHANGED,
        listener -> {
          listener.onLoadingChanged(eventTime, isLoading);
          listener.onIsLoadingChanged(eventTime, isLoading);
        });
  }

  @Override
  public void onAvailableCommandsChanged(Player.Commands availableCommands) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_AVAILABLE_COMMANDS_CHANGED,
        listener -> listener.onAvailableCommandsChanged(eventTime, availableCommands));
  }

  @SuppressWarnings("deprecation") // Implementing and calling deprecated listener method.
  @Override
  public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        /* eventFlag= */ C.INDEX_UNSET,
        listener -> listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState));
  }

  @Override
  public final void onPlaybackStateChanged(@Player.State int playbackState) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_PLAYBACK_STATE_CHANGED,
        listener -> listener.onPlaybackStateChanged(eventTime, playbackState));
  }

  @Override
  public final void onPlayWhenReadyChanged(
      boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_PLAY_WHEN_READY_CHANGED,
        listener -> listener.onPlayWhenReadyChanged(eventTime, playWhenReady, reason));
  }

  @Override
  public final void onPlaybackSuppressionReasonChanged(
      @PlaybackSuppressionReason int playbackSuppressionReason) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED,
        listener ->
            listener.onPlaybackSuppressionReasonChanged(eventTime, playbackSuppressionReason));
  }

  @Override
  public void onIsPlayingChanged(boolean isPlaying) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_IS_PLAYING_CHANGED,
        listener -> listener.onIsPlayingChanged(eventTime, isPlaying));
  }

  @Override
  public final void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_REPEAT_MODE_CHANGED,
        listener -> listener.onRepeatModeChanged(eventTime, repeatMode));
  }

  @Override
  public final void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
        listener -> listener.onShuffleModeChanged(eventTime, shuffleModeEnabled));
  }

  @Override
  public final void onPlayerError(PlaybackException error) {
    EventTime eventTime = getEventTimeForErrorEvent(error);
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_PLAYER_ERROR,
        listener -> listener.onPlayerError(eventTime, error));
  }

  @Override
  public void onPlayerErrorChanged(@Nullable PlaybackException error) {
    EventTime eventTime = getEventTimeForErrorEvent(error);
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_PLAYER_ERROR,
        listener -> listener.onPlayerErrorChanged(eventTime, error));
  }

  @SuppressWarnings("deprecation") // Implementing deprecated method.
  @Override
  public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
    // Do nothing. Handled by non-deprecated onPositionDiscontinuity.
  }

  // Calling deprecated callback.
  @SuppressWarnings("deprecation")
  @Override
  public final void onPositionDiscontinuity(
      Player.PositionInfo oldPosition,
      Player.PositionInfo newPosition,
      @Player.DiscontinuityReason int reason) {
    if (reason == Player.DISCONTINUITY_REASON_SEEK) {
      isSeeking = false;
    }
    mediaPeriodQueueTracker.onPositionDiscontinuity(checkNotNull(player));
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_POSITION_DISCONTINUITY,
        listener -> {
          listener.onPositionDiscontinuity(eventTime, reason);
          listener.onPositionDiscontinuity(eventTime, oldPosition, newPosition, reason);
        });
  }

  @Override
  public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_PLAYBACK_PARAMETERS_CHANGED,
        listener -> listener.onPlaybackParametersChanged(eventTime, playbackParameters));
  }

  @Override
  public void onSeekBackIncrementChanged(long seekBackIncrementMs) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_SEEK_BACK_INCREMENT_CHANGED,
        listener -> listener.onSeekBackIncrementChanged(eventTime, seekBackIncrementMs));
  }

  @Override
  public void onSeekForwardIncrementChanged(long seekForwardIncrementMs) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_SEEK_FORWARD_INCREMENT_CHANGED,
        listener -> listener.onSeekForwardIncrementChanged(eventTime, seekForwardIncrementMs));
  }

  @Override
  public void onMaxSeekToPreviousPositionChanged(long maxSeekToPreviousPositionMs) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED,
        listener ->
            listener.onMaxSeekToPreviousPositionChanged(eventTime, maxSeekToPreviousPositionMs));
  }

  @Override
  public void onMediaMetadataChanged(MediaMetadata mediaMetadata) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_MEDIA_METADATA_CHANGED,
        listener -> listener.onMediaMetadataChanged(eventTime, mediaMetadata));
  }

  @Override
  public void onPlaylistMetadataChanged(MediaMetadata playlistMetadata) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_PLAYLIST_METADATA_CHANGED,
        listener -> listener.onPlaylistMetadataChanged(eventTime, playlistMetadata));
  }

  @Override
  public final void onMetadata(Metadata metadata) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_METADATA,
        listener -> listener.onMetadata(eventTime, metadata));
  }

  @SuppressWarnings("deprecation") // Implementing and calling deprecated listener method.
  @Override
  public void onCues(List<Cue> cues) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime, AnalyticsListener.EVENT_CUES, listener -> listener.onCues(eventTime, cues));
  }

  @Override
  public void onCues(CueGroup cueGroup) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime, AnalyticsListener.EVENT_CUES, listener -> listener.onCues(eventTime, cueGroup));
  }

  @Override
  public final void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_SKIP_SILENCE_ENABLED_CHANGED,
        listener -> listener.onSkipSilenceEnabledChanged(eventTime, skipSilenceEnabled));
  }

  @Override
  public final void onAudioSessionIdChanged(int audioSessionId) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_AUDIO_SESSION_ID,
        listener -> listener.onAudioSessionIdChanged(eventTime, audioSessionId));
  }

  @Override
  public final void onAudioAttributesChanged(AudioAttributes audioAttributes) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_AUDIO_ATTRIBUTES_CHANGED,
        listener -> listener.onAudioAttributesChanged(eventTime, audioAttributes));
  }

  @SuppressWarnings("deprecation") // Calling deprecated listener method.
  @Override
  public final void onVideoSizeChanged(VideoSize videoSize) {
    EventTime eventTime = generateReadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_VIDEO_SIZE_CHANGED,
        listener -> {
          listener.onVideoSizeChanged(eventTime, videoSize);
          listener.onVideoSizeChanged(
              eventTime,
              videoSize.width,
              videoSize.height,
              /* unappliedRotationDegrees= */ 0,
              videoSize.pixelWidthHeightRatio);
        });
  }

  @Override
  public void onTrackSelectionParametersChanged(TrackSelectionParameters parameters) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED,
        listener -> listener.onTrackSelectionParametersChanged(eventTime, parameters));
  }

  @Override
  public void onDeviceInfoChanged(DeviceInfo deviceInfo) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_DEVICE_INFO_CHANGED,
        listener -> listener.onDeviceInfoChanged(eventTime, deviceInfo));
  }

  @Override
  public void onDeviceVolumeChanged(int volume, boolean muted) {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_DEVICE_VOLUME_CHANGED,
        listener -> listener.onDeviceVolumeChanged(eventTime, volume, muted));
  }

  @SuppressWarnings("UngroupedOverloads") // Grouped by interface.
  @Override
  public void onRenderedFirstFrame() {
    // Do nothing. Handled by onRenderedFirstFrame call with additional parameters.
  }

  @Override
  public void onEvents(Player player, Player.Events events) {
    // Do nothing. AnalyticsCollector issues its own onEvents.
  }

  // BandwidthMeter.EventListener implementation.

  @Override
  public final void onBandwidthSample(int elapsedMs, long bytesTransferred, long bitrateEstimate) {
    EventTime eventTime = generateLoadingMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_BANDWIDTH_ESTIMATE,
        listener ->
            listener.onBandwidthEstimate(eventTime, elapsedMs, bytesTransferred, bitrateEstimate));
  }

  // DrmSessionEventListener implementation.

  @Override
  @SuppressWarnings("deprecation") // Calls deprecated listener method.
  public final void onDrmSessionAcquired(
      int windowIndex, @Nullable MediaPeriodId mediaPeriodId, @DrmSession.State int state) {
    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_DRM_SESSION_ACQUIRED,
        listener -> {
          listener.onDrmSessionAcquired(eventTime);
          listener.onDrmSessionAcquired(eventTime, state);
        });
  }

  @Override
  public final void onDrmKeysLoaded(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_DRM_KEYS_LOADED,
        listener -> listener.onDrmKeysLoaded(eventTime));
  }

  @Override
  public final void onDrmSessionManagerError(
      int windowIndex, @Nullable MediaPeriodId mediaPeriodId, Exception error) {
    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_DRM_SESSION_MANAGER_ERROR,
        listener -> listener.onDrmSessionManagerError(eventTime, error));
  }

  @Override
  public final void onDrmKeysRestored(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_DRM_KEYS_RESTORED,
        listener -> listener.onDrmKeysRestored(eventTime));
  }

  @Override
  public final void onDrmKeysRemoved(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_DRM_KEYS_REMOVED,
        listener -> listener.onDrmKeysRemoved(eventTime));
  }

  @Override
  public final void onDrmSessionReleased(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
    EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_DRM_SESSION_RELEASED,
        listener -> listener.onDrmSessionReleased(eventTime));
  }

  // Internal methods.

  /**
   * Sends an event to registered listeners.
   *
   * @param eventTime The {@link EventTime} to report.
   * @param eventFlag An integer flag indicating the type of the event, or {@link C#INDEX_UNSET} to
   *     report this event without flag.
   * @param eventInvocation The event.
   */
  protected final void sendEvent(
      EventTime eventTime, int eventFlag, ListenerSet.Event<AnalyticsListener> eventInvocation) {
    eventTimes.put(eventFlag, eventTime);
    listeners.sendEvent(eventFlag, eventInvocation);
  }

  /** Generates an {@link EventTime} for the currently playing item in the player. */
  protected final EventTime generateCurrentPlayerMediaPeriodEventTime() {
    return generateEventTime(mediaPeriodQueueTracker.getCurrentPlayerMediaPeriod());
  }

  /** Returns a new {@link EventTime} for the specified timeline, window and media period id. */
  @RequiresNonNull("player")
  protected final 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.equals(player.getCurrentTimeline())
            && windowIndex == player.getCurrentMediaItemIndex();
    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();
    }
    @Nullable
    MediaPeriodId currentMediaPeriodId = mediaPeriodQueueTracker.getCurrentPlayerMediaPeriod();
    return new EventTime(
        realtimeMs,
        timeline,
        windowIndex,
        mediaPeriodId,
        eventPositionMs,
        player.getCurrentTimeline(),
        player.getCurrentMediaItemIndex(),
        currentMediaPeriodId,
        player.getCurrentPosition(),
        player.getTotalBufferedDuration());
  }

  private void releaseInternal() {
    EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
    sendEvent(
        eventTime,
        AnalyticsListener.EVENT_PLAYER_RELEASED,
        listener -> listener.onPlayerReleased(eventTime));
    listeners.release();
  }

  private EventTime generateEventTime(@Nullable MediaPeriodId mediaPeriodId) {
    checkNotNull(player);
    @Nullable
    Timeline knownTimeline =
        mediaPeriodId == null
            ? null
            : mediaPeriodQueueTracker.getMediaPeriodIdTimeline(mediaPeriodId);
    if (mediaPeriodId == null || knownTimeline == null) {
      int windowIndex = player.getCurrentMediaItemIndex();
      Timeline timeline = player.getCurrentTimeline();
      boolean windowIsInTimeline = windowIndex < timeline.getWindowCount();
      return generateEventTime(
          windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null);
    }
    int windowIndex = knownTimeline.getPeriodByUid(mediaPeriodId.periodUid, period).windowIndex;
    return generateEventTime(knownTimeline, windowIndex, mediaPeriodId);
  }

  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) {
    checkNotNull(player);
    if (mediaPeriodId != null) {
      boolean isInKnownTimeline =
          mediaPeriodQueueTracker.getMediaPeriodIdTimeline(mediaPeriodId) != null;
      return isInKnownTimeline
          ? generateEventTime(mediaPeriodId)
          : generateEventTime(Timeline.EMPTY, windowIndex, mediaPeriodId);
    }
    Timeline timeline = player.getCurrentTimeline();
    boolean windowIsInTimeline = windowIndex < timeline.getWindowCount();
    return generateEventTime(
        windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null);
  }

  private EventTime getEventTimeForErrorEvent(@Nullable PlaybackException error) {
    if (error instanceof ExoPlaybackException) {
      ExoPlaybackException exoError = (ExoPlaybackException) error;
      if (exoError.mediaPeriodId != null) {
        return generateEventTime(exoError.mediaPeriodId);
      }
    }
    return generateCurrentPlayerMediaPeriodEventTime();
  }

  /** 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.

    private final Period period;

    private ImmutableList<MediaPeriodId> mediaPeriodQueue;
    private ImmutableMap<MediaPeriodId, Timeline> mediaPeriodTimelines;
    @Nullable private MediaPeriodId currentPlayerMediaPeriod;
    private @MonotonicNonNull MediaPeriodId playingMediaPeriod;
    private @MonotonicNonNull MediaPeriodId readingMediaPeriod;

    public MediaPeriodQueueTracker(Period period) {
      this.period = period;
      mediaPeriodQueue = ImmutableList.of();
      mediaPeriodTimelines = ImmutableMap.of();
    }

    /**
     * Returns the {@link MediaPeriodId} of the media period corresponding the current position of
     * the player.
     *
     * <p>May be null if no matching media period has been created yet.
     */
    @Nullable
    public MediaPeriodId getCurrentPlayerMediaPeriod() {
      return currentPlayerMediaPeriod;
    }

    /**
     * Returns the {@link MediaPeriodId} of the media period at the front of the queue. If 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.
     */
    @Nullable
    public MediaPeriodId getPlayingMediaPeriod() {
      return playingMediaPeriod;
    }

    /**
     * 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 no media period has been created yet.
     */
    @Nullable
    public MediaPeriodId getReadingMediaPeriod() {
      return readingMediaPeriod;
    }

    /**
     * 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.
     *
     * <p>May be null, if no media period is active yet.
     */
    @Nullable
    public MediaPeriodId getLoadingMediaPeriod() {
      return mediaPeriodQueue.isEmpty() ? null : Iterables.getLast(mediaPeriodQueue);
    }

    /**
     * Returns the most recent {@link Timeline} for the given {@link MediaPeriodId}, or null if no
     * timeline is available.
     */
    @Nullable
    public Timeline getMediaPeriodIdTimeline(MediaPeriodId mediaPeriodId) {
      return mediaPeriodTimelines.get(mediaPeriodId);
    }

    /** Updates the queue tracker with a reported position discontinuity. */
    public void onPositionDiscontinuity(Player player) {
      currentPlayerMediaPeriod =
          findCurrentPlayerMediaPeriodInQueue(player, mediaPeriodQueue, playingMediaPeriod, period);
    }

    /** Updates the queue tracker with a reported timeline change. */
    public void onTimelineChanged(Player player) {
      currentPlayerMediaPeriod =
          findCurrentPlayerMediaPeriodInQueue(player, mediaPeriodQueue, playingMediaPeriod, period);
      updateMediaPeriodTimelines(/* preferredTimeline= */ player.getCurrentTimeline());
    }

    /** Updates the queue tracker to a new queue of media periods. */
    public void onQueueUpdated(
        List<MediaPeriodId> queue, @Nullable MediaPeriodId readingPeriod, Player player) {
      mediaPeriodQueue = ImmutableList.copyOf(queue);
      if (!queue.isEmpty()) {
        playingMediaPeriod = queue.get(0);
        readingMediaPeriod = checkNotNull(readingPeriod);
      }
      if (currentPlayerMediaPeriod == null) {
        currentPlayerMediaPeriod =
            findCurrentPlayerMediaPeriodInQueue(
                player, mediaPeriodQueue, playingMediaPeriod, period);
      }
      updateMediaPeriodTimelines(/* preferredTimeline= */ player.getCurrentTimeline());
    }

    private void updateMediaPeriodTimelines(Timeline preferredTimeline) {
      ImmutableMap.Builder<MediaPeriodId, Timeline> builder = ImmutableMap.builder();
      if (mediaPeriodQueue.isEmpty()) {
        addTimelineForMediaPeriodId(builder, playingMediaPeriod, preferredTimeline);
        if (!Objects.equal(readingMediaPeriod, playingMediaPeriod)) {
          addTimelineForMediaPeriodId(builder, readingMediaPeriod, preferredTimeline);
        }
        if (!Objects.equal(currentPlayerMediaPeriod, playingMediaPeriod)
            && !Objects.equal(currentPlayerMediaPeriod, readingMediaPeriod)) {
          addTimelineForMediaPeriodId(builder, currentPlayerMediaPeriod, preferredTimeline);
        }
      } else {
        for (int i = 0; i < mediaPeriodQueue.size(); i++) {
          addTimelineForMediaPeriodId(builder, mediaPeriodQueue.get(i), preferredTimeline);
        }
        if (!mediaPeriodQueue.contains(currentPlayerMediaPeriod)) {
          addTimelineForMediaPeriodId(builder, currentPlayerMediaPeriod, preferredTimeline);
        }
      }
      mediaPeriodTimelines = builder.buildOrThrow();
    }

    private void addTimelineForMediaPeriodId(
        ImmutableMap.Builder<MediaPeriodId, Timeline> mediaPeriodTimelinesBuilder,
        @Nullable MediaPeriodId mediaPeriodId,
        Timeline preferredTimeline) {
      if (mediaPeriodId == null) {
        return;
      }
      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
    private static MediaPeriodId findCurrentPlayerMediaPeriodInQueue(
        Player player,
        ImmutableList<MediaPeriodId> mediaPeriodQueue,
        @Nullable MediaPeriodId playingMediaPeriod,
        Period period) {
      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(
                      Util.msToUs(player.getCurrentPosition()) - period.getPositionInWindowUs());
      for (int i = 0; i < mediaPeriodQueue.size(); i++) {
        MediaPeriodId mediaPeriodId = mediaPeriodQueue.get(i);
        if (isMatchingMediaPeriod(
            mediaPeriodId,
            playerPeriodUid,
            player.isPlayingAd(),
            player.getCurrentAdGroupIndex(),
            player.getCurrentAdIndexInAdGroup(),
            playerNextAdGroupIndex)) {
          return mediaPeriodId;
        }
      }
      if (mediaPeriodQueue.isEmpty() && playingMediaPeriod != null) {
        if (isMatchingMediaPeriod(
            playingMediaPeriod,
            playerPeriodUid,
            player.isPlayingAd(),
            player.getCurrentAdGroupIndex(),
            player.getCurrentAdIndexInAdGroup(),
            playerNextAdGroupIndex)) {
          return playingMediaPeriod;
        }
      }
      return null;
    }

    private static boolean isMatchingMediaPeriod(
        MediaPeriodId mediaPeriodId,
        @Nullable Object playerPeriodUid,
        boolean isPlayingAd,
        int playerAdGroupIndex,
        int playerAdIndexInAdGroup,
        int playerNextAdGroupIndex) {
      if (!mediaPeriodId.periodUid.equals(playerPeriodUid)) {
        return false;
      }
      // Timeline period matches. Still need to check ad information.
      return (isPlayingAd
              && mediaPeriodId.adGroupIndex == playerAdGroupIndex
              && mediaPeriodId.adIndexInAdGroup == playerAdIndexInAdGroup)
          || (!isPlayingAd
              && mediaPeriodId.adGroupIndex == C.INDEX_UNSET
              && mediaPeriodId.nextAdGroupIndex == playerNextAdGroupIndex);
    }
  }
}