summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsCollector.java881
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsListener.java514
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java23
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java355
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java120
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStats.java980
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java1059
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/package-info.java19
8 files changed, 3951 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
new file mode 100644
index 0000000000..6bdb4c7727
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
@@ -0,0 +1,881 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.analytics;
+
+import android.view.Surface;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.PlaybackSuppressionReason;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline.Period;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline.Window;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioAttributes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioRendererEventListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderCounters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoRendererEventListener;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
+
+/**
+ * Data collector which is able to forward analytics events to {@link AnalyticsListener}s by
+ * listening to all available ExoPlayer listeners.
+ */
+public class AnalyticsCollector
+ implements Player.EventListener,
+ MetadataOutput,
+ AudioRendererEventListener,
+ VideoRendererEventListener,
+ MediaSourceEventListener,
+ BandwidthMeter.EventListener,
+ DefaultDrmSessionEventListener,
+ VideoListener,
+ AudioListener {
+
+ private final CopyOnWriteArraySet<AnalyticsListener> listeners;
+ private final Clock clock;
+ private final Window window;
+ private final MediaPeriodQueueTracker mediaPeriodQueueTracker;
+
+ private @MonotonicNonNull Player player;
+
+ /**
+ * Creates an analytics collector.
+ *
+ * @param clock A {@link Clock} used to generate timestamps.
+ */
+ public AnalyticsCollector(Clock clock) {
+ this.clock = Assertions.checkNotNull(clock);
+ listeners = new CopyOnWriteArraySet<>();
+ mediaPeriodQueueTracker = new MediaPeriodQueueTracker();
+ window = new Window();
+ }
+
+ /**
+ * Adds a listener for analytics events.
+ *
+ * @param listener The listener to add.
+ */
+ public void addListener(AnalyticsListener listener) {
+ listeners.add(listener);
+ }
+
+ /**
+ * Removes a previously added analytics event listener.
+ *
+ * @param listener The listener to remove.
+ */
+ public void removeListener(AnalyticsListener listener) {
+ listeners.remove(listener);
+ }
+
+ /**
+ * Sets the player for which data will be collected. Must only be called if no player has been set
+ * yet or the current player is idle.
+ *
+ * @param player The {@link Player} for which data will be collected.
+ */
+ public void setPlayer(Player player) {
+ Assertions.checkState(
+ this.player == null || mediaPeriodQueueTracker.mediaPeriodInfoQueue.isEmpty());
+ this.player = Assertions.checkNotNull(player);
+ }
+
+ // External events.
+
+ /**
+ * Notify analytics collector that a seek operation will start. Should be called before the player
+ * adjusts its state and position to the seek.
+ */
+ public final void notifySeekStarted() {
+ if (!mediaPeriodQueueTracker.isSeeking()) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ mediaPeriodQueueTracker.onSeekStarted();
+ for (AnalyticsListener listener : listeners) {
+ listener.onSeekStarted(eventTime);
+ }
+ }
+ }
+
+ /**
+ * Resets the analytics collector for a new media source. Should be called before the player is
+ * prepared with a new media source.
+ */
+ public final void resetForNewMediaSource() {
+ // Copying the list is needed because onMediaPeriodReleased will modify the list.
+ List<MediaPeriodInfo> mediaPeriodInfos =
+ new ArrayList<>(mediaPeriodQueueTracker.mediaPeriodInfoQueue);
+ for (MediaPeriodInfo mediaPeriodInfo : mediaPeriodInfos) {
+ onMediaPeriodReleased(mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId);
+ }
+ }
+
+ // MetadataOutput implementation.
+
+ @Override
+ public final void onMetadata(Metadata metadata) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onMetadata(eventTime, metadata);
+ }
+ }
+
+ // AudioRendererEventListener implementation.
+
+ @Override
+ public final void onAudioEnabled(DecoderCounters counters) {
+ // The renderers are only enabled after we changed the playing media period.
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_AUDIO, counters);
+ }
+ }
+
+ @Override
+ public final void onAudioDecoderInitialized(
+ String decoderName, long initializedTimestampMs, long initializationDurationMs) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDecoderInitialized(
+ eventTime, C.TRACK_TYPE_AUDIO, decoderName, initializationDurationMs);
+ }
+ }
+
+ @Override
+ public final void onAudioInputFormatChanged(Format format) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_AUDIO, format);
+ }
+ }
+
+ @Override
+ public final void onAudioSinkUnderrun(
+ int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onAudioUnderrun(eventTime, bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+ }
+ }
+
+ @Override
+ public final void onAudioDisabled(DecoderCounters counters) {
+ // The renderers are disabled after we changed the playing media period on the playback thread
+ // but before this change is reported to the app thread.
+ EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_AUDIO, counters);
+ }
+ }
+
+ // AudioListener implementation.
+
+ @Override
+ public final void onAudioSessionId(int audioSessionId) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onAudioSessionId(eventTime, audioSessionId);
+ }
+ }
+
+ @Override
+ public void onAudioAttributesChanged(AudioAttributes audioAttributes) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onAudioAttributesChanged(eventTime, audioAttributes);
+ }
+ }
+
+ @Override
+ public void onVolumeChanged(float audioVolume) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onVolumeChanged(eventTime, audioVolume);
+ }
+ }
+
+ // VideoRendererEventListener implementation.
+
+ @Override
+ public final void onVideoEnabled(DecoderCounters counters) {
+ // The renderers are only enabled after we changed the playing media period.
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_VIDEO, counters);
+ }
+ }
+
+ @Override
+ public final void onVideoDecoderInitialized(
+ String decoderName, long initializedTimestampMs, long initializationDurationMs) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDecoderInitialized(
+ eventTime, C.TRACK_TYPE_VIDEO, decoderName, initializationDurationMs);
+ }
+ }
+
+ @Override
+ public final void onVideoInputFormatChanged(Format format) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_VIDEO, format);
+ }
+ }
+
+ @Override
+ public final void onDroppedFrames(int count, long elapsedMs) {
+ EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDroppedVideoFrames(eventTime, count, elapsedMs);
+ }
+ }
+
+ @Override
+ public final void onVideoDisabled(DecoderCounters counters) {
+ // The renderers are disabled after we changed the playing media period on the playback thread
+ // but before this change is reported to the app thread.
+ EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_VIDEO, counters);
+ }
+ }
+
+ @Override
+ public final void onRenderedFirstFrame(@Nullable Surface surface) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onRenderedFirstFrame(eventTime, surface);
+ }
+ }
+
+ // VideoListener implementation.
+
+ @Override
+ public final void onRenderedFirstFrame() {
+ // Do nothing. Already reported in VideoRendererEventListener.onRenderedFirstFrame.
+ }
+
+ @Override
+ public final void onVideoSizeChanged(
+ int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onVideoSizeChanged(
+ eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
+ }
+ }
+
+ @Override
+ public void onSurfaceSizeChanged(int width, int height) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onSurfaceSizeChanged(eventTime, width, height);
+ }
+ }
+
+ // MediaSourceEventListener implementation.
+
+ @Override
+ public final void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {
+ mediaPeriodQueueTracker.onMediaPeriodCreated(windowIndex, mediaPeriodId);
+ EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
+ for (AnalyticsListener listener : listeners) {
+ listener.onMediaPeriodCreated(eventTime);
+ }
+ }
+
+ @Override
+ public final void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {
+ EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
+ if (mediaPeriodQueueTracker.onMediaPeriodReleased(mediaPeriodId)) {
+ for (AnalyticsListener listener : listeners) {
+ listener.onMediaPeriodReleased(eventTime);
+ }
+ }
+ }
+
+ @Override
+ public final void onLoadStarted(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData) {
+ EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
+ for (AnalyticsListener listener : listeners) {
+ listener.onLoadStarted(eventTime, loadEventInfo, mediaLoadData);
+ }
+ }
+
+ @Override
+ public final void onLoadCompleted(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData) {
+ EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
+ for (AnalyticsListener listener : listeners) {
+ listener.onLoadCompleted(eventTime, loadEventInfo, mediaLoadData);
+ }
+ }
+
+ @Override
+ public final void onLoadCanceled(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData) {
+ EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
+ for (AnalyticsListener listener : listeners) {
+ listener.onLoadCanceled(eventTime, loadEventInfo, mediaLoadData);
+ }
+ }
+
+ @Override
+ public final void onLoadError(
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData,
+ IOException error,
+ boolean wasCanceled) {
+ EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
+ for (AnalyticsListener listener : listeners) {
+ listener.onLoadError(eventTime, loadEventInfo, mediaLoadData, error, wasCanceled);
+ }
+ }
+
+ @Override
+ public final void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {
+ mediaPeriodQueueTracker.onReadingStarted(mediaPeriodId);
+ EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
+ for (AnalyticsListener listener : listeners) {
+ listener.onReadingStarted(eventTime);
+ }
+ }
+
+ @Override
+ public final void onUpstreamDiscarded(
+ int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {
+ EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
+ for (AnalyticsListener listener : listeners) {
+ listener.onUpstreamDiscarded(eventTime, mediaLoadData);
+ }
+ }
+
+ @Override
+ public final void onDownstreamFormatChanged(
+ int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {
+ EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
+ for (AnalyticsListener listener : listeners) {
+ listener.onDownstreamFormatChanged(eventTime, mediaLoadData);
+ }
+ }
+
+ // Player.EventListener implementation.
+
+ // TODO: Add onFinishedReportingChanges to Player.EventListener to know when a set of simultaneous
+ // callbacks finished. This helps to assign exactly the same EventTime to all of them instead of
+ // having slightly different real times.
+
+ @Override
+ public final void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
+ mediaPeriodQueueTracker.onTimelineChanged(timeline);
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onTimelineChanged(eventTime, reason);
+ }
+ }
+
+ @Override
+ public final void onTracksChanged(
+ TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onTracksChanged(eventTime, trackGroups, trackSelections);
+ }
+ }
+
+ @Override
+ public final void onLoadingChanged(boolean isLoading) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onLoadingChanged(eventTime, isLoading);
+ }
+ }
+
+ @Override
+ public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState);
+ }
+ }
+
+ @Override
+ public void onPlaybackSuppressionReasonChanged(
+ @PlaybackSuppressionReason int playbackSuppressionReason) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onPlaybackSuppressionReasonChanged(eventTime, playbackSuppressionReason);
+ }
+ }
+
+ @Override
+ public void onIsPlayingChanged(boolean isPlaying) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onIsPlayingChanged(eventTime, isPlaying);
+ }
+ }
+
+ @Override
+ public final void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onRepeatModeChanged(eventTime, repeatMode);
+ }
+ }
+
+ @Override
+ public final void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onShuffleModeChanged(eventTime, shuffleModeEnabled);
+ }
+ }
+
+ @Override
+ public final void onPlayerError(ExoPlaybackException error) {
+ EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onPlayerError(eventTime, error);
+ }
+ }
+
+ @Override
+ public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
+ mediaPeriodQueueTracker.onPositionDiscontinuity(reason);
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onPositionDiscontinuity(eventTime, reason);
+ }
+ }
+
+ @Override
+ public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onPlaybackParametersChanged(eventTime, playbackParameters);
+ }
+ }
+
+ @Override
+ public final void onSeekProcessed() {
+ if (mediaPeriodQueueTracker.isSeeking()) {
+ mediaPeriodQueueTracker.onSeekProcessed();
+ EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onSeekProcessed(eventTime);
+ }
+ }
+ }
+
+ // BandwidthMeter.Listener implementation.
+
+ @Override
+ public final void onBandwidthSample(int elapsedMs, long bytes, long bitrate) {
+ EventTime eventTime = generateLoadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onBandwidthEstimate(eventTime, elapsedMs, bytes, bitrate);
+ }
+ }
+
+ // DefaultDrmSessionManager.EventListener implementation.
+
+ @Override
+ public final void onDrmSessionAcquired() {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDrmSessionAcquired(eventTime);
+ }
+ }
+
+ @Override
+ public final void onDrmKeysLoaded() {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDrmKeysLoaded(eventTime);
+ }
+ }
+
+ @Override
+ public final void onDrmSessionManagerError(Exception error) {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDrmSessionManagerError(eventTime, error);
+ }
+ }
+
+ @Override
+ public final void onDrmKeysRestored() {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDrmKeysRestored(eventTime);
+ }
+ }
+
+ @Override
+ public final void onDrmKeysRemoved() {
+ EventTime eventTime = generateReadingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDrmKeysRemoved(eventTime);
+ }
+ }
+
+ @Override
+ public final void onDrmSessionReleased() {
+ EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime();
+ for (AnalyticsListener listener : listeners) {
+ listener.onDrmSessionReleased(eventTime);
+ }
+ }
+
+ // Internal methods.
+
+ /** Returns read-only set of registered listeners. */
+ protected Set<AnalyticsListener> getListeners() {
+ return Collections.unmodifiableSet(listeners);
+ }
+
+ /** Returns a new {@link EventTime} for the specified timeline, window and media period id. */
+ @RequiresNonNull("player")
+ protected EventTime generateEventTime(
+ Timeline timeline, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
+ if (timeline.isEmpty()) {
+ // Ensure media period id is only reported together with a valid timeline.
+ mediaPeriodId = null;
+ }
+ long realtimeMs = clock.elapsedRealtime();
+ long eventPositionMs;
+ boolean isInCurrentWindow =
+ timeline == player.getCurrentTimeline() && windowIndex == player.getCurrentWindowIndex();
+ if (mediaPeriodId != null && mediaPeriodId.isAd()) {
+ boolean isCurrentAd =
+ isInCurrentWindow
+ && player.getCurrentAdGroupIndex() == mediaPeriodId.adGroupIndex
+ && player.getCurrentAdIndexInAdGroup() == mediaPeriodId.adIndexInAdGroup;
+ // Assume start position of 0 for future ads.
+ eventPositionMs = isCurrentAd ? player.getCurrentPosition() : 0;
+ } else if (isInCurrentWindow) {
+ eventPositionMs = player.getContentPosition();
+ } else {
+ // Assume default start position for future content windows. If timeline is not available yet,
+ // assume start position of 0.
+ eventPositionMs =
+ timeline.isEmpty() ? 0 : timeline.getWindow(windowIndex, window).getDefaultPositionMs();
+ }
+ return new EventTime(
+ realtimeMs,
+ timeline,
+ windowIndex,
+ mediaPeriodId,
+ eventPositionMs,
+ player.getCurrentPosition(),
+ player.getTotalBufferedDuration());
+ }
+
+ private EventTime generateEventTime(@Nullable MediaPeriodInfo mediaPeriodInfo) {
+ Assertions.checkNotNull(player);
+ if (mediaPeriodInfo == null) {
+ int windowIndex = player.getCurrentWindowIndex();
+ mediaPeriodInfo = mediaPeriodQueueTracker.tryResolveWindowIndex(windowIndex);
+ if (mediaPeriodInfo == null) {
+ Timeline timeline = player.getCurrentTimeline();
+ boolean windowIsInTimeline = windowIndex < timeline.getWindowCount();
+ return generateEventTime(
+ windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null);
+ }
+ }
+ return generateEventTime(
+ mediaPeriodInfo.timeline, mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId);
+ }
+
+ private EventTime generateLastReportedPlayingMediaPeriodEventTime() {
+ return generateEventTime(mediaPeriodQueueTracker.getLastReportedPlayingMediaPeriod());
+ }
+
+ private EventTime generatePlayingMediaPeriodEventTime() {
+ return generateEventTime(mediaPeriodQueueTracker.getPlayingMediaPeriod());
+ }
+
+ private EventTime generateReadingMediaPeriodEventTime() {
+ return generateEventTime(mediaPeriodQueueTracker.getReadingMediaPeriod());
+ }
+
+ private EventTime generateLoadingMediaPeriodEventTime() {
+ return generateEventTime(mediaPeriodQueueTracker.getLoadingMediaPeriod());
+ }
+
+ private EventTime generateMediaPeriodEventTime(
+ int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
+ Assertions.checkNotNull(player);
+ if (mediaPeriodId != null) {
+ MediaPeriodInfo mediaPeriodInfo = mediaPeriodQueueTracker.getMediaPeriodInfo(mediaPeriodId);
+ return mediaPeriodInfo != null
+ ? generateEventTime(mediaPeriodInfo)
+ : generateEventTime(Timeline.EMPTY, windowIndex, mediaPeriodId);
+ }
+ Timeline timeline = player.getCurrentTimeline();
+ boolean windowIsInTimeline = windowIndex < timeline.getWindowCount();
+ return generateEventTime(
+ windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null);
+ }
+
+ /** Keeps track of the active media periods and currently playing and reading media period. */
+ private static final class MediaPeriodQueueTracker {
+
+ // TODO: Investigate reporting MediaPeriodId in renderer events and adding a listener of queue
+ // changes, which would hopefully remove the need to track the queue here.
+
+ private final ArrayList<MediaPeriodInfo> mediaPeriodInfoQueue;
+ private final HashMap<MediaPeriodId, MediaPeriodInfo> mediaPeriodIdToInfo;
+ private final Period period;
+
+ @Nullable private MediaPeriodInfo lastPlayingMediaPeriod;
+ @Nullable private MediaPeriodInfo lastReportedPlayingMediaPeriod;
+ @Nullable private MediaPeriodInfo readingMediaPeriod;
+ private Timeline timeline;
+ private boolean isSeeking;
+
+ public MediaPeriodQueueTracker() {
+ mediaPeriodInfoQueue = new ArrayList<>();
+ mediaPeriodIdToInfo = new HashMap<>();
+ period = new Period();
+ timeline = Timeline.EMPTY;
+ }
+
+ /**
+ * Returns the {@link MediaPeriodInfo} of the media period in the front of the queue. This is
+ * the playing media period unless the player hasn't started playing yet (in which case it is
+ * the loading media period or null). While the player is seeking or preparing, this method will
+ * always return null to reflect the uncertainty about the current playing period. May also be
+ * null, if the timeline is empty or no media period is active yet.
+ */
+ @Nullable
+ public MediaPeriodInfo getPlayingMediaPeriod() {
+ return mediaPeriodInfoQueue.isEmpty() || timeline.isEmpty() || isSeeking
+ ? null
+ : mediaPeriodInfoQueue.get(0);
+ }
+
+ /**
+ * Returns the {@link MediaPeriodInfo} of the currently playing media period. This is the
+ * publicly reported period which should always match {@link Player#getCurrentPeriodIndex()}
+ * unless the player is currently seeking or being prepared in which case the previous period is
+ * reported until the seek or preparation is processed. May be null, if no media period is
+ * active yet.
+ */
+ @Nullable
+ public MediaPeriodInfo getLastReportedPlayingMediaPeriod() {
+ return lastReportedPlayingMediaPeriod;
+ }
+
+ /**
+ * Returns the {@link MediaPeriodInfo} of the media period currently being read by the player.
+ * May be null, if the player is not reading a media period.
+ */
+ @Nullable
+ public MediaPeriodInfo getReadingMediaPeriod() {
+ return readingMediaPeriod;
+ }
+
+ /**
+ * Returns the {@link MediaPeriodInfo} of the media period at the end of the queue which is
+ * currently loading or will be the next one loading. May be null, if no media period is active
+ * yet.
+ */
+ @Nullable
+ public MediaPeriodInfo getLoadingMediaPeriod() {
+ return mediaPeriodInfoQueue.isEmpty()
+ ? null
+ : mediaPeriodInfoQueue.get(mediaPeriodInfoQueue.size() - 1);
+ }
+
+ /** Returns the {@link MediaPeriodInfo} for the given {@link MediaPeriodId}. */
+ @Nullable
+ public MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId mediaPeriodId) {
+ return mediaPeriodIdToInfo.get(mediaPeriodId);
+ }
+
+ /** Returns whether the player is currently seeking. */
+ public boolean isSeeking() {
+ return isSeeking;
+ }
+
+ /**
+ * Tries to find an existing media period info from the specified window index. Only returns a
+ * non-null media period info if there is a unique, unambiguous match.
+ */
+ @Nullable
+ public MediaPeriodInfo tryResolveWindowIndex(int windowIndex) {
+ MediaPeriodInfo match = null;
+ for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) {
+ MediaPeriodInfo info = mediaPeriodInfoQueue.get(i);
+ int periodIndex = timeline.getIndexOfPeriod(info.mediaPeriodId.periodUid);
+ if (periodIndex != C.INDEX_UNSET
+ && timeline.getPeriod(periodIndex, period).windowIndex == windowIndex) {
+ if (match != null) {
+ // Ambiguous match.
+ return null;
+ }
+ match = info;
+ }
+ }
+ return match;
+ }
+
+ /** Updates the queue with a reported position discontinuity . */
+ public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
+ lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod;
+ }
+
+ /** Updates the queue with a reported timeline change. */
+ public void onTimelineChanged(Timeline timeline) {
+ for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) {
+ MediaPeriodInfo newMediaPeriodInfo =
+ updateMediaPeriodInfoToNewTimeline(mediaPeriodInfoQueue.get(i), timeline);
+ mediaPeriodInfoQueue.set(i, newMediaPeriodInfo);
+ mediaPeriodIdToInfo.put(newMediaPeriodInfo.mediaPeriodId, newMediaPeriodInfo);
+ }
+ if (readingMediaPeriod != null) {
+ readingMediaPeriod = updateMediaPeriodInfoToNewTimeline(readingMediaPeriod, timeline);
+ }
+ this.timeline = timeline;
+ lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod;
+ }
+
+ /** Updates the queue with a reported start of seek. */
+ public void onSeekStarted() {
+ isSeeking = true;
+ }
+
+ /** Updates the queue with a reported processed seek. */
+ public void onSeekProcessed() {
+ isSeeking = false;
+ lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod;
+ }
+
+ /** Updates the queue with a newly created media period. */
+ public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {
+ int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid);
+ boolean isInTimeline = periodIndex != C.INDEX_UNSET;
+ MediaPeriodInfo mediaPeriodInfo =
+ new MediaPeriodInfo(
+ mediaPeriodId,
+ isInTimeline ? timeline : Timeline.EMPTY,
+ isInTimeline ? timeline.getPeriod(periodIndex, period).windowIndex : windowIndex);
+ mediaPeriodInfoQueue.add(mediaPeriodInfo);
+ mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo);
+ lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0);
+ if (mediaPeriodInfoQueue.size() == 1 && !timeline.isEmpty()) {
+ lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod;
+ }
+ }
+
+ /**
+ * Updates the queue with a released media period. Returns whether the media period was still in
+ * the queue.
+ */
+ public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId) {
+ MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId);
+ if (mediaPeriodInfo == null) {
+ // The media period has already been removed from the queue in resetForNewMediaSource().
+ return false;
+ }
+ mediaPeriodInfoQueue.remove(mediaPeriodInfo);
+ if (readingMediaPeriod != null && mediaPeriodId.equals(readingMediaPeriod.mediaPeriodId)) {
+ readingMediaPeriod = mediaPeriodInfoQueue.isEmpty() ? null : mediaPeriodInfoQueue.get(0);
+ }
+ if (!mediaPeriodInfoQueue.isEmpty()) {
+ lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0);
+ }
+ return true;
+ }
+
+ /** Update the queue with a change in the reading media period. */
+ public void onReadingStarted(MediaPeriodId mediaPeriodId) {
+ readingMediaPeriod = mediaPeriodIdToInfo.get(mediaPeriodId);
+ }
+
+ private MediaPeriodInfo updateMediaPeriodInfoToNewTimeline(
+ MediaPeriodInfo info, Timeline newTimeline) {
+ int newPeriodIndex = newTimeline.getIndexOfPeriod(info.mediaPeriodId.periodUid);
+ if (newPeriodIndex == C.INDEX_UNSET) {
+ // Media period is not yet or no longer available in the new timeline. Keep it as it is.
+ return info;
+ }
+ int newWindowIndex = newTimeline.getPeriod(newPeriodIndex, period).windowIndex;
+ return new MediaPeriodInfo(info.mediaPeriodId, newTimeline, newWindowIndex);
+ }
+ }
+
+ /** Information about a media period and its associated timeline. */
+ private static final class MediaPeriodInfo {
+
+ /** The {@link MediaPeriodId} of the media period. */
+ public final MediaPeriodId mediaPeriodId;
+ /**
+ * The {@link Timeline} in which the media period can be found. Or {@link Timeline#EMPTY} if the
+ * media period is not part of a known timeline yet.
+ */
+ public final Timeline timeline;
+ /**
+ * The window index of the media period in the timeline. If the timeline is empty, this is the
+ * prospective window index.
+ */
+ public final int windowIndex;
+
+ public MediaPeriodInfo(MediaPeriodId mediaPeriodId, Timeline timeline, int windowIndex) {
+ this.mediaPeriodId = mediaPeriodId;
+ this.timeline = timeline;
+ this.windowIndex = windowIndex;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsListener.java
new file mode 100644
index 0000000000..a265268c19
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/AnalyticsListener.java
@@ -0,0 +1,514 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.analytics;
+
+import android.view.Surface;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.DiscontinuityReason;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.PlaybackSuppressionReason;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.TimelineChangeReason;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioAttributes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioSink;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderCounters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import java.io.IOException;
+
+/**
+ * A listener for analytics events.
+ *
+ * <p>All events are recorded with an {@link EventTime} specifying the elapsed real time and media
+ * time at the time of the event.
+ *
+ * <p>All methods have no-op default implementations to allow selective overrides.
+ */
+public interface AnalyticsListener {
+
+ /** Time information of an event. */
+ final class EventTime {
+
+ /**
+ * Elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} at the time of the
+ * event, in milliseconds.
+ */
+ public final long realtimeMs;
+
+ /** Timeline at the time of the event. */
+ public final Timeline timeline;
+
+ /**
+ * Window index in the {@link #timeline} this event belongs to, or the prospective window index
+ * if the timeline is not yet known and empty.
+ */
+ public final int windowIndex;
+
+ /**
+ * Media period identifier for the media period this event belongs to, or {@code null} if the
+ * event is not associated with a specific media period.
+ */
+ @Nullable public final MediaPeriodId mediaPeriodId;
+
+ /**
+ * Position in the window or ad this event belongs to at the time of the event, in milliseconds.
+ */
+ public final long eventPlaybackPositionMs;
+
+ /**
+ * Position in the current timeline window ({@link Player#getCurrentWindowIndex()}) or the
+ * currently playing ad at the time of the event, in milliseconds.
+ */
+ public final long currentPlaybackPositionMs;
+
+ /**
+ * Total buffered duration from {@link #currentPlaybackPositionMs} at the time of the event, in
+ * milliseconds. This includes pre-buffered data for subsequent ads and windows.
+ */
+ public final long totalBufferedDurationMs;
+
+ /**
+ * @param realtimeMs Elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} at
+ * the time of the event, in milliseconds.
+ * @param timeline Timeline at the time of the event.
+ * @param windowIndex Window index in the {@link #timeline} this event belongs to, or the
+ * prospective window index if the timeline is not yet known and empty.
+ * @param mediaPeriodId Media period identifier for the media period this event belongs to, or
+ * {@code null} if the event is not associated with a specific media period.
+ * @param eventPlaybackPositionMs Position in the window or ad this event belongs to at the time
+ * of the event, in milliseconds.
+ * @param currentPlaybackPositionMs Position in the current timeline window ({@link
+ * Player#getCurrentWindowIndex()}) or the currently playing ad at the time of the event, in
+ * milliseconds.
+ * @param totalBufferedDurationMs Total buffered duration from {@link
+ * #currentPlaybackPositionMs} at the time of the event, in milliseconds. This includes
+ * pre-buffered data for subsequent ads and windows.
+ */
+ public EventTime(
+ long realtimeMs,
+ Timeline timeline,
+ int windowIndex,
+ @Nullable MediaPeriodId mediaPeriodId,
+ long eventPlaybackPositionMs,
+ long currentPlaybackPositionMs,
+ long totalBufferedDurationMs) {
+ this.realtimeMs = realtimeMs;
+ this.timeline = timeline;
+ this.windowIndex = windowIndex;
+ this.mediaPeriodId = mediaPeriodId;
+ this.eventPlaybackPositionMs = eventPlaybackPositionMs;
+ this.currentPlaybackPositionMs = currentPlaybackPositionMs;
+ this.totalBufferedDurationMs = totalBufferedDurationMs;
+ }
+ }
+
+ /**
+ * Called when the player state changed.
+ *
+ * @param eventTime The event time.
+ * @param playWhenReady Whether the playback will proceed when ready.
+ * @param playbackState The new {@link Player.State playback state}.
+ */
+ default void onPlayerStateChanged(
+ EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) {}
+
+ /**
+ * Called when playback suppression reason changed.
+ *
+ * @param eventTime The event time.
+ * @param playbackSuppressionReason The new {@link PlaybackSuppressionReason}.
+ */
+ default void onPlaybackSuppressionReasonChanged(
+ EventTime eventTime, @PlaybackSuppressionReason int playbackSuppressionReason) {}
+
+ /**
+ * Called when the player starts or stops playing.
+ *
+ * @param eventTime The event time.
+ * @param isPlaying Whether the player is playing.
+ */
+ default void onIsPlayingChanged(EventTime eventTime, boolean isPlaying) {}
+
+ /**
+ * Called when the timeline changed.
+ *
+ * @param eventTime The event time.
+ * @param reason The reason for the timeline change.
+ */
+ default void onTimelineChanged(EventTime eventTime, @TimelineChangeReason int reason) {}
+
+ /**
+ * Called when a position discontinuity occurred.
+ *
+ * @param eventTime The event time.
+ * @param reason The reason for the position discontinuity.
+ */
+ default void onPositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason) {}
+
+ /**
+ * Called when a seek operation started.
+ *
+ * @param eventTime The event time.
+ */
+ default void onSeekStarted(EventTime eventTime) {}
+
+ /**
+ * Called when a seek operation was processed.
+ *
+ * @param eventTime The event time.
+ */
+ default void onSeekProcessed(EventTime eventTime) {}
+
+ /**
+ * Called when the playback parameters changed.
+ *
+ * @param eventTime The event time.
+ * @param playbackParameters The new playback parameters.
+ */
+ default void onPlaybackParametersChanged(
+ EventTime eventTime, PlaybackParameters playbackParameters) {}
+
+ /**
+ * Called when the repeat mode changed.
+ *
+ * @param eventTime The event time.
+ * @param repeatMode The new repeat mode.
+ */
+ default void onRepeatModeChanged(EventTime eventTime, @Player.RepeatMode int repeatMode) {}
+
+ /**
+ * Called when the shuffle mode changed.
+ *
+ * @param eventTime The event time.
+ * @param shuffleModeEnabled Whether the shuffle mode is enabled.
+ */
+ default void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {}
+
+ /**
+ * Called when the player starts or stops loading data from a source.
+ *
+ * @param eventTime The event time.
+ * @param isLoading Whether the player is loading.
+ */
+ default void onLoadingChanged(EventTime eventTime, boolean isLoading) {}
+
+ /**
+ * Called when a fatal player error occurred.
+ *
+ * @param eventTime The event time.
+ * @param error The error.
+ */
+ default void onPlayerError(EventTime eventTime, ExoPlaybackException error) {}
+
+ /**
+ * Called when the available or selected tracks for the renderers changed.
+ *
+ * @param eventTime The event time.
+ * @param trackGroups The available tracks. May be empty.
+ * @param trackSelections The track selections for each renderer. May contain null elements.
+ */
+ default void onTracksChanged(
+ EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {}
+
+ /**
+ * Called when a media source started loading data.
+ *
+ * @param eventTime The event time.
+ * @param loadEventInfo The {@link LoadEventInfo} defining the load event.
+ * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
+ */
+ default void onLoadStarted(
+ EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
+
+ /**
+ * Called when a media source completed loading data.
+ *
+ * @param eventTime The event time.
+ * @param loadEventInfo The {@link LoadEventInfo} defining the load event.
+ * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
+ */
+ default void onLoadCompleted(
+ EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
+
+ /**
+ * Called when a media source canceled loading data.
+ *
+ * @param eventTime The event time.
+ * @param loadEventInfo The {@link LoadEventInfo} defining the load event.
+ * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
+ */
+ default void onLoadCanceled(
+ EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
+
+ /**
+ * Called when a media source loading error occurred. These errors are just for informational
+ * purposes and the player may recover.
+ *
+ * @param eventTime The event time.
+ * @param loadEventInfo The {@link LoadEventInfo} defining the load event.
+ * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
+ * @param error The load error.
+ * @param wasCanceled Whether the load was canceled as a result of the error.
+ */
+ default void onLoadError(
+ EventTime eventTime,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData,
+ IOException error,
+ boolean wasCanceled) {}
+
+ /**
+ * Called when the downstream format sent to the renderers changed.
+ *
+ * @param eventTime The event time.
+ * @param mediaLoadData The {@link MediaLoadData} defining the newly selected media data.
+ */
+ default void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {}
+
+ /**
+ * Called when data is removed from the back of a media buffer, typically so that it can be
+ * re-buffered in a different format.
+ *
+ * @param eventTime The event time.
+ * @param mediaLoadData The {@link MediaLoadData} defining the media being discarded.
+ */
+ default void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) {}
+
+ /**
+ * Called when a media source created a media period.
+ *
+ * @param eventTime The event time.
+ */
+ default void onMediaPeriodCreated(EventTime eventTime) {}
+
+ /**
+ * Called when a media source released a media period.
+ *
+ * @param eventTime The event time.
+ */
+ default void onMediaPeriodReleased(EventTime eventTime) {}
+
+ /**
+ * Called when the player started reading a media period.
+ *
+ * @param eventTime The event time.
+ */
+ default void onReadingStarted(EventTime eventTime) {}
+
+ /**
+ * Called when the bandwidth estimate for the current data source has been updated.
+ *
+ * @param eventTime The event time.
+ * @param totalLoadTimeMs The total time spend loading this update is based on, in milliseconds.
+ * @param totalBytesLoaded The total bytes loaded this update is based on.
+ * @param bitrateEstimate The bandwidth estimate, in bits per second.
+ */
+ default void onBandwidthEstimate(
+ EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {}
+
+ /**
+ * Called when the output surface size changed.
+ *
+ * @param eventTime The event time.
+ * @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.
+ */
+ default void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {}
+
+ /**
+ * Called when there is {@link Metadata} associated with the current playback time.
+ *
+ * @param eventTime The event time.
+ * @param metadata The metadata.
+ */
+ default void onMetadata(EventTime eventTime, Metadata metadata) {}
+
+ /**
+ * Called when an audio or video decoder has been enabled.
+ *
+ * @param eventTime The event time.
+ * @param trackType The track type of the enabled decoder. Either {@link C#TRACK_TYPE_AUDIO} or
+ * {@link C#TRACK_TYPE_VIDEO}.
+ * @param decoderCounters The accumulated event counters associated with this decoder.
+ */
+ default void onDecoderEnabled(
+ EventTime eventTime, int trackType, DecoderCounters decoderCounters) {}
+
+ /**
+ * Called when an audio or video decoder has been initialized.
+ *
+ * @param eventTime The event time.
+ * @param trackType The track type of the initialized decoder. Either {@link C#TRACK_TYPE_AUDIO}
+ * or {@link C#TRACK_TYPE_VIDEO}.
+ * @param decoderName The decoder that was created.
+ * @param initializationDurationMs Time taken to initialize the decoder, in milliseconds.
+ */
+ default void onDecoderInitialized(
+ EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) {}
+
+ /**
+ * Called when an audio or video decoder input format changed.
+ *
+ * @param eventTime The event time.
+ * @param trackType The track type of the decoder whose format changed. Either {@link
+ * C#TRACK_TYPE_AUDIO} or {@link C#TRACK_TYPE_VIDEO}.
+ * @param format The new input format for the decoder.
+ */
+ default void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {}
+
+ /**
+ * Called when an audio or video decoder has been disabled.
+ *
+ * @param eventTime The event time.
+ * @param trackType The track type of the disabled decoder. Either {@link C#TRACK_TYPE_AUDIO} or
+ * {@link C#TRACK_TYPE_VIDEO}.
+ * @param decoderCounters The accumulated event counters associated with this decoder.
+ */
+ default void onDecoderDisabled(
+ EventTime eventTime, int trackType, DecoderCounters decoderCounters) {}
+
+ /**
+ * Called when the audio session id is set.
+ *
+ * @param eventTime The event time.
+ * @param audioSessionId The audio session id.
+ */
+ default void onAudioSessionId(EventTime eventTime, int audioSessionId) {}
+
+ /**
+ * Called when the audio attributes change.
+ *
+ * @param eventTime The event time.
+ * @param audioAttributes The audio attributes.
+ */
+ default void onAudioAttributesChanged(EventTime eventTime, AudioAttributes audioAttributes) {}
+
+ /**
+ * Called when the volume changes.
+ *
+ * @param eventTime The event time.
+ * @param volume The new volume, with 0 being silence and 1 being unity gain.
+ */
+ default void onVolumeChanged(EventTime eventTime, float volume) {}
+
+ /**
+ * Called when an audio underrun occurred.
+ *
+ * @param eventTime The event time.
+ * @param bufferSize The size of the {@link AudioSink}'s buffer, in bytes.
+ * @param bufferSizeMs The size of the {@link AudioSink}'s buffer, in milliseconds, if it is
+ * configured for PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output,
+ * as the buffered media can have a variable bitrate so the duration may be unknown.
+ * @param elapsedSinceLastFeedMs The time since the {@link AudioSink} was last fed data.
+ */
+ default void onAudioUnderrun(
+ EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {}
+
+ /**
+ * Called after video frames have been dropped.
+ *
+ * @param eventTime The event time.
+ * @param droppedFrames The number of dropped frames since the last call to this method.
+ * @param elapsedMs The duration in milliseconds over which the frames were dropped. This duration
+ * 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.
+ */
+ default void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {}
+
+ /**
+ * Called before a frame is rendered for the first time since setting the surface, and each time
+ * there's a change in the size or pixel aspect ratio of the video being rendered.
+ *
+ * @param eventTime The event time.
+ * @param width The width of the video.
+ * @param height The height of the video.
+ * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise
+ * rotation in degrees that the application should apply for the video for it to be rendered
+ * in the correct orientation. This value will always be zero on API levels 21 and above,
+ * since the renderer will apply all necessary rotations internally.
+ * @param pixelWidthHeightRatio The width to height ratio of each pixel.
+ */
+ default void onVideoSizeChanged(
+ EventTime eventTime,
+ int width,
+ int height,
+ int unappliedRotationDegrees,
+ float pixelWidthHeightRatio) {}
+
+ /**
+ * Called when a frame is rendered for the first time since setting the surface, and when a frame
+ * is rendered for the first time since the renderer was reset.
+ *
+ * @param eventTime The event time.
+ * @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if
+ * the renderer renders to something that isn't a {@link Surface}.
+ */
+ default void onRenderedFirstFrame(EventTime eventTime, @Nullable Surface surface) {}
+
+ /**
+ * Called each time a drm session is acquired.
+ *
+ * @param eventTime The event time.
+ */
+ default void onDrmSessionAcquired(EventTime eventTime) {}
+
+ /**
+ * Called each time drm keys are loaded.
+ *
+ * @param eventTime The event time.
+ */
+ default void onDrmKeysLoaded(EventTime eventTime) {}
+
+ /**
+ * Called when a drm error occurs. These errors are just for informational purposes and the player
+ * may recover.
+ *
+ * @param eventTime The event time.
+ * @param error The error.
+ */
+ default void onDrmSessionManagerError(EventTime eventTime, Exception error) {}
+
+ /**
+ * Called each time offline drm keys are restored.
+ *
+ * @param eventTime The event time.
+ */
+ default void onDrmKeysRestored(EventTime eventTime) {}
+
+ /**
+ * Called each time offline drm keys are removed.
+ *
+ * @param eventTime The event time.
+ */
+ default void onDrmKeysRemoved(EventTime eventTime) {}
+
+ /**
+ * Called each time a drm session is released.
+ *
+ * @param eventTime The event time.
+ */
+ default void onDrmSessionReleased(EventTime eventTime) {}
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java
new file mode 100644
index 0000000000..f56ac3fef0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.analytics;
+
+/**
+ * @deprecated Use {@link AnalyticsListener} directly for selective overrides as all methods are
+ * implemented as no-op default methods.
+ */
+@Deprecated
+public abstract class DefaultAnalyticsListener implements AnalyticsListener {}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java
new file mode 100644
index 0000000000..710934bd36
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.analytics;
+
+import android.util.Base64;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.DiscontinuityReason;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Random;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
+
+/**
+ * Default {@link PlaybackSessionManager} which instantiates a new session for each window in the
+ * timeline and also for each ad within the windows.
+ *
+ * <p>Sessions are identified by Base64-encoded, URL-safe, random strings.
+ */
+public final class DefaultPlaybackSessionManager implements PlaybackSessionManager {
+
+ private static final Random RANDOM = new Random();
+ private static final int SESSION_ID_LENGTH = 12;
+
+ private final Timeline.Window window;
+ private final Timeline.Period period;
+ private final HashMap<String, SessionDescriptor> sessions;
+
+ private @MonotonicNonNull Listener listener;
+ private Timeline currentTimeline;
+ @Nullable private MediaPeriodId currentMediaPeriodId;
+ @Nullable private String activeSessionId;
+
+ /** Creates session manager. */
+ public DefaultPlaybackSessionManager() {
+ window = new Timeline.Window();
+ period = new Timeline.Period();
+ sessions = new HashMap<>();
+ currentTimeline = Timeline.EMPTY;
+ }
+
+ @Override
+ public void setListener(Listener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public synchronized String getSessionForMediaPeriodId(
+ Timeline timeline, MediaPeriodId mediaPeriodId) {
+ int windowIndex = timeline.getPeriodByUid(mediaPeriodId.periodUid, period).windowIndex;
+ return getOrAddSession(windowIndex, mediaPeriodId).sessionId;
+ }
+
+ @Override
+ public synchronized boolean belongsToSession(EventTime eventTime, String sessionId) {
+ SessionDescriptor sessionDescriptor = sessions.get(sessionId);
+ if (sessionDescriptor == null) {
+ return false;
+ }
+ sessionDescriptor.maybeSetWindowSequenceNumber(eventTime.windowIndex, eventTime.mediaPeriodId);
+ return sessionDescriptor.belongsToSession(eventTime.windowIndex, eventTime.mediaPeriodId);
+ }
+
+ @Override
+ public synchronized void updateSessions(EventTime eventTime) {
+ boolean isObviouslyFinished =
+ eventTime.mediaPeriodId != null
+ && currentMediaPeriodId != null
+ && eventTime.mediaPeriodId.windowSequenceNumber
+ < currentMediaPeriodId.windowSequenceNumber;
+ if (!isObviouslyFinished) {
+ SessionDescriptor descriptor =
+ getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId);
+ if (!descriptor.isCreated) {
+ descriptor.isCreated = true;
+ Assertions.checkNotNull(listener).onSessionCreated(eventTime, descriptor.sessionId);
+ if (activeSessionId == null) {
+ updateActiveSession(eventTime, descriptor);
+ }
+ }
+ }
+ }
+
+ @Override
+ public synchronized void handleTimelineUpdate(EventTime eventTime) {
+ Assertions.checkNotNull(listener);
+ Timeline previousTimeline = currentTimeline;
+ currentTimeline = eventTime.timeline;
+ Iterator<SessionDescriptor> iterator = sessions.values().iterator();
+ while (iterator.hasNext()) {
+ SessionDescriptor session = iterator.next();
+ if (!session.tryResolvingToNewTimeline(previousTimeline, currentTimeline)) {
+ iterator.remove();
+ if (session.isCreated) {
+ if (session.sessionId.equals(activeSessionId)) {
+ activeSessionId = null;
+ }
+ listener.onSessionFinished(
+ eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false);
+ }
+ }
+ }
+ handlePositionDiscontinuity(eventTime, Player.DISCONTINUITY_REASON_INTERNAL);
+ }
+
+ @Override
+ public synchronized void handlePositionDiscontinuity(
+ EventTime eventTime, @DiscontinuityReason int reason) {
+ Assertions.checkNotNull(listener);
+ boolean hasAutomaticTransition =
+ reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION
+ || reason == Player.DISCONTINUITY_REASON_AD_INSERTION;
+ Iterator<SessionDescriptor> iterator = sessions.values().iterator();
+ while (iterator.hasNext()) {
+ SessionDescriptor session = iterator.next();
+ if (session.isFinishedAtEventTime(eventTime)) {
+ iterator.remove();
+ if (session.isCreated) {
+ boolean isRemovingActiveSession = session.sessionId.equals(activeSessionId);
+ boolean isAutomaticTransition = hasAutomaticTransition && isRemovingActiveSession;
+ if (isRemovingActiveSession) {
+ activeSessionId = null;
+ }
+ listener.onSessionFinished(eventTime, session.sessionId, isAutomaticTransition);
+ }
+ }
+ }
+ SessionDescriptor activeSessionDescriptor =
+ getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId);
+ if (eventTime.mediaPeriodId != null
+ && eventTime.mediaPeriodId.isAd()
+ && (currentMediaPeriodId == null
+ || currentMediaPeriodId.windowSequenceNumber
+ != eventTime.mediaPeriodId.windowSequenceNumber
+ || currentMediaPeriodId.adGroupIndex != eventTime.mediaPeriodId.adGroupIndex
+ || currentMediaPeriodId.adIndexInAdGroup != eventTime.mediaPeriodId.adIndexInAdGroup)) {
+ // New ad playback started. Find corresponding content session and notify ad playback started.
+ MediaPeriodId contentMediaPeriodId =
+ new MediaPeriodId(
+ eventTime.mediaPeriodId.periodUid, eventTime.mediaPeriodId.windowSequenceNumber);
+ SessionDescriptor contentSession =
+ getOrAddSession(eventTime.windowIndex, contentMediaPeriodId);
+ if (contentSession.isCreated && activeSessionDescriptor.isCreated) {
+ listener.onAdPlaybackStarted(
+ eventTime, contentSession.sessionId, activeSessionDescriptor.sessionId);
+ }
+ }
+ updateActiveSession(eventTime, activeSessionDescriptor);
+ }
+
+ private SessionDescriptor getOrAddSession(
+ int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
+ // There should only be one matching session if mediaPeriodId is non-null. If mediaPeriodId is
+ // null, there may be multiple matching sessions with different window sequence numbers or
+ // adMediaPeriodIds. The best match is the one with the smaller window sequence number, and for
+ // windows with ads, the content session is preferred over ad sessions.
+ SessionDescriptor bestMatch = null;
+ long bestMatchWindowSequenceNumber = Long.MAX_VALUE;
+ for (SessionDescriptor sessionDescriptor : sessions.values()) {
+ sessionDescriptor.maybeSetWindowSequenceNumber(windowIndex, mediaPeriodId);
+ if (sessionDescriptor.belongsToSession(windowIndex, mediaPeriodId)) {
+ long windowSequenceNumber = sessionDescriptor.windowSequenceNumber;
+ if (windowSequenceNumber == C.INDEX_UNSET
+ || windowSequenceNumber < bestMatchWindowSequenceNumber) {
+ bestMatch = sessionDescriptor;
+ bestMatchWindowSequenceNumber = windowSequenceNumber;
+ } else if (windowSequenceNumber == bestMatchWindowSequenceNumber
+ && Util.castNonNull(bestMatch).adMediaPeriodId != null
+ && sessionDescriptor.adMediaPeriodId != null) {
+ bestMatch = sessionDescriptor;
+ }
+ }
+ }
+ if (bestMatch == null) {
+ String sessionId = generateSessionId();
+ bestMatch = new SessionDescriptor(sessionId, windowIndex, mediaPeriodId);
+ sessions.put(sessionId, bestMatch);
+ }
+ return bestMatch;
+ }
+
+ @RequiresNonNull("listener")
+ private void updateActiveSession(EventTime eventTime, SessionDescriptor sessionDescriptor) {
+ currentMediaPeriodId = eventTime.mediaPeriodId;
+ if (sessionDescriptor.isCreated) {
+ activeSessionId = sessionDescriptor.sessionId;
+ if (!sessionDescriptor.isActive) {
+ sessionDescriptor.isActive = true;
+ listener.onSessionActive(eventTime, sessionDescriptor.sessionId);
+ }
+ }
+ }
+
+ private static String generateSessionId() {
+ byte[] randomBytes = new byte[SESSION_ID_LENGTH];
+ RANDOM.nextBytes(randomBytes);
+ return Base64.encodeToString(randomBytes, Base64.URL_SAFE | Base64.NO_WRAP);
+ }
+
+ /**
+ * Descriptor for a session.
+ *
+ * <p>The session may be described in one of three ways:
+ *
+ * <ul>
+ * <li>A window index with unset window sequence number and a null ad media period id
+ * <li>A content window with index and sequence number, but a null ad media period id.
+ * <li>An ad with all values set.
+ * </ul>
+ */
+ private final class SessionDescriptor {
+
+ private final String sessionId;
+
+ private int windowIndex;
+ private long windowSequenceNumber;
+ private @MonotonicNonNull MediaPeriodId adMediaPeriodId;
+
+ private boolean isCreated;
+ private boolean isActive;
+
+ public SessionDescriptor(
+ String sessionId, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
+ this.sessionId = sessionId;
+ this.windowIndex = windowIndex;
+ this.windowSequenceNumber =
+ mediaPeriodId == null ? C.INDEX_UNSET : mediaPeriodId.windowSequenceNumber;
+ if (mediaPeriodId != null && mediaPeriodId.isAd()) {
+ this.adMediaPeriodId = mediaPeriodId;
+ }
+ }
+
+ public boolean tryResolvingToNewTimeline(Timeline oldTimeline, Timeline newTimeline) {
+ windowIndex = resolveWindowIndexToNewTimeline(oldTimeline, newTimeline, windowIndex);
+ if (windowIndex == C.INDEX_UNSET) {
+ return false;
+ }
+ if (adMediaPeriodId == null) {
+ return true;
+ }
+ int newPeriodIndex = newTimeline.getIndexOfPeriod(adMediaPeriodId.periodUid);
+ return newPeriodIndex != C.INDEX_UNSET;
+ }
+
+ public boolean belongsToSession(
+ int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) {
+ if (eventMediaPeriodId == null) {
+ // Events without concrete media period id are for all sessions of the same window.
+ return eventWindowIndex == windowIndex;
+ }
+ if (adMediaPeriodId == null) {
+ // If this is a content session, only events for content with the same window sequence
+ // number belong to this session.
+ return !eventMediaPeriodId.isAd()
+ && eventMediaPeriodId.windowSequenceNumber == windowSequenceNumber;
+ }
+ // If this is an ad session, only events for this ad belong to the session.
+ return eventMediaPeriodId.windowSequenceNumber == adMediaPeriodId.windowSequenceNumber
+ && eventMediaPeriodId.adGroupIndex == adMediaPeriodId.adGroupIndex
+ && eventMediaPeriodId.adIndexInAdGroup == adMediaPeriodId.adIndexInAdGroup;
+ }
+
+ public void maybeSetWindowSequenceNumber(
+ int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) {
+ if (windowSequenceNumber == C.INDEX_UNSET
+ && eventWindowIndex == windowIndex
+ && eventMediaPeriodId != null
+ && !eventMediaPeriodId.isAd()) {
+ // Set window sequence number for this session as soon as we have one.
+ windowSequenceNumber = eventMediaPeriodId.windowSequenceNumber;
+ }
+ }
+
+ public boolean isFinishedAtEventTime(EventTime eventTime) {
+ if (windowSequenceNumber == C.INDEX_UNSET) {
+ // Sessions with unspecified window sequence number are kept until we know more.
+ return false;
+ }
+ if (eventTime.mediaPeriodId == null) {
+ // For event times without media period id (e.g. after seek to new window), we only keep
+ // sessions of this window.
+ return windowIndex != eventTime.windowIndex;
+ }
+ if (eventTime.mediaPeriodId.windowSequenceNumber > windowSequenceNumber) {
+ // All past window sequence numbers are finished.
+ return true;
+ }
+ if (adMediaPeriodId == null) {
+ // Current or future content is not finished.
+ return false;
+ }
+ int eventPeriodIndex = eventTime.timeline.getIndexOfPeriod(eventTime.mediaPeriodId.periodUid);
+ int adPeriodIndex = eventTime.timeline.getIndexOfPeriod(adMediaPeriodId.periodUid);
+ if (eventTime.mediaPeriodId.windowSequenceNumber < adMediaPeriodId.windowSequenceNumber
+ || eventPeriodIndex < adPeriodIndex) {
+ // Ads in future windows or periods are not finished.
+ return false;
+ }
+ if (eventPeriodIndex > adPeriodIndex) {
+ // Ads in past periods are finished.
+ return true;
+ }
+ if (eventTime.mediaPeriodId.isAd()) {
+ int eventAdGroup = eventTime.mediaPeriodId.adGroupIndex;
+ int eventAdIndex = eventTime.mediaPeriodId.adIndexInAdGroup;
+ // Finished if event is for an ad after this one in the same period.
+ return eventAdGroup > adMediaPeriodId.adGroupIndex
+ || (eventAdGroup == adMediaPeriodId.adGroupIndex
+ && eventAdIndex > adMediaPeriodId.adIndexInAdGroup);
+ } else {
+ // Finished if the event is for content after this ad.
+ return eventTime.mediaPeriodId.nextAdGroupIndex == C.INDEX_UNSET
+ || eventTime.mediaPeriodId.nextAdGroupIndex > adMediaPeriodId.adGroupIndex;
+ }
+ }
+
+ private int resolveWindowIndexToNewTimeline(
+ Timeline oldTimeline, Timeline newTimeline, int windowIndex) {
+ if (windowIndex >= oldTimeline.getWindowCount()) {
+ return windowIndex < newTimeline.getWindowCount() ? windowIndex : C.INDEX_UNSET;
+ }
+ oldTimeline.getWindow(windowIndex, window);
+ for (int periodIndex = window.firstPeriodIndex;
+ periodIndex <= window.lastPeriodIndex;
+ periodIndex++) {
+ Object periodUid = oldTimeline.getUidOfPeriod(periodIndex);
+ int newPeriodIndex = newTimeline.getIndexOfPeriod(periodUid);
+ if (newPeriodIndex != C.INDEX_UNSET) {
+ return newTimeline.getPeriod(newPeriodIndex, period).windowIndex;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java
new file mode 100644
index 0000000000..d3c6f7dd20
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.analytics;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.DiscontinuityReason;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+
+/**
+ * Manager for active playback sessions.
+ *
+ * <p>The manager keeps track of the association between window index and/or media period id to
+ * session identifier.
+ */
+public interface PlaybackSessionManager {
+
+ /** A listener for session updates. */
+ interface Listener {
+
+ /**
+ * Called when a new session is created as a result of {@link #updateSessions(EventTime)}.
+ *
+ * @param eventTime The {@link EventTime} at which the session is created.
+ * @param sessionId The identifier of the new session.
+ */
+ void onSessionCreated(EventTime eventTime, String sessionId);
+
+ /**
+ * Called when a session becomes active, i.e. playing in the foreground.
+ *
+ * @param eventTime The {@link EventTime} at which the session becomes active.
+ * @param sessionId The identifier of the session.
+ */
+ void onSessionActive(EventTime eventTime, String sessionId);
+
+ /**
+ * Called when a session is interrupted by ad playback.
+ *
+ * @param eventTime The {@link EventTime} at which the ad playback starts.
+ * @param contentSessionId The session identifier of the content session.
+ * @param adSessionId The identifier of the ad session.
+ */
+ void onAdPlaybackStarted(EventTime eventTime, String contentSessionId, String adSessionId);
+
+ /**
+ * Called when a session is permanently finished.
+ *
+ * @param eventTime The {@link EventTime} at which the session finished.
+ * @param sessionId The identifier of the finished session.
+ * @param automaticTransitionToNextPlayback Whether the session finished because of an automatic
+ * transition to the next playback item.
+ */
+ void onSessionFinished(
+ EventTime eventTime, String sessionId, boolean automaticTransitionToNextPlayback);
+ }
+
+ /**
+ * Sets the listener to be notified of session updates. Must be called before the session manager
+ * is used.
+ *
+ * @param listener The {@link Listener} to be notified of session updates.
+ */
+ void setListener(Listener listener);
+
+ /**
+ * Returns the session identifier for the given media period id.
+ *
+ * <p>Note that this will reserve a new session identifier if it doesn't exist yet, but will not
+ * call any {@link Listener} callbacks.
+ *
+ * @param timeline The timeline, {@code mediaPeriodId} is part of.
+ * @param mediaPeriodId A {@link MediaPeriodId}.
+ */
+ String getSessionForMediaPeriodId(Timeline timeline, MediaPeriodId mediaPeriodId);
+
+ /**
+ * Returns whether an event time belong to a session.
+ *
+ * @param eventTime The {@link EventTime}.
+ * @param sessionId A session identifier.
+ * @return Whether the event belongs to the specified session.
+ */
+ boolean belongsToSession(EventTime eventTime, String sessionId);
+
+ /**
+ * Updates or creates sessions based on a player {@link EventTime}.
+ *
+ * @param eventTime The {@link EventTime}.
+ */
+ void updateSessions(EventTime eventTime);
+
+ /**
+ * Updates the session associations to a new timeline.
+ *
+ * @param eventTime The event time with the timeline change.
+ */
+ void handleTimelineUpdate(EventTime eventTime);
+
+ /**
+ * Handles a position discontinuity.
+ *
+ * @param eventTime The event time of the position discontinuity.
+ * @param reason The {@link DiscontinuityReason}.
+ */
+ void handlePositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStats.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStats.java
new file mode 100644
index 0000000000..eef0f6e7ce
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStats.java
@@ -0,0 +1,980 @@
+/*
+ * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.analytics;
+
+import android.os.SystemClock;
+import android.util.Pair;
+import androidx.annotation.IntDef;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Collections;
+import java.util.List;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/** Statistics about playbacks. */
+public final class PlaybackStats {
+
+ /**
+ * State of a playback. One of {@link #PLAYBACK_STATE_NOT_STARTED}, {@link
+ * #PLAYBACK_STATE_JOINING_FOREGROUND}, {@link #PLAYBACK_STATE_JOINING_BACKGROUND}, {@link
+ * #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED}, {@link #PLAYBACK_STATE_SEEKING},
+ * {@link #PLAYBACK_STATE_BUFFERING}, {@link #PLAYBACK_STATE_PAUSED_BUFFERING}, {@link
+ * #PLAYBACK_STATE_SEEK_BUFFERING}, {@link #PLAYBACK_STATE_SUPPRESSED}, {@link
+ * #PLAYBACK_STATE_SUPPRESSED_BUFFERING}, {@link #PLAYBACK_STATE_ENDED}, {@link
+ * #PLAYBACK_STATE_STOPPED}, {@link #PLAYBACK_STATE_FAILED}, {@link
+ * #PLAYBACK_STATE_INTERRUPTED_BY_AD} or {@link #PLAYBACK_STATE_ABANDONED}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
+ @IntDef({
+ PLAYBACK_STATE_NOT_STARTED,
+ PLAYBACK_STATE_JOINING_BACKGROUND,
+ PLAYBACK_STATE_JOINING_FOREGROUND,
+ PLAYBACK_STATE_PLAYING,
+ PLAYBACK_STATE_PAUSED,
+ PLAYBACK_STATE_SEEKING,
+ PLAYBACK_STATE_BUFFERING,
+ PLAYBACK_STATE_PAUSED_BUFFERING,
+ PLAYBACK_STATE_SEEK_BUFFERING,
+ PLAYBACK_STATE_SUPPRESSED,
+ PLAYBACK_STATE_SUPPRESSED_BUFFERING,
+ PLAYBACK_STATE_ENDED,
+ PLAYBACK_STATE_STOPPED,
+ PLAYBACK_STATE_FAILED,
+ PLAYBACK_STATE_INTERRUPTED_BY_AD,
+ PLAYBACK_STATE_ABANDONED
+ })
+ @interface PlaybackState {}
+ /** Playback has not started (initial state). */
+ public static final int PLAYBACK_STATE_NOT_STARTED = 0;
+ /** Playback is buffering in the background for initial playback start. */
+ public static final int PLAYBACK_STATE_JOINING_BACKGROUND = 1;
+ /** Playback is buffering in the foreground for initial playback start. */
+ public static final int PLAYBACK_STATE_JOINING_FOREGROUND = 2;
+ /** Playback is actively playing. */
+ public static final int PLAYBACK_STATE_PLAYING = 3;
+ /** Playback is paused but ready to play. */
+ public static final int PLAYBACK_STATE_PAUSED = 4;
+ /** Playback is handling a seek. */
+ public static final int PLAYBACK_STATE_SEEKING = 5;
+ /** Playback is buffering to resume active playback. */
+ public static final int PLAYBACK_STATE_BUFFERING = 6;
+ /** Playback is buffering while paused. */
+ public static final int PLAYBACK_STATE_PAUSED_BUFFERING = 7;
+ /** Playback is buffering after a seek. */
+ public static final int PLAYBACK_STATE_SEEK_BUFFERING = 8;
+ /** Playback is suppressed (e.g. due to audio focus loss). */
+ public static final int PLAYBACK_STATE_SUPPRESSED = 9;
+ /** Playback is suppressed (e.g. due to audio focus loss) while buffering to resume a playback. */
+ public static final int PLAYBACK_STATE_SUPPRESSED_BUFFERING = 10;
+ /** Playback has reached the end of the media. */
+ public static final int PLAYBACK_STATE_ENDED = 11;
+ /** Playback is stopped and can be restarted. */
+ public static final int PLAYBACK_STATE_STOPPED = 12;
+ /** Playback is stopped due a fatal error and can be retried. */
+ public static final int PLAYBACK_STATE_FAILED = 13;
+ /** Playback is interrupted by an ad. */
+ public static final int PLAYBACK_STATE_INTERRUPTED_BY_AD = 14;
+ /** Playback is abandoned before reaching the end of the media. */
+ public static final int PLAYBACK_STATE_ABANDONED = 15;
+ /** Total number of playback states. */
+ /* package */ static final int PLAYBACK_STATE_COUNT = 16;
+
+ /** Empty playback stats. */
+ public static final PlaybackStats EMPTY = merge(/* nothing */ );
+
+ /**
+ * Returns the combined {@link PlaybackStats} for all input {@link PlaybackStats}.
+ *
+ * <p>Note that the full history of events is not kept as the history only makes sense in the
+ * context of a single playback.
+ *
+ * @param playbackStats Array of {@link PlaybackStats} to combine.
+ * @return The combined {@link PlaybackStats}.
+ */
+ public static PlaybackStats merge(PlaybackStats... playbackStats) {
+ int playbackCount = 0;
+ long[] playbackStateDurationsMs = new long[PLAYBACK_STATE_COUNT];
+ long firstReportedTimeMs = C.TIME_UNSET;
+ int foregroundPlaybackCount = 0;
+ int abandonedBeforeReadyCount = 0;
+ int endedCount = 0;
+ int backgroundJoiningCount = 0;
+ long totalValidJoinTimeMs = C.TIME_UNSET;
+ int validJoinTimeCount = 0;
+ int totalPauseCount = 0;
+ int totalPauseBufferCount = 0;
+ int totalSeekCount = 0;
+ int totalRebufferCount = 0;
+ long maxRebufferTimeMs = C.TIME_UNSET;
+ int adPlaybackCount = 0;
+ long totalVideoFormatHeightTimeMs = 0;
+ long totalVideoFormatHeightTimeProduct = 0;
+ long totalVideoFormatBitrateTimeMs = 0;
+ long totalVideoFormatBitrateTimeProduct = 0;
+ long totalAudioFormatTimeMs = 0;
+ long totalAudioFormatBitrateTimeProduct = 0;
+ int initialVideoFormatHeightCount = 0;
+ int initialVideoFormatBitrateCount = 0;
+ int totalInitialVideoFormatHeight = C.LENGTH_UNSET;
+ long totalInitialVideoFormatBitrate = C.LENGTH_UNSET;
+ int initialAudioFormatBitrateCount = 0;
+ long totalInitialAudioFormatBitrate = C.LENGTH_UNSET;
+ long totalBandwidthTimeMs = 0;
+ long totalBandwidthBytes = 0;
+ long totalDroppedFrames = 0;
+ long totalAudioUnderruns = 0;
+ int fatalErrorPlaybackCount = 0;
+ int fatalErrorCount = 0;
+ int nonFatalErrorCount = 0;
+ for (PlaybackStats stats : playbackStats) {
+ playbackCount += stats.playbackCount;
+ for (int i = 0; i < PLAYBACK_STATE_COUNT; i++) {
+ playbackStateDurationsMs[i] += stats.playbackStateDurationsMs[i];
+ }
+ if (firstReportedTimeMs == C.TIME_UNSET) {
+ firstReportedTimeMs = stats.firstReportedTimeMs;
+ } else if (stats.firstReportedTimeMs != C.TIME_UNSET) {
+ firstReportedTimeMs = Math.min(firstReportedTimeMs, stats.firstReportedTimeMs);
+ }
+ foregroundPlaybackCount += stats.foregroundPlaybackCount;
+ abandonedBeforeReadyCount += stats.abandonedBeforeReadyCount;
+ endedCount += stats.endedCount;
+ backgroundJoiningCount += stats.backgroundJoiningCount;
+ if (totalValidJoinTimeMs == C.TIME_UNSET) {
+ totalValidJoinTimeMs = stats.totalValidJoinTimeMs;
+ } else if (stats.totalValidJoinTimeMs != C.TIME_UNSET) {
+ totalValidJoinTimeMs += stats.totalValidJoinTimeMs;
+ }
+ validJoinTimeCount += stats.validJoinTimeCount;
+ totalPauseCount += stats.totalPauseCount;
+ totalPauseBufferCount += stats.totalPauseBufferCount;
+ totalSeekCount += stats.totalSeekCount;
+ totalRebufferCount += stats.totalRebufferCount;
+ if (maxRebufferTimeMs == C.TIME_UNSET) {
+ maxRebufferTimeMs = stats.maxRebufferTimeMs;
+ } else if (stats.maxRebufferTimeMs != C.TIME_UNSET) {
+ maxRebufferTimeMs = Math.max(maxRebufferTimeMs, stats.maxRebufferTimeMs);
+ }
+ adPlaybackCount += stats.adPlaybackCount;
+ totalVideoFormatHeightTimeMs += stats.totalVideoFormatHeightTimeMs;
+ totalVideoFormatHeightTimeProduct += stats.totalVideoFormatHeightTimeProduct;
+ totalVideoFormatBitrateTimeMs += stats.totalVideoFormatBitrateTimeMs;
+ totalVideoFormatBitrateTimeProduct += stats.totalVideoFormatBitrateTimeProduct;
+ totalAudioFormatTimeMs += stats.totalAudioFormatTimeMs;
+ totalAudioFormatBitrateTimeProduct += stats.totalAudioFormatBitrateTimeProduct;
+ initialVideoFormatHeightCount += stats.initialVideoFormatHeightCount;
+ initialVideoFormatBitrateCount += stats.initialVideoFormatBitrateCount;
+ if (totalInitialVideoFormatHeight == C.LENGTH_UNSET) {
+ totalInitialVideoFormatHeight = stats.totalInitialVideoFormatHeight;
+ } else if (stats.totalInitialVideoFormatHeight != C.LENGTH_UNSET) {
+ totalInitialVideoFormatHeight += stats.totalInitialVideoFormatHeight;
+ }
+ if (totalInitialVideoFormatBitrate == C.LENGTH_UNSET) {
+ totalInitialVideoFormatBitrate = stats.totalInitialVideoFormatBitrate;
+ } else if (stats.totalInitialVideoFormatBitrate != C.LENGTH_UNSET) {
+ totalInitialVideoFormatBitrate += stats.totalInitialVideoFormatBitrate;
+ }
+ initialAudioFormatBitrateCount += stats.initialAudioFormatBitrateCount;
+ if (totalInitialAudioFormatBitrate == C.LENGTH_UNSET) {
+ totalInitialAudioFormatBitrate = stats.totalInitialAudioFormatBitrate;
+ } else if (stats.totalInitialAudioFormatBitrate != C.LENGTH_UNSET) {
+ totalInitialAudioFormatBitrate += stats.totalInitialAudioFormatBitrate;
+ }
+ totalBandwidthTimeMs += stats.totalBandwidthTimeMs;
+ totalBandwidthBytes += stats.totalBandwidthBytes;
+ totalDroppedFrames += stats.totalDroppedFrames;
+ totalAudioUnderruns += stats.totalAudioUnderruns;
+ fatalErrorPlaybackCount += stats.fatalErrorPlaybackCount;
+ fatalErrorCount += stats.fatalErrorCount;
+ nonFatalErrorCount += stats.nonFatalErrorCount;
+ }
+ return new PlaybackStats(
+ playbackCount,
+ playbackStateDurationsMs,
+ /* playbackStateHistory */ Collections.emptyList(),
+ /* mediaTimeHistory= */ Collections.emptyList(),
+ firstReportedTimeMs,
+ foregroundPlaybackCount,
+ abandonedBeforeReadyCount,
+ endedCount,
+ backgroundJoiningCount,
+ totalValidJoinTimeMs,
+ validJoinTimeCount,
+ totalPauseCount,
+ totalPauseBufferCount,
+ totalSeekCount,
+ totalRebufferCount,
+ maxRebufferTimeMs,
+ adPlaybackCount,
+ /* videoFormatHistory= */ Collections.emptyList(),
+ /* audioFormatHistory= */ Collections.emptyList(),
+ totalVideoFormatHeightTimeMs,
+ totalVideoFormatHeightTimeProduct,
+ totalVideoFormatBitrateTimeMs,
+ totalVideoFormatBitrateTimeProduct,
+ totalAudioFormatTimeMs,
+ totalAudioFormatBitrateTimeProduct,
+ initialVideoFormatHeightCount,
+ initialVideoFormatBitrateCount,
+ totalInitialVideoFormatHeight,
+ totalInitialVideoFormatBitrate,
+ initialAudioFormatBitrateCount,
+ totalInitialAudioFormatBitrate,
+ totalBandwidthTimeMs,
+ totalBandwidthBytes,
+ totalDroppedFrames,
+ totalAudioUnderruns,
+ fatalErrorPlaybackCount,
+ fatalErrorCount,
+ nonFatalErrorCount,
+ /* fatalErrorHistory= */ Collections.emptyList(),
+ /* nonFatalErrorHistory= */ Collections.emptyList());
+ }
+
+ /** The number of individual playbacks for which these stats were collected. */
+ public final int playbackCount;
+
+ // Playback state stats.
+
+ /**
+ * The playback state history as ordered pairs of the {@link EventTime} at which a state became
+ * active and the {@link PlaybackState}.
+ */
+ public final List<Pair<EventTime, @PlaybackState Integer>> playbackStateHistory;
+ /**
+ * The media time history as an ordered list of long[2] arrays with [0] being the realtime as
+ * returned by {@code SystemClock.elapsedRealtime()} and [1] being the media time at this
+ * realtime, in milliseconds.
+ */
+ public final List<long[]> mediaTimeHistory;
+ /**
+ * The elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} of the first
+ * reported playback event, or {@link C#TIME_UNSET} if no event has been reported.
+ */
+ public final long firstReportedTimeMs;
+ /** The number of playbacks which were the active foreground playback at some point. */
+ public final int foregroundPlaybackCount;
+ /** The number of playbacks which were abandoned before they were ready to play. */
+ public final int abandonedBeforeReadyCount;
+ /** The number of playbacks which reached the ended state at least once. */
+ public final int endedCount;
+ /** The number of playbacks which were pre-buffered in the background. */
+ public final int backgroundJoiningCount;
+ /**
+ * The total time spent joining the playback, in milliseconds, or {@link C#TIME_UNSET} if no valid
+ * join time could be determined.
+ *
+ * <p>Note that this does not include background joining time. A join time may be invalid if the
+ * playback never reached {@link #PLAYBACK_STATE_PLAYING} or {@link #PLAYBACK_STATE_PAUSED}, or
+ * joining was interrupted by a seek, stop, or error state.
+ */
+ public final long totalValidJoinTimeMs;
+ /**
+ * The number of playbacks with a valid join time as documented in {@link #totalValidJoinTimeMs}.
+ */
+ public final int validJoinTimeCount;
+ /** The total number of times a playback has been paused. */
+ public final int totalPauseCount;
+ /** The total number of times a playback has been paused while rebuffering. */
+ public final int totalPauseBufferCount;
+ /**
+ * The total number of times a seek occurred. This includes seeks happening before playback
+ * resumed after another seek.
+ */
+ public final int totalSeekCount;
+ /**
+ * The total number of times a rebuffer occurred. This excludes initial joining and buffering
+ * after seek.
+ */
+ public final int totalRebufferCount;
+ /**
+ * The maximum time spent during a single rebuffer, in milliseconds, or {@link C#TIME_UNSET} if no
+ * rebuffer occurred.
+ */
+ public final long maxRebufferTimeMs;
+ /** The number of ad playbacks. */
+ public final int adPlaybackCount;
+
+ // Format stats.
+
+ /**
+ * The video format history as ordered pairs of the {@link EventTime} at which a format started
+ * being used and the {@link Format}. The {@link Format} may be null if no video format was used.
+ */
+ public final List<Pair<EventTime, @NullableType Format>> videoFormatHistory;
+ /**
+ * The audio format history as ordered pairs of the {@link EventTime} at which a format started
+ * being used and the {@link Format}. The {@link Format} may be null if no audio format was used.
+ */
+ public final List<Pair<EventTime, @NullableType Format>> audioFormatHistory;
+ /** The total media time for which video format height data is available, in milliseconds. */
+ public final long totalVideoFormatHeightTimeMs;
+ /**
+ * The accumulated sum of all video format heights, in pixels, times the time the format was used
+ * for playback, in milliseconds.
+ */
+ public final long totalVideoFormatHeightTimeProduct;
+ /** The total media time for which video format bitrate data is available, in milliseconds. */
+ public final long totalVideoFormatBitrateTimeMs;
+ /**
+ * The accumulated sum of all video format bitrates, in bits per second, times the time the format
+ * was used for playback, in milliseconds.
+ */
+ public final long totalVideoFormatBitrateTimeProduct;
+ /** The total media time for which audio format data is available, in milliseconds. */
+ public final long totalAudioFormatTimeMs;
+ /**
+ * The accumulated sum of all audio format bitrates, in bits per second, times the time the format
+ * was used for playback, in milliseconds.
+ */
+ public final long totalAudioFormatBitrateTimeProduct;
+ /** The number of playbacks with initial video format height data. */
+ public final int initialVideoFormatHeightCount;
+ /** The number of playbacks with initial video format bitrate data. */
+ public final int initialVideoFormatBitrateCount;
+ /**
+ * The total initial video format height for all playbacks, in pixels, or {@link C#LENGTH_UNSET}
+ * if no initial video format data is available.
+ */
+ public final int totalInitialVideoFormatHeight;
+ /**
+ * The total initial video format bitrate for all playbacks, in bits per second, or {@link
+ * C#LENGTH_UNSET} if no initial video format data is available.
+ */
+ public final long totalInitialVideoFormatBitrate;
+ /** The number of playbacks with initial audio format bitrate data. */
+ public final int initialAudioFormatBitrateCount;
+ /**
+ * The total initial audio format bitrate for all playbacks, in bits per second, or {@link
+ * C#LENGTH_UNSET} if no initial audio format data is available.
+ */
+ public final long totalInitialAudioFormatBitrate;
+
+ // Bandwidth stats.
+
+ /** The total time for which bandwidth measurement data is available, in milliseconds. */
+ public final long totalBandwidthTimeMs;
+ /** The total bytes transferred during {@link #totalBandwidthTimeMs}. */
+ public final long totalBandwidthBytes;
+
+ // Renderer quality stats.
+
+ /** The total number of dropped video frames. */
+ public final long totalDroppedFrames;
+ /** The total number of audio underruns. */
+ public final long totalAudioUnderruns;
+
+ // Error stats.
+
+ /**
+ * The total number of playback with at least one fatal error. Errors are fatal if playback
+ * stopped due to this error.
+ */
+ public final int fatalErrorPlaybackCount;
+ /** The total number of fatal errors. Errors are fatal if playback stopped due to this error. */
+ public final int fatalErrorCount;
+ /**
+ * The total number of non-fatal errors. Error are non-fatal if playback can recover from the
+ * error without stopping.
+ */
+ public final int nonFatalErrorCount;
+ /**
+ * The history of fatal errors as ordered pairs of the {@link EventTime} at which an error
+ * occurred and the error. Errors are fatal if playback stopped due to this error.
+ */
+ public final List<Pair<EventTime, Exception>> fatalErrorHistory;
+ /**
+ * The history of non-fatal errors as ordered pairs of the {@link EventTime} at which an error
+ * occurred and the error. Error are non-fatal if playback can recover from the error without
+ * stopping.
+ */
+ public final List<Pair<EventTime, Exception>> nonFatalErrorHistory;
+
+ private final long[] playbackStateDurationsMs;
+
+ /* package */ PlaybackStats(
+ int playbackCount,
+ long[] playbackStateDurationsMs,
+ List<Pair<EventTime, @PlaybackState Integer>> playbackStateHistory,
+ List<long[]> mediaTimeHistory,
+ long firstReportedTimeMs,
+ int foregroundPlaybackCount,
+ int abandonedBeforeReadyCount,
+ int endedCount,
+ int backgroundJoiningCount,
+ long totalValidJoinTimeMs,
+ int validJoinTimeCount,
+ int totalPauseCount,
+ int totalPauseBufferCount,
+ int totalSeekCount,
+ int totalRebufferCount,
+ long maxRebufferTimeMs,
+ int adPlaybackCount,
+ List<Pair<EventTime, @NullableType Format>> videoFormatHistory,
+ List<Pair<EventTime, @NullableType Format>> audioFormatHistory,
+ long totalVideoFormatHeightTimeMs,
+ long totalVideoFormatHeightTimeProduct,
+ long totalVideoFormatBitrateTimeMs,
+ long totalVideoFormatBitrateTimeProduct,
+ long totalAudioFormatTimeMs,
+ long totalAudioFormatBitrateTimeProduct,
+ int initialVideoFormatHeightCount,
+ int initialVideoFormatBitrateCount,
+ int totalInitialVideoFormatHeight,
+ long totalInitialVideoFormatBitrate,
+ int initialAudioFormatBitrateCount,
+ long totalInitialAudioFormatBitrate,
+ long totalBandwidthTimeMs,
+ long totalBandwidthBytes,
+ long totalDroppedFrames,
+ long totalAudioUnderruns,
+ int fatalErrorPlaybackCount,
+ int fatalErrorCount,
+ int nonFatalErrorCount,
+ List<Pair<EventTime, Exception>> fatalErrorHistory,
+ List<Pair<EventTime, Exception>> nonFatalErrorHistory) {
+ this.playbackCount = playbackCount;
+ this.playbackStateDurationsMs = playbackStateDurationsMs;
+ this.playbackStateHistory = Collections.unmodifiableList(playbackStateHistory);
+ this.mediaTimeHistory = Collections.unmodifiableList(mediaTimeHistory);
+ this.firstReportedTimeMs = firstReportedTimeMs;
+ this.foregroundPlaybackCount = foregroundPlaybackCount;
+ this.abandonedBeforeReadyCount = abandonedBeforeReadyCount;
+ this.endedCount = endedCount;
+ this.backgroundJoiningCount = backgroundJoiningCount;
+ this.totalValidJoinTimeMs = totalValidJoinTimeMs;
+ this.validJoinTimeCount = validJoinTimeCount;
+ this.totalPauseCount = totalPauseCount;
+ this.totalPauseBufferCount = totalPauseBufferCount;
+ this.totalSeekCount = totalSeekCount;
+ this.totalRebufferCount = totalRebufferCount;
+ this.maxRebufferTimeMs = maxRebufferTimeMs;
+ this.adPlaybackCount = adPlaybackCount;
+ this.videoFormatHistory = Collections.unmodifiableList(videoFormatHistory);
+ this.audioFormatHistory = Collections.unmodifiableList(audioFormatHistory);
+ this.totalVideoFormatHeightTimeMs = totalVideoFormatHeightTimeMs;
+ this.totalVideoFormatHeightTimeProduct = totalVideoFormatHeightTimeProduct;
+ this.totalVideoFormatBitrateTimeMs = totalVideoFormatBitrateTimeMs;
+ this.totalVideoFormatBitrateTimeProduct = totalVideoFormatBitrateTimeProduct;
+ this.totalAudioFormatTimeMs = totalAudioFormatTimeMs;
+ this.totalAudioFormatBitrateTimeProduct = totalAudioFormatBitrateTimeProduct;
+ this.initialVideoFormatHeightCount = initialVideoFormatHeightCount;
+ this.initialVideoFormatBitrateCount = initialVideoFormatBitrateCount;
+ this.totalInitialVideoFormatHeight = totalInitialVideoFormatHeight;
+ this.totalInitialVideoFormatBitrate = totalInitialVideoFormatBitrate;
+ this.initialAudioFormatBitrateCount = initialAudioFormatBitrateCount;
+ this.totalInitialAudioFormatBitrate = totalInitialAudioFormatBitrate;
+ this.totalBandwidthTimeMs = totalBandwidthTimeMs;
+ this.totalBandwidthBytes = totalBandwidthBytes;
+ this.totalDroppedFrames = totalDroppedFrames;
+ this.totalAudioUnderruns = totalAudioUnderruns;
+ this.fatalErrorPlaybackCount = fatalErrorPlaybackCount;
+ this.fatalErrorCount = fatalErrorCount;
+ this.nonFatalErrorCount = nonFatalErrorCount;
+ this.fatalErrorHistory = Collections.unmodifiableList(fatalErrorHistory);
+ this.nonFatalErrorHistory = Collections.unmodifiableList(nonFatalErrorHistory);
+ }
+
+ /**
+ * Returns the total time spent in a given {@link PlaybackState}, in milliseconds.
+ *
+ * @param playbackState A {@link PlaybackState}.
+ * @return Total spent in the given playback state, in milliseconds
+ */
+ public long getPlaybackStateDurationMs(@PlaybackState int playbackState) {
+ return playbackStateDurationsMs[playbackState];
+ }
+
+ /**
+ * Returns the {@link PlaybackState} at the given time.
+ *
+ * @param realtimeMs The time as returned by {@link SystemClock#elapsedRealtime()}.
+ * @return The {@link PlaybackState} at that time, or {@link #PLAYBACK_STATE_NOT_STARTED} if the
+ * given time is before the first known playback state in the history.
+ */
+ public @PlaybackState int getPlaybackStateAtTime(long realtimeMs) {
+ @PlaybackState int state = PLAYBACK_STATE_NOT_STARTED;
+ for (Pair<EventTime, @PlaybackState Integer> timeAndState : playbackStateHistory) {
+ if (timeAndState.first.realtimeMs > realtimeMs) {
+ break;
+ }
+ state = timeAndState.second;
+ }
+ return state;
+ }
+
+ /**
+ * Returns the estimated media time at the given realtime, in milliseconds, or {@link
+ * C#TIME_UNSET} if the media time history is unknown.
+ *
+ * @param realtimeMs The realtime as returned by {@link SystemClock#elapsedRealtime()}.
+ * @return The estimated media time in milliseconds at this realtime, {@link C#TIME_UNSET} if no
+ * estimate can be given.
+ */
+ public long getMediaTimeMsAtRealtimeMs(long realtimeMs) {
+ if (mediaTimeHistory.isEmpty()) {
+ return C.TIME_UNSET;
+ }
+ int nextIndex = 0;
+ while (nextIndex < mediaTimeHistory.size()
+ && mediaTimeHistory.get(nextIndex)[0] <= realtimeMs) {
+ nextIndex++;
+ }
+ if (nextIndex == 0) {
+ return mediaTimeHistory.get(0)[1];
+ }
+ if (nextIndex == mediaTimeHistory.size()) {
+ return mediaTimeHistory.get(mediaTimeHistory.size() - 1)[1];
+ }
+ long prevRealtimeMs = mediaTimeHistory.get(nextIndex - 1)[0];
+ long prevMediaTimeMs = mediaTimeHistory.get(nextIndex - 1)[1];
+ long nextRealtimeMs = mediaTimeHistory.get(nextIndex)[0];
+ long nextMediaTimeMs = mediaTimeHistory.get(nextIndex)[1];
+ long realtimeDurationMs = nextRealtimeMs - prevRealtimeMs;
+ if (realtimeDurationMs == 0) {
+ return prevMediaTimeMs;
+ }
+ float fraction = (float) (realtimeMs - prevRealtimeMs) / realtimeDurationMs;
+ return prevMediaTimeMs + (long) ((nextMediaTimeMs - prevMediaTimeMs) * fraction);
+ }
+
+ /**
+ * Returns the mean time spent joining the playback, in milliseconds, or {@link C#TIME_UNSET} if
+ * no valid join time is available. Only includes playbacks with valid join times as documented in
+ * {@link #totalValidJoinTimeMs}.
+ */
+ public long getMeanJoinTimeMs() {
+ return validJoinTimeCount == 0 ? C.TIME_UNSET : totalValidJoinTimeMs / validJoinTimeCount;
+ }
+
+ /**
+ * Returns the total time spent joining the playback in foreground, in milliseconds. This does
+ * include invalid join times where the playback never reached {@link #PLAYBACK_STATE_PLAYING} or
+ * {@link #PLAYBACK_STATE_PAUSED}, or joining was interrupted by a seek, stop, or error state.
+ */
+ public long getTotalJoinTimeMs() {
+ return getPlaybackStateDurationMs(PLAYBACK_STATE_JOINING_FOREGROUND);
+ }
+
+ /** Returns the total time spent actively playing, in milliseconds. */
+ public long getTotalPlayTimeMs() {
+ return getPlaybackStateDurationMs(PLAYBACK_STATE_PLAYING);
+ }
+
+ /**
+ * Returns the mean time spent actively playing per foreground playback, in milliseconds, or
+ * {@link C#TIME_UNSET} if no playback has been in foreground.
+ */
+ public long getMeanPlayTimeMs() {
+ return foregroundPlaybackCount == 0
+ ? C.TIME_UNSET
+ : getTotalPlayTimeMs() / foregroundPlaybackCount;
+ }
+
+ /** Returns the total time spent in a paused state, in milliseconds. */
+ public long getTotalPausedTimeMs() {
+ return getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED)
+ + getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED_BUFFERING);
+ }
+
+ /**
+ * Returns the mean time spent in a paused state per foreground playback, in milliseconds, or
+ * {@link C#TIME_UNSET} if no playback has been in foreground.
+ */
+ public long getMeanPausedTimeMs() {
+ return foregroundPlaybackCount == 0
+ ? C.TIME_UNSET
+ : getTotalPausedTimeMs() / foregroundPlaybackCount;
+ }
+
+ /**
+ * Returns the total time spent rebuffering, in milliseconds. This excludes initial join times,
+ * buffer times after a seek and buffering while paused.
+ */
+ public long getTotalRebufferTimeMs() {
+ return getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING);
+ }
+
+ /**
+ * Returns the mean time spent rebuffering per foreground playback, in milliseconds, or {@link
+ * C#TIME_UNSET} if no playback has been in foreground. This excludes initial join times, buffer
+ * times after a seek and buffering while paused.
+ */
+ public long getMeanRebufferTimeMs() {
+ return foregroundPlaybackCount == 0
+ ? C.TIME_UNSET
+ : getTotalRebufferTimeMs() / foregroundPlaybackCount;
+ }
+
+ /**
+ * Returns the mean time spent during a single rebuffer, in milliseconds, or {@link C#TIME_UNSET}
+ * if no rebuffer was recorded. This excludes initial join times and buffer times after a seek.
+ */
+ public long getMeanSingleRebufferTimeMs() {
+ return totalRebufferCount == 0
+ ? C.TIME_UNSET
+ : (getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING)
+ + getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED_BUFFERING))
+ / totalRebufferCount;
+ }
+
+ /**
+ * Returns the total time spent from the start of a seek until playback is ready again, in
+ * milliseconds.
+ */
+ public long getTotalSeekTimeMs() {
+ return getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING)
+ + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING);
+ }
+
+ /**
+ * Returns the mean time spent per foreground playback from the start of a seek until playback is
+ * ready again, in milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground.
+ */
+ public long getMeanSeekTimeMs() {
+ return foregroundPlaybackCount == 0
+ ? C.TIME_UNSET
+ : getTotalSeekTimeMs() / foregroundPlaybackCount;
+ }
+
+ /**
+ * Returns the mean time spent from the start of a single seek until playback is ready again, in
+ * milliseconds, or {@link C#TIME_UNSET} if no seek occurred.
+ */
+ public long getMeanSingleSeekTimeMs() {
+ return totalSeekCount == 0 ? C.TIME_UNSET : getTotalSeekTimeMs() / totalSeekCount;
+ }
+
+ /**
+ * Returns the total time spent actively waiting for playback, in milliseconds. This includes all
+ * join times, rebuffer times and seek times, but excludes times without user intention to play,
+ * e.g. all paused states.
+ */
+ public long getTotalWaitTimeMs() {
+ return getPlaybackStateDurationMs(PLAYBACK_STATE_JOINING_FOREGROUND)
+ + getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING)
+ + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING)
+ + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING);
+ }
+
+ /**
+ * Returns the mean time spent actively waiting for playback per foreground playback, in
+ * milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground. This includes all
+ * join times, rebuffer times and seek times, but excludes times without user intention to play,
+ * e.g. all paused states.
+ */
+ public long getMeanWaitTimeMs() {
+ return foregroundPlaybackCount == 0
+ ? C.TIME_UNSET
+ : getTotalWaitTimeMs() / foregroundPlaybackCount;
+ }
+
+ /** Returns the total time spent playing or actively waiting for playback, in milliseconds. */
+ public long getTotalPlayAndWaitTimeMs() {
+ return getTotalPlayTimeMs() + getTotalWaitTimeMs();
+ }
+
+ /**
+ * Returns the mean time spent playing or actively waiting for playback per foreground playback,
+ * in milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground.
+ */
+ public long getMeanPlayAndWaitTimeMs() {
+ return foregroundPlaybackCount == 0
+ ? C.TIME_UNSET
+ : getTotalPlayAndWaitTimeMs() / foregroundPlaybackCount;
+ }
+
+ /** Returns the total time covered by any playback state, in milliseconds. */
+ public long getTotalElapsedTimeMs() {
+ long totalTimeMs = 0;
+ for (int i = 0; i < PLAYBACK_STATE_COUNT; i++) {
+ totalTimeMs += playbackStateDurationsMs[i];
+ }
+ return totalTimeMs;
+ }
+
+ /**
+ * Returns the mean time covered by any playback state per playback, in milliseconds, or {@link
+ * C#TIME_UNSET} if no playback was recorded.
+ */
+ public long getMeanElapsedTimeMs() {
+ return playbackCount == 0 ? C.TIME_UNSET : getTotalElapsedTimeMs() / playbackCount;
+ }
+
+ /**
+ * Returns the ratio of foreground playbacks which were abandoned before they were ready to play,
+ * or {@code 0.0} if no playback has been in foreground.
+ */
+ public float getAbandonedBeforeReadyRatio() {
+ int foregroundAbandonedBeforeReady =
+ abandonedBeforeReadyCount - (playbackCount - foregroundPlaybackCount);
+ return foregroundPlaybackCount == 0
+ ? 0f
+ : (float) foregroundAbandonedBeforeReady / foregroundPlaybackCount;
+ }
+
+ /**
+ * Returns the ratio of foreground playbacks which reached the ended state at least once, or
+ * {@code 0.0} if no playback has been in foreground.
+ */
+ public float getEndedRatio() {
+ return foregroundPlaybackCount == 0 ? 0f : (float) endedCount / foregroundPlaybackCount;
+ }
+
+ /**
+ * Returns the mean number of times a playback has been paused per foreground playback, or {@code
+ * 0.0} if no playback has been in foreground.
+ */
+ public float getMeanPauseCount() {
+ return foregroundPlaybackCount == 0 ? 0f : (float) totalPauseCount / foregroundPlaybackCount;
+ }
+
+ /**
+ * Returns the mean number of times a playback has been paused while rebuffering per foreground
+ * playback, or {@code 0.0} if no playback has been in foreground.
+ */
+ public float getMeanPauseBufferCount() {
+ return foregroundPlaybackCount == 0
+ ? 0f
+ : (float) totalPauseBufferCount / foregroundPlaybackCount;
+ }
+
+ /**
+ * Returns the mean number of times a seek occurred per foreground playback, or {@code 0.0} if no
+ * playback has been in foreground. This includes seeks happening before playback resumed after
+ * another seek.
+ */
+ public float getMeanSeekCount() {
+ return foregroundPlaybackCount == 0 ? 0f : (float) totalSeekCount / foregroundPlaybackCount;
+ }
+
+ /**
+ * Returns the mean number of times a rebuffer occurred per foreground playback, or {@code 0.0} if
+ * no playback has been in foreground. This excludes initial joining and buffering after seek.
+ */
+ public float getMeanRebufferCount() {
+ return foregroundPlaybackCount == 0 ? 0f : (float) totalRebufferCount / foregroundPlaybackCount;
+ }
+
+ /**
+ * Returns the ratio of wait times to the total time spent playing and waiting, or {@code 0.0} if
+ * no time was spend playing or waiting. This is equivalent to {@link #getTotalWaitTimeMs()} /
+ * {@link #getTotalPlayAndWaitTimeMs()} and also to {@link #getJoinTimeRatio()} + {@link
+ * #getRebufferTimeRatio()} + {@link #getSeekTimeRatio()}.
+ */
+ public float getWaitTimeRatio() {
+ long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs();
+ return playAndWaitTimeMs == 0 ? 0f : (float) getTotalWaitTimeMs() / playAndWaitTimeMs;
+ }
+
+ /**
+ * Returns the ratio of foreground join time to the total time spent playing and waiting, or
+ * {@code 0.0} if no time was spend playing or waiting. This is equivalent to {@link
+ * #getTotalJoinTimeMs()} / {@link #getTotalPlayAndWaitTimeMs()}.
+ */
+ public float getJoinTimeRatio() {
+ long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs();
+ return playAndWaitTimeMs == 0 ? 0f : (float) getTotalJoinTimeMs() / playAndWaitTimeMs;
+ }
+
+ /**
+ * Returns the ratio of rebuffer time to the total time spent playing and waiting, or {@code 0.0}
+ * if no time was spend playing or waiting. This is equivalent to {@link
+ * #getTotalRebufferTimeMs()} / {@link #getTotalPlayAndWaitTimeMs()}.
+ */
+ public float getRebufferTimeRatio() {
+ long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs();
+ return playAndWaitTimeMs == 0 ? 0f : (float) getTotalRebufferTimeMs() / playAndWaitTimeMs;
+ }
+
+ /**
+ * Returns the ratio of seek time to the total time spent playing and waiting, or {@code 0.0} if
+ * no time was spend playing or waiting. This is equivalent to {@link #getTotalSeekTimeMs()} /
+ * {@link #getTotalPlayAndWaitTimeMs()}.
+ */
+ public float getSeekTimeRatio() {
+ long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs();
+ return playAndWaitTimeMs == 0 ? 0f : (float) getTotalSeekTimeMs() / playAndWaitTimeMs;
+ }
+
+ /**
+ * Returns the rate of rebuffer events, in rebuffers per play time second, or {@code 0.0} if no
+ * time was spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenRebuffers()}.
+ */
+ public float getRebufferRate() {
+ long playTimeMs = getTotalPlayTimeMs();
+ return playTimeMs == 0 ? 0f : 1000f * totalRebufferCount / playTimeMs;
+ }
+
+ /**
+ * Returns the mean play time between rebuffer events, in seconds. This is equivalent to 1.0 /
+ * {@link #getRebufferRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}.
+ */
+ public float getMeanTimeBetweenRebuffers() {
+ return 1f / getRebufferRate();
+ }
+
+ /**
+ * Returns the mean initial video format height, in pixels, or {@link C#LENGTH_UNSET} if no video
+ * format data is available.
+ */
+ public int getMeanInitialVideoFormatHeight() {
+ return initialVideoFormatHeightCount == 0
+ ? C.LENGTH_UNSET
+ : totalInitialVideoFormatHeight / initialVideoFormatHeightCount;
+ }
+
+ /**
+ * Returns the mean initial video format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if
+ * no video format data is available.
+ */
+ public int getMeanInitialVideoFormatBitrate() {
+ return initialVideoFormatBitrateCount == 0
+ ? C.LENGTH_UNSET
+ : (int) (totalInitialVideoFormatBitrate / initialVideoFormatBitrateCount);
+ }
+
+ /**
+ * Returns the mean initial audio format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if
+ * no audio format data is available.
+ */
+ public int getMeanInitialAudioFormatBitrate() {
+ return initialAudioFormatBitrateCount == 0
+ ? C.LENGTH_UNSET
+ : (int) (totalInitialAudioFormatBitrate / initialAudioFormatBitrateCount);
+ }
+
+ /**
+ * Returns the mean video format height, in pixels, or {@link C#LENGTH_UNSET} if no video format
+ * data is available. This is a weighted average taking the time the format was used for playback
+ * into account.
+ */
+ public int getMeanVideoFormatHeight() {
+ return totalVideoFormatHeightTimeMs == 0
+ ? C.LENGTH_UNSET
+ : (int) (totalVideoFormatHeightTimeProduct / totalVideoFormatHeightTimeMs);
+ }
+
+ /**
+ * Returns the mean video format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if no
+ * video format data is available. This is a weighted average taking the time the format was used
+ * for playback into account.
+ */
+ public int getMeanVideoFormatBitrate() {
+ return totalVideoFormatBitrateTimeMs == 0
+ ? C.LENGTH_UNSET
+ : (int) (totalVideoFormatBitrateTimeProduct / totalVideoFormatBitrateTimeMs);
+ }
+
+ /**
+ * Returns the mean audio format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if no
+ * audio format data is available. This is a weighted average taking the time the format was used
+ * for playback into account.
+ */
+ public int getMeanAudioFormatBitrate() {
+ return totalAudioFormatTimeMs == 0
+ ? C.LENGTH_UNSET
+ : (int) (totalAudioFormatBitrateTimeProduct / totalAudioFormatTimeMs);
+ }
+
+ /**
+ * Returns the mean network bandwidth based on transfer measurements, in bits per second, or
+ * {@link C#LENGTH_UNSET} if no transfer data is available.
+ */
+ public int getMeanBandwidth() {
+ return totalBandwidthTimeMs == 0
+ ? C.LENGTH_UNSET
+ : (int) (totalBandwidthBytes * 8000 / totalBandwidthTimeMs);
+ }
+
+ /**
+ * Returns the mean rate at which video frames are dropped, in dropped frames per play time
+ * second, or {@code 0.0} if no time was spent playing.
+ */
+ public float getDroppedFramesRate() {
+ long playTimeMs = getTotalPlayTimeMs();
+ return playTimeMs == 0 ? 0f : 1000f * totalDroppedFrames / playTimeMs;
+ }
+
+ /**
+ * Returns the mean rate at which audio underruns occurred, in underruns per play time second, or
+ * {@code 0.0} if no time was spent playing.
+ */
+ public float getAudioUnderrunRate() {
+ long playTimeMs = getTotalPlayTimeMs();
+ return playTimeMs == 0 ? 0f : 1000f * totalAudioUnderruns / playTimeMs;
+ }
+
+ /**
+ * Returns the ratio of foreground playbacks which experienced fatal errors, or {@code 0.0} if no
+ * playback has been in foreground.
+ */
+ public float getFatalErrorRatio() {
+ return foregroundPlaybackCount == 0
+ ? 0f
+ : (float) fatalErrorPlaybackCount / foregroundPlaybackCount;
+ }
+
+ /**
+ * Returns the rate of fatal errors, in errors per play time second, or {@code 0.0} if no time was
+ * spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenFatalErrors()}.
+ */
+ public float getFatalErrorRate() {
+ long playTimeMs = getTotalPlayTimeMs();
+ return playTimeMs == 0 ? 0f : 1000f * fatalErrorCount / playTimeMs;
+ }
+
+ /**
+ * Returns the mean play time between fatal errors, in seconds. This is equivalent to 1.0 / {@link
+ * #getFatalErrorRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}.
+ */
+ public float getMeanTimeBetweenFatalErrors() {
+ return 1f / getFatalErrorRate();
+ }
+
+ /**
+ * Returns the mean number of non-fatal errors per foreground playback, or {@code 0.0} if no
+ * playback has been in foreground.
+ */
+ public float getMeanNonFatalErrorCount() {
+ return foregroundPlaybackCount == 0 ? 0f : (float) nonFatalErrorCount / foregroundPlaybackCount;
+ }
+
+ /**
+ * Returns the rate of non-fatal errors, in errors per play time second, or {@code 0.0} if no time
+ * was spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenNonFatalErrors()}.
+ */
+ public float getNonFatalErrorRate() {
+ long playTimeMs = getTotalPlayTimeMs();
+ return playTimeMs == 0 ? 0f : 1000f * nonFatalErrorCount / playTimeMs;
+ }
+
+ /**
+ * Returns the mean play time between non-fatal errors, in seconds. This is equivalent to 1.0 /
+ * {@link #getNonFatalErrorRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}.
+ */
+ public float getMeanTimeBetweenNonFatalErrors() {
+ return 1f / getNonFatalErrorRate();
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java
new file mode 100644
index 0000000000..058a3a97c1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java
@@ -0,0 +1,1059 @@
+/*
+ * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.analytics;
+
+import android.os.SystemClock;
+import android.util.Pair;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline.Period;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.PlaybackStats.PlaybackState;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * {@link AnalyticsListener} to gather {@link PlaybackStats} from the player.
+ *
+ * <p>For accurate measurements, the listener should be added to the player before loading media,
+ * i.e., {@link Player#getPlaybackState()} should be {@link Player#STATE_IDLE}.
+ *
+ * <p>Playback stats are gathered separately for each playback session, i.e. each window in the
+ * {@link Timeline} and each single ad.
+ */
+public final class PlaybackStatsListener
+ implements AnalyticsListener, PlaybackSessionManager.Listener {
+
+ /** A listener for {@link PlaybackStats} updates. */
+ public interface Callback {
+
+ /**
+ * Called when a playback session ends and its {@link PlaybackStats} are ready.
+ *
+ * @param eventTime The {@link EventTime} at which the playback session started. Can be used to
+ * identify the playback session.
+ * @param playbackStats The {@link PlaybackStats} for the ended playback session.
+ */
+ void onPlaybackStatsReady(EventTime eventTime, PlaybackStats playbackStats);
+ }
+
+ private final PlaybackSessionManager sessionManager;
+ private final Map<String, PlaybackStatsTracker> playbackStatsTrackers;
+ private final Map<String, EventTime> sessionStartEventTimes;
+ @Nullable private final Callback callback;
+ private final boolean keepHistory;
+ private final Period period;
+
+ private PlaybackStats finishedPlaybackStats;
+ @Nullable private String activeContentPlayback;
+ @Nullable private String activeAdPlayback;
+ private boolean playWhenReady;
+ @Player.State private int playbackState;
+ private boolean isSuppressed;
+ private float playbackSpeed;
+
+ /**
+ * Creates listener for playback stats.
+ *
+ * @param keepHistory Whether the reported {@link PlaybackStats} should keep the full history of
+ * events.
+ * @param callback An optional callback for finished {@link PlaybackStats}.
+ */
+ public PlaybackStatsListener(boolean keepHistory, @Nullable Callback callback) {
+ this.callback = callback;
+ this.keepHistory = keepHistory;
+ sessionManager = new DefaultPlaybackSessionManager();
+ playbackStatsTrackers = new HashMap<>();
+ sessionStartEventTimes = new HashMap<>();
+ finishedPlaybackStats = PlaybackStats.EMPTY;
+ playWhenReady = false;
+ playbackState = Player.STATE_IDLE;
+ playbackSpeed = 1f;
+ period = new Period();
+ sessionManager.setListener(this);
+ }
+
+ /**
+ * Returns the combined {@link PlaybackStats} for all playback sessions this listener was and is
+ * listening to.
+ *
+ * <p>Note that these {@link PlaybackStats} will not contain the full history of events.
+ *
+ * @return The combined {@link PlaybackStats} for all playback sessions.
+ */
+ public PlaybackStats getCombinedPlaybackStats() {
+ PlaybackStats[] allPendingPlaybackStats = new PlaybackStats[playbackStatsTrackers.size() + 1];
+ allPendingPlaybackStats[0] = finishedPlaybackStats;
+ int index = 1;
+ for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) {
+ allPendingPlaybackStats[index++] = tracker.build(/* isFinal= */ false);
+ }
+ return PlaybackStats.merge(allPendingPlaybackStats);
+ }
+
+ /**
+ * Returns the {@link PlaybackStats} for the currently playback session, or null if no session is
+ * active.
+ *
+ * @return {@link PlaybackStats} for the current playback session.
+ */
+ @Nullable
+ public PlaybackStats getPlaybackStats() {
+ PlaybackStatsTracker activeStatsTracker =
+ activeAdPlayback != null
+ ? playbackStatsTrackers.get(activeAdPlayback)
+ : activeContentPlayback != null
+ ? playbackStatsTrackers.get(activeContentPlayback)
+ : null;
+ return activeStatsTracker == null ? null : activeStatsTracker.build(/* isFinal= */ false);
+ }
+
+ /**
+ * Finishes all pending playback sessions. Should be called when the listener is removed from the
+ * player or when the player is released.
+ */
+ public void finishAllSessions() {
+ // TODO: Add AnalyticsListener.onAttachedToPlayer and onDetachedFromPlayer to auto-release with
+ // an actual EventTime. Should also simplify other cases where the listener needs to be released
+ // separately from the player.
+ HashMap<String, PlaybackStatsTracker> trackerCopy = new HashMap<>(playbackStatsTrackers);
+ EventTime dummyEventTime =
+ new EventTime(
+ SystemClock.elapsedRealtime(),
+ Timeline.EMPTY,
+ /* windowIndex= */ 0,
+ /* mediaPeriodId= */ null,
+ /* eventPlaybackPositionMs= */ 0,
+ /* currentPlaybackPositionMs= */ 0,
+ /* totalBufferedDurationMs= */ 0);
+ for (String session : trackerCopy.keySet()) {
+ onSessionFinished(dummyEventTime, session, /* automaticTransition= */ false);
+ }
+ }
+
+ // PlaybackSessionManager.Listener implementation.
+
+ @Override
+ public void onSessionCreated(EventTime eventTime, String session) {
+ PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime);
+ tracker.onPlayerStateChanged(
+ eventTime, playWhenReady, playbackState, /* belongsToPlayback= */ true);
+ tracker.onIsSuppressedChanged(eventTime, isSuppressed, /* belongsToPlayback= */ true);
+ tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed);
+ playbackStatsTrackers.put(session, tracker);
+ sessionStartEventTimes.put(session, eventTime);
+ }
+
+ @Override
+ public void onSessionActive(EventTime eventTime, String session) {
+ Assertions.checkNotNull(playbackStatsTrackers.get(session)).onForeground(eventTime);
+ if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) {
+ activeAdPlayback = session;
+ } else {
+ activeContentPlayback = session;
+ }
+ }
+
+ @Override
+ public void onAdPlaybackStarted(EventTime eventTime, String contentSession, String adSession) {
+ Assertions.checkState(Assertions.checkNotNull(eventTime.mediaPeriodId).isAd());
+ long contentPositionUs =
+ eventTime
+ .timeline
+ .getPeriodByUid(eventTime.mediaPeriodId.periodUid, period)
+ .getAdGroupTimeUs(eventTime.mediaPeriodId.adGroupIndex);
+ EventTime contentEventTime =
+ new EventTime(
+ eventTime.realtimeMs,
+ eventTime.timeline,
+ eventTime.windowIndex,
+ new MediaPeriodId(
+ eventTime.mediaPeriodId.periodUid,
+ eventTime.mediaPeriodId.windowSequenceNumber,
+ eventTime.mediaPeriodId.adGroupIndex),
+ /* eventPlaybackPositionMs= */ C.usToMs(contentPositionUs),
+ eventTime.currentPlaybackPositionMs,
+ eventTime.totalBufferedDurationMs);
+ Assertions.checkNotNull(playbackStatsTrackers.get(contentSession))
+ .onInterruptedByAd(contentEventTime);
+ }
+
+ @Override
+ public void onSessionFinished(EventTime eventTime, String session, boolean automaticTransition) {
+ if (session.equals(activeAdPlayback)) {
+ activeAdPlayback = null;
+ } else if (session.equals(activeContentPlayback)) {
+ activeContentPlayback = null;
+ }
+ PlaybackStatsTracker tracker = Assertions.checkNotNull(playbackStatsTrackers.remove(session));
+ EventTime startEventTime = Assertions.checkNotNull(sessionStartEventTimes.remove(session));
+ if (automaticTransition) {
+ // Simulate ENDED state to record natural ending of playback.
+ tracker.onPlayerStateChanged(
+ eventTime, /* playWhenReady= */ true, Player.STATE_ENDED, /* belongsToPlayback= */ false);
+ }
+ tracker.onFinished(eventTime);
+ PlaybackStats playbackStats = tracker.build(/* isFinal= */ true);
+ finishedPlaybackStats = PlaybackStats.merge(finishedPlaybackStats, playbackStats);
+ if (callback != null) {
+ callback.onPlaybackStatsReady(startEventTime, playbackStats);
+ }
+ }
+
+ // AnalyticsListener implementation.
+
+ @Override
+ public void onPlayerStateChanged(
+ EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) {
+ this.playWhenReady = playWhenReady;
+ this.playbackState = playbackState;
+ sessionManager.updateSessions(eventTime);
+ for (String session : playbackStatsTrackers.keySet()) {
+ boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
+ playbackStatsTrackers
+ .get(session)
+ .onPlayerStateChanged(eventTime, playWhenReady, playbackState, belongsToPlayback);
+ }
+ }
+
+ @Override
+ public void onPlaybackSuppressionReasonChanged(
+ EventTime eventTime, int playbackSuppressionReason) {
+ isSuppressed = playbackSuppressionReason != Player.PLAYBACK_SUPPRESSION_REASON_NONE;
+ sessionManager.updateSessions(eventTime);
+ for (String session : playbackStatsTrackers.keySet()) {
+ boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
+ playbackStatsTrackers
+ .get(session)
+ .onIsSuppressedChanged(eventTime, isSuppressed, belongsToPlayback);
+ }
+ }
+
+ @Override
+ public void onTimelineChanged(EventTime eventTime, int reason) {
+ sessionManager.handleTimelineUpdate(eventTime);
+ sessionManager.updateSessions(eventTime);
+ for (String session : playbackStatsTrackers.keySet()) {
+ if (sessionManager.belongsToSession(eventTime, session)) {
+ playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime);
+ }
+ }
+ }
+
+ @Override
+ public void onPositionDiscontinuity(EventTime eventTime, int reason) {
+ sessionManager.handlePositionDiscontinuity(eventTime, reason);
+ sessionManager.updateSessions(eventTime);
+ for (String session : playbackStatsTrackers.keySet()) {
+ if (sessionManager.belongsToSession(eventTime, session)) {
+ playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime);
+ }
+ }
+ }
+
+ @Override
+ public void onSeekStarted(EventTime eventTime) {
+ sessionManager.updateSessions(eventTime);
+ for (String session : playbackStatsTrackers.keySet()) {
+ if (sessionManager.belongsToSession(eventTime, session)) {
+ playbackStatsTrackers.get(session).onSeekStarted(eventTime);
+ }
+ }
+ }
+
+ @Override
+ public void onSeekProcessed(EventTime eventTime) {
+ sessionManager.updateSessions(eventTime);
+ for (String session : playbackStatsTrackers.keySet()) {
+ if (sessionManager.belongsToSession(eventTime, session)) {
+ playbackStatsTrackers.get(session).onSeekProcessed(eventTime);
+ }
+ }
+ }
+
+ @Override
+ public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {
+ sessionManager.updateSessions(eventTime);
+ for (String session : playbackStatsTrackers.keySet()) {
+ if (sessionManager.belongsToSession(eventTime, session)) {
+ playbackStatsTrackers.get(session).onFatalError(eventTime, error);
+ }
+ }
+ }
+
+ @Override
+ public void onPlaybackParametersChanged(
+ EventTime eventTime, PlaybackParameters playbackParameters) {
+ playbackSpeed = playbackParameters.speed;
+ sessionManager.updateSessions(eventTime);
+ for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) {
+ tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed);
+ }
+ }
+
+ @Override
+ public void onTracksChanged(
+ EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
+ sessionManager.updateSessions(eventTime);
+ for (String session : playbackStatsTrackers.keySet()) {
+ if (sessionManager.belongsToSession(eventTime, session)) {
+ playbackStatsTrackers.get(session).onTracksChanged(eventTime, trackSelections);
+ }
+ }
+ }
+
+ @Override
+ public void onLoadStarted(
+ EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
+ sessionManager.updateSessions(eventTime);
+ for (String session : playbackStatsTrackers.keySet()) {
+ if (sessionManager.belongsToSession(eventTime, session)) {
+ playbackStatsTrackers.get(session).onLoadStarted(eventTime);
+ }
+ }
+ }
+
+ @Override
+ public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {
+ sessionManager.updateSessions(eventTime);
+ for (String session : playbackStatsTrackers.keySet()) {
+ if (sessionManager.belongsToSession(eventTime, session)) {
+ playbackStatsTrackers.get(session).onDownstreamFormatChanged(eventTime, mediaLoadData);
+ }
+ }
+ }
+
+ @Override
+ public void onVideoSizeChanged(
+ EventTime eventTime,
+ int width,
+ int height,
+ int unappliedRotationDegrees,
+ float pixelWidthHeightRatio) {
+ sessionManager.updateSessions(eventTime);
+ for (String session : playbackStatsTrackers.keySet()) {
+ if (sessionManager.belongsToSession(eventTime, session)) {
+ playbackStatsTrackers.get(session).onVideoSizeChanged(eventTime, width, height);
+ }
+ }
+ }
+
+ @Override
+ public void onBandwidthEstimate(
+ EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {
+ sessionManager.updateSessions(eventTime);
+ for (String session : playbackStatsTrackers.keySet()) {
+ if (sessionManager.belongsToSession(eventTime, session)) {
+ playbackStatsTrackers.get(session).onBandwidthData(totalLoadTimeMs, totalBytesLoaded);
+ }
+ }
+ }
+
+ @Override
+ public void onAudioUnderrun(
+ EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
+ sessionManager.updateSessions(eventTime);
+ for (String session : playbackStatsTrackers.keySet()) {
+ if (sessionManager.belongsToSession(eventTime, session)) {
+ playbackStatsTrackers.get(session).onAudioUnderrun();
+ }
+ }
+ }
+
+ @Override
+ public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {
+ sessionManager.updateSessions(eventTime);
+ for (String session : playbackStatsTrackers.keySet()) {
+ if (sessionManager.belongsToSession(eventTime, session)) {
+ playbackStatsTrackers.get(session).onDroppedVideoFrames(droppedFrames);
+ }
+ }
+ }
+
+ @Override
+ public void onLoadError(
+ EventTime eventTime,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData,
+ IOException error,
+ boolean wasCanceled) {
+ sessionManager.updateSessions(eventTime);
+ for (String session : playbackStatsTrackers.keySet()) {
+ if (sessionManager.belongsToSession(eventTime, session)) {
+ playbackStatsTrackers.get(session).onNonFatalError(eventTime, error);
+ }
+ }
+ }
+
+ @Override
+ public void onDrmSessionManagerError(EventTime eventTime, Exception error) {
+ sessionManager.updateSessions(eventTime);
+ for (String session : playbackStatsTrackers.keySet()) {
+ if (sessionManager.belongsToSession(eventTime, session)) {
+ playbackStatsTrackers.get(session).onNonFatalError(eventTime, error);
+ }
+ }
+ }
+
+ /** Tracker for playback stats of a single playback. */
+ private static final class PlaybackStatsTracker {
+
+ // Final stats.
+ private final boolean keepHistory;
+ private final long[] playbackStateDurationsMs;
+ private final List<Pair<EventTime, @PlaybackState Integer>> playbackStateHistory;
+ private final List<long[]> mediaTimeHistory;
+ private final List<Pair<EventTime, @NullableType Format>> videoFormatHistory;
+ private final List<Pair<EventTime, @NullableType Format>> audioFormatHistory;
+ private final List<Pair<EventTime, Exception>> fatalErrorHistory;
+ private final List<Pair<EventTime, Exception>> nonFatalErrorHistory;
+ private final boolean isAd;
+
+ private long firstReportedTimeMs;
+ private boolean hasBeenReady;
+ private boolean hasEnded;
+ private boolean isJoinTimeInvalid;
+ private int pauseCount;
+ private int pauseBufferCount;
+ private int seekCount;
+ private int rebufferCount;
+ private long maxRebufferTimeMs;
+ private int initialVideoFormatHeight;
+ private long initialVideoFormatBitrate;
+ private long initialAudioFormatBitrate;
+ private long videoFormatHeightTimeMs;
+ private long videoFormatHeightTimeProduct;
+ private long videoFormatBitrateTimeMs;
+ private long videoFormatBitrateTimeProduct;
+ private long audioFormatTimeMs;
+ private long audioFormatBitrateTimeProduct;
+ private long bandwidthTimeMs;
+ private long bandwidthBytes;
+ private long droppedFrames;
+ private long audioUnderruns;
+ private int fatalErrorCount;
+ private int nonFatalErrorCount;
+
+ // Current player state tracking.
+ private @PlaybackState int currentPlaybackState;
+ private long currentPlaybackStateStartTimeMs;
+ private boolean isSeeking;
+ private boolean isForeground;
+ private boolean isInterruptedByAd;
+ private boolean isFinished;
+ private boolean playWhenReady;
+ @Player.State private int playerPlaybackState;
+ private boolean isSuppressed;
+ private boolean hasFatalError;
+ private boolean startedLoading;
+ private long lastRebufferStartTimeMs;
+ @Nullable private Format currentVideoFormat;
+ @Nullable private Format currentAudioFormat;
+ private long lastVideoFormatStartTimeMs;
+ private long lastAudioFormatStartTimeMs;
+ private float currentPlaybackSpeed;
+
+ /**
+ * Creates a tracker for playback stats.
+ *
+ * @param keepHistory Whether to keep a full history of events.
+ * @param startTime The {@link EventTime} at which the playback stats start.
+ */
+ public PlaybackStatsTracker(boolean keepHistory, EventTime startTime) {
+ this.keepHistory = keepHistory;
+ playbackStateDurationsMs = new long[PlaybackStats.PLAYBACK_STATE_COUNT];
+ playbackStateHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
+ mediaTimeHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
+ videoFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
+ audioFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
+ fatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
+ nonFatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList();
+ currentPlaybackState = PlaybackStats.PLAYBACK_STATE_NOT_STARTED;
+ currentPlaybackStateStartTimeMs = startTime.realtimeMs;
+ playerPlaybackState = Player.STATE_IDLE;
+ firstReportedTimeMs = C.TIME_UNSET;
+ maxRebufferTimeMs = C.TIME_UNSET;
+ isAd = startTime.mediaPeriodId != null && startTime.mediaPeriodId.isAd();
+ initialAudioFormatBitrate = C.LENGTH_UNSET;
+ initialVideoFormatBitrate = C.LENGTH_UNSET;
+ initialVideoFormatHeight = C.LENGTH_UNSET;
+ currentPlaybackSpeed = 1f;
+ }
+
+ /**
+ * Notifies the tracker of a player state change event, including all player state changes while
+ * the playback is not in the foreground.
+ *
+ * @param eventTime The {@link EventTime}.
+ * @param playWhenReady Whether the playback will proceed when ready.
+ * @param playbackState The current {@link Player.State}.
+ * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback.
+ */
+ public void onPlayerStateChanged(
+ EventTime eventTime,
+ boolean playWhenReady,
+ @Player.State int playbackState,
+ boolean belongsToPlayback) {
+ this.playWhenReady = playWhenReady;
+ playerPlaybackState = playbackState;
+ if (playbackState != Player.STATE_IDLE) {
+ hasFatalError = false;
+ }
+ if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED) {
+ isInterruptedByAd = false;
+ }
+ maybeUpdatePlaybackState(eventTime, belongsToPlayback);
+ }
+
+ /**
+ * Notifies the tracker of a change to the playback suppression (e.g. due to audio focus loss),
+ * including all updates while the playback is not in the foreground.
+ *
+ * @param eventTime The {@link EventTime}.
+ * @param isSuppressed Whether playback is suppressed.
+ * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback.
+ */
+ public void onIsSuppressedChanged(
+ EventTime eventTime, boolean isSuppressed, boolean belongsToPlayback) {
+ this.isSuppressed = isSuppressed;
+ maybeUpdatePlaybackState(eventTime, belongsToPlayback);
+ }
+
+ /**
+ * Notifies the tracker of a position discontinuity or timeline update for the current playback.
+ *
+ * @param eventTime The {@link EventTime}.
+ */
+ public void onPositionDiscontinuity(EventTime eventTime) {
+ isInterruptedByAd = false;
+ maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
+ }
+
+ /**
+ * Notifies the tracker of the start of a seek in the current playback.
+ *
+ * @param eventTime The {@link EventTime}.
+ */
+ public void onSeekStarted(EventTime eventTime) {
+ isSeeking = true;
+ maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
+ }
+
+ /**
+ * Notifies the tracker of a seek has been processed in the current playback.
+ *
+ * @param eventTime The {@link EventTime}.
+ */
+ public void onSeekProcessed(EventTime eventTime) {
+ isSeeking = false;
+ maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
+ }
+
+ /**
+ * Notifies the tracker of fatal player error in the current playback.
+ *
+ * @param eventTime The {@link EventTime}.
+ */
+ public void onFatalError(EventTime eventTime, Exception error) {
+ fatalErrorCount++;
+ if (keepHistory) {
+ fatalErrorHistory.add(Pair.create(eventTime, error));
+ }
+ hasFatalError = true;
+ isInterruptedByAd = false;
+ isSeeking = false;
+ maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
+ }
+
+ /**
+ * Notifies the tracker that a load for the current playback has started.
+ *
+ * @param eventTime The {@link EventTime}.
+ */
+ public void onLoadStarted(EventTime eventTime) {
+ startedLoading = true;
+ maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
+ }
+
+ /**
+ * Notifies the tracker that the current playback became the active foreground playback.
+ *
+ * @param eventTime The {@link EventTime}.
+ */
+ public void onForeground(EventTime eventTime) {
+ isForeground = true;
+ maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
+ }
+
+ /**
+ * Notifies the tracker that the current playback has been interrupted for ad playback.
+ *
+ * @param eventTime The {@link EventTime}.
+ */
+ public void onInterruptedByAd(EventTime eventTime) {
+ isInterruptedByAd = true;
+ isSeeking = false;
+ maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
+ }
+
+ /**
+ * Notifies the tracker that the current playback has finished.
+ *
+ * @param eventTime The {@link EventTime}. Not guaranteed to belong to the current playback.
+ */
+ public void onFinished(EventTime eventTime) {
+ isFinished = true;
+ maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ false);
+ }
+
+ /**
+ * Notifies the tracker that the track selection for the current playback changed.
+ *
+ * @param eventTime The {@link EventTime}.
+ * @param trackSelections The new {@link TrackSelectionArray}.
+ */
+ public void onTracksChanged(EventTime eventTime, TrackSelectionArray trackSelections) {
+ boolean videoEnabled = false;
+ boolean audioEnabled = false;
+ for (TrackSelection trackSelection : trackSelections.getAll()) {
+ if (trackSelection != null && trackSelection.length() > 0) {
+ int trackType = MimeTypes.getTrackType(trackSelection.getFormat(0).sampleMimeType);
+ if (trackType == C.TRACK_TYPE_VIDEO) {
+ videoEnabled = true;
+ } else if (trackType == C.TRACK_TYPE_AUDIO) {
+ audioEnabled = true;
+ }
+ }
+ }
+ if (!videoEnabled) {
+ maybeUpdateVideoFormat(eventTime, /* newFormat= */ null);
+ }
+ if (!audioEnabled) {
+ maybeUpdateAudioFormat(eventTime, /* newFormat= */ null);
+ }
+ }
+
+ /**
+ * Notifies the tracker that a format being read by the renderers for the current playback
+ * changed.
+ *
+ * @param eventTime The {@link EventTime}.
+ * @param mediaLoadData The {@link MediaLoadData} describing the format change.
+ */
+ public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {
+ if (mediaLoadData.trackType == C.TRACK_TYPE_VIDEO
+ || mediaLoadData.trackType == C.TRACK_TYPE_DEFAULT) {
+ maybeUpdateVideoFormat(eventTime, mediaLoadData.trackFormat);
+ } else if (mediaLoadData.trackType == C.TRACK_TYPE_AUDIO) {
+ maybeUpdateAudioFormat(eventTime, mediaLoadData.trackFormat);
+ }
+ }
+
+ /**
+ * Notifies the tracker that the video size for the current playback changed.
+ *
+ * @param eventTime The {@link EventTime}.
+ * @param width The video width in pixels.
+ * @param height The video height in pixels.
+ */
+ public void onVideoSizeChanged(EventTime eventTime, int width, int height) {
+ if (currentVideoFormat != null && currentVideoFormat.height == Format.NO_VALUE) {
+ Format formatWithHeight = currentVideoFormat.copyWithVideoSize(width, height);
+ maybeUpdateVideoFormat(eventTime, formatWithHeight);
+ }
+ }
+
+ /**
+ * Notifies the tracker of a playback speed change, including all playback speed changes while
+ * the playback is not in the foreground.
+ *
+ * @param eventTime The {@link EventTime}.
+ * @param playbackSpeed The new playback speed.
+ */
+ public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) {
+ maybeUpdateMediaTimeHistory(eventTime.realtimeMs, eventTime.eventPlaybackPositionMs);
+ maybeRecordVideoFormatTime(eventTime.realtimeMs);
+ maybeRecordAudioFormatTime(eventTime.realtimeMs);
+ currentPlaybackSpeed = playbackSpeed;
+ }
+
+ /** Notifies the builder of an audio underrun for the current playback. */
+ public void onAudioUnderrun() {
+ audioUnderruns++;
+ }
+
+ /**
+ * Notifies the tracker of dropped video frames for the current playback.
+ *
+ * @param droppedFrames The number of dropped video frames.
+ */
+ public void onDroppedVideoFrames(int droppedFrames) {
+ this.droppedFrames += droppedFrames;
+ }
+
+ /**
+ * Notifies the tracker of bandwidth measurement data for the current playback.
+ *
+ * @param timeMs The time for which bandwidth measurement data is available, in milliseconds.
+ * @param bytes The bytes transferred during {@code timeMs}.
+ */
+ public void onBandwidthData(long timeMs, long bytes) {
+ bandwidthTimeMs += timeMs;
+ bandwidthBytes += bytes;
+ }
+
+ /**
+ * Notifies the tracker of a non-fatal error in the current playback.
+ *
+ * @param eventTime The {@link EventTime}.
+ * @param error The error.
+ */
+ public void onNonFatalError(EventTime eventTime, Exception error) {
+ nonFatalErrorCount++;
+ if (keepHistory) {
+ nonFatalErrorHistory.add(Pair.create(eventTime, error));
+ }
+ }
+
+ /**
+ * Builds the playback stats.
+ *
+ * @param isFinal Whether this is the final build and no further events are expected.
+ */
+ public PlaybackStats build(boolean isFinal) {
+ long[] playbackStateDurationsMs = this.playbackStateDurationsMs;
+ List<long[]> mediaTimeHistory = this.mediaTimeHistory;
+ if (!isFinal) {
+ long buildTimeMs = SystemClock.elapsedRealtime();
+ playbackStateDurationsMs =
+ Arrays.copyOf(this.playbackStateDurationsMs, PlaybackStats.PLAYBACK_STATE_COUNT);
+ long lastStateDurationMs = Math.max(0, buildTimeMs - currentPlaybackStateStartTimeMs);
+ playbackStateDurationsMs[currentPlaybackState] += lastStateDurationMs;
+ maybeUpdateMaxRebufferTimeMs(buildTimeMs);
+ maybeRecordVideoFormatTime(buildTimeMs);
+ maybeRecordAudioFormatTime(buildTimeMs);
+ mediaTimeHistory = new ArrayList<>(this.mediaTimeHistory);
+ if (keepHistory && currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING) {
+ mediaTimeHistory.add(guessMediaTimeBasedOnElapsedRealtime(buildTimeMs));
+ }
+ }
+ boolean isJoinTimeInvalid = this.isJoinTimeInvalid || !hasBeenReady;
+ long validJoinTimeMs =
+ isJoinTimeInvalid
+ ? C.TIME_UNSET
+ : playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND];
+ boolean hasBackgroundJoin =
+ playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND] > 0;
+ List<Pair<EventTime, @NullableType Format>> videoHistory =
+ isFinal ? videoFormatHistory : new ArrayList<>(videoFormatHistory);
+ List<Pair<EventTime, @NullableType Format>> audioHistory =
+ isFinal ? audioFormatHistory : new ArrayList<>(audioFormatHistory);
+ return new PlaybackStats(
+ /* playbackCount= */ 1,
+ playbackStateDurationsMs,
+ isFinal ? playbackStateHistory : new ArrayList<>(playbackStateHistory),
+ mediaTimeHistory,
+ firstReportedTimeMs,
+ /* foregroundPlaybackCount= */ isForeground ? 1 : 0,
+ /* abandonedBeforeReadyCount= */ hasBeenReady ? 0 : 1,
+ /* endedCount= */ hasEnded ? 1 : 0,
+ /* backgroundJoiningCount= */ hasBackgroundJoin ? 1 : 0,
+ validJoinTimeMs,
+ /* validJoinTimeCount= */ isJoinTimeInvalid ? 0 : 1,
+ pauseCount,
+ pauseBufferCount,
+ seekCount,
+ rebufferCount,
+ maxRebufferTimeMs,
+ /* adPlaybackCount= */ isAd ? 1 : 0,
+ videoHistory,
+ audioHistory,
+ videoFormatHeightTimeMs,
+ videoFormatHeightTimeProduct,
+ videoFormatBitrateTimeMs,
+ videoFormatBitrateTimeProduct,
+ audioFormatTimeMs,
+ audioFormatBitrateTimeProduct,
+ /* initialVideoFormatHeightCount= */ initialVideoFormatHeight == C.LENGTH_UNSET ? 0 : 1,
+ /* initialVideoFormatBitrateCount= */ initialVideoFormatBitrate == C.LENGTH_UNSET ? 0 : 1,
+ initialVideoFormatHeight,
+ initialVideoFormatBitrate,
+ /* initialAudioFormatBitrateCount= */ initialAudioFormatBitrate == C.LENGTH_UNSET ? 0 : 1,
+ initialAudioFormatBitrate,
+ bandwidthTimeMs,
+ bandwidthBytes,
+ droppedFrames,
+ audioUnderruns,
+ /* fatalErrorPlaybackCount= */ fatalErrorCount > 0 ? 1 : 0,
+ fatalErrorCount,
+ nonFatalErrorCount,
+ fatalErrorHistory,
+ nonFatalErrorHistory);
+ }
+
+ private void maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlayback) {
+ @PlaybackState int newPlaybackState = resolveNewPlaybackState();
+ if (newPlaybackState == currentPlaybackState) {
+ return;
+ }
+ Assertions.checkArgument(eventTime.realtimeMs >= currentPlaybackStateStartTimeMs);
+
+ long stateDurationMs = eventTime.realtimeMs - currentPlaybackStateStartTimeMs;
+ playbackStateDurationsMs[currentPlaybackState] += stateDurationMs;
+ if (firstReportedTimeMs == C.TIME_UNSET) {
+ firstReportedTimeMs = eventTime.realtimeMs;
+ }
+ isJoinTimeInvalid |= isInvalidJoinTransition(currentPlaybackState, newPlaybackState);
+ hasBeenReady |= isReadyState(newPlaybackState);
+ hasEnded |= newPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED;
+ if (!isPausedState(currentPlaybackState) && isPausedState(newPlaybackState)) {
+ pauseCount++;
+ }
+ if (newPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING) {
+ seekCount++;
+ }
+ if (!isRebufferingState(currentPlaybackState) && isRebufferingState(newPlaybackState)) {
+ rebufferCount++;
+ lastRebufferStartTimeMs = eventTime.realtimeMs;
+ }
+ if (isRebufferingState(currentPlaybackState)
+ && currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING
+ && newPlaybackState == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING) {
+ pauseBufferCount++;
+ }
+
+ maybeUpdateMediaTimeHistory(
+ eventTime.realtimeMs,
+ /* mediaTimeMs= */ belongsToPlayback ? eventTime.eventPlaybackPositionMs : C.TIME_UNSET);
+ maybeUpdateMaxRebufferTimeMs(eventTime.realtimeMs);
+ maybeRecordVideoFormatTime(eventTime.realtimeMs);
+ maybeRecordAudioFormatTime(eventTime.realtimeMs);
+
+ currentPlaybackState = newPlaybackState;
+ currentPlaybackStateStartTimeMs = eventTime.realtimeMs;
+ if (keepHistory) {
+ playbackStateHistory.add(Pair.create(eventTime, currentPlaybackState));
+ }
+ }
+
+ private @PlaybackState int resolveNewPlaybackState() {
+ if (isFinished) {
+ // Keep VIDEO_STATE_ENDED if playback naturally ended (or progressed to next item).
+ return currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED
+ ? PlaybackStats.PLAYBACK_STATE_ENDED
+ : PlaybackStats.PLAYBACK_STATE_ABANDONED;
+ } else if (isSeeking) {
+ // Seeking takes precedence over errors such that we report a seek while in error state.
+ return PlaybackStats.PLAYBACK_STATE_SEEKING;
+ } else if (hasFatalError) {
+ return PlaybackStats.PLAYBACK_STATE_FAILED;
+ } else if (!isForeground) {
+ // Before the playback becomes foreground, only report background joining and not started.
+ return startedLoading
+ ? PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND
+ : PlaybackStats.PLAYBACK_STATE_NOT_STARTED;
+ } else if (isInterruptedByAd) {
+ return PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD;
+ } else if (playerPlaybackState == Player.STATE_ENDED) {
+ return PlaybackStats.PLAYBACK_STATE_ENDED;
+ } else if (playerPlaybackState == Player.STATE_BUFFERING) {
+ if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_NOT_STARTED
+ || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND
+ || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND
+ || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) {
+ return PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND;
+ }
+ if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING
+ || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING) {
+ return PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING;
+ }
+ if (!playWhenReady) {
+ return PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING;
+ }
+ return isSuppressed
+ ? PlaybackStats.PLAYBACK_STATE_SUPPRESSED_BUFFERING
+ : PlaybackStats.PLAYBACK_STATE_BUFFERING;
+ } else if (playerPlaybackState == Player.STATE_READY) {
+ if (!playWhenReady) {
+ return PlaybackStats.PLAYBACK_STATE_PAUSED;
+ }
+ return isSuppressed
+ ? PlaybackStats.PLAYBACK_STATE_SUPPRESSED
+ : PlaybackStats.PLAYBACK_STATE_PLAYING;
+ } else if (playerPlaybackState == Player.STATE_IDLE
+ && currentPlaybackState != PlaybackStats.PLAYBACK_STATE_NOT_STARTED) {
+ // This case only applies for calls to player.stop(). All other IDLE cases are handled by
+ // !isForeground, hasFatalError or isSuspended. NOT_STARTED is deliberately ignored.
+ return PlaybackStats.PLAYBACK_STATE_STOPPED;
+ }
+ return currentPlaybackState;
+ }
+
+ private void maybeUpdateMaxRebufferTimeMs(long nowMs) {
+ if (isRebufferingState(currentPlaybackState)) {
+ long rebufferDurationMs = nowMs - lastRebufferStartTimeMs;
+ if (maxRebufferTimeMs == C.TIME_UNSET || rebufferDurationMs > maxRebufferTimeMs) {
+ maxRebufferTimeMs = rebufferDurationMs;
+ }
+ }
+ }
+
+ private void maybeUpdateMediaTimeHistory(long realtimeMs, long mediaTimeMs) {
+ if (!keepHistory) {
+ return;
+ }
+ if (currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PLAYING) {
+ if (mediaTimeMs == C.TIME_UNSET) {
+ return;
+ }
+ if (!mediaTimeHistory.isEmpty()) {
+ long previousMediaTimeMs = mediaTimeHistory.get(mediaTimeHistory.size() - 1)[1];
+ if (previousMediaTimeMs != mediaTimeMs) {
+ mediaTimeHistory.add(new long[] {realtimeMs, previousMediaTimeMs});
+ }
+ }
+ }
+ mediaTimeHistory.add(
+ mediaTimeMs == C.TIME_UNSET
+ ? guessMediaTimeBasedOnElapsedRealtime(realtimeMs)
+ : new long[] {realtimeMs, mediaTimeMs});
+ }
+
+ private long[] guessMediaTimeBasedOnElapsedRealtime(long realtimeMs) {
+ long[] previousKnownMediaTimeHistory = mediaTimeHistory.get(mediaTimeHistory.size() - 1);
+ long previousRealtimeMs = previousKnownMediaTimeHistory[0];
+ long previousMediaTimeMs = previousKnownMediaTimeHistory[1];
+ long elapsedMediaTimeEstimateMs =
+ (long) ((realtimeMs - previousRealtimeMs) * currentPlaybackSpeed);
+ long mediaTimeEstimateMs = previousMediaTimeMs + elapsedMediaTimeEstimateMs;
+ return new long[] {realtimeMs, mediaTimeEstimateMs};
+ }
+
+ private void maybeUpdateVideoFormat(EventTime eventTime, @Nullable Format newFormat) {
+ if (Util.areEqual(currentVideoFormat, newFormat)) {
+ return;
+ }
+ maybeRecordVideoFormatTime(eventTime.realtimeMs);
+ if (newFormat != null) {
+ if (initialVideoFormatHeight == C.LENGTH_UNSET && newFormat.height != Format.NO_VALUE) {
+ initialVideoFormatHeight = newFormat.height;
+ }
+ if (initialVideoFormatBitrate == C.LENGTH_UNSET && newFormat.bitrate != Format.NO_VALUE) {
+ initialVideoFormatBitrate = newFormat.bitrate;
+ }
+ }
+ currentVideoFormat = newFormat;
+ if (keepHistory) {
+ videoFormatHistory.add(Pair.create(eventTime, currentVideoFormat));
+ }
+ }
+
+ private void maybeUpdateAudioFormat(EventTime eventTime, @Nullable Format newFormat) {
+ if (Util.areEqual(currentAudioFormat, newFormat)) {
+ return;
+ }
+ maybeRecordAudioFormatTime(eventTime.realtimeMs);
+ if (newFormat != null
+ && initialAudioFormatBitrate == C.LENGTH_UNSET
+ && newFormat.bitrate != Format.NO_VALUE) {
+ initialAudioFormatBitrate = newFormat.bitrate;
+ }
+ currentAudioFormat = newFormat;
+ if (keepHistory) {
+ audioFormatHistory.add(Pair.create(eventTime, currentAudioFormat));
+ }
+ }
+
+ private void maybeRecordVideoFormatTime(long nowMs) {
+ if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING
+ && currentVideoFormat != null) {
+ long mediaDurationMs = (long) ((nowMs - lastVideoFormatStartTimeMs) * currentPlaybackSpeed);
+ if (currentVideoFormat.height != Format.NO_VALUE) {
+ videoFormatHeightTimeMs += mediaDurationMs;
+ videoFormatHeightTimeProduct += mediaDurationMs * currentVideoFormat.height;
+ }
+ if (currentVideoFormat.bitrate != Format.NO_VALUE) {
+ videoFormatBitrateTimeMs += mediaDurationMs;
+ videoFormatBitrateTimeProduct += mediaDurationMs * currentVideoFormat.bitrate;
+ }
+ }
+ lastVideoFormatStartTimeMs = nowMs;
+ }
+
+ private void maybeRecordAudioFormatTime(long nowMs) {
+ if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING
+ && currentAudioFormat != null
+ && currentAudioFormat.bitrate != Format.NO_VALUE) {
+ long mediaDurationMs = (long) ((nowMs - lastAudioFormatStartTimeMs) * currentPlaybackSpeed);
+ audioFormatTimeMs += mediaDurationMs;
+ audioFormatBitrateTimeProduct += mediaDurationMs * currentAudioFormat.bitrate;
+ }
+ lastAudioFormatStartTimeMs = nowMs;
+ }
+
+ private static boolean isReadyState(@PlaybackState int state) {
+ return state == PlaybackStats.PLAYBACK_STATE_PLAYING
+ || state == PlaybackStats.PLAYBACK_STATE_PAUSED
+ || state == PlaybackStats.PLAYBACK_STATE_SUPPRESSED;
+ }
+
+ private static boolean isPausedState(@PlaybackState int state) {
+ return state == PlaybackStats.PLAYBACK_STATE_PAUSED
+ || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING;
+ }
+
+ private static boolean isRebufferingState(@PlaybackState int state) {
+ return state == PlaybackStats.PLAYBACK_STATE_BUFFERING
+ || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING
+ || state == PlaybackStats.PLAYBACK_STATE_SUPPRESSED_BUFFERING;
+ }
+
+ private static boolean isInvalidJoinTransition(
+ @PlaybackState int oldState, @PlaybackState int newState) {
+ if (oldState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND
+ && oldState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND
+ && oldState != PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) {
+ return false;
+ }
+ return newState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND
+ && newState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND
+ && newState != PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD
+ && newState != PlaybackStats.PLAYBACK_STATE_PLAYING
+ && newState != PlaybackStats.PLAYBACK_STATE_PAUSED
+ && newState != PlaybackStats.PLAYBACK_STATE_SUPPRESSED
+ && newState != PlaybackStats.PLAYBACK_STATE_ENDED;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/package-info.java
new file mode 100644
index 0000000000..08556b00b0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package org.mozilla.thirdparty.com.google.android.exoplayer2.analytics;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi;