diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java index 488237fd82..e05289e8c0 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java @@ -47,6 +47,7 @@ import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.analytics.AnalyticsListener; +import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector; import androidx.media3.exoplayer.audio.AudioSink; import androidx.media3.exoplayer.audio.DefaultAudioSink; import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer; @@ -580,7 +581,7 @@ public interface ExoPlayer extends Player { this.analyticsCollectorSupplier = analyticsCollectorSupplier != null ? analyticsCollectorSupplier - : () -> new AnalyticsCollector(checkNotNull(clock)); + : () -> new DefaultAnalyticsCollector(checkNotNull(clock)); looper = Util.getCurrentOrMainLooper(); audioAttributes = AudioAttributes.DEFAULT; wakeMode = C.WAKE_MODE_NONE; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/AnalyticsCollector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/AnalyticsCollector.java index 72334349d7..03751eed2f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/AnalyticsCollector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/AnalyticsCollector.java @@ -15,123 +15,52 @@ */ 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.media.AudioTrack; import android.media.MediaCodec; import android.media.MediaCodec.CodecException; import android.os.Looper; import android.os.SystemClock; -import android.util.SparseArray; import android.view.Surface; -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.TrackGroupArray; -import androidx.media3.common.TrackSelectionArray; -import androidx.media3.common.TrackSelectionParameters; -import androidx.media3.common.TracksInfo; -import androidx.media3.common.VideoSize; -import androidx.media3.common.text.Cue; -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.decoder.DecoderException; 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.drm.DrmSessionEventListener; -import androidx.media3.exoplayer.source.LoadEventInfo; -import androidx.media3.exoplayer.source.MediaLoadData; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.MediaSourceEventListener; import androidx.media3.exoplayer.upstream.BandwidthMeter; import androidx.media3.exoplayer.video.VideoDecoderOutputBufferRenderer; -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}. + * Interface for data collectors that forward analytics events to {@link AnalyticsListener + * AnalyticsListeners}. */ @UnstableApi -public class AnalyticsCollector - implements Player.Listener, +public interface AnalyticsCollector + extends Player.Listener, MediaSourceEventListener, BandwidthMeter.EventListener, DrmSessionEventListener { - private final Clock clock; - private final Period period; - private final Window window; - private final MediaPeriodQueueTracker mediaPeriodQueueTracker; - private final SparseArray eventTimes; - - private ListenerSet 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 AnalyticsCollector(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<>(); - } - /** * Adds a listener for analytics events. * * @param listener The listener to add. */ - @CallSuper - public void addListener(AnalyticsListener listener) { - checkNotNull(listener); - listeners.add(listener); - } + void addListener(AnalyticsListener listener); /** * Removes a previously added analytics event listener. * * @param listener The listener to remove. */ - @CallSuper - public void removeListener(AnalyticsListener listener) { - listeners.remove(listener); - } + void removeListener(AnalyticsListener listener); /** * Sets the player for which data will be collected. Must only be called if no player has been set @@ -140,28 +69,13 @@ public class AnalyticsCollector * @param player The {@link Player} for which data will be collected. * @param looper The {@link Looper} used for listener callbacks. */ - @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))); - } + void setPlayer(Player player, Looper looper); /** * Releases the collector. Must be called after the player for which data is collected has been * released. */ - @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); - } + void release(); /** * Updates the playback queue information used for event association. @@ -172,26 +86,13 @@ public class AnalyticsCollector * @param readingPeriod The media period in the queue that is currently being read by renderers, * or null if the queue is empty. */ - public final void updateMediaPeriodQueueInfo( - List queue, @Nullable MediaPeriodId readingPeriod) { - mediaPeriodQueueTracker.onQueueUpdated(queue, readingPeriod, checkNotNull(player)); - } - - // External events. + void updateMediaPeriodQueueInfo(List queue, @Nullable MediaPeriodId readingPeriod); /** * Notify analytics collector that a seek operation will start. Should be called before the player * adjusts its state and position to the seek. */ - @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)); - } - } + void notifySeekStarted(); // Audio events. @@ -201,17 +102,7 @@ public class AnalyticsCollector * @param counters {@link DecoderCounters} that will be updated by the audio renderer for as long * as it remains enabled. */ - @SuppressWarnings("deprecation") // Calling deprecated listener method. - public final void onAudioEnabled(DecoderCounters counters) { - EventTime eventTime = generateReadingMediaPeriodEventTime(); - sendEvent( - eventTime, - AnalyticsListener.EVENT_AUDIO_ENABLED, - listener -> { - listener.onAudioEnabled(eventTime, counters); - listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_AUDIO, counters); - }); - } + void onAudioEnabled(DecoderCounters counters); /** * Called when a audio decoder is created. @@ -221,21 +112,8 @@ public class AnalyticsCollector * finished. * @param initializationDurationMs The time taken to initialize the decoder in milliseconds. */ - @SuppressWarnings("deprecation") // Calling deprecated listener method. - 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); - listener.onDecoderInitialized( - eventTime, C.TRACK_TYPE_AUDIO, decoderName, initializationDurationMs); - }); - } + void onAudioDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs); /** * Called when the format of the media being consumed by the audio renderer changes. @@ -245,19 +123,8 @@ public class AnalyticsCollector * decoder instance can be reused for the new format, or {@code null} if the renderer did not * have a decoder. */ - @SuppressWarnings("deprecation") // Calling deprecated listener method. - 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); - listener.onAudioInputFormatChanged(eventTime, format, decoderReuseEvaluation); - listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_AUDIO, format); - }); - } + void onAudioInputFormatChanged( + Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation); /** * Called when the audio position has increased for the first time since the last pause or @@ -266,13 +133,7 @@ public class AnalyticsCollector * @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at * which playout started. */ - public final void onAudioPositionAdvancing(long playoutStartSystemTimeMs) { - EventTime eventTime = generateReadingMediaPeriodEventTime(); - sendEvent( - eventTime, - AnalyticsListener.EVENT_AUDIO_POSITION_ADVANCING, - listener -> listener.onAudioPositionAdvancing(eventTime, playoutStartSystemTimeMs)); - } + void onAudioPositionAdvancing(long playoutStartSystemTimeMs); /** * Called when an audio underrun occurs. @@ -282,45 +143,21 @@ public class AnalyticsCollector * encoded audio. {@link C#TIME_UNSET} if the output buffer contains non-PCM encoded audio. * @param elapsedSinceLastFeedMs The time since audio was last written to the output buffer. */ - 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)); - } + void onAudioUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs); /** * Called when a audio decoder is released. * * @param decoderName The audio decoder that was released. */ - public final void onAudioDecoderReleased(String decoderName) { - EventTime eventTime = generateReadingMediaPeriodEventTime(); - sendEvent( - eventTime, - AnalyticsListener.EVENT_AUDIO_DECODER_RELEASED, - listener -> listener.onAudioDecoderReleased(eventTime, decoderName)); - } + void onAudioDecoderReleased(String decoderName); /** * Called when the audio renderer is disabled. * * @param counters {@link DecoderCounters} that were updated by the audio renderer. */ - @SuppressWarnings("deprecation") // Calling deprecated listener method. - public final void onAudioDisabled(DecoderCounters counters) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); - sendEvent( - eventTime, - AnalyticsListener.EVENT_AUDIO_DISABLED, - listener -> { - listener.onAudioDisabled(eventTime, counters); - listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_AUDIO, counters); - }); - } + void onAudioDisabled(DecoderCounters counters); /** * Called when {@link AudioSink} has encountered an error. @@ -332,13 +169,7 @@ public class AnalyticsCollector * AudioSink.InitializationException}, a {@link AudioSink.WriteException}, or an {@link * AudioSink.UnexpectedDiscontinuityException}. */ - public final void onAudioSinkError(Exception audioSinkError) { - EventTime eventTime = generateReadingMediaPeriodEventTime(); - sendEvent( - eventTime, - AnalyticsListener.EVENT_AUDIO_SINK_ERROR, - listener -> listener.onAudioSinkError(eventTime, audioSinkError)); - } + void onAudioSinkError(Exception audioSinkError); /** * Called when an audio decoder encounters an error. @@ -346,26 +177,7 @@ public class AnalyticsCollector * @param audioCodecError The error. Typically a {@link CodecException} if the renderer uses * {@link MediaCodec}, or a {@link DecoderException} if the renderer uses a software decoder. */ - public final void onAudioCodecError(Exception audioCodecError) { - EventTime eventTime = generateReadingMediaPeriodEventTime(); - sendEvent( - eventTime, - AnalyticsListener.EVENT_AUDIO_CODEC_ERROR, - listener -> listener.onAudioCodecError(eventTime, audioCodecError)); - } - - /** - * Called when the volume changes. - * - * @param volume The new volume, with 0 being silence and 1 being unity gain. - */ - public final void onVolumeChanged(float volume) { - EventTime eventTime = generateReadingMediaPeriodEventTime(); - sendEvent( - eventTime, - AnalyticsListener.EVENT_VOLUME_CHANGED, - listener -> listener.onVolumeChanged(eventTime, volume)); - } + void onAudioCodecError(Exception audioCodecError); // Video events. @@ -375,17 +187,7 @@ public class AnalyticsCollector * @param counters {@link DecoderCounters} that will be updated by the video renderer for as long * as it remains enabled. */ - @SuppressWarnings("deprecation") // Calling deprecated listener method. - public final void onVideoEnabled(DecoderCounters counters) { - EventTime eventTime = generateReadingMediaPeriodEventTime(); - sendEvent( - eventTime, - AnalyticsListener.EVENT_VIDEO_ENABLED, - listener -> { - listener.onVideoEnabled(eventTime, counters); - listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_VIDEO, counters); - }); - } + void onVideoEnabled(DecoderCounters counters); /** * Called when a video decoder is created. @@ -395,21 +197,8 @@ public class AnalyticsCollector * finished. * @param initializationDurationMs The time taken to initialize the decoder in milliseconds. */ - @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); - listener.onDecoderInitialized( - eventTime, C.TRACK_TYPE_VIDEO, decoderName, initializationDurationMs); - }); - } + void onVideoDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs); /** * Called when the format of the media being consumed by the video renderer changes. @@ -419,19 +208,8 @@ public class AnalyticsCollector * decoder instance can be reused for the new format, or {@code null} if the renderer did not * have a decoder. */ - @SuppressWarnings("deprecation") // Calling deprecated listener method. - 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); - listener.onVideoInputFormatChanged(eventTime, format, decoderReuseEvaluation); - listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_VIDEO, format); - }); - } + void onVideoInputFormatChanged( + Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation); /** * Called to report the number of frames dropped by the video renderer. Dropped frames are @@ -443,43 +221,21 @@ public class AnalyticsCollector * is timed from when the renderer was started or from when dropped frames were last reported * (whichever was more recent), and not from when the first of the reported drops occurred. */ - public final void onDroppedFrames(int count, long elapsedMs) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); - sendEvent( - eventTime, - AnalyticsListener.EVENT_DROPPED_VIDEO_FRAMES, - listener -> listener.onDroppedVideoFrames(eventTime, count, elapsedMs)); - } + void onDroppedFrames(int count, long elapsedMs); /** * Called when a video decoder is released. * * @param decoderName The video decoder that was released. */ - public final void onVideoDecoderReleased(String decoderName) { - EventTime eventTime = generateReadingMediaPeriodEventTime(); - sendEvent( - eventTime, - AnalyticsListener.EVENT_VIDEO_DECODER_RELEASED, - listener -> listener.onVideoDecoderReleased(eventTime, decoderName)); - } + void onVideoDecoderReleased(String decoderName); /** * Called when the video renderer is disabled. * * @param counters {@link DecoderCounters} that were updated by the video renderer. */ - @SuppressWarnings("deprecation") // Calling deprecated listener method. - public final void onVideoDisabled(DecoderCounters counters) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); - sendEvent( - eventTime, - AnalyticsListener.EVENT_VIDEO_DISABLED, - listener -> { - listener.onVideoDisabled(eventTime, counters); - listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_VIDEO, counters); - }); - } + void onVideoDisabled(DecoderCounters counters); /** * Called when a frame is rendered for the first time since setting the output, or since the @@ -489,13 +245,7 @@ public class AnalyticsCollector * renderers may have other output types (e.g., a {@link VideoDecoderOutputBufferRenderer}). * @param renderTimeMs The {@link SystemClock#elapsedRealtime()} when the frame was rendered. */ - public final void onRenderedFirstFrame(Object output, long renderTimeMs) { - EventTime eventTime = generateReadingMediaPeriodEventTime(); - sendEvent( - eventTime, - AnalyticsListener.EVENT_RENDERED_FIRST_FRAME, - listener -> listener.onRenderedFirstFrame(eventTime, output, renderTimeMs)); - } + void onRenderedFirstFrame(Object output, long renderTimeMs); /** * Called to report the video processing offset of video frames processed by the video renderer. @@ -515,14 +265,7 @@ public class AnalyticsCollector * video frames processed by the renderer in microseconds. * @param frameCount The number of samples included in the {@code totalProcessingOffsetUs}. */ - 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)); - } + void onVideoFrameProcessingOffset(long totalProcessingOffsetUs, int frameCount); /** * Called when a video decoder encounters an error. @@ -537,860 +280,5 @@ public class AnalyticsCollector * @param videoCodecError The error. Typically a {@link CodecException} if the renderer uses * {@link MediaCodec}, or a {@link DecoderException} if the renderer uses a software decoder. */ - public final void onVideoCodecError(Exception videoCodecError) { - EventTime eventTime = generateReadingMediaPeriodEventTime(); - sendEvent( - eventTime, - AnalyticsListener.EVENT_VIDEO_CODEC_ERROR, - listener -> listener.onVideoCodecError(eventTime, videoCodecError)); - } - - /** - * Called each time there's a change in the size of the surface onto which the video is being - * rendered. - * - * @param width The surface width in pixels. May be {@link C#LENGTH_UNSET} if unknown, or 0 if the - * video is not rendered onto a surface. - * @param height The surface height in pixels. May be {@link C#LENGTH_UNSET} if unknown, or 0 if - * the video is not rendered onto a surface. - */ - public 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 - @SuppressWarnings("deprecation") // Implementing and calling deprecate listener method - public final void onTracksChanged( - TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - sendEvent( - eventTime, - AnalyticsListener.EVENT_TRACKS_CHANGED, - listener -> listener.onTracksChanged(eventTime, trackGroups, trackSelections)); - } - - @Override - public void onTracksInfoChanged(TracksInfo tracksInfo) { - EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - sendEvent( - eventTime, - AnalyticsListener.EVENT_TRACKS_CHANGED, - listener -> listener.onTracksInfoChanged(eventTime, tracksInfo)); - } - - @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)); - } - - @Override - public void onCues(List cues) { - EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - sendEvent( - eventTime, AnalyticsListener.EVENT_CUES, listener -> listener.onCues(eventTime, cues)); - } - - @SuppressWarnings("deprecation") // Implementing and calling deprecated listener method. - @Override - public final void onSeekProcessed() { - EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - sendEvent( - eventTime, /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onSeekProcessed(eventTime)); - } - - @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, - videoSize.unappliedRotationDegrees, - 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 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(new MediaPeriodId(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 mediaPeriodQueue; - private ImmutableMap 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. - * - *

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

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

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

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 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 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 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 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); - } - } + void onVideoCodecError(Exception videoCodecError); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollector.java new file mode 100644 index 0000000000..b3d9c3669d --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollector.java @@ -0,0 +1,1215 @@ +/* + * 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.TrackGroupArray; +import androidx.media3.common.TrackSelectionArray; +import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.common.TracksInfo; +import androidx.media3.common.VideoSize; +import androidx.media3.common.text.Cue; +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.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 eventTimes; + + private ListenerSet 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<>(); + } + + @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 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)); + } + } + + // Audio events. + + @SuppressWarnings("deprecation") // Calling deprecated listener method. + @Override + public final void onAudioEnabled(DecoderCounters counters) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + sendEvent( + eventTime, + AnalyticsListener.EVENT_AUDIO_ENABLED, + listener -> { + listener.onAudioEnabled(eventTime, counters); + listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_AUDIO, 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); + listener.onDecoderInitialized( + eventTime, C.TRACK_TYPE_AUDIO, decoderName, initializationDurationMs); + }); + } + + @SuppressWarnings("deprecation") // Calling deprecated listener method. + @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); + listener.onAudioInputFormatChanged(eventTime, format, decoderReuseEvaluation); + listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_AUDIO, format); + }); + } + + @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 + @SuppressWarnings("deprecation") // Calling deprecated listener method. + public final void onAudioDisabled(DecoderCounters counters) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + sendEvent( + eventTime, + AnalyticsListener.EVENT_AUDIO_DISABLED, + listener -> { + listener.onAudioDisabled(eventTime, counters); + listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_AUDIO, 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 final void onVolumeChanged(float volume) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + sendEvent( + eventTime, + AnalyticsListener.EVENT_VOLUME_CHANGED, + listener -> listener.onVolumeChanged(eventTime, volume)); + } + + // Video events. + + @Override + @SuppressWarnings("deprecation") // Calling deprecated listener method. + public final void onVideoEnabled(DecoderCounters counters) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + sendEvent( + eventTime, + AnalyticsListener.EVENT_VIDEO_ENABLED, + listener -> { + listener.onVideoEnabled(eventTime, counters); + listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_VIDEO, 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); + listener.onDecoderInitialized( + eventTime, C.TRACK_TYPE_VIDEO, decoderName, initializationDurationMs); + }); + } + + @Override + @SuppressWarnings("deprecation") // Calling deprecated listener method. + 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); + listener.onVideoInputFormatChanged(eventTime, format, decoderReuseEvaluation); + listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_VIDEO, format); + }); + } + + @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 + @SuppressWarnings("deprecation") // Calling deprecated listener method. + public final void onVideoDisabled(DecoderCounters counters) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + sendEvent( + eventTime, + AnalyticsListener.EVENT_VIDEO_DISABLED, + listener -> { + listener.onVideoDisabled(eventTime, counters); + listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_VIDEO, 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 + @SuppressWarnings("deprecation") // Implementing and calling deprecate listener method + public final void onTracksChanged( + TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); + sendEvent( + eventTime, + AnalyticsListener.EVENT_TRACKS_CHANGED, + listener -> listener.onTracksChanged(eventTime, trackGroups, trackSelections)); + } + + @Override + public void onTracksInfoChanged(TracksInfo tracksInfo) { + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); + sendEvent( + eventTime, + AnalyticsListener.EVENT_TRACKS_CHANGED, + listener -> listener.onTracksInfoChanged(eventTime, tracksInfo)); + } + + @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)); + } + + @Override + public void onCues(List cues) { + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); + sendEvent( + eventTime, AnalyticsListener.EVENT_CUES, listener -> listener.onCues(eventTime, cues)); + } + + @SuppressWarnings("deprecation") // Implementing and calling deprecated listener method. + @Override + public final void onSeekProcessed() { + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); + sendEvent( + eventTime, /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onSeekProcessed(eventTime)); + } + + @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, + videoSize.unappliedRotationDegrees, + 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 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(new MediaPeriodId(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 mediaPeriodQueue; + private ImmutableMap 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. + * + *

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

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

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

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 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 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 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 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); + } + } +} diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java index bf5f152a6f..ef8b0477f0 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java @@ -31,6 +31,7 @@ import androidx.media3.common.Timeline; import androidx.media3.common.TracksInfo; import androidx.media3.common.util.Clock; import androidx.media3.exoplayer.analytics.AnalyticsCollector; +import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.MediaSource.MediaSourceCaller; @@ -83,7 +84,7 @@ public final class MediaPeriodQueueTest { @Before public void setUp() { - AnalyticsCollector analyticsCollector = new AnalyticsCollector(Clock.DEFAULT); + AnalyticsCollector analyticsCollector = new DefaultAnalyticsCollector(Clock.DEFAULT); analyticsCollector.setPlayer( new ExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build(), Looper.getMainLooper()); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaSourceListTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaSourceListTest.java index 32052e2228..42a65f24d7 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaSourceListTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaSourceListTest.java @@ -32,6 +32,7 @@ import androidx.media3.common.Timeline; import androidx.media3.common.util.Clock; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.analytics.AnalyticsCollector; +import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.ShuffleOrder; @@ -58,7 +59,7 @@ public class MediaSourceListTest { @Before public void setUp() { - AnalyticsCollector analyticsCollector = new AnalyticsCollector(Clock.DEFAULT); + AnalyticsCollector analyticsCollector = new DefaultAnalyticsCollector(Clock.DEFAULT); analyticsCollector.setPlayer( new ExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build(), Looper.getMainLooper()); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/AnalyticsCollectorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java similarity index 99% rename from libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/AnalyticsCollectorTest.java rename to libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java index 35e59c9740..7aff4de60f 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/AnalyticsCollectorTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java @@ -55,12 +55,12 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.argThat; +import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.same; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -134,11 +134,11 @@ import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.robolectric.shadows.ShadowLooper; -/** Integration test for {@link AnalyticsCollector}. */ +/** Integration test for {@link DefaultAnalyticsCollector}. */ @RunWith(AndroidJUnit4.class) -public final class AnalyticsCollectorTest { +public final class DefaultAnalyticsCollectorTest { - private static final String TAG = "AnalyticsCollectorTest"; + private static final String TAG = "DefaultAnalyticsCollectorTest"; // Deprecated event constants. private static final long EVENT_PLAYER_STATE_CHANGED = 1L << 63; @@ -193,14 +193,14 @@ public final class AnalyticsCollectorTest { private EventWindowAndPeriodId window1Period0Seq1; @Test - public void analyticsCollector_overridesAllPlayerListenerMethods() throws Exception { + public void defaultAnalyticsCollector_overridesAllPlayerListenerMethods() throws Exception { // Verify that AnalyticsCollector forwards all Player.Listener methods to AnalyticsListener. for (Method method : Player.Listener.class.getDeclaredMethods()) { assertThat( - AnalyticsCollector.class + DefaultAnalyticsCollector.class .getMethod(method.getName(), method.getParameterTypes()) .getDeclaringClass()) - .isEqualTo(AnalyticsCollector.class); + .isEqualTo(DefaultAnalyticsCollector.class); } } @@ -1964,7 +1964,7 @@ public final class AnalyticsCollectorTest { @Test public void recursiveListenerInvocation_arrivesInCorrectOrder() { - AnalyticsCollector analyticsCollector = new AnalyticsCollector(Clock.DEFAULT); + AnalyticsCollector analyticsCollector = new DefaultAnalyticsCollector(Clock.DEFAULT); analyticsCollector.setPlayer( new ExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build(), Looper.myLooper()); diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestExoPlayerBuilder.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestExoPlayerBuilder.java index caa7887a42..3917a4bf24 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestExoPlayerBuilder.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestExoPlayerBuilder.java @@ -29,7 +29,7 @@ import androidx.media3.exoplayer.LoadControl; import androidx.media3.exoplayer.Renderer; import androidx.media3.exoplayer.RenderersFactory; import androidx.media3.exoplayer.SimpleExoPlayer; -import androidx.media3.exoplayer.analytics.AnalyticsCollector; +import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.exoplayer.upstream.BandwidthMeter; @@ -302,7 +302,7 @@ public class TestExoPlayerBuilder { .setTrackSelector(trackSelector) .setLoadControl(loadControl) .setBandwidthMeter(bandwidthMeter) - .setAnalyticsCollector(new AnalyticsCollector(clock)) + .setAnalyticsCollector(new DefaultAnalyticsCollector(clock)) .setClock(clock) .setUseLazyPreparation(useLazyPreparation) .setLooper(looper)