Commit f25c7a85 by tonihei Committed by Oliver Woodman

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
parent 68627717
/*
* 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);
}
}
}
/*
* 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);
}
/*
* 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) {}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment