From e5f2e44c29bc31aee73de2fa799ef08dea4982a1 Mon Sep 17 00:00:00 2001 From: krocard Date: Fri, 11 Feb 2022 13:58:30 +0000 Subject: [PATCH] Split AnalyticsCollector in interface and default Impl This will allow R8 to strip out the implementation if it is not needed for an app. #minor-release PiperOrigin-RevId: 427983730 --- RELEASENOTES.md | 2 + docs/analytics.md | 8 +- .../google/android/exoplayer2/ExoPlayer.java | 3 +- .../analytics/AnalyticsCollector.java | 1176 +--------------- .../analytics/DefaultAnalyticsCollector.java | 1213 +++++++++++++++++ .../exoplayer2/MediaPeriodQueueTest.java | 3 +- .../exoplayer2/MediaSourceListTest.java | 3 +- ...ava => DefaultAnalyticsCollectorTest.java} | 18 +- .../testutil/TestExoPlayerBuilder.java | 4 +- 9 files changed, 1268 insertions(+), 1162 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsCollector.java rename library/core/src/test/java/com/google/android/exoplayer2/analytics/{AnalyticsCollectorTest.java => DefaultAnalyticsCollectorTest.java} (99%) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 90b8500fda..f3f76c73d6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -54,6 +54,8 @@ `ExoPlayer.Builder.setUsePlatformDiagnostics(false)`. * Updated some `AnalyticsListener.EventFlags` constant values to match values in `Player.EventFlags`. + * Split `AnalyticsCollector` into an interface and default implementation + to allow it to be stripped by R8 if an app doesn't need it. * Android 12 compatibility: * Upgrade the Cast extension to depend on `com.google.android.gms:play-services-cast-framework:20.1.0`. Earlier diff --git a/docs/analytics.md b/docs/analytics.md index f748cc1885..f2bf7a9a8f 100644 --- a/docs/analytics.md +++ b/docs/analytics.md @@ -225,16 +225,16 @@ new PlaybackStatsListener( In case you need to add custom events to the analytics data, you need to save these events in your own data structure and combine them with the reported -`PlaybackStats` later. If it helps, you can extend `AnalyticsCollector` to be -able to generate `EventTime` instances for your custom events and send them to -the already registered listeners as shown in the following example. +`PlaybackStats` later. If it helps, you can extend `DefaultAnalyticsCollector` +to be able to generate `EventTime` instances for your custom events and send +them to the already registered listeners as shown in the following example. ~~~ interface ExtendedListener extends AnalyticsListener { void onCustomEvent(EventTime eventTime); } -class ExtendedCollector extends AnalyticsCollector { +class ExtendedCollector extends DefaultAnalyticsCollector { public void customEvent() { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); sendEvent(eventTime, CUSTOM_EVENT_ID, listener -> { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index d4c53fb981..0258bcad7b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -32,6 +32,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.analytics.AnalyticsListener; +import com.google.android.exoplayer2.analytics.DefaultAnalyticsCollector; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.audio.AuxEffectInfo; @@ -565,7 +566,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/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index bc074c4eb7..efad6a1682 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -15,121 +15,50 @@ */ package com.google.android.exoplayer2.analytics; -import static com.google.android.exoplayer2.util.Assertions.checkNotNull; -import static com.google.android.exoplayer2.util.Assertions.checkState; -import static com.google.android.exoplayer2.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 com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DeviceInfo; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.MediaItem; -import com.google.android.exoplayer2.MediaMetadata; -import com.google.android.exoplayer2.PlaybackException; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Player.DiscontinuityReason; -import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.Timeline.Window; -import com.google.android.exoplayer2.TracksInfo; -import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; -import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderException; import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; -import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionEventListener; -import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.source.LoadEventInfo; -import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.util.Clock; -import com.google.android.exoplayer2.util.HandlerWrapper; -import com.google.android.exoplayer2.util.ListenerSet; -import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer; -import com.google.android.exoplayer2.video.VideoSize; -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}. */ -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 @@ -138,28 +67,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. @@ -170,26 +84,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. @@ -199,17 +100,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. @@ -219,21 +110,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. @@ -243,19 +121,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 @@ -264,13 +131,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. @@ -280,45 +141,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. @@ -330,13 +167,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. @@ -344,26 +175,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. @@ -373,17 +185,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. @@ -393,21 +195,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. @@ -417,19 +206,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 @@ -441,43 +219,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 @@ -487,13 +243,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. @@ -513,14 +263,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. @@ -535,860 +278,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/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsCollector.java new file mode 100644 index 0000000000..f277c1a165 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsCollector.java @@ -0,0 +1,1213 @@ +/* + * 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 com.google.android.exoplayer2.analytics; + +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; + +import android.os.Looper; +import android.util.SparseArray; +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.DeviceInfo; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaMetadata; +import com.google.android.exoplayer2.PlaybackException; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.TracksInfo; +import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; +import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.source.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaLoadData; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; +import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.HandlerWrapper; +import com.google.android.exoplayer2.util.ListenerSet; +import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.VideoSize; +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}. + */ +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/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java index 5263f957a9..a2cef5fcd5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java @@ -25,6 +25,7 @@ import android.os.Looper; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.analytics.DefaultAnalyticsCollector; import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; @@ -77,7 +78,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/library/core/src/test/java/com/google/android/exoplayer2/MediaSourceListTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaSourceListTest.java index 53ba1dac82..8f4a6c10c3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaSourceListTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaSourceListTest.java @@ -29,6 +29,7 @@ import android.os.Looper; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.analytics.DefaultAnalyticsCollector; import com.google.android.exoplayer2.analytics.PlayerId; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ShuffleOrder; @@ -55,7 +56,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/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsCollectorTest.java similarity index 99% rename from library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsCollectorTest.java index f5d37b11b0..f00e2a019a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/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/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayerBuilder.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayerBuilder.java index b607c52723..c29799f11e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayerBuilder.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayerBuilder.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.analytics.DefaultAnalyticsCollector; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.BandwidthMeter; @@ -300,7 +300,7 @@ public class TestExoPlayerBuilder { .setTrackSelector(trackSelector) .setLoadControl(loadControl) .setBandwidthMeter(bandwidthMeter) - .setAnalyticsCollector(new AnalyticsCollector(clock)) + .setAnalyticsCollector(new DefaultAnalyticsCollector(clock)) .setClock(clock) .setUseLazyPreparation(useLazyPreparation) .setLooper(looper)