diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java new file mode 100644 index 0000000000..3a937a832d --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -0,0 +1,782 @@ +/* + * 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 com.google.android.exoplayer2.analytics; + +import android.net.NetworkInfo; +import android.support.annotation.Nullable; +import android.view.Surface; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; +import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataOutput; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.video.VideoRendererEventListener; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * 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, + AdsMediaSource.EventListener, + DefaultDrmSessionEventListener { + + /** Factory for an analytics collector. */ + public static class Factory { + + /** + * Creates an analytics collector for the specified player. + * + * @param player The {@link Player} for which data will be collected. + * @param clock A {@link Clock} used to generate timestamps. + * @return An analytics collector. + */ + public AnalyticsCollector createAnalyticsCollector(Player player, Clock clock) { + return new AnalyticsCollector(player, clock); + } + } + + private final CopyOnWriteArraySet listeners; + private final Player player; + private final Clock clock; + private final Period period; + private final Window window; + private final MediaPeriodQueueTracker mediaPeriodQueueTracker; + + /** + * Creates an analytics collector for the specified player. + * + * @param player The {@link Player} for which data will be collected. + * @param clock A {@link Clock} used to generate timestamps. + */ + protected AnalyticsCollector(Player player, Clock clock) { + this.player = Assertions.checkNotNull(player); + this.clock = Assertions.checkNotNull(clock); + listeners = new CopyOnWriteArraySet<>(); + mediaPeriodQueueTracker = new MediaPeriodQueueTracker(); + period = new Period(); + 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); + } + + // 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); + } + } + } + + /** + * Notify analytics collector that the viewport size changed. + * + * @param width The new width of the viewport in device-independent pixels (dp). + * @param height The new height of the viewport in device-independent pixels (dp). + */ + public final void notifyViewportSizeChanged(int width, int height) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onViewportSizeChange(eventTime, width, height); + } + } + + /** + * Notify analytics collector that the network type or connectivity changed. + * + * @param networkInfo The new network info, or null if no network connection exists. + */ + public final void notifyNetworkTypeChanged(@Nullable NetworkInfo networkInfo) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onNetworkTypeChanged(eventTime, networkInfo); + } + } + + // 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 onAudioSessionId(int audioSessionId) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onAudioSessionId(eventTime, audioSessionId); + } + } + + @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); + } + } + + // 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 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 final void onRenderedFirstFrame(Surface surface) { + EventTime eventTime = generateReadingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onRenderedFirstFrame(eventTime, surface); + } + } + + @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); + } + } + + // MediaSourceEventListener implementation. + + @Override + public final void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { + mediaPeriodQueueTracker.onMediaPeriodCreated(mediaPeriodId); + EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onMediaPeriodCreated(eventTime); + } + } + + @Override + public final void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { + mediaPeriodQueueTracker.onMediaPeriodReleased(mediaPeriodId); + EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onMediaPeriodReleased(eventTime); + } + } + + @Override + public final void onLoadStarted( + int windowIndex, + @Nullable MediaPeriodId mediaPeriodId, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData) { + EventTime eventTime = generateEventTime(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 = generateEventTime(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 = generateEventTime(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 = generateEventTime(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 = generateEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onReadingStarted(eventTime); + } + } + + @Override + public final void onUpstreamDiscarded( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { + EventTime eventTime = generateEventTime(windowIndex, mediaPeriodId); + for (AnalyticsListener listener : listeners) { + listener.onUpstreamDiscarded(eventTime, mediaLoadData); + } + } + + @Override + public final void onDownstreamFormatChanged( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { + EventTime eventTime = generateEventTime(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, Object manifest, @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, int playbackState) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState); + } + } + + @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 = generatePlayingMediaPeriodEventTime(); + 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 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); + } + } + + // AdsMediaSource.EventListener implementation. + + @Override + public final void onAdLoadError(IOException error) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onAdLoadError(eventTime, error); + } + } + + @Override + public final void onInternalAdLoadError(RuntimeException error) { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onInternalAdLoadError(eventTime, error); + } + } + + @Override + public final void onAdClicked() { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onAdClicked(eventTime); + } + } + + @Override + public final void onAdTapped() { + EventTime eventTime = generatePlayingMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onAdTapped(eventTime); + } + } + + // Internal methods. + + /** Returns read-only set of registered listeners. */ + protected Set getListeners() { + return Collections.unmodifiableSet(listeners); + } + + /** Returns a new {@link EventTime} for the specified window index and media period id. */ + protected EventTime generateEventTime(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { + long realtimeMs = clock.elapsedRealtime(); + Timeline timeline = player.getCurrentTimeline(); + long eventPositionMs; + if (windowIndex == player.getCurrentWindowIndex()) { + if (mediaPeriodId != null && mediaPeriodId.isAd()) { + // This event is for an ad in the currently playing window. + eventPositionMs = + player.getCurrentAdGroupIndex() == mediaPeriodId.adGroupIndex + && player.getCurrentAdIndexInAdGroup() == mediaPeriodId.adIndexInAdGroup + ? player.getCurrentPosition() + : 0 /* Assume start position of 0 for a future ad. */; + } else { + // This event is for content in the currently playing window. + eventPositionMs = player.getContentPosition(); + } + } else if (timeline.isEmpty() || (mediaPeriodId != null && mediaPeriodId.isAd())) { + // This event is for an unknown future window or for an ad in a future window. + // Assume start position of zero. + eventPositionMs = 0; + } else { + // This event is for content in a future window. Assume default start position. + eventPositionMs = timeline.getWindow(windowIndex, window).getDefaultPositionMs(); + } + // TODO(b/30792113): implement this properly (player.getTotalBufferedDuration()). + long bufferedDurationMs = player.getBufferedPosition() - player.getContentPosition(); + return new EventTime( + realtimeMs, + timeline, + windowIndex, + mediaPeriodId, + eventPositionMs, + player.getCurrentPosition(), + bufferedDurationMs); + } + + private EventTime generateEventTime(@Nullable MediaPeriodId mediaPeriodId) { + Timeline timeline = player.getCurrentTimeline(); + if (mediaPeriodId == null) { + mediaPeriodId = mediaPeriodQueueTracker.tryResolveWindowIndex(player.getCurrentWindowIndex()); + } + int windowIndex = + mediaPeriodId == null || timeline.isEmpty() + ? player.getCurrentWindowIndex() + : timeline.getPeriod(mediaPeriodId.periodIndex, period).windowIndex; + return generateEventTime(windowIndex, 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()); + } + + /** Keeps track of the active media periods and currently playing and reading media period. */ + private static final class MediaPeriodQueueTracker { + + private final ArrayList activeMediaPeriods; + private final Period period; + + private MediaPeriodId lastReportedPlayingMediaPeriod; + private MediaPeriodId readingMediaPeriod; + private Timeline timeline; + private boolean isSeeking; + + public MediaPeriodQueueTracker() { + activeMediaPeriods = new ArrayList<>(); + period = new Period(); + } + + /** + * Returns the {@link MediaPeriodId} 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. + */ + public @Nullable MediaPeriodId getPlayingMediaPeriod() { + return activeMediaPeriods.isEmpty() || timeline.isEmpty() || isSeeking + ? null + : activeMediaPeriods.get(0); + } + + /** + * Returns the {@link MediaPeriodId} 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. + */ + public @Nullable MediaPeriodId getLastReportedPlayingMediaPeriod() { + return lastReportedPlayingMediaPeriod; + } + + /** + * Returns the {@link MediaPeriodId} of the media period currently being read by the player. May + * be null, if the player is not reading a media period. + */ + public @Nullable MediaPeriodId getReadingMediaPeriod() { + return readingMediaPeriod; + } + + /** + * Returns the {@link MediaPeriodId} of the media period at the end of the queue which is + * currently loading or will be the next one loading. May be null, if no media period is active + * yet. + */ + public @Nullable MediaPeriodId getLoadingMediaPeriod() { + return activeMediaPeriods.isEmpty() + ? null + : activeMediaPeriods.get(activeMediaPeriods.size() - 1); + } + + /** Returns whether the player is currently seeking. */ + public boolean isSeeking() { + return isSeeking; + } + + /** + * Tries to find an existing media period from the specified window index. Only returns a + * non-null media period id if there is a unique, unambiguous match. + */ + public @Nullable MediaPeriodId tryResolveWindowIndex(int windowIndex) { + MediaPeriodId match = null; + if (timeline != null && !timeline.isEmpty()) { + for (int i = 0; i < activeMediaPeriods.size(); i++) { + MediaPeriodId mediaPeriodId = activeMediaPeriods.get(i); + if (timeline.getPeriod(mediaPeriodId.periodIndex, period).windowIndex == windowIndex) { + if (match != null) { + // Ambiguous match. + return null; + } + match = mediaPeriodId; + } + } + } + return match; + } + + /** Updates the queue with a reported position discontinuity . */ + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + updateLastReportedPlayingMediaPeriod(); + } + + /** Updates the queue with a reported timeline change. */ + public void onTimelineChanged(Timeline timeline) { + for (int i = 0; i < activeMediaPeriods.size(); i++) { + activeMediaPeriods.set( + i, updateMediaPeriodIdToNewTimeline(activeMediaPeriods.get(i), timeline)); + } + if (readingMediaPeriod != null) { + readingMediaPeriod = updateMediaPeriodIdToNewTimeline(readingMediaPeriod, timeline); + } + this.timeline = timeline; + updateLastReportedPlayingMediaPeriod(); + } + + /** 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; + updateLastReportedPlayingMediaPeriod(); + } + + /** Updates the queue with a newly created media period. */ + public void onMediaPeriodCreated(MediaPeriodId mediaPeriodId) { + activeMediaPeriods.add(mediaPeriodId); + if (activeMediaPeriods.size() == 1 && !timeline.isEmpty()) { + updateLastReportedPlayingMediaPeriod(); + } + } + + /** Updates the queue with a released media period. */ + public void onMediaPeriodReleased(MediaPeriodId mediaPeriodId) { + activeMediaPeriods.remove(mediaPeriodId); + if (mediaPeriodId.equals(readingMediaPeriod)) { + readingMediaPeriod = activeMediaPeriods.isEmpty() ? null : activeMediaPeriods.get(0); + } + } + + /** Update the queue with a change in the reading media period. */ + public void onReadingStarted(MediaPeriodId mediaPeriodId) { + readingMediaPeriod = mediaPeriodId; + } + + private void updateLastReportedPlayingMediaPeriod() { + lastReportedPlayingMediaPeriod = + activeMediaPeriods.isEmpty() ? null : activeMediaPeriods.get(0); + } + + private MediaPeriodId updateMediaPeriodIdToNewTimeline( + MediaPeriodId mediaPeriodId, Timeline newTimeline) { + if (newTimeline.isEmpty()) { + return mediaPeriodId; + } + Object uid = timeline.getPeriod(mediaPeriodId.periodIndex, period, /* setIds= */ true).uid; + int newIndex = newTimeline.getIndexOfPeriod(uid); + return newIndex == C.INDEX_UNSET + ? mediaPeriodId + : mediaPeriodId.copyWithPeriodIndex(newIndex); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java new file mode 100644 index 0000000000..30357b08ef --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -0,0 +1,497 @@ +/* + * 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 com.google.android.exoplayer2.analytics; + +import android.net.NetworkInfo; +import android.support.annotation.Nullable; +import android.view.Surface; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.TimelineChangeReason; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.audio.AudioSink; +import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import java.io.IOException; + +/** + * A listener for analytics events. + * + *

All events are recorded with an {@link EventTime} specifying the elapsed real time and media + * time at the time of the event. + */ +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 {@code 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. + */ + public final @Nullable 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 ({@code timeline.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 {@code 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 ({@code + * timeline.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 One of the {@link Player}.STATE constants. + */ + void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int playbackState); + + /** + * Called when the timeline changed. + * + * @param eventTime The event time. + * @param reason The reason for the timeline change. + */ + 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. + */ + void onPositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason); + + /** + * Called when a seek operation started. + * + * @param eventTime The event time. + */ + void onSeekStarted(EventTime eventTime); + + /** + * Called when a seek operation was processed. + * + * @param eventTime The event time. + */ + void onSeekProcessed(EventTime eventTime); + + /** + * Called when the playback parameters changed. + * + * @param eventTime The event time. + * @param playbackParameters The new playback parameters. + */ + void onPlaybackParametersChanged(EventTime eventTime, PlaybackParameters playbackParameters); + + /** + * Called when the repeat mode changed. + * + * @param eventTime The event time. + * @param repeatMode The new repeat mode. + */ + 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. + */ + 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. + */ + void onLoadingChanged(EventTime eventTime, boolean isLoading); + + /** + * Called when a fatal player error occurred. + * + * @param eventTime The event time. + * @param error The error. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData); + + /** + * Called when a media source created a media period. + * + * @param eventTime The event time. + */ + void onMediaPeriodCreated(EventTime eventTime); + + /** + * Called when a media source released a media period. + * + * @param eventTime The event time. + */ + void onMediaPeriodReleased(EventTime eventTime); + + /** + * Called when the player started reading a media period. + * + * @param eventTime The event time. + */ + 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. + */ + void onBandwidthEstimate( + EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate); + + /** + * Called when the viewport size of the output surface changed. + * + * @param eventTime The event time. + * @param width The width of the viewport in device-independent pixels (dp). + * @param height The height of the viewport in device-independent pixels (dp). + */ + void onViewportSizeChange(EventTime eventTime, int width, int height); + + /** + * Called when the type of the network connection changed. + * + * @param eventTime The event time. + * @param networkInfo The network info for the current connection, or null if disconnected. + */ + void onNetworkTypeChanged(EventTime eventTime, @Nullable NetworkInfo networkInfo); + + /** + * Called when there is {@link Metadata} associated with the current playback time. + * + * @param eventTime The event time. + * @param metadata The metadata. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + void onAudioSessionId(EventTime eventTime, int audioSessionId); + + /** + * 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. + */ + 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. + */ + 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. + */ + 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}. + */ + void onRenderedFirstFrame(EventTime eventTime, Surface surface); + + /** + * Called if there was an error loading one or more ads. The ads loader will skip the problematic + * ad(s). + * + * @param eventTime The event time. + * @param error The error. + */ + void onAdLoadError(EventTime eventTime, IOException error); + + /** + * Called when an unexpected internal error is encountered while loading ads. The ads loader will + * skip all remaining ads, as the error is not recoverable. + * + * @param eventTime The event time. + * @param error The error. + */ + void onInternalAdLoadError(EventTime eventTime, RuntimeException error); + + /** + * Called when the user clicks through an ad (for example, following a 'learn more' link). + * + * @param eventTime The event time. + */ + void onAdClicked(EventTime eventTime); + + /** + * Called when the user taps a non-clickthrough part of an ad. + * + * @param eventTime The event time. + */ + void onAdTapped(EventTime eventTime); + + /** + * Called each time drm keys are loaded. + * + * @param eventTime The event time. + */ + 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. + */ + void onDrmSessionManagerError(EventTime eventTime, Exception error); + + /** + * Called each time offline drm keys are restored. + * + * @param eventTime The event time. + */ + void onDrmKeysRestored(EventTime eventTime); + + /** + * Called each time offline drm keys are removed. + * + * @param eventTime The event time. + */ + void onDrmKeysRemoved(EventTime eventTime); +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java new file mode 100644 index 0000000000..a53a85ed71 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultAnalyticsListener.java @@ -0,0 +1,178 @@ +/* + * 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 com.google.android.exoplayer2.analytics; + +import android.net.NetworkInfo; +import android.view.Surface; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import java.io.IOException; + +/** + * {@link AnalyticsListener} allowing selective overrides. All methods are implemented as no-ops. + */ +public abstract class DefaultAnalyticsListener implements AnalyticsListener { + + @Override + public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int playbackState) {} + + @Override + public void onTimelineChanged(EventTime eventTime, int reason) {} + + @Override + public void onPositionDiscontinuity(EventTime eventTime, int reason) {} + + @Override + public void onSeekStarted(EventTime eventTime) {} + + @Override + public void onSeekProcessed(EventTime eventTime) {} + + @Override + public void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) {} + + @Override + public void onRepeatModeChanged(EventTime eventTime, int repeatMode) {} + + @Override + public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {} + + @Override + public void onLoadingChanged(EventTime eventTime, boolean isLoading) {} + + @Override + public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {} + + @Override + public void onTracksChanged( + EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {} + + @Override + public void onLoadStarted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {} + + @Override + public void onLoadCompleted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {} + + @Override + public void onLoadCanceled( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {} + + @Override + public void onLoadError( + EventTime eventTime, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) {} + + @Override + public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {} + + @Override + public void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) {} + + @Override + public void onMediaPeriodCreated(EventTime eventTime) {} + + @Override + public void onMediaPeriodReleased(EventTime eventTime) {} + + @Override + public void onReadingStarted(EventTime eventTime) {} + + @Override + public void onBandwidthEstimate( + EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {} + + @Override + public void onViewportSizeChange(EventTime eventTime, int width, int height) {} + + @Override + public void onNetworkTypeChanged(EventTime eventTime, NetworkInfo networkInfo) {} + + @Override + public void onMetadata(EventTime eventTime, Metadata metadata) {} + + @Override + public void onDecoderEnabled( + EventTime eventTime, int trackType, DecoderCounters decoderCounters) {} + + @Override + public void onDecoderInitialized( + EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) {} + + @Override + public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {} + + @Override + public void onDecoderDisabled( + EventTime eventTime, int trackType, DecoderCounters decoderCounters) {} + + @Override + public void onAudioSessionId(EventTime eventTime, int audioSessionId) {} + + @Override + public void onAudioUnderrun( + EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {} + + @Override + public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {} + + @Override + public void onVideoSizeChanged( + EventTime eventTime, + int width, + int height, + int unappliedRotationDegrees, + float pixelWidthHeightRatio) {} + + @Override + public void onRenderedFirstFrame(EventTime eventTime, Surface surface) {} + + @Override + public void onAdLoadError(EventTime eventTime, IOException error) {} + + @Override + public void onInternalAdLoadError(EventTime eventTime, RuntimeException error) {} + + @Override + public void onAdClicked(EventTime eventTime) {} + + @Override + public void onAdTapped(EventTime eventTime) {} + + @Override + public void onDrmKeysLoaded(EventTime eventTime) {} + + @Override + public void onDrmSessionManagerError(EventTime eventTime, Exception error) {} + + @Override + public void onDrmKeysRestored(EventTime eventTime) {} + + @Override + public void onDrmKeysRemoved(EventTime eventTime) {} +}