Add analytics listener interface and default data collector.

The data collector keeps track of active media periods to assign each event to
the correct media period and/or window. This information, together with other
information like playback position and buffered duration, is then forwarded
with the event to all registered listeners.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=191726408
This commit is contained in:
tonihei 2018-04-05 04:54:11 -07:00 committed by Oliver Woodman
parent 6862771725
commit f25c7a859a
3 changed files with 1457 additions and 0 deletions

View File

@ -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<AnalyticsListener> 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<AnalyticsListener> 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<MediaPeriodId> 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);
}
}
}

View File

@ -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.
*
* <p>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);
}

View File

@ -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) {}
}