• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.exoplayer2.analytics;
17 
18 import android.view.Surface;
19 import androidx.annotation.Nullable;
20 import com.google.android.exoplayer2.C;
21 import com.google.android.exoplayer2.ExoPlaybackException;
22 import com.google.android.exoplayer2.Format;
23 import com.google.android.exoplayer2.PlaybackParameters;
24 import com.google.android.exoplayer2.Player;
25 import com.google.android.exoplayer2.Player.PlaybackSuppressionReason;
26 import com.google.android.exoplayer2.Timeline;
27 import com.google.android.exoplayer2.Timeline.Period;
28 import com.google.android.exoplayer2.Timeline.Window;
29 import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;
30 import com.google.android.exoplayer2.audio.AudioAttributes;
31 import com.google.android.exoplayer2.audio.AudioListener;
32 import com.google.android.exoplayer2.audio.AudioRendererEventListener;
33 import com.google.android.exoplayer2.decoder.DecoderCounters;
34 import com.google.android.exoplayer2.drm.DrmSessionEventListener;
35 import com.google.android.exoplayer2.metadata.Metadata;
36 import com.google.android.exoplayer2.metadata.MetadataOutput;
37 import com.google.android.exoplayer2.source.LoadEventInfo;
38 import com.google.android.exoplayer2.source.MediaLoadData;
39 import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
40 import com.google.android.exoplayer2.source.MediaSourceEventListener;
41 import com.google.android.exoplayer2.source.TrackGroupArray;
42 import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
43 import com.google.android.exoplayer2.upstream.BandwidthMeter;
44 import com.google.android.exoplayer2.util.Assertions;
45 import com.google.android.exoplayer2.util.Clock;
46 import com.google.android.exoplayer2.video.VideoListener;
47 import com.google.android.exoplayer2.video.VideoRendererEventListener;
48 import java.io.IOException;
49 import java.util.ArrayList;
50 import java.util.Collections;
51 import java.util.HashMap;
52 import java.util.List;
53 import java.util.Set;
54 import java.util.concurrent.CopyOnWriteArraySet;
55 import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
56 import org.checkerframework.checker.nullness.qual.RequiresNonNull;
57 
58 /**
59  * Data collector which is able to forward analytics events to {@link AnalyticsListener}s by
60  * listening to all available ExoPlayer listeners.
61  */
62 public class AnalyticsCollector
63     implements Player.EventListener,
64         MetadataOutput,
65         AudioRendererEventListener,
66         VideoRendererEventListener,
67         MediaSourceEventListener,
68         BandwidthMeter.EventListener,
69         DrmSessionEventListener,
70         VideoListener,
71         AudioListener {
72 
73   private final CopyOnWriteArraySet<AnalyticsListener> listeners;
74   private final Clock clock;
75   private final Window window;
76   private final MediaPeriodQueueTracker mediaPeriodQueueTracker;
77 
78   private @MonotonicNonNull Player player;
79   private boolean isSeeking;
80 
81   /**
82    * Creates an analytics collector.
83    *
84    * @param clock A {@link Clock} used to generate timestamps.
85    */
AnalyticsCollector(Clock clock)86   public AnalyticsCollector(Clock clock) {
87     this.clock = Assertions.checkNotNull(clock);
88     listeners = new CopyOnWriteArraySet<>();
89     mediaPeriodQueueTracker = new MediaPeriodQueueTracker();
90     window = new Window();
91   }
92 
93   /**
94    * Adds a listener for analytics events.
95    *
96    * @param listener The listener to add.
97    */
addListener(AnalyticsListener listener)98   public void addListener(AnalyticsListener listener) {
99     listeners.add(listener);
100   }
101 
102   /**
103    * Removes a previously added analytics event listener.
104    *
105    * @param listener The listener to remove.
106    */
removeListener(AnalyticsListener listener)107   public void removeListener(AnalyticsListener listener) {
108     listeners.remove(listener);
109   }
110 
111   /**
112    * Sets the player for which data will be collected. Must only be called if no player has been set
113    * yet or the current player is idle.
114    *
115    * @param player The {@link Player} for which data will be collected.
116    */
setPlayer(Player player)117   public void setPlayer(Player player) {
118     Assertions.checkState(
119         this.player == null || mediaPeriodQueueTracker.mediaPeriodInfoQueue.isEmpty());
120     this.player = Assertions.checkNotNull(player);
121   }
122 
123   // External events.
124 
125   /**
126    * Notify analytics collector that a seek operation will start. Should be called before the player
127    * adjusts its state and position to the seek.
128    */
notifySeekStarted()129   public final void notifySeekStarted() {
130     if (!isSeeking) {
131       EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
132       isSeeking = true;
133       for (AnalyticsListener listener : listeners) {
134         listener.onSeekStarted(eventTime);
135       }
136     }
137   }
138 
139   /** Resets the analytics collector for a new playlist. */
resetForNewPlaylist()140   public final void resetForNewPlaylist() {
141     // Copying the list is needed because onMediaPeriodReleased will modify the list.
142     List<MediaPeriodInfo> mediaPeriodInfos =
143         new ArrayList<>(mediaPeriodQueueTracker.mediaPeriodInfoQueue);
144     for (MediaPeriodInfo mediaPeriodInfo : mediaPeriodInfos) {
145       onMediaPeriodReleased(mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId);
146     }
147   }
148 
149   // MetadataOutput implementation.
150 
151   @Override
onMetadata(Metadata metadata)152   public final void onMetadata(Metadata metadata) {
153     EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
154     for (AnalyticsListener listener : listeners) {
155       listener.onMetadata(eventTime, metadata);
156     }
157   }
158 
159   // AudioRendererEventListener implementation.
160 
161   @Override
onAudioEnabled(DecoderCounters counters)162   public final void onAudioEnabled(DecoderCounters counters) {
163     EventTime eventTime = generateReadingMediaPeriodEventTime();
164     for (AnalyticsListener listener : listeners) {
165       listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_AUDIO, counters);
166     }
167   }
168 
169   @Override
onAudioDecoderInitialized( String decoderName, long initializedTimestampMs, long initializationDurationMs)170   public final void onAudioDecoderInitialized(
171       String decoderName, long initializedTimestampMs, long initializationDurationMs) {
172     EventTime eventTime = generateReadingMediaPeriodEventTime();
173     for (AnalyticsListener listener : listeners) {
174       listener.onDecoderInitialized(
175           eventTime, C.TRACK_TYPE_AUDIO, decoderName, initializationDurationMs);
176     }
177   }
178 
179   @Override
onAudioInputFormatChanged(Format format)180   public final void onAudioInputFormatChanged(Format format) {
181     EventTime eventTime = generateReadingMediaPeriodEventTime();
182     for (AnalyticsListener listener : listeners) {
183       listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_AUDIO, format);
184     }
185   }
186 
187   @Override
onAudioSinkUnderrun( int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs)188   public final void onAudioSinkUnderrun(
189       int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
190     EventTime eventTime = generateReadingMediaPeriodEventTime();
191     for (AnalyticsListener listener : listeners) {
192       listener.onAudioUnderrun(eventTime, bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
193     }
194   }
195 
196   @Override
onAudioDisabled(DecoderCounters counters)197   public final void onAudioDisabled(DecoderCounters counters) {
198     EventTime eventTime = generatePlayingMediaPeriodEventTime();
199     for (AnalyticsListener listener : listeners) {
200       listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_AUDIO, counters);
201     }
202   }
203 
204   // AudioListener implementation.
205 
206   @Override
onAudioSessionId(int audioSessionId)207   public final void onAudioSessionId(int audioSessionId) {
208     EventTime eventTime = generateReadingMediaPeriodEventTime();
209     for (AnalyticsListener listener : listeners) {
210       listener.onAudioSessionId(eventTime, audioSessionId);
211     }
212   }
213 
214   @Override
onAudioAttributesChanged(AudioAttributes audioAttributes)215   public void onAudioAttributesChanged(AudioAttributes audioAttributes) {
216     EventTime eventTime = generateReadingMediaPeriodEventTime();
217     for (AnalyticsListener listener : listeners) {
218       listener.onAudioAttributesChanged(eventTime, audioAttributes);
219     }
220   }
221 
222   @Override
onSkipSilenceEnabledChanged(boolean skipSilenceEnabled)223   public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {
224     EventTime eventTime = generateReadingMediaPeriodEventTime();
225     for (AnalyticsListener listener : listeners) {
226       listener.onSkipSilenceEnabledChanged(eventTime, skipSilenceEnabled);
227     }
228   }
229 
230   @Override
onVolumeChanged(float audioVolume)231   public void onVolumeChanged(float audioVolume) {
232     EventTime eventTime = generateReadingMediaPeriodEventTime();
233     for (AnalyticsListener listener : listeners) {
234       listener.onVolumeChanged(eventTime, audioVolume);
235     }
236   }
237 
238   // VideoRendererEventListener implementation.
239 
240   @Override
onVideoEnabled(DecoderCounters counters)241   public final void onVideoEnabled(DecoderCounters counters) {
242     EventTime eventTime = generateReadingMediaPeriodEventTime();
243     for (AnalyticsListener listener : listeners) {
244       listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_VIDEO, counters);
245     }
246   }
247 
248   @Override
onVideoDecoderInitialized( String decoderName, long initializedTimestampMs, long initializationDurationMs)249   public final void onVideoDecoderInitialized(
250       String decoderName, long initializedTimestampMs, long initializationDurationMs) {
251     EventTime eventTime = generateReadingMediaPeriodEventTime();
252     for (AnalyticsListener listener : listeners) {
253       listener.onDecoderInitialized(
254           eventTime, C.TRACK_TYPE_VIDEO, decoderName, initializationDurationMs);
255     }
256   }
257 
258   @Override
onVideoInputFormatChanged(Format format)259   public final void onVideoInputFormatChanged(Format format) {
260     EventTime eventTime = generateReadingMediaPeriodEventTime();
261     for (AnalyticsListener listener : listeners) {
262       listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_VIDEO, format);
263     }
264   }
265 
266   @Override
onDroppedFrames(int count, long elapsedMs)267   public final void onDroppedFrames(int count, long elapsedMs) {
268     EventTime eventTime = generatePlayingMediaPeriodEventTime();
269     for (AnalyticsListener listener : listeners) {
270       listener.onDroppedVideoFrames(eventTime, count, elapsedMs);
271     }
272   }
273 
274   @Override
onVideoDisabled(DecoderCounters counters)275   public final void onVideoDisabled(DecoderCounters counters) {
276     EventTime eventTime = generatePlayingMediaPeriodEventTime();
277     for (AnalyticsListener listener : listeners) {
278       listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_VIDEO, counters);
279     }
280   }
281 
282   @Override
onRenderedFirstFrame(@ullable Surface surface)283   public final void onRenderedFirstFrame(@Nullable Surface surface) {
284     EventTime eventTime = generateReadingMediaPeriodEventTime();
285     for (AnalyticsListener listener : listeners) {
286       listener.onRenderedFirstFrame(eventTime, surface);
287     }
288   }
289 
290   @Override
onVideoFrameProcessingOffset( long totalProcessingOffsetUs, int frameCount, Format format)291   public final void onVideoFrameProcessingOffset(
292       long totalProcessingOffsetUs, int frameCount, Format format) {
293     EventTime eventTime = generatePlayingMediaPeriodEventTime();
294     for (AnalyticsListener listener : listeners) {
295       listener.onVideoFrameProcessingOffset(eventTime, totalProcessingOffsetUs, frameCount, format);
296     }
297   }
298 
299   // VideoListener implementation.
300 
301   @Override
onRenderedFirstFrame()302   public final void onRenderedFirstFrame() {
303     // Do nothing. Already reported in VideoRendererEventListener.onRenderedFirstFrame.
304   }
305 
306   @Override
onVideoSizeChanged( int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio)307   public final void onVideoSizeChanged(
308       int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
309     EventTime eventTime = generateReadingMediaPeriodEventTime();
310     for (AnalyticsListener listener : listeners) {
311       listener.onVideoSizeChanged(
312           eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
313     }
314   }
315 
316   @Override
onSurfaceSizeChanged(int width, int height)317   public void onSurfaceSizeChanged(int width, int height) {
318     EventTime eventTime = generateReadingMediaPeriodEventTime();
319     for (AnalyticsListener listener : listeners) {
320       listener.onSurfaceSizeChanged(eventTime, width, height);
321     }
322   }
323 
324   // MediaSourceEventListener implementation.
325 
326   @Override
onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId)327   public final void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {
328     mediaPeriodQueueTracker.onMediaPeriodCreated(
329         windowIndex, mediaPeriodId, Assertions.checkNotNull(player));
330     EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
331     for (AnalyticsListener listener : listeners) {
332       listener.onMediaPeriodCreated(eventTime);
333     }
334   }
335 
336   @Override
onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId)337   public final void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {
338     EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
339     if (mediaPeriodQueueTracker.onMediaPeriodReleased(
340         mediaPeriodId, Assertions.checkNotNull(player))) {
341       for (AnalyticsListener listener : listeners) {
342         listener.onMediaPeriodReleased(eventTime);
343       }
344     }
345   }
346 
347   @Override
onLoadStarted( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData)348   public final void onLoadStarted(
349       int windowIndex,
350       @Nullable MediaPeriodId mediaPeriodId,
351       LoadEventInfo loadEventInfo,
352       MediaLoadData mediaLoadData) {
353     EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
354     for (AnalyticsListener listener : listeners) {
355       listener.onLoadStarted(eventTime, loadEventInfo, mediaLoadData);
356     }
357   }
358 
359   @Override
onLoadCompleted( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData)360   public final void onLoadCompleted(
361       int windowIndex,
362       @Nullable MediaPeriodId mediaPeriodId,
363       LoadEventInfo loadEventInfo,
364       MediaLoadData mediaLoadData) {
365     EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
366     for (AnalyticsListener listener : listeners) {
367       listener.onLoadCompleted(eventTime, loadEventInfo, mediaLoadData);
368     }
369   }
370 
371   @Override
onLoadCanceled( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData)372   public final void onLoadCanceled(
373       int windowIndex,
374       @Nullable MediaPeriodId mediaPeriodId,
375       LoadEventInfo loadEventInfo,
376       MediaLoadData mediaLoadData) {
377     EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
378     for (AnalyticsListener listener : listeners) {
379       listener.onLoadCanceled(eventTime, loadEventInfo, mediaLoadData);
380     }
381   }
382 
383   @Override
onLoadError( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, boolean wasCanceled)384   public final void onLoadError(
385       int windowIndex,
386       @Nullable MediaPeriodId mediaPeriodId,
387       LoadEventInfo loadEventInfo,
388       MediaLoadData mediaLoadData,
389       IOException error,
390       boolean wasCanceled) {
391     EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
392     for (AnalyticsListener listener : listeners) {
393       listener.onLoadError(eventTime, loadEventInfo, mediaLoadData, error, wasCanceled);
394     }
395   }
396 
397   @Override
onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId)398   public final void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {
399     mediaPeriodQueueTracker.onReadingStarted(mediaPeriodId);
400     EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
401     for (AnalyticsListener listener : listeners) {
402       listener.onReadingStarted(eventTime);
403     }
404   }
405 
406   @Override
onUpstreamDiscarded( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData)407   public final void onUpstreamDiscarded(
408       int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {
409     EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
410     for (AnalyticsListener listener : listeners) {
411       listener.onUpstreamDiscarded(eventTime, mediaLoadData);
412     }
413   }
414 
415   @Override
onDownstreamFormatChanged( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData)416   public final void onDownstreamFormatChanged(
417       int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {
418     EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
419     for (AnalyticsListener listener : listeners) {
420       listener.onDownstreamFormatChanged(eventTime, mediaLoadData);
421     }
422   }
423 
424   // Player.EventListener implementation.
425 
426   // TODO: Add onFinishedReportingChanges to Player.EventListener to know when a set of simultaneous
427   // callbacks finished. This helps to assign exactly the same EventTime to all of them instead of
428   // having slightly different real times.
429 
430   @Override
onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason)431   public final void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
432     mediaPeriodQueueTracker.onTimelineChanged(timeline, Assertions.checkNotNull(player));
433     EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
434     for (AnalyticsListener listener : listeners) {
435       listener.onTimelineChanged(eventTime, reason);
436     }
437   }
438 
439   @Override
onTracksChanged( TrackGroupArray trackGroups, TrackSelectionArray trackSelections)440   public final void onTracksChanged(
441       TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
442     EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
443     for (AnalyticsListener listener : listeners) {
444       listener.onTracksChanged(eventTime, trackGroups, trackSelections);
445     }
446   }
447 
448   @Override
onIsLoadingChanged(boolean isLoading)449   public final void onIsLoadingChanged(boolean isLoading) {
450     EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
451     for (AnalyticsListener listener : listeners) {
452       listener.onIsLoadingChanged(eventTime, isLoading);
453     }
454   }
455 
456   @SuppressWarnings("deprecation")
457   @Override
onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState)458   public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
459     EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
460     for (AnalyticsListener listener : listeners) {
461       listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState);
462     }
463   }
464 
465   @Override
onPlaybackStateChanged(@layer.State int state)466   public final void onPlaybackStateChanged(@Player.State int state) {
467     EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
468     for (AnalyticsListener listener : listeners) {
469       listener.onPlaybackStateChanged(eventTime, state);
470     }
471   }
472 
473   @Override
onPlayWhenReadyChanged( boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason)474   public final void onPlayWhenReadyChanged(
475       boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {
476     EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
477     for (AnalyticsListener listener : listeners) {
478       listener.onPlayWhenReadyChanged(eventTime, playWhenReady, reason);
479     }
480   }
481 
482   @Override
onPlaybackSuppressionReasonChanged( @laybackSuppressionReason int playbackSuppressionReason)483   public void onPlaybackSuppressionReasonChanged(
484       @PlaybackSuppressionReason int playbackSuppressionReason) {
485     EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
486     for (AnalyticsListener listener : listeners) {
487       listener.onPlaybackSuppressionReasonChanged(eventTime, playbackSuppressionReason);
488     }
489   }
490 
491   @Override
onIsPlayingChanged(boolean isPlaying)492   public void onIsPlayingChanged(boolean isPlaying) {
493     EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
494     for (AnalyticsListener listener : listeners) {
495       listener.onIsPlayingChanged(eventTime, isPlaying);
496     }
497   }
498 
499   @Override
onRepeatModeChanged(@layer.RepeatMode int repeatMode)500   public final void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
501     EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
502     for (AnalyticsListener listener : listeners) {
503       listener.onRepeatModeChanged(eventTime, repeatMode);
504     }
505   }
506 
507   @Override
onShuffleModeEnabledChanged(boolean shuffleModeEnabled)508   public final void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
509     EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
510     for (AnalyticsListener listener : listeners) {
511       listener.onShuffleModeChanged(eventTime, shuffleModeEnabled);
512     }
513   }
514 
515   @Override
onPlayerError(ExoPlaybackException error)516   public final void onPlayerError(ExoPlaybackException error) {
517     EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
518     for (AnalyticsListener listener : listeners) {
519       listener.onPlayerError(eventTime, error);
520     }
521   }
522 
523   @Override
onPositionDiscontinuity(@layer.DiscontinuityReason int reason)524   public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
525     if (reason == Player.DISCONTINUITY_REASON_SEEK) {
526       isSeeking = false;
527     }
528     mediaPeriodQueueTracker.onPositionDiscontinuity(Assertions.checkNotNull(player));
529     EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
530     for (AnalyticsListener listener : listeners) {
531       listener.onPositionDiscontinuity(eventTime, reason);
532     }
533   }
534 
535   /**
536    * @deprecated Use {@link #onPlaybackSpeedChanged(float)} and {@link
537    *     #onSkipSilenceEnabledChanged(boolean)} instead.
538    */
539   @SuppressWarnings("deprecation")
540   @Deprecated
541   @Override
onPlaybackParametersChanged(PlaybackParameters playbackParameters)542   public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
543     EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
544     for (AnalyticsListener listener : listeners) {
545       listener.onPlaybackParametersChanged(eventTime, playbackParameters);
546     }
547   }
548 
549   @Override
onPlaybackSpeedChanged(float playbackSpeed)550   public void onPlaybackSpeedChanged(float playbackSpeed) {
551     EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
552     for (AnalyticsListener listener : listeners) {
553       listener.onPlaybackSpeedChanged(eventTime, playbackSpeed);
554     }
555   }
556 
557   @SuppressWarnings("deprecation")
558   @Override
onSeekProcessed()559   public final void onSeekProcessed() {
560     EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
561     for (AnalyticsListener listener : listeners) {
562       listener.onSeekProcessed(eventTime);
563     }
564   }
565 
566   // BandwidthMeter.Listener implementation.
567 
568   @Override
onBandwidthSample(int elapsedMs, long bytes, long bitrate)569   public final void onBandwidthSample(int elapsedMs, long bytes, long bitrate) {
570     EventTime eventTime = generateLoadingMediaPeriodEventTime();
571     for (AnalyticsListener listener : listeners) {
572       listener.onBandwidthEstimate(eventTime, elapsedMs, bytes, bitrate);
573     }
574   }
575 
576   // DefaultDrmSessionManager.EventListener implementation.
577 
578   @Override
onDrmSessionAcquired()579   public final void onDrmSessionAcquired() {
580     EventTime eventTime = generateReadingMediaPeriodEventTime();
581     for (AnalyticsListener listener : listeners) {
582       listener.onDrmSessionAcquired(eventTime);
583     }
584   }
585 
586   @Override
onDrmKeysLoaded()587   public final void onDrmKeysLoaded() {
588     EventTime eventTime = generateReadingMediaPeriodEventTime();
589     for (AnalyticsListener listener : listeners) {
590       listener.onDrmKeysLoaded(eventTime);
591     }
592   }
593 
594   @Override
onDrmSessionManagerError(Exception error)595   public final void onDrmSessionManagerError(Exception error) {
596     EventTime eventTime = generateReadingMediaPeriodEventTime();
597     for (AnalyticsListener listener : listeners) {
598       listener.onDrmSessionManagerError(eventTime, error);
599     }
600   }
601 
602   @Override
onDrmKeysRestored()603   public final void onDrmKeysRestored() {
604     EventTime eventTime = generateReadingMediaPeriodEventTime();
605     for (AnalyticsListener listener : listeners) {
606       listener.onDrmKeysRestored(eventTime);
607     }
608   }
609 
610   @Override
onDrmKeysRemoved()611   public final void onDrmKeysRemoved() {
612     EventTime eventTime = generateReadingMediaPeriodEventTime();
613     for (AnalyticsListener listener : listeners) {
614       listener.onDrmKeysRemoved(eventTime);
615     }
616   }
617 
618   @Override
onDrmSessionReleased()619   public final void onDrmSessionReleased() {
620     EventTime eventTime = generatePlayingMediaPeriodEventTime();
621     for (AnalyticsListener listener : listeners) {
622       listener.onDrmSessionReleased(eventTime);
623     }
624   }
625 
626   // Internal methods.
627 
628   /** Returns read-only set of registered listeners. */
getListeners()629   protected Set<AnalyticsListener> getListeners() {
630     return Collections.unmodifiableSet(listeners);
631   }
632 
633   /** Returns a new {@link EventTime} for the specified timeline, window and media period id. */
634   @RequiresNonNull("player")
generateEventTime( Timeline timeline, int windowIndex, @Nullable MediaPeriodId mediaPeriodId)635   protected EventTime generateEventTime(
636       Timeline timeline, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
637     if (timeline.isEmpty()) {
638       // Ensure media period id is only reported together with a valid timeline.
639       mediaPeriodId = null;
640     }
641     long realtimeMs = clock.elapsedRealtime();
642     long eventPositionMs;
643     boolean isInCurrentWindow =
644         timeline.equals(player.getCurrentTimeline())
645             && windowIndex == player.getCurrentWindowIndex();
646     if (mediaPeriodId != null && mediaPeriodId.isAd()) {
647       boolean isCurrentAd =
648           isInCurrentWindow
649               && player.getCurrentAdGroupIndex() == mediaPeriodId.adGroupIndex
650               && player.getCurrentAdIndexInAdGroup() == mediaPeriodId.adIndexInAdGroup;
651       // Assume start position of 0 for future ads.
652       eventPositionMs = isCurrentAd ? player.getCurrentPosition() : 0;
653     } else if (isInCurrentWindow) {
654       eventPositionMs = player.getContentPosition();
655     } else {
656       // Assume default start position for future content windows. If timeline is not available yet,
657       // assume start position of 0.
658       eventPositionMs =
659           timeline.isEmpty() ? 0 : timeline.getWindow(windowIndex, window).getDefaultPositionMs();
660     }
661     return new EventTime(
662         realtimeMs,
663         timeline,
664         windowIndex,
665         mediaPeriodId,
666         eventPositionMs,
667         player.getCurrentPosition(),
668         player.getTotalBufferedDuration());
669   }
670 
generateEventTime(@ullable MediaPeriodInfo mediaPeriodInfo)671   private EventTime generateEventTime(@Nullable MediaPeriodInfo mediaPeriodInfo) {
672     Assertions.checkNotNull(player);
673     if (mediaPeriodInfo == null) {
674       int windowIndex = player.getCurrentWindowIndex();
675       Timeline timeline = player.getCurrentTimeline();
676       boolean windowIsInTimeline = windowIndex < timeline.getWindowCount();
677       return generateEventTime(
678           windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null);
679     }
680     return generateEventTime(
681         mediaPeriodInfo.timeline, mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId);
682   }
683 
684   private EventTime generateCurrentPlayerMediaPeriodEventTime() {
685     return generateEventTime(mediaPeriodQueueTracker.getCurrentPlayerMediaPeriod());
686   }
687 
688   private EventTime generatePlayingMediaPeriodEventTime() {
689     return generateEventTime(mediaPeriodQueueTracker.getPlayingMediaPeriod());
690   }
691 
692   private EventTime generateReadingMediaPeriodEventTime() {
693     return generateEventTime(mediaPeriodQueueTracker.getReadingMediaPeriod());
694   }
695 
696   private EventTime generateLoadingMediaPeriodEventTime() {
697     return generateEventTime(mediaPeriodQueueTracker.getLoadingMediaPeriod());
698   }
699 
700   private EventTime generateMediaPeriodEventTime(
701       int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
702     Assertions.checkNotNull(player);
703     if (mediaPeriodId != null) {
704       MediaPeriodInfo mediaPeriodInfo = mediaPeriodQueueTracker.getMediaPeriodInfo(mediaPeriodId);
705       return mediaPeriodInfo != null
706           ? generateEventTime(mediaPeriodInfo)
707           : generateEventTime(Timeline.EMPTY, windowIndex, mediaPeriodId);
708     }
709     Timeline timeline = player.getCurrentTimeline();
710     boolean windowIsInTimeline = windowIndex < timeline.getWindowCount();
711     return generateEventTime(
712         windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null);
713   }
714 
715   /** Keeps track of the active media periods and currently playing and reading media period. */
716   private static final class MediaPeriodQueueTracker {
717 
718     // TODO: Investigate reporting MediaPeriodId in renderer events and adding a listener of queue
719     // changes, which would hopefully remove the need to track the queue here.
720 
721     private final ArrayList<MediaPeriodInfo> mediaPeriodInfoQueue;
722     private final HashMap<MediaPeriodId, MediaPeriodInfo> mediaPeriodIdToInfo;
723     private final Period period;
724 
725     @Nullable private MediaPeriodInfo currentPlayerMediaPeriod;
726     private @MonotonicNonNull MediaPeriodInfo playingMediaPeriod;
727     private @MonotonicNonNull MediaPeriodInfo readingMediaPeriod;
728     private Timeline timeline;
729 
730     public MediaPeriodQueueTracker() {
731       mediaPeriodInfoQueue = new ArrayList<>();
732       mediaPeriodIdToInfo = new HashMap<>();
733       period = new Period();
734       timeline = Timeline.EMPTY;
735     }
736 
737     /**
738      * Returns the {@link MediaPeriodInfo} of the media period corresponding the current position of
739      * the player.
740      *
741      * <p>May be null if no matching media period has been created yet.
742      */
743     @Nullable
744     public MediaPeriodInfo getCurrentPlayerMediaPeriod() {
745       return currentPlayerMediaPeriod;
746     }
747 
748     /**
749      * Returns the {@link MediaPeriodInfo} of the media period at the front of the queue. If the
750      * queue is empty, this is the last media period which was at the front of the queue.
751      *
752      * <p>May be null, if no media period has been created yet.
753      */
754     @Nullable
755     public MediaPeriodInfo getPlayingMediaPeriod() {
756       return playingMediaPeriod;
757     }
758 
759     /**
760      * Returns the {@link MediaPeriodInfo} of the media period currently being read by the player.
761      *
762      * <p>May be null, if the player has not started reading any media period.
763      */
764     @Nullable
765     public MediaPeriodInfo getReadingMediaPeriod() {
766       return readingMediaPeriod;
767     }
768 
769     /**
770      * Returns the {@link MediaPeriodInfo} of the media period at the end of the queue which is
771      * currently loading or will be the next one loading.
772      *
773      * <p>May be null, if no media period is active yet.
774      */
775     @Nullable
776     public MediaPeriodInfo getLoadingMediaPeriod() {
777       return mediaPeriodInfoQueue.isEmpty()
778           ? null
779           : mediaPeriodInfoQueue.get(mediaPeriodInfoQueue.size() - 1);
780     }
781 
782     /** Returns the {@link MediaPeriodInfo} for the given {@link MediaPeriodId}. */
783     @Nullable
784     public MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId mediaPeriodId) {
785       return mediaPeriodIdToInfo.get(mediaPeriodId);
786     }
787 
788     /** Updates the queue with a reported position discontinuity. */
789     public void onPositionDiscontinuity(Player player) {
790       currentPlayerMediaPeriod = findMatchingMediaPeriodInQueue(player);
791     }
792 
793     /** Updates the queue with a reported timeline change. */
794     public void onTimelineChanged(Timeline timeline, Player player) {
795       for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) {
796         MediaPeriodInfo newMediaPeriodInfo =
797             updateMediaPeriodInfoToNewTimeline(mediaPeriodInfoQueue.get(i), timeline);
798         mediaPeriodInfoQueue.set(i, newMediaPeriodInfo);
799         mediaPeriodIdToInfo.put(newMediaPeriodInfo.mediaPeriodId, newMediaPeriodInfo);
800       }
801       if (!mediaPeriodInfoQueue.isEmpty()) {
802         playingMediaPeriod = mediaPeriodInfoQueue.get(0);
803       } else if (playingMediaPeriod != null) {
804         playingMediaPeriod = updateMediaPeriodInfoToNewTimeline(playingMediaPeriod, timeline);
805       }
806       if (readingMediaPeriod != null) {
807         readingMediaPeriod = updateMediaPeriodInfoToNewTimeline(readingMediaPeriod, timeline);
808       } else if (playingMediaPeriod != null) {
809         readingMediaPeriod = playingMediaPeriod;
810       }
811       this.timeline = timeline;
812       currentPlayerMediaPeriod = findMatchingMediaPeriodInQueue(player);
813     }
814 
815     /** Updates the queue with a newly created media period. */
816     public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId, Player player) {
817       int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid);
818       boolean isInTimeline = periodIndex != C.INDEX_UNSET;
819       MediaPeriodInfo mediaPeriodInfo =
820           new MediaPeriodInfo(
821               mediaPeriodId,
822               isInTimeline ? timeline : Timeline.EMPTY,
823               isInTimeline ? timeline.getPeriod(periodIndex, period).windowIndex : windowIndex);
824       mediaPeriodInfoQueue.add(mediaPeriodInfo);
825       mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo);
826       playingMediaPeriod = mediaPeriodInfoQueue.get(0);
827       if (currentPlayerMediaPeriod == null && isMatchingPlayingMediaPeriod(player)) {
828         currentPlayerMediaPeriod = playingMediaPeriod;
829       }
830       if (mediaPeriodInfoQueue.size() == 1) {
831         readingMediaPeriod = playingMediaPeriod;
832       }
833     }
834 
835     /**
836      * Updates the queue with a released media period. Returns whether the media period was still in
837      * the queue.
838      */
839     public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId, Player player) {
840       @Nullable MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId);
841       if (mediaPeriodInfo == null) {
842         // The media period has already been removed from the queue in resetForNewPlaylist().
843         return false;
844       }
845       mediaPeriodInfoQueue.remove(mediaPeriodInfo);
846       if (readingMediaPeriod != null && mediaPeriodId.equals(readingMediaPeriod.mediaPeriodId)) {
847         readingMediaPeriod =
848             mediaPeriodInfoQueue.isEmpty()
849                 ? Assertions.checkNotNull(playingMediaPeriod)
850                 : mediaPeriodInfoQueue.get(0);
851       }
852       if (!mediaPeriodInfoQueue.isEmpty()) {
853         playingMediaPeriod = mediaPeriodInfoQueue.get(0);
854       }
855       if (currentPlayerMediaPeriod == null && isMatchingPlayingMediaPeriod(player)) {
856         currentPlayerMediaPeriod = playingMediaPeriod;
857       }
858       return true;
859     }
860 
861     /** Update the queue with a change in the reading media period. */
862     public void onReadingStarted(MediaPeriodId mediaPeriodId) {
863       @Nullable MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.get(mediaPeriodId);
864       if (mediaPeriodInfo == null) {
865         // The media period has already been removed from the queue in resetForNewPlaylist().
866         return;
867       }
868       readingMediaPeriod = mediaPeriodInfo;
869     }
870 
871     @Nullable
872     private MediaPeriodInfo findMatchingMediaPeriodInQueue(Player player) {
873       Timeline playerTimeline = player.getCurrentTimeline();
874       int playerPeriodIndex = player.getCurrentPeriodIndex();
875       @Nullable
876       Object playerPeriodUid =
877           playerTimeline.isEmpty() ? null : playerTimeline.getUidOfPeriod(playerPeriodIndex);
878       int playerNextAdGroupIndex =
879           player.isPlayingAd() || playerTimeline.isEmpty()
880               ? C.INDEX_UNSET
881               : playerTimeline
882                   .getPeriod(playerPeriodIndex, period)
883                   .getAdGroupIndexAfterPositionUs(
884                       C.msToUs(player.getCurrentPosition()) - period.getPositionInWindowUs());
885       for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) {
886         MediaPeriodInfo mediaPeriodInfo = mediaPeriodInfoQueue.get(i);
887         if (isMatchingMediaPeriod(
888             mediaPeriodInfo,
889             playerTimeline,
890             player.getCurrentWindowIndex(),
891             playerPeriodUid,
892             player.isPlayingAd(),
893             player.getCurrentAdGroupIndex(),
894             player.getCurrentAdIndexInAdGroup(),
895             playerNextAdGroupIndex)) {
896           return mediaPeriodInfo;
897         }
898       }
899       if (mediaPeriodInfoQueue.isEmpty() && playingMediaPeriod != null) {
900         if (isMatchingMediaPeriod(
901             playingMediaPeriod,
902             playerTimeline,
903             player.getCurrentWindowIndex(),
904             playerPeriodUid,
905             player.isPlayingAd(),
906             player.getCurrentAdGroupIndex(),
907             player.getCurrentAdIndexInAdGroup(),
908             playerNextAdGroupIndex)) {
909           return playingMediaPeriod;
910         }
911       }
912       return null;
913     }
914 
915     private boolean isMatchingPlayingMediaPeriod(Player player) {
916       if (playingMediaPeriod == null) {
917         return false;
918       }
919       Timeline playerTimeline = player.getCurrentTimeline();
920       int playerPeriodIndex = player.getCurrentPeriodIndex();
921       @Nullable
922       Object playerPeriodUid =
923           playerTimeline.isEmpty() ? null : playerTimeline.getUidOfPeriod(playerPeriodIndex);
924       int playerNextAdGroupIndex =
925           player.isPlayingAd() || playerTimeline.isEmpty()
926               ? C.INDEX_UNSET
927               : playerTimeline
928                   .getPeriod(playerPeriodIndex, period)
929                   .getAdGroupIndexAfterPositionUs(
930                       C.msToUs(player.getCurrentPosition()) - period.getPositionInWindowUs());
931       return isMatchingMediaPeriod(
932           playingMediaPeriod,
933           playerTimeline,
934           player.getCurrentWindowIndex(),
935           playerPeriodUid,
936           player.isPlayingAd(),
937           player.getCurrentAdGroupIndex(),
938           player.getCurrentAdIndexInAdGroup(),
939           playerNextAdGroupIndex);
940     }
941 
942     private static boolean isMatchingMediaPeriod(
943         MediaPeriodInfo mediaPeriodInfo,
944         Timeline playerTimeline,
945         int playerWindowIndex,
946         @Nullable Object playerPeriodUid,
947         boolean isPlayingAd,
948         int playerAdGroupIndex,
949         int playerAdIndexInAdGroup,
950         int playerNextAdGroupIndex) {
951       if (mediaPeriodInfo.timeline.isEmpty()
952           || !mediaPeriodInfo.timeline.equals(playerTimeline)
953           || mediaPeriodInfo.windowIndex != playerWindowIndex
954           || !mediaPeriodInfo.mediaPeriodId.periodUid.equals(playerPeriodUid)) {
955         return false;
956       }
957       // Timeline period matches. Still need to check ad information.
958       return (isPlayingAd
959               && mediaPeriodInfo.mediaPeriodId.adGroupIndex == playerAdGroupIndex
960               && mediaPeriodInfo.mediaPeriodId.adIndexInAdGroup == playerAdIndexInAdGroup)
961           || (!isPlayingAd
962               && mediaPeriodInfo.mediaPeriodId.adGroupIndex == C.INDEX_UNSET
963               && mediaPeriodInfo.mediaPeriodId.nextAdGroupIndex == playerNextAdGroupIndex);
964     }
965 
966     private MediaPeriodInfo updateMediaPeriodInfoToNewTimeline(
967         MediaPeriodInfo info, Timeline newTimeline) {
968       int newPeriodIndex = newTimeline.getIndexOfPeriod(info.mediaPeriodId.periodUid);
969       if (newPeriodIndex == C.INDEX_UNSET) {
970         // Media period is not yet or no longer available in the new timeline. Keep it as it is.
971         return info;
972       }
973       int newWindowIndex = newTimeline.getPeriod(newPeriodIndex, period).windowIndex;
974       return new MediaPeriodInfo(info.mediaPeriodId, newTimeline, newWindowIndex);
975     }
976   }
977 
978   /** Information about a media period and its associated timeline. */
979   private static final class MediaPeriodInfo {
980 
981     /** The {@link MediaPeriodId} of the media period. */
982     public final MediaPeriodId mediaPeriodId;
983     /**
984      * The {@link Timeline} in which the media period can be found. Or {@link Timeline#EMPTY} if the
985      * media period is not part of a known timeline yet.
986      */
987     public final Timeline timeline;
988     /**
989      * The window index of the media period in the timeline. If the timeline is empty, this is the
990      * prospective window index.
991      */
992     public final int windowIndex;
993 
994     public MediaPeriodInfo(MediaPeriodId mediaPeriodId, Timeline timeline, int windowIndex) {
995       this.mediaPeriodId = mediaPeriodId;
996       this.timeline = timeline;
997       this.windowIndex = windowIndex;
998     }
999   }
1000 }
1001